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

test(evm): unit tests for evm_ante #1912

Merged
merged 16 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#1901](https://github.com/NibiruChain/nibiru/pull/1901) - test(evm): more e2e test contracts for edge cases
- [#1907](https://github.com/NibiruChain/nibiru/pull/1907) - test(evm): grpc_query full coverage
- [#1909](https://github.com/NibiruChain/nibiru/pull/1909) - chore(evm): set is_london true by default and removed from config
- [#1912](https://github.com/NibiruChain/nibiru/pull/1912) - test(evm): unit tests for evm_ante_sigverify

#### Dapp modules: perp, spot, oracle, etc

Expand Down
49 changes: 49 additions & 0 deletions app/evmante_emit_event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2023-2024 Nibi, Inc.
package app

import (
"strconv"

errorsmod "cosmossdk.io/errors"
"github.com/NibiruChain/nibiru/x/evm"
sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
)

// EthEmitEventDecorator emit events in ante handler in case of tx execution failed (out of block gas limit).
type EthEmitEventDecorator struct {
AppKeepers
}

// NewEthEmitEventDecorator creates a new EthEmitEventDecorator
func NewEthEmitEventDecorator(k AppKeepers) EthEmitEventDecorator {
return EthEmitEventDecorator{AppKeepers: k}

Check warning on line 20 in app/evmante_emit_event.go

View check run for this annotation

Codecov / codecov/patch

app/evmante_emit_event.go#L19-L20

Added lines #L19 - L20 were not covered by tests
}

// AnteHandle emits some basic events for the eth messages
func (eeed EthEmitEventDecorator) AnteHandle(
ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler,
) (newCtx sdk.Context, err error) {

Check warning on line 26 in app/evmante_emit_event.go

View check run for this annotation

Codecov / codecov/patch

app/evmante_emit_event.go#L26

Added line #L26 was not covered by tests
// After eth tx passed ante handler, the fee is deducted and nonce increased,
// it shouldn't be ignored by json-rpc. We need to emit some events at the
// very end of ante handler to be indexed by the consensus engine.
txIndex := eeed.EvmKeeper.EVMState().BlockTxIndex.GetOr(ctx, 0)

Check warning on line 30 in app/evmante_emit_event.go

View check run for this annotation

Codecov / codecov/patch

app/evmante_emit_event.go#L30

Added line #L30 was not covered by tests

for i, msg := range tx.GetMsgs() {
msgEthTx, ok := msg.(*evm.MsgEthereumTx)
if !ok {
return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil))

Check warning on line 35 in app/evmante_emit_event.go

View check run for this annotation

Codecov / codecov/patch

app/evmante_emit_event.go#L32-L35

Added lines #L32 - L35 were not covered by tests
}

// emit ethereum tx hash as an event so that it can be indexed by
// Tendermint for query purposes it's emitted in ante handler, so we can
// query failed transaction (out of block gas limit).
ctx.EventManager().EmitEvent(sdk.NewEvent(
evm.EventTypeEthereumTx,
sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, msgEthTx.Hash),
sdk.NewAttribute(evm.AttributeKeyTxIndex, strconv.FormatUint(txIndex+uint64(i), 10)), // #nosec G701
))

Check warning on line 45 in app/evmante_emit_event.go

View check run for this annotation

Codecov / codecov/patch

app/evmante_emit_event.go#L41-L45

Added lines #L41 - L45 were not covered by tests
}

return next(ctx, tx, simulate)

Check warning on line 48 in app/evmante_emit_event.go

View check run for this annotation

Codecov / codecov/patch

app/evmante_emit_event.go#L48

Added line #L48 was not covered by tests
}
File renamed without changes.
106 changes: 106 additions & 0 deletions app/evmante_gas_wanted_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package app_test

import (
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx"

"github.com/NibiruChain/nibiru/app"
"github.com/NibiruChain/nibiru/x/evm/evmtest"
)

