Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions block/da_includer.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ func (m *Manager) DAIncluderLoop(ctx context.Context, errCh chan<- error) {
break
}
if daIncluded {
m.logger.Debug("both header and data are DA-included, advancing height", "height", nextHeight)
if err := m.SetRollkitHeightToDAHeight(ctx, nextHeight); err != nil {
errCh <- fmt.Errorf("failed to set rollkit height to DA height: %w", err)
return
}
// Both header and data are DA-included, so we can advance the height
if err := m.incrementDAIncludedHeight(ctx); err != nil {
errCh <- fmt.Errorf("error while incrementing DA included height: %w", err)
Expand Down
53 changes: 43 additions & 10 deletions block/da_includer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package block
import (
"context"
"encoding/binary"
"fmt"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -53,11 +54,19 @@ func TestDAIncluderLoop_AdvancesHeightWhenBothDAIncluded(t *testing.T) {
header, data := types.GetRandomBlock(5, 1, "testchain")
headerHash := header.Hash().String()
dataHash := data.DACommitment().String()
m.headerCache.SetDAIncluded(headerHash)
m.dataCache.SetDAIncluded(dataHash)
m.headerCache.SetDAIncluded(headerHash, uint64(1))
m.dataCache.SetDAIncluded(dataHash, uint64(1))

store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Once()
store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Times(2)
store.On("GetBlockData", mock.Anything, uint64(6)).Return(nil, nil, assert.AnError).Once()
// Mock expectations for SetRollkitHeightToDAHeight method
headerHeightBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(headerHeightBytes, uint64(1))
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, uint64(5)), headerHeightBytes).Return(nil).Once()
dataHeightBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(dataHeightBytes, uint64(1))
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, uint64(5)), dataHeightBytes).Return(nil).Once()
// Mock expectations for incrementDAIncludedHeight method
heightBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(heightBytes, expectedDAIncludedHeight)
store.On("SetMetadata", mock.Anything, DAIncludedHeightKey, heightBytes).Return(nil).Once()
Expand Down Expand Up @@ -93,7 +102,7 @@ func TestDAIncluderLoop_StopsWhenHeaderNotDAIncluded(t *testing.T) {

header, data := types.GetRandomBlock(5, 1, "testchain")
// m.headerCache.SetDAIncluded(headerHash) // Not set
m.dataCache.SetDAIncluded(data.DACommitment().String())
m.dataCache.SetDAIncluded(data.DACommitment().String(), uint64(1))

store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Once()

Expand Down Expand Up @@ -124,7 +133,7 @@ func TestDAIncluderLoop_StopsWhenDataNotDAIncluded(t *testing.T) {

header, data := types.GetRandomBlock(5, 1, "testchain")
headerHash := header.Hash().String()
m.headerCache.SetDAIncluded(headerHash)
m.headerCache.SetDAIncluded(headerHash, uint64(1))
// m.dataCache.SetDAIncluded(data.DACommitment().String()) // Not set

store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Once()
Expand Down Expand Up @@ -272,12 +281,22 @@ func TestDAIncluderLoop_MultipleConsecutiveHeightsDAIncluded(t *testing.T) {
headers[i], dataBlocks[i] = types.GetRandomBlock(height, numTxs, "testchain")
headerHash := headers[i].Hash().String()
dataHash := dataBlocks[i].DACommitment().String()
m.headerCache.SetDAIncluded(headerHash)
m.dataCache.SetDAIncluded(dataHash)
store.On("GetBlockData", mock.Anything, height).Return(headers[i], dataBlocks[i], nil).Once()
m.headerCache.SetDAIncluded(headerHash, uint64(i+1))
m.dataCache.SetDAIncluded(dataHash, uint64(i+1))
store.On("GetBlockData", mock.Anything, height).Return(headers[i], dataBlocks[i], nil).Times(2) // Called by IsDAIncluded and SetRollkitHeightToDAHeight
}
// Next height returns error
store.On("GetBlockData", mock.Anything, startDAIncludedHeight+uint64(numConsecutive+1)).Return(nil, nil, assert.AnError).Once()
// Mock expectations for SetRollkitHeightToDAHeight method calls
for i := 0; i < numConsecutive; i++ {
height := startDAIncludedHeight + uint64(i+1)
headerHeightBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(headerHeightBytes, uint64(i+1))
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, height), headerHeightBytes).Return(nil).Once()
dataHeightBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(dataHeightBytes, uint64(i+1))
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, height), dataHeightBytes).Return(nil).Once()
}
store.On("SetMetadata", mock.Anything, DAIncludedHeightKey, mock.Anything).Return(nil).Times(numConsecutive)
exec.On("SetFinal", mock.Anything, mock.Anything).Return(nil).Times(numConsecutive)

