Repository: rollkit/rollkit
Pull request #2224: feat: adr & Impl: lazy aggregation
Author: login: tac0turtle
Created: 2025-04-29T19:19:24Z | Merged: 2025-05-05T12:43:36Z

<!--
Please read and fill out this form before submitting your PR.

Please make sure you have reviewed our contributors guide before submitting your
first PR.

NOTE: PR titles should follow semantic commits: https://www.conventionalcommits.org/en/v1.0.0/
-->

## Overview

This ADR proposes a solution for maintaining consistency between DA layer blocks and execution layer blocks during periods of inactivity in Rollkit's lazy aggregation mechanism. 

Currently, empty blocks are skipped on the DA layer, breaking the 1:1 block mapping expected by EVM environments. The proposed solution introduces a minimal sentinel transaction for empty blocks, ensuring consistent block heights while minimizing DA costs.

closes https://github.com/rollkit/rollkit/issues/2193

TL;DR:
- Introduces sentinel markers for empty blocks on DA layer
- Maintains block height consistency for EVM compatibility
- Optimizes DA space usage with minimal 3-byte markers
- Preserves existing lazy aggregation performance


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Documentation**
  - Added a new architecture decision record (ADR) outlining the approach for handling empty blocks to ensure consistent block height mapping between Rollkit and the Data Availability layer. The document details the mechanism for signaling empty blocks, maintaining timing information, and ensuring reliable syncing.
- **New Features**
  - Introduced lazy aggregation mode with dual timers to produce blocks regularly during inactivity and promptly when transactions arrive.
  - Enabled creation and publishing of empty blocks to maintain consistent block timing and height mapping.
  - Added transaction notification signaling to trigger timely block production.
- **Bug Fixes**
  - Improved block production logic to preserve proper timing and deterministic block creation even when no transactions are present.
- **Tests**
  - Added comprehensive unit tests covering lazy aggregation behavior, timer interactions, empty block creation, and transaction notification handling.
- **Chores**
  - Renamed configuration flags and fields for lazy mode and block interval for clarity and consistency.
  - Linked transaction reaper component with block manager to notify on new transactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/block/aggregation.go b/block/aggregation.go
index 29dcd86610..29e2dd58d8 100644
--- a/block/aggregation.go
+++ b/block/aggregation.go
@@ -36,7 +36,7 @@ func (m *Manager) AggregationLoop(ctx context.Context) {
 	// Lazy Aggregator mode.
 	// In Lazy Aggregator mode, blocks are built only when there are
 	// transactions or every LazyBlockTime.
-	if m.config.Node.LazyAggregator {
+	if m.config.Node.LazyMode {
 		m.lazyAggregationLoop(ctx, blockTimer)
 		return
 	}
@@ -55,21 +55,38 @@ func (m *Manager) lazyAggregationLoop(ctx context.Context, blockTimer *time.Time
 			return
 
 		case <-lazyTimer.C:
+			m.logger.Debug("Lazy timer triggered block production")
+			m.produceBlock(ctx, "lazy_timer", lazyTimer, blockTimer)
+
 		case <-blockTimer.C:
+			if m.txsAvailable {
+				m.produceBlock(ctx, "block_timer", lazyTimer, blockTimer)
+				m.txsAvailable = false
+			} else {
+				// Ensure we keep ticking even when there are no txs
+				blockTimer.Reset(m.config.Node.BlockTime.Duration)
+			}
+		case <-m.txNotifyCh:
+			m.txsAvailable = true
 		}
+	}
+}
 
-		// Reset the start time
-		start := time.Now()
+// produceBlock handles the common logic for producing a block and resetting timers
+func (m *Manager) produceBlock(ctx context.Context, mode string, lazyTimer, blockTimer *time.Timer) {
+	// Record the start time
+	start := time.Now()
 
-		// Attempt to publish the block regardless of activity
-		if err := m.publishBlock(ctx); err != nil && ctx.Err() == nil {
-			m.logger.Error("error while publishing block", "error", err)
-		}
-
-		// Reset both timers for the next aggregation window
-		lazyTimer.Reset(getRemainingSleep(start, m.config.Node.LazyBlockTime.Duration))
-		blockTimer.Reset(getRemainingSleep(start, m.config.Node.BlockTime.Duration))
+	// Attempt to publish the block
+	if err := m.publishBlock(ctx); err != nil && ctx.Err() == nil {
+		m.logger.Error("error while publishing block", "mode", mode, "error", err)
+	} else {
+		m.logger.Debug("Successfully published block", "mode", mode)
 	}
+
+	// Reset both timers for the next aggregation window
+	lazyTimer.Reset(getRemainingSleep(start, m.config.Node.LazyBlockInterval.Duration))
+	blockTimer.Reset(getRemainingSleep(start, m.config.Node.BlockTime.Duration))
 }
 
 func (m *Manager) normalAggregationLoop(ctx context.Context, blockTimer *time.Timer) {
@@ -87,6 +104,12 @@ func (m *Manager) normalAggregationLoop(ctx context.Context, blockTimer *time.Ti
 			// Reset the blockTimer to signal the next block production
 			// period based on the block time.
 			blockTimer.Reset(getRemainingSleep(start, m.config.Node.BlockTime.Duration))
+
+		case <-m.txNotifyCh:
+			// Transaction notifications are intentionally ignored in normal mode
+			// to avoid triggering block production outside the scheduled intervals.
+			// We just update the txsAvailable flag for tracking purposes
+			m.txsAvailable = true
 		}
 	}
 }