func (s *TestSuite) TestGasWantedDecorator() {
testCases := []struct {
name string
ctxSetup func(deps *evmtest.TestDeps)
txSetup func(deps *evmtest.TestDeps) sdk.Tx
wantErr string
}{
{
name: "happy: non fee tx type",
txSetup: func(deps *evmtest.TestDeps) sdk.Tx {
return happyCreateContractTx(deps)
},
wantErr: "",
},
{
name: "happy: tx without gas, block gas limit 1000",
ctxSetup: func(deps *evmtest.TestDeps) {
cp := &tmproto.ConsensusParams{
Block: &tmproto.BlockParams{MaxGas: 1000},
}
deps.Ctx = deps.Ctx.WithConsensusParams(cp)
},
txSetup: func(deps *evmtest.TestDeps) sdk.Tx {
return legacytx.StdTx{
Msgs: []sdk.Msg{
happyCreateContractTx(deps),
},
}
},
wantErr: "",
},
{
name: "happy: tx with gas wanted 500, block gas limit 1000",
ctxSetup: func(deps *evmtest.TestDeps) {
cp := &tmproto.ConsensusParams{
Block: &tmproto.BlockParams{MaxGas: 1000},
}
deps.Ctx = deps.Ctx.WithConsensusParams(cp)
},
txSetup: func(deps *evmtest.TestDeps) sdk.Tx {
return legacytx.StdTx{
Msgs: []sdk.Msg{
happyCreateContractTx(deps),
},
Fee: legacytx.StdFee{Gas: 500},
}
},
wantErr: "",
},
{
name: "sad: tx with gas wanted 1000, block gas limit 500",
ctxSetup: func(deps *evmtest.TestDeps) {
cp := &tmproto.ConsensusParams{
Block: &tmproto.BlockParams{
MaxGas: 500,
},
}
deps.Ctx = deps.Ctx.WithConsensusParams(cp)
},
txSetup: func(deps *evmtest.TestDeps) sdk.Tx {
return legacytx.StdTx{
Msgs: []sdk.Msg{
happyCreateContractTx(deps),
},
Fee: legacytx.StdFee{Gas: 1000},
}
},
wantErr: "exceeds block gas limit",
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
deps := evmtest.NewTestDeps()
stateDB := deps.StateDB()
anteDec := app.NewGasWantedDecorator(deps.Chain.AppKeepers)

tx := tc.txSetup(&deps)
s.Require().NoError(stateDB.Commit())

deps.Ctx = deps.Ctx.WithIsCheckTx(true)
if tc.ctxSetup != nil {
tc.ctxSetup(&deps)
}
_, err := anteDec.AnteHandle(
deps.Ctx, tx, false, NextNoOpAnteHandler,
)
if tc.wantErr != "" {
s.Require().ErrorContains(err, tc.wantErr)
return
}
s.Require().NoError(err)
})
}
}
159 changes: 4 additions & 155 deletions app/evmante_setup_ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,11 @@
package app

import (
"errors"
"strconv"

errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
gethcore "github.com/ethereum/go-ethereum/core/types"

"github.com/NibiruChain/nibiru/x/evm"
)

// EthSetupContextDecorator is adapted from SetUpContextDecorator from cosmos-sdk, it ignores gas consumption
Expand All @@ -37,7 +30,10 @@
// all transactions must implement GasTx
_, ok := tx.(authante.GasTx)
if !ok {
return ctx, errorsmod.Wrapf(errortypes.ErrInvalidType, "invalid transaction type %T, expected GasTx", tx)
return ctx, errorsmod.Wrapf(
errortypes.ErrInvalidType,
"invalid transaction type %T, expected GasTx", tx,
)

Check warning on line 36 in app/evmante_setup_ctx.go

View check run for this annotation

Codecov / codecov/patch

app/evmante_setup_ctx.go#L33-L36

Added lines #L33 - L36 were not covered by tests
}

// We need to setup an empty gas config so that the gas is consistent with Ethereum.
Expand All @@ -50,150 +46,3 @@
esc.EvmKeeper.ResetTransientGasUsed(ctx)
return next(newCtx, tx, simulate)
}

// EthEmitEventDecorator emit events in ante handler in case of tx execution failed (out of block gas limit).
type EthEmitEventDecorator struct {
AppKeepers
}

// NewEthEmitEventDecorator creates a new EthEmitEventDecorator
func NewEthEmitEventDecorator(k AppKeepers) EthEmitEventDecorator {
return EthEmitEventDecorator{AppKeepers: k}
}

// AnteHandle emits some basic events for the eth messages
func (eeed EthEmitEventDecorator) AnteHandle(
ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler,
) (newCtx sdk.Context, err error) {
// After eth tx passed ante handler, the fee is deducted and nonce increased,
// it shouldn't be ignored by json-rpc. We need to emit some events at the
// very end of ante handler to be indexed by the consensus engine.
txIndex := eeed.EvmKeeper.EVMState().BlockTxIndex.GetOr(ctx, 0)

for i, msg := range tx.GetMsgs() {
msgEthTx, ok := msg.(*evm.MsgEthereumTx)
if !ok {
return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil))
}

// emit ethereum tx hash as an event so that it can be indexed by
// Tendermint for query purposes it's emitted in ante handler, so we can
// query failed transaction (out of block gas limit).
ctx.EventManager().EmitEvent(sdk.NewEvent(
evm.EventTypeEthereumTx,
sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, msgEthTx.Hash),
sdk.NewAttribute(evm.AttributeKeyTxIndex, strconv.FormatUint(txIndex+uint64(i), 10)), // #nosec G701
))
}

return next(ctx, tx, simulate)
}

// EthValidateBasicDecorator is adapted from ValidateBasicDecorator from cosmos-sdk, it ignores ErrNoSignatures
type EthValidateBasicDecorator struct {
AppKeepers
}

// NewEthValidateBasicDecorator creates a new EthValidateBasicDecorator
func NewEthValidateBasicDecorator(k AppKeepers) EthValidateBasicDecorator {
return EthValidateBasicDecorator{
AppKeepers: k,
}
}

