Обновить формат Solana user PDA

This commit is contained in:
AidarKC
2026-05-24 19:41:13 +03:00
parent 74df7e2645
commit baef264bd0
7 changed files with 861 additions and 411 deletions
+306 -166
View File
@@ -1,225 +1,365 @@
# SHINY USER FORMAT v1.0 (DRAFT)
# Solana user_pda: итоговый целевой формат пользовательской записи
Документ описывает целевой бинарный формат пользовательской записи в `user_pda` для программы `shine_users`.
Документ описывает целевой формат пользовательской PDA-записи `user_pda` для Solana-программы `shine_users`.
## 1) Статус версии и цель
Это не формат основного блокчейна SHiNE и не документация по `AddBlock`. Основной блокчейн SHiNE описан отдельно в `Dev_Docs/Blockchain/`.
- Текущий on-chain формат: `v1.0`.
- Этот документ: `v1.0 (draft)` для текущего этапа.
- Цель текущей версии: зафиксировать рабочий формат и сразу оставить в нем поля для будущего расширения.
Статус документа: итоговый согласованный формат, к которому приведены `create_user_pda`, `update_user_pda` и тестовый сериализатор Solana-модуля.
Новые статусные поля:
- `root_key_status`
- `blockchain_key_status`
- `device_key_status`
## 1. Назначение user_pda
Текущее значение каждого статуса: `0` (ключ создан и не менялся).
`user_pda` хранит публичное состояние пользователя в Solana:
## 2) Общие правила кодирования
- логин пользователя;
- неизменяемые параметры создания записи;
- корневой публичный ключ пользователя;
- ключ устройства;
- данные одного или нескольких пользовательских блокчейнов SHiNE;
- серверные данные пользователя, если пользователь выступает сервером;
- серверы доступа пользователя;
- счетчики/лимиты;
- подпись записи.
- Числа: Little Endian (`LE`).
- Строки: `UTF-8` с префиксом длины `u8`.
- Публичные ключи: 32 байта (`Pubkey`).
- Подпись: 64 байта (Ed25519).
- Размер PDA фиксированный: `USER_PDA_SPACE` (сейчас 1024 байта).
- `record_len` хранит длину полезной записи от `magic` до `signature` включительно (без `padding`).
На первом этапе поддерживается один пользовательский блокчейн SHiNE, но формат блока блокчейна сразу допускает повторение таких блоков в будущем.
## 3) Единый список полей в порядке хранения
## 2. Адрес PDA
1. `magic`
Размер: 5 байт.
Значение: `"SHiNE"`.
Назначение: маркер формата записи.
Адрес пользовательской PDA вычисляется по логину:
2. `format_major`
Размер: 1 байт (`u8`).
Текущее значение: `1`.
Назначение: major-версия формата.
- seed prefix: `login=`;
- второй seed: нормализованный логин в нижнем регистре;
- program id: программа `shine_users`.
3. `format_minor`
Размер: 1 байт (`u8`).
Текущее значение: `0`.
Назначение: minor-версия формата.
Один логин соответствует одной `user_pda`.
4. `record_len`
Размер: 2 байта (`u16`, LE).
Назначение: длина полезных данных записи (без `padding`).
## 3. Общие правила кодирования
5. `created_at_ms`
Размер: 8 байт (`u64`, LE).
Назначение: время создания записи (Unix time, ms).
- Числа кодируются в 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`.
6. `updated_at_ms`
Размер: 8 байт (`u64`, LE).
Назначение: время последнего обновления записи (Unix time, ms).
## 4. Верхний формат записи
7. `record_number` (`version`)
Размер: 4 байта (`u32`, LE).
Назначение: порядковый номер записи пользователя.
Правило обновления: новая запись должна иметь `last_record_number + 1`; проверяется программой.
Первые 9 полей фиксированы и идут строго в указанном порядке. Это общий заголовок записи.
8. `prev_record_hash` (`prev_hash`)
Размер: 32 байта.
Назначение: хэш unsigned-части предыдущей записи для связи истории.
| N | Поле | Тип | Размер | Правило |
|---|------|-----|--------|---------|
| 1 | `magic` | bytes | 5 | Всегда `SHiNE`. |
| 2 | `format_major` | `u8` | 1 | Для нового формата: `2`. |
| 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. `login_len`
Размер: 1 байт (`u8`).
Назначение: длина поля `login` в байтах.
После первых 9 полей идет набор типизированных блоков:
10. `login`
Размер: `login_len` байт (UTF-8).
Назначение: логин пользователя.
Текущие ограничения: от 1 до 25 символов, только `a-z`, `0-9`, `_`.
```text
UserPdaRecordV2
- fixed_header: поля 1..9
- blocks_count: u8
- blocks: TypedBlock[blocks_count]
- signature: [u8; 64]
- padding: bytes до размера PDA, если нужен
```
11. `root_key_status`
Размер: 1 байт (`u8`).
Текущее значение: `0`.
Назначение: статус `root_key`.
Комментарий: будущие статусы ротации зарезервированы, смена root-ключа пока не реализована.
`blocks_count` входит в unsigned-часть записи и подписывается.
12. `root_key`
Размер: 32 байта (`Pubkey`).
Назначение: корневой ключ пользователя для подписи записи.
## 5. Типы блоков
13. `blockchain_key_status`
Размер: 1 байт (`u8`).
Текущее значение: `0`.
Назначение: статус `blockchain_key`.
Комментарий: будущие статусы ротации зарезервированы.
Зарезервированные значения `block_type`:
14. `blockchain_key`
Размер: 32 байта (`Pubkey`).
Назначение: рабочий блокчейн-ключ пользователя.
| block_type | Блок | Назначение |
|------------|------|------------|
| `1` | `RootKeyBlock` | Корневой ключ пользователя. |
| `2` | `DeviceKeyBlock` | Ключ устройства пользователя. |
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
| `50` | `TrustedStateBlock` | Счетчик trusted-связей. |
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
15. `device_key_status`
Размер: 1 байт (`u8`).
Текущее значение: `0`.
Назначение: статус `device_key`.
Комментарий: будущие статусы ротации зарезервированы.
Правила:
16. `device_key`
Размер: 32 байта (`Pubkey`).
Назначение: ключ устройства пользователя.
- неизвестный `block_type` в `format_major = 2` считается ошибкой;
- обязательные блоки: `RootKeyBlock`, `DeviceKeyBlock`, `BlockchainRegistryBlock`;
- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `TrustedStateBlock`;
- каждый обязательный блок должен встречаться ровно один раз;
- порядок блоков в записи фиксируется для простоты проверки:
`RootKey`, `DeviceKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `TrustedState`.
17. `chain_number`
Размер: 2 байта (`u16`, LE).
Назначение: номер блокчейн-профиля пользователя.
Текущее использование: базовый сценарий с одним профилем (обычно `1`).
## 6. RootKeyBlock
18. `balance`
Размер: 8 байт (`u64`, LE).
Назначение: лимит/баланс пользователя.
Смена `root_key` пока не проектируется и не реализуется. Блок фиксирует только стадию `0`.
19. `is_server`
Размер: 1 байт (`u8`).
Значения: `0` или `1`.
Назначение: флаг серверного профиля.
```text
RootKeyBlock
- block_type: u8 = 1
- block_version: u8 = 0
- root_key: [u8; 32]
```
20. `server_key` (только если `is_server = 1`)
Размер: 32 байта (`Pubkey`).
Назначение: публичный ключ сервера.
Правила:
21. `server_address_len` (только если `is_server = 1`)
Размер: 1 байт (`u8`).
Назначение: длина строки `server_address`.
- при создании задается корневой публичный ключ пользователя;
- при обновлении `root_key` должен совпадать с предыдущей записью;
- ротация root-key будет отдельным форматом/сценарием в будущем.
22. `server_address` (только если `is_server = 1`)
Размер: `server_address_len` байт (UTF-8).
Назначение: адрес сервера.
## 7. DeviceKeyBlock
23. `sync_servers_count` (только если `is_server = 1`)
Размер: 1 байт (`u8`).
Назначение: количество серверов, с которыми сервер синхронизирует данные.
Ограничение: максимум `32`.
Смена `device_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один ключ устройства.
24. Повтор `sync_servers_count` раз (только если `is_server = 1`):
`server_login_len` — 1 байт (`u8`),
`server_login``server_login_len` байт (UTF-8).
Назначение: логины серверов синхронизации.
```text
DeviceKeyBlock
- block_type: u8 = 2
- block_version: u8 = 0
- device_key: [u8; 32]
```
25. `access_servers_count`
Размер: 1 байт (`u8`).
Назначение: количество серверов доступа (relay), через которые можно достучаться до пользователя.
Правила:
26. Повтор `access_servers_count` раз:
`server_login_len` — 1 байт (`u8`),
`server_login``server_login_len` байт (UTF-8).
Назначение: логины серверов доступа.
- при создании задается текущий публичный ключ устройства;
- при обновлении ключ устройства может быть обновлен только если это отдельно разрешено бизнес-логикой инструкции;
- история устройств и несколько устройств в этом формате не хранятся.
27. `trusted_count`
Размер: 1 байт (`u8`).
Назначение: текущее число trusted-контактов.
Текущее состояние: пока только счетчик, без отдельной trusted-логики.
## 8. BlockchainRegistryBlock
28. `reserved`
Размер: 5 байт.
Текущее значение: `0x00 0x00 0x00 0x00 0x00`.
Назначение: резерв под будущие расширения.
Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список.
29. `signature`
Размер: 64 байта.
Назначение: Ed25519-подпись хэша unsigned-части записи.
```text
BlockchainRegistryBlock
- block_type: u8 = 3
- block_version: u8 = 0
- blockchain_count: u8
- blockchain_records: BlockchainRecord[blockchain_count]
```
30. `padding`
Размер: до полного `USER_PDA_SPACE`.
Текущее значение: `0x00`.
Назначение: добивка до фиксированного размера PDA.
Правила:
## 4) Что подписывается
- на первом этапе `blockchain_count = 1`;
- в будущем можно увеличить количество записей без изменения смысла `BlockchainRecord`;
- каждый `BlockchainRecord` описывает один пользовательский SHiNE-блокчейн.
Подписывается SHA-256 от unsigned-части записи:
- от `magic` до `reserved` включительно;
- без `signature`;
- без `padding`.
## 9. BlockchainRecord
## 5) Что сейчас работает в логике
```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
```
Сейчас в рабочем потоке используются 2 операции:
1. `create_user_pda` — регистрация пользователя.
2. `update_user_pda` — обновление записи пользователя.
`blockchain_type`:
Через `update_user_pda` сейчас можно:
- увеличить `balance` через `additional_limit`;
- обновить серверные поля (`is_server`, `server_key`, `server_address`, `sync_servers`, `access_servers`);
- увеличить `record_number` (`version`) на 1.
| Значение | Смысл |
|----------|-------|
| `1` | Основной пользовательский SHiNE-блокчейн. |
Оплата идет на адрес, заданный в `REGISTRATION_FEE_RECEIVER` (не в DAO по умолчанию).
Поля:
## 6) Ограничения и отложенные расширения
- `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, где лежит выгруженный пользовательский канал/состояние.
Это функции и сценарии, которые предусмотрены структурой данных формата `v1.0`, но пока не реализованы программно.
Arweave `tx_id` - обычное поле внутри записи конкретного блокчейна. Solana-программа не проверяет, что такой Arweave transaction действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя.
1. Смена ключей пока недоступна
`root_key`, `blockchain_key`, `device_key` считаются без ротации; статусные поля пока фактически только `0`.
## 10. Правила обновления BlockchainRecord
2. Multi-chain профили пока не реализованы
Пока используется один базовый профиль (`chain_number`), расширение до нескольких профилей/форков — отдельный этап.
При обновлении записи:
3. Trusted-логика пока не реализована
Пока хранится только `trusted_count`; список trusted, очередь, таймеры и голосование будут добавляться отдельно.
- `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`;
- `arweave_tx_id` можно добавить или заменить на новый, если пользователь выгрузил более актуальное состояние в Arweave;
- уменьшать лимит, число блоков или занятый размер нельзя.
4. Работа с несколькими серверами на уровне приложения ограничена
В записи можно хранить `sync_servers` и `access_servers`, но фактическая клиентская логика выбора/обхода серверов может быть ограничена.
Сообщение `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
```
## 7) Константы и фиксированные значения (точки будущего расширения)
Алгоритм:
Ниже перечислены места, где сейчас используются константы/фиксированные значения, а в будущем возможна доработка:
```text
message = SHA-256(LastBlockState bytes)
last_block_signature = Ed25519(blockchain_public_key, message)
```
1. Версия формата: `format_major = 1`, `format_minor = 0`.
Расширение: переход на следующую минорную/мажорную версию при изменении бинарной схемы.
Причина проверки подписи `LastBlockState`: `root_key` управляет Solana-записью пользователя, а `blockchain_public_key` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
2. Размер PDA: `USER_PDA_SPACE = 1024`.
Расширение: увеличение размера или переход на иное хранение при росте структуры.
## 11. ServerProfileBlock
3. Статусы ключей: все три `*_key_status` пока равны `0`.
Расширение: добавить коды состояний для ротации/восстановления ключей.
Блок присутствует, если пользователь выступает сервером.
4. `chain_number`: текущий рабочий сценарий с одним профилем (обычно `1`).
Расширение: поддержка нескольких блокчейн-форков.
```text
ServerProfileBlock
- block_type: u8 = 30
- block_version: u8 = 0
- is_server: u8
- server_key: [u8; 32], только если 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
```
5. `trusted_count`: пока только счетчик, обычно `0`.
Расширение: отдельные структуры trusted-списка, очередей и таймеров.
Правила:
6. `reserved` (5 байт): сейчас всегда нули.
Расширение: использовать как флаги/дополнительные поля без слома общей схемы.
- `is_server = 0` означает, что серверных данных нет;
- `is_server = 1` означает, что пользователь публикует серверный профиль;
- `sync_servers_count` максимум `32`;
- `server_address` - строковый адрес сервера в формате, который будет отдельно закреплен на уровне приложения;
- `sync_servers` - логины пользователей системы, через которых этот сервер пытается синхронизироваться. 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. TrustedStateBlock
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
```text
TrustedStateBlock
- block_type: u8 = 50
- block_version: u8 = 0
- trusted_count: u8 = 0
```
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
## 14. Подпись 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`.
Смену формата подписи сейчас не трогаем.
## 15. Регистрация пользователя
При регистрации:
- PDA еще не должна существовать;
- логин проходит проверку формата и login guard;
- `record_number = 0`;
- `prev_record_hash = 0x00...00`;
- `created_at_ms = updated_at_ms`;
- обязательные блоки присутствуют;
- создается минимум один `BlockchainRecord`;
- стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит;
- `used_bytes <= paid_limit_bytes`;
- пользователь платит регистрационную комиссию;
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
- вся unsigned-часть записи подписана `root_key`.
## 16. Обновление пользователя
При обновлении:
- 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 не валидирует.
## 17. Отличия от старого линейного формата
Старый формат после `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.