From 4f211af49a48726c5aa80dccc42a35953e524b5f Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Thu, 30 Aug 2018 14:32:32 -0400 Subject: [PATCH 1/4] Update ethermint appt to accept an SDK address --- app/ethermint.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/ethermint.go b/app/ethermint.go index ad9bbd56..be17045f 100644 --- a/app/ethermint.go +++ b/app/ethermint.go @@ -15,6 +15,7 @@ import ( "github.com/cosmos/ethermint/handlers" "github.com/cosmos/ethermint/types" + ethcmn "github.com/ethereum/go-ethereum/common" ethparams "github.com/ethereum/go-ethereum/params" abci "github.com/tendermint/tendermint/abci/types" @@ -58,12 +59,13 @@ type ( // NewEthermintApp returns a reference to a new initialized Ethermint // application. func NewEthermintApp( - logger tmlog.Logger, db dbm.DB, ethChainCfg *ethparams.ChainConfig, baseAppOptions ...func(*bam.BaseApp), + logger tmlog.Logger, db dbm.DB, ethChainCfg *ethparams.ChainConfig, + sdkAddr ethcmn.Address, baseAppOpts ...func(*bam.BaseApp), ) *EthermintApp { codec := CreateCodec() app := &EthermintApp{ - BaseApp: bam.NewBaseApp(appName, logger, db, types.TxDecoder(codec), baseAppOptions...), + BaseApp: bam.NewBaseApp(appName, logger, db, types.TxDecoder(codec, sdkAddr), baseAppOpts...), codec: codec, accountKey: sdk.NewKVStoreKey("acc"), mainKey: sdk.NewKVStoreKey("main"), @@ -177,7 +179,7 @@ func (app *EthermintApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) } // CreateCodec creates a new amino wire codec and registers all the necessary -// structures and interfaces needed for the application. +// concrete types and interfaces needed for the application. func CreateCodec() *wire.Codec { codec := wire.NewCodec() From 562124266829ba1d0eb325c1ec229ebf6a03893d Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Thu, 30 Aug 2018 17:36:14 -0400 Subject: [PATCH 2/4] Revert tx logic to use embedded txs with dedicated addr --- handlers/ante.go | 34 +++-- types/test_common.go | 121 +++++++++++++++ types/tx.go | 339 +++++++++++++++++++++++++------------------ types/tx_test.go | 158 +++++++++----------- types/utils.go | 2 +- types/utils_test.go | 16 +- types/wire.go | 3 - 7 files changed, 419 insertions(+), 254 deletions(-) create mode 100644 types/test_common.go diff --git a/handlers/ante.go b/handlers/ante.go index 473e1abb..f9ff96a2 100644 --- a/handlers/ante.go +++ b/handlers/ante.go @@ -9,8 +9,6 @@ import ( "github.com/cosmos/ethermint/types" ethcmn "github.com/ethereum/go-ethereum/common" - - ethtypes "github.com/ethereum/go-ethereum/core/types" ) const ( @@ -39,7 +37,7 @@ func AnteHandler(am auth.AccountMapper, _ auth.FeeCollectionKeeper) sdk.AnteHand switch tx := tx.(type) { case types.Transaction: - gasLimit = int64(tx.Data.GasLimit) + gasLimit = int64(tx.Data().GasLimit) handler = handleEthTx case auth.StdTx: gasLimit = tx.Fee.Gas @@ -91,16 +89,28 @@ func handleEthTx(sdkCtx sdk.Context, tx sdk.Tx, am auth.AccountMapper) (sdk.Cont return sdkCtx, sdk.ErrInternal(fmt.Sprintf("invalid chainID: %s", sdkCtx.ChainID())).Result(), true } - // validate signature - gethTx := ethTx.ConvertTx(chainID) - signer := ethtypes.NewEIP155Signer(chainID) + sdkCtx.GasMeter().ConsumeGas(verifySigCost, "ante: verify Ethereum signature") - _, err := signer.Sender(&gethTx) + addr, err := ethTx.VerifySig(chainID) if err != nil { return sdkCtx, sdk.ErrUnauthorized("signature verification failed").Result(), true } - return sdkCtx, sdk.Result{GasWanted: int64(ethTx.Data.GasLimit)}, false + acc := am.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 + } + + err = acc.SetSequence(seq + 1) + if err != nil { + return sdkCtx, sdk.ErrInternal(err.Error()).Result(), true + } + + am.SetAccount(sdkCtx, acc) + return sdkCtx, sdk.Result{GasWanted: int64(ethTx.Data().GasLimit)}, false } // handleEmbeddedTx implements an ante handler for an SDK transaction. It @@ -122,15 +132,15 @@ func handleEmbeddedTx(sdkCtx sdk.Context, tx sdk.Tx, am auth.AccountMapper) (sdk for i, sig := range stdTx.Signatures { signer := ethcmn.BytesToAddress(signerAddrs[i].Bytes()) - signerAcc, err := validateSignature(sdkCtx, stdTx, signer, sig, am) + acc, err := validateSignature(sdkCtx, stdTx, signer, sig, am) if err.Code() != sdk.CodeOK { - return sdkCtx, err.Result(), false + return sdkCtx, err.Result(), true } // TODO: Fees! - am.SetAccount(sdkCtx, signerAcc) - signerAccs[i] = signerAcc + am.SetAccount(sdkCtx, acc) + signerAccs[i] = acc } newCtx := auth.WithSigners(sdkCtx, signerAccs) diff --git a/types/test_common.go b/types/test_common.go new file mode 100644 index 00000000..6dc76eb6 --- /dev/null +++ b/types/test_common.go @@ -0,0 +1,121 @@ +// nolint +package types + +import ( + "crypto/ecdsa" + "math/big" + + 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" +) + +var ( + TestSDKAddr = GenerateEthAddress() + TestChainID = big.NewInt(3) + + TestPrivKey1, _ = ethcrypto.GenerateKey() + TestPrivKey2, _ = ethcrypto.GenerateKey() + + TestAddr1 = PrivKeyToEthAddress(TestPrivKey1) + TestAddr2 = PrivKeyToEthAddress(TestPrivKey2) +) + +func NewTestCodec() *wire.Codec { + codec := wire.NewCodec() + + RegisterWire(codec) + auth.RegisterWire(codec) + wire.RegisterCrypto(codec) + codec.RegisterConcrete(&sdk.TestMsg{}, "test/TestMsg", nil) + + return codec +} + +func NewTestStdFee() auth.StdFee { + return auth.NewStdFee(5000, sdk.NewCoin("photon", sdk.NewInt(150))) +} + +func NewTestStdTx( + chainID *big.Int, msgs []sdk.Msg, accNums []int64, 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 +} + +func NewTestSDKTxs( + codec *wire.Codec, chainID *big.Int, to ethcmn.Address, msgs []sdk.Msg, + accNums []int64, seqs []int64, pKeys []*ecdsa.PrivateKey, fee auth.StdFee, +) []*Transaction { + + txs := make([]*Transaction, len(pKeys)) + stdTx := NewTestStdTx(chainID, msgs, accNums, seqs, pKeys, fee) + payload := codec.MustMarshalBinary(stdTx) + + for i, privKey := range pKeys { + ethTx := NewTransaction(uint64(seqs[i]), to, big.NewInt(10), 1000, big.NewInt(100), payload) + + ethTx.Sign(chainID, privKey) + txs[i] = ethTx + } + + return txs +} diff --git a/types/tx.go b/types/tx.go index 3c3b7dd2..00a696c9 100644 --- a/types/tx.go +++ b/types/tx.go @@ -1,8 +1,10 @@ package types import ( + "bytes" "crypto/ecdsa" "fmt" + "io" "math/big" "sync/atomic" @@ -13,17 +15,20 @@ import ( 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" ) -var _ sdk.Tx = (*Transaction)(nil) - const ( // TypeTxEthereum reflects an Ethereum Transaction type. TypeTxEthereum = "Ethereum" ) +// ---------------------------------------------------------------------------- +// 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 @@ -33,7 +38,7 @@ type ( // Note: The transaction also implements the sdk.Msg interface to perform // basic validation that is done in the BaseApp. Transaction struct { - Data TxData + data TxData // caches hash atomic.Value @@ -41,96 +46,136 @@ type ( from atomic.Value } - // TxData defines internal Ethereum transaction information + // 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"` + Price *big.Int `json:"gasPrice"` GasLimit uint64 `json:"gas"` - Recipient *ethcmn.Address `json:"to"` // nil means contract creation - Amount sdk.Int `json:"value"` + Recipient *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation + Amount *big.Int `json:"value"` Payload []byte `json:"input"` - Signature *EthSignature `json:"signature"` + + // 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"` + Hash *ethcmn.Hash `json:"hash" rlp:"-"` } - // EthSignature reflects an Ethereum signature. We wrap this in a structure - // to support Amino serialization of transactions. - EthSignature struct { - v, r, s *big.Int + // sigCache is used to cache the derived sender and contains the signer used + // to derive it. + sigCache struct { + signer ethtypes.Signer + from ethcmn.Address } ) -// 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. +// NewTransaction 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 { + 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{ - Recipient: &to, AccountNonce: nonce, + Recipient: to, Payload: payload, GasLimit: gasLimit, - Amount: amount, - Price: gasPrice, - Signature: NewEthSignature(new(big.Int), new(big.Int), new(big.Int)), + Amount: new(big.Int), + Price: new(big.Int), + V: new(big.Int), + R: new(big.Int), + S: new(big.Int), } - return Transaction{Data: txData} + 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 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), - }) +func (tx *Transaction) Sign(chainID *big.Int, priv *ecdsa.PrivateKey) { + txHash := tx.SigHash(chainID) - sig, err := ethcrypto.Sign(h[:], priv) + sig, err := ethcrypto.Sign(txHash[:], priv) if err != nil { panic(err) } @@ -147,13 +192,48 @@ func (tx *Transaction) Sign(chainID sdk.Int, priv *ecdsa.PrivateKey) { 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)) + chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2)) v.Add(v, chainIDMul) } - tx.Data.Signature.v = v - tx.Data.Signature.r = r - tx.Data.Signature.s = s + 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 @@ -165,11 +245,11 @@ func (tx Transaction) Type() string { // 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 { + if tx.data.Price.Sign() != 1 { return ErrInvalidValue(DefaultCodespace, "price must be positive") } - if tx.Data.Amount.Sign() != 1 { + if tx.data.Amount.Sign() != 1 { return ErrInvalidValue(DefaultCodespace, "amount must be positive") } @@ -192,44 +272,57 @@ 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. -// -// TODO: To be removed in #470 -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()) } -// TxDecoder returns an sdk.TxDecoder that given raw transaction bytes, -// attempts to decode them into a valid sdk.Tx. -func TxDecoder(codec *wire.Codec) sdk.TxDecoder { +// 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) (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 *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") + return nil, sdk.ErrTxDecode("transaction bytes are empty") } - var tx sdk.Tx - - // The given codec should have all the appropriate message types - // registered. - err := codec.UnmarshalBinary(txBytes, &tx) + err := rlp.DecodeBytes(txBytes, &tx) if err != nil { - return nil, sdk.ErrTxDecode("failed to decode tx").TraceSDK(err.Error()) + 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 @@ -237,20 +330,20 @@ func TxDecoder(codec *wire.Codec) sdk.TxDecoder { } // recoverEthSig recovers a signature according to the Ethereum specification. -func recoverEthSig(es *EthSignature, chainID *big.Int) []byte { +func recoverEthSig(R, S, Vb, chainID *big.Int) []byte { var v byte - r, s := es.r.Bytes(), es.s.Bytes() + 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(es.v.Uint64() - 27) + v = byte(Vb.Uint64() - 27) } else { chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2)) - V := new(big.Int).Sub(es.v, chainIDMul) + V := new(big.Int).Sub(Vb, chainIDMul) v = byte(V.Uint64() - 35) } @@ -259,43 +352,11 @@ func recoverEthSig(es *EthSignature, chainID *big.Int) []byte { return sig } -func rlpHash(x interface{}) (h ethcmn.Hash) { +func rlpHash(x interface{}) (hash 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 - } + hasher.Sum(hash[:0]) return } diff --git a/types/tx_test.go b/types/tx_test.go index 7a75caa9..0df8a8a7 100644 --- a/types/tx_test.go +++ b/types/tx_test.go @@ -3,83 +3,57 @@ 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" - ethcrypto "github.com/ethereum/go-ethereum/crypto" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/require" ) -var ( - testChainID = sdk.NewInt(3) +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}) - testPrivKey1, _ = ethcrypto.GenerateKey() - testPrivKey2, _ = ethcrypto.GenerateKey() + txRLP, err := rlp.EncodeToBytes(txs[0]) + require.NoError(t, err) - testAddr1 = PrivKeyToEthAddress(testPrivKey1) - testAddr2 = PrivKeyToEthAddress(testPrivKey2) -) + gtxRLP, err := rlp.EncodeToBytes(gtxs[0]) + require.NoError(t, err) -func newTestCodec() *wire.Codec { - codec := wire.NewCodec() - - RegisterWire(codec) - codec.RegisterConcrete(auth.StdTx{}, "test/StdTx", nil) - codec.RegisterConcrete(&sdk.TestMsg{}, "test/TestMsg", nil) - wire.RegisterCrypto(codec) - - return codec + require.Equal(t, gtxRLP, txRLP) } -func newStdFee() auth.StdFee { - return auth.NewStdFee(5000, sdk.NewCoin("photon", sdk.NewInt(150))) -} +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}) -func newTestStdTx( - chainID sdk.Int, msgs []sdk.Msg, pKeys []*ecdsa.PrivateKey, - accNums []int64, seqs []int64, fee auth.StdFee, -) sdk.Tx { + txRLP, err := rlp.EncodeToBytes(txs[0]) + require.NoError(t, err) - sigs := make([]auth.StdSignature, len(pKeys)) + gtxRLP, err := rlp.EncodeToBytes(gtxs[0]) + require.NoError(t, err) - for i, priv := range pKeys { - signBytes := GetStdTxSignBytes(chainID.String(), accNums[i], seqs[i], newStdFee(), msgs, "") + var ( + decodedTx Transaction + decodedGtx ethtypes.Transaction + ) - sig, err := ethcrypto.Sign(signBytes, priv) - if err != nil { - panic(err) - } + err = rlp.DecodeBytes(txRLP, &decodedTx) + require.NoError(t, err) - sigs[i] = auth.StdSignature{Signature: sig, AccountNumber: accNums[i], Sequence: seqs[i]} - } + err = rlp.DecodeBytes(gtxRLP, &decodedGtx) + require.NoError(t, err) - return auth.NewStdTx(msgs, fee, sigs, "") -} - -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 + require.Equal(t, decodedGtx.Hash(), decodedTx.Hash()) } func TestValidation(t *testing.T) { - ethTxs := newTestEthTxs( - testChainID, - []*ecdsa.PrivateKey{testPrivKey1}, - []ethcmn.Address{testAddr1}, + ethTxs := NewTestEthTxs( + TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1}, ) testCases := []struct { @@ -89,13 +63,13 @@ func TestValidation(t *testing.T) { }{ {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) + 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 = sdk.NewInt(-1) + tx := msg.(*Transaction) + tx.data.Amount = big.NewInt(-1) return tx }, true}, } @@ -112,59 +86,57 @@ func TestValidation(t *testing.T) { } } -func TestTransactionGetMsgs(t *testing.T) { - ethTxs := newTestEthTxs( - testChainID, - []*ecdsa.PrivateKey{testPrivKey1}, - []ethcmn.Address{testAddr1}, +func TestTransactionVerifySig(t *testing.T) { + txs := NewTestEthTxs( + TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1}, ) - msgs := ethTxs[0].GetMsgs() - require.Len(t, msgs, 1) - require.Equal(t, ethTxs[0], msgs[0]) + addr, err := txs[0].VerifySig(TestChainID) + require.NoError(t, err) + require.Equal(t, TestAddr1, addr) - expectedMsgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(testAddr1.Bytes()))} - etx := newTestStdTx( - testChainID, expectedMsgs, []*ecdsa.PrivateKey{testPrivKey1}, - []int64{0}, []int64{0}, newStdFee(), - ) - - msgs = etx.GetMsgs() - require.Len(t, msgs, len(expectedMsgs)) - require.Equal(t, expectedMsgs, msgs) + 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) + testCodec := NewTestCodec() + txDecoder := TxDecoder(testCodec, TestSDKAddr) msgs := []sdk.Msg{sdk.NewTestMsg()} // create a non-SDK Ethereum transaction - emintTx := NewTransaction( - uint64(0), testAddr1, sdk.NewInt(10), 100, sdk.NewInt(100), nil, + txs := NewTestEthTxs( + TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1}, ) - emintTx.Sign(testChainID, testPrivKey1) + + txBytes, err := rlp.EncodeToBytes(txs[0]) + require.NoError(t, err) // require the transaction to properly decode into a Transaction - txBytes := testCodec.MustMarshalBinary(emintTx) - tx, err := txDecoder(txBytes) + decodedTx, err := txDecoder(txBytes) require.NoError(t, err) - require.Equal(t, emintTx, tx) + 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, []*ecdsa.PrivateKey{testPrivKey1}, - []int64{0}, []int64{0}, newStdFee(), + txs = NewTestSDKTxs( + testCodec, TestChainID, TestSDKAddr, msgs, []int64{0}, []int64{0}, + []*ecdsa.PrivateKey{TestPrivKey1}, NewTestStdFee(), ) - // require the transaction to properly decode into a Transaction - txBytes = testCodec.MustMarshalBinary(stdTx) - tx, err = txDecoder(txBytes) + txBytes, err = rlp.EncodeToBytes(txs[0]) require.NoError(t, err) - require.Equal(t, stdTx, tx) + + // require the transaction to properly decode into a Transaction + stdTx := NewTestStdTx(TestChainID, msgs, []int64{0}, []int64{0}, []*ecdsa.PrivateKey{TestPrivKey1}, NewTestStdFee()) + 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 - tx, err = txDecoder([]byte{}) + decodedTx, err = txDecoder([]byte{}) require.Error(t, err) - require.Nil(t, tx) + require.Nil(t, decodedTx) } diff --git a/types/utils.go b/types/utils.go index 9c4f5328..7135a583 100644 --- a/types/utils.go +++ b/types/utils.go @@ -35,7 +35,7 @@ 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") + return errors.Wrap(err, "failed to derive public key from signature") } else if ethcrypto.PubkeyToAddress(*pk) != signer { return fmt.Errorf("invalid signature for signer: %s", signer) } diff --git a/types/utils_test.go b/types/utils_test.go index 39c9e291..12e27d8f 100644 --- a/types/utils_test.go +++ b/types/utils_test.go @@ -10,23 +10,27 @@ import ( ) func TestValidateSigner(t *testing.T) { - msgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(testAddr1.Bytes()))} + msgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(TestAddr1.Bytes()))} // create message signing structure and bytes - signBytes := GetStdTxSignBytes(testChainID.String(), 0, 0, newStdFee(), msgs, "") + signBytes := GetStdTxSignBytes(TestChainID.String(), 0, 0, NewTestStdFee(), msgs, "") // require signing not to fail - sig, err := ethcrypto.Sign(signBytes, testPrivKey1) + sig, err := ethcrypto.Sign(signBytes, TestPrivKey1) require.NoError(t, err) // require signature to be valid - err = ValidateSigner(signBytes, sig, testAddr1) + err = ValidateSigner(signBytes, sig, TestAddr1) require.NoError(t, err) - sig, err = ethcrypto.Sign(signBytes, testPrivKey2) + sig, err = ethcrypto.Sign(signBytes, TestPrivKey2) require.NoError(t, err) // require signature to be invalid - err = ValidateSigner(signBytes, sig, testAddr1) + 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/types/wire.go b/types/wire.go index 23f69aeb..07e27c6f 100644 --- a/types/wire.go +++ b/types/wire.go @@ -16,7 +16,4 @@ func init() { func RegisterWire(codec *wire.Codec) { sdk.RegisterWire(codec) codec.RegisterConcrete(&Account{}, "types/Account", nil) - codec.RegisterConcrete(&EthSignature{}, "types/EthSignature", nil) - codec.RegisterConcrete(TxData{}, "types/TxData", nil) - codec.RegisterConcrete(Transaction{}, "types/Transaction", nil) } From 8ca5a1c04302eb255b0b469f6db9e781880a3701 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Thu, 30 Aug 2018 17:55:45 -0400 Subject: [PATCH 3/4] Update tx spec doc --- docs/spec/transactions/README.md | 35 +++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/spec/transactions/README.md b/docs/spec/transactions/README.md index 040d00bd..7fe16367 100644 --- a/docs/spec/transactions/README.md +++ b/docs/spec/transactions/README.md @@ -6,15 +6,30 @@ 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 to handle -Ethereum transactions and utilizing the SDK's `auth.StdTx` for Cosmos -transactions. Both of these structures are registered with an [Amino](https://github.com/tendermint/go-amino) codec, so the `TxDecoder` that in invoked -during the `BaseApp#runTx`, will be able to decode raw transaction bytes into the -appropriate transaction type which will then be passed onto handlers downstream. +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. + +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. __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. ## Transactions & Messages @@ -33,16 +48,16 @@ no distinction between transactions and messages. 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 `auth.StdTx` Cosmos routed transactions. Signatures over the -`Transaction` type are identical to Ethereum. However, the `auth.StdTx` contains +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 `auth.StdTx` 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 `auth.StdTx` is responsible for paying the fees. +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. ## Gas & Fees From 4add2e7df7070b00628d79c80d52350d7cc59555 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Fri, 31 Aug 2018 08:35:23 -0400 Subject: [PATCH 4/4] Remove ChainConfig from ethermint constructor --- app/ethermint.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/ethermint.go b/app/ethermint.go index be17045f..b911cd76 100644 --- a/app/ethermint.go +++ b/app/ethermint.go @@ -16,7 +16,6 @@ import ( "github.com/cosmos/ethermint/types" ethcmn "github.com/ethereum/go-ethereum/common" - ethparams "github.com/ethereum/go-ethereum/params" abci "github.com/tendermint/tendermint/abci/types" tmcmn "github.com/tendermint/tendermint/libs/common" @@ -59,8 +58,7 @@ type ( // NewEthermintApp returns a reference to a new initialized Ethermint // application. func NewEthermintApp( - logger tmlog.Logger, db dbm.DB, ethChainCfg *ethparams.ChainConfig, - sdkAddr ethcmn.Address, baseAppOpts ...func(*bam.BaseApp), + logger tmlog.Logger, db dbm.DB, sdkAddr ethcmn.Address, baseAppOpts ...func(*bam.BaseApp), ) *EthermintApp { codec := CreateCodec()