Skip to content

Commit 8bb1ba7

Browse files
feat: Expose Rollkit height to DA Height mapping (#2363)
<!-- 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 Expose Rollkit height to DA Height mapping Closes: #2360 <!-- Please provide an explanation of the PR, including the appropriate context, background, goal, and rationale. If there is an issue with this information, please provide a tl;dr and link the issue. Ex: Closes #<issue number> --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Tracks and stores the specific height at which data is included in the DA layer, enabling precise synchronization and mapping between Rollkit and DA block heights. - Enhanced logging to provide more detailed context on DA inclusion events. - **Bug Fixes** - Ensures correct association of DA inclusion heights with headers and data, improving block processing accuracy and error handling. - **Documentation** - Updated block validity specifications to clarify focus on data validation and field requirements. - **Tests** - Expanded tests to cover DA height tracking, error scenarios, concurrency, and metadata persistence, ensuring robust and reliable behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 7d85ee0 commit 8bb1ba7

12 files changed

Lines changed: 362 additions & 39 deletions

File tree

block/da_includer.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ func (m *Manager) DAIncluderLoop(ctx context.Context, errCh chan<- error) {
2828
break
2929
}
3030
if daIncluded {
31+
m.logger.Debug("both header and data are DA-included, advancing height", "height", nextHeight)
32+
if err := m.SetRollkitHeightToDAHeight(ctx, nextHeight); err != nil {
33+
errCh <- fmt.Errorf("failed to set rollkit height to DA height: %w", err)
34+
return
35+
}
3136
// Both header and data are DA-included, so we can advance the height
3237
if err := m.incrementDAIncludedHeight(ctx); err != nil {
3338
errCh <- fmt.Errorf("error while incrementing DA included height: %w", err)

block/da_includer_test.go

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package block
33
import (
44
"context"
55
"encoding/binary"
6+
"fmt"
67
"sync"
78
"testing"
89
"time"
@@ -53,11 +54,19 @@ func TestDAIncluderLoop_AdvancesHeightWhenBothDAIncluded(t *testing.T) {
5354
header, data := types.GetRandomBlock(5, 1, "testchain")
5455
headerHash := header.Hash().String()
5556
dataHash := data.DACommitment().String()
56-
m.headerCache.SetDAIncluded(headerHash)
57-
m.dataCache.SetDAIncluded(dataHash)
57+
m.headerCache.SetDAIncluded(headerHash, uint64(1))
58+
m.dataCache.SetDAIncluded(dataHash, uint64(1))
5859

59-
store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Once()
60+
store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Times(2)
6061
store.On("GetBlockData", mock.Anything, uint64(6)).Return(nil, nil, assert.AnError).Once()
62+
// Mock expectations for SetRollkitHeightToDAHeight method
63+
headerHeightBytes := make([]byte, 8)
64+
binary.LittleEndian.PutUint64(headerHeightBytes, uint64(1))
65+
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, uint64(5)), headerHeightBytes).Return(nil).Once()
66+
dataHeightBytes := make([]byte, 8)
67+
binary.LittleEndian.PutUint64(dataHeightBytes, uint64(1))
68+
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, uint64(5)), dataHeightBytes).Return(nil).Once()
69+
// Mock expectations for incrementDAIncludedHeight method
6170
heightBytes := make([]byte, 8)
6271
binary.LittleEndian.PutUint64(heightBytes, expectedDAIncludedHeight)
6372
store.On("SetMetadata", mock.Anything, DAIncludedHeightKey, heightBytes).Return(nil).Once()
@@ -93,7 +102,7 @@ func TestDAIncluderLoop_StopsWhenHeaderNotDAIncluded(t *testing.T) {
93102

94103
header, data := types.GetRandomBlock(5, 1, "testchain")
95104
// m.headerCache.SetDAIncluded(headerHash) // Not set
96-
m.dataCache.SetDAIncluded(data.DACommitment().String())
105+
m.dataCache.SetDAIncluded(data.DACommitment().String(), uint64(1))
97106

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

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

125134
header, data := types.GetRandomBlock(5, 1, "testchain")
126135
headerHash := header.Hash().String()
127-
m.headerCache.SetDAIncluded(headerHash)
136+
m.headerCache.SetDAIncluded(headerHash, uint64(1))
128137
// m.dataCache.SetDAIncluded(data.DACommitment().String()) // Not set
129138

130139
store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Once()
@@ -272,12 +281,22 @@ func TestDAIncluderLoop_MultipleConsecutiveHeightsDAIncluded(t *testing.T) {
272281
headers[i], dataBlocks[i] = types.GetRandomBlock(height, numTxs, "testchain")
273282
headerHash := headers[i].Hash().String()
274283
dataHash := dataBlocks[i].DACommitment().String()
275-
m.headerCache.SetDAIncluded(headerHash)
276-
m.dataCache.SetDAIncluded(dataHash)
277-
store.On("GetBlockData", mock.Anything, height).Return(headers[i], dataBlocks[i], nil).Once()
284+
m.headerCache.SetDAIncluded(headerHash, uint64(i+1))
285+
m.dataCache.SetDAIncluded(dataHash, uint64(i+1))
286+
store.On("GetBlockData", mock.Anything, height).Return(headers[i], dataBlocks[i], nil).Times(2) // Called by IsDAIncluded and SetRollkitHeightToDAHeight
278287
}
279288
// Next height returns error
280289
store.On("GetBlockData", mock.Anything, startDAIncludedHeight+uint64(numConsecutive+1)).Return(nil, nil, assert.AnError).Once()
290+
// Mock expectations for SetRollkitHeightToDAHeight method calls
291+
for i := 0; i < numConsecutive; i++ {
292+
height := startDAIncludedHeight + uint64(i+1)
293+
headerHeightBytes := make([]byte, 8)
294+
binary.LittleEndian.PutUint64(headerHeightBytes, uint64(i+1))
295+
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, height), headerHeightBytes).Return(nil).Once()
296+
dataHeightBytes := make([]byte, 8)
297+
binary.LittleEndian.PutUint64(dataHeightBytes, uint64(i+1))
298+
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, height), dataHeightBytes).Return(nil).Once()
299+
}
281300
store.On("SetMetadata", mock.Anything, DAIncludedHeightKey, mock.Anything).Return(nil).Times(numConsecutive)
282301
exec.On("SetFinal", mock.Anything, mock.Anything).Return(nil).Times(numConsecutive)
283302

