Skip to content

Commit

Permalink
feat(chain)!: rm get_chain_position and associated methods
Browse files Browse the repository at this point in the history
  • Loading branch information
evanlinjin committed Nov 19, 2024
1 parent 1c2fabc commit 8c34407
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 327 deletions.
211 changes: 0 additions & 211 deletions crates/chain/src/tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -739,217 +739,6 @@ impl<A: Clone + Ord> TxGraph<A> {
}

impl<A: Anchor> TxGraph<A> {
/// Get the position of the transaction in `chain` with tip `chain_tip`.
///
/// Chain data is fetched from `chain`, a [`ChainOracle`] implementation.
///
/// This method returns `Ok(None)` if the transaction is not found in the chain, and no longer
/// belongs in the mempool. The following factors are used to approximate whether an
/// unconfirmed transaction exists in the mempool (not evicted):
///
/// 1. Unconfirmed transactions that conflict with confirmed transactions are evicted.
/// 2. Unconfirmed transactions that spend from transactions that are evicted, are also
/// evicted.
/// 3. Given two conflicting unconfirmed transactions, the transaction with the lower
/// `last_seen_unconfirmed` parameter is evicted. A transaction's `last_seen_unconfirmed`
/// parameter is the max of all it's descendants' `last_seen_unconfirmed` parameters. If the
/// final `last_seen_unconfirmed`s are the same, the transaction with the lower `txid` (by
/// lexicographical order) is evicted.
///
/// # Error
///
/// An error will occur if the [`ChainOracle`] implementation (`chain`) fails. If the
/// [`ChainOracle`] is infallible, [`get_chain_position`] can be used instead.
///
/// [`get_chain_position`]: Self::get_chain_position
pub fn try_get_chain_position<C: ChainOracle>(
&self,
chain: &C,
chain_tip: BlockId,
txid: Txid,
) -> Result<Option<ChainPosition<&A>>, C::Error> {
let tx_node = match self.txs.get(&txid) {
Some(v) => v,
None => return Ok(None),
};

for anchor in self.anchors.get(&txid).unwrap_or(&self.empty_anchors) {
match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? {
Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))),
_ => continue,
}
}

// If no anchors are in best chain and we don't have a last_seen, we can return
// early because by definition the tx doesn't have a chain position.
let last_seen = match self.last_seen.get(&txid) {
Some(t) => *t,
None => return Ok(None),
};

// The tx is not anchored to a block in the best chain, which means that it
// might be in mempool, or it might have been dropped already.
// Let's check conflicts to find out!
let tx = match tx_node {
TxNodeInternal::Whole(tx) => {
// A coinbase tx that is not anchored in the best chain cannot be unconfirmed and
// should always be filtered out.
if tx.is_coinbase() {
return Ok(None);
}
tx.clone()
}
TxNodeInternal::Partial(_) => {
// Partial transactions (outputs only) cannot have conflicts.
return Ok(None);
}
};

// We want to retrieve all the transactions that conflict with us, plus all the
// transactions that conflict with our unconfirmed ancestors, since they conflict with us
// as well.
// We only traverse unconfirmed ancestors since conflicts of confirmed transactions
// cannot be in the best chain.

// First of all, we retrieve all our ancestors. Since we're using `new_include_root`, the
// resulting array will also include `tx`
let unconfirmed_ancestor_txs =
TxAncestors::new_include_root(self, tx.clone(), |_, ancestor_tx: Arc<Transaction>| {
let tx_node = self.get_tx_node(ancestor_tx.as_ref().compute_txid())?;
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
// the best chain)
for block in tx_node.anchors {
match chain.is_block_in_chain(block.anchor_block(), chain_tip) {
Ok(Some(true)) => return None,
Err(e) => return Some(Err(e)),
_ => continue,
}
}
Some(Ok(tx_node))
})
.collect::<Result<Vec<_>, C::Error>>()?;

// We determine our tx's last seen, which is the max between our last seen,
// and our unconf descendants' last seen.
let unconfirmed_descendants_txs = TxDescendants::new_include_root(
self,
tx.as_ref().compute_txid(),
|_, descendant_txid: Txid| {
let tx_node = self.get_tx_node(descendant_txid)?;
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
// the best chain)
for block in tx_node.anchors {
match chain.is_block_in_chain(block.anchor_block(), chain_tip) {
Ok(Some(true)) => return None,
Err(e) => return Some(Err(e)),
_ => continue,
}
}
Some(Ok(tx_node))
},
)
.collect::<Result<Vec<_>, C::Error>>()?;

let tx_last_seen = unconfirmed_descendants_txs
.iter()
.max_by_key(|tx| tx.last_seen_unconfirmed)
.map(|tx| tx.last_seen_unconfirmed)
.expect("descendants always includes at least one transaction (the root tx");

