fix(model): replace O(N²) prefix sum in checkDuplicateTransactionsInSubtree (regression from #198)#904
Conversation
…ubtree (regression from bsv-blockchain#198) The loop summing b.SubtreeSlices[i].Size() for i < subIdx was O(N) per subtree, making the full checkDuplicateTransactions pass O(N²). Each Size() acquires a read lock, so at N=450k subtrees this saturates all cores with lock contention. The loop comment claiming (subIdx * subtreeSize) is invalid was wrong: baseIdx is the sum of subtrees BEFORE subIdx. Per Block.go:1293, every subtree except the last must be full-size, so all subtrees before subIdx are full-size regardless of whether subIdx itself is the smaller last subtree. The O(1) formula is always correct. Adds TestCheckDuplicateTransactions_LargeBlockNotQuadratic (N=20k subtrees, completes in <1s vs tens of seconds with the old code) and TestCheckDuplicateTransactionsInSubtree_BaseIndexCorrectness covering subIdx=0, smaller last subtree, subtreeSize=1, and cross-subtree dup detection.
|
🤖 Claude Code Review Status: Complete Review SummaryThis PR correctly fixes a critical O(N²) performance regression in duplicate transaction checking. The change is mathematically sound, well-tested, and follows project conventions. Findings: No issues found. Analysis: The fix replaces an O(N) loop that summed all predecessor subtree sizes with a closed-form O(1) calculation (baseIdx = subIdx * subtreeSize). This is correct because:
The inline comment correctly quotes the invariant and explains the optimization, satisfying the "Philosophy" requirement from AGENTS.md to make tradeoffs explicit. |
|
Benchmark Comparison ReportBaseline: Current: Summary
All benchmark results (sec/op)
Threshold: >10% with p < 0.05 | Generated: 2026-05-19 19:07 UTC |
|
Closing as duplicate of #910 — Simon's PR lands the same A subset of edge-case subtests from this PR is worth carrying forward — |



Closes #900.
Summary
Block.checkDuplicateTransactionsInSubtreewalked all preceding subtrees on every invocation, callingSubtree.Size()per subtree — an O(N) prefix sum executed undercheckDuplicateTransactionsConcurrencyworkers in parallel, producing O(N²) atomic RWMutex traffic. On a 450k-subtree block this saturated 11.6 cores inSize()lock churn with effectively zero useful work.Regression introduced in
58a68d376(PR #198).Fix
Replace the O(N) loop with
baseIdx := subIdx * subtreeSize. The "all subtrees need to be the same size as the first tree, except the last one" invariant (model/Block.go:1289) means every subtree beforesubIdxis full-size — the smaller-last-subtree never appears in the sum. Closed-form computation is O(1) per call.Comment quotes the invariant inline so a future drift in the invariant's location doesn't decouple from the formula.
Test plan
TestCheckDuplicateTransactions_LargeBlockNotQuadratic— synthetic 20k-subtree block; verifies wall-time well under a 30s budget. (Local: ~18ms.)TestCheckDuplicateTransactionsInSubtree_BaseIndexCorrectness— 4 subtests coveringsubIdx=0, smaller-last-subtree,subtreeSize=1degenerate, cross-subtree duplicate detection.go test -race ./model/...clean.make lint-newclean.