Skip to content

Commit 0a50aa6

Browse files
committed
fix: reject unexpected DA proposers early
1 parent 58ea3e8 commit 0a50aa6

4 files changed

Lines changed: 82 additions & 1 deletion

File tree

block/internal/syncing/da_retriever.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,17 @@ type pendingDataCleaner interface {
3232
removePendingData(height uint64)
3333
}
3434

35+
type expectedProposerProvider func(height uint64) ([]byte, bool)
36+
3537
// daRetriever handles DA retrieval operations for syncing
3638
type daRetriever struct {
3739
client da.Client
3840
cache cache.CacheManager
3941
genesis genesis.Genesis
4042
logger zerolog.Logger
4143

44+
expectedProposer expectedProposerProvider
45+
4246
mu sync.Mutex
4347
// transient cache, only full event need to be passed to the syncer
4448
// on restart, will be refetch as da height is updated by syncer
@@ -68,6 +72,10 @@ func NewDARetriever(
6872
}
6973
}
7074

75+
func (r *daRetriever) setExpectedProposerProvider(provider expectedProposerProvider) {
76+
r.expectedProposer = provider
77+
}
78+
7179
// RetrieveFromDA retrieves blocks from the specified DA height and returns height events
7280
func (r *daRetriever) RetrieveFromDA(ctx context.Context, daHeight uint64) ([]common.DAHeightEvent, error) {
7381
r.logger.Debug().Uint64("da_height", daHeight).Msg("retrieving from DA")
@@ -309,6 +317,11 @@ func (r *daRetriever) tryDecodeHeader(bz []byte, daHeight uint64) *types.SignedH
309317
return nil
310318
}
311319

320+
if err := r.assertExpectedProposer(header); err != nil {
321+
r.logger.Debug().Err(err).Msg("unexpected proposer")
322+
return nil
323+
}
324+
312325
if isValidEnvelope && !r.strictMode {
313326
r.logger.Info().Uint64("height", header.Height()).Msg("valid DA envelope detected, switching to STRICT MODE")
314327
r.strictMode = true
@@ -329,6 +342,21 @@ func (r *daRetriever) tryDecodeHeader(bz []byte, daHeight uint64) *types.SignedH
329342
return header
330343
}
331344

