@@ -13,6 +13,7 @@ import (
1313 "sync"
1414 "time"
1515
16+ libshare "github.com/celestiaorg/go-square/v3/share"
1617 "github.com/rs/zerolog"
1718
1819 blobrpc "github.com/evstack/ev-node/pkg/da/jsonrpc"
@@ -27,6 +28,18 @@ const (
2728 DefaultBlockTime = 1 * time .Second
2829)
2930
31+ // subscriber holds a registered subscription's channel and namespace filter.
32+ type subscriber struct {
33+ ch chan subscriptionEvent
34+ ns libshare.Namespace
35+ }
36+
37+ // subscriptionEvent is sent to subscribers when a new DA block is produced.
38+ type subscriptionEvent struct {
39+ height uint64
40+ blobs []* blobrpc.Blob
41+ }
42+
3043// LocalDA is a simple implementation of in-memory DA. Not production ready! Intended only for testing!
3144//
3245// Data is stored in a map, where key is a serialized sequence number. This key is returned as ID.
@@ -43,6 +56,10 @@ type LocalDA struct {
4356 blockTime time.Duration
4457 lastTime time.Time // tracks last timestamp to ensure monotonicity
4558
59+ // Subscriber registry (protected by mu)
60+ subscribers map [int ]* subscriber
61+ nextSubID int
62+
4663 logger zerolog.Logger
4764}
4865
@@ -57,6 +74,7 @@ func NewLocalDA(logger zerolog.Logger, opts ...func(*LocalDA) *LocalDA) *LocalDA
5774 data : make (map [uint64 ][]kvp ),
5875 timestamps : make (map [uint64 ]time.Time ),
5976 blobData : make (map [uint64 ][]* blobrpc.Blob ),
77+ subscribers : make (map [int ]* subscriber ),
6078 maxBlobSize : DefaultMaxBlobSize ,
6179 blockTime : DefaultBlockTime ,
6280 lastTime : time .Now (),
@@ -209,6 +227,7 @@ func (d *LocalDA) SubmitWithOptions(ctx context.Context, blobs []datypes.Blob, g
209227
210228 d .data [d .height ] = append (d .data [d .height ], kvp {ids [i ], blob })
211229 }
230+ d .notifySubscribers (d .height )
212231 d .logger .Info ().Uint64 ("newHeight" , d .height ).Int ("count" , len (ids )).Msg ("SubmitWithOptions successful" )
213232 return ids , nil
214233}
@@ -239,6 +258,7 @@ func (d *LocalDA) Submit(ctx context.Context, blobs []datypes.Blob, gasPrice flo
239258
240259 d .data [d .height ] = append (d .data [d .height ], kvp {ids [i ], blob })
241260 }
261+ d .notifySubscribers (d .height )
242262 d .logger .Info ().Uint64 ("newHeight" , d .height ).Int ("count" , len (ids )).Msg ("Submit successful" )
243263 return ids , nil
244264}
@@ -335,5 +355,68 @@ func (d *LocalDA) produceEmptyBlock() {
335355 defer d .mu .Unlock ()
336356 d .height ++
337357 d .timestamps [d .height ] = d .monotonicTime ()
358+ d .notifySubscribers (d .height )
338359 d .logger .Debug ().Uint64 ("height" , d .height ).Msg ("produced empty block" )
339360}
361+
362+ // subscribe registers a new subscriber for blobs matching the given namespace.
363+ // Returns a read-only channel and a subscription ID for later unsubscription.
364+ // Must NOT be called with d.mu held.
365+ func (d * LocalDA ) subscribe (ns libshare.Namespace ) (<- chan subscriptionEvent , int ) {
366+ d .mu .Lock ()
367+ defer d .mu .Unlock ()
368+
369+ id := d .nextSubID
370+ d .nextSubID ++
371+ ch := make (chan subscriptionEvent , 64 )
372+ d .subscribers [id ] = & subscriber {ch : ch , ns : ns }
373+ d .logger .Info ().Int ("subID" , id ).Str ("namespace" , hex .EncodeToString (ns .Bytes ())).Msg ("subscriber registered" )
374+ return ch , id
375+ }
376+
377+ // unsubscribe removes a subscriber and closes its channel.
378+ // Must NOT be called with d.mu held.
379+ func (d * LocalDA ) unsubscribe (id int ) {
380+ d .mu .Lock ()
381+ defer d .mu .Unlock ()
382+
383+ if sub , ok := d .subscribers [id ]; ok {
384+ close (sub .ch )
385+ delete (d .subscribers , id )
386+ d .logger .Info ().Int ("subID" , id ).Msg ("subscriber unregistered" )
387+ }
388+ }
389+
390+ // notifySubscribers sends a subscriptionEvent to all registered subscribers.
391+ // For each subscriber, only blobs matching the subscriber's namespace are included.
392+ // Slow consumers (full channel) are dropped to avoid blocking block production.
393+ // MUST be called with d.mu held.
394+ func (d * LocalDA ) notifySubscribers (height uint64 ) {
395+ if len (d .subscribers ) == 0 {
396+ return
397+ }
398+
399+ allBlobs := d .blobData [height ] // may be nil for empty blocks
400+
401+ for id , sub := range d .subscribers {
402+ // Filter blobs matching subscriber namespace
403+ var matched []* blobrpc.Blob
404+ for _ , b := range allBlobs {
405+ if b != nil && b .Namespace ().Equals (sub .ns ) {
406+ matched = append (matched , b )
407+ }
408+ }
409+
410+ evt := subscriptionEvent {
411+ height : height ,
412+ blobs : matched ,
413+ }
414+
415+ select {
416+ case sub .ch <- evt :
417+ default :
418+ // Slow consumer — drop to avoid blocking block production
419+ d .logger .Warn ().Int ("subID" , id ).Uint64 ("height" , height ).Msg ("dropping event for slow subscriber" )
420+ }
421+ }
422+ }
0 commit comments