Skip to content

Commit d73669e

Browse files
committed
Merge #1034: Implement linked-list LocalChain and update chain-src crates/examples
b206a98 fix: Even more refactoring to code and documentation (志宇) bea8e5a fix: `TxGraph::missing_blocks` logic (志宇) db15e03 fix: improve more docs and more refactoring (志宇) 95312d4 fix: docs and some minor refactoring (志宇) 8bf7a99 Refactor `debug_assertions` checks for `LocalChain` (志宇) 315e7e0 fix: rm duplicate `bdk_tmp_plan` module (志宇) af705da Add exclusion of example cli `*.db` files in `.gitignore` (志宇) eabeb6c Implement linked-list `LocalChain` and update chain-src crates/examples (志宇) Pull request description: Fixes #997 Replaces #1002 ### Description This PR changes the `LocalChain` implementation to have blocks stored as a linked-list. This allows the data-src thread to hold a shared ref to a single checkpoint and have access to the whole history of checkpoints without cloning or keeping a lock on `LocalChain`. The APIs of `bdk::Wallet`, `esplora` and `electrum` are also updated to reflect these changes. Note that the `esplora` crate is rewritten to anchor txs in the confirmation block (using the esplora API's tx status block_hash). This guarantees 100% consistency between anchor blocks and their transactions (instead of anchoring txs to the latest tip). `ExploraExt` now has separate methods for updating the `TxGraph` and `LocalChain`. A new method `TxGraph::missing_blocks` is introduced for finding "floating anchors" of a `TxGraph` update (given a chain). Additional changes: * `test_local_chain.rs` is refactored to make test cases easier to write. Additional tests are also added. * Examples are updated. * Exclude example-cli `*.db` files in `.gitignore`. * Rm duplicate `bdk_tmp_plan` module. ### Notes to the reviewers This is the smallest possible division of #1002 without resulting in PRs that do not compile. Since we have changed the API of `LocalChain`, we also need to change `esplora`, `electrum` crates and examples alongside `bdk::Wallet`. ### Changelog notice * Implement linked-list `LocalChain`. This allows the data-src thread to hold a shared ref to a single checkpoint and have access to the whole history of checkpoints without cloning or keeping a lock on `LocalChain`. * Rewrote `esplora` chain-src crate to anchor txs to their confirmation blocks (using esplora API's tx-status `block_hash`). ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: LLFourn: ACK b206a98 Tree-SHA512: a513eecb4f1aae6a5c06a69854e4492961424312a75a42d74377d363b364e3d52415bc81b4aa3fbc3f369ded19bddd07ab895130ebba288e8a43e9d6186e9fcc
2 parents 9330567 + b206a98 commit d73669e

File tree

26 files changed

+1704
-1965
lines changed

26 files changed

+1704
-1965
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ Cargo.lock
44

55
*.swp
66
.idea
7+
8+
# Example persisted files.
9+
*.db

crates/bdk/src/wallet/mod.rs

