1use crate::{
2 ChainEvent, CheckError, CheckedProof, CompactError, Headers, Height, Hint, HintStatus, Proof,
3 Timestamp, TxId,
4};
5use std::sync::Arc;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct Mined {
15 checked: Arc<CheckedProof>,
16}
17impl Mined {
18 #[must_use]
20 pub fn height(&self) -> Height {
21 self.checked.height()
22 }
23 #[must_use]
25 pub fn checked(&self) -> &CheckedProof {
26 &self.checked
27 }
28}
29#[derive(Clone, Debug, PartialEq, Eq)]
31pub enum Word {
32 Unknown,
34 Built,
36 Announced,
38 Seen,
40 Mined(Mined),
42 Stale,
44 Rejected {
46 reason: String,
48 },
49 Conflicted {
51 competitors: Vec<TxId>,
53 },
54 Abandoned,
56}
57impl Word {
58 #[must_use]
60 pub fn chain(&self) -> Option<&Self> {
61 match self {
62 Self::Mined(_) | Self::Stale | Self::Rejected { .. } | Self::Conflicted { .. } => {
63 Some(self)
64 }
65 _ => None,
66 }
67 }
68 #[must_use]
70 pub fn height(&self) -> Option<Height> {
71 if let Self::Mined(m) = self {
72 Some(m.height())
73 } else {
74 None
75 }
76 }
77}
78#[derive(Clone, Debug, PartialEq, Eq)]
80pub enum NodeVerdict {
81 Rejected {
83 reason: String,
85 },
86 Conflicted {
88 competitors: Vec<TxId>,
90 },
91}
92#[derive(Clone, Debug, PartialEq, Eq)]
94pub enum Evidence {
95 Proof(Proof),
97 Competitor(Proof),
99 Chain(ChainEvent),
101 Verdict(NodeVerdict),
103 Recheck,
105}
106#[derive(Clone, Debug, PartialEq, Eq)]
108pub enum HostAction {
109 Build,
111 Abandon,
113 SpendAttempt,
115 Tick(Timestamp),
117}
118#[derive(Clone, Debug, PartialEq, Eq)]
120pub enum Input {
121 Evidence(Evidence),
123 Hint(Hint),
125 Host(HostAction),
127}
128#[derive(Clone, Debug, PartialEq, Eq)]
130pub enum Reask {
131 Reorg {
133 fork_height: Height,
136 },
137 Fork {
141 height: Height,
143 },
144 Hint(Hint),
146 ProofFailed {
148 height: Height,
150 },
151 Recheck,
153 Age,
155 Spend,
157}
158#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct Params {
161 pub age_threshold: Timestamp,
163}
164#[derive(Clone, Debug, PartialEq, Eq)]
166pub struct State {
167 txid: TxId,
168 word: Word,
169 evidence: Option<Arc<CheckedProof>>,
170 suspect: bool,
171 hints: Vec<Hint>,
172 last_hint_at: Option<Timestamp>,
173 reask: Option<Reask>,
174}
175impl State {
176 pub fn new(txid: impl Into<TxId>) -> Self {
178 Self {
179 txid: txid.into().to_ascii_lowercase(),
180 word: Word::Unknown,
181 evidence: None,
182 suspect: false,
183 hints: vec![],
184 last_hint_at: None,
185 reask: None,
186 }
187 }
188 #[must_use]
190 pub fn txid(&self) -> &str {
191 &self.txid
192 }
193 #[must_use]
195 pub fn word(&self) -> &Word {
196 &self.word
197 }
198 #[must_use]
200 pub fn evidence(&self) -> Option<&CheckedProof> {
201 self.evidence.as_deref()
202 }
203 #[must_use]
205 pub fn suspect(&self) -> bool {
206 self.suspect
207 }
208 #[must_use]
210 pub fn hints(&self) -> &[Hint] {
211 &self.hints
212 }
213 #[must_use]
215 pub fn last_hint_at(&self) -> Option<Timestamp> {
216 self.last_hint_at
217 }
218 #[must_use]
220 pub fn reask(&self) -> Option<&Reask> {
221 self.reask.as_ref()
222 }
223 #[must_use]
229 pub fn to_compact(&self) -> Vec<u8> {
230 crate::compact::encode(&crate::compact::Fields {
231 txid: &self.txid,
232 word: &self.word,
233 evidence: self.evidence.as_deref(),
234 suspect: self.suspect,
235 hints: &self.hints,
236 last_hint_at: self.last_hint_at,
237 reask: self.reask.as_ref(),
238 })
239 }
240 pub fn from_compact(bytes: &[u8]) -> Result<Self, CompactError> {
268 let parts = crate::compact::decode(bytes)?;
269 let evidence = parts.evidence.map(Arc::new);
270 let mined = parts.word.is_none();
271 let word = if let Some(word) = parts.word {
272 word
273 } else {
274 let checked = evidence
275 .as_ref()
276 .ok_or(CompactError::Malformed("mined word with no stored proof"))?;
277 Word::Mined(Mined {
278 checked: Arc::clone(checked),
279 })
280 };
281 let mut state = Self {
282 txid: parts.txid,
283 word,
284 evidence,
285 suspect: parts.suspect,
286 hints: parts.hints,
287 last_hint_at: parts.last_hint_at,
288 reask: parts.reask,
289 };
290 if mined {
291 state.suspect = true;
292 state.reask.get_or_insert(Reask::Recheck);
293 }
294 Ok(state)
295 }
296 pub fn step<H: Headers + ?Sized>(
304 &mut self,
305 params: &Params,
306 headers: &H,
307 input: Input,
308 ) -> Result<(), CheckError> {
309 match input {
310 Input::Hint(hint) => self.on_hint(hint),
311 Input::Host(action) => self.on_host(params, &action),
312 Input::Evidence(Evidence::Proof(proof)) => self.on_proof(headers, proof)?,
313 Input::Evidence(Evidence::Competitor(proof)) => self.on_competitor(headers, proof)?,
314 Input::Evidence(Evidence::Chain(event)) => self.on_chain(headers, &event)?,
315 Input::Evidence(Evidence::Verdict(verdict)) => self.on_verdict(verdict),
316 Input::Evidence(Evidence::Recheck) => self.recheck(headers)?,
317 }
318 Ok(())
319 }
320
321 fn on_hint(&mut self, hint: Hint) {
322 match (&self.word, &hint.status) {
323 (Word::Built, HintStatus::Accepted) => self.word = Word::Announced,
324 (Word::Built | Word::Announced, HintStatus::Seen) => self.word = Word::Seen,
325 _ => {}
326 }
327 if !agrees(&self.word, &hint.status) {
328 self.reask = Some(Reask::Hint(hint.clone()));
329 }
330 self.last_hint_at = Some(hint.observed);
331 self.hints.insert(0, hint);
332 }
333
334 fn on_host(&mut self, params: &Params, action: &HostAction) {
335 match *action {
336 HostAction::Build if self.word == Word::Unknown => self.word = Word::Built,
337 HostAction::Abandon if matches!(self.word, Word::Unknown | Word::Built) => {
338 self.word = Word::Abandoned;
339 }
340 HostAction::SpendAttempt => self.reask = Some(Reask::Spend),
341 HostAction::Tick(now)
344 if matches!(self.word, Word::Announced | Word::Seen)
345 && self
346 .last_hint_at
347 .and_then(|t| t.checked_add(params.age_threshold))
348 .is_some_and(|deadline| deadline <= now) =>
349 {
350 self.reask = Some(Reask::Age);
351 }
352 _ => {}
353 }
354 }
355
356 fn on_proof<H: Headers + ?Sized>(
357 &mut self,
358 headers: &H,
359 proof: Proof,
360 ) -> Result<(), CheckError> {
361 let height = proof.height();
362 if proof.txid() != self.txid {
363 self.reask = Some(Reask::ProofFailed { height });
364 return Err(CheckError::TxidMismatch);
365 }
366 match crate::evidence::check_snapshot(headers, proof) {
367 Ok(checked) => {
368 let checked = Arc::new(checked);
369 self.word = Word::Mined(Mined {
370 checked: Arc::clone(&checked),
371 });
372 self.evidence = Some(checked);
373 self.suspect = false;
374 self.reask = None;
375 Ok(())
376 }
377 Err(error) => {
378 self.reask = Some(Reask::ProofFailed { height });
379 Err(error)
380 }
381 }
382 }
383
384 fn on_competitor<H: Headers + ?Sized>(
385 &mut self,
386 headers: &H,
387 proof: Proof,
388 ) -> Result<(), CheckError> {
389 let height = proof.height();
390 let competitor = proof.txid().to_owned();
391 match crate::evidence::check_snapshot(headers, proof) {
392 Ok(_) => {
393 match &mut self.word {
394 Word::Mined(_) => self.reask = Some(Reask::Recheck),
395 Word::Conflicted { competitors } => competitors.insert(0, competitor),
396 _ => {
397 self.word = Word::Conflicted {
398 competitors: vec![competitor],
399 }
400 }
401 }
402 Ok(())
403 }
404 Err(error) => {
405 self.reask = Some(Reask::ProofFailed { height });
406 Err(error)
407 }
408 }
409 }
410
411 fn recheck<H: Headers + ?Sized>(&mut self, headers: &H) -> Result<(), CheckError> {
412 let Word::Mined(mined) = &self.word else {
413 return Ok(());
414 };
415 let height = mined.height();
416 let answer = headers.header_at(height);
417 match answer {
418 Ok(Some(header)) if header.merkle_root == mined.checked().root() => {
419 self.suspect = false;
420 self.reask = None;
421 }
422 Ok(Some(_)) => self.stale(height),
423 Ok(None) => {
424 self.reask = Some(Reask::Recheck);
425 return Err(CheckError::Unavailable(height));
426 }
427 Err(error) => {
428 self.reask = Some(Reask::Recheck);
429 return Err(CheckError::Headers(error));
430 }
431 }
432 Ok(())
433 }
434
435 fn stale(&mut self, fork_height: Height) {
436 self.word = Word::Stale;
437 self.suspect = false;
438 self.reask = Some(Reask::Reorg { fork_height });
439 }
440
441 pub(crate) fn on_chain<H: Headers + ?Sized>(
442 &mut self,
443 headers: &H,
444 event: &ChainEvent,
445 ) -> Result<(), CheckError> {
446 match event {
447 ChainEvent::Tip { .. } if self.suspect => self.recheck(headers)?,
448 ChainEvent::Fork { height, .. } if self.word.height().is_some_and(|h| *height <= h) => {
449 self.suspect = true;
450 self.reask = Some(Reask::Fork { height: *height });
451 }
452 ChainEvent::Reorg { fork_height, .. }
453 if self.word.height().is_some_and(|h| *fork_height <= h) =>
454 {
455 self.stale(*fork_height);
456 }
457 ChainEvent::Split { fork_height, .. }
461 if self.word.height().is_some_and(|h| *fork_height <= h) =>
462 {
463 self.suspect = true;
464 self.reask = Some(Reask::Fork {
465 height: *fork_height,
466 });
467 }
468 ChainEvent::SplitResolved { fork_height, .. }
470 if self.suspect && self.word.height().is_some_and(|h| *fork_height <= h) =>
471 {
472 self.recheck(headers)?;
473 }
474 ChainEvent::Invalidated { block_hash } => {
475 if let Word::Mined(mined) = &self.word {
476 if &mined.checked().header().hash == block_hash {
477 self.stale(mined.height());
478 }
479 }
480 }
481 _ => {}
482 }
483 Ok(())
484 }
485
486 fn on_verdict(&mut self, verdict: NodeVerdict) {
487 if matches!(self.word, Word::Mined(_)) {
488 self.reask = Some(Reask::Recheck);
489 return;
490 }
491 self.word = match verdict {
492 NodeVerdict::Rejected { reason } => Word::Rejected { reason },
493 NodeVerdict::Conflicted { mut competitors } => {
494 if let Word::Conflicted {
495 competitors: previous,
496 } = &self.word
497 {
498 competitors.extend(previous.iter().cloned());
499 }
500 Word::Conflicted { competitors }
501 }
502 };
503 }
504}
505
506fn agrees(word: &Word, status: &HintStatus) -> bool {
507 match (word, status) {
508 (
509 Word::Announced | Word::Seen,
510 HintStatus::Accepted | HintStatus::MempoolPresent { .. },
511 )
512 | (Word::Seen, HintStatus::Seen)
513 | (Word::Stale, HintStatus::StaleBlock)
514 | (Word::Rejected { .. }, HintStatus::Rejected { .. })
515 | (Word::Conflicted { .. }, HintStatus::DoubleSpend { .. }) => true,
516 (Word::Mined(mined), HintStatus::Mined { height }) => mined.height() == *height,
517 _ => false,
518 }
519}