Новый протокол Solana PDA 1.2

This commit is contained in:
AidarKC
2026-09-26 11:01:14 +03:00
parent 6e5b57fd7c
commit c997057a37
47 changed files with 2392 additions and 4480 deletions
+2 -1
View File
@@ -9,7 +9,8 @@
- `shine_login_guard.md`
- `shine_payments.md`
2. Документы по форматам в `doc/formats/`:
- `shine-user-pda-format-v.1.0.md`
- `shine-user-pda-format-v.1.2.md` — текущий формат;
- `shine-user-pda-format-v.1.0.md` — исторический legacy-формат; новые записи его не создают, миграции нет.
Эти документы должны быть достаточными для повторной реализации программ и форматов с нуля.
@@ -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-инструкцию можно удалить из программы.
+92 -730
View File
@@ -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 сервер доступа.
Позже лимиты можно увеличить без изменения структуры блоков.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import {
parseShineUserPda,
serializeUnsignedRecordFromState,
} from '../../../shine-UI/js/services/shine-user-pda-service.js';
const bytes = (len, seed) => Uint8Array.from({ length: len }, (_, i) => (seed + i) & 0xff);
const state = {
login: 'codec_test',
createdAtMs: 1_000n,
updatedAtMs: 2_000n,
recordNumber: 4,
prevRecordHash: bytes(32, 1),
rootKey: bytes(32, 40),
clientKey: bytes(32, 80),
forks: [
{ blockchainKey: bytes(32, 120), createdAtMs: 1_000n, paidLimitBytes: 100_000n },
{ blockchainKey: bytes(32, 160), createdAtMs: 2_000n, paidLimitBytes: 200_000n },
],
serverAddresses: [
{ addressFormatType: 1, addressFormatVersion: 0, address: 'https://s.example' },
],
accessServers: ['access1'],
};
const unsigned = serializeUnsignedRecordFromState(state);
const full = new Uint8Array(unsigned.length + 64);
full.set(unsigned);
full.set(bytes(64, 200), unsigned.length);
const parsed = parseShineUserPda(full);
assert.equal(parsed.formatMajor, 1);
assert.equal(parsed.formatMinor, 2);
assert.equal(parsed.isLegacy, false);
assert.equal(parsed.recordNumber, 4);
assert.equal(parsed.forks.length, 2);
assert.equal(parsed.forks[1].createdAtMs, 2_000n);
assert.equal(parsed.forks[1].paidLimitBytes, 200_000n);
assert.equal(parsed.serverAddresses.length, 1);
assert.deepEqual(parsed.accessServers, ['access1']);
assert.equal(parsed.blockchain.blockchainName, 'codec_test-002');
console.log(`PDA 1.2 codec smoke test OK (${full.length} bytes)`);
+2 -1
View File
@@ -1,3 +1,4 @@
// Legacy 1.0 e2e kept only as a historical fixture. PDA 1.2 uses the native compact instruction wire format; see pda-v1-2-codec.mjs and the 1.2 format spec.
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import {
@@ -189,7 +190,7 @@ function extractSigFromEdIx(ixData: Buffer): Buffer {
return ixData.subarray(signatureOffset, signatureOffset + 64);
}
describe("shine_users e2e", () => {
describe.skip("legacy shine_users PDA 1.0 e2e (reference only)", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const provider = anchor.getProvider() as anchor.AnchorProvider;
const program = anchor.workspace.shine as Program<Shine>;