Expand Down Expand Up @@ -312,11 +331,25 @@ func TestDAIncluderLoop_AdvancesHeightWhenDataHashIsEmptyAndHeaderDAIncluded(t *

header, data := types.GetRandomBlock(5, 0, "testchain")
headerHash := header.Hash().String()
m.headerCache.SetDAIncluded(headerHash)
m.headerCache.SetDAIncluded(headerHash, uint64(1))
// Do NOT set data as DA-included

store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Once()
store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Times(2) // Called by IsDAIncluded and SetRollkitHeightToDAHeight
store.On("GetBlockData", mock.Anything, uint64(6)).Return(nil, nil, assert.AnError).Once()
// Mock expectations for SetRollkitHeightToDAHeight method
headerHeightBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(headerHeightBytes, uint64(1))
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, uint64(5)), headerHeightBytes).Return(nil).Once()
// Note: For empty data, data SetMetadata call should still be made but data won't be marked as DA-included in cache,
// so SetRollkitHeightToDAHeight will fail when trying to get the DA height for data
// Actually, let's check if this case is handled differently for empty txs
// Let me check what happens with empty txs by adding the data cache entry as well
dataHash := data.DACommitment().String()
m.dataCache.SetDAIncluded(dataHash, uint64(1))
dataHeightBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(dataHeightBytes, uint64(1))
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, uint64(5)), dataHeightBytes).Return(nil).Once()
// Mock expectations for incrementDAIncludedHeight method
heightBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(heightBytes, expectedDAIncludedHeight)
store.On("SetMetadata", mock.Anything, DAIncludedHeightKey, heightBytes).Return(nil).Once()
Expand Down
42 changes: 42 additions & 0 deletions block/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const (

// LastBatchDataKey is the key used for persisting the last batch data in store.
LastBatchDataKey = "l"

// RollkitHeightToDAHeightKey is the key used for persisting the mapping rollkit height to da height in store.
RollkitHeightToDAHeightKey = "rhb"
)

var (
Expand Down Expand Up @@ -486,6 +489,45 @@ func (m *Manager) IsDAIncluded(ctx context.Context, height uint64) (bool, error)
return isIncluded, nil
}

// SetRollkitHeightToDAHeight stores the mapping from a Rollkit block height to the corresponding
// DA (Data Availability) layer heights where the block's header and data were included.
// This mapping is persisted in the store metadata and is used to track which DA heights
// contain the block components for a given Rollkit height.
//
// For blocks with empty transactions, both header and data use the same DA height since
// empty transaction data is not actually published to the DA layer.
func (m *Manager) SetRollkitHeightToDAHeight(ctx context.Context, height uint64) error {
header, data, err := m.store.GetBlockData(ctx, height)
if err != nil {
return err
}
headerHash, dataHash := header.Hash(), data.DACommitment()
headerHeightBytes := make([]byte, 8)
daHeightForHeader, ok := m.headerCache.GetDAIncludedHeight(headerHash.String())
if !ok {
return fmt.Errorf("header hash %s not found in cache", headerHash)
}
binary.LittleEndian.PutUint64(headerHeightBytes, daHeightForHeader)
if err := m.store.SetMetadata(ctx, fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, height), headerHeightBytes); err != nil {
return err
}
dataHeightBytes := make([]byte, 8)
// For empty transactions, use the same DA height as the header
if bytes.Equal(dataHash, dataHashForEmptyTxs) {
binary.LittleEndian.PutUint64(dataHeightBytes, daHeightForHeader)
} else {
daHeightForData, ok := m.dataCache.GetDAIncludedHeight(dataHash.String())
if !ok {
return fmt.Errorf("data hash %s not found in cache", dataHash.String())
}
binary.LittleEndian.PutUint64(dataHeightBytes, daHeightForData)
}
if err := m.store.SetMetadata(ctx, fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, height), dataHeightBytes); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add a test case with a more real world scenario in which the heights header and data are included at different heights.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the height in the test TestSetRollkitHeightToDAHeight: Success_WithTransactions more different to account for this

