# 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: - логин пользователя; - неизменяемые параметры создания записи; - публичный 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-связей. | | `255` | `ReservedBlock` | Зарезервировано, пока не используется. | Правила: - неизвестный `block_type` в `format_major = 1` считается ошибкой; - обязательные блоки: `RecoveryKeyBlock`, `RootKeyBlock`, `ClientKeyBlock`, `BlockchainRegistryBlock`; - необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `SessionsBlock`, `TrustedStateBlock`; - каждый обязательный блок должен встречаться ровно один раз; - порядок блоков в записи фиксируется для простоты проверки: `RecoveryKey`, `RootKey`, `ClientKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `Sessions`, `TrustedState`. ## 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`. На первом этапе для основного блокчейна пользователя используется имя вида `-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-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. 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. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы; - точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE. ## 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-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат. ## 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`; - стартовый `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`; - лимиты блокчейнов могут только увеличиваться; - занятый размер и номер последнего блока не могут уменьшаться; - при увеличении оплаченного лимита пользователь доплачивает комиссию; - 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.