Files
salvium-rs/test/bulletproofs_plus.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

548 lines
16 KiB
JavaScript

/**
* Bulletproofs+ Tests
*
* Tests for the pure JavaScript Bulletproofs+ implementation.
*/
import {
bytesToScalar,
scalarToBytes,
bytesToPoint,
hashToScalar,
hashToPoint,
initGenerators,
initTranscript,
parseProof,
multiScalarMul,
verifyBulletproofPlus,
verifyBulletproofPlusBatch,
Point,
// Proving functions
randomScalar,
bulletproofPlusProve,
proveRange,
proveRangeMultiple,
serializeProof,
L,
INV_EIGHT
} from '../src/bulletproofs_plus.js';
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`${name}`);
passed++;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
failed++;
}
}
async function testAsync(name, fn) {
try {
await fn();
console.log(`${name}`);
passed++;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
failed++;
}
}
function assertEqual(actual, expected, message = '') {
if (actual !== expected) {
throw new Error(`${message} Expected ${expected}, got ${actual}`);
}
}
function assertTrue(condition, message = '') {
if (!condition) {
throw new Error(message || 'Expected true');
}
}
function assertExists(value, message = '') {
if (value === undefined || value === null) {
throw new Error(`${message} Value is ${value}`);
}
}
// ============================================================
// Scalar Conversion Tests
// ============================================================
console.log('\n--- Scalar Conversion Tests ---');
test('bytesToScalar converts little-endian bytes', () => {
const bytes = new Uint8Array(32);
bytes[0] = 1;
const scalar = bytesToScalar(bytes);
assertEqual(scalar, 1n);
});
test('bytesToScalar handles larger values', () => {
const bytes = new Uint8Array(32);
bytes[0] = 0xff;
bytes[1] = 0xff;
const scalar = bytesToScalar(bytes);
assertEqual(scalar, 65535n);
});
test('scalarToBytes converts to little-endian', () => {
const scalar = 256n;
const bytes = scalarToBytes(scalar);
assertEqual(bytes[0], 0);
assertEqual(bytes[1], 1);
});
test('bytesToScalar and scalarToBytes are inverses', () => {
const original = 12345678901234567890n;
const bytes = scalarToBytes(original);
const recovered = bytesToScalar(bytes);
assertEqual(recovered, original);
});
// ============================================================
// Point Conversion Tests
// ============================================================
console.log('\n--- Point Conversion Tests ---');
test('bytesToPoint decodes base point', () => {
const baseBytes = Point.BASE.toBytes();
const point = bytesToPoint(baseBytes);
assertTrue(point.equals(Point.BASE));
});
test('bytesToPoint throws on invalid encoding', () => {
const invalidBytes = new Uint8Array(32);
invalidBytes.fill(0xff);
let threw = false;
try {
bytesToPoint(invalidBytes);
} catch (e) {
threw = true;
}
assertTrue(threw, 'Should throw on invalid point');
});
// ============================================================
// Hash Functions Tests
// ============================================================
console.log('\n--- Hash Functions Tests ---');
test('hashToScalar produces deterministic output', () => {
const data = new Uint8Array([1, 2, 3, 4]);
const scalar1 = hashToScalar(data);
const scalar2 = hashToScalar(data);
assertEqual(scalar1, scalar2);
});
test('hashToScalar produces different output for different input', () => {
const data1 = new Uint8Array([1, 2, 3, 4]);
const data2 = new Uint8Array([5, 6, 7, 8]);
const scalar1 = hashToScalar(data1);
const scalar2 = hashToScalar(data2);
assertTrue(scalar1 !== scalar2, 'Different inputs should produce different outputs');
});
test('hashToPoint produces valid point', () => {
const data = new TextEncoder().encode('test data');
const point = hashToPoint(data);
assertExists(point);
// Point should not be identity
assertTrue(!point.equals(Point.ZERO), 'Should not be identity point');
});
// ============================================================
// Generator Tests
// ============================================================
console.log('\n--- Generator Tests ---');
test('initGenerators creates G and H', () => {
const gens = initGenerators(64);
assertExists(gens.G);
assertExists(gens.H);
assertTrue(gens.G.equals(Point.BASE));
});
test('initGenerators creates Gi and Hi arrays', () => {
const gens = initGenerators(64);
assertEqual(gens.Gi.length, 64);
assertEqual(gens.Hi.length, 64);
});
test('Gi and Hi are distinct points', () => {
const gens = initGenerators(64);
assertTrue(!gens.Gi[0].equals(gens.Hi[0]), 'Gi[0] should differ from Hi[0]');
assertTrue(!gens.Gi[0].equals(gens.G), 'Gi[0] should differ from G');
});
test('initGenerators is cached', () => {
const gens1 = initGenerators(64);
const gens2 = initGenerators(64);
assertTrue(gens1 === gens2, 'Should return cached generators');
});
// ============================================================
// Transcript Tests
// ============================================================
console.log('\n--- Transcript Tests ---');
test('initTranscript produces deterministic output', () => {
const t1 = initTranscript();
const t2 = initTranscript();
for (let i = 0; i < 32; i++) {
assertEqual(t1[i], t2[i]);
}
});
// ============================================================
// Multiscalar Multiplication Tests
// ============================================================
console.log('\n--- Multiscalar Multiplication Tests ---');
test('multiScalarMul with single point', () => {
const G = Point.BASE;
const result = multiScalarMul([5n], [G]);
const expected = G.multiply(5n);
assertTrue(result.equals(expected));
});
test('multiScalarMul with multiple points', () => {
const G = Point.BASE;
const H = G.multiply(7n);
const result = multiScalarMul([3n, 5n], [G, H]);
const expected = G.multiply(3n).add(H.multiply(5n));
assertTrue(result.equals(expected));
});
test('multiScalarMul with zero scalar', () => {
const G = Point.BASE;
const result = multiScalarMul([0n, 5n], [G, G]);
const expected = G.multiply(5n);
assertTrue(result.equals(expected));
});
test('multiScalarMul with empty arrays', () => {
const result = multiScalarMul([], []);
assertTrue(result.equals(Point.ZERO));
});
// ============================================================
// Proof Parsing Tests
// ============================================================
console.log('\n--- Proof Parsing Tests ---');
test('parseProof extracts correct structure', () => {
// Create a minimal mock proof (6 rounds = 384 bytes for L/R + 192 bytes header)
const proofBytes = new Uint8Array(32 * 6 + 64 * 6);
// Fill with valid point encodings (use base point bytes)
const baseBytes = Point.BASE.toBytes();
// A, A1, B
for (let i = 0; i < 3; i++) {
proofBytes.set(baseBytes, i * 32);
}
// r1, s1, d1 (scalars - just use small values)
proofBytes[96] = 1; // r1
proofBytes[128] = 2; // s1
proofBytes[160] = 3; // d1
// L and R pairs (6 rounds)
for (let i = 0; i < 12; i++) {
proofBytes.set(baseBytes, 192 + i * 32);
}
const proof = parseProof(proofBytes);
assertExists(proof.A);
assertExists(proof.A1);
assertExists(proof.B);
assertEqual(proof.r1, 1n);
assertEqual(proof.s1, 2n);
assertEqual(proof.d1, 3n);
assertEqual(proof.L.length, 6);
assertEqual(proof.R.length, 6);
});
test('parseProof throws on too-short proof', () => {
const shortProof = new Uint8Array(100);
let threw = false;
try {
parseProof(shortProof);
} catch (e) {
threw = true;
}
assertTrue(threw, 'Should throw on short proof');
});
// ============================================================
// Performance Benchmark
// ============================================================
console.log('\n--- Performance Benchmark ---');
testAsync('Benchmark: Generator initialization', async () => {
// Clear cache for benchmark
const start = performance.now();
const gens = initGenerators(1024);
const elapsed = performance.now() - start;
console.log(` Generator init (1024 points): ${elapsed.toFixed(2)}ms`);
assertTrue(elapsed < 30000, 'Should complete in reasonable time');
});
testAsync('Benchmark: 100 scalar multiplications', async () => {
const G = Point.BASE;
const start = performance.now();
for (let i = 0; i < 100; i++) {
G.multiply(BigInt(i + 1));
}
const elapsed = performance.now() - start;
console.log(` 100 scalar mults: ${elapsed.toFixed(2)}ms (${(elapsed/100).toFixed(2)}ms each)`);
assertTrue(elapsed < 10000, 'Should complete in reasonable time');
});
testAsync('Benchmark: MSM with 64 points', async () => {
const gens = initGenerators(64);
const scalars = [];
for (let i = 0; i < 64; i++) {
scalars.push(BigInt(i + 1));
}
const start = performance.now();
const result = multiScalarMul(scalars, gens.Gi.slice(0, 64));
const elapsed = performance.now() - start;
console.log(` MSM (64 points): ${elapsed.toFixed(2)}ms`);
assertTrue(elapsed < 10000, 'Should complete in reasonable time');
});
testAsync('Benchmark: MSM with 128 points (single output proof size)', async () => {
const gens = initGenerators(128);
const scalars = [];
for (let i = 0; i < 128; i++) {
scalars.push(BigInt(i + 1));
}
const start = performance.now();
const result = multiScalarMul(scalars, gens.Gi.slice(0, 128));
const elapsed = performance.now() - start;
console.log(` MSM (128 points): ${elapsed.toFixed(2)}ms`);
assertTrue(elapsed < 20000, 'Should complete in reasonable time');
});
testAsync('Benchmark: MSM with 256 points (full BP+ verification size)', async () => {
const gens = initGenerators(256);
const scalars = [];
for (let i = 0; i < 256; i++) {
scalars.push(BigInt(i + 1));
}
const start = performance.now();
const result = multiScalarMul(scalars, gens.Gi.slice(0, 256));
const elapsed = performance.now() - start;
console.log(` MSM (256 points): ${elapsed.toFixed(2)}ms`);
assertTrue(elapsed < 30000, 'Should complete in reasonable time');
});
// ============================================================
// Proof Generation Tests
// ============================================================
console.log('\n--- Proof Generation Tests ---');
test('randomScalar generates valid scalar', () => {
const s = randomScalar();
assertTrue(s >= 0n, 'Scalar should be non-negative');
assertTrue(s < L, 'Scalar should be less than L');
});
test('randomScalar generates different values', () => {
const s1 = randomScalar();
const s2 = randomScalar();
assertTrue(s1 !== s2, 'Two random scalars should differ');
});
test('proveRange generates proof for single amount', () => {
const amount = 1000000n; // 1 SAL in atomic units
const mask = randomScalar();
const proof = proveRange(amount, mask);
assertExists(proof.V);
assertExists(proof.A);
assertExists(proof.A1);
assertExists(proof.B);
assertExists(proof.r1);
assertExists(proof.s1);
assertExists(proof.d1);
assertExists(proof.L);
assertExists(proof.R);
assertEqual(proof.V.length, 1, 'Should have 1 commitment');
assertEqual(proof.L.length, 6, 'Should have 6 L points for 64-bit proof');
assertEqual(proof.R.length, 6, 'Should have 6 R points');
});
test('proveRange proof verifies correctly', () => {
const amount = 12345678n;
const mask = randomScalar();
const proof = proveRange(amount, mask);
// Verify the proof
const valid = verifyBulletproofPlus(proof.V, proof);
assertTrue(valid, 'Proof should verify');
});
test('proveRange works for zero amount', () => {
const amount = 0n;
const mask = randomScalar();
const proof = proveRange(amount, mask);
const valid = verifyBulletproofPlus(proof.V, proof);
assertTrue(valid, 'Zero amount proof should verify');
});
test('proveRange works for max amount (2^64 - 1)', () => {
const amount = (1n << 64n) - 1n;
const mask = randomScalar();
const proof = proveRange(amount, mask);
const valid = verifyBulletproofPlus(proof.V, proof);
assertTrue(valid, 'Max amount proof should verify');
});
test('proveRangeMultiple generates proof for 2 amounts', () => {
const amounts = [100n, 200n];
const masks = [randomScalar(), randomScalar()];
const proof = proveRangeMultiple(amounts, masks);
assertEqual(proof.V.length, 2, 'Should have 2 commitments');
assertEqual(proof.L.length, 7, 'Should have 7 L points for 2-amount proof');
});
test('proveRangeMultiple proof verifies correctly', () => {
const amounts = [1000000n, 2000000n];
const masks = [randomScalar(), randomScalar()];
const proof = proveRangeMultiple(amounts, masks);
const valid = verifyBulletproofPlus(proof.V, proof);
assertTrue(valid, 'Multi-amount proof should verify');
});
test('serializeProof produces correct size', () => {
const amount = 100n;
const mask = randomScalar();
const proof = proveRange(amount, mask);
const bytes = serializeProof(proof);
// Size = 3 points + 3 scalars + 6 L/R pairs = 6*32 + 6*64 = 576 bytes
assertEqual(bytes.length, 576, 'Serialized proof should be 576 bytes');
});
test('serialized proof can be parsed and verified', () => {
const amount = 999n;
const mask = randomScalar();
const proof = proveRange(amount, mask);
const bytes = serializeProof(proof);
// Parse it back
const parsed = parseProof(bytes);
// Verify with original V
const valid = verifyBulletproofPlus(proof.V, parsed);
assertTrue(valid, 'Parsed proof should verify');
});
test('invalid amount (>= 2^64) throws error', () => {
const amount = 1n << 64n; // Exactly 2^64, out of range
const mask = randomScalar();
let threw = false;
try {
proveRange(amount, mask);
} catch (e) {
threw = true;
}
assertTrue(threw, 'Should throw for out-of-range amount');
});
test('mismatched amounts/masks throws error', () => {
const amounts = [100n, 200n];
const masks = [randomScalar()]; // Only 1 mask
let threw = false;
try {
proveRangeMultiple(amounts, masks);
} catch (e) {
threw = true;
}
assertTrue(threw, 'Should throw for mismatched arrays');
});
// ============================================================
// Proof Generation Benchmarks
// ============================================================
console.log('\n--- Proof Generation Benchmarks ---');
testAsync('Benchmark: Single amount proof generation', async () => {
const amount = 1000000000n;
const mask = randomScalar();
const start = performance.now();
const proof = proveRange(amount, mask);
const elapsed = performance.now() - start;
console.log(` Single proof generation: ${elapsed.toFixed(2)}ms`);
assertTrue(elapsed < 60000, 'Should complete in reasonable time');
});
testAsync('Benchmark: Proof generation + verification round-trip', async () => {
const amount = 123456789n;
const mask = randomScalar();
const start = performance.now();
const proof = proveRange(amount, mask);
const genTime = performance.now() - start;
const verifyStart = performance.now();
const valid = verifyBulletproofPlus(proof.V, proof);
const verifyTime = performance.now() - verifyStart;
console.log(` Generation: ${genTime.toFixed(2)}ms, Verification: ${verifyTime.toFixed(2)}ms`);
console.log(` Round-trip: ${(genTime + verifyTime).toFixed(2)}ms`);
assertTrue(valid, 'Proof should verify');
});
// ============================================================
// Summary
// ============================================================
console.log('\n--- Bulletproofs+ 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 tests passed!');
}