Files
salvium-rs/test/bulletproofs_plus.test.js
T
Matt Hess 949719c2e7 Add transfer/sweep/stake API, integrate BP+ proofs, dual CN+CARROT wallet keys
- Wire up full transaction lifecycle: UTXO selection → input prep → build → serialize → broadcast
   - Integrate bulletproofPlusProve() into buildTransaction() (replaces placeholder)
   - Fix BP+ serialization to match Salvium binary format (varint-prefixed vectors)
   - Fix serializeTransaction() ordering to match Salvium consensus (prefix → RCT base → prunable)
   - Create src/wallet/transfer.js with transfer(), sweep(), stake() high-level functions
   - Fix getOutputIndexes to use portable storage binary format
   - Fix prepareInputs to use getOutputDistribution for decoy selection
   - Handle coinbase outputs with null mask (identity scalar + zeroCommit)
   - Refactor Wallet class to always derive BOTH legacy CN and CARROT keys from seed
     (matching Salvium C++ carrot_and_legacy_account::generate behavior)
   - Add getLegacyAddress(), getCarrotAddress(), height-based auto-format in getAddress()
   - Bump wallet JSON to v3 with both addresses and full CARROT key set
   - Backward-compatible: old wallet files with seed upgrade seamlessly
2026-02-02 01:00:11 +00:00

566 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', () => {
// Build a proof in Salvium binary format:
// varint(V.len), V[], A, A1, B, r1, s1, d1, varint(L.len), L[], varint(R.len), R[]
const baseBytes = Point.BASE.toBytes();
const chunks = [];
// V: 1 commitment
chunks.push(new Uint8Array([1])); // varint(1)
chunks.push(baseBytes);
// A, A1, B
chunks.push(baseBytes);
chunks.push(baseBytes);
chunks.push(baseBytes);
// r1, s1, d1 (scalars)
const s1b = new Uint8Array(32); s1b[0] = 1;
const s2b = new Uint8Array(32); s2b[0] = 2;
const s3b = new Uint8Array(32); s3b[0] = 3;
chunks.push(s1b);
chunks.push(s2b);
chunks.push(s3b);
// L: 6 entries
chunks.push(new Uint8Array([6])); // varint(6)
for (let i = 0; i < 6; i++) chunks.push(baseBytes);
// R: 6 entries
chunks.push(new Uint8Array([6])); // varint(6)
for (let i = 0; i < 6; i++) chunks.push(baseBytes);
// Concatenate
let totalLen = 0;
for (const c of chunks) totalLen += c.length;
const proofBytes = new Uint8Array(totalLen);
let off = 0;
for (const c of chunks) { proofBytes.set(c, off); off += c.length; }
const proof = parseProof(proofBytes);
assertExists(proof.A);
assertExists(proof.A1);
assertExists(proof.B);
assertEqual(proof.V.length, 1);
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);
// Salvium binary format: varint(V.len) + V + A + A1 + B + r1 + s1 + d1 + varint(L.len) + L + varint(R.len) + R
// For single amount: 1 + 32 + 3*32 + 3*32 + 1 + 6*32 + 1 + 6*32 = 611 bytes
assertEqual(bytes.length, 611, 'Serialized proof should be 611 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!');
}