feat!(precompile): Add registry and genesis tests (#1999)

* feat!(precompile): Add registry and genesis tests

Based on evgeniy-scherbina's work, this adds a new precompile module
which defines a contract moudule with an example noop contract that
will be will be used for implementing test functions.  In addition,
it defines a registry module that instantiates stateful precompile
contracts and associates them with an address in a global registry
defined in kava-labs/go-ethereum. See precompile/README.md for more
information.

The kava-labs/go-ethereum and kava-labs/etheremint replace statements
are updated to support these changes as well as an update to kvtool
which includes genesis state for the registry.NoopContractAddress and
initializes the contract's EthAccount with a non-zero sequence and
codehash set to keccak256(0x01), and sets the contract code to 0x01.
See tests/e2e/e2e_precompile_genesis_test.go for an overview of the
expected genesis state for an enabled precompile.

Co-authored-by: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com>

* chore: Precompile readme improvements

This fixes a typo (import -> important) and uses package terminology
instead of unclear module terminology.  This aligns best with golang
terminology were modules and packages are distinctly different and
modules are defined using go.mod.

* chore: Improve noop contract godoc

Add a more meaningful godoc where the noop contract is constructed.

* chore(e2e): Improve comments around query checks

Improve the clarity of comments around where the error is checked for
accounts and why it is not checked directly.

In addition, improve comment on why both grpc and rpc code is fetched
and where they are used.

---------

Co-authored-by: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com>
This commit is contained in:
Nick DeLuca 2024-08-09 09:55:31 -07:00 committed by GitHub
parent 33932e8ad6
commit ab3cf7c994
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 344 additions and 7 deletions

View File

@ -165,6 +165,7 @@ func (suite *EIP712TestSuite) SetupTest() {
evmtypes.DefaultChainConfig(), // ChainConfig
nil, // extraEIPs
nil, // eip712AllowedMsgs
nil, // enabledPrecompiles
),
nil,
)

View File

