Skip to content

Commit

Permalink
feat!: introduce tx_graph::Update
Browse files Browse the repository at this point in the history
Instead of updating a `TxGraph` with a `TxGraph`, we introduce a
dedicated data object (`tx_graph::Update`). This brings us closer to
completing bitcoindevkit#1543.
  • Loading branch information
evanlinjin committed Aug 23, 2024
1 parent 71a3e0e commit 5321527
Show file tree
Hide file tree
Showing 20 changed files with 439 additions and 342 deletions.
11 changes: 4 additions & 7 deletions crates/chain/src/indexed_tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,10 @@ where
/// Apply an `update` directly.
///
/// `update` is a [`TxGraph<A>`] and the resultant changes is returned as [`ChangeSet`].
pub fn apply_update(&mut self, update: TxGraph<A>) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.apply_update(update);
let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet {
tx_graph: graph,
indexer,
}
pub fn apply_update(&mut self, update: tx_graph::Update<A>) -> ChangeSet<A, I::ChangeSet> {
let tx_graph = self.graph.apply_update(update);
let indexer = self.index_tx_graph_changeset(&tx_graph);
ChangeSet { tx_graph, indexer }
}

/// Insert a floating `txout` of given `outpoint`.
Expand Down
10 changes: 5 additions & 5 deletions crates/chain/src/spk_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{
alloc::{boxed::Box, collections::VecDeque, vec::Vec},
collections::BTreeMap,
local_chain::CheckPoint,
ConfirmationBlockTime, Indexed, TxGraph,
ConfirmationBlockTime, Indexed,
};
use bitcoin::{OutPoint, Script, ScriptBuf, Txid};

