SHA256
1353 lines
38 KiB
Markdown
1353 lines
38 KiB
Markdown
# 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`.
|