@ -107,6 +107,7 @@ import (
"github.com/kava-labs/kava/app/ante"
kavaparams "github.com/kava-labs/kava/app/params"
_ "github.com/kava-labs/kava/precompile/registry" // Ensure precompiles are registered when using the app module
"github.com/kava-labs/kava/x/auction"
auctionkeeper "github.com/kava-labs/kava/x/auction/keeper"
auctiontypes "github.com/kava-labs/kava/x/auction/types"

View File

@ -19,6 +19,7 @@ import (
vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
solomachine "github.com/cosmos/ibc-go/v7/modules/light-clients/06-solomachine"
ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint"
"github.com/ethereum/go-ethereum/precompile/modules"
evmtypes "github.com/evmos/ethermint/x/evm/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -119,6 +120,16 @@ func TestLegacyMsgAreAminoRegistered(t *testing.T) {
}
}
// TestPrecompilesAreRegistered asserts that we have loaded the global precompile registry
// by checking if at least one precompile is set
//
// If this test fails then has '_ "github.com/kava-labs/kava/precompile/registry"' been imported?
func TestPrecompilesAreRegistered(t *testing.T) {
assert.Greater(t, len(modules.RegisteredModules()), 0,
"expected precompile registry to be imported and have at least one registered precompile",
)
}
// catchPanic returns the panic value of the passed function. The second return indicates if the function panicked.
func catchPanic(f func()) (panicValue interface{}, didPanic bool) {
didPanic = true

8
go.mod
View File

@ -234,9 +234,13 @@ replace (
github.com/cosmos/cosmos-sdk => github.com/kava-labs/cosmos-sdk v0.47.10-iavl-v1-kava.1
// See https://github.com/cosmos/cosmos-sdk/pull/13093
github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2
github.com/ethereum/go-ethereum => github.com/Kava-Labs/go-ethereum v1.10.27-0.20240308170502-da7973e5eee0
// Tracking kava-labs/go-ethereum kava/release/v1.10 branch
// TODO: Tag before release
github.com/ethereum/go-ethereum => github.com/Kava-Labs/go-ethereum v1.10.27-0.20240513233504-6e038346780b
// Use ethermint fork that respects min-gas-price with NoBaseFee true and london enabled, and includes eip712 support
github.com/evmos/ethermint => github.com/kava-labs/ethermint v0.21.1-0.20240719175058-d18ee9bd0f84
// Tracking kava-labs/etheremint master branch
// TODO: Tag before release
github.com/evmos/ethermint => github.com/kava-labs/ethermint v0.21.1-0.20240802224012-586960857184
// See https://github.com/cosmos/cosmos-sdk/pull/10401, https://github.com/cosmos/cosmos-sdk/commit/0592ba6158cd0bf49d894be1cef4faeec59e8320
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.9.0
// Downgraded to avoid bugs in following commits which causes "version does not exist" errors

8
go.sum
View File

@ -225,8 +225,8 @@ github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3
github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Kava-Labs/go-ethereum v1.10.27-0.20240308170502-da7973e5eee0 h1:pPFzOjEZmihLk70TQRPUCWs8uar6nfh4vZ/I1r0zeso=
github.com/Kava-Labs/go-ethereum v1.10.27-0.20240308170502-da7973e5eee0/go.mod h1:/6CsT5Ceen2WPLI/oCA3xMcZ5sWMF/D46SjM/ayY0Oo=
github.com/Kava-Labs/go-ethereum v1.10.27-0.20240513233504-6e038346780b h1:gTLS1VJL+6TEcttEmQ9rAmm/UZZw23WlQf6n8R/Xjx0=
github.com/Kava-Labs/go-ethereum v1.10.27-0.20240513233504-6e038346780b/go.mod h1:tvRm5KYJQ6LT+uss34spIP1oi6JgCfsFYiMKwJLZr6M=
github.com/Kava-Labs/opendb v0.0.0-20240719173129-a2f11f6d7e51 h1:tMTENCeSPIJO8yCpEQbT15XYXt4EFNQUx3s334uxVts=
github.com/Kava-Labs/opendb v0.0.0-20240719173129-a2f11f6d7e51/go.mod h1:LbPsJiWvj90NT3Y9YV8EFPkWfvp8A15Tp88qqKa3LxA=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@ -894,8 +894,8 @@ github.com/kava-labs/cometbft-db v0.9.1-kava.2 h1:ZQaio886ifvml9XtJB4IYHhlArgA3+
github.com/kava-labs/cometbft-db v0.9.1-kava.2/go.mod h1:PvUZbx7zeR7I4CAvtKBoii/5ia5gXskKjDjIVpt7gDw=
github.com/kava-labs/cosmos-sdk v0.47.10-iavl-v1-kava.1 h1:vQwrm3sdAG1pkwrsi2mmCHSGDje5fzUR6vApEux/nVA=
github.com/kava-labs/cosmos-sdk v0.47.10-iavl-v1-kava.1/go.mod h1:OwLYEBcsnijCLE8gYkwQ7jycZZ/Acd+a83pJU+V+MKw=
github.com/kava-labs/ethermint v0.21.1-0.20240719175058-d18ee9bd0f84 h1:kAoQaPVKpEIG8t6+F3crawO7vgJIGlUy68XayjkYz8A=
github.com/kava-labs/ethermint v0.21.1-0.20240719175058-d18ee9bd0f84/go.mod h1:kbyr3La2Co3Hy3U3N2EvVk7W1srQ2x88JUpgsu2KrXo=
github.com/kava-labs/ethermint v0.21.1-0.20240802224012-586960857184 h1:MWwCXFnkagXk93QiiD41I+S9wyrHZUQWLRFKo2tXL6A=
github.com/kava-labs/ethermint v0.21.1-0.20240802224012-586960857184/go.mod h1:kbyr3La2Co3Hy3U3N2EvVk7W1srQ2x88JUpgsu2KrXo=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=

27
precompile/README.md Normal file
View File

@ -0,0 +1,27 @@
# Precompile
The `precompile` package defines stateful precompile contracts used for creating precompile contracts that enhance the EVM, enable interactions between cosmos state and EVM state, or are used soley for testing purposes.
This package is made of two subpackages:
- `contracts` - Defines stateful precompiles and their constructors.
- `registry` - Defines stateful precompile addresses and registers them with the global registry
defined at `github.com/kava-labs/go-ethereum/precompile/modules`.
This is architected to isolate the dependency on the global registry (`github.com/kava-labs/go-ethereum/precompile/modules` package) to as few places as possible, have one source of truth for address registration (see `./registry/registry.go`), and isolate registration testing to one package.
In order to use the precompile registry, it must be imported for it's init function to run and register the precompiles. For the kava app, this is done in the `app.go` file with the import `_ "github.com/kava-labs/kava/precompile/registry"`. This is done in the `app.go` since this is the main file used for app dependencies and so all modules using the app cause the registry to be loaded. This is important for any consumers of the app outside of the kava cmd, as well as test code using the app for integration and unit testing.
## Defining a new precompile
1) Add the expected 0x address to the expected list in `./registry/registry_test.go`.
2) Create a new sub-directory under `./contracts` with a `contract_test.go` and `contract.go` file.
3) Implement `NewContract` function with associated tests in contract and contract test files.
4) Add the contract registration to `./registry/registry.go`.
## Contracts
### Noop
This contract is used for testing purposes only and should not be used on public chains. The functions of this contract (once implemented), will be used to exercise and test the various aspects of the EVM such as gas usage, argument parsing, events, etc. The specific operations tested under this contract are still to be determined.

