Добавить спецификации Solana программ и вынести формат PDA
This commit is contained in:
parent
a9510a6d36
commit
6b0379bfdc
@ -36,7 +36,7 @@
|
|||||||
- Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в:
|
- Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в:
|
||||||
- `Dev_Docs/Solana_Architecture/README.md`
|
- `Dev_Docs/Solana_Architecture/README.md`
|
||||||
- Документ формата пользовательской PDA-записи `shine_users` находится в:
|
- Документ формата пользовательской 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`.
|
- Актуальная документация по форматам блокчейна находится в `Dev_Docs/Blockchain/README.md`.
|
||||||
|
|||||||
@ -35,9 +35,11 @@
|
|||||||
Предварительные значения:
|
Предварительные значения:
|
||||||
|
|
||||||
- тип `1` - обычная пользовательская сессия;
|
- тип `1` - обычная пользовательская сессия;
|
||||||
- тип `10` - саб-сервер пользователя;
|
- тип `100` - саб-сервер пользователя;
|
||||||
- версия `1` - первая рабочая версия формата записи сессии.
|
- версия `1` - первая рабочая версия формата записи сессии.
|
||||||
|
|
||||||
|
На текущем этапе под это уже зарезервирован отдельный блок `SessionsBlock` с `block_type = 55`, а `TrustedStateBlock` остаётся на `50`.
|
||||||
|
|
||||||
Важно: саб-серверов у одного пользователя может быть несколько.
|
Важно: саб-серверов у одного пользователя может быть несколько.
|
||||||
|
|
||||||
## Архитектурный принцип
|
## Архитектурный принцип
|
||||||
@ -74,14 +76,14 @@
|
|||||||
- максимальную длину `sessionName`;
|
- максимальную длину `sessionName`;
|
||||||
- правила удаления и обновления записи;
|
- правила удаления и обновления записи;
|
||||||
- правила ротации `sessionPubKey`.
|
- правила ротации `sessionPubKey`.
|
||||||
6. Продумать, как UI и сервер будут отличать тип `1` и тип `10`.
|
6. Продумать, как UI и сервер будут отличать тип `1` и тип `100`.
|
||||||
7. Определить, какие внутренние сообщения саб-сервера останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации.
|
7. Определить, какие внутренние сообщения саб-сервера останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации.
|
||||||
8. Добавить API/операции чтения и обновления списка сессий, если для этого не хватит существующих механизмов.
|
8. Добавить API/операции чтения и обновления списка сессий, если для этого не хватит существующих механизмов.
|
||||||
9. После реализации обязательно обновить документацию.
|
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_Architecture/README.md`
|
||||||
- `Dev_Docs/Инициализация_Solana_регистрации/README.md`
|
- `Dev_Docs/Инициализация_Solana_регистрации/README.md`
|
||||||
- `Dev_Docs/Keys/README.md`
|
- `Dev_Docs/Keys/README.md`
|
||||||
|
|||||||
@ -32,6 +32,12 @@
|
|||||||
|
|
||||||
Один логин соответствует одной `user_pda`.
|
Один логин соответствует одной `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. Общие правила кодирования
|
## 3. Общие правила кодирования
|
||||||
|
|
||||||
- Числа кодируются в Little Endian.
|
- Числа кодируются в Little Endian.
|
||||||
@ -84,6 +90,7 @@ UserPdaRecordV1
|
|||||||
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
|
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
|
||||||
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
|
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
|
||||||
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
||||||
|
| `55` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. |
|
||||||
| `50` | `TrustedStateBlock` | Счетчик trusted-связей. |
|
| `50` | `TrustedStateBlock` | Счетчик trusted-связей. |
|
||||||
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
|
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
|
||||||
|
|
||||||
@ -91,10 +98,10 @@ UserPdaRecordV1
|
|||||||
|
|
||||||
- неизвестный `block_type` в `format_major = 1` считается ошибкой;
|
- неизвестный `block_type` в `format_major = 1` считается ошибкой;
|
||||||
- обязательные блоки: `RootKeyBlock`, `DeviceKeyBlock`, `BlockchainRegistryBlock`;
|
- обязательные блоки: `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
|
## 6. RootKeyBlock
|
||||||
|
|
||||||
@ -197,6 +204,7 @@ Arweave `tx_id` - обычное поле внутри записи конкре
|
|||||||
- `used_bytes <= paid_limit_bytes`;
|
- `used_bytes <= paid_limit_bytes`;
|
||||||
- если `last_block_number` увеличился, то должны быть переданы новый `last_block_hash` и новая `last_block_signature`;
|
- если `last_block_number` увеличился, то должны быть переданы новый `last_block_hash` и новая `last_block_signature`;
|
||||||
- `last_block_signature` проверяется через Ed25519-инструкцию Solana: подпись должна соответствовать хэшу сообщения `LastBlockState` и `blockchain_public_key`;
|
- `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;
|
- `arweave_tx_id` можно добавить или заменить на новый, если пользователь выгрузил более актуальное состояние в Arweave;
|
||||||
- уменьшать лимит, число блоков или занятый размер нельзя.
|
- уменьшать лимит, число блоков или занятый размер нельзя.
|
||||||
|
|
||||||
@ -230,7 +238,8 @@ ServerProfileBlock
|
|||||||
- block_type: u8 = 30
|
- block_type: u8 = 30
|
||||||
- block_version: u8 = 0
|
- block_version: u8 = 0
|
||||||
- is_server: u8
|
- 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
|
- server_address: string, только если is_server = 1
|
||||||
- sync_servers_count: u8, только если is_server = 1
|
- sync_servers_count: u8, только если is_server = 1
|
||||||
- sync_servers: string[sync_servers_count], только если is_server = 1
|
- sync_servers: string[sync_servers_count], только если is_server = 1
|
||||||
@ -240,9 +249,11 @@ ServerProfileBlock
|
|||||||
|
|
||||||
- `is_server = 0` означает, что серверных данных нет;
|
- `is_server = 0` означает, что серверных данных нет;
|
||||||
- `is_server = 1` означает, что пользователь публикует серверный профиль;
|
- `is_server = 1` означает, что пользователь публикует серверный профиль;
|
||||||
|
- `address_format_type` — тип формата адреса сервера: `1` = URL-строка (например `https://shineup.me/ws`);
|
||||||
|
- `address_format_version` — версия формата адреса, сейчас `0`;
|
||||||
- `sync_servers_count` максимум `32`;
|
- `sync_servers_count` максимум `32`;
|
||||||
- `server_address` - строковый адрес сервера в формате, который будет отдельно закреплен на уровне приложения;
|
- `server_address` - строковый адрес сервера в соответствии с `address_format_type`;
|
||||||
- `sync_servers` - логины пользователей системы, через которых этот сервер пытается синхронизироваться. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
|
- `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
|
||||||
|
|
||||||
## 12. AccessServersBlock
|
## 12. AccessServersBlock
|
||||||
|
|
||||||
@ -263,7 +274,53 @@ AccessServersBlock
|
|||||||
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
|
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
|
||||||
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
|
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой 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-логика не реализована полностью, поэтому блок хранит только счетчик.
|
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
|
||||||
|
|
||||||
@ -276,7 +333,7 @@ TrustedStateBlock
|
|||||||
|
|
||||||
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
|
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
|
||||||
|
|
||||||
## 14. Подпись user_pda
|
## 15. Подпись user_pda
|
||||||
|
|
||||||
Подписывается не вся PDA целиком, а unsigned-часть записи:
|
Подписывается не вся PDA целиком, а unsigned-часть записи:
|
||||||
|
|
||||||
@ -293,10 +350,11 @@ signature = Ed25519(root_key, message)
|
|||||||
```
|
```
|
||||||
|
|
||||||
Solana-программа проверяет подпись через встроенную Ed25519-инструкцию. Подписантом должен быть `root_key` из `RootKeyBlock`.
|
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`;
|
- `created_at_ms = updated_at_ms`;
|
||||||
- обязательные блоки присутствуют;
|
- обязательные блоки присутствуют;
|
||||||
- создается минимум один `BlockchainRecord`;
|
- создается минимум один `BlockchainRecord`;
|
||||||
|
- новый `SessionsBlock` может присутствовать, но при обычной регистрации сейчас записывается пустой список с `sessions_mode = 1`;
|
||||||
- стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит;
|
- стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит;
|
||||||
- `used_bytes <= paid_limit_bytes`;
|
- `used_bytes <= paid_limit_bytes`;
|
||||||
- пользователь платит регистрационную комиссию;
|
- пользователь платит регистрационную комиссию;
|
||||||
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
|
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
|
||||||
- вся unsigned-часть записи подписана `root_key`.
|
- вся unsigned-часть записи подписана `root_key`.
|
||||||
|
|
||||||
## 16. Обновление пользователя
|
## 17. Обновление пользователя
|
||||||
|
|
||||||
При обновлении:
|
При обновлении:
|
||||||
|
|
||||||
@ -328,7 +387,7 @@ Solana-программа проверяет подпись через встр
|
|||||||
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
|
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
|
||||||
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
|
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
|
||||||
|
|
||||||
## 17. Отличия от старого линейного формата
|
## 18. Отличия от старого линейного формата
|
||||||
|
|
||||||
Старый формат после `login` хранил поля линейно:
|
Старый формат после `login` хранил поля линейно:
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
Связанные документы:
|
Связанные документы:
|
||||||
|
|
||||||
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` — single source of truth по деплою и первичной инициализации регистрации пользователей.
|
- `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-модуля.
|
- `shine-solana/shine/doc/FUNDS_FLOW.md` — короткая справка по денежным потокам внутри Solana-модуля.
|
||||||
|
|
||||||
## Кратко
|
## Кратко
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.124
|
client.version=1.2.125
|
||||||
server.version=1.2.116
|
server.version=1.2.117
|
||||||
|
|||||||
@ -2,11 +2,18 @@
|
|||||||
|
|
||||||
## Documentation Rule
|
## 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
|
## Language Rule
|
||||||
|
|
||||||
@ -21,9 +28,16 @@
|
|||||||
|
|
||||||
Если меняется бизнес-логика смарт-контрактов, сериализация PDA, правила переводов или экономика:
|
Если меняется бизнес-логика смарт-контрактов, сериализация PDA, правила переводов или экономика:
|
||||||
|
|
||||||
1. Обновить соответствующий документ в `doc/` в том же изменении.
|
1. Обновить соответствующий документ в `doc/programs/` и/или `doc/formats/` в том же изменении.
|
||||||
2. Если документ сразу обновить нельзя, обязательно явно согласовать это с пользователем в чате и зафиксировать план обновления.
|
2. Если документ сразу обновить нельзя, обязательно явно согласовать это с пользователем в чате и зафиксировать план обновления.
|
||||||
|
|
||||||
|
Отдельное обязательное правило:
|
||||||
|
|
||||||
|
1. Документы по программам и документ формата PDA должны всегда совпадать с кодом программ.
|
||||||
|
2. Перед любым изменением кода нужно явно проверять, затрагивает ли оно поведение, PDA, поля, проверки, экономику или формат.
|
||||||
|
3. Если затрагивает, документ обновляется обязательно.
|
||||||
|
4. Нельзя сознательно оставлять код и документы в рассинхроне без отдельной явной договоренности с пользователем.
|
||||||
|
|
||||||
## Rule: Git Push
|
## Rule: Git Push
|
||||||
|
|
||||||
Для push в удаленный репозиторий использовать токен из переменной окружения:
|
Для push в удаленный репозиторий использовать токен из переменной окружения:
|
||||||
|
|||||||
424
shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md
Normal file
424
shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md
Normal 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.
|
||||||
138
shine-solana/shine/doc/programs/shine_login_guard.md
Normal file
138
shine-solana/shine/doc/programs/shine_login_guard.md
Normal 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` и документации.
|
||||||
378
shine-solana/shine/doc/programs/shine_payments.md
Normal file
378
shine-solana/shine/doc/programs/shine_payments.md
Normal 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-ограничения и округления;
|
||||||
|
- ту же текущую модель денежных потоков, если она не меняется отдельным решением.
|
||||||
|
|
||||||
|
Если при переписи вы решите поменять экономическую модель, сначала нужно обновить этот документ.
|
||||||
501
shine-solana/shine/doc/programs/shine_users.md
Normal file
501
shine-solana/shine/doc/programs/shine_users.md
Normal 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.
|
||||||
Loading…
Reference in New Issue
Block a user