Skip to content
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

feat(precompiles): EIP-7212: secp256r1 curve #1922

Merged
merged 15 commits into from
Oct 23, 2023
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 ...")
fedekunze marked this conversation as resolved.
Show resolved Hide resolved
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,
}
}
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
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it correct this copyright?

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

Choose a reason for hiding this comment

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

Suggested change
// Address defines the address of the staking compile contract.
// Address defines the address of the p256 compile contract.

// address: 0x0000000000000000000000000000000000000013
func (Precompile) Address() common.Address {
return common.BytesToAddress([]byte{19})
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don't we use the address as a const instead of passing []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) {
fedekunze marked this conversation as resolved.
Show resolved Hide resolved
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

fedekunze marked this conversation as resolved.
Show resolved Hide resolved
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
}