SHA256
Новый протокол Solana PDA 1.2
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
# Solana user_pda: итоговый целевой формат пользовательской записи
|
||||
# Solana user_pda: формат 1.0 (LEGACY)
|
||||
|
||||
Документ описывает целевой формат пользовательской PDA-записи `user_pda` для Solana-программы `shine_users`.
|
||||
Документ описывает legacy-формат пользовательской PDA-записи `user_pda` для Solana-программы `shine_users`.
|
||||
|
||||
Это не формат основного блокчейна SHiNE и не документация по `AddBlock`. Основной блокчейн SHiNE описан отдельно в `docs/Blockchain/`.
|
||||
|
||||
Статус документа: итоговый согласованный формат, к которому приведены `create_user_pda`, `update_user_pda` и тестовый сериализатор Solana-модуля.
|
||||
Статус документа: legacy. Новые записи создаются только в формате 1.2; миграции 1.0 → 1.2 нет. Формат сохранён как историческое описание тестовых PDA, которые можно закрыть отдельной временной инструкцией.
|
||||
|
||||
## 1. Назначение user_pda
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# Solana `user_pda`: формат 1.2
|
||||
|
||||
Актуальный бинарный формат пользовательской PDA программы `shine_users`.
|
||||
Формат пользовательских SHiNE-блоков и Arweave этим изменением не меняется.
|
||||
|
||||
## Назначение
|
||||
|
||||
PDA 1.2 хранит идентичность и authority пользователя, append-only историю blockchain keys, оплаченный лимит и маршрутизацию.
|
||||
Состояние вершины пользовательского blockchain в Solana больше не хранится.
|
||||
|
||||
Из PDA 1.2 удалены `RecoveryKeyBlock`, `SessionsBlock`, `TrustedStateBlock`, `ArchiveHeadBlock`, `sync_servers[]`, а также старые `used_bytes`, `last_block_*`, `arweave_tx_id`, `blockchain_name/type`.
|
||||
|
||||
## Общие правила кодирования
|
||||
|
||||
- Little Endian;
|
||||
- `Pubkey` — 32 байта;
|
||||
- Ed25519 signature — 64 байта;
|
||||
- hash — 32 байта;
|
||||
- строка — `len:u8 + UTF-8 bytes`;
|
||||
- `block_type:u8 + block_version:u8` есть у каждого typed block;
|
||||
- фиксированные `RootKeyBlock` и `ClientKeyBlock` не содержат длину;
|
||||
- любой variable block: `block_type:u8 + block_version:u8 + payload_len:u16 + payload`;
|
||||
- неизвестный variable block можно пропустить по `payload_len`.
|
||||
|
||||
Все известные блоки PDA 1.2 имеют `block_version = 0`.
|
||||
|
||||
## Header
|
||||
|
||||
```text
|
||||
magic: [u8;5] = "SHiNE"
|
||||
format_major: u8 = 1
|
||||
format_minor: u8 = 2
|
||||
record_len: u16
|
||||
created_at_ms: u64
|
||||
updated_at_ms: u64
|
||||
record_number: u32
|
||||
prev_record_hash: [u8;32]
|
||||
login_len: u8
|
||||
login: [u8;login_len]
|
||||
blocks_count: u8
|
||||
blocks: TypedBlock[blocks_count]
|
||||
signature: [u8;64]
|
||||
```
|
||||
|
||||
`record_len` включает подпись и не включает padding Solana account.
|
||||
`record_number=0` на create и увеличивается программой на 1 после каждого успешного update.
|
||||
`prev_record_hash=0` на create; при update это hash unsigned-части предыдущей PDA.
|
||||
|
||||
## Типы блоков
|
||||
|
||||
| type | блок | статус |
|
||||
|---:|---|---|
|
||||
| 1 | `RootKeyBlock` | обязательный |
|
||||
| 2 | `ClientKeyBlock` | обязательный |
|
||||
| 3 | `BlockchainRegistryBlock` | обязательный |
|
||||
| 30 | `ServerProfileBlock` | опциональный |
|
||||
| 40 | `AccessServersBlock` | опциональный |
|
||||
|
||||
## RootKeyBlock
|
||||
|
||||
```text
|
||||
block_type: u8 = 1
|
||||
block_version: u8 = 0
|
||||
root_key: [u8;32]
|
||||
```
|
||||
|
||||
`root_key` — холодный recovery authority. Blockchain authority не может изменить root. Root authority может изменить root и остальные поля.
|
||||
|
||||
## ClientKeyBlock
|
||||
|
||||
```text
|
||||
block_type: u8 = 2
|
||||
block_version: u8 = 0
|
||||
client_key: [u8;32]
|
||||
```
|
||||
|
||||
Client key не является authority PDA. Он может использоваться клиентским уровнем и как fee payer Solana-транзакции.
|
||||
|
||||
## BlockchainRegistryBlock
|
||||
|
||||
```text
|
||||
block_type: u8 = 3
|
||||
block_version: u8 = 0
|
||||
payload_len: u16
|
||||
|
||||
fork_count: u16
|
||||
forks[fork_count]:
|
||||
blockchain_key: [u8;32]
|
||||
created_at_ms: u64
|
||||
paid_limit_bytes: u32
|
||||
```
|
||||
|
||||
Одна fork-запись занимает 44 байта.
|
||||
|
||||
Правила:
|
||||
|
||||
- `fork_count >= 1`;
|
||||
- последний fork — активный;
|
||||
- старые fork-записи append-only: их нельзя удалить, переставить или изменить;
|
||||
- один blockchain key нельзя добавить повторно;
|
||||
- `paid_limit_bytes` старых fork неизменяем;
|
||||
- лимит можно пополнять только у последнего fork;
|
||||
- новый fork наследует текущий лимит плюс оплаченный top-up;
|
||||
- обычный blockchain authority может создать новый fork не раньше чем через 72 часа после `created_at_ms` активного fork;
|
||||
- root recovery может создать fork без 72-часового ожидания.
|
||||
|
||||
`created_at_ms` передаёт клиент и включает в подписываемую PDA. Программа принимает его только если он отличается от Solana Clock не более чем на ±5 минут.
|
||||
|
||||
## ServerProfileBlock
|
||||
|
||||
Блок отсутствует у обычного пользователя. Наличие блока означает, что PDA публикует SHiNE-server endpoint.
|
||||
|
||||
```text
|
||||
block_type: u8 = 30
|
||||
block_version: u8 = 0
|
||||
payload_len: u16
|
||||
|
||||
address_count: u8
|
||||
addresses[address_count]:
|
||||
address_format_type: u8
|
||||
address_format_version: u8
|
||||
address_len: u8
|
||||
address: [u8;address_len]
|
||||
```
|
||||
|
||||
PDA 1.2 разрешает **ровно один адрес**, если `ServerProfileBlock` присутствует. Массив/count сохранён для будущего увеличения лимита без изменения бинарной структуры.
|
||||
|
||||
`address_format_type + address_format_version` позволяют позже стандартизовать URL, IPv4, IPv6, Tor/I2P и другие адресные форматы.
|
||||
|
||||
## AccessServersBlock
|
||||
|
||||
```text
|
||||
block_type: u8 = 40
|
||||
block_version: u8 = 0
|
||||
payload_len: u16
|
||||
|
||||
server_count: u8
|
||||
servers[server_count]:
|
||||
login_len: u8
|
||||
login: [u8;login_len]
|
||||
```
|
||||
|
||||
PDA 1.2 разрешает `server_count = 0` или `1`. Count оставлен для будущего расширения.
|
||||
|
||||
## Authority и подпись
|
||||
|
||||
Новая PDA хранит `signature[64]` authority **нового состояния** по hash unsigned PDA:
|
||||
|
||||
- обычный update — активный blockchain key;
|
||||
- новый fork — новый blockchain key;
|
||||
- root-mode — root нового состояния;
|
||||
- смена root — новый root.
|
||||
|
||||
На update программа дополнительно требует Ed25519-подпись authority предыдущего состояния по тому же hash нового unsigned state. Эта transition-подпись не хранится внутри PDA.
|
||||
|
||||
Blockchain-mode не может изменить root. Root-mode может изменить root и выполнить recovery. Одновременно менять root и добавлять blockchain fork одной транзакцией запрещено.
|
||||
|
||||
## Create
|
||||
|
||||
Создаются только PDA 1.2.
|
||||
|
||||
```text
|
||||
record_number = 0
|
||||
prev_record_hash = 0x00 * 32
|
||||
fork_count = 1
|
||||
fork[0].blockchain_key = initial blockchain key
|
||||
fork[0].created_at_ms = created_at_ms
|
||||
fork[0].paid_limit_bytes = start_bonus + paid top-up
|
||||
```
|
||||
|
||||
Root доказывает владение recovery-key отдельной Ed25519 instruction, а `signature[64]` новой PDA делает initial blockchain key.
|
||||
|
||||
## Legacy 1.0
|
||||
|
||||
Нормальные create/update/read flows нового клиента и сервера поддерживают только PDA 1.2.
|
||||
|
||||
Временно существует instruction `close_legacy_pda` для тестовых PDA 1.0. Она:
|
||||
|
||||
- принимает только PDA с `format_major=1`, `format_minor=0`;
|
||||
- не может закрыть 1.2;
|
||||
- закрывает старый program-owned account;
|
||||
- переводит его lamports вызывающему signer.
|
||||
|
||||
Legacy migration в 1.2 отсутствует. После удаления тестовых PDA временную close-инструкцию можно удалить из программы.
|
||||
@@ -1,775 +1,137 @@
|
||||
# Программа `shine_users`
|
||||
|
||||
Документ описывает целевое поведение Solana-программы регистрации пользователей SHiNE.
|
||||
Актуальный пользовательский формат — `user_pda 1.2`.
|
||||
Полный бинарный формат: `doc/formats/shine-user-pda-format-v.1.2.md`.
|
||||
Legacy 1.0 считается тестовым и не мигрируется.
|
||||
|
||||
Назначение документа:
|
||||
## Что хранит программа
|
||||
|
||||
- быть источником истины при поддержке текущей реализации;
|
||||
- позволить заново реализовать программу без Anchor;
|
||||
- зафиксировать инварианты, форматы и правила проверки.
|
||||
- `root_key` — cold recovery authority;
|
||||
- `client_key`;
|
||||
- append-only список blockchain fork keys с временем создания и оплаченной квотой;
|
||||
- один optional server address;
|
||||
- ноль или один access server;
|
||||
- `record_number`, `prev_record_hash` и переносимую Ed25519-подпись состояния.
|
||||
|
||||
Если код программы расходится с этим документом, это считается ошибкой: нужно либо исправить код, либо обновить документ в том же изменении.
|
||||
Пользовательские SHiNE-блоки/Arweave этим патчем не меняются.
|
||||
|
||||
## 1. Назначение программы
|
||||
## Инструкции
|
||||
|
||||
`shine_users` хранит публичную пользовательскую запись SHiNE в Solana PDA и управляет её экономикой.
|
||||
### `1 init_users_economy_config`
|
||||
Без изменений.
|
||||
|
||||
Программа отвечает за:
|
||||
### `2 update_users_economy_config`
|
||||
Без изменений.
|
||||
|
||||
- создание `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:
|
||||
|
||||
- `SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6`
|
||||
|
||||
Внешние зависимости по логике:
|
||||
|
||||
- `shine_payments`
|
||||
- используется только как источник PDA inflow-вольта;
|
||||
- `shine_login_guard`
|
||||
- используется для классификации логина через CPI;
|
||||
- системная программа Solana;
|
||||
- sysvar `instructions`.
|
||||
|
||||
Текущие зашитые внешние адреса:
|
||||
|
||||
- `DAO_AUTHORITY`: `aiShm43fZjm3YkMs22sYL1bpXaL3bVxv7SSraPHzVgq`
|
||||
- `shine_payments`: `SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v`
|
||||
- `shine_login_guard`: `SHiGxGsXGioQYCYhchQ5R7KWoxN5UjFAFsucPf6sfnh`
|
||||
|
||||
## 3. PDA и seed-правила
|
||||
|
||||
### 3.1. Пользовательская PDA
|
||||
|
||||
Пользовательская запись строится так:
|
||||
|
||||
- seed prefix: `user_login=`
|
||||
- второй seed: логин в нижнем регистре
|
||||
- program id: `shine_users`
|
||||
|
||||
Формула:
|
||||
### `3 create_user_pda`
|
||||
|
||||
```text
|
||||
user_pda = PDA(["user_login=", lower(login)], shine_users_program_id)
|
||||
tag:u8 = 3
|
||||
login:string_u8
|
||||
root_key:[32]
|
||||
created_at_ms:u64
|
||||
additional_limit:u64
|
||||
client_key:[32]
|
||||
blockchain_key:[32]
|
||||
address_count:u8 // 0 или 1
|
||||
[address]
|
||||
access_server_count:u8 // 0 или 1
|
||||
[access_server]
|
||||
record_signature:[64]
|
||||
[promo_seller_login:string_u8]
|
||||
```
|
||||
|
||||
### 3.2. Economy config PDA
|
||||
|
||||
PDA экономических настроек:
|
||||
|
||||
- seed: `shine_users_economy_config`
|
||||
|
||||
Формула:
|
||||
Server address:
|
||||
|
||||
```text
|
||||
users_economy_config_pda = PDA(["shine_users_economy_config"], shine_users_program_id)
|
||||
address_format_type:u8
|
||||
address_format_version:u8
|
||||
address:string_u8
|
||||
```
|
||||
|
||||
### 3.3. Правило создания PDA (защита от «минирования» адреса)
|
||||
Fee payer — любой переданный signer; он не обязан совпадать с client key.
|
||||
`created_at_ms` должен быть в пределах ±5 минут от Solana Clock.
|
||||
Root даёт proof-of-possession, а initial blockchain key подписывает саму новую PDA.
|
||||
|
||||
Адрес пользовательской PDA выводится из логина и публично предсказуем: зная желаемый логин,
|
||||
любой может заранее вычислить адрес записи и перевести на него немного лампортов обычным
|
||||
system-переводом. Если бы создание шло строго через `system_instruction::create_account`,
|
||||
такой «подсев» приводил бы к ошибке «account already in use» и навсегда блокировал бы
|
||||
регистрацию этого логина (targeted-DoS / сквоттинг логинов), причём без оплаты комиссии.
|
||||
### `4 update_user_pda`
|
||||
|
||||
Поэтому `create_pda_account` создаёт аккаунт устойчиво к предзаполненному балансу:
|
||||
|
||||
- если на адресе нет лампортов — обычный `create_account` (быстрый путь);
|
||||
- если лампорты уже есть — «создание поверх предзаполненного»: добор ренты переводом,
|
||||
затем `allocate` + `assign` под подписью PDA.
|
||||
|
||||
Проверки повторной инициализации (`owner == System Program` и пустые данные) остаются и
|
||||
не зависят от баланса аккаунта.
|
||||
|
||||
### 3.4. PDA продавца promo-логинов
|
||||
|
||||
PDA продавца строится так:
|
||||
|
||||
- seed prefix: `promo_seller=`
|
||||
- второй seed: логин продавца в нижнем регистре
|
||||
|
||||
Формула:
|
||||
Работает только с PDA 1.2.
|
||||
|
||||
```text
|
||||
promo_seller_pda = PDA(["promo_seller=", lower(seller_login)], shine_users_program_id)
|
||||
tag:u8 = 4
|
||||
login:string_u8
|
||||
new_root_key:[32]
|
||||
updated_at_ms:u64
|
||||
additional_limit:u64
|
||||
new_client_key:[32]
|
||||
auth_mode:u8 // 0 blockchain, 1 root
|
||||
new_blockchain_present:u8
|
||||
[new_blockchain_key:[32]]
|
||||
address_count:u8 // 0 или 1
|
||||
[address]
|
||||
access_server_count:u8 // 0 или 1
|
||||
[access_server]
|
||||
record_signature:[64]
|
||||
```
|
||||
|
||||
В программе может существовать сколько угодно таких PDA, по одному на каждого продавца.
|
||||
Программа сама вычисляет `record_number=old+1` и `prev_record_hash`.
|
||||
`updated_at_ms` должен быть в пределах ±5 минут от Solana Clock.
|
||||
|
||||
### 3.5. Строгий список аккаунтов (нет «лишних» аккаунтов)
|
||||
Перед update идут две Ed25519 instructions по hash нового unsigned state:
|
||||
|
||||
Все инструкции `shine_users` читают строго фиксированный набор аккаунтов и после этого
|
||||
явно требуют, чтобы в переданном списке больше ничего не было
|
||||
(`require!(it.next().is_none(), InvalidInstruction)`). Если вызывающий добавит лишние
|
||||
аккаунты в хвост, инструкция завершится ошибкой `InvalidInstruction (1)`.
|
||||
1. старый authority разрешает переход;
|
||||
2. authority нового состояния подписывает запись; эта подпись сохраняется в PDA.
|
||||
|
||||
Это не закрывает отдельной уязвимости (каждый используемый аккаунт и так строго
|
||||
валидируется по signer/owner/адресу PDA), а является defense-in-depth и приводит поведение
|
||||
к единому виду с `shine_payments`, где такая же проверка стоит во всех инструкциях.
|
||||
Списки аккаунтов в разделах ниже надо считать исчерпывающими и точными по количеству.
|
||||
Blockchain-mode:
|
||||
|
||||
## 4. Состояния программы
|
||||
- старый authority = последний blockchain key;
|
||||
- root менять нельзя;
|
||||
- новый fork — только после 72 часов от времени активного fork.
|
||||
|
||||
### 4.1. `UsersEconomyConfigState`
|
||||
Root-mode:
|
||||
|
||||
Хранится в `users_economy_config_pda`.
|
||||
- старый authority = root;
|
||||
- root можно менять;
|
||||
- recovery fork может обходить 72-часовой cooldown.
|
||||
|
||||
Поля:
|
||||
Root rotation и blockchain fork одной транзакцией запрещены.
|
||||
|
||||
- `version: u8`
|
||||
- `registration_fee_lamports: u64`
|
||||
- `lamports_per_limit_step: u64`
|
||||
- `start_bonus_limit: u64`
|
||||
### `5 upsert_promo_seller`
|
||||
Без изменений.
|
||||
|
||||
Смысл:
|
||||
### `6 close_legacy_pda`
|
||||
|
||||
- `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 формата.
|
||||
|
||||
Согласованная клиентская схема деривации для первой публичной версии:
|
||||
Временная cleanup-инструкция для тестовых PDA 1.0.
|
||||
|
||||
```text
|
||||
seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || suffix_utf8)
|
||||
tag:u8 = 6
|
||||
login:string_u8
|
||||
```
|
||||
|
||||
Согласованные suffix:
|
||||
Accounts:
|
||||
|
||||
1. caller signer + writable;
|
||||
2. legacy user PDA writable.
|
||||
|
||||
Программа проверяет PDA seed/owner и `format=1.0`, переводит все lamports PDA вызывающему и обнуляет account data. PDA 1.2 этой инструкцией закрыть нельзя.
|
||||
|
||||
## BlockchainRegistry 1.2
|
||||
|
||||
```text
|
||||
"recovery.key"
|
||||
"root.key"
|
||||
"blockchain.key"
|
||||
"client.key"
|
||||
fork_count:u16
|
||||
forks[]:
|
||||
blockchain_key:[32]
|
||||
created_at_ms:u64
|
||||
paid_limit_bytes:u32
|
||||
```
|
||||
|
||||
On-chain инвариант только один:
|
||||
Последний fork активный. Старые записи неизменяемы.
|
||||
|
||||
- публичные ключи и подписи должны соответствовать друг другу.
|
||||
## Server/access limits в 1.2
|
||||
|
||||
## 7. Логин и login guard
|
||||
Бинарный формат использует count/arrays для forward compatibility, но текущая программа намеренно ограничивает:
|
||||
|
||||
Перед созданием пользователя логин обязан пройти две проверки:
|
||||
|
||||
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
|
||||
|
||||
При создании пользователя off-chain клиент может использовать опциональный `promo_code`.
|
||||
|
||||
Формат строки:
|
||||
|
||||
```text
|
||||
1<seller_login>-<signature_base58>
|
||||
```
|
||||
|
||||
Где:
|
||||
|
||||
- первый символ `1` — версия формата promo-кода;
|
||||
- `seller_login` — логин продавца, по которому ищется `promo_seller_pda`;
|
||||
- `signature_base58` — Ed25519-подпись по сообщению:
|
||||
|
||||
```text
|
||||
shine_promo_v1:<login_из_запроса_create_user_pda>
|
||||
```
|
||||
|
||||
В сам `create_user_pda` при этом передаётся только `seller_login`, а подпись проверяется по отдельной Ed25519-инструкции в транзакции.
|
||||
|
||||
Если 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_seller_login`
|
||||
|
||||
### Бинарный 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_seller_login: string_u8
|
||||
```
|
||||
|
||||
### Обязательные проверки
|
||||
|
||||
1. Логин валиден по синтаксису.
|
||||
2. Если promo-код не передан, логин разрешён `shine_login_guard`.
|
||||
3. Если promo-регистрация включена, программа получает `promo_seller_login`, а связанная Ed25519-инструкция должна:
|
||||
- ссылаться на существующий `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:<login>`;
|
||||
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:<login>`;
|
||||
- ту же зависимость от inflow vault программы `shine_payments`.
|
||||
|
||||
Сознательно не сохранялись:
|
||||
|
||||
- структура Anchor `Context`;
|
||||
- Anchor discriminator'ы и Anchor-ABI инструкций;
|
||||
- старые seed'ы, которые конфликтовали с уже существующим Anchor-состоянием в devnet;
|
||||
- внутренние helper-функции старой реализации.
|
||||
|
||||
## ArchiveHeadBlock и серверный SHINE-ARCHIVE
|
||||
|
||||
Формат User PDA поддерживает необязательный `ArchiveHeadBlock` (`block_type = 100`, `block_version = 0`):
|
||||
|
||||
```text
|
||||
archive_tx_id [32]
|
||||
archive_hash [32]
|
||||
```
|
||||
|
||||
Он хранит текущую голову архива конкретного SHiNE-аккаунта: raw Arweave TX ID и SHA-256 соответствующего большого `SHINE-ARCHIVE`. Подробный бинарный формат и серверный workflow находятся в `docs/Archive/01_PROTOCOL_v1.0.md`.
|
||||
|
||||
Отдельной инструкции программы для архива нет. Используется существующий `update_user_pda`. Парсер update instruction обратно совместим:
|
||||
|
||||
- legacy payload без archive extension сохраняет старый block `100`;
|
||||
- новый payload может заменить/очистить archive head;
|
||||
- итоговая полная User PDA запись, включая block `100`, покрывается обычной root-подписью.
|
||||
|
||||
Это позволяет обычным старым клиентским обновлениям профиля не стирать archive head серверного publisher-а.
|
||||
- `ServerProfileBlock`: один адрес;
|
||||
- `AccessServersBlock`: 0 или 1 сервер доступа.
|
||||
|
||||
Позже лимиты можно увеличить без изменения структуры блоков.
|
||||
|
||||
Reference in New Issue
Block a user