22 KiB
Программа 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:
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
Формула:
user_pda = PDA(["user_login=", lower(login)], shine_users_program_id)
3.2. Economy config PDA
PDA экономических настроек:
- seed:
shine_users_economy_config
Формула:
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: u8registration_fee_lamports: u64lamports_per_limit_step: u64start_bonus_limit: u64
Смысл:
registration_fee_lamports— базовая плата за регистрацию;lamports_per_limit_step— стоимость одного шага лимита;start_bonus_limit— стартовый бесплатный лимит записи, который получает новый пользователь.
4.2. user_pda
Формат пользовательской записи описан отдельно:
Этот документ описывает именно логику программы, а не байтовую структуру блока.
5. Константы и базовые правила
Базовые значения из текущей логики:
- seed
user_pda:user_login= - seed economy config:
shine_users_economy_config - стартовый размер
user_pda:768байт LIMIT_STEP = 10_000START_REGISTRATION_FEE_LAMPORTS = 10_000_000START_LAMPORTS_PER_LIMIT_STEP = 100_000START_BONUS_LIMIT = 100_000
Правила:
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 формата.
Согласованная клиентская схема деривации для первой публичной версии:
seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || suffix_utf8)
Согласованные suffix:
"recovery.key"
"root.key"
"blockchain.key"
"client.key"
On-chain инвариант только один:
- публичные ключи и подписи должны соответствовать друг другу.
7. Логин и login guard
Перед созданием пользователя логин обязан пройти две проверки:
- базовая syntactic validation внутри
shine_users; - 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
- 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
- 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_pdashine_login_guard_program
Входные данные
- логин
recovery_keyroot_keycreated_at_msadditional_limit- mutable fields записи
rootsignature по unsigned части
Бинарный ABI
- 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]
Обязательные проверки
- Логин валиден по синтаксису.
- Логин разрешён
shine_login_guard. additional_limit % LIMIT_STEP == 0.- inflow vault совпадает с PDA программы
shine_payments. user_pdaвычислена правильно и ещё не существует.- Поля блокчейна валидны.
- Поля server/session/trusted валидны по формату.
last_block_signatureсоответствуетLastBlockState.root signatureсоответствует unsigned части записи.- Размер сериализованной записи не превышает допустимый стартовый размер PDA или иные ограничения реализации.
Экономика
При создании:
- пользователь получает
start_bonus_limit; - дополнительно может купить
additional_limit; - итоговый оплаченный лимит:
paid_limit_bytes = start_bonus_limit + additional_limit
Комиссия:
total_fee = registration_fee_lamports + limit_fee(additional_limit)
Где:
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 =
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
Обязательные проверки
- PDA существует и принадлежит
shine_users. - Новый логин совпадает со старым.
created_at_msсовпадает со старым.recovery_keyсовпадает со старым.root_keyсовпадает со старым.client_keyсовпадает со старым.version = old.record_number + 1.prev_hash = hash(unsigned_old_record).additional_limit % LIMIT_STEP == 0.blockchain_nameиblockchain_public_keyне меняются.paid_limit_bytesне уменьшается.used_bytesне уменьшается.last_block_numberне уменьшается.- Если состояние блокчейна изменилось,
last_block_signatureзаново проверяется через Ed25519. - Новая unsigned часть записи подписана
root_key. - При необходимости PDA может быть расширена через realloc.
Экономика
При update оплачивается только докупаемый лимит:
topup_fee = limit_fee(additional_limit)
Если additional_limit = 0, доплата не требуется.
Бинарный ABI
- 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]
12. Ed25519-проверки и порядок инструкций
В транзакции должны стоять две встроенные Ed25519-инструкции прямо перед вызовом shine_users:
- подпись
root_keyпо unsigned записи; - подпись
blockchain_public_keyпоLastBlockState.
Текущая логика shine_users читает их через sysvar instructions относительно текущего индекса:
-2—root_key-1—blockchain_public_key
Это правило порядка является частью контракта между off-chain клиентом и программой.
13. LastBlockState
Сообщение для подписи blockchain_public_key:
- constant: "SHiNE_LAST_BLOCK"
- login
- blockchain_name
- last_block_number
- last_block_hash[32]
- used_bytes
Алгоритм:
message_hash = SHA-256(LastBlockState bytes)
signature = Ed25519(blockchain_private_key, message_hash)
14. Валидируемые mutable-поля записи
Программа допускает обновление:
used_byteslast_block_numberlast_block_hashlast_block_signaturearweave_tx_idis_serverserver profileaccess_serverssessions_modesessionstrusted_countadditional_limit
Программа не допускает update:
logincreated_at_msrecovery_keyroot_keyclient_keyblockchain_nameblockchain_public_keyblockchain_type
15. Правила серверных и сессионных полей
UserMutableFieldsV1
- 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 = 1sessions = []
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-функции старой реализации.