7f01cafc62
Crypto Backend Refactoring
──────────────────────────
- Delete src/ed25519.js — all scalar/point operations now route through
the crypto provider (WASM/FFI/JSI backends only)
- Remove duplicate JS BigInt implementations of scReduce32, scReduce64,
scalarAdd, scalarMul from scanning.js, carrot.js, and carrot-scanning.js;
delegate to Rust backend via crypto/index.js
- Remove debug/test exports from index.js (randomPoint, testDouble,
getBasePoint, isOnCurve, etc.) that were only used during development
- Rebuild WASM binary (476KB → 487KB) with updated Rust crate
Consolidated CryptoNote Scanner (cn_scan)
─────────────────────────────────────────
- New Rust module crates/salvium-crypto/src/cn_scan.rs replaces 5-12
individual FFI round-trips per output with a single native call:
view tag check → derive subaddress pubkey → subaddress map lookup →
amount decryption → commitment mask → key image generation
- FFI wrapper salvium_cn_scan_output in ffi.rs + C header declaration
- FFI backend scanCnOutput() with full subaddress map marshaling
(32-byte key + u32 major/minor LE per entry) and JSON result parsing
- JSI backend delegation via this.native.cnScanOutput()
- wallet-sync.js _scanCNOutput() tries native path first when available
(FFI/JSI), falls through to existing JS pipeline for WASM/JS backends
- Change pub(crate) visibility on subaddress.rs cn_subaddress_secret_key
- 8 Rust unit tests covering view tag, amount, commitment mask,
subaddress matching, and key image generation
- Verified identical results: WASM (JS fallback) and FFI (native cn_scan)
produce same 964 outputs at same chain height; FFI is 3x faster sync
(0.8s vs 2.5s) with 12x less heap (12MB vs 150MB)
RCT Batch Signature Verification
─────────────────────────────────
- New Rust module crates/salvium-crypto/src/rct_verify.rs — single-call
verification of all ring signatures in a transaction (CLSAG + TCLSAG),
avoiding N individual JS↔Rust boundary crossings
- Computes pre-MLSAG message hash matching C++ get_pre_mlsag_hash
- FFI export salvium_verify_rct_signatures with flat byte array interface
- FFI backend verifyRctSignatures() method
- JS backend stub returns null (validation.js handles JS fallback)
- validation.js: 200+ lines of RCT verification logic including
flattenKeyImages, packTclsagSigsFlat, packClsagSigsFlat helpers
Transaction Expansion
─────────────────────
- transaction.js: add expandTransaction() matching C++ expand_transaction_2
(copies key images from prefix inputs into TCLSAG/CLSAG signature structs)
- New test/expand-transaction.test.js (634 lines)
- New test/rct-verify-testnet.test.js (430 lines)
Mining Resilience
─────────────────
- salvium-miner main.rs: retry get_info and get_block_template up to 5
times with 2s delay for transient daemon errors
- full-testnet.js mineTo(): retry miner up to 3 times with 3s delay,
check for partial progress between attempts
Testnet Tooling
───────────────
- sync-only.js: CRYPTO_BACKEND env var for A/B testing (wasm vs ffi)
- full-testnet.js: daemon URL update (node12.whiskymine.io)
- Debug scripts for cn_scan development (debug-cn-scan/marshal/match/wasm)
- Android .gitignore and build-bundle.sh for mobile builds
493 lines
17 KiB
JavaScript
493 lines
17 KiB
JavaScript
/**
|
|
* Key Image Tests
|
|
*
|
|
* Tests for key image generation, validation, and export/import.
|
|
* Phase 4 of salvium-js implementation.
|
|
*/
|
|
|
|
import {
|
|
hashToPoint,
|
|
generateKeyImage,
|
|
deriveKeyImageGenerator,
|
|
isValidKeyImage,
|
|
keyImageToY,
|
|
keyImageFromY,
|
|
exportKeyImages,
|
|
importKeyImages
|
|
} from '../src/keyimage.js';
|
|
import {
|
|
generateSeed,
|
|
deriveKeys
|
|
} from '../src/carrot.js';
|
|
import {
|
|
generateKeyDerivation,
|
|
derivePublicKey,
|
|
deriveSecretKey
|
|
} from '../src/scanning.js';
|
|
import {
|
|
bytesToHex,
|
|
hexToBytes
|
|
} from '../src/index.js';
|
|
import { scalarMultBase } from '../src/crypto/index.js';
|
|
import { initCrypto } from '../src/crypto/index.js';
|
|
|
|
await initCrypto();
|
|
|
|
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++;
|
|
}
|
|
}
|
|
|
|
function assertEqual(actual, expected, message = '') {
|
|
if (actual !== expected) {
|
|
throw new Error(`${message} Expected "${expected}", got "${actual}"`);
|
|
}
|
|
}
|
|
|
|
function assertLength(value, length, message = '') {
|
|
if (value.length !== length) {
|
|
throw new Error(`${message} Expected length ${length}, got ${value.length}`);
|
|
}
|
|
}
|
|
|
|
function assertTrue(value, message = '') {
|
|
if (!value) {
|
|
throw new Error(`${message} Expected true, got ${value}`);
|
|
}
|
|
}
|
|
|
|
function assertNotNull(value, message = '') {
|
|
if (value === null || value === undefined) {
|
|
throw new Error(`${message} Expected non-null value`);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Setup Test Keys
|
|
// ============================================================
|
|
|
|
console.log('\n--- Setting Up Test Keys ---');
|
|
|
|
// Known test seed for reproducible tests
|
|
const TEST_SEED = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001');
|
|
const keys = deriveKeys(TEST_SEED);
|
|
|
|
// Simulate a transaction secret key
|
|
const TX_SECRET_KEY = hexToBytes('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef');
|
|
|
|
console.log(` Spend Public Key: ${bytesToHex(keys.spendPublicKey).substring(0, 16)}...`);
|
|
|
|
// ============================================================
|
|
// Hash to Point Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Hash to Point Tests ---');
|
|
|
|
test('hashToPoint produces 32-byte result', () => {
|
|
const hp = hashToPoint(keys.spendPublicKey);
|
|
assertNotNull(hp);
|
|
assertLength(hp, 32);
|
|
});
|
|
|
|
test('hashToPoint is deterministic', () => {
|
|
const hp1 = hashToPoint(keys.spendPublicKey);
|
|
const hp2 = hashToPoint(keys.spendPublicKey);
|
|
assertEqual(bytesToHex(hp1), bytesToHex(hp2));
|
|
});
|
|
|
|
test('hashToPoint produces different results for different inputs', () => {
|
|
const hp1 = hashToPoint(keys.spendPublicKey);
|
|
const hp2 = hashToPoint(keys.viewPublicKey);
|
|
assertTrue(bytesToHex(hp1) !== bytesToHex(hp2));
|
|
});
|
|
|
|
test('hashToPoint accepts hex string input', () => {
|
|
const hp1 = hashToPoint(keys.spendPublicKey);
|
|
const hp2 = hashToPoint(bytesToHex(keys.spendPublicKey));
|
|
assertEqual(bytesToHex(hp1), bytesToHex(hp2));
|
|
});
|
|
|
|
test('hashToPoint produces valid curve points', () => {
|
|
const hp = hashToPoint(keys.spendPublicKey);
|
|
assertTrue(isValidKeyImage(hp), 'Hash-to-point result should be valid curve point');
|
|
});
|
|
|
|
// ============================================================
|
|
// Key Image Generation Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Key Image Generation Tests ---');
|
|
|
|
test('generateKeyImage produces 32-byte result', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
const outputPubKey = derivePublicKey(derivation, 0, keys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, 0, keys.spendSecretKey);
|
|
|
|
const keyImage = generateKeyImage(outputPubKey, outputSecKey);
|
|
assertNotNull(keyImage);
|
|
assertLength(keyImage, 32);
|
|
});
|
|
|
|
test('generateKeyImage is deterministic', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
const outputPubKey = derivePublicKey(derivation, 0, keys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, 0, keys.spendSecretKey);
|
|
|
|
const ki1 = generateKeyImage(outputPubKey, outputSecKey);
|
|
const ki2 = generateKeyImage(outputPubKey, outputSecKey);
|
|
assertEqual(bytesToHex(ki1), bytesToHex(ki2));
|
|
});
|
|
|
|
test('generateKeyImage produces different images for different outputs', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
|
|
// Output 0
|
|
const outputPubKey0 = derivePublicKey(derivation, 0, keys.spendPublicKey);
|
|
const outputSecKey0 = deriveSecretKey(derivation, 0, keys.spendSecretKey);
|
|
const ki0 = generateKeyImage(outputPubKey0, outputSecKey0);
|
|
|
|
// Output 1
|
|
const outputPubKey1 = derivePublicKey(derivation, 1, keys.spendPublicKey);
|
|
const outputSecKey1 = deriveSecretKey(derivation, 1, keys.spendSecretKey);
|
|
const ki1 = generateKeyImage(outputPubKey1, outputSecKey1);
|
|
|
|
assertTrue(bytesToHex(ki0) !== bytesToHex(ki1), 'Different outputs should have different key images');
|
|
});
|
|
|
|
test('generateKeyImage accepts hex string inputs', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
const outputPubKey = derivePublicKey(derivation, 0, keys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, 0, keys.spendSecretKey);
|
|
|
|
const ki1 = generateKeyImage(outputPubKey, outputSecKey);
|
|
const ki2 = generateKeyImage(bytesToHex(outputPubKey), bytesToHex(outputSecKey));
|
|
assertEqual(bytesToHex(ki1), bytesToHex(ki2));
|
|
});
|
|
|
|
test('Same output with different derivations produces same key image', () => {
|
|
// This tests that key image depends only on the final output keys
|
|
const txPubKey1 = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation1 = generateKeyDerivation(txPubKey1, keys.viewSecretKey);
|
|
const outputPubKey = derivePublicKey(derivation1, 0, keys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation1, 0, keys.spendSecretKey);
|
|
|
|
// Generate key image
|
|
const ki = generateKeyImage(outputPubKey, outputSecKey);
|
|
|
|
// Verify: secretKey * G should equal publicKey
|
|
const computedPubKey = scalarMultBase(outputSecKey);
|
|
assertEqual(bytesToHex(computedPubKey), bytesToHex(outputPubKey),
|
|
'Output secret/public key pair should be valid');
|
|
|
|
// Key image should be valid
|
|
assertTrue(isValidKeyImage(ki));
|
|
});
|
|
|
|
// ============================================================
|
|
// Key Image Validation Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Key Image Validation Tests ---');
|
|
|
|
test('isValidKeyImage accepts valid key images', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
const outputPubKey = derivePublicKey(derivation, 0, keys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, 0, keys.spendSecretKey);
|
|
const ki = generateKeyImage(outputPubKey, outputSecKey);
|
|
|
|
assertTrue(isValidKeyImage(ki));
|
|
});
|
|
|
|
test('isValidKeyImage rejects all zeros', () => {
|
|
const zeros = new Uint8Array(32);
|
|
assertTrue(!isValidKeyImage(zeros), 'All zeros should be rejected');
|
|
});
|
|
|
|
test('isValidKeyImage rejects wrong length', () => {
|
|
const short = new Uint8Array(31);
|
|
const long = new Uint8Array(33);
|
|
assertTrue(!isValidKeyImage(short), 'Short input should be rejected');
|
|
assertTrue(!isValidKeyImage(long), 'Long input should be rejected');
|
|
});
|
|
|
|
test('isValidKeyImage accepts hex string input', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
const outputPubKey = derivePublicKey(derivation, 0, keys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, 0, keys.spendSecretKey);
|
|
const ki = generateKeyImage(outputPubKey, outputSecKey);
|
|
|
|
assertTrue(isValidKeyImage(bytesToHex(ki)));
|
|
});
|
|
|
|
// ============================================================
|
|
// Key Image Generator Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Key Image Generator Tests ---');
|
|
|
|
test('deriveKeyImageGenerator equals hashToPoint', () => {
|
|
const gen1 = deriveKeyImageGenerator(keys.spendPublicKey);
|
|
const gen2 = hashToPoint(keys.spendPublicKey);
|
|
assertEqual(bytesToHex(gen1), bytesToHex(gen2));
|
|
});
|
|
|
|
test('deriveKeyImageGenerator produces 32 bytes', () => {
|
|
const gen = deriveKeyImageGenerator(keys.spendPublicKey);
|
|
assertLength(gen, 32);
|
|
});
|
|
|
|
// ============================================================
|
|
// Y-Coordinate Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Y-Coordinate Tests ---');
|
|
|
|
test('keyImageToY extracts y-coordinate', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
const outputPubKey = derivePublicKey(derivation, 0, keys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, 0, keys.spendSecretKey);
|
|
const ki = generateKeyImage(outputPubKey, outputSecKey);
|
|
|
|
const { y, sign } = keyImageToY(ki);
|
|
assertLength(y, 32);
|
|
assertTrue(typeof sign === 'boolean');
|
|
});
|
|
|
|
test('keyImageFromY reconstructs key image', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
const outputPubKey = derivePublicKey(derivation, 0, keys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, 0, keys.spendSecretKey);
|
|
const ki = generateKeyImage(outputPubKey, outputSecKey);
|
|
|
|
const { y, sign } = keyImageToY(ki);
|
|
const reconstructed = keyImageFromY(y, sign);
|
|
|
|
assertEqual(bytesToHex(reconstructed), bytesToHex(ki));
|
|
});
|
|
|
|
test('keyImageToY/keyImageFromY round-trip', () => {
|
|
// Test multiple key images
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const outputPubKey = derivePublicKey(derivation, i, keys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, i, keys.spendSecretKey);
|
|
const ki = generateKeyImage(outputPubKey, outputSecKey);
|
|
|
|
const { y, sign } = keyImageToY(ki);
|
|
const reconstructed = keyImageFromY(y, sign);
|
|
|
|
assertEqual(bytesToHex(reconstructed), bytesToHex(ki), `Round-trip failed for index ${i}`);
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// Export/Import Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Export/Import Tests ---');
|
|
|
|
test('exportKeyImages returns correct format', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
|
|
const outputs = [];
|
|
for (let i = 0; i < 3; i++) {
|
|
outputs.push({
|
|
outputPublicKey: derivePublicKey(derivation, i, keys.spendPublicKey),
|
|
outputSecretKey: deriveSecretKey(derivation, i, keys.spendSecretKey),
|
|
outputIndex: i
|
|
});
|
|
}
|
|
|
|
const exported = exportKeyImages(outputs);
|
|
|
|
assertEqual(exported.length, 3);
|
|
for (let i = 0; i < 3; i++) {
|
|
assertTrue(typeof exported[i].keyImage === 'string');
|
|
assertTrue(typeof exported[i].outputPublicKey === 'string');
|
|
assertEqual(exported[i].outputIndex, i);
|
|
assertEqual(exported[i].keyImage.length, 64); // 32 bytes = 64 hex chars
|
|
}
|
|
});
|
|
|
|
test('importKeyImages creates correct map', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
|
|
const outputs = [];
|
|
for (let i = 0; i < 3; i++) {
|
|
outputs.push({
|
|
outputPublicKey: derivePublicKey(derivation, i, keys.spendPublicKey),
|
|
outputSecretKey: deriveSecretKey(derivation, i, keys.spendSecretKey),
|
|
outputIndex: i
|
|
});
|
|
}
|
|
|
|
const exported = exportKeyImages(outputs);
|
|
const imported = importKeyImages(exported);
|
|
|
|
assertTrue(imported instanceof Map);
|
|
assertEqual(imported.size, 3);
|
|
|
|
for (const exp of exported) {
|
|
assertTrue(imported.has(exp.outputPublicKey));
|
|
assertEqual(imported.get(exp.outputPublicKey), exp.keyImage);
|
|
}
|
|
});
|
|
|
|
test('Export/import round-trip preserves data', () => {
|
|
const txPubKey = scalarMultBase(TX_SECRET_KEY);
|
|
const derivation = generateKeyDerivation(txPubKey, keys.viewSecretKey);
|
|
|
|
const outputs = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
outputs.push({
|
|
outputPublicKey: derivePublicKey(derivation, i, keys.spendPublicKey),
|
|
outputSecretKey: deriveSecretKey(derivation, i, keys.spendSecretKey),
|
|
outputIndex: i
|
|
});
|
|
}
|
|
|
|
const exported = exportKeyImages(outputs);
|
|
const imported = importKeyImages(exported);
|
|
|
|
// Verify we can look up each key image
|
|
for (const output of outputs) {
|
|
const pubKeyHex = bytesToHex(output.outputPublicKey);
|
|
assertTrue(imported.has(pubKeyHex), `Missing key image for output ${output.outputIndex}`);
|
|
|
|
// Regenerate key image and verify it matches
|
|
const ki = generateKeyImage(output.outputPublicKey, output.outputSecretKey);
|
|
assertEqual(imported.get(pubKeyHex), bytesToHex(ki));
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// Full Workflow Tests
|
|
// ============================================================
|
|
|
|
console.log('\n--- Full Workflow Tests ---');
|
|
|
|
test('Complete key image workflow for owned output', () => {
|
|
// 1. Generate wallet
|
|
const seed = generateSeed();
|
|
const walletKeys = deriveKeys(seed);
|
|
|
|
// 2. Simulate receiving a transaction
|
|
const txSecret = new Uint8Array(32);
|
|
crypto.getRandomValues(txSecret);
|
|
const txPubKey = scalarMultBase(txSecret);
|
|
|
|
// 3. Compute derivation and output keys
|
|
const derivation = generateKeyDerivation(txPubKey, walletKeys.viewSecretKey);
|
|
const outputPubKey = derivePublicKey(derivation, 0, walletKeys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, 0, walletKeys.spendSecretKey);
|
|
|
|
// 4. Generate key image
|
|
const keyImage = generateKeyImage(outputPubKey, outputSecKey);
|
|
|
|
// 5. Verify key image is valid
|
|
assertTrue(isValidKeyImage(keyImage));
|
|
|
|
// 6. Verify public/secret key pair is valid
|
|
const computedPubKey = scalarMultBase(outputSecKey);
|
|
assertEqual(bytesToHex(computedPubKey), bytesToHex(outputPubKey));
|
|
});
|
|
|
|
test('Key images are unique per output', () => {
|
|
const seed = generateSeed();
|
|
const walletKeys = deriveKeys(seed);
|
|
|
|
const txSecret = new Uint8Array(32);
|
|
crypto.getRandomValues(txSecret);
|
|
const txPubKey = scalarMultBase(txSecret);
|
|
const derivation = generateKeyDerivation(txPubKey, walletKeys.viewSecretKey);
|
|
|
|
const keyImages = new Set();
|
|
|
|
// Generate key images for 20 outputs
|
|
for (let i = 0; i < 20; i++) {
|
|
const outputPubKey = derivePublicKey(derivation, i, walletKeys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, i, walletKeys.spendSecretKey);
|
|
const ki = generateKeyImage(outputPubKey, outputSecKey);
|
|
keyImages.add(bytesToHex(ki));
|
|
}
|
|
|
|
assertEqual(keyImages.size, 20, 'All key images should be unique');
|
|
});
|
|
|
|
test('View-only wallet can track spent outputs with imported key images', () => {
|
|
// Full wallet
|
|
const seed = generateSeed();
|
|
const fullWalletKeys = deriveKeys(seed);
|
|
|
|
// Simulate receiving 5 outputs
|
|
const txSecret = new Uint8Array(32);
|
|
crypto.getRandomValues(txSecret);
|
|
const txPubKey = scalarMultBase(txSecret);
|
|
const derivation = generateKeyDerivation(txPubKey, fullWalletKeys.viewSecretKey);
|
|
|
|
const outputs = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
const outputPubKey = derivePublicKey(derivation, i, fullWalletKeys.spendPublicKey);
|
|
const outputSecKey = deriveSecretKey(derivation, i, fullWalletKeys.spendSecretKey);
|
|
outputs.push({
|
|
outputPublicKey: outputPubKey,
|
|
outputSecretKey: outputSecKey,
|
|
outputIndex: i
|
|
});
|
|
}
|
|
|
|
// Full wallet exports key images
|
|
const exported = exportKeyImages(outputs);
|
|
|
|
// View-only wallet imports key images
|
|
const viewOnlyKeyImages = importKeyImages(exported);
|
|
|
|
// Simulate checking if outputs are spent
|
|
// In real implementation, this would check against blockchain key images
|
|
for (const output of outputs) {
|
|
const pubKeyHex = bytesToHex(output.outputPublicKey);
|
|
assertTrue(viewOnlyKeyImages.has(pubKeyHex), 'View-only should have key image');
|
|
|
|
// The view-only wallet now knows the key image for this output
|
|
// and can check if it appears in the blockchain (spent) or not
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// Summary
|
|
// ============================================================
|
|
|
|
console.log('\n--- Key Image 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 key image tests passed!');
|
|
}
|