Skip to content

fix: chained DD transfers fail — mempool DD input amount resolution (Bug #35)#400

Merged
JaredTate merged 1 commit intoDigiByte-Core:feature/digidollar-v1from
JohnnyLawDGB:fix/dd-mempool-conservation-lookup
Apr 14, 2026
Merged

fix: chained DD transfers fail — mempool DD input amount resolution (Bug #35)#400
JaredTate merged 1 commit intoDigiByte-Core:feature/digidollar-v1from
JohnnyLawDGB:fix/dd-mempool-conservation-lookup

Conversation

@JohnnyLawDGB
Copy link
Copy Markdown

Summary

  • Bug: Back-to-back DD transfers fail with transfer-dd-conservation-violation because the validator can't determine DD amounts from inputs whose creating transaction is still in the mempool
  • Root cause: All three DD amount lookup strategies in validation.cpp:1280-1305 fail for unconfirmed parent txs: txindex only indexes confirmed blocks, block-db lookup can't find mempool txs, and P2TR scripts don't encode DD amounts. The input's DD amount is silently dropped from the conservation sum.
  • Fix: Add mempool transaction lookup as strategy 0 (before txindex). CTxMemPool::get() retrieves the unconfirmed parent tx, then ExtractDDAmountFromTxRef() parses its OP_RETURN — same trustworthy path used for confirmed txs. The mempool pointer is passed via ValidationContext from MemPoolAccept::PreChecks where it's naturally available.

Reproduction

# Send $1 DD — succeeds (confirmed DD input)
digibyte-cli -rpcwallet=oracle senddigidollar "TD1..." 100

# Immediately send $2 DD — FAILS (change UTXO from above is unconfirmed)
digibyte-cli -rpcwallet=oracle senddigidollar "TD1..." 200
# Error: "transfer-dd-conservation-violation, DD not conserved: input=0, output=200"

# Wait for 1 block, retry — succeeds (now txindex/block-db can find it)

Changes

File Change
src/digidollar/validation.h Add const CTxMemPool* mempool to ValidationContext, forward-declare CTxMemPool
src/digidollar/validation.cpp Add ExtractDDAmountFromMempool() helper; insert mempool lookup as strategy 0 in both transfer conservation check and redemption DD input extraction
src/validation.cpp Pass &m_pool when constructing ValidationContext in MemPoolAccept::PreChecks

Safety

  • Mempool txs have already passed full validation (including their own conservation check) before acceptance, so their OP_RETURN data is trustworthy
  • CTxMemPool::get() takes its own lock (LOCK(cs)) — no external lock ordering concerns
  • Block validation path passes nullptr for mempool (unchanged behavior)
  • The mempool lookup is tried first as an optimization — confirmed txs won't be in the mempool, so it returns false quickly and falls through to txindex/block-db

Test plan

  • Chain two DD transfers back-to-back without waiting for a block — second transfer should succeed
  • Chain 5+ transfers rapidly — all should succeed
  • Confirmed DD transfers still work (no regression)
  • Block validation of DD transactions unchanged (mempool=nullptr)
  • Redemption of DD from unconfirmed transfer input works

🤖 Generated with Claude Code

…yte-Core#35)

The DD conservation validator could not determine input DD amounts when
the creating transaction was still in the mempool (unconfirmed). All
three existing lookup strategies fail for mempool txs:
  1. txindex — only indexes confirmed blocks
  2. block-db — tx not in any block yet
  3. scriptPubKey metadata — P2TR scripts don't encode DD amounts

This caused back-to-back DD transfers to fail with
"transfer-dd-conservation-violation" because the second transfer's
input (the first transfer's change output) had its DD amount silently
dropped from the conservation sum.

Add mempool transaction lookup as strategy 0 (before txindex). Mempool
txs have already passed full validation so their OP_RETURN is
trustworthy. Pass the mempool pointer via ValidationContext from
MemPoolAccept::PreChecks, where it is naturally available.

Applied to both the transfer conservation check and the redemption DD
input extraction paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@DigiSwarm
Copy link
Copy Markdown

Thanks @JohnnyLawDGB — nice clean fix.

Reviewed the diff and traced the code paths. The bug is confirmed real: all three DD amount lookup strategies (ExtractDDAmountFromPrevTx via txindex, ExtractDDAmountFromBlockDb, and the metadata registry path) fail for unconfirmed parent transactions, so chaining DD transfers without waiting for a block hits dd-input-amounts-unknown.

The fix is correct and well-scoped:

  • ExtractDDAmountFromMempool() delegates to the same ExtractDDAmountFromTxRef() used by the other strategies — no code duplication, same security checks (coinbase rejection, DD marker verification, type-aware OP_RETURN parsing)
  • CTxMemPool::get() takes its own LOCK(cs) internally — no lock ordering concerns
  • Mempool txs have already passed full validation before acceptance, so their OP_RETURN data is trustworthy
  • Block validation passes nullptr for the mempool pointer — zero impact on consensus
  • The ValidationContext default of nullptr keeps all existing callers backward-compatible

CI failures are unrelated (pre-existing MuSig2 test compilation issue in musig2_session_tests.cpp).

LGTM — approved for merge.

@JaredTate JaredTate merged commit 6105963 into DigiByte-Core:feature/digidollar-v1 Apr 14, 2026
0 of 2 checks passed
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.

3 participants