dig_network_block/
block.rs

1//! L2 block: header and body, with delegated root calculation.
2//!
3//! `L2Block::calculate_root()` defers to `header.calculate_root()` and
4//! `body.calculate_root()` and then composes them via `COMPUTE_BLOCK_ROOT`.
5//!
6//! Construction via `new` enforces invariants between header and body
7//! (counts and body_root) and can surface `HeaderError`/`BodyError` via
8//! transparent composition.
9
10use crate::dig_l2_definition as definitions;
11use crate::{body::L2BlockBody, emission::Emission, header::L2BlockHeader};
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15pub struct BuildL2BlockArgs<'ba> {
16    pub version: u32,
17    pub network_id: [u8; 32],
18    pub epoch: u64,
19    pub prev_block_root: [u8; 32],
20    pub proposer_pubkey: [u8; 48],
21    pub data: Vec<u8>,
22    pub extra_emissions: Vec<Emission>,
23    pub attester_pubkeys: &'ba [[u8; 48]],
24    pub cfg: &'ba crate::emission_config::ConsensusEmissionConfig,
25}
26
27/// Full L2 block containing a header and a body.
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub struct DigL2Block {
30    pub header: L2BlockHeader,
31    pub body: L2BlockBody,
32}
33
34impl DigL2Block {
35    /// Calculates the `BLOCK_ROOT` by composing the `HEADER_ROOT` and `BODY_ROOT`.
36    pub fn calculate_root(&self) -> definitions::Hash32 {
37        let header_root = self.header.calculate_root();
38        let body_root = self.body.calculate_root();
39        definitions::COMPUTE_BLOCK_ROOT(&header_root, &body_root)
40    }
41
42    /// Validates consistency between `header` and `body` and returns a block if valid.
43    ///
44    /// Checks:
45    /// - `data_count` and `emissions_count` match body lengths.
46    /// - `header.body_root` equals `body.calculate_root()`.
47    /// - If `expected_version` is provided, header version matches it.
48    pub fn new(
49        header: L2BlockHeader,
50        body: L2BlockBody,
51        expected_version: Option<u32>,
52    ) -> Result<Self, BlockError> {
53        if let Some(v) = expected_version {
54            header.validate_version(v)?;
55        }
56        // Compare roots first so that a mutated body triggers BodyRootMismatch
57        // which is typically the more informative error than counts mismatch.
58        let calc_body_root = body.calculate_root();
59        if header.body_root != calc_body_root {
60            return Err(BlockError::BodyRootMismatch {
61                header_body_root: header.body_root,
62                calculated: calc_body_root,
63            });
64        }
65        // Then validate counts for completeness.
66        header.validate_counts(body.data.len(), body.emissions.len())?;
67        Ok(DigL2Block { header, body })
68    }
69
70    /// Build a block from raw inputs, constructing required consensus emissions
71    /// and composing header/body deterministically.
72    ///
73    /// Steps:
74    /// - Validates the provided `ConsensusEmissionConfig` against the attester list.
75    /// - Uses `BUILD_CONSENSUS_EMISSIONS` to create mandatory emissions (proposer + attesters).
76    /// - Appends any `extra_emissions` provided by the caller.
77    /// - Assembles the body from `data` and all emissions, computes `body_root`.
78    /// - Fills header counts and `body_root`, leaving other header fields as provided.
79    pub fn build(args: &BuildL2BlockArgs<'_>) -> Result<Self, BlockError> {
80        // Validate config with respect to the number of attesters
81        args.cfg
82            .validate_for_attesters(args.attester_pubkeys.len())?;
83
84        // Build consensus emissions tuples then convert to Emission
85        let tuples = definitions::BUILD_CONSENSUS_EMISSIONS(
86            args.proposer_pubkey,
87            args.attester_pubkeys,
88            args.cfg.proposer_reward_share,
89            args.cfg.attester_reward_share,
90        )?;
91        let mut emissions: Vec<Emission> = tuples
92            .into_iter()
93            .map(|(pk, w)| Emission {
94                pubkey: pk,
95                weight: w,
96            })
97            .collect();
98        emissions.extend(args.extra_emissions.clone());
99
100        let body = L2BlockBody {
101            data: args.data.clone(),
102            emissions,
103        };
104        let body_root = body.calculate_root();
105
106        let header = L2BlockHeader {
107            version: args.version,
108            network_id: args.network_id,
109            epoch: args.epoch,
110            prev_block_root: args.prev_block_root,
111            body_root,
112            data_count: body.data.len() as u32,
113            emissions_count: body.emissions.len() as u32,
114            proposer_pubkey: args.proposer_pubkey,
115        };
116
117        Ok(DigL2Block { header, body })
118    }
119}
120
121/// Errors that can be returned by `DigL2Block` construction/validation.
122#[derive(Debug, Error)]
123pub enum BlockError {
124    /// Propagate header-level validation errors transparently.
125    #[error(transparent)]
126    Header(#[from] crate::header::HeaderError),
127
128    /// Propagate body-level errors transparently (not currently used, reserved for future checks).
129    #[error(transparent)]
130    Body(#[from] crate::body::BodyError),
131
132    /// The header's `body_root` does not match the calculated body root.
133    #[error("body_root mismatch: header {header_body_root:?} != calculated {calculated:?}")]
134    BodyRootMismatch {
135        header_body_root: [u8; 32],
136        calculated: [u8; 32],
137    },
138
139    /// Propagate definition-level errors (e.g., invalid attester share policy).
140    #[error(transparent)]
141    Definitions(#[from] crate::dig_l2_definition::DefinitionError),
142
143    /// Propagate configuration errors.
144    #[error(transparent)]
145    Config(#[from] crate::emission_config::EmissionConfigError),
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::emission::Emission;
152
153    fn make_body() -> L2BlockBody {
154        L2BlockBody {
155            data: vec![1, 2, 3],
156            emissions: vec![Emission {
157                pubkey: [5u8; 48],
158                weight: 10,
159            }],
160        }
161    }
162
163    fn make_header_for_body(body: &L2BlockBody) -> L2BlockHeader {
164        let body_root = body.calculate_root();
165        L2BlockHeader {
166            version: 1,
167            network_id: [0xabu8; 32],
168            epoch: 7,
169            prev_block_root: [0u8; 32],
170            body_root,
171            data_count: body.data.len() as u32,
172            emissions_count: body.emissions.len() as u32,
173            proposer_pubkey: [9u8; 48],
174        }
175    }
176
177    #[test]
178    fn block_root_composition_matches_definitions() {
179        let body = make_body();
180        let header = make_header_for_body(&body);
181        let block = DigL2Block::new(header, body, Some(1)).unwrap();
182        let h_root = block.header.calculate_root();
183        let b_root = block.body.calculate_root();
184        let expect = definitions::COMPUTE_BLOCK_ROOT(&h_root, &b_root);
185        assert_eq!(block.calculate_root(), expect);
186    }
187
188    #[test]
189    fn new_rejects_mismatched_counts() {
190        let body = make_body();
191        let mut header = make_header_for_body(&body);
192        header.data_count += 1; // wrong
193        let err = DigL2Block::new(header, body, Some(1)).unwrap_err();
194        match err {
195            BlockError::Header(crate::header::HeaderError::CountMismatch { .. }) => {}
196            _ => panic!("unexpected error type"),
197        }
198    }
199
200    #[test]
201    fn new_rejects_body_root_mismatch() {
202        let mut body = make_body();
203        let header = make_header_for_body(&body);
204        // change body so root no longer matches header
205        body.data.push(4);
206        let err = DigL2Block::new(header, body, Some(1)).unwrap_err();
207        match err {
208            BlockError::BodyRootMismatch { .. } => {}
209            _ => panic!("unexpected error type"),
210        }
211    }
212
213    #[test]
214    fn build_block_with_attesters_and_extras() {
215        let data = vec![1u8, 2, 3, 4];
216        let extra = vec![Emission {
217            pubkey: [0x33u8; 48],
218            weight: 7,
219        }];
220        let attesters = vec![[0x11u8; 48], [0x22u8; 48], [0x44u8; 48]];
221        let cfg = crate::emission_config::ConsensusEmissionConfig::new(12, 90);
222        let build_block_args = BuildL2BlockArgs {
223            version: 1,
224            network_id: [0xabu8; 32],
225            epoch: 7,
226            prev_block_root: [0u8; 32],
227            proposer_pubkey: [9u8; 48],
228            data,
229            extra_emissions: extra.clone(),
230            attester_pubkeys: &attesters,
231            cfg: &cfg,
232        };
233        let block = DigL2Block::build(&build_block_args).unwrap();
234
235        // Counts should reflect body lengths
236        assert_eq!(block.header.data_count as usize, block.body.data.len());
237        assert_eq!(
238            block.header.emissions_count as usize,
239            block.body.emissions.len()
240        );
241
242        // Roots should be consistent
243        let expect_body_root = block.body.calculate_root();
244        assert_eq!(block.header.body_root, expect_body_root);
245
246        // JSON round-trip of whole block
247        let s = serde_json::to_string(&block).unwrap();
248        let back: DigL2Block = serde_json::from_str(&s).unwrap();
249        assert_eq!(block, back);
250    }
251
252    #[test]
253    fn build_block_zero_attesters_policy() {
254        let cfg = crate::emission_config::ConsensusEmissionConfig::new(12, 0);
255        let bb_args = BuildL2BlockArgs {
256            version: 1,
257            network_id: [0xabu8; 32],
258            epoch: 7,
259            prev_block_root: [0u8; 32],
260            proposer_pubkey: [9u8; 48],
261            data: vec![],
262            extra_emissions: vec![],
263            attester_pubkeys: &[],
264            cfg: &cfg,
265        };
266        let b = DigL2Block::build(&bb_args).unwrap();
267        assert_eq!(b.body.emissions.len(), 1); // proposer only
268
269        // Now invalid: non-zero attester share but no attesters
270        let cfg_bad = crate::emission_config::ConsensusEmissionConfig::new(12, 1);
271        let bb_e_args = BuildL2BlockArgs {
272            version: 1,
273            network_id: [0u8; 32],
274            epoch: 7,
275            prev_block_root: [0u8; 32],
276            proposer_pubkey: [1u8; 48],
277            data: vec![],
278            extra_emissions: vec![],
279            attester_pubkeys: &[],
280            cfg: &cfg_bad,
281        };
282        let err = DigL2Block::build(&bb_e_args).unwrap_err();
283        match err {
284            BlockError::Config(
285                crate::emission_config::EmissionConfigError::NonZeroAttesterShareWithNoAttesters,
286            ) => {}
287            other => panic!("unexpected error: {other:?}"),
288        }
289    }
290}