fix(consensus): reject Sprout JoinSplits (unverified proof + vpub_new inflation vector)

Sprout JoinSplit proofs/sigs/nullifiers/anchors are never verified (the verifier
arg to CheckTransaction is unused), yet vpub_new is counted as transparent
value-in -- a forged all-zero JoinSplit mints arbitrary value from nothing.
Reproduced on an isolated ac_private=1 chain: 500,000 minted into a z-addr,
accepted + mined + verifychain=true.

Reject any non-coinbase tx carrying a JoinSplit in ContextualCheckTransaction
(covers both mempool acceptance and ConnectBlock). DragonX is Sapling-only from
genesis with zero JoinSplits in its history (mainnet supply audit), so this is
inert on all legitimate traffic and never invalidates a historical block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 08:10:14 -05:00
parent d159e72086
commit d52550a6fc

View File

@@ -1283,6 +1283,16 @@ bool ContextualCheckTransaction(int32_t slowflag,const CBlock *block, CBlockInde
const bool overwinterActive = nHeight >=1 ? true : false; //NetworkUpgradeActive(nHeight, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER);
const bool saplingActive = nHeight >=1 ? true : false; //NetworkUpgradeActive(nHeight, Params().GetConsensus(), Consensus::UPGRADE_SAPLING);
// SECURITY FIX (JoinSplit inflation): reject Sprout JoinSplits at consensus. DragonX is
// Sapling-only from genesis (zero JoinSplits in its entire history, verified by the mainnet
// supply audit); a tx carrying one is illegitimate. Their zk-proof/sig/nullifier/anchor are
// never verified while vpub_new is counted as transparent value-in -> unlimited inflation.
// Coinbase/notary (IsMint) exempt. Unconditional is safe: no historical block has a JoinSplit.
if (!tx.IsMint() && !tx.vjoinsplit.empty()) {
return state.DoS(100, error("ContextualCheckTransaction(): Sprout JoinSplits are disabled (inflation vector)"),
REJECT_INVALID, "bad-txns-joinsplit-disabled");
}
if (saplingActive) {
// Reject transactions with valid version but missing overwintered flag
if (tx.nVersion >= SAPLING_MIN_TX_VERSION && !tx.fOverwintered) {