1use 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub struct DigL2Block {
30 pub header: L2BlockHeader,
31 pub body: L2BlockBody,
32}
33
34impl DigL2Block {
35 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 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 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 header.validate_counts(body.data.len(), body.emissions.len())?;
67 Ok(DigL2Block { header, body })
68 }
69
70 pub fn build(args: &BuildL2BlockArgs<'_>) -> Result<Self, BlockError> {
80 args.cfg
82 .validate_for_attesters(args.attester_pubkeys.len())?;
83
84 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#[derive(Debug, Error)]
123pub enum BlockError {
124 #[error(transparent)]
126 Header(#[from] crate::header::HeaderError),
127
128 #[error(transparent)]
130 Body(#[from] crate::body::BodyError),
131
132 #[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 #[error(transparent)]
141 Definitions(#[from] crate::dig_l2_definition::DefinitionError),
142
143 #[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; 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 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 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 let expect_body_root = block.body.calculate_root();
244 assert_eq!(block.header.body_root, expect_body_root);
245
246 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); 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}