go-ethereum/libevm/precompiles/p256verify/p256verify.go
Arran Schlosberg 2672fbd7cd
feat: RIP-7212 precompile for P256 ECDSA verification (#184)
## 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).
2025-06-10 10:28:02 -04:00

122 lines
3.3 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 implements an EVM precompile to verify P256 ECDSA
// signatures, as described in RIP-7212.
package p256verify
import (
"crypto/ecdsa"
"crypto/elliptic"
"math/big"
"github.com/ava-labs/libevm/params"
)
// Precompile implements ECDSA verification on the P256 curve, as defined by
// [RIP-7212].
//
// [RIP-7212]: https://github.com/ethereum/RIPs/blob/1f55794f65caa4c4bb2b8d9bda7d713b8c734157/RIPS/rip-7212.md
type Precompile struct{}
// RequiredGas returns [params.P256VerifyGas].
func (Precompile) RequiredGas([]byte) uint64 {
return params.P256VerifyGas
}
const (
wordLen = 32
inputLen = 5 * wordLen
)
type input [inputLen]byte
type index int
const (
hashPos index = iota * wordLen
rPos
sPos
xPos
yPos
)
// Run parses and verifies the signature. On success it returns a 32-byte
// big-endian representation of the number 1, otherwise it returns an empty
// slice. The returned error is always nil.
func (Precompile) Run(sig []byte) ([]byte, error) {
if len(sig) != inputLen || !(*input)(sig).verify() {
return nil, nil
}
return bigEndianOne(), nil
}
func bigEndianOne() []byte {
return []byte{wordLen - 1: 1}
}
func (in *input) verify() bool {
key, ok := in.pubkey()
if !ok {
return false
}
return ecdsa.Verify(key, in.word(hashPos), in.bigWord(rPos), in.bigWord(sPos))
}
func (in *input) pubkey() (*ecdsa.PublicKey, bool) {
x := in.bigWord(xPos)
y := in.bigWord(yPos)
// There is no need to explicitly check for the point at infinity because
// [elliptic.Curve] documentation states that it's not on the curve and the
// check would therefore be performed twice.
// See https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/crypto/elliptic/nistec.go;l=132
curve := elliptic.P256()
if !curve.IsOnCurve(x, y) {
return nil, false
}
return &ecdsa.PublicKey{
Curve: curve,
X: x,
Y: y,
}, true
}
func (in *input) word(i index) []byte {
return in[i : i+wordLen]
}
func (in *input) bigWord(i index) *big.Int {
return new(big.Int).SetBytes(in.word(i))
}
// Pack packs the arguments into a byte slice compatible with [Precompile.Run].
// It does NOT perform any validation on its inputs and therefore may panic if,
// for example, a [big.Int] with >256 bits is received. Keys and signatures
// generated with [elliptic.GenerateKey] and [ecdsa.Sign] are valid inputs.
func Pack(hash [32]byte, r, s *big.Int, key *ecdsa.PublicKey) []byte {
var in input
copy(in.word(hashPos), hash[:])
r.FillBytes(in.word(rPos))
s.FillBytes(in.word(sPos))
key.X.FillBytes(in.word(xPos))
key.Y.FillBytes(in.word(yPos))
return in[:]
}