425 lines
24 KiB
Markdown
425 lines
24 KiB
Markdown
# Solana user_pda: итоговый целевой формат пользовательской записи
|
||
|
||
Документ описывает целевой формат пользовательской PDA-записи `user_pda` для Solana-программы `shine_users`.
|
||
|
||
Это не формат основного блокчейна SHiNE и не документация по `AddBlock`. Основной блокчейн SHiNE описан отдельно в `Dev_Docs/Blockchain/`.
|
||
|
||
Статус документа: итоговый согласованный формат, к которому приведены `create_user_pda`, `update_user_pda` и тестовый сериализатор Solana-модуля.
|
||
|
||
## 1. Назначение user_pda
|
||
|
||
`user_pda` хранит публичное состояние пользователя в Solana:
|
||
|
||
- логин пользователя;
|
||
- неизменяемые параметры создания записи;
|
||
- корневой публичный ключ пользователя;
|
||
- ключ устройства;
|
||
- данные одного или нескольких пользовательских блокчейнов SHiNE;
|
||
- серверные данные пользователя, если пользователь выступает сервером;
|
||
- серверы доступа пользователя;
|
||
- счетчики/лимиты;
|
||
- подпись записи.
|
||
|
||
На первом этапе поддерживается один пользовательский блокчейн SHiNE, но формат блока блокчейна сразу допускает повторение таких блоков в будущем.
|
||
|
||
## 2. Адрес PDA
|
||
|
||
Адрес пользовательской PDA вычисляется по логину:
|
||
|
||
- seed prefix: `login=`;
|
||
- второй seed: нормализованный логин в нижнем регистре;
|
||
- program id: программа `shine_users`.
|
||
|
||
Один логин соответствует одной `user_pda`.
|
||
|
||
## 2.1. Кто оплачивает create/update PDA
|
||
|
||
- Инструкции `create_user_pda` и `update_user_pda` оплачиваются с `device_key`.
|
||
- `root_key` используется для подписи unsigned части записи через Ed25519 instruction и не является fee payer.
|
||
- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `device_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 | Блок | Назначение |
|
||
|------------|------|------------|
|
||
| `1` | `RootKeyBlock` | Корневой ключ пользователя. |
|
||
| `2` | `DeviceKeyBlock` | Ключ устройства пользователя. |
|
||
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
|
||
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
|
||
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
||
| `55` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. |
|
||
| `50` | `TrustedStateBlock` | Счетчик trusted-связей. |
|
||
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
|
||
|
||
Правила:
|
||
|
||
- неизвестный `block_type` в `format_major = 1` считается ошибкой;
|
||
- обязательные блоки: `RootKeyBlock`, `DeviceKeyBlock`, `BlockchainRegistryBlock`;
|
||
- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `SessionsBlock`, `TrustedStateBlock`;
|
||
- каждый обязательный блок должен встречаться ровно один раз;
|
||
- порядок блоков в записи фиксируется для простоты проверки:
|
||
`RootKey`, `DeviceKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `Sessions`, `TrustedState`.
|
||
|
||
## 6. RootKeyBlock
|
||
|
||
Смена `root_key` пока не проектируется и не реализуется. Блок фиксирует только стадию `0`.
|
||
|
||
```text
|
||
RootKeyBlock
|
||
- block_type: u8 = 1
|
||
- block_version: u8 = 0
|
||
- root_key: [u8; 32]
|
||
```
|
||
|
||
Правила:
|
||
|
||
- при создании задается корневой публичный ключ пользователя;
|
||
- при обновлении `root_key` должен совпадать с предыдущей записью;
|
||
- ротация root-key будет отдельным форматом/сценарием в будущем.
|
||
|
||
## 7. DeviceKeyBlock
|
||
|
||
Смена `device_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один ключ устройства.
|
||
|
||
```text
|
||
DeviceKeyBlock
|
||
- block_type: u8 = 2
|
||
- block_version: u8 = 0
|
||
- device_key: [u8; 32]
|
||
```
|
||
|
||
Правила:
|
||
|
||
- при создании задается текущий публичный ключ устройства;
|
||
- при обновлении ключ устройства может быть обновлен только если это отдельно разрешено бизнес-логикой инструкции;
|
||
- история устройств и несколько устройств в этом формате не хранятся.
|
||
|
||
## 8. 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-блокчейн.
|
||
|
||
## 9. 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 действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя.
|
||
|
||
## 10. Правила обновления 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` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
|
||
|
||
## 11. 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-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
|
||
|
||
## 12. 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. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
|
||
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
|
||
|
||
## 13. SessionsBlock
|
||
|
||
Блок хранит опубликованные пользовательские сессии. На текущем этапе регистрация пользователя не добавляет туда записи автоматически, поэтому стандартный create/update продолжает работать с пустым списком.
|
||
|
||
```text
|
||
SessionsBlock
|
||
- block_type: u8 = 55
|
||
- 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` | Обычная пользовательская сессия. |
|
||
| `100` | Саб-сервер пользователя. |
|
||
|
||
Правила:
|
||
|
||
- максимум `64` записей на пользователя;
|
||
- `session_name` не пустой, максимум `64` байта;
|
||
- `session_name` может содержать только символы `[A-Za-z0-9_]`;
|
||
- `session_version` сейчас должна быть равна `1`;
|
||
- внутри одного блока должны быть уникальны и `session_name`, и `session_pub_key`;
|
||
- на текущем этапе UI и регистрация не обязаны добавлять туда записи автоматически.
|
||
|
||
## 14. TrustedStateBlock
|
||
|
||
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
|
||
|
||
```text
|
||
TrustedStateBlock
|
||
- block_type: u8 = 50
|
||
- block_version: u8 = 0
|
||
- trusted_count: u8 = 0
|
||
```
|
||
|
||
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
|
||
|
||
## 15. Подпись 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`-инструкцией программы.
|
||
|
||
Смену формата подписи сейчас не трогаем.
|
||
|
||
## 16. Регистрация пользователя
|
||
|
||
При регистрации:
|
||
|
||
- PDA еще не должна существовать;
|
||
- логин проходит проверку формата и login guard;
|
||
- `record_number = 0`;
|
||
- `prev_record_hash = 0x00...00`;
|
||
- `created_at_ms = updated_at_ms`;
|
||
- обязательные блоки присутствуют;
|
||
- создается минимум один `BlockchainRecord`;
|
||
- новый `SessionsBlock` может присутствовать, но при обычной регистрации сейчас записывается пустой список с `sessions_mode = 1`;
|
||
- стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит;
|
||
- `used_bytes <= paid_limit_bytes`;
|
||
- пользователь платит регистрационную комиссию;
|
||
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
|
||
- вся unsigned-часть записи подписана `root_key`.
|
||
|
||
## 17. Обновление пользователя
|
||
|
||
При обновлении:
|
||
|
||
- PDA должна существовать;
|
||
- `login`, `created_at_ms`, `root_key` не меняются;
|
||
- `record_number = previous_record_number + 1`;
|
||
- `prev_record_hash` равен хэшу unsigned-части предыдущей записи;
|
||
- `updated_at_ms` обновляется;
|
||
- unsigned-часть новой записи подписана `root_key`;
|
||
- лимиты блокчейнов могут только увеличиваться;
|
||
- занятый размер и номер последнего блока не могут уменьшаться;
|
||
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
|
||
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
|
||
|
||
## 18. Отличия от старого линейного формата
|
||
|
||
Старый формат после `login` хранил поля линейно:
|
||
|
||
- `root_key_status`;
|
||
- `root_key`;
|
||
- `blockchain_key_status`;
|
||
- `blockchain_key`;
|
||
- `device_key_status`;
|
||
- `device_key`;
|
||
- `chain_number`;
|
||
- `balance`;
|
||
- серверные поля;
|
||
- access-серверы;
|
||
- `trusted_count`;
|
||
- `reserved`;
|
||
- `signature`.
|
||
|
||
Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки:
|
||
|
||
- ключи становятся отдельными блоками;
|
||
- данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`;
|
||
- серверные данные и access-серверы отделяются от данных блокчейна;
|
||
- расширение формата делается добавлением новых версий блоков или новых `block_type`, а не вставкой полей в середину линейной записи.
|
||
|
||
## 18. Что пока не входит в формат
|
||
|
||
Пока не проектируем:
|
||
|
||
- ротацию `root_key`;
|
||
- сложную ротацию `device_key`;
|
||
- ротацию `blockchain_public_key`;
|
||
- проверку содержимого Arweave transaction;
|
||
- хранение полной истории пользовательского блокчейна внутри Solana;
|
||
- подключение Solana-модуля к сборке/деплою основного сервера SHiNE.
|