Skip to content
This repository has been archived by the owner on Mar 14, 2023. It is now read-only.

Commit

Permalink
Merge pull request #78 from evanlinjin/invalidation_consistency_tests
Browse files Browse the repository at this point in the history
Invalidation consistency tests
  • Loading branch information
evanlinjin authored Dec 7, 2022
2 parents dd8aa3f + e4639fd commit 6d2cdd5
Show file tree
Hide file tree
Showing 4 changed files with 334 additions and 112 deletions.
21 changes: 16 additions & 5 deletions bdk_core/src/chain_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,33 @@ impl<I: ChainIndex> ChainGraph<I> {
&self,
update: &Self,
) -> Result<ChangeSet<I>, sparse_chain::UpdateFailure<I>> {
let mut chain_changeset = self.chain.determine_changeset(&update.chain)?;
let (mut chain_changeset, invalid_from) = self.chain.determine_changeset(&update.chain)?;
let invalid_from: TxHeight = invalid_from.into();

let conflicting_original_txids = update
.chain
.iter_txids()
// skip txids that already exist in the original chain (for efficiency)
.filter(|&(_, txid)| self.chain.tx_index(*txid).is_none())
// skip txids that do not have full txs, as we can't check for conflicts for them
.filter_map(|&(_, txid)| update.graph.tx(txid))
.filter_map(|&(_, txid)| update.graph.tx(txid).or_else(|| self.graph.tx(txid)))
// choose original txs that conflicts with the update
.flat_map(|update_tx| {
self.graph
.conflicting_txids(update_tx)
.map(|(_, txid)| txid)
.filter(|&txid| self.chain.tx_index(txid).is_some())
.filter_map(|(_, txid)| self.chain.tx_index(txid).map(|i| (txid, i)))
});

for txid in conflicting_original_txids {
for (txid, original_index) in conflicting_original_txids {
// if the evicted txid lies before "invalid_from", we screwed up
if original_index.height() < invalid_from {
return Err(sparse_chain::UpdateFailure::<I>::InconsistentTx {
inconsistent_txid: txid,
original_index: original_index.clone(),
update_index: None,
});
}

chain_changeset.txids.insert(txid, None);
}

Expand Down
17 changes: 10 additions & 7 deletions bdk_core/src/sparse_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ pub enum UpdateFailure<I = TxHeight> {
/// connect to the existing chain. This error case contains the checkpoint height to include so
/// that the chains can connect.
NotConnected(u32),
/// The update contains inconsistent tx states (e.g. it changed the transaction's hieght).
/// The update contains inconsistent tx states (e.g. it changed the transaction's height).
/// This error is usually the inconsistency found.
InconsistentTx {
inconsistent_txid: Txid,
original_index: I,
update_index: I,
update_index: Option<I>,
},
}

Expand Down Expand Up @@ -164,7 +164,10 @@ impl<I: ChainIndex> SparseChain<I> {

/// Determine the changeset when `update` is applied to self. Invalidated checkpoints result in
/// invalidated transactions becoming "unconfirmed".
pub fn determine_changeset(&self, update: &Self) -> Result<ChangeSet<I>, UpdateFailure<I>> {
pub fn determine_changeset(
&self,
update: &Self,
) -> Result<(ChangeSet<I>, Option<u32>), UpdateFailure<I>> {
let agreement_point = update
.checkpoints
.iter()
Expand Down Expand Up @@ -203,7 +206,7 @@ impl<I: ChainIndex> SparseChain<I> {
return Err(UpdateFailure::InconsistentTx {
inconsistent_txid: txid,
original_index: I::clone(original_index),
update_index: update_index.clone(),
update_index: Some(update_index.clone()),
});
}
}
Expand All @@ -219,7 +222,7 @@ impl<I: ChainIndex> SparseChain<I> {
.collect(),
// invalidated transactions become unconfirmed
txids: self
.range_txids_by_height(TxHeight::Confirmed(invalid_from)..)
.range_txids_by_height(TxHeight::Confirmed(invalid_from)..TxHeight::Unconfirmed)
.map(|(_, txid)| (*txid, Some(I::max_ord_of_height(TxHeight::Unconfirmed))))
.collect(),
})
Expand Down Expand Up @@ -253,12 +256,12 @@ impl<I: ChainIndex> SparseChain<I> {
}
}

Ok(changeset)
Ok((changeset, invalid_from))
}

/// Tries to update `self` with another chain that connects to it.
pub fn apply_update(&mut self, update: Self) -> Result<(), UpdateFailure<I>> {
let changeset = self.determine_changeset(&update)?;
let (changeset, _) = self.determine_changeset(&update)?;
self.apply_changeset(changeset);
Ok(())
}
Expand Down
115 changes: 113 additions & 2 deletions bdk_core/tests/test_chain_graph.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
use bdk_core::{chain_graph::ChainGraph, TxHeight};
use bitcoin::{OutPoint, PackedLockTime, Transaction, TxIn, TxOut};
#[macro_use]
mod common;

use bdk_core::{
chain_graph::{ChainGraph, ChangeSet},
sparse_chain,
tx_graph::Additions,
BlockId, TxHeight,
};
use bitcoin::{OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness};

#[test]
fn test_spent_by() {
Expand Down Expand Up @@ -47,3 +55,106 @@ fn test_spent_by() {
assert_eq!(cg1.spent_by(op), Some((&TxHeight::Unconfirmed, tx2.txid())));
assert_eq!(cg2.spent_by(op), Some((&TxHeight::Unconfirmed, tx3.txid())));
}

#[test]
fn update_evicts_conflicting_tx() {
let cp_a = BlockId {
height: 0,
hash: h!("A"),
};
let cp_b = BlockId {
height: 1,
hash: h!("B"),
};

let tx_a = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut::default()],
};

let tx_b = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), 0),
script_sig: Script::new(),
sequence: Sequence::default(),
witness: Witness::new(),
}],
output: vec![TxOut::default()],
};

