Skip to content
This repository has been archived by the owner on Nov 30, 2021. It is now read-only.

Commit

Permalink
Emint tx type for eth_call and logs setup (#118)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
austinabell authored Oct 3, 2019
1 parent 6ba38d6 commit 8bb8b40
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 70 deletions.
120 changes: 52 additions & 68 deletions x/evm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()
Expand All @@ -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)
}
18 changes: 17 additions & 1 deletion x/evm/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
}
}

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions x/evm/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions x/evm/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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)
}
88 changes: 88 additions & 0 deletions x/evm/types/emint_msg.go
Original file line number Diff line number Diff line change
@@ -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
}
76 changes: 76 additions & 0 deletions x/evm/types/emint_msg_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
Loading

0 comments on commit 8bb8b40

Please sign in to comment.