From fbf3137b85b3e6328ee26c153a44f187a7e31b35 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 8 Aug 2018 21:06:34 -0400 Subject: [PATCH] Merge pull request #451: Transaction Implementation --- Gopkg.lock | 110 +++++------ Gopkg.toml | 13 +- app/ethermint.go | 71 +++++-- handlers/ante.go | 196 ++++++++++++++++++++ types/errors.go | 28 +++ types/tx.go | 441 ++++++++++++++++++++++++++++++++++++++++++++ types/tx_test.go | 326 ++++++++++++++++++++++++++++++++ types/utils.go | 41 ++++ types/utils_test.go | 36 ++++ types/wire.go | 22 +++ 10 files changed, 1214 insertions(+), 70 deletions(-) create mode 100644 handlers/ante.go create mode 100644 types/errors.go create mode 100644 types/tx.go create mode 100644 types/tx_test.go create mode 100644 types/utils.go create mode 100644 types/utils_test.go create mode 100644 types/wire.go diff --git a/Gopkg.lock b/Gopkg.lock index 396345f7..4b8fd799 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,30 +3,29 @@ [[projects]] branch = "master" - digest = "1:9881039571249d9ff92d4a3383a015c233a4c172f6b5a3d6d71e6c4bfdb70efc" + digest = "1:fcdf62d2d7e43c2565d6f8707ab4eae54dac702ed4bafb194b85139f0508929f" name = "github.com/aristanetworks/goarista" packages = ["monotime"] pruneopts = "T" - revision = "32f94db2e6faa2c7250286dfb4c7ad3dc0f3ead2" + revision = "b2d71c282dc706f4b4f6c15b65810e1202ecd53f" [[projects]] branch = "master" - digest = "1:cafb561ce87d0eaa309ad6853380d437df3c1142561c5afa700311825aa38df1" + digest = "1:d4d66abd43dbb9b5f5e6a176c5ed279c289f8db734904c047d95113a04aa2e60" name = "github.com/btcsuite/btcd" packages = ["btcec"] pruneopts = "T" - revision = "fdfc19097e7ac6b57035062056f5b7b4638b8898" + revision = "cf05f92c3f815bbd5091ed6c73eff51f7b1945e8" [[projects]] - branch = "master" - digest = "1:20d11bf9fd5c6d8f7bb393229fdd981db8b4350aa9a05f28856b64640851c9b8" + digest = "1:d0d998526cfb68788229a31c16a557fdf1fbbb510654be6b3732c2758e06b533" name = "github.com/btcsuite/btcutil" packages = ["bech32"] pruneopts = "T" - revision = "ab6388e0c60ae4834a1f57511e20c17b5f78be4b" + revision = "d4cc87b860166d00d6b5b9e0d3b3d71d6088d4d4" [[projects]] - digest = "1:83f4d10d8cbc8174248ac57b9788ff94f9e3af1999e8708fc50f80b7b91949fb" + digest = "1:36773b598dec105de46a87978ae14e64c8d2c45aa556b8e0ddfc62d6abc7c47e" name = "github.com/cosmos/cosmos-sdk" packages = [ "baseapp", @@ -37,11 +36,11 @@ "x/auth", ] pruneopts = "T" - revision = "1a1373cc220e402397ad536aee6b8f5b068914c6" - version = "v0.21.0" + revision = "23e3d5ac12145c02fcb4b4767d7dfccad782aee5" + version = "v0.23.1" [[projects]] - digest = "1:3aa953edddec96fd00285789ccd4a31efaff0a2979a3e35b77f5c19d5eaa37f7" + digest = "1:52f195ad0e20a92d8604c1ba3cd246c61644c03eaa454b5acd41be89841e0d10" name = "github.com/davecgh/go-spew" packages = ["spew"] pruneopts = "T" @@ -57,7 +56,7 @@ revision = "0bce6a6887123b67a60366d2c9fe2dfb74289d2e" [[projects]] - digest = "1:24ae9f4a9d6e2bed9aca667af245834c9a80c39d5ae32e3fc99a2ace91287047" + digest = "1:3238a0c355a81640974751f7d3bab21bf91035165f75c2c457959425c0422a4b" name = "github.com/ethereum/go-ethereum" packages = [ "common", @@ -94,7 +93,7 @@ version = "v1.8.11" [[projects]] - digest = "1:b18534450f89f7007960ff1804d63fb0cc6e7d1989446fcb05d77fb24afc51fc" + digest = "1:0b9c3ad6c948d57a379da9c4e1cdd989b1c73ddc5ec8673f52a9539ce60a109b" name = "github.com/go-kit/kit" packages = [ "log", @@ -122,7 +121,7 @@ version = "v1.7.0" [[projects]] - digest = "1:2a1db9bae44464f781d3637b67df38e896c6e1b9c902e27d24ee9037cb50f23b" + digest = "1:da39f4a22829ca95e63566208e0ea42d6f055f41dff1b14fdab88d88f62df653" name = "github.com/gogo/protobuf" packages = [ "gogoproto", @@ -133,11 +132,11 @@ "types", ] pruneopts = "T" - revision = "1adfc126b41513cc696b209667c8656ea7aac67c" - version = "v1.0.0" + revision = "636bf0302bc95575d69441b25a2603156ffdddf1" + version = "v1.1.1" [[projects]] - digest = "1:6f3df7b8eccb559fa1bda8dae71fdb5f24da5e9aec2696e21f19e6d24062602f" + digest = "1:832e17df5ff8bbe0e0693d2fb46c5e53f96c662ee804049ce3ab6557df74e3ab" name = "github.com/golang/protobuf" packages = [ "proto", @@ -147,19 +146,18 @@ "ptypes/timestamp", ] pruneopts = "T" - revision = "925541529c1fa6821df4e44ce2723319eb2be768" - version = "v1.0.0" + revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" + version = "v1.1.0" [[projects]] branch = "master" - digest = "1:968462840e6d86b12990015ac6ab297c022ccde102953040724be1df0e9e6c96" + digest = "1:6027b20c168728321bd99ad01f35118eded457b01c03e647a84833ab331f2f5b" name = "github.com/golang/snappy" packages = ["."] pruneopts = "T" revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" [[projects]] - branch = "master" digest = "1:cf296baa185baae04a9a7004efee8511d08e2f5f51d4cbe5375da89722d681db" name = "github.com/hashicorp/golang-lru" packages = [ @@ -179,7 +177,7 @@ [[projects]] branch = "master" - digest = "1:14f2079ea27e7c67ecdab4f35e774463abc4f9d1806b5d674c0594b52127ab1d" + digest = "1:dc6b1a6801b3055e9bd3da4cd1e568606eb48118cc6f28e947783aa5d998ad74" name = "github.com/jmhodges/levigo" packages = ["."] pruneopts = "T" @@ -210,15 +208,15 @@ version = "v1.0.0" [[projects]] - digest = "1:540558c17f78ee4f056aa043cf3389c283b56754db79112a2d64172e80e685db" + digest = "1:6cae6970d70fc5fe75bf83c48ee33e9c4c561a62d0b033254bee8dd5942b815a" name = "github.com/rs/cors" packages = ["."] pruneopts = "T" - revision = "ca016a06a5753f8ba03029c0aa5e54afb1bf713f" - version = "v1.4.0" + revision = "3fb1b69b103a84de38a19c3c6ec073dd6caa4d3f" + version = "v1.5.0" [[projects]] - digest = "1:daab027a0bfb143afb503f7b63673bfa8d44f69ce9484c6d19b97957aadc1252" + digest = "1:8be8b3743fc9795ec21bbd3e0fc28ff6234018e1a269b0a7064184be95ac13e0" name = "github.com/spf13/cobra" packages = ["."] pruneopts = "T" @@ -234,7 +232,7 @@ version = "v1.0.1" [[projects]] - digest = "1:c7f05297d9ad389d81e6d764388d97c4b6a64665eff9fd2550fbdd8545430b80" + digest = "1:e95496462101745805bd4e041a5b841e108c7cf761264d53648246308de2761e" name = "github.com/stretchr/testify" packages = [ "assert", @@ -246,7 +244,7 @@ [[projects]] branch = "master" - digest = "1:9c39a878048f4a5468675b814fb7d2528d622f8c3612511ff0b5e2a48d451ad2" + digest = "1:7d44c4d11eb65cfdc78c76040f37ef305b16474c019c98a8a7cf188fece2d574" name = "github.com/syndtr/goleveldb" packages = [ "leveldb", @@ -267,7 +265,7 @@ [[projects]] branch = "master" - digest = "1:8403202d034640f399279a4f735faabefeb6ee64bbcb03c9c93be1d4c7230382" + digest = "1:2b15c0442dc80b581ce7028b2e43029d2f3f985da43cb1d55f7bcdeca785bda0" name = "github.com/tendermint/ed25519" packages = [ ".", @@ -278,7 +276,7 @@ revision = "d8387025d2b9d158cf4efb07e7ebf814bcce2057" [[projects]] - digest = "1:4431caadcd2cc6a245bf0f6f61884029f5cc70833a8cc458cd1ba4a578b18c71" + digest = "1:0e2addab3f64ece97ca434b2bf2d4e8cb54a4509904a03be8c81da3fc2ddb245" name = "github.com/tendermint/go-amino" packages = ["."] pruneopts = "T" @@ -286,7 +284,7 @@ version = "0.10.1" [[projects]] - digest = "1:f11b64e85907d8909a820d0cd793dff19dc268432626411398aa0e26e8a61338" + digest = "1:bf042d2f7d1252b9dcae8e694e2f0a9b5294cb357c086fd86dc540d2f32c9fdf" name = "github.com/tendermint/iavl" packages = ["."] pruneopts = "T" @@ -294,13 +292,16 @@ version = "v0.9.2" [[projects]] - digest = "1:f056bee4848f0fff6c92e6b99d4972b175d8a8eec9700df30b143b2e6cc48ea8" + digest = "1:9f6704ae2aedbadf616e5850375c504909d46b6ea57d4679de2b7cbc715f08e1" name = "github.com/tendermint/tendermint" packages = [ "abci/server", "abci/types", "crypto", + "crypto/ed25519", + "crypto/encoding/amino", "crypto/merkle", + "crypto/secp256k1", "crypto/tmhash", "libs/bech32", "libs/common", @@ -311,28 +312,19 @@ "types", ] pruneopts = "T" - revision = "5ff65274b84ea905787a48512cc3124385bddf2f" - version = "v0.22.2" + revision = "d542d2c3945116697f60451e6a407082c41c3cc9" + version = "v0.22.8" [[projects]] branch = "master" - digest = "1:2c4971d2da7bb27fa225a119dc96af2119dd096869c1228438a0b5fda5f6fe15" + digest = "1:2cbe8758697d867fcebf73bcc69dff8e8abaa7fd65e5704e0744e522ccff4e6a" name = "golang.org/x/crypto" - packages = [ - "internal/subtle", - "nacl/secretbox", - "openpgp/armor", - "openpgp/errors", - "poly1305", - "ripemd160", - "salsa20/salsa", - ] + packages = ["ripemd160"] pruneopts = "T" - revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602" + revision = "f027049dab0ad238e394a753dba2d14753473a04" [[projects]] - branch = "master" - digest = "1:501f63fec0206818ab3dac086383bd2146841951c6bd6f3c4f72613f113f25fd" + digest = "1:5fdc7adede42f80d6201258355d478d856778e21d735f14972abd8ff793fdbf7" name = "golang.org/x/net" packages = [ "context", @@ -345,10 +337,10 @@ "websocket", ] pruneopts = "T" - revision = "d0887baf81f4598189d4e12a37c6da86f0bba4d0" + revision = "292b43bbf7cb8d35ddf40f8d5100ef3837cced3f" [[projects]] - digest = "1:24db346d9931fe01f1e9a02aba78ba22c1ecd55bf0f79dd10ba5169719cf002d" + digest = "1:6164911cb5e94e8d8d5131d646613ff82c14f5a8ce869de2f6d80d9889df8c5a" name = "golang.org/x/text" packages = [ "collate", @@ -371,14 +363,14 @@ version = "v0.3.0" [[projects]] - digest = "1:cfa1bbb9ee86ade0914bd5f8e8516386cf7d573957191ecb5163d8f6e023ca0c" + digest = "1:8cfa91d1b7f6b66fa9b1a738a4bc1325837b861e63fb9a2919931d68871bb770" name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] pruneopts = "T" revision = "7fd901a49ba6a7f87732eb344f6e3c5b19d1b200" [[projects]] - digest = "1:1faab7c2380bc84698a62531c4af8c9475fbc7b3b1b2696f2f94feff97c47a49" + digest = "1:adafc60b1d4688759f3fc8f9089e71dd17abd123f4729de6b913bf08c9143770" name = "google.golang.org/grpc" packages = [ ".", @@ -390,9 +382,11 @@ "credentials", "encoding", "encoding/proto", - "grpclb/grpc_lb_v1/messages", "grpclog", "internal", + "internal/backoff", + "internal/channelz", + "internal/grpcrand", "keepalive", "metadata", "naming", @@ -406,8 +400,8 @@ "transport", ] pruneopts = "T" - revision = "d11072e7ca9811b1100b80ca0269ac831f06d024" - version = "v1.11.3" + revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8" + version = "v1.13.0" [[projects]] digest = "1:3ccd10c863188cfe0d936fcfe6a055c95362e43af8e7039e33baade846928e74" @@ -419,7 +413,7 @@ [[projects]] branch = "v2" - digest = "1:35056a4c53d0b725735422545c3c11bdc9007da2fdb644fee96f3a6b7c42c69f" + digest = "1:dae137be246befa42ce4b48c0feff2c5796b8a5027139a283f31a21173744410" name = "gopkg.in/karalabe/cookiejar.v2" packages = ["collections/prque"] pruneopts = "T" @@ -441,6 +435,7 @@ "github.com/cosmos/cosmos-sdk/store", "github.com/cosmos/cosmos-sdk/types", "github.com/cosmos/cosmos-sdk/wire", + "github.com/cosmos/cosmos-sdk/x/auth", "github.com/ethereum/go-ethereum/common", "github.com/ethereum/go-ethereum/common/math", "github.com/ethereum/go-ethereum/consensus", @@ -450,14 +445,19 @@ "github.com/ethereum/go-ethereum/core/state", "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/sha3", "github.com/ethereum/go-ethereum/ethdb", "github.com/ethereum/go-ethereum/params", "github.com/ethereum/go-ethereum/rlp", "github.com/ethereum/go-ethereum/rpc", "github.com/ethereum/go-ethereum/trie", "github.com/hashicorp/golang-lru", + "github.com/pkg/errors", "github.com/stretchr/testify/require", + "github.com/tendermint/tendermint/libs/common", "github.com/tendermint/tendermint/libs/db", + "github.com/tendermint/tendermint/libs/log", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index de39b8ac..4a6a8e23 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -4,10 +4,11 @@ [[constraint]] name = "github.com/cosmos/cosmos-sdk" - version = "=0.21.0" + version = "=0.23.1" [[constraint]] name = "github.com/hashicorp/golang-lru" + revision = "0fb14efe8c47ae851c0034ed7a448854d3d34cf3" [[constraint]] name = "github.com/spf13/cobra" @@ -19,11 +20,19 @@ [[override]] name = "github.com/tendermint/tendermint" - version = "=0.22.2" + version = "=v0.22.8" [[constraint]] name = "github.com/stretchr/testify" version = "=1.2.2" +[[constraint]] + name = "github.com/pkg/errors" + version = "=0.8.0" + +[[override]] + name = "gopkg.in/fatih/set.v0" + version = "=0.1.0" + [prune] go-tests = true diff --git a/app/ethermint.go b/app/ethermint.go index ff68ac69..575452eb 100644 --- a/app/ethermint.go +++ b/app/ethermint.go @@ -2,38 +2,73 @@ package app import ( bam "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" + + "github.com/cosmos/ethermint/handlers" + "github.com/cosmos/ethermint/types" + + ethcmn "github.com/ethereum/go-ethereum/common" + ethparams "github.com/ethereum/go-ethereum/params" + + tmcmn "github.com/tendermint/tendermint/libs/common" + dbm "github.com/tendermint/tendermint/libs/db" + tmlog "github.com/tendermint/tendermint/libs/log" ) const ( appName = "Ethermint" ) -// EthermintApp implements an extended ABCI application. -type EthermintApp struct { - *bam.BaseApp +type ( + // EthermintApp implements an extended ABCI application. It is an application + // that may process transactions through Ethereum's EVM running atop of + // Tendermint consensus. + EthermintApp struct { + *bam.BaseApp - codec *wire.Codec - sealed bool + codec *wire.Codec + sealed bool - // TODO: stores and keys + accountKey *sdk.KVStoreKey + accountMapper auth.AccountMapper + // TODO: keys, stores, mappers, and keepers + } - // TODO: keepers - - // TODO: mappers -} + // Options is a function signature that provides the ability to modify + // options of an EthermintApp during initialization. + Options func(*EthermintApp) +) // NewEthermintApp returns a reference to a new initialized Ethermint // application. -func NewEthermintApp(opts ...func(*EthermintApp)) *EthermintApp { - app := &EthermintApp{} +func NewEthermintApp( + logger tmlog.Logger, db dbm.DB, ethChainCfg *ethparams.ChainConfig, + sdkAddr ethcmn.Address, opts ...Options, +) *EthermintApp { - // TODO: implement constructor + codec := CreateCodec() + app := &EthermintApp{ + BaseApp: bam.NewBaseApp(appName, codec, logger, db), + codec: codec, + accountKey: sdk.NewKVStoreKey("accounts"), + } + app.accountMapper = auth.NewAccountMapper(codec, app.accountKey, auth.ProtoBaseAccount) + + app.SetTxDecoder(types.TxDecoder(codec, sdkAddr)) + app.SetAnteHandler(handlers.AnteHandler(app.accountMapper)) + app.MountStoresIAVL(app.accountKey) for _, opt := range opts { opt(app) } + err := app.LoadLatestVersion(app.accountKey) + if err != nil { + tmcmn.Exit(err.Error()) + } + app.seal() return app } @@ -43,3 +78,13 @@ func NewEthermintApp(opts ...func(*EthermintApp)) *EthermintApp { func (app *EthermintApp) seal() { app.sealed = true } + +// CreateCodec creates a new amino wire codec and registers all the necessary +// structures and interfaces needed for the application. +func CreateCodec() *wire.Codec { + codec := wire.NewCodec() + + // Register other modules, types, and messages... + types.RegisterWire(codec) + return codec +} diff --git a/handlers/ante.go b/handlers/ante.go new file mode 100644 index 00000000..09ad13c1 --- /dev/null +++ b/handlers/ante.go @@ -0,0 +1,196 @@ +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" + + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +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, am auth.AccountMapper, +) (newCtx sdk.Context, res sdk.Result, abort bool) + +// AnteHandler handles Ethereum transactions and passes SDK transactions to the +// embeddedAnteHandler if it's an Ethermint transaction. The ante handler gets +// invoked after the BaseApp performs the runTx. At this point, the transaction +// should be properly decoded via the TxDecoder and should be of a proper type, +// Transaction or EmbeddedTx. +func AnteHandler(am auth.AccountMapper) sdk.AnteHandler { + return func(sdkCtx sdk.Context, tx sdk.Tx) (newCtx sdk.Context, res sdk.Result, abort bool) { + var ( + gasLimit int64 + handler internalAnteHandler + ) + + switch tx := tx.(type) { + case types.Transaction: + gasLimit = int64(tx.Data.GasLimit) + handler = handleEthTx + case types.EmbeddedTx: + 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, am) + } +} + +// handleEthTx implements an ante handler for an Ethereum transaction. It +// validates the signature and if valid returns an OK result. +// +// TODO: Do we need to do any further validation or account manipulation +// (e.g. increment nonce)? +func handleEthTx(sdkCtx sdk.Context, tx sdk.Tx, am auth.AccountMapper) (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 { + // TODO: ErrInternal may not be correct error to throw here? + return sdkCtx, sdk.ErrInternal(fmt.Sprintf("invalid chainID: %s", sdkCtx.ChainID())).Result(), true + } + + // validate signature + gethTx := ethTx.ConvertTx(chainID) + signer := ethtypes.NewEIP155Signer(chainID) + + _, err := signer.Sender(&gethTx) + if err != nil { + return sdkCtx, sdk.ErrUnauthorized("signature verification failed").Result(), true + } + + 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, am auth.AccountMapper) (sdk.Context, sdk.Result, bool) { + etx, ok := tx.(types.EmbeddedTx) + if !ok { + return sdkCtx, sdk.ErrInternal(fmt.Sprintf("invalid transaction: %T", tx)).Result(), true + } + + if err := validateEmbeddedTxBasic(etx); err != nil { + return sdkCtx, err.Result(), true + } + + signerAddrs := etx.GetRequiredSigners() + signerAccs := make([]auth.Account, len(signerAddrs)) + + // validate signatures + for i, sig := range etx.Signatures { + signer := ethcmn.BytesToAddress(signerAddrs[i].Bytes()) + + signerAcc, err := validateSignature(sdkCtx, etx, signer, sig, am) + if err.Code() != sdk.CodeOK { + return sdkCtx, err.Result(), false + } + + // TODO: Fees! + + am.SetAccount(sdkCtx, signerAcc) + signerAccs[i] = signerAcc + } + + newCtx := auth.WithSigners(sdkCtx, signerAccs) + + return newCtx, sdk.Result{GasWanted: etx.Fee.Gas}, false +} + +// validateEmbeddedTxBasic validates an EmbeddedTx based on things that don't +// depend on the context. +func validateEmbeddedTxBasic(etx types.EmbeddedTx) (err sdk.Error) { + sigs := etx.Signatures + if len(sigs) == 0 { + return sdk.ErrUnauthorized("transaction missing signatures") + } + + signerAddrs := etx.GetRequiredSigners() + if len(sigs) != len(signerAddrs) { + return sdk.ErrUnauthorized("invalid number of transaction signers") + } + + return nil +} + +func validateSignature( + sdkCtx sdk.Context, etx types.EmbeddedTx, signer ethcmn.Address, + sig []byte, am auth.AccountMapper, +) (acc auth.Account, sdkErr sdk.Error) { + + chainID := sdkCtx.ChainID() + + acc = am.GetAccount(sdkCtx, signer.Bytes()) + if acc == nil { + return nil, sdk.ErrUnknownAddress(fmt.Sprintf("no account with address %s found", signer)) + } + + signEtx := types.EmbeddedTxSign{ + ChainID: chainID, + AccountNumber: acc.GetAccountNumber(), + Sequence: acc.GetSequence(), + Messages: etx.Messages, + Fee: etx.Fee, + } + + err := acc.SetSequence(signEtx.Sequence + 1) + if err != nil { + return nil, sdk.ErrInternal(err.Error()) + } + + signBytes, err := signEtx.Bytes() + if err != nil { + return nil, sdk.ErrInternal(err.Error()) + } + + // consume gas for signature verification + sdkCtx.GasMeter().ConsumeGas(verifySigCost, "ante verify") + + if err := types.ValidateSigner(signBytes, sig, signer); err != nil { + return nil, sdk.ErrUnauthorized(err.Error()) + } + + return +} diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 00000000..03eee306 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,28 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // DefaultCodespace reserves a Codespace for Ethermint, as 0 and 1 are + // reserved by SDK. + DefaultCodespace sdk.CodespaceType = 2 + + // CodeInvalidValue reserves the CodeInvalidValue with first non-OK + // codetype. + CodeInvalidValue sdk.CodeType = 1 +) + +func codeToDefaultMsg(code sdk.CodeType) string { + switch code { + 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) +} diff --git a/types/tx.go b/types/tx.go new file mode 100644 index 00000000..a7043d63 --- /dev/null +++ b/types/tx.go @@ -0,0 +1,441 @@ +package types + +import ( + "bytes" + "crypto/ecdsa" + "crypto/sha256" + "encoding/json" + "fmt" + "math/big" + "sync/atomic" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "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" + ethsha "github.com/ethereum/go-ethereum/crypto/sha3" + "github.com/ethereum/go-ethereum/rlp" + + "github.com/pkg/errors" +) + +const ( + // TypeTxEthereum reflects an Ethereum Transaction type. + TypeTxEthereum = "Ethereum" +) + +// ---------------------------------------------------------------------------- +// Ethereum transaction +// ---------------------------------------------------------------------------- + +type ( + // Transaction implements the Ethereum transaction structure as an exact + // copy. 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 sdk.Int `json:"gasPrice"` + GasLimit uint64 `json:"gas"` + Recipient *ethcmn.Address `json:"to"` // nil means contract creation + Amount sdk.Int `json:"value"` + Payload []byte `json:"input"` + Signature *EthSignature `json:"signature"` + + // hash is only used when marshaling to JSON + Hash *ethcmn.Hash `json:"hash"` + } + + // EthSignature reflects an Ethereum signature. We wrap this in a structure + // to support Amino serialization of transactions. + EthSignature struct { + v, r, s *big.Int + } +) + +// NewEthSignature returns a new instantiated Ethereum signature. +func NewEthSignature(v, r, s *big.Int) *EthSignature { + return &EthSignature{v, r, s} +} + +func (es *EthSignature) sanitize() { + if es.v == nil { + es.v = new(big.Int) + } + if es.r == nil { + es.r = new(big.Int) + } + if es.s == nil { + es.s = new(big.Int) + } +} + +// MarshalAmino defines a custom encoding scheme for a EthSignature. +func (es EthSignature) MarshalAmino() ([3]string, error) { + es.sanitize() + return ethSigMarshalAmino(es) +} + +// UnmarshalAmino defines a custom decoding scheme for a EthSignature. +func (es *EthSignature) UnmarshalAmino(raw [3]string) error { + es.sanitize() + return ethSigUnmarshalAmino(es, raw) +} + +// NewTransaction mimics ethereum's NewTransaction function. It returns a +// reference to a new Ethereum Transaction. +func NewTransaction( + nonce uint64, to ethcmn.Address, amount sdk.Int, + gasLimit uint64, gasPrice sdk.Int, payload []byte, +) Transaction { + + if len(payload) > 0 { + payload = ethcmn.CopyBytes(payload) + } + + txData := TxData{ + Recipient: &to, + AccountNonce: nonce, + Payload: payload, + GasLimit: gasLimit, + Amount: amount, + Price: gasPrice, + Signature: NewEthSignature(new(big.Int), new(big.Int), new(big.Int)), + } + + return Transaction{Data: txData} +} + +// 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 sdk.Int, priv *ecdsa.PrivateKey) { + h := rlpHash([]interface{}{ + tx.Data.AccountNonce, + tx.Data.Price.BigInt(), + tx.Data.GasLimit, + tx.Data.Recipient, + tx.Data.Amount.BigInt(), + tx.Data.Payload, + chainID.BigInt(), uint(0), uint(0), + }) + + sig, err := ethcrypto.Sign(h[:], 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.BigInt(), big.NewInt(2)) + v.Add(v, chainIDMul) + } + + tx.Data.Signature.v = v + tx.Data.Signature.r = r + tx.Data.Signature.s = s +} + +// Type implements the sdk.Msg interface. It returns the type of the +// Transaction. +func (tx Transaction) Type() string { + return TypeTxEthereum +} + +// 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} +} + +// ConvertTx attempts to converts a Transaction to a new Ethereum transaction +// with the signature set. The signature if first recovered and then a new +// Transaction is created with that signature. If setting the signature fails, +// a panic will be triggered. +func (tx Transaction) ConvertTx(chainID *big.Int) ethtypes.Transaction { + gethTx := ethtypes.NewTransaction( + tx.Data.AccountNonce, *tx.Data.Recipient, tx.Data.Amount.BigInt(), + tx.Data.GasLimit, tx.Data.Price.BigInt(), tx.Data.Payload, + ) + + sig := recoverEthSig(tx.Data.Signature, chainID) + signer := ethtypes.NewEIP155Signer(chainID) + + gethTx, err := gethTx.WithSignature(signer, sig) + if err != nil { + panic(errors.Wrap(err, "failed to convert transaction with a given signature")) + } + + return *gethTx +} + +// 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 *wire.Codec) (EmbeddedTx, sdk.Error) { + etx := EmbeddedTx{} + + err := codec.UnmarshalBinary(tx.Data.Payload, &etx) + if err != nil { + return EmbeddedTx{}, sdk.ErrTxDecode("failed to encode embedded tx") + } + + return etx, nil +} + +// ---------------------------------------------------------------------------- +// embedded SDK transaction +// ---------------------------------------------------------------------------- + +type ( + // EmbeddedTx implements an SDK transaction. It is to be encoded into the + // payload field of an Ethereum transaction in order to route and handle SDK + // transactions. + EmbeddedTx struct { + Messages []sdk.Msg `json:"messages"` + Fee auth.StdFee `json:"fee"` + Signatures [][]byte `json:"signatures"` + } + + // embeddedSignDoc implements a simple SignDoc for a EmbeddedTx signer to + // sign over. + embeddedSignDoc struct { + ChainID string `json:"chainID"` + AccountNumber int64 `json:"accountNumber"` + Sequence int64 `json:"sequence"` + Messages []json.RawMessage `json:"messages"` + Fee json.RawMessage `json:"fee"` + } + + // EmbeddedTxSign implements a structure for containing the information + // necessary for building and signing an EmbeddedTx. + EmbeddedTxSign struct { + ChainID string + AccountNumber int64 + Sequence int64 + Messages []sdk.Msg + Fee auth.StdFee + } +) + +// GetMsgs implements the sdk.Tx interface. It returns all the SDK transaction +// messages. +func (etx EmbeddedTx) GetMsgs() []sdk.Msg { + return etx.Messages +} + +// GetRequiredSigners returns all the required signers of an SDK transaction +// accumulated from messages. It returns them in a deterministic fashion given +// a list of messages. +func (etx EmbeddedTx) GetRequiredSigners() []sdk.AccAddress { + seen := map[string]bool{} + + var signers []sdk.AccAddress + for _, msg := range etx.GetMsgs() { + for _, addr := range msg.GetSigners() { + if !seen[addr.String()] { + signers = append(signers, sdk.AccAddress(addr)) + seen[addr.String()] = true + } + } + } + + return signers +} + +// Bytes returns the EmbeddedTxSign signature bytes for a signer to sign over. +func (ets EmbeddedTxSign) Bytes() ([]byte, error) { + sigBytes, err := EmbeddedSignBytes(ets.ChainID, ets.AccountNumber, ets.Sequence, ets.Messages, ets.Fee) + if err != nil { + return nil, err + } + + hash := sha256.Sum256(sigBytes) + return hash[:], nil +} + +// EmbeddedSignBytes creates signature bytes for a signer to sign an embedded +// transaction. The signature bytes require a chainID and an account number. +// The signature bytes are JSON encoded. +func EmbeddedSignBytes(chainID string, accnum, sequence int64, msgs []sdk.Msg, fee auth.StdFee) ([]byte, error) { + var msgsBytes []json.RawMessage + for _, msg := range msgs { + msgsBytes = append(msgsBytes, json.RawMessage(msg.GetSignBytes())) + } + + signDoc := embeddedSignDoc{ + ChainID: chainID, + AccountNumber: accnum, + Sequence: sequence, + Messages: msgsBytes, + Fee: json.RawMessage(fee.Bytes()), + } + + bz, err := typesCodec.MarshalJSON(signDoc) + if err != nil { + errors.Wrap(err, "failed to JSON encode EmbeddedSignDoc") + } + + return bz, nil +} + +// ---------------------------------------------------------------------------- +// Utilities +// ---------------------------------------------------------------------------- + +// TxDecoder returns an sdk.TxDecoder that given raw transaction bytes, +// attempts to decode them into a Transaction or an EmbeddedTx or returning an +// error if decoding fails. +func TxDecoder(codec *wire.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("txBytes are empty") + } + + // The given codec should have all the appropriate message types + // registered. + err := codec.UnmarshalBinary(txBytes, &tx) + if err != nil { + return nil, sdk.ErrTxDecode("failed to decode tx").TraceSDK(err.Error()) + } + + // If the transaction is routed as an SDK transaction, decode and + // return the embedded 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(es *EthSignature, chainID *big.Int) []byte { + var v byte + + r, s := es.r.Bytes(), es.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(es.v.Uint64() - 27) + } else { + chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2)) + V := new(big.Int).Sub(es.v, chainIDMul) + + v = byte(V.Uint64() - 35) + } + + sig[64] = v + return sig +} + +func rlpHash(x interface{}) (h ethcmn.Hash) { + hasher := ethsha.NewKeccak256() + + rlp.Encode(hasher, x) + hasher.Sum(h[:0]) + + return h +} + +func ethSigMarshalAmino(es EthSignature) (raw [3]string, err error) { + vb, err := es.v.MarshalText() + if err != nil { + return raw, err + } + rb, err := es.r.MarshalText() + if err != nil { + return raw, err + } + sb, err := es.s.MarshalText() + if err != nil { + return raw, err + } + + raw[0], raw[1], raw[2] = string(vb), string(rb), string(sb) + return raw, err +} + +func ethSigUnmarshalAmino(es *EthSignature, raw [3]string) (err error) { + if err = es.v.UnmarshalText([]byte(raw[0])); err != nil { + return + } + if err = es.r.UnmarshalText([]byte(raw[1])); err != nil { + return + } + if err = es.s.UnmarshalText([]byte(raw[2])); err != nil { + return + } + + return +} diff --git a/types/tx_test.go b/types/tx_test.go new file mode 100644 index 00000000..b186f7b9 --- /dev/null +++ b/types/tx_test.go @@ -0,0 +1,326 @@ +package types + +import ( + "crypto/ecdsa" + "fmt" + "math/big" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "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" + "github.com/stretchr/testify/require" +) + +var ( + testChainID = sdk.NewInt(3) + + testPrivKey1, _ = ethcrypto.GenerateKey() + testPrivKey2, _ = ethcrypto.GenerateKey() + + testAddr1 = PrivKeyToEthAddress(testPrivKey1) + testAddr2 = PrivKeyToEthAddress(testPrivKey2) + + testSDKAddress = GenerateEthAddress() +) + +func newTestCodec() *wire.Codec { + codec := wire.NewCodec() + + RegisterWire(codec) + codec.RegisterConcrete(&sdk.TestMsg{}, "test/TestMsg", nil) + + return codec +} + +func newStdFee() auth.StdFee { + return auth.NewStdFee(5000, sdk.NewCoin("photon", 150)) +} + +func newTestEmbeddedTx( + chainID sdk.Int, msgs []sdk.Msg, pKeys []*ecdsa.PrivateKey, + accNums []int64, seqs []int64, fee auth.StdFee, +) sdk.Tx { + + sigs := make([][]byte, len(pKeys)) + + for i, priv := range pKeys { + signEtx := EmbeddedTxSign{chainID.String(), accNums[i], seqs[i], msgs, newStdFee()} + + signBytes, err := signEtx.Bytes() + if err != nil { + panic(err) + } + + sig, err := ethcrypto.Sign(signBytes, priv) + if err != nil { + panic(err) + } + + sigs[i] = sig + } + + return EmbeddedTx{msgs, fee, sigs} +} + +func newTestGethTxs(chainID sdk.Int, pKeys []*ecdsa.PrivateKey, addrs []ethcmn.Address) []ethtypes.Transaction { + txs := make([]ethtypes.Transaction, len(pKeys)) + + for i, priv := range pKeys { + ethTx := ethtypes.NewTransaction( + uint64(i), addrs[i], big.NewInt(10), 100, big.NewInt(100), nil, + ) + + signer := ethtypes.NewEIP155Signer(chainID.BigInt()) + ethTx, _ = ethtypes.SignTx(ethTx, signer, priv) + + txs[i] = *ethTx + } + + return txs +} + +func newTestEthTxs(chainID sdk.Int, pKeys []*ecdsa.PrivateKey, addrs []ethcmn.Address) []Transaction { + txs := make([]Transaction, len(pKeys)) + + for i, priv := range pKeys { + emintTx := NewTransaction( + uint64(i), addrs[i], sdk.NewInt(10), 100, sdk.NewInt(100), nil, + ) + + emintTx.Sign(chainID, priv) + + txs[i] = emintTx + } + + return txs +} + +func newTestSDKTxs( + codec *wire.Codec, chainID sdk.Int, msgs []sdk.Msg, pKeys []*ecdsa.PrivateKey, + accNums []int64, seqs []int64, fee auth.StdFee, +) []Transaction { + + txs := make([]Transaction, len(pKeys)) + etx := newTestEmbeddedTx(chainID, msgs, pKeys, accNums, seqs, fee) + + for i, priv := range pKeys { + payload := codec.MustMarshalBinary(etx) + + emintTx := NewTransaction( + uint64(i), testSDKAddress, sdk.NewInt(10), 100, + sdk.NewInt(100), payload, + ) + + emintTx.Sign(testChainID, priv) + + txs[i] = emintTx + } + + return txs +} + +func TestConvertTx(t *testing.T) { + gethTxs := newTestGethTxs( + testChainID, + []*ecdsa.PrivateKey{testPrivKey1, testPrivKey2}, + []ethcmn.Address{testAddr1, testAddr2}, + ) + ethTxs := newTestEthTxs( + testChainID, + []*ecdsa.PrivateKey{testPrivKey1, testPrivKey2}, + []ethcmn.Address{testAddr1, testAddr2}, + ) + + testCases := []struct { + ethTx ethtypes.Transaction + emintTx Transaction + expectedEq bool + }{ + {gethTxs[0], ethTxs[0], true}, + {gethTxs[0], ethTxs[1], false}, + {gethTxs[1], ethTxs[0], false}, + } + + for i, tc := range testCases { + convertedTx := tc.emintTx.ConvertTx(testChainID.BigInt()) + + if tc.expectedEq { + require.Equal(t, tc.ethTx, convertedTx, fmt.Sprintf("unexpected result: test case #%d", i)) + } else { + require.NotEqual(t, tc.ethTx, convertedTx, fmt.Sprintf("unexpected result: test case #%d", i)) + } + } +} + +func TestValidation(t *testing.T) { + ethTxs := newTestEthTxs( + testChainID, + []*ecdsa.PrivateKey{testPrivKey1}, + []ethcmn.Address{testAddr1}, + ) + + 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 = sdk.NewInt(-1) + return tx + }, true}, + {ethTxs[0], func(msg sdk.Msg) sdk.Msg { + tx := msg.(Transaction) + tx.Data.Amount = sdk.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 TestHasEmbeddedTx(t *testing.T) { + testCodec := newTestCodec() + msgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(testAddr1.Bytes()))} + + sdkTxs := newTestSDKTxs( + testCodec, testChainID, msgs, []*ecdsa.PrivateKey{testPrivKey1}, + []int64{0}, []int64{0}, newStdFee(), + ) + require.True(t, sdkTxs[0].HasEmbeddedTx(testSDKAddress)) + + ethTxs := newTestEthTxs( + testChainID, + []*ecdsa.PrivateKey{testPrivKey1}, + []ethcmn.Address{testAddr1}, + ) + require.False(t, ethTxs[0].HasEmbeddedTx(testSDKAddress)) +} + +func TestGetEmbeddedTx(t *testing.T) { + testCodec := newTestCodec() + msgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(testAddr1.Bytes()))} + + ethTxs := newTestEthTxs( + testChainID, + []*ecdsa.PrivateKey{testPrivKey1}, + []ethcmn.Address{testAddr1}, + ) + sdkTxs := newTestSDKTxs( + testCodec, testChainID, msgs, []*ecdsa.PrivateKey{testPrivKey1}, + []int64{0}, []int64{0}, newStdFee(), + ) + + etx, err := sdkTxs[0].GetEmbeddedTx(testCodec) + require.NoError(t, err) + require.NotEmpty(t, etx.Messages) + + etx, err = ethTxs[0].GetEmbeddedTx(testCodec) + require.Error(t, err) + require.Empty(t, etx.Messages) +} + +func TestTransactionGetMsgs(t *testing.T) { + ethTxs := newTestEthTxs( + testChainID, + []*ecdsa.PrivateKey{testPrivKey1}, + []ethcmn.Address{testAddr1}, + ) + + msgs := ethTxs[0].GetMsgs() + require.Len(t, msgs, 1) + require.Equal(t, ethTxs[0], msgs[0]) + + expectedMsgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(testAddr1.Bytes()))} + etx := newTestEmbeddedTx( + testChainID, expectedMsgs, []*ecdsa.PrivateKey{testPrivKey1}, + []int64{0}, []int64{0}, newStdFee(), + ) + + msgs = etx.GetMsgs() + require.Len(t, msgs, len(expectedMsgs)) + require.Equal(t, expectedMsgs, msgs) +} + +func TestGetRequiredSigners(t *testing.T) { + msgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(testAddr1.Bytes()))} + etx := newTestEmbeddedTx( + testChainID, msgs, []*ecdsa.PrivateKey{testPrivKey1}, + []int64{0}, []int64{0}, newStdFee(), + ) + + signers := etx.(EmbeddedTx).GetRequiredSigners() + require.Equal(t, []sdk.AccAddress{sdk.AccAddress(testAddr1.Bytes())}, signers) +} + +func TestTxDecoder(t *testing.T) { + testCodec := newTestCodec() + txDecoder := TxDecoder(testCodec, testSDKAddress) + msgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(testAddr1.Bytes()))} + + // create a non-SDK Ethereum transaction + emintTx := NewTransaction( + uint64(0), testAddr1, sdk.NewInt(10), 100, sdk.NewInt(100), nil, + ) + emintTx.Sign(testChainID, testPrivKey1) + + // require the transaction to properly decode into a Transaction + txBytes := testCodec.MustMarshalBinary(emintTx) + tx, err := txDecoder(txBytes) + require.NoError(t, err) + require.Equal(t, emintTx, tx) + + // create embedded transaction and encode + etx := newTestEmbeddedTx( + testChainID, msgs, []*ecdsa.PrivateKey{testPrivKey1}, + []int64{0}, []int64{0}, newStdFee(), + ) + + payload := testCodec.MustMarshalBinary(etx) + + expectedEtx := EmbeddedTx{} + testCodec.UnmarshalBinary(payload, &expectedEtx) + + emintTx = NewTransaction( + uint64(0), testSDKAddress, sdk.NewInt(10), 100, + sdk.NewInt(100), payload, + ) + emintTx.Sign(testChainID, testPrivKey1) + + // require the transaction to properly decode into a Transaction + txBytes = testCodec.MustMarshalBinary(emintTx) + tx, err = txDecoder(txBytes) + require.NoError(t, err) + require.Equal(t, expectedEtx, tx) + + // require the decoding to fail when no transaction bytes are given + tx, err = txDecoder([]byte{}) + require.Error(t, err) + require.Nil(t, tx) + + // create a non-SDK Ethereum transaction with an SDK address and garbage payload + emintTx = NewTransaction( + uint64(0), testSDKAddress, sdk.NewInt(10), 100, sdk.NewInt(100), []byte("garbage"), + ) + emintTx.Sign(testChainID, testPrivKey1) + + // require the transaction to fail decoding as the payload is invalid + txBytes = testCodec.MustMarshalBinary(emintTx) + tx, err = txDecoder(txBytes) + require.Error(t, err) + require.Nil(t, tx) +} diff --git a/types/utils.go b/types/utils.go new file mode 100644 index 00000000..01f8261d --- /dev/null +++ b/types/utils.go @@ -0,0 +1,41 @@ +package types + +import ( + "crypto/ecdsa" + "fmt" + + ethcmn "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + + "github.com/pkg/errors" +) + +// GenerateAddress generates an Ethereum address. +func GenerateEthAddress() ethcmn.Address { + priv, err := ethcrypto.GenerateKey() + if err != nil { + panic(err) + } + + return PrivKeyToEthAddress(priv) +} + +// PrivKeyToEthAddress generates an Ethereum address given an ECDSA private key. +func PrivKeyToEthAddress(p *ecdsa.PrivateKey) ethcmn.Address { + return ethcrypto.PubkeyToAddress(ecdsa.PublicKey(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. +func ValidateSigner(signBytes, sig []byte, signer ethcmn.Address) error { + pk, err := ethcrypto.SigToPub(signBytes, sig) + + if err != nil { + return errors.Wrap(err, "signature verification failed") + } else if ethcrypto.PubkeyToAddress(*pk) != signer { + return fmt.Errorf("invalid signature for signer: %s", signer) + } + + return nil +} diff --git a/types/utils_test.go b/types/utils_test.go new file mode 100644 index 00000000..cb12c841 --- /dev/null +++ b/types/utils_test.go @@ -0,0 +1,36 @@ +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 + signEtx := EmbeddedTxSign{testChainID.String(), 0, 0, msgs, newStdFee()} + + // create signing bytes and sign + signBytes, err := signEtx.Bytes() + require.NoError(t, err) + + // 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) +} diff --git a/types/wire.go b/types/wire.go new file mode 100644 index 00000000..5414fcdc --- /dev/null +++ b/types/wire.go @@ -0,0 +1,22 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" +) + +var typesCodec = wire.NewCodec() + +func init() { + RegisterWire(typesCodec) +} + +// RegisterWire registers all the necessary types with amino for the given +// codec. +func RegisterWire(codec *wire.Codec) { + sdk.RegisterWire(codec) + codec.RegisterConcrete(&EthSignature{}, "types/EthSignature", nil) + codec.RegisterConcrete(TxData{}, "types/TxData", nil) + codec.RegisterConcrete(Transaction{}, "types/Transaction", nil) + codec.RegisterConcrete(EmbeddedTx{}, "types/EmbeddedTx", nil) +}