@@ -312,11 +331,25 @@ func TestDAIncluderLoop_AdvancesHeightWhenDataHashIsEmptyAndHeaderDAIncluded(t *
312331

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

318-
store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Once()
337+
store.On("GetBlockData", mock.Anything, uint64(5)).Return(header, data, nil).Times(2) // Called by IsDAIncluded and SetRollkitHeightToDAHeight
319338
store.On("GetBlockData", mock.Anything, uint64(6)).Return(nil, nil, assert.AnError).Once()
339+
// Mock expectations for SetRollkitHeightToDAHeight method
340+
headerHeightBytes := make([]byte, 8)
341+
binary.LittleEndian.PutUint64(headerHeightBytes, uint64(1))
342+
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, uint64(5)), headerHeightBytes).Return(nil).Once()
343+
// Note: For empty data, data SetMetadata call should still be made but data won't be marked as DA-included in cache,
344+
// so SetRollkitHeightToDAHeight will fail when trying to get the DA height for data
345+
// Actually, let's check if this case is handled differently for empty txs
346+
// Let me check what happens with empty txs by adding the data cache entry as well
347+
dataHash := data.DACommitment().String()
348+
m.dataCache.SetDAIncluded(dataHash, uint64(1))
349+
dataHeightBytes := make([]byte, 8)
350+
binary.LittleEndian.PutUint64(dataHeightBytes, uint64(1))
351+
store.On("SetMetadata", mock.Anything, fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, uint64(5)), dataHeightBytes).Return(nil).Once()
352+
// Mock expectations for incrementDAIncludedHeight method
320353
heightBytes := make([]byte, 8)
321354
binary.LittleEndian.PutUint64(heightBytes, expectedDAIncludedHeight)
322355
store.On("SetMetadata", mock.Anything, DAIncludedHeightKey, heightBytes).Return(nil).Once()