// AnteHandle handles basic validation of tx
func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
// no need to validate basic on recheck tx, call next antehandler
if ctx.IsReCheckTx() {
return next(ctx, tx, simulate)
}

err := tx.ValidateBasic()
// ErrNoSignatures is fine with eth tx
if err != nil && !errors.Is(err, errortypes.ErrNoSignatures) {
return ctx, errorsmod.Wrap(err, "tx basic validation failed")
}

// For eth type cosmos tx, some fields should be verified as zero values,
// since we will only verify the signature against the hash of the MsgEthereumTx.Data
wrapperTx, ok := tx.(protoTxProvider)
if !ok {
return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid tx type %T, didn't implement interface protoTxProvider", tx)
}

protoTx := wrapperTx.GetProtoTx()
body := protoTx.Body
if body.Memo != "" || body.TimeoutHeight != uint64(0) || len(body.NonCriticalExtensionOptions) > 0 {
return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest,
"for eth tx body Memo TimeoutHeight NonCriticalExtensionOptions should be empty")
}

if len(body.ExtensionOptions) != 1 {
return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx length of ExtensionOptions should be 1")
}

authInfo := protoTx.AuthInfo
if len(authInfo.SignerInfos) > 0 {
return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo SignerInfos should be empty")
}

if authInfo.Fee.Payer != "" || authInfo.Fee.Granter != "" {
return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo Fee payer and granter should be empty")
}

sigs := protoTx.Signatures
if len(sigs) > 0 {
return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx Signatures should be empty")
}

txFee := sdk.Coins{}
txGasLimit := uint64(0)

evmParams := vbd.EvmKeeper.GetParams(ctx)
baseFee := vbd.EvmKeeper.GetBaseFee(ctx)
enableCreate := evmParams.GetEnableCreate()
enableCall := evmParams.GetEnableCall()
evmDenom := evmParams.GetEvmDenom()

for _, msg := range protoTx.GetMsgs() {
msgEthTx, ok := msg.(*evm.MsgEthereumTx)
if !ok {
return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil))
}

// Validate `From` field
if msgEthTx.From != "" {
return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid From %s, expect empty string", msgEthTx.From)
}

txGasLimit += msgEthTx.GetGas()

txData, err := evm.UnpackTxData(msgEthTx.Data)
if err != nil {
return ctx, errorsmod.Wrap(err, "failed to unpack MsgEthereumTx Data")
}

// return error if contract creation or call are disabled through governance
if !enableCreate && txData.GetTo() == nil {
return ctx, errorsmod.Wrap(evm.ErrCreateDisabled, "failed to create new contract")
} else if !enableCall && txData.GetTo() != nil {
return ctx, errorsmod.Wrap(evm.ErrCallDisabled, "failed to call contract")
}

if baseFee == nil && txData.TxType() == gethcore.DynamicFeeTxType {
return ctx, errorsmod.Wrap(gethcore.ErrTxTypeNotSupported, "dynamic fee tx not supported")
}

txFee = txFee.Add(sdk.Coin{Denom: evmDenom, Amount: sdkmath.NewIntFromBigInt(txData.Fee())})
}

if !authInfo.Fee.Amount.IsEqual(txFee) {
return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee Amount (%s != %s)", authInfo.Fee.Amount, txFee)
}

if authInfo.Fee.GasLimit != txGasLimit {
return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee GasLimit (%d != %d)", authInfo.Fee.GasLimit, txGasLimit)
}

return next(ctx, tx, simulate)
}
42 changes: 42 additions & 0 deletions app/evmante_setup_ctx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package app_test

import (
"math"

"github.com/NibiruChain/nibiru/app"
"github.com/NibiruChain/nibiru/x/evm/evmtest"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)

func (s *TestSuite) TestEthSetupContextDecorator() {
deps := evmtest.NewTestDeps()
stateDB := deps.StateDB()
anteDec := app.NewEthSetUpContextDecorator(deps.Chain.AppKeepers)

s.Require().NoError(stateDB.Commit())
tx := happyCreateContractTx(&deps)

// Set block gas used to non 0 to check that handler resets it
anteDec.EvmKeeper.EvmState.BlockGasUsed.Set(deps.Ctx, 1000)

// Ante handler returns new context
newCtx, err := anteDec.AnteHandle(
deps.Ctx, tx, false, NextNoOpAnteHandler,
)
s.Require().NoError(err)

// Check that ctx gas meter is set up to infinite
ctxGasMeter := newCtx.GasMeter()
s.Require().Equal(sdk.Gas(math.MaxUint64), ctxGasMeter.GasRemaining())

// Check that gas configs are reset to default
defaultGasConfig := storetypes.GasConfig{}
s.Require().Equal(defaultGasConfig, newCtx.KVGasConfig())
s.Require().Equal(defaultGasConfig, newCtx.TransientKVGasConfig())

// Check that block gas used is reset to 0
gas, err := anteDec.EvmKeeper.EvmState.BlockGasUsed.Get(newCtx)
s.Require().NoError(err)
s.Require().Equal(gas, uint64(0))
}
Loading
Loading