Skip to content

Commit 8b17bcf

Browse files
committed
feat!(chain): implement first_seen tracking
1 parent eeedb4e commit 8b17bcf

File tree

4 files changed

+229
-20
lines changed

4 files changed

+229
-20
lines changed

crates/chain/src/chain_data.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ pub enum ChainPosition<A> {
2727
},
2828
/// The chain data is not confirmed.
2929
Unconfirmed {
30+
/// When the chain data was first seen in the mempool.
31+
///
32+
/// This value will be `None` if the chain data was never seen in the mempool.
33+
first_seen: Option<u64>,
3034
/// When the chain data is last seen in the mempool.
3135
///
3236
/// This value will be `None` if the chain data was never seen in the mempool and only seen
@@ -58,7 +62,13 @@ impl<A: Clone> ChainPosition<&A> {
5862
anchor: anchor.clone(),
5963
transitively,
6064
},
61-
ChainPosition::Unconfirmed { last_seen } => ChainPosition::Unconfirmed { last_seen },
65+
ChainPosition::Unconfirmed {
66+
last_seen,
67+
first_seen,
68+
} => ChainPosition::Unconfirmed {
69+
last_seen,
70+
first_seen,
71+
},
6272
}
6373
}
6474
}
@@ -165,9 +175,11 @@ mod test {
165175
fn chain_position_ord() {
166176
let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
167177
last_seen: Some(10),
178+
first_seen: Some(10),
168179
};
169180
let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
170181
last_seen: Some(20),
182+
first_seen: Some(20),
171183
};
172184
let conf1 = ChainPosition::Confirmed {
173185
anchor: ConfirmationBlockTime {
@@ -197,4 +209,50 @@ mod test {
197209
"confirmation_height is higher then it should be higher ord"
198210
);
199211
}
212+
213+
#[test]
214+
fn test_sort_unconfirmed_chain_position() {
215+
let mut v = vec![
216+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
217+
first_seen: Some(5),
218+
last_seen: Some(20),
219+
},
220+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
221+
first_seen: Some(15),
222+
last_seen: Some(30),
223+
},
224+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
225+
first_seen: Some(1),
226+
last_seen: Some(10),
227+
},
228+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
229+
first_seen: Some(3),
230+
last_seen: Some(6),
231+
},
232+
];
233+
234+
v.sort();
235+
236+
assert_eq!(
237+
v,
238+
vec![
239+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
240+
first_seen: Some(1),
241+
last_seen: Some(10)
242+
},
243+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
244+
first_seen: Some(3),
245+
last_seen: Some(6)
246+
},
247+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
248+
first_seen: Some(5),
249+
last_seen: Some(20)
250+
},
251+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
252+
first_seen: Some(15),
253+
last_seen: Some(30)
254+
},
255+
]
256+
);
257+
}
200258
}

