# 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 .<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 .<00001>.<дд.мм.гг>..SHiNE-archive ``` Например: ```text archive01.00001.11.09.26.Xm32...kP9.SHiNE-archive ``` Поле `` — не слово `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 -> ..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.