mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-01 18:57:37 +00:00
## Why this should be merged Implementation for [ACP-204](https://github.com/avalanche-foundation/ACPs/pull/204). ## How this works Implements [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) and an additional `Pack(hash,r,s,key)` function with precompile-compatible output. ## How this was tested Unit tests including fuzzing, test case from the proposed but unmerged geth implementation, and vectors from [Project Wycheproof](https://github.com/C2SP/wycheproof).
264 lines
6.9 KiB
Go
264 lines
6.9 KiB
Go
// Copyright 2025 the libevm authors.
|
|
//
|
|
// The libevm additions to go-ethereum are free software: you can redistribute
|
|
// them and/or modify them under the terms of the GNU Lesser General Public License
|
|
// as published by the Free Software Foundation, either version 3 of the License,
|
|
// or (at your option) any later version.
|
|
//
|
|
// The libevm additions are distributed in the hope that they will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
|
|
// General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see
|
|
// <http://www.gnu.org/licenses/>.
|
|
|
|
package p256verify
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/asn1"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ava-labs/libevm/common"
|
|
"github.com/ava-labs/libevm/core/vm"
|
|
"github.com/ava-labs/libevm/libevm"
|
|
"github.com/ava-labs/libevm/libevm/ethtest"
|
|
"github.com/ava-labs/libevm/libevm/hookstest"
|
|
"github.com/ava-labs/libevm/params"
|
|
|
|
_ "embed"
|
|
)
|
|
|
|
var _ vm.PrecompiledContract = Precompile{}
|
|
|
|
// ulerdoganTestCase is the test case from
|
|
// https://github.com/ulerdogan/go-ethereum/blob/cec0b058115282168c5afc5197de3f6b5479dc4a/core/vm/testdata/precompiles/p256Verify.json,
|
|
// copied under LGPL. See the respective commit for copyright and license
|
|
// information.
|
|
const ulerdoganTestCase = `4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e`
|
|
|
|
//go:embed testdata/ecdsa_secp256r1_sha256_test.json
|
|
var wycheproofECDSASHA256 []byte
|
|
|
|
type testCase struct {
|
|
name string
|
|
in []byte
|
|
wantSuccess bool
|
|
}
|
|
|
|
func signAndPack(tb testing.TB, priv *ecdsa.PrivateKey, hash [32]byte) []byte {
|
|
tb.Helper()
|
|
r, s, err := ecdsa.Sign(rand.Reader, priv, hash[:])
|
|
require.NoError(tb, err, "ecdsa.Sign()")
|
|
return Pack(hash, r, s, &priv.PublicKey)
|
|
}
|
|
|
|
func TestPrecompile(t *testing.T) {
|
|
assert.Equal(t, params.P256VerifyGas, Precompile{}.RequiredGas(nil), "RequiredGas()")
|
|
|
|
tests := []testCase{
|
|
{
|
|
name: "empty_input",
|
|
},
|
|
{
|
|
name: "input_too_short",
|
|
in: make([]byte, inputLen-1),
|
|
},
|
|
{
|
|
name: "input_too_long",
|
|
in: make([]byte, inputLen+1),
|
|
},
|
|
{
|
|
name: "pub_key_at_infinity",
|
|
in: make([]byte, inputLen),
|
|
},
|
|
{
|
|
name: "pub_key_not_on_curve",
|
|
in: []byte{inputLen - 1: 1},
|
|
},
|
|
{
|
|
name: "ulerdogan",
|
|
in: common.Hex2Bytes(ulerdoganTestCase),
|
|
wantSuccess: true,
|
|
},
|
|
}
|
|
|
|
for range 50 {
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err, "ecdsa.GenerateKey(elliptic.P256(), crypto/rand.Reader)")
|
|
|
|
for range 50 {
|
|
var toSign [32]byte
|
|
_, err := rand.Read(toSign[:])
|
|
require.NoErrorf(t, err, "crypto/rand.Read(%T)", toSign)
|
|
|
|
in := signAndPack(t, priv, toSign)
|
|
tests = append(tests, testCase{
|
|
name: "fuzz_valid",
|
|
in: in,
|
|
wantSuccess: true,
|
|
})
|
|
corrupt := slices.Clone(in)
|
|
corrupt[0]++ // different signed hash
|
|
tests = append(tests, testCase{
|
|
name: "fuzz_invalid",
|
|
in: corrupt,
|
|
})
|
|
}
|
|
}
|
|
|
|
tests = append(tests, wycheproofTestCases(t)...)
|
|
if t.Failed() {
|
|
return
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := Precompile{}.Run(tt.in)
|
|
require.NoError(t, err, "Run() always returns nil, even on verification failure")
|
|
|
|
var want []byte
|
|
if tt.wantSuccess {
|
|
want = common.LeftPadBytes([]byte{1}, 32)
|
|
}
|
|
assert.Equal(t, want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
type jsonHex []byte
|
|
|
|
var _ json.Unmarshaler = (*jsonHex)(nil)
|
|
|
|
func (j *jsonHex) UnmarshalJSON(data []byte) error {
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return err
|
|
}
|
|
b, err := hex.DecodeString(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*j = b
|
|
return nil
|
|
}
|
|
|
|
func wycheproofTestCases(t *testing.T) []testCase {
|
|
t.Helper()
|
|
|
|
var raw struct {
|
|
Groups []struct {
|
|
Key struct {
|
|
X jsonHex `json:"wx"`
|
|
Y jsonHex `json:"wy"`
|
|
}
|
|
Tests []struct {
|
|
ID int `json:"tcId"`
|
|
Comment string
|
|
Preimage jsonHex `json:"msg"`
|
|
ASNSig jsonHex `json:"sig"`
|
|
Result string
|
|
} `json:"tests"`
|
|
} `json:"testGroups"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(wycheproofECDSASHA256, &raw))
|
|
|
|
var cases []testCase
|
|
for _, group := range raw.Groups {
|
|
key := &ecdsa.PublicKey{
|
|
Curve: elliptic.P256(),
|
|
X: new(big.Int).SetBytes(group.Key.X),
|
|
Y: new(big.Int).SetBytes(group.Key.Y),
|
|
}
|
|
|
|
for _, test := range group.Tests {
|
|
t.Run(fmt.Sprintf("parse_test_%d", test.ID), func(t *testing.T) {
|
|
// Many of the invalid cases are due to ASN1-specific problems,
|
|
// which aren't of concern to us.
|
|
include := test.Result == "valid" ||
|
|
strings.Contains(test.Comment, "r or s") ||
|
|
strings.Contains(test.Comment, "r and s") ||
|
|
slices.Contains(
|
|
[]int{
|
|
// Special cases of r and/or s.
|
|
286, 294, 295, 303, 304, 340, 341,
|
|
342, 343, 356, 357, 358, 359,
|
|
},
|
|
test.ID,
|
|
)
|
|
|
|
include = include && !slices.Contains(
|
|
// These cases have negative r or s value(s) with the same
|
|
// absolute value(s) as valid signatures. Packing and then
|
|
// unpacking via [big.Int.Bytes] therefore converts them to
|
|
// the valid, positive values that pass verification and
|
|
// raise false-positive test errors.
|
|
[]int{133, 139, 140},
|
|
test.ID,
|
|
)
|
|
if !include {
|
|
return
|
|
}
|
|
|
|
var rs [2]*big.Int
|
|
rest, err := asn1.Unmarshal(test.ASNSig, &rs)
|
|
if err != nil || len(rest) > 0 {
|
|
return
|
|
}
|
|
if rs[0].BitLen() > 256 || rs[1].BitLen() > 256 {
|
|
return
|
|
}
|
|
cases = append(cases, testCase{
|
|
name: fmt.Sprintf("wycheproof_ecdsa_secp256r1_sha256_%d", test.ID),
|
|
in: Pack(sha256.Sum256(test.Preimage), rs[0], rs[1], key),
|
|
wantSuccess: test.Result == "valid",
|
|
})
|
|
})
|
|
}
|
|
}
|
|
t.Logf("%d Wycheproof cases", len(cases))
|
|
return cases
|
|
}
|
|
|
|
func BenchmarkPrecompile(b *testing.B) {
|
|
in := common.Hex2Bytes(ulerdoganTestCase)
|
|
var p Precompile
|
|
|
|
for range b.N {
|
|
// Explicitly drop return values to placate the linter. The error is
|
|
// always nil and the input is tested above.
|
|
_, _ = p.Run(in)
|
|
}
|
|
}
|
|
|
|
func TestViaEVM(t *testing.T) {
|
|
addr := common.Address{42}
|
|
hooks := hookstest.Stub{
|
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
|
addr: Precompile{},
|
|
},
|
|
}
|
|
hooks.Register(t)
|
|
|
|
_, evm := ethtest.NewZeroEVM(t)
|
|
in := common.Hex2Bytes(ulerdoganTestCase)
|
|
|
|
got, _, err := evm.Call(vm.AccountRef{}, addr, in, 25000, uint256.NewInt(0))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte{31: 1}, got)
|
|
}
|