Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ECRecover EVM precompile #504

Merged
merged 16 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion constantine.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ----------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
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],
constantine/platforms/[abstractions, views]

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
Expand Down Expand Up @@ -51,18 +51,48 @@ 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,
message: openArray[byte],
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)
80 changes: 79 additions & 1 deletion constantine/ethereum_evm_precompiles.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion constantine/serialization/codecs_ecdsa_secp256k1.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
Loading
Loading