reth_optimism_consensus/validation/
mod.rs
1pub mod canyon;
4pub mod isthmus;
5
6use crate::proof::calculate_receipt_root_optimism;
7use alloc::vec::Vec;
8use alloy_consensus::{BlockHeader, TxReceipt, EMPTY_OMMER_ROOT_HASH};
9use alloy_primitives::{Bloom, B256};
10use alloy_trie::EMPTY_ROOT_HASH;
11use op_alloy_consensus::{decode_holocene_extra_data, EIP1559ParamError};
12use reth_chainspec::{BaseFeeParams, EthChainSpec};
13use reth_consensus::ConsensusError;
14use reth_optimism_forks::OpHardforks;
15use reth_optimism_primitives::DepositReceipt;
16use reth_primitives_traits::{receipt::gas_spent_by_transactions, BlockBody, GotExpected};
17
18pub fn validate_body_against_header_op<B, H>(
25 chain_spec: impl OpHardforks,
26 body: &B,
27 header: &H,
28) -> Result<(), ConsensusError>
29where
30 B: BlockBody,
31 H: reth_primitives_traits::BlockHeader,
32{
33 let ommers_hash = body.calculate_ommers_root();
34 if Some(header.ommers_hash()) != ommers_hash {
35 return Err(ConsensusError::BodyOmmersHashDiff(
36 GotExpected {
37 got: ommers_hash.unwrap_or(EMPTY_OMMER_ROOT_HASH),
38 expected: header.ommers_hash(),
39 }
40 .into(),
41 ))
42 }
43
44 let tx_root = body.calculate_tx_root();
45 if header.transactions_root() != tx_root {
46 return Err(ConsensusError::BodyTransactionRootDiff(
47 GotExpected { got: tx_root, expected: header.transactions_root() }.into(),
48 ))
49 }
50
51 match (header.withdrawals_root(), body.calculate_withdrawals_root()) {
52 (Some(header_withdrawals_root), Some(withdrawals_root)) => {
53 if chain_spec.is_isthmus_active_at_timestamp(header.timestamp()) {
56 if withdrawals_root != EMPTY_ROOT_HASH {
58 return Err(ConsensusError::BodyWithdrawalsRootDiff(
59 GotExpected { got: withdrawals_root, expected: EMPTY_ROOT_HASH }.into(),
60 ))
61 }
62 } else {
63 if withdrawals_root != header_withdrawals_root {
65 return Err(ConsensusError::BodyWithdrawalsRootDiff(
66 GotExpected { got: withdrawals_root, expected: header_withdrawals_root }
67 .into(),
68 ))
69 }
70 }
71 }
72 (None, None) => {
73 }
75 _ => return Err(ConsensusError::WithdrawalsRootUnexpected),
76 }
77
78 Ok(())
79}
80
81pub fn validate_block_post_execution<R: DepositReceipt>(
86 header: impl BlockHeader,
87 chain_spec: impl OpHardforks,
88 receipts: &[R],
89) -> Result<(), ConsensusError> {
90 if chain_spec.is_byzantium_active_at_block(header.number()) {
95 if let Err(error) = verify_receipts_optimism(
96 header.receipts_root(),
97 header.logs_bloom(),
98 receipts,
99 chain_spec,
100 header.timestamp(),
101 ) {
102 tracing::debug!(%error, ?receipts, "receipts verification failed");
103 return Err(error)
104 }
105 }
106
107 let cumulative_gas_used =
109 receipts.last().map(|receipt| receipt.cumulative_gas_used()).unwrap_or(0);
110 if header.gas_used() != cumulative_gas_used {
111 return Err(ConsensusError::BlockGasUsed {
112 gas: GotExpected { got: cumulative_gas_used, expected: header.gas_used() },
113 gas_spent_by_tx: gas_spent_by_transactions(receipts),
114 })
115 }
116
117 Ok(())
118}
119
120fn verify_receipts_optimism<R: DepositReceipt>(
122 expected_receipts_root: B256,
123 expected_logs_bloom: Bloom,
124 receipts: &[R],
125 chain_spec: impl OpHardforks,
126 timestamp: u64,
127) -> Result<(), ConsensusError> {
128 let receipts_with_bloom = receipts.iter().cloned().map(Into::into).collect::<Vec<_>>();
130 let receipts_root =
131 calculate_receipt_root_optimism(&receipts_with_bloom, chain_spec, timestamp);
132
133 let logs_bloom = receipts_with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom());
135
136 compare_receipts_root_and_logs_bloom(
137 receipts_root,
138 logs_bloom,
139 expected_receipts_root,
140 expected_logs_bloom,
141 )?;
142
143 Ok(())
144}
145
146fn compare_receipts_root_and_logs_bloom(
149 calculated_receipts_root: B256,
150 calculated_logs_bloom: Bloom,
151 expected_receipts_root: B256,
152 expected_logs_bloom: Bloom,
153) -> Result<(), ConsensusError> {
154 if calculated_receipts_root != expected_receipts_root {
155 return Err(ConsensusError::BodyReceiptRootDiff(
156 GotExpected { got: calculated_receipts_root, expected: expected_receipts_root }.into(),
157 ))
158 }
159
160 if calculated_logs_bloom != expected_logs_bloom {
161 return Err(ConsensusError::BodyBloomLogDiff(
162 GotExpected { got: calculated_logs_bloom, expected: expected_logs_bloom }.into(),
163 ))
164 }
165
166 Ok(())
167}
168
169pub fn decode_holocene_base_fee(
175 chain_spec: impl EthChainSpec + OpHardforks,
176 parent: impl BlockHeader,
177 timestamp: u64,
178) -> Result<u64, EIP1559ParamError> {
179 let (elasticity, denominator) = decode_holocene_extra_data(parent.extra_data())?;
180 let base_fee_params = if elasticity == 0 && denominator == 0 {
181 chain_spec.base_fee_params_at_timestamp(timestamp)
182 } else {
183 BaseFeeParams::new(denominator as u128, elasticity as u128)
184 };
185
186 Ok(parent.next_block_base_fee(base_fee_params).unwrap_or_default())
187}
188
189pub fn next_block_base_fee(
193 chain_spec: impl EthChainSpec + OpHardforks,
194 parent: impl BlockHeader,
195 timestamp: u64,
196) -> Result<u64, EIP1559ParamError> {
197 if chain_spec.is_holocene_active_at_timestamp(parent.timestamp()) {
201 Ok(decode_holocene_base_fee(chain_spec, parent, timestamp)?)
202 } else {
203 Ok(parent
204 .next_block_base_fee(chain_spec.base_fee_params_at_timestamp(timestamp))
205 .unwrap_or_default())
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use alloy_consensus::Header;
213 use alloy_primitives::{b256, hex, Bytes, U256};
214 use op_alloy_consensus::OpTxEnvelope;
215 use reth_chainspec::{ChainSpec, ForkCondition, Hardfork};
216 use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA};
217 use reth_optimism_forks::{OpHardfork, BASE_SEPOLIA_HARDFORKS};
218 use std::sync::Arc;
219
220 fn holocene_chainspec() -> Arc<OpChainSpec> {
221 let mut hardforks = BASE_SEPOLIA_HARDFORKS.clone();
222 hardforks.insert(OpHardfork::Holocene.boxed(), ForkCondition::Timestamp(1800000000));
223 Arc::new(OpChainSpec {
224 inner: ChainSpec {
225 chain: BASE_SEPOLIA.inner.chain,
226 genesis: BASE_SEPOLIA.inner.genesis.clone(),
227 genesis_header: BASE_SEPOLIA.inner.genesis_header.clone(),
228 paris_block_and_final_difficulty: Some((0, U256::from(0))),
229 hardforks,
230 base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(),
231 prune_delete_limit: 10000,
232 ..Default::default()
233 },
234 })
235 }
236
237 fn isthmus_chainspec() -> OpChainSpec {
238 let mut chainspec = BASE_SEPOLIA.as_ref().clone();
239 chainspec
240 .inner
241 .hardforks
242 .insert(OpHardfork::Isthmus.boxed(), ForkCondition::Timestamp(1800000000));
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 = next_block_base_fee(&op_chain_spec, &parent, 0);
256 assert_eq!(
257 base_fee.unwrap(),
258 parent
259 .next_block_base_fee(op_chain_spec.base_fee_params_at_timestamp(0))
260 .unwrap_or_default()
261 );
262 }
263
264 #[test]
265 fn test_get_base_fee_holocene_extra_data_not_set() {
266 let op_chain_spec = holocene_chainspec();
267 let parent = Header {
268 base_fee_per_gas: Some(1),
269 gas_used: 15763614,
270 gas_limit: 144000000,
271 timestamp: 1800000003,
272 extra_data: Bytes::from_static(&[0, 0, 0, 0, 0, 0, 0, 0, 0]),
273 ..Default::default()
274 };
275 let base_fee = next_block_base_fee(&op_chain_spec, &parent, 1800000005);
276 assert_eq!(
277 base_fee.unwrap(),
278 parent
279 .next_block_base_fee(op_chain_spec.base_fee_params_at_timestamp(0))
280 .unwrap_or_default()
281 );
282 }
283
284 #[test]
285 fn test_get_base_fee_holocene_extra_data_set() {
286 let parent = Header {
287 base_fee_per_gas: Some(1),
288 gas_used: 15763614,
289 gas_limit: 144000000,
290 extra_data: Bytes::from_static(&[0, 0, 0, 0, 8, 0, 0, 0, 8]),
291 timestamp: 1800000003,
292 ..Default::default()
293 };
294
295 let base_fee = next_block_base_fee(holocene_chainspec(), &parent, 1800000005);
296 assert_eq!(
297 base_fee.unwrap(),
298 parent
299 .next_block_base_fee(BaseFeeParams::new(0x00000008, 0x00000008))
300 .unwrap_or_default()
301 );
302 }
303
304 #[test]
306 fn test_get_base_fee_holocene_extra_data_set_base_sepolia() {
307 let parent = Header {
308 base_fee_per_gas: Some(507),
309 gas_used: 4847634,
310 gas_limit: 60000000,
311 extra_data: hex!("00000000fa0000000a").into(),
312 timestamp: 1735315544,
313 ..Default::default()
314 };
315
316 let base_fee = next_block_base_fee(&*BASE_SEPOLIA, &parent, 1735315546).unwrap();
317 assert_eq!(base_fee, 507);
318 }
319
320 #[test]
321 fn body_against_header_isthmus() {
322 let chainspec = isthmus_chainspec();
323 let header = Header {
324 base_fee_per_gas: Some(507),
325 gas_used: 4847634,
326 gas_limit: 60000000,
327 extra_data: hex!("00000000fa0000000a").into(),
328 timestamp: 1800000000,
329 withdrawals_root: Some(b256!(
330 "0x611e1d75cbb77fa782d79485a8384e853bc92e56883c313a51e3f9feef9a9a71"
331 )),
332 ..Default::default()
333 };
334 let mut body = alloy_consensus::BlockBody::<OpTxEnvelope> {
335 transactions: vec![],
336 ommers: vec![],
337 withdrawals: Some(Default::default()),
338 };
339 validate_body_against_header_op(&chainspec, &body, &header).unwrap();
340
341 body.withdrawals.take();
342 validate_body_against_header_op(&chainspec, &body, &header).unwrap_err();
343 }
344}