Merge bitcoindevkit/bdk#926: Introduce redesigned bdk_chain
structures
b799a5728b7e08a18059ffee7fe7ee6078354977 [bdk_chain_redesign] Add tests for `IndexedTxGraph` with `LocalChain` (rajarshimaitra)
8cd0328eec0b55f10dd085597a435b689b37444a [bdk_chain_redesign] Implement `OwnedIndexer` for indexers (rajarshimaitra)
911af34f509ad0b4c86c0be819d79b202679e3e5 [bdk_chain_redesign] Fix calculation bugs. (rajarshimaitra)
e536307e5c5532fc0cac0657a26609cfd26115bf [bdk_chain_redesign] Fix `tx_graph::Additions::append` logic (志宇)
f101dde09b6067c17b0b64ea3e3efd358a32a820 [bdk_chain_redesign] Fix `tx_graph::Additions::append` logic (志宇)
1b152647c557d2ff492cd241999c5320f7c4f6c5 [bdk_chain_redesign] Change `insert_relevant_txs` method (志宇)
ecc74ce4cd1ab0c51c2e9bcaa4f7d53390dd4d9b [bdk_chain_redesign] Docs for `is_mature` and `is_confirmed_and_spendable` (志宇)
ac336aa32f485cb253ac7ea5cae3b4bcc78cf507 [bdk_chain_redesign] Make `insert_relevant_txs` topologically-agnostic (志宇)
165b874dfedb2e05a7ea5d56e6f80577ada48d73 [bdk_chain_redesign] Add test for `insert_relevant_txs` (志宇)
f3e7b67bf1195eadf48a8f26f960b3f39d6134f7 [bdk_chain_redesign] Various tweaks and fixes (志宇)
03c128311a687249238fb109f804cde25ffddad2 [bdk_chain_redesign] Revert changes to `SparseChain` (志宇)
34a7bf5afe2d78db47b149f4958f0a0365b017d0 [bdk_chain_redesign] Rm unnecessary code and premature optimisation (志宇)
6c495707423cc7ce26c3027893dc812281b73053 [bdk_chain_redesign] Rm `HashSet` from `TxGraph::relevant_heights` (志宇)
1003fe2ee6167e110b0195e2431560b5b222e2f1 [bdk_chain_redesign] Test `LocalChain` (志宇)
7175a82c04b0a3f0c5716f4a37e122da70b8ceac [bdk_chain_redesign] Add tests for `TxGraph::relevant_heights` (志宇)
8e36a2e5f6d0fe2813dca9a13005e0b1be7e94c3 [bdk_chain_redesign] Remove incomplete logic (志宇)
81436fcd72a3c45bb10a098be28de0116322d22d [bdk_chain_redesign] Fix `Anchor` definition + docs (志宇)
001efdd1cb658f3a2929ffcf9047e6b07e9bd15c Include tests for new updates of TxGraph (rajarshimaitra)
10ab77c549e597d0d8157d94ae6fa3b4d39fd5dc [bdk_chain_redesign] MOVE `TxIndex` into `indexed_chain_graph.rs` (志宇)
7d92337b932fdcfec7008da8ed81f2b4b6e7a069 [bdk_chain_redesign] Remove `IndexedTxGraph::last_height` (志宇)
a7fbe0ac672cde6c308737fc98020f6693071a5f [bdk_chain_redesign] Documentation improvements (志宇)
ee1060f2ff168e6aaffa41882be2b319729f7de8 [bdk_chain_redesign] Simplify `LocalChain` (志宇)
611d2e3ea2ed9249ddf04e0f9089642160e5c901 [bdk_chain_redesign] Consistent `ChainOracle` (志宇)
bff80ec378fab29556099f9830bcb42911658710 [bdk_chain_redesign] Improve `BlockAnchor` docs (志宇)
24cd8c5cc7f3a6bd0db2bd45642f08a28ea5337a [bdk_chain_redesign] More tweaks and renamings (志宇)
ddd5e951f5ec77070034c7390a635d8d5bd7cb85 [bdk_chain_redesign] Modify signature of `TxIndex` (志宇)
da4cef044d4a3ad0f44ff1e33936c93c38c2f774 [bdk_chain_redesign] Introduce `Append` trait for additions (志宇)
89cfa4d78e059f9fe2544b690bbbf90e92b3efee [bdk_chain_redesign] Better names, comments and generic bounds (志宇)
6e59dce10b66212d7180cadabba887cc4d20fc32 [bdk_chain_redesign] `chain_oracle::Cache` (志宇)
a7eaebbb77f8794c5ff3717aaf0cf73dd5a77480 [bdk_chain_redesign] Add serde support for `IndexedAdditions` (志宇)
c09cd2afce4e649caa2797628edaffae08a60628 [bdk_chain_redesign] Added methods to `LocalChain` (志宇)
7810059ed0f23cae7dee61fe587a1c8f3f49480a [bdk_chain_redesign] `TxGraph` tweaks (志宇)
a63ffe97397cd14bc0a13ea5e96ceddf8b63a4f0 [bdk_chain_redesign] Simplify `TxIndex` (志宇)
a1172def7df478c61076b0d99d5d0f5f9cd99da6 [bdk_chain_redesign] Revert some API changes (志宇)
8c906170c96919cd8c65e306b8351fe01e139fd4 [bdk_chain_redesign] Make default anchor for `TxGraph` as `()` (志宇)
468701a1295c90761749e1bb46cc201cc7f95613 [bdk_chain_redesign] Initial work on `LocalChain`. (志宇)
34d0277e44fd054c8d463dfa756d8531ccda3ca9 [bdk_chain_redesign] Rm anchor type param for structs that don't use it (志宇)
3440a057110fbbcc653f2f8c7d58175472299bae [bdk_chain_redesign] Add docs (志宇)
236c50fa7bace29a0373dd16416ecebbb6dc1ae8 [bdk_chain_redesign] `IndexedTxGraph` keeps track of the last synced height (志宇)
e902c10295ba430bf0522b92ffab68cd60bd1666 [bdk_chain_redesign] Fix `apply_additions` logic for `IndexedTxGraph`. (志宇)
313965d8c84f5de43cafa58c7bd9250aea93b22c [bdk_chain_redesign] `mut_index` should be `index_mut` (志宇)
db7883d813e97229340c32a8fa82a9a13bac7361 [bdk_chain_redesign] Add balance methods to `IndexedTxGraph` (志宇)
d0a2aa83befce5dda26f8b3ae05449f6967df25a [bdk_chain_redesign] Add `apply_additions` to `IndexedTxGraph` (志宇)
6cbb18d409d84ea0c399d9b3ecb0cdb49cc0b32e [bdk_chain_redesign] MOVE: `IndexedTxGraph` into submodule (志宇)
784cd34e3db727659dbb26c428ed9096927286c1 [bdk_chain_redesign] List chain data methods can be try/non-try (志宇)
43b648fee02291858dfcab9b639c55a0bc3fad81 [bdk_chain_redesign] Add `..in_chain` methods (志宇)
61a8606fbcaec933f915c4f0600cd6f5e35636e8 [bdk_chain_redesign] Introduce `ChainOracle` and `TxIndex` traits (志宇)
5ae5fe30ebd53d72fe567509506ae0cda7a3a244 [bdk_chain_redesign] Introduce `BlockAnchor` trait (志宇)
Pull request description:
### Description
This is part of #895
The initial `bdk_chain` structures allowed updating to be done without blocking IO (alongside many other benefits). However, the requirement to have transactions "perfectly positioned" in our `SparseChain` increased complexity of the syncing API. Updates needed to be meticulously crafted to properly connect with the original `SparseChain`. Additionally, it forced us to keep a local copy of the "best chain" data (which may not always be needed depending on the chain source).
The redesigned structures, as introduced by this PR, fixes these shortcomings.
1. Instead of `SparseChain`, we introduce the ability to `Anchor` a transaction to multiple blocks that may or may not be in the same chain history. We expand `TxGraph` to records these anchors (while still maintaining the *monotone* nature of `TxGraph`). When updating our new `TxGraph`, we don't need to complicated *are-we-connected* checks that we need for `SparseChain`.
2. The chain source, in combination with our new`TxGraph` is all that we need to determine the "chain position" of a transaction. The chain source only needs to implement a single trait `ChainOracle`. This typically does not need to be updated (as it is remote), although we can have a special `ChainOracle` implementation that is stored locally. This only needs block height and hash information, reducing the scope of *non-monotine* structures that need to be updated.
**What is done:**
* `TxGraph` includes anchors (ids of blocks that the tx is seen in) and last-seem unix timestamp (for determining which non-confirmed tx we should consider as part of "best chain" if there are conflicts). This structure continues to be fully "monotone"; we can introduce data in any manner and not risk resulting in an inconsistent state.
* `LocalChain` includes the "checkpoint" logic of `SparseChain` but removes the `txid` data. `LocalChain` implements the `ChainOracle` trait. Any blockchain-source can also implement the `ChainOracle` trait.
* `IndexedTxGraph` is introduced and contains two fields; an internal `TxGraph` struct and a `TxIndex` implementation. These two fields will be updated atomically and can replace the functionality of `keychain::Tracker`.
**What is in-progress:**
* ~@evanlinjin: The `TxIndex` trait should not have `is_{}_relevant` methods as we cannot guarantee this across all transaction indexes. We should introduce extension traits for these (https://github.com/bitcoindevkit/bdk/pull/926#discussion_r1159286393).~
* ~@evanlinjin: `BlockAnchor` should be defined as "if this block is in chain, then this tx must be in chain". Therefore, the anchor does not provide us with the exact confirmation height of the tx. We need to introduce an extension trait `ExactConfirmationHeightAnchor` for certain operations (https://github.com/bitcoindevkit/bdk/pull/926#discussion_r1155480352).~
**What will be done external to this PR:**
* @rajarshimaitra: Persisting `indexed_tx_graph::Additions` (#937).
* @notmandatory: Update examples to use redesigned structures.
* Update `bdk::Wallet` to use the redesigned structures.
### Changelog notice
* Initial implementation of the `bdk_chain` redesign (as mentioned in #895). Currently, only the `bdk_chain` core structures are implemented. Persistence and updates to the current examples and `bdk::Wallet` will be done in separate PRs.
### Checklists
#### All Submissions:
* [x] I've signed all my commits
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
* [x] I ran `cargo fmt` and `cargo clippy` before committing
#### New Features:
* [x] I've added tests for the new feature
* [x] I've added docs for the new feature
ACKs for top commit:
rajarshimaitra:
ACK b799a5728b
danielabrozzoni:
Partial ACK b799a5728b7e08a18059ffee7fe7ee6078354977 - good job! I haven't looked at the tests or at the methods implementations in depth, but I have reviewed the architecture and it looks good. I have a few non-blocking questions.
Tree-SHA512: 8c386354cbd02f0701b5134991b65d206a54d19a2e78ab204e6ff1fa78a18f16299051bc0bf4ff4d2f5a0adab9b15658fa53cd0de2ca16969f4bf6a485225082
This commit is contained in:
commit
139e3d3802
@ -2,9 +2,29 @@ use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid};
|
||||
|
||||
use crate::{
|
||||
sparse_chain::{self, ChainPosition},
|
||||
COINBASE_MATURITY,
|
||||
Anchor, COINBASE_MATURITY,
|
||||
};
|
||||
|
||||
/// Represents an observation 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> {
|
||||
/// 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: Clone> ObservedAs<&A> {
|
||||
pub fn cloned(self) -> ObservedAs<A> {
|
||||
match self {
|
||||
ObservedAs::Confirmed(a) => ObservedAs::Confirmed(a.clone()),
|
||||
ObservedAs::Unconfirmed(last_seen) => ObservedAs::Unconfirmed(last_seen),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the height at which a transaction is confirmed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(
|
||||
@ -118,7 +138,7 @@ impl ConfirmationTime {
|
||||
}
|
||||
|
||||
/// A reference to a block in the canonical chain.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
@ -140,6 +160,12 @@ impl Default for BlockId {
|
||||
}
|
||||
}
|
||||
|
||||
impl Anchor for BlockId {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u32, BlockHash)> for BlockId {
|
||||
fn from((height, hash): (u32, BlockHash)) -> Self {
|
||||
Self { height, hash }
|
||||
@ -162,21 +188,21 @@ impl From<(&u32, &BlockHash)> for BlockId {
|
||||
}
|
||||
|
||||
/// A `TxOut` with as much data as we can retrieve about it
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FullTxOut<I> {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FullTxOut<P> {
|
||||
/// 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: I,
|
||||
pub chain_position: P,
|
||||
/// The txid and chain position of the transaction (if any) that has spent this output.
|
||||
pub spent_by: Option<(I, Txid)>,
|
||||
pub spent_by: Option<(P, Txid)>,
|
||||
/// Whether this output is on a coinbase transaction.
|
||||
pub is_on_coinbase: bool,
|
||||
}
|
||||
|
||||
impl<I: ChainPosition> FullTxOut<I> {
|
||||
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
|
||||
@ -215,4 +241,69 @@ impl<I: ChainPosition> FullTxOut<I> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make test
|
||||
impl<A: Anchor> FullTxOut<ObservedAs<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(_) => {
|
||||
debug_assert!(false, "coinbase tx can never be unconfirmed");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let age = tip.saturating_sub(tx_height);
|
||||
if age + 1 < COINBASE_MATURITY {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether the utxo is/was/will be spendable with chain `tip`.
|
||||
///
|
||||
/// 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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let confirmation_height = match &self.chain_position {
|
||||
ObservedAs::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
|
||||
ObservedAs::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 spending_anchor.anchor_block().height <= tip {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use crate::{
|
||||
collections::HashSet,
|
||||
sparse_chain::{self, ChainPosition, SparseChain},
|
||||
tx_graph::{self, TxGraph},
|
||||
BlockId, ForEachTxOut, FullTxOut, TxHeight,
|
||||
Append, BlockId, ForEachTxOut, FullTxOut, TxHeight,
|
||||
};
|
||||
use alloc::{string::ToString, vec::Vec};
|
||||
use bitcoin::{OutPoint, Transaction, TxOut, Txid};
|
||||
|
21
crates/chain/src/chain_oracle.rs
Normal file
21
crates/chain/src/chain_oracle.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use crate::BlockId;
|
||||
|
||||
/// Represents a service that tracks the blockchain.
|
||||
///
|
||||
/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`]
|
||||
/// is an ancestor of another "static block".
|
||||
///
|
||||
/// [`is_block_in_chain`]: Self::is_block_in_chain
|
||||
pub trait ChainOracle {
|
||||
/// Error type.
|
||||
type Error: core::fmt::Debug;
|
||||
|
||||
/// Determines whether `block` of [`BlockId`] exists as an ancestor of `static_block`.
|
||||
///
|
||||
/// If `None` is returned, it means the implementation cannot determine whether `block` exists.
|
||||
fn is_block_in_chain(
|
||||
&self,
|
||||
block: BlockId,
|
||||
static_block: BlockId,
|
||||
) -> Result<Option<bool>, Self::Error>;
|
||||
}
|
325
crates/chain/src/indexed_tx_graph.rs
Normal file
325
crates/chain/src/indexed_tx_graph.rs
Normal file
@ -0,0 +1,325 @@
|
||||
use core::convert::Infallible;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxOut};
|
||||
|
||||
use crate::{
|
||||
keychain::Balance,
|
||||
tx_graph::{Additions, TxGraph},
|
||||
Anchor, Append, BlockId, ChainOracle, FullTxOut, ObservedAs,
|
||||
};
|
||||
|
||||
/// A struct that combines [`TxGraph`] and an [`Indexer`] implementation.
|
||||
///
|
||||
/// This structure ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
|
||||
pub struct IndexedTxGraph<A, I> {
|
||||
/// Transaction index.
|
||||
pub index: I,
|
||||
graph: TxGraph<A>,
|
||||
}
|
||||
|
||||
impl<A, I: Default> Default for IndexedTxGraph<A, I> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
graph: Default::default(),
|
||||
index: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
|
||||
/// Get a reference of the internal transaction graph.
|
||||
pub fn graph(&self) -> &TxGraph<A> {
|
||||
&self.graph
|
||||
}
|
||||
|
||||
/// Applies the [`IndexedAdditions`] to the [`IndexedTxGraph`].
|
||||
pub fn apply_additions(&mut self, additions: IndexedAdditions<A, I::Additions>) {
|
||||
let IndexedAdditions {
|
||||
graph_additions,
|
||||
index_additions,
|
||||
} = additions;
|
||||
|
||||
self.index.apply_additions(index_additions);
|
||||
|
||||
for tx in &graph_additions.tx {
|
||||
self.index.index_tx(tx);
|
||||
}
|
||||
for (&outpoint, txout) in &graph_additions.txout {
|
||||
self.index.index_txout(outpoint, txout);
|
||||
}
|
||||
|
||||
self.graph.apply_additions(graph_additions);
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
|
||||
where
|
||||
I::Additions: Default + Append,
|
||||
{
|
||||
/// Apply an `update` directly.
|
||||
///
|
||||
/// `update` is a [`TxGraph<A>`] and the resultant changes is returned as [`IndexedAdditions`].
|
||||
pub fn apply_update(&mut self, update: TxGraph<A>) -> IndexedAdditions<A, I::Additions> {
|
||||
let graph_additions = self.graph.apply_update(update);
|
||||
|
||||
let mut index_additions = I::Additions::default();
|
||||
for added_tx in &graph_additions.tx {
|
||||
index_additions.append(self.index.index_tx(added_tx));
|
||||
}
|
||||
for (&added_outpoint, added_txout) in &graph_additions.txout {
|
||||
index_additions.append(self.index.index_txout(added_outpoint, added_txout));
|
||||
}
|
||||
|
||||
IndexedAdditions {
|
||||
graph_additions,
|
||||
index_additions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a floating `txout` of given `outpoint`.
|
||||
pub fn insert_txout(
|
||||
&mut self,
|
||||
outpoint: OutPoint,
|
||||
txout: &TxOut,
|
||||
) -> IndexedAdditions<A, I::Additions> {
|
||||
let mut update = TxGraph::<A>::default();
|
||||
let _ = update.insert_txout(outpoint, txout.clone());
|
||||
self.apply_update(update)
|
||||
}
|
||||
|
||||
/// Insert and index a transaction into the graph.
|
||||
///
|
||||
/// `anchors` can be provided to anchor the transaction to various blocks. `seen_at` is a
|
||||
/// unix timestamp of when the transaction is last seen.
|
||||
pub fn insert_tx(
|
||||
&mut self,
|
||||
tx: &Transaction,
|
||||
anchors: impl IntoIterator<Item = A>,
|
||||
seen_at: Option<u64>,
|
||||
) -> IndexedAdditions<A, I::Additions> {
|
||||
let txid = tx.txid();
|
||||
|
||||
let mut update = TxGraph::<A>::default();
|
||||
if self.graph.get_tx(txid).is_none() {
|
||||
let _ = update.insert_tx(tx.clone());
|
||||
}
|
||||
for anchor in anchors.into_iter() {
|
||||
let _ = update.insert_anchor(txid, anchor);
|
||||
}
|
||||
if let Some(seen_at) = seen_at {
|
||||
let _ = update.insert_seen_at(txid, seen_at);
|
||||
}
|
||||
|
||||
self.apply_update(update)
|
||||
}
|
||||
|
||||
/// Insert relevant transactions from the given `txs` iterator.
|
||||
///
|
||||
/// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant
|
||||
/// transactions in `txs` will be ignored. `txs` do not need to be in topological order.
|
||||
///
|
||||
/// `anchors` can be provided to anchor the transactions to blocks. `seen_at` is a unix
|
||||
/// timestamp of when the transactions are last seen.
|
||||
pub fn insert_relevant_txs<'t>(
|
||||
&mut self,
|
||||
txs: impl IntoIterator<Item = (&'t Transaction, impl IntoIterator<Item = A>)>,
|
||||
seen_at: Option<u64>,
|
||||
) -> IndexedAdditions<A, I::Additions> {
|
||||
// The algorithm below allows for non-topologically ordered transactions by using two loops.
|
||||
// This is achieved by:
|
||||
// 1. insert all txs into the index. If they are irrelevant then that's fine it will just
|
||||
// not store anything about them.
|
||||
// 2. decide whether to insert them into the graph depending on whether `is_tx_relevant`
|
||||
// returns true or not. (in a second loop).
|
||||
let mut additions = IndexedAdditions::<A, I::Additions>::default();
|
||||
let mut transactions = Vec::new();
|
||||
for (tx, anchors) in txs.into_iter() {
|
||||
additions.index_additions.append(self.index.index_tx(tx));
|
||||
transactions.push((tx, anchors));
|
||||
}
|
||||
additions.append(
|
||||
transactions
|
||||
.into_iter()
|
||||
.filter_map(|(tx, anchors)| match self.index.is_tx_relevant(tx) {
|
||||
true => Some(self.insert_tx(tx, anchors, seen_at)),
|
||||
false => None,
|
||||
})
|
||||
.fold(Default::default(), |mut acc, other| {
|
||||
acc.append(other);
|
||||
acc
|
||||
}),
|
||||
);
|
||||
additions
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor, I: OwnedIndexer> IndexedTxGraph<A, I> {
|
||||
pub fn try_list_owned_txouts<'a, C: ChainOracle + 'a>(
|
||||
&'a self,
|
||||
chain: &'a C,
|
||||
chain_tip: BlockId,
|
||||
) -> impl Iterator<Item = Result<FullTxOut<ObservedAs<A>>, C::Error>> + 'a {
|
||||
self.graph()
|
||||
.try_list_chain_txouts(chain, chain_tip)
|
||||
.filter(|r| {
|
||||
if let Ok(full_txout) = r {
|
||||
if !self.index.is_spk_owned(&full_txout.txout.script_pubkey) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_owned_txouts<'a, C: ChainOracle<Error = Infallible> + 'a>(
|
||||
&'a self,
|
||||
chain: &'a C,
|
||||
chain_tip: BlockId,
|
||||
) -> impl Iterator<Item = FullTxOut<ObservedAs<A>>> + 'a {
|
||||
self.try_list_owned_txouts(chain, chain_tip)
|
||||
.map(|r| r.expect("oracle is infallible"))
|
||||
}
|
||||
|
||||
pub fn try_list_owned_unspents<'a, C: ChainOracle + 'a>(
|
||||
&'a self,
|
||||
chain: &'a C,
|
||||
chain_tip: BlockId,
|
||||
) -> impl Iterator<Item = Result<FullTxOut<ObservedAs<A>>, C::Error>> + 'a {
|
||||
self.graph()
|
||||
.try_list_chain_unspents(chain, chain_tip)
|
||||
.filter(|r| {
|
||||
if let Ok(full_txout) = r {
|
||||
if !self.index.is_spk_owned(&full_txout.txout.script_pubkey) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_owned_unspents<'a, C: ChainOracle<Error = Infallible> + 'a>(
|
||||
&'a self,
|
||||
chain: &'a C,
|
||||
chain_tip: BlockId,
|
||||
) -> impl Iterator<Item = FullTxOut<ObservedAs<A>>> + 'a {
|
||||
self.try_list_owned_unspents(chain, chain_tip)
|
||||
.map(|r| r.expect("oracle is infallible"))
|
||||
}
|
||||
|
||||
pub fn try_balance<C, F>(
|
||||
&self,
|
||||
chain: &C,
|
||||
chain_tip: BlockId,
|
||||
mut should_trust: F,
|
||||
) -> Result<Balance, C::Error>
|
||||
where
|
||||
C: ChainOracle,
|
||||
F: FnMut(&Script) -> bool,
|
||||
{
|
||||
let tip_height = chain_tip.anchor_block().height;
|
||||
|
||||
let mut immature = 0;
|
||||
let mut trusted_pending = 0;
|
||||
let mut untrusted_pending = 0;
|
||||
let mut confirmed = 0;
|
||||
|
||||
for res in self.try_list_owned_unspents(chain, chain_tip) {
|
||||
let txout = res?;
|
||||
|
||||
match &txout.chain_position {
|
||||
ObservedAs::Confirmed(_) => {
|
||||
if txout.is_confirmed_and_spendable(tip_height) {
|
||||
confirmed += txout.txout.value;
|
||||
} else if !txout.is_mature(tip_height) {
|
||||
immature += txout.txout.value;
|
||||
}
|
||||
}
|
||||
ObservedAs::Unconfirmed(_) => {
|
||||
if should_trust(&txout.txout.script_pubkey) {
|
||||
trusted_pending += txout.txout.value;
|
||||
} else {
|
||||
untrusted_pending += txout.txout.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Balance {
|
||||
immature,
|
||||
trusted_pending,
|
||||
untrusted_pending,
|
||||
confirmed,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn balance<C, F>(&self, chain: &C, chain_tip: BlockId, should_trust: F) -> Balance
|
||||
where
|
||||
C: ChainOracle<Error = Infallible>,
|
||||
F: FnMut(&Script) -> bool,
|
||||
{
|
||||
self.try_balance(chain, chain_tip, should_trust)
|
||||
.expect("error is infallible")
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure that represents changes to an [`IndexedTxGraph`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(
|
||||
crate = "serde_crate",
|
||||
bound(
|
||||
deserialize = "A: Ord + serde::Deserialize<'de>, IA: serde::Deserialize<'de>",
|
||||
serialize = "A: Ord + serde::Serialize, IA: serde::Serialize"
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[must_use]
|
||||
pub struct IndexedAdditions<A, IA> {
|
||||
/// [`TxGraph`] additions.
|
||||
pub graph_additions: Additions<A>,
|
||||
/// [`Indexer`] additions.
|
||||
pub index_additions: IA,
|
||||
}
|
||||
|
||||
impl<A, IA: Default> Default for IndexedAdditions<A, IA> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
graph_additions: Default::default(),
|
||||
index_additions: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor, IA: Append> Append for IndexedAdditions<A, IA> {
|
||||
fn append(&mut self, other: Self) {
|
||||
self.graph_additions.append(other.graph_additions);
|
||||
self.index_additions.append(other.index_additions);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a structure that can index transaction data.
|
||||
pub trait Indexer {
|
||||
/// The resultant "additions" when new transaction data is indexed.
|
||||
type Additions;
|
||||
|
||||
/// Scan and index the given `outpoint` and `txout`.
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions;
|
||||
|
||||
/// Scan and index the given transaction.
|
||||
fn index_tx(&mut self, tx: &Transaction) -> Self::Additions;
|
||||
|
||||
/// Apply additions to itself.
|
||||
fn apply_additions(&mut self, additions: Self::Additions);
|
||||
|
||||
/// Determines whether the transaction should be included in the index.
|
||||
fn is_tx_relevant(&self, tx: &Transaction) -> bool;
|
||||
}
|
||||
|
||||
/// A trait that extends [`Indexer`] to also index "owned" script pubkeys.
|
||||
pub trait OwnedIndexer: Indexer {
|
||||
/// Determines whether a given script pubkey (`spk`) is owned.
|
||||
fn is_spk_owned(&self, spk: &Script) -> bool;
|
||||
}
|
@ -14,12 +14,13 @@
|
||||
//! [`KeychainChangeSet`]s.
|
||||
//!
|
||||
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
|
||||
|
||||
use crate::{
|
||||
chain_graph::{self, ChainGraph},
|
||||
collections::BTreeMap,
|
||||
sparse_chain::ChainPosition,
|
||||
tx_graph::TxGraph,
|
||||
ForEachTxOut,
|
||||
Append, ForEachTxOut,
|
||||
};
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
@ -69,12 +70,12 @@ impl<K> DerivationAdditions<K> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Ord> DerivationAdditions<K> {
|
||||
impl<K: Ord> Append for DerivationAdditions<K> {
|
||||
/// Append another [`DerivationAdditions`] into self.
|
||||
///
|
||||
/// If the keychain already exists, increase the index when the other's index > self's index.
|
||||
/// If the keychain did not exist, append the new keychain.
|
||||
pub fn append(&mut self, mut other: Self) {
|
||||
fn append(&mut self, mut other: Self) {
|
||||
self.0.iter_mut().for_each(|(key, index)| {
|
||||
if let Some(other_index) = other.0.remove(key) {
|
||||
*index = other_index.max(*index);
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
collections::*,
|
||||
indexed_tx_graph::{Indexer, OwnedIndexer},
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
ForEachTxOut, SpkTxOutIndex,
|
||||
};
|
||||
@ -7,6 +8,8 @@ use alloc::{borrow::Cow, vec::Vec};
|
||||
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, TxOut};
|
||||
use core::{fmt::Debug, ops::Deref};
|
||||
|
||||
use crate::Append;
|
||||
|
||||
use super::DerivationAdditions;
|
||||
|
||||
/// Maximum [BIP32](https://bips.xyz/32) derivation index.
|
||||
@ -88,6 +91,32 @@ impl<K> Deref for KeychainTxOutIndex<K> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Clone + Ord + Debug + 'static> Indexer for KeychainTxOutIndex<K> {
|
||||
type Additions = DerivationAdditions<K>;
|
||||
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
|
||||
self.scan_txout(outpoint, txout)
|
||||
}
|
||||
|
||||
fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::Additions {
|
||||
self.scan(tx)
|
||||
}
|
||||
|
||||
fn apply_additions(&mut self, additions: Self::Additions) {
|
||||
self.apply_additions(additions)
|
||||
}
|
||||
|
||||
fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool {
|
||||
self.is_relevant(tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Clone + Ord + Debug + 'static> OwnedIndexer for KeychainTxOutIndex<K> {
|
||||
fn is_spk_owned(&self, spk: &Script) -> bool {
|
||||
self.inner().is_spk_owned(spk)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// Scans an object for relevant outpoints, which are stored and indexed internally.
|
||||
///
|
||||
|
@ -24,11 +24,15 @@ mod spk_txout_index;
|
||||
pub use spk_txout_index::*;
|
||||
mod chain_data;
|
||||
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::*;
|
||||
mod chain_oracle;
|
||||
pub use chain_oracle::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod example_utils;
|
||||
|
203
crates/chain/src/local_chain.rs
Normal file
203
crates/chain/src/local_chain.rs
Normal file
@ -0,0 +1,203 @@
|
||||
use core::convert::Infallible;
|
||||
|
||||
use alloc::collections::{BTreeMap, BTreeSet};
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
use crate::{BlockId, ChainOracle};
|
||||
|
||||
/// This is a local implementation of [`ChainOracle`].
|
||||
///
|
||||
/// TODO: We need a cache/snapshot thing for chain oracle.
|
||||
/// * Minimize calls to remotes.
|
||||
/// * Can we cache it forever? Should we drop stuff?
|
||||
/// * Assume anything deeper than (i.e. 10) blocks won't be reorged.
|
||||
/// * Is this a cache on txs or block? or both?
|
||||
/// TODO: Parents of children are confirmed if children are confirmed.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct LocalChain {
|
||||
blocks: BTreeMap<u32, BlockHash>,
|
||||
}
|
||||
|
||||
impl ChainOracle for LocalChain {
|
||||
type Error = Infallible;
|
||||
|
||||
fn is_block_in_chain(
|
||||
&self,
|
||||
block: BlockId,
|
||||
static_block: BlockId,
|
||||
) -> Result<Option<bool>, Self::Error> {
|
||||
if block.height > static_block.height {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(
|
||||
match (
|
||||
self.blocks.get(&block.height),
|
||||
self.blocks.get(&static_block.height),
|
||||
) {
|
||||
(Some(&hash), Some(&static_hash)) => {
|
||||
Some(hash == block.hash && static_hash == static_block.hash)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<BTreeMap<u32, BlockHash>> for LocalChain {
|
||||
fn as_ref(&self) -> &BTreeMap<u32, BlockHash> {
|
||||
&self.blocks
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
||||
fn from(value: LocalChain) -> Self {
|
||||
value.blocks
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<u32, BlockHash>> for LocalChain {
|
||||
fn from(value: BTreeMap<u32, BlockHash>) -> Self {
|
||||
Self { blocks: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalChain {
|
||||
pub fn from_blocks<B>(blocks: B) -> Self
|
||||
where
|
||||
B: IntoIterator<Item = BlockId>,
|
||||
{
|
||||
Self {
|
||||
blocks: blocks.into_iter().map(|b| (b.height, b.hash)).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tip(&self) -> Option<BlockId> {
|
||||
self.blocks
|
||||
.iter()
|
||||
.last()
|
||||
.map(|(&height, &hash)| BlockId { height, hash })
|
||||
}
|
||||
|
||||
/// Get a block at the given height.
|
||||
pub fn get_block(&self, height: u32) -> Option<BlockId> {
|
||||
self.blocks
|
||||
.get(&height)
|
||||
.map(|&hash| BlockId { height, hash })
|
||||
}
|
||||
|
||||
/// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights
|
||||
/// are to be re-filled.
|
||||
pub fn determine_changeset(&self, update: &Self) -> Result<ChangeSet, UpdateNotConnectedError> {
|
||||
let update = update.as_ref();
|
||||
let update_tip = match update.keys().last().cloned() {
|
||||
Some(tip) => tip,
|
||||
None => return Ok(ChangeSet::default()),
|
||||
};
|
||||
|
||||
// this is the latest height where both the update and local chain has the same block hash
|
||||
let agreement_height = update
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|&(u_height, u_hash)| self.blocks.get(u_height) == Some(u_hash))
|
||||
.map(|(&height, _)| height);
|
||||
|
||||
// the lower bound of the range to invalidate
|
||||
let invalidate_lb = match agreement_height {
|
||||
Some(height) if height == update_tip => u32::MAX,
|
||||
Some(height) => height + 1,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
// the first block's height to invalidate in the local chain
|
||||
let invalidate_from_height = self.blocks.range(invalidate_lb..).next().map(|(&h, _)| h);
|
||||
|
||||
// the first block of height to invalidate (if any) should be represented in the update
|
||||
if let Some(first_invalid_height) = invalidate_from_height {
|
||||
if !update.contains_key(&first_invalid_height) {
|
||||
return Err(UpdateNotConnectedError(first_invalid_height));
|
||||
}
|
||||
}
|
||||
|
||||
let mut changeset: BTreeMap<u32, Option<BlockHash>> = match invalidate_from_height {
|
||||
Some(first_invalid_height) => {
|
||||
// the first block of height to invalidate should be represented in the update
|
||||
if !update.contains_key(&first_invalid_height) {
|
||||
return Err(UpdateNotConnectedError(first_invalid_height));
|
||||
}
|
||||
self.blocks
|
||||
.range(first_invalid_height..)
|
||||
.map(|(height, _)| (*height, None))
|
||||
.collect()
|
||||
}
|
||||
None => BTreeMap::new(),
|
||||
};
|
||||
for (height, update_hash) in update {
|
||||
let original_hash = self.blocks.get(height);
|
||||
if Some(update_hash) != original_hash {
|
||||
changeset.insert(*height, Some(*update_hash));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Applies the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: ChangeSet) {
|
||||
for (height, blockhash) in changeset {
|
||||
match blockhash {
|
||||
Some(blockhash) => self.blocks.insert(height, blockhash),
|
||||
None => self.blocks.remove(&height),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates [`LocalChain`] with an update [`LocalChain`].
|
||||
///
|
||||
/// 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, update: Self) -> Result<ChangeSet, UpdateNotConnectedError> {
|
||||
let changeset = self.determine_changeset(&update)?;
|
||||
self.apply_changeset(changeset.clone());
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
pub fn initial_changeset(&self) -> ChangeSet {
|
||||
self.blocks
|
||||
.iter()
|
||||
.map(|(&height, &hash)| (height, Some(hash)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn heights(&self) -> BTreeSet<u32> {
|
||||
self.blocks.keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`].
|
||||
///
|
||||
/// [`determine_changeset`]: LocalChain::determine_changeset
|
||||
pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
|
||||
|
||||
/// Represents an update failure of [`LocalChain`] due to the update not connecting to the original
|
||||
/// chain.
|
||||
///
|
||||
/// 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.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct UpdateNotConnectedError(pub u32);
|
||||
|
||||
impl core::fmt::Display for UpdateNotConnectedError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"the update cannot connect with the chain, try include block at height {}",
|
||||
self.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for UpdateNotConnectedError {}
|
@ -2,6 +2,7 @@ use core::ops::RangeBounds;
|
||||
|
||||
use crate::{
|
||||
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
|
||||
indexed_tx_graph::{Indexer, OwnedIndexer},
|
||||
ForEachTxOut,
|
||||
};
|
||||
use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid};
|
||||
@ -52,6 +53,34 @@ impl<I> Default for SpkTxOutIndex<I> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Clone + Ord + 'static> Indexer for SpkTxOutIndex<I> {
|
||||
type Additions = ();
|
||||
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
|
||||
self.scan_txout(outpoint, txout);
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn index_tx(&mut self, tx: &Transaction) -> Self::Additions {
|
||||
self.scan(tx);
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn apply_additions(&mut self, _additions: Self::Additions) {
|
||||
// This applies nothing.
|
||||
}
|
||||
|
||||
fn is_tx_relevant(&self, tx: &Transaction) -> bool {
|
||||
self.is_relevant(tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Clone + Ord + 'static> OwnedIndexer for SpkTxOutIndex<I> {
|
||||
fn is_spk_owned(&self, spk: &Script) -> bool {
|
||||
self.spk_indices.get(spk).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// This macro is used instead of a member function of `SpkTxOutIndex`, which would result in a
|
||||
/// compiler error[E0521]: "borrowed data escapes out of closure" when we attempt to take a
|
||||
/// reference out of the `ForEachTxOut` closure during scanning.
|
||||
|
@ -1,3 +1,6 @@
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::collections::BTreeSet;
|
||||
use crate::BlockId;
|
||||
use bitcoin::{Block, OutPoint, Transaction, TxOut};
|
||||
|
||||
/// Trait to do something with every txout contained in a structure.
|
||||
@ -31,3 +34,50 @@ impl ForEachTxOut for Transaction {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait that "anchors" blockchain data to a specific block of height and hash.
|
||||
///
|
||||
/// I.e. If transaction A is anchored in block B, then if block B is in the best chain, we can
|
||||
/// assume that transaction A is also confirmed in the best chain. This does not necessarily mean
|
||||
/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a
|
||||
/// parent block of B.
|
||||
pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash {
|
||||
/// Returns the [`BlockId`] that the associated blockchain data is "anchored" in.
|
||||
fn anchor_block(&self) -> BlockId;
|
||||
|
||||
/// Get the upper bound of the chain data's confirmation height.
|
||||
///
|
||||
/// The default definition gives a pessimistic answer. This can be overridden by the `Anchor`
|
||||
/// implementation for a more accurate value.
|
||||
fn confirmation_height_upper_bound(&self) -> u32 {
|
||||
self.anchor_block().height
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor> Anchor for &'static A {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
<A as Anchor>::anchor_block(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait that makes an object appendable.
|
||||
pub trait Append {
|
||||
/// Append another object of the same type onto `self`.
|
||||
fn append(&mut self, other: Self);
|
||||
}
|
||||
|
||||
impl Append for () {
|
||||
fn append(&mut self, _other: Self) {}
|
||||
}
|
||||
|
||||
impl<K: Ord, V> Append for BTreeMap<K, V> {
|
||||
fn append(&mut self, mut other: Self) {
|
||||
BTreeMap::append(self, &mut other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ord> Append for BTreeSet<T> {
|
||||
fn append(&mut self, mut other: Self) {
|
||||
BTreeSet::append(self, &mut other)
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,14 @@ macro_rules! h {
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! local_chain {
|
||||
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*])
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! chain {
|
||||
($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) };
|
||||
|
@ -136,6 +136,7 @@ fn update_evicts_conflicting_tx() {
|
||||
graph: tx_graph::Additions {
|
||||
tx: [tx_b2.clone()].into(),
|
||||
txout: [].into(),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
@ -215,6 +216,7 @@ fn update_evicts_conflicting_tx() {
|
||||
graph: tx_graph::Additions {
|
||||
tx: [tx_b2].into(),
|
||||
txout: [].into(),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
@ -363,7 +365,7 @@ fn test_get_tx_in_chain() {
|
||||
let _ = cg.insert_tx(tx.clone(), TxHeight::Unconfirmed).unwrap();
|
||||
assert_eq!(
|
||||
cg.get_tx_in_chain(tx.txid()),
|
||||
Some((&TxHeight::Unconfirmed, &tx))
|
||||
Some((&TxHeight::Unconfirmed, &tx,))
|
||||
);
|
||||
}
|
||||
|
||||
@ -395,9 +397,9 @@ fn test_iterate_transactions() {
|
||||
assert_eq!(
|
||||
cg.transactions_in_chain().collect::<Vec<_>>(),
|
||||
vec![
|
||||
(&TxHeight::Confirmed(0), &txs[2]),
|
||||
(&TxHeight::Confirmed(1), &txs[0]),
|
||||
(&TxHeight::Unconfirmed, &txs[1]),
|
||||
(&TxHeight::Confirmed(0), &txs[2],),
|
||||
(&TxHeight::Confirmed(1), &txs[0],),
|
||||
(&TxHeight::Unconfirmed, &txs[1],),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
540
crates/chain/tests/test_indexed_tx_graph.rs
Normal file
540
crates/chain/tests/test_indexed_tx_graph.rs
Normal file
@ -0,0 +1,540 @@
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
|
||||
keychain::{Balance, DerivationAdditions, KeychainTxOutIndex},
|
||||
tx_graph::Additions,
|
||||
BlockId, ObservedAs,
|
||||
};
|
||||
use bitcoin::{secp256k1::Secp256k1, BlockHash, OutPoint, Script, Transaction, TxIn, TxOut};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
|
||||
/// in topological order.
|
||||
///
|
||||
/// Given 3 transactions (A, B, C), where A has 2 owned outputs. B and C spends an output each of A.
|
||||
/// Typically, we would only know whether B and C are relevant if we have indexed A (A's outpoints
|
||||
/// are associated with owned spks in the index). Ensure insertion and indexing is topological-
|
||||
/// agnostic.
|
||||
#[test]
|
||||
fn insert_relevant_txs() {
|
||||
const DESCRIPTOR: &str = "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)";
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTOR)
|
||||
.expect("must be valid");
|
||||
let spk_0 = descriptor.at_derivation_index(0).script_pubkey();
|
||||
let spk_1 = descriptor.at_derivation_index(9).script_pubkey();
|
||||
|
||||
let mut graph = IndexedTxGraph::<BlockId, KeychainTxOutIndex<()>>::default();
|
||||
graph.index.add_keychain((), descriptor);
|
||||
graph.index.set_lookahead(&(), 10);
|
||||
|
||||
let tx_a = Transaction {
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: 10_000,
|
||||
script_pubkey: spk_0,
|
||||
},
|
||||
TxOut {
|
||||
value: 20_000,
|
||||
script_pubkey: spk_1,
|
||||
},
|
||||
],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
let tx_b = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_a.txid(), 0),
|
||||
..Default::default()
|
||||
}],
|
||||
..common::new_tx(1)
|
||||
};
|
||||
|
||||
let tx_c = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_a.txid(), 1),
|
||||
..Default::default()
|
||||
}],
|
||||
..common::new_tx(2)
|
||||
};
|
||||
|
||||
let txs = [tx_c, tx_b, tx_a];
|
||||
|
||||
assert_eq!(
|
||||
graph.insert_relevant_txs(txs.iter().map(|tx| (tx, None)), None),
|
||||
IndexedAdditions {
|
||||
graph_additions: Additions {
|
||||
tx: txs.into(),
|
||||
..Default::default()
|
||||
},
|
||||
index_additions: DerivationAdditions([((), 9_u32)].into()),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_owned_txouts() {
|
||||
let mut local_chain = local_chain![
|
||||
(0, h!("Block 0")),
|
||||
(1, h!("Block 1")),
|
||||
(2, h!("Block 2")),
|
||||
(3, h!("Block 3"))
|
||||
];
|
||||
|
||||
let desc_1 : &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)";
|
||||
let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), desc_1).unwrap();
|
||||
let desc_2 : &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)";
|
||||
let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), desc_2).unwrap();
|
||||
|
||||
let mut graph = IndexedTxGraph::<BlockId, KeychainTxOutIndex<String>>::default();
|
||||
|
||||
graph.index.add_keychain("keychain_1".into(), desc_1);
|
||||
graph.index.add_keychain("keychain_2".into(), desc_2);
|
||||
|
||||
graph.index.set_lookahead_for_all(10);
|
||||
|
||||
let mut trusted_spks = Vec::new();
|
||||
let mut untrusted_spks = Vec::new();
|
||||
|
||||
{
|
||||
for _ in 0..10 {
|
||||
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_1".to_string());
|
||||
trusted_spks.push(script.clone());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
for _ in 0..10 {
|
||||
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_2".to_string());
|
||||
untrusted_spks.push(script.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let trust_predicate = |spk: &Script| trusted_spks.contains(spk);
|
||||
|
||||
// tx1 is coinbase transaction received at trusted keychain at block 0.
|
||||
let tx1 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::null(),
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 70000,
|
||||
script_pubkey: trusted_spks[0].clone(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx2 is an incoming transaction received at untrusted keychain at block 1.
|
||||
let tx2 = Transaction {
|
||||
output: vec![TxOut {
|
||||
value: 30000,
|
||||
script_pubkey: untrusted_spks[0].clone(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx3 spends tx2 and gives a change back in trusted keychain. Confirmed at Block 2.
|
||||
let tx3 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx2.txid(), 0),
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 10000,
|
||||
script_pubkey: trusted_spks[1].clone(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx4 is an external transaction receiving at untrusted keychain, unconfirmed.
|
||||
let tx4 = Transaction {
|
||||
output: vec![TxOut {
|
||||
value: 20000,
|
||||
script_pubkey: untrusted_spks[1].clone(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
|
||||
let tx5 = Transaction {
|
||||
output: vec![TxOut {
|
||||
value: 15000,
|
||||
script_pubkey: trusted_spks[2].clone(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx6 is an unrelated transaction confirmed at 3.
|
||||
let tx6 = common::new_tx(0);
|
||||
|
||||
let _ = graph.insert_relevant_txs(
|
||||
[&tx1, &tx2, &tx3, &tx6]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, tx)| (*tx, [local_chain.get_block(i as u32).unwrap()])),
|
||||
None,
|
||||
);
|
||||
|
||||
let _ = graph.insert_relevant_txs([&tx4, &tx5].iter().map(|tx| (*tx, None)), Some(100));
|
||||
|
||||
// AT Block 0
|
||||
{
|
||||
let txouts = graph
|
||||
.list_owned_txouts(&local_chain, local_chain.get_block(0).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let utxos = graph
|
||||
.list_owned_unspents(&local_chain, local_chain.get_block(0).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let balance = graph.balance(
|
||||
&local_chain,
|
||||
local_chain.get_block(0).unwrap(),
|
||||
trust_predicate,
|
||||
);
|
||||
|
||||
let confirmed_txouts_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let unconfirmed_txout_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let confirmed_utxos_txid = utxos
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let unconfirmed_utxos_txid = utxos
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
assert_eq!(txouts.len(), 5);
|
||||
assert_eq!(utxos.len(), 4);
|
||||
|
||||
assert_eq!(confirmed_txouts_txid, [tx1.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_txout_txid,
|
||||
[tx2.txid(), tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_utxos_txid,
|
||||
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 25000, // tx3 + tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 0 // Nothing is confirmed yet
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// AT Block 1
|
||||
{
|
||||
let txouts = graph
|
||||
.list_owned_txouts(&local_chain, local_chain.get_block(1).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let utxos = graph
|
||||
.list_owned_unspents(&local_chain, local_chain.get_block(1).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let balance = graph.balance(
|
||||
&local_chain,
|
||||
local_chain.get_block(1).unwrap(),
|
||||
trust_predicate,
|
||||
);
|
||||
|
||||
let confirmed_txouts_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let unconfirmed_txout_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let confirmed_utxos_txid = utxos
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let unconfirmed_utxos_txid = utxos
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
assert_eq!(txouts.len(), 5);
|
||||
assert_eq!(utxos.len(), 4);
|
||||
|
||||
// tx2 gets into confirmed txout set
|
||||
assert_eq!(confirmed_txouts_txid, [tx1.txid(), tx2.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_txout_txid,
|
||||
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
// tx2 doesn't get into confirmed utxos set
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_utxos_txid,
|
||||
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
// Balance breakup remains same
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 25000, // tx3 + tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 0 // Nothing is confirmed yet
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// AT Block 2
|
||||
{
|
||||
let txouts = graph
|
||||
.list_owned_txouts(&local_chain, local_chain.get_block(2).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let utxos = graph
|
||||
.list_owned_unspents(&local_chain, local_chain.get_block(2).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let balance = graph.balance(
|
||||
&local_chain,
|
||||
local_chain.get_block(2).unwrap(),
|
||||
trust_predicate,
|
||||
);
|
||||
|
||||
let confirmed_txouts_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let unconfirmed_txout_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let confirmed_utxos_txid = utxos
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let unconfirmed_utxos_txid = utxos
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
assert_eq!(txouts.len(), 5);
|
||||
assert_eq!(utxos.len(), 4);
|
||||
|
||||
// tx3 now gets into the confirmed txout set
|
||||
assert_eq!(
|
||||
confirmed_txouts_txid,
|
||||
[tx1.txid(), tx2.txid(), tx3.txid()].into()
|
||||
);
|
||||
assert_eq!(unconfirmed_txout_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
// tx3 also gets into confirmed utxo set
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
|
||||
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 10000 // tx3 got confirmed
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// AT Block 110
|
||||
{
|
||||
let mut local_chain_extension = (4..150)
|
||||
.map(|i| (i as u32, h!("random")))
|
||||
.collect::<BTreeMap<u32, BlockHash>>();
|
||||
|
||||
local_chain_extension.insert(3, h!("Block 3"));
|
||||
|
||||
local_chain
|
||||
.apply_update(local_chain_extension.into())
|
||||
.unwrap();
|
||||
|
||||
let txouts = graph
|
||||
.list_owned_txouts(&local_chain, local_chain.get_block(110).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let utxos = graph
|
||||
.list_owned_unspents(&local_chain, local_chain.get_block(110).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let balance = graph.balance(
|
||||
&local_chain,
|
||||
local_chain.get_block(110).unwrap(),
|
||||
trust_predicate,
|
||||
);
|
||||
|
||||
let confirmed_txouts_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let unconfirmed_txout_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let confirmed_utxos_txid = utxos
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let unconfirmed_utxos_txid = utxos
|
||||
.iter()
|
||||
.filter_map(|full_txout| {
|
||||
if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
println!("TxOuts : {:#?}", txouts);
|
||||
println!("UTXOS {:#?}", utxos);
|
||||
println!("{:#?}", balance);
|
||||
|
||||
assert_eq!(txouts.len(), 5);
|
||||
assert_eq!(utxos.len(), 4);
|
||||
|
||||
assert_eq!(
|
||||
confirmed_txouts_txid,
|
||||
[tx1.txid(), tx2.txid(), tx3.txid()].into()
|
||||
);
|
||||
assert_eq!(unconfirmed_txout_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
|
||||
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 0, // immature coinbase
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 80000 // tx1 got matured
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use bdk_chain::{
|
||||
keychain::{Balance, KeychainTracker},
|
||||
miniscript::{
|
||||
@ -40,7 +41,7 @@ fn test_insert_tx() {
|
||||
.chain_graph()
|
||||
.transactions_in_chain()
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(&ConfirmationTime::Unconfirmed, &tx)]
|
||||
vec![(&ConfirmationTime::Unconfirmed, &tx,)]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@ -66,7 +67,7 @@ fn test_balance() {
|
||||
One,
|
||||
Two,
|
||||
}
|
||||
let mut tracker = KeychainTracker::<Keychain, TxHeight>::default();
|
||||
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);
|
||||
|
@ -293,7 +293,6 @@ fn test_wildcard_derivations() {
|
||||
let _ = txout_index.reveal_to_target(&TestKeychain::External, 25);
|
||||
|
||||
(0..=15)
|
||||
.into_iter()
|
||||
.chain(vec![17, 20, 23].into_iter())
|
||||
.for_each(|index| assert!(txout_index.mark_used(&TestKeychain::External, index)));
|
||||
|
||||
@ -310,7 +309,7 @@ fn test_wildcard_derivations() {
|
||||
|
||||
// - Use all the derived till 26.
|
||||
// - next_unused() = ((27, <spk>), DerivationAdditions)
|
||||
(0..=26).into_iter().for_each(|index| {
|
||||
(0..=26).for_each(|index| {
|
||||
txout_index.mark_used(&TestKeychain::External, index);
|
||||
});
|
||||
|
||||
|
167
crates/chain/tests/test_local_chain.rs
Normal file
167
crates/chain/tests/test_local_chain.rs
Normal file
@ -0,0 +1,167 @@
|
||||
use bdk_chain::local_chain::{LocalChain, UpdateNotConnectedError};
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn add_first_tip() {
|
||||
let chain = LocalChain::default();
|
||||
assert_eq!(
|
||||
chain.determine_changeset(&local_chain![(0, h!("A"))]),
|
||||
Ok([(0, Some(h!("A")))].into()),
|
||||
"add first tip"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_second_tip() {
|
||||
let chain = local_chain![(0, h!("A"))];
|
||||
assert_eq!(
|
||||
chain.determine_changeset(&local_chain![(0, h!("A")), (1, h!("B"))]),
|
||||
Ok([(1, Some(h!("B")))].into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_disjoint_chains_cannot_merge() {
|
||||
let chain1 = local_chain![(0, h!("A"))];
|
||||
let chain2 = local_chain![(1, h!("B"))];
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Err(UpdateNotConnectedError(0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_chains_should_merge() {
|
||||
let chain1 = local_chain![(0, h!("A"))];
|
||||
let chain2 = local_chain![(0, h!("A"))];
|
||||
assert_eq!(chain1.determine_changeset(&chain2), Ok(Default::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_introduce_older_checkpoints() {
|
||||
let chain1 = local_chain![(2, h!("C")), (3, h!("D"))];
|
||||
let chain2 = local_chain![(1, h!("B")), (2, h!("C"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Ok([(1, Some(h!("B")))].into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fix_blockhash_before_agreement_point() {
|
||||
let chain1 = local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))];
|
||||
let chain2 = local_chain![(0, h!("fix")), (1, h!("we-agree"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Ok([(0, Some(h!("fix")))].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 = local_chain![(1, h!("B")), (2, h!("C"))];
|
||||
let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Ok([(0, Some(h!("A"))), (3, Some(h!("D")))].into()),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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 = local_chain![(1, h!("B")), (2, h!("C"))];
|
||||
let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Err(UpdateNotConnectedError(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.
|
||||
#[test]
|
||||
fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation() {
|
||||
let chain1 = local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))];
|
||||
let chain2 = local_chain![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Ok([
|
||||
(2, Some(h!("B'"))),
|
||||
(3, Some(h!("C'"))),
|
||||
(4, Some(h!("D"))),
|
||||
(5, None),
|
||||
]
|
||||
.into())
|
||||
);
|
||||
}
|
||||
|
||||
/// 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 = local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))];
|
||||
let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Ok([
|
||||
(1, Some(h!("B'"))),
|
||||
(2, Some(h!("C'"))),
|
||||
(3, Some(h!("D"))),
|
||||
(4, None)
|
||||
]
|
||||
.into())
|
||||
)
|
||||
}
|
||||
|
||||
/// 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 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))];
|
||||
let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Err(UpdateNotConnectedError(0))
|
||||
)
|
||||
}
|
@ -2,13 +2,19 @@
|
||||
mod common;
|
||||
use bdk_chain::{
|
||||
collections::*,
|
||||
local_chain::LocalChain,
|
||||
tx_graph::{Additions, TxGraph},
|
||||
Append, BlockId, ObservedAs,
|
||||
};
|
||||
use bitcoin::{
|
||||
hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid,
|
||||
};
|
||||
use bitcoin::{hashes::Hash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid};
|
||||
use core::iter;
|
||||
use std::vec;
|
||||
|
||||
#[test]
|
||||
fn insert_txouts() {
|
||||
// 2 (Outpoint, TxOut) tupples that denotes original data in the graph, as partial transactions.
|
||||
let original_ops = [
|
||||
(
|
||||
OutPoint::new(h!("tx1"), 1),
|
||||
@ -26,6 +32,7 @@ fn insert_txouts() {
|
||||
),
|
||||
];
|
||||
|
||||
// Another (OutPoint, TxOut) tupple to be used as update as partial transaction.
|
||||
let update_ops = [(
|
||||
OutPoint::new(h!("tx2"), 0),
|
||||
TxOut {
|
||||
@ -34,8 +41,32 @@ fn insert_txouts() {
|
||||
},
|
||||
)];
|
||||
|
||||
// One full transaction to be included in the update
|
||||
let update_txs = Transaction {
|
||||
version: 0x01,
|
||||
lock_time: PackedLockTime(0),
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::null(),
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 30_000,
|
||||
script_pubkey: Script::new(),
|
||||
}],
|
||||
};
|
||||
|
||||
// Conf anchor used to mark the full transaction as confirmed.
|
||||
let conf_anchor = ObservedAs::Confirmed(BlockId {
|
||||
height: 100,
|
||||
hash: h!("random blockhash"),
|
||||
});
|
||||
|
||||
// Unconfirmed anchor to mark the partial transactions as unconfirmed
|
||||
let unconf_anchor = ObservedAs::<BlockId>::Unconfirmed(1000000);
|
||||
|
||||
// Make the original graph
|
||||
let mut graph = {
|
||||
let mut graph = TxGraph::default();
|
||||
let mut graph = TxGraph::<ObservedAs<BlockId>>::default();
|
||||
for (outpoint, txout) in &original_ops {
|
||||
assert_eq!(
|
||||
graph.insert_txout(*outpoint, txout.clone()),
|
||||
@ -48,9 +79,11 @@ fn insert_txouts() {
|
||||
graph
|
||||
};
|
||||
|
||||
// Make the update graph
|
||||
let update = {
|
||||
let mut graph = TxGraph::default();
|
||||
for (outpoint, txout) in &update_ops {
|
||||
// Insert partials transactions
|
||||
assert_eq!(
|
||||
graph.insert_txout(*outpoint, txout.clone()),
|
||||
Additions {
|
||||
@ -58,24 +91,101 @@ fn insert_txouts() {
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
// Mark them unconfirmed.
|
||||
assert_eq!(
|
||||
graph.insert_anchor(outpoint.txid, unconf_anchor),
|
||||
Additions {
|
||||
tx: [].into(),
|
||||
txout: [].into(),
|
||||
anchors: [(unconf_anchor, outpoint.txid)].into(),
|
||||
last_seen: [].into()
|
||||
}
|
||||
);
|
||||
// Mark them last seen at.
|
||||
assert_eq!(
|
||||
graph.insert_seen_at(outpoint.txid, 1000000),
|
||||
Additions {
|
||||
tx: [].into(),
|
||||
txout: [].into(),
|
||||
anchors: [].into(),
|
||||
last_seen: [(outpoint.txid, 1000000)].into()
|
||||
}
|
||||
);
|
||||
}
|
||||
// Insert the full transaction
|
||||
assert_eq!(
|
||||
graph.insert_tx(update_txs.clone()),
|
||||
Additions {
|
||||
tx: [update_txs.clone()].into(),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
// Mark it as confirmed.
|
||||
assert_eq!(
|
||||
graph.insert_anchor(update_txs.txid(), conf_anchor),
|
||||
Additions {
|
||||
tx: [].into(),
|
||||
txout: [].into(),
|
||||
anchors: [(conf_anchor, update_txs.txid())].into(),
|
||||
last_seen: [].into()
|
||||
}
|
||||
);
|
||||
graph
|
||||
};
|
||||
|
||||
// Check the resulting addition.
|
||||
let additions = graph.determine_additions(&update);
|
||||
|
||||
assert_eq!(
|
||||
additions,
|
||||
Additions {
|
||||
tx: [].into(),
|
||||
tx: [update_txs.clone()].into(),
|
||||
txout: update_ops.into(),
|
||||
anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(),
|
||||
last_seen: [(h!("tx2"), 1000000)].into()
|
||||
}
|
||||
);
|
||||
|
||||
// Apply addition and check the new graph counts.
|
||||
graph.apply_additions(additions);
|
||||
assert_eq!(graph.all_txouts().count(), 3);
|
||||
assert_eq!(graph.full_transactions().count(), 0);
|
||||
assert_eq!(graph.partial_transactions().count(), 2);
|
||||
assert_eq!(graph.all_txouts().count(), 4);
|
||||
assert_eq!(graph.full_txs().count(), 1);
|
||||
assert_eq!(graph.floating_txouts().count(), 3);
|
||||
|
||||
// Check TxOuts are fetched correctly from the graph.
|
||||
assert_eq!(
|
||||
graph.tx_outputs(h!("tx1")).expect("should exists"),
|
||||
[
|
||||
(
|
||||
1u32,
|
||||
&TxOut {
|
||||
value: 10_000,
|
||||
script_pubkey: Script::new(),
|
||||
}
|
||||
),
|
||||
(
|
||||
2u32,
|
||||
&TxOut {
|
||||
value: 20_000,
|
||||
script_pubkey: Script::new(),
|
||||
}
|
||||
)
|
||||
]
|
||||
.into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
graph.tx_outputs(update_txs.txid()).expect("should exists"),
|
||||
[(
|
||||
0u32,
|
||||
&TxOut {
|
||||
value: 30_000,
|
||||
script_pubkey: Script::new()
|
||||
}
|
||||
)]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -90,10 +200,10 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() {
|
||||
output: vec![],
|
||||
};
|
||||
|
||||
let mut graph = TxGraph::default();
|
||||
let mut graph = TxGraph::<()>::default();
|
||||
let _ = graph.insert_tx(tx);
|
||||
assert!(graph.outspends(OutPoint::null()).is_empty());
|
||||
assert!(graph.tx_outspends(Txid::all_zeros()).next().is_none());
|
||||
assert!(graph.tx_spends(Txid::all_zeros()).next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -120,8 +230,8 @@ fn insert_tx_graph_keeps_track_of_spend() {
|
||||
output: vec![],
|
||||
};
|
||||
|
||||
let mut graph1 = TxGraph::default();
|
||||
let mut graph2 = TxGraph::default();
|
||||
let mut graph1 = TxGraph::<()>::default();
|
||||
let mut graph2 = TxGraph::<()>::default();
|
||||
|
||||
// insert in different order
|
||||
let _ = graph1.insert_tx(tx1.clone());
|
||||
@ -149,14 +259,14 @@ fn insert_tx_can_retrieve_full_tx_from_graph() {
|
||||
output: vec![TxOut::default()],
|
||||
};
|
||||
|
||||
let mut graph = TxGraph::default();
|
||||
let mut graph = TxGraph::<()>::default();
|
||||
let _ = graph.insert_tx(tx.clone());
|
||||
assert_eq!(graph.get_tx(tx.txid()), Some(&tx));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_tx_displaces_txouts() {
|
||||
let mut tx_graph = TxGraph::default();
|
||||
let mut tx_graph = TxGraph::<()>::default();
|
||||
let tx = Transaction {
|
||||
version: 0x01,
|
||||
lock_time: PackedLockTime(0),
|
||||
@ -212,7 +322,7 @@ fn insert_tx_displaces_txouts() {
|
||||
|
||||
#[test]
|
||||
fn insert_txout_does_not_displace_tx() {
|
||||
let mut tx_graph = TxGraph::default();
|
||||
let mut tx_graph = TxGraph::<()>::default();
|
||||
let tx = Transaction {
|
||||
version: 0x01,
|
||||
lock_time: PackedLockTime(0),
|
||||
@ -268,7 +378,7 @@ fn insert_txout_does_not_displace_tx() {
|
||||
|
||||
#[test]
|
||||
fn test_calculate_fee() {
|
||||
let mut graph = TxGraph::default();
|
||||
let mut graph = TxGraph::<()>::default();
|
||||
let intx1 = Transaction {
|
||||
version: 0x01,
|
||||
lock_time: PackedLockTime(0),
|
||||
@ -362,7 +472,7 @@ fn test_calculate_fee_on_coinbase() {
|
||||
output: vec![TxOut::default()],
|
||||
};
|
||||
|
||||
let graph = TxGraph::default();
|
||||
let graph = TxGraph::<()>::default();
|
||||
|
||||
assert_eq!(graph.calculate_fee(&tx), Some(0));
|
||||
}
|
||||
@ -404,7 +514,7 @@ fn test_conflicting_descendants() {
|
||||
let txid_a = tx_a.txid();
|
||||
let txid_b = tx_b.txid();
|
||||
|
||||
let mut graph = TxGraph::default();
|
||||
let mut graph = TxGraph::<()>::default();
|
||||
let _ = graph.insert_tx(tx_a);
|
||||
let _ = graph.insert_tx(tx_b);
|
||||
|
||||
@ -480,7 +590,7 @@ fn test_descendants_no_repeat() {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut graph = TxGraph::default();
|
||||
let mut graph = TxGraph::<()>::default();
|
||||
let mut expected_txids = BTreeSet::new();
|
||||
|
||||
// these are NOT descendants of `tx_a`
|
||||
@ -510,3 +620,263 @@ fn test_descendants_no_repeat() {
|
||||
}
|
||||
assert!(expected_txids.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_spends() {
|
||||
let local_chain: LocalChain = (0..=100)
|
||||
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
|
||||
.collect::<BTreeMap<u32, BlockHash>>()
|
||||
.into();
|
||||
let tip = local_chain.tip().expect("must have tip");
|
||||
|
||||
// The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx.
|
||||
// The parent tx is confirmed at block 95.
|
||||
let tx_0 = Transaction {
|
||||
input: vec![],
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: 10_000,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
TxOut {
|
||||
value: 20_000,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// The first confirmed transaction spends vout: 0. And is confirmed at block 98.
|
||||
let tx_1 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_0.txid(), 0),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: 5_000,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
TxOut {
|
||||
value: 5_000,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// The second transactions spends vout:1, and is unconfirmed.
|
||||
let tx_2 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_0.txid(), 1),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: 10_000,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
TxOut {
|
||||
value: 10_000,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
let mut graph = TxGraph::<BlockId>::default();
|
||||
|
||||
let _ = graph.insert_tx(tx_0.clone());
|
||||
let _ = graph.insert_tx(tx_1.clone());
|
||||
let _ = graph.insert_tx(tx_2.clone());
|
||||
|
||||
[95, 98]
|
||||
.iter()
|
||||
.zip([&tx_0, &tx_1].into_iter())
|
||||
.for_each(|(ht, tx)| {
|
||||
let block_id = local_chain.get_block(*ht).expect("block expected");
|
||||
let _ = graph.insert_anchor(tx.txid(), block_id);
|
||||
});
|
||||
|
||||
// Assert that confirmed spends are returned correctly.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 0))
|
||||
.unwrap(),
|
||||
(
|
||||
ObservedAs::Confirmed(&local_chain.get_block(98).expect("block expected")),
|
||||
tx_1.txid()
|
||||
)
|
||||
);
|
||||
|
||||
// Check if chain position is returned correctly.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_position(&local_chain, tip, tx_0.txid())
|
||||
.expect("position expected"),
|
||||
ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))
|
||||
);
|
||||
|
||||
// As long the unconfirmed tx isn't marked as seen, chain_spend will return None.
|
||||
assert!(graph
|
||||
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
|
||||
.is_none());
|
||||
|
||||
// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
|
||||
let _ = graph.insert_seen_at(tx_2.txid(), 1234567);
|
||||
|
||||
// Check chain spend returned correctly.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
|
||||
.unwrap(),
|
||||
(ObservedAs::Unconfirmed(1234567), tx_2.txid())
|
||||
);
|
||||
|
||||
// A conflicting transaction that conflicts with tx_1.
|
||||
let tx_1_conflict = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_0.txid(), 0),
|
||||
..Default::default()
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
let _ = graph.insert_tx(tx_1_conflict.clone());
|
||||
|
||||
// Because this tx conflicts with an already confirmed transaction, chain position should return none.
|
||||
assert!(graph
|
||||
.get_chain_position(&local_chain, tip, tx_1_conflict.txid())
|
||||
.is_none());
|
||||
|
||||
// Another conflicting tx that conflicts with tx_2.
|
||||
let tx_2_conflict = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_0.txid(), 1),
|
||||
..Default::default()
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// Insert in graph and mark it as seen.
|
||||
let _ = graph.insert_tx(tx_2_conflict.clone());
|
||||
let _ = graph.insert_seen_at(tx_2_conflict.txid(), 1234568);
|
||||
|
||||
// This should return a valid observation with correct last seen.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_position(&local_chain, tip, tx_2_conflict.txid())
|
||||
.expect("position expected"),
|
||||
ObservedAs::Unconfirmed(1234568)
|
||||
);
|
||||
|
||||
// Chain_spend now catches the new transaction as the spend.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
|
||||
.expect("expect observation"),
|
||||
(ObservedAs::Unconfirmed(1234568), tx_2_conflict.txid())
|
||||
);
|
||||
|
||||
// Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict`
|
||||
assert!(graph
|
||||
.get_chain_position(&local_chain, tip, tx_2.txid())
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relevant_heights() {
|
||||
let mut graph = TxGraph::<BlockId>::default();
|
||||
|
||||
let tx1 = common::new_tx(1);
|
||||
let tx2 = common::new_tx(2);
|
||||
|
||||
let _ = graph.insert_tx(tx1.clone());
|
||||
assert_eq!(
|
||||
graph.relevant_heights().collect::<Vec<_>>(),
|
||||
vec![],
|
||||
"no anchors in graph"
|
||||
);
|
||||
|
||||
let _ = graph.insert_anchor(
|
||||
tx1.txid(),
|
||||
BlockId {
|
||||
height: 3,
|
||||
hash: h!("3a"),
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
graph.relevant_heights().collect::<Vec<_>>(),
|
||||
vec![3],
|
||||
"one anchor at height 3"
|
||||
);
|
||||
|
||||
let _ = graph.insert_anchor(
|
||||
tx1.txid(),
|
||||
BlockId {
|
||||
height: 3,
|
||||
hash: h!("3b"),
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
graph.relevant_heights().collect::<Vec<_>>(),
|
||||
vec![3],
|
||||
"introducing duplicate anchor at height 3, must not iterate over duplicate heights"
|
||||
);
|
||||
|
||||
let _ = graph.insert_anchor(
|
||||
tx1.txid(),
|
||||
BlockId {
|
||||
height: 4,
|
||||
hash: h!("4a"),
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
graph.relevant_heights().collect::<Vec<_>>(),
|
||||
vec![3, 4],
|
||||
"anchors in height 3 and now 4"
|
||||
);
|
||||
|
||||
let _ = graph.insert_anchor(
|
||||
tx2.txid(),
|
||||
BlockId {
|
||||
height: 5,
|
||||
hash: h!("5a"),
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
graph.relevant_heights().collect::<Vec<_>>(),
|
||||
vec![3, 4, 5],
|
||||
"anchor for non-existant tx is inserted at height 5, must still be in relevant heights",
|
||||
);
|
||||
}
|
||||
|
||||
/// Ensure that `last_seen` values only increase during [`Append::append`].
|
||||
#[test]
|
||||
fn test_additions_last_seen_append() {
|
||||
let txid: Txid = h!("test txid");
|
||||
|
||||
let test_cases: &[(Option<u64>, Option<u64>)] = &[
|
||||
(Some(5), Some(6)),
|
||||
(Some(5), Some(5)),
|
||||
(Some(6), Some(5)),
|
||||
(None, Some(5)),
|
||||
(Some(5), None),
|
||||
];
|
||||
|
||||
for (original_ls, update_ls) in test_cases {
|
||||
let mut original = Additions::<()> {
|
||||
last_seen: original_ls.map(|ls| (txid, ls)).into_iter().collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let update = Additions::<()> {
|
||||
last_seen: update_ls.map(|ls| (txid, ls)).into_iter().collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
original.append(update);
|
||||
assert_eq!(
|
||||
&original.last_seen.get(&txid).cloned(),
|
||||
Ord::max(original_ls, update_ls),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ use bdk_chain::{
|
||||
Descriptor, DescriptorPublicKey,
|
||||
},
|
||||
sparse_chain::{self, ChainPosition},
|
||||
DescriptorExt, FullTxOut,
|
||||
Append, DescriptorExt, FullTxOut,
|
||||
};
|
||||
use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
|
||||
use bdk_file_store::KeychainStore;
|
||||
|
Loading…
x
Reference in New Issue
Block a user