Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
evanlinjin committed Nov 19, 2024
1 parent 3bdb612 commit 6abc6d0
Show file tree
Hide file tree
Showing 13 changed files with 574 additions and 291 deletions.
1 change: 1 addition & 0 deletions crates/bitcoind_rpc/tests/test_emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
trusted_pending: SEND_AMOUNT * reorg_count as u64,
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
..Balance::default()
},
Expand Down
331 changes: 331 additions & 0 deletions crates/chain/src/canonical_iter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
use core::cmp::Reverse;

use crate::collections::{btree_set, hash_map, BTreeSet, HashMap, HashSet, VecDeque};
use crate::tx_graph::{CanonicalTx, TxAncestors, TxDescendants, TxNode};
use crate::{Anchor, ChainOracle, ChainPosition, TxGraph};
use alloc::sync::Arc;
use alloc::vec::Vec;
use bdk_core::BlockId;
use bitcoin::{Transaction, Txid};

/// A set of canonical transactions.
pub type CanonicalSet<A> = HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>;

type ToProcess = btree_set::IntoIter<Reverse<(LastSeen, Txid)>>;

/// Iterates over canonical txs.
pub struct CanonicalIter<'g, A, C> {
tx_graph: &'g TxGraph<A>,
chain: &'g C,
chain_tip: BlockId,

to_process: ToProcess,
canonical: CanonicalSet<A>,
not_canonical: HashSet<Txid>,
queue: VecDeque<Txid>,
}

impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
/// Constructs [`CanonicalIter`].
pub fn new(tx_graph: &'g TxGraph<A>, chain: &'g C, chain_tip: BlockId) -> Self {
let to_process = tx_graph
.full_txs()
.filter_map(|tx_node| {
Some(Reverse((
tx_graph.last_seen_in(tx_node.txid)?,
tx_node.txid,
)))
})
.collect::<BTreeSet<_>>()
.into_iter();
Self {
tx_graph,
chain,
chain_tip,
to_process,
canonical: HashMap::default(),
not_canonical: HashSet::default(),
queue: VecDeque::default(),
}
}

fn canonicalize_by_traversing_backwards(
&mut self,
txid: Txid,
last_seen: Option<LastSeen>,
) -> Result<(), C::Error> {
type TxWithId = (Txid, Arc<Transaction>);
let tx = match self.tx_graph.get_tx(txid) {
Some(tx) => tx,
None => return Ok(()),
};
let maybe_canonical = TxAncestors::new_include_root(
self.tx_graph,
tx,
|_: usize, tx: Arc<Transaction>| -> Option<Result<TxWithId, C::Error>> {
let txid = tx.compute_txid();
match self.is_canonical(txid, tx.is_coinbase()) {
// Break when we know if something is definitely canonical or definitely not
// canonical.
Ok(Some(_)) => None,
Ok(None) => Some(Ok((txid, tx))),
Err(err) => Some(Err(err)),
}
},
)
.collect::<Result<Vec<_>, C::Error>>()?;

// TODO: Check if this is correct. This assumes that `last_seen` values are fully
// transitive. I.e. if A is an ancestor of B, then the most recent timestamp between A & B
// also applies to A.
let starting_txid = txid;
if let Some(last_seen) = last_seen {
for (txid, tx) in maybe_canonical {
if !self.not_canonical.contains(&txid) {
self.mark_canonical(
tx,
CanonicalReason::from_descendant_last_seen(starting_txid, last_seen),
);
}
}
}
Ok(())
}
fn is_canonical(&mut self, txid: Txid, is_coinbase: bool) -> Result<Option<bool>, C::Error> {
if self.canonical.contains_key(&txid) {
return Ok(Some(true));
}
if self.not_canonical.contains(&txid) {
return Ok(Some(false));
}
let tx = match self.tx_graph.get_tx(txid) {
Some(tx) => tx,
None => return Ok(None),
};
for anchor in self
.tx_graph
.all_anchors()
.get(&txid)
.unwrap_or(&BTreeSet::new())
{
if self
.chain
.is_block_in_chain(anchor.anchor_block(), self.chain_tip)?
== Some(true)
{
self.mark_canonical(tx, CanonicalReason::from_anchor(anchor.clone()));
return Ok(Some(true));
}
}
if is_coinbase {
// Coinbase transactions cannot exist in mempool.
return Ok(Some(false));
}
for (_, conflicting_txid) in self.tx_graph.direct_conflicts(&tx) {
if self.canonical.contains_key(&conflicting_txid) {
self.mark_not_canonical(txid);
return Ok(Some(false));
}
}
Ok(None)
}

fn mark_not_canonical(&mut self, txid: Txid) {
TxDescendants::new_include_root(self.tx_graph, txid, |_: usize, txid: Txid| -> Option<()> {
if self.not_canonical.insert(txid) {
Some(())
} else {
None
}
})
.for_each(|_| {})
}

fn mark_canonical(&mut self, tx: Arc<Transaction>, reason: CanonicalReason<A>) {
let starting_txid = tx.compute_txid();
if !self.insert_canonical(starting_txid, tx.clone(), reason.clone()) {
return;
}
TxAncestors::new_exclude_root(
self.tx_graph,
tx,
|_: usize, tx: Arc<Transaction>| -> Option<()> {
let this_reason = reason.clone().with_descendant(starting_txid);
if self.insert_canonical(tx.compute_txid(), tx, this_reason) {
Some(())
} else {
None
}
},
)
.for_each(|_| {})
}

