SHA256
1620 lines
45 KiB
Markdown
1620 lines
45 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` переводит локальные курсоры на новую точку.
|
||
|
||
На первом этапе предполагается один архивный server, но формат изначально не должен запрещать появление других архиваторов.
|
||
|
||
---
|
||
|
||
# 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. Необходимые ключи архивного сервера
|
||
|
||
Архивный 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. Расписание публикации
|
||
|
||
Архивная публикация запускается один раз в сутки в заданное локальное время. Она НЕ использует интервал «каждые N минут».
|
||
|
||
Пример:
|
||
|
||
```properties
|
||
archive.publish.enabled=true
|
||
archive.publish.time=00:00
|
||
archive.publish.zoneId=
|
||
```
|
||
|
||
Для v1.0 значение по умолчанию:
|
||
|
||
```text
|
||
00:00
|
||
```
|
||
|
||
То есть новый snapshot и новый большой архивный блок создаются один раз в сутки в полночь.
|
||
|
||
Если `archive.publish.zoneId` пуст, используется системная timezone сервера. При необходимости её можно задать явно, например `Europe/Warsaw`. Это сохраняет публикацию ровно в указанное локальное время даже при переходах летнего/зимнего времени.
|
||
|
||
Незавершённый archive job после рестарта не ждёт следующей полуночи: сервер продолжает именно его сразу. Новый snapshot при старте вне назначенного времени не создаётся.
|
||
|
||
## 6.1. Первая архивная публикация
|
||
|
||
Если у данного archive publisher ещё нет подтверждённых архивных курсоров, первая публикация берёт ВСЁ локально известное состояние:
|
||
|
||
```text
|
||
для каждой blockchain_name:
|
||
source block 0 .. current local head
|
||
```
|
||
|
||
То есть первый большой архивный блок содержит все SHiNE-блоки, которые сервер успел узнать к моменту первого суточного snapshot. После успешной публикации следующие большие блоки содержат только дельту относительно подтверждённых курсоров.
|
||
|
||
## 6.2. Локальная папка и имена файлов
|
||
|
||
Перед любой сетевой загрузкой большой блок сначала полностью создаётся на локальном диске. По умолчанию каталог:
|
||
|
||
```text
|
||
data/archive/
|
||
```
|
||
|
||
До получения Arweave TX ID файл имеет временное имя:
|
||
|
||
```text
|
||
<login>.<00001>.<дд.мм.гг>.tmp.SHiNE-archive
|
||
```
|
||
|
||
Например:
|
||
|
||
```text
|
||
archive01.00001.11.09.26.tmp.SHiNE-archive
|
||
```
|
||
|
||
Дата — реальная дата создания/freeze snapshot большого блока в timezone archive publisher-а. Номер имеет минимальную ширину 5 цифр. Пять цифр — форматирование, а не лимит: блок `100000` получает шестизначный номер.
|
||
|
||
После успешной загрузки Arweave возвращает реальный TX ID. Сервер сначала надёжно сохраняет TX ID в БД, затем атомарно переименовывает тот же локальный файл в:
|
||
|
||
```text
|
||
<login>.<00001>.<дд.мм.гг>.<ARWEAVE_TX_ID>.SHiNE-archive
|
||
```
|
||
|
||
Например:
|
||
|
||
```text
|
||
archive01.00001.11.09.26.Xm32...kP9.SHiNE-archive
|
||
```
|
||
|
||
Поле `<ARWEAVE_TX_ID>` — не слово `trx`, а настоящий Base64URL TX ID загруженного объекта в Arweave. Финальный файл остаётся локально как постоянная копия.
|
||
|
||
Crash recovery обязан продолжать работу с этим же файлом. Если TX ID уже сохранён, но процесс упал до rename, при следующем запуске сервер вычисляет финальное имя из сохранённого TX ID и завершает переименование без повторной сборки дельты.
|
||
|
||
---
|
||
|
||
# 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
|
||
```
|
||
|
||
Если собранный frozen job превышает этот лимит, v1.0 останавливает публикацию с явной ошибкой `ArchiveTooLargeException` и не двигает курсоры. Практически лимит очень велик; для такого сервера следует уменьшить объём данных между суточными закрытиями или реализовать деление snapshot на несколько big blocks. Автоматическое деление одного snapshot на несколько big blocks оставлено как совместимое будущее расширение.
|
||
|
||
---
|
||
|
||
# 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
|
||
first = 1
|
||
next = 2
|
||
next = 3
|
||
...
|
||
```
|
||
|
||
Первый реально публикуемый большой блок имеет номер `1`, поэтому его локальное имя содержит `00001`. Неудачная незавершённая попытка не становится частью опубликованной archive-цепочки.
|
||
|
||
---
|
||
|
||
# 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:
|
||
#1
|
||
#2
|
||
#3
|
||
...
|
||
#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.
|
||
|
||
Для первого большого блока (`#1`), у которого нет родителя:
|
||
|
||
```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` удаляется из активного протокола; archive publisher использует отдельный `ArweaveArchiveService`.
|
||
|
||
Его следует переделать/переименовать, например в:
|
||
|
||
```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.time=00:00
|
||
archive.publish.zoneId=
|
||
archive.workDir=data/archive
|
||
|
||
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
|
||
|
||
# Отдельного archive.solana.rpcUrl нет.
|
||
# Используется solana.users.sync.rpcUrl, а если он пуст — обычный solana.rpcUrl.
|
||
```
|
||
|
||
---
|
||
|
||
# 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
|
||
|
||
- [x] `archive_chain_cursor`
|
||
- [x] `archive_publish_job`
|
||
- [x] `archive_publish_job_chain`
|
||
- [x] schema migration
|
||
- [x] crash recovery
|
||
|
||
## Archive writer
|
||
|
||
- [x] magic `SHINE-ARCHIVE`
|
||
- [x] major/minor version
|
||
- [x] fixed header
|
||
- [x] creator login
|
||
- [x] FULL previous big block table
|
||
- [x] one chunk per `blockchain_name`
|
||
- [x] many raw records inside one chunk
|
||
- [x] one backlink per chunk
|
||
- [x] closer login
|
||
- [x] SHA-256
|
||
- [x] Ed25519 signature
|
||
- [x] max file size < 4 GiB
|
||
- [x] local `.tmp.SHiNE-archive -> .<ArweaveTX>.SHiNE-archive` lifecycle in `data/archive`
|
||
|
||
## Arweave
|
||
|
||
- [x] rename/refactor `TestFreeAvatarArweaveService`
|
||
- [x] remove avatar-specific logic
|
||
- [x] large/chunked upload
|
||
- [x] confirmation polling
|
||
|
||
## Solana
|
||
|
||
- [x] add PDA block type `100`
|
||
- [x] update Rust codec
|
||
- [x] update Java codec
|
||
- [x] update JS codec/UI writer
|
||
- [x] use ordinary `update_user_pda`
|
||
- [x] server-side transaction writer
|
||
- [x] root signature
|
||
- [x] client fee payer
|
||
- [x] wait for `finalized`
|
||
|
||
## Scheduler
|
||
|
||
- [x] `archive.publish.enabled`
|
||
- [x] `archive.publish.time`
|
||
- [x] `archive.publish.zoneId`
|
||
- [x] no concurrent jobs
|
||
- [ ] future: автоматическое split > max size (v1.0 сейчас безопасно останавливается без cursor commit)
|
||
- [x] 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
|
||
| ref[0] -> BigBlock #1 / hash / TX
|
||
| ref[1] -> BigBlock #2 / hash / TX
|
||
| ...
|
||
| ref[98] -> 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.
|