# Спецификация ИТХ v1 — ежедневное закрытие блокчейна SHiNE Статус: дизайн согласован пользователем, реализации в коде нет. Краткое описание для людей: [README.md](./README.md). Документ рассчитан на то, чтобы по нему можно было написать код без дополнительных уточнений. Все байтовые порядки — big-endian (BE), все хэши — SHA-256 (32 байта), все подписи — Ed25519, все времена — UTC. --- ## 1. Термины | Термин | Значение | | --- | --- | | **День D** | Календарные сутки UTC `[D 00:00:00; D+1 00:00:00)`. | | **dayIndex** | Порядковый номер суток от эпохи графика: `dayIndex = (началоСуток_D − epochStart) / 86400_000`. У `epochStart` dayIndex = 0. | | **Дежурный** | Сервер, закрывающий день D: `rota.servers[dayIndex mod rota.servers.length]`. | | **Чекпоинт** | Артефакт закрытия дня: контейнер (заголовок + state-список + бандл), одним файлом в Arweave. | | **ИТХ** | Итоговый хэш дня = `stateRoot` — меркл-корень по вершинам всех цепочек. | | **Бандл** | Все блоки, не покрытые предыдущим признанным чекпоинтом. | | **checkpointHash** | SHA-256 канонического preimage заголовка (раздел 5.3). Идентификатор чекпоинта. | | **Признанный чекпоинт** | Последний чекпоинт, на который указывают PDA-слоты больше половины серверов графика. | ## 2. График дежурств (rota PDA, DAO) Отдельный PDA, создаётся и обновляется **только DAO authority** (точная реализация — в Solana-модуле `shine-solana/shine/`, отдельным решением; предлагаемые seeds: `["checkpoint_rota"]`). Логическое содержимое: ``` RotaV1 - version: u8 = 1 - epoch_start_utc_day: u32 // число суток от unix-эпохи (00:00 UTC) - servers_count: u8 - servers: [ login_len: u8, login: UTF-8 ] * servers_count // упорядоченный список ``` Правила: - Дежурный дня D определяется по состоянию rota PDA на момент `00:00 UTC дня D+1` (момент начала окна закрытия). Изменение списка не пересчитывает прошлые дни. - Вход в график — **по доверию**, не за деньги: DAO ведёт список как временный уровень, пока не сформировано сообщество сияющих; целевое состояние — приём/исключение серверов через голосование сияющих (см. TODO). - При изменении состава серверов будущее распределение сдвигается автоматически — это допустимо, календарь «дата → сервер» не ведётся. ## 3. Расписание суток (UTC) | Окно | Действие | | --- | --- | | `D+1 00:00 – 06:00` | Дежурный строит чекпоинт дня D, заливает в Arweave, обновляет свой PDA-слот (`role=closed`), публикует пост в канале закрытий. | | `D+1 06:00 – 24:00` | Остальные серверы проверяют; при успехе — свой PDA-слот (`role=agreed`) + лайк на пост. Проверять можно и раньше 06:00, как только чекпоинт появился. | - Нет валидного чекпоинта дня D к началу следующего окна закрытия → день D **пропущен**: никто не меняет свои слоты; дежурный дня D+1 включает блоки за все непокрытые дни и ссылается `prev`-полями на последний признанный чекпоинт. - Сервер, отстававший несколько дней, догоняет **последовательно**: проверяет цепочку чекпоинтов по одному (каждый — относительно предыдущего) и в конце ставит свой слот на последний проверенный. Опоздавшее согласие допустимо в любой момент. ## 4. Что попадает в бандл (правило среза) - Срез = момент начала сборки чекпоинта дежурным (внутри окна `00:00–06:00`). - В бандл входят все блоки, известные дежурному на момент среза и **не покрытые предыдущим признанным чекпоинтом**: для каждой цепочки `c` — блоки с номера `prevState[c].lastNumber + 1` до локальной вершины; новые цепочки — целиком, с первого блока (номер `0`). - Блоки, созданные уже утром D+1 до среза, допустимо включать — это не ошибка. - Полнота относительно других серверов **не требуется**: блоки, которых у дежурного не было, войдут в следующий чекпоинт. Проверяющие не считают «у меня есть больше» причиной несогласия. - **Выбор ветки (fork-choice дежурного)**: если из вершины `prevState[c]` растут несколько валидных веток (пользователь подписал противоречащие блоки), дежурный берёт самую длинную валидную; при равной длине — ветку с меньшим `hash32` первого расходящегося блока (байтовое сравнение). Ветки, не растущие из `prevState[c]`, кандидатами не являются. Подробнее о форках — раздел 10. ## 5. Формат чекпоинта (один файл / одна Arweave-транзакция) ### 5.1. Контейнер ``` magic: 8 байт ASCII "SHNCKPT1" headerLen: u32 BE — длина headerJson в байтах headerJson: UTF-8 JSON (раздел 5.2) bundle: записи бандла подряд (раздел 5.4), суммарной длиной до конца файла ``` Рекомендуемые Arweave-теги: `App-Name: SHiNE`, `Type: checkpoint`, `Day-Index: ` (теги информационные, доверия к ним нет — истина в подписи). ### 5.2. headerJson ```json { "v": 1, "dayIndex": 123, "dayDate": "2026-07-12", "closedBy": "serverlogin", "closedAtMs": 1789200000000, "serverPubKeyB58": "…", "prevCheckpointHashHex": "…64 hex… | null", "prevArweaveTxId": "… | null", "stateRootHex": "…64 hex…", "stateCount": 3500, "bundleRootHex": "…64 hex…", "bundleCount": 4200, "state": [ { "name": "alice-001", "lastNumber": 17, "lastHashHex": "…64 hex…" } ], "signatureB64": "…" } ``` - `state` отсортирован по `name` **байтово по UTF-8, по возрастанию**, без дублей. - `dayDate` — дата дня D (для читаемости; проверяется соответствие `dayIndex`). - Для генезис-чекпоинта `prevCheckpointHashHex` и `prevArweaveTxId` равны `null`, `prevState` считается пустым, бандл содержит всю историю. - Все hex — в нижнем регистре. ### 5.3. Канонический preimage, checkpointHash и подпись Preimage — UTF-8 строка из 9 строк, разделитель `\n` (без завершающего `\n`; `null`-значения кодируются как `-`): ``` SHiNE-CHECKPOINT-V1 ``` - `checkpointHash = SHA-256(preimage)`. - `signatureB64 = Ed25519(preimage)` актуальным клиентским ключом дежурного сервера (ключ из его `user_pda`, действующий на момент закрытия; `serverPubKeyB58` его дублирует). - `checkpointHash` не зависит от форматирования JSON — только от полей preimage. ### 5.4. Запись бандла Записи идут подряд, отсортированы по `(name байтово, blockNumber)` по возрастанию: ``` nameLen: u16 BE name: UTF-8 (blockchainName) blockNumber: u64 BE blockLen: u32 BE blockBytes: полный блок как в хранилище: preimage + sigMarker + signature64 ``` `bundleCount` в заголовке = число записей; парсер обязан проверить, что записи заканчиваются ровно на конце файла. ### 5.5. Меркл-правила (общие для state и бандла) - Лист: `SHA-256(0x00 || данные листа)`. - Узел: `SHA-256(0x01 || left32 || right32)`. - При нечётном числе узлов на уровне последний **поднимается без изменения**. - Пустое дерево → 32 нулевых байта. Данные листа: - **state** (порядок листьев = порядок `state`): `nameBytes || 0x00 || lastNumber u64 BE || lastHash32`. Корень = `stateRoot` = **ИТХ**. - **бандл** (порядок листьев = порядок записей): полные байты записи из 5.4 (включая nameLen/name/blockNumber/blockLen). Корень = `bundleRoot`. ## 6. Слот чекпоинта в PDA сервера Новый типизированный блок в `user_pda` сервера (рядом с `ServerProfileBlock (30)`; финальный номер `block_type` и правки `shine-user-pda-format-v.1.0.md` — при реализации Solana-части, отдельным решением). Предложение: ``` CheckpointStateBlock (block_type = 60, block_version = 0) - day_index: u32 - checkpoint_hash: [32] - arweave_tx_len: u8, arweave_tx: ASCII - closed_by_len: u8, closed_by: UTF-8 - my_role: u8 // 1 = closed (я закрывал), 2 = agreed (я согласен) - updated_at_ms: u64 ``` Правила: - Слот **один**, перезаписывается на месте; истории в PDA нет (история — цепочка чекпоинтов в Arweave и посты в канале). - Дежурный после заливки ставит свой чекпоинт с `my_role=1`. - Проверивший и согласный сервер ставит тот же `(day_index, checkpoint_hash, arweave_tx, closed_by)` с `my_role=2`. - **Несогласие = не трогать слот.** Отдельной записи «против» нет. Правило признания (для наблюдателей и восстановления): > Актуальный чекпоинт = последний (максимальный `day_index`), на который указывают > одинаковые `(day_index, checkpoint_hash)` в слотах **больше половины** серверов > из rota PDA. Рекомендация клиентам: до достижения половины считать актуальным > предыдущий признанный. Правило признания — правило **чтения**, а не записи: - дефолтное множество для подсчёта — список rota PDA; - любой наблюдатель (клиент, сторонний сервер, восстановление) вправе применять то же правило к **собственному списку доверенных серверов**; DAO-список — рекомендация по умолчанию (конфиг сервера: `checkpoint.trust.logins`; пусто = rota PDA); - серверы графика при закрытии и проверке обязаны использовать общий DAO-список, иначе ротация теряет общую точку отсчёта. Коротко: **запись — по общему списку, чтение — по любому**. Параллельные группы закрытий допустимы by design: любая другая группа серверов может вести собственную цепочку чекпоинтов над теми же данными (свой генезис, свои PDA-слоты, свои Arweave-транзакции). Группы механически не пересекаются (разные `prev`-цепочки) и не могут подделать содержимое блоков — только заверять историю или не включать её часть. Запрещать это кодом сознательно не планируется; «официальность» DAO-группы держится только на том, что её список — дефолт в клиентах. ## 7. Канал закрытий в Сиянии - У каждого сервера графика — свой публичный канал `type=1`, slug `checkpoints` (создаётся обычным `TECH_CREATE_CHANNEL`). - При закрытии дежурный пишет в свой канал `TEXT_POST`: ``` /.checkpoint ``` - Согласие другого сервера — `REACTION_LIKE`, таргетирующий блок этого поста. Отзыв согласия — `REACTION_UNLIKE`. Комментарии не требуются. - Смысл: человекочитаемое зеркало + криптографическая проверяемость через Arweave (пост и лайки подписаны ключами серверов, зарегистрированными в Solana), с лагом в один день (попадут в следующий бандл). - Машинный якорь «здесь и сейчас» — PDA-слоты; канал вторичен. - При реализации обязательно дописать команду `/.checkpoint` в `docs/Blockchain/02_Channel_Commands.md`. ## 8. Алгоритм дежурного (закрытие дня D) 1. В окне `D+1 00:00–06:00` убедиться по rota PDA, что дежурный — я. 2. Определить предыдущий признанный чекпоинт (свой слот + слоты остальных, раздел 6); загрузить его `state` (локальный кэш или Arweave). Для генезиса — пустой. 3. Зафиксировать срез: для каждой цепочки — вершина выбранной ветки `(lastNumber, lastHash)` по правилу fork-choice из раздела 4. 4. Собрать бандл по разделу 4, отсортировать по 5.4, посчитать `bundleRoot`. 5. Сформировать `state` по срезу, отсортировать, посчитать `stateRoot` (ИТХ). 6. Собрать preimage, `checkpointHash`, подписать серверным ключом. 7. Залить контейнер одной Arweave-транзакцией (JWK сервера с хоста, вне git), дождаться подтверждения, получить `arweaveTxId`. 8. Обновить свой PDA-слот: `(dayIndex, checkpointHash, arweaveTxId, closedBy=я, role=1)`. 9. Опубликовать пост `/.checkpoint …` в своём канале закрытий. 10. Любая ошибка на шагах 7–9 → повторять до конца окна; не успел — день пропущен, ничего не публиковать частично (PDA-слот и пост ставить только после успешного Arweave). ## 9. Алгоритм проверяющего Вход: обнаружен новый чекпоинт дня D от сервера S (по его PDA-слоту и/или посту). 1. **График**: `S == rota[dayIndex mod N]` по состоянию rota PDA на `00:00 D+1`; `dayIndex` больше `day_index` последнего признанного локально чекпоинта. 2. Скачать Arweave-транзакцию из слота S; проверить `magic`, длины, парсинг. 3. Пересчитать preimage → `checkpointHash`; сверить со слотом S (и постом, если есть). 4. Проверить `signatureB64` ключом S (актуальный клиентский ключ из `user_pda` S; допускается ключ, действовавший в день закрытия — см. TODO про ротацию). 5. `prevCheckpointHashHex`/`prevArweaveTxId` == локально признанный крайний чекпоинт. Иначе — отказ. 6. Пересчитать `bundleRoot` по записям; сверить. Проверить сортировку и `bundleCount`. 7. **Мостик prev→state** (полнота бандла): для каждой цепочки из `state` блоки бандла образуют непрерывную последовательность номеров и `prevHash`-ссылок от `prevState[c]` (или от блока 0 для новой цепочки) ровно до `state[c]`. Для каждого блока: `hash32 == SHA-256(preimage)`, структура валидна по правилам `docs/Blockchain/01_Common_Block_Format.md`, подпись Ed25519 валидна ключом владельца цепочки, действующим на день закрытия. 8. Пересчитать `stateRoot` по `state`; сверить (это и есть проверка ИТХ). 9. **Конфликты с локальными данными**: если локально для `(цепочка, номер)` есть блок с другим `hash32` — побеждает версия чекпоинта: локальная ветка отбрасывается (реорганизация), согласие НЕ отклоняется. Отказ по конфликту возможен только когда чекпоинт противоречит уже признанному прошлому — это ловится шагом 7 (мостик не соединится с `prevState`). «У меня есть блоки, которых нет в бандле» — тоже НЕ отказ. 10. Импортировать недостающие блоки бандла в локальное хранилище тем же валидационным путём, что и межсерверная синхронизация (`sync-between-servers.md`). 11. Успех → обновить свой PDA-слот (`role=2`) и поставить лайк на пост S. Отказ → не делать ничего on-chain; подробно залогировать причину (рекомендуемый префикс лога: `CHECKPOINT_REJECT`). Проверка детерминирована: два честных сервера с любым набором локальных блоков приходят к одному вердикту. Проверяется валидность выбора дежурного и стыковка с прошлым чекпоинтом, но не «длиннейшесть» выбранной ветки — её никто, кроме дежурного, проверить не может, и условием согласия она не является. ## 10. Форки пользовательских цепочек Форк = два блока одной цепочки с одинаковым `blockNumber`, разным `hash32` и валидными подписями владельца. Случайно-честно такое не возникает: каждый блок подписывается владельцем и ссылается на `prevHash`. Пара таких блоков — самодостаточная криптографическая улика, проверяемая любым сервером локально, без арбитра и голосования. Лесенка разруливания: | Ситуация | Арбитр | Исход | | --- | --- | --- | | Ветки разошлись до чекпоинта | Дежурный (длиннейшая + tie-break, раздел 4) | Проигравшая ветка отбрасывается, цепочка живёт | | Локальная ветка против зачекпоинченной в своей группе | Цепочка чекпоинтов | Локальная ветка отбрасывается (шаг 9.9) | | Чекпоинт противоречит признанному прошлому | Проверяющие | Согласий нет, день пропущен | | Противоречие между архивами двух групп закрытий | Никто — улика | Заморозка цепочки | Заморозка применяется **только** в последнем случае — когда обе ветки уже зачекпоинчены разными группами закрытий и неизменны в Arweave: слить два неизменных архива невозможно в принципе. - Улика: два конфликтующих блока, каждый из которых входит в валидный чекпоинт своей группы. - Сервер, проверивший улику, перестаёт принимать `AddBlock` в эту цепочку (код отказа: `chain_frozen`); улика синхронизируется партнёрам как обычные данные, каждый проверяет и замораживает независимо. - Уже записанная история цепочки остаётся валидной и читаемой навсегда. - Пользователь продолжает с новой цепочкой `-` (например, `login-002`); `BlockchainRegistryBlock` в `user_pda` уже поддерживает несколько цепочек. Старые связи/подписки указывают на корни старой цепочки — репутационная история не переносится, это осознанная часть последствий. Обязательное правило для клиентов (защищает честных пользователей от гонок): повтор отправки блока после таймаута/ошибки — только **байт-в-байт** тем же блоком. Байт-идентичный дубль — дедупликация, а не форк. Пересборка блока с новым `timestamp` на тот же номер создаёт форк на ровном месте. ## 11. Восстановление сервера с нуля 1. Прочитать rota PDA → список серверов. 2. Прочитать их PDA-слоты → признанный чекпоинт (правило раздела 6). 3. От него по `prevArweaveTxId` спуститься до генезиса, затем раскатать бандлы от генезиса вверх, валидируя каждый чекпоинт по разделу 9 (шаги 2–8). 4. Хвост после последнего чекпоинта добрать живой синхронизацией с серверами. ## 12. Стоимость и ключи - Каждый сервер платит сам: Arweave — за свои дежурные дни, Solana — за свои слоты. - Arweave JWK сервера хранится только на хосте (например, `/home/player/SHiNE/secrets/…`), путь задаётся конфигом; в git не попадает — по той же схеме, что `test.freeAvatar.walletJwkPath`. ## 13. Ориентир для реализации на сервере (не нормативно) - `CheckpointScheduler` — таймер окон 00:00/06:00 UTC, определение роли по rota PDA. - `CheckpointBuilder` — срез, бандл, меркл, preimage, подпись (разделы 4–5, 8). - `ArweaveCheckpointUploader` — заливка контейнера, ожидание подтверждения. - `CheckpointVerifier` — раздел 9; переиспользовать валидацию блоков из `AddBlock`/sync. - `CheckpointSolanaSlot` — чтение/запись `CheckpointStateBlock`, чтение rota PDA. - `CheckpointChannelPublisher` — пост `/.checkpoint` и лайки через обычный `AddBlock`. - Конфиги (предложение): `checkpoint.enabled`, `checkpoint.arweave.jwkPath`, `checkpoint.rota.pda` (или деривация по seeds), `checkpoint.closeWindowHours=6`. - Локально хранить: `state` последнего признанного чекпоинта + его `(dayIndex, hash, arweaveTxId)` — чтобы не ходить в Arweave на каждое закрытие. ## 14. TODO / сознательно отложено - **История ключей при ротации**: если сервер/пользователь меняет ключ несколько раз за день, для проверки старых подписей нужна история версий ключей. v1 живёт с «ключ, актуальный на день закрытия»; при внедрении ротации — хранить версии. - **Механизм новой цепочки `-002`**: пользовательский flow «продолжить с новой цепочкой» после заморозки (регистрация в `BlockchainRegistryBlock`, UI) пока не реализован — см. раздел 10. - **Детерминированный выбор дежурного**: `дежурный = SHA-256(prevCheckpointHash || dayIndex) mod N` по байтово отсортированным логинам — убирает порядок списка как рычаг управления, DAO остаётся только вход/выход участников. - **Передача ведения графика сообществу**: вход в группу закрытий — по доверию, не за деньги. Сейчас список ведёт DAO — это временный уровень, пока сообщество сияющих не сформировано; в перспективе приём/исключение серверов — через голосование сияющих. Депозитный/платный вход сознательно отвергнут. - **Автопропуск штрафника** в ротации (2 подряд закрытия без единого согласия). - **On-chain кворум** вместо правила чтения «>половины» — если понадобится. - **Сжатие бандла** (gzip) и **чанкование** очень больших бандлов на несколько Arweave-транзакций с манифестом. v1 — один несжатый файл. - Экономия Solana-транзакций: считать согласия только по лайкам канала — возможно после обкатки v1. ## 15. Связанные документы (обновлять при реализации) - `docs/Blockchain/01_Common_Block_Format.md` — формат блока (используется в 5.4, 9.7). - `docs/Blockchain/02_Channel_Commands.md` — дописать `/.checkpoint`. - `docs/Blockchain/sync-between-servers.md` — путь импорта блоков (9.10). - `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` — дописать `CheckpointStateBlock` и rota PDA при реализации Solana-части. - `docs/API/04_Add_Block_to_Blockchain_API.md` — при реализации дописать код отказа `chain_frozen` (раздел 10). - `docs/Blockchain/CHANGELOG.md` — фиксировать изменения.