let tx_b2 = Transaction {
version: 0x02,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), 0),
script_sig: Script::new(),
sequence: Sequence::default(),
witness: Witness::new(),
}],
output: vec![TxOut::default(), TxOut::default()],
};

let cg1 = {
let mut cg = ChainGraph::default();
cg.insert_checkpoint(cp_a).expect("should insert cp");
cg.insert_tx(tx_a.clone(), TxHeight::Confirmed(0))
.expect("should insert tx");
cg.insert_tx(tx_b.clone(), TxHeight::Unconfirmed)
.expect("should insert tx");
cg
};
let cg2 = {
let mut cg = ChainGraph::default();
cg.insert_tx(tx_b2.clone(), TxHeight::Unconfirmed)
.expect("should insert tx");
cg
};
assert_eq!(
cg1.determine_changeset(&cg2),
Ok(ChangeSet::<TxHeight> {
chain: sparse_chain::ChangeSet {
checkpoints: Default::default(),
txids: [
(tx_b.txid(), None),
(tx_b2.txid(), Some(TxHeight::Unconfirmed))
]
.into()
},
graph: Additions {
tx: [tx_b2.clone()].into(),
txout: [].into()
},
}),
"tx should be evicted from mempool"
);

let cg1 = {
let mut cg = ChainGraph::default();
cg.insert_checkpoint(cp_a).expect("should insert cp");
cg.insert_checkpoint(cp_b).expect("should insert cp");
cg.insert_tx(tx_a.clone(), TxHeight::Confirmed(0))
.expect("should insert tx");
cg.insert_tx(tx_b.clone(), TxHeight::Confirmed(1))
.expect("should insert tx");
cg
};
let cg2 = {
let mut cg = ChainGraph::default();
cg.insert_tx(tx_b2.clone(), TxHeight::Unconfirmed)
.expect("should insert tx");
cg
};
assert_eq!(
cg1.determine_changeset(&cg2),
Err(sparse_chain::UpdateFailure::InconsistentTx {
inconsistent_txid: tx_b.txid(),
original_index: TxHeight::Confirmed(1),
update_index: None
}),
"fail if tx is evicted from valid block"
);
}
Loading

0 comments on commit 6d2cdd5

Please sign in to comment.