# Программа `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 devnet/localnet: - `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ` Внешние зависимости по логике: - `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` и пустые данные) остаются и не зависят от баланса аккаунта. ### 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 При создании пользователя допускается опциональный `promo_code`. Формат строки: ```text 1- ``` Где: - первый символ `1` — версия формата promo-кода; - `seller_login` — логин продавца, по которому ищется `promo_seller_pda`; - `signature_base58` — Ed25519-подпись по сообщению: ```text shine_promo_v1: ``` Если 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_code` ### Бинарный 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_code: string_u8 ``` ### Обязательные проверки 1. Логин валиден по синтаксису. 2. Если promo-код не передан, логин разрешён `shine_login_guard`. 3. Если promo-код передан, он должен: - иметь формат `1-`; - ссылаться на существующий `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:`; 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:`; - ту же зависимость от inflow vault программы `shine_payments`. Сознательно не сохранялись: - структура Anchor `Context`; - Anchor discriminator'ы и Anchor-ABI инструкций; - старые seed'ы, которые конфликтовали с уже существующим Anchor-состоянием в devnet; - внутренние helper-функции старой реализации.