use std::fmt;
use crate::{Hash, Height};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EventHeader {
pub version: u32,
pub previous_hash: Hash,
pub merkle_root: Hash,
pub time: u32,
pub bits: u32,
pub nonce: u32,
pub height: Height,
pub hash: Hash,
pub chain_work: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Outpoint {
pub txid: Hash,
pub vout: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SplitTip {
pub hash: Hash,
pub height: Height,
pub chain_work: String,
pub rungs: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ChainEvent {
Tip {
height: Height,
hash: Hash,
time: u32,
header: EventHeader,
},
Fork {
height: Height,
competing_tips: [EventHeader; 2],
depth: u32,
},
Reorg {
fork_height: Height,
depth: u32,
deactivated_headers: Vec<EventHeader>,
new_tip: EventHeader,
},
Invalidated {
block_hash: Hash,
},
Frozen {
outpoint: Outpoint,
},
TipAge {
seconds: u64,
tip: EventHeader,
},
Split {
key: String,
fork_height: Height,
tips: [SplitTip; 2],
},
SplitResolved {
key: String,
fork_height: Height,
},
}
impl ChainEvent {
#[must_use]
pub fn version(&self) -> u64 {
match self {
Self::Split { .. } | Self::SplitResolved { .. } => 2,
_ => 1,
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(
remote = "ChainEvent",
tag = "kind",
rename_all = "camelCase",
deny_unknown_fields
)]
enum EventFields {
Tip {
height: Height,
hash: Hash,
time: u32,
header: EventHeader,
},
Fork {
height: Height,
#[serde(rename = "competingTips")]
competing_tips: [EventHeader; 2],
depth: u32,
},
Reorg {
#[serde(rename = "forkHeight")]
fork_height: Height,
depth: u32,
#[serde(rename = "deactivatedHeaders")]
deactivated_headers: Vec<EventHeader>,
#[serde(rename = "newTip")]
new_tip: EventHeader,
},
Invalidated {
#[serde(rename = "blockHash")]
block_hash: Hash,
},
Frozen {
outpoint: Outpoint,
},
TipAge {
seconds: u64,
tip: EventHeader,
},
Split {
key: String,
#[serde(rename = "forkHeight")]
fork_height: Height,
tips: [SplitTip; 2],
},
SplitResolved {
key: String,
#[serde(rename = "forkHeight")]
fork_height: Height,
},
}
impl Serialize for ChainEvent {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(Serialize)]
struct EnvelopeRef<'a> {
v: u64,
#[serde(flatten, with = "EventFields")]
event: &'a ChainEvent,
}
EnvelopeRef {
v: self.version(),
event: self,
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ChainEvent {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
decode_value(value).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ChainEnvelope {
v: u64,
#[serde(flatten, with = "EventFields")]
event: ChainEvent,
}
impl ChainEnvelope {
#[must_use]
pub fn new(event: ChainEvent) -> Self {
Self {
v: event.version(),
event,
}
}
#[must_use]
pub fn version(&self) -> u64 {
self.v
}
#[must_use]
pub fn event(&self) -> &ChainEvent {
&self.event
}
#[must_use]
pub fn into_event(self) -> ChainEvent {
self.event
}
}
impl<'de> Deserialize<'de> for ChainEnvelope {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
ChainEvent::deserialize(deserializer).map(Self::new)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DecodeError {
UnknownShape {
version: Option<u64>,
detail: String,
},
UnknownVersion(u64),
UnknownKind(String),
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for DecodeError {}
pub fn decode_envelope(bytes: &[u8]) -> Result<ChainEvent, DecodeError> {
let value: serde_json::Value =
serde_json::from_slice(bytes).map_err(|e| DecodeError::UnknownShape {
version: None,
detail: e.to_string(),
})?;
decode_value(value)
}
fn decode_value(mut value: serde_json::Value) -> Result<ChainEvent, DecodeError> {
let version = value.get("v").and_then(serde_json::Value::as_u64);
let Some(v) = version else {
return Err(DecodeError::UnknownShape {
version,
detail: "missing or malformed v".into(),
});
};
if !matches!(v, 1 | 2) {
return Err(DecodeError::UnknownVersion(v));
}
if let Some(object) = value.as_object_mut() {
object.remove("v");
}
let kind = value
.get("kind")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| DecodeError::UnknownShape {
version: Some(v),
detail: "missing or malformed kind".into(),
})?;
let known = match v {
1 => matches!(
kind,
"tip" | "fork" | "reorg" | "invalidated" | "frozen" | "tipAge"
),
_ => matches!(kind, "split" | "splitResolved"),
};
if !known {
return Err(DecodeError::UnknownKind(kind.into()));
}
let event = EventFields::deserialize(value).map_err(|e| DecodeError::UnknownShape {
version: Some(v),
detail: e.to_string(),
})?;
if !valid_event(&event) {
return Err(DecodeError::UnknownShape {
version: Some(v),
detail: "inconsistent chain event".into(),
});
}
Ok(event)
}
fn valid_event(event: &ChainEvent) -> bool {
match event {
ChainEvent::Tip {
height,
hash,
time,
header,
} => {
valid_header(header)
&& *height == header.height
&& *hash == header.hash
&& *time == header.time
}
ChainEvent::Fork {
height,
competing_tips,
depth,
} => {
*depth > 0
&& competing_tips.iter().all(valid_header)
&& competing_tips[0].hash != competing_tips[1].hash
&& competing_tips[0]
.height
.checked_sub(*depth)
.and_then(|h| h.checked_add(1))
== Some(*height)
}
ChainEvent::Reorg {
fork_height,
depth,
deactivated_headers,
new_tip,
} => {
*depth > 0
&& *depth <= 400
&& deactivated_headers.len() == *depth as usize
&& valid_header(new_tip)
&& deactivated_headers.iter().all(valid_header)
&& deactivated_headers
.last()
.is_some_and(|h| h.height == *fork_height)
&& deactivated_headers
.windows(2)
.all(|p| p[0].height == p[1].height + 1 && p[0].previous_hash == p[1].hash)
}
ChainEvent::Invalidated { block_hash } => valid_hash(block_hash),
ChainEvent::Frozen { outpoint } => valid_hash(&outpoint.txid),
ChainEvent::TipAge { tip, .. } => valid_header(tip),
ChainEvent::Split {
key,
fork_height,
tips,
} => {
valid_split_key(key, *fork_height)
&& tips.iter().all(|tip| valid_split_tip(tip, *fork_height))
&& tips[0].hash != tips[1].hash
&& tips[0]
.rungs
.iter()
.all(|name| !tips[1].rungs.contains(name))
}
ChainEvent::SplitResolved { key, fork_height } => valid_split_key(key, *fork_height),
}
}
fn valid_split_key(key: &str, fork_height: Height) -> bool {
fork_height > 0
&& i32::try_from(fork_height).is_ok()
&& key.strip_prefix("split:").is_some_and(valid_hash)
}
fn valid_split_tip(tip: &SplitTip, fork_height: Height) -> bool {
valid_hash(&tip.hash)
&& valid_hash(&tip.chain_work)
&& tip.height >= fork_height
&& i32::try_from(tip.height).is_ok()
&& !tip.rungs.is_empty()
&& tip.rungs.iter().all(|name| !name.is_empty())
&& tip.rungs.windows(2).all(|names| names[0] < names[1])
}
fn valid_header(header: &EventHeader) -> bool {
i32::try_from(header.height).is_ok()
&& valid_hash(&header.hash)
&& valid_hash(&header.previous_hash)
&& valid_hash(&header.merkle_root)
&& valid_hash(&header.chain_work)
}
fn valid_hash(hash: &str) -> bool {
hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_hexdigit())
}