30 KiB
Спецификация ИТХ 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:00–06: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, slugcheckpoints(создаётся обычным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)
- В окне
D+1 00:00–06:00убедиться по rota PDA, что дежурный — я. - Определить предыдущий признанный чекпоинт (свой слот + слоты остальных, раздел 6);
загрузить его
state(локальный кэш или Arweave). Для генезиса — пустой. - Зафиксировать срез: для каждой цепочки — вершина выбранной ветки
(lastNumber, lastHash)по правилу fork-choice из раздела 4. - Собрать бандл по разделу 4, отсортировать по 5.4, посчитать
bundleRoot. - Сформировать
stateпо срезу, отсортировать, посчитатьstateRoot(ИТХ). - Собрать preimage,
checkpointHash, подписать серверным ключом. - Залить контейнер одной Arweave-транзакцией (JWK сервера с хоста, вне git),
дождаться подтверждения, получить
arweaveTxId. - Обновить свой PDA-слот:
(dayIndex, checkpointHash, arweaveTxId, closedBy=я, role=1). - Опубликовать пост
/.checkpoint …в своём канале закрытий. - Любая ошибка на шагах 7–9 → повторять до конца окна; не успел — день пропущен, ничего не публиковать частично (PDA-слот и пост ставить только после успешного Arweave).
9. Алгоритм проверяющего
Вход: обнаружен новый чекпоинт дня D от сервера S (по его PDA-слоту и/или посту).
- График:
S == rota[dayIndex mod N]по состоянию rota PDA на00:00 D+1;dayIndexбольшеday_indexпоследнего признанного локально чекпоинта. - Скачать Arweave-транзакцию из слота S; проверить
magic, длины, парсинг. - Пересчитать preimage →
checkpointHash; сверить со слотом S (и постом, если есть). - Проверить
signatureB64ключом S (актуальный клиентский ключ изuser_pdaS; допускается ключ, действовавший в день закрытия — см. TODO про ротацию). prevCheckpointHashHex/prevArweaveTxId== локально признанный крайний чекпоинт. Иначе — отказ.- Пересчитать
bundleRootпо записям; сверить. Проверить сортировку иbundleCount. - Мостик prev→state (полнота бандла): для каждой цепочки из
stateблоки бандла образуют непрерывную последовательность номеров иprevHash-ссылок отprevState[c](или от блока 0 для новой цепочки) ровно доstate[c]. Для каждого блока:hash32 == SHA-256(preimage), структура валидна по правиламdocs/Blockchain/01_Common_Block_Format.md, подпись Ed25519 валидна ключом владельца цепочки, действующим на день закрытия. - Пересчитать
stateRootпоstate; сверить (это и есть проверка ИТХ). - Конфликты с локальными данными: если локально для
(цепочка, номер)есть блок с другимhash32— побеждает версия чекпоинта: локальная ветка отбрасывается (реорганизация), согласие НЕ отклоняется. Отказ по конфликту возможен только когда чекпоинт противоречит уже признанному прошлому — это ловится шагом 7 (мостик не соединится сprevState). «У меня есть блоки, которых нет в бандле» — тоже НЕ отказ. - Импортировать недостающие блоки бандла в локальное хранилище тем же валидационным
путём, что и межсерверная синхронизация (
sync-between-servers.md). - Успех → обновить свой 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. Восстановление сервера с нуля
- Прочитать rota PDA → список серверов.
- Прочитать их PDA-слоты → признанный чекпоинт (правило раздела 6).
- От него по
prevArweaveTxIdспуститься до генезиса, затем раскатать бандлы от генезиса вверх, валидируя каждый чекпоинт по разделу 9 (шаги 2–8). - Хвост после последнего чекпоинта добрать живой синхронизацией с серверами.
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— фиксировать изменения.