Files
SHiNE-server/docs/SHINE_ARCHIVE_PROTOCOL_v1.0_RU.md
T

1353 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SHiNE Archive Protocol v1.0
## Полная русская спецификация серверной фиксации больших блоков, Arweave, Solana User PDA и навигации по предыдущим записям пользователя
**Статус:** рабочая спецификация v1.0
**Формат:** бинарный
**Magic:** `SHINE-ARCHIVE`
**Версия:** два отдельных байта `major` / `minor`
---
# 1. Назначение
Протокол предназначен для периодической долговременной фиксации новых SHiNE-блоков.
Стартовая схема:
```text
Основной SHiNE-сервер
|
| существующая межсерверная синхронизация
v
Raspberry / архивный SHiNE-сервер
|
+--> видит все новые локальные SHiNE-блоки
+--> фиксирует стабильную дельту
+--> собирает большой SHINE-ARCHIVE блок
+--> загружает его в Arweave
+--> ждёт закрепления Arweave-транзакции
+--> обычным update своего User PDA записывает новый archive head
```
Существующую межсерверную синхронизацию в v1 менять не требуется.
Архивный сервер не создаёт и не переподписывает пользовательские сообщения. Он берёт уже существующие пользовательские записи и добавляет только серверную навигационную метаинформацию большого блока.
---
# 2. Основные принципы
1. Публикуется только новая дельта.
2. Дельта замораживается до начала загрузки.
3. Новые данные, пришедшие во время публикации, попадают в следующий большой блок.
4. После загрузки сервер ждёт закрепления Arweave-транзакции.
5. Только после закрепления Arweave сервер обновляет свой User PDA в Solana.
6. Только после `finalized` в Solana локальные курсоры считаются окончательно сдвинутыми.
7. Большой блок имеет собственный SHA-256 и подпись составившего его SHiNE-аккаунта.
8. Каждый большой блок содержит список предыдущих больших блоков с номером, hash и Arweave TX ID.
9. В v1 в начало каждого нового большого блока записывается полный список всех известных предыдущих больших блоков данной ветки.
10. Для каждого пользователя сервер строит обратную цепочку не на одну предыдущую запись, а на **предыдущий большой блок, где были записи этого пользователя, плюс список всех его записей внутри того блока**.
---
# 3. Ключи архивного сервера
Для первой реализации серверу требуются:
- `root private key` SHiNE-аккаунта;
- `client private key` SHiNE-аккаунта;
- Arweave JWK/private wallet key.
`blockchain private key` для самой архивной публикации не требуется: пользовательские записи уже существуют и уже подписаны.
В v1 большой блок может подписываться root key того SHiNE-аккаунта, который его сформировал.
---
# 4. Локальное состояние дельты
Нужно различать:
1. уже окончательно опубликованное состояние;
2. текущую замороженную дельту, которая ещё проходит Arweave/Solana.
Рекомендуются три таблицы.
## 4.1. `archive_chain_cursor`
```sql
CREATE TABLE archive_chain_cursor (
blockchain_name TEXT PRIMARY KEY,
last_archived_block_number BIGINT NOT NULL,
last_archived_block_hash BYTEA NOT NULL,
updated_at_ms BIGINT NOT NULL
);
```
Она хранит точку, до которой конкретная локальная blockchain уже окончательно вошла в опубликованный и зафиксированный archive head.
## 4.2. `archive_publish_job`
```sql
CREATE TABLE archive_publish_job (
id BIGSERIAL PRIMARY KEY,
archive_number BIGINT NOT NULL,
status TEXT NOT NULL,
parent_arweave_tx_id BYTEA,
parent_archive_hash BYTEA,
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
```
## 4.3. `archive_publish_job_chain`
```sql
CREATE TABLE archive_publish_job_chain (
job_id BIGINT NOT NULL,
blockchain_name TEXT NOT NULL,
from_block_number BIGINT NOT NULL,
to_block_number BIGINT NOT NULL,
previous_block_hash BYTEA NOT NULL,
last_block_hash BYTEA NOT NULL,
PRIMARY KEY (job_id, blockchain_name)
);
```
Сырые блоки в staging-таблицы копировать не нужно. В job фиксируются только точные диапазоны и крайние hash.
---
# 5. Заморозка дельты
Пример:
```text
локально сейчас:
Alice = 180
Bob = 94
уже опубликовано:
Alice = 160
Bob = 90
```
Новый job фиксирует:
```text
Alice: 161..180
Bob: 91..94
```
Если во время Arweave upload локальное состояние стало:
```text
Alice = 185
Bob = 97
```
текущий job НЕ меняется.
Следующая публикация начнётся с:
```text
Alice: 181..185
Bob: 95..97
```
---
# 6. Общая структура большого блока
Большой опубликованный файл одновременно является:
- архивной дельтой;
- индексом предыдущих больших блоков;
- контейнером пользовательских записей;
- навигационным узлом для истории каждого пользователя;
- подписанным объектом того SHiNE-аккаунта, который его сформировал.
Структура:
```text
+------------------------------------------------+
| PREFIX / HEADER |
| magic = SHINE-ARCHIVE |
| version major/minor |
| размеры/счётчики |
| номер большого блока |
| время |
| LOGIN составителя |
| режим таблицы предыдущих блоков |
| число предыдущих блоков |
+------------------------------------------------+
| PREVIOUS BLOCK REFERENCES TABLE |
| |
| block_number + block_hash + arweave_tx_id |
| block_number + block_hash + arweave_tx_id |
| ... |
+------------------------------------------------+
| USER RECORDS / SERVER NAVIGATION METADATA |
| ... |
+------------------------------------------------+
| FOOTER |
| LOGIN закрывшего блок |
| block_hash |
| signature |
+------------------------------------------------+
```
Логин в header и footer должен быть одинаковым.
Дублирование логина намеренное:
- header позволяет сразу при скачивании начала файла увидеть, кто составил блок;
- footer делает закрытие блока человекочитаемым рядом с hash и подписью.
---
# 7. Magic и версия
Magic:
```text
SHINE-ARCHIVE
```
Это ровно 13 ASCII-байт:
```text
53 48 49 4E 45 2D 41 52 43 48 49 56 45
```
Версия:
```text
u8 version_major
u8 version_minor
```
Для этой спецификации:
```text
1
0
```
то есть `v1.0`.
---
# 8. Порядок байтов
Все fixed-size integer-поля v1 хранятся в:
```text
big-endian
```
---
# 9. Header большого блока v1.0
Header должен быть устроен так, чтобы клиент мог сначала скачать только небольшой префикс файла, узнать размер всей начальной индексной области и затем докачать только её.
Формат:
```text
bytes[13] magic = "SHINE-ARCHIVE"
u8 version_major
u8 version_minor
u32 header_size
u32 block_number
u64 created_at_ms
u8 creator_login_length
bytes[N] creator_login UTF-8
u8 references_mode
u32 references_count
u16 reference_entry_size
u32 records_count
```
## 9.1. `header_size`
`header_size` — абсолютный byte offset от начала файла до первого байта области пользовательских записей.
То есть включает:
```text
header prefix
+ creator_login
+ всю таблицу PreviousBlockReference
```
Клиент может:
1. скачать первые условные 64–128 байт;
2. прочитать `header_size`;
3. докачать `0 .. header_size-1`;
4. уже иметь все адреса предыдущих больших блоков;
5. после этого скачивать только нужные user records.
## 9.2. `block_number`
```text
u32
```
Номер большого итогового блока данной ветки.
Диапазон:
```text
0 .. 4 294 967 295
```
Для предполагаемой частоты закрытия это более чем достаточный запас.
## 9.3. `created_at_ms`
```text
u64
```
Unix Epoch UTC в миллисекундах.
Это время фиксации snapshot текущего большого блока.
## 9.4. `creator_login`
Логин SHiNE-пользователя/сервера, который сформировал этот большой блок.
Пример:
```text
raspberry-archive
```
Это поле находится именно в header, поэтому клиент видит автора уже после скачивания начала блока.
`creator_login` входит в hash большого блока и тем самым криптографически привязан к его подписи.
## 9.5. `references_mode`
```text
u8
```
Зарезервированные значения:
```text
0 = FULL_HISTORY
1 = PARTIAL_HISTORY
```
Для v1 publisher MUST использовать:
```text
FULL_HISTORY
```
То есть каждый новый большой блок содержит ссылки на все известные предыдущие большие блоки своей ветки.
`PARTIAL_HISTORY` резервируется на будущее, если когда-нибудь полный список станет слишком большим.
## 9.6. `references_count`
```text
u32
```
Количество элементов в таблице предыдущих больших блоков.
## 9.7. `reference_entry_size`
```text
u16
```
Для v1:
```text
68
```
Поле оставлено явно, чтобы будущая minor/major версия могла расширить `PreviousBlockReference`, а клиент всё ещё мог корректно пропускать неизвестные дополнительные байты.
## 9.8. `records_count`
```text
u32
```
Количество пользовательских record-контейнеров внутри большого блока.
---
# 10. Таблица всех предыдущих больших блоков
Сразу после header идёт:
```text
PreviousBlockReference[references_count]
```
В v1 это **полная история предыдущих больших блоков данной ветки**.
При одном закрытии в сутки даже через год это около 365 элементов, поэтому простота полного списка важнее преждевременной оптимизации.
---
# 11. `PreviousBlockReference v1`
Формат одного элемента:
```text
u32 block_number
bytes[32] block_hash
bytes[32] arweave_tx_id
```
Размер:
```text
4 + 32 + 32 = 68 байт
```
Смысл:
```text
block_number
-> логический номер большого блока
block_hash
-> SHA-256 / канонический hash точного содержимого большого блока
arweave_tx_id
-> raw 32-byte TX ID, по которому этот большой блок можно скачать из Arweave
```
То есть:
```text
HASH = идентичность содержимого
TXID = место хранения
```
Оба поля обязательны.
Внешнее строковое Base64URL-представление TX ID внутрь бинарного файла не записывается.
Таблица v1 SHOULD быть отсортирована детерминированно по `block_number`, затем по `block_hash`.
---
# 12. Почему в v1 записываются все предыдущие блоки
Если закрывать примерно один большой блок в сутки:
```text
365 × 68 ≈ 24.8 KB в год
```
Даже через много лет это остаётся небольшим overhead по сравнению с полезными данными.
Главное преимущество: любой новый большой блок сразу является индексом всей предыдущей ветки.
Клиент не обязан идти:
```text
365 -> 364 -> 363 -> ... -> 95
```
Чтобы получить адрес блока 95.
Он получает его из начала текущего блока.
---
# 13. Пользовательская обратная навигация
Навигация пользователя строится не на одну предыдущую запись.
Правило v1:
> Для каждого пользователя сервер находит **предыдущий большой блок**, в котором существовала хотя бы одна запись этого пользователя, и сохраняет ссылку на этот большой блок вместе со списком `offset + size` **всех записей этого пользователя в том предыдущем большом блоке**.
Например:
```text
Большой block 100:
Alice сегодня имеет 2 записи.
Предыдущий большой блок, где Alice присутствовала:
block 95.
В block 95 у Alice было 3 записи:
offset 8123 / size 417
offset 9001 / size 351
offset 14020 / size 692
```
Тогда server-generated ссылка Alice из block 100 содержит все три диапазона.
---
# 14. `PreviousUserRecordsRef v1`
Формат:
```text
u32 previous_block_ref
u32 previous_records_count
repeat previous_records_count times:
u32 previous_record_offset
u32 previous_record_size
```
Минимальный размер:
```text
8 байт
```
Плюс:
```text
8 байт на каждую предыдущую запись пользователя
```
Пример для 3 сообщений:
```text
previous_block_ref = 94
previous_records_count = 3
8123 417
9001 351
14020 692
```
Если `PreviousBlockReference[94]` описывает big block 95, клиент получает:
```text
block number = 95
block hash = ...
Arweave TXID = ...
```
и может HTTP Range-запросами забрать только три нужных диапазона.
---
# 15. `previous_block_ref`
`previous_block_ref` — индекс в таблице `PreviousBlockReference` **текущего большого блока**.
То есть пользовательская ссылка не повторяет 32-byte TX ID и 32-byte hash.
Она хранит только компактный индекс.
Специальное значение:
```text
0xFFFFFFFF
```
означает:
```text
у пользователя ещё не было записей в предыдущих больших блоках
```
В этом случае:
```text
previous_records_count = 0
```
---
# 16. Смещение и размер записи
`previous_record_offset`:
```text
u32
```
Абсолютный offset от первого байта соответствующего большого `SHINE-ARCHIVE` файла до первого байта нужной архивированной user-record.
`previous_record_size`:
```text
u32
```
Полный размер этой архивированной user-record в байтах.
Таким образом, зная:
```text
TX ID
block hash
offset
size
```
клиент может скачать ровно нужную пользовательскую запись.
---
# 17. Как сервер добавляет пользовательскую ссылку
`PreviousUserRecordsRef` создаёт **сервер, который формирует большой блок**, а не пользовательский клиент.
Оригинальные подписанные пользователем bytes изменять нельзя.
Поэтому server navigation metadata хранится как часть контейнера большого блока вокруг/после оригинальных user-record bytes и покрывается:
- `block_hash` большого блока;
- подписью сервера/пользователя, который закрыл большой блок.
Для каждого пользователя в текущем большом блоке достаточно одного `PreviousUserRecordsRef`, связанного с последней записью этого пользователя в текущем блоке.
Если у пользователя сегодня в текущем большом блоке две записи, сервер после их сериализации добавляет один navigation tail, который указывает на предыдущий большой блок пользователя и перечисляет все его записи там.
При чтении предыдущего большого блока та же схема даёт следующий переход назад.
Получается:
```text
Alice in block 100
|
| PreviousUserRecordsRef
| -> block 95
| -> offsets of ALL Alice records in block 95
v
Alice records in block 95
|
| server navigation metadata из block 95
| -> block 81
| -> offsets of ALL Alice records in block 81
v
...
```
---
# 18. Рекомендуемый контейнер архивированной пользовательской записи
Чтобы сервер мог добавлять свою навигационную метаинформацию, не изменяя исходную пользовательскую подпись, v1 SHOULD использовать серверный envelope.
Рекомендуемая модель:
```text
u32 archived_record_size
u32 raw_user_record_size
bytes[N] raw_user_record
u8 navigation_flags
[optional server navigation data]
```
Где:
```text
navigation_flags bit 0 = PreviousUserRecordsRef присутствует
```
У большинства записей:
```text
navigation_flags = 0
```
У последней записи конкретного пользователя в текущем большом блоке:
```text
navigation_flags bit0 = 1
```
и далее сериализован `PreviousUserRecordsRef`.
`archived_record_size` позволяет Range-reader получить и полностью декодировать конкретный archived record.
Исходный `raw_user_record` остаётся байт-в-байт неизменным.
---
# 19. Пример пользовательской цепочки
```text
BIG BLOCK 100
Alice record A
Alice record B + PreviousUserRecordsRef:
previous_block_ref -> block 95
previous_records_count = 3
[8123,417]
[9001,351]
[14020,692]
|
v
BIG BLOCK 95
Alice old record #1
Alice old record #2
Alice old record #3 + PreviousUserRecordsRef:
previous_block_ref -> block 81
previous_records_count = 1
[4412,380]
|
v
BIG BLOCK 81
...
```
Так клиент получает всю историю пользователя назад блок за блоком, но скачивает только конкретные нужные byte ranges.
---
# 20. Hash большого блока
В footer записывается:
```text
bytes[32] block_hash
```
`block_hash` вычисляется как:
```text
SHA256(
все байты большого блока
от magic
через header
через creator_login
через PreviousBlockReference table
через все user records и server navigation metadata
через footer closer_login_length + closer_login
)
```
Само поле `block_hash` и `closer_signature` в hash НЕ входят.
---
# 21. Footer: кто закрыл блок, hash и подпись
В конце каждого большого блока MUST быть:
```text
u8 closer_login_length
bytes[N] closer_login UTF-8
bytes[32] block_hash
bytes[64] closer_signature
```
Логические значения:
```text
1. кто сформировал/закрыл блок;
2. hash точных bytes блока;
3. подпись закрывающего аккаунта.
```
`closer_login` MUST точно совпадать с `creator_login` из header.
Если значения отличаются — блок невалиден.
---
# 22. Подпись большого блока
Алгоритм v1:
```text
Ed25519
```
Подписываемый payload:
```text
ASCII("SHINE-ARCHIVE-V1") || block_hash
```
То есть:
```text
closer_signature =
Ed25519Sign(
closer_private_key,
"SHINE-ARCHIVE-V1" || block_hash
)
```
Для первой реализации `closer_private_key` может быть root private key SHiNE-аккаунта `creator_login` / `closer_login`.
Public key для проверки берётся из соответствующего User PDA в Solana.
---
# 23. Archive head в Solana User PDA
Отдельный PDA создавать не требуется.
Используется тот же существующий User PDA.
Новый block type:
```text
100
```
Формат:
```text
u8 block_type = 100
u8 block_version = 0
bytes[32] archive_tx_id
bytes[32] archive_hash
```
Размер:
```text
66 байт
```
Solana тем самым хранит:
```text
где находится текущая голова -> Arweave TX ID
какие exact bytes являются правильными -> SHA-256
```
---
# 24. Обновление Solana — обычный User PDA update
**Отдельная Solana instruction для archive head не требуется.**
Архивный сервер использует существующий обычный механизм обновления собственного User PDA:
1. читает текущий PDA;
2. сохраняет все существующие поля;
3. меняет/добавляет только block type `100`;
4. пересобирает новую PDA-запись;
5. выполняет существующую root-signature проверку;
6. `client key` подписывает/оплачивает Solana transaction;
7. ждёт `finalized`.
Важно: все новые Java/JS/Rust сериализаторы обязаны понимать block type `100` и сохранять его при обычных обновлениях PDA.
---
# 25. `TestFreeAvatarArweaveService`
Старый сервис больше не нужен как avatar test service.
Его следует переименовать, например, в:
```text
ArweaveArchiveService
```
и переделать только под серверную фиксацию.
Нужно оставить/переиспользовать:
- JWK parsing;
- wallet address;
- reward calculation;
- balance check;
- RSA-PSS signing;
- Arweave HTTP.
Удалить avatar-specific части:
- PNG/JPEG/WebP проверки;
- avatar quota;
- лимит 128 KiB;
- avatar DAO;
- avatar-specific API.
Новый сервис должен уметь:
```text
publishArchive(...)
waitForConfirmation(...)
```
и корректно работать с большими/chunked uploads.
---
# 26. Ожидание закрепления Arweave
После получения TX ID сервер НЕ обновляет Solana сразу.
Он ждёт заданное число подтверждений.
Рекомендуемые настройки:
```properties
archive.arweave.minConfirmations=1
archive.arweave.confirmPollSeconds=30
archive.arweave.confirmTimeoutMinutes=180
```
Только после `ARWEAVE_CONFIRMED` разрешён update User PDA.
---
# 27. Порядок публикации
Строго:
```text
1. Синхронизировать/иметь актуальные локальные блоки.
2. Прочитать archive_chain_cursor.
3. Зафиксировать snapshot новых диапазонов.
4. Создать archive_publish_job.
5. Собрать большой SHINE-ARCHIVE v1.0.
6. В header записать creator_login.
7. В начало блока записать FULL_HISTORY список всех предыдущих больших блоков:
block_number + hash + Arweave TX ID.
8. Для каждого пользователя сформировать server navigation metadata:
previous big block + список всех offset/size его записей там.
9. Посчитать block_hash.
10. В footer записать closer_login, block_hash, signature.
11. Загрузить файл в Arweave.
12. Сохранить TX ID в publish job.
13. Дождаться закрепления Arweave.
14. Обычным User PDA update записать block type 100:
archive_tx_id + archive_hash.
15. Дождаться Solana finalized.
16. В одной локальной DB transaction сдвинуть archive_chain_cursor.
17. Отметить job CURSORS_COMMITTED.
```
Если новых данных нет — пустой большой блок создавать не нужно.
---
# 28. Crash recovery
## Crash после snapshot
Продолжить с теми же frozen ranges.
## Crash после построения файла
Использовать тот же файл и тот же hash.
## Crash после Arweave upload
Не загружать второй раз. Использовать сохранённый TX ID.
## Crash после Arweave confirmation
Продолжить с User PDA update.
## Crash после Solana submit
Сначала проверить текущий PDA/transaction.
## Crash после Solana finalized, но до cursor commit
Если PDA уже содержит ожидаемые:
```text
archive_tx_id
archive_hash
```
то безопасно выполнить локальный cursor commit.
---
# 29. Настройки scheduler
```properties
archive.publish.enabled=false
archive.publish.intervalMinutes=720
archive.publish.initialDelayMinutes=15
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
```
Для первой версии разумно начать с:
```text
720 минут = раз в 12 часов
```
Частота обычной межсерверной синхронизации меняется независимо.
---
# 30. Проверка большого блока клиентом
Клиент SHOULD выполнять:
```text
1. Скачать начало файла.
2. Проверить magic.
3. Прочитать major/minor.
4. Прочитать header_size.
5. Прочитать creator_login.
6. Докачать весь header + PreviousBlockReference table.
7. При необходимости получить нужные previous block TXID/hash.
8. Скачать нужные user record ranges.
9. При полной проверке файла пересчитать block_hash.
10. Проверить closer_login == creator_login.
11. Получить public key автора из User PDA.
12. Проверить Ed25519 signature.
```
---
# 31. Пример навигации Alice
Допустим клиент уже имеет актуальную запись Alice из block 100.
Server navigation metadata говорит:
```text
previous_block_ref = 94
previous_records_count = 3
[8123, 417]
[9001, 351]
[14020, 692]
```
Из начала block 100:
```text
PreviousBlockReference[94]:
block_number = 95
block_hash = H95
arweave_tx_id = TX95
```
Клиент:
```text
1. идёт в TX95;
2. проверяет H95;
3. делает Range на 8123/417;
4. Range на 9001/351;
5. Range на 14020/692;
6. получает все три Alice records из block 95;
7. из server navigation metadata последней Alice record получает следующий переход, например на block 81;
8. повторяет.
```
То есть история читается:
```text
100 -> 95 -> 81 -> 63 -> ...
```
но на каждом шаге скачиваются только конкретные записи данного пользователя.
---
# 32. Почему hash и TX ID нужны одновременно
Нельзя оставить только TX ID.
Правильная модель:
```text
block_number
-> логический номер
block_hash
-> криптографическая идентичность
arweave_tx_id
-> текущее физическое расположение
```
Если в будущем большой блок будет перенесён из Arweave в другое хранилище, его hash останется тем же.
---
# 33. Будущее `PARTIAL_HISTORY`
В первой реализации используется только:
```text
references_mode = FULL_HISTORY
```
На будущее уже зарезервирован:
```text
PARTIAL_HISTORY
```
Будущая версия сможет, например, хранить:
- последние N больших блоков;
- checkpoint-блоки;
- только реально используемые ссылки;
- многоуровневый индекс.
Но это НЕ требуется для v1.
---
# 34. Итоговые бинарные структуры v1.0
## Header
```text
bytes[13] magic = "SHINE-ARCHIVE"
u8 version_major
u8 version_minor
u32 header_size
u32 block_number
u64 created_at_ms
u8 creator_login_length
bytes[N] creator_login
u8 references_mode
u32 references_count
u16 reference_entry_size
u32 records_count
```
## PreviousBlockReference
```text
u32 block_number
bytes[32] block_hash
bytes[32] arweave_tx_id
```
Размер v1:
```text
68 bytes
```
## PreviousUserRecordsRef
```text
u32 previous_block_ref
u32 previous_records_count
repeat previous_records_count:
u32 previous_record_offset
u32 previous_record_size
```
## User archived envelope — рекомендуемая форма
```text
u32 archived_record_size
u32 raw_user_record_size
bytes[N] raw_user_record
u8 navigation_flags
[optional PreviousUserRecordsRef]
```
## Footer
```text
u8 closer_login_length
bytes[N] closer_login
bytes[32] block_hash
bytes[64] closer_signature
```
## Solana User PDA block 100
```text
u8 block_type = 100
u8 block_version = 0
bytes[32] archive_tx_id
bytes[32] archive_hash
```
---
# 35. Обязательные проверки v1
Publisher MUST:
- не изменять frozen job после snapshot;
- проверять cursor hash;
- не двигать cursor до Solana finalized;
- не публиковать пустую дельту;
- записывать полный список предыдущих больших блоков;
- хранить hash и TX ID каждого previous block;
- записывать creator login в header;
- повторять тот же login в footer;
- подписывать hash большого блока;
- добавлять для пользователя ссылку на предыдущий большой блок и offsets/sizes всех его записей там.
Reader MUST:
- проверять magic/version;
- проверять границы offsets;
- проверять hash скачанного большого блока;
- проверять `closer_login == creator_login`;
- проверять подпись;
- проверять, что скачанные user records действительно принадлежат ожидаемому пользователю;
- не считать Arweave TX ID самостоятельным доказательством корректности содержимого.
---
# 36. Реализационный checklist
## Solana
- [ ] Добавить User PDA block type `100`.
- [ ] Добавить `archive_tx_id [32]`.
- [ ] Добавить `archive_hash [32]`.
- [ ] Научить обычный User PDA update читать и сохранять block 100.
- [ ] Серверу дать возможность обычным update менять block 100.
- [ ] Ждать `finalized`.
## Database
- [ ] `archive_chain_cursor`.
- [ ] `archive_publish_job`.
- [ ] `archive_publish_job_chain`.
## Arweave
- [ ] Переименовать `TestFreeAvatarArweaveService` в `ArweaveArchiveService`.
- [ ] Удалить avatar-specific код.
- [ ] Реализовать большие/chunked uploads.
- [ ] Реализовать ожидание confirmations.
- [ ] Сохранять TX ID до ожидания подтверждений.
## Большой блок
- [ ] Magic `SHINE-ARCHIVE`.
- [ ] `version_major u8`.
- [ ] `version_minor u8`.
- [ ] `header_size`.
- [ ] `block_number u32`.
- [ ] `created_at_ms u64`.
- [ ] `creator_login` в header.
- [ ] `FULL_HISTORY` previous block table.
- [ ] `block_number + block_hash + txid` для каждого previous block.
- [ ] Server-side `PreviousUserRecordsRef`.
- [ ] Список всех `offset + size` пользовательских записей из предыдущего big block.
- [ ] `closer_login + block_hash + signature` в footer.
## Scheduler / recovery
- [ ] Настраиваемый interval.
- [ ] Initial delay.
- [ ] Один active job.
- [ ] Resume после restart.
- [ ] Cursor commit только после Solana finalized.
---
# 37. Краткая итоговая модель
```text
SOLANA USER PDA
|
+--> block 100
archive TX ID
archive SHA-256
|
v
SHINE-ARCHIVE block N
|
| HEADER:
| version
| block number
| time
| CREATOR LOGIN
|
| PREVIOUS BLOCK TABLE:
| #0 number/hash/TXID
| #1 number/hash/TXID
| ... все предыдущие в v1
|
| USER DATA:
| original signed user records
| server navigation metadata
| previous block ref
| ALL previous user record offsets/sizes
|
| FOOTER:
| same creator/closer login
| hash
| Ed25519 signature
v
SHINE-ARCHIVE block N-1
|
v
...
```
Это завершает согласованную спецификацию `SHiNE Archive Protocol v1.0`.