Skip to main content

bsv_tracker/
tracker.rs

1use crate::{
2    ChainEvent, CheckError, Hash, Headers, Height, Input, Params, Reask, State, TxId, Word,
3};
4use std::collections::{BTreeMap, BTreeSet};
5
6/// A named result of an event on one affected transaction.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct ChainUpdate {
9    /// The transaction selected by an index, never by a chain scan.
10    pub txid: TxId,
11    /// The transition's result. The updated state's re-ask survives an error.
12    pub result: Result<(), CheckError>,
13    /// The next host action after the event.
14    pub reask: Option<Reask>,
15}
16
17/// A host's tracked transactions, with indexes for targeted chain events.
18/// Hints, ages and spend attempts name one transaction. A tip uses only the
19/// suspect set; reorgs, forks and splits use only the relevant mined height
20/// range.
21#[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    /// An empty host registry with explicit age parameters.
51    #[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    /// Begin tracking one transaction; repeating this does not erase its state.
62    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    /// An immutable state, or none when the host does not track this txid.
69    #[must_use]
70    pub fn get(&self, txid: &str) -> Option<&State> {
71        self.states.get(&txid.to_ascii_lowercase())
72    }
73    /// Number of transactions this host tracks.
74    #[must_use]
75    pub fn len(&self) -> usize {
76        self.states.len()
77    }
78    /// Whether the registry is empty.
79    #[must_use]
80    pub fn is_empty(&self) -> bool {
81        self.states.is_empty()
82    }
83    /// Apply an input to one transaction and maintain the event indexes,
84    /// including after a failed proof or header lookup.
85    ///
86    /// # Errors
87    ///
88    /// The [`CheckError`] of [`State::step`] for that transaction.
89    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    /// Deliver an evidence event only to the set it names. The output contains
131    /// every selected txid and each fault for the host to report.
132    // By reference would break the public API; a later minor (bsv-stack-lean #134).
133    #[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}