Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) {
DefaultMetricsProvider(config.Instrumentation),
logger,
nil, // sequencerVerifier
nil, // l1Tracker
nil, // sequencerSigner
nil, // ha: no HA in default node
)
Expand Down Expand Up @@ -503,6 +504,7 @@ func createSequencerComponents(
pool *bc.BlockPool,
logger log.Logger,
verifier sequencer.SequencerVerifier,
l1Tracker sequencer.L1Tracker,
signer sequencer.Signer,
sigStore *sequencer.SignatureStore,
ha sequencer.SequencerHA,
Expand All @@ -512,6 +514,7 @@ func createSequencerComponents(
l2Node,
logger,
verifier,
l1Tracker,
signer,
sigStore,
ha,
Expand All @@ -528,6 +531,7 @@ func createSequencerComponents(
stateV2,
logger,
verifier,
l1Tracker,
sigStore,
)
broadcastReactor.SetLogger(logger.With("module", "sequencer"))
Expand Down Expand Up @@ -783,6 +787,7 @@ func NewNode(
metricsProvider MetricsProvider,
logger log.Logger,
sequencerVerifier sequencer.SequencerVerifier,
l1Tracker sequencer.L1Tracker,
sequencerSigner sequencer.Signer,
ha sequencer.SequencerHA,
options ...Option,
Expand Down Expand Up @@ -1015,6 +1020,7 @@ func NewNode(
bcR.Pool(),
logger,
sequencerVerifier,
l1Tracker,
sequencerSigner,
sigStore,
ha, // HA service injected from NewNode caller; nil disables HA mode
Expand Down Expand Up @@ -1724,4 +1730,3 @@ func (n *Node) StartReactorsAfterReorg(currentHeight int64) error {
}
return bcR.SwitchToBlockSyncFromReorg(currentHeight)
}

1 change: 1 addition & 0 deletions rpc/test/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ func NewTendermint(app abci.Application, opts *Options) *nm.Node {
nm.DefaultMetricsProvider(config.Instrumentation),
logger,
nil, // sequencerVerifier
nil, // sequencerHealthGate
nil, // sequencerSigner
nil, // ha: no HA in RPC test node
)
Expand Down
20 changes: 18 additions & 2 deletions sequencer/broadcast_reactor.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ type BlockBroadcastReactor struct {
routinesStarted atomic.Bool
logger log.Logger

verifier SequencerVerifier
sigStore *SignatureStore
verifier SequencerVerifier
l1Tracker L1Tracker // required: gates peer-block sync on L1 freshness
sigStore *SignatureStore

// syncRequests tracks pending sync channel requests, keyed by height.
// Used to reject unsolicited responses before decode/verification.
Expand Down Expand Up @@ -132,6 +133,7 @@ func NewBlockBroadcastReactor(
stateV2 *StateV2,
logger log.Logger,
verifier SequencerVerifier,
l1Tracker L1Tracker,
sigStore *SignatureStore,
) *BlockBroadcastReactor {
r := &BlockBroadcastReactor{
Expand All @@ -147,6 +149,7 @@ func NewBlockBroadcastReactor(
blockReqLimiter: NewPeerRateLimiter(blockRequestRateLimit, blockRequestBurst),
logger: logger.With("module", "broadcastReactor"),
verifier: verifier,
l1Tracker: l1Tracker,
sigStore: sigStore,
}
r.BaseReactor = *p2p.NewBaseReactor("BlockBroadcast", r)
Expand Down Expand Up @@ -303,6 +306,14 @@ func (r *BlockBroadcastReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte
if !r.routinesStarted.Load() {
return
}
// L1 tracker: while L1 is stale, drop all inbound P2P messages. We may be
// blind to L1 SequencerUpdated events, so we neither accept/verify peer
// blocks (verification uses the verifier's stale L1-derived sequencer set,
// which could let a revoked-sequencer block through or mis-ban honest peers)
// nor serve sync requests. Placed before decode so nothing is processed.
if r.l1Tracker.IsHalt() {
return
}
r.logger.Debug("Receive message", "chId", chID, "src", src.ID(), "len", len(msgBytes))
msg, err := decodeMsg(msgBytes)
if err != nil {
Expand Down Expand Up @@ -544,6 +555,11 @@ func (r *BlockBroadcastReactor) tryApplyFromCache() {
// checkSyncGap: request missing blocks via SequencerSyncChannel
// All sync requests go through this method (no longer uses blocksync pool)
func (r *BlockBroadcastReactor) checkSyncGap() {
// L1 tracker: while L1 is stale, do not actively pull blocks from peers.
if r.l1Tracker.IsHalt() {
return
}

localHeight := r.stateV2.LatestHeight()
maxPeerHeight := r.getPool().MaxPeerHeight()
gap := maxPeerHeight - localHeight
Expand Down
30 changes: 30 additions & 0 deletions sequencer/broadcast_reactor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,33 @@ func TestBanPeer_PersistentPeerAddPeerNotRejected(t *testing.T) {
require.False(t, r.isBanned(mp.ID()),
"persistent peer must not be considered banned after misbehavior")
}

// ----------------------------------------------------------------------------
// L1 health gate on the sync path
// ----------------------------------------------------------------------------

func TestReceive_L1HaltDropsMessages(t *testing.T) {
// routinesStarted=true and L1 halted: Receive must drop the message at the
// L1 gate, before decode/verify/ban — so it must NOT panic even though
// stateV2/Switch are nil and the payload is garbage.
r := newReactorForTest()
r.logger = log.NewNopLogger()
r.routinesStarted.Store(true)
r.l1Tracker = &mockL1Tracker{halt: true}

require.NotPanics(t, func() {
r.Receive(BlockBroadcastChannel, nil, []byte("garbage"))
}, "Receive should drop messages at the L1 gate when halted, before decode")
}

func TestCheckSyncGap_L1HaltBlocksRequests(t *testing.T) {
// With L1 halted, checkSyncGap must return before calling
// stateV2.LatestHeight()/getPool() (both nil here) — so it must NOT panic.
r := newReactorForTest()
r.logger = log.NewNopLogger()
r.l1Tracker = &mockL1Tracker{halt: true}

require.NotPanics(t, func() {
r.checkSyncGap()
}, "checkSyncGap should early-return at the L1 gate before dereferencing nil pool/stateV2")
}
11 changes: 11 additions & 0 deletions sequencer/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,14 @@ type SequencerHA interface {
// Must be called before Start().
SetOnBlockApplied(fn func(*BlockV2) error)
}

// L1Tracker reports whether L1 RPC has fallen too far behind for this node to
// safely act. When L1 is stale, the node may be blind to L1 SequencerUpdated
// events, so the sequencer must stop producing and fullnodes must stop syncing
// to avoid following a revoked sequencer. Implemented by the L1 tracker in the
// node binary and required by StateV2 / the broadcast reactor.
type L1Tracker interface {
// IsHalt reports whether L1 is stale enough that this node must halt block
// production and sync.
IsHalt() bool
}
21 changes: 15 additions & 6 deletions sequencer/state_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ type StateV2 struct {
latestBlock *BlockV2

// Dependencies
l2Node l2node.L2Node
signer Signer
verifier SequencerVerifier
sigStore *SignatureStore
ha SequencerHA // nil = single-node mode
logger log.Logger
l2Node l2node.L2Node
signer Signer
verifier SequencerVerifier
l1Tracker L1Tracker // required: gates block production on L1 freshness
sigStore *SignatureStore
ha SequencerHA // nil = single-node mode
logger log.Logger

// Block production
blockInterval time.Duration // empty-block fallback interval (default 3s)
Expand All @@ -57,6 +58,7 @@ func NewStateV2(
l2Node l2node.L2Node,
logger log.Logger,
verifier SequencerVerifier,
l1Tracker L1Tracker,
signer Signer,
sigStore *SignatureStore,
ha SequencerHA,
Expand All @@ -69,6 +71,7 @@ func NewStateV2(
l2Node: l2Node,
signer: signer,
verifier: verifier,
l1Tracker: l1Tracker,
sigStore: sigStore,
ha: ha,
blockInterval: BlockInterval,
Expand Down Expand Up @@ -203,6 +206,12 @@ func (s *StateV2) isActiveSequencer() bool {
return false
}

// L1 tracker: stop producing if L1 RPC is stale (we may be blind to
// SequencerUpdated events on L1 and could produce as a revoked sequencer).
if s.l1Tracker.IsHalt() {
return false
}

s.mtx.RLock()
lb := s.latestBlock
s.mtx.RUnlock()
Expand Down
Loading
Loading