status-go/transactions/transactor_test.go
saledjenic 79b1c547d1
Improvements on resolving nonce (#5658)
* chore_: unused `BuildTx` function removed from the processor interface and types that are implement it

Since the `BuildTx` function is not used anywhere, it's removed from the code.

* fix_: resolving nonce improvements

When the app sends more than a single tx from the same account on the same chain, some
chains do not return appropriate nonce (they do not consider pending txs), because of
that we place more tx with the same nonce, where all but the first one fail.

Changes in this PR keep track of nonces being used in the same sending/bridging flow, which means
for the first tx from the multi txs the app asks the chain for the nonce, and every next nonce is resolved
by incrementing the last used nonce by 1.
2024-08-12 08:07:32 -04:00

492 lines
15 KiB
Go

package transactions
import (
"fmt"
"math/big"
"reflect"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/suite"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
gethtypes "github.com/ethereum/go-ethereum/core/types"
gethcrypto "github.com/ethereum/go-ethereum/crypto"
gethparams "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
wallet_common "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/sqlite"
"github.com/status-im/status-go/t/utils"
"github.com/status-im/status-go/transactions/fake"
)
func TestTransactorSuite(t *testing.T) {
utils.Init()
suite.Run(t, new(TransactorSuite))
}
type TransactorSuite struct {
suite.Suite
server *gethrpc.Server
client *gethrpc.Client
txServiceMockCtrl *gomock.Controller
txServiceMock *fake.MockPublicTransactionPoolAPI
nodeConfig *params.NodeConfig
manager *Transactor
}
func (s *TransactorSuite) SetupTest() {
s.txServiceMockCtrl = gomock.NewController(s.T())
s.server, s.txServiceMock = fake.NewTestServer(s.txServiceMockCtrl)
s.client = gethrpc.DialInProc(s.server)
// expected by simulated backend
chainID := gethparams.AllEthashProtocolChanges.ChainID.Uint64()
db, err := sqlite.OpenUnecryptedDB(sqlite.InMemoryPath) // dummy to make rpc.Client happy
s.Require().NoError(err)
rpcClient, _ := rpc.NewClient(s.client, chainID, params.UpstreamRPCConfig{}, nil, db, nil)
rpcClient.UpstreamChainID = chainID
nodeConfig, err := utils.MakeTestNodeConfigWithDataDir("", "/tmp", chainID)
s.Require().NoError(err)
s.nodeConfig = nodeConfig
s.manager = NewTransactor()
s.manager.sendTxTimeout = time.Second
s.manager.SetNetworkID(chainID)
s.manager.SetRPC(rpcClient, time.Second)
}
func (s *TransactorSuite) TearDownTest() {
s.txServiceMockCtrl.Finish()
s.server.Stop()
s.client.Close()
}
var (
testGas = hexutil.Uint64(defaultGas + 1)
testGasPrice = (*hexutil.Big)(big.NewInt(10))
testNonce = hexutil.Uint64(10)
)
func (s *TransactorSuite) setupTransactionPoolAPI(args SendTxArgs, returnNonce, resultNonce hexutil.Uint64, account *account.SelectedExtKey, txErr error) {
// Expect calls to gas functions only if there are no user defined values.
// And also set the expected gas and gas price for RLP encoding the expected tx.
var usedGas hexutil.Uint64
var usedGasPrice *big.Int
s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), gomock.Eq(common.Address(account.Address)), gethrpc.PendingBlockNumber).Return(&returnNonce, nil)
if !args.IsDynamicFeeTx() {
if args.GasPrice == nil {
usedGasPrice = (*big.Int)(testGasPrice)
s.txServiceMock.EXPECT().GasPrice(gomock.Any()).Return(testGasPrice, nil)
} else {
usedGasPrice = (*big.Int)(args.GasPrice)
}
}
if args.Gas == nil {
s.txServiceMock.EXPECT().EstimateGas(gomock.Any(), gomock.Any()).Return(testGas, nil)
usedGas = testGas
} else {
usedGas = *args.Gas
}
// Prepare the transaction and RLP encode it.
data := s.rlpEncodeTx(args, s.nodeConfig, account, &resultNonce, usedGas, usedGasPrice)
// Expect the RLP encoded transaction.
s.txServiceMock.EXPECT().SendRawTransaction(gomock.Any(), data).Return(common.Hash{}, txErr)
}
func (s *TransactorSuite) rlpEncodeTx(args SendTxArgs, config *params.NodeConfig, account *account.SelectedExtKey, nonce *hexutil.Uint64, gas hexutil.Uint64, gasPrice *big.Int) hexutil.Bytes {
var txData gethtypes.TxData
to := common.Address(*args.To)
if args.IsDynamicFeeTx() {
gasTipCap := (*big.Int)(args.MaxPriorityFeePerGas)
gasFeeCap := (*big.Int)(args.MaxFeePerGas)
txData = &gethtypes.DynamicFeeTx{
Nonce: uint64(*nonce),
Gas: uint64(gas),
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
To: &to,
Value: args.Value.ToInt(),
Data: args.GetInput(),
}
} else {
txData = &gethtypes.LegacyTx{
Nonce: uint64(*nonce),
GasPrice: gasPrice,
Gas: uint64(gas),
To: &to,
Value: args.Value.ToInt(),
Data: args.GetInput(),
}
}
newTx := gethtypes.NewTx(txData)
chainID := big.NewInt(int64(s.nodeConfig.NetworkID))
signedTx, err := gethtypes.SignTx(newTx, gethtypes.NewLondonSigner(chainID), account.AccountKey.PrivateKey)
s.NoError(err)
data, err := signedTx.MarshalBinary()
s.NoError(err)
return hexutil.Bytes(data)
}
func (s *TransactorSuite) TestGasValues() {
key, _ := gethcrypto.GenerateKey()
selectedAccount := &account.SelectedExtKey{
Address: account.FromAddress(utils.TestConfig.Account1.WalletAddress),
AccountKey: &types.Key{PrivateKey: key},
}
testCases := []struct {
name string
gas *hexutil.Uint64
gasPrice *hexutil.Big
maxFeePerGas *hexutil.Big
maxPriorityFeePerGas *hexutil.Big
}{
{
"noGasDef",
nil,
nil,
nil,
nil,
},
{
"gasDefined",
&testGas,
nil,
nil,
nil,
},
{
"gasPriceDefined",
nil,
testGasPrice,
nil,
nil,
},
{
"nilSignTransactionSpecificArgs",
nil,
nil,
nil,
nil,
},
{
"maxFeeAndPriorityset",
nil,
nil,
testGasPrice,
testGasPrice,
},
}
for _, testCase := range testCases {
s.T().Run(testCase.name, func(t *testing.T) {
s.SetupTest()
args := SendTxArgs{
From: account.FromAddress(utils.TestConfig.Account1.WalletAddress),
To: account.ToAddress(utils.TestConfig.Account2.WalletAddress),
Gas: testCase.gas,
GasPrice: testCase.gasPrice,
MaxFeePerGas: testCase.maxFeePerGas,
MaxPriorityFeePerGas: testCase.maxPriorityFeePerGas,
}
s.setupTransactionPoolAPI(args, testNonce, testNonce, selectedAccount, nil)
hash, _, err := s.manager.SendTransaction(args, selectedAccount, -1)
s.NoError(err)
s.False(reflect.DeepEqual(hash, common.Hash{}))
})
}
}
func (s *TransactorSuite) setupBuildTransactionMocks(args SendTxArgs, account *account.SelectedExtKey) {
s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), gomock.Eq(common.Address(account.Address)), gethrpc.PendingBlockNumber).Return(&testNonce, nil)
if !args.IsDynamicFeeTx() && args.GasPrice == nil {
s.txServiceMock.EXPECT().GasPrice(gomock.Any()).Return(testGasPrice, nil)
}
if args.Gas == nil {
s.txServiceMock.EXPECT().EstimateGas(gomock.Any(), gomock.Any()).Return(testGas, nil)
}
}
func (s *TransactorSuite) TestBuildAndValidateTransaction() {
key, _ := gethcrypto.GenerateKey()
selectedAccount := &account.SelectedExtKey{
Address: account.FromAddress(utils.TestConfig.Account1.WalletAddress),
AccountKey: &types.Key{PrivateKey: key},
}
chainID := s.nodeConfig.NetworkID
fromAddress := account.FromAddress(utils.TestConfig.Account1.WalletAddress)
toAddress := account.ToAddress(utils.TestConfig.Account2.WalletAddress)
value := (*hexutil.Big)(big.NewInt(10))
expectedGasPrice := (*big.Int)(testGasPrice)
expectedGas := uint64(testGas)
expectedNonce := uint64(testNonce)
s.T().Run("DynamicFeeTransaction", func(t *testing.T) {
s.SetupTest()
gas := hexutil.Uint64(21000)
args := SendTxArgs{
From: fromAddress,
To: toAddress,
Gas: &gas,
Value: value,
MaxFeePerGas: testGasPrice,
MaxPriorityFeePerGas: testGasPrice,
}
s.setupBuildTransactionMocks(args, selectedAccount)
tx, _, err := s.manager.ValidateAndBuildTransaction(chainID, args, -1)
s.NoError(err)
s.Equal(tx.Gas(), uint64(gas), "The gas shouldn't be estimated, but should use the gas from the Tx")
s.Equal(tx.GasFeeCap(), expectedGasPrice, "The maxFeePerGas should be the same as in the original Tx")
s.Equal(tx.GasTipCap(), expectedGasPrice, "The maxPriorityFeePerGas should be the same as in the original Tx")
s.Equal(tx.Type(), uint8(gethtypes.DynamicFeeTxType), "The transaction type should be DynamicFeeTxType")
})
s.T().Run("DynamicFeeTransaction with gas estimation", func(t *testing.T) {
s.SetupTest()
args := SendTxArgs{
From: fromAddress,
To: toAddress,
Value: value,
MaxFeePerGas: testGasPrice,
MaxPriorityFeePerGas: testGasPrice,
}
s.setupBuildTransactionMocks(args, selectedAccount)
tx, _, err := s.manager.ValidateAndBuildTransaction(chainID, args, -1)
s.NoError(err)
s.Equal(tx.Gas(), expectedGas, "The gas should be estimated if not present in the original Tx")
s.Equal(tx.Nonce(), expectedNonce, "The nonce should be added if not present in the original Tx")
s.Equal(tx.GasFeeCap(), expectedGasPrice, "The maxFeePerGas should be the same as in the original Tx")
s.Equal(tx.GasTipCap(), expectedGasPrice, "The maxPriorityFeePerGas should be the same as in the original Tx")
s.Equal(tx.Type(), uint8(gethtypes.DynamicFeeTxType), "The transaction type should be DynamicFeeTxType")
})
s.T().Run("LegacyTransaction", func(t *testing.T) {
s.SetupTest()
gas := hexutil.Uint64(21000)
gasPrice := (*hexutil.Big)(big.NewInt(10))
args := SendTxArgs{
From: fromAddress,
To: toAddress,
Value: value,
Gas: &gas,
GasPrice: gasPrice,
}
s.setupBuildTransactionMocks(args, selectedAccount)
tx, _, err := s.manager.ValidateAndBuildTransaction(chainID, args, -1)
s.NoError(err)
s.Equal(tx.Gas(), uint64(gas), "The gas shouldn't be estimated, but should use the gas from the Tx")
s.Equal(tx.GasPrice(), expectedGasPrice, "The gasPrice should be the same as in the original Tx")
s.Equal(tx.Type(), uint8(gethtypes.LegacyTxType), "The transaction type should be LegacyTxType")
})
s.T().Run("LegacyTransaction without gas estimation", func(t *testing.T) {
s.SetupTest()
args := SendTxArgs{
From: fromAddress,
To: toAddress,
Value: value,
}
s.setupBuildTransactionMocks(args, selectedAccount)
tx, _, err := s.manager.ValidateAndBuildTransaction(chainID, args, -1)
s.NoError(err)
s.Equal(tx.Gas(), expectedGas, "The gas should be estimated if not present in the original Tx")
s.Equal(tx.GasPrice(), expectedGasPrice, "The gasPrice should be estimated if not present in the original Tx")
s.Equal(tx.Type(), uint8(gethtypes.LegacyTxType), "The transaction type should be LegacyTxType")
})
}
func (s *TransactorSuite) TestArgsValidation() {
args := SendTxArgs{
From: account.FromAddress(utils.TestConfig.Account1.WalletAddress),
To: account.ToAddress(utils.TestConfig.Account2.WalletAddress),
Data: types.HexBytes([]byte{0x01, 0x02}),
Input: types.HexBytes([]byte{0x02, 0x01}),
}
s.False(args.Valid())
selectedAccount := &account.SelectedExtKey{
Address: account.FromAddress(utils.TestConfig.Account1.WalletAddress),
}
_, _, err := s.manager.SendTransaction(args, selectedAccount, -1)
s.EqualError(err, ErrInvalidSendTxArgs.Error())
}
func (s *TransactorSuite) TestAccountMismatch() {
args := SendTxArgs{
From: account.FromAddress(utils.TestConfig.Account1.WalletAddress),
To: account.ToAddress(utils.TestConfig.Account2.WalletAddress),
}
var err error
// missing account
_, _, err = s.manager.SendTransaction(args, nil, -1)
s.EqualError(err, account.ErrNoAccountSelected.Error())
// mismatched accounts
selectedAccount := &account.SelectedExtKey{
Address: account.FromAddress(utils.TestConfig.Account2.WalletAddress),
}
_, _, err = s.manager.SendTransaction(args, selectedAccount, -1)
s.EqualError(err, ErrInvalidTxSender.Error())
}
func (s *TransactorSuite) TestSendTransactionWithSignature() {
privKey, err := crypto.GenerateKey()
s.Require().NoError(err)
address := crypto.PubkeyToAddress(privKey.PublicKey)
scenarios := []struct {
nonceFromNetwork hexutil.Uint64
txNonce hexutil.Uint64
expectError bool
}{
{
nonceFromNetwork: hexutil.Uint64(0),
txNonce: hexutil.Uint64(0),
expectError: false,
},
{
nonceFromNetwork: hexutil.Uint64(0),
txNonce: hexutil.Uint64(1),
expectError: true,
},
}
for _, localScenario := range scenarios {
// to satisfy gosec: C601 checks
scenario := localScenario
desc := fmt.Sprintf("nonceFromNetwork: %d, tx nonce: %d, expect error: %v", scenario.nonceFromNetwork, scenario.txNonce, scenario.expectError)
s.T().Run(desc, func(t *testing.T) {
nonce := scenario.txNonce
from := address
to := address
value := (*hexutil.Big)(big.NewInt(10))
gas := hexutil.Uint64(21000)
gasPrice := (*hexutil.Big)(big.NewInt(2000000000))
data := []byte{}
chainID := big.NewInt(int64(s.nodeConfig.NetworkID))
args := SendTxArgs{
From: from,
To: &to,
Gas: &gas,
GasPrice: gasPrice,
Value: value,
Nonce: &nonce,
Data: nil,
}
// simulate transaction signed externally
signer := gethtypes.NewLondonSigner(chainID)
tx := gethtypes.NewTransaction(uint64(nonce), common.Address(to), (*big.Int)(value), uint64(gas), (*big.Int)(gasPrice), data)
hash := signer.Hash(tx)
sig, err := gethcrypto.Sign(hash[:], privKey)
s.Require().NoError(err)
txWithSig, err := tx.WithSignature(signer, sig)
s.Require().NoError(err)
expectedEncodedTx, err := rlp.EncodeToBytes(txWithSig)
s.Require().NoError(err)
s.txServiceMock.EXPECT().
GetTransactionCount(gomock.Any(), common.Address(address), gethrpc.PendingBlockNumber).
Return(&scenario.nonceFromNetwork, nil)
if !scenario.expectError {
s.txServiceMock.EXPECT().
SendRawTransaction(gomock.Any(), hexutil.Bytes(expectedEncodedTx)).
Return(common.Hash{}, nil)
}
tx, err = s.manager.BuildTransactionWithSignature(s.nodeConfig.NetworkID, args, sig)
if scenario.expectError {
s.Error(err)
} else {
s.NoError(err)
_, err = s.manager.SendTransactionWithSignature(common.Address(args.From), args.Symbol, args.MultiTransactionID, tx)
if scenario.expectError {
s.Error(err)
} else {
s.NoError(err)
}
}
})
}
}
func (s *TransactorSuite) TestSendTransactionWithSignature_InvalidSignature() {
args := SendTxArgs{}
_, err := s.manager.BuildTransactionWithSignature(1, args, []byte{})
s.Equal(ErrInvalidSignatureSize, err)
}
func (s *TransactorSuite) TestHashTransaction() {
privKey, err := crypto.GenerateKey()
s.Require().NoError(err)
address := crypto.PubkeyToAddress(privKey.PublicKey)
remoteNonce := hexutil.Uint64(1)
txNonce := hexutil.Uint64(0)
from := address
to := address
value := (*hexutil.Big)(big.NewInt(10))
gas := hexutil.Uint64(21000)
gasPrice := (*hexutil.Big)(big.NewInt(2000000000))
args := SendTxArgs{
From: from,
To: &to,
Gas: &gas,
GasPrice: gasPrice,
Value: value,
Nonce: &txNonce,
Data: nil,
}
s.txServiceMock.EXPECT().
GetTransactionCount(gomock.Any(), common.Address(address), gethrpc.PendingBlockNumber).
Return(&remoteNonce, nil)
newArgs, hash, err := s.manager.HashTransaction(args)
s.Require().NoError(err)
// args should be updated with the right nonce
s.NotEqual(*args.Nonce, *newArgs.Nonce)
s.Equal(remoteNonce, *newArgs.Nonce)
s.NotEqual(common.Hash{}, hash)
}
func (s *TransactorSuite) TestStoreAndTrackPendingTx() {
s.Nil(s.manager.pendingTracker)
// Empty tracker doesn't produce error
err := s.manager.StoreAndTrackPendingTx(common.Address{}, "", 0, wallet_common.MultiTransactionIDType(0), nil)
s.NoError(err)
}