-
Notifications
You must be signed in to change notification settings - Fork 493
feat: EIP-7951 for ECDSA on P-256 curve #1649
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
base: master
Are you sure you want to change the base?
Changes from all commits
265d08f
847ba74
3bc2819
3f6b94b
72ad80d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package evmprecompiles | ||
|
|
||
| import ( | ||
| "github.com/consensys/gnark/frontend" | ||
| "github.com/consensys/gnark/std/algebra/emulated/sw_emulated" | ||
| "github.com/consensys/gnark/std/math/emulated" | ||
| "github.com/consensys/gnark/std/signature/ecdsa" | ||
| ) | ||
|
|
||
| // P256Verify implements [P256Verify] precompile contract at address 0x100. | ||
| // | ||
| // This circuit performs ECDSA signature verification over the secp256r1 | ||
| // elliptic curve (also known as P-256 or prime256v1). | ||
| // | ||
| // [P256Verify]: https://eips.ethereum.org/EIPS/eip-7951 | ||
| func P256Verify(api frontend.API, | ||
| msgHash emulated.Element[emulated.P256Fr], | ||
| r, s emulated.Element[emulated.P256Fr], | ||
| qx, qy emulated.Element[emulated.P256Fp], | ||
| ) frontend.Variable { | ||
| // Input validation: | ||
| // 1. input_length == 160 ==> checked by the arithmetization | ||
| // 2. 0 < r < n and 0 < s < n ==> checked by the arithmetization/ECDATA and enforced in `IsValid()` | ||
| // 3. 0 ≤ qx < p and 0 ≤ qy < p ==> checked by the arithmetization/ECDATA | ||
| // 4. (qx, qy) is a valid point on the curve P256 ==> checked by the arithmetization/ECDATA | ||
| // 5. (qx, qy) is not (0,0) ==> checked by the arithmetization/ECDATA | ||
| pk := ecdsa.PublicKey[emulated.P256Fp, emulated.P256Fr]{ | ||
| X: qx, | ||
| Y: qy, | ||
| } | ||
| sig := ecdsa.Signature[emulated.P256Fr]{ | ||
| R: r, | ||
| S: s, | ||
| } | ||
| verified := pk.IsValid(api, sw_emulated.GetCurveParams[emulated.P256Fp](), &msgHash, &sig) | ||
| return verified | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,145 @@ | ||||||
| package evmprecompiles | ||||||
|
|
||||||
| import ( | ||||||
| "bytes" | ||||||
| "crypto/rand" | ||||||
| "encoding/hex" | ||||||
| "encoding/json" | ||||||
| "fmt" | ||||||
| "math/big" | ||||||
| "os" | ||||||
| "testing" | ||||||
|
|
||||||
| "github.com/consensys/gnark-crypto/ecc" | ||||||
| "github.com/consensys/gnark-crypto/ecc/secp256r1/ecdsa" | ||||||
| "github.com/consensys/gnark/frontend" | ||||||
| "github.com/consensys/gnark/std/math/emulated" | ||||||
| "github.com/consensys/gnark/test" | ||||||
| ) | ||||||
|
|
||||||
| type p256verifyCircuit struct { | ||||||
| MsgHash emulated.Element[emulated.P256Fr] | ||||||
| R emulated.Element[emulated.P256Fr] | ||||||
| S emulated.Element[emulated.P256Fr] | ||||||
| Qx, Qy emulated.Element[emulated.P256Fp] | ||||||
| Expected frontend.Variable | ||||||
| } | ||||||
|
|
||||||
| func (c *p256verifyCircuit) Define(api frontend.API) error { | ||||||
| res := P256Verify(api, c.MsgHash, c.R, c.S, c.Qx, c.Qy) | ||||||
| api.AssertIsEqual(c.Expected, res) | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func TestP256VerifyCircuit(t *testing.T) { | ||||||
| assert := test.NewAssert(t) | ||||||
| // key generation | ||||||
| sk, err := ecdsa.GenerateKey(rand.Reader) | ||||||
| if err != nil { | ||||||
| t.Fatal("generate", err) | ||||||
| } | ||||||
| pk := sk.PublicKey | ||||||
| // signing | ||||||
| msg := []byte("test") | ||||||
| sigBuf, err := sk.Sign(msg, nil) | ||||||
| if err != nil { | ||||||
| t.Fatal("sign", err) | ||||||
| } | ||||||
| // verification | ||||||
| verified, err := sk.PublicKey.Verify(sigBuf, msg, nil) | ||||||
| if err != nil { | ||||||
| t.Fatal("verify", err) | ||||||
| } | ||||||
| // marshalling | ||||||
| var sig ecdsa.Signature | ||||||
| sig.SetBytes(sigBuf[:]) | ||||||
| var r, s big.Int | ||||||
| r.SetBytes(sig.R[:]) | ||||||
| s.SetBytes(sig.S[:]) | ||||||
| hash := ecdsa.HashToInt(msg) | ||||||
| var expected frontend.Variable | ||||||
| if verified { | ||||||
| expected = 1 | ||||||
| } | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
|
||||||
| circuit := p256verifyCircuit{} | ||||||
| witness := p256verifyCircuit{ | ||||||
| MsgHash: emulated.ValueOf[emulated.P256Fr](hash), | ||||||
| R: emulated.ValueOf[emulated.P256Fr](r), | ||||||
| S: emulated.ValueOf[emulated.P256Fr](s), | ||||||
| Qx: emulated.ValueOf[emulated.P256Fp](pk.A.X), | ||||||
| Qy: emulated.ValueOf[emulated.P256Fp](pk.A.Y), | ||||||
| Expected: expected, | ||||||
| } | ||||||
| err = test.IsSolved(&circuit, &witness, ecc.BN254.ScalarField()) | ||||||
| assert.NoError(err) | ||||||
| } | ||||||
|
|
||||||
| func TestP256VerifyCircuitWithEIPVectors(t *testing.T) { | ||||||
| assert := test.NewAssert(t) | ||||||
| data, err := os.ReadFile("test_vectors/p256verify_vectors_clean.json") | ||||||
| if err != nil { | ||||||
| t.Fatalf("read vectors.json: %v", err) | ||||||
| } | ||||||
|
|
||||||
| var vecs []vector | ||||||
| if err := json.Unmarshal(data, &vecs); err != nil { | ||||||
| t.Fatalf("unmarshal: %v", err) | ||||||
| } | ||||||
| for i, v := range vecs { | ||||||
| h, r, s, qx, qy := splitInput160(v.Input) | ||||||
| verified := expectedBool(v.Expected) | ||||||
| expected := frontend.Variable(0) | ||||||
| if verified { | ||||||
| expected = 1 | ||||||
| } | ||||||
| witness := p256verifyCircuit{ | ||||||
| MsgHash: emulated.ValueOf[emulated.P256Fr](*h), | ||||||
| R: emulated.ValueOf[emulated.P256Fr](*r), | ||||||
| S: emulated.ValueOf[emulated.P256Fr](*s), | ||||||
| Qx: emulated.ValueOf[emulated.P256Fp](*qx), | ||||||
| Qy: emulated.ValueOf[emulated.P256Fp](*qy), | ||||||
| Expected: expected, | ||||||
| } | ||||||
|
|
||||||
| circuit := p256verifyCircuit{} | ||||||
|
|
||||||
| t.Run(fmt.Sprintf("vector_%03d_%s", i, v.Name), func(t *testing.T) { | ||||||
| err := test.IsSolved(&circuit, &witness, ecc.BN254.ScalarField()) | ||||||
| assert.NoError(err) | ||||||
| }) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Assert Context Breaks Subtest IsolationThe |
||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // --- utils | ||||||
| type vector struct { | ||||||
| Name string `json:"Name,omitempty"` | ||||||
| Input string `json:"Input"` | ||||||
| Expected string `json:"Expected"` | ||||||
| } | ||||||
|
|
||||||
| func splitInput160(hexInput string) (h, r, s, qx, qy *big.Int) { | ||||||
| raw, err := hex.DecodeString(hexInput) | ||||||
| if err != nil { | ||||||
| panic(err) | ||||||
| } | ||||||
| if len(raw) != 160 { | ||||||
| return nil, nil, nil, nil, nil | ||||||
|
||||||
| return nil, nil, nil, nil, nil | |
| panic(fmt.Sprintf("splitInput160: input length is %d bytes, expected 160", len(raw))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comments reference "arithmetization" and "ECDATA" which are zkEVM-specific implementation details. For a general-purpose gnark circuit, these validations are not performed by external systems. The comments should clarify that when used outside a zkEVM context, the caller must ensure: (1) input length is 160 bytes, (2) 0 < r < n and 0 < s < n, (3) 0 ≤ qx < p and 0 ≤ qy < p, (4) (qx, qy) is on the P-256 curve, and (5) (qx, qy) ≠ (0,0). Alternatively, consider adding explicit validation within the circuit for general-purpose use.