Skip to content

Commit fe09eab

Browse files
committed
feat(chain)!: Change TxGraph to understand missing-at & last-missing
The missing-at and last-missing timestamp informs the `TxGraph` when the transaction was last deemed as missing from the mempool (evicted). The canonicalization algorithm is changed to disregard transactions with a last-missing timestamp greater or equal to it's last-seen timestamp. The exception is when we have a canonical descendant due to rules of transitivity.
1 parent 210725a commit fe09eab

File tree

2 files changed

+63
-5
lines changed

2 files changed

+63
-5
lines changed

crates/chain/src/tx_graph.rs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ pub struct TxGraph<A = ConfirmationBlockTime> {
145145
spends: BTreeMap<OutPoint, HashSet<Txid>>,
146146
anchors: HashMap<Txid, BTreeSet<A>>,
147147
last_seen: HashMap<Txid, u64>,
148+
last_missing: HashMap<Txid, u64>,
148149

149150
txs_by_highest_conf_heights: BTreeSet<(u32, Txid)>,
150151
txs_by_last_seen: BTreeSet<(u64, Txid)>,
@@ -162,6 +163,7 @@ impl<A> Default for TxGraph<A> {
162163
spends: Default::default(),
163164
anchors: Default::default(),
164165
last_seen: Default::default(),
166+
last_missing: Default::default(),
165167
txs_by_highest_conf_heights: Default::default(),
166168
txs_by_last_seen: Default::default(),
167169
empty_outspends: Default::default(),
@@ -715,6 +717,30 @@ impl<A: Anchor> TxGraph<A> {
715717
changeset
716718
}
717719

720+
/// Inserts the given `missing_at` for `txid`
721+
pub fn insert_missing_at(&mut self, txid: Txid, missing_at: u64) -> ChangeSet<A> {
722+
let is_changed = match self.last_missing.entry(txid) {
723+
hash_map::Entry::Occupied(mut e) => {
724+
let last_missing = e.get_mut();
725+
let change = *last_missing < missing_at;
726+
if change {
727+
*last_missing = missing_at;
728+
}
729+
change
730+
}
731+
hash_map::Entry::Vacant(e) => {
732+
e.insert(missing_at);
733+
true
734+
}
735+
};
736+
737+
let mut changeset = ChangeSet::<A>::default();
738+
if is_changed {
739+
changeset.last_missing.insert(txid, missing_at);
740+
}
741+
changeset
742+
}
743+
718744
/// Extends this graph with the given `update`.
719745
///
720746
/// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that
@@ -765,6 +791,14 @@ impl<A: Anchor> TxGraph<A> {
765791
changeset.merge(self.insert_seen_at(txid, seen_at));
766792
}
767793
}
794+
for txid in update.missing {
795+
// We want the `missing_at` value to override the `last_seen` value of the transaction.
796+
// If there is no `last_seen`, there is no need for the `missing_at` value since the
797+
// transaction will not be canonical anyway.
798+
if let Some(&missing_at) = self.last_seen.get(&txid) {
799+
changeset.merge(self.insert_missing_at(txid, missing_at));
800+
}
801+
}
768802
changeset
769803
}
770804

@@ -782,6 +816,7 @@ impl<A: Anchor> TxGraph<A> {
782816
.flat_map(|(txid, anchors)| anchors.iter().map(|a| (a.clone(), *txid)))
783817
.collect(),
784818
last_seen: self.last_seen.iter().map(|(&k, &v)| (k, v)).collect(),
819+
last_missing: self.last_missing.iter().map(|(&k, &v)| (k, v)).collect(),
785820
}
786821
}
787822

@@ -799,6 +834,9 @@ impl<A: Anchor> TxGraph<A> {
799834
for (txid, seen_at) in changeset.last_seen {
800835
let _ = self.insert_seen_at(txid, seen_at);
801836
}
837+
for (txid, missing_at) in changeset.last_missing {
838+
let _ = self.insert_missing_at(txid, missing_at);
839+
}
802840
}
803841
}
804842

