diff --git a/op-acceptance-tests/tests/interop/message/interop_msg_test.go b/op-acceptance-tests/tests/interop/message/interop_msg_test.go index 21f6cab0a65dc..df0f0f88a8d58 100644 --- a/op-acceptance-tests/tests/interop/message/interop_msg_test.go +++ b/op-acceptance-tests/tests/interop/message/interop_msg_test.go @@ -22,7 +22,7 @@ func TestInitExecMsg(gt *testing.T) { // Trigger random init message at chain A initIntent, _ := alice.SendInitMessage(interop.RandomInitTrigger(rng, eventLoggerAddress, rng.Intn(5), rng.Intn(30))) // Make sure supervisor indexs block which includes init message - sys.Supervisor.AdvanceUnsafeHead(alice.ChainID(), 2) + sys.Supervisor.AdvancedUnsafeHead(alice.ChainID(), 2) // Single event in tx so index is 0 bob.SendExecMessage(initIntent, 0) } diff --git a/op-acceptance-tests/tests/interop/sync/redundant_interop/init_test.go b/op-acceptance-tests/tests/interop/sync/redundant_interop/init_test.go new file mode 100644 index 0000000000000..cb752dd444f59 --- /dev/null +++ b/op-acceptance-tests/tests/interop/sync/redundant_interop/init_test.go @@ -0,0 +1,11 @@ +package sync + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +func TestMain(m *testing.M) { + presets.DoMain(m, presets.WithRedundantInterop()) +} diff --git a/op-acceptance-tests/tests/interop/sync/redundant_interop/interop_sync_test.go b/op-acceptance-tests/tests/interop/sync/redundant_interop/interop_sync_test.go new file mode 100644 index 0000000000000..0004309facd39 --- /dev/null +++ b/op-acceptance-tests/tests/interop/sync/redundant_interop/interop_sync_test.go @@ -0,0 +1,87 @@ +package sync + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// TestUnsafeChainKnownToL2CL tests the below scenario: +// supervisor cross-safe ahead of L2CL cross-safe, aka L2CL can "skip" forward to match safety of supervisor. +// To create this out-of-sync scenario, we follow the steps below: +// 1. Make sequencer (L2CL), verifier (L2CL), and supervisor sync for a few blocks. +// - Sequencer and verifier are connected via P2P, which makes their unsafe heads in sync. +// - Both L2CLs are in managed mode, digesting L1 blocks from the supervisor and reporting unsafe and safe blocks back to the supervisor. +// - Wait enough for both L2CLs advance safe heads. +// 2. Disconnect the P2P connection between the sequencer and verifier. +// - The verifier will not receive unsafe heads via P2P, and can only update unsafe heads matching with safe heads by reading L1 batches. +// - The verifier safe head will lag behind or match the sequencer and supervisor because all three components share the same L1 view. +// 3. Stop verifier L2CL +// - The verifier will not be able to advance unsafe head and safe head. +// - The sequencer will advance unsafe head and safe head, as well as synced with supervisor. +// 4. Wait until sequencer and supervisor diverged enough from the verifier. +// - To make the verifier held unsafe blocks which are already viewed as safe by sequencer and supervisor, we wait. +// - Wait until supervisor viewed safe head number is large enough than the stopped verifier's safe head view. +// 5. Restart the verifier. +// - The verifier will not sync via P2P but only able to advance unsafe and safe heads by reading L1 batches. +// - The verifier will quickly catch up with the sequencer safe head as well as the supervisor. +// - The verifier will "skip" processing already known unsafe blocks, and consolidate them into safe blocks. +func TestUnsafeChainKnownToL2CL(gt *testing.T) { + t := devtest.SerialT(gt) + + sys := presets.NewRedundantInterop(t) + logger := sys.Log.With("Test", "TestUnsafeChainKnownToL2CL") + require := sys.T.Require() + + logger.Info("make sure verifier safe head advances") + dsl.CheckAll(t, + sys.L2CLA.Advanced(types.CrossSafe, 5, 30), + sys.L2CLA2.Advanced(types.CrossSafe, 5, 30), + ) + + safeA2 := sys.L2ELA2.BlockRefByLabel(eth.Safe) + logger.Info("verifier advanced safe head", "number", safeA2.Number) + unsafeA2 := sys.L2ELA2.BlockRefByLabel(eth.Unsafe) + logger.Info("verifier advanced unsafe head", "number", unsafeA2.Number) + + // For making verifier stop advancing unsafe head via P2P + logger.Info("disconnect p2p between L2CLs") + sys.L2CLA.DisconnectPeer(sys.L2CLA2) + sys.L2CLA2.DisconnectPeer(sys.L2CLA) + + // For making verifer not sync at all + logger.Info("stop verifier") + sys.L2CLA2.Stop() + + delta := uint64(10) + logger.Info("wait until supervisor reaches safe head", "delta", delta) + sys.Supervisor.AdvancedSafeHead(sys.L2ChainA.ChainID(), delta, 30) + + // Restarted verifier will advance its unsafe head by reading L1 but not by P2P + logger.Info("restart verifier") + sys.L2CLA2.Start() + + safeA2 = sys.L2ELA2.BlockRefByLabel(eth.Safe) + logger.Info("verifier safe head after restart", "number", safeA2.Number) + unsafeA2 = sys.L2ELA2.BlockRefByLabel(eth.Unsafe) + logger.Info("verifier unsafe head after restart", "number", unsafeA2.Number) + + // Make sure there are unsafe blocks to be consolidated: + // To check verifier does not have to process blocks since unsafe blocks are already processed + require.Greater(unsafeA2.Number, safeA2.Number) + + logger.Info("make sure verifier unsafe head was consolidated to safe") + dsl.CheckAll(t, sys.L2CLA2.Reached(types.CrossSafe, unsafeA2.Number, 30)) + + safeA := sys.L2ELA.BlockRefByLabel(eth.Safe) + target := safeA.Number + delta + logger.Info("make sure verifier unsafe head advances due to safe head advances", "target", target, "delta", delta) + dsl.CheckAll(t, sys.L2CLA2.Reached(types.LocalUnsafe, target, 30)) + + block := sys.L2ELA2.BlockRefByNumber(unsafeA2.Number) + require.Equal(unsafeA2.Hash, block.Hash) +} diff --git a/op-acceptance-tests/tests/interop/sync/init_test.go b/op-acceptance-tests/tests/interop/sync/simple_interop/init_test.go similarity index 100% rename from op-acceptance-tests/tests/interop/sync/init_test.go rename to op-acceptance-tests/tests/interop/sync/simple_interop/init_test.go diff --git a/op-acceptance-tests/tests/interop/sync/interop_sync_test.go b/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go similarity index 100% rename from op-acceptance-tests/tests/interop/sync/interop_sync_test.go rename to op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go diff --git a/op-devstack/dsl/l2_cl.go b/op-devstack/dsl/l2_cl.go index 38cadba597d9f..af6757eacc29b 100644 --- a/op-devstack/dsl/l2_cl.go +++ b/op-devstack/dsl/l2_cl.go @@ -1,8 +1,16 @@ package dsl import ( + "context" + "errors" + "fmt" + "time" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-service/apis" "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/retry" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) // L2CLNode wraps a stack.L2CLNode interface for DSL operations @@ -10,14 +18,16 @@ type L2CLNode struct { commonImpl inner stack.L2CLNode control stack.ControlPlane + chainID eth.ChainID } // NewL2CLNode creates a new L2CLNode DSL wrapper -func NewL2CLNode(inner stack.L2CLNode, control stack.ControlPlane) *L2CLNode { +func NewL2CLNode(inner stack.L2CLNode, control stack.ControlPlane, chainID eth.ChainID) *L2CLNode { return &L2CLNode{ commonImpl: commonFromT(inner.T()), inner: inner, control: control, + chainID: chainID, } } @@ -31,10 +41,7 @@ func (cl *L2CLNode) Escape() stack.L2CLNode { } func (cl *L2CLNode) SafeL2BlockRef() eth.L2BlockRef { - syncStatus, err := cl.Escape().RollupAPI().SyncStatus(cl.ctx) - cl.require.NoError(err, "Expected to get sync status") - - return syncStatus.SafeL2 + return cl.HeadBlockRef(types.CrossSafe) } func (cl *L2CLNode) Start() { @@ -44,3 +51,83 @@ func (cl *L2CLNode) Start() { func (cl *L2CLNode) Stop() { cl.control.L2CLNodeState(cl.inner.ID(), stack.Stop) } + +func (cl *L2CLNode) SyncStatus() *eth.SyncStatus { + ctx, cancel := context.WithTimeout(cl.ctx, DefaultTimeout) + defer cancel() + syncStatus, err := cl.inner.RollupAPI().SyncStatus(ctx) + cl.require.NoError(err) + return syncStatus +} + +// HeadBlockRef fetches L2CL sync status and returns block ref with given safety level +func (cl *L2CLNode) HeadBlockRef(lvl types.SafetyLevel) eth.L2BlockRef { + syncStatus := cl.SyncStatus() + var blockRef eth.L2BlockRef + switch lvl { + case types.Finalized: + blockRef = syncStatus.FinalizedL2 + case types.CrossSafe: + blockRef = syncStatus.SafeL2 + case types.LocalSafe: + blockRef = syncStatus.LocalSafeL2 + case types.CrossUnsafe: + blockRef = syncStatus.CrossUnsafeL2 + case types.LocalUnsafe: + blockRef = syncStatus.UnsafeL2 + default: + cl.require.NoError(errors.New("invalid safety level")) + } + return blockRef +} + +func (cl *L2CLNode) ChainID() eth.ChainID { + return cl.chainID +} + +// Advanced returns a lambda that checks the L2CL chain head with given safety level advanced more than delta block number +// Composable with other lambdas to wait in parallel +func (cl *L2CLNode) Advanced(lvl types.SafetyLevel, delta uint64, attempts int) CheckFunc { + return func() error { + initial := cl.HeadBlockRef(lvl) + target := initial.Number + delta + cl.log.Info("expecting chain to advance", "id", cl.inner.ID(), "chain", cl.chainID, "label", lvl, "delta", delta) + return cl.Reached(lvl, target, attempts)() + } +} + +// Reached returns a lambda that checks the L2CL chain head with given safety level reaches the target block number +// Composable with other lambdas to wait in parallel +func (cl *L2CLNode) Reached(lvl types.SafetyLevel, target uint64, attempts int) CheckFunc { + return func() error { + cl.log.Info("expecting chain to reach", "id", cl.inner.ID(), "chain", cl.chainID, "label", lvl, "target", target) + return retry.Do0(cl.ctx, attempts, &retry.FixedStrategy{Dur: 2 * time.Second}, + func() error { + head := cl.HeadBlockRef(lvl) + if head.Number >= target { + cl.log.Info("chain advanced", "id", cl.inner.ID(), "chain", cl.chainID, "label", lvl, "target", target) + return nil + } + cl.log.Info("Chain sync status", "id", cl.inner.ID(), "chain", cl.chainID, "label", lvl, "target", target, "current", head.Number) + return fmt.Errorf("expected head to advance: %s", lvl) + }) + } +} + +func (cl *L2CLNode) PeerInfo() *apis.PeerInfo { + peerInfo, err := cl.inner.P2PAPI().Self(cl.ctx) + cl.require.NoError(err, "failed to get peer info") + return peerInfo +} + +func (cl *L2CLNode) Peers() *apis.PeerDump { + peerDump, err := cl.inner.P2PAPI().Peers(cl.ctx, true) + cl.require.NoError(err, "failed to get peers") + return peerDump +} + +func (cl *L2CLNode) DisconnectPeer(peer *L2CLNode) { + peerInfo := peer.PeerInfo() + err := cl.inner.P2PAPI().DisconnectPeer(cl.ctx, peerInfo.PeerID) + cl.require.NoError(err, "failed to disconnect peer") +} diff --git a/op-devstack/dsl/l2_el.go b/op-devstack/dsl/l2_el.go index a2acf363b6454..ed62ed3eb9631 100644 --- a/op-devstack/dsl/l2_el.go +++ b/op-devstack/dsl/l2_el.go @@ -79,3 +79,11 @@ func (el *L2ELNode) DoesNotAdvance(label eth.BlockLabel) CheckFunc { return nil } } + +func (el *L2ELNode) BlockRefByNumber(num uint64) eth.BlockRef { + ctx, cancel := context.WithTimeout(el.ctx, DefaultTimeout) + defer cancel() + block, err := el.inner.EthClient().BlockRefByNumber(ctx, num) + el.require.NoError(err, "block not found using block label") + return block +} diff --git a/op-devstack/dsl/supervisor.go b/op-devstack/dsl/supervisor.go index 710bdcd2fce34..8aaa095acb50b 100644 --- a/op-devstack/dsl/supervisor.go +++ b/op-devstack/dsl/supervisor.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/retry" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/status" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) type Supervisor struct { @@ -88,36 +89,55 @@ func (s *Supervisor) FetchSyncStatus() eth.SupervisorSyncStatus { } func (s *Supervisor) SafeBlockID(chainID eth.ChainID) eth.BlockID { - ctx, cancel := context.WithTimeout(s.ctx, DefaultTimeout) - defer cancel() - syncStatus, err := retry.Do[eth.SupervisorSyncStatus](ctx, 2, retry.Fixed(500*time.Millisecond), func() (eth.SupervisorSyncStatus, error) { - syncStatus, err := s.inner.QueryAPI().SyncStatus(s.ctx) - if errors.Is(err, status.ErrStatusTrackerNotReady) { - s.log.Debug("Sync status not ready from supervisor") - } - return syncStatus, err - }) - s.require.NoError(err, "Failed to fetch sync status") + return s.L2HeadBlockID(chainID, types.CrossSafe) +} - return syncStatus.Chains[chainID].CrossSafe +// L2HeadBlockID fetches supervisor sync status and returns block id with given safety level +func (s *Supervisor) L2HeadBlockID(chainID eth.ChainID, lvl types.SafetyLevel) eth.BlockID { + supervisorSyncStatus := s.FetchSyncStatus() + supervisorChainSyncStatus, ok := supervisorSyncStatus.Chains[chainID] + s.require.True(ok, "chain id not found in supervisor sync status") + var blockID eth.BlockID + switch lvl { + case types.Finalized: + blockID = supervisorChainSyncStatus.Finalized + case types.CrossSafe: + blockID = supervisorChainSyncStatus.CrossSafe + case types.LocalSafe: + blockID = supervisorChainSyncStatus.LocalSafe + case types.CrossUnsafe: + blockID = supervisorChainSyncStatus.CrossUnsafe + case types.LocalUnsafe: + blockID = supervisorChainSyncStatus.LocalUnsafe.ID() + default: + s.require.NoError(errors.New("invalid safety level")) + } + return blockID } -func (s *Supervisor) AdvanceUnsafeHead(chainID eth.ChainID, block uint64) { - initial := s.FetchSyncStatus() - chInitial, ok := initial.Chains[chainID] - s.require.True(ok, fmt.Sprintf("chain sync status not found: chain id: %d", chainID)) - required := chInitial.LocalUnsafe.Number + block - attempts := int(block + 3) // intentionally allow few more attempts for avoid flaking +// AdvancedL2Head checks the supervisor view of L2CL chain head with given safety level advanced more than delta block number +func (s *Supervisor) AdvancedL2Head(chainID eth.ChainID, delta uint64, lvl types.SafetyLevel, attempts int) { + chInitial := s.L2HeadBlockID(chainID, lvl) + target := chInitial.Number + delta err := retry.Do0(s.ctx, attempts, &retry.FixedStrategy{Dur: 2 * time.Second}, func() error { - chStatus := s.FetchSyncStatus().Chains[chainID] - s.log.Info("Supervisor view of unsafe head", "chain", chainID, "unsafe", chStatus.LocalUnsafe) - if chStatus.LocalUnsafe.Number < required { - s.log.Info("Unsafe head sync status not ready", - "chain", chainID, "initialUnsafe", chInitial.LocalUnsafe, "currentUnsafe", chStatus.LocalUnsafe, "minRequired", required) - return fmt.Errorf("expected head to advance") + chStatus := s.L2HeadBlockID(chainID, lvl) + s.log.Info("Supervisor view", + "chain", chainID, "label", lvl, "initial", chInitial.Number, "current", chStatus.Number, "target", target) + if chStatus.Number >= target { + s.log.Info("Supervisor view advanced", "chain", chainID, "label", lvl, "target", target) + return nil } - return nil + return fmt.Errorf("expected head to advance: %s", lvl) }) s.require.NoError(err) } + +func (s *Supervisor) AdvancedUnsafeHead(chainID eth.ChainID, block uint64) { + attempts := int(block + 3) // intentionally allow few more attempts for avoid flaking + s.AdvancedL2Head(chainID, block, types.LocalUnsafe, attempts) +} + +func (s *Supervisor) AdvancedSafeHead(chainID eth.ChainID, block uint64, attempts int) { + s.AdvancedL2Head(chainID, block, types.CrossSafe, attempts) +} diff --git a/op-devstack/presets/interop.go b/op-devstack/presets/interop.go index cce6601b907f8..36390d9ee0b69 100644 --- a/op-devstack/presets/interop.go +++ b/op-devstack/presets/interop.go @@ -80,8 +80,8 @@ func NewSimpleInterop(t devtest.T) *SimpleInterop { L2ChainB: dsl.NewL2Network(l2B), L2ELA: dsl.NewL2ELNode(l2A.L2ELNode(match.Assume(t, match.FirstL2EL))), L2ELB: dsl.NewL2ELNode(l2B.L2ELNode(match.Assume(t, match.FirstL2EL))), - L2CLA: dsl.NewL2CLNode(l2A.L2CLNode(match.Assume(t, match.FirstL2CL)), orch.ControlPlane()), - L2CLB: dsl.NewL2CLNode(l2B.L2CLNode(match.Assume(t, match.FirstL2CL)), orch.ControlPlane()), + L2CLA: dsl.NewL2CLNode(l2A.L2CLNode(match.Assume(t, match.FirstL2CL)), orch.ControlPlane(), l2A.ChainID()), + L2CLB: dsl.NewL2CLNode(l2B.L2CLNode(match.Assume(t, match.FirstL2CL)), orch.ControlPlane(), l2B.ChainID()), Wallet: dsl.NewHDWallet(t, devkeys.TestMnemonic, 30), FaucetA: dsl.NewFaucet(l2A.Faucet(match.Assume(t, match.FirstFaucet))), FaucetB: dsl.NewFaucet(l2B.Faucet(match.Assume(t, match.FirstFaucet))), @@ -116,3 +116,26 @@ func WithInteropNotAtGenesis() stack.CommonOption { } }) } + +type RedundantInterop struct { + SimpleInterop + + L2ELA2 *dsl.L2ELNode + L2CLA2 *dsl.L2CLNode +} + +func WithRedundantInterop() stack.CommonOption { + return stack.MakeCommon(sysgo.RedundantInteropSystem(&sysgo.RedundantInteropSystemIDs{})) +} + +func NewRedundantInterop(t devtest.T) *RedundantInterop { + simpleInterop := NewSimpleInterop(t) + orch := Orchestrator() + l2A := simpleInterop.L2ChainA.Escape() + out := &RedundantInterop{ + SimpleInterop: *simpleInterop, + L2ELA2: dsl.NewL2ELNode(l2A.L2ELNode(match.Assume(t, match.SecondL2EL))), + L2CLA2: dsl.NewL2CLNode(l2A.L2CLNode(match.Assume(t, match.SecondL2CL)), orch.ControlPlane(), l2A.ChainID()), + } + return out +} diff --git a/op-devstack/stack/match/second.go b/op-devstack/stack/match/second.go new file mode 100644 index 0000000000000..1fa5e81ec6548 --- /dev/null +++ b/op-devstack/stack/match/second.go @@ -0,0 +1,6 @@ +package match + +import "github.com/ethereum-optimism/optimism/op-devstack/stack" + +var SecondL2EL = Second[stack.L2ELNodeID, stack.L2ELNode]() +var SecondL2CL = Second[stack.L2CLNodeID, stack.L2CLNode]() diff --git a/op-devstack/sysgo/contracts.go b/op-devstack/sysgo/contracts.go index 61fc3b5f7a83d..22574f3aad88b 100644 --- a/op-devstack/sysgo/contracts.go +++ b/op-devstack/sysgo/contracts.go @@ -33,9 +33,9 @@ func ensureDir(dirPath string) error { // Different tests might be nested in subdirectories of the op-e2e dir. func findMonorepoRoot(testPath string) (string, error) { path := "./" - // Only search up 5 directories + // Only search up 6 directories // Avoids infinite recursion if the root isn't found for some reason - for i := 0; i < 5; i++ { + for i := 0; i < 6; i++ { _, err := os.Stat(path + testPath) if errors.Is(err, os.ErrNotExist) { path = path + "../" diff --git a/op-devstack/sysgo/sync_test.go b/op-devstack/sysgo/sync_test.go index 04168ba0ce2af..d92b82f6d2f5e 100644 --- a/op-devstack/sysgo/sync_test.go +++ b/op-devstack/sysgo/sync_test.go @@ -19,8 +19,8 @@ import ( // TestL2CLSyncP2P checks that unsafe head is propagated from sequencer to verifier. // Tests started/restarted L2CL advances unsafe head via P2P connection. func TestL2CLSyncP2P(gt *testing.T) { - var ids DefaultRedundancyInteropSystemIDs - opt := DefaultRedundancyInteropSystem(&ids) + var ids RedundantInteropSystemIDs + opt := RedundantInteropSystem(&ids) logger := testlog.Logger(gt, log.LevelInfo) @@ -135,8 +135,8 @@ func TestL2CLSyncP2P(gt *testing.T) { // - The verifier will quickly catch up with the sequencer unsafe head as well as the supervisor. // - The verifier will process previously unknown unsafe blocks and advance its unsafe head. func TestUnsafeChainUnknownToL2CL(gt *testing.T) { - var ids DefaultRedundancyInteropSystemIDs - opt := DefaultRedundancyInteropSystem(&ids) + var ids RedundantInteropSystemIDs + opt := RedundantInteropSystem(&ids) logger := testlog.Logger(gt, log.LevelInfo) diff --git a/op-devstack/sysgo/system.go b/op-devstack/sysgo/system.go index e5ff67dffad76..6c34ec778a559 100644 --- a/op-devstack/sysgo/system.go +++ b/op-devstack/sysgo/system.go @@ -116,18 +116,18 @@ func DefaultInteropSystem(dest *DefaultInteropSystemIDs) stack.Option[*Orchestra return opt } -type DefaultRedundancyInteropSystemIDs struct { +type RedundantInteropSystemIDs struct { DefaultInteropSystemIDs L2A2CL stack.L2CLNodeID L2A2EL stack.L2ELNodeID } -func DefaultRedundancyInteropSystem(dest *DefaultRedundancyInteropSystemIDs) stack.Option[*Orchestrator] { +func RedundantInteropSystem(dest *RedundantInteropSystemIDs) stack.Option[*Orchestrator] { l1ID := eth.ChainIDFromUInt64(900) l2AID := eth.ChainIDFromUInt64(901) l2BID := eth.ChainIDFromUInt64(902) - ids := DefaultRedundancyInteropSystemIDs{ + ids := RedundantInteropSystemIDs{ DefaultInteropSystemIDs: NewDefaultInteropSystemIDs(l1ID, l2AID, l2BID), L2A2CL: stack.L2CLNodeID{Key: "verifier", ChainID: l2AID}, L2A2EL: stack.L2ELNodeID{Key: "verifier", ChainID: l2AID}, diff --git a/op-supervisor/supervisor/types/types.go b/op-supervisor/supervisor/types/types.go index 930ea5138de2c..3cc5072eb09ea 100644 --- a/op-supervisor/supervisor/types/types.go +++ b/op-supervisor/supervisor/types/types.go @@ -8,9 +8,9 @@ import ( "math" "strconv" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/holiman/uint256" - "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethTypes "github.com/ethereum/go-ethereum/core/types"