Skip to content

TxGraph doesn't index null-prevout spends of non-coinbase txs, so their conflicts go undetected #2286

Closed as not planned

Description

@noahjoeris

Describe the bug

TxGraph::insert_tx skips every null previous output when indexing spends. A malformed transaction with a null prevout and a second input is not coinbase, but its null input is still skipped. Two such transactions sharing only the null prevout are therefore missed by direct_conflicts, and both remain in CanonicalView after recording their last-seen timestamps.

This requires consensus-invalid transaction data.

This issue was found by AI.

To Reproduce

Add this as crates/chain/tests/test_null_outpoint.rs, then run cargo test -p bdk_chain --test test_null_outpoint:

use bdk_chain::{local_chain::LocalChain, BlockId, TxGraph};
use bitcoin::{absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf,
              Transaction, TxIn, TxOut, Txid};

#[test]
fn null_prevout_conflicts_remain_canonical() {
    let mut graph = TxGraph::<BlockId>::default();
    for tag in [1u8, 2] {
        let tx = Transaction {
            version: transaction::Version::TWO,
            lock_time: absolute::LockTime::ZERO,
            input: [OutPoint::null(), OutPoint::new(Txid::from_byte_array([tag; 32]), 0)]
                .map(|previous_output| TxIn { previous_output, ..Default::default() })
                .to_vec(),
            output: vec![TxOut {
                value: Amount::from_sat(1),
                script_pubkey: ScriptBuf::new(),
            }],
        };
        assert!(!tx.is_coinbase());
        let _ = graph.insert_seen_at(tx.compute_txid(), u64::from(tag));
        let _ = graph.insert_tx(tx);
    }
    let chain = LocalChain::from_blocks(
        [(0, BlockHash::all_zeros())].into_iter().collect(),
    ).unwrap();
    let view = chain.canonical_view(&graph, chain.tip().block_id(), Default::default());
    let count = view.txs().len();
    assert!(count <= 1, "canonical spenders of null outpoint: {count}");
}

The assertion fails with canonical spenders of null outpoint: 2.

Expected behavior

Canonicalization should remain conflict-free for accepted non-coinbase transactions, including this malformed-input case.

Activity

  1. added a commit that references this issue on Oct 23, 2026
    30017ce
  2. moved this from Todo to In Progress in BDK Chainon Oct 23, 2026
  3. changed the title [-]Canonical view admits conflicting transactions with null prevouts[/-] [+]TxGraph doesn't index null-prevout spends of non-coinbase txs, so their conflicts go undetected[/+] on Oct 26, 2026
  4. evanlinjin commented on Oct 26, 2026

    @evanlinjin
    Member

    Closing as not planned. The repro is accurate: insert_tx skips any null prevout, not just a coinbase's. But it only bites on consensus-invalid transactions (bad-txns-prevout-null). No node relays those and no block contains them, so a wallet only sees one from a broken or hostile chain source, which we treat as trusted (see #2371). Fixing it would add special-casing to TxGraph for data that can't occur on a real network.

    Thanks @noahjoeris for the clear report and repro, and @AlphaSparrow for the fix attempt in #2435 (and the MSRV CI fix that came with it). If you find a way to hit this with valid transactions, please reopen.

  5. moved this from In Progress to Done in BDK Chainon Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions