SHA256
Обновить UI чатов и связей и добавить документацию ИТХ
This commit is contained in:
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.310
|
client.version=1.2.311
|
||||||
server.version=1.2.289
|
server.version=1.2.290
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# История изменений документации блокчейна
|
# История изменений документации блокчейна
|
||||||
|
|
||||||
|
## 2026-07-13 14:04:59 +0400
|
||||||
|
- Базовый коммит-ориентир: `af12d7b`.
|
||||||
|
- Добавлена новая папка документации `docs/ИТХ/` — ежедневное закрытие блокчейна (чекпоинты):
|
||||||
|
- `docs/ИТХ/README.md` — краткий обзор для людей;
|
||||||
|
- `docs/ИТХ/Спецификация_ИТХ_v1.md` — подробная спецификация для реализации
|
||||||
|
(формат чекпоинта в Arweave, меркл-правила ИТХ, rota PDA DAO, слот `CheckpointStateBlock`
|
||||||
|
в PDA серверов, канал закрытий с командой `/.checkpoint`, алгоритмы дежурного и проверяющего).
|
||||||
|
- Формат блоков и правила `AddBlock` не изменены: это новый мета-слой поверх существующего блокчейна.
|
||||||
|
- Код пока не реализован; при реализации обязательно дописать `/.checkpoint`
|
||||||
|
в `02_Channel_Commands.md` и `CheckpointStateBlock` в формат `user_pda`.
|
||||||
|
- В `README.md` этого каталога добавлен раздел «Смежная документация» со ссылками на `docs/ИТХ/`.
|
||||||
|
|
||||||
# 2026-06-26 17:45:18 +0400
|
# 2026-06-26 17:45:18 +0400
|
||||||
- Базовый коммит-ориентир: `44a1ba0`.
|
- Базовый коммит-ориентир: `44a1ba0`.
|
||||||
- На `t.shineup.me` подтверждена рабочая схема startup sync и full-resync:
|
- На `t.shineup.me` подтверждена рабочая схема startup sync и full-resync:
|
||||||
|
|||||||
@@ -24,6 +24,11 @@
|
|||||||
10. [CHANGELOG.md](./CHANGELOG.md)
|
10. [CHANGELOG.md](./CHANGELOG.md)
|
||||||
Журнал изменений документации.
|
Журнал изменений документации.
|
||||||
|
|
||||||
|
## Смежная документация
|
||||||
|
- [../ИТХ/README.md](../ИТХ/README.md) — ежедневное закрытие блокчейна (ИТХ): краткий обзор.
|
||||||
|
- [../ИТХ/Спецификация_ИТХ_v1.md](../ИТХ/Спецификация_ИТХ_v1.md) — точная спецификация чекпоинтов (Arweave/Solana/канал закрытий).
|
||||||
|
- [sync-between-servers.md](./sync-between-servers.md) — живая межсерверная синхронизация блоков и DM.
|
||||||
|
|
||||||
## Важные ограничения MVP
|
## Важные ограничения MVP
|
||||||
- Каналы `type=100` и `type=200` присутствуют в формате, но сейчас не используются в UI.
|
- Каналы `type=100` и `type=200` присутствуют в формате, но сейчас не используются в UI.
|
||||||
- Поддерживаемый рабочий сценарий UI на текущем этапе: `stories (type=0)` и `public (type=1)`.
|
- Поддерживаемый рабочий сценарий UI на текущем этапе: `stories (type=0)` и `public (type=1)`.
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# ИТХ — ежедневное закрытие блокчейна SHiNE (кратко, для людей)
|
||||||
|
|
||||||
|
Статус: дизайн согласован, код не написан.
|
||||||
|
Подробная спецификация для реализации: [Спецификация_ИТХ_v1.md](./Спецификация_ИТХ_v1.md).
|
||||||
|
|
||||||
|
## Что это
|
||||||
|
|
||||||
|
Раз в сутки один из серверов SHiNE («дежурный») делает **закрытие дня**:
|
||||||
|
|
||||||
|
1. Строит меркл-дерево по вершинам всех пользовательских цепочек —
|
||||||
|
его корень называется **ИТХ** (итоговый хэш дня).
|
||||||
|
2. Собирает **бандл** — все новые блоки, появившиеся с прошлого закрытия,
|
||||||
|
с хэшами и подписями пользователей.
|
||||||
|
3. Заливает всё это одним файлом (**чекпоинт**) в Arweave.
|
||||||
|
Каждый чекпоинт ссылается на предыдущий — получается цепочка чекпоинтов.
|
||||||
|
4. Пишет в свой Solana PDA: «крайний чекпоинт — вот такой хэш, лежит по такому Arweave-адресу».
|
||||||
|
5. Публикует пост о закрытии в своём канале закрытий прямо в Сиянии.
|
||||||
|
|
||||||
|
Остальные серверы проверяют чекпоинт, и если всё сходится:
|
||||||
|
|
||||||
|
- обновляют слот в **своём** PDA на этот же чекпоинт («согласен»);
|
||||||
|
- ставят **лайк** на пост дежурного в канале.
|
||||||
|
|
||||||
|
Несогласие = просто ничего не делать (слот остаётся на прошлом чекпоинте).
|
||||||
|
|
||||||
|
## Зачем
|
||||||
|
|
||||||
|
- **Неизменяемость истории.** Прошлые дни зафиксированы в Arweave и Solana —
|
||||||
|
их нельзя переписать даже сговором всех живых серверов.
|
||||||
|
- **Независимая проверяемость.** Любой желающий может скачать цепочку чекпоинтов
|
||||||
|
из Arweave и проверить всю историю, не доверяя ни одному живому серверу.
|
||||||
|
- **Резерв последней надежды.** Даже если все серверы погибнут,
|
||||||
|
история до последнего чекпоинта не потеряна.
|
||||||
|
|
||||||
|
В повседневной работе серверы синхронизируются друг с другом напрямую —
|
||||||
|
Arweave почти не читается, это гарантия, а не рабочая лошадка.
|
||||||
|
|
||||||
|
## Кто дежурит
|
||||||
|
|
||||||
|
График ведёт DAO в специальном PDA: упорядоченный список логинов серверов
|
||||||
|
и дата начала эпохи. Дежурный дня = `dayIndex % количество_серверов`.
|
||||||
|
Обновлять список может только DAO.
|
||||||
|
|
||||||
|
## Расписание суток (UTC)
|
||||||
|
|
||||||
|
| Время | Что происходит |
|
||||||
|
| --- | --- |
|
||||||
|
| День D, 00:00–24:00 | Обычная работа, блоки копятся. |
|
||||||
|
| D+1, 00:00–06:00 | Дежурный закрывает день D: Arweave → PDA → пост в канал. |
|
||||||
|
| D+1, до 24:00 | Остальные проверяют и подтверждают (PDA + лайк). |
|
||||||
|
|
||||||
|
Если дежурный не закрыл (или закрыл неверно) — день просто пропускается:
|
||||||
|
никто ничего не подтверждает, а следующий дежурный включает в свой бандл
|
||||||
|
блоки за все пропущенные дни и ссылается на последний признанный чекпоинт.
|
||||||
|
|
||||||
|
## Как понять, какой чекпоинт «настоящий»
|
||||||
|
|
||||||
|
**Актуальный чекпоинт = последний, на который указывают слоты
|
||||||
|
больше половины серверов из графика.** Кворума on-chain нет —
|
||||||
|
каждый сервер публикует только своё мнение, а наблюдатель считает голоса сам.
|
||||||
|
Сервер, который систематически ошибается, DAO просто исключает из списка.
|
||||||
|
|
||||||
|
## Восстановление сервера с нуля
|
||||||
|
|
||||||
|
1. Прочитать DAO PDA → список серверов графика.
|
||||||
|
2. Прочитать их PDA-слоты → последний признанный чекпоинт (>половины голосов).
|
||||||
|
3. Скачать из Arweave цепочку чекпоинтов от генезиса и раскатать бандлы.
|
||||||
|
4. «Хвост» после последнего чекпоинта добрать живой синхронизацией с серверами.
|
||||||
|
|
||||||
|
## Кто платит
|
||||||
|
|
||||||
|
Каждый сервер платит сам: за свои Arweave-загрузки в свои дежурные дни
|
||||||
|
и за свои Solana-транзакции.
|
||||||
|
|
||||||
|
## Связанные документы
|
||||||
|
|
||||||
|
- [Спецификация_ИТХ_v1.md](./Спецификация_ИТХ_v1.md) — точный формат и алгоритмы (для реализации).
|
||||||
|
- `docs/Blockchain/` — формат блоков и цепочек SHiNE.
|
||||||
|
- `docs/Blockchain/sync-between-servers.md` — живая межсерверная синхронизация.
|
||||||
|
- `docs/Solana_Architecture/README.md` — устройство Solana-части.
|
||||||
|
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` — формат `user_pda`.
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
# Спецификация ИТХ 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`
|
||||||
|
(момент начала окна закрытия). Изменение списка не пересчитывает прошлые дни.
|
||||||
|
- При изменении состава серверов будущее распределение сдвигается автоматически —
|
||||||
|
это допустимо, календарь «дата → сервер» не ведётся.
|
||||||
|
|
||||||
|
## 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 до среза, допустимо включать — это не ошибка.
|
||||||
|
- Полнота относительно других серверов **не требуется**: блоки, которых у дежурного
|
||||||
|
не было, войдут в следующий чекпоинт. Проверяющие не считают «у меня есть больше»
|
||||||
|
причиной несогласия.
|
||||||
|
|
||||||
|
## 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. Рекомендация клиентам: до достижения половины считать актуальным
|
||||||
|
> предыдущий признанный.
|
||||||
|
|
||||||
|
## 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:00–06:00` убедиться по rota PDA, что дежурный — я.
|
||||||
|
2. Определить предыдущий признанный чекпоинт (свой слот + слоты остальных, раздел 6);
|
||||||
|
загрузить его `state` (локальный кэш или Arweave). Для генезиса — пустой.
|
||||||
|
3. Зафиксировать срез: для каждой цепочки — локальная вершина `(lastNumber, lastHash)`.
|
||||||
|
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` — отказ (v1 форки автоматически не разруливает; см. TODO).
|
||||||
|
«У меня есть блоки, которых нет в бандле» — НЕ отказ.
|
||||||
|
10. Импортировать недостающие блоки бандла в локальное хранилище тем же валидационным
|
||||||
|
путём, что и межсерверная синхронизация (`sync-between-servers.md`).
|
||||||
|
11. Успех → обновить свой PDA-слот (`role=2`) и поставить лайк на пост S.
|
||||||
|
Отказ → не делать ничего on-chain; подробно залогировать причину
|
||||||
|
(рекомендуемый префикс лога: `CHECKPOINT_REJECT`).
|
||||||
|
|
||||||
|
Проверка детерминирована: два честных сервера с любым набором локальных блоков
|
||||||
|
приходят к одному вердикту, кроме шага 9 (локальный конфликт), который сигналит
|
||||||
|
о реальном форке пользовательской цепочки.
|
||||||
|
|
||||||
|
## 10. Восстановление сервера с нуля
|
||||||
|
|
||||||
|
1. Прочитать rota PDA → список серверов.
|
||||||
|
2. Прочитать их PDA-слоты → признанный чекпоинт (правило раздела 6).
|
||||||
|
3. От него по `prevArweaveTxId` спуститься до генезиса, затем раскатать бандлы
|
||||||
|
от генезиса вверх, валидируя каждый чекпоинт по разделу 9 (шаги 2–8).
|
||||||
|
4. Хвост после последнего чекпоинта добрать живой синхронизацией с серверами.
|
||||||
|
|
||||||
|
## 11. Стоимость и ключи
|
||||||
|
|
||||||
|
- Каждый сервер платит сам: Arweave — за свои дежурные дни, Solana — за свои слоты.
|
||||||
|
- Arweave JWK сервера хранится только на хосте (например,
|
||||||
|
`/home/player/SHiNE/secrets/…`), путь задаётся конфигом; в git не попадает —
|
||||||
|
по той же схеме, что `test.freeAvatar.walletJwkPath`.
|
||||||
|
|
||||||
|
## 12. Ориентир для реализации на сервере (не нормативно)
|
||||||
|
|
||||||
|
- `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 на каждое закрытие.
|
||||||
|
|
||||||
|
## 13. TODO / сознательно отложено
|
||||||
|
|
||||||
|
- **История ключей при ротации**: если сервер/пользователь меняет ключ несколько раз
|
||||||
|
за день, для проверки старых подписей нужна история версий ключей. v1 живёт с
|
||||||
|
«ключ, актуальный на день закрытия»; при внедрении ротации — хранить версии.
|
||||||
|
- **Разруливание форков** пользовательских цепочек (шаг 9.9): правило «наиболее
|
||||||
|
длинная валидная цепочка» + детерминированный tie-break. v1 — отказ и лог.
|
||||||
|
- **Автопропуск штрафника** в ротации (2 подряд закрытия без единого согласия).
|
||||||
|
- **On-chain кворум** вместо правила чтения «>половины» — если понадобится.
|
||||||
|
- **Сжатие бандла** (gzip) и **чанкование** очень больших бандлов на несколько
|
||||||
|
Arweave-транзакций с манифестом. v1 — один несжатый файл.
|
||||||
|
- Экономия Solana-транзакций: считать согласия только по лайкам канала — возможно
|
||||||
|
после обкатки v1.
|
||||||
|
|
||||||
|
## 14. Связанные документы (обновлять при реализации)
|
||||||
|
|
||||||
|
- `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/Blockchain/CHANGELOG.md` — фиксировать изменения.
|
||||||
@@ -406,6 +406,31 @@ function formatMessageTime(valueMs) {
|
|||||||
}).format(new Date(timeMs));
|
}).format(new Date(timeMs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCallDurationCompact(totalSeconds) {
|
||||||
|
const total = Math.max(0, Math.floor(Number(totalSeconds || 0)));
|
||||||
|
const hours = Math.floor(total / 3600);
|
||||||
|
const minutes = Math.floor((total % 3600) / 60);
|
||||||
|
const seconds = total % 60;
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
return `${minutes}:${String(seconds).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCallStatusText(callSummary) {
|
||||||
|
if (!callSummary) return '';
|
||||||
|
if (callSummary.status === 'completed') {
|
||||||
|
return `Длительность: ${formatCallDurationCompact(callSummary.durationSec)}`;
|
||||||
|
}
|
||||||
|
const reason = String(callSummary.reason || '').trim().toLowerCase();
|
||||||
|
if (reason === 'offline') return 'Не дозвонился: абонент не в сети';
|
||||||
|
if (reason === 'no_answer') return 'Не дозвонился: нет ответа';
|
||||||
|
if (reason === 'connect_failed') return 'Не дозвонился: не удалось установить соединение';
|
||||||
|
if (reason === 'busy') return 'Не дозвонился: абонент занят';
|
||||||
|
if (reason === 'declined') return 'Не дозвонился: звонок отклонён';
|
||||||
|
return 'Не дозвонился';
|
||||||
|
}
|
||||||
|
|
||||||
function resolveDeliveryStatus(msg) {
|
function resolveDeliveryStatus(msg) {
|
||||||
if (msg?.from !== 'out') return '';
|
if (msg?.from !== 'out') return '';
|
||||||
if (msg?.secondTick) return '✓✓';
|
if (msg?.secondTick) return '✓✓';
|
||||||
@@ -503,6 +528,32 @@ function renderLog(list, chatId, { onOpenActions, markAsRead = true, scrollMode
|
|||||||
const bubbleKind = String(msg?.kind || autoKind || '').trim();
|
const bubbleKind = String(msg?.kind || autoKind || '').trim();
|
||||||
bubble.className = `bubble ${msg.from}${bubbleKind ? ` ${bubbleKind}` : ''}`;
|
bubble.className = `bubble ${msg.from}${bubbleKind ? ` ${bubbleKind}` : ''}`;
|
||||||
|
|
||||||
|
if (parsedText.callSummary) {
|
||||||
|
const callCard = document.createElement('div');
|
||||||
|
callCard.className = 'bubble-call-card';
|
||||||
|
|
||||||
|
const callHead = document.createElement('div');
|
||||||
|
callHead.className = 'bubble-call-head';
|
||||||
|
|
||||||
|
const callIcon = document.createElement('span');
|
||||||
|
callIcon.className = 'bubble-call-icon';
|
||||||
|
callIcon.textContent = '☎';
|
||||||
|
|
||||||
|
const callTitle = document.createElement('span');
|
||||||
|
callTitle.className = 'bubble-call-title';
|
||||||
|
callTitle.textContent = msg?.from === 'out' ? 'Исходящий звонок' : 'Входящий звонок';
|
||||||
|
|
||||||
|
callHead.append(callIcon, callTitle);
|
||||||
|
|
||||||
|
const callMetaLine = document.createElement('div');
|
||||||
|
callMetaLine.className = 'bubble-call-line bubble-call-line--muted';
|
||||||
|
const callStatus = buildCallStatusText(parsedText.callSummary);
|
||||||
|
callMetaLine.textContent = callStatus;
|
||||||
|
|
||||||
|
callCard.append(callHead, callMetaLine);
|
||||||
|
bubble.append(callCard);
|
||||||
|
}
|
||||||
|
|
||||||
const textNode = document.createElement('div');
|
const textNode = document.createElement('div');
|
||||||
textNode.className = 'bubble-text';
|
textNode.className = 'bubble-text';
|
||||||
textNode.textContent = parsedText.displayText || '';
|
textNode.textContent = parsedText.displayText || '';
|
||||||
@@ -533,7 +584,9 @@ function renderLog(list, chatId, { onOpenActions, markAsRead = true, scrollMode
|
|||||||
bubble.append(replyBox);
|
bubble.append(replyBox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!parsedText.callSummary) {
|
||||||
bubble.append(textNode);
|
bubble.append(textNode);
|
||||||
|
}
|
||||||
|
|
||||||
const metaNode = document.createElement('div');
|
const metaNode = document.createElement('div');
|
||||||
metaNode.className = 'bubble-meta';
|
metaNode.className = 'bubble-meta';
|
||||||
|
|||||||
@@ -268,16 +268,6 @@ export function render({ navigate, route }) {
|
|||||||
return makeProfileRoute(cleanLogin);
|
return makeProfileRoute(cleanLogin);
|
||||||
}
|
}
|
||||||
|
|
||||||
function helpText() {
|
|
||||||
return [
|
|
||||||
'Обозначения на экране связей:',
|
|
||||||
'• Синие линии — близкие друзья.',
|
|
||||||
'• Оранжевые линии — родственники.',
|
|
||||||
'• Стрелка на линии — односторонняя связь.',
|
|
||||||
'• Если стрелки нет, связь взаимная.',
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function persistHistory() {
|
function persistHistory() {
|
||||||
persistedCenterLogin = centerLogin;
|
persistedCenterLogin = centerLogin;
|
||||||
persistedCenterHistory = [...centerHistory];
|
persistedCenterHistory = [...centerHistory];
|
||||||
@@ -523,7 +513,6 @@ export function render({ navigate, route }) {
|
|||||||
},
|
},
|
||||||
rightActions: [
|
rightActions: [
|
||||||
{ label: 'Найти', onClick: openSearchModal },
|
{ label: 'Найти', onClick: openSearchModal },
|
||||||
{ label: '?', onClick: () => window.alert(helpText()) },
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const backBtnEl = header.querySelector('.header-left .icon-btn');
|
const backBtnEl = header.querySelector('.header-left .icon-btn');
|
||||||
|
|||||||
@@ -1062,11 +1062,52 @@
|
|||||||
.bubble.call-tech {
|
.bubble.call-tech {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
border-radius: 10px;
|
border-radius: 16px;
|
||||||
border: 1px solid rgba(131, 162, 223, 0.32);
|
border: 1px solid rgba(132, 170, 231, 0.28);
|
||||||
background: rgba(40, 55, 84, 0.55);
|
background: rgba(24, 36, 58, 0.72);
|
||||||
color: #dce8ff;
|
color: #dce8ff;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-call-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: min(280px, 72vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-call-head {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-call-icon {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(80, 170, 255, 0.18);
|
||||||
|
border: 1px solid rgba(123, 191, 255, 0.28);
|
||||||
|
color: #d9ecff;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-call-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #eef6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-call-line {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: rgba(230, 240, 255, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-call-line--muted {
|
||||||
|
color: rgba(188, 207, 238, 0.78);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
@@ -1523,17 +1564,21 @@ textarea.input {
|
|||||||
|
|
||||||
.network-header-overlay {
|
.network-header-overlay {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: max(8px, env(safe-area-inset-top));
|
top: max(14px, calc(env(safe-area-inset-top) + 8px));
|
||||||
left: max(8px, env(safe-area-inset-left));
|
left: max(8px, env(safe-area-inset-left));
|
||||||
right: max(8px, env(safe-area-inset-right));
|
right: max(8px, env(safe-area-inset-right));
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
padding: 9px 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-header-overlay.page-header {
|
.network-header-overlay.page-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(42px, 1fr) auto minmax(58px, 1fr);
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
gap: 6px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-header-overlay .header-left,
|
.network-header-overlay .header-left,
|
||||||
@@ -1542,20 +1587,30 @@ textarea.input {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.network-header-overlay .header-left {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-header-overlay .header-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.network-header-overlay .icon-btn {
|
.network-header-overlay .icon-btn {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
min-height: 30px;
|
min-height: 32px;
|
||||||
padding: 5px 8px;
|
padding: 6px 10px;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-header-overlay .page-title {
|
.network-header-overlay .page-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
line-height: 32px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.45);
|
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user