Skip to content

fix(drive-abci)!: fail expired withdrawals below Core's dust threshold instead of re-signing them forever - #4737

Merged
QuantumExplorer merged 1 commit into
v4.2-devfrom
claude/mnowatch-stuck-broadcasts-31bcbd
Sep 14, 2026
Merged

QuantumExplorer merged 1 commit into
v4.2-devfrom
claude/mnowatch-stuck-broadcasts-31bcbd

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

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 sendrawtransaction fails, the withdrawal expires 48 Core blocks later, and rebroadcast_expired_withdrawal_documents re-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 whose status enum admits it. SYSTEM_DATA_CONTRACT_VERSIONS_V3 selects withdrawals: 2, and transition_to_version_14 re-stores the contract with apply_contract, the same way it re-stores DashPay.
  • rebroadcast_expired_withdrawal_documents v2: 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, None in V1 to V3. The threshold helper core_dust_threshold_duffs in rs-dpp mirrors Core's GetDustThreshold and is parity-tested against rust-dashcore's Script::dust_value (546 P2PKH, 540 P2SH at the default rate).
  • No refund. 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. The credits stay locked in that pool, where they already were.
  • delete_withdrawal_data_trigger v2 lets owners delete FAILED withdrawals as they can COMPLETE ones.
  • Latent bug fixed en route: the masternode-reward-shares and wallet-utils contract crates dispatched on system_data_contracts.withdrawals instead 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?

  • New unit tests: rebroadcast v2 (191-duff withdrawal ends FAILED with its untied transaction gone and nothing queued; a 1000-duff withdrawal is re-signed with the new sign height and its transaction queued; the rule refuses to run on a version table without a dust rate; the 545/546-duff boundary), delete trigger v2 (FAILED and COMPLETE deletable, QUEUED/POOLED/BROADCASTED/EXPIRED rejected), the v13 to v14 transition (stored status enum 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:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Protocol version 14 now identifies expired withdrawals below Core’s dust threshold and marks them as Failed instead of repeatedly re-signing them.
    • Failed withdrawals can now be deleted, alongside completed withdrawals.
    • Added withdrawals contract version 2 with expanded schema support.
  • Bug Fixes

    • Corrected contract version selection for withdrawals, wallet utilities, and masternode reward shares.
    • Added dust-threshold handling consistent with Core’s relay rules.

…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>
@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 59ec8447-cf90-4e57-b843-35cae2c080a3

📥 Commits

Reviewing files that changed from the base of the PR and between 01d9447 and 610ca66.

📒 Files selected for processing (22)
  • packages/masternode-reward-shares-contract/src/lib.rs
  • packages/rs-dpp/src/withdrawal/core_dust_threshold.rs
  • packages/rs-dpp/src/withdrawal/mod.rs
  • packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs
  • packages/rs-drive-abci/src/execution/platform_events/withdrawals/rebroadcast_expired_withdrawal_documents/mod.rs
  • packages/rs-drive-abci/src/execution/platform_events/withdrawals/rebroadcast_expired_withdrawal_documents/v2/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v2/mod.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs
  • packages/rs-platform-version/src/version/mocks/v2_test.rs
  • packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs
  • packages/rs-platform-version/src/version/system_limits/mod.rs
  • packages/rs-platform-version/src/version/system_limits/v1.rs
  • packages/rs-platform-version/src/version/system_limits/v2.rs
  • packages/rs-platform-version/src/version/system_limits/v3.rs
  • packages/rs-platform-version/src/version/system_limits/v4.rs
  • packages/rs-platform-version/src/version/v14.rs
  • packages/wallet-utils-contract/src/lib.rs
  • packages/withdrawals-contract/schema/v2/withdrawals-documents.json
  • packages/withdrawals-contract/src/lib.rs
  • packages/withdrawals-contract/src/v2/mod.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Withdrawal dust handling

Layer / File(s) Summary
Withdrawal contract and protocol versioning
packages/withdrawals-contract/..., packages/rs-platform-version/..., packages/masternode-reward-shares-contract/src/lib.rs, packages/wallet-utils-contract/src/lib.rs
Withdrawals v2 adds the terminal FAILED status and schema. Protocol v14 configures Core’s dust relay fee and updates method versions. Contract loaders now use their matching system-contract version fields.
Core dust threshold calculation
packages/rs-dpp/src/withdrawal/*
Adds Core-compatible dust threshold calculation and tests for script types, OP_RETURN outputs, fee scaling, and default thresholds.
Expired withdrawal dust processing
packages/rs-drive-abci/src/execution/platform_events/withdrawals/rebroadcast_expired_withdrawal_documents/*
Version 2 marks expired withdrawals below the Core dust threshold as FAILED and removes their broadcasted transactions. Other expired withdrawals return to the signing queue.
FAILED withdrawal deletion
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/*
Version 2 permits deletion of COMPLETE and FAILED withdrawals and rejects other statuses.
Protocol upgrade contract activation
packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs
The v14 transition re-stores the withdrawals contract so the stored status enum includes FAILED.

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
Loading

Merge Risk: ⚪ Minimal · up to 610ca

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)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #3615 requires Core-compatible dust handling and resolution of the stuck withdrawal. The PR adds the Core dust threshold helper, protocol v14 system limit, FAILED status, and v2 expired-withdraw… Add the required credit refund for a withdrawal marked FAILED, including automated coverage for the affected amount. Preserve the Core-compatible dust check and terminal failure behavior.
Out of Scope Changes check ⚠️ Warning The changes in packages/masternode-reward-shares-contract/src/lib.rs and packages/wallet-utils-contract/src/lib.rs correct contract-version field dispatch for contracts unrelated to the dust withd… Remove these unrelated dispatch fixes from this pull request, or link and scope them to a separate issue.
Docstring Coverage ⚠️ Warning 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:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: expired withdrawals below Core's dust threshold now fail instead of being re-signed indefinitely.
Full details: Linked Issues check

Explanation

Issue #3615 requires Core-compatible dust handling and resolution of the stuck withdrawal. The PR adds the Core dust threshold helper, protocol v14 system limit, FAILED status, and v2 expired-withdrawal handling. The handler removes the untied transaction and does not refund the withdrawn credits. This does not satisfy the issue request that the affected amount can be credited back.

Full details: Out of Scope Changes check

Explanation

The changes in packages/masternode-reward-shares-contract/src/lib.rs and packages/wallet-utils-contract/src/lib.rs correct contract-version field dispatch for contracts unrelated to the dust withdrawal requirements in issue #3615. The linked issue does not require these changes.

Full details: Docstring Coverage

Explanation

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.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/mnowatch-stuck-broadcasts-31bcbd

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 14, 2026
@thepastaclaw

thepastaclaw commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

🔍 Review in progress — actively reviewing now (commit 610ca66) · triage: critical

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.75362% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.29%. Comparing base (66efe32) to head (610ca66).
⚠️ Report is 2 commits behind head on v4.2-dev.

Files with missing lines Patch % Lines
...events_on_first_block_of_protocol_change/v0/mod.rs 74.39% 21 Missing ⚠️
...batch/data_triggers/triggers/withdrawals/v2/mod.rs 92.97% 13 Missing ⚠️
...rebroadcast_expired_withdrawal_documents/v2/mod.rs 97.51% 9 Missing ⚠️
...kages/rs-dpp/src/withdrawal/core_dust_threshold.rs 94.33% 3 Missing ⚠️
...ns/batch/data_triggers/triggers/withdrawals/mod.rs 0.00% 3 Missing ⚠️
...ls/rebroadcast_expired_withdrawal_documents/mod.rs 80.00% 1 Missing ⚠️
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     
Components Coverage Δ
dpp 83.67% <94.33%> (-0.02%) ⬇️
drive 84.35% <ø> (+0.25%) ⬆️
drive-abci 86.05% <92.62%> (-0.20%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 70.53% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 39.13% <ø> (+4.02%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Member Author

Reviewed head 610ca66e538e. No actionable findings.

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.

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed

@QuantumExplorer
QuantumExplorer merged commit 5659f3d into v4.2-dev Sep 14, 2026
48 checks passed
@QuantumExplorer
QuantumExplorer deleted the claude/mnowatch-stuck-broadcasts-31bcbd branch September 14, 2026 15:18
QuantumExplorer added a commit that referenced this pull request Sep 14, 2026
Pre-existing on v4.2-dev since #4737 (the end-to-end suite job fails on it at
9361d4c): the protocol version 14 withdrawals data trigger allows deleting
FAILED withdrawals too, and says so in its error message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Platform withdrawals send Dust amounts according to Core

2 participants