From 9a71344ca94fde1deda9db37ee74294653f95db3 Mon Sep 17 00:00:00 2001 From: saito Date: Fri, 7 Jun 2024 09:03:16 +0800 Subject: [PATCH] [action]: add unit test for eth tx decode verify (#4291) * test(action): add unit test for eth tx decode verify * chore: remove unused code --- action/builder_test.go | 65 +++++++- action/rlp_tx_test.go | 365 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 426 insertions(+), 4 deletions(-) diff --git a/action/builder_test.go b/action/builder_test.go index f729bfb903..ea72762ded 100644 --- a/action/builder_test.go +++ b/action/builder_test.go @@ -6,16 +6,20 @@ package action import ( + "crypto/ecdsa" "encoding/hex" "math/big" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + ethercrypto "github.com/ethereum/go-ethereum/crypto" + iotexcrypto "github.com/iotexproject/go-pkgs/crypto" "github.com/iotexproject/iotex-address/address" + "github.com/iotexproject/iotex-proto/golang/iotextypes" "github.com/stretchr/testify/require" - "github.com/iotexproject/iotex-core/pkg/util/assertions" + . "github.com/iotexproject/iotex-core/pkg/util/assertions" "github.com/iotexproject/iotex-core/pkg/version" ) @@ -82,7 +86,7 @@ func TestBuildRewardingAction(t *testing.T) { t.Run("Success", func(t *testing.T) { method := _claimRewardingMethod t.Run("ClaimRewarding", func(t *testing.T) { - inputs := assertions.MustNoErrorV(method.Inputs.Pack(big.NewInt(101), []byte("any"), "")) + inputs := MustNoErrorV(method.Inputs.Pack(big.NewInt(101), []byte("any"), "")) elp, err := eb.BuildRewardingAction(types.NewTx(&types.LegacyTx{ Nonce: 1, GasPrice: big.NewInt(10004), @@ -102,7 +106,7 @@ func TestBuildRewardingAction(t *testing.T) { eb := &EnvelopeBuilder{} eb.SetChainID(4689) - inputs := assertions.MustNoErrorV(method.Inputs.Pack(big.NewInt(100), []byte("any"), "")) + inputs := MustNoErrorV(method.Inputs.Pack(big.NewInt(100), []byte("any"), "")) to := common.HexToAddress("0xA576C141e5659137ddDa4223d209d4744b2106BE") tx := types.NewTx(&types.LegacyTx{ Nonce: 0, @@ -119,7 +123,7 @@ func TestBuildRewardingAction(t *testing.T) { }) method = _depositRewardMethod t.Run("DepositRewarding", func(t *testing.T) { - inputs := assertions.MustNoErrorV(method.Inputs.Pack(big.NewInt(102), []byte("any"))) + inputs := MustNoErrorV(method.Inputs.Pack(big.NewInt(102), []byte("any"))) elp, err := eb.BuildRewardingAction(types.NewTx(&types.LegacyTx{ Nonce: 1, GasPrice: big.NewInt(10004), @@ -136,3 +140,56 @@ func TestBuildRewardingAction(t *testing.T) { }) }) } + +func ptr[V any](v V) *V { return &v } + +func TestEthTxUtils(t *testing.T) { + r := require.New(t) + var ( + skhex = "708361c6460c93f027df0823eb440f3270ee2937944f2de933456a3900d6dd1a" + sk1, _ = iotexcrypto.HexStringToPrivateKey(skhex) + sk2, _ = ethercrypto.HexToECDSA(skhex) + chainID = uint32(4689) + builder = (&Builder{}). + SetNonce(100). + SetGasLimit(21000). + SetGasPrice(big.NewInt(101)) + ) + + pk1 := sk1.PublicKey() + iotexhexaddr := hex.EncodeToString(pk1.Bytes()) + + pk2 := sk2.Public().(*ecdsa.PublicKey) + etherhexaddr := hex.EncodeToString(ethercrypto.FromECDSAPub(pk2)) + + r.Equal(iotexhexaddr, etherhexaddr) + + tx, _ := ptr((&ClaimFromRewardingFundBuilder{Builder: *builder}). + SetAddress("0xA576C141e5659137ddDa4223d209d4744b2106BE"). + SetData([]byte("any")). + SetAmount(big.NewInt(1)).Build()).ToEthTx(chainID) + + var ( + signer1, _ = NewEthSigner(iotextypes.Encoding_ETHEREUM_EIP155, chainID) + sig1, _ = sk1.Sign(tx.Hash().Bytes()) + signer2 = types.NewEIP2930Signer(big.NewInt(int64(chainID))) + sig2, _ = ethercrypto.Sign(tx.Hash().Bytes(), sk2) + ) + r.Equal(signer1, signer2) + r.Equal(sig1, sig2) + + tx1, _ := RawTxToSignedTx(tx, signer1, sig1) + tx2, _ := tx.WithSignature(signer2, sig2) + r.Equal(tx1.Hash(), tx2.Hash()) + + sender1, _ := signer1.Sender(tx1) + sender2, _ := signer2.Sender(tx2) + r.Equal(sender1, sender2) + + pk1recover, _ := iotexcrypto.RecoverPubkey(signer1.Hash(tx).Bytes(), sig1) + pk2recoverbytes, _ := ethercrypto.Ecrecover(signer2.Hash(tx).Bytes(), sig2) + r.Equal(pk1recover.Bytes(), pk2recoverbytes) + pk2recover, _ := ethercrypto.SigToPub(signer2.Hash(tx).Bytes(), sig2) + + r.Equal(ethercrypto.FromECDSAPub(pk2recover), pk2recoverbytes) +} diff --git a/action/rlp_tx_test.go b/action/rlp_tx_test.go index 8b7c392bf8..9a50b26b8a 100644 --- a/action/rlp_tx_test.go +++ b/action/rlp_tx_test.go @@ -4,15 +4,20 @@ import ( "bytes" "encoding/hex" "math/big" + "strings" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + iotexcrypto "github.com/iotexproject/go-pkgs/crypto" "github.com/iotexproject/go-pkgs/hash" "github.com/iotexproject/iotex-address/address" "github.com/iotexproject/iotex-proto/golang/iotextypes" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" + + . "github.com/iotexproject/iotex-core/pkg/util/assertions" + "github.com/iotexproject/iotex-core/pkg/version" ) func TestGenerateRlp(t *testing.T) { @@ -477,6 +482,366 @@ func TestEthTxDecodeVerify(t *testing.T) { } } +func TestEthTxDecodeVerifyV2(t *testing.T) { + var ( + r = require.New(t) + sk, _ = iotexcrypto.HexStringToPrivateKey("708361c6460c93f027df0823eb440f3270ee2937944f2de933456a3900d6dd1a") + nonce = uint64(100) + amount = big.NewInt(1) + to = "io1c04r4jc8q6u57phxpla639u5h5v2t3s062hyxa" + addrto = common.BytesToAddress(MustNoErrorV(address.FromString(to)).Bytes()) + data = []byte("any") + gasLimit = uint64(21000) + gasPrice = big.NewInt(101) + chainID = uint32(4689) + ) + r.NotNil(sk) + + elpbuilder := (&EnvelopeBuilder{}). + SetNonce(nonce). + SetGasPrice(gasPrice). + SetGasLimit(gasLimit). + SetChainID(chainID) + + tests := []struct { + name string // case name + encoding iotextypes.Encoding // assign and check signer's type + txto string // assertion of tx.To() + txamount *big.Int // assertion of tx.Value() + txdata []byte // assertion of tx.Data() + action EthCompatibleAction // eth compatible action + builder func(*types.Transaction) (Envelope, error) // envelope builder + }{ + { + name: "Transfer", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: to, + txamount: amount, + txdata: data, + action: MustNoErrorV(NewTransfer(nonce, amount, to, data, gasLimit, gasPrice)), + builder: elpbuilder.BuildTransfer, + }, + { + name: "Execution", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: to, + txamount: amount, + txdata: data, + action: MustNoErrorV(NewExecution(to, nonce, amount, gasLimit, gasPrice, data)), + builder: elpbuilder.BuildExecution, + }, + { + name: "ExecutionEmptyTo", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: "", + txamount: amount, + txdata: data, + action: MustNoErrorV(NewExecution("", nonce, amount, gasLimit, gasPrice, data)), + builder: elpbuilder.BuildExecution, + }, + { + name: "CreateStake", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _createStakeMethod.ID, + MustNoErrorV(_createStakeMethod.Inputs.Pack("name", amount, uint32(86400), true, data))..., + ), + action: MustNoErrorV(NewCreateStake(nonce, "name", amount.String(), 86400, true, data, gasLimit, gasPrice)), + builder: elpbuilder.BuildStakingAction, + }, + { + name: "DepositToStake", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _depositToStakeMethod.ID, + MustNoErrorV(_depositToStakeMethod.Inputs.Pack(uint64(10), amount, data))..., + ), + action: MustNoErrorV(NewDepositToStake(nonce, 10, amount.String(), data, gasLimit, gasPrice)), + builder: elpbuilder.BuildStakingAction, + }, + { + name: "ChangeStakeCandidate", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _changeCandidateMethod.ID, + MustNoErrorV(_changeCandidateMethod.Inputs.Pack("name", uint64(11), data))..., + ), + action: MustNoErrorV(NewChangeCandidate(nonce, "name", 11, data, gasLimit, gasPrice)), + builder: elpbuilder.BuildStakingAction, + }, + { + name: "UnStake", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _unstakeMethod.ID, + MustNoErrorV(_unstakeMethod.Inputs.Pack(uint64(12), data))..., + ), + action: MustNoErrorV(NewUnstake(nonce, 12, data, gasLimit, gasPrice)), + builder: elpbuilder.BuildStakingAction, + }, + { + name: "StakeWithdraw", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _withdrawStakeMethod.ID, + MustNoErrorV(_withdrawStakeMethod.Inputs.Pack(uint64(13), data))..., + ), + action: MustNoErrorV(NewWithdrawStake(nonce, 13, data, gasLimit, gasPrice)), + builder: elpbuilder.BuildStakingAction, + }, + { + name: "ReStake", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _restakeMethod.ID, + MustNoErrorV(_restakeMethod.Inputs.Pack(uint64(14), uint32(7200), false, data))..., + ), + action: MustNoErrorV(NewRestake(nonce, 14, 7200, false, data, gasLimit, gasPrice)), + builder: elpbuilder.BuildStakingAction, + }, + { + name: "TransferStakeOwnership", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _transferStakeMethod.ID, + MustNoErrorV(_transferStakeMethod.Inputs.Pack( + common.BytesToAddress(MustNoErrorV(address.FromString(to)).Bytes()), + uint64(15), data), + )..., + ), + action: MustNoErrorV(NewTransferStake(nonce, to, 15, data, gasLimit, gasPrice)), + builder: elpbuilder.BuildStakingAction, + }, + { + name: "RegisterStakeCandidate", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _candidateRegisterMethod.ID, + MustNoErrorV(_candidateRegisterMethod.Inputs.Pack( + "name", addrto, addrto, addrto, amount, uint32(6400), false, data, + ))..., + ), + action: MustNoErrorV(NewCandidateRegister( + nonce, "name", to, to, to, amount.String(), + 6400, false, data, gasLimit, gasPrice, + )), + builder: elpbuilder.BuildStakingAction, + }, + { + name: "UpdateStakeCandidate", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _candidateUpdateMethod.ID, + MustNoErrorV(_candidateUpdateMethod.Inputs.Pack("name", addrto, addrto))..., + ), + action: MustNoErrorV(NewCandidateUpdate(nonce, "name", to, to, gasLimit, gasPrice)), + builder: elpbuilder.BuildStakingAction, + }, + { + name: "StakeCandidateActivate", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + candidateActivateMethod.ID, + MustNoErrorV(candidateActivateMethod.Inputs.Pack(uint64(16)))..., + ), + action: &CandidateActivate{ + AbstractAction: AbstractAction{ + version: version.ProtocolVersion, + chainID: chainID, + nonce: nonce, + gasLimit: gasLimit, + gasPrice: gasPrice, + }, + bucketID: 16, + }, + builder: elpbuilder.BuildStakingAction, + }, + { + name: "StakeCandidateEndorsement", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + candidateEndorsementMethod.ID, + MustNoErrorV(candidateEndorsementMethod.Inputs.Pack(uint64(17), false))..., + ), + action: &CandidateEndorsement{ + AbstractAction: AbstractAction{ + version: version.ProtocolVersion, + chainID: chainID, + nonce: nonce, + gasLimit: gasLimit, + gasPrice: gasPrice, + }, + bucketIndex: 17, + }, + builder: elpbuilder.BuildStakingAction, + }, + { + name: "StakeCandidateTransferOwnership", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.StakingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _candidateTransferOwnershipMethod.ID, + MustNoErrorV(_candidateTransferOwnershipMethod.Inputs.Pack(addrto, data))..., + ), + action: MustNoErrorV(NewCandidateTransferOwnership(nonce, gasLimit, gasPrice, to, data)), + builder: elpbuilder.BuildStakingAction, + }, + { + name: "ClaimFromRewardingFund", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.RewardingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _claimRewardingMethod.ID, + MustNoErrorV(_claimRewardingMethod.Inputs.Pack(amount, data, to))..., + ), + action: &ClaimFromRewardingFund{ + AbstractAction: AbstractAction{ + version: version.ProtocolVersion, + chainID: chainID, + nonce: nonce, + gasLimit: gasLimit, + gasPrice: gasPrice, + }, + amount: amount, + address: to, + data: data, + }, + builder: elpbuilder.BuildRewardingAction, + }, + { + name: "DepositToRewardingFund", + encoding: iotextypes.Encoding_ETHEREUM_EIP155, + txto: MustNoErrorV(address.FromBytes(address.RewardingProtocolAddrHash[:])).String(), + txamount: big.NewInt(0), + txdata: append( + _depositRewardMethod.ID, + MustNoErrorV(_depositRewardMethod.Inputs.Pack(amount, data))..., + ), + action: &DepositToRewardingFund{ + AbstractAction: AbstractAction{ + version: version.ProtocolVersion, + chainID: chainID, + nonce: nonce, + gasLimit: gasLimit, + gasPrice: gasPrice, + }, + amount: amount, + data: data, + }, + builder: elpbuilder.BuildRewardingAction, + }, + { + name: "UnprotectedExecution", + encoding: iotextypes.Encoding_ETHEREUM_UNPROTECTED, + txto: to, + txamount: amount, + txdata: data, + action: MustNoErrorV(NewExecution(to, nonce, amount, gasLimit, gasPrice, data)), + builder: elpbuilder.BuildExecution, + }, + } + + verifytx := func(tx *types.Transaction, enc iotextypes.Encoding, amount *big.Int, data []byte, to string, sigv *big.Int) { + r.Equal(tx.Nonce(), nonce) + r.Equal(tx.Gas(), gasLimit) + r.Equal(tx.GasPrice(), gasPrice) + r.Equal(tx.Type(), uint8(types.LegacyTxType)) + r.Equal(tx.Value(), amount) + r.Equal(tx.Data(), data) + if to == "" { + r.Nil(tx.To()) + } else { + r.Equal(to, strings.ToLower(tx.To().String())) + } + if enc == iotextypes.Encoding_ETHEREUM_UNPROTECTED { + r.False(tx.Protected()) + r.Zero(tx.ChainId().Uint64()) + r.True(sigv.Uint64() == 27 || sigv.Uint64() == 28) + } else if enc == iotextypes.Encoding_ETHEREUM_EIP155 { + r.True(tx.Protected()) + r.Equal(tx.ChainId().Uint64(), uint64(chainID)) + r.True(sigv.Uint64() > 28) + } + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + toaddr := "" + if tt.txto != "" { + toaddr = "0x" + hex.EncodeToString(MustNoErrorV(address.FromString(tt.txto)).Bytes()) + } + // build eth tx from test case + var ( + tx = MustNoErrorV(tt.action.ToEthTx(chainID)) + signer = MustNoErrorV(NewEthSigner(tt.encoding, chainID)) + signature = MustNoErrorV(sk.Sign(tx.Hash().Bytes())) + builttx = MustNoErrorV(RawTxToSignedTx(tx, signer, signature)) + raw = MustNoErrorV(builttx.MarshalBinary()) + ) + + sv, _, _ := builttx.RawSignatureValues() + verifytx(builttx, tt.encoding, tt.txamount, tt.txdata, toaddr, sv) + + // decode eth tx from builttx hex raw + decodedtx, err := DecodeEtherTx(hex.EncodeToString(raw)) + r.NoError(err) + enc, sig, pk, err := ExtractTypeSigPubkey(decodedtx) + r.NoError(err) + r.Equal(enc, tt.encoding) + r.Equal(sig[:64], signature[:64]) + sv, _, _ = decodedtx.RawSignatureValues() + verifytx(decodedtx, tt.encoding, tt.txamount, tt.txdata, toaddr, sv) + + // build elp from eth tx by native converter + elp, err := tt.builder(decodedtx) + r.NoError(err) + sealed, err := (&Deserializer{}). + SetEvmNetworkID(chainID). + ActionToSealedEnvelope(&iotextypes.Action{ + Core: elp.Proto(), + SenderPubKey: pk.Bytes(), + Signature: sig, + Encoding: tt.encoding, + }) + r.NoError(err) + action, ok := sealed.Action().(EthCompatibleAction) + r.True(ok) + convertedrawtx, err := action.ToEthTx(chainID) + r.NoError(err) + r.NoError(sealed.VerifySignature()) + convertedsignedtx, err := RawTxToSignedTx(convertedrawtx, signer, signature) + r.NoError(err) + r.Equal(convertedsignedtx.Hash(), decodedtx.Hash()) + sv, _, _ = convertedsignedtx.RawSignatureValues() + verifytx(convertedsignedtx, tt.encoding, tt.txamount, tt.txdata, toaddr, sv) + }) + } +} + func convertToNativeProto(tx *types.Transaction, actType string) *iotextypes.ActionCore { elpBuilder := &EnvelopeBuilder{} elpBuilder.SetGasLimit(tx.Gas()).SetGasPrice(tx.GasPrice()).SetNonce(tx.Nonce())