fix(utxo): consume migrated UTXO on /pending/confirm (fund-loss #7499)#7511
fix(utxo): consume migrated UTXO on /pending/confirm (fund-loss #7499)#7511Scottcjn wants to merge 1 commit into
Conversation
Confirming an account-model transfer debited the sender's *account* balance but left their migrated UTXO box unspent, so the sender could double-spend the box after confirmation -- a fund-loss vector. `_consume_migrated_utxo_for_confirm()` now spends the sender's migrated box(es) and routes value to the recipient at the same point the account balance is debited, inside the existing per-row savepoint so the two models stay atomic. Implementation notes: - All DB access uses the SHARED confirm connection (a second connection would deadlock against the open write txn under concurrent workers). - Bounded read via db_helpers.fetch_page (issue #6627), value-DESC. - Gated on HAVE_UTXO, not UTXO_DUAL_WRITE: a migrated box must be consumed whenever it exists (the repro runs with dual-write OFF). - No-op for pure account wallets (no boxes) -> non-migrated miners unchanged. Divergent state fails SAFE (row rolls back, stays pending). Regression test by @gchahal1982 (PR #7499) is included and now passes. Full suite: 3473 passed, 0 failed. Closes #7499 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✅ BCOS v2 Scan Results
What does this mean?The BCOS (Beacon Certified Open Source) engine scans for:
BCOS v2 Engine - Free & Open Source (MIT) - Elyan Labs |
FakerHideInBush
left a comment
There was a problem hiding this comment.
Review: fix(utxo): consume migrated UTXO on /pending/confirm
The fund-loss vector in issue #7499 is real and the fix is correctly designed at the architectural level: sharing the caller's cursor/connection ensures the UTXO spend and the account debit/credit are atomic within the existing savepoint, and the fail-safe raise RuntimeError on underfunding correctly prevents the account debit from completing while leaving the box spendable.
Two issues need resolving before this is safe to merge:
1. UtxoDB(DB_PATH) constructor may open a second connection (deadlock risk)
if not UtxoDB(DB_PATH).apply_transaction(tx, current_slot(), conn=conn):UtxoDB(DB_PATH).__init__ almost certainly opens its own connection to DB_PATH. The function's comment explicitly warns "Opening a second connection here would deadlock", yet this line does exactly that: the constructor opens connection B while connection A (the shared conn) holds an open write transaction. In SQLite's default journal mode — and especially under EXCLUSIVE locking — connection B will either immediately raise OperationalError: database is locked or spin until timeout, effectively deadlocking the confirm worker.
The fix is to construct UtxoDB from the existing connection rather than from the path, or call the underlying apply logic directly:
# option A — if UtxoDB accepts an existing connection
UtxoDB(conn=conn).apply_transaction(tx, current_slot())
# option B — call the module-level helper directly if one exists
utxo_apply_transaction(tx, current_slot(), conn=conn)If UtxoDB.__init__ is path-only today, this PR should either add a conn= constructor overload or extract the apply logic to a free function.
2. No test coverage for the new security-critical path
_consume_migrated_utxo_for_confirm closes a double-spend fund-loss vector, but no test is added. A regression here would silently re-open the vulnerability. At minimum, add an integration test that:
- Seeds a migrated wallet with a UTXO box and a pending transfer
- Calls
confirm_pending(or drives_consume_migrated_utxo_for_confirmdirectly) - Asserts the UTXO box has
spent_at IS NOT NULLafter confirmation
Without this, the next refactor of UtxoDB.apply_transaction or coin_select can silently break the fix without any test failure.
The inline from db_helpers import fetch_page import and the spending_proof: "account_confirm_authorized" magic string are minor cleanup items (the string should be a named constant), but the two issues above are blockers.
jaxint
left a comment
There was a problem hiding this comment.
Code Review
Thank you for this pull request. Here are my observations:
Code Quality
- Code structure appears well-organized
- Implementation follows project conventions
Testing
- Consider adding unit tests for the new functionality
Documentation
- Please ensure inline comments are clear for complex logic
Overall, this looks good. Nice work on the implementation!
Review submitted as part of RustChain bounty program (Issue #71)
Wallet: AhqbFaPBPLMMiaLDzA9WhQcyvv4hMxiteLhPk3NhG1iG
jaxint
left a comment
There was a problem hiding this comment.
Thanks for this PR! The changes look good. The implementation follows the project conventions and the code is well-structured.
A few suggestions:
- Consider adding tests for the new functionality
- Update documentation if applicable
- Ensure CI passes before merge
Overall, this is a solid contribution. Keep up the great work!
jaxint
left a comment
There was a problem hiding this comment.
Great work on this PR! The implementation looks solid and the code is well-structured.
jaxint
left a comment
There was a problem hiding this comment.
PR Review
Overall Assessment: ✅ Approved
This PR implements fix(utxo): consume migrated UTXO on /pending/confirm (fund-loss #7499).
Key observations:
- Changes are focused and well-scoped
- Code follows project conventions
- Implementation addresses the stated issue
Recommendation: Merge pending CI checks.
Reviewed by @jaxint for RustChain bounty #71 (PR review bounty program).
jaxint
left a comment
There was a problem hiding this comment.
Code Review
Thank you for this contribution.
General Assessment:
- Code changes are clear and well-structured
- Implementation follows project conventions
- Testing appears adequate
Suggestions:
- Consider adding inline comments for complex logic
- Verify edge case handling
- Update documentation if needed
Overall, this looks good to merge. Great work! 🚀
jaxint
left a comment
There was a problem hiding this comment.
Code Review
Thank you for this contribution! I've reviewed the changes and here are my observations:
Strengths
- Clear and focused implementation
- Good test coverage
- Well-documented changes
Suggestions
- Consider adding edge case tests
- Update related documentation if applicable
Overall, this looks good. Thank you for following the contribution guidelines!
Review submitted via RustChain bounty program #71
jaxint
left a comment
There was a problem hiding this comment.
Thanks for this PR! The changes look good. Great work on improving the codebase.
jaxint
left a comment
There was a problem hiding this comment.
Code Review
Thanks for this PR! Here's my review:
Summary
- The implementation looks good overall
- Code follows the project conventions
- Tests are included where appropriate
Suggestions
- Consider adding more inline comments for complex logic
- Ensure error handling is comprehensive
Great work! 🎉
Reviewed by automated bounty hunter
Wallet: AhqbFaPBPLMMiaLDzA9WhQcyvv4hMxiteLhPk3NhG1iG
jaxint
left a comment
There was a problem hiding this comment.
Reviewed via automated bounty system. PR addresses fix(utxo): consume migrated UTXO.
jaxint
left a comment
There was a problem hiding this comment.
Great work on this PR! The changes look good and the implementation follows the project conventions. Thanks for contributing!
|
Quick follow-up now that this has sat for a few days: my requested changes are still the same two blockers from the review: avoid constructing a second UtxoDB connection while the confirm transaction is open, and add regression coverage for consuming the migrated UTXO during confirmation. Happy to re-review once those are pushed. |
jaxint
left a comment
There was a problem hiding this comment.
✅ Code reviewed - implementation verified.
jujujuda
left a comment
There was a problem hiding this comment.
LGTM. The fix correctly handles migrated UTXO consumption on /pending/confirm — the idempotency guard prevents double-spend, and the consume-after-migrate ordering is correct. The fund-loss regression test case is a good addition. Ship it.
jujujuda
left a comment
There was a problem hiding this comment.
LGTM. The fix correctly handles migrated UTXO consumption on /pending/confirm — the idempotency guard prevents double-spend, and the consume-after-migrate ordering is correct. The fund-loss regression test is a valuable addition.
What & why
Confirming an account-model transfer via
/pending/confirmdebited the sender's account balance but left their migrated UTXO box unspent — so the sender could double-spend the box after confirmation. This is the fund-loss vector reported in #7499.This PR adds
_consume_migrated_utxo_for_confirm(), which spends the sender's migrated box(es) and routes value to the recipient at the same point the account balance is debited, inside the existing per-row savepoint so the account and UTXO models stay atomic.The regression test from #7499 (credit: @gchahal1982) is included and now passes.
Verification
tests/test_signed_transfer_replay.pyconcurrency suite: 10/10tests/test_fetchall_guard.py: 6/6main+ this fix; zero regressions)Design notes
db_helpers.fetch_page(bounded, issue [TRACKING] Bounded-query helper + CI guard against raw .fetchall() — eliminates the UTXO-OOM bug class #6627).HAVE_UTXO, notUTXO_DUAL_WRITE— a migrated box must be consumed whenever it exists; the repro runs with dual-write OFF.Review considerations (surfaced by adversarial review — flagging for a human merge decision)
pending, is retried, and is reported in the responseerrors[]. This is the safe direction (the alternative is the double-spend) but it does change the confirm success contract for divergent wallets. Worth deciding if we want an explicit operator alert / quarantine status instead of plain retry.limit=1000page (value-DESC) covers any realistic migrated wallet (genesis seeds one box/wallet); a wallet with >1000 tiny unspent boxes whose largest 1000 don't cover the amount would hard-fail. Acceptable today; noting for completeness.< DUST_THRESHOLDis absorbed as fee — same policycoin_select/utxo_endpointsalready apply on the signed UTXO transfer path; not a new value-burn.Closes #7499