Skip to content

Commit

Permalink
feat(precompiles): EIP-7212: secp256r1 curve (#1922)
Browse files Browse the repository at this point in the history
* p256

* register precompile

* upgrade handler

* integration tests

* tests

* integration tests

* changelog

* rm unused code

* cleanup old tests

* fix ginkgo

* fix: added new precompile address to bech32 slice so they cannot accept Transfer

---------

Co-authored-by: Vlad <vladislav.varadinov@gmail.com>
  • Loading branch information
fedekunze and Vvaradinov committed Oct 23, 2023
1 parent fab6142 commit 2ad91e2
Show file tree
Hide file tree
Showing 17 changed files with 625 additions and 4 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ Ref: https://keepachangelog.com/en/1.0.0/
- (imp) [#1847](https://github.com/evmos/evmos/pull/1847) Remove crisis module in v15 upgrade handler.
- (evm) [#1900](https://github.com/evmos/evmos/pull/1900) Enable [EIP 3855](https://eips.ethereum.org/EIPS/eip-3855) (`PUSH0` opcode) by default.


### Features

- (p256) [#1922](https://github.com/evmos/evmos/pull/1922) [EIP-7212](https://eips.ethereum.org/EIPS/eip-7212) `secp256r1` curve precompile

### Improvements

- (ics20) [#1850](https://github.com/evmos/evmos/pull/1850) Extract common Grant checking and accepting methods.
Expand Down
10 changes: 10 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ import (
v13 "github.com/evmos/evmos/v15/app/upgrades/v13"
v14 "github.com/evmos/evmos/v15/app/upgrades/v14"
v15 "github.com/evmos/evmos/v15/app/upgrades/v15"
v16 "github.com/evmos/evmos/v15/app/upgrades/v16"
v8 "github.com/evmos/evmos/v15/app/upgrades/v8"
v81 "github.com/evmos/evmos/v15/app/upgrades/v8_1"
v82 "github.com/evmos/evmos/v15/app/upgrades/v8_2"
Expand Down Expand Up @@ -1325,6 +1326,15 @@ func (app *Evmos) setupUpgradeHandlers() {
),
)

// v15 upgrade handler
app.UpgradeKeeper.SetUpgradeHandler(
v16.UpgradeName,
v16.CreateUpgradeHandler(
app.mm, app.configurator,
app.EvmKeeper,
),
)

// When a planned update height is reached, the old binary will panic
// writing on disk the height and name of the update that triggered it
// This will read that value, and execute the preparations for the upgrade.
Expand Down
10 changes: 10 additions & 0 deletions app/upgrades/v16/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright Tharsis Labs Ltd.(Evmos)
// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
package v15

const (
// UpgradeName is the shared upgrade plan name for mainnet
UpgradeName = "v16.0.0"
// UpgradeInfo defines the binaries that will be used for the upgrade
UpgradeInfo = `'{"binaries":{"darwin/amd64":"https://github.com/evmos/evmos/releases/download/v16.0.0/evmos_16.0.0_Darwin_arm64.tar.gz","darwin/x86_64":"https://github.com/evmos/evmos/releases/download/v16.0.0/evmos_16.0.0_Darwin_x86_64.tar.gz","linux/arm64":"https://github.com/evmos/evmos/releases/download/v16.0.0/evmos_16.0.0_Linux_arm64.tar.gz","linux/amd64":"https://github.com/evmos/evmos/releases/download/v16.0.0/evmos_16.0.0_Linux_amd64.tar.gz","windows/x86_64":"https://github.com/evmos/evmos/releases/download/v16.0.0/evmos_16.0.0_Windows_x86_64.zip"}}'`
)
35 changes: 35 additions & 0 deletions app/upgrades/v16/upgrades.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright Tharsis Labs Ltd.(Evmos)
// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
package v15

import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
"github.com/evmos/evmos/v15/precompiles/p256"
"github.com/evmos/evmos/v15/utils"
evmkeeper "github.com/evmos/evmos/v15/x/evm/keeper"
)

// CreateUpgradeHandler creates an SDK upgrade handler for v15.0.0
func CreateUpgradeHandler(
mm *module.Manager,
configurator module.Configurator,
ek *evmkeeper.Keeper,
) upgradetypes.UpgradeHandler {
return func(ctx sdk.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) {
logger := ctx.Logger().With("upgrade", UpgradeName)

p256Address := p256.Precompile{}.Address()
// enable secp256r1 precompile on testnet
if utils.IsTestnet(ctx.ChainID()) {
if err := ek.EnablePrecompiles(ctx, p256Address); err != nil {
logger.Error("failed to enable precompiles", "error", err.Error())
}
}

// Leave modules are as-is to avoid running InitGenesis.
logger.Debug("running module migrations ...")
return mm.RunMigrations(ctx, configurator, vm)
}
}
52 changes: 52 additions & 0 deletions crypto/secp256r1/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2014 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it 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 go-ethereum library is distributed in the hope that it 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 secp256r1

import (
"crypto/ecdsa"
"crypto/elliptic"
"math/big"
)

// Verifies the given signature (r, s) for the given hash and public key (x, y).
func Verify(hash []byte, r, s, x, y *big.Int) bool {
// Create the public key format
publicKey := newECDSAPublicKey(x, y)

// Check if they are invalid public key coordinates
if publicKey == nil {
return false
}

// Verify the signature with the public key,
// then return true if it's valid, false otherwise
return ecdsa.Verify(publicKey, hash, r, s)
}

// newECDSAPublicKey creates an ECDSA P256 public key from the given coordinates
func newECDSAPublicKey(x, y *big.Int) *ecdsa.PublicKey {
// Check if the given coordinates are valid and in the reference point (infinity)
if x == nil || y == nil || x.Sign() == 0 && y.Sign() == 0 || !elliptic.P256().IsOnCurve(x, y) {
return nil
}

return &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: x,
Y: y,
}
}
1 change: 1 addition & 0 deletions precompiles/common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var (
DefaultChainID = evmosutils.MainnetChainID + "-1"
// DefaultPrecompilesBech32 is the standard bech32 address for the precompiles
DefaultPrecompilesBech32 = []string{
"evmos1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqn2svlxe", // secp256r1 curve precompile
"evmos1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqq4xrkxv", // Staking precompile
"evmos1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqpgshrm7", // Distribution precompile
"evmos1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqzxrz44p", // ICS20 transfer precompile
Expand Down
166 changes: 166 additions & 0 deletions precompiles/p256/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright Tharsis Labs Ltd.(Evmos)
// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
package p256_test

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"

"github.com/cometbft/cometbft/crypto"
"github.com/ethereum/go-ethereum/common"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/evmos/evmos/v15/precompiles/p256"
"github.com/evmos/evmos/v15/testutil/integration/factory"
"github.com/evmos/evmos/v15/testutil/integration/grpc"
testkeyring "github.com/evmos/evmos/v15/testutil/integration/keyring"
"github.com/evmos/evmos/v15/testutil/integration/network"
"github.com/evmos/evmos/v15/testutil/integration/utils"
evmtypes "github.com/evmos/evmos/v15/x/evm/types"
)

type IntegrationTestSuite struct {
network network.Network
factory factory.TxFactory
keyring testkeyring.Keyring
precompileAddress common.Address
p256Priv *ecdsa.PrivateKey
}

var _ = Describe("Calling p256 precompile directly", Label("P256 Precompile"), Ordered, func() {
var s *IntegrationTestSuite

BeforeAll(func() {
keyring := testkeyring.New(1)
integrationNetwork := network.New(
network.WithPreFundedAccounts(keyring.GetAllAccAddrs()...),
)
grpcHandler := grpc.NewIntegrationHandler(integrationNetwork)
txFactory := factory.New(integrationNetwork, grpcHandler)
p256Priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
Expect(err).To(BeNil())

s = &IntegrationTestSuite{
network: integrationNetwork,
factory: txFactory,
keyring: keyring,
precompileAddress: p256.Precompile{}.Address(),
p256Priv: p256Priv,
}
})

AfterEach(func() {
// Start each test with a fresh block
err := s.network.NextBlock()
Expect(err).To(BeNil())
})

When("the precompile is enabled in the EVM params", func() {
DescribeTable("execute contract call", func(inputFn func() (input, expOutput []byte, expErr string)) {
senderKey := s.keyring.GetKey(0)

input, expOutput, expErr := inputFn()
args := evmtypes.EvmTxArgs{
To: &s.precompileAddress,
Input: input,
}

resDeliverTx, err := s.factory.ExecuteEthTx(senderKey.Priv, args)
Expect(err).To(BeNil())
Expect(resDeliverTx.IsOK()).To(Equal(true), "transaction should have succeeded", resDeliverTx.GetLog())

res, err := utils.DecodeResponseDeliverTx(resDeliverTx)
Expect(err).To(BeNil())
Expect(res.VmError).To(Equal(expErr), "expected different vm error")
Expect(res.Ret).To(Equal(expOutput))
},
Entry(
"valid signature",
func() (input, expOutput []byte, expErr string) {
input = signMsg([]byte("hello world"), s.p256Priv)
return input, trueValue, ""
},
),
Entry(
"invalid signature",
func() (input, expOutput []byte, expErr string) {
privB, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
Expect(err).To(BeNil())

hash := crypto.Sha256([]byte("hello world"))

rInt, sInt, err := ecdsa.Sign(rand.Reader, s.p256Priv, hash)
Expect(err).To(BeNil())

input = make([]byte, p256.VerifyInputLength)
copy(input[0:32], hash)
copy(input[32:64], rInt.Bytes())
copy(input[64:96], sInt.Bytes())
copy(input[96:128], privB.PublicKey.X.Bytes())
copy(input[128:160], privB.PublicKey.Y.Bytes())
return input, nil, ""
},
),
)
})

When("the precompile is not enabled in the EVM params", func() {
BeforeEach(func() {
params := evmtypes.DefaultParams()
addr := s.precompileAddress.String()
var activePrecompiles []string
for _, precompile := range params.ActivePrecompiles {
if precompile != addr {
activePrecompiles = append(activePrecompiles, precompile)
}
}
params.ActivePrecompiles = activePrecompiles
err := s.network.UpdateEvmParams(params)
Expect(err).To(BeNil())
})

DescribeTable("execute contract call", func(inputFn func() (input []byte)) {
senderKey := s.keyring.GetKey(0)

input := inputFn()
args := evmtypes.EvmTxArgs{
To: &s.precompileAddress,
Input: input,
}

_, err := s.factory.ExecuteEthTx(senderKey.Priv, args)
Expect(err).To(HaveOccurred(), "expected error while calling the precompile")
Expect(err.Error()).To(ContainSubstring("precompile not enabled"))
},
Entry(
"valid signature",
func() (input []byte) {
input = signMsg([]byte("hello world"), s.p256Priv)
return input
},
),
Entry(
"invalid signature",
func() (input []byte) {
privB, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
Expect(err).To(BeNil())

hash := crypto.Sha256([]byte("hello world"))

rInt, sInt, err := ecdsa.Sign(rand.Reader, s.p256Priv, hash)
Expect(err).To(BeNil())

input = make([]byte, p256.VerifyInputLength)
copy(input[0:32], hash)
copy(input[32:64], rInt.Bytes())
copy(input[64:96], sInt.Bytes())
copy(input[96:128], privB.PublicKey.X.Bytes())
copy(input[128:160], privB.PublicKey.Y.Bytes())
return input
},
),
)
})
})
82 changes: 82 additions & 0 deletions precompiles/p256/p256.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2014 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it 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 go-ethereum library is distributed in the hope that it 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 p256

import (
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/evmos/evmos/v15/crypto/secp256r1"
)

var _ vm.PrecompiledContract = &Precompile{}

const (
// P256VerifyGas is the secp256r1 elliptic curve signature verifier gas price
VerifyGas uint64 = 3450
// Required input length is 160 bytes
VerifyInputLength = 160
)

// Precompile secp256r1 (P256) signature verification
// implemented as a native contract as per EIP-7212
// See https://eips.ethereum.org/EIPS/eip-7212 for details
type Precompile struct{}

// Address defines the address of the staking compile contract.
// address: 0x0000000000000000000000000000000000000013
func (Precompile) Address() common.Address {
return common.BytesToAddress([]byte{19})
}

// RequiredGas returns the gas required to execute the precompiled contract
func (p Precompile) RequiredGas(_ []byte) uint64 {
return VerifyGas
}

// Run executes the P256 signature verification using ECDSA.
// Input data: 160 bytes of data including:
// - 32 bytes of the signed data hash
// - 32 bytes of the r component of the signature
// - 32 bytes of the s component of the signature
// - 32 bytes of the x coordinate of the public key
// - 32 bytes of the y coordinate of the public key
// Output data: 32 bytes of result data and error
// - If the signature verification process succeeds, it returns 1 in 32 bytes format
func (p *Precompile) Run(_ *vm.EVM, contract *vm.Contract, _ bool) (bz []byte, err error) {
input := contract.Input
// Check the input length
if len(input) != VerifyInputLength {
// Input length is invalid
return nil, nil
}

// Extract the hash, r, s, x, y from the input
hash := input[0:32]
r, s := new(big.Int).SetBytes(input[32:64]), new(big.Int).SetBytes(input[64:96])
x, y := new(big.Int).SetBytes(input[96:128]), new(big.Int).SetBytes(input[128:160])

// Verify the secp256r1 signature

if secp256r1.Verify(hash, r, s, x, y) {
// Signature is valid
return common.LeftPadBytes(common.Big1.Bytes(), 32), nil
}

// Signature is invalid
return nil, nil
}

0 comments on commit 2ad91e2

Please sign in to comment.