Replace O(n²) sync lookups with HashMap for O(1) access#245
Replace O(n²) sync lookups with HashMap for O(1) access#245schjonhaug merged 5 commits intomasterfrom
Conversation
Replace all 8 .find() calls in sync.rs that linearly scanned transaction collections inside loops, creating O(n²) complexity. Descriptor wallet sync (RBF/CPFP detection): - Build bdk_tx_map HashMap after collecting BDK transactions - Replace 4 .find() calls in RBF conflict and CPFP detection Address-based wallet sync (confirmation tracking): - Build existing_tx_map HashMap from existing transactions - Replace 2 .find() calls in single-address sync - Replace 2 .find() calls in grouped address watch sync Benchmarked with 673 transactions (48 unconfirmed, active CPFP chains): - CPFP detection: 74ms → 3ms (24x faster) - End-to-end tx processing: 116ms → 36ms (3.3x faster) Closes #236
🤖 Gemini Code ReviewHere is a review of the proposed changes to Code Quality and Best Practices
Potential Bugs or Issues
Performance Considerations
Security Concerns
Test Coverage
|
🤖 Codex Code Review
Suggested tests to add:
Security concerns specific to this diff: none identified. |
🤖 Claude Code ReviewNow I have a thorough picture of the PR. Let me provide the review. Pull Request ReviewThis PR bundles two independent changes: a backend performance optimization in Backend: O(n²) → O(1) HashMap Optimization (
|
| Area | Verdict |
|---|---|
| Performance optimization logic | ✅ Correct and well-measured |
Type improvements (Txid keys) |
✅ Better than String keys |
canonical_txids round-trip |
|
Nested loop from_str |
|
existing_tx_map key type |
|
| Frontend Suspense boundary | ✅ Correct |
| External link security | ✅ Good |
| i18n completeness | ✅ All 9 locales |
| Test coverage |
The optimization is sound and the benchmark data justifies it. The remaining issues are all minor; none are blockers.
- Remove redundant `existing_txids` HashSet in both address-watch functions; `existing_tx_map.contains_key()` replaces it, eliminating an extra allocation and iteration per sync cycle - Key `bdk_tx_map` by native `Txid` instead of `String` to avoid heap-allocating a hex string for every BDK transaction; only the fewer lookup keys are parsed via `Txid::from_str()`
- Collect bdk_tx_map directly from iterator, skip intermediate Vec - Replace contains_key + get double lookup with single if-let in both address-watch functions - Add warn! logging on Txid::from_str parse failures for debugging
…pect_err - Replace canonical_txids Vec with HashSet<Txid> so the conflict filter is O(1) per element instead of O(n) - Build conflicted_txids as Vec<Txid> directly from BDK graph, eliminating the Txid → String → Txid round-trip for conflict lookups - Use inspect_err instead of map_err for logging side effects, which is the idiomatic Rust pattern (stable since 1.76)
Summary
.find()calls insync.rsthat linearly scanned transaction collections inside loops, creating O(n²) complexitybdk_tx_mapandexisting_tx_mapHashMaps for O(1) lookups in RBF detection, CPFP detection, and address-based confirmation trackingBenchmark results
Tested with a 673-transaction stress wallet (48 unconfirmed, active CPFP chains) on regtest:
.find())The improvement grows quadratically with transaction count — at 50K+ transactions the O(n²) pattern would take minutes.
Changes
Descriptor wallet sync (RBF/CPFP detection):
bdk_tx_mapHashMap after collecting BDK transactions.find()calls in RBF conflict and CPFP child/parent lookupsAddress-based wallet sync (confirmation tracking):
existing_tx_mapHashMap from existing transactions.find()calls in single-address sync.find()calls in grouped address watch syncTest plan
cargo build— compiles cleanlycargo test -- --test-threads=1— all 147 tests pass./dev.sh create-stress-wallet 500(624 transactions)Closes #236