SHiNE-server/shine-solana/shine/doc/programs/shine_users.md
2026-06-22 21:57:09 +04:00

22 KiB
Raw Blame History

Программа 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

Формула:

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: 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

Формат пользовательской записи описан отдельно:

Этот документ описывает именно логику программы, а не байтовую структуру блока.

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. Ключи и подписи

В записи участвуют четыре ключевых роли:

  • 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

Перед созданием пользователя логин обязан пройти две проверки:

  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

- 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_pda
  • shine_login_guard_program

Входные данные

  • логин
  • recovery_key
  • root_key
  • created_at_ms
  • additional_limit
  • mutable fields записи
  • root signature по 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]

Обязательные проверки

  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;
  • итоговый оплаченный лимит:
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

Обязательные проверки

  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 оплачивается только докупаемый лимит:

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:

  1. подпись root_key по unsigned записи;
  2. подпись blockchain_public_key по LastBlockState.

Текущая логика shine_users читает их через sysvar instructions относительно текущего индекса:

  • -2root_key
  • -1blockchain_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_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

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 = 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-функции старой реализации.