Skip to content
9 changes: 8 additions & 1 deletion block/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -1210,11 +1210,18 @@ func (m *Manager) publishBlock(ctx context.Context) error {

batchData, err := m.retrieveBatch(ctx)
if errors.Is(err, ErrNoBatch) {
return fmt.Errorf("no batch to process")
m.logger.Info("No batch retrieved from sequencer, skipping block production")
return nil // Indicate no block was produced
} else if err != nil {
return fmt.Errorf("failed to get transactions from batch: %w", err)
}

// Check if the batch contains any transactions
if len(batchData.Batch.Transactions) == 0 {
Comment thread
tac0turtle marked this conversation as resolved.
Resolved
m.logger.Info("Retrieved batch contains no transactions, skipping block production")
return nil // Indicate no block was produced
}

// sanity check timestamp for monotonically increasing
if batchData.Time.Before(lastHeaderTime) {
return fmt.Errorf("timestamp is not monotonically increasing: %s < %s", batchData.Time, m.getLastBlockTime())
Expand Down
192 changes: 192 additions & 0 deletions block/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

coreda "github.com/rollkit/rollkit/core/da"
coreexecutor "github.com/rollkit/rollkit/core/execution"
coresequencer "github.com/rollkit/rollkit/core/sequencer"
"github.com/rollkit/rollkit/da"
damocks "github.com/rollkit/rollkit/da/mocks"
"github.com/rollkit/rollkit/pkg/cache"
Expand All @@ -30,6 +31,35 @@ import (
"github.com/rollkit/rollkit/types"
)

// mockSequencer is a local mock implementation for coresequencer.Sequencer
type mockSequencer struct {
mock.Mock
}

func (m *mockSequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextBatchRequest) (*coresequencer.GetNextBatchResponse, error) {
args := m.Called(ctx, req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*coresequencer.GetNextBatchResponse), args.Error(1)
}

func (m *mockSequencer) SubmitRollupBatchTxs(ctx context.Context, req coresequencer.SubmitRollupBatchTxsRequest) (*coresequencer.SubmitRollupBatchTxsResponse, error) {
args := m.Called(ctx, req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*coresequencer.SubmitRollupBatchTxsResponse), args.Error(1)
}

func (m *mockSequencer) VerifyBatch(ctx context.Context, req coresequencer.VerifyBatchRequest) (*coresequencer.VerifyBatchResponse, error) {
args := m.Called(ctx, req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*coresequencer.VerifyBatchResponse), args.Error(1)
}

// WithinDuration asserts that the two durations are within the specified tolerance of each other.
func WithinDuration(t *testing.T, expected, actual, tolerance time.Duration) bool {
diff := expected - actual
Expand Down Expand Up @@ -410,6 +440,168 @@ func Test_publishBlock_ManagerNotProposer(t *testing.T) {
require.ErrorIs(err, ErrNotProposer)
}

func Test_publishBlock_NoBatch(t *testing.T) {
require := require.New(t)
ctx := context.Background()

// Setup manager with mocks
mockStore := mocks.NewStore(t)
mockSeq := new(mockSequencer)
mockExecutor := coreexecutor.NewDummyExecutor()
logger := log.NewTestLogger(t)
chainID := "Test_publishBlock_NoBatch"
genesisData, privKey, _ := types.GetGenesisWithPrivkey(chainID)
noopSigner, err := noopsigner.NewNoopSigner(privKey)
require.NoError(err)

m := &Manager{
store: mockStore,
sequencer: mockSeq,
exec: mockExecutor,
logger: logger,
isProposer: true,
proposerKey: noopSigner,
genesis: genesisData,
config: config.Config{
Node: config.NodeConfig{
MaxPendingBlocks: 0,
},
},
pendingHeaders: &PendingHeaders{
store: mockStore,
logger: logger,
},
lastStateMtx: &sync.RWMutex{},
metrics: NopMetrics(),
}

mockStore.On("GetMetadata", ctx, LastSubmittedHeightKey).Return([]byte{0}, nil)
err = m.pendingHeaders.init()
require.NoError(err)

// Mock store calls for height and previous block/commit
currentHeight := uint64(1)
mockStore.On("Height", ctx).Return(currentHeight, nil)
mockSignature := types.Signature([]byte{1, 2, 3})
mockStore.On("GetSignature", ctx, currentHeight).Return(&mockSignature, nil)
// Use GetRandomBlock which returns header and data
lastHeader, lastData := types.GetRandomBlock(currentHeight, 0, chainID)
mockStore.On("GetBlockData", ctx, currentHeight).Return(lastHeader, lastData, nil)
// Mock GetBlockData for newHeight to indicate no pending block exists
mockStore.On("GetBlockData", ctx, currentHeight+1).Return(nil, nil, errors.New("not found"))

// No need to mock GetTxs on the dummy executor if its default behavior is acceptable

// Mock sequencer SubmitRollupBatchTxs (should still be called)
submitReqMatcher := mock.MatchedBy(func(req coresequencer.SubmitRollupBatchTxsRequest) bool {
return string(req.RollupId) == chainID // Ensure correct rollup ID
})
mockSeq.On("SubmitRollupBatchTxs", ctx, submitReqMatcher).Return(&coresequencer.SubmitRollupBatchTxsResponse{}, nil)

// *** Crucial Mock: Sequencer returns ErrNoBatch ***
batchReqMatcher := mock.MatchedBy(func(req coresequencer.GetNextBatchRequest) bool {
return string(req.RollupId) == chainID // Ensure correct rollup ID
})
mockSeq.On("GetNextBatch", ctx, batchReqMatcher).Return(nil, ErrNoBatch)

// Call publishBlock
err = m.publishBlock(ctx)

// Assertions
require.NoError(err, "publishBlock should return nil error when no batch is available")

// Verify mocks: Ensure methods after the check were NOT called
mockStore.AssertNotCalled(t, "SaveBlockData", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
// We can't directly assert non-calls on m.createBlock or m.applyBlock easily without more complex setup,
// but returning nil error and not calling SaveBlockData implies they weren't reached.
mockSeq.AssertExpectations(t) // Ensure GetNextBatch was called
mockStore.AssertExpectations(t)
}

func Test_publishBlock_EmptyBatch(t *testing.T) {
require := require.New(t)
ctx := context.Background()

// Setup manager with mocks
mockStore := mocks.NewStore(t)
mockSeq := new(mockSequencer) // Use the local mock struct
mockExecutor := coreexecutor.NewDummyExecutor()
logger := log.NewTestLogger(t)
chainID := "Test_publishBlock_EmptyBatch"
genesisData, privKey, _ := types.GetGenesisWithPrivkey(chainID)
noopSigner, err := noopsigner.NewNoopSigner(privKey)
require.NoError(err)

m := &Manager{
store: mockStore,
sequencer: mockSeq, // Assign the local mock
exec: mockExecutor,
logger: logger,
isProposer: true,
proposerKey: noopSigner,
genesis: genesisData,
config: config.Config{
Node: config.NodeConfig{
MaxPendingBlocks: 0, // No limit
},
},
pendingHeaders: &PendingHeaders{ // Basic pending headers mock
store: mockStore,
logger: logger,
},
lastStateMtx: &sync.RWMutex{},
metrics: NopMetrics(),
}

mockStore.On("GetMetadata", ctx, LastSubmittedHeightKey).Return([]byte{0}, nil)
err = m.pendingHeaders.init()
require.NoError(err)

// Mock store calls
currentHeight := uint64(1)
mockStore.On("Height", ctx).Return(currentHeight, nil)
mockSignature := types.Signature([]byte{1, 2, 3})
mockStore.On("GetSignature", ctx, currentHeight).Return(&mockSignature, nil)
// Use GetRandomBlock
lastHeader, lastData := types.GetRandomBlock(currentHeight, 0, chainID)
mockStore.On("GetBlockData", ctx, currentHeight).Return(lastHeader, lastData, nil)
mockStore.On("GetBlockData", ctx, currentHeight+1).Return(nil, nil, errors.New("not found")) // No pending block

// No need to mock GetTxs on the dummy executor

// Mock sequencer SubmitRollupBatchTxs
submitReqMatcher := mock.MatchedBy(func(req coresequencer.SubmitRollupBatchTxsRequest) bool {
return string(req.RollupId) == chainID
})
mockSeq.On("SubmitRollupBatchTxs", ctx, submitReqMatcher).Return(&coresequencer.SubmitRollupBatchTxsResponse{}, nil)

// *** Crucial Mock: Sequencer returns an empty batch ***
emptyBatchResponse := &coresequencer.GetNextBatchResponse{
Batch: &coresequencer.Batch{
Transactions: [][]byte{}, // Empty transactions
},
Timestamp: time.Now(),
BatchData: [][]byte{[]byte("some_batch_data")}, // Sequencer might return metadata even if empty
}
batchReqMatcher := mock.MatchedBy(func(req coresequencer.GetNextBatchRequest) bool {
return string(req.RollupId) == chainID
})
mockSeq.On("GetNextBatch", ctx, batchReqMatcher).Return(emptyBatchResponse, nil)
// Mock store SetMetadata for the last batch data
mockStore.On("SetMetadata", ctx, LastBatchDataKey, mock.Anything).Return(nil)

// Call publishBlock
err = m.publishBlock(ctx)

// Assertions
require.NoError(err, "publishBlock should return nil error when the batch is empty")

// Verify mocks: Ensure methods after the check were NOT called
mockStore.AssertNotCalled(t, "SaveBlockData", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
mockSeq.AssertExpectations(t)
mockStore.AssertExpectations(t)
}

// TestAggregationLoop tests the AggregationLoop function
func TestAggregationLoop(t *testing.T) {
mockStore := new(mocks.Store)
Expand Down
2 changes: 1 addition & 1 deletion da/cmd/local-da/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func main() {
da := NewLocalDA(logger)

srv := proxy.NewServer(host, port, da)
logger.Info("Listening on: %s:%s", host, port)
logger.Info("Listening on", host, port)
if err := srv.Start(context.Background()); err != nil {
logger.Info("error while serving:", err)
}
Expand Down
11 changes: 9 additions & 2 deletions sequencers/single/sequencer.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ func (c *Sequencer) SubmitRollupBatchTxs(ctx context.Context, req coresequencer.
return nil, ErrInvalidRollupId
}

if req.Batch == nil || len(req.Batch.Transactions) == 0 {
c.logger.Info("Skipping submission of empty batch", "rollupId", string(req.RollupId))
// Return success without processing the empty batch
return &coresequencer.SubmitRollupBatchTxsResponse{}, nil
}

batch := coresequencer.Batch{Transactions: req.Batch.Transactions}

// First try to send to DA submission channel
Expand Down Expand Up @@ -193,6 +199,7 @@ daSubmitRetryLoop:

// Attempt to submit the batch to the DA layer
res := c.dalc.Submit(ctx, currentBatch.Transactions, maxBlobSize, gasPrice)
fmt.Println(res, "res", "therere")
Comment thread
tac0turtle marked this conversation as resolved.
Resolved

gasMultiplier, err := c.dalc.GasMultiplier(ctx)
if err != nil {
Expand Down Expand Up @@ -235,7 +242,7 @@ daSubmitRetryLoop:

case coreda.StatusNotIncludedInBlock, coreda.StatusAlreadyInMempool:
// For mempool-related issues, use a longer backoff and increase gas price
c.logger.Error("DA layer submission failed", "error", res.Message, "attempt", attempt)
c.logger.Error("single sequcner: DA layer submission failed", "error", res.Message, "attempt", attempt)
backoff = c.batchTime * time.Duration(defaultMempoolTTL)

// Increase gas price to prioritize the transaction
Expand All @@ -252,7 +259,7 @@ daSubmitRetryLoop:

default:
// For other errors, use exponential backoff
c.logger.Error("DA layer submission failed", "error", res.Message, "attempt", attempt)
c.logger.Error("single sequcner: DA layer submission failed", "error", res.Message, "attempt", attempt)
backoff = c.exponentialBackoff(backoff)
}

Expand Down
44 changes: 44 additions & 0 deletions sequencers/single/sequencer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,50 @@ func TestSequencer_SubmitRollupBatchTxs(t *testing.T) {
}
}

func TestSequencer_SubmitRollupBatchTxs_EmptyBatch(t *testing.T) {
// Initialize a new sequencer
metrics, _ := NopMetrics()
dummyDA := coreda.NewDummyDA(100_000_000, 0, 0)
db := ds.NewMapDatastore()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rollupId := []byte("rollup1")
seq, err := NewSequencer(ctx, log.NewNopLogger(), db, dummyDA, []byte("namespace"), rollupId, 10*time.Second, metrics, false)
require.NoError(t, err, "Failed to create sequencer")
defer func() {
err := db.Close()
require.NoError(t, err, "Failed to close sequencer")
}()

// Test submitting an empty batch
res, err := seq.SubmitRollupBatchTxs(context.Background(), coresequencer.SubmitRollupBatchTxsRequest{
RollupId: rollupId,
Batch: &coresequencer.Batch{Transactions: [][]byte{}}, // Empty transactions
})
require.NoError(t, err, "Submitting empty batch should not return an error")
require.NotNil(t, res, "Response should not be nil for empty batch submission")

// Verify that no batch was added to the queue
nextBatchResp, err := seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{RollupId: rollupId})
require.NoError(t, err, "Getting next batch after empty submission failed")
require.NotNil(t, nextBatchResp, "GetNextBatch response should not be nil")
require.Empty(t, nextBatchResp.Batch.Transactions, "Queue should be empty after submitting an empty batch")

// Test submitting a nil batch
res, err = seq.SubmitRollupBatchTxs(context.Background(), coresequencer.SubmitRollupBatchTxsRequest{
RollupId: rollupId,
Batch: nil, // Nil batch
})
require.NoError(t, err, "Submitting nil batch should not return an error")
require.NotNil(t, res, "Response should not be nil for nil batch submission")

// Verify again that no batch was added to the queue
nextBatchResp, err = seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{RollupId: rollupId})
require.NoError(t, err, "Getting next batch after nil submission failed")
require.NotNil(t, nextBatchResp, "GetNextBatch response should not be nil")
require.Empty(t, nextBatchResp.Batch.Transactions, "Queue should still be empty after submitting a nil batch")
}

func TestSequencer_GetNextBatch_NoLastBatch(t *testing.T) {
db := ds.NewMapDatastore()

Expand Down