SHA256
Аввив 2.0 - работает, заливка!!
This commit is contained in:
@@ -1,310 +1,49 @@
|
||||
# API для разработчиков: 04 — Запись и чтение блока блокчейна
|
||||
# AddBlock API
|
||||
|
||||
Документ описывает **текущий рабочий формат** сетевых вызовов:
|
||||
## Назначение
|
||||
|
||||
- `AddBlock` — запись любого блока в блокчейн пользователя;
|
||||
- `GetBlockchainBlock` — публичное чтение одного конкретного блока по имени цепочки и номеру.
|
||||
Добавляет один готовый подписанный пользовательский SHiNE Frame v1 / ANS-104 DataItem в blockchain.
|
||||
|
||||
`GetBlockchainBlock` нужен в том числе для межсерверной синхронизации и для открытого чтения публичного блокчейна по одному блоку.
|
||||
|
||||
> Важный принцип: на уровне JSON API сейчас есть **один универсальный метод** записи — `AddBlock`.
|
||||
> Конкретный смысл записи задаётся типом самого бинарного блока (`type/subType/version` в заголовке блока).
|
||||
|
||||
## 1. Что делает `AddBlock`
|
||||
|
||||
`AddBlock`:
|
||||
- принимает имя блокчейна и base64 бинарного блока;
|
||||
- проверяет непрерывность цепочки (`blockNumber`, `prevHash`);
|
||||
- проверяет формат и подпись Ed25519;
|
||||
- валидирует `body` по правилам типа блока;
|
||||
- сохраняет блок и обновляет состояние цепочки.
|
||||
|
||||
## 2. JSON формат запроса
|
||||
|
||||
`op = "AddBlock"`.
|
||||
## Request
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "AddBlock",
|
||||
"requestId": "req-1001",
|
||||
"requestId": "...",
|
||||
"payload": {
|
||||
"blockchainName": "alice-001",
|
||||
"blockNumber": 12,
|
||||
"prevBlockHash": "ab12...ff",
|
||||
"blockBytesB64": "AAAB..."
|
||||
"blockchainName": "alice-...",
|
||||
"blockNumber": 42,
|
||||
"prevBlockHash": "64 hex chars",
|
||||
"blockBytesB64": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Поля `payload`:
|
||||
- `blockchainName` — обязательно, формат `login-NNN`.
|
||||
- `blockNumber` — обязательно (временное legacy-поле для совместимости; должно совпасть с номером внутри бинарного блока).
|
||||
- `prevBlockHash` — legacy-поле, сейчас сервер использует `prevHash` из бинарного блока и состояние цепочки.
|
||||
- `blockBytesB64` — обязательно: **полный бинарный блок** (`preimage + sigMarker + signature`) в Base64.
|
||||
- `blockchainName` — целевая пользовательская chain;
|
||||
- `blockNumber` — должен совпадать с номером внутри Frame v1 и быть `serverLast + 1`;
|
||||
- `prevBlockHash` — 32-byte SHiNE hash предыдущего Frame в hex; должен совпадать с `prevHash32` внутри Frame и серверной вершиной;
|
||||
- `blockBytesB64` — **полный serialized ANS-104 DataItem** в Base64.
|
||||
|
||||
## 3. Успешный ответ
|
||||
Старый формат `preimage + sigMarker + signature` не поддерживается.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "AddBlock",
|
||||
"requestId": "req-1001",
|
||||
"status": 200,
|
||||
"ok": true,
|
||||
"payload": {
|
||||
"reasonCode": null,
|
||||
"serverLastGlobalNumber": 12,
|
||||
"serverLastGlobalHash": "9f0e...a1"
|
||||
}
|
||||
}
|
||||
```
|
||||
## Требования к DataItem
|
||||
|
||||
## 4. Ошибка (единый формат)
|
||||
- generic Ed25519 signature type `2`;
|
||||
- Ed25519 signature 64 bytes;
|
||||
- owner 32 bytes и равен текущему blockchain public key;
|
||||
- обязательный тестовый tag `App=test5590`;
|
||||
- для channel block — `c=<canonical_channel_slug>`;
|
||||
- `data` содержит SHiNE Frame v1 (`frameCode=1`).
|
||||
|
||||
При ошибках сервер отдаёт `Net_Exception_Response` со стандартными полями и дополнительно с состоянием сервера для ресинка:
|
||||
## Основные ошибки
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "AddBlock",
|
||||
"requestId": "req-1001",
|
||||
"status": 400,
|
||||
"ok": false,
|
||||
"error": "bad_prev_hash",
|
||||
"message": "Некорректный prevHash (цепочка не совпадает)",
|
||||
"payload": {
|
||||
"serverLastGlobalNumber": 11,
|
||||
"serverLastGlobalHash": "c3d4...98"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `bad_app_tag` — отсутствует/неверен `App=test5590`;
|
||||
- `bad_channel_tag` — `c` отсутствует, лишний или не совпадает с canonical slug;
|
||||
- `bad_signature` / `signature_verify_failed` — DataItem не подписан текущим blockchain key;
|
||||
- `bad_block_number` — нарушена последовательность;
|
||||
- `bad_prev_hash` — нарушена SHiNE hash chain;
|
||||
- `bad_block_bytes` — DataItem/Frame не парсится.
|
||||
|
||||
### Основные `reasonCode`
|
||||
## Storage/publish
|
||||
|
||||
- `empty_blockchain_name`, `bad_blockchain_name`
|
||||
- `blockchain_state_not_found`
|
||||
- `bad_block_base64`, `bad_block_format`, `bad_block_body`
|
||||
- `bad_block_number`, `req_global_mismatch`, `bad_prev_hash`
|
||||
- `bad_signature`, `signature_verify_failed`
|
||||
- `prev_line_block_not_found`, `bad_prev_line_hash`
|
||||
- `limit_exceeded`
|
||||
- `chain_resync_in_progress` — цепочка временно заблокирована полным resync
|
||||
- `repost_disabled` — репосты временно отключены до будущей реализации
|
||||
- `entrypoint_edit_forbidden` — `TEXT_ENTRYPOINT` нельзя редактировать через `TEXT_EDIT_POST`
|
||||
- `status_confirmed_target_must_be_status_action` — `STATUS_CONFIRMED` должен ссылаться на статусный блок
|
||||
- `status_action_target_not_allowed` — выбранный `STATUS_ACTION` нельзя ставить на этот тип материала
|
||||
- `bad_channel_meta_line`, `channel_not_found`, `bad_channel_meta_*`, `channel_meta_*_too_long` — ошибки `TEXT_CHANNEL_META`
|
||||
- `internal_error`
|
||||
|
||||
## 5. Какие блоки реально можно добавлять через `AddBlock`
|
||||
|
||||
Через `AddBlock` можно писать поддержанные форматы, кроме явно отключённых временных фич:
|
||||
|
||||
1. **TECH (type=0)**
|
||||
- `HEADER_COMPAT (subType=0)`
|
||||
- `TECH_CREATE_CHANNEL (subType=1)`
|
||||
|
||||
2. **TEXT (type=1)**
|
||||
- `TEXT_POST (10)`
|
||||
- `TEXT_EDIT_POST (11)`
|
||||
- `TEXT_REPLY (20)`
|
||||
- `TEXT_EDIT_REPLY (21)`
|
||||
- `TEXT_RATING (30)` — target-based отзыв на конкретный блок
|
||||
- `TEXT_REPOST (50)` — формат зарезервирован, но новые блоки временно отклоняются с `repost_disabled`
|
||||
- `TEXT_CHANNEL_META (90)` — скрытый технический снимок профиля канала
|
||||
- `TEXT_ENTRYPOINT (100)` — входная страница канала
|
||||
- `TEXT_EXERCISE (110)` — line-based материал упражнения
|
||||
- `TEXT_SERVICE (120)` — line-based материал услуги / процедуры
|
||||
- `TEXT_COURSE (130)` — line-based материал курса
|
||||
|
||||
3. **REACTION (type=2)**
|
||||
- `REACTION_LIKE (1)`
|
||||
|
||||
4. **CONNECTION (type=3)**
|
||||
- `CONNECTION_FRIEND (10)`
|
||||
- `CONNECTION_UNFRIEND (11)`
|
||||
- `CONNECTION_CONTACT (20)`
|
||||
- `CONNECTION_UNCONTACT (21)`
|
||||
- `CONNECTION_FOLLOW (30)`
|
||||
- `CONNECTION_UNFOLLOW (31)`
|
||||
- `CONNECTION_SPOUSE (40)`
|
||||
- `CONNECTION_UNSPOUSE (41)`
|
||||
- `CONNECTION_PARENT (50)`
|
||||
- `CONNECTION_UNPARENT (51)`
|
||||
- `CONNECTION_CHILD (52)`
|
||||
- `CONNECTION_UNCHILD (53)`
|
||||
- `CONNECTION_SIBLING (54)`
|
||||
- `CONNECTION_UNSIBLING (55)`
|
||||
- `CONNECTION_KNOWN_PERSON (60)`
|
||||
- `CONNECTION_UNKNOWN_PERSON (61)`
|
||||
- `CONNECTION_SHINE_CONFIRMED (70)`
|
||||
- `CONNECTION_SHINE_UNCONFIRMED (71)`
|
||||
- `CONNECTION_SHINE_SEEN (74)`
|
||||
- `CONNECTION_SHINE_UNSEEN (75)`
|
||||
|
||||
5. **USER_PARAM (type=4)**
|
||||
- `USER_PARAM_TEXT_TEXT (1)`
|
||||
|
||||
6. **STATUS_ACTION (type=5)**
|
||||
- `STATUS_DONE_ONCE (10)`
|
||||
- `STATUS_LEARNED (20)`
|
||||
- `STATUS_SERVICE_PASSED (30)`
|
||||
- `STATUS_CONFIRMED (100)`
|
||||
- `STATUS_INTERESTED (110)`
|
||||
- `STATUS_STARTED (120)`
|
||||
- `STATUS_IN_STUDY (130)`
|
||||
- `STATUS_ABANDONED (140)`
|
||||
- `STATUS_COMPLETED (150)`
|
||||
|
||||
## 6. Практические payload-форматы для каналов и вложений
|
||||
|
||||
`AddBlock` не имеет отдельных JSON-полей для вложений, аватаров или человекочитаемого имени канала. Клиент собирает бинарный блок нужного типа, а новые данные кладёт в текстовые поля тела блока по правилам blockchain-формата.
|
||||
|
||||
### Вложения в сообщениях
|
||||
|
||||
Для `TEXT_POST`, `TEXT_REPLY`, `TEXT_EDIT_POST` и `TEXT_EDIT_REPLY` вложения записываются в начало текста сообщения одним или несколькими тегами `S:att v=1`.
|
||||
|
||||
Пример текстового содержимого body:
|
||||
|
||||
```text
|
||||
<S:att;v=1;nm=photo.jpg;sz=248193;sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>
|
||||
<S:att;v=1;nm=report.pdf;sz=845221;sha256=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc;ar=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD>
|
||||
Текст сообщения
|
||||
```
|
||||
|
||||
Пример вложения с отдельным preview-файлом:
|
||||
|
||||
```text
|
||||
<S:att;v=1;nm=video.mp4;sz=5820193;sha256=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;ar=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC;preAr=DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD;preSha256=eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee>
|
||||
```
|
||||
|
||||
Сервер хранит это как обычный `TEXT`-блок. Отображение карусели, картинок, видео и карточек файлов делает клиент. Полная спецификация тега находится в `docs/Blockchain/15_TEXT_Attachments.md`.
|
||||
|
||||
### Создание публичного канала с профилем
|
||||
|
||||
Для публичного канала начальный профиль пишется одним блоком `TECH_CREATE_CHANNEL`. Поле `channelDescription` содержит meta-текст:
|
||||
|
||||
```text
|
||||
<S:title;v=1;Человекочитаемое имя канала>
|
||||
<S:ava;v=1;sz=248193;sha256=3f2c8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=AbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdE>
|
||||
Описание канала
|
||||
```
|
||||
|
||||
Ограничение `channelDescription` — до `2048` UTF-8 байт. Новый клиент не пишет отдельный `TEXT_CHANNEL_META` сразу после создания канала: создание канала и начальный профиль должны попадать в один `TECH_CREATE_CHANNEL`.
|
||||
|
||||
### Изменение профиля канала
|
||||
|
||||
Последующие изменения аватара, человекочитаемого имени или описания канала пишутся отдельным скрытым `TEXT_CHANNEL_META (subType=90)`.
|
||||
|
||||
Текстовое содержимое body использует тот же формат полного снимка профиля:
|
||||
|
||||
```text
|
||||
<S:title;v=1;Новое имя канала>
|
||||
<S:ava;v=1;sz=248193;sha256=3f2c8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;ar=AbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdEfAbCdE>
|
||||
Новое описание канала
|
||||
```
|
||||
|
||||
Каждый `TEXT_CHANNEL_META` является полным состоянием профиля на момент записи. Если аватара нет, тег `S:ava` не пишется. Если описания нет, после meta-тегов не добавляется хвостовой текст. Полная спецификация находится в `docs/Blockchain/16_TEXT_Channel_Meta.md`.
|
||||
|
||||
## 7. Хватает ли функций сейчас
|
||||
|
||||
Коротко: **для записи событий в блокчейн — хватает**, для полноценного клиентского чтения — **пока не хватает**.
|
||||
|
||||
Что есть:
|
||||
- единый надёжный write-путь `AddBlock`;
|
||||
- есть `GetFriendsLists` и API по `UserParam`;
|
||||
- есть унифицированные коды ошибок и поля для ресинхронизации.
|
||||
|
||||
Что пока ограничивает продукт:
|
||||
- нет полноценного read API для каналов/постов/тредов;
|
||||
- нет API списка подписок с серверными счётчиками непрочитанного;
|
||||
- нет ленты событий (новые ответы/лайки/подписки) как отдельного RPC.
|
||||
|
||||
## 8. Рекомендации по клиенту при записи блоков
|
||||
|
||||
1. Перед отправкой держать локальный `lastNumber/lastHash`.
|
||||
2. При `bad_prev_hash` или `bad_block_number`:
|
||||
- взять `serverLastGlobalNumber/serverLastGlobalHash` из ошибки,
|
||||
- пересобрать следующий блок на актуальной вершине.
|
||||
3. Для edit-блоков всегда ссылаться на **оригинальный** блок, а не на предыдущий edit.
|
||||
4. Для связей/подписок использовать target на **root** (HEADER или CREATE_CHANNEL), а не на произвольный пост.
|
||||
|
||||
|
||||
## 9. USER_PARAM для «личных данных»
|
||||
|
||||
Да, на текущем API это можно добавить **без изменения серверного кода**:
|
||||
|
||||
- в `UserParam` поле `param` сейчас не ограничено фиксированным справочником;
|
||||
- сервер хранит пары `param -> value` как строки (при наличии корректной подписи и `time_ms`);
|
||||
- чтение уже есть через `GetUserParam` и `ListUserParams`.
|
||||
|
||||
Рекомендуемый стартовый набор ключей для профиля (MVP):
|
||||
|
||||
- `name`
|
||||
- `last_name`
|
||||
- `address_physical`
|
||||
- `address_web`
|
||||
- `phone`
|
||||
|
||||
Практическая рекомендация: заранее зафиксировать единый словарь ключей в клиенте/документации, чтобы избежать дублей вида `lastname` vs `last_name`, `site` vs `address_web` и т.д.
|
||||
|
||||
Ограничения, которые важно учесть:
|
||||
|
||||
- сейчас нет серверной ACL-политики чтения параметров (в MVP их может читать любой клиент, который знает `login`);
|
||||
- нет валидации формата значений для конкретных ключей (телефон, URL и т.д. проверяются только на стороне клиента);
|
||||
- нет отдельного индекса/поиска по этим полям — только точечное чтение и listing по `login`.
|
||||
|
||||
---
|
||||
|
||||
## 10. `GetBlockchainBlock`
|
||||
|
||||
### Назначение
|
||||
|
||||
Публичное чтение одного конкретного блока из цепочки.
|
||||
|
||||
Нужно для:
|
||||
|
||||
- открытого чтения блокчейна по одному блоку;
|
||||
- межсерверной синхронизации;
|
||||
- восстановления/докачки отсутствующего хвоста цепочки.
|
||||
|
||||
### JSON формат запроса
|
||||
|
||||
`op = "GetBlockchainBlock"`.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "GetBlockchainBlock",
|
||||
"requestId": "req-2001",
|
||||
"payload": {
|
||||
"blockchainName": "alice-001",
|
||||
"blockNumber": 12
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Поля `payload`:
|
||||
|
||||
- `blockchainName` — обязательно, формат `login-NNN`.
|
||||
- `blockNumber` — обязательно, номер блока в цепочке, `>= 0`.
|
||||
|
||||
### Успешный ответ
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "GetBlockchainBlock",
|
||||
"requestId": "req-2001",
|
||||
"status": 200,
|
||||
"ok": true,
|
||||
"payload": {
|
||||
"blockchainName": "alice-001",
|
||||
"blockNumber": 12,
|
||||
"blockHash": "9f0eaabbccddeeff00112233445566778899aabbccddeeff0011223344556677",
|
||||
"blockBytesB64": "AAAB..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибки
|
||||
|
||||
- `400 / BAD_FIELDS` — некорректные `blockchainName` или `blockNumber`.
|
||||
- `404 / BLOCK_NOT_FOUND` — такого блока нет.
|
||||
- `500 / INTERNAL_ERROR` — внутренняя ошибка сервера.
|
||||
После успешного локального AddBlock полный DataItem хранится в PostgreSQL и ставится в Arweave publish queue. Импорт из Arweave использует ту же проверку, но не ставит блок на повторную публикацию.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,137 +0,0 @@
|
||||
# Карта реализации SHiNE Archive Publisher v1.0
|
||||
|
||||
Этот документ связывает протокол с конкретными файлами проекта. Он нужен, чтобы другой агент мог быстро понять, где искать каждую часть реализации.
|
||||
|
||||
## 1. Новый Java-модуль `shine-server-archive`
|
||||
|
||||
Путь: `SHiNE-server/shine-server-archive/`.
|
||||
|
||||
Основные классы:
|
||||
|
||||
- `ArchivePublisherScheduler` — включает publisher только при `archive.publish.enabled=true`, сразу продолжает незавершённый job после рестарта и планирует новый snapshot один раз в сутки в `archive.publish.time`. Следующая дата вычисляется по `ZoneId`, а не как `+24h`, поэтому DST не сдвигает локальную полночь.
|
||||
- `ArchivePublisherService` — state machine: snapshot → локальный файл → Arweave → confirmations → User PDA → Solana finalized → cursor commit.
|
||||
- `ArchivePublisherConfig` — читает настройки. Для Solana RPC отдельного archive URL нет: используется `solana.users.sync.rpcUrl`, затем fallback на `solana.rpcUrl`.
|
||||
- `ShineArchiveWriter` — сериализует бинарный big block `SHINE-ARCHIVE v1.0`, FULL reference table, по одному chunk на `blockchain_name`, footer/hash/signature.
|
||||
- `ArchiveFileNames` — временное и финальное локальные имена.
|
||||
- `ArweaveArchiveService` + `ArweaveMerkle` — Arweave v2 transaction + chunk upload + polling confirmations.
|
||||
- `SolanaArchiveHeadWriter` — обычный `update_user_pda`: root signature + текущая last-block signature + client key fee payer, затем ожидание `finalized`.
|
||||
- `ArchiveKeyLoader` — читает Ed25519 seed/keypair из raw 32/64 bytes, Solana JSON 32/64 или Base64/PKCS8.
|
||||
|
||||
## 2. Локальное состояние PostgreSQL
|
||||
|
||||
Миграция: `SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v22.sql`.
|
||||
|
||||
Таблицы:
|
||||
|
||||
### `archive_chain_cursor`
|
||||
Одна строка на `blockchain_name`. Хранит только **последнее окончательно опубликованное** состояние:
|
||||
|
||||
- последний source block number/hash;
|
||||
- big block number/hash, где находится последний chunk;
|
||||
- offset/size последнего chunk.
|
||||
|
||||
Если blockchain не попала в новый big block, эта строка не меняется.
|
||||
|
||||
### `archive_publish_job`
|
||||
Crash-safe state одной большой публикации и путь к локальному файлу.
|
||||
|
||||
Основные состояния:
|
||||
`SNAPSHOT_CREATED → FILE_BUILT → ARWEAVE_UPLOADED → ARWEAVE_CONFIRMED → SOLANA_SUBMITTED → SOLANA_FINALIZED → CURSORS_COMMITTED`.
|
||||
|
||||
### `archive_publish_job_chain`
|
||||
Frozen range каждой blockchain текущего job + старые и новые координаты chunk. Пока job не finalized, `archive_chain_cursor` не двигается.
|
||||
|
||||
`DatabaseInitializer` автоматически применяет migration v22 при старте существующей БД. Новая БД создаётся уже со схемой v22.
|
||||
|
||||
## 3. Как считается первая дельта
|
||||
|
||||
`ArchivePublisherService.createFrozenJob()` проходит по `BlockchainStateDAO.listAll()`.
|
||||
|
||||
- Если cursor для `blockchain_name` отсутствует: `from = 0`, поэтому первый архив содержит всё локально известное состояние `0..head`.
|
||||
- Если cursor существует: `from = last_archived + 1`.
|
||||
- Перед продолжением проверяется hash cursor-блока.
|
||||
|
||||
Папка `data/archive` сама по себе **не является источником истины** о том, был ли первый архив. Источник истины — БД cursor/job. Поэтому удаление локального файла не приводит к ошибочной повторной полной публикации.
|
||||
|
||||
## 4. Локальный lifecycle файла
|
||||
|
||||
До появления Arweave TX ID:
|
||||
|
||||
`<login>.<00001>.<dd.MM.yy>.tmp.SHiNE-archive`
|
||||
|
||||
После полной успешной загрузки transaction header + chunks в Arweave:
|
||||
|
||||
`<login>.<00001>.<dd.MM.yy>.<ARWEAVE_TX_ID>.SHiNE-archive`
|
||||
|
||||
Дата — реальная дата freeze snapshot в timezone archive publisher-а. Номер начинается с `00001`. Пять цифр — минимальная ширина, а не лимит.
|
||||
|
||||
После rename файл остаётся локально. При crash после сохранения TX ID, но до rename, recovery переименует тот же файл и не загрузит его повторно.
|
||||
|
||||
## 5. User PDA block type `100`
|
||||
|
||||
Содержимое:
|
||||
|
||||
```text
|
||||
u8 block_type = 100
|
||||
u8 block_version = 0
|
||||
bytes[32] archive_tx_id
|
||||
bytes[32] archive_hash
|
||||
```
|
||||
|
||||
Используется существующий `update_user_pda`; отдельной instruction нет.
|
||||
|
||||
Совместимость:
|
||||
- legacy update без archive extension должен сохранить старый archive head;
|
||||
- новый update может заменить/очистить block `100`;
|
||||
- Java/JS codecs и PostgreSQL Solana sync умеют читать новый блок.
|
||||
|
||||
Ключевой Rust-файл: `shine-solana/shine/programs/shine_users/src/lib.rs`.
|
||||
|
||||
## 6. Startup сервера
|
||||
|
||||
`WsServer` после текущего Solana/users sync и inter-server blockchain sync вызывает `ArchivePublisherScheduler.startOrLog()`.
|
||||
|
||||
При `archive.publish.enabled=false` scheduler пишет лог о выключенной функции и больше ничего не делает. Ключи/Arweave wallet на обычном сервере тогда не требуются.
|
||||
|
||||
## 7. Legacy TestFreeAvatar
|
||||
|
||||
Старый временный `TestFreeAvatarArweaveService` больше не является частью активного WS-протокола. Registry/API документация убраны. При наложении changed-files ZIP поверх старого дерева старые исходники физически останутся, поэтому их список для удаления находится в `05_PATCH_CONTENTS_AND_REMOVALS.md`.
|
||||
|
||||
|
||||
## Trusted importer / location index / Viewer
|
||||
|
||||
Server importer:
|
||||
|
||||
```text
|
||||
shine-server-archive/src/main/java/server/archive/ArchiveImportConfig.java
|
||||
shine-server-archive/src/main/java/server/archive/ArchiveImportScheduler.java
|
||||
shine-server-archive/src/main/java/server/archive/ArchiveImportService.java
|
||||
shine-server-archive/src/main/java/server/archive/ShineArchiveReader.java
|
||||
```
|
||||
|
||||
Database:
|
||||
|
||||
```text
|
||||
shine-server-db/src/main/java/shine/db/dao/ArchiveImportDAO.java
|
||||
shine-server-db/src/main/java/shine/db/archive/ArchiveBlockchainLocation.java
|
||||
shine-server-db/src/main/java/shine/db/archive/ArchivePublisherHead.java
|
||||
shine-server-db/src/main/resources/postgres/migration_v23.sql
|
||||
shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java
|
||||
```
|
||||
|
||||
`PostgresStorageRepository` сохраняет `archive_imported=true` при повторном sync того же head и автоматически сбрасывает флаг в `false`, если `archive_head_tx_id` или `archive_head_hash` изменились.
|
||||
|
||||
WS API:
|
||||
|
||||
```text
|
||||
GetArchiveBlockchainLocation
|
||||
```
|
||||
|
||||
UI:
|
||||
|
||||
```text
|
||||
shine-UI/js/pages/blockchain-archive-view.js
|
||||
shine-UI/Blockchain-Viewer.html
|
||||
```
|
||||
|
||||
`Blockchain-Viewer.html` получает `tx + offset + size + blockchain`, идёт назад по `PreviousBlockchainChunkRef` и использует существующий parser каналов старого Viewer-а.
|
||||
@@ -1,466 +0,0 @@
|
||||
# Деплой SHiNE Archive Publisher v1.0 на тестовый сервер
|
||||
|
||||
Документ рассчитан на человека или автономного coding/deploy агента. Выполнять шаги по порядку. Не включать publisher до проверки Solana-программы и ключей.
|
||||
|
||||
## 0. Что именно меняется
|
||||
|
||||
Нужны изменения одновременно в:
|
||||
|
||||
1. серверном Java-коде;
|
||||
2. PostgreSQL schema v23;
|
||||
3. Solana-программе `shine_users` (PDA block type `100` + backward-compatible update parser);
|
||||
4. Java/JS User PDA codecs.
|
||||
|
||||
**Критично:** новый серверный writer отправляет расширенный обычный `update_user_pda`. Если в целевом кластере работает старая `shine_users`, включать publisher нельзя.
|
||||
|
||||
---
|
||||
|
||||
# 1. Применить пакет к исходникам
|
||||
|
||||
ZIP из этой поставки содержит только новые/изменённые файлы с путями относительно корня репозитория.
|
||||
|
||||
Сделать backup текущего проекта, затем распаковать ZIP поверх рабочего дерева.
|
||||
|
||||
После распаковки удалить legacy-файлы из списка `05_PATCH_CONTENTS_AND_REMOVALS.md`.
|
||||
|
||||
Проверить:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
или, если это не git checkout, сравнить список файлов с manifest из той же документации.
|
||||
|
||||
---
|
||||
|
||||
# 2. Обязательно обновить `shine_users` в нужном Solana-кластере
|
||||
|
||||
## 2.1. Проверить целевой кластер
|
||||
|
||||
Не деплоить вслепую. Сначала:
|
||||
|
||||
```bash
|
||||
solana config get
|
||||
```
|
||||
|
||||
и проверить RPC/кластер, upgrade authority и Program ID. В проекте `shine_users` использует Program ID:
|
||||
|
||||
```text
|
||||
SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6
|
||||
```
|
||||
|
||||
Если тестовый сервер использует mainnet RPC, обновляется именно mainnet-программа. Если тестовый контур использует devnet — сначала убедиться, что программа с нужным ID действительно существует в devnet.
|
||||
|
||||
## 2.2. Собрать Solana program
|
||||
|
||||
```bash
|
||||
cd shine-solana/shine
|
||||
anchor build
|
||||
```
|
||||
|
||||
Минимальная host-проверка Rust, если Anchor/SBF toolchain временно недоступен:
|
||||
|
||||
```bash
|
||||
cargo build -p shine_users
|
||||
```
|
||||
|
||||
Но для реального deploy нужен SBF/Anchor build.
|
||||
|
||||
## 2.3. Обновить существующую программу
|
||||
|
||||
Использовать существующий project deploy/upgrade authority. Типовой вариант:
|
||||
|
||||
```bash
|
||||
solana program deploy target/deploy/shine_users.so \
|
||||
--program-id target/deploy/shine_users-keypair.json \
|
||||
--upgrade-authority /PATH/TO/UPGRADE_AUTHORITY.json \
|
||||
--url <TARGET_RPC_URL>
|
||||
```
|
||||
|
||||
Если в проекте используется рабочий Anchor deploy workflow, допустимо использовать его вместо прямого `solana program deploy`; главное — сохранить тот же Program ID.
|
||||
|
||||
После обновления:
|
||||
|
||||
```bash
|
||||
solana program show SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6 --url <TARGET_RPC_URL>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 3. Собрать серверный JAR
|
||||
|
||||
Из корня репозитория:
|
||||
|
||||
```bash
|
||||
./gradlew clean shadowJar
|
||||
```
|
||||
|
||||
Ожидаемый файл:
|
||||
|
||||
```text
|
||||
SHiNE-server/build/libs/shine-server.jar
|
||||
```
|
||||
|
||||
Если Gradle wrapper не может скачать зависимости, сборку выполнять на машине/CI с доступом к Maven/Gradle или с уже заполненным cache.
|
||||
|
||||
---
|
||||
|
||||
# 4. Подготовить PostgreSQL backup
|
||||
|
||||
Перед первым стартом версии со schema v23 сделать backup тестовой БД. Например:
|
||||
|
||||
```bash
|
||||
pg_dump -Fc -d '<DATABASE_URL_OR_NAME>' -f shine-before-archive-v22.dump
|
||||
```
|
||||
|
||||
Точная команда зависит от текущей схемы доступа PostgreSQL.
|
||||
|
||||
При старте сервер последовательно применит `migration_v22.sql` и `migration_v23.sql`, если это требуется текущей версии БД. Вручную migrations выполнять обычно не нужно.
|
||||
|
||||
После старта проверить:
|
||||
|
||||
```sql
|
||||
SELECT * FROM db_schema_version WHERE id=1;
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
|
||||
```text
|
||||
schema_version = 23
|
||||
```
|
||||
|
||||
И наличие:
|
||||
|
||||
```sql
|
||||
SELECT to_regclass('public.archive_chain_cursor');
|
||||
SELECT to_regclass('public.archive_publish_job');
|
||||
SELECT to_regclass('public.archive_publish_job_chain');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 5. Подготовить секреты на тестовом сервере
|
||||
|
||||
Пример:
|
||||
|
||||
```bash
|
||||
sudo -u player mkdir -p /home/player/SHiNE/secrets
|
||||
sudo chmod 700 /home/player/SHiNE/secrets
|
||||
```
|
||||
|
||||
Положить:
|
||||
|
||||
```text
|
||||
/home/player/SHiNE/secrets/archive-arweave-wallet.json
|
||||
/home/player/SHiNE/secrets/server-root.key
|
||||
/home/player/SHiNE/secrets/server-client.key
|
||||
```
|
||||
|
||||
Права:
|
||||
|
||||
```bash
|
||||
sudo chown player:player /home/player/SHiNE/secrets/*
|
||||
sudo chmod 600 /home/player/SHiNE/secrets/*
|
||||
```
|
||||
|
||||
### Форматы root/client key
|
||||
|
||||
Поддерживаются:
|
||||
- raw seed 32 bytes;
|
||||
- raw keypair 64 bytes;
|
||||
- Solana JSON array на 32/64 байта;
|
||||
- Base58 seed на 32 байта или Solana secret key на 64 байта;
|
||||
- Base64 raw/PKCS8, где seed извлекается из последних 32 bytes.
|
||||
|
||||
### Arweave wallet
|
||||
|
||||
Ожидается RSA JWK с полями `n,e,d,p,q,dp,dq,qi`. Кошелёк должен иметь достаточно AR для размера первого полного архива.
|
||||
|
||||
---
|
||||
|
||||
# 6. Настроить внешний `application.properties`
|
||||
|
||||
Сервер читает внешний `application.properties` из **WorkingDirectory процесса** и накладывает его поверх встроенного конфига. Сохранять существующие DB/Solana/server параметры и добавить archive-секцию.
|
||||
|
||||
Минимум:
|
||||
|
||||
```properties
|
||||
archive.publish.enabled=true
|
||||
archive.publish.time=00:00
|
||||
archive.publish.zoneId=Europe/Warsaw
|
||||
archive.workDir=data/archive
|
||||
archive.maxFileBytes=4000000000
|
||||
|
||||
archive.arweave.gateway=https://arweave.net
|
||||
archive.arweave.walletJwkPath=/home/player/SHiNE/secrets/archive-arweave-wallet.json
|
||||
archive.arweave.minConfirmations=1
|
||||
archive.arweave.confirmPollSeconds=30
|
||||
archive.arweave.confirmTimeoutMinutes=180
|
||||
|
||||
archive.solana.rootKeyPath=/home/player/SHiNE/secrets/server-root.key
|
||||
archive.solana.clientKeyPath=/home/player/SHiNE/secrets/server-client.key
|
||||
archive.solana.confirmPollSeconds=5
|
||||
archive.solana.confirmTimeoutMinutes=30
|
||||
archive.solana.commitment=finalized
|
||||
```
|
||||
|
||||
`archive.publish.zoneId` выбрать осознанно. Если оставить пустым, используется timezone JVM/машины. Для ежедневного запуска ровно в нужную локальную полночь лучше задать ZoneId явно.
|
||||
|
||||
### Solana RPC
|
||||
|
||||
**Отдельного archive RPC нет.** Writer использует:
|
||||
|
||||
1. `solana.users.sync.rpcUrl`, если он задан;
|
||||
2. иначе `solana.rpcUrl`.
|
||||
|
||||
Поэтому существующий рабочий RPC не дублировать в archive settings.
|
||||
|
||||
---
|
||||
|
||||
# 7. Убедиться, что server login соответствует ключам
|
||||
|
||||
`server.SHiNE.login` должен быть тем User PDA, чей archive head будет обновляться.
|
||||
|
||||
На startup `SolanaArchiveHeadWriter` проверяет:
|
||||
|
||||
```text
|
||||
derive(root private) == UserPDA.root_key
|
||||
derive(client private) == UserPDA.client_key
|
||||
```
|
||||
|
||||
При несовпадении archive publisher не стартует.
|
||||
|
||||
---
|
||||
|
||||
# 8. Развернуть JAR
|
||||
|
||||
Можно использовать существующий `deploy/scripts/deploy_server.sh`. Он:
|
||||
|
||||
- собирает `shadowJar`;
|
||||
- копирует JAR;
|
||||
- создаёт/обновляет systemd unit;
|
||||
- перезапускает сервис.
|
||||
|
||||
Типовой запуск задаётся уже существующими переменными проекта. Либо вручную заменить `shine-server.jar` в рабочей директории и перезапустить systemd service.
|
||||
|
||||
После deploy убедиться, что WorkingDirectory содержит внешний `application.properties`.
|
||||
|
||||
---
|
||||
|
||||
# 9. Первый startup
|
||||
|
||||
Смотреть лог:
|
||||
|
||||
```bash
|
||||
sudo journalctl -u <SERVICE_NAME> -f
|
||||
```
|
||||
|
||||
Ожидаемые события:
|
||||
|
||||
1. DB migration до v22;
|
||||
2. обычный Solana users sync;
|
||||
3. обычный server-to-server blockchain sync;
|
||||
4. archive publisher preflight;
|
||||
5. строка примерно:
|
||||
|
||||
```text
|
||||
Archive publisher включён: login=... dir=data/archive dailyAt=00:00 zone=...
|
||||
Следующая архивная публикация запланирована на ...
|
||||
```
|
||||
|
||||
Если остался незавершённый job, он будет продолжен **сразу после старта**, не ожидая полуночи. Новый snapshot создаётся только по расписанию.
|
||||
|
||||
---
|
||||
|
||||
# 10. Что произойдёт в первую полночь
|
||||
|
||||
Если `archive_chain_cursor` пуст:
|
||||
|
||||
- сервер проходит все локально известные `blockchain_name`;
|
||||
- для каждой берёт range `0..current_head`;
|
||||
- создаёт первый big block `#1`;
|
||||
- для каждой blockchain создаёт максимум один `UserBlockchainChunk`;
|
||||
- внутри chunk лежат все её raw records из frozen range;
|
||||
- backlink первого chunk пустой (`previous_big_block_ref = 0xFFFFFFFF`);
|
||||
- создаёт локальный файл, например:
|
||||
|
||||
```text
|
||||
data/archive/archive01.00001.12.09.26.tmp.SHiNE-archive
|
||||
```
|
||||
|
||||
- загружает его в Arweave;
|
||||
- после успешной загрузки переименовывает тот же файл, например:
|
||||
|
||||
```text
|
||||
data/archive/archive01.00001.12.09.26.<REAL_TX_ID>.SHiNE-archive
|
||||
```
|
||||
|
||||
- ждёт confirmations;
|
||||
- обычным `update_user_pda` записывает block type `100`;
|
||||
- ждёт Solana `finalized`;
|
||||
- только затем commit-ит cursors.
|
||||
|
||||
Если новых данных нет, пустой big block не создаётся.
|
||||
|
||||
---
|
||||
|
||||
# 11. Проверка результата
|
||||
|
||||
## Локальные файлы
|
||||
|
||||
```bash
|
||||
ls -lah data/archive/
|
||||
```
|
||||
|
||||
После успешного upload `.tmp.SHiNE-archive` для завершённого job оставаться не должен; должен быть файл с реальным TX ID в имени.
|
||||
|
||||
## Job DB
|
||||
|
||||
```sql
|
||||
SELECT id, big_block_number, status, created_at_ms, local_archive_path,
|
||||
arweave_confirmations, solana_signature, error_text
|
||||
FROM archive_publish_job
|
||||
ORDER BY id DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
Успех:
|
||||
|
||||
```text
|
||||
status = CURSORS_COMMITTED
|
||||
```
|
||||
|
||||
## Cursors
|
||||
|
||||
```sql
|
||||
SELECT blockchain_name,
|
||||
last_archived_source_block_number,
|
||||
last_archive_big_block_number,
|
||||
last_chunk_offset,
|
||||
last_chunk_size
|
||||
FROM archive_chain_cursor
|
||||
ORDER BY blockchain_name;
|
||||
```
|
||||
|
||||
## Локальная проекция User PDA
|
||||
|
||||
После очередной Solana sync:
|
||||
|
||||
```sql
|
||||
SELECT login, record_number, archive_head_tx_id, archive_head_hash
|
||||
FROM solana_user_pda_current
|
||||
WHERE login = '<SERVER_LOGIN>';
|
||||
```
|
||||
|
||||
`archive_head_tx_id` и `archive_head_hash` должны быть непустыми.
|
||||
|
||||
## Arweave
|
||||
|
||||
TX берётся прямо из имени финального файла. Проверить:
|
||||
|
||||
```bash
|
||||
curl -sS 'https://arweave.net/tx/<TX_ID>/status'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 12. Быстрый тест до полуночи
|
||||
|
||||
Если не хочется ждать 00:00, на тестовом сервере временно установить `archive.publish.time` на ближайшие 5–10 минут в будущем в выбранной `archive.publish.zoneId`, затем перезапустить сервис.
|
||||
|
||||
После проверки вернуть:
|
||||
|
||||
```properties
|
||||
archive.publish.time=00:00
|
||||
```
|
||||
|
||||
Не использовать интервал в минутах: scheduler специально работает по календарному локальному времени один раз в сутки.
|
||||
|
||||
---
|
||||
|
||||
# 13. Откат / выключение
|
||||
|
||||
Самый безопасный функциональный rollback:
|
||||
|
||||
```properties
|
||||
archive.publish.enabled=false
|
||||
```
|
||||
|
||||
и рестарт сервера. Тогда обычная серверная работа продолжается, archive scheduler ничего не публикует.
|
||||
|
||||
Не удалять `archive_chain_cursor`/job таблицы без причины: они нужны, чтобы после повторного включения publisher продолжил дельту, а не загрузил всю историю заново.
|
||||
|
||||
Уже опубликованные Arweave данные являются постоянными и обычным rollback сервера не удаляются.
|
||||
|
||||
---
|
||||
|
||||
# 14. Наиболее вероятные ошибки
|
||||
|
||||
### `Root key archive publisher-а не совпадает с User PDA`
|
||||
Положен неправильный root key или неверный `server.SHiNE.login`.
|
||||
|
||||
### `Client key archive publisher-а не совпадает с User PDA`
|
||||
Неверный client key.
|
||||
|
||||
### `Недостаточно AR`
|
||||
Пополнить Arweave wallet. Первый архив может быть существенно больше ежедневных дельт.
|
||||
|
||||
### Arweave upload прошёл, Solana update не прошёл
|
||||
Не удалять локальный файл/job. После исправления RPC/program/key причины restart продолжит незавершённый job.
|
||||
|
||||
### `archive cursor hash не совпадает`
|
||||
Локальная blockchain изменилась относительно уже зафиксированного cursor. Не форсировать публикацию; сначала разобраться с resync/fork.
|
||||
|
||||
### Старый `shine_users`
|
||||
Если новый update payload отклоняется программой, проверить, что целевая Solana `shine_users` действительно обновлена этой версией.
|
||||
|
||||
|
||||
## Trusted archive importer
|
||||
|
||||
На обычном тестовом сервере publisher можно оставить выключенным, но разрешить импорт от конкретного архиватора:
|
||||
|
||||
```properties
|
||||
archive.publish.enabled=false
|
||||
archive.import.allowedPublishers=<LOGIN_ARCHIVE_SERVER>
|
||||
archive.import.intervalMinutes=60
|
||||
archive.import.workDir=data/archive-import
|
||||
```
|
||||
|
||||
Несколько логинов:
|
||||
|
||||
```properties
|
||||
archive.import.allowedPublishers=server-a,server-b
|
||||
```
|
||||
|
||||
Пустая строка означает, что importer не запускается.
|
||||
|
||||
После старта проверить логи:
|
||||
|
||||
```text
|
||||
Archive importer включён. approvedPublishers=...
|
||||
```
|
||||
|
||||
Если сервер подключается к publisher впервые, importer скачает head, прочитает FULL reference table и обработает все ещё не известные big blocks от старых к новым.
|
||||
|
||||
Проверка БД:
|
||||
|
||||
```sql
|
||||
SELECT login, archive_head_tx_id, archive_imported, archive_last_imported_tx_id
|
||||
FROM solana_user_pda_current
|
||||
WHERE is_server=TRUE AND archive_head_tx_id<>''
|
||||
ORDER BY login;
|
||||
|
||||
SELECT blockchain_name, publisher_login, arweave_tx_id,
|
||||
big_block_number, chunk_offset, chunk_size, source_last_block_number
|
||||
FROM archive_blockchain_location
|
||||
ORDER BY updated_at_ms DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
После деплоя UI файл должен быть доступен по:
|
||||
|
||||
```text
|
||||
https://<UI_HOST>/Blockchain-Viewer.html
|
||||
```
|
||||
|
||||
В приложении: `Настройки → Архив блокчейна`.
|
||||
@@ -1,113 +0,0 @@
|
||||
# Проверка и эксплуатация Archive Publisher
|
||||
|
||||
## Перед ночным тестом
|
||||
|
||||
- [ ] Новый `shine_users` уже развёрнут на том Solana-кластере, который использует сервер.
|
||||
- [ ] Серверный JAR собран из этого пакета.
|
||||
- [ ] БД забэкаплена.
|
||||
- [ ] `archive.publish.enabled=true`.
|
||||
- [ ] `archive.publish.time=00:00`.
|
||||
- [ ] `archive.publish.zoneId` соответствует желаемой локальной полуночи.
|
||||
- [ ] `server.SHiNE.login` соответствует root/client keys.
|
||||
- [ ] Arweave JWK читается пользователем процесса.
|
||||
- [ ] На Arweave wallet достаточно AR.
|
||||
- [ ] `data/archive` доступна на запись.
|
||||
- [ ] В логе есть `Archive publisher включён` и точное время следующего запуска.
|
||||
|
||||
## Во время job
|
||||
|
||||
Нормальная последовательность логов/статусов:
|
||||
|
||||
```text
|
||||
SNAPSHOT_CREATED
|
||||
FILE_BUILT
|
||||
ARWEAVE_UPLOADED
|
||||
ARWEAVE_CONFIRMED
|
||||
SOLANA_SUBMITTED
|
||||
SOLANA_FINALIZED
|
||||
CURSORS_COMMITTED
|
||||
```
|
||||
|
||||
После `FILE_BUILT` существует `.tmp.SHiNE-archive`.
|
||||
После полного Arweave upload имя уже содержит настоящий TX ID.
|
||||
|
||||
## После успешного первого job
|
||||
|
||||
Проверить:
|
||||
|
||||
```bash
|
||||
find data/archive -maxdepth 1 -type f -name '*.SHiNE-archive' -ls
|
||||
```
|
||||
|
||||
```sql
|
||||
SELECT big_block_number, status, local_archive_path, arweave_confirmations
|
||||
FROM archive_publish_job ORDER BY id DESC LIMIT 1;
|
||||
```
|
||||
|
||||
```sql
|
||||
SELECT count(*) AS archived_blockchains FROM archive_chain_cursor;
|
||||
```
|
||||
|
||||
```sql
|
||||
SELECT login, archive_head_tx_id, archive_head_hash
|
||||
FROM solana_user_pda_current
|
||||
WHERE login='<SERVER_LOGIN>';
|
||||
```
|
||||
|
||||
## Проверка второй публикации
|
||||
|
||||
До следующей полуночи добавить несколько новых SHiNE records только в часть blockchain. После следующего job:
|
||||
|
||||
- в новый big block должны попасть только изменившиеся blockchain;
|
||||
- одна blockchain в новом big block должна иметь один chunk независимо от числа новых records;
|
||||
- cursor blockchain, которая не изменилась, должен остаться на старом big block/chunk;
|
||||
- backlink изменившегося chunk должен указывать на предыдущий chunk этой же blockchain;
|
||||
- FULL reference table нового big block должна содержать все предыдущие finalized big blocks.
|
||||
|
||||
## Crash/restart сценарии
|
||||
|
||||
### Restart после FILE_BUILT
|
||||
Должен использоваться тот же frozen job и тот же локальный файл.
|
||||
|
||||
### Restart после ARWEAVE_UPLOADED
|
||||
Не должно быть повторной оплаты/upload. Если TX сохранён, но rename не успел произойти, recovery переименует `.tmp` в имя с TX ID.
|
||||
|
||||
### Restart после SOLANA_FINALIZED
|
||||
При совпадении PDA head с job сервер должен только commit cursors.
|
||||
|
||||
## Обычный сервер без публикации
|
||||
|
||||
Проверить отдельно:
|
||||
|
||||
```properties
|
||||
archive.publish.enabled=false
|
||||
```
|
||||
|
||||
Сервер должен запускаться без Arweave/root/client archive key files и не создавать `archive_publish_job`.
|
||||
|
||||
|
||||
## Проверка trusted importer
|
||||
|
||||
1. На принимающем сервере указать только тестовый publisher:
|
||||
|
||||
```properties
|
||||
archive.import.allowedPublishers=<publisher-login>
|
||||
```
|
||||
|
||||
2. Перезапустить сервер.
|
||||
3. Дождаться Solana PDA sync и цикла importer-а.
|
||||
4. Проверить, что у publisher в `solana_user_pda_current` после успешного цикла `archive_imported=true`, а `archive_last_imported_tx_id=archive_head_tx_id`.
|
||||
5. Проверить `archive_blockchain_location`.
|
||||
6. Для blockchain, которой локально не хватало блоков, убедиться, что `blockchain_state.last_block_number` вырос.
|
||||
7. Для уже существующих блоков importer должен пропускать совпадающий hash, а не создавать дубликат.
|
||||
8. Временно удалить publisher из whitelist и убедиться, что новые archive heads больше не скачиваются.
|
||||
|
||||
### Проверка Viewer
|
||||
|
||||
Открыть `Настройки → Архив блокчейна`, получить ссылку и проверить:
|
||||
|
||||
- `tx`, `offset`, `size`, `blockchain` присутствуют;
|
||||
- Viewer собирает несколько chunks по backlink;
|
||||
- неправильный `blockchain` в URL приводит к ошибке проверки;
|
||||
- `channel` открывает нужный канал;
|
||||
- `message` прокручивает к нужному block number.
|
||||
@@ -1,38 +0,0 @@
|
||||
# Состав текущего patch-пакета
|
||||
|
||||
Этот этап рассчитан **поверх последнего рабочего ZIP**, присланного после успешного запуска archive publisher.
|
||||
|
||||
Пакет этого этапа добавляет:
|
||||
|
||||
- trusted archive importer;
|
||||
- whitelist publisher-ов через настройки сервера;
|
||||
- строгую проверку больших `SHINE-ARCHIVE`;
|
||||
- импорт недостающих raw SHiNE blocks через обычный validator `AddBlock`;
|
||||
- локальные поля состояния импорта в `solana_user_pda_current` и таблицу `archive_blockchain_location`;
|
||||
- schema migration v23;
|
||||
- WS API `GetArchiveBlockchainLocation`;
|
||||
- экран `Настройки → Архив блокчейна`;
|
||||
- `shine-UI/Blockchain-Viewer.html`;
|
||||
- документацию importer/viewer.
|
||||
|
||||
## Удаления
|
||||
|
||||
В **этом** обновлении удалять файлы не требуется.
|
||||
|
||||
Старые test/free-avatar исходники, если они всё ещё физически присутствуют в рабочем дереве, этим patch-пакетом не затрагиваются. Они не относятся к trusted archive importer и не должны удаляться автоматически при наложении этого обновления.
|
||||
|
||||
## Как накладывать ZIP changed-files
|
||||
|
||||
ZIP содержит только новые/изменённые файлы с путями от корня репозитория.
|
||||
|
||||
Распаковать поверх той рабочей версии, из которой сделан пакет, с заменой совпадающих файлов.
|
||||
|
||||
После наложения:
|
||||
|
||||
```bash
|
||||
./gradlew shadowJar
|
||||
```
|
||||
|
||||
Если Gradle wrapper ещё не установлен локально, сначала обеспечить доступ к уже используемой версии Gradle/кэшу.
|
||||
|
||||
При старте сервер сам должен поднять schema с v22 до v23.
|
||||
@@ -1,50 +0,0 @@
|
||||
# Manifest changed/new files
|
||||
|
||||
Основа сравнения: последний присланный рабочий ZIP `3d14e34c-4249-4e11-8042-3f3349c8e9fd.zip`.
|
||||
|
||||
- Изменённых файлов: 19
|
||||
- Новых файлов: 14
|
||||
- Удаляемых файлов: 0
|
||||
|
||||
## Изменённые файлы
|
||||
|
||||
- `SHiNE-server/shine-server-archive/src/main/java/server/archive/ArchivePublisherService.java`
|
||||
- `SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java`
|
||||
- `SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql`
|
||||
- `SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java`
|
||||
- `SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java`
|
||||
- `SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java`
|
||||
- `SHiNE-server/src/main/java/server/ws/WsServer.java`
|
||||
- `SHiNE-server/src/main/resources/application.properties`
|
||||
- `docs/Archive/01_PROTOCOL_v1.0.md`
|
||||
- `docs/Archive/02_IMPLEMENTATION_MAP.md`
|
||||
- `docs/Archive/03_DEPLOY_TEST_SERVER.md`
|
||||
- `docs/Archive/04_TEST_AND_OPERATIONS.md`
|
||||
- `docs/Archive/05_PATCH_CONTENTS_AND_REMOVALS.md`
|
||||
- `docs/Archive/06_FILE_MANIFEST.md`
|
||||
- `docs/Archive/README.md`
|
||||
- `docs/Archive/archive-publisher.example.properties`
|
||||
- `shine-UI/js/app.js`
|
||||
- `shine-UI/js/pages/settings-view.js`
|
||||
- `shine-UI/js/services/auth-service.js`
|
||||
|
||||
## Новые файлы
|
||||
|
||||
- `SHiNE-server/shine-server-archive/src/main/java/server/archive/ArchiveImportConfig.java`
|
||||
- `SHiNE-server/shine-server-archive/src/main/java/server/archive/ArchiveImportScheduler.java`
|
||||
- `SHiNE-server/shine-server-archive/src/main/java/server/archive/ArchiveImportService.java`
|
||||
- `SHiNE-server/shine-server-archive/src/main/java/server/archive/ShineArchiveReader.java`
|
||||
- `SHiNE-server/shine-server-db/src/main/java/shine/db/archive/ArchiveBlockchainLocation.java`
|
||||
- `SHiNE-server/shine-server-db/src/main/java/shine/db/archive/ArchivePublisherHead.java`
|
||||
- `SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ArchiveImportDAO.java`
|
||||
- `SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v23.sql`
|
||||
- `SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_GetArchiveBlockchainLocation_Handler.java`
|
||||
- `SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/entyties/Net_GetArchiveBlockchainLocation_Request.java`
|
||||
- `SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/entyties/Net_GetArchiveBlockchainLocation_Response.java`
|
||||
- `docs/Archive/07_ARCHIVE_IMPORT_AND_VIEWER.md`
|
||||
- `shine-UI/Blockchain-Viewer.html`
|
||||
- `shine-UI/js/pages/blockchain-archive-view.js`
|
||||
|
||||
## Удаления
|
||||
|
||||
На этом этапе файлов для удаления нет.
|
||||
@@ -1,303 +0,0 @@
|
||||
# Импорт доверенных SHINE-ARCHIVE и Blockchain Viewer
|
||||
|
||||
Этот документ описывает вторую половину архивной системы: как обычный SHiNE-сервер узнаёт о новых archive head других серверов, кому доверяет, как импортирует недостающие SHiNE-блоки и как UI получает ссылку на историю конкретной `blockchain_name`.
|
||||
|
||||
## 1. Источник archive head
|
||||
|
||||
Archive importer **не делает отдельные Solana RPC-запросы**.
|
||||
|
||||
Уже существующий Solana Users Sync разбирает User PDA block type `100` и копирует его в локальную таблицу:
|
||||
|
||||
```text
|
||||
solana_user_pda_current.archive_head_tx_id
|
||||
solana_user_pda_current.archive_head_hash
|
||||
```
|
||||
|
||||
Для локального состояния importer v23 добавляет туда же:
|
||||
|
||||
```text
|
||||
archive_imported BOOLEAN
|
||||
archive_last_imported_tx_id TEXT
|
||||
```
|
||||
|
||||
Это локальные поля сервера, в Solana они не записываются.
|
||||
|
||||
Когда обычный Solana Users Sync видит тот же archive head повторно, `archive_imported` сохраняется как есть.
|
||||
|
||||
Когда `archive_head_tx_id` или `archive_head_hash` изменился:
|
||||
|
||||
```text
|
||||
archive_imported = false
|
||||
```
|
||||
|
||||
а `archive_last_imported_tx_id` сохраняет последнюю успешно обработанную точку и позволяет продолжить после сбоя.
|
||||
|
||||
## 2. Whitelist доверенных publisher-ов
|
||||
|
||||
В `application.properties` задаётся список логинов серверов, архивы которых разрешено принимать:
|
||||
|
||||
```properties
|
||||
archive.import.allowedPublishers=archive-server-1,archive-server-2
|
||||
archive.import.intervalMinutes=60
|
||||
archive.import.workDir=data/archive-import
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- логины разделяются запятыми;
|
||||
- сравнение без учёта регистра;
|
||||
- пустое `archive.import.allowedPublishers=` полностью выключает importer;
|
||||
- принимаются только строки `solana_user_pda_current` с `is_server=true`;
|
||||
- whitelist является только первым фильтром, криптографические проверки всё равно обязательны.
|
||||
|
||||
## 3. Периодическая проверка
|
||||
|
||||
После запуска сервера importer делает первую проверку примерно через 10 секунд, затем по умолчанию раз в 60 минут.
|
||||
|
||||
Каждый цикл — дешёвый запрос только к локальной PostgreSQL:
|
||||
|
||||
```text
|
||||
approved publisher
|
||||
AND is_server=true
|
||||
AND archive_head_tx_id != ''
|
||||
AND archive_imported=false
|
||||
```
|
||||
|
||||
Если таких строк нет, Arweave не вызывается.
|
||||
|
||||
## 4. Проверки archive block
|
||||
|
||||
Для каждого pending publisher сервер проверяет:
|
||||
|
||||
1. publisher находится в whitelist;
|
||||
2. `archive_head_tx_id/archive_head_hash` уже пришли через обычный User PDA sync;
|
||||
3. SHA-256 скачанного файла совпадает с `archive_head_hash`;
|
||||
4. `creator_login == closer_login == publisher login`;
|
||||
5. Ed25519 archive signature проверяется root key publisher-а из User PDA;
|
||||
6. каждый вложенный raw SHiNE block проходит обычную SHiNE-проверку через существующий `AddBlock` path.
|
||||
|
||||
Подпись большого архива защищает контейнер и навигацию, а подписи обычных SHiNE blocks защищают сами пользовательские данные.
|
||||
|
||||
## 5. Как определяется, что head новый
|
||||
|
||||
Отдельная таблица обработанных TX для основной логики не нужна.
|
||||
|
||||
Текущий User PDA snapshot уже содержит:
|
||||
|
||||
```text
|
||||
archive_head_tx_id
|
||||
archive_head_hash
|
||||
archive_imported
|
||||
archive_last_imported_tx_id
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
archive_head_tx_id = TX100
|
||||
archive_imported = false
|
||||
archive_last_imported_tx_id = TX97
|
||||
```
|
||||
|
||||
Это означает: Solana уже объявила `TX100` текущей головой publisher-а, но локальный сервер успел импортировать только до `TX97`.
|
||||
|
||||
После полной успешной обработки `TX100`:
|
||||
|
||||
```text
|
||||
archive_imported = true
|
||||
archive_last_imported_tx_id = TX100
|
||||
```
|
||||
|
||||
При следующем новом PDA head Users Sync сам сбросит `archive_imported=false`.
|
||||
|
||||
## 6. Догон пропущенных больших блоков
|
||||
|
||||
Если сервер был выключен и вместо `TX97` сразу увидел `TX100`, он скачивает и проверяет `TX100`, читает его FULL reference table и находит `TX97`.
|
||||
|
||||
После этого импортирует только:
|
||||
|
||||
```text
|
||||
TX98
|
||||
TX99
|
||||
TX100
|
||||
```
|
||||
|
||||
После каждого полностью импортированного большого блока `archive_last_imported_tx_id` сдвигается вперёд.
|
||||
|
||||
Если сервер впервые видит publisher и `archive_last_imported_tx_id` пустой, импортируются все previous refs от старых к новым, затем текущий head.
|
||||
|
||||
Если непустой `archive_last_imported_tx_id` отсутствует в FULL history текущего head, importer останавливается: это рассматривается как возможная смена/fork archive chain, а не как повод молча забыть старый cursor.
|
||||
|
||||
## 7. Crash recovery
|
||||
|
||||
Если процесс упал после `TX98`, но до `TX100`:
|
||||
|
||||
```text
|
||||
archive_imported = false
|
||||
archive_last_imported_tx_id = TX98
|
||||
```
|
||||
|
||||
Следующий часовой цикл продолжит с `TX99`.
|
||||
|
||||
Если процесс успел импортировать head и записать `archive_last_imported_tx_id = TX100`, но упал до установки `archive_imported=true`, следующий цикл просто завершит отметку без повторной загрузки всей цепочки.
|
||||
|
||||
Все cursor updates выполняются условно по ожидаемому `archive_head_tx_id`. Если обычный Solana Users Sync успел заменить head во время импорта, старый процесс не сможет пометить новый head импортированным.
|
||||
|
||||
## 8. Импорт `UserBlockchainChunk`
|
||||
|
||||
Для каждого chunk:
|
||||
|
||||
1. берётся lock этой `blockchain_name`;
|
||||
2. если локального `blockchain_state` нет, identity создаётся по синхронизированному User PDA;
|
||||
3. raw records разбираются как обычные `BchBlockEntry`;
|
||||
4. уже существующий block допускается только при совпадении hash;
|
||||
5. новый block должен идти строго `localLast + 1`;
|
||||
6. новый block добавляется существующим validator/write path;
|
||||
7. конфликт hash или gap останавливает импорт этой archive chain.
|
||||
|
||||
## 9. Индекс последнего archive chunk
|
||||
|
||||
Таблица:
|
||||
|
||||
```text
|
||||
archive_blockchain_location
|
||||
```
|
||||
|
||||
содержит для каждой `blockchain_name`:
|
||||
|
||||
```text
|
||||
blockchain_name
|
||||
publisher_login
|
||||
arweave_tx_id
|
||||
archive_hash
|
||||
big_block_number
|
||||
chunk_offset
|
||||
chunk_size
|
||||
source_last_block_number
|
||||
updated_at_ms
|
||||
```
|
||||
|
||||
Если blockchain встретилась в новом archive block, её location обновляется. Если не встретилась — старая ссылка остаётся.
|
||||
|
||||
Эту таблицу заполняют как trusted importer, так и локальный archive publisher.
|
||||
|
||||
## 10. API для UI
|
||||
|
||||
WS operation:
|
||||
|
||||
```text
|
||||
GetArchiveBlockchainLocation
|
||||
```
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "GetArchiveBlockchainLocation",
|
||||
"blockchainName": "alice-001"
|
||||
}
|
||||
```
|
||||
|
||||
Response содержит:
|
||||
|
||||
```text
|
||||
blockchainName
|
||||
publisherLogin
|
||||
arweaveTxId
|
||||
archiveHash
|
||||
bigBlockNumber
|
||||
chunkOffset
|
||||
chunkSize
|
||||
sourceLastBlockNumber
|
||||
```
|
||||
|
||||
## 11. UI и ссылка Viewer
|
||||
|
||||
В настройках пользователя есть экран `Архив блокчейна`.
|
||||
|
||||
Viewer-файл:
|
||||
|
||||
```text
|
||||
shine-UI/Blockchain-Viewer.html
|
||||
```
|
||||
|
||||
Основные параметры ссылки:
|
||||
|
||||
```text
|
||||
/Blockchain-Viewer.html
|
||||
?tx=<ARWEAVE_TX_ID>
|
||||
&offset=<CHUNK_OFFSET>
|
||||
&size=<CHUNK_SIZE>
|
||||
&blockchain=<BLOCKCHAIN_NAME>
|
||||
```
|
||||
|
||||
Дополнительно:
|
||||
|
||||
```text
|
||||
&channel=<CHANNEL_NAME>
|
||||
&message=<BLOCK_NUMBER>
|
||||
```
|
||||
|
||||
`blockchain` используется также для проверки: если загруженный chunk имеет другое имя blockchain, Viewer прекращает обработку.
|
||||
|
||||
`channel` открывает нужный канал, а `message` прокручивает к указанному сообщению/block number и выделяет его.
|
||||
|
||||
## 12. Как Viewer собирает всю цепочку
|
||||
|
||||
Viewer начинает с последнего `TX + offset + size`:
|
||||
|
||||
```text
|
||||
последний UserBlockchainChunk
|
||||
↓
|
||||
PreviousBlockchainChunkRef
|
||||
↓
|
||||
FULL reference table текущего big block
|
||||
↓
|
||||
TX предыдущего big block
|
||||
↓
|
||||
Range предыдущего chunk
|
||||
↓
|
||||
следующий backlink
|
||||
↓
|
||||
до NO_REFERENCE
|
||||
```
|
||||
|
||||
Чужие chunks скачивать не требуется.
|
||||
|
||||
## 13. Минимальная настройка принимающего сервера
|
||||
|
||||
```properties
|
||||
archive.publish.enabled=false
|
||||
archive.import.allowedPublishers=server-a,server-b
|
||||
archive.import.intervalMinutes=60
|
||||
archive.import.workDir=data/archive-import
|
||||
```
|
||||
|
||||
Если импорт архивов не нужен:
|
||||
|
||||
```properties
|
||||
archive.import.allowedPublishers=
|
||||
```
|
||||
|
||||
Тогда importer вообще не запускается.
|
||||
|
||||
## 14. Диагностика PostgreSQL
|
||||
|
||||
Pending archive heads:
|
||||
|
||||
```sql
|
||||
SELECT login, archive_head_tx_id, archive_imported, archive_last_imported_tx_id
|
||||
FROM solana_user_pda_current
|
||||
WHERE is_server = TRUE
|
||||
AND archive_head_tx_id <> ''
|
||||
ORDER BY login;
|
||||
```
|
||||
|
||||
Последние известные пользовательские chunks:
|
||||
|
||||
```sql
|
||||
SELECT blockchain_name, publisher_login, arweave_tx_id,
|
||||
big_block_number, chunk_offset, chunk_size, source_last_block_number
|
||||
FROM archive_blockchain_location
|
||||
ORDER BY updated_at_ms DESC;
|
||||
```
|
||||
@@ -1,42 +0,0 @@
|
||||
# SHiNE Archive Publisher — документация
|
||||
|
||||
Эта папка — **актуальная точка входа** для механизма серверной архивации SHiNE в Arweave с фиксацией archive head в Solana User PDA.
|
||||
|
||||
Если задачу выполняет другая нейронка/агент, читать документы нужно в таком порядке:
|
||||
|
||||
1. `01_PROTOCOL_v1.0.md` — бинарный формат `SHINE-ARCHIVE`, big-block references, `UserBlockchainChunk`, подписи, PDA block `100`, crash-safety.
|
||||
2. `02_IMPLEMENTATION_MAP.md` — как спецификация разложена по Java/Rust/JS/SQL файлам текущего проекта.
|
||||
3. `03_DEPLOY_TEST_SERVER.md` — полный порядок установки на тестовый сервер, включая обязательный апгрейд `shine_users`, конфиг, ключи, сборку и запуск.
|
||||
4. `04_TEST_AND_OPERATIONS.md` — что проверять до полуночи, после полуночи и при сбоях.
|
||||
5. `05_PATCH_CONTENTS_AND_REMOVALS.md` — какие файлы содержит пакет и какие legacy test-free-avatar файлы нужно удалить при наложении ZIP поверх старого исходника.
|
||||
6. `07_ARCHIVE_IMPORT_AND_VIEWER.md` — whitelist доверенных publisher-ов, импорт archive chain, индекс последнего chunk, UI и `Blockchain-Viewer.html`.
|
||||
7. `archive-publisher.example.properties` — пример конфигурации publisher + importer.
|
||||
|
||||
## Коротко
|
||||
|
||||
- Архиватор **по умолчанию выключен**: `archive.publish.enabled=false`.
|
||||
- При включении создаёт новый snapshot **один раз в сутки в заданное локальное время**, по умолчанию `00:00`.
|
||||
- При первом успешном запуске, когда архивных курсоров ещё нет, в первый big block попадает **всё локально известное состояние всех blockchain, начиная с source block 0**.
|
||||
- Далее публикуется только дельта.
|
||||
- Один `blockchain_name` в одном big block представлен максимум одним `UserBlockchainChunk`; внутри него лежат все новые raw SHiNE records этой цепочки.
|
||||
- В конце chunk одна ссылка на предыдущий chunk этой же blockchain. Если blockchain в текущем big block отсутствует, её cursor/head не меняется.
|
||||
- Готовый файл сначала существует локально как `<login>.<00001>.<dd.MM.yy>.tmp.SHiNE-archive`. После успешной загрузки в Arweave он переименовывается в `<login>.<00001>.<dd.MM.yy>.<REAL_ARWEAVE_TX_ID>.SHiNE-archive` и остаётся локально.
|
||||
- После Arweave confirmations обычным `update_user_pda` обновляется PDA block type `100`: `archive_tx_id[32] + archive_hash[32]`.
|
||||
- Cursor commit выполняется только после Solana `finalized`.
|
||||
|
||||
## Важно перед тестом
|
||||
|
||||
Изменён формат/парсер `shine_users`. **Нельзя просто заменить серверный JAR и включить archive publisher, если целевая Solana-программа `shine_users` ещё не обновлена кодом из этого пакета.** Сначала обновить программу на нужном кластере, затем сервер.
|
||||
|
||||
|
||||
## Импорт архивов других серверов
|
||||
|
||||
Импорт по умолчанию также выключен. Настройка:
|
||||
|
||||
```properties
|
||||
archive.import.allowedPublishers=
|
||||
```
|
||||
|
||||
Пустой список означает: не доверять архивам ни одного внешнего сервера. Для разрешения перечислить логины через запятую. Подробности — `07_ARCHIVE_IMPORT_AND_VIEWER.md`.
|
||||
|
||||
Текущая схема БД: **v23**. `v22` добавила publisher, `v23` добавляет локальные `archive_imported/archive_last_imported_tx_id`, trusted importer и универсальный `archive_blockchain_location`.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Минимальный пример для archive-capable сервера.
|
||||
# Добавлять во внешний application.properties; существующие DB/Solana/server настройки не удалять.
|
||||
|
||||
archive.publish.enabled=true
|
||||
archive.publish.time=00:00
|
||||
archive.publish.zoneId=Europe/Warsaw
|
||||
archive.workDir=data/archive
|
||||
archive.maxFileBytes=4000000000
|
||||
|
||||
archive.arweave.gateway=https://arweave.net
|
||||
archive.arweave.walletJwkPath=/home/player/SHiNE/secrets/archive-arweave-wallet.json
|
||||
archive.arweave.minConfirmations=1
|
||||
archive.arweave.confirmPollSeconds=30
|
||||
archive.arweave.confirmTimeoutMinutes=180
|
||||
|
||||
# Отдельный archive Solana RPC НЕ задаётся.
|
||||
# Используется solana.users.sync.rpcUrl, иначе solana.rpcUrl.
|
||||
archive.solana.rootKeyPath=/home/player/SHiNE/secrets/server-root.key
|
||||
archive.solana.clientKeyPath=/home/player/SHiNE/secrets/server-client.key
|
||||
# Эти файлы могут содержать Base58 seed 32 bytes или Base58 Solana secret key 64 bytes.
|
||||
archive.solana.confirmPollSeconds=5
|
||||
archive.solana.confirmTimeoutMinutes=30
|
||||
archive.solana.commitment=finalized
|
||||
|
||||
|
||||
# =============================================================
|
||||
# Trusted archive import (independent from publisher)
|
||||
# Empty = do not import archives from any external server.
|
||||
# Comma-separated SHiNE server logins, case-insensitive.
|
||||
# =============================================================
|
||||
archive.import.allowedPublishers=
|
||||
archive.import.intervalMinutes=60
|
||||
archive.import.workDir=data/archive-import
|
||||
@@ -1,49 +1,116 @@
|
||||
# Общий формат добавляемого блока (Frame v0)
|
||||
# Общий формат пользовательского блока SHiNE — Frame v1 / ANS-104
|
||||
|
||||
Этот файл описывает **единый бинарный формат** блока, который клиент отправляет через `AddBlock` в поле `blockBytesB64`.
|
||||
Актуальный формат не поддерживает старый Frame v0. Новый пользовательский блок сразу создаётся как готовый подписанный ANS-104 DataItem.
|
||||
|
||||
## 1. Полная структура блока
|
||||
## 1. Два уровня формата
|
||||
|
||||
Блок состоит из двух частей:
|
||||
Полный объект, который клиент отправляет в `AddBlock`, хранится в PostgreSQL и затем архивируется в Arweave:
|
||||
|
||||
1. **PREIMAGE** (подписывается)
|
||||
2. **TAIL** (маркер подписи + подпись)
|
||||
```text
|
||||
ANS-104 DataItem
|
||||
signature_type = 2 (generic Ed25519)
|
||||
signature = 64 bytes
|
||||
owner = 32-byte blockchain public key
|
||||
target = absent
|
||||
anchor = absent
|
||||
tags
|
||||
data = SHiNE Frame v1
|
||||
```
|
||||
|
||||
### PREIMAGE
|
||||
Цепочка SHiNE **не зависит от Arweave**. Arweave DataItem ID хранится отдельно и используется для дедупликации/поиска, но не является `prevHash`.
|
||||
|
||||
- `frameCode (uint16)`
|
||||
- `prevHash32 (32 bytes)`
|
||||
- `blockSize (int32)` — размер PREIMAGE
|
||||
- `blockNumber (int32)`
|
||||
- `timestamp (int64)`
|
||||
- `type (uint16)`
|
||||
- `subType (uint16)`
|
||||
- `version (uint16)`
|
||||
- `bodyBytes (N)`
|
||||
## 2. SHiNE Frame v1
|
||||
|
||||
### TAIL
|
||||
Все целые поля Frame v1 — BigEndian.
|
||||
|
||||
- `sigMarker (uint16)`
|
||||
- `signature64 (64 bytes, Ed25519)`
|
||||
| Поле | Размер | Описание |
|
||||
|---|---:|---|
|
||||
| `frameCode` | 2 | `0x0001` |
|
||||
| `prevHash32` | 32 | SHA-256 полного Frame v1 предыдущего SHiNE-блока; для блока 0 — нули |
|
||||
| `blockSize` | 4 | точный размер Frame v1, включая header и body |
|
||||
| `blockNumber` | 4 | номер блока, начиная с 0 |
|
||||
| `timestamp` | 8 | Unix time seconds |
|
||||
| `type` | 2 | тип сообщения |
|
||||
| `subType` | 2 | подтип |
|
||||
| `version` | 2 | версия body |
|
||||
| `body` | N | данные конкретного типа |
|
||||
|
||||
## 2. Что проверяет сервер при AddBlock
|
||||
`FRAME_HEADER_SIZE = 56` bytes.
|
||||
|
||||
- `frameCode` должен быть `0x0000`.
|
||||
- `sigMarker` должен быть `0x0100`.
|
||||
- `blockNumber` должен идти строго по порядку (`last + 1`).
|
||||
- `prevHash32` должен совпасть с вершиной цепочки на сервере.
|
||||
- `body` должен пройти `check()` для конкретного типа.
|
||||
- подпись должна валидироваться публичным ключом блокчейна.
|
||||
```text
|
||||
blockHash32 = SHA256(FrameV1Bytes)
|
||||
next.prevHash32 = blockHash32
|
||||
```
|
||||
|
||||
## 3. Ограничения
|
||||
Подписи внутри Frame v1 нет. Единственная подпись пользователя — подпись окружающего ANS-104 DataItem.
|
||||
|
||||
- максимальный полный размер блока: до 4 MiB;
|
||||
- timestamp не должен сильно уходить в будущее;
|
||||
- `bodyBytes` парсится по `type/subType/version` из заголовка блока.
|
||||
## 3. ANS-104 DataItem
|
||||
|
||||
## 4. Почему это важно
|
||||
Для тестового контура используется generic Ed25519 signature type `2`: подпись 64 bytes, owner 32 bytes. Это отдельный generic Ed25519 signer type; Solana-specific signer в текущем Turbo/arbundles имеет другой type.
|
||||
|
||||
Одинаковый общий формат позволяет:
|
||||
- передавать разные виды записей через один RPC `AddBlock`;
|
||||
- валидировать блоки единообразно;
|
||||
- расширять типы `body`, не ломая каркас блока.
|
||||
Подписывается стандартный ANS-104 deep-hash:
|
||||
|
||||
```text
|
||||
[
|
||||
"dataitem",
|
||||
"1",
|
||||
"2",
|
||||
owner,
|
||||
target(empty),
|
||||
anchor(empty),
|
||||
rawAvroTags,
|
||||
FrameV1Bytes
|
||||
]
|
||||
```
|
||||
|
||||
`rawAvroTags` — ровно те Avro-serialized bytes тегов, которые лежат внутри DataItem.
|
||||
|
||||
`dataItemId32 = SHA256(signature64)`.
|
||||
|
||||
### Обязательный тестовый тег
|
||||
|
||||
Каждый блок:
|
||||
|
||||
```text
|
||||
App = test5590
|
||||
```
|
||||
|
||||
Это временное namespace-значение для разработки. Перед реальным запуском оно будет заменено отдельным изменением протокола/кода.
|
||||
|
||||
### Канальный тег
|
||||
|
||||
Если блок относится к конкретному каналу, он дополнительно содержит:
|
||||
|
||||
```text
|
||||
c = <canonical_channel_slug>
|
||||
```
|
||||
|
||||
Slug входит в подпись DataItem и не может быть изменён сервером после подписи.
|
||||
|
||||
## 4. Что хранится в PostgreSQL
|
||||
|
||||
`blocks.block_bytes` содержит **полный serialized ANS-104 DataItem**, а не только Frame.
|
||||
|
||||
Отдельно индексируются:
|
||||
|
||||
- `block_hash` — SHA-256(Frame v1);
|
||||
- `block_signature` — 64-byte Ed25519 signature из DataItem;
|
||||
- `data_item_id` — SHA-256(signature), UNIQUE;
|
||||
- `block_number`, `bch_name`, message fields;
|
||||
- состояние публикации в Arweave.
|
||||
|
||||
Локальные `.bch`, `.tmp_bch` и marker-файлы для пользовательских blockchain больше не используются.
|
||||
|
||||
## 5. Проверка AddBlock
|
||||
|
||||
Сервер обязан:
|
||||
|
||||
1. распарсить полный ANS-104 DataItem;
|
||||
2. проверить `App=test5590`;
|
||||
3. проверить `c`, если тип блока требует канал;
|
||||
4. проверить ANS-104 Ed25519 подпись;
|
||||
5. проверить, что `owner` равен текущему blockchain public key пользователя;
|
||||
6. распарсить Frame v1 и body;
|
||||
7. проверить `blockNumber == last + 1`;
|
||||
8. проверить `prevHash32 == lastBlockHash`;
|
||||
9. записать DataItem и новое состояние атомарно в PostgreSQL.
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# ANS-104 / Arweave transport для пользовательских блоков SHiNE
|
||||
|
||||
## Цель
|
||||
|
||||
Каждый пользовательский блок уже на клиенте является самостоятельным подписанным ANS-104 DataItem. Сервер не переподписывает пользовательский контент: он проверяет его, хранит в PostgreSQL и объединяет готовые DataItems в стандартный ANS-104 bundle.
|
||||
|
||||
## Child DataItem tags
|
||||
|
||||
Обязательно для тестового контура:
|
||||
|
||||
```text
|
||||
App=test5590
|
||||
```
|
||||
|
||||
Дополнительно для блоков конкретного канала:
|
||||
|
||||
```text
|
||||
c=<canonical_channel_slug>
|
||||
```
|
||||
|
||||
Теги входят в ANS-104 подпись пользователя.
|
||||
|
||||
## Publisher
|
||||
|
||||
По умолчанию цикл — раз в 15 минут.
|
||||
|
||||
```text
|
||||
blocks.arweave_publish_pending=true
|
||||
↓
|
||||
готовые serialized DataItems
|
||||
↓
|
||||
ANS-104 binary bundle
|
||||
↓
|
||||
обычная Arweave L1 transaction
|
||||
```
|
||||
|
||||
Если pending-блоков нет, транзакция не создаётся.
|
||||
|
||||
Root transaction содержит стандартные bundle tags:
|
||||
|
||||
```text
|
||||
Bundle-Format=binary
|
||||
Bundle-Version=2.0.0
|
||||
Content-Type=application/octet-stream
|
||||
App=test5590-batch
|
||||
```
|
||||
|
||||
`App=test5590-batch` намеренно отличается от child `App=test5590`, чтобы discovery-запрос находил пользовательские блоки, а не root bundles.
|
||||
|
||||
После успешной L1-загрузки сервер ставит child-блокам:
|
||||
|
||||
- `arweave_publish_pending=false`;
|
||||
- `arweave_published_at_ms`;
|
||||
- `arweave_root_tx_id`.
|
||||
|
||||
## Importer
|
||||
|
||||
Каждый сервер может независимо искать:
|
||||
|
||||
```text
|
||||
App=test5590
|
||||
```
|
||||
|
||||
через GraphQL gateway с cursor pagination.
|
||||
|
||||
Для каждого нового DataItem:
|
||||
|
||||
1. взять `id` и `bundledIn.id`;
|
||||
2. получить root bundle;
|
||||
3. извлечь точные serialized bytes child DataItem по bundle index;
|
||||
4. проверить `dataItemId == SHA256(signature)`;
|
||||
5. проверить ANS-104 Ed25519 подпись;
|
||||
6. определить пользователя по `owner`;
|
||||
7. применить обычные проверки `AddBlock`;
|
||||
8. записать в PostgreSQL с `arweave_publish_pending=false`.
|
||||
|
||||
### Блоки могут прийти не по порядку
|
||||
|
||||
Discovery/import использует persistent queue `arweave_block_import_queue`. Если, например, block 102 увиден раньше block 101, block 102 остаётся `PENDING`; после появления 101 очередь повторно проигрывается.
|
||||
|
||||
## Дедупликация и несколько серверов
|
||||
|
||||
Один и тот же готовый DataItem имеет один `data_item_id = SHA256(signature)`. Если несколько серверов включили его в разные root bundles, локально это всё равно один логический блок: `blocks.data_item_id` уникален.
|
||||
|
||||
Импортированный из Arweave блок **не ставится обратно в publish queue**. Это предотвращает бесконечное переархивирование между серверами.
|
||||
|
||||
## Локальное хранение
|
||||
|
||||
Пользовательские blockchain-файлы на диске больше не используются. Полный serialized DataItem находится в `blocks.block_bytes` PostgreSQL.
|
||||
|
||||
## Настройки
|
||||
|
||||
См. `application.properties` и `CODEX_APPLY_ANS104_TEST5590_PATCH.md`.
|
||||
|
||||
## Что намеренно не входит в этот патч
|
||||
|
||||
Remote/homeserver signing path, связанный с внешним homeserver/ESP32 signer, не мигрируется этим патчем. Каталог `ESP32/` не изменяется. До отдельной миграции новый Frame v1/ANS-104 production path рассчитан на клиент, у которого локально доступен blockchain Ed25519 key.
|
||||
@@ -236,3 +236,16 @@
|
||||
- Добавлена поддержка командного префикса `/.` и команды `/.desc` для актуализации описания канала при чтении.
|
||||
- Зафиксированы команды `/.add` и `/.remove` для каналов `type=200` (зарезервировано под расширение участниками).
|
||||
- В `AGENTS.md` добавлено обязательное правило актуализации документации в `docs/Blockchain/`.
|
||||
|
||||
## 2026-09-20 — Frame v1 + ANS-104 DataItem / Arweave per-block transport
|
||||
|
||||
- Основание: пользователь явно подтвердил breaking change и запуск с чистой историей; patch archive, commit hash ещё отсутствует.
|
||||
- Удалена поддержка старого Frame v0 (`preimage + sigMarker + signature`).
|
||||
- Новый пользовательский block — SHiNE Frame v1 внутри стандартного ANS-104 DataItem, generic Ed25519 signature type 2.
|
||||
- Signing message синхронизирован с фактическим arbundles/Turbo DataItem: включает строку signature type и raw Avro tag bytes.
|
||||
- SHiNE chain hash остаётся независимым: `SHA256(FrameV1Bytes)`; Arweave DataItem ID хранится отдельно.
|
||||
- В тестовом контуре каждый DataItem подписывает tag `App=test5590`; channel blocks дополнительно `c=<canonical slug>`.
|
||||
- PostgreSQL стал единственным локальным хранилищем пользовательских block bytes; `.bch/.tmp_bch/marker` storage/recovery удалён.
|
||||
- Старый custom `SHINE-ARCHIVE` pipeline удалён. Добавлены standard ANS-104 bundle publisher через существующий Arweave L1 uploader и GraphQL importer.
|
||||
- Импортированные из Arweave blocks не публикуются повторно; дедупликация идёт по `data_item_id`.
|
||||
- ESP32 и связанный device code этим изменением не затронуты.
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Инструкция Codex: применить ANS-104 test5590 patch
|
||||
|
||||
## Цель
|
||||
|
||||
Перевести пользовательский blockchain SHiNE на Frame v1 внутри готовых ANS-104 DataItems и убрать старый SHINE-ARCHIVE/файловое хранение цепочек.
|
||||
|
||||
## Жёсткое ограничение
|
||||
|
||||
**Не изменять ничего в `ESP32/`.** В этом patch нет ни одного файла `ESP32/**`.
|
||||
|
||||
Remote/homeserver signer, завязанный на устройство, намеренно не мигрирован. Не пытаться «заодно исправить» его в рамках этого patch.
|
||||
|
||||
## Применение
|
||||
|
||||
1. Распаковать patch поверх корня репозитория, сохраняя относительные пути.
|
||||
2. Удалить все пути из корневого `DELETE_FILES.txt`.
|
||||
3. Проверить, что `git diff -- ESP32` пуст.
|
||||
4. Использовать чистую/dev test DB. `migration_v24.sql` намеренно откажется мигрировать непустую blockchain DB, потому что backward compatibility со старым block format не требуется.
|
||||
|
||||
## Arweave config
|
||||
|
||||
Минимально для публикации:
|
||||
|
||||
```properties
|
||||
arweave.blocks.publish.enabled=true
|
||||
arweave.blocks.publish.intervalMinutes=15
|
||||
arweave.blocks.publish.gateway=https://arweave.net
|
||||
arweave.blocks.publish.walletJwkPath=/ABSOLUTE/SECRET/PATH/arweave-wallet.json
|
||||
```
|
||||
|
||||
JWK не коммитить.
|
||||
|
||||
Для discovery/import:
|
||||
|
||||
```properties
|
||||
arweave.blocks.sync.enabled=true
|
||||
arweave.blocks.sync.intervalMinutes=15
|
||||
arweave.blocks.sync.gateway=https://turbo-gateway.com
|
||||
arweave.blocks.sync.startBlockHeight=0
|
||||
```
|
||||
|
||||
На тестах желательно установить `startBlockHeight` на высоту начала `test5590`, чтобы не сканировать лишнюю историю.
|
||||
|
||||
## Test namespace
|
||||
|
||||
Child DataItem:
|
||||
|
||||
```text
|
||||
App=test5590
|
||||
```
|
||||
|
||||
Channel child:
|
||||
|
||||
```text
|
||||
App=test5590
|
||||
c=<canonical_channel_slug>
|
||||
```
|
||||
|
||||
Root bundle:
|
||||
|
||||
```text
|
||||
Bundle-Format=binary
|
||||
Bundle-Version=2.0.0
|
||||
App=test5590-batch
|
||||
```
|
||||
|
||||
Перед production-start test namespace должен быть заменён отдельным осознанным изменением.
|
||||
|
||||
## Проверки после применения
|
||||
|
||||
Из корня репозитория:
|
||||
|
||||
```bash
|
||||
node --check shine-UI/js/services/ans104-data-item.js
|
||||
node --check shine-UI/js/services/auth-service.js
|
||||
node --check shine-UI/js/app.js
|
||||
node --check shine-UI/js/pages/settings-view.js
|
||||
```
|
||||
|
||||
Java/Gradle:
|
||||
|
||||
```bash
|
||||
./gradlew testClasses
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
Затем локальный smoke test по штатной инструкции проекта, например `./gradlew startLocal`.
|
||||
|
||||
В среде, где готовился patch, Gradle wrapper не смог скачать Gradle 8.14 из-за отсутствия внешнего сетевого доступа к `services.gradle.org`. Поэтому полный Gradle compile/test обязательно прогнать после применения в обычной dev-среде.
|
||||
|
||||
## Smoke scenario
|
||||
|
||||
1. Создать/использовать тестового пользователя с локальным blockchain Ed25519 key.
|
||||
2. Добавить обычный block и убедиться, что `blocks.block_bytes` начинается с ANS-104 DataItem, а `data_item_id` заполнен.
|
||||
3. Создать channel и post; проверить `c=<canonical slug>`.
|
||||
4. Включить publisher, дождаться цикла или вызвать сервис тестом; проверить root Arweave tx.
|
||||
5. На второй чистой test DB включить importer и убедиться, что `App=test5590` blocks восстанавливаются в правильном порядке.
|
||||
6. Убедиться, что imported blocks имеют `arweave_publish_pending=false`.
|
||||
7. Проверить, что повторный discovery не создаёт дублей.
|
||||
|
||||
## Не делать в этом patch
|
||||
|
||||
- не добавлять backward compatibility Frame v0;
|
||||
- не возвращать `.bch` storage;
|
||||
- не возвращать SHINE-ARCHIVE;
|
||||
- не менять ESP32;
|
||||
- не мигрировать remote/homeserver signing без отдельного решения пользователя;
|
||||
- не заменять `prevHash` на Arweave DataItem ID.
|
||||
+34
-41
@@ -1,47 +1,40 @@
|
||||
# Документация блокчейна SHiNE (MVP)
|
||||
# SHiNE Blockchain
|
||||
|
||||
Этот каталог описывает только текущий рабочий формат протокола для MVP.
|
||||
Актуальная версия пользовательского blockchain использует **SHiNE Frame v1 внутри подписанного ANS-104 DataItem**. Старый Frame v0 и файловое хранение `.bch` не поддерживаются.
|
||||
|
||||
## Основные документы
|
||||
1. [01_Common_Block_Format.md](./01_Common_Block_Format.md)
|
||||
Единый бинарный формат блока (Frame v0), подпись, базовые проверки.
|
||||
2. [02_Blockchain_Kinds_and_Lines.md](./02_Blockchain_Kinds_and_Lines.md)
|
||||
Виды цепочек и правила line-полей.
|
||||
3. [10_TECH_Blocks.md](./10_TECH_Blocks.md)
|
||||
Системные блоки (`msg_type=0`).
|
||||
4. [11_TEXT_Blocks.md](./11_TEXT_Blocks.md)
|
||||
Текстовые блоки (`msg_type=1`).
|
||||
5. [12_REACTION_Blocks.md](./12_REACTION_Blocks.md)
|
||||
Реакции (`msg_type=2`).
|
||||
6. [13_CONNECTION_Blocks.md](./13_CONNECTION_Blocks.md)
|
||||
Социальные связи (`msg_type=3`).
|
||||
7. [14_USER_PARAM_Blocks.md](./14_USER_PARAM_Blocks.md)
|
||||
Параметры пользователя (`msg_type=4`).
|
||||
8. [15_STATUS_ACTION_Blocks.md](./15_STATUS_ACTION_Blocks.md)
|
||||
Статусные действия пользователя (`msg_type=5`).
|
||||
9. [16_TEXT_Attachments.md](./16_TEXT_Attachments.md)
|
||||
Вложения в TEXT-сообщениях через `S:att v=1`, включая опциональные `preAr/preSha256` для видео и крупных изображений.
|
||||
10. [16_TEXT_Channel_Meta.md](./16_TEXT_Channel_Meta.md)
|
||||
Скрытый `TEXT_CHANNEL_META` для профиля канала.
|
||||
11. [01_Channel_Types_and_CreateChannel.md](./01_Channel_Types_and_CreateChannel.md)
|
||||
Типы каналов и формат `CreateChannelBody`.
|
||||
12. [02_Channel_Commands.md](./02_Channel_Commands.md)
|
||||
Команды в текстовых сообщениях каналов.
|
||||
13. [CHANGELOG.md](./CHANGELOG.md)
|
||||
Журнал изменений документации.
|
||||
|
||||
## Смежная документация
|
||||
- [../ИТХ/README.md](../ИТХ/README.md) — ежедневное закрытие блокчейна (ИТХ): краткий обзор.
|
||||
- [../ИТХ/Спецификация_ИТХ_v1.md](../ИТХ/Спецификация_ИТХ_v1.md) — точная спецификация чекпоинтов (Arweave/Solana/канал закрытий).
|
||||
- [sync-between-servers.md](./sync-between-servers.md) — живая межсерверная синхронизация блокчейна и доставка DM на единственный сервер получателя.
|
||||
- [`01_Common_Block_Format.md`](01_Common_Block_Format.md) — точный Frame v1, ANS-104, подпись, хэши и теги.
|
||||
- [`17_ANS104_Arweave_Transport.md`](17_ANS104_Arweave_Transport.md) — публикация блоков в Arweave и обратный импорт между серверами.
|
||||
- [`sync-between-servers.md`](sync-between-servers.md) — межсерверная синхронизация и full-resync без файловой копии blockchain.
|
||||
- [`CHANGELOG.md`](CHANGELOG.md) — история изменений протокола.
|
||||
|
||||
## Важные ограничения MVP
|
||||
- Каналы `type=100` и `type=200` присутствуют в формате, но сейчас не используются в UI.
|
||||
- Поддерживаемый рабочий сценарий UI на текущем этапе: `stories (type=0)` и `public (type=1)`.
|
||||
## Короткая схема
|
||||
|
||||
## Обязательное сопровождение
|
||||
- При любом изменении формата/правил блокчейна в коде документы этого каталога обновляются в том же наборе изменений.
|
||||
- Обычный `AddBlock` сейчас пишет через `<blockchainName>.tmp_bch`, `<blockchainName>.write_check` и `<blockchainName>.write_pending`; эта схема и `BlockchainTmpRecoveryOnStartup` должны быть описаны в актуальной документации по синхронизации и recovery.
|
||||
- Для runtime-агрегатов статистики `user_stats_state` и `channel_stats_state` действует тот же принцип derived state: они обновляются вместе с `AddBlock` и полностью пересобираются при full resync.
|
||||
- Если в старых данных есть канал владельца, которого ещё нет в `solana_user_pda_current`, сервер не падает: `channel_stats_state` всё равно обновляется, а `user_stats_state` создаётся только после появления пользователя в Solana PDA.
|
||||
- Каждое обновление документов фиксируется в `CHANGELOG.md` с датой/временем и хэшем коммита-основания.
|
||||
```text
|
||||
User
|
||||
-> создаёт Frame v1
|
||||
-> tags: App=test5590, при канале c=<slug>
|
||||
-> Ed25519 подписывает ANS-104 deep-hash
|
||||
-> готовый DataItem
|
||||
-> AddBlock
|
||||
|
||||
Server
|
||||
-> verify DataItem + SHiNE chain
|
||||
-> PostgreSQL
|
||||
-> каждые ~15 минут ANS-104 bundle
|
||||
-> Arweave L1
|
||||
|
||||
Other servers
|
||||
-> GraphQL App=test5590
|
||||
-> скачивают/извлекают DataItem
|
||||
-> verify
|
||||
-> PostgreSQL без повторной публикации
|
||||
```
|
||||
|
||||
## Важные свойства
|
||||
|
||||
- SHiNE `prevHash` остаётся SHA-256 предыдущего Frame v1 и не зависит от Arweave.
|
||||
- `data_item_id` нужен для Arweave, поиска и дедупликации.
|
||||
- PostgreSQL — единственное локальное хранилище пользовательских блоков.
|
||||
- Тестовый namespace `App=test5590` специально отделён от будущего production namespace.
|
||||
|
||||
@@ -1,280 +1,42 @@
|
||||
# Синхронизация блокчейнов и доставка DM между серверами SHiNE
|
||||
# Синхронизация blockchain между SHiNE-серверами
|
||||
|
||||
Документ описывает архитектуру и протокол синхронизации данных между партнёрскими серверами SHiNE.
|
||||
## Локальная модель хранения
|
||||
|
||||
## 1. Зачем нужна синхронизация
|
||||
PostgreSQL является единственным локальным хранилищем пользовательских блоков. Файлы `<blockchain>.bch`, `.tmp_bch`, `.write_pending`, `.write_check`, `.resync_pending` не используются.
|
||||
|
||||
Пользователи SHiNE могут быть «приписаны» к разным серверам.
|
||||
Когда пользователь A (на сервере X) пишет пользователю B (на сервере Y):
|
||||
`blocks.block_bytes` хранит полный подписанный ANS-104 DataItem. `blockchain_state` хранит текущую вершину цепочки.
|
||||
|
||||
1. Сервер X принимает сообщение;
|
||||
2. Сервер X должен переслать DM-блок серверу Y;
|
||||
3. Сервер Y сохраняет блок и доставляет в активные сессии пользователя B.
|
||||
## Обычный AddBlock
|
||||
|
||||
Аналогично, блоки пользовательского блокчейна (записи `AddBlock`) должны синхронизироваться,
|
||||
чтобы любой партнёрский сервер мог отдать полную историю пользователя.
|
||||
Все проверки и запись выполняются под lock конкретной chain. После проверки Frame v1, подписи и `prevHash` одна SQL-транзакция записывает block + derived state + новую вершину.
|
||||
|
||||
## 2. Список серверов синхронизации (`sync_servers`)
|
||||
Локально созданный пользовательский блок получает `arweave_publish_pending=true`.
|
||||
|
||||
Каждый сервер регистрирует в своей Solana PDA список `sync_servers` —
|
||||
логины SHiNE-аккаунтов партнёрских серверов, с которыми он синхронизируется.
|
||||
## Периодический peer-to-peer sync
|
||||
|
||||
`sync_servers` относится к серверному узлу и не является списком
|
||||
access-серверов обычного пользователя. У пользователя действует только первый
|
||||
`access_servers[0]`.
|
||||
Старый межсерверный P2P sync может получать `GetBlockchainBlock` и применять полученный полный DataItem через `AddBlock`. При divergence full-resync:
|
||||
|
||||
- Список хранится в блоке `ServerProfileBlock` внутри `user_pda` сервера.
|
||||
- Адрес каждого партнёрского сервера читается из его PDA на Solana.
|
||||
- Синхронизация двусторонняя: оба сервера должны иметь друг друга в `sync_servers`.
|
||||
1. берёт lock chain;
|
||||
2. очищает derived rows/blocks/state через `BlockchainResyncCleanupDAO`;
|
||||
3. пересоздаёт state из актуального пользовательского реестра;
|
||||
4. последовательно проигрывает удалённую цепочку с блока 0;
|
||||
5. никаких файловых swap/recovery операций нет.
|
||||
|
||||
## 3. Что синхронизируется
|
||||
## Arweave sync
|
||||
|
||||
### 3.1 Личные сообщения (DM)
|
||||
Дополнительно каждый сервер может независимо включить `ArweaveBlockSyncScheduler`.
|
||||
|
||||
- Все DM-блоки форматов типов `1/2` (текст) и `3/4` (read-receipt).
|
||||
- Сервер-отправитель: сохраняет пару и ставит асинхронную delivery-задачу.
|
||||
- Сервер-получатель: сохраняет входящий блок в `signed_messages`, затем доставляет его активным сессиям.
|
||||
- Дедупликация по уникальному `message_key = from|to|timeMs|nonce|type`.
|
||||
- Между access-серверами одного пользователя DM не реплицируются.
|
||||
- Полная актуальная схема: `docs/Personal_Messages/Доставка_и_синхронизация_DM.md`.
|
||||
Он ищет child DataItems по тестовому тегу `App=test5590`, проверяет их и импортирует через ту же бизнес-проверку AddBlock. Импортированные блоки не публикуются повторно.
|
||||
|
||||
### 3.2 Блоки пользовательского блокчейна
|
||||
Если блоки обнаружены не по порядку, persistent queue оставляет более поздние блоки pending до появления предыдущих.
|
||||
|
||||
- Все блоки `AddBlock` пользователей, зарегистрированных на сервере или синхронизирующихся через него.
|
||||
- Синхронизируются в обе стороны между всеми партнёрами из `sync_servers`.
|
||||
- Порядок блоков сохраняется (по глобальному номеру блока и хэшу).
|
||||
- Дедупликация по глобальному номеру блока и хэшу.
|
||||
## Источник истины для цепочки
|
||||
|
||||
### 3.3 Пользовательские настройки
|
||||
Arweave не участвует в вычислении SHiNE chain hash:
|
||||
|
||||
Пользовательские настройки хранятся локально на единственном access-сервере и
|
||||
между серверами не синхронизируются.
|
||||
```text
|
||||
block_hash = SHA256(FrameV1Bytes)
|
||||
next.prevHash32 = block_hash
|
||||
```
|
||||
|
||||
## 4. Текущая реализованная схема
|
||||
|
||||
На текущем этапе сервер уже умеет базовую межсерверную синхронизацию пользовательских блокчейнов.
|
||||
|
||||
### 4.1 Что уже сделано
|
||||
|
||||
1. При старте сервер читает свой `server.SHiNE.login`.
|
||||
2. По этому логину он загружает из Solana свою server PDA.
|
||||
3. Из неё вытаскивает список `sync_servers`.
|
||||
4. Для каждого логина партнёра сервер читает его PDA и сохраняет локально:
|
||||
- `login`
|
||||
- `server_address`
|
||||
5. После этого:
|
||||
- новые локальные `AddBlock` рассылаются партнёрам в фоне;
|
||||
- при старте запускается periodic sync;
|
||||
- periodic sync повторяется каждые `12` часов после старта.
|
||||
|
||||
### 4.2 Какие server-to-server API уже используются
|
||||
|
||||
- `ListBlockchainHeads` — список heads всех локальных цепочек партнёра;
|
||||
- `GetBlockchainBlock` — чтение одного конкретного блока партнёра;
|
||||
- `GetSyncUserProfile` — минимальный профиль пользователя для локального создания runtime-проекции пользователя и `blockchain_state` без обращения в Solana RPC.
|
||||
|
||||
### 4.3 Как сейчас работает periodic sync
|
||||
|
||||
Для каждого сервера из локальной таблицы `sync_servers`:
|
||||
|
||||
1. запрашивается `ListBlockchainHeads`;
|
||||
2. для каждой удалённой цепочки сравниваются:
|
||||
- `lastBlockNumber`
|
||||
- `lastBlockHash`
|
||||
- локальное состояние;
|
||||
3. если локальная цепочка слабее, сервер по одному блоку вызывает `GetBlockchainBlock`;
|
||||
4. каждый скачанный блок локально применяется через существующий `AddBlock`;
|
||||
5. если у сервера ещё нет локальной записи пользователя/цепочки, перед этим подготавливается локальная runtime-проекция пользователя и `blockchain_state`.
|
||||
6. если во время replay обнаруживается рассинхрон или на одинаковой высоте удалённая цепочка сильнее, запускается полный resync:
|
||||
- цепочка помечается in-memory как `resync in progress`;
|
||||
- создаётся marker-file в `data/`;
|
||||
- в одной SQL-транзакции очищаются локальные данные цепочки и корректируются чужие счётчики;
|
||||
- удаляются `.bch` и `.tmp_bch`;
|
||||
- цепочка подтягивается заново с `0` через `GetBlockchainBlock`.
|
||||
- обычный `AddBlock` на эту цепочку в этот момент возвращает `chain_resync_in_progress`.
|
||||
|
||||
### 4.4 Как именно работает full resync
|
||||
|
||||
Full resync запускается только тогда, когда:
|
||||
|
||||
- локальная chain отстаёт и обычная докачка хвоста упирается в `bad_prev_hash` или `bad_block_number`;
|
||||
- либо высота цепочек одинаковая, но удалённая версия сильнее по правилу:
|
||||
- `lastBlockNumber`;
|
||||
- `fileSizeBytes`;
|
||||
- `lastBlockHash`.
|
||||
|
||||
Порядок действий:
|
||||
|
||||
1. Ставится in-memory guard на `blockchainName`.
|
||||
2. Создаётся marker-file `<blockchainName>.resync_pending`.
|
||||
3. Обычный `AddBlock` на эту chain временно получает `chain_resync_in_progress`.
|
||||
4. Вызывается атомарный SQL cleanup одной chain:
|
||||
- уменьшаются чужие `likes_count` и `replies_count`;
|
||||
- удаляются локальные derived-state записи этой chain;
|
||||
- удаляются `blocks` и `blockchain_state` этой chain.
|
||||
5. Удаляются файлы `<blockchainName>.bch` и `<blockchainName>.tmp_bch`.
|
||||
6. Локальная chain создаётся заново через `GetSyncUserProfile` или через Solana import, если `sync.importUserProfileFromPartner.enabled=false`.
|
||||
7. Chain replay-ится с `0` через `GetBlockchainBlock`.
|
||||
8. Если всё прошло успешно, marker-file удаляется.
|
||||
9. Если на любом шаге произошёл сбой, marker-file остаётся на диске, и сервер добивает эту chain при следующем старте.
|
||||
|
||||
Важно:
|
||||
|
||||
- full resync не делает умный rollback по одному блоку;
|
||||
- full resync не трогает DM-таблицы и current users слой;
|
||||
- висячие cross-chain ссылки считаются допустимым поведением системы.
|
||||
|
||||
### 4.5 Как работает обычный `AddBlock` и его recovery
|
||||
|
||||
Обычная запись блока теперь тоже идёт через временные артефакты:
|
||||
|
||||
1. собирается `<blockchainName>.tmp_bch` как полный кандидат на замену основного файла;
|
||||
2. пишется маленький sidecar `<blockchainName>.write_check` с `blockNumber` и `blockHash`;
|
||||
3. только после этого создаётся пустой marker `<blockchainName>.write_pending`;
|
||||
4. выполняется SQL-транзакция;
|
||||
5. после `commit` tmp атомарно ставится на место основного `.bch`;
|
||||
6. marker и sidecar удаляются.
|
||||
|
||||
На старте `BlockchainTmpRecoveryOnStartup` смотрит именно на эту пару:
|
||||
|
||||
- если `write_pending` есть, recovery проверяет sidecar и БД, а затем либо завершает swap, либо чистит временные файлы;
|
||||
- если `write_pending` нет, а `tmp_bch` или `write_check` остались, это мусор и он удаляется;
|
||||
- `resync_pending` сюда не относится, это отдельный recovery-поток.
|
||||
|
||||
### 4.6 Startup recovery по marker-file
|
||||
|
||||
При старте сервер идёт в таком порядке:
|
||||
|
||||
1. `BlockchainTmpRecoveryOnStartup` для `*.write_pending` и orphan `*.tmp_bch` / `*.write_check`;
|
||||
2. `BlockchainResyncRecoveryOnStartup` для `*.resync_pending`;
|
||||
3. только потом поднимается обычный сервер и запускается `PeriodicBlockchainSyncService`.
|
||||
|
||||
Если marker-file существует:
|
||||
|
||||
- сервер не должен начинать обычную работу поверх этой chain;
|
||||
- recovery снова выполняет cleanup и replay с нуля;
|
||||
- если recovery не завершился, marker остаётся, и сервер не переходит к обычному режиму для этой chain.
|
||||
|
||||
### 4.7 Зачем понадобился `GetSyncUserProfile`
|
||||
|
||||
Изначально подготовка локальной цепочки делалась через Solana:
|
||||
|
||||
- из `blockchainName` извлекался `login`;
|
||||
- сервер вызывал import пользователя из Solana PDA;
|
||||
- по данным PDA локально создавались runtime-проекция пользователя и `blockchain_state`.
|
||||
|
||||
На практике это упёрлось в ограничение внешнего Solana RPC: при чистом старте и массовой подтяжке чужих цепочек сервер мог получать `HTTP 429`.
|
||||
|
||||
Поэтому добавлен отдельный обходной режим:
|
||||
|
||||
- настройка `sync.importUserProfileFromPartner.enabled=true`
|
||||
- в этом режиме сервер **не ходит в Solana RPC** для создания локальной цепочки во время sync;
|
||||
- вместо этого он запрашивает у сервера-партнёра `GetSyncUserProfile` и создаёт локальную запись по данным партнёра.
|
||||
- если локальная runtime-проекция пользователя уже существует, sync восстанавливает только `blockchain_state` и не трогает user-layer.
|
||||
|
||||
Это временная практическая заплатка, чтобы clean-start sync не зависел от rate limit внешнего Solana endpoint.
|
||||
|
||||
### 4.8 Что делает настройка `sync.importUserProfileFromPartner.enabled`
|
||||
|
||||
- `false` — стандартный режим, подготовка локального пользователя идёт через Solana PDA;
|
||||
- `true` — sync-режим обхода Solana, локальный пользователь создаётся по server-to-server `GetSyncUserProfile`.
|
||||
|
||||
Настройка влияет именно на этап подготовки отсутствующей локальной цепочки во время periodic sync.
|
||||
|
||||
## 5. Реализованный постоянный server-to-server транспорт
|
||||
|
||||
Этот раздел не меняет текущую семантику DM, settings и blockchain. Он описывает
|
||||
единый постоянный WSS-транспорт, через который выполняются уже существующие операции.
|
||||
|
||||
### 5.1 Межсерверное соединение
|
||||
|
||||
- Серверы устанавливают постоянное исходящее WebSocket-соединение друг с другом.
|
||||
- Адрес партнёра определяется по `server_address` из его Solana PDA.
|
||||
- После подключения отправляется `ServerHello` с `serverLogin`, версией протокола и capabilities.
|
||||
- На текущем этапе `serverLogin` принимается на доверии; подпись Ed25519 корневым ключом сервера отложена.
|
||||
- При разрыве выполняется переподключение с jitter/backoff до 60 секунд.
|
||||
- После 120 секунд отсутствия полезного трафика отправляется WebSocket ping; pong ожидается 15 секунд.
|
||||
- Один физический канал переиспользуют доставка DM и blockchain.
|
||||
|
||||
### 5.2 Доставка новых данных (push)
|
||||
|
||||
- При получении нового блока сервер может немедленно пушить его всем подключённым партнёрам.
|
||||
- Партнёр подтверждает приём (ACK). Без ACK — повтор с backoff.
|
||||
- DM использует отдельное расписание, описанное в `docs/Personal_Messages/Доставка_и_синхронизация_DM.md`.
|
||||
|
||||
### 5.3 Начальная синхронизация (backfill)
|
||||
|
||||
- При первом подключении к партнёру серверы могут обмениваться курсорами состояния блокчейнов.
|
||||
- Сервер с более полной историей досылает недостающее партнёру.
|
||||
- DM-history backfill между access-серверами отсутствует.
|
||||
|
||||
### 5.4 Разрешение конфликтов
|
||||
|
||||
- Блоки пользовательского блокчейна: порядок определяется глобальным номером блока.
|
||||
Конфликтующие ветки (fork) разрешаются по правилам `AddBlock` (см. `docs/Blockchain/README.md`).
|
||||
- DM: конфликтов нет, `message_key` уникален.
|
||||
|
||||
## 6. Маршрутизация DM между серверами
|
||||
|
||||
При отправке DM от пользователя A к пользователю B:
|
||||
|
||||
1. Клиент A отправляет пару блоков на свой сервер X.
|
||||
2. Сервер X валидирует и локально сохраняет пару.
|
||||
3. До ответа клиенту X отправляет входящую копию на единственный
|
||||
`access_servers[0]` пользователя B через `ReceiveIncomingMessage`.
|
||||
4. Успешное сохранение на сервере B означает терминальный `delivered`.
|
||||
5. При неудаче выполняются повторы через 30 секунд, 5 минут, 25 минут и 1 час.
|
||||
6. После неудачной попытки через час ставится терминальный `failed`.
|
||||
|
||||
Изменения routing B учитываются до терминального состояния, потому что список маршрутов перечитывается на каждой попытке.
|
||||
|
||||
## 7. Безопасность
|
||||
|
||||
- Все блоки подписаны ключами пользователя на клиенте — сервер не может подделать содержимое.
|
||||
- Серверы не расшифровывают DM-контент; E2EE уже выполняется клиентами.
|
||||
- При синхронизации каждый блок проходит валидацию подписи на принимающем сервере.
|
||||
- Межсерверная авторизация DM-операций пока отложена; `sourceServerLogin` временно считается доверенным.
|
||||
|
||||
## 8. Статус реализации
|
||||
|
||||
| Компонент | Статус |
|
||||
|-----------|--------|
|
||||
| Регистрация серверной PDA в Solana | ✅ Реализовано |
|
||||
| Чтение `sync_servers` из PDA | ✅ Реализовано |
|
||||
| Локальная таблица `sync_servers` | ✅ Реализовано |
|
||||
| Публичный `ListBlockchainHeads` | ✅ Реализовано |
|
||||
| Публичный `GetBlockchainBlock` | ✅ Реализовано |
|
||||
| Публичный `GetSyncUserProfile` | ✅ Реализовано |
|
||||
| Плановый blockchain sync при старте + каждые 12 часов | ✅ Реализовано |
|
||||
| Обход Solana RPC через `sync.importUserProfileFromPartner.enabled` | ✅ Реализовано |
|
||||
| Обычный `AddBlock` через `tmp_bch`/`write_check`/`write_pending` | ✅ Реализовано |
|
||||
| Межсерверный постоянный WebSocket-канал | ✅ Реализован общий `ServerConnectionPool` |
|
||||
| Асинхронная доставка DM на единственный access-сервер получателя | ✅ Реализовано |
|
||||
| Retry DM до 1 часа + UI-state | ✅ Реализовано |
|
||||
| Репликация DM и настроек между access-серверами | Не используется |
|
||||
| Push блоков блокчейна партнёрам | ✅ Выполняется через постоянный WSS-пул |
|
||||
| Periodic backfill отсутствующего хвоста | ✅ Реализовано |
|
||||
| Разрешение рассинхрона / divergence | ✅ Реализована базовая full-resync схема во время periodic sync |
|
||||
| Startup recovery по `*.resync_pending` marker-file | ✅ Реализовано |
|
||||
| Маршрутизация DM только через `access_servers[0]` | ✅ Реализовано |
|
||||
| Криптографическая server-to-server авторизация DM | Нужна реализация |
|
||||
|
||||
Текущая версия сервера использует постоянный WSS-пул для существующих
|
||||
server-to-server JSON-операций. Отдельной будущей задачей остаётся
|
||||
криптографическая авторизация server-to-server вызовов.
|
||||
|
||||
Следующие отдельные шаги после текущего этапа:
|
||||
- отдельно проверить full-resync и startup-recovery на реальном тестовом прогоне после ручного удаления БД/файлов.
|
||||
|
||||
### 8.1 Практическая проверка на тестовом сервере
|
||||
|
||||
Проверка на `server2.shineup.me` показала, что текущая схема действительно поднимает цепочку при старте:
|
||||
|
||||
- после рестарта сервер сначала проходит `BlockchainTmpRecovery`;
|
||||
- затем обрабатывает `BlockchainResyncRecovery`;
|
||||
- после этого сам догружает цепочку `aidartest-001` с `shineup.me`;
|
||||
- итоговое состояние на тестовом сервере:
|
||||
- `blockchain_state.last_block_number = 13`
|
||||
- `blocks` по `aidartest-001` = `14` записей
|
||||
|
||||
Это подтверждает, что startup sync и full-resync flow работают в живом сценарии, а не только в коде.
|
||||
Поэтому цепочка остаётся проверяемой и переносимой независимо от конкретного storage backend.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -94,7 +94,7 @@ UserPdaRecordV1
|
||||
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
||||
| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и homeserver-ы. |
|
||||
| `70` | `TrustedStateBlock` | Счетчик trusted-связей. |
|
||||
| `100` | `ArchiveHeadBlock` | Текущая голова серверного SHINE-ARCHIVE: Arweave TX ID + SHA-256 архива. |
|
||||
| `100` | `ArchiveHeadBlock` | Legacy/reserved. Новый per-block ANS-104 transport не использует это поле для пользовательской истории. |
|
||||
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
|
||||
|
||||
Правила:
|
||||
@@ -374,7 +374,7 @@ ArchiveHeadBlock
|
||||
|
||||
Семантика:
|
||||
|
||||
- `archive_tx_id` — raw 32-byte Arweave transaction id последнего опубликованного большого `SHINE-ARCHIVE`; текстовая Base64URL-форма получается вне PDA;
|
||||
- `archive_tx_id` — legacy/reserved поле старой archive-head схемы; новый per-block ANS-104 transport его не обновляет;
|
||||
- `archive_hash` — SHA-256 большого archive block по правилам `docs/Archive/01_PROTOCOL_v1.0.md`;
|
||||
- отсутствие block `100` означает, что аккаунт ещё не объявлял archive head;
|
||||
- обычный legacy `update_user_pda`, в instruction которого archive extension отсутствует, **обязан сохранить существующий ArchiveHeadBlock без изменений**;
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
- upgrade-authority программы после проверки передается DAO;
|
||||
- пользовательские операции `create_user_pda` и `update_user_pda` остаются доступными обычным пользователям при корректных подписях и оплате.
|
||||
|
||||
## ArchiveHeadBlock и серверный SHINE-ARCHIVE
|
||||
## ArchiveHeadBlock (legacy/reserved)
|
||||
|
||||
Формат User PDA поддерживает необязательный `ArchiveHeadBlock` (`block_type = 100`, `block_version = 0`):
|
||||
|
||||
@@ -145,7 +145,7 @@ archive_tx_id [32]
|
||||
archive_hash [32]
|
||||
```
|
||||
|
||||
Он хранит текущую голову архива конкретного SHiNE-аккаунта: raw Arweave TX ID и SHA-256 соответствующего большого `SHINE-ARCHIVE`. Подробный бинарный формат и серверный workflow находятся в `docs/Archive/01_PROTOCOL_v1.0.md`.
|
||||
Поле `ArchiveHeadBlock` осталось в Solana/PDA как legacy/reserved для совместимости формата PDA. Новый transport пользовательских блоков не использует server-level SHINE-ARCHIVE или archive head: каждый пользовательский block публикуется как ANS-104 DataItem внутри стандартных bundles.
|
||||
|
||||
Отдельной инструкции программы для архива нет. Используется существующий `update_user_pda`. Парсер update instruction обратно совместим:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
shine-server-blockchain — это библиотека, которая задаёт формат блока, правила парсинга/валидации тела, крипто-проверку (hash+Ed25519) и безопасную работу с файлами блокчейна (data/<name>.bch через временный .tmp_bch).
|
||||
shine-server-blockchain задаёт Frame v1, ANS-104 DataItem, правила парсинга/валидации body и криптографическую проверку. Пользовательские chain-файлы на диске больше не используются; полные DataItems хранятся в PostgreSQL.
|
||||
|
||||
Как устроена структура и логика работы
|
||||
|
||||
@@ -33,7 +33,6 @@ BchCryptoVerifier отвечает за “как получить хэш и к
|
||||
4) Утилиты вокруг имени и файлов
|
||||
|
||||
BlockchainNameUtil — извлекает login из blockchainName (отрезает 3 символа суффикса).
|
||||
FileStoreUtil — безопасное файловое хранилище:
|
||||
|
||||
|
||||
5) Объяснение структуры работы
|
||||
|
||||
Reference in New Issue
Block a user