Skip to main content

reth_bb/
evm_config.rs

1//! EVM configuration for merged big-block payloads.
2
3pub(crate) use reth_engine_primitives::BigBlockData;
4
5use alloy_consensus::Header;
6use alloy_eips::Decodable2718;
7use alloy_primitives::{Bytes, U256};
8use alloy_rpc_types::engine::ExecutionData;
9use core::convert::Infallible;
10use reth_chainspec::{ChainSpec, EthChainSpec, EthereumHardforks};
11use reth_engine_primitives::ExecutionPayload;
12use reth_ethereum_forks::Hardforks;
13use reth_ethereum_primitives::{Block, EthPrimitives, TransactionSigned};
14use reth_evm::{
15    precompile_cache::PrecompileCacheMap, BlockAssembler, BlockAssemblerInput, BlockExecutionError,
16    ConfigureEngineEvm, ConfigureEvm, EvmEnvFor, EvmFor, ExecutableTxIterator, ExecutionCtxFor,
17    NextBlockEnvAttributes, SpecFor,
18};
19use reth_evm_ethereum::{
20    EthBigBlockExecutorFactory, EthBigBlockPlan, EthBigBlockSegment, EthEvmConfig,
21    ExecutableRecoveredTx,
22};
23use reth_primitives_traits::{BlockTy, HeaderTy, SealedBlock, SealedHeader, SignedTransaction};
24use reth_storage_errors::any::AnyError;
25
26/// Block assembler marker for replay-only big-block execution.
27#[derive(Debug, Clone, Copy, Default)]
28pub struct BbBlockAssembler;
29
30impl<C> BlockAssembler<EthBigBlockExecutorFactory<C>> for BbBlockAssembler
31where
32    C: EthChainSpec<Header = Header> + EthereumHardforks + 'static,
33{
34    type Block = Block;
35
36    fn assemble_block(
37        &self,
38        _input: BlockAssemblerInput<'_, '_, EthBigBlockExecutorFactory<C>, Header>,
39    ) -> Result<Self::Block, BlockExecutionError> {
40        unreachable!("big-block execution is replay-only")
41    }
42}
43
44/// EVM configuration for big-block execution.
45#[derive(Debug, Clone)]
46pub struct BbEvmConfig<C = ChainSpec> {
47    inner: EthEvmConfig<C>,
48    executor_factory: EthBigBlockExecutorFactory<C>,
49    block_assembler: BbBlockAssembler,
50}
51
52impl<C> BbEvmConfig<C> {
53    /// Creates a big-block EVM configuration from the standard Ethereum configuration.
54    pub fn new(inner: EthEvmConfig<C>) -> Self
55    where
56        EthEvmConfig<C>: Clone,
57        EthBigBlockExecutorFactory<C>: Clone,
58    {
59        let executor_factory = EthBigBlockExecutorFactory::new(inner.executor_factory.clone());
60        Self { inner, executor_factory, block_assembler: BbBlockAssembler }
61    }
62}
63
64impl<C> ConfigureEvm for BbEvmConfig<C>
65where
66    C: EthChainSpec<Header = Header> + EthereumHardforks + Hardforks + 'static,
67{
68    type Primitives = EthPrimitives;
69    type Error = Infallible;
70    type NextBlockEnvCtx = NextBlockEnvAttributes;
71    type BlockExecutorFactory = EthBigBlockExecutorFactory<C>;
72    type BlockAssembler = BbBlockAssembler;
73
74    fn evm_with_precompile_cache<'a, DB>(
75        &self,
76        db: DB,
77        evm_env: EvmEnvFor<Self>,
78        cache_map: PrecompileCacheMap<SpecFor<Self>>,
79        metrics_enabled: bool,
80    ) -> EvmFor<'a, Self>
81    where
82        DB: evm2::evm::Database + 'a,
83    {
84        self.inner.evm_with_precompile_cache(db, evm_env, cache_map, metrics_enabled)
85    }
86
87    fn block_executor_factory(&self) -> &Self::BlockExecutorFactory {
88        &self.executor_factory
89    }
90
91    fn block_assembler(&self) -> &Self::BlockAssembler {
92        &self.block_assembler
93    }
94
95    fn with_jit_support_enabled(self, enabled: bool) -> Self {
96        Self::new(self.inner.with_jit_support_enabled(enabled))
97    }
98
99    fn jit_backend(&self) -> Option<&dyn reth_evm::JitBackend> {
100        self.inner.jit_backend()
101    }
102
103    fn evm_env(&self, header: &HeaderTy<Self::Primitives>) -> Result<EvmEnvFor<Self>, Self::Error> {
104        self.inner.evm_env(header)
105    }
106
107    fn next_evm_env(
108        &self,
109        parent: &HeaderTy<Self::Primitives>,
110        attributes: &Self::NextBlockEnvCtx,
111    ) -> Result<EvmEnvFor<Self>, Self::Error> {
112        self.inner.next_evm_env(parent, attributes)
113    }
114
115    fn context_for_block<'a>(
116        &self,
117        _block: &'a SealedBlock<BlockTy<Self::Primitives>>,
118    ) -> Result<ExecutionCtxFor<'a, Self>, Self::Error>
119    where
120        Self: 'a,
121    {
122        unreachable!("big blocks EVM should only be used from within the engine pipeline")
123    }
124
125    fn context_for_next_block(
126        &self,
127        _parent: &SealedHeader<HeaderTy<Self::Primitives>>,
128        _attributes: Self::NextBlockEnvCtx,
129    ) -> Result<ExecutionCtxFor<'_, Self>, Self::Error> {
130        unreachable!("big blocks EVM should only be used from within the engine pipeline")
131    }
132}
133
134impl<C> ConfigureEngineEvm<BigBlockData<ExecutionData>> for BbEvmConfig<C>
135where
136    C: EthChainSpec<Header = Header> + EthereumHardforks + Hardforks + 'static,
137{
138    fn evm_env_for_payload(
139        &self,
140        payload: &BigBlockData<ExecutionData>,
141    ) -> Result<EvmEnvFor<Self>, Self::Error> {
142        let first = payload.env_switches.first().expect("big-block payload has no segments");
143        let mut env = self.inner.evm_env_for_payload(first)?;
144        // Prewarming uses this environment for transactions from every segment, whose fee caps
145        // can be below the first segment's base fee.
146        env.version.features.remove(evm2::EvmFeatures::BASE_FEE_CHECK);
147        env.block.gas_limit = U256::from(payload.gas_limit());
148        Ok(env)
149    }
150
151    fn context_for_payload<'a>(
152        &self,
153        payload: &'a BigBlockData<ExecutionData>,
154    ) -> Result<ExecutionCtxFor<'a, Self>, Self::Error> {
155        assert!(!payload.env_switches.is_empty(), "big-block payload has no segments");
156
157        let mut start_tx = 0;
158        let mut segments = Vec::with_capacity(payload.env_switches.len());
159        for data in &payload.env_switches {
160            let evm_env = self.inner.evm_env_for_payload(data)?;
161            let ctx = self.inner.context_for_payload(data)?;
162            segments.push(EthBigBlockSegment { start_tx, evm_env, ctx });
163            start_tx += data.payload.transactions().len();
164        }
165
166        Ok(EthBigBlockPlan::new(segments, payload.prior_block_hashes.clone(), start_tx))
167    }
168
169    fn tx_iterator_for_payload(
170        &self,
171        payload: &BigBlockData<ExecutionData>,
172    ) -> Result<impl ExecutableTxIterator<Self>, Self::Error> {
173        let transactions = payload
174            .env_switches
175            .iter()
176            .flat_map(|data| data.payload.transactions().iter().cloned())
177            .collect::<Vec<_>>();
178        let convert = |tx: Bytes| {
179            let tx = TransactionSigned::decode_2718_exact(tx.as_ref()).map_err(AnyError::new)?;
180            let signer = tx.try_recover().map_err(AnyError::new)?;
181            Ok::<_, AnyError>(ExecutableRecoveredTx::new(tx.with_signer(signer)))
182        };
183
184        Ok((transactions, convert))
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use alloy_consensus::TxLegacy;
192    use alloy_primitives::{Address, Signature, TxKind};
193    use alloy_rpc_types::engine::{ExecutionPayloadSidecar, ExecutionPayloadV1};
194    use evm2::evm::{EmptyDB, SystemTx};
195    use metrics_util::debugging::{DebugValue, DebuggingRecorder};
196    use reth_evm::EvmEnv;
197    use reth_evm_ethereum::EthEvmEnv;
198
199    #[test]
200    fn prewarm_accepts_later_segment_with_lower_base_fee() {
201        let segment = |base_fee| ExecutionData {
202            payload: ExecutionPayloadV1::from_block_unchecked(
203                Default::default(),
204                &Block {
205                    header: Header {
206                        number: 16_000_000,
207                        timestamp: 1_670_000_000,
208                        gas_limit: 30_000_000,
209                        base_fee_per_gas: Some(base_fee),
210                        ..Default::default()
211                    },
212                    body: Default::default(),
213                },
214            )
215            .into(),
216            sidecar: ExecutionPayloadSidecar::none(),
217        };
218        let payload = BigBlockData {
219            env_switches: vec![segment(100u64), segment(50u64)],
220            prior_block_hashes: Vec::new(),
221            block_number: 16_000_000,
222            merged_block_access_list: None,
223        };
224        let config = BbEvmConfig::new(EthEvmConfig::mainnet());
225        let env = config
226            .evm_env_for_payload(&payload)
227            .unwrap()
228            .with_nonce_check_disabled()
229            .with_balance_check_disabled();
230        let mut evm = config.evm_with_env(EmptyDB::default(), env);
231        let tx = TransactionSigned::new_unhashed(
232            TxLegacy {
233                gas_price: 50,
234                gas_limit: 21_000,
235                to: TxKind::Call(Address::with_last_byte(100)),
236                ..Default::default()
237            }
238            .into(),
239            Signature::new(U256::from(1), U256::from(1), false),
240        )
241        .with_signer(Address::with_last_byte(101))
242        .convert();
243        assert!(evm.transact(&tx).unwrap().discard().status);
244        assert_eq!(evm.block().gas_limit, U256::from(60_000_000));
245        assert!(config
246            .context_for_payload(&payload)
247            .unwrap()
248            .segments
249            .iter()
250            .all(|segment| segment.evm_env.version.feature(evm2::EvmFeatures::BASE_FEE_CHECK)));
251    }
252
253    #[test]
254    fn precompile_cache_is_opt_in_per_evm() {
255        let recorder = DebuggingRecorder::new();
256        metrics::with_local_recorder(&recorder, || {
257            let metrics_snapshot = || {
258                recorder
259                    .snapshotter()
260                    .snapshot()
261                    .into_vec()
262                    .into_iter()
263                    .filter(|(key, ..)| key.key().name().starts_with("sync.caching.precompile"))
264                    .collect::<Vec<_>>()
265            };
266            let config = BbEvmConfig::new(EthEvmConfig::mainnet());
267            let cache_map = PrecompileCacheMap::default();
268            let identity = Address::with_last_byte(4);
269            let sha256 = Address::with_last_byte(2);
270            let ecadd = Address::with_last_byte(6);
271            let call = |cache_metrics: Option<bool>, address, input: Bytes| {
272                let mut evm = match cache_metrics {
273                    Some(metrics_enabled) => config.evm_with_precompile_cache(
274                        EmptyDB::default(),
275                        EthEvmEnv::default(),
276                        cache_map.clone(),
277                        metrics_enabled,
278                    ),
279                    None => config.evm_with_env(EmptyDB::default(), EthEvmEnv::default()),
280                };
281                evm.system_call(SystemTx::new(address, input)).unwrap().commit().status
282            };
283
284            // EVMs the engine did not wrap do not use the cache.
285            let warm_input = Bytes::from_static(b"prewarmed");
286            assert!(call(None, identity, warm_input.clone()));
287            assert!(call(None, identity, warm_input.clone()));
288            assert!(metrics_snapshot().is_empty());
289
290            // Prewarming fills the shared cache without recording execution metrics.
291            assert!(call(Some(false), identity, warm_input.clone()));
292            assert!(metrics_snapshot().is_empty());
293
294            assert!(call(Some(true), identity, warm_input.clone()));
295            let new_input = Bytes::from_static(b"uncached");
296            assert!(call(Some(true), identity, new_input.clone()));
297            assert!(call(Some(true), identity, new_input));
298            assert!(call(Some(true), sha256, warm_input.clone()));
299            assert!(!call(Some(true), ecadd, Bytes::from(vec![0xff; 128])));
300
301            let snapshot = metrics_snapshot();
302            let counter = |address, name| {
303                let label = format!("0x{address:02x}");
304                let (_, _, _, value) = snapshot
305                    .iter()
306                    .find(|(key, ..)| {
307                        key.key().name() == name &&
308                            key.key()
309                                .labels()
310                                .any(|l| l.key() == "address" && l.value() == label)
311                    })
312                    .expect("address-labelled metric registered");
313                value
314            };
315            assert_eq!(
316                counter(identity, "sync.caching.precompile_cache_hits"),
317                &DebugValue::Counter(2)
318            );
319            assert_eq!(
320                counter(identity, "sync.caching.precompile_cache_misses"),
321                &DebugValue::Counter(1)
322            );
323            assert_eq!(
324                counter(sha256, "sync.caching.precompile_cache_misses"),
325                &DebugValue::Counter(1)
326            );
327            assert_eq!(counter(ecadd, "sync.caching.precompile_errors"), &DebugValue::Counter(1));
328            assert!(matches!(
329                counter(identity, "sync.caching.precompile_cache_size"),
330                DebugValue::Gauge(_)
331            ));
332
333            // An EVM created after the cache was filled is still uncached unless wrapped.
334            assert!(call(None, identity, warm_input));
335            // The debugging recorder resets values on each snapshot.
336            assert!(metrics_snapshot().iter().all(|(.., value)| {
337                value == &DebugValue::Counter(0) || value == &DebugValue::Gauge(0.0.into())
338            }));
339        });
340    }
341
342    #[cfg(feature = "jit")]
343    #[test]
344    fn jit_support_reaches_big_block_executor() {
345        let config = BbEvmConfig::new(EthEvmConfig::mainnet());
346        assert!(config.jit_backend().is_some());
347        assert!(!config.evm_factory().jit_support_enabled());
348
349        let config = config.with_jit_support();
350        assert!(config.evm_factory().jit_support_enabled());
351        assert!(!config.with_jit_support_enabled(false).evm_factory().jit_support_enabled());
352    }
353}