diff --git a/AGENTS.md b/AGENTS.md index ff7a0be..e7f7464 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ - Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в: - `Dev_Docs/Solana_Architecture/README.md` - Документ формата пользовательской PDA-записи `shine_users` находится в: - - `shine-solana/shine/doc/SHiNE-user-format-v.1.0.md` + - `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` ## Документация блокчейна - Актуальная документация по форматам блокчейна находится в `Dev_Docs/Blockchain/README.md`. diff --git a/Dev_Docs/Future_Features/medium/2026-06-02_сессионные_саб_серверы_в_pda.md b/Dev_Docs/Future_Features/medium/2026-06-02_сессионные_саб_серверы_в_pda.md index 0f480b0..46bd937 100644 --- a/Dev_Docs/Future_Features/medium/2026-06-02_сессионные_саб_серверы_в_pda.md +++ b/Dev_Docs/Future_Features/medium/2026-06-02_сессионные_саб_серверы_в_pda.md @@ -35,9 +35,11 @@ Предварительные значения: - тип `1` - обычная пользовательская сессия; -- тип `10` - саб-сервер пользователя; +- тип `100` - саб-сервер пользователя; - версия `1` - первая рабочая версия формата записи сессии. +На текущем этапе под это уже зарезервирован отдельный блок `SessionsBlock` с `block_type = 55`, а `TrustedStateBlock` остаётся на `50`. + Важно: саб-серверов у одного пользователя может быть несколько. ## Архитектурный принцип @@ -74,14 +76,14 @@ - максимальную длину `sessionName`; - правила удаления и обновления записи; - правила ротации `sessionPubKey`. -6. Продумать, как UI и сервер будут отличать тип `1` и тип `10`. +6. Продумать, как UI и сервер будут отличать тип `1` и тип `100`. 7. Определить, какие внутренние сообщения саб-сервера останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации. 8. Добавить API/операции чтения и обновления списка сессий, если для этого не хватит существующих механизмов. 9. После реализации обязательно обновить документацию. ## Что нужно обновить при реализации -- `shine-solana/shine/doc/SHiNE-user-format-v.1.0.md` +- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` - `Dev_Docs/Solana_Architecture/README.md` - `Dev_Docs/Инициализация_Solana_регистрации/README.md` - `Dev_Docs/Keys/README.md` diff --git a/Dev_Docs/Solana/user_pda/README.md b/Dev_Docs/Solana/user_pda/README.md index 036c130..02fd1e3 100644 --- a/Dev_Docs/Solana/user_pda/README.md +++ b/Dev_Docs/Solana/user_pda/README.md @@ -32,6 +32,12 @@ Один логин соответствует одной `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. @@ -84,6 +90,7 @@ UserPdaRecordV1 | `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. | | `30` | `ServerProfileBlock` | Серверные данные пользователя. | | `40` | `AccessServersBlock` | Серверы доступа/relay. | +| `55` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. | | `50` | `TrustedStateBlock` | Счетчик trusted-связей. | | `255` | `ReservedBlock` | Зарезервировано, пока не используется. | @@ -91,10 +98,10 @@ UserPdaRecordV1 - неизвестный `block_type` в `format_major = 1` считается ошибкой; - обязательные блоки: `RootKeyBlock`, `DeviceKeyBlock`, `BlockchainRegistryBlock`; -- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `TrustedStateBlock`; +- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `SessionsBlock`, `TrustedStateBlock`; - каждый обязательный блок должен встречаться ровно один раз; - порядок блоков в записи фиксируется для простоты проверки: - `RootKey`, `DeviceKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `TrustedState`. + `RootKey`, `DeviceKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `Sessions`, `TrustedState`. ## 6. RootKeyBlock @@ -197,6 +204,7 @@ Arweave `tx_id` - обычное поле внутри записи конкре - `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; - уменьшать лимит, число блоков или занятый размер нельзя. @@ -230,7 +238,8 @@ ServerProfileBlock - block_type: u8 = 30 - block_version: u8 = 0 - is_server: u8 -- server_key: [u8; 32], только если is_server = 1 +- 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 @@ -240,9 +249,11 @@ ServerProfileBlock - `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` - строковый адрес сервера в формате, который будет отдельно закреплен на уровне приложения; -- `sync_servers` - логины пользователей системы, через которых этот сервер пытается синхронизироваться. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы. +- `server_address` - строковый адрес сервера в соответствии с `address_format_type`; +- `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы. ## 12. AccessServersBlock @@ -263,7 +274,53 @@ AccessServersBlock - `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы; - точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE. -## 13. TrustedStateBlock +## 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-логика не реализована полностью, поэтому блок хранит только счетчик. @@ -276,7 +333,7 @@ TrustedStateBlock Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат. -## 14. Подпись user_pda +## 15. Подпись user_pda Подписывается не вся PDA целиком, а unsigned-часть записи: @@ -293,10 +350,11 @@ signature = Ed25519(root_key, message) ``` Solana-программа проверяет подпись через встроенную Ed25519-инструкцию. Подписантом должен быть `root_key` из `RootKeyBlock`. +Для `shine_users` эта инструкция должна стоять в транзакции сразу перед Ed25519-инструкцией `last_block_signature` и непосредственно перед самой `create/update`-инструкцией программы. Смену формата подписи сейчас не трогаем. -## 15. Регистрация пользователя +## 16. Регистрация пользователя При регистрации: @@ -307,13 +365,14 @@ Solana-программа проверяет подпись через встр - `created_at_ms = updated_at_ms`; - обязательные блоки присутствуют; - создается минимум один `BlockchainRecord`; +- новый `SessionsBlock` может присутствовать, но при обычной регистрации сейчас записывается пустой список с `sessions_mode = 1`; - стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит; - `used_bytes <= paid_limit_bytes`; - пользователь платит регистрационную комиссию; - если покупается дополнительный лимит, пользователь платит комиссию за этот лимит; - вся unsigned-часть записи подписана `root_key`. -## 16. Обновление пользователя +## 17. Обновление пользователя При обновлении: @@ -328,7 +387,7 @@ Solana-программа проверяет подпись через встр - при увеличении оплаченного лимита пользователь доплачивает комиссию; - Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует. -## 17. Отличия от старого линейного формата +## 18. Отличия от старого линейного формата Старый формат после `login` хранил поля линейно: diff --git a/Dev_Docs/Solana_Architecture/README.md b/Dev_Docs/Solana_Architecture/README.md index 2252f7b..5ac7041 100644 --- a/Dev_Docs/Solana_Architecture/README.md +++ b/Dev_Docs/Solana_Architecture/README.md @@ -9,7 +9,7 @@ Связанные документы: - `Dev_Docs/Инициализация_Solana_регистрации/README.md` — single source of truth по деплою и первичной инициализации регистрации пользователей. -- `shine-solana/shine/doc/SHiNE-user-format-v.1.0.md` — точный формат `user_pda` для `shine_users`. +- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` — точный формат `user_pda` для `shine_users`. - `shine-solana/shine/doc/FUNDS_FLOW.md` — короткая справка по денежным потокам внутри Solana-модуля. ## Кратко diff --git a/VERSION.properties b/VERSION.properties index ed12268..7af343b 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.124 -server.version=1.2.116 +client.version=1.2.125 +server.version=1.2.117 diff --git a/shine-solana/shine/AGENTS.md b/shine-solana/shine/AGENTS.md index 9fd62bb..0cc0386 100644 --- a/shine-solana/shine/AGENTS.md +++ b/shine-solana/shine/AGENTS.md @@ -2,11 +2,18 @@ ## Documentation Rule -В проекте есть спецификация пользовательской PDA-записи: +В проекте есть два обязательных класса документов: -- актуальные документы в `doc/`. +1. Документы по программам в `doc/programs/`: + - `shine_users.md` + - `shine_login_guard.md` + - `shine_payments.md` +2. Документы по форматам в `doc/formats/`: + - `shine-user-pda-format-v.1.0.md` -Если меняется формат записи, сериализация, правила подписи, `prev_hash`, экономика лимитов или связанные ограничения create/update, соответствующую документацию в `doc/` нужно обновлять в том же изменении. +Эти документы должны быть достаточными для повторной реализации программ и форматов с нуля. + +Если меняется формат записи, сериализация, правила подписи, `prev_hash`, экономика лимитов, правила очередей, правила CPI, seed PDA или связанные ограничения create/update, соответствующую документацию в `doc/` нужно обновлять в том же изменении. ## Language Rule @@ -21,9 +28,16 @@ Если меняется бизнес-логика смарт-контрактов, сериализация PDA, правила переводов или экономика: -1. Обновить соответствующий документ в `doc/` в том же изменении. +1. Обновить соответствующий документ в `doc/programs/` и/или `doc/formats/` в том же изменении. 2. Если документ сразу обновить нельзя, обязательно явно согласовать это с пользователем в чате и зафиксировать план обновления. +Отдельное обязательное правило: + +1. Документы по программам и документ формата PDA должны всегда совпадать с кодом программ. +2. Перед любым изменением кода нужно явно проверять, затрагивает ли оно поведение, PDA, поля, проверки, экономику или формат. +3. Если затрагивает, документ обновляется обязательно. +4. Нельзя сознательно оставлять код и документы в рассинхроне без отдельной явной договоренности с пользователем. + ## Rule: Git Push Для push в удаленный репозиторий использовать токен из переменной окружения: diff --git a/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md b/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md new file mode 100644 index 0000000..02fd1e3 --- /dev/null +++ b/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md @@ -0,0 +1,424 @@ +# 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`. На первом этапе для основного блокчейна пользователя используется имя вида `-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. diff --git a/shine-solana/shine/doc/programs/shine_login_guard.md b/shine-solana/shine/doc/programs/shine_login_guard.md new file mode 100644 index 0000000..94b3dd2 --- /dev/null +++ b/shine-solana/shine/doc/programs/shine_login_guard.md @@ -0,0 +1,138 @@ +# Программа `shine_login_guard` + +Документ описывает целевое поведение программы классификации логинов SHiNE. + +Задача программы: + +- по логину определить класс регистрации; +- вернуть результат в `return_data`; +- не хранить PDA и не вести собственное состояние. + +## 1. Назначение + +`shine_login_guard` — маленькая служебная программа, которую вызывает `shine_users` через CPI до регистрации пользователя. + +Она отвечает только на вопрос: + +- можно ли зарегистрировать логин автоматически; +- является ли логин premium; +- является ли логин trademark/company. + +## 2. Program ID + +Текущий program id: + +- `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` + +## 3. Состояние + +У программы нет PDA-состояний и persistent storage. + +Она опирается на статически сгенерированные словари, встроенные в бинарник на этапе сборки. + +## 4. Внешние артефакты + +Словари хранятся в: + +- `src/dictionaries/premium/**/*.txt` +- `src/dictionaries/trademarks/**/*.txt` + +На этапе build: + +- все `.txt` файлы рекурсивно собираются; +- слова нормализуются; +- дубль-слова детектятся и логируются как warning; +- итоговые массивы слов вшиваются в generated Rust file. + +## 5. Нормализация словаря + +Слово словаря считается валидным, если: + +- не пустое; +- длина не больше `20`; +- содержит только ASCII-буквы и цифры; +- после `trim()` и `lowercase()` остаётся валидным. + +Символ `_` в словарных словах не допускается. + +## 6. Единственная инструкция + +Инструкция: + +- `classify_login(login: String)` + +Аккаунты: + +- signer + +Signer здесь технический и нужен как согласованный интерфейс вызова; бизнес-смысл подписи отсутствует. + +## 7. Выходные классы + +Программа возвращает `u32` через `set_return_data`: + +- `0` — `CLASS_FREE` +- `1` — `CLASS_PREMIUM` +- `2` — `CLASS_TRADEMARK` + +## 8. Правила нормализации логина + +Перед классификацией логин нормализуется: + +- пустой логин запрещён; +- длина исходного логина не больше `20`; +- символ `_` удаляется; +- остальные символы обязаны быть ASCII alnum; +- буквы приводятся к нижнему регистру. + +Если нормализация не удалась: + +- логин считается `CLASS_PREMIUM`. + +Это намеренно жёсткое поведение: сомнительный логин не должен автоматически попадать в free-класс. + +## 9. Правила классификации + +Алгоритм: + +1. Нормализовать логин. +2. Попробовать разрезать его на `1..3` слов из словарей. +3. Если среди найденных частей есть trademark-слово, вернуть `CLASS_TRADEMARK`. +4. Если есть только premium-слова, вернуть `CLASS_PREMIUM`. +5. Если словарное совпадение не найдено и длина нормализованного логина `<= 7`, вернуть `CLASS_PREMIUM`. +6. Иначе вернуть `CLASS_FREE`. + +## 10. Разбиение на слова + +Максимум слов в логине: + +- `3` + +Алгоритм разбиения: + +- DFS/backtracking по префиксам строки; +- на каждом шаге берётся префикс длиной `1..20`; +- если префикс найден в premium или trademark словаре, поиск продолжается на остатке строки; +- если найден хотя бы один путь с trademark-словом, итог = `CLASS_TRADEMARK`; +- если найден путь только с premium-словами, итог = `CLASS_PREMIUM`. + +## 11. Инварианты + +Должно быть сохранено при переписи: + +- тот же способ нормализации логина; +- та же семантика классов `0/1/2`; +- тот же лимит `MAX_WORDS_PER_LOGIN = 3`; +- тот же fallback `len <= 7 => premium`, если словарь ничего не нашёл; +- тот же порядок приоритета `TRADEMARK > PREMIUM > FREE`. + +## 12. Что не обязано сохраниться при переписи + +Можно менять без изменения бизнес-логики: + +- build pipeline словарей; +- структуру хранения слов внутри бинарника; +- способ поиска слов, если сохраняется тот же результат; +- техническую форму возврата, если внешний контракт для `shine_users` останется совместимым. + +Но если меняется именно интерфейс `return_data` или классы, это уже требует обновления `shine_users` и документации. diff --git a/shine-solana/shine/doc/programs/shine_payments.md b/shine-solana/shine/doc/programs/shine_payments.md new file mode 100644 index 0000000..0f5977d --- /dev/null +++ b/shine-solana/shine/doc/programs/shine_payments.md @@ -0,0 +1,378 @@ +# Программа `shine_payments` + +Документ описывает текущее целевое поведение программы `shine_payments`. + +Назначение программы: + +- хранить общие настройки экономической модели платежей и выплат; +- принимать покупку тикетов очереди выплат; +- хранить очереди и отдельные ticket PDA; +- выдавать лимиты менеджерам на ручное добавление тикетов; +- выполнять пошаговые выплаты из inflow-вольта. + +Если код и этот документ расходятся, нужно в том же изменении синхронизировать либо код, либо документ. + +## 1. Program ID + +Текущий program id: + +- `m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR` + +## 2. Основная модель + +`shine_payments` разделяет две вещи: + +1. регистрацию долга/обязательств через тикеты; +2. фактическую выплату через отдельный inflow-вольт. + +Это важно: + +- покупка тикета не означает немедленную выплату из вольта; +- payout выполняется отдельной инструкцией `step_payout`; +- inflow-вольт должен быть заранее или отдельно пополнен. + +## 3. PDA и seed-правила + +### 3.1. Single PDA + +- config: `shine_payments_config` +- coef/limit: `shine_payments_coef_limit` +- queues: `shine_payments_queues` +- inflow vault: `shine_payments_inflow_vault` + +### 3.2. Ticket PDA + +- queue 1 seed prefix: `shine_payments_q1_ticket` +- queue 2 seed prefix: `shine_payments_q2_ticket` +- второй seed: `ticket_index` в little-endian `u64` + +### 3.3. Manager allowance PDA + +- seed prefix: `shine_p_manager_allow` +- второй seed: `manager_wallet.as_ref()` + +## 4. Состояния программы + +### 4.1. `ConfigState` + +Поля: + +- `version: u8` +- `dao_wallet: Pubkey` +- `inflow_vault: Pubkey` + +### 4.2. `CoefLimitState` + +Поля: + +- `version: u8` +- `coef_ppm: u64` +- `limit_usd_cents: u64` +- `call_reward_lamports: u64` + +Смысл: + +- `coef_ppm` — множитель payout в ppm; +- `limit_usd_cents` — лимит суммы очереди Q1; +- `call_reward_lamports` — награда вызывающему `step_payout`. + +### 4.3. `QueuesState` + +Поля агрегатов: + +- `q1_tickets_total` +- `q1_tickets_paid` +- `q1_sum_total_usd_cents` +- `q1_sum_paid_usd_cents` +- `q2_tickets_total` +- `q2_tickets_paid` +- `q2_sum_total_usd_cents` +- `q2_sum_paid_usd_cents` + +### 4.4. `TicketState` + +Поля: + +- `version: u8` +- `queue_id: u8` +- `index: u64` +- `is_paid: bool` +- `recipient_wallet: Pubkey` +- `payout_usd_cents: u64` +- `debt_before_usd_cents: u64` + +### 4.5. `ManagerAllowanceState` + +Поля: + +- `version: u8` +- `manager_wallet: Pubkey` +- `q1_available_usd_cents: u64` +- `q2_available_usd_cents: u64` + +### 4.6. `VaultState` + +Поля: + +- `version: u8` + +Это техническая метка существования inflow PDA. Лампорты лежат на самом PDA-аккаунте. + +## 5. Константы и экономика + +Текущие базовые значения: + +- `COEF_SCALE_PPM = 1_000_000` +- `START_COEF_PPM = 5_000_000` (5.0x) +- `START_LIMIT_USD_CENTS = 1_000_000` (10_000 USD) +- `START_CALL_REWARD_LAMPORTS = 8_000_000` +- `MAX_CALL_REWARD_LAMPORTS = 10_000_000` +- `ORACLE_MAX_AGE_SECS = 120` +- цена SOL/USD берётся из Pyth. + +## 6. Инструкция `init` + +### Назначение + +Инициализировать все базовые PDA программы. + +### Создаваемые PDA + +- `config_pda` +- `coef_limit_pda` +- `queues_pda` +- `inflow_vault_pda` + +### Кто может вызвать + +Технически любой signer, если система ещё не инициализирована. + +### Что записывается + +- `dao_wallet` берётся из settings/deploy config; +- `inflow_vault` указывается как PDA самой программы; +- коэффициенты и лимиты пишутся стартовыми значениями; +- очереди стартуют с нулями. + +## 7. Инструкция `update_coef_limit` + +### Назначение + +Изменить коэффициент выплат, лимит очереди Q1 и награду за шаг выплат. + +### Авторизация + +Только `dao_wallet` из `ConfigState`. + +### Проверки + +- signer = `config.dao_wallet` +- `coef_ppm > 0` +- `limit_usd_cents > 0` +- `call_reward_lamports <= MAX_CALL_REWARD_LAMPORTS` + +## 8. Инструкция `grant_manager_limits` + +### Назначение + +Выдать менеджеру квоты на добавление тикетов вручную. + +### Авторизация + +Только DAO. + +### Поведение + +- создаёт `manager_allowance_pda`, если её ещё нет; +- увеличивает доступные лимиты Q1/Q2 для указанного менеджера; +- не добавляет тикеты сама. + +## 9. Инструкции покупки тикета + +Есть три входных варианта: + +- `buy_ticket` +- `buy_ticket_usd` +- `buy_ticket_sol` + +Все они в итоге сводятся к одному действию: + +- рассчитывается `purchase_usd_cents`; +- создаётся ticket в очереди `Q1`; +- обновляются агрегаты очереди `Q1`. + +### 9.1. Общие правила + +- ticket создаётся только в `Q1`; +- очередь `Q1` временно блокируется, если достигнут её суммарный лимит; +- `payout_usd_cents = purchase_usd_cents * coef_ppm / COEF_SCALE_PPM`; +- `recipient_wallet` записывается в ticket. + +### 9.2. Важная деталь денежного потока + +В текущей реализации при покупке тикета лампорты переводятся: + +- не во inflow vault; +- а напрямую в `dao_wallet`. + +То есть: + +- ticket фиксирует долг на будущую выплату; +- а inflow vault — отдельный источник средств для исполнения payouts. + +Эта деталь обязательно должна быть осознана при переписи: либо её сохраняют как сознательную модель, либо отдельно меняют и тогда обновляют этот документ. + +## 10. Инструкция `manager_add_ticket` + +### Назначение + +Дать менеджеру возможность вручную создать ticket в `Q1` или `Q2`. + +### Авторизация + +Signer должен совпадать с `manager_wallet` в `manager_allowance_pda`. + +### Проверки + +- `queue_id` только `1` или `2`; +- `payout_usd_cents > 0`; +- доступный allowance по очереди не меньше суммы тикета; +- ticket PDA ещё не существует. + +### Эффект + +- создаётся ticket; +- allowance уменьшается; +- агрегаты соответствующей очереди увеличиваются. + +## 11. Инструкция `step_payout` + +### Назначение + +Сделать один шаг исполнения очереди выплат. + +### Выбор очереди + +- если в `Q1` есть pending ticket, сначала обслуживается `Q1`; +- если `Q1` пустая, обслуживается `Q2`. + +### Что считается pending + +```text +pending = tickets_total - tickets_paid +``` + +### Как выбирается ticket + +Берётся следующий индекс: + +```text +next_index = tickets_paid + 1 +``` + +### Проверки + +- `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda` валидны; +- `dao_wallet` совпадает с `config.dao_wallet`; +- `next_ticket_pda` совпадает с PDA следующего тикета; +- тикет ещё не оплачен; +- `ticket_recipient_wallet` совпадает с `ticket.recipient_wallet`; +- в inflow vault достаточно средств на весь шаг. + +### Сколько переводится + +Для target queue: + +- получателю тикета переводится `ticket_lamports`; +- DAO переводится `dao_lamports`; +- вызвавшему шаг переводится `call_reward_lamports`. + +`dao_multiplier`: + +- для `Q1` = `1` +- для `Q2` = `2` + +То есть DAO получает: + +- `1x payout_usd` для Q1; +- `2x payout_usd` для Q2. + +### Источник денег + +Все три перевода идут из inflow vault PDA. + +### Если pending нет + +Если обе очереди пусты: + +- весь доступный остаток inflow vault переводится в DAO wallet. + +## 12. Инструкция `change_ticket_recipient` + +### Назначение + +Позволить текущему получателю тикета поменять адрес получения, пока ticket ещё не исполнен. + +### Авторизация + +Только текущий `ticket.recipient_wallet`. + +### Ограничения + +Нельзя менять получателя у следующего тикета на выплату в активной очереди. + +Логика: + +- если в `Q1` есть pending — следующий ticket определяется в `Q1`; +- иначе берётся следующий ticket в `Q2`; +- если текущий ticket и есть этот ближайший ticket, смена recipient запрещена. + +## 13. Pyth oracle и конвертация + +Программа использует Pyth SOL/USD. + +Проверки oracle: + +- передан именно тот oracle account, что указан в settings; +- feed id совпадает с ожидаемым; +- цена не старше `ORACLE_MAX_AGE_SECS`; +- цена положительная и корректно переводима в ratio. + +Внутренние преобразования: + +- `lamports -> usd_cents` делаются с округлением вниз; +- `usd_cents -> lamports` делаются с округлением вверх. + +Это важно, чтобы не недоплачивать обязательства при payout. + +## 14. Ошибки и классы отказа + +Программа должна различать как минимум: + +- неверный inflow vault; +- неверный DAO wallet; +- неавторизованный DAO; +- неавторизованный manager; +- неверный manager allowance PDA; +- неверный queue id; +- тикет уже выплачен; +- неверный recipient; +- нельзя сменить recipient ближайшего тикета; +- недостаточно средств inflow vault для step payout; +- queue temporarily paused из-за лимита; +- oracle account/feed/price invalid; +- slippage exceeded. + +## 15. Что должно сохраниться при переписи без Anchor + +Обязательно сохранить: + +- те же PDA seed-правила; +- те же состояния и поля; +- ту же модель очередей Q1/Q2; +- ту же приоритетность `Q1` над `Q2` в `step_payout`; +- ту же логику allowance менеджера; +- те же oracle-ограничения и округления; +- ту же текущую модель денежных потоков, если она не меняется отдельным решением. + +Если при переписи вы решите поменять экономическую модель, сначала нужно обновить этот документ. diff --git a/shine-solana/shine/doc/programs/shine_users.md b/shine-solana/shine/doc/programs/shine_users.md new file mode 100644 index 0000000..be2ff35 --- /dev/null +++ b/shine-solana/shine/doc/programs/shine_users.md @@ -0,0 +1,501 @@ +# Программа `shine_users` + +Документ описывает целевое поведение Solana-программы регистрации пользователей SHiNE. + +Назначение документа: + +- быть источником истины при поддержке текущей реализации; +- позволить заново реализовать программу без Anchor; +- зафиксировать инварианты, форматы и правила проверки. + +Если код программы расходится с этим документом, это считается ошибкой: нужно либо исправить код, либо обновить документ в том же изменении. + +## 1. Назначение программы + +`shine_users` хранит публичную пользовательскую запись SHiNE в Solana PDA и управляет её экономикой. + +Программа отвечает за: + +- создание `user_pda` по логину; +- обновление `user_pda` без смены логина и root key; +- хранение economy-конфига регистрации; +- взимание комиссии за регистрацию и увеличение лимита; +- проверку Ed25519-подписей `root_key` и `blockchain_public_key`; +- проверку связности новой версии записи с предыдущей через `prev_record_hash`. + +Программа не отвечает за: + +- хранение приватных ключей; +- проверку существования Arweave tx; +- валидацию того, что логины из `sync_servers` или `access_servers` реально существуют как серверы; +- выполнение SHiNE-блокчейна пользователя; +- хранение серверных auth-сессий. + +## 2. Program ID и внешние зависимости + +Текущий program id devnet/localnet: + +- `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm` + +Внешние зависимости по логике: + +- `shine_payments` + - используется только как источник PDA inflow-вольта; +- `shine_login_guard` + - используется для классификации логина через CPI; +- системная программа Solana; +- sysvar `instructions`. + +## 3. PDA и seed-правила + +### 3.1. Пользовательская PDA + +Пользовательская запись строится так: + +- seed prefix: `login=` +- второй seed: логин в нижнем регистре +- program id: `shine_users` + +Формула: + +```text +user_pda = PDA(["login=", lower(login)], shine_users_program_id) +``` + +### 3.2. Economy config PDA + +PDA экономических настроек: + +- seed: `shine_users_economy_config` + +Формула: + +```text +users_economy_config_pda = PDA(["shine_users_economy_config"], shine_users_program_id) +``` + +## 4. Состояния программы + +### 4.1. `UsersEconomyConfigState` + +Хранится в `users_economy_config_pda`. + +Поля: + +- `version: u8` +- `registration_fee_lamports: u64` +- `lamports_per_limit_step: u64` +- `start_bonus_limit: u64` + +Смысл: + +- `registration_fee_lamports` — базовая плата за регистрацию; +- `lamports_per_limit_step` — стоимость одного шага лимита; +- `start_bonus_limit` — стартовый бесплатный лимит записи, который получает новый пользователь. + +### 4.2. `user_pda` + +Формат пользовательской записи описан отдельно: + +- [shine-user-pda-format-v.1.0.md](/home/ai/work/SHiNE/SHiNE-server-sha256/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md) + +Этот документ описывает именно логику программы, а не байтовую структуру блока. + +## 5. Константы и базовые правила + +Базовые значения из текущей логики: + +- seed `user_pda`: `login=` +- seed economy config: `shine_users_economy_config` +- стартовый размер `user_pda`: `768` байт +- `LIMIT_STEP = 10_000` +- `START_REGISTRATION_FEE_LAMPORTS = 10_000_000` +- `START_LAMPORTS_PER_LIMIT_STEP = 100_000` +- `START_BONUS_LIMIT = 100_000` + +Правила: + +- `additional_limit` всегда кратен `LIMIT_STEP`; +- `paid_limit_bytes` не может уменьшаться; +- `used_bytes` не может уменьшаться; +- `last_block_number` не может уменьшаться; +- `root_key` после создания не меняется; +- логин после создания не меняется; +- `created_at_ms` после создания не меняется. + +## 6. Ключи и подписи + +В записи участвуют три ключевых роли: + +- `root_key` + - корневая подпись самой записи; +- `device_key` + - текущий плательщик и signer транзакции create/update; +- `blockchain_public_key` + - ключ подтверждения вершины пользовательского SHiNE-блокчейна. + +### 6.1. Что программа видит on-chain + +Программа работает только с: + +- публичными ключами `32` байта; +- подписями `64` байта; +- сообщениями для Ed25519-проверки. + +Программа не знает и не должна знать: + +- PKCS#8 контейнеры; +- PEM; +- способ хранения приватного ключа на клиенте; +- откуда клиент извлёк `seed32` для `device_key`. + +### 6.2. Практика клиентской генерации ключей + +Off-chain клиентская логика может хранить приватные ключи в PKCS#8 и извлекать `seed32` для `device`-signer. Это допустимая клиентская реализация, но не часть on-chain формата. + +On-chain инвариант только один: + +- публичные ключи и подписи должны соответствовать друг другу. + +## 7. Логин и login guard + +Перед созданием пользователя логин обязан пройти две проверки: + +1. базовая syntactic validation внутри `shine_users`; +2. CPI-вызов `shine_login_guard::classify_login`. + +### 7.1. Базовая проверка логина + +Логин должен: + +- быть не пустым; +- быть длиной не больше `20` символов; +- содержать только `A-Z`, `a-z`, `0-9`, `_`. + +### 7.2. Классификация через `shine_login_guard` + +`shine_users` вызывает `shine_login_guard` и читает `return_data`. + +Классы: + +- `0` — логин разрешён; +- `1` — premium login, регистрация запрещена автоматически; +- `2` — trademark login, требует отдельного review и не регистрируется автоматически. + +Если `shine_login_guard` вернул что-то иное или return_data некорректны, это ошибка. + +## 8. Инструкция `init_users_economy_config` + +### Назначение + +Создать `users_economy_config_pda` со стартовыми параметрами экономики. + +### Аккаунты + +- signer/payer +- `users_economy_config_pda` +- system program + +### Правила + +- PDA должна ещё не существовать; +- адрес PDA обязан совпадать с seed `shine_users_economy_config`; +- в PDA записывается стартовый `UsersEconomyConfigState`. + +## 9. Инструкция `update_users_economy_config` + +### Назначение + +Изменить экономические параметры регистрации. + +### Авторизация + +Только `DAO_AUTHORITY`. + +### Аккаунты + +- signer +- `users_economy_config_pda` + +### Правила + +- signer должен совпадать с `DAO_AUTHORITY`; +- PDA должна существовать и принадлежать программе; +- `lamports_per_limit_step > 0`. + +## 10. Инструкция `create_user_pda` + +### Назначение + +Создать новую пользовательскую запись по логину. + +### Кто платит + +Плательщик транзакции и signer инструкции: + +- `device_key` + +Это принципиальное правило: + +- `root_key` только подписывает запись; +- `device_key` оплачивает rent/fees/registration flow. + +### Аккаунты + +- signer = `device_key` +- `user_pda` +- system program +- inflow vault PDA из `shine_payments` +- sysvar `instructions` +- `users_economy_config_pda` +- `shine_login_guard_program` + +### Входные данные + +- логин +- `root_key` +- `created_at_ms` +- `additional_limit` +- mutable fields записи +- `root` signature по unsigned части + +### Обязательные проверки + +1. Логин валиден по синтаксису. +2. Логин разрешён `shine_login_guard`. +3. `additional_limit % LIMIT_STEP == 0`. +4. inflow vault совпадает с PDA программы `shine_payments`. +5. `user_pda` вычислена правильно и ещё не существует. +6. Поля блокчейна валидны. +7. Поля server/session/trusted валидны по формату. +8. `last_block_signature` соответствует `LastBlockState`. +9. `root signature` соответствует unsigned части записи. +10. Размер сериализованной записи не превышает допустимый стартовый размер PDA или иные ограничения реализации. + +### Экономика + +При создании: + +- пользователь получает `start_bonus_limit`; +- дополнительно может купить `additional_limit`; +- итоговый оплаченный лимит: + +```text +paid_limit_bytes = start_bonus_limit + additional_limit +``` + +Комиссия: + +```text +total_fee = registration_fee_lamports + limit_fee(additional_limit) +``` + +Где: + +```text +limit_fee(additional_limit) = (additional_limit / LIMIT_STEP) * lamports_per_limit_step +``` + +### Результат + +- создаётся PDA; +- в неё записывается полная запись `user_pda`; +- средства переводятся в inflow vault `shine_payments`. + +## 11. Инструкция `update_user_pda` + +### Назначение + +Создать новую версию той же пользовательской записи. + +### Авторизация + +Те же роли: + +- signer/fee payer = `device_key` +- подпись записи = `root_key` +- подпись вершины блокчейна = `blockchain_public_key` + +### Аккаунты + +- signer = `device_key` +- `user_pda` +- system program +- inflow vault PDA из `shine_payments` +- sysvar `instructions` +- `users_economy_config_pda` + +### Обязательные проверки + +1. PDA существует и принадлежит `shine_users`. +2. Новый логин совпадает со старым. +3. `created_at_ms` совпадает со старым. +4. `root_key` совпадает со старым. +5. `version = old.record_number + 1`. +6. `prev_hash = hash(unsigned_old_record)`. +7. `additional_limit % LIMIT_STEP == 0`. +8. `blockchain_name` и `blockchain_public_key` не меняются. +9. `paid_limit_bytes` не уменьшается. +10. `used_bytes` не уменьшается. +11. `last_block_number` не уменьшается. +12. Если состояние блокчейна изменилось, `last_block_signature` заново проверяется через Ed25519. +13. Новая unsigned часть записи подписана `root_key`. +14. При необходимости PDA может быть расширена через realloc. + +### Экономика + +При update оплачивается только докупаемый лимит: + +```text +topup_fee = limit_fee(additional_limit) +``` + +Если `additional_limit = 0`, доплата не требуется. + +### Важная пометка о текущем состоянии + +Логика `update_user_pda` в текущей Anchor-версии описана именно так и должна работать именно так. + +Но фактическая текущая Anchor-реализация на момент подготовки этого документа имеет runtime-проблемы в Solana BPF: + +- create-сценарий работает корректно; +- update-сценарий логически реализован, но требует исправления багов реализации. + +При переписи на чистый Rust нужно сохранять именно правила этого документа, а не привязываться к деталям текущей Anchor-реализации. + +## 12. Ed25519-проверки и порядок инструкций + +В транзакции должны стоять две встроенные Ed25519-инструкции прямо перед вызовом `shine_users`: + +1. подпись `root_key` по unsigned записи; +2. подпись `blockchain_public_key` по `LastBlockState`. + +Текущая логика `shine_users` читает их через sysvar `instructions` относительно текущего индекса: + +- `-2` — `root_key` +- `-1` — `blockchain_public_key` + +Это правило порядка является частью контракта между off-chain клиентом и программой. + +## 13. LastBlockState + +Сообщение для подписи `blockchain_public_key`: + +```text +- constant: "SHiNE_LAST_BLOCK" +- login +- blockchain_name +- last_block_number +- last_block_hash[32] +- used_bytes +``` + +Алгоритм: + +```text +message_hash = SHA-256(LastBlockState bytes) +signature = Ed25519(blockchain_private_key, message_hash) +``` + +## 14. Валидируемые mutable-поля записи + +Программа допускает обновление: + +- `device_key` +- `used_bytes` +- `last_block_number` +- `last_block_hash` +- `last_block_signature` +- `arweave_tx_id` +- `is_server` +- `server profile` +- `access_servers` +- `sessions_mode` +- `sessions` +- `trusted_count` +- `additional_limit` + +Программа не допускает update: + +- `login` +- `created_at_ms` +- `root_key` +- `blockchain_name` +- `blockchain_public_key` +- `blockchain_type` + +## 15. Правила серверных и сессионных полей + +### Server profile + +Если `is_server = false`: + +- `server_address` должен быть пустой; +- `sync_servers` должен быть пустой. + +Если `is_server = true`: + +- `server_address` обязателен; +- `sync_servers.len() <= 32`. + +### Sessions block + +Формат сессий описан в PDA-формате, но логика такая: + +- максимум `64` записей; +- `sessions_mode` допускает только `1` и `10`; +- `session_type` допускает `1` и `100`; +- `session_version` сейчас только `1`; +- `session_name` должен содержать только `[A-Za-z0-9_]`; +- `session_name` и `session_pub_key` уникальны внутри списка. + +На текущем этапе обычная регистрация пользователя должна продолжать работать с: + +- `sessions_mode = 1` +- `sessions = []` + +## 16. Realloc поведения PDA + +Запись может расти. Если новая сериализованная запись длиннее текущего размера PDA: + +- PDA разрешено увеличить через realloc; +- нельзя делать чрезмерный рост одним шагом выше внутреннего лимита реализации; +- перед realloc нужно обеспечить rent для нового размера. + +## 17. Ошибки и классы отказа + +Программа должна различать как минимум такие классы ошибок: + +- неверный логин; +- premium/trademark login; +- неверный PDA адрес; +- PDA уже существует / PDA пустая / PDA не принадлежит программе; +- неверный формат записи; +- неверная подпись root; +- неверная подпись last block state; +- попытка изменить immutable поля; +- неверная версия; +- неверный `prev_hash`; +- попытка уменьшить лимит/used_bytes/block number; +- overflow; +- неверный inflow vault; +- неверный DAO authority для economy config. + +## 18. Что должно сохраниться при переписи без Anchor + +При переписи на чистый Rust нужно сохранить без изменений: + +- те же PDA seed-правила; +- тот же формат `user_pda`; +- ту же экономику регистрации и topup; +- тот же порядок Ed25519-инструкций; +- те же immutable/mutable правила; +- ту же валидацию логина и CPI в `shine_login_guard`; +- ту же зависимость от inflow vault программы `shine_payments`. + +Что не обязано сохраниться: + +- структура Anchor `Context`; +- Anchor discriminator'ы; +- внутренние helper-функции текущей реализации; +- текущие runtime-баги update-path.