reth_optimism_consensus/validation/
mod.rs1pub mod canyon;
4pub mod isthmus;
5
6pub use reth_optimism_chainspec::decode_holocene_base_fee;
8
9use crate::proof::calculate_receipt_root_optimism;
10use alloc::vec::Vec;
11use alloy_consensus::{BlockHeader, TxReceipt, EMPTY_OMMER_ROOT_HASH};
12use alloy_eips::Encodable2718;
13use alloy_primitives::{Bloom, Bytes, B256};
14use alloy_trie::EMPTY_ROOT_HASH;
15use reth_consensus::ConsensusError;
16use reth_optimism_forks::OpHardforks;
17use reth_optimism_primitives::DepositReceipt;
18use reth_primitives_traits::{receipt::gas_spent_by_transactions, BlockBody, GotExpected};
19
20pub fn validate_body_against_header_op<B, H>(
27 chain_spec: impl OpHardforks,
28 body: &B,
29 header: &H,
30) -> Result<(), ConsensusError>
31where
32 B: BlockBody,
33 H: reth_primitives_traits::BlockHeader,
34{
35 let ommers_hash = body.calculate_ommers_root();
36 if Some(header.ommers_hash()) != ommers_hash {
37 return Err(ConsensusError::BodyOmmersHashDiff(
38 GotExpected {
39 got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
40 expected: header.ommers_hash(),
41 }
42 .into(),
43 ))
44 }
45
46 let tx_root = body.calculate_tx_root();
47 if header.transactions_root() != tx_root {
48 return Err(ConsensusError::BodyTransactionRootDiff(
49 GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
50 ))
51 }
52
53 match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
54 (Some(header_withdrawals_root), Some(withdrawals_root)) => {
55 if chain_spec.is_isthmus_active_at_timestamp(header.timestamp()) {
58 if withdrawals_root != EMPTY_ROOT_HASH {
60 return Err(ConsensusError::BodyWithdrawalsRootDiff(
61 GotExpected { got: withdrawals_root, expected: EMPTY_ROOT_HASH }.into(),
62 ))
63 }
64 } else {
65 if withdrawals_root != header_withdrawals_root {
67 return Err(ConsensusError::BodyWithdrawalsRootDiff(
68 GotExpected { got: withdrawals_root, expected: header_withdrawals_root }
69 .into(),
70 ))
71 }
72 }
73 }
74 (None, None) => {
75 }
77 _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
78 }
79
80 Ok(())
81}
82
83pub fn validate_block_post_execution<R: DepositReceipt>(
88 header: impl BlockHeader,
89 chain_spec: impl OpHardforks,
90 receipts: &[R],
91) -> Result<(), ConsensusError> {
92 if chain_spec.is_byzantium_active_at_block(header.number()) &&
97 let Err(error) = verify_receipts_optimism(
98 header.receipts_root(),
99 header.logs_bloom(),
100 receipts,
101 chain_spec,
102 header.timestamp(),
103 )
104 {
105 let receipts = receipts
106 .iter()
107 .map(|r| Bytes::from(r.with_bloom_ref().encoded_2718()))
108 .collect::<Vec<_>>();
109 tracing::debug!(%error, ?receipts, "receipts verification failed");
110 return Err(error)
111 }
112
113 let cumulative_gas_used =
115 receipts.last().map(|receipt| receipt.cumulative_gas_used()).unwrap_or(0);
116 if header.gas_used() != cumulative_gas_used {
117 return Err(ConsensusError::BlockGasUsed {
118 gas: GotExpected { got: cumulative_gas_used, expected: header.gas_used() },
119 gas_spent_by_tx: gas_spent_by_transactions(receipts),
120 })
121 }
122
123 Ok(())
124}
125
126fn verify_receipts_optimism<R: DepositReceipt>(
128 expected_receipts_root: B256,
129 expected_logs_bloom: Bloom,
130 receipts: &[R],
131 chain_spec: impl OpHardforks,
132 timestamp: u64,
133) -> Result<(), ConsensusError> {
134 let receipts_with_bloom = receipts.iter().map(TxReceipt::with_bloom_ref).collect::<Vec<_>>();
136 let receipts_root =
137 calculate_receipt_root_optimism(&receipts_with_bloom, chain_spec, timestamp);
138
139 let logs_bloom = receipts_with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref());
141
142 compare_receipts_root_and_logs_bloom(
143 receipts_root,
144 logs_bloom,
145 expected_receipts_root,
146 expected_logs_bloom,
147 )?;
148
149 Ok(())
150}
151
152fn compare_receipts_root_and_logs_bloom(
155 calculated_receipts_root: B256,
156 calculated_logs_bloom: Bloom,
157 expected_receipts_root: B256,
158 expected_logs_bloom: Bloom,
159) -> Result<(), ConsensusError> {
160 if calculated_receipts_root != expected_receipts_root {
161 return Err(ConsensusError::BodyReceiptRootDiff(
162 GotExpected { got: calculated_receipts_root, expected: expected_receipts_root }.into(),
163 ))
164 }
165
166 if calculated_logs_bloom != expected_logs_bloom {
167 return Err(ConsensusError::BodyBloomLogDiff(
168 GotExpected { got: calculated_logs_bloom, expected: expected_logs_bloom }.into(),
169 ))
170 }
171
172 Ok(())
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use alloy_consensus::Header;
179 use alloy_primitives::{b256, hex, Bytes, U256};
180 use op_alloy_consensus::OpTxEnvelope;
181 use reth_chainspec::{BaseFeeParams, ChainSpec, EthChainSpec, ForkCondition, Hardfork};
182 use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA};
183 use reth_optimism_forks::{OpHardfork, BASE_SEPOLIA_HARDFORKS};
184 use std::sync::Arc;
185
186 const JOVIAN_TIMESTAMP: u64 = 1900000000;
187 const BLOCK_TIME_SECONDS: u64 = 2;
188
189 fn holocene_chainspec() -> Arc<OpChainSpec> {
190 let mut hardforks = BASE_SEPOLIA_HARDFORKS.clone();
191 hardforks.insert(OpHardfork::Holocene.boxed(), ForkCondition::Timestamp(1800000000));
192 Arc::new(OpChainSpec {
193 inner: ChainSpec {
194 chain: BASE_SEPOLIA.inner.chain,
195 genesis: BASE_SEPOLIA.inner.genesis.clone(),
196 genesis_header: BASE_SEPOLIA.inner.genesis_header.clone(),
197 paris_block_and_final_difficulty: Some((0, U256::from(0))),
198 hardforks,
199 base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(),
200 prune_delete_limit: 10000,
201 ..Default::default()
202 },
203 })
204 }
205
206 fn isthmus_chainspec() -> OpChainSpec {
207 let mut chainspec = BASE_SEPOLIA.as_ref().clone();
208 chainspec
209 .inner
210 .hardforks
211 .insert(OpHardfork::Isthmus.boxed(), ForkCondition::Timestamp(1800000000));
212 chainspec
213 }
214
215 fn jovian_chainspec() -> OpChainSpec {
216 let mut chainspec = BASE_SEPOLIA.as_ref().clone();
217 chainspec
218 .inner
219 .hardforks
220 .insert(OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(1900000000));
221 chainspec
222 }
223
224 #[test]
225 fn test_get_base_fee_pre_holocene() {
226 let op_chain_spec = BASE_SEPOLIA.clone();
227 let parent = Header {
228 base_fee_per_gas: Some(1),
229 gas_used: 15763614,
230 gas_limit: 144000000,
231 ..Default::default()
232 };
233 let base_fee =
234 reth_optimism_chainspec::OpChainSpec::next_block_base_fee(&op_chain_spec, &parent, 0);
235 assert_eq!(
236 base_fee.unwrap(),
237 op_chain_spec.next_block_base_fee(&parent, 0).unwrap_or_default()
238 );
239 }
240
241 #[test]
242 fn test_get_base_fee_holocene_extra_data_not_set() {
243 let op_chain_spec = holocene_chainspec();
244 let parent = Header {
245 base_fee_per_gas: Some(1),
246 gas_used: 15763614,
247 gas_limit: 144000000,
248 timestamp: 1800000003,
249 extra_data: Bytes::from_static(&[0, 0, 0, 0, 0, 0, 0, 0, 0]),
250 ..Default::default()
251 };
252 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
253 &op_chain_spec,
254 &parent,
255 1800000005,
256 );
257 assert_eq!(
258 base_fee.unwrap(),
259 op_chain_spec.next_block_base_fee(&parent, 0).unwrap_or_default()
260 );
261 }
262
263 #[test]
264 fn test_get_base_fee_holocene_extra_data_set() {
265 let parent = Header {
266 base_fee_per_gas: Some(1),
267 gas_used: 15763614,
268 gas_limit: 144000000,
269 extra_data: Bytes::from_static(&[0, 0, 0, 0, 8, 0, 0, 0, 8]),
270 timestamp: 1800000003,
271 ..Default::default()
272 };
273
274 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
275 &holocene_chainspec(),
276 &parent,
277 1800000005,
278 );
279 assert_eq!(
280 base_fee.unwrap(),
281 parent
282 .next_block_base_fee(BaseFeeParams::new(0x00000008, 0x00000008))
283 .unwrap_or_default()
284 );
285 }
286
287 #[test]
289 fn test_get_base_fee_holocene_extra_data_set_base_sepolia() {
290 let parent = Header {
291 base_fee_per_gas: Some(507),
292 gas_used: 4847634,
293 gas_limit: 60000000,
294 extra_data: hex!("00000000fa0000000a").into(),
295 timestamp: 1735315544,
296 ..Default::default()
297 };
298
299 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
300 &*BASE_SEPOLIA,
301 &parent,
302 1735315546,
303 )
304 .unwrap();
305 assert_eq!(base_fee, 507);
306 }
307
308 #[test]
309 fn test_get_base_fee_holocene_extra_data_set_and_min_base_fee_set() {
310 const MIN_BASE_FEE: u64 = 10;
311
312 let mut extra_data = Vec::new();
313 extra_data.append(&mut hex!("00000000fa0000000a").to_vec());
315 extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
317 let extra_data = Bytes::from(extra_data);
318
319 let parent = Header {
320 base_fee_per_gas: Some(507),
321 gas_used: 4847634,
322 gas_limit: 60000000,
323 extra_data,
324 timestamp: 1735315544,
325 ..Default::default()
326 };
327
328 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
329 &*BASE_SEPOLIA,
330 &parent,
331 1735315546,
332 );
333 assert_eq!(base_fee, None);
334 }
335
336 const JOVIAN_EXTRA_DATA_VERSION_BYTE: u8 = 1;
338
339 #[test]
340 fn test_get_base_fee_jovian_extra_data_and_min_base_fee_not_set() {
341 let op_chain_spec = jovian_chainspec();
342
343 let mut extra_data = Vec::new();
344 extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
345 extra_data.append(&mut [0_u8; 8].to_vec());
347 let extra_data = Bytes::from(extra_data);
348
349 let parent = Header {
350 base_fee_per_gas: Some(1),
351 gas_used: 15763614,
352 gas_limit: 144000000,
353 timestamp: JOVIAN_TIMESTAMP,
354 extra_data,
355 ..Default::default()
356 };
357 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
358 &op_chain_spec,
359 &parent,
360 JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
361 );
362 assert_eq!(base_fee, None);
363 }
364
365 #[test]
367 fn test_get_base_fee_jovian_default_extra_data_and_min_base_fee() {
368 const CURR_BASE_FEE: u64 = 1;
369 const MIN_BASE_FEE: u64 = 10;
370
371 let mut extra_data = Vec::new();
372 extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
373 extra_data.append(&mut [0_u8; 8].to_vec());
375 extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
377 let extra_data = Bytes::from(extra_data);
378
379 let op_chain_spec = jovian_chainspec();
380 let parent = Header {
381 base_fee_per_gas: Some(CURR_BASE_FEE),
382 gas_used: 15763614,
383 gas_limit: 144000000,
384 timestamp: JOVIAN_TIMESTAMP,
385 extra_data,
386 ..Default::default()
387 };
388 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
389 &op_chain_spec,
390 &parent,
391 JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
392 );
393 assert_eq!(base_fee, Some(MIN_BASE_FEE));
394 }
395
396 #[test]
398 fn test_jovian_min_base_fee_cannot_decrease() {
399 const MIN_BASE_FEE: u64 = 10;
400
401 let mut extra_data = Vec::new();
402 extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
403 extra_data.append(&mut [0_u8; 8].to_vec());
405 extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
407 let extra_data = Bytes::from(extra_data);
408
409 let op_chain_spec = jovian_chainspec();
410
411 let parent = Header {
413 base_fee_per_gas: Some(MIN_BASE_FEE),
414 gas_used: 10,
415 gas_limit: 144000000,
416 timestamp: JOVIAN_TIMESTAMP,
417 extra_data: extra_data.clone(),
418 ..Default::default()
419 };
420 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
421 &op_chain_spec,
422 &parent,
423 JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
424 );
425 assert_eq!(base_fee, Some(MIN_BASE_FEE));
426
427 let parent = Header {
429 base_fee_per_gas: Some(MIN_BASE_FEE),
430 gas_used: 144000000,
431 gas_limit: 144000000,
432 timestamp: JOVIAN_TIMESTAMP,
433 extra_data,
434 ..Default::default()
435 };
436 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
437 &op_chain_spec,
438 &parent,
439 JOVIAN_TIMESTAMP + 2 * BLOCK_TIME_SECONDS,
440 );
441 assert_eq!(base_fee, Some(MIN_BASE_FEE + 1));
442 }
443
444 #[test]
445 fn test_jovian_base_fee_can_decrease_if_above_min_base_fee() {
446 const MIN_BASE_FEE: u64 = 10;
447
448 let mut extra_data = Vec::new();
449 extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
450 extra_data.append(&mut [0_u8; 8].to_vec());
452 extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
454 let extra_data = Bytes::from(extra_data);
455
456 let op_chain_spec = jovian_chainspec();
457
458 let parent = Header {
459 base_fee_per_gas: Some(100 * MIN_BASE_FEE),
460 gas_used: 10,
461 gas_limit: 144000000,
462 timestamp: JOVIAN_TIMESTAMP,
463 extra_data,
464 ..Default::default()
465 };
466 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
467 &op_chain_spec,
468 &parent,
469 JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
470 )
471 .unwrap();
472 assert_eq!(
473 base_fee,
474 op_chain_spec
475 .inner
476 .next_block_base_fee(&parent, JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS)
477 .unwrap()
478 );
479 }
480
481 #[test]
482 fn body_against_header_isthmus() {
483 let chainspec = isthmus_chainspec();
484 let header = Header {
485 base_fee_per_gas: Some(507),
486 gas_used: 4847634,
487 gas_limit: 60000000,
488 extra_data: hex!("00000000fa0000000a").into(),
489 timestamp: 1800000000,
490 withdrawals_root: Some(b256!(
491 "0x611e1d75cbb77fa782d79485a8384e853bc92e56883c313a51e3f9feef9a9a71"
492 )),
493 ..Default::default()
494 };
495 let mut body = alloy_consensus::BlockBody::<OpTxEnvelope> {
496 transactions: vec![],
497 ommers: vec![],
498 withdrawals: Some(Default::default()),
499 };
500 validate_body_against_header_op(&chainspec, &body, &header).unwrap();
501
502 body.withdrawals.take();
503 validate_body_against_header_op(&chainspec, &body, &header).unwrap_err();
504 }
505}