View File

@ -0,0 +1,23 @@
package noop
import (
"fmt"
"github.com/ethereum/go-ethereum/precompile/contract"
)
// NewContract returns a new noop stateful precompiled contract.
//
// This contract is used for testing purposes only and should not be used on public chains.
// The functions of this contract (once implemented), will be used to exercise and test the various aspects of
// the EVM such as gas usage, argument parsing, events, etc. The specific operations tested under this contract are
// still to be determined.
func NewContract() (contract.StatefulPrecompiledContract, error) {
precompile, err := contract.NewStatefulPrecompileContract([]*contract.StatefulPrecompileFunction{})
if err != nil {
return nil, fmt.Errorf("failed to instantiate noop precompile: %w", err)
}
return precompile, nil
}

View File

@ -0,0 +1,18 @@
package noop_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/kava-labs/kava/precompile/contracts/noop"
)
// TestContractConstructor ensures we have a valid constructor. This will fail
// if we attempt to define invalid or duplicate function selectors.
func TestContractConstructor(t *testing.T) {
precompile, err := noop.NewContract()
require.NoError(t, err, "expected precompile not error when created")
assert.NotNil(t, precompile, "expected precompile contract to be defined")
}

View File

@ -0,0 +1,46 @@
package registry
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/precompile/contract"
"github.com/ethereum/go-ethereum/precompile/modules"
"github.com/kava-labs/kava/precompile/contracts/noop"
)
const (
// NoopContractAddress the primary noop contract address for testing
NoopContractAddress = "0x9000000000000000000000000000000000000001"
// NoopContractAddress2 the secondary noop contract address for testing
NoopContractAddress2 = "0x9000000000000000000000000000000000000002"
)
// init registers stateful precompile contracts with the global precompile registry
// defined in kava-labs/go-ethereum/precompile/modules
func init() {
register(NoopContractAddress, noop.NewContract)
register(NoopContractAddress2, noop.NewContract)
}
// register accepts a 0x address string and a stateful precompile contract constructor, instantiates the
// precompile contract via the constructor, and registers it with the precompile module registry.
//
// This panics if the contract can not be created or the module can not be registered
func register(address string, newContract func() (contract.StatefulPrecompiledContract, error)) {
contract, err := newContract()
if err != nil {
panic(fmt.Errorf("error creating contract for address %s: %w", address, err))
}
module := modules.Module{
Address: common.HexToAddress(address),
Contract: contract,
}
err = modules.RegisterModule(module)
if err != nil {
panic(fmt.Errorf("error registering contract module for address %s: %w", address, err))
}
}

