Skip to content

[Consensus] Reject ProRegTx that spends its own collateral - #2985

Merged
Fuzzbawls merged 2 commits into
PIVX-Project:masterfrom
Liquid369:2026_fix_protx_collateral_selfspend
Oct 21, 2026
Merged

Fuzzbawls merged 2 commits into
PIVX-Project:masterfrom
Liquid369:2026_fix_protx_collateral_selfspend

Conversation

@Liquid369

Copy link
Copy Markdown
Member

A ProRegTx whose VIN includes the collateral outpoint it declares passes full validation today and halts block production when ProRegTx is enabled.

Path

AcceptToMemoryPool calls CheckSpecialTx (validation.cpp:491) before the tx's inputs are applied to the view, so GetUTXOCoin still finds the collateral and the tx is accepted to the mempool.

ConnectBlock runs UpdateCoins before ProcessSpecialTxsInBlock. By then, the collateral is spent, CheckProRegTx fails, and BlockAssembler throws on the TestBlockValidity failure rather than dropping the tx. Block production stops for as long as the tx is in the mempool.

existsProviderTxConflict does not catch it: mapNextTx records other transactions' inputs and cannot see the candidate's own.

Fix

Rejecting the tx at mempool time with bad-protx-collateral-used-as-input.

Placed ahead of GetUTXOCoin so the reject reason names the real cause at both call sites. After it, the ConnectBlock path reports bad-protx-collateral instead, which sends you looking at the UTXO set.

Only the external-collateral branch needs it. When collateralOutpoint.hash is null, the collateral is an output of this same tx and cannot also be one of its inputs.

Test

The regression test builds a fully valid ProRegTx, collateral ownership signature included. CreateProRegTx never sets vchSig, so no existing test constructs a valid external-collateral ProRegTx, and a test without it is rejected by CheckStringSig and never reaches the collateral rule.

Without this commit, CheckSpecialTx returns true for that tx with an empty reject reason. With it, `bad-protx-collateral-used-as

Verified locally: 11/11 cases pass across determinis evo_specialtx_testsandevo_trivialvalidation`.

Origin

Extracted from #2804, where it is one commit inside a 4500-line GUI change. #2804 will drop this on rebase.

Such a tx passes AcceptToMemoryPool, which runs CheckSpecialTx before applying
inputs to the view, so GetUTXOCoin still finds the collateral. ConnectBlock runs
UpdateCoins (validation.cpp:1602) before ProcessSpecialTxsInBlock (:1671), so by
then the coin is spent and validation fails. BlockAssembler throws on that failure
instead of dropping the tx (blockassembler.cpp:253), so block production stops for
as long as the tx sits in the mempool.

Checked ahead of GetUTXOCoin so the reject reason names the real cause at both call
sites; after it, ConnectBlock reports the misleading bad-protx-collateral instead.
Only the external-collateral branch needs this: when collateralOutpoint.hash is null
the collateral is an output of this same tx and cannot also be one of its inputs.

The regression test builds a fully valid ProRegTx, collateral ownership signature
included. Without that the tx is rejected by CheckStringSig and never reaches the
collateral rule. Without this commit CheckSpecialTx returns true for it.

Fix extracted from PIVX-Project#2804, where it is one commit inside a 4500-line GUI change.
@RainDeveloper69

Copy link
Copy Markdown

utACK 01165fe

Built locally on macOS arm64 and verified with:

  • deterministicmns_tests
  • evo_specialtx_tests
  • tiertwo_deterministicmns.py
  • relevant make-level test targets

All tests passed. I also verified the validation flow and the self-collateral-spend case described in the PR. The fix is minimal and correctly rejects the invalid ProRegTx before it can enter the mempool, while preserving valid external and internal collateral handling.

No blocking issues found.

@mkorovkin2

Copy link
Copy Markdown

ACK

Correct, minimal guard in CheckProRegTx (src/evo/specialtx_validation.cpp:154-158): rejects an external-collateral ProRegTx whose collateral outpoint is also its own input. Matches neighbouring DoS(10)/REJECT_INVALID checks; no consensus risk (ConnectBlock already rejected such blocks).

Tested:

  • targeted dip3_protx rc=0; full make check rc=0 (87/87 logs);
  • check-security rc=0; full functional --all 107/107 passed (4 env-only skips)
  • differential proof: PR test + unpatched source fails at evo_deterministicmns_tests.cpp:498-499 (pre-fix CheckSpecialTx returns true, empty reason); test is non-vacuous

Nits: line-numbered comment will drift; test calls CheckSpecialTx directly, so the block-halt claim is argued not E2E-proven; guard precedes CheckInputsHash (DoS-10 vs DoS-100);

A wallet whose only coin is the collateral makes protx_register fund the
fee from that coin, which builds the self-spending ProRegTx through the
normal RPC path. The test expects bad-protx-collateral-used-as-input, an
empty mempool, and a block still being mined. Without the fix the RPC
accepts the tx and the test fails.

@Duddino Duddino left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ACK 4c37486

@Fuzzbawls Fuzzbawls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ACK 4c37486

@Fuzzbawls
Fuzzbawls merged commit 4b3bc73 into PIVX-Project:master Oct 21, 2026
81 of 84 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants