From 08c83d9134bf53e16b1aec7d133ad8bff52c3a9c Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Sat, 20 Dec 2025 08:53:24 -0800 Subject: [PATCH 1/4] feat(testda): add header support with GetHeaderByHeight method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- test/testda/dummy.go | 73 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/test/testda/dummy.go b/test/testda/dummy.go index ecfa5cc85..0901e3e30 100644 --- a/test/testda/dummy.go +++ b/test/testda/dummy.go @@ -15,13 +15,27 @@ const ( DefaultMaxBlobSize = 2 * 1024 * 1024 ) +// Header contains DA layer header information for a given height. +// This mirrors the structure used by real DA clients like Celestia. +type Header struct { + Height uint64 + Timestamp time.Time +} + +// Time returns the block time from the header. +// This mirrors the jsonrpc.Header.Time() method. +func (h *Header) Time() time.Time { + return h.Timestamp +} + // DummyDA is a test implementation of the DA client interface. -// It supports blob storage, height simulation, and failure injection. +// It supports blob storage, height simulation, failure injection, and header retrieval. type DummyDA struct { mu sync.Mutex height atomic.Uint64 maxBlobSz uint64 blobs map[uint64]map[string][][]byte // height -> namespace -> blobs + headers map[uint64]*Header // height -> header (with timestamp) failSubmit atomic.Bool tickerMu sync.Mutex @@ -50,6 +64,7 @@ func New(opts ...Option) *DummyDA { d := &DummyDA{ maxBlobSz: DefaultMaxBlobSize, blobs: make(map[uint64]map[string][][]byte), + headers: make(map[uint64]*Header), } for _, opt := range opts { opt(d) @@ -81,6 +96,7 @@ func (d *DummyDA) Submit(_ context.Context, data [][]byte, _ float64, namespace blobSz += uint64(len(b)) } + now := time.Now() d.mu.Lock() height := d.height.Add(1) if d.blobs[height] == nil { @@ -88,6 +104,13 @@ func (d *DummyDA) Submit(_ context.Context, data [][]byte, _ float64, namespace } nsKey := string(namespace) d.blobs[height][nsKey] = append(d.blobs[height][nsKey], data...) + // Store header with timestamp for this height + if d.headers[height] == nil { + d.headers[height] = &Header{ + Height: height, + Timestamp: now, + } + } d.mu.Unlock() return datypes.ResultSubmit{ @@ -96,7 +119,7 @@ func (d *DummyDA) Submit(_ context.Context, data [][]byte, _ float64, namespace Height: height, BlobSize: blobSz, SubmittedCount: uint64(len(data)), - Timestamp: time.Now(), + Timestamp: now, }, } } @@ -109,6 +132,11 @@ func (d *DummyDA) Retrieve(_ context.Context, height uint64, namespace []byte) d if byHeight != nil { blobs = byHeight[string(namespace)] } + // Get timestamp from header if available, otherwise use current time + timestamp := time.Now() + if header := d.headers[height]; header != nil { + timestamp = header.Timestamp + } d.mu.Unlock() if len(blobs) == 0 { @@ -117,7 +145,7 @@ func (d *DummyDA) Retrieve(_ context.Context, height uint64, namespace []byte) d Code: datypes.StatusNotFound, Height: height, Message: datypes.ErrBlobNotFound.Error(), - Timestamp: time.Now(), + Timestamp: timestamp, }, } } @@ -128,7 +156,7 @@ func (d *DummyDA) Retrieve(_ context.Context, height uint64, namespace []byte) d Code: datypes.StatusSuccess, Height: height, IDs: ids, - Timestamp: time.Now(), + Timestamp: timestamp, }, Data: blobs, } @@ -202,7 +230,16 @@ func (d *DummyDA) StartHeightTicker(interval time.Duration) func() { for { select { case <-ticker.C: - d.height.Add(1) + now := time.Now() + height := d.height.Add(1) + d.mu.Lock() + if d.headers[height] == nil { + d.headers[height] = &Header{ + Height: height, + Timestamp: now, + } + } + d.mu.Unlock() case <-stopCh: return } @@ -219,11 +256,12 @@ func (d *DummyDA) StartHeightTicker(interval time.Duration) func() { } } -// Reset clears all stored blobs and resets the height. +// Reset clears all stored blobs, headers, and resets the height. func (d *DummyDA) Reset() { d.mu.Lock() d.height.Store(0) d.blobs = make(map[uint64]map[string][][]byte) + d.headers = make(map[uint64]*Header) d.failSubmit.Store(false) d.mu.Unlock() @@ -234,3 +272,26 @@ func (d *DummyDA) Reset() { } d.tickerMu.Unlock() } + +// GetHeaderByHeight retrieves the header for the given DA height. +// This mirrors the HeaderAPI.GetByHeight method from the real DA client. +// Returns nil if no header exists for the given height. +func (d *DummyDA) GetHeaderByHeight(_ context.Context, height uint64) (*Header, error) { + d.mu.Lock() + header := d.headers[height] + d.mu.Unlock() + + if header == nil { + // Return a header with current time if height is within known range + // This mimics the behavior of a real DA layer where empty blocks still have headers + currentHeight := d.height.Load() + if height <= currentHeight && height > 0 { + return &Header{ + Height: height, + Timestamp: time.Now(), + }, nil + } + return nil, datypes.ErrHeightFromFuture + } + return header, nil +} From c6173599ea51a2f98c17ba3cbaee2dc1d6fdb500 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Mon, 22 Dec 2025 05:32:46 -0800 Subject: [PATCH 2/4] dont create mock headers --- test/testda/dummy.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/testda/dummy.go b/test/testda/dummy.go index 0901e3e30..699c3237a 100644 --- a/test/testda/dummy.go +++ b/test/testda/dummy.go @@ -282,16 +282,11 @@ func (d *DummyDA) GetHeaderByHeight(_ context.Context, height uint64) (*Header, d.mu.Unlock() if header == nil { - // Return a header with current time if height is within known range - // This mimics the behavior of a real DA layer where empty blocks still have headers currentHeight := d.height.Load() - if height <= currentHeight && height > 0 { - return &Header{ - Height: height, - Timestamp: time.Now(), - }, nil + if height > currentHeight { + return nil, datypes.ErrHeightFromFuture } - return nil, datypes.ErrHeightFromFuture + return nil, datypes.ErrBlobNotFound } return header, nil } From 1ea830e42422fa91b36c218d8845ced29f1c7759 Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 22 Dec 2025 05:34:19 -0800 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- test/testda/dummy.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/testda/dummy.go b/test/testda/dummy.go index 699c3237a..af41c5802 100644 --- a/test/testda/dummy.go +++ b/test/testda/dummy.go @@ -133,10 +133,10 @@ func (d *DummyDA) Retrieve(_ context.Context, height uint64, namespace []byte) d blobs = byHeight[string(namespace)] } // Get timestamp from header if available, otherwise use current time - timestamp := time.Now() - if header := d.headers[height]; header != nil { - timestamp = header.Timestamp - } +var timestamp time.Time +if header := d.headers[height]; header != nil { + timestamp = header.Timestamp +} d.mu.Unlock() if len(blobs) == 0 { From 648aa39800ed1097e699ce013d44e9661bb8ff58 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Tue, 23 Dec 2025 03:47:18 -0800 Subject: [PATCH 4/4] lint --- test/testda/dummy.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/testda/dummy.go b/test/testda/dummy.go index af41c5802..633bf1cf9 100644 --- a/test/testda/dummy.go +++ b/test/testda/dummy.go @@ -133,10 +133,10 @@ func (d *DummyDA) Retrieve(_ context.Context, height uint64, namespace []byte) d blobs = byHeight[string(namespace)] } // Get timestamp from header if available, otherwise use current time -var timestamp time.Time -if header := d.headers[height]; header != nil { - timestamp = header.Timestamp -} + var timestamp time.Time + if header := d.headers[height]; header != nil { + timestamp = header.Timestamp + } d.mu.Unlock() if len(blobs) == 0 {