345+
func (r *daRetriever) assertExpectedProposer(header *types.SignedHeader) error {
346+
if r.expectedProposer == nil {
347+
return nil
348+
}
349+
350+
expected, ok := r.expectedProposer(header.Height())
351+
if !ok || len(expected) == 0 {
352+
return nil
353+
}
354+
if !bytes.Equal(header.ProposerAddress, expected) {
355+
return fmt.Errorf("%w - got: %x, want: %x", types.ErrUnexpectedProposer, header.ProposerAddress, expected)
356+
}
357+
return nil
358+
}
359+
332360
// tryDecodeData attempts to decode a blob as signed data
333361
func (r *daRetriever) tryDecodeData(bz []byte, daHeight uint64) *types.Data {
334362
var signedData types.SignedData

block/internal/syncing/da_retriever_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,33 @@ func TestDARetriever_ProcessBlobs_KeepsDataForLaterHeaderAfterCandidateEvent(t *
338338
require.Equal(t, data.Data.DACommitment().String(), events[0].Data.DACommitment().String())
339339
}
340340

341+
func TestDARetriever_ProcessBlobs_RejectsUnexpectedProposerBeforePendingHeader(t *testing.T) {
342+
expectedAddr, expectedPub, expectedSigner := buildSyncTestSigner(t)
343+
wrongAddr, wrongPub, wrongSigner := buildSyncTestSigner(t)
344+
gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: expectedAddr}
345+
346+
r := newTestDARetriever(t, nil, config.DefaultConfig(), gen)
347+
r.setExpectedProposerProvider(func(height uint64) ([]byte, bool) {
348+
if height != 5 {
349+
return nil, false
350+
}
351+
return expectedAddr, true
352+
})
353+
354+
_, data := makeSignedDataBytes(t, gen.ChainID, 5, expectedAddr, expectedPub, expectedSigner, 1)
355+
wrongHeaderBin, _ := makeSignedHeaderBytes(t, gen.ChainID, 5, wrongAddr, wrongPub, wrongSigner, nil, &data.Data, nil)
356+
correctHeaderBin, correctHeader := makeSignedHeaderBytes(t, gen.ChainID, 5, expectedAddr, expectedPub, expectedSigner, nil, &data.Data, nil)
357+
358+
events := r.processBlobs(context.Background(), [][]byte{wrongHeaderBin}, 100)
359+
require.Empty(t, events)
360+
require.NotContains(t, r.pendingHeaders, uint64(5), "unexpected proposer must not occupy the pending header slot")
361+
362+
events = r.processBlobs(context.Background(), [][]byte{correctHeaderBin}, 101)
363+
require.Empty(t, events)
364+
require.Contains(t, r.pendingHeaders, uint64(5), "expected proposer should be accepted as pending while data is missing")
365+
require.Equal(t, correctHeader.Hash().String(), r.pendingHeaders[5].Hash().String())
366+
}
367+
341368
func TestDARetriever_ProcessBlobs_MultipleHeadersCrossDAHeightMatching(t *testing.T) {
342369

343370
addr, pub, signer := buildSyncTestSigner(t)

block/internal/syncing/syncer.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,9 @@ func (s *Syncer) Start(ctx context.Context) (err error) {
181181
}
182182

183183
// Initialize handlers
184-
s.daRetriever = NewDARetriever(s.daClient, s.cache, s.genesis, s.logger)
184+
daRetriever := NewDARetriever(s.daClient, s.cache, s.genesis, s.logger)
185+
daRetriever.setExpectedProposerProvider(s.expectedProposerForHeight)
186+
s.daRetriever = daRetriever
185187
if s.config.Instrumentation.IsTracingEnabled() {
186188
s.daRetriever = WithTracingDARetriever(s.daRetriever)
187189
}
@@ -302,6 +304,14 @@ func (s *Syncer) SetLastState(state types.State) {
302304
s.lastState.Store(&state)
303305
}
304306

307+
func (s *Syncer) expectedProposerForHeight(height uint64) ([]byte, bool) {
308+
state := s.getLastState()
309+
if height != state.LastBlockHeight+1 || len(state.NextProposerAddress) == 0 {
310+
return nil, false
311+
}
312+
return state.NextProposerAddress, true
313+
}
314+
305315
// initializeState loads the current sync state
306316
func (s *Syncer) initializeState() error {
307317
// Load state from store

block/internal/syncing/syncer_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,22 @@ func TestSyncer_ValidateBlock_UsesStateNextProposer(t *testing.T) {
214214
require.Contains(t, err.Error(), "unexpected proposer")
215215
}
216216

217+
func TestSyncer_ExpectedProposerForHeight_OnlyNextHeight(t *testing.T) {
218+
addr, _, _ := buildSyncTestSigner(t)
219+
s := &Syncer{lastState: &atomic.Pointer[types.State]{}}
220+
s.SetLastState(types.State{
221+
LastBlockHeight: 4,
222+
NextProposerAddress: addr,
223+
})
224+
225+
got, ok := s.expectedProposerForHeight(5)
226+
require.True(t, ok)
227+
require.Equal(t, addr, got)
228+
229+
_, ok = s.expectedProposerForHeight(6)
230+
require.False(t, ok)
231+
}
232+
217233
func TestSyncer_TrySyncNextBlock_ClassifiesExternalValidationFailures(t *testing.T) {
218234
expectedAddr, expectedPub, expectedSigner := buildSyncTestSigner(t)
219235
wrongAddr, wrongPub, wrongSigner := buildSyncTestSigner(t)

0 commit comments

Comments
 (0)