use crate::{
ChainEvent, CheckError, CheckedProof, Headers, Height, Hint, HintStatus, Proof, Timestamp, TxId,
};
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Mined {
checked: Arc<CheckedProof>,
}
impl Mined {
pub fn height(&self) -> Height {
self.checked.height()
}
pub fn checked(&self) -> &CheckedProof {
&self.checked
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Word {
Unknown,
Built,
Announced,
Seen,
Mined(Mined),
Stale,
Rejected { reason: String },
Conflicted { competitors: Vec<TxId> },
Abandoned,
}
impl Word {
pub fn chain(&self) -> Option<&Self> {
match self {
Self::Mined(_) | Self::Stale | Self::Rejected { .. } | Self::Conflicted { .. } => {
Some(self)
}
_ => None,
}
}
pub fn height(&self) -> Option<Height> {
if let Self::Mined(m) = self {
Some(m.height())
} else {
None
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NodeVerdict {
Rejected { reason: String },
Conflicted { competitors: Vec<TxId> },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Evidence {
Proof(Proof),
Competitor(Proof),
Chain(ChainEvent),
Verdict(NodeVerdict),
Recheck,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HostAction {
Build,
Abandon,
SpendAttempt,
Tick(Timestamp),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Input {
Evidence(Evidence),
Hint(Hint),
Host(HostAction),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Reask {
Reorg { fork_height: Height },
Fork { height: Height },
Hint(Hint),
ProofFailed { height: Height },
Recheck,
Age,
Spend,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Params {
pub age_threshold: Timestamp,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct State {
txid: TxId,
word: Word,
evidence: Option<Arc<CheckedProof>>,
suspect: bool,
hints: Vec<Hint>,
last_hint_at: Option<Timestamp>,
reask: Option<Reask>,
}
impl State {
pub fn new(txid: impl Into<TxId>) -> Self {
Self {
txid: txid.into().to_ascii_lowercase(),
word: Word::Unknown,
evidence: None,
suspect: false,
hints: vec![],
last_hint_at: None,
reask: None,
}
}
pub fn txid(&self) -> &str {
&self.txid
}
pub fn word(&self) -> &Word {
&self.word
}
pub fn evidence(&self) -> Option<&CheckedProof> {
self.evidence.as_deref()
}
pub fn suspect(&self) -> bool {
self.suspect
}
pub fn hints(&self) -> &[Hint] {
&self.hints
}
pub fn last_hint_at(&self) -> Option<Timestamp> {
self.last_hint_at
}
pub fn reask(&self) -> Option<&Reask> {
self.reask.as_ref()
}
pub fn step<H: Headers + ?Sized>(
&mut self,
params: &Params,
headers: &H,
input: Input,
) -> Result<(), CheckError> {
match input {
Input::Hint(hint) => self.on_hint(hint),
Input::Host(action) => self.on_host(params, action),
Input::Evidence(Evidence::Proof(proof)) => self.on_proof(headers, proof)?,
Input::Evidence(Evidence::Competitor(proof)) => self.on_competitor(headers, proof)?,
Input::Evidence(Evidence::Chain(event)) => self.on_chain(headers, &event)?,
Input::Evidence(Evidence::Verdict(verdict)) => self.on_verdict(verdict),
Input::Evidence(Evidence::Recheck) => self.recheck(headers)?,
}
Ok(())
}
fn on_hint(&mut self, hint: Hint) {
match (&self.word, &hint.status) {
(Word::Built, HintStatus::Accepted) => self.word = Word::Announced,
(Word::Built | Word::Announced, HintStatus::Seen) => self.word = Word::Seen,
_ => {}
}
if !agrees(&self.word, &hint.status) {
self.reask = Some(Reask::Hint(hint.clone()));
}
self.last_hint_at = Some(hint.observed);
self.hints.insert(0, hint);
}
fn on_host(&mut self, params: &Params, action: HostAction) {
match action {
HostAction::Build if self.word == Word::Unknown => self.word = Word::Built,
HostAction::Abandon if matches!(self.word, Word::Unknown | Word::Built) => {
self.word = Word::Abandoned
}
HostAction::SpendAttempt => self.reask = Some(Reask::Spend),
HostAction::Tick(now)
if matches!(self.word, Word::Announced | Word::Seen)
&& self
.last_hint_at
.and_then(|t| t.checked_add(params.age_threshold))
.is_some_and(|deadline| deadline <= now) =>
{
self.reask = Some(Reask::Age);
}
_ => {}
}
}
fn on_proof<H: Headers + ?Sized>(
&mut self,
headers: &H,
proof: Proof,
) -> Result<(), CheckError> {
let height = proof.height();
if proof.txid() != self.txid {
self.reask = Some(Reask::ProofFailed { height });
return Err(CheckError::TxidMismatch);
}
match crate::evidence::check_snapshot(headers, proof) {
Ok(checked) => {
let checked = Arc::new(checked);
self.word = Word::Mined(Mined {
checked: Arc::clone(&checked),
});
self.evidence = Some(checked);
self.suspect = false;
self.reask = None;
Ok(())
}
Err(error) => {
self.reask = Some(Reask::ProofFailed { height });
Err(error)
}
}
}
fn on_competitor<H: Headers + ?Sized>(
&mut self,
headers: &H,
proof: Proof,
) -> Result<(), CheckError> {
let height = proof.height();
let competitor = proof.txid().to_owned();
match crate::evidence::check_snapshot(headers, proof) {
Ok(_) => {
match &mut self.word {
Word::Mined(_) => self.reask = Some(Reask::Recheck),
Word::Conflicted { competitors } => competitors.insert(0, competitor),
_ => {
self.word = Word::Conflicted {
competitors: vec![competitor],
}
}
}
Ok(())
}
Err(error) => {
self.reask = Some(Reask::ProofFailed { height });
Err(error)
}
}
}
fn recheck<H: Headers + ?Sized>(&mut self, headers: &H) -> Result<(), CheckError> {
let Word::Mined(mined) = &self.word else {
return Ok(());
};
let height = mined.height();
let answer = headers.header_at(height);
match answer {
Ok(Some(header)) if header.merkle_root == mined.checked().root() => {
self.suspect = false;
self.reask = None;
}
Ok(Some(_)) => self.stale(height),
Ok(None) => {
self.reask = Some(Reask::Recheck);
return Err(CheckError::Unavailable(height));
}
Err(error) => {
self.reask = Some(Reask::Recheck);
return Err(CheckError::Headers(error));
}
}
Ok(())
}
fn stale(&mut self, fork_height: Height) {
self.word = Word::Stale;
self.suspect = false;
self.reask = Some(Reask::Reorg { fork_height });
}
pub(crate) fn on_chain<H: Headers + ?Sized>(
&mut self,
headers: &H,
event: &ChainEvent,
) -> Result<(), CheckError> {
match event {
ChainEvent::Tip { .. } if self.suspect => self.recheck(headers)?,
ChainEvent::Fork { height, .. } if self.word.height().is_some_and(|h| *height <= h) => {
self.suspect = true;
self.reask = Some(Reask::Fork { height: *height });
}
ChainEvent::Reorg { fork_height, .. }
if self.word.height().is_some_and(|h| *fork_height <= h) =>
{
self.stale(*fork_height)
}
ChainEvent::Invalidated { block_hash } => {
if let Word::Mined(mined) = &self.word {
if &mined.checked().header().hash == block_hash {
self.stale(mined.height());
}
}
}
_ => {}
}
Ok(())
}
fn on_verdict(&mut self, verdict: NodeVerdict) {
if matches!(self.word, Word::Mined(_)) {
self.reask = Some(Reask::Recheck);
return;
}
self.word = match verdict {
NodeVerdict::Rejected { reason } => Word::Rejected { reason },
NodeVerdict::Conflicted { mut competitors } => {
if let Word::Conflicted {
competitors: previous,
} = &self.word
{
competitors.extend(previous.iter().cloned());
}
Word::Conflicted { competitors }
}
};
}
}
fn agrees(word: &Word, status: &HintStatus) -> bool {
match (word, status) {
(Word::Announced | Word::Seen, HintStatus::Accepted)
| (Word::Seen, HintStatus::Seen)
| (Word::Stale, HintStatus::StaleBlock)
| (Word::Rejected { .. }, HintStatus::Rejected { .. })
| (Word::Conflicted { .. }, HintStatus::DoubleSpend { .. }) => true,
(Word::Mined(mined), HintStatus::Mined { height }) => mined.height() == *height,
_ => false,
}
}