crates/chain/src/tx_graph.rs

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ pub struct TxGraph<A = ConfirmationBlockTime> {
177177
txs: HashMap<Txid, TxNodeInternal>,
178178
spends: BTreeMap<OutPoint, HashSet<Txid>>,
179179
anchors: HashMap<Txid, BTreeSet<A>>,
180+
first_seen: HashMap<Txid, u64>,
180181
last_seen: HashMap<Txid, u64>,
181182
last_evicted: HashMap<Txid, u64>,
182183

@@ -195,6 +196,7 @@ impl<A> Default for TxGraph<A> {
195196
txs: Default::default(),
196197
spends: Default::default(),
197198
anchors: Default::default(),
199+
first_seen: Default::default(),
198200
last_seen: Default::default(),
199201
last_evicted: Default::default(),
200202
txs_by_highest_conf_heights: Default::default(),
@@ -214,8 +216,10 @@ pub struct TxNode<'a, T, A> {
214216
pub tx: T,
215217
/// The blocks that the transaction is "anchored" in.
216218
pub anchors: &'a BTreeSet<A>,
219+
/// The first-seen unix timestamp of the transaction as unconfirmed.
220+
pub first_seen: Option<u64>,
217221
/// The last-seen unix timestamp of the transaction as unconfirmed.
218-
pub last_seen_unconfirmed: Option<u64>,
222+
pub last_seen: Option<u64>,
219223
}
220224

221225
impl<T, A> Deref for TxNode<'_, T, A> {
@@ -337,7 +341,8 @@ impl<A> TxGraph<A> {
337341
txid,
338342
tx: tx.clone(),
339343
anchors: self.anchors.get(&txid).unwrap_or(&self.empty_anchors),
340-
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
344+
first_seen: self.first_seen.get(&txid).copied(),
345+
last_seen: self.last_seen.get(&txid).copied(),
341346
}),
342347
TxNodeInternal::Partial(_) => None,
343348
})
@@ -348,7 +353,7 @@ impl<A> TxGraph<A> {
348353
&self,
349354
) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
350355
self.full_txs().filter_map(|tx| {
351-
if tx.anchors.is_empty() && tx.last_seen_unconfirmed.is_none() {
356+
if tx.anchors.is_empty() && tx.last_seen.is_none() {
352357
Some(tx)
353358
} else {
354359
None
@@ -372,7 +377,8 @@ impl<A> TxGraph<A> {
372377
txid,
373378
tx: tx.clone(),
374379
anchors: self.anchors.get(&txid).unwrap_or(&self.empty_anchors),
375-
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
380+
first_seen: self.first_seen.get(&txid).copied(),
381+
last_seen: self.last_seen.get(&txid).copied(),
376382
}),
377383
_ => None,
378384
}
@@ -787,10 +793,48 @@ impl<A: Anchor> TxGraph<A> {
787793
changeset
788794
}
789795

790-
/// Inserts the given `seen_at` for `txid` into [`TxGraph`].
796+
/// Updates the first-seen and last-seen timestamps for a given `txid` in the [`TxGraph`].
791797
///
792-
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`.
798+
/// This method records the time a transaction was observed by updating both:
799+
/// - the **first-seen** timestamp, which only changes if `seen_at` is earlier than the current value, and
800+
/// - the **last-seen** timestamp, which only changes if `seen_at` is later than the current value.
801+
///
802+
/// `seen_at` is a UNIX timestamp in seconds.
803+
///
804+
/// Returns a [`ChangeSet`] representing any changes applied.
793805
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
806+
let mut changeset_first_seen = self.update_first_seen(txid, seen_at);
807+
let changeset_last_seen = self.update_last_seen(txid, seen_at);
808+
changeset_first_seen.merge(changeset_last_seen);
809+
changeset_first_seen
810+
}
811+
812+
/// Updates `first_seen` given a new `seen_at`.
813+
fn update_first_seen(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
814+
let is_changed = match self.first_seen.entry(txid) {
815+
hash_map::Entry::Occupied(mut e) => {
816+
let first_seen = e.get_mut();
817+
let change = *first_seen > seen_at;
818+
if change {
819+
*first_seen = seen_at;
820+
}
821+
change
822+
}
823+
hash_map::Entry::Vacant(e) => {
824+
e.insert(seen_at);
825+
true
826+
}
827+
};
828+
829+
let mut changeset = ChangeSet::<A>::default();
830+
if is_changed {
831+
changeset.first_seen.insert(txid, seen_at);
832+
}
833+
changeset
834+
}
835+
836+
/// Updates `last_seen` given a new `seen_at`.
837+
fn update_last_seen(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
794838
let mut old_last_seen = None;
795839
let is_changed = match self.last_seen.entry(txid) {
796840
hash_map::Entry::Occupied(mut e) => {
@@ -904,6 +948,7 @@ impl<A: Anchor> TxGraph<A> {
904948
.iter()
905949
.flat_map(|(txid, anchors)| anchors.iter().map(|a| (a.clone(), *txid)))
906950
.collect(),
951+
first_seen: self.first_seen.iter().map(|(&k, &v)| (k, v)).collect(),
907952
last_seen: self.last_seen.iter().map(|(&k, &v)| (k, v)).collect(),
908953
last_evicted: self.last_evicted.iter().map(|(&k, &v)| (k, v)).collect(),
909954
}
@@ -978,11 +1023,13 @@ impl<A: Anchor> TxGraph<A> {
9781023
transitively: None,
9791024
},
9801025
None => ChainPosition::Unconfirmed {
981-
last_seen: tx_node.last_seen_unconfirmed,
1026+
first_seen: tx_node.first_seen,
1027+
last_seen: tx_node.last_seen,
9821028
},
9831029
},
9841030
None => ChainPosition::Unconfirmed {
985-
last_seen: tx_node.last_seen_unconfirmed,
1031+
first_seen: tx_node.first_seen,
1032+
last_seen: tx_node.last_seen,
9861033
},
9871034
},
9881035
CanonicalReason::Anchor { anchor, descendant } => match descendant {
@@ -1003,9 +1050,13 @@ impl<A: Anchor> TxGraph<A> {
10031050
},
10041051
CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
10051052
ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
1053+
first_seen: tx_node.first_seen,
10061054
last_seen: Some(last_seen),
10071055
},
1008-
ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None },
1056+
ObservedIn::Block(_) => ChainPosition::Unconfirmed {
1057+
first_seen: tx_node.first_seen,
1058+
last_seen: None,
1059+
},
10091060
},
10101061
};
10111062
Ok(CanonicalTx {
@@ -1372,6 +1423,9 @@ pub struct ChangeSet<A = ()> {
13721423
/// Added timestamps of when a transaction is last evicted from the mempool.
13731424
#[cfg_attr(feature = "serde", serde(default))]
13741425
pub last_evicted: BTreeMap<Txid, u64>,
1426+
/// Added first-seen unix timestamps of transactions.
1427+
#[cfg_attr(feature = "serde", serde(default))]
1428+
pub first_seen: BTreeMap<Txid, u64>,
13751429
}
13761430

13771431
impl<A> Default for ChangeSet<A> {
@@ -1380,6 +1434,7 @@ impl<A> Default for ChangeSet<A> {
13801434
txs: Default::default(),
13811435
txouts: Default::default(),
13821436
anchors: Default::default(),
1437+
first_seen: Default::default(),
13831438
last_seen: Default::default(),
13841439
last_evicted: Default::default(),
13851440
}
@@ -1428,6 +1483,18 @@ impl<A: Ord> Merge for ChangeSet<A> {
14281483
self.txouts.extend(other.txouts);
14291484
self.anchors.extend(other.anchors);
14301485

1486+
// first_seen timestamps should only decrease
1487+
self.first_seen.extend(
1488+
other
1489+
.first_seen
1490+
.into_iter()
1491+
.filter(|(txid, update_fs)| match self.first_seen.get(txid) {
1492+
Some(existing) => update_fs < existing,
1493+
None => true,
1494+
})
1495+
.collect::<Vec<_>>(),
1496+
);
1497+
14311498
// last_seen timestamps should only increase
14321499
self.last_seen.extend(
14331500
other
@@ -1450,6 +1517,7 @@ impl<A: Ord> Merge for ChangeSet<A> {
14501517
self.txs.is_empty()
14511518
&& self.txouts.is_empty()
14521519
&& self.anchors.is_empty()
1520+
&& self.first_seen.is_empty()
14531521
&& self.last_seen.is_empty()
14541522
&& self.last_evicted.is_empty()
14551523
}
@@ -1470,6 +1538,7 @@ impl<A: Ord> ChangeSet<A> {
14701538
anchors: BTreeSet::<(A2, Txid)>::from_iter(
14711539
self.anchors.into_iter().map(|(a, txid)| (f(a), txid)),
14721540
),
1541+
first_seen: self.first_seen,
14731542
last_seen: self.last_seen,
14741543
last_evicted: self.last_evicted,
14751544
}

crates/chain/tests/test_indexed_tx_graph.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,10 @@ fn test_get_chain_position() {
632632
},
633633
anchor: None,
634634
last_seen: Some(2),
635-
exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
635+
exp_pos: Some(ChainPosition::Unconfirmed {
636+
last_seen: Some(2),
637+
first_seen: Some(2),
638+
}),
636639
},
637640
TestCase {
638641
name: "tx anchor in best chain - confirmed",
@@ -661,7 +664,10 @@ fn test_get_chain_position() {
661664
},
662665
anchor: Some(block_id!(2, "B'")),
663666
last_seen: Some(2),
664-
exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
667+
exp_pos: Some(ChainPosition::Unconfirmed {
668+
last_seen: Some(2),
669+
first_seen: Some(2),
670+
}),
665671
},
666672
TestCase {
667673
name: "tx unknown anchor - unconfirmed",
@@ -674,7 +680,10 @@ fn test_get_chain_position() {
674680
},
675681
anchor: Some(block_id!(2, "B'")),
676682
last_seen: None,
677-
exp_pos: Some(ChainPosition::Unconfirmed { last_seen: None }),
683+
exp_pos: Some(ChainPosition::Unconfirmed {
684+
last_seen: None,
685+
first_seen: None,
686+
}),
678687
},
679688
]
680689
.into_iter()

0 commit comments

Comments
 (0)