Skip to content

Commit 4b54e7a

Browse files
committed
feat(syncer): verify force inclusion for p2p blocks
1 parent 544c0b9 commit 4b54e7a

File tree

2 files changed

+36
-28
lines changed

2 files changed

+36
-28
lines changed

block/internal/syncing/syncer.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ type Syncer struct {
110110
gracePeriodMultiplier *atomic.Pointer[float64]
111111
blockFullnessEMA *atomic.Pointer[float64]
112112
gracePeriodConfig forcedInclusionGracePeriodConfig
113+
p2pHeightHints map[uint64]uint64 // map[height]daHeight
113114

114115
// Lifecycle
115116
ctx context.Context
@@ -177,6 +178,7 @@ func NewSyncer(
177178
gracePeriodMultiplier: gracePeriodMultiplier,
178179
blockFullnessEMA: blockFullnessEMA,
179180
gracePeriodConfig: newForcedInclusionGracePeriodConfig(),
181+
p2pHeightHints: make(map[uint64]uint64),
180182
}
181183
}
182184

@@ -541,6 +543,8 @@ func (s *Syncer) processHeightEvent(event *common.DAHeightEvent) {
541543
Uint64("da_height_hint", daHeightHint).
542544
Msg("P2P event with DA height hint, triggering targeted DA retrieval")
543545

546+
s.p2pHeightHints[height] = daHeightHint
547+
544548
// Trigger targeted DA retrieval in background via worker pool
545549
s.asyncDARetriever.RequestRetrieval(daHeightHint)
546550
}
@@ -581,7 +585,7 @@ func (s *Syncer) processHeightEvent(event *common.DAHeightEvent) {
581585
}
582586

