Skip to main content

ethrex_levm/
precompiles.rs

1use bytes::{Buf, Bytes};
2use ethrex_common::H160;
3use ethrex_common::seeded_hash::SeededHashMap;
4use ethrex_common::utils::u256_from_big_endian_const;
5use ethrex_common::{
6    Address, H256, U256, types::Fork, types::Fork::*, utils::u256_from_big_endian,
7};
8use ethrex_crypto::{Crypto, CryptoError};
9use rustc_hash::FxHashMap;
10use std::borrow::Cow;
11use std::collections::{BTreeMap, BTreeSet, hash_map::Entry};
12use std::sync::RwLock;
13
14use crate::gas_cost::{MODEXP_STATIC_COST, P256_VERIFY_COST};
15use crate::vm::VMType;
16use crate::{
17    constants::VERSIONED_HASH_VERSION_KZG,
18    errors::{ExceptionalHalt, InternalError, PrecompileError, VMError},
19    gas_cost::{
20        self, BLAKE2F_ROUND_COST, BLS12_381_G1_K_DISCOUNT, BLS12_381_G1ADD_COST,
21        BLS12_381_G2_K_DISCOUNT, BLS12_381_G2ADD_COST, BLS12_381_MAP_FP_TO_G1_COST,
22        BLS12_381_MAP_FP2_TO_G2_COST, ECADD_COST, ECMUL_COST, G1_MUL_COST, G2_MUL_COST,
23        POINT_EVALUATION_COST,
24    },
25};
26
27pub const BLAKE2F_ELEMENT_SIZE: usize = 8;
28
29pub const SIZE_PRECOMPILES_PRE_CANCUN: u64 = 9;
30pub const SIZE_PRECOMPILES_CANCUN: u64 = 10;
31pub const SIZE_PRECOMPILES_PRAGUE: u64 = 17;
32
33pub const BLS12_381_G1_MSM_PAIR_LENGTH: usize = 160;
34pub const BLS12_381_G2_MSM_PAIR_LENGTH: usize = 288;
35pub const BLS12_381_PAIRING_CHECK_PAIR_LENGTH: usize = 384;
36
37const BLS12_381_FP2_VALID_INPUT_LENGTH: usize = 128;
38const BLS12_381_FP_VALID_INPUT_LENGTH: usize = 64;
39
40pub const FIELD_ELEMENT_WITHOUT_PADDING_LENGTH: usize = 48;
41pub const PADDED_FIELD_ELEMENT_SIZE_IN_BYTES: usize = 64;
42
43pub const G1_POINT_AT_INFINITY: [u8; 128] = [0_u8; 128];
44pub const G2_POINT_AT_INFINITY: [u8; 256] = [0_u8; 256];
45
46/// Precomputed result for MAP_FP2_TO_G2 when input is (Fp::zero(), Fp::zero()).
47/// The bls12_381 crate's map_to_curve does not handle the zero Fp2 case correctly,
48/// so we return the known-correct padded result directly.
49const FP2_ZERO_MAPPED_TO_G2: [u8; 256] = [
50    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,
51    25, 132, 141, 194, 156, 226, 102, 244, 19, 208, 45, 211, 29, 155, 157, 68, 236, 12, 121, 205,
52    97, 241, 139, 7, 93, 219, 166, 215, 189, 32, 183, 255, 39, 164, 179, 36, 191, 206, 0, 0, 0, 0,
53    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,
54    235, 250, 126, 35, 65, 13, 185, 61, 227, 159, 176, 109, 112, 37, 250, 149, 233, 111, 250, 66,
55    138, 122, 39, 195, 174, 77, 212, 180, 11, 210, 81, 172, 101, 136, 146, 0, 0, 0, 0, 0, 0, 0, 0,
56    0, 0, 0, 0, 0, 0, 0, 0, 2, 96, 224, 54, 68, 209, 162, 195, 33, 37, 107, 50, 70, 186, 210, 184,
57    149, 202, 209, 56, 144, 203, 230, 248, 93, 245, 81, 6, 160, 211, 52, 96, 79, 177, 67, 199, 160,
58    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,
59    4, 198, 151, 119, 164, 63, 11, 218, 7, 103, 157, 88, 5, 230, 63, 24, 207, 78, 14, 124, 97, 18,
60    172, 127, 112, 38, 109, 25, 155, 79, 118, 174, 39, 198, 38, 154, 60, 238, 189, 174, 48, 128,
61    110, 154, 118, 170, 223, 92,
62];
63
64pub struct Precompile {
65    pub address: H160,
66    pub name: &'static str,
67    pub active_since_fork: Fork,
68}
69
70pub const ECRECOVER: Precompile = Precompile {
71    address: H160([
72        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
73        0x00, 0x00, 0x00, 0x00, 0x01,
74    ]),
75    name: "ECREC",
76    active_since_fork: Paris,
77};
78
79pub const SHA2_256: Precompile = Precompile {
80    address: H160([
81        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
82        0x00, 0x00, 0x00, 0x00, 0x02,
83    ]),
84    name: "SHA256",
85    active_since_fork: Paris,
86};
87
88pub const RIPEMD_160: Precompile = Precompile {
89    address: H160([
90        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
91        0x00, 0x00, 0x00, 0x00, 0x03,
92    ]),
93    name: "RIPEMD160",
94    active_since_fork: Paris,
95};
96
97pub const IDENTITY: Precompile = Precompile {
98    address: H160([
99        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
100        0x00, 0x00, 0x00, 0x00, 0x04,
101    ]),
102    name: "ID",
103    active_since_fork: Paris,
104};
105
106pub const MODEXP: Precompile = Precompile {
107    address: H160([
108        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
109        0x00, 0x00, 0x00, 0x00, 0x05,
110    ]),
111    name: "MODEXP",
112    active_since_fork: Paris,
113};
114
115pub const ECADD: Precompile = Precompile {
116    address: H160([
117        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
118        0x00, 0x00, 0x00, 0x00, 0x06,
119    ]),
120    name: "BN254_ADD",
121    active_since_fork: Paris,
122};
123
124pub const ECMUL: Precompile = Precompile {
125    address: H160([
126        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
127        0x00, 0x00, 0x00, 0x00, 0x07,
128    ]),
129    name: "BN254_MUL",
130    active_since_fork: Paris,
131};
132
133pub const ECPAIRING: Precompile = Precompile {
134    address: H160([
135        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
136        0x00, 0x00, 0x00, 0x00, 0x08,
137    ]),
138    name: "BN254_PAIRING",
139    active_since_fork: Paris,
140};
141
142pub const BLAKE2F: Precompile = Precompile {
143    address: H160([
144        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
145        0x00, 0x00, 0x00, 0x00, 0x09,
146    ]),
147    name: "BLAKE2F",
148    active_since_fork: Paris,
149};
150
151pub const POINT_EVALUATION: Precompile = Precompile {
152    address: H160([
153        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
154        0x00, 0x00, 0x00, 0x00, 0x0a,
155    ]),
156    name: "KZG_POINT_EVALUATION",
157    active_since_fork: Cancun,
158};
159
160pub const BLS12_G1ADD: Precompile = Precompile {
161    address: H160([
162        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
163        0x00, 0x00, 0x00, 0x00, 0x0b,
164    ]),
165    name: "BLS12_G1ADD",
166    active_since_fork: Prague,
167};
168
169pub const BLS12_G1MSM: Precompile = Precompile {
170    address: H160([
171        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
172        0x00, 0x00, 0x00, 0x00, 0x0c,
173    ]),
174    name: "BLS12_G1MSM",
175    active_since_fork: Prague,
176};
177
178pub const BLS12_G2ADD: Precompile = Precompile {
179    address: H160([
180        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
181        0x00, 0x00, 0x00, 0x00, 0x0d,
182    ]),
183    name: "BLS12_G2ADD",
184    active_since_fork: Prague,
185};
186
187pub const BLS12_G2MSM: Precompile = Precompile {
188    address: H160([
189        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
190        0x00, 0x00, 0x00, 0x00, 0x0e,
191    ]),
192    name: "BLS12_G2MSM",
193    active_since_fork: Prague,
194};
195
196pub const BLS12_PAIRING_CHECK: Precompile = Precompile {
197    address: H160([
198        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
199        0x00, 0x00, 0x00, 0x00, 0x0f,
200    ]),
201    name: "BLS12_PAIRING_CHECK",
202    active_since_fork: Prague,
203};
204
205pub const BLS12_MAP_FP_TO_G1: Precompile = Precompile {
206    address: H160([
207        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
208        0x00, 0x00, 0x00, 0x00, 0x10,
209    ]),
210    name: "BLS12_MAP_FP_TO_G1",
211    active_since_fork: Prague,
212};
213
214pub const BLS12_MAP_FP2_TO_G2: Precompile = Precompile {
215    address: H160([
216        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
217        0x00, 0x00, 0x00, 0x00, 0x11,
218    ]),
219    name: "BLS12_MAP_FP2_TO_G2",
220    active_since_fork: Prague,
221};
222
223pub const P256VERIFY: Precompile = Precompile {
224    address: H160([
225        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
226        0x00, 0x00, 0x00, 0x01, 0x00,
227    ]),
228    name: "P256VERIFY",
229    active_since_fork: Osaka,
230};
231
232/// EXECUTE precompile address (0x0101) for Native Rollups (EIP-8079).
233/// Active only on L1 at or above `Fork::LStar`. The L1-only + LStar gate is
234/// enforced in `is_precompile` (the `matches!(vm_type, VMType::L1)` check);
235/// `execute_precompile`'s dispatch only re-checks `fork >= LStar` and relies on
236/// `is_precompile` having already rejected the L2 case at the call site.
237/// NOTE: `EXECUTE` is intentionally NOT in the `PRECOMPILES` array, so its
238/// `active_since_fork` below is never consulted by `precompiles_for_fork` — the
239/// gating is entirely the hand-written checks above. The field is set for
240/// documentation only.
241pub const EXECUTE: Precompile = Precompile {
242    address: H160([
243        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
244        0x00, 0x00, 0x00, 0x01, 0x01,
245    ]),
246    name: "EXECUTE",
247    active_since_fork: LStar,
248};
249
250pub const PRECOMPILES: [Precompile; 18] = [
251    ECRECOVER,
252    SHA2_256,
253    RIPEMD_160,
254    IDENTITY,
255    MODEXP,
256    ECADD,
257    ECMUL,
258    ECPAIRING,
259    BLAKE2F,
260    POINT_EVALUATION,
261    BLS12_G1ADD,
262    BLS12_G1MSM,
263    BLS12_G2ADD,
264    BLS12_G2MSM,
265    BLS12_PAIRING_CHECK,
266    BLS12_MAP_FP_TO_G1,
267    BLS12_MAP_FP2_TO_G2,
268    P256VERIFY,
269];
270
271pub fn precompiles_for_fork(fork: Fork) -> impl Iterator<Item = Precompile> {
272    PRECOMPILES
273        .into_iter()
274        .filter(move |precompile| precompile.active_since_fork <= fork)
275}
276
277pub fn is_precompile(address: &Address, fork: Fork, vm_type: VMType) -> bool {
278    if fork >= Fork::LStar && matches!(vm_type, VMType::L1) && *address == EXECUTE.address {
279        return true;
280    }
281    (matches!(vm_type, VMType::L2(_)) && *address == P256VERIFY.address)
282        || precompiles_for_fork(fork).any(|precompile| precompile.address == *address)
283}
284
285/// Precompile-dispatch changes requested by a geth State Override Set. Simulation-only:
286/// this is `None` on every consensus path, in which case precompile dispatch is
287/// bit-identical to [`is_precompile`].
288///
289/// Two independent effects, both matching geth's `StateOverride.Apply`:
290///
291/// - `movePrecompileToAddress` makes a destination address dispatch the precompile that
292///   lives at the named source.
293/// - **Any** overridden address stops dispatching as a precompile. geth does
294///   `delete(precompiles, addr)` for every address in the override set, not only the
295///   sources of a move, so overriding `0x04`'s balance alone takes the identity precompile
296///   out of the active set and leaves an ordinary account behind.
297#[derive(Debug, Default, Clone, PartialEq, Eq)]
298pub struct PrecompileMoves {
299    /// Overridden addresses, which no longer dispatch as precompiles.
300    suppressed: BTreeSet<Address>,
301    /// Destination -> the original precompile address whose implementation runs there.
302    relocated: BTreeMap<Address, Address>,
303}
304
305impl PrecompileMoves {
306    /// Build from the `(precompile_address, destination_address)` relocations and the full
307    /// set of overridden addresses.
308    ///
309    /// A later pair wins if two moves name the same destination, which is also what
310    /// geth's `StateOverride.Apply` does: it assigns `precompiles[dest]` per move, in
311    /// address order, and only refuses a destination that is itself overridden.
312    pub fn from_parts(
313        moves: impl IntoIterator<Item = (Address, Address)>,
314        overridden: impl IntoIterator<Item = Address>,
315    ) -> Self {
316        let mut result = Self {
317            suppressed: overridden.into_iter().collect(),
318            relocated: BTreeMap::new(),
319        };
320        for (source, destination) in moves {
321            result.relocated.insert(destination, source);
322        }
323        result
324    }
325
326    pub fn is_empty(&self) -> bool {
327        self.suppressed.is_empty() && self.relocated.is_empty()
328    }
329
330    /// True if `address` is overridden and so no longer dispatches as a precompile.
331    pub fn is_suppressed(&self, address: &Address) -> bool {
332        self.suppressed.contains(address)
333    }
334
335    /// The address whose precompile implementation should run when `address` is
336    /// called, or `None` if `address` is not a relocation destination.
337    pub fn source_for(&self, address: &Address) -> Option<Address> {
338        self.relocated.get(address).copied()
339    }
340}
341
342/// [`is_precompile`], honoring any simulation-only precompile relocations.
343///
344/// With `moves == None` this is exactly [`is_precompile`] — the consensus path.
345pub fn is_precompile_with_moves(
346    address: &Address,
347    fork: Fork,
348    vm_type: VMType,
349    moves: Option<&PrecompileMoves>,
350) -> bool {
351    let Some(moves) = moves else {
352        return is_precompile(address, fork, vm_type);
353    };
354    // A destination wins over the address's own identity: `movePrecompileToAddress`
355    // may legitimately target an address that is itself a precompile. A destination
356    // cannot also be overridden — the RPC layer rejects that, as geth does — so this
357    // never has to arbitrate between a relocation and a suppression.
358    if let Some(source) = moves.source_for(address) {
359        return is_precompile(&source, fork, vm_type);
360    }
361    if moves.is_suppressed(address) {
362        return false;
363    }
364    is_precompile(address, fork, vm_type)
365}
366
367/// The address whose precompile implementation should execute for a call to
368/// `address`. Identity mapping unless `address` is a relocation destination.
369pub fn effective_precompile_address(address: Address, moves: Option<&PrecompileMoves>) -> Address {
370    moves
371        .and_then(|m| m.source_for(&address))
372        .unwrap_or(address)
373}
374
375/// Upper bound on the memory one [`PrecompileCache`] may hold. A cache lives for a
376/// single block, and a block's working set sits far below this, so in normal operation
377/// no result is ever turned away.
378pub const PRECOMPILE_CACHE_MAX_BYTES: usize = 128 * 1024 * 1024;
379
380/// Largest precompile input that is looked up in, or added to, the [`PrecompileCache`].
381/// A lookup hashes and compares the whole input, while a precompile may read only a small
382/// part of it (MODEXP with zero lengths reads 96 bytes of any input), so caching a large
383/// input would make a cheap call cost time in proportion to its size. Real calls, such as
384/// a pairing check over a few pairs or an RSA-sized MODEXP, are far smaller.
385pub const PRECOMPILE_CACHE_MAX_INPUT_BYTES: usize = 4096;
386
387#[derive(Default)]
388struct PrecompileCacheEntries {
389    /// Results by precompile, then by calldata. Two levels so a lookup can borrow the
390    /// caller's calldata as the key: a single `(Address, Bytes)` key needs an owned one.
391    /// The inner map is seeded like `TransientStorage`: contracts choose the calldata.
392    map: FxHashMap<Address, SeededHashMap<Bytes, (Bytes, u64)>>,
393    /// Sum of [`PrecompileCache::entry_size`] over `map`.
394    used_bytes: usize,
395}
396
397impl PrecompileCacheEntries {
398    fn contains(&self, address: &Address, calldata: &[u8]) -> bool {
399        self.map
400            .get(address)
401            .is_some_and(|calls| calls.contains_key(calldata))
402    }
403}
404
405/// Per-block cache for precompile results shared between warmer and executor.
406///
407/// Holds at most its budget, [`PRECOMPILE_CACHE_MAX_BYTES`] unless built with
408/// [`PrecompileCache::with_max_bytes`], as counted by [`PrecompileCache::entry_size`];
409/// once full, further results are simply not cached.
410/// Nothing is evicted: the cache is dropped with its block, so there is no recency worth
411/// tracking, and lookups stay on the shared read lock.
412pub struct PrecompileCache {
413    cache: RwLock<PrecompileCacheEntries>,
414    max_bytes: usize,
415}
416
417impl Default for PrecompileCache {
418    fn default() -> Self {
419        Self::with_max_bytes(PRECOMPILE_CACHE_MAX_BYTES)
420    }
421}
422
423impl PrecompileCache {
424    pub fn new() -> Self {
425        Self::default()
426    }
427
428    /// A cache that holds at most `max_bytes`, as counted by [`PrecompileCache::entry_size`].
429    pub fn with_max_bytes(max_bytes: usize) -> Self {
430        Self {
431            cache: RwLock::default(),
432            max_bytes,
433        }
434    }
435
436    /// Bytes one entry is charged against the budget: its calldata and output, plus
437    /// the fixed size of the key and value it is stored as. Hash-table slack and the
438    /// per-precompile tables are not counted, so the budget is approximate, not an exact
439    /// resident size.
440    pub fn entry_size(calldata_len: usize, output_len: usize) -> usize {
441        calldata_len
442            .saturating_add(output_len)
443            .saturating_add(size_of::<(Bytes, (Bytes, u64))>())
444    }
445
446    pub fn get(&self, address: &Address, calldata: &[u8]) -> Option<(Bytes, u64)> {
447        // Graceful degradation: if the lock is poisoned (a thread panicked while
448        // holding it), skip the cache rather than propagating the panic. The cache
449        // is a pure optimization — missing it only costs a recomputation.
450        let entries = self
451            .cache
452            .read()
453            .unwrap_or_else(|poisoned| poisoned.into_inner());
454        entries.map.get(address)?.get(calldata).cloned()
455    }
456
457    /// Stores copies of `calldata` and `output`, never the caller's buffers: calldata can be
458    /// a view of a larger allocation (the calling frame's memory, or a slice of a
459    /// transaction's data), and keeping a reference would hold that whole allocation while
460    /// the budget counts only the length.
461    ///
462    /// The copies are made only for a call that is not cached yet and fits the budget, and
463    /// outside the write lock. The warmer and the executor often compute the same call, and
464    /// the second insert then costs a lookup under the shared read lock and nothing more.
465    pub fn insert(&self, address: Address, calldata: &[u8], output: &[u8], gas_cost: u64) {
466        let entry_size = Self::entry_size(calldata.len(), output.len());
467        {
468            let entries = self
469                .cache
470                .read()
471                .unwrap_or_else(|poisoned| poisoned.into_inner());
472            if entries.used_bytes.saturating_add(entry_size) > self.max_bytes
473                || entries.contains(&address, calldata)
474            {
475                return;
476            }
477        }
478        let calldata = Bytes::copy_from_slice(calldata);
479        let output = Bytes::copy_from_slice(output);
480        let mut guard = self
481            .cache
482            .write()
483            .unwrap_or_else(|poisoned| poisoned.into_inner());
484        let entries = &mut *guard;
485        // Checked again: another thread may have inserted since the read lock was released.
486        let used_bytes = entries.used_bytes.saturating_add(entry_size);
487        if used_bytes > self.max_bytes {
488            return;
489        }
490        // The warmer and the executor can both compute the same call; the result is
491        // identical, so keep the first one and charge it once.
492        if let Entry::Vacant(slot) = entries.map.entry(address).or_default().entry(calldata) {
493            slot.insert((output, gas_cost));
494            entries.used_bytes = used_bytes;
495        }
496    }
497}
498
499#[expect(clippy::as_conversions, clippy::indexing_slicing)]
500pub fn execute_precompile(
501    address: Address,
502    calldata: &[u8],
503    gas_remaining: &mut u64,
504    fork: Fork,
505    cache: Option<&PrecompileCache>,
506    crypto: &dyn Crypto,
507    _stateless_validator: Option<&dyn crate::StatelessValidator>,
508) -> Result<Bytes, VMError> {
509    type PrecompileFn = fn(&[u8], &mut u64, Fork, &dyn Crypto) -> Result<Bytes, VMError>;
510
511    const PRECOMPILES: [Option<PrecompileFn>; 512] = const {
512        let mut precompiles = [const { None }; 512];
513        precompiles[ECRECOVER.address.0[19] as usize] = Some(ecrecover as PrecompileFn);
514        precompiles[IDENTITY.address.0[19] as usize] = Some(identity as PrecompileFn);
515        precompiles[SHA2_256.address.0[19] as usize] = Some(sha2_256 as PrecompileFn);
516        precompiles[RIPEMD_160.address.0[19] as usize] = Some(ripemd_160 as PrecompileFn);
517        precompiles[MODEXP.address.0[19] as usize] = Some(modexp as PrecompileFn);
518        precompiles[ECADD.address.0[19] as usize] = Some(ecadd as PrecompileFn);
519        precompiles[ECMUL.address.0[19] as usize] = Some(ecmul as PrecompileFn);
520        precompiles[ECPAIRING.address.0[19] as usize] = Some(ecpairing as PrecompileFn);
521        precompiles[BLAKE2F.address.0[19] as usize] = Some(blake2f as PrecompileFn);
522        precompiles[POINT_EVALUATION.address.0[19] as usize] =
523            Some(point_evaluation as PrecompileFn);
524        precompiles[BLS12_G1ADD.address.0[19] as usize] = Some(bls12_g1add as PrecompileFn);
525        precompiles[BLS12_G1MSM.address.0[19] as usize] = Some(bls12_g1msm as PrecompileFn);
526        precompiles[BLS12_G2ADD.address.0[19] as usize] = Some(bls12_g2add as PrecompileFn);
527        precompiles[BLS12_G2MSM.address.0[19] as usize] = Some(bls12_g2msm as PrecompileFn);
528        precompiles[BLS12_PAIRING_CHECK.address.0[19] as usize] =
529            Some(bls12_pairing_check as PrecompileFn);
530        precompiles[BLS12_MAP_FP_TO_G1.address.0[19] as usize] =
531            Some(bls12_map_fp_to_g1 as PrecompileFn);
532        precompiles[BLS12_MAP_FP2_TO_G2.address.0[19] as usize] =
533            Some(bls12_map_fp2_to_g2 as PrecompileFn);
534        precompiles
535            [u16::from_be_bytes([P256VERIFY.address.0[18], P256VERIFY.address.0[19]]) as usize] =
536            Some(p_256_verify as PrecompileFn);
537        precompiles
538    };
539
540    // EXECUTE precompile is dispatched before the const table (LStar runtime gate;
541    // L1-only enforcement is at the is_precompile call site).
542    if fork >= Fork::LStar && address == EXECUTE.address {
543        return crate::execute_precompile::execute_precompile(
544            calldata,
545            gas_remaining,
546            fork,
547            crypto,
548            _stateless_validator,
549        );
550    }
551
552    if address[0..18] != [0u8; 18] {
553        return Err(VMError::Internal(InternalError::InvalidPrecompileAddress));
554    }
555    let index = u16::from_be_bytes([address[18], address[19]]) as usize;
556
557    let precompile = PRECOMPILES
558        .get(index)
559        .copied()
560        .flatten()
561        .ok_or(VMError::Internal(InternalError::InvalidPrecompileAddress))?;
562
563    // Check cache (skip identity -- copy is cheaper than lookup -- and large inputs)
564    let cache = cache.filter(|_| {
565        address != IDENTITY.address && calldata.len() <= PRECOMPILE_CACHE_MAX_INPUT_BYTES
566    });
567    if let Some((output, gas_cost)) = cache.and_then(|c| c.get(&address, calldata)) {
568        increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
569        return Ok(output);
570    }
571
572    #[cfg(feature = "perf_opcode_timings")]
573    let precompile_time_start = std::time::Instant::now();
574
575    let gas_before = *gas_remaining;
576    let result = precompile(calldata, gas_remaining, fork, crypto);
577
578    #[cfg(feature = "perf_opcode_timings")]
579    {
580        let time = precompile_time_start.elapsed();
581        let mut timings = crate::timings::PRECOMPILES_TIMINGS.lock().expect("poison");
582        timings.update(address, time);
583    }
584
585    // Cache result on success
586    if let Some(cache) = cache
587        && let Ok(output) = &result
588    {
589        let gas_cost = gas_before.saturating_sub(*gas_remaining);
590        cache.insert(address, calldata, output, gas_cost);
591    }
592
593    result
594}
595
596/// Consumes gas and if it's higher than the gas limit returns an error.
597pub(crate) fn increase_precompile_consumed_gas(
598    gas_cost: u64,
599    gas_remaining: &mut u64,
600) -> Result<(), VMError> {
601    *gas_remaining = gas_remaining
602        .checked_sub(gas_cost)
603        .ok_or(PrecompileError::NotEnoughGas)?;
604    Ok(())
605}
606
607/// When slice length is less than `target_len`, the rest is filled with zeros. If slice length is
608/// more than `target_len`, the excess bytes are kept.
609#[inline(always)]
610pub(crate) fn fill_with_zeros(calldata: &[u8], target_len: usize) -> Cow<'_, [u8]> {
611    if calldata.len() >= target_len {
612        return Cow::Borrowed(calldata);
613    }
614    let mut padded_calldata = calldata.to_vec();
615    padded_calldata.resize(target_len, 0);
616    Cow::Owned(padded_calldata)
617}
618
619fn crypto_error_to_precompile(e: CryptoError) -> VMError {
620    match e {
621        CryptoError::InvalidPoint(_) => PrecompileError::InvalidPoint.into(),
622        CryptoError::InvalidInput(_) => PrecompileError::ParsingInputError.into(),
623        CryptoError::InvalidSignature => PrecompileError::ParsingInputError.into(),
624        CryptoError::InvalidRecoveryId => PrecompileError::ParsingInputError.into(),
625        CryptoError::RecoveryFailed => PrecompileError::ParsingInputError.into(),
626        CryptoError::VerificationFailed => PrecompileError::ParsingInputError.into(),
627        CryptoError::Other(_) => PrecompileError::ParsingInputError.into(),
628        // Misconfiguration rather than bad input: the BLS12-381 backend is
629        // unavailable (host built without `blst`, or a provider failed to
630        // override the trait default). Surface it as an internal error.
631        CryptoError::Unsupported(msg) => InternalError::Custom(msg.to_string()).into(),
632    }
633}
634
635/// Map crypto errors for BLS12-381 G1 operations (add).
636/// The old code returned BLS12381G1PointNotInCurve for invalid G1 points.
637fn bls12_g1_crypto_error(e: CryptoError) -> VMError {
638    match e {
639        CryptoError::InvalidPoint(_) => PrecompileError::BLS12381G1PointNotInCurve.into(),
640        other => crypto_error_to_precompile(other),
641    }
642}
643
644/// Map crypto errors for BLS12-381 G2 operations (add).
645/// The old code returned BLS12381G2PointNotInCurve for invalid G2 points.
646fn bls12_g2_crypto_error(e: CryptoError) -> VMError {
647    match e {
648        CryptoError::InvalidPoint(_) => PrecompileError::BLS12381G2PointNotInCurve.into(),
649        other => crypto_error_to_precompile(other),
650    }
651}
652
653/// Map crypto errors for BLS12-381 MSM and pairing operations.
654/// The old code used `G1Affine::from_uncompressed` (checked), which returned
655/// ParsingInputError for any invalid point (not on curve or not in subgroup).
656fn bls12_msm_pairing_crypto_error(e: CryptoError) -> VMError {
657    match e {
658        CryptoError::InvalidPoint(_) => PrecompileError::ParsingInputError.into(),
659        other => crypto_error_to_precompile(other),
660    }
661}
662
663/// ## ECRECOVER precompile.
664/// Elliptic curve digital signature algorithm (ECDSA) public key recovery function.
665///
666/// Input is 128 bytes (padded with zeros if shorter):
667///   [0..32)  : keccak-256 hash (message digest)
668///   [32..64) : v (27 or 28)
669///   [64..128): r||s (64 bytes)
670///
671/// Returns the recovered address.
672pub fn ecrecover(
673    calldata: &[u8],
674    gas_remaining: &mut u64,
675    _fork: Fork,
676    crypto: &dyn Crypto,
677) -> Result<Bytes, VMError> {
678    use crate::gas_cost::ECRECOVER_COST;
679
680    increase_precompile_consumed_gas(ECRECOVER_COST, gas_remaining)?;
681
682    const INPUT_LEN: usize = 128;
683    const WORD: usize = 32;
684
685    let input = fill_with_zeros(calldata, INPUT_LEN);
686
687    #[expect(
688        clippy::indexing_slicing,
689        reason = "fill_with_zeros guarantees len >= 128"
690    )]
691    let raw_hash: &[u8] = &input[0..WORD];
692    #[expect(
693        clippy::indexing_slicing,
694        reason = "fill_with_zeros guarantees len >= 128"
695    )]
696    let raw_v: &[u8] = &input[WORD..WORD * 2];
697    #[expect(
698        clippy::indexing_slicing,
699        reason = "fill_with_zeros guarantees len >= 128"
700    )]
701    let raw_sig: &[u8] = &input[WORD * 2..WORD * 2 + 64];
702
703    // EVM expects v ∈ {27, 28}. Anything else is invalid → empty return.
704    let recid_byte: u8 = match u8::try_from(u256_from_big_endian(raw_v)) {
705        Ok(27) => 0,
706        Ok(28) => 1,
707        _ => return Ok(Bytes::new()),
708    };
709
710    let msg_hash: [u8; 32] = raw_hash
711        .try_into()
712        .map_err(|_| InternalError::TypeConversion)?;
713    let sig: [u8; 64] = raw_sig
714        .try_into()
715        .map_err(|_| InternalError::TypeConversion)?;
716
717    let pk_hash = match crypto.secp256k1_ecrecover(&sig, recid_byte, &msg_hash) {
718        Ok(h) => h,
719        Err(_) => return Ok(Bytes::new()),
720    };
721
722    // Address is the last 20 bytes of the keccak hash of the public key.
723    let mut out = [0u8; 32];
724    out[12..32].copy_from_slice(&pk_hash[12..32]);
725
726    Ok(Bytes::copy_from_slice(&out))
727}
728
729/// Returns the calldata received
730pub fn identity(
731    calldata: &[u8],
732    gas_remaining: &mut u64,
733    _fork: Fork,
734    _crypto: &dyn Crypto,
735) -> Result<Bytes, VMError> {
736    let gas_cost = gas_cost::identity(calldata.len())?;
737
738    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
739
740    Ok(Bytes::copy_from_slice(calldata))
741}
742
743/// Returns the calldata hashed by sha2-256 algorithm
744pub fn sha2_256(
745    calldata: &[u8],
746    gas_remaining: &mut u64,
747    _fork: Fork,
748    crypto: &dyn Crypto,
749) -> Result<Bytes, VMError> {
750    let gas_cost = gas_cost::sha2_256(calldata.len())?;
751
752    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
753
754    let digest = crypto.sha256(calldata);
755    Ok(Bytes::copy_from_slice(&digest))
756}
757
758/// Returns the calldata hashed by ripemd-160 algorithm, padded by zeros at left
759pub fn ripemd_160(
760    calldata: &[u8],
761    gas_remaining: &mut u64,
762    _fork: Fork,
763    crypto: &dyn Crypto,
764) -> Result<Bytes, VMError> {
765    let gas_cost = gas_cost::ripemd_160(calldata.len())?;
766
767    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
768
769    let result = crypto.ripemd160(calldata);
770    Ok(Bytes::copy_from_slice(&result))
771}
772
773/// Returns the result of the module-exponentiation operation
774#[expect(clippy::indexing_slicing, reason = "bounds checked at start")]
775pub fn modexp(
776    calldata: &[u8],
777    gas_remaining: &mut u64,
778    fork: Fork,
779    crypto: &dyn Crypto,
780) -> Result<Bytes, VMError> {
781    // If calldata does not reach the required length, we should fill the rest with zeros
782    let calldata = fill_with_zeros(calldata, 96);
783
784    // Defer converting to a U256 after the zero check.
785    if fork < Fork::Osaka {
786        let base_size_bytes: [u8; 32] = calldata[0..32].try_into()?;
787        let modulus_size_bytes: [u8; 32] = calldata[64..96].try_into()?;
788        const ZERO_BYTES: [u8; 32] = [0u8; 32];
789
790        if base_size_bytes == ZERO_BYTES && modulus_size_bytes == ZERO_BYTES {
791            // On Berlin or newer there is a floor cost for the modexp precompile
792            increase_precompile_consumed_gas(MODEXP_STATIC_COST, gas_remaining)?;
793            return Ok(Bytes::new());
794        }
795    }
796
797    // The try_into are infallible and the compiler optimizes them out, even without unsafe.
798    // https://godbolt.org/z/h8rW8M3c4
799    let base_size = u256_from_big_endian_const::<32>(calldata[0..32].try_into()?);
800    let modulus_size = u256_from_big_endian_const::<32>(calldata[64..96].try_into()?);
801    let exponent_size = u256_from_big_endian_const::<32>(calldata[32..64].try_into()?);
802
803    if fork >= Fork::Osaka {
804        if base_size > U256::from(1024) {
805            return Err(PrecompileError::ModExpBaseTooLarge.into());
806        }
807        if exponent_size > U256::from(1024) {
808            return Err(PrecompileError::ModExpExpTooLarge.into());
809        }
810        if modulus_size > U256::from(1024) {
811            return Err(PrecompileError::ModExpModulusTooLarge.into());
812        }
813    }
814
815    // Because on some cases conversions to usize exploded before the check of the zero value could be done
816    let base_size = usize::try_from(base_size).map_err(|_| PrecompileError::ParsingInputError)?;
817    let exponent_size =
818        usize::try_from(exponent_size).map_err(|_| PrecompileError::ParsingInputError)?;
819    let modulus_size =
820        usize::try_from(modulus_size).map_err(|_| PrecompileError::ParsingInputError)?;
821
822    // Charge gas from the declared lengths BEFORE anything is sized by them. Pre-Osaka the
823    // lengths are uncapped, so a caller can declare operands far larger than any gas limit
824    // pays for; zero-padding them first would attempt that allocation (aborting the process
825    // when it cannot be satisfied) instead of failing the call with out-of-gas. Only the
826    // first 32 bytes of the exponent feed the cost (EIP-2565), so only those are read here.
827    // An exponent that starts past the end of the calldata (including a start offset that
828    // overflows) reads as zero, like any other missing operand bytes.
829    let exponent_start = base_size.saturating_add(96);
830    let exponent_head_size = exponent_size.min(32);
831    let exponent_head = get_slice_or_default(
832        &calldata,
833        exponent_start,
834        exponent_start.saturating_add(exponent_head_size),
835        exponent_head_size,
836    )?;
837
838    // Gas computation uses malachite Natural to compute bit length of exponent
839    use malachite::Natural;
840    use malachite::base::num::conversion::traits::*;
841    let exp_first_32 = Natural::from_power_of_2_digits_desc(8u64, exponent_head.iter().cloned())
842        .ok_or(InternalError::TypeConversion)?;
843
844    let gas_cost = gas_cost::modexp(&exp_first_32, base_size, exponent_size, modulus_size, fork)?;
845
846    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
847
848    if base_size == 0 && modulus_size == 0 {
849        return Ok(Bytes::new());
850    }
851
852    // The gas just charged bounds the declared lengths, so the operands can be materialized.
853    let base_limit = base_size.checked_add(96).ok_or(InternalError::Overflow)?;
854
855    let exponent_limit = exponent_size
856        .checked_add(base_limit)
857        .ok_or(InternalError::Overflow)?;
858
859    let modulus_limit = modulus_size
860        .checked_add(exponent_limit)
861        .ok_or(InternalError::Overflow)?;
862
863    let b = get_slice_or_default(&calldata, 96, base_limit, base_size)?;
864    let e = get_slice_or_default(&calldata, base_limit, exponent_limit, exponent_size)?;
865    let m = get_slice_or_default(&calldata, exponent_limit, modulus_limit, modulus_size)?;
866
867    let result = crypto
868        .modexp(&b, &e, &m)
869        .map_err(|_| VMError::from(PrecompileError::ParsingInputError))?;
870
871    let res_bytes = increase_left_pad(&Bytes::from(result), modulus_size);
872
873    Ok(res_bytes.slice(..modulus_size))
874}
875
876/// This function returns the slice between the lower and upper limit of the calldata (as a vector),
877/// padding with zeros at the end if necessary.
878///
879/// Uses Cow so that the best case of no resizing doesn't require an allocation. Fails with
880/// out-of-gas when the host cannot provide `size_to_expand` bytes: the gas charged for them
881/// bounds the size under any real gas limit, but a simulation run without one can pay for an
882/// operand no machine can allocate, and that must fail the call rather than abort the process.
883#[expect(clippy::indexing_slicing, reason = "bounds checked")]
884fn get_slice_or_default<'c>(
885    calldata: &'c [u8],
886    lower_limit: usize,
887    upper_limit: usize,
888    size_to_expand: usize,
889) -> Result<Cow<'c, [u8]>, VMError> {
890    let upper_limit = calldata.len().min(upper_limit);
891    if let Some(data) = calldata.get(lower_limit..upper_limit)
892        && !data.is_empty()
893    {
894        if data.len() == size_to_expand {
895            return Ok(data.into());
896        }
897        let mut extended = Vec::new();
898        extended
899            .try_reserve_exact(size_to_expand)
900            .map_err(|_| ExceptionalHalt::OutOfGas)?;
901        let copy_size = size_to_expand.min(data.len());
902        extended.extend_from_slice(&data[..copy_size]);
903        extended.resize(size_to_expand, 0);
904        return Ok(extended.into());
905    }
906    Ok(Vec::new().into())
907}
908
909/// If the result size is less than needed, pads left with zeros.
910#[inline(always)]
911pub fn increase_left_pad(result: &Bytes, m_size: usize) -> Bytes {
912    #[expect(
913        clippy::arithmetic_side_effects,
914        clippy::indexing_slicing,
915        reason = "overflow checked with the if condition, bounds checked"
916    )]
917    if result.len() < m_size {
918        let mut padded_result = vec![0u8; m_size];
919        let size_diff = m_size - result.len();
920        padded_result[size_diff..].copy_from_slice(result);
921
922        padded_result.into()
923    } else {
924        // this clone is cheap (Arc)
925        result.clone()
926    }
927}
928
929/// Makes a point addition on the elliptic curve 'alt_bn128'
930pub fn ecadd(
931    calldata: &[u8],
932    gas_remaining: &mut u64,
933    _fork: Fork,
934    crypto: &dyn Crypto,
935) -> Result<Bytes, VMError> {
936    // If calldata does not reach the required length, we should fill the rest with zeros
937    let calldata = fill_with_zeros(calldata, 128);
938
939    increase_precompile_consumed_gas(ECADD_COST, gas_remaining)?;
940
941    let (Some(first_point), Some(second_point)) =
942        (parse_bn254_g1(&calldata, 0), parse_bn254_g1(&calldata, 64))
943    else {
944        return Err(InternalError::Slicing.into());
945    };
946    validate_bn254_g1_coords(&first_point)?;
947    validate_bn254_g1_coords(&second_point)?;
948
949    #[expect(clippy::indexing_slicing, reason = "calldata padded to 128 bytes")]
950    let result = crypto
951        .bn254_g1_add(&calldata[..64], &calldata[64..128])
952        .map_err(crypto_error_to_precompile)?;
953
954    Ok(Bytes::copy_from_slice(&result))
955}
956
957/// Makes a scalar multiplication on the elliptic curve 'alt_bn128'
958pub fn ecmul(
959    calldata: &[u8],
960    gas_remaining: &mut u64,
961    _fork: Fork,
962    crypto: &dyn Crypto,
963) -> Result<Bytes, VMError> {
964    // If calldata does not reach the required length, we should fill the rest with zeros
965    let calldata = fill_with_zeros(calldata, 96);
966    increase_precompile_consumed_gas(ECMUL_COST, gas_remaining)?;
967
968    let (Some(g1), Some(_scalar)) = (
969        parse_bn254_g1(&calldata, 0),
970        parse_bn254_scalar(&calldata, 64),
971    ) else {
972        return Err(InternalError::Slicing.into());
973    };
974    validate_bn254_g1_coords(&g1)?;
975
976    #[expect(clippy::indexing_slicing, reason = "calldata padded to 96 bytes")]
977    let result = crypto
978        .bn254_g1_mul(&calldata[..64], &calldata[64..96])
979        .map_err(crypto_error_to_precompile)?;
980
981    Ok(Bytes::copy_from_slice(&result))
982}
983
984const ALT_BN128_PRIME: U256 = U256([
985    0x3c208c16d87cfd47,
986    0x97816a916871ca8d,
987    0xb85045b68181585d,
988    0x30644e72e131a029,
989]);
990
991pub struct G1(U256, U256);
992impl G1 {
993    /// According to EIP-197, the point at infinity (also called neutral element of G1 or zero) is encoded as (0, 0)
994    pub fn is_zero(&self) -> bool {
995        self.0.is_zero() && self.1.is_zero()
996    }
997}
998pub struct G2(U256, U256, U256, U256);
999impl G2 {
1000    /// According to EIP-197, the point at infinity (also called neutral element of G2 or zero) is encoded as (0, 0, 0, 0)
1001    pub fn is_zero(&self) -> bool {
1002        self.0.is_zero() && self.1.is_zero() && self.2.is_zero() && self.3.is_zero()
1003    }
1004}
1005
1006/// Parses 32 bytes as BN254 scalar
1007#[inline]
1008fn parse_bn254_scalar(buf: &[u8], offset: usize) -> Option<U256> {
1009    buf.get(offset..offset.checked_add(32)?)
1010        .map(u256_from_big_endian)
1011}
1012
1013/// Parses 64 bytes as a BN254 G1 point
1014#[inline]
1015fn parse_bn254_g1(buf: &[u8], offset: usize) -> Option<G1> {
1016    let chunk = buf.get(offset..offset.checked_add(64)?)?;
1017    let (x_bytes, y_bytes) = chunk.split_at_checked(32)?;
1018    Some(G1(
1019        u256_from_big_endian(x_bytes),
1020        u256_from_big_endian(y_bytes),
1021    ))
1022}
1023
1024/// Parses 128 bytes as a BN254 G2 point
1025fn parse_bn254_g2(buf: &[u8], offset: usize) -> Option<G2> {
1026    let chunk = buf.get(offset..offset.checked_add(128)?)?;
1027    let (g2_xy, rest) = chunk.split_at_checked(32)?;
1028    let (g2_xx, rest) = rest.split_at_checked(32)?;
1029    let (g2_yy, g2_yx) = rest.split_at_checked(32)?;
1030    Some(G2(
1031        u256_from_big_endian(g2_xx),
1032        u256_from_big_endian(g2_xy),
1033        u256_from_big_endian(g2_yx),
1034        u256_from_big_endian(g2_yy),
1035    ))
1036}
1037
1038#[inline]
1039fn validate_bn254_g1_coords(g1: &G1) -> Result<(), VMError> {
1040    // check each element is in field
1041    if g1.0 >= ALT_BN128_PRIME || g1.1 >= ALT_BN128_PRIME {
1042        return Err(PrecompileError::CoordinateExceedsFieldModulus.into());
1043    }
1044    Ok(())
1045}
1046
1047#[inline]
1048fn validate_bn254_g2_coords(g2: &G2) -> Result<(), VMError> {
1049    // check each element is in field
1050    if g2.0 >= ALT_BN128_PRIME
1051        || g2.1 >= ALT_BN128_PRIME
1052        || g2.2 >= ALT_BN128_PRIME
1053        || g2.3 >= ALT_BN128_PRIME
1054    {
1055        return Err(PrecompileError::CoordinateExceedsFieldModulus.into());
1056    }
1057    Ok(())
1058}
1059
1060/// Performs a bilinear pairing on points on the elliptic curve 'alt_bn128', returns 1 on success and 0 on failure
1061pub fn ecpairing(
1062    calldata: &[u8],
1063    gas_remaining: &mut u64,
1064    _fork: Fork,
1065    crypto: &dyn Crypto,
1066) -> Result<Bytes, VMError> {
1067    // The input must always be a multiple of 192 (6 32-byte values)
1068    if !calldata.len().is_multiple_of(192) {
1069        return Err(PrecompileError::ParsingInputError.into());
1070    }
1071
1072    let inputs_amount = calldata.len() / 192;
1073    let gas_cost = gas_cost::ecpairing(inputs_amount)?;
1074    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
1075
1076    let mut pairs: Vec<(&[u8], &[u8])> = Vec::new();
1077    for input in calldata.chunks_exact(192) {
1078        let (Some(g1), Some(g2)) = (parse_bn254_g1(input, 0), parse_bn254_g2(input, 64)) else {
1079            return Err(InternalError::Slicing.into());
1080        };
1081        validate_bn254_g1_coords(&g1)?;
1082        validate_bn254_g2_coords(&g2)?;
1083        #[expect(clippy::indexing_slicing, reason = "chunks_exact guarantees 192 bytes")]
1084        pairs.push((&input[..64], &input[64..192]));
1085    }
1086
1087    let pairing_check = if pairs.is_empty() {
1088        true
1089    } else {
1090        crypto
1091            .bn254_pairing_check(&pairs)
1092            .map_err(crypto_error_to_precompile)?
1093    };
1094
1095    let mut result = [0; 32];
1096    result[31] = u8::from(pairing_check);
1097    Ok(Bytes::from_owner(result))
1098}
1099
1100/// Returns the result of Blake2 hashing algorithm given a certain parameters from the calldata.
1101pub fn blake2f(
1102    calldata: &[u8],
1103    gas_remaining: &mut u64,
1104    _fork: Fork,
1105    crypto: &dyn Crypto,
1106) -> Result<Bytes, VMError> {
1107    if calldata.len() != 213 {
1108        return Err(PrecompileError::ParsingInputError.into());
1109    }
1110
1111    #[expect(clippy::indexing_slicing, reason = "length checked above")]
1112    let mut calldata = &calldata[..213];
1113
1114    let rounds = calldata.get_u32();
1115
1116    let gas_cost = u64::from(rounds) * BLAKE2F_ROUND_COST;
1117    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
1118
1119    let mut h = [0; 8];
1120
1121    h.copy_from_slice(&std::array::from_fn::<u64, 8, _>(|_| calldata.get_u64_le()));
1122
1123    let mut m = [0; 16];
1124
1125    m.copy_from_slice(&std::array::from_fn::<u64, 16, _>(|_| {
1126        calldata.get_u64_le()
1127    }));
1128
1129    let mut t = [0; 2];
1130    t.copy_from_slice(&std::array::from_fn::<u64, 2, _>(|_| calldata.get_u64_le()));
1131
1132    let f = calldata.get_u8();
1133    if f != 0 && f != 1 {
1134        return Err(PrecompileError::ParsingInputError.into());
1135    }
1136    let f = f == 1;
1137
1138    crypto.blake2_compress(rounds, &mut h, m, t, f);
1139
1140    Ok(Bytes::from_iter(
1141        h.into_iter().flat_map(|value| value.to_le_bytes()),
1142    ))
1143}
1144
1145/// Converts the provided commitment to match the provided versioned_hash.
1146fn kzg_commitment_to_versioned_hash(commitment_bytes: &[u8; 48], crypto: &dyn Crypto) -> H256 {
1147    let mut versioned_hash: [u8; 32] = crypto.sha256(commitment_bytes);
1148    versioned_hash[0] = VERSIONED_HASH_VERSION_KZG;
1149    versioned_hash.into()
1150}
1151
1152const POINT_EVALUATION_OUTPUT_BYTES: [u8; 64] = [
1153    // Big endian FIELD_ELEMENTS_PER_BLOB bytes
1154    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1155    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
1156    // Big endian BLS_MODULUS bytes
1157    0x73, 0xED, 0xA7, 0x53, 0x29, 0x9D, 0x7D, 0x48, 0x33, 0x39, 0xD8, 0x08, 0x09, 0xA1, 0xD8, 0x05,
1158    0x53, 0xBD, 0xA4, 0x02, 0xFF, 0xFE, 0x5B, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01,
1159];
1160
1161/// Makes verifications on the received point, proof and commitment, if true returns a constant value
1162fn point_evaluation(
1163    calldata: &[u8],
1164    gas_remaining: &mut u64,
1165    _fork: Fork,
1166    crypto: &dyn Crypto,
1167) -> Result<Bytes, VMError> {
1168    if calldata.len() != 192 {
1169        return Err(PrecompileError::ParsingInputError.into());
1170    }
1171
1172    // Consume gas
1173    let gas_cost = POINT_EVALUATION_COST;
1174    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
1175
1176    // Parse inputs
1177    let versioned_hash: [u8; 32] = calldata
1178        .get(..32)
1179        .ok_or(InternalError::Slicing)?
1180        .try_into()
1181        .map_err(|_| InternalError::TypeConversion)?;
1182
1183    let z: [u8; 32] = calldata
1184        .get(32..64)
1185        .ok_or(InternalError::Slicing)?
1186        .try_into()
1187        .map_err(|_| InternalError::TypeConversion)?;
1188
1189    let y: [u8; 32] = calldata
1190        .get(64..96)
1191        .ok_or(InternalError::Slicing)?
1192        .try_into()
1193        .map_err(|_| InternalError::TypeConversion)?;
1194
1195    let commitment: [u8; 48] = calldata
1196        .get(96..144)
1197        .ok_or(InternalError::Slicing)?
1198        .try_into()
1199        .map_err(|_| InternalError::TypeConversion)?;
1200
1201    let proof: [u8; 48] = calldata
1202        .get(144..192)
1203        .ok_or(InternalError::Slicing)?
1204        .try_into()
1205        .map_err(|_| InternalError::TypeConversion)?;
1206
1207    // Perform the evaluation
1208
1209    // This checks if the commitment is equal to the versioned hash
1210    if kzg_commitment_to_versioned_hash(&commitment, crypto) != H256::from(versioned_hash) {
1211        return Err(PrecompileError::ParsingInputError.into());
1212    }
1213
1214    // This verifies the proof from a point (z, y) and a commitment
1215    crypto
1216        .verify_kzg_proof(&z, &y, &commitment, &proof)
1217        .map_err(|_| VMError::from(PrecompileError::ParsingInputError))?;
1218
1219    // The first 32 bytes consist of the number of field elements in the blob, and the
1220    // other 32 bytes consist of the modulus used in the BLS signature scheme.
1221    let output = POINT_EVALUATION_OUTPUT_BYTES.to_vec();
1222
1223    Ok(Bytes::from(output))
1224}
1225
1226/// Signature verification in the "secp256r1" elliptic curve
1227/// If the verification succeeds, returns 1 in a 32-bit big-endian format.
1228/// If the verification fails, returns an empty `Bytes` object.
1229/// Implemented following https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7951.md
1230pub fn p_256_verify(
1231    calldata: &[u8],
1232    gas_remaining: &mut u64,
1233    _fork: Fork,
1234    crypto: &dyn Crypto,
1235) -> Result<Bytes, VMError> {
1236    increase_precompile_consumed_gas(P256_VERIFY_COST, gas_remaining)
1237        .map_err(|_| PrecompileError::NotEnoughGas)?;
1238
1239    // Validate input data length is 160 bytes
1240    if calldata.len() != 160 {
1241        return Ok(Bytes::new());
1242    }
1243
1244    // Parse parameters
1245    #[expect(
1246        clippy::indexing_slicing,
1247        reason = "length of the calldata is checked before slicing"
1248    )]
1249    let msg: &[u8; 32] = calldata[0..32].try_into()?;
1250    #[expect(clippy::indexing_slicing, reason = "length checked")]
1251    let r: &[u8; 32] = calldata[32..64].try_into()?;
1252    #[expect(clippy::indexing_slicing, reason = "length checked")]
1253    let s: &[u8; 32] = calldata[64..96].try_into()?;
1254    #[expect(clippy::indexing_slicing, reason = "length checked")]
1255    let pk_x: &[u8; 32] = calldata[96..128].try_into()?;
1256    #[expect(clippy::indexing_slicing, reason = "length checked")]
1257    let pk_y: &[u8; 32] = calldata[128..160].try_into()?;
1258
1259    // Build 64-byte sig (r||s) and 64-byte pk (x||y)
1260    let mut sig_bytes = [0u8; 64];
1261    sig_bytes[..32].copy_from_slice(r);
1262    sig_bytes[32..].copy_from_slice(s);
1263
1264    let mut pk_bytes = [0u8; 64];
1265    pk_bytes[..32].copy_from_slice(pk_x);
1266    pk_bytes[32..].copy_from_slice(pk_y);
1267
1268    let success = crypto.secp256r1_verify(msg, &sig_bytes, &pk_bytes);
1269
1270    // If the verification succeeds, returns 1 in a 32-bit big-endian format.
1271    // If the verification fails, returns an empty `Bytes` object.
1272    if success {
1273        const RESULT: [u8; 32] = [
1274            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,
1275            0, 0, 1,
1276        ];
1277        Ok(Bytes::from_static(&RESULT))
1278    } else {
1279        Ok(Bytes::new())
1280    }
1281}
1282
1283/// Parse a 64-byte padded BLS12-381 field element into a 48-byte unpadded element.
1284/// The first 16 bytes must be zero (padding). Returns error if padding is invalid.
1285fn parse_bls12_padded_fp(padded: &[u8; 64]) -> Result<[u8; 48], VMError> {
1286    if padded[..16] != [0u8; 16] {
1287        return Err(PrecompileError::ParsingInputError.into());
1288    }
1289    let fp: [u8; 48] = padded[16..64]
1290        .try_into()
1291        .map_err(|_| InternalError::TypeConversion)?;
1292    Ok(fp)
1293}
1294
1295pub fn bls12_g1add(
1296    calldata: &[u8],
1297    gas_remaining: &mut u64,
1298    _fork: Fork,
1299    crypto: &dyn Crypto,
1300) -> Result<Bytes, VMError> {
1301    let (x_data, calldata) = calldata
1302        .split_first_chunk::<128>()
1303        .ok_or(PrecompileError::ParsingInputError)?;
1304    let (y_data, calldata) = calldata
1305        .split_first_chunk::<128>()
1306        .ok_or(PrecompileError::ParsingInputError)?;
1307    if !calldata.is_empty() {
1308        return Err(PrecompileError::ParsingInputError.into());
1309    }
1310
1311    // Apply precompile gas cost.
1312    increase_precompile_consumed_gas(BLS12_381_G1ADD_COST, gas_remaining)
1313        .map_err(|_| PrecompileError::NotEnoughGas)?;
1314
1315    // Parse two 128-byte padded G1 points into 48-byte unpadded coordinates.
1316    let ax = parse_bls12_padded_fp(
1317        x_data[..64]
1318            .try_into()
1319            .map_err(|_| InternalError::TypeConversion)?,
1320    )?;
1321    let ay = parse_bls12_padded_fp(
1322        x_data[64..128]
1323            .try_into()
1324            .map_err(|_| InternalError::TypeConversion)?,
1325    )?;
1326    let bx = parse_bls12_padded_fp(
1327        y_data[..64]
1328            .try_into()
1329            .map_err(|_| InternalError::TypeConversion)?,
1330    )?;
1331    let by = parse_bls12_padded_fp(
1332        y_data[64..128]
1333            .try_into()
1334            .map_err(|_| InternalError::TypeConversion)?,
1335    )?;
1336
1337    let result = crypto
1338        .bls12_381_g1_add((ax, ay), (bx, by))
1339        .map_err(bls12_g1_crypto_error)?;
1340
1341    // Re-pad the 96-byte unpadded result (x||y each 48 bytes) to 128 bytes.
1342    let mut output = [0u8; 128];
1343    output[16..64].copy_from_slice(&result[..48]);
1344    output[80..128].copy_from_slice(&result[48..96]);
1345    Ok(Bytes::copy_from_slice(&output))
1346}
1347
1348pub fn bls12_g1msm(
1349    calldata: &[u8],
1350    gas_remaining: &mut u64,
1351    _fork: Fork,
1352    crypto: &dyn Crypto,
1353) -> Result<Bytes, VMError> {
1354    if calldata.is_empty() || !calldata.len().is_multiple_of(BLS12_381_G1_MSM_PAIR_LENGTH) {
1355        return Err(PrecompileError::ParsingInputError.into());
1356    }
1357
1358    let k = calldata.len() / BLS12_381_G1_MSM_PAIR_LENGTH;
1359    let required_gas = gas_cost::bls12_msm(k, &BLS12_381_G1_K_DISCOUNT, G1_MUL_COST)?;
1360    increase_precompile_consumed_gas(required_gas, gas_remaining)?;
1361
1362    #[allow(clippy::type_complexity)]
1363    let mut pairs: Vec<(([u8; 48], [u8; 48]), [u8; 32])> = Vec::with_capacity(k);
1364
1365    #[expect(
1366        clippy::arithmetic_side_effects,
1367        clippy::indexing_slicing,
1368        reason = "bounds checked"
1369    )]
1370    for i in 0..k {
1371        let point_offset = i * BLS12_381_G1_MSM_PAIR_LENGTH;
1372        let scalar_offset = point_offset + 128;
1373        let pair_end = scalar_offset + 32;
1374
1375        let point_bytes = &calldata[point_offset..scalar_offset];
1376        let scalar_bytes = &calldata[scalar_offset..pair_end];
1377
1378        let px = parse_bls12_padded_fp(
1379            point_bytes[..64]
1380                .try_into()
1381                .map_err(|_| InternalError::TypeConversion)?,
1382        )?;
1383        let py = parse_bls12_padded_fp(
1384            point_bytes[64..128]
1385                .try_into()
1386                .map_err(|_| InternalError::TypeConversion)?,
1387        )?;
1388        let scalar: [u8; 32] = scalar_bytes
1389            .try_into()
1390            .map_err(|_| InternalError::TypeConversion)?;
1391
1392        pairs.push(((px, py), scalar));
1393    }
1394
1395    let result = crypto
1396        .bls12_381_g1_msm(&pairs)
1397        .map_err(bls12_msm_pairing_crypto_error)?;
1398
1399    // Re-pad output: 96-byte result → 128-byte padded
1400    let mut output = [0u8; 128];
1401    output[16..64].copy_from_slice(&result[..48]);
1402    output[80..128].copy_from_slice(&result[48..96]);
1403    Ok(Bytes::copy_from_slice(&output))
1404}
1405
1406pub fn bls12_g2add(
1407    calldata: &[u8],
1408    gas_remaining: &mut u64,
1409    _fork: Fork,
1410    crypto: &dyn Crypto,
1411) -> Result<Bytes, VMError> {
1412    let (x_data, calldata) = calldata
1413        .split_first_chunk::<256>()
1414        .ok_or(PrecompileError::ParsingInputError)?;
1415    let (y_data, calldata) = calldata
1416        .split_first_chunk::<256>()
1417        .ok_or(PrecompileError::ParsingInputError)?;
1418    if !calldata.is_empty() {
1419        return Err(PrecompileError::ParsingInputError.into());
1420    }
1421
1422    // Apply precompile gas cost.
1423    increase_precompile_consumed_gas(BLS12_381_G2ADD_COST, gas_remaining)
1424        .map_err(|_| PrecompileError::NotEnoughGas)?;
1425
1426    // Parse two 256-byte padded G2 points into four 48-byte unpadded coordinates each.
1427    // G2 point layout: x_0(64) || x_1(64) || y_0(64) || y_1(64) = 256 bytes
1428    let ax0 = parse_bls12_padded_fp(
1429        x_data[0..64]
1430            .try_into()
1431            .map_err(|_| InternalError::TypeConversion)?,
1432    )?;
1433    let ax1 = parse_bls12_padded_fp(
1434        x_data[64..128]
1435            .try_into()
1436            .map_err(|_| InternalError::TypeConversion)?,
1437    )?;
1438    let ay0 = parse_bls12_padded_fp(
1439        x_data[128..192]
1440            .try_into()
1441            .map_err(|_| InternalError::TypeConversion)?,
1442    )?;
1443    let ay1 = parse_bls12_padded_fp(
1444        x_data[192..256]
1445            .try_into()
1446            .map_err(|_| InternalError::TypeConversion)?,
1447    )?;
1448
1449    let bx0 = parse_bls12_padded_fp(
1450        y_data[0..64]
1451            .try_into()
1452            .map_err(|_| InternalError::TypeConversion)?,
1453    )?;
1454    let bx1 = parse_bls12_padded_fp(
1455        y_data[64..128]
1456            .try_into()
1457            .map_err(|_| InternalError::TypeConversion)?,
1458    )?;
1459    let by0 = parse_bls12_padded_fp(
1460        y_data[128..192]
1461            .try_into()
1462            .map_err(|_| InternalError::TypeConversion)?,
1463    )?;
1464    let by1 = parse_bls12_padded_fp(
1465        y_data[192..256]
1466            .try_into()
1467            .map_err(|_| InternalError::TypeConversion)?,
1468    )?;
1469
1470    let result = crypto
1471        .bls12_381_g2_add((ax0, ax1, ay0, ay1), (bx0, bx1, by0, by1))
1472        .map_err(bls12_g2_crypto_error)?;
1473
1474    // Re-pad the 192-byte unpadded result to 256 bytes.
1475    // Unpadded: x_0(48) || x_1(48) || y_0(48) || y_1(48) = 192 bytes
1476    // Padded output: x_0_padded(64) || x_1_padded(64) || y_0_padded(64) || y_1_padded(64) = 256 bytes
1477    let mut output = [0u8; 256];
1478    output[16..64].copy_from_slice(&result[0..48]);
1479    output[80..128].copy_from_slice(&result[48..96]);
1480    output[144..192].copy_from_slice(&result[96..144]);
1481    output[208..256].copy_from_slice(&result[144..192]);
1482    Ok(Bytes::copy_from_slice(&output))
1483}
1484
1485pub fn bls12_g2msm(
1486    calldata: &[u8],
1487    gas_remaining: &mut u64,
1488    _fork: Fork,
1489    crypto: &dyn Crypto,
1490) -> Result<Bytes, VMError> {
1491    if calldata.is_empty() || !calldata.len().is_multiple_of(BLS12_381_G2_MSM_PAIR_LENGTH) {
1492        return Err(PrecompileError::ParsingInputError.into());
1493    }
1494
1495    let k = calldata.len() / BLS12_381_G2_MSM_PAIR_LENGTH;
1496    let required_gas = gas_cost::bls12_msm(k, &BLS12_381_G2_K_DISCOUNT, G2_MUL_COST)?;
1497    increase_precompile_consumed_gas(required_gas, gas_remaining)?;
1498
1499    #[allow(clippy::type_complexity)]
1500    let mut pairs: Vec<(([u8; 48], [u8; 48], [u8; 48], [u8; 48]), [u8; 32])> =
1501        Vec::with_capacity(k);
1502
1503    #[expect(
1504        clippy::indexing_slicing,
1505        clippy::arithmetic_side_effects,
1506        reason = "bounds checked"
1507    )]
1508    for i in 0..k {
1509        let point_offset = i * BLS12_381_G2_MSM_PAIR_LENGTH;
1510        let scalar_offset = point_offset + 256;
1511        let pair_end = scalar_offset + 32;
1512
1513        let point_bytes = &calldata[point_offset..scalar_offset];
1514        let scalar_bytes = &calldata[scalar_offset..pair_end];
1515
1516        let x0 = parse_bls12_padded_fp(
1517            point_bytes[0..64]
1518                .try_into()
1519                .map_err(|_| InternalError::TypeConversion)?,
1520        )?;
1521        let x1 = parse_bls12_padded_fp(
1522            point_bytes[64..128]
1523                .try_into()
1524                .map_err(|_| InternalError::TypeConversion)?,
1525        )?;
1526        let y0 = parse_bls12_padded_fp(
1527            point_bytes[128..192]
1528                .try_into()
1529                .map_err(|_| InternalError::TypeConversion)?,
1530        )?;
1531        let y1 = parse_bls12_padded_fp(
1532            point_bytes[192..256]
1533                .try_into()
1534                .map_err(|_| InternalError::TypeConversion)?,
1535        )?;
1536        let scalar: [u8; 32] = scalar_bytes
1537            .try_into()
1538            .map_err(|_| InternalError::TypeConversion)?;
1539
1540        pairs.push(((x0, x1, y0, y1), scalar));
1541    }
1542
1543    let result = crypto
1544        .bls12_381_g2_msm(&pairs)
1545        .map_err(bls12_msm_pairing_crypto_error)?;
1546
1547    // Re-pad the 192-byte unpadded result to 256 bytes.
1548    let mut output = [0u8; 256];
1549    output[16..64].copy_from_slice(&result[0..48]);
1550    output[80..128].copy_from_slice(&result[48..96]);
1551    output[144..192].copy_from_slice(&result[96..144]);
1552    output[208..256].copy_from_slice(&result[144..192]);
1553    Ok(Bytes::copy_from_slice(&output))
1554}
1555
1556pub fn bls12_pairing_check(
1557    calldata: &[u8],
1558    gas_remaining: &mut u64,
1559    _fork: Fork,
1560    crypto: &dyn Crypto,
1561) -> Result<Bytes, VMError> {
1562    if calldata.is_empty()
1563        || !calldata
1564            .len()
1565            .is_multiple_of(BLS12_381_PAIRING_CHECK_PAIR_LENGTH)
1566    {
1567        return Err(PrecompileError::ParsingInputError.into());
1568    }
1569
1570    // GAS
1571    let k = calldata.len() / BLS12_381_PAIRING_CHECK_PAIR_LENGTH;
1572    let gas_cost = gas_cost::bls12_pairing_check(k)?;
1573    increase_precompile_consumed_gas(gas_cost, gas_remaining)?;
1574
1575    #[allow(clippy::type_complexity)]
1576    let mut pairs: Vec<(
1577        ([u8; 48], [u8; 48]),
1578        ([u8; 48], [u8; 48], [u8; 48], [u8; 48]),
1579    )> = Vec::with_capacity(k);
1580
1581    #[expect(
1582        clippy::indexing_slicing,
1583        clippy::arithmetic_side_effects,
1584        reason = "bounds checked"
1585    )]
1586    for i in 0..k {
1587        let g1_offset = i * BLS12_381_PAIRING_CHECK_PAIR_LENGTH;
1588        let g2_offset = g1_offset + 128;
1589        let pair_end = g2_offset + 256;
1590
1591        let g1_bytes = &calldata[g1_offset..g2_offset];
1592        let g2_bytes = &calldata[g2_offset..pair_end];
1593
1594        let g1x = parse_bls12_padded_fp(
1595            g1_bytes[0..64]
1596                .try_into()
1597                .map_err(|_| InternalError::TypeConversion)?,
1598        )?;
1599        let g1y = parse_bls12_padded_fp(
1600            g1_bytes[64..128]
1601                .try_into()
1602                .map_err(|_| InternalError::TypeConversion)?,
1603        )?;
1604
1605        let g2x0 = parse_bls12_padded_fp(
1606            g2_bytes[0..64]
1607                .try_into()
1608                .map_err(|_| InternalError::TypeConversion)?,
1609        )?;
1610        let g2x1 = parse_bls12_padded_fp(
1611            g2_bytes[64..128]
1612                .try_into()
1613                .map_err(|_| InternalError::TypeConversion)?,
1614        )?;
1615        let g2y0 = parse_bls12_padded_fp(
1616            g2_bytes[128..192]
1617                .try_into()
1618                .map_err(|_| InternalError::TypeConversion)?,
1619        )?;
1620        let g2y1 = parse_bls12_padded_fp(
1621            g2_bytes[192..256]
1622                .try_into()
1623                .map_err(|_| InternalError::TypeConversion)?,
1624        )?;
1625
1626        pairs.push(((g1x, g1y), (g2x0, g2x1, g2y0, g2y1)));
1627    }
1628
1629    let result = crypto
1630        .bls12_381_pairing_check(&pairs)
1631        .map_err(bls12_msm_pairing_crypto_error)?;
1632
1633    if result {
1634        let mut out = vec![0_u8; 31];
1635        out.push(1);
1636        Ok(Bytes::from(out))
1637    } else {
1638        Ok(Bytes::copy_from_slice(&[0_u8; 32]))
1639    }
1640}
1641
1642pub fn bls12_map_fp_to_g1(
1643    calldata: &[u8],
1644    gas_remaining: &mut u64,
1645    _fork: Fork,
1646    crypto: &dyn Crypto,
1647) -> Result<Bytes, VMError> {
1648    if calldata.len() != BLS12_381_FP_VALID_INPUT_LENGTH {
1649        return Err(PrecompileError::ParsingInputError.into());
1650    }
1651
1652    // GAS
1653    increase_precompile_consumed_gas(BLS12_381_MAP_FP_TO_G1_COST, gas_remaining)?;
1654
1655    // Parse the 64-byte padded field element into 48-byte unpadded.
1656    #[expect(clippy::indexing_slicing, reason = "bounds checked")]
1657    let fp = parse_bls12_padded_fp(
1658        calldata[0..PADDED_FIELD_ELEMENT_SIZE_IN_BYTES]
1659            .try_into()
1660            .map_err(|_| InternalError::TypeConversion)?,
1661    )?;
1662
1663    let result = crypto
1664        .bls12_381_fp_to_g1(&fp)
1665        .map_err(crypto_error_to_precompile)?;
1666
1667    // Re-pad the 96-byte unpadded G1 result to 128 bytes.
1668    let mut output = [0u8; 128];
1669    output[16..64].copy_from_slice(&result[0..48]);
1670    output[80..128].copy_from_slice(&result[48..96]);
1671    Ok(Bytes::copy_from_slice(&output))
1672}
1673
1674pub fn bls12_map_fp2_to_g2(
1675    calldata: &[u8],
1676    gas_remaining: &mut u64,
1677    _fork: Fork,
1678    crypto: &dyn Crypto,
1679) -> Result<Bytes, VMError> {
1680    if calldata.len() != BLS12_381_FP2_VALID_INPUT_LENGTH {
1681        return Err(PrecompileError::ParsingInputError.into());
1682    }
1683
1684    // GAS
1685    increase_precompile_consumed_gas(BLS12_381_MAP_FP2_TO_G2_COST, gas_remaining)?;
1686
1687    // Parse the two 64-byte padded field elements into 48-byte unpadded.
1688    #[expect(clippy::indexing_slicing, reason = "bounds checked")]
1689    let c0 = parse_bls12_padded_fp(
1690        calldata[0..PADDED_FIELD_ELEMENT_SIZE_IN_BYTES]
1691            .try_into()
1692            .map_err(|_| InternalError::TypeConversion)?,
1693    )?;
1694    #[expect(clippy::indexing_slicing, reason = "bounds checked")]
1695    let c1 = parse_bls12_padded_fp(
1696        calldata[PADDED_FIELD_ELEMENT_SIZE_IN_BYTES..BLS12_381_FP2_VALID_INPUT_LENGTH]
1697            .try_into()
1698            .map_err(|_| InternalError::TypeConversion)?,
1699    )?;
1700
1701    if c0 == [0u8; 48] && c1 == [0u8; 48] {
1702        return Ok(Bytes::copy_from_slice(&FP2_ZERO_MAPPED_TO_G2));
1703    }
1704
1705    let result = crypto
1706        .bls12_381_fp2_to_g2((c0, c1))
1707        .map_err(crypto_error_to_precompile)?;
1708
1709    // Re-pad the 192-byte unpadded G2 result to 256 bytes.
1710    // Unpadded: x_0(48) || x_1(48) || y_0(48) || y_1(48) = 192 bytes
1711    // Padded output: x_0_padded(64) || x_1_padded(64) || y_0_padded(64) || y_1_padded(64) = 256 bytes
1712    let mut output = [0u8; 256];
1713    output[16..64].copy_from_slice(&result[0..48]);
1714    output[80..128].copy_from_slice(&result[48..96]);
1715    output[144..192].copy_from_slice(&result[96..144]);
1716    output[208..256].copy_from_slice(&result[144..192]);
1717    Ok(Bytes::copy_from_slice(&output))
1718}
1719
1720#[cfg(test)]
1721#[allow(clippy::unwrap_used, clippy::arithmetic_side_effects)]
1722mod tests {
1723    use super::*;
1724    use crate::errors::ExceptionalHalt;
1725    use ethrex_common::types::Fork;
1726    use ethrex_crypto::NativeCrypto;
1727
1728    /// MODEXP calldata: the 96-byte header (base, exponent and modulus lengths as 32-byte
1729    /// big-endian words) followed by `operands`, which may be shorter than the declared
1730    /// lengths (the precompile zero-pads missing bytes).
1731    fn modexp_calldata(base_len: u64, exp_len: u64, mod_len: u64, operands: &[u8]) -> Bytes {
1732        let mut calldata = Vec::with_capacity(96 + operands.len());
1733        for len in [base_len, exp_len, mod_len] {
1734            calldata.extend_from_slice(&[0u8; 24]);
1735            calldata.extend_from_slice(&len.to_be_bytes());
1736        }
1737        calldata.extend_from_slice(operands);
1738        Bytes::from(calldata)
1739    }
1740
1741    /// Pre-Osaka MODEXP has no cap on the declared operand lengths (EIP-7823 only applies
1742    /// from Osaka), so a caller can declare a length the gas limit can never pay for. The
1743    /// call must fail with out-of-gas before any operand is sized by that length: sizing
1744    /// first would try to allocate the declared length, and an allocation of 2^62 bytes
1745    /// aborts the process rather than returning an error.
1746    #[test]
1747    fn modexp_pre_osaka_huge_declared_length_halts_with_out_of_gas() {
1748        // Both fit in a 64-bit usize, so they survive the length conversion and reach the
1749        // padding step. 2^62 overflows the gas formula; 2^33 does not, but its cost is far
1750        // above any gas limit. Neither must ever be allocated.
1751        for huge in [1u64 << 62, 1u64 << 33] {
1752            // One operand declared huge at a time, the other two one byte each. The three
1753            // operand bytes make every operand region non-empty, which is the branch of the
1754            // padding helper that allocates.
1755            for (base_len, exp_len, mod_len) in [(huge, 1, 1), (1, huge, 1), (1, 1, huge)] {
1756                let calldata = modexp_calldata(base_len, exp_len, mod_len, &[1, 1, 1]);
1757                let mut gas_remaining = 30_000_000;
1758                let result = modexp(&calldata, &mut gas_remaining, Fork::Prague, &NativeCrypto);
1759                assert!(
1760                    matches!(
1761                        result,
1762                        Err(VMError::ExceptionalHalt(
1763                            ExceptionalHalt::OutOfGas
1764                                | ExceptionalHalt::Precompile(PrecompileError::NotEnoughGas)
1765                        ))
1766                    ),
1767                    "lengths ({base_len}, {exp_len}, {mod_len}) must halt with out-of-gas, got {result:?}"
1768                );
1769            }
1770        }
1771    }
1772
1773    /// EIP-198 worked example under EIP-2565 pricing: 3^(p-1) mod p for the secp256k1 field
1774    /// prime p is 1 (Fermat), for 1360 gas. Pins the ordinary path: gas and output.
1775    #[test]
1776    fn modexp_eip198_example_gas_and_output() {
1777        // p - 1 and p, 32 bytes each.
1778        let mut exponent = [0xffu8; 32];
1779        exponent[27..].copy_from_slice(&[0xfe, 0xff, 0xff, 0xfc, 0x2e]);
1780        let mut modulus = [0xffu8; 32];
1781        modulus[27..].copy_from_slice(&[0xfe, 0xff, 0xff, 0xfc, 0x2f]);
1782        let mut operands = vec![0x03];
1783        operands.extend_from_slice(&exponent);
1784        operands.extend_from_slice(&modulus);
1785        let calldata = modexp_calldata(1, 32, 32, &operands);
1786
1787        let mut gas_remaining = 100_000;
1788        let output = modexp(&calldata, &mut gas_remaining, Fork::Prague, &NativeCrypto).unwrap();
1789
1790        let mut expected = [0u8; 32];
1791        expected[31] = 1;
1792        assert_eq!(output.as_ref(), expected);
1793        assert_eq!(100_000 - gas_remaining, 1360);
1794    }
1795
1796    /// The exponent head that prices the call is read straight from the calldata, so a
1797    /// calldata that ends inside the exponent must still price it zero-padded on the right
1798    /// (0x80 followed by 31 missing bytes is 2^255, 256 significant bits) and a missing
1799    /// modulus must still read as zero, which yields a zero output of the declared length.
1800    #[test]
1801    fn modexp_truncated_calldata_prices_zero_padded_exponent_head() {
1802        let calldata = modexp_calldata(1, 32, 32, &[0x03, 0x80]);
1803
1804        let mut gas_remaining = 100_000;
1805        let output = modexp(&calldata, &mut gas_remaining, Fork::Prague, &NativeCrypto).unwrap();
1806
1807        // words(32) = 4, multiplication complexity 16, iteration count 255: 16 * 255 / 3.
1808        assert_eq!(100_000 - gas_remaining, 1360);
1809        assert_eq!(output.as_ref(), [0u8; 32]);
1810    }
1811
1812    /// A simulation run without a gas cap can pay for an operand no host can allocate: with
1813    /// the largest 64-bit gas limit, a 2^61-byte exponent (one-byte base and modulus) costs
1814    /// about 6.1e18, so the charge succeeds and the padding step is reached. Reserving 2^61
1815    /// bytes fails on every 64-bit machine (it exceeds the address space), and the call must
1816    /// fail with out-of-gas there instead of aborting the process.
1817    #[test]
1818    fn modexp_unsatisfiable_operand_allocation_halts_with_out_of_gas() {
1819        let calldata = modexp_calldata(1, 1 << 61, 1, &[1, 1, 1]);
1820        let mut gas_remaining = u64::MAX;
1821        let result = modexp(&calldata, &mut gas_remaining, Fork::Prague, &NativeCrypto);
1822        assert!(
1823            matches!(
1824                result,
1825                Err(VMError::ExceptionalHalt(ExceptionalHalt::OutOfGas))
1826            ),
1827            "an operand the host cannot allocate must halt with out-of-gas, got {result:?}"
1828        );
1829    }
1830
1831    /// I3 gate test: EXECUTE precompile is L1-only and only active since LStar.
1832    #[test]
1833    fn execute_precompile_gated_by_lstar_and_l1() {
1834        let execute_addr = EXECUTE.address;
1835        // Pre-LStar L1: not a precompile.
1836        assert!(!is_precompile(&execute_addr, Fork::Amsterdam, VMType::L1));
1837        // LStar on L2: not a precompile (L1-only, I3).
1838        assert!(!is_precompile(
1839            &execute_addr,
1840            Fork::LStar,
1841            VMType::L2(Default::default())
1842        ));
1843        // LStar on L1: is a precompile.
1844        assert!(is_precompile(&execute_addr, Fork::LStar, VMType::L1));
1845    }
1846}
1847
1848#[cfg(test)]
1849mod seeded_calldata_tests {
1850    use super::*;
1851    use std::hash::BuildHasher;
1852
1853    /// Fails if `state` hashes a key like a fresh instance of its type does, as every
1854    /// instance of an unseeded hasher does.
1855    #[track_caller]
1856    fn assert_seeded<S: BuildHasher + Default>(state: &S) {
1857        let calldata = Bytes::from_static(&[0x5a; 32]);
1858        assert_ne!(state.hash_one(&calldata), S::default().hash_one(&calldata));
1859    }
1860
1861    /// Calldata is chosen by the calling contract, so the per-precompile result map must
1862    /// not let crafted inputs share one bucket.
1863    #[test]
1864    fn precompile_results_are_seeded() {
1865        let mut entries = PrecompileCacheEntries::default();
1866        assert_seeded(entries.map.entry(Address::zero()).or_default().hasher());
1867    }
1868}