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..91ba757c32b79 --- /dev/null +++ b/op-challenger/game/fault/trace/super/provider_supernode.go @@ -0,0 +1,166 @@ +package super + +import ( + "context" + "fmt" + "slices" + + "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/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" +) + +type SuperNodeRootProvider interface { + SuperRootAtTimestamp(ctx context.Context, timestamp uint64) (eth.SuperRootAtTimestampResponse, 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 + } + + prevSuper := prevRoot.Data.Super + expectedState := interopTypes.TransitionState{ + SuperRoot: prevSuper.Marshal(), + PendingProgress: make([]interopTypes.OptimisticBlock, 0, step), + Step: step, + } + + // Should already be sorted but be defensive and sort it ourselves + slices.SortFunc(nextRoot.ChainIDs, func(a, b eth.ChainID) int { + return a.Cmp(b) + }) + for i := uint64(0); i < min(step, uint64(len(nextRoot.ChainIDs))); i++ { + chainID := nextRoot.ChainIDs[i] + // Check if the chain's optimistic root was safe at the game's L1 head + optimistic, ok := nextRoot.OptimisticAtTimestamp[chainID] + if !ok { + // No block at this timestamp for a chain that needs to be processed at this step, so return invalid + return InvalidTransition, nil + } + 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..f1ee1ed29ec8f --- /dev/null +++ b/op-challenger/game/fault/trace/super/provider_supernode_test.go @@ -0,0 +1,556 @@ +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/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 := eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: []eth.ChainID{eth.ChainIDFromUInt64(1), eth.ChainIDFromUInt64(2)}, + Data: ð.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 := eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: []eth.ChainID{eth.ChainIDFromUInt64(1), eth.ChainIDFromUInt64(2)}, + Data: ð.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 := eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: []eth.ChainID{eth.ChainIDFromUInt64(1), eth.ChainIDFromUInt64(2)}, + Data: ð.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 := eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: []eth.ChainID{eth.ChainIDFromUInt64(1), eth.ChainIDFromUInt64(2)}, + Data: ð.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.OptimisticAtTimestamp[eth.ChainIDFromUInt64(1)] = eth.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.OptimisticAtTimestamp[eth.ChainIDFromUInt64(2)] = eth.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, eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: []eth.ChainID{eth.ChainIDFromUInt64(1), eth.ChainIDFromUInt64(2)}, + Data: nil, + }) + + claim, err := provider.Get(context.Background(), types.RootPosition) + require.NoError(t, err) + require.Equal(t, InvalidTransitionHash, claim) + }) + + t.Run("NextSuperRootTimestampBeyondAllChainHeads", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + prev, _ := createValidSuperNodeSuperRoots(l1Head) + stubSupervisor.Add(prev) + stubSupervisor.AddAtTimestamp(prestateTimestamp+1, eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: prev.ChainIDs, + Data: nil, + }) + + // All steps should be the invalid transition hash as there are no chains with optimistic blocks + 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("NextSuperRootTimestampBeyondFirstChainHead", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + prev, next := createValidSuperNodeSuperRoots(l1Head) + stubSupervisor.Add(prev) + stubSupervisor.AddAtTimestamp(prestateTimestamp+1, eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: prev.ChainIDs, + OptimisticAtTimestamp: map[eth.ChainID]eth.OutputWithRequiredL1{ + eth.ChainIDFromUInt64(2): next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(2)], + }, + Data: nil, + }) + // All steps should be the invalid transition hash because the first chain is invalid. + 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("NextSuperRootTimestampBeyondSecondChainHead", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + prev, next := createValidSuperNodeSuperRoots(l1Head) + stubSupervisor.Add(prev) + stubSupervisor.AddAtTimestamp(prestateTimestamp+1, eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: next.ChainIDs, + OptimisticAtTimestamp: map[eth.ChainID]eth.OutputWithRequiredL1{ + eth.ChainIDFromUInt64(1): next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(1)], + }, + Data: nil, + }) + // 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") + + // All remaining steps should be the invalid transition hash because the second chain is invalid. + 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("PreviousSuperRootTimestampBeyondChainHead", func(t *testing.T) { + provider, stubSupervisor, l1Head := createSuperNodeProvider(t) + stubSupervisor.AddAtTimestamp(prestateTimestamp, eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: []eth.ChainID{eth.ChainIDFromUInt64(1), eth.ChainIDFromUInt64(2)}, + Data: nil, + }) + stubSupervisor.AddAtTimestamp(prestateTimestamp+1, eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: []eth.ChainID{eth.ChainIDFromUInt64(1), eth.ChainIDFromUInt64(2)}, + 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 := eth.SuperRootAtTimestampResponse{ + CurrentL1: eth.BlockID{Number: l1Head.Number - 1, Hash: common.Hash{0xaa}}, + ChainIDs: []eth.ChainID{eth.ChainIDFromUInt64(1), eth.ChainIDFromUInt64(2)}, + Data: ð.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, eth.SuperRootAtTimestampResponse{ + CurrentL1: eth.BlockID{Number: l1Head.Number - 1, Hash: common.Hash{0xaa}}, + ChainIDs: []eth.ChainID{eth.ChainIDFromUInt64(1), eth.ChainIDFromUInt64(2)}, + }) + _, 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, eth.SuperRootAtTimestampResponse{ + CurrentL1: eth.BlockID{Number: l1Head.Number - 1, Hash: common.Hash{0xaa}}, + ChainIDs: []eth.ChainID{eth.ChainIDFromUInt64(1), eth.ChainIDFromUInt64(2)}, + }) + _, 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]eth.SuperRootAtTimestampResponse), + } + 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) (eth.SuperRootAtTimestampResponse, eth.SuperRootAtTimestampResponse) { + rng := rand.New(rand.NewSource(1)) + outputA1 := testutils.RandomOutputV0(rng) + outputA2 := testutils.RandomOutputV0(rng) + outputB1 := testutils.RandomOutputV0(rng) + outputB2 := testutils.RandomOutputV0(rng) + chainID1 := eth.ChainIDFromUInt64(1) + chainID2 := eth.ChainIDFromUInt64(2) + prevSuper := eth.NewSuperV1( + prestateTimestamp, + eth.ChainIDAndOutput{ChainID: chainID1, Output: eth.OutputRoot(outputA1)}, + eth.ChainIDAndOutput{ChainID: chainID2, Output: eth.OutputRoot(outputB1)}) + nextSuper := eth.NewSuperV1(prestateTimestamp+1, + eth.ChainIDAndOutput{ChainID: chainID1, Output: eth.OutputRoot(outputA2)}, + eth.ChainIDAndOutput{ChainID: chainID2, Output: eth.OutputRoot(outputB2)}) + + prevResponse := eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: []eth.ChainID{chainID1, chainID2}, + OptimisticAtTimestamp: map[eth.ChainID]eth.OutputWithRequiredL1{ + chainID1: { + Output: toOutputResponse(outputA1), + RequiredL1: l1Head, + }, + chainID2: { + Output: toOutputResponse(outputB1), + RequiredL1: l1Head, + }, + }, + Data: ð.SuperRootResponseData{ + VerifiedRequiredL1: l1Head, + Super: prevSuper, + SuperRoot: eth.SuperRoot(prevSuper), + }, + } + nextResponse := eth.SuperRootAtTimestampResponse{ + CurrentL1: l1Head, + ChainIDs: []eth.ChainID{chainID1, chainID2}, + OptimisticAtTimestamp: map[eth.ChainID]eth.OutputWithRequiredL1{ + chainID1: { + Output: toOutputResponse(outputA2), + RequiredL1: l1Head, + }, + chainID2: { + Output: toOutputResponse(outputB2), + RequiredL1: l1Head, + }, + }, + Data: ð.SuperRootResponseData{ + VerifiedRequiredL1: l1Head, + Super: nextSuper, + SuperRoot: eth.SuperRoot(nextSuper), + }, + } + return prevResponse, nextResponse +} + +func expectSuperNodeValidTransition(t *testing.T, provider *SuperNodeTraceProvider, prev eth.SuperRootAtTimestampResponse, next eth.SuperRootAtTimestampResponse) { + chain1OptimisticBlock := interopTypes.OptimisticBlock{ + BlockHash: next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(1)].Output.BlockRef.Hash, + OutputRoot: next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(1)].Output.OutputRoot, + } + chain2OptimisticBlock := interopTypes.OptimisticBlock{ + BlockHash: next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(2)].Output.BlockRef.Hash, + OutputRoot: next.OptimisticAtTimestamp[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]eth.SuperRootAtTimestampResponse +} + +func (s *stubSuperNodeRootProvider) Add(root eth.SuperRootAtTimestampResponse) { + superV1 := root.Data.Super.(*eth.SuperV1) + s.AddAtTimestamp(superV1.Timestamp, root) +} + +func (s *stubSuperNodeRootProvider) AddAtTimestamp(timestamp uint64, root eth.SuperRootAtTimestampResponse) { + if s.rootsByTimestamp == nil { + s.rootsByTimestamp = make(map[uint64]eth.SuperRootAtTimestampResponse) + } + s.rootsByTimestamp[timestamp] = root +} + +func (s *stubSuperNodeRootProvider) SuperRootAtTimestamp(_ context.Context, timestamp uint64) (eth.SuperRootAtTimestampResponse, 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 eth.SuperRootAtTimestampResponse{}, fmt.Errorf("wowsers, now response for timestamp %v", timestamp) + } + return root, nil +} diff --git a/op-service/eth/superroot_at_timestamp.go b/op-service/eth/superroot_at_timestamp.go index c842233f4981f..e2e742478b7d2 100644 --- a/op-service/eth/superroot_at_timestamp.go +++ b/op-service/eth/superroot_at_timestamp.go @@ -29,6 +29,9 @@ type SuperRootAtTimestampResponse struct { // from the L1 data currently processed. OptimisticAtTimestamp map[ChainID]OutputWithRequiredL1 `json:"optimistic_at_timestamp"` + // ChainIDs are the chain IDs in the dependency set at the requested timestamp, sorted ascending. + ChainIDs []ChainID `json:"chain_ids"` + // 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 `json:"data,omitempty"` diff --git a/op-supernode/supernode/activity/superroot/superroot.go b/op-supernode/supernode/activity/superroot/superroot.go index 0224fd3c10cdd..e4ae516a4f234 100644 --- a/op-supernode/supernode/activity/superroot/superroot.go +++ b/op-supernode/supernode/activity/superroot/superroot.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "github.com/ethereum-optimism/optimism/op-service/eth" cc "github.com/ethereum-optimism/optimism/op-supernode/supernode/chain_container" @@ -60,8 +61,10 @@ func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (eth.Supe } notFound := false + chainIDs := make([]eth.ChainID, 0, len(s.chains)) // collect verified and optimistic L2 and L1 blocks at the given timestamp for chainID, chain := range s.chains { + chainIDs = append(chainIDs, chainID) // 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 errors.Is(err, ethereum.NotFound) { @@ -99,9 +102,13 @@ func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (eth.Supe } } + slices.SortFunc(chainIDs, func(a, b eth.ChainID) int { + return a.Cmp(b) + }) response := eth.SuperRootAtTimestampResponse{ CurrentL1: minCurrentL1, OptimisticAtTimestamp: optimistic, + ChainIDs: chainIDs, } if !notFound { // Build super root from collected outputs