1pub mod canyon;
4pub mod isthmus;
5
6use reth_execution_types::BlockExecutionResult;
8pub use reth_optimism_chainspec::decode_holocene_base_fee;
9
10use crate::proof::calculate_receipt_root_optimism;
11use alloc::vec::Vec;
12use alloy_consensus::{BlockHeader, TxReceipt, EMPTY_OMMER_ROOT_HASH};
13use alloy_eips::Encodable2718;
14use alloy_primitives::{Bloom, Bytes, B256};
15use alloy_trie::EMPTY_ROOT_HASH;
16use reth_consensus::ConsensusError;
17use reth_optimism_forks::OpHardforks;
18use reth_optimism_primitives::DepositReceipt;
19use reth_primitives_traits::{receipt::gas_spent_by_transactions, BlockBody, GotExpected};
20
21pub fn validate_body_against_header_op<B, H>(
28 chain_spec: impl OpHardforks,
29 body: &B,
30 header: &H,
31) -> Result<(), ConsensusError>
32where
33 B: BlockBody,
34 H: reth_primitives_traits::BlockHeader,
35{
36 let ommers_hash = body.calculate_ommers_root();
37 if Some(header.ommers_hash()) != ommers_hash {
38 return Err(ConsensusError::BodyOmmersHashDiff(
39 GotExpected {
40 got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
41 expected: header.ommers_hash(),
42 }
43 .into(),
44 ))
45 }
46
47 let tx_root = body.calculate_tx_root();
48 if header.transactions_root() != tx_root {
49 return Err(ConsensusError::BodyTransactionRootDiff(
50 GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
51 ))
52 }
53
54 match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
55 (Some(header_withdrawals_root), Some(withdrawals_root)) => {
56 if chain_spec.is_isthmus_active_at_timestamp(header.timestamp()) {
59 if withdrawals_root != EMPTY_ROOT_HASH {
61 return Err(ConsensusError::BodyWithdrawalsRootDiff(
62 GotExpected { got: withdrawals_root, expected: EMPTY_ROOT_HASH }.into(),
63 ))
64 }
65 } else {
66 if withdrawals_root != header_withdrawals_root {
68 return Err(ConsensusError::BodyWithdrawalsRootDiff(
69 GotExpected { got: withdrawals_root, expected: header_withdrawals_root }
70 .into(),
71 ))
72 }
73 }
74 }
75 (None, None) => {
76 }
78 _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
79 }
80
81 Ok(())
82}
83
84pub fn validate_block_post_execution<R: DepositReceipt>(
89 header: impl BlockHeader,
90 chain_spec: impl OpHardforks,
91 result: &BlockExecutionResult<R>,
92) -> Result<(), ConsensusError> {
93 if chain_spec.is_jovian_active_at_timestamp(header.timestamp()) {
95 let computed_blob_gas_used = result.blob_gas_used;
96 let header_blob_gas_used =
97 header.blob_gas_used().ok_or(ConsensusError::BlobGasUsedMissing)?;
98
99 if computed_blob_gas_used != header_blob_gas_used {
100 return Err(ConsensusError::BlobGasUsedDiff(GotExpected {
101 got: computed_blob_gas_used,
102 expected: header_blob_gas_used,
103 }));
104 }
105 }
106
107 let receipts = &result.receipts;
108
109 if chain_spec.is_byzantium_active_at_block(header.number()) &&
114 let Err(error) = verify_receipts_optimism(
115 header.receipts_root(),
116 header.logs_bloom(),
117 receipts,
118 chain_spec,
119 header.timestamp(),
120 )
121 {
122 let receipts = receipts
123 .iter()
124 .map(|r| Bytes::from(r.with_bloom_ref().encoded_2718()))
125 .collect::<Vec<_>>();
126 tracing::debug!(%error, ?receipts, "receipts verification failed");
127 return Err(error)
128 }
129
130 let cumulative_gas_used =
132 receipts.last().map(|receipt| receipt.cumulative_gas_used()).unwrap_or(0);
133 if header.gas_used() != cumulative_gas_used {
134 return Err(ConsensusError::BlockGasUsed {
135 gas: GotExpected { got: cumulative_gas_used, expected: header.gas_used() },
136 gas_spent_by_tx: gas_spent_by_transactions(receipts),
137 })
138 }
139
140 Ok(())
141}
142
143fn verify_receipts_optimism<R: DepositReceipt>(
145 expected_receipts_root: B256,
146 expected_logs_bloom: Bloom,
147 receipts: &[R],
148 chain_spec: impl OpHardforks,
149 timestamp: u64,
150) -> Result<(), ConsensusError> {
151 let receipts_with_bloom = receipts.iter().map(TxReceipt::with_bloom_ref).collect::<Vec<_>>();
153 let receipts_root =
154 calculate_receipt_root_optimism(&receipts_with_bloom, chain_spec, timestamp);
155
156 let logs_bloom = receipts_with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref());
158
159 compare_receipts_root_and_logs_bloom(
160 receipts_root,
161 logs_bloom,
162 expected_receipts_root,
163 expected_logs_bloom,
164 )?;
165
166 Ok(())
167}
168
169fn compare_receipts_root_and_logs_bloom(
172 calculated_receipts_root: B256,
173 calculated_logs_bloom: Bloom,
174 expected_receipts_root: B256,
175 expected_logs_bloom: Bloom,
176) -> Result<(), ConsensusError> {
177 if calculated_receipts_root != expected_receipts_root {
178 return Err(ConsensusError::BodyReceiptRootDiff(
179 GotExpected { got: calculated_receipts_root, expected: expected_receipts_root }.into(),
180 ))
181 }
182
183 if calculated_logs_bloom != expected_logs_bloom {
184 return Err(ConsensusError::BodyBloomLogDiff(
185 GotExpected { got: calculated_logs_bloom, expected: expected_logs_bloom }.into(),
186 ))
187 }
188
189 Ok(())
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use alloy_consensus::Header;
196 use alloy_eips::eip7685::Requests;
197 use alloy_primitives::{b256, hex, Bytes, U256};
198 use op_alloy_consensus::OpTxEnvelope;
199 use reth_chainspec::{BaseFeeParams, ChainSpec, EthChainSpec, ForkCondition, Hardfork};
200 use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA};
201 use reth_optimism_forks::{OpHardfork, BASE_SEPOLIA_HARDFORKS};
202 use reth_optimism_primitives::OpReceipt;
203 use std::sync::Arc;
204
205 const HOLOCENE_TIMESTAMP: u64 = 1700000000;
206 const ISTHMUS_TIMESTAMP: u64 = 1750000000;
207 const JOVIAN_TIMESTAMP: u64 = 1800000000;
208 const BLOCK_TIME_SECONDS: u64 = 2;
209
210 fn holocene_chainspec() -> Arc<OpChainSpec> {
211 let mut hardforks = BASE_SEPOLIA_HARDFORKS.clone();
212 hardforks
213 .insert(OpHardfork::Holocene.boxed(), ForkCondition::Timestamp(HOLOCENE_TIMESTAMP));
214 Arc::new(OpChainSpec {
215 inner: ChainSpec {
216 chain: BASE_SEPOLIA.inner.chain,
217 genesis: BASE_SEPOLIA.inner.genesis.clone(),
218 genesis_header: BASE_SEPOLIA.inner.genesis_header.clone(),
219 paris_block_and_final_difficulty: Some((0, U256::from(0))),
220 hardforks,
221 base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(),
222 prune_delete_limit: 10000,
223 ..Default::default()
224 },
225 })
226 }
227
228 fn isthmus_chainspec() -> OpChainSpec {
229 let mut chainspec = BASE_SEPOLIA.as_ref().clone();
230 chainspec
231 .inner
232 .hardforks
233 .insert(OpHardfork::Isthmus.boxed(), ForkCondition::Timestamp(ISTHMUS_TIMESTAMP));
234 chainspec
235 }
236
237 fn jovian_chainspec() -> OpChainSpec {
238 let mut chainspec = BASE_SEPOLIA.as_ref().clone();
239 chainspec
240 .inner
241 .hardforks
242 .insert(OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(JOVIAN_TIMESTAMP));
243 chainspec
244 }
245
246 #[test]
247 fn test_get_base_fee_pre_holocene() {
248 let op_chain_spec = BASE_SEPOLIA.clone();
249 let parent = Header {
250 base_fee_per_gas: Some(1),
251 gas_used: 15763614,
252 gas_limit: 144000000,
253 ..Default::default()
254 };
255 let base_fee =
256 reth_optimism_chainspec::OpChainSpec::next_block_base_fee(&op_chain_spec, &parent, 0);
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_not_set() {
265 let op_chain_spec = holocene_chainspec();
266 let parent = Header {
267 base_fee_per_gas: Some(1),
268 gas_used: 15763614,
269 gas_limit: 144000000,
270 timestamp: HOLOCENE_TIMESTAMP + 3,
271 extra_data: Bytes::from_static(&[0, 0, 0, 0, 0, 0, 0, 0, 0]),
272 ..Default::default()
273 };
274 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
275 &op_chain_spec,
276 &parent,
277 HOLOCENE_TIMESTAMP + 5,
278 );
279 assert_eq!(
280 base_fee.unwrap(),
281 op_chain_spec.next_block_base_fee(&parent, 0).unwrap_or_default()
282 );
283 }
284
285 #[test]
286 fn test_get_base_fee_holocene_extra_data_set() {
287 let parent = Header {
288 base_fee_per_gas: Some(1),
289 gas_used: 15763614,
290 gas_limit: 144000000,
291 extra_data: Bytes::from_static(&[0, 0, 0, 0, 8, 0, 0, 0, 8]),
292 timestamp: HOLOCENE_TIMESTAMP + 3,
293 ..Default::default()
294 };
295
296 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
297 &holocene_chainspec(),
298 &parent,
299 HOLOCENE_TIMESTAMP + 5,
300 );
301 assert_eq!(
302 base_fee.unwrap(),
303 parent
304 .next_block_base_fee(BaseFeeParams::new(0x00000008, 0x00000008))
305 .unwrap_or_default()
306 );
307 }
308
309 #[test]
311 fn test_get_base_fee_holocene_extra_data_set_base_sepolia() {
312 let parent = Header {
313 base_fee_per_gas: Some(507),
314 gas_used: 4847634,
315 gas_limit: 60000000,
316 extra_data: hex!("00000000fa0000000a").into(),
317 timestamp: 1735315544,
318 ..Default::default()
319 };
320
321 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
322 &*BASE_SEPOLIA,
323 &parent,
324 1735315546,
325 )
326 .unwrap();
327 assert_eq!(base_fee, 507);
328 }
329
330 #[test]
331 fn test_get_base_fee_holocene_extra_data_set_and_min_base_fee_set() {
332 const MIN_BASE_FEE: u64 = 10;
333
334 let mut extra_data = Vec::new();
335 extra_data.append(&mut hex!("00000000fa0000000a").to_vec());
337 extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
339 let extra_data = Bytes::from(extra_data);
340
341 let parent = Header {
342 base_fee_per_gas: Some(507),
343 gas_used: 4847634,
344 gas_limit: 60000000,
345 extra_data,
346 timestamp: 1735315544,
347 ..Default::default()
348 };
349
350 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
351 &*BASE_SEPOLIA,
352 &parent,
353 1735315546,
354 );
355 assert_eq!(base_fee, None);
356 }
357
358 const JOVIAN_EXTRA_DATA_VERSION_BYTE: u8 = 1;
360
361 #[test]
362 fn test_get_base_fee_jovian_extra_data_and_min_base_fee_not_set() {
363 let op_chain_spec = jovian_chainspec();
364
365 let mut extra_data = Vec::new();
366 extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
367 extra_data.append(&mut [0_u8; 8].to_vec());
369 let extra_data = Bytes::from(extra_data);
370
371 let parent = Header {
372 base_fee_per_gas: Some(1),
373 gas_used: 15763614,
374 gas_limit: 144000000,
375 timestamp: JOVIAN_TIMESTAMP,
376 extra_data,
377 ..Default::default()
378 };
379 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
380 &op_chain_spec,
381 &parent,
382 JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
383 );
384 assert_eq!(base_fee, None);
385 }
386
387 #[test]
389 fn test_get_base_fee_jovian_default_extra_data_and_min_base_fee() {
390 const CURR_BASE_FEE: u64 = 1;
391 const MIN_BASE_FEE: u64 = 10;
392
393 let mut extra_data = Vec::new();
394 extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
395 extra_data.append(&mut [0_u8; 8].to_vec());
397 extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
399 let extra_data = Bytes::from(extra_data);
400
401 let op_chain_spec = jovian_chainspec();
402 let parent = Header {
403 base_fee_per_gas: Some(CURR_BASE_FEE),
404 gas_used: 15763614,
405 gas_limit: 144000000,
406 timestamp: JOVIAN_TIMESTAMP,
407 extra_data,
408 ..Default::default()
409 };
410 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
411 &op_chain_spec,
412 &parent,
413 JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
414 );
415 assert_eq!(base_fee, Some(MIN_BASE_FEE));
416 }
417
418 #[test]
420 fn test_jovian_min_base_fee_cannot_decrease() {
421 const MIN_BASE_FEE: u64 = 10;
422
423 let mut extra_data = Vec::new();
424 extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
425 extra_data.append(&mut [0_u8; 8].to_vec());
427 extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
429 let extra_data = Bytes::from(extra_data);
430
431 let op_chain_spec = jovian_chainspec();
432
433 let parent = Header {
435 base_fee_per_gas: Some(MIN_BASE_FEE),
436 gas_used: 10,
437 gas_limit: 144000000,
438 timestamp: JOVIAN_TIMESTAMP,
439 extra_data: extra_data.clone(),
440 ..Default::default()
441 };
442 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
443 &op_chain_spec,
444 &parent,
445 JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
446 );
447 assert_eq!(base_fee, Some(MIN_BASE_FEE));
448
449 let parent = Header {
451 base_fee_per_gas: Some(MIN_BASE_FEE),
452 gas_used: 144000000,
453 gas_limit: 144000000,
454 timestamp: JOVIAN_TIMESTAMP,
455 extra_data,
456 ..Default::default()
457 };
458 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
459 &op_chain_spec,
460 &parent,
461 JOVIAN_TIMESTAMP + 2 * BLOCK_TIME_SECONDS,
462 );
463 assert_eq!(base_fee, Some(MIN_BASE_FEE + 1));
464 }
465
466 #[test]
467 fn test_jovian_base_fee_can_decrease_if_above_min_base_fee() {
468 const MIN_BASE_FEE: u64 = 10;
469
470 let mut extra_data = Vec::new();
471 extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE);
472 extra_data.append(&mut [0_u8; 8].to_vec());
474 extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec());
476 let extra_data = Bytes::from(extra_data);
477
478 let op_chain_spec = jovian_chainspec();
479
480 let parent = Header {
481 base_fee_per_gas: Some(100 * MIN_BASE_FEE),
482 gas_used: 10,
483 gas_limit: 144000000,
484 timestamp: JOVIAN_TIMESTAMP,
485 extra_data,
486 ..Default::default()
487 };
488 let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee(
489 &op_chain_spec,
490 &parent,
491 JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS,
492 )
493 .unwrap();
494 assert_eq!(
495 base_fee,
496 op_chain_spec
497 .inner
498 .next_block_base_fee(&parent, JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS)
499 .unwrap()
500 );
501 }
502
503 #[test]
504 fn body_against_header_isthmus() {
505 let chainspec = isthmus_chainspec();
506 let header = Header {
507 base_fee_per_gas: Some(507),
508 gas_used: 4847634,
509 gas_limit: 60000000,
510 extra_data: hex!("00000000fa0000000a").into(),
511 timestamp: 1800000000,
512 withdrawals_root: Some(b256!(
513 "0x611e1d75cbb77fa782d79485a8384e853bc92e56883c313a51e3f9feef9a9a71"
514 )),
515 ..Default::default()
516 };
517 let mut body = alloy_consensus::BlockBody::<OpTxEnvelope> {
518 transactions: vec![],
519 ommers: vec![],
520 withdrawals: Some(Default::default()),
521 };
522 validate_body_against_header_op(&chainspec, &body, &header).unwrap();
523
524 body.withdrawals.take();
525 validate_body_against_header_op(&chainspec, &body, &header).unwrap_err();
526 }
527
528 #[test]
529 fn test_jovian_blob_gas_used_validation() {
530 const BLOB_GAS_USED: u64 = 1000;
531 const GAS_USED: u64 = 5000;
532
533 let chainspec = jovian_chainspec();
534 let header = Header {
535 timestamp: JOVIAN_TIMESTAMP,
536 blob_gas_used: Some(BLOB_GAS_USED),
537 ..Default::default()
538 };
539
540 let result = BlockExecutionResult::<OpReceipt> {
541 blob_gas_used: BLOB_GAS_USED,
542 receipts: vec![],
543 requests: Requests::default(),
544 gas_used: GAS_USED,
545 };
546 validate_block_post_execution(&header, &chainspec, &result).unwrap();
547 }
548
549 #[test]
550 fn test_jovian_blob_gas_used_validation_mismatched() {
551 const BLOB_GAS_USED: u64 = 1000;
552 const GAS_USED: u64 = 5000;
553
554 let chainspec = jovian_chainspec();
555 let header = Header {
556 timestamp: JOVIAN_TIMESTAMP,
557 blob_gas_used: Some(BLOB_GAS_USED + 1),
558 ..Default::default()
559 };
560
561 let result = BlockExecutionResult::<OpReceipt> {
562 blob_gas_used: BLOB_GAS_USED,
563 receipts: vec![],
564 requests: Requests::default(),
565 gas_used: GAS_USED,
566 };
567 assert_eq!(
568 validate_block_post_execution(&header, &chainspec, &result),
569 Err(ConsensusError::BlobGasUsedDiff(GotExpected {
570 got: BLOB_GAS_USED,
571 expected: BLOB_GAS_USED + 1,
572 }))
573 );
574 }
575}