Skip to content

fix: conflict ProRegTx collateral reuse with in-mempool masternode updates - #7489

Open
PastaPastaPasta wants to merge 3 commits into
dashpay:developfrom
PastaPastaPasta:sec/v054
Open

fix: conflict ProRegTx collateral reuse with in-mempool masternode updates#7489
PastaPastaPasta wants to merge 3 commits into
dashpay:developfrom
PastaPastaPasta:sec/v054

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

A ProRegTx that reuses an external collateral replaces the masternode that collateral
currently backs — the old masternode ceases to exist and its proTxHash becomes unknown.

The mempool does not model that. It tracks collateral spends
(removeProTxSpentCollateralConflicts), but reuse is not a spend, so:

  • A replacement ProRegTx and an update (ProUpServTx / ProUpRegTx / ProUpRevTx) targeting the
    masternode being replaced can sit in the mempool at the same time.
  • If both are selected into one block, BuildNewListFromBlock() deletes the masternode when
    it applies the ProRegTx, then fails bad-protx-hash on the update.
  • That aborts block assembly in the miner.

The same hazard exists when the replacement ProRegTx arrives in a block rather than through
the mempool: the stale update is left behind and can never be mined.

What was done?

Three commits.

1. Mempool admission (existsProviderTxConflict) — the two directions are now
symmetric:

  • A ProRegTx reusing an external collateral conflicts if any mempool transaction still
    references the incumbent masternode's proTxHash.
  • A ProUpServTx / ProUpRegTx / ProUpRevTx conflicts if a pending ProRegTx already claims its
    masternode's collateral outpoint.

This is first-in-wins with no RBF-style tiebreak. That is a deliberate policy choice: both
transactions are individually valid, and the only requirement is that they not coexist.
Preferring the incumbent would mean evicting an already-accepted transaction on arrival of a
new one, which is a bigger policy change than this problem warrants.

2. Eviction on block connect (removeProTxConflicts) — when a collateral-reusing
ProRegTx is mined, any mempool transaction still targeting the replaced masternode is
removed. This covers the case where the ProRegTx never passed through this node's mempool,
so the admission check above never ran.

3. Refactor — the removal loop inside removeProTxSpentCollateralConflicts is extracted
as CTxMemPool::removeProTxReferences() and reused by both call sites. No behaviour change;
it just avoids a second copy of the iterator-invalidation-safe loop.

One thing reviewers should look at

The eviction in (2) calls dmnman->GetListAtChainTip() and needs it to resolve to the list
at the previous tip, so GetMNByCollateral() returns the masternode being replaced rather
than its replacement. That holds because CDeterministicMNManager::tipIndex is assigned in
UpdatedBlockTip(), which fires from ActivateBestChain() (validation.cpp:3339-3340) —
after ConnectTip() calls removeForBlock() (validation.cpp:3025).

removeProTxSpentCollateralConflicts() already depends on this ordering, so the new code is
consistent with the file. The difference is that there it is an optimisation, whereas here it
is load-bearing: if the ordering changed, GetMNByCollateral() would return the replacement
masternode and the eviction would silently become a no-op. I have noted the dependency in a
comment, but flagging it explicitly since it is the least obvious part of the change.

How Has This Been Tested?

test_mempool_proreg_replacement_update_conflict in
src/test/evo_deterministicmns_tests.cpp, added in the commit preceding the fix. It
covers both paths:

  • admission, via existsProviderTxConflict() in both directions;
  • eviction, via removeForBlock() with a block containing the replacement ProRegTx.

Without the fix the admission assertions fail (2 failures) — the conflicting pair is
accepted. With the fix, evo_dip3_activation_tests passes (19 cases).

Built and run on macOS/arm64 against current develop.

Breaking Changes

Mempool policy only; no consensus rules change. Some transaction pairs that were previously
accepted together are now mutually exclusive in the mempool. Neither could have been mined
together, so nothing that was previously minable is rejected.

Known follow-up

Coverage is C++ unit level only. A functional test exercising the miner path end to end
would strengthen this; happy to add one in this PR if reviewers would prefer it before merge.

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 made corresponding changes to the documentation
  • I have assigned this pull request to a milestone

@PastaPastaPasta PastaPastaPasta changed the title fix(mempool): conflict ProRegTx collateral reuse with in-mempool masternode updates fix: conflict ProRegTx collateral reuse with in-mempool masternode updates Aug 1, 2026
@PastaPastaPasta
PastaPastaPasta marked this pull request as ready for review August 8, 2026 18:00
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b1fc45df-64ec-40b1-b072-733b22b058be

📥 Commits

Reviewing files that changed from the base of the PR and between 37b24bd and 3256c90.

📒 Files selected for processing (3)
  • src/test/evo_deterministicmns_tests.cpp
  • src/txmempool.cpp
  • src/txmempool.h

Walkthrough

The mempool now centralizes removal of transactions that reference an invalid ProTx. External-collateral ProRegTx replacements remove updates targeting the displaced masternode. Conflict checks cover provider service, registrar, and revoke transactions in both conflict directions. A V19 deterministic masternode regression test verifies insertion-order handling, mempool list rebuilding, and stale-update eviction after the replacement is mined.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: ⚪ Minimal · up to 3256c

