# 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, которые можно закрыть отдельной временной инструкцией.
Актуальный бинарный формат пользовательской 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 можно пропустить по `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 одной транзакцией запрещено.
Off-chain клиентская логика может хранить приватные ключи в PKCS#8 и извлекать `seed32` для `client`-signer. Это допустимая клиентская реализация, но не часть on-chain формата.
Согласованная клиентская схема деривации для первой публичной версии:
Временная cleanup-инструкция для тестовых PDA 1.0.
Программа проверяет 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`.
Формат сессий описан в 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 сервер доступа.
Позже лимиты можно увеличить без изменения структуры блоков.
// 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*asanchorfrom"@coral-xyz/anchor";
import{Program}from"@coral-xyz/anchor";
import{
@@ -189,7 +190,7 @@ function extractSigFromEdIx(ixData: Buffer): Buffer {
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.