From 1f85fc28bef1eb222bfa776bd42d6257591fcb22 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Thu, 16 Mar 2023 16:21:51 +0100 Subject: [PATCH 1/2] Add utilities for applying blocks - ChainGraph::determine_relevant_changeset - KeychainTracker::determine_relevant_changeset, KeychainTracker::determine_additions, KeychainTracker::apply_update_relevant, - TxGraph::determine_relevant_additions --- crates/chain/src/chain_graph.rs | 37 ++++++++++++++++++++- crates/chain/src/keychain/tracker.rs | 48 +++++++++++++++++++++++++--- crates/chain/src/sparse_chain.rs | 18 +++++++++++ crates/chain/src/tx_graph.rs | 17 ++++++++++ 4 files changed, 114 insertions(+), 6 deletions(-) diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index acf104e79..8c5efdccf 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -1,6 +1,6 @@ //! Module for structures that combine the features of [`sparse_chain`] and [`tx_graph`]. use crate::{ - collections::HashSet, + collections::{BTreeSet, HashSet}, sparse_chain::{self, ChainPosition, SparseChain}, tx_graph::{self, TxGraph}, BlockId, ForEachTxOut, FullTxOut, TxHeight, @@ -291,6 +291,41 @@ where Ok(changeset) } + /// Calculates the difference between self and `update` in the form of a [`ChangeSet`], + /// filtering the irrelevant transactions + pub fn determine_relevant_changeset( + &self, + update: ChainGraph

, + is_relevant: F, + ) -> Result, UpdateError

> + where + F: Fn(&Transaction) -> bool, + { + let graph_additions = self + .graph + .determine_relevant_additions(update.graph, is_relevant); + + let relevant_txids = graph_additions + .clone() + .tx + .into_iter() + .map(|t| t.txid()) + .collect::>(); + + let chain_changeset = self + .chain + .determine_relevant_changeset(update.chain, &relevant_txids) + .map_err(UpdateError::Chain)?; + + let mut changeset = ChangeSet { + chain: chain_changeset, + graph: graph_additions, + }; + + self.fix_conflicts(&mut changeset)?; + Ok(changeset) + } + /// Calculates the difference between self and `update` in the form of a [`ChangeSet`]. pub fn determine_changeset( &self, diff --git a/crates/chain/src/keychain/tracker.rs b/crates/chain/src/keychain/tracker.rs index fff5ee2b4..9440d3588 100644 --- a/crates/chain/src/keychain/tracker.rs +++ b/crates/chain/src/keychain/tracker.rs @@ -66,7 +66,33 @@ where &self, scan: &KeychainScan, ) -> Result, chain_graph::UpdateError

