Skip to content
This repository has been archived by the owner on Oct 7, 2020. It is now read-only.

R4R: CheckTx AnteHandler for Ethereum Tx Messages #505

Merged
merged 33 commits into from
Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
862f32a
Remove remnants of old sdk ante handler logic
Nov 29, 2018
8b884f7
Update SDK and TM deps
Nov 29, 2018
ef52d37
Update sequence and account number types
Nov 29, 2018
e64d448
Add CodeInvalidChainID
Nov 29, 2018
7a70d3c
Update TestMsgEthereumTxSig
Nov 29, 2018
aef5bb2
Rename Ethereum tx message
Nov 30, 2018
801f28d
Use new tx decoder in the ethermint app
Nov 30, 2018
f313c7c
Update app tests APIs
Nov 30, 2018
480d4bb
Update ante handler to prevent spam/dos
Nov 30, 2018
f4c49c3
Update ethereum msg signing/verification logic
Dec 3, 2018
2b34e2b
Update SDK to revision with auth params
Dec 6, 2018
f56010f
Update app test utils
Dec 6, 2018
92716ed
Update account keeper creation in app init
Dec 6, 2018
b37671f
Implement secp256k1 key types
Dec 6, 2018
d728333
Register crypto codec in the app
Dec 6, 2018
9234d51
Update the evm types and utils
Dec 6, 2018
db4cb00
Update app test utils
Dec 6, 2018
f25653e
Wrap up ante handler and unit tests
Dec 7, 2018
5b151eb
Fix importer
Dec 7, 2018
a65e270
Fix linting
Dec 7, 2018
1cebcf2
Remove debug require statement
Dec 7, 2018
025233b
Remove pointer from To method
Dec 7, 2018
db6d2dd
Move sig check to after inartistic gas check
Dec 14, 2018
3ba2eb1
Add comment on chainID parsing
Dec 14, 2018
a75a79d
Updated validateIntrinsicGas godoc
Dec 14, 2018
dd9b05e
Implement Fee method on eth tx msg
Dec 17, 2018
4dc71b0
Add reference to spec for recoverEthSig
Dec 17, 2018
1e89c29
Upgrade TM to v0.27.0
Dec 17, 2018
8c8d2c0
Update SDK revision and ante handler
Dec 17, 2018
725f6d2
Fix importer
Dec 17, 2018
0a40f38
Update SDK version to latest develop revision
Dec 18, 2018
ca5df89
Add note on min fees denom
Dec 18, 2018
1b2fca7
Update nonce validation
Dec 18, 2018
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
22 changes: 11 additions & 11 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

[[constraint]]
name = "github.com/cosmos/cosmos-sdk"
# TODO: Replace this with 0.27
revision = "1ea0e4c457fc105b48131a60e3d28c6c1bb32cc0"
revision = "75fbce8e854c888063cab7e22930e2a6e382880f"
# version = "v0.27.0"
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved

[[constraint]]
name = "github.com/hashicorp/golang-lru"
Expand Down Expand Up @@ -40,20 +40,20 @@

[[override]]
name = "github.com/tendermint/go-amino"
version = "v0.14.0"

[[override]]
name = "github.com/tendermint/iavl"
version = "=v0.11.1"
version = "v0.14.1"

[[override]]
name = "golang.org/x/crypto"
source = "https://github.com/tendermint/crypto"
revision = "3764759f34a542a3aef74d6b02e35be7ab893bba"

[[override]]
name = "github.com/tendermint/iavl"
version = "v0.12.0"

[[override]]
name = "github.com/tendermint/tendermint"
version = "v0.26.1"
revision = "v0.27.0-dev1"
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved

[[override]]
name = "golang.org/x/sys"
Expand Down
175 changes: 128 additions & 47 deletions app/ante.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
package app

import (
"encoding/hex"
"fmt"
"math/big"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/tendermint/tendermint/crypto/secp256k1"

"github.com/cosmos/ethermint/types"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
)

var dummySecp256k1Pubkey secp256k1.PubKeySecp256k1 // used for tx simulation

