diff --git a/app/ethermint.go b/app/ethermint.go index 8b487e7e3..5d601dba8 100644 --- a/app/ethermint.go +++ b/app/ethermint.go @@ -25,6 +25,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/x/supply" + eminttypes "github.com/cosmos/ethermint/types" evmtypes "github.com/cosmos/ethermint/x/evm/types" abci "github.com/tendermint/tendermint/abci/types" @@ -79,6 +80,8 @@ func MakeCodec() *codec.Codec { ModuleBasics.RegisterCodec(cdc) sdk.RegisterCodec(cdc) codec.RegisterCrypto(cdc) + eminttypes.RegisterCodec(cdc) + return cdc } @@ -128,7 +131,7 @@ func NewEthermintApp( keys := sdk.NewKVStoreKeys(bam.MainStoreKey, auth.StoreKey, staking.StoreKey, supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, - gov.StoreKey, params.StoreKey) + gov.StoreKey, params.StoreKey, evmtypes.EvmStoreKey, evmtypes.EvmCodeKey) tkeys := sdk.NewTransientStoreKeys(staking.TStoreKey, params.TStoreKey) app := &EthermintApp{ @@ -151,7 +154,7 @@ func NewEthermintApp( crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace) // add keepers - app.accountKeeper = auth.NewAccountKeeper(app.cdc, keys[auth.StoreKey], authSubspace, auth.ProtoBaseAccount) + app.accountKeeper = auth.NewAccountKeeper(app.cdc, keys[auth.StoreKey], authSubspace, eminttypes.ProtoBaseAccount) app.bankKeeper = bank.NewBaseKeeper(app.accountKeeper, bankSubspace, bank.DefaultCodespace, app.ModuleAccountAddrs()) app.supplyKeeper = supply.NewKeeper(app.cdc, keys[supply.StoreKey], app.accountKeeper, app.bankKeeper, maccPerms) stakingKeeper := staking.NewKeeper(app.cdc, keys[staking.StoreKey], tkeys[staking.TStoreKey], @@ -162,6 +165,7 @@ func NewEthermintApp( app.slashingKeeper = slashing.NewKeeper(app.cdc, keys[slashing.StoreKey], &stakingKeeper, slashingSubspace, slashing.DefaultCodespace) app.crisisKeeper = crisis.NewKeeper(crisisSubspace, invCheckPeriod, app.supplyKeeper, auth.FeeCollectorName) + app.evmKeeper = evm.NewKeeper(app.accountKeeper, keys[evmtypes.EvmStoreKey], keys[evmtypes.EvmCodeKey], cdc) // register the proposal types govRouter := gov.NewRouter() @@ -204,7 +208,7 @@ func NewEthermintApp( app.mm.SetOrderInitGenesis( genaccounts.ModuleName, distr.ModuleName, staking.ModuleName, auth.ModuleName, bank.ModuleName, slashing.ModuleName, gov.ModuleName, - mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName, + mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName, evmtypes.ModuleName, ) app.mm.RegisterInvariants(&app.crisisKeeper) diff --git a/types/account.go b/types/account.go index c4c3d85ab..5bf6000d9 100644 --- a/types/account.go +++ b/types/account.go @@ -28,8 +28,8 @@ type Account struct { // merkle root of the storage trie // - // TODO: good chance we may not need this - Root ethcmn.Hash + // TODO: add back root if needed (marshalling is broken if not initializing) + // Root ethcmn.Hash CodeHash []byte } diff --git a/types/account_retriever.go b/types/account_retriever.go new file mode 100644 index 000000000..51372c038 --- /dev/null +++ b/types/account_retriever.go @@ -0,0 +1,77 @@ +package types + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/exported" + auth "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// ** Modified version of github.com/cosmos/cosmos-sdk/x/auth/types/account_retriever.go +// ** to allow passing in a codec for decoding Account types +// AccountRetriever defines the properties of a type that can be used to +// retrieve accounts. +type AccountRetriever struct { + querier auth.NodeQuerier + codec *codec.Codec +} + +// * Modified to allow a codec to be passed in +// NewAccountRetriever initialises a new AccountRetriever instance. +func NewAccountRetriever(querier auth.NodeQuerier, codec *codec.Codec) AccountRetriever { + if codec == nil { + codec = auth.ModuleCdc + } + return AccountRetriever{querier: querier, codec: codec} +} + +// GetAccount queries for an account given an address and a block height. An +// error is returned if the query or decoding fails. +func (ar AccountRetriever) GetAccount(addr sdk.AccAddress) (exported.Account, error) { + account, _, err := ar.GetAccountWithHeight(addr) + return account, err +} + +// GetAccountWithHeight queries for an account given an address. Returns the +// height of the query with the account. An error is returned if the query +// or decoding fails. +func (ar AccountRetriever) GetAccountWithHeight(addr sdk.AccAddress) (exported.Account, int64, error) { + // ** This line was changed to use non-static codec + bs, err := ar.codec.MarshalJSON(auth.NewQueryAccountParams(addr)) + if err != nil { + return nil, 0, err + } + + res, height, err := ar.querier.QueryWithData(fmt.Sprintf("custom/%s/%s", auth.QuerierRoute, auth.QueryAccount), bs) + if err != nil { + return nil, 0, err + } + + var account exported.Account + // ** This line was changed to use non-static codec + if err := ar.codec.UnmarshalJSON(res, &account); err != nil { + return nil, 0, err + } + + return account, height, nil +} + +// EnsureExists returns an error if no account exists for the given address else nil. +func (ar AccountRetriever) EnsureExists(addr sdk.AccAddress) error { + if _, err := ar.GetAccount(addr); err != nil { + return err + } + return nil +} + +// GetAccountNumberSequence returns sequence and account number for the given address. +// It returns an error if the account couldn't be retrieved from the state. +func (ar AccountRetriever) GetAccountNumberSequence(addr sdk.AccAddress) (uint64, uint64, error) { + acc, err := ar.GetAccount(addr) + if err != nil { + return 0, 0, err + } + return acc.GetAccountNumber(), acc.GetSequence(), nil +} diff --git a/types/errors.go b/types/errors.go index 90cfe3e37..43a381105 100644 --- a/types/errors.go +++ b/types/errors.go @@ -9,8 +9,11 @@ const ( // DefaultCodespace reserves a Codespace for Ethermint. DefaultCodespace sdk.CodespaceType = "ethermint" - CodeInvalidValue sdk.CodeType = 1 - CodeInvalidChainID sdk.CodeType = 2 + CodeInvalidValue sdk.CodeType = 1 + CodeInvalidChainID sdk.CodeType = 2 + CodeInvalidSender sdk.CodeType = 3 + CodeVMExecution sdk.CodeType = 4 + CodeInvalidIntrinsicGas sdk.CodeType = 5 ) // CodeToDefaultMsg takes the CodeType variable and returns the error string @@ -20,19 +23,38 @@ func CodeToDefaultMsg(code sdk.CodeType) string { return "invalid value" case CodeInvalidChainID: return "invalid chain ID" + case CodeInvalidSender: + return "could not derive sender from transaction" + case CodeVMExecution: + return "error while executing evm transaction" + case CodeInvalidIntrinsicGas: + return "invalid intrinsic gas" default: return sdk.CodeToDefaultMsg(code) } } -// ErrInvalidValue returns a standardized SDK error resulting from an invalid -// value. +// ErrInvalidValue returns a standardized SDK error resulting from an invalid value. func ErrInvalidValue(msg string) sdk.Error { return sdk.NewError(DefaultCodespace, CodeInvalidValue, msg) } -// ErrInvalidChainID returns a standardized SDK error resulting from an invalid -// chain ID. +// ErrInvalidChainID returns a standardized SDK error resulting from an invalid chain ID. func ErrInvalidChainID(msg string) sdk.Error { return sdk.NewError(DefaultCodespace, CodeInvalidChainID, msg) } + +// ErrInvalidSender returns a standardized SDK error resulting from an invalid transaction sender. +func ErrInvalidSender(msg string) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeInvalidSender, msg) +} + +// ErrVMExecution returns a standardized SDK error resulting from an error in EVM execution. +func ErrVMExecution(msg string) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeVMExecution, msg) +} + +// ErrVMExecution returns a standardized SDK error resulting from an error in EVM execution. +func ErrInvalidIntrinsicGas(msg string) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeInvalidIntrinsicGas, msg) +} diff --git a/x/evm/client/cli/tx.go b/x/evm/client/cli/tx.go index d6a6e71ee..da49a85e2 100644 --- a/x/evm/client/cli/tx.go +++ b/x/evm/client/cli/tx.go @@ -88,9 +88,9 @@ func GetCmdGenTx(cdc *codec.Codec) *cobra.Command { // GetCmdGenTx generates an ethereum transaction func GetCmdGenETHTx(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "generate-eth-tx [nonce] [ethaddress] [amount] [gaslimit] [gasprice] [payload]", - Short: "geberate and broadcast an Ethereum tx", - Args: cobra.ExactArgs(6), + Use: "generate-eth-tx [amount] [gaslimit] [gasprice] [payload] []", + Short: "generate and broadcast an Ethereum tx. If address is not specified, contract will be created", + Args: cobra.RangeArgs(4, 5), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := emintUtils.NewETHCLIContext().WithCodec(cdc) @@ -101,35 +101,41 @@ func GetCmdGenETHTx(cdc *codec.Codec) *cobra.Command { panic(err) } - nonce, err := strconv.ParseUint(args[0], 0, 64) + coins, err := sdk.ParseCoins(args[0]) if err != nil { return err } - coins, err := sdk.ParseCoins(args[2]) + gasLimit, err := strconv.ParseUint(args[1], 0, 64) if err != nil { return err } - gasLimit, err := strconv.ParseUint(args[3], 0, 64) + gasPrice, err := strconv.ParseUint(args[2], 0, 64) if err != nil { return err } - gasPrice, err := strconv.ParseUint(args[4], 0, 64) + payload := args[3] + + txBldr, err = emintUtils.PrepareTxBuilder(txBldr, cliCtx) if err != nil { return err } - payload := args[5] + var tx *types.EthereumTxMsg + if len(args) == 5 { + tx = types.NewEthereumTxMsg(txBldr.Sequence(), ethcmn.HexToAddress(args[4]), big.NewInt(coins.AmountOf(emintTypes.DenomDefault).Int64()), gasLimit, new(big.Int).SetUint64(gasPrice), []byte(payload)) + } else { + tx = types.NewEthereumTxMsgContract(txBldr.Sequence(), big.NewInt(coins.AmountOf(emintTypes.DenomDefault).Int64()), gasLimit, new(big.Int).SetUint64(gasPrice), []byte(payload)) + } - tx := types.NewEthereumTxMsg(nonce, ethcmn.HexToAddress(args[1]), big.NewInt(coins.AmountOf(emintTypes.DenomDefault).Int64()), gasLimit, new(big.Int).SetUint64(gasPrice), []byte(payload)) err = tx.ValidateBasic() if err != nil { return err } - return emintUtils.BroadcastETHTx(cliCtx, txBldr.WithSequence(nonce).WithKeybase(kb), tx) + return emintUtils.BroadcastETHTx(cliCtx, txBldr.WithKeybase(kb), tx) }, } } diff --git a/x/evm/client/utils/tx.go b/x/evm/client/utils/tx.go index e20da0feb..06f6a8298 100644 --- a/x/evm/client/utils/tx.go +++ b/x/evm/client/utils/tx.go @@ -42,10 +42,6 @@ func GenerateOrBroadcastETHMsgs(cliCtx context.CLIContext, txBldr authtypes.TxBu // BroadcastETHTx Broadcasts an Ethereum Tx not wrapped in a Std Tx func BroadcastETHTx(cliCtx context.CLIContext, txBldr authtypes.TxBuilder, tx *evmtypes.EthereumTxMsg) error { - txBldr, err := utils.PrepareTxBuilder(txBldr, cliCtx) - if err != nil { - return err - } fromName := cliCtx.GetFromName() @@ -346,3 +342,34 @@ func getFromFields(from string, genOnly bool) (sdk.AccAddress, string, error) { return info.GetAddress(), info.GetName(), nil } + +// * Overriden function from cosmos-sdk/auth/client/utils/tx.go +// PrepareTxBuilder populates a TxBuilder in preparation for the build of a Tx. +func PrepareTxBuilder(txBldr authtypes.TxBuilder, cliCtx context.CLIContext) (authtypes.TxBuilder, error) { + from := cliCtx.GetFromAddress() + + // * Function is needed to override to use different account getter (to not use static codec) + accGetter := emint.NewAccountRetriever(cliCtx, cliCtx.Codec) + if err := accGetter.EnsureExists(from); err != nil { + return txBldr, err + } + + txbldrAccNum, txbldrAccSeq := txBldr.AccountNumber(), txBldr.Sequence() + // TODO: (ref #1903) Allow for user supplied account number without + // automatically doing a manual lookup. + if txbldrAccNum == 0 || txbldrAccSeq == 0 { + num, seq, err := accGetter.GetAccountNumberSequence(from) + if err != nil { + return txBldr, err + } + + if txbldrAccNum == 0 { + txBldr = txBldr.WithAccountNumber(num) + } + if txbldrAccSeq == 0 { + txBldr = txBldr.WithSequence(seq) + } + } + + return txBldr, nil +} diff --git a/x/evm/handler.go b/x/evm/handler.go index 867e8daec..043a49ae5 100644 --- a/x/evm/handler.go +++ b/x/evm/handler.go @@ -2,8 +2,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" + emint "github.com/cosmos/ethermint/types" "github.com/cosmos/ethermint/x/evm/types" ) @@ -22,20 +29,103 @@ func NewHandler(keeper Keeper) sdk.Handler { // Handle an Ethereum specific tx func handleETHTxMsg(ctx sdk.Context, keeper Keeper, msg types.EthereumTxMsg) sdk.Result { - // TODO: Implement transaction logic if err := msg.ValidateBasic(); err != nil { - return sdk.ErrUnknownRequest("Basic validation failed").Result() + return err.Result() + } + + // 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() } - // If no to address, create contract with evm.Create(...) + // Verify signature and retrieve sender address + sender, err := msg.VerifySig(intChainID) + 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() + } + + 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) + ) - // Else Call contract with evm.Call(...) + 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() + } - // Refund remaining gas from tx (Will supply keeper need to be introduced to evm Keeper to do this) + // 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: Remove commit from tx handler (should be done at end of block) + _, err = keeper.csdb.Commit(true) + if err != nil { + return sdk.ErrUnknownRequest("Failed to write data to kv store").Result() + } + + // TODO: Consume gas from sender + + return sdk.Result{Log: addr.Hex(), GasUsed: msg.Data.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) - return sdk.Result{} + // // 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/module.go b/x/evm/module.go index bb7968f73..b20d3006a 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -94,7 +94,8 @@ func (am AppModule) NewQuerierHandler() sdk.Querier { func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} -func (am AppModule) EndBlock(sdk.Context, abci.RequestEndBlock) []abci.ValidatorUpdate { +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + // TODO: Commit database here ? return []abci.ValidatorUpdate{} } diff --git a/x/evm/types/chain_config.go b/x/evm/types/chain_config.go new file mode 100644 index 000000000..9db2fc94a --- /dev/null +++ b/x/evm/types/chain_config.go @@ -0,0 +1,26 @@ +package types + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +// GenerateChainConfig returns an Ethereum chainconfig for EVM state transitions +func GenerateChainConfig(chainID *big.Int) *params.ChainConfig { + // TODO: Update chainconfig to take in parameters for fork blocks + return ¶ms.ChainConfig{ + ChainID: chainID, + HomesteadBlock: big.NewInt(0), + DAOForkBlock: big.NewInt(0), + DAOForkSupport: true, + EIP150Block: big.NewInt(0), + EIP150Hash: common.HexToHash("0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0"), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + } +} diff --git a/x/evm/types/state_object.go b/x/evm/types/state_object.go index 7f8eb11f4..fb198cb08 100644 --- a/x/evm/types/state_object.go +++ b/x/evm/types/state_object.go @@ -80,7 +80,12 @@ type ( func newObject(db *CommitStateDB, accProto auth.Account) *stateObject { acc, ok := accProto.(*types.Account) if !ok { - panic(fmt.Sprintf("invalid account type for state object: %T", acc)) + // State object can be created from a baseAccount + baseAccount, ok := accProto.(*auth.BaseAccount) + if !ok { + panic(fmt.Sprintf("invalid account type for state object: %T", accProto)) + } + acc = &types.Account{BaseAccount: baseAccount} } if acc.CodeHash == nil {