66 "errors"
77 "fmt"
88 "strings"
9+ "sync"
910 "time"
1011
1112 "github.com/celestiaorg/go-square/v3/share"
@@ -37,6 +38,7 @@ type client struct {
3738 dataNamespaceBz []byte
3839 forcedNamespaceBz []byte
3940 hasForcedNamespace bool
41+ timestampCache * blockTimestampCache
4042}
4143
4244// Ensure client implements the FullClient interface (Client + BlobGetter + Verifier).
@@ -45,8 +47,72 @@ var _ FullClient = (*client)(nil)
4547const (
4648 blockTimestampFetchMaxAttempts = 3
4749 blockTimestampFetchBackoff = 100 * time .Millisecond
50+ blockTimestampCacheWindow = 2048
4851)
4952
53+ type blockTimestampCache struct {
54+ mu sync.RWMutex
55+ byHeight map [uint64 ]time.Time
56+ highest uint64
57+ window uint64
58+ }
59+
60+ func newBlockTimestampCache (window uint64 ) * blockTimestampCache {
61+ if window == 0 {
62+ window = blockTimestampCacheWindow
63+ }
64+ return & blockTimestampCache {
65+ byHeight : make (map [uint64 ]time.Time ),
66+ window : window ,
67+ }
68+ }
69+
70+ func (c * blockTimestampCache ) get (height uint64 ) (time.Time , bool ) {
71+ c .mu .RLock ()
72+ defer c .mu .RUnlock ()
73+
74+ blockTime , ok := c .byHeight [height ]
75+ return blockTime , ok
76+ }
77+
78+ func (c * blockTimestampCache ) put (height uint64 , blockTime time.Time ) {
79+ if c == nil || blockTime .IsZero () {
80+ return
81+ }
82+
83+ blockTime = blockTime .UTC ()
84+
85+ c .mu .Lock ()
86+ defer c .mu .Unlock ()
87+
88+ minRetained := c .minRetainedHeightLocked ()
89+ if minRetained > 0 && height < minRetained {
90+ return
91+ }
92+
93+ if height > c .highest {
94+ c .highest = height
95+ }
96+ c .byHeight [height ] = blockTime
97+
98+ minRetained = c .minRetainedHeightLocked ()
99+ if minRetained == 0 {
100+ return
101+ }
102+ for cachedHeight := range c .byHeight {
103+ if cachedHeight < minRetained {
104+ delete (c .byHeight , cachedHeight )
105+ }
106+ }
107+ }
108+
109+ func (c * blockTimestampCache ) minRetainedHeightLocked () uint64 {
110+ if c .window == 0 || c .highest < c .window - 1 {
111+ return 0
112+ }
113+ return c .highest - c .window + 1
114+ }
115+
50116// NewClient creates a new blob client wrapper with pre-calculated namespace bytes.
51117func NewClient (cfg Config ) FullClient {
52118 if cfg .DA == nil {
@@ -71,6 +137,7 @@ func NewClient(cfg Config) FullClient {
71137 dataNamespaceBz : datypes .NamespaceFromString (cfg .DataNamespace ).Bytes (),
72138 forcedNamespaceBz : forcedNamespaceBz ,
73139 hasForcedNamespace : hasForcedNamespace ,
140+ timestampCache : newBlockTimestampCache (blockTimestampCacheWindow ),
74141 }
75142}
76143
@@ -197,7 +264,9 @@ func (c *client) getBlockTimestamp(ctx context.Context, height uint64) (time.Tim
197264 header , err := c .headerAPI .GetByHeight (headerCtx , height )
198265 cancel ()
199266 if err == nil {
200- return header .Time (), nil
267+ blockTime := header .Time ().UTC ()
268+ c .storeBlockTimestamp (height , blockTime )
269+ return blockTime , nil
201270 }
202271 lastErr = err
203272
@@ -225,10 +294,38 @@ func (c *client) getBlockTimestamp(ctx context.Context, height uint64) (time.Tim
225294 return time.Time {}, fmt .Errorf ("get header timestamp for block %d after %d attempts: %w" , height , blockTimestampFetchMaxAttempts , lastErr )
226295}
227296
297+ func (c * client ) cachedBlockTimestamp (height uint64 ) (time.Time , bool ) {
298+ return c .timestampCache .get (height )
299+ }
300+
301+ func (c * client ) storeBlockTimestamp (height uint64 , blockTime time.Time ) {
302+ c .timestampCache .put (height , blockTime )
303+ }
304+
305+ func (c * client ) resolveBlockTimestamp (ctx context.Context , height uint64 , strict bool ) (time.Time , error ) {
306+ if ! strict {
307+ if blockTime , ok := c .cachedBlockTimestamp (height ); ok {
308+ return blockTime , nil
309+ }
310+ return time.Time {}, nil
311+ }
312+
313+ return c .getBlockTimestamp (ctx , height )
314+ }
315+
316+ // RetrieveBlobs retrieves blobs without blocking on DA header timestamps.
317+ func (c * client ) RetrieveBlobs (ctx context.Context , height uint64 , namespace []byte ) datypes.ResultRetrieve {
318+ return c .retrieve (ctx , height , namespace , false )
319+ }
320+
228321// Retrieve retrieves blobs from the DA layer at the specified height and namespace.
229322// It uses GetAll to fetch all blobs at once.
230323// The timestamp is derived from the DA block header to ensure determinism.
231324func (c * client ) Retrieve (ctx context.Context , height uint64 , namespace []byte ) datypes.ResultRetrieve {
325+ return c .retrieve (ctx , height , namespace , true )
326+ }
327+
328+ func (c * client ) retrieve (ctx context.Context , height uint64 , namespace []byte , strictTimestamp bool ) datypes.ResultRetrieve {
232329 ns , err := share .NewNamespaceFromBytes (namespace )
233330 if err != nil {
234331 return datypes.ResultRetrieve {
@@ -250,8 +347,7 @@ func (c *client) Retrieve(ctx context.Context, height uint64, namespace []byte)
250347 switch {
251348 case strings .Contains (err .Error (), datypes .ErrBlobNotFound .Error ()):
252349 c .logger .Debug ().Uint64 ("height" , height ).Msg ("No blobs found at height" )
253- // Fetch block timestamp for deterministic responses using parent context
254- blockTime , err := c .getBlockTimestamp (ctx , height )
350+ blockTime , err := c .resolveBlockTimestamp (ctx , height , strictTimestamp )
255351 if err != nil {
256352 c .logger .Error ().Uint64 ("height" , height ).Err (err ).Msg ("failed to get block timestamp" )
257353 return datypes.ResultRetrieve {
@@ -293,8 +389,7 @@ func (c *client) Retrieve(ctx context.Context, height uint64, namespace []byte)
293389 }
294390 }
295391
296- // Fetch block timestamp for deterministic responses using parent context
297- blockTime , err := c .getBlockTimestamp (ctx , height )
392+ blockTime , err := c .resolveBlockTimestamp (ctx , height , strictTimestamp )
298393 if err != nil {
299394 c .logger .Error ().Uint64 ("height" , height ).Err (err ).Msg ("failed to get block timestamp" )
300395 return datypes.ResultRetrieve {
@@ -426,14 +521,14 @@ func (c *client) Subscribe(ctx context.Context, namespace []byte, includeTimesta
426521 var blockTime time.Time
427522 // Use header time if available (celestia-node v0.21.0+)
428523 if resp .Header != nil && ! resp .Header .Time .IsZero () {
429- blockTime = resp .Header .Time
524+ blockTime = resp .Header .Time .UTC ()
525+ c .storeBlockTimestamp (resp .Height , blockTime )
430526 } else if includeTimestamp {
431527 // Fallback to fetching timestamp for older nodes
432528 blockTime , err = c .getBlockTimestamp (ctx , resp .Height )
433529 if err != nil {
434530 c .logger .Error ().Uint64 ("height" , resp .Height ).Err (err ).Msg ("failed to get DA block timestamp for subscription event" )
435- blockTime = time .Now ()
436- // TODO: we should retry fetching the timestamp. Current time may mess block time consistency for based sequencers.
531+ blockTime = time.Time {}
437532 }
438533 }
439534 select {
0 commit comments