java-topology/defects/bcoin/test/bcoin-0001-test.js

135 lines
3.3 KiB
JavaScript

'use strict';
/**
* bcoin-0001: gettxoutproof Block.hasTX linear scan O(H*T)
*
* Block.hasTX(hash) calls Block.indexOf(hash) which scans
* block.txs linearly. When called in a loop over H hashes,
* total cost is O(H*T). Fix: build a Set from tx hashes
* before the loop, reducing to O(H+T).
*
* This test verifies the fix by simulating the pattern with
* realistic block sizes and measuring operation counts.
*/
// Simulate Buffer hashes
function makeHash(i) {
const buf = Buffer.alloc(32, 0);
buf.writeUInt32LE(i, 0);
return buf;
}
// Simulate block.txs array
function makeBlock(txCount) {
const txs = [];
for (let i = 0; i < txCount; i++) {
txs.push({ _hash: makeHash(i), hash() { return this._hash; } });
}
return { txs };
}
// BEFORE: linear scan per hasTX call (original Block.indexOf pattern)
function hasTXLinear(block, hash) {
for (let i = 0; i < block.txs.length; i++) {
if (block.txs[i].hash().equals(hash))
return true;
}
return false;
}
function verifyBefore(block, hashes) {
let ops = 0;
for (const hash of hashes) {
for (let i = 0; i < block.txs.length; i++) {
ops++;
if (block.txs[i].hash().equals(hash))
break;
}
}
return ops;
}
// AFTER: build hash set, O(1) lookup
function verifyAfter(block, hashes) {
let ops = 0;
const txSet = new Map();
for (const tx of block.txs) {
ops++;
txSet.set(tx.hash().toString('hex'), true);
}
for (const hash of hashes) {
ops++;
txSet.has(hash.toString('hex'));
}
return ops;
}
// Test correctness
function testCorrectness() {
const block = makeBlock(100);
const hashes = [makeHash(0), makeHash(50), makeHash(99)];
// Linear should find all
for (const hash of hashes) {
if (!hasTXLinear(block, hash)) {
console.log('FAIL: linear scan did not find hash');
process.exit(1);
}
}
// Set-based should also find all
const txSet = new Map();
for (const tx of block.txs)
txSet.set(tx.hash().toString('hex'), true);
for (const hash of hashes) {
if (!txSet.has(hash.toString('hex'))) {
console.log('FAIL: set-based lookup did not find hash');
process.exit(1);
}
}
// Should NOT find missing hash
const missing = makeHash(999);
if (hasTXLinear(block, missing)) {
console.log('FAIL: linear scan found non-existent hash');
process.exit(1);
}
if (txSet.has(missing.toString('hex'))) {
console.log('FAIL: set-based found non-existent hash');
process.exit(1);
}
console.log('PASS: correctness');
}
// Test performance ratio
function testPerformance() {
const T = 4000; // max block txs
const H = 100; // number of hashes to check
const block = makeBlock(T);
// Pick H hashes spread across the block
const hashes = [];
for (let i = 0; i < H; i++)
hashes.push(makeHash(Math.floor(i * T / H)));
const opsBefore = verifyBefore(block, hashes);
const opsAfter = verifyAfter(block, hashes);
const ratio = opsBefore / opsAfter;
console.log(`T=${T}, H=${H}`);
console.log(`Before: ${opsBefore} ops`);
console.log(`After: ${opsAfter} ops`);
console.log(`Ratio: ${ratio.toFixed(1)}x`);
if (ratio < 2) {
console.log('FAIL: expected at least 2x improvement');
process.exit(1);
}
console.log('PASS: performance');
}
testCorrectness();
testPerformance();