Files
SHiNE-server/shine-solana/shine/doc/programs/shine_users.md

756 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Программа `shine_users`
Документ описывает целевое поведение Solana-программы регистрации пользователей SHiNE.
Назначение документа:
- быть источником истины при поддержке текущей реализации;
- позволить заново реализовать программу без Anchor;
- зафиксировать инварианты, форматы и правила проверки.
Если код программы расходится с этим документом, это считается ошибкой: нужно либо исправить код, либо обновить документ в том же изменении.
## 1. Назначение программы
`shine_users` хранит публичную пользовательскую запись SHiNE в Solana PDA и управляет её экономикой.
Программа отвечает за:
- создание `user_pda` по логину;
- обновление `user_pda` без смены логина и root key;
- хранение economy-конфига регистрации;
- хранение PDA продавцов promo-логинов;
- взимание комиссии за регистрацию и увеличение лимита;
- проверку Ed25519-подписей `root_key` и `blockchain_public_key`;
- проверку promo-подписей продавца для обхода premium/trademark login guard;
- проверку связности новой версии записи с предыдущей через `prev_record_hash`.
Программа не отвечает за:
- хранение приватных ключей;
- проверку существования Arweave tx;
- валидацию того, что логины из `sync_servers` или `access_servers` реально существуют как серверы;
- выполнение SHiNE-блокчейна пользователя;
- хранение серверных auth-сессий.
## 2. Program ID и внешние зависимости
Текущий program id:
- `SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6`
Внешние зависимости по логике:
- `shine_payments`
- используется только как источник PDA inflow-вольта;
- `shine_login_guard`
- используется для классификации логина через CPI;
- системная программа Solana;
- sysvar `instructions`.
Текущие зашитые внешние адреса:
- `DAO_AUTHORITY`: `aiShm43fZjm3YkMs22sYL1bpXaL3bVxv7SSraPHzVgq`
- `shine_payments`: `SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v`
- `shine_login_guard`: `SHiGxGsXGioQYCYhchQ5R7KWoxN5UjFAFsucPf6sfnh`
## 3. PDA и seed-правила
### 3.1. Пользовательская PDA
Пользовательская запись строится так:
- seed prefix: `user_login=`
- второй seed: логин в нижнем регистре
- program id: `shine_users`
Формула:
```text
user_pda = PDA(["user_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)
```
### 3.3. Правило создания PDA (защита от «минирования» адреса)
Адрес пользовательской PDA выводится из логина и публично предсказуем: зная желаемый логин,
любой может заранее вычислить адрес записи и перевести на него немного лампортов обычным
system-переводом. Если бы создание шло строго через `system_instruction::create_account`,
такой «подсев» приводил бы к ошибке «account already in use» и навсегда блокировал бы
регистрацию этого логина (targeted-DoS / сквоттинг логинов), причём без оплаты комиссии.
Поэтому `create_pda_account` создаёт аккаунт устойчиво к предзаполненному балансу:
- если на адресе нет лампортов — обычный `create_account` (быстрый путь);
- если лампорты уже есть — «создание поверх предзаполненного»: добор ренты переводом,
затем `allocate` + `assign` под подписью PDA.
Проверки повторной инициализации (`owner == System Program` и пустые данные) остаются и
не зависят от баланса аккаунта.
### 3.4. PDA продавца promo-логинов
PDA продавца строится так:
- seed prefix: `promo_seller=`
- второй seed: логин продавца в нижнем регистре
Формула:
```text
promo_seller_pda = PDA(["promo_seller=", lower(seller_login)], shine_users_program_id)
```
В программе может существовать сколько угодно таких PDA, по одному на каждого продавца.
### 3.5. Строгий список аккаунтов (нет «лишних» аккаунтов)
Все инструкции `shine_users` читают строго фиксированный набор аккаунтов и после этого
явно требуют, чтобы в переданном списке больше ничего не было
(`require!(it.next().is_none(), InvalidInstruction)`). Если вызывающий добавит лишние
аккаунты в хвост, инструкция завершится ошибкой `InvalidInstruction (1)`.
Это не закрывает отдельной уязвимости (каждый используемый аккаунт и так строго
валидируется по signer/owner/адресу PDA), а является defense-in-depth и приводит поведение
к единому виду с `shine_payments`, где такая же проверка стоит во всех инструкциях.
Списки аккаунтов в разделах ниже надо считать исчерпывающими и точными по количеству.
## 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)
Этот документ описывает именно логику программы, а не байтовую структуру блока.
### 4.3. `promo_seller_pda`
PDA продавца красивых логинов хранит:
- `version: u8`
- `remaining_sales: u64`
- `min_login_length: u8`
- `signer_pubkey: [u8; 32]`
Смысл:
- `remaining_sales` — сколько ещё красивых логинов продавец может выдать;
- `min_login_length` — минимальная длина логина, которую продавец имеет право раздавать;
- `signer_pubkey` — публичный Ed25519-ключ, которым off-chain подписываются promo-коды.
## 5. Константы и базовые правила
Базовые значения из текущей логики:
- seed `user_pda`: `user_login=`
- seed economy config: `shine_users_economy_config`
- seed promo seller PDA: `promo_seller=`
- стартовый размер `user_pda`: `768` байт
- размер `promo_seller_pda`: `64` байта
- `LIMIT_STEP = 10_000`
- `START_REGISTRATION_FEE_LAMPORTS = 10_000_000`
- `START_LAMPORTS_PER_LIMIT_STEP = 100_000`
- `START_BONUS_LIMIT = 100_000`
- префикс promo-сообщения: `shine_promo_v1:`
Правила:
- `additional_limit` всегда кратен `LIMIT_STEP`;
- `paid_limit_bytes` не может уменьшаться;
- `used_bytes` не может уменьшаться;
- `last_block_number` не может уменьшаться;
- `root_key` после создания не меняется;
- логин после создания не меняется;
- `created_at_ms` после создания не меняется.
## 6. Ключи и подписи
В записи участвуют четыре ключевых роли:
- `recovery_key`
- публичный recovery-ключ пользователя для будущих сценариев восстановления;
- `root_key`
- корневая подпись самой записи;
- `client_key`
- текущий плательщик и signer транзакции create/update;
- `blockchain_public_key`
- ключ подтверждения вершины пользовательского SHiNE-блокчейна.
### 6.1. Что программа видит on-chain
Программа работает только с:
- публичными ключами `32` байта;
- подписями `64` байта;
- сообщениями для Ed25519-проверки.
Программа не знает и не должна знать:
- PKCS#8 контейнеры;
- PEM;
- способ хранения приватного ключа на клиенте;
- откуда клиент извлёк `seed32` для `client_key`.
### 6.2. Практика клиентской генерации ключей
Off-chain клиентская логика может хранить приватные ключи в PKCS#8 и извлекать `seed32` для `client`-signer. Это допустимая клиентская реализация, но не часть on-chain формата.
Согласованная клиентская схема деривации для первой публичной версии:
```text
seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || suffix_utf8)
```
Согласованные suffix:
```text
"recovery.key"
"root.key"
"blockchain.key"
"client.key"
```
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 некорректны, это ошибка.
### 7.3. Promo-обход login guard
При создании пользователя off-chain клиент может использовать опциональный `promo_code`.
Формат строки:
```text
1<seller_login>-<signature_base58>
```
Где:
- первый символ `1` — версия формата promo-кода;
- `seller_login` — логин продавца, по которому ищется `promo_seller_pda`;
- `signature_base58` — Ed25519-подпись по сообщению:
```text
shine_promo_v1:<login_из_запроса_create_user_pda>
```
В сам `create_user_pda` при этом передаётся только `seller_login`, а подпись проверяется по отдельной Ed25519-инструкции в транзакции.
Если promo-код валиден, то:
- premium/trademark-проверка через `shine_login_guard` не выполняется;
- базовая синтаксическая проверка логина всё равно остаётся обязательной;
- `remaining_sales` уменьшается на `1`;
- длина логина должна быть не меньше `min_login_length`.
Если promo-код не передан, обычная CPI-проверка через `shine_login_guard` обязательна.
## 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`.
### Бинарный ABI
```text
- tag: u8 = 1
```
## 9. Инструкция `update_users_economy_config`
### Назначение
Изменить экономические параметры регистрации.
### Авторизация
Только `DAO_AUTHORITY`.
### Аккаунты
- signer
- `users_economy_config_pda`
### Правила
- signer должен совпадать с `DAO_AUTHORITY`;
- PDA должна существовать и принадлежать программе;
- `lamports_per_limit_step > 0`.
### Бинарный ABI
```text
- tag: u8 = 2
- registration_fee_lamports: u64 LE
- lamports_per_limit_step: u64 LE
- start_bonus_limit: u64 LE
```
## 10. Инструкция `create_user_pda`
### Назначение
Создать новую пользовательскую запись по логину.
### Кто платит
Плательщик транзакции и signer инструкции:
- `client_key`
Это принципиальное правило:
- `root_key` только подписывает запись;
- `client_key` оплачивает rent/fees/registration flow.
### Аккаунты
- signer = `client_key`
- `user_pda`
- system program
- inflow vault PDA из `shine_payments`
- sysvar `instructions`
- `users_economy_config_pda`
- `shine_login_guard_program`
- опционально: `promo_seller_pda`, если регистрация идёт по promo-коду
### Входные данные
- логин
- `recovery_key`
- `root_key`
- `created_at_ms`
- `additional_limit`
- mutable fields записи
- `root` signature по unsigned части
- опционально `promo_seller_login`
### Бинарный ABI
```text
- tag: u8 = 3
- login: string_u8
- recovery_key: [u8; 32]
- root_key: [u8; 32]
- created_at_ms: u64 LE
- additional_limit: u64 LE
- fields: UserMutableFieldsV1
- root_signature: [u8; 64]
- optional trailing promo_seller_login: string_u8
```
### Обязательные проверки
1. Логин валиден по синтаксису.
2. Если promo-код не передан, логин разрешён `shine_login_guard`.
3. Если promo-регистрация включена, программа получает `promo_seller_login`, а связанная Ed25519-инструкция должна:
- ссылаться на существующий `promo_seller_pda`;
- проходить Ed25519-проверку через `signer_pubkey` продавца;
- иметь `remaining_sales > 0`;
- удовлетворять правилу `login.len() >= min_login_length`.
4. `additional_limit % LIMIT_STEP == 0`.
5. inflow vault совпадает с PDA программы `shine_payments`.
6. `user_pda` вычислена правильно и ещё не существует.
7. Поля блокчейна валидны.
8. Поля server/session/trusted валидны по формату.
9. `last_block_signature` соответствует `LastBlockState`.
10. `root signature` соответствует unsigned части записи.
11. Размер сериализованной записи не превышает допустимый стартовый размер 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`;
- если используется promo-код, в `promo_seller_pda` уменьшается `remaining_sales`;
- средства переводятся в inflow vault `shine_payments`.
## 11. Инструкция `upsert_promo_seller`
### Назначение
Создать нового продавца promo-логинов или полностью перезаписать настройки уже существующего продавца.
### Авторизация
Только `DAO_AUTHORITY`.
### Аккаунты
- signer
- `promo_seller_pda`
- system program
### Входные данные
- `seller_login`
- `remaining_sales`
- `min_login_length`
- `signer_pubkey`
### Бинарный ABI
```text
- tag: u8 = 5
- seller_login: string_u8
- remaining_sales: u64 LE
- min_login_length: u8
- signer_pubkey: [u8; 32]
```
### Правила
- signer должен совпадать с `DAO_AUTHORITY`;
- `seller_login` проходит ту же базовую проверку логина;
- `min_login_length` лежит в диапазоне `1..20`;
- адрес PDA обязан совпадать с `promo_seller=` + `lower(seller_login)`;
- если PDA ещё нет, она создаётся;
- если PDA уже есть, её состояние полностью перезаписывается.
## 12. Инструкция `update_user_pda`
### Назначение
Создать новую версию той же пользовательской записи.
### Авторизация
Те же роли:
- signer/fee payer = `client_key`
- подпись записи = `root_key`
- подпись вершины блокчейна = `blockchain_public_key`
### Аккаунты
- signer = `client_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. `recovery_key` совпадает со старым.
5. `root_key` совпадает со старым.
6. `client_key` совпадает со старым.
7. `version = old.record_number + 1`.
8. `prev_hash = hash(unsigned_old_record)`.
9. `additional_limit % LIMIT_STEP == 0`.
10. `blockchain_name` и `blockchain_public_key` не меняются.
11. `paid_limit_bytes` не уменьшается.
12. `used_bytes` не уменьшается.
13. `last_block_number` не уменьшается.
14. Если состояние блокчейна изменилось, `last_block_signature` заново проверяется через Ed25519.
15. Новая unsigned часть записи подписана `root_key`.
16. При необходимости PDA может быть расширена через realloc.
### Экономика
При update оплачивается только докупаемый лимит:
```text
topup_fee = limit_fee(additional_limit)
```
Если `additional_limit = 0`, доплата не требуется.
### Бинарный ABI
```text
- tag: u8 = 4
- login: string_u8
- recovery_key: [u8; 32]
- root_key: [u8; 32]
- created_at_ms: u64 LE
- updated_at_ms: u64 LE
- version: u32 LE
- prev_hash: [u8; 32]
- additional_limit: u64 LE
- fields: UserMutableFieldsV1
- root_signature: [u8; 64]
```
## 13. Ed25519-проверки и порядок инструкций
В обычной транзакции `create/update` должны стоять две встроенные 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 клиентом и программой.
Если `create_user_pda` вызывается с promo-кодом, то перед ними добавляется ещё одна Ed25519-инструкция:
1. подпись продавца promo-логина по строке `shine_promo_v1:<login>`;
2. подпись `root_key` по unsigned записи;
3. подпись `blockchain_public_key` по `LastBlockState`.
Тогда программа читает:
- `-3` — promo-подпись;
- `-2``root_key`;
- `-1``blockchain_public_key`.
## 14. 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)
```
## 15. Валидируемые mutable-поля записи
Программа допускает обновление:
- `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`
- `recovery_key`
- `root_key`
- `client_key`
- `blockchain_name`
- `blockchain_public_key`
- `blockchain_type`
## 16. Правила серверных и сессионных полей
### UserMutableFieldsV1
```text
- client_key: [u8; 32]
- blockchain_public_key: [u8; 32]
- blockchain_name: string_u8
- used_bytes: u64 LE
- last_block_number: u32 LE
- last_block_hash: [u8; 32]
- last_block_signature: [u8; 64]
- arweave_tx_id: string_u8
- is_server: u8
- if is_server = 1:
- address_format_type: u8
- address_format_version: u8
- server_address: string_u8
- sync_servers_count: u8
- sync_servers[sync_servers_count]: string_u8[]
- access_servers_count: u8
- access_servers[access_servers_count]: string_u8[]
- sessions_mode: u8
- sessions_count: u8
- sessions[sessions_count]:
- session_type: u8
- session_version: u8
- session_name: string_u8
- session_pub_key: [u8; 32]
- trusted_count: u8
```
### 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`, `50` и `100`;
- `session_version` сейчас только `1`;
- `session_name` должен содержать только `[A-Za-z0-9_]`;
- `session_name` и `session_pub_key` уникальны внутри списка.
На текущем этапе обычная регистрация пользователя должна продолжать работать с:
- `sessions_mode = 1`
- `sessions = []`
## 17. Realloc поведения PDA
Запись может расти. Если новая сериализованная запись длиннее текущего размера PDA:
- PDA разрешено увеличить через realloc;
- нельзя делать чрезмерный рост одним шагом выше внутреннего лимита реализации;
- перед realloc нужно обеспечить rent для нового размера.
## 18. Ошибки и классы отказа
Программа должна различать как минимум такие классы ошибок:
- неверный логин;
- 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 и promo seller update;
- неверный promo-код;
- несуществующий или повреждённый `promo_seller_pda`;
- исчерпанная promo-квота;
- promo-подпись не совпадает;
- логин короче `min_login_length`.
## 19. Что должно сохраниться при переписи без Anchor
В текущей чисто-rust реализации уже сохранены:
- те же PDA seed-правила;
- тот же формат `user_pda`;
- ту же экономику регистрации и topup;
- тот же порядок Ed25519-инструкций;
- те же immutable/mutable правила;
- ту же валидацию логина и CPI в `shine_login_guard`;
- ту же promo-механику с PDA продавцов и подписью `shine_promo_v1:<login>`;
- ту же зависимость от inflow vault программы `shine_payments`.
Сознательно не сохранялись:
- структура Anchor `Context`;
- Anchor discriminator'ы и Anchor-ABI инструкций;
- старые seed'ы, которые конфликтовали с уже существующим Anchor-состоянием в devnet;
- внутренние helper-функции старой реализации.