Files
SHiNE-server/docs/SHINE_ARCHIVE_PROTOCOL_v1.0_RU_FINAL.md
T
2026-09-12 10:17:23 +03:00

1620 lines
45 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
## Полная спецификация серверной архивации SHiNE-блоков в Arweave и фиксации головы архива в Solana
**Статус:** согласованная спецификация v1.0
**Язык:** русский
**Magic:** `SHINE-ARCHIVE`
**Версия протокола:** `1.0`
**Endian:** big-endian для всех фиксированных целых чисел
---
# 1. Назначение
Этот документ описывает механизм, по которому SHiNE-сервер периодически:
1. синхронизирует обычные SHiNE-блоки существующим механизмом;
2. определяет новые локальные блоки, ещё не попавшие в архив;
3. фиксирует неизменяемую дельту;
4. группирует все новые записи одной `blockchain_name` в один блок пользователя внутри большого архивного блока;
5. добавляет к каждому такому пользовательскому блоку одну ссылку на предыдущий пользовательский блок этой же `blockchain_name`;
6. формирует один большой бинарный блок `SHINE-ARCHIVE`;
7. загружает его в Arweave;
8. ждёт закрепления/подтверждения Arweave-транзакции;
9. обычным обновлением своего существующего User PDA записывает новый archive head в Solana;
10. только после Solana `finalized` переводит локальные курсоры на новую точку.
На первом этапе предполагается один архивный Raspberry-сервер, но формат изначально не должен запрещать появление других архиваторов.
---
# 2. Основные понятия
## 2.1. Исходный SHiNE-блок / запись
Это существующие в серверной БД оригинальные `block_bytes`, относящиеся к конкретной `blockchain_name`.
Они уже содержат пользовательские данные и существующую криптографию SHiNE.
Архиватор:
- не изменяет их;
- не переподписывает их;
- не переводит их в Base64;
- не пересобирает их содержимое.
## 2.2. `blockchain_name`
В архивном формате основной идентификатор пользовательской цепочки — именно `blockchain_name`.
Отдельно хранить `login` для пользовательского блока не требуется.
`blockchain_name` уже однозначно идентифицирует конкретную пользовательскую blockchain и логически включает пользовательскую идентичность + номер/вариант цепочки.
## 2.3. Большой архивный блок
Один файл/объект `SHINE-ARCHIVE`, который сервер формирует за один цикл архивной публикации.
Он содержит:
- header;
- список предыдущих больших архивных блоков;
- по одному `UserBlockchainChunk` для каждой `blockchain_name`, у которой есть новые записи;
- footer;
- SHA-256 большого блока;
- подпись сервера, который этот большой блок сформировал и закрыл.
Один большой архивный блок после публикации соответствует одной Arweave-транзакции.
## 2.4. `UserBlockchainChunk`
В одном большом архивном блоке для одной `blockchain_name` существует максимум один такой chunk.
Если за текущую дельту у пользователя/цепочки накопилось 1, 3, 5 или 20 записей, все они помещаются внутрь одного `UserBlockchainChunk`.
Навигационная ссылка одна на весь chunk.
---
# 3. Главная модель пользовательской истории
Допустим `alice-001` имела записи:
```text
BigBlock 70:
AliceChunk = 2 записи
BigBlock 81:
AliceChunk = 5 записей
BigBlock 95:
AliceChunk = 3 записи
BigBlock 100:
AliceChunk = 4 записи
```
Тогда история `alice-001` выглядит:
```text
BigBlock 100
AliceChunk
4 записи
|
+--> PreviousBlockchainChunkRef
|
v
BigBlock 95
AliceChunk
3 записи
|
+--> PreviousBlockchainChunkRef
|
v
BigBlock 81
AliceChunk
5 записей
|
v
BigBlock 70
```
Навигация идёт пачками записей одной blockchain, а не по одной записи за раз.
---
# 4. Сервер сам добавляет навигацию
Пользователь не обязан знать:
- в какой большой архивный блок попадут его записи;
- какой будет Arweave TX ID;
- какие будут offsets;
- где находится предыдущий chunk.
Все эти данные сервер добавляет при формировании большого блока.
Исходные подписанные пользовательские `block_bytes` остаются неизменными.
`PreviousBlockchainChunkRef` является внешней серверной архивной метаинформацией и защищается hash + подписью всего большого архивного блока.
---
# 5. Необходимые ключи архивного сервера
Архивный Raspberry/server хранит:
## 5.1. SHiNE root private key
Используется:
- для подписания новой версии User PDA при обычном `update_user_pda`;
- в v1.0 — для подписи закрытого большого архивного блока.
## 5.2. SHiNE client private key
Используется как Solana transaction signer / fee payer в соответствии с текущей логикой `shine_users`.
## 5.3. Arweave JWK / private wallet key
Используется для загрузки и оплаты Arweave-транзакции.
## 5.4. Blockchain private key
Для архивной публикации не требуется.
Архиватор не создаёт новые пользовательские blockchain-блоки.
---
# 6. Расписание публикации
Архивная публикация запускается один раз в сутки в заданное локальное время. Она НЕ использует интервал «каждые 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.