status-go/geth/txqueue/txqueue_manager_test.go

325 lines
10 KiB
Go
Raw Normal View History

package txqueue
import (
"context"
"math/big"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/suite"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/geth/rpc"
"github.com/status-im/status-go/geth/txqueue/fake"
. "github.com/status-im/status-go/testing"
)
func TestTxQueueTestSuite(t *testing.T) {
suite.Run(t, new(TxQueueTestSuite))
}
type TxQueueTestSuite struct {
suite.Suite
nodeManagerMockCtrl *gomock.Controller
nodeManagerMock *common.MockNodeManager
accountManagerMockCtrl *gomock.Controller
accountManagerMock *common.MockAccountManager
server *gethrpc.Server
client *gethrpc.Client
txServiceMockCtrl *gomock.Controller
txServiceMock *fake.MockFakePublicTransactionPoolAPI
}
func (s *TxQueueTestSuite) SetupTest() {
s.nodeManagerMockCtrl = gomock.NewController(s.T())
s.accountManagerMockCtrl = gomock.NewController(s.T())
s.txServiceMockCtrl = gomock.NewController(s.T())
s.nodeManagerMock = common.NewMockNodeManager(s.nodeManagerMockCtrl)
s.accountManagerMock = common.NewMockAccountManager(s.accountManagerMockCtrl)
s.server, s.txServiceMock = fake.NewTestServer(s.txServiceMockCtrl)
s.client = gethrpc.DialInProc(s.server)
rpclient, _ := rpc.NewClient(s.client, params.UpstreamRPCConfig{})
s.nodeManagerMock.EXPECT().RPCClient().Return(rpclient)
}
func (s *TxQueueTestSuite) TearDownTest() {
s.nodeManagerMockCtrl.Finish()
s.accountManagerMockCtrl.Finish()
s.txServiceMockCtrl.Finish()
s.server.Stop()
s.client.Close()
}
func (s *TxQueueTestSuite) setupTransactionPoolAPI(account *common.SelectedExtKey, nonce hexutil.Uint64, gas hexutil.Big, txErr error) {
s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), account.Address, gethrpc.PendingBlockNumber).Return(&nonce, nil)
s.txServiceMock.EXPECT().GasPrice(gomock.Any()).Return(big.NewInt(10), nil)
s.txServiceMock.EXPECT().EstimateGas(gomock.Any(), gomock.Any()).Return(&gas, nil)
s.txServiceMock.EXPECT().SendRawTransaction(gomock.Any(), gomock.Any()).Return(gethcommon.Hash{}, txErr)
}
func (s *TxQueueTestSuite) setupStatusBackend(account *common.SelectedExtKey, password string, passwordErr error) {
nodeConfig, nodeErr := params.NewNodeConfig("/tmp", params.RopstenNetworkID, true)
s.nodeManagerMock.EXPECT().NodeConfig().Return(nodeConfig, nodeErr)
s.accountManagerMock.EXPECT().SelectedAccount().Return(account, nil)
s.accountManagerMock.EXPECT().VerifyAccountPassword(nodeConfig.KeyStoreDir, account.Address.String(), password).Return(
nil, passwordErr)
}
func (s *TxQueueTestSuite) TestCompleteTransaction() {
password := TestConfig.Account1.Password
key, _ := crypto.GenerateKey()
account := &common.SelectedExtKey{
Address: common.FromAddress(TestConfig.Account1.Address),
AccountKey: &keystore.Key{PrivateKey: key},
}
s.setupStatusBackend(account, password, nil)
nonce := hexutil.Uint64(10)
gas := hexutil.Big(*big.NewInt(defaultGas + 1))
s.setupTransactionPoolAPI(account, nonce, gas, nil)
txQueueManager := NewManager(s.nodeManagerMock, s.accountManagerMock)
txQueueManager.Start()
defer txQueueManager.Stop()
tx := txQueueManager.CreateTransaction(context.Background(), common.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
})
// TransactionQueueHandler is required to enqueue a transaction.
txQueueManager.SetTransactionQueueHandler(func(queuedTx *common.QueuedTx) {
s.Equal(tx.ID, queuedTx.ID)
})
txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) {
s.Equal(tx.ID, queuedTx.ID)
s.NoError(err)
})
err := txQueueManager.QueueTransaction(tx)
s.NoError(err)
w := make(chan struct{})
go func() {
hash, err := txQueueManager.CompleteTransaction(tx.ID, password)
s.NoError(err)
s.Equal(tx.Hash, hash)
close(w)
}()
err = txQueueManager.WaitForTransaction(tx)
s.NoError(err)
// Check that error is assigned to the transaction.
s.NoError(tx.Err)
// Transaction should be already removed from the queue.
s.False(txQueueManager.TransactionQueue().Has(tx.ID))
s.NoError(WaitClosed(w, time.Second))
}
func (s *TxQueueTestSuite) TestCompleteTransactionMultipleTimes() {
password := TestConfig.Account1.Password
key, _ := crypto.GenerateKey()
account := &common.SelectedExtKey{
Address: common.FromAddress(TestConfig.Account1.Address),
AccountKey: &keystore.Key{PrivateKey: key},
}
s.setupStatusBackend(account, password, nil)
nonce := hexutil.Uint64(10)
gas := hexutil.Big(*big.NewInt(defaultGas + 1))
s.setupTransactionPoolAPI(account, nonce, gas, nil)
txQueueManager := NewManager(s.nodeManagerMock, s.accountManagerMock)
txQueueManager.Start()
defer txQueueManager.Stop()
tx := txQueueManager.CreateTransaction(context.Background(), common.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
})
// TransactionQueueHandler is required to enqueue a transaction.
txQueueManager.SetTransactionQueueHandler(func(queuedTx *common.QueuedTx) {
s.Equal(tx.ID, queuedTx.ID)
})
txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) {
s.Equal(tx.ID, queuedTx.ID)
s.NoError(err)
})
err := txQueueManager.QueueTransaction(tx)
s.NoError(err)
var (
wg sync.WaitGroup
mu sync.Mutex
completedTx int
inprogressTx int
txCount = 3
)
for i := 0; i < txCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := txQueueManager.CompleteTransaction(tx.ID, password)
mu.Lock()
if err == nil {
completedTx++
} else if err == ErrQueuedTxInProgress {
inprogressTx++
} else {
s.Fail("tx failed with unexpected error: ", err.Error())
}
mu.Unlock()
}()
}
err = txQueueManager.WaitForTransaction(tx)
s.NoError(err)
// Check that error is assigned to the transaction.
s.NoError(tx.Err)
// Transaction should be already removed from the queue.
s.False(txQueueManager.TransactionQueue().Has(tx.ID))
// Wait for all CompleteTransaction calls.
wg.Wait()
s.Equal(1, completedTx, "only 1 tx expected to be completed")
s.Equal(txCount-1, inprogressTx, "txs expected to be reported as inprogress")
}
func (s *TxQueueTestSuite) TestAccountMismatch() {
s.accountManagerMock.EXPECT().SelectedAccount().Return(&common.SelectedExtKey{
Address: common.FromAddress(TestConfig.Account2.Address),
}, nil)
txQueueManager := NewManager(s.nodeManagerMock, s.accountManagerMock)
txQueueManager.Start()
defer txQueueManager.Stop()
tx := txQueueManager.CreateTransaction(context.Background(), common.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
})
// TransactionQueueHandler is required to enqueue a transaction.
txQueueManager.SetTransactionQueueHandler(func(queuedTx *common.QueuedTx) {
s.Equal(tx.ID, queuedTx.ID)
})
// Missmatched address is a recoverable error, that's why
// the return handler is called.
txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) {
s.Equal(tx.ID, queuedTx.ID)
s.Equal(ErrInvalidCompleteTxSender, err)
s.Nil(tx.Err)
})
err := txQueueManager.QueueTransaction(tx)
s.NoError(err)
_, err = txQueueManager.CompleteTransaction(tx.ID, TestConfig.Account1.Password)
s.Equal(err, ErrInvalidCompleteTxSender)
// Transaction should stay in the queue as mismatched accounts
// is a recoverable error.
s.True(txQueueManager.TransactionQueue().Has(tx.ID))
}
func (s *TxQueueTestSuite) TestInvalidPassword() {
password := "invalid-password"
key, _ := crypto.GenerateKey()
account := &common.SelectedExtKey{
Address: common.FromAddress(TestConfig.Account1.Address),
AccountKey: &keystore.Key{PrivateKey: key},
}
s.setupStatusBackend(account, password, keystore.ErrDecrypt)
txQueueManager := NewManager(s.nodeManagerMock, s.accountManagerMock)
txQueueManager.Start()
defer txQueueManager.Stop()
tx := txQueueManager.CreateTransaction(context.Background(), common.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
})
// TransactionQueueHandler is required to enqueue a transaction.
txQueueManager.SetTransactionQueueHandler(func(queuedTx *common.QueuedTx) {
s.Equal(tx.ID, queuedTx.ID)
})
// Missmatched address is a revocable error, that's why
// the return handler is called.
txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) {
s.Equal(tx.ID, queuedTx.ID)
s.Equal(keystore.ErrDecrypt, err)
s.Nil(tx.Err)
})
err := txQueueManager.QueueTransaction(tx)
s.NoError(err)
_, err = txQueueManager.CompleteTransaction(tx.ID, password)
s.Equal(err.Error(), keystore.ErrDecrypt.Error())
// Transaction should stay in the queue as mismatched accounts
// is a recoverable error.
s.True(txQueueManager.TransactionQueue().Has(tx.ID))
}
func (s *TxQueueTestSuite) TestDiscardTransaction() {
txQueueManager := NewManager(s.nodeManagerMock, s.accountManagerMock)
txQueueManager.Start()
defer txQueueManager.Stop()
tx := txQueueManager.CreateTransaction(context.Background(), common.SendTxArgs{
From: common.FromAddress(TestConfig.Account1.Address),
To: common.ToAddress(TestConfig.Account2.Address),
})
// TransactionQueueHandler is required to enqueue a transaction.
txQueueManager.SetTransactionQueueHandler(func(queuedTx *common.QueuedTx) {
s.Equal(tx.ID, queuedTx.ID)
})
txQueueManager.SetTransactionReturnHandler(func(queuedTx *common.QueuedTx, err error) {
s.Equal(tx.ID, queuedTx.ID)
s.Equal(ErrQueuedTxDiscarded, err)
})
err := txQueueManager.QueueTransaction(tx)
s.NoError(err)
w := make(chan struct{})
go func() {
err := txQueueManager.DiscardTransaction(tx.ID)
s.NoError(err)
close(w)
}()
err = txQueueManager.WaitForTransaction(tx)
s.Equal(ErrQueuedTxDiscarded, err)
// Check that error is assigned to the transaction.
s.Equal(ErrQueuedTxDiscarded, tx.Err)
// Transaction should be already removed from the queue.
s.False(txQueueManager.TransactionQueue().Has(tx.ID))
s.NoError(WaitClosed(w, time.Second))
}