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
This commit is contained in:
Austin Abell 2019-10-03 12:46:02 -04:00 committed by GitHub
parent 6ba38d6cee
commit 8bb8b40b32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 340 additions and 73 deletions

View File

@ -3,15 +3,15 @@ package evm
import ( import (
"fmt" "fmt"
"math/big" "math/big"
"time"
"github.com/ethereum/go-ethereum/common" "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" sdk "github.com/cosmos/cosmos-sdk/types"
authutils "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
emint "github.com/cosmos/ethermint/types" emint "github.com/cosmos/ethermint/types"
"github.com/cosmos/ethermint/x/evm/types" "github.com/cosmos/ethermint/x/evm/types"
tm "github.com/tendermint/tendermint/types"
) )
// NewHandler returns a handler for Ethermint type messages. // NewHandler returns a handler for Ethermint type messages.
@ -20,6 +20,8 @@ func NewHandler(keeper Keeper) sdk.Handler {
switch msg := msg.(type) { switch msg := msg.(type) {
case types.EthereumTxMsg: case types.EthereumTxMsg:
return handleETHTxMsg(ctx, keeper, msg) return handleETHTxMsg(ctx, keeper, msg)
case types.EmintMsg:
return handleEmintMsg(ctx, keeper, msg)
default: default:
errMsg := fmt.Sprintf("Unrecognized ethermint Msg type: %v", msg.Type()) errMsg := fmt.Sprintf("Unrecognized ethermint Msg type: %v", msg.Type())
return sdk.ErrUnknownRequest(errMsg).Result() return sdk.ErrUnknownRequest(errMsg).Result()
@ -44,82 +46,64 @@ func handleETHTxMsg(ctx sdk.Context, keeper Keeper, msg types.EthereumTxMsg) sdk
if err != nil { if err != nil {
return emint.ErrInvalidSender(err.Error()).Result() return emint.ErrInvalidSender(err.Error()).Result()
} }
contractCreation := msg.To() == nil
// Pay intrinsic gas st := types.StateTransition{
// TODO: Check config for homestead enabled Sender: sender,
cost, err := core.IntrinsicGas(msg.Data.Payload, contractCreation, true) 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,
}
// Encode transaction by default Tx encoder
txEncoder := authutils.GetTxEncoder(types.ModuleCdc)
txBytes, err := txEncoder(msg)
if err != nil { if err != nil {
return emint.ErrInvalidIntrinsicGas(err.Error()).Result() return sdk.ErrInternal(err.Error()).Result()
}
txHash := tm.Tx(txBytes).Hash()
// Prepare db for logs
keeper.csdb.Prepare(common.BytesToHash(txHash), common.Hash{}, keeper.txCount.get())
keeper.txCount.increment()
return st.TransitionCSDB(ctx)
} }
usableGas := msg.Data.GasLimit - cost func handleEmintMsg(ctx sdk.Context, keeper Keeper, msg types.EmintMsg) sdk.Result {
if err := msg.ValidateBasic(); err != nil {
// Create context for evm return err.Result()
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{}) // parse the chainID from a string to a base-10 integer
intChainID, ok := new(big.Int).SetString(ctx.ChainID(), 10)
var ( if !ok {
leftOverGas uint64 return emint.ErrInvalidChainID(fmt.Sprintf("invalid chainID: %s", ctx.ChainID())).Result()
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 st := types.StateTransition{
if vmerr != nil { Sender: common.BytesToAddress(msg.From.Bytes()),
return emint.ErrVMExecution(vmerr.Error()).Result() AccountNonce: msg.AccountNonce,
Price: msg.Price.BigInt(),
GasLimit: msg.GasLimit,
Amount: msg.Amount.BigInt(),
Payload: msg.Payload,
Csdb: keeper.csdb,
ChainID: intChainID,
} }
// Refund remaining gas from tx (Check these values and ensure gas is being consumed correctly) if msg.Recipient != nil {
refundGas(keeper.csdb, &leftOverGas, msg.Data.GasLimit, context.GasPrice, sender) to := common.BytesToAddress(msg.Recipient.Bytes())
st.Recipient = &to
// add balance for the processor of the tx (determine who rewards are being processed to)
// TODO: Double check nothing needs to be done here
keeper.csdb.Finalise(true) // Change to depend on config
// TODO: Consume gas from sender
return sdk.Result{Data: addr.Bytes(), GasUsed: msg.Data.GasLimit - leftOverGas}
} }
func refundGas( // Prepare db for logs
st vm.StateDB, gasRemaining *uint64, initialGas uint64, gasPrice *big.Int, keeper.csdb.Prepare(common.Hash{}, common.Hash{}, keeper.txCount.get()) // Cannot provide tx hash
from common.Address, keeper.txCount.increment()
) {
// 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. return st.TransitionCSDB(ctx)
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)
} }

View File

@ -23,6 +23,21 @@ type Keeper struct {
csdb *types.CommitStateDB csdb *types.CommitStateDB
cdc *codec.Codec cdc *codec.Codec
blockKey sdk.StoreKey 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 // 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), csdb: types.NewCommitStateDB(sdk.Context{}, ak, storageKey, codeKey),
cdc: cdc, cdc: cdc,
blockKey: blockKey, 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) store := ctx.KVStore(k.blockKey)
bz := store.Get(hash) bz := store.Get(hash)
if bytes.Equal(bz, []byte{}) { 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) k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &height)
return return

View File

@ -107,6 +107,7 @@ func (am AppModule) NewQuerierHandler() sdk.Querier {
func (am AppModule) BeginBlock(ctx sdk.Context, bl abci.RequestBeginBlock) { func (am AppModule) BeginBlock(ctx sdk.Context, bl abci.RequestBeginBlock) {
// Consider removing this when using evm as module without web3 API // 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.SetBlockHashMapping(ctx, bl.Header.LastBlockId.GetHash(), bl.Header.GetHeight()-1)
am.keeper.txCount.reset()
} }
// EndBlock function for module at end of block // EndBlock function for module at end of block

View File

@ -5,6 +5,7 @@ import (
"github.com/cosmos/ethermint/crypto" "github.com/cosmos/ethermint/crypto"
) )
// ModuleCdc defines the codec to be used by evm module
var ModuleCdc = codec.New() var ModuleCdc = codec.New()
func init() { func init() {
@ -19,5 +20,6 @@ func init() {
// RegisterCodec registers concrete types and interfaces on the given codec. // RegisterCodec registers concrete types and interfaces on the given codec.
func RegisterCodec(cdc *codec.Codec) { func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(&EthereumTxMsg{}, "ethermint/MsgEthereumTx", nil) cdc.RegisterConcrete(&EthereumTxMsg{}, "ethermint/MsgEthereumTx", nil)
cdc.RegisterConcrete(&EmintMsg{}, "ethermint/MsgEmint", nil)
crypto.RegisterCodec(cdc) crypto.RegisterCodec(cdc)
} }

88
x/evm/types/emint_msg.go Normal file
View File

@ -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
}

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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 { func (csdb *CommitStateDB) WithContext(ctx sdk.Context) *CommitStateDB {
csdb.ctx = ctx csdb.ctx = ctx
return csdb return csdb
@ -372,7 +373,7 @@ func (csdb *CommitStateDB) Commit(deleteEmptyObjects bool) (root ethcmn.Hash, er
return 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 // removing the csdb destructed objects and clearing the journal as well as the
// refunds. // refunds.
func (csdb *CommitStateDB) Finalise(deleteEmptyObjects bool) { func (csdb *CommitStateDB) Finalise(deleteEmptyObjects bool) {