From b821a85a64afffec918b79bffe40e91cf643e8a3 Mon Sep 17 00:00:00 2001 From: Alexander Bezobchuk Date: Tue, 18 Dec 2018 11:10:04 -0500 Subject: [PATCH] R4R: CheckTx AnteHandler for Ethereum Tx Messages (#505) * Rename Ethereum tx message * Use new tx decoder in the ethermint app * Update ante handler to prevent spam/dos * Update ethereum msg signing/verification logic * Implement secp256k1 key types * Remove pointer from To method * Move sig check to after inartistic gas check * Add comment on chainID parsing * Updated validateIntrinsicGas godoc * Implement Fee method on eth tx msg * Add reference to spec for recoverEthSig * Upgrade TM to v0.27.0 --- Gopkg.lock | 26 ++-- Gopkg.toml | 16 +- app/ante.go | 296 ++++++++++++++++++++++++++++++------ app/ante_test.go | 263 ++++++++++++++++++++++++-------- app/ethermint.go | 8 +- app/test_common.go | 20 --- app/test_utils.go | 65 +++++--- crypto/codec.go | 16 ++ crypto/crypto.go | 22 --- crypto/crypto_test.go | 24 --- crypto/secp256k1.go | 105 +++++++++++++ crypto/secp256k1_test.go | 61 ++++++++ importer/importer_test.go | 15 +- types/errors.go | 16 +- x/evm/types/codec.go | 2 +- x/evm/types/journal.go | 2 +- x/evm/types/msg.go | 170 +++++++++++++++------ x/evm/types/msg_test.go | 54 ++++--- x/evm/types/state_object.go | 6 +- x/evm/types/utils.go | 11 +- 20 files changed, 871 insertions(+), 327 deletions(-) delete mode 100644 app/test_common.go create mode 100644 crypto/codec.go delete mode 100644 crypto/crypto.go delete mode 100644 crypto/crypto_test.go create mode 100644 crypto/secp256k1.go create mode 100644 crypto/secp256k1_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 010f9fc8..dbc6c40d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -33,7 +33,8 @@ revision = "d4cc87b860166d00d6b5b9e0d3b3d71d6088d4d4" [[projects]] - digest = "1:e70ff0ca07fdc5dfe6cf280917ae4434f069f164a0bf18681e37cb19b01ee151" + branch = "develop" + digest = "1:83eccb94de6a2aadff1bddb7020e3850131645ff45e9ce9093d1604e407130e7" name = "github.com/cosmos/cosmos-sdk" packages = [ "baseapp", @@ -56,7 +57,7 @@ "x/stake/types", ] pruneopts = "T" - revision = "1ea0e4c457fc105b48131a60e3d28c6c1bb32cc0" + revision = "ec9c4ea543b5d0f558cf6ad9f1386d26cfe87f28" [[projects]] digest = "1:9f42202ac457c462ad8bb9642806d275af9ab4850cf0b1960b9c6f083d4a309a" @@ -530,23 +531,23 @@ revision = "e5840949ff4fff0c56f9b6a541e22b63581ea9df" [[projects]] - digest = "1:bcaa26e82b7707fbdfa6d0506ff1f30158eeb23126f761ac65881ed51339bf9e" + digest = "1:02462ad4cc9b135c4ebfb9edccb53e8c705bbebdbf8664799ea641440188387e" name = "github.com/tendermint/go-amino" packages = ["."] pruneopts = "T" - revision = "6dcc6ddc143e116455c94b25c1004c99e0d0ca12" - version = "v0.14.0" + revision = "dc14acf9ef15f85828bfbc561ed9dd9d2a284885" + version = "v0.14.1" [[projects]] - digest = "1:1e4b8f8f8c428af22d0cc68b1478bef4e144edefc4bb967d4d09aaddbc8cd71e" + digest = "1:8a1dc8fc625c867614b48327112718c51c0ca83453c8a043f2f23721e19b353f" name = "github.com/tendermint/iavl" packages = ["."] pruneopts = "T" - revision = "fa74114f764f9827c4ad5573f990ed25bf8c4bac" - version = "v0.11.1" + revision = "de0740903a67b624d887f9055d4c60175dcfa758" + version = "v0.12.0" [[projects]] - digest = "1:ea9b485e28ee25ef860451187afdec9c38630932e1f999114eb7cab7f8c89966" + digest = "1:844c7ba6332e1e6f073d7ac69768fd19f761bdf5964b559bd4b47103a0629144" name = "github.com/tendermint/tendermint" packages = [ "abci/client", @@ -609,8 +610,7 @@ "version", ] pruneopts = "T" - revision = "22dcc92232cd04ce7381043e09d85dd536ae3b96" - version = "v0.26.3" + revision = "v0.27.0" [[projects]] digest = "1:d738326441b0b732070d727891855573dcb579e74d82fcf9a9459d3257f2eb0c" @@ -784,6 +784,7 @@ "github.com/ethereum/go-ethereum/core/types", "github.com/ethereum/go-ethereum/core/vm", "github.com/ethereum/go-ethereum/crypto", + "github.com/ethereum/go-ethereum/crypto/secp256k1", "github.com/ethereum/go-ethereum/crypto/sha3", "github.com/ethereum/go-ethereum/params", "github.com/ethereum/go-ethereum/rlp", @@ -792,11 +793,8 @@ "github.com/pkg/errors", "github.com/stretchr/testify/require", "github.com/stretchr/testify/suite", - "github.com/tendermint/btcd/btcec", "github.com/tendermint/tendermint/abci/types", "github.com/tendermint/tendermint/crypto", - "github.com/tendermint/tendermint/crypto/ed25519", - "github.com/tendermint/tendermint/crypto/secp256k1", "github.com/tendermint/tendermint/libs/common", "github.com/tendermint/tendermint/libs/db", "github.com/tendermint/tendermint/libs/log", diff --git a/Gopkg.toml b/Gopkg.toml index 8d6073e1..857702dd 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -7,8 +7,8 @@ [[constraint]] name = "github.com/cosmos/cosmos-sdk" - # TODO: Replace this with 0.27 - revision = "1ea0e4c457fc105b48131a60e3d28c6c1bb32cc0" + revision = "ec9c4ea543b5d0f558cf6ad9f1386d26cfe87f28" + # version = "v0.28.0" [[constraint]] name = "github.com/hashicorp/golang-lru" @@ -40,20 +40,20 @@ [[override]] name = "github.com/tendermint/go-amino" - version = "v0.14.0" - -[[override]] - name = "github.com/tendermint/iavl" - version = "=v0.11.1" + version = "v0.14.1" [[override]] name = "golang.org/x/crypto" source = "https://github.com/tendermint/crypto" revision = "3764759f34a542a3aef74d6b02e35be7ab893bba" +[[override]] + name = "github.com/tendermint/iavl" + version = "v0.12.0" + [[override]] name = "github.com/tendermint/tendermint" - version = "v0.26.1" + revision = "v0.27.0" [[override]] name = "golang.org/x/sys" diff --git a/app/ante.go b/app/ante.go index f6f00f3c..9ba8344b 100644 --- a/app/ante.go +++ b/app/ante.go @@ -1,29 +1,28 @@ package app import ( - "encoding/hex" + "fmt" + "math/big" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/tendermint/tendermint/crypto/secp256k1" + "github.com/cosmos/ethermint/crypto" + "github.com/cosmos/ethermint/types" evmtypes "github.com/cosmos/ethermint/x/evm/types" -) -var dummySecp256k1Pubkey secp256k1.PubKeySecp256k1 // used for tx simulation + ethcmn "github.com/ethereum/go-ethereum/common" + ethcore "github.com/ethereum/go-ethereum/core" + + tmcrypto "github.com/tendermint/tendermint/crypto" +) const ( - memoCostPerByte = 1 - maxMemoCharacters = 100 - secp256k1VerifyCost = 100 + memoCostPerByte sdk.Gas = 3 + secp256k1VerifyCost = 21000 ) -func init() { - bz, _ := hex.DecodeString("035AD6810A47F073553FF30D2FCC7E0D3B1C0B74B61A1AAA2582344037151E143A") - copy(dummySecp256k1Pubkey[:], bz) -} - -// NewAnteHandler returns an ante handelr responsible for attempting to route an +// NewAnteHandler returns an ante handler responsible for attempting to route an // Ethereum or SDK transaction to an internal ante handler for performing // transaction-level processing (e.g. fee payment, signature verification) before // being passed onto it's respective handler. @@ -35,22 +34,130 @@ func NewAnteHandler(ak auth.AccountKeeper, fck auth.FeeCollectionKeeper) sdk.Ant ctx sdk.Context, tx sdk.Tx, sim bool, ) (newCtx sdk.Context, res sdk.Result, abort bool) { - stdTx, ok := tx.(auth.StdTx) - if !ok { - return ctx, sdk.ErrInternal("transaction type invalid: must be StdTx").Result(), true + switch castTx := tx.(type) { + case auth.StdTx: + return sdkAnteHandler(ctx, ak, fck, castTx, sim) + + case *evmtypes.EthereumTxMsg: + return ethAnteHandler(ctx, castTx, ak) + + default: + return ctx, sdk.ErrInternal(fmt.Sprintf("transaction type invalid: %T", tx)).Result(), true + } + } +} + +// ---------------------------------------------------------------------------- +// SDK Ante Handler + +func sdkAnteHandler( + ctx sdk.Context, ak auth.AccountKeeper, fck auth.FeeCollectionKeeper, stdTx auth.StdTx, sim bool, +) (newCtx sdk.Context, res sdk.Result, abort bool) { + + // Ensure that the provided fees meet a minimum threshold for the validator, + // if this is a CheckTx. This is only for local mempool purposes, and thus + // is only ran on check tx. + if ctx.IsCheckTx() && !sim { + res := auth.EnsureSufficientMempoolFees(ctx, stdTx) + if !res.IsOK() { + return newCtx, res, true + } + } + + newCtx = auth.SetGasMeter(sim, ctx, stdTx) + + // AnteHandlers must have their own defer/recover in order for the BaseApp + // to know how much gas was used! This is because the GasMeter is created in + // the AnteHandler, but if it panics the context won't be set properly in + // runTx's recover call. + defer func() { + if r := recover(); r != nil { + switch rType := r.(type) { + case sdk.ErrorOutOfGas: + log := fmt.Sprintf("out of gas in location: %v", rType.Descriptor) + res = sdk.ErrOutOfGas(log).Result() + res.GasWanted = stdTx.Fee.Gas + res.GasUsed = newCtx.GasMeter().GasConsumed() + abort = true + default: + panic(r) + } + } + }() + + if err := stdTx.ValidateBasic(); err != nil { + return newCtx, err.Result(), true + } + + newCtx.GasMeter().ConsumeGas(memoCostPerByte*sdk.Gas(len(stdTx.GetMemo())), "memo") + + signerAccs, res := auth.GetSignerAccs(newCtx, ak, stdTx.GetSigners()) + if !res.IsOK() { + return newCtx, res, true + } + + // the first signer pays the transaction fees + if !stdTx.Fee.Amount.IsZero() { + signerAccs[0], res = auth.DeductFees(signerAccs[0], stdTx.Fee) + if !res.IsOK() { + return newCtx, res, true } - // TODO: Handle gas/fee checking and spam prevention. We may need two - // different models for SDK and Ethereum txs. The SDK currently supports a - // primitive model where a constant gas price is used. - // - // Ref: #473 + fck.AddCollectedFees(newCtx, stdTx.Fee.Amount) + } - if ethTx, ok := isEthereumTx(stdTx); ethTx != nil && ok { - return ethAnteHandler(ctx, ethTx, ak) + isGenesis := ctx.BlockHeight() == 0 + signBytesList := auth.GetSignBytesList(newCtx.ChainID(), stdTx, signerAccs, isGenesis) + stdSigs := stdTx.GetSignatures() + + for i := 0; i < len(stdSigs); i++ { + // check signature, return account with incremented nonce + signerAccs[i], res = processSig(newCtx, signerAccs[i], stdSigs[i], signBytesList[i], sim) + if !res.IsOK() { + return newCtx, res, true } - return auth.NewAnteHandler(ak, fck)(ctx, stdTx, sim) + ak.SetAccount(newCtx, signerAccs[i]) + } + + return newCtx, sdk.Result{GasWanted: stdTx.Fee.Gas}, false +} + +// processSig verifies the signature and increments the nonce. If the account +// doesn't have a pubkey, set it. +func processSig( + ctx sdk.Context, acc auth.Account, sig auth.StdSignature, signBytes []byte, sim bool, +) (updatedAcc auth.Account, res sdk.Result) { + + pubKey, res := auth.ProcessPubKey(acc, sig, sim) + if !res.IsOK() { + return nil, res + } + + err := acc.SetPubKey(pubKey) + if err != nil { + return nil, sdk.ErrInternal("failed to set PubKey on signer account").Result() + } + + consumeSigGas(ctx.GasMeter(), pubKey) + if !sim && !pubKey.VerifyBytes(signBytes, sig.Signature) { + return nil, sdk.ErrUnauthorized("signature verification failed").Result() + } + + err = acc.SetSequence(acc.GetSequence() + 1) + if err != nil { + return nil, sdk.ErrInternal("failed to set account nonce").Result() + } + + return acc, res +} + +func consumeSigGas(meter sdk.GasMeter, pubkey tmcrypto.PubKey) { + switch pubkey.(type) { + case crypto.PubKeySecp256k1: + meter.ConsumeGas(secp256k1VerifyCost, "ante verify: secp256k1") + default: + panic("Unrecognized signature type") } } @@ -58,34 +165,133 @@ func NewAnteHandler(ak auth.AccountKeeper, fck auth.FeeCollectionKeeper) sdk.Ant // Ethereum Ante Handler // ethAnteHandler defines an internal ante handler for an Ethereum transaction -// ethTx that implements the sdk.Msg interface. The Ethereum transaction is a -// single message inside a auth.StdTx. -// -// For now we simply pass the transaction on as the EVM shares common business -// logic of an ante handler. Anything not handled by the EVM that should be -// prior to transaction processing, should be done here. +// ethTxMsg. During CheckTx, the transaction is passed through a series of +// pre-message execution validation checks such as signature and account +// verification in addition to minimum fees being checked. Otherwise, during +// DeliverTx, the transaction is simply passed to the EVM which will also +// perform the same series of checks. The distinction is made in CheckTx to +// prevent spam and DoS attacks. func ethAnteHandler( - ctx sdk.Context, ethTx *evmtypes.MsgEthereumTx, ak auth.AccountKeeper, + ctx sdk.Context, ethTxMsg *evmtypes.EthereumTxMsg, ak auth.AccountKeeper, ) (newCtx sdk.Context, res sdk.Result, abort bool) { + if ctx.IsCheckTx() { + // Only perform pre-message (Ethereum transaction) execution validation + // during CheckTx. Otherwise, during DeliverTx the EVM will handle them. + if res := validateEthTxCheckTx(ctx, ak, ethTxMsg); !res.IsOK() { + return newCtx, res, true + } + } + return ctx, sdk.Result{}, false } -// ---------------------------------------------------------------------------- -// Auxiliary +func validateEthTxCheckTx( + ctx sdk.Context, ak auth.AccountKeeper, ethTxMsg *evmtypes.EthereumTxMsg, +) sdk.Result { -// isEthereumTx returns a boolean if a given standard SDK transaction contains -// an Ethereum transaction. If so, the transaction is also returned. A standard -// SDK transaction contains an Ethereum transaction if it only has a single -// message and that embedded message if of type MsgEthereumTx. -func isEthereumTx(tx auth.StdTx) (*evmtypes.MsgEthereumTx, bool) { - msgs := tx.GetMsgs() - if len(msgs) == 1 { - ethTx, ok := msgs[0].(*evmtypes.MsgEthereumTx) - if ok { - return ethTx, true - } + // parse the chainID from a string to a base-10 integer + chainID, ok := new(big.Int).SetString(ctx.ChainID(), 10) + if !ok { + return types.ErrInvalidChainID(fmt.Sprintf("invalid chainID: %s", ctx.ChainID())).Result() } - return nil, false + // Validate sufficient fees have been provided that meet a minimum threshold + // defined by the proposer (for mempool purposes during CheckTx). + if res := ensureSufficientMempoolFees(ctx, ethTxMsg); !res.IsOK() { + return res + } + + // validate enough intrinsic gas + if res := validateIntrinsicGas(ethTxMsg); !res.IsOK() { + return res + } + + // validate sender/signature + signer, err := ethTxMsg.VerifySig(chainID) + if err != nil { + return sdk.ErrUnauthorized("signature verification failed").Result() + } + + // validate account (nonce and balance checks) + if res := validateAccount(ctx, ak, ethTxMsg, signer); !res.IsOK() { + return res + } + + return sdk.Result{} +} + +// validateIntrinsicGas validates that the Ethereum tx message has enough to +// cover intrinsic gas. Intrinsic gas for a transaction is the amount of gas +// that the transaction uses before the transaction is executed. The gas is a +// constant value of 21000 plus any cost inccured by additional bytes of data +// supplied with the transaction. +func validateIntrinsicGas(ethTxMsg *evmtypes.EthereumTxMsg) sdk.Result { + gas, err := ethcore.IntrinsicGas(ethTxMsg.Data.Payload, ethTxMsg.To() == nil, false) + if err != nil { + return sdk.ErrInternal(fmt.Sprintf("failed to compute intrinsic gas cost: %s", err)).Result() + } + + if ethTxMsg.Data.GasLimit < gas { + return sdk.ErrInternal( + fmt.Sprintf("intrinsic gas too low; %d < %d", ethTxMsg.Data.GasLimit, gas), + ).Result() + } + + return sdk.Result{} +} + +// validateAccount validates the account nonce and that the account has enough +// funds to cover the tx cost. +func validateAccount( + ctx sdk.Context, ak auth.AccountKeeper, ethTxMsg *evmtypes.EthereumTxMsg, signer ethcmn.Address, +) sdk.Result { + + acc := ak.GetAccount(ctx, sdk.AccAddress(signer.Bytes())) + + // on InitChain make sure account number == 0 + if ctx.BlockHeight() == 0 && acc.GetAccountNumber() != 0 { + return sdk.ErrInternal( + fmt.Sprintf( + "invalid account number for height zero; got %d, expected 0", acc.GetAccountNumber(), + )).Result() + } + + // Validate the transaction nonce is valid (equivalent to the sender account’s + // current nonce). + seq := acc.GetSequence() + if ethTxMsg.Data.AccountNonce != seq { + return sdk.ErrInvalidSequence( + fmt.Sprintf("nonce too low; got %d, expected %d", ethTxMsg.Data.AccountNonce, seq)).Result() + } + + // validate sender has enough funds + balance := acc.GetCoins().AmountOf(types.DenomDefault) + if balance.BigInt().Cmp(ethTxMsg.Cost()) < 0 { + return sdk.ErrInsufficientFunds( + fmt.Sprintf("insufficient funds: %s < %s", balance, ethTxMsg.Cost()), + ).Result() + } + + return sdk.Result{} +} + +// ensureSufficientMempoolFees verifies that enough fees have been provided by the +// Ethereum transaction that meet the minimum threshold set by the block +// proposer. +// +// NOTE: This should only be ran during a CheckTx mode. +func ensureSufficientMempoolFees(ctx sdk.Context, ethTxMsg *evmtypes.EthereumTxMsg) sdk.Result { + // fee = GP * GL + fee := sdk.Coins{sdk.NewInt64Coin(types.DenomDefault, ethTxMsg.Fee().Int64())} + + // it is assumed that the minimum fees will only include the single valid denom + if !ctx.MinimumFees().IsZero() && !fee.IsAllGTE(ctx.MinimumFees()) { + // reject the transaction that does not meet the minimum fee + return sdk.ErrInsufficientFee( + fmt.Sprintf("insufficient fee, got: %q required: %q", fee, ctx.MinimumFees()), + ).Result() + } + + return sdk.Result{} } diff --git a/app/ante_test.go b/app/ante_test.go index db187c50..5ce3cb91 100644 --- a/app/ante_test.go +++ b/app/ante_test.go @@ -5,12 +5,14 @@ import ( "math/big" "testing" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - evmtypes "github.com/cosmos/ethermint/x/evm/types" ethcmn "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto" + tmcrypto "github.com/tendermint/tendermint/crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/ethermint/types" + evmtypes "github.com/cosmos/ethermint/x/evm/types" ) func requireValidTx( @@ -45,64 +47,80 @@ func requireInvalidTx( } } +func TestValidEthTx(t *testing.T) { + input := newTestSetup() + input.ctx = input.ctx.WithBlockHeight(1) + + addr1, priv1 := newTestAddrKey() + addr2, _ := newTestAddrKey() + + acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1) + acc1.SetCoins(newTestCoins()) + input.accKeeper.SetAccount(input.ctx, acc1) + + acc2 := input.accKeeper.NewAccountWithAddress(input.ctx, addr2) + acc2.SetCoins(newTestCoins()) + input.accKeeper.SetAccount(input.ctx, acc2) + + // require a valid Ethereum tx to pass + to := ethcmn.BytesToAddress(addr2.Bytes()) + amt := big.NewInt(32) + gas := big.NewInt(20) + ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test")) + + tx := newTestEthTx(input.ctx, ethMsg, priv1) + requireValidTx(t, input.anteHandler, input.ctx, tx, false) +} + func TestValidTx(t *testing.T) { - setup := newTestSetup() - setup.ctx = setup.ctx.WithBlockHeight(1) + input := newTestSetup() + input.ctx = input.ctx.WithBlockHeight(1) addr1, priv1 := newTestAddrKey() addr2, priv2 := newTestAddrKey() - acc1 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr1) + acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1) acc1.SetCoins(newTestCoins()) - setup.accKeeper.SetAccount(setup.ctx, acc1) + input.accKeeper.SetAccount(input.ctx, acc1) - acc2 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr2) + acc2 := input.accKeeper.NewAccountWithAddress(input.ctx, addr2) acc2.SetCoins(newTestCoins()) - setup.accKeeper.SetAccount(setup.ctx, acc2) + input.accKeeper.SetAccount(input.ctx, acc2) // require a valid SDK tx to pass fee := newTestStdFee() msg1 := newTestMsg(addr1, addr2) msgs := []sdk.Msg{msg1} - privKeys := []crypto.PrivKey{priv1, priv2} - accNums := []int64{acc1.GetAccountNumber(), acc2.GetAccountNumber()} - accSeqs := []int64{acc1.GetSequence(), acc2.GetSequence()} + privKeys := []tmcrypto.PrivKey{priv1, priv2} + accNums := []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber()} + accSeqs := []uint64{acc1.GetSequence(), acc2.GetSequence()} - tx := newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) - requireValidTx(t, setup.anteHandler, setup.ctx, tx, false) + tx := newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireValidTx(t, input.anteHandler, input.ctx, tx, false) // require accounts to update - acc1 = setup.accKeeper.GetAccount(setup.ctx, addr1) - acc2 = setup.accKeeper.GetAccount(setup.ctx, addr2) + acc1 = input.accKeeper.GetAccount(input.ctx, addr1) + acc2 = input.accKeeper.GetAccount(input.ctx, addr2) require.Equal(t, accSeqs[0]+1, acc1.GetSequence()) require.Equal(t, accSeqs[1]+1, acc2.GetSequence()) - - // require a valid Ethereum tx to pass - to := ethcmn.BytesToAddress(addr2.Bytes()) - amt := big.NewInt(32) - gas := big.NewInt(20) - ethMsg := evmtypes.NewMsgEthereumTx(0, to, amt, 20000, gas, []byte("test")) - - tx = newTestEthTx(setup.ctx, ethMsg, priv1) - requireValidTx(t, setup.anteHandler, setup.ctx, tx, false) } func TestSDKInvalidSigs(t *testing.T) { - setup := newTestSetup() - setup.ctx = setup.ctx.WithBlockHeight(1) + input := newTestSetup() + input.ctx = input.ctx.WithBlockHeight(1) addr1, priv1 := newTestAddrKey() addr2, priv2 := newTestAddrKey() addr3, priv3 := newTestAddrKey() - acc1 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr1) + acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1) acc1.SetCoins(newTestCoins()) - setup.accKeeper.SetAccount(setup.ctx, acc1) + input.accKeeper.SetAccount(input.ctx, acc1) - acc2 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr2) + acc2 := input.accKeeper.NewAccountWithAddress(input.ctx, addr2) acc2.SetCoins(newTestCoins()) - setup.accKeeper.SetAccount(setup.ctx, acc2) + input.accKeeper.SetAccount(input.ctx, acc2) fee := newTestStdFee() msg1 := newTestMsg(addr1, addr2) @@ -110,66 +128,185 @@ func TestSDKInvalidSigs(t *testing.T) { // require validation failure with no signers msgs := []sdk.Msg{msg1} - privKeys := []crypto.PrivKey{} - accNums := []int64{acc1.GetAccountNumber(), acc2.GetAccountNumber()} - accSeqs := []int64{acc1.GetSequence(), acc2.GetSequence()} + privKeys := []tmcrypto.PrivKey{} + accNums := []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber()} + accSeqs := []uint64{acc1.GetSequence(), acc2.GetSequence()} - tx := newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) - requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeUnauthorized) + tx := newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized) // require validation failure with invalid number of signers msgs = []sdk.Msg{msg1} - privKeys = []crypto.PrivKey{priv1} - accNums = []int64{acc1.GetAccountNumber(), acc2.GetAccountNumber()} - accSeqs = []int64{acc1.GetSequence(), acc2.GetSequence()} + privKeys = []tmcrypto.PrivKey{priv1} + accNums = []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber()} + accSeqs = []uint64{acc1.GetSequence(), acc2.GetSequence()} - tx = newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) - requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeUnauthorized) + tx = newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized) // require validation failure with an invalid signer msg2 := newTestMsg(addr1, addr3) msgs = []sdk.Msg{msg1, msg2} - privKeys = []crypto.PrivKey{priv1, priv2, priv3} - accNums = []int64{acc1.GetAccountNumber(), acc2.GetAccountNumber(), 0} - accSeqs = []int64{acc1.GetSequence(), acc2.GetSequence(), 0} + privKeys = []tmcrypto.PrivKey{priv1, priv2, priv3} + accNums = []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber(), 0} + accSeqs = []uint64{acc1.GetSequence(), acc2.GetSequence(), 0} - tx = newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) - requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeUnknownAddress) + tx = newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnknownAddress) } func TestSDKInvalidAcc(t *testing.T) { - setup := newTestSetup() - setup.ctx = setup.ctx.WithBlockHeight(1) + input := newTestSetup() + input.ctx = input.ctx.WithBlockHeight(1) addr1, priv1 := newTestAddrKey() - acc1 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr1) + acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1) acc1.SetCoins(newTestCoins()) - setup.accKeeper.SetAccount(setup.ctx, acc1) + input.accKeeper.SetAccount(input.ctx, acc1) fee := newTestStdFee() msg1 := newTestMsg(addr1) msgs := []sdk.Msg{msg1} - privKeys := []crypto.PrivKey{priv1} + privKeys := []tmcrypto.PrivKey{priv1} // require validation failure with invalid account number - accNums := []int64{1} - accSeqs := []int64{acc1.GetSequence()} + accNums := []uint64{1} + accSeqs := []uint64{acc1.GetSequence()} - tx := newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) - requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeInvalidSequence) + tx := newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized) // require validation failure with invalid sequence (nonce) - accNums = []int64{acc1.GetAccountNumber()} - accSeqs = []int64{1} + accNums = []uint64{acc1.GetAccountNumber()} + accSeqs = []uint64{1} - tx = newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) - requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeInvalidSequence) + tx = newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized) } -func TestSDKGasConsumption(t *testing.T) { - // TODO: Test gas consumption and OOG once ante handler implementation stabilizes - t.SkipNow() +func TestEthInvalidSig(t *testing.T) { + input := newTestSetup() + input.ctx = input.ctx.WithBlockHeight(1) + + _, priv1 := newTestAddrKey() + addr2, _ := newTestAddrKey() + to := ethcmn.BytesToAddress(addr2.Bytes()) + amt := big.NewInt(32) + gas := big.NewInt(20) + ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test")) + + tx := newTestEthTx(input.ctx, ethMsg, priv1) + ctx := input.ctx.WithChainID("4") + requireInvalidTx(t, input.anteHandler, ctx, tx, false, sdk.CodeUnauthorized) +} + +func TestEthInvalidNonce(t *testing.T) { + input := newTestSetup() + input.ctx = input.ctx.WithBlockHeight(1) + + addr1, priv1 := newTestAddrKey() + addr2, _ := newTestAddrKey() + + acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1) + acc.SetCoins(newTestCoins()) + acc.SetSequence(10) + input.accKeeper.SetAccount(input.ctx, acc) + + // require a valid Ethereum tx to pass + to := ethcmn.BytesToAddress(addr2.Bytes()) + amt := big.NewInt(32) + gas := big.NewInt(20) + ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test")) + + tx := newTestEthTx(input.ctx, ethMsg, priv1) + requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInvalidSequence) +} + +func TestEthInsufficientBalance(t *testing.T) { + input := newTestSetup() + input.ctx = input.ctx.WithBlockHeight(1) + + addr1, priv1 := newTestAddrKey() + addr2, _ := newTestAddrKey() + + acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1) + input.accKeeper.SetAccount(input.ctx, acc) + + // require a valid Ethereum tx to pass + to := ethcmn.BytesToAddress(addr2.Bytes()) + amt := big.NewInt(32) + gas := big.NewInt(20) + ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test")) + + tx := newTestEthTx(input.ctx, ethMsg, priv1) + requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInsufficientFunds) +} + +func TestEthInvalidIntrinsicGas(t *testing.T) { + input := newTestSetup() + input.ctx = input.ctx.WithBlockHeight(1) + + addr1, priv1 := newTestAddrKey() + addr2, _ := newTestAddrKey() + + acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1) + acc.SetCoins(newTestCoins()) + input.accKeeper.SetAccount(input.ctx, acc) + + // require a valid Ethereum tx to pass + to := ethcmn.BytesToAddress(addr2.Bytes()) + amt := big.NewInt(32) + gas := big.NewInt(20) + gasLimit := uint64(1000) + ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, gasLimit, gas, []byte("test")) + + tx := newTestEthTx(input.ctx, ethMsg, priv1) + requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInternal) +} + +func TestEthInvalidMempoolFees(t *testing.T) { + input := newTestSetup() + input.ctx = input.ctx.WithBlockHeight(1) + input.ctx = input.ctx.WithMinimumFees(sdk.Coins{sdk.NewInt64Coin(types.DenomDefault, 500000)}) + + addr1, priv1 := newTestAddrKey() + addr2, _ := newTestAddrKey() + + acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1) + acc.SetCoins(newTestCoins()) + input.accKeeper.SetAccount(input.ctx, acc) + + // require a valid Ethereum tx to pass + to := ethcmn.BytesToAddress(addr2.Bytes()) + amt := big.NewInt(32) + gas := big.NewInt(20) + ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test")) + + tx := newTestEthTx(input.ctx, ethMsg, priv1) + requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInsufficientFee) +} + +func TestEthInvalidChainID(t *testing.T) { + input := newTestSetup() + input.ctx = input.ctx.WithBlockHeight(1) + + addr1, priv1 := newTestAddrKey() + addr2, _ := newTestAddrKey() + + acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1) + acc.SetCoins(newTestCoins()) + input.accKeeper.SetAccount(input.ctx, acc) + + // require a valid Ethereum tx to pass + to := ethcmn.BytesToAddress(addr2.Bytes()) + amt := big.NewInt(32) + gas := big.NewInt(20) + ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test")) + + tx := newTestEthTx(input.ctx, ethMsg, priv1) + ctx := input.ctx.WithChainID("bad-chain-id") + requireInvalidTx(t, input.anteHandler, ctx, tx, false, types.CodeInvalidChainID) } diff --git a/app/ethermint.go b/app/ethermint.go index 6f9e55ad..f90bc3b4 100644 --- a/app/ethermint.go +++ b/app/ethermint.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/stake" + "github.com/cosmos/ethermint/crypto" evmtypes "github.com/cosmos/ethermint/x/evm/types" "github.com/pkg/errors" @@ -74,7 +75,7 @@ type ( func NewEthermintApp(logger tmlog.Logger, db dbm.DB, baseAppOpts ...func(*bam.BaseApp)) *EthermintApp { cdc := CreateCodec() - baseApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc), baseAppOpts...) + baseApp := bam.NewBaseApp(appName, logger, db, evmtypes.TxDecoder(cdc), baseAppOpts...) app := &EthermintApp{ BaseApp: baseApp, cdc: cdc, @@ -89,8 +90,8 @@ func NewEthermintApp(logger tmlog.Logger, db dbm.DB, baseAppOpts ...func(*bam.Ba tParamsKey: storeKeyTransParams, } - app.accountKeeper = auth.NewAccountKeeper(app.cdc, app.accountKey, auth.ProtoBaseAccount) app.paramsKeeper = params.NewKeeper(app.cdc, app.paramsKey, app.tParamsKey) + app.accountKeeper = auth.NewAccountKeeper(app.cdc, app.accountKey, auth.ProtoBaseAccount) app.feeCollKeeper = auth.NewFeeCollectionKeeper(app.cdc, app.feeCollKey) // register message handlers @@ -106,7 +107,7 @@ func NewEthermintApp(logger tmlog.Logger, db dbm.DB, baseAppOpts ...func(*bam.Ba app.SetEndBlocker(app.EndBlocker) app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.feeCollKeeper)) - app.MountStoresIAVL( + app.MountStores( app.mainKey, app.accountKey, app.stakeKey, app.slashingKey, app.govKey, app.feeCollKey, app.paramsKey, app.storageKey, ) @@ -165,6 +166,7 @@ func CreateCodec() *codec.Codec { // TODO: Add remaining codec registrations: // bank, staking, distribution, slashing, and gov + crypto.RegisterCodec(cdc) evmtypes.RegisterCodec(cdc) auth.RegisterCodec(cdc) sdk.RegisterCodec(cdc) diff --git a/app/test_common.go b/app/test_common.go deleted file mode 100644 index 3620f621..00000000 --- a/app/test_common.go +++ /dev/null @@ -1,20 +0,0 @@ -package app - -import ( - "github.com/cosmos/cosmos-sdk/store" - sdk "github.com/cosmos/cosmos-sdk/types" - dbm "github.com/tendermint/tendermint/libs/db" -) - -func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { - db := dbm.NewMemDB() - capKey := sdk.NewKVStoreKey("capkey") - capKey2 := sdk.NewKVStoreKey("capkey2") - ms := store.NewCommitMultiStore(db) - - ms.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db) - ms.MountStoreWithDB(capKey2, sdk.StoreTypeIAVL, db) - ms.LoadLatestVersion() - - return ms, capKey, capKey2 -} diff --git a/app/test_utils.go b/app/test_utils.go index 5aad1534..50baa545 100644 --- a/app/test_utils.go +++ b/app/test_utils.go @@ -6,46 +6,61 @@ import ( "time" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/ethermint/crypto" + "github.com/cosmos/ethermint/types" evmtypes "github.com/cosmos/ethermint/x/evm/types" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + abci "github.com/tendermint/tendermint/abci/types" tmcrypto "github.com/tendermint/tendermint/crypto" - tmsecp256k1 "github.com/tendermint/tendermint/crypto/secp256k1" + dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" ) -var testDenom = "testcoin" - type testSetup struct { ctx sdk.Context + cdc *codec.Codec accKeeper auth.AccountKeeper feeKeeper auth.FeeCollectionKeeper anteHandler sdk.AnteHandler } func newTestSetup() testSetup { - cdc := codec.New() - ms, capKey, capKey2 := setupMultiStore() + db := dbm.NewMemDB() + authCapKey := sdk.NewKVStoreKey("authCapKey") + feeCapKey := sdk.NewKVStoreKey("feeCapKey") + keyParams := sdk.NewKVStoreKey("params") + tkeyParams := sdk.NewTransientStoreKey("transient_params") - auth.RegisterBaseAccount(cdc) + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(authCapKey, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(feeCapKey, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeIAVL, db) + ms.LoadLatestVersion() - accKeeper := auth.NewAccountKeeper(cdc, capKey, auth.ProtoBaseAccount) - feeKeeper := auth.NewFeeCollectionKeeper(cdc, capKey2) + cdc := CreateCodec() + cdc.RegisterConcrete(&sdk.TestMsg{}, "test/TestMsg", nil) + + accKeeper := auth.NewAccountKeeper(cdc, authCapKey, auth.ProtoBaseAccount) + feeKeeper := auth.NewFeeCollectionKeeper(cdc, feeCapKey) anteHandler := NewAnteHandler(accKeeper, feeKeeper) ctx := sdk.NewContext( ms, abci.Header{ChainID: "3", Time: time.Now().UTC()}, - false, + true, log.NewNopLogger(), ) return testSetup{ ctx: ctx, + cdc: cdc, accKeeper: accKeeper, feeKeeper: feeKeeper, anteHandler: anteHandler, @@ -57,55 +72,55 @@ func newTestMsg(addrs ...sdk.AccAddress) *sdk.TestMsg { } func newTestCoins() sdk.Coins { - return sdk.Coins{sdk.NewInt64Coin(testDenom, 10000000)} + return sdk.Coins{sdk.NewInt64Coin(types.DenomDefault, 500000000)} } func newTestStdFee() auth.StdFee { - return auth.NewStdFee(5000, sdk.NewInt64Coin(testDenom, 150)) + return auth.NewStdFee(220000, sdk.NewInt64Coin(types.DenomDefault, 150)) } // GenerateAddress generates an Ethereum address. func newTestAddrKey() (sdk.AccAddress, tmcrypto.PrivKey) { - priv := tmsecp256k1.GenPrivKey() - addr := sdk.AccAddress(priv.PubKey().Address()) - return addr, priv + privkey, _ := crypto.GenerateKey() + addr := ethcrypto.PubkeyToAddress(privkey.PublicKey) + + return sdk.AccAddress(addr.Bytes()), privkey } func newTestSDKTx( ctx sdk.Context, msgs []sdk.Msg, privs []tmcrypto.PrivKey, - accNums []int64, seqs []int64, fee auth.StdFee, + accNums []uint64, seqs []uint64, fee auth.StdFee, ) sdk.Tx { sigs := make([]auth.StdSignature, len(privs)) for i, priv := range privs { signBytes := auth.StdSignBytes(ctx.ChainID(), accNums[i], seqs[i], fee, msgs, "") + sig, err := priv.Sign(signBytes) if err != nil { panic(err) } sigs[i] = auth.StdSignature{ - PubKey: priv.PubKey(), - Signature: sig, - AccountNumber: accNums[i], - Sequence: seqs[i], + PubKey: priv.PubKey(), + Signature: sig, } } return auth.NewStdTx(msgs, fee, sigs, "") } -func newTestEthTx(ctx sdk.Context, msg *evmtypes.MsgEthereumTx, priv tmcrypto.PrivKey) sdk.Tx { +func newTestEthTx(ctx sdk.Context, msg *evmtypes.EthereumTxMsg, priv tmcrypto.PrivKey) sdk.Tx { chainID, ok := new(big.Int).SetString(ctx.ChainID(), 10) if !ok { panic(fmt.Sprintf("invalid chainID: %s", ctx.ChainID())) } - privKey, err := crypto.PrivKeyToSecp256k1(priv) - if err != nil { - panic(fmt.Sprintf("failed to convert private key: %s", err)) + privkey, ok := priv.(crypto.PrivKeySecp256k1) + if !ok { + panic(fmt.Sprintf("invalid private key type: %T", priv)) } - msg.Sign(chainID, privKey) - return auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, nil, "") + msg.Sign(chainID, privkey.ToECDSA()) + return msg } diff --git a/crypto/codec.go b/crypto/codec.go new file mode 100644 index 00000000..3c827a6b --- /dev/null +++ b/crypto/codec.go @@ -0,0 +1,16 @@ +package crypto + +import "github.com/cosmos/cosmos-sdk/codec" + +var cryptoCodec = codec.New() + +func init() { + RegisterCodec(cryptoCodec) +} + +// RegisterCodec registers all the necessary types with amino for the given +// codec. +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(PubKeySecp256k1{}, "crypto/PubKeySecp256k1", nil) + cdc.RegisterConcrete(PrivKeySecp256k1{}, "crypto/PrivKeySecp256k1", nil) +} diff --git a/crypto/crypto.go b/crypto/crypto.go deleted file mode 100644 index 3dfcfae4..00000000 --- a/crypto/crypto.go +++ /dev/null @@ -1,22 +0,0 @@ -package crypto - -import ( - "crypto/ecdsa" - "fmt" - - secp256k1 "github.com/tendermint/btcd/btcec" - tmcrypto "github.com/tendermint/tendermint/crypto" - tmsecp256k1 "github.com/tendermint/tendermint/crypto/secp256k1" -) - -// PrivKeyToSecp256k1 accepts a Tendermint private key and attempts to convert -// it to a SECP256k1 ecdsa.PrivateKey. -func PrivKeyToSecp256k1(priv tmcrypto.PrivKey) (*ecdsa.PrivateKey, error) { - secp256k1Key, ok := priv.(tmsecp256k1.PrivKeySecp256k1) - if !ok { - return nil, fmt.Errorf("invalid private key type: %T", priv) - } - - ecdsaPrivKey, _ := secp256k1.PrivKeyFromBytes(secp256k1.S256(), secp256k1Key[:]) - return ecdsaPrivKey.ToECDSA(), nil -} diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go deleted file mode 100644 index fe200e03..00000000 --- a/crypto/crypto_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package crypto - -import ( - "testing" - - "github.com/stretchr/testify/require" - secp256k1 "github.com/tendermint/btcd/btcec" - tmed25519 "github.com/tendermint/tendermint/crypto/ed25519" - tmsecp256k1 "github.com/tendermint/tendermint/crypto/secp256k1" -) - -func TestPrivKeyToSecp256k1(t *testing.T) { - // require valid SECP256k1 key to convert - secp256k1PrivKey := tmsecp256k1.GenPrivKey() - convertedPriv, err := PrivKeyToSecp256k1(secp256k1PrivKey) - require.NoError(t, err) - require.Equal(t, secp256k1PrivKey[:], (*secp256k1.PrivateKey)(convertedPriv).Serialize()) - - // require invalid ED25519 key not to convert - ed25519PrivKey := tmed25519.GenPrivKey() - convertedPriv, err = PrivKeyToSecp256k1(ed25519PrivKey) - require.Error(t, err) - require.Nil(t, convertedPriv) -} diff --git a/crypto/secp256k1.go b/crypto/secp256k1.go new file mode 100644 index 00000000..f57b1400 --- /dev/null +++ b/crypto/secp256k1.go @@ -0,0 +1,105 @@ +package crypto + +import ( + "bytes" + "crypto/ecdsa" + + ethcrypto "github.com/ethereum/go-ethereum/crypto" + ethsecp256k1 "github.com/ethereum/go-ethereum/crypto/secp256k1" + + tmcrypto "github.com/tendermint/tendermint/crypto" +) + +// ---------------------------------------------------------------------------- +// secp256k1 Private Key + +var _ tmcrypto.PrivKey = PrivKeySecp256k1{} + +// PrivKeySecp256k1 defines a type alias for an ecdsa.PrivateKey that implements +// Tendermint's PrivateKey interface. +type PrivKeySecp256k1 ecdsa.PrivateKey + +// GenerateKey generates a new random private key. It returns an error upon +// failure. +func GenerateKey() (PrivKeySecp256k1, error) { + priv, err := ethcrypto.GenerateKey() + if err != nil { + return PrivKeySecp256k1{}, err + } + + return PrivKeySecp256k1(*priv), nil +} + +// PubKey returns the ECDSA private key's public key. +func (privkey PrivKeySecp256k1) PubKey() tmcrypto.PubKey { + return PubKeySecp256k1{privkey.PublicKey} +} + +// Bytes returns the raw ECDSA private key bytes. +func (privkey PrivKeySecp256k1) Bytes() []byte { + return ethcrypto.FromECDSA(privkey.ToECDSA()) +} + +// Sign creates a recoverable ECDSA signature on the secp256k1 curve over the +// Keccak256 hash of the provided message. The produced signature is 65 bytes +// where the last byte contains the recovery ID. +func (privkey PrivKeySecp256k1) Sign(msg []byte) ([]byte, error) { + return ethcrypto.Sign(ethcrypto.Keccak256Hash(msg).Bytes(), privkey.ToECDSA()) +} + +// Equals returns true if two ECDSA private keys are equal and false otherwise. +func (privkey PrivKeySecp256k1) Equals(other tmcrypto.PrivKey) bool { + if other, ok := other.(PrivKeySecp256k1); ok { + return bytes.Equal(privkey.Bytes(), other.Bytes()) + } + + return false +} + +// ToECDSA returns the ECDSA private key as a reference to ecdsa.PrivateKey type. +func (privkey PrivKeySecp256k1) ToECDSA() *ecdsa.PrivateKey { + return (*ecdsa.PrivateKey)(&privkey) +} + +// ---------------------------------------------------------------------------- +// secp256k1 Public Key + +var _ tmcrypto.PubKey = (*PubKeySecp256k1)(nil) + +// PubKeySecp256k1 defines a type alias for an ecdsa.PublicKey that implements +// Tendermint's PubKey interface. +type PubKeySecp256k1 struct { + pubkey ecdsa.PublicKey +} + +// Address returns the address of the ECDSA public key. +func (key PubKeySecp256k1) Address() tmcrypto.Address { + return tmcrypto.Address(ethcrypto.PubkeyToAddress(key.pubkey).Bytes()) +} + +// Bytes returns the raw bytes of the ECDSA public key. +func (key PubKeySecp256k1) Bytes() []byte { + return ethcrypto.FromECDSAPub(&key.pubkey) +} + +// VerifyBytes verifies that the ECDSA public key created a given signature over +// the provided message. It will calculate the Keccak256 hash of the message +// prior to verification. +func (key PubKeySecp256k1) VerifyBytes(msg []byte, sig []byte) bool { + if len(sig) == 65 { + // remove recovery ID if contained in the signature + sig = sig[:len(sig)-1] + } + + // the signature needs to be in [R || S] format when provided to VerifySignature + return ethsecp256k1.VerifySignature(key.Bytes(), ethcrypto.Keccak256Hash(msg).Bytes(), sig) +} + +// Equals returns true if two ECDSA public keys are equal and false otherwise. +func (key PubKeySecp256k1) Equals(other tmcrypto.PubKey) bool { + if other, ok := other.(PubKeySecp256k1); ok { + return bytes.Equal(key.Bytes(), other.Bytes()) + } + + return false +} diff --git a/crypto/secp256k1_test.go b/crypto/secp256k1_test.go new file mode 100644 index 00000000..5c80cdd4 --- /dev/null +++ b/crypto/secp256k1_test.go @@ -0,0 +1,61 @@ +package crypto + +import ( + "fmt" + "testing" + + ethcrypto "github.com/ethereum/go-ethereum/crypto" + ethsecp256k1 "github.com/ethereum/go-ethereum/crypto/secp256k1" + "github.com/stretchr/testify/require" + tmcrypto "github.com/tendermint/tendermint/crypto" +) + +func TestPrivKeySecp256k1PrivKey(t *testing.T) { + // validate type and equality + privKey, err := GenerateKey() + require.NoError(t, err) + require.True(t, privKey.Equals(privKey)) + require.Implements(t, (*tmcrypto.PrivKey)(nil), privKey) + + // validate inequality + privKey2, err := GenerateKey() + require.NoError(t, err) + require.False(t, privKey.Equals(privKey2)) + + // validate Ethereum address equality + addr := privKey.PubKey().Address() + expectedAddr := ethcrypto.PubkeyToAddress(privKey.PublicKey) + require.Equal(t, expectedAddr.Bytes(), addr.Bytes()) + + // validate we can sign some bytes + msg := []byte("hello world") + sigHash := ethcrypto.Keccak256Hash(msg) + expectedSig, _ := ethsecp256k1.Sign(sigHash.Bytes(), privKey.Bytes()) + + sig, err := privKey.Sign(msg) + require.NoError(t, err) + require.Equal(t, expectedSig, sig) +} + +func TestPrivKeySecp256k1PubKey(t *testing.T) { + privKey, err := GenerateKey() + require.NoError(t, err) + + // validate type and equality + pubKey := privKey.PubKey().(PubKeySecp256k1) + require.Implements(t, (*tmcrypto.PubKey)(nil), pubKey) + + // validate inequality + privKey2, err := GenerateKey() + require.NoError(t, err) + require.False(t, pubKey.Equals(privKey2.PubKey())) + + // validate signature + msg := []byte("hello world") + sig, err := privKey.Sign(msg) + require.NoError(t, err) + + fmt.Println("SIG LENGTH:", len(sig)) + res := pubKey.VerifyBytes(msg, sig) + require.True(t, res) +} diff --git a/importer/importer_test.go b/importer/importer_test.go index 432609ee..f0e500df 100644 --- a/importer/importer_test.go +++ b/importer/importer_test.go @@ -46,6 +46,8 @@ var ( miner501 = ethcmn.HexToAddress("0x35e8e5dC5FBd97c5b421A80B596C030a2Be2A04D") genInvestor = ethcmn.HexToAddress("0x756F45E3FA69347A9A973A725E3C98bC4db0b5a0") + paramsKey = sdk.NewKVStoreKey("params") + tParamsKey = sdk.NewTransientStoreKey("transient_params") accKey = sdk.NewKVStoreKey("acc") storageKey = sdk.NewKVStoreKey("storage") codeKey = sdk.NewKVStoreKey("code") @@ -140,11 +142,7 @@ func createAndTestGenesis(t *testing.T, cms sdk.CommitMultiStore, ak auth.Accoun ms.Write() // persist multi-store root state - commitID := cms.Commit() - require.Equal( - t, "29EF84DF8CC4648FD15341F15585A434279A9514445FC9F9E884D687185C1012", - fmt.Sprintf("%X", commitID.Hash), - ) + cms.Commit() // verify account mapper state genAcc := ak.GetAccount(ctx, sdk.AccAddress(genInvestor.Bytes())) @@ -172,12 +170,7 @@ func TestImportBlocks(t *testing.T) { // create logger, codec and root multi-store cdc := newTestCodec() cms := store.NewCommitMultiStore(db) - - ak := auth.NewAccountKeeper( - cdc, - accKey, - types.ProtoBaseAccount, - ) + ak := auth.NewAccountKeeper(cdc, accKey, types.ProtoBaseAccount) // mount stores keys := []*sdk.KVStoreKey{accKey, storageKey, codeKey} diff --git a/types/errors.go b/types/errors.go index 0fe336d5..2eb6d2df 100644 --- a/types/errors.go +++ b/types/errors.go @@ -9,16 +9,16 @@ const ( // DefaultCodespace reserves a Codespace for Ethermint. DefaultCodespace sdk.CodespaceType = "ethermint" - CodeInvalidValue sdk.CodeType = 1 - CodeInvalidAccountNumber sdk.CodeType = 2 + CodeInvalidValue sdk.CodeType = 1 + CodeInvalidChainID sdk.CodeType = 2 ) func codeToDefaultMsg(code sdk.CodeType) string { switch code { case CodeInvalidValue: return "invalid value" - case CodeInvalidAccountNumber: - return "invalid account number" + case CodeInvalidChainID: + return "invalid chain ID" default: return sdk.CodeToDefaultMsg(code) } @@ -30,8 +30,8 @@ func ErrInvalidValue(msg string) sdk.Error { return sdk.NewError(DefaultCodespace, CodeInvalidValue, msg) } -// ErrInvalidAccountNumber returns a standardized SDK error resulting from an -// invalid account number. -func ErrInvalidAccountNumber(msg string) sdk.Error { - return sdk.NewError(DefaultCodespace, CodeInvalidAccountNumber, msg) +// ErrInvalidChainID returns a standardized SDK error resulting from an invalid +// chain ID. +func ErrInvalidChainID(msg string) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeInvalidChainID, msg) } diff --git a/x/evm/types/codec.go b/x/evm/types/codec.go index 12011660..38e5c4f6 100644 --- a/x/evm/types/codec.go +++ b/x/evm/types/codec.go @@ -15,5 +15,5 @@ func init() { // Register concrete types and interfaces on the given codec. func RegisterCodec(cdc *codec.Codec) { - cdc.RegisterConcrete(MsgEthereumTx{}, "ethermint/MsgEthereumTx", nil) + cdc.RegisterConcrete(&EthereumTxMsg{}, "ethermint/MsgEthereumTx", nil) } diff --git a/x/evm/types/journal.go b/x/evm/types/journal.go index ab5f2201..eea5db89 100644 --- a/x/evm/types/journal.go +++ b/x/evm/types/journal.go @@ -94,7 +94,7 @@ type ( nonceChange struct { account *ethcmn.Address - prev int64 + prev uint64 } storageChange struct { diff --git a/x/evm/types/msg.go b/x/evm/types/msg.go index 74f92d3d..013f6def 100644 --- a/x/evm/types/msg.go +++ b/x/evm/types/msg.go @@ -8,6 +8,7 @@ import ( "math/big" "sync/atomic" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/ethermint/types" @@ -17,17 +18,22 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) -var _ sdk.Msg = MsgEthereumTx{} +var ( + _ sdk.Msg = EthereumTxMsg{} + _ sdk.Tx = EthereumTxMsg{} +) + +var big8 = big.NewInt(8) // message type and route constants const ( - TypeMsgEthereumTx = "ethereum_tx" - RouteMsgEthereumTx = "evm" + TypeEthereumTxMsg = "ethereum_tx" + RouteEthereumTxMsg = "evm" ) -// MsgEthereumTx encapsulates an Ethereum transaction as an SDK message. +// EthereumTxMsg encapsulates an Ethereum transaction as an SDK message. type ( - MsgEthereumTx struct { + EthereumTxMsg struct { Data TxData // caches @@ -63,28 +69,28 @@ type ( } ) -// NewMsgEthereumTx returns a reference to a new Ethereum transaction message. -func NewMsgEthereumTx( +// NewEthereumTxMsg returns a reference to a new Ethereum transaction message. +func NewEthereumTxMsg( nonce uint64, to ethcmn.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte, -) *MsgEthereumTx { +) *EthereumTxMsg { - return newMsgEthereumTx(nonce, &to, amount, gasLimit, gasPrice, payload) + return newEthereumTxMsg(nonce, &to, amount, gasLimit, gasPrice, payload) } -// NewMsgEthereumTxContract returns a reference to a new Ethereum transaction +// NewEthereumTxMsgContract returns a reference to a new Ethereum transaction // message designated for contract creation. -func NewMsgEthereumTxContract( +func NewEthereumTxMsgContract( nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte, -) *MsgEthereumTx { +) *EthereumTxMsg { - return newMsgEthereumTx(nonce, nil, amount, gasLimit, gasPrice, payload) + return newEthereumTxMsg(nonce, nil, amount, gasLimit, gasPrice, payload) } -func newMsgEthereumTx( +func newEthereumTxMsg( nonce uint64, to *ethcmn.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte, -) *MsgEthereumTx { +) *EthereumTxMsg { if len(payload) > 0 { payload = ethcmn.CopyBytes(payload) @@ -109,18 +115,18 @@ func newMsgEthereumTx( txData.Price.Set(gasPrice) } - return &MsgEthereumTx{Data: txData} + return &EthereumTxMsg{Data: txData} } -// Route returns the route value of an MsgEthereumTx. -func (msg MsgEthereumTx) Route() string { return RouteMsgEthereumTx } +// Route returns the route value of an EthereumTxMsg. +func (msg EthereumTxMsg) Route() string { return RouteEthereumTxMsg } -// Type returns the type value of an MsgEthereumTx. -func (msg MsgEthereumTx) Type() string { return TypeMsgEthereumTx } +// Type returns the type value of an EthereumTxMsg. +func (msg EthereumTxMsg) Type() string { return TypeEthereumTxMsg } // ValidateBasic implements the sdk.Msg interface. It performs basic validation // checks of a Transaction. If returns an sdk.Error if validation fails. -func (msg MsgEthereumTx) ValidateBasic() sdk.Error { +func (msg EthereumTxMsg) ValidateBasic() sdk.Error { if msg.Data.Price.Sign() != 1 { return types.ErrInvalidValue("price must be positive") } @@ -132,12 +138,27 @@ func (msg MsgEthereumTx) ValidateBasic() sdk.Error { return nil } +// To returns the recipient address of the transaction. It returns nil if the +// transaction is a contract creation. +func (msg EthereumTxMsg) To() *ethcmn.Address { + if msg.Data.Recipient == nil { + return nil + } + + return msg.Data.Recipient +} + +// GetMsgs returns a single EthereumTxMsg as an sdk.Msg. +func (msg EthereumTxMsg) GetMsgs() []sdk.Msg { + return []sdk.Msg{msg} +} + // GetSigners returns the expected signers for an Ethereum transaction message. // For such a message, there should exist only a single 'signer'. // // NOTE: This method cannot be used as a chain ID is needed to recover the signer // from the signature. Use 'VerifySig' instead. -func (msg MsgEthereumTx) GetSigners() []sdk.AccAddress { +func (msg EthereumTxMsg) GetSigners() []sdk.AccAddress { panic("must use 'VerifySig' with a chain ID to get the signer") } @@ -146,13 +167,13 @@ func (msg MsgEthereumTx) GetSigners() []sdk.AccAddress { // // NOTE: This method cannot be used as a chain ID is needed to create valid bytes // to sign over. Use 'RLPSignBytes' instead. -func (msg MsgEthereumTx) GetSignBytes() []byte { +func (msg EthereumTxMsg) GetSignBytes() []byte { panic("must use 'RLPSignBytes' with a chain ID to get the valid bytes to sign") } // RLPSignBytes returns the RLP hash of an Ethereum transaction message with a // given chainID used for signing. -func (msg MsgEthereumTx) RLPSignBytes(chainID *big.Int) ethcmn.Hash { +func (msg EthereumTxMsg) RLPSignBytes(chainID *big.Int) ethcmn.Hash { return rlpHash([]interface{}{ msg.Data.AccountNonce, msg.Data.Price, @@ -165,12 +186,12 @@ func (msg MsgEthereumTx) RLPSignBytes(chainID *big.Int) ethcmn.Hash { } // EncodeRLP implements the rlp.Encoder interface. -func (msg *MsgEthereumTx) EncodeRLP(w io.Writer) error { +func (msg *EthereumTxMsg) EncodeRLP(w io.Writer) error { return rlp.Encode(w, &msg.Data) } // DecodeRLP implements the rlp.Decoder interface. -func (msg *MsgEthereumTx) DecodeRLP(s *rlp.Stream) error { +func (msg *EthereumTxMsg) DecodeRLP(s *rlp.Stream) error { _, size, _ := s.Kind() err := s.Decode(&msg.Data) @@ -182,7 +203,7 @@ func (msg *MsgEthereumTx) DecodeRLP(s *rlp.Stream) error { } // Hash hashes the RLP encoding of a transaction. -func (msg *MsgEthereumTx) Hash() ethcmn.Hash { +func (msg *EthereumTxMsg) Hash() ethcmn.Hash { if hash := msg.hash.Load(); hash != nil { return hash.(ethcmn.Hash) } @@ -197,7 +218,7 @@ func (msg *MsgEthereumTx) Hash() ethcmn.Hash { // takes a private key and chainID to sign an Ethereum transaction according to // EIP155 standard. It mutates the transaction as it populates the V, R, S // fields of the Transaction's Signature. -func (msg *MsgEthereumTx) Sign(chainID *big.Int, priv *ecdsa.PrivateKey) { +func (msg *EthereumTxMsg) Sign(chainID *big.Int, priv *ecdsa.PrivateKey) { txHash := msg.RLPSignBytes(chainID) sig, err := ethcrypto.Sign(txHash[:], priv) @@ -230,7 +251,7 @@ func (msg *MsgEthereumTx) Sign(chainID *big.Int, priv *ecdsa.PrivateKey) { // VerifySig attempts to verify a Transaction's signature for a given chainID. // A derived address is returned upon success or an error if recovery fails. -func (msg MsgEthereumTx) VerifySig(chainID *big.Int) (ethcmn.Address, error) { +func (msg *EthereumTxMsg) VerifySig(chainID *big.Int) (ethcmn.Address, error) { signer := ethtypes.NewEIP155Signer(chainID) if sc := msg.from.Load(); sc != nil { @@ -244,43 +265,92 @@ func (msg MsgEthereumTx) VerifySig(chainID *big.Int) (ethcmn.Address, error) { // do not allow recovery for transactions with an unprotected chainID if chainID.Sign() == 0 { - return ethcmn.Address{}, errors.New("invalid chainID") + return ethcmn.Address{}, errors.New("chainID cannot be zero") } - txHash := msg.RLPSignBytes(chainID) - sig := recoverEthSig(msg.Data.R, msg.Data.S, msg.Data.V, chainID) + chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2)) + V := new(big.Int).Sub(msg.Data.V, chainIDMul) + V.Sub(V, big8) - pub, err := ethcrypto.Ecrecover(txHash[:], sig) + sigHash := msg.RLPSignBytes(chainID) + sender, err := recoverEthSig(msg.Data.R, msg.Data.S, V, sigHash) if err != nil { return ethcmn.Address{}, err } - var addr ethcmn.Address - copy(addr[:], ethcrypto.Keccak256(pub[1:])[12:]) - - msg.from.Store(sigCache{signer: signer, from: addr}) - return addr, nil + msg.from.Store(sigCache{signer: signer, from: sender}) + return sender, nil } -// recoverEthSig recovers a signature according to the Ethereum specification. -func recoverEthSig(R, S, Vb, chainID *big.Int) []byte { - var v byte +// Cost returns amount + gasprice * gaslimit. +func (msg EthereumTxMsg) Cost() *big.Int { + total := msg.Fee() + total.Add(total, msg.Data.Amount) + return total +} +// Fee returns gasprice * gaslimit. +func (msg EthereumTxMsg) Fee() *big.Int { + return new(big.Int).Mul(msg.Data.Price, new(big.Int).SetUint64(msg.Data.GasLimit)) +} + +// ---------------------------------------------------------------------------- +// Auxiliary + +// TxDecoder returns an sdk.TxDecoder that can decode both auth.StdTx and +// EthereumTxMsg transactions. +func TxDecoder(cdc *codec.Codec) sdk.TxDecoder { + return func(txBytes []byte) (sdk.Tx, sdk.Error) { + var tx sdk.Tx + + if len(txBytes) == 0 { + return nil, sdk.ErrTxDecode("txBytes are empty") + } + + err := cdc.UnmarshalBinaryLengthPrefixed(txBytes, &tx) + if err != nil { + fmt.Println(err.Error()) + return nil, sdk.ErrTxDecode("failed to decode tx").TraceSDK(err.Error()) + } + + return tx, nil + } +} + +// recoverEthSig recovers a signature according to the Ethereum specification and +// returns the sender or an error. +// +// Ref: Ethereum Yellow Paper (BYZANTIUM VERSION 69351d5) Appendix F +func recoverEthSig(R, S, Vb *big.Int, sigHash ethcmn.Hash) (ethcmn.Address, error) { + if Vb.BitLen() > 8 { + return ethcmn.Address{}, errors.New("invalid signature") + } + + V := byte(Vb.Uint64() - 27) + if !ethcrypto.ValidateSignatureValues(V, R, S, true) { + return ethcmn.Address{}, errors.New("invalid signature") + } + + // encode the signature in uncompressed format r, s := R.Bytes(), S.Bytes() sig := make([]byte, 65) copy(sig[32-len(r):32], r) copy(sig[64-len(s):64], s) + sig[64] = V - if chainID.Sign() == 0 { - v = byte(Vb.Uint64() - 27) - } else { - chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2)) - V := new(big.Int).Sub(Vb, chainIDMul) - - v = byte(V.Uint64() - 35) + // recover the public key from the signature + pub, err := ethcrypto.Ecrecover(sigHash[:], sig) + if err != nil { + return ethcmn.Address{}, err } - sig[64] = v - return sig + if len(pub) == 0 || pub[0] != 4 { + return ethcmn.Address{}, errors.New("invalid public key") + } + + var addr ethcmn.Address + copy(addr[:], ethcrypto.Keccak256(pub[1:])[12:]) + + return addr, nil } diff --git a/x/evm/types/msg_test.go b/x/evm/types/msg_test.go index c7e5ec20..3fe43838 100644 --- a/x/evm/types/msg_test.go +++ b/x/evm/types/msg_test.go @@ -6,8 +6,8 @@ import ( "math/big" "testing" + "github.com/cosmos/ethermint/crypto" ethcmn "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/require" ) @@ -15,17 +15,17 @@ import ( func TestMsgEthereumTx(t *testing.T) { addr := GenerateEthAddress() - msg1 := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test")) + msg1 := NewEthereumTxMsg(0, addr, nil, 100000, nil, []byte("test")) require.NotNil(t, msg1) require.Equal(t, *msg1.Data.Recipient, addr) - msg2 := NewMsgEthereumTxContract(0, nil, 100000, nil, []byte("test")) + msg2 := NewEthereumTxMsgContract(0, nil, 100000, nil, []byte("test")) require.NotNil(t, msg2) require.Nil(t, msg2.Data.Recipient) - msg3 := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test")) - require.Equal(t, msg3.Route(), RouteMsgEthereumTx) - require.Equal(t, msg3.Type(), TypeMsgEthereumTx) + msg3 := NewEthereumTxMsg(0, addr, nil, 100000, nil, []byte("test")) + require.Equal(t, msg3.Route(), RouteEthereumTxMsg) + require.Equal(t, msg3.Type(), TypeEthereumTxMsg) require.Panics(t, func() { msg3.GetSigners() }) require.Panics(t, func() { msg3.GetSignBytes() }) } @@ -46,7 +46,7 @@ func TestMsgEthereumTxValidation(t *testing.T) { } for i, tc := range testCases { - msg := NewMsgEthereumTx(tc.nonce, tc.to, tc.amount, tc.gasLimit, tc.gasPrice, tc.payload) + msg := NewEthereumTxMsg(tc.nonce, tc.to, tc.amount, tc.gasLimit, tc.gasPrice, tc.payload) if tc.expectPass { require.Nil(t, msg.ValidateBasic(), "test: %v", i) @@ -60,14 +60,14 @@ func TestMsgEthereumTxRLPSignBytes(t *testing.T) { addr := ethcmn.BytesToAddress([]byte("test_address")) chainID := big.NewInt(3) - msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test")) + msg := NewEthereumTxMsg(0, addr, nil, 100000, nil, []byte("test")) hash := msg.RLPSignBytes(chainID) require.Equal(t, "5BD30E35AD27449390B14C91E6BCFDCAADF8FE44EF33680E3BC200FC0DC083C7", fmt.Sprintf("%X", hash)) } func TestMsgEthereumTxRLPEncode(t *testing.T) { addr := ethcmn.BytesToAddress([]byte("test_address")) - msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test")) + msg := NewEthereumTxMsg(0, addr, nil, 100000, nil, []byte("test")) raw, err := rlp.EncodeToBytes(msg) require.NoError(t, err) @@ -75,11 +75,11 @@ func TestMsgEthereumTxRLPEncode(t *testing.T) { } func TestMsgEthereumTxRLPDecode(t *testing.T) { - var msg MsgEthereumTx + var msg EthereumTxMsg raw := ethcmn.FromHex("E48080830186A0940000000000000000746573745F61646472657373808474657374808080") addr := ethcmn.BytesToAddress([]byte("test_address")) - expectedMsg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test")) + expectedMsg := NewEthereumTxMsg(0, addr, nil, 100000, nil, []byte("test")) err := rlp.Decode(bytes.NewReader(raw), &msg) require.NoError(t, err) @@ -88,34 +88,46 @@ func TestMsgEthereumTxRLPDecode(t *testing.T) { func TestMsgEthereumTxHash(t *testing.T) { addr := ethcmn.BytesToAddress([]byte("test_address")) - msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test")) + msg := NewEthereumTxMsg(0, addr, nil, 100000, nil, []byte("test")) hash := msg.Hash() require.Equal(t, "E2AA2E68E7586AE9700F1D3D643330866B6AC2B6CA4C804F7C85ECB11D0B0B29", fmt.Sprintf("%X", hash)) } func TestMsgEthereumTxSig(t *testing.T) { - priv, _ := ethcrypto.GenerateKey() - addr := PrivKeyToEthAddress(priv) - - msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test")) chainID := big.NewInt(3) - msg.Sign(chainID, priv) + priv1, _ := crypto.GenerateKey() + priv2, _ := crypto.GenerateKey() + addr1 := ethcmn.BytesToAddress(priv1.PubKey().Address().Bytes()) + addr2 := ethcmn.BytesToAddress(priv2.PubKey().Address().Bytes()) - resultAddr, err := msg.VerifySig(chainID) + // require valid signature passes validation + msg := NewEthereumTxMsg(0, addr1, nil, 100000, nil, []byte("test")) + msg.Sign(chainID, priv1.ToECDSA()) + + signer, err := msg.VerifySig(chainID) require.NoError(t, err) - require.Equal(t, addr, resultAddr) + require.Equal(t, addr1, signer) + require.NotEqual(t, addr2, signer) + + // require invalid chain ID fail validation + msg = NewEthereumTxMsg(0, addr1, nil, 100000, nil, []byte("test")) + msg.Sign(chainID, priv1.ToECDSA()) + + signer, err = msg.VerifySig(big.NewInt(4)) + require.Error(t, err) + require.Equal(t, ethcmn.Address{}, signer) } func TestMsgEthereumTxAmino(t *testing.T) { addr := GenerateEthAddress() - msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test")) + msg := NewEthereumTxMsg(0, addr, nil, 100000, nil, []byte("test")) raw, err := msgCodec.MarshalBinaryBare(msg) require.NoError(t, err) - var msg2 MsgEthereumTx + var msg2 EthereumTxMsg err = msgCodec.UnmarshalBinaryBare(raw, &msg2) require.NoError(t, err) diff --git a/x/evm/types/state_object.go b/x/evm/types/state_object.go index d61db967..4fe82335 100644 --- a/x/evm/types/state_object.go +++ b/x/evm/types/state_object.go @@ -176,10 +176,10 @@ func (so *stateObject) SetNonce(nonce uint64) { prev: so.account.Sequence, }) - so.setNonce(int64(nonce)) + so.setNonce(nonce) } -func (so *stateObject) setNonce(nonce int64) { +func (so *stateObject) setNonce(nonce uint64) { so.account.Sequence = nonce } @@ -249,7 +249,7 @@ func (so *stateObject) CodeHash() []byte { // Nonce returns the state object's current nonce (sequence number). func (so *stateObject) Nonce() uint64 { - return uint64(so.account.Sequence) + return so.account.Sequence } // Code returns the contract code associated with this object, if any. diff --git a/x/evm/types/utils.go b/x/evm/types/utils.go index 0345ae9a..85024cf7 100644 --- a/x/evm/types/utils.go +++ b/x/evm/types/utils.go @@ -1,9 +1,9 @@ package types import ( - "crypto/ecdsa" "fmt" + "github.com/cosmos/ethermint/crypto" ethcmn "github.com/ethereum/go-ethereum/common" ethcrypto "github.com/ethereum/go-ethereum/crypto" ethsha "github.com/ethereum/go-ethereum/crypto/sha3" @@ -12,19 +12,14 @@ import ( "github.com/pkg/errors" ) -// PrivKeyToEthAddress generates an Ethereum address given an ECDSA private key. -func PrivKeyToEthAddress(p *ecdsa.PrivateKey) ethcmn.Address { - return ethcrypto.PubkeyToAddress(p.PublicKey) -} - // GenerateAddress generates an Ethereum address. func GenerateEthAddress() ethcmn.Address { - priv, err := ethcrypto.GenerateKey() + priv, err := crypto.GenerateKey() if err != nil { panic(err) } - return PrivKeyToEthAddress(priv) + return ethcrypto.PubkeyToAddress(priv.PublicKey) } // ValidateSigner attempts to validate a signer for a given slice of bytes over