dig_network_block/
header.rs

1//! L2 block header: metadata and commitments to the body.
2//!
3//! The header owns calculation of its `HEADER_ROOT`, which is a Merkle root of
4//! individually domain-separated header fields. This allows proving single
5//! fields against the overall `BLOCK_ROOT` without revealing the entire header.
6
7use crate::dig_l2_definition as definitions;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11/// Header for an L2 block.
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct L2BlockHeader {
14    /// Block version; must match network consensus version.
15    pub version: u32,
16    /// Network ID (32 bytes), serialized as `0x` hex.
17    #[serde(with = "crate::serde_hex::hex32")]
18    pub network_id: [u8; 32],
19    /// Epoch number.
20    pub epoch: u64,
21    /// Previous block root (32 bytes), serialized as `0x` hex.
22    #[serde(with = "crate::serde_hex::hex32")]
23    pub prev_block_root: [u8; 32],
24    /// Body root (32 bytes), serialized as `0x` hex.
25    #[serde(with = "crate::serde_hex::hex32")]
26    pub body_root: [u8; 32],
27    /// Count of data items (bytes) in the body.
28    pub data_count: u32,
29    /// Count of emissions in the body.
30    pub emissions_count: u32,
31    /// Proposer public key (48 bytes), serialized as `0x` hex.
32    #[serde(with = "crate::serde_hex::hex48")]
33    pub proposer_pubkey: [u8; 48],
34}
35
36impl L2BlockHeader {
37    /// Calculates the `HEADER_ROOT` using the spec function.
38    pub fn calculate_root(&self) -> definitions::Hash32 {
39        definitions::COMPUTE_HEADER_ROOT(self)
40    }
41
42    /// Validates that the header version matches the expected consensus version.
43    pub fn validate_version(&self, expected_version: u32) -> Result<(), HeaderError> {
44        if self.version != expected_version {
45            return Err(HeaderError::VersionMismatch {
46                expected: expected_version,
47                found: self.version,
48            });
49        }
50        Ok(())
51    }
52
53    /// Validates that `data_count` and `emissions_count` match the provided body lengths.
54    pub fn validate_counts(
55        &self,
56        data_len: usize,
57        emissions_len: usize,
58    ) -> Result<(), HeaderError> {
59        if self.data_count as usize != data_len {
60            return Err(HeaderError::CountMismatch {
61                field: "data_count",
62                expected: self.data_count as usize,
63                actual: data_len,
64            });
65        }
66        if self.emissions_count as usize != emissions_len {
67            return Err(HeaderError::CountMismatch {
68                field: "emissions_count",
69                expected: self.emissions_count as usize,
70                actual: emissions_len,
71            });
72        }
73        Ok(())
74    }
75}
76
77/// Errors that can be emitted by header-level validation or operations.
78#[derive(Debug, Error)]
79pub enum HeaderError {
80    /// Header version does not match expected network consensus version.
81    #[error("version mismatch: expected {expected}, found {found}")]
82    VersionMismatch { expected: u32, found: u32 },
83
84    /// A header item count did not match the body lengths.
85    #[error("{field} mismatch: header has {expected}, body has {actual}")]
86    CountMismatch {
87        field: &'static str,
88        expected: usize,
89        actual: usize,
90    },
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    fn sample_header() -> L2BlockHeader {
98        L2BlockHeader {
99            version: 1,
100            network_id: [1u8; 32],
101            epoch: 10,
102            prev_block_root: [2u8; 32],
103            body_root: [3u8; 32],
104            data_count: 2,
105            emissions_count: 1,
106            proposer_pubkey: [9u8; 48],
107        }
108    }
109
110    #[test]
111    fn header_root_changes_when_field_changes() {
112        let h1 = sample_header();
113        let mut h2 = sample_header();
114        assert_eq!(h1.calculate_root(), h2.calculate_root());
115        h2.data_count = 3;
116        assert_ne!(h1.calculate_root(), h2.calculate_root());
117    }
118
119    #[test]
120    fn version_validation() {
121        let h = sample_header();
122        assert!(h.validate_version(1).is_ok());
123        let e = h.validate_version(2).unwrap_err();
124        match e {
125            HeaderError::VersionMismatch { expected, found } => {
126                assert_eq!(expected, 2);
127                assert_eq!(found, 1);
128            }
129            _ => panic!("unexpected error variant"),
130        }
131    }
132
133    #[test]
134    fn counts_validation() {
135        let h = sample_header();
136        assert!(h.validate_counts(2, 1).is_ok());
137        let e = h.validate_counts(1, 1).unwrap_err();
138        match e {
139            HeaderError::CountMismatch {
140                field,
141                expected,
142                actual,
143            } => {
144                assert_eq!(field, "data_count");
145                assert_eq!(expected, 2);
146                assert_eq!(actual, 1);
147            }
148            _ => panic!("unexpected error variant"),
149        }
150    }
151}