diff --git a/block/aggregation_test.go b/block/aggregation_test.go
index f16872cf29..fe512a4da8 100644
--- a/block/aggregation_test.go
+++ b/block/aggregation_test.go
@@ -45,8 +45,8 @@ func TestAggregationLoop_Normal_BasicInterval(t *testing.T) {
 		logger:    logger,
 		config: config.Config{
 			Node: config.NodeConfig{
-				BlockTime:      config.DurationWrapper{Duration: blockTime},
-				LazyAggregator: false,
+				BlockTime: config.DurationWrapper{Duration: blockTime},
+				LazyMode:  false,
 			},
 			DA: config.DAConfig{
 				BlockTime: config.DurationWrapper{Duration: 1 * time.Second},
@@ -142,8 +142,8 @@ func TestAggregationLoop_Normal_PublishBlockError(t *testing.T) {
 		logger:    mockLogger,
 		config: config.Config{
 			Node: config.NodeConfig{
-				BlockTime:      config.DurationWrapper{Duration: blockTime},
-				LazyAggregator: false,
+				BlockTime: config.DurationWrapper{Duration: blockTime},
+				LazyMode:  false,
 			},
 			DA: config.DAConfig{
 				BlockTime: config.DurationWrapper{Duration: 1 * time.Second},
diff --git a/block/lazy_aggregation_test.go b/block/lazy_aggregation_test.go
new file mode 100644
index 0000000000..3c2ff77985
--- /dev/null
+++ b/block/lazy_aggregation_test.go
@@ -0,0 +1,403 @@
+package block
+
+import (
+	"context"
+	"errors"
+	"sync"
+	"testing"
+	"time"
+
+	"cosmossdk.io/log"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/rollkit/rollkit/pkg/config"
+)
+
+// mockPublishBlock is used to control the behavior of publishBlock during tests
+type mockPublishBlock struct {
+	mu    sync.Mutex
+	calls chan struct{}
+	err   error
+	delay time.Duration // Optional delay to simulate processing time
+}
+
+// reset clears the calls channel in mockPublishBlock.
+func (m *mockPublishBlock) reset() {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	// Clear the channel
+	for len(m.calls) > 0 {
+		<-m.calls
+	}
+}
+
+func (m *mockPublishBlock) publish(ctx context.Context) error {
+	m.mu.Lock()
+	err := m.err
+	delay := m.delay
+	m.mu.Unlock()
+
+	if delay > 0 {
+		time.Sleep(delay)
+	}
+	// Non-blocking send in case the channel buffer is full or receiver is not ready
+	select {
+	case m.calls <- struct{}{}:
+	default:
+	}
+	return err
+}
+
+func setupTestManager(t *testing.T, blockTime, lazyTime time.Duration) (*Manager, *mockPublishBlock) {
+	t.Helper()
+	pubMock := &mockPublishBlock{
+		calls: make(chan struct{}, 10), // Buffer to avoid blocking in tests
+	}
+	logger := log.NewTestLogger(t)
+	m := &Manager{
+		logger: logger,
+		config: config.Config{
+			Node: config.NodeConfig{
+				BlockTime:         config.DurationWrapper{Duration: blockTime},
+				LazyBlockInterval: config.DurationWrapper{Duration: lazyTime},
+				LazyMode:          true, // Ensure lazy mode is active
+			},
+		},
+		publishBlock: pubMock.publish,
+	}
+	return m, pubMock
+}
+
+// TestLazyAggregationLoop_BlockTimerTrigger tests that a block is published when the blockTimer fires first.
+func TestLazyAggregationLoop_BlockTimerTrigger(t *testing.T) {
+	require := require.New(t)
+
+	// Create a mock for the publishBlock function that counts calls
+	callCount := 0
+	mockPublishFn := func(ctx context.Context) error {
+		callCount++
+		return nil
+	}
+
+	// Setup a manager with our mock publish function
+	blockTime := 50 * time.Millisecond
+	lazyTime := 200 * time.Millisecond // Lazy timer fires later
+	m, _ := setupTestManager(t, blockTime, lazyTime)
+	m.publishBlock = mockPublishFn
+
+	// Set txsAvailable to true to ensure block timer triggers block production
+	m.txsAvailable = true
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	// Start the lazy aggregation loop
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		blockTimer := time.NewTimer(0) // Fire immediately first time
+		defer blockTimer.Stop()
+		m.lazyAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for at least one block to be published
+	time.Sleep(blockTime * 2)
+
+	// Cancel the context to stop the loop
+	cancel()
+	wg.Wait()
+
+	// Verify that at least one block was published
+	require.GreaterOrEqual(callCount, 1, "Expected at least one block to be published")
+}
+
+// TestLazyAggregationLoop_LazyTimerTrigger tests that a block is published when the lazyTimer fires first.
+func TestLazyAggregationLoop_LazyTimerTrigger(t *testing.T) {
+	assert := assert.New(t)
+	require := require.New(t)
+
+	blockTime := 200 * time.Millisecond // Block timer fires later
+	lazyTime := 50 * time.Millisecond
+	m, pubMock := setupTestManager(t, blockTime, lazyTime)
+
+	// Set txsAvailable to false to ensure lazy timer triggers block production
+	// and block timer doesn't
+	m.txsAvailable = false
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		// Use real timers for this test
+		blockTimer := time.NewTimer(0) // Fire immediately first time
+		defer blockTimer.Stop()
+		m.lazyAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for the first publish call triggered by the initial immediate lazyTimer fire
+	select {
+	case <-pubMock.calls:
+		// Good, first block published by lazy timer
+	case <-time.After(2 * lazyTime): // Give some buffer
+		require.Fail("timed out waiting for first block publication")
+	}
+
+	// Wait for the second publish call, triggered by lazyTimer reset
+	select {
+	case <-pubMock.calls:
+		// Good, second block published by lazyTimer
+	case <-time.After(2 * lazyTime): // Give some buffer
+		require.Fail("timed out waiting for second block publication (lazyTimer)")
+	}
+
+	// Ensure blockTimer didn't trigger a publish yet (since txsAvailable is false)
+	assert.Len(pubMock.calls, 0, "Expected no more publish calls yet")
+
+	cancel()
+	wg.Wait()
+}
+
+// TestLazyAggregationLoop_PublishError tests that the loop continues after a publish error.
+func TestLazyAggregationLoop_PublishError(t *testing.T) {
+	require := require.New(t)
+
+	blockTime := 50 * time.Millisecond
+	lazyTime := 100 * time.Millisecond
+	m, pubMock := setupTestManager(t, blockTime, lazyTime)
+
+	pubMock.mu.Lock()
+	pubMock.err = errors.New("publish failed")
+	pubMock.mu.Unlock()
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		// Use real timers
+		blockTimer := time.NewTimer(0)
+		defer blockTimer.Stop()
+		m.lazyAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for the first publish attempt (which will fail)
+	select {
+	case <-pubMock.calls:
+	case <-time.After(2 * blockTime):
+		require.Fail("timed out waiting for first block publication attempt")
+	}
+
+	// Remove the error for subsequent calls
+	pubMock.mu.Lock()
+	pubMock.err = nil
+	pubMock.mu.Unlock()
+
+	// Wait for the second publish attempt (should succeed)
+	// Use a longer timeout since we need to wait for either the lazy timer or block timer to fire
+	select {
+	case <-pubMock.calls:
+	case <-time.After(2 * lazyTime): // Use the longer of the two timers with some buffer
+		require.Fail("timed out waiting for second block publication attempt after error")
+	}
+
+	cancel()
+	wg.Wait()
+}
+
+// TestGetRemainingSleep tests the calculation of sleep duration.
+func TestGetRemainingSleep(t *testing.T) {
+	assert := assert.New(t)
+
+	interval := 100 * time.Millisecond
+
+	// Case 1: Elapsed time is less than interval
+	start1 := time.Now().Add(-30 * time.Millisecond) // Started 30ms ago
+	sleep1 := getRemainingSleep(start1, interval)
+	// Expecting interval - elapsed = 100ms - 30ms = 70ms (allow for slight variations)
+	assert.InDelta(interval-30*time.Millisecond, sleep1, float64(5*time.Millisecond), "Case 1 failed")
+
+	// Case 2: Elapsed time is greater than or equal to interval
+	start2 := time.Now().Add(-120 * time.Millisecond) // Started 120ms ago
+	sleep2 := getRemainingSleep(start2, interval)
+	// Expecting minimum sleep time
+	assert.Equal(time.Millisecond, sleep2, "Case 2 failed")
+
+	// Case 3: Elapsed time is exactly the interval
+	start3 := time.Now().Add(-100 * time.Millisecond) // Started 100ms ago
+	sleep3 := getRemainingSleep(start3, interval)
+	// Expecting minimum sleep time
+	assert.Equal(time.Millisecond, sleep3, "Case 3 failed")
+
+	// Case 4: Zero elapsed time
+	start4 := time.Now()
+	sleep4 := getRemainingSleep(start4, interval)
+	assert.InDelta(interval, sleep4, float64(5*time.Millisecond), "Case 4 failed")
+}
+
+// TestLazyAggregationLoop_TxNotification tests that transaction notifications trigger block production in lazy mode
+func TestLazyAggregationLoop_TxNotification(t *testing.T) {
+	require := require.New(t)
+
+	blockTime := 200 * time.Millisecond
+	lazyTime := 500 * time.Millisecond
+	m, pubMock := setupTestManager(t, blockTime, lazyTime)
+	m.config.Node.LazyMode = true
+
+	// Create the notification channel
+	m.txNotifyCh = make(chan struct{}, 1)
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		// Start with a timer that won't fire immediately
+		blockTimer := time.NewTimer(blockTime)
+		defer blockTimer.Stop()
+		m.lazyAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for the initial lazy timer to fire and publish a block
+	select {
+	case <-pubMock.calls:
+		// Initial block was published by lazy timer
+	case <-time.After(100 * time.Millisecond):
+		require.Fail("Initial block was not published")
+	}
+
+	// Reset the mock to track new calls
+	pubMock.reset()
+
+	// Wait a bit to ensure the loop is running with reset timers
+	time.Sleep(20 * time.Millisecond)
+
+	// Send a transaction notification
+	m.NotifyNewTransactions()
+
+	// Wait for the block timer to fire and check txsAvailable
+	select {
+	case <-pubMock.calls:
+		// Block was published, which is what we expect
+	case <-time.After(blockTime + 50*time.Millisecond):
+		require.Fail("Block was not published after transaction notification")
+	}
+
+	// Reset the mock again
+	pubMock.reset()
+
+	// Send another notification immediately
+	m.NotifyNewTransactions()
+
+	// Wait for the next block timer to fire
+	select {
+	case <-pubMock.calls:
+		// Block was published after notification
+	case <-time.After(blockTime + 50*time.Millisecond):
+		require.Fail("Block was not published after second notification")
+	}
+
+	cancel()
+	wg.Wait()
+}
+
+// TestEmptyBlockCreation tests that empty blocks are created with the correct dataHash
+func TestEmptyBlockCreation(t *testing.T) {
+	require := require.New(t)
+
+	// Create a mock for the publishBlock function that captures the context
+	var capturedCtx context.Context
+	mockPublishFn := func(ctx context.Context) error {
+		capturedCtx = ctx
+		return nil
+	}
+
+	// Setup a manager with our mock publish function
+	blockTime := 50 * time.Millisecond
+	lazyTime := 100 * time.Millisecond
+	m, _ := setupTestManager(t, blockTime, lazyTime)
+	m.publishBlock = mockPublishFn
+
+	// Create a context we can cancel
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	// Create timers for the test
+	lazyTimer := time.NewTimer(lazyTime)
+	blockTimer := time.NewTimer(blockTime)
+	defer lazyTimer.Stop()
+	defer blockTimer.Stop()
+
+	// Call produceBlock directly to test empty block creation
+	m.produceBlock(ctx, "test_trigger", lazyTimer, blockTimer)
+
+	// Verify that the context was passed correctly
+	require.NotNil(capturedCtx, "Context should have been captured by mock publish function")
+	require.Equal(ctx, capturedCtx, "Context should match the one passed to produceBlock")
+}
+
+// TestNormalAggregationLoop_TxNotification tests that transaction notifications are handled in normal mode
+func TestNormalAggregationLoop_TxNotification(t *testing.T) {
+	require := require.New(t)
+
+	blockTime := 100 * time.Millisecond
+	m, pubMock := setupTestManager(t, blockTime, 0)
+	m.config.Node.LazyMode = false
+
+	// Create the notification channel
+	m.txNotifyCh = make(chan struct{}, 1)
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		blockTimer := time.NewTimer(blockTime)
+		defer blockTimer.Stop()
+		m.normalAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for the first block to be published by the timer
+	select {
+	case <-pubMock.calls:
+		// Block was published by timer, which is expected
+	case <-time.After(blockTime * 2):
+		require.Fail("Block was not published by timer")
+	}
+
+	// Reset the publish mock to track new calls
+	pubMock.reset()
+
+	// Send a transaction notification
+	m.NotifyNewTransactions()
+
+	// In normal mode, the notification should not trigger an immediate block
+	select {
+	case <-pubMock.calls:
+		// If we enable the optional enhancement to reset the timer, this might happen
+		// But with the current implementation, this should not happen
+		require.Fail("Block was published immediately after notification in normal mode")
+	case <-time.After(blockTime / 2):
+		// This is expected - no immediate block
+	}
+
+	// Wait for the next regular block
+	select {
+	case <-pubMock.calls:
+		// Block was published by timer, which is expected
+	case <-time.After(blockTime * 2):
+		require.Fail("Block was not published by timer after notification")
+	}
+
+	cancel()
+	wg.Wait()
+}
diff --git a/block/manager.go b/block/manager.go
index 52422a39ef..5fcc6ee0bc 100644
--- a/block/manager.go
+++ b/block/manager.go
@@ -126,7 +126,7 @@ type Manager struct {
 	logger log.Logger
 
 	// For usage by Lazy Aggregator mode
-	buildingBlock bool
+	txsAvailable bool
 
 	pendingHeaders *PendingHeaders
 
@@ -148,6 +148,9 @@ type Manager struct {
 	// publishBlock is the function used to publish blocks. It defaults to
 	// the manager's publishBlock method but can be overridden for testing.
 	publishBlock publishBlockFunc
+
+	// txNotifyCh is used to signal when new transactions are available
+	txNotifyCh chan struct{}
 }
 
 // getInitialState tries to load lastState from Store, and if it's not available it reads genesis.
@@ -281,9 +284,9 @@ func NewManager(
 		config.Node.BlockTime.Duration = defaultBlockTime
 	}
 
-	if config.Node.LazyBlockTime.Duration == 0 {
+	if config.Node.LazyBlockInterval.Duration == 0 {
 		logger.Info("Using default lazy block time", "LazyBlockTime", defaultLazyBlockTime)
-		config.Node.LazyBlockTime.Duration = defaultLazyBlockTime
+		config.Node.LazyBlockInterval.Duration = defaultLazyBlockTime
 	}
 
 	if config.DA.MempoolTTL == 0 {
@@ -332,7 +335,7 @@ func NewManager(
 		dataCache:      cache.NewCache[types.Data](),
 		retrieveCh:     make(chan struct{}, 1),
 		logger:         logger,
-		buildingBlock:  false,
+		txsAvailable:   false,
 		pendingHeaders: pendingHeaders,
 		metrics:        seqMetrics,
 		sequencer:      sequencer,
@@ -340,6 +343,7 @@ func NewManager(
 		da:             da,
 		gasPrice:       gasPrice,
 		gasMultiplier:  gasMultiplier,
+		txNotifyCh:     make(chan struct{}, 1), // Non-blocking channel
 	}
 	agg.init(ctx)
 	// Set the default publishBlock implementation
@@ -445,15 +449,19 @@ func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
 			"txCount", len(res.Batch.Transactions),
 			"timestamp", res.Timestamp)
 
+		var errRetrieveBatch error
+		// Even if there are no transactions, return the batch with timestamp
+		// This allows empty blocks to maintain proper timing
 		if len(res.Batch.Transactions) == 0 {
-			return nil, ErrNoBatch
+			errRetrieveBatch = ErrNoBatch
 		}
-		h := convertBatchDataToBytes(res.BatchData)
-		if err := m.store.SetMetadata(ctx, LastBatchDataKey, h); err != nil {
+		// Even if there are no transactions, update lastBatchData so we don’t
+		// repeatedly emit the same empty batch, and persist it to metadata.
+		if err := m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData)); err != nil {
 			m.logger.Error("error while setting last batch hash", "error", err)
 		}
 		m.lastBatchData = res.BatchData
-		return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, nil
+		return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, errRetrieveBatch
 	}
 	return nil, ErrNoBatch
 }
@@ -483,10 +491,12 @@ func (m *Manager) publishBlockInternal(ctx context.Context) error {
 		lastHeaderTime time.Time
 		err            error
 	)
+
 	height, err := m.store.Height(ctx)
 	if err != nil {
 		return fmt.Errorf("error while getting store height: %w", err)
 	}
+
 	newHeight := height + 1
 	// this is a special case, when first block is produced - there is no previous commit
 	if newHeight <= m.genesis.InitialHeight {
@@ -521,26 +531,30 @@ func (m *Manager) publishBlockInternal(ctx context.Context) error {
 		data = pendingData
 	} else {
 		batchData, err := m.retrieveBatch(ctx)
-		if errors.Is(err, ErrNoBatch) {
-			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)
+		if err != nil {
+			if errors.Is(err, ErrNoBatch) {
+				if batchData == nil {
+					m.logger.Info("No batch retrieved from sequencer, skipping block production")
+					return nil
+				}
+				m.logger.Info("Creating empty block", "height", newHeight)
+			} else {
+				return fmt.Errorf("failed to get transactions from batch: %w", err)
+			}
+		} else {
+			if batchData.Before(lastHeaderTime) {
+				return fmt.Errorf("timestamp is not monotonically increasing: %s < %s", batchData.Time, m.getLastBlockTime())
+			}
+			m.logger.Info("Creating and publishing block", "height", newHeight)
+			m.logger.Debug("block info", "num_tx", len(batchData.Transactions))
 		}
 
-		// sanity check timestamp for monotonically increasing
-		if batchData.Before(lastHeaderTime) {
-			return fmt.Errorf("timestamp is not monotonically increasing: %s < %s", batchData.Time, m.getLastBlockTime())
-		}
-		m.logger.Info("Creating and publishing block", "height", newHeight)
 		header, data, err = m.createBlock(ctx, newHeight, lastSignature, lastHeaderHash, batchData)
 		if err != nil {
 			return err
 		}
-		m.logger.Debug("block info", "num_tx", len(data.Txs))
 
-		err = m.store.SaveBlockData(ctx, header, data, &signature)
-		if err != nil {
+		if err = m.store.SaveBlockData(ctx, header, data, &signature); err != nil {
 			return SaveBlockError{err}
 		}
 	}
@@ -664,25 +678,7 @@ func (m *Manager) createBlock(ctx context.Context, height uint64, lastSignature
 	return m.execCreateBlock(ctx, height, lastSignature, lastHeaderHash, m.lastState, batchData)
 }
 
-func (m *Manager) applyBlock(ctx context.Context, header *types.SignedHeader, data *types.Data) (types.State, error) {
-	m.lastStateMtx.RLock()
-	defer m.lastStateMtx.RUnlock()
-	return m.execApplyBlock(ctx, m.lastState, header, data)
-}
-
-func (m *Manager) execValidate(_ types.State, _ *types.SignedHeader, _ *types.Data) error {
-	// TODO(tzdybal): implement
-	return nil
-}
-
-func (m *Manager) execCommit(ctx context.Context, newState types.State, h *types.SignedHeader, _ *types.Data) ([]byte, error) {
-	err := m.exec.SetFinal(ctx, h.Height())
-	return newState.AppHash, err
-}
-
-func (m *Manager) execCreateBlock(_ context.Context, height uint64, lastSignature *types.Signature, lastHeaderHash types.Hash, lastState types.State, batchData *BatchData) (*types.SignedHeader, *types.Data, error) {
-	batchDataIDs := convertBatchDataToBytes(batchData.Data)
-
+func (m *Manager) execCreateBlock(ctx context.Context, height uint64, lastSignature *types.Signature, lastHeaderHash types.Hash, lastState types.State, batchData *BatchData) (*types.SignedHeader, *types.Data, error) {
 	if m.signer == nil {
 		return nil, nil, fmt.Errorf("signer is nil; cannot create block")
 	}
@@ -701,21 +697,32 @@ func (m *Manager) execCreateBlock(_ context.Context, height uint64, lastSignatur
 		return nil, nil, fmt.Errorf("proposer address is not the same as the genesis proposer address %x != %x", address, m.genesis.ProposerAddress)
 	}
 
+	// Determine if this is an empty block
+	isEmpty := batchData.Batch == nil || len(batchData.Transactions) == 0
+
+	// Set the appropriate data hash based on whether this is an empty block
+	var dataHash types.Hash
+	if isEmpty {
+		dataHash = dataHashForEmptyTxs // Use dataHashForEmptyTxs for empty blocks
+	} else {
+		dataHash = convertBatchDataToBytes(batchData.Data)
+	}
+
 	header := &types.SignedHeader{
 		Header: types.Header{
 			Version: types.Version{
-				Block: lastState.Version.Block,
-				App:   lastState.Version.App,
+				Block: m.lastState.Version.Block,
+				App:   m.lastState.Version.App,
 			},
 			BaseHeader: types.BaseHeader{
-				ChainID: lastState.ChainID,
+				ChainID: m.lastState.ChainID,
 				Height:  height,
 				Time:    uint64(batchData.UnixNano()), //nolint:gosec // why is time unix? (tac0turtle)
 			},
 			LastHeaderHash:  lastHeaderHash,
-			DataHash:        batchDataIDs,
+			DataHash:        dataHash,
 			ConsensusHash:   make(types.Hash, 32),
-			AppHash:         lastState.AppHash,
+			AppHash:         m.lastState.AppHash,
 			ProposerAddress: m.genesis.ProposerAddress,
 		},
 		Signature: *lastSignature,
@@ -725,16 +732,38 @@ func (m *Manager) execCreateBlock(_ context.Context, height uint64, lastSignatur
 		},
 	}
 
+	// Create block data with appropriate transactions
 	blockData := &types.Data{
-		Txs: make(types.Txs, len(batchData.Transactions)),
+		Txs: make(types.Txs, 0), // Start with empty transaction list
 	}
-	for i := range batchData.Transactions {
-		blockData.Txs[i] = types.Tx(batchData.Transactions[i])
+
+	// Only add transactions if this is not an empty block
+	if !isEmpty {
+		blockData.Txs = make(types.Txs, len(batchData.Transactions))
+		for i := range batchData.Transactions {
+			blockData.Txs[i] = types.Tx(batchData.Transactions[i])
+		}
 	}
 
 	return header, blockData, nil
 }
 
+func (m *Manager) applyBlock(ctx context.Context, header *types.SignedHeader, data *types.Data) (types.State, error) {
+	m.lastStateMtx.RLock()
+	defer m.lastStateMtx.RUnlock()
+	return m.execApplyBlock(ctx, m.lastState, header, data)
+}
+
+func (m *Manager) execValidate(_ types.State, _ *types.SignedHeader, _ *types.Data) error {
+	// TODO(tzdybal): implement
+	return nil
+}
+
+func (m *Manager) execCommit(ctx context.Context, newState types.State, h *types.SignedHeader, _ *types.Data) ([]byte, error) {
+	err := m.exec.SetFinal(ctx, h.Height())
+	return newState.AppHash, err
+}
+
 func (m *Manager) execApplyBlock(ctx context.Context, lastState types.State, header *types.SignedHeader, data *types.Data) (types.State, error) {
 	rawTxs := make([][]byte, len(data.Txs))
 	for i := range data.Txs {
@@ -846,3 +875,16 @@ func (m *Manager) getSignature(header types.Header) (types.Signature, error) {
 	}
 	return m.signer.Sign(b)
 }
+
+// NotifyNewTransactions signals that new transactions are available for processing
+// This method will be called by the Reaper when it receives new transactions
+func (m *Manager) NotifyNewTransactions() {
+	// Non-blocking send to avoid slowing down the transaction submission path
+	select {
+	case m.txNotifyCh <- struct{}{}:
+		// Successfully sent notification
+	default:
+		// Channel buffer is full, which means a notification is already pending
+		// This is fine, as we just need to trigger one block production
+	}
+}
diff --git a/block/publish_block_test.go b/block/publish_block_test.go
index f3440250c5..a52f50b89b 100644
--- a/block/publish_block_test.go
+++ b/block/publish_block_test.go
@@ -227,6 +227,9 @@ func Test_publishBlock_EmptyBatch(t *testing.T) {
 	noopSigner, err := noopsigner.NewNoopSigner(privKey)
 	require.NoError(err)
 
+	daH := atomic.Uint64{}
+	daH.Store(0)
+
 	m := &Manager{
 		store:     mockStore,
 		sequencer: mockSeq,
@@ -245,6 +248,18 @@ func Test_publishBlock_EmptyBatch(t *testing.T) {
 		},
 		lastStateMtx: &sync.RWMutex{},
 		metrics:      NopMetrics(),
+		lastState: types.State{
+			ChainID:         chainID,
+			InitialHeight:   1,
+			LastBlockHeight: 1,
+			LastBlockTime:   time.Now(),
+			AppHash:         []byte("initialAppHash"),
+		},
+		headerCache: cache.NewCache[types.SignedHeader](),
+		dataCache:   cache.NewCache[types.Data](),
+		HeaderCh:    make(chan *types.SignedHeader, 1),
+		DataCh:      make(chan *types.Data, 1),
+		daHeight:    &daH,
 	}
 
 	m.publishBlock = m.publishBlockInternal
@@ -279,19 +294,34 @@ func Test_publishBlock_EmptyBatch(t *testing.T) {
 	})
 	mockSeq.On("GetNextBatch", ctx, batchReqMatcher).Return(emptyBatchResponse, nil).Once()
 
+	// Mock SetMetadata for LastBatchDataKey (required for empty batch handling)
+	mockStore.On("SetMetadata", ctx, "l", mock.AnythingOfType("[]uint8")).Return(nil).Once()
+
+	// With our new implementation, we should expect SaveBlockData to be called for empty blocks
+	mockStore.On("SaveBlockData", ctx, mock.AnythingOfType("*types.SignedHeader"), mock.AnythingOfType("*types.Data"), mock.AnythingOfType("*types.Signature")).Return(nil).Once()
+
+	// We should also expect ExecuteTxs to be called with an empty transaction list
+	newAppHash := []byte("newAppHash")
+	mockExec.On("ExecuteTxs", ctx, mock.Anything, currentHeight+1, mock.AnythingOfType("time.Time"), m.lastState.AppHash).Return(newAppHash, uint64(100), nil).Once()
+
+	// SetFinal should be called
+	mockExec.On("SetFinal", ctx, currentHeight+1).Return(nil).Once()
+
+	// SetHeight should be called
+	mockStore.On("SetHeight", ctx, currentHeight+1).Return(nil).Once()
+
+	// UpdateState should be called
+	mockStore.On("UpdateState", ctx, mock.AnythingOfType("types.State")).Return(nil).Once()
+
+	// SaveBlockData should be called again after validation
+	mockStore.On("SaveBlockData", ctx, mock.AnythingOfType("*types.SignedHeader"), mock.AnythingOfType("*types.Data"), mock.AnythingOfType("*types.Signature")).Return(nil).Once()
+
 	// 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)
-	mockExec.AssertNotCalled(t, "ExecuteTxs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
-	mockExec.AssertNotCalled(t, "SetFinal", mock.Anything, mock.Anything)
-	mockStore.AssertNotCalled(t, "SetHeight", mock.Anything, mock.Anything)
-	mockStore.AssertNotCalled(t, "UpdateState", mock.Anything, mock.Anything)
-
 	mockSeq.AssertExpectations(t)
 	mockStore.AssertExpectations(t)
 	mockExec.AssertExpectations(t)
diff --git a/block/reaper.go b/block/reaper.go
index a517747bf0..5c18c1cd29 100644
--- a/block/reaper.go
+++ b/block/reaper.go
@@ -27,6 +27,7 @@ type Reaper struct {
 	logger    log.Logger
 	ctx       context.Context
 	seenStore ds.Batching
+	manager   *Manager
 }
 
 // NewReaper creates a new Reaper instance with persistent seenTx storage.
@@ -45,6 +46,11 @@ func NewReaper(ctx context.Context, exec coreexecutor.Executor, sequencer corese
 	}
 }
 
+// SetManager sets the Manager reference for transaction notifications
+func (r *Reaper) SetManager(manager *Manager) {
+	r.manager = manager
+}
+
 // Start begins the reaping process at the specified interval.
 func (r *Reaper) Start(ctx context.Context) {
 	r.ctx = ctx
@@ -110,6 +116,12 @@ func (r *Reaper) SubmitTxs() {
 		}
 	}
 
+	// Notify the manager that new transactions are available
+	if r.manager != nil && len(newTxs) > 0 {
+		r.logger.Debug("Notifying manager of new transactions")
+		r.manager.NotifyNewTransactions()
+	}
+
 	r.logger.Debug("Reaper successfully submitted txs")
 }
 
diff --git a/node/full.go b/node/full.go
index 21413a8359..c5e95c1f85 100644
--- a/node/full.go
+++ b/node/full.go
@@ -97,16 +97,6 @@ func newFullNode(
 
 	store := store.New(mainKV)
 
-	reaper := block.NewReaper(
-		ctx,
-		exec,
-		sequencer,
-		genesis.ChainID,
-		nodeConfig.Node.BlockTime.Duration,
-		logger.With("module", "Reaper"),
-		mainKV,
-	)
-
 	blockManager, err := initBlockManager(
 		ctx,
 		signer,
@@ -127,6 +117,19 @@ func newFullNode(
 		return nil, err
 	}
 
+	reaper := block.NewReaper(
+		ctx,
+		exec,
+		sequencer,
+		genesis.ChainID,
+		nodeConfig.Node.BlockTime.Duration,
+		logger.With("module", "Reaper"),
+		mainKV,
+	)
+
+	// Connect the reaper to the manager for transaction notifications
+	reaper.SetManager(blockManager)
+
 	node := &FullNode{
 		genesis:      genesis,
 		nodeConfig:   nodeConfig,
diff --git a/node/helpers_test.go b/node/helpers_test.go
index f334db00dc..226fb8e229 100644
--- a/node/helpers_test.go
+++ b/node/helpers_test.go
@@ -22,9 +22,9 @@ func getTestConfig(t *testing.T, n int) rollkitconfig.Config {
 	return rollkitconfig.Config{
 		RootDir: t.TempDir(),
 		Node: rollkitconfig.NodeConfig{
-			Aggregator:    true,
-			BlockTime:     rollkitconfig.DurationWrapper{Duration: 500 * time.Millisecond},
-			LazyBlockTime: rollkitconfig.DurationWrapper{Duration: 5 * time.Second},
+			Aggregator:        true,
+			BlockTime:         rollkitconfig.DurationWrapper{Duration: 500 * time.Millisecond},
+			LazyBlockInterval: rollkitconfig.DurationWrapper{Duration: 5 * time.Second},
 		},
 		DA: rollkitconfig.DAConfig{
 			Address:   MockDAAddress,
diff --git a/pkg/cmd/run_node_test.go b/pkg/cmd/run_node_test.go
index 98c387a65e..f94e334c00 100644
--- a/pkg/cmd/run_node_test.go
+++ b/pkg/cmd/run_node_test.go
@@ -63,8 +63,8 @@ func TestParseFlags(t *testing.T) {
 		"--rollkit.da.mempool_ttl", "10",
 		"--rollkit.da.namespace", "namespace",
 		"--rollkit.da.start_height", "100",
-		"--rollkit.node.lazy_aggregator",
-		"--rollkit.node.lazy_block_time", "2m",
+		"--rollkit.node.lazy_mode",
+		"--rollkit.node.lazy_block_interval", "2m",
 		"--rollkit.node.light",
 		"--rollkit.node.max_pending_blocks", "100",
 		"--rollkit.node.trusted_hash", "abcdef1234567890",
@@ -123,8 +123,8 @@ func TestParseFlags(t *testing.T) {
 		{"DAMempoolTTL", nodeConfig.DA.MempoolTTL, uint64(10)},
 		{"DANamespace", nodeConfig.DA.Namespace, "namespace"},
 		{"DAStartHeight", nodeConfig.DA.StartHeight, uint64(100)},
-		{"LazyAggregator", nodeConfig.Node.LazyAggregator, true},
-		{"LazyBlockTime", nodeConfig.Node.LazyBlockTime.Duration, 2 * time.Minute},
+		{"LazyAggregator", nodeConfig.Node.LazyMode, true},
+		{"LazyBlockTime", nodeConfig.Node.LazyBlockInterval.Duration, 2 * time.Minute},
 		{"Light", nodeConfig.Node.Light, true},
 		{"MaxPendingBlocks", nodeConfig.Node.MaxPendingBlocks, uint64(100)},
 		{"TrustedHash", nodeConfig.Node.TrustedHash, "abcdef1234567890"},
diff --git a/pkg/config/config.go b/pkg/config/config.go
index ee1a34bf08..7394222967 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -37,11 +37,11 @@ const (
 	// FlagTrustedHash is a flag for specifying the trusted hash
 	FlagTrustedHash = "rollkit.node.trusted_hash"
 	// FlagLazyAggregator is a flag for enabling lazy aggregation mode that only produces blocks when transactions are available
-	FlagLazyAggregator = "rollkit.node.lazy_aggregator"
+	FlagLazyAggregator = "rollkit.node.lazy_mode"
 	// FlagMaxPendingBlocks is a flag to limit and pause block production when too many blocks are waiting for DA confirmation
 	FlagMaxPendingBlocks = "rollkit.node.max_pending_blocks"
 	// FlagLazyBlockTime is a flag for specifying the maximum interval between blocks in lazy aggregation mode
-	FlagLazyBlockTime = "rollkit.node.lazy_block_time"
+	FlagLazyBlockTime = "rollkit.node.lazy_block_interval"
 
 	// Data Availability configuration flags
 
@@ -162,10 +162,10 @@ type NodeConfig struct {
 	Light      bool `yaml:"light" comment:"Run node in light mode"`
 
 	// Block management configuration
-	BlockTime        DurationWrapper `mapstructure:"block_time" yaml:"block_time" comment:"Block time (duration). Examples: \"500ms\", \"1s\", \"5s\", \"1m\", \"2m30s\", \"10m\"."`
-	MaxPendingBlocks uint64          `mapstructure:"max_pending_blocks" yaml:"max_pending_blocks" comment:"Maximum number of blocks pending DA submission. When this limit is reached, the aggregator pauses block production until some blocks are confirmed. Use 0 for no limit."`
-	LazyAggregator   bool            `mapstructure:"lazy_aggregator" yaml:"lazy_aggregator" comment:"Enables lazy aggregation mode, where blocks are only produced when transactions are available or after LazyBlockTime. Optimizes resources by avoiding empty block creation during periods of inactivity."`
-	LazyBlockTime    DurationWrapper `mapstructure:"lazy_block_time" yaml:"lazy_block_time" comment:"Maximum interval between blocks in lazy aggregation mode (LazyAggregator). Ensures blocks are produced periodically even without transactions to keep the chain active. Generally larger than BlockTime."`
+	BlockTime         DurationWrapper `mapstructure:"block_time" yaml:"block_time" comment:"Block time (duration). Examples: \"500ms\", \"1s\", \"5s\", \"1m\", \"2m30s\", \"10m\"."`
+	MaxPendingBlocks  uint64          `mapstructure:"max_pending_blocks" yaml:"max_pending_blocks" comment:"Maximum number of blocks pending DA submission. When this limit is reached, the aggregator pauses block production until some blocks are confirmed. Use 0 for no limit."`
+	LazyMode          bool            `mapstructure:"lazy_mode" yaml:"lazy_mode" comment:"Enables lazy aggregation mode, where blocks are only produced when transactions are available or after LazyBlockTime. Optimizes resources by avoiding empty block creation during periods of inactivity."`
+	LazyBlockInterval DurationWrapper `mapstructure:"lazy_block_interval" yaml:"lazy_block_interval" comment:"Maximum interval between blocks in lazy aggregation mode (LazyAggregator). Ensures blocks are produced periodically even without transactions to keep the chain active. Generally larger than BlockTime."`
 
 	// Header configuration
 	TrustedHash string `mapstructure:"trusted_hash" yaml:"trusted_hash" comment:"Initial trusted hash used to bootstrap the header exchange service. Allows nodes to start synchronizing from a specific trusted point in the chain instead of genesis. When provided, the node will fetch the corresponding header/block from peers using this hash and use it as a starting point for synchronization. If not provided, the node will attempt to fetch the genesis block instead."`
@@ -240,9 +240,9 @@ func AddFlags(cmd *cobra.Command) {
 	cmd.Flags().Bool(FlagLight, def.Node.Light, "run light client")
 	cmd.Flags().Duration(FlagBlockTime, def.Node.BlockTime.Duration, "block time (for aggregator mode)")
 	cmd.Flags().String(FlagTrustedHash, def.Node.TrustedHash, "initial trusted hash to start the header exchange service")
-	cmd.Flags().Bool(FlagLazyAggregator, def.Node.LazyAggregator, "produce blocks only when transactions are available or after lazy block time")
+	cmd.Flags().Bool(FlagLazyAggregator, def.Node.LazyMode, "produce blocks only when transactions are available or after lazy block time")
 	cmd.Flags().Uint64(FlagMaxPendingBlocks, def.Node.MaxPendingBlocks, "maximum blocks pending DA confirmation before pausing block production (0 for no limit)")
-	cmd.Flags().Duration(FlagLazyBlockTime, def.Node.LazyBlockTime.Duration, "maximum interval between blocks in lazy aggregation mode")
+	cmd.Flags().Duration(FlagLazyBlockTime, def.Node.LazyBlockInterval.Duration, "maximum interval between blocks in lazy aggregation mode")
 
 	// Data Availability configuration flags
 	cmd.Flags().String(FlagDAAddress, def.DA.Address, "DA address (host:port)")
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 7974a1f0a6..da44d7fd7a 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -27,12 +27,12 @@ func TestDefaultConfig(t *testing.T) {
 	assert.Equal(t, "", def.DA.SubmitOptions)
 	assert.Equal(t, "", def.DA.Namespace)
 	assert.Equal(t, 1*time.Second, def.Node.BlockTime.Duration)
-	assert.Equal(t, 15*time.Second, def.DA.BlockTime.Duration)
+	assert.Equal(t, 6*time.Second, def.DA.BlockTime.Duration)
 	assert.Equal(t, uint64(0), def.DA.StartHeight)
 	assert.Equal(t, uint64(0), def.DA.MempoolTTL)
 	assert.Equal(t, uint64(0), def.Node.MaxPendingBlocks)
-	assert.Equal(t, false, def.Node.LazyAggregator)
-	assert.Equal(t, 60*time.Second, def.Node.LazyBlockTime.Duration)
+	assert.Equal(t, false, def.Node.LazyMode)
+	assert.Equal(t, 60*time.Second, def.Node.LazyBlockInterval.Duration)
 	assert.Equal(t, "", def.Node.TrustedHash)
 	assert.Equal(t, "file", def.Signer.SignerType)
 	assert.Equal(t, "config", def.Signer.SignerPath)
@@ -57,9 +57,9 @@ func TestAddFlags(t *testing.T) {
 	assertFlagValue(t, flags, FlagLight, DefaultConfig.Node.Light)
 	assertFlagValue(t, flags, FlagBlockTime, DefaultConfig.Node.BlockTime.Duration)
 	assertFlagValue(t, flags, FlagTrustedHash, DefaultConfig.Node.TrustedHash)
-	assertFlagValue(t, flags, FlagLazyAggregator, DefaultConfig.Node.LazyAggregator)
+	assertFlagValue(t, flags, FlagLazyAggregator, DefaultConfig.Node.LazyMode)
 	assertFlagValue(t, flags, FlagMaxPendingBlocks, DefaultConfig.Node.MaxPendingBlocks)
-	assertFlagValue(t, flags, FlagLazyBlockTime, DefaultConfig.Node.LazyBlockTime.Duration)
+	assertFlagValue(t, flags, FlagLazyBlockTime, DefaultConfig.Node.LazyBlockInterval.Duration)
 
 	// DA flags
 	assertFlagValue(t, flags, FlagDAAddress, DefaultConfig.DA.Address)
@@ -232,7 +232,7 @@ signer:
 	cmd.SetArgs([]string{
 		"--home=" + tempDir,
 		"--rollkit.da.gas_price=0.5",
-		"--rollkit.node.lazy_aggregator=true",
+		"--rollkit.node.lazy_mode=true",
 	})
 	err = cmd.Execute()
 	require.NoError(t, err)
@@ -245,7 +245,7 @@ signer:
 	v := viper.New()
 	v.Set(FlagRootDir, tempDir)
 	v.Set("rollkit.da.gas_price", "0.5")
-	v.Set("rollkit.node.lazy_aggregator", true)
+	v.Set("rollkit.node.lazy_mode", true)
 
 	// Load configuration using the new LoadFromViper method
 	cfgFromViper, err := LoadFromViper(v)
@@ -254,7 +254,7 @@ signer:
 	// Compare the results - they should be identical
 	require.Equal(t, cfgFromLoad.RootDir, cfgFromViper.RootDir, "RootDir should match")
 	require.Equal(t, cfgFromLoad.DA.GasPrice, cfgFromViper.DA.GasPrice, "DA.GasPrice should match")
-	require.Equal(t, cfgFromLoad.Node.LazyAggregator, cfgFromViper.Node.LazyAggregator, "Node.LazyAggregator should match")
+	require.Equal(t, cfgFromLoad.Node.LazyMode, cfgFromViper.Node.LazyMode, "Node.LazyMode should match")
 	require.Equal(t, cfgFromLoad.Node.Aggregator, cfgFromViper.Node.Aggregator, "Node.Aggregator should match")
 	require.Equal(t, cfgFromLoad.Node.BlockTime, cfgFromViper.Node.BlockTime, "Node.BlockTime should match")
 	require.Equal(t, cfgFromLoad.DA.Address, cfgFromViper.DA.Address, "DA.Address should match")
diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go
index b3d52cd521..6b5ce0474b 100644
--- a/pkg/config/defaults.go
+++ b/pkg/config/defaults.go
@@ -45,16 +45,16 @@ var DefaultConfig = Config{
 		Peers:         "",
 	},
 	Node: NodeConfig{
-		Aggregator:     false,
-		BlockTime:      DurationWrapper{1 * time.Second},
-		LazyAggregator: false,
-		LazyBlockTime:  DurationWrapper{60 * time.Second},
-		Light:          false,
-		TrustedHash:    "",
+		Aggregator:        false,
+		BlockTime:         DurationWrapper{1 * time.Second},
+		LazyMode:          false,
+		LazyBlockInterval: DurationWrapper{60 * time.Second},
+		Light:             false,
+		TrustedHash:       "",
 	},
 	DA: DAConfig{
 		Address:       "http://localhost:7980",
-		BlockTime:     DurationWrapper{15 * time.Second},
+		BlockTime:     DurationWrapper{6 * time.Second},
 		GasPrice:      -1,
 		GasMultiplier: 0,
 	},
diff --git a/rollups/testapp/cmd/init_test.go b/rollups/testapp/cmd/init_test.go
index eb9aae6f75..a002e6f574 100644
--- a/rollups/testapp/cmd/init_test.go
+++ b/rollups/testapp/cmd/init_test.go
@@ -71,7 +71,7 @@ func TestInitCommand(t *testing.T) {
 	require.Contains(t, yamlContent, "da:")
 	require.Contains(t, yamlContent, "block_time: ")
 	require.Contains(t, yamlContent, "15s")
-	require.Contains(t, yamlContent, "lazy_block_time: ")
+	require.Contains(t, yamlContent, "lazy_block_interval: ")
 	require.Contains(t, yamlContent, "1m0s")
 
 	// Verify addresses
diff --git a/rollups/testapp/kv/kvexecutor_test.go b/rollups/testapp/kv/kvexecutor_test.go
index a6fd24d1d6..774c8b7735 100644
--- a/rollups/testapp/kv/kvexecutor_test.go
+++ b/rollups/testapp/kv/kvexecutor_test.go
@@ -1,6 +1,7 @@
 package executor
 
 import (
+	"bytes"
 	"context"
 	"reflect"
 	"strings"
@@ -32,7 +33,7 @@ func TestInitChain_Idempotency(t *testing.T) {
 	if err != nil {
 		t.Fatalf("InitChain failed on second call: %v", err)
 	}
-	if !reflect.DeepEqual(stateRoot1, stateRoot2) {
+	if !bytes.Equal(stateRoot1, stateRoot2) {
 		t.Errorf("Genesis state roots do not match: %s vs %s", stateRoot1, stateRoot2)
 	}
 	if maxBytes2 != 1024 {
diff --git a/specs/lazy-adr/adr-007-header-commit-to-shares.md b/specs/lazy-adr/adr-007-header-commit-to-shares.md
index 283fece0ae..acc5d08cde 100644
--- a/specs/lazy-adr/adr-007-header-commit-to-shares.md
+++ b/specs/lazy-adr/adr-007-header-commit-to-shares.md
@@ -56,13 +56,10 @@ Discarded
 
 ### Positive
 
-
 ### Negative
 
-
 ### Neutral
 
-
 ## References
 
 1. <https://github.com/rollkit/rollkit/issues/133>
diff --git a/specs/lazy-adr/adr-021-lazy-aggregation.md b/specs/lazy-adr/adr-021-lazy-aggregation.md
new file mode 100644
index 0000000000..40543440cb
--- /dev/null
+++ b/specs/lazy-adr/adr-021-lazy-aggregation.md
@@ -0,0 +1,209 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+- 2024-01-24: Revised to use existing empty batch mechanism
+- 2024-01-25: Updated with implementation details from aggregation.go
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+## Decision
+
+Leverage the existing empty batch mechanism and `dataHashForEmptyTxs` to maintain block height consistency.
+
+## Detailed Design
+
+### Implementation Details
+
+1. **Modified Batch Retrieval**:
+
+    The batch retrieval mechanism has been modified to handle empty batches differently. Instead of discarding empty batches, we now return them with the ErrNoBatch error, allowing the caller to create empty blocks with proper timestamps. This ensures that block timing remains consistent even during periods of inactivity.
+
+    ```go
+    func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+        res, err := m.sequencer.GetNextBatch(ctx, req)
+        if err != nil {
+            return nil, err
+        }
+
+        if res != nil && res.Batch != nil {
+            m.logger.Debug("Retrieved batch",
+                "txCount", len(res.Batch.Transactions),
+                "timestamp", res.Timestamp)
+
+            var errRetrieveBatch error
+            // Even if there are no transactions, return the batch with timestamp
+            // This allows empty blocks to maintain proper timing
+            if len(res.Batch.Transactions) == 0 {
+                errRetrieveBatch = ErrNoBatch
+            }
+            // Even if there are no transactions, update lastBatchData so we don't
+            // repeatedly emit the same empty batch, and persist it to metadata.
+            if err := m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData)); err != nil {
+                m.logger.Error("error while setting last batch hash", "error", err)
+            }
+            m.lastBatchData = res.BatchData
+            return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, errRetrieveBatch
+	    }
+	    return nil, ErrNoBatch
+    }
+    ```
+
+2. **Empty Block Creation**:
+
+    The block publishing logic has been enhanced to create empty blocks when a batch with no transactions is received. This uses the special `dataHashForEmptyTxs` value to indicate an empty batch, maintaining the block height consistency with the DA layer while minimizing overhead.
+
+    ```go
+    // In publishBlock method
+    batchData, err := m.retrieveBatch(ctx)
+		if err != nil {
+			if errors.Is(err, ErrNoBatch) {
+				if batchData == nil {
+					m.logger.Info("No batch retrieved from sequencer, skipping block production")
+					return nil
+				}
+				m.logger.Info("Creating empty block", "height", newHeight)
+			} else {
+				return fmt.Errorf("failed to get transactions from batch: %w", err)
+			}
+		} else {
+			if batchData.Before(lastHeaderTime) {
+				return fmt.Errorf("timestamp is not monotonically increasing: %s < %s", batchData.Time, m.getLastBlockTime())
+			}
+			m.logger.Info("Creating and publishing block", "height", newHeight)
+			m.logger.Debug("block info", "num_tx", len(batchData.Batch.Transactions))
+		}
+
+		header, data, err = m.createBlock(ctx, newHeight, lastSignature, lastHeaderHash, batchData)
+		if err != nil {
+			return err
+		}
+
+		if err = m.store.SaveBlockData(ctx, header, data, &signature); err != nil {
+			return SaveBlockError{err}
+		}
+    ```
+
+3. **Lazy Aggregation Loop**:
+
+    A dedicated lazy aggregation loop has been implemented with dual timer mechanisms. The `lazyTimer` ensures blocks are produced at regular intervals even during network inactivity, while the `blockTimer` handles normal block production when transactions are available. Transaction notifications from the `Reaper` to the `Manager` are now handled via the `txNotifyCh` channel: when the `Reaper` detects new transactions, it calls `Manager.NotifyNewTransactions()`, which performs a non-blocking signal on this channel. See the tests in `block/lazy_aggregation_test.go` for verification of this behavior.
+
+    ```go
+    // In Reaper.SubmitTxs
+    if r.manager != nil && len(newTxs) > 0 {
+        r.logger.Debug("Notifying manager of new transactions")
+        r.manager.NotifyNewTransactions() // Signals txNotifyCh
+    }
+
+    // In Manager.NotifyNewTransactions
+    func (m *Manager) NotifyNewTransactions() {
+        select {
+        case m.txNotifyCh <- struct{}{}:
+            // Successfully sent notification
+        default:
+            // Channel buffer is full, notification already pending
+        }
+    }
+    // Modified lazyAggregationLoop
+    func (m *Manager) lazyAggregationLoop(ctx context.Context, blockTimer *time.Timer) {
+        // lazyTimer triggers block publication even during inactivity
+        lazyTimer := time.NewTimer(0)
+        defer lazyTimer.Stop()
+
+        for {
+            select {
+            case <-ctx.Done():
+                return
+
+            case <-lazyTimer.C:
+                m.logger.Debug("Lazy timer triggered block production")
+                m.produceBlock(ctx, "lazy_timer", lazyTimer, blockTimer)
+
+            case <-blockTimer.C:
+                if m.txsAvailable {
+                    m.produceBlock(ctx, "block_timer", lazyTimer, blockTimer)
+                    m.txsAvailable = false
+                } else {
+                    // Ensure we keep ticking even when there are no txs
+                    blockTimer.Reset(m.config.Node.BlockTime.Duration)
+                }
+            case <-m.txNotifyCh:
+                m.txsAvailable = true
+            }
+        }
+    }
+    ```
+
+4. **Block Production**:
+
+    The block production function centralizes the logic for publishing blocks and resetting timers. It records the start time, attempts to publish a block, and then intelligently resets both timers based on the elapsed time. This ensures that block production remains on schedule even if the block creation process takes significant time.
+
+    ```go
+    func (m *Manager) produceBlock(ctx context.Context, trigger string, lazyTimer, blockTimer *time.Timer) {
+        // Record the start time
+        start := time.Now()
+
+        // Attempt to publish the block
+        if err := m.publishBlock(ctx); err != nil && ctx.Err() == nil {
+            m.logger.Error("error while publishing block", "trigger", trigger, "error", err)
+        } else {
+            m.logger.Debug("Successfully published block", "trigger", trigger)
+        }
+
+        // Reset both timers for the next aggregation window
+        lazyTimer.Reset(getRemainingSleep(start, m.config.Node.LazyBlockInterval.Duration))
+        blockTimer.Reset(getRemainingSleep(start, m.config.Node.BlockTime.Duration))
+    }
+    ```
+
+### Key Changes
+
+1. Return batch with timestamp even when empty, allowing proper block timing
+2. Use existing `dataHashForEmptyTxs` for empty block indication
+3. Leverage current sync mechanisms that already handle empty blocks
+4. Implement a dedicated lazy aggregation loop with two timers:
+   - `blockTimer`: Triggers block production at regular intervals when transactions are available
+   - `lazyTimer`: Ensures blocks are produced even during periods of inactivity
+5. Maintain transaction availability tracking via the `txsAvailable` flag and notification channel
+
+### Efficiency Considerations
+
+- Minimal DA layer overhead for empty blocks
+- Reuses existing empty block detection mechanism
+- Maintains proper block timing using batch timestamps
+- Intelligent timer management to account for block production time
+- Non-blocking transaction notification channel to prevent backpressure
+
+## Status
+
+Implemented
+
+## Consequences
+
+### Positive
+
+- Maintains consistent block heights between DA and execution layers
+- Leverages existing empty block mechanisms
+- Simpler implementation than sentinel-based approach
+- Preserves proper block timing
+- Provides deterministic block production even during network inactivity
+- Reduces latency for transaction inclusion during active periods
+
+### Negative
+
+- Small DA layer overhead for empty blocks
+- Additional complexity in timer management
+
+### Neutral
+
+- Requires careful handling of batch timestamps
+- Maintains backward compatibility with existing Rollkit deployments
+
+## References
+
+- [Block Manager Implementation](block/manager.go)
+- [Block Aggregation Implementation](block/aggregation.go)
+- [Lazy Aggregation Tests](block/lazy_aggregation_test.go)
diff --git a/specs/src/specs/block-manager.md b/specs/src/specs/block-manager.md
index b60b51e27b..ab498b2653 100644
--- a/specs/src/specs/block-manager.md
+++ b/specs/src/specs/block-manager.md
@@ -63,7 +63,8 @@ Block manager configuration options:
 |BlockTime|time.Duration|time interval used for block production and block retrieval from block store ([`defaultBlockTime`][defaultBlockTime])|
 |DABlockTime|time.Duration|time interval used for both block publication to DA network and block retrieval from DA network ([`defaultDABlockTime`][defaultDABlockTime])|
 |DAStartHeight|uint64|block retrieval from DA network starts from this height|
-|LazyBlockTime|time.Duration|time interval used for block production in lazy aggregator mode even when there are no transactions ([`defaultLazyBlockTime`][defaultLazyBlockTime])|
+|LazyBlockInterval|time.Duration|time interval used for block production in lazy aggregator mode even when there are no transactions ([`defaultLazyBlockTime`][defaultLazyBlockTime])|
+|LazyMode|bool|when set to true, enables lazy aggregation mode which produces blocks only when transactions are available or at LazyBlockInterval intervals|
 
 ### Block Production
 
@@ -71,7 +72,12 @@ When the full node is operating as a sequencer (aka aggregator), the block manag
 
 In `normal` mode, the block manager runs a timer, which is set to the `BlockTime` configuration parameter, and continuously produces blocks at `BlockTime` intervals.
 
-In `lazy` mode, the block manager starts building a block when any transaction becomes available in the mempool. After the first notification of the transaction availability, the manager will wait for a 1 second timer to finish, in order to collect as many transactions from the mempool as possible. The 1 second delay is chosen in accordance with the default block time of 1s. The block manager also notifies the full node after every lazy block building.
+In `lazy` mode, the block manager implements a dual timer mechanism:
+
+1. A `blockTimer` that triggers block production at regular intervals when transactions are available
+2. A `lazyTimer` that ensures blocks are produced at `LazyBlockInterval` intervals even during periods of inactivity
+
+The block manager starts building a block when any transaction becomes available in the mempool via a notification channel (`txNotifyCh`). When the `Reaper` detects new transactions, it calls `Manager.NotifyNewTransactions()`, which performs a non-blocking signal on this channel. The block manager also produces empty blocks at regular intervals to maintain consistency with the DA layer, ensuring a 1:1 mapping between DA layer blocks and execution layer blocks.
 
 #### Building the Block
 
@@ -154,6 +160,12 @@ The communication between the full node and block manager:
 * The block manager loads the initial state from the local store and uses genesis if not found in the local store, when the node (re)starts.
 * The default mode for sequencer nodes is normal (not lazy).
 * The sequencer can produce empty blocks.
+* In lazy aggregation mode, the block manager maintains consistency with the DA layer by producing empty blocks at regular intervals, ensuring a 1:1 mapping between DA layer blocks and execution layer blocks.
+* The lazy aggregation mechanism uses a dual timer approach:
+  * A `blockTimer` that triggers block production when transactions are available
+  * A `lazyTimer` that ensures blocks are produced even during periods of inactivity
+* Empty batches are handled differently in lazy mode - instead of discarding them, they are returned with the `ErrNoBatch` error, allowing the caller to create empty blocks with proper timestamps.
+* Transaction notifications from the `Reaper` to the `Manager` are handled via a non-blocking notification channel (`txNotifyCh`) to prevent backpressure.
 * The block manager uses persistent storage (disk) when the `root_dir` and `db_path` configuration parameters are specified in `config.yaml` file under the app directory. If these configuration parameters are not specified, the in-memory storage is used, which will not be persistent if the node stops.
 * The block manager does not re-apply the block again (in other words, create a new updated state and persist it) when a block was initially applied using P2P block sync, but later was DA included during DA retrieval. The block is only marked DA included in this case.
 * The data sync store is created by prefixing `dataSync` on the main data store.
@@ -187,6 +199,8 @@ See [tutorial] for running a multi-node network with both sequencer and non-sequ
 
 [8] [Data Availability](./da.md)
 
+[9] [Lazy Aggregation with DA Layer Consistency ADR](../../lazy-adr/adr-021-lazy-aggregation.md)
+
 [maxSubmitAttempts]: https://github.com/rollkit/rollkit/blob/main/block/manager.go#L50
 [defaultBlockTime]: https://github.com/rollkit/rollkit/blob/main/block/manager.go#L36
 [defaultDABlockTime]: https://github.com/rollkit/rollkit/blob/main/block/manager.go#L33


Reviews:
user: login: Manav-Aggarwal
body: left a small nit, but looks good overall. Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T21:03:07Z
user: login: gupadhyaya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T06:00:44Z
user: login: tac0turtle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T07:43:05Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (3)</summary><blockquote>

`14-14`: **Clarify the origin of `dataHashForEmptyTxs`**  
It would help readers to include a reference or link to where `dataHashForEmptyTxs` is defined (e.g., in constants or config) so that its provenance and format are immediately clear.

---

`85-85`: **Nit: add missing article for clarity**  
Consider updating the phrasing for consistency and readability:  
```diff
-1. Return batch with timestamp even when empty, allowing proper block timing
+1. Return a batch with a timestamp even when empty, allowing proper block timing
```

---

`87-88`: **Unify terminology regarding sentinel transactions**  
The ADR states “No new data structures or special transactions needed,” yet earlier discussions mentioned a minimal sentinel transaction. To avoid confusion, reconcile these terms or add a short note comparing the two approaches and why you opted to reuse `dataHashForEmptyTxs` over introducing a sentinel.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7fd4583a9aabee01a24c8b86051cc58ca747b916 and ed5dce13ded94a351bb524b99810433ef4a3e9b6.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `specs/lazy-adr/adr-021-lazy-aggregation.md` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md</summary>

[uncategorized] ~84-~84: You might be missing the article “a” here.
Context: ...  ### Key Changes  1. Return batch with timestamp even when empty, allowing proper block ...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: rollkit-docker / docker-build (GHCR; ghcr.io/rollkit/rollkit)
* GitHub Check: test / Run Unit Tests
* GitHub Check: test / Build All Rollkit Binaries
* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (2)</summary>

`10-10`: **Specify applicability across execution environments**  
The context focuses on EVM compatibility, but it’s unclear whether ABCI‐based chains have the same 1:1 block mapping requirement. Please clarify if this ADR is EVM‐specific or applies universally to ABCI environments as well.

---

`73-80`: **Verify existing sync logic covers all edge cases**  
The ADR relies on `handleEmptyDataHash` to seamlessly sync empty blocks without waiting for data. Please ensure there are unit or integration tests that simulate out‐of‐order header delivery, network delays, and repeated empty blocks to verify no deadlocks or missed block heights occur.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T07:45:01Z
user: login: Manav-Aggarwal
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-30T12:51:03Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T19:53:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (6)</summary><blockquote>

`5-6`: **Use a consistent, machine‐parsable date format in the changelog**  
The entries `2024-01-24` are fine, but consider using full ISO 8601 timestamps (e.g. `2024-01-24T15:04Z`) or at least standardizing on `YYYY-MM-DD` throughout all ADRs to facilitate automated tooling.

---

`23-28`: **Normalize code‐fence indentation for Go snippets**  
The opening `````go``` block is indented by spaces, which breaks syntax highlighting in many renderers. Move the fence to the left margin:  
```diff
-    ```go
+```go
```

---

`65-67`: **Add explicit non-empty‐block return path**  
The snippet omits the actual return for non-empty blocks (lines 65–67). For clarity, include pseudocode or a placeholder, e.g.:  
```go
// Non-empty block: compute hash and return
header := &types.Header{ /* populate fields */ }
return header, nil
```

---

`74-81`: **Normalize Go snippet indentation and imports**  
The `handleEmptyDataHash` code block is also indented and lacks an explicit `import "bytes"` statement in the snippet. For completeness and consistent formatting, align the fence at margin and show needed imports:  
```go
import "bytes"

func (m *Manager) handleEmptyDataHash(ctx context.Context, header *types.Header) {
    if bytes.Equal(header.DataHash, dataHashForEmptyTxs) {
        // ... existing empty block handling
    }
}
```

---

`85-89`: **Refine phrasing in Key Changes list**  
For consistency with other ADRs, prepend articles and maintain parallel structure. For example:  
```diff
-1. Return batch with timestamp even when empty, allowing proper block timing
+1. Return a batch with its timestamp even when empty, preserving block timing
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~85-~85: You might be missing the article “a” here.
Context: ...  ### Key Changes  1. Return batch with timestamp even when empty, allowing proper block ...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

</details>

</details>

---

`98-106`: **Fix nested list indentation in Testing Strategy**  
The unit-test and integration-test bullets mix two- and three-space indents. Standardize on two spaces for sub-items to ensure proper rendering:  
```diff
-1. **Unit Tests**:
-   - Empty batch handling
-    - Timestamp preservation
-    - Block height consistency
+1. **Unit Tests**:
+  - Empty batch handling
+  - Timestamp preservation
+  - Block height consistency
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ed5dce13ded94a351bb524b99810433ef4a3e9b6 and e7472d70bbb2861bef23e5e88380482a6b7a03e6.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `specs/lazy-adr/adr-021-lazy-aggregation.md` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md</summary>

[uncategorized] ~45-~45: Loose punctuation mark.
Context: ...ta, 		}, nil 	} 	return nil, ErrNoBatch }     }     ```  2. **Header Creation for...

(UNLIKELY_OPENING_PUNCTUATION)

---

[uncategorized] ~85-~85: You might be missing the article “a” here.
Context: ...  ### Key Changes  1. Return batch with timestamp even when empty, allowing proper block ...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: rollkit-docker / docker-build (GHCR; ghcr.io/rollkit/rollkit)
* GitHub Check: Analyze (go)
* GitHub Check: test / Run Unit Tests
* GitHub Check: test / Build All Rollkit Binaries
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (1)</summary>

`10-11`: **Clarify whether this applies to ABCI as well as EVM**  
Manav-Aggarwal raised a good point: is the sentinel‐transaction mechanism only required for EVM execution environments, or should ABCI chains adopt the same approach? Adding a note under Context or Decision will help readers understand scope and avoid confusion.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T19:57:48Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🧹 Nitpick comments (9)</summary><blockquote>

<details>
<summary>pkg/cmd/run_node_test.go (2)</summary><blockquote>

`65-66`: **Boolean flag is parsed as “true”, but being explicit avoids surprises**

Passing `--rollkit.node.lazy_mode` without a value relies on Cobra’s “implicit-true” behaviour for boolean flags. That works, yet it is easy for readers (or future migration to a `DurationFlag`) to miss. Consider supplying `=true` for clarity and to keep the style consistent with `--rollkit.node.aggregator=false` a few lines above.

```diff
-        "--rollkit.node.lazy_mode",
+        "--rollkit.node.lazy_mode=true",
```

---

`125-126`: **Table headings still use the old field names**

The field assertions are correct, but the labels `"LazyAggregator"` and `"LazyBlockTime"` no longer reflect the renamed config fields. Renaming keeps the test self-documenting.

```diff
-        {"LazyAggregator", nodeConfig.Node.LazyMode, true},
-        {"LazyBlockTime", nodeConfig.Node.LazyBlockInterval.Duration, 2 * time.Minute},
+        {"LazyMode", nodeConfig.Node.LazyMode, true},
+        {"LazyBlockInterval", nodeConfig.Node.LazyBlockInterval.Duration, 2 * time.Minute},
```

</blockquote></details>
<details>
<summary>block/publish_block_test.go (2)</summary><blockquote>

`230-263`: **Avoid pointer-of-value pattern with atomic.Uint64**

Creating a value `daH` and then taking `&daH` works, but `atomic.Uint64` contains a `sync/atomic` primitive underneath which is preferable to allocate directly with `new(atomic.Uint64)` to make the ownership clearer and prevent accidental copies.

```diff
-    daH := atomic.Uint64{}
-    daH.Store(0)
-    ...
-    daHeight:    &daH,
+    daHeight:    new(atomic.Uint64),
 ```
Then initialise with `m.daHeight.Store(0)`.

---

`297-315`: **Tests depend on exact internal call count — fragile against refactors**

The expectations for `SaveBlockData` are duplicated because the implementation currently calls it twice. A small refactor (e.g., early-return optimisation) will break the test even if behaviour is unchanged. Consider asserting `Times(2)` instead of two distinct `On(...)` lines, or using `AssertNumberOfCalls`.

Example:

```go
mockStore.On("SaveBlockData", ctx,
    mock.AnythingOfType("*types.SignedHeader"),
    mock.AnythingOfType("*types.Data"),
    mock.AnythingOfType("*types.Signature"),
).Return(nil).Twice()
```
This keeps the test intention (called twice) but is less brittle.

</blockquote></details>
<details>
<summary>block/manager.go (1)</summary><blockquote>

`133-134`: **Potential data race on `txsAvailable`**

`txsAvailable` is a plain boolean that is:

* written by the aggregation loop goroutine, and  
* read by test code (and could be read by other goroutines calling `Manager` helpers in the future).

While today only one goroutine mutates it, the field is not documented or protected, so accidental cross-goroutine access could introduce races. Either:

1. convert it to `atomic.Bool`, or  
2. guard access with the existing manager mutex.

At minimum, add a comment stating the expected single-writer, single-reader pattern.  



Also applies to: 352-353, 965-976

</blockquote></details>
<details>
<summary>block/lazy_aggregation_test.go (3)</summary><blockquote>

`52-70`: **Tests construct a partially-initialised `Manager`**

`setupTestManager` builds a `Manager` with many nil dependencies (store, executor, timers, etc.).  
If future refactors dereference any of those fields, unrelated tests will start panicking.

Consider providing a minimal fake implementation (or using the real constructor with mocks) to avoid hidden coupling between production and test initialisation.

---

`136-159`: **Flaky assertion: relying on channel length**

`assert.Len(pubMock.calls, 0)` races with the goroutine still executing; another publish could slip in between the length check and the assertion, causing intermittent failures.

Prefer a timed `select`:

```go
select {
case <-pubMock.calls:
    t.Fatalf("unexpected publish")
case <-time.After(10 * time.Millisecond):
    // no publish as expected
}
```

---

`214-241`: **`getRemainingSleep` tolerance is too tight**

The ±5 ms delta makes the test fail on slower CI runners.  Consider using a percentage (e.g. 20 %) or `assert.LessOrEqual(sleep, interval)` to keep the test robust across environments.

</blockquote></details>
<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (1)</summary><blockquote>

`23-24`: **Document the deviation from Go error conventions**

The ADR recommends returning a non-nil `BatchData` together with `ErrNoBatch`. This pattern is unusual in Go and may surprise contributors.

Add a short rationale (e.g. _“we intentionally break the 0-value-on-error convention to preserve the timestamp; all callers must handle the non-nil value”_) or reconsider the API as suggested in code review.  



Also applies to: 37-45

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e7472d70bbb2861bef23e5e88380482a6b7a03e6 and 86150790deec6cd4ce59419d5ed94f3a2dd09752.

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `block/aggregation.go` (3 hunks)
* `block/aggregation_test.go` (2 hunks)
* `block/lazy_aggregation_test.go` (1 hunks)
* `block/manager.go` (9 hunks)
* `block/publish_block_test.go` (3 hunks)
* `block/reaper.go` (3 hunks)
* `node/full.go` (1 hunks)
* `node/helpers_test.go` (1 hunks)
* `pkg/cmd/run_node_test.go` (2 hunks)
* `pkg/config/config.go` (3 hunks)
* `pkg/config/config_test.go` (5 hunks)
* `pkg/config/defaults.go` (1 hunks)
* `specs/lazy-adr/adr-007-header-commit-to-shares.md` (0 hunks)
* `specs/lazy-adr/adr-021-lazy-aggregation.md` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* specs/lazy-adr/adr-007-header-commit-to-shares.md

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (3)</summary>

* block/aggregation_test.go
* pkg/config/defaults.go
* pkg/config/config.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (4)</summary>

<details>
<summary>node/full.go (1)</summary><blockquote>

<details>
<summary>block/reaper.go (1)</summary>

* `NewReaper` (34-47)

</details>

</blockquote></details>
<details>
<summary>block/reaper.go (1)</summary><blockquote>

<details>
<summary>block/manager.go (1)</summary>

* `Manager` (96-158)

</details>

</blockquote></details>
<details>
<summary>block/publish_block_test.go (4)</summary><blockquote>

<details>
<summary>test/mocks/external/hstore.go (1)</summary>

* `Store` (13-15)

</details>
<details>
<summary>types/state.go (1)</summary>

* `State` (20-39)

</details>
<details>
<summary>types/signed_header.go (1)</summary>

* `SignedHeader` (22-27)

</details>
<details>
<summary>types/block.go (1)</summary>

* `Data` (33-36)

</details>

</blockquote></details>
<details>
<summary>block/manager.go (5)</summary><blockquote>

<details>
<summary>types/block.go (3)</summary>

* `Data` (33-36)
* `Version` (16-19)
* `Signature` (39-39)

</details>
<details>
<summary>block/errors.go (2)</summary>

* `ErrNoBatch` (17-17)
* `SaveBlockError` (24-26)

</details>
<details>
<summary>types/tx.go (2)</summary>

* `Txs` (7-7)
* `Tx` (4-4)

</details>
<details>
<summary>types/header.go (3)</summary>

* `Hash` (14-14)
* `Header` (35-62)
* `BaseHeader` (25-32)

</details>
<details>
<summary>types/signed_header.go (1)</summary>

* `SignedHeader` (22-27)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

<details>
<summary>block/manager.go</summary>

[failure] 774-774: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

---

[failure] 773-773: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

---

[failure] 732-732: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md</summary>

[uncategorized] ~6-~6: You might be missing the article “the” here.
Context: ...tial draft - 2024-01-24: Revised to use existing empty batch mechanism - 2024-01-25: Upd...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: rollkit-docker / docker-build (GHCR; ghcr.io/rollkit/rollkit)
* GitHub Check: test / Run E2E System Tests
* GitHub Check: test / Build All Rollkit Binaries
* GitHub Check: test / Run Unit Tests
* GitHub Check: test / Run Single Sequencer Tests
* GitHub Check: Analyze (actions)
* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>node/helpers_test.go (1)</summary>

`25-27`: **Updated field name to match the renamed lazy block timing parameter.**

The configuration field has been correctly updated from `LazyBlockTime` to `LazyBlockInterval`, which reflects the renaming of this parameter across the codebase. This change improves naming clarity while maintaining the same 5-second duration value.

</details>
<details>
<summary>node/full.go (1)</summary>

`119-130`: **Improved reaper-manager integration for transaction notifications.**

The reaper initialization has been properly moved after the block manager initialization, and a new connection has been established between them via `reaper.SetManager(blockManager)`. This allows the reaper to notify the manager when new transactions are available, which is essential for the lazy aggregation mechanism.

</details>
<details>
<summary>block/reaper.go (3)</summary>

`30-30`: **Added manager reference to enable transaction notifications.**

This new field allows the Reaper to hold a reference to the Manager, enabling communication about transaction availability.

---

`49-52`: **Added setter method for manager reference.**

Clean implementation of a setter method that allows the dependency to be injected after Reaper creation.

---

`119-123`: **Implemented transaction notification mechanism.**

After submitting new transactions, the reaper now notifies the manager about their availability. This is crucial for the lazy aggregation mechanism to trigger block production when transactions are ready to be included in a block.

The code properly checks both for a non-nil manager and the existence of new transactions before sending the notification.

</details>
<details>
<summary>pkg/config/config_test.go (5)</summary>

`34-35`: **Updated assertion checks to reflect renamed configuration parameters.**

The test has been correctly updated to check for the renamed fields: `LazyMode` (previously `LazyAggregator`) and `LazyBlockInterval` (previously `LazyBlockTime`).

---

`60-62`: **Updated flag assertions to use the renamed configuration fields.**

Flag assertions now correctly reference the renamed configuration properties `LazyMode` and `LazyBlockInterval`.

---

`235-236`: **Updated CLI flag name in test arguments.**

The command-line flag name has been updated to use `lazy_mode` instead of `lazy_aggregator`, consistent with the field renaming.

---

`248-249`: **Updated Viper configuration key to use the renamed field.**

The Viper configuration key has been updated to use `rollkit.node.lazy_mode` to match the renamed field.

---

`257-257`: **Updated comparison comment to reflect the renamed field.**

The comment in the equality assertion has been updated to reference the new field name while maintaining the same comparison logic.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:15:05Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>block/manager.go (1)</summary><blockquote>

`732-740`: _🛠️ Refactor suggestion_

**Proper use of dataHashForEmptyTxs for empty blocks**

The code correctly identifies empty blocks and assigns the special `dataHashForEmptyTxs` to distinguish them from blocks with transactions. This is essential for downstream components to handle empty blocks appropriately.

However, this implementation uses direct concatenation for non-empty blocks rather than a cryptographic hash function:

Replace with a proper digest function for data hash:

```diff
-	dataHash = convertBatchDataToBytes(batchData.Data)
+	hash := sha256.Sum256(convertBatchDataToBytes(batchData.Data))
+	dataHash = hash[:]
```

This would keep the header size bounded and provide a proper cryptographic commitment.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

[failure] 732-732: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>block/manager.go (1)</summary><blockquote>

`732-733`: **Simplify embedded field access**

The static analysis tool flagged a potential simplification opportunity:

```diff
-	isEmpty := batchData.Batch == nil || len(batchData.Batch.Transactions) == 0
+	isEmpty := batchData == nil || batchData.Batch == nil || len(batchData.Transactions) == 0
```

This removes the unnecessary embedded field reference, making the code slightly cleaner.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

[failure] 732-732: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

</details>

</details>

</blockquote></details>
<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (1)</summary><blockquote>

`21-55`: **Well-explained batch retrieval design with sample code**

The modified batch retrieval approach is clearly explained with sample code that demonstrates the key change: returning empty batches with timestamps along with `ErrNoBatch`. This preserves timing information while signaling the absence of transactions.

One minor issue in the sample code:

The sample code could use tabs for indentation to be consistent with Go coding standards:

```diff
-        res, err := m.sequencer.GetNextBatch(ctx, req)
+	res, err := m.sequencer.GetNextBatch(ctx, req)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 86150790deec6cd4ce59419d5ed94f3a2dd09752 and 7fb554a9c5a9e3f7722fc196af7f7f8c543871a1.

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `block/aggregation.go` (3 hunks)
* `block/aggregation_test.go` (2 hunks)
* `block/lazy_aggregation_test.go` (1 hunks)
* `block/manager.go` (9 hunks)
* `block/publish_block_test.go` (3 hunks)
* `block/reaper.go` (3 hunks)
* `node/full.go` (1 hunks)
* `node/helpers_test.go` (1 hunks)
* `pkg/cmd/run_node_test.go` (2 hunks)
* `pkg/config/config.go` (3 hunks)
* `pkg/config/config_test.go` (5 hunks)
* `pkg/config/defaults.go` (1 hunks)
* `specs/lazy-adr/adr-007-header-commit-to-shares.md` (0 hunks)
* `specs/lazy-adr/adr-021-lazy-aggregation.md` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* specs/lazy-adr/adr-007-header-commit-to-shares.md

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (9)</summary>

* block/aggregation_test.go
* node/helpers_test.go
* pkg/config/defaults.go
* pkg/config/config_test.go
* node/full.go
* block/reaper.go
* pkg/cmd/run_node_test.go
* block/aggregation.go
* pkg/config/config.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>block/publish_block_test.go (4)</summary><blockquote>

<details>
<summary>test/mocks/external/hstore.go (1)</summary>

* `Store` (13-15)

</details>
<details>
<summary>types/state.go (1)</summary>

* `State` (20-39)

</details>
<details>
<summary>types/signed_header.go (1)</summary>

* `SignedHeader` (22-27)

</details>
<details>
<summary>types/block.go (1)</summary>

* `Data` (33-36)

</details>

</blockquote></details>
<details>
<summary>block/manager.go (5)</summary><blockquote>

<details>
<summary>types/block.go (3)</summary>

* `Data` (33-36)
* `Version` (16-19)
* `Signature` (39-39)

</details>
<details>
<summary>block/errors.go (2)</summary>

* `ErrNoBatch` (17-17)
* `SaveBlockError` (24-26)

</details>
<details>
<summary>types/tx.go (2)</summary>

* `Txs` (7-7)
* `Tx` (4-4)

</details>
<details>
<summary>types/header.go (3)</summary>

* `Hash` (14-14)
* `Header` (35-62)
* `BaseHeader` (25-32)

</details>
<details>
<summary>types/signed_header.go (1)</summary>

* `SignedHeader` (22-27)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

<details>
<summary>block/manager.go</summary>

[failure] 774-774: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

---

[failure] 773-773: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

---

[failure] 732-732: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md</summary>

[uncategorized] ~6-~6: You might be missing the article “the” here.
Context: ...tial draft - 2024-01-24: Revised to use existing empty batch mechanism - 2024-01-25: Upd...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: rollkit-docker / docker-build (GHCR; ghcr.io/rollkit/rollkit)
* GitHub Check: test / Run Unit Tests
* GitHub Check: test / Build All Rollkit Binaries
* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>block/publish_block_test.go (3)</summary>

`230-232`: **Good: Using atomic for thread-safe DA height tracking**

The use of an atomic variable for the DA height ensures proper synchronization when the value is accessed from different goroutines, which is a good practice in Go.

---

`251-263`: **Thorough test setup with additional required fields**

The test is correctly updated to initialize essential fields in the Manager struct that are needed for the new empty block creation logic. This includes:
- lastState with proper chain identity and height tracking
- headerCache and dataCache for block caching
- HeaderCh and DataCh for communication
- daHeight for DA layer tracking

These changes ensure the test properly simulates a real manager environment.

---

`297-314`: **Key behavior change: Empty batches now trigger full block processing**

This is an important change reflecting the ADR's design. The test now correctly expects that empty batches trigger:
1. SaveBlockData (twice - before and after validation)
2. ExecuteTxs with an empty transaction list 
3. Final state updates

This replaces previous assertions that these methods were NOT called, which is consistent with the design decision to create empty blocks for better DA layer consistency.

</details>
<details>
<summary>block/manager.go (6)</summary>

`64-67`: **Properly defined sentinel hash for empty blocks**

The `dataHashForEmptyTxs` is correctly defined as a fixed-size hash to represent empty blocks, allowing for consistent handling of empty blocks throughout the system. This enables downstream components to recognize empty blocks without needing to access the full block data.

---

`133-134`: **Improved semantics with txsAvailable flag and notification channel**

The replacement of `buildingBlock` with `txsAvailable` provides clearer semantics, and the addition of `txNotifyCh` enables non-blocking notifications of new transaction availability. This is a key part of the lazy aggregation design, allowing the system to react to transaction availability without polling.



Also applies to: 156-157

---

`291-294`: **Clearer naming with LazyBlockInterval**

Renaming from `LazyBlockTime` to `LazyBlockInterval` makes the purpose clearer - it's an interval between blocks rather than a timestamp. This improves code readability and aligns with the concept described in the ADR.

---

`555-573`: **Key implementation of empty block creation**

This code block implements the core decision from the ADR - creating empty blocks when no transactions are present to ensure consistency with the DA layer. The code correctly:

1. Checks for the empty batch case (`errors.Is(err, ErrNoBatch)`)
2. Creates an empty block when a batch with timestamp is available (`if batchData != nil`)
3. Skips block production only when no batch data is available at all

This approach maintains block height consistency while minimizing overhead.

---

`965-976`: **Well-designed non-blocking transaction notification**

The `NotifyNewTransactions` method implements a non-blocking notification pattern to avoid slowing down the transaction submission path. This is excellent since it:

1. Uses a select with default case to make the send non-blocking
2. Includes a comment explaining why this approach was chosen
3. Acknowledges that missing a notification is acceptable since we only need one trigger

This pattern prevents backpressure from the block production subsystem affecting transaction submission.

---

`470-478`: <details>
<summary>❓ Verification inconclusive</summary>

**Unconventional pattern: Returning non-nil data with an error**

The `retrieveBatch` method now returns a fully-populated `*BatchData` along with `ErrNoBatch` for the empty-batch case. This breaks Go's convention where typically if an error is returned, other return values are considered invalid.

While this approach enables your empty block design, it could confuse maintainers or introduce subtle bugs if callers follow the conventional pattern of ignoring return values when errors occur.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for all callers of retrieveBatch to verify they handle the non-nil return with error correctly

echo "Searching for all calls to retrieveBatch:"
rg -A 2 "retrieveBatch\(ctx\)" --type go
```

Length of output: 346

---

🏁 Script executed:

```shell
#!/bin/bash
# Find the implementation of retrieveBatch
rg -R "retrieveBatch" -C 3 --type go
```

Length of output: 441

---



</details>

**Document `retrieveBatch`’s ErrNoBatch behavior**

To avoid confusion around the unconventional pattern of returning a non-nil `*BatchData` alongside `ErrNoBatch`, please:

- Add or update the Go doc comment on the `retrieveBatch` method to clearly state that when no transactions are found, it still returns a valid `*BatchData` and the sentinel error `ErrNoBatch`.
- (Optional) Consider renaming `ErrNoBatch` to something more descriptive (e.g. `ErrEmptyBatch`) to make its semantics self-evident.

fix_required

</details>
<details>
<summary>block/lazy_aggregation_test.go (7)</summary>

`17-50`: **Well-structured mock with thread-safe implementation**

The `mockPublishBlock` implementation is well-designed:

1. Uses proper mutex synchronization for thread safety
2. Provides a buffered channel for tracking calls
3. Includes a reset method to clear call history between tests
4. Handles optional delay to simulate processing time
5. Implements non-blocking channel sends to avoid test deadlocks

This provides a robust foundation for testing asynchronous behavior.

---

`73-113`: **Comprehensive test for block timer-triggered production**

This test effectively verifies that blocks are published when the block timer fires and transactions are available. It:

1. Sets up a controlled environment with specific timer durations
2. Explicitly sets `txsAvailable` to true to simulate available transactions
3. Uses context cancellation for clean test termination
4. Verifies that at least one block was published

The test design allows for reliable verification of the timer-based behavior.

---

`116-162`: **Thorough test of lazy timer with verification of expected behavior**

This test carefully validates the lazy timer's behavior by:

1. Setting larger block timer vs. smaller lazy timer to ensure lazy timer fires first
2. Setting txsAvailable to false to ensure block timer doesn't trigger publication
3. Using channel selection with timeouts to robustly verify timing expectations
4. Checking lazy timer reset behavior with the second publication
5. Verifying no additional unexpected publications occurred

This is a high-quality test for a complex timing-dependent behavior.

---

`165-211`: **Good resilience testing for error handling**

This test ensures the lazy aggregation loop continues operating after encountering errors:

1. Injects an error via the mock
2. Verifies the first publish attempt occurs despite the error
3. Removes the error and verifies the second attempt succeeds
4. Uses appropriate timeouts for robust timing verification

Testing error resilience is crucial for production systems, and this test does it well.

---

`214-241`: **Accurate timer sleep calculation tests**

This test verifies the correct calculation of remaining sleep durations based on elapsed time, which is crucial for maintaining consistent block production schedules. It properly tests:

1. Normal case where elapsed time < interval
2. Edge case where elapsed time > interval
3. Boundary case where elapsed time = interval
4. Special case with zero elapsed time

The use of delta assertions is appropriate for time-based checks.

---

`244-309`: **Complete test for transaction notification in lazy mode**

This test thoroughly verifies that transaction notifications correctly trigger block production in lazy mode:

1. Sets up realistic timers and environment
2. Waits for the initial lazy timer to trigger block production 
3. Resets tracking state and sends a transaction notification
4. Verifies that a block is published after the notification
5. Tests multiple notifications to ensure consistent behavior

The test structure demonstrates a good understanding of the lazy aggregation mechanism.

---

`347-403`: **Important verification of normal mode behavior difference**

This test verifies the critical distinction in how transaction notifications are handled in normal (non-lazy) mode:

1. Correctly sets up a non-lazy mode environment
2. Verifies that the first block is published by the regular timer
3. Sends a transaction notification and confirms it does NOT trigger immediate block production
4. Verifies that blocks still get published by the regular timer

This test ensures that the notification mechanism doesn't change behavior in normal mode, which is an important constraint.

</details>
<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (5)</summary>

`1-11`: **Well-structured ADR with clear context for the change**

The ADR provides excellent context about the consistency issue between Rollkit's lazy aggregation and the DA layer, clearly identifying the problem of block height mismatch due to skipped empty blocks. This establishes a solid foundation for the proposed solution.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~6-~6: You might be missing the article “the” here.
Context: ...tial draft - 2024-01-24: Revised to use existing empty batch mechanism - 2024-01-25: Upd...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

---

`13-15`: **Concise decision leveraging existing mechanisms**

The decision to leverage the existing empty batch mechanism and `dataHashForEmptyTxs` is pragmatic and minimizes code changes while solving the identified problem.

---

`144-151`: **Clear key changes summary**

This section effectively summarizes the key technical aspects of the design:
1. Returning batches with timestamps even when empty
2. Using the existing dataHashForEmptyTxs for empty blocks
3. Leveraging current sync mechanisms
4. The dual timer design (block timer and lazy timer)
5. Transaction availability tracking

This provides a concise reference of the major implementation points.

---

`152-159`: **Thoughtful efficiency considerations**

The ADR addresses important efficiency concerns that might arise with this design:
- Minimal DA layer overhead for empty blocks
- Reuse of existing mechanisms
- Proper block timing
- Intelligent timer management
- Non-blocking notifications

This shows thorough consideration of performance implications.

---

`166-184`: **Balanced consequences analysis**

The ADR presents a balanced view of the consequences:

Positive:
- Consistent block heights
- Leveraging existing mechanisms
- Simpler than alternatives
- Preserves timing and reduces latency

Negative:
- Small DA overhead
- Additional timer complexity

Neutral:
- Timestamp handling requirements
- Backward compatibility considerations

This comprehensive analysis helps readers understand the tradeoffs.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:32:10Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (5)</summary><blockquote>

<details>
<summary>block/manager.go (5)</summary><blockquote>

`830-861`: _🛠️ Refactor suggestion_

**`DataHash` should be a digest, not raw concatenation**

`convertBatchDataToBytes(batchData.Data)` concatenates length-prefixed blobs and stores the raw bytes in `Header.DataHash`. This makes the header size unbounded and undermines the purpose of a hash field (fixed-width commitment).

 
```diff
func convertBatchDataToBytes(batchData [][]byte) []byte {
    // If batchData is nil or empty, return an empty byte slice
    if len(batchData) == 0 {
        return []byte{}
    }

+   // Create a concatenated representation for hashing
    // Format: 4 bytes (length) + data for each entry
    totalSize := 0
    for _, data := range batchData {
        totalSize += 4 + len(data) // 4 bytes for length prefix + data length
    }

    // Allocate buffer with calculated capacity
    result := make([]byte, 0, totalSize)

    // Add length-prefixed data
    for _, data := range batchData {
        // Encode length as 4-byte big-endian integer
        lengthBytes := make([]byte, 4)
        binary.LittleEndian.PutUint32(lengthBytes, uint32(len(data)))

        // Append length prefix
        result = append(result, lengthBytes...)

        // Append actual data
        result = append(result, data...)
    }

+   // Hash the concatenated data to create a fixed-size digest
+   hash := sha256.Sum256(result)
+   return hash[:]
-   return result
}
```

Remember to add the `crypto/sha256` import.

---

`470-478`: _⚠️ Potential issue_

**Avoid returning a non-nil value together with an error**

`retrieveBatch` now returns a fully-populated `*BatchData` **and** `ErrNoBatch` for the empty-batch case. In Go, it's conventional that if `err != nil`, the other return values are ignored. Callers following this convention may discard the batch timestamp you rely on, defeating the purpose of this change.

Consider either:
1. Adding a boolean flag to indicate empty batches
2. Defining a special error type that doesn't represent a failure

 
```diff
-			return &BatchData{
-				Batch: res.Batch,
-				Time:  res.Timestamp,
-				Data:  res.BatchData,
-			}, ErrNoBatch
+			// Option 1: Use a boolean flag to indicate empty batches
+			emptyBatch := &BatchData{
+				Batch: res.Batch,
+				Time:  res.Timestamp,
+				Data:  res.BatchData,
+			}
+			return emptyBatch, nil, true
```

Or define a special error type:
```go
// EmptyBatchSignal indicates an empty batch that should still be processed
type EmptyBatchSignal struct{}

func (e EmptyBatchSignal) Error() string {
    return "empty batch (not an error)"
}

// IsEmptyBatchSignal checks if an error is just a signal for an empty batch
func IsEmptyBatchSignal(err error) bool {
    _, ok := err.(EmptyBatchSignal)
    return ok
}
```

---

`565-568`: _⚠️ Potential issue_

**Duplicate `SaveBlockData` calls write an unsigned header first**

The empty-block branch writes the block to the store before the genuine proposer signature is calculated, then rewrites it again after `applyBlock`. Persisting an unsigned header, even transiently, risks other components indexing or gossiping an invalid block.

 
Remove the early write entirely as suggested in the previous comment.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 567-568: block/manager.go#L567-L568
Added lines #L567 - L568 were not covered by tests

</details>

</details>

---

`471-476`: _⚠️ Potential issue_

**`lastBatchData` not updated for empty batches**

Inside the empty-batch branch, the code skips persisting `LastBatchDataKey` and leaves `m.lastBatchData` untouched. If the sequencer keeps returning the same empty batch, the manager will treat every retrieval as "new", potentially producing an unbounded stream of empty blocks.

 
```diff
			// Even if there are no transactions, return the batch with timestamp
			// This allows empty blocks to maintain proper timing
			if len(res.Batch.Transactions) == 0 {
+				h := convertBatchDataToBytes(res.BatchData)
+				if err := m.store.SetMetadata(ctx, LastBatchDataKey, h); err != nil {
+					m.logger.Error("error while setting last batch hash for empty batch", "error", err)
+				}
+				m.lastBatchData = res.BatchData
				return &BatchData{
					Batch: res.Batch,
					Time:  res.Timestamp,
					Data:  res.BatchData,
				}, ErrNoBatch
			}
```

---

`555-573`: _🛠️ Refactor suggestion_

**Good approach for handling empty blocks**

The implementation correctly creates and processes empty blocks when no transactions are available but a batch with timestamp exists. This aligns with the ADR's goal of maintaining block height consistency.

However, there's duplicate code for saving block data. The empty block case saves data immediately after creation, then the block gets saved again after processing in line 636.


```diff
-				err = m.store.SaveBlockData(ctx, header, data, &signature)
-				if err != nil {
-					return SaveBlockError{err}
-				}
```

This removes the duplicate save operation, ensuring the block is only saved with a proper signature after successful validation and application.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 562-563: block/manager.go#L562-L563
Added lines #L562 - L563 were not covered by tests

---

[warning] 567-568: block/manager.go#L567-L568
Added lines #L567 - L568 were not covered by tests

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>block/manager.go (1)</summary><blockquote>

`65-66`: **Clarify the purpose and generation method of `dataHashForEmptyTxs`**

This constant appears to be a predefined hash used for empty blocks. However, it's not clear how this value was generated or what guarantees it provides (uniqueness, determinism).

Consider adding a comment explaining:
1. How this value was derived
2. Whether it's a cryptographic hash or a deterministic value
3. Any guarantees it provides for empty block identification


```diff
var (
-	// dataHashForEmptyTxs to be used while only syncing headers from DA and no p2p to get the Data for no txs scenarios, the syncing can proceed without getting stuck forever.
+	// dataHashForEmptyTxs is a predefined hash value representing empty transaction sets.
+	// It serves two purposes:
+	// 1. Allows block syncing to proceed without getting stuck when no transactions are available
+	// 2. Provides a consistent identifier for empty blocks to maintain height consistency
+	// This value is the SHA-256 hash of a canonical empty transaction set representation.
	dataHashForEmptyTxs = []byte{110, 52, 11, 156, 255, 179, 122, 152, 156, 165, 68, 230, 187, 120, 10, 44, 120, 144, 29, 63, 179, 55, 56, 118, 133, 17, 163, 6, 23, 175, 160, 29}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7fb554a9c5a9e3f7722fc196af7f7f8c543871a1 and 4219dd261c97c6e6629ab90571550bf37290fde4.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `block/manager.go` (10 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>block/manager.go (6)</summary><blockquote>

<details>
<summary>types/block.go (2)</summary>

* `Data` (33-36)
* `Version` (16-19)

</details>
<details>
<summary>block/errors.go (2)</summary>

* `ErrNoBatch` (17-17)
* `SaveBlockError` (24-26)

</details>
<details>
<summary>types/tx.go (2)</summary>

* `Txs` (7-7)
* `Tx` (4-4)

</details>
<details>
<summary>types/header.go (3)</summary>

* `Hash` (14-14)
* `Header` (35-62)
* `BaseHeader` (25-32)

</details>
<details>
<summary>types/signed_header.go (1)</summary>

* `SignedHeader` (22-27)

</details>
<details>
<summary>types/state.go (1)</summary>

* `State` (20-39)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>block/manager.go</summary>

[warning] 291-291: block/manager.go#L291
Added line #L291 was not covered by tests

---

[warning] 293-293: block/manager.go#L293
Added line #L293 was not covered by tests

---

[warning] 352-352: block/manager.go#L352
Added line #L352 was not covered by tests

---

[warning] 359-359: block/manager.go#L359
Added line #L359 was not covered by tests

---

[warning] 517-517: block/manager.go#L517
Added line #L517 was not covered by tests

---

[warning] 562-563: block/manager.go#L562-L563
Added lines #L562 - L563 were not covered by tests

---

[warning] 567-568: block/manager.go#L567-L568
Added lines #L567 - L568 were not covered by tests

---

[warning] 580-581: block/manager.go#L580-L581
Added lines #L580 - L581 were not covered by tests

---

[warning] 585-586: block/manager.go#L585-L586
Added lines #L585 - L586 were not covered by tests

---

[warning] 591-592: block/manager.go#L591-L592
Added lines #L591 - L592 were not covered by tests

---

[warning] 917-917: block/manager.go#L917
Added line #L917 was not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>block/manager.go (7)</summary>

`156-157`: **Good implementation of non-blocking notification channel**

The addition of `txNotifyCh` with a buffer size of 1 is a good design choice for signaling transaction availability without blocking the transaction submission path.

---

`731-740`: **Good use of special hash for empty blocks**

The implementation properly identifies empty blocks and uses the predefined `dataHashForEmptyTxs` value to mark them. This allows for consistent identification and special handling of empty blocks throughout the system.

---

`767-777`: **Clean implementation of conditional transaction handling**

The code properly handles both empty and non-empty blocks, only adding transactions to the block data when there are transactions available in the batch.

---

`782-796`: **Good modularization of execution methods**

Breaking down the execution methods into separate functions (`applyBlock`, `execValidate`, `execCommit`) improves code organization and testability.

---

`910-921`: **Well-designed non-blocking notification mechanism**

The `NotifyNewTransactions` method is elegantly implemented:
1. It uses a non-blocking send with `select` and `default` to avoid slowing down transaction submission
2. It properly handles the case when a notification is already pending
3. It's well-commented and explains the design rationale

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 917-917: block/manager.go#L917
Added line #L917 was not covered by tests

</details>

</details>

---

`291-294`: **Good configuration parameter renaming**

Renaming from `LazyBlockTime` to `LazyBlockInterval` improves clarity and better reflects the parameter's purpose.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 291-291: block/manager.go#L291
Added line #L291 was not covered by tests

---

[warning] 293-293: block/manager.go#L293
Added line #L293 was not covered by tests

</details>

</details>

---

`577-581`: **Good timestamp validation check**

The addition of a timestamp validation check prevents blocks with invalid timestamps from being created, ensuring monotonically increasing timestamps in the blockchain.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 580-581: block/manager.go#L580-L581
Added lines #L580 - L581 were not covered by tests

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:39:59Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>block/manager.go (3)</summary><blockquote>

`470-482`: **The error handling pattern still needs to be fixed**

`retrieveBatch` still returns a fully-populated `*BatchData` together with `ErrNoBatch` for empty batches. This violates Go's convention where if `err != nil`, the other return values are typically ignored.


Consider one of these alternatives:
- Add a boolean field to `BatchData` to indicate empty batches
- Create a custom sentinel error type that doesn't represent a failure
- Return different error types for "no batch" vs "empty batch"

Whatever approach you choose, ensure it's clearly documented and all callers are updated accordingly.

---

`568-571`: **Block storage is still occurring before signature calculation**

The block is being saved to storage before calculating the proposer signature (which happens at line 608). This risks other components indexing or gossiping an invalid block.


Consider one of these options:
- Remove this early write entirely (preferred)
- Move this block to after the signature is calculated and validated

---

`736-743`: **`DataHash` calculation still uses raw concatenation**

For non-empty blocks, the code is still using raw concatenation for the `dataHash` rather than a proper cryptographic digest.


Replace with an actual digest to maintain a fixed-width commitment:

```diff
-	dataHash = convertBatchDataToBytes(batchData.Data)
+	hash := sha256.Sum256(convertBatchDataToBytes(batchData.Data))
+	dataHash = hash[:]
```

Remember to add the `crypto/sha256` import.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>block/manager.go (1)</summary><blockquote>

`785-800`: **Consider implementing `execValidate` method**

The `execValidate` method is currently a stub with a TODO. While not critical for the current PR implementation, consider implementing proper validation logic to ensure block integrity.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4219dd261c97c6e6629ab90571550bf37290fde4 and 4949f09aeecb2fac627259f1e1f7bfcb5d879f81.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `block/aggregation.go` (3 hunks)
* `block/manager.go` (10 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* block/aggregation.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>block/manager.go (4)</summary>

`557-577`: **Empty block handling implementation looks good**

The implementation for creating empty blocks when needed keeps proper block timing and maintains block height consistency with the DA layer. This directly addresses the PR objective of ensuring a one-to-one block mapping required by EVM environments.

---

`734-743`: **Good implementation of empty block hash handling**

The explicit check for empty blocks and the use of a predefined `dataHashForEmptyTxs` constant is a clean approach for handling empty blocks.

---

`913-924`: **Well-designed transaction notification system**

The `NotifyNewTransactions` implementation is efficient with:
- Non-blocking channel send to avoid slowing down the transaction path
- Buffer of size 1 to prevent duplicate notifications
- Clear comments explaining the behavior

This complements the lazy aggregation design by allowing the system to react promptly to new transactions.

---

`791-794`: **Add validation logic for empty blocks**

When implementing the `execValidate` method, ensure it properly validates empty blocks with the sentinel hash, confirming the hash matches `dataHashForEmptyTxs` when a block contains no transactions.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:48:48Z
user: login: Manav-Aggarwal
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-02T16:12:52Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:13:20Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>block/manager.go (1)</summary><blockquote>

`476-481`: **Ensure consistent error handling across callers.**

The method now returns a fully-populated `*BatchData` **and** `ErrNoBatch` for the empty-batch case. This pattern deviates from Go conventions where return values are typically ignored when an error is non-nil.

Make sure all callers of this method understand this unique behavior and don't discard the batch data when checking for `ErrNoBatch`.


```shell
#!/bin/bash
# Find all callers of retrieveBatch to verify they handle the error correctly

echo "Searching for calls to retrieveBatch:"
rg "retrieveBatch\(" --type go -A 10 -B 2

echo -e "\nSearching for error handling of ErrNoBatch:"
rg "ErrNoBatch" --type go -A 5 -B 2
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>block/manager.go (1)</summary><blockquote>

`64-67`: **Consider documenting the source of `dataHashForEmptyTxs` value.**

The predefined hash for empty transactions is crucial for the lazy aggregation mechanism, but there's no explanation of how this value was derived. Consider adding a comment explaining whether this is:
1. A hash of an empty transaction set
2. A special sentinel value chosen arbitrarily
3. Derived from some other source

This would help maintainers understand the significance of this specific byte sequence.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4949f09aeecb2fac627259f1e1f7bfcb5d879f81 and 03aa3e42d28139cb6346845e15c4d8058ec20d4d.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `block/manager.go` (11 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: rollkit-docker / docker-build (GHCR; ghcr.io/rollkit/rollkit)
* GitHub Check: test / Run Unit Tests
* GitHub Check: test / Build All Rollkit Binaries
* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>block/manager.go (12)</summary>

`133-134`: **Improved flag naming for better semantics.**

Changed `buildingBlock` to `txsAvailable` to more accurately reflect that the flag indicates transaction availability rather than a block building state.

---

`156-157`: **Good addition of notification channel for transaction availability.**

The buffered channel `txNotifyCh` enables non-blocking signaling when new transactions are available, supporting the lazy aggregation mechanism described in the ADR.

---

`291-294`: **Configuration parameter renaming for clarity.**

Changed `LazyBlockTime` to `LazyBlockInterval` for better semantic clarity, aligning with the terminology used in the lazy aggregation implementation.

---

`359-360`: **Proper initialization of notification channel.**

The `txNotifyCh` is initialized as a buffered channel with size 1, allowing for non-blocking notifications, as described in the comment.

---

`465-482`: **Improved batch retrieval to support empty blocks.**

This change ensures that empty batches are properly processed, maintaining the DA layer's timestamp consistency. The code now:
- Returns batches with timestamps even when empty
- Correctly persists `LastBatchDataKey` and updates `m.lastBatchData` for all batches
- Returns appropriate error to signal empty batch condition

This implementation directly supports the ADR's approach for handling empty blocks with sentinel transactions.

---

`550-569`: **Implemented empty block creation for consistent block heights.**

The modified logic creates empty blocks when batches contain no transactions, using a predefined `dataHashForEmptyTxs` constant as a sentinel marker. Only skips block production if no batch is retrieved at all.

This addresses the core issue described in the PR objectives - maintaining consistency between DA layer blocks and execution layer blocks by ensuring empty blocks are properly represented.

---

`571-588`: **Improved block creation process for non-empty blocks.**

The code now checks timestamps to ensure monotonically increasing times and properly creates blocks with transactions when available. This complements the empty block handling and maintains consistent behavior across both empty and non-empty blocks.

---

`710-739`: **Enhanced block creation with proper empty block detection.**

The implementation now:
1. Verifies the signer's identity against the genesis proposer address
2. Explicitly determines if a block is empty
3. Uses a special data hash for empty blocks (`dataHashForEmptyTxs`)

This approach minimizes DA layer costs while maintaining block height consistency, aligning with the ADR's goals.

---

`764-775`: **Clean implementation of conditional transaction handling.**

The code creates an appropriate transaction list based on whether the block is empty, ensuring that empty blocks have no transactions but maintain their marker through the data hash.

---

`780-795`: **Good refactoring of block application logic.**

Breaking down the block application into smaller, focused methods (`applyBlock`, `execValidate`, `execCommit`) improves code organization and maintainability. Each method has a clear single responsibility.

---

`908-919`: **Well-implemented transaction notification mechanism.**

The `NotifyNewTransactions` method uses a non-blocking channel send to avoid slowing down the transaction submission path. This is an excellent pattern for signaling without causing backpressure.

This method will be called by the Reaper when new transactions are received, allowing the lazy aggregation loop to react to transaction availability immediately rather than waiting for the next timer interval.

---

`729-738`: <details>
<summary>✅ Verification successful</summary>

**Verify that `dataHashForEmptyTxs` is used consistently.**

For empty blocks, a predefined data hash is used rather than computing one. While this works for marking empty blocks, ensure this special value is recognized consistently throughout the codebase, particularly in block verification logic.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for all usages of dataHashForEmptyTxs to verify consistent handling

# Find all references to the dataHashForEmptyTxs constant
echo "Searching for references to dataHashForEmptyTxs:"
rg "dataHashForEmptyTxs" --type go

# Look for any block validation or verification code that might need to handle this special hash
echo -e "\nSearching for block validation code that should handle empty blocks:"
rg "ValidateBasic|Validate.*Block|verify.*Block" --type go
```

Length of output: 3064

---



</details>

**dataHashForEmptyTxs handling verified**

- block/manager.go: `dataHashForEmptyTxs` is defined and used for empty‐block hashing.  
- block/sync.go: sync logic explicitly checks `header.DataHash == dataHashForEmptyTxs`.  
- block/sync_test.go: tests cover the empty‐block data hash path.  

No other block‐validation or verification routines need special‐case handling of this hash.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:14:12Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (1)</summary><blockquote>

`182-188`: **Include issue reference in the ADR**

The ADR should include a reference to the GitHub issue (#2193) mentioned in the PR description for better traceability.


Consider adding:
```diff
## References

- [Block Manager Implementation](block/manager.go)
- [Block Aggregation Implementation](block/aggregation.go)
- [Lazy Aggregation Tests](block/lazy_aggregation_test.go)
+- [GitHub Issue #2193](https://github.com/rollkit/rollkit/issues/2193)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 03aa3e42d28139cb6346845e15c4d8058ec20d4d and 1c149e0ab6e3e272f70ed158326e166e1c03ac4e.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `block/manager.go` (11 hunks)
* `specs/lazy-adr/adr-021-lazy-aggregation.md` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>block/manager.go (6)</summary><blockquote>

<details>
<summary>block/errors.go (2)</summary>

* `ErrNoBatch` (17-17)
* `SaveBlockError` (24-26)

</details>
<details>
<summary>types/block.go (3)</summary>

* `Data` (33-36)
* `Signature` (39-39)
* `Version` (16-19)

</details>
<details>
<summary>types/tx.go (2)</summary>

* `Txs` (7-7)
* `Tx` (4-4)

</details>
<details>
<summary>types/header.go (3)</summary>

* `Hash` (14-14)
* `Header` (35-62)
* `BaseHeader` (25-32)

</details>
<details>
<summary>types/state.go (1)</summary>

* `State` (20-39)

</details>
<details>
<summary>types/signed_header.go (1)</summary>

* `SignedHeader` (22-27)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md</summary>

[uncategorized] ~6-~6: You might be missing the article “the” here.
Context: ...tial draft - 2024-01-24: Revised to use existing empty batch mechanism - 2024-01-25: Upd...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: test / Build All Rollkit Binaries
* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>block/manager.go (11)</summary>

`133-133`: **Appropriate flag for tracking transaction availability**

The `txsAvailable` boolean flag replaces the previous `buildingBlock` flag, providing a clearer, more meaningful representation of transaction state for lazy aggregation.

---

`156-157`: **Well-designed notification channel for transaction availability**

The buffered channel design allows non-blocking notifications when new transactions arrive, which is crucial for preventing backpressure in the transaction submission path.

---

`291-293`: **Appropriate rename from LazyBlockTime to LazyBlockInterval**

This naming change better reflects the purpose of this configuration parameter, matching the terminology used in the ADR.

---

`352-352`: **Proper initialization of lazy aggregation components**

The `txsAvailable` flag is correctly initialized to false, and the notification channel has an appropriate buffer size of 1, consistent with the design in the ADR.



Also applies to: 359-359

---

`465-483`: **Modified batch retrieval for empty blocks consistency**

The implementation correctly returns batch data with timestamp even for empty batches, using `ErrNoBatch` to indicate emptiness. This maintains proper timing for empty blocks and ensures consistent block heights.

Importantly, the implementation now persists metadata and updates `lastBatchData` even for empty batches, preventing duplicate empty block emissions.

---

`553-571`: **Empty block creation with proper error handling**

The code now properly handles empty batches by creating empty blocks with timestamps, which maintains block height consistency with the DA layer. The distinction between an empty batch and no batch at all is well-managed, with appropriate logging.

---

`574-591`: **Timestamp validation for batch data**

The code includes an important timestamp validation to ensure monotonically increasing block times, preventing potential consensus or application-level issues.

---

`732-741`: **Clear data hash handling for empty blocks**

The implementation uses the special `dataHashForEmptyTxs` constant for empty blocks, which allows downstream components to identify empty blocks without requiring data retrieval.

---

`767-778`: **Proper initialization of transaction data for empty blocks**

The code initializes the transaction list appropriately based on whether the block is empty, avoiding unnecessary allocations for empty blocks.

---

`783-797`: **Improved code organization with extracted methods**

Breaking down block execution logic into separate methods (`applyBlock`, `execValidate`, `execCommit`) improves modularity and maintainability.

---

`911-922`: **Efficient non-blocking transaction notification**

The `NotifyNewTransactions` method uses a non-blocking channel send operation with a default case, ensuring that the transaction submission path is never blocked by the notification process. This design prevents backpressure and maintains system responsiveness.

</details>
<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (6)</summary>

`1-13`: **Well-structured ADR with clear context and problem statement**

The ADR effectively describes the problem of inconsistency between Rollkit's lazy aggregation mechanism and the DA layer, particularly for EVM environments that expect a 1:1 mapping between DA layer blocks and execution layer blocks.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~6-~6: You might be missing the article “the” here.
Context: ...tial draft - 2024-01-24: Revised to use existing empty batch mechanism - 2024-01-25: Upd...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

---

`14-15`: **Concise decision statement leveraging existing mechanisms**

The decision to use the existing empty batch mechanism with `dataHashForEmptyTxs` is a pragmatic approach that minimizes complexity and avoids introducing new data structures.

---

`21-53`: **Accurate documentation of batch retrieval modifications**

The code example in the ADR correctly illustrates the key changes made to the `retrieveBatch` method, including returning batches with timestamps even when empty and using `ErrNoBatch` to signal emptiness.

---

`85-116`: **Well-described lazy aggregation loop with dual timers**

The ADR clearly explains the purpose and operation of the dual timer mechanism (`lazyTimer` and `blockTimer`), which is crucial for understanding the block production timing.

---

`142-157`: **Comprehensive summary of key changes and efficiency considerations**

This section effectively summarizes the important changes and efficiency considerations, providing a quick reference for developers.

---

`158-181`: **Balanced analysis of consequences**

The ADR presents a thoughtful analysis of the positive, negative, and neutral consequences of the implementation, showing a thorough consideration of the design's impact.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:32:42Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:36:08Z
user: login: coderabbitai[bot]
body: > [!CAUTION]
> Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.  If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

**Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>block/manager.go (3)</summary><blockquote>

`470-475`: **Improve error handling clarity**

The current implementation always sets `errRetrieveBatch` to `ErrNoBatch` for empty batches, but could be more explicit about setting it to `nil` for non-empty batches. This would make the logic clearer and follow common Go error handling patterns.


```diff
var errRetrieveBatch error
// Even if there are no transactions, return the batch with timestamp
// This allows empty blocks to maintain proper timing
if len(res.Batch.Transactions) == 0 {
    errRetrieveBatch = ErrNoBatch
+} else {
+    errRetrieveBatch = nil
}
```

---

`567-567`: **Avoid indirect embedded field reference**

The code uses the embedded `Batch` field indirectly, which triggered a static analysis warning. For better code clarity and to follow Go's best practices, access the transactions directly through the embedded field.


```diff
-m.logger.Debug("block info", "num_tx", len(batchData.Batch.Transactions))
+m.logger.Debug("block info", "num_tx", len(batchData.Transactions))
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

[failure] 567-567: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

</details>

</details>

---

`897-908`: **Consider adding metrics for transaction notifications**

The non-blocking notification channel design is excellent to prevent backpressure on transaction submission, but it would be useful to track how many notifications are dropped when the channel is full.


Consider adding a metric that's incremented when a notification can't be sent because the channel is full:

```diff
func (m *Manager) NotifyNewTransactions() {
    // Non-blocking send to avoid slowing down the transaction submission path
    select {
    case m.txNotifyCh <- struct{}{}:
        // Successfully sent notification
    default:
        // Channel buffer is full, which means a notification is already pending
        // This is fine, as we just need to trigger one block production
+        if m.metrics != nil {
+            m.metrics.DroppedTxNotifications.Inc()
+        }
    }
}
```

This would require adding a `DroppedTxNotifications` counter to your metrics structure.

</blockquote></details>
<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (1)</summary><blockquote>

`26-52`: **Fix request parameter inconsistency in code example**

The ADR code example doesn't show the initialization of the `req` parameter used in the `GetNextBatch` call, making it inconsistent with the actual implementation. This could confuse developers.


```diff
func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+    req := coresequencer.GetNextBatchRequest{
+        RollupId:      []byte(m.genesis.ChainID),
+        LastBatchData: m.lastBatchData,
+    }
+
    res, err := m.sequencer.GetNextBatch(ctx, req)
    if err != nil {
        return nil, err
    }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🛑 Comments failed to post (1)</summary><blockquote>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (1)</summary><blockquote>

129-144: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Is the solution specific to EVM environments?**

The ADR mentions that this solution addresses the "expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments," but doesn't clarify if this is also necessary for ABCI environments. Could you add a note clarifying whether this approach is specifically needed for EVM or if it benefits all execution environments?

---

🌐 Web query:

```web
Are consistent block heights between DA and execution layers primarily a requirement for EVM environments or also necessary for ABCI applications?
```

💡 Result:

Consistent block heights between data availability (DA) and execution layers are critical for **both EVM environments and ABCI-based applications**, though the mechanisms for maintaining consistency differ based on their architectural frameworks.

### **EVM Environments**
In **EVM chains**, block height acts as a linear sequence counter for transaction ordering and state transitions. While the search results don't explicitly address DA layers in EVM, maintaining height consistency between execution and DA layers ensures:
- **Deterministic transaction ordering**: Prevents forks or conflicts in state transitions.
- **Synchronization**: Ensures execution nodes process blocks in the correct sequence by referencing DA layer data.

### **ABCI Applications**
For **ABCI-based chains** (e.g., Cosmos SDK, Babylon Chain), block height consistency is **explicitly enforced** through protocol design and synchronization logic:
1. **Handshake Protocol**:  
   - Nodes use `Info` requests to sync the `LastBlockHeight` and `LastBlockAppHash` on restart, ensuring both Tendermint (consensus/DA) and the application layer agree on the latest state[6][9].
   - Example: Tendermint replays blocks from the last agreed height if discrepancies arise[6].

2. **Cross-Layer Validation**:  
   - In ABCI++, validators use `ExtendVote` and `VerifyVoteExtension` to commit data (e.g., DA proofs) from other chains, which relies on height consistency to validate cross-chain transactions[9].
   - Projects like Babylon Chain require strict height alignment to prevent panics in finality modules, especially during delegation status checks and block processing[7].

3. **Consensus and Security**:  
   - Inconsistent heights in ABCI applications can lead to critical failures, such as unintended event processing (e.g., misordered `ACTIVATED`/`EXPIRED` events) or chain halts[7].
   - ABCI's `ProcessProposal` and `FinalizeBlock` ensure execution layers process only blocks validated and agreed upon by the DA layer[9].

### **Comparison Table**
| Aspect                | EVM Environments       | ABCI Applications               |
|-----------------------|------------------------|----------------------------------|
| **Height Sync**       | Implicit via consensus | Explicit handshake protocols[6] |
| **Cross-Layer Proofs**| Not inherently defined | Vote extensions (ABCI++)[9]     |
| **Failure Scenarios** | Forks/state mismatches | Node panics, chain halts[7][9]  |

While **EVM chains implicitly depend on height consistency**, ABCI applications **explicitly enforce it** through protocol-level safeguards, making it a **universal requirement** for secure and deterministic blockchain operation.

Citations:

- 1: https://www.nadcab.com/blog/blockchain-block-height
- 2: https://www.cnbctv18.com/cryptocurrency/blockchain-height-size-time-and-other-specifications-of-a-block-14835241.htm
- 3: https://www.nadcab.com/blog/block-height-affects-bitcoin-development
- 4: https://www.techscience.com/csse/v36n2/41119/html
- 5: https://fastercapital.com/content/Block-Height--Building-an-Immutable-Ledger-in-Blockchain.html
- 6: https://cosmos-network.gitbooks.io/cosmos-academy/content/cosmos-for-developers/tendermint/abci-protocol.html
- 7: https://docs.babylonlabs.io/assets/files/zellic_babylon_genesis_chain_audit_2025_03.pdf
- 8: https://accountinginsights.org/what-is-block-height-and-why-does-it-matter-in-cryptocurrency/
- 9: https://informal.systems/blog/abci-v2-unlocks-this
- 10: https://scholar.tecnico.ulisboa.pt/api/records/CEQgJJ9bDvRHeM_7fndCvrGp_uqncSgcWKvv/file/affe2b7fc56d930ca931ad37b80064e48970ec031ae49a1109dc10d223d89583.pdf

---



</details>

**Clarify Universal Block Height Consistency Requirement**

This ADR currently calls out EVM chains but block‐height alignment is critical for **both** EVM and ABCI‐based execution environments. Please update the explanatory note in `specs/lazy-adr/adr-021-lazy-aggregation.md` as follows:

• File: `specs/lazy-adr/adr-021-lazy-aggregation.md`  
  – Around the sentence:  
    “This solution addresses the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.”

Suggested diff:
```diff
- This solution addresses the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+ This solution addresses the expected 1:1 mapping between DA layer blocks and execution layer blocks across all execution environments.  
+ In EVM chains, this ensures deterministic transaction ordering and synchronization, while in ABCI chains it is explicitly enforced via Tendermint’s handshake protocols (e.g., `Info` requests for `LastBlockHeight`/`LastBlockAppHash`) and vote‐extension mechanisms (`ExtendVote`/`VerifyVoteExtension`).
```



> Committable suggestion skipped: line range outside the PR's diff.

</blockquote></details>

</blockquote></details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:38:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (1)</summary><blockquote>

`25-53`: **Batch retrieval mechanism needs parameter definition**

In the code example, the variable `req` is used in line 27 but isn't defined within the snippet. This could cause confusion for developers implementing this pattern.

```diff
-        res, err := m.sequencer.GetNextBatch(ctx, req)
+        req := coresequencer.GetNextBatchRequest{
+            RollupId:      []byte(m.genesis.ChainID),
+            LastBatchData: m.lastBatchData,
+        }
+        res, err := m.sequencer.GetNextBatch(ctx, req)
```

</blockquote></details>
<details>
<summary>block/manager.go (1)</summary><blockquote>

`470-483`: **Handle empty batches with care**

The modified `retrieveBatch` method correctly returns batches with timestamps even when empty, enabling consistent block timing. However, returning a non-nil value with an error (`ErrNoBatch`) breaks Go's error handling convention and could cause confusion.

Consider using a more explicit approach to signal empty batches:

```diff
-		var errRetrieveBatch error
-		// Even if there are no transactions, return the batch with timestamp
-		// This allows empty blocks to maintain proper timing
-		if len(res.Batch.Transactions) == 0 {
-			errRetrieveBatch = ErrNoBatch
-		}
-		// Even if there are no transactions, update lastBatchData so we don't
-		// repeatedly emit the same empty batch, and persist it to metadata.
-		if err := m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData)); err != nil {
-			m.logger.Error("error while setting last batch hash", "error", err)
-		}
-		m.lastBatchData = res.BatchData
-		return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, errRetrieveBatch
+		// Update lastBatchData regardless of transaction count
+		if err := m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData)); err != nil {
+			m.logger.Error("error while setting last batch hash", "error", err)
+		}
+		m.lastBatchData = res.BatchData
+
+		batch := &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}
+		
+		// For empty batches, signal this with a custom field rather than an error
+		if len(res.Batch.Transactions) == 0 {
+			return batch, ErrNoBatch
+		}
+		return batch, nil
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f8826b4d7a144170d9517b9d41f25477c05f70ff and 0ea76571d7d7e3b83549f29acaa96fcc4cf2d6b1.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `block/manager.go` (11 hunks)
* `block/publish_block_test.go` (3 hunks)
* `specs/lazy-adr/adr-021-lazy-aggregation.md` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>block/publish_block_test.go (4)</summary><blockquote>

<details>
<summary>test/mocks/external/hstore.go (1)</summary>

* `Store` (13-15)

</details>
<details>
<summary>types/state.go (1)</summary>

* `State` (20-39)

</details>
<details>
<summary>types/signed_header.go (1)</summary>

* `SignedHeader` (22-27)

</details>
<details>
<summary>types/block.go (1)</summary>

* `Data` (33-36)

</details>

</blockquote></details>
<details>
<summary>block/manager.go (6)</summary><blockquote>

<details>
<summary>block/errors.go (1)</summary>

* `ErrNoBatch` (17-17)

</details>
<details>
<summary>types/block.go (3)</summary>

* `Data` (33-36)
* `Signature` (39-39)
* `Version` (16-19)

</details>
<details>
<summary>types/header.go (3)</summary>

* `Hash` (14-14)
* `Header` (35-62)
* `BaseHeader` (25-32)

</details>
<details>
<summary>types/state.go (1)</summary>

* `State` (20-39)

</details>
<details>
<summary>types/signed_header.go (1)</summary>

* `SignedHeader` (22-27)

</details>
<details>
<summary>types/tx.go (2)</summary>

* `Txs` (7-7)
* `Tx` (4-4)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md</summary>

[uncategorized] ~6-~6: You might be missing the article “the” here.
Context: ...tial draft - 2024-01-24: Revised to use existing empty batch mechanism - 2024-01-25: Upd...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>
<details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

<details>
<summary>block/manager.go</summary>

[failure] 567-567: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: rollkit-docker / docker-build (GHCR; ghcr.io/rollkit/rollkit)
* GitHub Check: test / Build All Rollkit Binaries
* GitHub Check: test / Run Unit Tests
* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (13)</summary><blockquote>

<details>
<summary>block/publish_block_test.go (3)</summary>

`230-232`: **Good handling of DA height initialization**

The initialization of the atomic uint64 for DA height is properly set up before being passed to the Manager struct. This ensures the daHeight field is correctly initialized for empty batch testing.

---

`251-262`: **Well-structured test setup for empty block handling**

The additional fields in the Manager initialization (lastState, caches, channels, daHeight) properly reflect the new requirements for empty batch processing. This enables complete testing of the lazy aggregation mechanism's handling of empty blocks.

---

`297-317`: **Changed test expectations reflect new empty block behavior**

The test now expects appropriate method calls for empty batch processing, which aligns with the design in ADR-021. Previously, the test expected these methods to NOT be called, but with the new lazy aggregation design, empty batches should still trigger block creation, saving, and execution.

</details>
<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (4)</summary>

`1-12`: **Clear architectural context for lazy aggregation**

The ADR effectively defines the problem: inconsistency between DA layer blocks and execution layer blocks during periods of inactivity, which breaks the expected 1:1 mapping needed by EVM environments. This context sets the stage for why the changes are necessary.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~6-~6: You might be missing the article “the” here.
Context: ...tial draft - 2024-01-24: Revised to use existing empty batch mechanism - 2024-01-25: Upd...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

---

`55-88`: **Well-designed empty block creation logic**

The block publishing logic correctly distinguishes between completely missing batches (skipping block production) and empty batches (creating empty blocks with proper timing). This ensures consistent block heights while minimizing DA overhead.

---

`91-144`: **Robust dual-timer design for lazy aggregation**

The lazy aggregation loop with dual timers (`lazyTimer` and `blockTimer`) provides an elegant solution for deterministic block production during network inactivity while still optimizing for transaction inclusion latency. The timer reset logic in `produceBlock` intelligently accounts for block production time.

---

`168-187`: **Well-balanced tradeoffs analysis**

The ADR provides a thorough analysis of the consequences, acknowledging both positive aspects (consistent block heights, simpler implementation) and negative aspects (small DA layer overhead, timer complexity). This balanced view helps implementers understand the tradeoffs involved.

</details>
<details>
<summary>block/manager.go (6)</summary>

`156-157`: **Good design for transaction notification**

Adding a dedicated notification channel for transactions enables efficient signaling without blocking the transaction submission path, which is important for maintaining system responsiveness.

---

`291-294`: **Appropriate naming change for clarity**

Renaming `LazyBlockTime` to `LazyBlockInterval` better reflects its purpose as a regular interval for block production during periods of inactivity, improving code readability and maintainability.

---

`352-353`: **State tracking for transaction availability**

Replacing `buildingBlock` with `txsAvailable` and adding a buffered notification channel provides a more robust mechanism for tracking transaction availability and triggering block production accordingly.



Also applies to: 359-360

---

`552-568`: **Improved block publishing logic for empty batches**

The modified `publishBlockInternal` method correctly handles empty batches by creating blocks for them, ensuring consistent block heights with the DA layer. The branch structure clearly separates three cases: no batch, empty batch, and normal batch.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

[failure] 567-567: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

</details>

</details>

---

`718-764`: **Clear distinction between empty and non-empty blocks**

The improved `execCreateBlock` method properly differentiates between empty and non-empty blocks, using the predefined `dataHashForEmptyTxs` marker for empty blocks. This approach maintains consistency with the existing sync mechanisms.


However, there's a potential optimization in line 719:

```diff
-	isEmpty := batchData.Batch == nil || len(batchData.Transactions) == 0
+	isEmpty := batchData.Batch == nil || len(batchData.Batch.Transactions) == 0
```

The static analyzer flagged this as a potential issue - accessing `Transactions` directly from `batchData` instead of through `batchData.Batch`.

---

`897-908`: **Efficient non-blocking transaction notification**

The `NotifyNewTransactions` method uses a non-blocking send pattern to avoid slowing down the transaction submission path, with appropriate handling of the case where the channel buffer is full. This is an excellent design choice for performance.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:47:19Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:57:27Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (6)</summary><blockquote>

`5-7`: **Add the definite article for clarity in changelog entries**

The changelog bullets read more smoothly with “the” before “existing empty batch mechanism”. For example:
```diff
- 2024-01-24: Revised to use existing empty batch mechanism
+ 2024-01-24: Revised to use the existing empty batch mechanism
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~6-~6: You might be missing the article “the” here.
Context: ...tial draft - 2024-01-24: Revised to use existing empty batch mechanism - 2024-01-25: Upd...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

---

`25-53`: **Fix Markdown code fence indentation and formatting**

All Go snippets are indented under the Markdown list, causing the fences to be parsed incorrectly. Move the triple-backtick fences to the left margin and use Go-style tabs inside:
```diff
-    ```go
-    func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+```go
+func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
     res, err := m.sequencer.GetNextBatch(ctx, req)
     if err != nil {
         return nil, err
     }
     // ...
 }
-    ```
+```  
```

---

`37-42`: **Document intentional deviation from Go error conventions**

Returning non-nil `BatchData` alongside `ErrNoBatch` breaks the usual Go idiom (“non-nil error implies zero returns”). Please add a brief note in the ADR explaining why this pattern was chosen and how callers should handle `ErrNoBatch`.

---

`59-88`: **Normalize indentation in the `publishBlock` snippet**

Lines 62–67 mix tabs and spaces, which may confuse readers. Unindent the fences and apply a consistent tab-based indent inside:
```diff
-    ```go
-    // In publishBlock method
-    batchData, err := m.retrieveBatch(ctx)
-        if err != nil {
-            if errors.Is(err, ErrNoBatch) {
+```go
+// In publishBlock method
+batchData, err := m.retrieveBatch(ctx)
+if err != nil {
+    if errors.Is(err, ErrNoBatch) {
         if batchData == nil {
             m.logger.Info("No batch retrieved from sequencer, skipping block production")
             return nil
```

---

`94-138`: **Align the lazy aggregation loop example formatting**

The `NotifyNewTransactions` and `lazyAggregationLoop` code is indented under the Markdown list and uses inconsistent spacing. To improve readability:
- Unindent the fences to the left margin.
- Use tabs (via `gofmt`) inside the fences.
- Ensure blank lines between methods.

---

`144-160`: **Standardize the `produceBlock` snippet formatting**

As above, remove leading whitespace before the code fence and format the code block with `gofmt`-style tabs. This makes the example ready to copy/paste and compile.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0ea76571d7d7e3b83549f29acaa96fcc4cf2d6b1 and 86c62bd280eb50cadbc385b6cdc2af2007e35ed0.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `specs/lazy-adr/adr-021-lazy-aggregation.md` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md</summary>

[uncategorized] ~6-~6: You might be missing the article “the” here.
Context: ...tial draft - 2024-01-24: Revised to use existing empty batch mechanism - 2024-01-25: Upd...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: rollkit-docker / docker-build (GHCR; ghcr.io/rollkit/rollkit)
* GitHub Check: test / Run Unit Tests
* GitHub Check: test / Build All Rollkit Binaries
* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>specs/lazy-adr/adr-021-lazy-aggregation.md (2)</summary>

`1-210`: **ADR is comprehensive and well-structured**

The document clearly outlines the context, decision, detailed design, efficiency considerations, and consequences. It aligns well with the PR objectives and provides a solid foundation for implementation.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~6-~6: You might be missing the article “the” here.
Context: ...tial draft - 2024-01-24: Revised to use existing empty batch mechanism - 2024-01-25: Upd...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

---

`26-27`: **Clarify or define the `req` parameter**

The call to `m.sequencer.GetNextBatch(ctx, req)` references `req`, which isn’t introduced in this snippet. Either document what `req` represents or adjust the example to match the actual signature:
```diff
-func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
-    res, err := m.sequencer.GetNextBatch(ctx, req)
+func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+    // replace `req` with an appropriate request object, e.g., nextBatchReq
+    res, err := m.sequencer.GetNextBatch(ctx, nextBatchReq)
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T17:55:51Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>block/aggregation.go (1)</summary><blockquote>

`79-94`: _⚠️ Potential issue_

**Timer reset operation is missing safety checks**

The `produceBlock` method resets timers without following the safe pattern for timer reset in Go. This can lead to data race conditions or undefined behavior, especially in Go versions before 1.22.


```diff
func (m *Manager) produceBlock(ctx context.Context, trigger string, lazyTimer, blockTimer *time.Timer) {
	// Record the start time
	start := time.Now()

	// Attempt to publish the block
	if err := m.publishBlock(ctx); err != nil && ctx.Err() == nil {
		m.logger.Error("error while publishing block", "trigger", trigger, "error", err)
	} else {
		m.logger.Debug("Successfully published block", "trigger", trigger)
	}

	// Reset both timers for the next aggregation window
-	lazyTimer.Reset(getRemainingSleep(start, m.config.Node.LazyBlockInterval.Duration))
-	blockTimer.Reset(getRemainingSleep(start, m.config.Node.BlockTime.Duration))
+	// Safe timer reset pattern
+	lazyTimer.Reset(getRemainingSleep(start, m.config.Node.LazyBlockInterval.Duration))
+	if !blockTimer.Stop() {
+		select {
+		case <-blockTimer.C:
+		default:
+		}
+	}
+	blockTimer.Reset(getRemainingSleep(start, m.config.Node.BlockTime.Duration))
}
```

This matches the safe pattern mentioned in the previous review comment, which is crucial for proper timer behavior.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>block/lazy_aggregation_test.go (3)</summary><blockquote>

`141-159`: **Risk of flaky test due to tight timing constraints**

The test uses very tight timing constraints and makes assumptions about exact timer firing behavior, which could lead to flaky tests, especially in CI environments with varying resource constraints.


Consider making the test more resilient by:

1. Using larger time buffers (e.g., 3x instead of 2x)
2. Adding more context in failure messages that include actual elapsed times
3. Using a channel-based approach rather than sleep to synchronize test steps

Example improvement:

```diff
  // Wait for the first publish call triggered by the initial immediate lazyTimer fire
  select {
  case <-pubMock.calls:
      // Good, first block published by lazy timer
-  case <-time.After(2 * lazyTime): // Give some buffer
-      require.Fail("timed out waiting for first block publication")
+  case <-time.After(3 * lazyTime): // More generous buffer
+      require.Fail(fmt.Sprintf("timed out waiting for first block publication after %v", 3*lazyTime))
  }
```

---

`253-253`: **Consider increasing txNotifyCh buffer size**

In a production environment with many transactions, a buffer size of 1 might be too small and could lead to dropped notifications if they come in rapid succession.


```diff
-	m.txNotifyCh = make(chan struct{}, 1)
+	m.txNotifyCh = make(chan struct{}, 10) // Larger buffer to handle bursts of notifications
```

---

`17-50`: **mockPublishBlock can be improved with a more idiomatic Go approach**

The mock implementation is functional but could be more idiomatic and robust with a few enhancements.


```diff
// mockPublishBlock is used to control the behavior of publishBlock during tests
type mockPublishBlock struct {
	mu    sync.Mutex
	calls chan struct{}
	err   error
	delay time.Duration // Optional delay to simulate processing time
+	callCount int       // Track total calls made
}

// reset clears the calls channel in mockPublishBlock.
func (m *mockPublishBlock) reset() {
	m.mu.Lock()
	defer m.mu.Unlock()
+	m.callCount = 0
	// Clear the channel
	for len(m.calls) > 0 {
		<-m.calls
	}
}

func (m *mockPublishBlock) publish(ctx context.Context) error {
	m.mu.Lock()
	err := m.err
	delay := m.delay
+	m.callCount++
	m.mu.Unlock()

	if delay > 0 {
		time.Sleep(delay)
	}
	// Non-blocking send in case the channel buffer is full or receiver is not ready
	select {
	case m.calls <- struct{}{}:
	default:
+		// Log a warning when notifications are dropped
+		fmt.Printf("Warning: publish notification dropped, channel full\n")
	}
	return err
}

+// GetCallCount returns the total number of calls made to the mock
+func (m *mockPublishBlock) GetCallCount() int {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	return m.callCount
+}
```

This implementation:
1. Adds a reliable call counter that doesn't rely on channel capacity
2. Logs warnings when notifications are dropped
3. Provides a thread-safe method to check call count

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 86c62bd280eb50cadbc385b6cdc2af2007e35ed0 and fddf780fb8866178ff3d9f07689a7c7bd35b655c.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `block/aggregation.go` (5 hunks)
* `block/lazy_aggregation_test.go` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: test / Run Unit Tests</summary>

<details>
<summary>block/aggregation.go</summary>

[failure] 136-136: 
undefined: setupTestManager

</details>

</details>
<details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

<details>
<summary>block/aggregation.go</summary>

[failure] 136-136: 
undefined: setupTestManager) (typecheck)

</details>

</details>
<details>
<summary>🪛 GitHub Actions: CI and Release</summary>

<details>
<summary>block/aggregation.go</summary>

[error] 136-136: Build error: undefined identifier 'setupTestManager' at line 136.

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>block/aggregation.go (1)</summary>

`65-72`: **Block timer state updated correctly for maintaining continuous ticking**

Good improvement - the block timer is now correctly reset when no transactions are available. This fixes the issue mentioned in the previous review where the block timer could stall if there were no transactions.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T19:00:26Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>block/lazy_aggregation_test.go (2)</summary><blockquote>

`52-70`: **The setupTestManager function needs to be moved to a shared test package.**

This function is referenced in `block/aggregation.go` tests but would be inaccessible outside this file, potentially causing build failures. Moving it to a shared test utilities file that can be imported by all test files would resolve this.

---

`311-344`: _⚠️ Potential issue_

**Test is incomplete for empty block creation verification**

While the test verifies that the context is passed correctly, it doesn't actually verify that empty blocks are created with the correct dataHash as stated in the test name.


Enhance the test to verify the actual creation of empty blocks with correct dataHash. This will require modifying the mocked publish function to check if the context contains the expected values for empty blocks.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>block/lazy_aggregation_test.go (3)</summary><blockquote>

`25-33`: **Potential inefficiency in channel clearing**

The `reset` method clears the channel by consuming elements individually, which could be inefficient for large channels. A more efficient approach would be to recreate the channel.

```diff
func (m *mockPublishBlock) reset() {
	m.mu.Lock()
	defer m.mu.Unlock()
-	// Clear the channel
-	for len(m.calls) > 0 {
-		<-m.calls
-	}
+	// Replace the channel with a new one
+	m.calls = make(chan struct{}, cap(m.calls))
}
```

---

`280-281`: **Use assertion instead of blind sleep**

Using arbitrary sleep durations without assertions can lead to flaky tests. Replace this sleep with a check or assertion.

```diff
-	// Wait a bit to ensure the loop is running with reset timers
-	time.Sleep(20 * time.Millisecond)
+	// Ensure the loop is ready with reset timers
+	select {
+	case <-time.After(20 * time.Millisecond):
+		// Timer reset completed
+	case <-pubMock.calls:
+		require.Fail("Unexpected publish call during timer reset period")
+	}
```

---

`217-241`: **Make test timings more tolerance to system load variations**

The time-based assertions using `InDelta` are using very tight tolerances (5ms), which can lead to flaky tests on different systems or under load. Consider using proportional tolerances instead.

```diff
-	assert.InDelta(interval-30*time.Millisecond, sleep1, float64(5*time.Millisecond), "Case 1 failed")
+	// Allow tolerance of 10% of the expected duration or 10ms, whichever is larger
+	expectedSleep := interval - 30*time.Millisecond
+	tolerance := float64(max(time.Duration(float64(expectedSleep)*0.1), 10*time.Millisecond))
+	assert.InDelta(expectedSleep, sleep1, tolerance, 
+		"Expected sleep time should be close to interval - elapsed time")
```

Apply similar changes to the other time-based assertions to make them more robust against system variations.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bb80260f054303e67885431baf1673f0e01b3126 and bed358cbb785349155d95e9295c7a8fc1a19e0ea.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `block/aggregation.go` (3 hunks)
* `block/lazy_aggregation_test.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* block/aggregation.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: rollkit-docker / docker-build (GHCR; ghcr.io/rollkit/rollkit)
* GitHub Check: test / Build All Rollkit Binaries
* GitHub Check: test / Run Unit Tests
* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>block/lazy_aggregation_test.go (5)</summary>

`1-15`: **LGTM: Well-structured imports and package organization**

The imports are well-organized and the package structure follows Go conventions.

---

`72-113`: **LGTM: Block timer trigger test is properly implemented**

This test correctly verifies that blocks are published when the block timer fires first, with appropriate setup and assertions.

---

`243-309`: **LGTM: Transaction notification test handles race conditions well**

The test for transaction notifications in lazy mode effectively verifies that notifications properly trigger block production, with good synchronization mechanisms to avoid race conditions.

---

`346-403`: **LGTM: Normal mode transaction notification test verifies correct behavior**

This test correctly verifies that in normal (non-lazy) mode, transaction notifications do not cause immediate block production, consistent with the expected behavior.

---

`35-50`: _🛠️ Refactor suggestion_

**Fix potential race condition in publish method**

There's a potential race condition in the `publish` method where `err` and `delay` are read with a lock, then the lock is released before using these values.

```diff
func (m *mockPublishBlock) publish(ctx context.Context) error {
	m.mu.Lock()
	err := m.err
	delay := m.delay
	m.mu.Unlock()

	if delay > 0 {
		time.Sleep(delay)
	}
	// Non-blocking send in case the channel buffer is full or receiver is not ready
	select {
	case m.calls <- struct{}{}:
	default:
	}
	return err
}
```

Consider keeping the lock for the entire operation or making copies of the mutable state that won't change:

```diff
func (m *mockPublishBlock) publish(ctx context.Context) error {
	var err error
	var delay time.Duration
	
	m.mu.Lock()
	err = m.err
	delay = m.delay
	m.mu.Unlock()

	if delay > 0 {
		time.Sleep(delay)
	}
	
	select {
	case m.calls <- struct{}{}:
	default:
	}
	return err
}
```



> Likely an incorrect or invalid review comment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T19:09:13Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>block/manager.go (1)</summary><blockquote>

`549-549`: **Fix static analysis warning about unnecessary field selector**

The current code references `batchData.Batch.Transactions` which causes a QF1008 warning because `Batch` is an embedded field. You can simplify this.

```diff
-			m.logger.Debug("block info", "num_tx", len(batchData.Batch.Transactions))
+			m.logger.Debug("block info", "num_tx", len(batchData.Transactions))
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

[failure] 549-549: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

</details>

</details>

</blockquote></details>
<details>
<summary>specs/src/specs/block-manager.md (1)</summary><blockquote>

`66-67`: **Updated configuration documentation to match implementation changes**

The documentation now correctly reflects the renamed `LazyBlockInterval` parameter and adds the new `LazyMode` boolean configuration, aligning with the code changes.


However, there's a minor markdown formatting issue with the table pipe style detected by the static analyzer.

```diff
-|LazyBlockInterval|time.Duration|time interval used for block production in lazy aggregator mode even when there are no transactions ([`defaultLazyBlockTime`][defaultLazyBlockTime])|
-|LazyMode|bool|when set to true, enables lazy aggregation mode which produces blocks only when transactions are available or at LazyBlockInterval intervals|
+LazyBlockInterval|time.Duration|time interval used for block production in lazy aggregator mode even when there are no transactions ([`defaultLazyBlockTime`][defaultLazyBlockTime])
+LazyMode|bool|when set to true, enables lazy aggregation mode which produces blocks only when transactions are available or at LazyBlockInterval intervals
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

66-66: Table pipe style
Expected: no_leading_or_trailing; Actual: leading_and_trailing; Unexpected leading pipe

(MD055, table-pipe-style)

---

66-66: Table pipe style
Expected: no_leading_or_trailing; Actual: leading_and_trailing; Unexpected trailing pipe

(MD055, table-pipe-style)

---

67-67: Table pipe style
Expected: no_leading_or_trailing; Actual: leading_and_trailing; Unexpected leading pipe

(MD055, table-pipe-style)

---

67-67: Table pipe style
Expected: no_leading_or_trailing; Actual: leading_and_trailing; Unexpected trailing pipe

(MD055, table-pipe-style)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bed358cbb785349155d95e9295c7a8fc1a19e0ea and 78679e25b9c90ce06326aeada3c03faa7fa40b10.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `block/aggregation_test.go` (2 hunks)
* `block/manager.go` (11 hunks)
* `node/full.go` (1 hunks)
* `pkg/cmd/run_node_test.go` (2 hunks)
* `specs/src/specs/block-manager.md` (3 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* node/full.go
* pkg/cmd/run_node_test.go
* block/aggregation_test.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>block/manager.go (6)</summary><blockquote>

<details>
<summary>block/errors.go (1)</summary>

* `ErrNoBatch` (17-17)

</details>
<details>
<summary>types/block.go (3)</summary>

* `Data` (33-36)
* `Signature` (39-39)
* `Version` (16-19)

</details>
<details>
<summary>types/header.go (3)</summary>

* `Hash` (14-14)
* `Header` (35-62)
* `BaseHeader` (25-32)

</details>
<details>
<summary>types/state.go (1)</summary>

* `State` (20-39)

</details>
<details>
<summary>types/signed_header.go (1)</summary>

* `SignedHeader` (22-27)

</details>
<details>
<summary>types/tx.go (2)</summary>

* `Txs` (7-7)
* `Tx` (4-4)

</details>

</blockquote></details>

</details><details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>specs/src/specs/block-manager.md</summary>

66-66: Table pipe style
Expected: no_leading_or_trailing; Actual: leading_and_trailing; Unexpected leading pipe

(MD055, table-pipe-style)

---

66-66: Table pipe style
Expected: no_leading_or_trailing; Actual: leading_and_trailing; Unexpected trailing pipe

(MD055, table-pipe-style)

---

67-67: Table pipe style
Expected: no_leading_or_trailing; Actual: leading_and_trailing; Unexpected leading pipe

(MD055, table-pipe-style)

---

67-67: Table pipe style
Expected: no_leading_or_trailing; Actual: leading_and_trailing; Unexpected trailing pipe

(MD055, table-pipe-style)

</details>

</details>
<details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

<details>
<summary>block/manager.go</summary>

[failure] 549-549: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: test / Build All Rollkit Binaries
* GitHub Check: Analyze (go)
* GitHub Check: Summary

</details>

<details>
<summary>🔇 Additional comments (13)</summary><blockquote>

<details>
<summary>block/manager.go (10)</summary>

`61-62`: **Sentinel data hash for empty transactions is now used to maintain DA consistency**

The introduction of this predefined hash for empty blocks is a critical part of the sentinel marker design described in the PR, ensuring consistent representation of empty blocks on the DA layer.

---

`129-129`: **Added tracking flag for transaction availability in lazy mode**

This boolean flag enables the lazy aggregation mechanism to track when transactions are available for inclusion in blocks, supporting the dual timer approach mentioned in the PR.

---

`152-153`: **Added notification channel for transaction availability**

The non-blocking channel design is an effective solution to notify the block manager of new transactions without creating backpressure on the transaction submission path.

---

`287-289`: **Renamed LazyBlockTime to LazyBlockInterval for clarity**

This naming change better represents the purpose of this configuration option - it's an interval between blocks rather than just a time measurement.

---

`452-465`: **Modified retrieveBatch to support empty batch processing**

The function now properly handles empty batches by returning them with an `ErrNoBatch` error but still providing the timestamp, enabling empty block creation with proper timing. The metadata and `lastBatchData` updates prevent repeated empty batch emissions.

---

`534-550`: **Enhanced empty block handling in publishBlockInternal**

This change is central to addressing the DA layer consistency issue described in the PR. The function now:
1. Only skips block production if no batch is retrieved at all
2. Creates empty blocks when batches contain no transactions
3. Logs the specific production scenario for better observability

This ensures consistent block height mapping with the DA layer, fulfilling the PR objective.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

[failure] 549-549: 
QF1008: could remove embedded field "Batch" from selector (staticcheck)

</details>

</details>

---

`701-709`: **Added empty block detection and special data hash handling**

This implementation correctly identifies empty blocks and uses the predefined sentinel data hash, ensuring consistent representation of empty blocks on the DA layer.

---

`735-746`: **Optimized transaction handling for empty blocks**

The code now efficiently handles transaction preparation based on the block type (empty or not), avoiding unnecessary processing for empty blocks while ensuring proper structure.

---

`751-765`: **Consolidated block application and validation methods**

These methods simplify the code by moving execution logic directly into the Manager struct, making the control flow more straightforward and maintainable.

---

`879-890`: **Added non-blocking transaction notification method**

This method efficiently notifies the manager about new transactions without blocking the submission path. The non-blocking channel send with a default case is an excellent pattern to prevent backpressure.

</details>
<details>
<summary>specs/src/specs/block-manager.md (3)</summary>

`75-81`: **Added comprehensive documentation for the dual timer mechanism**

The documentation now clearly explains the dual timer approach in lazy mode, including:
1. The blockTimer for transaction-triggered production
2. The lazyTimer for regular interval production during inactivity
3. The notification channel mechanism
4. The purpose of empty blocks for DA layer consistency

This documentation accurately reflects the implementation and provides context for why these changes were needed.

---

`163-169`: **Added detailed assumptions and considerations for lazy aggregation**

The extended documentation section covers important implementation details:
1. DA layer consistency through empty blocks
2. The dual timer approach
3. Empty batch handling
4. Non-blocking notification channel design

This ensures developers understand the design decisions and implementation details of the lazy aggregation mechanism.

---

`200-202`: **Added reference to the new Lazy Aggregation ADR**

The addition of a reference to the new ADR ensures that developers can find the detailed design document explaining the rationale and implementation details of the lazy aggregation mechanism.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T10:10:15Z
user: login: tzdybal
body: LGTM.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:39:25Z
user: login: gupadhyaya
body: lgtm
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:42:06Z
user: login: tac0turtle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:48:48Z

Review comments:
diff_hunk: @@ -0,0 +1,160 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+The current implementation in `block/aggregation.go` needs to be enhanced to maintain consistency between DA layer blocks and execution layer blocks, even during periods of inactivity.
+
+## Alternative Approaches
+
+### 1. Skip Empty Blocks Entirely
+
+- Continue current behavior of not posting empty blocks
+- **Rejected** because it breaks EVM execution layer expectations and creates inconsistent block mapping
+
+### 2. Post Full Block Structure for Empty Blocks
+
+- Post regular blocks with empty transaction lists
+- **Rejected** because it wastes DA layer space unnecessarily
+
+### 3. Sentinel Empty Block Marker (Chosen)
+
+- Post a special single-byte transaction to mark empty blocks
+- Optimize DA layer space usage while maintaining block consistency
+- Provides clear distinction between empty and failed/missed blocks
+
+## Decision
+
+Implement a sentinel-based empty block marking system that:
+
+1. Posts a special sentinel transaction to the DA layer for empty blocks
+2. Maintains block height consistency between DA and execution layers
+3. Optimizes DA layer space usage
+4. Preserves existing lazy aggregation timing behavior
+
+## Detailed Design
+
+### User Requirements
+
+- Maintain consistent block heights between DA and execution layers
+- Minimize DA layer costs for empty blocks
+- Preserve existing lazy aggregation performance characteristics
+- Support EVM execution layer expectations
+
+### Systems Affected
+
+1. Block Manager (`block/aggregation.go`)
+2. Transaction Processing Pipeline
+3. DA Layer Interface
+4. Block Execution Logic
+
+### Data Structures
+
+```go
+const (
+    // EmptyBlockSentinel is a special byte sequence marking empty blocks
+    EmptyBlockSentinel = []byte{0x00, 0xE0, 0x0E} // "EMPTY" in minimal form
+)
+
+type BlockData struct {
+    Txs [][]byte
+    IsEmpty bool
+}
+```
+
+### Implementation Details
+
+1. **Empty Block Detection**:
+
+    ```go
+    func (m *Manager) isEmptyBlock(txs [][]byte) bool {
+        return len(txs) == 1 && bytes.Equal(txs[0], EmptyBlockSentinel)
+    }
+    ```
+
+2. **Modified Aggregation Loop**:
+
+    ```go
+    func (m *Manager) lazyAggregationLoop(ctx context.Context, blockTimer *timTimer) {
+        // ... existing timer setup ...
+
+        for {
+            select {
+            case <-ctx.Done():
+                return
+            case <-lazyTimer.C:
+                // No transactions available, create empty block
+                if err := m.publishEmptyBlock(ctx); err != nil {
+                    m.logger.Error("error publishing empty block", "error", err)
+                }
+            case <-blockTimer.C:
+                // Normal block production with available transactions
+                if err := m.publishBlock(ctx); err != nil {
+                    m.logger.Error("error publishing block", "error", err)
+                }
+            }
+        }
+    }
+
+    func (m *Manager) publishEmptyBlock(ctx context.Context) error {
+        return m.publishBlock(ctx, EmptyBlockSentinel)
path: specs/lazy-adr/adr-021-lazy-aggregation.md
user: login: Manav-Aggarwal
body: minor nit: golang doesn't support method overloading so we'd need a separate function since there's already a `publishBlock` that takes one argument `context`😅 
created_at: 2025-04-29T21:02:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,160 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+The current implementation in `block/aggregation.go` needs to be enhanced to maintain consistency between DA layer blocks and execution layer blocks, even during periods of inactivity.
+
+## Alternative Approaches
+
+### 1. Skip Empty Blocks Entirely
+
+- Continue current behavior of not posting empty blocks
+- **Rejected** because it breaks EVM execution layer expectations and creates inconsistent block mapping
+
+### 2. Post Full Block Structure for Empty Blocks
+
+- Post regular blocks with empty transaction lists
+- **Rejected** because it wastes DA layer space unnecessarily
+
+### 3. Sentinel Empty Block Marker (Chosen)
path: specs/lazy-adr/adr-021-lazy-aggregation.md
user: login: gupadhyaya
body: i don't think we need a special tx. also no need for special `publishEmptyBlock`. 

you can still use the single `publishBlock`. but modify `retrieveBatch` logic a bit.

* instead of returning `nil, ErrNoBatch` return `batch, ErrNoBatch`: https://github.com/rollkit/rollkit/blob/main/block/manager.go#L473

* https://github.com/rollkit/rollkit/blob/main/block/manager.go#L566 check for `batchData` for not nil so that you can use the timestamp of this empty batch for proposing the header.

* put `dataHashForEmptyTxs` in the DataHash field if the `data` is nil to indicate empty batch in https://github.com/rollkit/rollkit/blob/main/block/manager.go#L747, so that when we parse such headers, we don't expect the batch. note that this code is already present: https://github.com/rollkit/rollkit/blob/main/block/sync.go#L56 and https://github.com/rollkit/rollkit/blob/main/block/sync.go#L170
created_at: 2025-04-30T06:00:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+The current implementation in `block/aggregation.go` needs to be enhanced to maintain consistency between DA layer blocks and execution layer blocks, even during periods of inactivity.
+
+## Alternative Approaches
+
+### 1. Skip Empty Blocks Entirely
+
+- Continue current behavior of not posting empty blocks
+- **Rejected** because it breaks EVM execution layer expectations and creates inconsistent block mapping
+
+### 2. Post Full Block Structure for Empty Blocks
+
+- Post regular blocks with empty transaction lists
+- **Rejected** because it wastes DA layer space unnecessarily
+
+### 3. Sentinel Empty Block Marker (Chosen)
path: specs/lazy-adr/adr-021-lazy-aggregation.md
user: login: tac0turtle
body: amended based on the feedback
created_at: 2025-04-30T07:43:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,132 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+- 2024-01-24: Revised to use existing empty batch mechanism
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+## Decision
+
+Leverage the existing empty batch mechanism and `dataHashForEmptyTxs` to maintain block height consistency.
+
+## Detailed Design
+
+### Implementation Details
+
+1. **Modified Batch Retrieval**:
+
+    ```go
+    func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+        res, err := m.sequencer.GetBatch(ctx)
+        if err != nil {
+            return nil, err
+        }
+
+        if res != nil && res.Batch != nil {
+            // Even if there are no transactions, return the batch with timestamp
+            // This allows empty blocks to maintain proper timing
+            if len(res.Batch.Transactions) == 0 {
+                return &BatchData{
+                    Batch: res.Batch,
+                    Time: res.Timestamp,
+                    Data: res.BatchData,
+                }, ErrNoBatch
+            }
+            return &BatchData{
+                Batch: res.Batch,
+                Time: res.Timestamp,
+                Data: res.BatchData,
+            }, nil
+        }
+        return nil, ErrNoBatch
+    }
+    ```
path: specs/lazy-adr/adr-021-lazy-aggregation.md
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid returning non-nil data with an error**  
By returning a populated `BatchData` along with `ErrNoBatch`, this method breaks Go’s convention that non-nil errors imply zeroed return values. Consider refactoring to explicitly signal an empty batch—either with a boolean flag, a custom result struct, or an enumeration—so callers don’t conflate “no batch” with an actual error.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T07:45:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,132 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+- 2024-01-24: Revised to use existing empty batch mechanism
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+## Decision
+
+Leverage the existing empty batch mechanism and `dataHashForEmptyTxs` to maintain block height consistency.
+
+## Detailed Design
+
+### Implementation Details
+
+1. **Modified Batch Retrieval**:
+
+    ```go
+    func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+        res, err := m.sequencer.GetBatch(ctx)
+        if err != nil {
+            return nil, err
+        }
+
+        if res != nil && res.Batch != nil {
+            // Even if there are no transactions, return the batch with timestamp
+            // This allows empty blocks to maintain proper timing
+            if len(res.Batch.Transactions) == 0 {
+                return &BatchData{
+                    Batch: res.Batch,
+                    Time: res.Timestamp,
+                    Data: res.BatchData,
+                }, ErrNoBatch
+            }
+            return &BatchData{
+                Batch: res.Batch,
+                Time: res.Timestamp,
+                Data: res.BatchData,
+            }, nil
+        }
+        return nil, ErrNoBatch
+    }
+    ```
+
+2. **Header Creation for Empty Blocks**:
+
+    ```go
+    func (m *Manager) createHeader(ctx context.Context, batchData *BatchData) (*types.Header, error) {
+        // Use batch timestamp even for empty blocks
+        timestamp := batchData.Time
+
+        if batchData.Transactions == nil || len(batchData.Transactions) == 0 {
+            // Use dataHashForEmptyTxs to indicate empty batch
+            return &types.Header{
+                DataHash: dataHashForEmptyTxs,
+                Timestamp: timestamp,
+                // ... other header fields
+            }, nil
+        }
+        // Normal header creation for non-empty blocks
+        // ...
+    }
+    ```
path: specs/lazy-adr/adr-021-lazy-aggregation.md
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix field reference and missing non-empty return**  
1. `batchData.Transactions` is not a field on `BatchData`; transactions live under `batchData.Batch.Transactions`.  
2. There’s no return path for non-empty batches, which will cause a compile‐time error.  

Proposed diff:  
```diff
-func (m *Manager) createHeader(ctx context.Context, batchData *BatchData) (*types.Header, error) {
-   timestamp := batchData.Time
-
-   if batchData.Transactions == nil || len(batchData.Transactions) == 0 {
-       // Use dataHashForEmptyTxs to indicate empty batch
-       return &types.Header{
-           DataHash:  dataHashForEmptyTxs,
-           Timestamp: timestamp,
-           // ... other header fields
-       }, nil
-   }
-   // Normal header creation for non-empty blocks
-   // ...
-}
+func (m *Manager) createHeader(ctx context.Context, batchData *BatchData) (*types.Header, error) {
+   timestamp := batchData.Time
+
+   // Empty block: mark with empty data hash
+   if batchData.Batch == nil || len(batchData.Batch.Transactions) == 0 {
+       return &types.Header{
+           DataHash:  dataHashForEmptyTxs,
+           Timestamp: timestamp,
+           // ... other header fields
+       }, nil
+   }
+
+   // Non-empty block: compute and set proper DataHash
+   header := &types.Header{
+       DataHash:  computeDataHash(batchData.Data),
+       Timestamp: timestamp,
+       // ... other header fields
+   }
+   return header, nil
+}
```  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        func (m *Manager) createHeader(ctx context.Context, batchData *BatchData) (*types.Header, error) {
            // Use batch timestamp even for empty blocks
            timestamp := batchData.Time

            // Empty block: mark with empty data hash
            if batchData.Batch == nil || len(batchData.Batch.Transactions) == 0 {
                return &types.Header{
                    DataHash:  dataHashForEmptyTxs,
                    Timestamp: timestamp,
                    // ... other header fields
                }, nil
            }

            // Non-empty block: compute and set proper DataHash
            header := &types.Header{
                DataHash:  computeDataHash(batchData.Data),
                Timestamp: timestamp,
                // ... other header fields
            }
            return header, nil
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits fbe8b10 to 8615079
created_at: 2025-04-30T07:45:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,132 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+- 2024-01-24: Revised to use existing empty batch mechanism
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+## Decision
+
+Leverage the existing empty batch mechanism and `dataHashForEmptyTxs` to maintain block height consistency.
+
+## Detailed Design
+
+### Implementation Details
+
+1. **Modified Batch Retrieval**:
+
+    ```go
+    func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+        res, err := m.sequencer.GetBatch(ctx)
+        if err != nil {
+            return nil, err
+        }
+
+        if res != nil && res.Batch != nil {
+            // Even if there are no transactions, return the batch with timestamp
+            // This allows empty blocks to maintain proper timing
+            if len(res.Batch.Transactions) == 0 {
+                return &BatchData{
+                    Batch: res.Batch,
+                    Time: res.Timestamp,
+                    Data: res.BatchData,
+                }, ErrNoBatch
+            }
+            return &BatchData{
+                Batch: res.Batch,
+                Time: res.Timestamp,
+                Data: res.BatchData,
+            }, nil
+        }
+        return nil, ErrNoBatch
path: specs/lazy-adr/adr-021-lazy-aggregation.md
user: login: graphite-app[bot]
body: The code sample in this ADR uses spaces for indentation rather than tabs. According to Go's standard formatting conventions, tabs should be used for indentation. While this is just example code in documentation, maintaining consistent formatting with actual implementation code would improve readability and avoid confusion when this design is implemented.
```suggestion
func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
	res, err := m.sequencer.GetBatch(ctx)
	if err != nil {
		return nil, err
	}

	if res != nil && res.Batch != nil {
		// Even if there are no transactions, return the batch with timestamp
		// This allows empty blocks to maintain proper timing
		if len(res.Batch.Transactions) == 0 {
			return &BatchData{
				Batch: res.Batch,
				Time:  res.Timestamp,
				Data:  res.BatchData,
			}, ErrNoBatch
		}
		return &BatchData{
			Batch: res.Batch,
			Time:  res.Timestamp,
			Data:  res.BatchData,
		}, nil
	}
	return nil, ErrNoBatch
}
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=rollkit&ref=ai-review-comment) (based on [custom rules](https://app.graphite.dev/diamond/?org=rollkit&ref=ai-review-comment&view=settings))*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-04-30T19:53:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+- 2024-01-24: Revised to use existing empty batch mechanism
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+## Decision
+
+Leverage the existing empty batch mechanism and `dataHashForEmptyTxs` to maintain block height consistency.
+
+## Detailed Design
+
+### Implementation Details
+
+1. **Modified Batch Retrieval**:
+
+    ```go
+func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+	res, err := m.sequencer.GetBatch(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	if res != nil && res.Batch != nil {
+		// Even if there are no transactions, return the batch with timestamp
+		// This allows empty blocks to maintain proper timing
+		if len(res.Batch.Transactions) == 0 {
+			return &BatchData{
+				Batch: res.Batch,
+				Time:  res.Timestamp,
+				Data:  res.BatchData,
+			}, ErrNoBatch
+		}
+		return &BatchData{
+			Batch: res.Batch,
+			Time:  res.Timestamp,
+			Data:  res.BatchData,
+		}, nil
+	}
+	return nil, ErrNoBatch
+}
+    }
path: specs/lazy-adr/adr-021-lazy-aggregation.md
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove extraneous closing brace**  
Line 46 correctly closes `func retrieveBatch`, but line 47 contains an extra `}`, which will confuse readers. Delete the stray brace.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~45-~45: Loose punctuation mark.
Context: ...ta, 		}, nil 	} 	return nil, ErrNoBatch }     }     ```  2. **Header Creation for...

(UNLIKELY_OPENING_PUNCTUATION)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits fbe8b10 to 8615079
created_at: 2025-04-30T19:57:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+- 2024-01-24: Revised to use existing empty batch mechanism
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+## Decision
+
+Leverage the existing empty batch mechanism and `dataHashForEmptyTxs` to maintain block height consistency.
+
+## Detailed Design
+
+### Implementation Details
+
+1. **Modified Batch Retrieval**:
+
+    ```go
+func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+	res, err := m.sequencer.GetBatch(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	if res != nil && res.Batch != nil {
+		// Even if there are no transactions, return the batch with timestamp
+		// This allows empty blocks to maintain proper timing
+		if len(res.Batch.Transactions) == 0 {
+			return &BatchData{
+				Batch: res.Batch,
+				Time:  res.Timestamp,
+				Data:  res.BatchData,
+			}, ErrNoBatch
+		}
+		return &BatchData{
+			Batch: res.Batch,
+			Time:  res.Timestamp,
+			Data:  res.BatchData,
+		}, nil
+	}
+	return nil, ErrNoBatch
+}
+    }
+    ```
+
+2. **Header Creation for Empty Blocks**:
+
+    ```go
+    func (m *Manager) createHeader(ctx context.Context, batchData *BatchData) (*types.Header, error) {
+        // Use batch timestamp even for empty blocks
+        timestamp := batchData.Time
+
+        if batchData.Transactions == nil || len(batchData.Transactions) == 0 {
+            // Use dataHashForEmptyTxs to indicate empty batch
+            return &types.Header{
+                DataHash: dataHashForEmptyTxs,
+                Timestamp: timestamp,
+                // ... other header fields
+            }, nil
path: specs/lazy-adr/adr-021-lazy-aggregation.md
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix field reference for transactions**  
`BatchData` doesn’t expose `Transactions` directly—transactions live under `batchData.Batch.Transactions`. Update the condition to:  
```diff
-if batchData.Transactions == nil || len(batchData.Transactions) == 0 {
+if batchData.Batch == nil || len(batchData.Batch.Transactions) == 0 {
```  
This ensures the empty‐block path is detected correctly.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        if batchData.Batch == nil || len(batchData.Batch.Transactions) == 0 {
            // Use dataHashForEmptyTxs to indicate empty batch
            return &types.Header{
                DataHash: dataHashForEmptyTxs,
                Timestamp: timestamp,
                // ... other header fields
            }, nil
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits fbe8b10 to 8615079
created_at: 2025-04-30T19:57:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,21 +55,37 @@ func (m *Manager) lazyAggregationLoop(ctx context.Context, blockTimer *time.Time
 			return
 
 		case <-lazyTimer.C:
-		case <-blockTimer.C:
-		}
+			m.logger.Debug("Lazy timer triggered block production")
+			m.produceBlock(ctx, "lazy_timer", lazyTimer, blockTimer)
 
-		// Reset the start time
-		start := time.Now()
+		case <-blockTimer.C:
+			m.logger.Debug("Block timer triggered block production")
+			if m.txsAvailable {
+				m.produceBlock(ctx, "block_timer", lazyTimer, blockTimer)
+			}
+			m.txsAvailable = false
 
-		// Attempt to publish the block regardless of activity
-		if err := m.publishBlock(ctx); err != nil && ctx.Err() == nil {
-			m.logger.Error("error while publishing block", "error", err)
+		case <-m.txNotifyCh:
+			m.txsAvailable = true
 		}
+	}
+}
+
+// produceBlock handles the common logic for producing a block and resetting timers
+func (m *Manager) produceBlock(ctx context.Context, trigger string, lazyTimer, blockTimer *time.Timer) {
+	// Record the start time
+	start := time.Now()
 
-		// Reset both timers for the next aggregation window
-		lazyTimer.Reset(getRemainingSleep(start, m.config.Node.LazyBlockTime.Duration))
-		blockTimer.Reset(getRemainingSleep(start, m.config.Node.BlockTime.Duration))
+	// Attempt to publish the block
+	if err := m.publishBlock(ctx); err != nil && ctx.Err() == nil {
+		m.logger.Error("error while publishing block", "trigger", trigger, "error", err)
+	} else {
+		m.logger.Debug("Successfully published block", "trigger", trigger)
 	}
+
+	// Reset both timers for the next aggregation window
+	lazyTimer.Reset(getRemainingSleep(start, m.config.Node.LazyBlockInterval.Duration))
+	blockTimer.Reset(getRemainingSleep(start, m.config.Node.BlockTime.Duration))
 }
path: block/aggregation.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`produceBlock` should reset timers only after successful `Reset` call**

`time.Timer.Reset` must be called on *stopped* or *expired* timers. `lazyTimer` is guaranteed to have fired (we’re in its case) but `blockTimer` may still be running when `produceBlock` is called from the lazy timer path, leading to undefined behaviour (race panic) in Go <1.22.

Safe pattern:

```go
lazyTimer.Reset(getRemainingSleep(...))       // safe – it just fired
if !blockTimer.Stop() { <-blockTimer.C }      // drain if needed
blockTimer.Reset(getRemainingSleep(...))
```

Consider adding the explicit `Stop`+drain before `Reset` for both timers to avoid this edge case.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T15:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,21 +55,37 @@
 			return
 
 		case <-lazyTimer.C:
-		case <-blockTimer.C:
-		}
+			m.logger.Debug("Lazy timer triggered block production")
+			m.produceBlock(ctx, "lazy_timer", lazyTimer, blockTimer)
 
-		// Reset the start time
-		start := time.Now()
+		case <-blockTimer.C:
+			m.logger.Debug("Block timer triggered block production")
+			if m.txsAvailable {
+				m.produceBlock(ctx, "block_timer", lazyTimer, blockTimer)
+			}
+			m.txsAvailable = false
 
-		// Attempt to publish the block regardless of activity
-		if err := m.publishBlock(ctx); err != nil && ctx.Err() == nil {
-			m.logger.Error("error while publishing block", "error", err)
+		case <-m.txNotifyCh:
+			m.txsAvailable = true
path: block/aggregation.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`blockTimer` is never reset when no txs are available – timer stalls**

If `txsAvailable` is false, the `blockTimer` branch drains the timer’s channel but does not call `Reset`, so the timer stops firing permanently until the lazy timer ticks. This defeats the purpose of the block timer and can delay block production indefinitely after the first idle interval.

```diff
case <-blockTimer.C:
    m.logger.Debug("Block timer triggered block production")
    if m.txsAvailable {
        m.produceBlock(ctx, "block_timer", lazyTimer, blockTimer)
        m.txsAvailable = false
+   } else {
+       // Ensure we keep ticking even when there are no txs
+       blockTimer.Reset(m.config.Node.BlockTime.Duration)
    }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        case <-blockTimer.C:
            m.logger.Debug("Block timer triggered block production")
            if m.txsAvailable {
                m.produceBlock(ctx, "block_timer", lazyTimer, blockTimer)
                m.txsAvailable = false
            } else {
                // Ensure we keep ticking even when there are no txs
                blockTimer.Reset(m.config.Node.BlockTime.Duration)
            }

        case <-m.txNotifyCh:
            m.txsAvailable = true
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 4949f09
created_at: 2025-05-02T15:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -463,8 +467,14 @@
 			"txCount", len(res.Batch.Transactions),
 			"timestamp", res.Timestamp)
 
+		// Even if there are no transactions, return the batch with timestamp
+		// This allows empty blocks to maintain proper timing
 		if len(res.Batch.Transactions) == 0 {
-			return nil, ErrNoBatch
+			return &BatchData{
+				Batch: res.Batch,
+				Time:  res.Timestamp,
+				Data:  res.BatchData,
+			}, ErrNoBatch
 		}
path: block/manager.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid returning a non-nil value together with an error**

`retrieveBatch` now returns a fully–populated `*BatchData` **and** `ErrNoBatch` for the empty-batch case.  
In Go it is conventional that *if `err != nil`, the other return values are ignored* (and therefore often assumed to be zero / nil). Call-sites that follow that convention may discard the batch timestamp you rely on, defeating the purpose of this change and potentially causing subtle bugs.

Consider one of the following options instead:

```diff
- return &BatchData{ ... }, ErrNoBatch
+ return &BatchData{ ... }, nil, true   // add a boolean “empty” flag
```

or define a sentinel error type that **does not** represent a failure (so callers don’t ignore the data) and document this clearly.  
Either way, ensure all callers are updated accordingly.  



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T15:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -679,7 +709,74 @@
 func (m *Manager) createBlock(ctx context.Context, height uint64, lastSignature *types.Signature, lastHeaderHash types.Hash, batchData *BatchData) (*types.SignedHeader, *types.Data, error) {
 	m.lastStateMtx.RLock()
 	defer m.lastStateMtx.RUnlock()
-	return m.execCreateBlock(ctx, height, lastSignature, lastHeaderHash, m.lastState, batchData)
+
+	if m.signer == nil {
+		return nil, nil, fmt.Errorf("signer is nil; cannot create block")
+	}
+
+	key, err := m.signer.GetPublic()
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to get proposer public key: %w", err)
+	}
+
+	// check that the proposer address is the same as the genesis proposer address
+	address, err := m.signer.GetAddress()
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to get proposer address: %w", err)
+	}
+	if !bytes.Equal(m.genesis.ProposerAddress, address) {
+		return nil, nil, fmt.Errorf("proposer address is not the same as the genesis proposer address %x != %x", address, m.genesis.ProposerAddress)
+	}
+
+	// Determine if this is an empty block
+	isEmpty := batchData.Batch == nil || len(batchData.Batch.Transactions) == 0
+
+	// Set the appropriate data hash based on whether this is an empty block
+	var dataHash types.Hash
+	if isEmpty {
+		dataHash = dataHashForEmptyTxs // Use dataHashForEmptyTxs for empty blocks
+	} else {
+		dataHash = convertBatchDataToBytes(batchData.Data)
+	}
path: block/manager.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`DataHash` should be a digest, not raw concatenation**

`convertBatchDataToBytes(batchData.Data)` concatenates length-prefixed blobs and stores the raw bytes in `Header.DataHash`.  
That makes the header size unbounded and undermines the very purpose of a *hash* field (fixed-width commitment).

Replace with an actual digest (e.g. SHA-256) and keep the constant 32-byte width:

```diff
-	dataHash = convertBatchDataToBytes(batchData.Data)
+	hash := sha256.Sum256(convertBatchDataToBytes(batchData.Data))
+	dataHash = hash[:]
```

Remember to add the `crypto/sha256` import.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T15:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -463,8 +467,14 @@
 			"txCount", len(res.Batch.Transactions),
 			"timestamp", res.Timestamp)
 
+		// Even if there are no transactions, return the batch with timestamp
+		// This allows empty blocks to maintain proper timing
 		if len(res.Batch.Transactions) == 0 {
-			return nil, ErrNoBatch
+			return &BatchData{
+				Batch: res.Batch,
+				Time:  res.Timestamp,
+				Data:  res.BatchData,
path: block/manager.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`lastBatchData` not updated for empty batches**

Inside the empty-batch branch we skip persisting `LastBatchDataKey` and leave `m.lastBatchData` untouched.  
If the sequencer keeps returning the same empty batch, the manager will treat every retrieval as “new”, potentially producing an unbounded stream of empty blocks.

```diff
			// Even if there are no transactions, return the batch with timestamp
-			if len(res.Batch.Transactions) == 0 {
+			if len(res.Batch.Transactions) == 0 {
+				_ = m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData))
+				m.lastBatchData = res.BatchData
 				return &BatchData{ … }, ErrNoBatch
 			}
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        // This allows empty blocks to maintain proper timing
        if len(res.Batch.Transactions) == 0 {
            // Even if there are no transactions, update lastBatchData so we don’t
            // repeatedly emit the same empty batch, and persist it to metadata.
            _ = m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData))
            m.lastBatchData = res.BatchData
            return &BatchData{
                Batch: res.Batch,
                Time:  res.Timestamp,
                Data:  res.BatchData,
            }, ErrNoBatch
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 4949f09
created_at: 2025-05-02T15:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -540,26 +552,44 @@
 	} else {
 		batchData, err := m.retrieveBatch(ctx)
 		if errors.Is(err, ErrNoBatch) {
-			m.logger.Info("No batch retrieved from sequencer, skipping block production")
-			return nil // Indicate no block was produced
+			// Even with no transactions, we still want to create a block with an empty batch
+			if batchData != nil {
+				m.logger.Info("Creating empty block", "height", newHeight)
+
+				// Create an empty block
+				header, data, err = m.createBlock(ctx, newHeight, lastSignature, lastHeaderHash, batchData)
+				if err != nil {
+					return err
+				}
+
+				err = m.store.SaveBlockData(ctx, header, data, &signature)
+				if err != nil {
+					return SaveBlockError{err}
+				}
+			} else {
+				// If we don't have a batch at all (not even an empty one), skip block production
+				m.logger.Info("No batch retrieved from sequencer, skipping block production")
+				return nil
+			}
 		} else if err != nil {
 			return fmt.Errorf("failed to get transactions from batch: %w", err)
-		}
-
-		// sanity check timestamp for monotonically increasing
-		if batchData.Before(lastHeaderTime) {
-			return fmt.Errorf("timestamp is not monotonically increasing: %s < %s", batchData.Time, m.getLastBlockTime())
-		}
-		m.logger.Info("Creating and publishing block", "height", newHeight)
-		header, data, err = m.createBlock(ctx, newHeight, lastSignature, lastHeaderHash, batchData)
-		if err != nil {
-			return err
-		}
-		m.logger.Debug("block info", "num_tx", len(data.Txs))
+		} else {
+			// We have a batch with transactions
+			// sanity check timestamp for monotonically increasing
+			if batchData.Before(lastHeaderTime) {
+				return fmt.Errorf("timestamp is not monotonically increasing: %s < %s", batchData.Time, m.getLastBlockTime())
+			}
+			m.logger.Info("Creating and publishing block", "height", newHeight)
+			header, data, err = m.createBlock(ctx, newHeight, lastSignature, lastHeaderHash, batchData)
+			if err != nil {
+				return err
+			}
+			m.logger.Debug("block info", "num_tx", len(data.Txs))
 
-		err = m.store.SaveBlockData(ctx, header, data, &signature)
-		if err != nil {
-			return SaveBlockError{err}
+			err = m.store.SaveBlockData(ctx, header, data, &signature)
+			if err != nil {
+				return SaveBlockError{err}
+			}
 		}
path: block/manager.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Duplicate `SaveBlockData` calls write an unsigned header first**

The empty-block branch writes the block to the store **before** the genuine proposer signature is calculated, then rewrites it again after `applyBlock`.  
Persisting an unsigned header, even transiently, risks other components indexing or gossiping an invalid block.

Option A – remove the early write entirely (preferred):

```diff
-				err = m.store.SaveBlockData(ctx, header, data, &signature)
-				if err != nil {
-					return SaveBlockError{err}
-				}
```

Option B – delay storage until the signature is available (i.e. move this block below the `getSignature` call).  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
			} else {
				// If we don't have a batch at all (not even an empty one), skip block production
				m.logger.Info("No batch retrieved from sequencer, skipping block production")
				return nil
			}
		} else if err != nil {
			return fmt.Errorf("failed to get transactions from batch: %w", err)
		} else {
			// We have a batch with transactions
			// sanity check timestamp for monotonically increasing
			if batchData.Before(lastHeaderTime) {
				return fmt.Errorf("timestamp is not monotonically increasing: %s < %s", batchData.Time, m.getLastBlockTime())
			}
			m.logger.Info("Creating and publishing block", "height", newHeight)
			header, data, err = m.createBlock(ctx, newHeight, lastSignature, lastHeaderHash, batchData)
			if err != nil {
				return err
			}
			m.logger.Debug("block info", "num_tx", len(data.Txs))

			err = m.store.SaveBlockData(ctx, header, data, &signature)
			if err != nil {
				return SaveBlockError{err}
			}
		}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit f8826b4
created_at: 2025-05-02T15:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -458,22 +462,23 @@ func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
 		return nil, err
 	}
 
+	errRetrieveBatch := ErrNoBatch
 	if res != nil && res.Batch != nil {
 		m.logger.Debug("Retrieved batch",
 			"txCount", len(res.Batch.Transactions),
 			"timestamp", res.Timestamp)
 
-		if len(res.Batch.Transactions) == 0 {
-			return nil, ErrNoBatch
-		}
-		h := convertBatchDataToBytes(res.BatchData)
-		if err := m.store.SetMetadata(ctx, LastBatchDataKey, h); err != nil {
-			m.logger.Error("error while setting last batch hash", "error", err)
+		// Even if there are no transactions, return the batch with timestamp
+		// This allows empty blocks to maintain proper timing
+		if len(res.Batch.Transactions) > 0 {
+			errRetrieveBatch = nil
 		}
-		m.lastBatchData = res.BatchData
-		return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, nil
 	}
-	return nil, ErrNoBatch
+	if err := m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData)); err != nil {
+		m.logger.Error("error while setting last batch hash", "error", err)
+	}
+	m.lastBatchData = res.BatchData
+	return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, errRetrieveBatch
path: block/manager.go
user: login: graphite-app[bot]
body: The error handling in `retrieveBatch` appears inconsistent. The function sets `errRetrieveBatch = nil` when transactions exist, but this conditional logic doesn't properly control the return flow. 

Currently, regardless of whether transactions exist or not, the code always proceeds to set metadata and return the batch with `errRetrieveBatch`. This means that even when transactions are present (and `errRetrieveBatch` is set to `nil`), the function will still return `ErrNoBatch` for empty batches.

Consider restructuring to ensure the correct error is returned in each case:

```go
if res != nil && res.Batch != nil {
    m.logger.Debug("Retrieved batch",
        "txCount", len(res.Batch.Transactions),
        "timestamp", res.Timestamp)

    // Set metadata and update lastBatchData
    if err := m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData)); err != nil {
        m.logger.Error("error while setting last batch hash", "error", err)
    }
    m.lastBatchData = res.BatchData

    // Return appropriate error based on transaction count
    if len(res.Batch.Transactions) > 0 {
        return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, nil
    }
    return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, ErrNoBatch
}
return nil, ErrNoBatch
```
```suggestion
	errRetrieveBatch := ErrNoBatch
	if res != nil && res.Batch != nil {
		m.logger.Debug("Retrieved batch",
			"txCount", len(res.Batch.Transactions),
			"timestamp", res.Timestamp)

		// Set metadata and update lastBatchData
		if err := m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData)); err != nil {
			m.logger.Error("error while setting last batch hash", "error", err)
		}
		m.lastBatchData = res.BatchData

		// Return appropriate error based on transaction count
		if len(res.Batch.Transactions) > 0 {
			return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, nil
		}
		// Even if there are no transactions, return the batch with timestamp
		// This allows empty blocks to maintain proper timing
		return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, ErrNoBatch
	}
	return nil, ErrNoBatch
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=rollkit&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-02T16:13:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,187 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+- 2024-01-24: Revised to use existing empty batch mechanism
+- 2024-01-25: Updated with implementation details from aggregation.go
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+## Decision
+
+Leverage the existing empty batch mechanism and `dataHashForEmptyTxs` to maintain block height consistency.
+
+## Detailed Design
+
+### Implementation Details
+
+1. **Modified Batch Retrieval**:
+
+    The batch retrieval mechanism has been modified to handle empty batches differently. Instead of discarding empty batches, we now return them with the ErrNoBatch error, allowing the caller to create empty blocks with proper timestamps. This ensures that block timing remains consistent even during periods of inactivity.
+
+    ```go
+    func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+        res, err := m.sequencer.GetNextBatch(ctx, req)
+        if err != nil {
+            return nil, err
+        }
+
+        if res != nil && res.Batch != nil {
+            m.logger.Debug("Retrieved batch",
+                "txCount", len(res.Batch.Transactions),
+                "timestamp", res.Timestamp)
+
+            var errRetrieveBatch error
+            // Even if there are no transactions, return the batch with timestamp
+            // This allows empty blocks to maintain proper timing
+            if len(res.Batch.Transactions) == 0 {
+                errRetrieveBatch = ErrNoBatch
+            }
+            // Even if there are no transactions, update lastBatchData so we don’t
+            // repeatedly emit the same empty batch, and persist it to metadata.
+            if err := m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData)); err != nil {
+                m.logger.Error("error while setting last batch hash", "error", err)
+            }
+            m.lastBatchData = res.BatchData
+            return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, errRetrieveBatch
+	    }
+	    return nil, ErrNoBatch
+    }
+    ```
+
+2. **Empty Block Creation**:
+
+    The block publishing logic has been enhanced to create empty blocks when a batch with no transactions is received. This uses the special `dataHashForEmptyTxs` value to indicate an empty batch, maintaining the block height consistency with the DA layer while minimizing overhead.
+
+    ```go
+    // In publishBlock method
+    batchData, err := m.retrieveBatch(ctx)
+    if errors.Is(err, ErrNoBatch) {
+        // Even with no transactions, we still want to create a block with an empty batch
+        if batchData != nil {
+            m.logger.Info("Creating empty block", "height", newHeight)
+
+            // For empty blocks, use dataHashForEmptyTxs to indicate empty batch
+            header, data, err = m.createEmptyBlock(ctx, newHeight, lastSignature, lastHeaderHash, batchData)
+            if err != nil {
+                return err
+            }
+
+            err = m.store.SaveBlockData(ctx, header, data, &signature)
+            if err != nil {
+                return SaveBlockError{err}
+            }
+        } else {
+            // If we don't have a batch at all (not even an empty one), skip block production
+            m.logger.Info("No batch retrieved from sequencer, skipping block production")
+            return nil
+        }
+    }
+    ```
path: specs/lazy-adr/adr-021-lazy-aggregation.md
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Inconsistency between ADR and implementation**

The `createEmptyBlock` method mentioned in the ADR example doesn't exist in the actual implementation. Instead, the code uses a single `createBlock` method with logic to handle empty blocks.


While the overall approach is consistent, the ADR should be updated to reflect the actual implementation for clarity and to avoid confusion for developers.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T16:32:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -254,7 +254,7 @@ signer:
 	// Compare the results - they should be identical
 	require.Equal(t, cfgFromLoad.RootDir, cfgFromViper.RootDir, "RootDir should match")
 	require.Equal(t, cfgFromLoad.DA.GasPrice, cfgFromViper.DA.GasPrice, "DA.GasPrice should match")
-	require.Equal(t, cfgFromLoad.Node.LazyAggregator, cfgFromViper.Node.LazyAggregator, "Node.LazyAggregator should match")
+	require.Equal(t, cfgFromLoad.Node.LazyMode, cfgFromViper.Node.LazyMode, "Node.LazyAggregator should match")
path: pkg/config/config_test.go
user: login: graphite-app[bot]
body: The assertion message still refers to `Node.LazyAggregator` but should be updated to `Node.LazyMode` to match the actual field name being tested. This would improve clarity and maintain consistency with the renamed configuration field.
```suggestion
	require.Equal(t, cfgFromLoad.Node.LazyMode, cfgFromViper.Node.LazyMode, "Node.LazyMode should match")
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=rollkit&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-02T16:36:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,209 @@
+# ADR 021: Lazy Aggregation with DA Layer Consistency
+
+## Changelog
+
+- 2024-01-24: Initial draft
+- 2024-01-24: Revised to use existing empty batch mechanism
+- 2024-01-25: Updated with implementation details from aggregation.go
+
+## Context
+
+Rollkit's lazy aggregation mechanism currently produces blocks at set intervals when no transactions are present, and immediately when transactions are available. However, this approach creates inconsistency with the DA layer (Celestia) as empty blocks are not posted to the DA layer. This breaks the expected 1:1 mapping between DA layer blocks and execution layer blocks in EVM environments.
+
+## Decision
+
+Leverage the existing empty batch mechanism and `dataHashForEmptyTxs` to maintain block height consistency.
+
+## Detailed Design
+
+### Implementation Details
+
+1. **Modified Batch Retrieval**:
+
+    The batch retrieval mechanism has been modified to handle empty batches differently. Instead of discarding empty batches, we now return them with the ErrNoBatch error, allowing the caller to create empty blocks with proper timestamps. This ensures that block timing remains consistent even during periods of inactivity.
+
+    ```go
+    func (m *Manager) retrieveBatch(ctx context.Context) (*BatchData, error) {
+        res, err := m.sequencer.GetNextBatch(ctx, req)
+        if err != nil {
+            return nil, err
+        }
+
+        if res != nil && res.Batch != nil {
+            m.logger.Debug("Retrieved batch",
+                "txCount", len(res.Batch.Transactions),
+                "timestamp", res.Timestamp)
+
+            var errRetrieveBatch error
+            // Even if there are no transactions, return the batch with timestamp
+            // This allows empty blocks to maintain proper timing
+            if len(res.Batch.Transactions) == 0 {
+                errRetrieveBatch = ErrNoBatch
+            }
+            // Even if there are no transactions, update lastBatchData so we don't
+            // repeatedly emit the same empty batch, and persist it to metadata.
+            if err := m.store.SetMetadata(ctx, LastBatchDataKey, convertBatchDataToBytes(res.BatchData)); err != nil {
+                m.logger.Error("error while setting last batch hash", "error", err)
+            }
+            m.lastBatchData = res.BatchData
+            return &BatchData{Batch: res.Batch, Time: res.Timestamp, Data: res.BatchData}, errRetrieveBatch
+	    }
+	    return nil, ErrNoBatch
+    }
+    ```
+
+2. **Empty Block Creation**:
+
+    The block publishing logic has been enhanced to create empty blocks when a batch with no transactions is received. This uses the special `dataHashForEmptyTxs` value to indicate an empty batch, maintaining the block height consistency with the DA layer while minimizing overhead.
+
+    ```go
+    // In publishBlock method
+    batchData, err := m.retrieveBatch(ctx)
+		if err != nil {
+			if errors.Is(err, ErrNoBatch) {
+				if batchData == nil {
+					m.logger.Info("No batch retrieved from sequencer, skipping block production")
+					return nil
+				}
+				m.logger.Info("Creating empty block", "height", newHeight)
+			} else {
+				return fmt.Errorf("failed to get transactions from batch: %w", err)
+			}
+		} else {
+			if batchData.Before(lastHeaderTime) {
+				return fmt.Errorf("timestamp is not monotonically increasing: %s < %s", batchData.Time, m.getLastBlockTime())
+			}
+			m.logger.Info("Creating and publishing block", "height", newHeight)
+			m.logger.Debug("block info", "num_tx", len(batchData.Batch.Transactions))
+		}
+
+		header, data, err = m.createBlock(ctx, newHeight, lastSignature, lastHeaderHash, batchData)
+		if err != nil {
+			return err
+		}
+
+		if err = m.store.SaveBlockData(ctx, header, data, &signature); err != nil {
+			return SaveBlockError{err}
+		}
+    ```
+
+3. **Lazy Aggregation Loop**:
+
+    A dedicated lazy aggregation loop has been implemented with dual timer mechanisms. The `lazyTimer` ensures blocks are produced at regular intervals even during network inactivity, while the `blockTimer` handles normal block production when transactions are available. Transaction notifications from the `Reaper` to the `Manager` are now handled via the `txNotifyCh` channel: when the `Reaper` detects new transactions, it calls `Manager.NotifyNewTransactions()`, which performs a non-blocking signal on this channel. See the tests in `block/lazy_aggregation_test.go` for verification of this behavior.
+
+    ```go
+    // In Reaper.SubmitTxs
+    if r.manager != nil && len(newTxs) > 0 {
+        r.logger.Debug("Notifying manager of new transactions")
+        r.manager.NotifyNewTransactions() // Signals txNotifyCh
+    }
+
+    // In Manager.NotifyNewTransactions
+    func (m *Manager) NotifyNewTransactions() {
+        select {
+        case m.txNotifyCh <- struct{}{}:
+            // Successfully sent notification
+        default:
+            // Channel buffer is full, notification already pending
+        }
+    }
+    // Modiified lazyAggregationLoop
path: specs/lazy-adr/adr-021-lazy-aggregation.md
user: login: graphite-app[bot]
body: There's a small typo in the comment: `Modiified` should be `Modified`. This doesn't affect functionality but should be corrected for documentation clarity.
```suggestion
    // Modified lazyAggregationLoop
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=rollkit&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-02T16:57:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,3 +127,62 @@
 
 	return time.Millisecond
 }
+
+// TestNormalAggregationLoop_TxNotification tests that transaction notifications are handled in normal mode
+func TestNormalAggregationLoop_TxNotification(t *testing.T) {
+	require := require.New(t)
+
+	blockTime := 100 * time.Millisecond
+	m, pubMock := setupTestManager(t, blockTime, 0)
path: block/aggregation.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix undefined reference to setupTestManager**

The test is referencing `setupTestManager` which is defined in `lazy_aggregation_test.go` but isn't accessible in this file, causing build failures.


You need to either:

1. Move the `setupTestManager` function to a common test utilities file accessible by both tests, or
2. Duplicate the function in this test file with a local implementation.

Here's a quick fix to add the function to this file:

```go
func setupTestManager(t *testing.T, blockTime, lazyTime time.Duration) (*Manager, *mockPublishBlock) {
	t.Helper()
	pubMock := &mockPublishBlock{
		calls: make(chan struct{}, 10), // Buffer to avoid blocking in tests
	}
	logger := log.NewTestLogger(t)
	m := &Manager{
		logger: logger,
		config: config.Config{
			Node: config.NodeConfig{
				BlockTime:         config.DurationWrapper{Duration: blockTime},
				LazyBlockInterval: config.DurationWrapper{Duration: lazyTime},
				LazyMode:          true, // Ensure lazy mode is active
			},
		},
		publishBlock: pubMock.publish,
	}
	return m, pubMock
}

// mockPublishBlock is used to control the behavior of publishBlock during tests
type mockPublishBlock struct {
	mu    sync.Mutex
	calls chan struct{}
	err   error
	delay time.Duration // Optional delay to simulate processing time
}

func (m *mockPublishBlock) reset() {
	m.mu.Lock()
	defer m.mu.Unlock()
	// Clear the channel
	for len(m.calls) > 0 {
		<-m.calls
	}
}

func (m *mockPublishBlock) publish(ctx context.Context) error {
	m.mu.Lock()
	err := m.err
	delay := m.delay
	m.mu.Unlock()

	if delay > 0 {
		time.Sleep(delay)
	}
	// Non-blocking send in case the channel buffer is full or receiver is not ready
	select {
	case m.calls <- struct{}{}:
	default:
	}
	return err
}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: test / Run Unit Tests</summary>

[failure] 136-136: 
undefined: setupTestManager

</details>
<details>
<summary>🪛 GitHub Check: lint / golangci-lint</summary>

[failure] 136-136: 
undefined: setupTestManager) (typecheck)

</details>
<details>
<summary>🪛 GitHub Actions: CI and Release</summary>

[error] 136-136: Build error: undefined identifier 'setupTestManager' at line 136.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T19:00:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,344 @@
+package block
+
+import (
+	"context"
+	"errors"
+	"sync"
+	"testing"
+	"time"
+
+	"cosmossdk.io/log"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/rollkit/rollkit/pkg/config"
+)
+
+// mockPublishBlock is used to control the behavior of publishBlock during tests
+type mockPublishBlock struct {
+	mu    sync.Mutex
+	calls chan struct{}
+	err   error
+	delay time.Duration // Optional delay to simulate processing time
+}
+
+// reset clears the calls channel in mockPublishBlock.
+func (m *mockPublishBlock) reset() {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	// Clear the channel
+	for len(m.calls) > 0 {
+		<-m.calls
+	}
+}
+
+func (m *mockPublishBlock) publish(ctx context.Context) error {
+	m.mu.Lock()
+	err := m.err
+	delay := m.delay
+	m.mu.Unlock()
+
+	if delay > 0 {
+		time.Sleep(delay)
+	}
+	// Non-blocking send in case the channel buffer is full or receiver is not ready
+	select {
+	case m.calls <- struct{}{}:
+	default:
+	}
+	return err
+}
+
+func setupTestManager(t *testing.T, blockTime, lazyTime time.Duration) (*Manager, *mockPublishBlock) {
+	t.Helper()
+	pubMock := &mockPublishBlock{
+		calls: make(chan struct{}, 10), // Buffer to avoid blocking in tests
+	}
+	logger := log.NewTestLogger(t)
+	m := &Manager{
+		logger: logger,
+		config: config.Config{
+			Node: config.NodeConfig{
+				BlockTime:         config.DurationWrapper{Duration: blockTime},
+				LazyBlockInterval: config.DurationWrapper{Duration: lazyTime},
+				LazyMode:          true, // Ensure lazy mode is active
+			},
+		},
+		publishBlock: pubMock.publish,
+	}
+	return m, pubMock
+}
path: block/lazy_aggregation_test.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Move setupTestManager to a shared test package or file**

This function is referenced in `block/aggregation.go` tests but is inaccessible outside this file, causing build failures.


Consider moving this function (and its dependencies like `mockPublishBlock`) to a shared test utilities file that can be imported by all test files. A common pattern is to create a `testutil` package or a `testing.go` file accessible to all tests.

Alternative: Make this function available by adding a build tag like `// +build testing` at the top of the file, though a shared test package would be clearer.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T19:00:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,344 @@
+package block
+
+import (
+	"context"
+	"errors"
+	"sync"
+	"testing"
+	"time"
+
+	"cosmossdk.io/log"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/rollkit/rollkit/pkg/config"
+)
+
+// mockPublishBlock is used to control the behavior of publishBlock during tests
+type mockPublishBlock struct {
+	mu    sync.Mutex
+	calls chan struct{}
+	err   error
+	delay time.Duration // Optional delay to simulate processing time
+}
+
+// reset clears the calls channel in mockPublishBlock.
+func (m *mockPublishBlock) reset() {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	// Clear the channel
+	for len(m.calls) > 0 {
+		<-m.calls
+	}
+}
+
+func (m *mockPublishBlock) publish(ctx context.Context) error {
+	m.mu.Lock()
+	err := m.err
+	delay := m.delay
+	m.mu.Unlock()
+
+	if delay > 0 {
+		time.Sleep(delay)
+	}
+	// Non-blocking send in case the channel buffer is full or receiver is not ready
+	select {
+	case m.calls <- struct{}{}:
+	default:
+	}
+	return err
+}
+
+func setupTestManager(t *testing.T, blockTime, lazyTime time.Duration) (*Manager, *mockPublishBlock) {
+	t.Helper()
+	pubMock := &mockPublishBlock{
+		calls: make(chan struct{}, 10), // Buffer to avoid blocking in tests
+	}
+	logger := log.NewTestLogger(t)
+	m := &Manager{
+		logger: logger,
+		config: config.Config{
+			Node: config.NodeConfig{
+				BlockTime:         config.DurationWrapper{Duration: blockTime},
+				LazyBlockInterval: config.DurationWrapper{Duration: lazyTime},
+				LazyMode:          true, // Ensure lazy mode is active
+			},
+		},
+		publishBlock: pubMock.publish,
+	}
+	return m, pubMock
+}
+
+// TestLazyAggregationLoop_BlockTimerTrigger tests that a block is published when the blockTimer fires first.
+func TestLazyAggregationLoop_BlockTimerTrigger(t *testing.T) {
+	require := require.New(t)
+
+	// Create a mock for the publishBlock function that counts calls
+	callCount := 0
+	mockPublishFn := func(ctx context.Context) error {
+		callCount++
+		return nil
+	}
+
+	// Setup a manager with our mock publish function
+	blockTime := 50 * time.Millisecond
+	lazyTime := 200 * time.Millisecond // Lazy timer fires later
+	m, _ := setupTestManager(t, blockTime, lazyTime)
+	m.publishBlock = mockPublishFn
+
+	// Set txsAvailable to true to ensure block timer triggers block production
+	m.txsAvailable = true
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	// Start the lazy aggregation loop
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		blockTimer := time.NewTimer(0) // Fire immediately first time
+		defer blockTimer.Stop()
+		m.lazyAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for at least one block to be published
+	time.Sleep(blockTime * 2)
+
+	// Cancel the context to stop the loop
+	cancel()
+	wg.Wait()
+
+	// Verify that at least one block was published
+	require.GreaterOrEqual(callCount, 1, "Expected at least one block to be published")
+}
+
+// TestLazyAggregationLoop_LazyTimerTrigger tests that a block is published when the lazyTimer fires first.
+func TestLazyAggregationLoop_LazyTimerTrigger(t *testing.T) {
+	assert := assert.New(t)
+	require := require.New(t)
+
+	blockTime := 200 * time.Millisecond // Block timer fires later
+	lazyTime := 50 * time.Millisecond
+	m, pubMock := setupTestManager(t, blockTime, lazyTime)
+
+	// Set txsAvailable to false to ensure lazy timer triggers block production
+	// and block timer doesn't
+	m.txsAvailable = false
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		// Use real timers for this test
+		blockTimer := time.NewTimer(0) // Fire immediately first time
+		defer blockTimer.Stop()
+		m.lazyAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for the first publish call triggered by the initial immediate lazyTimer fire
+	select {
+	case <-pubMock.calls:
+		// Good, first block published by lazy timer
+	case <-time.After(2 * lazyTime): // Give some buffer
+		require.Fail("timed out waiting for first block publication")
+	}
+
+	// Wait for the second publish call, triggered by lazyTimer reset
+	select {
+	case <-pubMock.calls:
+		// Good, second block published by lazyTimer
+	case <-time.After(2 * lazyTime): // Give some buffer
+		require.Fail("timed out waiting for second block publication (lazyTimer)")
+	}
+
+	// Ensure blockTimer didn't trigger a publish yet (since txsAvailable is false)
+	assert.Len(pubMock.calls, 0, "Expected no more publish calls yet")
+
+	cancel()
+	wg.Wait()
+}
+
+// TestLazyAggregationLoop_PublishError tests that the loop continues after a publish error.
+func TestLazyAggregationLoop_PublishError(t *testing.T) {
+	require := require.New(t)
+
+	blockTime := 50 * time.Millisecond
+	lazyTime := 100 * time.Millisecond
+	m, pubMock := setupTestManager(t, blockTime, lazyTime)
+
+	pubMock.mu.Lock()
+	pubMock.err = errors.New("publish failed")
+	pubMock.mu.Unlock()
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		// Use real timers
+		blockTimer := time.NewTimer(0)
+		defer blockTimer.Stop()
+		m.lazyAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for the first publish attempt (which will fail)
+	select {
+	case <-pubMock.calls:
+	case <-time.After(2 * blockTime):
+		require.Fail("timed out waiting for first block publication attempt")
+	}
+
+	// Remove the error for subsequent calls
+	pubMock.mu.Lock()
+	pubMock.err = nil
+	pubMock.mu.Unlock()
+
+	// Wait for the second publish attempt (should succeed)
+	// Use a longer timeout since we need to wait for either the lazy timer or block timer to fire
+	select {
+	case <-pubMock.calls:
+	case <-time.After(2 * lazyTime): // Use the longer of the two timers with some buffer
+		require.Fail("timed out waiting for second block publication attempt after error")
+	}
+
+	cancel()
+	wg.Wait()
+}
+
+// TestGetRemainingSleep tests the calculation of sleep duration.
+func TestGetRemainingSleep(t *testing.T) {
+	assert := assert.New(t)
+
+	interval := 100 * time.Millisecond
+
+	// Case 1: Elapsed time is less than interval
+	start1 := time.Now().Add(-30 * time.Millisecond) // Started 30ms ago
+	sleep1 := getRemainingSleep(start1, interval)
+	// Expecting interval - elapsed = 100ms - 30ms = 70ms (allow for slight variations)
+	assert.InDelta(interval-30*time.Millisecond, sleep1, float64(5*time.Millisecond), "Case 1 failed")
+
+	// Case 2: Elapsed time is greater than or equal to interval
+	start2 := time.Now().Add(-120 * time.Millisecond) // Started 120ms ago
+	sleep2 := getRemainingSleep(start2, interval)
+	// Expecting minimum sleep time
+	assert.Equal(time.Millisecond, sleep2, "Case 2 failed")
+
+	// Case 3: Elapsed time is exactly the interval
+	start3 := time.Now().Add(-100 * time.Millisecond) // Started 100ms ago
+	sleep3 := getRemainingSleep(start3, interval)
+	// Expecting minimum sleep time
+	assert.Equal(time.Millisecond, sleep3, "Case 3 failed")
+
+	// Case 4: Zero elapsed time
+	start4 := time.Now()
+	sleep4 := getRemainingSleep(start4, interval)
+	assert.InDelta(interval, sleep4, float64(5*time.Millisecond), "Case 4 failed")
+}
+
+// TestLazyAggregationLoop_TxNotification tests that transaction notifications trigger block production in lazy mode
+func TestLazyAggregationLoop_TxNotification(t *testing.T) {
+	require := require.New(t)
+
+	blockTime := 200 * time.Millisecond
+	lazyTime := 500 * time.Millisecond
+	m, pubMock := setupTestManager(t, blockTime, lazyTime)
+	m.config.Node.LazyMode = true
+
+	// Create the notification channel
+	m.txNotifyCh = make(chan struct{}, 1)
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		// Start with a timer that won't fire immediately
+		blockTimer := time.NewTimer(blockTime)
+		defer blockTimer.Stop()
+		m.lazyAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for the initial lazy timer to fire and publish a block
+	select {
+	case <-pubMock.calls:
+		// Initial block was published by lazy timer
+	case <-time.After(100 * time.Millisecond):
+		require.Fail("Initial block was not published")
+	}
+
+	// Reset the mock to track new calls
+	pubMock.reset()
+
+	// Wait a bit to ensure the loop is running with reset timers
+	time.Sleep(20 * time.Millisecond)
+
+	// Send a transaction notification
+	m.NotifyNewTransactions()
+
+	// Wait for the block timer to fire and check txsAvailable
+	select {
+	case <-pubMock.calls:
+		// Block was published, which is what we expect
+	case <-time.After(blockTime + 50*time.Millisecond):
+		require.Fail("Block was not published after transaction notification")
+	}
+
+	// Reset the mock again
+	pubMock.reset()
+
+	// Send another notification immediately
+	m.NotifyNewTransactions()
+
+	// Wait for the next block timer to fire
+	select {
+	case <-pubMock.calls:
+		// Block was published after notification
+	case <-time.After(blockTime + 50*time.Millisecond):
+		require.Fail("Block was not published after second notification")
+	}
+
+	cancel()
+	wg.Wait()
+}
+
+// TestEmptyBlockCreation tests that empty blocks are created with the correct dataHash
+func TestEmptyBlockCreation(t *testing.T) {
+	require := require.New(t)
+
+	// Create a mock for the publishBlock function that captures the context
+	var capturedCtx context.Context
+	mockPublishFn := func(ctx context.Context) error {
+		capturedCtx = ctx
+		return nil
+	}
+
+	// Setup a manager with our mock publish function
+	blockTime := 50 * time.Millisecond
+	lazyTime := 100 * time.Millisecond
+	m, _ := setupTestManager(t, blockTime, lazyTime)
+	m.publishBlock = mockPublishFn
+
+	// Create a context we can cancel
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	// Create timers for the test
+	lazyTimer := time.NewTimer(lazyTime)
+	blockTimer := time.NewTimer(blockTime)
+	defer lazyTimer.Stop()
+	defer blockTimer.Stop()
+
+	// Call produceBlock directly to test empty block creation
+	m.produceBlock(ctx, "test_trigger", lazyTimer, blockTimer)
+
+	// Verify that the context was passed correctly
+	require.NotNil(capturedCtx, "Context should have been captured by mock publish function")
+	require.Equal(ctx, capturedCtx, "Context should match the one passed to produceBlock")
+}
path: block/lazy_aggregation_test.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Test is incomplete for empty block creation verification**

While the test verifies that the context is passed correctly, it doesn't actually verify that empty blocks are created with the correct dataHash as stated in the test name.


Enhance the test to verify the actual creation of empty blocks with correct dataHash:

```diff
// TestEmptyBlockCreation tests that empty blocks are created with the correct dataHash
func TestEmptyBlockCreation(t *testing.T) {
	require := require.New(t)

	// Create a mock for the publishBlock function that captures the context
	var capturedCtx context.Context
+	var emptyBlockCreated bool
+	var dataHashUsed []byte
	mockPublishFn := func(ctx context.Context) error {
		capturedCtx = ctx
+		// Check if this is an empty block request by examining context values
+		if dataHash, ok := ctx.Value(emptyDataHashKey).([]byte); ok {
+			emptyBlockCreated = true
+			dataHashUsed = dataHash
+		}
		return nil
	}

	// Setup a manager with our mock publish function
	blockTime := 50 * time.Millisecond
	lazyTime := 100 * time.Millisecond
	m, _ := setupTestManager(t, blockTime, lazyTime)
	m.publishBlock = mockPublishFn

	// Create a context we can cancel
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Create timers for the test
	lazyTimer := time.NewTimer(lazyTime)
	blockTimer := time.NewTimer(blockTime)
	defer lazyTimer.Stop()
	defer blockTimer.Stop()

	// Call produceBlock directly to test empty block creation
	m.produceBlock(ctx, "test_trigger", lazyTimer, blockTimer)

	// Verify that the context was passed correctly
	require.NotNil(capturedCtx, "Context should have been captured by mock publish function")
	require.Equal(ctx, capturedCtx, "Context should match the one passed to produceBlock")
+	
+	// Verify empty block creation with correct dataHash
+	require.True(emptyBlockCreated, "Empty block should be created")
+	require.Equal(dataHashForEmptyTxs, dataHashUsed, "Empty block should use the correct dataHash")
}
```

Note: You'll need to modify `produceBlock` to pass the empty data hash in the context and have the proper context key defined.


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T19:00:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,403 @@
+package block
+
+import (
+	"context"
+	"errors"
+	"sync"
+	"testing"
+	"time"
+
+	"cosmossdk.io/log"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/rollkit/rollkit/pkg/config"
+)
+
+// mockPublishBlock is used to control the behavior of publishBlock during tests
+type mockPublishBlock struct {
+	mu    sync.Mutex
+	calls chan struct{}
+	err   error
+	delay time.Duration // Optional delay to simulate processing time
+}
+
+// reset clears the calls channel in mockPublishBlock.
+func (m *mockPublishBlock) reset() {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	// Clear the channel
+	for len(m.calls) > 0 {
+		<-m.calls
+	}
+}
+
+func (m *mockPublishBlock) publish(ctx context.Context) error {
+	m.mu.Lock()
+	err := m.err
+	delay := m.delay
+	m.mu.Unlock()
+
+	if delay > 0 {
+		time.Sleep(delay)
+	}
+	// Non-blocking send in case the channel buffer is full or receiver is not ready
+	select {
+	case m.calls <- struct{}{}:
+	default:
+	}
+	return err
+}
+
+func setupTestManager(t *testing.T, blockTime, lazyTime time.Duration) (*Manager, *mockPublishBlock) {
+	t.Helper()
+	pubMock := &mockPublishBlock{
+		calls: make(chan struct{}, 10), // Buffer to avoid blocking in tests
+	}
+	logger := log.NewTestLogger(t)
+	m := &Manager{
+		logger: logger,
+		config: config.Config{
+			Node: config.NodeConfig{
+				BlockTime:         config.DurationWrapper{Duration: blockTime},
+				LazyBlockInterval: config.DurationWrapper{Duration: lazyTime},
+				LazyMode:          true, // Ensure lazy mode is active
+			},
+		},
+		publishBlock: pubMock.publish,
+	}
+	return m, pubMock
+}
+
+// TestLazyAggregationLoop_BlockTimerTrigger tests that a block is published when the blockTimer fires first.
+func TestLazyAggregationLoop_BlockTimerTrigger(t *testing.T) {
+	require := require.New(t)
+
+	// Create a mock for the publishBlock function that counts calls
+	callCount := 0
+	mockPublishFn := func(ctx context.Context) error {
+		callCount++
+		return nil
+	}
+
+	// Setup a manager with our mock publish function
+	blockTime := 50 * time.Millisecond
+	lazyTime := 200 * time.Millisecond // Lazy timer fires later
+	m, _ := setupTestManager(t, blockTime, lazyTime)
+	m.publishBlock = mockPublishFn
+
+	// Set txsAvailable to true to ensure block timer triggers block production
+	m.txsAvailable = true
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	// Start the lazy aggregation loop
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		blockTimer := time.NewTimer(0) // Fire immediately first time
+		defer blockTimer.Stop()
+		m.lazyAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for at least one block to be published
+	time.Sleep(blockTime * 2)
+
+	// Cancel the context to stop the loop
+	cancel()
+	wg.Wait()
+
+	// Verify that at least one block was published
+	require.GreaterOrEqual(callCount, 1, "Expected at least one block to be published")
+}
+
+// TestLazyAggregationLoop_LazyTimerTrigger tests that a block is published when the lazyTimer fires first.
+func TestLazyAggregationLoop_LazyTimerTrigger(t *testing.T) {
+	assert := assert.New(t)
+	require := require.New(t)
+
+	blockTime := 200 * time.Millisecond // Block timer fires later
+	lazyTime := 50 * time.Millisecond
+	m, pubMock := setupTestManager(t, blockTime, lazyTime)
+
+	// Set txsAvailable to false to ensure lazy timer triggers block production
+	// and block timer doesn't
+	m.txsAvailable = false
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		// Use real timers for this test
+		blockTimer := time.NewTimer(0) // Fire immediately first time
+		defer blockTimer.Stop()
+		m.lazyAggregationLoop(ctx, blockTimer)
+	}()
+
+	// Wait for the first publish call triggered by the initial immediate lazyTimer fire
+	select {
+	case <-pubMock.calls:
+		// Good, first block published by lazy timer
+	case <-time.After(2 * lazyTime): // Give some buffer
+		require.Fail("timed out waiting for first block publication")
+	}
+
+	// Wait for the second publish call, triggered by lazyTimer reset
+	select {
+	case <-pubMock.calls:
+		// Good, second block published by lazyTimer
+	case <-time.After(2 * lazyTime): // Give some buffer
+		require.Fail("timed out waiting for second block publication (lazyTimer)")
+	}
+
+	// Ensure blockTimer didn't trigger a publish yet (since txsAvailable is false)
+	assert.Len(pubMock.calls, 0, "Expected no more publish calls yet")
+
path: block/lazy_aggregation_test.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Incorrect assertion for empty channel**

This assertion is checking the length of the channel, but `len(pubMock.calls)` will return the number of elements available to read, not the total number including those already consumed. This makes the assertion unreliable.

```diff
-	// Ensure blockTimer didn't trigger a publish yet (since txsAvailable is false)
-	assert.Len(pubMock.calls, 0, "Expected no more publish calls yet")
+	// Ensure no additional publish calls happen for a short period
+	select {
+	case <-pubMock.calls:
+		assert.Fail("Unexpected additional publish call")
+	case <-time.After(blockTime / 4): // Short duration to check for unexpected calls
+		// Expected: no more publish calls
+	}
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T19:09:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,21 +55,38 @@ func (m *Manager) lazyAggregationLoop(ctx context.Context, blockTimer *time.Time
 			return
 
 		case <-lazyTimer.C:
+			m.logger.Debug("Lazy timer triggered block production")
+			m.produceBlock(ctx, "lazy_timer", lazyTimer, blockTimer)
+
 		case <-blockTimer.C:
+			if m.txsAvailable {
+				m.produceBlock(ctx, "block_timer", lazyTimer, blockTimer)
+				m.txsAvailable = false
+			} else {
+				// Ensure we keep ticking even when there are no txs
+				blockTimer.Reset(m.config.Node.BlockTime.Duration)
+			}
+		case <-m.txNotifyCh:
+			m.txsAvailable = true
 		}
+	}
+}
 
-		// Reset the start time
-		start := time.Now()
+// produceBlock handles the common logic for producing a block and resetting timers
+func (m *Manager) produceBlock(ctx context.Context, trigger string, lazyTimer, blockTimer *time.Timer) {
path: block/aggregation.go
user: login: tzdybal
body: Not a big fan of `trigger string`, but in this case it makes sense, and it keeps it simple.
created_at: 2025-05-05T11:38:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,21 +55,38 @@ func (m *Manager) lazyAggregationLoop(ctx context.Context, blockTimer *time.Time
 			return
 
 		case <-lazyTimer.C:
+			m.logger.Debug("Lazy timer triggered block production")
+			m.produceBlock(ctx, "lazy_timer", lazyTimer, blockTimer)
+
 		case <-blockTimer.C:
+			if m.txsAvailable {
+				m.produceBlock(ctx, "block_timer", lazyTimer, blockTimer)
+				m.txsAvailable = false
+			} else {
+				// Ensure we keep ticking even when there are no txs
+				blockTimer.Reset(m.config.Node.BlockTime.Duration)
+			}
+		case <-m.txNotifyCh:
+			m.txsAvailable = true
 		}
+	}
+}
 
-		// Reset the start time
-		start := time.Now()
+// produceBlock handles the common logic for producing a block and resetting timers
+func (m *Manager) produceBlock(ctx context.Context, trigger string, lazyTimer, blockTimer *time.Timer) {
path: block/aggregation.go
user: login: tac0turtle
body: ```suggestion
func (m *Manager) produceBlock(ctx context.Context, mode string, lazyTimer, blockTimer *time.Timer) {
```
created_at: 2025-05-05T11:48:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-29T19:19:34Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: rate limited by coderabbit.ai -->

> [!WARNING]
> ## Rate limit exceeded
> 
> @tac0turtle has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait **9 minutes and 45 seconds** before requesting another review.
> 
> <details>
> <summary>⌛ How to resolve this issue?</summary>
> 
> After the wait time has elapsed, a review can be triggered using the `@coderabbitai review` command as a PR comment. Alternatively, push new commits to this PR.
> 
> We recommend that you space out your commits to avoid hitting the rate limit.
> 
> </details>
> 
> 
> <details>
> <summary>🚦 How do rate limits work?</summary>
> 
> CodeRabbit enforces hourly rate limits for each developer per organization.
> 
> Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.
> 
> Please see our [FAQ](https://docs.coderabbit.ai/faq) for further information.
> 
> </details>
> 
> <details>
> <summary>📥 Commits</summary>
> 
> Reviewing files that changed from the base of the PR and between b3e1d8307d12cf023c5b05737269dcd28ed5b8aa and 4d0c8be52d7c68a56b8dfc49bb096ca5c16dc1a5.
> 
> </details>
> 
> <details>
> <summary>📒 Files selected for processing (1)</summary>
> 
> * `pkg/config/config_test.go` (5 hunks)
> 
> </details>

<!-- end of auto-generated comment: rate limited by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces a new ADR document defining a design to ensure consistent handling of empty blocks in Rollkit's lazy aggregation mechanism, aligning with the Data Availability (DA) layer (Celestia). It modifies batch retrieval to always return batches with timestamps, even if empty, and uses a special `dataHashForEmptyTxs` to represent empty blocks. Block publishing logic is enhanced to create empty blocks when no transactions are present, and a dedicated lazy aggregation loop with dual timers manages block production timing. Several methods in `block/manager.go` were added or refactored to implement this design. Comprehensive tests were introduced to cover lazy aggregation behavior and empty block creation. Related configuration flags and fields were renamed for clarity.

## Changes

| File(s)                                         | Change Summary                                                                                                    |
|------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| specs/lazy-adr/adr-021-lazy-aggregation.md     | New ADR document detailing design for consistent empty block handling in lazy aggregation with the DA layer.      |
| block/manager.go                                | Added `retrieveBatch` returning batches with timestamps including empty batches; refactored `publishBlock` to create empty blocks using `dataHashForEmptyTxs`; added `lazyAggregationLoop` and `produceBlock` methods; replaced `buildingBlock` flag with `txsAvailable` and added transaction notification channel; rewrote `createBlock` for explicit empty block handling and proposer verification; added `NotifyNewTransactions` method; consolidated block creation and state application methods. |
| block/aggregation.go                            | Refactored aggregation loop to use `LazyMode` flag; introduced `produceBlock` helper method; enhanced timer and transaction notification handling; improved logging. |
| block/aggregation_test.go                       | Updated tests to replace deprecated `LazyAggregator` with `LazyMode` in node config.                             |
| block/lazy_aggregation_test.go                  | New comprehensive tests for lazy aggregation loop behavior, timer interactions, block production triggers, and empty block creation. |
| block/publish_block_test.go                      | Enhanced test to expect full processing of empty batches including saving block data and executing empty transactions. |
| block/reaper.go                                  | Added `manager` field and setter; notify manager of new transactions after submission.                           |
| node/full.go                                     | Changed initialization order to set reaper's manager reference after block manager creation.                    |
| node/helpers_test.go                             | Renamed config field from `LazyBlockTime` to `LazyBlockInterval` in test helper.                                 |
| pkg/cmd/run_node_test.go                         | Updated test flags and config field names from `lazy_aggregator` and `lazy_block_time` to `lazy_mode` and `lazy_block_interval`. |
| pkg/config/config.go                             | Renamed config flags and struct fields: `LazyAggregator` → `LazyMode`, `LazyBlockTime` → `LazyBlockInterval`.     |
| pkg/config/config_test.go                        | Updated test references for renamed config fields `LazyAggregator` → `LazyMode` and `LazyBlockTime` → `LazyBlockInterval`. |
| pkg/config/defaults.go                           | Renamed default config fields `LazyAggregator` → `LazyMode` and `LazyBlockTime` → `LazyBlockInterval`.            |
| specs/lazy-adr/adr-007-header-commit-to-shares.md | Removed empty lines in Consequences section; no content changes.                                                  |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant Manager
    participant DA_Layer

    Client->>Manager: Request batch
    Manager->>Manager: retrieveBatch(ctx)
    alt Batch has transactions
        Manager->>Manager: createBlock(ctx, batchData)
        Manager->>DA_Layer: Post block with DataHash
    else Batch is empty
        Manager->>Manager: createBlock(ctx, batchData with dataHashForEmptyTxs)
        Manager->>DA_Layer: Post empty block with dataHashForEmptyTxs
    else No batch retrieved
        Manager-->>Client: Skip block production
    end
    DA_Layer->>Manager: Confirm block
    Manager->>Client: Block/header info
```

```mermaid
sequenceDiagram
    participant LazyLoop as LazyAggregationLoop
    participant Manager

    loop
        alt lazyTimer fires
            LazyLoop->>Manager: produceBlock(trigger="lazyTimer")
        else blockTimer fires
            LazyLoop->>Manager: if txsAvailable produceBlock(trigger="blockTimer")
        else txNotifyCh receives
            LazyLoop->>Manager: set txsAvailable = true
        end
    end
```

## Assessment against linked issues

| Objective                                                                                                       | Addressed | Explanation                                                                                      |
|----------------------------------------------------------------------------------------------------------------|-----------|------------------------------------------------------------------------------------------------|
| Ensure lazy aggregation produces blocks at predefined intervals even without transactions (empty blocks).      | ✅        | Implementation creates empty blocks with `dataHashForEmptyTxs` and uses dual timers for timing. |
| Provide a workaround for EVM to support empty blocks without inserting fake transactions.                       | ✅        | Empty blocks are created using a special data hash, avoiding fake transactions.                  |
| Enable lazy mode to produce blocks only when transactions are available or at heartbeat intervals.             | ✅        | Dual timer lazy aggregation loop triggers blocks on transaction availability or lazy timer.     |
| Reactivate and cover lazy mode tests which were previously disabled.                                           | ✅        | New comprehensive tests for lazy aggregation behavior and empty block creation are added.       |

## Possibly related PRs

- rollkit/rollkit#1779: This PR modifies the block manager's aggregation loop and timer logic, including lazy aggregation handling and the `getRemainingSleep` function, closely related to the aggregation loop refactoring here.
- rollkit/rollkit#1859: Both PRs address lazy aggregation and empty block handling in `block/manager.go`, though this PR introduces a redesigned lazy aggregation loop and batch retrieval, while the other focuses on empty data hash handling and notification mechanisms.
- rollkit/rollkit#2173: This PR removes the batch queue and modifies batch retrieval and block publishing logic, directly related to the changes here that also remove the batch queue and adjust batch handling for lazy aggregation.

## Suggested labels

`T:spec-and-docs`, `T:dev-usability-and-ux`

## Suggested reviewers

- tuxcanfly  
- Manav-Aggarwal  
- gupadhyaya

## Poem

> 🐇 In the land where blocks hop and play,  
> Empty or full, they find their way.  
> Timers tick and hashes align,  
> Lazy aggregation now works fine.  
> With hops and bounds, the chain stays strong,  
> Consistency sings its joyful song! 🎶✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=rollkit/rollkit&utm_content=2224):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-29T19:19:39Z
body: The latest Buf updates on your PR. Results from workflow <a href="https://github.com/rollkit/rollkit/actions/runs/14836636876">CI and Release / buf-check (pull_request)</a>.

<table><tr><th>Build</th><th>Format</th><th>Lint</th><th>Breaking</th><th>Updated (UTC)</th></tr><tr><td><code>✅ passed</code></td><td><code>⏩ skipped</code></td><td><code>✅ passed</code></td><td><code>✅ passed</code></td><td>May 5, 2025, 12:39 PM</td></tr></table>
<!-- buf CI and Release:buf-check -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-29T19:19:41Z
body: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.6.1
:---:
| <p></p> :rocket: View preview at <br> https://rollkit.github.io/rollkit/pr-preview/pr-2224/ <br><br>
| <h6>Built to branch [`gh-pages`](https://github.com/rollkit/rollkit/tree/gh-pages) at 2025-05-02 19:12 UTC. <br> Preview will be ready when the [GitHub Pages deployment](https://github.com/rollkit/rollkit/deployments) is complete. <br><br> </h6>
<!-- Sticky Pull Request Commentpr-preview -->
author_association: CONTRIBUTOR
user: login: Manav-Aggarwal
created_at: 2025-04-29T22:27:44Z
body: btw is this only necessary for EVM execution environments? I don't recall it being necessary for ABCI
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-04-30T07:43:21Z
body: ## [Codecov](https://app.codecov.io/gh/rollkit/rollkit/pull/2224?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit) Report
Attention: Patch coverage is `82.60870%` with `20 lines` in your changes missing coverage. Please review.
> Project coverage is 54.83%. Comparing base [(`f04b887`)](https://app.codecov.io/gh/rollkit/rollkit/commit/f04b887c07b9eb64a7ca2d64123008237eb9d6b1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit) to head [(`4d0c8be`)](https://app.codecov.io/gh/rollkit/rollkit/commit/4d0c8be52d7c68a56b8dfc49bb096ca5c16dc1a5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/rollkit/rollkit/pull/2224?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit) | Patch % | Lines |
|---|---|---|
| [block/manager.go](https://app.codecov.io/gh/rollkit/rollkit/pull/2224?src=pr&el=tree&filepath=block%2Fmanager.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit#diff-YmxvY2svbWFuYWdlci5nbw==) | 80.59% | [10 Missing and 3 partials :warning: ](https://app.codecov.io/gh/rollkit/rollkit/pull/2224?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit) |
| [block/reaper.go](https://app.codecov.io/gh/rollkit/rollkit/pull/2224?src=pr&el=tree&filepath=block%2Freaper.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit#diff-YmxvY2svcmVhcGVyLmdv) | 0.00% | [5 Missing and 1 partial :warning: ](https://app.codecov.io/gh/rollkit/rollkit/pull/2224?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit) |
| [block/aggregation.go](https://app.codecov.io/gh/rollkit/rollkit/pull/2224?src=pr&el=tree&filepath=block%2Faggregation.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit#diff-YmxvY2svYWdncmVnYXRpb24uZ28=) | 96.29% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/rollkit/rollkit/pull/2224?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2224      +/-   ##
==========================================
+ Coverage   54.22%   54.83%   +0.60%     
==========================================
  Files          53       53              
  Lines        4592     4645      +53     
==========================================
+ Hits         2490     2547      +57     
+ Misses       1840     1834       -6     
- Partials      262      264       +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/rollkit/rollkit/pull/2224?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rollkit).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: Manav-Aggarwal
created_at: 2025-05-01T22:14:31Z
body: Small rendering issue, should fix before merging
author_association: MEMBER
user: login: Manav-Aggarwal
created_at: 2025-05-02T16:38:48Z
body: @coderabbit review
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-05-02T16:38:53Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: CONTRIBUTOR