From a3619584f8ff4fcb83138fcb5bfb74c9bf955171 Mon Sep 17 00:00:00 2001 From: Alexander Bezobchuk Date: Wed, 28 Nov 2018 17:19:22 -0500 Subject: [PATCH] TX Routing Refactor (#496) --- Gopkg.lock | 89 ++++---- Gopkg.toml | 30 ++- app/ante.go | 91 ++++++++ app/ante_test.go | 175 +++++++++++++++ app/ethermint.go | 82 ++++--- {handlers => app}/test_common.go | 8 +- app/test_utils.go | 111 ++++++++++ crypto/crypto.go | 22 ++ crypto/crypto_test.go | 24 ++ docs/spec/transactions/README.md | 63 ++---- handlers/ante.go | 203 ----------------- importer/importer_test.go | 14 +- types/{wire.go => codec.go} | 3 - types/errors.go | 29 ++- types/store_keys.go | 16 -- types/test_common.go | 103 --------- types/tx.go | 367 ------------------------------- types/tx_test.go | 140 ------------ types/utils_test.go | 36 --- x/evm/handler.go | 1 + x/evm/types/codec.go | 19 ++ x/evm/types/msg.go | 286 ++++++++++++++++++++++++ x/evm/types/msg_test.go | 123 +++++++++++ {types => x/evm/types}/utils.go | 28 +-- 24 files changed, 1031 insertions(+), 1032 deletions(-) create mode 100644 app/ante.go create mode 100644 app/ante_test.go rename {handlers => app}/test_common.go (60%) create mode 100644 app/test_utils.go create mode 100644 crypto/crypto.go create mode 100644 crypto/crypto_test.go delete mode 100644 handlers/ante.go rename types/{wire.go => codec.go} (71%) delete mode 100644 types/store_keys.go delete mode 100644 types/test_common.go delete mode 100644 types/tx.go delete mode 100644 types/tx_test.go delete mode 100644 types/utils_test.go create mode 100644 x/evm/handler.go create mode 100644 x/evm/types/codec.go create mode 100644 x/evm/types/msg.go create mode 100644 x/evm/types/msg_test.go rename {types => x/evm/types}/utils.go (70%) diff --git a/Gopkg.lock b/Gopkg.lock index 02a6d3087..010f9fc8a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,11 +3,11 @@ [[projects]] branch = "master" - digest = "1:495c7006c2f48b705f0d89fd8449a2ae70622bb748788d9d17caafa65a6769f9" + digest = "1:8038f3159385d2017d12ce7d8ccf25e59554a265d76d8c31f8c80acb589da6c6" name = "github.com/aristanetworks/goarista" packages = ["monotime"] pruneopts = "T" - revision = "33151c4543a79b013e8e6799ef45b2ba88c3cd1c" + revision = "5bb443fba8e05f4a819301a63af91fe3cbadcc17" [[projects]] branch = "master" @@ -33,7 +33,7 @@ revision = "d4cc87b860166d00d6b5b9e0d3b3d71d6088d4d4" [[projects]] - digest = "1:912c494215c339688331953ba09cb9698a4797fe647d93a4e0a117c9c7b960a2" + digest = "1:e70ff0ca07fdc5dfe6cf280917ae4434f069f164a0bf18681e37cb19b01ee151" name = "github.com/cosmos/cosmos-sdk" packages = [ "baseapp", @@ -56,7 +56,7 @@ "x/stake/types", ] pruneopts = "T" - revision = "075ddce79acb77fe88f849f93fb3036e48ffb555" + revision = "1ea0e4c457fc105b48131a60e3d28c6c1bb32cc0" [[projects]] digest = "1:9f42202ac457c462ad8bb9642806d275af9ab4850cf0b1960b9c6f083d4a309a" @@ -74,13 +74,6 @@ revision = "cbaa98ba5575e67703b32b4b19f73c91f3c4159e" version = "v1.7.1" -[[projects]] - digest = "1:c7644c73a3d23741fdba8a99b1464e021a224b7e205be497271a8003a15ca41b" - name = "github.com/ebuchman/fail-test" - packages = ["."] - pruneopts = "T" - revision = "95f809107225be108efcf10a3509e4ea6ceef3c4" - [[projects]] branch = "master" digest = "1:67d0b50be0549e610017cb91e0b0b745ec0cad7c613bc8e18ff2d1c1fc8825a7" @@ -378,14 +371,16 @@ version = "v1.0.0" [[projects]] - digest = "1:f4f3858737fd9db5cf3ef8019c918a798a987d4d11f7e531c54dfe70d4708642" + digest = "1:98aa8bc119587e8bddd558bf2921a645ea6c0ff3195760142113d4dc7cab509f" name = "github.com/prometheus/client_golang" packages = [ "prometheus", + "prometheus/internal", "prometheus/promhttp", ] pruneopts = "T" - revision = "ae27198cdd90bf12cd134ad79d1366a6cf49f632" + revision = "abad2d1bd44235a26707c172eab6bca5bf2dbad3" + version = "v0.9.1" [[projects]] branch = "master" @@ -397,7 +392,7 @@ [[projects]] branch = "master" - digest = "1:2d9b03513fadf4adf193b3570f5ef65ee57b658d9f11e901a06d17baf2bdc88b" + digest = "1:95442856f5c1df4ff0be91b5a320ee717dd539d4091a3574aeb96bfbd407aa41" name = "github.com/prometheus/common" packages = [ "expfmt", @@ -405,7 +400,7 @@ "model", ] pruneopts = "T" - revision = "7e9e6cabbd393fc208072eedef99188d0ce788b6" + revision = "0b1957f9d949dfa3084171a6ec5642b38055276a" [[projects]] branch = "master" @@ -455,12 +450,12 @@ version = "v1.1.2" [[projects]] - digest = "1:516e71bed754268937f57d4ecb190e01958452336fa73dbac880894164e91c1f" + digest = "1:08d65904057412fc0270fc4812a1c90c594186819243160dc779a402d4b6d0bc" name = "github.com/spf13/cast" packages = ["."] pruneopts = "T" - revision = "8965335b8c7107321228e3e3702cab9832751bac" - version = "v1.2.0" + revision = "8c9545af88b134710ab1cd196795e7f2388358d7" + version = "v1.3.0" [[projects]] digest = "1:52565bd966162d1f4579757f66ce6a7ca9054e7f6b662f0c7c96e4dd228fd017" @@ -508,7 +503,7 @@ [[projects]] branch = "master" - digest = "1:ea4a45f31f55c7a42ba3063baa646ac94eb7ee9afe60c1fd2c8b396930222620" + digest = "1:fa0605d74039818b662892c950cfd9938ab81ebb5f8e2479ceb4734cdae21df3" name = "github.com/syndtr/goleveldb" packages = [ "leveldb", @@ -525,7 +520,7 @@ "leveldb/util", ] pruneopts = "T" - revision = "6b91fda63f2e36186f1c9d0e48578defb69c5d43" + revision = "f9080354173f192dfc8821931eacf9cfd6819253" [[projects]] digest = "1:71ffd1fca92b4972ecd588cf13d9929d4f444659788e9128d055a9126498d41d" @@ -535,35 +530,23 @@ revision = "e5840949ff4fff0c56f9b6a541e22b63581ea9df" [[projects]] - branch = "master" - digest = "1:2b15c0442dc80b581ce7028b2e43029d2f3f985da43cb1d55f7bcdeca785bda0" - name = "github.com/tendermint/ed25519" - packages = [ - ".", - "edwards25519", - "extra25519", - ] - pruneopts = "T" - revision = "d8387025d2b9d158cf4efb07e7ebf814bcce2057" - -[[projects]] - digest = "1:25c97d29878b5f821bb17a379469f0f923426188241bf2aa81c18728cdc6927c" + digest = "1:bcaa26e82b7707fbdfa6d0506ff1f30158eeb23126f761ac65881ed51339bf9e" name = "github.com/tendermint/go-amino" packages = ["."] pruneopts = "T" - revision = "faa6e731944e2b7b6a46ad202902851e8ce85bee" - version = "v0.12.0" + revision = "6dcc6ddc143e116455c94b25c1004c99e0d0ca12" + version = "v0.14.0" [[projects]] - digest = "1:2ecd824e1615a8becefea26637fe24576f3800260f5dc91ffe44b37bdbd27878" + digest = "1:1e4b8f8f8c428af22d0cc68b1478bef4e144edefc4bb967d4d09aaddbc8cd71e" name = "github.com/tendermint/iavl" packages = ["."] pruneopts = "T" - revision = "3acc91fb8811db2c5409a855ae1f8e441fe98e2d" - version = "v0.11.0" + revision = "fa74114f764f9827c4ad5573f990ed25bf8c4bac" + version = "v0.11.1" [[projects]] - digest = "1:b8bd45120cbea639592420b1d5363f102d819ea89d6239f4dae2a0814c76a6d2" + digest = "1:ea9b485e28ee25ef860451187afdec9c38630932e1f999114eb7cab7f8c89966" name = "github.com/tendermint/tendermint" packages = [ "abci/client", @@ -592,8 +575,8 @@ "libs/clist", "libs/common", "libs/db", - "libs/errors", "libs/events", + "libs/fail", "libs/flowrate", "libs/log", "libs/pubsub", @@ -614,7 +597,6 @@ "rpc/core", "rpc/core/types", "rpc/grpc", - "rpc/lib", "rpc/lib/client", "rpc/lib/server", "rpc/lib/types", @@ -627,16 +609,17 @@ "version", ] pruneopts = "T" - revision = "90eda9bfb6e6daeed1c8015df41cb36772d91778" - version = "v0.25.1-rc0" + revision = "22dcc92232cd04ce7381043e09d85dd536ae3b96" + version = "v0.26.3" [[projects]] - branch = "master" - digest = "1:56a43b9f51e5c5ea734e866b82d57c842b022c795a0611ff5f57f3d7c47de45d" + digest = "1:d738326441b0b732070d727891855573dcb579e74d82fcf9a9459d3257f2eb0c" name = "golang.org/x/crypto" packages = [ "chacha20poly1305", "curve25519", + "ed25519", + "ed25519/internal/edwards25519", "hkdf", "internal/chacha20", "internal/subtle", @@ -650,7 +633,8 @@ "ssh/terminal", ] pruneopts = "T" - revision = "0c41d7ab0a0ee717d4590a44bcb987dfd9e183eb" + revision = "3764759f34a542a3aef74d6b02e35be7ab893bba" + source = "https://github.com/tendermint/crypto" [[projects]] digest = "1:5fdc7adede42f80d6201258355d478d856778e21d735f14972abd8ff793fdbf7" @@ -673,8 +657,7 @@ revision = "292b43bbf7cb8d35ddf40f8d5100ef3837cced3f" [[projects]] - branch = "master" - digest = "1:d6d9bf31934efd450aeb0a07061dce8a0e84bc80e4aae05841dd588325832a40" + digest = "1:96672c90ede3a9cd379151c13c436c93efe845c4a3fdd2ce2a94e9c96f233a2c" name = "golang.org/x/sys" packages = [ "cpu", @@ -682,7 +665,7 @@ "windows", ] pruneopts = "T" - revision = "5cd93ef61a7c8f0f858690154eb6de2e69415fa1" + revision = "4e1fef5609515ec7a2cee7b5de30ba6d9b438cbf" [[projects]] digest = "1:6164911cb5e94e8d8d5131d646613ff82c14f5a8ce869de2f6d80d9889df8c5a" @@ -721,11 +704,11 @@ [[projects]] branch = "master" - digest = "1:849525811c9f6ae1f5bd9b866adb4c9436f4a12d767f48e33bf343596d4aafd7" + digest = "1:2460b53d2a66eb9897a17c59ce16c82eeec9affaa31a3ce5814d254abc80fbbd" name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] pruneopts = "T" - revision = "94acd270e44e65579b9ee3cdab25034d33fed608" + revision = "5fc9ac5403620be16bcdb0c8e7644b1178472c3b" [[projects]] digest = "1:adafc60b1d4688759f3fc8f9089e71dd17abd123f4729de6b913bf08c9143770" @@ -809,7 +792,11 @@ "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 5ba05cc72..8d6073e11 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -7,9 +7,8 @@ [[constraint]] name = "github.com/cosmos/cosmos-sdk" - # TODO: Remove this once 0.25 has been released - revision = "075ddce79acb77fe88f849f93fb3036e48ffb555" - # version = "=0.24.2" + # TODO: Replace this with 0.27 + revision = "1ea0e4c457fc105b48131a60e3d28c6c1bb32cc0" [[constraint]] name = "github.com/hashicorp/golang-lru" @@ -27,23 +26,38 @@ name = "github.com/pkg/errors" version = "=0.8.0" -# dependecy overrides +[[constraint]] + name = "golang.org/x/net" + revision = "292b43bbf7cb8d35ddf40f8d5100ef3837cced3f" + +####################### +# dependecy overrides # +####################### [[override]] name = "gopkg.in/fatih/set.v0" version = "=0.1.0" +[[override]] + name = "github.com/tendermint/go-amino" + version = "v0.14.0" + [[override]] name = "github.com/tendermint/iavl" - version = "=v0.11.0" + version = "=v0.11.1" + +[[override]] + name = "golang.org/x/crypto" + source = "https://github.com/tendermint/crypto" + revision = "3764759f34a542a3aef74d6b02e35be7ab893bba" [[override]] name = "github.com/tendermint/tendermint" - version = "=0.25.1-rc0" + version = "v0.26.1" [[override]] - name = "github.com/tendermint/go-amino" - version = "=v0.12.0" + name = "golang.org/x/sys" + revision = "4e1fef5609515ec7a2cee7b5de30ba6d9b438cbf" [prune] go-tests = true diff --git a/app/ante.go b/app/ante.go new file mode 100644 index 000000000..f6f00f3c7 --- /dev/null +++ b/app/ante.go @@ -0,0 +1,91 @@ +package app + +import ( + "encoding/hex" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/tendermint/tendermint/crypto/secp256k1" + + evmtypes "github.com/cosmos/ethermint/x/evm/types" +) + +var dummySecp256k1Pubkey secp256k1.PubKeySecp256k1 // used for tx simulation + +const ( + memoCostPerByte = 1 + maxMemoCharacters = 100 + secp256k1VerifyCost = 100 +) + +func init() { + bz, _ := hex.DecodeString("035AD6810A47F073553FF30D2FCC7E0D3B1C0B74B61A1AAA2582344037151E143A") + copy(dummySecp256k1Pubkey[:], bz) +} + +// NewAnteHandler returns an ante handelr 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. +// +// NOTE: The EVM will already consume (intrinsic) gas for signature verification +// and covering input size as well as handling nonce incrementing. +func NewAnteHandler(ak auth.AccountKeeper, fck auth.FeeCollectionKeeper) sdk.AnteHandler { + return func( + 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 + } + + // 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 + + if ethTx, ok := isEthereumTx(stdTx); ethTx != nil && ok { + return ethAnteHandler(ctx, ethTx, ak) + } + + return auth.NewAnteHandler(ak, fck)(ctx, stdTx, sim) + } +} + +// ---------------------------------------------------------------------------- +// 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. +func ethAnteHandler( + ctx sdk.Context, ethTx *evmtypes.MsgEthereumTx, ak auth.AccountKeeper, +) (newCtx sdk.Context, res sdk.Result, abort bool) { + + return ctx, sdk.Result{}, false +} + +// ---------------------------------------------------------------------------- +// Auxiliary + +// 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 + } + } + + return nil, false +} diff --git a/app/ante_test.go b/app/ante_test.go new file mode 100644 index 000000000..db187c507 --- /dev/null +++ b/app/ante_test.go @@ -0,0 +1,175 @@ +package app + +import ( + "fmt" + "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" +) + +func requireValidTx( + t *testing.T, anteHandler sdk.AnteHandler, ctx sdk.Context, tx sdk.Tx, sim bool, +) { + + _, result, abort := anteHandler(ctx, tx, sim) + require.Equal(t, sdk.CodeOK, result.Code, result.Log) + require.False(t, abort) + require.True(t, result.IsOK()) +} + +func requireInvalidTx( + t *testing.T, anteHandler sdk.AnteHandler, ctx sdk.Context, + tx sdk.Tx, sim bool, code sdk.CodeType, +) { + + newCtx, result, abort := anteHandler(ctx, tx, sim) + require.True(t, abort) + require.Equal(t, code, result.Code, fmt.Sprintf("invalid result: %v", result)) + + if code == sdk.CodeOutOfGas { + stdTx, ok := tx.(auth.StdTx) + require.True(t, ok, "tx must be in form auth.StdTx") + + // require GasWanted is set correctly + require.Equal(t, stdTx.Fee.Gas, result.GasWanted, "'GasWanted' wanted not set correctly") + require.True(t, result.GasUsed > result.GasWanted, "'GasUsed' not greater than GasWanted") + + // require that context is set correctly + require.Equal(t, result.GasUsed, newCtx.GasMeter().GasConsumed(), "Context not updated correctly") + } +} + +func TestValidTx(t *testing.T) { + setup := newTestSetup() + setup.ctx = setup.ctx.WithBlockHeight(1) + + addr1, priv1 := newTestAddrKey() + addr2, priv2 := newTestAddrKey() + + acc1 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr1) + acc1.SetCoins(newTestCoins()) + setup.accKeeper.SetAccount(setup.ctx, acc1) + + acc2 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr2) + acc2.SetCoins(newTestCoins()) + setup.accKeeper.SetAccount(setup.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()} + + tx := newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireValidTx(t, setup.anteHandler, setup.ctx, tx, false) + + // require accounts to update + acc1 = setup.accKeeper.GetAccount(setup.ctx, addr1) + acc2 = setup.accKeeper.GetAccount(setup.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) + + addr1, priv1 := newTestAddrKey() + addr2, priv2 := newTestAddrKey() + addr3, priv3 := newTestAddrKey() + + acc1 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr1) + acc1.SetCoins(newTestCoins()) + setup.accKeeper.SetAccount(setup.ctx, acc1) + + acc2 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr2) + acc2.SetCoins(newTestCoins()) + setup.accKeeper.SetAccount(setup.ctx, acc2) + + fee := newTestStdFee() + msg1 := newTestMsg(addr1, addr2) + + // 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()} + + tx := newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireInvalidTx(t, setup.anteHandler, setup.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()} + + tx = newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireInvalidTx(t, setup.anteHandler, setup.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} + + tx = newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeUnknownAddress) +} + +func TestSDKInvalidAcc(t *testing.T) { + setup := newTestSetup() + setup.ctx = setup.ctx.WithBlockHeight(1) + + addr1, priv1 := newTestAddrKey() + + acc1 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr1) + acc1.SetCoins(newTestCoins()) + setup.accKeeper.SetAccount(setup.ctx, acc1) + + fee := newTestStdFee() + msg1 := newTestMsg(addr1) + msgs := []sdk.Msg{msg1} + privKeys := []crypto.PrivKey{priv1} + + // require validation failure with invalid account number + accNums := []int64{1} + accSeqs := []int64{acc1.GetSequence()} + + tx := newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeInvalidSequence) + + // require validation failure with invalid sequence (nonce) + accNums = []int64{acc1.GetAccountNumber()} + accSeqs = []int64{1} + + tx = newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee) + requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeInvalidSequence) +} + +func TestSDKGasConsumption(t *testing.T) { + // TODO: Test gas consumption and OOG once ante handler implementation stabilizes + t.SkipNow() +} diff --git a/app/ethermint.go b/app/ethermint.go index c683d59b0..6f9e55adf 100644 --- a/app/ethermint.go +++ b/app/ethermint.go @@ -3,7 +3,6 @@ package app import ( bam "github.com/cosmos/cosmos-sdk/baseapp" "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/cosmos-sdk/x/bank" @@ -11,21 +10,30 @@ import ( "github.com/cosmos/cosmos-sdk/x/params" "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/stake" + + evmtypes "github.com/cosmos/ethermint/x/evm/types" + "github.com/pkg/errors" - "github.com/cosmos/ethermint/handlers" - "github.com/cosmos/ethermint/types" - - ethcmn "github.com/ethereum/go-ethereum/common" - abci "github.com/tendermint/tendermint/abci/types" tmcmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" tmlog "github.com/tendermint/tendermint/libs/log" ) -const ( - appName = "Ethermint" +const appName = "Ethermint" + +// application multi-store keys +var ( + storeKeyAccount = sdk.NewKVStoreKey("acc") + storeKeyStorage = sdk.NewKVStoreKey("contract_storage") + storeKeyMain = sdk.NewKVStoreKey("main") + storeKeyStake = sdk.NewKVStoreKey("stake") + storeKeySlashing = sdk.NewKVStoreKey("slashing") + storeKeyGov = sdk.NewKVStoreKey("gov") + storeKeyFeeColl = sdk.NewKVStoreKey("fee") + storeKeyParams = sdk.NewKVStoreKey("params") + storeKeyTransParams = sdk.NewTransientStoreKey("transient_params") ) type ( @@ -59,38 +67,35 @@ type ( // NewEthermintApp returns a reference to a new initialized Ethermint // application. -func NewEthermintApp(logger tmlog.Logger, db dbm.DB, sdkAddr ethcmn.Address) *EthermintApp { +// +// TODO: Ethermint needs to support being bootstrapped as an application running +// in a sovereign zone and as an application running with a shared security model. +// For now, it will support only running as a sovereign application. +func NewEthermintApp(logger tmlog.Logger, db dbm.DB, baseAppOpts ...func(*bam.BaseApp)) *EthermintApp { cdc := CreateCodec() - cms := store.NewCommitMultiStore(db) - - baseAppOpts := []func(*bam.BaseApp){ - func(bApp *bam.BaseApp) { bApp.SetCMS(cms) }, - } - baseApp := bam.NewBaseApp(appName, logger, db, types.TxDecoder(cdc, sdkAddr), baseAppOpts...) + baseApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc), baseAppOpts...) app := &EthermintApp{ BaseApp: baseApp, cdc: cdc, - accountKey: types.StoreKeyAccount, - storageKey: types.StoreKeyStorage, - mainKey: types.StoreKeyMain, - stakeKey: types.StoreKeyStake, - slashingKey: types.StoreKeySlashing, - govKey: types.StoreKeyGov, - feeCollKey: types.StoreKeyFeeColl, - paramsKey: types.StoreKeyParams, - tParamsKey: types.StoreKeyTransParams, + accountKey: storeKeyAccount, + storageKey: storeKeyStorage, + mainKey: storeKeyMain, + stakeKey: storeKeyStake, + slashingKey: storeKeySlashing, + govKey: storeKeyGov, + feeCollKey: storeKeyFeeColl, + paramsKey: storeKeyParams, + tParamsKey: storeKeyTransParams, } - // set application keepers and mappers app.accountKeeper = auth.NewAccountKeeper(app.cdc, app.accountKey, auth.ProtoBaseAccount) app.paramsKeeper = params.NewKeeper(app.cdc, app.paramsKey, app.tParamsKey) app.feeCollKeeper = auth.NewFeeCollectionKeeper(app.cdc, app.feeCollKey) // register message handlers app.Router(). - // TODO: Do we need to mount bank and IBC handlers? Should be handled - // directly in the EVM. + // TODO: add remaining routes AddRoute("stake", stake.NewHandler(app.stakeKeeper)). AddRoute("slashing", slashing.NewHandler(app.slashingKeeper)). AddRoute("gov", gov.NewHandler(app.govKeeper)) @@ -99,7 +104,7 @@ func NewEthermintApp(logger tmlog.Logger, db dbm.DB, sdkAddr ethcmn.Address) *Et app.SetInitChainer(app.initChainer) app.SetBeginBlocker(app.BeginBlocker) app.SetEndBlocker(app.EndBlocker) - app.SetAnteHandler(handlers.AnteHandler(app.accountKeeper, app.feeCollKeeper)) + app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.feeCollKeeper)) app.MountStoresIAVL( app.mainKey, app.accountKey, app.stakeKey, app.slashingKey, @@ -117,19 +122,28 @@ func NewEthermintApp(logger tmlog.Logger, db dbm.DB, sdkAddr ethcmn.Address) *Et // BeginBlocker signals the beginning of a block. It performs application // updates on the start of every block. -func (app *EthermintApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { +func (app *EthermintApp) BeginBlocker( + _ sdk.Context, _ abci.RequestBeginBlock, +) abci.ResponseBeginBlock { + return abci.ResponseBeginBlock{} } // EndBlocker signals the end of a block. It performs application updates on // the end of every block. -func (app *EthermintApp) EndBlocker(ctx sdk.Context, _ abci.RequestEndBlock) abci.ResponseEndBlock { +func (app *EthermintApp) EndBlocker( + _ sdk.Context, _ abci.RequestEndBlock, +) abci.ResponseEndBlock { + return abci.ResponseEndBlock{} } // initChainer initializes the application blockchain with validators and other // state data from TendermintCore. -func (app *EthermintApp) initChainer(_ sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { +func (app *EthermintApp) initChainer( + _ sdk.Context, req abci.RequestInitChain, +) abci.ResponseInitChain { + var genesisState GenesisState stateJSON := req.AppStateBytes @@ -148,8 +162,12 @@ func (app *EthermintApp) initChainer(_ sdk.Context, req abci.RequestInitChain) a func CreateCodec() *codec.Codec { cdc := codec.New() - types.RegisterCodec(cdc) + // TODO: Add remaining codec registrations: + // bank, staking, distribution, slashing, and gov + + evmtypes.RegisterCodec(cdc) auth.RegisterCodec(cdc) + sdk.RegisterCodec(cdc) codec.RegisterCrypto(cdc) return cdc diff --git a/handlers/test_common.go b/app/test_common.go similarity index 60% rename from handlers/test_common.go rename to app/test_common.go index 05af90f93..3620f621e 100644 --- a/handlers/test_common.go +++ b/app/test_common.go @@ -1,4 +1,4 @@ -package handlers +package app import ( "github.com/cosmos/cosmos-sdk/store" @@ -6,13 +6,15 @@ import ( dbm "github.com/tendermint/tendermint/libs/db" ) -func createTestMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) { +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 + return ms, capKey, capKey2 } diff --git a/app/test_utils.go b/app/test_utils.go new file mode 100644 index 000000000..5aad1534a --- /dev/null +++ b/app/test_utils.go @@ -0,0 +1,111 @@ +package app + +import ( + "fmt" + "math/big" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + + "github.com/cosmos/ethermint/crypto" + evmtypes "github.com/cosmos/ethermint/x/evm/types" + + abci "github.com/tendermint/tendermint/abci/types" + tmcrypto "github.com/tendermint/tendermint/crypto" + tmsecp256k1 "github.com/tendermint/tendermint/crypto/secp256k1" + "github.com/tendermint/tendermint/libs/log" +) + +var testDenom = "testcoin" + +type testSetup struct { + ctx sdk.Context + accKeeper auth.AccountKeeper + feeKeeper auth.FeeCollectionKeeper + anteHandler sdk.AnteHandler +} + +func newTestSetup() testSetup { + cdc := codec.New() + ms, capKey, capKey2 := setupMultiStore() + + auth.RegisterBaseAccount(cdc) + + accKeeper := auth.NewAccountKeeper(cdc, capKey, auth.ProtoBaseAccount) + feeKeeper := auth.NewFeeCollectionKeeper(cdc, capKey2) + anteHandler := NewAnteHandler(accKeeper, feeKeeper) + + ctx := sdk.NewContext( + ms, + abci.Header{ChainID: "3", Time: time.Now().UTC()}, + false, + log.NewNopLogger(), + ) + + return testSetup{ + ctx: ctx, + accKeeper: accKeeper, + feeKeeper: feeKeeper, + anteHandler: anteHandler, + } +} + +func newTestMsg(addrs ...sdk.AccAddress) *sdk.TestMsg { + return sdk.NewTestMsg(addrs...) +} + +func newTestCoins() sdk.Coins { + return sdk.Coins{sdk.NewInt64Coin(testDenom, 10000000)} +} + +func newTestStdFee() auth.StdFee { + return auth.NewStdFee(5000, sdk.NewInt64Coin(testDenom, 150)) +} + +// GenerateAddress generates an Ethereum address. +func newTestAddrKey() (sdk.AccAddress, tmcrypto.PrivKey) { + priv := tmsecp256k1.GenPrivKey() + addr := sdk.AccAddress(priv.PubKey().Address()) + return addr, priv +} + +func newTestSDKTx( + ctx sdk.Context, msgs []sdk.Msg, privs []tmcrypto.PrivKey, + accNums []int64, seqs []int64, 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], + } + } + + return auth.NewStdTx(msgs, fee, sigs, "") +} + +func newTestEthTx(ctx sdk.Context, msg *evmtypes.MsgEthereumTx, 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)) + } + + msg.Sign(chainID, privKey) + return auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, nil, "") +} diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 000000000..3dfcfae4c --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,22 @@ +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 new file mode 100644 index 000000000..fe200e03d --- /dev/null +++ b/crypto/crypto_test.go @@ -0,0 +1,24 @@ +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/docs/spec/transactions/README.md b/docs/spec/transactions/README.md index 7fe163673..703025076 100644 --- a/docs/spec/transactions/README.md +++ b/docs/spec/transactions/README.md @@ -6,58 +6,39 @@ and subject to change. ## Routing Ethermint needs to parse and handle transactions routed for both the EVM and for -the Cosmos hub. We attempt to achieve this by mimicking [Geth's](https://github.com/ethereum/go-ethereum) `Transaction` structure and utilizing -the `Payload` as the potential encoding of a Cosmos-routed transaction. What -designates this encoding, and ultimately routing, is the `Recipient` address -- -if this address matches some global unique predefined and configured address, -we regard it as a transaction meant for Cosmos, otherwise, the transaction is a -pure Ethereum transaction and will be executed in the EVM. - -For Cosmos routed transactions, the `Transaction.Payload` will contain an [Amino](https://github.com/tendermint/go-amino) encoded embedded transaction that must -implement the `sdk.Tx` interface. Note, the embedding (outer) `Transaction` is -still RLP encoded in order to preserve compatibility with existing tooling. In -addition, at launch, Ethermint will only support the `auth.StdTx` embedded Cosmos -transaction type. +the Cosmos hub. We attempt to achieve this by mimicking +[Geth's](https://github.com/ethereum/go-ethereum) `Transaction` structure and +treat it as a unique Cosmos SDK message type. An Ethereum transaction is a single +[`sdk.Msg`](https://godoc.org/github.com/cosmos/cosmos-sdk/types#Msg) contained +in an [`auth.StdTx`](https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth#StdTx). +All relevant Ethereum transaction information is contained in this message. This +includes the signature, gas, payload, etc. Being that Ethermint implements the Tendermint ABCI application interface, as transactions are consumed, they are passed through a series of handlers. Once such -handler, `runTx`, is responsible for invoking the `TxDecoder` which performs the -business logic of properly deserializing raw transaction bytes into either an -Ethereum transaction or a Cosmos transaction. +handler, the `AnteHandler`, is responsible for performing preliminary message +execution business logic such as fee payment, signature verification, etc. This is +particular to Cosmos SDK routed transactions. Ethereum routed transactions will +bypass this as the EVM handles the same business logic. -__Note__: Our goal is to utilize Geth as a library, at least as much as possible, -so it should be expected that these types and the operations you may perform on -them will keep in line with Ethereum (e.g. signature algorithms and gas/fees). -In addition, we aim to have existing tooling and frameworks in the Ethereum -ecosystem have 100% compatibility with creating transactions in Ethermint. +Ethereum routed transactions coming from a web3 source are expected to be RLP +encoded, however all internal interaction between Ethermint and Tendermint will +utilize Amino encoding. -## Transactions & Messages - -The SDK distinguishes between transactions (`sdk.Tx`) and messages (`sdk.Msg`). -A `sdk.Tx` is a list of `sdk.Msg` wrapped with authentication and fee data. Users -can create messages containing arbitrary information by implementing the `sdk.Msg` -interface. - -In Ethermint, the `Transaction` type implements the Cosmos SDK `sdk.Tx` interface. -It addition, it implements the Cosmos SDK `sdk.Msg` interface for the sole purpose -of being to perform basic validation checks in the `BaseApp`. It, however, has -no distinction between transactions and messages. +__Note__: Our goal is to utilize Geth/Turbo-Geth as a library, at least as much +as possible, so it should be expected that these types and the operations you may +perform on them will keep in line with Ethereum (e.g. signature algorithms and +gas/fees). In addition, we aim to have existing tooling and frameworks in the +Ethereum ecosystem have 100% compatibility with creating transactions in Ethermint. ## Signatures Ethermint supports [EIP-155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) signatures. A `Transaction` is expected to have a single signature for Ethereum routed transactions. However, just as in Cosmos, Ethermint will support multiple -signers for embedded Cosmos routed transactions. Signatures over the -`Transaction` type are identical to Ethereum. However, the embedded transaction contains -a canonical signature structure that contains the signature itself and other -information such as an account's sequence number. This, in addition to the chainID, -helps prevent "replay attacks", where the same message could be executed over and -over again. - -An embedded transaction's list of signatures must much the unique list of addresses -returned by each message's `GetSigners` call. In addition, the address of first -signer of the embedded transaction is responsible for paying the fees. +signers for non-Ethereum transactions. Signatures over the +`Transaction` type are identical to Ethereum and the signatures will not be duplicated +in the embedding [`auth.StdTx`](https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth#StdTx). ## Gas & Fees diff --git a/handlers/ante.go b/handlers/ante.go deleted file mode 100644 index 2b95feafc..000000000 --- a/handlers/ante.go +++ /dev/null @@ -1,203 +0,0 @@ -package handlers - -import ( - "fmt" - "math/big" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - - "github.com/cosmos/ethermint/types" - ethcmn "github.com/ethereum/go-ethereum/common" -) - -const ( - // TODO: Ported from the SDK and may have a different context/value for - // Ethermint. - verifySigCost = 100 -) - -// internalAnteHandler reflects a function signature an internal ante handler -// must implementing. Internal ante handlers will be dependant upon the -// transaction type. -type internalAnteHandler func( - sdkCtx sdk.Context, tx sdk.Tx, accMapper auth.AccountKeeper, -) (newCtx sdk.Context, res sdk.Result, abort bool) - -// AnteHandler is 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. -func AnteHandler(ak auth.AccountKeeper, _ auth.FeeCollectionKeeper) sdk.AnteHandler { - return func(sdkCtx sdk.Context, tx sdk.Tx, _ bool) (newCtx sdk.Context, res sdk.Result, abort bool) { - var ( - handler internalAnteHandler - gasLimit int64 - ) - - switch tx := tx.(type) { - case types.Transaction: - gasLimit = int64(tx.Data().GasLimit) - handler = handleEthTx - case auth.StdTx: - gasLimit = tx.Fee.Gas - handler = handleEmbeddedTx - default: - return sdkCtx, sdk.ErrInternal(fmt.Sprintf("invalid transaction: %T", tx)).Result(), true - } - - newCtx = sdkCtx.WithGasMeter(sdk.NewGasMeter(gasLimit)) - - // 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. - 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 = gasLimit - res.GasUsed = newCtx.GasMeter().GasConsumed() - abort = true - default: - panic(r) - } - } - }() - - return handler(newCtx, tx, ak) - } -} - -// handleEthTx implements an ante handler for an Ethereum transaction. It -// validates the signature and if valid returns an OK result. -func handleEthTx(sdkCtx sdk.Context, tx sdk.Tx, ak auth.AccountKeeper) (sdk.Context, sdk.Result, bool) { - ethTx, ok := tx.(types.Transaction) - if !ok { - return sdkCtx, sdk.ErrInternal(fmt.Sprintf("invalid transaction: %T", tx)).Result(), true - } - - // the SDK chainID is a string representation of integer - chainID, ok := new(big.Int).SetString(sdkCtx.ChainID(), 10) - if !ok { - return sdkCtx, sdk.ErrInternal(fmt.Sprintf("invalid chainID: %s", sdkCtx.ChainID())).Result(), true - } - - sdkCtx.GasMeter().ConsumeGas(verifySigCost, "ante: verify Ethereum signature") - - addr, err := ethTx.VerifySig(chainID) - if err != nil { - return sdkCtx, sdk.ErrUnauthorized("signature verification failed").Result(), true - } - - acc := ak.GetAccount(sdkCtx, addr.Bytes()) - - // validate the account nonce (referred to as sequence in the AccountMapper) - seq := acc.GetSequence() - if ethTx.Data().AccountNonce != uint64(seq) { - return sdkCtx, sdk.ErrInvalidSequence(fmt.Sprintf("invalid account nonce; expected: %d", seq)).Result(), true - } - - // TODO: The EVM will handle incrementing the nonce (sequence number) - // - // TODO: Investigate gas consumption models as the EVM instruction set has its - // own and we should probably not charge for additional gas where we don't have - // to. - - ak.SetAccount(sdkCtx, acc) - return sdkCtx, sdk.Result{GasWanted: int64(ethTx.Data().GasLimit)}, false -} - -// handleEmbeddedTx implements an ante handler for an SDK transaction. It -// validates the signature and if valid returns an OK result. -func handleEmbeddedTx(sdkCtx sdk.Context, tx sdk.Tx, ak auth.AccountKeeper) (sdk.Context, sdk.Result, bool) { - stdTx, ok := tx.(auth.StdTx) - if !ok { - return sdkCtx, sdk.ErrInternal(fmt.Sprintf("invalid transaction: %T", tx)).Result(), true - } - - if err := validateStdTxBasic(stdTx); err != nil { - return sdkCtx, err.Result(), true - } - - signerAddrs := stdTx.GetSigners() - signerAccs := make([]auth.Account, len(signerAddrs)) - - // validate signatures - for i, sig := range stdTx.Signatures { - signer := ethcmn.BytesToAddress(signerAddrs[i].Bytes()) - - acc, err := validateSignature(sdkCtx, stdTx, signer, sig, ak) - if err != nil { - return sdkCtx, err.Result(), true - } - - // TODO: Fees! - - ak.SetAccount(sdkCtx, acc) - signerAccs[i] = acc - } - - newCtx := auth.WithSigners(sdkCtx, signerAccs) - - return newCtx, sdk.Result{GasWanted: stdTx.Fee.Gas}, false -} - -// validateStdTxBasic validates an auth.StdTx based on parameters that do not -// depend on the context. -func validateStdTxBasic(stdTx auth.StdTx) (err sdk.Error) { - sigs := stdTx.Signatures - if len(sigs) == 0 { - return sdk.ErrUnauthorized("transaction missing signatures") - } - - signerAddrs := stdTx.GetSigners() - if len(sigs) != len(signerAddrs) { - return sdk.ErrUnauthorized("invalid number of transaction signers") - } - - return nil -} - -func validateSignature( - sdkCtx sdk.Context, stdTx auth.StdTx, signer ethcmn.Address, - sig auth.StdSignature, ak auth.AccountKeeper, -) (acc auth.Account, sdkErr sdk.Error) { - - chainID := sdkCtx.ChainID() - - acc = ak.GetAccount(sdkCtx, signer.Bytes()) - if acc == nil { - return nil, sdk.ErrUnknownAddress(fmt.Sprintf("no account with address %s found", signer)) - } - - accNum := acc.GetAccountNumber() - if accNum != sig.AccountNumber { - return nil, sdk.ErrInvalidSequence( - fmt.Sprintf("invalid account number; got %d, expected %d", sig.AccountNumber, accNum)) - } - - accSeq := acc.GetSequence() - if accSeq != sig.Sequence { - return nil, sdk.ErrInvalidSequence( - fmt.Sprintf("invalid account sequence; got %d, expected %d", sig.Sequence, accSeq)) - } - - err := acc.SetSequence(accSeq + 1) - if err != nil { - return nil, sdk.ErrInternal(err.Error()) - } - - signBytes := types.GetStdTxSignBytes(chainID, accNum, accSeq, stdTx.Fee, stdTx.GetMsgs(), stdTx.Memo) - - // consume gas for signature verification - sdkCtx.GasMeter().ConsumeGas(verifySigCost, "ante signature verification") - - if err := types.ValidateSigner(signBytes, sig.Signature, signer); err != nil { - return nil, sdk.ErrUnauthorized(err.Error()) - } - - return -} diff --git a/importer/importer_test.go b/importer/importer_test.go index 206c90d92..432609eef 100644 --- a/importer/importer_test.go +++ b/importer/importer_test.go @@ -66,8 +66,10 @@ func init() { func newTestCodec() *codec.Codec { cdc := codec.New() + evmtypes.RegisterCodec(cdc) types.RegisterCodec(cdc) auth.RegisterCodec(cdc) + sdk.RegisterCodec(cdc) codec.RegisterCrypto(cdc) return cdc @@ -139,7 +141,10 @@ func createAndTestGenesis(t *testing.T, cms sdk.CommitMultiStore, ak auth.Accoun // persist multi-store root state commitID := cms.Commit() - require.Equal(t, "12D4DB63083D5B01824A35BB70BF671686D60532", fmt.Sprintf("%X", commitID.Hash)) + require.Equal( + t, "29EF84DF8CC4648FD15341F15585A434279A9514445FC9F9E884D687185C1012", + fmt.Sprintf("%X", commitID.Hash), + ) // verify account mapper state genAcc := ak.GetAccount(ctx, sdk.AccAddress(genInvestor.Bytes())) @@ -168,8 +173,7 @@ func TestImportBlocks(t *testing.T) { cdc := newTestCodec() cms := store.NewCommitMultiStore(db) - // create account mapper - am := auth.NewAccountKeeper( + ak := auth.NewAccountKeeper( cdc, accKey, types.ProtoBaseAccount, @@ -188,7 +192,7 @@ func TestImportBlocks(t *testing.T) { require.NoError(t, err) // set and test genesis block - createAndTestGenesis(t, cms, am) + createAndTestGenesis(t, cms, ak) // open blockchain export file blockchainInput, err := os.Open(flagBlockchain) @@ -230,7 +234,7 @@ func TestImportBlocks(t *testing.T) { ctx := sdk.NewContext(ms, abci.Header{}, false, logger) ctx = ctx.WithBlockHeight(int64(block.NumberU64())) - stateDB := createStateDB(t, ctx, am) + stateDB := createStateDB(t, ctx, ak) if chainConfig.DAOForkSupport && chainConfig.DAOForkBlock != nil && chainConfig.DAOForkBlock.Cmp(block.Number()) == 0 { ethmisc.ApplyDAOHardFork(stateDB) diff --git a/types/wire.go b/types/codec.go similarity index 71% rename from types/wire.go rename to types/codec.go index cc3783a8d..e1855df9d 100644 --- a/types/wire.go +++ b/types/codec.go @@ -2,7 +2,6 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" ) var typesCodec = codec.New() @@ -14,7 +13,5 @@ func init() { // RegisterCodec registers all the necessary types with amino for the given // codec. func RegisterCodec(cdc *codec.Codec) { - sdk.RegisterCodec(cdc) - cdc.RegisterConcrete(&Transaction{}, "types/Transaction", nil) cdc.RegisterConcrete(&Account{}, "types/Account", nil) } diff --git a/types/errors.go b/types/errors.go index 03eee3069..0fe336d55 100644 --- a/types/errors.go +++ b/types/errors.go @@ -4,25 +4,34 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +// Ethermint error codes const ( - // DefaultCodespace reserves a Codespace for Ethermint, as 0 and 1 are - // reserved by SDK. - DefaultCodespace sdk.CodespaceType = 2 + // DefaultCodespace reserves a Codespace for Ethermint. + DefaultCodespace sdk.CodespaceType = "ethermint" - // CodeInvalidValue reserves the CodeInvalidValue with first non-OK - // codetype. - CodeInvalidValue sdk.CodeType = 1 + CodeInvalidValue sdk.CodeType = 1 + CodeInvalidAccountNumber sdk.CodeType = 2 ) func codeToDefaultMsg(code sdk.CodeType) string { switch code { + case CodeInvalidValue: + return "invalid value" + case CodeInvalidAccountNumber: + return "invalid account number" default: return sdk.CodeToDefaultMsg(code) } } -// ErrInvalidValue returns a standardized SDK error for a given codespace and -// message. -func ErrInvalidValue(codespace sdk.CodespaceType, msg string) sdk.Error { - return sdk.NewError(codespace, CodeInvalidValue, msg) +// ErrInvalidValue returns a standardized SDK error resulting from an invalid +// value. +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) } diff --git a/types/store_keys.go b/types/store_keys.go deleted file mode 100644 index 5365de97b..000000000 --- a/types/store_keys.go +++ /dev/null @@ -1,16 +0,0 @@ -package types - -import sdk "github.com/cosmos/cosmos-sdk/types" - -// Application multi-store keys -var ( - StoreKeyAccount = sdk.NewKVStoreKey("acc") - StoreKeyStorage = sdk.NewKVStoreKey("contract_storage") - StoreKeyMain = sdk.NewKVStoreKey("main") - StoreKeyStake = sdk.NewKVStoreKey("stake") - StoreKeySlashing = sdk.NewKVStoreKey("slashing") - StoreKeyGov = sdk.NewKVStoreKey("gov") - StoreKeyFeeColl = sdk.NewKVStoreKey("fee") - StoreKeyParams = sdk.NewKVStoreKey("params") - StoreKeyTransParams = sdk.NewTransientStoreKey("transient_params") -) diff --git a/types/test_common.go b/types/test_common.go deleted file mode 100644 index 80c5cde9e..000000000 --- a/types/test_common.go +++ /dev/null @@ -1,103 +0,0 @@ -// nolint -package types - -import ( - "crypto/ecdsa" - "math/big" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - ethcmn "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" - ethcrypto "github.com/ethereum/go-ethereum/crypto" -) - -// test variables -var ( - TestSDKAddr = GenerateEthAddress() - TestChainID = big.NewInt(3) - - TestPrivKey1, _ = ethcrypto.GenerateKey() - TestPrivKey2, _ = ethcrypto.GenerateKey() - - TestAddr1 = PrivKeyToEthAddress(TestPrivKey1) - TestAddr2 = PrivKeyToEthAddress(TestPrivKey2) -) - -func NewTestCodec() *codec.Codec { - cdc := codec.New() - - RegisterCodec(cdc) - auth.RegisterCodec(cdc) - codec.RegisterCrypto(cdc) - cdc.RegisterConcrete(&sdk.TestMsg{}, "test/TestMsg", nil) - - return cdc -} - -func NewTestStdFee() auth.StdFee { - return auth.NewStdFee(5000, sdk.NewCoin("photon", sdk.NewInt(150))) -} - -func NewTestStdTx( - chainID *big.Int, msgs []sdk.Msg, accNums, seqs []int64, pKeys []*ecdsa.PrivateKey, fee auth.StdFee, -) sdk.Tx { - - sigs := make([]auth.StdSignature, len(pKeys)) - - for i, priv := range pKeys { - signBytes := GetStdTxSignBytes(chainID.String(), accNums[i], seqs[i], NewTestStdFee(), msgs, "") - - sig, err := ethcrypto.Sign(signBytes, priv) - if err != nil { - panic(err) - } - - sigs[i] = auth.StdSignature{Signature: sig, AccountNumber: accNums[i], Sequence: seqs[i]} - } - - return auth.NewStdTx(msgs, fee, sigs, "") -} - -func NewTestGethTxs( - chainID *big.Int, seqs []int64, addrs []ethcmn.Address, pKeys []*ecdsa.PrivateKey, -) []*ethtypes.Transaction { - - txs := make([]*ethtypes.Transaction, len(pKeys)) - - for i, privKey := range pKeys { - ethTx := ethtypes.NewTransaction( - uint64(seqs[i]), addrs[i], big.NewInt(10), 1000, big.NewInt(100), []byte{}, - ) - - signer := ethtypes.NewEIP155Signer(chainID) - - ethTx, err := ethtypes.SignTx(ethTx, signer, privKey) - if err != nil { - panic(err) - } - - txs[i] = ethTx - } - - return txs -} - -func NewTestEthTxs( - chainID *big.Int, seqs []int64, addrs []ethcmn.Address, pKeys []*ecdsa.PrivateKey, -) []*Transaction { - - txs := make([]*Transaction, len(pKeys)) - - for i, privKey := range pKeys { - ethTx := NewTransaction( - uint64(seqs[i]), addrs[i], big.NewInt(10), 1000, big.NewInt(100), []byte{}, - ) - - ethTx.Sign(chainID, privKey) - txs[i] = ethTx - } - - return txs -} diff --git a/types/tx.go b/types/tx.go deleted file mode 100644 index 8a7abf28a..000000000 --- a/types/tx.go +++ /dev/null @@ -1,367 +0,0 @@ -package types - -import ( - "bytes" - "crypto/ecdsa" - "fmt" - "io" - "math/big" - "sync/atomic" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - ethcmn "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - ethsha "github.com/ethereum/go-ethereum/crypto/sha3" - "github.com/ethereum/go-ethereum/rlp" - "github.com/pkg/errors" -) - -// TODO: Move to the EVM module - -// message constants -const ( - TypeTxEthereum = "Ethereum" - RouteTxEthereum = "evm" -) - -// ---------------------------------------------------------------------------- -// Ethereum transaction -// ---------------------------------------------------------------------------- - -var _ sdk.Tx = (*Transaction)(nil) - -type ( - // Transaction implements the Ethereum transaction structure as an exact - // replica. It implements the Cosmos sdk.Tx interface. Due to the private - // fields, it must be replicated here and cannot be embedded or used - // directly. - // - // Note: The transaction also implements the sdk.Msg interface to perform - // basic validation that is done in the BaseApp. - Transaction struct { - data TxData - - // caches - hash atomic.Value - size atomic.Value - from atomic.Value - } - - // TxData implements the Ethereum transaction data structure as an exact - // copy. It is used solely as intended in Ethereum abiding by the protocol - // except for the payload field which may embed a Cosmos SDK transaction. - TxData struct { - AccountNonce uint64 `json:"nonce"` - Price *big.Int `json:"gasPrice"` - GasLimit uint64 `json:"gas"` - Recipient *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation - Amount *big.Int `json:"value"` - Payload []byte `json:"input"` - - // signature values - V *big.Int `json:"v"` - R *big.Int `json:"r"` - S *big.Int `json:"s"` - - // hash is only used when marshaling to JSON - Hash *ethcmn.Hash `json:"hash" rlp:"-"` - } - - // sigCache is used to cache the derived sender and contains the signer used - // to derive it. - sigCache struct { - signer ethtypes.Signer - from ethcmn.Address - } -) - -// NewTransaction returns a reference to a new Ethereum transaction. -func NewTransaction( - nonce uint64, to ethcmn.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte, -) *Transaction { - - return newTransaction(nonce, &to, amount, gasLimit, gasPrice, payload) -} - -// NewContractCreation returns a reference to a new Ethereum transaction -// designated for contract creation. -func NewContractCreation( - nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte, -) *Transaction { - - return newTransaction(nonce, nil, amount, gasLimit, gasPrice, payload) -} - -func newTransaction( - nonce uint64, to *ethcmn.Address, amount *big.Int, - gasLimit uint64, gasPrice *big.Int, payload []byte, -) *Transaction { - - if len(payload) > 0 { - payload = ethcmn.CopyBytes(payload) - } - - txData := TxData{ - AccountNonce: nonce, - Recipient: to, - Payload: payload, - GasLimit: gasLimit, - Amount: new(big.Int), - Price: new(big.Int), - V: new(big.Int), - R: new(big.Int), - S: new(big.Int), - } - - if amount != nil { - txData.Amount.Set(amount) - } - if gasPrice != nil { - txData.Price.Set(gasPrice) - } - - return &Transaction{data: txData} -} - -// Data returns the Transaction's data. -func (tx Transaction) Data() TxData { - return tx.data -} - -// EncodeRLP implements the rlp.Encoder interface. -func (tx *Transaction) EncodeRLP(w io.Writer) error { - return rlp.Encode(w, &tx.data) -} - -// DecodeRLP implements the rlp.Decoder interface. -func (tx *Transaction) DecodeRLP(s *rlp.Stream) error { - _, size, _ := s.Kind() - err := s.Decode(&tx.data) - if err == nil { - tx.size.Store(ethcmn.StorageSize(rlp.ListSize(size))) - } - - return err -} - -// Hash hashes the RLP encoding of a transaction. -func (tx *Transaction) Hash() ethcmn.Hash { - if hash := tx.hash.Load(); hash != nil { - return hash.(ethcmn.Hash) - } - - v := rlpHash(tx) - tx.hash.Store(v) - return v -} - -// SigHash returns the RLP hash of a transaction with a given chainID used for -// signing. -func (tx Transaction) SigHash(chainID *big.Int) ethcmn.Hash { - return rlpHash([]interface{}{ - tx.data.AccountNonce, - tx.data.Price, - tx.data.GasLimit, - tx.data.Recipient, - tx.data.Amount, - tx.data.Payload, - chainID, uint(0), uint(0), - }) -} - -// Sign calculates a secp256k1 ECDSA signature and signs the transaction. It -// 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 (tx *Transaction) Sign(chainID *big.Int, priv *ecdsa.PrivateKey) { - txHash := tx.SigHash(chainID) - - sig, err := ethcrypto.Sign(txHash[:], priv) - if err != nil { - panic(err) - } - - if len(sig) != 65 { - panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig))) - } - - r := new(big.Int).SetBytes(sig[:32]) - s := new(big.Int).SetBytes(sig[32:64]) - - var v *big.Int - if chainID.Sign() == 0 { - v = new(big.Int).SetBytes([]byte{sig[64] + 27}) - } else { - v = big.NewInt(int64(sig[64] + 35)) - chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2)) - v.Add(v, chainIDMul) - } - - tx.data.V = v - tx.data.R = r - tx.data.S = s -} - -// 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 (tx Transaction) VerifySig(chainID *big.Int) (ethcmn.Address, error) { - signer := ethtypes.NewEIP155Signer(chainID) - - if sc := tx.from.Load(); sc != nil { - sigCache := sc.(sigCache) - // If the signer used to derive from in a previous - // call is not the same as used current, invalidate - // the cache. - if sigCache.signer.Equal(signer) { - return sigCache.from, nil - } - } - - // do not allow recovery for transactions with an unprotected chainID - if chainID.Sign() == 0 { - return ethcmn.Address{}, errors.New("invalid chainID") - } - - txHash := tx.SigHash(chainID) - sig := recoverEthSig(tx.data.R, tx.data.S, tx.data.V, chainID) - - pub, err := ethcrypto.Ecrecover(txHash[:], sig) - if err != nil { - return ethcmn.Address{}, err - } - - var addr ethcmn.Address - copy(addr[:], ethcrypto.Keccak256(pub[1:])[12:]) - - tx.from.Store(sigCache{signer: signer, from: addr}) - return addr, nil -} - -// Type implements the sdk.Msg interface. It returns the type of the -// Transaction. -func (tx Transaction) Type() string { return TypeTxEthereum } - -// Route implements the sdk.Msg interface. It returns the route of the -// Transaction. -func (tx Transaction) Route() string { return RouteTxEthereum } - -// ValidateBasic implements the sdk.Msg interface. It performs basic validation -// checks of a Transaction. If returns an sdk.Error if validation fails. -func (tx Transaction) ValidateBasic() sdk.Error { - if tx.data.Price.Sign() != 1 { - return ErrInvalidValue(DefaultCodespace, "price must be positive") - } - - if tx.data.Amount.Sign() != 1 { - return ErrInvalidValue(DefaultCodespace, "amount must be positive") - } - - return nil -} - -// GetSignBytes performs a no-op and should not be used. It implements the -// sdk.Msg Interface -func (tx Transaction) GetSignBytes() (sigBytes []byte) { return } - -// GetSigners performs a no-op and should not be used. It implements the -// sdk.Msg Interface -// -// CONTRACT: The transaction must already be signed. -func (tx Transaction) GetSigners() (signers []sdk.AccAddress) { return } - -// GetMsgs returns a single message containing the Transaction itself. It -// implements the Cosmos sdk.Tx interface. -func (tx Transaction) GetMsgs() []sdk.Msg { - return []sdk.Msg{tx} -} - -// hasEmbeddedTx returns a boolean reflecting if the transaction contains an -// SDK transaction or not based on the recipient address. -func (tx Transaction) hasEmbeddedTx(addr ethcmn.Address) bool { - return bytes.Equal(tx.data.Recipient.Bytes(), addr.Bytes()) -} - -// GetEmbeddedTx returns the embedded SDK transaction from an Ethereum -// transaction. It returns an error if decoding the inner transaction fails. -// -// CONTRACT: The payload field of an Ethereum transaction must contain a valid -// encoded SDK transaction. -func (tx Transaction) GetEmbeddedTx(codec *codec.Codec) (sdk.Tx, sdk.Error) { - var etx sdk.Tx - - err := codec.UnmarshalBinary(tx.data.Payload, &etx) - if err != nil { - return etx, sdk.ErrTxDecode("failed to decode embedded transaction") - } - - return etx, nil -} - -// ---------------------------------------------------------------------------- -// Utilities -// ---------------------------------------------------------------------------- - -// TxDecoder returns an sdk.TxDecoder that given raw transaction bytes and an -// SDK address, attempts to decode them into a Transaction or an EmbeddedTx or -// returning an error if decoding fails. -func TxDecoder(codec *codec.Codec, sdkAddress ethcmn.Address) sdk.TxDecoder { - return func(txBytes []byte) (sdk.Tx, sdk.Error) { - var tx = Transaction{} - - if len(txBytes) == 0 { - return nil, sdk.ErrTxDecode("transaction bytes are empty") - } - - err := rlp.DecodeBytes(txBytes, &tx) - if err != nil { - return nil, sdk.ErrTxDecode("failed to decode transaction").TraceSDK(err.Error()) - } - - // If the transaction is routed as an SDK transaction, decode and return - // the embedded SDK transaction. - if tx.hasEmbeddedTx(sdkAddress) { - etx, err := tx.GetEmbeddedTx(codec) - if err != nil { - return nil, err - } - - return etx, nil - } - - return tx, nil - } -} - -// recoverEthSig recovers a signature according to the Ethereum specification. -func recoverEthSig(R, S, Vb, chainID *big.Int) []byte { - var v byte - - r, s := R.Bytes(), S.Bytes() - sig := make([]byte, 65) - - copy(sig[32-len(r):32], r) - copy(sig[64-len(s):64], s) - - 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) - } - - sig[64] = v - return sig -} - -func rlpHash(x interface{}) (hash ethcmn.Hash) { - hasher := ethsha.NewKeccak256() - - rlp.Encode(hasher, x) - hasher.Sum(hash[:0]) - - return -} diff --git a/types/tx_test.go b/types/tx_test.go deleted file mode 100644 index abe74bb7b..000000000 --- a/types/tx_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package types - -import ( - "crypto/ecdsa" - "fmt" - "math/big" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - ethcmn "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" - "github.com/stretchr/testify/require" -) - -func TestTransactionRLPEncode(t *testing.T) { - txs := NewTestEthTxs(TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1}) - gtxs := NewTestGethTxs(TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1}) - - txRLP, err := rlp.EncodeToBytes(txs[0]) - require.NoError(t, err) - - gtxRLP, err := rlp.EncodeToBytes(gtxs[0]) - require.NoError(t, err) - - require.Equal(t, gtxRLP, txRLP) -} - -func TestTransactionRLPDecode(t *testing.T) { - txs := NewTestEthTxs(TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1}) - gtxs := NewTestGethTxs(TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1}) - - txRLP, err := rlp.EncodeToBytes(txs[0]) - require.NoError(t, err) - - gtxRLP, err := rlp.EncodeToBytes(gtxs[0]) - require.NoError(t, err) - - var ( - decodedTx Transaction - decodedGtx ethtypes.Transaction - ) - - err = rlp.DecodeBytes(txRLP, &decodedTx) - require.NoError(t, err) - - err = rlp.DecodeBytes(gtxRLP, &decodedGtx) - require.NoError(t, err) - - require.Equal(t, decodedGtx.Hash(), decodedTx.Hash()) -} - -func TestValidation(t *testing.T) { - ethTxs := NewTestEthTxs( - TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1}, - ) - - testCases := []struct { - msg sdk.Msg - mutate func(sdk.Msg) sdk.Msg - expectedErr bool - }{ - {ethTxs[0], func(msg sdk.Msg) sdk.Msg { return msg }, false}, - {ethTxs[0], func(msg sdk.Msg) sdk.Msg { - tx := msg.(*Transaction) - tx.data.Price = big.NewInt(-1) - return tx - }, true}, - {ethTxs[0], func(msg sdk.Msg) sdk.Msg { - tx := msg.(*Transaction) - tx.data.Amount = big.NewInt(-1) - return tx - }, true}, - } - - for i, tc := range testCases { - msg := tc.mutate(tc.msg) - err := msg.ValidateBasic() - - if tc.expectedErr { - require.NotEqual(t, sdk.CodeOK, err.Code(), fmt.Sprintf("expected error: test case #%d", i)) - } else { - require.NoError(t, err, fmt.Sprintf("unexpected error: test case #%d", i)) - } - } -} - -func TestTransactionVerifySig(t *testing.T) { - txs := NewTestEthTxs( - TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1}, - ) - - addr, err := txs[0].VerifySig(TestChainID) - require.NoError(t, err) - require.Equal(t, TestAddr1, addr) - - addr, err = txs[0].VerifySig(big.NewInt(100)) - require.Error(t, err) - require.NotEqual(t, TestAddr1, addr) -} - -func TestTxDecoder(t *testing.T) { - testCodec := NewTestCodec() - txDecoder := TxDecoder(testCodec, TestSDKAddr) - msgs := []sdk.Msg{sdk.NewTestMsg()} - - // create a non-SDK Ethereum transaction - txs := NewTestEthTxs( - TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1}, - ) - - txBytes, err := rlp.EncodeToBytes(txs[0]) - require.NoError(t, err) - - // require the transaction to properly decode into a Transaction - decodedTx, err := txDecoder(txBytes) - require.NoError(t, err) - require.IsType(t, Transaction{}, decodedTx) - require.Equal(t, txs[0].data, (decodedTx.(Transaction)).data) - - // create a SDK (auth.StdTx) transaction and encode - stdTx := NewTestStdTx(TestChainID, msgs, []int64{0}, []int64{0}, []*ecdsa.PrivateKey{TestPrivKey1}, NewTestStdFee()) - payload := testCodec.MustMarshalBinary(stdTx) - tx := NewTransaction(0, TestSDKAddr, big.NewInt(10), 1000, big.NewInt(100), payload) - - txBytes, err = rlp.EncodeToBytes(tx) - require.NoError(t, err) - - // require the transaction to properly decode into a Transaction - decodedTx, err = txDecoder(txBytes) - require.NoError(t, err) - require.IsType(t, auth.StdTx{}, decodedTx) - require.Equal(t, stdTx, decodedTx) - - // require the decoding to fail when no transaction bytes are given - decodedTx, err = txDecoder([]byte{}) - require.Error(t, err) - require.Nil(t, decodedTx) -} diff --git a/types/utils_test.go b/types/utils_test.go deleted file mode 100644 index 12e27d8f1..000000000 --- a/types/utils_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package types - -import ( - "testing" - - "github.com/stretchr/testify/require" - - sdk "github.com/cosmos/cosmos-sdk/types" - ethcrypto "github.com/ethereum/go-ethereum/crypto" -) - -func TestValidateSigner(t *testing.T) { - msgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(TestAddr1.Bytes()))} - - // create message signing structure and bytes - signBytes := GetStdTxSignBytes(TestChainID.String(), 0, 0, NewTestStdFee(), msgs, "") - - // require signing not to fail - sig, err := ethcrypto.Sign(signBytes, TestPrivKey1) - require.NoError(t, err) - - // require signature to be valid - err = ValidateSigner(signBytes, sig, TestAddr1) - require.NoError(t, err) - - sig, err = ethcrypto.Sign(signBytes, TestPrivKey2) - require.NoError(t, err) - - // require signature to be invalid - err = ValidateSigner(signBytes, sig, TestAddr1) - require.Error(t, err) - - // require invalid signature bytes return an error - err = ValidateSigner([]byte{}, sig, TestAddr2) - require.Error(t, err) -} diff --git a/x/evm/handler.go b/x/evm/handler.go new file mode 100644 index 000000000..e998e5586 --- /dev/null +++ b/x/evm/handler.go @@ -0,0 +1 @@ +package evm diff --git a/x/evm/types/codec.go b/x/evm/types/codec.go new file mode 100644 index 000000000..12011660f --- /dev/null +++ b/x/evm/types/codec.go @@ -0,0 +1,19 @@ +package types + +import "github.com/cosmos/cosmos-sdk/codec" + +var msgCodec = codec.New() + +func init() { + cdc := codec.New() + + RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + + msgCodec = cdc.Seal() +} + +// Register concrete types and interfaces on the given codec. +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(MsgEthereumTx{}, "ethermint/MsgEthereumTx", nil) +} diff --git a/x/evm/types/msg.go b/x/evm/types/msg.go new file mode 100644 index 000000000..74f92d3d8 --- /dev/null +++ b/x/evm/types/msg.go @@ -0,0 +1,286 @@ +package types + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "io" + "math/big" + "sync/atomic" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/ethermint/types" + + ethcmn "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" +) + +var _ sdk.Msg = MsgEthereumTx{} + +// message type and route constants +const ( + TypeMsgEthereumTx = "ethereum_tx" + RouteMsgEthereumTx = "evm" +) + +// MsgEthereumTx encapsulates an Ethereum transaction as an SDK message. +type ( + MsgEthereumTx struct { + Data TxData + + // caches + hash atomic.Value + size atomic.Value + from atomic.Value + } + + // TxData implements the Ethereum transaction data structure. It is used + // solely as intended in Ethereum abiding by the protocol. + TxData struct { + AccountNonce uint64 `json:"nonce"` + Price *big.Int `json:"gasPrice"` + GasLimit uint64 `json:"gas"` + Recipient *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation + Amount *big.Int `json:"value"` + Payload []byte `json:"input"` + + // signature values + V *big.Int `json:"v"` + R *big.Int `json:"r"` + S *big.Int `json:"s"` + + // hash is only used when marshaling to JSON + Hash *ethcmn.Hash `json:"hash" rlp:"-"` + } + + // sigCache is used to cache the derived sender and contains the signer used + // to derive it. + sigCache struct { + signer ethtypes.Signer + from ethcmn.Address + } +) + +// NewMsgEthereumTx returns a reference to a new Ethereum transaction message. +func NewMsgEthereumTx( + nonce uint64, to ethcmn.Address, amount *big.Int, + gasLimit uint64, gasPrice *big.Int, payload []byte, +) *MsgEthereumTx { + + return newMsgEthereumTx(nonce, &to, amount, gasLimit, gasPrice, payload) +} + +// NewMsgEthereumTxContract returns a reference to a new Ethereum transaction +// message designated for contract creation. +func NewMsgEthereumTxContract( + nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte, +) *MsgEthereumTx { + + return newMsgEthereumTx(nonce, nil, amount, gasLimit, gasPrice, payload) +} + +func newMsgEthereumTx( + nonce uint64, to *ethcmn.Address, amount *big.Int, + gasLimit uint64, gasPrice *big.Int, payload []byte, +) *MsgEthereumTx { + + if len(payload) > 0 { + payload = ethcmn.CopyBytes(payload) + } + + txData := TxData{ + AccountNonce: nonce, + Recipient: to, + Payload: payload, + GasLimit: gasLimit, + Amount: new(big.Int), + Price: new(big.Int), + V: new(big.Int), + R: new(big.Int), + S: new(big.Int), + } + + if amount != nil { + txData.Amount.Set(amount) + } + if gasPrice != nil { + txData.Price.Set(gasPrice) + } + + return &MsgEthereumTx{Data: txData} +} + +// Route returns the route value of an MsgEthereumTx. +func (msg MsgEthereumTx) Route() string { return RouteMsgEthereumTx } + +// Type returns the type value of an MsgEthereumTx. +func (msg MsgEthereumTx) Type() string { return TypeMsgEthereumTx } + +// 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 { + if msg.Data.Price.Sign() != 1 { + return types.ErrInvalidValue("price must be positive") + } + + if msg.Data.Amount.Sign() != 1 { + return types.ErrInvalidValue("amount must be positive") + } + + return nil +} + +// 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 { + panic("must use 'VerifySig' with a chain ID to get the signer") +} + +// GetSignBytes returns the Amino bytes of an Ethereum transaction message used +// for signing. +// +// 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 { + 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 { + return rlpHash([]interface{}{ + msg.Data.AccountNonce, + msg.Data.Price, + msg.Data.GasLimit, + msg.Data.Recipient, + msg.Data.Amount, + msg.Data.Payload, + chainID, uint(0), uint(0), + }) +} + +// EncodeRLP implements the rlp.Encoder interface. +func (msg *MsgEthereumTx) 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 { + _, size, _ := s.Kind() + + err := s.Decode(&msg.Data) + if err == nil { + msg.size.Store(ethcmn.StorageSize(rlp.ListSize(size))) + } + + return err +} + +// Hash hashes the RLP encoding of a transaction. +func (msg *MsgEthereumTx) Hash() ethcmn.Hash { + if hash := msg.hash.Load(); hash != nil { + return hash.(ethcmn.Hash) + } + + v := rlpHash(msg) + msg.hash.Store(v) + + return v +} + +// Sign calculates a secp256k1 ECDSA signature and signs the transaction. It +// 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) { + txHash := msg.RLPSignBytes(chainID) + + sig, err := ethcrypto.Sign(txHash[:], priv) + if err != nil { + panic(err) + } + + if len(sig) != 65 { + panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig))) + } + + r := new(big.Int).SetBytes(sig[:32]) + s := new(big.Int).SetBytes(sig[32:64]) + + var v *big.Int + + if chainID.Sign() == 0 { + v = new(big.Int).SetBytes([]byte{sig[64] + 27}) + } else { + v = big.NewInt(int64(sig[64] + 35)) + chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2)) + + v.Add(v, chainIDMul) + } + + msg.Data.V = v + msg.Data.R = r + msg.Data.S = s +} + +// 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) { + signer := ethtypes.NewEIP155Signer(chainID) + + if sc := msg.from.Load(); sc != nil { + sigCache := sc.(sigCache) + // If the signer used to derive from in a previous call is not the same as + // used current, invalidate the cache. + if sigCache.signer.Equal(signer) { + return sigCache.from, nil + } + } + + // do not allow recovery for transactions with an unprotected chainID + if chainID.Sign() == 0 { + return ethcmn.Address{}, errors.New("invalid chainID") + } + + txHash := msg.RLPSignBytes(chainID) + sig := recoverEthSig(msg.Data.R, msg.Data.S, msg.Data.V, chainID) + + pub, err := ethcrypto.Ecrecover(txHash[:], sig) + 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 +} + +// recoverEthSig recovers a signature according to the Ethereum specification. +func recoverEthSig(R, S, Vb, chainID *big.Int) []byte { + var v byte + + r, s := R.Bytes(), S.Bytes() + sig := make([]byte, 65) + + copy(sig[32-len(r):32], r) + copy(sig[64-len(s):64], s) + + 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) + } + + sig[64] = v + return sig +} diff --git a/x/evm/types/msg_test.go b/x/evm/types/msg_test.go new file mode 100644 index 000000000..c7e5ec206 --- /dev/null +++ b/x/evm/types/msg_test.go @@ -0,0 +1,123 @@ +package types + +import ( + "bytes" + "fmt" + "math/big" + "testing" + + 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" +) + +func TestMsgEthereumTx(t *testing.T) { + addr := GenerateEthAddress() + + msg1 := NewMsgEthereumTx(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")) + 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) + require.Panics(t, func() { msg3.GetSigners() }) + require.Panics(t, func() { msg3.GetSignBytes() }) +} + +func TestMsgEthereumTxValidation(t *testing.T) { + testCases := []struct { + nonce uint64 + to ethcmn.Address + amount *big.Int + gasLimit uint64 + gasPrice *big.Int + payload []byte + expectPass bool + }{ + {amount: big.NewInt(100), gasPrice: big.NewInt(100000), expectPass: true}, + {amount: big.NewInt(-1), gasPrice: big.NewInt(100000), expectPass: false}, + {amount: big.NewInt(100), gasPrice: big.NewInt(-1), expectPass: false}, + } + + for i, tc := range testCases { + msg := NewMsgEthereumTx(tc.nonce, tc.to, tc.amount, tc.gasLimit, tc.gasPrice, tc.payload) + + if tc.expectPass { + require.Nil(t, msg.ValidateBasic(), "test: %v", i) + } else { + require.NotNil(t, msg.ValidateBasic(), "test: %v", i) + } + } +} + +func TestMsgEthereumTxRLPSignBytes(t *testing.T) { + addr := ethcmn.BytesToAddress([]byte("test_address")) + chainID := big.NewInt(3) + + msg := NewMsgEthereumTx(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")) + + raw, err := rlp.EncodeToBytes(msg) + require.NoError(t, err) + require.Equal(t, ethcmn.FromHex("E48080830186A0940000000000000000746573745F61646472657373808474657374808080"), raw) +} + +func TestMsgEthereumTxRLPDecode(t *testing.T) { + var msg MsgEthereumTx + + raw := ethcmn.FromHex("E48080830186A0940000000000000000746573745F61646472657373808474657374808080") + addr := ethcmn.BytesToAddress([]byte("test_address")) + expectedMsg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test")) + + err := rlp.Decode(bytes.NewReader(raw), &msg) + require.NoError(t, err) + require.Equal(t, expectedMsg.Data, msg.Data) +} + +func TestMsgEthereumTxHash(t *testing.T) { + addr := ethcmn.BytesToAddress([]byte("test_address")) + msg := NewMsgEthereumTx(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) + + resultAddr, err := msg.VerifySig(chainID) + require.NoError(t, err) + require.Equal(t, addr, resultAddr) +} + +func TestMsgEthereumTxAmino(t *testing.T) { + addr := GenerateEthAddress() + msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test")) + + raw, err := msgCodec.MarshalBinaryBare(msg) + require.NoError(t, err) + + var msg2 MsgEthereumTx + + err = msgCodec.UnmarshalBinaryBare(raw, &msg2) + require.NoError(t, err) + require.Equal(t, msg.Data, msg2.Data) +} diff --git a/types/utils.go b/x/evm/types/utils.go similarity index 70% rename from types/utils.go rename to x/evm/types/utils.go index ae4c7c09b..0345ae9a3 100644 --- a/types/utils.go +++ b/x/evm/types/utils.go @@ -2,17 +2,21 @@ package types import ( "crypto/ecdsa" - "crypto/sha256" "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" ethcmn "github.com/ethereum/go-ethereum/common" ethcrypto "github.com/ethereum/go-ethereum/crypto" + ethsha "github.com/ethereum/go-ethereum/crypto/sha3" + "github.com/ethereum/go-ethereum/rlp" "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() @@ -23,11 +27,6 @@ func GenerateEthAddress() ethcmn.Address { return PrivKeyToEthAddress(priv) } -// PrivKeyToEthAddress generates an Ethereum address given an ECDSA private key. -func PrivKeyToEthAddress(p *ecdsa.PrivateKey) ethcmn.Address { - return ethcrypto.PubkeyToAddress(p.PublicKey) -} - // ValidateSigner attempts to validate a signer for a given slice of bytes over // which a signature and signer is given. An error is returned if address // derived from the signature and bytes signed does not match the given signer. @@ -43,10 +42,11 @@ func ValidateSigner(signBytes, sig []byte, signer ethcmn.Address) error { return nil } -// GetStdTxSignBytes returns the signature bytes for an auth.StdTx transaction -// that is compatible with Ethereum's signature mechanism. -func GetStdTxSignBytes(chainID string, accNum int64, seq int64, fee auth.StdFee, msgs []sdk.Msg, memo string) []byte { - signBytes := auth.StdSignBytes(chainID, accNum, seq, fee, msgs, "") - hash := sha256.Sum256(signBytes) - return hash[:] +func rlpHash(x interface{}) (hash ethcmn.Hash) { + hasher := ethsha.NewKeccak256() + + rlp.Encode(hasher, x) + hasher.Sum(hash[:0]) + + return }