135 lines
3.3 KiB
JavaScript
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();
|