SHA256
UI: обновлена шапка каналов, закрыты pending-задачи и обновлены версии
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
import { sha256Bytes, signBytes, importPkcs8Ed25519, base64ToBytes } from './crypto-utils.js';
|
||||
import { extractSeed32FromPkcs8B64 } from './device-key-utils.js';
|
||||
import {
|
||||
SHINE_USERS_PROGRAM_ID,
|
||||
SHINE_PAYMENTS_PROGRAM_ID,
|
||||
SHINE_LOGIN_GUARD_PROGRAM_ID,
|
||||
} from '../solana-programs.js';
|
||||
|
||||
const CREATE_USER_PDA_DISCRIMINATOR = new Uint8Array([139, 157, 13, 41, 142, 174, 226, 214]);
|
||||
const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111';
|
||||
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';
|
||||
|
||||
let solanaLibPromise = null;
|
||||
function loadSolanaLib() {
|
||||
if (!solanaLibPromise) solanaLibPromise = import('https://esm.sh/@solana/web3.js@1.98.4?bundle');
|
||||
return solanaLibPromise;
|
||||
}
|
||||
|
||||
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); }
|
||||
}
|
||||
|
||||
// Matches Rust serialize_last_block_state (initial zero state)
|
||||
function buildLastBlockStateBytes(login, blockchainName) {
|
||||
const enc = new TextEncoder();
|
||||
const prefix = enc.encode('SHiNE_LAST_BLOCK');
|
||||
const loginB = enc.encode(login);
|
||||
const bchB = enc.encode(blockchainName);
|
||||
const buf = [];
|
||||
for (const x of prefix) buf.push(x);
|
||||
buf.push(loginB.length);
|
||||
for (const x of loginB) buf.push(x);
|
||||
buf.push(bchB.length);
|
||||
for (const x of bchB) buf.push(x);
|
||||
pushU32LE(buf, 0); // last_block_number = 0
|
||||
for (let i = 0; i < 32; i++) buf.push(0); // last_block_hash = [0;32]
|
||||
pushU64LE(buf, 0n); // used_bytes = 0
|
||||
return new Uint8Array(buf);
|
||||
}
|
||||
|
||||
// Matches Rust serialize_unsigned_record for initial registration
|
||||
function buildUnsignedRecordBytes(
|
||||
login, createdAtMs, rootKey32, deviceKey32, blockchainKey32,
|
||||
blockchainName, paidLimitBytes, lastBlockSig64,
|
||||
) {
|
||||
const enc = new TextEncoder();
|
||||
const loginB = enc.encode(login);
|
||||
const bchB = enc.encode(blockchainName);
|
||||
const accessB = enc.encode('shineup.me');
|
||||
const buf = [];
|
||||
|
||||
// Fixed header: MAGIC(5) + FORMAT_MAJOR(1) + FORMAT_MINOR(1) + record_len_placeholder(2)
|
||||
buf.push(0x53, 0x48, 0x69, 0x4E, 0x45, 1, 0, 0, 0); // indices 0..8
|
||||
|
||||
pushU64LE(buf, createdAtMs); // created_at_ms
|
||||
pushU64LE(buf, createdAtMs); // updated_at_ms = same
|
||||
pushU32LE(buf, 0); // record_number = 0
|
||||
for (let i = 0; i < 32; i++) buf.push(0); // prev_record_hash = [0;32]
|
||||
|
||||
buf.push(loginB.length);
|
||||
for (const x of loginB) buf.push(x);
|
||||
|
||||
buf.push(5); // blocks_count (non-server)
|
||||
|
||||
// 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)
|
||||
buf.push(3, 0, 1, 1); // type, ver, count=1, blockchain_type=1(MAIN_USER)
|
||||
buf.push(bchB.length);
|
||||
for (const x of bchB) buf.push(x);
|
||||
for (const x of blockchainKey32) buf.push(x);
|
||||
pushU64LE(buf, paidLimitBytes); // paid_limit_bytes
|
||||
pushU64LE(buf, 0n); // used_bytes = 0
|
||||
pushU32LE(buf, 0); // last_block_number = 0
|
||||
for (let i = 0; i < 32; i++) buf.push(0); // last_block_hash = [0;32]
|
||||
for (const x of lastBlockSig64) buf.push(x); // last_block_signature
|
||||
buf.push(0); // arweave_present = 0
|
||||
|
||||
// AccessServersBlock (type=40, ver=0)
|
||||
buf.push(40, 0, 1, accessB.length);
|
||||
for (const x of accessB) buf.push(x);
|
||||
|
||||
// TrustedStateBlock (type=50, ver=0, trusted_count=0)
|
||||
buf.push(50, 0, 0);
|
||||
|
||||
// Patch record_len at indices 7-8: total = buf.length + 64 (signature)
|
||||
const recLen = buf.length + 64;
|
||||
buf[7] = recLen & 0xFF;
|
||||
buf[8] = (recLen >> 8) & 0xFF;
|
||||
|
||||
return new Uint8Array(buf);
|
||||
}
|
||||
|
||||
// Builds Ed25519 program instruction data for one signature
|
||||
function buildEd25519IxData(sig64, pubkey32, msgHash32) {
|
||||
const sigOff = 16;
|
||||
const pkOff = sigOff + 64; // 80
|
||||
const msgOff = pkOff + 32; // 112
|
||||
const data = new Uint8Array(msgOff + 32); // 144 bytes total
|
||||
const v = new DataView(data.buffer);
|
||||
data[0] = 1; data[1] = 0; // num_signatures=1, padding
|
||||
v.setUint16(2, sigOff, true);
|
||||
v.setUint16(4, 0xFFFF, true); // same instruction
|
||||
v.setUint16(6, pkOff, true);
|
||||
v.setUint16(8, 0xFFFF, true);
|
||||
v.setUint16(10, msgOff, true);
|
||||
v.setUint16(12, 32, true); // message_data_size = 32
|
||||
v.setUint16(14, 0xFFFF, true);
|
||||
data.set(sig64, sigOff);
|
||||
data.set(pubkey32, pkOff);
|
||||
data.set(msgHash32, msgOff);
|
||||
return data;
|
||||
}
|
||||
|
||||
function readStartBonusLimit(data) {
|
||||
// Borsh: version(u8=1) + reg_fee(u64=8) + lamports_per_step(u64=8) + start_bonus_limit(u64=8)
|
||||
const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
||||
return v.getBigUint64(17, true);
|
||||
}
|
||||
|
||||
function serializeCreateUserPdaArgs(
|
||||
login, rootKey32, createdAtMs, deviceKey32, blockchainKey32,
|
||||
blockchainName, lastBlockSig64, 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(0n); // used_bytes
|
||||
b.u32(0); // last_block_number
|
||||
b.vecU8(new Uint8Array(32)); // last_block_hash
|
||||
b.vecU8(lastBlockSig64); // last_block_signature
|
||||
b.str(''); // arweave_tx_id
|
||||
b.bool(false); // is_server
|
||||
b.bytes32(new Uint8Array(32)); // server_key (default)
|
||||
b.str(''); // server_address
|
||||
b.vecStr([]); // sync_servers
|
||||
b.vecStr(['shineup.me']); // access_servers
|
||||
b.u8(0); // trusted_count
|
||||
b.vecU8(rootSig64); // signature
|
||||
return b.result();
|
||||
}
|
||||
|
||||
export async function registerUserOnSolana({ login, keyBundle, 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 = login.toLowerCase();
|
||||
const blockchainName = `${loginNorm}-001`;
|
||||
|
||||
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 rootPrivKey = await importPkcs8Ed25519(keyBundle.rootPair.privatePkcs8B64);
|
||||
const bchPrivKey = await importPkcs8Ed25519(keyBundle.blockchainPair.privatePkcs8B64);
|
||||
|
||||
const deviceSeed32 = extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64);
|
||||
const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);
|
||||
|
||||
const ecoAccount = await connection.getAccountInfo(economyConfigPda);
|
||||
if (!ecoAccount) {
|
||||
throw new Error('Economy config не инициализирован. Запустите init_users_economy_config.');
|
||||
}
|
||||
const startBonusLimit = readStartBonusLimit(ecoAccount.data);
|
||||
const createdAtMs = BigInt(Date.now());
|
||||
|
||||
// Sign LastBlockState with blockchain key
|
||||
const lbsBytes = buildLastBlockStateBytes(loginNorm, blockchainName);
|
||||
const lbsHash = await sha256Bytes(lbsBytes);
|
||||
const lastBlockSig64 = await signBytes(bchPrivKey, lbsHash);
|
||||
|
||||
// Build and sign unsigned PDA record with root key
|
||||
const unsignedRecord = buildUnsignedRecordBytes(
|
||||
loginNorm, createdAtMs, rootKey32, deviceKey32, blockchainKey32,
|
||||
blockchainName, startBonusLimit, lastBlockSig64,
|
||||
);
|
||||
const unsignedHash = await sha256Bytes(unsignedRecord);
|
||||
const rootSig64 = await signBytes(rootPrivKey, unsignedHash);
|
||||
|
||||
const ixData = serializeCreateUserPdaArgs(
|
||||
loginNorm, rootKey32, createdAtMs, deviceKey32, blockchainKey32,
|
||||
blockchainName, lastBlockSig64, rootSig64,
|
||||
);
|
||||
|
||||
// Ed25519 instructions must precede create_user_pda
|
||||
const ed25519RootIx = new solana.TransactionInstruction({
|
||||
programId: ed25519Program,
|
||||
keys: [],
|
||||
data: Buffer.from(buildEd25519IxData(rootSig64, rootKey32, unsignedHash)),
|
||||
});
|
||||
const ed25519BchIx = new solana.TransactionInstruction({
|
||||
programId: ed25519Program,
|
||||
keys: [],
|
||||
data: Buffer.from(buildEd25519IxData(lastBlockSig64, blockchainKey32, lbsHash)),
|
||||
});
|
||||
const createUserIx = 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: Buffer.from(ixData),
|
||||
});
|
||||
|
||||
const sig = await solana.sendAndConfirmTransaction(
|
||||
connection,
|
||||
new solana.Transaction().add(ed25519RootIx, ed25519BchIx, createUserIx),
|
||||
[deviceKeypair],
|
||||
{ commitment: 'confirmed' },
|
||||
);
|
||||
|
||||
return { signature: sig, blockchainName };
|
||||
}
|
||||
Reference in New Issue
Block a user