Skip to content
Open
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
37 changes: 37 additions & 0 deletions std/evmprecompiles/256-p256verify.go
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
Comment on lines +22 to +26
Copy link

Copilot AI Nov 14, 2025

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.

Suggested change
// 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
// The following checks are performed by external systems in the zkEVM context (arithmetization/ECDATA).
// For general-purpose use outside zkEVM, 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 a valid point on the P-256 curve,
// 5. (qx, qy) ≠ (0,0).

Copilot uses AI. Check for mistakes.
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
}
145 changes: 145 additions & 0 deletions std/evmprecompiles/256-p256verify_test.go
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"
"io/ioutil"
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The io/ioutil package is deprecated since Go 1.16. Replace ioutil.ReadFile with os.ReadFile and update the import to use "os" instead of "io/ioutil".

Suggested change
"io/ioutil"

Copilot uses AI. Check for mistakes.
"math/big"
"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
}

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 := ioutil.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)
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Assert Context Breaks Subtest Isolation

The assert helper is created with the outer test context but used inside t.Run subtests. When assert.NoError(err) fails, it reports to the parent test rather than the subtest, causing the entire test function to stop instead of just failing the specific subtest. This prevents other test vectors from running and makes it difficult to identify which specific vector failed. The assertion should use the subtest's t parameter instead.

Fix in Cursor Fix in Web

}
}

// --- 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
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function splitInput160 silently returns nil values when the input length is not 160 bytes. This could lead to nil pointer dereferences on line 97-101 where the returned values are dereferenced. Consider returning an error or panicking with a descriptive message if the length check fails.

Suggested change
return nil, nil, nil, nil, nil
panic(fmt.Sprintf("splitInput160: input length is %d bytes, expected 160", len(raw)))

Copilot uses AI. Check for mistakes.
}
h = new(big.Int).SetBytes(raw[0:32])
r = new(big.Int).SetBytes(raw[32:64])
s = new(big.Int).SetBytes(raw[64:96])
qx = new(big.Int).SetBytes(raw[96:128])
qy = new(big.Int).SetBytes(raw[128:160])
return
}

func expectedBool(s string) bool {
raw, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
one := make([]byte, 32)
one[31] = 1
return bytes.Equal(raw, one)
}
Loading
Loading