Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(chain)!: Remove Anchor trait and make anchors unique to (Txid, BlockId) #1515

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions crates/bitcoind_rpc/tests/test_emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use bdk_bitcoind_rpc::Emitter;
use bdk_chain::{
bitcoin::{Address, Amount, Txid},
local_chain::{CheckPoint, LocalChain},
Balance, BlockId, IndexedTxGraph, Merge, SpkTxOutIndex,
Balance, BlockId, BlockTime, IndexedTxGraph, Merge, SpkTxOutIndex,
};
use bdk_testenv::{anyhow, TestEnv};
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
Expand Down Expand Up @@ -149,7 +149,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
println!("mined blocks!");

let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
let mut indexed_tx_graph = IndexedTxGraph::new({
let mut index = SpkTxOutIndex::<usize>::default();
index.insert_spk(0, addr_0.script_pubkey());
index.insert_spk(1, addr_1.script_pubkey());
Expand Down Expand Up @@ -206,17 +206,19 @@ fn test_into_tx_graph() -> anyhow::Result<()> {

// mine a block that confirms the 3 txs
let exp_block_hash = env.mine_blocks(1, None)?[0];
let exp_block_height = env.rpc_client().get_block_info(&exp_block_hash)?.height as u32;
let exp_block = env.rpc_client().get_block_info(&exp_block_hash)?;
let exp_block_height = exp_block.height as u32;
let exp_block_time = exp_block.time as u32;
let exp_anchors = exp_txids
.iter()
.map({
let anchor = BlockId {
let blockid = BlockId {
height: exp_block_height,
hash: exp_block_hash,
};
move |&txid| (anchor, txid)
move |&txid| ((txid, blockid), BlockTime::new(exp_block_time))
})
.collect::<BTreeSet<_>>();
.collect::<BTreeMap<_, _>>();

// must receive mined block which will confirm the transactions.
{
Expand Down Expand Up @@ -277,7 +279,7 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {

fn process_block(
recv_chain: &mut LocalChain,
recv_graph: &mut IndexedTxGraph<BlockId, SpkTxOutIndex<()>>,
recv_graph: &mut IndexedTxGraph<BlockTime, SpkTxOutIndex<()>>,
block: Block,
block_height: u32,
) -> anyhow::Result<()> {
Expand All @@ -288,7 +290,7 @@ fn process_block(

fn sync_from_emitter<C>(
recv_chain: &mut LocalChain,
recv_graph: &mut IndexedTxGraph<BlockId, SpkTxOutIndex<()>>,
recv_graph: &mut IndexedTxGraph<BlockTime, SpkTxOutIndex<()>>,
emitter: &mut Emitter<C>,
) -> anyhow::Result<()>
where
Expand All @@ -303,7 +305,7 @@ where

fn get_balance(
recv_chain: &LocalChain,
recv_graph: &IndexedTxGraph<BlockId, SpkTxOutIndex<()>>,
recv_graph: &IndexedTxGraph<BlockTime, SpkTxOutIndex<()>>,
) -> anyhow::Result<Balance> {
let chain_tip = recv_chain.tip().block_id();
let outpoints = recv_graph.index.outpoints().clone();
Expand Down Expand Up @@ -341,7 +343,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {

// setup receiver
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
let mut recv_graph = IndexedTxGraph::new({
let mut recv_index = SpkTxOutIndex::default();
recv_index.insert_spk((), spk_to_track.clone());
recv_index
Expand Down
144 changes: 40 additions & 104 deletions crates/chain/src/chain_data.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,33 @@
use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid};

use crate::{Anchor, AnchorFromBlockPosition, COINBASE_MATURITY};
use crate::{Anchor, BlockTime, COINBASE_MATURITY};

/// 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 ChainPosition<A> {
/// The chain data is seen as confirmed, and in anchored by `A`.
Confirmed(A),
/// The chain data is seen as confirmed, and in anchored by `Anchor`.
Confirmed(Anchor, A),
/// The chain data is not confirmed and last seen in the mempool at this timestamp.
Unconfirmed(u64),
}

impl<A> ChainPosition<A> {
/// Returns whether [`ChainPosition`] is confirmed or not.
pub fn is_confirmed(&self) -> bool {
matches!(self, Self::Confirmed(_))
matches!(self, Self::Confirmed(_, _))
}
}

impl<A: Clone> ChainPosition<&A> {
/// Maps a [`ChainPosition<&A>`] into a [`ChainPosition<A>`] by cloning the contents.
impl<A: Clone> ChainPosition<A> {
/// Maps a [`ChainPosition`] into a [`ChainPosition`] by cloning the contents.
pub fn cloned(self) -> ChainPosition<A> {
match self {
ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()),
ChainPosition::Confirmed(anchor, a) => ChainPosition::Confirmed(anchor, a),
ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen),
}
}
}

impl<A: Anchor> ChainPosition<A> {
/// Determines the upper bound of the confirmation height.
pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
match self {
ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()),
ChainPosition::Unconfirmed(_) => None,
}
}
}

/// Block height and timestamp at which a transaction is confirmed.
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
Expand Down Expand Up @@ -74,12 +62,12 @@ impl ConfirmationTime {
}
}

impl From<ChainPosition<ConfirmationBlockTime>> for ConfirmationTime {
fn from(observed_as: ChainPosition<ConfirmationBlockTime>) -> Self {
impl From<ChainPosition<BlockTime>> for ConfirmationTime {
fn from(observed_as: ChainPosition<BlockTime>) -> Self {
match observed_as {
ChainPosition::Confirmed(a) => Self::Confirmed {
height: a.block_id.height,
time: a.confirmation_time,
ChainPosition::Confirmed((_txid, blockid), anchor_meta) => Self::Confirmed {
height: blockid.height,
time: *anchor_meta.as_ref() as u64,
},
ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen },
}
Expand All @@ -103,18 +91,6 @@ pub struct BlockId {
pub hash: BlockHash,
}

impl Anchor for BlockId {
fn anchor_block(&self) -> Self {
*self
}
}

impl AnchorFromBlockPosition for BlockId {
fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
block_id
}
}

impl Default for BlockId {
fn default() -> Self {
Self {
Expand Down Expand Up @@ -145,41 +121,6 @@ impl From<(&u32, &BlockHash)> for BlockId {
}
}

/// An [`Anchor`] implementation that also records the exact confirmation time of the transaction.
///
/// Refer to [`Anchor`] for more details.
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub struct ConfirmationBlockTime {
/// The anchor block.
pub block_id: BlockId,
/// The confirmation time of the transaction being anchored.
pub confirmation_time: u64,
}

impl Anchor for ConfirmationBlockTime {
fn anchor_block(&self) -> BlockId {
self.block_id
}

fn confirmation_height_upper_bound(&self) -> u32 {
self.block_id.height
}
}

impl AnchorFromBlockPosition for ConfirmationBlockTime {
fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
Self {
block_id,
confirmation_time: block.header.time as _,
}
}
}

/// A `TxOut` with as much data as we can retrieve about it
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FullTxOut<A> {
Expand All @@ -195,18 +136,12 @@ pub struct FullTxOut<A> {
pub is_on_coinbase: bool,
}

impl<A: Anchor> FullTxOut<A> {
impl<A> FullTxOut<A> {
/// Whether the `txout` is considered mature.
///
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
/// method may return false-negatives. In other words, interpreted confirmation count may be
/// less than the actual value.
///
/// [`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 {
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
ChainPosition::Confirmed((_, blockid), _) => blockid.height,
ChainPosition::Unconfirmed(_) => {
debug_assert!(false, "coinbase tx can never be unconfirmed");
return false;
Expand All @@ -224,28 +159,22 @@ impl<A: Anchor> FullTxOut<A> {
/// Whether the utxo is/was/will be spendable with chain `tip`.
///
/// This method does not take into account the lock time.
///
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
/// method may return false-negatives. In other words, interpreted confirmation count may be
/// less than the actual value.
///
/// [`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 {
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
ChainPosition::Confirmed((_, blockid), _) => blockid.height,
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((ChainPosition::Confirmed(spending_anchor), _)) = &self.spent_by {
if spending_anchor.anchor_block().height <= tip {
if let Some((ChainPosition::Confirmed((_, spending_blockid), _), _)) = &self.spent_by {
if spending_blockid.height <= tip {
return false;
}
}
Expand All @@ -257,25 +186,32 @@ impl<A: Anchor> FullTxOut<A> {
#[cfg(test)]
mod test {
use super::*;
use crate::BlockTime;

#[test]
fn chain_position_ord() {
let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(10);
let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(20);
let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime {
confirmation_time: 20,
block_id: BlockId {
height: 9,
..Default::default()
},
});
let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime {
confirmation_time: 15,
block_id: BlockId {
height: 12,
..Default::default()
},
});
let unconf1 = ChainPosition::Unconfirmed(10);
let unconf2 = ChainPosition::Unconfirmed(20);
let conf1 = ChainPosition::Confirmed(
(
Txid::all_zeros(),
BlockId {
height: 9,
..Default::default()
},
),
BlockTime::new(20),
);
let conf2 = ChainPosition::Confirmed(
(
Txid::all_zeros(),
BlockId {
height: 12,
..Default::default()
},
),
BlockTime::new(15),
);

assert!(unconf2 > unconf1, "higher last_seen means higher ord");
assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");
Expand Down
5 changes: 4 additions & 1 deletion crates/chain/src/changeset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ impl<K, A> core::default::Default for CombinedChangeSet<K, A> {
}

#[cfg(feature = "miniscript")]
impl<K: Ord, A: crate::Anchor> crate::Merge for CombinedChangeSet<K, A> {
impl<K: Ord, A> crate::Merge for CombinedChangeSet<K, A>
where
A: Ord + Clone,
{
fn merge(&mut self, other: Self) {
crate::Merge::merge(&mut self.chain, other.chain);
crate::Merge::merge(&mut self.indexed_tx_graph, other.indexed_tx_graph);
Expand Down
Loading
Loading