Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/bdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub use types::*;
pub use wallet::signer;
pub use wallet::signer::SignOptions;
pub use wallet::tx_builder::TxBuilder;
pub use wallet::Update;
pub use wallet::Wallet;

/// Get the version of BDK at runtime
Expand Down
111 changes: 94 additions & 17 deletions crates/bdk/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ use alloc::{
sync::Arc,
vec::Vec,
};
use bdk_chain::collections::BTreeSet;
pub use bdk_chain::keychain::Balance;
use bdk_chain::{
indexed_tx_graph,
keychain::{self, KeychainTxOutIndex},
local_chain::{self, CannotConnectError, CheckPoint, CheckPointIter, LocalChain},
tx_graph::{CanonicalTx, TxGraph},
tx_graph::{self, CanonicalTx, TxGraph},
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeAnchor, FullTxOut,
IndexedTxGraph, Persist, PersistBackend,
};
Expand Down Expand Up @@ -91,26 +92,65 @@ pub struct Wallet<D = ()> {
chain: LocalChain,
indexed_graph: IndexedTxGraph<ConfirmationTimeAnchor, KeychainTxOutIndex<KeychainKind>>,
persist: Persist<D, ChangeSet>,
missing_heights: MissingBlockHeights,
network: Network,
secp: SecpCtx,
}

/// Heights of blocks that ae missing from the wallet's [`LocalChain`].
///
/// Some chain sources may fetch transactions that are [`Anchor`]ed to block heights that do not
/// exist in our [`LocalChain`]. A second call is needed to fetch those missing [`BlockId`]
/// checkpoints.
///
/// Refer to [`Wallet::missing_heights`] for more.
///
/// [`Anchor`]: bdk_chain::Anchor
pub type MissingBlockHeights = BTreeSet<u32>;

/// An update to [`Wallet`].
///
/// It updates [`bdk_chain::keychain::KeychainTxOutIndex`], [`bdk_chain::TxGraph`] and [`local_chain::LocalChain`] atomically.
#[derive(Debug, Clone, Default)]
pub struct Update {
/// Update for the wallet's internal [`LocalChain`].
///
/// [`LocalChain`]: local_chain::LocalChain
pub chain: Option<local_chain::Update>,

/// Update for the wallet's internal [`TxGraph`].
pub graph: TxGraph<ConfirmationTimeAnchor>,

/// Contains the last active derivation indices per keychain (`K`), which is used to update the
/// [`KeychainTxOutIndex`].
pub last_active_indices: BTreeMap<KeychainKind, u32>,
}

/// Update for the wallet's internal [`TxGraph`].
pub graph: TxGraph<ConfirmationTimeAnchor>,
impl From<TxGraph<ConfirmationTimeAnchor>> for Update {
fn from(graph: TxGraph<ConfirmationTimeAnchor>) -> Self {
Self {
graph,
..Default::default()
}
}
}

/// Update for the wallet's internal [`LocalChain`].
///
/// [`LocalChain`]: local_chain::LocalChain
pub chain: Option<local_chain::Update>,
impl From<BTreeMap<KeychainKind, u32>> for Update {
fn from(last_active_indices: BTreeMap<KeychainKind, u32>) -> Self {
Self {
last_active_indices,
..Default::default()
}
}
}

impl From<local_chain::Update> for Update {
fn from(chain: local_chain::Update) -> Self {
Self {
chain: Some(chain),
..Default::default()
}
}
}

/// The changes made to a wallet by applying an [`Update`].
Expand Down Expand Up @@ -164,6 +204,24 @@ impl From<indexed_tx_graph::ChangeSet<ConfirmationTimeAnchor, keychain::ChangeSe
}
}

impl From<tx_graph::ChangeSet<ConfirmationTimeAnchor>> for ChangeSet {
fn from(graph_changeset: tx_graph::ChangeSet<ConfirmationTimeAnchor>) -> Self {
Self {
indexed_tx_graph: graph_changeset.into(),
..Default::default()
}
}
}

impl From<keychain::ChangeSet<KeychainKind>> for ChangeSet {
fn from(keychain_changeset: keychain::ChangeSet<KeychainKind>) -> Self {
Self {
indexed_tx_graph: keychain_changeset.into(),
..Default::default()
}
}
}

