Skip to content

Commit 08c83d9

Browse files
tac0turtleclaude
andcommitted
feat(testda): add header support with GetHeaderByHeight method
Add header storage and retrieval to DummyDA to support timestamp determinism in tests. This enables tests to use the same header retrieval pattern as the real DA client. Changes: - Add Header struct with Height, Timestamp, and Time() method - Store headers with timestamps when blobs are submitted - Store headers when height ticker advances - Add GetHeaderByHeight method mirroring HeaderAPI.GetByHeight - Update Retrieve to use stored timestamps for consistency Closes #2944 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d386df5 commit 08c83d9

File tree

1 file changed

+67
-6
lines changed

1 file changed

+67
-6
lines changed

test/testda/dummy.go

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,27 @@ const (
1515
DefaultMaxBlobSize = 2 * 1024 * 1024
1616
)
1717

18+
// Header contains DA layer header information for a given height.
19+
// This mirrors the structure used by real DA clients like Celestia.
20+
type Header struct {
21+
Height uint64
22+
Timestamp time.Time
23+
}
24+
25+
// Time returns the block time from the header.
26+
// This mirrors the jsonrpc.Header.Time() method.
27+
func (h *Header) Time() time.Time {
28+
return h.Timestamp
29+
}
30+
1831
// DummyDA is a test implementation of the DA client interface.
19-
// It supports blob storage, height simulation, and failure injection.
32+
// It supports blob storage, height simulation, failure injection, and header retrieval.
2033
type DummyDA struct {
2134
mu sync.Mutex
2235
height atomic.Uint64
2336
maxBlobSz uint64
2437
blobs map[uint64]map[string][][]byte // height -> namespace -> blobs
38+
headers map[uint64]*Header // height -> header (with timestamp)
2539
failSubmit atomic.Bool
2640

2741
tickerMu sync.Mutex
@@ -50,6 +64,7 @@ func New(opts ...Option) *DummyDA {
5064
d := &DummyDA{
5165
maxBlobSz: DefaultMaxBlobSize,
5266
blobs: make(map[uint64]map[string][][]byte),
67+
headers: make(map[uint64]*Header),
5368
}
5469
for _, opt := range opts {
5570
opt(d)
@@ -81,13 +96,21 @@ func (d *DummyDA) Submit(_ context.Context, data [][]byte, _ float64, namespace
8196
blobSz += uint64(len(b))
8297
}
8398

99+
now := time.Now()
84100
d.mu.Lock()
85101
height := d.height.Add(1)
86102
if d.blobs[height] == nil {
87103
d.blobs[height] = make(map[string][][]byte)
88104
}
89105
nsKey := string(namespace)
90106
d.blobs[height][nsKey] = append(d.blobs[height][nsKey], data...)
107+
// Store header with timestamp for this height
108+
if d.headers[height] == nil {
109+
d.headers[height] = &Header{
110+
Height: height,
111+
Timestamp: now,
112+
}
113+
}
91114
d.mu.Unlock()
92115

93116
return datypes.ResultSubmit{
@@ -96,7 +119,7 @@ func (d *DummyDA) Submit(_ context.Context, data [][]byte, _ float64, namespace
96119
Height: height,
97120
BlobSize: blobSz,
98121
SubmittedCount: uint64(len(data)),
99-
Timestamp: time.Now(),
122+
Timestamp: now,
100123
},
101124
}
102125
}
@@ -109,6 +132,11 @@ func (d *DummyDA) Retrieve(_ context.Context, height uint64, namespace []byte) d
109132
if byHeight != nil {
110133
blobs = byHeight[string(namespace)]
111134
}
135+
// Get timestamp from header if available, otherwise use current time
136+
timestamp := time.Now()
137+
if header := d.headers[height]; header != nil {
138+
timestamp = header.Timestamp
139+
}
112140
d.mu.Unlock()
113141

114142
if len(blobs) == 0 {
@@ -117,7 +145,7 @@ func (d *DummyDA) Retrieve(_ context.Context, height uint64, namespace []byte) d
117145
Code: datypes.StatusNotFound,
118146
Height: height,
119147
Message: datypes.ErrBlobNotFound.Error(),
120-
Timestamp: time.Now(),
148+
Timestamp: timestamp,
121149
},
122150
}
123151
}
@@ -128,7 +156,7 @@ func (d *DummyDA) Retrieve(_ context.Context, height uint64, namespace []byte) d
128156
Code: datypes.StatusSuccess,
129157
Height: height,
130158
IDs: ids,
131-
Timestamp: time.Now(),
159+
Timestamp: timestamp,
132160
},
133161
Data: blobs,
134162
}
@@ -202,7 +230,16 @@ func (d *DummyDA) StartHeightTicker(interval time.Duration) func() {
202230
for {
203231
select {
204232
case <-ticker.C:
205-
d.height.Add(1)
233+
now := time.Now()
234+
height := d.height.Add(1)
235+
d.mu.Lock()
236+
if d.headers[height] == nil {
237+
d.headers[height] = &Header{
238+
Height: height,
239+
Timestamp: now,
240+
}
241+
}
242+
d.mu.Unlock()
206243
case <-stopCh:
207244
return
208245
}
@@ -219,11 +256,12 @@ func (d *DummyDA) StartHeightTicker(interval time.Duration) func() {
219256
}
220257
}
221258

222-
// Reset clears all stored blobs and resets the height.
259+
// Reset clears all stored blobs, headers, and resets the height.
223260
func (d *DummyDA) Reset() {
224261
d.mu.Lock()
225262
d.height.Store(0)
226263
d.blobs = make(map[uint64]map[string][][]byte)
264+
d.headers = make(map[uint64]*Header)
227265
d.failSubmit.Store(false)
228266
d.mu.Unlock()
229267

@@ -234,3 +272,26 @@ func (d *DummyDA) Reset() {
234272
}
235273
d.tickerMu.Unlock()
236274
}
275+
276+
// GetHeaderByHeight retrieves the header for the given DA height.
277+
// This mirrors the HeaderAPI.GetByHeight method from the real DA client.
278+
// Returns nil if no header exists for the given height.
279+
func (d *DummyDA) GetHeaderByHeight(_ context.Context, height uint64) (*Header, error) {
280+
d.mu.Lock()
281+
header := d.headers[height]
282+
d.mu.Unlock()
283+
284+
if header == nil {
285+
// Return a header with current time if height is within known range
286+
// This mimics the behavior of a real DA layer where empty blocks still have headers
287+
currentHeight := d.height.Load()
288+
if height <= currentHeight && height > 0 {
289+
return &Header{
290+
Height: height,
291+
Timestamp: time.Now(),
292+
}, nil
293+
}
294+
return nil, datypes.ErrHeightFromFuture
295+
}
296+
return header, nil
297+
}

0 commit comments

Comments
 (0)