Новый протокол Solana PDA 1.2

This commit is contained in:
AidarKC
2026-09-26 11:01:14 +03:00
parent 6e5b57fd7c
commit c997057a37
47 changed files with 2392 additions and 4480 deletions
+1 -1
View File
@@ -55,7 +55,7 @@
- физическое соединение создаётся одно на `serverLogin`;
- логические операции DM, settings и blockchain используют один WSS;
- завершение `RemoteSyncSession` не закрывает физический сокет;
- известные peer берутся из `sync_servers` и первых действующих маршрутов
- известные peer берутся из актуальных server PDA/access-server маршрутов
`user_access_servers_current`;
- список перечитывается каждые 30 секунд;
- при изменении URL соединение пересоздаётся;
+3 -4
View File
@@ -100,8 +100,7 @@ homeserver.key:home-a -> ключ A
homeserver.key:home-b -> ключ B
```
Публичный ключ homeserver-а публикуется в `SessionsBlock` пользовательской PDA как
`session_pub_key` с `session_type = 100`, имя — в `session_name` (формат PDA §13).
В PDA 1.2 `SessionsBlock` удалён, поэтому homeserver-ключи больше не публикуются через user PDA. Сама детерминированная деривация ключа сохранена как отдельный механизм устройства; способ его будущей публикации/авторизации определяется отдельно от PDA 1.2.
> Это переименование прежней схемы `subserver.key:<имя>` → `homeserver.key:<имя>`.
> Термин «саб-сервер» по проекту заменяется на «homeserver».
@@ -130,8 +129,8 @@ homeserver.key:home-b -> ключ B
- старый тестовый вариант; оставлен как legacy-скетч для сравнения и диагностики.
### Формат PDA (куда попадают ключи)
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
— `RootKeyBlock` §6, `ClientKeyBlock` §7, `blockchain_public_key` §9, `SessionsBlock`/`session_type=100` §13, оплата §2.1.
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.2.md`
— `RootKeyBlock`, `ClientKeyBlock`, append-only `BlockchainRegistryBlock` и экономика лимита. `SessionsBlock` в PDA 1.2 отсутствует.
### Сервер (тестовый seed)
- `SHiNE-server/src/test/java/test/it/cases/SeedDataPopulationHelper.java` `deriveKeysFromPassword` (~246) —
@@ -14,11 +14,10 @@
- сервер проверяет формат, пользователей и подпись до сохранения;
- повторная доставка одной ревизии идемпотентна;
- более старая ревизия не заменяет новую;
- у каждого пользователя действует только access_servers[0];
- дополнительные элементы старой PDA игнорируются без fallback;
- PDA 1.2 допускает максимум один access server; если он задан, используется единственная запись;
- старые PDA 1.0 не участвуют в текущем runtime-протоколе;
- DM и настройки не реплицируются между access-серверами одного пользователя;
- sync_servers серверного PDA используются только для синхронизации
пользовательских блокчейнов.
- пользовательские blockchain синхронизируются через Arweave; `sync_servers` в PDA 1.2 отсутствует.
## 3. Типы DM
@@ -567,3 +567,20 @@ SYNC_POLL_INTERVAL_SECONDS=300
7. добавить запись в `current` и `history`;
8. добавить periodic guard раз в 5 минут;
9. сохранить отдельный `main` для запуска как процесса.
## PDA 1.2 (2026-09-25)
Модуль синхронизации принимает только текущую PDA 1.2. Legacy PDA 1.0 не проецируются в runtime-state и не мигрируются: их можно закрыть отдельной временной инструкцией `close_legacy_pda`.
Для PDA 1.2:
- `RecoveryKeyBlock`, sessions, trusted state, archive head и `sync_servers` отсутствуют;
- `BlockchainRegistryBlock` содержит append-only список fork в порядке `blockchain_key + created_at_ms + paid_limit_bytes`;
- `ServerProfileBlock` в 1.2 допускает один адрес сервера, `AccessServersBlock` — 0 или 1 access server;
- в compatibility SQL-поля проецируется **последний** fork как активный `blockchain_key/paid_limit_bytes`;
- полный список fork сохраняется в `blockchain_forks_json`, поэтому поиск пользователя по старому blockchain key остаётся возможным;
- `blockchain_name` для compatibility view вычисляется как `<normalized_login>-NNN`, где `NNN` соответствует индексу fork + 1;
- удалённые tip-поля (`used_bytes`, `last_block_*`, Arweave tx id) в PDA 1.2 больше не являются источником истины и в compatibility snapshot заполняются нейтральными значениями;
- server profile считается присутствующим, если опубликован один server address; в старые SQL-поля временно проецируется этот адрес.
Create/update транзакции больше не реконструируются байт-в-байт из instruction args. После обнаружения изменения sync-модуль перечитывает фактическую текущую PDA через Solana RPC и декодирует её. Это исключает дублирование on-chain сериализации. `close_legacy_pda` не создаёт новое состояние PDA и для runtime-sync не является пользовательским update.
+14 -506
View File
@@ -1,512 +1,20 @@
# Solana user_pda: итоговый целевой формат пользовательской записи
# SHiNE User PDA
Документ описывает целевой формат пользовательской PDA-записи `user_pda` для Solana-программы `shine_users`.
Актуальный формат: **1.2**.
Это не формат основного блокчейна SHiNE и не документация по `AddBlock`. Основной блокчейн SHiNE описан отдельно в `docs/Blockchain/`.
Каноническая спецификация находится в:
Статус документа: итоговый согласованный формат, к которому приведены `create_user_pda`, `update_user_pda` и тестовый сериализатор Solana-модуля.
`shine-solana/shine/doc/formats/shine-user-pda-format-v.1.2.md`
## 1. Назначение user_pda
Ключевые отличия от тестового 1.0:
`user_pda` хранит публичное состояние пользователя в Solana:
- удалены Recovery/Sessions/Trusted/ArchiveHead и `sync_servers`;
- root — cold recovery authority;
- последний blockchain key — обычный authority;
- BlockchainRegistry хранит append-only fork history: `key[32] + created_at_ms:u64 + paid_limit_bytes:u32`;
- ServerProfile допускает один адрес в 1.2, AccessServers — 0/1 сервер;
- variable blocks имеют `payload_len:u16` для forward compatibility;
- create/update работают только с 1.2;
- legacy 1.0 можно только временно закрыть `close_legacy_pda` и вернуть lamports caller-у.
- логин пользователя;
- неизменяемые параметры создания записи;
- публичный recovery-ключ пользователя;
- корневой публичный ключ пользователя;
- клиентский публичный ключ пользователя;
- данные одного или нескольких пользовательских блокчейнов SHiNE;
- серверные данные пользователя, если пользователь выступает сервером;
- серверы доступа пользователя;
- счетчики/лимиты;
- подпись записи.
На первом этапе поддерживается один пользовательский блокчейн SHiNE, но формат блока блокчейна сразу допускает повторение таких блоков в будущем.
## 2. Адрес PDA
Адрес пользовательской PDA вычисляется по логину:
- seed prefix: `user_login=`;
- второй seed: нормализованный логин в нижнем регистре;
- program id: программа `shine_users`.
Один логин соответствует одной `user_pda`.
## 2.1. Кто оплачивает create/update PDA
- Инструкции `create_user_pda` и `update_user_pda` оплачиваются с `client_key`.
- `root_key` используется для подписи unsigned части записи через Ed25519 instruction и не является fee payer.
- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `client_key`.
## 3. Общие правила кодирования
- Числа кодируются в Little Endian.
- `u8`, `u16`, `u32`, `u64` имеют обычный фиксированный размер.
- Публичный ключ Solana/Ed25519: 32 байта.
- Ed25519-подпись: 64 байта.
- SHA-256/Solana hash: 32 байта.
- Строка переменной длины: `len: u8` + `bytes[len]` в UTF-8.
- Arweave `tx_id`: строка переменной длины. Ожидаемая практическая длина base64url tx id - 43 байта, но формат хранит длину явно.
- Все типизированные блоки после фиксированного заголовка начинаются с `block_type: u8` и `block_version: u8`.
- Отдельный `block_len` у типизированных блоков не хранится: блоки парсятся по известным полям, счетчикам и строкам с `len: u8`.
## 4. Верхний формат записи
Первые 9 полей фиксированы и идут строго в указанном порядке. Это общий заголовок записи.
| N | Поле | Тип | Размер | Правило |
|---|------|-----|--------|---------|
| 1 | `magic` | bytes | 5 | Всегда `SHiNE`. |
| 2 | `format_major` | `u8` | 1 | Для первого формата: `1`. |
| 3 | `format_minor` | `u8` | 1 | Для первой версии нового формата: `0`. |
| 4 | `record_len` | `u16` | 2 | Длина полезной записи от `magic` до `signature` включительно, без padding. |
| 5 | `created_at_ms` | `u64` | 8 | Время создания записи, Unix time в миллисекундах. Не меняется. |
| 6 | `updated_at_ms` | `u64` | 8 | Время последнего обновления записи. |
| 7 | `record_number` | `u32` | 4 | Номер версии записи пользователя. При создании `0`, при обновлении +1. |
| 8 | `prev_record_hash` | bytes | 32 | Хэш unsigned-части предыдущей записи. При создании 32 нулевых байта. |
| 9 | `login` | string | `1 + len` | Логин пользователя. Не меняется. |
После первых 9 полей идет набор типизированных блоков:
```text
UserPdaRecordV1
- fixed_header: поля 1..9
- blocks_count: u8
- blocks: TypedBlock[blocks_count]
- signature: [u8; 64]
- padding: bytes до размера PDA, если нужен
```
`blocks_count` входит в unsigned-часть записи и подписывается.
## 5. Типы блоков
Зарезервированные значения `block_type`:
| block_type | Блок | Назначение |
|------------|------|------------|
| `0` | `RecoveryKeyBlock` | Ключ восстановления пользователя. |
| `1` | `RootKeyBlock` | Корневой ключ пользователя. |
| `2` | `ClientKeyBlock` | Клиентский ключ пользователя. |
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и homeserver-ы. |
| `70` | `TrustedStateBlock` | Счетчик trusted-связей. |
| `100` | `ArchiveHeadBlock` | Legacy/reserved. Новый per-block ANS-104 transport не использует это поле для пользовательской истории. |
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
Правила:
- неизвестный `block_type` в `format_major = 1` считается ошибкой;
- обязательные блоки: `RecoveryKeyBlock`, `RootKeyBlock`, `ClientKeyBlock`, `BlockchainRegistryBlock`;
- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `SessionsBlock`, `TrustedStateBlock`, `ArchiveHeadBlock`;
- каждый обязательный блок должен встречаться ровно один раз;
- порядок блоков в записи фиксируется для простоты проверки:
`RecoveryKey`, `RootKey`, `ClientKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `Sessions`, `TrustedState`, `ArchiveHead`.
## 6. RecoveryKeyBlock
Recovery-ключ нужен для будущих сценариев восстановления и ротации остальных ключей. В текущей версии он только публикуется в записи и не меняется через обычный `update_user_pda`.
```text
RecoveryKeyBlock
- block_type: u8 = 0
- block_version: u8 = 0
- recovery_key: [u8; 32]
```
Правила:
- при создании задается публичный recovery-ключ пользователя;
- при обновлении `recovery_key` должен совпадать с предыдущей записью;
- приватный `recovery.key` в PDA не хранится;
- отдельная ротация recovery-ключа будет отдельным форматом/сценарием в будущем.
## 7. RootKeyBlock
Смена `root_key` пока не проектируется и не реализуется. Блок фиксирует только стадию `0`.
```text
RootKeyBlock
- block_type: u8 = 1
- block_version: u8 = 0
- root_key: [u8; 32]
```
Правила:
- при создании задается корневой публичный ключ пользователя;
- при обновлении `root_key` должен совпадать с предыдущей записью;
- ротация root-key будет отдельным форматом/сценарием в будущем.
## 8. ClientKeyBlock
Смена `client_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один клиентский ключ пользователя.
```text
ClientKeyBlock
- block_type: u8 = 2
- block_version: u8 = 0
- client_key: [u8; 32]
```
Правила:
- при создании задается текущий клиентский публичный ключ пользователя;
- при обновлении `client_key` должен совпадать с предыдущей записью;
- история устройств и несколько клиентских ключей в этом формате не хранятся.
## 9. BlockchainRegistryBlock
Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список.
```text
BlockchainRegistryBlock
- block_type: u8 = 3
- block_version: u8 = 0
- blockchain_count: u8
- blockchain_records: BlockchainRecord[blockchain_count]
```
Правила:
- на первом этапе `blockchain_count = 1`;
- в будущем можно увеличить количество записей без изменения смысла `BlockchainRecord`;
- каждый `BlockchainRecord` описывает один пользовательский SHiNE-блокчейн.
## 10. BlockchainRecord
```text
BlockchainRecord
- blockchain_type: u8
- blockchain_name: string
- blockchain_public_key: [u8; 32]
- paid_limit_bytes: u64
- used_bytes: u64
- last_block_number: u32
- last_block_hash: [u8; 32]
- last_block_signature: [u8; 64]
- arweave_present: u8
- arweave_tx_id: string, только если arweave_present = 1
```
`blockchain_type`:
| Значение | Смысл |
|----------|-------|
| `1` | Основной пользовательский SHiNE-блокчейн. |
Поля:
- `blockchain_name` - строковое имя пользовательского блокчейна, например `login-001`. На первом этапе для основного блокчейна пользователя используется имя вида `<login>-001`, потому что это первый блокчейн этого пользователя.
- `blockchain_public_key` - публичный ключ блокчейна пользователя.
- `paid_limit_bytes` - оплаченный лимит хранения/записей в байтах.
- `used_bytes` - сколько байт уже занято в пользовательском SHiNE-блокчейне.
- `last_block_number` - номер последнего известного блока пользовательского блокчейна.
- `last_block_hash` - хэш последнего известного блока.
- `last_block_signature` - подпись хэша специального сообщения о вершине блокчейна ключом `blockchain_public_key`.
- `arweave_present` - `0`, если ссылки нет; `1`, если ссылка есть.
- `arweave_tx_id` - Arweave transaction id, где лежит выгруженный пользовательский канал/состояние.
Arweave `tx_id` - обычное поле внутри записи конкретного блокчейна. Solana-программа не проверяет, что такой Arweave transaction действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя.
## 11. Правила обновления BlockchainRecord
При обновлении записи:
- `blockchain_type` для существующей записи не меняется;
- `blockchain_public_key` пока не ротируется автоматически; смена ключа требует отдельного согласованного сценария;
- `paid_limit_bytes` может только увеличиваться или оставаться прежним;
- при увеличении `paid_limit_bytes` пользователь платит комиссию в Solana по тарифам программы;
- `used_bytes` может только увеличиваться или оставаться прежним;
- `last_block_number` может только увеличиваться или оставаться прежним;
- `used_bytes <= paid_limit_bytes`;
- если `last_block_number` увеличился, то должны быть переданы новый `last_block_hash` и новая `last_block_signature`;
- `last_block_signature` проверяется через Ed25519-инструкцию Solana: подпись должна соответствовать хэшу сообщения `LastBlockState` и `blockchain_public_key`;
- в транзакции `create_user_pda` / `update_user_pda` две Ed25519-инструкции должны идти непосредственно перед вызовом `shine_users`: сначала подпись `root_key`, затем подпись `blockchain_public_key`;
- `arweave_tx_id` можно добавить или заменить на новый, если пользователь выгрузил более актуальное состояние в Arweave;
- уменьшать лимит, число блоков или занятый размер нельзя.
Сообщение `LastBlockState`, которое хэшируется и подписывается ключом `blockchain_public_key`:
```text
LastBlockState
- constant: bytes = "SHiNE_LAST_BLOCK"
- login: string
- blockchain_name: string
- last_block_number: u32
- last_block_hash: [u8; 32]
- used_bytes: u64
```
Алгоритм:
```text
message = SHA-256(LastBlockState bytes)
last_block_signature = Ed25519(blockchain_public_key, message)
```
Причина проверки подписи `LastBlockState`: `root_key` управляет Solana-записью пользователя, а `blockchain_public_key` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
## 12. ServerProfileBlock
Блок присутствует, если пользователь выступает сервером.
```text
ServerProfileBlock
- block_type: u8 = 30
- block_version: u8 = 0
- is_server: u8
- address_format_type: u8, только если is_server = 1
- address_format_version: u8, только если is_server = 1
- server_address: string, только если is_server = 1
- sync_servers_count: u8, только если is_server = 1
- sync_servers: string[sync_servers_count], только если is_server = 1
```
Правила:
- `is_server = 0` означает, что серверных данных нет;
- `is_server = 1` означает, что пользователь публикует серверный профиль;
- `address_format_type` — тип формата адреса сервера: `1` = URL-строка (например `https://shineup.me/ws`);
- `address_format_version` — версия формата адреса, сейчас `0`;
- `sync_servers_count` максимум `32`;
- `server_address` - строковый адрес сервера в соответствии с `address_format_type`;
- `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует серверные данные и пользовательские блокчейны SHiNE. Это server-to-server список партнёров самого серверного узла, а не список серверов доставки личных сообщений для обычного пользователя. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
## 13. AccessServersBlock
Блок хранит серверы доступа/relay для пользователя.
```text
AccessServersBlock
- block_type: u8 = 40
- block_version: u8 = 0
- access_servers_count: u8
- access_servers: string[access_servers_count]
```
Правила:
- блок может отсутствовать, если серверы доступа не заданы;
- список может обновляться при изменении маршрутизации пользователя;
- `access_servers` — массив логинов серверов доступа/relay. Формат массива
сохранён для совместимости, но текущая клиентская и серверная логика использует
только первый элемент `access_servers[0]`;
- остальные элементы игнорируются без fallback;
- официальный UI записывает ровно один сервер и позволяет только заменить его;
- Solana-программа не обязана проверять, что логин действительно зарегистрирован
как сервер.
## 14. SessionsBlock
Блок хранит опубликованные пользовательские сессии. На текущем этапе регистрация пользователя не добавляет туда записи автоматически, поэтому стандартный create/update продолжает работать с пустым списком.
```text
SessionsBlock
- block_type: u8 = 50
- block_version: u8 = 0
- sessions_mode: u8
- sessions_count: u8
- sessions: SessionRecord[sessions_count]
```
`sessions_mode`:
| Значение | Смысл |
|----------|-------|
| `1` | Можно использовать и сессии, зарегистрированные в PDA, и сессии, созданные вне PDA. |
| `10` | Зарезервировано на будущее: можно использовать только сессии, опубликованные в PDA. |
Сейчас рабочий режим по умолчанию: `sessions_mode = 1`. Серверная логика пока не реализует особое поведение для `10`; это задел под будущее расширение.
```text
SessionRecord
- session_type: u8
- session_version: u8
- session_name: string
- session_pub_key: [u8; 32]
```
`session_type`:
| Значение | Смысл |
|----------|-------|
| `1` | Обычная пользовательская сессия. |
| `50` | Кошелёк пользователя. |
| `100` | Homeserver пользователя. |
Правила:
- максимум `64` записей на пользователя;
- `session_name` не пустой, максимум `64` байта;
- `session_name` может содержать только символы `[A-Za-z0-9_]`;
- `session_version` сейчас должна быть равна `1`;
- внутри одного блока должны быть уникальны и `session_name`, и `session_pub_key`;
- на текущем этапе UI и регистрация не обязаны добавлять туда записи автоматически.
## 15. TrustedStateBlock
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
```text
TrustedStateBlock
- block_type: u8 = 70
- block_version: u8 = 0
- trusted_count: u8 = 0
```
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
## 15.1. ArchiveHeadBlock
Необязательный блок текущей головы серверного архива. Он используется archive-capable сервером и хранится в том же User PDA.
```text
ArchiveHeadBlock
- block_type: u8 = 100
- block_version: u8 = 0
- archive_tx_id: [u8; 32]
- archive_hash: [u8; 32]
```
Семантика:
- `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 без изменений**;
- расширенный `update_user_pda` может заменить archive head или явно очистить его; отдельной Solana instruction для архива нет.
`ArchiveHeadBlock` входит в unsigned bytes User PDA и тем самым покрывается обычной root-подписью записи.
## 16. Подпись user_pda
Подписывается не вся PDA целиком, а unsigned-часть записи:
- от `magic` до последнего байта последнего типизированного блока включительно;
- включая `record_len`, `blocks_count`, все заголовки блоков и тела блоков;
- без поля `signature`;
- без padding.
Алгоритм:
```text
message = hash(unsigned_record_bytes)
signature = Ed25519(root_key, message)
```
Solana-программа проверяет подпись через встроенную Ed25519-инструкцию. Подписантом должен быть `root_key` из `RootKeyBlock`.
Для `shine_users` эта инструкция должна стоять в транзакции сразу перед Ed25519-инструкцией `last_block_signature` и непосредственно перед самой `create/update`-инструкцией программы.
Смену формата подписи сейчас не трогаем.
## 17. Регистрация пользователя
При регистрации:
- PDA еще не должна существовать;
- логин проходит проверку формата и login guard;
- `record_number = 0`;
- `prev_record_hash = 0x00...00`;
- `created_at_ms = updated_at_ms`;
- обязательные блоки присутствуют;
- создается минимум один `BlockchainRecord`;
- новый `SessionsBlock` может присутствовать, но при обычной регистрации сейчас записывается пустой список с `sessions_mode = 1`;
- `ArchiveHeadBlock` при регистрации не обязателен; обычный пользователь/сервер может начать публиковать архив позже;
- стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит;
- `used_bytes <= paid_limit_bytes`;
- пользователь платит регистрационную комиссию;
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
- вся unsigned-часть записи подписана `root_key`.
## 18. Обновление пользователя
При обновлении:
- PDA должна существовать;
- `login`, `created_at_ms`, `recovery_key`, `root_key`, `client_key` не меняются;
- `record_number = previous_record_number + 1`;
- `prev_record_hash` равен хэшу unsigned-части предыдущей записи;
- `updated_at_ms` обновляется;
- unsigned-часть новой записи подписана `root_key`;
- если archive extension в instruction отсутствует (legacy client), старый `ArchiveHeadBlock` сохраняется; если extension присутствует, применяется переданное `archive_head_update`;
- лимиты блокчейнов могут только увеличиваться;
- занятый размер и номер последнего блока не могут уменьшаться;
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
## 19. Отличия от старого линейного формата
Старый формат после `login` хранил поля линейно:
- `root_key_status`;
- `root_key`;
- `blockchain_key_status`;
- `blockchain_key`;
- `client_key_status`;
- `client_key`;
- `chain_number`;
- `balance`;
- серверные поля;
- access-серверы;
- `trusted_count`;
- `reserved`;
- `signature`.
Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки:
- recovery-ключ становится отдельным обязательным блоком;
- ключи становятся отдельными блоками;
- данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`;
- серверные данные и access-серверы отделяются от данных блокчейна;
- расширение формата делается добавлением новых версий блоков или новых `block_type`, а не вставкой полей в середину линейной записи.
## 20. Деривация ключей из master secret
Сама Solana-программа не вычисляет ключи из секрета и не хранит приватные ключи. Но текущая согласованная клиентская схема деривации для публичной версии формата фиксируется здесь как reference для UI/ESP32/внешних клиентов.
Базовая формула:
```text
seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || suffix_utf8)
```
Где:
- `master_secret32` — 32-байтовый master secret пользователя;
- `suffix_utf8` — строка назначения ключа.
Согласованные suffix:
```text
"recovery.key"
"root.key"
"blockchain.key"
"client.key"
```
Соответствие:
```text
recovery.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "recovery.key")
root.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "root.key")
blockchain.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "blockchain.key")
client.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "client.key")
```
Далее каждая строка `seed` интерпретируется off-chain как `seed32` для отдельной пары Ed25519.
## 21. Что пока не входит в формат
Пока не проектируем:
- ротацию `recovery_key`;
- ротацию `root_key`;
- сложную ротацию `client_key`;
- ротацию `blockchain_public_key`;
- проверку содержимого Arweave transaction;
- хранение полной истории пользовательского блокчейна внутри Solana;
- подключение Solana-модуля к сборке/деплою основного сервера SHiNE.
Формат пользовательских SHiNE/Arweave блоков этим изменением не меняется.
+18 -141
View File
@@ -2,156 +2,33 @@
## Кратко
`shine_users` — вторая программа Solana-модуля SHiNE. Она отвечает за создание и обновление пользовательской PDA-записи, проверку подписи записи, проверку логина через `shine_login_guard` и оплату регистрации/дополнительного лимита.
`shine_users` создаёт и обновляет пользовательскую PDA SHiNE, проверяет authority/signature, логин через `shine_login_guard` и оплату регистрации/дополнительного лимита через `shine_payments`.
Папка программы: `shine-solana/shine/programs/shine_users/`.
Текущий формат пользовательской записи — **PDA 1.2**. Каноническая бинарная спецификация находится в:
## Текущие функции
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.2.md`
- `shine-solana/shine/doc/programs/shine_users.md`
1. `init_users_economy_config`
- Создает PDA с экономическими настройками пользователей.
- Записывает стартовую регистрационную комиссию, цену шага лимита и стартовый бонус лимита.
Эти документы являются источником истины; здесь фиксируется только архитектурное резюме.
2. `update_users_economy_config`
- Обновляет экономические настройки.
- Требует подпись `DAO_AUTHORITY` из общего deploy-конфига.
## PDA 1.2
3. `create_user_pda`
- Проверяет логин через `shine_login_guard`.
- Проверяет структуру полей пользователя.
- Проверяет подпись записи root-ключом пользователя.
- Создает `user_pda` по seed `login=<normalized_login>`.
- Переводит оплату регистрации и дополнительного лимита в `shine_payments::inflow_vault_pda`.
Основные данные:
4. `update_user_pda`
- Проверяет неизменяемые поля пользователя.
- Проверяет `prev_hash`, новую подпись и новое состояние последнего блока.
- При необходимости расширяет PDA.
- Переводит оплату дополнительного лимита в `shine_payments::inflow_vault_pda`.
- `RootKeyBlock` — cold recovery authority;
- `ClientKeyBlock` — клиентский/кошелёчный ключ;
- `BlockchainRegistryBlock` — append-only список fork: `blockchain_key[32] + created_at_ms:u64 + paid_limit_bytes:u32`; последний fork активен;
- необязательный `ServerProfileBlock` — в 1.2 ровно один адрес сервера;
- необязательный `AccessServersBlock` — в 1.2 максимум один access server.
## Аргументы инструкций
Из нового формата удалены `RecoveryKeyBlock`, `SessionsBlock`, `TrustedStateBlock`, `ArchiveHeadBlock`, `sync_servers` и поля вершины пользовательского blockchain. Сами пользовательские блоки и их история живут вне PDA; долговременное хранение/синхронизация выполняется через Arweave.
`init_users_economy_config` аргументов не принимает.
Обычный update выполняется активным последним blockchain key и не может менять root. Root используется для recovery и может менять root. Внутри PDA сохраняются `record_number`, `prev_record_hash` и Ed25519-подпись нового состояния.
`update_users_economy_config`:
Новый обычный fork можно добавить не раньше чем через 72 часа после `created_at_ms` текущего fork; root-recovery может обойти cooldown. Клиентский timestamp принимается только в окне ±5 минут относительно Solana Clock.
- `registration_fee_lamports: u64`
- `lamports_per_limit_step: u64`
- `start_bonus_limit: u64`
Legacy PDA 1.0 не мигрируются. Для тестовых 1.0 аккаунтов предусмотрена временная инструкция `close_legacy_pda`, возвращающая lamports вызывающему signer.
`create_user_pda`:
- `login: String`
- `root_key: Pubkey`
- `created_at_ms: u64`
- `additional_limit: u64`
- `fields: UserMutableFields`
- `signature: Vec<u8>`
`update_user_pda`:
- `login: String`
- `root_key: Pubkey`
- `created_at_ms: u64`
- `updated_at_ms: u64`
- `version: u32`
- `prev_hash: Vec<u8>`
- `additional_limit: u64`
- `fields: UserMutableFields`
- `signature: Vec<u8>`
`UserMutableFields`:
- `client_key: Pubkey`
- `blockchain_public_key: Pubkey`
- `blockchain_name: String`
- `used_bytes: u64`
- `last_block_number: u32`
- `last_block_hash: Vec<u8>` — ровно 32 байта
- `last_block_signature: Vec<u8>` — ровно 64 байта
- `arweave_tx_id: String`
- `is_server: bool`
- `server_key: Pubkey`
- `server_address: String`
- `sync_servers: Vec<String>`
- `access_servers: Vec<String>` — формат остаётся массивом, но runtime
использует только первый элемент
- `trusted_count: u8`
## Главные PDA
1. `user_pda`
- PDA записи пользователя.
- Seed: `login=<normalized_login>`.
- Создается отдельно для каждого логина.
- Стартовый размер: `768` байт.
- При обновлении может расширяться через `realloc`, но один auto-realloc ограничен `10_000` байт.
2. `users_economy_config_pda`
- PDA с настройками экономики.
- Seed: `shine_users_economy_config`.
- Хранит регистрационную комиссию, цену шага лимита и стартовый бонус.
- Размер PDA: `8 + 96` байт.
## Текущие параметры экономики
Параметры initial config из `programs/shine_users/src/settings.rs`:
| Поле | Значение | Смысл |
| --- | --- | --- |
| `START_REGISTRATION_FEE_LAMPORTS` | `10_000_000` | стартовая комиссия регистрации, 0.01 SOL |
| `LIMIT_STEP` | `10_000` | шаг `additional_limit` |
| `START_LAMPORTS_PER_LIMIT_STEP` | `100_000` | 0.0001 SOL за один шаг лимита |
| `START_BONUS_LIMIT` | `100_000` | стартовый бесплатный лимит при регистрации |
`additional_limit` в create/update должен быть кратен `LIMIT_STEP`.
## Связь с другими программами
`shine_users` зависит от:
- `shine_login_guard` — для проверки логина при создании пользователя;
- `shine_payments` — для вычисления и проверки `inflow_vault_pda`, куда уходят платежи.
`create_user_pda` делает CPI-вызов `shine_login_guard::classify_login` и принимает только результат `0`. Premium/trademark логины сейчас отклоняются ошибками `PremiumLogin` или `TrademarkLoginRequiresReview`.
Подпись `user_pda` и подпись состояния последнего блока проверяются через встроенную Solana Ed25519-инструкцию, которая должна идти раньше инструкции `shine_users` в той же транзакции.
## Деньги
Деньги из `shine_users` идут только в `inflow_vault_pda` программы `shine_payments`.
Потоки:
- `create_user_pda`: регистрационная комиссия + оплата `additional_limit`;
- `update_user_pda`: оплата `additional_limit`, если она больше нуля.
## Ключи и управление
На старте удобно считать, что у программы есть отдельный управляющий ключ `key_2`.
Целевая модель:
- economy-настройки меняет DAO-authority;
- upgrade-authority программы после проверки передается DAO;
- пользовательские операции `create_user_pda` и `update_user_pda` остаются доступными обычным пользователям при корректных подписях и оплате.
## ArchiveHeadBlock (legacy/reserved)
Формат User PDA поддерживает необязательный `ArchiveHeadBlock` (`block_type = 100`, `block_version = 0`):
```text
archive_tx_id [32]
archive_hash [32]
```
Поле `ArchiveHeadBlock` осталось в Solana/PDA как legacy/reserved для совместимости формата PDA. Новый transport пользовательских блоков не использует server-level SHINE-ARCHIVE или archive head: каждый пользовательский block публикуется как ANS-104 DataItem внутри стандартных bundles.
Отдельной инструкции программы для архива нет. Используется существующий `update_user_pda`. Парсер update instruction обратно совместим:
- legacy payload без archive extension сохраняет старый block `100`;
- новый payload может заменить/очистить archive head;
- итоговая полная User PDA запись, включая block `100`, покрывается обычной root-подписью.
Это позволяет обычным старым клиентским обновлениям профиля не стирать archive head серверного publisher-а.
## Экономика
Параметры экономики хранятся в `users_economy_config_pda`. `paid_limit_bytes` каждого fork — `u32`; старые fork-записи неизменяемы, пополняется только активный fork. Подробные формулы и ограничения описаны в документации программы.