Skip to main content

bsv_tracker/
events.rs

1use std::fmt;
2
3use crate::{Hash, Height};
4use serde::{Deserialize, Serialize};
5
6/// The eight public header fields and cumulative work in the #32 envelope.
7/// \[SRC\] rust-chaintracks@a62f9ed docs/CHAIN-EVENTS.md:13-23;
8/// src/events.rs:13-25 fixes the Rust integer widths.
9/// These are wire data, never a checked inclusion capability.
10#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase", deny_unknown_fields)]
12pub struct EventHeader {
13    pub version: u32,
14    pub previous_hash: Hash,
15    pub merkle_root: Hash,
16    pub time: u32,
17    pub bits: u32,
18    pub nonce: u32,
19    pub height: Height,
20    pub hash: Hash,
21    pub chain_work: String,
22}
23
24/// A frozen output's display-order txid and output index.
25/// \[SRC\] rust-chaintracks@a62f9ed src/events.rs:43-48.
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct Outpoint {
29    pub txid: Hash,
30    pub vout: u32,
31}
32
33/// The six #32 events, serialized and deserialized with `v: 1` and `kind`.
34/// \[SRC\] rust-chaintracks@a62f9ed docs/CHAIN-EVENTS.md:24-35.
35/// For typed host faults, use [`decode_envelope`].
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub enum ChainEvent {
38    /// A new active tip. Only suspect transactions need a recheck.
39    Tip {
40        height: Height,
41        hash: Hash,
42        time: u32,
43        header: EventHeader,
44    },
45    /// A fork that has not resolved.
46    Fork {
47        height: Height,
48        competing_tips: [EventHeader; 2],
49        depth: u32,
50    },
51    /// Blocks at or above a fork point have been replaced.
52    Reorg {
53        fork_height: Height,
54        depth: u32,
55        deactivated_headers: Vec<EventHeader>,
56        new_tip: EventHeader,
57    },
58    /// The alert system invalidated a block.
59    Invalidated { block_hash: Hash },
60    /// A frozen output. The wallet owns its meaning; it moves no tracker word.
61    Frozen { outpoint: Outpoint },
62    /// Tip age. The header snapshot implements the fail closed response.
63    TipAge { seconds: u64, tip: EventHeader },
64}
65
66// Remote serde derives keep the unversioned body private. Both public decode
67// paths pass through decode_value before a ChainEvent can be delivered.
68#[derive(Serialize, Deserialize)]
69#[serde(
70    remote = "ChainEvent",
71    tag = "kind",
72    rename_all = "camelCase",
73    deny_unknown_fields
74)]
75enum EventFields {
76    Tip {
77        height: Height,
78        hash: Hash,
79        time: u32,
80        header: EventHeader,
81    },
82    Fork {
83        height: Height,
84        #[serde(rename = "competingTips")]
85        competing_tips: [EventHeader; 2],
86        depth: u32,
87    },
88    Reorg {
89        #[serde(rename = "forkHeight")]
90        fork_height: Height,
91        depth: u32,
92        #[serde(rename = "deactivatedHeaders")]
93        deactivated_headers: Vec<EventHeader>,
94        #[serde(rename = "newTip")]
95        new_tip: EventHeader,
96    },
97    Invalidated {
98        #[serde(rename = "blockHash")]
99        block_hash: Hash,
100    },
101    Frozen {
102        outpoint: Outpoint,
103    },
104    TipAge {
105        seconds: u64,
106        tip: EventHeader,
107    },
108}
109
110impl Serialize for ChainEvent {
111    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
112        #[derive(Serialize)]
113        struct EnvelopeRef<'a> {
114            v: u64,
115            #[serde(flatten, with = "EventFields")]
116            event: &'a ChainEvent,
117        }
118        EnvelopeRef { v: 1, event: self }.serialize(serializer)
119    }
120}
121
122impl<'de> Deserialize<'de> for ChainEvent {
123    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
124        let value = serde_json::Value::deserialize(deserializer)?;
125        decode_value(value).map_err(serde::de::Error::custom)
126    }
127}
128
129/// The flat version 1 chain-event envelope from rust-chaintracks@a62f9ed.
130#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
131pub struct ChainEnvelope {
132    /// The envelope version; currently exactly 1.
133    v: u64,
134    /// The event's kind and fields, flattened beside the version.
135    #[serde(flatten, with = "EventFields")]
136    event: ChainEvent,
137}
138
139impl ChainEnvelope {
140    /// Wrap an event in the supported version.
141    pub fn new(event: ChainEvent) -> Self {
142        Self { v: 1, event }
143    }
144    /// The validated wire version.
145    pub fn version(&self) -> u64 {
146        self.v
147    }
148    /// The decoded event.
149    pub fn event(&self) -> &ChainEvent {
150        &self.event
151    }
152    /// Consume the envelope to deliver its decoded event.
153    pub fn into_event(self) -> ChainEvent {
154        self.event
155    }
156}
157
158impl<'de> Deserialize<'de> for ChainEnvelope {
159    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
160        ChainEvent::deserialize(deserializer).map(Self::new)
161    }
162}
163
164/// A fault the host must report, without feeding an event to the tracker.
165#[derive(Clone, Debug, PartialEq, Eq)]
166pub enum DecodeError {
167    /// A missing, malformed or unrecognized body.
168    UnknownShape {
169        version: Option<u64>,
170        detail: String,
171    },
172    /// The sender uses a version this client does not know.
173    UnknownVersion(u64),
174    /// The sender uses an event kind this client does not know.
175    UnknownKind(String),
176}
177impl fmt::Display for DecodeError {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        write!(f, "{self:?}")
180    }
181}
182impl std::error::Error for DecodeError {}
183
184/// Pure decoding, with the version gated before the shape, independent of JSON
185/// key order. Unknown fields, kinds and versions are reported errors.
186pub fn decode_envelope(bytes: &[u8]) -> Result<ChainEvent, DecodeError> {
187    let value: serde_json::Value =
188        serde_json::from_slice(bytes).map_err(|e| DecodeError::UnknownShape {
189            version: None,
190            detail: e.to_string(),
191        })?;
192    decode_value(value)
193}
194
195fn decode_value(mut value: serde_json::Value) -> Result<ChainEvent, DecodeError> {
196    let version = value.get("v").and_then(serde_json::Value::as_u64);
197    let Some(v) = version else {
198        return Err(DecodeError::UnknownShape {
199            version,
200            detail: "missing or malformed v".into(),
201        });
202    };
203    if v != 1 {
204        return Err(DecodeError::UnknownVersion(v));
205    }
206    value
207        .as_object_mut()
208        .expect("v was read from an object")
209        .remove("v");
210    let kind = value
211        .get("kind")
212        .and_then(serde_json::Value::as_str)
213        .ok_or_else(|| DecodeError::UnknownShape {
214            version: Some(v),
215            detail: "missing or malformed kind".into(),
216        })?;
217    if !matches!(
218        kind,
219        "tip" | "fork" | "reorg" | "invalidated" | "frozen" | "tipAge"
220    ) {
221        return Err(DecodeError::UnknownKind(kind.into()));
222    }
223    let event = EventFields::deserialize(value).map_err(|e| DecodeError::UnknownShape {
224        version: Some(v),
225        detail: e.to_string(),
226    })?;
227    if !valid_event(&event) {
228        return Err(DecodeError::UnknownShape {
229            version: Some(v),
230            detail: "inconsistent chain event".into(),
231        });
232    }
233    Ok(event)
234}
235
236// Match the emitter's shape/relationship checks, without establishing PoW,
237// active-chain membership or inclusion. [SRC] rust-chaintracks@a62f9ed
238// src/events.rs:125-185. The tracker still obtains evidence via Headers::check.
239fn valid_event(event: &ChainEvent) -> bool {
240    match event {
241        ChainEvent::Tip {
242            height,
243            hash,
244            time,
245            header,
246        } => {
247            valid_header(header)
248                && *height == header.height
249                && *hash == header.hash
250                && *time == header.time
251        }
252        ChainEvent::Fork {
253            height,
254            competing_tips,
255            depth,
256        } => {
257            *depth > 0
258                && competing_tips.iter().all(valid_header)
259                && competing_tips[0].hash != competing_tips[1].hash
260                && competing_tips[0]
261                    .height
262                    .checked_sub(*depth)
263                    .and_then(|h| h.checked_add(1))
264                    == Some(*height)
265        }
266        ChainEvent::Reorg {
267            fork_height,
268            depth,
269            deactivated_headers,
270            new_tip,
271        } => {
272            *depth > 0
273                && *depth <= 400
274                && deactivated_headers.len() == *depth as usize
275                && valid_header(new_tip)
276                && deactivated_headers.iter().all(valid_header)
277                && deactivated_headers
278                    .last()
279                    .is_some_and(|h| h.height == *fork_height)
280                && deactivated_headers
281                    .windows(2)
282                    .all(|p| p[0].height == p[1].height + 1 && p[0].previous_hash == p[1].hash)
283        }
284        ChainEvent::Invalidated { block_hash } => valid_hash(block_hash),
285        ChainEvent::Frozen { outpoint } => valid_hash(&outpoint.txid),
286        ChainEvent::TipAge { tip, .. } => valid_header(tip),
287    }
288}
289
290fn valid_header(header: &EventHeader) -> bool {
291    header.height <= i32::MAX as u32
292        && valid_hash(&header.hash)
293        && valid_hash(&header.previous_hash)
294        && valid_hash(&header.merkle_root)
295        && valid_hash(&header.chain_work)
296}
297
298fn valid_hash(hash: &str) -> bool {
299    hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_hexdigit())
300}