const (
memoCostPerByte = 1
maxMemoCharacters = 100
secp256k1VerifyCost = 100
ethcmn "github.com/ethereum/go-ethereum/common"
ethcore "github.com/ethereum/go-ethereum/core"
)

func init() {
bz, _ := hex.DecodeString("035AD6810A47F073553FF30D2FCC7E0D3B1C0B74B61A1AAA2582344037151E143A")
copy(dummySecp256k1Pubkey[:], bz)
}

// NewAnteHandler returns an ante handelr responsible for attempting to route an
// NewAnteHandler returns an ante handler responsible for attempting to route an
// Ethereum or SDK transaction to an internal ante handler for performing
// transaction-level processing (e.g. fee payment, signature verification) before
// being passed onto it's respective handler.
Expand All @@ -35,57 +26,147 @@ func NewAnteHandler(ak auth.AccountKeeper, fck auth.FeeCollectionKeeper) sdk.Ant
ctx sdk.Context, tx sdk.Tx, sim bool,
) (newCtx sdk.Context, res sdk.Result, abort bool) {

stdTx, ok := tx.(auth.StdTx)
if !ok {
return ctx, sdk.ErrInternal("transaction type invalid: must be StdTx").Result(), true
}
switch castTx := tx.(type) {
case auth.StdTx:
return auth.NewAnteHandler(ak, fck)(ctx, castTx, sim)
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved

// TODO: Handle gas/fee checking and spam prevention. We may need two
// different models for SDK and Ethereum txs. The SDK currently supports a
// primitive model where a constant gas price is used.
//
// Ref: #473
case *evmtypes.EthereumTxMsg:
return ethAnteHandler(ctx, castTx, ak)

if ethTx, ok := isEthereumTx(stdTx); ethTx != nil && ok {
return ethAnteHandler(ctx, ethTx, ak)
default:
return ctx, sdk.ErrInternal(fmt.Sprintf("transaction type invalid: %T", tx)).Result(), true
}

return auth.NewAnteHandler(ak, fck)(ctx, stdTx, sim)
}
}

// ----------------------------------------------------------------------------
// Ethereum Ante Handler

// ethAnteHandler defines an internal ante handler for an Ethereum transaction
// ethTx that implements the sdk.Msg interface. The Ethereum transaction is a
// single message inside a auth.StdTx.
//
// For now we simply pass the transaction on as the EVM shares common business
// logic of an ante handler. Anything not handled by the EVM that should be
// prior to transaction processing, should be done here.
// ethTxMsg. During CheckTx, the transaction is passed through a series of
// pre-message execution validation checks such as signature and account
// verification in addition to minimum fees being checked. Otherwise, during
// DeliverTx, the transaction is simply passed to the EVM which will also
// perform the same series of checks. The distinction is made in CheckTx to
// prevent spam and DoS attacks.
func ethAnteHandler(
ctx sdk.Context, ethTx *evmtypes.MsgEthereumTx, ak auth.AccountKeeper,
ctx sdk.Context, ethTxMsg *evmtypes.EthereumTxMsg, ak auth.AccountKeeper,
) (newCtx sdk.Context, res sdk.Result, abort bool) {

if ctx.IsCheckTx() {
// Only perform pre-message (Ethereum transaction) execution validation
// during CheckTx. Otherwise, during DeliverTx the EVM will handle them.
if res := validateEthTxCheckTx(ctx, ak, ethTxMsg); !res.IsOK() {
return newCtx, res, true
}
}

return ctx, sdk.Result{}, false
}

// ----------------------------------------------------------------------------
// Auxiliary

