Skip to content

Commit 57aa405

Browse files
committed
fix(da): fix polling fallback when ws not available
1 parent 79b5b4f commit 57aa405

11 files changed

Lines changed: 148 additions & 7 deletions

File tree

apps/evm/server/force_inclusion_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ func (m *mockDA) HasForcedInclusionNamespace() bool {
8585
return true
8686
}
8787

88+
func (m *mockDA) SupportsSubscribe() bool { return true }
89+
8890
func (m *mockDA) GetLatestDAHeight(_ context.Context) (uint64, error) {
8991
return 0, nil
9092
}

block/internal/da/async_block_retriever_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ func TestAsyncBlockRetriever_SubscriptionDrivenCaching(t *testing.T) {
5353
client := &mocks.MockClient{}
5454
fiNs := datypes.NamespaceFromString("test-fi-ns").Bytes()
5555

56+
client.On("SupportsSubscribe").Return(true)
57+
5658
// Create a subscription channel that delivers one event then blocks.
5759
subCh := make(chan datypes.SubscriptionEvent, 1)
5860
subCh <- datypes.SubscriptionEvent{
@@ -104,6 +106,8 @@ func TestAsyncBlockRetriever_CatchupFillsGaps(t *testing.T) {
104106
client := &mocks.MockClient{}
105107
fiNs := datypes.NamespaceFromString("test-fi-ns").Bytes()
106108

109+
client.On("SupportsSubscribe").Return(true)
110+
107111
// Subscription delivers height 105 (no blobs — just a signal).
108112
subCh := make(chan datypes.SubscriptionEvent, 1)
109113
subCh <- datypes.SubscriptionEvent{Height: 105}
@@ -153,6 +157,8 @@ func TestAsyncBlockRetriever_HeightFromFuture(t *testing.T) {
153157
client := &mocks.MockClient{}
154158
fiNs := datypes.NamespaceFromString("test-fi-ns").Bytes()
155159

160+
client.On("SupportsSubscribe").Return(true)
161+
156162
// Subscription delivers height 100 with no blobs.
157163
subCh := make(chan datypes.SubscriptionEvent)
158164
client.On("Subscribe", mock.Anything, fiNs, mock.Anything).Return((<-chan datypes.SubscriptionEvent)(subCh), nil).Once()
@@ -187,6 +193,8 @@ func TestAsyncBlockRetriever_StopGracefully(t *testing.T) {
187193
client := &mocks.MockClient{}
188194
fiNs := datypes.NamespaceFromString("test-fi-ns").Bytes()
189195

196+
client.On("SupportsSubscribe").Return(true)
197+
190198
blockCh := make(chan datypes.SubscriptionEvent)
191199
client.On("Subscribe", mock.Anything, fiNs, mock.Anything).Return((<-chan datypes.SubscriptionEvent)(blockCh), nil).Maybe()
192200
client.On("Retrieve", mock.Anything, mock.Anything, fiNs).Return(datypes.ResultRetrieve{
@@ -211,6 +219,8 @@ func TestAsyncBlockRetriever_ReconnectOnSubscriptionError(t *testing.T) {
211219
client := &mocks.MockClient{}
212220
fiNs := datypes.NamespaceFromString("test-fi-ns").Bytes()
213221

222+
client.On("SupportsSubscribe").Return(true)
223+
214224
// First subscription closes immediately (simulating error).
215225
closedCh := make(chan datypes.SubscriptionEvent)
216226
close(closedCh)

block/internal/da/client.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type client struct {
3838
dataNamespaceBz []byte
3939
forcedNamespaceBz []byte
4040
hasForcedNamespace bool
41+
isWebSocket bool
4142
timestampCache *blockTimestampCache
4243
}
4344

@@ -137,6 +138,7 @@ func NewClient(cfg Config) FullClient {
137138
dataNamespaceBz: datypes.NamespaceFromString(cfg.DataNamespace).Bytes(),
138139
forcedNamespaceBz: forcedNamespaceBz,
139140
hasForcedNamespace: hasForcedNamespace,
141+
isWebSocket: cfg.DA.IsWebSocket,
140142
timestampCache: newBlockTimestampCache(blockTimestampCacheWindow),
141143
}
142144
}
@@ -485,6 +487,12 @@ func (c *client) HasForcedInclusionNamespace() bool {
485487
return c.hasForcedNamespace
486488
}
487489

490+
// SupportsSubscribe reports whether the underlying transport supports
491+
// channel-based subscriptions (WebSocket).
492+
func (c *client) SupportsSubscribe() bool {
493+
return c.isWebSocket
494+
}
495+
488496
// Subscribe subscribes to blobs in the given namespace via the celestia-node
489497
// Subscribe API. It returns a channel that emits a SubscriptionEvent for every
490498
// DA block containing a matching blob. The channel is closed when ctx is

block/internal/da/interface.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ type Client interface {
3131
// GetLatestDAHeight returns the latest height available on the DA layer.
3232
GetLatestDAHeight(ctx context.Context) (uint64, error)
3333

34+
// SupportsSubscribe reports whether the underlying transport supports
35+
// channel-based subscriptions (WebSocket). When false, callers must use
36+
// polling-based retrieval via Retrieve instead.
37+
SupportsSubscribe() bool
38+
3439
// Namespace accessors.
3540
GetHeaderNamespace() []byte
3641
GetDataNamespace() []byte

block/internal/da/subscriber.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,15 @@ func (s *Subscriber) Start(ctx context.Context) error {
122122

123123
ctx, cancel := context.WithCancel(ctx)
124124
s.cancel = cancel
125-
s.wg.Add(2)
126125
s.lifecycleMu.Unlock()
127126

128-
go s.followLoop(ctx)
127+
if s.client.SupportsSubscribe() {
128+
s.wg.Add(2)
129+
go s.followLoop(ctx)
130+
} else {
131+
s.wg.Add(2)
132+
go s.pollLoop(ctx)
133+
}
129134
go s.catchupLoop(ctx)
130135

131136
return nil
@@ -167,6 +172,57 @@ func (s *Subscriber) signalCatchup() {
167172
}
168173
}
169174

175+
// pollLoop periodically queries the latest DA height and triggers
176+
// catchup when new heights are available. This is used when the
177+
// underlying transport does not support channel-based subscriptions (HTTP).
178+
func (s *Subscriber) pollLoop(ctx context.Context) {
179+
defer s.wg.Done()
180+
181+
s.logger.Info().Msg("starting poll loop")
182+
defer s.logger.Info().Msg("poll loop stopped")
183+
184+
// Do an immediate poll on startup so we don't wait for the first tick.
185+
s.pollDAHeight(ctx)
186+
187+
ticker := time.NewTicker(s.daBlockTime)
188+
defer ticker.Stop()
189+
190+
for {
191+
select {
192+
case <-ctx.Done():
193+
return
194+
case <-ticker.C:
195+
s.pollDAHeight(ctx)
196+
}
197+
}
198+
}
199+
200+
// pollDAHeight queries GetLatestDAHeight and signals catchup when a new height
201+
// is observed.
202+
func (s *Subscriber) pollDAHeight(ctx context.Context) {
203+
height, err := s.client.GetLatestDAHeight(ctx)
204+
if err != nil {
205+
if ctx.Err() != nil {
206+
return
207+
}
208+
s.logger.Warn().Err(err).Msg("poll: failed to get latest DA height")
209+
return
210+
}
211+
212+
cur := s.highestSeenDAHeight.Load()
213+
if height <= cur {
214+
return
215+
}
216+
217+
s.seenSubscriptionEvent.Store(true)
218+
s.logger.Debug().
219+
Uint64("new_da_height", height).
220+
Uint64("current_highest_seen", cur).
221+
Msg("poll: observed new DA height")
222+
223+
s.updateHighest(height)
224+
}
225+
170226
// followLoop subscribes to DA blob events and keeps highestSeenDAHeight up to date.
171227
func (s *Subscriber) followLoop(ctx context.Context) {
172228
defer s.wg.Done()

block/internal/da/tracing.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ func (t *tracedClient) GetForcedInclusionNamespace() []byte {
165165
func (t *tracedClient) HasForcedInclusionNamespace() bool {
166166
return t.inner.HasForcedInclusionNamespace()
167167
}
168+
func (t *tracedClient) SupportsSubscribe() bool {
169+
return t.inner.SupportsSubscribe()
170+
}
168171
func (t *tracedClient) Subscribe(ctx context.Context, namespace []byte, includeTimestamp bool) (<-chan datypes.SubscriptionEvent, error) {
169172
return t.inner.Subscribe(ctx, namespace, includeTimestamp)
170173
}

block/internal/da/tracing_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func (m *mockFullClient) GetHeaderNamespace() []byte {
7474
func (m *mockFullClient) GetDataNamespace() []byte { return []byte{0x02} }
7575
func (m *mockFullClient) GetForcedInclusionNamespace() []byte { return []byte{0x03} }
7676
func (m *mockFullClient) HasForcedInclusionNamespace() bool { return true }
77+
func (m *mockFullClient) SupportsSubscribe() bool { return true }
7778

7879
// setup a tracer provider + span recorder
7980
func setupDATrace(t *testing.T, inner FullClient) (FullClient, *tracetest.SpanRecorder) {

block/internal/syncing/syncer_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ func makeSignedHeaderBytes(
116116
func setupMockDAClient(tb testing.TB) (da.Client, chan datypes.SubscriptionEvent) {
117117
mockClient := testmocks.NewMockClient(tb)
118118
eventCh := make(chan datypes.SubscriptionEvent, 1)
119+
mockClient.EXPECT().SupportsSubscribe().Return(true).Maybe()
119120
mockClient.EXPECT().Subscribe(mock.Anything, mock.Anything, mock.Anything).Return((<-chan datypes.SubscriptionEvent)(eventCh), nil).Maybe()
120121
return mockClient, eventCh
121122
}

pkg/da/jsonrpc/client.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import (
1313

1414
// Client dials the celestia-node RPC "blob" and "header" namespaces.
1515
type Client struct {
16-
Blob BlobAPI
17-
Header HeaderAPI
18-
closer jsonrpc.ClientCloser
16+
Blob BlobAPI
17+
Header HeaderAPI
18+
IsWebSocket bool
19+
closer jsonrpc.ClientCloser
1920
}
2021

2122
// Close closes the underlying JSON-RPC connection.
@@ -78,9 +79,15 @@ func NewWSClient(ctx context.Context, logger zerolog.Logger, addr, token string,
7879
client, err := NewClient(ctx, httpToWS(addr), token, authHeaderName)
7980
if err != nil {
8081
logger.Warn().Err(err).Msg("DA websocket connection failed, falling back to DA polling")
81-
return NewClient(ctx, addr, token, authHeaderName)
82+
client, err = NewClient(ctx, addr, token, authHeaderName)
83+
if err != nil {
84+
return nil, err
85+
}
86+
client.IsWebSocket = false
87+
return client, nil
8288
}
8389

90+
client.IsWebSocket = true
8491
return client, nil
8592
}
8693

test/mocks/da.go

Lines changed: 45 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)