block/manager.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ const (
5555

5656
// LastBatchDataKey is the key used for persisting the last batch data in store.
5757
LastBatchDataKey = "l"
58+
59+
// RollkitHeightToDAHeightKey is the key used for persisting the mapping rollkit height to da height in store.
60+
RollkitHeightToDAHeightKey = "rhb"
5861
)
5962

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

492+
// SetRollkitHeightToDAHeight stores the mapping from a Rollkit block height to the corresponding
493+
// DA (Data Availability) layer heights where the block's header and data were included.
494+
// This mapping is persisted in the store metadata and is used to track which DA heights
495+
// contain the block components for a given Rollkit height.
496+
//
497+
// For blocks with empty transactions, both header and data use the same DA height since
498+
// empty transaction data is not actually published to the DA layer.
499+
func (m *Manager) SetRollkitHeightToDAHeight(ctx context.Context, height uint64) error {
500+
header, data, err := m.store.GetBlockData(ctx, height)
501+
if err != nil {
502+
return err
503+
}
504+
headerHash, dataHash := header.Hash(), data.DACommitment()
505+
headerHeightBytes := make([]byte, 8)
506+
daHeightForHeader, ok := m.headerCache.GetDAIncludedHeight(headerHash.String())
507+
if !ok {
508+
return fmt.Errorf("header hash %s not found in cache", headerHash)
509+
}
510+
binary.LittleEndian.PutUint64(headerHeightBytes, daHeightForHeader)
511+
if err := m.store.SetMetadata(ctx, fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, height), headerHeightBytes); err != nil {
512+
return err
513+
}
514+
dataHeightBytes := make([]byte, 8)
515+
// For empty transactions, use the same DA height as the header
516+
if bytes.Equal(dataHash, dataHashForEmptyTxs) {
517+
binary.LittleEndian.PutUint64(dataHeightBytes, daHeightForHeader)
518+
} else {
519+
daHeightForData, ok := m.dataCache.GetDAIncludedHeight(dataHash.String())
520+
if !ok {
521+
return fmt.Errorf("data hash %s not found in cache", dataHash.String())
522+
}
523+
binary.LittleEndian.PutUint64(dataHeightBytes, daHeightForData)
524+
}
525+
if err := m.store.SetMetadata(ctx, fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, height), dataHeightBytes); err != nil {
526+
return err
527+
}
528+
return nil
529+
}
530+
489531
// GetExecutor returns the executor used by the manager.
490532
//
491533
// Note: this is a temporary method to allow testing the manager.

block/manager_test.go

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,11 @@ func TestIsDAIncluded(t *testing.T) {
194194
require.False(m.IsDAIncluded(ctx, height))
195195

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

200200
// Set the data as DAIncluded and verify IsDAIncluded returns true
201-
m.dataCache.SetDAIncluded(data.DACommitment().String())
201+
m.dataCache.SetDAIncluded(data.DACommitment().String(), uint64(1))
202202
require.True(m.IsDAIncluded(ctx, height))
203203
}
204204

