evm: support legacy tx (#109)

* evm: support legacy tx

* lint and minor fix

* tx data constructor

* tx data tests

* ante handler tests

* fill msg sender

* c++
This commit is contained in:
Federico Kunze 2021-06-11 09:38:51 -04:00 committed by GitHub
parent 0c423e8fa1
commit e8f6f7838e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 393 additions and 205 deletions

View File

@ -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

View File

@ -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

View File

@ -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, &ethtypes.AccessList{})
tx2 := evmtypes.NewMsgEthereumTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), 1000, big.NewInt(1), nil, &ethtypes.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,

View File

@ -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 &ethtypes.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)
}

View File

@ -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: &ethtypes.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: &ethcmn.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: &ethtypes.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)
}

223
x/evm/types/tx_data.go Normal file
View File

@ -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 &ethtypes.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 &ethtypes.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))
}

View File

@ -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)
}
}