Add live sync tests, fix MockDaemon for new batch engine

- Add sync-live.test.js: real daemon sync test with stats reporting
    (binary path, adaptive batching, throughput, tx counts)
  - Add sync-live-fallback.test.js: binary vs JSON fallback comparison
  - Fix MockDaemon getBlock() to return json field (needed by _syncBatch)
  - Fix DEFAULT_BATCH_SIZE assertion (100 → 10)
This commit is contained in:
Matt Hess
2026-02-09 15:44:55 +00:00
parent 99b7239d9d
commit 32b60e09f6
3 changed files with 357 additions and 3 deletions
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bun
/**
* Live Sync Fallback Test
*
* Tests the JSON fallback path by disabling binary endpoint.
* Compares performance against binary path.
*
* Usage:
* bun test/sync-live-fallback.test.js [--daemon URL] [--start HEIGHT] [--blocks N]
*/
import { WalletSync, SYNC_STATUS, DEFAULT_BATCH_SIZE } from '../src/wallet-sync.js';
import { MemoryStorage } from '../src/wallet-store.js';
import { DaemonRPC } from '../src/rpc/daemon.js';
import { randomScalar, scalarMultBase } from '../src/crypto/index.js';
import { bytesToHex } from '../src/address.js';
const args = process.argv.slice(2);
function getArg(name, fallback) {
const idx = args.indexOf(name);
return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : fallback;
}
const DAEMON_URL = getArg('--daemon', 'http://core2.whiskymine.io:19081');
const START_HEIGHT = parseInt(getArg('--start', '1000'), 10);
const BLOCK_COUNT = parseInt(getArg('--blocks', '100'), 10);
function generateTestKeys() {
const viewSec = randomScalar();
const spendSec = randomScalar();
const spendPub = scalarMultBase(spendSec);
return {
viewSecretKey: bytesToHex(viewSec),
spendSecretKey: bytesToHex(spendSec),
spendPublicKey: bytesToHex(spendPub),
};
}
async function runSync(daemon, label) {
const storage = new MemoryStorage();
await storage.open();
await storage.setSyncHeight(START_HEIGHT);
const keys = generateTestKeys();
const sync = new WalletSync({ storage, daemon, keys, batchSize: DEFAULT_BATCH_SIZE });
const stats = { batches: 0, blocks: 0, batchSizes: [] };
sync.on('newBlock', () => { stats.blocks++; });
sync.on('batchComplete', (data) => {
stats.batches++;
stats.batchSizes.push(data.batchSize);
});
const targetHeight = START_HEIGHT + BLOCK_COUNT;
const origGetInfo = daemon.getInfo.bind(daemon);
daemon.getInfo = async () => {
const r = await origGetInfo();
if (r.success) r.result.height = targetHeight;
return r;
};
const t0 = Date.now();
try {
await sync.start(START_HEIGHT);
} catch (e) {
console.error(` [${label}] Sync failed: ${e.message}`);
}
const elapsed = Date.now() - t0;
// Restore getInfo
daemon.getInfo = origGetInfo;
console.log(` [${label}] ${stats.blocks} blocks in ${(elapsed / 1000).toFixed(2)}s (${(stats.blocks / (elapsed / 1000)).toFixed(1)} blocks/sec)`);
console.log(` [${label}] ${stats.batches} batches, sizes: [${stats.batchSizes.join(', ')}]`);
console.log(` [${label}] Status: ${sync.status}`);
await storage.close();
return { elapsed, blocks: stats.blocks, status: sync.status };
}
async function main() {
console.log('=== Sync Fallback Path Comparison ===\n');
console.log(`Daemon: ${DAEMON_URL}`);
console.log(`Range: ${START_HEIGHT}${START_HEIGHT + BLOCK_COUNT}\n`);
const daemon = new DaemonRPC({ url: DAEMON_URL, timeout: 30000 });
const info = await daemon.getInfo();
if (!info.success) {
console.error('Failed to connect:', info.error);
process.exit(1);
}
console.log(`Daemon height: ${info.result.height}\n`);
// 1. Binary path
console.log('--- Binary Bulk Fetch ---');
const binResult = await runSync(daemon, 'binary');
// 2. JSON fallback path (disable binary endpoint)
console.log('\n--- JSON Fallback (parallel) ---');
const origGetBlocksByHeight = daemon.getBlocksByHeight;
daemon.getBlocksByHeight = null; // Force JSON fallback
const jsonResult = await runSync(daemon, 'json');
daemon.getBlocksByHeight = origGetBlocksByHeight; // Restore
// 3. Compare
console.log('\n--- Comparison ---');
const speedup = jsonResult.elapsed / binResult.elapsed;
console.log(`Binary: ${(binResult.elapsed / 1000).toFixed(2)}s`);
console.log(`JSON: ${(jsonResult.elapsed / 1000).toFixed(2)}s`);
console.log(`Binary is ${speedup.toFixed(1)}x faster`);
const pass = binResult.status === 'complete' && jsonResult.status === 'complete';
console.log(`\nBoth paths: ${pass ? 'PASS' : 'FAIL'}`);
process.exit(pass ? 0 : 1);
}
main().catch(e => { console.error('Fatal:', e); process.exit(1); });
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env bun
/**
* Live Sync Engine Test
*
* Tests wallet-sync.js against a real Salvium daemon.
* Syncs a range of blocks and reports:
* - Binary bulk fetch vs JSON fallback path
* - Adaptive batch sizing behavior
* - Throughput (blocks/sec)
* - Transaction scanning (miner_tx / protocol_tx / regular)
*
* Usage:
* bun test/sync-live.test.js [--daemon URL] [--start HEIGHT] [--blocks N]
*
* Defaults:
* --daemon http://core2.whiskymine.io:19081
* --start 1000
* --blocks 200
*/
import { WalletSync, SYNC_STATUS, DEFAULT_BATCH_SIZE } from '../src/wallet-sync.js';
import { MemoryStorage, WalletOutput } from '../src/wallet-store.js';
import { DaemonRPC } from '../src/rpc/daemon.js';
import { randomScalar, scalarMultBase } from '../src/crypto/index.js';
import { bytesToHex } from '../src/address.js';
// ── CLI args ──────────────────────────────────────────────────────────────────
const args = process.argv.slice(2);
function getArg(name, fallback) {
const idx = args.indexOf(name);
return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : fallback;
}
const DAEMON_URL = getArg('--daemon', 'http://core2.whiskymine.io:19081');
const START_HEIGHT = parseInt(getArg('--start', '1000'), 10);
const BLOCK_COUNT = parseInt(getArg('--blocks', '200'), 10);
// ── Stats ─────────────────────────────────────────────────────────────────────
const stats = {
binaryBatches: 0,
jsonBatches: 0,
batchSizes: [],
msPerBlock: [],
blocksPerSec: [],
events: { newBlock: 0, syncProgress: 0, batchComplete: 0 },
errors: [],
txCounts: { minerTx: 0, protocolTx: 0, regular: 0 },
};
// ── Helpers ───────────────────────────────────────────────────────────────────
function generateTestKeys() {
// Generate random wallet keys for scanning (won't find outputs, but exercises the code)
const viewSec = randomScalar();
const spendSec = randomScalar();
const spendPub = scalarMultBase(spendSec);
return {
viewSecretKey: bytesToHex(viewSec),
spendSecretKey: bytesToHex(spendSec),
spendPublicKey: bytesToHex(spendPub),
};
}
// ── Main ──────────────────────────────────────────────────────────────────────
async function main() {
console.log('=== Live Sync Engine Test ===\n');
console.log(`Daemon: ${DAEMON_URL}`);
console.log(`Range: ${START_HEIGHT}${START_HEIGHT + BLOCK_COUNT}`);
console.log(`Initial batch size: ${DEFAULT_BATCH_SIZE}\n`);
// 1. Connect to daemon
const daemon = new DaemonRPC({ url: DAEMON_URL, timeout: 30000 });
const info = await daemon.getInfo();
if (!info.success) {
console.error('Failed to connect to daemon:', info.error);
process.exit(1);
}
const chainHeight = info.result.height;
console.log(`Daemon height: ${chainHeight}`);
if (START_HEIGHT + BLOCK_COUNT > chainHeight) {
console.error(`Requested range exceeds chain height (${chainHeight}). Adjust --start or --blocks.`);
process.exit(1);
}
// 2. Quick sanity: test binary endpoint availability
console.log('\nTesting binary endpoint (getBlocksByHeight)...');
try {
const binTest = await daemon.getBlocksByHeight([START_HEIGHT]);
if (binTest.success && binTest.result.blocks?.length === 1) {
console.log(' Binary endpoint: AVAILABLE');
const blk = binTest.result.blocks[0];
console.log(` Block blob size: ${blk.block?.length || blk.block?.byteLength || '?'} bytes`);
console.log(` Embedded txs: ${blk.txs?.length || 0}`);
} else {
console.log(' Binary endpoint: NOT AVAILABLE (will use JSON fallback)');
}
} catch (e) {
console.log(` Binary endpoint: ERROR — ${e.message}`);
}
// 3. Create sync engine with test wallet
const storage = new MemoryStorage();
await storage.open();
await storage.setSyncHeight(START_HEIGHT);
const keys = generateTestKeys();
const sync = new WalletSync({
storage,
daemon,
keys,
batchSize: DEFAULT_BATCH_SIZE,
});
// 4. Wire up events
sync.on('newBlock', (data) => {
stats.events.newBlock++;
if (data.hasMinerTx) stats.txCounts.minerTx++;
if (data.hasProtocolTx) stats.txCounts.protocolTx++;
stats.txCounts.regular += data.txCount || 0;
});
sync.on('syncProgress', (data) => {
stats.events.syncProgress++;
// Log every 50 blocks
if (stats.events.syncProgress % 50 === 0) {
console.log(` Progress: ${data.currentHeight} / ${data.targetHeight} (${data.percentComplete.toFixed(1)}%)`);
}
});
sync.on('batchComplete', (data) => {
stats.events.batchComplete++;
stats.batchSizes.push(data.batchSize);
stats.msPerBlock.push(data.msPerBlock);
stats.blocksPerSec.push(data.blocksPerSec);
});
sync.on('syncError', (error) => {
stats.errors.push(error.message);
});
// Override target height so we only sync BLOCK_COUNT blocks
// We do this by patching daemon.getInfo to return our target
const originalGetInfo = daemon.getInfo.bind(daemon);
const targetHeight = START_HEIGHT + BLOCK_COUNT;
daemon.getInfo = async function () {
const result = await originalGetInfo();
if (result.success) {
result.result.height = targetHeight;
}
return result;
};
// 5. Run sync
console.log(`\nSyncing ${BLOCK_COUNT} blocks (${START_HEIGHT}${targetHeight})...\n`);
const syncStart = Date.now();
try {
await sync.start(START_HEIGHT);
} catch (e) {
console.error(`\nSync failed: ${e.message}`);
if (e.stack) console.error(e.stack);
}
const syncElapsed = Date.now() - syncStart;
// 6. Report results
console.log('\n=== Results ===\n');
console.log(`Status: ${sync.status}`);
console.log(`Time: ${(syncElapsed / 1000).toFixed(1)}s`);
console.log(`Blocks synced: ${stats.events.newBlock}`);
console.log(`Overall throughput: ${(stats.events.newBlock / (syncElapsed / 1000)).toFixed(1)} blocks/sec`);
console.log('\n--- Batch Sizing ---');
console.log(`Total batches: ${stats.events.batchComplete}`);
if (stats.batchSizes.length > 0) {
console.log(`Batch size range: ${Math.min(...stats.batchSizes)}${Math.max(...stats.batchSizes)}`);
const avgBatch = stats.batchSizes.reduce((a, b) => a + b, 0) / stats.batchSizes.length;
console.log(`Average batch size: ${avgBatch.toFixed(1)}`);
console.log(`Batch size progression: [${stats.batchSizes.join(', ')}]`);
}
console.log('\n--- Per-Block Timing ---');
if (stats.msPerBlock.length > 0) {
const avgMs = stats.msPerBlock.reduce((a, b) => a + b, 0) / stats.msPerBlock.length;
console.log(`Average ms/block: ${avgMs.toFixed(1)}`);
console.log(`Min ms/block: ${Math.min(...stats.msPerBlock)}`);
console.log(`Max ms/block: ${Math.max(...stats.msPerBlock)}`);
const avgBps = stats.blocksPerSec.reduce((a, b) => a + b, 0) / stats.blocksPerSec.length;
console.log(`Average blocks/sec (batch): ${avgBps.toFixed(1)}`);
}
console.log('\n--- Transactions ---');
console.log(`Miner TXs processed: ${stats.txCounts.minerTx}`);
console.log(`Protocol TXs processed: ${stats.txCounts.protocolTx}`);
console.log(`Regular TXs processed: ${stats.txCounts.regular}`);
console.log('\n--- Events ---');
console.log(`newBlock events: ${stats.events.newBlock}`);
console.log(`syncProgress events: ${stats.events.syncProgress}`);
console.log(`batchComplete events: ${stats.events.batchComplete}`);
if (stats.errors.length > 0) {
console.log('\n--- Errors ---');
for (const err of stats.errors) {
console.log(` ${err}`);
}
}
// 7. Verify
console.log('\n--- Verification ---');
const finalHeight = await storage.getSyncHeight();
const expected = targetHeight;
const pass = sync.status === SYNC_STATUS.COMPLETE && finalHeight >= expected - 1;
console.log(`Final sync height: ${finalHeight}`);
console.log(`Expected: >= ${expected - 1}`);
console.log(`Status: ${pass ? 'PASS' : 'FAIL'}`);
await storage.close();
if (!pass) {
console.log('\nTest FAILED');
process.exit(1);
} else {
console.log('\nTest PASSED');
process.exit(0);
}
}
main().catch(e => {
console.error('Fatal:', e);
process.exit(1);
});
+8 -3
View File
@@ -97,6 +97,7 @@ class MockDaemon {
async getBlock(opts) {
const height = opts.height;
this.callLog.push(`getBlock(${height})`);
const txHashes = this.blocks[height]?.txHashes || [];
return {
success: true,
result: {
@@ -105,7 +106,11 @@ class MockDaemon {
hash: `block_hash_${height}`,
timestamp: 1700000000 + height * 120
},
tx_hashes: this.blocks[height]?.txHashes || [],
json: JSON.stringify({
miner_tx: { version: 2, unlock_time: height + 60, vin: [{ gen: { height } }], vout: [], extra: [] },
tx_hashes: txHashes
}),
tx_hashes: txHashes,
miner_tx_hash: `miner_tx_${height}`
}
};
@@ -153,8 +158,8 @@ test('SYNC_STATUS has correct values', () => {
assertEqual(SYNC_STATUS.ERROR, 'error');
});
test('DEFAULT_BATCH_SIZE is 100', () => {
assertEqual(DEFAULT_BATCH_SIZE, 100);
test('DEFAULT_BATCH_SIZE is 10', () => {
assertEqual(DEFAULT_BATCH_SIZE, 10);
});
test('SYNC_UNLOCK_BLOCKS is 10', () => {