|
| 1 | +use core::cmp::Reverse; |
| 2 | + |
| 3 | +use crate::collections::{btree_set, hash_map, BTreeSet, HashMap, HashSet, VecDeque}; |
| 4 | +use crate::tx_graph::{CanonicalTx, TxAncestors, TxDescendants, TxNode}; |
| 5 | +use crate::{Anchor, ChainOracle, ChainPosition, TxGraph}; |
| 6 | +use alloc::sync::Arc; |
| 7 | +use alloc::vec::Vec; |
| 8 | +use bdk_core::BlockId; |
| 9 | +use bitcoin::{Transaction, Txid}; |
| 10 | + |
| 11 | +type ToProcess = btree_set::IntoIter<Reverse<(LastSeen, Txid)>>; |
| 12 | + |
| 13 | +/// Iterates over canonical txs. |
| 14 | +pub struct CanonicalIter<'g, A, C> { |
| 15 | + tx_graph: &'g TxGraph<A>, |
| 16 | + chain: &'g C, |
| 17 | + chain_tip: BlockId, |
| 18 | + |
| 19 | + to_process: ToProcess, |
| 20 | + canonical: HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>, |
| 21 | + not_canonical: HashSet<Txid>, |
| 22 | + queue: VecDeque<Txid>, |
| 23 | +} |
| 24 | + |
| 25 | +impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { |
| 26 | + /// Constructs [`CanonicalIter`]. |
| 27 | + pub fn new(tx_graph: &'g TxGraph<A>, chain: &'g C, chain_tip: BlockId) -> Self { |
| 28 | + let to_process = tx_graph |
| 29 | + .full_txs() |
| 30 | + .filter_map(|tx_node| { |
| 31 | + Some(Reverse(( |
| 32 | + tx_graph.last_seen_in(tx_node.txid)?, |
| 33 | + tx_node.txid, |
| 34 | + ))) |
| 35 | + }) |
| 36 | + .collect::<BTreeSet<_>>() |
| 37 | + .into_iter(); |
| 38 | + Self { |
| 39 | + tx_graph, |
| 40 | + chain, |
| 41 | + chain_tip, |
| 42 | + to_process, |
| 43 | + canonical: HashMap::default(), |
| 44 | + not_canonical: HashSet::default(), |
| 45 | + queue: VecDeque::default(), |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + fn canonicalize_by_traversing_backwards( |
| 50 | + &mut self, |
| 51 | + txid: Txid, |
| 52 | + last_seen: Option<LastSeen>, |
| 53 | + ) -> Result<(), C::Error> { |
| 54 | + type TxWithId = (Txid, Arc<Transaction>); |
| 55 | + let tx = match self.tx_graph.get_tx(txid) { |
| 56 | + Some(tx) => tx, |
| 57 | + None => return Ok(()), |
| 58 | + }; |
| 59 | + let maybe_canonical = TxAncestors::new_include_root( |
| 60 | + self.tx_graph, |
| 61 | + tx, |
| 62 | + |_: usize, tx: Arc<Transaction>| -> Option<Result<TxWithId, C::Error>> { |
| 63 | + let txid = tx.compute_txid(); |
| 64 | + match self.is_canonical(txid, tx.is_coinbase()) { |
| 65 | + // Break when we know if something is definitely canonical or definitely not |
| 66 | + // canonical. |
| 67 | + Ok(Some(_)) => None, |
| 68 | + Ok(None) => Some(Ok((txid, tx))), |
| 69 | + Err(err) => Some(Err(err)), |
| 70 | + } |
| 71 | + }, |
| 72 | + ) |
| 73 | + .collect::<Result<Vec<_>, C::Error>>()?; |
| 74 | + |
| 75 | + // TODO: Check if this is correct. This assumes that `last_seen` values are fully |
| 76 | + // transitive. I.e. if A is an ancestor of B, then the most recent timestamp between A & B |
| 77 | + // also applies to A. |
| 78 | + let starting_txid = txid; |
| 79 | + if let Some(last_seen) = last_seen { |
| 80 | + for (txid, tx) in maybe_canonical { |
| 81 | + if !self.not_canonical.contains(&txid) { |
| 82 | + self.mark_canonical( |
| 83 | + tx, |
| 84 | + CanonicalReason::from_descendant_last_seen(starting_txid, last_seen), |
| 85 | + ); |
| 86 | + } |
| 87 | + } |
| 88 | + } |
| 89 | + Ok(()) |
| 90 | + } |
| 91 | + fn is_canonical(&mut self, txid: Txid, is_coinbase: bool) -> Result<Option<bool>, C::Error> { |
| 92 | + if self.canonical.contains_key(&txid) { |
| 93 | + return Ok(Some(true)); |
| 94 | + } |
| 95 | + if self.not_canonical.contains(&txid) { |
| 96 | + return Ok(Some(false)); |
| 97 | + } |
| 98 | + let tx = match self.tx_graph.get_tx(txid) { |
| 99 | + Some(tx) => tx, |
| 100 | + None => return Ok(None), |
| 101 | + }; |
| 102 | + for anchor in self |
| 103 | + .tx_graph |
| 104 | + .all_anchors() |
| 105 | + .get(&txid) |
| 106 | + .unwrap_or(&BTreeSet::new()) |
| 107 | + { |
| 108 | + if self |
| 109 | + .chain |
| 110 | + .is_block_in_chain(anchor.anchor_block(), self.chain_tip)? |
| 111 | + == Some(true) |
| 112 | + { |
| 113 | + self.mark_canonical(tx, CanonicalReason::from_anchor(anchor.clone())); |
| 114 | + return Ok(Some(true)); |
| 115 | + } |
| 116 | + } |
| 117 | + if is_coinbase { |
| 118 | + // Coinbase transactions cannot exist in mempool. |
| 119 | + return Ok(Some(false)); |
| 120 | + } |
| 121 | + for (_, conflicting_txid) in self.tx_graph.direct_conflicts(&tx) { |
| 122 | + if self.canonical.contains_key(&conflicting_txid) { |
| 123 | + self.mark_not_canonical(txid); |
| 124 | + return Ok(Some(false)); |
| 125 | + } |
| 126 | + } |
| 127 | + Ok(None) |
| 128 | + } |
| 129 | + |
| 130 | + fn mark_not_canonical(&mut self, txid: Txid) { |
| 131 | + TxDescendants::new_include_root(self.tx_graph, txid, |_: usize, txid: Txid| -> Option<()> { |
| 132 | + if self.not_canonical.insert(txid) { |
| 133 | + Some(()) |
| 134 | + } else { |
| 135 | + None |
| 136 | + } |
| 137 | + }) |
| 138 | + .for_each(|_| {}) |
| 139 | + } |
| 140 | + |
| 141 | + fn mark_canonical(&mut self, tx: Arc<Transaction>, reason: CanonicalReason<A>) { |
| 142 | + let starting_txid = tx.compute_txid(); |
| 143 | + if !self.insert_canonical(starting_txid, tx.clone(), reason.clone()) { |
| 144 | + return; |
| 145 | + } |
| 146 | + TxAncestors::new_exclude_root( |
| 147 | + self.tx_graph, |
| 148 | + tx, |
| 149 | + |_: usize, tx: Arc<Transaction>| -> Option<()> { |
| 150 | + let this_reason = reason.clone().with_descendant(starting_txid); |
| 151 | + if self.insert_canonical(tx.compute_txid(), tx, this_reason) { |
| 152 | + Some(()) |
| 153 | + } else { |
| 154 | + None |
| 155 | + } |
| 156 | + }, |
| 157 | + ) |
| 158 | + .for_each(|_| {}) |
| 159 | + } |
| 160 | + |
| 161 | + fn insert_canonical( |
| 162 | + &mut self, |
| 163 | + txid: Txid, |
| 164 | + tx: Arc<Transaction>, |
| 165 | + reason: CanonicalReason<A>, |
| 166 | + ) -> bool { |
| 167 | + match self.canonical.entry(txid) { |
| 168 | + hash_map::Entry::Occupied(_) => false, |
| 169 | + hash_map::Entry::Vacant(entry) => { |
| 170 | + entry.insert((tx, reason)); |
| 171 | + self.queue.push_back(txid); |
| 172 | + true |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | +} |
| 177 | + |
| 178 | +impl<'g, A: Anchor, C: ChainOracle> Iterator for CanonicalIter<'g, A, C> { |
| 179 | + type Item = Result<(Txid, Arc<Transaction>, CanonicalReason<A>), C::Error>; |
| 180 | + |
| 181 | + fn next(&mut self) -> Option<Self::Item> { |
| 182 | + loop { |
| 183 | + if let Some(txid) = self.queue.pop_front() { |
| 184 | + let (tx, reason) = self |
| 185 | + .canonical |
| 186 | + .get(&txid) |
| 187 | + .cloned() |
| 188 | + .expect("reason must exist"); |
| 189 | + return Some(Ok((txid, tx, reason))); |
| 190 | + } |
| 191 | + |
| 192 | + let Reverse((last_seen, txid)) = self.to_process.next()?; |
| 193 | + if let Err(err) = self.canonicalize_by_traversing_backwards(txid, Some(last_seen)) { |
| 194 | + return Some(Err(err)); |
| 195 | + } |
| 196 | + } |
| 197 | + } |
| 198 | +} |
| 199 | + |
| 200 | +/// Represents when and where a given transaction is last seen. |
| 201 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] |
| 202 | +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] |
| 203 | +pub enum LastSeen { |
| 204 | + /// The transaction was last seen in the mempool at the given unix timestamp. |
| 205 | + Mempool(u64), |
| 206 | + /// The transaction was last seen in a block of height. |
| 207 | + Block(u32), |
| 208 | +} |
| 209 | + |
| 210 | +/// The reason why a transaction is canonical. |
| 211 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 212 | +pub enum CanonicalReason<A> { |
| 213 | + /// This transaction is anchored in the best chain by `A`, and therefore canonical. |
| 214 | + Anchor { |
| 215 | + /// The anchor that anchored the transaction in the chain. |
| 216 | + anchor: A, |
| 217 | + /// Whether the anchor is of the transaction's descendant. |
| 218 | + descendant: Option<Txid>, |
| 219 | + }, |
| 220 | + /// This transaction does not conflict with any other transaction with a more recent `last_seen` |
| 221 | + /// value or one that is anchored in the best chain. |
| 222 | + LastSeen { |
| 223 | + /// The [`LastSeen`] value of the transaction. |
| 224 | + last_seen: LastSeen, |
| 225 | + /// Whether the [`LastSeen`] value is of the transaction's descendant. |
| 226 | + descendant: Option<Txid>, |
| 227 | + }, |
| 228 | +} |
| 229 | + |
| 230 | +impl<A> CanonicalReason<A> { |
| 231 | + /// Constructs a [`CanonicalReason`] from an `anchor`. |
| 232 | + pub fn from_anchor(anchor: A) -> Self { |
| 233 | + Self::Anchor { |
| 234 | + anchor, |
| 235 | + descendant: None, |
| 236 | + } |
| 237 | + } |
| 238 | + |
| 239 | + /// Constructs a [`CanonicalReason`] from a `descendant`'s `anchor`. |
| 240 | + pub fn from_descendant_anchor(descendant: Txid, anchor: A) -> Self { |
| 241 | + Self::Anchor { |
| 242 | + anchor, |
| 243 | + descendant: Some(descendant), |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + /// Constructs a [`CanonicalReason`] from a `last_seen` value. |
| 248 | + pub fn from_last_seen(last_seen: LastSeen) -> Self { |
| 249 | + Self::LastSeen { |
| 250 | + last_seen, |
| 251 | + descendant: None, |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | + /// Constructs a [`CanonicalReason`] from a `descendant`'s `last_seen` value. |
| 256 | + pub fn from_descendant_last_seen(descendant: Txid, last_seen: LastSeen) -> Self { |
| 257 | + Self::LastSeen { |
| 258 | + last_seen, |
| 259 | + descendant: Some(descendant), |
| 260 | + } |
| 261 | + } |
| 262 | + |
| 263 | + /// Adds a `descendant` to the [`CanonicalReason`]. |
| 264 | + /// |
| 265 | + /// This signals that either the [`LastSeen`] or [`Anchor`] value belongs to the transaction's |
| 266 | + /// descendant. |
| 267 | + #[must_use] |
| 268 | + pub fn with_descendant(self, descendant: Txid) -> Self { |
| 269 | + match self { |
| 270 | + CanonicalReason::Anchor { anchor, .. } => Self::Anchor { |
| 271 | + anchor, |
| 272 | + descendant: Some(descendant), |
| 273 | + }, |
| 274 | + CanonicalReason::LastSeen { last_seen, .. } => Self::LastSeen { |
| 275 | + last_seen, |
| 276 | + descendant: Some(descendant), |
| 277 | + }, |
| 278 | + } |
| 279 | + } |
| 280 | + |
| 281 | + /// This signals that either the [`LastSeen`] or [`Anchor`] value belongs to the transaction's |
| 282 | + /// descendant. |
| 283 | + pub fn descendant(&self) -> &Option<Txid> { |
| 284 | + match self { |
| 285 | + CanonicalReason::Anchor { descendant, .. } => descendant, |
| 286 | + CanonicalReason::LastSeen { descendant, .. } => descendant, |
| 287 | + } |
| 288 | + } |
| 289 | +} |
| 290 | + |
| 291 | +/// Helper to create canonical tx. |
| 292 | +pub fn make_canonical_tx<'a, A: Anchor, C: ChainOracle>( |
| 293 | + chain: &C, |
| 294 | + chain_tip: BlockId, |
| 295 | + tx_node: TxNode<'a, Arc<Transaction>, A>, |
| 296 | + canonical_reason: CanonicalReason<A>, |
| 297 | +) -> Result<CanonicalTx<'a, Arc<Transaction>, A>, C::Error> { |
| 298 | + let chain_position = match canonical_reason { |
| 299 | + CanonicalReason::Anchor { anchor, descendant } => match descendant { |
| 300 | + Some(desc_txid) => { |
| 301 | + let direct_anchor = tx_node |
| 302 | + .anchors |
| 303 | + .iter() |
| 304 | + .find_map(|a| -> Option<Result<A, C::Error>> { |
| 305 | + match chain.is_block_in_chain(a.anchor_block(), chain_tip) { |
| 306 | + Ok(Some(true)) => Some(Ok(a.clone())), |
| 307 | + Ok(Some(false)) | Ok(None) => None, |
| 308 | + Err(err) => Some(Err(err)), |
| 309 | + } |
| 310 | + }) |
| 311 | + .transpose()?; |
| 312 | + match direct_anchor { |
| 313 | + Some(anchor) => ChainPosition::Confirmed(anchor), |
| 314 | + None => ChainPosition::ConfirmedByTransitivity(desc_txid, anchor), |
| 315 | + } |
| 316 | + } |
| 317 | + None => ChainPosition::Confirmed(anchor), |
| 318 | + }, |
| 319 | + CanonicalReason::LastSeen { last_seen, .. } => match last_seen { |
| 320 | + LastSeen::Mempool(last_seen) => ChainPosition::Unconfirmed(last_seen), |
| 321 | + LastSeen::Block(_) => ChainPosition::UnconfirmedAndNotSeen, |
| 322 | + }, |
| 323 | + }; |
| 324 | + Ok(CanonicalTx { |
| 325 | + chain_position, |
| 326 | + tx_node, |
| 327 | + }) |
| 328 | +} |
0 commit comments