bitcoindevkit / bitcoindevkit/bdk
`IndexedTxGraph::apply_block` does not check merkle root
- Dominant language
- Rust
- Stars
- 1.1k
- Forks
- 483
- Avg merge
- 20d 3h
- Merged PRs (30d)
- 3
Description
Describe the bug
`IndexedTxGraph::apply_block_relevant` and `IndexedTxGraph::apply_block` anchor transactions without verifying that the block’s `txdata` matches its header Merkle root.
A caller that accepts an unvalidated block body can attach fabricated transactions to a genuine block hash, causing them to appear confirmed.
## To Reproduce
```rust
#[test]
fn apply_block_relevant_confirms_txs_not_in_merkle_root() {
use bdk_chain::{
local_chain::LocalChain, spk_txout::SpkTxOutIndex, BlockId, IndexedTxGraph,
};
use bitcoin::{
absolute, hashes::Hash, transaction, Amount, Network, OutPoint, ScriptBuf,
Transaction, TxIn, TxOut, Txid,
};
let script = ScriptBuf::new();
let mut graph = IndexedTxGraph::>::default();
assert!(graph.index.insert_spk((), script.clone()));
let mut block = bitcoin::constants::genesis_block(Network::Bitcoin);
let genuine_hash = block.block_hash();
block.txdata = vec![Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::new(Txid::from_byte_array([1; 32]), 0),
..Default::default()
}],
output: vec![TxOut {
value: Amount::from_sat(50_000),
script_pubkey: script,
}],
}];
assert_eq!(block.block_hash(), genuine_hash);
assert!(!block.check_merkle_root());
let chain = LocalChain::from_blocks([(0, genuine_hash)].into_iter().collect()).unwrap();
let _ = graph.apply_block_relevant(&block, 0);
let view = chain.canonical_view(
graph.graph(),
chain.tip().block_id(),
Default::default(),
);
let balance = view.balance(
graph.index.outpoints().iter().cloned(),
|_, _| true,
0,
);
assert_eq!(balance.confirmed, Amount::from_sat(50_000));
}
```
## Expected behavior
Document that these methods trust `txdata` matches the header merkle root. Callers must check that before calling. Add `debug_assert!(block.check_merkle_root())` so the fake-body case fails in debug.
**Alternative:** Reject on merkle mismatch before updating graph/index. That would be a breaking change (`ChangeSet` → `Result`).
## Build environment
- BDK tag/commit: acc06e53220960caa89efd5984d7b43914640dd4
## Which backend(s) are relevant (if any)?
- [x] None / not backend-related (e.g. `bdk_chain`, `bdk_core`)
## Is this blocking production use?
- [x] No
## Additional context
The block header remains genuine, so `block.block_hash()` is unchanged; only its transaction list is replaced. `LocalChain` therefore accepts the anchor as canonical even though the transactions were never committed by the block header.
Contributor guide
Research direction
Start at the IndexedTxGraph::apply_block_relevant and IndexedTxGraph::apply_block entry points and run the reproduction test from the issue. Check how each method handles block txdata versus the header merkle root; done means the documented trust requirement is clear and debug builds fail for the fabricated-body case without changing the public error behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- blockchain
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100