diff --git a/sequencesender/mock_etherman.go b/sequencesender/mock_etherman.go new file mode 100644 index 0000000000..f4a2943aff --- /dev/null +++ b/sequencesender/mock_etherman.go @@ -0,0 +1,161 @@ +// Code generated by mockery v2.39.0. DO NOT EDIT. + +package sequencesender + +import ( + context "context" + + common "github.com/ethereum/go-ethereum/common" + + coretypes "github.com/ethereum/go-ethereum/core/types" + + mock "github.com/stretchr/testify/mock" + + types "github.com/0xPolygonHermez/zkevm-node/etherman/types" +) + +// EthermanMock is an autogenerated mock type for the etherman type +type EthermanMock struct { + mock.Mock +} + +// BuildSequenceBatchesTxData provides a mock function with given fields: sender, sequences, l2Coinbase +func (_m *EthermanMock) BuildSequenceBatchesTxData(sender common.Address, sequences []types.Sequence, l2Coinbase common.Address) (*common.Address, []byte, error) { + ret := _m.Called(sender, sequences, l2Coinbase) + + if len(ret) == 0 { + panic("no return value specified for BuildSequenceBatchesTxData") + } + + var r0 *common.Address + var r1 []byte + var r2 error + if rf, ok := ret.Get(0).(func(common.Address, []types.Sequence, common.Address) (*common.Address, []byte, error)); ok { + return rf(sender, sequences, l2Coinbase) + } + if rf, ok := ret.Get(0).(func(common.Address, []types.Sequence, common.Address) *common.Address); ok { + r0 = rf(sender, sequences, l2Coinbase) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*common.Address) + } + } + + if rf, ok := ret.Get(1).(func(common.Address, []types.Sequence, common.Address) []byte); ok { + r1 = rf(sender, sequences, l2Coinbase) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]byte) + } + } + + if rf, ok := ret.Get(2).(func(common.Address, []types.Sequence, common.Address) error); ok { + r2 = rf(sender, sequences, l2Coinbase) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// EstimateGasSequenceBatches provides a mock function with given fields: sender, sequences, l2Coinbase +func (_m *EthermanMock) EstimateGasSequenceBatches(sender common.Address, sequences []types.Sequence, l2Coinbase common.Address) (*coretypes.Transaction, error) { + ret := _m.Called(sender, sequences, l2Coinbase) + + if len(ret) == 0 { + panic("no return value specified for EstimateGasSequenceBatches") + } + + var r0 *coretypes.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(common.Address, []types.Sequence, common.Address) (*coretypes.Transaction, error)); ok { + return rf(sender, sequences, l2Coinbase) + } + if rf, ok := ret.Get(0).(func(common.Address, []types.Sequence, common.Address) *coretypes.Transaction); ok { + r0 = rf(sender, sequences, l2Coinbase) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(common.Address, []types.Sequence, common.Address) error); ok { + r1 = rf(sender, sequences, l2Coinbase) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLatestBatchNumber provides a mock function with given fields: +func (_m *EthermanMock) GetLatestBatchNumber() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetLatestBatchNumber") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLatestBlockHeader provides a mock function with given fields: ctx +func (_m *EthermanMock) GetLatestBlockHeader(ctx context.Context) (*coretypes.Header, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetLatestBlockHeader") + } + + var r0 *coretypes.Header + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*coretypes.Header, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.Header); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.Header) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewEthermanMock creates a new instance of EthermanMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEthermanMock(t interface { + mock.TestingT + Cleanup(func()) +}) *EthermanMock { + mock := &EthermanMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/sequencesender/mock_ethtxmanager.go b/sequencesender/mock_ethtxmanager.go new file mode 100644 index 0000000000..b07b0149f9 --- /dev/null +++ b/sequencesender/mock_ethtxmanager.go @@ -0,0 +1,58 @@ +// Code generated by mockery v2.39.0. DO NOT EDIT. + +package sequencesender + +import ( + context "context" + big "math/big" + + common "github.com/ethereum/go-ethereum/common" + + ethtxmanager "github.com/0xPolygonHermez/zkevm-node/ethtxmanager" + + mock "github.com/stretchr/testify/mock" + + pgx "github.com/jackc/pgx/v4" +) + +// EthTxManagerMock is an autogenerated mock type for the ethTxManager type +type EthTxManagerMock struct { + mock.Mock +} + +// Add provides a mock function with given fields: ctx, owner, id, from, to, value, data, gasOffset, dbTx +func (_m *EthTxManagerMock) Add(ctx context.Context, owner string, id string, from common.Address, to *common.Address, value *big.Int, data []byte, gasOffset uint64, dbTx pgx.Tx) error { + ret := _m.Called(ctx, owner, id, from, to, value, data, gasOffset, dbTx) + + if len(ret) == 0 { + panic("no return value specified for Add") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, common.Address, *common.Address, *big.Int, []byte, uint64, pgx.Tx) error); ok { + r0 = rf(ctx, owner, id, from, to, value, data, gasOffset, dbTx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ProcessPendingMonitoredTxs provides a mock function with given fields: ctx, owner, failedResultHandler, dbTx +func (_m *EthTxManagerMock) ProcessPendingMonitoredTxs(ctx context.Context, owner string, failedResultHandler ethtxmanager.ResultHandler, dbTx pgx.Tx) { + _m.Called(ctx, owner, failedResultHandler, dbTx) +} + +// NewEthTxManagerMock creates a new instance of EthTxManagerMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEthTxManagerMock(t interface { + mock.TestingT + Cleanup(func()) +}) *EthTxManagerMock { + mock := &EthTxManagerMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/sequencesender/mock_state.go b/sequencesender/mock_state.go new file mode 100644 index 0000000000..b823809846 --- /dev/null +++ b/sequencesender/mock_state.go @@ -0,0 +1,295 @@ +// Code generated by mockery v2.39.0. DO NOT EDIT. + +package sequencesender + +import ( + context "context" + + pgx "github.com/jackc/pgx/v4" + mock "github.com/stretchr/testify/mock" + + state "github.com/0xPolygonHermez/zkevm-node/state" + + time "time" +) + +// StateMock is an autogenerated mock type for the stateInterface type +type StateMock struct { + mock.Mock +} + +// GetBatchByNumber provides a mock function with given fields: ctx, batchNumber, dbTx +func (_m *StateMock) GetBatchByNumber(ctx context.Context, batchNumber uint64, dbTx pgx.Tx) (*state.Batch, error) { + ret := _m.Called(ctx, batchNumber, dbTx) + + if len(ret) == 0 { + panic("no return value specified for GetBatchByNumber") + } + + var r0 *state.Batch + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, pgx.Tx) (*state.Batch, error)); ok { + return rf(ctx, batchNumber, dbTx) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, pgx.Tx) *state.Batch); ok { + r0 = rf(ctx, batchNumber, dbTx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*state.Batch) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, pgx.Tx) error); ok { + r1 = rf(ctx, batchNumber, dbTx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetBlockByNumber provides a mock function with given fields: ctx, blockNumber, dbTx +func (_m *StateMock) GetBlockByNumber(ctx context.Context, blockNumber uint64, dbTx pgx.Tx) (*state.Block, error) { + ret := _m.Called(ctx, blockNumber, dbTx) + + if len(ret) == 0 { + panic("no return value specified for GetBlockByNumber") + } + + var r0 *state.Block + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, pgx.Tx) (*state.Block, error)); ok { + return rf(ctx, blockNumber, dbTx) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, pgx.Tx) *state.Block); ok { + r0 = rf(ctx, blockNumber, dbTx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*state.Block) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, pgx.Tx) error); ok { + r1 = rf(ctx, blockNumber, dbTx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetForcedBatch provides a mock function with given fields: ctx, forcedBatchNumber, dbTx +func (_m *StateMock) GetForcedBatch(ctx context.Context, forcedBatchNumber uint64, dbTx pgx.Tx) (*state.ForcedBatch, error) { + ret := _m.Called(ctx, forcedBatchNumber, dbTx) + + if len(ret) == 0 { + panic("no return value specified for GetForcedBatch") + } + + var r0 *state.ForcedBatch + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, pgx.Tx) (*state.ForcedBatch, error)); ok { + return rf(ctx, forcedBatchNumber, dbTx) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, pgx.Tx) *state.ForcedBatch); ok { + r0 = rf(ctx, forcedBatchNumber, dbTx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*state.ForcedBatch) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, pgx.Tx) error); ok { + r1 = rf(ctx, forcedBatchNumber, dbTx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetL2BlocksByBatchNumber provides a mock function with given fields: ctx, batchNumber, dbTx +func (_m *StateMock) GetL2BlocksByBatchNumber(ctx context.Context, batchNumber uint64, dbTx pgx.Tx) ([]state.L2Block, error) { + ret := _m.Called(ctx, batchNumber, dbTx) + + if len(ret) == 0 { + panic("no return value specified for GetL2BlocksByBatchNumber") + } + + var r0 []state.L2Block + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, pgx.Tx) ([]state.L2Block, error)); ok { + return rf(ctx, batchNumber, dbTx) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, pgx.Tx) []state.L2Block); ok { + r0 = rf(ctx, batchNumber, dbTx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]state.L2Block) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, pgx.Tx) error); ok { + r1 = rf(ctx, batchNumber, dbTx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLastBatchNumber provides a mock function with given fields: ctx, dbTx +func (_m *StateMock) GetLastBatchNumber(ctx context.Context, dbTx pgx.Tx) (uint64, error) { + ret := _m.Called(ctx, dbTx) + + if len(ret) == 0 { + panic("no return value specified for GetLastBatchNumber") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, pgx.Tx) (uint64, error)); ok { + return rf(ctx, dbTx) + } + if rf, ok := ret.Get(0).(func(context.Context, pgx.Tx) uint64); ok { + r0 = rf(ctx, dbTx) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, pgx.Tx) error); ok { + r1 = rf(ctx, dbTx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLastClosedBatch provides a mock function with given fields: ctx, dbTx +func (_m *StateMock) GetLastClosedBatch(ctx context.Context, dbTx pgx.Tx) (*state.Batch, error) { + ret := _m.Called(ctx, dbTx) + + if len(ret) == 0 { + panic("no return value specified for GetLastClosedBatch") + } + + var r0 *state.Batch + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, pgx.Tx) (*state.Batch, error)); ok { + return rf(ctx, dbTx) + } + if rf, ok := ret.Get(0).(func(context.Context, pgx.Tx) *state.Batch); ok { + r0 = rf(ctx, dbTx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*state.Batch) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, pgx.Tx) error); ok { + r1 = rf(ctx, dbTx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLastVirtualBatchNum provides a mock function with given fields: ctx, dbTx +func (_m *StateMock) GetLastVirtualBatchNum(ctx context.Context, dbTx pgx.Tx) (uint64, error) { + ret := _m.Called(ctx, dbTx) + + if len(ret) == 0 { + panic("no return value specified for GetLastVirtualBatchNum") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, pgx.Tx) (uint64, error)); ok { + return rf(ctx, dbTx) + } + if rf, ok := ret.Get(0).(func(context.Context, pgx.Tx) uint64); ok { + r0 = rf(ctx, dbTx) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, pgx.Tx) error); ok { + r1 = rf(ctx, dbTx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTimeForLatestBatchVirtualization provides a mock function with given fields: ctx, dbTx +func (_m *StateMock) GetTimeForLatestBatchVirtualization(ctx context.Context, dbTx pgx.Tx) (time.Time, error) { + ret := _m.Called(ctx, dbTx) + + if len(ret) == 0 { + panic("no return value specified for GetTimeForLatestBatchVirtualization") + } + + var r0 time.Time + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, pgx.Tx) (time.Time, error)); ok { + return rf(ctx, dbTx) + } + if rf, ok := ret.Get(0).(func(context.Context, pgx.Tx) time.Time); ok { + r0 = rf(ctx, dbTx) + } else { + r0 = ret.Get(0).(time.Time) + } + + if rf, ok := ret.Get(1).(func(context.Context, pgx.Tx) error); ok { + r1 = rf(ctx, dbTx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsBatchClosed provides a mock function with given fields: ctx, batchNum, dbTx +func (_m *StateMock) IsBatchClosed(ctx context.Context, batchNum uint64, dbTx pgx.Tx) (bool, error) { + ret := _m.Called(ctx, batchNum, dbTx) + + if len(ret) == 0 { + panic("no return value specified for IsBatchClosed") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, pgx.Tx) (bool, error)); ok { + return rf(ctx, batchNum, dbTx) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, pgx.Tx) bool); ok { + r0 = rf(ctx, batchNum, dbTx) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, pgx.Tx) error); ok { + r1 = rf(ctx, batchNum, dbTx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewStateMock creates a new instance of StateMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStateMock(t interface { + mock.TestingT + Cleanup(func()) +}) *StateMock { + mock := &StateMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/sequencesender/sequencesender.go b/sequencesender/sequencesender.go index c201854ed3..980d5cbf43 100644 --- a/sequencesender/sequencesender.go +++ b/sequencesender/sequencesender.go @@ -18,8 +18,10 @@ import ( ) const ( - ethTxManagerOwner = "sequencer" - monitoredIDFormat = "sequence-from-%v-to-%v" + ethTxManagerOwner = "sequencer" + monitoredIDFormat = "sequence-from-%v-to-%v" + retriesSanityCheck = 8 + waitRetrySanityCheck = 15 * time.Second ) var ( @@ -27,6 +29,10 @@ var ( // than some meaningful limit a user might use. This is not a consensus error // making the transaction invalid, rather a DOS protection. ErrOversizedData = errors.New("oversized data") + // ErrSyncVirtualGreaterSequenced is returned by the isSynced function when the last virtual batch is greater that the last SC sequenced batch + ErrSyncVirtualGreaterSequenced = errors.New("last virtual batch is greater than last SC sequenced batch") + // ErrSyncVirtualGreaterTrusted is returned by the isSynced function when the last virtual batch is greater that the last trusted batch closed + ErrSyncVirtualGreaterTrusted = errors.New("last virtual batch is greater than last trusted batch closed") ) // SequenceSender represents a sequence sender @@ -98,7 +104,11 @@ func (s *SequenceSender) tryToSendSequence(ctx context.Context) { } // Check if synchronizer is up to date - if !s.isSynced(ctx) { + synced, err := s.isSynced(ctx, retriesSanityCheck, waitRetrySanityCheck) + if err != nil { + s.halt(ctx, err) + } + if !synced { log.Info("wait virtual state to be synced...") time.Sleep(5 * time.Second) // nolint:gomnd return @@ -403,39 +413,54 @@ func isDataForEthTxTooBig(err error) bool { errors.Is(err, ethman.ErrContentLengthTooLarge) } -func (s *SequenceSender) isSynced(ctx context.Context) bool { +func (s *SequenceSender) isSynced(ctx context.Context, retries int, waitRetry time.Duration) (bool, error) { lastVirtualBatchNum, err := s.state.GetLastVirtualBatchNum(ctx, nil) if err != nil && err != state.ErrNotFound { log.Warnf("failed to get last virtual batch number, err: %v", err) - return false + return false, nil } lastTrustedBatchClosed, err := s.state.GetLastClosedBatch(ctx, nil) if err != nil && err != state.ErrNotFound { log.Warnf("failed to get last trusted batch closed, err: %v", err) - return false + return false, nil } lastSCBatchNum, err := s.etherman.GetLatestBatchNumber() if err != nil { log.Warnf("failed to get from the SC last sequenced batch number, err: %v", err) - return false + return false, nil } if lastVirtualBatchNum < lastSCBatchNum { log.Infof("waiting for the state to be synced, last virtual batch: %d, last SC sequenced batch: %d", lastVirtualBatchNum, lastSCBatchNum) - return false + return false, nil } else if lastVirtualBatchNum > lastSCBatchNum { // Sanity check: virtual batch number cannot be greater than last batch sequenced in the SC - s.halt(ctx, fmt.Errorf("last virtual batch %d is greater than last SC sequenced batch %d", lastVirtualBatchNum, lastSCBatchNum)) - return false + // we will retry some times to check that really the last sequenced batch in the SC is lower that the las virtual batch + log.Warnf("last virtual batch %d is greater than last SC sequenced batch %d, retrying...", lastVirtualBatchNum, lastSCBatchNum) + for i := 0; i < retries; i++ { + time.Sleep(waitRetry) + lastSCBatchNum, err = s.etherman.GetLatestBatchNumber() + if err != nil { + log.Warnf("failed to get from the SC last sequenced batch number, err: %v", err) + return false, nil + } + if lastVirtualBatchNum == lastSCBatchNum { // last virtual batch is equals to last sequenced batch in the SC, everything is ok we continue + break + } else if i == retries-1 { // it's the last retry, we halt sequence-sender + log.Errorf("last virtual batch %d is greater than last SC sequenced batch %d", lastVirtualBatchNum, lastSCBatchNum) + return false, ErrSyncVirtualGreaterSequenced + } + } + log.Infof("last virtual batch %d is equal to last SC sequenced batch %d, continuing...", lastVirtualBatchNum, lastSCBatchNum) } // At this point lastVirtualBatchNum = lastEthBatchNum. Check trusted batches if lastTrustedBatchClosed.BatchNumber >= lastVirtualBatchNum { - return true + return true, nil } else { // Sanity check: virtual batch number cannot be greater than last trusted batch closed - s.halt(ctx, fmt.Errorf("last virtual batch %d is greater than last trusted batch closed %d", lastVirtualBatchNum, lastTrustedBatchClosed.BatchNumber)) - return false + log.Errorf("last virtual batch %d is greater than last trusted batch closed %d", lastVirtualBatchNum, lastTrustedBatchClosed.BatchNumber) + return false, ErrSyncVirtualGreaterTrusted } } diff --git a/sequencesender/sequencesender_test.go b/sequencesender/sequencesender_test.go new file mode 100644 index 0000000000..71554eddd3 --- /dev/null +++ b/sequencesender/sequencesender_test.go @@ -0,0 +1,146 @@ +package sequencesender + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/0xPolygonHermez/zkevm-node/state" + "github.com/stretchr/testify/assert" +) + +func TestIsSynced(t *testing.T) { + const ( + retries = 3 + waitRetry = 1 * time.Second + ) + + type IsSyncedTestCase = struct { + name string + lastVirtualBatchNum uint64 + lastTrustedBatchClosed uint64 + lastSCBatchNum []uint64 + expectedResult bool + err error + } + + mockError := errors.New("error") + + stateMock := new(StateMock) + ethermanMock := new(EthermanMock) + ethTxManagerMock := new(EthTxManagerMock) + ssender, err := New(Config{}, stateMock, ethermanMock, ethTxManagerMock, nil) + assert.NoError(t, err) + + testCases := []IsSyncedTestCase{ + { + name: "is synced", + lastVirtualBatchNum: 10, + lastTrustedBatchClosed: 12, + lastSCBatchNum: []uint64{10}, + expectedResult: true, + err: nil, + }, + { + name: "not synced", + lastVirtualBatchNum: 9, + lastTrustedBatchClosed: 12, + lastSCBatchNum: []uint64{10}, + expectedResult: false, + err: nil, + }, + { + name: "error virtual > trusted", + lastVirtualBatchNum: 10, + lastTrustedBatchClosed: 9, + lastSCBatchNum: []uint64{10}, + expectedResult: false, + err: ErrSyncVirtualGreaterTrusted, + }, + { + name: "error virtual > sc sequenced", + lastVirtualBatchNum: 11, + lastTrustedBatchClosed: 12, + lastSCBatchNum: []uint64{10, 10, 10, 10}, + expectedResult: false, + err: ErrSyncVirtualGreaterSequenced, + }, + { + name: "is synced, sc sequenced retries", + lastVirtualBatchNum: 11, + lastTrustedBatchClosed: 12, + lastSCBatchNum: []uint64{10, 10, 11}, + expectedResult: true, + err: nil, + }, + { + name: "is synced, sc sequenced retries (last)", + lastVirtualBatchNum: 11, + lastTrustedBatchClosed: 12, + lastSCBatchNum: []uint64{10, 10, 10, 11}, + expectedResult: true, + err: nil, + }, + { + name: "error state.GetLastVirtualBatchNum", + lastVirtualBatchNum: 0, + lastTrustedBatchClosed: 12, + lastSCBatchNum: []uint64{0}, + expectedResult: false, + err: nil, + }, + { + name: "error state.GetLastClosedBatch", + lastVirtualBatchNum: 11, + lastTrustedBatchClosed: 0, + lastSCBatchNum: []uint64{0}, + expectedResult: false, + err: nil, + }, + { + name: "error etherman.GetLatestBatchNumber", + lastVirtualBatchNum: 11, + lastTrustedBatchClosed: 12, + lastSCBatchNum: []uint64{0}, + expectedResult: false, + err: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var returnError error + returnError = nil + + if tc.lastVirtualBatchNum == 0 { + returnError = mockError + } + stateMock.On("GetLastVirtualBatchNum", context.Background(), nil).Return(tc.lastVirtualBatchNum, returnError).Once() + + if returnError == nil { // if previous call to mock function returns error then this function will be not called inside isSynced + if tc.lastTrustedBatchClosed == 0 { + returnError = mockError + } + stateMock.On("GetLastClosedBatch", context.Background(), nil).Return(&state.Batch{BatchNumber: tc.lastTrustedBatchClosed}, returnError).Once() + } + + if returnError == nil { // if previous call to mock function returns error then this function will be not called inside isSynced + for _, num := range tc.lastSCBatchNum { + if num == 0 { // 0 means the function returns error + returnError = mockError + } + ethermanMock.On("GetLatestBatchNumber").Return(num, returnError).Once() + } + } + + synced, err := ssender.isSynced(context.Background(), retries, waitRetry) + + assert.EqualValues(t, tc.expectedResult, synced) + assert.EqualValues(t, tc.err, err) + + ethermanMock.AssertExpectations(t) + stateMock.AssertExpectations(t) + }) + } +}