-
Notifications
You must be signed in to change notification settings - Fork 311
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3bdb612
commit 6abc6d0
Showing
13 changed files
with
574 additions
and
291 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
} |
Oops, something went wrong.