SHA256
Обновить UI чатов и связей и добавить документацию ИТХ
This commit is contained in:
+2
-2
@@ -1,2 +1,2 @@
|
||||
client.version=1.2.310
|
||||
server.version=1.2.289
|
||||
client.version=1.2.311
|
||||
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
|
||||
- Базовый коммит-ориентир: `44a1ba0`.
|
||||
- На `t.shineup.me` подтверждена рабочая схема startup sync и full-resync:
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
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
|
||||
- Каналы `type=100` и `type=200` присутствуют в формате, но сейчас не используются в UI.
|
||||
- Поддерживаемый рабочий сценарий 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));
|
||||
}
|
||||
|
||||
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) {
|
||||
if (msg?.from !== 'out') return '';
|
||||
if (msg?.secondTick) return '✓✓';
|
||||
@@ -503,6 +528,32 @@ function renderLog(list, chatId, { onOpenActions, markAsRead = true, scrollMode
|
||||
const bubbleKind = String(msg?.kind || autoKind || '').trim();
|
||||
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');
|
||||
textNode.className = 'bubble-text';
|
||||
textNode.textContent = parsedText.displayText || '';
|
||||
@@ -533,7 +584,9 @@ function renderLog(list, chatId, { onOpenActions, markAsRead = true, scrollMode
|
||||
bubble.append(replyBox);
|
||||
}
|
||||
}
|
||||
bubble.append(textNode);
|
||||
if (!parsedText.callSummary) {
|
||||
bubble.append(textNode);
|
||||
}
|
||||
|
||||
const metaNode = document.createElement('div');
|
||||
metaNode.className = 'bubble-meta';
|
||||
|
||||
@@ -268,16 +268,6 @@ export function render({ navigate, route }) {
|
||||
return makeProfileRoute(cleanLogin);
|
||||
}
|
||||
|
||||
function helpText() {
|
||||
return [
|
||||
'Обозначения на экране связей:',
|
||||
'• Синие линии — близкие друзья.',
|
||||
'• Оранжевые линии — родственники.',
|
||||
'• Стрелка на линии — односторонняя связь.',
|
||||
'• Если стрелки нет, связь взаимная.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function persistHistory() {
|
||||
persistedCenterLogin = centerLogin;
|
||||
persistedCenterHistory = [...centerHistory];
|
||||
@@ -523,7 +513,6 @@ export function render({ navigate, route }) {
|
||||
},
|
||||
rightActions: [
|
||||
{ label: 'Найти', onClick: openSearchModal },
|
||||
{ label: '?', onClick: () => window.alert(helpText()) },
|
||||
],
|
||||
});
|
||||
const backBtnEl = header.querySelector('.header-left .icon-btn');
|
||||
|
||||
@@ -1062,11 +1062,52 @@
|
||||
.bubble.call-tech {
|
||||
max-width: 90%;
|
||||
justify-self: center;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(131, 162, 223, 0.32);
|
||||
background: rgba(40, 55, 84, 0.55);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(132, 170, 231, 0.28);
|
||||
background: rgba(24, 36, 58, 0.72);
|
||||
color: #dce8ff;
|
||||
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 {
|
||||
@@ -1523,17 +1564,21 @@ textarea.input {
|
||||
|
||||
.network-header-overlay {
|
||||
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));
|
||||
right: max(8px, env(safe-area-inset-right));
|
||||
margin-bottom: 0;
|
||||
z-index: 12;
|
||||
pointer-events: none;
|
||||
padding: 9px 12px 0;
|
||||
}
|
||||
|
||||
.network-header-overlay.page-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(42px, 1fr) auto minmax(58px, 1fr);
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
gap: 6px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.network-header-overlay .header-left,
|
||||
@@ -1542,20 +1587,30 @@ textarea.input {
|
||||
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 {
|
||||
pointer-events: auto;
|
||||
min-height: 30px;
|
||||
padding: 5px 8px;
|
||||
font-size: 11px;
|
||||
min-height: 32px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.network-header-overlay .page-title {
|
||||
font-size: 16px;
|
||||
line-height: 32px;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user