diff --git a/constantine.nimble b/constantine.nimble index 28a47c35..b2c78db4 100644 --- a/constantine.nimble +++ b/constantine.nimble @@ -609,7 +609,8 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[ ("tests/t_ethereum_verkle_ipa_primitives.nim", false), # Signatures - ("tests/ecdsa/t_ecdsa_verify_openssl.nim", false), + # NOTE: Requires OpenSSL version >=v3.3 for to Keccak256 support + # ("tests/ecdsa/t_ecdsa_verify_openssl.nim", false), # Proof systems # ---------------------------------------------------------- diff --git a/constantine/ecdsa_secp256k1.nim b/constantine/eth_ecdsa_signatures.nim similarity index 65% rename from constantine/ecdsa_secp256k1.nim rename to constantine/eth_ecdsa_signatures.nim index bc28b188..540baff9 100644 --- a/constantine/ecdsa_secp256k1.nim +++ b/constantine/eth_ecdsa_signatures.nim @@ -9,7 +9,7 @@ import constantine/zoo_exports, constantine/signatures/ecdsa, - constantine/hashes/h_sha256, + constantine/hashes, constantine/named/algebras, constantine/math/elliptic/[ec_shortweierstrass_affine], constantine/math/[arithmetic, ec_shortweierstrass], @@ -17,7 +17,7 @@ import export NonceSampler -const prefix_ffi = "ctt_ecdsa_secp256k1_" +const prefix_ffi = "ctt_eth_ecdsa" type SecretKey* {.byref, exportc: prefix_ffi & "seckey".} = object ## A Secp256k1 secret key @@ -51,7 +51,7 @@ proc sign*(sig: var Signature, ## Sign `message` using `secretKey` and store the signature in `sig`. The nonce ## will either be randomly sampled `nsRandom` or deterministically calculated according ## to RFC6979 (`nsRfc6979`) - sig.coreSign(secretKey.raw, message, sha256, nonceSampler) + sig.coreSign(secretKey.raw, message, keccak256, nonceSampler) proc verify*( publicKey: PublicKey, @@ -59,10 +59,40 @@ proc verify*( signature: Signature ): bool {.libPrefix: prefix_ffi, genCharAPI.} = ## Verify `signature` using `publicKey` for `message`. - result = publicKey.raw.coreVerify(message, signature, sha256) + result = publicKey.raw.coreVerify(message, signature, keccak256) func derive_pubkey*(public_key: var PublicKey, secret_key: SecretKey) {.libPrefix: prefix_ffi.} = ## Derive the public key matching with a secret key ## ## The secret_key MUST be validated public_key.raw.derivePubkey(secret_key.raw) + +proc recoverPubkey*( + publicKey: var PublicKey, + message: openArray[byte], + signature: Signature, + evenY: bool +) {.libPrefix: prefix_ffi, genCharAPI.} = + ## Verify `signature` using `publicKey` for `message`. + ## + ## `evenY == true` returns the public key corresponding to the + ## even `y` coordinate of the `R` point. + publicKey.raw.recoverPubkey(signature, message, evenY, keccak256) + +proc recoverPubkeyFromDigest*( + publicKey: var PublicKey, + msgHash: Fr[Secp256k1], + signature: Signature, + evenY: bool +) {.libPrefix: prefix_ffi.} = + ## Verify `signature` using `publicKey` for the given message digest + ## given as a scalar in the field `Fr[Secp256k1]`. + ## + ## `evenY == true` returns the public key corresponding to the + ## even `y` coordinate of the `R` point. + ## + ## As this overload works directly with a message hash as a scalar, + ## it requires no hash function. Internally, it also calls the + ## `verify` implementation, which already takes a scalar and thus + ## requires no hash function there either. + publicKey.raw.recoverPubkeyImpl_vartime(signature, msgHash, evenY) diff --git a/constantine/ethereum_evm_precompiles.nim b/constantine/ethereum_evm_precompiles.nim index 852c6c32..c551a1a2 100644 --- a/constantine/ethereum_evm_precompiles.nim +++ b/constantine/ethereum_evm_precompiles.nim @@ -22,7 +22,9 @@ import ./hash_to_curve/hash_to_curve, # For KZG point precompile ./ethereum_eip4844_kzg, - ./serialization/codecs_status_codes + ./serialization/codecs_status_codes, + # ECDSA for ECRecover + ./eth_ecdsa_signatures # For KZG point precompile export EthereumKZGContext, TrustedSetupFormat, TrustedSetupStatus, trusted_setup_load, trusted_setup_delete @@ -48,6 +50,7 @@ type cttEVM_PointNotOnCurve cttEVM_PointNotInSubgroup cttEVM_VerificationFailure + cttEVM_MalformedSignature func eth_evm_sha256*(r: var openArray[byte], inputs: openArray[byte]): CttEVMStatus {.libPrefix: prefix_ffi, meter.} = ## SHA256 @@ -1276,3 +1279,78 @@ func eth_evm_kzg_point_evaluation*(ctx: ptr EthereumKZGContext, r.toOpenArray(32, 64-1).marshal(Fr[BLS12_381].getModulus(), bigEndian) result = cttEVM_Success + +import std / importutils # Alternatively make `r`, `s` visible or define setter or constructor +func eth_evm_ecrecover*(r: var openArray[byte], + input: openArray[byte]): CttEVMStatus {.libPrefix: prefix_ffi, meter.} = + ## Attempts to recover the public key, which was used to sign the given `data` + ## to obtain the given signature `sig`. + ## + ## If the signature is invalid, the result array `r` will contain the neutral + ## element of the curve. + ## + ## Inputs: + ## - `r`: Array of the recovered public key. An elliptic curve point in affine + ## coordinates (`EC_ShortW_Aff[Fp[Secp256k1], G1]`). + ## - `input`: The input data as an array of 128 bytes. The data is as follows: + ## - 32 byte: `keccak256` digest of the message that was signed + ## - 32 byte: `v`, decides if the even or odd coordinate in `R` was used + ## - 32 byte: `r` of the signature, scalar `Fr[Secp256k1]` + ## - 32 byte: `s` of the signature, scalar `Fr[Secp256k1]` + ## + ## Implementation follows Geth here: + ## https://github.com/ethereum/go-ethereum/blob/341647f1865dab437a690dc1424ba71495de2dd8/core/vm/contracts.go#L243-L272 + ## + ## and to a lesser extent the Ethereum Yellow Paper in appendix F: + ## https://ethereum.github.io/yellowpaper/paper.pdf + ## + ## Internal Geth implementation in: + ## https://github.com/ethereum/go-ethereum/blob/master/signer/core/signed_data.go#L292-L319 + if len(input) != 128: + return cttEVM_InvalidInputSize + + if len(r) != 32: + return cttEVM_InvalidOutputSize + + # 1. construct message hash as scalar in field `Fr[Secp256k1]` + var msgBI {.noinit.}: BigInt[256] + msgBI.unmarshal(input.toOpenArray(0, 32-1), bigEndian) + var msgHash {.noinit.}: Fr[Secp256k1] + msgHash.fromBig(msgBI) + + # 2. verify `v` data is valid + ## XXX: Or construct a `BigInt[256]` instead and compare? (or compare with uint64s?) + for i in 32 ..< 63: # first 31 bytes must be zero for a valid `v` + if input[i] != byte 0: + return cttEVM_MalformedSignature + let v = input[63] + if v notin [byte 0, 1, 27, 28]: + return cttEVM_MalformedSignature + # 2a. determine if even or odd `y` coordinate + let evenY = v in [byte 0, 27] # 0 / 27 indicates `y` to be even, 1 / 28 odd + + # 3. unmarshal signature data + var signature {.noinit.}: Signature + privateAccess(Signature) + var rSig {.noinit}, sSig {.noinit.}: BigInt[256] + rSig.unmarshal(input.toOpenArray(64, 96-1), bigEndian) + sSig.unmarshal(input.toOpenArray(96, 128-1), bigEndian) + signature.r = Fr[Secp256k1].fromBig(rSig) + signature.s = Fr[Secp256k1].fromBig(sSig) + + # 4. perform pubkey recovery + var pubKey {.noinit.}: PublicKey + pubKey.recoverPubkeyFromDigest(msgHash, signature, evenY) + + # 4. now calculate the Ethereum address of the public key (keccak256) + privateAccess(PublicKey) + var rawPubkey {.noinit.}: array[64, byte] # `[x, y]` coordinates of public key + rawPubkey.toOpenArray( 0, 32-1).marshal(pubKey.raw.x, bigEndian) + rawPubkey.toOpenArray(32, 64-1).marshal(pubKey.raw.y, bigEndian) + var dgst {.noinit.}: array[32, byte] # keccak256 digest + keccak256.hash(dgst, rawPubkey) + + # 5. and effectively truncate to last 20 bytes of digest + r.rawCopy(12, dgst, 12, 20) + + result = cttEVM_Success diff --git a/constantine/serialization/codecs_ecdsa_secp256k1.nim b/constantine/serialization/codecs_ecdsa_secp256k1.nim index c92f84b5..40981ee8 100644 --- a/constantine/serialization/codecs_ecdsa_secp256k1.nim +++ b/constantine/serialization/codecs_ecdsa_secp256k1.nim @@ -38,7 +38,7 @@ import constantine/math/arithmetic/finite_fields, constantine/math/elliptic/ec_shortweierstrass_affine, constantine/math/io/io_bigints, - constantine/ecdsa_secp256k1 + constantine/eth_ecdsa_signatures import std / [strutils, base64, math, importutils] diff --git a/constantine/signatures/ecdsa.nim b/constantine/signatures/ecdsa.nim index 088c8845..6f324302 100644 --- a/constantine/signatures/ecdsa.nim +++ b/constantine/signatures/ecdsa.nim @@ -176,7 +176,7 @@ proc generateNonce[Name: static Algebra]( proc signImpl[Name: static Algebra; Sig]( sig: var Sig, secretKey: Fr[Name], - message: openArray[byte], + msgHash: Fr[Name], H: type CryptoHash, nonceSampler: NonceSampler = nsRandom) = ## Sign a given `message` using the `secretKey`. @@ -185,20 +185,13 @@ proc signImpl[Name: static Algebra; Sig]( ## but passing `nonceSampler = nsRfc6979` uses RFC 6979 to compute ## a deterministic nonce (and thus deterministic signature) given ## the message and private key as base. - # 1. hash the message in big endian order - var dgst {.noinit.}: array[H.digestSize, byte] - H.hash(dgst, message) - var message_hash: Fr[Name] - # if `dgst` uses more bytes than - message_hash.fromDigest(dgst, truncateInput = true) - # Generator of the curve const G = Name.getGenerator($G1) # loop until we found a valid (non zero) signature while true: # Generate random nonce - var k = generateNonce(nonceSampler, message_hash, secretKey, H) + var k = generateNonce(nonceSampler, msgHash, secretKey, H) var R {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] # Calculate r (x-coordinate of kG) @@ -217,9 +210,9 @@ proc signImpl[Name: static Algebra; Sig]( # we'd need to truncate to N bits for N being bits in modulo `n`) var s {.noinit.}: Fr[Name] s.prod(r, secretKey) # `r * secretKey` - s += message_hash # `message_hash + r * secretKey` - k.inv() # `k := k⁻¹` - s *= k # `k⁻¹ * (message_hash + r * secretKey)` + s += msgHash # `msgHash + r * secretKey` + k.inv() # `k := k⁻¹` + s *= k # `k⁻¹ * (msgHash + r * secretKey)` # get inversion of `s` for 'lower-s normalization' var sneg = s # inversion of `s` sneg.neg() # q - s @@ -254,33 +247,33 @@ proc coreSign*[Sig, SecKey]( ## indifferentiable from a random oracle [MRH04] under a reasonable ## cryptographic assumption. ## - `message` is the message to hash - signature.signImpl(secretKey, message, H, nonceSampler) + # 1. hash the message in big endian order + var dgst {.noinit.}: array[H.digestSize, byte] + H.hash(dgst, message) + var msgHash: Fr[SecKey.Name] + # if `dgst` uses more bits than scalar in `Fr`, truncate + msgHash.fromDigest(dgst, truncateInput = true) + # 2. sign + signature.signImpl(secretKey, msgHash, H, nonceSampler) proc verifyImpl[Name: static Algebra; Sig]( publicKey: EC_ShortW_Aff[Fp[Name], G1], - signature: Sig, # tuple[r, s: Fr[Name]], - message: openArray[byte], - H: type CryptoHash, + signature: Sig, + msgHash: Fr[Name] ): bool = ## Verify a given `signature` for a `message` using the given `publicKey`. - # 1. Hash the message (same as in signing) - var dgst {.noinit.}: array[H.digestSize, byte] - H.hash(dgst, message) - var e {.noinit.}: Fr[Name] - e.fromDigest(dgst, truncateInput = true) - - # 2. Compute w = s⁻¹ + # 1. Compute w = s⁻¹ var w = signature.s w.inv() # w = s⁻¹ - # 3. Compute u₁ = ew and u₂ = rw + # 2. Compute u₁ = ew and u₂ = rw var u1 {.noinit.}: Fr[Name] u2 {.noinit.}: Fr[Name] - u1.prod(e, w) + u1.prod(msgHash, w) u2.prod(signature.r, w) - # 4. Compute u₁G + u₂Q + # 3. Compute u₁G + u₂Q var point1 {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] point2 {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] @@ -291,11 +284,11 @@ proc verifyImpl[Name: static Algebra; Sig]( var R {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] R.sum(point1, point2) - # 5. Get x coordinate (in `Fp`) and convert to `Fr` (like in signing) + # 4. Get x coordinate (in `Fp`) and convert to `Fr` (like in signing) let x = R.getAffine().x let r_computed = Fr[Name].fromBig(x.toBig()) - # 6. Verify r_computed equals provided r + # 5. Verify r_computed equals provided r result = bool(r_computed == signature.r) func coreVerify*[Pubkey, Sig]( @@ -308,4 +301,110 @@ func coreVerify*[Pubkey, Sig]( ## This assumes that the PublicKey and Signatures ## have been pre-checked for non-infinity and being in the correct subgroup ## (likely on deserialization) - result = pubKey.verifyImpl(signature, message, H) + # 1. Hash the message (same as in signing) + var dgst {.noinit.}: array[H.digestSize, byte] + H.hash(dgst, message) + var msgHash {.noinit.}: Fr[pubkey.F.Name] + msgHash.fromDigest(dgst, truncateInput = true) + # 2. verify + result = pubKey.verifyImpl(signature, msgHash) + +proc recoverPubkeyImpl_vartime*[Name: static Algebra; Sig]( + recovered: var EC_ShortW_Aff[Fp[Name], G1], + signature: Sig, + msgHash: Fr[Name], + evenY: bool) = + ## Attempts to recover an associated public key to the given `signature` and + ## hash of a message `msgHash`. + ## + ## Note that as the signature is only dependent on the `x` coordinate of the + ## curve point `R`, two public keys verify the signature. The one with even + ## and the one with odd `y` coordinate (one even & one odd due to curve prime + ## order). + ## + ## `evenY` decides whether we recover the public key associated with the even + ## `y` coordinate of `R` or the odd one. Both verify the (message, signature) + ## pair. + ## + ## If the signature is invalid, `recovered` will be set to the neutral element. + type + ECAff = EC_ShortW_Aff[Fp[Name], G1] + ECJac = EC_ShortW_Jac[Fp[Name], G1] + # 1. Set to neutral so if we don't find a valid signature, return neutral + recovered.setNeutral() + const G = Name.getGenerator($G1) + + let rInit = signature.r.toBig() # initial `r` + var x1 = Fp[Name].fromBig(signature.r.toBig()) # as coordinate in Fp + let M = Fp[Name].fromBig(Fr[Name].getModulus()) + + # Due to the conversion of the `x` coordinate in `Fp` of the point `R` in the signing process + # to a scalar in `Fr`, we potentially reduce it modulo the curve order (if `x >= r` with + # `r` the curve order). + # As we don't know if this is the case, we need to loop until we either find a valid signature, + # adding `M` each iteration or until we roll over again, in which case the signature is invalid. + # NOTE: For secp256k1 this is _extremely_ unlikely, because prime of the curve `p` and subgroup + # order `r` are so close! + var validSig = false + while (not validSig) and bool(x1.toBig() <= rInit): + # 1. Get base `R` point + var R {.noinit.}: EC_ShortW_Aff[Fp[Name], G1] + let valid = R.trySetFromCoordX(x1) # from `r = x1` + if not bool(valid): + x1 += M # add modulus of `Fr`. As long as we don't overflow in `Fp` we try again + continue # try next `i` in `x1 = r + i·M` + + let isEven = R.y.toBig().isEven() + # 2. only negate `y ↦ -y` if current and target even-ness disagree + R.y.cneg(isEven xor SecretBool evenY) + + # 3. perform recovery calculation, `Q = -m·r⁻¹ * G + s·r⁻¹ * R` + # Note: Calculate with `r⁻¹` included in each coefficient to avoid 3rd `scalarMul`. + var rInv = signature.r + rInv.inv() # `r⁻¹` + + var u1 {.noinit.}, u2 {.noinit.}: Fr[Name] + u1.prod(msgHash, rInv) # `u₁ = m·r⁻¹` + u1.neg() # `u₁ = -m·r⁻¹` + u2.prod(signature.s, rInv) # `u₂ = s·r⁻¹` + + var Q {.noinit.}: ECJac # the potential public key + var point1 {.noinit.}, point2 {.noinit.}: ECJac + point1.scalarMul(u1, G) # `p₁ = u₁ * G` + point2.scalarMul(u2, R) # `p₂ = u₂ * R` + Q.sum(point1, point2) # `Q = p₁ + p₂` + + # 4. Verify signature with this point + validSig = Q.getAffine().verifyImpl(signature, msgHash) + + # 5. If valid copy to `recovered`, else keep neutral point + recovered.ccopy(Q.getAffine(), SecretBool validSig) # Copy `Q` if valid + # 6. try next `i` in `x1 = r + i·M` + x1 += M + +proc recoverPubkey*[Pubkey; Sig]( + recovered: var Pubkey, + signature: Sig, + message: openArray[byte], + evenY: bool, + H: type CryptoHash) = + ## Attempts to recover an associated public key to the given `signature` and + ## hash of a message `msgHash`. + ## + ## Note that as the signature is only dependent on the `x` coordinate of the + ## curve point `R`, two public keys verify the signature. The one with even + ## and the one with odd `y` coordinate (one even & one odd due to curve prime + ## order). + ## + ## `evenY` decides whether we recover the public key associated with the even + ## `y` coordinate of `R` or the odd one. Both verify the (message, signature) + ## pair. + ## + ## If the signature is invalid, `recovered` will be set to the neutral element. + # 1. Hash the message (same as in signing) + var dgst {.noinit.}: array[H.digestSize, byte] + H.hash(dgst, message) + var msgHash {.noinit.}: Fr[recovered.F.Name] + msgHash.fromDigest(dgst, truncateInput = true) + # 2. recover + recovered.recoverPubkeyImpl_vartime(signature, msgHash, evenY) diff --git a/tests/ecdsa/t_ecdsa_verify_openssl.nim b/tests/ecdsa/t_ecdsa_verify_openssl.nim index dac80a50..9d3a3711 100644 --- a/tests/ecdsa/t_ecdsa_verify_openssl.nim +++ b/tests/ecdsa/t_ecdsa_verify_openssl.nim @@ -8,6 +8,8 @@ We generate test vectors following these cases: Further, generate signatures using Constantine, which we verify with OpenSSL. + +NOTE: This test requires OpenSSL version >= 3.3, for Keccak256 support. ]## import @@ -17,7 +19,7 @@ import constantine/serialization/[codecs, codecs_ecdsa, codecs_ecdsa_secp256k1], constantine/math/arithmetic/[bigints, finite_fields], constantine/platforms/abstractions, - constantine/ecdsa_secp256k1 + constantine/eth_ecdsa_signatures when not defined(windows) and not defined(macosx): # Windows (at least in GH actions CI) does not provide, among others `BN_new` @@ -66,7 +68,7 @@ func getPublicKey(secKey: SecretKey): PublicKey {.noinit.} = template toOA(x: string): untyped = toOpenArrayByte(x, 0, x.len-1) when not defined(windows) and not defined(macosx): # see above - proc signAndVerify(num: int, msg = "", nonceSampler = nsRandom) = + proc signVerifyRecover(num: int, msg = "", nonceSampler = nsRandom) = ## Generates `num` signatures and verify them against OpenSSL. ## ## If `msg` is given, use a fixed message. Otherwise will generate a message with @@ -111,6 +113,22 @@ when not defined(windows) and not defined(macosx): # see above derSig.toDER(rOslFr, sOslFr) check derSig.data == osSig + # Attempt to recover public key from signature and hash. Two possible public keys + # verify the signature. Only one of them is the public key we actually derived from + # our private key. So recover both, check they verify the signature and one of them + # is equal to our initial public key. + var recEven {.noinit.}: PublicKey + var recOdd {.noinit.}: PublicKey + recEven.recoverPubkey(toOA msg, sigCTT, evenY = true) + recOdd.recoverPubkey(toOA msg, sigCTT, evenY = false) + + # Check both verify signature + check recEven.verify(toOA msg, sigCTT) + check recOdd.verify(toOA msg, sigCTT) + + # Check one of them is equal to our initial pubkey + check pubkeys_are_equal(recEven, pubKey) or pubkeys_are_equal(recOdd, pubKey) + proc verifyPemWriter(num: int, msg = "") = ## We verify our PEM writers in a bit of a roundabout way. ## @@ -148,7 +166,6 @@ when not defined(windows) and not defined(macosx): # see above ## using deterministic nonce generation and verifies the signature comes out ## identical each time. var derSig: DerSignature[DerSigSize(Secp256k1)] - let secKey = generatePrivateKey() var sig {.noinit.}: Signature sig.sign(secKey, toOA msg, nonceSampler = nsRfc6979) @@ -171,10 +188,10 @@ when not defined(windows) and not defined(macosx): # see above suite "ECDSA over secp256k1": test "Verify OpenSSL generated signatures from a fixed message (different nonces)": - signAndVerify(100, "Hello, Constantine!") # fixed message + signVerifyRecover(100, "Hello, Constantine!") # fixed message test "Verify OpenSSL generated signatures for different messages": - signAndVerify(100) # randomly generated message + signVerifyRecover(100) # randomly generated message test "Verify deterministic nonce generation via RFC6979 yields deterministic signatures": signRfc6979("Hello, Constantine!") diff --git a/tests/openssl_wrapper.nim b/tests/openssl_wrapper.nim index a5be8268..dc5f7116 100644 --- a/tests/openssl_wrapper.nim +++ b/tests/openssl_wrapper.nim @@ -92,6 +92,9 @@ proc EVP_DigestSignInit*(ctx: EVP_MD_CTX, e: pointer, pkey: EVP_PKEY): cint + +proc EVP_MD_fetch*(ctx: OSSL_LIB_CTX, algorithm: cstring, properties: cstring): pointer + proc EVP_DigestSign*(ctx: EVP_MD_CTX, sig: ptr byte, siglen: ptr uint, @@ -138,7 +141,12 @@ proc signMessageOpenSSL*(sig: var array[72, byte], msg: openArray[byte], key: EV let ctx = EVP_MD_CTX_new() var pctx: EVP_PKEY_CTX - if EVP_DigestSignInit(ctx, addr pctx, EVP_sha256(), nil, key) <= 0: + let md = EVP_MD_fetch(nil, "KECCAK-256", nil) + if md.isNil: + raise newException(Exception, "Failed to fetch KECCAK-256") + + + if EVP_DigestSignInit(ctx, addr pctx, md, nil, key) <= 0: raise newException(Exception, "Signing init failed") # Get required signature length diff --git a/tests/t_ethereum_evm_precompiles.nim b/tests/t_ethereum_evm_precompiles.nim index 69b0c642..b0d87303 100644 --- a/tests/t_ethereum_evm_precompiles.nim +++ b/tests/t_ethereum_evm_precompiles.nim @@ -82,7 +82,9 @@ template runPrecompileTests(filename: string, funcname: untyped, outsize: int, n else: let status = funcname(r, inputbytes) if status != cttEVM_Success: - doAssert test.ExpectedError.len > 0, "[Test Failure]\n" & + # `ecRecover.json` has failing test vectors where no `ExpectedError` exists, but the + # `Expected` output is simply empty. + doAssert test.ExpectedError.len > 0 or test.Expected.len == 0, "[Test Failure]\n" & " " & test.Name & "\n" & " " & funcname.astToStr & "\n" & " " & "Nim proc returned failure, but test expected to pass.\n" & @@ -163,3 +165,5 @@ runPrecompileTests("eip-2537/map_fp2_to_G2_bls.json", eth_evm_bls12381_map_fp2_t runPrecompileTests("eip-2537/fail-map_fp2_to_G2_bls.json", eth_evm_bls12381_map_fp2_to_g2, 256) runPrecompileTests("eip-4844/pointEvaluation.json", eth_evm_kzg_point_evaluation, 64, true) + +runPrecompileTests("ecRecover.json", eth_evm_ecrecover, 32)