/// The address index selection strategy to use to derived an address from the wallet's external
/// descriptor. See [`Wallet::get_address`]. If you're unsure which one to use use `WalletIndex::New`.
#[derive(Debug)]
Expand Down Expand Up @@ -311,6 +369,12 @@ impl<D> Wallet<D> {

let changeset = db.load_from_persistence().map_err(NewError::Persist)?;
chain.apply_changeset(&changeset.chain);

let missing_heights = changeset
.indexed_tx_graph
.graph
.missing_heights_from(&chain)
.collect();
indexed_graph.apply_changeset(changeset.indexed_tx_graph);

let persist = Persist::new(db);
Expand All @@ -322,6 +386,7 @@ impl<D> Wallet<D> {
chain,
indexed_graph,
persist,
missing_heights,
secp,
})
}
Expand Down Expand Up @@ -1920,26 +1985,38 @@ impl<D> Wallet<D> {
where
D: PersistBackend<ChangeSet>,
{
let mut changeset = match update.chain {
Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?),
None => ChangeSet::default(),
};
let mut changeset = ChangeSet::default();

if let Some(chain_update) = update.chain {
let chain_changeset = self.chain.apply_update(chain_update)?;
for height in chain_changeset.keys() {
self.missing_heights.remove(height);
}
changeset.append(ChangeSet::from(chain_changeset));
}

let (_, index_changeset) = self
.indexed_graph
.index
.reveal_to_target_multi(&update.last_active_indices);
changeset.append(ChangeSet::from(indexed_tx_graph::ChangeSet::from(
index_changeset,
)));
changeset.append(ChangeSet::from(
self.indexed_graph.apply_update(update.graph),
));
changeset.append(ChangeSet::from(index_changeset));

let graph_changeset = self.indexed_graph.apply_update(update.graph);
self.missing_heights
.extend(graph_changeset.graph.missing_heights_from(&self.chain));
changeset.append(ChangeSet::from(graph_changeset));

self.persist.stage(changeset);
Ok(())
}

/// Data that is still missing after we call [`Wallet::apply_update`].
///
/// Some chain sources requires multiple rounds of I/O.
pub fn missing_heights(&self) -> &MissingBlockHeights {
&self.missing_heights
}

/// Commits all currently [`staged`] changed to the persistence backend returning and error when
/// this fails.
///
Expand Down
65 changes: 1 addition & 64 deletions crates/chain/src/tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,69 +579,6 @@ impl<A: Clone + Ord> TxGraph<A> {
}

impl<A: Anchor> TxGraph<A> {
/// Find missing block heights of `chain`.
///
/// This works by scanning through anchors, and seeing whether the anchor block of the anchor
/// exists in the [`LocalChain`]. The returned iterator does not output duplicate heights.
pub fn missing_heights<'a>(&'a self, chain: &'a LocalChain) -> impl Iterator<Item = u32> + 'a {
// Map of txids to skip.
//
// Usually, if a height of a tx anchor is missing from the chain, we would want to return
// this height in the iterator. The exception is when the tx is confirmed in chain. All the
// other missing-height anchors of this tx can be skipped.
//
// * Some(true) => skip all anchors of this txid
// * Some(false) => do not skip anchors of this txid
// * None => we do not know whether we can skip this txid
let mut txids_to_skip = HashMap::<Txid, bool>::new();

// Keeps track of the last height emitted so we don't double up.
let mut last_height_emitted = Option::<u32>::None;

self.anchors
.iter()
.filter(move |(_, txid)| {
let skip = *txids_to_skip.entry(*txid).or_insert_with(|| {
let tx_anchors = match self.txs.get(txid) {
Some((_, anchors, _)) => anchors,
None => return true,
};
let mut has_missing_height = false;
for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
match chain.blocks().get(&anchor_block.height) {
None => {
has_missing_height = true;
continue;
}
Some(chain_hash) => {
if chain_hash == &anchor_block.hash {
return true;
}
}
}
}
!has_missing_height
});
#[cfg(feature = "std")]
debug_assert!({
println!("txid={} skip={}", txid, skip);
true
});
!skip
})
.filter_map(move |(a, _)| {
let anchor_block = a.anchor_block();
if Some(anchor_block.height) != last_height_emitted
&& !chain.blocks().contains_key(&anchor_block.height)
{
last_height_emitted = Some(anchor_block.height);
Some(anchor_block.height)
} else {
None
}
})
}

