| .. | ||
| README.md | ||
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 полей идет набор типизированных блоков:
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.
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.
RootKeyBlock
- block_type: u8 = 1
- block_version: u8 = 0
- root_key: [u8; 32]
Правила:
- при создании задается корневой публичный ключ пользователя;
- при обновлении
root_keyдолжен совпадать с предыдущей записью; - ротация root-key будет отдельным форматом/сценарием в будущем.
8. ClientKeyBlock
Смена client_key пока также не проектируется как отдельная ротация. В версии 0 хранится один клиентский ключ пользователя.
ClientKeyBlock
- block_type: u8 = 2
- block_version: u8 = 0
- client_key: [u8; 32]
Правила:
- при создании задается текущий клиентский публичный ключ пользователя;
- при обновлении
client_keyдолжен совпадать с предыдущей записью; - история устройств и несколько клиентских ключей в этом формате не хранятся.
9. BlockchainRegistryBlock
Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список.
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
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:
LastBlockState
- constant: bytes = "SHiNE_LAST_BLOCK"
- login: string
- blockchain_name: string
- last_block_number: u32
- last_block_hash: [u8; 32]
- used_bytes: u64
Алгоритм:
message = SHA-256(LastBlockState bytes)
last_block_signature = Ed25519(blockchain_public_key, message)
Причина проверки подписи LastBlockState: root_key управляет Solana-записью пользователя, а blockchain_public_key подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
12. ServerProfileBlock
Блок присутствует, если пользователь выступает сервером.
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 для пользователя.
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 продолжает работать с пустым списком.
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; это задел под будущее расширение.
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-логика не реализована полностью, поэтому блок хранит только счетчик.
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.
Алгоритм:
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/внешних клиентов.
Базовая формула:
seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || suffix_utf8)
Где:
master_secret32— 32-байтовый master secret пользователя;suffix_utf8— строка назначения ключа.
Согласованные suffix:
"recovery.key"
"root.key"
"blockchain.key"
"client.key"
Соответствие:
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.