// Now we traverse our ancestors and consider all their conflicts
for tx_node in unconfirmed_ancestor_txs {
// We retrieve all the transactions conflicting with this specific ancestor
let conflicting_txs =
self.walk_conflicts(tx_node.tx.as_ref(), |_, txid| self.get_tx_node(txid));

// If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then
// this tx cannot exist in the best chain
for conflicting_tx in conflicting_txs {
for block in conflicting_tx.anchors {
if chain.is_block_in_chain(block.anchor_block(), chain_tip)? == Some(true) {
return Ok(None);
}
}
if conflicting_tx.last_seen_unconfirmed > tx_last_seen {
return Ok(None);
}
if conflicting_tx.last_seen_unconfirmed == Some(last_seen)
&& conflicting_tx.as_ref().compute_txid() > tx.as_ref().compute_txid()
{
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
return Ok(None);
}
}
}

Ok(Some(ChainPosition::Unconfirmed(last_seen)))
}

/// Get the position of the transaction in `chain` with tip `chain_tip`.
///
/// This is the infallible version of [`try_get_chain_position`].
///
/// [`try_get_chain_position`]: Self::try_get_chain_position
pub fn get_chain_position<C: ChainOracle<Error = Infallible>>(
&self,
chain: &C,
chain_tip: BlockId,
txid: Txid,
) -> Option<ChainPosition<&A>> {
self.try_get_chain_position(chain, chain_tip, txid)
.expect("error is infallible")
}

/// Get the txid of the spending transaction and where the spending transaction is observed in
/// the `chain` of `chain_tip`.
///
/// If no in-chain transaction spends `outpoint`, `None` will be returned.
///
/// # Error
///
/// An error will occur only if the [`ChainOracle`] implementation (`chain`) fails.
///
/// If the [`ChainOracle`] is infallible, [`get_chain_spend`] can be used instead.
///
/// [`get_chain_spend`]: Self::get_chain_spend
pub fn try_get_chain_spend<C: ChainOracle>(
&self,
chain: &C,
chain_tip: BlockId,
outpoint: OutPoint,
) -> Result<Option<(ChainPosition<&A>, Txid)>, C::Error> {
if self
.try_get_chain_position(chain, chain_tip, outpoint.txid)?
.is_none()
{
return Ok(None);
}
if let Some(spends) = self.spends.get(&outpoint) {
for &txid in spends {
if let Some(observed_at) = self.try_get_chain_position(chain, chain_tip, txid)? {
return Ok(Some((observed_at, txid)));
}
}
}
Ok(None)
}

/// Get the txid of the spending transaction and where the spending transaction is observed in
/// the `chain` of `chain_tip`.
///
/// This is the infallible version of [`try_get_chain_spend`]
///
/// [`try_get_chain_spend`]: Self::try_get_chain_spend
pub fn get_chain_spend<C: ChainOracle<Error = Infallible>>(
&self,
chain: &C,
static_block: BlockId,
outpoint: OutPoint,
) -> Option<(ChainPosition<&A>, Txid)> {
self.try_get_chain_spend(chain, static_block, outpoint)
.expect("error is infallible")
}

/// List graph transactions that are in `chain` with `chain_tip`.
///
/// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is
Expand Down
21 changes: 12 additions & 9 deletions crates/chain/tests/test_indexed_tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,14 +587,17 @@ fn test_get_chain_position() {
}

// check chain position
let res = graph
let chain_pos = graph
.graph()
.get_chain_position(chain, chain.tip().block_id(), txid);
assert_eq!(
res.map(ChainPosition::cloned),
exp_pos,
"failed test case: {name}"
);
.list_canonical_txs(chain, chain.tip().block_id())
.find_map(|canon_tx| {
if canon_tx.tx_node.txid == txid {
Some(canon_tx.chain_position)
} else {
None
}
});
assert_eq!(chain_pos, exp_pos, "failed test case: {name}");
}

[
Expand Down Expand Up @@ -651,7 +654,7 @@ fn test_get_chain_position() {
exp_pos: Some(ChainPosition::Unconfirmed(2)),
},
TestCase {
name: "tx unknown anchor - no chain pos",
name: "tx unknown anchor - unconfirmed",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
Expand All @@ -661,7 +664,7 @@ fn test_get_chain_position() {
},
anchor: Some(block_id!(2, "B'")),
last_seen: None,
exp_pos: None,
exp_pos: Some(ChainPosition::UnconfirmedAndNotSeen),
},
]
.into_iter()
Expand Down
Loading

0 comments on commit 8c34407

Please sign in to comment.