From 1e8b9f60931b02994fe784d506bb4eff21ac110a Mon Sep 17 00:00:00 2001 From: Theodore Schnepper Date: Wed, 13 May 2026 10:55:02 -0600 Subject: [PATCH] Attempt to fix progression halting via tainted SyncStatus When a `SyncStatus` from the `L2` comes in and moves the `SafeL2` reference backwards (at the very least in the case of `0` specifically) we can end up in a state where progression of the L2 chain through Espresso halts entirely. One such case where this can occur is when the `Streamer` gets `Reset` back to a prior point, or even potentially to a `Future` point, which causes a desync in the progress of a `ChannelManager` versus the `Streamer` itself causing the `Streamer` not to have the next expected `Batch` to be served. To address this specific issue, when loading `batches` for the `L1` submission, we add additional consideration to the `ChannelManager` to ensure that we are looking at the correct block in addition to the correct parent hash. --- espresso/environment/8_reorg_test.go | 34 +++++++++++++++ op-batcher/batcher/espresso.go | 64 +++++++++++++++++++++------- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/espresso/environment/8_reorg_test.go b/espresso/environment/8_reorg_test.go index 1d5c91022b9..092ef93dfbb 100644 --- a/espresso/environment/8_reorg_test.go +++ b/espresso/environment/8_reorg_test.go @@ -243,6 +243,8 @@ func TestE2eDevnetWithL1Reorg(t *testing.T) { if have, want := err, error(nil); have != want { t.Fatalf("failed to start dev environment with espresso dev node:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) } + defer env.Stop(t, system) + defer env.Stop(t, devNode) caffNode, err := env.LaunchCaffNode(t, system, devNode) if have, want := err, error(nil); have != want { @@ -254,3 +256,35 @@ func TestE2eDevnetWithL1Reorg(t *testing.T) { runL1Reorg(ctx, t, system) } + +// TestE2eEspressoStreamerSyncStatusTainting is a test that is meant to ensure +// that a tainted `SyncStatus` coming in to set the `safeBatchNumber` of the +// `Refresh` call to something like `0`, will ultimately not result in halting +// the progression of the chain through `Espresso`. +func TestE2eEspressoStreamerSyncStatusTainting(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + launcher := new(env.EspressoDevNodeLauncherDocker) + + system, devNode, err := launcher.StartE2eDevnet(ctx, t) + require.NoError(t, err) + + defer env.Stop(t, system) + defer env.Stop(t, devNode) + + l2Seq := system.NodeClient(e2esys.RoleSeq) + // l1Client := system.NodeClient(e2esys.RoleL1) + // caffClient := system.NodeClient(env.RoleCaffNode) + geth.WaitForBlockToBeFinalized(big.NewInt(5), l2Seq, time.Minute*2) + + l2Node := system.RollupClient(e2esys.RoleSeq) + + status, err := l2Node.SyncStatus(ctx) + require.NoError(t, err) + + err = system.BatchSubmitter.EspressoStreamer().Refresh(ctx, status.FinalizedL1, 0, status.FinalizedL2.L1Origin) + require.NoError(t, err) + + geth.WaitForBlockToBeFinalized(big.NewInt(int64(status.UnsafeL2.Number)+2), l2Seq, time.Minute*5) +} diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index 985a1ca7eed..d82a049a95d 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -859,6 +859,28 @@ func (l *BatchSubmitter) espressoSyncAndRefresh(ctx context.Context, newSyncStat } } +// skipStreamerToTargetBlockHeight will consume batches from the +// `EspressoStreamer` until we reach the target block height, or run out +// of batches to consume. +// +// NOTE: this is **NOT** guaranteed to ensure that the next `Batch` will +// be the expected height, it's just a best effort. +func (l *BatchSubmitter) skipStreamerToTargetBlockHeight(ctx context.Context, targetHeight uint64) { + streamer := l.EspressoStreamer() + batch := streamer.Peek(ctx) + for batch != nil && batch.Number() < targetHeight { + // Consume the Batch + streamer.Next(ctx) + batch = streamer.Peek(ctx) + } +} + +// isHashNoptSet is a helper function to check if a hash is the zero +// value (not set). +func isHashEmpty(hash common.Hash) bool { + return hash == (common.Hash{}) +} + // peekNextBatch returns the next batch from the streamer, performing a fork check // against an expected parent hash. // @@ -867,26 +889,36 @@ func (l *BatchSubmitter) espressoSyncAndRefresh(ctx context.Context, newSyncStat // the one position where we can set tip to the known safe head. Otherwise we accept the batch as-is. func (l *BatchSubmitter) peekNextBatch(ctx context.Context, syncStatus *eth.SyncStatus) *derive.EspressoBatch { l.channelMgrMutex.Lock() - tip := l.channelMgr.tip + var tipBlock SizedBlock + if len(l.channelMgr.blocks) > 0 { + tipBlock = l.channelMgr.blocks[len(l.channelMgr.blocks)-1] + } l.channelMgrMutex.Unlock() + targetBlock := syncStatus.SafeL2.Number + 1 + tipHash := syncStatus.SafeL2.Hash + if tipBlock != (SizedBlock{}) && tipBlock.Block != nil { + targetBlock = tipBlock.NumberU64() + 1 + tipHash = tipBlock.Hash() + } else if batch := l.EspressoStreamer().Peek(ctx); batch != nil && batch.Number() == targetBlock { + // Log indicating that we utilized the SafeL2 Hash as the next + // Streamer entry matched our expected Safe L2, and we didn't + // have any tip information from the Channel Manager. + l.Log.Debug( + "setting tip to safe l2 hash", + "batchNr", batch.Number(), + "batchParent", batch.Header().ParentHash.Hex(), + "tip", tipHash, + ) + } + + l.skipStreamerToTargetBlockHeight(ctx, targetBlock) batch := l.EspressoStreamer().Peek(ctx) if batch == nil { return nil } - // Check if we can set the tip if not set - if tip == (common.Hash{}) && (*batch).Number() == syncStatus.SafeL2.Number+1 { - l.Log.Info( - "setting tip to safe l2 hash", - "batchNr", (*batch).Number(), - "batchParent", (*batch).Header().ParentHash.Hex(), - "tip", tip, - ) - tip = syncStatus.SafeL2.Hash - } - - if tip == (common.Hash{}) { + if isHashEmpty(tipHash) { l.Log.Warn( "tip is not set, taking available batch", "blockParentHash", (*batch).Header().ParentHash.Hex(), @@ -895,14 +927,14 @@ func (l *BatchSubmitter) peekNextBatch(ctx context.Context, syncStatus *eth.Sync return batch } - if (*batch).Header().ParentHash != tip { + if batch.Header().ParentHash != tipHash { l.Log.Warn( "head batch fork mismatch, seeking to proper head", "batchNr", (*batch).Number(), "batchParent", (*batch).Header().ParentHash, - "tip", tip, + "tip", tipHash, ) - l.EspressoStreamer().SetProperHead(tip) + l.EspressoStreamer().SetProperHead(tipHash) return nil }