use crate::{
ChainEvent, CheckError, Hash, Headers, Height, Input, Params, Reask, State, TxId, Word,
};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChainUpdate {
pub txid: TxId,
pub result: Result<(), CheckError>,
pub reask: Option<Reask>,
}
#[derive(Clone, Debug)]
pub struct Tracker {
params: Params,
states: BTreeMap<TxId, State>,
mined: BTreeMap<Height, BTreeSet<TxId>>,
headers: BTreeMap<Hash, BTreeSet<TxId>>,
suspect: BTreeSet<TxId>,
}
#[derive(Debug, PartialEq, Eq)]
struct Index {
height: Option<Height>,
header: Option<Hash>,
suspect: bool,
}
impl Index {
fn of(state: &State) -> Self {
Self {
height: state.word().height(),
header: match state.word() {
Word::Mined(m) => Some(m.checked().header().hash.clone()),
_ => None,
},
suspect: state.suspect(),
}
}
}
impl Tracker {
pub fn new(params: Params) -> Self {
Self {
params,
states: BTreeMap::new(),
mined: BTreeMap::new(),
headers: BTreeMap::new(),
suspect: BTreeSet::new(),
}
}
pub fn track(&mut self, txid: impl Into<TxId>) -> &State {
let txid = txid.into().to_ascii_lowercase();
self.states
.entry(txid.clone())
.or_insert_with(|| State::new(txid))
}
pub fn get(&self, txid: &str) -> Option<&State> {
self.states.get(&txid.to_ascii_lowercase())
}
pub fn len(&self) -> usize {
self.states.len()
}
pub fn is_empty(&self) -> bool {
self.states.is_empty()
}
pub fn apply<H: Headers + ?Sized>(
&mut self,
txid: &str,
headers: &H,
input: Input,
) -> Result<(), CheckError> {
self.apply_transition(txid, |params, state| state.step(params, headers, input))
}
fn apply_transition(
&mut self,
txid: &str,
transition: impl FnOnce(&Params, &mut State) -> Result<(), CheckError>,
) -> Result<(), CheckError> {
let txid = txid.to_ascii_lowercase();
let state = self
.states
.entry(txid.clone())
.or_insert_with(|| State::new(&txid));
let old = Index::of(state);
let result = transition(&self.params, state);
let new = Index::of(state);
if old != new {
if let Some(h) = old.height {
remove_index(&mut self.mined, &h, &txid);
}
if let Some(hash) = old.header {
remove_index(&mut self.headers, &hash, &txid);
}
self.suspect.remove(&txid);
if let Some(h) = new.height {
self.mined.entry(h).or_default().insert(txid.clone());
}
if let Some(hash) = new.header {
self.headers.entry(hash).or_default().insert(txid.clone());
}
if new.suspect {
self.suspect.insert(txid);
}
}
result
}
pub fn on_chain<H: Headers + ?Sized>(
&mut self,
headers: &H,
event: ChainEvent,
) -> Vec<ChainUpdate> {
let targets: Vec<TxId> = match &event {
ChainEvent::Fork { height, .. } => self.at_or_above(*height),
ChainEvent::Reorg { fork_height, .. } => self.at_or_above(*fork_height),
ChainEvent::Invalidated { block_hash } => self
.headers
.get(block_hash)
.map(|ids| ids.iter().cloned().collect())
.unwrap_or_default(),
ChainEvent::Tip { .. } => self.suspect.iter().cloned().collect(),
ChainEvent::Frozen { .. } | ChainEvent::TipAge { .. } => vec![],
};
targets
.into_iter()
.map(|txid| {
let result =
self.apply_transition(&txid, |_, state| state.on_chain(headers, &event));
let reask = self.states[&txid].reask().cloned();
ChainUpdate {
txid,
result,
reask,
}
})
.collect()
}
fn at_or_above(&self, height: Height) -> Vec<TxId> {
self.mined
.range(height..)
.flat_map(|(_, ids)| ids.iter().cloned())
.collect()
}
}
fn remove_index<K: Ord>(index: &mut BTreeMap<K, BTreeSet<TxId>>, key: &K, txid: &str) {
if let Some(ids) = index.get_mut(key) {
ids.remove(txid);
if ids.is_empty() {
index.remove(key);
}
}
}