Expand Down Expand Up @@ -345,8 +345,8 @@ impl<I> SyncRequest<I> {
#[must_use]
#[derive(Debug)]
pub struct SyncResult<A = ConfirmationBlockTime> {
/// The update to apply to the receiving [`TxGraph`].
pub graph_update: TxGraph<A>,
/// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph).
pub graph_update: crate::tx_graph::Update<A>,
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
pub chain_update: Option<CheckPoint>,
}
Expand Down Expand Up @@ -497,8 +497,8 @@ impl<K: Ord + Clone> FullScanRequest<K> {
#[derive(Debug)]
pub struct FullScanResult<K, A = ConfirmationBlockTime> {
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
pub graph_update: TxGraph<A>,
/// The update to apply to the receiving [`TxGraph`].
pub graph_update: crate::tx_graph::Update<A>,
/// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph).
pub chain_update: Option<CheckPoint>,
/// Last active indices for the corresponding keychains (`K`).
pub last_active_indices: BTreeMap<K, u32>,
Expand Down
99 changes: 88 additions & 11 deletions crates/chain/src/tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,17 @@
//!
//! ```
//! # use bdk_chain::{Merge, BlockId};
//! # use bdk_chain::tx_graph::TxGraph;
//! # use bdk_chain::tx_graph::{self, TxGraph};
//! # use bdk_chain::example_utils::*;
//! # use bitcoin::Transaction;
//! # use std::sync::Arc;
//! # let tx_a = tx_from_hex(RAW_TX_1);
//! # let tx_b = tx_from_hex(RAW_TX_2);
//! let mut graph: TxGraph = TxGraph::default();
//! let update = TxGraph::new(vec![tx_a, tx_b]);
//!
//! let mut update = tx_graph::Update::default();
//! update.txs.push(Arc::new(tx_a));
//! update.txs.push(Arc::new(tx_b));
//!
//! // apply the update graph
//! let changeset = graph.apply_update(update.clone());
Expand All @@ -101,6 +105,79 @@ use core::{
ops::{Deref, RangeInclusive},
};

/// Data object used to update the [`TxGraph`] with.
#[derive(Debug, Clone)]
pub struct Update<A = ()> {
/// Full transactions.
pub txs: Vec<Arc<Transaction>>,
/// Floating txouts.
pub txouts: BTreeMap<OutPoint, TxOut>,
/// Transaction anchors.
pub anchors: BTreeSet<(A, Txid)>,
/// Seen at times for transactions.
pub seen_ats: HashMap<Txid, u64>,
}

impl<A> Default for Update<A> {
fn default() -> Self {
Self {
txs: Default::default(),
txouts: Default::default(),
anchors: Default::default(),
seen_ats: Default::default(),
}
}
}

impl<A> From<TxGraph<A>> for Update<A> {
fn from(graph: TxGraph<A>) -> Self {
Self {
txs: graph.full_txs().map(|tx_node| tx_node.tx).collect(),
txouts: graph
.floating_txouts()
.map(|(op, txo)| (op, txo.clone()))
.collect(),
anchors: graph.anchors,
seen_ats: graph.last_seen.into_iter().collect(),
}
}
}

impl<A: Ord + Clone> From<Update<A>> for TxGraph<A> {
fn from(update: Update<A>) -> Self {
let mut graph = TxGraph::<A>::default();
let _ = graph.apply_update(update);
graph
}
}

impl<A: Ord> Update<A> {
/// Update the [`seen_ats`](Self::seen_ats) for all unanchored transactions.
pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) {
let seen_ats = &mut self.seen_ats;
let anchors = &self.anchors;
let unanchored_txids = self.txs.iter().map(|tx| tx.compute_txid()).filter(|txid| {
for (_, anchor_txid) in anchors {
if txid == anchor_txid {
return false;
}
}
true
});
for txid in unanchored_txids {
seen_ats.insert(txid, seen_at);
}
}

/// Extend this update with `other`.
pub fn extend(&mut self, other: Update<A>) {
self.txs.extend(other.txs);
self.txouts.extend(other.txouts);
self.anchors.extend(other.anchors);
self.seen_ats.extend(other.seen_ats);
}
}

/// A graph of transactions and spends.
///
/// See the [module-level documentation] for more.
Expand Down Expand Up @@ -690,19 +767,19 @@ impl<A: Clone + Ord> TxGraph<A> {
///
/// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that
/// exist in `update` but not in `self`).
pub fn apply_update(&mut self, update: TxGraph<A>) -> ChangeSet<A> {
pub fn apply_update(&mut self, update: Update<A>) -> ChangeSet<A> {
let mut changeset = ChangeSet::<A>::default();
for tx_node in update.full_txs() {
changeset.merge(self.insert_tx(tx_node.tx));
for tx in update.txs {
changeset.merge(self.insert_tx(tx));
}
for (outpoint, txout) in update.floating_txouts() {
changeset.merge(self.insert_txout(outpoint, txout.clone()));
for (outpoint, txout) in update.txouts {
changeset.merge(self.insert_txout(outpoint, txout));
}
for (anchor, txid) in &update.anchors {
changeset.merge(self.insert_anchor(*txid, anchor.clone()));
for (anchor, txid) in update.anchors {
changeset.merge(self.insert_anchor(txid, anchor));
}
for (&txid, &last_seen) in &update.last_seen {
changeset.merge(self.insert_seen_at(txid, last_seen));
for (txid, seen_at) in update.seen_ats {
changeset.merge(self.insert_seen_at(txid, seen_at));
}
changeset
}
Expand Down
88 changes: 22 additions & 66 deletions crates/chain/tests/test_tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

#[macro_use]
mod common;
use bdk_chain::tx_graph::CalculateFeeError;
use bdk_chain::tx_graph::{self, CalculateFeeError};
use bdk_chain::{
collections::*,
local_chain::LocalChain,
Expand Down Expand Up @@ -49,7 +49,7 @@ fn insert_txouts() {
)];

// One full transaction to be included in the update
let update_txs = Transaction {
let update_tx = Transaction {
version: transaction::Version::ONE,
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn {
Expand All @@ -63,17 +63,17 @@ fn insert_txouts() {
};

// Conf anchor used to mark the full transaction as confirmed.
let conf_anchor = ChainPosition::Confirmed(BlockId {
let conf_anchor = BlockId {
height: 100,
hash: h!("random blockhash"),
});
};

// Unconfirmed anchor to mark the partial transactions as unconfirmed
let unconf_anchor = ChainPosition::<BlockId>::Unconfirmed(1000000);
// Unconfirmed seen_at timestamp to mark the partial transactions as unconfirmed.
let unconf_seen_at = 1000000_u64;

// Make the original graph
let mut graph = {
let mut graph = TxGraph::<ChainPosition<BlockId>>::default();
let mut graph = TxGraph::<BlockId>::default();
for (outpoint, txout) in &original_ops {
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
Expand All @@ -88,57 +88,21 @@ fn insert_txouts() {

// Make the update graph
let update = {
let mut graph = TxGraph::default();
let mut update = tx_graph::Update::default();
for (outpoint, txout) in &update_ops {
// Insert partials transactions
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
ChangeSet {
txouts: [(*outpoint, txout.clone())].into(),
..Default::default()
}
);
// Insert partials transactions.
update.txouts.insert(*outpoint, txout.clone());
// Mark them unconfirmed.
assert_eq!(
graph.insert_anchor(outpoint.txid, unconf_anchor),
ChangeSet {
txs: [].into(),
txouts: [].into(),
anchors: [(unconf_anchor, outpoint.txid)].into(),
last_seen: [].into()
}
);
// Mark them last seen at.
assert_eq!(
graph.insert_seen_at(outpoint.txid, 1000000),
ChangeSet {
txs: [].into(),
txouts: [].into(),
anchors: [].into(),
last_seen: [(outpoint.txid, 1000000)].into()
}
);
update.seen_ats.insert(outpoint.txid, unconf_seen_at);
}
// Insert the full transaction
assert_eq!(
graph.insert_tx(update_txs.clone()),
ChangeSet {
txs: [Arc::new(update_txs.clone())].into(),
..Default::default()
}
);

// Insert the full transaction.
update.txs.push(update_tx.clone().into());
// Mark it as confirmed.
assert_eq!(
graph.insert_anchor(update_txs.compute_txid(), conf_anchor),
ChangeSet {
txs: [].into(),
txouts: [].into(),
anchors: [(conf_anchor, update_txs.compute_txid())].into(),
last_seen: [].into()
}
);
graph
update
.anchors
.insert((conf_anchor, update_tx.compute_txid()));
update
};

// Check the resulting addition.
Expand All @@ -147,13 +111,9 @@ fn insert_txouts() {
assert_eq!(
changeset,
ChangeSet {
txs: [Arc::new(update_txs.clone())].into(),
txs: [Arc::new(update_tx.clone())].into(),
txouts: update_ops.clone().into(),
anchors: [
(conf_anchor, update_txs.compute_txid()),
(unconf_anchor, h!("tx2"))
]
.into(),
anchors: [(conf_anchor, update_tx.compute_txid()),].into(),
last_seen: [(h!("tx2"), 1000000)].into()
}
);
Expand Down Expand Up @@ -188,7 +148,7 @@ fn insert_txouts() {

assert_eq!(
graph
.tx_outputs(update_txs.compute_txid())
.tx_outputs(update_tx.compute_txid())
.expect("should exists"),
[(
0u32,
Expand All @@ -204,13 +164,9 @@ fn insert_txouts() {
assert_eq!(
graph.initial_changeset(),
ChangeSet {
txs: [Arc::new(update_txs.clone())].into(),
txs: [Arc::new(update_tx.clone())].into(),
txouts: update_ops.into_iter().chain(original_ops).collect(),
anchors: [
(conf_anchor, update_txs.compute_txid()),
(unconf_anchor, h!("tx2"))
]
.into(),
anchors: [(conf_anchor, update_tx.compute_txid()),].into(),
last_seen: [(h!("tx2"), 1000000)].into()
}
);
Expand Down
Loading

0 comments on commit 5321527

Please sign in to comment.