Skip to content

Commit 180e4ee

Browse files
committed
feat(chain): implement first_seen tracking
1 parent 370497c commit 180e4ee

File tree

4 files changed

+213
-9
lines changed

4 files changed

+213
-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
@@ -53,7 +57,13 @@ impl<A: Clone> ChainPosition<&A> {
5357
anchor: anchor.clone(),
5458
transitively,
5559
},
56-
ChainPosition::Unconfirmed { last_seen } => ChainPosition::Unconfirmed { last_seen },
60+
ChainPosition::Unconfirmed {
61+
last_seen,
62+
first_seen,
63+
} => ChainPosition::Unconfirmed {
64+
last_seen,
65+
first_seen,
66+
},
5767
}
5868
}
5969
}
@@ -160,9 +170,11 @@ mod test {
160170
fn chain_position_ord() {
161171
let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
162172
last_seen: Some(10),
173+
first_seen: Some(10),
163174
};
164175
let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
165176
last_seen: Some(20),
177+
first_seen: Some(20),
166178
};
167179
let conf1 = ChainPosition::Confirmed {
168180
anchor: ConfirmationBlockTime {
@@ -192,4 +204,50 @@ mod test {
192204
"confirmation_height is higher then it should be higher ord"
193205
);
194206
}
207+
208+
#[test]
209+
fn test_sort_unconfirmed_chain_position() {
210+
let mut v = vec![
211+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
212+
first_seen: Some(5),
213+
last_seen: Some(20),
214+
},
215+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
216+
first_seen: Some(15),
217+
last_seen: Some(30),
218+
},
219+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
220+
first_seen: Some(1),
221+
last_seen: Some(10),
222+
},
223+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
224+
first_seen: Some(3),
225+
last_seen: Some(6),
226+
},
227+
];
228+
229+
v.sort();
230+
231+
assert_eq!(
232+
v,
233+
vec![
234+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
235+
first_seen: Some(1),
236+
last_seen: Some(10)
237+
},
238+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
239+
first_seen: Some(3),
240+
last_seen: Some(6)
241+
},
242+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
243+
first_seen: Some(5),
244+
last_seen: Some(20)
245+
},
246+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
247+
first_seen: Some(15),
248+
last_seen: Some(30)
249+
},
250+
]
251+
);
252+
}
195253
}

crates/chain/src/tx_graph.rs

+63-2
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ pub struct TxGraph<A = ConfirmationBlockTime> {
176176
txs: HashMap<Txid, TxNodeInternal>,
177177
spends: BTreeMap<OutPoint, HashSet<Txid>>,
178178
anchors: HashMap<Txid, BTreeSet<A>>,
179+
first_seen: HashMap<Txid, u64>,
179180
last_seen: HashMap<Txid, u64>,
180181
last_evicted: HashMap<Txid, u64>,
181182

@@ -194,6 +195,7 @@ impl<A> Default for TxGraph<A> {
194195
txs: Default::default(),
195196
spends: Default::default(),
196197
anchors: Default::default(),
198+
first_seen: Default::default(),
197199
last_seen: Default::default(),
198200
last_evicted: Default::default(),
199201
txs_by_highest_conf_heights: Default::default(),
@@ -213,6 +215,8 @@ pub struct TxNode<'a, T, A> {
213215
pub tx: T,
214216
/// The blocks that the transaction is "anchored" in.
215217
pub anchors: &'a BTreeSet<A>,
218+
/// The first-seen unix timestamp of the transaction.
219+
pub first_seen: Option<u64>,
216220
/// The last-seen unix timestamp of the transaction as unconfirmed.
217221
pub last_seen_unconfirmed: Option<u64>,
218222
}
@@ -324,6 +328,7 @@ impl<A> TxGraph<A> {
324328
txid,
325329
tx: tx.clone(),
326330
anchors: self.anchors.get(&txid).unwrap_or(&self.empty_anchors),
331+
first_seen: self.first_seen.get(&txid).copied(),
327332
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
328333
}),
329334
TxNodeInternal::Partial(_) => None,
@@ -359,6 +364,7 @@ impl<A> TxGraph<A> {
359364
txid,
360365
tx: tx.clone(),
361366
anchors: self.anchors.get(&txid).unwrap_or(&self.empty_anchors),
367+
first_seen: self.first_seen.get(&txid).copied(),
362368
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
363369
}),
364370
_ => None,
@@ -719,8 +725,40 @@ impl<A: Anchor> TxGraph<A> {
719725

720726
/// Inserts the given `seen_at` for `txid` into [`TxGraph`].
721727
///
722-
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`.
728+
/// Note that [`TxGraph`] keeps track of the latest `seen_at` and the earliest `seen_at`.
723729
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
730+
let mut changeset_first_seen = self.update_first_seen(txid, seen_at);
731+
let changeset_last_seen = self.update_last_seen(txid, seen_at);
732+
changeset_first_seen.merge(changeset_last_seen);
733+
changeset_first_seen
734+
}
735+
736+
/// Updates `first_seen` given a new `seen_at`.
737+
fn update_first_seen(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
738+
let is_changed = match self.first_seen.entry(txid) {
739+
hash_map::Entry::Occupied(mut e) => {
740+
let first_seen = e.get_mut();
741+
let change = *first_seen > seen_at;
742+
if change {
743+
*first_seen = seen_at;
744+
}
745+
change
746+
}
747+
hash_map::Entry::Vacant(e) => {
748+
e.insert(seen_at);
749+
true
750+
}
751+
};
752+
753+
let mut changeset = ChangeSet::<A>::default();
754+
if is_changed {
755+
changeset.first_seen.insert(txid, seen_at);
756+
}
757+
changeset
758+
}
759+
760+
/// Updates `last_seen` given a new `seen_at`.
761+
fn update_last_seen(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
724762
let mut old_last_seen = None;
725763
let is_changed = match self.last_seen.entry(txid) {
726764
hash_map::Entry::Occupied(mut e) => {
@@ -814,6 +852,7 @@ impl<A: Anchor> TxGraph<A> {
814852
.iter()
815853
.flat_map(|(txid, anchors)| anchors.iter().map(|a| (a.clone(), *txid)))
816854
.collect(),
855+
first_seen: self.first_seen.iter().map(|(&k, &v)| (k, v)).collect(),
817856
last_seen: self.last_seen.iter().map(|(&k, &v)| (k, v)).collect(),
818857
last_evicted: self.last_evicted.iter().map(|(&k, &v)| (k, v)).collect(),
819858
}
@@ -894,8 +933,12 @@ impl<A: Anchor> TxGraph<A> {
894933
CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
895934
ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
896935
last_seen: Some(last_seen),
936+
first_seen: tx_node.first_seen,
937+
},
938+
ObservedIn::Block(_) => ChainPosition::Unconfirmed {
939+
last_seen: None,
940+
first_seen: tx_node.first_seen,
897941
},
898-
ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None },
899942
},
900943
};
901944
Ok(CanonicalTx {
@@ -1255,6 +1298,9 @@ pub struct ChangeSet<A = ()> {
12551298
/// Added timestamps of when a transaction is last evicted from the mempool.
12561299
#[cfg_attr(feature = "serde", serde(default))]
12571300
pub last_evicted: BTreeMap<Txid, u64>,
1301+
/// Added first-seen unix timestamps of transactions.
1302+
#[cfg_attr(feature = "serde", serde(default))]
1303+
pub first_seen: BTreeMap<Txid, u64>,
12581304
}
12591305

12601306
impl<A> Default for ChangeSet<A> {
@@ -1263,6 +1309,7 @@ impl<A> Default for ChangeSet<A> {
12631309
txs: Default::default(),
12641310
txouts: Default::default(),
12651311
anchors: Default::default(),
1312+
first_seen: Default::default(),
12661313
last_seen: Default::default(),
12671314
last_evicted: Default::default(),
12681315
}
@@ -1311,6 +1358,18 @@ impl<A: Ord> Merge for ChangeSet<A> {
13111358
self.txouts.extend(other.txouts);
13121359
self.anchors.extend(other.anchors);
13131360

1361+
// first_seen timestamps should only decrease
1362+
self.first_seen.extend(
1363+
other
1364+
.first_seen
1365+
.into_iter()
1366+
.filter(|(txid, update_fs)| match self.first_seen.get(txid) {
1367+
Some(existing) => update_fs < existing,
1368+
None => true,
1369+
})
1370+
.collect::<Vec<_>>(),
1371+
);
1372+
13141373
// last_seen timestamps should only increase
13151374
self.last_seen.extend(
13161375
other
@@ -1333,6 +1392,7 @@ impl<A: Ord> Merge for ChangeSet<A> {
13331392
self.txs.is_empty()
13341393
&& self.txouts.is_empty()
13351394
&& self.anchors.is_empty()
1395+
&& self.first_seen.is_empty()
13361396
&& self.last_seen.is_empty()
13371397
&& self.last_evicted.is_empty()
13381398
}
@@ -1353,6 +1413,7 @@ impl<A: Ord> ChangeSet<A> {
13531413
anchors: BTreeSet::<(A2, Txid)>::from_iter(
13541414
self.anchors.into_iter().map(|(a, txid)| (f(a), txid)),
13551415
),
1416+
first_seen: self.first_seen,
13561417
last_seen: self.last_seen,
13571418
last_evicted: self.last_evicted,
13581419
}

crates/chain/tests/test_indexed_tx_graph.rs

+12-3
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,10 @@ fn test_get_chain_position() {
625625
},
626626
anchor: None,
627627
last_seen: Some(2),
628-
exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
628+
exp_pos: Some(ChainPosition::Unconfirmed {
629+
last_seen: Some(2),
630+
first_seen: Some(2),
631+
}),
629632
},
630633
TestCase {
631634
name: "tx anchor in best chain - confirmed",
@@ -654,7 +657,10 @@ fn test_get_chain_position() {
654657
},
655658
anchor: Some(block_id!(2, "B'")),
656659
last_seen: Some(2),
657-
exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
660+
exp_pos: Some(ChainPosition::Unconfirmed {
661+
last_seen: Some(2),
662+
first_seen: Some(2),
663+
}),
658664
},
659665
TestCase {
660666
name: "tx unknown anchor - unconfirmed",
@@ -667,7 +673,10 @@ fn test_get_chain_position() {
667673
},
668674
anchor: Some(block_id!(2, "B'")),
669675
last_seen: None,
670-
exp_pos: Some(ChainPosition::Unconfirmed { last_seen: None }),
676+
exp_pos: Some(ChainPosition::Unconfirmed {
677+
last_seen: None,
678+
first_seen: None,
679+
}),
671680
},
672681
]
673682
.into_iter()

0 commit comments

Comments
 (0)