diff --git a/op-challenger/game/fault/trace/super/provider_supernode.go b/op-challenger/game/fault/trace/super/provider_supernode.go new file mode 100644 index 0000000000000..f02844ccd36de --- /dev/null +++ b/op-challenger/game/fault/trace/super/provider_supernode.go @@ -0,0 +1,169 @@ +package super + +import ( + "context" + "fmt" + + "github.com/ethereum-optimism/optimism/op-challenger/game/client" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + interopTypes "github.com/ethereum-optimism/optimism/op-program/client/interop/types" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supernode/supernode/activity/superroot" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" +) + +type SuperNodeRootProvider interface { + // TODO: If AtTimestampResponse is being reused it should be put in op-service + SuperRootAtTimestamp(ctx context.Context, timestamp uint64) (superroot.AtTimestampResponse, error) +} + +type SuperNodeTraceProvider struct { + PreimagePrestateProvider + logger log.Logger + rootProvider SuperNodeRootProvider + prestateTimestamp uint64 + poststateTimestamp uint64 + l1Head eth.BlockID + gameDepth types.Depth +} + +func NewSuperNodeTraceProvider(logger log.Logger, prestateProvider PreimagePrestateProvider, rootProvider SuperNodeRootProvider, l1Head eth.BlockID, gameDepth types.Depth, prestateTimestamp, poststateTimestamp uint64) *SuperNodeTraceProvider { + return &SuperNodeTraceProvider{ + logger: logger, + PreimagePrestateProvider: prestateProvider, + rootProvider: rootProvider, + prestateTimestamp: prestateTimestamp, + poststateTimestamp: poststateTimestamp, + l1Head: l1Head, + gameDepth: gameDepth, + } +} + +func (s *SuperNodeTraceProvider) Get(ctx context.Context, pos types.Position) (common.Hash, error) { + preimage, err := s.GetPreimageBytes(ctx, pos) + if err != nil { + return common.Hash{}, err + } + return crypto.Keccak256Hash(preimage), nil +} + +func (s *SuperNodeTraceProvider) getPreimageBytesAtTimestampBoundary(ctx context.Context, timestamp uint64) ([]byte, error) { + root, err := s.rootProvider.SuperRootAtTimestamp(ctx, timestamp) + if err != nil { + return nil, fmt.Errorf("failed to retrieve super root at timestamp %v: %w", timestamp, err) + } + if root.CurrentL1.Number < s.l1Head.Number { + // Node has not processed the game's L1 head so it is not safe to play until it syncs further. + return nil, client.ErrNotInSync + } + if root.Data == nil { + // No block at this timestamp so it must be invalid + return InvalidTransition, nil + } + if root.Data.VerifiedRequiredL1.Number > s.l1Head.Number { + return InvalidTransition, nil + } + return root.Data.Super.Marshal(), nil +} + +func (s *SuperNodeTraceProvider) GetPreimageBytes(ctx context.Context, pos types.Position) ([]byte, error) { + // Find the timestamp and step at position + timestamp, step, err := s.ComputeStep(pos) + if err != nil { + return nil, err + } + s.logger.Trace("Getting claim", "pos", pos.ToGIndex(), "timestamp", timestamp, "step", step) + if step == 0 { + return s.getPreimageBytesAtTimestampBoundary(ctx, timestamp) + } + // Fetch the super root at the next timestamp since we are part way through the transition to it + prevRoot, err := s.rootProvider.SuperRootAtTimestamp(ctx, timestamp) + if err != nil { + return nil, fmt.Errorf("failed to retrieve previous super root at timestamp %v: %w", timestamp, err) + } + if prevRoot.CurrentL1.Number < s.l1Head.Number { + return nil, client.ErrNotInSync + } + if prevRoot.Data == nil { + // No block at this timestamp so it must be invalid + return InvalidTransition, nil + } + if prevRoot.Data.VerifiedRequiredL1.Number > s.l1Head.Number { + // The previous root was not safe at the game L1 head so we must have already transitioned to the invalid hash + // prior to this step and it then repeats forever. + return InvalidTransition, nil + } + nextTimestamp := timestamp + 1 + nextRoot, err := s.rootProvider.SuperRootAtTimestamp(ctx, nextTimestamp) + if err != nil { + return nil, fmt.Errorf("failed to retrieve next super root at timestamp %v: %w", nextTimestamp, err) + } + if nextRoot.CurrentL1.Number < s.l1Head.Number { + return nil, client.ErrNotInSync + } + if nextRoot.Data == nil { + // No block at this timestamp so it must be invalid + return InvalidTransition, nil + } + + prevSuper := prevRoot.Data.Super + expectedState := interopTypes.TransitionState{ + SuperRoot: prevSuper.Marshal(), + PendingProgress: make([]interopTypes.OptimisticBlock, 0, step), + Step: step, + } + nextSuperV1, ok := nextRoot.Data.Super.(*eth.SuperV1) + if !ok { + return nil, fmt.Errorf("unsupported super root type %T", nextRoot.Data.Super) + } + for i := uint64(0); i < min(step, uint64(len(nextSuperV1.Chains))); i++ { + chainInfo := nextSuperV1.Chains[i] + // Check if the chain's optimistic root was safe at the game's L1 head + optimistic, ok := nextRoot.Data.UnverifiedAtTimestamp[chainInfo.ChainID] + if !ok { + return nil, fmt.Errorf("no safe head known for chain %v at %v: %w", chainInfo.ChainID, nextTimestamp, err) + } + if optimistic.RequiredL1.Number > s.l1Head.Number { + // Not enough data on L1 to derive the optimistic block, move to invalid transition. + return InvalidTransition, nil + } + + expectedState.PendingProgress = append(expectedState.PendingProgress, interopTypes.OptimisticBlock{ + BlockHash: optimistic.Output.BlockRef.Hash, + OutputRoot: optimistic.Output.OutputRoot, + }) + } + return expectedState.Marshal(), nil +} + +func (s *SuperNodeTraceProvider) ComputeStep(pos types.Position) (timestamp uint64, step uint64, err error) { + bigIdx := pos.TraceIndex(s.gameDepth) + if !bigIdx.IsUint64() { + err = fmt.Errorf("%w: %v", ErrIndexTooBig, bigIdx) + return + } + + traceIdx := bigIdx.Uint64() + 1 + timestampIncrements := traceIdx / StepsPerTimestamp + timestamp = s.prestateTimestamp + timestampIncrements + if timestamp >= s.poststateTimestamp { // Apply trace extension once the claimed timestamp is reached + timestamp = s.poststateTimestamp + step = 0 + } else { + step = traceIdx % StepsPerTimestamp + } + return +} + +func (s *SuperNodeTraceProvider) GetStepData(_ context.Context, _ types.Position) (prestate []byte, proofData []byte, preimageData *types.PreimageOracleData, err error) { + return nil, nil, nil, ErrGetStepData +} + +func (s *SuperNodeTraceProvider) GetL2BlockNumberChallenge(_ context.Context) (*types.InvalidL2BlockNumberChallenge, error) { + // Never need to challenge L2 block number for super root games. + return nil, types.ErrL2BlockNumberValid +} + +var _ types.TraceProvider = (*SuperNodeTraceProvider)(nil) diff --git a/op-challenger/game/fault/trace/super/provider_supernode_test.go b/op-challenger/game/fault/trace/super/provider_supernode_test.go new file mode 100644 index 0000000000000..af4c96e320a00 --- /dev/null +++ b/op-challenger/game/fault/trace/super/provider_supernode_test.go @@ -0,0 +1,496 @@ +package super + +import ( + "context" + "fmt" + "math/big" + "math/rand" + "testing" + + "github.com/ethereum-optimism/optimism/op-challenger/game/client" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + interopTypes "github.com/ethereum-optimism/optimism/op-program/client/interop/types" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/testutils" + "github.com/ethereum-optimism/optimism/op-supernode/supernode/activity/superroot" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +func TestSuperNodeProvider_Get(t *testing.T) { + t.Run("AtPostState", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + expectedSuper := eth.NewSuperV1(poststateTimestamp, eth.ChainIDAndOutput{ + ChainID: eth.ChainIDFromUInt64(1), + Output: eth.Bytes32{0xbb}, + }) + response := superroot.AtTimestampResponse{ + CurrentL1: l1Head, + Data: &superroot.SuperRootResponseData{ + VerifiedRequiredL1: l1Head, + Super: expectedSuper, + SuperRoot: eth.SuperRoot(expectedSuper), + }, + } + stubSupervisor.Add(response) + claim, err := provider.Get(context.Background(), types.RootPosition) + require.NoError(t, err) + require.Equal(t, common.Hash(eth.SuperRoot(expectedSuper)), claim) + }) + + t.Run("AtNewTimestamp", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + expectedSuper := eth.NewSuperV1(prestateTimestamp+1, eth.ChainIDAndOutput{ + ChainID: eth.ChainIDFromUInt64(1), + Output: eth.Bytes32{0xbb}, + }) + response := superroot.AtTimestampResponse{ + CurrentL1: l1Head, + Data: &superroot.SuperRootResponseData{ + VerifiedRequiredL1: l1Head, + Super: expectedSuper, + SuperRoot: eth.SuperRoot(expectedSuper), + }, + } + stubSupervisor.Add(response) + claim, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(StepsPerTimestamp-1))) + require.NoError(t, err) + require.Equal(t, common.Hash(eth.SuperRoot(expectedSuper)), claim) + }) + + t.Run("ValidTransitionBetweenFirstTwoSuperRoots", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + prev, next := createValidSuperNodeSuperRoots(l1Head) + stubSupervisor.Add(prev) + stubSupervisor.Add(next) + + expectSuperNodeValidTransition(t, provider, prev, next) + }) + + t.Run("Step0SuperRootIsSafeBeforeGameL1Head", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + expectedSuper := eth.NewSuperV1(poststateTimestamp, eth.ChainIDAndOutput{ + ChainID: eth.ChainIDFromUInt64(1), + Output: eth.Bytes32{0xbb}, + }) + response := superroot.AtTimestampResponse{ + CurrentL1: l1Head, + Data: &superroot.SuperRootResponseData{ + VerifiedRequiredL1: eth.BlockID{Number: l1Head.Number - 10, Hash: common.Hash{0xcc}}, + Super: expectedSuper, + SuperRoot: eth.SuperRoot(expectedSuper), + }, + } + stubSupervisor.Add(response) + claim, err := provider.Get(context.Background(), types.RootPosition) + require.NoError(t, err) + require.Equal(t, common.Hash(eth.SuperRoot(expectedSuper)), claim) + }) + + t.Run("Step0SuperRootNotSafeAtGameL1Head", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + expectedSuper := eth.NewSuperV1(poststateTimestamp, eth.ChainIDAndOutput{ + ChainID: eth.ChainIDFromUInt64(1), + Output: eth.Bytes32{0xbb}, + }) + response := superroot.AtTimestampResponse{ + CurrentL1: l1Head, + Data: &superroot.SuperRootResponseData{ + VerifiedRequiredL1: eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xcc}}, + Super: expectedSuper, + SuperRoot: eth.SuperRoot(expectedSuper), + }, + } + stubSupervisor.Add(response) + claim, err := provider.Get(context.Background(), types.RootPosition) + require.NoError(t, err) + require.Equal(t, InvalidTransitionHash, claim) + }) + + t.Run("NextSuperRootSafeBeforeGameL1Head", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + prev, next := createValidSuperNodeSuperRoots(l1Head) + // Make super roots be safe earlier + prev.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number - 10, Hash: common.Hash{0xaa}} + next.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number - 5, Hash: common.Hash{0xbb}} + stubSupervisor.Add(prev) + stubSupervisor.Add(next) + expectSuperNodeValidTransition(t, provider, prev, next) + }) + + t.Run("PreviousSuperRootNotSafeAtGameL1Head", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + prev, next := createValidSuperNodeSuperRoots(l1Head) + // Make super roots be safe only after L1 head + prev.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xaa}} + next.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number + 2, Hash: common.Hash{0xbb}} + stubSupervisor.Add(prev) + stubSupervisor.Add(next) + + // All steps should be the invalid transition hash. + for i := int64(0); i < StepsPerTimestamp+1; i++ { + claim, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(i))) + require.NoError(t, err) + require.Equalf(t, InvalidTransitionHash, claim, "incorrect claim at index %d", i) + } + }) + + t.Run("FirstChainUnsafe", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + prev, next := createValidSuperNodeSuperRoots(l1Head) + // Make super roots be safe only after L1 head + prev.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number, Hash: common.Hash{0xaa}} + next.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xbb}} + next.Data.UnverifiedAtTimestamp[eth.ChainIDFromUInt64(1)] = superroot.OutputWithRequiredL1{ + Output: ð.OutputResponse{ + OutputRoot: eth.Bytes32{0xad}, + BlockRef: eth.L2BlockRef{Hash: common.Hash{0xcd}}, + WithdrawalStorageRoot: common.Hash{0xde}, + StateRoot: common.Hash{0xdf}, + }, + RequiredL1: eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xbb}}, + } + stubSupervisor.Add(prev) + stubSupervisor.Add(next) + + // All steps should be the invalid transition hash. + for i := int64(0); i < StepsPerTimestamp+1; i++ { + claim, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(i))) + require.NoError(t, err) + require.Equalf(t, InvalidTransitionHash, claim, "incorrect claim at index %d", i) + } + }) + + t.Run("SecondChainUnsafe", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + prev, next := createValidSuperNodeSuperRoots(l1Head) + // Make super roots be safe only after L1 head + prev.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number, Hash: common.Hash{0xaa}} + next.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xbb}} + next.Data.UnverifiedAtTimestamp[eth.ChainIDFromUInt64(2)] = superroot.OutputWithRequiredL1{ + Output: ð.OutputResponse{ + OutputRoot: eth.Bytes32{0xad}, + BlockRef: eth.L2BlockRef{Hash: common.Hash{0xcd}}, + WithdrawalStorageRoot: common.Hash{0xde}, + StateRoot: common.Hash{0xdf}, + }, + RequiredL1: eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xbb}}, + } + stubSupervisor.Add(prev) + stubSupervisor.Add(next) + + // First step should be valid because we can reach the required block on chain 1 + claim, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(0))) + require.NoError(t, err) + require.NotEqual(t, InvalidTransitionHash, claim, "incorrect claim at index 0") + + // Remaining steps should be the invalid transition hash. + for i := int64(1); i < StepsPerTimestamp+1; i++ { + claim, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(i))) + require.NoError(t, err) + require.Equalf(t, InvalidTransitionHash, claim, "incorrect claim at index %d", i) + } + }) + + t.Run("Step0ForTimestampBeyondChainHead", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + stubSupervisor.AddAtTimestamp(poststateTimestamp, superroot.AtTimestampResponse{ + CurrentL1: l1Head, + Data: nil, + }) + + claim, err := provider.Get(context.Background(), types.RootPosition) + require.NoError(t, err) + require.Equal(t, InvalidTransitionHash, claim) + }) + + t.Run("NextSuperRootTimestampBeyondChainHead", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + prev, _ := createValidSuperNodeSuperRoots(l1Head) + stubSupervisor.Add(prev) + stubSupervisor.AddAtTimestamp(prestateTimestamp+1, superroot.AtTimestampResponse{ + CurrentL1: l1Head, + Data: nil, + }) + // All steps should be the invalid transition hash. + for i := int64(0); i < StepsPerTimestamp+1; i++ { + claim, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(i))) + require.NoError(t, err) + require.Equalf(t, InvalidTransitionHash, claim, "incorrect claim at index %d", i) + } + }) + + t.Run("PreviousSuperRootTimestampBeyondChainHead", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + stubSupervisor.AddAtTimestamp(prestateTimestamp, superroot.AtTimestampResponse{ + CurrentL1: l1Head, + Data: nil, + }) + stubSupervisor.AddAtTimestamp(prestateTimestamp+1, superroot.AtTimestampResponse{ + CurrentL1: l1Head, + Data: nil, + }) + + // All steps should be the invalid transition hash. + for i := int64(0); i < StepsPerTimestamp+1; i++ { + claim, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(i))) + require.NoError(t, err) + require.Equalf(t, InvalidTransitionHash, claim, "incorrect claim at index %d", i) + } + }) + + t.Run("Step0NotInSync", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + expectedSuper := eth.NewSuperV1(poststateTimestamp, eth.ChainIDAndOutput{ + ChainID: eth.ChainIDFromUInt64(1), + Output: eth.Bytes32{0xbb}, + }) + response := superroot.AtTimestampResponse{ + CurrentL1: eth.BlockID{Number: l1Head.Number - 1, Hash: common.Hash{0xaa}}, + Data: &superroot.SuperRootResponseData{ + VerifiedRequiredL1: eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xcc}}, + Super: expectedSuper, + SuperRoot: eth.SuperRoot(expectedSuper), + }, + } + stubSupervisor.Add(response) + _, err := provider.Get(context.Background(), types.RootPosition) + require.ErrorIs(t, err, client.ErrNotInSync) + }) + + t.Run("PreviousSuperRootNotInSync", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + stubSupervisor.AddAtTimestamp(prestateTimestamp, superroot.AtTimestampResponse{ + CurrentL1: eth.BlockID{Number: l1Head.Number - 1, Hash: common.Hash{0xaa}}, + }) + _, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(1))) + require.ErrorIs(t, err, client.ErrNotInSync) + }) + + t.Run("NextSuperRootNotInSync", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + prev, _ := createValidSuperNodeSuperRoots(l1Head) + // Previous gives an in sync response + stubSupervisor.Add(prev) + // But next gives an out of sync response + stubSupervisor.AddAtTimestamp(prestateTimestamp+1, superroot.AtTimestampResponse{ + CurrentL1: eth.BlockID{Number: l1Head.Number - 1, Hash: common.Hash{0xaa}}, + }) + _, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(1))) + require.ErrorIs(t, err, client.ErrNotInSync) + }) +} + +func TestSuperNodeProvider_ComputeStep(t *testing.T) { + t.Run("ErrorWhenTraceIndexTooBig", func(t *testing.T) { + // Uses a big game depth so the trace index doesn't fit in uint64 + provider := NewSuperNodeTraceProvider(testlog.Logger(t, log.LvlInfo), nil, &stubSuperNodeRootProvider{}, eth.BlockID{}, 65, prestateTimestamp, poststateTimestamp) + // Left-most position in top game + _, _, err := provider.ComputeStep(types.RootPosition) + require.ErrorIs(t, err, ErrIndexTooBig) + }) + + t.Run("FirstTimestampSteps", func(t *testing.T) { + provider, _, _ := createSuperNodeProvider(t) + for i := int64(0); i < StepsPerTimestamp-1; i++ { + timestamp, step, err := provider.ComputeStep(types.NewPosition(gameDepth, big.NewInt(i))) + require.NoError(t, err) + // The prestate must be a super root and is on the timestamp boundary. + // So the first step has the same timestamp and increments step from 0 to 1. + require.Equalf(t, prestateTimestamp, timestamp, "Incorrect timestamp at trace index %d", i) + require.Equalf(t, uint64(i+1), step, "Incorrect step at trace index %d", i) + } + }) + + t.Run("SecondTimestampSteps", func(t *testing.T) { + provider, _, _ := createSuperNodeProvider(t) + for i := int64(-1); i < StepsPerTimestamp-1; i++ { + traceIndex := StepsPerTimestamp + i + timestamp, step, err := provider.ComputeStep(types.NewPosition(gameDepth, big.NewInt(traceIndex))) + require.NoError(t, err) + // We should now be iterating through the steps of the second timestamp - 1s after the prestate + require.Equalf(t, prestateTimestamp+1, timestamp, "Incorrect timestamp at trace index %d", traceIndex) + require.Equalf(t, uint64(i+1), step, "Incorrect step at trace index %d", traceIndex) + } + }) + + t.Run("LimitToPoststateTimestamp", func(t *testing.T) { + provider, _, _ := createSuperNodeProvider(t) + timestamp, step, err := provider.ComputeStep(types.RootPosition) + require.NoError(t, err) + require.Equal(t, poststateTimestamp, timestamp, "Incorrect timestamp at root position") + require.Equal(t, uint64(0), step, "Incorrect step at trace index at root position") + }) + + t.Run("StepShouldLoopBackToZero", func(t *testing.T) { + provider, _, _ := createSuperNodeProvider(t) + prevTimestamp := prestateTimestamp + prevStep := uint64(0) // Absolute prestate is always on a timestamp boundary, so step 0 + for traceIndex := int64(0); traceIndex < 5*StepsPerTimestamp; traceIndex++ { + timestamp, step, err := provider.ComputeStep(types.NewPosition(gameDepth, big.NewInt(traceIndex))) + require.NoError(t, err) + if timestamp == prevTimestamp { + require.Equal(t, prevStep+1, step, "Incorrect step at trace index %d", traceIndex) + } else { + require.Equal(t, prevTimestamp+1, timestamp, "Incorrect timestamp at trace index %d", traceIndex) + require.Zero(t, step, "Incorrect step at trace index %d", traceIndex) + require.Equal(t, uint64(StepsPerTimestamp-1), prevStep, "Should only loop back to step 0 after the consolidation step") + } + prevTimestamp = timestamp + prevStep = step + } + }) +} + +func TestSuperNodeProvider_GetStepDataReturnsError(t *testing.T) { + provider, _, _ := createSuperNodeProvider(t) + _, _, _, err := provider.GetStepData(context.Background(), types.RootPosition) + require.ErrorIs(t, err, ErrGetStepData) +} + +func TestSuperNodeProvider_GetL2BlockNumberChallengeReturnsError(t *testing.T) { + provider, _, _ := createSuperNodeProvider(t) + _, err := provider.GetL2BlockNumberChallenge(context.Background()) + require.ErrorIs(t, err, types.ErrL2BlockNumberValid) +} + +func createSuperNodeProvider(t *testing.T) (*SuperNodeTraceProvider, *stubSuperNodeRootProvider, eth.BlockID) { + logger := testlog.Logger(t, log.LvlInfo) + l1Head := eth.BlockID{Number: 23542, Hash: common.Hash{0xab, 0xcd}} + stubSupervisor := &stubSuperNodeRootProvider{ + rootsByTimestamp: make(map[uint64]superroot.AtTimestampResponse), + } + provider := NewSuperNodeTraceProvider(logger, nil, stubSupervisor, l1Head, gameDepth, prestateTimestamp, poststateTimestamp) + return provider, stubSupervisor, l1Head +} + +func toOutputResponse(output *eth.OutputV0) *eth.OutputResponse { + return ð.OutputResponse{ + Version: output.Version(), + OutputRoot: eth.OutputRoot(output), + BlockRef: eth.L2BlockRef{ + Hash: output.BlockHash, + }, + WithdrawalStorageRoot: common.Hash(output.MessagePasserStorageRoot), + StateRoot: common.Hash(output.StateRoot), + } +} + +func createValidSuperNodeSuperRoots(l1Head eth.BlockID) (superroot.AtTimestampResponse, superroot.AtTimestampResponse) { + rng := rand.New(rand.NewSource(1)) + outputA1 := testutils.RandomOutputV0(rng) + outputA2 := testutils.RandomOutputV0(rng) + outputB1 := testutils.RandomOutputV0(rng) + outputB2 := testutils.RandomOutputV0(rng) + prevSuper := eth.NewSuperV1( + prestateTimestamp, + eth.ChainIDAndOutput{ChainID: eth.ChainIDFromUInt64(1), Output: eth.OutputRoot(outputA1)}, + eth.ChainIDAndOutput{ChainID: eth.ChainIDFromUInt64(2), Output: eth.OutputRoot(outputB1)}) + nextSuper := eth.NewSuperV1(prestateTimestamp+1, + eth.ChainIDAndOutput{ChainID: eth.ChainIDFromUInt64(1), Output: eth.OutputRoot(outputA2)}, + eth.ChainIDAndOutput{ChainID: eth.ChainIDFromUInt64(2), Output: eth.OutputRoot(outputB2)}) + + prevResponse := superroot.AtTimestampResponse{ + CurrentL1: l1Head, + Data: &superroot.SuperRootResponseData{ + UnverifiedAtTimestamp: map[eth.ChainID]superroot.OutputWithRequiredL1{ + eth.ChainIDFromUInt64(1): { + Output: toOutputResponse(outputA1), + RequiredL1: l1Head, + }, + eth.ChainIDFromUInt64(2): { + Output: toOutputResponse(outputB1), + RequiredL1: l1Head, + }, + }, + VerifiedRequiredL1: l1Head, + Super: prevSuper, + SuperRoot: eth.SuperRoot(prevSuper), + }, + } + nextResponse := superroot.AtTimestampResponse{ + CurrentL1: l1Head, + Data: &superroot.SuperRootResponseData{ + UnverifiedAtTimestamp: map[eth.ChainID]superroot.OutputWithRequiredL1{ + eth.ChainIDFromUInt64(1): { + Output: toOutputResponse(outputA2), + RequiredL1: l1Head, + }, + eth.ChainIDFromUInt64(2): { + Output: toOutputResponse(outputB2), + RequiredL1: l1Head, + }, + }, + VerifiedRequiredL1: l1Head, + Super: nextSuper, + SuperRoot: eth.SuperRoot(nextSuper), + }, + } + return prevResponse, nextResponse +} + +func expectSuperNodeValidTransition(t *testing.T, provider *SuperNodeTraceProvider, prev superroot.AtTimestampResponse, next superroot.AtTimestampResponse) { + chain1OptimisticBlock := interopTypes.OptimisticBlock{ + BlockHash: next.Data.UnverifiedAtTimestamp[eth.ChainIDFromUInt64(1)].Output.BlockRef.Hash, + OutputRoot: next.Data.UnverifiedAtTimestamp[eth.ChainIDFromUInt64(1)].Output.OutputRoot, + } + chain2OptimisticBlock := interopTypes.OptimisticBlock{ + BlockHash: next.Data.UnverifiedAtTimestamp[eth.ChainIDFromUInt64(2)].Output.BlockRef.Hash, + OutputRoot: next.Data.UnverifiedAtTimestamp[eth.ChainIDFromUInt64(2)].Output.OutputRoot, + } + expectedFirstStep := &interopTypes.TransitionState{ + SuperRoot: prev.Data.Super.Marshal(), + PendingProgress: []interopTypes.OptimisticBlock{chain1OptimisticBlock}, + Step: 1, + } + claim, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(0))) + require.NoError(t, err) + require.Equal(t, expectedFirstStep.Hash(), claim) + + expectedSecondStep := &interopTypes.TransitionState{ + SuperRoot: prev.Data.Super.Marshal(), + PendingProgress: []interopTypes.OptimisticBlock{chain1OptimisticBlock, chain2OptimisticBlock}, + Step: 2, + } + claim, err = provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(1))) + require.NoError(t, err) + require.Equal(t, expectedSecondStep.Hash(), claim) + + for step := uint64(3); step < StepsPerTimestamp; step++ { + expectedPaddingStep := &interopTypes.TransitionState{ + SuperRoot: prev.Data.Super.Marshal(), + PendingProgress: []interopTypes.OptimisticBlock{chain1OptimisticBlock, chain2OptimisticBlock}, + Step: step, + } + claim, err = provider.Get(context.Background(), types.NewPosition(gameDepth, new(big.Int).SetUint64(step-1))) + require.NoError(t, err) + require.Equalf(t, expectedPaddingStep.Hash(), claim, "incorrect hash at step %v", step) + } +} + +type stubSuperNodeRootProvider struct { + rootsByTimestamp map[uint64]superroot.AtTimestampResponse +} + +func (s *stubSuperNodeRootProvider) Add(root superroot.AtTimestampResponse) { + superV1 := root.Data.Super.(*eth.SuperV1) + s.AddAtTimestamp(superV1.Timestamp, root) +} + +func (s *stubSuperNodeRootProvider) AddAtTimestamp(timestamp uint64, root superroot.AtTimestampResponse) { + if s.rootsByTimestamp == nil { + s.rootsByTimestamp = make(map[uint64]superroot.AtTimestampResponse) + } + s.rootsByTimestamp[timestamp] = root +} + +func (s *stubSuperNodeRootProvider) SuperRootAtTimestamp(_ context.Context, timestamp uint64) (superroot.AtTimestampResponse, error) { + root, ok := s.rootsByTimestamp[timestamp] + if !ok { + // This is not the not found response - the test just didn't configure a response, so return a generic error + return superroot.AtTimestampResponse{}, fmt.Errorf("wowsers, now response for timestamp %v", timestamp) + } + return root, nil +} diff --git a/op-challenger/game/generic/player.go b/op-challenger/game/generic/player.go index 5545a51b7095f..6e3d0155330da 100644 --- a/op-challenger/game/generic/player.go +++ b/op-challenger/game/generic/player.go @@ -135,7 +135,10 @@ func (g *GamePlayer) ProgressGame(ctx context.Context) gameTypes.GameStatus { return g.status } g.logger.Trace("Checking if actions are required") - if err := g.actor.Act(ctx); err != nil { + if err := g.actor.Act(ctx); errors.Is(err, client.ErrNotInSync) { + g.logger.Warn("Local node not sufficiently up to date to act on game", "err", err) + return g.status + } else if err != nil { g.logger.Error("Error when acting on game", "err", err) } status, err := g.loader.GetStatus(ctx) diff --git a/op-service/eth/super_root.go b/op-service/eth/super_root.go index 1b9601ccbd1f0..8334a37b310a6 100644 --- a/op-service/eth/super_root.go +++ b/op-service/eth/super_root.go @@ -78,6 +78,28 @@ func (o *SuperV1) Marshal() []byte { return buf } +func (o *SuperV1) MarshalJSON() ([]byte, error) { + return json.Marshal(&superV1JsonMarshalling{ + Timestamp: hexutil.Uint64(o.Timestamp), + Chains: o.Chains, + }) +} + +func (o *SuperV1) UnmarshalJSON(input []byte) error { + var dec superV1JsonMarshalling + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + o.Timestamp = uint64(dec.Timestamp) + o.Chains = dec.Chains + return nil +} + +type superV1JsonMarshalling struct { + Timestamp hexutil.Uint64 `json:"timestamp"` + Chains []ChainIDAndOutput `json:"chains"` +} + func UnmarshalSuperRoot(data []byte) (Super, error) { if len(data) < 1 { return nil, ErrInvalidSuperRoot diff --git a/op-service/eth/super_root_test.go b/op-service/eth/super_root_test.go index 316b94e63e2f9..f7968dceeba16 100644 --- a/op-service/eth/super_root_test.go +++ b/op-service/eth/super_root_test.go @@ -2,6 +2,7 @@ package eth import ( "encoding/binary" + "encoding/json" "testing" "github.com/stretchr/testify/require" @@ -77,6 +78,33 @@ func TestSuperRootV1Codec(t *testing.T) { }) } +func TestSuperRootV1JSON(t *testing.T) { + t.Run("UseHexForTimestamp", func(t *testing.T) { + chainA := ChainIDAndOutput{ChainID: ChainIDFromUInt64(11), Output: Bytes32{0x01}} + superRoot := NewSuperV1(7000, chainA) + jsonData, err := json.Marshal(superRoot) + require.NoError(t, err) + + values := make(map[string]any) + err = json.Unmarshal(jsonData, &values) + require.NoError(t, err) + require.Equal(t, "0x1b58", values["timestamp"]) + }) + + t.Run("RoundTrip", func(t *testing.T) { + chainA := ChainIDAndOutput{ChainID: ChainIDFromUInt64(11), Output: Bytes32{0x01}} + chainB := ChainIDAndOutput{ChainID: ChainIDFromUInt64(12), Output: Bytes32{0x02}} + chainC := ChainIDAndOutput{ChainID: ChainIDFromUInt64(13), Output: Bytes32{0x03}} + superRoot := NewSuperV1(7000, chainA, chainB, chainC) + data, err := json.Marshal(superRoot) + require.NoError(t, err) + var actual SuperV1 + err = json.Unmarshal(data, &actual) + require.NoError(t, err) + require.Equal(t, superRoot, &actual) + }) +} + func TestResponseToSuper(t *testing.T) { t.Run("SingleChain", func(t *testing.T) { input := SuperRootResponse{ diff --git a/op-supernode/supernode/activity/superroot/superroot.go b/op-supernode/supernode/activity/superroot/superroot.go index 31d4a9c29fe0b..c958bf76c0da3 100644 --- a/op-supernode/supernode/activity/superroot/superroot.go +++ b/op-supernode/supernode/activity/superroot/superroot.go @@ -2,11 +2,13 @@ package superroot import ( "context" + "errors" "fmt" "github.com/ethereum-optimism/optimism/op-service/eth" cc "github.com/ethereum-optimism/optimism/op-supernode/supernode/chain_container" - "github.com/ethereum/go-ethereum" + "github.com/ethereum-optimism/optimism/op-supernode/supernode/chain_container/engine_controller" + "github.com/ethereum/go-ethereum/common/hexutil" gethlog "github.com/ethereum/go-ethereum/log" ) @@ -33,48 +35,56 @@ func (s *Superroot) RPCService() interface{} { return &superrootAPI{s: s} } type superrootAPI struct{ s *Superroot } // OutputWithSource is the full Output and its source L1 block -type OutputWithSource struct { - Output *eth.OutputResponse - SourceL1 eth.BlockID +type OutputWithRequiredL1 struct { + Output *eth.OutputResponse `json:"output"` + RequiredL1 eth.BlockID `json:"required_l1"` } // L2WithRequiredL1 is a verified L2 block and the minimum L1 block at which the verification is possible type L2WithRequiredL1 struct { - L2 eth.BlockID - MinRequiredL1 eth.BlockID + L2 eth.BlockID `json:"l2"` + RequiredL1 eth.BlockID `json:"required_l1"` } -// atTimestampResponse is the response superroot_atTimestamp -// it contains: -// - CurrentL1Derived: the current L1 block that each chain has derived up to (without any verification) -// - CurrentL1Verified: the current L1 block that each verifier has processed up to -// - VerifiedAtTimestamp: the L2 blocks which are fully verified at the given timestamp, and the minimum L1 block at which verification is possible -// - OptimisticAtTimestamp: the L2 blocks which would be applied if verification were assumed to be successful, and their L1 sources -// - SuperRoot: the superroot at the given timestamp using verified L2 blocks -type atTimestampResponse struct { - CurrentL1Derived map[eth.ChainID]eth.BlockID - CurrentL1Verified map[string]eth.BlockID - VerifiedAtTimestamp map[eth.ChainID]L2WithRequiredL1 - OptimisticAtTimestamp map[eth.ChainID]OutputWithSource - MinCurrentL1 eth.BlockID - MinVerifiedRequiredL1 eth.BlockID - SuperRoot eth.Bytes32 +type SuperRootResponseData struct { + // UnverifiedAtTimestamp is the L2 block that would be applied if verification were assumed to be successful, + // and the minimum L1 block required to derive them. + UnverifiedAtTimestamp map[eth.ChainID]OutputWithRequiredL1 `json:"unverified_at_timestamp"` + + // VerifiedRequiredL1 is the minimum L1 block including the required data to fully verify all blocks at this timestamp + VerifiedRequiredL1 eth.BlockID `json:"verified_required_l1"` + + // Super is the unhashed data for the superroot at the given timestamp after all verification is applied. + Super eth.Super `json:"super"` + + // SuperRoot is the superroot at the given timestamp after all verification is applied. + SuperRoot eth.Bytes32 `json:"super_root"` +} + +// AtTimestampResponse is the response superroot_atTimestamp +type AtTimestampResponse struct { + // CurrentL1Derived is a map from chain ID to the highest L1 block that has been fully derived for that chain. It may not have been fully validated. + CurrentL1Derived map[eth.ChainID]eth.BlockID `json:"current_l1_derived"` + + // CurrentL1 is the highest L1 block that has been fully derived and verified by all chains. + CurrentL1 eth.BlockID `json:"current_l1"` + + // Data provides information about the super root at the requested timestamp if present. If block data at the + // requested timestamp is not present, the data will be nil. + Data *SuperRootResponseData } // AtTimestamp computes the super-root at the given timestamp, plus additional information about the current L1s, verified L2s, and optimistic L2s -func (api *superrootAPI) AtTimestamp(ctx context.Context, timestamp uint64) (atTimestampResponse, error) { - return api.s.atTimestamp(ctx, timestamp) +func (api *superrootAPI) AtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (AtTimestampResponse, error) { + return api.s.atTimestamp(ctx, uint64(timestamp)) } -func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (atTimestampResponse, error) { +func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (AtTimestampResponse, error) { currentL1Derived := map[eth.ChainID]eth.BlockID{} - // there are no Verification Activities yet, so there is no call to make to collect their CurrentL1 - // this will be replaced with a call to the Verification Activities when they are implemented - currentL1Verified := map[string]eth.BlockID{} verified := map[eth.ChainID]L2WithRequiredL1{} - optimistic := map[eth.ChainID]OutputWithSource{} + optimistic := map[eth.ChainID]OutputWithRequiredL1{} minCurrentL1 := eth.BlockID{} - minVerifiedRequiredL1 := eth.BlockID{} + maxVerifiedRequiredL1 := eth.BlockID{} chainOutputs := make([]eth.ChainIDAndOutput, 0, len(s.chains)) // get current l1s @@ -84,7 +94,7 @@ func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (atTimest currentL1, err := chain.CurrentL1(ctx) if err != nil { s.log.Warn("failed to get current L1", "chain_id", chainID.String(), "err", err) - return atTimestampResponse{}, err + return AtTimestampResponse{}, err } currentL1Derived[chainID] = currentL1.ID() if currentL1.ID().Number < minCurrentL1.Number || minCurrentL1 == (eth.BlockID{}) { @@ -96,39 +106,46 @@ func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (atTimest for chainID, chain := range s.chains { // verifiedAt returns the L2 block which is fully verified at the given timestamp, and the minimum L1 block at which verification is possible verifiedL2, verifiedL1, err := chain.VerifiedAt(ctx, timestamp) - if err != nil { + if errors.Is(err, engine_controller.ErrNotFound) { + // We don't have a fully verified block at the specified timestamp so no super root can be produced. + // Return only the current derived L1 block info. + return AtTimestampResponse{ + CurrentL1Derived: currentL1Derived, + CurrentL1: minCurrentL1, + }, nil + } else if err != nil { s.log.Warn("failed to get verified L1", "chain_id", chainID.String(), "err", err) - return atTimestampResponse{}, fmt.Errorf("%w: %w", ethereum.NotFound, err) + return AtTimestampResponse{}, fmt.Errorf("failed to get verified L1 for chain ID %v: %w", chainID, err) } verified[chainID] = L2WithRequiredL1{ - L2: verifiedL2, - MinRequiredL1: verifiedL1, + L2: verifiedL2, + RequiredL1: verifiedL1, } - if verifiedL1.Number < minVerifiedRequiredL1.Number || minVerifiedRequiredL1 == (eth.BlockID{}) { - minVerifiedRequiredL1 = verifiedL1 + if verifiedL1.Number > maxVerifiedRequiredL1.Number || maxVerifiedRequiredL1 == (eth.BlockID{}) { + maxVerifiedRequiredL1 = verifiedL1 } // Compute output root at or before timestamp using the verified L2 block number outRoot, err := chain.OutputRootAtL2BlockNumber(ctx, verifiedL2.Number) if err != nil { s.log.Warn("failed to compute output root at L2 block", "chain_id", chainID.String(), "l2_number", verifiedL2.Number, "err", err) - return atTimestampResponse{}, fmt.Errorf("%w: %w", ethereum.NotFound, err) + return AtTimestampResponse{}, fmt.Errorf("failed to compute output root at L2 block %v for chain ID %v: %w", verifiedL2.Number, chainID, err) } chainOutputs = append(chainOutputs, eth.ChainIDAndOutput{ChainID: chainID, Output: outRoot}) // Optimistic output is the full output at the optimistic L2 block for the timestamp optimisticOut, err := chain.OptimisticOutputAtTimestamp(ctx, timestamp) if err != nil { s.log.Warn("failed to get optimistic L1", "chain_id", chainID.String(), "err", err) - return atTimestampResponse{}, fmt.Errorf("%w: %w", ethereum.NotFound, err) + return AtTimestampResponse{}, fmt.Errorf("failed to get optimistic L1 for chain ID %v: %w", chainID, err) } // Also include the source L1 for context _, optimisticL1, err := chain.OptimisticAt(ctx, timestamp) if err != nil { s.log.Warn("failed to get optimistic source L1", "chain_id", chainID.String(), "err", err) - return atTimestampResponse{}, fmt.Errorf("%w: %w", ethereum.NotFound, err) + return AtTimestampResponse{}, fmt.Errorf("failed to get optimistic L1 for chain ID %v: %w", chainID, err) } - optimistic[chainID] = OutputWithSource{ - Output: optimisticOut, - SourceL1: optimisticL1, + optimistic[chainID] = OutputWithRequiredL1{ + Output: optimisticOut, + RequiredL1: optimisticL1, } } @@ -136,13 +153,14 @@ func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (atTimest superV1 := eth.NewSuperV1(timestamp, chainOutputs...) superRoot := eth.SuperRoot(superV1) - return atTimestampResponse{ - CurrentL1Derived: currentL1Derived, - CurrentL1Verified: currentL1Verified, - VerifiedAtTimestamp: verified, - OptimisticAtTimestamp: optimistic, - MinCurrentL1: minCurrentL1, - MinVerifiedRequiredL1: minVerifiedRequiredL1, - SuperRoot: superRoot, + return AtTimestampResponse{ + CurrentL1Derived: currentL1Derived, + CurrentL1: minCurrentL1, + Data: &SuperRootResponseData{ + UnverifiedAtTimestamp: optimistic, + VerifiedRequiredL1: maxVerifiedRequiredL1, + Super: superV1, + SuperRoot: superRoot, + }, }, nil } diff --git a/op-supernode/supernode/activity/superroot/superroot_test.go b/op-supernode/supernode/activity/superroot/superroot_test.go index f85acd8572ee2..ee1cdf67543f0 100644 --- a/op-supernode/supernode/activity/superroot/superroot_test.go +++ b/op-supernode/supernode/activity/superroot/superroot_test.go @@ -7,6 +7,8 @@ import ( "github.com/ethereum-optimism/optimism/op-service/eth" cc "github.com/ethereum-optimism/optimism/op-supernode/supernode/chain_container" + "github.com/ethereum-optimism/optimism/op-supernode/supernode/chain_container/engine_controller" + "github.com/ethereum/go-ethereum/common/hexutil" gethlog "github.com/ethereum/go-ethereum/log" "github.com/stretchr/testify/require" ) @@ -98,13 +100,12 @@ func TestSuperroot_AtTimestamp_Succeeds(t *testing.T) { out, err := api.AtTimestamp(context.Background(), 123) require.NoError(t, err) require.Len(t, out.CurrentL1Derived, 2) - require.Len(t, out.VerifiedAtTimestamp, 2) - require.Len(t, out.OptimisticAtTimestamp, 2) + require.Len(t, out.Data.UnverifiedAtTimestamp, 2) // min values - require.Equal(t, uint64(2000), out.MinCurrentL1.Number) - require.Equal(t, uint64(1000), out.MinVerifiedRequiredL1.Number) + require.Equal(t, uint64(2000), out.CurrentL1.Number) + require.Equal(t, uint64(1100), out.Data.VerifiedRequiredL1.Number) // With zero outputs, the superroot will be deterministic, just ensure it's set - _ = out.SuperRoot + _ = out.Data.SuperRoot } func TestSuperroot_AtTimestamp_ComputesSuperRoot(t *testing.T) { @@ -132,7 +133,7 @@ func TestSuperroot_AtTimestamp_ComputesSuperRoot(t *testing.T) { ts := uint64(123) s := New(gethlog.New(), chains) api := &superrootAPI{s: s} - resp, err := api.AtTimestamp(context.Background(), ts) + resp, err := api.AtTimestamp(context.Background(), hexutil.Uint64(ts)) require.NoError(t, err) // Compute expected super root @@ -141,7 +142,7 @@ func TestSuperroot_AtTimestamp_ComputesSuperRoot(t *testing.T) { {ChainID: eth.ChainIDFromUInt64(420), Output: out2}, } expected := eth.SuperRoot(eth.NewSuperV1(ts, chainOutputs...)) - require.Equal(t, expected, resp.SuperRoot) + require.Equal(t, expected, resp.Data.SuperRoot) } func TestSuperroot_AtTimestamp_ErrorOnCurrentL1(t *testing.T) { @@ -170,6 +171,20 @@ func TestSuperroot_AtTimestamp_ErrorOnVerifiedAt(t *testing.T) { require.Error(t, err) } +func TestSuperroot_AtTimestamp_NotFoundErrorOnVerifiedAt(t *testing.T) { + t.Parallel() + chains := map[eth.ChainID]cc.ChainContainer{ + eth.ChainIDFromUInt64(10): &mockCC{ + verifiedErr: fmt.Errorf("no block: %w", engine_controller.ErrNotFound), + }, + } + s := New(gethlog.New(), chains) + api := &superrootAPI{s: s} + response, err := api.AtTimestamp(context.Background(), 123) + require.NoError(t, err) + require.Nil(t, response.Data) +} + func TestSuperroot_AtTimestamp_ErrorOnOutputRoot(t *testing.T) { t.Parallel() chains := map[eth.ChainID]cc.ChainContainer{ @@ -207,8 +222,7 @@ func TestSuperroot_AtTimestamp_EmptyChains(t *testing.T) { out, err := api.AtTimestamp(context.Background(), 123) require.NoError(t, err) require.Len(t, out.CurrentL1Derived, 0) - require.Len(t, out.VerifiedAtTimestamp, 0) - require.Len(t, out.OptimisticAtTimestamp, 0) + require.Len(t, out.Data.UnverifiedAtTimestamp, 0) } // assertErr returns a generic error instance used to signal mock failures.