Обновить формат Solana user PDA

This commit is contained in:
AidarKC 2026-05-24 19:41:13 +03:00
parent 74df7e2645
commit baef264bd0
7 changed files with 861 additions and 411 deletions

View File

@ -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

View File

@ -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

View File

@ -1,2 +1,2 @@
client.version=1.2.89 client.version=1.2.90
server.version=1.2.83 server.version=1.2.84

View File

@ -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.

View File

@ -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)?;
out.push(record.access_servers.len() as u8); write_trusted_state_block(&mut out, record);
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,72 +696,81 @@ 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);
let sync_count = read_u8(useful, &mut cursor)? as usize; device_key = Some(Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?));
require!(sync_count <= MAX_SYNC_SERVERS, ErrCode::InvalidRecordData); }
let mut out = Vec::with_capacity(sync_count); BLOCK_TYPE_BLOCKCHAIN_REGISTRY => {
for _ in 0..sync_count { require!(blockchain.is_none(), ErrCode::InvalidRecordData);
out.push(read_len_prefixed_string(useful, &mut cursor)?); 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)?; let signature = read_fixed_64(useful, &mut cursor)?;
require!(cursor == useful.len(), ErrCode::InvalidRecordLength); require!(cursor == useful.len(), ErrCode::InvalidRecordLength);
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 {
.map_err(|_| error!(ErrCode::InvalidSignature))?; 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)?; fn serialize_last_block_state(record: &UserRecord) -> Result<Vec<u8>> {
require_keys_eq!(parsed.pubkey, *root_key, ErrCode::InvalidSignature); let mut out = Vec::new();
require!( out.extend_from_slice(LAST_BLOCK_STATE_PREFIX);
parsed.message == msg_hash.as_ref(), write_len_prefixed_string(&mut out, &record.login)?;
ErrCode::InvalidSignature write_len_prefixed_string(&mut out, &record.blockchain.blockchain_name)?;
); out.extend_from_slice(&record.blockchain.last_block_number.to_le_bytes());
require!(parsed.signature == provided_sig, ErrCode::InvalidSignature); out.extend_from_slice(&record.blockchain.last_block_hash);
out.extend_from_slice(&record.blockchain.used_bytes.to_le_bytes());
Ok(parsed.signature) 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)

View File

@ -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);

View File

@ -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
} }
} }