Files
salvium-rs/test/mining.test.js
T
Matt Hess 714721874e Add RandomX proof-of-work implementation with WASM acceleration
- Full RandomX implementation (light mode + full mode)
   - WASM-accelerated Argon2d cache init (37x faster than pure JS)
   - WASM-accelerated SuperscalarHash with SIMD support
   - Parallel dataset generation using worker threads (8 workers)
   - Light mode: ~4s init, Full mode: ~8min init
   - Mining utilities (difficulty calculation, block construction)
   - Progress callbacks for long-running operations

   Performance targets 2023+ platforms (WASM SIMD: Chrome 91+,
   Firefox 89+, Safari 16.4+, Node 16.4+)
2026-01-15 19:19:27 +00:00

484 lines
15 KiB
JavaScript

/**
* Mining Module Tests
*/
import {
MINING_CONSTANTS,
parseBlockTemplate,
parseDifficulty,
treeHash,
setNonce,
getNonce,
checkHash,
difficultyToTarget,
hashToDifficulty,
formatDifficulty,
formatHashrate,
findNonceOffset,
calculateHashrate,
estimateBlockTime,
formatDuration,
createMiningContext
} from '../src/mining.js';
import { cnFastHash } from '../src/keccak.js';
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`${name}`);
passed++;
} catch (e) {
console.log(`${name}`);
console.log(` Error: ${e.message}`);
failed++;
}
}
function assert(condition, message) {
if (!condition) throw new Error(message || 'Assertion failed');
}
function assertEqual(a, b, message) {
if (a !== b) {
throw new Error(message || `Expected ${a} to equal ${b}`);
}
}
function bytesEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
// Helper to create test hashes
function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
function bytesToHex(bytes) {
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
console.log('\n--- Mining Constants Tests ---');
test('MINING_CONSTANTS has expected values', () => {
assertEqual(MINING_CONSTANTS.RX_BLOCK_VERSION, 12, 'RandomX version should be 12');
assertEqual(MINING_CONSTANTS.NONCE_SIZE, 4, 'Nonce size should be 4');
assertEqual(MINING_CONSTANTS.DIFFICULTY_TARGET, 120, 'Block time should be 120s');
assertEqual(MINING_CONSTANTS.DIFFICULTY_WINDOW, 720, 'Difficulty window should be 720');
assertEqual(MINING_CONSTANTS.MAX_EXTRA_NONCE_SIZE, 255, 'Max extra nonce should be 255');
});
console.log('\n--- Difficulty Parsing Tests ---');
test('parseDifficulty handles 64-bit difficulty', () => {
const result = parseDifficulty(1000000, null, null);
assertEqual(result, 1000000n, 'Should parse 64-bit difficulty');
});
test('parseDifficulty handles wide_difficulty hex string', () => {
const result = parseDifficulty(0, '0x100000000', 0);
assertEqual(result, 0x100000000n, 'Should parse wide difficulty');
});
test('parseDifficulty handles difficulty_top64', () => {
const result = parseDifficulty(0xFFFFFFFFn, null, 1);
assertEqual(result, (1n << 64n) | 0xFFFFFFFFn, 'Should combine top64 and low64');
});
console.log('\n--- Tree Hash Tests ---');
test('treeHash with 1 hash returns that hash', () => {
const hash = cnFastHash(new TextEncoder().encode('test'));
const result = treeHash([hash]);
assert(bytesEqual(result, hash), 'Single hash should return itself');
});
test('treeHash with 2 hashes returns hash of concatenation', () => {
const hash1 = cnFastHash(new TextEncoder().encode('test1'));
const hash2 = cnFastHash(new TextEncoder().encode('test2'));
const result = treeHash([hash1, hash2]);
// Manual calculation
const combined = new Uint8Array(64);
combined.set(hash1, 0);
combined.set(hash2, 32);
const expected = cnFastHash(combined);
assert(bytesEqual(result, expected), 'Two hashes should be hashed together');
});
test('treeHash with 3 hashes uses CN tree algorithm', () => {
const hashes = [
cnFastHash(new TextEncoder().encode('tx1')),
cnFastHash(new TextEncoder().encode('tx2')),
cnFastHash(new TextEncoder().encode('tx3'))
];
const result = treeHash(hashes);
assert(result.length === 32, 'Should return 32-byte hash');
});
test('treeHash with 4 hashes builds proper tree', () => {
const hashes = [
cnFastHash(new TextEncoder().encode('tx1')),
cnFastHash(new TextEncoder().encode('tx2')),
cnFastHash(new TextEncoder().encode('tx3')),
cnFastHash(new TextEncoder().encode('tx4'))
];
const result = treeHash(hashes);
assert(result.length === 32, 'Should return 32-byte hash');
});
test('treeHash is deterministic', () => {
const hashes = [
cnFastHash(new TextEncoder().encode('tx1')),
cnFastHash(new TextEncoder().encode('tx2')),
cnFastHash(new TextEncoder().encode('tx3'))
];
const result1 = treeHash(hashes);
const result2 = treeHash(hashes);
assert(bytesEqual(result1, result2), 'Should be deterministic');
});
console.log('\n--- Nonce Manipulation Tests ---');
test('setNonce writes nonce correctly', () => {
const blob = new Uint8Array(100);
const result = setNonce(blob, 0x12345678, 39);
assertEqual(result[39], 0x78, 'Byte 0 should be LSB');
assertEqual(result[40], 0x56, 'Byte 1');
assertEqual(result[41], 0x34, 'Byte 2');
assertEqual(result[42], 0x12, 'Byte 3 should be MSB');
});
test('getNonce reads nonce correctly', () => {
const blob = new Uint8Array(100);
blob[39] = 0x78;
blob[40] = 0x56;
blob[41] = 0x34;
blob[42] = 0x12;
const result = getNonce(blob, 39);
assertEqual(result, 0x12345678, 'Should read nonce correctly');
});
test('setNonce/getNonce roundtrip', () => {
const blob = new Uint8Array(100);
const nonce = 0xDEADBEEF;
const modified = setNonce(blob, nonce, 50);
const recovered = getNonce(modified, 50);
assertEqual(recovered, nonce, 'Should roundtrip correctly');
});
test('setNonce handles zero', () => {
const blob = new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
const result = setNonce(blob, 0, 0);
assertEqual(result[0], 0, 'Should set to zero');
assertEqual(result[1], 0, 'Should set to zero');
assertEqual(result[2], 0, 'Should set to zero');
assertEqual(result[3], 0, 'Should set to zero');
});
test('setNonce handles max uint32', () => {
const blob = new Uint8Array(10);
const result = setNonce(blob, 0xFFFFFFFF, 0);
assertEqual(result[0], 0xFF, 'All bytes should be 0xFF');
assertEqual(result[1], 0xFF, 'All bytes should be 0xFF');
assertEqual(result[2], 0xFF, 'All bytes should be 0xFF');
assertEqual(result[3], 0xFF, 'All bytes should be 0xFF');
});
console.log('\n--- Difficulty Check Tests ---');
test('checkHash returns true for easy difficulty', () => {
// Any hash should pass difficulty 1
const hash = cnFastHash(new TextEncoder().encode('test'));
assert(checkHash(hash, 1n), 'Difficulty 1 should always pass');
});
test('checkHash returns false for impossible difficulty', () => {
// Max difficulty (2^256) should never pass
const hash = cnFastHash(new TextEncoder().encode('test'));
const maxDifficulty = 1n << 256n;
assert(!checkHash(hash, maxDifficulty), 'Max difficulty should never pass');
});
test('checkHash handles zero difficulty', () => {
const hash = cnFastHash(new TextEncoder().encode('test'));
assert(!checkHash(hash, 0n), 'Zero difficulty should return false');
});
test('checkHash with all-zeros hash passes any difficulty', () => {
const hash = new Uint8Array(32); // All zeros
assert(checkHash(hash, 1n), 'Zero hash should pass difficulty 1');
assert(checkHash(hash, 1000000n), 'Zero hash should pass any difficulty');
});
test('checkHash with high hash fails high difficulty', () => {
// Create a hash with high value (mostly 0xFF)
const hash = new Uint8Array(32).fill(0xFF);
assert(!checkHash(hash, 1000000000000n), 'High hash should fail high difficulty');
});
console.log('\n--- Difficulty Conversion Tests ---');
test('difficultyToTarget computes correct target', () => {
const target = difficultyToTarget(1000000n);
// Target = 2^256 / 1000000
const expected = (1n << 256n) / 1000000n;
assertEqual(target, expected, 'Should compute correct target');
});
test('difficultyToTarget handles difficulty 1', () => {
const target = difficultyToTarget(1n);
const expected = (1n << 256n);
assertEqual(target, expected, 'Difficulty 1 should give max target');
});
test('hashToDifficulty computes equivalent difficulty', () => {
const hash = new Uint8Array(32);
hash[31] = 0x01; // Small hash value
const diff = hashToDifficulty(hash);
assert(diff > 0n, 'Should compute positive difficulty');
});
test('hashToDifficulty returns 0 for zero hash', () => {
const hash = new Uint8Array(32);
const diff = hashToDifficulty(hash);
assertEqual(diff, 0n, 'Zero hash should return zero difficulty');
});
console.log('\n--- Format Functions Tests ---');
test('formatDifficulty formats small values', () => {
assertEqual(formatDifficulty(500n), '500', 'Should format small values');
});
test('formatDifficulty formats K values', () => {
const result = formatDifficulty(5000n);
assert(result.includes('K'), 'Should use K suffix');
});
test('formatDifficulty formats M values', () => {
const result = formatDifficulty(5000000n);
assert(result.includes('M'), 'Should use M suffix');
});
test('formatDifficulty formats G values', () => {
const result = formatDifficulty(5000000000n);
assert(result.includes('G'), 'Should use G suffix');
});
test('formatDifficulty formats T values', () => {
const result = formatDifficulty(5000000000000n);
assert(result.includes('T'), 'Should use T suffix');
});
test('formatHashrate formats H/s', () => {
const result = formatHashrate(500);
assert(result.includes('H/s'), 'Should include H/s');
});
test('formatHashrate formats KH/s', () => {
const result = formatHashrate(5000);
assert(result.includes('KH/s'), 'Should include KH/s');
});
test('formatHashrate formats MH/s', () => {
const result = formatHashrate(5000000);
assert(result.includes('MH/s'), 'Should include MH/s');
});
test('formatDuration formats seconds', () => {
assertEqual(formatDuration(45), '45s', 'Should format seconds');
});
test('formatDuration formats minutes and seconds', () => {
const result = formatDuration(125);
assert(result.includes('m') && result.includes('s'), 'Should include m and s');
});
test('formatDuration formats hours', () => {
const result = formatDuration(3700);
assert(result.includes('h'), 'Should include h');
});
test('formatDuration formats days', () => {
const result = formatDuration(100000);
assert(result.includes('d'), 'Should include d');
});
test('formatDuration handles infinity', () => {
assertEqual(formatDuration(Infinity), '∞', 'Should handle infinity');
});
console.log('\n--- Mining Statistics Tests ---');
test('calculateHashrate computes correctly', () => {
const rate = calculateHashrate(1000, 10);
assertEqual(rate, 100, 'Should compute 100 H/s');
});
test('calculateHashrate handles zero time', () => {
const rate = calculateHashrate(1000, 0);
assertEqual(rate, 0, 'Zero time should return 0');
});
test('estimateBlockTime computes correctly', () => {
const time = estimateBlockTime(100, 1000n);
assertEqual(time, 10, '1000/100 = 10 seconds');
});
test('estimateBlockTime handles zero hashrate', () => {
const time = estimateBlockTime(0, 1000n);
assertEqual(time, Infinity, 'Zero hashrate should return Infinity');
});
console.log('\n--- Block Template Parsing Tests ---');
test('parseBlockTemplate parses basic fields', () => {
const template = parseBlockTemplate({
difficulty: 1000000,
height: 12345,
prev_hash: 'abc123',
expected_reward: 1000000000000,
reserved_offset: 130,
blocktemplate_blob: 'deadbeef',
blockhashing_blob: 'cafebabe'
});
assertEqual(template.difficulty, 1000000n, 'Should parse difficulty');
assertEqual(template.height, 12345n, 'Should parse height');
assertEqual(template.prevHash, 'abc123', 'Should parse prev_hash');
assertEqual(template.expectedReward, 1000000000000n, 'Should parse reward');
assertEqual(template.reservedOffset, 130, 'Should parse reserved_offset');
});
test('parseBlockTemplate handles seed hash', () => {
const template = parseBlockTemplate({
difficulty: 1000,
height: 100,
prev_hash: '',
expected_reward: 0,
reserved_offset: 0,
blocktemplate_blob: '',
blockhashing_blob: '',
seed_height: 64,
seed_hash: 'abcd1234',
next_seed_hash: 'efgh5678'
});
assertEqual(template.seedHeight, 64n, 'Should parse seed_height');
assertEqual(template.seedHash, 'abcd1234', 'Should parse seed_hash');
assertEqual(template.nextSeedHash, 'efgh5678', 'Should parse next_seed_hash');
});
console.log('\n--- Mining Context Tests ---');
test('createMiningContext creates functional context', () => {
// Mock RandomX function that returns predictable hash
const mockRandomX = (blob, seed) => {
return cnFastHash(blob);
};
const ctx = createMiningContext(mockRandomX);
assert(typeof ctx.tryNonce === 'function', 'Should have tryNonce');
assert(typeof ctx.mineRange === 'function', 'Should have mineRange');
});
test('createMiningContext tryNonce returns correct structure', () => {
const mockRandomX = (blob, seed) => cnFastHash(blob);
const ctx = createMiningContext(mockRandomX);
const blob = new Uint8Array(100);
const seed = new Uint8Array(32);
const result = ctx.tryNonce(blob, seed, 0, 39, 1n);
assert('found' in result, 'Should have found property');
assert('hash' in result, 'Should have hash property');
assert('nonce' in result, 'Should have nonce property');
assert(result.hash.length === 32, 'Hash should be 32 bytes');
});
test('createMiningContext mineRange finds solution with low difficulty', () => {
const mockRandomX = (blob, seed) => cnFastHash(blob);
const ctx = createMiningContext(mockRandomX);
const blob = new Uint8Array(100);
const seed = new Uint8Array(32);
// Difficulty 1 should pass immediately
const result = ctx.mineRange(blob, seed, 0, 10, 39, 1n);
assert(result !== null, 'Should find solution with difficulty 1');
assert(result.found, 'Should mark as found');
});
test('createMiningContext mineRange returns null when not found', () => {
const mockRandomX = (blob, seed) => {
// Return high hash that won't pass
return new Uint8Array(32).fill(0xFF);
};
const ctx = createMiningContext(mockRandomX);
const blob = new Uint8Array(100);
const seed = new Uint8Array(32);
// Very high difficulty
const result = ctx.mineRange(blob, seed, 0, 10, 39, 1n << 250n);
assert(result === null, 'Should return null when not found');
});
console.log('\n--- Find Nonce Offset Tests ---');
test('findNonceOffset finds offset after header fields', () => {
// Create a simple block header:
// major_version: 1 byte (value < 128, so single byte varint)
// minor_version: 1 byte
// timestamp: 1 byte (small value)
// prev_id: 32 bytes
// Then nonce at offset 35
const blob = new Uint8Array(100);
blob[0] = 14; // major_version
blob[1] = 0; // minor_version
blob[2] = 100; // timestamp (small)
// prev_id takes bytes 3-34
// nonce should be at 35
const offset = findNonceOffset(blob);
assertEqual(offset, 35, 'Should find nonce at offset 35 for simple header');
});
test('findNonceOffset handles multi-byte varints', () => {
const blob = new Uint8Array(100);
blob[0] = 0x80 | 14; // major_version with continuation
blob[1] = 0x01; // continuation byte
blob[2] = 0; // minor_version
blob[3] = 100; // timestamp
// prev_id takes bytes 4-35
// nonce should be at 36
const offset = findNonceOffset(blob);
assertEqual(offset, 36, 'Should handle multi-byte varints');
});
// Summary
console.log('\n--- Mining Test Summary ---');
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`Total: ${passed + failed}`);
if (failed > 0) {
console.log('\n✗ Some tests failed!');
process.exit(1);
} else {
console.log('\n✓ All mining tests passed!');
}