56fbea363c
─────────────────────
- Rebuild WASM binary via wasm-pack — adds batch exports that were missing
from stale binary (cn_subaddress_map_batch, carrot_subaddress_map_batch,
derive_carrot_keys_batch, compute_carrot_view_tag, decrypt_carrot_amount,
parse_extra, serialize_tx_extra, compute_tx_prefix_hash, and 8 more)
- Deprecate JS backend scalar/point ops — JS backend now provides hashing
only (keccak256, blake2b, sha256); all EC math requires WASM or FFI
- Add initCrypto() entry point that loads WASM automatically at startup
- Wire batch subaddress, CARROT key derivation, CARROT helpers, and
tx_extra parsing/serialization through all four backends (WASM/FFI/JSI/JS)
- Extend crypto provider with 15 new delegating functions for the
unified backend interface
FFI use-after-free fix (critical)
─────────────────────────────────
- Fix Bun segfault / SIGILL crash when using CRYPTO_BACKEND=ffi:
toArrayBuffer() from bun:ffi returns a zero-copy VIEW of Rust-owned
heap memory. Calling salvium_storage_free_buf() afterwards left the
Uint8Array pointing to freed memory — classic use-after-free.
With small buffers the freed memory wasn't reused (masking the bug);
at 50×200 subaddress scale (~400KB) the allocator reused immediately,
corrupting data and crashing with SIGILL at 1.11GB RSS.
- Fix: .slice() after toArrayBuffer() to copy into JS-owned memory
before freeing the Rust buffer. Applied to all 4 affected call sites:
cnSubaddressMapBatch, carrotSubaddressMapBatch, parseExtra,
serializeTxExtra.
Subaddress batch fallback hardening
────────────────────────────────────
- Log warnings (not silent catch) when batch WASM/FFI calls fail,
so the fallback to per-item JS loop is visible in console output
Rust crate (salvium-crypto)
───────────────────────────
- Add crates/salvium-crypto/src/subaddress.rs — batch CN + CARROT
subaddress map generation with #[wasm_bindgen] + #[no_mangle] FFI
- Add crates/salvium-crypto/src/carrot_keys.rs — batch CARROT key
derivation (full + view-only) from master secret
- Add crates/salvium-crypto/src/tx_format.rs — tx_extra parse/serialize
and tx prefix hash computation
- Extend ffi.rs with 15 new extern "C" entry points for the above
- Extend lib.rs with matching #[wasm_bindgen] entry points
Test suite updates
──────────────────
- Add initCrypto() to all test files that perform key derivation or
EC math (wallet-class, persistent-wallet, integration-sync, keys,
keyimage, subaddress, scanning, address, transaction, etc.)
- Fix integration-sync.test.js: always init WASM unless FFI explicitly
requested (JS backend can't do scalar/point ops post-deprecation)
- Rewrite test/full-testnet.js: replace hardcoded phase functions with
data-driven FORKS[] table covering all 10 hard forks (HF1–HF10),
WASM miner probes at every fork boundary, per-fork TX tests with
era-appropriate asset types and address formats, --resume-from and
--skip-mining CLI flags
README
──────
- Update JS backend description: "Hashing only (keccak, blake2b)"
- Add initCrypto() requirement to Quick Start and all code examples
- Add full-testnet commands to Testing section
120 lines
4.0 KiB
JavaScript
120 lines
4.0 KiB
JavaScript
#!/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, initCrypto } 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);
|
|
}
|
|
|
|
await initCrypto();
|
|
main().catch(e => { console.error('Fatal:', e); process.exit(1); });
|