Skip to main content

ethrex_crypto/
provider.rs

1#[cfg(not(feature = "std"))]
2use alloc::{
3    string::{String, ToString},
4    vec,
5    vec::Vec,
6};
7
8use ethereum_types::Address;
9use sha2::Digest as _;
10
11/// Errors from crypto operations. Opaque — does not leak library-specific types.
12#[derive(Debug, thiserror::Error)]
13pub enum CryptoError {
14    #[error("invalid signature")]
15    InvalidSignature,
16    #[error("invalid recovery id")]
17    InvalidRecoveryId,
18    #[error("recovery failed")]
19    RecoveryFailed,
20    #[error("invalid point: {0}")]
21    InvalidPoint(&'static str),
22    #[error("invalid input: {0}")]
23    InvalidInput(&'static str),
24    #[error("verification failed")]
25    VerificationFailed,
26    #[error("unsupported: {0}")]
27    Unsupported(&'static str),
28    #[error("{0}")]
29    Other(String),
30}
31
32/// Error returned by the BLS12-381 trait defaults when no backend is available:
33/// the host backend (`blst`) is compiled out and no provider override is in
34/// place. zkVM guest providers override these methods, so this is never hit on
35/// the guest; on the host the `blst` feature (default-on) supplies the backend.
36#[cfg(not(feature = "blst"))]
37const BLS_UNSUPPORTED: &str =
38    "bls12_381 requires the `blst` feature (host/L1) or a zkVM provider override";
39
40/// All cryptographic operations the EVM needs.
41///
42/// Implementors provide the actual crypto — native libraries, zkVM circuits,
43/// or anything else. ethrex's EVM code depends only on this trait.
44///
45/// Default implementations use native system libraries. Implementors only
46/// override methods where they need different behavior (e.g. zkVM-accelerated
47/// ECDSA or pairing checks).
48///
49/// Methods take `&self` to support `&dyn Crypto` (dynamic dispatch).
50/// Implementations are typically zero-sized structs.
51///
52/// # zkVM implementors
53///
54/// The following methods **must** be overridden for zkVM targets, as their
55/// default implementations use native C libraries (secp256k1, ark-bn254, etc.)
56/// that cannot run inside a zkVM guest:
57///
58/// - [`secp256k1_ecrecover`](Crypto::secp256k1_ecrecover) — uses `libsecp256k1` C library
59/// - [`recover_signer`](Crypto::recover_signer) — uses `libsecp256k1` C library
60/// - [`bn254_g1_add`](Crypto::bn254_g1_add), [`bn254_g1_mul`](Crypto::bn254_g1_mul),
61///   [`bn254_pairing_check`](Crypto::bn254_pairing_check) — use `ark-bn254`
62/// - [`bls12_381_g1_add`](Crypto::bls12_381_g1_add), [`bls12_381_g2_add`](Crypto::bls12_381_g2_add),
63///   [`bls12_381_g1_msm`](Crypto::bls12_381_g1_msm), [`bls12_381_g2_msm`](Crypto::bls12_381_g2_msm),
64///   [`bls12_381_pairing_check`](Crypto::bls12_381_pairing_check) — use `bls12_381` crate
65///   [`bls12_381_map_fp_to_g1`](Crypto::bls12_381_map_fp_to_g1),
66///   [`bls12_381_map_fp2_to_g2`](Crypto::bls12_381_map_fp2_to_g2) — use `bls12_381` crate
67///
68/// Non-overridden methods will silently use the native default, which will
69/// fail to compile or panic at runtime inside a zkVM guest.
70pub trait Crypto: Send + Sync + core::fmt::Debug {
71    // ── ECDSA (secp256k1) ──────────────────────────────────────────────
72
73    /// Recover the Ethereum address from a 64-byte signature + recovery id + 32-byte message hash.
74    /// Used by the ECRECOVER precompile (0x01).
75    /// Returns the 32-byte keccak hash of the uncompressed public key (address is last 20 bytes).
76    #[cfg(feature = "secp256k1")]
77    fn secp256k1_ecrecover(
78        &self,
79        sig: &[u8; 64],
80        recid: u8,
81        msg: &[u8; 32],
82    ) -> Result<[u8; 32], CryptoError> {
83        let recovery_id = secp256k1::ecdsa::RecoveryId::try_from(recid as i32)
84            .map_err(|_| CryptoError::InvalidRecoveryId)?;
85
86        let recoverable_sig =
87            secp256k1::ecdsa::RecoverableSignature::from_compact(sig, recovery_id)
88                .map_err(|_| CryptoError::InvalidSignature)?;
89
90        let message = secp256k1::Message::from_digest(*msg);
91
92        let public_key = recoverable_sig
93            .recover(&message)
94            .map_err(|_| CryptoError::RecoveryFailed)?;
95
96        let hash = crate::keccak::keccak_hash(&public_key.serialize_uncompressed()[1..]);
97        Ok(hash)
98    }
99
100    #[cfg(not(feature = "secp256k1"))]
101    fn secp256k1_ecrecover(
102        &self,
103        sig: &[u8; 64],
104        recid: u8,
105        msg: &[u8; 32],
106    ) -> Result<[u8; 32], CryptoError> {
107        use k256::{
108            AffinePoint, ProjectivePoint, Scalar,
109            elliptic_curve::{
110                PrimeField,
111                group::prime::PrimeCurveAffine,
112                ops::{Invert, LinearCombination, Reduce},
113                point::DecompressPoint,
114                sec1::ToEncodedPoint,
115            },
116        };
117
118        // Parse r and s as scalars, rejecting values >= curve order
119        let r_bytes = k256::FieldBytes::from_slice(&sig[..32]);
120        let s_bytes = k256::FieldBytes::from_slice(&sig[32..]);
121        let r: Option<Scalar> = Scalar::from_repr(*r_bytes).into();
122        let s: Option<Scalar> = Scalar::from_repr(*s_bytes).into();
123
124        let (Some(r), Some(s)) = (r, s) else {
125            return Err(CryptoError::InvalidSignature);
126        };
127
128        if r.is_zero().into() || s.is_zero().into() {
129            return Err(CryptoError::InvalidSignature);
130        }
131
132        // Decompress R from r and recovery id parity.
133        // Note: recid >= 2 means R.x = r + n (curve order), which has ~2^-128
134        // probability on secp256k1 and never occurs in practice. We don't handle
135        // it here — decompression will simply fail and return RecoveryFailed.
136        let y_is_odd = (recid & 1) != 0;
137        let r_point: Option<AffinePoint> =
138            AffinePoint::decompress(r_bytes, u8::from(y_is_odd).into()).into();
139        let Some(r_point) = r_point else {
140            return Err(CryptoError::RecoveryFailed);
141        };
142
143        // Recover public key: pk = r^(-1) * (s*R - z*G)
144        let r_proj = ProjectivePoint::from(r_point);
145        let z = <Scalar as Reduce<k256::U256>>::reduce_bytes(k256::FieldBytes::from_slice(msg));
146        let r_inv: Option<Scalar> = r.invert_vartime().into();
147        let Some(r_inv) = r_inv else {
148            return Err(CryptoError::RecoveryFailed);
149        };
150        let u1 = -(r_inv * z);
151        let u2 = r_inv * s;
152        let pk = ProjectivePoint::lincomb(&ProjectivePoint::GENERATOR, &u1, &r_proj, &u2);
153
154        let pk_affine = pk.to_affine();
155        if bool::from(pk_affine.is_identity()) {
156            return Err(CryptoError::RecoveryFailed);
157        }
158        let uncompressed = pk_affine.to_encoded_point(false);
159        let hash = crate::keccak::keccak_hash(&uncompressed.as_bytes()[1..]);
160        Ok(hash)
161    }
162
163    /// Recover the signer's **uncompressed** secp256k1 public key (`0x04 || X || Y`).
164    ///
165    /// [`Self::recover_signer`] hashes this and keeps the last 20 bytes; the
166    /// stateless-validation wire format needs the key itself, so that a guest can
167    /// verify signatures against a supplied key instead of running `ecrecover`.
168    /// Applies the same EIP-2 low-s rejection, so the two agree on which
169    /// signatures are valid.
170    ///
171    /// Host-only: this exists to *produce* `SszStatelessInput::public_keys`. Guests
172    /// only ever consume them, so no zkVM provider needs to override it.
173    #[cfg(feature = "secp256k1")]
174    fn recover_public_key(&self, sig: &[u8; 65], msg: &[u8; 32]) -> Result<[u8; 65], CryptoError> {
175        // EIP-2: reject high-s signatures (s > secp256k1n/2), matching recover_signer.
176        const SECP256K1_N_HALF: [u8; 32] =
177            hex_literal::hex!("7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0");
178        if sig[32..64] > SECP256K1_N_HALF[..] {
179            return Err(CryptoError::InvalidSignature);
180        }
181
182        let recovery_id = secp256k1::ecdsa::RecoveryId::try_from(sig[64] as i32)
183            .map_err(|_| CryptoError::InvalidRecoveryId)?;
184        let recoverable_sig = secp256k1::ecdsa::RecoverableSignature::from_compact(
185            sig[..64]
186                .try_into()
187                .map_err(|_| CryptoError::InvalidSignature)?,
188            recovery_id,
189        )
190        .map_err(|_| CryptoError::InvalidSignature)?;
191
192        let public_key = recoverable_sig
193            .recover(&secp256k1::Message::from_digest(*msg))
194            .map_err(|_| CryptoError::RecoveryFailed)?;
195
196        Ok(public_key.serialize_uncompressed())
197    }
198
199    /// Recover the signer address from a 65-byte signature (r||s||v) + 32-byte message hash.
200    /// Used by transaction validation (tx.sender()) and EIP-7702 authority recovery.
201    fn recover_signer(&self, sig: &[u8; 65], msg: &[u8; 32]) -> Result<Address, CryptoError> {
202        // EIP-2: reject high-s signatures (s > secp256k1n/2)
203        const SECP256K1_N_HALF: [u8; 32] =
204            hex_literal::hex!("7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0");
205        if sig[32..64] > SECP256K1_N_HALF[..] {
206            return Err(CryptoError::InvalidSignature);
207        }
208
209        let hash = self.secp256k1_ecrecover(
210            sig[..64]
211                .try_into()
212                .map_err(|_| CryptoError::InvalidSignature)?,
213            sig[64],
214            msg,
215        )?;
216        Ok(Address::from_slice(&hash[12..]))
217    }
218
219    // ── Hashing ────────────────────────────────────────────────────────
220
221    /// Keccak-256 hash. Used by the KECCAK256 opcode (0x20) and address derivation.
222    fn keccak256(&self, input: &[u8]) -> [u8; 32] {
223        crate::keccak::keccak_hash(input)
224    }
225
226    /// SHA-256 hash. Used by SHA2-256 precompile (0x02) and KZG point evaluation.
227    fn sha256(&self, input: &[u8]) -> [u8; 32] {
228        sha2::Sha256::digest(input).into()
229    }
230
231    /// RIPEMD-160 hash (zero-padded to 32 bytes). Used by RIPEMD-160 precompile (0x03).
232    fn ripemd160(&self, input: &[u8]) -> [u8; 32] {
233        let mut hasher = ripemd::Ripemd160::new();
234        hasher.update(input);
235        let result = hasher.finalize();
236
237        let mut output = [0u8; 32];
238        output[12..].copy_from_slice(&result);
239        output
240    }
241
242    // ── BN254 (alt_bn128) ──────────────────────────────────────────────
243
244    /// G1 point addition. Used by ECADD precompile (0x06).
245    /// Input: two uncompressed G1 points (64 bytes each as big-endian x||y).
246    /// Output: uncompressed G1 point (64 bytes).
247    fn bn254_g1_add(&self, p1: &[u8], p2: &[u8]) -> Result<[u8; 64], CryptoError> {
248        use ark_bn254::Fq;
249        use ark_ec::CurveGroup;
250        use ark_ff::{BigInteger, PrimeField as _, Zero};
251
252        let parse_point = |bytes: &[u8]| -> Result<ark_bn254::G1Affine, CryptoError> {
253            if bytes.len() < 64 {
254                return Err(CryptoError::InvalidInput("G1 point must be 64 bytes"));
255            }
256            let x = Fq::from_be_bytes_mod_order(&bytes[..32]);
257            let y = Fq::from_be_bytes_mod_order(&bytes[32..64]);
258
259            if x.is_zero() && y.is_zero() {
260                return Ok(ark_bn254::G1Affine::identity());
261            }
262
263            let point = ark_bn254::G1Affine::new_unchecked(x, y);
264            if !point.is_on_curve() {
265                return Err(CryptoError::InvalidPoint("G1 point not on curve"));
266            }
267            Ok(point)
268        };
269
270        let pt1 = parse_point(p1)?;
271        let pt2 = parse_point(p2)?;
272
273        #[allow(clippy::arithmetic_side_effects)]
274        let sum = (pt1 + pt2).into_affine();
275
276        let mut out = [0u8; 64];
277        out[..32].copy_from_slice(&sum.x.into_bigint().to_bytes_be());
278        out[32..].copy_from_slice(&sum.y.into_bigint().to_bytes_be());
279        Ok(out)
280    }
281
282    /// G1 scalar multiplication. Used by ECMUL precompile (0x07).
283    /// Input: uncompressed G1 point (64 bytes) + scalar (32 bytes big-endian).
284    /// Output: uncompressed G1 point (64 bytes).
285    fn bn254_g1_mul(&self, point: &[u8], scalar: &[u8]) -> Result<[u8; 64], CryptoError> {
286        use ark_bn254::{Fq, Fr as FrArk};
287        use ark_ec::CurveGroup;
288        use ark_ff::{BigInteger, PrimeField as _, Zero};
289        use core::ops::Mul as _;
290
291        if point.len() < 64 || scalar.len() < 32 {
292            return Err(CryptoError::InvalidInput("invalid input length"));
293        }
294
295        let x = Fq::from_be_bytes_mod_order(&point[..32]);
296        let y = Fq::from_be_bytes_mod_order(&point[32..64]);
297
298        if x.is_zero() && y.is_zero() {
299            return Ok([0u8; 64]);
300        }
301
302        let pt = ark_bn254::G1Affine::new_unchecked(x, y);
303        if !pt.is_on_curve() {
304            return Err(CryptoError::InvalidPoint("G1 point not on curve"));
305        }
306
307        let s = FrArk::from_be_bytes_mod_order(scalar);
308        if s.is_zero() {
309            return Ok([0u8; 64]);
310        }
311
312        let result = pt.mul(s).into_affine();
313
314        let mut out = [0u8; 64];
315        out[..32].copy_from_slice(&result.x.into_bigint().to_bytes_be());
316        out[32..].copy_from_slice(&result.y.into_bigint().to_bytes_be());
317        Ok(out)
318    }
319
320    /// Pairing check. Used by ECPAIRING precompile (0x08).
321    /// Input: pairs of (G1 64 bytes, G2 128 bytes) as raw byte slices.
322    /// Returns true if the pairing equation holds.
323    fn bn254_pairing_check(&self, pairs: &[(&[u8], &[u8])]) -> Result<bool, CryptoError> {
324        use ark_bn254::{Bn254, Fq, G1Affine, G2Affine};
325        use ark_ec::pairing::Pairing;
326        use ark_ff::{One, PrimeField as _, QuadExtField, Zero};
327
328        let mut g1_points = Vec::with_capacity(pairs.len());
329        let mut g2_points = Vec::with_capacity(pairs.len());
330
331        for (g1_bytes, g2_bytes) in pairs {
332            if g1_bytes.len() < 64 {
333                return Err(CryptoError::InvalidInput("G1 must be 64 bytes"));
334            }
335            let g1x = Fq::from_be_bytes_mod_order(&g1_bytes[..32]);
336            let g1y = Fq::from_be_bytes_mod_order(&g1_bytes[32..64]);
337
338            let g1 = if g1x.is_zero() && g1y.is_zero() {
339                G1Affine::identity()
340            } else {
341                let p = G1Affine::new_unchecked(g1x, g1y);
342                if !p.is_on_curve() || !p.is_in_correct_subgroup_assuming_on_curve() {
343                    return Err(CryptoError::InvalidPoint("G1 not on BN254 curve"));
344                }
345                p
346            };
347            g1_points.push(g1);
348
349            if g2_bytes.len() < 128 {
350                return Err(CryptoError::InvalidInput("G2 must be 128 bytes"));
351            }
352
353            let g2_x_im = Fq::from_be_bytes_mod_order(&g2_bytes[..32]);
354            let g2_x_re = Fq::from_be_bytes_mod_order(&g2_bytes[32..64]);
355            let g2_y_im = Fq::from_be_bytes_mod_order(&g2_bytes[64..96]);
356            let g2_y_re = Fq::from_be_bytes_mod_order(&g2_bytes[96..128]);
357
358            let g2 =
359                if g2_x_im.is_zero() && g2_x_re.is_zero() && g2_y_im.is_zero() && g2_y_re.is_zero()
360                {
361                    G2Affine::identity()
362                } else {
363                    let p = G2Affine::new_unchecked(
364                        QuadExtField::new(g2_x_re, g2_x_im),
365                        QuadExtField::new(g2_y_re, g2_y_im),
366                    );
367                    if !p.is_on_curve() || !p.is_in_correct_subgroup_assuming_on_curve() {
368                        return Err(CryptoError::InvalidPoint("G2 not on BN254 curve"));
369                    }
370                    p
371                };
372            g2_points.push(g2);
373        }
374
375        Ok(Bn254::multi_pairing(g1_points, g2_points).0 == QuadExtField::one())
376    }
377
378    // ── Modular arithmetic ─────────────────────────────────────────────
379
380    /// Modular exponentiation (arbitrary precision).
381    /// Used by MODEXP precompile (0x05).
382    #[cfg(feature = "std")]
383    fn modexp(&self, base: &[u8], exp: &[u8], modulus: &[u8]) -> Result<Vec<u8>, CryptoError> {
384        use malachite::base::num::arithmetic::traits::ModPow as _;
385        use malachite::base::num::basic::traits::Zero as _;
386        use malachite::{Natural, base::num::conversion::traits::*};
387
388        let base_nat = Natural::from_power_of_2_digits_desc(8u64, base.iter().cloned())
389            .ok_or(CryptoError::InvalidInput("base"))?;
390        let exp_nat = Natural::from_power_of_2_digits_desc(8u64, exp.iter().cloned())
391            .ok_or(CryptoError::InvalidInput("exponent"))?;
392        let mod_nat = Natural::from_power_of_2_digits_desc(8u64, modulus.iter().cloned())
393            .ok_or(CryptoError::InvalidInput("modulus"))?;
394
395        let result = if mod_nat == Natural::ZERO {
396            Natural::ZERO
397        } else if exp_nat == Natural::ZERO {
398            Natural::from(1_u8) % &mod_nat
399        } else {
400            let base_mod = base_nat % &mod_nat;
401            base_mod.mod_pow(&exp_nat, &mod_nat)
402        };
403
404        let res_bytes: Vec<u8> = result.to_power_of_2_digits_desc(8);
405        pad_modexp_output(res_bytes, modulus.len())
406    }
407
408    #[cfg(not(feature = "std"))]
409    fn modexp(&self, base: &[u8], exp: &[u8], modulus: &[u8]) -> Result<Vec<u8>, CryptoError> {
410        use num_bigint::BigUint;
411
412        let base_nat = BigUint::from_bytes_be(base);
413        let exp_nat = BigUint::from_bytes_be(exp);
414        let mod_nat = BigUint::from_bytes_be(modulus);
415
416        let result = if mod_nat == BigUint::ZERO {
417            BigUint::ZERO
418        } else if exp_nat == BigUint::ZERO {
419            BigUint::from(1_u8) % &mod_nat
420        } else {
421            base_nat.modpow(&exp_nat, &mod_nat)
422        };
423
424        let res_bytes = result.to_bytes_be();
425        pad_modexp_output(res_bytes, modulus.len())
426    }
427
428    /// 256-bit modular multiplication.
429    /// Used by the MULMOD opcode. Default impl uses standard bigint arithmetic.
430    /// ZisK overrides with a native circuit instruction.
431    fn mulmod256(&self, a: &[u8; 32], b: &[u8; 32], m: &[u8; 32]) -> [u8; 32] {
432        let a = ethereum_types::U256::from_big_endian(a);
433        let b = ethereum_types::U256::from_big_endian(b);
434        let m = ethereum_types::U256::from_big_endian(m);
435
436        let result = if m.is_zero() {
437            ethereum_types::U256::zero()
438        } else {
439            let product = a.full_mul(b);
440            let m512 = ethereum_types::U512::from(m);
441            if product < m512 {
442                // Product fits below modulus — no division needed.
443                product.try_into().unwrap_or(ethereum_types::U256::zero())
444            } else {
445                let (_, rem) = product.div_mod(m512);
446                rem.try_into().unwrap_or(ethereum_types::U256::zero())
447            }
448        };
449
450        result.to_big_endian()
451    }
452
453    // ── Blake2 ─────────────────────────────────────────────────────────
454
455    /// Blake2b compression function F. Used by BLAKE2F precompile (0x09).
456    fn blake2_compress(&self, rounds: u32, h: &mut [u64; 8], m: [u64; 16], t: [u64; 2], f: bool) {
457        #[allow(clippy::as_conversions)]
458        crate::blake2f::blake2b_f(rounds as usize, h, &m, &t, f);
459    }
460
461    // ── secp256r1 (P-256) ──────────────────────────────────────────────
462
463    /// P-256 signature verification. Used by P256VERIFY precompile (0x0100, Osaka).
464    fn secp256r1_verify(&self, msg: &[u8; 32], sig: &[u8; 64], pk: &[u8; 64]) -> bool {
465        use p256::{
466            EncodedPoint,
467            ecdsa::{Signature as P256Signature, signature::hazmat::PrehashVerifier},
468            elliptic_curve::bigint::U256 as P256Uint,
469        };
470
471        let r = P256Uint::from_be_slice(&sig[..32]);
472        let s = P256Uint::from_be_slice(&sig[32..]);
473
474        const P256_N: P256Uint = P256Uint::from_be_hex(
475            "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551",
476        );
477
478        if r == P256Uint::ZERO || r >= P256_N || s == P256Uint::ZERO || s >= P256_N {
479            return false;
480        }
481
482        let x_bytes: &[u8; 32] = match pk[..32].try_into() {
483            Ok(b) => b,
484            Err(_) => return false,
485        };
486        let y_bytes: &[u8; 32] = match pk[32..].try_into() {
487            Ok(b) => b,
488            Err(_) => return false,
489        };
490
491        let Ok(verifier) = p256::ecdsa::VerifyingKey::from_encoded_point(
492            &EncodedPoint::from_affine_coordinates(x_bytes.into(), y_bytes.into(), false),
493        ) else {
494            return false;
495        };
496
497        let r_arr: [u8; 32] = sig[..32].try_into().unwrap_or([0u8; 32]);
498        let s_arr: [u8; 32] = sig[32..].try_into().unwrap_or([0u8; 32]);
499
500        let Ok(signature) = P256Signature::from_scalars(r_arr, s_arr) else {
501            return false;
502        };
503
504        verifier.verify_prehash(msg, &signature).is_ok()
505    }
506
507    // ── KZG ────────────────────────────────────────────────────────────
508
509    /// KZG point evaluation. Used by POINT_EVALUATION precompile (0x0a, Cancun).
510    #[cfg(feature = "c-kzg")]
511    fn verify_kzg_proof(
512        &self,
513        z: &[u8; 32],
514        y: &[u8; 32],
515        commitment: &[u8; 48],
516        proof: &[u8; 48],
517    ) -> Result<(), CryptoError> {
518        let c_kzg_settings = c_kzg::ethereum_kzg_settings(crate::kzg::KZG_PRECOMPUTE);
519        c_kzg_settings
520            .verify_kzg_proof(
521                &(*commitment).into(),
522                &(*z).into(),
523                &(*y).into(),
524                &(*proof).into(),
525            )
526            .map_err(|e| CryptoError::Other(e.to_string()))
527            .and_then(|valid| {
528                if valid {
529                    Ok(())
530                } else {
531                    Err(CryptoError::VerificationFailed)
532                }
533            })
534    }
535
536    #[cfg(not(feature = "c-kzg"))]
537    fn verify_kzg_proof(
538        &self,
539        z: &[u8; 32],
540        y: &[u8; 32],
541        commitment: &[u8; 48],
542        proof: &[u8; 48],
543    ) -> Result<(), CryptoError> {
544        crate::kzg::verify_kzg_proof(*commitment, *z, *y, *proof)
545            .map_err(|e| CryptoError::Other(e.to_string()))
546            .and_then(|valid| {
547                if valid {
548                    Ok(())
549                } else {
550                    Err(CryptoError::VerificationFailed)
551                }
552            })
553    }
554
555    /// Verify blob KZG proof. Used by blob transaction validation.
556    #[cfg(feature = "c-kzg")]
557    fn verify_blob_kzg_proof(
558        &self,
559        blob: &[u8],
560        commitment: &[u8; 48],
561        proof: &[u8; 48],
562    ) -> Result<bool, CryptoError> {
563        use crate::kzg::BYTES_PER_BLOB;
564
565        let blob_arr: [u8; BYTES_PER_BLOB] = blob
566            .try_into()
567            .map_err(|_| CryptoError::InvalidInput("blob must be 131072 bytes"))?;
568
569        let c_kzg_settings = c_kzg::ethereum_kzg_settings(crate::kzg::KZG_PRECOMPUTE);
570        c_kzg_settings
571            .verify_blob_kzg_proof(&blob_arr.into(), &(*commitment).into(), &(*proof).into())
572            .map_err(|e| CryptoError::Other(e.to_string()))
573    }
574
575    #[cfg(not(feature = "c-kzg"))]
576    fn verify_blob_kzg_proof(
577        &self,
578        blob: &[u8],
579        commitment: &[u8; 48],
580        proof: &[u8; 48],
581    ) -> Result<bool, CryptoError> {
582        use crate::kzg::BYTES_PER_BLOB;
583
584        let blob_arr: [u8; BYTES_PER_BLOB] = blob
585            .try_into()
586            .map_err(|_| CryptoError::InvalidInput("blob must be 131072 bytes"))?;
587
588        crate::kzg::verify_blob_kzg_proof(blob_arr, *commitment, *proof)
589            .map_err(|e| CryptoError::Other(e.to_string()))
590    }
591
592    // ── BLS12-381 (Prague, EIP-2537) ───────────────────────────────────
593
594    // The BLS12-381 (EIP-2537) operations default to the assembly-optimized
595    // `blst` backend on the host (the `blst` feature, default-on). When `blst`
596    // is compiled out — i.e. zkVM guest builds — these defaults return an error
597    // and the guest `Crypto` providers (Sp1/Risc0/OpenVm via the portable
598    // `bls12_381` crate, Zisk via FFI) override every one of them. The portable
599    // pure-Rust implementation lives in `ethrex-guest-program`, so the published
600    // `ethrex-crypto` crate carries no git dependency.
601
602    /// G1 addition. Returns 96-byte unpadded G1 point.
603    fn bls12_381_g1_add(
604        &self,
605        a: ([u8; 48], [u8; 48]),
606        b: ([u8; 48], [u8; 48]),
607    ) -> Result<[u8; 96], CryptoError> {
608        #[cfg(feature = "blst")]
609        {
610            crate::bls_blst::g1_add(a, b)
611        }
612        #[cfg(not(feature = "blst"))]
613        {
614            let _ = (a, b);
615            Err(CryptoError::Unsupported(BLS_UNSUPPORTED))
616        }
617    }
618
619    /// G1 multi-scalar multiplication. Returns 96-byte unpadded G1 point.
620    #[allow(clippy::type_complexity)]
621    fn bls12_381_g1_msm(
622        &self,
623        pairs: &[(([u8; 48], [u8; 48]), [u8; 32])],
624    ) -> Result<[u8; 96], CryptoError> {
625        #[cfg(feature = "blst")]
626        {
627            crate::bls_blst::g1_msm(pairs)
628        }
629        #[cfg(not(feature = "blst"))]
630        {
631            let _ = pairs;
632            Err(CryptoError::Unsupported(BLS_UNSUPPORTED))
633        }
634    }
635
636    /// G2 addition. Returns 192-byte unpadded G2 point.
637    fn bls12_381_g2_add(
638        &self,
639        a: ([u8; 48], [u8; 48], [u8; 48], [u8; 48]),
640        b: ([u8; 48], [u8; 48], [u8; 48], [u8; 48]),
641    ) -> Result<[u8; 192], CryptoError> {
642        #[cfg(feature = "blst")]
643        {
644            crate::bls_blst::g2_add(a, b)
645        }
646        #[cfg(not(feature = "blst"))]
647        {
648            let _ = (a, b);
649            Err(CryptoError::Unsupported(BLS_UNSUPPORTED))
650        }
651    }
652
653    /// G2 multi-scalar multiplication. Returns 192-byte unpadded G2 point.
654    #[allow(clippy::type_complexity)]
655    fn bls12_381_g2_msm(
656        &self,
657        pairs: &[(([u8; 48], [u8; 48], [u8; 48], [u8; 48]), [u8; 32])],
658    ) -> Result<[u8; 192], CryptoError> {
659        #[cfg(feature = "blst")]
660        {
661            crate::bls_blst::g2_msm(pairs)
662        }
663        #[cfg(not(feature = "blst"))]
664        {
665            let _ = pairs;
666            Err(CryptoError::Unsupported(BLS_UNSUPPORTED))
667        }
668    }
669
670    /// BLS12-381 pairing check.
671    #[allow(clippy::type_complexity)]
672    fn bls12_381_pairing_check(
673        &self,
674        pairs: &[(
675            ([u8; 48], [u8; 48]),
676            ([u8; 48], [u8; 48], [u8; 48], [u8; 48]),
677        )],
678    ) -> Result<bool, CryptoError> {
679        #[cfg(feature = "blst")]
680        {
681            crate::bls_blst::pairing_check(pairs)
682        }
683        #[cfg(not(feature = "blst"))]
684        {
685            let _ = pairs;
686            Err(CryptoError::Unsupported(BLS_UNSUPPORTED))
687        }
688    }
689
690    /// Map field element to G1 point.
691    fn bls12_381_fp_to_g1(&self, fp: &[u8; 48]) -> Result<[u8; 96], CryptoError> {
692        #[cfg(feature = "blst")]
693        {
694            crate::bls_blst::fp_to_g1(fp)
695        }
696        #[cfg(not(feature = "blst"))]
697        {
698            let _ = fp;
699            Err(CryptoError::Unsupported(BLS_UNSUPPORTED))
700        }
701    }
702
703    /// Map field element pair to G2 point.
704    fn bls12_381_fp2_to_g2(&self, fp2: ([u8; 48], [u8; 48])) -> Result<[u8; 192], CryptoError> {
705        #[cfg(feature = "blst")]
706        {
707            crate::bls_blst::fp2_to_g2(fp2)
708        }
709        #[cfg(not(feature = "blst"))]
710        {
711            let _ = fp2;
712            Err(CryptoError::Unsupported(BLS_UNSUPPORTED))
713        }
714    }
715}
716
717// ── Modexp helper ──────────────────────────────────────────────────────────
718
719/// Pad or truncate modexp result bytes to match the modulus length.
720fn pad_modexp_output(res_bytes: Vec<u8>, modulus_len: usize) -> Result<Vec<u8>, CryptoError> {
721    let mut out = vec![0u8; modulus_len];
722    if res_bytes.len() <= modulus_len {
723        let offset = modulus_len - res_bytes.len();
724        out[offset..].copy_from_slice(&res_bytes);
725    } else {
726        out.copy_from_slice(&res_bytes[res_bytes.len() - modulus_len..]);
727    }
728    Ok(out)
729}
730
731// The BLS12-381 point parse/serialize helpers that backed the portable trait
732// defaults now live in `ethrex-guest-program` alongside the guest `Crypto`
733// providers, so this crate no longer depends on the `bls12_381` git fork.