diff --git a/CHANGELOG.md b/CHANGELOG.md index 83e8d7c2..0954c00e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements -* (evm) [tharsis#24](https://github.com/tharsis/ethermint/pull/24)Implement metrics for `MsgEthereumTx`, state transtitions, `BeginBlock` and `EndBlock`. +* (evm) [tharsis#66](https://github.com/tharsis/ethermint/issues/66) Support legacy transaction types for signing. +* (evm) [tharsis#24](https://github.com/tharsis/ethermint/pull/24) Implement metrics for `MsgEthereumTx`, state transtitions, `BeginBlock` and `EndBlock`. * (deps) [\#602](https://github.com/cosmos/ethermint/pull/856) Bump tendermint version to [v0.39.3](https://github.com/tendermint/tendermint/releases/tag/v0.39.3) ### Bug Fixes diff --git a/app/ante/eth.go b/app/ante/eth.go index ad7abbef..6c686ee8 100644 --- a/app/ante/eth.go +++ b/app/ante/eth.go @@ -48,6 +48,10 @@ func (esvd EthSigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, s return next(ctx, tx, simulate) } + if len(tx.GetMsgs()) != 1 { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "only 1 ethereum msg supported per tx, got %d", len(tx.GetMsgs())) + } + // get and set account must be called with an infinite gas meter in order to prevent // additional gas from being deducted. infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) @@ -63,18 +67,21 @@ func (esvd EthSigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, s blockNum := big.NewInt(ctx.BlockHeight()) signer := ethtypes.MakeSigner(ethCfg, blockNum) - for _, msg := range tx.GetMsgs() { - msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{}) - } - - if _, err := signer.Sender(msgEthTx.AsTransaction()); err != nil { - return ctx, sdkerrors.Wrap(sdkerrors.ErrorInvalidSigner, err.Error()) - } + msg := tx.GetMsgs()[0] + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{}) } - return next(ctx, tx, simulate) + sender, err := signer.Sender(msgEthTx.AsTransaction()) + if err != nil { + return ctx, sdkerrors.Wrap(sdkerrors.ErrorInvalidSigner, err.Error()) + } + + // set up the sender to the transaction field if not already + msgEthTx.From = sender.Hex() + + return next(ctx, msgEthTx, simulate) } // EthAccountVerificationDecorator validates an account balance checks @@ -228,10 +235,6 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula // additional gas from being deducted. infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) - if len(tx.GetMsgs()) != 1 { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "only 1 ethereum msg supported per tx, got %d", len(tx.GetMsgs())) - } - // reset the refund gas value in the keeper for the current transaction egcd.evmKeeper.ResetRefundTransient(infCtx) @@ -331,6 +334,7 @@ func (ctd CanTransferDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate } ethCfg := config.EthereumConfig(ctd.evmKeeper.ChainID()) + signer := ethtypes.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight())) for _, msg := range tx.GetMsgs() { msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) @@ -338,7 +342,7 @@ func (ctd CanTransferDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", msg) } - coreMsg, err := msgEthTx.AsMessage() + coreMsg, err := msgEthTx.AsMessage(signer) if err != nil { return ctx, err } @@ -409,12 +413,9 @@ func (ald AccessListDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", msg) } - coreMsg, err := msgEthTx.AsMessage() - if err != nil { - return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "tx cannot be expressed as core.Message: %s", err.Error()) - } + sender := common.BytesToAddress(msgEthTx.GetFrom()) - ald.evmKeeper.PrepareAccessList(coreMsg.From(), coreMsg.To(), vm.ActivePrecompiles(rules), coreMsg.AccessList()) + ald.evmKeeper.PrepareAccessList(sender, msgEthTx.To(), vm.ActivePrecompiles(rules), *msgEthTx.Data.Accesses.ToEthAccessList()) } // set the original gas meter diff --git a/app/ante/eth_test.go b/app/ante/eth_test.go index 1b6678b5..e0e6446e 100644 --- a/app/ante/eth_test.go +++ b/app/ante/eth_test.go @@ -310,13 +310,16 @@ func (suite AnteTestSuite) TestEthGasConsumeDecorator() { func (suite AnteTestSuite) TestCanTransferDecorator() { dec := ante.NewCanTransferDecorator(suite.app.EvmKeeper) - addr, _ := newTestAddrKey() + addr, privKey := newTestAddrKey() - tx := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil) - tx2 := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil) + tx := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, ðtypes.AccessList{}) + tx2 := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, ðtypes.AccessList{}) tx.From = addr.Hex() + err := tx.Sign(suite.ethSigner, tests.NewSigner(privKey)) + suite.Require().NoError(err) + testCases := []struct { name string tx sdk.Tx @@ -376,7 +379,6 @@ func (suite AnteTestSuite) TestAccessListDecorator() { tx := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil) tx2 := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, al) - tx3 := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, nil) tx.From = addr.Hex() tx2.From = addr.Hex() @@ -388,7 +390,6 @@ func (suite AnteTestSuite) TestAccessListDecorator() { expPass bool }{ {"invalid transaction type", &invalidTx{}, func() {}, false}, - {"AsMessage failed", tx3, func() {}, false}, { "success - no access list", tx, diff --git a/x/evm/types/msg.go b/x/evm/types/msg.go index 4efbff23..4a8f3eaa 100644 --- a/x/evm/types/msg.go +++ b/x/evm/types/msg.go @@ -10,7 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth/ante" "github.com/cosmos/ethermint/types" - ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" ) @@ -29,7 +29,7 @@ const ( // NewMsgEthereumTx returns a reference to a new Ethereum transaction message. func NewMsgEthereumTx( - chainID *big.Int, nonce uint64, to *ethcmn.Address, amount *big.Int, + chainID *big.Int, nonce uint64, to *common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, input []byte, accesses *ethtypes.AccessList, ) *MsgEthereumTx { return newMsgEthereumTx(chainID, nonce, to, amount, gasLimit, gasPrice, input, accesses) @@ -45,71 +45,38 @@ func NewMsgEthereumTxContract( } func newMsgEthereumTx( - chainID *big.Int, nonce uint64, to *ethcmn.Address, amount *big.Int, + chainID *big.Int, nonce uint64, to *common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, input []byte, accesses *ethtypes.AccessList, ) *MsgEthereumTx { - if len(input) > 0 { - input = ethcmn.CopyBytes(input) - } - - var toHex string - if to != nil { - toHex = to.Hex() - } - - var chainIDBz []byte - if chainID != nil { - chainIDBz = chainID.Bytes() - } - - txData := &TxData{ - ChainID: chainIDBz, - Nonce: nonce, - To: toHex, - Input: input, - GasLimit: gasLimit, - Amount: []byte{}, - GasPrice: []byte{}, - Accesses: NewAccessList(accesses), - V: []byte{}, - R: []byte{}, - S: []byte{}, - } - - if amount != nil { - txData.Amount = amount.Bytes() - } - if gasPrice != nil { - txData.GasPrice = gasPrice.Bytes() - } - + txData := newTxData(chainID, nonce, to, amount, gasLimit, gasPrice, input, accesses) return &MsgEthereumTx{Data: txData} } // fromEthereumTx populates the message fields from the given ethereum transaction func (msg *MsgEthereumTx) FromEthereumTx(tx *ethtypes.Transaction) { - to := "" - if tx.To() != nil { - to = tx.To().Hex() - } - - al := tx.AccessList() - v, r, s := tx.RawSignatureValues() - msg.Data = &TxData{ - ChainID: tx.ChainId().Bytes(), Nonce: tx.Nonce(), Input: tx.Data(), GasLimit: tx.Gas(), - To: to, - Amount: tx.Value().Bytes(), - GasPrice: tx.GasPrice().Bytes(), - Accesses: NewAccessList(&al), - V: v.Bytes(), - R: r.Bytes(), - S: s.Bytes(), } + v, r, s := tx.RawSignatureValues() + if tx.To() != nil { + msg.Data.To = tx.To().Hex() + } + if tx.Value() != nil { + msg.Data.Amount = tx.Value().Bytes() + } + if tx.GasPrice() != nil { + msg.Data.GasPrice = tx.GasPrice().Bytes() + } + if tx.AccessList() != nil { + al := tx.AccessList() + msg.Data.Accesses = NewAccessList(&al) + } + + msg.Data.setSignatureValues(tx.ChainId(), v, r, s) + msg.Size_ = float64(tx.Size()) msg.Hash = tx.Hash().Hex() } @@ -123,41 +90,19 @@ func (msg MsgEthereumTx) Type() string { return TypeMsgEthereumTx } // ValidateBasic implements the sdk.Msg interface. It performs basic validation // checks of a Transaction. If returns an error if validation fails. func (msg MsgEthereumTx) ValidateBasic() error { - gasPrice := new(big.Int).SetBytes(msg.Data.GasPrice) - if gasPrice.Sign() == -1 { - return sdkerrors.Wrapf(ErrInvalidGasPrice, "gas price cannot be negative %s", gasPrice) - } - - // Amount can be 0 - amount := new(big.Int).SetBytes(msg.Data.Amount) - if amount.Sign() == -1 { - return sdkerrors.Wrapf(ErrInvalidAmount, "amount cannot be negative %s", amount) - } - - if msg.Data.To != "" { - if err := types.ValidateAddress(msg.Data.To); err != nil { - return sdkerrors.Wrap(err, "invalid to address") - } - } - if msg.From != "" { if err := types.ValidateAddress(msg.From); err != nil { return sdkerrors.Wrap(err, "invalid from address") } } - return nil + return msg.Data.Validate() } // To returns the recipient address of the transaction. It returns nil if the // transaction is a contract creation. -func (msg MsgEthereumTx) To() *ethcmn.Address { - if msg.Data.To == "" { - return nil - } - - recipient := ethcmn.HexToAddress(msg.Data.To) - return &recipient +func (msg MsgEthereumTx) To() *common.Address { + return msg.Data.to() } // GetMsgs returns a single MsgEthereumTx as an sdk.Msg. @@ -173,10 +118,10 @@ func (msg MsgEthereumTx) GetSigners() []sdk.AccAddress { v, r, s := msg.RawSignatureValues() if msg.From == "" || v == nil || r == nil || s == nil { - panic("must use 'Sign' with a chain ID to get the signer") + panic("must use 'Sign' to get the signer address") } - signer := sdk.AccAddress(ethcmn.HexToAddress(msg.From).Bytes()) + signer := sdk.AccAddress(common.HexToAddress(msg.From).Bytes()) return []sdk.AccAddress{signer} } @@ -221,34 +166,32 @@ func (msg *MsgEthereumTx) Sign(ethSigner ethtypes.Signer, keyringSigner keyring. // GetGas implements the GasTx interface. It returns the GasLimit of the transaction. func (msg MsgEthereumTx) GetGas() uint64 { - return msg.Data.GasLimit + return msg.Data.gas() } // Fee returns gasprice * gaslimit. func (msg MsgEthereumTx) Fee() *big.Int { - gasPrice := new(big.Int).SetBytes(msg.Data.GasPrice) - gasLimit := new(big.Int).SetUint64(msg.Data.GasLimit) + gasPrice := msg.Data.gasPrice() + gasLimit := new(big.Int).SetUint64(msg.Data.gas()) return new(big.Int).Mul(gasPrice, gasLimit) } // ChainID returns which chain id this transaction was signed for (if at all) func (msg *MsgEthereumTx) ChainID() *big.Int { - return new(big.Int).SetBytes(msg.Data.ChainID) + return msg.Data.chainID() } // Cost returns amount + gasprice * gaslimit. func (msg MsgEthereumTx) Cost() *big.Int { total := msg.Fee() - total.Add(total, new(big.Int).SetBytes(msg.Data.Amount)) + total.Add(total, msg.Data.amount()) return total } // RawSignatureValues returns the V, R, S signature values of the transaction. // The return values should not be modified by the caller. func (msg MsgEthereumTx) RawSignatureValues() (v, r, s *big.Int) { - return new(big.Int).SetBytes(msg.Data.V), - new(big.Int).SetBytes(msg.Data.R), - new(big.Int).SetBytes(msg.Data.S) + return msg.Data.rawSignatureValues() } // GetFrom loads the ethereum sender address from the sigcache and returns an @@ -258,7 +201,7 @@ func (msg *MsgEthereumTx) GetFrom() sdk.AccAddress { return nil } - return ethcmn.HexToAddress(msg.From).Bytes() + return common.HexToAddress(msg.From).Bytes() } // AsTransaction creates an Ethereum Transaction type from the msg fields @@ -266,64 +209,7 @@ func (msg MsgEthereumTx) AsTransaction() *ethtypes.Transaction { return ethtypes.NewTx(msg.Data.AsEthereumData()) } -// AsMessage creates an Ethereum core.Message from the msg fields. This method -// fails if the sender address is not defined -func (msg MsgEthereumTx) AsMessage() (core.Message, error) { - if msg.From == "" { - return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "'from' address cannot be empty") - } - - from := ethcmn.HexToAddress(msg.From) - - var to *ethcmn.Address - if msg.Data.To != "" { - toAddr := ethcmn.HexToAddress(msg.Data.To) - to = &toAddr - } - - var accessList ethtypes.AccessList - if msg.Data.Accesses != nil { - accessList = *msg.Data.Accesses.ToEthAccessList() - } - - return ethtypes.NewMessage( - from, - to, - msg.Data.Nonce, - new(big.Int).SetBytes(msg.Data.Amount), - msg.Data.GasLimit, - new(big.Int).SetBytes(msg.Data.GasPrice), - msg.Data.Input, - accessList, - true, - ), nil -} - -// AsEthereumData returns an AccessListTx transaction data from the proto-formatted -// TxData defined on the Cosmos EVM. -func (data *TxData) AsEthereumData() ethtypes.TxData { - var to *ethcmn.Address - if data.To != "" { - toAddr := ethcmn.HexToAddress(data.To) - to = &toAddr - } - - var accessList ethtypes.AccessList - if data.Accesses != nil { - accessList = *data.Accesses.ToEthAccessList() - } - - return ðtypes.AccessListTx{ - ChainID: new(big.Int).SetBytes(data.ChainID), - Nonce: data.Nonce, - GasPrice: new(big.Int).SetBytes(data.GasPrice), - Gas: data.GasLimit, - To: to, - Value: new(big.Int).SetBytes(data.Amount), - Data: data.Input, - AccessList: accessList, - V: new(big.Int).SetBytes(data.V), - R: new(big.Int).SetBytes(data.R), - S: new(big.Int).SetBytes(data.S), - } +// AsMessage creates an Ethereum core.Message from the msg fields +func (msg MsgEthereumTx) AsMessage(signer ethtypes.Signer) (core.Message, error) { + return msg.AsTransaction().AsMessage(signer) } diff --git a/x/evm/types/msg_test.go b/x/evm/types/msg_test.go index 3fc9a023..12cad7c5 100644 --- a/x/evm/types/msg_test.go +++ b/x/evm/types/msg_test.go @@ -12,6 +12,7 @@ import ( "github.com/cosmos/ethermint/tests" ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" ) @@ -65,91 +66,101 @@ func (suite *MsgsTestSuite) TestMsgEthereumTx_ValidateBasic() { to *ethcmn.Address amount *big.Int gasPrice *big.Int + from string + accessList *ethtypes.AccessList + chainID *big.Int expectPass bool }{ - {msg: "pass with recipient", to: &suite.to, amount: big.NewInt(100), gasPrice: big.NewInt(100000), expectPass: true}, - {msg: "pass contract", to: nil, amount: big.NewInt(100), gasPrice: big.NewInt(100000), expectPass: true}, + {msg: "pass with recipient - Legacy Tx", to: &suite.to, amount: big.NewInt(100), gasPrice: big.NewInt(100000), expectPass: true}, + {msg: "pass with recipient - AccessList Tx", to: &suite.to, amount: big.NewInt(100), gasPrice: big.NewInt(0), accessList: ðtypes.AccessList{}, chainID: big.NewInt(1), expectPass: true}, + {msg: "pass contract - Legacy Tx", to: nil, amount: big.NewInt(100), gasPrice: big.NewInt(100000), expectPass: true}, {msg: "invalid recipient", to: ðcmn.Address{}, amount: big.NewInt(-1), gasPrice: big.NewInt(1000), expectPass: false}, - // NOTE: these can't be effectively tested because the SetBytes function from big.Int only sets - // the absolute value + {msg: "nil amount", to: &suite.to, amount: nil, gasPrice: big.NewInt(1000), expectPass: false}, {msg: "negative amount", to: &suite.to, amount: big.NewInt(-1), gasPrice: big.NewInt(1000), expectPass: true}, + {msg: "nil gas price", to: &suite.to, amount: big.NewInt(100), gasPrice: nil, expectPass: false}, {msg: "negative gas price", to: &suite.to, amount: big.NewInt(100), gasPrice: big.NewInt(-1), expectPass: true}, {msg: "zero gas price", to: &suite.to, amount: big.NewInt(100), gasPrice: big.NewInt(0), expectPass: true}, + {msg: "invalid from address", to: &suite.to, amount: big.NewInt(100), gasPrice: big.NewInt(0), from: ethcmn.Address{}.Hex(), expectPass: false}, + {msg: "chain ID not set on AccessListTx", to: &suite.to, amount: big.NewInt(100), gasPrice: big.NewInt(0), accessList: ðtypes.AccessList{}, chainID: nil, expectPass: false}, } for i, tc := range testCases { - msg := NewMsgEthereumTx(suite.chainID, 0, tc.to, tc.amount, 0, tc.gasPrice, nil, nil) + msg := NewMsgEthereumTx(tc.chainID, 0, tc.to, tc.amount, 0, tc.gasPrice, nil, tc.accessList) + msg.From = tc.from err := msg.ValidateBasic() if tc.expectPass { - suite.Require().NoError(err, "valid test %d failed: %s", i, tc.msg) + suite.Require().NoError(err, "valid test %d failed: %s, %v", i, tc.msg, msg) } else { - suite.Require().Error(err, "invalid test %d passed: %s", i, tc.msg) + suite.Require().Error(err, "invalid test %d passed: %s, %v", i, tc.msg, msg.Data) } } } func (suite *MsgsTestSuite) TestMsgEthereumTx_Sign() { - msg := NewMsgEthereumTx(suite.chainID, 0, &suite.to, nil, 100000, nil, []byte("test"), nil) - testCases := []struct { msg string + tx *MsgEthereumTx ethSigner ethtypes.Signer - malleate func() + malleate func(tx *MsgEthereumTx) expectPass bool }{ { "pass - EIP2930 signer", + NewMsgEthereumTx(suite.chainID, 0, &suite.to, nil, 100000, nil, []byte("test"), &types.AccessList{}), ethtypes.NewEIP2930Signer(suite.chainID), - func() { msg.From = suite.from.Hex() }, + func(tx *MsgEthereumTx) { tx.From = suite.from.Hex() }, true, }, - // TODO: support legacy txs { - "not supported - EIP155 signer", + "pass - EIP155 signer", + NewMsgEthereumTx(suite.chainID, 0, &suite.to, nil, 100000, nil, []byte("test"), nil), ethtypes.NewEIP155Signer(suite.chainID), - func() { msg.From = suite.from.Hex() }, - false, + func(tx *MsgEthereumTx) { tx.From = suite.from.Hex() }, + true, }, { - "not supported - Homestead signer", + "pass - Homestead signer", + NewMsgEthereumTx(suite.chainID, 0, &suite.to, nil, 100000, nil, []byte("test"), nil), ethtypes.HomesteadSigner{}, - func() { msg.From = suite.from.Hex() }, - false, + func(tx *MsgEthereumTx) { tx.From = suite.from.Hex() }, + true, }, { - "not supported - Frontier signer", + "pass - Frontier signer", + NewMsgEthereumTx(suite.chainID, 0, &suite.to, nil, 100000, nil, []byte("test"), nil), ethtypes.FrontierSigner{}, - func() { msg.From = suite.from.Hex() }, - false, + func(tx *MsgEthereumTx) { tx.From = suite.from.Hex() }, + true, }, { "no from address ", + NewMsgEthereumTx(suite.chainID, 0, &suite.to, nil, 100000, nil, []byte("test"), &types.AccessList{}), ethtypes.NewEIP2930Signer(suite.chainID), - func() { msg.From = "" }, + func(tx *MsgEthereumTx) { tx.From = "" }, false, }, { "from address ≠ signer address", + NewMsgEthereumTx(suite.chainID, 0, &suite.to, nil, 100000, nil, []byte("test"), &types.AccessList{}), ethtypes.NewEIP2930Signer(suite.chainID), - func() { msg.From = suite.to.Hex() }, + func(tx *MsgEthereumTx) { tx.From = suite.to.Hex() }, false, }, } for i, tc := range testCases { - tc.malleate() + tc.malleate(tc.tx) - err := msg.Sign(tc.ethSigner, suite.signer) + err := tc.tx.Sign(tc.ethSigner, suite.signer) if tc.expectPass { suite.Require().NoError(err, "valid test %d failed: %s", i, tc.msg) - tx := msg.AsTransaction() - signer := ethtypes.NewEIP2930Signer(suite.chainID) + tx := tc.tx.AsTransaction() - sender, err := ethtypes.Sender(signer, tx) + sender, err := ethtypes.Sender(tc.ethSigner, tx) suite.Require().NoError(err, tc.msg) - suite.Require().Equal(msg.From, sender.Hex(), tc.msg) + suite.Require().Equal(tc.tx.From, sender.Hex(), tc.msg) } else { suite.Require().Error(err, "invalid test %d passed: %s", i, tc.msg) } diff --git a/x/evm/types/tx_data.go b/x/evm/types/tx_data.go new file mode 100644 index 00000000..8d4712a9 --- /dev/null +++ b/x/evm/types/tx_data.go @@ -0,0 +1,223 @@ +package types + +import ( + "math/big" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/ethermint/types" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +// var _ ethtypes.TxData = &TxData{} + +func newTxData( + chainID *big.Int, nonce uint64, to *common.Address, amount *big.Int, + gasLimit uint64, gasPrice *big.Int, input []byte, accesses *ethtypes.AccessList, +) *TxData { + txData := &TxData{ + Nonce: nonce, + GasLimit: gasLimit, + } + + if len(input) > 0 { + txData.Input = common.CopyBytes(input) + } + + if to != nil { + txData.To = to.Hex() + } + + if accesses != nil { + txData.Accesses = NewAccessList(accesses) + + // NOTE: we don't populate chain id on LegacyTx type + if chainID != nil { + txData.ChainID = chainID.Bytes() + } + } + + if amount != nil { + txData.Amount = amount.Bytes() + } + if gasPrice != nil { + txData.GasPrice = gasPrice.Bytes() + } + return txData +} + +func (data *TxData) txType() byte { + if data.Accesses == nil { + return ethtypes.LegacyTxType + } + return ethtypes.AccessListTxType +} + +func (data *TxData) chainID() *big.Int { + if data.txType() == ethtypes.LegacyTxType { + v, _, _ := data.rawSignatureValues() + return DeriveChainID(v) + } + + if data.ChainID == nil { + return nil + } + + return new(big.Int).SetBytes(data.ChainID) +} + +func (data *TxData) accessList() ethtypes.AccessList { + if data.Accesses == nil { + return nil + } + return *data.Accesses.ToEthAccessList() +} + +func (data *TxData) data() []byte { + return common.CopyBytes(data.Input) +} + +func (data *TxData) gas() uint64 { + return data.GasLimit +} + +func (data *TxData) gasPrice() *big.Int { + if data.GasPrice == nil { + return nil + } + return new(big.Int).SetBytes(data.GasPrice) +} + +func (data *TxData) amount() *big.Int { + if data.Amount == nil { + return nil + } + return new(big.Int).SetBytes(data.Amount) +} + +func (data *TxData) nonce() uint64 { return data.Nonce } + +func (data *TxData) to() *common.Address { + if data.To == "" { + return nil + } + to := common.HexToAddress(data.To) + return &to +} + +// AsEthereumData returns an AccessListTx transaction data from the proto-formatted +// TxData defined on the Cosmos EVM. +func (data *TxData) AsEthereumData() ethtypes.TxData { + v, r, s := data.rawSignatureValues() + if data.Accesses == nil { + return ðtypes.LegacyTx{ + Nonce: data.nonce(), + GasPrice: data.gasPrice(), + Gas: data.gas(), + To: data.to(), + Value: data.amount(), + Data: data.data(), + V: v, + R: r, + S: s, + } + } + + return ðtypes.AccessListTx{ + ChainID: data.chainID(), + Nonce: data.nonce(), + GasPrice: data.gasPrice(), + Gas: data.gas(), + To: data.to(), + Value: data.amount(), + Data: data.data(), + AccessList: data.accessList(), + V: v, + R: r, + S: s, + } +} + +// rawSignatureValues returns the V, R, S signature values of the transaction. +// The return values should not be modified by the caller. +func (data *TxData) rawSignatureValues() (v, r, s *big.Int) { + if len(data.V) > 0 { + v = new(big.Int).SetBytes(data.V) + } + if len(data.R) > 0 { + r = new(big.Int).SetBytes(data.R) + } + if len(data.S) > 0 { + s = new(big.Int).SetBytes(data.S) + } + return v, r, s +} + +func (data *TxData) setSignatureValues(chainID, v, r, s *big.Int) { + if v != nil { + data.V = v.Bytes() + } + if r != nil { + data.R = r.Bytes() + } + if s != nil { + data.S = s.Bytes() + } + if data.txType() == ethtypes.AccessListTxType && chainID != nil { + data.ChainID = chainID.Bytes() + } +} + +// Validate performs a basic validation of the tx data fields. +func (data TxData) Validate() error { + gasPrice := data.gasPrice() + if gasPrice == nil { + return sdkerrors.Wrap(ErrInvalidGasPrice, "cannot be nil") + } + + if gasPrice.Sign() == -1 { + return sdkerrors.Wrapf(ErrInvalidGasPrice, "gas price cannot be negative %s", gasPrice) + } + + amount := data.amount() + if amount == nil { + return sdkerrors.Wrap(ErrInvalidAmount, "cannot be nil") + } + + // Amount can be 0 + if amount.Sign() == -1 { + return sdkerrors.Wrapf(ErrInvalidAmount, "amount cannot be negative %s", amount) + } + + if data.To != "" { + if err := types.ValidateAddress(data.To); err != nil { + return sdkerrors.Wrap(err, "invalid to address") + } + } + + if data.txType() == ethtypes.AccessListTxType && data.chainID() == nil { + return sdkerrors.Wrap( + sdkerrors.ErrInvalidChainID, + "chain ID must be present on AccessList txs", + ) + } + + return nil +} + +// DeriveChainID derives the chain id from the given v parameter +func DeriveChainID(v *big.Int) *big.Int { + if v == nil { + return nil + } + + if v.BitLen() <= 64 { + v := v.Uint64() + if v == 27 || v == 28 { + return new(big.Int) + } + return new(big.Int).SetUint64((v - 35) / 2) + } + v = new(big.Int).Sub(v, big.NewInt(35)) + return v.Div(v, big.NewInt(2)) +} diff --git a/x/evm/types/tx_data_test.go b/x/evm/types/tx_data_test.go new file mode 100644 index 00000000..843c20a2 --- /dev/null +++ b/x/evm/types/tx_data_test.go @@ -0,0 +1,65 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/cosmos/ethermint/tests" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestTxData_chainID(t *testing.T) { + testCases := []struct { + msg string + data TxData + expChainID *big.Int + }{ + { + "access list tx", TxData{Accesses: AccessList{}, ChainID: big.NewInt(1).Bytes()}, big.NewInt(1), + }, + { + "access list tx, nil chain ID", TxData{Accesses: AccessList{}}, nil, + }, + { + "legacy tx, derived", TxData{}, nil, + }, + } + + for _, tc := range testCases { + chainID := tc.data.chainID() + require.Equal(t, chainID, tc.expChainID, tc.msg) + } +} + +func TestTxData_DeriveChainID(t *testing.T) { + testCases := []struct { + msg string + data TxData + expChainID *big.Int + from common.Address + }{ + { + "v = 0", TxData{V: big.NewInt(0).Bytes()}, nil, tests.GenerateAddress(), + }, + { + "v = 1", TxData{V: big.NewInt(1).Bytes()}, big.NewInt(9223372036854775791), tests.GenerateAddress(), + }, + { + "v = 27", TxData{V: big.NewInt(27).Bytes()}, new(big.Int), tests.GenerateAddress(), + }, + { + "v = 28", TxData{V: big.NewInt(28).Bytes()}, new(big.Int), tests.GenerateAddress(), + }, + { + "v = nil ", TxData{V: nil}, nil, tests.GenerateAddress(), + }, + } + + for _, tc := range testCases { + v, _, _ := tc.data.rawSignatureValues() + + chainID := DeriveChainID(v) + require.Equal(t, tc.expChainID, chainID, tc.msg) + } +}