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:
parent
6ba38d6cee
commit
8bb8b40b32
120
x/evm/handler.go
120
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)
|
||||
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,
|
||||
}
|
||||
|
||||
// Encode transaction by default Tx encoder
|
||||
txEncoder := authutils.GetTxEncoder(types.ModuleCdc)
|
||||
txBytes, err := txEncoder(msg)
|
||||
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
|
||||
|
||||
// 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,
|
||||
func handleEmintMsg(ctx sdk.Context, keeper Keeper, msg types.EmintMsg) sdk.Result {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err.Result()
|
||||
}
|
||||
|
||||
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)
|
||||
// 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()
|
||||
}
|
||||
|
||||
// handle errors
|
||||
if vmerr != nil {
|
||||
return emint.ErrVMExecution(vmerr.Error()).Result()
|
||||
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,
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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}
|
||||
if msg.Recipient != nil {
|
||||
to := common.BytesToAddress(msg.Recipient.Bytes())
|
||||
st.Recipient = &to
|
||||
}
|
||||
|
||||
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
|
||||
// Prepare db for logs
|
||||
keeper.csdb.Prepare(common.Hash{}, common.Hash{}, keeper.txCount.get()) // Cannot provide tx hash
|
||||
keeper.txCount.increment()
|
||||
|
||||
// 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)
|
||||
return st.TransitionCSDB(ctx)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
88
x/evm/types/emint_msg.go
Normal file
88
x/evm/types/emint_msg.go
Normal 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
|
||||
}
|
76
x/evm/types/emint_msg_test.go
Normal file
76
x/evm/types/emint_msg_test.go
Normal 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())
|
||||
}
|
99
x/evm/types/state_transition.go
Normal file
99
x/evm/types/state_transition.go
Normal 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)
|
||||
}
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user