+31-32
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub use bdk_chain::keychain::Balance;
2323
use bdk_chain::{
2424
indexed_tx_graph::IndexedAdditions,
2525
keychain::{KeychainTxOutIndex, LocalChangeSet, LocalUpdate},
26-
local_chain::{self, LocalChain, UpdateNotConnectedError},
26+
local_chain::{self, CannotConnectError, CheckPoint, CheckPointIter, LocalChain},
2727
tx_graph::{CanonicalTx, TxGraph},
2828
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeAnchor, FullTxOut,
2929
IndexedTxGraph, Persist, PersistBackend,
@@ -32,8 +32,8 @@ use bitcoin::consensus::encode::serialize;
3232
use bitcoin::secp256k1::Secp256k1;
3333
use bitcoin::util::psbt;
3434
use bitcoin::{
35-
Address, BlockHash, EcdsaSighashType, LockTime, Network, OutPoint, SchnorrSighashType, Script,
36-
Sequence, Transaction, TxOut, Txid, Witness,
35+
Address, EcdsaSighashType, LockTime, Network, OutPoint, SchnorrSighashType, Script, Sequence,
36+
Transaction, TxOut, Txid, Witness,
3737
};
3838
use core::fmt;
3939
use core::ops::Deref;
@@ -245,7 +245,7 @@ impl<D> Wallet<D> {
245245
};
246246

247247
let changeset = db.load_from_persistence().map_err(NewError::Persist)?;
248-
chain.apply_changeset(changeset.chain_changeset);
248+
chain.apply_changeset(&changeset.chain_changeset);
249249
indexed_graph.apply_additions(changeset.indexed_additions);
250250

251251
let persist = Persist::new(db);
@@ -370,19 +370,19 @@ impl<D> Wallet<D> {
370370
.graph()
371371
.filter_chain_unspents(
372372
&self.chain,
373-
self.chain.tip().unwrap_or_default(),
373+
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
374374
self.indexed_graph.index.outpoints().iter().cloned(),
375375
)
376376
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
377377
}
378378

379379
/// Get all the checkpoints the wallet is currently storing indexed by height.
380-
pub fn checkpoints(&self) -> &BTreeMap<u32, BlockHash> {
381-
self.chain.blocks()
380+
pub fn checkpoints(&self) -> CheckPointIter {
381+
self.chain.iter_checkpoints()
382382
}
383383

384384
/// Returns the latest checkpoint.
385-
pub fn latest_checkpoint(&self) -> Option<BlockId> {
385+
pub fn latest_checkpoint(&self) -> Option<CheckPoint> {
386386
self.chain.tip()
387387
}
388388

@@ -420,7 +420,7 @@ impl<D> Wallet<D> {
420420
.graph()
421421
.filter_chain_unspents(
422422
&self.chain,
423-
self.chain.tip().unwrap_or_default(),
423+
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
424424
core::iter::once((spk_i, op)),
425425
)
426426
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
@@ -437,7 +437,7 @@ impl<D> Wallet<D> {
437437
let canonical_tx = CanonicalTx {
438438
observed_as: graph.get_chain_position(
439439
&self.chain,
440-
self.chain.tip().unwrap_or_default(),
440+
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
441441
txid,
442442
)?,
443443
node: graph.get_tx_node(txid)?,
@@ -460,7 +460,7 @@ impl<D> Wallet<D> {
460460
pub fn insert_checkpoint(
461461
&mut self,
462462
block_id: BlockId,
463-
) -> Result<bool, local_chain::InsertBlockNotMatchingError>
463+
) -> Result<bool, local_chain::InsertBlockError>
464464
where
465465
D: PersistBackend<ChangeSet>,
466466
{
@@ -504,13 +504,13 @@ impl<D> Wallet<D> {
504504
.range(height..)
505505
.next()
506506
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
507-
tip_height: self.chain.tip().map(|b| b.height),
507+
tip_height: self.chain.tip().map(|b| b.height()),
508508
tx_height: height,
509509
})
510-
.map(|(&anchor_height, &anchor_hash)| ConfirmationTimeAnchor {
510+
.map(|(&anchor_height, &hash)| ConfirmationTimeAnchor {
511511
anchor_block: BlockId {
512512
height: anchor_height,
513-
hash: anchor_hash,
513+
hash,
514514
},
515515
confirmation_height: height,
516516
confirmation_time: time,
@@ -531,17 +531,18 @@ impl<D> Wallet<D> {
531531
pub fn transactions(
532532
&self,
533533
) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeAnchor>> + '_ {
534-
self.indexed_graph
535-
.graph()
536-
.list_chain_txs(&self.chain, self.chain.tip().unwrap_or_default())
534+
self.indexed_graph.graph().list_chain_txs(
535+
&self.chain,
536+
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
537+
)
537538
}
538539

539540
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
540541
/// values.
541542
pub fn get_balance(&self) -> Balance {
542543
self.indexed_graph.graph().balance(
543544
&self.chain,
544-
self.chain.tip().unwrap_or_default(),
545+
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
545546
self.indexed_graph.index.outpoints().iter().cloned(),
546547
|&(k, _), _| k == KeychainKind::Internal,
547548
)
@@ -715,8 +716,7 @@ impl<D> Wallet<D> {
715716
None => self
716717
.chain
717718
.tip()
718-
.and_then(|cp| cp.height.into())
719-
.map(|height| LockTime::from_height(height).expect("Invalid height")),
719+
.map(|cp| LockTime::from_height(cp.height()).expect("Invalid height")),
720720
h => h,
721721
};
722722

@@ -1030,7 +1030,7 @@ impl<D> Wallet<D> {
10301030
) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
10311031
let graph = self.indexed_graph.graph();
10321032
let txout_index = &self.indexed_graph.index;
1033-
let chain_tip = self.chain.tip().unwrap_or_default();
1033+
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
10341034

10351035
let mut tx = graph
10361036
.get_tx(txid)
@@ -1265,7 +1265,7 @@ impl<D> Wallet<D> {
12651265
psbt: &mut psbt::PartiallySignedTransaction,
12661266
sign_options: SignOptions,
12671267
) -> Result<bool, Error> {
1268-
let chain_tip = self.chain.tip().unwrap_or_default();
1268+
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
12691269

12701270
let tx = &psbt.unsigned_tx;
12711271
let mut finished = true;
@@ -1288,7 +1288,7 @@ impl<D> Wallet<D> {
12881288
});
12891289
let current_height = sign_options
12901290
.assume_height
1291-
.or(self.chain.tip().map(|b| b.height));
1291+
.or(self.chain.tip().map(|b| b.height()));
12921292

12931293
debug!(
12941294
"Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}",
@@ -1433,7 +1433,7 @@ impl<D> Wallet<D> {
14331433
must_only_use_confirmed_tx: bool,
14341434
current_height: Option<u32>,
14351435
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
1436-
let chain_tip = self.chain.tip().unwrap_or_default();
1436+
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
14371437
// must_spend <- manually selected utxos
14381438
// may_spend <- all other available utxos
14391439
let mut may_spend = self.get_available_utxos();
@@ -1698,27 +1698,26 @@ impl<D> Wallet<D> {
16981698

16991699
/// Applies an update to the wallet and stages the changes (but does not [`commit`] them).
17001700
///
1701-
/// This returns whether the `update` resulted in any changes.
1702-
///
17031701
/// Usually you create an `update` by interacting with some blockchain data source and inserting
17041702
/// transactions related to your wallet into it.
17051703
///
17061704
/// [`commit`]: Self::commit
1707-
pub fn apply_update(&mut self, update: Update) -> Result<bool, UpdateNotConnectedError>
1705+
pub fn apply_update(&mut self, update: Update) -> Result<(), CannotConnectError>
17081706
where
17091707
D: PersistBackend<ChangeSet>,
17101708
{
1711-
let mut changeset: ChangeSet = self.chain.apply_update(update.chain)?.into();
1709+
let mut changeset = ChangeSet::from(self.chain.apply_update(update.chain)?);
17121710
let (_, index_additions) = self
17131711
.indexed_graph
17141712
.index
1715-
.reveal_to_target_multi(&update.keychain);
1713+
.reveal_to_target_multi(&update.last_active_indices);
17161714
changeset.append(ChangeSet::from(IndexedAdditions::from(index_additions)));
1717-
changeset.append(self.indexed_graph.apply_update(update.graph).into());
1715+
changeset.append(ChangeSet::from(
1716+
self.indexed_graph.apply_update(update.graph),
1717+
));
17181718

1719-
let changed = !changeset.is_empty();
17201719
self.persist.stage(changeset);
1721-
Ok(changed)
1720+
Ok(())
17221721
}
17231722

17241723
/// Commits all curently [`staged`] changed to the persistence backend returning and error when

crates/bdk/tests/wallet.rs

+7-8
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) ->
4444

4545
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
4646
let height = match wallet.latest_checkpoint() {
47-
Some(BlockId { height, .. }) => ConfirmationTime::Confirmed { height, time: 0 },
47+
Some(cp) => ConfirmationTime::Confirmed {
48+
height: cp.height(),
49+
time: 0,
50+
},
4851
None => ConfirmationTime::Unconfirmed { last_seen: 0 },
4952
};
5053
receive_output(wallet, value, height)
@@ -222,7 +225,7 @@ fn test_create_tx_fee_sniping_locktime_last_sync() {
222225
// If there's no current_height we're left with using the last sync height
223226
assert_eq!(
224227
psbt.unsigned_tx.lock_time.0,
225-
wallet.latest_checkpoint().unwrap().height
228+
wallet.latest_checkpoint().unwrap().height()
226229
);
227230
}
228231

@@ -426,11 +429,7 @@ fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() {
426429
fn test_create_tx_drain_to_and_utxos() {
427430
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
428431
let addr = wallet.get_address(New);
429-
let utxos: Vec<_> = wallet
430-
.list_unspent()
431-
.into_iter()
432-
.map(|u| u.outpoint)
433-
.collect();
432+
let utxos: Vec<_> = wallet.list_unspent().map(|u| u.outpoint).collect();
434433
let mut builder = wallet.build_tx();
435434
builder
436435
.drain_to(addr.script_pubkey())
@@ -1482,7 +1481,7 @@ fn test_bump_fee_drain_wallet() {
14821481
.insert_tx(
14831482
tx.clone(),
14841483
ConfirmationTime::Confirmed {
1485-
height: wallet.latest_checkpoint().unwrap().height,
1484+
height: wallet.latest_checkpoint().unwrap().height(),
14861485
time: 42_000,
14871486
},
14881487
)

crates/chain/src/keychain.rs

+23-15
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
1212
1313
use crate::{
14-
collections::BTreeMap,
15-
indexed_tx_graph::IndexedAdditions,
16-
local_chain::{self, LocalChain},
17-
tx_graph::TxGraph,
14+
collections::BTreeMap, indexed_tx_graph::IndexedAdditions, local_chain, tx_graph::TxGraph,
1815
Anchor, Append,
1916
};
2017

@@ -85,24 +82,33 @@ impl<K> AsRef<BTreeMap<K, u32>> for DerivationAdditions<K> {
8582
}
8683
}
8784

88-
/// A structure to update [`KeychainTxOutIndex`], [`TxGraph`] and [`LocalChain`]
89-
/// atomically.
90-
#[derive(Debug, Clone, PartialEq)]
85+
/// A structure to update [`KeychainTxOutIndex`], [`TxGraph`] and [`LocalChain`] atomically.
86+
///
87+
/// [`LocalChain`]: local_chain::LocalChain
88+
#[derive(Debug, Clone)]
9189
pub struct LocalUpdate<K, A> {
92-
/// Last active derivation index per keychain (`K`).
93-
pub keychain: BTreeMap<K, u32>,
90+
/// Contains the last active derivation indices per keychain (`K`), which is used to update the
91+
/// [`KeychainTxOutIndex`].
92+
pub last_active_indices: BTreeMap<K, u32>,
93+
9494
/// Update for the [`TxGraph`].
9595
pub graph: TxGraph<A>,
96+
9697
/// Update for the [`LocalChain`].
97-
pub chain: LocalChain,
98+
///
99+
/// [`LocalChain`]: local_chain::LocalChain
100+
pub chain: local_chain::Update,
98101
}
99102

100-
impl<K, A> Default for LocalUpdate<K, A> {
101-
fn default() -> Self {
103+
impl<K, A> LocalUpdate<K, A> {
104+
/// Construct a [`LocalUpdate`] with a given [`local_chain::Update`].
105+
///
106+
/// [`CheckPoint`]: local_chain::CheckPoint
107+
pub fn new(chain_update: local_chain::Update) -> Self {
102108
Self {
103-
keychain: Default::default(),
104-
graph: Default::default(),
105-
chain: Default::default(),
109+
last_active_indices: BTreeMap::new(),
110+
graph: TxGraph::default(),
111+
chain: chain_update,
106112
}
107113
}
108114
}
@@ -122,6 +128,8 @@ impl<K, A> Default for LocalUpdate<K, A> {
122128
)]
123129
pub struct LocalChangeSet<K, A> {
124130
/// Changes to the [`LocalChain`].
131+
///
132+
/// [`LocalChain`]: local_chain::LocalChain
125133
pub chain_changeset: local_chain::ChangeSet,
126134

127135
/// Additions to [`IndexedTxGraph`].

0 commit comments

Comments
 (0)