1use std::fmt;
2
3use crate::{Hash, Height};
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase", deny_unknown_fields)]
12pub struct EventHeader {
13 pub version: u32,
15 pub previous_hash: Hash,
17 pub merkle_root: Hash,
19 pub time: u32,
21 pub bits: u32,
23 pub nonce: u32,
25 pub height: Height,
27 pub hash: Hash,
29 pub chain_work: String,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct Outpoint {
38 pub txid: Hash,
40 pub vout: u32,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase", deny_unknown_fields)]
49pub struct SplitTip {
50 pub hash: Hash,
52 pub height: Height,
54 pub chain_work: String,
56 pub rungs: Vec<String>,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
66pub enum ChainEvent {
67 Tip {
69 height: Height,
71 hash: Hash,
73 time: u32,
75 header: EventHeader,
77 },
78 Fork {
80 height: Height,
82 competing_tips: [EventHeader; 2],
84 depth: u32,
86 },
87 Reorg {
89 fork_height: Height,
91 depth: u32,
93 deactivated_headers: Vec<EventHeader>,
95 new_tip: EventHeader,
97 },
98 Invalidated {
100 block_hash: Hash,
102 },
103 Frozen {
105 outpoint: Outpoint,
107 },
108 TipAge {
110 seconds: u64,
112 tip: EventHeader,
114 },
115 Split {
118 key: String,
120 fork_height: Height,
122 tips: [SplitTip; 2],
124 },
125 SplitResolved {
127 key: String,
129 fork_height: Height,
131 },
132}
133
134impl ChainEvent {
135 #[must_use]
139 pub fn version(&self) -> u64 {
140 match self {
141 Self::Split { .. } | Self::SplitResolved { .. } => 2,
142 _ => 1,
143 }
144 }
145}
146
147#[derive(Serialize, Deserialize)]
150#[serde(
151 remote = "ChainEvent",
152 tag = "kind",
153 rename_all = "camelCase",
154 deny_unknown_fields
155)]
156enum EventFields {
157 Tip {
158 height: Height,
159 hash: Hash,
160 time: u32,
161 header: EventHeader,
162 },
163 Fork {
164 height: Height,
165 #[serde(rename = "competingTips")]
166 competing_tips: [EventHeader; 2],
167 depth: u32,
168 },
169 Reorg {
170 #[serde(rename = "forkHeight")]
171 fork_height: Height,
172 depth: u32,
173 #[serde(rename = "deactivatedHeaders")]
174 deactivated_headers: Vec<EventHeader>,
175 #[serde(rename = "newTip")]
176 new_tip: EventHeader,
177 },
178 Invalidated {
179 #[serde(rename = "blockHash")]
180 block_hash: Hash,
181 },
182 Frozen {
183 outpoint: Outpoint,
184 },
185 TipAge {
186 seconds: u64,
187 tip: EventHeader,
188 },
189 Split {
190 key: String,
191 #[serde(rename = "forkHeight")]
192 fork_height: Height,
193 tips: [SplitTip; 2],
194 },
195 SplitResolved {
196 key: String,
197 #[serde(rename = "forkHeight")]
198 fork_height: Height,
199 },
200}
201
202impl Serialize for ChainEvent {
203 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
204 #[derive(Serialize)]
205 struct EnvelopeRef<'a> {
206 v: u64,
207 #[serde(flatten, with = "EventFields")]
208 event: &'a ChainEvent,
209 }
210 EnvelopeRef {
211 v: self.version(),
212 event: self,
213 }
214 .serialize(serializer)
215 }
216}
217
218impl<'de> Deserialize<'de> for ChainEvent {
219 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
220 let value = serde_json::Value::deserialize(deserializer)?;
221 decode_value(value).map_err(serde::de::Error::custom)
222 }
223}
224
225#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
228pub struct ChainEnvelope {
229 v: u64,
231 #[serde(flatten, with = "EventFields")]
233 event: ChainEvent,
234}
235
236impl ChainEnvelope {
237 #[must_use]
239 pub fn new(event: ChainEvent) -> Self {
240 Self {
241 v: event.version(),
242 event,
243 }
244 }
245 #[must_use]
247 pub fn version(&self) -> u64 {
248 self.v
249 }
250 #[must_use]
252 pub fn event(&self) -> &ChainEvent {
253 &self.event
254 }
255 #[must_use]
257 pub fn into_event(self) -> ChainEvent {
258 self.event
259 }
260}
261
262impl<'de> Deserialize<'de> for ChainEnvelope {
263 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
264 ChainEvent::deserialize(deserializer).map(Self::new)
265 }
266}
267
268#[derive(Clone, Debug, PartialEq, Eq)]
270pub enum DecodeError {
271 UnknownShape {
273 version: Option<u64>,
275 detail: String,
277 },
278 UnknownVersion(u64),
280 UnknownKind(String),
282}
283impl fmt::Display for DecodeError {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 write!(f, "{self:?}")
286 }
287}
288impl std::error::Error for DecodeError {}
289
290pub fn decode_envelope(bytes: &[u8]) -> Result<ChainEvent, DecodeError> {
303 let value: serde_json::Value =
304 serde_json::from_slice(bytes).map_err(|e| DecodeError::UnknownShape {
305 version: None,
306 detail: e.to_string(),
307 })?;
308 decode_value(value)
309}
310
311fn decode_value(mut value: serde_json::Value) -> Result<ChainEvent, DecodeError> {
312 let version = value.get("v").and_then(serde_json::Value::as_u64);
313 let Some(v) = version else {
314 return Err(DecodeError::UnknownShape {
315 version,
316 detail: "missing or malformed v".into(),
317 });
318 };
319 if !matches!(v, 1 | 2) {
320 return Err(DecodeError::UnknownVersion(v));
321 }
322 if let Some(object) = value.as_object_mut() {
324 object.remove("v");
325 }
326 let kind = value
327 .get("kind")
328 .and_then(serde_json::Value::as_str)
329 .ok_or_else(|| DecodeError::UnknownShape {
330 version: Some(v),
331 detail: "missing or malformed kind".into(),
332 })?;
333 let known = match v {
338 1 => matches!(
339 kind,
340 "tip" | "fork" | "reorg" | "invalidated" | "frozen" | "tipAge"
341 ),
342 _ => matches!(kind, "split" | "splitResolved"),
343 };
344 if !known {
345 return Err(DecodeError::UnknownKind(kind.into()));
346 }
347 let event = EventFields::deserialize(value).map_err(|e| DecodeError::UnknownShape {
348 version: Some(v),
349 detail: e.to_string(),
350 })?;
351 if !valid_event(&event) {
352 return Err(DecodeError::UnknownShape {
353 version: Some(v),
354 detail: "inconsistent chain event".into(),
355 });
356 }
357 Ok(event)
358}
359
360fn valid_event(event: &ChainEvent) -> bool {
366 match event {
367 ChainEvent::Tip {
368 height,
369 hash,
370 time,
371 header,
372 } => {
373 valid_header(header)
374 && *height == header.height
375 && *hash == header.hash
376 && *time == header.time
377 }
378 ChainEvent::Fork {
379 height,
380 competing_tips,
381 depth,
382 } => {
383 *depth > 0
384 && competing_tips.iter().all(valid_header)
385 && competing_tips[0].hash != competing_tips[1].hash
386 && competing_tips[0]
387 .height
388 .checked_sub(*depth)
389 .and_then(|h| h.checked_add(1))
390 == Some(*height)
391 }
392 ChainEvent::Reorg {
393 fork_height,
394 depth,
395 deactivated_headers,
396 new_tip,
397 } => {
398 *depth > 0
399 && *depth <= 400
400 && deactivated_headers.len() == *depth as usize
401 && valid_header(new_tip)
402 && deactivated_headers.iter().all(valid_header)
403 && deactivated_headers
404 .last()
405 .is_some_and(|h| h.height == *fork_height)
406 && deactivated_headers
407 .windows(2)
408 .all(|p| p[0].height == p[1].height + 1 && p[0].previous_hash == p[1].hash)
409 }
410 ChainEvent::Invalidated { block_hash } => valid_hash(block_hash),
411 ChainEvent::Frozen { outpoint } => valid_hash(&outpoint.txid),
412 ChainEvent::TipAge { tip, .. } => valid_header(tip),
413 ChainEvent::Split {
417 key,
418 fork_height,
419 tips,
420 } => {
421 valid_split_key(key, *fork_height)
422 && tips.iter().all(|tip| valid_split_tip(tip, *fork_height))
423 && tips[0].hash != tips[1].hash
424 && tips[0]
425 .rungs
426 .iter()
427 .all(|name| !tips[1].rungs.contains(name))
428 }
429 ChainEvent::SplitResolved { key, fork_height } => valid_split_key(key, *fork_height),
431 }
432}
433
434fn valid_split_key(key: &str, fork_height: Height) -> bool {
437 fork_height > 0
438 && i32::try_from(fork_height).is_ok()
439 && key.strip_prefix("split:").is_some_and(valid_hash)
440}
441
442fn valid_split_tip(tip: &SplitTip, fork_height: Height) -> bool {
444 valid_hash(&tip.hash)
445 && valid_hash(&tip.chain_work)
446 && tip.height >= fork_height
447 && i32::try_from(tip.height).is_ok()
448 && !tip.rungs.is_empty()
449 && tip.rungs.iter().all(|name| !name.is_empty())
450 && tip.rungs.windows(2).all(|names| names[0] < names[1])
451}
452
453fn valid_header(header: &EventHeader) -> bool {
454 i32::try_from(header.height).is_ok()
455 && valid_hash(&header.hash)
456 && valid_hash(&header.previous_hash)
457 && valid_hash(&header.merkle_root)
458 && valid_hash(&header.chain_work)
459}
460
461fn valid_hash(hash: &str) -> bool {
462 hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_hexdigit())
463}