@@ -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 .
2033type 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.
223260func (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