# Программа `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` и пустые данные) остаются и не зависят от баланса аккаунта. ### 3.4. Строгий список аккаунтов (нет «лишних» аккаунтов) Все инструкции `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) Этот документ описывает именно логику программы, а не байтовую структуру блока. ## 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`, `50` и `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-функции старой реализации.