// isEthereumTx returns a boolean if a given standard SDK transaction contains
// an Ethereum transaction. If so, the transaction is also returned. A standard
// SDK transaction contains an Ethereum transaction if it only has a single
// message and that embedded message if of type MsgEthereumTx.
func isEthereumTx(tx auth.StdTx) (*evmtypes.MsgEthereumTx, bool) {
msgs := tx.GetMsgs()
if len(msgs) == 1 {
ethTx, ok := msgs[0].(*evmtypes.MsgEthereumTx)
if ok {
return ethTx, true
}
func validateEthTxCheckTx(
ctx sdk.Context, ak auth.AccountKeeper, ethTxMsg *evmtypes.EthereumTxMsg,
) sdk.Result {

chainID, ok := new(big.Int).SetString(ctx.ChainID(), 10)
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
return types.ErrInvalidChainID(fmt.Sprintf("invalid chainID: %s", ctx.ChainID())).Result()
}

// Validate sufficient fees have been provided that meet a minimum threshold
// defined by the proposer (for mempool purposes during CheckTx).
if res := ensureSufficientMempoolFees(ctx, ethTxMsg); !res.IsOK() {
return res
}

// validate sender/signature
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
signer, err := ethTxMsg.VerifySig(chainID)
if err != nil {
return sdk.ErrUnauthorized("signature verification failed").Result()
}

// validate account (nonce and balance checks)
if res := validateAccount(ctx, ak, ethTxMsg, signer); !res.IsOK() {
return res
}

// validate enough intrinsic gas
if res := validateIntrinsicGas(ethTxMsg); !res.IsOK() {
return res
}

return sdk.Result{}
}

// validateIntrinsicGas validates that the Ethereum tx message has enough to
// cover intrinsic gas.
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
func validateIntrinsicGas(ethTxMsg *evmtypes.EthereumTxMsg) sdk.Result {
gas, err := ethcore.IntrinsicGas(ethTxMsg.Data.Payload, ethTxMsg.To() == nil, false)
if err != nil {
return sdk.ErrInternal(fmt.Sprintf("failed to compute intrinsic gas cost: %s", err)).Result()
}

if ethTxMsg.Data.GasLimit < gas {
return sdk.ErrInternal(
fmt.Sprintf("intrinsic gas too low; %d < %d", ethTxMsg.Data.GasLimit, gas),
).Result()
}

return sdk.Result{}
}

// validateAccount validates the account nonce and that the account has enough
// funds to cover the tx cost.
func validateAccount(
ctx sdk.Context, ak auth.AccountKeeper, ethTxMsg *evmtypes.EthereumTxMsg, signer ethcmn.Address,
) sdk.Result {

acc := ak.GetAccount(ctx, sdk.AccAddress(signer.Bytes()))

// on InitChain make sure account number == 0
if ctx.BlockHeight() == 0 && acc.GetAccountNumber() != 0 {
return sdk.ErrInternal(
fmt.Sprintf(
"invalid account number for height zero; got %d, expected 0", acc.GetAccountNumber(),
)).Result()
}

seq := acc.GetSequence()
if ethTxMsg.Data.AccountNonce < seq {
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
return sdk.ErrInvalidSequence(
fmt.Sprintf("nonce too low; got %d, expected >= %d", ethTxMsg.Data.AccountNonce, seq)).Result()
}

// validate sender has enough funds
balance := acc.GetCoins().AmountOf(types.DenomDefault)
if balance.BigInt().Cmp(ethTxMsg.Cost()) < 0 {
return sdk.ErrInsufficientFunds(
fmt.Sprintf("insufficient funds: %s < %s", balance, ethTxMsg.Cost()),
).Result()
}

return sdk.Result{}
}

// ensureSufficientMempoolFees verifies that enough fees have been provided by the
// Ethereum transaction that meet the minimum threshold set by the block
// proposer.
//
// NOTE: This should only be ran during a CheckTx mode.
func ensureSufficientMempoolFees(ctx sdk.Context, ethTxMsg *evmtypes.EthereumTxMsg) sdk.Result {
// fee = GP * GL
feeAmt := new(big.Int).Mul(ethTxMsg.Data.Price, new(big.Int).SetUint64(ethTxMsg.Data.GasLimit))
fee := sdk.Coins{sdk.NewInt64Coin(types.DenomDefault, feeAmt.Int64())}
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved

if !ctx.MinimumFees().IsZero() && !fee.IsAllGTE(ctx.MinimumFees()) {
// reject the transaction that does not meet the minimum fee
return sdk.ErrInsufficientFee(
fmt.Sprintf("insufficient fee, got: %q required: %q", fee, ctx.MinimumFees()),
).Result()
}

return nil, false
return sdk.Result{}
}
Loading