fix(wallet): don't cache CoinJoin rounds for txes unknown to the wallet - #7636
fix(wallet): don't cache CoinJoin rounds for txes unknown to the wallet#7636PastaPastaPasta wants to merge 1 commit into
Conversation
GetRealOutpointCoinJoinRounds() memoizes its result in mapOutpointRoundsCache, including the -1 it returns for an outpoint whose transaction the wallet has no record of. Nothing invalidates that entry when the wallet later learns about the transaction (the only invalidation is ClearCoinJoinRoundsCache(), called from the coinjoinsalt RPCs), so the -1 sticks for the lifetime of the wallet in memory. This used to be unreachable in practice because every caller passed wallet-owned outpoints. Since dash#7261, fundrawtransaction, send and walletcreatefundedpsbt feed arbitrary user-supplied preset inputs into IsFullyMixed(), so querying an outpoint before the wallet knows its transaction (e.g. while a rescan is still running) permanently marks it as having no rounds: a coin that is in fact a fully mixed denomination is then excluded from use_cj spends, missing from the anonymized balance and re-qualifies for mixing. Drop the cache entry on that path instead of memoizing it; the recursion only descends into IsMine inputs whose transactions are always present, so the directly queried outpoint is the only case that can be unknown.
|
✅ Final review complete — no blockers (commit 5fa6b4a) |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. Walkthrough
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to This change prevents unknown wallet transactions from being permanently treated as having no CoinJoin rounds and adds regression coverage for re-evaluation after the transaction is learned. No actionable merge-blocking risk remains beyond normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Codex/Sol only (Phase 2 disabled)
The exact-head diff safely removes only the provisional cache entry for an unknown transaction, while preserving recursion protection for known wallet transactions. The regression test covers the stale-cache sequence, and ./src/test/test_dash --run_test=coinjoin_tests passed locally; no in-scope issue was confirmed.
Source: reviewer backends gpt-5.6-sol (general and dash-core-commit-history); final verifier backend gpt-5.6-sol; orchestration-only openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).
Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed),gpt-5.6-sol— dash-core-commit-history (completed) - Verifier:
gpt-5.6-sol— verifier - Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
- Secondary pass: disabled (
temporary_phase2_sonnet_disable)
Issue being fixed or feature implemented
Follow-up to #7261.
CWallet::GetRealOutpointCoinJoinRounds()memoizes its result inmapOutpointRoundsCache, including the-1it returns for an outpoint whose transaction the wallet has no record of. Nothing invalidates that entry when the wallet later learns about the transaction — the only invalidation isClearCoinJoinRoundsCache(), called from thecoinjoinsaltRPCs.This used to be unreachable in practice because every caller passed wallet-owned outpoints. Since #7261,
fundrawtransaction,sendandwalletcreatefundedpsbtfeed arbitrary user-supplied preset inputs intoIsFullyMixed(), so querying an outpoint before the wallet knows its transaction (e.g. while a rescan is still running) permanently poisons the cache: once the wallet does add that transaction, a coin that is in fact a fully mixed denomination keeps reporting-1rounds for the lifetime of the wallet in memory. It is then excluded fromuse_cjspends, missing from the anonymized balance ingetbalances, and re-qualifies for mixing, so the client would re-mix (and pay fees on) an already-mixed coin.What was done?
Don't memoize the unknown-transaction result: drop the just-inserted cache entry on that path and return
-1directly. The recursion inGetRealOutpointCoinJoinRounds()only descends into inputs for whichInputIsMine()is true, which requires the previous transaction to be inmapWallet, so the directly queried outpoint is the only case that can be unknown — the emplace-based cycle protection for the recursive path is unaffected.Added a unit regression test that queries an outpoint before the wallet knows its transaction (expects
-1) and again afterAddToWallet()(expects the real rounds). The test fails with-1 != 1without the fix.How Has This Been Tested?
src/wallet/test/coinjoin_tests.cpp:./src/test/test_dash --run_test=coinjoin_testspasses with the fix and fails without it.test/functional/test_runner.py rpc_coinjoin.py wallet_send.py wallet_fundrawtransaction.pyall pass locally (macOS arm64,--enable-debug).Breaking Changes
None. The only behavior change is that an unknown outpoint is re-evaluated on subsequent queries instead of being permanently cached as having no rounds.
Checklist: