1use crate::{
2 ChainEvent, CheckError, Hash, Headers, Height, Input, Params, Reask, State, TxId, Word,
3};
4use std::collections::{BTreeMap, BTreeSet};
5
6#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct ChainUpdate {
9 pub txid: TxId,
11 pub result: Result<(), CheckError>,
13 pub reask: Option<Reask>,
15}
16
17#[derive(Clone, Debug)]
22pub struct Tracker {
23 params: Params,
24 states: BTreeMap<TxId, State>,
25 mined: BTreeMap<Height, BTreeSet<TxId>>,
26 headers: BTreeMap<Hash, BTreeSet<TxId>>,
27 suspect: BTreeSet<TxId>,
28}
29
30#[derive(Debug, PartialEq, Eq)]
31struct Index {
32 height: Option<Height>,
33 header: Option<Hash>,
34 suspect: bool,
35}
36impl Index {
37 fn of(state: &State) -> Self {
38 Self {
39 height: state.word().height(),
40 header: match state.word() {
41 Word::Mined(m) => Some(m.checked().header().hash.clone()),
42 _ => None,
43 },
44 suspect: state.suspect(),
45 }
46 }
47}
48
49impl Tracker {
50 #[must_use]
52 pub fn new(params: Params) -> Self {
53 Self {
54 params,
55 states: BTreeMap::new(),
56 mined: BTreeMap::new(),
57 headers: BTreeMap::new(),
58 suspect: BTreeSet::new(),
59 }
60 }
61 pub fn track(&mut self, txid: impl Into<TxId>) -> &State {
63 let txid = txid.into().to_ascii_lowercase();
64 self.states
65 .entry(txid.clone())
66 .or_insert_with(|| State::new(txid))
67 }
68 #[must_use]
70 pub fn get(&self, txid: &str) -> Option<&State> {
71 self.states.get(&txid.to_ascii_lowercase())
72 }
73 #[must_use]
75 pub fn len(&self) -> usize {
76 self.states.len()
77 }
78 #[must_use]
80 pub fn is_empty(&self) -> bool {
81 self.states.is_empty()
82 }
83 pub fn apply<H: Headers + ?Sized>(
90 &mut self,
91 txid: &str,
92 headers: &H,
93 input: Input,
94 ) -> Result<(), CheckError> {
95 self.apply_transition(txid, |params, state| state.step(params, headers, input))
96 }
97 fn apply_transition(
98 &mut self,
99 txid: &str,
100 transition: impl FnOnce(&Params, &mut State) -> Result<(), CheckError>,
101 ) -> Result<(), CheckError> {
102 let txid = txid.to_ascii_lowercase();
103 let state = self
104 .states
105 .entry(txid.clone())
106 .or_insert_with(|| State::new(&txid));
107 let old = Index::of(state);
108 let result = transition(&self.params, state);
109 let new = Index::of(state);
110 if old != new {
111 if let Some(h) = old.height {
112 remove_index(&mut self.mined, &h, &txid);
113 }
114 if let Some(hash) = old.header {
115 remove_index(&mut self.headers, &hash, &txid);
116 }
117 self.suspect.remove(&txid);
118 if let Some(h) = new.height {
119 self.mined.entry(h).or_default().insert(txid.clone());
120 }
121 if let Some(hash) = new.header {
122 self.headers.entry(hash).or_default().insert(txid.clone());
123 }
124 if new.suspect {
125 self.suspect.insert(txid);
126 }
127 }
128 result
129 }
130 #[allow(clippy::needless_pass_by_value)]
134 pub fn on_chain<H: Headers + ?Sized>(
135 &mut self,
136 headers: &H,
137 event: ChainEvent,
138 ) -> Vec<ChainUpdate> {
139 let targets: Vec<TxId> = match &event {
140 ChainEvent::Fork { height, .. } => self.at_or_above(*height),
141 ChainEvent::Reorg { fork_height, .. }
142 | ChainEvent::Split { fork_height, .. }
143 | ChainEvent::SplitResolved { fork_height, .. } => self.at_or_above(*fork_height),
144 ChainEvent::Invalidated { block_hash } => self
145 .headers
146 .get(block_hash)
147 .map(|ids| ids.iter().cloned().collect())
148 .unwrap_or_default(),
149 ChainEvent::Tip { .. } => self.suspect.iter().cloned().collect(),
150 ChainEvent::Frozen { .. } | ChainEvent::TipAge { .. } => vec![],
151 };
152 targets
153 .into_iter()
154 .map(|txid| {
155 let result =
156 self.apply_transition(&txid, |_, state| state.on_chain(headers, &event));
157 let reask = self.states[&txid].reask().cloned();
158 ChainUpdate {
159 txid,
160 result,
161 reask,
162 }
163 })
164 .collect()
165 }
166 fn at_or_above(&self, height: Height) -> Vec<TxId> {
167 self.mined
168 .range(height..)
169 .flat_map(|(_, ids)| ids.iter().cloned())
170 .collect()
171 }
172}
173
174fn remove_index<K: Ord>(index: &mut BTreeMap<K, BTreeSet<TxId>>, key: &K, txid: &str) {
175 if let Some(ids) = index.get_mut(key) {
176 ids.remove(txid);
177 if ids.is_empty() {
178 index.remove(key);
179 }
180 }
181}