SHA256
1554 lines
40 KiB
Markdown
1554 lines
40 KiB
Markdown
# SHiNE Archive Protocol v1.0
|
||
|
||
## Полная спецификация серверной архивации SHiNE-блоков в Arweave и фиксации головы архива в Solana
|
||
|
||
**Статус:** согласованная спецификация v1.0
|
||
**Язык:** русский
|
||
**Magic:** `SHINE-ARCHIVE`
|
||
**Версия протокола:** `1.0`
|
||
**Endian:** big-endian для всех фиксированных целых чисел
|
||
|
||
---
|
||
|
||
# 1. Назначение
|
||
|
||
Этот документ описывает механизм, по которому SHiNE-сервер периодически:
|
||
|
||
1. синхронизирует обычные SHiNE-блоки существующим механизмом;
|
||
2. определяет новые локальные блоки, ещё не попавшие в архив;
|
||
3. фиксирует неизменяемую дельту;
|
||
4. группирует все новые записи одной `blockchain_name` в один блок пользователя внутри большого архивного блока;
|
||
5. добавляет к каждому такому пользовательскому блоку одну ссылку на предыдущий пользовательский блок этой же `blockchain_name`;
|
||
6. формирует один большой бинарный блок `SHINE-ARCHIVE`;
|
||
7. загружает его в Arweave;
|
||
8. ждёт закрепления/подтверждения Arweave-транзакции;
|
||
9. обычным обновлением своего существующего User PDA записывает новый archive head в Solana;
|
||
10. только после Solana `finalized` переводит локальные курсоры на новую точку.
|
||
|
||
На первом этапе предполагается один архивный Raspberry-сервер, но формат изначально не должен запрещать появление других архиваторов.
|
||
|
||
---
|
||
|
||
# 2. Основные понятия
|
||
|
||
## 2.1. Исходный SHiNE-блок / запись
|
||
|
||
Это существующие в серверной БД оригинальные `block_bytes`, относящиеся к конкретной `blockchain_name`.
|
||
|
||
Они уже содержат пользовательские данные и существующую криптографию SHiNE.
|
||
|
||
Архиватор:
|
||
|
||
- не изменяет их;
|
||
- не переподписывает их;
|
||
- не переводит их в Base64;
|
||
- не пересобирает их содержимое.
|
||
|
||
## 2.2. `blockchain_name`
|
||
|
||
В архивном формате основной идентификатор пользовательской цепочки — именно `blockchain_name`.
|
||
|
||
Отдельно хранить `login` для пользовательского блока не требуется.
|
||
|
||
`blockchain_name` уже однозначно идентифицирует конкретную пользовательскую blockchain и логически включает пользовательскую идентичность + номер/вариант цепочки.
|
||
|
||
## 2.3. Большой архивный блок
|
||
|
||
Один файл/объект `SHINE-ARCHIVE`, который сервер формирует за один цикл архивной публикации.
|
||
|
||
Он содержит:
|
||
|
||
- header;
|
||
- список предыдущих больших архивных блоков;
|
||
- по одному `UserBlockchainChunk` для каждой `blockchain_name`, у которой есть новые записи;
|
||
- footer;
|
||
- SHA-256 большого блока;
|
||
- подпись сервера, который этот большой блок сформировал и закрыл.
|
||
|
||
Один большой архивный блок после публикации соответствует одной Arweave-транзакции.
|
||
|
||
## 2.4. `UserBlockchainChunk`
|
||
|
||
В одном большом архивном блоке для одной `blockchain_name` существует максимум один такой chunk.
|
||
|
||
Если за текущую дельту у пользователя/цепочки накопилось 1, 3, 5 или 20 записей, все они помещаются внутрь одного `UserBlockchainChunk`.
|
||
|
||
Навигационная ссылка одна на весь chunk.
|
||
|
||
---
|
||
|
||
# 3. Главная модель пользовательской истории
|
||
|
||
Допустим `alice-001` имела записи:
|
||
|
||
```text
|
||
BigBlock 70:
|
||
AliceChunk = 2 записи
|
||
|
||
BigBlock 81:
|
||
AliceChunk = 5 записей
|
||
|
||
BigBlock 95:
|
||
AliceChunk = 3 записи
|
||
|
||
BigBlock 100:
|
||
AliceChunk = 4 записи
|
||
```
|
||
|
||
Тогда история `alice-001` выглядит:
|
||
|
||
```text
|
||
BigBlock 100
|
||
AliceChunk
|
||
4 записи
|
||
|
|
||
+--> PreviousBlockchainChunkRef
|
||
|
|
||
v
|
||
BigBlock 95
|
||
AliceChunk
|
||
3 записи
|
||
|
|
||
+--> PreviousBlockchainChunkRef
|
||
|
|
||
v
|
||
BigBlock 81
|
||
AliceChunk
|
||
5 записей
|
||
|
|
||
v
|
||
BigBlock 70
|
||
```
|
||
|
||
Навигация идёт пачками записей одной blockchain, а не по одной записи за раз.
|
||
|
||
---
|
||
|
||
# 4. Сервер сам добавляет навигацию
|
||
|
||
Пользователь не обязан знать:
|
||
|
||
- в какой большой архивный блок попадут его записи;
|
||
- какой будет Arweave TX ID;
|
||
- какие будут offsets;
|
||
- где находится предыдущий chunk.
|
||
|
||
Все эти данные сервер добавляет при формировании большого блока.
|
||
|
||
Исходные подписанные пользовательские `block_bytes` остаются неизменными.
|
||
|
||
`PreviousBlockchainChunkRef` является внешней серверной архивной метаинформацией и защищается hash + подписью всего большого архивного блока.
|
||
|
||
---
|
||
|
||
# 5. Необходимые ключи архивного сервера
|
||
|
||
Архивный Raspberry/server хранит:
|
||
|
||
## 5.1. SHiNE root private key
|
||
|
||
Используется:
|
||
|
||
- для подписания новой версии User PDA при обычном `update_user_pda`;
|
||
- в v1.0 — для подписи закрытого большого архивного блока.
|
||
|
||
## 5.2. SHiNE client private key
|
||
|
||
Используется как Solana transaction signer / fee payer в соответствии с текущей логикой `shine_users`.
|
||
|
||
## 5.3. Arweave JWK / private wallet key
|
||
|
||
Используется для загрузки и оплаты Arweave-транзакции.
|
||
|
||
## 5.4. Blockchain private key
|
||
|
||
Для архивной публикации не требуется.
|
||
|
||
Архиватор не создаёт новые пользовательские blockchain-блоки.
|
||
|
||
---
|
||
|
||
# 6. Периодичность
|
||
|
||
Частота архивной публикации конфигурируется отдельно от существующей межсерверной синхронизации.
|
||
|
||
Пример:
|
||
|
||
```properties
|
||
archive.publish.enabled=true
|
||
archive.publish.intervalMinutes=720
|
||
archive.publish.initialDelayMinutes=15
|
||
```
|
||
|
||
`720` минут = раз в 12 часов.
|
||
|
||
---
|
||
|
||
# 7. Состояние, относительно которого считается дельта
|
||
|
||
Нужно различать:
|
||
|
||
1. уже окончательно опубликованное состояние;
|
||
2. текущий замороженный job, который ещё проходит Arweave/Solana.
|
||
|
||
Нельзя считать дельту от живой головы во время уже запущенной публикации.
|
||
|
||
---
|
||
|
||
# 8. Таблица `archive_chain_cursor`
|
||
|
||
Хранит окончательно подтверждённое архивное состояние для каждой `blockchain_name`.
|
||
|
||
```sql
|
||
CREATE TABLE archive_chain_cursor (
|
||
blockchain_name TEXT PRIMARY KEY,
|
||
|
||
last_archived_source_block_number BIGINT NOT NULL,
|
||
last_archived_source_block_hash BYTEA NOT NULL,
|
||
|
||
last_archive_big_block_number BIGINT,
|
||
last_archive_big_block_hash BYTEA,
|
||
|
||
last_chunk_offset BIGINT,
|
||
last_chunk_size BIGINT,
|
||
|
||
updated_at_ms BIGINT NOT NULL
|
||
);
|
||
```
|
||
|
||
Назначение:
|
||
|
||
- `last_archived_source_block_number` — до какого исходного SHiNE-блока этой blockchain всё успешно опубликовано;
|
||
- `last_archived_source_block_hash` — hash этого исходного блока;
|
||
- `last_archive_big_block_number` и `last_archive_big_block_hash` — в каком большом `SHINE-ARCHIVE` находится последний chunk этой blockchain;
|
||
- `last_chunk_offset` и `last_chunk_size` — точное положение последнего chunk внутри большого блока.
|
||
|
||
Этого достаточно, чтобы следующая публикация создала один backlink на предыдущий chunk.
|
||
|
||
---
|
||
|
||
# 9. Отдельный индекс по каждой записи не нужен
|
||
|
||
Поскольку все новые записи одной `blockchain_name` агрегируются в один `UserBlockchainChunk`, не нужно хранить offsets каждой отдельной записи в истории.
|
||
|
||
Для следующей публикации достаточно помнить:
|
||
|
||
```text
|
||
предыдущий big block
|
||
предыдущий big block hash
|
||
chunk offset
|
||
chunk size
|
||
```
|
||
|
||
---
|
||
|
||
# 10. Таблица `archive_publish_job`
|
||
|
||
Хранит crash-safe состояние одной архивной публикации.
|
||
|
||
```sql
|
||
CREATE TABLE archive_publish_job (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
|
||
big_block_number BIGINT NOT NULL,
|
||
status TEXT NOT NULL,
|
||
|
||
created_at_ms BIGINT NOT NULL,
|
||
|
||
local_archive_path TEXT,
|
||
|
||
archive_hash BYTEA,
|
||
arweave_tx_id BYTEA,
|
||
|
||
arweave_confirmations INTEGER,
|
||
solana_signature TEXT,
|
||
|
||
error_text TEXT,
|
||
updated_at_ms BIGINT NOT NULL
|
||
);
|
||
```
|
||
|
||
Рекомендуемые статусы:
|
||
|
||
```text
|
||
SNAPSHOT_CREATED
|
||
FILE_BUILT
|
||
ARWEAVE_UPLOADED
|
||
ARWEAVE_CONFIRMED
|
||
SOLANA_SUBMITTED
|
||
SOLANA_FINALIZED
|
||
CURSORS_COMMITTED
|
||
FAILED
|
||
```
|
||
|
||
Завершённые rows этой таблицы одновременно дают локальную историю больших блоков:
|
||
|
||
```text
|
||
big_block_number
|
||
archive_hash
|
||
arweave_tx_id
|
||
```
|
||
|
||
По ним строится FULL-список предыдущих больших блоков следующей публикации.
|
||
|
||
---
|
||
|
||
# 11. Таблица `archive_publish_job_chain`
|
||
|
||
Хранит frozen range каждой blockchain в текущем job и координаты создаваемого chunk.
|
||
|
||
```sql
|
||
CREATE TABLE archive_publish_job_chain (
|
||
job_id BIGINT NOT NULL,
|
||
blockchain_name TEXT NOT NULL,
|
||
|
||
from_source_block_number BIGINT NOT NULL,
|
||
to_source_block_number BIGINT NOT NULL,
|
||
|
||
previous_source_block_hash BYTEA NOT NULL,
|
||
last_source_block_hash BYTEA NOT NULL,
|
||
|
||
previous_archive_big_block_number BIGINT,
|
||
previous_archive_big_block_hash BYTEA,
|
||
previous_chunk_offset BIGINT,
|
||
previous_chunk_size BIGINT,
|
||
|
||
new_chunk_offset BIGINT,
|
||
new_chunk_size BIGINT,
|
||
|
||
PRIMARY KEY (job_id, blockchain_name)
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
# 12. Заморозка дельты
|
||
|
||
Пример:
|
||
|
||
```text
|
||
локально:
|
||
alice-001 = source block 180
|
||
bob-001 = source block 94
|
||
|
||
опубликовано:
|
||
alice-001 = 160
|
||
bob-001 = 90
|
||
```
|
||
|
||
Новый job фиксирует:
|
||
|
||
```text
|
||
alice-001: 161..180
|
||
bob-001: 91..94
|
||
```
|
||
|
||
Если во время загрузки появятся:
|
||
|
||
```text
|
||
alice-001 181..185
|
||
bob-001 95..97
|
||
```
|
||
|
||
они попадут только в следующий job.
|
||
|
||
---
|
||
|
||
# 13. Проверка курсора перед публикацией
|
||
|
||
Перед созданием frozen range сервер проверяет:
|
||
|
||
```text
|
||
local hash(last_archived_source_block_number)
|
||
==
|
||
archive_chain_cursor.last_archived_source_block_hash
|
||
```
|
||
|
||
При несовпадении публикация этой chain останавливается до resync/fork handling.
|
||
|
||
---
|
||
|
||
# 14. Ограничение размера большого блока
|
||
|
||
Offsets и sizes внутри `SHINE-ARCHIVE v1.0` используют `u32`.
|
||
|
||
Поэтому один большой archive-файл MUST быть меньше `4 GiB`.
|
||
|
||
Рекомендуемый запас:
|
||
|
||
```properties
|
||
archive.maxFileBytes=4000000000
|
||
```
|
||
|
||
Если данных больше, один scheduler-run формирует несколько последовательных больших блоков.
|
||
|
||
---
|
||
|
||
# 15. Magic и версия
|
||
|
||
Файл начинается ASCII:
|
||
|
||
```text
|
||
SHINE-ARCHIVE
|
||
```
|
||
|
||
Размер: 13 байт.
|
||
|
||
Далее:
|
||
|
||
```text
|
||
u8 version_major
|
||
u8 version_minor
|
||
```
|
||
|
||
Для v1.0:
|
||
|
||
```text
|
||
1
|
||
0
|
||
```
|
||
|
||
---
|
||
|
||
# 16. Общий layout большого блока
|
||
|
||
```text
|
||
+------------------------------------------+
|
||
| FIXED HEADER |
|
||
+------------------------------------------+
|
||
| creator_login |
|
||
+------------------------------------------+
|
||
| PREVIOUS BIG BLOCK REFERENCES |
|
||
| ref[0] |
|
||
| ref[1] |
|
||
| ... |
|
||
+------------------------------------------+
|
||
| UserBlockchainChunk #1 |
|
||
| UserBlockchainChunk #2 |
|
||
| ... |
|
||
+------------------------------------------+
|
||
| FOOTER: closer_login |
|
||
| block_hash |
|
||
| signature |
|
||
+------------------------------------------+
|
||
```
|
||
|
||
---
|
||
|
||
# 17. Fixed Header v1.0
|
||
|
||
Все integers — big-endian.
|
||
|
||
| Поле | Тип | Размер |
|
||
|---|---:|---:|
|
||
| magic | bytes[13] | 13 |
|
||
| version_major | u8 | 1 |
|
||
| version_minor | u8 | 1 |
|
||
| header_size | u32 | 4 |
|
||
| big_block_number | u32 | 4 |
|
||
| created_at_ms | u64 | 8 |
|
||
| creator_login_length | u8 | 1 |
|
||
| references_mode | u8 | 1 |
|
||
| references_count | u32 | 4 |
|
||
| reference_entry_size | u16 | 2 |
|
||
| parent_reference_index | u32 | 4 |
|
||
| blockchain_chunks_count | u32 | 4 |
|
||
| total_user_records_count | u32 | 4 |
|
||
|
||
После fixed header немедленно идут `creator_login` bytes.
|
||
|
||
`header_size` — byte offset первого `PreviousBigBlockReference`.
|
||
|
||
---
|
||
|
||
# 18. `creator_login`
|
||
|
||
SHiNE login пользователя/сервера, который сформировал большой блок.
|
||
|
||
```text
|
||
u8 creator_login_length
|
||
bytes[N] creator_login UTF-8
|
||
```
|
||
|
||
Этот login находится в начале файла, поэтому его можно узнать без скачивания payload.
|
||
|
||
---
|
||
|
||
# 19. `big_block_number`
|
||
|
||
```text
|
||
u32
|
||
```
|
||
|
||
Рекомендуемая нумерация:
|
||
|
||
```text
|
||
genesis = 0
|
||
next = 1
|
||
next = 2
|
||
...
|
||
```
|
||
|
||
---
|
||
|
||
# 20. `created_at_ms`
|
||
|
||
```text
|
||
u64
|
||
```
|
||
|
||
Unix Epoch UTC milliseconds.
|
||
|
||
Это время заморозки содержимого big block.
|
||
|
||
---
|
||
|
||
# 21. Режим ссылок на предыдущие большие блоки
|
||
|
||
```text
|
||
references_mode u8
|
||
```
|
||
|
||
Значения:
|
||
|
||
```text
|
||
0 = FULL
|
||
1 = PARTIAL, зарезервировано на будущее
|
||
```
|
||
|
||
Writer v1.0 MUST использовать `FULL`.
|
||
|
||
---
|
||
|
||
# 22. FULL previous-block table
|
||
|
||
Новый big block содержит ссылки на ВСЕ предыдущие большие блоки своей ветки.
|
||
|
||
Пример:
|
||
|
||
```text
|
||
BigBlock #365
|
||
|
||
References:
|
||
#0
|
||
#1
|
||
#2
|
||
...
|
||
#364
|
||
```
|
||
|
||
При одном big block в день через год таблица занимает примерно 24.8 KiB.
|
||
|
||
---
|
||
|
||
# 23. `PreviousBigBlockReference`
|
||
|
||
Фиксированный размер: 68 байт.
|
||
|
||
| Поле | Тип | Размер |
|
||
|---|---:|---:|
|
||
| big_block_number | u32 | 4 |
|
||
| big_block_hash | bytes[32] | 32 |
|
||
| arweave_tx_id | bytes[32] | 32 |
|
||
|
||
Семантика:
|
||
|
||
```text
|
||
big_block_number = логический номер
|
||
big_block_hash = криптографическая идентичность
|
||
arweave_tx_id = где скачать блок
|
||
```
|
||
|
||
---
|
||
|
||
# 24. `references_count`
|
||
|
||
Количество entries в таблице ссылок.
|
||
|
||
```text
|
||
u32
|
||
```
|
||
|
||
---
|
||
|
||
# 25. `reference_entry_size`
|
||
|
||
```text
|
||
u16
|
||
```
|
||
|
||
Для v1.0:
|
||
|
||
```text
|
||
68
|
||
```
|
||
|
||
Поле оставлено для будущего расширения entry.
|
||
|
||
---
|
||
|
||
# 26. `parent_reference_index`
|
||
|
||
```text
|
||
u32
|
||
```
|
||
|
||
Индекс непосредственного родителя внутри reference table.
|
||
|
||
Для genesis:
|
||
|
||
```text
|
||
0xFFFFFFFF
|
||
```
|
||
|
||
В простой FULL-линейной ветке обычно это последний entry.
|
||
|
||
---
|
||
|
||
# 27. Быстрое скачивание начала блока
|
||
|
||
Клиент сначала скачивает fixed header + creator login.
|
||
|
||
Из него узнаёт:
|
||
|
||
```text
|
||
header_size
|
||
references_count
|
||
reference_entry_size
|
||
```
|
||
|
||
Затем скачивает только reference table и уже имеет адреса/hash всех предыдущих больших блоков.
|
||
|
||
Payload пользователей можно пока не скачивать.
|
||
|
||
---
|
||
|
||
# 28. `blockchain_chunks_count`
|
||
|
||
```text
|
||
u32
|
||
```
|
||
|
||
Количество `UserBlockchainChunk` в текущем big block.
|
||
|
||
Одна `blockchain_name` встречается максимум один раз.
|
||
|
||
---
|
||
|
||
# 29. `total_user_records_count`
|
||
|
||
```text
|
||
u32
|
||
```
|
||
|
||
Общее количество исходных SHiNE-записей во всех chunks.
|
||
|
||
---
|
||
|
||
# 30. Порядок chunks и records
|
||
|
||
Для детерминированной сериализации chunks SHOULD сортироваться по `blockchain_name ASC`.
|
||
|
||
Внутри chunk исходные записи MUST идти по возрастанию номера исходного SHiNE-блока.
|
||
|
||
---
|
||
|
||
# 31. Формат `UserBlockchainChunk`
|
||
|
||
```text
|
||
u32 chunk_size
|
||
|
||
u8 blockchain_name_length
|
||
bytes[N] blockchain_name UTF-8
|
||
|
||
u32 records_count
|
||
|
||
repeat records_count:
|
||
u32 record_size
|
||
bytes[M] raw_record_bytes
|
||
|
||
u32 previous_big_block_ref
|
||
u32 previous_chunk_offset
|
||
u32 previous_chunk_size
|
||
```
|
||
|
||
---
|
||
|
||
# 32. `chunk_size`
|
||
|
||
```text
|
||
u32
|
||
```
|
||
|
||
Полный размер chunk от первого байта `chunk_size` до последнего байта `previous_chunk_size` включительно.
|
||
|
||
Это позволяет:
|
||
|
||
- быстро пропустить chunk;
|
||
- скачать его Range-запросом;
|
||
- сохранить его точные координаты для следующего backlink.
|
||
|
||
---
|
||
|
||
# 33. `blockchain_name` внутри chunk
|
||
|
||
Хранится только `blockchain_name`.
|
||
|
||
Отдельный `login` не нужен.
|
||
|
||
---
|
||
|
||
# 34. `records_count`
|
||
|
||
```text
|
||
u32
|
||
```
|
||
|
||
Если за период у `alice-001` появилось 7 записей:
|
||
|
||
```text
|
||
records_count = 7
|
||
```
|
||
|
||
Все 7 находятся в одном chunk.
|
||
|
||
---
|
||
|
||
# 35. Raw records
|
||
|
||
Каждая исходная запись сериализуется:
|
||
|
||
```text
|
||
u32 record_size
|
||
bytes[record_size] raw_record_bytes
|
||
```
|
||
|
||
`raw_record_bytes` — оригинальные данные SHiNE из БД.
|
||
|
||
Сервер не меняет их содержимое.
|
||
|
||
---
|
||
|
||
# 36. Один backlink на весь chunk
|
||
|
||
После всех raw records находится ровно один `PreviousBlockchainChunkRef`.
|
||
|
||
Он относится ко всей пачке записей blockchain.
|
||
|
||
Он не повторяется у отдельных записей.
|
||
|
||
---
|
||
|
||
# 37. `PreviousBlockchainChunkRef`
|
||
|
||
| Поле | Тип | Размер |
|
||
|---|---:|---:|
|
||
| previous_big_block_ref | u32 | 4 |
|
||
| previous_chunk_offset | u32 | 4 |
|
||
| previous_chunk_size | u32 | 4 |
|
||
|
||
Итого 12 байт на весь chunk.
|
||
|
||
---
|
||
|
||
# 38. `previous_big_block_ref`
|
||
|
||
Индекс в `PreviousBigBlockReferences[]` текущего big block.
|
||
|
||
Он указывает на тот предыдущий big block, где находится предыдущий chunk этой же `blockchain_name`.
|
||
|
||
Это именно reference index, а не номер блока.
|
||
|
||
---
|
||
|
||
# 39. `previous_chunk_offset`
|
||
|
||
```text
|
||
u32
|
||
```
|
||
|
||
Byte offset от начала предыдущего `SHINE-ARCHIVE` до первого байта предыдущего chunk этой blockchain.
|
||
|
||
Первый байт chunk — поле `chunk_size`.
|
||
|
||
---
|
||
|
||
# 40. `previous_chunk_size`
|
||
|
||
```text
|
||
u32
|
||
```
|
||
|
||
Полный размер предыдущего chunk.
|
||
|
||
Поэтому клиент может Range-запросом забрать сразу всю предыдущую пачку записей пользователя.
|
||
|
||
---
|
||
|
||
# 41. Первая публикация blockchain
|
||
|
||
Если у данной `blockchain_name` нет предыдущего архивного chunk:
|
||
|
||
```text
|
||
previous_big_block_ref = 0xFFFFFFFF
|
||
previous_chunk_offset = 0
|
||
previous_chunk_size = 0
|
||
```
|
||
|
||
---
|
||
|
||
# 42. Пример backlink-цепочки
|
||
|
||
В `BigBlock #100` есть `alice-001` chunk из 4 записей.
|
||
|
||
Его backlink:
|
||
|
||
```text
|
||
previous_big_block_ref = 94
|
||
previous_chunk_offset = 8123
|
||
previous_chunk_size = 1770
|
||
```
|
||
|
||
`ref[94]` содержит:
|
||
|
||
```text
|
||
big_block_number = 95
|
||
big_block_hash = ...
|
||
arweave_tx_id = ...
|
||
```
|
||
|
||
Клиент:
|
||
|
||
1. получает TX big block #95;
|
||
2. проверяет hash;
|
||
3. делает Range на `8123 .. 8123+1770`;
|
||
4. получает весь предыдущий `alice-001` chunk;
|
||
5. читает его записи;
|
||
6. берёт следующий backlink.
|
||
|
||
---
|
||
|
||
# 43. Почему TX ID не хранится в каждом chunk
|
||
|
||
TX ID и hash предыдущих больших блоков уже один раз записаны в общей reference table.
|
||
|
||
Поэтому в chunk достаточно компактного `u32 previous_big_block_ref`.
|
||
|
||
---
|
||
|
||
# 44. Footer большого блока
|
||
|
||
После последнего chunk:
|
||
|
||
```text
|
||
u8 closer_login_length
|
||
bytes[N] closer_login UTF-8
|
||
|
||
bytes[32] block_hash
|
||
bytes[64] closer_signature
|
||
```
|
||
|
||
---
|
||
|
||
# 45. `closer_login`
|
||
|
||
Login пользователя/сервера, который сформировал и закрыл big block.
|
||
|
||
В v1.0:
|
||
|
||
```text
|
||
closer_login MUST == creator_login
|
||
```
|
||
|
||
Login повторяется намеренно:
|
||
|
||
- header даёт автора сразу;
|
||
- footer явно фиксирует того, кто закрыл блок;
|
||
- hash покрывает footer login;
|
||
- verifier проверяет совпадение.
|
||
|
||
---
|
||
|
||
# 46. `block_hash`
|
||
|
||
```text
|
||
bytes[32]
|
||
SHA-256
|
||
```
|
||
|
||
Хэшируется всё от первого байта magic до последнего байта `closer_login` включительно.
|
||
|
||
Не входят:
|
||
|
||
```text
|
||
block_hash
|
||
closer_signature
|
||
```
|
||
|
||
Псевдокод:
|
||
|
||
```text
|
||
hash_input =
|
||
fixed_header
|
||
+ creator_login
|
||
+ references
|
||
+ all UserBlockchainChunks
|
||
+ closer_login_length
|
||
+ closer_login
|
||
|
||
block_hash = SHA256(hash_input)
|
||
```
|
||
|
||
---
|
||
|
||
# 47. `closer_signature`
|
||
|
||
```text
|
||
bytes[64]
|
||
Ed25519
|
||
```
|
||
|
||
Подписывается:
|
||
|
||
```text
|
||
ASCII("SHINE-ARCHIVE-V1") || block_hash
|
||
```
|
||
|
||
Для v1.0 используется root private key аккаунта `creator_login`.
|
||
|
||
---
|
||
|
||
# 48. Проверка подписи
|
||
|
||
Verifier:
|
||
|
||
1. читает `creator_login`;
|
||
2. проверяет `creator_login == closer_login`;
|
||
3. получает root public key аккаунта из User PDA / исторического состояния Solana;
|
||
4. пересчитывает `block_hash`;
|
||
5. проверяет Ed25519 signature.
|
||
|
||
Старые архивы должны оставаться проверяемыми после ротации root key, поэтому исторические PDA-состояния нужно сохранять/уметь получать.
|
||
|
||
---
|
||
|
||
# 49. Собственный Arweave TX ID текущего блока
|
||
|
||
Текущий файл не может заранее знать свой будущий TX ID.
|
||
|
||
Поэтому собственный TX ID:
|
||
|
||
- не входит в сам текущий файл;
|
||
- сохраняется после upload;
|
||
- записывается в Solana PDA;
|
||
- появляется как previous-block reference уже в следующем big block.
|
||
|
||
---
|
||
|
||
# 50. User PDA block type `100`
|
||
|
||
Archive head хранится прямо в существующем User PDA publisher-а.
|
||
|
||
Формат:
|
||
|
||
```text
|
||
u8 block_type = 100
|
||
u8 block_version = 0
|
||
bytes[32] archive_tx_id
|
||
bytes[32] archive_hash
|
||
```
|
||
|
||
Итого 66 байт.
|
||
|
||
`archive_hash` = `block_hash` последнего успешно опубликованного big block.
|
||
|
||
---
|
||
|
||
# 51. Обновление Solana
|
||
|
||
Отдельная новая Solana instruction не требуется.
|
||
|
||
Используется существующий `update_user_pda`.
|
||
|
||
Нужно расширить существующие serializer/parser/codec так, чтобы block type 100:
|
||
|
||
- читался;
|
||
- записывался;
|
||
- сохранялся обычными обновлениями;
|
||
- мог быть изменён архивным сервером обычным update User PDA.
|
||
|
||
---
|
||
|
||
# 52. Полный порядок публикации
|
||
|
||
```text
|
||
1. Существующая server-to-server sync независимо обновляет локальные blockchains.
|
||
|
||
2. Scheduler запускает archive job.
|
||
|
||
3. Проверяется отсутствие другого активного archive job.
|
||
|
||
4. Читаются archive_chain_cursor.
|
||
|
||
5. Читаются текущие локальные blockchain heads.
|
||
|
||
6. Для каждой blockchain вычисляется frozen delta.
|
||
|
||
7. Создаётся archive_publish_job.
|
||
|
||
8. Создаются archive_publish_job_chain rows.
|
||
|
||
9. Загружается история всех успешно finalized previous big blocks.
|
||
|
||
10. Формируется fixed header.
|
||
|
||
11. Записывается creator_login.
|
||
|
||
12. Записывается FULL PreviousBigBlockReferences.
|
||
|
||
13. Для каждой blockchain_name:
|
||
- читается frozen range;
|
||
- создаётся ровно один UserBlockchainChunk;
|
||
- внутрь кладутся все новые records;
|
||
- добавляется один PreviousBlockchainChunkRef;
|
||
- сохраняются new_chunk_offset/new_chunk_size.
|
||
|
||
14. Записывается closer_login.
|
||
|
||
15. Вычисляется SHA-256 block_hash.
|
||
|
||
16. Создаётся Ed25519 closer_signature.
|
||
|
||
17. Файл закрывается.
|
||
|
||
18. Файл загружается в Arweave.
|
||
|
||
19. TX ID сохраняется в archive_publish_job.
|
||
|
||
20. Сервер ждёт требуемое число Arweave confirmations.
|
||
|
||
21. Обычным update_user_pda обновляется block type 100:
|
||
archive_tx_id
|
||
archive_hash
|
||
|
||
22. Сервер ждёт Solana finalized.
|
||
|
||
23. В одной DB transaction обновляются archive_chain_cursor.
|
||
|
||
24. Job получает CURSORS_COMMITTED.
|
||
```
|
||
|
||
---
|
||
|
||
# 53. Cursor commit
|
||
|
||
Для каждой blockchain из job:
|
||
|
||
```text
|
||
last_archived_source_block_number = to_source_block_number
|
||
last_archived_source_block_hash = last_source_block_hash
|
||
|
||
last_archive_big_block_number = current big_block_number
|
||
last_archive_big_block_hash = current block_hash
|
||
|
||
last_chunk_offset = new_chunk_offset
|
||
last_chunk_size = new_chunk_size
|
||
```
|
||
|
||
Следующая публикация сразу знает, куда должен вести backlink.
|
||
|
||
---
|
||
|
||
# 54. Arweave service
|
||
|
||
Старый `TestFreeAvatarArweaveService` больше не нужен как avatar-specific сервис.
|
||
|
||
Его следует переделать/переименовать, например в:
|
||
|
||
```text
|
||
ArweaveArchiveService
|
||
```
|
||
|
||
Он должен:
|
||
|
||
- использовать Arweave JWK;
|
||
- подписывать и оплачивать upload;
|
||
- поддерживать большие/chunked uploads;
|
||
- возвращать TX ID;
|
||
- ждать подтверждения;
|
||
- опрашивать confirmations.
|
||
|
||
Рекомендуемые настройки:
|
||
|
||
```properties
|
||
archive.arweave.gateway=https://arweave.net
|
||
archive.arweave.walletJwkPath=/opt/shine/secrets/archive-wallet.json
|
||
archive.arweave.minConfirmations=1
|
||
archive.arweave.confirmPollSeconds=30
|
||
archive.arweave.confirmTimeoutMinutes=180
|
||
```
|
||
|
||
---
|
||
|
||
# 55. Solana finalization
|
||
|
||
После обычного `update_user_pda` сервер должен дождаться:
|
||
|
||
```text
|
||
finalized
|
||
```
|
||
|
||
Только после этого разрешается сдвинуть локальные курсоры.
|
||
|
||
---
|
||
|
||
# 56. Crash recovery
|
||
|
||
## После `SNAPSHOT_CREATED`
|
||
|
||
Возобновить тот же frozen job.
|
||
|
||
## После `FILE_BUILT`
|
||
|
||
Использовать тот же локальный файл.
|
||
|
||
## После `ARWEAVE_UPLOADED`
|
||
|
||
Не загружать второй раз. Продолжить по сохранённому TX ID.
|
||
|
||
## После `ARWEAVE_CONFIRMED`
|
||
|
||
Продолжить Solana update.
|
||
|
||
## После `SOLANA_SUBMITTED`
|
||
|
||
Сначала проверить transaction / текущий PDA.
|
||
|
||
## После `SOLANA_FINALIZED`, но до cursor commit
|
||
|
||
Проверить:
|
||
|
||
```text
|
||
PDA.archive_tx_id == expected_tx
|
||
PDA.archive_hash == expected_hash
|
||
```
|
||
|
||
После этого безопасно выполнить cursor commit.
|
||
|
||
---
|
||
|
||
# 57. Нет новых данных
|
||
|
||
Если новых записей нет ни в одной blockchain:
|
||
|
||
```text
|
||
не создавать пустой SHINE-ARCHIVE
|
||
не загружать Arweave
|
||
не обновлять Solana
|
||
```
|
||
|
||
---
|
||
|
||
# 58. FULL references как стартовая стратегия
|
||
|
||
v1.0 сознательно хранит ВСЕ previous big blocks в каждом новом big block.
|
||
|
||
Плюсы:
|
||
|
||
- актуальный блок сразу даёт карту всей предыдущей истории;
|
||
- не нужно идти parent-by-parent;
|
||
- backlink может использовать компактный ref index;
|
||
- reader очень простой.
|
||
|
||
При одном big block в сутки:
|
||
|
||
```text
|
||
1 год: ~24.8 KiB
|
||
5 лет: ~121 KiB
|
||
20 лет: ~485 KiB
|
||
```
|
||
|
||
---
|
||
|
||
# 59. PARTIAL references
|
||
|
||
`references_mode = 1` зарезервирован на будущее.
|
||
|
||
v1.0 writer его не реализует.
|
||
|
||
В будущем PARTIAL может означать:
|
||
|
||
- только реально используемые previous blocks;
|
||
- последние N blocks;
|
||
- checkpoint references;
|
||
- смешанную стратегию.
|
||
|
||
---
|
||
|
||
# 60. Чтение истории одной blockchain
|
||
|
||
Имея актуальный chunk `alice-001`, клиент:
|
||
|
||
1. читает все records chunk;
|
||
2. читает `PreviousBlockchainChunkRef`;
|
||
3. если ref = `0xFFFFFFFF`, история закончилась;
|
||
4. иначе берёт соответствующий `PreviousBigBlockReference`;
|
||
5. получает TX ID + hash previous big block;
|
||
6. Range-запросом читает `previous_chunk_offset/previous_chunk_size`;
|
||
7. получает сразу всю предыдущую пачку записей `alice-001`;
|
||
8. повторяет.
|
||
|
||
Так можно читать историю конкретной blockchain, не скачивая чужие chunks.
|
||
|
||
---
|
||
|
||
# 61. Дубликаты
|
||
|
||
Если исходная SHiNE-запись уже есть у получателя, она пропускается/дедуплицируется стандартной логикой.
|
||
|
||
---
|
||
|
||
# 62. Конфликты исходной blockchain
|
||
|
||
Архивная ветка сама по себе не является пользовательским конфликтом.
|
||
|
||
Конфликт возникает, когда одна `blockchain_name` имеет два несовместимых валидно подписанных продолжения одной предыдущей точки.
|
||
|
||
Для v1 безопасно:
|
||
|
||
```text
|
||
сохранить обе ветки
|
||
пометить blockchain CONFLICTED
|
||
остановить автоматическое продолжение
|
||
```
|
||
|
||
Механизм разрешения fork можно добавить позже.
|
||
|
||
---
|
||
|
||
# 63. Авторство большого блока
|
||
|
||
Автор указан дважды:
|
||
|
||
## Header
|
||
|
||
```text
|
||
creator_login
|
||
```
|
||
|
||
## Footer
|
||
|
||
```text
|
||
closer_login
|
||
block_hash
|
||
closer_signature
|
||
```
|
||
|
||
Для v1.0:
|
||
|
||
```text
|
||
creator_login == closer_login
|
||
```
|
||
|
||
Это означает: именно этот SHiNE-аккаунт сформировал и криптографически закрыл big block.
|
||
|
||
---
|
||
|
||
# 64. Подпись сервера не заменяет подписи пользователей
|
||
|
||
`closer_signature` доказывает авторство большого archive block.
|
||
|
||
Каждая пользовательская raw record всё равно проверяется существующей криптографией SHiNE.
|
||
|
||
---
|
||
|
||
# 65. Независимость от Arweave
|
||
|
||
Arweave TX ID — location.
|
||
|
||
SHA-256 — identity.
|
||
|
||
Поэтому архивы можно перенести на другой storage и всё равно проверять.
|
||
|
||
---
|
||
|
||
# 66. Block type 100 в Solana — текущая голова
|
||
|
||
User PDA publisher-а хранит только:
|
||
|
||
```text
|
||
current archive TX ID
|
||
current archive SHA-256
|
||
```
|
||
|
||
Детальная история находится внутри big blocks.
|
||
|
||
---
|
||
|
||
# 67. Startup preflight
|
||
|
||
Перед запуском archive publisher сервер SHOULD проверить:
|
||
|
||
```text
|
||
derivePublic(root_private) == UserPDA.root_public_key
|
||
```
|
||
|
||
и:
|
||
|
||
```text
|
||
derivePublic(client_private) == UserPDA.client_key
|
||
```
|
||
|
||
При несовпадении archive publisher не запускается.
|
||
|
||
---
|
||
|
||
# 68. Рекомендуемые настройки
|
||
|
||
```properties
|
||
archive.publish.enabled=false
|
||
archive.publish.intervalMinutes=720
|
||
archive.publish.initialDelayMinutes=15
|
||
|
||
archive.maxFileBytes=4000000000
|
||
|
||
archive.arweave.gateway=https://arweave.net
|
||
archive.arweave.walletJwkPath=/opt/shine/secrets/archive-wallet.json
|
||
archive.arweave.minConfirmations=1
|
||
archive.arweave.confirmPollSeconds=30
|
||
archive.arweave.confirmTimeoutMinutes=180
|
||
|
||
archive.solana.rootKeyPath=/opt/shine/secrets/root.key
|
||
archive.solana.clientKeyPath=/opt/shine/secrets/client.key
|
||
archive.solana.commitment=finalized
|
||
```
|
||
|
||
---
|
||
|
||
# 69. Требование к `AppConfig`
|
||
|
||
Archive settings должны читаться из:
|
||
|
||
- Java system properties;
|
||
- environment variables;
|
||
- `application.properties`.
|
||
|
||
`getInt()` и `getBoolean()` должны использовать общую логику `getParam()`.
|
||
|
||
---
|
||
|
||
# 70. Что переиспользуется из текущего сервера
|
||
|
||
Существующие механизмы:
|
||
|
||
```text
|
||
BlockchainStateDAO.listAll()
|
||
BlocksDAO.listRangeByNumber(...)
|
||
```
|
||
|
||
дают необходимые heads и raw block ranges.
|
||
|
||
Используются существующие:
|
||
|
||
```text
|
||
block_bytes
|
||
block_hash
|
||
block_signature
|
||
blockchain_name
|
||
block_number
|
||
```
|
||
|
||
Server-to-server sync в v1.0 не меняется.
|
||
|
||
---
|
||
|
||
# 71. Блокировки
|
||
|
||
Можно использовать существующий blockchain lock только на коротком этапе:
|
||
|
||
```text
|
||
проверить cursor hash
|
||
прочитать frozen range
|
||
проверить конечный hash
|
||
сохранить/сериализовать frozen data
|
||
```
|
||
|
||
Lock не удерживается во время Arweave/Solana ожиданий.
|
||
|
||
---
|
||
|
||
# 72. Реализационный checklist
|
||
|
||
## Database
|
||
|
||
- [ ] `archive_chain_cursor`
|
||
- [ ] `archive_publish_job`
|
||
- [ ] `archive_publish_job_chain`
|
||
- [ ] schema migration
|
||
- [ ] crash recovery
|
||
|
||
## Archive writer
|
||
|
||
- [ ] magic `SHINE-ARCHIVE`
|
||
- [ ] major/minor version
|
||
- [ ] fixed header
|
||
- [ ] creator login
|
||
- [ ] FULL previous big block table
|
||
- [ ] one chunk per `blockchain_name`
|
||
- [ ] many raw records inside one chunk
|
||
- [ ] one backlink per chunk
|
||
- [ ] closer login
|
||
- [ ] SHA-256
|
||
- [ ] Ed25519 signature
|
||
- [ ] max file size < 4 GiB
|
||
|
||
## Arweave
|
||
|
||
- [ ] rename/refactor `TestFreeAvatarArweaveService`
|
||
- [ ] remove avatar-specific logic
|
||
- [ ] large/chunked upload
|
||
- [ ] confirmation polling
|
||
|
||
## Solana
|
||
|
||
- [ ] add PDA block type `100`
|
||
- [ ] update Rust codec
|
||
- [ ] update Java codec
|
||
- [ ] update JS codec/UI writer
|
||
- [ ] use ordinary `update_user_pda`
|
||
- [ ] server-side transaction writer
|
||
- [ ] root signature
|
||
- [ ] client fee payer
|
||
- [ ] wait for `finalized`
|
||
|
||
## Scheduler
|
||
|
||
- [ ] `archive.publish.enabled`
|
||
- [ ] `archive.publish.intervalMinutes`
|
||
- [ ] `archive.publish.initialDelayMinutes`
|
||
- [ ] no concurrent jobs
|
||
- [ ] split files > max size
|
||
- [ ] skip when no new blocks
|
||
|
||
---
|
||
|
||
# 73. Итоговая бинарная схема v1.0
|
||
|
||
```text
|
||
SHINE-ARCHIVE
|
||
version_major u8
|
||
version_minor u8
|
||
|
||
header_size u32
|
||
big_block_number u32
|
||
created_at_ms u64
|
||
|
||
creator_login_length u8
|
||
|
||
references_mode u8
|
||
references_count u32
|
||
reference_entry_size u16
|
||
parent_reference_index u32
|
||
|
||
blockchain_chunks_count u32
|
||
total_user_records_count u32
|
||
|
||
creator_login bytes
|
||
|
||
|
||
PreviousBigBlockReference[references_count]:
|
||
|
||
big_block_number u32
|
||
big_block_hash [32]
|
||
arweave_tx_id [32]
|
||
|
||
|
||
UserBlockchainChunk[blockchain_chunks_count]:
|
||
|
||
chunk_size u32
|
||
|
||
blockchain_name_length u8
|
||
blockchain_name bytes
|
||
|
||
records_count u32
|
||
|
||
repeat records_count:
|
||
record_size u32
|
||
raw_record_bytes
|
||
|
||
previous_big_block_ref u32
|
||
previous_chunk_offset u32
|
||
previous_chunk_size u32
|
||
|
||
|
||
FOOTER:
|
||
|
||
closer_login_length u8
|
||
closer_login bytes
|
||
|
||
block_hash [32]
|
||
closer_signature [64]
|
||
```
|
||
|
||
---
|
||
|
||
# 74. Основная архитектура одним рисунком
|
||
|
||
```text
|
||
SOLANA USER PDA
|
||
|
|
||
| block type 100
|
||
| TX + HASH
|
||
v
|
||
BigBlock #100
|
||
|
|
||
+-- Header
|
||
| creator = server-A
|
||
|
|
||
+-- References
|
||
| #0 -> BigBlock #0 / hash / TX
|
||
| #1 -> BigBlock #1 / hash / TX
|
||
| ...
|
||
| #99 -> BigBlock #99 / hash / TX
|
||
|
|
||
+-- alice-001 chunk
|
||
| records x4
|
||
| |
|
||
| +--> BigBlock #95 / AliceChunk
|
||
|
|
||
+-- bob-001 chunk
|
||
| records x2
|
||
| |
|
||
| +--> BigBlock #98 / BobChunk
|
||
|
|
||
+-- kate-002 chunk
|
||
| records x10
|
||
| |
|
||
| +--> BigBlock #74 / KateChunk
|
||
|
|
||
+-- Footer
|
||
closer = server-A
|
||
SHA-256
|
||
Ed25519 signature
|
||
```
|
||
|
||
---
|
||
|
||
# 75. Итоговые решения v1.0
|
||
|
||
- один большой блок = одна архивная публикация;
|
||
- в начале лежит полный список всех предыдущих больших блоков;
|
||
- каждый previous block reference содержит `number + SHA-256 + Arweave TX ID`;
|
||
- одна `blockchain_name` встречается в большом блоке максимум одним chunk;
|
||
- все новые записи этой blockchain объединяются в этот chunk;
|
||
- chunk имеет одну ссылку на предыдущий chunk этой же blockchain;
|
||
- backlink состоит из `previous big block ref + chunk offset + chunk size`;
|
||
- сервер сам создаёт backlink;
|
||
- пользовательские raw records не меняются;
|
||
- login сервера есть в header и footer;
|
||
- большой блок закрывается SHA-256 + Ed25519 signature сервера;
|
||
- Solana User PDA block `100` хранит текущий TX ID + archive hash;
|
||
- PDA обновляется обычным `update_user_pda`;
|
||
- Arweave используется как долговременное хранилище;
|
||
- публикация считается завершённой только после Arweave confirmation + Solana finalized + cursor commit.
|