return err
}
return nil
}

// GetExecutor returns the executor used by the manager.
//
// Note: this is a temporary method to allow testing the manager.
Expand Down
139 changes: 137 additions & 2 deletions block/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,11 @@ func TestIsDAIncluded(t *testing.T) {
require.False(m.IsDAIncluded(ctx, height))

// Set the hash as DAIncluded and verify IsDAIncluded returns true
m.headerCache.SetDAIncluded(header.Hash().String())
m.headerCache.SetDAIncluded(header.Hash().String(), uint64(1))
require.False(m.IsDAIncluded(ctx, height))

// Set the data as DAIncluded and verify IsDAIncluded returns true
m.dataCache.SetDAIncluded(data.DACommitment().String())
m.dataCache.SetDAIncluded(data.DACommitment().String(), uint64(1))
require.True(m.IsDAIncluded(ctx, height))
}

Expand Down Expand Up @@ -1065,3 +1065,138 @@ func TestConfigurationDefaults(t *testing.T) {
require.Contains(err.Error(), "insufficient bytes for length prefix")
})
}

// TestSetRollkitHeightToDAHeight tests the SetRollkitHeightToDAHeight method which maps
// rollkit block heights to their corresponding DA heights for both headers and data.
// This method is critical for tracking which DA heights contain specific rollkit blocks.
func TestSetRollkitHeightToDAHeight(t *testing.T) {
t.Run("Success_WithTransactions", func(t *testing.T) {
require := require.New(t)
ctx := context.Background()
m, mockStore := getManager(t, nil, 0, 0)

// Create a block with transactions
header, data := types.GetRandomBlock(5, 3, "testchain")
height := uint64(5)

// Mock store expectations
mockStore.On("GetBlockData", mock.Anything, height).Return(header, data, nil)

// Set DA included heights in cache
headerHeight := uint64(10)
dataHeight := uint64(11)
m.headerCache.SetDAIncluded(header.Hash().String(), headerHeight)
m.dataCache.SetDAIncluded(data.DACommitment().String(), dataHeight)

// Mock metadata storage
headerKey := fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, height)
dataKey := fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, height)
mockStore.On("SetMetadata", mock.Anything, headerKey, mock.Anything).Return(nil)
mockStore.On("SetMetadata", mock.Anything, dataKey, mock.Anything).Return(nil)

// Call the method
err := m.SetRollkitHeightToDAHeight(ctx, height)
require.NoError(err)

mockStore.AssertExpectations(t)
})

t.Run("Success_EmptyTransactions", func(t *testing.T) {
require := require.New(t)
ctx := context.Background()
m, mockStore := getManager(t, nil, 0, 0)

// Create a block with no transactions
header, data := types.GetRandomBlock(5, 0, "testchain")
height := uint64(5)

// Mock store expectations
mockStore.On("GetBlockData", mock.Anything, height).Return(header, data, nil)

// Only set header as DA included (data uses special empty hash)
headerHeight := uint64(10)
m.headerCache.SetDAIncluded(header.Hash().String(), headerHeight)
// Note: we don't set data in cache for empty transactions

// Mock metadata storage - both should use header height for empty transactions
headerKey := fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, height)
dataKey := fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, height)
mockStore.On("SetMetadata", mock.Anything, headerKey, mock.Anything).Return(nil)
mockStore.On("SetMetadata", mock.Anything, dataKey, mock.Anything).Return(nil)

// Call the method
err := m.SetRollkitHeightToDAHeight(ctx, height)
require.NoError(err)

mockStore.AssertExpectations(t)
})

t.Run("Error_HeaderNotInCache", func(t *testing.T) {
require := require.New(t)
ctx := context.Background()
m, mockStore := getManager(t, nil, 0, 0)

// Create a block
header, data := types.GetRandomBlock(5, 3, "testchain")
height := uint64(5)

// Mock store expectations
mockStore.On("GetBlockData", mock.Anything, height).Return(header, data, nil)

// Don't set header in cache, but set data
m.dataCache.SetDAIncluded(data.DACommitment().String(), uint64(11))

// Call the method - should fail
err := m.SetRollkitHeightToDAHeight(ctx, height)
require.Error(err)
require.Contains(err.Error(), "header hash")
require.Contains(err.Error(), "not found in cache")

mockStore.AssertExpectations(t)
})