View File

@ -0,0 +1,33 @@
package registry_test
import (
"testing"
"github.com/ethereum/go-ethereum/precompile/modules"
"github.com/stretchr/testify/assert"
)
// TestRegisteredPrecompiles asserts precompiles are registered
//
// In addition, this serves as an integration test to
// 1. Ensure modules.RegisteredModules() is returning addresses in the correct ascending order
// 2. Ensure that that the address defined in the module is correct. Since we use common.HexToAddress and
// then back to 0x encoded string, we can be certain that the string defined in the module is the
// expected length, not missing 0's, etc.
func TestRegisteredPrecompilesAddresses(t *testing.T) {
// build list of 0x addresses that are registered
registeredModules := modules.RegisteredModules()
registeredPrecompiles := make([]string, 0, len(registeredModules))
for _, rp := range registeredModules {
registeredPrecompiles = append(registeredPrecompiles, rp.Address.String())
}
expectedPrecompiles := []string{
// 0x9 address space used for e2e & integration tests
"0x9000000000000000000000000000000000000001", // noop
"0x9000000000000000000000000000000000000002", // noop (duplicated for testing)
}
assert.Equal(t, expectedPrecompiles, registeredPrecompiles,
"expected registered precompile address list to match to match expected")
}

View File

@ -0,0 +1,173 @@
package e2e_test
import (
"context"
"math/big"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
ethermint "github.com/evmos/ethermint/types"
evmtypes "github.com/evmos/ethermint/x/evm/types"
"github.com/kava-labs/kava/precompile/registry"
)
// TestPrecompileGenesis tests that the the following is true for enabled precompiles:
//
// - An enabled precompile has an EthAccount with matching code hash,
// sequence of 1, and no public key
// - An enabled precompile has code equal to 0x01
// - An enabled precompile has a nonce of 1
//
// This is important to ensure the genesis setup for precompiles is correct.
func (suite *IntegrationTestSuite) TestPrecompileGenesis() {
type fixture struct {
address string
expectIsEnabled bool
expectIsEthAccount bool
expectCode []byte
expectNonce uint64
}
// enabled represnets the expected state for a registered precompile
// that is enabled at genesis
enabled := func(address string) func() fixture {
return func() fixture {
return fixture{
address: address,
expectIsEnabled: true,
expectIsEthAccount: true,
expectCode: []byte{0x01},
expectNonce: uint64(1),
}
}
}
// disabled represents the expected state for a registered precompile
// that is not enabled at genesis
disabled := func(address string) func() fixture {
return func() fixture {
return fixture{
address: address,
expectIsEnabled: false,
expectIsEthAccount: false,
expectCode: []byte{},
expectNonce: uint64(0),
}
}
}
testCases := []struct {
name string
genFixture func() fixture
}{
{
name: "noop contract address is enabled and initialized",
genFixture: enabled(registry.NoopContractAddress),
},
{
name: "noop contract address second address is disabled and not initialized",
genFixture: disabled(registry.NoopContractAddress2),
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
tf := tc.genFixture()
//
// Addresses
//
evmAddress := common.HexToAddress(tf.address)
sdkAddress := sdk.AccAddress(evmAddress.Bytes())
//
// Heights
//
// We ensure all queries happen at block 1 after genesis
// and help ensure determisitc behavior
genesisHeight := big.NewInt(1)
grpcGenesisContext := suite.Kava.Grpc.CtxAtHeight(genesisHeight.Int64())
//
// Queries
//
evmParamsResp, err := suite.Kava.Grpc.Query.Evm.Params(grpcGenesisContext, &evmtypes.QueryParamsRequest{})
suite.Require().NoError(err)
// accountErr is checked during in the assertions below if tf.expectIsEthAccount is true
// This is due to the service returning a not found error for address that are not enabled,
// which can be ignored when we do not expect an eth account to exist.
accountResponse, accountErr := suite.Kava.Grpc.Query.Auth.Account(
grpcGenesisContext, &authtypes.QueryAccountRequest{Address: sdkAddress.String()})
var account authtypes.AccountI
if accountErr == nil {
err = suite.Kava.EncodingConfig.Marshaler.UnpackAny(accountResponse.Account, &account)
suite.Require().NoError(err)
}
// We ensure both the evm json rpc and x/evm grpc code endpoints return the same value
// in the assertions below
grpcCodeResponse, err := suite.Kava.Grpc.Query.Evm.Code(grpcGenesisContext,
&evmtypes.QueryCodeRequest{Address: evmAddress.String()})
suite.Require().NoError(err)
rpcCode, err := suite.Kava.EvmClient.CodeAt(context.Background(), evmAddress, genesisHeight)
suite.Require().NoError(err)
nonce, err := suite.Kava.EvmClient.NonceAt(context.Background(), evmAddress, genesisHeight)
suite.Require().NoError(err)
//
// Assertions
//
if tf.expectIsEnabled {
suite.Containsf(evmParamsResp.Params.EnabledPrecompiles, tf.address,
"expected %s to be enabled in evm params", tf.address)
} else {
suite.NotContainsf(evmParamsResp.Params.EnabledPrecompiles, tf.address,
"expected %s to not be enabled in evm params", tf.address)
}
// Fail if fixuture configuration is invalid
if tf.expectIsEthAccount && len(tf.expectCode) == 0 {
suite.Failf("an eth account must have expected code for address %s", tf.address)
}
// Run addition EthAccount assertions
if tf.expectIsEthAccount {
suite.Require().NoErrorf(accountErr, "expected account query to not error for address %s", tf.address)
// All contracts including precompiles must be EthAccount's
ethAccount, isEthAccount := account.(*ethermint.EthAccount)
suite.Require().Truef(isEthAccount, "expected account at address %s to be an eth account", tf.address)
// Code hash must always match the EthAccount
codeHash := ethAccount.GetCodeHash()
suite.Equalf(crypto.Keccak256Hash(tf.expectCode), codeHash,
"expected codehash for account %s to match expected code", tf.address)
// A precompile (and contract) should never have a public key set
suite.Nilf(ethAccount.PubKey, "expected account %s to have no public key", tf.address)
// Assert the account sequence matches the expected nonce
// This a duplicate of the nonce assertion below, but also helps ensure these
// two sources agree
suite.Equal(tf.expectNonce, ethAccount.GetSequence())
}
// We assert both methods of code retrieval report the same value
suite.Equalf(tf.expectCode, rpcCode, "expected code for address %s to match expected", tf.address)
// The GRPC endpoint returns []byte(nil) when the code is not in state, which is different from
// the rpc endpoint that returns []byte{}.
grpcExpectedCode := tf.expectCode
if len(grpcExpectedCode) == 0 {
grpcExpectedCode = []byte(nil)
}
suite.Equalf(grpcExpectedCode, grpcCodeResponse.Code, "expected code for address %s to match expected", tf.address)
// We assert this outside of the account context since the evm rpc always returns a nonce
suite.Equalf(tf.expectNonce, nonce, "expected nonce for address %s to match expected", tf.address)
})
}
}

@ -1 +1 @@
Subproject commit d98dac4213b0e34ac41793e54c42731e395ab752
Subproject commit f78c74549f62d975d8de99c7bd158b3d92e5b145