@@ -1065,3 +1065,138 @@ func TestConfigurationDefaults(t *testing.T) {
10651065
require.Contains(err.Error(), "insufficient bytes for length prefix")
10661066
})
10671067
}
1068+
1069+
// TestSetRollkitHeightToDAHeight tests the SetRollkitHeightToDAHeight method which maps
1070+
// rollkit block heights to their corresponding DA heights for both headers and data.
1071+
// This method is critical for tracking which DA heights contain specific rollkit blocks.
1072+
func TestSetRollkitHeightToDAHeight(t *testing.T) {
1073+
t.Run("Success_WithTransactions", func(t *testing.T) {
1074+
require := require.New(t)
1075+
ctx := context.Background()
1076+
m, mockStore := getManager(t, nil, 0, 0)
1077+
1078+
// Create a block with transactions
1079+
header, data := types.GetRandomBlock(5, 3, "testchain")
1080+
height := uint64(5)
1081+
1082+
// Mock store expectations
1083+
mockStore.On("GetBlockData", mock.Anything, height).Return(header, data, nil)
1084+
1085+
// Set DA included heights in cache
1086+
headerHeight := uint64(10)
1087+
dataHeight := uint64(20)
1088+
m.headerCache.SetDAIncluded(header.Hash().String(), headerHeight)
1089+
m.dataCache.SetDAIncluded(data.DACommitment().String(), dataHeight)
1090+
1091+
// Mock metadata storage
1092+
headerKey := fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, height)
1093+
dataKey := fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, height)
1094+
mockStore.On("SetMetadata", mock.Anything, headerKey, mock.Anything).Return(nil)
1095+
mockStore.On("SetMetadata", mock.Anything, dataKey, mock.Anything).Return(nil)
1096+
1097+
// Call the method
1098+
err := m.SetRollkitHeightToDAHeight(ctx, height)
1099+
require.NoError(err)
1100+
1101+
mockStore.AssertExpectations(t)
1102+
})
1103+
1104+
t.Run("Success_EmptyTransactions", func(t *testing.T) {
1105+
require := require.New(t)
1106+
ctx := context.Background()
1107+
m, mockStore := getManager(t, nil, 0, 0)
1108+
1109+
// Create a block with no transactions
1110+
header, data := types.GetRandomBlock(5, 0, "testchain")
1111+
height := uint64(5)
1112+
1113+
// Mock store expectations
1114+
mockStore.On("GetBlockData", mock.Anything, height).Return(header, data, nil)
1115+
1116+
// Only set header as DA included (data uses special empty hash)
1117+
headerHeight := uint64(10)
1118+
m.headerCache.SetDAIncluded(header.Hash().String(), headerHeight)
1119+
// Note: we don't set data in cache for empty transactions
1120+
1121+
// Mock metadata storage - both should use header height for empty transactions
1122+
headerKey := fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, height)
1123+
dataKey := fmt.Sprintf("%s/%d/d", RollkitHeightToDAHeightKey, height)
1124+
mockStore.On("SetMetadata", mock.Anything, headerKey, mock.Anything).Return(nil)
1125+
mockStore.On("SetMetadata", mock.Anything, dataKey, mock.Anything).Return(nil)
1126+
1127+
// Call the method
1128+
err := m.SetRollkitHeightToDAHeight(ctx, height)
1129+
require.NoError(err)
1130+
1131+
mockStore.AssertExpectations(t)
1132+
})
1133+
1134+
t.Run("Error_HeaderNotInCache", func(t *testing.T) {
1135+
require := require.New(t)
1136+
ctx := context.Background()
1137+
m, mockStore := getManager(t, nil, 0, 0)
1138+
1139+
// Create a block
1140+
header, data := types.GetRandomBlock(5, 3, "testchain")
1141+
height := uint64(5)
1142+
1143+
// Mock store expectations
1144+
mockStore.On("GetBlockData", mock.Anything, height).Return(header, data, nil)
1145+
1146+
// Don't set header in cache, but set data
1147+
m.dataCache.SetDAIncluded(data.DACommitment().String(), uint64(11))
1148+
1149+
// Call the method - should fail
1150+
err := m.SetRollkitHeightToDAHeight(ctx, height)
1151+
require.Error(err)
1152+
require.Contains(err.Error(), "header hash")
1153+
require.Contains(err.Error(), "not found in cache")
1154+
1155+
mockStore.AssertExpectations(t)
1156+
})
1157+
1158+
t.Run("Error_DataNotInCache_NonEmptyTxs", func(t *testing.T) {
1159+
require := require.New(t)
1160+
ctx := context.Background()
1161+
m, mockStore := getManager(t, nil, 0, 0)
1162+
1163+
// Create a block with transactions
1164+
header, data := types.GetRandomBlock(5, 3, "testchain")
1165+
height := uint64(5)
1166+
1167+
// Mock store expectations
1168+
mockStore.On("GetBlockData", mock.Anything, height).Return(header, data, nil)
1169+
1170+
// Set header but not data in cache
1171+
m.headerCache.SetDAIncluded(header.Hash().String(), uint64(10))
1172+
1173+
// Mock metadata storage for header (should succeed)
1174+
headerKey := fmt.Sprintf("%s/%d/h", RollkitHeightToDAHeightKey, height)
1175+
mockStore.On("SetMetadata", mock.Anything, headerKey, mock.Anything).Return(nil)
1176+
1177+
// Call the method - should fail on data lookup
1178+
err := m.SetRollkitHeightToDAHeight(ctx, height)
1179+
require.Error(err)
1180+
require.Contains(err.Error(), "data hash")
1181+
require.Contains(err.Error(), "not found in cache")
1182+
1183+
mockStore.AssertExpectations(t)
1184+
})
1185+
1186+
t.Run("Error_BlockNotFound", func(t *testing.T) {
1187+
require := require.New(t)
1188+
ctx := context.Background()
1189+
m, mockStore := getManager(t, nil, 0, 0)
1190+
1191+
height := uint64(999) // Non-existent height
1192+
1193+
// Mock store expectations
1194+
mockStore.On("GetBlockData", mock.Anything, height).Return(nil, nil, errors.New("block not found"))
1195+
1196+
// Call the method - should fail
1197+
err := m.SetRollkitHeightToDAHeight(ctx, height)
1198+
require.Error(err)
1199+
1200+
mockStore.AssertExpectations(t)
1201+
})
1202+
}

