diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml
index e4e788f067..2ea618c4ae 100644
--- a/.github/workflows/cont_integration.yml
+++ b/.github/workflows/cont_integration.yml
@@ -238,7 +238,7 @@ jobs:
check-wasm:
needs: prepare
name: Check WASM
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
env:
CC: clang-14
CFLAGS: -I/usr/include
diff --git a/ci/pin-msrv.sh b/ci/pin-msrv.sh
index 703558e419..265ccbc526 100755
--- a/ci/pin-msrv.sh
+++ b/ci/pin-msrv.sh
@@ -14,4 +14,3 @@ cargo update -p idna_adapter --precise "1.2.1"
cargo update -p icu_normalizer --precise "2.1.1"
cargo update -p icu_provider --precise "2.1.1"
cargo update -p icu_locale_core --precise "2.1.1"
-cargo update -p yoke-derive --precise "0.8.2"
diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs
index 3c6ab69e59..570db24595 100644
--- a/crates/chain/src/tx_graph.rs
+++ b/crates/chain/src/tx_graph.rs
@@ -567,10 +567,12 @@ impl TxGraph {
&'g self,
tx: &'g Transaction,
) -> impl Iterator- + 'g {
+ let is_coinbase = tx.is_coinbase();
let txid = tx.compute_txid();
tx.input
.iter()
.enumerate()
+ .filter(move |_| !is_coinbase)
.filter_map(move |(vin, txin)| self.spends.get(&txin.previous_output).zip(Some(vin)))
.flat_map(|(spends, vin)| core::iter::repeat(vin).zip(spends.iter().cloned()))
.filter(move |(_, conflicting_txid)| *conflicting_txid != txid)
@@ -721,15 +723,14 @@ impl TxGraph {
}
}
partial_tx => {
- for txin in &tx.input {
- // this means the tx is coinbase so there is no previous output
- if txin.previous_output.is_null() {
- continue;
+ // Coinbase transactions have no previous outputs to spend.
+ if !tx.is_coinbase() {
+ for txin in &tx.input {
+ self.spends
+ .entry(txin.previous_output)
+ .or_default()
+ .insert(txid);
}
- self.spends
- .entry(txin.previous_output)
- .or_default()
- .insert(txid);
}
*partial_tx = TxNodeInternal::Whole(tx.clone());
changeset.txs.insert(tx);
diff --git a/crates/chain/tests/test_null_outpoint.rs b/crates/chain/tests/test_null_outpoint.rs
new file mode 100644
index 0000000000..6bb3cebfdc
--- /dev/null
+++ b/crates/chain/tests/test_null_outpoint.rs
@@ -0,0 +1,155 @@
+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::::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}");
+}
+
+#[test]
+fn test_null_prevout_direct_conflicts() {
+ let mut graph = TxGraph::::default();
+
+ let tx1 = Transaction {
+ version: transaction::Version::TWO,
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![
+ TxIn {
+ previous_output: OutPoint::null(),
+ ..Default::default()
+ },
+ TxIn {
+ previous_output: OutPoint::new(Txid::from_byte_array([1; 32]), 0),
+ ..Default::default()
+ },
+ ],
+ output: vec![TxOut {
+ value: Amount::from_sat(1),
+ script_pubkey: ScriptBuf::new(),
+ }],
+ };
+ let tx2 = Transaction {
+ version: transaction::Version::TWO,
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![
+ TxIn {
+ previous_output: OutPoint::null(),
+ ..Default::default()
+ },
+ TxIn {
+ previous_output: OutPoint::new(Txid::from_byte_array([2; 32]), 0),
+ ..Default::default()
+ },
+ ],
+ output: vec![TxOut {
+ value: Amount::from_sat(1),
+ script_pubkey: ScriptBuf::new(),
+ }],
+ };
+
+ assert!(!tx1.is_coinbase());
+ assert!(!tx2.is_coinbase());
+
+ let txid1 = tx1.compute_txid();
+ let txid2 = tx2.compute_txid();
+
+ let _ = graph.insert_tx(tx1.clone());
+ let _ = graph.insert_tx(tx2.clone());
+
+ // Both non-coinbase transactions spend OutPoint::null(), so they must conflict with each other
+ let conflicts1: Vec<_> = graph.direct_conflicts(&tx1).collect();
+ let conflicts2: Vec<_> = graph.direct_conflicts(&tx2).collect();
+
+ assert_eq!(conflicts1, vec![(0, txid2)]);
+ assert_eq!(conflicts2, vec![(0, txid1)]);
+ assert_eq!(graph.outspends(OutPoint::null()).len(), 2);
+}
+
+#[test]
+fn test_coinbase_isolated_from_null_prevout_conflicts() {
+ let mut graph = TxGraph::::default();
+
+ // Valid coinbase transaction
+ let coinbase = Transaction {
+ version: transaction::Version::TWO,
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![TxIn {
+ previous_output: OutPoint::null(),
+ ..Default::default()
+ }],
+ output: vec![TxOut {
+ value: Amount::from_sat(50_000),
+ script_pubkey: ScriptBuf::new(),
+ }],
+ };
+ assert!(coinbase.is_coinbase());
+ let cb_txid = coinbase.compute_txid();
+ let _ = graph.insert_tx(coinbase.clone());
+
+ // Coinbase does not count as spending OutPoint::null()
+ assert!(graph.outspends(OutPoint::null()).is_empty());
+ assert_eq!(graph.direct_conflicts(&coinbase).count(), 0);
+
+ // Malformed non-coinbase transaction spending OutPoint::null()
+ let malformed = Transaction {
+ version: transaction::Version::TWO,
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![
+ TxIn {
+ previous_output: OutPoint::null(),
+ ..Default::default()
+ },
+ TxIn {
+ previous_output: OutPoint::new(Txid::from_byte_array([3; 32]), 0),
+ ..Default::default()
+ },
+ ],
+ output: vec![TxOut {
+ value: Amount::from_sat(1),
+ script_pubkey: ScriptBuf::new(),
+ }],
+ };
+ assert!(!malformed.is_coinbase());
+ let malformed_txid = malformed.compute_txid();
+ let _ = graph.insert_tx(malformed.clone());
+
+ // The coinbase still has no conflicts and is not considered in conflict with the malformed tx
+ assert_eq!(graph.direct_conflicts(&coinbase).count(), 0);
+ // The malformed tx does not conflict with the coinbase either
+ assert_eq!(graph.direct_conflicts(&malformed).count(), 0);
+ // Only the malformed tx is listed under outspends of null outpoint
+ let null_outspends = graph.outspends(OutPoint::null());
+ assert_eq!(null_outspends.len(), 1);
+ assert!(null_outspends.contains(&malformed_txid));
+ assert!(!null_outspends.contains(&cb_txid));
+}