Files
SHiNE-server/docs/ИТХ/Спецификация_ИТХ_v1.md
T

30 KiB
Raw Blame History

Спецификация ИТХ v1 — ежедневное закрытие блокчейна SHiNE

Статус: дизайн согласован пользователем, реализации в коде нет. Краткое описание для людей: 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:0006:00).
  • В бандл входят все блоки, известные дежурному на момент среза и не покрытые предыдущим признанным чекпоинтом: для каждой цепочки c — блоки с номера prevState[c].lastNumber + 1 до локальной вершины; новые цепочки — целиком, с первого блока (номер 0).
  • Блоки, созданные уже утром D+1 до среза, допустимо включать — это не ошибка.
  • Полнота относительно других серверов не требуется: блоки, которых у дежурного не было, войдут в следующий чекпоинт. Проверяющие не считают «у меня есть больше» причиной несогласия.
  • Выбор ветки (fork-choice дежурного): если из вершины prevState[c] растут несколько валидных веток (пользователь подписал противоречащие блоки), дежурный берёт самую длинную валидную; при равной длине — ветку с меньшим hash32 первого расходящегося блока (байтовое сравнение). Ветки, не растущие из prevState[c], кандидатами не являются. Подробнее о форках — раздел 10.

5. Формат чекпоинта (один файл / одна Arweave-транзакция)

5.1. Контейнер

magic:      16 байт ASCII "SHiNE-CHECKPOINT" (без завершающего нуля)
headerLen:  u32 BE — длина headerJson в байтах
headerJson: UTF-8 JSON (раздел 5.2)
bundle:     записи бандла подряд (раздел 5.4), суммарной длиной до конца файла

Магик читаемый — открыв файл в hex-редакторе, видно, что это (по образцу "SQLite format 3\0"). Версия формата в магик не входит: она в поле v заголовка и в первой строке preimage (SHiNE-CHECKPOINT-V1).

Рекомендуемые Arweave-теги: App-Name: SHiNE, Type: checkpoint, Day-Index: <dayIndex> (теги информационные, доверия к ним нет — истина в подписи).

5.2. headerJson

{
  "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
<dayIndex>
<dayDate>
<closedBy>
<prevCheckpointHashHex | ->
<prevArweaveTxId | ->
<stateRootHex>
<bundleRootHex>
<closedAtMs>
  • 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 <dayIndex> <checkpointHashHex> <arweaveTxId>
    
  • Согласие другого сервера — REACTION_LIKE, таргетирующий блок этого поста. Отзыв согласия — REACTION_UNLIKE. Комментарии не требуются.

  • Смысл: человекочитаемое зеркало + криптографическая проверяемость через Arweave (пост и лайки подписаны ключами серверов, зарегистрированными в Solana), с лагом в один день (попадут в следующий бандл).

  • Машинный якорь «здесь и сейчас» — PDA-слоты; канал вторичен.

  • При реализации обязательно дописать команду /.checkpoint в docs/Blockchain/02_Channel_Commands.md.

8. Алгоритм дежурного (закрытие дня D)

  1. В окне D+1 00:0006: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>-<NNN+1> (например, 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 — фиксировать изменения.