Implement linked-list LocalChain and update chain-src crates/examples
This commit 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. * Fix `tempfile` dev dependency of `bdk_file_store` to work with MSRV Co-authored-by: LLFourn <lloyd.fourn@gmail.com>
This commit is contained in:
@@ -23,7 +23,7 @@ pub use bdk_chain::keychain::Balance;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph::IndexedAdditions,
|
||||
keychain::{KeychainTxOutIndex, LocalChangeSet, LocalUpdate},
|
||||
local_chain::{self, LocalChain, UpdateNotConnectedError},
|
||||
local_chain::{self, CannotConnectError, CheckPoint, CheckPointIter, LocalChain},
|
||||
tx_graph::{CanonicalTx, TxGraph},
|
||||
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeAnchor, FullTxOut,
|
||||
IndexedTxGraph, Persist, PersistBackend,
|
||||
@@ -32,8 +32,8 @@ use bitcoin::consensus::encode::serialize;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::util::psbt;
|
||||
use bitcoin::{
|
||||
Address, BlockHash, EcdsaSighashType, LockTime, Network, OutPoint, SchnorrSighashType, Script,
|
||||
Sequence, Transaction, TxOut, Txid, Witness,
|
||||
Address, EcdsaSighashType, LockTime, Network, OutPoint, SchnorrSighashType, Script, Sequence,
|
||||
Transaction, TxOut, Txid, Witness,
|
||||
};
|
||||
use core::fmt;
|
||||
use core::ops::Deref;
|
||||
@@ -245,7 +245,7 @@ impl<D> Wallet<D> {
|
||||
};
|
||||
|
||||
let changeset = db.load_from_persistence().map_err(NewError::Persist)?;
|
||||
chain.apply_changeset(changeset.chain_changeset);
|
||||
chain.apply_changeset(&changeset.chain_changeset);
|
||||
indexed_graph.apply_additions(changeset.indexed_additions);
|
||||
|
||||
let persist = Persist::new(db);
|
||||
@@ -370,19 +370,19 @@ impl<D> Wallet<D> {
|
||||
.graph()
|
||||
.filter_chain_unspents(
|
||||
&self.chain,
|
||||
self.chain.tip().unwrap_or_default(),
|
||||
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
|
||||
self.indexed_graph.index.outpoints().iter().cloned(),
|
||||
)
|
||||
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
|
||||
}
|
||||
|
||||
/// Get all the checkpoints the wallet is currently storing indexed by height.
|
||||
pub fn checkpoints(&self) -> &BTreeMap<u32, BlockHash> {
|
||||
self.chain.blocks()
|
||||
pub fn checkpoints(&self) -> CheckPointIter {
|
||||
self.chain.iter_checkpoints()
|
||||
}
|
||||
|
||||
/// Returns the latest checkpoint.
|
||||
pub fn latest_checkpoint(&self) -> Option<BlockId> {
|
||||
pub fn latest_checkpoint(&self) -> Option<CheckPoint> {
|
||||
self.chain.tip()
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ impl<D> Wallet<D> {
|
||||
.graph()
|
||||
.filter_chain_unspents(
|
||||
&self.chain,
|
||||
self.chain.tip().unwrap_or_default(),
|
||||
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
|
||||
core::iter::once((spk_i, op)),
|
||||
)
|
||||
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
|
||||
@@ -437,7 +437,7 @@ impl<D> Wallet<D> {
|
||||
let canonical_tx = CanonicalTx {
|
||||
observed_as: graph.get_chain_position(
|
||||
&self.chain,
|
||||
self.chain.tip().unwrap_or_default(),
|
||||
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
|
||||
txid,
|
||||
)?,
|
||||
node: graph.get_tx_node(txid)?,
|
||||
@@ -460,7 +460,7 @@ impl<D> Wallet<D> {
|
||||
pub fn insert_checkpoint(
|
||||
&mut self,
|
||||
block_id: BlockId,
|
||||
) -> Result<bool, local_chain::InsertBlockNotMatchingError>
|
||||
) -> Result<bool, local_chain::InsertBlockError>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
@@ -500,17 +500,17 @@ impl<D> Wallet<D> {
|
||||
// anchor tx to checkpoint with lowest height that is >= position's height
|
||||
let anchor = self
|
||||
.chain
|
||||
.blocks()
|
||||
.heights()
|
||||
.range(height..)
|
||||
.next()
|
||||
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
|
||||
tip_height: self.chain.tip().map(|b| b.height),
|
||||
tip_height: self.chain.tip().map(|b| b.height()),
|
||||
tx_height: height,
|
||||
})
|
||||
.map(|(&anchor_height, &anchor_hash)| ConfirmationTimeAnchor {
|
||||
.map(|(&anchor_height, &hash)| ConfirmationTimeAnchor {
|
||||
anchor_block: BlockId {
|
||||
height: anchor_height,
|
||||
hash: anchor_hash,
|
||||
hash,
|
||||
},
|
||||
confirmation_height: height,
|
||||
confirmation_time: time,
|
||||
@@ -531,9 +531,10 @@ impl<D> Wallet<D> {
|
||||
pub fn transactions(
|
||||
&self,
|
||||
) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeAnchor>> + '_ {
|
||||
self.indexed_graph
|
||||
.graph()
|
||||
.list_chain_txs(&self.chain, self.chain.tip().unwrap_or_default())
|
||||
self.indexed_graph.graph().list_chain_txs(
|
||||
&self.chain,
|
||||
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
|
||||
@@ -541,7 +542,7 @@ impl<D> Wallet<D> {
|
||||
pub fn get_balance(&self) -> Balance {
|
||||
self.indexed_graph.graph().balance(
|
||||
&self.chain,
|
||||
self.chain.tip().unwrap_or_default(),
|
||||
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
|
||||
self.indexed_graph.index.outpoints().iter().cloned(),
|
||||
|&(k, _), _| k == KeychainKind::Internal,
|
||||
)
|
||||
@@ -715,8 +716,7 @@ impl<D> Wallet<D> {
|
||||
None => self
|
||||
.chain
|
||||
.tip()
|
||||
.and_then(|cp| cp.height.into())
|
||||
.map(|height| LockTime::from_height(height).expect("Invalid height")),
|
||||
.map(|cp| LockTime::from_height(cp.height()).expect("Invalid height")),
|
||||
h => h,
|
||||
};
|
||||
|
||||
@@ -1030,7 +1030,7 @@ impl<D> Wallet<D> {
|
||||
) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
|
||||
let graph = self.indexed_graph.graph();
|
||||
let txout_index = &self.indexed_graph.index;
|
||||
let chain_tip = self.chain.tip().unwrap_or_default();
|
||||
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
|
||||
|
||||
let mut tx = graph
|
||||
.get_tx(txid)
|
||||
@@ -1265,7 +1265,7 @@ impl<D> Wallet<D> {
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
sign_options: SignOptions,
|
||||
) -> Result<bool, Error> {
|
||||
let chain_tip = self.chain.tip().unwrap_or_default();
|
||||
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
|
||||
|
||||
let tx = &psbt.unsigned_tx;
|
||||
let mut finished = true;
|
||||
@@ -1288,7 +1288,7 @@ impl<D> Wallet<D> {
|
||||
});
|
||||
let current_height = sign_options
|
||||
.assume_height
|
||||
.or(self.chain.tip().map(|b| b.height));
|
||||
.or(self.chain.tip().map(|b| b.height()));
|
||||
|
||||
debug!(
|
||||
"Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}",
|
||||
@@ -1433,7 +1433,7 @@ impl<D> Wallet<D> {
|
||||
must_only_use_confirmed_tx: bool,
|
||||
current_height: Option<u32>,
|
||||
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
|
||||
let chain_tip = self.chain.tip().unwrap_or_default();
|
||||
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
|
||||
// must_spend <- manually selected utxos
|
||||
// may_spend <- all other available utxos
|
||||
let mut may_spend = self.get_available_utxos();
|
||||
@@ -1697,24 +1697,25 @@ impl<D> Wallet<D> {
|
||||
}
|
||||
|
||||
/// Applies an update to the wallet and stages the changes (but does not [`commit`] them).
|
||||
///
|
||||
/// This returns whether the `update` resulted in any changes.
|
||||
/// Returns whether the `update` resulted in any changes.
|
||||
///
|
||||
/// Usually you create an `update` by interacting with some blockchain data source and inserting
|
||||
/// transactions related to your wallet into it.
|
||||
///
|
||||
/// [`commit`]: Self::commit
|
||||
pub fn apply_update(&mut self, update: Update) -> Result<bool, UpdateNotConnectedError>
|
||||
pub fn apply_update(&mut self, update: Update) -> Result<bool, CannotConnectError>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
let mut changeset: ChangeSet = self.chain.apply_update(update.chain)?.into();
|
||||
let mut changeset = ChangeSet::from(self.chain.apply_update(update.chain)?);
|
||||
let (_, index_additions) = self
|
||||
.indexed_graph
|
||||
.index
|
||||
.reveal_to_target_multi(&update.keychain);
|
||||
changeset.append(ChangeSet::from(IndexedAdditions::from(index_additions)));
|
||||
changeset.append(self.indexed_graph.apply_update(update.graph).into());
|
||||
changeset.append(ChangeSet::from(
|
||||
self.indexed_graph.apply_update(update.graph),
|
||||
));
|
||||
|
||||
let changed = !changeset.is_empty();
|
||||
self.persist.stage(changeset);
|
||||
|
||||
@@ -44,7 +44,10 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) ->
|
||||
|
||||
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
|
||||
let height = match wallet.latest_checkpoint() {
|
||||
Some(BlockId { height, .. }) => ConfirmationTime::Confirmed { height, time: 0 },
|
||||
Some(cp) => ConfirmationTime::Confirmed {
|
||||
height: cp.height(),
|
||||
time: 0,
|
||||
},
|
||||
None => ConfirmationTime::Unconfirmed { last_seen: 0 },
|
||||
};
|
||||
receive_output(wallet, value, height)
|
||||
@@ -222,7 +225,7 @@ fn test_create_tx_fee_sniping_locktime_last_sync() {
|
||||
// If there's no current_height we're left with using the last sync height
|
||||
assert_eq!(
|
||||
psbt.unsigned_tx.lock_time.0,
|
||||
wallet.latest_checkpoint().unwrap().height
|
||||
wallet.latest_checkpoint().unwrap().height()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -426,11 +429,7 @@ fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() {
|
||||
fn test_create_tx_drain_to_and_utxos() {
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_address(New);
|
||||
let utxos: Vec<_> = wallet
|
||||
.list_unspent()
|
||||
.into_iter()
|
||||
.map(|u| u.outpoint)
|
||||
.collect();
|
||||
let utxos: Vec<_> = wallet.list_unspent().map(|u| u.outpoint).collect();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.drain_to(addr.script_pubkey())
|
||||
@@ -1482,7 +1481,7 @@ fn test_bump_fee_drain_wallet() {
|
||||
.insert_tx(
|
||||
tx.clone(),
|
||||
ConfirmationTime::Confirmed {
|
||||
height: wallet.latest_checkpoint().unwrap().height,
|
||||
height: wallet.latest_checkpoint().unwrap().height(),
|
||||
time: 42_000,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user