From 8bb8b40b32654bd2d9ac62934d5062f6661406a7 Mon Sep 17 00:00:00 2001 From: Austin Abell Date: Thu, 3 Oct 2019 12:46:02 -0400 Subject: [PATCH] Emint tx type for eth_call and logs setup (#118) * Implement new tx message type for eth_call and module txs and abstracted state transition, prepared db for logs * Added transaction indexing to evm keeper * Alternative count type --- x/evm/handler.go | 120 ++++++++++++++------------------ x/evm/keeper.go | 18 ++++- x/evm/module.go | 1 + x/evm/types/codec.go | 2 + x/evm/types/emint_msg.go | 88 +++++++++++++++++++++++ x/evm/types/emint_msg_test.go | 76 ++++++++++++++++++++ x/evm/types/state_transition.go | 99 ++++++++++++++++++++++++++ x/evm/types/statedb.go | 3 +- 8 files changed, 337 insertions(+), 70 deletions(-) create mode 100644 x/evm/types/emint_msg.go create mode 100644 x/evm/types/emint_msg_test.go create mode 100644 x/evm/types/state_transition.go diff --git a/x/evm/handler.go b/x/evm/handler.go index 8eefba3eb..5782afcb6 100644 --- a/x/evm/handler.go +++ b/x/evm/handler.go @@ -3,15 +3,15 @@ package evm import ( "fmt" "math/big" - "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/vm" sdk "github.com/cosmos/cosmos-sdk/types" + authutils "github.com/cosmos/cosmos-sdk/x/auth/client/utils" emint "github.com/cosmos/ethermint/types" "github.com/cosmos/ethermint/x/evm/types" + + tm "github.com/tendermint/tendermint/types" ) // NewHandler returns a handler for Ethermint type messages. @@ -20,6 +20,8 @@ func NewHandler(keeper Keeper) sdk.Handler { switch msg := msg.(type) { case types.EthereumTxMsg: return handleETHTxMsg(ctx, keeper, msg) + case types.EmintMsg: + return handleEmintMsg(ctx, keeper, msg) default: errMsg := fmt.Sprintf("Unrecognized ethermint Msg type: %v", msg.Type()) return sdk.ErrUnknownRequest(errMsg).Result() @@ -44,82 +46,64 @@ func handleETHTxMsg(ctx sdk.Context, keeper Keeper, msg types.EthereumTxMsg) sdk if err != nil { return emint.ErrInvalidSender(err.Error()).Result() } - contractCreation := msg.To() == nil - // Pay intrinsic gas - // TODO: Check config for homestead enabled - cost, err := core.IntrinsicGas(msg.Data.Payload, contractCreation, true) - if err != nil { - return emint.ErrInvalidIntrinsicGas(err.Error()).Result() + st := types.StateTransition{ + Sender: sender, + AccountNonce: msg.Data.AccountNonce, + Price: msg.Data.Price, + GasLimit: msg.Data.GasLimit, + Recipient: msg.Data.Recipient, + Amount: msg.Data.Amount, + Payload: msg.Data.Payload, + Csdb: keeper.csdb, + ChainID: intChainID, } - usableGas := msg.Data.GasLimit - cost - - // Create context for evm - context := vm.Context{ - CanTransfer: core.CanTransfer, - Transfer: core.Transfer, - Origin: sender, - Coinbase: common.Address{}, - BlockNumber: big.NewInt(ctx.BlockHeight()), - Time: big.NewInt(time.Now().Unix()), - Difficulty: big.NewInt(0x30000), // unused - GasLimit: ctx.GasMeter().Limit(), - GasPrice: ctx.MinGasPrices().AmountOf(emint.DenomDefault).Int, - } - - vmenv := vm.NewEVM(context, keeper.csdb.WithContext(ctx), types.GenerateChainConfig(intChainID), vm.Config{}) - - var ( - leftOverGas uint64 - addr common.Address - vmerr error - senderRef = vm.AccountRef(sender) - ) - - if contractCreation { - _, addr, leftOverGas, vmerr = vmenv.Create(senderRef, msg.Data.Payload, usableGas, msg.Data.Amount) - } else { - // Increment the nonce for the next transaction - keeper.csdb.SetNonce(sender, keeper.csdb.GetNonce(sender)+1) - _, leftOverGas, vmerr = vmenv.Call(senderRef, *msg.To(), msg.Data.Payload, usableGas, msg.Data.Amount) - } - - // handle errors - if vmerr != nil { - return emint.ErrVMExecution(vmerr.Error()).Result() + // Encode transaction by default Tx encoder + txEncoder := authutils.GetTxEncoder(types.ModuleCdc) + txBytes, err := txEncoder(msg) + if err != nil { + return sdk.ErrInternal(err.Error()).Result() } + txHash := tm.Tx(txBytes).Hash() - // Refund remaining gas from tx (Check these values and ensure gas is being consumed correctly) - refundGas(keeper.csdb, &leftOverGas, msg.Data.GasLimit, context.GasPrice, sender) + // Prepare db for logs + keeper.csdb.Prepare(common.BytesToHash(txHash), common.Hash{}, keeper.txCount.get()) + keeper.txCount.increment() - // add balance for the processor of the tx (determine who rewards are being processed to) - // TODO: Double check nothing needs to be done here + return st.TransitionCSDB(ctx) +} - keeper.csdb.Finalise(true) // Change to depend on config +func handleEmintMsg(ctx sdk.Context, keeper Keeper, msg types.EmintMsg) sdk.Result { + if err := msg.ValidateBasic(); err != nil { + return err.Result() + } - // TODO: Consume gas from sender + // parse the chainID from a string to a base-10 integer + intChainID, ok := new(big.Int).SetString(ctx.ChainID(), 10) + if !ok { + return emint.ErrInvalidChainID(fmt.Sprintf("invalid chainID: %s", ctx.ChainID())).Result() + } - return sdk.Result{Data: addr.Bytes(), GasUsed: msg.Data.GasLimit - leftOverGas} -} + st := types.StateTransition{ + Sender: common.BytesToAddress(msg.From.Bytes()), + AccountNonce: msg.AccountNonce, + Price: msg.Price.BigInt(), + GasLimit: msg.GasLimit, + Amount: msg.Amount.BigInt(), + Payload: msg.Payload, + Csdb: keeper.csdb, + ChainID: intChainID, + } -func refundGas( - st vm.StateDB, gasRemaining *uint64, initialGas uint64, gasPrice *big.Int, - from common.Address, -) { - // Apply refund counter, capped to half of the used gas. - refund := (initialGas - *gasRemaining) / 2 - if refund > st.GetRefund() { - refund = st.GetRefund() + if msg.Recipient != nil { + to := common.BytesToAddress(msg.Recipient.Bytes()) + st.Recipient = &to } - *gasRemaining += refund - // Return ETH for remaining gas, exchanged at the original rate. - remaining := new(big.Int).Mul(new(big.Int).SetUint64(*gasRemaining), gasPrice) - st.AddBalance(from, remaining) + // Prepare db for logs + keeper.csdb.Prepare(common.Hash{}, common.Hash{}, keeper.txCount.get()) // Cannot provide tx hash + keeper.txCount.increment() - // // Also return remaining gas to the block gas counter so it is - // // available for the next transaction. - // TODO: Return gas to block gas meter? - // st.gp.AddGas(st.gas) + return st.TransitionCSDB(ctx) } diff --git a/x/evm/keeper.go b/x/evm/keeper.go index 4eb14f42f..fe31c1e5e 100644 --- a/x/evm/keeper.go +++ b/x/evm/keeper.go @@ -23,6 +23,21 @@ type Keeper struct { csdb *types.CommitStateDB cdc *codec.Codec blockKey sdk.StoreKey + txCount *count +} + +type count int + +func (c *count) get() int { + return (int)(*c) +} + +func (c *count) increment() { + *c = *c + 1 +} + +func (c *count) reset() { + *c = 0 } // NewKeeper generates new evm module keeper @@ -32,6 +47,7 @@ func NewKeeper(ak auth.AccountKeeper, storageKey, codeKey sdk.StoreKey, csdb: types.NewCommitStateDB(sdk.Context{}, ak, storageKey, codeKey), cdc: cdc, blockKey: blockKey, + txCount: new(count), } } @@ -53,7 +69,7 @@ func (k *Keeper) GetBlockHashMapping(ctx sdk.Context, hash []byte) (height int64 store := ctx.KVStore(k.blockKey) bz := store.Get(hash) if bytes.Equal(bz, []byte{}) { - panic(fmt.Errorf("block with hash %s not found", ethcmn.Bytes2Hex(hash))) + panic(fmt.Errorf("block with hash %s not found", ethcmn.BytesToHash(hash))) } k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &height) return diff --git a/x/evm/module.go b/x/evm/module.go index 3f403f0e0..7d3033578 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -107,6 +107,7 @@ func (am AppModule) NewQuerierHandler() sdk.Querier { func (am AppModule) BeginBlock(ctx sdk.Context, bl abci.RequestBeginBlock) { // Consider removing this when using evm as module without web3 API am.keeper.SetBlockHashMapping(ctx, bl.Header.LastBlockId.GetHash(), bl.Header.GetHeight()-1) + am.keeper.txCount.reset() } // EndBlock function for module at end of block diff --git a/x/evm/types/codec.go b/x/evm/types/codec.go index 5c0818174..eddab1d05 100644 --- a/x/evm/types/codec.go +++ b/x/evm/types/codec.go @@ -5,6 +5,7 @@ import ( "github.com/cosmos/ethermint/crypto" ) +// ModuleCdc defines the codec to be used by evm module var ModuleCdc = codec.New() func init() { @@ -19,5 +20,6 @@ func init() { // RegisterCodec registers concrete types and interfaces on the given codec. func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(&EthereumTxMsg{}, "ethermint/MsgEthereumTx", nil) + cdc.RegisterConcrete(&EmintMsg{}, "ethermint/MsgEmint", nil) crypto.RegisterCodec(cdc) } diff --git a/x/evm/types/emint_msg.go b/x/evm/types/emint_msg.go new file mode 100644 index 000000000..41be900ee --- /dev/null +++ b/x/evm/types/emint_msg.go @@ -0,0 +1,88 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/ethermint/types" + ethcmn "github.com/ethereum/go-ethereum/common" +) + +var ( + _ sdk.Msg = EmintMsg{} +) + +const ( + // TypeEmintMsg defines the type string of Emint message + TypeEmintMsg = "emint_tx" +) + +// EmintMsg implements a cosmos equivalent structure for Ethereum transactions +type EmintMsg struct { + AccountNonce uint64 `json:"nonce"` + Price sdk.Int `json:"gasPrice"` + GasLimit uint64 `json:"gas"` + Recipient *sdk.AccAddress `json:"to" rlp:"nil"` // nil means contract creation + Amount sdk.Int `json:"value"` + Payload []byte `json:"input"` + + // From address (formerly derived from signature) + From sdk.AccAddress `json:"from"` +} + +// NewEmintMsg returns a reference to a new Ethermint transaction +func NewEmintMsg( + nonce uint64, to *sdk.AccAddress, amount sdk.Int, + gasLimit uint64, gasPrice sdk.Int, payload []byte, from sdk.AccAddress, +) EmintMsg { + return EmintMsg{ + AccountNonce: nonce, + Price: gasPrice, + GasLimit: gasLimit, + Recipient: to, + Amount: amount, + Payload: payload, + From: from, + } +} + +// Route should return the name of the module +func (msg EmintMsg) Route() string { return RouterKey } + +// Type returns the action of the message +func (msg EmintMsg) Type() string { return TypeEmintMsg } + +// GetSignBytes encodes the message for signing +func (msg EmintMsg) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +// ValidateBasic runs stateless checks on the message +func (msg EmintMsg) ValidateBasic() sdk.Error { + if msg.Price.Sign() != 1 { + return types.ErrInvalidValue(fmt.Sprintf("Price must be positive: %x", msg.Price)) + } + + // Amount can be 0 + if msg.Amount.Sign() == -1 { + return types.ErrInvalidValue(fmt.Sprintf("amount cannot be negative: %x", msg.Amount)) + } + + return nil +} + +// GetSigners defines whose signature is required +func (msg EmintMsg) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +// To returns the recipient address of the transaction. It returns nil if the +// transaction is a contract creation. +func (msg EmintMsg) To() *ethcmn.Address { + if msg.Recipient == nil { + return nil + } + + addr := ethcmn.BytesToAddress(msg.Recipient.Bytes()) + return &addr +} diff --git a/x/evm/types/emint_msg_test.go b/x/evm/types/emint_msg_test.go new file mode 100644 index 000000000..f28f19474 --- /dev/null +++ b/x/evm/types/emint_msg_test.go @@ -0,0 +1,76 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +func TestEmintMsg(t *testing.T) { + addr := newSdkAddress() + fromAddr := newSdkAddress() + + msg := NewEmintMsg(0, &addr, sdk.NewInt(1), 100000, sdk.NewInt(2), []byte("test"), fromAddr) + require.NotNil(t, msg) + require.Equal(t, msg.Recipient, &addr) + + require.Equal(t, msg.Route(), RouterKey) + require.Equal(t, msg.Type(), TypeEmintMsg) +} + +func TestEmintMsgValidation(t *testing.T) { + testCases := []struct { + nonce uint64 + to *sdk.AccAddress + amount sdk.Int + gasLimit uint64 + gasPrice sdk.Int + payload []byte + expectPass bool + from sdk.AccAddress + }{ + {amount: sdk.NewInt(100), gasPrice: sdk.NewInt(100000), expectPass: true}, + {amount: sdk.NewInt(0), gasPrice: sdk.NewInt(100000), expectPass: true}, + {amount: sdk.NewInt(-1), gasPrice: sdk.NewInt(100000), expectPass: false}, + {amount: sdk.NewInt(100), gasPrice: sdk.NewInt(-1), expectPass: false}, + } + + for i, tc := range testCases { + msg := NewEmintMsg(tc.nonce, tc.to, tc.amount, tc.gasLimit, tc.gasPrice, tc.payload, tc.from) + + if tc.expectPass { + require.Nil(t, msg.ValidateBasic(), "test: %v", i) + } else { + require.NotNil(t, msg.ValidateBasic(), "test: %v", i) + } + } +} + +func TestEmintEncodingAndDecoding(t *testing.T) { + addr := newSdkAddress() + fromAddr := newSdkAddress() + + msg := NewEmintMsg(0, &addr, sdk.NewInt(1), 100000, sdk.NewInt(2), []byte("test"), fromAddr) + + raw, err := cdc.MarshalBinaryBare(msg) + require.NoError(t, err) + + var msg2 EmintMsg + err = cdc.UnmarshalBinaryBare(raw, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.AccountNonce, msg2.AccountNonce) + require.Equal(t, msg.Recipient, msg2.Recipient) + require.Equal(t, msg.Amount, msg2.Amount) + require.Equal(t, msg.GasLimit, msg2.GasLimit) + require.Equal(t, msg.Price, msg2.Price) + require.Equal(t, msg.Payload, msg2.Payload) + require.Equal(t, msg.From, msg2.From) +} + +func newSdkAddress() sdk.AccAddress { + tmpKey := secp256k1.GenPrivKey().PubKey() + return sdk.AccAddress(tmpKey.Address().Bytes()) +} diff --git a/x/evm/types/state_transition.go b/x/evm/types/state_transition.go new file mode 100644 index 000000000..4565f9eba --- /dev/null +++ b/x/evm/types/state_transition.go @@ -0,0 +1,99 @@ +package types + +import ( + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/vm" + + sdk "github.com/cosmos/cosmos-sdk/types" + emint "github.com/cosmos/ethermint/types" +) + +// StateTransition defines data to transitionDB in evm +type StateTransition struct { + Sender common.Address + AccountNonce uint64 + Price *big.Int + GasLimit uint64 + Recipient *common.Address + Amount *big.Int + Payload []byte + Csdb *CommitStateDB + ChainID *big.Int +} + +// TransitionCSDB performs an evm state transition from a transaction +func (st StateTransition) TransitionCSDB(ctx sdk.Context) sdk.Result { + contractCreation := st.Recipient == nil + + // Create context for evm + context := vm.Context{ + CanTransfer: core.CanTransfer, + Transfer: core.Transfer, + Origin: st.Sender, + Coinbase: common.Address{}, + BlockNumber: big.NewInt(ctx.BlockHeight()), + Time: big.NewInt(time.Now().Unix()), + Difficulty: big.NewInt(0x30000), // unused + GasLimit: ctx.GasMeter().Limit(), + GasPrice: ctx.MinGasPrices().AmountOf(emint.DenomDefault).Int, + } + + vmenv := vm.NewEVM(context, st.Csdb.WithContext(ctx), GenerateChainConfig(st.ChainID), vm.Config{}) + + var ( + leftOverGas uint64 + addr common.Address + vmerr error + senderRef = vm.AccountRef(st.Sender) + ) + + if contractCreation { + _, addr, leftOverGas, vmerr = vmenv.Create(senderRef, st.Payload, st.GasLimit, st.Amount) + } else { + // Increment the nonce for the next transaction + st.Csdb.SetNonce(st.Sender, st.Csdb.GetNonce(st.Sender)+1) + _, leftOverGas, vmerr = vmenv.Call(senderRef, *st.Recipient, st.Payload, st.GasLimit, st.Amount) + } + + // handle errors + if vmerr != nil { + return emint.ErrVMExecution(vmerr.Error()).Result() + } + + // Refund remaining gas from tx (Check these values and ensure gas is being consumed correctly) + refundGas(st.Csdb, &leftOverGas, st.GasLimit, context.GasPrice, st.Sender) + + // add balance for the processor of the tx (determine who rewards are being processed to) + // TODO: Double check nothing needs to be done here + + st.Csdb.Finalise(true) // Change to depend on config + + // TODO: Consume gas from sender + + return sdk.Result{Data: addr.Bytes(), GasUsed: st.GasLimit - leftOverGas} +} + +func refundGas( + st vm.StateDB, gasRemaining *uint64, initialGas uint64, gasPrice *big.Int, + from common.Address, +) { + // Apply refund counter, capped to half of the used gas. + refund := (initialGas - *gasRemaining) / 2 + if refund > st.GetRefund() { + refund = st.GetRefund() + } + *gasRemaining += refund + + // // Return ETH for remaining gas, exchanged at the original rate. + // remaining := new(big.Int).Mul(new(big.Int).SetUint64(*gasRemaining), gasPrice) + // st.AddBalance(from, remaining) + + // // Also return remaining gas to the block gas counter so it is + // // available for the next transaction. + // TODO: Return gas to block gas meter? + // st.gp.AddGas(st.gas) +} diff --git a/x/evm/types/statedb.go b/x/evm/types/statedb.go index 869d78959..c944bf115 100644 --- a/x/evm/types/statedb.go +++ b/x/evm/types/statedb.go @@ -96,6 +96,7 @@ func NewCommitStateDB(ctx sdk.Context, ak auth.AccountKeeper, storageKey, codeKe } } +// WithContext returns a Database with an updated sdk context func (csdb *CommitStateDB) WithContext(ctx sdk.Context) *CommitStateDB { csdb.ctx = ctx return csdb @@ -372,7 +373,7 @@ func (csdb *CommitStateDB) Commit(deleteEmptyObjects bool) (root ethcmn.Hash, er return } -// Finalize finalizes the state objects (accounts) state by setting their state, +// Finalise finalizes the state objects (accounts) state by setting their state, // removing the csdb destructed objects and clearing the journal as well as the // refunds. func (csdb *CommitStateDB) Finalise(deleteEmptyObjects bool) {