diff --git a/Dev_Docs/Pending_Features/2026-06-04_2315_pure_rust_solana_users_and_login_guard.md b/Dev_Docs/Pending_Features/2026-06-04_2315_pure_rust_solana_users_and_login_guard.md new file mode 100644 index 0000000..85cf4a5 --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-04_2315_pure_rust_solana_users_and_login_guard.md @@ -0,0 +1,33 @@ +# Pure Rust `shine_users` и `shine_login_guard` + +Статус: `pending` + +## Что сделано + +- `shine_login_guard` переписан без Anchor на чистый Rust/Solana SDK. +- `shine_users` переписан без Anchor на чистый Rust/Solana SDK. +- Для `shine_users` введён новый instruction ABI без Anchor discriminator'ов. +- Для `shine_users` используются новые seed'ы: + - `user_login=` для `user_pda` + - `shine_users_economy_config_v2` для economy PDA +- Формат блоков PDA синхронизирован: + - `SessionsBlock = 50` + - `TrustedStateBlock = 70` +- UI JS-модуль и Java lazy-import обновлены под новые seeds/ABI/коды блоков. + +## Что проверить руками + +1. В обычном UI выполнить регистрацию нового пользователя в Solana. +2. Проверить, что после регистрации читается новая `user_pda`. +3. В server UI выполнить создание server PDA. +4. В server UI выполнить update server PDA. +5. Проверить, что после update растёт `record_number`. +6. Проверить, что lazy-import на сервере читает новый формат PDA без ошибок. +7. Проверить, что старые Anchor discriminator'ы больше нигде не требуются. + +## Ожидаемый результат + +- Регистрация и update работают на новых чисто-rust программах. +- UI не использует старый Anchor ABI. +- Серверный Java parser читает новый формат PDA. +- Ошибок `out of memory` и anchor-specific падений больше нет. diff --git a/Dev_Docs/Solana/user_pda/README.md b/Dev_Docs/Solana/user_pda/README.md index 02fd1e3..e520ff9 100644 --- a/Dev_Docs/Solana/user_pda/README.md +++ b/Dev_Docs/Solana/user_pda/README.md @@ -26,7 +26,7 @@ Адрес пользовательской PDA вычисляется по логину: -- seed prefix: `login=`; +- seed prefix: `user_login=`; - второй seed: нормализованный логин в нижнем регистре; - program id: программа `shine_users`. @@ -90,8 +90,8 @@ UserPdaRecordV1 | `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. | | `30` | `ServerProfileBlock` | Серверные данные пользователя. | | `40` | `AccessServersBlock` | Серверы доступа/relay. | -| `55` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. | -| `50` | `TrustedStateBlock` | Счетчик trusted-связей. | +| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. | +| `70` | `TrustedStateBlock` | Счетчик trusted-связей. | | `255` | `ReservedBlock` | Зарезервировано, пока не используется. | Правила: @@ -280,7 +280,7 @@ AccessServersBlock ```text SessionsBlock -- block_type: u8 = 55 +- block_type: u8 = 50 - block_version: u8 = 0 - sessions_mode: u8 - sessions_count: u8 @@ -326,7 +326,7 @@ SessionRecord ```text TrustedStateBlock -- block_type: u8 = 50 +- block_type: u8 = 70 - block_version: u8 = 0 - trusted_count: u8 = 0 ``` diff --git a/Dev_Docs/Инициализация_Solana_регистрации/README.md b/Dev_Docs/Инициализация_Solana_регистрации/README.md index 72375a5..4fcad7d 100644 --- a/Dev_Docs/Инициализация_Solana_регистрации/README.md +++ b/Dev_Docs/Инициализация_Solana_регистрации/README.md @@ -79,7 +79,7 @@ anchor deploy -p shine_users Страница сама вычисляет PDA `users_economy_config` по seed: -- seed: `shine_users_economy_config` +- seed: `shine_users_economy_config_v2` - program: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm` ## Кто оплачивает create/update user_pda diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java index c25cbb6..3183fbe 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java @@ -190,7 +190,7 @@ public final class SolanaUserPdaImportService { int n = u8(raw, c++); c += n; } - } else if (blockType == 55) { + } else if (blockType == 50) { int sessionsMode = u8(raw, c++); if (sessionsMode != 1 && sessionsMode != 10) return null; int sessionsCount = u8(raw, c++); @@ -202,7 +202,7 @@ public final class SolanaUserPdaImportService { c += n; c += 32; // session_pub_key } - } else if (blockType == 50) { + } else if (blockType == 70) { c += 1; } else { return null; diff --git a/VERSION.properties b/VERSION.properties index 803df9d..ae3499c 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.127 -server.version=1.2.119 +client.version=1.2.128 +server.version=1.2.120 diff --git a/shine-UI/js/services/shine-user-pda-service.js b/shine-UI/js/services/shine-user-pda-service.js index c20d8d8..d4491b8 100644 --- a/shine-UI/js/services/shine-user-pda-service.js +++ b/shine-UI/js/services/shine-user-pda-service.js @@ -10,10 +10,9 @@ import { const MAGIC = 'SHiNE'; const LAST_BLOCK_STATE_PREFIX = 'SHiNE_LAST_BLOCK'; const SHINE_PAYMENTS_INFLOW_VAULT_SEED = 'shine_payments_inflow_vault'; +const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login='; const LIMIT_STEP = 10_000n; const BLOCKCHAIN_TYPE_MAIN_USER = 1; -const CREATE_USER_PDA_DISCRIMINATOR = new Uint8Array([139, 157, 13, 41, 142, 174, 226, 214]); -const UPDATE_USER_PDA_DISCRIMINATOR = new Uint8Array([42, 133, 114, 232, 38, 245, 167, 234]); const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111'; const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111'; @@ -22,8 +21,8 @@ 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_SESSIONS = 55; -const BLOCK_TYPE_TRUSTED_STATE = 50; +const BLOCK_TYPE_SESSIONS = 50; +const BLOCK_TYPE_TRUSTED_STATE = 70; const SESSIONS_MODE_MIXED = 1; const SESSION_TYPE_USER = 1; const SESSION_TYPE_SUBSERVER = 100; @@ -175,60 +174,88 @@ function buildEd25519IxData(sig64, pubkey32, msgHash32) { function serializeCreateUserPdaArgs(args) { const buf = []; - for (const x of CREATE_USER_PDA_DISCRIMINATOR) buf.push(x); - pushStrU32(buf, args.login); + buf.push(3); + pushStrU8(buf, args.login); for (const x of args.rootKey32) buf.push(x); pushU64LE(buf, args.createdAtMs); - pushU64LE(buf, 0n); + pushU64LE(buf, BigInt(args.additionalLimitBytes || 0n)); for (const x of args.deviceKey32) buf.push(x); for (const x of args.blockchainPublicKey32) buf.push(x); - pushStrU32(buf, args.blockchainName); + pushStrU8(buf, args.blockchainName); pushU64LE(buf, args.usedBytes); pushU32LE(buf, args.lastBlockNumber); - pushVecU8(buf, args.lastBlockHash32); - pushVecU8(buf, args.lastBlockSignature64); - pushStrU32(buf, args.arweaveTxId); + for (const x of args.lastBlockHash32) buf.push(x); + for (const x of args.lastBlockSignature64) buf.push(x); + pushStrU8(buf, args.arweaveTxId); buf.push(args.isServer ? 1 : 0); - buf.push(Number(args.addressFormatType || 0) & 0xff); - buf.push(Number(args.addressFormatVersion || 0) & 0xff); - pushStrU32(buf, args.serverAddress); - pushVecStrU32(buf, args.syncServers); - pushVecStrU32(buf, args.accessServers); + if (args.isServer) { + buf.push(Number(args.addressFormatType || 0) & 0xff); + buf.push(Number(args.addressFormatVersion || 0) & 0xff); + pushStrU8(buf, args.serverAddress); + const syncServers = Array.isArray(args.syncServers) ? args.syncServers : []; + buf.push(syncServers.length & 0xff); + for (const value of syncServers) pushStrU8(buf, value); + } + const accessServers = Array.isArray(args.accessServers) ? args.accessServers : []; + buf.push(accessServers.length & 0xff); + for (const value of accessServers) pushStrU8(buf, value); buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff); - pushSessionRecordsU32(buf, args.sessions); + const sessions = Array.isArray(args.sessions) ? args.sessions : []; + buf.push(sessions.length & 0xff); + for (const session of sessions) { + buf.push(Number(session?.sessionType || 0) & 0xff); + buf.push(Number(session?.sessionVersion || 0) & 0xff); + pushStrU8(buf, String(session?.sessionName || '')); + const key = session?.sessionPubKey32 || new Uint8Array(32); + for (const x of key) buf.push(x); + } buf.push(Number(args.trustedCount || 0) & 0xff); - pushVecU8(buf, args.rootSignature64); + for (const x of args.rootSignature64) buf.push(x); return new Uint8Array(buf); } function serializeUpdateUserPdaArgs(args) { const buf = []; - for (const x of UPDATE_USER_PDA_DISCRIMINATOR) buf.push(x); - pushStrU32(buf, args.login); + buf.push(4); + pushStrU8(buf, args.login); for (const x of args.rootKey32) buf.push(x); pushU64LE(buf, args.createdAtMs); pushU64LE(buf, args.updatedAtMs); pushU32LE(buf, args.version); - pushVecU8(buf, args.prevHash32); + for (const x of args.prevHash32) buf.push(x); pushU64LE(buf, args.additionalLimitBytes); for (const x of args.deviceKey32) buf.push(x); for (const x of args.blockchainPublicKey32) buf.push(x); - pushStrU32(buf, args.blockchainName); + pushStrU8(buf, args.blockchainName); pushU64LE(buf, args.usedBytes); pushU32LE(buf, args.lastBlockNumber); - pushVecU8(buf, args.lastBlockHash32); - pushVecU8(buf, args.lastBlockSignature64); - pushStrU32(buf, args.arweaveTxId); + for (const x of args.lastBlockHash32) buf.push(x); + for (const x of args.lastBlockSignature64) buf.push(x); + pushStrU8(buf, args.arweaveTxId); buf.push(args.isServer ? 1 : 0); - buf.push(Number(args.addressFormatType || 0) & 0xff); - buf.push(Number(args.addressFormatVersion || 0) & 0xff); - pushStrU32(buf, args.serverAddress); - pushVecStrU32(buf, args.syncServers); - pushVecStrU32(buf, args.accessServers); + if (args.isServer) { + buf.push(Number(args.addressFormatType || 0) & 0xff); + buf.push(Number(args.addressFormatVersion || 0) & 0xff); + pushStrU8(buf, args.serverAddress); + const syncServers = Array.isArray(args.syncServers) ? args.syncServers : []; + buf.push(syncServers.length & 0xff); + for (const value of syncServers) pushStrU8(buf, value); + } + const accessServers = Array.isArray(args.accessServers) ? args.accessServers : []; + buf.push(accessServers.length & 0xff); + for (const value of accessServers) pushStrU8(buf, value); buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff); - pushSessionRecordsU32(buf, args.sessions); + const sessions = Array.isArray(args.sessions) ? args.sessions : []; + buf.push(sessions.length & 0xff); + for (const session of sessions) { + buf.push(Number(session?.sessionType || 0) & 0xff); + buf.push(Number(session?.sessionVersion || 0) & 0xff); + pushStrU8(buf, String(session?.sessionName || '')); + const key = session?.sessionPubKey32 || new Uint8Array(32); + for (const x of key) buf.push(x); + } buf.push(Number(args.trustedCount || 0) & 0xff); - pushVecU8(buf, args.rootSignature64); + for (const x of args.rootSignature64) buf.push(x); return new Uint8Array(buf); } @@ -500,7 +527,7 @@ export function serializeUnsignedRecordFromState(stateLike) { pushU32LE(buf, state.recordNumber); for (const x of state.prevRecordHash) buf.push(x); pushStrU8(buf, state.login); - buf.push(state.isServer ? 6 : 5); + buf.push(state.isServer ? 7 : 6); buf.push(BLOCK_TYPE_ROOT_KEY, 0); for (const x of state.rootKey) buf.push(x); @@ -569,7 +596,7 @@ export async function readShineUserPda({ login, solanaEndpoint }) { const connection = new solana.Connection(endpoint, 'confirmed'); const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID); const enc = new TextEncoder(); - const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode('login='), enc.encode(cleanLogin)], usersProgram); + const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)], usersProgram); const accountInfo = await connection.getAccountInfo(userPda, 'confirmed'); if (!accountInfo?.data) throw new Error(`PDA не найдена для логина «${cleanLogin}»`); return { @@ -625,7 +652,7 @@ async function buildCreateContext({ login, keyBundle, solanaEndpoint }) { const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID); const enc = new TextEncoder(); - const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode('login='), enc.encode(cleanLogin)], usersProgram); + const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)], usersProgram); const [economyConfigPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram); const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram); @@ -724,6 +751,7 @@ async function createShineUserPdaOnSolana({ login: cleanLogin, rootKey32: ctx.rootKey32, createdAtMs, + additionalLimitBytes: 0n, deviceKey32: ctx.deviceKey32, blockchainPublicKey32: ctx.blockchainKey32, blockchainName, @@ -851,7 +879,7 @@ export async function updateShineUserPdaOnSolana({ const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID); const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID); const enc = new TextEncoder(); - const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode('login='), enc.encode(cleanLogin)], usersProgram); + const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)], usersProgram); const [economyPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram); const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram); const deviceSeed32 = extractSeed32FromPkcs8B64(devicePrivatePkcs8B64); diff --git a/shine-UI/js/solana-programs.js b/shine-UI/js/solana-programs.js index 8c84213..06e948a 100644 --- a/shine-UI/js/solana-programs.js +++ b/shine-UI/js/solana-programs.js @@ -3,6 +3,6 @@ export const SOLANA_ENDPOINT_DEFAULT = 'https://api.devnet.solana.com'; // Программа регистрации пользователей SHiNE (shine_users), задеплоена в devnet. export const SHINE_USERS_PROGRAM_ID = 'FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm'; -export const SHINE_USERS_ECONOMY_CONFIG_SEED = 'shine_users_economy_config'; +export const SHINE_USERS_ECONOMY_CONFIG_SEED = 'shine_users_economy_config_v2'; export const SHINE_LOGIN_GUARD_PROGRAM_ID = '3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo'; export const SHINE_PAYMENTS_PROGRAM_ID = 'm48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR'; diff --git a/shine-solana/shine/Cargo.lock b/shine-solana/shine/Cargo.lock index 83a9304..fc6fa47 100644 --- a/shine-solana/shine/Cargo.lock +++ b/shine-solana/shine/Cargo.lock @@ -3564,7 +3564,7 @@ dependencies = [ name = "shine_login_guard" version = "0.1.0" dependencies = [ - "anchor-lang", + "solana-program 2.2.1", ] [[package]] @@ -3580,9 +3580,7 @@ dependencies = [ name = "shine_users" version = "0.1.0" dependencies = [ - "anchor-lang", - "common", - "shine_login_guard", + "solana-program 2.2.1", ] [[package]] diff --git a/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md b/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md index 02fd1e3..e520ff9 100644 --- a/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md +++ b/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md @@ -26,7 +26,7 @@ Адрес пользовательской PDA вычисляется по логину: -- seed prefix: `login=`; +- seed prefix: `user_login=`; - второй seed: нормализованный логин в нижнем регистре; - program id: программа `shine_users`. @@ -90,8 +90,8 @@ UserPdaRecordV1 | `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. | | `30` | `ServerProfileBlock` | Серверные данные пользователя. | | `40` | `AccessServersBlock` | Серверы доступа/relay. | -| `55` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. | -| `50` | `TrustedStateBlock` | Счетчик trusted-связей. | +| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. | +| `70` | `TrustedStateBlock` | Счетчик trusted-связей. | | `255` | `ReservedBlock` | Зарезервировано, пока не используется. | Правила: @@ -280,7 +280,7 @@ AccessServersBlock ```text SessionsBlock -- block_type: u8 = 55 +- block_type: u8 = 50 - block_version: u8 = 0 - sessions_mode: u8 - sessions_count: u8 @@ -326,7 +326,7 @@ SessionRecord ```text TrustedStateBlock -- block_type: u8 = 50 +- block_type: u8 = 70 - block_version: u8 = 0 - trusted_count: u8 = 0 ``` diff --git a/shine-solana/shine/doc/programs/shine_login_guard.md b/shine-solana/shine/doc/programs/shine_login_guard.md index 94b3dd2..32135df 100644 --- a/shine-solana/shine/doc/programs/shine_login_guard.md +++ b/shine-solana/shine/doc/programs/shine_login_guard.md @@ -61,11 +61,19 @@ - `classify_login(login: String)` +Бинарный ABI инструкции: + +```text +- tag: u8 = 1 +- login_len: u32 LE +- login_bytes[login_len]: UTF-8 +``` + Аккаунты: -- signer +- аккаунты не требуются -Signer здесь технический и нужен как согласованный интерфейс вызова; бизнес-смысл подписи отсутствует. +Подпись транзакции для самой классификации не имеет бизнес-смысла. Если вызов идёт через CPI из `shine_users`, программа `shine_login_guard` не требует signer-аккаунтов вообще. ## 7. Выходные классы diff --git a/shine-solana/shine/doc/programs/shine_users.md b/shine-solana/shine/doc/programs/shine_users.md index be2ff35..3eca818 100644 --- a/shine-solana/shine/doc/programs/shine_users.md +++ b/shine-solana/shine/doc/programs/shine_users.md @@ -52,26 +52,26 @@ Пользовательская запись строится так: -- seed prefix: `login=` +- seed prefix: `user_login=` - второй seed: логин в нижнем регистре - program id: `shine_users` Формула: ```text -user_pda = PDA(["login=", lower(login)], shine_users_program_id) +user_pda = PDA(["user_login=", lower(login)], shine_users_program_id) ``` ### 3.2. Economy config PDA PDA экономических настроек: -- seed: `shine_users_economy_config` +- seed: `shine_users_economy_config_v2` Формула: ```text -users_economy_config_pda = PDA(["shine_users_economy_config"], shine_users_program_id) +users_economy_config_pda = PDA(["shine_users_economy_config_v2"], shine_users_program_id) ``` ## 4. Состояния программы @@ -105,8 +105,8 @@ users_economy_config_pda = PDA(["shine_users_economy_config"], shine_users_progr Базовые значения из текущей логики: -- seed `user_pda`: `login=` -- seed economy config: `shine_users_economy_config` +- seed `user_pda`: `user_login=` +- seed economy config: `shine_users_economy_config_v2` - стартовый размер `user_pda`: `768` байт - `LIMIT_STEP = 10_000` - `START_REGISTRATION_FEE_LAMPORTS = 10_000_000` @@ -199,9 +199,15 @@ On-chain инвариант только один: ### Правила - PDA должна ещё не существовать; -- адрес PDA обязан совпадать с seed `shine_users_economy_config`; +- адрес PDA обязан совпадать с seed `shine_users_economy_config_v2`; - в PDA записывается стартовый `UsersEconomyConfigState`. +### Бинарный ABI + +```text +- tag: u8 = 1 +``` + ## 9. Инструкция `update_users_economy_config` ### Назначение @@ -223,6 +229,15 @@ On-chain инвариант только один: - PDA должна существовать и принадлежать программе; - `lamports_per_limit_step > 0`. +### Бинарный ABI + +```text +- tag: u8 = 2 +- registration_fee_lamports: u64 LE +- lamports_per_limit_step: u64 LE +- start_bonus_limit: u64 LE +``` + ## 10. Инструкция `create_user_pda` ### Назначение @@ -259,6 +274,18 @@ On-chain инвариант только один: - mutable fields записи - `root` signature по unsigned части +### Бинарный ABI + +```text +- tag: u8 = 3 +- login: string_u8 +- root_key: [u8; 32] +- created_at_ms: u64 LE +- additional_limit: u64 LE +- fields: UserMutableFieldsV1 +- root_signature: [u8; 64] +``` + ### Обязательные проверки 1. Логин валиден по синтаксису. @@ -352,16 +379,20 @@ topup_fee = limit_fee(additional_limit) Если `additional_limit = 0`, доплата не требуется. -### Важная пометка о текущем состоянии +### Бинарный ABI -Логика `update_user_pda` в текущей Anchor-версии описана именно так и должна работать именно так. - -Но фактическая текущая Anchor-реализация на момент подготовки этого документа имеет runtime-проблемы в Solana BPF: - -- create-сценарий работает корректно; -- update-сценарий логически реализован, но требует исправления багов реализации. - -При переписи на чистый Rust нужно сохранять именно правила этого документа, а не привязываться к деталям текущей Anchor-реализации. +```text +- tag: u8 = 4 +- login: string_u8 +- root_key: [u8; 32] +- created_at_ms: u64 LE +- updated_at_ms: u64 LE +- version: u32 LE +- prev_hash: [u8; 32] +- additional_limit: u64 LE +- fields: UserMutableFieldsV1 +- root_signature: [u8; 64] +``` ## 12. Ed25519-проверки и порядок инструкций @@ -426,6 +457,36 @@ signature = Ed25519(blockchain_private_key, message_hash) ## 15. Правила серверных и сессионных полей +### UserMutableFieldsV1 + +```text +- device_key: [u8; 32] +- blockchain_public_key: [u8; 32] +- blockchain_name: string_u8 +- used_bytes: u64 LE +- last_block_number: u32 LE +- last_block_hash: [u8; 32] +- last_block_signature: [u8; 64] +- arweave_tx_id: string_u8 +- is_server: u8 +- if is_server = 1: + - address_format_type: u8 + - address_format_version: u8 + - server_address: string_u8 + - sync_servers_count: u8 + - sync_servers[sync_servers_count]: string_u8[] +- access_servers_count: u8 +- access_servers[access_servers_count]: string_u8[] +- sessions_mode: u8 +- sessions_count: u8 +- sessions[sessions_count]: + - session_type: u8 + - session_version: u8 + - session_name: string_u8 + - session_pub_key: [u8; 32] +- trusted_count: u8 +``` + ### Server profile Если `is_server = false`: @@ -483,7 +544,7 @@ signature = Ed25519(blockchain_private_key, message_hash) ## 18. Что должно сохраниться при переписи без Anchor -При переписи на чистый Rust нужно сохранить без изменений: +В текущей чисто-rust реализации уже сохранены: - те же PDA seed-правила; - тот же формат `user_pda`; @@ -493,9 +554,9 @@ signature = Ed25519(blockchain_private_key, message_hash) - ту же валидацию логина и CPI в `shine_login_guard`; - ту же зависимость от inflow vault программы `shine_payments`. -Что не обязано сохраниться: +Сознательно не сохранялись: - структура Anchor `Context`; -- Anchor discriminator'ы; -- внутренние helper-функции текущей реализации; -- текущие runtime-баги update-path. +- Anchor discriminator'ы и Anchor-ABI инструкций; +- старые seed'ы, которые конфликтовали с уже существующим Anchor-состоянием в devnet; +- внутренние helper-функции старой реализации. diff --git a/shine-solana/shine/programs/shine_login_guard/Cargo.toml b/shine-solana/shine/programs/shine_login_guard/Cargo.toml index 52206b0..25b325e 100644 --- a/shine-solana/shine/programs/shine_login_guard/Cargo.toml +++ b/shine-solana/shine/programs/shine_login_guard/Cargo.toml @@ -12,15 +12,9 @@ doctest = false bench = false [dependencies] -anchor-lang = "0.31.1" +solana-program = "2.1.21" [features] default = [] no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -anchor-debug = [] -custom-heap = [] -custom-panic = [] cpi = [] -idl-build = ["anchor-lang/idl-build"] diff --git a/shine-solana/shine/programs/shine_login_guard/src/lib.rs b/shine-solana/shine/programs/shine_login_guard/src/lib.rs index 212d540..91111a2 100644 --- a/shine-solana/shine/programs/shine_login_guard/src/lib.rs +++ b/shine-solana/shine/programs/shine_login_guard/src/lib.rs @@ -1,39 +1,83 @@ -use anchor_lang::prelude::*; -use anchor_lang::solana_program::program::set_return_data; - -declare_id!("3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo"); +use solana_program::{ + account_info::AccountInfo, + entrypoint, + entrypoint::ProgramResult, + program::set_return_data, + program_error::ProgramError, + pubkey::Pubkey, +}; mod wordlist { include!(concat!(env!("OUT_DIR"), "/generated_dictionary.rs")); } -const CLASS_FREE: u32 = 0; -const CLASS_PREMIUM: u32 = 1; -const CLASS_TRADEMARK: u32 = 2; -const MAX_WORDS_PER_LOGIN: usize = 3; +pub const INSTRUCTION_CLASSIFY_LOGIN: u8 = 1; +pub const CLASS_FREE: u32 = 0; +pub const CLASS_PREMIUM: u32 = 1; +pub const CLASS_TRADEMARK: u32 = 2; +pub const MAX_WORDS_PER_LOGIN: usize = 3; +pub const MAX_LOGIN_LEN: usize = 20; -#[program] -pub mod shine_login_guard { - use super::*; +solana_program::declare_id!("3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo"); - pub fn classify_login(_ctx: Context, login: String) -> Result<()> { - let class = classify(&login); - set_return_data(&class.to_le_bytes()); - Ok(()) +entrypoint!(process_instruction); + +pub fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let class = match parse_instruction(instruction_data)? { + Instruction::ClassifyLogin { login } => classify(&login), + }; + set_return_data(&class.to_le_bytes()); + Ok(()) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Instruction { + ClassifyLogin { login: String }, +} + +fn parse_instruction(data: &[u8]) -> Result { + let (&tag, rest) = data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + match tag { + INSTRUCTION_CLASSIFY_LOGIN => { + let login = read_string_u32(rest)?; + Ok(Instruction::ClassifyLogin { login }) + } + _ => Err(ProgramError::InvalidInstructionData), } } -#[derive(Accounts)] -pub struct ClassifyLogin<'info> { - pub signer: Signer<'info>, +fn read_string_u32(data: &[u8]) -> Result { + if data.len() < 4 { + return Err(ProgramError::InvalidInstructionData); + } + let len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize; + let body = data + .get(4..4 + len) + .ok_or(ProgramError::InvalidInstructionData)?; + std::str::from_utf8(body) + .map(|s| s.to_string()) + .map_err(|_| ProgramError::InvalidInstructionData) } -fn classify(login: &str) -> u32 { +pub fn build_classify_login_instruction_data(login: &str) -> Vec { + let bytes = login.as_bytes(); + let mut out = Vec::with_capacity(1 + 4 + bytes.len()); + out.push(INSTRUCTION_CLASSIFY_LOGIN); + out.extend_from_slice(&(bytes.len() as u32).to_le_bytes()); + out.extend_from_slice(bytes); + out +} + +pub fn classify(login: &str) -> u32 { let Some(normalized) = normalize_login(login) else { return CLASS_PREMIUM; }; - // Сначала пытаемся классифицировать по словарям (в т.ч. trademark/company), - // и только если не нашли совпадений — применяем правило короткого логина. if let Some(v) = classify_split(&normalized) { return v; } @@ -44,7 +88,7 @@ fn classify(login: &str) -> u32 { } fn normalize_login(login: &str) -> Option { - if login.is_empty() || login.len() > 20 { + if login.is_empty() || login.len() > MAX_LOGIN_LEN { return None; } let mut out = String::with_capacity(login.len()); @@ -57,7 +101,7 @@ fn normalize_login(login: &str) -> Option { } out.push(ch.to_ascii_lowercase()); } - if out.is_empty() || out.len() > 20 { + if out.is_empty() || out.len() > MAX_LOGIN_LEN { return None; } Some(out) @@ -74,7 +118,7 @@ fn classify_split(login: &str) -> Option { if depth >= MAX_WORDS_PER_LOGIN { return None; } - let max_piece = rest.len().min(20); + let max_piece = rest.len().min(MAX_LOGIN_LEN); let mut premium_found = false; for i in 1..=max_piece { let candidate = &rest[..i]; @@ -87,7 +131,7 @@ fn classify_split(login: &str) -> Option { Some(CLASS_TRADEMARK) => return Some(CLASS_TRADEMARK), Some(CLASS_PREMIUM) => premium_found = true, _ => {} - }; + } } if premium_found { Some(CLASS_PREMIUM) @@ -105,3 +149,35 @@ fn is_premium_word(word: &str) -> bool { fn is_trademark_word(word: &str) -> bool { wordlist::TRADEMARK_WORDS.binary_search(&word).is_ok() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalize_removes_underscores_and_lowercases() { + assert_eq!(normalize_login("Ab_C1"), Some("abc1".to_string())); + } + + #[test] + fn normalize_rejects_invalid_chars() { + assert_eq!(normalize_login("ab-c"), None); + } + + #[test] + fn short_unknown_login_is_premium() { + assert_eq!(classify("abc123"), CLASS_PREMIUM); + } + + #[test] + fn long_unknown_login_is_free() { + assert_eq!(classify("abcdefgh9"), CLASS_FREE); + } + + #[test] + fn instruction_roundtrip() { + let data = build_classify_login_instruction_data("Alice_1"); + let parsed = parse_instruction(&data).unwrap(); + assert_eq!(parsed, Instruction::ClassifyLogin { login: "Alice_1".to_string() }); + } +} diff --git a/shine-solana/shine/programs/shine_users/Cargo.toml b/shine-solana/shine/programs/shine_users/Cargo.toml index 87b5c0f..7752a3f 100644 --- a/shine-solana/shine/programs/shine_users/Cargo.toml +++ b/shine-solana/shine/programs/shine_users/Cargo.toml @@ -12,18 +12,10 @@ doctest = false bench = false [dependencies] -anchor-lang = "0.31.1" -common = { path = "../common" } -shine_login_guard = { path = "../shine_login_guard", features = ["cpi", "no-entrypoint"] } - +solana-program = "2.1.21" [features] default = [] no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -anchor-debug = [] custom-heap = [] custom-panic = [] -cpi = [] -idl-build = ["anchor-lang/idl-build"] diff --git a/shine-solana/shine/programs/shine_users/src/lib.rs b/shine-solana/shine/programs/shine_users/src/lib.rs index 0ba5e32..11d7ad1 100644 --- a/shine-solana/shine/programs/shine_users/src/lib.rs +++ b/shine-solana/shine/programs/shine_users/src/lib.rs @@ -1,32 +1,1035 @@ -use anchor_lang::prelude::*; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint, + entrypoint::ProgramResult, + hash::hashv, + instruction::Instruction, + program::{get_return_data, invoke, invoke_signed}, + program_error::ProgramError, + program_memory::sol_memcmp, + pubkey::Pubkey, + rent::Rent, + system_instruction, + system_program, + sysvar::{instructions::{load_current_index_checked, load_instruction_at_checked}, Sysvar}, +}; +use std::{convert::TryFrom, str::FromStr}; pub mod settings; -pub mod users; -use users::*; +solana_program::declare_id!("FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm"); +entrypoint!(process_instruction); -declare_id!("FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm"); +const MAGIC: &[u8; 5] = b"SHiNE"; +const FORMAT_MAJOR: u8 = 1; +const FORMAT_MINOR: u8 = 0; +const MAX_SYNC_SERVERS: usize = 32; +const MAX_SESSIONS: usize = 64; +const MAX_SESSION_NAME_LEN: usize = 64; +const MAX_AUTO_REALLOC_INCREASE: usize = 10_000; +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_SESSIONS: u8 = 50; +const BLOCK_TYPE_TRUSTED_STATE: u8 = 70; +const BLOCK_VERSION_0: u8 = 0; +const BLOCKCHAIN_TYPE_MAIN_USER: u8 = 1; +const SESSIONS_MODE_MIXED: u8 = 1; +const SESSIONS_MODE_PDA_ONLY: u8 = 10; +const SESSION_TYPE_USER: u8 = 1; +const SESSION_TYPE_SUBSERVER: u8 = 100; +const LAST_BLOCK_STATE_PREFIX: &[u8] = b"SHiNE_LAST_BLOCK"; -#[program] -pub mod shine { - use super::*; +const IX_INIT_USERS_ECONOMY_CONFIG: u8 = 1; +const IX_UPDATE_USERS_ECONOMY_CONFIG: u8 = 2; +const IX_CREATE_USER_PDA: u8 = 3; +const IX_UPDATE_USER_PDA: u8 = 4; +const LOGIN_GUARD_IX_CLASSIFY_LOGIN: u8 = 1; - pub fn init_users_economy_config(ctx: Context) -> Result<()> { - users::init_users_economy_config(ctx) - } +#[repr(u32)] +#[derive(Clone, Copy, Debug)] +enum ShineUsersError { + InvalidInstruction = 1, + InvalidSigner = 2, + InvalidPdaAddress = 3, + UserAlreadyExists = 4, + EmptyPdaData = 5, + InvalidRecordData = 6, + InvalidRecordMagic = 7, + InvalidRecordFormat = 8, + InvalidRecordLength = 9, + InvalidLogin = 10, + InvalidLimitIncrement = 11, + InvalidFeeReceiver = 12, + InvalidLoginGuardResponse = 13, + PremiumLogin = 14, + TrademarkLoginRequiresReview = 15, + InvalidVersion = 16, + InvalidPrevHash = 17, + ImmutableFieldChanged = 18, + BalanceDecrease = 19, + InvalidSignature = 20, + RecordTooLarge = 21, + MathOverflow = 22, + SystemAlreadyInitialized = 23, + MissingRequiredSignature = 24, + InvalidSystemProgram = 25, + InvalidAccountOwner = 26, + InvalidAccountData = 27, +} - pub fn update_users_economy_config( - ctx: Context, - args: UpdateUsersEconomyConfigArgs, - ) -> Result<()> { - users::update_users_economy_config(ctx, args) - } - - pub fn create_user_pda(ctx: Context, args: CreateUserPdaArgs) -> Result<()> { - users::create_user_pda(ctx, args) - } - - pub fn update_user_pda(ctx: Context, args: UpdateUserPdaArgs) -> Result<()> { - users::update_user_pda(ctx, args) +impl From for ProgramError { + fn from(value: ShineUsersError) -> Self { + ProgramError::Custom(value as u32) } } + +macro_rules! require { + ($cond:expr, $err:expr) => { + if !($cond) { + return Err(ProgramError::from($err)); + } + }; +} + +macro_rules! require_keys_eq { + ($left:expr, $right:expr, $err:expr) => { + if $left != $right { + return Err(ProgramError::from($err)); + } + }; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SessionRecord { + pub session_type: u8, + pub session_version: u8, + pub session_name: String, + pub session_pub_key: Pubkey, +} + +#[derive(Clone, Debug)] +pub struct UserMutableFields { + pub device_key: Pubkey, + pub blockchain_public_key: Pubkey, + pub blockchain_name: String, + 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, + pub is_server: bool, + pub address_format_type: u8, + pub address_format_version: u8, + pub server_address: String, + pub sync_servers: Vec, + pub access_servers: Vec, + pub sessions_mode: u8, + pub sessions: Vec, + pub trusted_count: u8, +} + +#[derive(Clone, Debug)] +pub struct CreateUserPdaArgs { + pub login: String, + pub root_key: Pubkey, + pub created_at_ms: u64, + pub additional_limit: u64, + pub fields: UserMutableFields, + pub signature: [u8; 64], +} + +#[derive(Clone, Debug)] +pub struct UpdateUserPdaArgs { + pub login: String, + pub root_key: Pubkey, + pub created_at_ms: u64, + pub updated_at_ms: u64, + pub version: u32, + pub prev_hash: [u8; 32], + pub additional_limit: u64, + pub fields: UserMutableFields, + pub signature: [u8; 64], +} + +#[derive(Clone, Debug)] +pub struct UpdateUsersEconomyConfigArgs { + pub registration_fee_lamports: u64, + pub lamports_per_limit_step: u64, + pub start_bonus_limit: u64, +} + +#[derive(Clone, Debug)] +pub struct UsersEconomyConfigState { + pub version: u8, + pub registration_fee_lamports: u64, + pub lamports_per_limit_step: u64, + pub start_bonus_limit: u64, +} + +#[derive(Clone, Debug)] +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(Clone, Debug)] +pub struct UserRecord { + pub created_at_ms: u64, + pub updated_at_ms: u64, + pub record_number: u32, + pub prev_record_hash: [u8; 32], + pub login: String, + pub root_key: Pubkey, + pub device_key: Pubkey, + pub blockchain: BlockchainRecord, + pub is_server: bool, + pub address_format_type: u8, + pub address_format_version: u8, + pub server_address: String, + pub sync_servers: Vec, + pub access_servers: Vec, + pub sessions_mode: u8, + pub sessions: Vec, + pub trusted_count: u8, + pub signature: [u8; 64], +} + +struct Reader<'a> { + data: &'a [u8], + cursor: usize, +} + +impl<'a> Reader<'a> { + fn new(data: &'a [u8]) -> Self { Self { data, cursor: 0 } } + fn read_u8(&mut self) -> Result { + let v = *self.data.get(self.cursor).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + self.cursor += 1; + Ok(v) + } + fn read_u32(&mut self) -> Result { + let end = self.cursor.checked_add(4).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + self.cursor = end; + Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]])) + } + fn read_u64(&mut self) -> Result { + let end = self.cursor.checked_add(8).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + self.cursor = end; + Ok(u64::from_le_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])) + } + fn read_fixed_32(&mut self) -> Result<[u8; 32], ProgramError> { + let end = self.cursor.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + self.cursor = end; + <[u8; 32]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction)) + } + fn read_fixed_64(&mut self) -> Result<[u8; 64], ProgramError> { + let end = self.cursor.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + self.cursor = end; + <[u8; 64]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction)) + } + fn read_pubkey(&mut self) -> Result { + Ok(Pubkey::new_from_array(self.read_fixed_32()?)) + } + fn read_string_u8(&mut self) -> Result { + let len = self.read_u8()? as usize; + let end = self.cursor.checked_add(len).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; + self.cursor = end; + std::str::from_utf8(s).map(|v| v.to_string()).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction)) + } + fn finish(self) -> Result<(), ProgramError> { + require!(self.cursor == self.data.len(), ShineUsersError::InvalidInstruction); + Ok(()) + } +} + +fn process_instruction(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + let mut r = Reader::new(instruction_data); + let tag = r.read_u8()?; + match tag { + IX_INIT_USERS_ECONOMY_CONFIG => { + r.finish()?; + process_init_users_economy_config(program_id, accounts) + } + IX_UPDATE_USERS_ECONOMY_CONFIG => { + let args = UpdateUsersEconomyConfigArgs { + registration_fee_lamports: r.read_u64()?, + lamports_per_limit_step: r.read_u64()?, + start_bonus_limit: r.read_u64()?, + }; + r.finish()?; + process_update_users_economy_config(program_id, accounts, args) + } + IX_CREATE_USER_PDA => { + let args = parse_create_args(&mut r)?; + r.finish()?; + process_create_user_pda(program_id, accounts, args) + } + IX_UPDATE_USER_PDA => { + let args = parse_update_args(&mut r)?; + r.finish()?; + process_update_user_pda(program_id, accounts, args) + } + _ => Err(ProgramError::from(ShineUsersError::InvalidInstruction)), + } +} + +fn parse_create_args(r: &mut Reader<'_>) -> Result { + Ok(CreateUserPdaArgs { + login: r.read_string_u8()?, + root_key: r.read_pubkey()?, + created_at_ms: r.read_u64()?, + additional_limit: r.read_u64()?, + fields: parse_fields(r)?, + signature: r.read_fixed_64()?, + }) +} + +fn parse_update_args(r: &mut Reader<'_>) -> Result { + Ok(UpdateUserPdaArgs { + login: r.read_string_u8()?, + root_key: r.read_pubkey()?, + created_at_ms: r.read_u64()?, + updated_at_ms: r.read_u64()?, + version: r.read_u32()?, + prev_hash: r.read_fixed_32()?, + additional_limit: r.read_u64()?, + fields: parse_fields(r)?, + signature: r.read_fixed_64()?, + }) +} + +fn parse_fields(r: &mut Reader<'_>) -> Result { + let device_key = r.read_pubkey()?; + let blockchain_public_key = r.read_pubkey()?; + let blockchain_name = r.read_string_u8()?; + let used_bytes = r.read_u64()?; + let last_block_number = r.read_u32()?; + let last_block_hash = r.read_fixed_32()?; + let last_block_signature = r.read_fixed_64()?; + let arweave_tx_id = r.read_string_u8()?; + let is_server = r.read_u8()? == 1; + let (address_format_type, address_format_version, server_address, sync_servers) = if is_server { + let aft = r.read_u8()?; + let afv = r.read_u8()?; + let addr = r.read_string_u8()?; + let cnt = r.read_u8()? as usize; + let mut list = Vec::with_capacity(cnt); + for _ in 0..cnt { list.push(r.read_string_u8()?); } + (aft, afv, addr, list) + } else { + (0, 0, String::new(), Vec::new()) + }; + let access_count = r.read_u8()? as usize; + let mut access_servers = Vec::with_capacity(access_count); + for _ in 0..access_count { access_servers.push(r.read_string_u8()?); } + let sessions_mode = r.read_u8()?; + let sessions_count = r.read_u8()? as usize; + let mut sessions = Vec::with_capacity(sessions_count); + for _ in 0..sessions_count { + sessions.push(SessionRecord { + session_type: r.read_u8()?, + session_version: r.read_u8()?, + session_name: r.read_string_u8()?, + session_pub_key: r.read_pubkey()?, + }); + } + let trusted_count = r.read_u8()?; + Ok(UserMutableFields { + device_key, + blockchain_public_key, + blockchain_name, + used_bytes, + last_block_number, + last_block_hash, + last_block_signature, + arweave_tx_id, + is_server, + address_format_type, + address_format_version, + server_address, + sync_servers, + access_servers, + sessions_mode, + sessions, + trusted_count, + }) +} + +fn process_init_users_economy_config(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let mut it = accounts.iter(); + let signer = next_account_info(&mut it)?; + let users_economy_config_pda = next_account_info(&mut it)?; + let system_program_ai = next_account_info(&mut it)?; + + require!(signer.is_signer, ShineUsersError::InvalidSigner); + require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram); + + let (expected_pda, bump) = find_users_economy_config_pda(program_id); + require_keys_eq!(expected_pda, *users_economy_config_pda.key, ShineUsersError::InvalidPdaAddress); + require!(users_economy_config_pda.owner == &system_program::id(), ShineUsersError::SystemAlreadyInitialized); + require!(users_economy_config_pda.data_is_empty(), ShineUsersError::SystemAlreadyInitialized); + + let state = UsersEconomyConfigState { + version: 1, + registration_fee_lamports: settings::START_REGISTRATION_FEE_LAMPORTS, + lamports_per_limit_step: settings::START_LAMPORTS_PER_LIMIT_STEP, + start_bonus_limit: settings::START_BONUS_LIMIT, + }; + let data = serialize_users_economy_config(&state); + create_pda_account( + signer, + users_economy_config_pda, + system_program_ai, + program_id, + &[settings::USERS_ECONOMY_CONFIG_SEED, &[bump]], + data.len(), + )?; + write_pda_exact(users_economy_config_pda, &data)?; + Ok(()) +} + +fn process_update_users_economy_config(program_id: &Pubkey, accounts: &[AccountInfo], args: UpdateUsersEconomyConfigArgs) -> ProgramResult { + let mut it = accounts.iter(); + let signer = next_account_info(&mut it)?; + let users_economy_config_pda = next_account_info(&mut it)?; + + require!(signer.is_signer, ShineUsersError::InvalidSigner); + let dao_authority = Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| ProgramError::from(ShineUsersError::InvalidSigner))?; + require_keys_eq!(dao_authority, *signer.key, ShineUsersError::InvalidSigner); + let (expected_pda, _) = find_users_economy_config_pda(program_id); + require_keys_eq!(expected_pda, *users_economy_config_pda.key, ShineUsersError::InvalidPdaAddress); + require!(users_economy_config_pda.owner == program_id, ShineUsersError::InvalidPdaAddress); + require!(args.lamports_per_limit_step > 0, ShineUsersError::InvalidRecordData); + + let mut state = read_users_economy_config(users_economy_config_pda)?; + state.registration_fee_lamports = args.registration_fee_lamports; + state.lamports_per_limit_step = args.lamports_per_limit_step; + state.start_bonus_limit = args.start_bonus_limit; + write_pda_exact(users_economy_config_pda, &serialize_users_economy_config(&state))?; + Ok(()) +} + +fn process_create_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: CreateUserPdaArgs) -> ProgramResult { + let mut it = accounts.iter(); + let signer = next_account_info(&mut it)?; + let user_pda = next_account_info(&mut it)?; + let system_program_ai = next_account_info(&mut it)?; + let inflow_vault = next_account_info(&mut it)?; + let instructions_sysvar = next_account_info(&mut it)?; + let users_economy_config_pda = next_account_info(&mut it)?; + let login_guard_program = next_account_info(&mut it)?; + + require!(signer.is_signer, ShineUsersError::InvalidSigner); + require_keys_eq!(*signer.key, args.fields.device_key, ShineUsersError::InvalidSigner); + require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram); + + validate_login(&args.login)?; + validate_fields(&args.fields)?; + validate_inflow_vault(inflow_vault)?; + require!(args.additional_limit % settings::LIMIT_STEP == 0, ShineUsersError::InvalidLimitIncrement); + require_keys_eq!(*login_guard_program.key, Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID).map_err(|_| ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?, ShineUsersError::InvalidLoginGuardResponse); + classify_login_or_fail(login_guard_program, &args.login)?; + + let economy = read_users_economy_config(users_economy_config_pda)?; + let login_seed = login_seed_normalized(&args.login); + let (expected_pda, bump) = find_user_pda(program_id, &login_seed); + require_keys_eq!(expected_pda, *user_pda.key, ShineUsersError::InvalidPdaAddress); + require!(user_pda.owner == &system_program::id(), ShineUsersError::UserAlreadyExists); + require!(user_pda.data_is_empty(), ShineUsersError::UserAlreadyExists); + + let start_balance = economy.start_bonus_limit.checked_add(args.additional_limit).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; + let mut record = UserRecord { + created_at_ms: args.created_at_ms, + updated_at_ms: args.created_at_ms, + record_number: 0, + prev_record_hash: ZERO_HASH, + login: args.login, + root_key: args.root_key, + device_key: args.fields.device_key, + blockchain: BlockchainRecord { + blockchain_type: BLOCKCHAIN_TYPE_MAIN_USER, + blockchain_name: args.fields.blockchain_name, + 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: args.fields.last_block_hash, + last_block_signature: args.fields.last_block_signature, + arweave_tx_id: args.fields.arweave_tx_id, + }, + is_server: args.fields.is_server, + address_format_type: args.fields.address_format_type, + address_format_version: args.fields.address_format_version, + server_address: args.fields.server_address, + sync_servers: args.fields.sync_servers, + access_servers: args.fields.access_servers, + sessions_mode: args.fields.sessions_mode, + sessions: args.fields.sessions, + trusted_count: args.fields.trusted_count, + signature: [0; 64], + }; + validate_blockchain_limits(&record.blockchain, 0, 0, true)?; + verify_last_block_state_signature(instructions_sysvar, &record)?; + let unsigned = serialize_unsigned_record(&record)?; + let unsigned_hash = hashv(&[&unsigned]); + record.signature = verify_record_signature_hash(instructions_sysvar, &record.root_key, &args.signature, unsigned_hash.as_ref())?; + + let serialized = pad_to_fixed_size(serialize_full_record(&record)?, settings::USER_PDA_SPACE)?; + create_pda_account( + signer, + user_pda, + system_program_ai, + program_id, + &[settings::USER_PDA_SEED_PREFIX.as_bytes(), login_seed.as_bytes(), &[bump]], + settings::USER_PDA_SPACE, + )?; + write_pda_exact(user_pda, &serialized)?; + + let total_fee = economy.registration_fee_lamports.checked_add(limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; + transfer_lamports(signer, inflow_vault, system_program_ai, total_fee)?; + Ok(()) +} + +fn process_update_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: UpdateUserPdaArgs) -> ProgramResult { + let mut it = accounts.iter(); + let signer = next_account_info(&mut it)?; + let user_pda = next_account_info(&mut it)?; + let system_program_ai = next_account_info(&mut it)?; + let inflow_vault = next_account_info(&mut it)?; + let instructions_sysvar = next_account_info(&mut it)?; + let users_economy_config_pda = next_account_info(&mut it)?; + + require!(signer.is_signer, ShineUsersError::InvalidSigner); + require_keys_eq!(*signer.key, args.fields.device_key, ShineUsersError::InvalidSigner); + require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram); + + validate_login(&args.login)?; + validate_fields(&args.fields)?; + validate_inflow_vault(inflow_vault)?; + require!(args.additional_limit % settings::LIMIT_STEP == 0, ShineUsersError::InvalidLimitIncrement); + let economy = read_users_economy_config(users_economy_config_pda)?; + + let normalized_login = login_seed_normalized(&args.login); + require_keys_eq!(find_user_pda(program_id, &normalized_login).0, *user_pda.key, ShineUsersError::InvalidPdaAddress); + require!(user_pda.owner == program_id, ShineUsersError::InvalidPdaAddress); + + let old_record = deserialize_record_from_pda(&read_pda_all(user_pda)?)?; + require!(old_record.login == args.login, ShineUsersError::ImmutableFieldChanged); + require!(old_record.created_at_ms == args.created_at_ms, ShineUsersError::ImmutableFieldChanged); + require_keys_eq!(old_record.root_key, args.root_key, ShineUsersError::ImmutableFieldChanged); + require!(args.version == old_record.record_number.saturating_add(1), ShineUsersError::InvalidVersion); + require!(hash_unsigned_record(&old_record)? == args.prev_hash, ShineUsersError::InvalidPrevHash); + require!(args.fields.blockchain_name == old_record.blockchain.blockchain_name, ShineUsersError::ImmutableFieldChanged); + require_keys_eq!(args.fields.blockchain_public_key, old_record.blockchain.blockchain_public_key, ShineUsersError::ImmutableFieldChanged); + + let new_balance = old_record.blockchain.paid_limit_bytes.checked_add(args.additional_limit).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; + require!(new_balance >= old_record.blockchain.paid_limit_bytes, ShineUsersError::BalanceDecrease); + + let blockchain_state_unchanged = old_record.blockchain.used_bytes == args.fields.used_bytes + && old_record.blockchain.last_block_number == args.fields.last_block_number + && old_record.blockchain.last_block_hash == args.fields.last_block_hash + && old_record.blockchain.last_block_signature == args.fields.last_block_signature + && old_record.blockchain.arweave_tx_id == args.fields.arweave_tx_id; + + let mut new_record = build_update_record(&old_record, &args, new_balance)?; + validate_blockchain_limits(&new_record.blockchain, old_record.blockchain.used_bytes, old_record.blockchain.last_block_number, false)?; + if !blockchain_state_unchanged { + verify_last_block_state_signature(instructions_sysvar, &new_record)?; + } + let unsigned = serialize_unsigned_record(&new_record)?; + let unsigned_hash = hashv(&[&unsigned]); + new_record.signature = verify_record_signature_hash(instructions_sysvar, &old_record.root_key, &args.signature, unsigned_hash.as_ref())?; + + let serialized = serialize_full_record(&new_record)?; + ensure_pda_size_and_rent(user_pda, signer, system_program_ai, serialized.len())?; + write_pda_prefix(user_pda, &serialized)?; + + let topup_fee = limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?; + if topup_fee > 0 { + transfer_lamports(signer, inflow_vault, system_program_ai, topup_fee)?; + } + Ok(()) +} + +fn build_update_record(old_record: &UserRecord, args: &UpdateUserPdaArgs, new_balance: u64) -> Result { + Ok(UserRecord { + created_at_ms: old_record.created_at_ms, + updated_at_ms: args.updated_at_ms, + record_number: args.version, + prev_record_hash: args.prev_hash, + login: old_record.login.clone(), + root_key: old_record.root_key, + device_key: args.fields.device_key, + blockchain: BlockchainRecord { + blockchain_type: old_record.blockchain.blockchain_type, + blockchain_name: args.fields.blockchain_name.clone(), + blockchain_public_key: args.fields.blockchain_public_key, + paid_limit_bytes: new_balance, + used_bytes: args.fields.used_bytes, + last_block_number: args.fields.last_block_number, + last_block_hash: args.fields.last_block_hash, + last_block_signature: args.fields.last_block_signature, + arweave_tx_id: args.fields.arweave_tx_id.clone(), + }, + is_server: args.fields.is_server, + address_format_type: args.fields.address_format_type, + address_format_version: args.fields.address_format_version, + server_address: args.fields.server_address.clone(), + sync_servers: args.fields.sync_servers.clone(), + access_servers: args.fields.access_servers.clone(), + sessions_mode: args.fields.sessions_mode, + sessions: args.fields.sessions.clone(), + trusted_count: args.fields.trusted_count, + signature: [0; 64], + }) +} + +fn classify_login_or_fail(login_guard_program: &AccountInfo, login: &str) -> ProgramResult { + let login_guard_program_id = Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID) + .map_err(|_| ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?; + require_keys_eq!(*login_guard_program.key, login_guard_program_id, ShineUsersError::InvalidLoginGuardResponse); + let mut data = Vec::with_capacity(1 + 4 + login.len()); + data.push(LOGIN_GUARD_IX_CLASSIFY_LOGIN); + data.extend_from_slice(&(login.len() as u32).to_le_bytes()); + data.extend_from_slice(login.as_bytes()); + let ix = Instruction { program_id: login_guard_program_id, accounts: vec![], data }; + invoke(&ix, &[login_guard_program.clone()])?; + let (program_id, raw) = get_return_data().ok_or(ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?; + require_keys_eq!(program_id, login_guard_program_id, ShineUsersError::InvalidLoginGuardResponse); + require!(raw.len() == 4, ShineUsersError::InvalidLoginGuardResponse); + let class = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]); + match class { + 0 => Ok(()), + 1 => Err(ProgramError::from(ShineUsersError::PremiumLogin)), + 2 => Err(ProgramError::from(ShineUsersError::TrademarkLoginRequiresReview)), + _ => Err(ProgramError::from(ShineUsersError::InvalidLoginGuardResponse)), + } +} + +fn serialize_users_economy_config(state: &UsersEconomyConfigState) -> Vec { + let mut out = Vec::with_capacity(1 + 8 + 8 + 8); + out.push(state.version); + out.extend_from_slice(&state.registration_fee_lamports.to_le_bytes()); + out.extend_from_slice(&state.lamports_per_limit_step.to_le_bytes()); + out.extend_from_slice(&state.start_bonus_limit.to_le_bytes()); + out +} + +fn read_users_economy_config(pda: &AccountInfo) -> Result { + let raw = read_pda_all(pda)?; + require!(!raw.is_empty(), ShineUsersError::EmptyPdaData); + require!(raw.len() >= 25, ShineUsersError::InvalidAccountData); + Ok(UsersEconomyConfigState { + version: raw[0], + registration_fee_lamports: u64::from_le_bytes(raw[1..9].try_into().unwrap()), + lamports_per_limit_step: u64::from_le_bytes(raw[9..17].try_into().unwrap()), + start_bonus_limit: u64::from_le_bytes(raw[17..25].try_into().unwrap()), + }) +} + +fn deserialize_record_from_pda(raw: &[u8]) -> Result { + require!(raw.len() >= 9, ShineUsersError::InvalidRecordData); + require!(sol_memcmp(&raw[0..5], MAGIC, 5) == 0, ShineUsersError::InvalidRecordMagic); + require!(raw[5] == FORMAT_MAJOR && raw[6] == FORMAT_MINOR, ShineUsersError::InvalidRecordFormat); + let record_len = u16::from_le_bytes([raw[7], raw[8]]) as usize; + require!(record_len >= 9 + 64, ShineUsersError::InvalidRecordLength); + require!(record_len <= raw.len(), ShineUsersError::InvalidRecordLength); + let useful = &raw[..record_len]; + let mut cursor = 9usize; + + let created_at_ms = read_u64_from(useful, &mut cursor)?; + let updated_at_ms = read_u64_from(useful, &mut cursor)?; + let record_number = read_u32_from(useful, &mut cursor)?; + let prev_record_hash = read_fixed_32_from(useful, &mut cursor)?; + let login = read_len_prefixed_string_from(useful, &mut cursor)?; + + let blocks_count = read_u8_from(useful, &mut cursor)? as usize; + let mut root_key = None; + let mut device_key = None; + let mut blockchain = None; + let mut is_server = false; + let mut address_format_type = 0u8; + let mut address_format_version = 0u8; + let mut server_address = String::new(); + let mut sync_servers = Vec::new(); + let mut access_servers = Vec::new(); + let mut sessions_mode = SESSIONS_MODE_MIXED; + let mut sessions = Vec::new(); + let mut trusted_count = 0u8; + + for _ in 0..blocks_count { + let block_type = read_u8_from(useful, &mut cursor)?; + let block_version = read_u8_from(useful, &mut cursor)?; + require!(block_version == BLOCK_VERSION_0, ShineUsersError::InvalidRecordFormat); + match block_type { + BLOCK_TYPE_ROOT_KEY => { + require!(root_key.is_none(), ShineUsersError::InvalidRecordData); + root_key = Some(Pubkey::new_from_array(read_fixed_32_from(useful, &mut cursor)?)); + } + BLOCK_TYPE_DEVICE_KEY => { + require!(device_key.is_none(), ShineUsersError::InvalidRecordData); + device_key = Some(Pubkey::new_from_array(read_fixed_32_from(useful, &mut cursor)?)); + } + BLOCK_TYPE_BLOCKCHAIN_REGISTRY => { + require!(blockchain.is_none(), ShineUsersError::InvalidRecordData); + let count = read_u8_from(useful, &mut cursor)?; + require!(count == 1, ShineUsersError::InvalidRecordData); + blockchain = Some(read_blockchain_record(useful, &mut cursor)?); + } + BLOCK_TYPE_SERVER_PROFILE => { + require!(!is_server, ShineUsersError::InvalidRecordData); + is_server = read_u8_from(useful, &mut cursor)? == 1; + require!(is_server, ShineUsersError::InvalidRecordData); + address_format_type = read_u8_from(useful, &mut cursor)?; + address_format_version = read_u8_from(useful, &mut cursor)?; + server_address = read_len_prefixed_string_from(useful, &mut cursor)?; + let sync_count = read_u8_from(useful, &mut cursor)? as usize; + require!(sync_count <= MAX_SYNC_SERVERS, ShineUsersError::InvalidRecordData); + for _ in 0..sync_count { sync_servers.push(read_len_prefixed_string_from(useful, &mut cursor)?); } + } + BLOCK_TYPE_ACCESS_SERVERS => { + require!(access_servers.is_empty(), ShineUsersError::InvalidRecordData); + let access_count = read_u8_from(useful, &mut cursor)? as usize; + for _ in 0..access_count { access_servers.push(read_len_prefixed_string_from(useful, &mut cursor)?); } + } + BLOCK_TYPE_SESSIONS => { + require!(sessions.is_empty(), ShineUsersError::InvalidRecordData); + sessions_mode = read_u8_from(useful, &mut cursor)?; + let sessions_count = read_u8_from(useful, &mut cursor)? as usize; + require!(sessions_count <= MAX_SESSIONS, ShineUsersError::InvalidRecordData); + for _ in 0..sessions_count { sessions.push(read_session_record(useful, &mut cursor)?); } + } + BLOCK_TYPE_TRUSTED_STATE => { + trusted_count = read_u8_from(useful, &mut cursor)?; + } + _ => return Err(ProgramError::from(ShineUsersError::InvalidRecordFormat)), + } + } + validate_sessions_fields(sessions_mode, &sessions)?; + let signature = read_fixed_64_from(useful, &mut cursor)?; + require!(cursor == useful.len(), ShineUsersError::InvalidRecordLength); + + Ok(UserRecord { + created_at_ms, + updated_at_ms, + record_number, + prev_record_hash, + login, + root_key: root_key.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?, + device_key: device_key.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?, + blockchain: blockchain.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?, + is_server, + address_format_type, + address_format_version, + server_address, + sync_servers, + access_servers, + sessions_mode, + sessions, + trusted_count, + signature, + }) +} + +fn read_blockchain_record(data: &[u8], cursor: &mut usize) -> Result { + let blockchain_type = read_u8_from(data, cursor)?; + require!(blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER, ShineUsersError::InvalidRecordData); + let blockchain_name = read_len_prefixed_string_from(data, cursor)?; + let blockchain_public_key = Pubkey::new_from_array(read_fixed_32_from(data, cursor)?); + let paid_limit_bytes = read_u64_from(data, cursor)?; + let used_bytes = read_u64_from(data, cursor)?; + let last_block_number = read_u32_from(data, cursor)?; + let last_block_hash = read_fixed_32_from(data, cursor)?; + let last_block_signature = read_fixed_64_from(data, cursor)?; + let arweave_present = read_u8_from(data, cursor)?; + let arweave_tx_id = match arweave_present { 0 => String::new(), 1 => read_len_prefixed_string_from(data, cursor)?, _ => return Err(ProgramError::from(ShineUsersError::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 read_session_record(data: &[u8], cursor: &mut usize) -> Result { + Ok(SessionRecord { + session_type: read_u8_from(data, cursor)?, + session_version: read_u8_from(data, cursor)?, + session_name: read_len_prefixed_string_from(data, cursor)?, + session_pub_key: Pubkey::new_from_array(read_fixed_32_from(data, cursor)?), + }) +} + +fn serialize_unsigned_record(record: &UserRecord) -> Result, ProgramError> { + let login_bytes = record.login.as_bytes(); + require!(login_bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidLogin); + let mut out = Vec::new(); + out.extend_from_slice(MAGIC); + out.push(FORMAT_MAJOR); + out.push(FORMAT_MINOR); + out.extend_from_slice(&0u16.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.record_number.to_le_bytes()); + out.extend_from_slice(&record.prev_record_hash); + out.push(login_bytes.len() as u8); + out.extend_from_slice(login_bytes); + + let blocks_count = if record.is_server { 7 } else { 6 }; + out.push(blocks_count); + write_root_key_block(&mut out, record); + write_device_key_block(&mut out, record); + write_blockchain_registry_block(&mut out, &record.blockchain)?; + if record.is_server { write_server_profile_block(&mut out, record)?; } + write_access_servers_block(&mut out, record)?; + write_sessions_block(&mut out, record)?; + write_trusted_state_block(&mut out, record); + + let record_len = out.len().checked_add(64).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; + require!(record_len <= u16::MAX as usize, ShineUsersError::RecordTooLarge); + let len_bytes = (record_len as u16).to_le_bytes(); + out[7] = len_bytes[0]; + out[8] = len_bytes[1]; + Ok(out) +} + +fn serialize_full_record(record: &UserRecord) -> Result, ProgramError> { + let mut out = serialize_unsigned_record(record)?; + out.extend_from_slice(&record.signature); + Ok(out) +} + +fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32], ProgramError> { + let unsigned = serialize_unsigned_record(record)?; + let digest = hashv(&[&unsigned]); + let mut out = [0u8; 32]; + out.copy_from_slice(digest.as_ref()); + Ok(out) +} + +fn write_root_key_block(out: &mut Vec, 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, 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, blockchain: &BlockchainRecord) -> Result<(), ProgramError> { out.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY); out.push(BLOCK_VERSION_0); out.push(1); write_blockchain_record(out, blockchain) } +fn write_blockchain_record(out: &mut Vec, blockchain: &BlockchainRecord) -> Result<(), ProgramError> { + 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, record: &UserRecord) -> Result<(), ProgramError> { + out.push(BLOCK_TYPE_SERVER_PROFILE); out.push(BLOCK_VERSION_0); out.push(1); out.push(record.address_format_type); out.push(record.address_format_version); write_len_prefixed_string(out, &record.server_address)?; require!(record.sync_servers.len() <= MAX_SYNC_SERVERS, ShineUsersError::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, record: &UserRecord) -> Result<(), ProgramError> { + out.push(BLOCK_TYPE_ACCESS_SERVERS); out.push(BLOCK_VERSION_0); require!(record.access_servers.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); out.push(record.access_servers.len() as u8); for login in &record.access_servers { write_len_prefixed_string(out, login)?; } Ok(()) +} +fn write_sessions_block(out: &mut Vec, record: &UserRecord) -> Result<(), ProgramError> { + out.push(BLOCK_TYPE_SESSIONS); out.push(BLOCK_VERSION_0); out.push(record.sessions_mode); require!(record.sessions.len() <= MAX_SESSIONS, ShineUsersError::InvalidRecordData); out.push(record.sessions.len() as u8); for session in &record.sessions { out.push(session.session_type); out.push(session.session_version); write_len_prefixed_string(out, &session.session_name)?; out.extend_from_slice(session.session_pub_key.as_ref()); } Ok(()) +} +fn write_trusted_state_block(out: &mut Vec, 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, value: &str) -> Result<(), ProgramError> { let bytes = value.as_bytes(); require!(bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); out.push(bytes.len() as u8); out.extend_from_slice(bytes); Ok(()) } + +fn verify_record_signature_hash(instructions_sysvar: &AccountInfo, root_key: &Pubkey, signature: &[u8; 64], message_hash: &[u8]) -> Result<[u8; 64], ProgramError> { + verify_ed25519_signature_instruction(instructions_sysvar, -2, root_key, signature, message_hash)?; + Ok(*signature) +} + +fn verify_last_block_state_signature(instructions_sysvar: &AccountInfo, record: &UserRecord) -> ProgramResult { + let message = serialize_last_block_state(record)?; + let msg_hash = hashv(&[&message]); + verify_ed25519_signature_instruction(instructions_sysvar, -1, &record.blockchain.blockchain_public_key, &record.blockchain.last_block_signature, msg_hash.as_ref()) +} + +fn serialize_last_block_state(record: &UserRecord) -> Result, ProgramError> { + 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 ParsedEd25519Ref<'a> { + pubkey: Pubkey, + signature: [u8; 64], + message: &'a [u8], +} + +fn verify_ed25519_signature_instruction(instructions_sysvar: &AccountInfo, index_relative_to_current: i64, expected_pubkey: &Pubkey, expected_signature: &[u8; 64], expected_message: &[u8]) -> ProgramResult { + require_keys_eq!(*instructions_sysvar.key, solana_program::sysvar::instructions::id(), ShineUsersError::InvalidSignature); + let current_index = load_current_index_checked(instructions_sysvar).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))? as i64; + let target_index = current_index.checked_add(index_relative_to_current).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; + require!(target_index >= 0, ShineUsersError::InvalidSignature); + let ed_ix = load_instruction_at_checked(target_index as usize, instructions_sysvar).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))?; + require_keys_eq!(ed_ix.program_id, solana_program::ed25519_program::id(), ShineUsersError::InvalidSignature); + let parsed = parse_ed25519_ix(ed_ix.data.as_slice())?; + require!(parsed.pubkey == *expected_pubkey, ShineUsersError::InvalidSignature); + require!(parsed.signature == *expected_signature, ShineUsersError::InvalidSignature); + require!(parsed.message == expected_message, ShineUsersError::InvalidSignature); + Ok(()) +} + +fn parse_ed25519_ix<'a>(data: &'a [u8]) -> Result, ProgramError> { + require!(data.len() >= 16, ShineUsersError::InvalidSignature); + require!(data[0] == 1, ShineUsersError::InvalidSignature); + let signature_offset = le_u16(data, 2)? as usize; + let signature_ix_index = le_u16(data, 4)?; + let pubkey_offset = le_u16(data, 6)? as usize; + let pubkey_ix_index = le_u16(data, 8)?; + let message_offset = le_u16(data, 10)? as usize; + let message_size = le_u16(data, 12)? as usize; + let message_ix_index = le_u16(data, 14)?; + require!(signature_ix_index == u16::MAX, ShineUsersError::InvalidSignature); + require!(pubkey_ix_index == u16::MAX, ShineUsersError::InvalidSignature); + require!(message_ix_index == u16::MAX, ShineUsersError::InvalidSignature); + + let signature_end = signature_offset.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; + let pubkey_end = pubkey_offset.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; + let message_end = message_offset.checked_add(message_size).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; + let signature_slice = data.get(signature_offset..signature_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; + let pubkey_slice = data.get(pubkey_offset..pubkey_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; + let message = data.get(message_offset..message_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; + let mut signature = [0u8; 64]; signature.copy_from_slice(signature_slice); + let pubkey = Pubkey::new_from_array(<[u8; 32]>::try_from(pubkey_slice).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))?); + Ok(ParsedEd25519Ref { pubkey, signature, message }) +} + +fn le_u16(data: &[u8], offset: usize) -> Result { + let end = offset.checked_add(2).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; + let s = data.get(offset..end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; + Ok(u16::from_le_bytes([s[0], s[1]])) +} + +fn validate_login(login: &str) -> ProgramResult { + require!(!login.is_empty(), ShineUsersError::InvalidLogin); + require!(login.len() <= 20, ShineUsersError::InvalidLogin); + for ch in login.chars() { if !(ch.is_ascii_alphabetic() || ch.is_ascii_digit() || ch == '_') { return Err(ProgramError::from(ShineUsersError::InvalidLogin)); } } + Ok(()) +} +fn login_seed_normalized(login: &str) -> String { login.to_ascii_lowercase() } + +fn validate_fields(fields: &UserMutableFields) -> ProgramResult { + require!(!fields.blockchain_name.is_empty(), ShineUsersError::InvalidRecordData); + require!(fields.blockchain_name.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); + require!(fields.arweave_tx_id.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); + if fields.is_server { + require!(!fields.server_address.is_empty(), ShineUsersError::InvalidRecordData); + require!(fields.server_address.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); + require!(fields.sync_servers.len() <= MAX_SYNC_SERVERS, ShineUsersError::InvalidRecordData); + for login in &fields.sync_servers { require!(!login.is_empty(), ShineUsersError::InvalidRecordData); require!(login.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); } + } else { + require!(fields.server_address.is_empty(), ShineUsersError::InvalidRecordData); + require!(fields.sync_servers.is_empty(), ShineUsersError::InvalidRecordData); + } + require!(fields.access_servers.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); + for login in &fields.access_servers { require!(!login.is_empty(), ShineUsersError::InvalidRecordData); require!(login.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); } + validate_sessions_fields(fields.sessions_mode, &fields.sessions) +} + +fn validate_sessions_fields(mode: u8, sessions: &[SessionRecord]) -> ProgramResult { + require!(mode == SESSIONS_MODE_MIXED || mode == SESSIONS_MODE_PDA_ONLY, ShineUsersError::InvalidRecordData); + require!(sessions.len() <= MAX_SESSIONS, ShineUsersError::InvalidRecordData); + for i in 0..sessions.len() { + validate_session_record(&sessions[i])?; + for j in (i + 1)..sessions.len() { + require!(sessions[i].session_name != sessions[j].session_name, ShineUsersError::InvalidRecordData); + require!(sessions[i].session_pub_key != sessions[j].session_pub_key, ShineUsersError::InvalidRecordData); + } + } + Ok(()) +} + +fn validate_session_record(session: &SessionRecord) -> ProgramResult { + require!(session.session_type == SESSION_TYPE_USER || session.session_type == SESSION_TYPE_SUBSERVER, ShineUsersError::InvalidRecordData); + require!(session.session_version == 1, ShineUsersError::InvalidRecordData); + let bytes = session.session_name.as_bytes(); + require!(!bytes.is_empty(), ShineUsersError::InvalidRecordData); + require!(bytes.len() <= MAX_SESSION_NAME_LEN, ShineUsersError::InvalidRecordData); + for &b in bytes { require!(b.is_ascii_alphanumeric() || b == b'_', ShineUsersError::InvalidRecordData); } + Ok(()) +} + +fn validate_blockchain_limits(blockchain: &BlockchainRecord, old_used_bytes: u64, old_last_block_number: u32, is_create: bool) -> ProgramResult { + require!(blockchain.blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER, ShineUsersError::InvalidRecordData); + require!(blockchain.used_bytes <= blockchain.paid_limit_bytes, ShineUsersError::InvalidRecordData); + if !is_create { + require!(blockchain.used_bytes >= old_used_bytes && blockchain.last_block_number >= old_last_block_number, ShineUsersError::InvalidRecordData); + } + Ok(()) +} + +fn validate_inflow_vault(inflow_vault: &AccountInfo) -> ProgramResult { + let payments_program_id = Pubkey::from_str(settings::SHINE_PAYMENTS_PROGRAM_ID).map_err(|_| ProgramError::from(ShineUsersError::InvalidFeeReceiver))?; + let (expected, _) = Pubkey::find_program_address(&[settings::SHINE_PAYMENTS_INFLOW_VAULT_SEED], &payments_program_id); + require_keys_eq!(expected, *inflow_vault.key, ShineUsersError::InvalidFeeReceiver); + Ok(()) +} + +fn transfer_lamports<'a>(payer: &AccountInfo<'a>, recipient: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, lamports: u64) -> ProgramResult { + if lamports == 0 { return Ok(()); } + let ix = system_instruction::transfer(payer.key, recipient.key, lamports); + invoke(&ix, &[payer.clone(), recipient.clone(), system_program_ai.clone()]) +} + +fn create_pda_account<'a>(payer: &AccountInfo<'a>, pda: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, owner: &Pubkey, seeds: &[&[u8]], space: usize) -> ProgramResult { + let rent = Rent::get()?; + let lamports = rent.minimum_balance(space); + let ix = system_instruction::create_account(payer.key, pda.key, lamports, space as u64, owner); + invoke_signed(&ix, &[payer.clone(), pda.clone(), system_program_ai.clone()], &[seeds]) +} + +fn ensure_pda_size_and_rent<'a>(pda: &AccountInfo<'a>, payer: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, required_len: usize) -> ProgramResult { + let current_len = pda.data_len(); + if required_len <= current_len { return Ok(()); } + let increase = required_len.checked_sub(current_len).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; + require!(increase <= MAX_AUTO_REALLOC_INCREASE, ShineUsersError::RecordTooLarge); + let rent = Rent::get()?; + let required_lamports = rent.minimum_balance(required_len); + let current_lamports = pda.lamports(); + let top_up = required_lamports.saturating_sub(current_lamports); + if top_up > 0 { transfer_lamports(payer, pda, system_program_ai, top_up)?; } + pda.realloc(required_len, false) +} + +fn find_user_pda(program_id: &Pubkey, login: &str) -> (Pubkey, u8) { Pubkey::find_program_address(&[settings::USER_PDA_SEED_PREFIX.as_bytes(), login.as_bytes()], program_id) } +fn find_users_economy_config_pda(program_id: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&[settings::USERS_ECONOMY_CONFIG_SEED], program_id) } +fn limit_fee_lamports(limit_delta: u64, lamports_per_limit_step: u64) -> Result { (limit_delta / settings::LIMIT_STEP).checked_mul(lamports_per_limit_step).ok_or(ProgramError::from(ShineUsersError::MathOverflow)) } +fn pad_to_fixed_size(mut bytes: Vec, target_size: usize) -> Result, ProgramError> { require!(bytes.len() <= target_size, ShineUsersError::RecordTooLarge); bytes.resize(target_size, 0); Ok(bytes) } + +fn read_pda_all(pda: &AccountInfo) -> Result, ProgramError> { Ok(pda.try_borrow_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?.to_vec()) } +fn write_pda_exact(pda: &AccountInfo, data: &[u8]) -> ProgramResult { let mut dst = pda.try_borrow_mut_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?; require!(data.len() <= dst.len(), ShineUsersError::RecordTooLarge); dst[..data.len()].copy_from_slice(data); for b in &mut dst[data.len()..] { *b = 0; } Ok(()) } +fn write_pda_prefix(pda: &AccountInfo, data: &[u8]) -> ProgramResult { let mut dst = pda.try_borrow_mut_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?; require!(data.len() <= dst.len(), ShineUsersError::RecordTooLarge); dst[..data.len()].copy_from_slice(data); Ok(()) } + +fn read_u8_from(data: &[u8], cursor: &mut usize) -> Result { let v = *data.get(*cursor).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor += 1; Ok(v) } +fn read_u32_from(data: &[u8], cursor: &mut usize) -> Result { let end = cursor.checked_add(4).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]])) } +fn read_u64_from(data: &[u8], cursor: &mut usize) -> Result { let end = cursor.checked_add(8).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; Ok(u64::from_le_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])) } +fn read_fixed_32_from(data: &[u8], cursor: &mut usize) -> Result<[u8; 32], ProgramError> { let end = cursor.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; <[u8; 32]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) } +fn read_fixed_64_from(data: &[u8], cursor: &mut usize) -> Result<[u8; 64], ProgramError> { let end = cursor.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; <[u8; 64]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) } +fn read_len_prefixed_string_from(data: &[u8], cursor: &mut usize) -> Result { let len = read_u8_from(data, cursor)? as usize; let end = cursor.checked_add(len).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; std::str::from_utf8(s).map(|v| v.to_string()).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) } diff --git a/shine-solana/shine/programs/shine_users/src/settings.rs b/shine-solana/shine/programs/shine_users/src/settings.rs index f83d7cd..da0ca3f 100644 --- a/shine-solana/shine/programs/shine_users/src/settings.rs +++ b/shine-solana/shine/programs/shine_users/src/settings.rs @@ -1,30 +1,27 @@ -use common::deploy_config; - -/// `USER_PDA_SEED_PREFIX` — префикс seed для пользовательского PDA (`login=<...>`). -pub const USER_PDA_SEED_PREFIX: &str = "login="; -/// `USERS_ECONOMY_CONFIG_SEED` — seed PDA с экономическими параметрами программы `shine_users`. -pub const USERS_ECONOMY_CONFIG_SEED: &[u8] = b"shine_users_economy_config"; -/// `USER_PDA_SPACE` — стартовый размер PDA пользователя, дальше запись может расширяться через realloc. +/// Префикс seed для пользовательского PDA новой чисто-rust реализации `shine_users`. +pub const USER_PDA_SEED_PREFIX: &str = "user_login="; +/// Seed PDA с экономическими параметрами программы `shine_users`. +pub const USERS_ECONOMY_CONFIG_SEED: &[u8] = b"shine_users_economy_config_v2"; +/// Стартовый размер PDA пользователя, дальше запись может расширяться через realloc. pub const USER_PDA_SPACE: usize = 768; -/// `USERS_ECONOMY_CONFIG_SPACE` — размер PDA с экономическими параметрами `shine_users`. -pub const USERS_ECONOMY_CONFIG_SPACE: usize = 8 + 96; +/// Размер PDA с экономическими параметрами `shine_users`. +pub const USERS_ECONOMY_CONFIG_SPACE: usize = 32; -/// `DAO_AUTHORITY` — адрес DAO-авторити, который имеет право обновлять economy-конфиг. -pub const DAO_AUTHORITY: &str = deploy_config::DAO_AUTHORITY; +/// Адрес DAO authority, который имеет право обновлять economy-конфиг. +pub const DAO_AUTHORITY: &str = "FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P"; -/// `SHINE_PAYMENTS_PROGRAM_ID` — адрес программы `shine_payments`, от которой вычисляется PDA inflow-вольта. -pub const SHINE_PAYMENTS_PROGRAM_ID: &str = deploy_config::SHINE_PAYMENTS_PROGRAM_ID; -/// `SHINE_PAYMENTS_INFLOW_VAULT_SEED` — seed inflow-вольта в программе `shine_payments` (должен совпадать с payments settings). +/// Адрес программы `shine_payments`, от которой вычисляется PDA inflow-вольта. +pub const SHINE_PAYMENTS_PROGRAM_ID: &str = "m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR"; +/// Seed inflow-вольта в программе `shine_payments`. pub const SHINE_PAYMENTS_INFLOW_VAULT_SEED: &[u8] = b"shine_payments_inflow_vault"; -/// `SHINE_LOGIN_GUARD_PROGRAM_ID` — адрес отдельной программы проверки премиальности логина. -pub const SHINE_LOGIN_GUARD_PROGRAM_ID: &str = deploy_config::SHINE_LOGIN_GUARD_PROGRAM_ID; -/// `START_REGISTRATION_FEE_LAMPORTS` — стартовая комиссия регистрации (0.01 SOL) для initial economy-конфига. +/// Адрес отдельной программы проверки премиальности логина. +pub const SHINE_LOGIN_GUARD_PROGRAM_ID: &str = "3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo"; + +/// Стартовая комиссия регистрации (0.01 SOL) для initial economy-конфига. pub const START_REGISTRATION_FEE_LAMPORTS: u64 = 10_000_000; - -/// `LIMIT_STEP` — шаг пополнения лимита; `additional_limit` должен быть кратен этому значению. +/// Шаг пополнения лимита. pub const LIMIT_STEP: u64 = 10_000; -/// `START_LAMPORTS_PER_LIMIT_STEP` — стартовая цена одного шага лимита (0.0001 SOL за 10_000 лимита). +/// Стартовая цена одного шага лимита. pub const START_LAMPORTS_PER_LIMIT_STEP: u64 = 100_000; - -/// `START_BONUS_LIMIT` — стартовый бонус лимита, выдаваемый пользователю при создании записи. +/// Стартовый бонус лимита. pub const START_BONUS_LIMIT: u64 = 100_000; diff --git a/shine-solana/shine/programs/shine_users/src/users.rs b/shine-solana/shine/programs/shine_users/src/users.rs deleted file mode 100644 index 6604d23..0000000 --- a/shine-solana/shine/programs/shine_users/src/users.rs +++ /dev/null @@ -1,1456 +0,0 @@ -use crate::settings; -use anchor_lang::prelude::*; -use anchor_lang::solana_program::{ - ed25519_program, - hash::hashv, - program::{get_return_data, invoke}, - system_instruction, - sysvar::instructions::get_instruction_relative, -}; -use common::utils::{create_pda, safe_read_pda, write_to_pda, ErrCode}; -use std::str::FromStr; - -const MAGIC: &[u8; 5] = b"SHiNE"; -const FORMAT_MAJOR: u8 = 1; -const FORMAT_MINOR: u8 = 0; -const MAX_SYNC_SERVERS: usize = 32; -const MAX_SESSIONS: usize = 64; -const MAX_SESSION_NAME_LEN: usize = 64; -const MAX_AUTO_REALLOC_INCREASE: usize = 10_000; -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_TYPE_SESSIONS: u8 = 55; -const BLOCK_VERSION_0: u8 = 0; -const BLOCKCHAIN_TYPE_MAIN_USER: u8 = 1; -const SESSIONS_MODE_MIXED: u8 = 1; -const SESSIONS_MODE_PDA_ONLY: u8 = 10; -const SESSION_TYPE_USER: u8 = 1; -const SESSION_TYPE_SUBSERVER: u8 = 100; -const LAST_BLOCK_STATE_PREFIX: &[u8] = b"SHiNE_LAST_BLOCK"; - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)] -pub struct SessionRecord { - pub session_type: u8, - pub session_version: u8, - pub session_name: String, - pub session_pub_key: Pubkey, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct UserMutableFields { - pub device_key: Pubkey, - pub blockchain_public_key: Pubkey, - pub blockchain_name: String, - pub used_bytes: u64, - pub last_block_number: u32, - pub last_block_hash: Vec, - pub last_block_signature: Vec, - pub arweave_tx_id: String, - pub is_server: bool, - pub address_format_type: u8, - pub address_format_version: u8, - pub server_address: String, - pub sync_servers: Vec, - pub access_servers: Vec, - pub sessions_mode: u8, - pub sessions: Vec, - pub trusted_count: u8, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct CreateUserPdaArgs { - pub login: String, - pub root_key: Pubkey, - pub created_at_ms: u64, - pub additional_limit: u64, - pub fields: UserMutableFields, - pub signature: Vec, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct UpdateUserPdaArgs { - pub login: String, - pub root_key: Pubkey, - pub created_at_ms: u64, - pub updated_at_ms: u64, - pub version: u32, - pub prev_hash: Vec, - pub additional_limit: u64, - pub fields: UserMutableFields, - pub signature: Vec, -} - -pub struct UserRecord { - pub created_at_ms: u64, - pub updated_at_ms: u64, - pub record_number: u32, - pub prev_record_hash: [u8; 32], - pub login: String, - pub root_key: Pubkey, - pub device_key: Pubkey, - pub blockchain: BlockchainRecord, - pub is_server: bool, - pub address_format_type: u8, - pub address_format_version: u8, - pub server_address: String, - pub sync_servers: Vec, - pub access_servers: Vec, - pub sessions_mode: u8, - pub sessions: Vec, - pub trusted_count: u8, - pub signature: [u8; 64], -} - -pub struct BlockchainRecord { - pub blockchain_type: u8, - pub blockchain_name: String, - pub blockchain_public_key: Pubkey, - pub paid_limit_bytes: u64, - pub used_bytes: u64, - pub last_block_number: u32, - pub last_block_hash: [u8; 32], - pub last_block_signature: [u8; 64], - pub arweave_tx_id: String, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct UsersEconomyConfigState { - pub version: u8, - pub registration_fee_lamports: u64, - pub lamports_per_limit_step: u64, - pub start_bonus_limit: u64, -} - -#[derive(Accounts)] -pub struct CreateUserPda<'info> { - /// CHECK: подписант транзакции, валидируется Anchor как signer и mut. - #[account(mut, signer)] - pub signer: AccountInfo<'info>, - /// CHECK: PDA пользователя, адрес проверяется вручную через seed в обработчике. - #[account(mut)] - pub user_pda: AccountInfo<'info>, - pub system_program: Program<'info, System>, - /// CHECK: inflow-вольт shine_payments, адрес проверяется вручную как PDA. - #[account(mut)] - pub inflow_vault: AccountInfo<'info>, - /// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции. - pub instructions: AccountInfo<'info>, - /// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную. - pub users_economy_config_pda: AccountInfo<'info>, - pub login_guard_program: Program<'info, shine_login_guard::program::ShineLoginGuard>, -} - -#[derive(Accounts)] -pub struct UpdateUserPda<'info> { - /// CHECK: подписант транзакции, валидируется Anchor как signer и mut. - #[account(mut, signer)] - pub signer: AccountInfo<'info>, - /// CHECK: PDA пользователя, адрес проверяется вручную через seed в обработчике. - #[account(mut)] - pub user_pda: AccountInfo<'info>, - pub system_program: Program<'info, System>, - /// CHECK: inflow-вольт shine_payments, адрес проверяется вручную как PDA. - #[account(mut)] - pub inflow_vault: AccountInfo<'info>, - /// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции. - pub instructions: AccountInfo<'info>, - /// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную. - pub users_economy_config_pda: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct InitUsersEconomyConfig<'info> { - /// CHECK: подписант и плательщик, валидируется Anchor как signer и mut. - #[account(mut, signer)] - pub signer: AccountInfo<'info>, - /// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную. - #[account(mut)] - pub users_economy_config_pda: AccountInfo<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct UpdateUsersEconomyConfig<'info> { - /// CHECK: подписант (должен быть DAO authority из settings). - #[account(mut, signer)] - pub signer: AccountInfo<'info>, - /// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную. - #[account(mut)] - pub users_economy_config_pda: AccountInfo<'info>, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct UpdateUsersEconomyConfigArgs { - pub registration_fee_lamports: u64, - pub lamports_per_limit_step: u64, - pub start_bonus_limit: u64, -} - -pub fn init_users_economy_config(ctx: Context) -> Result<()> { - let (expected_pda, bump) = find_users_economy_config_pda(ctx.program_id); - require_keys_eq!( - expected_pda, - ctx.accounts.users_economy_config_pda.key(), - ErrCode::InvalidPdaAddress - ); - require!( - ctx.accounts.users_economy_config_pda.owner == &Pubkey::default(), - ErrCode::SystemAlreadyInitialized - ); - - let state = UsersEconomyConfigState { - version: 1, - registration_fee_lamports: settings::START_REGISTRATION_FEE_LAMPORTS, - lamports_per_limit_step: settings::START_LAMPORTS_PER_LIMIT_STEP, - start_bonus_limit: settings::START_BONUS_LIMIT, - }; - let bytes = state - .try_to_vec() - .map_err(|_| error!(ErrCode::DeserializationError))?; - let seeds: &[&[u8]] = &[settings::USERS_ECONOMY_CONFIG_SEED, &[bump]]; - create_pda( - &ctx.accounts.users_economy_config_pda, - &ctx.accounts.signer, - &ctx.accounts.system_program.to_account_info(), - ctx.program_id, - seeds, - settings::USERS_ECONOMY_CONFIG_SPACE as u64, - )?; - write_to_pda(&ctx.accounts.users_economy_config_pda, &bytes)?; - Ok(()) -} - -pub fn update_users_economy_config( - ctx: Context, - args: UpdateUsersEconomyConfigArgs, -) -> Result<()> { - let dao_authority = - Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| error!(ErrCode::InvalidSigner))?; - require_keys_eq!( - dao_authority, - ctx.accounts.signer.key(), - ErrCode::InvalidSigner - ); - - let (expected_pda, _) = find_users_economy_config_pda(ctx.program_id); - require_keys_eq!( - expected_pda, - ctx.accounts.users_economy_config_pda.key(), - ErrCode::InvalidPdaAddress - ); - require!( - ctx.accounts.users_economy_config_pda.owner == ctx.program_id, - ErrCode::InvalidPdaAddress - ); - require!(args.lamports_per_limit_step > 0, ErrCode::InvalidRecordData); - - let mut state = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?; - state.registration_fee_lamports = args.registration_fee_lamports; - state.lamports_per_limit_step = args.lamports_per_limit_step; - state.start_bonus_limit = args.start_bonus_limit; - let bytes = state - .try_to_vec() - .map_err(|_| error!(ErrCode::DeserializationError))?; - write_to_pda(&ctx.accounts.users_economy_config_pda, &bytes)?; - Ok(()) -} - -pub fn create_user_pda(ctx: Context, args: CreateUserPdaArgs) -> Result<()> { - let CreateUserPdaArgs { - login, - root_key, - created_at_ms, - additional_limit, - fields, - signature, - } = args; - let UserMutableFields { - device_key, - blockchain_public_key, - blockchain_name, - used_bytes, - last_block_number, - last_block_hash, - last_block_signature, - arweave_tx_id, - is_server, - address_format_type, - address_format_version, - server_address, - sync_servers, - access_servers, - sessions_mode, - sessions, - trusted_count, - } = fields; - - validate_login(&login)?; - require_keys_eq!( - ctx.accounts.login_guard_program.key(), - Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID) - .map_err(|_| error!(ErrCode::InvalidLoginGuardResponse))?, - ErrCode::InvalidLoginGuardResponse - ); - classify_login_or_fail( - &ctx.accounts.login_guard_program.to_account_info(), - &ctx.accounts.signer, - &login, - )?; - validate_fields(&UserMutableFields { - device_key, - blockchain_public_key, - blockchain_name: blockchain_name.clone(), - used_bytes, - last_block_number, - last_block_hash: last_block_hash.clone(), - last_block_signature: last_block_signature.clone(), - arweave_tx_id: arweave_tx_id.clone(), - is_server, - address_format_type, - address_format_version, - server_address: server_address.clone(), - sync_servers: sync_servers.clone(), - access_servers: access_servers.clone(), - sessions_mode, - sessions: sessions.clone(), - trusted_count, - })?; - validate_inflow_vault(&ctx.accounts.inflow_vault)?; - require!( - additional_limit % settings::LIMIT_STEP == 0, - ErrCode::InvalidLimitIncrement - ); - let economy = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?; - - let login_seed = login_seed_normalized(&login); - let (expected_pda, bump) = find_user_pda(ctx.program_id, &login_seed); - require_keys_eq!( - expected_pda, - ctx.accounts.user_pda.key(), - ErrCode::InvalidPdaAddress - ); - require!( - ctx.accounts.user_pda.owner == &Pubkey::default(), - ErrCode::UserAlreadyExists - ); - - let start_balance = economy - .start_bonus_limit - .checked_add(additional_limit) - .ok_or(error!(ErrCode::MathOverflow))?; - - let mut record = UserRecord { - created_at_ms, - updated_at_ms: created_at_ms, - record_number: 0, - prev_record_hash: ZERO_HASH, - login, - root_key, - device_key, - blockchain: BlockchainRecord { - blockchain_type: BLOCKCHAIN_TYPE_MAIN_USER, - blockchain_name, - blockchain_public_key, - paid_limit_bytes: start_balance, - used_bytes, - last_block_number, - last_block_hash: vec_to_hash32(&last_block_hash)?, - last_block_signature: vec_to_signature(&last_block_signature)?, - arweave_tx_id, - }, - is_server, - address_format_type, - address_format_version, - server_address, - sync_servers, - access_servers, - sessions_mode, - sessions, - trusted_count, - signature: [0; 64], - }; - validate_blockchain_limits(&record.blockchain, 0, 0, true)?; - verify_last_block_state_signature(&ctx.accounts.instructions, &record)?; - - let unsigned = serialize_unsigned_record(&record)?; - let unsigned_hash = hashv(&[&unsigned]); - drop(unsigned); - record.signature = verify_record_signature_hash( - &ctx.accounts.instructions, - &record.root_key, - &signature, - unsigned_hash.as_ref(), - )?; - - let serialized = serialize_full_record(&record)?; - require!( - serialized.len() <= settings::USER_PDA_SPACE, - ErrCode::RecordTooLarge - ); - let padded = pad_to_fixed_size(serialized, settings::USER_PDA_SPACE)?; - - let pda_seeds: &[&[u8]] = &[ - settings::USER_PDA_SEED_PREFIX.as_bytes(), - login_seed.as_bytes(), - &[bump], - ]; - create_pda( - &ctx.accounts.user_pda, - &ctx.accounts.signer, - &ctx.accounts.system_program.to_account_info(), - ctx.program_id, - pda_seeds, - settings::USER_PDA_SPACE as u64, - )?; - write_to_pda(&ctx.accounts.user_pda, &padded)?; - - let total_fee = economy - .registration_fee_lamports - .checked_add(limit_fee_lamports(additional_limit, economy.lamports_per_limit_step)?) - .ok_or(error!(ErrCode::MathOverflow))?; - transfer_lamports( - &ctx.accounts.signer, - &ctx.accounts.inflow_vault, - &ctx.accounts.system_program.to_account_info(), - total_fee, - )?; - - Ok(()) -} - -fn classify_login_or_fail<'info>( - login_guard_program: &AccountInfo<'info>, - signer: &AccountInfo<'info>, - login: &str, -) -> Result<()> { - let cpi_ctx = CpiContext::new( - login_guard_program.clone(), - shine_login_guard::cpi::accounts::ClassifyLogin { - signer: signer.to_account_info(), - }, - ); - shine_login_guard::cpi::classify_login(cpi_ctx, login.to_string())?; - - let (program_id, raw) = get_return_data().ok_or(error!(ErrCode::InvalidLoginGuardResponse))?; - require_keys_eq!( - program_id, - *login_guard_program.key, - ErrCode::InvalidLoginGuardResponse - ); - require!(raw.len() == 4, ErrCode::InvalidLoginGuardResponse); - let class = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]); - match class { - 0 => Ok(()), - 1 => Err(error!(ErrCode::PremiumLogin)), - 2 => Err(error!(ErrCode::TrademarkLoginRequiresReview)), - _ => Err(error!(ErrCode::InvalidLoginGuardResponse)), - } -} - -pub fn update_user_pda(ctx: Context, args: UpdateUserPdaArgs) -> Result<()> { - validate_login(&args.login)?; - validate_fields(&args.fields)?; - validate_inflow_vault(&ctx.accounts.inflow_vault)?; - require!( - args.additional_limit % settings::LIMIT_STEP == 0, - ErrCode::InvalidLimitIncrement - ); - let economy = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?; - - require_keys_eq!( - { - let normalized_login = login_seed_normalized(&args.login); - find_user_pda(ctx.program_id, &normalized_login).0 - }, - ctx.accounts.user_pda.key(), - ErrCode::InvalidPdaAddress - ); - require!( - ctx.accounts.user_pda.owner == ctx.program_id, - ErrCode::InvalidPdaAddress - ); - - let raw = safe_read_pda(&ctx.accounts.user_pda); - require!(!raw.is_empty(), ErrCode::EmptyPdaData); - let old_record = Box::new(deserialize_record_from_pda(raw.as_slice())?); - drop(raw); - - require!( - old_record.login == args.login, - ErrCode::ImmutableFieldChanged - ); - require!( - old_record.created_at_ms == args.created_at_ms, - ErrCode::ImmutableFieldChanged - ); - require_keys_eq!( - old_record.root_key, - args.root_key, - ErrCode::ImmutableFieldChanged - ); - require!( - args.version == old_record.record_number.saturating_add(1), - ErrCode::InvalidVersion - ); - - let provided_prev_hash = Box::new(vec_to_hash32(&args.prev_hash)?); - require!( - hash_unsigned_record(&old_record)? == *provided_prev_hash, - ErrCode::InvalidPrevHash - ); - - let new_balance = Box::new(old_record - .blockchain - .paid_limit_bytes - .checked_add(args.additional_limit) - .ok_or(error!(ErrCode::MathOverflow))?); - require!( - *new_balance >= old_record.blockchain.paid_limit_bytes, - ErrCode::BalanceDecrease - ); - let blockchain_state_unchanged = old_record.blockchain.used_bytes == args.fields.used_bytes - && old_record.blockchain.last_block_number == args.fields.last_block_number - && old_record.blockchain.last_block_hash.as_slice() == args.fields.last_block_hash.as_slice() - && old_record.blockchain.last_block_signature.as_slice() - == args.fields.last_block_signature.as_slice() - && old_record.blockchain.arweave_tx_id == args.fields.arweave_tx_id; - - require!( - args.fields.blockchain_name == old_record.blockchain.blockchain_name - && args.fields.blockchain_public_key - == old_record.blockchain.blockchain_public_key, - ErrCode::ImmutableFieldChanged - ); - { - let candidate_blockchain = - build_candidate_blockchain_for_update(&old_record, *new_balance, &args.fields)?; - validate_blockchain_limits( - &candidate_blockchain, - old_record.blockchain.used_bytes, - old_record.blockchain.last_block_number, - false, - )?; - } - if !blockchain_state_unchanged { - verify_last_block_state_signature_candidate( - &ctx.accounts.instructions, - &old_record, - args.updated_at_ms, - args.version, - *provided_prev_hash, - *new_balance, - &args.fields, - )?; - } - - let unsigned = serialize_unsigned_update_candidate( - &old_record, - args.updated_at_ms, - args.version, - *provided_prev_hash, - *new_balance, - &args.fields, - )?; - let unsigned_hash = hashv(&[&unsigned]); - drop(unsigned); - let verified_signature = Box::new(verify_record_signature_hash( - &ctx.accounts.instructions, - &old_record.root_key, - &args.signature, - unsigned_hash.as_ref(), - )?); - let mut new_record = build_update_record( - &old_record, - args.updated_at_ms, - args.version, - *provided_prev_hash, - *new_balance, - &args.fields, - )?; - new_record.signature = *verified_signature; - drop(old_record); - - let serialized = serialize_full_record(&new_record)?; - ensure_pda_size_and_rent( - &ctx.accounts.user_pda, - &ctx.accounts.signer, - &ctx.accounts.system_program.to_account_info(), - serialized.len(), - )?; - write_to_pda(&ctx.accounts.user_pda, &serialized)?; - - let topup_fee = limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?; - if topup_fee > 0 { - transfer_lamports( - &ctx.accounts.signer, - &ctx.accounts.inflow_vault, - &ctx.accounts.system_program.to_account_info(), - topup_fee, - )?; - } - - Ok(()) -} - -#[inline(never)] -fn build_candidate_blockchain_for_update( - old_record: &UserRecord, - new_balance: u64, - fields: &UserMutableFields, -) -> Result { - Ok(BlockchainRecord { - blockchain_type: old_record.blockchain.blockchain_type, - blockchain_name: fields.blockchain_name.clone(), - blockchain_public_key: fields.blockchain_public_key, - paid_limit_bytes: new_balance, - used_bytes: fields.used_bytes, - last_block_number: fields.last_block_number, - last_block_hash: vec_to_hash32(&fields.last_block_hash)?, - last_block_signature: vec_to_signature(&fields.last_block_signature)?, - arweave_tx_id: fields.arweave_tx_id.clone(), - }) -} - -#[inline(never)] -fn build_update_record( - old_record: &UserRecord, - updated_at_ms: u64, - version: u32, - prev_record_hash: [u8; 32], - new_balance: u64, - fields: &UserMutableFields, -) -> Result { - Ok(UserRecord { - created_at_ms: old_record.created_at_ms, - updated_at_ms, - record_number: version, - prev_record_hash, - login: old_record.login.clone(), - root_key: old_record.root_key, - device_key: fields.device_key, - blockchain: build_candidate_blockchain_for_update(old_record, new_balance, fields)?, - is_server: fields.is_server, - address_format_type: fields.address_format_type, - address_format_version: fields.address_format_version, - server_address: fields.server_address.clone(), - sync_servers: fields.sync_servers.clone(), - access_servers: fields.access_servers.clone(), - sessions_mode: fields.sessions_mode, - sessions: fields.sessions.clone(), - trusted_count: fields.trusted_count, - signature: [0; 64], - }) -} - -#[inline(never)] -fn verify_last_block_state_signature_candidate( - instructions_sysvar: &AccountInfo, - old_record: &UserRecord, - updated_at_ms: u64, - version: u32, - prev_record_hash: [u8; 32], - new_balance: u64, - fields: &UserMutableFields, -) -> Result<()> { - let candidate = build_update_record( - old_record, - updated_at_ms, - version, - prev_record_hash, - new_balance, - fields, - )?; - verify_last_block_state_signature(instructions_sysvar, &candidate) -} - -#[inline(never)] -fn serialize_unsigned_update_candidate( - old_record: &UserRecord, - updated_at_ms: u64, - version: u32, - prev_record_hash: [u8; 32], - new_balance: u64, - fields: &UserMutableFields, -) -> Result> { - let candidate = build_update_record( - old_record, - updated_at_ms, - version, - prev_record_hash, - new_balance, - fields, - )?; - serialize_unsigned_record(&candidate) -} - -fn serialize_unsigned_record(record: &UserRecord) -> Result> { - let login_bytes = record.login.as_bytes(); - require!(login_bytes.len() <= u8::MAX as usize, ErrCode::InvalidLogin); - - let mut out = Vec::new(); - out.extend_from_slice(MAGIC); - out.push(FORMAT_MAJOR); - out.push(FORMAT_MINOR); - out.extend_from_slice(&0u16.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.record_number.to_le_bytes()); - out.extend_from_slice(&record.prev_record_hash); - - out.push(login_bytes.len() as u8); - out.extend_from_slice(login_bytes); - - let blocks_count = if record.is_server { 7 } else { 6 }; - out.push(blocks_count); - write_root_key_block(&mut out, record); - write_device_key_block(&mut out, record); - write_blockchain_registry_block(&mut out, &record.blockchain)?; - if record.is_server { - write_server_profile_block(&mut out, record)?; - } - write_access_servers_block(&mut out, record)?; - write_sessions_block(&mut out, record)?; - write_trusted_state_block(&mut out, record); - - let record_len = out - .len() - .checked_add(64) - .ok_or(error!(ErrCode::MathOverflow))?; - require!(record_len <= u16::MAX as usize, ErrCode::RecordTooLarge); - let len_bytes = (record_len as u16).to_le_bytes(); - out[7] = len_bytes[0]; - out[8] = len_bytes[1]; - - Ok(out) -} - -fn serialize_full_record(record: &UserRecord) -> Result> { - let mut out = serialize_unsigned_record(record)?; - out.extend_from_slice(&record.signature); - Ok(out) -} - -fn write_root_key_block(out: &mut Vec, 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, 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, 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, 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, record: &UserRecord) -> Result<()> { - out.push(BLOCK_TYPE_SERVER_PROFILE); - out.push(BLOCK_VERSION_0); - out.push(1); - out.push(record.address_format_type); - out.push(record.address_format_version); - 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, 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_sessions_block(out: &mut Vec, record: &UserRecord) -> Result<()> { - out.push(BLOCK_TYPE_SESSIONS); - out.push(BLOCK_VERSION_0); - out.push(record.sessions_mode); - require!( - record.sessions.len() <= MAX_SESSIONS, - ErrCode::InvalidRecordData - ); - out.push(record.sessions.len() as u8); - for session in &record.sessions { - write_session_record(out, session)?; - } - Ok(()) -} - -fn write_session_record(out: &mut Vec, session: &SessionRecord) -> Result<()> { - out.push(session.session_type); - out.push(session.session_version); - write_len_prefixed_string(out, &session.session_name)?; - out.extend_from_slice(session.session_pub_key.as_ref()); - Ok(()) -} - -fn write_trusted_state_block(out: &mut Vec, 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, 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 { - 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 read_session_record(data: &[u8], cursor: &mut usize) -> Result { - let session_type = read_u8(data, cursor)?; - let session_version = read_u8(data, cursor)?; - let session_name = read_len_prefixed_string(data, cursor)?; - let session_pub_key = Pubkey::new_from_array(read_fixed_32(data, cursor)?); - Ok(SessionRecord { - session_type, - session_version, - session_name, - session_pub_key, - }) -} - -fn deserialize_record_from_pda(raw: &[u8]) -> Result { - require!(raw.len() >= 9, ErrCode::InvalidRecordData); - require!(&raw[0..5] == MAGIC, ErrCode::InvalidRecordMagic); - require!( - raw[5] == FORMAT_MAJOR && raw[6] == FORMAT_MINOR, - ErrCode::InvalidRecordFormat - ); - - let record_len = u16::from_le_bytes([raw[7], raw[8]]) as usize; - require!(record_len >= 9 + 64, ErrCode::InvalidRecordLength); - require!(record_len <= raw.len(), ErrCode::InvalidRecordLength); - - let useful = &raw[..record_len]; - let mut cursor = 9usize; - - let created_at_ms = read_u64(useful, &mut cursor)?; - let updated_at_ms = read_u64(useful, &mut cursor)?; - let record_number = read_u32(useful, &mut cursor)?; - let prev_record_hash = read_fixed_32(useful, &mut cursor)?; - let login = read_len_prefixed_string(useful, &mut cursor)?; - - let blocks_count = read_u8(useful, &mut cursor)? as usize; - let mut root_key: Option = None; - let mut device_key: Option = None; - let mut blockchain: Option = None; - let mut is_server = false; - let mut address_format_type = 0u8; - let mut address_format_version = 0u8; - let mut server_address = String::new(); - let mut sync_servers = Vec::new(); - let mut access_servers = Vec::new(); - let mut sessions_mode = SESSIONS_MODE_MIXED; - let mut sessions = Vec::new(); - let mut trusted_count = 0u8; - - for _ in 0..blocks_count { - let block_type = read_u8(useful, &mut cursor)?; - let block_version = read_u8(useful, &mut cursor)?; - require!( - block_version == BLOCK_VERSION_0, - ErrCode::InvalidRecordFormat - ); - match block_type { - BLOCK_TYPE_ROOT_KEY => { - require!(root_key.is_none(), ErrCode::InvalidRecordData); - root_key = Some(Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?)); - } - BLOCK_TYPE_DEVICE_KEY => { - require!(device_key.is_none(), ErrCode::InvalidRecordData); - device_key = Some(Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?)); - } - BLOCK_TYPE_BLOCKCHAIN_REGISTRY => { - require!(blockchain.is_none(), ErrCode::InvalidRecordData); - let count = read_u8(useful, &mut cursor)?; - require!(count == 1, ErrCode::InvalidRecordData); - blockchain = Some(read_blockchain_record(useful, &mut cursor)?); - } - BLOCK_TYPE_SERVER_PROFILE => { - require!(!is_server, ErrCode::InvalidRecordData); - is_server = read_u8(useful, &mut cursor)? == 1; - require!(is_server, ErrCode::InvalidRecordData); - address_format_type = read_u8(useful, &mut cursor)?; - address_format_version = read_u8(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_SESSIONS => { - require!(sessions.is_empty(), ErrCode::InvalidRecordData); - sessions_mode = read_u8(useful, &mut cursor)?; - let sessions_count = read_u8(useful, &mut cursor)? as usize; - require!(sessions_count <= MAX_SESSIONS, ErrCode::InvalidRecordData); - for _ in 0..sessions_count { - sessions.push(read_session_record(useful, &mut cursor)?); - } - } - BLOCK_TYPE_TRUSTED_STATE => { - trusted_count = read_u8(useful, &mut cursor)?; - } - _ => return Err(error!(ErrCode::InvalidRecordFormat)), - } - } - - validate_sessions_fields(sessions_mode, &sessions)?; - let signature = read_fixed_64(useful, &mut cursor)?; - require!(cursor == useful.len(), ErrCode::InvalidRecordLength); - - Ok(UserRecord { - created_at_ms, - updated_at_ms, - record_number, - prev_record_hash, - login, - root_key: root_key.ok_or(error!(ErrCode::InvalidRecordData))?, - device_key: device_key.ok_or(error!(ErrCode::InvalidRecordData))?, - blockchain: blockchain.ok_or(error!(ErrCode::InvalidRecordData))?, - is_server, - address_format_type, - address_format_version, - server_address, - sync_servers, - access_servers, - sessions_mode, - sessions, - trusted_count, - signature, - }) -} - -fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32]> { - let unsigned = serialize_unsigned_record(record)?; - let digest = hashv(&[&unsigned]); - let mut out = [0u8; 32]; - out.copy_from_slice(digest.as_ref()); - Ok(out) -} - -fn verify_record_signature_hash( - instructions_sysvar: &AccountInfo, - root_key: &Pubkey, - signature: &[u8], - message_hash: &[u8], -) -> Result<[u8; 64]> { - let provided_sig = vec_to_signature(signature)?; - verify_ed25519_signature_instruction( - instructions_sysvar, - -2, - root_key, - &provided_sig, - message_hash, - )?; - 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, - -1, - &record.blockchain.blockchain_public_key, - &record.blockchain.last_block_signature, - msg_hash.as_ref(), - ) -} - -fn verify_ed25519_signature_instruction( - instructions_sysvar: &AccountInfo, - index_relative_to_current: i64, - expected_pubkey: &Pubkey, - expected_signature: &[u8; 64], - expected_message: &[u8], -) -> Result<()> { - require_keys_eq!( - *instructions_sysvar.key, - anchor_lang::solana_program::sysvar::instructions::id(), - ErrCode::InvalidSignature - ); - let ed_ix = get_instruction_relative(index_relative_to_current, instructions_sysvar) - .map_err(|_| error!(ErrCode::InvalidSignature))?; - require_keys_eq!(ed_ix.program_id, ed25519_program::id(), ErrCode::InvalidSignature); - let parsed = parse_ed25519_ix(ed_ix.data.as_slice())?; - require!(parsed.pubkey == *expected_pubkey, ErrCode::InvalidSignature); - require!(parsed.signature == *expected_signature, ErrCode::InvalidSignature); - require!(parsed.message == expected_message, ErrCode::InvalidSignature); - Ok(()) -} - -fn serialize_last_block_state(record: &UserRecord) -> Result> { - 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 ParsedEd25519Ref<'a> { - pubkey: Pubkey, - signature: [u8; 64], - message: &'a [u8], -} - -fn parse_ed25519_ix<'a>(data: &'a [u8]) -> Result> { - require!(data.len() >= 16, ErrCode::InvalidSignature); - require!(data[0] == 1, ErrCode::InvalidSignature); - - let signature_offset = le_u16(data, 2)? as usize; - let signature_ix_index = le_u16(data, 4)?; - let pubkey_offset = le_u16(data, 6)? as usize; - let pubkey_ix_index = le_u16(data, 8)?; - let message_offset = le_u16(data, 10)? as usize; - let message_size = le_u16(data, 12)? as usize; - let message_ix_index = le_u16(data, 14)?; - - require!(signature_ix_index == u16::MAX, ErrCode::InvalidSignature); - require!(pubkey_ix_index == u16::MAX, ErrCode::InvalidSignature); - require!(message_ix_index == u16::MAX, ErrCode::InvalidSignature); - - let signature_end = signature_offset - .checked_add(64) - .ok_or(error!(ErrCode::InvalidSignature))?; - let pubkey_end = pubkey_offset - .checked_add(32) - .ok_or(error!(ErrCode::InvalidSignature))?; - let message_end = message_offset - .checked_add(message_size) - .ok_or(error!(ErrCode::InvalidSignature))?; - - let signature_slice = data - .get(signature_offset..signature_end) - .ok_or(error!(ErrCode::InvalidSignature))?; - let pubkey_slice = data - .get(pubkey_offset..pubkey_end) - .ok_or(error!(ErrCode::InvalidSignature))?; - let message = data - .get(message_offset..message_end) - .ok_or(error!(ErrCode::InvalidSignature))?; - - let mut signature = [0u8; 64]; - signature.copy_from_slice(signature_slice); - let pubkey = Pubkey::new_from_array( - <[u8; 32]>::try_from(pubkey_slice).map_err(|_| error!(ErrCode::InvalidSignature))?, - ); - - Ok(ParsedEd25519Ref { - pubkey, - signature, - message, - }) -} - -fn le_u16(data: &[u8], offset: usize) -> Result { - let end = offset - .checked_add(2) - .ok_or(error!(ErrCode::InvalidSignature))?; - let s = data - .get(offset..end) - .ok_or(error!(ErrCode::InvalidSignature))?; - Ok(u16::from_le_bytes([s[0], s[1]])) -} - -fn validate_login(login: &str) -> Result<()> { - require!(!login.is_empty(), ErrCode::InvalidLogin); - require!(login.len() <= 20, ErrCode::InvalidLogin); - for ch in login.chars() { - if !(ch.is_ascii_alphabetic() || ch.is_ascii_digit() || ch == '_') { - return Err(error!(ErrCode::InvalidLogin)); - } - } - Ok(()) -} - -fn login_seed_normalized(login: &str) -> String { - login.to_ascii_lowercase() -} - -fn validate_fields(fields: &UserMutableFields) -> Result<()> { - require!( - !fields.blockchain_name.is_empty(), - ErrCode::InvalidRecordData - ); - require!( - fields.blockchain_name.as_bytes().len() <= u8::MAX as usize, - ErrCode::InvalidRecordData - ); - require!( - fields.last_block_hash.len() == 32 && fields.last_block_signature.len() == 64, - ErrCode::InvalidRecordData - ); - require!( - fields.arweave_tx_id.as_bytes().len() <= u8::MAX as usize, - ErrCode::InvalidRecordData - ); - if fields.is_server { - require!( - !fields.server_address.is_empty(), - ErrCode::InvalidRecordData - ); - require!( - fields.server_address.as_bytes().len() <= u8::MAX as usize, - ErrCode::InvalidRecordData - ); - require!( - fields.sync_servers.len() <= MAX_SYNC_SERVERS, - ErrCode::InvalidRecordData - ); - for login in &fields.sync_servers { - require!(!login.is_empty(), ErrCode::InvalidRecordData); - require!( - login.as_bytes().len() <= u8::MAX as usize, - ErrCode::InvalidRecordData - ); - } - } else { - require!(fields.server_address.is_empty(), ErrCode::InvalidRecordData); - require!(fields.sync_servers.is_empty(), ErrCode::InvalidRecordData); - } - require!( - fields.access_servers.len() <= u8::MAX as usize, - ErrCode::InvalidRecordData - ); - for login in &fields.access_servers { - require!(!login.is_empty(), ErrCode::InvalidRecordData); - require!( - login.as_bytes().len() <= u8::MAX as usize, - ErrCode::InvalidRecordData - ); - } - validate_sessions_fields(fields.sessions_mode, &fields.sessions)?; - Ok(()) -} - -fn validate_sessions_fields(mode: u8, sessions: &[SessionRecord]) -> Result<()> { - require!( - mode == SESSIONS_MODE_MIXED || mode == SESSIONS_MODE_PDA_ONLY, - ErrCode::InvalidRecordData - ); - require!(sessions.len() <= MAX_SESSIONS, ErrCode::InvalidRecordData); - - for i in 0..sessions.len() { - validate_session_record(&sessions[i])?; - for j in (i + 1)..sessions.len() { - require!( - sessions[i].session_name != sessions[j].session_name, - ErrCode::InvalidRecordData - ); - require!( - sessions[i].session_pub_key != sessions[j].session_pub_key, - ErrCode::InvalidRecordData - ); - } - } - Ok(()) -} - -fn validate_session_record(session: &SessionRecord) -> Result<()> { - require!( - session.session_type == SESSION_TYPE_USER || session.session_type == SESSION_TYPE_SUBSERVER, - ErrCode::InvalidRecordData - ); - require!(session.session_version == 1, ErrCode::InvalidRecordData); - let bytes = session.session_name.as_bytes(); - require!(!bytes.is_empty(), ErrCode::InvalidRecordData); - require!(bytes.len() <= MAX_SESSION_NAME_LEN, ErrCode::InvalidRecordData); - for &b in bytes { - let ok = b.is_ascii_alphanumeric() || b == b'_'; - require!(ok, ErrCode::InvalidRecordData); - } - Ok(()) -} - -fn validate_blockchain_limits( - blockchain: &BlockchainRecord, - old_used_bytes: u64, - old_last_block_number: u32, - is_create: bool, -) -> Result<()> { - require!( - blockchain.blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER, - ErrCode::InvalidRecordData - ); - require!( - blockchain.used_bytes <= blockchain.paid_limit_bytes, - ErrCode::InvalidRecordData - ); - if !is_create { - require!( - blockchain.used_bytes >= old_used_bytes - && blockchain.last_block_number >= old_last_block_number, - ErrCode::InvalidRecordData - ); - } - Ok(()) -} - -fn validate_inflow_vault(inflow_vault: &AccountInfo) -> Result<()> { - let payments_program_id = Pubkey::from_str(settings::SHINE_PAYMENTS_PROGRAM_ID) - .map_err(|_| error!(ErrCode::InvalidFeeReceiver))?; - let (expected, _) = Pubkey::find_program_address( - &[settings::SHINE_PAYMENTS_INFLOW_VAULT_SEED], - &payments_program_id, - ); - require_keys_eq!(expected, *inflow_vault.key, ErrCode::InvalidFeeReceiver); - Ok(()) -} - -fn transfer_lamports<'info>( - payer: &AccountInfo<'info>, - recipient: &AccountInfo<'info>, - system_program: &AccountInfo<'info>, - lamports: u64, -) -> Result<()> { - if lamports == 0 { - return Ok(()); - } - let ix = system_instruction::transfer(payer.key, recipient.key, lamports); - invoke( - &ix, - &[payer.clone(), recipient.clone(), system_program.clone()], - )?; - Ok(()) -} - -fn ensure_pda_size_and_rent<'info>( - pda: &AccountInfo<'info>, - payer: &AccountInfo<'info>, - system_program: &AccountInfo<'info>, - required_len: usize, -) -> Result<()> { - let current_len = pda.data_len(); - if required_len <= current_len { - return Ok(()); - } - - let increase = required_len - .checked_sub(current_len) - .ok_or(error!(ErrCode::MathOverflow))?; - require!( - increase <= MAX_AUTO_REALLOC_INCREASE, - ErrCode::RecordTooLarge - ); - - let rent = Rent::get()?; - let required_lamports = rent.minimum_balance(required_len); - let current_lamports = pda.lamports(); - let top_up = required_lamports.saturating_sub(current_lamports); - if top_up > 0 { - transfer_lamports(payer, pda, system_program, top_up)?; - } - - pda.realloc(required_len, false)?; - Ok(()) -} - -fn limit_fee_lamports(limit_delta: u64, lamports_per_limit_step: u64) -> Result { - let units = limit_delta / settings::LIMIT_STEP; - units - .checked_mul(lamports_per_limit_step) - .ok_or(error!(ErrCode::MathOverflow)) -} - -fn find_user_pda(program_id: &Pubkey, login: &str) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[settings::USER_PDA_SEED_PREFIX.as_bytes(), login.as_bytes()], - program_id, - ) -} - -fn find_users_economy_config_pda(program_id: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address(&[settings::USERS_ECONOMY_CONFIG_SEED], program_id) -} - -fn read_users_economy_config(pda: &AccountInfo) -> Result { - let raw = safe_read_pda(pda); - require!(!raw.is_empty(), ErrCode::EmptyPdaData); - let mut slice: &[u8] = &raw; - UsersEconomyConfigState::deserialize(&mut slice) - .map_err(|_| error!(ErrCode::DeserializationError)) -} - -fn pad_to_fixed_size(mut bytes: Vec, target_size: usize) -> Result> { - require!(bytes.len() <= target_size, ErrCode::RecordTooLarge); - bytes.resize(target_size, 0); - Ok(bytes) -} - -fn vec_to_signature(input: &[u8]) -> Result<[u8; 64]> { - require!(input.len() == 64, ErrCode::InvalidSignature); - let mut out = [0u8; 64]; - out.copy_from_slice(input); - Ok(out) -} - -fn vec_to_hash32(input: &[u8]) -> Result<[u8; 32]> { - require!(input.len() == 32, ErrCode::InvalidPrevHash); - let mut out = [0u8; 32]; - out.copy_from_slice(input); - Ok(out) -} - -fn read_u8(data: &[u8], cursor: &mut usize) -> Result { - let v = *data - .get(*cursor) - .ok_or(error!(ErrCode::InvalidRecordData))?; - *cursor += 1; - Ok(v) -} - -fn read_u32(data: &[u8], cursor: &mut usize) -> Result { - let end = cursor - .checked_add(4) - .ok_or(error!(ErrCode::InvalidRecordData))?; - let slice = data - .get(*cursor..end) - .ok_or(error!(ErrCode::InvalidRecordData))?; - *cursor = end; - Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]])) -} - -fn read_u64(data: &[u8], cursor: &mut usize) -> Result { - let end = cursor - .checked_add(8) - .ok_or(error!(ErrCode::InvalidRecordData))?; - let slice = data - .get(*cursor..end) - .ok_or(error!(ErrCode::InvalidRecordData))?; - *cursor = end; - Ok(u64::from_le_bytes([ - slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7], - ])) -} - -fn read_fixed_32(data: &[u8], cursor: &mut usize) -> Result<[u8; 32]> { - let end = cursor - .checked_add(32) - .ok_or(error!(ErrCode::InvalidRecordData))?; - let slice = data - .get(*cursor..end) - .ok_or(error!(ErrCode::InvalidRecordData))?; - *cursor = end; - let mut out = [0u8; 32]; - out.copy_from_slice(slice); - Ok(out) -} - -fn read_fixed_64(data: &[u8], cursor: &mut usize) -> Result<[u8; 64]> { - let end = cursor - .checked_add(64) - .ok_or(error!(ErrCode::InvalidRecordData))?; - let slice = data - .get(*cursor..end) - .ok_or(error!(ErrCode::InvalidRecordData))?; - *cursor = end; - let mut out = [0u8; 64]; - out.copy_from_slice(slice); - Ok(out) -} - -fn read_len_prefixed_string(data: &[u8], cursor: &mut usize) -> Result { - let len = read_u8(data, cursor)? as usize; - let end = cursor - .checked_add(len) - .ok_or(error!(ErrCode::InvalidRecordData))?; - let slice = data - .get(*cursor..end) - .ok_or(error!(ErrCode::InvalidRecordData))?; - *cursor = end; - let value = std::str::from_utf8(slice).map_err(|_| error!(ErrCode::InvalidRecordData))?; - Ok(value.to_string()) -}