Обновить формат Solana user PDA
This commit is contained in:
parent
74df7e2645
commit
baef264bd0
@ -0,0 +1,23 @@
|
||||
# Solana user_pda v2
|
||||
|
||||
## Краткое описание
|
||||
|
||||
Функции `create_user_pda` и `update_user_pda` в Solana-модуле переведены на блочный формат пользовательской PDA-записи `format_major = 2`.
|
||||
|
||||
## Что проверять
|
||||
|
||||
- Создание `user_pda` через `create_user_pda`.
|
||||
- Обновление `user_pda` через `update_user_pda`.
|
||||
- Проверку root-подписи записи.
|
||||
- Проверку подписи `LastBlockState` ключом `blockchain_public_key`.
|
||||
- Корректную запись блоков `RootKey`, `DeviceKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `TrustedState`.
|
||||
- Рост `paid_limit_bytes`, `used_bytes` и `last_block_number` без возможности уменьшения.
|
||||
- Совместимость тестового клиента с актуальной IDL после `anchor build`.
|
||||
|
||||
## Ожидаемый результат
|
||||
|
||||
Пользовательская PDA создается и обновляется в формате `format_major = 2`, содержит один основной блокчейн `blockchain_type = 1` с именем `<login>-001`, а неверные подписи или попытки уменьшить счетчики отклоняются программой.
|
||||
|
||||
## Статус
|
||||
|
||||
pending
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
Это не формат основного блокчейна SHiNE и не документация по `AddBlock`. Основной блокчейн SHiNE описан отдельно в `Dev_Docs/Blockchain/`.
|
||||
|
||||
Статус документа: итоговый согласованный целевой формат. Текущий код Solana-модуля пока использует старый линейный формат записи; этот документ должен стать основой для изменения кода, тестов и интеграции с сервером.
|
||||
Статус документа: итоговый согласованный формат, к которому приведены `create_user_pda`, `update_user_pda` и тестовый сериализатор Solana-модуля.
|
||||
|
||||
## 1. Назначение user_pda
|
||||
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
client.version=1.2.89
|
||||
server.version=1.2.83
|
||||
client.version=1.2.90
|
||||
server.version=1.2.84
|
||||
|
||||
@ -1,225 +1,365 @@
|
||||
# SHINY USER FORMAT v1.0 (DRAFT)
|
||||
# Solana user_pda: итоговый целевой формат пользовательской записи
|
||||
|
||||
Документ описывает целевой бинарный формат пользовательской записи в `user_pda` для программы `shine_users`.
|
||||
Документ описывает целевой формат пользовательской PDA-записи `user_pda` для Solana-программы `shine_users`.
|
||||
|
||||
## 1) Статус версии и цель
|
||||
Это не формат основного блокчейна SHiNE и не документация по `AddBlock`. Основной блокчейн SHiNE описан отдельно в `Dev_Docs/Blockchain/`.
|
||||
|
||||
- Текущий on-chain формат: `v1.0`.
|
||||
- Этот документ: `v1.0 (draft)` для текущего этапа.
|
||||
- Цель текущей версии: зафиксировать рабочий формат и сразу оставить в нем поля для будущего расширения.
|
||||
Статус документа: итоговый согласованный формат, к которому приведены `create_user_pda`, `update_user_pda` и тестовый сериализатор Solana-модуля.
|
||||
|
||||
Новые статусные поля:
|
||||
- `root_key_status`
|
||||
- `blockchain_key_status`
|
||||
- `device_key_status`
|
||||
## 1. Назначение user_pda
|
||||
|
||||
Текущее значение каждого статуса: `0` (ключ создан и не менялся).
|
||||
`user_pda` хранит публичное состояние пользователя в Solana:
|
||||
|
||||
## 2) Общие правила кодирования
|
||||
- логин пользователя;
|
||||
- неизменяемые параметры создания записи;
|
||||
- корневой публичный ключ пользователя;
|
||||
- ключ устройства;
|
||||
- данные одного или нескольких пользовательских блокчейнов SHiNE;
|
||||
- серверные данные пользователя, если пользователь выступает сервером;
|
||||
- серверы доступа пользователя;
|
||||
- счетчики/лимиты;
|
||||
- подпись записи.
|
||||
|
||||
- Числа: Little Endian (`LE`).
|
||||
- Строки: `UTF-8` с префиксом длины `u8`.
|
||||
- Публичные ключи: 32 байта (`Pubkey`).
|
||||
- Подпись: 64 байта (Ed25519).
|
||||
- Размер PDA фиксированный: `USER_PDA_SPACE` (сейчас 1024 байта).
|
||||
- `record_len` хранит длину полезной записи от `magic` до `signature` включительно (без `padding`).
|
||||
На первом этапе поддерживается один пользовательский блокчейн SHiNE, но формат блока блокчейна сразу допускает повторение таких блоков в будущем.
|
||||
|
||||
## 3) Единый список полей в порядке хранения
|
||||
## 2. Адрес PDA
|
||||
|
||||
1. `magic`
|
||||
Размер: 5 байт.
|
||||
Значение: `"SHiNE"`.
|
||||
Назначение: маркер формата записи.
|
||||
Адрес пользовательской PDA вычисляется по логину:
|
||||
|
||||
2. `format_major`
|
||||
Размер: 1 байт (`u8`).
|
||||
Текущее значение: `1`.
|
||||
Назначение: major-версия формата.
|
||||
- seed prefix: `login=`;
|
||||
- второй seed: нормализованный логин в нижнем регистре;
|
||||
- program id: программа `shine_users`.
|
||||
|
||||
3. `format_minor`
|
||||
Размер: 1 байт (`u8`).
|
||||
Текущее значение: `0`.
|
||||
Назначение: minor-версия формата.
|
||||
Один логин соответствует одной `user_pda`.
|
||||
|
||||
4. `record_len`
|
||||
Размер: 2 байта (`u16`, LE).
|
||||
Назначение: длина полезных данных записи (без `padding`).
|
||||
## 3. Общие правила кодирования
|
||||
|
||||
5. `created_at_ms`
|
||||
Размер: 8 байт (`u64`, LE).
|
||||
Назначение: время создания записи (Unix time, ms).
|
||||
- Числа кодируются в Little Endian.
|
||||
- `u8`, `u16`, `u32`, `u64` имеют обычный фиксированный размер.
|
||||
- Публичный ключ Solana/Ed25519: 32 байта.
|
||||
- Ed25519-подпись: 64 байта.
|
||||
- SHA-256/Solana hash: 32 байта.
|
||||
- Строка переменной длины: `len: u8` + `bytes[len]` в UTF-8.
|
||||
- Arweave `tx_id`: строка переменной длины. Ожидаемая практическая длина base64url tx id - 43 байта, но формат хранит длину явно.
|
||||
- Все типизированные блоки после фиксированного заголовка начинаются с `block_type: u8` и `block_version: u8`.
|
||||
- Отдельный `block_len` у типизированных блоков не хранится: блоки парсятся по известным полям, счетчикам и строкам с `len: u8`.
|
||||
|
||||
6. `updated_at_ms`
|
||||
Размер: 8 байт (`u64`, LE).
|
||||
Назначение: время последнего обновления записи (Unix time, ms).
|
||||
## 4. Верхний формат записи
|
||||
|
||||
7. `record_number` (`version`)
|
||||
Размер: 4 байта (`u32`, LE).
|
||||
Назначение: порядковый номер записи пользователя.
|
||||
Правило обновления: новая запись должна иметь `last_record_number + 1`; проверяется программой.
|
||||
Первые 9 полей фиксированы и идут строго в указанном порядке. Это общий заголовок записи.
|
||||
|
||||
8. `prev_record_hash` (`prev_hash`)
|
||||
Размер: 32 байта.
|
||||
Назначение: хэш unsigned-части предыдущей записи для связи истории.
|
||||
| N | Поле | Тип | Размер | Правило |
|
||||
|---|------|-----|--------|---------|
|
||||
| 1 | `magic` | bytes | 5 | Всегда `SHiNE`. |
|
||||
| 2 | `format_major` | `u8` | 1 | Для нового формата: `2`. |
|
||||
| 3 | `format_minor` | `u8` | 1 | Для первой версии нового формата: `0`. |
|
||||
| 4 | `record_len` | `u16` | 2 | Длина полезной записи от `magic` до `signature` включительно, без padding. |
|
||||
| 5 | `created_at_ms` | `u64` | 8 | Время создания записи, Unix time в миллисекундах. Не меняется. |
|
||||
| 6 | `updated_at_ms` | `u64` | 8 | Время последнего обновления записи. |
|
||||
| 7 | `record_number` | `u32` | 4 | Номер версии записи пользователя. При создании `0`, при обновлении +1. |
|
||||
| 8 | `prev_record_hash` | bytes | 32 | Хэш unsigned-части предыдущей записи. При создании 32 нулевых байта. |
|
||||
| 9 | `login` | string | `1 + len` | Логин пользователя. Не меняется. |
|
||||
|
||||
9. `login_len`
|
||||
Размер: 1 байт (`u8`).
|
||||
Назначение: длина поля `login` в байтах.
|
||||
После первых 9 полей идет набор типизированных блоков:
|
||||
|
||||
10. `login`
|
||||
Размер: `login_len` байт (UTF-8).
|
||||
Назначение: логин пользователя.
|
||||
Текущие ограничения: от 1 до 25 символов, только `a-z`, `0-9`, `_`.
|
||||
```text
|
||||
UserPdaRecordV2
|
||||
- fixed_header: поля 1..9
|
||||
- blocks_count: u8
|
||||
- blocks: TypedBlock[blocks_count]
|
||||
- signature: [u8; 64]
|
||||
- padding: bytes до размера PDA, если нужен
|
||||
```
|
||||
|
||||
11. `root_key_status`
|
||||
Размер: 1 байт (`u8`).
|
||||
Текущее значение: `0`.
|
||||
Назначение: статус `root_key`.
|
||||
Комментарий: будущие статусы ротации зарезервированы, смена root-ключа пока не реализована.
|
||||
`blocks_count` входит в unsigned-часть записи и подписывается.
|
||||
|
||||
12. `root_key`
|
||||
Размер: 32 байта (`Pubkey`).
|
||||
Назначение: корневой ключ пользователя для подписи записи.
|
||||
## 5. Типы блоков
|
||||
|
||||
13. `blockchain_key_status`
|
||||
Размер: 1 байт (`u8`).
|
||||
Текущее значение: `0`.
|
||||
Назначение: статус `blockchain_key`.
|
||||
Комментарий: будущие статусы ротации зарезервированы.
|
||||
Зарезервированные значения `block_type`:
|
||||
|
||||
14. `blockchain_key`
|
||||
Размер: 32 байта (`Pubkey`).
|
||||
Назначение: рабочий блокчейн-ключ пользователя.
|
||||
| block_type | Блок | Назначение |
|
||||
|------------|------|------------|
|
||||
| `1` | `RootKeyBlock` | Корневой ключ пользователя. |
|
||||
| `2` | `DeviceKeyBlock` | Ключ устройства пользователя. |
|
||||
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
|
||||
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
|
||||
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
||||
| `50` | `TrustedStateBlock` | Счетчик trusted-связей. |
|
||||
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
|
||||
|
||||
15. `device_key_status`
|
||||
Размер: 1 байт (`u8`).
|
||||
Текущее значение: `0`.
|
||||
Назначение: статус `device_key`.
|
||||
Комментарий: будущие статусы ротации зарезервированы.
|
||||
Правила:
|
||||
|
||||
16. `device_key`
|
||||
Размер: 32 байта (`Pubkey`).
|
||||
Назначение: ключ устройства пользователя.
|
||||
- неизвестный `block_type` в `format_major = 2` считается ошибкой;
|
||||
- обязательные блоки: `RootKeyBlock`, `DeviceKeyBlock`, `BlockchainRegistryBlock`;
|
||||
- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `TrustedStateBlock`;
|
||||
- каждый обязательный блок должен встречаться ровно один раз;
|
||||
- порядок блоков в записи фиксируется для простоты проверки:
|
||||
`RootKey`, `DeviceKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `TrustedState`.
|
||||
|
||||
17. `chain_number`
|
||||
Размер: 2 байта (`u16`, LE).
|
||||
Назначение: номер блокчейн-профиля пользователя.
|
||||
Текущее использование: базовый сценарий с одним профилем (обычно `1`).
|
||||
## 6. RootKeyBlock
|
||||
|
||||
18. `balance`
|
||||
Размер: 8 байт (`u64`, LE).
|
||||
Назначение: лимит/баланс пользователя.
|
||||
Смена `root_key` пока не проектируется и не реализуется. Блок фиксирует только стадию `0`.
|
||||
|
||||
19. `is_server`
|
||||
Размер: 1 байт (`u8`).
|
||||
Значения: `0` или `1`.
|
||||
Назначение: флаг серверного профиля.
|
||||
```text
|
||||
RootKeyBlock
|
||||
- block_type: u8 = 1
|
||||
- block_version: u8 = 0
|
||||
- root_key: [u8; 32]
|
||||
```
|
||||
|
||||
20. `server_key` (только если `is_server = 1`)
|
||||
Размер: 32 байта (`Pubkey`).
|
||||
Назначение: публичный ключ сервера.
|
||||
Правила:
|
||||
|
||||
21. `server_address_len` (только если `is_server = 1`)
|
||||
Размер: 1 байт (`u8`).
|
||||
Назначение: длина строки `server_address`.
|
||||
- при создании задается корневой публичный ключ пользователя;
|
||||
- при обновлении `root_key` должен совпадать с предыдущей записью;
|
||||
- ротация root-key будет отдельным форматом/сценарием в будущем.
|
||||
|
||||
22. `server_address` (только если `is_server = 1`)
|
||||
Размер: `server_address_len` байт (UTF-8).
|
||||
Назначение: адрес сервера.
|
||||
## 7. DeviceKeyBlock
|
||||
|
||||
23. `sync_servers_count` (только если `is_server = 1`)
|
||||
Размер: 1 байт (`u8`).
|
||||
Назначение: количество серверов, с которыми сервер синхронизирует данные.
|
||||
Ограничение: максимум `32`.
|
||||
Смена `device_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один ключ устройства.
|
||||
|
||||
24. Повтор `sync_servers_count` раз (только если `is_server = 1`):
|
||||
`server_login_len` — 1 байт (`u8`),
|
||||
`server_login` — `server_login_len` байт (UTF-8).
|
||||
Назначение: логины серверов синхронизации.
|
||||
```text
|
||||
DeviceKeyBlock
|
||||
- block_type: u8 = 2
|
||||
- block_version: u8 = 0
|
||||
- device_key: [u8; 32]
|
||||
```
|
||||
|
||||
25. `access_servers_count`
|
||||
Размер: 1 байт (`u8`).
|
||||
Назначение: количество серверов доступа (relay), через которые можно достучаться до пользователя.
|
||||
Правила:
|
||||
|
||||
26. Повтор `access_servers_count` раз:
|
||||
`server_login_len` — 1 байт (`u8`),
|
||||
`server_login` — `server_login_len` байт (UTF-8).
|
||||
Назначение: логины серверов доступа.
|
||||
- при создании задается текущий публичный ключ устройства;
|
||||
- при обновлении ключ устройства может быть обновлен только если это отдельно разрешено бизнес-логикой инструкции;
|
||||
- история устройств и несколько устройств в этом формате не хранятся.
|
||||
|
||||
27. `trusted_count`
|
||||
Размер: 1 байт (`u8`).
|
||||
Назначение: текущее число trusted-контактов.
|
||||
Текущее состояние: пока только счетчик, без отдельной trusted-логики.
|
||||
## 8. BlockchainRegistryBlock
|
||||
|
||||
28. `reserved`
|
||||
Размер: 5 байт.
|
||||
Текущее значение: `0x00 0x00 0x00 0x00 0x00`.
|
||||
Назначение: резерв под будущие расширения.
|
||||
Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список.
|
||||
|
||||
29. `signature`
|
||||
Размер: 64 байта.
|
||||
Назначение: Ed25519-подпись хэша unsigned-части записи.
|
||||
```text
|
||||
BlockchainRegistryBlock
|
||||
- block_type: u8 = 3
|
||||
- block_version: u8 = 0
|
||||
- blockchain_count: u8
|
||||
- blockchain_records: BlockchainRecord[blockchain_count]
|
||||
```
|
||||
|
||||
30. `padding`
|
||||
Размер: до полного `USER_PDA_SPACE`.
|
||||
Текущее значение: `0x00`.
|
||||
Назначение: добивка до фиксированного размера PDA.
|
||||
Правила:
|
||||
|
||||
## 4) Что подписывается
|
||||
- на первом этапе `blockchain_count = 1`;
|
||||
- в будущем можно увеличить количество записей без изменения смысла `BlockchainRecord`;
|
||||
- каждый `BlockchainRecord` описывает один пользовательский SHiNE-блокчейн.
|
||||
|
||||
Подписывается SHA-256 от unsigned-части записи:
|
||||
- от `magic` до `reserved` включительно;
|
||||
- без `signature`;
|
||||
- без `padding`.
|
||||
## 9. BlockchainRecord
|
||||
|
||||
## 5) Что сейчас работает в логике
|
||||
```text
|
||||
BlockchainRecord
|
||||
- blockchain_type: u8
|
||||
- blockchain_name: string
|
||||
- blockchain_public_key: [u8; 32]
|
||||
- paid_limit_bytes: u64
|
||||
- used_bytes: u64
|
||||
- last_block_number: u32
|
||||
- last_block_hash: [u8; 32]
|
||||
- last_block_signature: [u8; 64]
|
||||
- arweave_present: u8
|
||||
- arweave_tx_id: string, только если arweave_present = 1
|
||||
```
|
||||
|
||||
Сейчас в рабочем потоке используются 2 операции:
|
||||
1. `create_user_pda` — регистрация пользователя.
|
||||
2. `update_user_pda` — обновление записи пользователя.
|
||||
`blockchain_type`:
|
||||
|
||||
Через `update_user_pda` сейчас можно:
|
||||
- увеличить `balance` через `additional_limit`;
|
||||
- обновить серверные поля (`is_server`, `server_key`, `server_address`, `sync_servers`, `access_servers`);
|
||||
- увеличить `record_number` (`version`) на 1.
|
||||
| Значение | Смысл |
|
||||
|----------|-------|
|
||||
| `1` | Основной пользовательский SHiNE-блокчейн. |
|
||||
|
||||
Оплата идет на адрес, заданный в `REGISTRATION_FEE_RECEIVER` (не в DAO по умолчанию).
|
||||
Поля:
|
||||
|
||||
## 6) Ограничения и отложенные расширения
|
||||
- `blockchain_name` - строковое имя пользовательского блокчейна, например `login-001`. На первом этапе для основного блокчейна пользователя используется имя вида `<login>-001`, потому что это первый блокчейн этого пользователя.
|
||||
- `blockchain_public_key` - публичный ключ блокчейна пользователя.
|
||||
- `paid_limit_bytes` - оплаченный лимит хранения/записей в байтах.
|
||||
- `used_bytes` - сколько байт уже занято в пользовательском SHiNE-блокчейне.
|
||||
- `last_block_number` - номер последнего известного блока пользовательского блокчейна.
|
||||
- `last_block_hash` - хэш последнего известного блока.
|
||||
- `last_block_signature` - подпись хэша специального сообщения о вершине блокчейна ключом `blockchain_public_key`.
|
||||
- `arweave_present` - `0`, если ссылки нет; `1`, если ссылка есть.
|
||||
- `arweave_tx_id` - Arweave transaction id, где лежит выгруженный пользовательский канал/состояние.
|
||||
|
||||
Это функции и сценарии, которые предусмотрены структурой данных формата `v1.0`, но пока не реализованы программно.
|
||||
Arweave `tx_id` - обычное поле внутри записи конкретного блокчейна. Solana-программа не проверяет, что такой Arweave transaction действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя.
|
||||
|
||||
1. Смена ключей пока недоступна
|
||||
`root_key`, `blockchain_key`, `device_key` считаются без ротации; статусные поля пока фактически только `0`.
|
||||
## 10. Правила обновления BlockchainRecord
|
||||
|
||||
2. Multi-chain профили пока не реализованы
|
||||
Пока используется один базовый профиль (`chain_number`), расширение до нескольких профилей/форков — отдельный этап.
|
||||
При обновлении записи:
|
||||
|
||||
3. Trusted-логика пока не реализована
|
||||
Пока хранится только `trusted_count`; список trusted, очередь, таймеры и голосование будут добавляться отдельно.
|
||||
- `blockchain_type` для существующей записи не меняется;
|
||||
- `blockchain_public_key` пока не ротируется автоматически; смена ключа требует отдельного согласованного сценария;
|
||||
- `paid_limit_bytes` может только увеличиваться или оставаться прежним;
|
||||
- при увеличении `paid_limit_bytes` пользователь платит комиссию в Solana по тарифам программы;
|
||||
- `used_bytes` может только увеличиваться или оставаться прежним;
|
||||
- `last_block_number` может только увеличиваться или оставаться прежним;
|
||||
- `used_bytes <= paid_limit_bytes`;
|
||||
- если `last_block_number` увеличился, то должны быть переданы новый `last_block_hash` и новая `last_block_signature`;
|
||||
- `last_block_signature` проверяется через Ed25519-инструкцию Solana: подпись должна соответствовать хэшу сообщения `LastBlockState` и `blockchain_public_key`;
|
||||
- `arweave_tx_id` можно добавить или заменить на новый, если пользователь выгрузил более актуальное состояние в Arweave;
|
||||
- уменьшать лимит, число блоков или занятый размер нельзя.
|
||||
|
||||
4. Работа с несколькими серверами на уровне приложения ограничена
|
||||
В записи можно хранить `sync_servers` и `access_servers`, но фактическая клиентская логика выбора/обхода серверов может быть ограничена.
|
||||
Сообщение `LastBlockState`, которое хэшируется и подписывается ключом `blockchain_public_key`:
|
||||
|
||||
```text
|
||||
LastBlockState
|
||||
- constant: bytes = "SHiNE_LAST_BLOCK"
|
||||
- login: string
|
||||
- blockchain_name: string
|
||||
- last_block_number: u32
|
||||
- last_block_hash: [u8; 32]
|
||||
- used_bytes: u64
|
||||
```
|
||||
|
||||
## 7) Константы и фиксированные значения (точки будущего расширения)
|
||||
Алгоритм:
|
||||
|
||||
Ниже перечислены места, где сейчас используются константы/фиксированные значения, а в будущем возможна доработка:
|
||||
```text
|
||||
message = SHA-256(LastBlockState bytes)
|
||||
last_block_signature = Ed25519(blockchain_public_key, message)
|
||||
```
|
||||
|
||||
1. Версия формата: `format_major = 1`, `format_minor = 0`.
|
||||
Расширение: переход на следующую минорную/мажорную версию при изменении бинарной схемы.
|
||||
Причина проверки подписи `LastBlockState`: `root_key` управляет Solana-записью пользователя, а `blockchain_public_key` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
|
||||
|
||||
2. Размер PDA: `USER_PDA_SPACE = 1024`.
|
||||
Расширение: увеличение размера или переход на иное хранение при росте структуры.
|
||||
## 11. ServerProfileBlock
|
||||
|
||||
3. Статусы ключей: все три `*_key_status` пока равны `0`.
|
||||
Расширение: добавить коды состояний для ротации/восстановления ключей.
|
||||
Блок присутствует, если пользователь выступает сервером.
|
||||
|
||||
4. `chain_number`: текущий рабочий сценарий с одним профилем (обычно `1`).
|
||||
Расширение: поддержка нескольких блокчейн-форков.
|
||||
```text
|
||||
ServerProfileBlock
|
||||
- block_type: u8 = 30
|
||||
- block_version: u8 = 0
|
||||
- is_server: u8
|
||||
- server_key: [u8; 32], только если is_server = 1
|
||||
- server_address: string, только если is_server = 1
|
||||
- sync_servers_count: u8, только если is_server = 1
|
||||
- sync_servers: string[sync_servers_count], только если is_server = 1
|
||||
```
|
||||
|
||||
5. `trusted_count`: пока только счетчик, обычно `0`.
|
||||
Расширение: отдельные структуры trusted-списка, очередей и таймеров.
|
||||
Правила:
|
||||
|
||||
6. `reserved` (5 байт): сейчас всегда нули.
|
||||
Расширение: использовать как флаги/дополнительные поля без слома общей схемы.
|
||||
- `is_server = 0` означает, что серверных данных нет;
|
||||
- `is_server = 1` означает, что пользователь публикует серверный профиль;
|
||||
- `sync_servers_count` максимум `32`;
|
||||
- `server_address` - строковый адрес сервера в формате, который будет отдельно закреплен на уровне приложения;
|
||||
- `sync_servers` - логины пользователей системы, через которых этот сервер пытается синхронизироваться. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
|
||||
|
||||
## 12. AccessServersBlock
|
||||
|
||||
Блок хранит серверы доступа/relay для пользователя.
|
||||
|
||||
```text
|
||||
AccessServersBlock
|
||||
- block_type: u8 = 40
|
||||
- block_version: u8 = 0
|
||||
- access_servers_count: u8
|
||||
- access_servers: string[access_servers_count]
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- блок может отсутствовать, если серверы доступа не заданы;
|
||||
- список может обновляться при изменении маршрутизации пользователя;
|
||||
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
|
||||
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
|
||||
|
||||
## 13. TrustedStateBlock
|
||||
|
||||
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
|
||||
|
||||
```text
|
||||
TrustedStateBlock
|
||||
- block_type: u8 = 50
|
||||
- block_version: u8 = 0
|
||||
- trusted_count: u8 = 0
|
||||
```
|
||||
|
||||
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
|
||||
|
||||
## 14. Подпись user_pda
|
||||
|
||||
Подписывается не вся PDA целиком, а unsigned-часть записи:
|
||||
|
||||
- от `magic` до последнего байта последнего типизированного блока включительно;
|
||||
- включая `record_len`, `blocks_count`, все заголовки блоков и тела блоков;
|
||||
- без поля `signature`;
|
||||
- без padding.
|
||||
|
||||
Алгоритм:
|
||||
|
||||
```text
|
||||
message = hash(unsigned_record_bytes)
|
||||
signature = Ed25519(root_key, message)
|
||||
```
|
||||
|
||||
Solana-программа проверяет подпись через встроенную Ed25519-инструкцию. Подписантом должен быть `root_key` из `RootKeyBlock`.
|
||||
|
||||
Смену формата подписи сейчас не трогаем.
|
||||
|
||||
## 15. Регистрация пользователя
|
||||
|
||||
При регистрации:
|
||||
|
||||
- PDA еще не должна существовать;
|
||||
- логин проходит проверку формата и login guard;
|
||||
- `record_number = 0`;
|
||||
- `prev_record_hash = 0x00...00`;
|
||||
- `created_at_ms = updated_at_ms`;
|
||||
- обязательные блоки присутствуют;
|
||||
- создается минимум один `BlockchainRecord`;
|
||||
- стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит;
|
||||
- `used_bytes <= paid_limit_bytes`;
|
||||
- пользователь платит регистрационную комиссию;
|
||||
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
|
||||
- вся unsigned-часть записи подписана `root_key`.
|
||||
|
||||
## 16. Обновление пользователя
|
||||
|
||||
При обновлении:
|
||||
|
||||
- PDA должна существовать;
|
||||
- `login`, `created_at_ms`, `root_key` не меняются;
|
||||
- `record_number = previous_record_number + 1`;
|
||||
- `prev_record_hash` равен хэшу unsigned-части предыдущей записи;
|
||||
- `updated_at_ms` обновляется;
|
||||
- unsigned-часть новой записи подписана `root_key`;
|
||||
- лимиты блокчейнов могут только увеличиваться;
|
||||
- занятый размер и номер последнего блока не могут уменьшаться;
|
||||
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
|
||||
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
|
||||
|
||||
## 17. Отличия от старого линейного формата
|
||||
|
||||
Старый формат после `login` хранил поля линейно:
|
||||
|
||||
- `root_key_status`;
|
||||
- `root_key`;
|
||||
- `blockchain_key_status`;
|
||||
- `blockchain_key`;
|
||||
- `device_key_status`;
|
||||
- `device_key`;
|
||||
- `chain_number`;
|
||||
- `balance`;
|
||||
- серверные поля;
|
||||
- access-серверы;
|
||||
- `trusted_count`;
|
||||
- `reserved`;
|
||||
- `signature`.
|
||||
|
||||
Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки:
|
||||
|
||||
- ключи становятся отдельными блоками;
|
||||
- данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`;
|
||||
- серверные данные и access-серверы отделяются от данных блокчейна;
|
||||
- расширение формата делается добавлением новых версий блоков или новых `block_type`, а не вставкой полей в середину линейной записи.
|
||||
|
||||
## 18. Что пока не входит в формат
|
||||
|
||||
Пока не проектируем:
|
||||
|
||||
- ротацию `root_key`;
|
||||
- сложную ротацию `device_key`;
|
||||
- ротацию `blockchain_public_key`;
|
||||
- проверку содержимого Arweave transaction;
|
||||
- хранение полной истории пользовательского блокчейна внутри Solana;
|
||||
- подключение Solana-модуля к сборке/деплою основного сервера SHiNE.
|
||||
|
||||
@ -12,19 +12,31 @@ use common::utils::{create_pda, safe_read_pda, write_to_pda, ErrCode};
|
||||
use std::str::FromStr;
|
||||
|
||||
const MAGIC: &[u8; 5] = b"SHiNE";
|
||||
const FORMAT_MAJOR: u8 = 1;
|
||||
const FORMAT_MAJOR: u8 = 2;
|
||||
const FORMAT_MINOR: u8 = 0;
|
||||
const KEY_STATUS_CREATED: u8 = 0;
|
||||
const MAX_SYNC_SERVERS: usize = 32;
|
||||
const MAX_AUTO_REALLOC_INCREASE: usize = 10_000;
|
||||
const RESERVED_BYTES: [u8; 5] = [0, 0, 0, 0, 0];
|
||||
const ZERO_HASH: [u8; 32] = [0; 32];
|
||||
const BLOCK_TYPE_ROOT_KEY: u8 = 1;
|
||||
const BLOCK_TYPE_DEVICE_KEY: u8 = 2;
|
||||
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY: u8 = 3;
|
||||
const BLOCK_TYPE_SERVER_PROFILE: u8 = 30;
|
||||
const BLOCK_TYPE_ACCESS_SERVERS: u8 = 40;
|
||||
const BLOCK_TYPE_TRUSTED_STATE: u8 = 50;
|
||||
const BLOCK_VERSION_0: u8 = 0;
|
||||
const BLOCKCHAIN_TYPE_MAIN_USER: u8 = 1;
|
||||
const LAST_BLOCK_STATE_PREFIX: &[u8] = b"SHiNE_LAST_BLOCK";
|
||||
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||||
pub struct UserMutableFields {
|
||||
pub blockchain_key: Pubkey,
|
||||
pub device_key: Pubkey,
|
||||
pub chain_number: u16,
|
||||
pub blockchain_public_key: Pubkey,
|
||||
pub blockchain_name: String,
|
||||
pub used_bytes: u64,
|
||||
pub last_block_number: u32,
|
||||
pub last_block_hash: Vec<u8>,
|
||||
pub last_block_signature: Vec<u8>,
|
||||
pub arweave_tx_id: String,
|
||||
pub is_server: bool,
|
||||
pub server_key: Pubkey,
|
||||
pub server_address: String,
|
||||
@ -59,17 +71,12 @@ pub struct UpdateUserPdaArgs {
|
||||
pub struct UserRecord {
|
||||
pub created_at_ms: u64,
|
||||
pub updated_at_ms: u64,
|
||||
pub version: u32,
|
||||
pub prev_hash: [u8; 32],
|
||||
pub record_number: u32,
|
||||
pub prev_record_hash: [u8; 32],
|
||||
pub login: String,
|
||||
pub root_key_status: u8,
|
||||
pub root_key: Pubkey,
|
||||
pub blockchain_key_status: u8,
|
||||
pub blockchain_key: Pubkey,
|
||||
pub device_key_status: u8,
|
||||
pub device_key: Pubkey,
|
||||
pub chain_number: u16,
|
||||
pub balance: u64,
|
||||
pub blockchain: BlockchainRecord,
|
||||
pub is_server: bool,
|
||||
pub server_key: Pubkey,
|
||||
pub server_address: String,
|
||||
@ -79,6 +86,18 @@ pub struct UserRecord {
|
||||
pub signature: [u8; 64],
|
||||
}
|
||||
|
||||
pub struct BlockchainRecord {
|
||||
pub blockchain_type: u8,
|
||||
pub blockchain_name: String,
|
||||
pub blockchain_public_key: Pubkey,
|
||||
pub paid_limit_bytes: u64,
|
||||
pub used_bytes: u64,
|
||||
pub last_block_number: u32,
|
||||
pub last_block_hash: [u8; 32],
|
||||
pub last_block_signature: [u8; 64],
|
||||
pub arweave_tx_id: String,
|
||||
}
|
||||
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||||
pub struct UsersEconomyConfigState {
|
||||
pub version: u8,
|
||||
@ -192,7 +211,11 @@ pub fn update_users_economy_config(
|
||||
) -> Result<()> {
|
||||
let dao_authority =
|
||||
Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| error!(ErrCode::InvalidSigner))?;
|
||||
require_keys_eq!(dao_authority, ctx.accounts.signer.key(), ErrCode::InvalidSigner);
|
||||
require_keys_eq!(
|
||||
dao_authority,
|
||||
ctx.accounts.signer.key(),
|
||||
ErrCode::InvalidSigner
|
||||
);
|
||||
|
||||
let (expected_pda, _) = find_users_economy_config_pda(ctx.program_id);
|
||||
require_keys_eq!(
|
||||
@ -258,17 +281,22 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
|
||||
let mut record = UserRecord {
|
||||
created_at_ms: args.created_at_ms,
|
||||
updated_at_ms: args.created_at_ms,
|
||||
version: 0,
|
||||
prev_hash: ZERO_HASH,
|
||||
record_number: 0,
|
||||
prev_record_hash: ZERO_HASH,
|
||||
login: args.login.clone(),
|
||||
root_key_status: KEY_STATUS_CREATED,
|
||||
root_key: args.root_key,
|
||||
blockchain_key_status: KEY_STATUS_CREATED,
|
||||
blockchain_key: args.fields.blockchain_key,
|
||||
device_key_status: KEY_STATUS_CREATED,
|
||||
device_key: args.fields.device_key,
|
||||
chain_number: args.fields.chain_number,
|
||||
balance: start_balance,
|
||||
blockchain: BlockchainRecord {
|
||||
blockchain_type: BLOCKCHAIN_TYPE_MAIN_USER,
|
||||
blockchain_name: args.fields.blockchain_name.clone(),
|
||||
blockchain_public_key: args.fields.blockchain_public_key,
|
||||
paid_limit_bytes: start_balance,
|
||||
used_bytes: args.fields.used_bytes,
|
||||
last_block_number: args.fields.last_block_number,
|
||||
last_block_hash: vec_to_hash32(&args.fields.last_block_hash)?,
|
||||
last_block_signature: vec_to_signature(&args.fields.last_block_signature)?,
|
||||
arweave_tx_id: args.fields.arweave_tx_id.clone(),
|
||||
},
|
||||
is_server: args.fields.is_server,
|
||||
server_key: args.fields.server_key,
|
||||
server_address: args.fields.server_address.clone(),
|
||||
@ -277,6 +305,8 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
|
||||
trusted_count: args.fields.trusted_count,
|
||||
signature: [0; 64],
|
||||
};
|
||||
validate_blockchain_limits(&record.blockchain, 0, 0, true)?;
|
||||
verify_last_block_state_signature(&ctx.accounts.instructions, &record)?;
|
||||
|
||||
let unsigned = serialize_unsigned_record(&record)?;
|
||||
record.signature = verify_record_signature(
|
||||
@ -310,7 +340,10 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
|
||||
|
||||
let total_fee = economy
|
||||
.registration_fee_lamports
|
||||
.checked_add(limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?)
|
||||
.checked_add(limit_fee_lamports(
|
||||
args.additional_limit,
|
||||
economy.lamports_per_limit_step,
|
||||
)?)
|
||||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||
transfer_lamports(
|
||||
&ctx.accounts.signer,
|
||||
@ -391,13 +424,7 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
|
||||
ErrCode::ImmutableFieldChanged
|
||||
);
|
||||
require!(
|
||||
old_record.root_key_status == KEY_STATUS_CREATED
|
||||
&& old_record.blockchain_key_status == KEY_STATUS_CREATED
|
||||
&& old_record.device_key_status == KEY_STATUS_CREATED,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
require!(
|
||||
args.version == old_record.version.saturating_add(1),
|
||||
args.version == old_record.record_number.saturating_add(1),
|
||||
ErrCode::InvalidVersion
|
||||
);
|
||||
|
||||
@ -409,25 +436,34 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
|
||||
);
|
||||
|
||||
let new_balance = old_record
|
||||
.balance
|
||||
.blockchain
|
||||
.paid_limit_bytes
|
||||
.checked_add(args.additional_limit)
|
||||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||
require!(new_balance >= old_record.balance, ErrCode::BalanceDecrease);
|
||||
require!(
|
||||
new_balance >= old_record.blockchain.paid_limit_bytes,
|
||||
ErrCode::BalanceDecrease
|
||||
);
|
||||
|
||||
let mut new_record = UserRecord {
|
||||
created_at_ms: old_record.created_at_ms,
|
||||
updated_at_ms: args.updated_at_ms,
|
||||
version: args.version,
|
||||
prev_hash: provided_prev_hash,
|
||||
record_number: args.version,
|
||||
prev_record_hash: provided_prev_hash,
|
||||
login: old_record.login.clone(),
|
||||
root_key_status: old_record.root_key_status,
|
||||
root_key: old_record.root_key,
|
||||
blockchain_key_status: old_record.blockchain_key_status,
|
||||
blockchain_key: args.fields.blockchain_key,
|
||||
device_key_status: old_record.device_key_status,
|
||||
device_key: args.fields.device_key,
|
||||
chain_number: args.fields.chain_number,
|
||||
balance: new_balance,
|
||||
blockchain: BlockchainRecord {
|
||||
blockchain_type: old_record.blockchain.blockchain_type,
|
||||
blockchain_name: args.fields.blockchain_name.clone(),
|
||||
blockchain_public_key: args.fields.blockchain_public_key,
|
||||
paid_limit_bytes: new_balance,
|
||||
used_bytes: args.fields.used_bytes,
|
||||
last_block_number: args.fields.last_block_number,
|
||||
last_block_hash: vec_to_hash32(&args.fields.last_block_hash)?,
|
||||
last_block_signature: vec_to_signature(&args.fields.last_block_signature)?,
|
||||
arweave_tx_id: args.fields.arweave_tx_id.clone(),
|
||||
},
|
||||
is_server: args.fields.is_server,
|
||||
server_key: args.fields.server_key,
|
||||
server_address: args.fields.server_address.clone(),
|
||||
@ -436,6 +472,20 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
|
||||
trusted_count: args.fields.trusted_count,
|
||||
signature: [0; 64],
|
||||
};
|
||||
require!(
|
||||
new_record.blockchain.blockchain_type == old_record.blockchain.blockchain_type
|
||||
&& new_record.blockchain.blockchain_name == old_record.blockchain.blockchain_name
|
||||
&& new_record.blockchain.blockchain_public_key
|
||||
== old_record.blockchain.blockchain_public_key,
|
||||
ErrCode::ImmutableFieldChanged
|
||||
);
|
||||
validate_blockchain_limits(
|
||||
&new_record.blockchain,
|
||||
old_record.blockchain.used_bytes,
|
||||
old_record.blockchain.last_block_number,
|
||||
false,
|
||||
)?;
|
||||
verify_last_block_state_signature(&ctx.accounts.instructions, &new_record)?;
|
||||
|
||||
let unsigned = serialize_unsigned_record(&new_record)?;
|
||||
new_record.signature = verify_record_signature(
|
||||
@ -471,13 +521,6 @@ fn serialize_unsigned_record(record: &UserRecord) -> Result<Vec<u8>> {
|
||||
let login_bytes = record.login.as_bytes();
|
||||
require!(login_bytes.len() <= u8::MAX as usize, ErrCode::InvalidLogin);
|
||||
|
||||
let server_address_bytes = record.server_address.as_bytes();
|
||||
require!(
|
||||
server_address_bytes.len() <= u8::MAX as usize,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
require!(record.access_servers.len() <= u8::MAX as usize, ErrCode::InvalidRecordData);
|
||||
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(MAGIC);
|
||||
out.push(FORMAT_MAJOR);
|
||||
@ -486,50 +529,22 @@ fn serialize_unsigned_record(record: &UserRecord) -> Result<Vec<u8>> {
|
||||
|
||||
out.extend_from_slice(&record.created_at_ms.to_le_bytes());
|
||||
out.extend_from_slice(&record.updated_at_ms.to_le_bytes());
|
||||
out.extend_from_slice(&record.version.to_le_bytes());
|
||||
out.extend_from_slice(&record.prev_hash);
|
||||
out.extend_from_slice(&record.record_number.to_le_bytes());
|
||||
out.extend_from_slice(&record.prev_record_hash);
|
||||
|
||||
out.push(login_bytes.len() as u8);
|
||||
out.extend_from_slice(login_bytes);
|
||||
|
||||
out.push(record.root_key_status);
|
||||
out.extend_from_slice(record.root_key.as_ref());
|
||||
out.push(record.blockchain_key_status);
|
||||
out.extend_from_slice(record.blockchain_key.as_ref());
|
||||
out.push(record.device_key_status);
|
||||
out.extend_from_slice(record.device_key.as_ref());
|
||||
|
||||
out.extend_from_slice(&record.chain_number.to_le_bytes());
|
||||
out.extend_from_slice(&record.balance.to_le_bytes());
|
||||
|
||||
out.push(if record.is_server { 1 } else { 0 });
|
||||
let blocks_count = if record.is_server { 6 } else { 5 };
|
||||
out.push(blocks_count);
|
||||
write_root_key_block(&mut out, record);
|
||||
write_device_key_block(&mut out, record);
|
||||
write_blockchain_registry_block(&mut out, &record.blockchain)?;
|
||||
if record.is_server {
|
||||
out.extend_from_slice(record.server_key.as_ref());
|
||||
out.push(server_address_bytes.len() as u8);
|
||||
out.extend_from_slice(server_address_bytes);
|
||||
require!(
|
||||
record.sync_servers.len() <= MAX_SYNC_SERVERS,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
out.push(record.sync_servers.len() as u8);
|
||||
for login in &record.sync_servers {
|
||||
let bytes = login.as_bytes();
|
||||
require!(bytes.len() <= u8::MAX as usize, ErrCode::InvalidRecordData);
|
||||
out.push(bytes.len() as u8);
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
write_server_profile_block(&mut out, record)?;
|
||||
}
|
||||
|
||||
out.push(record.access_servers.len() as u8);
|
||||
for login in &record.access_servers {
|
||||
let bytes = login.as_bytes();
|
||||
require!(bytes.len() <= u8::MAX as usize, ErrCode::InvalidRecordData);
|
||||
out.push(bytes.len() as u8);
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
out.push(record.trusted_count);
|
||||
out.extend_from_slice(&RESERVED_BYTES);
|
||||
write_access_servers_block(&mut out, record)?;
|
||||
write_trusted_state_block(&mut out, record);
|
||||
|
||||
let record_len = out
|
||||
.len()
|
||||
@ -549,6 +564,121 @@ fn serialize_full_record(record: &UserRecord) -> Result<Vec<u8>> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn write_root_key_block(out: &mut Vec<u8>, record: &UserRecord) {
|
||||
out.push(BLOCK_TYPE_ROOT_KEY);
|
||||
out.push(BLOCK_VERSION_0);
|
||||
out.extend_from_slice(record.root_key.as_ref());
|
||||
}
|
||||
|
||||
fn write_device_key_block(out: &mut Vec<u8>, record: &UserRecord) {
|
||||
out.push(BLOCK_TYPE_DEVICE_KEY);
|
||||
out.push(BLOCK_VERSION_0);
|
||||
out.extend_from_slice(record.device_key.as_ref());
|
||||
}
|
||||
|
||||
fn write_blockchain_registry_block(out: &mut Vec<u8>, blockchain: &BlockchainRecord) -> Result<()> {
|
||||
out.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY);
|
||||
out.push(BLOCK_VERSION_0);
|
||||
out.push(1);
|
||||
write_blockchain_record(out, blockchain)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_blockchain_record(out: &mut Vec<u8>, blockchain: &BlockchainRecord) -> Result<()> {
|
||||
out.push(blockchain.blockchain_type);
|
||||
write_len_prefixed_string(out, &blockchain.blockchain_name)?;
|
||||
out.extend_from_slice(blockchain.blockchain_public_key.as_ref());
|
||||
out.extend_from_slice(&blockchain.paid_limit_bytes.to_le_bytes());
|
||||
out.extend_from_slice(&blockchain.used_bytes.to_le_bytes());
|
||||
out.extend_from_slice(&blockchain.last_block_number.to_le_bytes());
|
||||
out.extend_from_slice(&blockchain.last_block_hash);
|
||||
out.extend_from_slice(&blockchain.last_block_signature);
|
||||
if blockchain.arweave_tx_id.is_empty() {
|
||||
out.push(0);
|
||||
} else {
|
||||
out.push(1);
|
||||
write_len_prefixed_string(out, &blockchain.arweave_tx_id)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_server_profile_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<()> {
|
||||
out.push(BLOCK_TYPE_SERVER_PROFILE);
|
||||
out.push(BLOCK_VERSION_0);
|
||||
out.push(1);
|
||||
out.extend_from_slice(record.server_key.as_ref());
|
||||
write_len_prefixed_string(out, &record.server_address)?;
|
||||
require!(
|
||||
record.sync_servers.len() <= MAX_SYNC_SERVERS,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
out.push(record.sync_servers.len() as u8);
|
||||
for login in &record.sync_servers {
|
||||
write_len_prefixed_string(out, login)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_access_servers_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<()> {
|
||||
out.push(BLOCK_TYPE_ACCESS_SERVERS);
|
||||
out.push(BLOCK_VERSION_0);
|
||||
require!(
|
||||
record.access_servers.len() <= u8::MAX as usize,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
out.push(record.access_servers.len() as u8);
|
||||
for login in &record.access_servers {
|
||||
write_len_prefixed_string(out, login)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_trusted_state_block(out: &mut Vec<u8>, record: &UserRecord) {
|
||||
out.push(BLOCK_TYPE_TRUSTED_STATE);
|
||||
out.push(BLOCK_VERSION_0);
|
||||
out.push(record.trusted_count);
|
||||
}
|
||||
|
||||
fn write_len_prefixed_string(out: &mut Vec<u8>, value: &str) -> Result<()> {
|
||||
let bytes = value.as_bytes();
|
||||
require!(bytes.len() <= u8::MAX as usize, ErrCode::InvalidRecordData);
|
||||
out.push(bytes.len() as u8);
|
||||
out.extend_from_slice(bytes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_blockchain_record(data: &[u8], cursor: &mut usize) -> Result<BlockchainRecord> {
|
||||
let blockchain_type = read_u8(data, cursor)?;
|
||||
require!(
|
||||
blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
let blockchain_name = read_len_prefixed_string(data, cursor)?;
|
||||
let blockchain_public_key = Pubkey::new_from_array(read_fixed_32(data, cursor)?);
|
||||
let paid_limit_bytes = read_u64(data, cursor)?;
|
||||
let used_bytes = read_u64(data, cursor)?;
|
||||
let last_block_number = read_u32(data, cursor)?;
|
||||
let last_block_hash = read_fixed_32(data, cursor)?;
|
||||
let last_block_signature = read_fixed_64(data, cursor)?;
|
||||
let arweave_present = read_u8(data, cursor)?;
|
||||
let arweave_tx_id = match arweave_present {
|
||||
0 => String::new(),
|
||||
1 => read_len_prefixed_string(data, cursor)?,
|
||||
_ => return Err(error!(ErrCode::InvalidRecordData)),
|
||||
};
|
||||
Ok(BlockchainRecord {
|
||||
blockchain_type,
|
||||
blockchain_name,
|
||||
blockchain_public_key,
|
||||
paid_limit_bytes,
|
||||
used_bytes,
|
||||
last_block_number,
|
||||
last_block_hash,
|
||||
last_block_signature,
|
||||
arweave_tx_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
|
||||
require!(raw.len() >= 9, ErrCode::InvalidRecordData);
|
||||
require!(&raw[0..5] == MAGIC, ErrCode::InvalidRecordMagic);
|
||||
@ -566,72 +696,81 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
|
||||
|
||||
let created_at_ms = read_u64(useful, &mut cursor)?;
|
||||
let updated_at_ms = read_u64(useful, &mut cursor)?;
|
||||
let version = read_u32(useful, &mut cursor)?;
|
||||
let prev_hash = read_fixed_32(useful, &mut cursor)?;
|
||||
let record_number = read_u32(useful, &mut cursor)?;
|
||||
let prev_record_hash = read_fixed_32(useful, &mut cursor)?;
|
||||
let login = read_len_prefixed_string(useful, &mut cursor)?;
|
||||
|
||||
let root_key_status = read_u8(useful, &mut cursor)?;
|
||||
let root_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?);
|
||||
let blockchain_key_status = read_u8(useful, &mut cursor)?;
|
||||
let blockchain_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?);
|
||||
let device_key_status = read_u8(useful, &mut cursor)?;
|
||||
let device_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?);
|
||||
let blocks_count = read_u8(useful, &mut cursor)? as usize;
|
||||
let mut root_key: Option<Pubkey> = None;
|
||||
let mut device_key: Option<Pubkey> = None;
|
||||
let mut blockchain: Option<BlockchainRecord> = None;
|
||||
let mut is_server = false;
|
||||
let mut server_key = Pubkey::default();
|
||||
let mut server_address = String::new();
|
||||
let mut sync_servers = Vec::new();
|
||||
let mut access_servers = Vec::new();
|
||||
let mut trusted_count = 0u8;
|
||||
|
||||
let chain_number = read_u16(useful, &mut cursor)?;
|
||||
let balance = read_u64(useful, &mut cursor)?;
|
||||
|
||||
let is_server = read_u8(useful, &mut cursor)? == 1;
|
||||
let (server_key, server_address) = if is_server {
|
||||
(
|
||||
Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?),
|
||||
read_len_prefixed_string(useful, &mut cursor)?,
|
||||
)
|
||||
} else {
|
||||
(Pubkey::default(), String::new())
|
||||
};
|
||||
|
||||
let sync_servers = if is_server {
|
||||
let sync_count = read_u8(useful, &mut cursor)? as usize;
|
||||
require!(sync_count <= MAX_SYNC_SERVERS, ErrCode::InvalidRecordData);
|
||||
let mut out = Vec::with_capacity(sync_count);
|
||||
for _ in 0..sync_count {
|
||||
out.push(read_len_prefixed_string(useful, &mut cursor)?);
|
||||
for _ in 0..blocks_count {
|
||||
let block_type = read_u8(useful, &mut cursor)?;
|
||||
let block_version = read_u8(useful, &mut cursor)?;
|
||||
require!(
|
||||
block_version == BLOCK_VERSION_0,
|
||||
ErrCode::InvalidRecordFormat
|
||||
);
|
||||
match block_type {
|
||||
BLOCK_TYPE_ROOT_KEY => {
|
||||
require!(root_key.is_none(), ErrCode::InvalidRecordData);
|
||||
root_key = Some(Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?));
|
||||
}
|
||||
BLOCK_TYPE_DEVICE_KEY => {
|
||||
require!(device_key.is_none(), ErrCode::InvalidRecordData);
|
||||
device_key = Some(Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?));
|
||||
}
|
||||
BLOCK_TYPE_BLOCKCHAIN_REGISTRY => {
|
||||
require!(blockchain.is_none(), ErrCode::InvalidRecordData);
|
||||
let count = read_u8(useful, &mut cursor)?;
|
||||
require!(count == 1, ErrCode::InvalidRecordData);
|
||||
blockchain = Some(read_blockchain_record(useful, &mut cursor)?);
|
||||
}
|
||||
BLOCK_TYPE_SERVER_PROFILE => {
|
||||
require!(!is_server, ErrCode::InvalidRecordData);
|
||||
is_server = read_u8(useful, &mut cursor)? == 1;
|
||||
require!(is_server, ErrCode::InvalidRecordData);
|
||||
server_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?);
|
||||
server_address = read_len_prefixed_string(useful, &mut cursor)?;
|
||||
let sync_count = read_u8(useful, &mut cursor)? as usize;
|
||||
require!(sync_count <= MAX_SYNC_SERVERS, ErrCode::InvalidRecordData);
|
||||
for _ in 0..sync_count {
|
||||
sync_servers.push(read_len_prefixed_string(useful, &mut cursor)?);
|
||||
}
|
||||
}
|
||||
BLOCK_TYPE_ACCESS_SERVERS => {
|
||||
require!(access_servers.is_empty(), ErrCode::InvalidRecordData);
|
||||
let access_count = read_u8(useful, &mut cursor)? as usize;
|
||||
for _ in 0..access_count {
|
||||
access_servers.push(read_len_prefixed_string(useful, &mut cursor)?);
|
||||
}
|
||||
}
|
||||
BLOCK_TYPE_TRUSTED_STATE => {
|
||||
trusted_count = read_u8(useful, &mut cursor)?;
|
||||
}
|
||||
_ => return Err(error!(ErrCode::InvalidRecordFormat)),
|
||||
}
|
||||
out
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let access_count = read_u8(useful, &mut cursor)? as usize;
|
||||
let mut access_servers = Vec::with_capacity(access_count);
|
||||
for _ in 0..access_count {
|
||||
access_servers.push(read_len_prefixed_string(useful, &mut cursor)?);
|
||||
}
|
||||
|
||||
let trusted_count = read_u8(useful, &mut cursor)?;
|
||||
require!(
|
||||
useful.get(cursor..cursor + 5) == Some(&RESERVED_BYTES),
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
cursor += 5;
|
||||
|
||||
let signature = read_fixed_64(useful, &mut cursor)?;
|
||||
require!(cursor == useful.len(), ErrCode::InvalidRecordLength);
|
||||
|
||||
Ok(UserRecord {
|
||||
created_at_ms,
|
||||
updated_at_ms,
|
||||
version,
|
||||
prev_hash,
|
||||
record_number,
|
||||
prev_record_hash,
|
||||
login,
|
||||
root_key_status,
|
||||
root_key,
|
||||
blockchain_key_status,
|
||||
blockchain_key,
|
||||
device_key_status,
|
||||
device_key,
|
||||
chain_number,
|
||||
balance,
|
||||
root_key: root_key.ok_or(error!(ErrCode::InvalidRecordData))?,
|
||||
device_key: device_key.ok_or(error!(ErrCode::InvalidRecordData))?,
|
||||
blockchain: blockchain.ok_or(error!(ErrCode::InvalidRecordData))?,
|
||||
is_server,
|
||||
server_key,
|
||||
server_address,
|
||||
@ -656,29 +795,71 @@ fn verify_record_signature(
|
||||
signature: &[u8],
|
||||
unsigned: &[u8],
|
||||
) -> Result<[u8; 64]> {
|
||||
let provided_sig = vec_to_signature(signature)?;
|
||||
let msg_hash = hashv(&[unsigned]);
|
||||
verify_ed25519_signature_instruction(
|
||||
instructions_sysvar,
|
||||
root_key,
|
||||
&provided_sig,
|
||||
msg_hash.as_ref(),
|
||||
)?;
|
||||
Ok(provided_sig)
|
||||
}
|
||||
|
||||
fn verify_last_block_state_signature(
|
||||
instructions_sysvar: &AccountInfo,
|
||||
record: &UserRecord,
|
||||
) -> Result<()> {
|
||||
let message = serialize_last_block_state(record)?;
|
||||
let msg_hash = hashv(&[&message]);
|
||||
verify_ed25519_signature_instruction(
|
||||
instructions_sysvar,
|
||||
&record.blockchain.blockchain_public_key,
|
||||
&record.blockchain.last_block_signature,
|
||||
msg_hash.as_ref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_ed25519_signature_instruction(
|
||||
instructions_sysvar: &AccountInfo,
|
||||
expected_pubkey: &Pubkey,
|
||||
expected_signature: &[u8; 64],
|
||||
expected_message: &[u8],
|
||||
) -> Result<()> {
|
||||
require_keys_eq!(
|
||||
*instructions_sysvar.key,
|
||||
anchor_lang::solana_program::sysvar::instructions::id(),
|
||||
ErrCode::InvalidSignature
|
||||
);
|
||||
let provided_sig = vec_to_signature(signature)?;
|
||||
let msg_hash = hashv(&[unsigned]);
|
||||
|
||||
let current_ix_index = load_current_index_checked(instructions_sysvar)
|
||||
.map_err(|_| error!(ErrCode::InvalidSignature))?;
|
||||
require!(current_ix_index > 0, ErrCode::InvalidSignature);
|
||||
let ed_ix = load_instruction_at_checked((current_ix_index - 1) as usize, instructions_sysvar)
|
||||
.map_err(|_| error!(ErrCode::InvalidSignature))?;
|
||||
for ix_index in 0..current_ix_index {
|
||||
let ed_ix = load_instruction_at_checked(ix_index as usize, instructions_sysvar)
|
||||
.map_err(|_| error!(ErrCode::InvalidSignature))?;
|
||||
if ed_ix.program_id != ed25519_program::id() {
|
||||
continue;
|
||||
}
|
||||
let parsed = parse_ed25519_ix(&ed_ix)?;
|
||||
if parsed.pubkey == *expected_pubkey
|
||||
&& parsed.signature == *expected_signature
|
||||
&& parsed.message == expected_message
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(error!(ErrCode::InvalidSignature))
|
||||
}
|
||||
|
||||
let parsed = parse_ed25519_ix(&ed_ix)?;
|
||||
require_keys_eq!(parsed.pubkey, *root_key, ErrCode::InvalidSignature);
|
||||
require!(
|
||||
parsed.message == msg_hash.as_ref(),
|
||||
ErrCode::InvalidSignature
|
||||
);
|
||||
require!(parsed.signature == provided_sig, ErrCode::InvalidSignature);
|
||||
|
||||
Ok(parsed.signature)
|
||||
fn serialize_last_block_state(record: &UserRecord) -> Result<Vec<u8>> {
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(LAST_BLOCK_STATE_PREFIX);
|
||||
write_len_prefixed_string(&mut out, &record.login)?;
|
||||
write_len_prefixed_string(&mut out, &record.blockchain.blockchain_name)?;
|
||||
out.extend_from_slice(&record.blockchain.last_block_number.to_le_bytes());
|
||||
out.extend_from_slice(&record.blockchain.last_block_hash);
|
||||
out.extend_from_slice(&record.blockchain.used_bytes.to_le_bytes());
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
struct ParsedEd25519 {
|
||||
@ -770,6 +951,22 @@ fn login_seed_normalized(login: &str) -> String {
|
||||
}
|
||||
|
||||
fn validate_fields(fields: &UserMutableFields) -> Result<()> {
|
||||
require!(
|
||||
!fields.blockchain_name.is_empty(),
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
require!(
|
||||
fields.blockchain_name.as_bytes().len() <= u8::MAX as usize,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
require!(
|
||||
fields.last_block_hash.len() == 32 && fields.last_block_signature.len() == 64,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
require!(
|
||||
fields.arweave_tx_id.as_bytes().len() <= u8::MAX as usize,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
if fields.is_server {
|
||||
require!(
|
||||
!fields.server_address.is_empty(),
|
||||
@ -808,6 +1005,30 @@ fn validate_fields(fields: &UserMutableFields) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_blockchain_limits(
|
||||
blockchain: &BlockchainRecord,
|
||||
old_used_bytes: u64,
|
||||
old_last_block_number: u32,
|
||||
is_create: bool,
|
||||
) -> Result<()> {
|
||||
require!(
|
||||
blockchain.blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
require!(
|
||||
blockchain.used_bytes <= blockchain.paid_limit_bytes,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
if !is_create {
|
||||
require!(
|
||||
blockchain.used_bytes >= old_used_bytes
|
||||
&& blockchain.last_block_number >= old_last_block_number,
|
||||
ErrCode::InvalidRecordData
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_inflow_vault(inflow_vault: &AccountInfo) -> Result<()> {
|
||||
let payments_program_id = Pubkey::from_str(settings::SHINE_PAYMENTS_PROGRAM_ID)
|
||||
.map_err(|_| error!(ErrCode::InvalidFeeReceiver))?;
|
||||
@ -850,7 +1071,10 @@ fn ensure_pda_size_and_rent<'info>(
|
||||
let increase = required_len
|
||||
.checked_sub(current_len)
|
||||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||
require!(increase <= MAX_AUTO_REALLOC_INCREASE, ErrCode::RecordTooLarge);
|
||||
require!(
|
||||
increase <= MAX_AUTO_REALLOC_INCREASE,
|
||||
ErrCode::RecordTooLarge
|
||||
);
|
||||
|
||||
let rent = Rent::get()?;
|
||||
let required_lamports = rent.minimum_balance(required_len);
|
||||
@ -918,17 +1142,6 @@ fn read_u8(data: &[u8], cursor: &mut usize) -> Result<u8> {
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn read_u16(data: &[u8], cursor: &mut usize) -> Result<u16> {
|
||||
let end = cursor
|
||||
.checked_add(2)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
let slice = data
|
||||
.get(*cursor..end)
|
||||
.ok_or(error!(ErrCode::InvalidRecordData))?;
|
||||
*cursor = end;
|
||||
Ok(u16::from_le_bytes([slice[0], slice[1]]))
|
||||
}
|
||||
|
||||
fn read_u32(data: &[u8], cursor: &mut usize) -> Result<u32> {
|
||||
let end = cursor
|
||||
.checked_add(4)
|
||||
|
||||
@ -4,7 +4,6 @@ import {
|
||||
Ed25519Program,
|
||||
PublicKey,
|
||||
SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
} from "@solana/web3.js";
|
||||
import { createHash } from "crypto";
|
||||
@ -12,23 +11,36 @@ import { expect } from "chai";
|
||||
import { Shine } from "../target/types/shine";
|
||||
|
||||
const MAGIC = Buffer.from("SHiNE", "utf8");
|
||||
const FORMAT_MAJOR = 1;
|
||||
const FORMAT_MAJOR = 2;
|
||||
const FORMAT_MINOR = 0;
|
||||
const RESERVED = Buffer.from([0, 0, 0, 0, 0]);
|
||||
const ZERO_HASH = Buffer.alloc(32, 0);
|
||||
const KEY_STATUS_CREATED = 0;
|
||||
const LAST_BLOCK_STATE_PREFIX = Buffer.from("SHiNE_LAST_BLOCK", "utf8");
|
||||
const BLOCK_TYPE_ROOT_KEY = 1;
|
||||
const BLOCK_TYPE_DEVICE_KEY = 2;
|
||||
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY = 3;
|
||||
const BLOCK_TYPE_SERVER_PROFILE = 30;
|
||||
const BLOCK_TYPE_ACCESS_SERVERS = 40;
|
||||
const BLOCK_TYPE_TRUSTED_STATE = 50;
|
||||
const BLOCK_VERSION_0 = 0;
|
||||
const BLOCKCHAIN_TYPE_MAIN_USER = 1;
|
||||
|
||||
const LIMIT_STEP = 10_000n;
|
||||
const START_BONUS_LIMIT = 100_000n;
|
||||
const USERS_ECONOMY_CONFIG_SEED = "shine_users_economy_config";
|
||||
const SHINE_PAYMENTS_PROGRAM_ID = new PublicKey("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR");
|
||||
const SHINE_LOGIN_GUARD_PROGRAM_ID = new PublicKey("3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo");
|
||||
const SHINE_PAYMENTS_PROGRAM_ID = new PublicKey(
|
||||
"m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR"
|
||||
);
|
||||
const SHINE_PAYMENTS_INFLOW_VAULT_SEED = "shine_payments_inflow_vault";
|
||||
|
||||
type MutableFields = {
|
||||
blockchainKey: PublicKey;
|
||||
deviceKey: PublicKey;
|
||||
chainNumber: number;
|
||||
blockchainPublicKey: PublicKey;
|
||||
blockchainName: string;
|
||||
usedBytes: bigint;
|
||||
lastBlockNumber: number;
|
||||
lastBlockHash: Buffer;
|
||||
lastBlockSignature: Buffer;
|
||||
arweaveTxId: string;
|
||||
isServer: boolean;
|
||||
serverKey: PublicKey;
|
||||
serverAddress: string;
|
||||
@ -40,17 +52,22 @@ type MutableFields = {
|
||||
type UnsignedRecord = {
|
||||
createdAtMs: bigint;
|
||||
updatedAtMs: bigint;
|
||||
version: number;
|
||||
prevHash: Buffer;
|
||||
recordNumber: number;
|
||||
prevRecordHash: Buffer;
|
||||
login: string;
|
||||
rootKeyStatus: number;
|
||||
rootKey: PublicKey;
|
||||
blockchainKeyStatus: number;
|
||||
blockchainKey: PublicKey;
|
||||
deviceKeyStatus: number;
|
||||
deviceKey: PublicKey;
|
||||
chainNumber: number;
|
||||
balance: bigint;
|
||||
blockchain: {
|
||||
blockchainType: number;
|
||||
blockchainName: string;
|
||||
blockchainPublicKey: PublicKey;
|
||||
paidLimitBytes: bigint;
|
||||
usedBytes: bigint;
|
||||
lastBlockNumber: number;
|
||||
lastBlockHash: Buffer;
|
||||
lastBlockSignature: Buffer;
|
||||
arweaveTxId: string;
|
||||
};
|
||||
isServer: boolean;
|
||||
serverKey: PublicKey;
|
||||
serverAddress: string;
|
||||
@ -59,12 +76,6 @@ type UnsignedRecord = {
|
||||
trustedCount: number;
|
||||
};
|
||||
|
||||
function u16le(v: number): Buffer {
|
||||
const b = Buffer.alloc(2);
|
||||
b.writeUInt16LE(v, 0);
|
||||
return b;
|
||||
}
|
||||
|
||||
function u32le(v: number): Buffer {
|
||||
const b = Buffer.alloc(4);
|
||||
b.writeUInt32LE(v, 0);
|
||||
@ -77,10 +88,12 @@ function u64le(v: bigint): Buffer {
|
||||
return b;
|
||||
}
|
||||
|
||||
function serializeUnsignedRecord(r: UnsignedRecord): Buffer {
|
||||
const loginBytes = Buffer.from(r.login, "utf8");
|
||||
const serverAddressBytes = Buffer.from(r.serverAddress, "utf8");
|
||||
function strBytes(value: string): Buffer {
|
||||
const bytes = Buffer.from(value, "utf8");
|
||||
return Buffer.concat([Buffer.from([bytes.length]), bytes]);
|
||||
}
|
||||
|
||||
function serializeUnsignedRecord(r: UnsignedRecord): Buffer {
|
||||
const out: Buffer[] = [];
|
||||
out.push(MAGIC);
|
||||
out.push(Buffer.from([FORMAT_MAJOR]));
|
||||
@ -89,44 +102,48 @@ function serializeUnsignedRecord(r: UnsignedRecord): Buffer {
|
||||
|
||||
out.push(u64le(r.createdAtMs));
|
||||
out.push(u64le(r.updatedAtMs));
|
||||
out.push(u32le(r.version));
|
||||
out.push(r.prevHash);
|
||||
out.push(u32le(r.recordNumber));
|
||||
out.push(r.prevRecordHash);
|
||||
out.push(strBytes(r.login));
|
||||
|
||||
out.push(Buffer.from([loginBytes.length]));
|
||||
out.push(loginBytes);
|
||||
|
||||
out.push(Buffer.from([r.rootKeyStatus]));
|
||||
out.push(Buffer.from([r.isServer ? 6 : 5]));
|
||||
out.push(Buffer.from([BLOCK_TYPE_ROOT_KEY, BLOCK_VERSION_0]));
|
||||
out.push(r.rootKey.toBuffer());
|
||||
out.push(Buffer.from([r.blockchainKeyStatus]));
|
||||
out.push(r.blockchainKey.toBuffer());
|
||||
out.push(Buffer.from([r.deviceKeyStatus]));
|
||||
out.push(Buffer.from([BLOCK_TYPE_DEVICE_KEY, BLOCK_VERSION_0]));
|
||||
out.push(r.deviceKey.toBuffer());
|
||||
out.push(Buffer.from([BLOCK_TYPE_BLOCKCHAIN_REGISTRY, BLOCK_VERSION_0, 1]));
|
||||
out.push(Buffer.from([r.blockchain.blockchainType]));
|
||||
out.push(strBytes(r.blockchain.blockchainName));
|
||||
out.push(r.blockchain.blockchainPublicKey.toBuffer());
|
||||
out.push(u64le(r.blockchain.paidLimitBytes));
|
||||
out.push(u64le(r.blockchain.usedBytes));
|
||||
out.push(u32le(r.blockchain.lastBlockNumber));
|
||||
out.push(r.blockchain.lastBlockHash);
|
||||
out.push(r.blockchain.lastBlockSignature);
|
||||
if (r.blockchain.arweaveTxId.length === 0) {
|
||||
out.push(Buffer.from([0]));
|
||||
} else {
|
||||
out.push(Buffer.from([1]));
|
||||
out.push(strBytes(r.blockchain.arweaveTxId));
|
||||
}
|
||||
|
||||
out.push(u16le(r.chainNumber));
|
||||
out.push(u64le(r.balance));
|
||||
|
||||
out.push(Buffer.from([r.isServer ? 1 : 0]));
|
||||
if (r.isServer) {
|
||||
out.push(Buffer.from([BLOCK_TYPE_SERVER_PROFILE, BLOCK_VERSION_0, 1]));
|
||||
out.push(r.serverKey.toBuffer());
|
||||
out.push(Buffer.from([serverAddressBytes.length]));
|
||||
out.push(serverAddressBytes);
|
||||
out.push(strBytes(r.serverAddress));
|
||||
out.push(Buffer.from([r.syncServers.length]));
|
||||
for (const s of r.syncServers) {
|
||||
const sb = Buffer.from(s, "utf8");
|
||||
out.push(Buffer.from([sb.length]));
|
||||
out.push(sb);
|
||||
out.push(strBytes(s));
|
||||
}
|
||||
}
|
||||
|
||||
out.push(Buffer.from([BLOCK_TYPE_ACCESS_SERVERS, BLOCK_VERSION_0]));
|
||||
out.push(Buffer.from([r.accessServers.length]));
|
||||
for (const s of r.accessServers) {
|
||||
const sb = Buffer.from(s, "utf8");
|
||||
out.push(Buffer.from([sb.length]));
|
||||
out.push(sb);
|
||||
out.push(strBytes(s));
|
||||
}
|
||||
|
||||
out.push(Buffer.from([BLOCK_TYPE_TRUSTED_STATE, BLOCK_VERSION_0]));
|
||||
out.push(Buffer.from([r.trustedCount]));
|
||||
out.push(RESERVED);
|
||||
|
||||
const unsigned = Buffer.concat(out);
|
||||
const recordLen = unsigned.length + 64;
|
||||
@ -138,6 +155,17 @@ function sha256(buf: Buffer): Buffer {
|
||||
return createHash("sha256").update(buf).digest();
|
||||
}
|
||||
|
||||
function serializeLastBlockState(r: UnsignedRecord): Buffer {
|
||||
return Buffer.concat([
|
||||
LAST_BLOCK_STATE_PREFIX,
|
||||
strBytes(r.login),
|
||||
strBytes(r.blockchain.blockchainName),
|
||||
u32le(r.blockchain.lastBlockNumber),
|
||||
r.blockchain.lastBlockHash,
|
||||
u64le(r.blockchain.usedBytes),
|
||||
]);
|
||||
}
|
||||
|
||||
function extractSigFromEdIx(ixData: Buffer): Buffer {
|
||||
const signatureOffset = ixData.readUInt16LE(2);
|
||||
return ixData.subarray(signatureOffset, signatureOffset + 64);
|
||||
@ -163,23 +191,25 @@ describe("shine_users e2e", () => {
|
||||
SHINE_PAYMENTS_PROGRAM_ID
|
||||
);
|
||||
|
||||
const economyAi = await provider.connection.getAccountInfo(usersEconomyConfigPda);
|
||||
const economyAi = await provider.connection.getAccountInfo(
|
||||
usersEconomyConfigPda
|
||||
);
|
||||
if (!economyAi) {
|
||||
await program.methods
|
||||
.initUsersEconomyConfig()
|
||||
.accounts({
|
||||
signer: provider.wallet.publicKey,
|
||||
usersEconomyConfigPda,
|
||||
systemProgram: SystemProgram.programId,
|
||||
})
|
||||
.rpc();
|
||||
}
|
||||
|
||||
const root = anchor.web3.Keypair.generate();
|
||||
const blockchainKey = anchor.web3.Keypair.generate().publicKey;
|
||||
const blockchain = anchor.web3.Keypair.generate();
|
||||
const deviceKey = anchor.web3.Keypair.generate().publicKey;
|
||||
const serverKey1 = anchor.web3.Keypair.generate().publicKey;
|
||||
const serverKey2 = anchor.web3.Keypair.generate().publicKey;
|
||||
const blockchainName = `${login}-001`;
|
||||
|
||||
const createdAtMs = BigInt(Date.now());
|
||||
const additionalLimitCreate = 20_000n;
|
||||
@ -188,17 +218,22 @@ describe("shine_users e2e", () => {
|
||||
const createRecord: UnsignedRecord = {
|
||||
createdAtMs,
|
||||
updatedAtMs: createdAtMs,
|
||||
version: 0,
|
||||
prevHash: ZERO_HASH,
|
||||
recordNumber: 0,
|
||||
prevRecordHash: ZERO_HASH,
|
||||
login,
|
||||
rootKeyStatus: KEY_STATUS_CREATED,
|
||||
rootKey: root.publicKey,
|
||||
blockchainKeyStatus: KEY_STATUS_CREATED,
|
||||
blockchainKey,
|
||||
deviceKeyStatus: KEY_STATUS_CREATED,
|
||||
deviceKey,
|
||||
chainNumber: 1,
|
||||
balance: START_BONUS_LIMIT + additionalLimitCreate,
|
||||
blockchain: {
|
||||
blockchainType: BLOCKCHAIN_TYPE_MAIN_USER,
|
||||
blockchainName,
|
||||
blockchainPublicKey: blockchain.publicKey,
|
||||
paidLimitBytes: START_BONUS_LIMIT + additionalLimitCreate,
|
||||
usedBytes: 0n,
|
||||
lastBlockNumber: 0,
|
||||
lastBlockHash: ZERO_HASH,
|
||||
lastBlockSignature: Buffer.alloc(64, 0),
|
||||
arweaveTxId: "",
|
||||
},
|
||||
isServer: true,
|
||||
serverKey: serverKey1,
|
||||
serverAddress: "https://srv-1.local",
|
||||
@ -206,6 +241,14 @@ describe("shine_users e2e", () => {
|
||||
accessServers: ["access_srv_1"],
|
||||
trustedCount: 0,
|
||||
};
|
||||
const createLastBlockHash = sha256(serializeLastBlockState(createRecord));
|
||||
const createLastBlockEdIx = Ed25519Program.createInstructionWithPrivateKey({
|
||||
privateKey: blockchain.secretKey,
|
||||
message: createLastBlockHash,
|
||||
});
|
||||
createRecord.blockchain.lastBlockSignature = extractSigFromEdIx(
|
||||
Buffer.from(createLastBlockEdIx.data)
|
||||
);
|
||||
|
||||
const createUnsigned = serializeUnsignedRecord(createRecord);
|
||||
const createHash = sha256(createUnsigned);
|
||||
@ -222,9 +265,16 @@ describe("shine_users e2e", () => {
|
||||
createdAtMs: new anchor.BN(createdAtMs.toString()),
|
||||
additionalLimit: new anchor.BN(additionalLimitCreate.toString()),
|
||||
fields: {
|
||||
blockchainKey,
|
||||
deviceKey,
|
||||
chainNumber: 1,
|
||||
blockchainPublicKey: blockchain.publicKey,
|
||||
blockchainName,
|
||||
usedBytes: new anchor.BN(
|
||||
createRecord.blockchain.usedBytes.toString()
|
||||
),
|
||||
lastBlockNumber: createRecord.blockchain.lastBlockNumber,
|
||||
lastBlockHash: createRecord.blockchain.lastBlockHash,
|
||||
lastBlockSignature: createRecord.blockchain.lastBlockSignature,
|
||||
arweaveTxId: "",
|
||||
isServer: true,
|
||||
serverKey: serverKey1,
|
||||
serverAddress: "https://srv-1.local",
|
||||
@ -237,15 +287,16 @@ describe("shine_users e2e", () => {
|
||||
.accounts({
|
||||
signer: provider.wallet.publicKey,
|
||||
userPda,
|
||||
systemProgram: SystemProgram.programId,
|
||||
inflowVault: inflowVaultPda,
|
||||
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||
usersEconomyConfigPda,
|
||||
loginGuardProgram: SHINE_LOGIN_GUARD_PROGRAM_ID,
|
||||
})
|
||||
.instruction();
|
||||
|
||||
await provider.sendAndConfirm(new Transaction().add(createEdIx, createIx), []);
|
||||
await provider.sendAndConfirm(
|
||||
new Transaction().add(createLastBlockEdIx, createEdIx, createIx),
|
||||
[]
|
||||
);
|
||||
|
||||
const createAcc = await provider.connection.getAccountInfo(userPda);
|
||||
expect(createAcc).not.eq(null);
|
||||
@ -257,17 +308,23 @@ describe("shine_users e2e", () => {
|
||||
const updateRecord: UnsignedRecord = {
|
||||
createdAtMs,
|
||||
updatedAtMs: createdAtMs + 1_000n,
|
||||
version: 1,
|
||||
prevHash: sha256(createUnsigned),
|
||||
recordNumber: 1,
|
||||
prevRecordHash: sha256(createUnsigned),
|
||||
login,
|
||||
rootKeyStatus: KEY_STATUS_CREATED,
|
||||
rootKey: root.publicKey,
|
||||
blockchainKeyStatus: KEY_STATUS_CREATED,
|
||||
blockchainKey: anchor.web3.Keypair.generate().publicKey,
|
||||
deviceKeyStatus: KEY_STATUS_CREATED,
|
||||
deviceKey: anchor.web3.Keypair.generate().publicKey,
|
||||
chainNumber: 1,
|
||||
balance: START_BONUS_LIMIT + additionalLimitCreate + additionalLimitUpdate,
|
||||
blockchain: {
|
||||
blockchainType: BLOCKCHAIN_TYPE_MAIN_USER,
|
||||
blockchainName,
|
||||
blockchainPublicKey: blockchain.publicKey,
|
||||
paidLimitBytes:
|
||||
START_BONUS_LIMIT + additionalLimitCreate + additionalLimitUpdate,
|
||||
usedBytes: 512n,
|
||||
lastBlockNumber: 1,
|
||||
lastBlockHash: sha256(Buffer.from("first-shine-block")),
|
||||
lastBlockSignature: Buffer.alloc(64, 0),
|
||||
arweaveTxId: "",
|
||||
},
|
||||
isServer: true,
|
||||
serverKey: serverKey2,
|
||||
serverAddress: "https://srv-2.local",
|
||||
@ -275,6 +332,14 @@ describe("shine_users e2e", () => {
|
||||
accessServers: ["access_srv_2", "access_srv_3"],
|
||||
trustedCount: 0,
|
||||
};
|
||||
const updateLastBlockHash = sha256(serializeLastBlockState(updateRecord));
|
||||
const updateLastBlockEdIx = Ed25519Program.createInstructionWithPrivateKey({
|
||||
privateKey: blockchain.secretKey,
|
||||
message: updateLastBlockHash,
|
||||
});
|
||||
updateRecord.blockchain.lastBlockSignature = extractSigFromEdIx(
|
||||
Buffer.from(updateLastBlockEdIx.data)
|
||||
);
|
||||
|
||||
const updateUnsigned = serializeUnsignedRecord(updateRecord);
|
||||
const updateHash = sha256(updateUnsigned);
|
||||
@ -294,9 +359,16 @@ describe("shine_users e2e", () => {
|
||||
prevHash: sha256(createUnsigned),
|
||||
additionalLimit: new anchor.BN(additionalLimitUpdate.toString()),
|
||||
fields: {
|
||||
blockchainKey: updateRecord.blockchainKey,
|
||||
deviceKey: updateRecord.deviceKey,
|
||||
chainNumber: 1,
|
||||
blockchainPublicKey: updateRecord.blockchain.blockchainPublicKey,
|
||||
blockchainName,
|
||||
usedBytes: new anchor.BN(
|
||||
updateRecord.blockchain.usedBytes.toString()
|
||||
),
|
||||
lastBlockNumber: updateRecord.blockchain.lastBlockNumber,
|
||||
lastBlockHash: updateRecord.blockchain.lastBlockHash,
|
||||
lastBlockSignature: updateRecord.blockchain.lastBlockSignature,
|
||||
arweaveTxId: "",
|
||||
isServer: true,
|
||||
serverKey: serverKey2,
|
||||
serverAddress: "https://srv-2.local",
|
||||
@ -309,14 +381,16 @@ describe("shine_users e2e", () => {
|
||||
.accounts({
|
||||
signer: provider.wallet.publicKey,
|
||||
userPda,
|
||||
systemProgram: SystemProgram.programId,
|
||||
inflowVault: inflowVaultPda,
|
||||
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||
usersEconomyConfigPda,
|
||||
})
|
||||
.instruction();
|
||||
|
||||
await provider.sendAndConfirm(new Transaction().add(updateEdIx, updateIx), []);
|
||||
await provider.sendAndConfirm(
|
||||
new Transaction().add(updateLastBlockEdIx, updateEdIx, updateIx),
|
||||
[]
|
||||
);
|
||||
|
||||
const updatedAcc = await provider.connection.getAccountInfo(userPda);
|
||||
expect(updatedAcc).not.eq(null);
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
"compilerOptions": {
|
||||
"types": ["mocha", "chai"],
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"lib": ["es2015"],
|
||||
"lib": ["es2020"],
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"target": "es2020",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user