Files
SHiNE-server/docs/ИТХ/Спецификация_ИТХ_v1.md
T
AidarKCandClaude Fable 5 2a4c2c64c9 ИТХ: вход в график закрытий по доверию, а не по депозиту
- Из TODO убран депозитный/платный вход — сознательно отвергнут.
- Зафиксирована модель: DAO ведёт список как временный уровень,
  пока не сформировано сообщество сияющих; целевое состояние —
  приём/исключение серверов через голосование сияющих.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 19:57:12 +04:00

395 lines
29 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.
# Спецификация ИТХ 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:0006: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: <dayIndex>` (теги информационные, доверия к ним нет — истина в подписи).
### 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
<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` — фиксировать изменения.