This change prevents conflicting masternode registration and update transactions from coexisting or remaining stale after block connection. No actionable merge-blocking risk remains, so it is merge-ready after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant CTxMemPool
  participant ProRegTx
  participant ProviderUpdate
  Test->>CTxMemPool: submit ProviderUpdate
  Test->>CTxMemPool: submit ProRegTx replacement
  CTxMemPool->>ProRegTx: check external collateral references
  CTxMemPool->>ProviderUpdate: detect displaced ProTx reference
  CTxMemPool-->>Test: reject conflict or remove stale update
  Test->>CTxMemPool: mine replacement
  CTxMemPool-->>Test: evict obsolete update
Loading

Possibly related PRs

  • dashpay/dash#7437: Modifies ProTx reference tracking and conflict handling in src/txmempool.cpp.
  • dashpay/dash#7473: Adds related deterministic masternode mempool conflict handling and tests.

Suggested reviewers: udjinm6, knst

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the mempool conflict detection, stale-update eviction, refactor, testing, and policy impact.
Title check ✅ Passed The title clearly identifies the main change: preventing conflicts between collateral-reusing ProRegTx transactions and in-mempool masternode updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@PastaPastaPasta
PastaPastaPasta requested review from UdjinM6 and knst and removed request for knst August 8, 2026 18:01
A ProRegTx that reuses a confirmed external collateral deletes the live MN mid-block. The same-block update for that proTxHash then fails BuildNewListFromBlock with bad-protx-hash and aborts CreateNewBlock. Assert the in-block hazard and that existsProviderTxConflict must reject either ordering in the mempool.
existsProviderTxConflict now links a replacement ProRegTx that reuses a live external collateral to any in-mempool ProUpServ/ProUpReg/ProUpRev for the MN being replaced, so both cannot coexist and CreateNewBlock cannot package the bad-protx-hash ordering. removeProTxConflicts also drops those updates when such a replacement is mined. Consensus block acceptance is unchanged; only mempool packaging/eviction is tightened.
The earlier fix inlined a copy of removeProTxSpentCollateralConflicts' inner loop into removeProTxConflicts, minus the diagnostic log on the should-never-happen branch. Hoist that loop into a named CTxMemPool::removeProTxReferences helper and call it from both sites, so the two paths that drop TXs naming a vanished MN cannot drift apart.

Also add a removeForBlock assertion to the new test. existsProviderTxConflict only gates our own acceptance and cannot stop an attacker from mining the replacement ProRegTx themselves; the eviction hunk in removeProTxConflicts is what keeps the orphaned update from stalling our block assembly afterwards, and it previously had no coverage. Verified as a negative control: the new assertion fails (1 != 0) with that hunk disabled.
@thepastaclaw

thepastaclaw commented Aug 8, 2026

Copy link
Copy Markdown

🔍 Review in progress — actively reviewing now (commit 3256c90)
Stage: Opus review + final verification
ETA: complete ~20:30 UTC (median 15m across 30 recent reviews)
Running 4m · Last checked: 2026-08-08 20:20 UTC

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ad32c44ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/txmempool.cpp
Comment on lines +1437 to +1438
if (auto dmn = dmnman->GetListAtChainTip().GetMNByCollateral(proTx.collateralOutpoint)) {
if (mapProTxRefs.find(dmn->proTxHash) != mapProTxRefs.end()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check conflicts between staged package members

When a regtest submitpackage contains the replacement ProRegTx as a parent and its update as the child, and a raised mempool minimum causes both to be evaluated using package feerates, AcceptMultipleTransactions() runs every PreChecks() before adding any package member to mapProTxCollaterals or mapProTxRefs. These checks therefore see only the pre-existing mempool, after which SubmitPackage() inserts both transactions without rechecking; because the dependency forces the replacement before the update, block assembly can still hit bad-protx-hash. Add a staged/package-wide provider-conflict check or recheck as each member is finalized.

AGENTS.md reference: AGENTS.md:L163-L163

Useful? React with 👍 / 👎.

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.

Good structural observation — AcceptMultipleTransactions() does run every PreChecks() (including existsProviderTxConflict()) against the pre-package mempool before SubmitPackage() inserts anything, so intra-package provider conflicts are not seen. Two clarifications on scope:

  1. This blind spot is pre-existing and generic to the whole provider-conflict layer, not introduced here. Every check in existsProviderTxConflict() has it: a package containing two ProRegTxs claiming the same collateral, the same owner key, the same operator key, or the same address bypasses those checks identically today. The checks added in this PR inherit the same property; they don't widen it.

  2. The package path is not reachable outside regtest. submitpackage hard-fails outside -regtest (src/rpc/mempool.cpp), and the only other ProcessNewPackage() caller is testmempoolaccept, which is test_accept-only and never submits. There is no p2p package relay in this codebase.

So the fix for this belongs in a package-wide provider-conflict pass (e.g. re-running existsProviderTxConflict() per member as each is finalized in SubmitPackage(), or a staged check that includes prior package members), covering all conflict classes at once — a separate change from this mempool-policy fix. Keeping it out of this PR; noting it as a known follow-up before package relay is ever exposed beyond regtest.


🤖 Posted autonomously by Claude on behalf of pasta.

@PastaPastaPasta

Copy link
Copy Markdown
Member Author

@coderabbitai review\n\n---\n🤖 Posted autonomously by Codex on behalf of pasta.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

@PastaPastaPasta I will review the changes in #7489.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

2 participants