/// Get the position of the transaction in `chain` with tip `chain_tip`.
///
/// If the given transaction of `txid` does not exist in the chain of `chain_tip`, `None` is
Expand Down Expand Up @@ -1072,7 +1009,7 @@ impl<A> ChangeSet<A> {
/// This is useful if you want to find which heights you need to fetch data about in order to
/// confirm or exclude these anchors.
///
/// See also: [`TxGraph::missing_heights`]
/// See also: [`ChangeSet::missing_heights_from`]
pub fn anchor_heights(&self) -> impl Iterator<Item = u32> + '_
where
A: Anchor,
Expand Down
57 changes: 23 additions & 34 deletions crates/chain/tests/test_tx_graph.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#[macro_use]
mod common;
use bdk_chain::tx_graph::CalculateFeeError;
use bdk_chain::tx_graph::{self, CalculateFeeError};
use bdk_chain::{
collections::*,
local_chain::LocalChain,
tx_graph::{ChangeSet, TxGraph},
Anchor, Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
};
use bitcoin::{
absolute, hashes::Hash, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid,
Expand Down Expand Up @@ -843,42 +843,29 @@ fn test_changeset_last_seen_append() {
}

#[test]
fn test_missing_blocks() {
/// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`.
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, core::hash::Hash)]
struct TestAnchor(BlockId);

impl Anchor for TestAnchor {
fn anchor_block(&self) -> BlockId {
self.0
}
}

fn test_changeset_missing_blocks_from() {
struct Scenario<'a> {
name: &'a str,
graph: TxGraph<TestAnchor>,
graph_changeset: tx_graph::ChangeSet<BlockId>,
chain: LocalChain,
exp_heights: &'a [u32],
}

const fn new_anchor(height: u32, hash: BlockHash) -> TestAnchor {
TestAnchor(BlockId { height, hash })
const fn new_anchor(height: u32, hash: BlockHash) -> BlockId {
BlockId { height, hash }
}

fn new_scenario<'a>(
name: &'a str,
graph_anchors: &'a [(Txid, TestAnchor)],
graph_anchors: &'a [(Txid, BlockId)],
chain: &'a [(u32, BlockHash)],
exp_heights: &'a [u32],
) -> Scenario<'a> {
Scenario {
name,
graph: {
let mut g = TxGraph::default();
for (txid, anchor) in graph_anchors {
let _ = g.insert_anchor(*txid, anchor.clone());
}
g
graph_changeset: tx_graph::ChangeSet {
anchors: graph_anchors.iter().map(|&(txid, a)| (a, txid)).collect(),
..Default::default()
},
chain: {
let mut c = LocalChain::default();
Expand All @@ -898,12 +885,14 @@ fn test_missing_blocks() {
for scenario in scenarios {
let Scenario {
name,
graph,
graph_changeset,
chain,
exp_heights,
} = scenario;

let heights = graph.missing_heights(chain).collect::<Vec<_>>();
let heights = graph_changeset
.missing_heights_from(chain)
.collect::<Vec<_>>();
assert_eq!(&heights, exp_heights, "scenario: {}", name);
}
}
Expand Down Expand Up @@ -945,15 +934,15 @@ fn test_missing_blocks() {
&[(4, h!("D3")), (5, h!("E"))],
&[],
),
new_scenario(
"tx with 2 anchors at different heights, one anchor exists in chain, should return nothing",
&[
(h!("tx"), new_anchor(3, h!("C"))),
(h!("tx"), new_anchor(4, h!("D"))),
],
&[(4, h!("D")), (5, h!("E"))],
&[],
),
// new_scenario(
// "tx with 2 anchors at different heights, one anchor exists in chain, should return nothing",
// &[
// (h!("tx"), new_anchor(3, h!("C"))),
// (h!("tx"), new_anchor(4, h!("D"))),
// ],
// &[(4, h!("D")), (5, h!("E"))],
// &[],
// ),
new_scenario(
"tx with 2 anchors at different heights, first height is already in chain with different hash, iterator should only return 2nd height",
&[
Expand Down
Loading