583587
// only save to p2p stores if the event came from DA
584-
if event.Source == common.SourceDA { // TODO(@julienrbrt): To be reverted once DA Hints are merged (https://github.com/evstack/ev-node/pull/2891)
588+
if event.Source == common.SourceDA {
585589
g, ctx := errgroup.WithContext(s.ctx)
586590
g.Go(func() error {
587591
// broadcast header locally only — prevents spamming the p2p network with old height notifications,
@@ -636,13 +640,17 @@ func (s *Syncer) trySyncNextBlock(event *common.DAHeightEvent) error {
636640
}
637641

638642
// Verify forced inclusion transactions if configured
639-
if event.Source == common.SourceDA {
640-
if err := s.verifyForcedInclusionTxs(currentState, data); err != nil {
641-
s.logger.Error().Err(err).Uint64("height", nextHeight).Msg("forced inclusion verification failed")
642-
if errors.Is(err, errMaliciousProposer) {
643-
s.cache.RemoveHeaderDAIncluded(headerHash)
644-
return err
645-
}
643+
currentDaHeight, ok := s.p2pHeightHints[nextHeight]
644+
if !ok {
645+
currentDaHeight = currentState.DAHeight
646+
} else {
647+
delete(s.p2pHeightHints, nextHeight)
648+
}
649+
if err := s.verifyForcedInclusionTxs(currentDaHeight, data); err != nil {
650+
s.logger.Error().Err(err).Uint64("height", nextHeight).Msg("forced inclusion verification failed")
651+
if errors.Is(err, errMaliciousProposer) {
652+
s.cache.RemoveHeaderDAIncluded(headerHash)
653+
return err
646654
}
647655
}
648656

@@ -855,7 +863,7 @@ func (s *Syncer) getEffectiveGracePeriod() uint64 {
855863
// Note: Due to block size constraints (MaxBytes), sequencers may defer forced inclusion transactions
856864
// to future blocks (smoothing). This is legitimate behavior within an epoch.
857865
// However, ALL forced inclusion txs from an epoch MUST be included before the next epoch begins or grace boundary (whichever comes later).
858-
func (s *Syncer) verifyForcedInclusionTxs(currentState types.State, data *types.Data) error {
866+
func (s *Syncer) verifyForcedInclusionTxs(daHeight uint64, data *types.Data) error {
859867
if s.fiRetriever == nil {
860868
return nil
861869
}
@@ -865,7 +873,7 @@ func (s *Syncer) verifyForcedInclusionTxs(currentState types.State, data *types.
865873
s.updateDynamicGracePeriod(blockFullness)
866874

867875
// Retrieve forced inclusion transactions from DA for current epoch
868-
forcedIncludedTxsEvent, err := s.fiRetriever.RetrieveForcedIncludedTxs(s.ctx, currentState.DAHeight)
876+
forcedIncludedTxsEvent, err := s.fiRetriever.RetrieveForcedIncludedTxs(s.ctx, daHeight)
869877
if err != nil {
870878
if errors.Is(err, da.ErrForceInclusionNotConfigured) {
871879
s.logger.Debug().Msg("forced inclusion namespace not configured, skipping verification")
@@ -928,10 +936,10 @@ func (s *Syncer) verifyForcedInclusionTxs(currentState types.State, data *types.
928936
effectiveGracePeriod := s.getEffectiveGracePeriod()
929937
graceBoundary := pending.EpochEnd + (effectiveGracePeriod * s.genesis.DAEpochForcedInclusion)
930938

931-
if currentState.DAHeight > graceBoundary {
939+
if daHeight > graceBoundary {
932940
maliciousTxs = append(maliciousTxs, pending)
933941
s.logger.Warn().
934-
Uint64("current_da_height", currentState.DAHeight).
942+
Uint64("current_da_height", daHeight).
935943
Uint64("epoch_end", pending.EpochEnd).
936944
Uint64("grace_boundary", graceBoundary).
937945
Uint64("base_grace_periods", s.gracePeriodConfig.basePeriod).
@@ -941,7 +949,7 @@ func (s *Syncer) verifyForcedInclusionTxs(currentState types.State, data *types.
941949
Msg("forced inclusion transaction past grace boundary - marking as malicious")
942950
} else {
943951
remainingPending = append(remainingPending, pending)
944-
if currentState.DAHeight > pending.EpochEnd {
952+
if daHeight > pending.EpochEnd {
945953
txsInGracePeriod++
946954
}
947955
}
@@ -965,7 +973,7 @@ func (s *Syncer) verifyForcedInclusionTxs(currentState types.State, data *types.
965973
effectiveGracePeriod := s.getEffectiveGracePeriod()
966974
s.logger.Error().
967975
Uint64("height", data.Height()).
968-
Uint64("current_da_height", currentState.DAHeight).
976+
Uint64("current_da_height", daHeight).
969977
Int("malicious_count", len(maliciousTxs)).
970978
Uint64("base_grace_periods", s.gracePeriodConfig.basePeriod).
971979
Uint64("effective_grace_periods", effectiveGracePeriod).
@@ -985,7 +993,7 @@ func (s *Syncer) verifyForcedInclusionTxs(currentState types.State, data *types.
985993

986994
s.logger.Info().
987995
Uint64("height", data.Height()).
988-
Uint64("da_height", currentState.DAHeight).
996+
Uint64("da_height", daHeight).
989997
Uint64("epoch_start", forcedIncludedTxsEvent.StartDaHeight).
990998
Uint64("epoch_end", forcedIncludedTxsEvent.EndDaHeight).
991999
Int("included_count", includedCount).

block/internal/syncing/syncer_forced_inclusion_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ func TestVerifyForcedInclusionTxs_AllTransactionsIncluded(t *testing.T) {
409409
currentState.DAHeight = 0
410410

411411
// Verify - should pass since all forced txs are included
412-
err = s.verifyForcedInclusionTxs(currentState, data)
412+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data)
413413
require.NoError(t, err)
414414
}
415415

@@ -484,7 +484,7 @@ func TestVerifyForcedInclusionTxs_MissingTransactions(t *testing.T) {
484484
currentState.DAHeight = 0
485485

486486
// Verify - should pass since forced tx blob may be legitimately deferred within the epoch
487-
err = s.verifyForcedInclusionTxs(currentState, data)
487+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data)
488488
require.NoError(t, err)
489489

490490
// Mock DA for next epoch to return no forced inclusion transactions
@@ -497,7 +497,7 @@ func TestVerifyForcedInclusionTxs_MissingTransactions(t *testing.T) {
497497
data2 := makeData(gen.ChainID, 2, 1)
498498
data2.Txs[0] = []byte("regular_tx_3")
499499

500-
err = s.verifyForcedInclusionTxs(currentState, data2)
500+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data2)
501501
require.NoError(t, err) // Should pass since DAHeight=1 equals grace boundary, not past it
502502

503503
// Mock DA for height 2 to return no forced inclusion transactions
@@ -510,7 +510,7 @@ func TestVerifyForcedInclusionTxs_MissingTransactions(t *testing.T) {
510510
data3 := makeData(gen.ChainID, 3, 1)
511511
data3.Txs[0] = types.Tx([]byte("regular_tx_4"))
512512

513-
err = s.verifyForcedInclusionTxs(currentState, data3)
513+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data3)
514514
require.Error(t, err)
515515
require.Contains(t, err.Error(), "sequencer is malicious")
516516
require.Contains(t, err.Error(), "past grace boundary")
@@ -588,7 +588,7 @@ func TestVerifyForcedInclusionTxs_PartiallyIncluded(t *testing.T) {
588588
currentState.DAHeight = 0
589589

590590
// Verify - should pass since dataBin2 may be legitimately deferred within the epoch
591-
err = s.verifyForcedInclusionTxs(currentState, data)
591+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data)
592592
require.NoError(t, err)
593593

594594
// Mock DA for next epoch to return no forced inclusion transactions
@@ -602,7 +602,7 @@ func TestVerifyForcedInclusionTxs_PartiallyIncluded(t *testing.T) {
602602
data2.Txs[0] = types.Tx([]byte("regular_tx_3"))
603603

604604
// Verify - should pass since we're at the grace boundary, not past it
605-
err = s.verifyForcedInclusionTxs(currentState, data2)
605+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data2)
606606
require.NoError(t, err)
607607

608608
// Mock DA for height 2 (when we move to DAHeight 2)
@@ -617,7 +617,7 @@ func TestVerifyForcedInclusionTxs_PartiallyIncluded(t *testing.T) {
617617
data3 := makeData(gen.ChainID, 3, 1)
618618
data3.Txs[0] = types.Tx([]byte("regular_tx_4"))
619619

620-
err = s.verifyForcedInclusionTxs(currentState, data3)
620+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data3)
621621
require.Error(t, err)
622622
require.Contains(t, err.Error(), "sequencer is malicious")
623623
require.Contains(t, err.Error(), "past grace boundary")
@@ -687,7 +687,7 @@ func TestVerifyForcedInclusionTxs_NoForcedTransactions(t *testing.T) {
687687
currentState.DAHeight = 0
688688

689689
// Verify - should pass since no forced txs to verify
690-
err = s.verifyForcedInclusionTxs(currentState, data)
690+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data)
691691
require.NoError(t, err)
692692
}
693693

@@ -748,7 +748,7 @@ func TestVerifyForcedInclusionTxs_NamespaceNotConfigured(t *testing.T) {
748748
currentState.DAHeight = 0
749749

750750
// Verify - should pass since namespace not configured
751-
err = s.verifyForcedInclusionTxs(currentState, data)
751+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data)
752752
require.NoError(t, err)
753753
}
754754

@@ -834,7 +834,7 @@ func TestVerifyForcedInclusionTxs_DeferralWithinEpoch(t *testing.T) {
834834
currentState.DAHeight = 104
835835

836836
// Verify - should pass since dataBin2 can be deferred within epoch
837-
err = s.verifyForcedInclusionTxs(currentState, data1)
837+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data1)
838838
require.NoError(t, err)
839839

840840
// Verify that dataBin2 is now tracked as pending
@@ -863,7 +863,7 @@ func TestVerifyForcedInclusionTxs_DeferralWithinEpoch(t *testing.T) {
863863
data2.Txs[1] = types.Tx(dataBin2) // The deferred one we're waiting for
864864

865865
// Verify - should pass since dataBin2 is now included and clears pending
866-
err = s.verifyForcedInclusionTxs(currentState, data2)
866+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data2)
867867
require.NoError(t, err)
868868

869869
// Verify that pending queue is now empty (dataBin2 was included)
@@ -957,7 +957,7 @@ func TestVerifyForcedInclusionTxs_MaliciousAfterEpochEnd(t *testing.T) {
957957
currentState.DAHeight = 102
958958

959959
// Verify - should pass, tx can be deferred within epoch
960-
err = s.verifyForcedInclusionTxs(currentState, data1)
960+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data1)
961961
require.NoError(t, err)
962962
}
963963

@@ -1050,6 +1050,6 @@ func TestVerifyForcedInclusionTxs_SmoothingExceedsEpoch(t *testing.T) {
10501050
currentState := s.getLastState()
10511051
currentState.DAHeight = 102 // At epoch end
10521052

1053-
err = s.verifyForcedInclusionTxs(currentState, data1)
1053+
err = s.verifyForcedInclusionTxs(currentState.DAHeight, data1)
10541054
require.NoError(t, err, "smoothing within epoch should be allowed")
10551055
}

0 commit comments

Comments
 (0)