@@ -969,9 +1007,14 @@ impl<A: Anchor> TxGraph<A> {
9691007

9701008
/// List txids by descending last-seen order.
9711009
///
972-
/// Transactions without last-seens are excluded.
973-
pub fn txids_by_descending_last_seen(&self) -> impl ExactSizeIterator<Item = (u64, Txid)> + '_ {
974-
self.txs_by_last_seen.iter().copied().rev()
1010+
/// Transactions without last-seens are excluded. Transactions with a last-missing timestamp
1011+
/// equal or higher than it's last-seen timestamp are excluded.
1012+
pub fn txids_by_descending_last_seen(&self) -> impl Iterator<Item = (u64, Txid)> + '_ {
1013+
self.txs_by_last_seen
1014+
.iter()
1015+
.copied()
1016+
.rev()
1017+
.filter(|(last_seen, txid)| !matches!(self.last_missing.get(txid), Some(last_missing) if last_missing >= last_seen))
9751018
}
9761019

9771020
/// Returns a [`CanonicalIter`].
@@ -1139,6 +1182,8 @@ pub struct ChangeSet<A = ()> {
11391182
pub anchors: BTreeSet<(A, Txid)>,
11401183
/// Added last-seen unix timestamps of transactions.
11411184
pub last_seen: BTreeMap<Txid, u64>,
1185+
/// Added timestamps of when a transaction is last missing from the mempool.
1186+
pub last_missing: BTreeMap<Txid, u64>,
11421187
}
11431188

11441189
impl<A> Default for ChangeSet<A> {
@@ -1148,6 +1193,7 @@ impl<A> Default for ChangeSet<A> {
11481193
txouts: Default::default(),
11491194
anchors: Default::default(),
11501195
last_seen: Default::default(),
1196+
last_missing: Default::default(),
11511197
}
11521198
}
11531199
}
@@ -1202,13 +1248,22 @@ impl<A: Ord> Merge for ChangeSet<A> {
12021248
.filter(|(txid, update_ls)| self.last_seen.get(txid) < Some(update_ls))
12031249
.collect::<Vec<_>>(),
12041250
);
1251+
// last_missing timestamps should only increase
1252+
self.last_missing.extend(
1253+
other
1254+
.last_missing
1255+
.into_iter()
1256+
.filter(|(txid, update_lm)| self.last_missing.get(txid) < Some(update_lm))
1257+
.collect::<Vec<_>>(),
1258+
);
12051259
}
12061260

12071261
fn is_empty(&self) -> bool {
12081262
self.txs.is_empty()
12091263
&& self.txouts.is_empty()
12101264
&& self.anchors.is_empty()
12111265
&& self.last_seen.is_empty()
1266+
&& self.last_missing.is_empty()
12121267
}
12131268
}
12141269

@@ -1228,6 +1283,7 @@ impl<A: Ord> ChangeSet<A> {
12281283
self.anchors.into_iter().map(|(a, txid)| (f(a), txid)),
12291284
),
12301285
last_seen: self.last_seen,
1286+
last_missing: self.last_missing,
12311287
}
12321288
}
12331289
}

crates/chain/tests/test_tx_graph.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ fn insert_txouts() {
115115
txs: [Arc::new(update_tx.clone())].into(),
116116
txouts: update_ops.clone().into(),
117117
anchors: [(conf_anchor, update_tx.compute_txid()),].into(),
118-
last_seen: [(hash!("tx2"), 1000000)].into()
118+
last_seen: [(hash!("tx2"), 1000000)].into(),
119+
last_missing: [].into(),
119120
}
120121
);
121122

@@ -168,7 +169,8 @@ fn insert_txouts() {
168169
txs: [Arc::new(update_tx.clone())].into(),
169170
txouts: update_ops.into_iter().chain(original_ops).collect(),
170171
anchors: [(conf_anchor, update_tx.compute_txid()),].into(),
171-
last_seen: [(hash!("tx2"), 1000000)].into()
172+
last_seen: [(hash!("tx2"), 1000000)].into(),
173+
last_missing: [].into(),
172174
}
173175
);
174176
}

0 commit comments

Comments
 (0)