Новый протокол Solana PDA 1.2

This commit is contained in:
AidarKC
2026-09-26 11:01:14 +03:00
parent 6e5b57fd7c
commit c997057a37
47 changed files with 2392 additions and 4480 deletions
+5 -14
View File
@@ -42,7 +42,7 @@
</div>
<h1>Регистрация серверного аккаунта</h1>
<p class="subtitle">Создаёт user_pda в Solana с флагом is_server=true</p>
<p class="subtitle">Создаёт серверную user_pda в Solana (PDA 1.2)</p>
<div class="card">
<h2>Параметры Solana</h2>
@@ -65,12 +65,8 @@
<input type="text" id="serverAddress" placeholder="Адрес сервера" />
</div>
<div class="field">
<label>Логины серверов c которыми будет синхронизировать записи в блокчейн этот сервер (Например shineupme)</label>
<textarea id="syncServers" placeholder="По одному логину на строку (можно оставить пустым)"></textarea>
</div>
<div class="field">
<label>Серверы доступа (access_servers, опционально)</label>
<textarea id="accessServers" placeholder="Обычно пусто для серверного PDA"></textarea>
<label>Сервер доступа (access_servers, 0 или 1)</label>
<textarea id="accessServers" placeholder="Один логин сервера доступа или пусто"></textarea>
</div>
</div>
@@ -99,17 +95,12 @@
<div class="sec-lbl">Ключевые пары (base58)</div>
<div class="kp-block">
<div class="kp-title">Recovery Key — восстановление аккаунта</div>
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="recoveryPub" placeholder="base58, ~44 символа" /></div>
<div class="kp-row"><span class="kp-lbl">Приватный</span><input class="kp-inp" type="text" id="recoveryPriv" placeholder="seed base58, ~44 символа" /></div>
</div>
<div class="kp-block">
<div class="kp-title">Root Key — подпись PDA-записи</div>
<div class="kp-title">Root Key — холодное восстановление / полный контроль PDA</div>
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="rootPub" placeholder="base58, ~44 символа" /></div>
<div class="kp-row"><span class="kp-lbl">Приватный</span><input class="kp-inp" type="text" id="rootPriv" placeholder="seed base58, ~44 символа" /></div>
</div>
<div class="kp-block">
<div class="kp-title">Blockchain Key — подпись LastBlockState</div>
<div class="kp-title">Blockchain Key — активный ключ блокчейна и обычный authority PDA</div>
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="bchPub" placeholder="base58, ~44 символа" /></div>
<div class="kp-row"><span class="kp-lbl">Приватный</span><input class="kp-inp" type="text" id="bchPriv" placeholder="seed base58, ~44 символа" /></div>
</div>
@@ -20,8 +20,6 @@ import { defaultServerAddress, defaultServerLogin, defaultSolanaEndpoint } from
const fieldMap = {
masterSecret: 'masterSecret',
recoveryPub: 'recoveryPub',
recoveryPriv: 'recoveryPriv',
rootPub: 'rootPub',
rootPriv: 'rootPriv',
bchPub: 'bchPub',
@@ -113,7 +111,6 @@ $('btnCreate').addEventListener('click', async () => {
login,
keyBundle,
serverAddress,
syncServers: parseLoginList($('syncServers').value),
accessServers: parseLoginList($('accessServers').value),
solanaEndpoint: endpoint,
});
+38 -67
View File
@@ -112,6 +112,7 @@ function renderParsed(parsed) {
renderRows([
{ label: 'PDA адрес', value: parsed.pdaAddress },
{ label: 'Формат', value: `${parsed.formatMajor}.${parsed.formatMinor}` },
{ label: 'Логин', value: parsed.login },
{ label: 'Статус', value: parsed.isServer ? 'server' : 'not server' },
{ label: 'recordNumber', value: String(parsed.recordNumber) },
@@ -126,11 +127,10 @@ function renderParsed(parsed) {
const badgeLine = $('badgeLine');
badgeLine.innerHTML = '';
const badges = [
{ label: 'format=1.2', kind: 'ok' },
{ label: `server=${parsed.isServer ? '1' : '0'}`, kind: parsed.isServer ? 'ok' : 'warn' },
{ label: `trusted=${Number(parsed.trustedCount || 0)}`, kind: Number(parsed.trustedCount || 0) > 0 ? 'ok' : 'warn' },
{ label: `sessions=${Array.isArray(parsed.sessions) ? parsed.sessions.length : 0}`, kind: 'info' },
{ label: `forks=${Array.isArray(parsed.forks) ? parsed.forks.length : 0}`, kind: 'info' },
{ label: `access=${Array.isArray(parsed.accessServers) ? parsed.accessServers.length : 0}`, kind: 'info' },
{ label: `sync=${Array.isArray(parsed.syncServers) ? parsed.syncServers.length : 0}`, kind: 'info' },
];
badges.forEach((item) => {
const badge = document.createElement('span');
@@ -141,84 +141,55 @@ function renderParsed(parsed) {
const blocks = $('blocksList');
blocks.innerHTML = '';
blocks.appendChild(renderBlock('RecoveryKeyBlock', 'block_type=0', [
{ label: 'recoveryKey32', value: base58(parsed.recoveryKey) },
]));
blocks.appendChild(renderBlock('RootKeyBlock', 'block_type=1', [
blocks.appendChild(renderBlock('RootKeyBlock', 'block_type=1, version=0', [
{ label: 'rootKey32', value: base58(parsed.rootKey) },
]));
blocks.appendChild(renderBlock('ClientKeyBlock', 'block_type=2', [
blocks.appendChild(renderBlock('ClientKeyBlock', 'block_type=2, version=0', [
{ label: 'clientKey32', value: base58(parsed.clientKey) },
]));
blocks.appendChild(renderBlock('BlockchainRegistryBlock', 'block_type=3', [
{ label: 'blockchainType', value: String(parsed.blockchain?.blockchainType ?? 0) },
{ label: 'blockchainName', value: parsed.blockchain?.blockchainName || '—' },
{ label: 'blockchainPublicKey32', value: base58(parsed.blockchain?.blockchainPublicKey) },
{ label: 'paidLimitBytes', value: formatBigInt(parsed.blockchain?.paidLimitBytes || 0n) },
{ label: 'usedBytes', value: formatBigInt(parsed.blockchain?.usedBytes || 0n) },
{ label: 'lastBlockNumber', value: String(parsed.blockchain?.lastBlockNumber ?? 0) },
{ label: 'lastBlockHash32', value: hex(parsed.blockchain?.lastBlockHash) },
{ label: 'lastBlockSignature64', value: base58(parsed.blockchain?.lastBlockSignature) },
{ label: 'arweaveTxId', value: parsed.blockchain?.arweaveTxId || '—' },
]));
blocks.appendChild(renderBlock('ServerProfileBlock', 'block_type=30', [
{ label: 'isServer', value: parsed.isServer ? '1' : '0' },
{ label: 'addressFormatType', value: String(parsed.addressFormatType ?? 0) },
{ label: 'addressFormatVersion', value: String(parsed.addressFormatVersion ?? 0) },
{ label: 'serverAddress', value: parsed.serverAddress || '—' },
{ label: 'syncServersCount', value: String(parsed.syncServers?.length || 0) },
{ label: 'syncServers', value: renderArrayLine(parsed.syncServers) },
]));
blocks.appendChild(renderBlock('AccessServersBlock', 'block_type=40', [
{ label: 'accessServersCount', value: String(parsed.accessServers?.length || 0) },
{ label: 'accessServers', value: renderArrayLine(parsed.accessServers) },
]));
blocks.appendChild(renderBlock('SessionsBlock', 'block_type=50', [
{ label: 'sessionsMode', value: String(parsed.sessionsMode ?? 0) },
{ label: 'sessionsCount', value: String(parsed.sessions?.length || 0) },
{ label: 'sessions', value: parsed.sessions?.length ? 'ниже' : '—' },
]));
blocks.appendChild(renderBlock('TrustedStateBlock', 'block_type=70', [
{ label: 'trustedCount', value: String(parsed.trustedCount ?? 0) },
]));
blocks.appendChild(renderSessionList(parsed.sessions));
$('rawJson').textContent = JSON.stringify({
const forkLines = (parsed.forks || []).map((fork, idx) =>
`#${idx}: ${base58(fork.blockchainKey)} · created=${formatTimestamp(fork.createdAtMs)} · limit=${formatBigInt(fork.paidLimitBytes)}`,
);
blocks.appendChild(renderBlock('BlockchainRegistryBlock', 'block_type=3, version=0', [
{ label: 'forkCount', value: String(parsed.forks?.length || 0) },
{ label: 'forks', value: forkLines.length ? forkLines.join('\n') : '—' },
]));
if (parsed.isServer) {
const addressLines = (parsed.serverAddresses || []).map((item) =>
`type=${item.addressFormatType}, v=${item.addressFormatVersion}: ${item.address}`,
);
blocks.appendChild(renderBlock('ServerProfileBlock', 'block_type=30, version=0', [
{ label: 'addressCount', value: String(parsed.serverAddresses?.length || 0) },
{ label: 'address', value: addressLines.length ? addressLines.join('\n') : '—' },
]));
}
if (Array.isArray(parsed.accessServers) && parsed.accessServers.length) {
blocks.appendChild(renderBlock('AccessServersBlock', 'block_type=40, version=0', [
{ label: 'accessServersCount', value: String(parsed.accessServers.length) },
{ label: 'accessServer', value: renderArrayLine(parsed.accessServers) },
]));
}
const raw = {
pdaAddress: parsed.pdaAddress,
format: `${parsed.formatMajor}.${parsed.formatMinor}`,
login: parsed.login,
isServer: parsed.isServer,
recordNumber: parsed.recordNumber,
createdAtMs: parsed.createdAtMs.toString(),
updatedAtMs: parsed.updatedAtMs.toString(),
recordLen: parsed.recordLen,
trustedCount: parsed.trustedCount,
addressFormatType: parsed.addressFormatType,
addressFormatVersion: parsed.addressFormatVersion,
serverAddress: parsed.serverAddress,
syncServers: parsed.syncServers,
accessServers: parsed.accessServers,
sessionsMode: parsed.sessionsMode,
sessions: parsed.sessions.map((s) => ({
sessionType: s.sessionType,
sessionVersion: s.sessionVersion,
sessionName: s.sessionName,
sessionPubKey32: base58(s.sessionPubKey32),
})),
recoveryKey: base58(parsed.recoveryKey),
prevRecordHash: hex(parsed.prevRecordHash),
rootKey: base58(parsed.rootKey),
clientKey: base58(parsed.clientKey),
blockchain: {
blockchainType: parsed.blockchain.blockchainType,
blockchainName: parsed.blockchain.blockchainName,
blockchainPublicKey: base58(parsed.blockchain.blockchainPublicKey),
paidLimitBytes: parsed.blockchain.paidLimitBytes.toString(),
usedBytes: parsed.blockchain.usedBytes.toString(),
lastBlockNumber: parsed.blockchain.lastBlockNumber,
lastBlockHash: hex(parsed.blockchain.lastBlockHash),
lastBlockSignature: base58(parsed.blockchain.lastBlockSignature),
arweaveTxId: parsed.blockchain.arweaveTxId || '',
},
}, null, 2);
serverAddresses: (parsed.serverAddresses || []).map((a) => ({ ...a })),
accessServers: parsed.accessServers || [],
signature: base58(parsed.signature),
};
$('rawJson').textContent = JSON.stringify(raw, null, 2);
}
$('btnLoad').addEventListener('click', async () => {
+2 -29
View File
@@ -111,18 +111,6 @@ async function pairFromSeedBase58(seedB58, explicitPubB58) {
export async function buildKeyBundleFromForm(fieldMap, options = {}) {
const requireBlockchain = options.requireBlockchain !== false;
let recovery = null;
const masterSecretValue = fieldMap.masterSecret ? String($(fieldMap.masterSecret).value || '').trim() : '';
if (masterSecretValue) {
const masterSecret32 = ensure32Bytes(base58ToBytes(masterSecretValue));
const recoveryPair = await deriveEd25519FromMasterSecret(masterSecret32, 'recovery.key');
recovery = {
publicKeyB64: recoveryPair.publicKeyB64,
privatePkcs8B64: recoveryPair.privatePkcs8B64,
publicKeyB58: bytesToBase58(base64ToBytes(recoveryPair.publicKeyB64)),
privateSeedB58: bytesToBase58(base64ToBytes(recoveryPair.privatePkcs8B64).slice(-32)),
};
}
const root = await pairFromSeedBase58($(fieldMap.rootPriv).value, $(fieldMap.rootPub).value);
const device = await pairFromSeedBase58($(fieldMap.devPriv).value, $(fieldMap.devPub).value);
const blockchainPriv = String($(fieldMap.bchPriv).value || '').trim();
@@ -134,9 +122,6 @@ export async function buildKeyBundleFromForm(fieldMap, options = {}) {
}
return {
keyBundle: {
recoveryPair: recovery
? { publicKeyB64: recovery.publicKeyB64, privatePkcs8B64: recovery.privatePkcs8B64 }
: null,
rootPair: { publicKeyB64: root.publicKeyB64, privatePkcs8B64: root.privatePkcs8B64 },
blockchainPair: blockchain
? { publicKeyB64: blockchain.publicKeyB64, privatePkcs8B64: blockchain.privatePkcs8B64 }
@@ -144,8 +129,6 @@ export async function buildKeyBundleFromForm(fieldMap, options = {}) {
clientPair: { publicKeyB64: device.publicKeyB64, privatePkcs8B64: device.privatePkcs8B64 },
},
normalized: {
recoveryPubB58: recovery?.publicKeyB58 || '',
recoveryPrivB58: recovery?.privateSeedB58 || '',
rootPubB58: root.publicKeyB58,
rootPrivB58: root.privateSeedB58,
bchPubB58: blockchain?.publicKeyB58 || '',
@@ -165,20 +148,14 @@ export async function deriveKeyBundleFromPassword({ login, password, onProgress
login: cleanLogin,
onProgress,
});
const [recoveryPair, rootPair, blockchainPair, clientPair] = await Promise.all([
deriveEd25519FromMasterSecret(masterSecret32, 'recovery.key'),
const [rootPair, blockchainPair, clientPair] = await Promise.all([
deriveEd25519FromMasterSecret(masterSecret32, 'root.key'),
deriveEd25519FromMasterSecret(masterSecret32, 'blockchain.key'),
deriveEd25519FromMasterSecret(masterSecret32, 'client.key'),
]);
return {
masterSecret32,
keyBundle: {
recoveryPair,
rootPair,
blockchainPair,
clientPair,
},
keyBundle: { rootPair, blockchainPair, clientPair },
};
}
@@ -186,10 +163,6 @@ export function fillKeyFields(fieldMap, keyBundle, masterSecret32) {
if (masterSecret32) {
$(fieldMap.masterSecret).value = bytesToBase58(masterSecret32);
}
if (fieldMap.recoveryPub && fieldMap.recoveryPriv && keyBundle.recoveryPair) {
$(fieldMap.recoveryPub).value = bytesToBase58(base64ToBytes(keyBundle.recoveryPair.publicKeyB64));
$(fieldMap.recoveryPriv).value = bytesToBase58(base64ToBytes(keyBundle.recoveryPair.privatePkcs8B64).slice(-32));
}
$(fieldMap.rootPub).value = bytesToBase58(base64ToBytes(keyBundle.rootPair.publicKeyB64));
$(fieldMap.rootPriv).value = bytesToBase58(base64ToBytes(keyBundle.rootPair.privatePkcs8B64).slice(-32));
$(fieldMap.bchPub).value = bytesToBase58(base64ToBytes(keyBundle.blockchainPair.publicKeyB64));
@@ -10,7 +10,6 @@ import {
formatBigInt,
formatTimestamp,
openDevnetTopup,
parseLoginList,
publicKeyBytesToBase58,
refreshDeviceBalance,
setGenMessage,
@@ -26,8 +25,6 @@ import { defaultServerAddress, defaultServerLogin, defaultSolanaEndpoint } from
const fieldMap = {
masterSecret: 'masterSecret',
recoveryPub: 'recoveryPub',
recoveryPriv: 'recoveryPriv',
rootPub: 'rootPub',
rootPriv: 'rootPriv',
bchPub: 'bchPub',
@@ -42,7 +39,6 @@ let currentPda = null;
function resetExpectedKeysUi() {
$('expectedKeysBox').style.display = 'none';
setText('expectedRecoveryPub', '');
setText('expectedRootPub', '');
setText('expectedBchPub', '');
setText('expectedDevPub', '');
@@ -52,7 +48,6 @@ function resetExpectedKeysUi() {
function renderExpectedKeys(parsed) {
$('expectedKeysBox').style.display = 'block';
setText('expectedRecoveryPub', publicKeyBytesToBase58(parsed.recoveryKey));
setText('expectedRootPub', publicKeyBytesToBase58(parsed.rootKey));
setText('expectedBchPub', publicKeyBytesToBase58(parsed.blockchain.blockchainPublicKey));
setText('expectedDevPub', publicKeyBytesToBase58(parsed.clientKey));
@@ -64,7 +59,6 @@ function compareCurrentFormKeysWithPda() {
const blockchainActual = String($('bchPub').value || '').trim();
return {
resultMap: {
recovery: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.recoveryKey), $('recoveryPub').value),
root: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.rootKey), $('rootPub').value),
blockchain: blockchainActual
? compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), blockchainActual)
@@ -178,17 +172,15 @@ $('btnLoad').addEventListener('click', async () => {
$('iCreated').textContent = formatTimestamp(parsed.createdAtMs);
$('iUpdated').textContent = formatTimestamp(parsed.updatedAtMs);
$('iSrvAddr').textContent = parsed.serverAddress || '—';
$('iSync').textContent = parsed.syncServers.length ? parsed.syncServers.join(', ') : '—';
$('iBch').textContent = parsed.blockchain.blockchainName;
$('iLimit').textContent = formatBigInt(parsed.blockchain.paidLimitBytes);
$('password').value = '';
$('serverAddress').value = parsed.serverAddress || '';
$('syncServers').value = parsed.syncServers.join('\n');
$('pdaInfo').style.display = 'block';
$('updateForm').style.display = 'block';
renderExpectedKeys(parsed);
setStatus($('status'), 'PDA загружена. Можно менять адрес или sync_servers.', 'success');
setStatus($('status'), 'PDA загружена. Можно менять адрес сервера.', 'success');
} catch (error) {
setStatus($('status'), error?.message || String(error), 'error');
} finally {
@@ -227,7 +219,6 @@ $('btnUpdate').addEventListener('click', async () => {
serverAddress,
addressFormatType: currentPda.addressFormatType ?? 1,
addressFormatVersion: currentPda.addressFormatVersion ?? 0,
syncServers: parseLoginList($('syncServers').value),
solanaEndpoint: endpoint,
});
+2 -16
View File
@@ -50,7 +50,7 @@
</div>
<h1>Обновление PDA сервера</h1>
<p class="subtitle">Меняет адрес сервера или список серверов синхронизации</p>
<p class="subtitle">Меняет адрес SHiNE-сервера и другие поля PDA 1.2</p>
<div class="card">
<h2>Параметры Solana</h2>
@@ -76,7 +76,6 @@
<div class="pda-row"><span class="pda-key">Создан</span><span class="pda-value" id="iCreated"></span></div>
<div class="pda-row"><span class="pda-key">Обновлён</span><span class="pda-value" id="iUpdated"></span></div>
<div class="pda-row"><span class="pda-key">Адрес сервера</span><span class="pda-value" id="iSrvAddr"></span></div>
<div class="pda-row"><span class="pda-key">sync_servers</span><span class="pda-value" id="iSync"></span></div>
<div class="pda-row"><span class="pda-key">Blockchain</span><span class="pda-value" id="iBch"></span></div>
<div class="pda-row"><span class="pda-key">Paid limit</span><span class="pda-value" id="iLimit"></span></div>
</div>
@@ -89,10 +88,6 @@
<label>Арес сервера (URL) например shineup.me</label>
<input type="text" id="serverAddress" placeholder="Адрес сервера" />
</div>
<div class="field">
<label>Логины серверов c которыми будет синхронизировать записи в блокчейн этот сервер (Например shineupme)</label>
<textarea id="syncServers" placeholder="По одному логину на строку (можно оставить пустым)"></textarea>
</div>
</div>
<div class="card">
@@ -120,12 +115,7 @@
<div class="sec-lbl">Ключевые пары (base58)</div>
<div class="kp-block">
<div class="kp-title">Recovery Key — восстановление аккаунта</div>
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="recoveryPub" placeholder="base58, ~44 символа" /></div>
<div class="kp-row"><span class="kp-lbl">Приватный</span><input class="kp-inp" type="text" id="recoveryPriv" placeholder="seed base58, ~44 символа" /></div>
</div>
<div class="kp-block">
<div class="kp-title">Root Key — подпись PDA-записи</div>
<div class="kp-title">Root Key — холодное восстановление / полный контроль PDA</div>
<div class="kp-row"><span class="kp-lbl">Публичный</span><input class="kp-inp" type="text" id="rootPub" placeholder="base58, ~44 символа" /></div>
<div class="kp-row"><span class="kp-lbl">Приватный</span><input class="kp-inp" type="text" id="rootPriv" placeholder="seed base58, ~44 символа" /></div>
</div>
@@ -150,10 +140,6 @@
<div class="expected-card" id="expectedKeysBox" style="display:none">
<div class="expected-ttl">Какие ключи ожидаются по уже загруженной PDA</div>
<div class="expected-row">
<div class="expected-lbl">Ожидаемый recovery public key</div>
<div class="expected-val" id="expectedRecoveryPub"></div>
</div>
<div class="expected-row">
<div class="expected-lbl">Ожидаемый root public key</div>
<div class="expected-val" id="expectedRootPub"></div>