"crates/chain",
"crates/file_store",
"crates/electrum",
+ "crates/esplora",
"example-crates/example_cli",
"example-crates/example_electrum",
- "example-crates/keychain_tracker_electrum",
- "example-crates/keychain_tracker_esplora",
- "example-crates/keychain_tracker_example_cli",
"example-crates/wallet_electrum",
"example-crates/wallet_esplora",
"example-crates/wallet_esplora_async",
.transactions()
.next()
.map_or(0, |canonical_tx| match canonical_tx.observed_as {
- bdk_chain::ObservedAs::Confirmed(a) => a.confirmation_height,
- bdk_chain::ObservedAs::Unconfirmed(_) => 0,
+ bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height,
+ bdk_chain::ChainPosition::Unconfirmed(_) => 0,
})
} else {
0
keychain::{KeychainTxOutIndex, LocalChangeSet, LocalUpdate},
local_chain::{self, LocalChain, UpdateNotConnectedError},
tx_graph::{CanonicalTx, TxGraph},
- Append, BlockId, ConfirmationTime, ConfirmationTimeAnchor, FullTxOut, ObservedAs, Persist,
+ Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeAnchor, FullTxOut, Persist,
PersistBackend,
};
use bitcoin::consensus::encode::serialize;
let pos = graph
.get_chain_position(&self.chain, chain_tip, txid)
.ok_or(Error::TransactionNotFound)?;
- if let ObservedAs::Confirmed(_) = pos {
+ if let ChainPosition::Confirmed(_) = pos {
return Err(Error::TransactionConfirmed);
}
.graph()
.get_chain_position(&self.chain, chain_tip, input.previous_output.txid)
.map(|observed_as| match observed_as {
- ObservedAs::Confirmed(a) => a.confirmation_height,
- ObservedAs::Unconfirmed(_) => u32::MAX,
+ ChainPosition::Confirmed(a) => a.confirmation_height,
+ ChainPosition::Unconfirmed(_) => u32::MAX,
});
let current_height = sign_options
.assume_height
fn new_local_utxo(
keychain: KeychainKind,
derivation_index: u32,
- full_txo: FullTxOut<ObservedAs<ConfirmationTimeAnchor>>,
+ full_txo: FullTxOut<ConfirmationTimeAnchor>,
) -> LocalUtxo {
LocalUtxo {
outpoint: full_txo.outpoint,
use bdk::FeeRate;
use bdk::KeychainKind;
use bdk_chain::BlockId;
+use bdk_chain::ConfirmationTime;
use bdk_chain::COINBASE_MATURITY;
-use bdk_chain::{ConfirmationTime, TxHeight};
use bitcoin::hashes::Hash;
use bitcoin::BlockHash;
use bitcoin::Script;
mod common;
use common::*;
-fn receive_output(wallet: &mut Wallet, value: u64, height: TxHeight) -> OutPoint {
+fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) -> OutPoint {
let tx = Transaction {
version: 1,
lock_time: PackedLockTime(0),
}],
};
- wallet
- .insert_tx(
- tx.clone(),
- match height {
- TxHeight::Confirmed(height) => ConfirmationTime::Confirmed {
- height,
- time: 42_000,
- },
- TxHeight::Unconfirmed => ConfirmationTime::Unconfirmed { last_seen: 0 },
- },
- )
- .unwrap();
+ wallet.insert_tx(tx.clone(), height).unwrap();
OutPoint {
txid: tx.txid(),
}
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
- let height = wallet.latest_checkpoint().map(|id| id.height).into();
+ let height = match wallet.latest_checkpoint() {
+ Some(BlockId { height, .. }) => ConfirmationTime::Confirmed { height, time: 0 },
+ None => ConfirmationTime::Unconfirmed { last_seen: 0 },
+ };
receive_output(wallet, value, height)
}
let (psbt, __details) = builder.finish().unwrap();
// Now we receive one transaction with 0 confirmations. We won't be able to use that for
// fee bumping, as it's still unconfirmed!
- receive_output(&mut wallet, 25_000, TxHeight::Unconfirmed);
+ receive_output(
+ &mut wallet,
+ 25_000,
+ ConfirmationTime::Unconfirmed { last_seen: 0 },
+ );
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
// We receive a tx with 0 confirmations, which will be used as an input
// in the drain tx.
- receive_output(&mut wallet, 25_000, TxHeight::Unconfirmed);
+ receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0));
let mut builder = wallet.build_tx();
builder
.drain_wallet()
use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid};
-use crate::{
- sparse_chain::{self, ChainPosition},
- Anchor, COINBASE_MATURITY,
-};
+use crate::{Anchor, COINBASE_MATURITY};
-/// Represents an observation of some chain data.
+/// Represents the observed position of some chain data.
///
/// The generic `A` should be a [`Anchor`] implementation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
-pub enum ObservedAs<A> {
+pub enum ChainPosition<A> {
/// The chain data is seen as confirmed, and in anchored by `A`.
Confirmed(A),
/// The chain data is seen in mempool at this given timestamp.
Unconfirmed(u64),
}
-impl<A> ObservedAs<A> {
- /// Returns whether [`ObservedAs`] is confirmed or not.
+impl<A> ChainPosition<A> {
+ /// Returns whether [`ChainPosition`] is confirmed or not.
pub fn is_confirmed(&self) -> bool {
matches!(self, Self::Confirmed(_))
}
}
-impl<A: Clone> ObservedAs<&A> {
- pub fn cloned(self) -> ObservedAs<A> {
+impl<A: Clone> ChainPosition<&A> {
+ pub fn cloned(self) -> ChainPosition<A> {
match self {
- ObservedAs::Confirmed(a) => ObservedAs::Confirmed(a.clone()),
- ObservedAs::Unconfirmed(last_seen) => ObservedAs::Unconfirmed(last_seen),
+ ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()),
+ ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen),
}
}
}
-/// Represents the height at which a transaction is confirmed.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-#[cfg_attr(
- feature = "serde",
- derive(serde::Deserialize, serde::Serialize),
- serde(crate = "serde_crate")
-)]
-pub enum TxHeight {
- Confirmed(u32),
- Unconfirmed,
-}
-
-impl Default for TxHeight {
- fn default() -> Self {
- Self::Unconfirmed
- }
-}
-
-impl core::fmt::Display for TxHeight {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+impl<A: Anchor> ChainPosition<A> {
+ pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
match self {
- Self::Confirmed(h) => core::write!(f, "confirmed_at({})", h),
- Self::Unconfirmed => core::write!(f, "unconfirmed"),
+ ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()),
+ ChainPosition::Unconfirmed(_) => None,
}
}
}
-impl From<Option<u32>> for TxHeight {
- fn from(opt: Option<u32>) -> Self {
- match opt {
- Some(h) => Self::Confirmed(h),
- None => Self::Unconfirmed,
- }
- }
-}
-
-impl From<TxHeight> for Option<u32> {
- fn from(height: TxHeight) -> Self {
- match height {
- TxHeight::Confirmed(h) => Some(h),
- TxHeight::Unconfirmed => None,
- }
- }
-}
-
-impl crate::sparse_chain::ChainPosition for TxHeight {
- fn height(&self) -> TxHeight {
- *self
- }
-
- fn max_ord_of_height(height: TxHeight) -> Self {
- height
- }
-
- fn min_ord_of_height(height: TxHeight) -> Self {
- height
- }
-}
-
-impl TxHeight {
- pub fn is_confirmed(&self) -> bool {
- matches!(self, Self::Confirmed(_))
- }
-}
-
/// Block height and timestamp at which a transaction is confirmed.
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
Unconfirmed { last_seen: u64 },
}
-impl sparse_chain::ChainPosition for ConfirmationTime {
- fn height(&self) -> TxHeight {
- match self {
- ConfirmationTime::Confirmed { height, .. } => TxHeight::Confirmed(*height),
- ConfirmationTime::Unconfirmed { .. } => TxHeight::Unconfirmed,
- }
- }
-
- fn max_ord_of_height(height: TxHeight) -> Self {
- match height {
- TxHeight::Confirmed(height) => Self::Confirmed {
- height,
- time: u64::MAX,
- },
- TxHeight::Unconfirmed => Self::Unconfirmed { last_seen: 0 },
- }
- }
-
- fn min_ord_of_height(height: TxHeight) -> Self {
- match height {
- TxHeight::Confirmed(height) => Self::Confirmed {
- height,
- time: u64::MIN,
- },
- TxHeight::Unconfirmed => Self::Unconfirmed { last_seen: 0 },
- }
+impl ConfirmationTime {
+ pub fn unconfirmed(last_seen: u64) -> Self {
+ Self::Unconfirmed { last_seen }
}
-}
-impl ConfirmationTime {
pub fn is_confirmed(&self) -> bool {
matches!(self, Self::Confirmed { .. })
}
}
-impl From<ObservedAs<ConfirmationTimeAnchor>> for ConfirmationTime {
- fn from(observed_as: ObservedAs<ConfirmationTimeAnchor>) -> Self {
+impl From<ChainPosition<ConfirmationTimeAnchor>> for ConfirmationTime {
+ fn from(observed_as: ChainPosition<ConfirmationTimeAnchor>) -> Self {
match observed_as {
- ObservedAs::Confirmed(a) => Self::Confirmed {
+ ChainPosition::Confirmed(a) => Self::Confirmed {
height: a.confirmation_height,
time: a.confirmation_time,
},
- ObservedAs::Unconfirmed(_) => Self::Unconfirmed { last_seen: 0 },
+ ChainPosition::Unconfirmed(_) => Self::Unconfirmed { last_seen: 0 },
}
}
}
}
/// A `TxOut` with as much data as we can retrieve about it
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-pub struct FullTxOut<P> {
+pub struct FullTxOut<A> {
/// The location of the `TxOut`.
pub outpoint: OutPoint,
/// The `TxOut`.
pub txout: TxOut,
/// The position of the transaction in `outpoint` in the overall chain.
- pub chain_position: P,
+ pub chain_position: ChainPosition<A>,
/// The txid and chain position of the transaction (if any) that has spent this output.
- pub spent_by: Option<(P, Txid)>,
+ pub spent_by: Option<(ChainPosition<A>, Txid)>,
/// Whether this output is on a coinbase transaction.
pub is_on_coinbase: bool,
}
-impl<P: ChainPosition> FullTxOut<P> {
- /// Whether the utxo is/was/will be spendable at `height`.
- ///
- /// It is spendable if it is not an immature coinbase output and no spending tx has been
- /// confirmed by that height.
- pub fn is_spendable_at(&self, height: u32) -> bool {
- if !self.is_mature(height) {
- return false;
- }
-
- if self.chain_position.height() > TxHeight::Confirmed(height) {
- return false;
- }
-
- match &self.spent_by {
- Some((spending_height, _)) => spending_height.height() > TxHeight::Confirmed(height),
- None => true,
- }
- }
-
- pub fn is_mature(&self, height: u32) -> bool {
- if self.is_on_coinbase {
- let tx_height = match self.chain_position.height() {
- TxHeight::Confirmed(tx_height) => tx_height,
- TxHeight::Unconfirmed => {
- debug_assert!(false, "coinbase tx can never be unconfirmed");
- return false;
- }
- };
- let age = height.saturating_sub(tx_height);
- if age + 1 < COINBASE_MATURITY {
- return false;
- }
- }
-
- true
- }
-}
-
-impl<A: Anchor> FullTxOut<ObservedAs<A>> {
+impl<A: Anchor> FullTxOut<A> {
/// Whether the `txout` is considered mature.
///
- /// This is the alternative version of [`is_mature`] which depends on `chain_position` being a
- /// [`ObservedAs<A>`] where `A` implements [`Anchor`].
- ///
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
/// method may return false-negatives. In other words, interpretted confirmation count may be
/// less than the actual value.
///
- /// [`is_mature`]: Self::is_mature
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
pub fn is_mature(&self, tip: u32) -> bool {
if self.is_on_coinbase {
let tx_height = match &self.chain_position {
- ObservedAs::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
- ObservedAs::Unconfirmed(_) => {
+ ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
+ ChainPosition::Unconfirmed(_) => {
debug_assert!(false, "coinbase tx can never be unconfirmed");
return false;
}
///
/// This method does not take into account the locktime.
///
- /// This is the alternative version of [`is_spendable_at`] which depends on `chain_position`
- /// being a [`ObservedAs<A>`] where `A` implements [`Anchor`].
- ///
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
/// method may return false-negatives. In other words, interpretted confirmation count may be
/// less than the actual value.
///
- /// [`is_spendable_at`]: Self::is_spendable_at
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool {
if !self.is_mature(tip) {
}
let confirmation_height = match &self.chain_position {
- ObservedAs::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
- ObservedAs::Unconfirmed(_) => return false,
+ ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
+ ChainPosition::Unconfirmed(_) => return false,
};
if confirmation_height > tip {
return false;
}
// if the spending tx is confirmed within tip height, the txout is no longer spendable
- if let Some((ObservedAs::Confirmed(spending_anchor), _)) = &self.spent_by {
+ if let Some((ChainPosition::Confirmed(spending_anchor), _)) = &self.spent_by {
if spending_anchor.anchor_block().height <= tip {
return false;
}
+++ /dev/null
-//! Module for structures that combine the features of [`sparse_chain`] and [`tx_graph`].
-use crate::{
- collections::HashSet,
- sparse_chain::{self, ChainPosition, SparseChain},
- tx_graph::{self, TxGraph},
- Append, BlockId, ForEachTxOut, FullTxOut, TxHeight,
-};
-use alloc::{string::ToString, vec::Vec};
-use bitcoin::{OutPoint, Transaction, TxOut, Txid};
-use core::fmt::Debug;
-
-/// A consistent combination of a [`SparseChain<P>`] and a [`TxGraph<T>`].
-///
-/// `SparseChain` only keeps track of transaction ids and their position in the chain, but you often
-/// want to store the full transactions as well. Additionally, you want to make sure that everything
-/// in the chain is consistent with the full transaction data. `ChainGraph` enforces these two
-/// invariants:
-///
-/// 1. Every transaction that is in the chain is also in the graph (you always have the full
-/// transaction).
-/// 2. No transactions in the chain conflict with each other, i.e., they don't double spend each
-/// other or have ancestors that double spend each other.
-///
-/// Note that the `ChainGraph` guarantees a 1:1 mapping between transactions in the `chain` and
-/// `graph` but not the other way around. Transactions may fall out of the *chain* (via re-org or
-/// mempool eviction) but will remain in the *graph*.
-#[derive(Clone, Debug, PartialEq)]
-pub struct ChainGraph<P = TxHeight> {
- chain: SparseChain<P>,
- graph: TxGraph,
-}
-
-impl<P> Default for ChainGraph<P> {
- fn default() -> Self {
- Self {
- chain: Default::default(),
- graph: Default::default(),
- }
- }
-}
-
-impl<P> AsRef<SparseChain<P>> for ChainGraph<P> {
- fn as_ref(&self) -> &SparseChain<P> {
- &self.chain
- }
-}
-
-impl<P> AsRef<TxGraph> for ChainGraph<P> {
- fn as_ref(&self) -> &TxGraph {
- &self.graph
- }
-}
-
-impl<P> AsRef<ChainGraph<P>> for ChainGraph<P> {
- fn as_ref(&self) -> &ChainGraph<P> {
- self
- }
-}
-
-impl<P> ChainGraph<P> {
- /// Returns a reference to the internal [`SparseChain`].
- pub fn chain(&self) -> &SparseChain<P> {
- &self.chain
- }
-
- /// Returns a reference to the internal [`TxGraph`].
- pub fn graph(&self) -> &TxGraph {
- &self.graph
- }
-}
-
-impl<P> ChainGraph<P>
-where
- P: ChainPosition,
-{
- /// Create a new chain graph from a `chain` and a `graph`.
- ///
- /// There are two reasons this can return an `Err`:
- ///
- /// 1. There is a transaction in the `chain` that does not have its corresponding full
- /// transaction in `graph`.
- /// 2. The `chain` has two transactions that are allegedly in it, but they conflict in the `graph`
- /// (so could not possibly be in the same chain).
- pub fn new(chain: SparseChain<P>, graph: TxGraph) -> Result<Self, NewError<P>> {
- let mut missing = HashSet::default();
- for (pos, txid) in chain.txids() {
- if let Some(tx) = graph.get_tx(*txid) {
- let conflict = graph
- .walk_conflicts(tx, |_, txid| Some((chain.tx_position(txid)?.clone(), txid)))
- .next();
- if let Some((conflict_pos, conflict)) = conflict {
- return Err(NewError::Conflict {
- a: (pos.clone(), *txid),
- b: (conflict_pos, conflict),
- });
- }
- } else {
- missing.insert(*txid);
- }
- }
-
- if !missing.is_empty() {
- return Err(NewError::Missing(missing));
- }
-
- Ok(Self { chain, graph })
- }
-
- /// Take an update in the form of a [`SparseChain<P>`][`SparseChain`] and attempt to turn it
- /// into a chain graph by filling in full transactions from `self` and from `new_txs`. This
- /// returns a `ChainGraph<P, Cow<T>>` where the [`Cow<'a, T>`] will borrow the transaction if it
- /// got it from `self`.
- ///
- /// This is useful when interacting with services like an electrum server which returns a list
- /// of txids and heights when calling [`script_get_history`], which can easily be inserted into a
- /// [`SparseChain<TxHeight>`][`SparseChain`]. From there, you need to figure out which full
- /// transactions you are missing in your chain graph and form `new_txs`. You then use
- /// `inflate_update` to turn this into an update `ChainGraph<P, Cow<Transaction>>` and finally
- /// use [`determine_changeset`] to generate the changeset from it.
- ///
- /// [`SparseChain`]: crate::sparse_chain::SparseChain
- /// [`Cow<'a, T>`]: std::borrow::Cow
- /// [`script_get_history`]: https://docs.rs/electrum-client/latest/electrum_client/trait.ElectrumApi.html#tymethod.script_get_history
- /// [`determine_changeset`]: Self::determine_changeset
- pub fn inflate_update(
- &self,
- update: SparseChain<P>,
- new_txs: impl IntoIterator<Item = Transaction>,
- ) -> Result<ChainGraph<P>, NewError<P>> {
- let mut inflated_chain = SparseChain::default();
- let mut inflated_graph = TxGraph::default();
-
- for (height, hash) in update.checkpoints().clone().into_iter() {
- let _ = inflated_chain
- .insert_checkpoint(BlockId { height, hash })
- .expect("must insert");
- }
-
- // [TODO] @evanlinjin: These need better comments
- // - copy transactions that have changed positions into the graph
- // - add new transactions to an inflated chain
- for (pos, txid) in update.txids() {
- match self.chain.tx_position(*txid) {
- Some(original_pos) => {
- if original_pos != pos {
- let tx = self
- .graph
- .get_tx(*txid)
- .expect("tx must exist as it is referenced in sparsechain")
- .clone();
- let _ = inflated_chain
- .insert_tx(*txid, pos.clone())
- .expect("must insert since this was already in update");
- let _ = inflated_graph.insert_tx(tx);
- }
- }
- None => {
- let _ = inflated_chain
- .insert_tx(*txid, pos.clone())
- .expect("must insert since this was already in update");
- }
- }
- }
-
- for tx in new_txs {
- let _ = inflated_graph.insert_tx(tx);
- }
-
- ChainGraph::new(inflated_chain, inflated_graph)
- }
-
- /// Gets the checkpoint limit.
- ///
- /// Refer to [`SparseChain::checkpoint_limit`] for more.
- pub fn checkpoint_limit(&self) -> Option<usize> {
- self.chain.checkpoint_limit()
- }
-
- /// Sets the checkpoint limit.
- ///
- /// Refer to [`SparseChain::set_checkpoint_limit`] for more.
- pub fn set_checkpoint_limit(&mut self, limit: Option<usize>) {
- self.chain.set_checkpoint_limit(limit)
- }
-
- /// Determines the changes required to invalidate checkpoints `from_height` (inclusive) and
- /// above. Displaced transactions will have their positions moved to [`TxHeight::Unconfirmed`].
- pub fn invalidate_checkpoints_preview(&self, from_height: u32) -> ChangeSet<P> {
- ChangeSet {
- chain: self.chain.invalidate_checkpoints_preview(from_height),
- ..Default::default()
- }
- }
-
- /// Invalidate checkpoints `from_height` (inclusive) and above. Displaced transactions will be
- /// re-positioned to [`TxHeight::Unconfirmed`].
- ///
- /// This is equivalent to calling [`Self::invalidate_checkpoints_preview`] and
- /// [`Self::apply_changeset`] in sequence.
- pub fn invalidate_checkpoints(&mut self, from_height: u32) -> ChangeSet<P>
- where
- ChangeSet<P>: Clone,
- {
- let changeset = self.invalidate_checkpoints_preview(from_height);
- self.apply_changeset(changeset.clone());
- changeset
- }
-
- /// Get a transaction currently in the underlying [`SparseChain`].
- ///
- /// This does not necessarily mean that it is *confirmed* in the blockchain; it might just be in
- /// the unconfirmed transaction list within the [`SparseChain`].
- pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, &Transaction)> {
- let position = self.chain.tx_position(txid)?;
- let full_tx = self.graph.get_tx(txid).expect("must exist");
- Some((position, full_tx))
- }
-
- /// Determines the changes required to insert a transaction into the inner [`ChainGraph`] and
- /// [`SparseChain`] at the given `position`.
- ///
- /// If inserting it into the chain `position` will result in conflicts, the returned
- /// [`ChangeSet`] should evict conflicting transactions.
- pub fn insert_tx_preview(
- &self,
- tx: Transaction,
- pos: P,
- ) -> Result<ChangeSet<P>, InsertTxError<P>> {
- let mut changeset = ChangeSet {
- chain: self.chain.insert_tx_preview(tx.txid(), pos)?,
- graph: self.graph.insert_tx_preview(tx),
- };
- self.fix_conflicts(&mut changeset)?;
- Ok(changeset)
- }
-
- /// Inserts [`Transaction`] at the given chain position.
- ///
- /// This is equivalent to calling [`Self::insert_tx_preview`] and [`Self::apply_changeset`] in
- /// sequence.
- pub fn insert_tx(&mut self, tx: Transaction, pos: P) -> Result<ChangeSet<P>, InsertTxError<P>> {
- let changeset = self.insert_tx_preview(tx, pos)?;
- self.apply_changeset(changeset.clone());
- Ok(changeset)
- }
-
- /// Determines the changes required to insert a [`TxOut`] into the internal [`TxGraph`].
- pub fn insert_txout_preview(&self, outpoint: OutPoint, txout: TxOut) -> ChangeSet<P> {
- ChangeSet {
- chain: Default::default(),
- graph: self.graph.insert_txout_preview(outpoint, txout),
- }
- }
-
- /// Inserts a [`TxOut`] into the internal [`TxGraph`].
- ///
- /// This is equivalent to calling [`Self::insert_txout_preview`] and [`Self::apply_changeset`]
- /// in sequence.
- pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet<P> {
- let changeset = self.insert_txout_preview(outpoint, txout);
- self.apply_changeset(changeset.clone());
- changeset
- }
-
- /// Determines the changes required to insert a `block_id` (a height and block hash) into the
- /// chain.
- ///
- /// If a checkpoint with a different hash already exists at that height, this will return an error.
- pub fn insert_checkpoint_preview(
- &self,
- block_id: BlockId,
- ) -> Result<ChangeSet<P>, InsertCheckpointError> {
- self.chain
- .insert_checkpoint_preview(block_id)
- .map(|chain_changeset| ChangeSet {
- chain: chain_changeset,
- ..Default::default()
- })
- }
-
- /// Inserts checkpoint into [`Self`].
- ///
- /// This is equivalent to calling [`Self::insert_checkpoint_preview`] and
- /// [`Self::apply_changeset`] in sequence.
- pub fn insert_checkpoint(
- &mut self,
- block_id: BlockId,
- ) -> Result<ChangeSet<P>, InsertCheckpointError> {
- let changeset = self.insert_checkpoint_preview(block_id)?;
- self.apply_changeset(changeset.clone());
- Ok(changeset)
- }
-
- /// Calculates the difference between self and `update` in the form of a [`ChangeSet`].
- pub fn determine_changeset(
- &self,
- update: &ChainGraph<P>,
- ) -> Result<ChangeSet<P>, UpdateError<P>> {
- let chain_changeset = self
- .chain
- .determine_changeset(&update.chain)
- .map_err(UpdateError::Chain)?;
-
- let mut changeset = ChangeSet {
- chain: chain_changeset,
- graph: self.graph.determine_additions(&update.graph),
- };
-
- self.fix_conflicts(&mut changeset)?;
- Ok(changeset)
- }
-
- /// Given a transaction, return an iterator of `txid`s that conflict with it (spends at least
- /// one of the same inputs). This iterator includes all descendants of conflicting transactions.
- ///
- /// This method only returns conflicts that exist in the [`SparseChain`] as transactions that
- /// are not included in [`SparseChain`] are already considered as evicted.
- pub fn tx_conflicts_in_chain<'a>(
- &'a self,
- tx: &'a Transaction,
- ) -> impl Iterator<Item = (&'a P, Txid)> + 'a {
- self.graph.walk_conflicts(tx, move |_, conflict_txid| {
- self.chain
- .tx_position(conflict_txid)
- .map(|conflict_pos| (conflict_pos, conflict_txid))
- })
- }
-
- /// Fix changeset conflicts.
- ///
- /// **WARNING:** If there are any missing full txs, conflict resolution will not be complete. In
- /// debug mode, this will result in panic.
- fn fix_conflicts(&self, changeset: &mut ChangeSet<P>) -> Result<(), UnresolvableConflict<P>> {
- let mut chain_conflicts = vec![];
-
- for (&txid, pos_change) in &changeset.chain.txids {
- let pos = match pos_change {
- Some(pos) => {
- // Ignore txs that are still in the chain -- we only care about new ones
- if self.chain.tx_position(txid).is_some() {
- continue;
- }
- pos
- }
- // Ignore txids that are being deleted by the change (they can't conflict)
- None => continue,
- };
-
- let mut full_tx = self.graph.get_tx(txid);
-
- if full_tx.is_none() {
- full_tx = changeset.graph.tx.iter().find(|tx| tx.txid() == txid)
- }
-
- debug_assert!(full_tx.is_some(), "should have full tx at this point");
-
- let full_tx = match full_tx {
- Some(full_tx) => full_tx,
- None => continue,
- };
-
- for (conflict_pos, conflict_txid) in self.tx_conflicts_in_chain(full_tx) {
- chain_conflicts.push((pos.clone(), txid, conflict_pos, conflict_txid))
- }
- }
-
- for (update_pos, update_txid, conflicting_pos, conflicting_txid) in chain_conflicts {
- // We have found a tx that conflicts with our update txid. Only allow this when the
- // conflicting tx will be positioned as "unconfirmed" after the update is applied.
- // If so, we will modify the changeset to evict the conflicting txid.
-
- // determine the position of the conflicting txid after the current changeset is applied
- let conflicting_new_pos = changeset
- .chain
- .txids
- .get(&conflicting_txid)
- .map(Option::as_ref)
- .unwrap_or(Some(conflicting_pos));
-
- match conflicting_new_pos {
- None => {
- // conflicting txid will be deleted, can ignore
- }
- Some(existing_new_pos) => match existing_new_pos.height() {
- TxHeight::Confirmed(_) => {
- // the new position of the conflicting tx is "confirmed", therefore cannot be
- // evicted, return error
- return Err(UnresolvableConflict {
- already_confirmed_tx: (conflicting_pos.clone(), conflicting_txid),
- update_tx: (update_pos, update_txid),
- });
- }
- TxHeight::Unconfirmed => {
- // the new position of the conflicting tx is "unconfirmed", therefore it can
- // be evicted
- changeset.chain.txids.insert(conflicting_txid, None);
- }
- },
- };
- }
-
- Ok(())
- }
-
- /// Applies `changeset` to `self`.
- ///
- /// **Warning** this method assumes that the changeset is correctly formed. If it is not, the
- /// chain graph may behave incorrectly in the future and panic unexpectedly.
- pub fn apply_changeset(&mut self, changeset: ChangeSet<P>) {
- self.chain.apply_changeset(changeset.chain);
- self.graph.apply_additions(changeset.graph);
- }
-
- /// Applies the `update` chain graph. Note this is shorthand for calling
- /// [`Self::determine_changeset()`] and [`Self::apply_changeset()`] in sequence.
- pub fn apply_update(&mut self, update: ChainGraph<P>) -> Result<ChangeSet<P>, UpdateError<P>> {
- let changeset = self.determine_changeset(&update)?;
- self.apply_changeset(changeset.clone());
- Ok(changeset)
- }
-
- /// Get the full transaction output at an outpoint if it exists in the chain and the graph.
- pub fn full_txout(&self, outpoint: OutPoint) -> Option<FullTxOut<P>> {
- self.chain.full_txout(&self.graph, outpoint)
- }
-
- /// Iterate over the full transactions and their position in the chain ordered by their position
- /// in ascending order.
- pub fn transactions_in_chain(&self) -> impl DoubleEndedIterator<Item = (&P, &Transaction)> {
- self.chain
- .txids()
- .map(move |(pos, txid)| (pos, self.graph.get_tx(*txid).expect("must exist")))
- }
-
- /// Find the transaction in the chain that spends `outpoint`.
- ///
- /// This uses the input/output relationships in the internal `graph`. Note that the transaction
- /// which includes `outpoint` does not need to be in the `graph` or the `chain` for this to
- /// return `Some(_)`.
- pub fn spent_by(&self, outpoint: OutPoint) -> Option<(&P, Txid)> {
- self.chain.spent_by(&self.graph, outpoint)
- }
-
- /// Whether the chain graph contains any data whatsoever.
- pub fn is_empty(&self) -> bool {
- self.chain.is_empty() && self.graph.is_empty()
- }
-}
-
-/// Represents changes to [`ChainGraph`].
-///
-/// This is essentially a combination of [`sparse_chain::ChangeSet`] and [`tx_graph::Additions`].
-#[derive(Debug, Clone, PartialEq)]
-#[cfg_attr(
- feature = "serde",
- derive(serde::Deserialize, serde::Serialize),
- serde(
- crate = "serde_crate",
- bound(
- deserialize = "P: serde::Deserialize<'de>",
- serialize = "P: serde::Serialize"
- )
- )
-)]
-#[must_use]
-pub struct ChangeSet<P> {
- pub chain: sparse_chain::ChangeSet<P>,
- pub graph: tx_graph::Additions,
-}
-
-impl<P> ChangeSet<P> {
- /// Returns `true` if this [`ChangeSet`] records no changes.
- pub fn is_empty(&self) -> bool {
- self.chain.is_empty() && self.graph.is_empty()
- }
-
- /// Returns `true` if this [`ChangeSet`] contains transaction evictions.
- pub fn contains_eviction(&self) -> bool {
- self.chain
- .txids
- .iter()
- .any(|(_, new_pos)| new_pos.is_none())
- }
-
- /// Appends the changes in `other` into self such that applying `self` afterward has the same
- /// effect as sequentially applying the original `self` and `other`.
- pub fn append(&mut self, other: ChangeSet<P>)
- where
- P: ChainPosition,
- {
- self.chain.append(other.chain);
- self.graph.append(other.graph);
- }
-}
-
-impl<P> Default for ChangeSet<P> {
- fn default() -> Self {
- Self {
- chain: Default::default(),
- graph: Default::default(),
- }
- }
-}
-
-impl<P> ForEachTxOut for ChainGraph<P> {
- fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) {
- self.graph.for_each_txout(f)
- }
-}
-
-impl<P> ForEachTxOut for ChangeSet<P> {
- fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) {
- self.graph.for_each_txout(f)
- }
-}
-
-/// Error that may occur when calling [`ChainGraph::new`].
-#[derive(Clone, Debug, PartialEq)]
-pub enum NewError<P> {
- /// Two transactions within the sparse chain conflicted with each other
- Conflict { a: (P, Txid), b: (P, Txid) },
- /// One or more transactions in the chain were not in the graph
- Missing(HashSet<Txid>),
-}
-
-impl<P: core::fmt::Debug> core::fmt::Display for NewError<P> {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- match self {
- NewError::Conflict { a, b } => write!(
- f,
- "Unable to inflate sparse chain to chain graph since transactions {:?} and {:?}",
- a, b
- ),
- NewError::Missing(missing) => write!(
- f,
- "missing full transactions for {}",
- missing
- .iter()
- .map(|txid| txid.to_string())
- .collect::<Vec<_>>()
- .join(", ")
- ),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl<P: core::fmt::Debug> std::error::Error for NewError<P> {}
-
-/// Error that may occur when inserting a transaction.
-///
-/// Refer to [`ChainGraph::insert_tx_preview`] and [`ChainGraph::insert_tx`].
-#[derive(Clone, Debug, PartialEq)]
-pub enum InsertTxError<P> {
- Chain(sparse_chain::InsertTxError<P>),
- UnresolvableConflict(UnresolvableConflict<P>),
-}
-
-impl<P: core::fmt::Debug> core::fmt::Display for InsertTxError<P> {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- match self {
- InsertTxError::Chain(inner) => core::fmt::Display::fmt(inner, f),
- InsertTxError::UnresolvableConflict(inner) => core::fmt::Display::fmt(inner, f),
- }
- }
-}
-
-impl<P> From<sparse_chain::InsertTxError<P>> for InsertTxError<P> {
- fn from(inner: sparse_chain::InsertTxError<P>) -> Self {
- Self::Chain(inner)
- }
-}
-
-#[cfg(feature = "std")]
-impl<P: core::fmt::Debug> std::error::Error for InsertTxError<P> {}
-
-/// A nice alias of [`sparse_chain::InsertCheckpointError`].
-pub type InsertCheckpointError = sparse_chain::InsertCheckpointError;
-
-/// Represents an update failure.
-#[derive(Clone, Debug, PartialEq)]
-pub enum UpdateError<P> {
- /// The update chain was inconsistent with the existing chain
- Chain(sparse_chain::UpdateError<P>),
- /// A transaction in the update spent the same input as an already confirmed transaction
- UnresolvableConflict(UnresolvableConflict<P>),
-}
-
-impl<P: core::fmt::Debug> core::fmt::Display for UpdateError<P> {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- match self {
- UpdateError::Chain(inner) => core::fmt::Display::fmt(inner, f),
- UpdateError::UnresolvableConflict(inner) => core::fmt::Display::fmt(inner, f),
- }
- }
-}
-
-impl<P> From<sparse_chain::UpdateError<P>> for UpdateError<P> {
- fn from(inner: sparse_chain::UpdateError<P>) -> Self {
- Self::Chain(inner)
- }
-}
-
-#[cfg(feature = "std")]
-impl<P: core::fmt::Debug> std::error::Error for UpdateError<P> {}
-
-/// Represents an unresolvable conflict between an update's transaction and an
-/// already-confirmed transaction.
-#[derive(Clone, Debug, PartialEq)]
-pub struct UnresolvableConflict<P> {
- pub already_confirmed_tx: (P, Txid),
- pub update_tx: (P, Txid),
-}
-
-impl<P: core::fmt::Debug> core::fmt::Display for UnresolvableConflict<P> {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- let Self {
- already_confirmed_tx,
- update_tx,
- } = self;
- write!(f, "update transaction {} at height {:?} conflicts with an already confirmed transaction {} at height {:?}",
- update_tx.1, update_tx.0, already_confirmed_tx.1, already_confirmed_tx.0)
- }
-}
-
-impl<P> From<UnresolvableConflict<P>> for UpdateError<P> {
- fn from(inner: UnresolvableConflict<P>) -> Self {
- Self::UnresolvableConflict(inner)
- }
-}
-
-impl<P> From<UnresolvableConflict<P>> for InsertTxError<P> {
- fn from(inner: UnresolvableConflict<P>) -> Self {
- Self::UnresolvableConflict(inner)
- }
-}
-
-#[cfg(feature = "std")]
-impl<P: core::fmt::Debug> std::error::Error for UnresolvableConflict<P> {}
//! has a `txout` containing an indexed script pubkey). Internally, this uses [`SpkTxOutIndex`], but
//! also maintains "revealed" and "lookahead" index counts per keychain.
//!
-//! [`KeychainTracker`] combines [`ChainGraph`] and [`KeychainTxOutIndex`] and enforces atomic
-//! changes between both these structures. [`KeychainScan`] is a structure used to update to
-//! [`KeychainTracker`] and changes made on a [`KeychainTracker`] are reported by
-//! [`KeychainChangeSet`]s.
-//!
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
use crate::{
- chain_graph::{self, ChainGraph},
collections::BTreeMap,
indexed_tx_graph::IndexedAdditions,
local_chain::{self, LocalChain},
- sparse_chain::ChainPosition,
tx_graph::TxGraph,
- Anchor, Append, ForEachTxOut,
+ Anchor, Append,
};
-#[cfg(feature = "miniscript")]
-pub mod persist;
-#[cfg(feature = "miniscript")]
-pub use persist::*;
-#[cfg(feature = "miniscript")]
-mod tracker;
-#[cfg(feature = "miniscript")]
-pub use tracker::*;
#[cfg(feature = "miniscript")]
mod txout_index;
#[cfg(feature = "miniscript")]
}
}
-#[derive(Clone, Debug, PartialEq)]
-/// An update that includes the last active indexes of each keychain.
-pub struct KeychainScan<K, P> {
- /// The update data in the form of a chain that could be applied
- pub update: ChainGraph<P>,
- /// The last active indexes of each keychain
- pub last_active_indices: BTreeMap<K, u32>,
-}
-
-impl<K, P> Default for KeychainScan<K, P> {
- fn default() -> Self {
- Self {
- update: Default::default(),
- last_active_indices: Default::default(),
- }
- }
-}
-
-impl<K, P> From<ChainGraph<P>> for KeychainScan<K, P> {
- fn from(update: ChainGraph<P>) -> Self {
- KeychainScan {
- update,
- last_active_indices: Default::default(),
- }
- }
-}
-
-/// Represents changes to a [`KeychainTracker`].
-///
-/// This is essentially a combination of [`DerivationAdditions`] and [`chain_graph::ChangeSet`].
-#[derive(Clone, Debug)]
-#[cfg_attr(
- feature = "serde",
- derive(serde::Deserialize, serde::Serialize),
- serde(
- crate = "serde_crate",
- bound(
- deserialize = "K: Ord + serde::Deserialize<'de>, P: serde::Deserialize<'de>",
- serialize = "K: Ord + serde::Serialize, P: serde::Serialize"
- )
- )
-)]
-#[must_use]
-pub struct KeychainChangeSet<K, P> {
- /// The changes in local keychain derivation indices
- pub derivation_indices: DerivationAdditions<K>,
- /// The changes that have occurred in the blockchain
- pub chain_graph: chain_graph::ChangeSet<P>,
-}
-
-impl<K, P> Default for KeychainChangeSet<K, P> {
- fn default() -> Self {
- Self {
- chain_graph: Default::default(),
- derivation_indices: Default::default(),
- }
- }
-}
-
-impl<K, P> KeychainChangeSet<K, P> {
- /// Returns whether the [`KeychainChangeSet`] is empty (no changes recorded).
- pub fn is_empty(&self) -> bool {
- self.chain_graph.is_empty() && self.derivation_indices.is_empty()
- }
-
- /// Appends the changes in `other` into `self` such that applying `self` afterward has the same
- /// effect as sequentially applying the original `self` and `other`.
- ///
- /// Note the derivation indices cannot be decreased, so `other` will only change the derivation
- /// index for a keychain, if it's value is higher than the one in `self`.
- pub fn append(&mut self, other: KeychainChangeSet<K, P>)
- where
- K: Ord,
- P: ChainPosition,
- {
- self.derivation_indices.append(other.derivation_indices);
- self.chain_graph.append(other.chain_graph);
- }
-}
-
-impl<K, P> From<chain_graph::ChangeSet<P>> for KeychainChangeSet<K, P> {
- fn from(changeset: chain_graph::ChangeSet<P>) -> Self {
- Self {
- chain_graph: changeset,
- ..Default::default()
- }
- }
-}
-
-impl<K, P> From<DerivationAdditions<K>> for KeychainChangeSet<K, P> {
- fn from(additions: DerivationAdditions<K>) -> Self {
- Self {
- derivation_indices: additions,
- ..Default::default()
- }
- }
-}
-
-impl<K, P> AsRef<TxGraph> for KeychainScan<K, P> {
- fn as_ref(&self) -> &TxGraph {
- self.update.graph()
- }
-}
-
-impl<K, P> ForEachTxOut for KeychainChangeSet<K, P> {
- fn for_each_txout(&self, f: impl FnMut((bitcoin::OutPoint, &bitcoin::TxOut))) {
- self.chain_graph.for_each_txout(f)
- }
-}
-
/// Balance, differentiated into various categories.
#[derive(Debug, PartialEq, Eq, Clone, Default)]
#[cfg_attr(
#[cfg(test)]
mod test {
- use crate::TxHeight;
-
use super::*;
+
#[test]
fn append_keychain_derivation_indices() {
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
rhs_di.insert(Keychain::Two, 5);
lhs_di.insert(Keychain::Three, 3);
rhs_di.insert(Keychain::Four, 4);
- let mut lhs = KeychainChangeSet {
- derivation_indices: DerivationAdditions(lhs_di),
- chain_graph: chain_graph::ChangeSet::<TxHeight>::default(),
- };
-
- let rhs = KeychainChangeSet {
- derivation_indices: DerivationAdditions(rhs_di),
- chain_graph: chain_graph::ChangeSet::<TxHeight>::default(),
- };
+ let mut lhs = DerivationAdditions(lhs_di);
+ let rhs = DerivationAdditions(rhs_di);
lhs.append(rhs);
// Exiting index doesn't update if the new index in `other` is lower than `self`.
- assert_eq!(lhs.derivation_indices.0.get(&Keychain::One), Some(&7));
+ assert_eq!(lhs.0.get(&Keychain::One), Some(&7));
// Existing index updates if the new index in `other` is higher than `self`.
- assert_eq!(lhs.derivation_indices.0.get(&Keychain::Two), Some(&5));
+ assert_eq!(lhs.0.get(&Keychain::Two), Some(&5));
// Existing index is unchanged if keychain doesn't exist in `other`.
- assert_eq!(lhs.derivation_indices.0.get(&Keychain::Three), Some(&3));
+ assert_eq!(lhs.0.get(&Keychain::Three), Some(&3));
// New keychain gets added if the keychain is in `other` but not in `self`.
- assert_eq!(lhs.derivation_indices.0.get(&Keychain::Four), Some(&4));
+ assert_eq!(lhs.0.get(&Keychain::Four), Some(&4));
}
}
+++ /dev/null
-//! Persistence for changes made to a [`KeychainTracker`].
-//!
-//! BDK's [`KeychainTracker`] needs somewhere to persist changes it makes during operation.
-//! Operations like giving out a new address are crucial to persist so that next time the
-//! application is loaded, it can find transactions related to that address.
-//!
-//! Note that the [`KeychainTracker`] does not read this persisted data during operation since it
-//! always has a copy in memory.
-//!
-//! [`KeychainTracker`]: crate::keychain::KeychainTracker
-
-use crate::{keychain, sparse_chain::ChainPosition};
-
-/// `Persist` wraps a [`PersistBackend`] to create a convenient staging area for changes before they
-/// are persisted. Not all changes made to the [`KeychainTracker`] need to be written to disk right
-/// away so you can use [`Persist::stage`] to *stage* it first and then [`Persist::commit`] to
-/// finally, write it to disk.
-///
-/// [`KeychainTracker`]: keychain::KeychainTracker
-#[derive(Debug)]
-pub struct Persist<K, P, B> {
- backend: B,
- stage: keychain::KeychainChangeSet<K, P>,
-}
-
-impl<K, P, B> Persist<K, P, B> {
- /// Create a new `Persist` from a [`PersistBackend`].
- pub fn new(backend: B) -> Self {
- Self {
- backend,
- stage: Default::default(),
- }
- }
-
- /// Stage a `changeset` to later persistence with [`commit`].
- ///
- /// [`commit`]: Self::commit
- pub fn stage(&mut self, changeset: keychain::KeychainChangeSet<K, P>)
- where
- K: Ord,
- P: ChainPosition,
- {
- self.stage.append(changeset)
- }
-
- /// Get the changes that haven't been committed yet
- pub fn staged(&self) -> &keychain::KeychainChangeSet<K, P> {
- &self.stage
- }
-
- /// Commit the staged changes to the underlying persistence backend.
- ///
- /// Returns a backend-defined error if this fails.
- pub fn commit(&mut self) -> Result<(), B::WriteError>
- where
- B: PersistBackend<K, P>,
- {
- self.backend.append_changeset(&self.stage)?;
- self.stage = Default::default();
- Ok(())
- }
-}
-
-/// A persistence backend for [`Persist`].
-pub trait PersistBackend<K, P> {
- /// The error the backend returns when it fails to write.
- type WriteError: core::fmt::Debug;
-
- /// The error the backend returns when it fails to load.
- type LoadError: core::fmt::Debug;
-
- /// Appends a new changeset to the persistent backend.
- ///
- /// It is up to the backend what it does with this. It could store every changeset in a list or
- /// it inserts the actual changes into a more structured database. All it needs to guarantee is
- /// that [`load_into_keychain_tracker`] restores a keychain tracker to what it should be if all
- /// changesets had been applied sequentially.
- ///
- /// [`load_into_keychain_tracker`]: Self::load_into_keychain_tracker
- fn append_changeset(
- &mut self,
- changeset: &keychain::KeychainChangeSet<K, P>,
- ) -> Result<(), Self::WriteError>;
-
- /// Applies all the changesets the backend has received to `tracker`.
- fn load_into_keychain_tracker(
- &mut self,
- tracker: &mut keychain::KeychainTracker<K, P>,
- ) -> Result<(), Self::LoadError>;
-}
-
-impl<K, P> PersistBackend<K, P> for () {
- type WriteError = ();
- type LoadError = ();
-
- fn append_changeset(
- &mut self,
- _changeset: &keychain::KeychainChangeSet<K, P>,
- ) -> Result<(), Self::WriteError> {
- Ok(())
- }
- fn load_into_keychain_tracker(
- &mut self,
- _tracker: &mut keychain::KeychainTracker<K, P>,
- ) -> Result<(), Self::LoadError> {
- Ok(())
- }
-}
+++ /dev/null
-use bitcoin::Transaction;
-use miniscript::{Descriptor, DescriptorPublicKey};
-
-use crate::{
- chain_graph::{self, ChainGraph},
- collections::*,
- keychain::{KeychainChangeSet, KeychainScan, KeychainTxOutIndex},
- sparse_chain::{self, SparseChain},
- tx_graph::TxGraph,
- BlockId, FullTxOut, TxHeight,
-};
-
-use super::{Balance, DerivationAdditions};
-
-/// A convenient combination of a [`KeychainTxOutIndex`] and a [`ChainGraph`].
-///
-/// The [`KeychainTracker`] atomically updates its [`KeychainTxOutIndex`] whenever new chain data is
-/// incorporated into its internal [`ChainGraph`].
-#[derive(Clone, Debug)]
-pub struct KeychainTracker<K, P> {
- /// Index between script pubkeys to transaction outputs
- pub txout_index: KeychainTxOutIndex<K>,
- chain_graph: ChainGraph<P>,
-}
-
-impl<K, P> KeychainTracker<K, P>
-where
- P: sparse_chain::ChainPosition,
- K: Ord + Clone + core::fmt::Debug,
-{
- /// Add a keychain to the tracker's `txout_index` with a descriptor to derive addresses.
- /// This is just shorthand for calling [`KeychainTxOutIndex::add_keychain`] on the internal
- /// `txout_index`.
- ///
- /// Adding a keychain means you will be able to derive new script pubkeys under that keychain
- /// and the tracker will discover transaction outputs with those script pubkeys.
- pub fn add_keychain(&mut self, keychain: K, descriptor: Descriptor<DescriptorPublicKey>) {
- self.txout_index.add_keychain(keychain, descriptor)
- }
-
- /// Get the internal map of keychains to their descriptors. This is just shorthand for calling
- /// [`KeychainTxOutIndex::keychains`] on the internal `txout_index`.
- pub fn keychains(&mut self) -> &BTreeMap<K, Descriptor<DescriptorPublicKey>> {
- self.txout_index.keychains()
- }
-
- /// Get the checkpoint limit of the internal [`SparseChain`].
- ///
- /// Refer to [`SparseChain::checkpoint_limit`] for more.
- pub fn checkpoint_limit(&self) -> Option<usize> {
- self.chain_graph.checkpoint_limit()
- }
-
- /// Set the checkpoint limit of the internal [`SparseChain`].
- ///
- /// Refer to [`SparseChain::set_checkpoint_limit`] for more.
- pub fn set_checkpoint_limit(&mut self, limit: Option<usize>) {
- self.chain_graph.set_checkpoint_limit(limit)
- }
-
- /// Determines the resultant [`KeychainChangeSet`] if the given [`KeychainScan`] is applied.
- ///
- /// Internally, we call [`ChainGraph::determine_changeset`] and also determine the additions of
- /// [`KeychainTxOutIndex`].
- pub fn determine_changeset(
- &self,
- scan: &KeychainScan<K, P>,
- ) -> Result<KeychainChangeSet<K, P>, chain_graph::UpdateError<P>> {
- // TODO: `KeychainTxOutIndex::determine_additions`
- let mut derivation_indices = scan.last_active_indices.clone();
- derivation_indices.retain(|keychain, index| {
- match self.txout_index.last_revealed_index(keychain) {
- Some(existing) => *index > existing,
- None => true,
- }
- });
-
- Ok(KeychainChangeSet {
- derivation_indices: DerivationAdditions(derivation_indices),
- chain_graph: self.chain_graph.determine_changeset(&scan.update)?,
- })
- }
-
- /// Directly applies a [`KeychainScan`] on [`KeychainTracker`].
- ///
- /// This is equivalent to calling [`determine_changeset`] and [`apply_changeset`] in sequence.
- ///
- /// [`determine_changeset`]: Self::determine_changeset
- /// [`apply_changeset`]: Self::apply_changeset
- pub fn apply_update(
- &mut self,
- scan: KeychainScan<K, P>,
- ) -> Result<KeychainChangeSet<K, P>, chain_graph::UpdateError<P>> {
- let changeset = self.determine_changeset(&scan)?;
- self.apply_changeset(changeset.clone());
- Ok(changeset)
- }
-
- /// Applies the changes in `changeset` to [`KeychainTracker`].
- ///
- /// Internally, this calls [`KeychainTxOutIndex::apply_additions`] and
- /// [`ChainGraph::apply_changeset`] in sequence.
- pub fn apply_changeset(&mut self, changeset: KeychainChangeSet<K, P>) {
- let KeychainChangeSet {
- derivation_indices,
- chain_graph,
- } = changeset;
- self.txout_index.apply_additions(derivation_indices);
- let _ = self.txout_index.scan(&chain_graph);
- self.chain_graph.apply_changeset(chain_graph)
- }
-
- /// Iterates through [`FullTxOut`]s that are considered to exist in our representation of the
- /// blockchain/mempool.
- ///
- /// In other words, these are `txout`s of confirmed and in-mempool transactions, based on our
- /// view of the blockchain/mempool.
- pub fn full_txouts(&self) -> impl Iterator<Item = (&(K, u32), FullTxOut<P>)> + '_ {
- self.txout_index
- .txouts()
- .filter_map(move |(spk_i, op, _)| Some((spk_i, self.chain_graph.full_txout(op)?)))
- }
-
- /// Iterates through [`FullTxOut`]s that are unspent outputs.
- ///
- /// Refer to [`full_txouts`] for more.
- ///
- /// [`full_txouts`]: Self::full_txouts
- pub fn full_utxos(&self) -> impl Iterator<Item = (&(K, u32), FullTxOut<P>)> + '_ {
- self.full_txouts()
- .filter(|(_, txout)| txout.spent_by.is_none())
- }
-
- /// Returns a reference to the internal [`ChainGraph`].
- pub fn chain_graph(&self) -> &ChainGraph<P> {
- &self.chain_graph
- }
-
- /// Returns a reference to the internal [`TxGraph`] (which is part of the [`ChainGraph`]).
- pub fn graph(&self) -> &TxGraph {
- self.chain_graph().graph()
- }
-
- /// Returns a reference to the internal [`SparseChain`] (which is part of the [`ChainGraph`]).
- pub fn chain(&self) -> &SparseChain<P> {
- self.chain_graph().chain()
- }
-
- /// Determines the changes as a result of inserting `block_id` (a height and block hash) into the
- /// tracker.
- ///
- /// The caller is responsible for guaranteeing that a block exists at that height. If a
- /// checkpoint already exists at that height with a different hash; this will return an error.
- /// Otherwise it will return `Ok(true)` if the checkpoint didn't already exist or `Ok(false)`
- /// if it did.
- ///
- /// **Warning**: This function modifies the internal state of the tracker. You are responsible
- /// for persisting these changes to disk if you need to restore them.
- pub fn insert_checkpoint_preview(
- &self,
- block_id: BlockId,
- ) -> Result<KeychainChangeSet<K, P>, chain_graph::InsertCheckpointError> {
- Ok(KeychainChangeSet {
- chain_graph: self.chain_graph.insert_checkpoint_preview(block_id)?,
- ..Default::default()
- })
- }
-
- /// Directly insert a `block_id` into the tracker.
- ///
- /// This is equivalent of calling [`insert_checkpoint_preview`] and [`apply_changeset`] in
- /// sequence.
- ///
- /// [`insert_checkpoint_preview`]: Self::insert_checkpoint_preview
- /// [`apply_changeset`]: Self::apply_changeset
- pub fn insert_checkpoint(
- &mut self,
- block_id: BlockId,
- ) -> Result<KeychainChangeSet<K, P>, chain_graph::InsertCheckpointError> {
- let changeset = self.insert_checkpoint_preview(block_id)?;
- self.apply_changeset(changeset.clone());
- Ok(changeset)
- }
-
- /// Determines the changes as a result of inserting a transaction into the inner [`ChainGraph`]
- /// and optionally into the inner chain at `position`.
- ///
- /// **Warning**: This function modifies the internal state of the chain graph. You are
- /// responsible for persisting these changes to disk if you need to restore them.
- pub fn insert_tx_preview(
- &self,
- tx: Transaction,
- pos: P,
- ) -> Result<KeychainChangeSet<K, P>, chain_graph::InsertTxError<P>> {
- Ok(KeychainChangeSet {
- chain_graph: self.chain_graph.insert_tx_preview(tx, pos)?,
- ..Default::default()
- })
- }
-
- /// Directly insert a transaction into the inner [`ChainGraph`] and optionally into the inner
- /// chain at `position`.
- ///
- /// This is equivalent of calling [`insert_tx_preview`] and [`apply_changeset`] in sequence.
- ///
- /// [`insert_tx_preview`]: Self::insert_tx_preview
- /// [`apply_changeset`]: Self::apply_changeset
- pub fn insert_tx(
- &mut self,
- tx: Transaction,
- pos: P,
- ) -> Result<KeychainChangeSet<K, P>, chain_graph::InsertTxError<P>> {
- let changeset = self.insert_tx_preview(tx, pos)?;
- self.apply_changeset(changeset.clone());
- Ok(changeset)
- }
-
- /// Returns the *balance* of the keychain, i.e., the value of unspent transaction outputs tracked.
- ///
- /// The caller provides a `should_trust` predicate which must decide whether the value of
- /// unconfirmed outputs on this keychain are guaranteed to be realized or not. For example:
- ///
- /// - For an *internal* (change) keychain, `should_trust` should generally be `true` since even if
- /// you lose an internal output due to eviction, you will always gain back the value from whatever output the
- /// unconfirmed transaction was spending (since that output is presumably from your wallet).
- /// - For an *external* keychain, you might want `should_trust` to return `false` since someone may cancel (by double spending)
- /// a payment made to addresses on that keychain.
- ///
- /// When in doubt set `should_trust` to return false. This doesn't do anything other than change
- /// where the unconfirmed output's value is accounted for in `Balance`.
- pub fn balance(&self, mut should_trust: impl FnMut(&K) -> bool) -> Balance {
- let mut immature = 0;
- let mut trusted_pending = 0;
- let mut untrusted_pending = 0;
- let mut confirmed = 0;
- let last_sync_height = self.chain().latest_checkpoint().map(|latest| latest.height);
- for ((keychain, _), utxo) in self.full_utxos() {
- let chain_position = &utxo.chain_position;
-
- match chain_position.height() {
- TxHeight::Confirmed(_) => {
- if utxo.is_on_coinbase {
- if utxo.is_mature(
- last_sync_height
- .expect("since it's confirmed we must have a checkpoint"),
- ) {
- confirmed += utxo.txout.value;
- } else {
- immature += utxo.txout.value;
- }
- } else {
- confirmed += utxo.txout.value;
- }
- }
- TxHeight::Unconfirmed => {
- if should_trust(keychain) {
- trusted_pending += utxo.txout.value;
- } else {
- untrusted_pending += utxo.txout.value;
- }
- }
- }
- }
-
- Balance {
- immature,
- trusted_pending,
- untrusted_pending,
- confirmed,
- }
- }
-
- /// Returns the balance of all spendable confirmed unspent outputs of this tracker at a
- /// particular height.
- pub fn balance_at(&self, height: u32) -> u64 {
- self.full_txouts()
- .filter(|(_, full_txout)| full_txout.is_spendable_at(height))
- .map(|(_, full_txout)| full_txout.txout.value)
- .sum()
- }
-}
-
-impl<K, P> Default for KeychainTracker<K, P> {
- fn default() -> Self {
- Self {
- txout_index: Default::default(),
- chain_graph: Default::default(),
- }
- }
-}
-
-impl<K, P> AsRef<SparseChain<P>> for KeychainTracker<K, P> {
- fn as_ref(&self) -> &SparseChain<P> {
- self.chain_graph.chain()
- }
-}
-
-impl<K, P> AsRef<TxGraph> for KeychainTracker<K, P> {
- fn as_ref(&self) -> &TxGraph {
- self.chain_graph.graph()
- }
-}
-
-impl<K, P> AsRef<ChainGraph<P>> for KeychainTracker<K, P> {
- fn as_ref(&self) -> &ChainGraph<P> {
- &self.chain_graph
- }
-}
///
/// This will panic if a different `descriptor` is introduced to the same `keychain`.
pub fn add_keychain(&mut self, keychain: K, descriptor: Descriptor<DescriptorPublicKey>) {
- let old_descriptor = &*self.keychains.entry(keychain).or_insert(descriptor.clone());
+ let old_descriptor = &*self
+ .keychains
+ .entry(keychain)
+ .or_insert_with(|| descriptor.clone());
assert_eq!(
&descriptor, old_descriptor,
"keychain already contains a different descriptor"
//! [Bitcoin Dev Kit]: https://bitcoindevkit.org/
#![no_std]
pub use bitcoin;
-pub mod chain_graph;
mod spk_txout_index;
pub use spk_txout_index::*;
mod chain_data;
pub mod indexed_tx_graph;
pub mod keychain;
pub mod local_chain;
-pub mod sparse_chain;
mod tx_data_traits;
pub mod tx_graph;
pub use tx_data_traits::*;
+++ /dev/null
-//! Module for structures that maintain sparse (purposely incomplete) snapshots of blockchain data.
-//!
-//! [`SparseChain`] stores [`Txid`]s ordered by an index that implements [`ChainPosition`] (this
-//! represents the transaction's position in the blockchain; by default, [`TxHeight`] is used).
-//! [`SparseChain`] also contains "checkpoints" which relate block height to block hash. Changes to
-//! a [`SparseChain`] is reported by returning [`ChangeSet`]s.
-//!
-//! # Updating [`SparseChain`]
-//!
-//! A sparsechain can be thought of as a consistent snapshot of history. A [`SparseChain`] can be
-//! updated by applying an update [`SparseChain`] on top, but only if they "connect" via their
-//! checkpoints and don't result in unexpected movements of transactions.
-//!
-//! ```
-//! # use bdk_chain::{BlockId, TxHeight, sparse_chain::*, example_utils::*};
-//! # use bitcoin::BlockHash;
-//! # let hash_a = new_hash::<BlockHash>("a");
-//! # let hash_b = new_hash::<BlockHash>("b");
-//! # let hash_c = new_hash::<BlockHash>("c");
-//! # let hash_d = new_hash::<BlockHash>("d");
-//! // create empty sparsechain
-//! let mut chain = SparseChain::<TxHeight>::default();
-//!
-//! /* Updating an empty sparsechain will always succeed */
-//!
-//! let update = SparseChain::from_checkpoints(vec![
-//! BlockId {
-//! height: 1,
-//! hash: hash_a,
-//! },
-//! BlockId {
-//! height: 2,
-//! hash: hash_b,
-//! },
-//! ]);
-//! let _ = chain
-//! .apply_update(update)
-//! .expect("updating an empty sparsechain will always succeed");
-//!
-//! /* To update a non-empty sparsechain, the update must connect */
-//!
-//! let update = SparseChain::from_checkpoints(vec![
-//! BlockId {
-//! height: 2,
-//! hash: hash_b,
-//! },
-//! BlockId {
-//! height: 3,
-//! hash: hash_c,
-//! },
-//! ]);
-//! let _ = chain
-//! .apply_update(update)
-//! .expect("we have connected at block height 2, so this must succeed");
-//! ```
-//!
-//! ## Invalid updates
-//!
-//! As shown above, sparsechains can be "connected" by comparing their checkpoints. However, there
-//! are situations where two sparsechains cannot connect in a way that guarantees consistency.
-//!
-//! ```
-//! # use bdk_chain::{BlockId, TxHeight, sparse_chain::*, example_utils::*};
-//! # use bitcoin::BlockHash;
-//! # let hash_a = new_hash::<BlockHash>("a");
-//! # let hash_b = new_hash::<BlockHash>("b");
-//! # let hash_c = new_hash::<BlockHash>("c");
-//! # let hash_d = new_hash::<BlockHash>("d");
-//! // our sparsechain has two checkpoints
-//! let chain = SparseChain::<TxHeight>::from_checkpoints(vec![
-//! BlockId {
-//! height: 1,
-//! hash: hash_a,
-//! },
-//! BlockId {
-//! height: 2,
-//! hash: hash_b,
-//! },
-//! ]);
-//!
-//! /* Example of an ambiguous update that does not fully connect */
-//!
-//! let ambiguous_update = SparseChain::from_checkpoints(vec![
-//! // the update sort of "connects" at checkpoint 1, but...
-//! BlockId {
-//! height: 1,
-//! hash: hash_a,
-//! },
-//! // we cannot determine whether checkpoint 3 connects with checkpoint 2
-//! BlockId {
-//! height: 3,
-//! hash: hash_c,
-//! },
-//! ]);
-//! let _ = chain
-//! .determine_changeset(&ambiguous_update)
-//! .expect_err("cannot apply ambiguous update");
-//!
-//! /* Example of an update that completely misses the point */
-//!
-//! let disconnected_update = SparseChain::from_checkpoints(vec![
-//! // the last checkpoint in the chain is 2, so 3 and 4 do not connect
-//! BlockId {
-//! height: 3,
-//! hash: hash_c,
-//! },
-//! BlockId {
-//! height: 4,
-//! hash: hash_d,
-//! },
-//! ]);
-//! let _ = chain
-//! .determine_changeset(&disconnected_update)
-//! .expect_err("cannot apply a totally-disconnected update");
-//! ```
-//!
-//! ## Handling reorgs
-//!
-//! Updates can be formed to evict data from the original sparsechain. This is useful for handling
-//! blockchain reorgs.
-//!
-//! ```
-//! # use bdk_chain::{BlockId, TxHeight, sparse_chain::*, example_utils::*};
-//! # use bitcoin::BlockHash;
-//! # let hash_a = new_hash::<BlockHash>("a");
-//! # let hash_b = new_hash::<BlockHash>("b");
-//! # let hash_c = new_hash::<BlockHash>("c");
-//! # let hash_d = new_hash::<BlockHash>("d");
-//! // our chain has a single checkpoint at height 11.
-//! let mut chain = SparseChain::<TxHeight>::from_checkpoints(vec![BlockId {
-//! height: 11,
-//! hash: hash_a,
-//! }]);
-//!
-//! // we detect a reorg at height 11, and we introduce a new checkpoint at height 12
-//! let update = SparseChain::from_checkpoints(vec![
-//! BlockId {
-//! height: 11,
-//! hash: hash_b,
-//! },
-//! BlockId {
-//! height: 12,
-//! hash: hash_c,
-//! },
-//! ]);
-//! let _ = chain
-//! .apply_update(update)
-//! .expect("we can evict/replace checkpoint 11 since it is the only checkpoint");
-//!
-//! // now our `chain` has two checkpoints (11:hash_b & 12:hash_c)
-//! // we detect another reorg, this time at height 12.
-//! let update = SparseChain::from_checkpoints(vec![
-//! // we connect at checkpoint 11 as this is our "point of agreement".
-//! BlockId {
-//! height: 11,
-//! hash: hash_b,
-//! },
-//! BlockId {
-//! height: 12,
-//! hash: hash_d,
-//! },
-//! ]);
-//! let _ = chain
-//! .apply_update(update)
-//! .expect("we have provided a valid point of agreement, so our reorg update will succeed");
-//! ```
-//!
-//! ## Movement of transactions during update
-//!
-//! If the original sparsechain and update sparsechain contain the same transaction at different
-//! [`ChainPosition`]s, the transaction is considered as "moved". There are various movements of a
-//! transaction that are invalid and update will fail.
-//!
-//! Valid movements:
-//!
-//! * When the transaction moved from unconfirmed (in original) to confirmed (in update). In other
-//! words, confirming transactions are allowed!
-//! * If there has been a reorg at height x, an originally confirmed transaction at height x or
-//! above, may move to another height (that is at x or above, including becoming unconfirmed).
-//!
-//! Invalid movements:
-//!
-//! * A confirmed transaction cannot move without a reorg.
-//! * Even with a reorg, an originally confirmed transaction cannot be moved below the height of the
-//! reorg.
-//!
-//! # Custom [`ChainPosition`]
-//!
-//! [`SparseChain`] maintains a list of txids ordered by [`ChainPosition`]. By default, [`TxHeight`]
-//! is used; however, additional data can be incorporated into the implementation.
-//!
-//! For example, we can have "perfect ordering" of transactions if our positional index is a
-//! combination of block height and transaction position in a block.
-//!
-//! ```
-//! # use bdk_chain::{BlockId, TxHeight, sparse_chain::*, example_utils::*};
-//! # use bitcoin::{BlockHash, Txid};
-//! # let hash_a = new_hash::<BlockHash>("a");
-//! # let txid_1 = new_hash::<Txid>("1");
-//! # let txid_2 = new_hash::<Txid>("2");
-//! # let txid_3 = new_hash::<Txid>("3");
-//! #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-//! pub enum TxPosition {
-//! Confirmed {
-//! height: u32, // height of block
-//! position: u32, // position of transaction in the block
-//! },
-//! Unconfirmed,
-//! }
-//!
-//! impl Default for TxPosition {
-//! fn default() -> Self {
-//! Self::Unconfirmed
-//! }
-//! }
-//!
-//! impl ChainPosition for TxPosition {
-//! fn height(&self) -> TxHeight {
-//! match self {
-//! Self::Confirmed { height, .. } => TxHeight::Confirmed(*height),
-//! Self::Unconfirmed => TxHeight::Unconfirmed,
-//! }
-//! }
-//!
-//! fn max_ord_of_height(height: TxHeight) -> Self {
-//! match height {
-//! TxHeight::Confirmed(height) => Self::Confirmed {
-//! height,
-//! position: u32::MAX,
-//! },
-//! TxHeight::Unconfirmed => Self::Unconfirmed,
-//! }
-//! }
-//!
-//! fn min_ord_of_height(height: TxHeight) -> Self {
-//! match height {
-//! TxHeight::Confirmed(height) => Self::Confirmed {
-//! height,
-//! position: u32::MIN,
-//! },
-//! TxHeight::Unconfirmed => Self::Unconfirmed,
-//! }
-//! }
-//! }
-//!
-//! let mut chain = SparseChain::<TxPosition>::default();
-//! let _ = chain
-//! .insert_checkpoint(BlockId {
-//! height: 10,
-//! hash: hash_a,
-//! })
-//! .unwrap();
-//! let _ = chain
-//! .insert_tx(
-//! txid_1,
-//! TxPosition::Confirmed {
-//! height: 9,
-//! position: 4321,
-//! },
-//! )
-//! .unwrap();
-//! let _ = chain
-//! .insert_tx(
-//! txid_2,
-//! TxPosition::Confirmed {
-//! height: 9,
-//! position: 1234,
-//! },
-//! )
-//! .unwrap();
-//! let _ = chain
-//! .insert_tx(
-//! txid_3,
-//! TxPosition::Confirmed {
-//! height: 10,
-//! position: 321,
-//! },
-//! )
-//! .unwrap();
-//!
-//! // transactions are ordered correctly
-//! assert_eq!(
-//! chain.txids().collect::<Vec<_>>(),
-//! vec![
-//! &(
-//! TxPosition::Confirmed {
-//! height: 9,
-//! position: 1234
-//! },
-//! txid_2
-//! ),
-//! &(
-//! TxPosition::Confirmed {
-//! height: 9,
-//! position: 4321
-//! },
-//! txid_1
-//! ),
-//! &(
-//! TxPosition::Confirmed {
-//! height: 10,
-//! position: 321
-//! },
-//! txid_3
-//! ),
-//! ],
-//! );
-//! ```
-use core::{
- fmt::Debug,
- ops::{Bound, RangeBounds},
-};
-
-use crate::{collections::*, tx_graph::TxGraph, BlockId, FullTxOut, TxHeight};
-use bitcoin::{hashes::Hash, BlockHash, OutPoint, Txid};
-
-/// This is a non-monotone structure that tracks relevant [`Txid`]s that are ordered by chain
-/// position `P`.
-///
-/// We use [`BlockHash`]s alongside their chain height as "checkpoints" to enforce consistency.
-///
-/// To "merge" two [`SparseChain`]s, the [`ChangeSet`] can be calculated by calling
-/// [`determine_changeset`] and applying the [`ChangeSet`] via [`apply_changeset`]. For convenience,
-/// [`apply_update`] does the above two steps in one call.
-///
-/// Refer to [module-level documentation] for more.
-///
-/// [`determine_changeset`]: Self::determine_changeset
-/// [`apply_changeset`]: Self::apply_changeset
-/// [`apply_update`]: Self::apply_update
-/// [module-level documentation]: crate::sparse_chain
-#[derive(Clone, Debug, PartialEq)]
-pub struct SparseChain<P = TxHeight> {
- /// Block height to checkpoint data.
- checkpoints: BTreeMap<u32, BlockHash>,
- /// Txids ordered by the pos `P`.
- ordered_txids: BTreeSet<(P, Txid)>,
- /// Confirmation heights of txids.
- txid_to_pos: HashMap<Txid, P>,
- /// Limit the number of checkpoints.
- checkpoint_limit: Option<usize>,
-}
-
-impl<P> AsRef<SparseChain<P>> for SparseChain<P> {
- fn as_ref(&self) -> &SparseChain<P> {
- self
- }
-}
-
-impl<P> Default for SparseChain<P> {
- fn default() -> Self {
- Self {
- checkpoints: Default::default(),
- ordered_txids: Default::default(),
- txid_to_pos: Default::default(),
- checkpoint_limit: Default::default(),
- }
- }
-}
-
-/// Represents a failure when trying to insert a [`Txid`] into [`SparseChain`].
-#[derive(Clone, Debug, PartialEq)]
-pub enum InsertTxError<P> {
- /// Occurs when the [`Txid`] is to be inserted at a height higher than the [`SparseChain`]'s tip.
- TxTooHigh {
- txid: Txid,
- tx_height: u32,
- tip_height: Option<u32>,
- },
- /// Occurs when the [`Txid`] is already in the [`SparseChain`], and the insertion would result in
- /// an unexpected move in [`ChainPosition`].
- TxMovedUnexpectedly {
- txid: Txid,
- original_pos: P,
- update_pos: P,
- },
-}
-
-impl<P: core::fmt::Debug> core::fmt::Display for InsertTxError<P> {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- match self {
- InsertTxError::TxTooHigh {
- txid,
- tx_height,
- tip_height,
- } => write!(
- f,
- "txid ({}) cannot be inserted at height ({}) greater than chain tip ({:?})",
- txid, tx_height, tip_height
- ),
- InsertTxError::TxMovedUnexpectedly {
- txid,
- original_pos,
- update_pos,
- } => write!(
- f,
- "txid ({}) insertion resulted in an expected positional move from {:?} to {:?}",
- txid, original_pos, update_pos
- ),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl<P: core::fmt::Debug> std::error::Error for InsertTxError<P> {}
-
-/// Represents a failure when trying to insert a checkpoint into [`SparseChain`].
-#[derive(Clone, Debug, PartialEq)]
-pub enum InsertCheckpointError {
- /// Occurs when a checkpoint of the same height already exists with a different [`BlockHash`].
- HashNotMatching {
- height: u32,
- original_hash: BlockHash,
- update_hash: BlockHash,
- },
-}
-
-impl core::fmt::Display for InsertCheckpointError {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- write!(f, "{:?}", self)
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for InsertCheckpointError {}
-
-/// Represents an update failure of [`SparseChain`].
-#[derive(Clone, Debug, PartialEq)]
-pub enum UpdateError<P = TxHeight> {
- /// The update cannot be applied to the chain because the chain suffix it represents did not
- /// connect to the existing chain. This error case contains the checkpoint height to include so
- /// that the chains can connect.
- NotConnected(u32),
- /// The update contains inconsistent tx states (e.g., it changed the transaction's height). This
- /// error is usually the inconsistency found.
- TxInconsistent {
- txid: Txid,
- original_pos: P,
- update_pos: P,
- },
-}
-
-impl<P: core::fmt::Debug> core::fmt::Display for UpdateError<P> {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- match self {
- Self::NotConnected(h) =>
- write!(f, "the checkpoints in the update could not be connected to the checkpoints in the chain, try include checkpoint of height {} to connect",
- h),
- Self::TxInconsistent { txid, original_pos, update_pos } =>
- write!(f, "tx ({}) had position ({:?}), but is ({:?}) in the update",
- txid, original_pos, update_pos),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl<P: core::fmt::Debug> std::error::Error for UpdateError<P> {}
-
-impl<P: ChainPosition> SparseChain<P> {
- /// Creates a new chain from a list of block hashes and heights. The caller must guarantee they
- /// are in the same chain.
- pub fn from_checkpoints<C>(checkpoints: C) -> Self
- where
- C: IntoIterator<Item = BlockId>,
- {
- Self {
- checkpoints: checkpoints
- .into_iter()
- .map(|block_id| block_id.into())
- .collect(),
- ..Default::default()
- }
- }
-
- /// Get the checkpoint for the last known tip.
- pub fn latest_checkpoint(&self) -> Option<BlockId> {
- self.checkpoints
- .iter()
- .last()
- .map(|(&height, &hash)| BlockId { height, hash })
- }
-
- /// Get the checkpoint at the given height if it exists.
- pub fn checkpoint_at(&self, height: u32) -> Option<BlockId> {
- self.checkpoints
- .get(&height)
- .map(|&hash| BlockId { height, hash })
- }
-
- /// Return the [`ChainPosition`] of a `txid`.
- ///
- /// This returns [`None`] if the transaction does not exist.
- pub fn tx_position(&self, txid: Txid) -> Option<&P> {
- self.txid_to_pos.get(&txid)
- }
-
- /// Return a [`BTreeMap`] of all checkpoints (block hashes by height).
- pub fn checkpoints(&self) -> &BTreeMap<u32, BlockHash> {
- &self.checkpoints
- }
-
- /// Return an iterator over checkpoints in a height range, in ascending height order.
- pub fn range_checkpoints(
- &self,
- range: impl RangeBounds<u32>,
- ) -> impl DoubleEndedIterator<Item = BlockId> + '_ {
- self.checkpoints
- .range(range)
- .map(|(&height, &hash)| BlockId { height, hash })
- }
-
- /// 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
- /// that is exactly the same height as one of `self`'s checkpoints.
- ///
- /// To invalidate from a given checkpoint, `update` must contain a checkpoint of the same height
- /// but different hash. Invalidated checkpoints result in invalidated transactions becoming
- /// "unconfirmed".
- ///
- /// An error will be returned if an update results in inconsistencies or if the update does
- /// not correctly connect with `self`.
- ///
- /// Refer to [module-level documentation] for more.
- ///
- /// [module-level documentation]: crate::sparse_chain
- pub fn determine_changeset(&self, update: &Self) -> Result<ChangeSet<P>, UpdateError<P>> {
- let agreement_point = update
- .checkpoints
- .iter()
- .rev()
- .find(|&(height, hash)| self.checkpoints.get(height) == Some(hash))
- .map(|(&h, _)| h);
-
- let last_update_cp = update.checkpoints.iter().last().map(|(&h, _)| h);
-
- // the lower bound of the invalidation range
- let invalid_lb = if last_update_cp.is_none() || last_update_cp == agreement_point {
- // if the agreement point is the last update checkpoint, or there are no update checkpoints,
- // no invalidation is required
- u32::MAX
- } else {
- agreement_point.map(|h| h + 1).unwrap_or(0)
- };
-
- // the first checkpoint of the sparsechain to invalidate (if any)
- let invalid_from = self.checkpoints.range(invalid_lb..).next().map(|(&h, _)| h);
-
- // the first checkpoint to invalidate (if any) should be represented in the update
- if let Some(first_invalid) = invalid_from {
- if !update.checkpoints.contains_key(&first_invalid) {
- return Err(UpdateError::NotConnected(first_invalid));
- }
- }
-
- for (&txid, update_pos) in &update.txid_to_pos {
- // ensure all currently confirmed txs are still at the same height (unless they are
- // within invalidation range, or to be confirmed)
- if let Some(original_pos) = &self.txid_to_pos.get(&txid) {
- if original_pos.height() < TxHeight::Confirmed(invalid_lb)
- && original_pos != &update_pos
- {
- return Err(UpdateError::TxInconsistent {
- txid,
- original_pos: P::clone(original_pos),
- update_pos: update_pos.clone(),
- });
- }
- }
- }
-
- // create initial change-set based on checkpoints and txids that are to be "invalidated".
- let mut changeset = invalid_from
- .map(|from_height| self.invalidate_checkpoints_preview(from_height))
- .unwrap_or_default();
-
- for (&height, &new_hash) in &update.checkpoints {
- let original_hash = self.checkpoints.get(&height).cloned();
-
- let update_hash = *changeset
- .checkpoints
- .entry(height)
- .and_modify(|change| *change = Some(new_hash))
- .or_insert_with(|| Some(new_hash));
-
- if original_hash == update_hash {
- changeset.checkpoints.remove(&height);
- }
- }
-
- for (txid, new_pos) in &update.txid_to_pos {
- let original_pos = self.txid_to_pos.get(txid).cloned();
-
- let update_pos = changeset
- .txids
- .entry(*txid)
- .and_modify(|change| *change = Some(new_pos.clone()))
- .or_insert_with(|| Some(new_pos.clone()));
-
- if original_pos == *update_pos {
- changeset.txids.remove(txid);
- }
- }
-
- Ok(changeset)
- }
-
- /// Updates [`SparseChain`] with another chain that connects to it.
- ///
- /// This is equivilant to calling [`determine_changeset`] and [`apply_changeset`] in sequence.
- ///
- /// [`determine_changeset`]: Self::determine_changeset
- /// [`apply_changeset`]: Self::apply_changeset
- pub fn apply_update(&mut self, update: Self) -> Result<ChangeSet<P>, UpdateError<P>> {
- let changeset = self.determine_changeset(&update)?;
- self.apply_changeset(changeset.clone());
- Ok(changeset)
- }
-
- pub fn apply_changeset(&mut self, changeset: ChangeSet<P>) {
- for (height, update_hash) in changeset.checkpoints {
- let _original_hash = match update_hash {
- Some(update_hash) => self.checkpoints.insert(height, update_hash),
- None => self.checkpoints.remove(&height),
- };
- }
-
- for (txid, update_pos) in changeset.txids {
- let original_pos = self.txid_to_pos.remove(&txid);
-
- if let Some(pos) = original_pos {
- self.ordered_txids.remove(&(pos, txid));
- }
-
- if let Some(pos) = update_pos {
- self.txid_to_pos.insert(txid, pos.clone());
- self.ordered_txids.insert((pos.clone(), txid));
- }
- }
-
- self.prune_checkpoints();
- }
-
- /// Derives a [`ChangeSet`] that assumes that there are no preceding changesets.
- ///
- /// The changeset returned will record additions of all [`Txid`]s and checkpoints included in
- /// [`Self`].
- pub fn initial_changeset(&self) -> ChangeSet<P> {
- ChangeSet {
- checkpoints: self
- .checkpoints
- .iter()
- .map(|(height, hash)| (*height, Some(*hash)))
- .collect(),
- txids: self
- .ordered_txids
- .iter()
- .map(|(pos, txid)| (*txid, Some(pos.clone())))
- .collect(),
- }
- }
-
- /// Determines the [`ChangeSet`] when checkpoints `from_height` (inclusive) and above are
- /// invalidated. Displaced [`Txid`]s will be repositioned to [`TxHeight::Unconfirmed`].
- pub fn invalidate_checkpoints_preview(&self, from_height: u32) -> ChangeSet<P> {
- ChangeSet::<P> {
- checkpoints: self
- .checkpoints
- .range(from_height..)
- .map(|(height, _)| (*height, None))
- .collect(),
- // invalidated transactions become unconfirmed
- txids: self
- .range_txids_by_height(TxHeight::Confirmed(from_height)..TxHeight::Unconfirmed)
- .map(|(_, txid)| (*txid, Some(P::max_ord_of_height(TxHeight::Unconfirmed))))
- .collect(),
- }
- }
-
- /// Invalidate checkpoints `from_height` (inclusive) and above.
- ///
- /// This is equivalent to calling [`invalidate_checkpoints_preview`] and [`apply_changeset`] in
- /// sequence.
- ///
- /// [`invalidate_checkpoints_preview`]: Self::invalidate_checkpoints_preview
- /// [`apply_changeset`]: Self::apply_changeset
- pub fn invalidate_checkpoints(&mut self, from_height: u32) -> ChangeSet<P> {
- let changeset = self.invalidate_checkpoints_preview(from_height);
- self.apply_changeset(changeset.clone());
- changeset
- }
-
- /// Determines the [`ChangeSet`] when all transactions of height [`TxHeight::Unconfirmed`] are
- /// removed completely.
- pub fn clear_mempool_preview(&self) -> ChangeSet<P> {
- let mempool_range = &(
- P::min_ord_of_height(TxHeight::Unconfirmed),
- Txid::all_zeros(),
- )..;
-
- let txids = self
- .ordered_txids
- .range(mempool_range)
- .map(|(_, txid)| (*txid, None))
- .collect();
-
- ChangeSet::<P> {
- txids,
- ..Default::default()
- }
- }
-
- /// Clears all transactions of height [`TxHeight::Unconfirmed`].
- ///
- /// This is equivalent to calling [`clear_mempool_preview`] and [`apply_changeset`] in sequence.
- ///
- /// [`clear_mempool_preview`]: Self::clear_mempool_preview
- /// [`apply_changeset`]: Self::apply_changeset
- /// [`ChangeSet`].
- pub fn clear_mempool(&mut self) -> ChangeSet<P> {
- let changeset = self.clear_mempool_preview();
- self.apply_changeset(changeset.clone());
- changeset
- }
-
- /// Determines the resultant [`ChangeSet`] if [`Txid`] was inserted at position `pos`.
- ///
- /// Changes to the [`Txid`]'s position are allowed (under the rules noted in
- /// [module-level documentation]) and will be reflected in the [`ChangeSet`].
- ///
- /// [module-level documentation]: crate::sparse_chain
- pub fn insert_tx_preview(&self, txid: Txid, pos: P) -> Result<ChangeSet<P>, InsertTxError<P>> {
- let mut update = Self::default();
-
- if let Some(block_id) = self.latest_checkpoint() {
- let _old_hash = update.checkpoints.insert(block_id.height, block_id.hash);
- debug_assert!(_old_hash.is_none());
- }
-
- let tip_height = self.checkpoints.iter().last().map(|(h, _)| *h);
- if let TxHeight::Confirmed(tx_height) = pos.height() {
- if Some(tx_height) > tip_height {
- return Err(InsertTxError::TxTooHigh {
- txid,
- tx_height,
- tip_height,
- });
- }
- }
-
- let _old_pos = update.txid_to_pos.insert(txid, pos.clone());
- debug_assert!(_old_pos.is_none());
-
- let _inserted = update.ordered_txids.insert((pos, txid));
- debug_assert!(_inserted, "must insert tx");
-
- match self.determine_changeset(&update) {
- Ok(changeset) => Ok(changeset),
- Err(UpdateError::NotConnected(_)) => panic!("should always connect"),
- Err(UpdateError::TxInconsistent {
- txid: inconsistent_txid,
- original_pos,
- update_pos,
- }) => Err(InsertTxError::TxMovedUnexpectedly {
- txid: inconsistent_txid,
- original_pos,
- update_pos,
- }),
- }
- }
-
- /// Inserts a given [`Txid`] at `pos`.
- ///
- /// This is equivilant to calling [`insert_tx_preview`] and [`apply_changeset`] in sequence.
- ///
- /// [`insert_tx_preview`]: Self::insert_tx_preview
- /// [`apply_changeset`]: Self::apply_changeset
- pub fn insert_tx(&mut self, txid: Txid, pos: P) -> Result<ChangeSet<P>, InsertTxError<P>> {
- let changeset = self.insert_tx_preview(txid, pos)?;
- self.apply_changeset(changeset.clone());
- Ok(changeset)
- }
-
- /// Determines the resultant [`ChangeSet`] if [`BlockId`] was inserted.
- ///
- /// If the change would result in a change in block hash of a certain height, insertion would
- /// fail.
- pub fn insert_checkpoint_preview(
- &self,
- block_id: BlockId,
- ) -> Result<ChangeSet<P>, InsertCheckpointError> {
- let mut update = Self::default();
-
- if let Some(block_id) = self.latest_checkpoint() {
- let _old_hash = update.checkpoints.insert(block_id.height, block_id.hash);
- debug_assert!(_old_hash.is_none());
- }
-
- if let Some(original_hash) = update.checkpoints.insert(block_id.height, block_id.hash) {
- if original_hash != block_id.hash {
- return Err(InsertCheckpointError::HashNotMatching {
- height: block_id.height,
- original_hash,
- update_hash: block_id.hash,
- });
- }
- }
-
- match self.determine_changeset(&update) {
- Ok(changeset) => Ok(changeset),
- Err(UpdateError::NotConnected(_)) => panic!("error should have caught above"),
- Err(UpdateError::TxInconsistent { .. }) => panic!("should never add txs"),
- }
- }
-
- /// Insert a checkpoint ([`BlockId`]).
- ///
- /// This is equivalent to calling [`insert_checkpoint_preview`] and [`apply_changeset`] in
- /// sequence.
- ///
- /// [`insert_checkpoint_preview`]: Self::insert_checkpoint_preview
- /// [`apply_changeset`]: Self::apply_changeset
- pub fn insert_checkpoint(
- &mut self,
- block_id: BlockId,
- ) -> Result<ChangeSet<P>, InsertCheckpointError> {
- let changeset = self.insert_checkpoint_preview(block_id)?;
- self.apply_changeset(changeset.clone());
- Ok(changeset)
- }
-
- /// Iterate over all [`Txid`]s ordered by their [`ChainPosition`].
- pub fn txids(&self) -> impl DoubleEndedIterator<Item = &(P, Txid)> + ExactSizeIterator + '_ {
- self.ordered_txids.iter()
- }
-
- /// Iterate over a sub-range of positioned [`Txid`]s.
- pub fn range_txids<R>(&self, range: R) -> impl DoubleEndedIterator<Item = &(P, Txid)> + '_
- where
- R: RangeBounds<(P, Txid)>,
- {
- let map_bound = |b: Bound<&(P, Txid)>| match b {
- Bound::Included((pos, txid)) => Bound::Included((pos.clone(), *txid)),
- Bound::Excluded((pos, txid)) => Bound::Excluded((pos.clone(), *txid)),
- Bound::Unbounded => Bound::Unbounded,
- };
-
- self.ordered_txids
- .range((map_bound(range.start_bound()), map_bound(range.end_bound())))
- }
-
- /// Iterate over a sub-range of positioned [`Txid`]s, where the range is defined by
- /// [`ChainPosition`] only.
- pub fn range_txids_by_position<R>(
- &self,
- range: R,
- ) -> impl DoubleEndedIterator<Item = &(P, Txid)> + '_
- where
- R: RangeBounds<P>,
- {
- let map_bound = |b: Bound<&P>, inc: Txid, exc: Txid| match b {
- Bound::Included(pos) => Bound::Included((pos.clone(), inc)),
- Bound::Excluded(pos) => Bound::Excluded((pos.clone(), exc)),
- Bound::Unbounded => Bound::Unbounded,
- };
-
- self.ordered_txids.range((
- map_bound(range.start_bound(), min_txid(), max_txid()),
- map_bound(range.end_bound(), max_txid(), min_txid()),
- ))
- }
-
- /// Iterate over a sub-range of positioned [`Txid`]s, where the range is defined by [`TxHeight`]
- /// only.
- pub fn range_txids_by_height<R>(
- &self,
- range: R,
- ) -> impl DoubleEndedIterator<Item = &(P, Txid)> + '_
- where
- R: RangeBounds<TxHeight>,
- {
- let ord_it = |height, is_max| match is_max {
- true => P::max_ord_of_height(height),
- false => P::min_ord_of_height(height),
- };
-
- let map_bound = |b: Bound<&TxHeight>, inc: (bool, Txid), exc: (bool, Txid)| match b {
- Bound::Included(&h) => Bound::Included((ord_it(h, inc.0), inc.1)),
- Bound::Excluded(&h) => Bound::Excluded((ord_it(h, exc.0), exc.1)),
- Bound::Unbounded => Bound::Unbounded,
- };
-
- self.ordered_txids.range((
- map_bound(range.start_bound(), (false, min_txid()), (true, max_txid())),
- map_bound(range.end_bound(), (true, max_txid()), (false, min_txid())),
- ))
- }
-
- /// Attempt to retrieve a [`FullTxOut`] of the given `outpoint`.
- ///
- /// This will return `Some` only if the output's transaction is in both `self` and `graph`.
- pub fn full_txout(&self, graph: &TxGraph, outpoint: OutPoint) -> Option<FullTxOut<P>> {
- let chain_pos = self.tx_position(outpoint.txid)?;
-
- let tx = graph.get_tx(outpoint.txid)?;
- let is_on_coinbase = tx.is_coin_base();
- let txout = tx.output.get(outpoint.vout as usize)?.clone();
-
- let spent_by = self
- .spent_by(graph, outpoint)
- .map(|(pos, txid)| (pos.clone(), txid));
-
- Some(FullTxOut {
- outpoint,
- txout,
- chain_position: chain_pos.clone(),
- spent_by,
- is_on_coinbase,
- })
- }
-
- /// Returns the value set as the checkpoint limit.
- ///
- /// Refer to [`set_checkpoint_limit`].
- ///
- /// [`set_checkpoint_limit`]: Self::set_checkpoint_limit
- pub fn checkpoint_limit(&self) -> Option<usize> {
- self.checkpoint_limit
- }
-
- /// Set the checkpoint limit.
- ///
- /// The checkpoint limit restricts the number of checkpoints that can be stored in [`Self`].
- /// Oldest checkpoints are pruned first.
- pub fn set_checkpoint_limit(&mut self, limit: Option<usize>) {
- self.checkpoint_limit = limit;
- self.prune_checkpoints();
- }
-
- /// Return [`Txid`]s that would be added to the sparse chain if this `changeset` was applied.
- pub fn changeset_additions<'a>(
- &'a self,
- changeset: &'a ChangeSet<P>,
- ) -> impl Iterator<Item = Txid> + 'a {
- changeset
- .txids
- .iter()
- .filter(move |(&txid, pos)| {
- pos.is_some() /*it was not a deletion*/ &&
- self.tx_position(txid).is_none() /* we don't have the txid already */
- })
- .map(|(&txid, _)| txid)
- }
-
- fn prune_checkpoints(&mut self) -> Option<BTreeMap<u32, BlockHash>> {
- let limit = self.checkpoint_limit?;
-
- // find the last height to be pruned
- let last_height = *self.checkpoints.keys().rev().nth(limit)?;
- // first height to be kept
- let keep_height = last_height + 1;
-
- let mut split = self.checkpoints.split_off(&keep_height);
- core::mem::swap(&mut self.checkpoints, &mut split);
-
- Some(split)
- }
-
- /// Finds the transaction in the chain that spends `outpoint`.
- ///
- /// [`TxGraph`] is used to provide the spend relationships.
- ///
- /// Note that the transaction including `outpoint` does not need to be in the `graph` or the
- /// `chain` for this to return `Some`.
- pub fn spent_by(&self, graph: &TxGraph, outpoint: OutPoint) -> Option<(&P, Txid)> {
- graph
- .outspends(outpoint)
- .iter()
- .find_map(|&txid| Some((self.tx_position(txid)?, txid)))
- }
-
- /// Returns whether the sparse chain contains any checkpoints or transactions.
- pub fn is_empty(&self) -> bool {
- self.checkpoints.is_empty() && self.txid_to_pos.is_empty()
- }
-}
-
-/// The return value of [`determine_changeset`].
-///
-/// [`determine_changeset`]: SparseChain::determine_changeset.
-#[derive(Debug, Clone, PartialEq)]
-#[cfg_attr(
- feature = "serde",
- derive(serde::Deserialize, serde::Serialize),
- serde(crate = "serde_crate")
-)]
-#[must_use]
-pub struct ChangeSet<P = TxHeight> {
- pub checkpoints: BTreeMap<u32, Option<BlockHash>>,
- pub txids: BTreeMap<Txid, Option<P>>,
-}
-
-impl<I> Default for ChangeSet<I> {
- fn default() -> Self {
- Self {
- checkpoints: Default::default(),
- txids: Default::default(),
- }
- }
-}
-
-impl<P> ChangeSet<P> {
- /// Appends the changes of `other` into self such that applying `self` afterward has the same
- /// effect as sequentially applying the original `self` and `other`.
- pub fn append(&mut self, mut other: Self)
- where
- P: ChainPosition,
- {
- self.checkpoints.append(&mut other.checkpoints);
- self.txids.append(&mut other.txids);
- }
-
- /// Whether this changeset contains no changes.
- pub fn is_empty(&self) -> bool {
- self.checkpoints.is_empty() && self.txids.is_empty()
- }
-}
-
-fn min_txid() -> Txid {
- Txid::from_inner([0x00; 32])
-}
-
-fn max_txid() -> Txid {
- Txid::from_inner([0xff; 32])
-}
-
-/// Represents a position in which transactions are ordered in [`SparseChain`].
-///
-/// [`ChainPosition`] implementations must be [`Ord`] by [`TxHeight`] first.
-pub trait ChainPosition:
- core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash + Send + Sync + 'static
-{
- /// Get the transaction height of the position.
- fn height(&self) -> TxHeight;
-
- /// Get the position's upper bound of a given height.
- fn max_ord_of_height(height: TxHeight) -> Self;
-
- /// Get the position's lower bound of a given height.
- fn min_ord_of_height(height: TxHeight) -> Self;
-
- /// Get the unconfirmed position.
- fn unconfirmed() -> Self {
- Self::max_ord_of_height(TxHeight::Unconfirmed)
- }
-}
-
-#[cfg(test)]
-pub mod verify_chain_position {
- use crate::{sparse_chain::ChainPosition, ConfirmationTime, TxHeight};
- use alloc::vec::Vec;
-
- pub fn verify_chain_position<P: ChainPosition>(head_count: u32, tail_count: u32) {
- let values = (0..head_count)
- .chain(u32::MAX - tail_count..u32::MAX)
- .flat_map(|i| {
- [
- P::min_ord_of_height(TxHeight::Confirmed(i)),
- P::max_ord_of_height(TxHeight::Confirmed(i)),
- ]
- })
- .chain([
- P::min_ord_of_height(TxHeight::Unconfirmed),
- P::max_ord_of_height(TxHeight::Unconfirmed),
- ])
- .collect::<Vec<_>>();
-
- for i in 0..values.len() {
- for j in 0..values.len() {
- if i == j {
- assert_eq!(values[i], values[j]);
- }
- if i < j {
- assert!(values[i] <= values[j]);
- }
- if i > j {
- assert!(values[i] >= values[j]);
- }
- }
- }
- }
-
- #[test]
- fn verify_tx_height() {
- verify_chain_position::<TxHeight>(1000, 1000);
- }
-
- #[test]
- fn verify_confirmation_time() {
- verify_chain_position::<ConfirmationTime>(1000, 1000);
- }
-}
/// Note there is no harm in scanning transactions that disappear from the blockchain or were never
/// in there in the first place. `SpkTxOutIndex` is intentionally *monotone* -- you cannot delete or
/// modify txouts that have been indexed. To find out which txouts from the index are actually in the
-/// chain or unspent, you must use other sources of information like a [`SparseChain`].
+/// chain or unspent, you must use other sources of information like a [`TxGraph`].
///
/// [`TxOut`]: bitcoin::TxOut
/// [`insert_spk`]: Self::insert_spk
/// [`Ord`]: core::cmp::Ord
/// [`scan`]: Self::scan
-/// [`SparseChain`]: crate::sparse_chain::SparseChain
+/// [`TxGraph`]: crate::tx_graph::TxGraph
#[derive(Clone, Debug)]
pub struct SpkTxOutIndex<I> {
/// script pubkeys ordered by index
//! ```
use crate::{
- collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ForEachTxOut,
- FullTxOut, ObservedAs,
+ collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ChainPosition,
+ ForEachTxOut, FullTxOut,
};
use alloc::vec::Vec;
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct CanonicalTx<'a, T, A> {
/// How the transaction is observed as (confirmed or unconfirmed).
- pub observed_as: ObservedAs<&'a A>,
+ pub observed_as: ChainPosition<&'a A>,
/// The transaction node (as part of the graph).
pub node: TxNode<'a, T, A>,
}
chain: &C,
chain_tip: BlockId,
txid: Txid,
- ) -> Result<Option<ObservedAs<&A>>, C::Error> {
+ ) -> Result<Option<ChainPosition<&A>>, C::Error> {
let (tx_node, anchors, last_seen) = match self.txs.get(&txid) {
Some(v) => v,
None => return Ok(None),
for anchor in anchors {
match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? {
- Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))),
+ Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))),
_ => continue,
}
}
}
}
- Ok(Some(ObservedAs::Unconfirmed(*last_seen)))
+ Ok(Some(ChainPosition::Unconfirmed(*last_seen)))
}
/// Get the position of the transaction in `chain` with tip `chain_tip`.
chain: &C,
chain_tip: BlockId,
txid: Txid,
- ) -> Option<ObservedAs<&A>> {
+ ) -> Option<ChainPosition<&A>> {
self.try_get_chain_position(chain, chain_tip, txid)
.expect("error is infallible")
}
chain: &C,
chain_tip: BlockId,
outpoint: OutPoint,
- ) -> Result<Option<(ObservedAs<&A>, Txid)>, C::Error> {
+ ) -> Result<Option<(ChainPosition<&A>, Txid)>, C::Error> {
if self
.try_get_chain_position(chain, chain_tip, outpoint.txid)?
.is_none()
chain: &C,
static_block: BlockId,
outpoint: OutPoint,
- ) -> Option<(ObservedAs<&A>, Txid)> {
+ ) -> Option<(ChainPosition<&A>, Txid)> {
self.try_get_chain_spend(chain, static_block, outpoint)
.expect("error is infallible")
}
chain: &'a C,
chain_tip: BlockId,
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
- ) -> impl Iterator<Item = Result<(OI, FullTxOut<ObservedAs<A>>), C::Error>> + 'a {
+ ) -> impl Iterator<Item = Result<(OI, FullTxOut<A>), C::Error>> + 'a {
outpoints
.into_iter()
.map(
chain: &'a C,
chain_tip: BlockId,
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
- ) -> impl Iterator<Item = (OI, FullTxOut<ObservedAs<A>>)> + 'a {
+ ) -> impl Iterator<Item = (OI, FullTxOut<A>)> + 'a {
self.try_filter_chain_txouts(chain, chain_tip, outpoints)
.map(|r| r.expect("oracle is infallible"))
}
chain: &'a C,
chain_tip: BlockId,
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
- ) -> impl Iterator<Item = Result<(OI, FullTxOut<ObservedAs<A>>), C::Error>> + 'a {
+ ) -> impl Iterator<Item = Result<(OI, FullTxOut<A>), C::Error>> + 'a {
self.try_filter_chain_txouts(chain, chain_tip, outpoints)
.filter(|r| match r {
// keep unspents, drop spents
chain: &'a C,
chain_tip: BlockId,
txouts: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
- ) -> impl Iterator<Item = (OI, FullTxOut<ObservedAs<A>>)> + 'a {
+ ) -> impl Iterator<Item = (OI, FullTxOut<A>)> + 'a {
self.try_filter_chain_unspents(chain, chain_tip, txouts)
.map(|r| r.expect("oracle is infallible"))
}
let (spk_i, txout) = res?;
match &txout.chain_position {
- ObservedAs::Confirmed(_) => {
+ ChainPosition::Confirmed(_) => {
if txout.is_confirmed_and_spendable(chain_tip.height) {
confirmed += txout.txout.value;
} else if !txout.is_mature(chain_tip.height) {
immature += txout.txout.value;
}
}
- ObservedAs::Unconfirmed(_) => {
+ ChainPosition::Unconfirmed(_) => {
if trust_predicate(&spk_i, &txout.txout.script_pubkey) {
trusted_pending += txout.txout.value;
} else {
+++ /dev/null
-#[macro_use]
-mod common;
-
-use bdk_chain::{
- chain_graph::*,
- collections::HashSet,
- sparse_chain,
- tx_graph::{self, TxGraph},
- BlockId, TxHeight,
-};
-use bitcoin::{OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness};
-
-#[test]
-fn test_spent_by() {
- let tx1 = Transaction {
- version: 0x01,
- lock_time: PackedLockTime(0),
- input: vec![],
- output: vec![TxOut::default()],
- };
-
- let op = OutPoint {
- txid: tx1.txid(),
- vout: 0,
- };
-
- let tx2 = Transaction {
- version: 0x01,
- lock_time: PackedLockTime(0),
- input: vec![TxIn {
- previous_output: op,
- ..Default::default()
- }],
- output: vec![],
- };
- let tx3 = Transaction {
- version: 0x01,
- lock_time: PackedLockTime(42),
- input: vec![TxIn {
- previous_output: op,
- ..Default::default()
- }],
- output: vec![],
- };
-
- let mut cg1 = ChainGraph::default();
- let _ = cg1
- .insert_tx(tx1, TxHeight::Unconfirmed)
- .expect("should insert");
- let mut cg2 = cg1.clone();
- let _ = cg1
- .insert_tx(tx2.clone(), TxHeight::Unconfirmed)
- .expect("should insert");
- let _ = cg2
- .insert_tx(tx3.clone(), TxHeight::Unconfirmed)
- .expect("should insert");
-
- assert_eq!(cg1.spent_by(op), Some((&TxHeight::Unconfirmed, tx2.txid())));
- assert_eq!(cg2.spent_by(op), Some((&TxHeight::Unconfirmed, tx3.txid())));
-}
-
-#[test]
-fn update_evicts_conflicting_tx() {
- let cp_a = BlockId {
- height: 0,
- hash: h!("A"),
- };
- let cp_b = BlockId {
- height: 1,
- hash: h!("B"),
- };
- let cp_b2 = BlockId {
- height: 1,
- hash: h!("B'"),
- };
-
- 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 tx_b2 = Transaction {
- version: 0x02,
- 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(), TxOut::default()],
- };
- {
- let mut cg1 = {
- let mut cg = ChainGraph::default();
- let _ = cg.insert_checkpoint(cp_a).expect("should insert cp");
- let _ = cg
- .insert_tx(tx_a.clone(), TxHeight::Confirmed(0))
- .expect("should insert tx");
- let _ = cg
- .insert_tx(tx_b.clone(), TxHeight::Unconfirmed)
- .expect("should insert tx");
- cg
- };
- let cg2 = {
- let mut cg = ChainGraph::default();
- let _ = cg
- .insert_tx(tx_b2.clone(), TxHeight::Unconfirmed)
- .expect("should insert tx");
- cg
- };
-
- let changeset = ChangeSet::<TxHeight> {
- chain: sparse_chain::ChangeSet {
- checkpoints: Default::default(),
- txids: [
- (tx_b.txid(), None),
- (tx_b2.txid(), Some(TxHeight::Unconfirmed)),
- ]
- .into(),
- },
- graph: tx_graph::Additions {
- tx: [tx_b2.clone()].into(),
- txout: [].into(),
- ..Default::default()
- },
- };
- assert_eq!(
- cg1.determine_changeset(&cg2),
- Ok(changeset.clone()),
- "tx should be evicted from mempool"
- );
-
- cg1.apply_changeset(changeset);
- }
-
- {
- let cg1 = {
- let mut cg = ChainGraph::default();
- let _ = cg.insert_checkpoint(cp_a).expect("should insert cp");
- let _ = cg.insert_checkpoint(cp_b).expect("should insert cp");
- let _ = cg
- .insert_tx(tx_a.clone(), TxHeight::Confirmed(0))
- .expect("should insert tx");
- let _ = cg
- .insert_tx(tx_b.clone(), TxHeight::Confirmed(1))
- .expect("should insert tx");
- cg
- };
- let cg2 = {
- let mut cg = ChainGraph::default();
- let _ = cg
- .insert_tx(tx_b2.clone(), TxHeight::Unconfirmed)
- .expect("should insert tx");
- cg
- };
- assert_eq!(
- cg1.determine_changeset(&cg2),
- Err(UpdateError::UnresolvableConflict(UnresolvableConflict {
- already_confirmed_tx: (TxHeight::Confirmed(1), tx_b.txid()),
- update_tx: (TxHeight::Unconfirmed, tx_b2.txid()),
- })),
- "fail if tx is evicted from valid block"
- );
- }
-
- {
- // Given 2 blocks `{A, B}`, and an update that invalidates block B with
- // `{A, B'}`, we expect txs that exist in `B` that conflicts with txs
- // introduced in the update to be successfully evicted.
- let mut cg1 = {
- let mut cg = ChainGraph::default();
- let _ = cg.insert_checkpoint(cp_a).expect("should insert cp");
- let _ = cg.insert_checkpoint(cp_b).expect("should insert cp");
- let _ = cg
- .insert_tx(tx_a, TxHeight::Confirmed(0))
- .expect("should insert tx");
- let _ = cg
- .insert_tx(tx_b.clone(), TxHeight::Confirmed(1))
- .expect("should insert tx");
- cg
- };
- let cg2 = {
- let mut cg = ChainGraph::default();
- let _ = cg.insert_checkpoint(cp_a).expect("should insert cp");
- let _ = cg.insert_checkpoint(cp_b2).expect("should insert cp");
- let _ = cg
- .insert_tx(tx_b2.clone(), TxHeight::Unconfirmed)
- .expect("should insert tx");
- cg
- };
-
- let changeset = ChangeSet::<TxHeight> {
- chain: sparse_chain::ChangeSet {
- checkpoints: [(1, Some(h!("B'")))].into(),
- txids: [
- (tx_b.txid(), None),
- (tx_b2.txid(), Some(TxHeight::Unconfirmed)),
- ]
- .into(),
- },
- graph: tx_graph::Additions {
- tx: [tx_b2].into(),
- txout: [].into(),
- ..Default::default()
- },
- };
- assert_eq!(
- cg1.determine_changeset(&cg2),
- Ok(changeset.clone()),
- "tx should be evicted from B",
- );
-
- cg1.apply_changeset(changeset);
- }
-}
-
-#[test]
-fn chain_graph_new_missing() {
- let tx_a = Transaction {
- version: 0x01,
- lock_time: PackedLockTime(0),
- input: vec![],
- output: vec![TxOut::default()],
- };
- let tx_b = Transaction {
- version: 0x02,
- lock_time: PackedLockTime(0),
- input: vec![],
- output: vec![TxOut::default()],
- };
-
- let update = chain!(
- index: TxHeight,
- checkpoints: [[0, h!("A")]],
- txids: [
- (tx_a.txid(), TxHeight::Confirmed(0)),
- (tx_b.txid(), TxHeight::Confirmed(0))
- ]
- );
- let mut graph = TxGraph::default();
-
- let mut expected_missing = HashSet::new();
- expected_missing.insert(tx_a.txid());
- expected_missing.insert(tx_b.txid());
-
- assert_eq!(
- ChainGraph::new(update.clone(), graph.clone()),
- Err(NewError::Missing(expected_missing.clone()))
- );
-
- let _ = graph.insert_tx(tx_b.clone());
- expected_missing.remove(&tx_b.txid());
-
- assert_eq!(
- ChainGraph::new(update.clone(), graph.clone()),
- Err(NewError::Missing(expected_missing.clone()))
- );
-
- let _ = graph.insert_txout(
- OutPoint {
- txid: tx_a.txid(),
- vout: 0,
- },
- tx_a.output[0].clone(),
- );
-
- assert_eq!(
- ChainGraph::new(update.clone(), graph.clone()),
- Err(NewError::Missing(expected_missing)),
- "inserting an output instead of full tx doesn't satisfy constraint"
- );
-
- let _ = graph.insert_tx(tx_a.clone());
-
- let new_graph = ChainGraph::new(update.clone(), graph.clone()).unwrap();
- let expected_graph = {
- let mut cg = ChainGraph::<TxHeight>::default();
- let _ = cg
- .insert_checkpoint(update.latest_checkpoint().unwrap())
- .unwrap();
- let _ = cg.insert_tx(tx_a, TxHeight::Confirmed(0)).unwrap();
- let _ = cg.insert_tx(tx_b, TxHeight::Confirmed(0)).unwrap();
- cg
- };
-
- assert_eq!(new_graph, expected_graph);
-}
-
-#[test]
-fn chain_graph_new_conflicts() {
- 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 tx_b2 = Transaction {
- version: 0x02,
- 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(), TxOut::default()],
- };
-
- let chain = chain!(
- index: TxHeight,
- checkpoints: [[5, h!("A")]],
- txids: [
- (tx_a.txid(), TxHeight::Confirmed(1)),
- (tx_b.txid(), TxHeight::Confirmed(2)),
- (tx_b2.txid(), TxHeight::Confirmed(3))
- ]
- );
-
- let graph = TxGraph::new([tx_a, tx_b, tx_b2]);
-
- assert!(matches!(
- ChainGraph::new(chain, graph),
- Err(NewError::Conflict { .. })
- ));
-}
-
-#[test]
-fn test_get_tx_in_chain() {
- let mut cg = ChainGraph::default();
- let tx = Transaction {
- version: 0x01,
- lock_time: PackedLockTime(0),
- input: vec![],
- output: vec![TxOut::default()],
- };
-
- let _ = cg.insert_tx(tx.clone(), TxHeight::Unconfirmed).unwrap();
- assert_eq!(
- cg.get_tx_in_chain(tx.txid()),
- Some((&TxHeight::Unconfirmed, &tx,))
- );
-}
-
-#[test]
-fn test_iterate_transactions() {
- let mut cg = ChainGraph::default();
- let txs = (0..3)
- .map(|i| Transaction {
- version: i,
- lock_time: PackedLockTime(0),
- input: vec![],
- output: vec![TxOut::default()],
- })
- .collect::<Vec<_>>();
- let _ = cg
- .insert_checkpoint(BlockId {
- height: 1,
- hash: h!("A"),
- })
- .unwrap();
- let _ = cg
- .insert_tx(txs[0].clone(), TxHeight::Confirmed(1))
- .unwrap();
- let _ = cg.insert_tx(txs[1].clone(), TxHeight::Unconfirmed).unwrap();
- let _ = cg
- .insert_tx(txs[2].clone(), TxHeight::Confirmed(0))
- .unwrap();
-
- assert_eq!(
- cg.transactions_in_chain().collect::<Vec<_>>(),
- vec![
- (&TxHeight::Confirmed(0), &txs[2],),
- (&TxHeight::Confirmed(1), &txs[0],),
- (&TxHeight::Unconfirmed, &txs[1],),
- ]
- );
-}
-
-/// Start with: block1, block2a, tx1, tx2a
-/// Update 1: block2a -> block2b , tx2a -> tx2b
-/// Update 2: block2b -> block2c , tx2b -> tx2a
-#[test]
-fn test_apply_changes_reintroduce_tx() {
- let block1 = BlockId {
- height: 1,
- hash: h!("block 1"),
- };
- let block2a = BlockId {
- height: 2,
- hash: h!("block 2a"),
- };
- let block2b = BlockId {
- height: 2,
- hash: h!("block 2b"),
- };
- let block2c = BlockId {
- height: 2,
- hash: h!("block 2c"),
- };
-
- let tx1 = Transaction {
- version: 0,
- lock_time: PackedLockTime(1),
- input: Vec::new(),
- output: [TxOut {
- value: 1,
- script_pubkey: Script::new(),
- }]
- .into(),
- };
-
- let tx2a = Transaction {
- version: 0,
- lock_time: PackedLockTime('a'.into()),
- input: [TxIn {
- previous_output: OutPoint::new(tx1.txid(), 0),
- ..Default::default()
- }]
- .into(),
- output: [TxOut {
- value: 0,
- ..Default::default()
- }]
- .into(),
- };
-
- let tx2b = Transaction {
- lock_time: PackedLockTime('b'.into()),
- ..tx2a.clone()
- };
-
- // block1, block2a, tx1, tx2a
- let mut cg = {
- let mut cg = ChainGraph::default();
- let _ = cg.insert_checkpoint(block1).unwrap();
- let _ = cg.insert_checkpoint(block2a).unwrap();
- let _ = cg.insert_tx(tx1, TxHeight::Confirmed(1)).unwrap();
- let _ = cg.insert_tx(tx2a.clone(), TxHeight::Confirmed(2)).unwrap();
- cg
- };
-
- // block2a -> block2b , tx2a -> tx2b
- let update = {
- let mut update = ChainGraph::default();
- let _ = update.insert_checkpoint(block1).unwrap();
- let _ = update.insert_checkpoint(block2b).unwrap();
- let _ = update
- .insert_tx(tx2b.clone(), TxHeight::Confirmed(2))
- .unwrap();
- update
- };
- assert_eq!(
- cg.apply_update(update).expect("should update"),
- ChangeSet {
- chain: changeset! {
- checkpoints: [(2, Some(block2b.hash))],
- txids: [(tx2a.txid(), None), (tx2b.txid(), Some(TxHeight::Confirmed(2)))]
- },
- graph: tx_graph::Additions {
- tx: [tx2b.clone()].into(),
- ..Default::default()
- },
- }
- );
-
- // block2b -> block2c , tx2b -> tx2a
- let update = {
- let mut update = ChainGraph::default();
- let _ = update.insert_checkpoint(block1).unwrap();
- let _ = update.insert_checkpoint(block2c).unwrap();
- let _ = update
- .insert_tx(tx2a.clone(), TxHeight::Confirmed(2))
- .unwrap();
- update
- };
- assert_eq!(
- cg.apply_update(update).expect("should update"),
- ChangeSet {
- chain: changeset! {
- checkpoints: [(2, Some(block2c.hash))],
- txids: [(tx2b.txid(), None), (tx2a.txid(), Some(TxHeight::Confirmed(2)))]
- },
- ..Default::default()
- }
- );
-}
-
-#[test]
-fn test_evict_descendants() {
- let block_1 = BlockId {
- height: 1,
- hash: h!("block 1"),
- };
-
- let block_2a = BlockId {
- height: 2,
- hash: h!("block 2 a"),
- };
-
- let block_2b = BlockId {
- height: 2,
- hash: h!("block 2 b"),
- };
-
- let tx_1 = Transaction {
- input: vec![TxIn {
- previous_output: OutPoint::new(h!("fake tx"), 0),
- ..Default::default()
- }],
- output: vec![TxOut {
- value: 10_000,
- script_pubkey: Script::new(),
- }],
- ..common::new_tx(1)
- };
- let tx_2 = Transaction {
- input: vec![TxIn {
- previous_output: OutPoint::new(tx_1.txid(), 0),
- ..Default::default()
- }],
- output: vec![
- TxOut {
- value: 20_000,
- script_pubkey: Script::new(),
- },
- TxOut {
- value: 30_000,
- script_pubkey: Script::new(),
- },
- ],
- ..common::new_tx(2)
- };
- let tx_3 = Transaction {
- input: vec![TxIn {
- previous_output: OutPoint::new(tx_2.txid(), 0),
- ..Default::default()
- }],
- output: vec![TxOut {
- value: 40_000,
- script_pubkey: Script::new(),
- }],
- ..common::new_tx(3)
- };
- let tx_4 = Transaction {
- input: vec![TxIn {
- previous_output: OutPoint::new(tx_2.txid(), 1),
- ..Default::default()
- }],
- output: vec![TxOut {
- value: 40_000,
- script_pubkey: Script::new(),
- }],
- ..common::new_tx(4)
- };
- let tx_5 = Transaction {
- input: vec![TxIn {
- previous_output: OutPoint::new(tx_4.txid(), 0),
- ..Default::default()
- }],
- output: vec![TxOut {
- value: 40_000,
- script_pubkey: Script::new(),
- }],
- ..common::new_tx(5)
- };
-
- let tx_conflict = Transaction {
- input: vec![TxIn {
- previous_output: OutPoint::new(tx_1.txid(), 0),
- ..Default::default()
- }],
- output: vec![TxOut {
- value: 12345,
- script_pubkey: Script::new(),
- }],
- ..common::new_tx(6)
- };
-
- // 1 is spent by 2, 2 is spent by 3 and 4, 4 is spent by 5
- let _txid_1 = tx_1.txid();
- let txid_2 = tx_2.txid();
- let txid_3 = tx_3.txid();
- let txid_4 = tx_4.txid();
- let txid_5 = tx_5.txid();
-
- // this tx conflicts with 2
- let txid_conflict = tx_conflict.txid();
-
- let cg = {
- let mut cg = ChainGraph::<TxHeight>::default();
- let _ = cg.insert_checkpoint(block_1);
- let _ = cg.insert_checkpoint(block_2a);
- let _ = cg.insert_tx(tx_1, TxHeight::Confirmed(1));
- let _ = cg.insert_tx(tx_2, TxHeight::Confirmed(2));
- let _ = cg.insert_tx(tx_3, TxHeight::Confirmed(2));
- let _ = cg.insert_tx(tx_4, TxHeight::Confirmed(2));
- let _ = cg.insert_tx(tx_5, TxHeight::Confirmed(2));
- cg
- };
-
- let update = {
- let mut cg = ChainGraph::<TxHeight>::default();
- let _ = cg.insert_checkpoint(block_1);
- let _ = cg.insert_checkpoint(block_2b);
- let _ = cg.insert_tx(tx_conflict.clone(), TxHeight::Confirmed(2));
- cg
- };
-
- assert_eq!(
- cg.determine_changeset(&update),
- Ok(ChangeSet {
- chain: changeset! {
- checkpoints: [(2, Some(block_2b.hash))],
- txids: [(txid_2, None), (txid_3, None), (txid_4, None), (txid_5, None), (txid_conflict, Some(TxHeight::Confirmed(2)))]
- },
- graph: tx_graph::Additions {
- tx: [tx_conflict.clone()].into(),
- ..Default::default()
- }
- })
- );
-
- let err = cg
- .insert_tx_preview(tx_conflict, TxHeight::Unconfirmed)
- .expect_err("must fail due to conflicts");
- assert!(matches!(err, InsertTxError::UnresolvableConflict(_)));
-}
keychain::{Balance, DerivationAdditions, KeychainTxOutIndex},
local_chain::LocalChain,
tx_graph::Additions,
- BlockId, ConfirmationHeightAnchor, ObservedAs,
+ BlockId, ChainPosition, ConfirmationHeightAnchor,
};
use bitcoin::{secp256k1::Secp256k1, BlockHash, OutPoint, Script, Transaction, TxIn, TxOut};
use miniscript::Descriptor;
let confirmed_txouts_txid = txouts
.iter()
.filter_map(|(_, full_txout)| {
- if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
+ if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
Some(full_txout.outpoint.txid)
} else {
None
let unconfirmed_txouts_txid = txouts
.iter()
.filter_map(|(_, full_txout)| {
- if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
+ if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
Some(full_txout.outpoint.txid)
} else {
None
let confirmed_utxos_txid = utxos
.iter()
.filter_map(|(_, full_txout)| {
- if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
+ if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
Some(full_txout.outpoint.txid)
} else {
None
let unconfirmed_utxos_txid = utxos
.iter()
.filter_map(|(_, full_txout)| {
- if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
+ if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
Some(full_txout.outpoint.txid)
} else {
None
+++ /dev/null
-#![cfg(feature = "miniscript")]
-#[macro_use]
-mod common;
-
-use bdk_chain::{
- keychain::{Balance, KeychainTracker},
- miniscript::{
- bitcoin::{secp256k1::Secp256k1, OutPoint, PackedLockTime, Transaction, TxOut},
- Descriptor,
- },
- BlockId, ConfirmationTime, TxHeight,
-};
-use bitcoin::TxIn;
-
-#[test]
-fn test_insert_tx() {
- let mut tracker = KeychainTracker::default();
- let secp = Secp256k1::new();
- let (descriptor, _) = Descriptor::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
- tracker.add_keychain((), descriptor.clone());
- let txout = TxOut {
- value: 100_000,
- script_pubkey: descriptor.at_derivation_index(5).script_pubkey(),
- };
-
- let tx = Transaction {
- version: 0x01,
- lock_time: PackedLockTime(0),
- input: vec![],
- output: vec![txout],
- };
-
- let _ = tracker.txout_index.reveal_to_target(&(), 5);
-
- let changeset = tracker
- .insert_tx_preview(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
- tracker.apply_changeset(changeset);
- assert_eq!(
- tracker
- .chain_graph()
- .transactions_in_chain()
- .collect::<Vec<_>>(),
- vec![(&ConfirmationTime::Unconfirmed { last_seen: 0 }, &tx,)]
- );
-
- assert_eq!(
- tracker
- .txout_index
- .txouts_of_keychain(&())
- .collect::<Vec<_>>(),
- vec![(
- 5,
- OutPoint {
- txid: tx.txid(),
- vout: 0
- }
- )]
- );
-}
-
-#[test]
-fn test_balance() {
- use core::str::FromStr;
- #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
- enum Keychain {
- One,
- Two,
- }
- let mut tracker = KeychainTracker::default();
- let one = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)#rg247h69").unwrap();
- let two = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1/*)#ju05rz2a").unwrap();
- tracker.add_keychain(Keychain::One, one);
- tracker.add_keychain(Keychain::Two, two);
-
- let tx1 = Transaction {
- version: 0x01,
- lock_time: PackedLockTime(0),
- input: vec![],
- output: vec![TxOut {
- value: 13_000,
- script_pubkey: tracker
- .txout_index
- .reveal_next_spk(&Keychain::One)
- .0
- .1
- .clone(),
- }],
- };
-
- let tx2 = Transaction {
- version: 0x01,
- lock_time: PackedLockTime(0),
- input: vec![],
- output: vec![TxOut {
- value: 7_000,
- script_pubkey: tracker
- .txout_index
- .reveal_next_spk(&Keychain::Two)
- .0
- .1
- .clone(),
- }],
- };
-
- let tx_coinbase = Transaction {
- version: 0x01,
- lock_time: PackedLockTime(0),
- input: vec![TxIn::default()],
- output: vec![TxOut {
- value: 11_000,
- script_pubkey: tracker
- .txout_index
- .reveal_next_spk(&Keychain::Two)
- .0
- .1
- .clone(),
- }],
- };
-
- assert!(tx_coinbase.is_coin_base());
-
- let _ = tracker
- .insert_checkpoint(BlockId {
- height: 5,
- hash: h!("1"),
- })
- .unwrap();
-
- let should_trust = |keychain: &Keychain| match *keychain {
- Keychain::One => false,
- Keychain::Two => true,
- };
-
- assert_eq!(tracker.balance(should_trust), Balance::default());
-
- let _ = tracker
- .insert_tx(tx1.clone(), TxHeight::Unconfirmed)
- .unwrap();
-
- assert_eq!(
- tracker.balance(should_trust),
- Balance {
- untrusted_pending: 13_000,
- ..Default::default()
- }
- );
-
- let _ = tracker
- .insert_tx(tx2.clone(), TxHeight::Unconfirmed)
- .unwrap();
-
- assert_eq!(
- tracker.balance(should_trust),
- Balance {
- trusted_pending: 7_000,
- untrusted_pending: 13_000,
- ..Default::default()
- }
- );
-
- let _ = tracker
- .insert_tx(tx_coinbase, TxHeight::Confirmed(0))
- .unwrap();
-
- assert_eq!(
- tracker.balance(should_trust),
- Balance {
- trusted_pending: 7_000,
- untrusted_pending: 13_000,
- immature: 11_000,
- ..Default::default()
- }
- );
-
- let _ = tracker.insert_tx(tx1, TxHeight::Confirmed(1)).unwrap();
-
- assert_eq!(
- tracker.balance(should_trust),
- Balance {
- trusted_pending: 7_000,
- untrusted_pending: 0,
- immature: 11_000,
- confirmed: 13_000,
- }
- );
-
- let _ = tracker.insert_tx(tx2, TxHeight::Confirmed(2)).unwrap();
-
- assert_eq!(
- tracker.balance(should_trust),
- Balance {
- trusted_pending: 0,
- untrusted_pending: 0,
- immature: 11_000,
- confirmed: 20_000,
- }
- );
-
- let _ = tracker
- .insert_checkpoint(BlockId {
- height: 98,
- hash: h!("98"),
- })
- .unwrap();
-
- assert_eq!(
- tracker.balance(should_trust),
- Balance {
- trusted_pending: 0,
- untrusted_pending: 0,
- immature: 11_000,
- confirmed: 20_000,
- }
- );
-
- let _ = tracker
- .insert_checkpoint(BlockId {
- height: 99,
- hash: h!("99"),
- })
- .unwrap();
-
- assert_eq!(
- tracker.balance(should_trust),
- Balance {
- trusted_pending: 0,
- untrusted_pending: 0,
- immature: 0,
- confirmed: 31_000,
- }
- );
-
- assert_eq!(tracker.balance_at(0), 0);
- assert_eq!(tracker.balance_at(1), 13_000);
- assert_eq!(tracker.balance_at(2), 20_000);
- assert_eq!(tracker.balance_at(98), 20_000);
- assert_eq!(tracker.balance_at(99), 31_000);
- assert_eq!(tracker.balance_at(100), 31_000);
-}
+++ /dev/null
-#[macro_use]
-mod common;
-
-use bdk_chain::{collections::BTreeSet, sparse_chain::*, BlockId, TxHeight};
-use bitcoin::{hashes::Hash, Txid};
-use core::ops::Bound;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
-pub struct TestIndex(TxHeight, u32);
-
-impl ChainPosition for TestIndex {
- fn height(&self) -> TxHeight {
- self.0
- }
-
- fn max_ord_of_height(height: TxHeight) -> Self {
- Self(height, u32::MAX)
- }
-
- fn min_ord_of_height(height: TxHeight) -> Self {
- Self(height, u32::MIN)
- }
-}
-
-impl TestIndex {
- pub fn new<H>(height: H, ext: u32) -> Self
- where
- H: Into<TxHeight>,
- {
- Self(height.into(), ext)
- }
-}
-
-#[test]
-fn add_first_checkpoint() {
- let chain = SparseChain::default();
- assert_eq!(
- chain.determine_changeset(&chain!([0, h!("A")])),
- Ok(changeset! {
- checkpoints: [(0, Some(h!("A")))],
- txids: []
- },),
- "add first tip"
- );
-}
-
-#[test]
-fn add_second_tip() {
- let chain = chain!([0, h!("A")]);
- assert_eq!(
- chain.determine_changeset(&chain!([0, h!("A")], [1, h!("B")])),
- Ok(changeset! {
- checkpoints: [(1, Some(h!("B")))],
- txids: []
- },),
- "extend tip by one"
- );
-}
-
-#[test]
-fn two_disjoint_chains_cannot_merge() {
- let chain1 = chain!([0, h!("A")]);
- let chain2 = chain!([1, h!("B")]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Err(UpdateError::NotConnected(0))
- );
-}
-
-#[test]
-fn duplicate_chains_should_merge() {
- let chain1 = chain!([0, h!("A")]);
- let chain2 = chain!([0, h!("A")]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(ChangeSet::default())
- );
-}
-
-#[test]
-fn duplicate_chains_with_txs_should_merge() {
- let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- let chain2 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(ChangeSet::default())
- );
-}
-
-#[test]
-fn duplicate_chains_with_different_txs_should_merge() {
- let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- let chain2 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx1"), TxHeight::Confirmed(0))]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [],
- txids: [(h!("tx1"), Some(TxHeight::Confirmed(0)))]
- })
- );
-}
-
-#[test]
-fn invalidate_first_and_only_checkpoint_without_tx_changes() {
- let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- let chain2 = chain!(checkpoints: [[0,h!("A'")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [(0, Some(h!("A'")))],
- txids: []
- },)
- );
-}
-
-#[test]
-fn invalidate_first_and_only_checkpoint_with_tx_move_forward() {
- let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- let chain2 = chain!(checkpoints: [[0,h!("A'")],[1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [(0, Some(h!("A'"))), (1, Some(h!("B")))],
- txids: [(h!("tx0"), Some(TxHeight::Confirmed(1)))]
- },)
- );
-}
-
-#[test]
-fn invalidate_first_and_only_checkpoint_with_tx_move_backward() {
- let chain1 = chain!(checkpoints: [[1,h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]);
- let chain2 = chain!(checkpoints: [[0,h!("A")],[1, h!("B'")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [(0, Some(h!("A"))), (1, Some(h!("B'")))],
- txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))]
- },)
- );
-}
-
-#[test]
-fn invalidate_a_checkpoint_and_try_and_move_tx_when_it_wasnt_within_invalidation() {
- let chain1 = chain!(checkpoints: [[0, h!("A")], [1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- let chain2 = chain!(checkpoints: [[0, h!("A")], [1, h!("B'")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Err(UpdateError::TxInconsistent {
- txid: h!("tx0"),
- original_pos: TxHeight::Confirmed(0),
- update_pos: TxHeight::Confirmed(1),
- })
- );
-}
-
-/// This test doesn't make much sense. We're invalidating a block at height 1 and moving it to
-/// height 0. It should be impossible for it to be at height 1 at any point if it was at height 0
-/// all along.
-#[test]
-fn move_invalidated_tx_into_earlier_checkpoint() {
- let chain1 = chain!(checkpoints: [[0, h!("A")], [1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]);
- let chain2 = chain!(checkpoints: [[0, h!("A")], [1, h!("B'")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [(1, Some(h!("B'")))],
- txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))]
- },)
- );
-}
-
-#[test]
-fn invalidate_first_and_only_checkpoint_with_tx_move_to_mempool() {
- let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- let chain2 = chain!(checkpoints: [[0,h!("A'")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [(0, Some(h!("A'")))],
- txids: [(h!("tx0"), Some(TxHeight::Unconfirmed))]
- },)
- );
-}
-
-#[test]
-fn confirm_tx_without_extending_chain() {
- let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
- let chain2 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [],
- txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))]
- },)
- );
-}
-
-#[test]
-fn confirm_tx_backwards_while_extending_chain() {
- let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
- let chain2 = chain!(checkpoints: [[0,h!("A")],[1,h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [(1, Some(h!("B")))],
- txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))]
- },)
- );
-}
-
-#[test]
-fn confirm_tx_in_new_block() {
- let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
- let chain2 = chain! {
- checkpoints: [[0,h!("A")], [1,h!("B")]],
- txids: [(h!("tx0"), TxHeight::Confirmed(1))]
- };
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [(1, Some(h!("B")))],
- txids: [(h!("tx0"), Some(TxHeight::Confirmed(1)))]
- },)
- );
-}
-
-#[test]
-fn merging_mempool_of_empty_chains_doesnt_fail() {
- let chain1 = chain!(checkpoints: [], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
- let chain2 = chain!(checkpoints: [], txids: [(h!("tx1"), TxHeight::Unconfirmed)]);
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [],
- txids: [(h!("tx1"), Some(TxHeight::Unconfirmed))]
- },)
- );
-}
-
-#[test]
-fn cannot_insert_confirmed_tx_without_checkpoints() {
- let chain = SparseChain::default();
- assert_eq!(
- chain.insert_tx_preview(h!("A"), TxHeight::Confirmed(0)),
- Err(InsertTxError::TxTooHigh {
- txid: h!("A"),
- tx_height: 0,
- tip_height: None
- })
- );
-}
-
-#[test]
-fn empty_chain_can_add_unconfirmed_transactions() {
- let chain1 = chain!(checkpoints: [[0, h!("A")]], txids: []);
- let chain2 = chain!(checkpoints: [], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [],
- txids: [ (h!("tx0"), Some(TxHeight::Unconfirmed)) ]
- },)
- );
-}
-
-#[test]
-fn can_update_with_shorter_chain() {
- let chain1 = chain!(checkpoints: [[1, h!("B")],[2, h!("C")]], txids: []);
- let chain2 = chain!(checkpoints: [[1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]);
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [],
- txids: [(h!("tx0"), Some(TxHeight::Confirmed(1)))]
- },)
- )
-}
-
-#[test]
-fn can_introduce_older_checkpoints() {
- let chain1 = chain!(checkpoints: [[2, h!("C")], [3, h!("D")]], txids: []);
- let chain2 = chain!(checkpoints: [[1, h!("B")], [2, h!("C")]], txids: []);
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [(1, Some(h!("B")))],
- txids: []
- },)
- );
-}
-
-#[test]
-fn fix_blockhash_before_agreement_point() {
- let chain1 = chain!([0, h!("im-wrong")], [1, h!("we-agree")]);
- let chain2 = chain!([0, h!("fix")], [1, h!("we-agree")]);
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [(0, Some(h!("fix")))],
- txids: []
- },)
- )
-}
-
-// TODO: Use macro
-#[test]
-fn cannot_change_ext_index_of_confirmed_tx() {
- let chain1 = chain!(
- index: TestIndex,
- checkpoints: [[1, h!("A")]],
- txids: [(h!("tx0"), TestIndex(TxHeight::Confirmed(1), 10))]
- );
- let chain2 = chain!(
- index: TestIndex,
- checkpoints: [[1, h!("A")]],
- txids: [(h!("tx0"), TestIndex(TxHeight::Confirmed(1), 20))]
- );
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Err(UpdateError::TxInconsistent {
- txid: h!("tx0"),
- original_pos: TestIndex(TxHeight::Confirmed(1), 10),
- update_pos: TestIndex(TxHeight::Confirmed(1), 20),
- }),
- )
-}
-
-#[test]
-fn can_change_index_of_unconfirmed_tx() {
- let chain1 = chain!(
- index: TestIndex,
- checkpoints: [[1, h!("A")]],
- txids: [(h!("tx1"), TestIndex(TxHeight::Unconfirmed, 10))]
- );
- let chain2 = chain!(
- index: TestIndex,
- checkpoints: [[1, h!("A")]],
- txids: [(h!("tx1"), TestIndex(TxHeight::Unconfirmed, 20))]
- );
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(ChangeSet {
- checkpoints: [].into(),
- txids: [(h!("tx1"), Some(TestIndex(TxHeight::Unconfirmed, 20)),)].into()
- },),
- )
-}
-
-/// B and C are in both chain and update
-/// ```
-/// | 0 | 1 | 2 | 3 | 4
-/// chain | B C
-/// update | A B C D
-/// ```
-/// This should succeed with the point of agreement being C and A should be added in addition.
-#[test]
-fn two_points_of_agreement() {
- let chain1 = chain!([1, h!("B")], [2, h!("C")]);
- let chain2 = chain!([0, h!("A")], [1, h!("B")], [2, h!("C")], [3, h!("D")]);
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [(0, Some(h!("A"))), (3, Some(h!("D")))]
- },),
- );
-}
-
-/// Update and chain does not connect:
-/// ```
-/// | 0 | 1 | 2 | 3 | 4
-/// chain | B C
-/// update | A B D
-/// ```
-/// This should fail as we cannot figure out whether C & D are on the same chain
-#[test]
-fn update_and_chain_does_not_connect() {
- let chain1 = chain!([1, h!("B")], [2, h!("C")]);
- let chain2 = chain!([0, h!("A")], [1, h!("B")], [3, h!("D")]);
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Err(UpdateError::NotConnected(2)),
- );
-}
-
-/// Transient invalidation:
-/// ```
-/// | 0 | 1 | 2 | 3 | 4 | 5
-/// chain | A B C E
-/// update | A B' C' D
-/// ```
-/// This should succeed and invalidate B,C and E with point of agreement being A.
-/// It should also invalidate transactions at height 1.
-#[test]
-fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation() {
- let chain1 = chain! {
- checkpoints: [[0, h!("A")], [2, h!("B")], [3, h!("C")], [5, h!("E")]],
- txids: [
- (h!("a"), TxHeight::Confirmed(0)),
- (h!("b1"), TxHeight::Confirmed(1)),
- (h!("b2"), TxHeight::Confirmed(2)),
- (h!("d"), TxHeight::Confirmed(3)),
- (h!("e"), TxHeight::Confirmed(5))
- ]
- };
- let chain2 = chain! {
- checkpoints: [[0, h!("A")], [2, h!("B'")], [3, h!("C'")], [4, h!("D")]],
- txids: [(h!("b1"), TxHeight::Confirmed(4)), (h!("b2"), TxHeight::Confirmed(3))]
- };
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [
- (2, Some(h!("B'"))),
- (3, Some(h!("C'"))),
- (4, Some(h!("D"))),
- (5, None)
- ],
- txids: [
- (h!("b1"), Some(TxHeight::Confirmed(4))),
- (h!("b2"), Some(TxHeight::Confirmed(3))),
- (h!("d"), Some(TxHeight::Unconfirmed)),
- (h!("e"), Some(TxHeight::Unconfirmed))
- ]
- },)
- );
-}
-
-/// Transient invalidation:
-/// ```
-/// | 0 | 1 | 2 | 3 | 4
-/// chain | B C E
-/// update | B' C' D
-/// ```
-///
-/// This should succeed and invalidate B, C and E with no point of agreement
-#[test]
-fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation_no_point_of_agreement() {
- let chain1 = chain!([1, h!("B")], [2, h!("C")], [4, h!("E")]);
- let chain2 = chain!([1, h!("B'")], [2, h!("C'")], [3, h!("D")]);
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [
- (1, Some(h!("B'"))),
- (2, Some(h!("C'"))),
- (3, Some(h!("D"))),
- (4, None)
- ]
- },)
- )
-}
-
-/// Transient invalidation:
-/// ```
-/// | 0 | 1 | 2 | 3 | 4
-/// chain | A B C E
-/// update | B' C' D
-/// ```
-///
-/// This should fail since although it tells us that B and C are invalid it doesn't tell us whether
-/// A was invalid.
-#[test]
-fn invalidation_but_no_connection() {
- let chain1 = chain!([0, h!("A")], [1, h!("B")], [2, h!("C")], [4, h!("E")]);
- let chain2 = chain!([1, h!("B'")], [2, h!("C'")], [3, h!("D")]);
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Err(UpdateError::NotConnected(0))
- )
-}
-
-#[test]
-fn checkpoint_limit_is_respected() {
- let mut chain1 = SparseChain::default();
- let _ = chain1
- .apply_update(chain!(
- [1, h!("A")],
- [2, h!("B")],
- [3, h!("C")],
- [4, h!("D")],
- [5, h!("E")]
- ))
- .unwrap();
-
- assert_eq!(chain1.checkpoints().len(), 5);
- chain1.set_checkpoint_limit(Some(4));
- assert_eq!(chain1.checkpoints().len(), 4);
-
- let _ = chain1
- .insert_checkpoint(BlockId {
- height: 6,
- hash: h!("F"),
- })
- .unwrap();
- assert_eq!(chain1.checkpoints().len(), 4);
-
- let changeset = chain1.determine_changeset(&chain!([6, h!("F")], [7, h!("G")]));
- assert_eq!(changeset, Ok(changeset!(checkpoints: [(7, Some(h!("G")))])));
-
- chain1.apply_changeset(changeset.unwrap());
-
- assert_eq!(chain1.checkpoints().len(), 4);
-}
-
-#[test]
-fn range_txids_by_height() {
- let mut chain = chain!(index: TestIndex, checkpoints: [[1, h!("block 1")], [2, h!("block 2")]]);
-
- let txids: [(TestIndex, Txid); 4] = [
- (
- TestIndex(TxHeight::Confirmed(1), u32::MIN),
- Txid::from_inner([0x00; 32]),
- ),
- (
- TestIndex(TxHeight::Confirmed(1), u32::MAX),
- Txid::from_inner([0xfe; 32]),
- ),
- (
- TestIndex(TxHeight::Confirmed(2), u32::MIN),
- Txid::from_inner([0x01; 32]),
- ),
- (
- TestIndex(TxHeight::Confirmed(2), u32::MAX),
- Txid::from_inner([0xff; 32]),
- ),
- ];
-
- // populate chain with txids
- for (index, txid) in txids {
- let _ = chain.insert_tx(txid, index).expect("should succeed");
- }
-
- // inclusive start
- assert_eq!(
- chain
- .range_txids_by_height(TxHeight::Confirmed(1)..)
- .collect::<Vec<_>>(),
- txids.iter().collect::<Vec<_>>(),
- );
-
- // exclusive start
- assert_eq!(
- chain
- .range_txids_by_height((Bound::Excluded(TxHeight::Confirmed(1)), Bound::Unbounded,))
- .collect::<Vec<_>>(),
- txids[2..].iter().collect::<Vec<_>>(),
- );
-
- // inclusive end
- assert_eq!(
- chain
- .range_txids_by_height((Bound::Unbounded, Bound::Included(TxHeight::Confirmed(2))))
- .collect::<Vec<_>>(),
- txids[..4].iter().collect::<Vec<_>>(),
- );
-
- // exclusive end
- assert_eq!(
- chain
- .range_txids_by_height(..TxHeight::Confirmed(2))
- .collect::<Vec<_>>(),
- txids[..2].iter().collect::<Vec<_>>(),
- );
-}
-
-#[test]
-fn range_txids_by_index() {
- let mut chain = chain!(index: TestIndex, checkpoints: [[1, h!("block 1")],[2, h!("block 2")]]);
-
- let txids: [(TestIndex, Txid); 4] = [
- (TestIndex(TxHeight::Confirmed(1), u32::MIN), h!("tx 1 min")),
- (TestIndex(TxHeight::Confirmed(1), u32::MAX), h!("tx 1 max")),
- (TestIndex(TxHeight::Confirmed(2), u32::MIN), h!("tx 2 min")),
- (TestIndex(TxHeight::Confirmed(2), u32::MAX), h!("tx 2 max")),
- ];
-
- // populate chain with txids
- for (index, txid) in txids {
- let _ = chain.insert_tx(txid, index).expect("should succeed");
- }
-
- // inclusive start
- assert_eq!(
- chain
- .range_txids_by_position(TestIndex(TxHeight::Confirmed(1), u32::MIN)..)
- .collect::<Vec<_>>(),
- txids.iter().collect::<Vec<_>>(),
- );
- assert_eq!(
- chain
- .range_txids_by_position(TestIndex(TxHeight::Confirmed(1), u32::MAX)..)
- .collect::<Vec<_>>(),
- txids[1..].iter().collect::<Vec<_>>(),
- );
-
- // exclusive start
- assert_eq!(
- chain
- .range_txids_by_position((
- Bound::Excluded(TestIndex(TxHeight::Confirmed(1), u32::MIN)),
- Bound::Unbounded
- ))
- .collect::<Vec<_>>(),
- txids[1..].iter().collect::<Vec<_>>(),
- );
- assert_eq!(
- chain
- .range_txids_by_position((
- Bound::Excluded(TestIndex(TxHeight::Confirmed(1), u32::MAX)),
- Bound::Unbounded
- ))
- .collect::<Vec<_>>(),
- txids[2..].iter().collect::<Vec<_>>(),
- );
-
- // inclusive end
- assert_eq!(
- chain
- .range_txids_by_position((
- Bound::Unbounded,
- Bound::Included(TestIndex(TxHeight::Confirmed(2), u32::MIN))
- ))
- .collect::<Vec<_>>(),
- txids[..3].iter().collect::<Vec<_>>(),
- );
- assert_eq!(
- chain
- .range_txids_by_position((
- Bound::Unbounded,
- Bound::Included(TestIndex(TxHeight::Confirmed(2), u32::MAX))
- ))
- .collect::<Vec<_>>(),
- txids[..4].iter().collect::<Vec<_>>(),
- );
-
- // exclusive end
- assert_eq!(
- chain
- .range_txids_by_position(..TestIndex(TxHeight::Confirmed(2), u32::MIN))
- .collect::<Vec<_>>(),
- txids[..2].iter().collect::<Vec<_>>(),
- );
- assert_eq!(
- chain
- .range_txids_by_position(..TestIndex(TxHeight::Confirmed(2), u32::MAX))
- .collect::<Vec<_>>(),
- txids[..3].iter().collect::<Vec<_>>(),
- );
-}
-
-#[test]
-fn range_txids() {
- let mut chain = SparseChain::default();
-
- let txids = (0..100)
- .map(|v| Txid::hash(v.to_string().as_bytes()))
- .collect::<BTreeSet<Txid>>();
-
- // populate chain
- for txid in &txids {
- let _ = chain
- .insert_tx(*txid, TxHeight::Unconfirmed)
- .expect("should succeed");
- }
-
- for txid in &txids {
- assert_eq!(
- chain
- .range_txids((TxHeight::Unconfirmed, *txid)..)
- .map(|(_, txid)| txid)
- .collect::<Vec<_>>(),
- txids.range(*txid..).collect::<Vec<_>>(),
- "range with inclusive start should succeed"
- );
-
- assert_eq!(
- chain
- .range_txids((
- Bound::Excluded((TxHeight::Unconfirmed, *txid)),
- Bound::Unbounded,
- ))
- .map(|(_, txid)| txid)
- .collect::<Vec<_>>(),
- txids
- .range((Bound::Excluded(*txid), Bound::Unbounded,))
- .collect::<Vec<_>>(),
- "range with exclusive start should succeed"
- );
-
- assert_eq!(
- chain
- .range_txids(..(TxHeight::Unconfirmed, *txid))
- .map(|(_, txid)| txid)
- .collect::<Vec<_>>(),
- txids.range(..*txid).collect::<Vec<_>>(),
- "range with exclusive end should succeed"
- );
-
- assert_eq!(
- chain
- .range_txids((
- Bound::Included((TxHeight::Unconfirmed, *txid)),
- Bound::Unbounded,
- ))
- .map(|(_, txid)| txid)
- .collect::<Vec<_>>(),
- txids
- .range((Bound::Included(*txid), Bound::Unbounded,))
- .collect::<Vec<_>>(),
- "range with inclusive end should succeed"
- );
- }
-}
-
-#[test]
-fn invalidated_txs_move_to_unconfirmed() {
- let chain1 = chain! {
- checkpoints: [[0, h!("A")], [1, h!("B")], [2, h!("C")]],
- txids: [
- (h!("a"), TxHeight::Confirmed(0)),
- (h!("b"), TxHeight::Confirmed(1)),
- (h!("c"), TxHeight::Confirmed(2)),
- (h!("d"), TxHeight::Unconfirmed)
- ]
- };
-
- let chain2 = chain!([0, h!("A")], [1, h!("B'")]);
-
- assert_eq!(
- chain1.determine_changeset(&chain2),
- Ok(changeset! {
- checkpoints: [
- (1, Some(h!("B'"))),
- (2, None)
- ],
- txids: [
- (h!("b"), Some(TxHeight::Unconfirmed)),
- (h!("c"), Some(TxHeight::Unconfirmed))
- ]
- },)
- );
-}
-
-#[test]
-fn change_tx_position_from_unconfirmed_to_confirmed() {
- let mut chain = SparseChain::<TxHeight>::default();
- let txid = h!("txid");
-
- let _ = chain.insert_tx(txid, TxHeight::Unconfirmed).unwrap();
-
- assert_eq!(chain.tx_position(txid), Some(&TxHeight::Unconfirmed));
- let _ = chain
- .insert_checkpoint(BlockId {
- height: 0,
- hash: h!("0"),
- })
- .unwrap();
- let _ = chain.insert_tx(txid, TxHeight::Confirmed(0)).unwrap();
-
- assert_eq!(chain.tx_position(txid), Some(&TxHeight::Confirmed(0)));
-}
collections::*,
local_chain::LocalChain,
tx_graph::{Additions, TxGraph},
- Append, BlockId, ConfirmationHeightAnchor, ObservedAs,
+ Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
};
use bitcoin::{
hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid,
};
// Conf anchor used to mark the full transaction as confirmed.
- let conf_anchor = ObservedAs::Confirmed(BlockId {
+ let conf_anchor = ChainPosition::Confirmed(BlockId {
height: 100,
hash: h!("random blockhash"),
});
// Unconfirmed anchor to mark the partial transactions as unconfirmed
- let unconf_anchor = ObservedAs::<BlockId>::Unconfirmed(1000000);
+ let unconf_anchor = ChainPosition::<BlockId>::Unconfirmed(1000000);
// Make the original graph
let mut graph = {
- let mut graph = TxGraph::<ObservedAs<BlockId>>::default();
+ let mut graph = TxGraph::<ChainPosition<BlockId>>::default();
for (outpoint, txout) in &original_ops {
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
assert_eq!(
graph.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 0)),
Some((
- ObservedAs::Confirmed(&ConfirmationHeightAnchor {
+ ChainPosition::Confirmed(&ConfirmationHeightAnchor {
anchor_block: tip,
confirmation_height: 98
}),
assert_eq!(
graph.get_chain_position(&local_chain, tip, tx_0.txid()),
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
- Some(ObservedAs::Confirmed(&ConfirmationHeightAnchor {
+ Some(ChainPosition::Confirmed(&ConfirmationHeightAnchor {
anchor_block: tip,
confirmation_height: 95
}))
// Even if unconfirmed tx has a last_seen of 0, it can still be part of a chain spend.
assert_eq!(
graph.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)),
- Some((ObservedAs::Unconfirmed(0), tx_2.txid())),
+ Some((ChainPosition::Unconfirmed(0), tx_2.txid())),
);
// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
graph
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
.unwrap(),
- (ObservedAs::Unconfirmed(1234567), tx_2.txid())
+ (ChainPosition::Unconfirmed(1234567), tx_2.txid())
);
// A conflicting transaction that conflicts with tx_1.
graph
.get_chain_position(&local_chain, tip, tx_2_conflict.txid())
.expect("position expected"),
- ObservedAs::Unconfirmed(1234568)
+ ChainPosition::Unconfirmed(1234568)
);
// Chain_spend now catches the new transaction as the spend.
graph
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
.expect("expect observation"),
- (ObservedAs::Unconfirmed(1234568), tx_2_conflict.txid())
+ (ChainPosition::Unconfirmed(1234568), tx_2_conflict.txid())
);
// Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict`
--- /dev/null
+use bdk_chain::{
+ bitcoin::{hashes::hex::FromHex, BlockHash, OutPoint, Script, Transaction, Txid},
+ keychain::LocalUpdate,
+ local_chain::LocalChain,
+ tx_graph::{self, TxGraph},
+ Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeAnchor,
+};
+use electrum_client::{Client, ElectrumApi, Error};
+use std::{
+ collections::{BTreeMap, BTreeSet, HashMap, HashSet},
+ fmt::Debug,
+};
+
+#[derive(Debug, Clone)]
+pub struct ElectrumUpdate<K, A> {
+ pub graph_update: HashMap<Txid, BTreeSet<A>>,
+ pub chain_update: LocalChain,
+ pub keychain_update: BTreeMap<K, u32>,
+}
+
+impl<K, A> Default for ElectrumUpdate<K, A> {
+ fn default() -> Self {
+ Self {
+ graph_update: Default::default(),
+ chain_update: Default::default(),
+ keychain_update: Default::default(),
+ }
+ }
+}
+
+impl<K, A: Anchor> ElectrumUpdate<K, A> {
+ pub fn missing_full_txs<A2>(&self, graph: &TxGraph<A2>) -> Vec<Txid> {
+ self.graph_update
+ .keys()
+ .filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
+ .cloned()
+ .collect()
+ }
+
+ pub fn finalize(
+ self,
+ client: &Client,
+ seen_at: Option<u64>,
+ missing: Vec<Txid>,
+ ) -> Result<LocalUpdate<K, A>, Error> {
+ let new_txs = client.batch_transaction_get(&missing)?;
+ let mut graph_update = TxGraph::<A>::new(new_txs);
+ for (txid, anchors) in self.graph_update {
+ if let Some(seen_at) = seen_at {
+ let _ = graph_update.insert_seen_at(txid, seen_at);
+ }
+ for anchor in anchors {
+ let _ = graph_update.insert_anchor(txid, anchor);
+ }
+ }
+ Ok(LocalUpdate {
+ keychain: self.keychain_update,
+ graph: graph_update,
+ chain: self.chain_update,
+ })
+ }
+}
+
+impl<K> ElectrumUpdate<K, ConfirmationHeightAnchor> {
+ /// Finalizes the [`ElectrumUpdate`] with `new_txs` and anchors of type
+ /// [`ConfirmationTimeAnchor`].
+ ///
+ /// **Note:** The confirmation time might not be precisely correct if there has been a reorg.
+ /// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
+ /// use it.
+ pub fn finalize_as_confirmation_time(
+ self,
+ client: &Client,
+ seen_at: Option<u64>,
+ missing: Vec<Txid>,
+ ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error> {
+ let update = self.finalize(client, seen_at, missing)?;
+
+ let relevant_heights = {
+ let mut visited_heights = HashSet::new();
+ update
+ .graph
+ .all_anchors()
+ .iter()
+ .map(|(a, _)| a.confirmation_height_upper_bound())
+ .filter(move |&h| visited_heights.insert(h))
+ .collect::<Vec<_>>()
+ };
+
+ let height_to_time = relevant_heights
+ .clone()
+ .into_iter()
+ .zip(
+ client
+ .batch_block_header(relevant_heights)?
+ .into_iter()
+ .map(|bh| bh.time as u64),
+ )
+ .collect::<HashMap<u32, u64>>();
+
+ let graph_additions = {
+ let old_additions = TxGraph::default().determine_additions(&update.graph);
+ tx_graph::Additions {
+ tx: old_additions.tx,
+ txout: old_additions.txout,
+ last_seen: old_additions.last_seen,
+ anchors: old_additions
+ .anchors
+ .into_iter()
+ .map(|(height_anchor, txid)| {
+ let confirmation_height = height_anchor.confirmation_height;
+ let confirmation_time = height_to_time[&confirmation_height];
+ let time_anchor = ConfirmationTimeAnchor {
+ anchor_block: height_anchor.anchor_block,
+ confirmation_height,
+ confirmation_time,
+ };
+ (time_anchor, txid)
+ })
+ .collect(),
+ }
+ };
+
+ Ok(LocalUpdate {
+ keychain: update.keychain,
+ graph: {
+ let mut graph = TxGraph::default();
+ graph.apply_additions(graph_additions);
+ graph
+ },
+ chain: update.chain,
+ })
+ }
+}
+
+pub trait ElectrumExt<A> {
+ fn get_tip(&self) -> Result<(u32, BlockHash), Error>;
+
+ fn scan<K: Ord + Clone>(
+ &self,
+ local_chain: &BTreeMap<u32, BlockHash>,
+ keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
+ txids: impl IntoIterator<Item = Txid>,
+ outpoints: impl IntoIterator<Item = OutPoint>,
+ stop_gap: usize,
+ batch_size: usize,
+ ) -> Result<ElectrumUpdate<K, A>, Error>;
+
+ fn scan_without_keychain(
+ &self,
+ local_chain: &BTreeMap<u32, BlockHash>,
+ misc_spks: impl IntoIterator<Item = Script>,
+ txids: impl IntoIterator<Item = Txid>,
+ outpoints: impl IntoIterator<Item = OutPoint>,
+ batch_size: usize,
+ ) -> Result<ElectrumUpdate<(), A>, Error> {
+ let spk_iter = misc_spks
+ .into_iter()
+ .enumerate()
+ .map(|(i, spk)| (i as u32, spk));
+
+ self.scan(
+ local_chain,
+ [((), spk_iter)].into(),
+ txids,
+ outpoints,
+ usize::MAX,
+ batch_size,
+ )
+ }
+}
+
+impl ElectrumExt<ConfirmationHeightAnchor> for Client {
+ fn get_tip(&self) -> Result<(u32, BlockHash), Error> {
+ // TODO: unsubscribe when added to the client, or is there a better call to use here?
+ self.block_headers_subscribe()
+ .map(|data| (data.height as u32, data.header.block_hash()))
+ }
+
+ fn scan<K: Ord + Clone>(
+ &self,
+ local_chain: &BTreeMap<u32, BlockHash>,
+ keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
+ txids: impl IntoIterator<Item = Txid>,
+ outpoints: impl IntoIterator<Item = OutPoint>,
+ stop_gap: usize,
+ batch_size: usize,
+ ) -> Result<ElectrumUpdate<K, ConfirmationHeightAnchor>, Error> {
+ let mut request_spks = keychain_spks
+ .into_iter()
+ .map(|(k, s)| (k, s.into_iter()))
+ .collect::<BTreeMap<K, _>>();
+ let mut scanned_spks = BTreeMap::<(K, u32), (Script, bool)>::new();
+
+ let txids = txids.into_iter().collect::<Vec<_>>();
+ let outpoints = outpoints.into_iter().collect::<Vec<_>>();
+
+ let update = loop {
+ let mut update = ElectrumUpdate::<K, ConfirmationHeightAnchor> {
+ chain_update: prepare_chain_update(self, local_chain)?,
+ ..Default::default()
+ };
+ let anchor_block = update
+ .chain_update
+ .tip()
+ .expect("must have atleast one block");
+
+ if !request_spks.is_empty() {
+ if !scanned_spks.is_empty() {
+ scanned_spks.append(&mut populate_with_spks(
+ self,
+ anchor_block,
+ &mut update,
+ &mut scanned_spks
+ .iter()
+ .map(|(i, (spk, _))| (i.clone(), spk.clone())),
+ stop_gap,
+ batch_size,
+ )?);
+ }
+ for (keychain, keychain_spks) in &mut request_spks {
+ scanned_spks.extend(
+ populate_with_spks(
+ self,
+ anchor_block,
+ &mut update,
+ keychain_spks,
+ stop_gap,
+ batch_size,
+ )?
+ .into_iter()
+ .map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
+ );
+ }
+ }
+
+ populate_with_txids(self, anchor_block, &mut update, &mut txids.iter().cloned())?;
+
+ // [TODO] cache transactions to reduce bandwidth
+ let _txs = populate_with_outpoints(
+ self,
+ anchor_block,
+ &mut update,
+ &mut outpoints.iter().cloned(),
+ )?;
+
+ // check for reorgs during scan process
+ let server_blockhash = self
+ .block_header(anchor_block.height as usize)?
+ .block_hash();
+ if anchor_block.hash != server_blockhash {
+ continue; // reorg
+ }
+
+ update.keychain_update = request_spks
+ .into_keys()
+ .filter_map(|k| {
+ scanned_spks
+ .range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
+ .rev()
+ .find(|(_, (_, active))| *active)
+ .map(|((_, i), _)| (k, *i))
+ })
+ .collect::<BTreeMap<_, _>>();
+ break update;
+ };
+
+ Ok(update)
+ }
+}
+
+/// Prepare an update "template" based on the checkpoints of the `local_chain`.
+fn prepare_chain_update(
+ client: &Client,
+ local_chain: &BTreeMap<u32, BlockHash>,
+) -> Result<LocalChain, Error> {
+ let mut update = LocalChain::default();
+
+ // Find the local chain block that is still there so our update can connect to the local chain.
+ for (&existing_height, &existing_hash) in local_chain.iter().rev() {
+ // TODO: a batch request may be safer, as a reorg that happens when we are obtaining
+ // `block_header`s will result in inconsistencies
+ let current_hash = client.block_header(existing_height as usize)?.block_hash();
+ let _ = update
+ .insert_block(BlockId {
+ height: existing_height,
+ hash: current_hash,
+ })
+ .expect("This never errors because we are working with a fresh chain");
+
+ if current_hash == existing_hash {
+ break;
+ }
+ }
+
+ // Insert the new tip so new transactions will be accepted into the sparsechain.
+ let tip = {
+ let (height, hash) = crate::get_tip(client)?;
+ BlockId { height, hash }
+ };
+ if update.insert_block(tip).is_err() {
+ // There has been a re-org before we even begin scanning addresses.
+ // Just recursively call (this should never happen).
+ return prepare_chain_update(client, local_chain);
+ }
+
+ Ok(update)
+}
+
+fn determine_tx_anchor(
+ anchor_block: BlockId,
+ raw_height: i32,
+ txid: Txid,
+) -> Option<ConfirmationHeightAnchor> {
+ // The electrum API has a weird quirk where an unconfirmed transaction is presented with a
+ // height of 0. To avoid invalid representation in our data structures, we manually set
+ // transactions residing in the genesis block to have height 0, then interpret a height of 0 as
+ // unconfirmed for all other transactions.
+ if txid
+ == Txid::from_hex("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
+ .expect("must deserialize genesis coinbase txid")
+ {
+ return Some(ConfirmationHeightAnchor {
+ anchor_block,
+ confirmation_height: 0,
+ });
+ }
+ match raw_height {
+ h if h <= 0 => {
+ debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
+ None
+ }
+ h => {
+ let h = h as u32;
+ if h > anchor_block.height {
+ None
+ } else {
+ Some(ConfirmationHeightAnchor {
+ anchor_block,
+ confirmation_height: h,
+ })
+ }
+ }
+ }
+}
+
+fn populate_with_outpoints<K>(
+ client: &Client,
+ anchor_block: BlockId,
+ update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
+ outpoints: &mut impl Iterator<Item = OutPoint>,
+) -> Result<HashMap<Txid, Transaction>, Error> {
+ let mut full_txs = HashMap::new();
+ for outpoint in outpoints {
+ let txid = outpoint.txid;
+ let tx = client.transaction_get(&txid)?;
+ debug_assert_eq!(tx.txid(), txid);
+ let txout = match tx.output.get(outpoint.vout as usize) {
+ Some(txout) => txout,
+ None => continue,
+ };
+ // attempt to find the following transactions (alongside their chain positions), and
+ // add to our sparsechain `update`:
+ let mut has_residing = false; // tx in which the outpoint resides
+ let mut has_spending = false; // tx that spends the outpoint
+ for res in client.script_get_history(&txout.script_pubkey)? {
+ if has_residing && has_spending {
+ break;
+ }
+
+ if res.tx_hash == txid {
+ if has_residing {
+ continue;
+ }
+ has_residing = true;
+ full_txs.insert(res.tx_hash, tx.clone());
+ } else {
+ if has_spending {
+ continue;
+ }
+ let res_tx = match full_txs.get(&res.tx_hash) {
+ Some(tx) => tx,
+ None => {
+ let res_tx = client.transaction_get(&res.tx_hash)?;
+ full_txs.insert(res.tx_hash, res_tx);
+ full_txs.get(&res.tx_hash).expect("just inserted")
+ }
+ };
+ has_spending = res_tx
+ .input
+ .iter()
+ .any(|txin| txin.previous_output == outpoint);
+ if !has_spending {
+ continue;
+ }
+ };
+
+ let anchor = determine_tx_anchor(anchor_block, res.height, res.tx_hash);
+
+ let tx_entry = update.graph_update.entry(res.tx_hash).or_default();
+ if let Some(anchor) = anchor {
+ tx_entry.insert(anchor);
+ }
+ }
+ }
+ Ok(full_txs)
+}
+
+fn populate_with_txids<K>(
+ client: &Client,
+ anchor_block: BlockId,
+ update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
+ txids: &mut impl Iterator<Item = Txid>,
+) -> Result<(), Error> {
+ for txid in txids {
+ let tx = match client.transaction_get(&txid) {
+ Ok(tx) => tx,
+ Err(electrum_client::Error::Protocol(_)) => continue,
+ Err(other_err) => return Err(other_err),
+ };
+
+ let spk = tx
+ .output
+ .get(0)
+ .map(|txo| &txo.script_pubkey)
+ .expect("tx must have an output");
+
+ let anchor = match client
+ .script_get_history(spk)?
+ .into_iter()
+ .find(|r| r.tx_hash == txid)
+ {
+ Some(r) => determine_tx_anchor(anchor_block, r.height, txid),
+ None => continue,
+ };
+
+ let tx_entry = update.graph_update.entry(txid).or_default();
+ if let Some(anchor) = anchor {
+ tx_entry.insert(anchor);
+ }
+ }
+ Ok(())
+}
+
+fn populate_with_spks<K, I: Ord + Clone>(
+ client: &Client,
+ anchor_block: BlockId,
+ update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
+ spks: &mut impl Iterator<Item = (I, Script)>,
+ stop_gap: usize,
+ batch_size: usize,
+) -> Result<BTreeMap<I, (Script, bool)>, Error> {
+ let mut unused_spk_count = 0_usize;
+ let mut scanned_spks = BTreeMap::new();
+
+ loop {
+ let spks = (0..batch_size)
+ .map_while(|_| spks.next())
+ .collect::<Vec<_>>();
+ if spks.is_empty() {
+ return Ok(scanned_spks);
+ }
+
+ let spk_histories = client.batch_script_get_history(spks.iter().map(|(_, s)| s))?;
+
+ for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
+ if spk_history.is_empty() {
+ scanned_spks.insert(spk_index, (spk, false));
+ unused_spk_count += 1;
+ if unused_spk_count > stop_gap {
+ return Ok(scanned_spks);
+ }
+ continue;
+ } else {
+ scanned_spks.insert(spk_index, (spk, true));
+ unused_spk_count = 0;
+ }
+
+ for tx in spk_history {
+ let tx_entry = update.graph_update.entry(tx.tx_hash).or_default();
+ if let Some(anchor) = determine_tx_anchor(anchor_block, tx.height, tx.tx_hash) {
+ tx_entry.insert(anchor);
+ }
+ }
+ }
+ }
+}
//! [`batch_transaction_get`]: ElectrumApi::batch_transaction_get
//! [`bdk_electrum_example`]: https://github.com/LLFourn/bdk_core_staging/tree/master/bdk_electrum_example
-use bdk_chain::{
- bitcoin::{hashes::hex::FromHex, BlockHash, OutPoint, Script, Transaction, Txid},
- chain_graph::{self, ChainGraph},
- keychain::KeychainScan,
- sparse_chain::{self, ChainPosition, SparseChain},
- tx_graph::TxGraph,
- BlockId, ConfirmationTime, TxHeight,
-};
+use bdk_chain::bitcoin::BlockHash;
use electrum_client::{Client, ElectrumApi, Error};
-use std::{
- collections::{BTreeMap, HashMap},
- fmt::Debug,
-};
-
-pub mod v2;
+mod electrum_ext;
pub use bdk_chain;
pub use electrum_client;
-
-/// Trait to extend [`electrum_client::Client`] functionality.
-///
-/// Refer to [crate-level documentation] for more.
-///
-/// [crate-level documentation]: crate
-pub trait ElectrumExt {
- /// Fetch the latest block height.
- fn get_tip(&self) -> Result<(u32, BlockHash), Error>;
-
- /// Scan the blockchain (via electrum) for the data specified. This returns a [`ElectrumUpdate`]
- /// which can be transformed into a [`KeychainScan`] after we find all the missing full
- /// transactions.
- ///
- /// - `local_chain`: the most recent block hashes present locally
- /// - `keychain_spks`: keychains that we want to scan transactions for
- /// - `txids`: transactions for which we want the updated [`ChainPosition`]s
- /// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
- /// want to included in the update
- fn scan<K: Ord + Clone>(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
- stop_gap: usize,
- batch_size: usize,
- ) -> Result<ElectrumUpdate<K, TxHeight>, Error>;
-
- /// Convenience method to call [`scan`] without requiring a keychain.
- ///
- /// [`scan`]: ElectrumExt::scan
- fn scan_without_keychain(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- misc_spks: impl IntoIterator<Item = Script>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
- batch_size: usize,
- ) -> Result<SparseChain, Error> {
- let spk_iter = misc_spks
- .into_iter()
- .enumerate()
- .map(|(i, spk)| (i as u32, spk));
-
- self.scan(
- local_chain,
- [((), spk_iter)].into(),
- txids,
- outpoints,
- usize::MAX,
- batch_size,
- )
- .map(|u| u.chain_update)
- }
-}
-
-impl ElectrumExt for Client {
- fn get_tip(&self) -> Result<(u32, BlockHash), Error> {
- // TODO: unsubscribe when added to the client, or is there a better call to use here?
- self.block_headers_subscribe()
- .map(|data| (data.height as u32, data.header.block_hash()))
- }
-
- fn scan<K: Ord + Clone>(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
- stop_gap: usize,
- batch_size: usize,
- ) -> Result<ElectrumUpdate<K, TxHeight>, Error> {
- let mut request_spks = keychain_spks
- .into_iter()
- .map(|(k, s)| {
- let iter = s.into_iter();
- (k, iter)
- })
- .collect::<BTreeMap<K, _>>();
- let mut scanned_spks = BTreeMap::<(K, u32), (Script, bool)>::new();
-
- let txids = txids.into_iter().collect::<Vec<_>>();
- let outpoints = outpoints.into_iter().collect::<Vec<_>>();
-
- let update = loop {
- let mut update = prepare_update(self, local_chain)?;
-
- if !request_spks.is_empty() {
- if !scanned_spks.is_empty() {
- let mut scanned_spk_iter = scanned_spks
- .iter()
- .map(|(i, (spk, _))| (i.clone(), spk.clone()));
- match populate_with_spks::<_, _>(
- self,
- &mut update,
- &mut scanned_spk_iter,
- stop_gap,
- batch_size,
- ) {
- Err(InternalError::Reorg) => continue,
- Err(InternalError::ElectrumError(e)) => return Err(e),
- Ok(mut spks) => scanned_spks.append(&mut spks),
- };
- }
- for (keychain, keychain_spks) in &mut request_spks {
- match populate_with_spks::<u32, _>(
- self,
- &mut update,
- keychain_spks,
- stop_gap,
- batch_size,
- ) {
- Err(InternalError::Reorg) => continue,
- Err(InternalError::ElectrumError(e)) => return Err(e),
- Ok(spks) => scanned_spks.extend(
- spks.into_iter()
- .map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
- ),
- };
- }
- }
-
- match populate_with_txids(self, &mut update, &mut txids.iter().cloned()) {
- Err(InternalError::Reorg) => continue,
- Err(InternalError::ElectrumError(e)) => return Err(e),
- Ok(_) => {}
- }
-
- match populate_with_outpoints(self, &mut update, &mut outpoints.iter().cloned()) {
- Err(InternalError::Reorg) => continue,
- Err(InternalError::ElectrumError(e)) => return Err(e),
- Ok(_txs) => { /* [TODO] cache full txs to reduce bandwidth */ }
- }
-
- // check for reorgs during scan process
- let our_tip = update
- .latest_checkpoint()
- .expect("update must have atleast one checkpoint");
- let server_blockhash = self.block_header(our_tip.height as usize)?.block_hash();
- if our_tip.hash != server_blockhash {
- continue; // reorg
- } else {
- break update;
- }
- };
-
- let last_active_index = request_spks
- .into_keys()
- .filter_map(|k| {
- scanned_spks
- .range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
- .rev()
- .find(|(_, (_, active))| *active)
- .map(|((_, i), _)| (k, *i))
- })
- .collect::<BTreeMap<_, _>>();
-
- Ok(ElectrumUpdate {
- chain_update: update,
- last_active_indices: last_active_index,
- })
- }
-}
-
-/// The result of [`ElectrumExt::scan`].
-pub struct ElectrumUpdate<K, P> {
- /// The internal [`SparseChain`] update.
- pub chain_update: SparseChain<P>,
- /// The last keychain script pubkey indices, which had transaction histories.
- pub last_active_indices: BTreeMap<K, u32>,
-}
-
-impl<K, P> Default for ElectrumUpdate<K, P> {
- fn default() -> Self {
- Self {
- chain_update: Default::default(),
- last_active_indices: Default::default(),
- }
- }
-}
-
-impl<K, P> AsRef<SparseChain<P>> for ElectrumUpdate<K, P> {
- fn as_ref(&self) -> &SparseChain<P> {
- &self.chain_update
- }
-}
-
-impl<K: Ord + Clone + Debug, P: ChainPosition> ElectrumUpdate<K, P> {
- /// Return a list of missing full transactions that are required to [`inflate_update`].
- ///
- /// [`inflate_update`]: bdk_chain::chain_graph::ChainGraph::inflate_update
- pub fn missing_full_txs<G>(&self, graph: G) -> Vec<&Txid>
- where
- G: AsRef<TxGraph>,
- {
- self.chain_update
- .txids()
- .filter(|(_, txid)| graph.as_ref().get_tx(*txid).is_none())
- .map(|(_, txid)| txid)
- .collect()
- }
-
- /// Transform the [`ElectrumUpdate`] into a [`KeychainScan`], which can be applied to a
- /// `tracker`.
- ///
- /// This will fail if there are missing full transactions not provided via `new_txs`.
- pub fn into_keychain_scan<CG>(
- self,
- new_txs: Vec<Transaction>,
- chain_graph: &CG,
- ) -> Result<KeychainScan<K, P>, chain_graph::NewError<P>>
- where
- CG: AsRef<ChainGraph<P>>,
- {
- Ok(KeychainScan {
- update: chain_graph
- .as_ref()
- .inflate_update(self.chain_update, new_txs)?,
- last_active_indices: self.last_active_indices,
- })
- }
-}
-
-impl<K: Ord + Clone + Debug> ElectrumUpdate<K, TxHeight> {
- /// Creates [`ElectrumUpdate<K, ConfirmationTime>`] from [`ElectrumUpdate<K, TxHeight>`].
- pub fn into_confirmation_time_update(
- self,
- client: &electrum_client::Client,
- ) -> Result<ElectrumUpdate<K, ConfirmationTime>, Error> {
- let heights = self
- .chain_update
- .range_txids_by_height(..TxHeight::Unconfirmed)
- .map(|(h, _)| match h {
- TxHeight::Confirmed(h) => *h,
- _ => unreachable!("already filtered out unconfirmed"),
- })
- .collect::<Vec<u32>>();
-
- let height_to_time = heights
- .clone()
- .into_iter()
- .zip(
- client
- .batch_block_header(heights)?
- .into_iter()
- .map(|bh| bh.time as u64),
- )
- .collect::<HashMap<u32, u64>>();
-
- let mut new_update = SparseChain::<ConfirmationTime>::from_checkpoints(
- self.chain_update.range_checkpoints(..),
- );
-
- for &(tx_height, txid) in self.chain_update.txids() {
- let conf_time = match tx_height {
- TxHeight::Confirmed(height) => ConfirmationTime::Confirmed {
- height,
- time: height_to_time[&height],
- },
- TxHeight::Unconfirmed => ConfirmationTime::Unconfirmed { last_seen: 0 },
- };
- let _ = new_update.insert_tx(txid, conf_time).expect("must insert");
- }
-
- Ok(ElectrumUpdate {
- chain_update: new_update,
- last_active_indices: self.last_active_indices,
- })
- }
-}
-
-#[derive(Debug)]
-enum InternalError {
- ElectrumError(Error),
- Reorg,
-}
-
-impl From<electrum_client::Error> for InternalError {
- fn from(value: electrum_client::Error) -> Self {
- Self::ElectrumError(value)
- }
-}
+pub use electrum_ext::*;
fn get_tip(client: &Client) -> Result<(u32, BlockHash), Error> {
// TODO: unsubscribe when added to the client, or is there a better call to use here?
.block_headers_subscribe()
.map(|data| (data.height as u32, data.header.block_hash()))
}
-
-/// Prepare an update sparsechain "template" based on the checkpoints of the `local_chain`.
-fn prepare_update(
- client: &Client,
- local_chain: &BTreeMap<u32, BlockHash>,
-) -> Result<SparseChain, Error> {
- let mut update = SparseChain::default();
-
- // Find the local chain block that is still there so our update can connect to the local chain.
- for (&existing_height, &existing_hash) in local_chain.iter().rev() {
- // TODO: a batch request may be safer, as a reorg that happens when we are obtaining
- // `block_header`s will result in inconsistencies
- let current_hash = client.block_header(existing_height as usize)?.block_hash();
- let _ = update
- .insert_checkpoint(BlockId {
- height: existing_height,
- hash: current_hash,
- })
- .expect("This never errors because we are working with a fresh chain");
-
- if current_hash == existing_hash {
- break;
- }
- }
-
- // Insert the new tip so new transactions will be accepted into the sparsechain.
- let tip = {
- let (height, hash) = get_tip(client)?;
- BlockId { height, hash }
- };
- if let Err(failure) = update.insert_checkpoint(tip) {
- match failure {
- sparse_chain::InsertCheckpointError::HashNotMatching { .. } => {
- // There has been a re-org before we even begin scanning addresses.
- // Just recursively call (this should never happen).
- return prepare_update(client, local_chain);
- }
- }
- }
-
- Ok(update)
-}
-
-/// This atrocity is required because electrum thinks a height of 0 means "unconfirmed", but there is
-/// such thing as a genesis block.
-///
-/// We contain an expectation for the genesis coinbase txid to always have a chain position of
-/// [`TxHeight::Confirmed(0)`].
-fn determine_tx_height(raw_height: i32, tip_height: u32, txid: Txid) -> TxHeight {
- if txid
- == Txid::from_hex("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
- .expect("must deserialize genesis coinbase txid")
- {
- return TxHeight::Confirmed(0);
- }
- match raw_height {
- h if h <= 0 => {
- debug_assert!(
- h == 0 || h == -1,
- "unexpected height ({}) from electrum server",
- h
- );
- TxHeight::Unconfirmed
- }
- h => {
- let h = h as u32;
- if h > tip_height {
- TxHeight::Unconfirmed
- } else {
- TxHeight::Confirmed(h)
- }
- }
- }
-}
-
-/// Populates the update [`SparseChain`] with related transactions and associated [`ChainPosition`]s
-/// of the provided `outpoints` (this is the tx which contains the outpoint and the one spending the
-/// outpoint).
-///
-/// Unfortunately, this is awkward to implement as electrum does not provide such an API. Instead, we
-/// will get the tx history of the outpoint's spk and try to find the containing tx and the
-/// spending tx.
-fn populate_with_outpoints(
- client: &Client,
- update: &mut SparseChain,
- outpoints: &mut impl Iterator<Item = OutPoint>,
-) -> Result<HashMap<Txid, Transaction>, InternalError> {
- let tip = update
- .latest_checkpoint()
- .expect("update must atleast have one checkpoint");
-
- let mut full_txs = HashMap::new();
- for outpoint in outpoints {
- let txid = outpoint.txid;
- let tx = client.transaction_get(&txid)?;
- debug_assert_eq!(tx.txid(), txid);
- let txout = match tx.output.get(outpoint.vout as usize) {
- Some(txout) => txout,
- None => continue,
- };
-
- // attempt to find the following transactions (alongside their chain positions), and
- // add to our sparsechain `update`:
- let mut has_residing = false; // tx in which the outpoint resides
- let mut has_spending = false; // tx that spends the outpoint
- for res in client.script_get_history(&txout.script_pubkey)? {
- if has_residing && has_spending {
- break;
- }
-
- if res.tx_hash == txid {
- if has_residing {
- continue;
- }
- has_residing = true;
- full_txs.insert(res.tx_hash, tx.clone());
- } else {
- if has_spending {
- continue;
- }
- let res_tx = match full_txs.get(&res.tx_hash) {
- Some(tx) => tx,
- None => {
- let res_tx = client.transaction_get(&res.tx_hash)?;
- full_txs.insert(res.tx_hash, res_tx);
- full_txs.get(&res.tx_hash).expect("just inserted")
- }
- };
- has_spending = res_tx
- .input
- .iter()
- .any(|txin| txin.previous_output == outpoint);
- if !has_spending {
- continue;
- }
- };
-
- let tx_height = determine_tx_height(res.height, tip.height, res.tx_hash);
-
- if let Err(failure) = update.insert_tx(res.tx_hash, tx_height) {
- match failure {
- sparse_chain::InsertTxError::TxTooHigh { .. } => {
- unreachable!("we should never encounter this as we ensured height <= tip");
- }
- sparse_chain::InsertTxError::TxMovedUnexpectedly { .. } => {
- return Err(InternalError::Reorg);
- }
- }
- }
- }
- }
- Ok(full_txs)
-}
-
-/// Populate an update [`SparseChain`] with transactions (and associated block positions) from
-/// the given `txids`.
-fn populate_with_txids(
- client: &Client,
- update: &mut SparseChain,
- txids: &mut impl Iterator<Item = Txid>,
-) -> Result<(), InternalError> {
- let tip = update
- .latest_checkpoint()
- .expect("update must have atleast one checkpoint");
- for txid in txids {
- let tx = match client.transaction_get(&txid) {
- Ok(tx) => tx,
- Err(electrum_client::Error::Protocol(_)) => continue,
- Err(other_err) => return Err(other_err.into()),
- };
-
- let spk = tx
- .output
- .get(0)
- .map(|txo| &txo.script_pubkey)
- .expect("tx must have an output");
-
- let tx_height = match client
- .script_get_history(spk)?
- .into_iter()
- .find(|r| r.tx_hash == txid)
- {
- Some(r) => determine_tx_height(r.height, tip.height, r.tx_hash),
- None => continue,
- };
-
- if let Err(failure) = update.insert_tx(txid, tx_height) {
- match failure {
- sparse_chain::InsertTxError::TxTooHigh { .. } => {
- unreachable!("we should never encounter this as we ensured height <= tip");
- }
- sparse_chain::InsertTxError::TxMovedUnexpectedly { .. } => {
- return Err(InternalError::Reorg);
- }
- }
- }
- }
- Ok(())
-}
-
-/// Populate an update [`SparseChain`] with transactions (and associated block positions) from
-/// the transaction history of the provided `spk`s.
-fn populate_with_spks<I, S>(
- client: &Client,
- update: &mut SparseChain,
- spks: &mut S,
- stop_gap: usize,
- batch_size: usize,
-) -> Result<BTreeMap<I, (Script, bool)>, InternalError>
-where
- I: Ord + Clone,
- S: Iterator<Item = (I, Script)>,
-{
- let tip = update.latest_checkpoint().map_or(0, |cp| cp.height);
- let mut unused_spk_count = 0_usize;
- let mut scanned_spks = BTreeMap::new();
-
- loop {
- let spks = (0..batch_size)
- .map_while(|_| spks.next())
- .collect::<Vec<_>>();
- if spks.is_empty() {
- return Ok(scanned_spks);
- }
-
- let spk_histories = client.batch_script_get_history(spks.iter().map(|(_, s)| s))?;
-
- for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
- if spk_history.is_empty() {
- scanned_spks.insert(spk_index, (spk, false));
- unused_spk_count += 1;
- if unused_spk_count > stop_gap {
- return Ok(scanned_spks);
- }
- continue;
- } else {
- scanned_spks.insert(spk_index, (spk, true));
- unused_spk_count = 0;
- }
-
- for tx in spk_history {
- let tx_height = determine_tx_height(tx.height, tip, tx.tx_hash);
-
- if let Err(failure) = update.insert_tx(tx.tx_hash, tx_height) {
- match failure {
- sparse_chain::InsertTxError::TxTooHigh { .. } => {
- unreachable!(
- "we should never encounter this as we ensured height <= tip"
- );
- }
- sparse_chain::InsertTxError::TxMovedUnexpectedly { .. } => {
- return Err(InternalError::Reorg);
- }
- }
- }
- }
- }
- }
-}
+++ /dev/null
-use bdk_chain::{
- bitcoin::{hashes::hex::FromHex, BlockHash, OutPoint, Script, Transaction, Txid},
- keychain::LocalUpdate,
- local_chain::LocalChain,
- tx_graph::{self, TxGraph},
- Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeAnchor,
-};
-use electrum_client::{Client, ElectrumApi, Error};
-use std::{
- collections::{BTreeMap, BTreeSet, HashMap, HashSet},
- fmt::Debug,
-};
-
-use crate::InternalError;
-
-#[derive(Debug, Clone)]
-pub struct ElectrumUpdate<K, A> {
- pub graph_update: HashMap<Txid, BTreeSet<A>>,
- pub chain_update: LocalChain,
- pub keychain_update: BTreeMap<K, u32>,
-}
-
-impl<K, A> Default for ElectrumUpdate<K, A> {
- fn default() -> Self {
- Self {
- graph_update: Default::default(),
- chain_update: Default::default(),
- keychain_update: Default::default(),
- }
- }
-}
-
-impl<K, A: Anchor> ElectrumUpdate<K, A> {
- pub fn missing_full_txs<A2>(&self, graph: &TxGraph<A2>) -> Vec<Txid> {
- self.graph_update
- .keys()
- .filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
- .cloned()
- .collect()
- }
-
- pub fn finalize(
- self,
- client: &Client,
- seen_at: Option<u64>,
- missing: Vec<Txid>,
- ) -> Result<LocalUpdate<K, A>, Error> {
- let new_txs = client.batch_transaction_get(&missing)?;
- let mut graph_update = TxGraph::<A>::new(new_txs);
- for (txid, anchors) in self.graph_update {
- if let Some(seen_at) = seen_at {
- let _ = graph_update.insert_seen_at(txid, seen_at);
- }
- for anchor in anchors {
- let _ = graph_update.insert_anchor(txid, anchor);
- }
- }
- Ok(LocalUpdate {
- keychain: self.keychain_update,
- graph: graph_update,
- chain: self.chain_update,
- })
- }
-}
-
-impl<K> ElectrumUpdate<K, ConfirmationHeightAnchor> {
- /// Finalizes the [`ElectrumUpdate`] with `new_txs` and anchors of type
- /// [`ConfirmationTimeAnchor`].
- ///
- /// **Note:** The confirmation time might not be precisely correct if there has been a reorg.
- /// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
- /// use it.
- pub fn finalize_as_confirmation_time(
- self,
- client: &Client,
- seen_at: Option<u64>,
- missing: Vec<Txid>,
- ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error> {
- let update = self.finalize(client, seen_at, missing)?;
-
- let relevant_heights = {
- let mut visited_heights = HashSet::new();
- update
- .graph
- .all_anchors()
- .iter()
- .map(|(a, _)| a.confirmation_height_upper_bound())
- .filter(move |&h| visited_heights.insert(h))
- .collect::<Vec<_>>()
- };
-
- let height_to_time = relevant_heights
- .clone()
- .into_iter()
- .zip(
- client
- .batch_block_header(relevant_heights)?
- .into_iter()
- .map(|bh| bh.time as u64),
- )
- .collect::<HashMap<u32, u64>>();
-
- let graph_additions = {
- let old_additions = TxGraph::default().determine_additions(&update.graph);
- tx_graph::Additions {
- tx: old_additions.tx,
- txout: old_additions.txout,
- last_seen: old_additions.last_seen,
- anchors: old_additions
- .anchors
- .into_iter()
- .map(|(height_anchor, txid)| {
- let confirmation_height = height_anchor.confirmation_height;
- let confirmation_time = height_to_time[&confirmation_height];
- let time_anchor = ConfirmationTimeAnchor {
- anchor_block: height_anchor.anchor_block,
- confirmation_height,
- confirmation_time,
- };
- (time_anchor, txid)
- })
- .collect(),
- }
- };
-
- Ok(LocalUpdate {
- keychain: update.keychain,
- graph: {
- let mut graph = TxGraph::default();
- graph.apply_additions(graph_additions);
- graph
- },
- chain: update.chain,
- })
- }
-}
-
-pub trait ElectrumExt<A> {
- fn get_tip(&self) -> Result<(u32, BlockHash), Error>;
-
- fn scan<K: Ord + Clone>(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
- stop_gap: usize,
- batch_size: usize,
- ) -> Result<ElectrumUpdate<K, A>, Error>;
-
- fn scan_without_keychain(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- misc_spks: impl IntoIterator<Item = Script>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
- batch_size: usize,
- ) -> Result<ElectrumUpdate<(), A>, Error> {
- let spk_iter = misc_spks
- .into_iter()
- .enumerate()
- .map(|(i, spk)| (i as u32, spk));
-
- self.scan(
- local_chain,
- [((), spk_iter)].into(),
- txids,
- outpoints,
- usize::MAX,
- batch_size,
- )
- }
-}
-
-impl ElectrumExt<ConfirmationHeightAnchor> for Client {
- fn get_tip(&self) -> Result<(u32, BlockHash), Error> {
- // TODO: unsubscribe when added to the client, or is there a better call to use here?
- self.block_headers_subscribe()
- .map(|data| (data.height as u32, data.header.block_hash()))
- }
-
- fn scan<K: Ord + Clone>(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
- stop_gap: usize,
- batch_size: usize,
- ) -> Result<ElectrumUpdate<K, ConfirmationHeightAnchor>, Error> {
- let mut request_spks = keychain_spks
- .into_iter()
- .map(|(k, s)| (k, s.into_iter()))
- .collect::<BTreeMap<K, _>>();
- let mut scanned_spks = BTreeMap::<(K, u32), (Script, bool)>::new();
-
- let txids = txids.into_iter().collect::<Vec<_>>();
- let outpoints = outpoints.into_iter().collect::<Vec<_>>();
-
- let update = loop {
- let mut update = ElectrumUpdate::<K, ConfirmationHeightAnchor> {
- chain_update: prepare_chain_update(self, local_chain)?,
- ..Default::default()
- };
- let anchor_block = update
- .chain_update
- .tip()
- .expect("must have atleast one block");
-
- if !request_spks.is_empty() {
- if !scanned_spks.is_empty() {
- let mut scanned_spk_iter = scanned_spks
- .iter()
- .map(|(i, (spk, _))| (i.clone(), spk.clone()));
- match populate_with_spks(
- self,
- anchor_block,
- &mut update,
- &mut scanned_spk_iter,
- stop_gap,
- batch_size,
- ) {
- Err(InternalError::Reorg) => continue,
- Err(InternalError::ElectrumError(e)) => return Err(e),
- Ok(mut spks) => scanned_spks.append(&mut spks),
- };
- }
- for (keychain, keychain_spks) in &mut request_spks {
- match populate_with_spks(
- self,
- anchor_block,
- &mut update,
- keychain_spks,
- stop_gap,
- batch_size,
- ) {
- Err(InternalError::Reorg) => continue,
- Err(InternalError::ElectrumError(e)) => return Err(e),
- Ok(spks) => scanned_spks.extend(
- spks.into_iter()
- .map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
- ),
- };
- }
- }
-
- match populate_with_txids(self, anchor_block, &mut update, &mut txids.iter().cloned()) {
- Err(InternalError::Reorg) => continue,
- Err(InternalError::ElectrumError(e)) => return Err(e),
- Ok(_) => {}
- }
-
- match populate_with_outpoints(
- self,
- anchor_block,
- &mut update,
- &mut outpoints.iter().cloned(),
- ) {
- Err(InternalError::Reorg) => continue,
- Err(InternalError::ElectrumError(e)) => return Err(e),
- Ok(_txs) => { /* [TODO] cache full txs to reduce bandwidth */ }
- }
-
- // check for reorgs during scan process
- let server_blockhash = self
- .block_header(anchor_block.height as usize)?
- .block_hash();
- if anchor_block.hash != server_blockhash {
- continue; // reorg
- }
-
- update.keychain_update = request_spks
- .into_keys()
- .filter_map(|k| {
- scanned_spks
- .range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
- .rev()
- .find(|(_, (_, active))| *active)
- .map(|((_, i), _)| (k, *i))
- })
- .collect::<BTreeMap<_, _>>();
- break update;
- };
-
- Ok(update)
- }
-}
-
-/// Prepare an update "template" based on the checkpoints of the `local_chain`.
-fn prepare_chain_update(
- client: &Client,
- local_chain: &BTreeMap<u32, BlockHash>,
-) -> Result<LocalChain, Error> {
- let mut update = LocalChain::default();
-
- // Find the local chain block that is still there so our update can connect to the local chain.
- for (&existing_height, &existing_hash) in local_chain.iter().rev() {
- // TODO: a batch request may be safer, as a reorg that happens when we are obtaining
- // `block_header`s will result in inconsistencies
- let current_hash = client.block_header(existing_height as usize)?.block_hash();
- let _ = update
- .insert_block(BlockId {
- height: existing_height,
- hash: current_hash,
- })
- .expect("This never errors because we are working with a fresh chain");
-
- if current_hash == existing_hash {
- break;
- }
- }
-
- // Insert the new tip so new transactions will be accepted into the sparsechain.
- let tip = {
- let (height, hash) = crate::get_tip(client)?;
- BlockId { height, hash }
- };
- if update.insert_block(tip).is_err() {
- // There has been a re-org before we even begin scanning addresses.
- // Just recursively call (this should never happen).
- return prepare_chain_update(client, local_chain);
- }
-
- Ok(update)
-}
-
-fn determine_tx_anchor(
- anchor_block: BlockId,
- raw_height: i32,
- txid: Txid,
-) -> Option<ConfirmationHeightAnchor> {
- // The electrum API has a weird quirk where an unconfirmed transaction is presented with a
- // height of 0. To avoid invalid representation in our data structures, we manually set
- // transactions residing in the genesis block to have height 0, then interpret a height of 0 as
- // unconfirmed for all other transactions.
- if txid
- == Txid::from_hex("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
- .expect("must deserialize genesis coinbase txid")
- {
- return Some(ConfirmationHeightAnchor {
- anchor_block,
- confirmation_height: 0,
- });
- }
- match raw_height {
- h if h <= 0 => {
- debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
- None
- }
- h => {
- let h = h as u32;
- if h > anchor_block.height {
- None
- } else {
- Some(ConfirmationHeightAnchor {
- anchor_block,
- confirmation_height: h,
- })
- }
- }
- }
-}
-
-fn populate_with_outpoints<K>(
- client: &Client,
- anchor_block: BlockId,
- update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
- outpoints: &mut impl Iterator<Item = OutPoint>,
-) -> Result<HashMap<Txid, Transaction>, InternalError> {
- let mut full_txs = HashMap::new();
- for outpoint in outpoints {
- let txid = outpoint.txid;
- let tx = client.transaction_get(&txid)?;
- debug_assert_eq!(tx.txid(), txid);
- let txout = match tx.output.get(outpoint.vout as usize) {
- Some(txout) => txout,
- None => continue,
- };
- // attempt to find the following transactions (alongside their chain positions), and
- // add to our sparsechain `update`:
- let mut has_residing = false; // tx in which the outpoint resides
- let mut has_spending = false; // tx that spends the outpoint
- for res in client.script_get_history(&txout.script_pubkey)? {
- if has_residing && has_spending {
- break;
- }
-
- if res.tx_hash == txid {
- if has_residing {
- continue;
- }
- has_residing = true;
- full_txs.insert(res.tx_hash, tx.clone());
- } else {
- if has_spending {
- continue;
- }
- let res_tx = match full_txs.get(&res.tx_hash) {
- Some(tx) => tx,
- None => {
- let res_tx = client.transaction_get(&res.tx_hash)?;
- full_txs.insert(res.tx_hash, res_tx);
- full_txs.get(&res.tx_hash).expect("just inserted")
- }
- };
- has_spending = res_tx
- .input
- .iter()
- .any(|txin| txin.previous_output == outpoint);
- if !has_spending {
- continue;
- }
- };
-
- let anchor = determine_tx_anchor(anchor_block, res.height, res.tx_hash);
-
- let tx_entry = update.graph_update.entry(res.tx_hash).or_default();
- if let Some(anchor) = anchor {
- tx_entry.insert(anchor);
- }
- }
- }
- Ok(full_txs)
-}
-
-fn populate_with_txids<K>(
- client: &Client,
- anchor_block: BlockId,
- update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
- txids: &mut impl Iterator<Item = Txid>,
-) -> Result<(), InternalError> {
- for txid in txids {
- let tx = match client.transaction_get(&txid) {
- Ok(tx) => tx,
- Err(electrum_client::Error::Protocol(_)) => continue,
- Err(other_err) => return Err(other_err.into()),
- };
-
- let spk = tx
- .output
- .get(0)
- .map(|txo| &txo.script_pubkey)
- .expect("tx must have an output");
-
- let anchor = match client
- .script_get_history(spk)?
- .into_iter()
- .find(|r| r.tx_hash == txid)
- {
- Some(r) => determine_tx_anchor(anchor_block, r.height, txid),
- None => continue,
- };
-
- let tx_entry = update.graph_update.entry(txid).or_default();
- if let Some(anchor) = anchor {
- tx_entry.insert(anchor);
- }
- }
- Ok(())
-}
-
-fn populate_with_spks<K, I: Ord + Clone>(
- client: &Client,
- anchor_block: BlockId,
- update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
- spks: &mut impl Iterator<Item = (I, Script)>,
- stop_gap: usize,
- batch_size: usize,
-) -> Result<BTreeMap<I, (Script, bool)>, InternalError> {
- let mut unused_spk_count = 0_usize;
- let mut scanned_spks = BTreeMap::new();
-
- loop {
- let spks = (0..batch_size)
- .map_while(|_| spks.next())
- .collect::<Vec<_>>();
- if spks.is_empty() {
- return Ok(scanned_spks);
- }
-
- let spk_histories = client.batch_script_get_history(spks.iter().map(|(_, s)| s))?;
-
- for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
- if spk_history.is_empty() {
- scanned_spks.insert(spk_index, (spk, false));
- unused_spk_count += 1;
- if unused_spk_count > stop_gap {
- return Ok(scanned_spks);
- }
- continue;
- } else {
- scanned_spks.insert(spk_index, (spk, true));
- unused_spk_count = 0;
- }
-
- for tx in spk_history {
- let tx_entry = update.graph_update.entry(tx.tx_hash).or_default();
- if let Some(anchor) = determine_tx_anchor(anchor_block, tx.height, tx.tx_hash) {
- tx_entry.insert(anchor);
- }
- }
- }
- }
-}
[dependencies]
bdk_chain = { path = "../chain", version = "0.4.0", features = ["serde", "miniscript"] }
-esplora-client = { version = "0.3", default-features = false }
+esplora-client = { version = "0.5", default-features = false }
async-trait = { version = "0.1.66", optional = true }
futures = { version = "0.3.26", optional = true }
[features]
-default = ["async-https", "blocking"]
+default = ["blocking"]
async = ["async-trait", "futures", "esplora-client/async"]
async-https = ["async", "esplora-client/async-https"]
blocking = ["esplora-client/blocking"]
// for blocking
use bdk_esplora::EsploraExt;
// for async
-use bdk_esplora::EsploraAsyncExt;
+// use bdk_esplora::EsploraAsyncExt;
```
For full examples, refer to [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora) (blocking) and [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async).
-use std::collections::BTreeMap;
-
use async_trait::async_trait;
use bdk_chain::{
bitcoin::{BlockHash, OutPoint, Script, Txid},
- chain_graph::ChainGraph,
- keychain::KeychainScan,
- sparse_chain, BlockId, ConfirmationTime,
+ collections::BTreeMap,
+ keychain::LocalUpdate,
+ BlockId, ConfirmationTimeAnchor,
};
-use esplora_client::{Error, OutputStatus};
-use futures::stream::{FuturesOrdered, TryStreamExt};
+use esplora_client::{Error, OutputStatus, TxStatus};
+use futures::{stream::FuturesOrdered, TryStreamExt};
-use crate::map_confirmation_time;
+use crate::map_confirmation_time_anchor;
/// Trait to extend [`esplora_client::AsyncClient`] functionality.
///
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EsploraAsyncExt {
- /// Scan the blockchain (via esplora) for the data specified and returns a [`KeychainScan`].
+ /// Scan the blockchain (via esplora) for the data specified and returns a
+ /// [`LocalUpdate<K, ConfirmationTimeAnchor>`].
///
/// - `local_chain`: the most recent block hashes present locally
/// - `keychain_spks`: keychains that we want to scan transactions for
- /// - `txids`: transactions for which we want updated [`ChainPosition`]s
+ /// - `txids`: transactions for which we want updated [`ConfirmationTimeAnchor`]s
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to included in the update
///
/// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
/// parallel.
- ///
- /// [`ChainPosition`]: bdk_chain::sparse_chain::ChainPosition
#[allow(clippy::result_large_err)] // FIXME
async fn scan<K: Ord + Clone + Send>(
&self,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
stop_gap: usize,
parallel_requests: usize,
- ) -> Result<KeychainScan<K, ConfirmationTime>, Error>;
+ ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error>;
/// Convenience method to call [`scan`] without requiring a keychain.
///
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
parallel_requests: usize,
- ) -> Result<ChainGraph<ConfirmationTime>, Error> {
- let wallet_scan = self
- .scan(
- local_chain,
- [(
- (),
- misc_spks
- .into_iter()
- .enumerate()
- .map(|(i, spk)| (i as u32, spk)),
- )]
- .into(),
- txids,
- outpoints,
- usize::MAX,
- parallel_requests,
- )
- .await?;
-
- Ok(wallet_scan.update)
+ ) -> Result<LocalUpdate<(), ConfirmationTimeAnchor>, Error> {
+ self.scan(
+ local_chain,
+ [(
+ (),
+ misc_spks
+ .into_iter()
+ .enumerate()
+ .map(|(i, spk)| (i as u32, spk)),
+ )]
+ .into(),
+ txids,
+ outpoints,
+ usize::MAX,
+ parallel_requests,
+ )
+ .await
}
}
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
stop_gap: usize,
parallel_requests: usize,
- ) -> Result<KeychainScan<K, ConfirmationTime>, Error> {
- let txids = txids.into_iter();
- let outpoints = outpoints.into_iter();
+ ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error> {
let parallel_requests = Ord::max(parallel_requests, 1);
- let mut scan = KeychainScan::default();
- let update = &mut scan.update;
- let last_active_indices = &mut scan.last_active_indices;
- for (&height, &original_hash) in local_chain.iter().rev() {
- let update_block_id = BlockId {
- height,
- hash: self.get_block_hash(height).await?,
+ let (mut update, tip_at_start) = loop {
+ let mut update = LocalUpdate::<K, ConfirmationTimeAnchor>::default();
+
+ for (&height, &original_hash) in local_chain.iter().rev() {
+ let update_block_id = BlockId {
+ height,
+ hash: self.get_block_hash(height).await?,
+ };
+ let _ = update
+ .chain
+ .insert_block(update_block_id)
+ .expect("cannot repeat height here");
+ if update_block_id.hash == original_hash {
+ break;
+ }
+ }
+
+ let tip_at_start = BlockId {
+ height: self.get_height().await?,
+ hash: self.get_tip_hash().await?,
};
- let _ = update
- .insert_checkpoint(update_block_id)
- .expect("cannot repeat height here");
- if update_block_id.hash == original_hash {
- break;
+
+ if update.chain.insert_block(tip_at_start).is_ok() {
+ break (update, tip_at_start);
}
- }
- let tip_at_start = BlockId {
- height: self.get_height().await?,
- hash: self.get_tip_hash().await?,
};
- if let Err(failure) = update.insert_checkpoint(tip_at_start) {
- match failure {
- sparse_chain::InsertCheckpointError::HashNotMatching { .. } => {
- // there was a re-org before we started scanning. We haven't consumed any iterators, so calling this function recursively is safe.
- return EsploraAsyncExt::scan(
- self,
- local_chain,
- keychain_spks,
- txids,
- outpoints,
- stop_gap,
- parallel_requests,
- )
- .await;
- }
- }
- }
for (keychain, spks) in keychain_spks {
let mut spks = spks.into_iter();
type IndexWithTxs = (u32, Vec<esplora_client::Tx>);
loop {
- let futures: FuturesOrdered<_> = (0..parallel_requests)
+ let futures = (0..parallel_requests)
.filter_map(|_| {
let (index, script) = spks.next()?;
let client = self.clone();
Result::<_, esplora_client::Error>::Ok((index, related_txs))
})
})
- .collect();
+ .collect::<FuturesOrdered<_>>();
let n_futures = futures.len();
- let idx_with_tx: Vec<IndexWithTxs> = futures.try_collect().await?;
-
- for (index, related_txs) in idx_with_tx {
+ for (index, related_txs) in futures.try_collect::<Vec<IndexWithTxs>>().await? {
if related_txs.is_empty() {
empty_scripts += 1;
} else {
empty_scripts = 0;
}
for tx in related_txs {
- let confirmation_time =
- map_confirmation_time(&tx.status, tip_at_start.height);
+ let anchor = map_confirmation_time_anchor(&tx.status, tip_at_start);
- if let Err(failure) = update.insert_tx(tx.to_tx(), confirmation_time) {
- use bdk_chain::{
- chain_graph::InsertTxError, sparse_chain::InsertTxError::*,
- };
- match failure {
- InsertTxError::Chain(TxTooHigh { .. }) => {
- unreachable!("chain position already checked earlier")
- }
- InsertTxError::Chain(TxMovedUnexpectedly { .. })
- | InsertTxError::UnresolvableConflict(_) => {
- /* implies reorg during a scan. We deal with that below */
- }
- }
+ let _ = update.graph.insert_tx(tx.to_tx());
+ if let Some(anchor) = anchor {
+ let _ = update.graph.insert_anchor(tx.txid, anchor);
}
}
}
}
if let Some(last_active_index) = last_active_index {
- last_active_indices.insert(keychain, last_active_index);
+ update.keychain.insert(keychain, last_active_index);
}
}
- for txid in txids {
- let (tx, tx_status) =
- match (self.get_tx(&txid).await?, self.get_tx_status(&txid).await?) {
- (Some(tx), Some(tx_status)) => (tx, tx_status),
- _ => continue,
- };
-
- let confirmation_time = map_confirmation_time(&tx_status, tip_at_start.height);
-
- if let Err(failure) = update.insert_tx(tx, confirmation_time) {
- use bdk_chain::{chain_graph::InsertTxError, sparse_chain::InsertTxError::*};
- match failure {
- InsertTxError::Chain(TxTooHigh { .. }) => {
- unreachable!("chain position already checked earlier")
+ for txid in txids.into_iter() {
+ if update.graph.get_tx(txid).is_none() {
+ match self.get_tx(&txid).await? {
+ Some(tx) => {
+ let _ = update.graph.insert_tx(tx);
}
- InsertTxError::Chain(TxMovedUnexpectedly { .. })
- | InsertTxError::UnresolvableConflict(_) => {
- /* implies reorg during a scan. We deal with that below */
+ None => continue,
+ }
+ }
+ match self.get_tx_status(&txid).await? {
+ tx_status if tx_status.confirmed => {
+ if let Some(anchor) = map_confirmation_time_anchor(&tx_status, tip_at_start) {
+ let _ = update.graph.insert_anchor(txid, anchor);
}
}
+ _ => continue,
}
}
- for op in outpoints {
+ for op in outpoints.into_iter() {
let mut op_txs = Vec::with_capacity(2);
- if let (Some(tx), Some(tx_status)) = (
+ if let (
+ Some(tx),
+ tx_status @ TxStatus {
+ confirmed: true, ..
+ },
+ ) = (
self.get_tx(&op.txid).await?,
self.get_tx_status(&op.txid).await?,
) {
}
for (tx, status) in op_txs {
- let confirmation_time = map_confirmation_time(&status, tip_at_start.height);
+ let txid = tx.txid();
+ let anchor = map_confirmation_time_anchor(&status, tip_at_start);
- if let Err(failure) = update.insert_tx(tx, confirmation_time) {
- use bdk_chain::{chain_graph::InsertTxError, sparse_chain::InsertTxError::*};
- match failure {
- InsertTxError::Chain(TxTooHigh { .. }) => {
- unreachable!("chain position already checked earlier")
- }
- InsertTxError::Chain(TxMovedUnexpectedly { .. })
- | InsertTxError::UnresolvableConflict(_) => {
- /* implies reorg during a scan. We deal with that below */
- }
- }
+ let _ = update.graph.insert_tx(tx);
+ if let Some(anchor) = anchor {
+ let _ = update.graph.insert_anchor(txid, anchor);
}
}
}
- let reorg_occurred = {
- if let Some(checkpoint) = ChainGraph::chain(update).latest_checkpoint() {
- self.get_block_hash(checkpoint.height).await? != checkpoint.hash
- } else {
- false
- }
- };
-
- if reorg_occurred {
- // A reorg occurred, so let's find out where all the txids we found are in the chain now.
- // XXX: collect required because of weird type naming issues
- let txids_found = ChainGraph::chain(update)
- .txids()
- .map(|(_, txid)| *txid)
+ if tip_at_start.hash != self.get_block_hash(tip_at_start.height).await? {
+ // A reorg occurred, so let's find out where all the txids we found are now in the chain
+ let txids_found = update
+ .graph
+ .full_txs()
+ .map(|tx_node| tx_node.txid)
.collect::<Vec<_>>();
- scan.update = EsploraAsyncExt::scan_without_keychain(
+ update.chain = EsploraAsyncExt::scan_without_keychain(
self,
local_chain,
[],
[],
parallel_requests,
)
- .await?;
+ .await?
+ .chain;
}
- Ok(scan)
+ Ok(update)
}
}
-use std::collections::BTreeMap;
+use bdk_chain::bitcoin::{BlockHash, OutPoint, Script, Txid};
+use bdk_chain::collections::BTreeMap;
+use bdk_chain::BlockId;
+use bdk_chain::{keychain::LocalUpdate, ConfirmationTimeAnchor};
+use esplora_client::{Error, OutputStatus, TxStatus};
-use bdk_chain::{
- bitcoin::{BlockHash, OutPoint, Script, Txid},
- chain_graph::ChainGraph,
- keychain::KeychainScan,
- sparse_chain, BlockId, ConfirmationTime,
-};
-use esplora_client::{Error, OutputStatus};
-
-use crate::map_confirmation_time;
+use crate::map_confirmation_time_anchor;
/// Trait to extend [`esplora_client::BlockingClient`] functionality.
///
///
/// [crate-level documentation]: crate
pub trait EsploraExt {
- /// Scan the blockchain (via esplora) for the data specified and returns a [`KeychainScan`].
+ /// Scan the blockchain (via esplora) for the data specified and returns a
+ /// [`LocalUpdate<K, ConfirmationTimeAnchor>`].
///
/// - `local_chain`: the most recent block hashes present locally
/// - `keychain_spks`: keychains that we want to scan transactions for
- /// - `txids`: transactions for which we want updated [`ChainPosition`]s
+ /// - `txids`: transactions for which we want updated [`ConfirmationTimeAnchor`]s
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to included in the update
///
/// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
/// parallel.
- ///
- /// [`ChainPosition`]: bdk_chain::sparse_chain::ChainPosition
#[allow(clippy::result_large_err)] // FIXME
fn scan<K: Ord + Clone>(
&self,
outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize,
parallel_requests: usize,
- ) -> Result<KeychainScan<K, ConfirmationTime>, Error>;
+ ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error>;
/// Convenience method to call [`scan`] without requiring a keychain.
///
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
parallel_requests: usize,
- ) -> Result<ChainGraph<ConfirmationTime>, Error> {
- let wallet_scan = self.scan(
+ ) -> Result<LocalUpdate<(), ConfirmationTimeAnchor>, Error> {
+ self.scan(
local_chain,
[(
(),
outpoints,
usize::MAX,
parallel_requests,
- )?;
-
- Ok(wallet_scan.update)
+ )
}
}
outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize,
parallel_requests: usize,
- ) -> Result<KeychainScan<K, ConfirmationTime>, Error> {
+ ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error> {
let parallel_requests = Ord::max(parallel_requests, 1);
- let mut scan = KeychainScan::default();
- let update = &mut scan.update;
- let last_active_indices = &mut scan.last_active_indices;
- for (&height, &original_hash) in local_chain.iter().rev() {
- let update_block_id = BlockId {
- height,
- hash: self.get_block_hash(height)?,
+ let (mut update, tip_at_start) = loop {
+ let mut update = LocalUpdate::<K, ConfirmationTimeAnchor>::default();
+
+ for (&height, &original_hash) in local_chain.iter().rev() {
+ let update_block_id = BlockId {
+ height,
+ hash: self.get_block_hash(height)?,
+ };
+ let _ = update
+ .chain
+ .insert_block(update_block_id)
+ .expect("cannot repeat height here");
+ if update_block_id.hash == original_hash {
+ break;
+ }
+ }
+
+ let tip_at_start = BlockId {
+ height: self.get_height()?,
+ hash: self.get_tip_hash()?,
};
- let _ = update
- .insert_checkpoint(update_block_id)
- .expect("cannot repeat height here");
- if update_block_id.hash == original_hash {
- break;
+
+ if update.chain.insert_block(tip_at_start).is_ok() {
+ break (update, tip_at_start);
}
- }
- let tip_at_start = BlockId {
- height: self.get_height()?,
- hash: self.get_tip_hash()?,
};
- if let Err(failure) = update.insert_checkpoint(tip_at_start) {
- match failure {
- sparse_chain::InsertCheckpointError::HashNotMatching { .. } => {
- // there was a re-org before we started scanning. We haven't consumed any iterators, so calling this function recursively is safe.
- return EsploraExt::scan(
- self,
- local_chain,
- keychain_spks,
- txids,
- outpoints,
- stop_gap,
- parallel_requests,
- );
- }
- }
- }
for (keychain, spks) in keychain_spks {
let mut spks = spks.into_iter();
empty_scripts = 0;
}
for tx in related_txs {
- let confirmation_time =
- map_confirmation_time(&tx.status, tip_at_start.height);
+ let anchor = map_confirmation_time_anchor(&tx.status, tip_at_start);
- if let Err(failure) = update.insert_tx(tx.to_tx(), confirmation_time) {
- use bdk_chain::{
- chain_graph::InsertTxError, sparse_chain::InsertTxError::*,
- };
- match failure {
- InsertTxError::Chain(TxTooHigh { .. }) => {
- unreachable!("chain position already checked earlier")
- }
- InsertTxError::Chain(TxMovedUnexpectedly { .. })
- | InsertTxError::UnresolvableConflict(_) => {
- /* implies reorg during a scan. We deal with that below */
- }
- }
+ let _ = update.graph.insert_tx(tx.to_tx());
+ if let Some(anchor) = anchor {
+ let _ = update.graph.insert_anchor(tx.txid, anchor);
}
}
}
}
if let Some(last_active_index) = last_active_index {
- last_active_indices.insert(keychain, last_active_index);
+ update.keychain.insert(keychain, last_active_index);
}
}
for txid in txids.into_iter() {
- let (tx, tx_status) = match (self.get_tx(&txid)?, self.get_tx_status(&txid)?) {
- (Some(tx), Some(tx_status)) => (tx, tx_status),
- _ => continue,
- };
-
- let confirmation_time = map_confirmation_time(&tx_status, tip_at_start.height);
-
- if let Err(failure) = update.insert_tx(tx, confirmation_time) {
- use bdk_chain::{chain_graph::InsertTxError, sparse_chain::InsertTxError::*};
- match failure {
- InsertTxError::Chain(TxTooHigh { .. }) => {
- unreachable!("chain position already checked earlier")
+ if update.graph.get_tx(txid).is_none() {
+ match self.get_tx(&txid)? {
+ Some(tx) => {
+ let _ = update.graph.insert_tx(tx);
}
- InsertTxError::Chain(TxMovedUnexpectedly { .. })
- | InsertTxError::UnresolvableConflict(_) => {
- /* implies reorg during a scan. We deal with that below */
+ None => continue,
+ }
+ }
+ match self.get_tx_status(&txid)? {
+ tx_status @ TxStatus {
+ confirmed: true, ..
+ } => {
+ if let Some(anchor) = map_confirmation_time_anchor(&tx_status, tip_at_start) {
+ let _ = update.graph.insert_anchor(txid, anchor);
}
}
+ _ => continue,
}
}
for op in outpoints.into_iter() {
let mut op_txs = Vec::with_capacity(2);
- if let (Some(tx), Some(tx_status)) =
- (self.get_tx(&op.txid)?, self.get_tx_status(&op.txid)?)
+ if let (
+ Some(tx),
+ tx_status @ TxStatus {
+ confirmed: true, ..
+ },
+ ) = (self.get_tx(&op.txid)?, self.get_tx_status(&op.txid)?)
{
op_txs.push((tx, tx_status));
if let Some(OutputStatus {
}
for (tx, status) in op_txs {
- let confirmation_time = map_confirmation_time(&status, tip_at_start.height);
+ let txid = tx.txid();
+ let anchor = map_confirmation_time_anchor(&status, tip_at_start);
- if let Err(failure) = update.insert_tx(tx, confirmation_time) {
- use bdk_chain::{chain_graph::InsertTxError, sparse_chain::InsertTxError::*};
- match failure {
- InsertTxError::Chain(TxTooHigh { .. }) => {
- unreachable!("chain position already checked earlier")
- }
- InsertTxError::Chain(TxMovedUnexpectedly { .. })
- | InsertTxError::UnresolvableConflict(_) => {
- /* implies reorg during a scan. We deal with that below */
- }
- }
+ let _ = update.graph.insert_tx(tx);
+ if let Some(anchor) = anchor {
+ let _ = update.graph.insert_anchor(txid, anchor);
}
}
}
- let reorg_occurred = {
- if let Some(checkpoint) = ChainGraph::chain(update).latest_checkpoint() {
- self.get_block_hash(checkpoint.height)? != checkpoint.hash
- } else {
- false
- }
- };
-
- if reorg_occurred {
- // A reorg occurred, so let's find out where all the txids we found are now in the chain.
- // XXX: collect required because of weird type naming issues
- let txids_found = ChainGraph::chain(update)
- .txids()
- .map(|(_, txid)| *txid)
+ if tip_at_start.hash != self.get_block_hash(tip_at_start.height)? {
+ // A reorg occurred, so let's find out where all the txids we found are now in the chain
+ let txids_found = update
+ .graph
+ .full_txs()
+ .map(|tx_node| tx_node.txid)
.collect::<Vec<_>>();
- scan.update = EsploraExt::scan_without_keychain(
+ update.chain = EsploraExt::scan_without_keychain(
self,
local_chain,
[],
txids_found,
[],
parallel_requests,
- )?;
+ )?
+ .chain;
}
- Ok(scan)
+ Ok(update)
}
}
#![doc = include_str!("../README.md")]
-use bdk_chain::{BlockId, ConfirmationTime, ConfirmationTimeAnchor};
+use bdk_chain::{BlockId, ConfirmationTimeAnchor};
use esplora_client::TxStatus;
pub use esplora_client;
-pub mod v2;
#[cfg(feature = "blocking")]
mod blocking_ext;
#[cfg(feature = "async")]
pub use async_ext::*;
-pub(crate) fn map_confirmation_time(
- tx_status: &TxStatus,
- height_at_start: u32,
-) -> ConfirmationTime {
- match (tx_status.block_time, tx_status.block_height) {
- (Some(time), Some(height)) if height <= height_at_start => {
- ConfirmationTime::Confirmed { height, time }
- }
- _ => ConfirmationTime::Unconfirmed { last_seen: 0 },
- }
-}
-
pub(crate) fn map_confirmation_time_anchor(
tx_status: &TxStatus,
tip_at_start: BlockId,
+++ /dev/null
-use async_trait::async_trait;
-use bdk_chain::{
- bitcoin::{BlockHash, OutPoint, Script, Txid},
- collections::BTreeMap,
- keychain::LocalUpdate,
- BlockId, ConfirmationTimeAnchor,
-};
-use esplora_client::{Error, OutputStatus};
-use futures::{stream::FuturesOrdered, TryStreamExt};
-
-use crate::map_confirmation_time_anchor;
-
-/// Trait to extend [`esplora_client::AsyncClient`] functionality.
-///
-/// This is the async version of [`EsploraExt`]. Refer to
-/// [crate-level documentation] for more.
-///
-/// [`EsploraExt`]: crate::EsploraExt
-/// [crate-level documentation]: crate
-#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
-#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
-pub trait EsploraAsyncExt {
- /// Scan the blockchain (via esplora) for the data specified and returns a
- /// [`LocalUpdate<K, ConfirmationTimeAnchor>`].
- ///
- /// - `local_chain`: the most recent block hashes present locally
- /// - `keychain_spks`: keychains that we want to scan transactions for
- /// - `txids`: transactions for which we want updated [`ChainPosition`]s
- /// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
- /// want to included in the update
- ///
- /// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
- /// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
- /// parallel.
- ///
- /// [`ChainPosition`]: bdk_chain::sparse_chain::ChainPosition
- #[allow(clippy::result_large_err)] // FIXME
- async fn scan<K: Ord + Clone + Send>(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- keychain_spks: BTreeMap<
- K,
- impl IntoIterator<IntoIter = impl Iterator<Item = (u32, Script)> + Send> + Send,
- >,
- txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
- outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
- stop_gap: usize,
- parallel_requests: usize,
- ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error>;
-
- /// Convenience method to call [`scan`] without requiring a keychain.
- ///
- /// [`scan`]: EsploraAsyncExt::scan
- #[allow(clippy::result_large_err)] // FIXME
- async fn scan_without_keychain(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = Script> + Send> + Send,
- txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
- outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
- parallel_requests: usize,
- ) -> Result<LocalUpdate<(), ConfirmationTimeAnchor>, Error> {
- self.scan(
- local_chain,
- [(
- (),
- misc_spks
- .into_iter()
- .enumerate()
- .map(|(i, spk)| (i as u32, spk)),
- )]
- .into(),
- txids,
- outpoints,
- usize::MAX,
- parallel_requests,
- )
- .await
- }
-}
-
-#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
-#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
-impl EsploraAsyncExt for esplora_client::AsyncClient {
- #[allow(clippy::result_large_err)] // FIXME
- async fn scan<K: Ord + Clone + Send>(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- keychain_spks: BTreeMap<
- K,
- impl IntoIterator<IntoIter = impl Iterator<Item = (u32, Script)> + Send> + Send,
- >,
- txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
- outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
- stop_gap: usize,
- parallel_requests: usize,
- ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error> {
- let parallel_requests = Ord::max(parallel_requests, 1);
-
- let (mut update, tip_at_start) = loop {
- let mut update = LocalUpdate::<K, ConfirmationTimeAnchor>::default();
-
- for (&height, &original_hash) in local_chain.iter().rev() {
- let update_block_id = BlockId {
- height,
- hash: self.get_block_hash(height).await?,
- };
- let _ = update
- .chain
- .insert_block(update_block_id)
- .expect("cannot repeat height here");
- if update_block_id.hash == original_hash {
- break;
- }
- }
-
- let tip_at_start = BlockId {
- height: self.get_height().await?,
- hash: self.get_tip_hash().await?,
- };
-
- if update.chain.insert_block(tip_at_start).is_ok() {
- break (update, tip_at_start);
- }
- };
-
- for (keychain, spks) in keychain_spks {
- let mut spks = spks.into_iter();
- let mut last_active_index = None;
- let mut empty_scripts = 0;
- type IndexWithTxs = (u32, Vec<esplora_client::Tx>);
-
- loop {
- let futures = (0..parallel_requests)
- .filter_map(|_| {
- let (index, script) = spks.next()?;
- let client = self.clone();
- Some(async move {
- let mut related_txs = client.scripthash_txs(&script, None).await?;
-
- let n_confirmed =
- related_txs.iter().filter(|tx| tx.status.confirmed).count();
- // esplora pages on 25 confirmed transactions. If there are 25 or more we
- // keep requesting to see if there's more.
- if n_confirmed >= 25 {
- loop {
- let new_related_txs = client
- .scripthash_txs(
- &script,
- Some(related_txs.last().unwrap().txid),
- )
- .await?;
- let n = new_related_txs.len();
- related_txs.extend(new_related_txs);
- // we've reached the end
- if n < 25 {
- break;
- }
- }
- }
-
- Result::<_, esplora_client::Error>::Ok((index, related_txs))
- })
- })
- .collect::<FuturesOrdered<_>>();
-
- let n_futures = futures.len();
-
- for (index, related_txs) in futures.try_collect::<Vec<IndexWithTxs>>().await? {
- if related_txs.is_empty() {
- empty_scripts += 1;
- } else {
- last_active_index = Some(index);
- empty_scripts = 0;
- }
- for tx in related_txs {
- let anchor = map_confirmation_time_anchor(&tx.status, tip_at_start);
-
- let _ = update.graph.insert_tx(tx.to_tx());
- if let Some(anchor) = anchor {
- let _ = update.graph.insert_anchor(tx.txid, anchor);
- }
- }
- }
-
- if n_futures == 0 || empty_scripts >= stop_gap {
- break;
- }
- }
-
- if let Some(last_active_index) = last_active_index {
- update.keychain.insert(keychain, last_active_index);
- }
- }
-
- for txid in txids.into_iter() {
- if update.graph.get_tx(txid).is_none() {
- match self.get_tx(&txid).await? {
- Some(tx) => {
- let _ = update.graph.insert_tx(tx);
- }
- None => continue,
- }
- }
- match self.get_tx_status(&txid).await? {
- Some(tx_status) => {
- if let Some(anchor) = map_confirmation_time_anchor(&tx_status, tip_at_start) {
- let _ = update.graph.insert_anchor(txid, anchor);
- }
- }
- None => continue,
- }
- }
-
- for op in outpoints.into_iter() {
- let mut op_txs = Vec::with_capacity(2);
- if let (Some(tx), Some(tx_status)) = (
- self.get_tx(&op.txid).await?,
- self.get_tx_status(&op.txid).await?,
- ) {
- op_txs.push((tx, tx_status));
- if let Some(OutputStatus {
- txid: Some(txid),
- status: Some(spend_status),
- ..
- }) = self.get_output_status(&op.txid, op.vout as _).await?
- {
- if let Some(spend_tx) = self.get_tx(&txid).await? {
- op_txs.push((spend_tx, spend_status));
- }
- }
- }
-
- for (tx, status) in op_txs {
- let txid = tx.txid();
- let anchor = map_confirmation_time_anchor(&status, tip_at_start);
-
- let _ = update.graph.insert_tx(tx);
- if let Some(anchor) = anchor {
- let _ = update.graph.insert_anchor(txid, anchor);
- }
- }
- }
-
- if tip_at_start.hash != self.get_block_hash(tip_at_start.height).await? {
- // A reorg occurred, so let's find out where all the txids we found are now in the chain
- let txids_found = update
- .graph
- .full_txs()
- .map(|tx_node| tx_node.txid)
- .collect::<Vec<_>>();
- update.chain = EsploraAsyncExt::scan_without_keychain(
- self,
- local_chain,
- [],
- txids_found,
- [],
- parallel_requests,
- )
- .await?
- .chain;
- }
-
- Ok(update)
- }
-}
+++ /dev/null
-use bdk_chain::bitcoin::{BlockHash, OutPoint, Script, Txid};
-use bdk_chain::collections::BTreeMap;
-use bdk_chain::BlockId;
-use bdk_chain::{keychain::LocalUpdate, ConfirmationTimeAnchor};
-use esplora_client::{Error, OutputStatus};
-
-use crate::map_confirmation_time_anchor;
-
-/// Trait to extend [`esplora_client::BlockingClient`] functionality.
-///
-/// Refer to [crate-level documentation] for more.
-///
-/// [crate-level documentation]: crate
-pub trait EsploraExt {
- /// Scan the blockchain (via esplora) for the data specified and returns a
- /// [`LocalUpdate<K, ConfirmationTimeAnchor>`].
- ///
- /// - `local_chain`: the most recent block hashes present locally
- /// - `keychain_spks`: keychains that we want to scan transactions for
- /// - `txids`: transactions for which we want updated [`ChainPosition`]s
- /// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
- /// want to included in the update
- ///
- /// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
- /// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
- /// parallel.
- ///
- /// [`ChainPosition`]: bdk_chain::sparse_chain::ChainPosition
- #[allow(clippy::result_large_err)] // FIXME
- fn scan<K: Ord + Clone>(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
- stop_gap: usize,
- parallel_requests: usize,
- ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error>;
-
- /// Convenience method to call [`scan`] without requiring a keychain.
- ///
- /// [`scan`]: EsploraExt::scan
- #[allow(clippy::result_large_err)] // FIXME
- fn scan_without_keychain(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- misc_spks: impl IntoIterator<Item = Script>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
- parallel_requests: usize,
- ) -> Result<LocalUpdate<(), ConfirmationTimeAnchor>, Error> {
- self.scan(
- local_chain,
- [(
- (),
- misc_spks
- .into_iter()
- .enumerate()
- .map(|(i, spk)| (i as u32, spk)),
- )]
- .into(),
- txids,
- outpoints,
- usize::MAX,
- parallel_requests,
- )
- }
-}
-
-impl EsploraExt for esplora_client::BlockingClient {
- fn scan<K: Ord + Clone>(
- &self,
- local_chain: &BTreeMap<u32, BlockHash>,
- keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
- stop_gap: usize,
- parallel_requests: usize,
- ) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error> {
- let parallel_requests = Ord::max(parallel_requests, 1);
-
- let (mut update, tip_at_start) = loop {
- let mut update = LocalUpdate::<K, ConfirmationTimeAnchor>::default();
-
- for (&height, &original_hash) in local_chain.iter().rev() {
- let update_block_id = BlockId {
- height,
- hash: self.get_block_hash(height)?,
- };
- let _ = update
- .chain
- .insert_block(update_block_id)
- .expect("cannot repeat height here");
- if update_block_id.hash == original_hash {
- break;
- }
- }
-
- let tip_at_start = BlockId {
- height: self.get_height()?,
- hash: self.get_tip_hash()?,
- };
-
- if update.chain.insert_block(tip_at_start).is_ok() {
- break (update, tip_at_start);
- }
- };
-
- for (keychain, spks) in keychain_spks {
- let mut spks = spks.into_iter();
- let mut last_active_index = None;
- let mut empty_scripts = 0;
- type IndexWithTxs = (u32, Vec<esplora_client::Tx>);
-
- loop {
- let handles = (0..parallel_requests)
- .filter_map(
- |_| -> Option<std::thread::JoinHandle<Result<IndexWithTxs, _>>> {
- let (index, script) = spks.next()?;
- let client = self.clone();
- Some(std::thread::spawn(move || {
- let mut related_txs = client.scripthash_txs(&script, None)?;
-
- let n_confirmed =
- related_txs.iter().filter(|tx| tx.status.confirmed).count();
- // esplora pages on 25 confirmed transactions. If there are 25 or more we
- // keep requesting to see if there's more.
- if n_confirmed >= 25 {
- loop {
- let new_related_txs = client.scripthash_txs(
- &script,
- Some(related_txs.last().unwrap().txid),
- )?;
- let n = new_related_txs.len();
- related_txs.extend(new_related_txs);
- // we've reached the end
- if n < 25 {
- break;
- }
- }
- }
-
- Result::<_, esplora_client::Error>::Ok((index, related_txs))
- }))
- },
- )
- .collect::<Vec<_>>();
-
- let n_handles = handles.len();
-
- for handle in handles {
- let (index, related_txs) = handle.join().unwrap()?; // TODO: don't unwrap
- if related_txs.is_empty() {
- empty_scripts += 1;
- } else {
- last_active_index = Some(index);
- empty_scripts = 0;
- }
- for tx in related_txs {
- let anchor = map_confirmation_time_anchor(&tx.status, tip_at_start);
-
- let _ = update.graph.insert_tx(tx.to_tx());
- if let Some(anchor) = anchor {
- let _ = update.graph.insert_anchor(tx.txid, anchor);
- }
- }
- }
-
- if n_handles == 0 || empty_scripts >= stop_gap {
- break;
- }
- }
-
- if let Some(last_active_index) = last_active_index {
- update.keychain.insert(keychain, last_active_index);
- }
- }
-
- for txid in txids.into_iter() {
- if update.graph.get_tx(txid).is_none() {
- match self.get_tx(&txid)? {
- Some(tx) => {
- let _ = update.graph.insert_tx(tx);
- }
- None => continue,
- }
- }
- match self.get_tx_status(&txid)? {
- Some(tx_status) => {
- if let Some(anchor) = map_confirmation_time_anchor(&tx_status, tip_at_start) {
- let _ = update.graph.insert_anchor(txid, anchor);
- }
- }
- None => continue,
- }
- }
-
- for op in outpoints.into_iter() {
- let mut op_txs = Vec::with_capacity(2);
- if let (Some(tx), Some(tx_status)) =
- (self.get_tx(&op.txid)?, self.get_tx_status(&op.txid)?)
- {
- op_txs.push((tx, tx_status));
- if let Some(OutputStatus {
- txid: Some(txid),
- status: Some(spend_status),
- ..
- }) = self.get_output_status(&op.txid, op.vout as _)?
- {
- if let Some(spend_tx) = self.get_tx(&txid)? {
- op_txs.push((spend_tx, spend_status));
- }
- }
- }
-
- for (tx, status) in op_txs {
- let txid = tx.txid();
- let anchor = map_confirmation_time_anchor(&status, tip_at_start);
-
- let _ = update.graph.insert_tx(tx);
- if let Some(anchor) = anchor {
- let _ = update.graph.insert_anchor(txid, anchor);
- }
- }
- }
-
- if tip_at_start.hash != self.get_block_hash(tip_at_start.height)? {
- // A reorg occurred, so let's find out where all the txids we found are now in the chain
- let txids_found = update
- .graph
- .full_txs()
- .map(|tx_node| tx_node.txid)
- .collect::<Vec<_>>();
- update.chain = EsploraExt::scan_without_keychain(
- self,
- local_chain,
- [],
- txids_found,
- [],
- parallel_requests,
- )?
- .chain;
- }
-
- Ok(update)
- }
-}
+++ /dev/null
-#[cfg(feature = "blocking")]
-mod blocking_ext;
-#[cfg(feature = "blocking")]
-pub use blocking_ext::*;
-
-#[cfg(feature = "async")]
-mod async_ext;
-#[cfg(feature = "async")]
-pub use async_ext::*;
# BDK File Store
This is a simple append-only flat file implementation of
-[`Persist`](`bdk_chain::keychain::persist::Persist`).
+[`Persist`](`bdk_chain::Persist`).
-The main structure is [`KeychainStore`](`crate::KeychainStore`), which can be used with [`bdk`]'s
+The main structure is [`Store`](`crate::Store`), which can be used with [`bdk`]'s
`Wallet` to persist wallet data into a flat file.
[`bdk`]: https://docs.rs/bdk/latest
+++ /dev/null
-//! Module for persisting data on disk.
-//!
-//! The star of the show is [`KeychainStore`], which maintains an append-only file of
-//! [`KeychainChangeSet`]s which can be used to restore a [`KeychainTracker`].
-use bdk_chain::{
- keychain::{KeychainChangeSet, KeychainTracker},
- sparse_chain,
-};
-use bincode::Options;
-use std::{
- fs::{File, OpenOptions},
- io::{self, Read, Seek, Write},
- path::Path,
-};
-
-use crate::{bincode_options, EntryIter, IterError};
-
-/// BDK File Store magic bytes length.
-const MAGIC_BYTES_LEN: usize = 12;
-
-/// BDK File Store magic bytes.
-const MAGIC_BYTES: [u8; MAGIC_BYTES_LEN] = [98, 100, 107, 102, 115, 48, 48, 48, 48, 48, 48, 48];
-
-/// Persists an append only list of `KeychainChangeSet<K,P>` to a single file.
-/// [`KeychainChangeSet<K,P>`] record the changes made to a [`KeychainTracker<K,P>`].
-#[derive(Debug)]
-pub struct KeychainStore<K, P> {
- db_file: File,
- changeset_type_params: core::marker::PhantomData<(K, P)>,
-}
-
-impl<K, P> KeychainStore<K, P>
-where
- K: Ord + Clone + core::fmt::Debug,
- P: sparse_chain::ChainPosition,
- KeychainChangeSet<K, P>: serde::Serialize + serde::de::DeserializeOwned,
-{
- /// Creates a new store from a [`File`].
- ///
- /// The file must have been opened with read and write permissions.
- ///
- /// [`File`]: std::fs::File
- pub fn new(mut file: File) -> Result<Self, FileError> {
- file.rewind()?;
-
- let mut magic_bytes = [0_u8; MAGIC_BYTES_LEN];
- file.read_exact(&mut magic_bytes)?;
-
- if magic_bytes != MAGIC_BYTES {
- return Err(FileError::InvalidMagicBytes(magic_bytes));
- }
-
- Ok(Self {
- db_file: file,
- changeset_type_params: Default::default(),
- })
- }
-
- /// Creates or loads a store from `db_path`. If no file exists there, it will be created.
- pub fn new_from_path<D: AsRef<Path>>(db_path: D) -> Result<Self, FileError> {
- let already_exists = db_path.as_ref().exists();
-
- let mut db_file = OpenOptions::new()
- .read(true)
- .write(true)
- .create(true)
- .open(db_path)?;
-
- if !already_exists {
- db_file.write_all(&MAGIC_BYTES)?;
- }
-
- Self::new(db_file)
- }
-
- /// Iterates over the stored changeset from first to last, changing the seek position at each
- /// iteration.
- ///
- /// The iterator may fail to read an entry and therefore return an error. However, the first time
- /// it returns an error will be the last. After doing so, the iterator will always yield `None`.
- ///
- /// **WARNING**: This method changes the write position in the underlying file. You should
- /// always iterate over all entries until `None` is returned if you want your next write to go
- /// at the end; otherwise, you will write over existing entries.
- pub fn iter_changesets(&mut self) -> Result<EntryIter<KeychainChangeSet<K, P>>, io::Error> {
- Ok(EntryIter::new(MAGIC_BYTES_LEN as u64, &mut self.db_file))
- }
-
- /// Loads all the changesets that have been stored as one giant changeset.
- ///
- /// This function returns a tuple of the aggregate changeset and a result that indicates
- /// whether an error occurred while reading or deserializing one of the entries. If so the
- /// changeset will consist of all of those it was able to read.
- ///
- /// You should usually check the error. In many applications, it may make sense to do a full
- /// wallet scan with a stop-gap after getting an error, since it is likely that one of the
- /// changesets it was unable to read changed the derivation indices of the tracker.
- ///
- /// **WARNING**: This method changes the write position of the underlying file. The next
- /// changeset will be written over the erroring entry (or the end of the file if none existed).
- pub fn aggregate_changeset(&mut self) -> (KeychainChangeSet<K, P>, Result<(), IterError>) {
- let mut changeset = KeychainChangeSet::default();
- let result = (|| {
- let iter_changeset = self.iter_changesets()?;
- for next_changeset in iter_changeset {
- changeset.append(next_changeset?);
- }
- Ok(())
- })();
-
- (changeset, result)
- }
-
- /// Reads and applies all the changesets stored sequentially to the tracker, stopping when it fails
- /// to read the next one.
- ///
- /// **WARNING**: This method changes the write position of the underlying file. The next
- /// changeset will be written over the erroring entry (or the end of the file if none existed).
- pub fn load_into_keychain_tracker(
- &mut self,
- tracker: &mut KeychainTracker<K, P>,
- ) -> Result<(), IterError> {
- for changeset in self.iter_changesets()? {
- tracker.apply_changeset(changeset?)
- }
- Ok(())
- }
-
- /// Append a new changeset to the file and truncate the file to the end of the appended changeset.
- ///
- /// The truncation is to avoid the possibility of having a valid but inconsistent changeset
- /// directly after the appended changeset.
- pub fn append_changeset(
- &mut self,
- changeset: &KeychainChangeSet<K, P>,
- ) -> Result<(), io::Error> {
- if changeset.is_empty() {
- return Ok(());
- }
-
- bincode_options()
- .serialize_into(&mut self.db_file, changeset)
- .map_err(|e| match *e {
- bincode::ErrorKind::Io(inner) => inner,
- unexpected_err => panic!("unexpected bincode error: {}", unexpected_err),
- })?;
-
- // truncate file after this changeset addition
- // if this is not done, data after this changeset may represent valid changesets, however
- // applying those changesets on top of this one may result in an inconsistent state
- let pos = self.db_file.stream_position()?;
- self.db_file.set_len(pos)?;
-
- // We want to make sure that derivation indices changes are written to disk as soon as
- // possible, so you know about the write failure before you give out the address in the application.
- if !changeset.derivation_indices.is_empty() {
- self.db_file.sync_data()?;
- }
-
- Ok(())
- }
-}
-
-/// Error that occurs due to problems encountered with the file.
-#[derive(Debug)]
-pub enum FileError {
- /// IO error, this may mean that the file is too short.
- Io(io::Error),
- /// Magic bytes do not match what is expected.
- InvalidMagicBytes([u8; MAGIC_BYTES_LEN]),
-}
-
-impl core::fmt::Display for FileError {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- match self {
- Self::Io(e) => write!(f, "io error trying to read file: {}", e),
- Self::InvalidMagicBytes(b) => write!(
- f,
- "file has invalid magic bytes: expected={:?} got={:?}",
- MAGIC_BYTES, b
- ),
- }
- }
-}
-
-impl From<io::Error> for FileError {
- fn from(value: io::Error) -> Self {
- Self::Io(value)
- }
-}
-
-impl std::error::Error for FileError {}
-
-#[cfg(test)]
-mod test {
- use super::*;
- use bdk_chain::{
- keychain::{DerivationAdditions, KeychainChangeSet},
- TxHeight,
- };
- use bincode::DefaultOptions;
- use std::{
- io::{Read, Write},
- vec::Vec,
- };
- use tempfile::NamedTempFile;
- #[derive(
- Debug,
- Clone,
- Copy,
- PartialOrd,
- Ord,
- PartialEq,
- Eq,
- Hash,
- serde::Serialize,
- serde::Deserialize,
- )]
- enum TestKeychain {
- External,
- Internal,
- }
-
- impl core::fmt::Display for TestKeychain {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::External => write!(f, "external"),
- Self::Internal => write!(f, "internal"),
- }
- }
- }
-
- #[test]
- fn magic_bytes() {
- assert_eq!(&MAGIC_BYTES, "bdkfs0000000".as_bytes());
- }
-
- #[test]
- fn new_fails_if_file_is_too_short() {
- let mut file = NamedTempFile::new().unwrap();
- file.write_all(&MAGIC_BYTES[..MAGIC_BYTES_LEN - 1])
- .expect("should write");
-
- match KeychainStore::<TestKeychain, TxHeight>::new(file.reopen().unwrap()) {
- Err(FileError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof),
- unexpected => panic!("unexpected result: {:?}", unexpected),
- };
- }
-
- #[test]
- fn new_fails_if_magic_bytes_are_invalid() {
- let invalid_magic_bytes = "ldkfs0000000";
-
- let mut file = NamedTempFile::new().unwrap();
- file.write_all(invalid_magic_bytes.as_bytes())
- .expect("should write");
-
- match KeychainStore::<TestKeychain, TxHeight>::new(file.reopen().unwrap()) {
- Err(FileError::InvalidMagicBytes(b)) => {
- assert_eq!(b, invalid_magic_bytes.as_bytes())
- }
- unexpected => panic!("unexpected result: {:?}", unexpected),
- };
- }
-
- #[test]
- fn append_changeset_truncates_invalid_bytes() {
- // initial data to write to file (magic bytes + invalid data)
- let mut data = [255_u8; 2000];
- data[..MAGIC_BYTES_LEN].copy_from_slice(&MAGIC_BYTES);
-
- let changeset = KeychainChangeSet {
- derivation_indices: DerivationAdditions(
- vec![(TestKeychain::External, 42)].into_iter().collect(),
- ),
- chain_graph: Default::default(),
- };
-
- let mut file = NamedTempFile::new().unwrap();
- file.write_all(&data).expect("should write");
-
- let mut store = KeychainStore::<TestKeychain, TxHeight>::new(file.reopen().unwrap())
- .expect("should open");
- match store.iter_changesets().expect("seek should succeed").next() {
- Some(Err(IterError::Bincode(_))) => {}
- unexpected_res => panic!("unexpected result: {:?}", unexpected_res),
- }
-
- store.append_changeset(&changeset).expect("should append");
-
- drop(store);
-
- let got_bytes = {
- let mut buf = Vec::new();
- file.reopen()
- .unwrap()
- .read_to_end(&mut buf)
- .expect("should read");
- buf
- };
-
- let expected_bytes = {
- let mut buf = MAGIC_BYTES.to_vec();
- DefaultOptions::new()
- .with_varint_encoding()
- .serialize_into(&mut buf, &changeset)
- .expect("should encode");
- buf
- };
-
- assert_eq!(got_bytes, expected_bytes);
- }
-}
#![doc = include_str!("../README.md")]
mod entry_iter;
-mod keychain_store;
mod store;
use std::io;
-use bdk_chain::{
- keychain::{KeychainChangeSet, KeychainTracker, PersistBackend},
- sparse_chain::ChainPosition,
-};
use bincode::{DefaultOptions, Options};
pub use entry_iter::*;
-pub use keychain_store::*;
pub use store::*;
pub(crate) fn bincode_options() -> impl bincode::Options {
}
impl<'a> std::error::Error for FileError<'a> {}
-
-impl<K, P> PersistBackend<K, P> for KeychainStore<K, P>
-where
- K: Ord + Clone + core::fmt::Debug,
- P: ChainPosition,
- KeychainChangeSet<K, P>: serde::Serialize + serde::de::DeserializeOwned,
-{
- type WriteError = std::io::Error;
-
- type LoadError = IterError;
-
- fn append_changeset(
- &mut self,
- changeset: &KeychainChangeSet<K, P>,
- ) -> Result<(), Self::WriteError> {
- KeychainStore::append_changeset(self, changeset)
- }
-
- fn load_into_keychain_tracker(
- &mut self,
- tracker: &mut KeychainTracker<K, P>,
- ) -> Result<(), Self::LoadError> {
- KeychainStore::load_into_keychain_tracker(self, tracker)
- }
-}
descriptor::{DescriptorSecretKey, KeyMap},
Descriptor, DescriptorPublicKey,
},
- Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, ObservedAs, Persist, PersistBackend,
+ Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, Persist, PersistBackend,
};
pub use bdk_file_store;
pub use clap;
graph: &KeychainTxGraph<A>,
chain: &O,
assets: &bdk_tmp_plan::Assets<K>,
-) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<ObservedAs<A>>)>, O::Error> {
+) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> {
let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
let outpoints = graph.index.outpoints().iter().cloned();
graph
.try_filter_chain_unspents(chain, chain_tip, outpoints)
.filter_map(
#[allow(clippy::type_complexity)]
- |r| -> Option<Result<(bdk_tmp_plan::Plan<K>, FullTxOut<ObservedAs<A>>), _>> {
+ |r| -> Option<Result<(bdk_tmp_plan::Plan<K>, FullTxOut<A>), _>> {
let (k, i, full_txo) = match r {
Err(err) => return Some(Err(err)),
Ok(((k, i), full_txo)) => (k, i, full_txo),
};
use bdk_electrum::{
electrum_client::{self, ElectrumApi},
- v2::{ElectrumExt, ElectrumUpdate},
+ ElectrumExt, ElectrumUpdate,
};
use example_cli::{
anyhow::{self, Context},
+++ /dev/null
-[package]
-name = "keychain_tracker_electrum_example"
-version = "0.1.0"
-edition = "2021"
-
-[dependencies]
-bdk_chain = { path = "../../crates/chain", features = ["serde"] }
-bdk_electrum = { path = "../../crates/electrum" }
-keychain_tracker_example_cli = { path = "../keychain_tracker_example_cli"}
+++ /dev/null
-# Keychain Tracker with electrum
-
-This example shows how you use the `KeychainTracker` from `bdk_chain` to create a simple command
-line wallet.
-
-
+++ /dev/null
-use bdk_chain::bitcoin::{Address, OutPoint, Txid};
-use bdk_electrum::bdk_chain::{self, bitcoin::Network, TxHeight};
-use bdk_electrum::{
- electrum_client::{self, ElectrumApi},
- ElectrumExt, ElectrumUpdate,
-};
-use keychain_tracker_example_cli::{
- self as cli,
- anyhow::{self, Context},
- clap::{self, Parser, Subcommand},
-};
-use std::{collections::BTreeMap, fmt::Debug, io, io::Write};
-
-#[derive(Subcommand, Debug, Clone)]
-enum ElectrumCommands {
- /// Scans the addresses in the wallet using the esplora API.
- Scan {
- /// When a gap this large has been found for a keychain, it will stop.
- #[clap(long, default_value = "5")]
- stop_gap: usize,
- #[clap(flatten)]
- scan_options: ScanOptions,
- },
- /// Scans particular addresses using the esplora API.
- Sync {
- /// Scan all the unused addresses.
- #[clap(long)]
- unused_spks: bool,
- /// Scan every address that you have derived.
- #[clap(long)]
- all_spks: bool,
- /// Scan unspent outpoints for spends or changes to confirmation status of residing tx.
- #[clap(long)]
- utxos: bool,
- /// Scan unconfirmed transactions for updates.
- #[clap(long)]
- unconfirmed: bool,
- #[clap(flatten)]
- scan_options: ScanOptions,
- },
-}
-
-#[derive(Parser, Debug, Clone, PartialEq)]
-pub struct ScanOptions {
- /// Set batch size for each script_history call to electrum client.
- #[clap(long, default_value = "25")]
- pub batch_size: usize,
-}
-
-fn main() -> anyhow::Result<()> {
- let (args, keymap, tracker, db) = cli::init::<ElectrumCommands, _>()?;
-
- let electrum_url = match args.network {
- Network::Bitcoin => "ssl://electrum.blockstream.info:50002",
- Network::Testnet => "ssl://electrum.blockstream.info:60002",
- Network::Regtest => "tcp://localhost:60401",
- Network::Signet => "tcp://signet-electrumx.wakiyamap.dev:50001",
- };
- let config = electrum_client::Config::builder()
- .validate_domain(matches!(args.network, Network::Bitcoin))
- .build();
-
- let client = electrum_client::Client::from_config(electrum_url, config)?;
-
- let electrum_cmd = match args.command.clone() {
- cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
- general_command => {
- return cli::handle_commands(
- general_command,
- |transaction| {
- let _txid = client.transaction_broadcast(transaction)?;
- Ok(())
- },
- &tracker,
- &db,
- args.network,
- &keymap,
- )
- }
- };
-
- let response = match electrum_cmd {
- ElectrumCommands::Scan {
- stop_gap,
- scan_options: scan_option,
- } => {
- let (spk_iterators, local_chain) = {
- // Get a short lock on the tracker to get the spks iterators
- // and local chain state
- let tracker = &*tracker.lock().unwrap();
- let spk_iterators = tracker
- .txout_index
- .spks_of_all_keychains()
- .into_iter()
- .map(|(keychain, iter)| {
- let mut first = true;
- let spk_iter = iter.inspect(move |(i, _)| {
- if first {
- eprint!("\nscanning {}: ", keychain);
- first = false;
- }
-
- eprint!("{} ", i);
- let _ = io::stdout().flush();
- });
- (keychain, spk_iter)
- })
- .collect::<BTreeMap<_, _>>();
- let local_chain = tracker.chain().checkpoints().clone();
- (spk_iterators, local_chain)
- };
-
- // we scan the spks **without** a lock on the tracker
- client.scan(
- &local_chain,
- spk_iterators,
- core::iter::empty(),
- core::iter::empty(),
- stop_gap,
- scan_option.batch_size,
- )?
- }
- ElectrumCommands::Sync {
- mut unused_spks,
- mut utxos,
- mut unconfirmed,
- all_spks,
- scan_options,
- } => {
- // Get a short lock on the tracker to get the spks we're interested in
- let tracker = tracker.lock().unwrap();
-
- if !(all_spks || unused_spks || utxos || unconfirmed) {
- unused_spks = true;
- unconfirmed = true;
- utxos = true;
- } else if all_spks {
- unused_spks = false;
- }
-
- let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::Script>> =
- Box::new(core::iter::empty());
- if all_spks {
- let all_spks = tracker
- .txout_index
- .all_spks()
- .iter()
- .map(|(k, v)| (*k, v.clone()))
- .collect::<Vec<_>>();
- spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
- eprintln!("scanning {:?}", index);
- script
- })));
- }
- if unused_spks {
- let unused_spks = tracker
- .txout_index
- .unused_spks(..)
- .map(|(k, v)| (*k, v.clone()))
- .collect::<Vec<_>>();
- spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
- eprintln!(
- "Checking if address {} {:?} has been used",
- Address::from_script(&script, args.network).unwrap(),
- index
- );
-
- script
- })));
- }
-
- let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
-
- if utxos {
- let utxos = tracker
- .full_utxos()
- .map(|(_, utxo)| utxo)
- .collect::<Vec<_>>();
- outpoints = Box::new(
- utxos
- .into_iter()
- .inspect(|utxo| {
- eprintln!(
- "Checking if outpoint {} (value: {}) has been spent",
- utxo.outpoint, utxo.txout.value
- );
- })
- .map(|utxo| utxo.outpoint),
- );
- };
-
- let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
-
- if unconfirmed {
- let unconfirmed_txids = tracker
- .chain()
- .range_txids_by_height(TxHeight::Unconfirmed..)
- .map(|(_, txid)| *txid)
- .collect::<Vec<_>>();
-
- txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
- eprintln!("Checking if {} is confirmed yet", txid);
- }));
- }
-
- let local_chain = tracker.chain().checkpoints().clone();
- // drop lock on tracker
- drop(tracker);
-
- // we scan the spks **without** a lock on the tracker
- ElectrumUpdate {
- chain_update: client
- .scan_without_keychain(
- &local_chain,
- spks,
- txids,
- outpoints,
- scan_options.batch_size,
- )
- .context("scanning the blockchain")?,
- ..Default::default()
- }
- }
- };
-
- let missing_txids = response.missing_full_txs(&*tracker.lock().unwrap());
-
- // fetch the missing full transactions **without** a lock on the tracker
- let new_txs = client
- .batch_transaction_get(missing_txids)
- .context("fetching full transactions")?;
-
- {
- // Get a final short lock to apply the changes
- let mut tracker = tracker.lock().unwrap();
- let changeset = {
- let scan = response.into_keychain_scan(new_txs, &*tracker)?;
- tracker.determine_changeset(&scan)?
- };
- db.lock().unwrap().append_changeset(&changeset)?;
- tracker.apply_changeset(changeset);
- };
-
- Ok(())
-}
+++ /dev/null
-/target
-Cargo.lock
-.bdk_example_db
+++ /dev/null
-[package]
-name = "keychain_tracker_esplora_example"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"] }
-bdk_esplora = { path = "../../crates/esplora" }
-keychain_tracker_example_cli = { path = "../keychain_tracker_example_cli" }
+++ /dev/null
-use bdk_chain::bitcoin::{Address, OutPoint, Txid};
-use bdk_chain::{bitcoin::Network, TxHeight};
-use bdk_esplora::esplora_client;
-use bdk_esplora::EsploraExt;
-
-use std::io::{self, Write};
-
-use keychain_tracker_example_cli::{
- self as cli,
- anyhow::{self, Context},
- clap::{self, Parser, Subcommand},
-};
-
-#[derive(Subcommand, Debug, Clone)]
-enum EsploraCommands {
- /// Scans the addresses in the wallet using the esplora API.
- Scan {
- /// When a gap this large has been found for a keychain, it will stop.
- #[clap(long, default_value = "5")]
- stop_gap: usize,
-
- #[clap(flatten)]
- scan_options: ScanOptions,
- },
- /// Scans particular addresses using esplora API.
- Sync {
- /// Scan all the unused addresses.
- #[clap(long)]
- unused_spks: bool,
- /// Scan every address that you have derived.
- #[clap(long)]
- all_spks: bool,
- /// Scan unspent outpoints for spends or changes to confirmation status of residing tx.
- #[clap(long)]
- utxos: bool,
- /// Scan unconfirmed transactions for updates.
- #[clap(long)]
- unconfirmed: bool,
-
- #[clap(flatten)]
- scan_options: ScanOptions,
- },
-}
-
-#[derive(Parser, Debug, Clone, PartialEq)]
-pub struct ScanOptions {
- #[clap(long, default_value = "5")]
- pub parallel_requests: usize,
-}
-
-fn main() -> anyhow::Result<()> {
- let (args, keymap, keychain_tracker, db) = cli::init::<EsploraCommands, _>()?;
- let esplora_url = match args.network {
- Network::Bitcoin => "https://mempool.space/api",
- Network::Testnet => "https://mempool.space/testnet/api",
- Network::Regtest => "http://localhost:3002",
- Network::Signet => "https://mempool.space/signet/api",
- };
-
- let client = esplora_client::Builder::new(esplora_url).build_blocking()?;
-
- let esplora_cmd = match args.command {
- cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd,
- general_command => {
- return cli::handle_commands(
- general_command,
- |transaction| Ok(client.broadcast(transaction)?),
- &keychain_tracker,
- &db,
- args.network,
- &keymap,
- )
- }
- };
-
- match esplora_cmd {
- EsploraCommands::Scan {
- stop_gap,
- scan_options,
- } => {
- let (spk_iterators, local_chain) = {
- // Get a short lock on the tracker to get the spks iterators
- // and local chain state
- let tracker = &*keychain_tracker.lock().unwrap();
- let spk_iterators = tracker
- .txout_index
- .spks_of_all_keychains()
- .into_iter()
- .map(|(keychain, iter)| {
- let mut first = true;
- (
- keychain,
- iter.inspect(move |(i, _)| {
- if first {
- eprint!("\nscanning {}: ", keychain);
- first = false;
- }
-
- eprint!("{} ", i);
- let _ = io::stdout().flush();
- }),
- )
- })
- .collect();
-
- let local_chain = tracker.chain().checkpoints().clone();
- (spk_iterators, local_chain)
- };
-
- // we scan the iterators **without** a lock on the tracker
- let wallet_scan = client
- .scan(
- &local_chain,
- spk_iterators,
- core::iter::empty(),
- core::iter::empty(),
- stop_gap,
- scan_options.parallel_requests,
- )
- .context("scanning the blockchain")?;
- eprintln!();
-
- {
- // we take a short lock to apply results to tracker and db
- let tracker = &mut *keychain_tracker.lock().unwrap();
- let db = &mut *db.lock().unwrap();
- let changeset = tracker.apply_update(wallet_scan)?;
- db.append_changeset(&changeset)?;
- }
- }
- EsploraCommands::Sync {
- mut unused_spks,
- mut utxos,
- mut unconfirmed,
- all_spks,
- scan_options,
- } => {
- // Get a short lock on the tracker to get the spks we're interested in
- let tracker = keychain_tracker.lock().unwrap();
-
- if !(all_spks || unused_spks || utxos || unconfirmed) {
- unused_spks = true;
- unconfirmed = true;
- utxos = true;
- } else if all_spks {
- unused_spks = false;
- }
-
- let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::Script>> =
- Box::new(core::iter::empty());
- if all_spks {
- let all_spks = tracker
- .txout_index
- .all_spks()
- .iter()
- .map(|(k, v)| (*k, v.clone()))
- .collect::<Vec<_>>();
- spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
- eprintln!("scanning {:?}", index);
- script
- })));
- }
- if unused_spks {
- let unused_spks = tracker
- .txout_index
- .unused_spks(..)
- .map(|(k, v)| (*k, v.clone()))
- .collect::<Vec<_>>();
- spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
- eprintln!(
- "Checking if address {} {:?} has been used",
- Address::from_script(&script, args.network).unwrap(),
- index
- );
-
- script
- })));
- }
-
- let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
-
- if utxos {
- let utxos = tracker
- .full_utxos()
- .map(|(_, utxo)| utxo)
- .collect::<Vec<_>>();
- outpoints = Box::new(
- utxos
- .into_iter()
- .inspect(|utxo| {
- eprintln!(
- "Checking if outpoint {} (value: {}) has been spent",
- utxo.outpoint, utxo.txout.value
- );
- })
- .map(|utxo| utxo.outpoint),
- );
- };
-
- let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
-
- if unconfirmed {
- let unconfirmed_txids = tracker
- .chain()
- .range_txids_by_height(TxHeight::Unconfirmed..)
- .map(|(_, txid)| *txid)
- .collect::<Vec<_>>();
-
- txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
- eprintln!("Checking if {} is confirmed yet", txid);
- }));
- }
-
- let local_chain = tracker.chain().checkpoints().clone();
-
- // drop lock on tracker
- drop(tracker);
-
- // we scan the desired spks **without** a lock on the tracker
- let scan = client
- .scan_without_keychain(
- &local_chain,
- spks,
- txids,
- outpoints,
- scan_options.parallel_requests,
- )
- .context("scanning the blockchain")?;
-
- {
- // we take a short lock to apply the results to the tracker and db
- let tracker = &mut *keychain_tracker.lock().unwrap();
- let changeset = tracker.apply_update(scan.into())?;
- let db = &mut *db.lock().unwrap();
- db.append_changeset(&changeset)?;
- }
- }
- }
-
- Ok(())
-}
+++ /dev/null
-[package]
-name = "keychain_tracker_example_cli"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-[dependencies]
-bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]}
-bdk_file_store = { path = "../../crates/file_store" }
-bdk_tmp_plan = { path = "../../nursery/tmp_plan" }
-bdk_coin_select = { path = "../../nursery/coin_select" }
-
-clap = { version = "3.2.23", features = ["derive", "env"] }
-anyhow = "1"
-serde = { version = "1", features = ["derive"] }
-serde_json = { version = "^1.0" }
+++ /dev/null
-Provides common command line processing logic between examples using the `KeychainTracker`
+++ /dev/null
-pub extern crate anyhow;
-use anyhow::{anyhow, Context, Result};
-use bdk_chain::{
- bitcoin::{
- secp256k1::Secp256k1,
- util::sighash::{Prevouts, SighashCache},
- Address, LockTime, Network, Sequence, Transaction, TxIn, TxOut,
- },
- chain_graph::InsertTxError,
- keychain::{DerivationAdditions, KeychainChangeSet, KeychainTracker},
- miniscript::{
- descriptor::{DescriptorSecretKey, KeyMap},
- Descriptor, DescriptorPublicKey,
- },
- sparse_chain::{self, ChainPosition},
- Append, DescriptorExt, FullTxOut,
-};
-use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
-use bdk_file_store::KeychainStore;
-use clap::{Parser, Subcommand};
-use std::{
- cmp::Reverse, collections::HashMap, fmt::Debug, path::PathBuf, sync::Mutex, time::Duration,
-};
-
-pub use bdk_file_store;
-pub use clap;
-
-#[derive(Parser)]
-#[clap(author, version, about, long_about = None)]
-#[clap(propagate_version = true)]
-pub struct Args<C: clap::Subcommand> {
- #[clap(env = "DESCRIPTOR")]
- pub descriptor: String,
- #[clap(env = "CHANGE_DESCRIPTOR")]
- pub change_descriptor: Option<String>,
-
- #[clap(env = "BITCOIN_NETWORK", long, default_value = "signet")]
- pub network: Network,
-
- #[clap(env = "BDK_DB_PATH", long, default_value = ".bdk_example_db")]
- pub db_path: PathBuf,
-
- #[clap(env = "BDK_CP_LIMIT", long, default_value = "20")]
- pub cp_limit: usize,
-
- #[clap(subcommand)]
- pub command: Commands<C>,
-}
-
-#[derive(Subcommand, Debug, Clone)]
-pub enum Commands<C: clap::Subcommand> {
- #[clap(flatten)]
- ChainSpecific(C),
- /// Address generation and inspection.
- Address {
- #[clap(subcommand)]
- addr_cmd: AddressCmd,
- },
- /// Get the wallet balance.
- Balance,
- /// TxOut related commands.
- #[clap(name = "txout")]
- TxOut {
- #[clap(subcommand)]
- txout_cmd: TxOutCmd,
- },
- /// Send coins to an address.
- Send {
- value: u64,
- address: Address,
- #[clap(short, default_value = "largest-first")]
- coin_select: CoinSelectionAlgo,
- },
-}
-
-#[derive(Clone, Debug)]
-pub enum CoinSelectionAlgo {
- LargestFirst,
- SmallestFirst,
- OldestFirst,
- NewestFirst,
- BranchAndBound,
-}
-
-impl Default for CoinSelectionAlgo {
- fn default() -> Self {
- Self::LargestFirst
- }
-}
-
-impl core::str::FromStr for CoinSelectionAlgo {
- type Err = anyhow::Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- use CoinSelectionAlgo::*;
- Ok(match s {
- "largest-first" => LargestFirst,
- "smallest-first" => SmallestFirst,
- "oldest-first" => OldestFirst,
- "newest-first" => NewestFirst,
- "bnb" => BranchAndBound,
- unknown => return Err(anyhow!("unknown coin selection algorithm '{}'", unknown)),
- })
- }
-}
-
-impl core::fmt::Display for CoinSelectionAlgo {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- use CoinSelectionAlgo::*;
- write!(
- f,
- "{}",
- match self {
- LargestFirst => "largest-first",
- SmallestFirst => "smallest-first",
- OldestFirst => "oldest-first",
- NewestFirst => "newest-first",
- BranchAndBound => "bnb",
- }
- )
- }
-}
-
-#[derive(Subcommand, Debug, Clone)]
-pub enum AddressCmd {
- /// Get the next unused address.
- Next,
- /// Get a new address regardless of the existing unused addresses.
- New,
- /// List all addresses
- List {
- #[clap(long)]
- change: bool,
- },
- Index,
-}
-
-#[derive(Subcommand, Debug, Clone)]
-pub enum TxOutCmd {
- List {
- /// Return only spent outputs.
- #[clap(short, long)]
- spent: bool,
- /// Return only unspent outputs.
- #[clap(short, long)]
- unspent: bool,
- /// Return only confirmed outputs.
- #[clap(long)]
- confirmed: bool,
- /// Return only unconfirmed outputs.
- #[clap(long)]
- unconfirmed: bool,
- },
-}
-
-#[derive(
- Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize,
-)]
-pub enum Keychain {
- External,
- Internal,
-}
-
-impl core::fmt::Display for Keychain {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Keychain::External => write!(f, "external"),
- Keychain::Internal => write!(f, "internal"),
- }
- }
-}
-
-/// A structure defining the output of an [`AddressCmd`]` execution.
-#[derive(serde::Serialize, serde::Deserialize)]
-pub struct AddrsOutput {
- keychain: String,
- index: u32,
- addrs: Address,
- used: bool,
-}
-
-pub fn run_address_cmd<P>(
- tracker: &Mutex<KeychainTracker<Keychain, P>>,
- db: &Mutex<KeychainStore<Keychain, P>>,
- addr_cmd: AddressCmd,
- network: Network,
-) -> Result<()>
-where
- P: bdk_chain::sparse_chain::ChainPosition,
- KeychainChangeSet<Keychain, P>: serde::Serialize + serde::de::DeserializeOwned,
-{
- let mut tracker = tracker.lock().unwrap();
- let txout_index = &mut tracker.txout_index;
-
- let addr_cmmd_output = match addr_cmd {
- AddressCmd::Next => Some(txout_index.next_unused_spk(&Keychain::External)),
- AddressCmd::New => Some(txout_index.reveal_next_spk(&Keychain::External)),
- _ => None,
- };
-
- if let Some(((index, spk), additions)) = addr_cmmd_output {
- let mut db = db.lock().unwrap();
- // update database since we're about to give out a new address
- db.append_changeset(&additions.into())?;
-
- let spk = spk.clone();
- let address =
- Address::from_script(&spk, network).expect("should always be able to derive address");
- eprintln!("This is the address at index {}", index);
- println!("{}", address);
- }
-
- match addr_cmd {
- AddressCmd::Next | AddressCmd::New => {
- /* covered */
- Ok(())
- }
- AddressCmd::Index => {
- for (keychain, derivation_index) in txout_index.last_revealed_indices() {
- println!("{:?}: {}", keychain, derivation_index);
- }
- Ok(())
- }
- AddressCmd::List { change } => {
- let target_keychain = match change {
- true => Keychain::Internal,
- false => Keychain::External,
- };
- for (index, spk) in txout_index.revealed_spks_of_keychain(&target_keychain) {
- let address = Address::from_script(spk, network)
- .expect("should always be able to derive address");
- println!(
- "{:?} {} used:{}",
- index,
- address,
- txout_index.is_used(&(target_keychain, index))
- );
- }
- Ok(())
- }
- }
-}
-
-pub fn run_balance_cmd<P: ChainPosition>(tracker: &Mutex<KeychainTracker<Keychain, P>>) {
- let tracker = tracker.lock().unwrap();
- let (confirmed, unconfirmed) =
- tracker
- .full_utxos()
- .fold((0, 0), |(confirmed, unconfirmed), (_, utxo)| {
- if utxo.chain_position.height().is_confirmed() {
- (confirmed + utxo.txout.value, unconfirmed)
- } else {
- (confirmed, unconfirmed + utxo.txout.value)
- }
- });
-
- println!("confirmed: {}", confirmed);
- println!("unconfirmed: {}", unconfirmed);
-}
-
-pub fn run_txo_cmd<K: Debug + Clone + Ord, P: ChainPosition>(
- txout_cmd: TxOutCmd,
- tracker: &Mutex<KeychainTracker<K, P>>,
- network: Network,
-) {
- match txout_cmd {
- TxOutCmd::List {
- unspent,
- spent,
- confirmed,
- unconfirmed,
- } => {
- let tracker = tracker.lock().unwrap();
- #[allow(clippy::type_complexity)] // FIXME
- let txouts: Box<dyn Iterator<Item = (&(K, u32), FullTxOut<P>)>> = match (unspent, spent)
- {
- (true, false) => Box::new(tracker.full_utxos()),
- (false, true) => Box::new(
- tracker
- .full_txouts()
- .filter(|(_, txout)| txout.spent_by.is_some()),
- ),
- _ => Box::new(tracker.full_txouts()),
- };
-
- #[allow(clippy::type_complexity)] // FIXME
- let txouts: Box<dyn Iterator<Item = (&(K, u32), FullTxOut<P>)>> =
- match (confirmed, unconfirmed) {
- (true, false) => Box::new(
- txouts.filter(|(_, txout)| txout.chain_position.height().is_confirmed()),
- ),
- (false, true) => Box::new(
- txouts.filter(|(_, txout)| !txout.chain_position.height().is_confirmed()),
- ),
- _ => txouts,
- };
-
- for (spk_index, full_txout) in txouts {
- let address =
- Address::from_script(&full_txout.txout.script_pubkey, network).unwrap();
-
- println!(
- "{:?} {} {} {} spent:{:?}",
- spk_index,
- full_txout.txout.value,
- full_txout.outpoint,
- address,
- full_txout.spent_by
- )
- }
- }
- }
-}
-
-#[allow(clippy::type_complexity)] // FIXME
-pub fn create_tx<P: ChainPosition>(
- value: u64,
- address: Address,
- coin_select: CoinSelectionAlgo,
- keychain_tracker: &mut KeychainTracker<Keychain, P>,
- keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
-) -> Result<(
- Transaction,
- Option<(DerivationAdditions<Keychain>, (Keychain, u32))>,
-)> {
- let mut additions = DerivationAdditions::default();
-
- let assets = bdk_tmp_plan::Assets {
- keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(),
- ..Default::default()
- };
-
- // TODO use planning module
- let mut candidates = planned_utxos(keychain_tracker, &assets).collect::<Vec<_>>();
-
- // apply coin selection algorithm
- match coin_select {
- CoinSelectionAlgo::LargestFirst => {
- candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value))
- }
- CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value),
- CoinSelectionAlgo::OldestFirst => {
- candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone())
- }
- CoinSelectionAlgo::NewestFirst => {
- candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone()))
- }
- CoinSelectionAlgo::BranchAndBound => {}
- }
-
- // turn the txos we chose into weight and value
- let wv_candidates = candidates
- .iter()
- .map(|(plan, utxo)| {
- WeightedValue::new(
- utxo.txout.value,
- plan.expected_weight() as _,
- plan.witness_version().is_some(),
- )
- })
- .collect();
-
- let mut outputs = vec![TxOut {
- value,
- script_pubkey: address.script_pubkey(),
- }];
-
- let internal_keychain = if keychain_tracker
- .txout_index
- .keychains()
- .get(&Keychain::Internal)
- .is_some()
- {
- Keychain::Internal
- } else {
- Keychain::External
- };
-
- let ((change_index, change_script), change_additions) = keychain_tracker
- .txout_index
- .next_unused_spk(&internal_keychain);
- additions.append(change_additions);
-
- // Clone to drop the immutable reference.
- let change_script = change_script.clone();
-
- let change_plan = bdk_tmp_plan::plan_satisfaction(
- &keychain_tracker
- .txout_index
- .keychains()
- .get(&internal_keychain)
- .expect("must exist")
- .at_derivation_index(change_index),
- &assets,
- )
- .expect("failed to obtain change plan");
-
- let mut change_output = TxOut {
- value: 0,
- script_pubkey: change_script,
- };
-
- let cs_opts = CoinSelectorOpt {
- target_feerate: 0.5,
- min_drain_value: keychain_tracker
- .txout_index
- .keychains()
- .get(&internal_keychain)
- .expect("must exist")
- .dust_value(),
- ..CoinSelectorOpt::fund_outputs(
- &outputs,
- &change_output,
- change_plan.expected_weight() as u32,
- )
- };
-
- // TODO: How can we make it easy to shuffle in order of inputs and outputs here?
- // apply coin selection by saying we need to fund these outputs
- let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts);
-
- // just select coins in the order provided until we have enough
- // only use the first result (least waste)
- let selection = match coin_select {
- CoinSelectionAlgo::BranchAndBound => {
- coin_select_bnb(Duration::from_secs(10), coin_selector.clone())
- .map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())?
- }
- _ => coin_selector.select_until_finished()?,
- };
- let (_, selection_meta) = selection.best_strategy();
-
- // get the selected utxos
- let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
-
- if let Some(drain_value) = selection_meta.drain_value {
- change_output.value = drain_value;
- // if the selection tells us to use change and the change value is sufficient, we add it as an output
- outputs.push(change_output)
- }
-
- let mut transaction = Transaction {
- version: 0x02,
- lock_time: keychain_tracker
- .chain()
- .latest_checkpoint()
- .and_then(|block_id| LockTime::from_height(block_id.height).ok())
- .unwrap_or(LockTime::ZERO)
- .into(),
- input: selected_txos
- .iter()
- .map(|(_, utxo)| TxIn {
- previous_output: utxo.outpoint,
- sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
- ..Default::default()
- })
- .collect(),
- output: outputs,
- };
-
- let prevouts = selected_txos
- .iter()
- .map(|(_, utxo)| utxo.txout.clone())
- .collect::<Vec<_>>();
- let sighash_prevouts = Prevouts::All(&prevouts);
-
- // first, set tx values for the plan so that we don't change them while signing
- for (i, (plan, _)) in selected_txos.iter().enumerate() {
- if let Some(sequence) = plan.required_sequence() {
- transaction.input[i].sequence = sequence
- }
- }
-
- // create a short lived transaction
- let _sighash_tx = transaction.clone();
- let mut sighash_cache = SighashCache::new(&_sighash_tx);
-
- for (i, (plan, _)) in selected_txos.iter().enumerate() {
- let requirements = plan.requirements();
- let mut auth_data = bdk_tmp_plan::SatisfactionMaterial::default();
- assert!(
- !requirements.requires_hash_preimages(),
- "can't have hash pre-images since we didn't provide any."
- );
- assert!(
- requirements.signatures.sign_with_keymap(
- i,
- keymap,
- &sighash_prevouts,
- None,
- None,
- &mut sighash_cache,
- &mut auth_data,
- &Secp256k1::default(),
- )?,
- "we should have signed with this input."
- );
-
- match plan.try_complete(&auth_data) {
- bdk_tmp_plan::PlanState::Complete {
- final_script_sig,
- final_script_witness,
- } => {
- if let Some(witness) = final_script_witness {
- transaction.input[i].witness = witness;
- }
-
- if let Some(script_sig) = final_script_sig {
- transaction.input[i].script_sig = script_sig;
- }
- }
- bdk_tmp_plan::PlanState::Incomplete(_) => {
- return Err(anyhow!(
- "we weren't able to complete the plan with our keys."
- ));
- }
- }
- }
-
- let change_info = if selection_meta.drain_value.is_some() {
- Some((additions, (internal_keychain, change_index)))
- } else {
- None
- };
-
- Ok((transaction, change_info))
-}
-
-pub fn handle_commands<C: clap::Subcommand, P>(
- command: Commands<C>,
- broadcast: impl FnOnce(&Transaction) -> Result<()>,
- // we Mutex around these not because we need them for a simple CLI app but to demonstrate how
- // all the stuff we're doing can be made thread-safe and not keep locks up over an IO bound.
- tracker: &Mutex<KeychainTracker<Keychain, P>>,
- store: &Mutex<KeychainStore<Keychain, P>>,
- network: Network,
- keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
-) -> Result<()>
-where
- P: ChainPosition,
- KeychainChangeSet<Keychain, P>: serde::Serialize + serde::de::DeserializeOwned,
-{
- match command {
- // TODO: Make these functions return stuffs
- Commands::Address { addr_cmd } => run_address_cmd(tracker, store, addr_cmd, network),
- Commands::Balance => {
- run_balance_cmd(tracker);
- Ok(())
- }
- Commands::TxOut { txout_cmd } => {
- run_txo_cmd(txout_cmd, tracker, network);
- Ok(())
- }
- Commands::Send {
- value,
- address,
- coin_select,
- } => {
- let (transaction, change_index) = {
- // take mutable ref to construct tx -- it is only open for a short time while building it.
- let tracker = &mut *tracker.lock().unwrap();
- let (transaction, change_info) =
- create_tx(value, address, coin_select, tracker, keymap)?;
-
- if let Some((change_derivation_changes, (change_keychain, index))) = change_info {
- // We must first persist to disk the fact that we've got a new address from the
- // change keychain so future scans will find the tx we're about to broadcast.
- // If we're unable to persist this, then we don't want to broadcast.
- let store = &mut *store.lock().unwrap();
- store.append_changeset(&change_derivation_changes.into())?;
-
- // We don't want other callers/threads to use this address while we're using it
- // but we also don't want to scan the tx we just created because it's not
- // technically in the blockchain yet.
- tracker.txout_index.mark_used(&change_keychain, index);
- (transaction, Some((change_keychain, index)))
- } else {
- (transaction, None)
- }
- };
-
- match (broadcast)(&transaction) {
- Ok(_) => {
- println!("Broadcasted Tx : {}", transaction.txid());
- let mut tracker = tracker.lock().unwrap();
- match tracker.insert_tx(transaction.clone(), P::unconfirmed()) {
- Ok(changeset) => {
- let store = &mut *store.lock().unwrap();
- // We know the tx is at least unconfirmed now. Note if persisting here fails,
- // it's not a big deal since we can always find it again form
- // blockchain.
- store.append_changeset(&changeset)?;
- Ok(())
- }
- Err(e) => match e {
- InsertTxError::Chain(e) => match e {
- // TODO: add insert_unconfirmed_tx to the chaingraph and sparsechain
- sparse_chain::InsertTxError::TxTooHigh { .. } => unreachable!("we are inserting at unconfirmed position"),
- sparse_chain::InsertTxError::TxMovedUnexpectedly { txid, original_pos, ..} => Err(anyhow!("the tx we created {} has already been confirmed at block {:?}", txid, original_pos)),
- },
- InsertTxError::UnresolvableConflict(e) => Err(e).context("another tx that conflicts with the one we tried to create has been confirmed"),
- }
- }
- }
- Err(e) => {
- let tracker = &mut *tracker.lock().unwrap();
- if let Some((keychain, index)) = change_index {
- // We failed to broadcast, so allow our change address to be used in the future
- tracker.txout_index.unmark_used(&keychain, index);
- }
- Err(e)
- }
- }
- }
- Commands::ChainSpecific(_) => {
- todo!("example code is meant to handle this!")
- }
- }
-}
-
-#[allow(clippy::type_complexity)] // FIXME
-pub fn init<C: clap::Subcommand, P>() -> anyhow::Result<(
- Args<C>,
- KeyMap,
- // These don't need to have mutexes around them, but we want the cli example code to make it obvious how they
- // are thread-safe, forcing the example developers to show where they would lock and unlock things.
- Mutex<KeychainTracker<Keychain, P>>,
- Mutex<KeychainStore<Keychain, P>>,
-)>
-where
- P: sparse_chain::ChainPosition,
- KeychainChangeSet<Keychain, P>: serde::Serialize + serde::de::DeserializeOwned,
-{
- let args = Args::<C>::parse();
- let secp = Secp256k1::default();
- let (descriptor, mut keymap) =
- Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &args.descriptor)?;
-
- let mut tracker = KeychainTracker::default();
- tracker.set_checkpoint_limit(Some(args.cp_limit));
-
- tracker
- .txout_index
- .add_keychain(Keychain::External, descriptor);
-
- let internal = args
- .change_descriptor
- .clone()
- .map(|descriptor| Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &descriptor))
- .transpose()?;
- if let Some((internal_descriptor, internal_keymap)) = internal {
- keymap.extend(internal_keymap);
- tracker
- .txout_index
- .add_keychain(Keychain::Internal, internal_descriptor);
- };
-
- let mut db = KeychainStore::<Keychain, P>::new_from_path(args.db_path.as_path())?;
-
- if let Err(e) = db.load_into_keychain_tracker(&mut tracker) {
- match tracker.chain().latest_checkpoint() {
- Some(checkpoint) => eprintln!("Failed to load all changesets from {}. Last checkpoint was at height {}. Error: {}", args.db_path.display(), checkpoint.height, e),
- None => eprintln!("Failed to load any checkpoints from {}: {}", args.db_path.display(), e),
-
- }
- eprintln!("⚠ Consider running a rescan of chain data.");
- }
-
- Ok((args, keymap, Mutex::new(tracker), Mutex::new(db)))
-}
-
-pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, P: ChainPosition>(
- tracker: &'a KeychainTracker<Keychain, P>,
- assets: &'a bdk_tmp_plan::Assets<AK>,
-) -> impl Iterator<Item = (bdk_tmp_plan::Plan<AK>, FullTxOut<P>)> + 'a {
- tracker
- .full_utxos()
- .filter_map(move |((keychain, derivation_index), full_txout)| {
- Some((
- bdk_tmp_plan::plan_satisfaction(
- &tracker
- .txout_index
- .keychains()
- .get(keychain)
- .expect("must exist since we have a utxo for it")
- .at_derivation_index(*derivation_index),
- assets,
- )?,
- full_txout,
- ))
- })
-}
use bdk::SignOptions;
use bdk::{bitcoin::Network, Wallet};
use bdk_electrum::electrum_client::{self, ElectrumApi};
-use bdk_electrum::v2::ElectrumExt;
+use bdk_electrum::ElectrumExt;
use bdk_file_store::Store;
fn main() -> Result<(), Box<dyn std::error::Error>> {
wallet::AddressIndex,
SignOptions, Wallet,
};
-use bdk_esplora::{esplora_client, v2::EsploraExt};
+use bdk_esplora::{esplora_client, EsploraExt};
use bdk_file_store::Store;
fn main() -> Result<(), Box<dyn std::error::Error>> {
wallet::AddressIndex,
SignOptions, Wallet,
};
-use bdk_esplora::{esplora_client, v2::EsploraAsyncExt};
+use bdk_esplora::{esplora_client, EsploraAsyncExt};
use bdk_file_store::Store;
const DB_MAGIC: &str = "bdk_wallet_esplora_async_example";