Skip to main content

cast/
evm_version.rs

1//! Detects the EVM version a node executes at a given block.
2//!
3//! Replaying a transaction needs the spec its chain applied at that block. For chains without a
4//! hardfork schedule known to Foundry, header fields only hint at it, and replaying an old block
5//! under a newer spec can make a transaction succeed locally that failed on-chain because an
6//! opcode was not active yet. [`probe_evm_version`] instead asks the node which features it
7//! executes.
8
9use alloy_network::{Network, TransactionBuilder};
10use alloy_primitives::{Bytes, U256};
11use alloy_provider::Provider;
12use alloy_rpc_types::BlockId;
13use foundry_compilers::artifacts::EvmVersion;
14use revm::bytecode::opcode::{
15    AND, CALL, CLZ, CODECOPY, CREATE, DUP6, EQ, GAS, INVALID, ISZERO, JUMPDEST, JUMPI, MCOPY,
16    MSTORE, MUL, OR, POP, PUSH0, PUSH1, PUSH2, PUSH3, RETURN, RETURNDATASIZE, SLOTNUM, STATICCALL,
17    STOP, SWAP1,
18};
19
20/// Probed features in activation order, each paired with the EVM version that introduced it and
21/// runtime code that halts exceptionally unless the feature is active.
22#[rustfmt::skip]
23const PROBES: [(EvmVersion, &[u8]); 5] = [
24    (EvmVersion::Shanghai, &[PUSH0, POP, STOP]),
25    (EvmVersion::Cancun, &[PUSH1, 0, PUSH1, 0, PUSH1, 0, MCOPY, STOP]),
26    // Calls the BLS12-381 G1ADD precompile (0x0b) with two points at infinity and requires its
27    // 128-byte result. Before Prague the call reaches an empty account and returns no data.
28    (EvmVersion::Prague, &[
29        // STATICCALL(GAS, 0x0b, 0, 256, 0, 0)
30        PUSH1, 0, PUSH1, 0, PUSH2, 0x01, 0x00, PUSH1, 0, PUSH1, 0x0b, GAS, STATICCALL,
31        // Halt unless the call succeeded and returned 128 bytes.
32        RETURNDATASIZE, PUSH1, 0x80, EQ, AND, PUSH1, 22, JUMPI, INVALID, JUMPDEST, STOP,
33    ]),
34    (EvmVersion::Osaka, &[PUSH1, 0, CLZ, POP, STOP]),
35    (EvmVersion::Amsterdam, &[SLOTNUM, POP, STOP]),
36];
37
38/// Gas forwarded to each probe. A failing probe consumes all of it.
39const PROBE_GAS: u32 = 20_000;
40
41/// Gas limit of the probe call, enough for the worst case in which every probe fails. Under
42/// EIP-8037 the six accounts the probe creates cost 1,101,600 state gas alone.
43const PROBE_CALL_GAS: u64 = 3_000_000;
44
45/// Returns the newest EVM version whose features, and those of every earlier probed version, the
46/// node executes at `block`.
47///
48/// Returns `None` if the node rejects the probe call or returns an unexpected result.
49pub async fn probe_evm_version<N: Network, P: Provider<N>>(
50    provider: &P,
51    block: BlockId,
52) -> Option<EvmVersion> {
53    let request = N::TransactionRequest::default()
54        .with_deploy_code(probe_code())
55        .with_gas_limit(PROBE_CALL_GAS);
56    let output = match provider.call(request).block(block).await {
57        Ok(output) => output,
58        Err(err) => {
59            trace!(%err, "EVM version probe failed");
60            return None;
61        }
62    };
63    let Ok(word) = <[u8; 32]>::try_from(output.as_ref()) else {
64        trace!(%output, "unexpected EVM version probe output");
65        return None;
66    };
67    let mask = U256::from_be_bytes(word);
68    if !(mask >> PROBES.len()).is_zero() {
69        trace!(%mask, "unexpected EVM version probe mask");
70        return None;
71    }
72    let version = evm_version_from_mask(mask);
73    trace!(?version, %mask, "probed EVM version");
74    Some(version)
75}
76
77/// Maps the probe result to the newest version whose probe and all earlier probes succeeded.
78///
79/// A node that fails every probe predates Shanghai. Paris is returned then, the newest version
80/// without `PUSH0`.
81fn evm_version_from_mask(mask: U256) -> EvmVersion {
82    PROBES
83        .iter()
84        .enumerate()
85        .take_while(|(bit, _)| mask.bit(*bit))
86        .last()
87        .map_or(EvmVersion::Paris, |(_, (version, _))| *version)
88}
89
90/// Builds the creation code of the probe call.
91///
92/// For every probe it deploys the probe's runtime code, calls it with a bounded gas stipend so a
93/// failure cannot exhaust the call, and sets bit `i` of the returned word if probe `i` succeeded.
94/// The code itself only uses opcodes that predate every probed feature.
95fn probe_code() -> Bytes {
96    const PER_PROBE: usize = 39;
97    const HEADER_AND_FOOTER: usize = 10;
98
99    let [_, gas @ ..] = PROBE_GAS.to_be_bytes();
100    // The accumulator for the result stays at the bottom of the stack.
101    let mut code = vec![PUSH1, 0];
102    let mut data = Vec::new();
103    let mut offset = HEADER_AND_FOOTER + PER_PROBE * PROBES.len();
104    for (bit, (_, runtime)) in PROBES.iter().enumerate() {
105        let initcode = deploy_code(runtime);
106        let len = u8::try_from(initcode.len()).expect("probe initcode length fits in PUSH1");
107        let [start_hi, start_lo] =
108            u16::try_from(offset).expect("probe initcode offset fits in PUSH2").to_be_bytes();
109        // CODECOPY(0, start, len)
110        code.extend([PUSH1, len, PUSH2, start_hi, start_lo, PUSH1, 0, CODECOPY]);
111        // CREATE(0, 0, len)
112        code.extend([PUSH1, len, PUSH1, 0, PUSH1, 0, CREATE]);
113        // CALL(PROBE_GAS, address, 0, 0, 0, 0, 0), duplicating the created address from below the
114        // five zero arguments.
115        code.extend([PUSH1, 0].repeat(5));
116        code.extend([DUP6, PUSH3, gas[0], gas[1], gas[2], CALL]);
117        // success && address != 0
118        code.extend([SWAP1, ISZERO, ISZERO, AND]);
119        // accumulator |= result << bit
120        code.extend([PUSH1, 1 << bit, MUL, OR]);
121        offset += initcode.len();
122        data.extend(initcode);
123    }
124    // MSTORE(0, accumulator) RETURN(0, 32)
125    code.extend([PUSH1, 0, MSTORE, PUSH1, 32, PUSH1, 0, RETURN]);
126    debug_assert_eq!(code.len(), HEADER_AND_FOOTER + PER_PROBE * PROBES.len());
127    code.extend(data);
128    code.into()
129}
130
131/// Returns initcode that deploys `runtime`.
132fn deploy_code(runtime: &[u8]) -> Vec<u8> {
133    const PREFIX_LEN: u8 = 12;
134    let len = u8::try_from(runtime.len()).expect("probe runtime length fits in PUSH1");
135    // CODECOPY(0, PREFIX_LEN, len) RETURN(0, len)
136    let mut code =
137        vec![PUSH1, len, PUSH1, PREFIX_LEN, PUSH1, 0, CODECOPY, PUSH1, len, PUSH1, 0, RETURN];
138    code.extend_from_slice(runtime);
139    code
140}
141
142#[cfg(test)]
143mod tests {
144
145    use super::*;
146    use alloy_primitives::TxKind;
147    use revm::{
148        Context, ExecuteEvm, MainBuilder, MainContext, context::TxEnv, database::InMemoryDB,
149        primitives::hardfork::SpecId,
150    };
151
152    #[test]
153    fn detects_executed_spec() {
154        for (spec, expected) in [
155            (SpecId::LONDON, EvmVersion::Paris),
156            (SpecId::MERGE, EvmVersion::Paris),
157            (SpecId::SHANGHAI, EvmVersion::Shanghai),
158            (SpecId::CANCUN, EvmVersion::Cancun),
159            (SpecId::PRAGUE, EvmVersion::Prague),
160            (SpecId::OSAKA, EvmVersion::Osaka),
161            (SpecId::AMSTERDAM, EvmVersion::Amsterdam),
162        ] {
163            let result = Context::mainnet()
164                .modify_cfg_chained(|cfg| cfg.set_spec_and_mainnet_gas_params(spec))
165                .with_db(InMemoryDB::default())
166                .build_mainnet()
167                .transact(
168                    TxEnv::builder()
169                        .kind(TxKind::Create)
170                        .data(probe_code())
171                        .gas_limit(PROBE_CALL_GAS)
172                        .build()
173                        .unwrap(),
174                )
175                .unwrap();
176            let output = result.result.into_output().unwrap();
177            assert_eq!(evm_version_from_mask(U256::from_be_slice(&output)), expected, "{spec:?}");
178        }
179    }
180
181    #[test]
182    fn ignores_probes_after_a_failed_one() {
183        assert_eq!(evm_version_from_mask(U256::from(0b1011)), EvmVersion::Cancun);
184    }
185}