Repository navigation
fix(drive-abci)!: fail expired withdrawals below Core's dust threshold instead of re-signing them forever - #4737
Conversation
…d instead of re-signing them forever Two mainnet withdrawals (indices 9815 and 12090, 191 and 200 duffs, admitted under the 190-duff floor that applied before protocol version 12) have cycled BROADCASTED -> EXPIRED -> BROADCASTED since March and June 2026. Core's mempool rejects any output below its dust threshold (546 duffs for P2PKH) and applies that check to asset unlocks too, so every node's sendrawtransaction fails, the withdrawal expires 48 Core blocks later, and the rebroadcast step re-signs the same untied transaction with a fresh quorum, with no attempt cap and no terminal state. See issue #3615. From protocol version 14: - `WithdrawalStatus::FAILED = 5`, a terminal status, and withdrawals contract v2 whose `status` enum admits it, re-stored at the v14 activation block like the DashPay contract. - `rebroadcast_expired_withdrawal_documents` v2 marks an expired withdrawal FAILED and drops its untied transaction from the broadcasted tree when the whole amount is below the dust threshold of its output script; every other expired withdrawal is re-signed as before. The whole amount is the right quantity to test because the payout never exceeds it under any converter version. The threshold mirrors Core's GetDustThreshold at the new `SystemLimits::core_dust_relay_fee_per_kb` (3000 duffs/kB in V4, None before), parity-tested against rust-dashcore's `Script::dust_value`. - The withdrawn credits are NOT returned. A quorum signature for the payout was released, dust is a mempool policy rather than a consensus rule, and Platform cannot prove a signed transaction will never be mined, so a refund could double-spend Core's credit pool. They stay locked in that pool. - `delete_withdrawal_data_trigger` v2 lets owners delete FAILED withdrawals as they can COMPLETE ones. Also fixes the masternode-reward-shares and wallet-utils contract crates, which dispatched on the withdrawals contract version instead of their own and broke genesis at v14 the moment withdrawals moved to v2. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (22)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughChangesWithdrawal dust handling
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Bug fix · Severity of issue fixed: Medium Sequence Diagram(s)sequenceDiagram
participant Platform
participant WithdrawalContract
participant Drive
participant Core
Platform->>WithdrawalContract: load withdrawals v2 schema
Platform->>Drive: process expired withdrawal
Drive->>Core: evaluate output dust threshold
Core-->>Drive: return dust threshold
alt amount is below threshold
Drive->>Drive: set status to FAILED
Drive->>Drive: remove broadcasted transaction
else amount meets threshold
Drive->>Drive: set status to BROADCASTED
Drive->>Drive: queue transaction for signing
end
Merge Risk: ⚪ Minimal · up to The updated withdrawal flow is internally consistent for the supported output scripts, and no actionable merge-blocking risk remains. 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
Full details: Linked Issues checkExplanation Issue Full details: Out of Scope Changes checkExplanation The changes in Full details: Docstring CoverageExplanation Docstring coverage is 60.53% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 38 functions across 21 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 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 |
|
🔍 Review in progress — actively reviewing now (commit 610ca66) · triage: critical |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v4.2-dev #4737 +/- ##
============================================
+ Coverage 83.15% 83.29% +0.13%
============================================
Files 2836 2839 +3
Lines 389307 389582 +275
============================================
+ Hits 323718 324486 +768
+ Misses 65589 65096 -493
🚀 New features to boost your workflow:
|
|
Reviewed head Checked PV14-only version dispatch, the withdrawals schema upgrade and contract-cache refresh, bounded processing of expired withdrawals, transaction removal versus re-queuing, dust thresholds for the supported output scripts, preservation of withdrawn credits, and owner deletion of terminal withdrawals. I did not identify a regression in these paths or a change to earlier protocol-version behavior. Validation: the Rust CI job passed 15,644 tests (1,086 skipped). Its logs confirm all 12 new tests passed: four dust-threshold tests, four rebroadcast tests, three deletion-trigger tests, and the withdrawals-schema upgrade test. Scope: source review and CI-log verification; I did not run local tests or rehearse activation, rollback, or restart against authentic mainnet/testnet state. This is a PR review, not protocol-release certification. |
Issue being fixed or feature implemented
Fixes #3615.
Two mainnet withdrawals (indices 9815 and 12090, 191 and 200 duffs, admitted under the 190-duff floor that applied before protocol version 12) have shown as "Broadcasted (sent to core)" on mnowatch since March and June 2026. Core's mempool rejects any output below its dust threshold (546 duffs for P2PKH) and applies that check to asset unlocks too, so every node's
sendrawtransactionfails, the withdrawal expires 48 Core blocks later, andrebroadcast_expired_withdrawal_documentsre-signs the same untied transaction with a fresh quorum. There is no attempt cap and no terminal state, so the cycle runs every ~2 hours forever, costing a quorum signing and a state write each time.What was done?
Gated to protocol version 14 (unreleased, so the PV14 tables are edited in place):
WithdrawalStatus::FAILED = 5(terminal) and withdrawals contract v2 whosestatusenum admits it.SYSTEM_DATA_CONTRACT_VERSIONS_V3selectswithdrawals: 2, andtransition_to_version_14re-stores the contract withapply_contract, the same way it re-stores DashPay.rebroadcast_expired_withdrawal_documentsv2: before re-queuing an EXPIRED withdrawal it compares the whole amount, in duffs, against the dust threshold of the withdrawal's output script. Below the threshold the withdrawal becomes FAILED and its untied transaction is deleted from the broadcasted tree so nothing signs it again; otherwise it is re-signed exactly as in v1. The whole amount is the right quantity to test: the payout never exceeds it under any converter version, so an amount below the threshold is unmineable under all of them, and a mineable withdrawal can never be failed by mistake.SystemLimits::core_dust_relay_fee_per_kb:Some(3000)in V4,Nonein V1 to V3. The threshold helpercore_dust_threshold_duffsin rs-dpp mirrors Core'sGetDustThresholdand is parity-tested against rust-dashcore'sScript::dust_value(546 P2PKH, 540 P2SH at the default rate).delete_withdrawal_data_triggerv2 lets owners delete FAILED withdrawals as they can COMPLETE ones.system_data_contracts.withdrawalsinstead of their own field, so genesis at PV14 failed the moment withdrawals moved to v2.On mainnet, once PV14 activates, a withdrawal sitting in EXPIRED fails on the first block, and one sitting in BROADCASTED fails within one expiry window (about two hours). Clients that decode the status with
WithdrawalStatus::try_from(u8)(mnowatch, Dash Evo Tool, whose history view also filters on COMPLETE and EXPIRED only) need to learn the new value.How Has This Been Tested?
statusenum goes from 0..4 to 0..5), and the rs-dpp dust helper.cargo test -p drive-abci --lib -- platform_events::withdrawals triggers::withdrawals perform_events_on_first_block_of_protocol_change: 49 passed.cargo test -p drive-abci --test strategy_tests -- run_chain_withdrawal_expired: passes (viable withdrawals still expire and re-sign at PV14).cargo test -p dpp --lib core_dust_threshold,cargo test -p withdrawals-contract, clippy clean on the changed crates with tests.Breaking Changes
Consensus change at protocol version 14: a new withdrawal status value, a withdrawals contract v2, a new rule in the per-block rebroadcast step, and a relaxed delete trigger. Frozen v13 behaviour is unchanged.
Checklist:
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes