java-topology/defects/bitcoin/unit/MiniMinerAncestorDeleteAlgorithm.java

261 lines
9.2 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
/**
* CWE-407 unit test: bitcoin-0001
*
* Models MiniMiner::DeleteAncestorPackage() + BuildMockTemplate().
*
* The real code maintains m_entries as a vector sorted by feerate. At each
* iteration it calls std::sort (re-sorting the whole vector), picks best,
* then calls DeleteAncestorPackage which does std::find(m_entries, anc) for
* each ancestor. After sort the best is at index 0, but for ancestor packages
* of size A > 1, the remaining ancestors are scattered throughout the sorted
* vector, requiring linear scans averaging O(E/2) each.
*
* SLOW: std::find linear scan — O(A × E) per DeleteAncestorPackage call
* FAST: HashMap index lookup — O(A) per DeleteAncestorPackage call
*
* To measure the defect accurately, we simulate multi-transaction ancestor
* packages by building a graph where each transaction has a parent in the
* mempool, so A grows with the chain depth.
*/
public class MiniMinerAncestorDeleteAlgorithm {
static long slowOps = 0;
static long fastOps = 0;
static class TxEntry {
final int txid;
final int parentTxid; // -1 = no parent (coinbase-like root)
int fee;
TxEntry(int txid, int parentTxid, int fee) {
this.txid = txid;
this.parentTxid = parentTxid;
this.fee = fee;
}
@Override public boolean equals(Object o) {
return o instanceof TxEntry && ((TxEntry) o).txid == txid;
}
@Override public int hashCode() { return txid; }
@Override public String toString() { return "Tx#" + txid; }
}
// -----------------------------------------------------------------------
// Build ancestor set for a given txid (all ancestors inclusive)
// -----------------------------------------------------------------------
static Set<TxEntry> computeAncestors(TxEntry root, HashMap<Integer, TxEntry> txMap) {
Set<TxEntry> ancs = new HashSet<>();
TxEntry cur = root;
while (cur != null) {
ancs.add(cur);
cur = cur.parentTxid >= 0 ? txMap.get(cur.parentTxid) : null;
}
return ancs;
}
// -----------------------------------------------------------------------
// SLOW: linear std::find to locate each ancestor in the entries vector
// -----------------------------------------------------------------------
static void deleteAncestorPackageSlow(List<TxEntry> entries, Set<TxEntry> ancestors) {
for (TxEntry anc : new ArrayList<>(ancestors)) {
// std::find(m_entries.begin(), m_entries.end(), anc) — O(E)
int idx = -1;
for (int i = 0; i < entries.size(); i++) {
slowOps++;
if (entries.get(i).txid == anc.txid) {
idx = i;
break;
}
}
if (idx >= 0) {
entries.remove(idx);
}
}
}
static int buildMockTemplateSlow(int numTxns, int chainLen) {
// Build chain: chain of length chainLen, repeated to fill numTxns
List<TxEntry> entries = new ArrayList<>();
HashMap<Integer, TxEntry> txMap = new HashMap<>();
Random rng = new Random(42);
for (int i = 0; i < numTxns; i++) {
int parent = (i % chainLen == 0) ? -1 : (i - 1);
TxEntry tx = new TxEntry(i, parent, rng.nextInt(100) + 1);
entries.add(tx);
txMap.put(i, tx);
}
// Shuffle entries to scatter them (simulating mempool arrival order ≠ feerate order)
Collections.shuffle(entries, new Random(99));
// Rebuild txMap based on txid (unchanged)
int iterations = 0;
while (!entries.isEmpty()) {
// Pick best: highest fee entry that has no unprocessed parents
TxEntry best = null;
for (TxEntry e : entries) {
if (e.parentTxid < 0 || !txMap.containsKey(e.parentTxid)) {
if (best == null || e.fee > best.fee) {
best = e;
}
}
}
if (best == null) best = entries.get(0); // fallback
// Compute ancestors (all in-mempool parents in the chain)
Set<TxEntry> ancestors = computeAncestors(best, txMap);
// Only include ancestors still in entries
ancestors.retainAll(new HashSet<>(entries));
deleteAncestorPackageSlow(entries, ancestors);
// Remove from txMap
for (TxEntry anc : ancestors) {
txMap.remove(anc.txid);
}
iterations++;
}
return iterations;
}
// -----------------------------------------------------------------------
// FAST: HashMap index for O(1) lookup
// -----------------------------------------------------------------------
static void deleteAncestorPackageFast(List<TxEntry> entries,
HashMap<Integer, Integer> index,
Set<TxEntry> ancestors) {
List<TxEntry> toDelete = new ArrayList<>(ancestors);
for (TxEntry anc : toDelete) {
fastOps++; // O(1) index lookup
Integer idx = index.remove(anc.txid);
if (idx == null) continue;
// Swap-and-pop: O(1) removal
int last = entries.size() - 1;
if (idx != last) {
TxEntry moved = entries.get(last);
entries.set(idx, moved);
index.put(moved.txid, idx);
}
entries.remove(last);
}
}
static int buildMockTemplateFast(int numTxns, int chainLen) {
List<TxEntry> entries = new ArrayList<>();
HashMap<Integer, TxEntry> txMap = new HashMap<>();
HashMap<Integer, Integer> index = new HashMap<>();
Random rng = new Random(42);
for (int i = 0; i < numTxns; i++) {
int parent = (i % chainLen == 0) ? -1 : (i - 1);
TxEntry tx = new TxEntry(i, parent, rng.nextInt(100) + 1);
entries.add(tx);
txMap.put(i, tx);
index.put(i, i);
}
Collections.shuffle(entries, new Random(99));
// Rebuild index after shuffle
index.clear();
for (int i = 0; i < entries.size(); i++) {
index.put(entries.get(i).txid, i);
}
int iterations = 0;
while (!entries.isEmpty()) {
TxEntry best = null;
for (TxEntry e : entries) {
if (e.parentTxid < 0 || !txMap.containsKey(e.parentTxid)) {
if (best == null || e.fee > best.fee) {
best = e;
}
}
}
if (best == null) best = entries.get(0);
Set<TxEntry> ancestors = computeAncestors(best, txMap);
ancestors.retainAll(new HashSet<>(entries));
deleteAncestorPackageFast(entries, index, ancestors);
for (TxEntry anc : ancestors) {
txMap.remove(anc.txid);
}
iterations++;
}
return iterations;
}
// -----------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------
static boolean runTest(int numTxns, int chainLen) {
slowOps = 0;
fastOps = 0;
int slowIter = buildMockTemplateSlow(numTxns, chainLen);
int fastIter = buildMockTemplateFast(numTxns, chainLen);
// Both should produce same number of iterations (ancestor packages processed)
if (slowIter != fastIter) {
System.out.printf("FAIL N=%d chain=%d: slowIter=%d != fastIter=%d%n",
numTxns, chainLen, slowIter, fastIter);
return false;
}
// Avoid divide-by-zero
double ratio = fastOps > 0 ? (double) slowOps / fastOps : (double) slowOps;
System.out.printf(" N=%d chain=%d iters=%d | slowOps=%d fastOps=%d ratio=%.1fx%n",
numTxns, chainLen, slowIter, slowOps, fastOps, ratio);
if (ratio < 5.0) {
System.out.printf("FAIL N=%d chain=%d: ratio %.1fx < 5x threshold%n",
numTxns, chainLen, ratio);
return false;
}
return true;
}
public static void main(String[] args) {
int pass = 0;
int fail = 0;
System.out.println("=== bitcoin-0001: MiniMiner DeleteAncestorPackage O(A×E) ===");
System.out.println();
// (numTxns, chainLen): chainLen>1 means multi-tx ancestor packages
int[][] tests = {
{50, 5},
{100, 5},
{200, 5},
{500, 5},
{200, 10},
};
for (int[] t : tests) {
boolean ok = runTest(t[0], t[1]);
if (ok) { pass++; } else { fail++; }
}
System.out.println();
System.out.printf("%d/%d PASS%n", pass, pass + fail);
if (fail > 0) {
System.exit(1);
}
}
}