Files
salvium-rs/assembly/vm.ts
T
Matt Hess 1066ca2110 Add full wallet implementation with storage, sync, and transaction support
New Features:
  - Wallet class with multi-account and subaddress support
  - PersistentWallet with blockchain sync and storage (Memory/IndexedDB)
  - Connection manager for daemon/wallet RPC failover
  - Query system for filtering outputs and transactions
  - Offline transaction signing (cold wallet support)
  - Multisig wallet support (N-of-M threshold signing)

  Daemon RPC:
  - Salvium-specific: getSupplyInfo(), getYieldInfo()
  - Mining control: startMining(), stopMining(), miningStatus()
  - Bandwidth/peer management, admin controls

  Transaction:
  - Full transaction building with UTXO selection
  - Transaction parsing and summarization
  - Fee estimation with priority levels
  - Decoy selection with gamma distribution

  Tests:
  - Wallet class tests (accounts, subaddresses, recovery)
  - Transaction builder and parser tests
  - UTXO selection tests
2026-01-18 21:39:05 +00:00

712 lines
18 KiB
TypeScript

/**
* RandomX Virtual Machine - AssemblyScript Implementation
*
* Full mode support with pre-computed dataset lookups.
* Native u64/f64 operations for maximum performance.
*
* Reference: RandomX specification and reference implementation
*/
import { blake2b } from './blake2b';
import {
superscalar_init,
superscalarHash,
get_reg as ss_get_reg
} from './superscalar';
// ============================================================================
// Constants
// ============================================================================
// Program parameters
const RANDOMX_PROGRAM_SIZE: u32 = 256; // Instructions per program
const RANDOMX_PROGRAM_ITERATIONS: u32 = 2048; // Iterations per hash
const RANDOMX_PROGRAM_COUNT: u32 = 8; // Programs per hash
// Scratchpad sizes
const RANDOMX_SCRATCHPAD_L3: u32 = 2097152; // 2MB
const RANDOMX_SCRATCHPAD_L2: u32 = 262144; // 256KB
const RANDOMX_SCRATCHPAD_L1: u32 = 16384; // 16KB
// Scratchpad masks
const RANDOMX_SCRATCHPAD_L3_MASK: u64 = 2097152 - 64; // 2MB - 64
const RANDOMX_SCRATCHPAD_L2_MASK: u64 = 262144 - 64; // 256KB - 64
const RANDOMX_SCRATCHPAD_L1_MASK: u64 = 16384 - 64; // 16KB - 64
// Dataset parameters
const RANDOMX_DATASET_ITEM_SIZE: u32 = 64; // 64 bytes per item
const RANDOMX_DATASET_ITEM_COUNT: u64 = 34078719; // Items in full dataset
const CACHE_LINE_ALIGN_MASK: u64 = ~63;
// For testing with small dataset
let datasetItemCount: u64 = 1024; // Default to small dataset
// Register counts
const REGISTERS_COUNT: u32 = 8;
const REGISTERS_COUNT_FLT: u32 = 4;
// Instruction opcodes (simplified set)
const OP_IADD_RS: u8 = 0;
const OP_IADD_M: u8 = 1;
const OP_ISUB_R: u8 = 2;
const OP_ISUB_M: u8 = 3;
const OP_IMUL_R: u8 = 4;
const OP_IMUL_M: u8 = 5;
const OP_IMULH_R: u8 = 6;
const OP_IMULH_M: u8 = 7;
const OP_ISMULH_R: u8 = 8;
const OP_ISMULH_M: u8 = 9;
const OP_IMUL_RCP: u8 = 10;
const OP_INEG_R: u8 = 11;
const OP_IXOR_R: u8 = 12;
const OP_IXOR_M: u8 = 13;
const OP_IROR_R: u8 = 14;
const OP_IROL_R: u8 = 15;
const OP_ISWAP_R: u8 = 16;
const OP_FSWAP_R: u8 = 17;
const OP_FADD_R: u8 = 18;
const OP_FADD_M: u8 = 19;
const OP_FSUB_R: u8 = 20;
const OP_FSUB_M: u8 = 21;
const OP_FSCAL_R: u8 = 22;
const OP_FMUL_R: u8 = 23;
const OP_FDIV_M: u8 = 24;
const OP_FSQRT_R: u8 = 25;
const OP_CBRANCH: u8 = 26;
const OP_CFROUND: u8 = 27;
const OP_ISTORE: u8 = 28;
const OP_NOP: u8 = 29;
// ============================================================================
// VM State
// ============================================================================
// Integer registers r0-r7
let r0: u64 = 0;
let r1: u64 = 0;
let r2: u64 = 0;
let r3: u64 = 0;
let r4: u64 = 0;
let r5: u64 = 0;
let r6: u64 = 0;
let r7: u64 = 0;
// Floating-point registers f0-f3 (low, high pairs)
let f0_lo: f64 = 0.0;
let f0_hi: f64 = 0.0;
let f1_lo: f64 = 0.0;
let f1_hi: f64 = 0.0;
let f2_lo: f64 = 0.0;
let f2_hi: f64 = 0.0;
let f3_lo: f64 = 0.0;
let f3_hi: f64 = 0.0;
// Floating-point registers e0-e3 (low, high pairs)
let e0_lo: f64 = 0.0;
let e0_hi: f64 = 0.0;
let e1_lo: f64 = 0.0;
let e1_hi: f64 = 0.0;
let e2_lo: f64 = 0.0;
let e2_hi: f64 = 0.0;
let e3_lo: f64 = 0.0;
let e3_hi: f64 = 0.0;
// Floating-point registers a0-a3 (low, high pairs) - read-only during execution
let a0_lo: f64 = 0.0;
let a0_hi: f64 = 0.0;
let a1_lo: f64 = 0.0;
let a1_hi: f64 = 0.0;
let a2_lo: f64 = 0.0;
let a2_hi: f64 = 0.0;
let a3_lo: f64 = 0.0;
let a3_hi: f64 = 0.0;
// Memory addresses
let ma: u64 = 0;
let mx: u64 = 0;
// Address register configuration
let readReg0: u8 = 0;
let readReg1: u8 = 2;
let readReg2: u8 = 4;
let readReg3: u8 = 6;
// Dataset offset
let datasetOffset: u64 = 0;
// E-mask for float operations
let eMask0: u64 = 0;
let eMask1: u64 = 0;
// Memory pointers
let scratchpadPtr: usize = 0;
let datasetPtr: usize = 0;
let programPtr: usize = 0;
// Mode flag: 0 = light (use superscalar), 1 = full (use dataset)
let fullMode: u8 = 0;
// Light mode: cache seed pointer and length (for superscalar program generation)
let cacheSeedPtr: usize = 0;
let cacheSeedLen: i32 = 0;
let cacheItemCount: u32 = 0;
// ============================================================================
// Register Access
// ============================================================================
@inline
function getR(idx: u8): u64 {
switch (idx & 7) {
case 0: return r0;
case 1: return r1;
case 2: return r2;
case 3: return r3;
case 4: return r4;
case 5: return r5;
case 6: return r6;
case 7: return r7;
default: return 0;
}
}
@inline
function setR(idx: u8, val: u64): void {
switch (idx & 7) {
case 0: r0 = val; break;
case 1: r1 = val; break;
case 2: r2 = val; break;
case 3: r3 = val; break;
case 4: r4 = val; break;
case 5: r5 = val; break;
case 6: r6 = val; break;
case 7: r7 = val; break;
default: break;
}
}
// ============================================================================
// Memory Operations
// ============================================================================
@inline
function readU64(ptr: usize, offset: u32): u64 {
return load<u64>(ptr + offset);
}
@inline
function writeU64(ptr: usize, offset: u32, val: u64): void {
store<u64>(ptr + offset, val);
}
@inline
function readF64(ptr: usize, offset: u32): f64 {
return load<f64>(ptr + offset);
}
@inline
function writeF64(ptr: usize, offset: u32, val: f64): void {
store<f64>(ptr + offset, val);
}
// ============================================================================
// Bit Operations
// ============================================================================
@inline
function rotr64(x: u64, n: u32): u64 {
return (x >> n) | (x << (64 - n));
}
@inline
function rotl64(x: u64, n: u32): u64 {
return (x << n) | (x >> (64 - n));
}
// Unsigned 64x64 -> high 64 bits
@inline
function mulhU64(a: u64, b: u64): u64 {
const aLo: u64 = a & 0xFFFFFFFF;
const aHi: u64 = a >> 32;
const bLo: u64 = b & 0xFFFFFFFF;
const bHi: u64 = b >> 32;
const mid1: u64 = aHi * bLo;
const mid2: u64 = aLo * bHi;
const lo: u64 = aLo * bLo;
const hi: u64 = aHi * bHi;
const carry: u64 = ((lo >> 32) + (mid1 & 0xFFFFFFFF) + (mid2 & 0xFFFFFFFF)) >> 32;
return hi + (mid1 >> 32) + (mid2 >> 32) + carry;
}
// Signed 64x64 -> high 64 bits
@inline
function mulhS64(a: u64, b: u64): u64 {
const negate: bool = ((a ^ b) >> 63) != 0;
if (<i64>a < 0) a = ~a + 1;
if (<i64>b < 0) b = ~b + 1;
let result = mulhU64(a, b);
if (negate) result = ~result + (((a * b) == 0) ? 1 : 0);
return result;
}
// Reciprocal for IMUL_RCP
@inline
function reciprocal(divisor: u64): u64 {
if (divisor == 0) return 0;
const p2exp63: u64 = 1 << 63;
let quotient: u64 = p2exp63 / divisor;
let remainder: u64 = p2exp63 % divisor;
let shift: u32 = 0;
while (remainder < divisor && shift < 63) {
remainder <<= 1;
quotient <<= 1;
shift++;
if (remainder >= divisor) {
quotient++;
remainder -= divisor;
}
}
return quotient;
}
// ============================================================================
// Float Conversion
// ============================================================================
@inline
function u64ToF64(x: u64): f64 {
return reinterpret<f64>(x);
}
@inline
function f64ToU64(x: f64): u64 {
return reinterpret<u64>(x);
}
// Convert integer to small positive float (used for 'a' registers)
@inline
function getSmallPositiveFloat(x: u64): f64 {
const exponent: u64 = ((x >> 59) & 0xF) + 0x3F8; // Exponent 0x3F8-0x407
const mantissa: u64 = x & 0x7FFFFFFFFFFFF; // 51 bits of mantissa
return u64ToF64((exponent << 52) | mantissa);
}
// Mask float mantissa for E registers
@inline
function maskRegisterExponent(x: f64, mask: u64): f64 {
const bits = f64ToU64(x);
const masked = (bits & 0x807FFFFFFFFFFFFF) | mask;
return u64ToF64(masked);
}
// ============================================================================
// Scratchpad Access
// ============================================================================
@inline
function spLoad64(addr: u32): u64 {
return readU64(scratchpadPtr, addr & <u32>RANDOMX_SCRATCHPAD_L3_MASK);
}
@inline
function spStore64(addr: u32, val: u64): void {
writeU64(scratchpadPtr, addr & <u32>RANDOMX_SCRATCHPAD_L3_MASK, val);
}
@inline
function spLoadF64(addr: u32): f64 {
return readF64(scratchpadPtr, addr & <u32>RANDOMX_SCRATCHPAD_L3_MASK);
}
// ============================================================================
// Dataset Access (Full Mode)
// ============================================================================
/**
* Read dataset item (64 bytes = 8 x u64)
* In full mode, reads from pre-computed dataset
* In light mode, would compute via superscalar (not implemented here)
*/
function readDatasetItem(itemIndex: u64): void {
if (fullMode == 1) {
// Full mode: Read from pre-computed dataset
const maskedIndex: u64 = itemIndex % datasetItemCount;
const offset: u32 = <u32>(maskedIndex * 64);
r0 ^= readU64(datasetPtr, offset);
r1 ^= readU64(datasetPtr, offset + 8);
r2 ^= readU64(datasetPtr, offset + 16);
r3 ^= readU64(datasetPtr, offset + 24);
r4 ^= readU64(datasetPtr, offset + 32);
r5 ^= readU64(datasetPtr, offset + 40);
r6 ^= readU64(datasetPtr, offset + 48);
r7 ^= readU64(datasetPtr, offset + 56);
} else {
// Light mode: Compute dataset item via superscalar hash
superscalarHash(itemIndex, cacheSeedPtr, cacheSeedLen);
// XOR superscalar results into VM registers
r0 ^= ss_get_reg(0);
r1 ^= ss_get_reg(1);
r2 ^= ss_get_reg(2);
r3 ^= ss_get_reg(3);
r4 ^= ss_get_reg(4);
r5 ^= ss_get_reg(5);
r6 ^= ss_get_reg(6);
r7 ^= ss_get_reg(7);
}
}
// ============================================================================
// Instruction Execution
// ============================================================================
/**
* Execute a single instruction
* Instruction format: opcode (1), dst (1), src (1), mod (1), imm32 (4)
*/
function executeInstruction(instrPtr: usize): void {
const opcode: u8 = load<u8>(instrPtr);
const dst: u8 = load<u8>(instrPtr + 1) & 7;
const src: u8 = load<u8>(instrPtr + 2) & 7;
const mod: u8 = load<u8>(instrPtr + 3);
const imm32: u32 = load<u32>(instrPtr + 4);
const imm64: u64 = <u64><i64><i32>imm32; // Sign-extend
switch (opcode) {
case OP_IADD_RS: {
const shift: u8 = (mod >> 2) & 3;
setR(dst, getR(dst) + (getR(src) << shift) + imm64);
break;
}
case OP_IADD_M: {
const addr: u32 = <u32>(getR(src) + imm64);
setR(dst, getR(dst) + spLoad64(addr));
break;
}
case OP_ISUB_R: {
setR(dst, getR(dst) - getR(src));
break;
}
case OP_ISUB_M: {
const addr: u32 = <u32>(getR(src) + imm64);
setR(dst, getR(dst) - spLoad64(addr));
break;
}
case OP_IMUL_R: {
setR(dst, getR(dst) * getR(src));
break;
}
case OP_IMUL_M: {
const addr: u32 = <u32>(getR(src) + imm64);
setR(dst, getR(dst) * spLoad64(addr));
break;
}
case OP_IMULH_R: {
setR(dst, mulhU64(getR(dst), getR(src)));
break;
}
case OP_IMULH_M: {
const addr: u32 = <u32>(getR(src) + imm64);
setR(dst, mulhU64(getR(dst), spLoad64(addr)));
break;
}
case OP_ISMULH_R: {
setR(dst, mulhS64(getR(dst), getR(src)));
break;
}
case OP_ISMULH_M: {
const addr: u32 = <u32>(getR(src) + imm64);
setR(dst, mulhS64(getR(dst), spLoad64(addr)));
break;
}
case OP_IMUL_RCP: {
if (imm32 != 0) {
setR(dst, getR(dst) * reciprocal(<u64>imm32));
}
break;
}
case OP_INEG_R: {
setR(dst, ~getR(dst) + 1);
break;
}
case OP_IXOR_R: {
setR(dst, getR(dst) ^ getR(src));
break;
}
case OP_IXOR_M: {
const addr: u32 = <u32>(getR(src) + imm64);
setR(dst, getR(dst) ^ spLoad64(addr));
break;
}
case OP_IROR_R: {
const shift: u8 = <u8>(getR(src) & 63);
setR(dst, rotr64(getR(dst), shift));
break;
}
case OP_IROL_R: {
const shift: u8 = <u8>(getR(src) & 63);
setR(dst, rotl64(getR(dst), shift));
break;
}
case OP_ISWAP_R: {
if (dst != src) {
const tmp = getR(dst);
setR(dst, getR(src));
setR(src, tmp);
}
break;
}
case OP_ISTORE: {
const addr: u32 = <u32>(getR(dst) + imm64);
spStore64(addr, getR(src));
break;
}
case OP_NOP:
default:
break;
}
}
// ============================================================================
// Program Execution
// ============================================================================
/**
* Execute one iteration of the program
*/
function executeIteration(): void {
// Calculate scratchpad addresses
const spMix: u64 = getR(readReg0) ^ getR(readReg1);
const spAddr0: u32 = <u32>(mx ^ spMix) & <u32>RANDOMX_SCRATCHPAD_L3_MASK;
const spAddr1: u32 = <u32>(ma ^ (spMix >> 32)) & <u32>RANDOMX_SCRATCHPAD_L3_MASK;
// Read from scratchpad into integer registers
r0 ^= spLoad64(spAddr0);
r1 ^= spLoad64(spAddr0 + 8);
r2 ^= spLoad64(spAddr0 + 16);
r3 ^= spLoad64(spAddr0 + 24);
r4 ^= spLoad64(spAddr0 + 32);
r5 ^= spLoad64(spAddr0 + 40);
r6 ^= spLoad64(spAddr0 + 48);
r7 ^= spLoad64(spAddr0 + 56);
// Read into float registers (simplified)
f0_lo = spLoadF64(spAddr1);
f0_hi = spLoadF64(spAddr1 + 8);
f1_lo = spLoadF64(spAddr1 + 16);
f1_hi = spLoadF64(spAddr1 + 24);
f2_lo = spLoadF64(spAddr1 + 32);
f2_hi = spLoadF64(spAddr1 + 40);
f3_lo = spLoadF64(spAddr1 + 48);
f3_hi = spLoadF64(spAddr1 + 56);
// Execute all instructions
for (let i: u32 = 0; i < RANDOMX_PROGRAM_SIZE; i++) {
executeInstruction(programPtr + i * 8);
}
// Update memory addresses
mx ^= getR(readReg2) ^ getR(readReg3);
mx &= CACHE_LINE_ALIGN_MASK;
// Dataset read
const datasetIndex: u64 = (ma + datasetOffset) / 64;
readDatasetItem(datasetIndex);
// Swap mx and ma
const tmp: u64 = mx;
mx = ma;
ma = tmp;
// Write to scratchpad
spStore64(spAddr1, r0);
spStore64(spAddr1 + 8, r1);
spStore64(spAddr1 + 16, r2);
spStore64(spAddr1 + 24, r3);
spStore64(spAddr1 + 32, r4);
spStore64(spAddr1 + 40, r5);
spStore64(spAddr1 + 48, r6);
spStore64(spAddr1 + 56, r7);
}
// ============================================================================
// Public API
// ============================================================================
/**
* Initialize VM with memory pointers
*/
export function vm_init(
scratchpad: usize,
dataset: usize,
program: usize,
mode: u8
): void {
scratchpadPtr = scratchpad;
datasetPtr = dataset;
programPtr = program;
fullMode = mode;
}
/**
* Set dataset item count (for bounds checking)
*/
export function vm_set_dataset_size(count: u64): void {
datasetItemCount = count > 0 ? count : 1;
}
/**
* Initialize light mode with cache
* @param cachePtr - Pointer to Argon2d cache (256MB)
* @param cacheItems - Number of 64-byte cache items
* @param seedPtr - Pointer to cache seed (key)
* @param seedLen - Length of seed in bytes
*/
export function vm_init_light(
cachePtr: usize,
cacheItems: u32,
seedPtr: usize,
seedLen: i32
): void {
// Initialize superscalar with cache
superscalar_init(cachePtr, cacheItems);
// Store seed info for superscalarHash calls
cacheSeedPtr = seedPtr;
cacheSeedLen = seedLen;
cacheItemCount = cacheItems;
// Set light mode
fullMode = 0;
}
/**
* Reset VM state for new hash
*/
export function vm_reset(): void {
r0 = 0; r1 = 0; r2 = 0; r3 = 0;
r4 = 0; r5 = 0; r6 = 0; r7 = 0;
f0_lo = 0.0; f0_hi = 0.0;
f1_lo = 0.0; f1_hi = 0.0;
f2_lo = 0.0; f2_hi = 0.0;
f3_lo = 0.0; f3_hi = 0.0;
e0_lo = 0.0; e0_hi = 0.0;
e1_lo = 0.0; e1_hi = 0.0;
e2_lo = 0.0; e2_hi = 0.0;
e3_lo = 0.0; e3_hi = 0.0;
ma = 0;
mx = 0;
}
/**
* Set configuration from program entropy
*/
export function vm_set_config(
maVal: u64,
mxVal: u64,
reg0: u8, reg1: u8, reg2: u8, reg3: u8,
offset: u64,
mask0: u64, mask1: u64
): void {
ma = maVal & CACHE_LINE_ALIGN_MASK;
mx = mxVal;
readReg0 = reg0 & 7;
readReg1 = reg1 & 7;
readReg2 = reg2 & 7;
readReg3 = reg3 & 7;
datasetOffset = offset;
eMask0 = mask0;
eMask1 = mask1;
}
/**
* Set 'a' registers (read-only during execution)
*/
export function vm_set_a_registers(
a0l: f64, a0h: f64,
a1l: f64, a1h: f64,
a2l: f64, a2h: f64,
a3l: f64, a3h: f64
): void {
a0_lo = a0l; a0_hi = a0h;
a1_lo = a1l; a1_hi = a1h;
a2_lo = a2l; a2_hi = a2h;
a3_lo = a3l; a3_hi = a3h;
}
/**
* Execute the full program (all iterations)
*/
export function vm_execute(): void {
for (let i: u32 = 0; i < RANDOMX_PROGRAM_ITERATIONS; i++) {
executeIteration();
}
}
/**
* Get register file as bytes (for final hash)
* Returns pointer to 256-byte buffer with all register values
*/
export function vm_get_register_file(outputPtr: usize): void {
// Integer registers (64 bytes)
writeU64(outputPtr, 0, r0);
writeU64(outputPtr, 8, r1);
writeU64(outputPtr, 16, r2);
writeU64(outputPtr, 24, r3);
writeU64(outputPtr, 32, r4);
writeU64(outputPtr, 40, r5);
writeU64(outputPtr, 48, r6);
writeU64(outputPtr, 56, r7);
// Float registers f (64 bytes)
writeF64(outputPtr, 64, f0_lo);
writeF64(outputPtr, 72, f0_hi);
writeF64(outputPtr, 80, f1_lo);
writeF64(outputPtr, 88, f1_hi);
writeF64(outputPtr, 96, f2_lo);
writeF64(outputPtr, 104, f2_hi);
writeF64(outputPtr, 112, f3_lo);
writeF64(outputPtr, 120, f3_hi);
// Float registers e (64 bytes)
writeF64(outputPtr, 128, e0_lo);
writeF64(outputPtr, 136, e0_hi);
writeF64(outputPtr, 144, e1_lo);
writeF64(outputPtr, 152, e1_hi);
writeF64(outputPtr, 160, e2_lo);
writeF64(outputPtr, 168, e2_hi);
writeF64(outputPtr, 176, e3_lo);
writeF64(outputPtr, 184, e3_hi);
// Float registers a (64 bytes)
writeF64(outputPtr, 192, a0_lo);
writeF64(outputPtr, 200, a0_hi);
writeF64(outputPtr, 208, a1_lo);
writeF64(outputPtr, 216, a1_hi);
writeF64(outputPtr, 224, a2_lo);
writeF64(outputPtr, 232, a2_hi);
writeF64(outputPtr, 240, a3_lo);
writeF64(outputPtr, 248, a3_hi);
}
/**
* Set integer register directly (for initialization)
*/
export function vm_set_r(idx: u8, val: u64): void {
setR(idx, val);
}
/**
* Get integer register value
*/
export function vm_get_r(idx: u8): u64 {
return getR(idx);
}