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 pub fn height(&self) -> Height {
19 self.checked.height()
20 }
21 pub fn checked(&self) -> &CheckedProof {
23 &self.checked
24 }
25}
26#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum Word {
29 Unknown,
30 Built,
31 Announced,
32 Seen,
33 Mined(Mined),
34 Stale,
35 Rejected { reason: String },
36 Conflicted { competitors: Vec<TxId> },
37 Abandoned,
38}
39impl Word {
40 pub fn chain(&self) -> Option<&Self> {
42 match self {
43 Self::Mined(_) | Self::Stale | Self::Rejected { .. } | Self::Conflicted { .. } => {
44 Some(self)
45 }
46 _ => None,
47 }
48 }
49 pub fn height(&self) -> Option<Height> {
51 if let Self::Mined(m) = self {
52 Some(m.height())
53 } else {
54 None
55 }
56 }
57}
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum NodeVerdict {
61 Rejected { reason: String },
62 Conflicted { competitors: Vec<TxId> },
63}
64#[derive(Clone, Debug, PartialEq, Eq)]
66pub enum Evidence {
67 Proof(Proof),
68 Competitor(Proof),
69 Chain(ChainEvent),
70 Verdict(NodeVerdict),
71 Recheck,
72}
73#[derive(Clone, Debug, PartialEq, Eq)]
75pub enum HostAction {
76 Build,
77 Abandon,
78 SpendAttempt,
79 Tick(Timestamp),
80}
81#[derive(Clone, Debug, PartialEq, Eq)]
83pub enum Input {
84 Evidence(Evidence),
85 Hint(Hint),
86 Host(HostAction),
87}
88#[derive(Clone, Debug, PartialEq, Eq)]
90pub enum Reask {
91 Reorg { fork_height: Height },
92 Fork { height: Height },
93 Hint(Hint),
94 ProofFailed { height: Height },
95 Recheck,
96 Age,
97 Spend,
98}
99#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct Params {
102 pub age_threshold: Timestamp,
103}
104#[derive(Clone, Debug, PartialEq, Eq)]
106pub struct State {
107 txid: TxId,
108 word: Word,
109 evidence: Option<Arc<CheckedProof>>,
110 suspect: bool,
111 hints: Vec<Hint>,
112 last_hint_at: Option<Timestamp>,
113 reask: Option<Reask>,
114}
115impl State {
116 pub fn new(txid: impl Into<TxId>) -> Self {
118 Self {
119 txid: txid.into().to_ascii_lowercase(),
120 word: Word::Unknown,
121 evidence: None,
122 suspect: false,
123 hints: vec![],
124 last_hint_at: None,
125 reask: None,
126 }
127 }
128 pub fn txid(&self) -> &str {
130 &self.txid
131 }
132 pub fn word(&self) -> &Word {
134 &self.word
135 }
136 pub fn evidence(&self) -> Option<&CheckedProof> {
138 self.evidence.as_deref()
139 }
140 pub fn suspect(&self) -> bool {
142 self.suspect
143 }
144 pub fn hints(&self) -> &[Hint] {
146 &self.hints
147 }
148 pub fn last_hint_at(&self) -> Option<Timestamp> {
150 self.last_hint_at
151 }
152 pub fn reask(&self) -> Option<&Reask> {
154 self.reask.as_ref()
155 }
156 pub fn step<H: Headers + ?Sized>(
159 &mut self,
160 params: &Params,
161 headers: &H,
162 input: Input,
163 ) -> Result<(), CheckError> {
164 match input {
165 Input::Hint(hint) => self.on_hint(hint),
166 Input::Host(action) => self.on_host(params, action),
167 Input::Evidence(Evidence::Proof(proof)) => self.on_proof(headers, proof)?,
168 Input::Evidence(Evidence::Competitor(proof)) => self.on_competitor(headers, proof)?,
169 Input::Evidence(Evidence::Chain(event)) => self.on_chain(headers, &event)?,
170 Input::Evidence(Evidence::Verdict(verdict)) => self.on_verdict(verdict),
171 Input::Evidence(Evidence::Recheck) => self.recheck(headers)?,
172 }
173 Ok(())
174 }
175
176 fn on_hint(&mut self, hint: Hint) {
177 match (&self.word, &hint.status) {
178 (Word::Built, HintStatus::Accepted) => self.word = Word::Announced,
179 (Word::Built | Word::Announced, HintStatus::Seen) => self.word = Word::Seen,
180 _ => {}
181 }
182 if !agrees(&self.word, &hint.status) {
183 self.reask = Some(Reask::Hint(hint.clone()));
184 }
185 self.last_hint_at = Some(hint.observed);
186 self.hints.insert(0, hint);
187 }
188
189 fn on_host(&mut self, params: &Params, action: HostAction) {
190 match action {
191 HostAction::Build if self.word == Word::Unknown => self.word = Word::Built,
192 HostAction::Abandon if matches!(self.word, Word::Unknown | Word::Built) => {
193 self.word = Word::Abandoned
194 }
195 HostAction::SpendAttempt => self.reask = Some(Reask::Spend),
196 HostAction::Tick(now)
199 if matches!(self.word, Word::Announced | Word::Seen)
200 && self
201 .last_hint_at
202 .and_then(|t| t.checked_add(params.age_threshold))
203 .is_some_and(|deadline| deadline <= now) =>
204 {
205 self.reask = Some(Reask::Age);
206 }
207 _ => {}
208 }
209 }
210
211 fn on_proof<H: Headers + ?Sized>(
212 &mut self,
213 headers: &H,
214 proof: Proof,
215 ) -> Result<(), CheckError> {
216 let height = proof.height();
217 if proof.txid() != self.txid {
218 self.reask = Some(Reask::ProofFailed { height });
219 return Err(CheckError::TxidMismatch);
220 }
221 match crate::evidence::check_snapshot(headers, proof) {
222 Ok(checked) => {
223 let checked = Arc::new(checked);
224 self.word = Word::Mined(Mined {
225 checked: Arc::clone(&checked),
226 });
227 self.evidence = Some(checked);
228 self.suspect = false;
229 self.reask = None;
230 Ok(())
231 }
232 Err(error) => {
233 self.reask = Some(Reask::ProofFailed { height });
234 Err(error)
235 }
236 }
237 }
238
239 fn on_competitor<H: Headers + ?Sized>(
240 &mut self,
241 headers: &H,
242 proof: Proof,
243 ) -> Result<(), CheckError> {
244 let height = proof.height();
245 let competitor = proof.txid().to_owned();
246 match crate::evidence::check_snapshot(headers, proof) {
247 Ok(_) => {
248 match &mut self.word {
249 Word::Mined(_) => self.reask = Some(Reask::Recheck),
250 Word::Conflicted { competitors } => competitors.insert(0, competitor),
251 _ => {
252 self.word = Word::Conflicted {
253 competitors: vec![competitor],
254 }
255 }
256 }
257 Ok(())
258 }
259 Err(error) => {
260 self.reask = Some(Reask::ProofFailed { height });
261 Err(error)
262 }
263 }
264 }
265
266 fn recheck<H: Headers + ?Sized>(&mut self, headers: &H) -> Result<(), CheckError> {
267 let Word::Mined(mined) = &self.word else {
268 return Ok(());
269 };
270 let height = mined.height();
271 let answer = headers.header_at(height);
272 match answer {
273 Ok(Some(header)) if header.merkle_root == mined.checked().root() => {
274 self.suspect = false;
275 self.reask = None;
276 }
277 Ok(Some(_)) => self.stale(height),
278 Ok(None) => {
279 self.reask = Some(Reask::Recheck);
280 return Err(CheckError::Unavailable(height));
281 }
282 Err(error) => {
283 self.reask = Some(Reask::Recheck);
284 return Err(CheckError::Headers(error));
285 }
286 }
287 Ok(())
288 }
289
290 fn stale(&mut self, fork_height: Height) {
291 self.word = Word::Stale;
292 self.suspect = false;
293 self.reask = Some(Reask::Reorg { fork_height });
294 }
295
296 pub(crate) fn on_chain<H: Headers + ?Sized>(
297 &mut self,
298 headers: &H,
299 event: &ChainEvent,
300 ) -> Result<(), CheckError> {
301 match event {
302 ChainEvent::Tip { .. } if self.suspect => self.recheck(headers)?,
303 ChainEvent::Fork { height, .. } if self.word.height().is_some_and(|h| *height <= h) => {
304 self.suspect = true;
305 self.reask = Some(Reask::Fork { height: *height });
306 }
307 ChainEvent::Reorg { fork_height, .. }
308 if self.word.height().is_some_and(|h| *fork_height <= h) =>
309 {
310 self.stale(*fork_height)
311 }
312 ChainEvent::Invalidated { block_hash } => {
313 if let Word::Mined(mined) = &self.word {
314 if &mined.checked().header().hash == block_hash {
315 self.stale(mined.height());
316 }
317 }
318 }
319 _ => {}
320 }
321 Ok(())
322 }
323
324 fn on_verdict(&mut self, verdict: NodeVerdict) {
325 if matches!(self.word, Word::Mined(_)) {
326 self.reask = Some(Reask::Recheck);
327 return;
328 }
329 self.word = match verdict {
330 NodeVerdict::Rejected { reason } => Word::Rejected { reason },
331 NodeVerdict::Conflicted { mut competitors } => {
332 if let Word::Conflicted {
333 competitors: previous,
334 } = &self.word
335 {
336 competitors.extend(previous.iter().cloned());
337 }
338 Word::Conflicted { competitors }
339 }
340 };
341 }
342}
343
344fn agrees(word: &Word, status: &HintStatus) -> bool {
345 match (word, status) {
346 (Word::Announced | Word::Seen, HintStatus::Accepted)
347 | (Word::Seen, HintStatus::Seen)
348 | (Word::Stale, HintStatus::StaleBlock)
349 | (Word::Rejected { .. }, HintStatus::Rejected { .. })
350 | (Word::Conflicted { .. }, HintStatus::DoubleSpend { .. }) => true,
351 (Word::Mined(mined), HintStatus::Mined { height }) => mined.height() == *height,
352 _ => false,
353 }
354}