t.Run("Error_DataNotInCache_NonEmptyTxs", func(t *testing.T) {
require := require.New(t)
ctx := context.Background()
m, mockStore := getManager(t, nil, 0, 0)

// Create a block with transactions
header, data := types.GetRandomBlock(5, 3, "testchain")
height := uint64(5)

// Mock store expectations
mockStore.On("GetBlockData", mock.Anything, height).Return(header, data, nil)

// Set header but not data in cache
m.headerCache.SetDAIncluded(header.Hash().String(), uint64(10))

// Mock metadata storage for header (should succeed)
headerKey := fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, height)
mockStore.On("SetMetadata", mock.Anything, headerKey, mock.Anything).Return(nil)

// Call the method - should fail on data lookup
err := m.SetRollkitHeightToDAHeight(ctx, height)
require.Error(err)
require.Contains(err.Error(), "data hash")
require.Contains(err.Error(), "not found in cache")

mockStore.AssertExpectations(t)
})

t.Run("Error_BlockNotFound", func(t *testing.T) {
require := require.New(t)
ctx := context.Background()
m, mockStore := getManager(t, nil, 0, 0)

height := uint64(999) // Non-existent height

// Mock store expectations
mockStore.On("GetBlockData", mock.Anything, height).Return(nil, nil, errors.New("block not found"))

// Call the method - should fail
err := m.SetRollkitHeightToDAHeight(ctx, height)
require.Error(err)

mockStore.AssertExpectations(t)
})
}
4 changes: 2 additions & 2 deletions block/retriever.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (m *Manager) handlePotentialHeader(ctx context.Context, bz []byte, daHeight
return true
}
headerHash := header.Hash().String()
m.headerCache.SetDAIncluded(headerHash)
m.headerCache.SetDAIncluded(headerHash, daHeight)
m.sendNonBlockingSignalToDAIncluderCh()
m.logger.Info("header marked as DA included", "headerHeight", header.Height(), "headerHash", headerHash)
if !m.headerCache.IsSeen(headerHash) {
Expand Down Expand Up @@ -168,7 +168,7 @@ func (m *Manager) handlePotentialData(ctx context.Context, bz []byte, daHeight u
}

dataHashStr := signedData.Data.DACommitment().String()
m.dataCache.SetDAIncluded(dataHashStr)
m.dataCache.SetDAIncluded(dataHashStr, daHeight)
m.sendNonBlockingSignalToDAIncluderCh()
m.logger.Info("signed data marked as DA included", "dataHash", dataHashStr, "daHeight", daHeight, "height", signedData.Height())
if !m.dataCache.IsSeen(dataHashStr) {
Expand Down
4 changes: 2 additions & 2 deletions block/retriever_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,9 +528,9 @@ func TestProcessNextDAHeader_HeaderAndDataAlreadySeen(t *testing.T) {

// Mark both header and data as seen and DA included
headerCache.SetSeen(headerHash)
headerCache.SetDAIncluded(headerHash)
headerCache.SetDAIncluded(headerHash, uint64(10))
dataCache.SetSeen(dataHash)
dataCache.SetDAIncluded(dataHash)
dataCache.SetDAIncluded(dataHash, uint64(10))

// Set up mocks with explicit logging
mockDAClient.On("GetIDs", mock.Anything, daHeight, mock.Anything).Return(&coreda.GetIDsResult{
Expand Down
4 changes: 2 additions & 2 deletions block/submitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func (m *Manager) submitHeadersToDA(ctx context.Context, headersToSubmit []*type
},
func(submitted []*types.SignedHeader, res *coreda.ResultSubmit, gasPrice float64) {
for _, header := range submitted {
m.headerCache.SetDAIncluded(header.Hash().String())
m.headerCache.SetDAIncluded(header.Hash().String(), res.Height)
}
lastSubmittedHeaderHeight := uint64(0)
if l := len(submitted); l > 0 {
Expand All @@ -197,7 +197,7 @@ func (m *Manager) submitDataToDA(ctx context.Context, signedDataToSubmit []*type
},
func(submitted []*types.SignedData, res *coreda.ResultSubmit, gasPrice float64) {
for _, signedData := range submitted {
m.dataCache.SetDAIncluded(signedData.DACommitment().String())
m.dataCache.SetDAIncluded(signedData.Data.DACommitment().String(), res.Height)
}
lastSubmittedDataHeight := uint64(0)
if l := len(submitted); l > 0 {
Expand Down
Loading