fn insert_canonical(
&mut self,
txid: Txid,
tx: Arc<Transaction>,
reason: CanonicalReason<A>,
) -> bool {
match self.canonical.entry(txid) {
hash_map::Entry::Occupied(_) => false,
hash_map::Entry::Vacant(entry) => {
entry.insert((tx, reason));
self.queue.push_back(txid);
true
}
}
}
}

impl<'g, A: Anchor, C: ChainOracle> Iterator for CanonicalIter<'g, A, C> {
type Item = Result<(Txid, Arc<Transaction>, CanonicalReason<A>), C::Error>;

fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(txid) = self.queue.pop_front() {
let (tx, reason) = self
.canonical
.get(&txid)
.cloned()
.expect("reason must exist");
return Some(Ok((txid, tx, reason)));
}

let Reverse((last_seen, txid)) = self.to_process.next()?;
if let Err(err) = self.canonicalize_by_traversing_backwards(txid, Some(last_seen)) {
return Some(Err(err));
}
}
}
}

/// Represents when and where a given transaction is last seen.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum LastSeen {
/// The transaction was last seen in the mempool at the given unix timestamp.
Mempool(u64),
/// The transaction was last seen in a block of height.
Block(u32),
}

/// The reason why a transaction is canonical.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CanonicalReason<A> {
/// This transaction is anchored in the best chain by `A`, and therefore canonical.
Anchor {
/// The anchor that anchored the transaction in the chain.
anchor: A,
/// Whether the anchor is of the transaction's descendant.
descendant: Option<Txid>,
},
/// This transaction does not conflict with any other transaction with a more recent `last_seen`
/// value or one that is anchored in the best chain.
LastSeen {
/// The [`LastSeen`] value of the transaction.
last_seen: LastSeen,
/// Whether the [`LastSeen`] value is of the transaction's descendant.
descendant: Option<Txid>,
},
}

impl<A> CanonicalReason<A> {
/// Constructs a [`CanonicalReason`] from an `anchor`.
pub fn from_anchor(anchor: A) -> Self {
Self::Anchor {
anchor,
descendant: None,
}
}

/// Constructs a [`CanonicalReason`] from a `descendant`'s `anchor`.
pub fn from_descendant_anchor(descendant: Txid, anchor: A) -> Self {
Self::Anchor {
anchor,
descendant: Some(descendant),
}
}

/// Constructs a [`CanonicalReason`] from a `last_seen` value.
pub fn from_last_seen(last_seen: LastSeen) -> Self {
Self::LastSeen {
last_seen,
descendant: None,
}
}

/// Constructs a [`CanonicalReason`] from a `descendant`'s `last_seen` value.
pub fn from_descendant_last_seen(descendant: Txid, last_seen: LastSeen) -> Self {
Self::LastSeen {
last_seen,
descendant: Some(descendant),
}
}

/// Adds a `descendant` to the [`CanonicalReason`].
///
/// This signals that either the [`LastSeen`] or [`Anchor`] value belongs to the transaction's
/// descendant.
#[must_use]
pub fn with_descendant(self, descendant: Txid) -> Self {
match self {
CanonicalReason::Anchor { anchor, .. } => Self::Anchor {
anchor,
descendant: Some(descendant),
},
CanonicalReason::LastSeen { last_seen, .. } => Self::LastSeen {
last_seen,
descendant: Some(descendant),
},
}
}

/// This signals that either the [`LastSeen`] or [`Anchor`] value belongs to the transaction's
/// descendant.
pub fn descendant(&self) -> &Option<Txid> {
match self {
CanonicalReason::Anchor { descendant, .. } => descendant,
CanonicalReason::LastSeen { descendant, .. } => descendant,
}
}
}

/// Helper to create canonical tx.
pub fn make_canonical_tx<'a, A: Anchor, C: ChainOracle>(
chain: &C,
chain_tip: BlockId,
tx_node: TxNode<'a, Arc<Transaction>, A>,
canonical_reason: CanonicalReason<A>,
) -> Result<CanonicalTx<'a, Arc<Transaction>, A>, C::Error> {
let chain_position = match canonical_reason {
CanonicalReason::Anchor { anchor, descendant } => match descendant {
Some(desc_txid) => {
let direct_anchor = tx_node
.anchors
.iter()
.find_map(|a| -> Option<Result<A, C::Error>> {
match chain.is_block_in_chain(a.anchor_block(), chain_tip) {
Ok(Some(true)) => Some(Ok(a.clone())),
Ok(Some(false)) | Ok(None) => None,
Err(err) => Some(Err(err)),
}
})
.transpose()?;
match direct_anchor {
Some(anchor) => ChainPosition::Confirmed(anchor),
None => ChainPosition::ConfirmedByTransitivity(desc_txid, anchor),
}
}
None => ChainPosition::Confirmed(anchor),
},
CanonicalReason::LastSeen { last_seen, .. } => match last_seen {
LastSeen::Mempool(last_seen) => ChainPosition::Unconfirmed(last_seen),
LastSeen::Block(_) => ChainPosition::UnconfirmedAndNotSeen,
},
};
Ok(CanonicalTx {
chain_position,
tx_node,
})
}
Loading

0 comments on commit 6abc6d0

Please sign in to comment.