Skip to content

Commit 9dd1205

Browse files
Vindaarmratsim
andauthored
Add ECRecover EVM precompile (#504)
* [ecdsa] pull message hashing out of `impl` procs Done so that future public key recovery can simply call `verifyImpl` to verify public key is found (signImpl changed to match). * [ecdsa] implement public key recovery * [tests] add test case to recover public key from sig&msgHash * [ecdsa] allow customizing the hash function to be used ECDSA over secp256k1 commonly uses both SHA256 (e.g. Bitcoin) and Keccak256 (e.g. Ethereum). Other combinations may also exist. We default to SHA256 for the time being. * [ecdsa] add `recoverPubkey` which directly takes a hash digest as scalar ECRecover provides the message hash and not the message. We need an API to pass that directly to the internal ECDSA procedure. We export the impl `vartime` routine for that purpose. We could alternatively also import that file using `{.all.}`. * [precompiles] add ECRecover Ethereum precompile We extend the CttEVMStatus enum by two further elements. One for an invalid signature in ECRecover and another for an invalid `v` value. * [tests] add test case for ECRecover * Update constantine/signatures/ecdsa.nim Co-authored-by: Mamy Ratsimbazafy <[email protected]> * Update constantine/signatures/ecdsa.nim Co-authored-by: Mamy Ratsimbazafy <[email protected]> * Update constantine/signatures/ecdsa.nim Co-authored-by: Mamy Ratsimbazafy <[email protected]> * [precompiles] remove invalid V enum field, invalid -> malformed sig * [ecdsa] rename ECDSA over secp256k1 file to eth specific * [ecdsa] remove hash from Eth ECDSA file, specific to Eth now * [tests] update the OpenSSL wrapper signing function to use Keccak256 * [ecdsa] name `recoverPubkey` -> `recoverPubkeyFromDigest` for variant Given that we generate a C API from the code, we need to differentiate the function names for the types. The default takes a message and this variant takes a digest (as used in Ethereum's precompile for ECRecover). * take out ECDSA test requiring OpenSSL v3.3 or higher --------- Co-authored-by: Mamy Ratsimbazafy <[email protected]>
1 parent ef2a91a commit 9dd1205

8 files changed

+280
-43
lines changed

constantine.nimble

+2-1
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,8 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[
610610
("tests/t_ethereum_verkle_ipa_primitives.nim", false),
611611

612612
# Signatures
613-
("tests/ecdsa/t_ecdsa_verify_openssl.nim", false),
613+
# NOTE: Requires OpenSSL version >=v3.3 for to Keccak256 support
614+
# ("tests/ecdsa/t_ecdsa_verify_openssl.nim", false),
614615

615616
# Proof systems
616617
# ----------------------------------------------------------

constantine/ecdsa_secp256k1.nim constantine/eth_ecdsa_signatures.nim

+34-4
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99
import
1010
constantine/zoo_exports,
1111
constantine/signatures/ecdsa,
12-
constantine/hashes/h_sha256,
12+
constantine/hashes,
1313
constantine/named/algebras,
1414
constantine/math/elliptic/[ec_shortweierstrass_affine],
1515
constantine/math/[arithmetic, ec_shortweierstrass],
1616
constantine/platforms/[abstractions, views]
1717

1818
export NonceSampler
1919

20-
const prefix_ffi = "ctt_ecdsa_secp256k1_"
20+
const prefix_ffi = "ctt_eth_ecdsa"
2121
type
2222
SecretKey* {.byref, exportc: prefix_ffi & "seckey".} = object
2323
## A Secp256k1 secret key
@@ -51,18 +51,48 @@ proc sign*(sig: var Signature,
5151
## Sign `message` using `secretKey` and store the signature in `sig`. The nonce
5252
## will either be randomly sampled `nsRandom` or deterministically calculated according
5353
## to RFC6979 (`nsRfc6979`)
54-
sig.coreSign(secretKey.raw, message, sha256, nonceSampler)
54+
sig.coreSign(secretKey.raw, message, keccak256, nonceSampler)
5555

5656
proc verify*(
5757
publicKey: PublicKey,
5858
message: openArray[byte],
5959
signature: Signature
6060
): bool {.libPrefix: prefix_ffi, genCharAPI.} =
6161
## Verify `signature` using `publicKey` for `message`.
62-
result = publicKey.raw.coreVerify(message, signature, sha256)
62+
result = publicKey.raw.coreVerify(message, signature, keccak256)
6363

6464
func derive_pubkey*(public_key: var PublicKey, secret_key: SecretKey) {.libPrefix: prefix_ffi.} =
6565
## Derive the public key matching with a secret key
6666
##
6767
## The secret_key MUST be validated
6868
public_key.raw.derivePubkey(secret_key.raw)
69+
70+
proc recoverPubkey*(
71+
publicKey: var PublicKey,
72+
message: openArray[byte],
73+
signature: Signature,
74+
evenY: bool
75+
) {.libPrefix: prefix_ffi, genCharAPI.} =
76+
## Verify `signature` using `publicKey` for `message`.
77+
##
78+
## `evenY == true` returns the public key corresponding to the
79+
## even `y` coordinate of the `R` point.
80+
publicKey.raw.recoverPubkey(signature, message, evenY, keccak256)
81+
82+
proc recoverPubkeyFromDigest*(
83+
publicKey: var PublicKey,
84+
msgHash: Fr[Secp256k1],
85+
signature: Signature,
86+
evenY: bool
87+
) {.libPrefix: prefix_ffi.} =
88+
## Verify `signature` using `publicKey` for the given message digest
89+
## given as a scalar in the field `Fr[Secp256k1]`.
90+
##
91+
## `evenY == true` returns the public key corresponding to the
92+
## even `y` coordinate of the `R` point.
93+
##
94+
## As this overload works directly with a message hash as a scalar,
95+
## it requires no hash function. Internally, it also calls the
96+
## `verify` implementation, which already takes a scalar and thus
97+
## requires no hash function there either.
98+
publicKey.raw.recoverPubkeyImpl_vartime(signature, msgHash, evenY)

constantine/ethereum_evm_precompiles.nim

+79-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import
2222
./hash_to_curve/hash_to_curve,
2323
# For KZG point precompile
2424
./ethereum_eip4844_kzg,
25-
./serialization/codecs_status_codes
25+
./serialization/codecs_status_codes,
26+
# ECDSA for ECRecover
27+
./eth_ecdsa_signatures
2628

2729
# For KZG point precompile
2830
export EthereumKZGContext, TrustedSetupFormat, TrustedSetupStatus, trusted_setup_load, trusted_setup_delete
@@ -48,6 +50,7 @@ type
4850
cttEVM_PointNotOnCurve
4951
cttEVM_PointNotInSubgroup
5052
cttEVM_VerificationFailure
53+
cttEVM_MalformedSignature
5154

5255
func eth_evm_sha256*(r: var openArray[byte], inputs: openArray[byte]): CttEVMStatus {.libPrefix: prefix_ffi, meter.} =
5356
## SHA256
@@ -1295,3 +1298,78 @@ func eth_evm_kzg_point_evaluation*(ctx: ptr EthereumKZGContext,
12951298
r.toOpenArray(32, 64-1).marshal(Fr[BLS12_381].getModulus(), bigEndian)
12961299

12971300
result = cttEVM_Success
1301+
1302+
import std / importutils # Alternatively make `r`, `s` visible or define setter or constructor
1303+
func eth_evm_ecrecover*(r: var openArray[byte],
1304+
input: openArray[byte]): CttEVMStatus {.libPrefix: prefix_ffi, meter.} =
1305+
## Attempts to recover the public key, which was used to sign the given `data`
1306+
## to obtain the given signature `sig`.
1307+
##
1308+
## If the signature is invalid, the result array `r` will contain the neutral
1309+
## element of the curve.
1310+
##
1311+
## Inputs:
1312+
## - `r`: Array of the recovered public key. An elliptic curve point in affine
1313+
## coordinates (`EC_ShortW_Aff[Fp[Secp256k1], G1]`).
1314+
## - `input`: The input data as an array of 128 bytes. The data is as follows:
1315+
## - 32 byte: `keccak256` digest of the message that was signed
1316+
## - 32 byte: `v`, decides if the even or odd coordinate in `R` was used
1317+
## - 32 byte: `r` of the signature, scalar `Fr[Secp256k1]`
1318+
## - 32 byte: `s` of the signature, scalar `Fr[Secp256k1]`
1319+
##
1320+
## Implementation follows Geth here:
1321+
## https://github.com/ethereum/go-ethereum/blob/341647f1865dab437a690dc1424ba71495de2dd8/core/vm/contracts.go#L243-L272
1322+
##
1323+
## and to a lesser extent the Ethereum Yellow Paper in appendix F:
1324+
## https://ethereum.github.io/yellowpaper/paper.pdf
1325+
##
1326+
## Internal Geth implementation in:
1327+
## https://github.com/ethereum/go-ethereum/blob/master/signer/core/signed_data.go#L292-L319
1328+
if len(input) != 128:
1329+
return cttEVM_InvalidInputSize
1330+
1331+
if len(r) != 32:
1332+
return cttEVM_InvalidOutputSize
1333+
1334+
# 1. construct message hash as scalar in field `Fr[Secp256k1]`
1335+
var msgBI {.noinit.}: BigInt[256]
1336+
msgBI.unmarshal(input.toOpenArray(0, 32-1), bigEndian)
1337+
var msgHash {.noinit.}: Fr[Secp256k1]
1338+
msgHash.fromBig(msgBI)
1339+
1340+
# 2. verify `v` data is valid
1341+
## XXX: Or construct a `BigInt[256]` instead and compare? (or compare with uint64s?)
1342+
for i in 32 ..< 63: # first 31 bytes must be zero for a valid `v`
1343+
if input[i] != byte 0:
1344+
return cttEVM_MalformedSignature
1345+
let v = input[63]
1346+
if v notin [byte 0, 1, 27, 28]:
1347+
return cttEVM_MalformedSignature
1348+
# 2a. determine if even or odd `y` coordinate
1349+
let evenY = v in [byte 0, 27] # 0 / 27 indicates `y` to be even, 1 / 28 odd
1350+
1351+
# 3. unmarshal signature data
1352+
var signature {.noinit.}: Signature
1353+
privateAccess(Signature)
1354+
var rSig {.noinit}, sSig {.noinit.}: BigInt[256]
1355+
rSig.unmarshal(input.toOpenArray(64, 96-1), bigEndian)
1356+
sSig.unmarshal(input.toOpenArray(96, 128-1), bigEndian)
1357+
signature.r = Fr[Secp256k1].fromBig(rSig)
1358+
signature.s = Fr[Secp256k1].fromBig(sSig)
1359+
1360+
# 4. perform pubkey recovery
1361+
var pubKey {.noinit.}: PublicKey
1362+
pubKey.recoverPubkeyFromDigest(msgHash, signature, evenY)
1363+
1364+
# 4. now calculate the Ethereum address of the public key (keccak256)
1365+
privateAccess(PublicKey)
1366+
var rawPubkey {.noinit.}: array[64, byte] # `[x, y]` coordinates of public key
1367+
rawPubkey.toOpenArray( 0, 32-1).marshal(pubKey.raw.x, bigEndian)
1368+
rawPubkey.toOpenArray(32, 64-1).marshal(pubKey.raw.y, bigEndian)
1369+
var dgst {.noinit.}: array[32, byte] # keccak256 digest
1370+
keccak256.hash(dgst, rawPubkey)
1371+
1372+
# 5. and effectively truncate to last 20 bytes of digest
1373+
r.rawCopy(12, dgst, 12, 20)
1374+
1375+
result = cttEVM_Success

constantine/serialization/codecs_ecdsa_secp256k1.nim

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import
3838
constantine/math/arithmetic/finite_fields,
3939
constantine/math/elliptic/ec_shortweierstrass_affine,
4040
constantine/math/io/io_bigints,
41-
constantine/ecdsa_secp256k1
41+
constantine/eth_ecdsa_signatures
4242

4343
import std / [strutils, base64, math, importutils]
4444

0 commit comments

Comments
 (0)