From eeb115584d2a7b626e0398927a4f39a061b954870719cc51a040af84022d18c8 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 3 Jun 2026 16:12:40 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D1=83=20server=20PDA=20=D0=B8=20=D0=B1=D0=B0=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=D1=81=20device=20(=D0=BD=D0=B5=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B5=D0=BD=D0=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...диагностика_server_pda_и_device_balance.md | 17 ++++ .../README.md | 9 +- VERSION.properties | 4 +- .../js/services/shine-user-pda-service.js | 4 +- shine-UI/server-ui/create-server-pda.html | 6 +- .../server-ui/js/create-server-pda-page.js | 19 +++- shine-UI/server-ui/js/server-ui-shared.js | 45 ++++++++++ .../server-ui/js/update-server-pda-page.js | 88 ++++++++++++++++++- shine-UI/server-ui/update-server-pda.html | 28 ++++++ .../shine/doc/SHiNE-user-format-v.1.0.md | 6 ++ 10 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 Dev_Docs/Pending_Features/2026-06-03_1648_диагностика_server_pda_и_device_balance.md diff --git a/Dev_Docs/Pending_Features/2026-06-03_1648_диагностика_server_pda_и_device_balance.md b/Dev_Docs/Pending_Features/2026-06-03_1648_диагностика_server_pda_и_device_balance.md new file mode 100644 index 0000000..4cabd96 --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-03_1648_диагностика_server_pda_и_device_balance.md @@ -0,0 +1,17 @@ +# Диагностика ключей server PDA и баланс device + +- статус: pending +- кратко: на странице обновления server PDA добавлена сверка ожидаемых ключей с уже загруженной PDA, предупреждение о неверном пароле, кнопка показа баланса device-аккаунта и уточнение, что create/update оплачиваются с deviceKey. + +## Что проверять +- На `update-server-pda.html` загрузить существующую PDA и убедиться, что видны ожидаемые `root/blockchain/device` public key. +- Ввести правильный пароль и нажать `Сгенерировать`: должно появиться сообщение, что ключи совпадают. +- Ввести неверный пароль и нажать `Сгенерировать`: должно появиться сообщение, что ключи не совпали и пароль, вероятно, неверный. +- На `create-server-pda.html` и `update-server-pda.html` нажать `Показать / обновить баланс device` и убедиться, что баланс читается по текущему `devPub`. +- Повторить `update_user_pda` после увеличения `heap frame` и проверить, ушла ли ошибка `memory allocation failed`. + +## Ожидаемый результат +- Пользователь видит, какие именно public key должны получиться для загруженной PDA. +- Ошибка неправильного пароля выявляется до отправки транзакции. +- Баланс device-кошелька читается прямо со страницы. +- Если проблема `OOM` была только в размере heap frame/compute budget клиента, `update_user_pda` начинает проходить. diff --git a/Dev_Docs/Инициализация_Solana_регистрации/README.md b/Dev_Docs/Инициализация_Solana_регистрации/README.md index 450c10c..72375a5 100644 --- a/Dev_Docs/Инициализация_Solana_регистрации/README.md +++ b/Dev_Docs/Инициализация_Solana_регистрации/README.md @@ -82,10 +82,17 @@ anchor deploy -p shine_users - seed: `shine_users_economy_config` - program: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm` +## Кто оплачивает create/update user_pda + +- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `deviceKey`. +- В UI это означает, что Solana fee payer всегда берётся из `device`-ключа пользователя/сервера. +- `rootKey` нужен для подписи unsigned PDA-записи, но не оплачивает транзакцию. +- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `deviceKey`. + ## Важно - `init_users_economy_config` выполняется один раз на программу. Если PDA уже создан, повторный вызов вернёт ошибку "already initialized" (это нормальное поведение). -- Серверные приватные ключи для Solana не используются: подписание делается кошельком пользователя в UI. +- Серверные приватные ключи для Solana не используются как отдельный backend-wallet: в UI/server UI транзакцию оплачивает именно `deviceKey`, а содержимое записи подписывает `rootKey`. - `shine_users` внутри `create_user_pda` требует корректный адрес `shine_login_guard` для CPI-классификации логина. Несовпадение адреса приведёт к ошибке регистрации. diff --git a/VERSION.properties b/VERSION.properties index 8b8b226..60c3ce7 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.120 -server.version=1.2.112 +client.version=1.2.121 +server.version=1.2.113 diff --git a/shine-UI/js/services/shine-user-pda-service.js b/shine-UI/js/services/shine-user-pda-service.js index 8f38e9b..dead6a3 100644 --- a/shine-UI/js/services/shine-user-pda-service.js +++ b/shine-UI/js/services/shine-user-pda-service.js @@ -913,8 +913,8 @@ export async function updateShineUserPdaOnSolana({ ], data: ixData, }); - const computeIx = solana.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }); - const heapIx = solana.ComputeBudgetProgram.requestHeapFrame({ bytes: 131_072 }); + const computeIx = solana.ComputeBudgetProgram.setComputeUnitLimit({ units: 800_000 }); + const heapIx = solana.ComputeBudgetProgram.requestHeapFrame({ bytes: 262_144 }); const signature = await solana.sendAndConfirmTransaction( connection, diff --git a/shine-UI/server-ui/create-server-pda.html b/shine-UI/server-ui/create-server-pda.html index 1de37ce..99e2248 100644 --- a/shine-UI/server-ui/create-server-pda.html +++ b/shine-UI/server-ui/create-server-pda.html @@ -29,6 +29,8 @@ .sol-adr { font-family:monospace; font-size:12px; word-break:break-all; margin-top:4px; } .sol-ht { font-size:11px; color:var(--text-muted); margin-top:4px; } .sol-topup-btn { margin-top:10px; width:100%; } + .sol-balance-btn { margin-top:10px; width:100%; } + .sol-balance { font-size:11px; color:var(--text-muted); margin-top:8px; word-break:break-all; } @@ -112,8 +114,10 @@
Положите SOL на этот адрес перед регистрацией:
-
Это Solana-адрес device-ключа (public key в base58 = Solana-адрес). С него оплачивается создание PDA.
+
Это Solana-адрес device-ключа (public key в base58 = Solana-адрес). С него оплачиваются и создание, и обновление PDA.
+ +
Баланс device ещё не запрашивался.
diff --git a/shine-UI/server-ui/js/create-server-pda-page.js b/shine-UI/server-ui/js/create-server-pda-page.js index 49bc60c..e1a61b6 100644 --- a/shine-UI/server-ui/js/create-server-pda-page.js +++ b/shine-UI/server-ui/js/create-server-pda-page.js @@ -8,6 +8,7 @@ import { deriveKeyBundleFromPassword, fillKeyFields, parseLoginList, + refreshDeviceBalance, setGenMessage, setStatus, setupPasswordEye, @@ -40,6 +41,21 @@ $('btnTopupDevnet').addEventListener('click', () => { } }); +$('btnRefreshBalance').addEventListener('click', async () => { + try { + $('btnRefreshBalance').disabled = true; + await refreshDeviceBalance({ + endpoint: $('endpoint').value, + deviceAddress: $('devPub').value, + targetNode: $('deviceBalance'), + }); + } catch (error) { + setStatus($('status'), error?.message || String(error), 'error'); + } finally { + $('btnRefreshBalance').disabled = false; + } +}); + $('btnGen').addEventListener('click', async () => { clearGenMessage($('genMsg')); clearStatus($('status')); @@ -53,7 +69,8 @@ $('btnGen').addEventListener('click', async () => { }); fillKeyFields(fieldMap, keyBundle, masterSecret32); updateSolAddress(fieldMap); - setGenMessage($('genMsg'), 'Ключи и master secret сгенерированы из логина и пароля.', 'ok'); + $('deviceBalance').textContent = 'Теперь можно проверить баланс device-аккаунта.'; + setGenMessage($('genMsg'), 'Ключи и master secret сгенерированы из логина и пароля. Транзакцию оплатит device-ключ.', 'ok'); } catch (error) { setGenMessage($('genMsg'), error?.message || String(error), 'err'); } finally { diff --git a/shine-UI/server-ui/js/server-ui-shared.js b/shine-UI/server-ui/js/server-ui-shared.js index 7cb8841..e8415b2 100644 --- a/shine-UI/server-ui/js/server-ui-shared.js +++ b/shine-UI/server-ui/js/server-ui-shared.js @@ -7,6 +7,7 @@ import { deriveMasterSecretFromPassword, publicKeyB64FromPkcs8Ed25519, } from '../../js/services/crypto-utils.js'; +import { formatSol, getBalanceSol } from '../../js/services/solana-wallet-service.js'; const LOGIN_RE = /^[a-z0-9_]{1,20}$/; const ED25519_PKCS8_PREFIX = new Uint8Array([ @@ -189,12 +190,56 @@ export function updateSolAddress(fieldMap) { } } +export function setText(idOrNode, value) { + const node = typeof idOrNode === 'string' ? $(idOrNode) : idOrNode; + if (node) node.textContent = String(value || ''); +} + export function wireDeviceAddressPreview(fieldMap) { const update = () => updateSolAddress(fieldMap); $(fieldMap.devPub).addEventListener('input', update); update(); } +export function publicKeyBytesToBase58(value) { + return bytesToBase58(value instanceof Uint8Array ? value : new Uint8Array(value || [])); +} + +export function compareExpectedPublicKeys(expected, actual) { + const exp = String(expected || '').trim(); + const act = String(actual || '').trim(); + return { + matches: Boolean(exp) && Boolean(act) && exp === act, + expected: exp, + actual: act, + }; +} + +export function summarizeKeyComparison(resultMap) { + const labels = { + root: 'root', + blockchain: 'blockchain', + device: 'device', + }; + const mismatches = Object.entries(resultMap) + .filter(([, result]) => !result.matches) + .map(([key]) => labels[key] || key); + return { + allMatch: mismatches.length === 0, + mismatches, + }; +} + +export async function refreshDeviceBalance({ endpoint, deviceAddress, targetNode }) { + const cleanEndpoint = String(endpoint || '').trim(); + const cleanAddress = String(deviceAddress || '').trim(); + if (!cleanEndpoint) throw new Error('Укажите Solana endpoint'); + if (!cleanAddress) throw new Error('Сначала укажите device-адрес'); + const balance = await getBalanceSol({ endpoint: cleanEndpoint, address: cleanAddress }); + setText(targetNode, `Баланс device: ${formatSol(balance.sol, 6)} SOL (${balance.lamports} lamports)`); + return balance; +} + export function buildDevnetTopupUrl(walletAddress) { const cleanWallet = String(walletAddress || '').trim(); const url = new URL('../devnet-topup-view', window.location.href); diff --git a/shine-UI/server-ui/js/update-server-pda-page.js b/shine-UI/server-ui/js/update-server-pda-page.js index 7eeadf7..821a222 100644 --- a/shine-UI/server-ui/js/update-server-pda-page.js +++ b/shine-UI/server-ui/js/update-server-pda-page.js @@ -4,15 +4,20 @@ import { buildKeyBundleFromForm, clearGenMessage, clearStatus, + compareExpectedPublicKeys, deriveKeyBundleFromPassword, fillKeyFields, formatBigInt, formatTimestamp, openDevnetTopup, parseLoginList, + publicKeyBytesToBase58, + refreshDeviceBalance, setGenMessage, setStatus, + setText, setupPasswordEye, + summarizeKeyComparison, updateSolAddress, validateLoginOrThrow, wireDeviceAddressPreview, @@ -32,9 +37,62 @@ const fieldMap = { let currentPda = null; +function resetExpectedKeysUi() { + $('expectedKeysBox').style.display = 'none'; + setText('expectedRootPub', ''); + setText('expectedBchPub', ''); + setText('expectedDevPub', ''); + clearGenMessage($('expectedKeysStatus')); + setText('deviceBalance', 'Баланс device ещё не запрашивался.'); +} + +function renderExpectedKeys(parsed) { + $('expectedKeysBox').style.display = 'block'; + setText('expectedRootPub', publicKeyBytesToBase58(parsed.rootKey)); + setText('expectedBchPub', publicKeyBytesToBase58(parsed.blockchain.blockchainPublicKey)); + setText('expectedDevPub', publicKeyBytesToBase58(parsed.deviceKey)); + setGenMessage($('expectedKeysStatus'), 'После генерации ключей этот блок покажет, совпадают ли они с уже записанной PDA.', 'warn'); +} + +function compareCurrentFormKeysWithPda() { + if (!currentPda) throw new Error('Сначала загрузите PDA'); + const blockchainActual = String($('bchPub').value || '').trim(); + return { + resultMap: { + root: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.rootKey), $('rootPub').value), + blockchain: blockchainActual + ? compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), blockchainActual) + : { matches: true, expected: publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), actual: '' }, + device: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.deviceKey), $('devPub').value), + }, + }; +} + +function renderComparisonStatus() { + if (!currentPda) return false; + try { + const { resultMap } = compareCurrentFormKeysWithPda(); + const summary = summarizeKeyComparison(resultMap); + if (summary.allMatch) { + setGenMessage($('expectedKeysStatus'), 'Ключи совпадают с загруженной PDA. Похоже, пароль верный.', 'ok'); + return true; + } + setGenMessage( + $('expectedKeysStatus'), + `Не совпали ключи: ${summary.mismatches.join(', ')}. Похоже, пароль неверный или введены не те ключи.`, + 'err', + ); + return false; + } catch (error) { + setGenMessage($('expectedKeysStatus'), error?.message || String(error), 'err'); + return false; + } +} + setupPasswordEye($('btnEye'), $('password')); wireDeviceAddressPreview(fieldMap); $('password').value = ''; +resetExpectedKeysUi(); $('btnTopupDevnet').addEventListener('click', () => { try { @@ -44,6 +102,21 @@ $('btnTopupDevnet').addEventListener('click', () => { } }); +$('btnRefreshBalance').addEventListener('click', async () => { + try { + $('btnRefreshBalance').disabled = true; + await refreshDeviceBalance({ + endpoint: $('endpoint').value, + deviceAddress: $('devPub').value, + targetNode: $('deviceBalance'), + }); + } catch (error) { + setStatus($('status'), error?.message || String(error), 'error'); + } finally { + $('btnRefreshBalance').disabled = false; + } +}); + $('btnGen').addEventListener('click', async () => { clearGenMessage($('genMsg')); clearStatus($('status')); @@ -54,7 +127,15 @@ $('btnGen').addEventListener('click', async () => { const { masterSecret32, keyBundle } = await deriveKeyBundleFromPassword({ login, password }); fillKeyFields(fieldMap, keyBundle, masterSecret32); updateSolAddress(fieldMap); - setGenMessage($('genMsg'), 'Ключи и master secret сгенерированы из логина и пароля.', 'ok'); + $('deviceBalance').textContent = 'Теперь можно проверить баланс device-аккаунта.'; + const keysOk = renderComparisonStatus(); + setGenMessage( + $('genMsg'), + keysOk + ? 'Ключи и master secret сгенерированы из логина и пароля. Device-ключ оплатит транзакцию.' + : 'Ключи сгенерированы, но не совпали с уже загруженной PDA. Скорее всего, пароль неверный.', + keysOk ? 'ok' : 'err', + ); } catch (error) { setGenMessage($('genMsg'), error?.message || String(error), 'err'); } finally { @@ -69,6 +150,7 @@ $('btnLoad').addEventListener('click', async () => { currentPda = null; $('pdaInfo').style.display = 'none'; $('updateForm').style.display = 'none'; + resetExpectedKeysUi(); try { const login = validateLoginOrThrow($('login').value); const endpoint = String($('endpoint').value || '').trim(); @@ -93,6 +175,7 @@ $('btnLoad').addEventListener('click', async () => { $('syncServers').value = parsed.syncServers.join('\n'); $('pdaInfo').style.display = 'block'; $('updateForm').style.display = 'block'; + renderExpectedKeys(parsed); setStatus($('status'), 'PDA загружена. Можно менять адрес или sync_servers.', 'success'); } catch (error) { setStatus($('status'), error?.message || String(error), 'error'); @@ -121,6 +204,9 @@ $('btnUpdate').addEventListener('click', async () => { $('devPub').value = normalized.devPubB58; $('devPriv').value = normalized.devPrivB58; updateSolAddress(fieldMap); + if (!renderComparisonStatus()) { + throw new Error('Ключи не совпадают с загруженной PDA. Проверьте пароль или введённые ключи.'); + } setStatus($('status'), 'Отправка update_user_pda в Solana...', 'info'); const result = await updateServerOnSolana({ diff --git a/shine-UI/server-ui/update-server-pda.html b/shine-UI/server-ui/update-server-pda.html index 941700b..e21a7e3 100644 --- a/shine-UI/server-ui/update-server-pda.html +++ b/shine-UI/server-ui/update-server-pda.html @@ -30,6 +30,15 @@ .sol-ht { font-size:11px; color:var(--text-muted); margin-top:4px; } .muted { font-size:12px; color:var(--text-muted); margin-bottom:14px; line-height:1.6; } .sol-topup-btn { margin-top:10px; width:100%; } + .sol-balance-btn { margin-top:10px; width:100%; } + .sol-balance { font-size:11px; color:var(--text-muted); margin-top:8px; word-break:break-all; } + .expected-card { margin-top:14px; background:#111520; border:1px solid #243147; border-radius:var(--radius); padding:12px 14px; } + .expected-ttl { font-size:12px; font-weight:600; color:#9dc4ff; margin-bottom:8px; } + .expected-row { margin-bottom:8px; } + .expected-row:last-child { margin-bottom:0; } + .expected-lbl { font-size:11px; color:var(--text-muted); margin-bottom:4px; } + .expected-val { font-family:monospace; font-size:11px; word-break:break-all; } + .gen-msg.warn { display:block; background:#2f2614; border:1px solid #5f4b22; color:#ffd37a; } @@ -128,8 +137,27 @@
Это Solana-адрес (base58) device-ключа. С него оплачивается транзакция.
+ +
Баланс device ещё не запрашивался.
+ +
diff --git a/shine-solana/shine/doc/SHiNE-user-format-v.1.0.md b/shine-solana/shine/doc/SHiNE-user-format-v.1.0.md index e4cebb8..1efee8c 100644 --- a/shine-solana/shine/doc/SHiNE-user-format-v.1.0.md +++ b/shine-solana/shine/doc/SHiNE-user-format-v.1.0.md @@ -32,6 +32,12 @@ Один логин соответствует одной `user_pda`. +## 2.1. Кто оплачивает create/update PDA + +- Инструкции `create_user_pda` и `update_user_pda` оплачиваются с `device_key`. +- `root_key` используется для подписи unsigned части записи через Ed25519 instruction и не является fee payer. +- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `device_key`. + ## 3. Общие правила кодирования - Числа кодируются в Little Endian.