Files
SHiNE-server/shine-server-UI-obsolete/js/server-pda-core.js
T

711 lines
29 KiB
JavaScript

// Логика управления серверной 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;
}