SHA256
shine_payments + shine_users: - create_pda_account переведён на «создание поверх предзаполненного» (allocate+assign+добор ренты), чтобы подсев лампортов на детерминированный адрес PDA (тикет/логин) не блокировал создание — закрыт LOW из аудита №1; в shine_payments is_uninitialized_account перестала зависеть от баланса. shine_payments (HIGH из аудита №2): - запрещён recipient == inflow_vault в buy_ticket*, manager_add_ticket и change_ticket_recipient; добавлена защита по умолчанию в transfer_from_vault (require vault.key != recipient.key). Это убирает алиасинг аккаунта в step_payout, который навсегда замораживал очередь выплат и средства вольта. Документация и учёт: - doc/programs/shine_payments.md §3.4, §10.1; doc/programs/shine_users.md §3.3; - Dev_Docs/audit: добавлен аудит №2, обе закрытые находки помечены ИСПРАВЛЕНО; - Dev_Docs/Pending_Features: две записи на ручную e2e-проверку на devnet; - VERSION.properties: client 1.2.161, server 1.2.150. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
580 lines
20 KiB
Markdown
580 lines
20 KiB
Markdown
# Программа `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: `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` и пустые данные) остаются и
|
|
не зависят от баланса аккаунта.
|
|
|
|
## 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`: `user_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`.
|
|
|
|
### Бинарный 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 инструкции:
|
|
|
|
- `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 части
|
|
|
|
### Бинарный ABI
|
|
|
|
```text
|
|
- tag: u8 = 3
|
|
- login: string_u8
|
|
- root_key: [u8; 32]
|
|
- created_at_ms: u64 LE
|
|
- additional_limit: u64 LE
|
|
- fields: UserMutableFieldsV1
|
|
- root_signature: [u8; 64]
|
|
```
|
|
|
|
### Обязательные проверки
|
|
|
|
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`, доплата не требуется.
|
|
|
|
### Бинарный ABI
|
|
|
|
```text
|
|
- tag: u8 = 4
|
|
- login: string_u8
|
|
- 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]
|
|
```
|
|
|
|
## 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. Правила серверных и сессионных полей
|
|
|
|
### UserMutableFieldsV1
|
|
|
|
```text
|
|
- device_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` и `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'ы и Anchor-ABI инструкций;
|
|
- старые seed'ы, которые конфликтовали с уже существующим Anchor-состоянием в devnet;
|
|
- внутренние helper-функции старой реализации.
|