SHiNE-server/shine-server-UI/js/server-pda-core.js

711 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Логика управления серверной PDA в Solana (shine_users)
// Автономный модуль для панели администратора сервера SHiNE
const SHINE_USERS_PROGRAM_ID = 'FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm';
const SHINE_PAYMENTS_PROGRAM_ID = 'm48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR';
const SHINE_LOGIN_GUARD_PROGRAM_ID = '3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo';
const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111';
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';
// Discriminator create_user_pda (sha256("global:create_user_pda")[0..8])
const CREATE_USER_PDA_DISCRIMINATOR = new Uint8Array([139, 157, 13, 41, 142, 174, 226, 214]);
let _solanaLib = null;
async function loadSolanaLib() {
if (!_solanaLib) _solanaLib = await import('https://esm.sh/@solana/web3.js@1.98.4?bundle');
return _solanaLib;
}
let _argon2Lib = null;
async function loadArgon2() {
if (!_argon2Lib) _argon2Lib = await import('https://esm.sh/@noble/hashes@1.8.0/argon2.js');
return _argon2Lib;
}
// -------------------------------------------------------------------
// Crypto (WebCrypto, Ed25519)
// -------------------------------------------------------------------
async function sha256Bytes(bytes) {
const buf = await crypto.subtle.digest('SHA-256', bytes);
return new Uint8Array(buf);
}
async function signEd25519(pkcs8B64, messageBytes) {
const pkcs8 = Uint8Array.from(atob(pkcs8B64), c => c.charCodeAt(0));
const key = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, false, ['sign']);
const sig = await crypto.subtle.sign({ name: 'Ed25519' }, key, messageBytes);
return new Uint8Array(sig);
}
function base64ToBytes(b64) {
return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
}
function extractSeed32FromPkcs8B64(pkcs8B64) {
// Ed25519 PKCS8 (48 байт): seed расположен начиная с байта 16
return base64ToBytes(pkcs8B64).slice(16, 48);
}
async function anchorDiscriminator(name) {
const hash = await sha256Bytes(new TextEncoder().encode(`global:${name}`));
return hash.slice(0, 8);
}
// -------------------------------------------------------------------
// Borsh-кодирование (Anchor-совместимое)
// -------------------------------------------------------------------
function pushU32LE(buf, v) {
const n = v >>> 0;
buf.push(n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF, (n >> 24) & 0xFF);
}
function pushU64LE(buf, bigV) {
const b = typeof bigV === 'bigint' ? bigV : BigInt(bigV);
const lo = Number(b & 0xFFFFFFFFn) >>> 0;
const hi = Number((b >> 32n) & 0xFFFFFFFFn) >>> 0;
pushU32LE(buf, lo);
pushU32LE(buf, hi);
}
class BorshBuf {
constructor() { this._b = []; }
u8(v) { this._b.push(v & 0xFF); }
u32(v) { pushU32LE(this._b, v); }
u64(v) { pushU64LE(this._b, v); }
bool(v) { this.u8(v ? 1 : 0); }
bytes32(b) { for (const x of b) this._b.push(x); }
vecU8(b) { this.u32(b.length); for (const x of b) this._b.push(x); }
str(s) {
const enc = new TextEncoder().encode(s);
this.u32(enc.length);
for (const x of enc) this._b.push(x);
}
vecStr(arr) {
this.u32(arr.length);
for (const s of arr) this.str(s);
}
raw(bytes) { for (const x of bytes) this._b.push(x); }
result() { return new Uint8Array(this._b); }
}
// -------------------------------------------------------------------
// Построение бинарного формата PDA (matches Rust serialize_unsigned_record)
// -------------------------------------------------------------------
function buildLastBlockStateBytes(login, blockchainName, lastBlockNumber, lastBlockHash32, usedBytes) {
const enc = new TextEncoder();
const buf = [];
for (const x of enc.encode('SHiNE_LAST_BLOCK')) buf.push(x);
const loginB = enc.encode(login);
buf.push(loginB.length); for (const x of loginB) buf.push(x);
const bchB = enc.encode(blockchainName);
buf.push(bchB.length); for (const x of bchB) buf.push(x);
pushU32LE(buf, lastBlockNumber);
for (const x of lastBlockHash32) buf.push(x);
pushU64LE(buf, usedBytes);
return new Uint8Array(buf);
}
function buildUnsignedRecordBytesServer({
login, createdAtMs, updatedAtMs, recordNumber, prevHash32,
rootKey32, deviceKey32, blockchainKey32, blockchainName,
paidLimitBytes, usedBytes, lastBlockNumber, lastBlockHash32, lastBlockSig64, arweaveTxId,
serverAddress, addressFormatType, addressFormatVersion, syncServers, accessServers, trustedCount,
}) {
const enc = new TextEncoder();
const loginB = enc.encode(login);
const bchB = enc.encode(blockchainName);
const buf = [];
// Заголовок: MAGIC(5) + FORMAT_MAJOR(1) + FORMAT_MINOR(1) + record_len_placeholder(2)
buf.push(0x53, 0x48, 0x69, 0x4E, 0x45, 1, 0, 0, 0); // байты 0..8
pushU64LE(buf, createdAtMs);
pushU64LE(buf, updatedAtMs);
pushU32LE(buf, recordNumber);
for (const x of prevHash32) buf.push(x);
buf.push(loginB.length);
for (const x of loginB) buf.push(x);
buf.push(6); // blocks_count = 6 (сервер)
// RootKeyBlock (type=1, ver=0)
buf.push(1, 0);
for (const x of rootKey32) buf.push(x);
// DeviceKeyBlock (type=2, ver=0)
buf.push(2, 0);
for (const x of deviceKey32) buf.push(x);
// BlockchainRegistryBlock (type=3, ver=0, count=1, blockchain_type=1)
buf.push(3, 0, 1, 1);
buf.push(bchB.length); for (const x of bchB) buf.push(x);
for (const x of blockchainKey32) buf.push(x);
pushU64LE(buf, paidLimitBytes);
pushU64LE(buf, usedBytes);
pushU32LE(buf, lastBlockNumber);
for (const x of lastBlockHash32) buf.push(x);
for (const x of lastBlockSig64) buf.push(x);
if (arweaveTxId) {
buf.push(1);
const aTxB = enc.encode(arweaveTxId);
buf.push(aTxB.length); for (const x of aTxB) buf.push(x);
} else {
buf.push(0);
}
// ServerProfileBlock (type=30, ver=0)
buf.push(30, 0);
buf.push(1); // is_server = 1
buf.push(addressFormatType & 0xFF);
buf.push(addressFormatVersion & 0xFF);
const srvB = enc.encode(serverAddress);
buf.push(srvB.length); for (const x of srvB) buf.push(x);
buf.push(syncServers.length);
for (const srv of syncServers) {
const sB = enc.encode(srv);
buf.push(sB.length); for (const x of sB) buf.push(x);
}
// AccessServersBlock (type=40, ver=0)
buf.push(40, 0, accessServers.length);
for (const srv of accessServers) {
const sB = enc.encode(srv);
buf.push(sB.length); for (const x of sB) buf.push(x);
}
// TrustedStateBlock (type=50, ver=0)
buf.push(50, 0, trustedCount & 0xFF);
// Записываем record_len: (длина буфера + 64 байта подписи)
const recLen = buf.length + 64;
buf[7] = recLen & 0xFF;
buf[8] = (recLen >> 8) & 0xFF;
return new Uint8Array(buf);
}
// -------------------------------------------------------------------
// Borsh-сериализация Anchor-инструкций
// -------------------------------------------------------------------
function serializeCreateServerPdaArgs({
login, rootKey32, createdAtMs, deviceKey32, blockchainKey32,
blockchainName, usedBytes, lastBlockNumber, lastBlockHash32,
lastBlockSig64, arweaveTxId, serverAddress, addressFormatType,
addressFormatVersion, syncServers, accessServers, trustedCount, rootSig64,
}) {
const b = new BorshBuf();
b.raw(CREATE_USER_PDA_DISCRIMINATOR);
b.str(login);
b.bytes32(rootKey32);
b.u64(createdAtMs);
b.u64(0n); // additional_limit
// UserMutableFields:
b.bytes32(deviceKey32);
b.bytes32(blockchainKey32);
b.str(blockchainName);
b.u64(usedBytes);
b.u32(lastBlockNumber);
b.vecU8(lastBlockHash32);
b.vecU8(lastBlockSig64);
b.str(arweaveTxId);
b.bool(true); // is_server
b.u8(addressFormatType);
b.u8(addressFormatVersion);
b.str(serverAddress);
b.vecStr(syncServers);
b.vecStr(accessServers);
b.u8(trustedCount);
b.vecU8(rootSig64);
return b.result();
}
async function serializeUpdateServerPdaArgs({
login, rootKey32, createdAtMs, updatedAtMs, version, prevHash32,
deviceKey32, blockchainKey32, blockchainName, usedBytes,
lastBlockNumber, lastBlockHash32, lastBlockSig64, arweaveTxId,
serverAddress, addressFormatType, addressFormatVersion,
syncServers, accessServers, trustedCount, rootSig64,
}) {
const discriminator = await anchorDiscriminator('update_user_pda');
const b = new BorshBuf();
b.raw(discriminator);
b.str(login);
b.bytes32(rootKey32);
b.u64(createdAtMs);
b.u64(updatedAtMs);
b.u32(version);
b.vecU8(prevHash32);
b.u64(0n); // additional_limit
// UserMutableFields:
b.bytes32(deviceKey32);
b.bytes32(blockchainKey32);
b.str(blockchainName);
b.u64(usedBytes);
b.u32(lastBlockNumber);
b.vecU8(lastBlockHash32);
b.vecU8(lastBlockSig64);
b.str(arweaveTxId);
b.bool(true); // is_server
b.u8(addressFormatType);
b.u8(addressFormatVersion);
b.str(serverAddress);
b.vecStr(syncServers);
b.vecStr(accessServers);
b.u8(trustedCount);
b.vecU8(rootSig64);
return b.result();
}
// -------------------------------------------------------------------
// Построитель Ed25519-инструкции Solana
// -------------------------------------------------------------------
function buildEd25519IxData(sig64, pubkey32, msgHash32) {
const sigOff = 16, pkOff = 80, msgOff = 112;
const data = new Uint8Array(msgOff + 32);
const v = new DataView(data.buffer);
data[0] = 1; data[1] = 0;
v.setUint16(2, sigOff, true); v.setUint16(4, 0xFFFF, true);
v.setUint16(6, pkOff, true); v.setUint16(8, 0xFFFF, true);
v.setUint16(10, msgOff, true); v.setUint16(12, 32, true); v.setUint16(14, 0xFFFF, true);
data.set(sig64, sigOff);
data.set(pubkey32, pkOff);
data.set(msgHash32, msgOff);
return data;
}
// -------------------------------------------------------------------
// Парсер бинарных данных PDA (matches Rust deserialize_record_from_pda)
// -------------------------------------------------------------------
export function parsePdaData(raw) {
const d = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
if (d.length < 9) throw new Error('PDA слишком короткая');
if (String.fromCharCode(d[0], d[1], d[2], d[3], d[4]) !== 'SHiNE') {
throw new Error('Неверный magic в PDA');
}
const view = new DataView(d.buffer, d.byteOffset);
const recordLen = view.getUint16(7, true);
if (recordLen < 9 + 64 || recordLen > d.length) throw new Error('Неверный record_len');
// Подписанная часть = байты [0 .. recordLen-64)
const unsignedBytes = d.slice(0, recordLen - 64);
let cur = 9;
const ru8 = () => d[cur++];
const ru32 = () => { const v = view.getUint32(cur, true); cur += 4; return v; };
const ru64 = () => { const v = view.getBigUint64(cur, true); cur += 8; return v; };
const rBytes = n => { const s = d.slice(cur, cur + n); cur += n; return s; };
const rStr = () => { const len = ru8(); return new TextDecoder().decode(rBytes(len)); };
const createdAtMs = ru64();
const updatedAtMs = ru64();
const recordNumber = ru32();
const prevRecordHash = rBytes(32);
const login = rStr();
const blocksCount = ru8();
let rootKey32 = null, deviceKey32 = null, blockchainData = null;
let isServer = false, serverData = null;
let accessServers = [], trustedCount = 0;
for (let i = 0; i < blocksCount; i++) {
const blockType = ru8();
ru8(); // block_version
if (blockType === 1) {
rootKey32 = rBytes(32);
} else if (blockType === 2) {
deviceKey32 = rBytes(32);
} else if (blockType === 3) {
const count = ru8();
const blockchainType = ru8();
const blockchainName = rStr();
const blockchainPublicKey = rBytes(32);
const paidLimitBytes = ru64();
const usedBytes = ru64();
const lastBlockNumber = ru32();
const lastBlockHash = rBytes(32);
const lastBlockSignature = rBytes(64);
const arweavePresent = ru8();
const arweaveTxId = arweavePresent === 1 ? rStr() : '';
blockchainData = {
blockchainType, blockchainName, blockchainPublicKey,
paidLimitBytes, usedBytes, lastBlockNumber,
lastBlockHash, lastBlockSignature, arweaveTxId,
};
} else if (blockType === 30) {
if (ru8() === 1) {
isServer = true;
const addressFormatType = ru8();
const addressFormatVersion = ru8();
const serverAddress = rStr();
const syncCount = ru8();
const syncServers = [];
for (let j = 0; j < syncCount; j++) syncServers.push(rStr());
serverData = { addressFormatType, addressFormatVersion, serverAddress, syncServers };
}
} else if (blockType === 40) {
const cnt = ru8();
for (let j = 0; j < cnt; j++) accessServers.push(rStr());
} else if (blockType === 50) {
trustedCount = ru8();
}
}
const signature = d.slice(cur, cur + 64);
return {
recordLen, unsignedBytes,
createdAtMs, updatedAtMs, recordNumber, prevRecordHash,
login, rootKey32, deviceKey32, blockchainData,
isServer, serverData, accessServers, trustedCount, signature,
};
}
// -------------------------------------------------------------------
// Вспомогательная: читает start_bonus_limit из economy config PDA
// -------------------------------------------------------------------
function readStartBonusLimit(data) {
// Borsh: version(u8=1) + reg_fee(u64) + lamports_per_step(u64) = 17 байт до start_bonus_limit
return new DataView(data.buffer, data.byteOffset, data.byteLength).getBigUint64(17, true);
}
// -------------------------------------------------------------------
// Читает и парсит существующую PDA с блокчейна
// -------------------------------------------------------------------
export async function readServerPdaData({ login, solanaEndpoint }) {
const solana = await loadSolanaLib();
const connection = new solana.Connection(String(solanaEndpoint), 'confirmed');
const loginNorm = String(login).trim().toLowerCase();
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
const enc = new TextEncoder();
const [userPda] = solana.PublicKey.findProgramAddressSync(
[enc.encode('login='), enc.encode(loginNorm)],
usersProgram,
);
const ai = await connection.getAccountInfo(userPda, 'confirmed');
if (!ai) throw new Error(`PDA не найдена для логина «${loginNorm}»`);
const parsed = parsePdaData(ai.data);
parsed.pdaAddress = userPda.toBase58();
return parsed;
}
// -------------------------------------------------------------------
// Регистрация нового серверного аккаунта в Solana
// -------------------------------------------------------------------
export async function registerServerOnSolana({
login, keyBundle, serverAddress,
addressFormatType = 1, addressFormatVersion = 0,
syncServers = [], accessServers = [],
solanaEndpoint,
}) {
const solana = await loadSolanaLib();
const connection = new solana.Connection(String(solanaEndpoint), 'confirmed');
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
const paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID);
const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID);
const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID);
const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID);
const enc = new TextEncoder();
const loginNorm = String(login).trim().toLowerCase();
const blockchainName = `${loginNorm}-001`;
const zeroHash32 = new Uint8Array(32);
const [userPda] = solana.PublicKey.findProgramAddressSync(
[enc.encode('login='), enc.encode(loginNorm)], usersProgram);
const [economyConfigPda] = solana.PublicKey.findProgramAddressSync(
[enc.encode('shine_users_economy_config')], usersProgram);
const [inflowVault] = solana.PublicKey.findProgramAddressSync(
[enc.encode('shine_payments_inflow_vault')], paymentsProgram);
const rootKey32 = base64ToBytes(keyBundle.rootPair.publicKeyB64);
const blockchainKey32 = base64ToBytes(keyBundle.blockchainPair.publicKeyB64);
const deviceKey32 = base64ToBytes(keyBundle.devicePair.publicKeyB64);
const deviceKeypair = solana.Keypair.fromSeed(
extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64));
const ecoAccount = await connection.getAccountInfo(economyConfigPda);
if (!ecoAccount) throw new Error('Economy config не инициализирован');
const paidLimitBytes = readStartBonusLimit(ecoAccount.data); // additional_limit = 0
const createdAtMs = BigInt(Date.now());
// Подписываем LastBlockState ключом блокчейна (начальное состояние: всё нули)
const lbsBytes = buildLastBlockStateBytes(loginNorm, blockchainName, 0, zeroHash32, 0n);
const lbsHash = await sha256Bytes(lbsBytes);
const lastBlockSig64 = await signEd25519(keyBundle.blockchainPair.privatePkcs8B64, lbsHash);
// Строим и подписываем беззнаковую запись PDA корневым ключом
const unsignedRecord = buildUnsignedRecordBytesServer({
login: loginNorm, createdAtMs, updatedAtMs: createdAtMs,
recordNumber: 0, prevHash32: zeroHash32,
rootKey32, deviceKey32, blockchainKey32, blockchainName,
paidLimitBytes, usedBytes: 0n, lastBlockNumber: 0,
lastBlockHash32: zeroHash32, lastBlockSig64, arweaveTxId: '',
serverAddress, addressFormatType, addressFormatVersion,
syncServers, accessServers, trustedCount: 0,
});
const unsignedHash = await sha256Bytes(unsignedRecord);
const rootSig64 = await signEd25519(keyBundle.rootPair.privatePkcs8B64, unsignedHash);
const ixData = serializeCreateServerPdaArgs({
login: loginNorm, rootKey32, createdAtMs, deviceKey32, blockchainKey32,
blockchainName, usedBytes: 0n, lastBlockNumber: 0,
lastBlockHash32: zeroHash32, lastBlockSig64, arweaveTxId: '',
serverAddress, addressFormatType, addressFormatVersion,
syncServers, accessServers, trustedCount: 0, rootSig64,
});
const tx = new solana.Transaction().add(
new solana.TransactionInstruction({
programId: ed25519Program, keys: [],
data: buildEd25519IxData(rootSig64, rootKey32, unsignedHash),
}),
new solana.TransactionInstruction({
programId: ed25519Program, keys: [],
data: buildEd25519IxData(lastBlockSig64, blockchainKey32, lbsHash),
}),
new solana.TransactionInstruction({
programId: usersProgram,
keys: [
{ pubkey: deviceKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: userPda, isSigner: false, isWritable: true },
{ pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: inflowVault, isSigner: false, isWritable: true },
{ pubkey: sysvarInstructions, isSigner: false, isWritable: false },
{ pubkey: economyConfigPda, isSigner: false, isWritable: false },
{ pubkey: loginGuardProgram, isSigner: false, isWritable: false },
],
data: ixData,
}),
);
const signature = await solana.sendAndConfirmTransaction(
connection, tx, [deviceKeypair], { commitment: 'confirmed' });
return { signature, pdaAddress: userPda.toBase58(), blockchainName };
}
// -------------------------------------------------------------------
// Обновление серверного профиля в существующей PDA
// Для обновления нужен только root-ключ (подпись записи) + device-ключ (оплата).
// Blockchain-ключ не нужен — переиспользуем существующую подпись LastBlockState из PDA.
// -------------------------------------------------------------------
export async function updateServerOnSolana({
login, keyBundle, serverAddress,
addressFormatType, addressFormatVersion,
syncServers,
solanaEndpoint,
}) {
const solana = await loadSolanaLib();
const connection = new solana.Connection(String(solanaEndpoint), 'confirmed');
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
const paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID);
const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID);
const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID);
const enc = new TextEncoder();
const loginNorm = String(login).trim().toLowerCase();
const [userPda] = solana.PublicKey.findProgramAddressSync(
[enc.encode('login='), enc.encode(loginNorm)], usersProgram);
const [economyConfigPda] = solana.PublicKey.findProgramAddressSync(
[enc.encode('shine_users_economy_config')], usersProgram);
const [inflowVault] = solana.PublicKey.findProgramAddressSync(
[enc.encode('shine_payments_inflow_vault')], paymentsProgram);
// Читаем существующую PDA
const ai = await connection.getAccountInfo(userPda, 'confirmed');
if (!ai) throw new Error(`PDA не найдена для логина «${loginNorm}»`);
const pda = parsePdaData(ai.data);
if (!pda.isServer) throw new Error('Эта PDA не является серверной (is_server = false)');
const bch = pda.blockchainData;
const deviceKeypair = solana.Keypair.fromSeed(
extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64));
// Формат адреса: берём из аргументов или из существующей PDA
const fmtType = addressFormatType ?? pda.serverData?.addressFormatType ?? 1;
const fmtVersion = addressFormatVersion ?? pda.serverData?.addressFormatVersion ?? 0;
// prev_hash = sha256(unsigned_bytes предыдущей записи)
const prevHash32 = await sha256Bytes(pda.unsignedBytes);
const updatedAtMs = BigInt(Date.now());
const newVersion = pda.recordNumber + 1;
// Строим новую беззнаковую запись
const unsignedRecord = buildUnsignedRecordBytesServer({
login: loginNorm,
createdAtMs: pda.createdAtMs, updatedAtMs,
recordNumber: newVersion, prevHash32,
rootKey32: pda.rootKey32, deviceKey32: pda.deviceKey32,
blockchainKey32: bch.blockchainPublicKey, blockchainName: bch.blockchainName,
paidLimitBytes: bch.paidLimitBytes, usedBytes: bch.usedBytes,
lastBlockNumber: bch.lastBlockNumber,
lastBlockHash32: bch.lastBlockHash, lastBlockSig64: bch.lastBlockSignature,
arweaveTxId: bch.arweaveTxId,
serverAddress, addressFormatType: fmtType, addressFormatVersion: fmtVersion,
syncServers, accessServers: pda.accessServers, trustedCount: pda.trustedCount,
});
const unsignedHash = await sha256Bytes(unsignedRecord);
const rootSig64 = await signEd25519(keyBundle.rootPair.privatePkcs8B64, unsignedHash);
// Хэш LastBlockState из существующей PDA (те же данные — та же подпись)
const lbsBytes = buildLastBlockStateBytes(
loginNorm, bch.blockchainName,
bch.lastBlockNumber, bch.lastBlockHash, bch.usedBytes);
const lbsHash = await sha256Bytes(lbsBytes);
const ixData = await serializeUpdateServerPdaArgs({
login: loginNorm, rootKey32: pda.rootKey32,
createdAtMs: pda.createdAtMs, updatedAtMs,
version: newVersion, prevHash32,
deviceKey32: pda.deviceKey32, blockchainKey32: bch.blockchainPublicKey,
blockchainName: bch.blockchainName,
usedBytes: bch.usedBytes, lastBlockNumber: bch.lastBlockNumber,
lastBlockHash32: bch.lastBlockHash, lastBlockSig64: bch.lastBlockSignature,
arweaveTxId: bch.arweaveTxId,
serverAddress, addressFormatType: fmtType, addressFormatVersion: fmtVersion,
syncServers, accessServers: pda.accessServers, trustedCount: pda.trustedCount,
rootSig64,
});
const tx = new solana.Transaction().add(
// Ed25519: подпись новой записи корневым ключом
new solana.TransactionInstruction({
programId: ed25519Program, keys: [],
data: buildEd25519IxData(rootSig64, pda.rootKey32, unsignedHash),
}),
// Ed25519: переиспользуем существующую подпись LastBlockState из PDA
new solana.TransactionInstruction({
programId: ed25519Program, keys: [],
data: buildEd25519IxData(bch.lastBlockSignature, bch.blockchainPublicKey, lbsHash),
}),
new solana.TransactionInstruction({
programId: usersProgram,
keys: [
{ pubkey: deviceKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: userPda, isSigner: false, isWritable: true },
{ pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: inflowVault, isSigner: false, isWritable: true },
{ pubkey: sysvarInstructions, isSigner: false, isWritable: false },
{ pubkey: economyConfigPda, isSigner: false, isWritable: false },
],
data: ixData,
}),
);
const signature = await solana.sendAndConfirmTransaction(
connection, tx, [deviceKeypair], { commitment: 'confirmed' });
return { signature, pdaAddress: userPda.toBase58() };
}
// -------------------------------------------------------------------
// Деривация keyBundle из логина + пароля
// Идентична логике SHiNE-клиента (crypto-utils.js):
// masterSecret = Argon2id(login+"\n"+password, salt=sha256("shine-auth-v2|login=...|suffix=master.secret"))
// rootPair = Ed25519(sha256(base64(master) + "|root.key"))
// blockchainPair = Ed25519(sha256(base64(master) + "|bch.key"))
// devicePair = Ed25519(sha256(base64(master) + "|dev.key"))
// -------------------------------------------------------------------
function _b64urlToStd(s) {
const n = s.replace(/-/g, '+').replace(/_/g, '/');
return n + '='.repeat((4 - n.length % 4) % 4);
}
function _ed25519Pkcs8FromSeed(seed32) {
const prefix = new Uint8Array([
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
]);
const out = new Uint8Array(prefix.length + 32);
out.set(prefix); out.set(seed32, prefix.length);
return out;
}
async function _deriveEd25519PairFromMasterSecret(masterSecret32, suffix) {
const enc = new TextEncoder();
const material = `${btoa(String.fromCharCode(...masterSecret32))}|${suffix}`;
const seed = await sha256Bytes(enc.encode(material));
const pkcs8 = _ed25519Pkcs8FromSeed(seed);
const privateKey = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign']);
const jwk = await crypto.subtle.exportKey('jwk', privateKey);
if (!jwk.x) throw new Error(`Не удалось получить публичный ключ (suffix=${suffix})`);
const pubBytes = base64ToBytes(_b64urlToStd(jwk.x));
return {
publicKeyB64: btoa(String.fromCharCode(...pubBytes)),
privatePkcs8B64: btoa(String.fromCharCode(...pkcs8)),
};
}
/**
* Выводит полный keyBundle из логина и пароля.
* Та же самая логика, что используется в SHiNE-клиенте при регистрации.
*
* @param {string} login — логин сервера (нормализуется в нижний регистр)
* @param {string} password — пароль
* @param {function} [onProgress] — коллбэк(0..1) прогресса Argon2id
* @returns {{ rootPair, blockchainPair, devicePair }}
*/
export async function deriveKeyBundleFromPassword({ login, password, onProgress }) {
const { argon2idAsync } = await loadArgon2();
const enc = new TextEncoder();
const loginNorm = String(login || '').trim().toLowerCase();
const pwd = String(password ?? '');
// Salt для master secret = sha256("shine-auth-v2|login=...|suffix=master.secret")[0..16]
const saltSource = `shine-auth-v2|login=${loginNorm}|suffix=master.secret`;
const saltFull = await sha256Bytes(enc.encode(saltSource));
const salt = saltFull.slice(0, 16);
const passBytes = enc.encode(`${loginNorm}\n${pwd}`);
const masterRaw = await argon2idAsync(passBytes, salt, {
t: 2, m: 65536, p: 1, dkLen: 32,
onProgress,
});
const masterSecret32 = new Uint8Array(masterRaw);
const [rootPair, blockchainPair, devicePair] = await Promise.all([
_deriveEd25519PairFromMasterSecret(masterSecret32, 'root.key'),
_deriveEd25519PairFromMasterSecret(masterSecret32, 'bch.key'),
_deriveEd25519PairFromMasterSecret(masterSecret32, 'dev.key'),
]);
const masterSecretB64 = btoa(String.fromCharCode(...masterSecret32));
return { masterSecretB64, rootPair, blockchainPair, devicePair };
}
// -------------------------------------------------------------------
// Кодирование байт в base58 (для отображения Solana-адреса)
// -------------------------------------------------------------------
export function base58Encode(bytes) {
const ALPHA = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let num = 0n;
for (const b of bytes) num = (num << 8n) | BigInt(b);
let result = '';
while (num > 0n) {
result = ALPHA[Number(num % 58n)] + result;
num /= 58n;
}
for (const b of bytes) {
if (b !== 0) break;
result = '1' + result;
}
return result;
}