Добавить спецификации Solana программ и вынести формат PDA

This commit is contained in:
AidarKC 2026-06-04 22:17:17 +04:00
parent a9510a6d36
commit 6b0379bfdc
10 changed files with 1537 additions and 21 deletions

View File

@ -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`.

View File

@ -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`

View File

@ -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` хранил поля линейно:

View File

@ -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-модуля.
## Кратко

View File

@ -1,2 +1,2 @@
client.version=1.2.124
server.version=1.2.116
client.version=1.2.125
server.version=1.2.117

View File

@ -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 в удаленный репозиторий использовать токен из переменной окружения:

View File

@ -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`. На первом этапе для основного блокчейна пользователя используется имя вида `<login>-001`, потому что это первый блокчейн этого пользователя.
- `blockchain_public_key` - публичный ключ блокчейна пользователя.
- `paid_limit_bytes` - оплаченный лимит хранения/записей в байтах.
- `used_bytes` - сколько байт уже занято в пользовательском SHiNE-блокчейне.
- `last_block_number` - номер последнего известного блока пользовательского блокчейна.
- `last_block_hash` - хэш последнего известного блока.
- `last_block_signature` - подпись хэша специального сообщения о вершине блокчейна ключом `blockchain_public_key`.
- `arweave_present` - `0`, если ссылки нет; `1`, если ссылка есть.
- `arweave_tx_id` - Arweave transaction id, где лежит выгруженный пользовательский канал/состояние.
Arweave `tx_id` - обычное поле внутри записи конкретного блокчейна. Solana-программа не проверяет, что такой Arweave transaction действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя.
## 10. Правила обновления BlockchainRecord
При обновлении записи:
- `blockchain_type` для существующей записи не меняется;
- `blockchain_public_key` пока не ротируется автоматически; смена ключа требует отдельного согласованного сценария;
- `paid_limit_bytes` может только увеличиваться или оставаться прежним;
- при увеличении `paid_limit_bytes` пользователь платит комиссию в Solana по тарифам программы;
- `used_bytes` может только увеличиваться или оставаться прежним;
- `last_block_number` может только увеличиваться или оставаться прежним;
- `used_bytes <= paid_limit_bytes`;
- если `last_block_number` увеличился, то должны быть переданы новый `last_block_hash` и новая `last_block_signature`;
- `last_block_signature` проверяется через Ed25519-инструкцию Solana: подпись должна соответствовать хэшу сообщения `LastBlockState` и `blockchain_public_key`;
- в транзакции `create_user_pda` / `update_user_pda` две Ed25519-инструкции должны идти непосредственно перед вызовом `shine_users`: сначала подпись `root_key`, затем подпись `blockchain_public_key`;
- `arweave_tx_id` можно добавить или заменить на новый, если пользователь выгрузил более актуальное состояние в Arweave;
- уменьшать лимит, число блоков или занятый размер нельзя.
Сообщение `LastBlockState`, которое хэшируется и подписывается ключом `blockchain_public_key`:
```text
LastBlockState
- constant: bytes = "SHiNE_LAST_BLOCK"
- login: string
- blockchain_name: string
- last_block_number: u32
- last_block_hash: [u8; 32]
- used_bytes: u64
```
Алгоритм:
```text
message = SHA-256(LastBlockState bytes)
last_block_signature = Ed25519(blockchain_public_key, message)
```
Причина проверки подписи `LastBlockState`: `root_key` управляет Solana-записью пользователя, а `blockchain_public_key` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
## 11. ServerProfileBlock
Блок присутствует, если пользователь выступает сервером.
```text
ServerProfileBlock
- block_type: u8 = 30
- block_version: u8 = 0
- is_server: u8
- address_format_type: u8, только если is_server = 1
- address_format_version: u8, только если is_server = 1
- server_address: string, только если is_server = 1
- sync_servers_count: u8, только если is_server = 1
- sync_servers: string[sync_servers_count], только если is_server = 1
```
Правила:
- `is_server = 0` означает, что серверных данных нет;
- `is_server = 1` означает, что пользователь публикует серверный профиль;
- `address_format_type` — тип формата адреса сервера: `1` = URL-строка (например `https://shineup.me/ws`);
- `address_format_version` — версия формата адреса, сейчас `0`;
- `sync_servers_count` максимум `32`;
- `server_address` - строковый адрес сервера в соответствии с `address_format_type`;
- `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
## 12. AccessServersBlock
Блок хранит серверы доступа/relay для пользователя.
```text
AccessServersBlock
- block_type: u8 = 40
- block_version: u8 = 0
- access_servers_count: u8
- access_servers: string[access_servers_count]
```
Правила:
- блок может отсутствовать, если серверы доступа не заданы;
- список может обновляться при изменении маршрутизации пользователя;
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
## 13. SessionsBlock
Блок хранит опубликованные пользовательские сессии. На текущем этапе регистрация пользователя не добавляет туда записи автоматически, поэтому стандартный create/update продолжает работать с пустым списком.
```text
SessionsBlock
- block_type: u8 = 55
- block_version: u8 = 0
- sessions_mode: u8
- sessions_count: u8
- sessions: SessionRecord[sessions_count]
```
`sessions_mode`:
| Значение | Смысл |
|----------|-------|
| `1` | Можно использовать и сессии, зарегистрированные в PDA, и сессии, созданные вне PDA. |
| `10` | Зарезервировано на будущее: можно использовать только сессии, опубликованные в PDA. |
Сейчас рабочий режим по умолчанию: `sessions_mode = 1`. Серверная логика пока не реализует особое поведение для `10`; это задел под будущее расширение.
```text
SessionRecord
- session_type: u8
- session_version: u8
- session_name: string
- session_pub_key: [u8; 32]
```
`session_type`:
| Значение | Смысл |
|----------|-------|
| `1` | Обычная пользовательская сессия. |
| `100` | Саб-сервер пользователя. |
Правила:
- максимум `64` записей на пользователя;
- `session_name` не пустой, максимум `64` байта;
- `session_name` может содержать только символы `[A-Za-z0-9_]`;
- `session_version` сейчас должна быть равна `1`;
- внутри одного блока должны быть уникальны и `session_name`, и `session_pub_key`;
- на текущем этапе UI и регистрация не обязаны добавлять туда записи автоматически.
## 14. TrustedStateBlock
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
```text
TrustedStateBlock
- block_type: u8 = 50
- block_version: u8 = 0
- trusted_count: u8 = 0
```
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
## 15. Подпись user_pda
Подписывается не вся PDA целиком, а unsigned-часть записи:
- от `magic` до последнего байта последнего типизированного блока включительно;
- включая `record_len`, `blocks_count`, все заголовки блоков и тела блоков;
- без поля `signature`;
- без padding.
Алгоритм:
```text
message = hash(unsigned_record_bytes)
signature = Ed25519(root_key, message)
```
Solana-программа проверяет подпись через встроенную Ed25519-инструкцию. Подписантом должен быть `root_key` из `RootKeyBlock`.
Для `shine_users` эта инструкция должна стоять в транзакции сразу перед Ed25519-инструкцией `last_block_signature` и непосредственно перед самой `create/update`-инструкцией программы.
Смену формата подписи сейчас не трогаем.
## 16. Регистрация пользователя
При регистрации:
- PDA еще не должна существовать;
- логин проходит проверку формата и login guard;
- `record_number = 0`;
- `prev_record_hash = 0x00...00`;
- `created_at_ms = updated_at_ms`;
- обязательные блоки присутствуют;
- создается минимум один `BlockchainRecord`;
- новый `SessionsBlock` может присутствовать, но при обычной регистрации сейчас записывается пустой список с `sessions_mode = 1`;
- стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит;
- `used_bytes <= paid_limit_bytes`;
- пользователь платит регистрационную комиссию;
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
- вся unsigned-часть записи подписана `root_key`.
## 17. Обновление пользователя
При обновлении:
- PDA должна существовать;
- `login`, `created_at_ms`, `root_key` не меняются;
- `record_number = previous_record_number + 1`;
- `prev_record_hash` равен хэшу unsigned-части предыдущей записи;
- `updated_at_ms` обновляется;
- unsigned-часть новой записи подписана `root_key`;
- лимиты блокчейнов могут только увеличиваться;
- занятый размер и номер последнего блока не могут уменьшаться;
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
## 18. Отличия от старого линейного формата
Старый формат после `login` хранил поля линейно:
- `root_key_status`;
- `root_key`;
- `blockchain_key_status`;
- `blockchain_key`;
- `device_key_status`;
- `device_key`;
- `chain_number`;
- `balance`;
- серверные поля;
- access-серверы;
- `trusted_count`;
- `reserved`;
- `signature`.
Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки:
- ключи становятся отдельными блоками;
- данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`;
- серверные данные и access-серверы отделяются от данных блокчейна;
- расширение формата делается добавлением новых версий блоков или новых `block_type`, а не вставкой полей в середину линейной записи.
## 18. Что пока не входит в формат
Пока не проектируем:
- ротацию `root_key`;
- сложную ротацию `device_key`;
- ротацию `blockchain_public_key`;
- проверку содержимого Arweave transaction;
- хранение полной истории пользовательского блокчейна внутри Solana;
- подключение Solana-модуля к сборке/деплою основного сервера SHiNE.

View File

@ -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` и документации.

View File

@ -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-ограничения и округления;
- ту же текущую модель денежных потоков, если она не меняется отдельным решением.
Если при переписи вы решите поменять экономическую модель, сначала нужно обновить этот документ.

View File

@ -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.