block/retriever.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ func (m *Manager) handlePotentialHeader(ctx context.Context, bz []byte, daHeight
133133
return true
134134
}
135135
headerHash := header.Hash().String()
136-
m.headerCache.SetDAIncluded(headerHash)
136+
m.headerCache.SetDAIncluded(headerHash, daHeight)
137137
m.sendNonBlockingSignalToDAIncluderCh()
138138
m.logger.Info("header marked as DA included", "headerHeight", header.Height(), "headerHash", headerHash)
139139
if !m.headerCache.IsSeen(headerHash) {
@@ -168,7 +168,7 @@ func (m *Manager) handlePotentialData(ctx context.Context, bz []byte, daHeight u
168168
}
169169

170170
dataHashStr := signedData.Data.DACommitment().String()
171-
m.dataCache.SetDAIncluded(dataHashStr)
171+
m.dataCache.SetDAIncluded(dataHashStr, daHeight)
172172
m.sendNonBlockingSignalToDAIncluderCh()
173173
m.logger.Info("signed data marked as DA included", "dataHash", dataHashStr, "daHeight", daHeight, "height", signedData.Height())
174174
if !m.dataCache.IsSeen(dataHashStr) {

block/retriever_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,9 +528,9 @@ func TestProcessNextDAHeader_HeaderAndDataAlreadySeen(t *testing.T) {
528528

529529
// Mark both header and data as seen and DA included
530530
headerCache.SetSeen(headerHash)
531-
headerCache.SetDAIncluded(headerHash)
531+
headerCache.SetDAIncluded(headerHash, uint64(10))
532532
dataCache.SetSeen(dataHash)
533-
dataCache.SetDAIncluded(dataHash)
533+
dataCache.SetDAIncluded(dataHash, uint64(10))
534534

535535
// Set up mocks with explicit logging
536536
mockDAClient.On("GetIDs", mock.Anything, daHeight, mock.Anything).Return(&coreda.GetIDsResult{

block/submitter.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func (m *Manager) submitHeadersToDA(ctx context.Context, headersToSubmit []*type
172172
},
173173
func(submitted []*types.SignedHeader, res *coreda.ResultSubmit, gasPrice float64) {
174174
for _, header := range submitted {
175-
m.headerCache.SetDAIncluded(header.Hash().String())
175+
m.headerCache.SetDAIncluded(header.Hash().String(), res.Height)
176176
}
177177
lastSubmittedHeaderHeight := uint64(0)
178178
if l := len(submitted); l > 0 {
@@ -197,7 +197,7 @@ func (m *Manager) submitDataToDA(ctx context.Context, signedDataToSubmit []*type
197197
},
198198
func(submitted []*types.SignedData, res *coreda.ResultSubmit, gasPrice float64) {
199199
for _, signedData := range submitted {
200-
m.dataCache.SetDAIncluded(signedData.DACommitment().String())
200+
m.dataCache.SetDAIncluded(signedData.Data.DACommitment().String(), res.Height)
201201
}
202202
lastSubmittedDataHeight := uint64(0)
203203
if l := len(submitted); l > 0 {

0 commit comments

Comments
 (0)