1use crate::{
2 ChainEvent, CheckError, CheckedProof, Headers, Height, Hint, HintStatus, Proof, Timestamp, TxId,
3};
4use std::sync::Arc;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct Mined {
14 checked: Arc<CheckedProof>,
15}
16impl Mined {
17 #[must_use]
19 pub fn height(&self) -> Height {
20 self.checked.height()
21 }
22 #[must_use]
24 pub fn checked(&self) -> &CheckedProof {
25 &self.checked
26 }
27}
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum Word {
31 Unknown,
33 Built,
35 Announced,
37 Seen,
39 Mined(Mined),
41 Stale,
43 Rejected {
45 reason: String,
47 },
48 Conflicted {
50 competitors: Vec<TxId>,
52 },
53 Abandoned,
55}
56impl Word {
57 #[must_use]
59 pub fn chain(&self) -> Option<&Self> {
60 match self {
61 Self::Mined(_) | Self::Stale | Self::Rejected { .. } | Self::Conflicted { .. } => {
62 Some(self)
63 }
64 _ => None,
65 }
66 }
67 #[must_use]
69 pub fn height(&self) -> Option<Height> {
70 if let Self::Mined(m) = self {
71 Some(m.height())
72 } else {
73 None
74 }
75 }
76}
77#[derive(Clone, Debug, PartialEq, Eq)]
79pub enum NodeVerdict {
80 Rejected {
82 reason: String,
84 },
85 Conflicted {
87 competitors: Vec<TxId>,
89 },
90}
91#[derive(Clone, Debug, PartialEq, Eq)]
93pub enum Evidence {
94 Proof(Proof),
96 Competitor(Proof),
98 Chain(ChainEvent),
100 Verdict(NodeVerdict),
102 Recheck,
104}
105#[derive(Clone, Debug, PartialEq, Eq)]
107pub enum HostAction {
108 Build,
110 Abandon,
112 SpendAttempt,
114 Tick(Timestamp),
116}
117#[derive(Clone, Debug, PartialEq, Eq)]
119pub enum Input {
120 Evidence(Evidence),
122 Hint(Hint),
124 Host(HostAction),
126}
127#[derive(Clone, Debug, PartialEq, Eq)]
129pub enum Reask {
130 Reorg {
132 fork_height: Height,
135 },
136 Fork {
140 height: Height,
142 },
143 Hint(Hint),
145 ProofFailed {
147 height: Height,
149 },
150 Recheck,
152 Age,
154 Spend,
156}
157#[derive(Clone, Debug, PartialEq, Eq)]
159pub struct Params {
160 pub age_threshold: Timestamp,
162}
163#[derive(Clone, Debug, PartialEq, Eq)]
165pub struct State {
166 txid: TxId,
167 word: Word,
168 evidence: Option<Arc<CheckedProof>>,
169 suspect: bool,
170 hints: Vec<Hint>,
171 last_hint_at: Option<Timestamp>,
172 reask: Option<Reask>,
173}
174impl State {
175 pub fn new(txid: impl Into<TxId>) -> Self {
177 Self {
178 txid: txid.into().to_ascii_lowercase(),
179 word: Word::Unknown,
180 evidence: None,
181 suspect: false,
182 hints: vec![],
183 last_hint_at: None,
184 reask: None,
185 }
186 }
187 #[must_use]
189 pub fn txid(&self) -> &str {
190 &self.txid
191 }
192 #[must_use]
194 pub fn word(&self) -> &Word {
195 &self.word
196 }
197 #[must_use]
199 pub fn evidence(&self) -> Option<&CheckedProof> {
200 self.evidence.as_deref()
201 }
202 #[must_use]
204 pub fn suspect(&self) -> bool {
205 self.suspect
206 }
207 #[must_use]
209 pub fn hints(&self) -> &[Hint] {
210 &self.hints
211 }
212 #[must_use]
214 pub fn last_hint_at(&self) -> Option<Timestamp> {
215 self.last_hint_at
216 }
217 #[must_use]
219 pub fn reask(&self) -> Option<&Reask> {
220 self.reask.as_ref()
221 }
222 pub fn step<H: Headers + ?Sized>(
230 &mut self,
231 params: &Params,
232 headers: &H,
233 input: Input,
234 ) -> Result<(), CheckError> {
235 match input {
236 Input::Hint(hint) => self.on_hint(hint),
237 Input::Host(action) => self.on_host(params, &action),
238 Input::Evidence(Evidence::Proof(proof)) => self.on_proof(headers, proof)?,
239 Input::Evidence(Evidence::Competitor(proof)) => self.on_competitor(headers, proof)?,
240 Input::Evidence(Evidence::Chain(event)) => self.on_chain(headers, &event)?,
241 Input::Evidence(Evidence::Verdict(verdict)) => self.on_verdict(verdict),
242 Input::Evidence(Evidence::Recheck) => self.recheck(headers)?,
243 }
244 Ok(())
245 }
246
247 fn on_hint(&mut self, hint: Hint) {
248 match (&self.word, &hint.status) {
249 (Word::Built, HintStatus::Accepted) => self.word = Word::Announced,
250 (Word::Built | Word::Announced, HintStatus::Seen) => self.word = Word::Seen,
251 _ => {}
252 }
253 if !agrees(&self.word, &hint.status) {
254 self.reask = Some(Reask::Hint(hint.clone()));
255 }
256 self.last_hint_at = Some(hint.observed);
257 self.hints.insert(0, hint);
258 }
259
260 fn on_host(&mut self, params: &Params, action: &HostAction) {
261 match *action {
262 HostAction::Build if self.word == Word::Unknown => self.word = Word::Built,
263 HostAction::Abandon if matches!(self.word, Word::Unknown | Word::Built) => {
264 self.word = Word::Abandoned;
265 }
266 HostAction::SpendAttempt => self.reask = Some(Reask::Spend),
267 HostAction::Tick(now)
270 if matches!(self.word, Word::Announced | Word::Seen)
271 && self
272 .last_hint_at
273 .and_then(|t| t.checked_add(params.age_threshold))
274 .is_some_and(|deadline| deadline <= now) =>
275 {
276 self.reask = Some(Reask::Age);
277 }
278 _ => {}
279 }
280 }
281
282 fn on_proof<H: Headers + ?Sized>(
283 &mut self,
284 headers: &H,
285 proof: Proof,
286 ) -> Result<(), CheckError> {
287 let height = proof.height();
288 if proof.txid() != self.txid {
289 self.reask = Some(Reask::ProofFailed { height });
290 return Err(CheckError::TxidMismatch);
291 }
292 match crate::evidence::check_snapshot(headers, proof) {
293 Ok(checked) => {
294 let checked = Arc::new(checked);
295 self.word = Word::Mined(Mined {
296 checked: Arc::clone(&checked),
297 });
298 self.evidence = Some(checked);
299 self.suspect = false;
300 self.reask = None;
301 Ok(())
302 }
303 Err(error) => {
304 self.reask = Some(Reask::ProofFailed { height });
305 Err(error)
306 }
307 }
308 }
309
310 fn on_competitor<H: Headers + ?Sized>(
311 &mut self,
312 headers: &H,
313 proof: Proof,
314 ) -> Result<(), CheckError> {
315 let height = proof.height();
316 let competitor = proof.txid().to_owned();
317 match crate::evidence::check_snapshot(headers, proof) {
318 Ok(_) => {
319 match &mut self.word {
320 Word::Mined(_) => self.reask = Some(Reask::Recheck),
321 Word::Conflicted { competitors } => competitors.insert(0, competitor),
322 _ => {
323 self.word = Word::Conflicted {
324 competitors: vec![competitor],
325 }
326 }
327 }
328 Ok(())
329 }
330 Err(error) => {
331 self.reask = Some(Reask::ProofFailed { height });
332 Err(error)
333 }
334 }
335 }
336
337 fn recheck<H: Headers + ?Sized>(&mut self, headers: &H) -> Result<(), CheckError> {
338 let Word::Mined(mined) = &self.word else {
339 return Ok(());
340 };
341 let height = mined.height();
342 let answer = headers.header_at(height);
343 match answer {
344 Ok(Some(header)) if header.merkle_root == mined.checked().root() => {
345 self.suspect = false;
346 self.reask = None;
347 }
348 Ok(Some(_)) => self.stale(height),
349 Ok(None) => {
350 self.reask = Some(Reask::Recheck);
351 return Err(CheckError::Unavailable(height));
352 }
353 Err(error) => {
354 self.reask = Some(Reask::Recheck);
355 return Err(CheckError::Headers(error));
356 }
357 }
358 Ok(())
359 }
360
361 fn stale(&mut self, fork_height: Height) {
362 self.word = Word::Stale;
363 self.suspect = false;
364 self.reask = Some(Reask::Reorg { fork_height });
365 }
366
367 pub(crate) fn on_chain<H: Headers + ?Sized>(
368 &mut self,
369 headers: &H,
370 event: &ChainEvent,
371 ) -> Result<(), CheckError> {
372 match event {
373 ChainEvent::Tip { .. } if self.suspect => self.recheck(headers)?,
374 ChainEvent::Fork { height, .. } if self.word.height().is_some_and(|h| *height <= h) => {
375 self.suspect = true;
376 self.reask = Some(Reask::Fork { height: *height });
377 }
378 ChainEvent::Reorg { fork_height, .. }
379 if self.word.height().is_some_and(|h| *fork_height <= h) =>
380 {
381 self.stale(*fork_height);
382 }
383 ChainEvent::Split { fork_height, .. }
387 if self.word.height().is_some_and(|h| *fork_height <= h) =>
388 {
389 self.suspect = true;
390 self.reask = Some(Reask::Fork {
391 height: *fork_height,
392 });
393 }
394 ChainEvent::SplitResolved { fork_height, .. }
396 if self.suspect && self.word.height().is_some_and(|h| *fork_height <= h) =>
397 {
398 self.recheck(headers)?;
399 }
400 ChainEvent::Invalidated { block_hash } => {
401 if let Word::Mined(mined) = &self.word {
402 if &mined.checked().header().hash == block_hash {
403 self.stale(mined.height());
404 }
405 }
406 }
407 _ => {}
408 }
409 Ok(())
410 }
411
412 fn on_verdict(&mut self, verdict: NodeVerdict) {
413 if matches!(self.word, Word::Mined(_)) {
414 self.reask = Some(Reask::Recheck);
415 return;
416 }
417 self.word = match verdict {
418 NodeVerdict::Rejected { reason } => Word::Rejected { reason },
419 NodeVerdict::Conflicted { mut competitors } => {
420 if let Word::Conflicted {
421 competitors: previous,
422 } = &self.word
423 {
424 competitors.extend(previous.iter().cloned());
425 }
426 Word::Conflicted { competitors }
427 }
428 };
429 }
430}
431
432fn agrees(word: &Word, status: &HintStatus) -> bool {
433 match (word, status) {
434 (Word::Announced | Word::Seen, HintStatus::Accepted)
435 | (Word::Seen, HintStatus::Seen)
436 | (Word::Stale, HintStatus::StaleBlock)
437 | (Word::Rejected { .. }, HintStatus::Rejected { .. })
438 | (Word::Conflicted { .. }, HintStatus::DoubleSpend { .. }) => true,
439 (Word::Mined(mined), HintStatus::Mined { height }) => mined.height() == *height,
440 _ => false,
441 }
442}