Skip to content

Commit 352ccbc

Browse files
committed
feat(chain): implement first_seen tracking
1 parent eeedb4e commit 352ccbc

File tree

4 files changed

+215
-9
lines changed

4 files changed

+215
-9
lines changed

crates/chain/src/chain_data.rs

+59-1
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

+65-2
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,6 +216,8 @@ 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.
220+
pub first_seen: Option<u64>,
217221
/// The last-seen unix timestamp of the transaction as unconfirmed.
218222
pub last_seen_unconfirmed: Option<u64>,
219223
}
@@ -337,6 +341,7 @@ impl<A> TxGraph<A> {
337341
txid,
338342
tx: tx.clone(),
339343
anchors: self.anchors.get(&txid).unwrap_or(&self.empty_anchors),
344+
first_seen: self.first_seen.get(&txid).copied(),
340345
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
341346
}),
342347
TxNodeInternal::Partial(_) => None,
@@ -372,6 +377,7 @@ impl<A> TxGraph<A> {
372377
txid,
373378
tx: tx.clone(),
374379
anchors: self.anchors.get(&txid).unwrap_or(&self.empty_anchors),
380+
first_seen: self.first_seen.get(&txid).copied(),
375381
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
376382
}),
377383
_ => None,
@@ -789,8 +795,40 @@ impl<A: Anchor> TxGraph<A> {
789795

790796
/// Inserts the given `seen_at` for `txid` into [`TxGraph`].
791797
///
792-
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`.
798+
/// Note that [`TxGraph`] keeps track of the latest `seen_at` and the earliest `seen_at`.
793799
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
800+
let mut changeset_first_seen = self.update_first_seen(txid, seen_at);
801+
let changeset_last_seen = self.update_last_seen(txid, seen_at);
802+
changeset_first_seen.merge(changeset_last_seen);
803+
changeset_first_seen
804+
}
805+
806+
/// Updates `first_seen` given a new `seen_at`.
807+
fn update_first_seen(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
808+
let is_changed = match self.first_seen.entry(txid) {
809+
hash_map::Entry::Occupied(mut e) => {
810+
let first_seen = e.get_mut();
811+
let change = *first_seen > seen_at;
812+
if change {
813+
*first_seen = seen_at;
814+
}
815+
change
816+
}
817+
hash_map::Entry::Vacant(e) => {
818+
e.insert(seen_at);
819+
true
820+
}
821+
};
822+
823+
let mut changeset = ChangeSet::<A>::default();
824+
if is_changed {
825+
changeset.first_seen.insert(txid, seen_at);
826+
}
827+
changeset
828+
}
829+
830+
/// Updates `last_seen` given a new `seen_at`.
831+
fn update_last_seen(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
794832
let mut old_last_seen = None;
795833
let is_changed = match self.last_seen.entry(txid) {
796834
hash_map::Entry::Occupied(mut e) => {
@@ -904,6 +942,7 @@ impl<A: Anchor> TxGraph<A> {
904942
.iter()
905943
.flat_map(|(txid, anchors)| anchors.iter().map(|a| (a.clone(), *txid)))
906944
.collect(),
945+
first_seen: self.first_seen.iter().map(|(&k, &v)| (k, v)).collect(),
907946
last_seen: self.last_seen.iter().map(|(&k, &v)| (k, v)).collect(),
908947
last_evicted: self.last_evicted.iter().map(|(&k, &v)| (k, v)).collect(),
909948
}
@@ -978,10 +1017,12 @@ impl<A: Anchor> TxGraph<A> {
9781017
transitively: None,
9791018
},
9801019
None => ChainPosition::Unconfirmed {
1020+
first_seen: tx_node.first_seen,
9811021
last_seen: tx_node.last_seen_unconfirmed,
9821022
},
9831023
},
9841024
None => ChainPosition::Unconfirmed {
1025+
first_seen: tx_node.first_seen,
9851026
last_seen: tx_node.last_seen_unconfirmed,
9861027
},
9871028
},
@@ -1003,9 +1044,13 @@ impl<A: Anchor> TxGraph<A> {
10031044
},
10041045
CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
10051046
ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
1047+
first_seen: tx_node.first_seen,
10061048
last_seen: Some(last_seen),
10071049
},
1008-
ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None },
1050+
ObservedIn::Block(_) => ChainPosition::Unconfirmed {
1051+
first_seen: tx_node.first_seen,
1052+
last_seen: None,
1053+
},
10091054
},
10101055
};
10111056
Ok(CanonicalTx {
@@ -1372,6 +1417,9 @@ pub struct ChangeSet<A = ()> {
13721417
/// Added timestamps of when a transaction is last evicted from the mempool.
13731418
#[cfg_attr(feature = "serde", serde(default))]
13741419
pub last_evicted: BTreeMap<Txid, u64>,
1420+
/// Added first-seen unix timestamps of transactions.
1421+
#[cfg_attr(feature = "serde", serde(default))]
1422+
pub first_seen: BTreeMap<Txid, u64>,
13751423
}
13761424

13771425
impl<A> Default for ChangeSet<A> {
@@ -1380,6 +1428,7 @@ impl<A> Default for ChangeSet<A> {
13801428
txs: Default::default(),
13811429
txouts: Default::default(),
13821430
anchors: Default::default(),
1431+
first_seen: Default::default(),
13831432
last_seen: Default::default(),
13841433
last_evicted: Default::default(),
13851434
}
@@ -1428,6 +1477,18 @@ impl<A: Ord> Merge for ChangeSet<A> {
14281477
self.txouts.extend(other.txouts);
14291478
self.anchors.extend(other.anchors);
14301479

1480+
// first_seen timestamps should only decrease
1481+
self.first_seen.extend(
1482+
other
1483+
.first_seen
1484+
.into_iter()
1485+
.filter(|(txid, update_fs)| match self.first_seen.get(txid) {
1486+
Some(existing) => update_fs < existing,
1487+
None => true,
1488+
})
1489+
.collect::<Vec<_>>(),
1490+
);
1491+
14311492
// last_seen timestamps should only increase
14321493
self.last_seen.extend(
14331494
other
@@ -1450,6 +1511,7 @@ impl<A: Ord> Merge for ChangeSet<A> {
14501511
self.txs.is_empty()
14511512
&& self.txouts.is_empty()
14521513
&& self.anchors.is_empty()
1514+
&& self.first_seen.is_empty()
14531515
&& self.last_seen.is_empty()
14541516
&& self.last_evicted.is_empty()
14551517
}
@@ -1470,6 +1532,7 @@ impl<A: Ord> ChangeSet<A> {
14701532
anchors: BTreeSet::<(A2, Txid)>::from_iter(
14711533
self.anchors.into_iter().map(|(a, txid)| (f(a), txid)),
14721534
),
1535+
first_seen: self.first_seen,
14731536
last_seen: self.last_seen,
14741537
last_evicted: self.last_evicted,
14751538
}

crates/chain/tests/test_indexed_tx_graph.rs

+12-3
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)