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