> { - // TODO: `KeychainTxOutIndex::determine_additions` + Ok(KeychainChangeSet { + derivation_indices: self.determine_additions(scan), + chain_graph: self.chain_graph.determine_changeset(&scan.update)?, + }) + } + + /// Determines the resultant [`KeychainChangeSet`] if the relevant part of a [`KeychainScan`] is applied. + /// + /// Internally, we call [`ChainGraph::determine_relevant_changeset`] and also determine the additions of + /// [`KeychainTxOutIndex`]. + pub fn determine_relevant_changeset( + &self, + scan: KeychainScan, + ) -> Result, chain_graph::UpdateError

> { + Ok(KeychainChangeSet { + derivation_indices: self.determine_additions(&scan), + chain_graph: self + .chain_graph + .determine_relevant_changeset(scan.update, |tx| self.txout_index.is_relevant(tx))?, + }) + } + + /// Determines the changes ([`DerivationAdditions`]) in the local keychain + /// derivation if the given [`KeychainScan`] is applied + // Note: we could accept `last_active_indices: BTreeMap` here, but we're accepting a whole + // KeychainScan for clarity + pub fn determine_additions(&self, scan: &KeychainScan) -> DerivationAdditions { let mut derivation_indices = scan.last_active_indices.clone(); derivation_indices.retain(|keychain, index| { match self.txout_index.last_revealed_index(keychain) { @@ -75,10 +101,7 @@ where } }); - Ok(KeychainChangeSet { - derivation_indices: DerivationAdditions(derivation_indices), - chain_graph: self.chain_graph.determine_changeset(&scan.update)?, - }) + DerivationAdditions(derivation_indices) } /// Directly applies a [`KeychainScan`] on [`KeychainTracker`]. @@ -96,6 +119,21 @@ where Ok(changeset) } + /// Directly applies the relevant part of a [`KeychainScan`] on [`KeychainTracker`]. + /// + /// This is equivilant to calling [`determine_relevant_changeset`] and [`apply_changeset`] in sequence. + /// + /// [`determine_relevant_changeset`]: Self::determine_relevant_changeset + /// [`apply_changeset`]: Self::apply_changeset + pub fn apply_update_relevant( + &mut self, + scan: KeychainScan, + ) -> Result, chain_graph::UpdateError

> { + let changeset = self.determine_relevant_changeset(scan)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + /// Applies the changes in `changeset` to [`KeychainTracker`]. /// /// Internally, this calls [`KeychainTxOutIndex::apply_additions`] and diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index b9c1e24ba..24c608c6b 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -509,6 +509,24 @@ impl SparseChain

{ .map(|(&height, &hash)| BlockId { height, hash }) } + /// Preview changes of updating [`Self`] with another chain that connects to it. + /// All the non-relevant transactions will be excluded. + /// + /// Refer to `determine_changeset` for more. + pub fn determine_relevant_changeset( + &self, + mut update: Self, + relevant_txids: &BTreeSet, + ) -> Result, UpdateError

> { + update + .ordered_txids + .retain(|(_, txid)| relevant_txids.contains(txid)); + update + .txid_to_pos + .retain(|txid, _| relevant_txids.contains(txid)); + self.determine_changeset(&update) + } + /// Preview changes of updating [`Self`] with another chain that connects to it. /// /// If the `update` wishes to introduce confirmed transactions, it must contain a checkpoint diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 3326ac4a2..96e12a1e6 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -289,6 +289,23 @@ impl TxGraph { additions } + /// Previews the resultant relevant [`Additions`] when [`Self`] is updated against the `update` graph. + /// + /// The [`Additions`] would be the set difference of the relevant `update` and `self` (transactions that + /// exist in `update` and are relevant, but not in `self`). + pub fn determine_relevant_additions(&self, mut update: TxGraph, is_relevant: F) -> Additions + where + F: Fn(&Transaction) -> bool, + { + // TODO: this always considers partial txs as relevant + update.txs.retain(|_, tx_node| match tx_node { + TxNode::Whole(tx) => is_relevant(tx), + _ => true, + }); + + self.determine_additions(&update) + } + /// Returns the resultant [`Additions`] if the given transaction is inserted. Does not actually /// mutate [`Self`]. /// From f8043bcd8c30da6c1a495ef0c16275613c10d786 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Thu, 16 Mar 2023 16:22:10 +0100 Subject: [PATCH 2/2] Test determine_relevant_changeset --- crates/chain/tests/test_chain_graph.rs | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/crates/chain/tests/test_chain_graph.rs b/crates/chain/tests/test_chain_graph.rs index 68f50b8f7..cffc4fb31 100644 --- a/crates/chain/tests/test_chain_graph.rs +++ b/crates/chain/tests/test_chain_graph.rs @@ -12,6 +12,10 @@ use bitcoin::{OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxO #[test] fn test_spent_by() { + // Creating three transactions: + // tx1 + // tx2: spends from `op` (tx1.txid():0) + // tx3: spends from `op` (tx1.txid():0) let tx1 = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -43,6 +47,9 @@ fn test_spent_by() { output: vec![], }; + // We create two checkpoints: + // - cg1, containing tx1 and tx2 + // - cg2, containing tx1 and tx3 let mut cg1 = ChainGraph::default(); let _ = cg1 .insert_tx(tx1, TxHeight::Unconfirmed) @@ -55,6 +62,7 @@ fn test_spent_by() { .insert_tx(tx3.clone(), TxHeight::Unconfirmed) .expect("should insert"); + // We query cg1 and cg2 to get the tx spending `op` assert_eq!(cg1.spent_by(op), Some((&TxHeight::Unconfirmed, tx2.txid()))); assert_eq!(cg2.spent_by(op), Some((&TxHeight::Unconfirmed, tx3.txid()))); } @@ -651,3 +659,56 @@ fn test_evict_descendants() { .expect_err("must fail due to conflicts"); assert!(matches!(err, InsertTxError::UnresolvableConflict(_))); } + +#[test] +fn test_chain_graph_relevant_changeset() { + 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 cg1 = ChainGraph::default(); + + // cg2 has two txs, but only tx_a is relevant + let cg2 = { + let mut cg = ChainGraph::default(); + let _ = cg + .insert_tx(tx_a.clone(), TxHeight::Unconfirmed) + .expect("should insert tx"); + let _ = cg + .insert_tx(tx_b, TxHeight::Unconfirmed) + .expect("should insert tx"); + cg + }; + + let changeset = ChangeSet:: { + chain: sparse_chain::ChangeSet { + checkpoints: Default::default(), + txids: [(tx_a.txid(), Some(TxHeight::Unconfirmed))].into(), + }, + graph: tx_graph::Additions { + tx: [tx_a.clone()].into(), + txout: [].into(), + }, + }; + + assert_eq!( + cg1.determine_relevant_changeset(cg2, |tx: &Transaction| { tx.txid() == tx_a.txid() }), + Ok(changeset), + "tx_b shouldn't be in the txgraph", + ); +}