Skip to main content

ethrex_levm/
precompiles.rs

1use bytes::{Buf, Bytes};
2use ethrex_common::H160;
3use ethrex_common::utils::u256_from_big_endian_const;
4use ethrex_common::{
5    Address, H256, U256, types::Fork, types::Fork::*, utils::u256_from_big_endian,
6};
7use ethrex_crypto::{Crypto, CryptoError};
8use rustc_hash::FxHashMap;
9use std::borrow::Cow;
10use std::sync::RwLock;
11
12use crate::gas_cost::{MODEXP_STATIC_COST, P256_VERIFY_COST};
13use crate::vm::VMType;
14use crate::{
15    constants::VERSIONED_HASH_VERSION_KZG,
16    errors::{InternalError, PrecompileError, VMError},
17    gas_cost::{
18        self, BLAKE2F_ROUND_COST, BLS12_381_G1_K_DISCOUNT, BLS12_381_G1ADD_COST,
19        BLS12_381_G2_K_DISCOUNT, BLS12_381_G2ADD_COST, BLS12_381_MAP_FP_TO_G1_COST,
20        BLS12_381_MAP_FP2_TO_G2_COST, ECADD_COST, ECMUL_COST, G1_MUL_COST, G2_MUL_COST,
21        POINT_EVALUATION_COST,
22    },
23};
24
25pub const BLAKE2F_ELEMENT_SIZE: usize = 8;
26
27pub const SIZE_PRECOMPILES_PRE_CANCUN: u64 = 9;
28pub const SIZE_PRECOMPILES_CANCUN: u64 = 10;
29pub const SIZE_PRECOMPILES_PRAGUE: u64 = 17;
30
31pub const BLS12_381_G1_MSM_PAIR_LENGTH: usize = 160;
32pub const BLS12_381_G2_MSM_PAIR_LENGTH: usize = 288;
33pub const BLS12_381_PAIRING_CHECK_PAIR_LENGTH: usize = 384;
34
35const BLS12_381_FP2_VALID_INPUT_LENGTH: usize = 128;
36const BLS12_381_FP_VALID_INPUT_LENGTH: usize = 64;
37
38pub const FIELD_ELEMENT_WITHOUT_PADDING_LENGTH: usize = 48;
39pub const PADDED_FIELD_ELEMENT_SIZE_IN_BYTES: usize = 64;
40
41pub const G1_POINT_AT_INFINITY: [u8; 128] = [0_u8; 128];
42pub const G2_POINT_AT_INFINITY: [u8; 256] = [0_u8; 256];
43
44/// Precomputed result for MAP_FP2_TO_G2 when input is (Fp::zero(), Fp::zero()).
45/// The bls12_381 crate's map_to_curve does not handle the zero Fp2 case correctly,
46/// so we return the known-correct padded result directly.
47const FP2_ZERO_MAPPED_TO_G2: [u8; 256] = [
48    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 131, 32, 137, 110, 201, 238, 249, 213, 230,
49    25, 132, 141, 194, 156, 226, 102, 244, 19, 208, 45, 211, 29, 155, 157, 68, 236, 12, 121, 205,
50    97, 241, 139, 7, 93, 219, 166, 215, 189, 32, 183, 255, 39, 164, 179, 36, 191, 206, 0, 0, 0, 0,
51    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 103, 209, 33, 24, 181, 163, 91, 176, 45, 46, 134, 179,
52    235, 250, 126, 35, 65, 13, 185, 61, 227, 159, 176, 109, 112, 37, 250, 149, 233, 111, 250, 66,
53    138, 122, 39, 195, 174, 77, 212, 180, 11, 210, 81, 172, 101, 136, 146, 0, 0, 0, 0, 0, 0, 0, 0,
54    0, 0, 0, 0, 0, 0, 0, 0, 2, 96, 224, 54, 68, 209, 162, 195, 33, 37, 107, 50, 70, 186, 210, 184,
55    149, 202, 209, 56, 144, 203, 230, 248, 93, 245, 81, 6, 160, 211, 52, 96, 79, 177, 67, 199, 160,
56    66, 216, 120, 0, 98, 113, 134, 91, 195, 89, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
57    4, 198, 151, 119, 164, 63, 11, 218, 7, 103, 157, 88, 5, 230, 63, 24, 207, 78, 14, 124, 97, 18,
58    172, 127, 112, 38, 109, 25, 155, 79, 118, 174, 39, 198, 38, 154, 60, 238, 189, 174, 48, 128,
59    110, 154, 118, 170, 223, 92,
60];
61
62pub struct Precompile {
63    pub address: H160,
64    pub name: &'static str,
65    pub active_since_fork: Fork,
66}
67
68pub const ECRECOVER: Precompile = Precompile {
69    address: H160([
70        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
71        0x00, 0x00, 0x00, 0x00, 0x01,
72    ]),
73    name: "ECREC",
74    active_since_fork: Paris,
75};
76
77pub const SHA2_256: Precompile = Precompile {
78    address: H160([
79        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
80        0x00, 0x00, 0x00, 0x00, 0x02,
81    ]),
82    name: "SHA256",
83    active_since_fork: Paris,
84};
85
86pub const RIPEMD_160: Precompile = Precompile {
87    address: H160([
88        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
89        0x00, 0x00, 0x00, 0x00, 0x03,
90    ]),
91    name: "RIPEMD160",
92    active_since_fork: Paris,
93};
94
95pub const IDENTITY: Precompile = Precompile {
96    address: H160([
97        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
98        0x00, 0x00, 0x00, 0x00, 0x04,
99    ]),
100    name: "ID",
101    active_since_fork: Paris,
102};
103
104pub const MODEXP: Precompile = Precompile {
105    address: H160([
106        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
107        0x00, 0x00, 0x00, 0x00, 0x05,
108    ]),
109    name: "MODEXP",
110    active_since_fork: Paris,
111};
112
113pub const ECADD: Precompile = Precompile {
114    address: H160([
115        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
116        0x00, 0x00, 0x00, 0x00, 0x06,
117    ]),
118    name: "BN254_ADD",
119    active_since_fork: Paris,
120};
121
122pub const ECMUL: Precompile = Precompile {
123    address: H160([
124        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
125        0x00, 0x00, 0x00, 0x00, 0x07,
126    ]),
127    name: "BN254_MUL",
128    active_since_fork: Paris,
129};
130
131pub const ECPAIRING: Precompile = Precompile {
132    address: H160([
133        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
134        0x00, 0x00, 0x00, 0x00, 0x08,
135    ]),
136    name: "BN254_PAIRING",
137    active_since_fork: Paris,
138};
139
140pub const BLAKE2F: Precompile = Precompile {
141    address: H160([
142        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
143        0x00, 0x00, 0x00, 0x00, 0x09,
144    ]),
145    name: "BLAKE2F",
146    active_since_fork: Paris,
147};
148
149pub const POINT_EVALUATION: Precompile = Precompile {
150    address: H160([
151        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
152        0x00, 0x00, 0x00, 0x00, 0x0a,
153    ]),
154    name: "KZG_POINT_EVALUATION",
155    active_since_fork: Cancun,
156};
157
158pub const BLS12_G1ADD: Precompile = Precompile {
159    address: H160([
160        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
161        0x00, 0x00, 0x00, 0x00, 0x0b,
162    ]),
163    name: "BLS12_G1ADD",
164    active_since_fork: Prague,
165};
166
167pub const BLS12_G1MSM: Precompile = Precompile {
168    address: H160([
169        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
170        0x00, 0x00, 0x00, 0x00, 0x0c,
171    ]),
172    name: "BLS12_G1MSM",
173    active_since_fork: Prague,
174};
175
176pub const BLS12_G2ADD: Precompile = Precompile {
177    address: H160([
178        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
179        0x00, 0x00, 0x00, 0x00, 0x0d,
180    ]),
181    name: "BLS12_G2ADD",
182    active_since_fork: Prague,
183};
184
185pub const BLS12_G2MSM: Precompile = Precompile {
186    address: H160([
187        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
188        0x00, 0x00, 0x00, 0x00, 0x0e,
189    ]),
190    name: "BLS12_G2MSM",
191    active_since_fork: Prague,
192};
193
194pub const BLS12_PAIRING_CHECK: Precompile = Precompile {
195    address: H160([
196        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
197        0x00, 0x00, 0x00, 0x00, 0x0f,
198    ]),
199    name: "BLS12_PAIRING_CHECK",
200    active_since_fork: Prague,
201};
202
203pub const BLS12_MAP_FP_TO_G1: Precompile = Precompile {
204    address: H160([
205        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
206        0x00, 0x00, 0x00, 0x00, 0x10,
207    ]),
208    name: "BLS12_MAP_FP_TO_G1",
209    active_since_fork: Prague,
210};
211
212pub const BLS12_MAP_FP2_TO_G2: Precompile = Precompile {
213    address: H160([
214        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
215        0x00, 0x00, 0x00, 0x00, 0x11,
216    ]),
217    name: "BLS12_MAP_FP2_TO_G2",
218    active_since_fork: Prague,
219};
220
221pub const P256VERIFY: Precompile = Precompile {
222    address: H160([
223        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
224        0x00, 0x00, 0x00, 0x01, 0x00,
225    ]),
226    name: "P256VERIFY",
227    active_since_fork: Osaka,
228};
229
230/// EXECUTE precompile address (0x0101) for Native Rollups (EIP-8079).
231/// Active only on L1 at or above `Fork::LStar`. The L1-only + LStar gate is
232/// enforced in `is_precompile` (the `matches!(vm_type, VMType::L1)` check);
233/// `execute_precompile`'s dispatch only re-checks `fork >= LStar` and relies on
234/// `is_precompile` having already rejected the L2 case at the call site.
235/// NOTE: `EXECUTE` is intentionally NOT in the `PRECOMPILES` array, so its
236/// `active_since_fork` below is never consulted by `precompiles_for_fork` — the
237/// gating is entirely the hand-written checks above. The field is set for
238/// documentation only.
239pub const EXECUTE: Precompile = Precompile {
240    address: H160([
241        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
242        0x00, 0x00, 0x00, 0x01, 0x01,
243    ]),
244    name: "EXECUTE",
245    active_since_fork: LStar,
246};
247
248pub const PRECOMPILES: [Precompile; 18] = [
249    ECRECOVER,
250    SHA2_256,
251    RIPEMD_160,
252    IDENTITY,
253    MODEXP,
254    ECADD,
255    ECMUL,
256    ECPAIRING,
257    BLAKE2F,
258    POINT_EVALUATION,
259    BLS12_G1ADD,
260    BLS12_G1MSM,
261    BLS12_G2ADD,
262    BLS12_G2MSM,
263    BLS12_PAIRING_CHECK,
264    BLS12_MAP_FP_TO_G1,
265    BLS12_MAP_FP2_TO_G2,
266    P256VERIFY,
267];
268
269pub fn precompiles_for_fork(fork: Fork) -> impl Iterator<Item = Precompile> {
270    PRECOMPILES
271        .into_iter()
272        .filter(move |precompile| precompile.active_since_fork <= fork)
273}
274
275pub fn is_precompile(address: &Address, fork: Fork, vm_type: VMType) -> bool {
276    if fork >= Fork::LStar && matches!(vm_type, VMType::L1) && *address == EXECUTE.address {
277        return true;
278    }
279    (matches!(vm_type, VMType::L2(_)) && *address == P256VERIFY.address)
280        || precompiles_for_fork(fork).any(|precompile| precompile.address == *address)
281}
282
283/// Per-block cache for precompile results shared between warmer and executor.
284pub struct PrecompileCache {
285    cache: RwLock<FxHashMap<(Address, Bytes), (Bytes, u64)>>,
286}
287
288impl Default for PrecompileCache {
289    fn default() -> Self {
290        Self {
291            cache: RwLock::new(FxHashMap::default()),
292        }
293    }
294}
295
296impl PrecompileCache {
297    pub fn new() -> Self {
298        Self::default()
299    }
300
301    pub fn get(&self, address: &Address, calldata: &Bytes) -> Option<(Bytes, u64)> {
302        // Graceful degradation: if the lock is poisoned (a thread panicked while
303        // holding it), skip the cache rather than propagating the panic. The cache
304        // is a pure optimization — missing it only costs a recomputation.
305        self.cache
306            .read()
307            .unwrap_or_else(|poisoned| poisoned.into_inner())
308            .get(&(*address, calldata.clone()))
309            .cloned()
310    }
311
312    pub fn insert(&self, address: Address, calldata: Bytes, output: Bytes, gas_cost: u64) {
313        self.cache
314            .write()
315            .unwrap_or_else(|poisoned| poisoned.into_inner())
316            .insert((address, calldata), (output, gas_cost));
317    }
318}
319
320#[expect(clippy::as_conversions, clippy::indexing_slicing)]
321pub fn execute_precompile(
322    address: Address,
323    calldata: &Bytes,
324    gas_remaining: &mut u64,
325    fork: Fork,
326    cache: Option<&PrecompileCache>,
327    crypto: &dyn Crypto,
328    _stateless_validator: Option<&dyn crate::StatelessValidator>,
329) -> Result<Bytes, VMError> {
330    type PrecompileFn = fn(&Bytes, &mut u64, Fork, &dyn Crypto) -> Result<Bytes, VMError>;
331
332    const PRECOMPILES: [Option<PrecompileFn>; 512] = const {
333        let mut precompiles = [const { None }; 512];
334        precompiles[ECRECOVER.address.0[19] as usize] = Some(ecrecover as PrecompileFn);
335        precompiles[IDENTITY.address.0[19] as usize] = Some(identity as PrecompileFn);
336        precompiles[SHA2_256.address.0[19] as usize] = Some(sha2_256 as PrecompileFn);
337        precompiles[RIPEMD_160.address.0[19] as usize] = Some(ripemd_160 as PrecompileFn);
338        precompiles[MODEXP.address.0[19] as usize] = Some(modexp as PrecompileFn);
339        precompiles[ECADD.address.0[19] as usize] = Some(ecadd as PrecompileFn);
340        precompiles[ECMUL.address.0[19] as usize] = Some(ecmul as PrecompileFn);
341        precompiles[ECPAIRING.address.0[19] as usize] = Some(ecpairing as PrecompileFn);
342        precompiles[BLAKE2F.address.0[19] as usize] = Some(blake2f as PrecompileFn);
343        precompiles[POINT_EVALUATION.address.0[19] as usize] =
344            Some(point_evaluation as PrecompileFn);
345        precompiles[BLS12_G1ADD.address.0[19] as usize] = Some(bls12_g1add as PrecompileFn);
346        precompiles[BLS12_G1MSM.address.0[19] as usize] = Some(bls12_g1msm as PrecompileFn);
347        precompiles[BLS12_G2ADD.address.0[19] as usize] = Some(bls12_g2add as PrecompileFn);
348        precompiles[BLS12_G2MSM.address.0[19] as usize] = Some(bls12_g2msm as PrecompileFn);
349        precompiles[BLS12_PAIRING_CHECK.address.0[19] as usize] =
350            Some(bls12_pairing_check as PrecompileFn);
351        precompiles[BLS12_MAP_FP_TO_G1.address.0[19] as usize] =
352            Some(bls12_map_fp_to_g1 as PrecompileFn);
353        precompiles[BLS12_MAP_FP2_TO_G2.address.0[19] as usize] =
354            Some(bls12_map_fp2_to_g2 as PrecompileFn);
355        precompiles
356            [u16::from_be_bytes([P256VERIFY.address.0[18], P256VERIFY.address.0[19]]) as usize] =
357            Some(p_256_verify as PrecompileFn);
358        precompiles
359    };
360
361    // EXECUTE precompile is dispatched before the const table (LStar runtime gate;
362    // L1-only enforcement is at the is_precompile call site).
363    if fork >= Fork::LStar && address == EXECUTE.address {
364        return crate::execute_precompile::execute_precompile(
365            calldata,
366            gas_remaining,
367            fork,
368            crypto,
369            _stateless_validator,
370        );
371    }
372
373    if address[0..18] != [0u8; 18] {
374        return Err(VMError::Internal(InternalError::InvalidPrecompileAddress));
375    }
376    let index = u16::from_be_bytes([address[18], address[19]]) as usize;
377
378    let precompile = PRECOMPILES
379        .get(index)
380        .copied()
381        .flatten()
382        .ok_or(VMError::Internal(InternalError::InvalidPrecompileAddress))?;
383
384    // Check cache (skip identity -- copy is cheaper than lookup)
385    if address != IDENTITY.address
386        && let Some((output, gas_cost)) = cache.and_then(|c| c.get(&address, calldata))
387    {
388        increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
389        return Ok(output);
390    }
391
392    #[cfg(feature = "perf_opcode_timings")]
393    let precompile_time_start = std::time::Instant::now();
394
395    let gas_before = *gas_remaining;
396    let result = precompile(calldata, gas_remaining, fork, crypto);
397
398    #[cfg(feature = "perf_opcode_timings")]
399    {
400        let time = precompile_time_start.elapsed();
401        let mut timings = crate::timings::PRECOMPILES_TIMINGS.lock().expect("poison");
402        timings.update(address, time);
403    }
404
405    // Cache result on success (skip identity)
406    if address != IDENTITY.address
407        && let Some(cache) = cache
408        && let Ok(output) = &result
409    {
410        let gas_cost = gas_before.saturating_sub(*gas_remaining);
411        cache.insert(address, calldata.clone(), output.clone(), gas_cost);
412    }
413
414    result
415}
416
417/// Consumes gas and if it's higher than the gas limit returns an error.
418pub(crate) fn increase_precompile_consumed_gas(
419    gas_cost: u64,
420    gas_remaining: &mut u64,
421) -> Result<(), VMError> {
422    *gas_remaining = gas_remaining
423        .checked_sub(gas_cost)
424        .ok_or(PrecompileError::NotEnoughGas)?;
425    Ok(())
426}
427
428/// When slice length is less than `target_len`, the rest is filled with zeros. If slice length is
429/// more than `target_len`, the excess bytes are kept.
430#[inline(always)]
431pub(crate) fn fill_with_zeros(calldata: &Bytes, target_len: usize) -> Bytes {
432    if calldata.len() >= target_len {
433        // this clone is cheap (Arc)
434        return calldata.clone();
435    }
436    let mut padded_calldata = calldata.to_vec();
437    padded_calldata.resize(target_len, 0);
438    padded_calldata.into()
439}
440
441fn crypto_error_to_precompile(e: CryptoError) -> VMError {
442    match e {
443        CryptoError::InvalidPoint(_) => PrecompileError::InvalidPoint.into(),
444        CryptoError::InvalidInput(_) => PrecompileError::ParsingInputError.into(),
445        CryptoError::InvalidSignature => PrecompileError::ParsingInputError.into(),
446        CryptoError::InvalidRecoveryId => PrecompileError::ParsingInputError.into(),
447        CryptoError::RecoveryFailed => PrecompileError::ParsingInputError.into(),
448        CryptoError::VerificationFailed => PrecompileError::ParsingInputError.into(),
449        CryptoError::Other(_) => PrecompileError::ParsingInputError.into(),
450        // Misconfiguration rather than bad input: the BLS12-381 backend is
451        // unavailable (host built without `blst`, or a provider failed to
452        // override the trait default). Surface it as an internal error.
453        CryptoError::Unsupported(msg) => InternalError::Custom(msg.to_string()).into(),
454    }
455}
456
457/// Map crypto errors for BLS12-381 G1 operations (add).
458/// The old code returned BLS12381G1PointNotInCurve for invalid G1 points.
459fn bls12_g1_crypto_error(e: CryptoError) -> VMError {
460    match e {
461        CryptoError::InvalidPoint(_) => PrecompileError::BLS12381G1PointNotInCurve.into(),
462        other => crypto_error_to_precompile(other),
463    }
464}
465
466/// Map crypto errors for BLS12-381 G2 operations (add).
467/// The old code returned BLS12381G2PointNotInCurve for invalid G2 points.
468fn bls12_g2_crypto_error(e: CryptoError) -> VMError {
469    match e {
470        CryptoError::InvalidPoint(_) => PrecompileError::BLS12381G2PointNotInCurve.into(),
471        other => crypto_error_to_precompile(other),
472    }
473}
474
475/// Map crypto errors for BLS12-381 MSM and pairing operations.
476/// The old code used `G1Affine::from_uncompressed` (checked), which returned
477/// ParsingInputError for any invalid point (not on curve or not in subgroup).
478fn bls12_msm_pairing_crypto_error(e: CryptoError) -> VMError {
479    match e {
480        CryptoError::InvalidPoint(_) => PrecompileError::ParsingInputError.into(),
481        other => crypto_error_to_precompile(other),
482    }
483}
484
485/// ## ECRECOVER precompile.
486/// Elliptic curve digital signature algorithm (ECDSA) public key recovery function.
487///
488/// Input is 128 bytes (padded with zeros if shorter):
489///   [0..32)  : keccak-256 hash (message digest)
490///   [32..64) : v (27 or 28)
491///   [64..128): r||s (64 bytes)
492///
493/// Returns the recovered address.
494pub fn ecrecover(
495    calldata: &Bytes,
496    gas_remaining: &mut u64,
497    _fork: Fork,
498    crypto: &dyn Crypto,
499) -> Result<Bytes, VMError> {
500    use crate::gas_cost::ECRECOVER_COST;
501
502    increase_precompile_consumed_gas(ECRECOVER_COST, gas_remaining)?;
503
504    const INPUT_LEN: usize = 128;
505    const WORD: usize = 32;
506
507    let input = fill_with_zeros(calldata, INPUT_LEN);
508
509    #[expect(
510        clippy::indexing_slicing,
511        reason = "fill_with_zeros guarantees len >= 128"
512    )]
513    let raw_hash: &[u8] = &input[0..WORD];
514    #[expect(
515        clippy::indexing_slicing,
516        reason = "fill_with_zeros guarantees len >= 128"
517    )]
518    let raw_v: &[u8] = &input[WORD..WORD * 2];
519    #[expect(
520        clippy::indexing_slicing,
521        reason = "fill_with_zeros guarantees len >= 128"
522    )]
523    let raw_sig: &[u8] = &input[WORD * 2..WORD * 2 + 64];
524
525    // EVM expects v ∈ {27, 28}. Anything else is invalid → empty return.
526    let recid_byte: u8 = match u8::try_from(u256_from_big_endian(raw_v)) {
527        Ok(27) => 0,
528        Ok(28) => 1,
529        _ => return Ok(Bytes::new()),
530    };
531
532    let msg_hash: [u8; 32] = raw_hash
533        .try_into()
534        .map_err(|_| InternalError::TypeConversion)?;
535    let sig: [u8; 64] = raw_sig
536        .try_into()
537        .map_err(|_| InternalError::TypeConversion)?;
538
539    let pk_hash = match crypto.secp256k1_ecrecover(&sig, recid_byte, &msg_hash) {
540        Ok(h) => h,
541        Err(_) => return Ok(Bytes::new()),
542    };
543
544    // Address is the last 20 bytes of the keccak hash of the public key.
545    let mut out = [0u8; 32];
546    out[12..32].copy_from_slice(&pk_hash[12..32]);
547
548    Ok(Bytes::copy_from_slice(&out))
549}
550
551/// Returns the calldata received
552pub fn identity(
553    calldata: &Bytes,
554    gas_remaining: &mut u64,
555    _fork: Fork,
556    _crypto: &dyn Crypto,
557) -> Result<Bytes, VMError> {
558    let gas_cost = gas_cost::identity(calldata.len())?;
559
560    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
561
562    Ok(calldata.clone())
563}
564
565/// Returns the calldata hashed by sha2-256 algorithm
566pub fn sha2_256(
567    calldata: &Bytes,
568    gas_remaining: &mut u64,
569    _fork: Fork,
570    crypto: &dyn Crypto,
571) -> Result<Bytes, VMError> {
572    let gas_cost = gas_cost::sha2_256(calldata.len())?;
573
574    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
575
576    let digest = crypto.sha256(calldata);
577    Ok(Bytes::copy_from_slice(&digest))
578}
579
580/// Returns the calldata hashed by ripemd-160 algorithm, padded by zeros at left
581pub fn ripemd_160(
582    calldata: &Bytes,
583    gas_remaining: &mut u64,
584    _fork: Fork,
585    crypto: &dyn Crypto,
586) -> Result<Bytes, VMError> {
587    let gas_cost = gas_cost::ripemd_160(calldata.len())?;
588
589    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
590
591    let result = crypto.ripemd160(calldata);
592    Ok(Bytes::copy_from_slice(&result))
593}
594
595/// Returns the result of the module-exponentiation operation
596#[expect(clippy::indexing_slicing, reason = "bounds checked at start")]
597pub fn modexp(
598    calldata: &Bytes,
599    gas_remaining: &mut u64,
600    fork: Fork,
601    crypto: &dyn Crypto,
602) -> Result<Bytes, VMError> {
603    // If calldata does not reach the required length, we should fill the rest with zeros
604    let calldata = fill_with_zeros(calldata, 96);
605
606    // Defer converting to a U256 after the zero check.
607    if fork < Fork::Osaka {
608        let base_size_bytes: [u8; 32] = calldata[0..32].try_into()?;
609        let modulus_size_bytes: [u8; 32] = calldata[64..96].try_into()?;
610        const ZERO_BYTES: [u8; 32] = [0u8; 32];
611
612        if base_size_bytes == ZERO_BYTES && modulus_size_bytes == ZERO_BYTES {
613            // On Berlin or newer there is a floor cost for the modexp precompile
614            increase_precompile_consumed_gas(MODEXP_STATIC_COST, gas_remaining)?;
615            return Ok(Bytes::new());
616        }
617    }
618
619    // The try_into are infallible and the compiler optimizes them out, even without unsafe.
620    // https://godbolt.org/z/h8rW8M3c4
621    let base_size = u256_from_big_endian_const::<32>(calldata[0..32].try_into()?);
622    let modulus_size = u256_from_big_endian_const::<32>(calldata[64..96].try_into()?);
623    let exponent_size = u256_from_big_endian_const::<32>(calldata[32..64].try_into()?);
624
625    if fork >= Fork::Osaka {
626        if base_size > U256::from(1024) {
627            return Err(PrecompileError::ModExpBaseTooLarge.into());
628        }
629        if exponent_size > U256::from(1024) {
630            return Err(PrecompileError::ModExpExpTooLarge.into());
631        }
632        if modulus_size > U256::from(1024) {
633            return Err(PrecompileError::ModExpModulusTooLarge.into());
634        }
635    }
636
637    // Because on some cases conversions to usize exploded before the check of the zero value could be done
638    let base_size = usize::try_from(base_size).map_err(|_| PrecompileError::ParsingInputError)?;
639    let exponent_size =
640        usize::try_from(exponent_size).map_err(|_| PrecompileError::ParsingInputError)?;
641    let modulus_size =
642        usize::try_from(modulus_size).map_err(|_| PrecompileError::ParsingInputError)?;
643
644    let base_limit = base_size.checked_add(96).ok_or(InternalError::Overflow)?;
645
646    let exponent_limit = exponent_size
647        .checked_add(base_limit)
648        .ok_or(InternalError::Overflow)?;
649
650    let modulus_limit = modulus_size
651        .checked_add(exponent_limit)
652        .ok_or(InternalError::Overflow)?;
653
654    let b = get_slice_or_default(&calldata, 96, base_limit, base_size);
655    let e = get_slice_or_default(&calldata, base_limit, exponent_limit, exponent_size);
656    let m = get_slice_or_default(&calldata, exponent_limit, modulus_limit, modulus_size);
657
658    // Gas computation uses malachite Natural to compute bit length of exponent
659    use malachite::Natural;
660    use malachite::base::num::conversion::traits::*;
661    let exp_first_32_bytes = e.get(0..32.min(exponent_size)).unwrap_or_default();
662    let exp_first_32 =
663        Natural::from_power_of_2_digits_desc(8u64, exp_first_32_bytes.iter().cloned())
664            .ok_or(InternalError::TypeConversion)?;
665
666    let gas_cost = gas_cost::modexp(&exp_first_32, base_size, exponent_size, modulus_size, fork)?;
667
668    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
669
670    if base_size == 0 && modulus_size == 0 {
671        return Ok(Bytes::new());
672    }
673
674    let result = crypto
675        .modexp(&b, &e, &m)
676        .map_err(|_| VMError::from(PrecompileError::ParsingInputError))?;
677
678    let res_bytes = increase_left_pad(&Bytes::from(result), modulus_size);
679
680    Ok(res_bytes.slice(..modulus_size))
681}
682
683/// This function returns the slice between the lower and upper limit of the calldata (as a vector),
684/// padding with zeros at the end if necessary.
685///
686/// Uses Cow so that the best case of no resizing doesn't require an allocation.
687#[expect(clippy::indexing_slicing, reason = "bounds checked")]
688fn get_slice_or_default<'c>(
689    calldata: &'c Bytes,
690    lower_limit: usize,
691    upper_limit: usize,
692    size_to_expand: usize,
693) -> Cow<'c, [u8]> {
694    let upper_limit = calldata.len().min(upper_limit);
695    if let Some(data) = calldata.get(lower_limit..upper_limit)
696        && !data.is_empty()
697    {
698        if data.len() == size_to_expand {
699            return data.into();
700        }
701        let mut extended = vec![0u8; size_to_expand];
702        let copy_size = size_to_expand.min(data.len());
703        extended[..copy_size].copy_from_slice(&data[..copy_size]);
704        return extended.into();
705    }
706    Vec::new().into()
707}
708
709/// If the result size is less than needed, pads left with zeros.
710#[inline(always)]
711pub fn increase_left_pad(result: &Bytes, m_size: usize) -> Bytes {
712    #[expect(
713        clippy::arithmetic_side_effects,
714        clippy::indexing_slicing,
715        reason = "overflow checked with the if condition, bounds checked"
716    )]
717    if result.len() < m_size {
718        let mut padded_result = vec![0u8; m_size];
719        let size_diff = m_size - result.len();
720        padded_result[size_diff..].copy_from_slice(result);
721
722        padded_result.into()
723    } else {
724        // this clone is cheap (Arc)
725        result.clone()
726    }
727}
728
729/// Makes a point addition on the elliptic curve 'alt_bn128'
730pub fn ecadd(
731    calldata: &Bytes,
732    gas_remaining: &mut u64,
733    _fork: Fork,
734    crypto: &dyn Crypto,
735) -> Result<Bytes, VMError> {
736    // If calldata does not reach the required length, we should fill the rest with zeros
737    let calldata = fill_with_zeros(calldata, 128);
738
739    increase_precompile_consumed_gas(ECADD_COST, gas_remaining)?;
740
741    let (Some(first_point), Some(second_point)) =
742        (parse_bn254_g1(&calldata, 0), parse_bn254_g1(&calldata, 64))
743    else {
744        return Err(InternalError::Slicing.into());
745    };
746    validate_bn254_g1_coords(&first_point)?;
747    validate_bn254_g1_coords(&second_point)?;
748
749    #[expect(clippy::indexing_slicing, reason = "calldata padded to 128 bytes")]
750    let result = crypto
751        .bn254_g1_add(&calldata[..64], &calldata[64..128])
752        .map_err(crypto_error_to_precompile)?;
753
754    Ok(Bytes::copy_from_slice(&result))
755}
756
757/// Makes a scalar multiplication on the elliptic curve 'alt_bn128'
758pub fn ecmul(
759    calldata: &Bytes,
760    gas_remaining: &mut u64,
761    _fork: Fork,
762    crypto: &dyn Crypto,
763) -> Result<Bytes, VMError> {
764    // If calldata does not reach the required length, we should fill the rest with zeros
765    let calldata = fill_with_zeros(calldata, 96);
766    increase_precompile_consumed_gas(ECMUL_COST, gas_remaining)?;
767
768    let (Some(g1), Some(_scalar)) = (
769        parse_bn254_g1(&calldata, 0),
770        parse_bn254_scalar(&calldata, 64),
771    ) else {
772        return Err(InternalError::Slicing.into());
773    };
774    validate_bn254_g1_coords(&g1)?;
775
776    #[expect(clippy::indexing_slicing, reason = "calldata padded to 96 bytes")]
777    let result = crypto
778        .bn254_g1_mul(&calldata[..64], &calldata[64..96])
779        .map_err(crypto_error_to_precompile)?;
780
781    Ok(Bytes::copy_from_slice(&result))
782}
783
784const ALT_BN128_PRIME: U256 = U256([
785    0x3c208c16d87cfd47,
786    0x97816a916871ca8d,
787    0xb85045b68181585d,
788    0x30644e72e131a029,
789]);
790
791pub struct G1(U256, U256);
792impl G1 {
793    /// According to EIP-197, the point at infinity (also called neutral element of G1 or zero) is encoded as (0, 0)
794    pub fn is_zero(&self) -> bool {
795        self.0.is_zero() && self.1.is_zero()
796    }
797}
798pub struct G2(U256, U256, U256, U256);
799impl G2 {
800    /// According to EIP-197, the point at infinity (also called neutral element of G2 or zero) is encoded as (0, 0, 0, 0)
801    pub fn is_zero(&self) -> bool {
802        self.0.is_zero() && self.1.is_zero() && self.2.is_zero() && self.3.is_zero()
803    }
804}
805
806/// Parses 32 bytes as BN254 scalar
807#[inline]
808fn parse_bn254_scalar(buf: &[u8], offset: usize) -> Option<U256> {
809    buf.get(offset..offset.checked_add(32)?)
810        .map(u256_from_big_endian)
811}
812
813/// Parses 64 bytes as a BN254 G1 point
814#[inline]
815fn parse_bn254_g1(buf: &[u8], offset: usize) -> Option<G1> {
816    let chunk = buf.get(offset..offset.checked_add(64)?)?;
817    let (x_bytes, y_bytes) = chunk.split_at_checked(32)?;
818    Some(G1(
819        u256_from_big_endian(x_bytes),
820        u256_from_big_endian(y_bytes),
821    ))
822}
823
824/// Parses 128 bytes as a BN254 G2 point
825fn parse_bn254_g2(buf: &[u8], offset: usize) -> Option<G2> {
826    let chunk = buf.get(offset..offset.checked_add(128)?)?;
827    let (g2_xy, rest) = chunk.split_at_checked(32)?;
828    let (g2_xx, rest) = rest.split_at_checked(32)?;
829    let (g2_yy, g2_yx) = rest.split_at_checked(32)?;
830    Some(G2(
831        u256_from_big_endian(g2_xx),
832        u256_from_big_endian(g2_xy),
833        u256_from_big_endian(g2_yx),
834        u256_from_big_endian(g2_yy),
835    ))
836}
837
838#[inline]
839fn validate_bn254_g1_coords(g1: &G1) -> Result<(), VMError> {
840    // check each element is in field
841    if g1.0 >= ALT_BN128_PRIME || g1.1 >= ALT_BN128_PRIME {
842        return Err(PrecompileError::CoordinateExceedsFieldModulus.into());
843    }
844    Ok(())
845}
846
847#[inline]
848fn validate_bn254_g2_coords(g2: &G2) -> Result<(), VMError> {
849    // check each element is in field
850    if g2.0 >= ALT_BN128_PRIME
851        || g2.1 >= ALT_BN128_PRIME
852        || g2.2 >= ALT_BN128_PRIME
853        || g2.3 >= ALT_BN128_PRIME
854    {
855        return Err(PrecompileError::CoordinateExceedsFieldModulus.into());
856    }
857    Ok(())
858}
859
860/// Performs a bilinear pairing on points on the elliptic curve 'alt_bn128', returns 1 on success and 0 on failure
861pub fn ecpairing(
862    calldata: &Bytes,
863    gas_remaining: &mut u64,
864    _fork: Fork,
865    crypto: &dyn Crypto,
866) -> Result<Bytes, VMError> {
867    // The input must always be a multiple of 192 (6 32-byte values)
868    if !calldata.len().is_multiple_of(192) {
869        return Err(PrecompileError::ParsingInputError.into());
870    }
871
872    let inputs_amount = calldata.len() / 192;
873    let gas_cost = gas_cost::ecpairing(inputs_amount)?;
874    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
875
876    let mut pairs: Vec<(&[u8], &[u8])> = Vec::new();
877    for input in calldata.chunks_exact(192) {
878        let (Some(g1), Some(g2)) = (parse_bn254_g1(input, 0), parse_bn254_g2(input, 64)) else {
879            return Err(InternalError::Slicing.into());
880        };
881        validate_bn254_g1_coords(&g1)?;
882        validate_bn254_g2_coords(&g2)?;
883        #[expect(clippy::indexing_slicing, reason = "chunks_exact guarantees 192 bytes")]
884        pairs.push((&input[..64], &input[64..192]));
885    }
886
887    let pairing_check = if pairs.is_empty() {
888        true
889    } else {
890        crypto
891            .bn254_pairing_check(&pairs)
892            .map_err(crypto_error_to_precompile)?
893    };
894
895    let mut result = [0; 32];
896    result[31] = u8::from(pairing_check);
897    Ok(Bytes::from_owner(result))
898}
899
900/// Returns the result of Blake2 hashing algorithm given a certain parameters from the calldata.
901pub fn blake2f(
902    calldata: &Bytes,
903    gas_remaining: &mut u64,
904    _fork: Fork,
905    crypto: &dyn Crypto,
906) -> Result<Bytes, VMError> {
907    if calldata.len() != 213 {
908        return Err(PrecompileError::ParsingInputError.into());
909    }
910
911    let mut calldata = calldata.slice(0..213);
912
913    let rounds = calldata.get_u32();
914
915    let gas_cost = u64::from(rounds) * BLAKE2F_ROUND_COST;
916    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
917
918    let mut h = [0; 8];
919
920    h.copy_from_slice(&std::array::from_fn::<u64, 8, _>(|_| calldata.get_u64_le()));
921
922    let mut m = [0; 16];
923
924    m.copy_from_slice(&std::array::from_fn::<u64, 16, _>(|_| {
925        calldata.get_u64_le()
926    }));
927
928    let mut t = [0; 2];
929    t.copy_from_slice(&std::array::from_fn::<u64, 2, _>(|_| calldata.get_u64_le()));
930
931    let f = calldata.get_u8();
932    if f != 0 && f != 1 {
933        return Err(PrecompileError::ParsingInputError.into());
934    }
935    let f = f == 1;
936
937    crypto.blake2_compress(rounds, &mut h, m, t, f);
938
939    Ok(Bytes::from_iter(
940        h.into_iter().flat_map(|value| value.to_le_bytes()),
941    ))
942}
943
944/// Converts the provided commitment to match the provided versioned_hash.
945fn kzg_commitment_to_versioned_hash(commitment_bytes: &[u8; 48], crypto: &dyn Crypto) -> H256 {
946    let mut versioned_hash: [u8; 32] = crypto.sha256(commitment_bytes);
947    versioned_hash[0] = VERSIONED_HASH_VERSION_KZG;
948    versioned_hash.into()
949}
950
951const POINT_EVALUATION_OUTPUT_BYTES: [u8; 64] = [
952    // Big endian FIELD_ELEMENTS_PER_BLOB bytes
953    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
954    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
955    // Big endian BLS_MODULUS bytes
956    0x73, 0xED, 0xA7, 0x53, 0x29, 0x9D, 0x7D, 0x48, 0x33, 0x39, 0xD8, 0x08, 0x09, 0xA1, 0xD8, 0x05,
957    0x53, 0xBD, 0xA4, 0x02, 0xFF, 0xFE, 0x5B, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01,
958];
959
960/// Makes verifications on the received point, proof and commitment, if true returns a constant value
961fn point_evaluation(
962    calldata: &Bytes,
963    gas_remaining: &mut u64,
964    _fork: Fork,
965    crypto: &dyn Crypto,
966) -> Result<Bytes, VMError> {
967    if calldata.len() != 192 {
968        return Err(PrecompileError::ParsingInputError.into());
969    }
970
971    // Consume gas
972    let gas_cost = POINT_EVALUATION_COST;
973    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
974
975    // Parse inputs
976    let versioned_hash: [u8; 32] = calldata
977        .get(..32)
978        .ok_or(InternalError::Slicing)?
979        .try_into()
980        .map_err(|_| InternalError::TypeConversion)?;
981
982    let z: [u8; 32] = calldata
983        .get(32..64)
984        .ok_or(InternalError::Slicing)?
985        .try_into()
986        .map_err(|_| InternalError::TypeConversion)?;
987
988    let y: [u8; 32] = calldata
989        .get(64..96)
990        .ok_or(InternalError::Slicing)?
991        .try_into()
992        .map_err(|_| InternalError::TypeConversion)?;
993
994    let commitment: [u8; 48] = calldata
995        .get(96..144)
996        .ok_or(InternalError::Slicing)?
997        .try_into()
998        .map_err(|_| InternalError::TypeConversion)?;
999
1000    let proof: [u8; 48] = calldata
1001        .get(144..192)
1002        .ok_or(InternalError::Slicing)?
1003        .try_into()
1004        .map_err(|_| InternalError::TypeConversion)?;
1005
1006    // Perform the evaluation
1007
1008    // This checks if the commitment is equal to the versioned hash
1009    if kzg_commitment_to_versioned_hash(&commitment, crypto) != H256::from(versioned_hash) {
1010        return Err(PrecompileError::ParsingInputError.into());
1011    }
1012
1013    // This verifies the proof from a point (z, y) and a commitment
1014    crypto
1015        .verify_kzg_proof(&z, &y, &commitment, &proof)
1016        .map_err(|_| VMError::from(PrecompileError::ParsingInputError))?;
1017
1018    // The first 32 bytes consist of the number of field elements in the blob, and the
1019    // other 32 bytes consist of the modulus used in the BLS signature scheme.
1020    let output = POINT_EVALUATION_OUTPUT_BYTES.to_vec();
1021
1022    Ok(Bytes::from(output))
1023}
1024
1025/// Signature verification in the "secp256r1" elliptic curve
1026/// If the verification succeeds, returns 1 in a 32-bit big-endian format.
1027/// If the verification fails, returns an empty `Bytes` object.
1028/// Implemented following https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7951.md
1029pub fn p_256_verify(
1030    calldata: &Bytes,
1031    gas_remaining: &mut u64,
1032    _fork: Fork,
1033    crypto: &dyn Crypto,
1034) -> Result<Bytes, VMError> {
1035    increase_precompile_consumed_gas(P256_VERIFY_COST, gas_remaining)
1036        .map_err(|_| PrecompileError::NotEnoughGas)?;
1037
1038    // Validate input data length is 160 bytes
1039    if calldata.len() != 160 {
1040        return Ok(Bytes::new());
1041    }
1042
1043    // Parse parameters
1044    #[expect(
1045        clippy::indexing_slicing,
1046        reason = "length of the calldata is checked before slicing"
1047    )]
1048    let msg: &[u8; 32] = calldata[0..32].try_into()?;
1049    #[expect(clippy::indexing_slicing, reason = "length checked")]
1050    let r: &[u8; 32] = calldata[32..64].try_into()?;
1051    #[expect(clippy::indexing_slicing, reason = "length checked")]
1052    let s: &[u8; 32] = calldata[64..96].try_into()?;
1053    #[expect(clippy::indexing_slicing, reason = "length checked")]
1054    let pk_x: &[u8; 32] = calldata[96..128].try_into()?;
1055    #[expect(clippy::indexing_slicing, reason = "length checked")]
1056    let pk_y: &[u8; 32] = calldata[128..160].try_into()?;
1057
1058    // Build 64-byte sig (r||s) and 64-byte pk (x||y)
1059    let mut sig_bytes = [0u8; 64];
1060    sig_bytes[..32].copy_from_slice(r);
1061    sig_bytes[32..].copy_from_slice(s);
1062
1063    let mut pk_bytes = [0u8; 64];
1064    pk_bytes[..32].copy_from_slice(pk_x);
1065    pk_bytes[32..].copy_from_slice(pk_y);
1066
1067    let success = crypto.secp256r1_verify(msg, &sig_bytes, &pk_bytes);
1068
1069    // If the verification succeeds, returns 1 in a 32-bit big-endian format.
1070    // If the verification fails, returns an empty `Bytes` object.
1071    if success {
1072        const RESULT: [u8; 32] = [
1073            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1074            0, 0, 1,
1075        ];
1076        Ok(Bytes::from_static(&RESULT))
1077    } else {
1078        Ok(Bytes::new())
1079    }
1080}
1081
1082/// Parse a 64-byte padded BLS12-381 field element into a 48-byte unpadded element.
1083/// The first 16 bytes must be zero (padding). Returns error if padding is invalid.
1084fn parse_bls12_padded_fp(padded: &[u8; 64]) -> Result<[u8; 48], VMError> {
1085    if padded[..16] != [0u8; 16] {
1086        return Err(PrecompileError::ParsingInputError.into());
1087    }
1088    let fp: [u8; 48] = padded[16..64]
1089        .try_into()
1090        .map_err(|_| InternalError::TypeConversion)?;
1091    Ok(fp)
1092}
1093
1094pub fn bls12_g1add(
1095    calldata: &Bytes,
1096    gas_remaining: &mut u64,
1097    _fork: Fork,
1098    crypto: &dyn Crypto,
1099) -> Result<Bytes, VMError> {
1100    let (x_data, calldata) = calldata
1101        .split_first_chunk::<128>()
1102        .ok_or(PrecompileError::ParsingInputError)?;
1103    let (y_data, calldata) = calldata
1104        .split_first_chunk::<128>()
1105        .ok_or(PrecompileError::ParsingInputError)?;
1106    if !calldata.is_empty() {
1107        return Err(PrecompileError::ParsingInputError.into());
1108    }
1109
1110    // Apply precompile gas cost.
1111    increase_precompile_consumed_gas(BLS12_381_G1ADD_COST, gas_remaining)
1112        .map_err(|_| PrecompileError::NotEnoughGas)?;
1113
1114    // Parse two 128-byte padded G1 points into 48-byte unpadded coordinates.
1115    let ax = parse_bls12_padded_fp(
1116        x_data[..64]
1117            .try_into()
1118            .map_err(|_| InternalError::TypeConversion)?,
1119    )?;
1120    let ay = parse_bls12_padded_fp(
1121        x_data[64..128]
1122            .try_into()
1123            .map_err(|_| InternalError::TypeConversion)?,
1124    )?;
1125    let bx = parse_bls12_padded_fp(
1126        y_data[..64]
1127            .try_into()
1128            .map_err(|_| InternalError::TypeConversion)?,
1129    )?;
1130    let by = parse_bls12_padded_fp(
1131        y_data[64..128]
1132            .try_into()
1133            .map_err(|_| InternalError::TypeConversion)?,
1134    )?;
1135
1136    let result = crypto
1137        .bls12_381_g1_add((ax, ay), (bx, by))
1138        .map_err(bls12_g1_crypto_error)?;
1139
1140    // Re-pad the 96-byte unpadded result (x||y each 48 bytes) to 128 bytes.
1141    let mut output = [0u8; 128];
1142    output[16..64].copy_from_slice(&result[..48]);
1143    output[80..128].copy_from_slice(&result[48..96]);
1144    Ok(Bytes::copy_from_slice(&output))
1145}
1146
1147pub fn bls12_g1msm(
1148    calldata: &Bytes,
1149    gas_remaining: &mut u64,
1150    _fork: Fork,
1151    crypto: &dyn Crypto,
1152) -> Result<Bytes, VMError> {
1153    if calldata.is_empty() || !calldata.len().is_multiple_of(BLS12_381_G1_MSM_PAIR_LENGTH) {
1154        return Err(PrecompileError::ParsingInputError.into());
1155    }
1156
1157    let k = calldata.len() / BLS12_381_G1_MSM_PAIR_LENGTH;
1158    let required_gas = gas_cost::bls12_msm(k, &BLS12_381_G1_K_DISCOUNT, G1_MUL_COST)?;
1159    increase_precompile_consumed_gas(required_gas, gas_remaining)?;
1160
1161    #[allow(clippy::type_complexity)]
1162    let mut pairs: Vec<(([u8; 48], [u8; 48]), [u8; 32])> = Vec::with_capacity(k);
1163
1164    #[expect(
1165        clippy::arithmetic_side_effects,
1166        clippy::indexing_slicing,
1167        reason = "bounds checked"
1168    )]
1169    for i in 0..k {
1170        let point_offset = i * BLS12_381_G1_MSM_PAIR_LENGTH;
1171        let scalar_offset = point_offset + 128;
1172        let pair_end = scalar_offset + 32;
1173
1174        let point_bytes = &calldata[point_offset..scalar_offset];
1175        let scalar_bytes = &calldata[scalar_offset..pair_end];
1176
1177        let px = parse_bls12_padded_fp(
1178            point_bytes[..64]
1179                .try_into()
1180                .map_err(|_| InternalError::TypeConversion)?,
1181        )?;
1182        let py = parse_bls12_padded_fp(
1183            point_bytes[64..128]
1184                .try_into()
1185                .map_err(|_| InternalError::TypeConversion)?,
1186        )?;
1187        let scalar: [u8; 32] = scalar_bytes
1188            .try_into()
1189            .map_err(|_| InternalError::TypeConversion)?;
1190
1191        pairs.push(((px, py), scalar));
1192    }
1193
1194    let result = crypto
1195        .bls12_381_g1_msm(&pairs)
1196        .map_err(bls12_msm_pairing_crypto_error)?;
1197
1198    // Re-pad output: 96-byte result → 128-byte padded
1199    let mut output = [0u8; 128];
1200    output[16..64].copy_from_slice(&result[..48]);
1201    output[80..128].copy_from_slice(&result[48..96]);
1202    Ok(Bytes::copy_from_slice(&output))
1203}
1204
1205pub fn bls12_g2add(
1206    calldata: &Bytes,
1207    gas_remaining: &mut u64,
1208    _fork: Fork,
1209    crypto: &dyn Crypto,
1210) -> Result<Bytes, VMError> {
1211    let (x_data, calldata) = calldata
1212        .split_first_chunk::<256>()
1213        .ok_or(PrecompileError::ParsingInputError)?;
1214    let (y_data, calldata) = calldata
1215        .split_first_chunk::<256>()
1216        .ok_or(PrecompileError::ParsingInputError)?;
1217    if !calldata.is_empty() {
1218        return Err(PrecompileError::ParsingInputError.into());
1219    }
1220
1221    // Apply precompile gas cost.
1222    increase_precompile_consumed_gas(BLS12_381_G2ADD_COST, gas_remaining)
1223        .map_err(|_| PrecompileError::NotEnoughGas)?;
1224
1225    // Parse two 256-byte padded G2 points into four 48-byte unpadded coordinates each.
1226    // G2 point layout: x_0(64) || x_1(64) || y_0(64) || y_1(64) = 256 bytes
1227    let ax0 = parse_bls12_padded_fp(
1228        x_data[0..64]
1229            .try_into()
1230            .map_err(|_| InternalError::TypeConversion)?,
1231    )?;
1232    let ax1 = parse_bls12_padded_fp(
1233        x_data[64..128]
1234            .try_into()
1235            .map_err(|_| InternalError::TypeConversion)?,
1236    )?;
1237    let ay0 = parse_bls12_padded_fp(
1238        x_data[128..192]
1239            .try_into()
1240            .map_err(|_| InternalError::TypeConversion)?,
1241    )?;
1242    let ay1 = parse_bls12_padded_fp(
1243        x_data[192..256]
1244            .try_into()
1245            .map_err(|_| InternalError::TypeConversion)?,
1246    )?;
1247
1248    let bx0 = parse_bls12_padded_fp(
1249        y_data[0..64]
1250            .try_into()
1251            .map_err(|_| InternalError::TypeConversion)?,
1252    )?;
1253    let bx1 = parse_bls12_padded_fp(
1254        y_data[64..128]
1255            .try_into()
1256            .map_err(|_| InternalError::TypeConversion)?,
1257    )?;
1258    let by0 = parse_bls12_padded_fp(
1259        y_data[128..192]
1260            .try_into()
1261            .map_err(|_| InternalError::TypeConversion)?,
1262    )?;
1263    let by1 = parse_bls12_padded_fp(
1264        y_data[192..256]
1265            .try_into()
1266            .map_err(|_| InternalError::TypeConversion)?,
1267    )?;
1268
1269    let result = crypto
1270        .bls12_381_g2_add((ax0, ax1, ay0, ay1), (bx0, bx1, by0, by1))
1271        .map_err(bls12_g2_crypto_error)?;
1272
1273    // Re-pad the 192-byte unpadded result to 256 bytes.
1274    // Unpadded: x_0(48) || x_1(48) || y_0(48) || y_1(48) = 192 bytes
1275    // Padded output: x_0_padded(64) || x_1_padded(64) || y_0_padded(64) || y_1_padded(64) = 256 bytes
1276    let mut output = [0u8; 256];
1277    output[16..64].copy_from_slice(&result[0..48]);
1278    output[80..128].copy_from_slice(&result[48..96]);
1279    output[144..192].copy_from_slice(&result[96..144]);
1280    output[208..256].copy_from_slice(&result[144..192]);
1281    Ok(Bytes::copy_from_slice(&output))
1282}
1283
1284pub fn bls12_g2msm(
1285    calldata: &Bytes,
1286    gas_remaining: &mut u64,
1287    _fork: Fork,
1288    crypto: &dyn Crypto,
1289) -> Result<Bytes, VMError> {
1290    if calldata.is_empty() || !calldata.len().is_multiple_of(BLS12_381_G2_MSM_PAIR_LENGTH) {
1291        return Err(PrecompileError::ParsingInputError.into());
1292    }
1293
1294    let k = calldata.len() / BLS12_381_G2_MSM_PAIR_LENGTH;
1295    let required_gas = gas_cost::bls12_msm(k, &BLS12_381_G2_K_DISCOUNT, G2_MUL_COST)?;
1296    increase_precompile_consumed_gas(required_gas, gas_remaining)?;
1297
1298    #[allow(clippy::type_complexity)]
1299    let mut pairs: Vec<(([u8; 48], [u8; 48], [u8; 48], [u8; 48]), [u8; 32])> =
1300        Vec::with_capacity(k);
1301
1302    #[expect(
1303        clippy::indexing_slicing,
1304        clippy::arithmetic_side_effects,
1305        reason = "bounds checked"
1306    )]
1307    for i in 0..k {
1308        let point_offset = i * BLS12_381_G2_MSM_PAIR_LENGTH;
1309        let scalar_offset = point_offset + 256;
1310        let pair_end = scalar_offset + 32;
1311
1312        let point_bytes = &calldata[point_offset..scalar_offset];
1313        let scalar_bytes = &calldata[scalar_offset..pair_end];
1314
1315        let x0 = parse_bls12_padded_fp(
1316            point_bytes[0..64]
1317                .try_into()
1318                .map_err(|_| InternalError::TypeConversion)?,
1319        )?;
1320        let x1 = parse_bls12_padded_fp(
1321            point_bytes[64..128]
1322                .try_into()
1323                .map_err(|_| InternalError::TypeConversion)?,
1324        )?;
1325        let y0 = parse_bls12_padded_fp(
1326            point_bytes[128..192]
1327                .try_into()
1328                .map_err(|_| InternalError::TypeConversion)?,
1329        )?;
1330        let y1 = parse_bls12_padded_fp(
1331            point_bytes[192..256]
1332                .try_into()
1333                .map_err(|_| InternalError::TypeConversion)?,
1334        )?;
1335        let scalar: [u8; 32] = scalar_bytes
1336            .try_into()
1337            .map_err(|_| InternalError::TypeConversion)?;
1338
1339        pairs.push(((x0, x1, y0, y1), scalar));
1340    }
1341
1342    let result = crypto
1343        .bls12_381_g2_msm(&pairs)
1344        .map_err(bls12_msm_pairing_crypto_error)?;
1345
1346    // Re-pad the 192-byte unpadded result to 256 bytes.
1347    let mut output = [0u8; 256];
1348    output[16..64].copy_from_slice(&result[0..48]);
1349    output[80..128].copy_from_slice(&result[48..96]);
1350    output[144..192].copy_from_slice(&result[96..144]);
1351    output[208..256].copy_from_slice(&result[144..192]);
1352    Ok(Bytes::copy_from_slice(&output))
1353}
1354
1355pub fn bls12_pairing_check(
1356    calldata: &Bytes,
1357    gas_remaining: &mut u64,
1358    _fork: Fork,
1359    crypto: &dyn Crypto,
1360) -> Result<Bytes, VMError> {
1361    if calldata.is_empty()
1362        || !calldata
1363            .len()
1364            .is_multiple_of(BLS12_381_PAIRING_CHECK_PAIR_LENGTH)
1365    {
1366        return Err(PrecompileError::ParsingInputError.into());
1367    }
1368
1369    // GAS
1370    let k = calldata.len() / BLS12_381_PAIRING_CHECK_PAIR_LENGTH;
1371    let gas_cost = gas_cost::bls12_pairing_check(k)?;
1372    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
1373
1374    #[allow(clippy::type_complexity)]
1375    let mut pairs: Vec<(
1376        ([u8; 48], [u8; 48]),
1377        ([u8; 48], [u8; 48], [u8; 48], [u8; 48]),
1378    )> = Vec::with_capacity(k);
1379
1380    #[expect(
1381        clippy::indexing_slicing,
1382        clippy::arithmetic_side_effects,
1383        reason = "bounds checked"
1384    )]
1385    for i in 0..k {
1386        let g1_offset = i * BLS12_381_PAIRING_CHECK_PAIR_LENGTH;
1387        let g2_offset = g1_offset + 128;
1388        let pair_end = g2_offset + 256;
1389
1390        let g1_bytes = &calldata[g1_offset..g2_offset];
1391        let g2_bytes = &calldata[g2_offset..pair_end];
1392
1393        let g1x = parse_bls12_padded_fp(
1394            g1_bytes[0..64]
1395                .try_into()
1396                .map_err(|_| InternalError::TypeConversion)?,
1397        )?;
1398        let g1y = parse_bls12_padded_fp(
1399            g1_bytes[64..128]
1400                .try_into()
1401                .map_err(|_| InternalError::TypeConversion)?,
1402        )?;
1403
1404        let g2x0 = parse_bls12_padded_fp(
1405            g2_bytes[0..64]
1406                .try_into()
1407                .map_err(|_| InternalError::TypeConversion)?,
1408        )?;
1409        let g2x1 = parse_bls12_padded_fp(
1410            g2_bytes[64..128]
1411                .try_into()
1412                .map_err(|_| InternalError::TypeConversion)?,
1413        )?;
1414        let g2y0 = parse_bls12_padded_fp(
1415            g2_bytes[128..192]
1416                .try_into()
1417                .map_err(|_| InternalError::TypeConversion)?,
1418        )?;
1419        let g2y1 = parse_bls12_padded_fp(
1420            g2_bytes[192..256]
1421                .try_into()
1422                .map_err(|_| InternalError::TypeConversion)?,
1423        )?;
1424
1425        pairs.push(((g1x, g1y), (g2x0, g2x1, g2y0, g2y1)));
1426    }
1427
1428    let result = crypto
1429        .bls12_381_pairing_check(&pairs)
1430        .map_err(bls12_msm_pairing_crypto_error)?;
1431
1432    if result {
1433        let mut out = vec![0_u8; 31];
1434        out.push(1);
1435        Ok(Bytes::from(out))
1436    } else {
1437        Ok(Bytes::copy_from_slice(&[0_u8; 32]))
1438    }
1439}
1440
1441pub fn bls12_map_fp_to_g1(
1442    calldata: &Bytes,
1443    gas_remaining: &mut u64,
1444    _fork: Fork,
1445    crypto: &dyn Crypto,
1446) -> Result<Bytes, VMError> {
1447    if calldata.len() != BLS12_381_FP_VALID_INPUT_LENGTH {
1448        return Err(PrecompileError::ParsingInputError.into());
1449    }
1450
1451    // GAS
1452    increase_precompile_consumed_gas(BLS12_381_MAP_FP_TO_G1_COST, gas_remaining)?;
1453
1454    // Parse the 64-byte padded field element into 48-byte unpadded.
1455    #[expect(clippy::indexing_slicing, reason = "bounds checked")]
1456    let fp = parse_bls12_padded_fp(
1457        calldata[0..PADDED_FIELD_ELEMENT_SIZE_IN_BYTES]
1458            .try_into()
1459            .map_err(|_| InternalError::TypeConversion)?,
1460    )?;
1461
1462    let result = crypto
1463        .bls12_381_fp_to_g1(&fp)
1464        .map_err(crypto_error_to_precompile)?;
1465
1466    // Re-pad the 96-byte unpadded G1 result to 128 bytes.
1467    let mut output = [0u8; 128];
1468    output[16..64].copy_from_slice(&result[0..48]);
1469    output[80..128].copy_from_slice(&result[48..96]);
1470    Ok(Bytes::copy_from_slice(&output))
1471}
1472
1473pub fn bls12_map_fp2_to_g2(
1474    calldata: &Bytes,
1475    gas_remaining: &mut u64,
1476    _fork: Fork,
1477    crypto: &dyn Crypto,
1478) -> Result<Bytes, VMError> {
1479    if calldata.len() != BLS12_381_FP2_VALID_INPUT_LENGTH {
1480        return Err(PrecompileError::ParsingInputError.into());
1481    }
1482
1483    // GAS
1484    increase_precompile_consumed_gas(BLS12_381_MAP_FP2_TO_G2_COST, gas_remaining)?;
1485
1486    // Parse the two 64-byte padded field elements into 48-byte unpadded.
1487    #[expect(clippy::indexing_slicing, reason = "bounds checked")]
1488    let c0 = parse_bls12_padded_fp(
1489        calldata[0..PADDED_FIELD_ELEMENT_SIZE_IN_BYTES]
1490            .try_into()
1491            .map_err(|_| InternalError::TypeConversion)?,
1492    )?;
1493    #[expect(clippy::indexing_slicing, reason = "bounds checked")]
1494    let c1 = parse_bls12_padded_fp(
1495        calldata[PADDED_FIELD_ELEMENT_SIZE_IN_BYTES..BLS12_381_FP2_VALID_INPUT_LENGTH]
1496            .try_into()
1497            .map_err(|_| InternalError::TypeConversion)?,
1498    )?;
1499
1500    if c0 == [0u8; 48] && c1 == [0u8; 48] {
1501        return Ok(Bytes::copy_from_slice(&FP2_ZERO_MAPPED_TO_G2));
1502    }
1503
1504    let result = crypto
1505        .bls12_381_fp2_to_g2((c0, c1))
1506        .map_err(crypto_error_to_precompile)?;
1507
1508    // Re-pad the 192-byte unpadded G2 result to 256 bytes.
1509    // Unpadded: x_0(48) || x_1(48) || y_0(48) || y_1(48) = 192 bytes
1510    // Padded output: x_0_padded(64) || x_1_padded(64) || y_0_padded(64) || y_1_padded(64) = 256 bytes
1511    let mut output = [0u8; 256];
1512    output[16..64].copy_from_slice(&result[0..48]);
1513    output[80..128].copy_from_slice(&result[48..96]);
1514    output[144..192].copy_from_slice(&result[96..144]);
1515    output[208..256].copy_from_slice(&result[144..192]);
1516    Ok(Bytes::copy_from_slice(&output))
1517}
1518
1519#[cfg(test)]
1520mod tests {
1521    use super::*;
1522    use ethrex_common::types::Fork;
1523
1524    /// I3 gate test: EXECUTE precompile is L1-only and only active since LStar.
1525    #[test]
1526    fn execute_precompile_gated_by_lstar_and_l1() {
1527        let execute_addr = EXECUTE.address;
1528        // Pre-LStar L1: not a precompile.
1529        assert!(!is_precompile(&execute_addr, Fork::Amsterdam, VMType::L1));
1530        // LStar on L2: not a precompile (L1-only, I3).
1531        assert!(!is_precompile(
1532            &execute_addr,
1533            Fork::LStar,
1534            VMType::L2(Default::default())
1535        ));
1536        // LStar on L1: is a precompile.
1537        assert!(is_precompile(&execute_addr, Fork::LStar, VMType::L1));
1538    }
1539}