[chain_redesign] Remove old structures

Other changes:

* The `async-https` feature of `bdk_esplora` is no longer default.
* Rename `ObservedAs` to `ChainPosition`.
* Set temporary MSRV to 1.60.0 to compile all workspace members will all
  features.
This commit is contained in:
志宇
2023-05-24 11:37:26 +08:00
parent 5860704b2d
commit 1c3cbefa4d
48 changed files with 310 additions and 7161 deletions

View File

@@ -1,102 +1,43 @@
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(
@@ -109,49 +50,24 @@ pub enum ConfirmationTime {
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 }
}
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 },
}
}
}
@@ -254,75 +170,32 @@ impl Anchor for ConfirmationTimeAnchor {
}
/// 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;
}
@@ -340,14 +213,10 @@ impl<A: Anchor> FullTxOut<ObservedAs<A>> {
///
/// 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) {
@@ -355,15 +224,15 @@ impl<A: Anchor> FullTxOut<ObservedAs<A>> {
}
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;
}

View File

@@ -1,639 +0,0 @@
//! 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> {}

View File

@@ -8,31 +8,16 @@
//! 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")]
@@ -187,116 +172,6 @@ impl<K, A> From<IndexedAdditions<A, DerivationAdditions<K>>> for LocalChangeSet<
}
}
#[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(
@@ -355,9 +230,8 @@ impl core::ops::Add for Balance {
#[cfg(test)]
mod test {
use crate::TxHeight;
use super::*;
#[test]
fn append_keychain_derivation_indices() {
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
@@ -375,25 +249,18 @@ mod test {
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));
}
}

View File

@@ -1,108 +0,0 @@
//! 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(())
}
}

View File

@@ -1,308 +0,0 @@
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
}
}

View File

@@ -166,7 +166,10 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
///
/// 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"

View File

@@ -19,7 +19,6 @@
//! [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;
@@ -27,7 +26,6 @@ pub use 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::*;

File diff suppressed because it is too large Load Diff

View File

@@ -20,13 +20,13 @@ use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid};
/// 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

View File

@@ -56,8 +56,8 @@
//! ```
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};
@@ -135,7 +135,7 @@ impl Default for TxNodeInternal {
#[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>,
}
@@ -614,7 +614,7 @@ impl<A: Anchor> TxGraph<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),
@@ -622,7 +622,7 @@ impl<A: Anchor> TxGraph<A> {
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,
}
}
@@ -651,7 +651,7 @@ impl<A: Anchor> TxGraph<A> {
}
}
Ok(Some(ObservedAs::Unconfirmed(*last_seen)))
Ok(Some(ChainPosition::Unconfirmed(*last_seen)))
}
/// Get the position of the transaction in `chain` with tip `chain_tip`.
@@ -664,7 +664,7 @@ impl<A: Anchor> TxGraph<A> {
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")
}
@@ -686,7 +686,7 @@ impl<A: Anchor> TxGraph<A> {
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()
@@ -714,7 +714,7 @@ impl<A: Anchor> TxGraph<A> {
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")
}
@@ -786,7 +786,7 @@ impl<A: Anchor> TxGraph<A> {
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(
@@ -837,7 +837,7 @@ impl<A: Anchor> TxGraph<A> {
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"))
}
@@ -865,7 +865,7 @@ impl<A: Anchor> TxGraph<A> {
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
@@ -886,7 +886,7 @@ impl<A: Anchor> TxGraph<A> {
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"))
}
@@ -919,14 +919,14 @@ impl<A: Anchor> TxGraph<A> {
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 {