diff --git a/op-challenger/game/fault/test/game_builder.go b/op-challenger/game/fault/test/game_builder.go index c474b393daf8..39a3a8f54f51 100644 --- a/op-challenger/game/fault/test/game_builder.go +++ b/op-challenger/game/fault/test/game_builder.go @@ -29,10 +29,14 @@ type GameBuilderSeq struct { } func (g *GameBuilder) Seq() *GameBuilderSeq { + return g.SeqFrom(g.Game.Claims()[0]) +} + +func (g *GameBuilder) SeqFrom(claim types.Claim) *GameBuilderSeq { return &GameBuilderSeq{ gameBuilder: g, builder: g.builder, - lastClaim: g.Game.Claims()[0], + lastClaim: claim, } } diff --git a/op-challenger/game/fault/trace/access.go b/op-challenger/game/fault/trace/access.go index fb024045d0b9..7ccce5d39b6d 100644 --- a/op-challenger/game/fault/trace/access.go +++ b/op-challenger/game/fault/trace/access.go @@ -7,8 +7,6 @@ import ( "github.com/ethereum/go-ethereum/common" ) -type ProviderCreator func(ctx context.Context, pre types.Claim, post types.Claim) (types.TraceProvider, error) - func NewSimpleTraceAccessor(trace types.TraceProvider) *Accessor { selector := func(_ context.Context, _ types.Game, _ types.Claim, _ types.Position) (types.TraceProvider, error) { return trace, nil @@ -16,8 +14,10 @@ func NewSimpleTraceAccessor(trace types.TraceProvider) *Accessor { return &Accessor{selector} } +type ProviderSelector func(ctx context.Context, game types.Game, ref types.Claim, pos types.Position) (types.TraceProvider, error) + type Accessor struct { - selector func(ctx context.Context, game types.Game, ref types.Claim, pos types.Position) (types.TraceProvider, error) + selector ProviderSelector } func (t *Accessor) Get(ctx context.Context, game types.Game, ref types.Claim, pos types.Position) (common.Hash, error) { diff --git a/op-challenger/game/fault/trace/alphabet/provider.go b/op-challenger/game/fault/trace/alphabet/provider.go index ac64a6da6639..a0ec33daf2de 100644 --- a/op-challenger/game/fault/trace/alphabet/provider.go +++ b/op-challenger/game/fault/trace/alphabet/provider.go @@ -3,6 +3,7 @@ package alphabet import ( "context" "errors" + "fmt" "math/big" "strings" @@ -43,7 +44,7 @@ func (ap *AlphabetTraceProvider) GetStepData(ctx context.Context, i types.Positi traceIndex = traceIndex.Sub(traceIndex, big.NewInt(1)) // The index cannot be larger than the maximum index as computed by the depth. if traceIndex.Cmp(big.NewInt(int64(ap.maxLen))) >= 0 { - return nil, nil, nil, ErrIndexTooLarge + return nil, nil, nil, fmt.Errorf("%w traceIndex: %v max: %v pos: %v", ErrIndexTooLarge, traceIndex, ap.maxLen, i) } // We extend the deepest hash to the maximum depth if the trace is not expansive. if traceIndex.Cmp(big.NewInt(int64(len(ap.state)))) >= 0 { @@ -54,6 +55,9 @@ func (ap *AlphabetTraceProvider) GetStepData(ctx context.Context, i types.Positi // Get returns the claim value at the given index in the trace. func (ap *AlphabetTraceProvider) Get(ctx context.Context, i types.Position) (common.Hash, error) { + if uint64(i.Depth()) > ap.depth { + return common.Hash{}, fmt.Errorf("%w depth: %v max: %v", ErrIndexTooLarge, i.Depth(), ap.depth) + } // Step data returns the pre-state, so add 1 to get the state for index i ti := i.TraceIndex(int(ap.depth)) postPosition := types.NewPosition(int(ap.depth), new(big.Int).Add(ti, big.NewInt(1))) diff --git a/op-challenger/game/fault/trace/alphabet/provider_test.go b/op-challenger/game/fault/trace/alphabet/provider_test.go index e3ec3cc3dce9..8e6a2cff65b8 100644 --- a/op-challenger/game/fault/trace/alphabet/provider_test.go +++ b/op-challenger/game/fault/trace/alphabet/provider_test.go @@ -92,6 +92,14 @@ func TestGet_IndexTooLarge(t *testing.T) { require.ErrorIs(t, err, ErrIndexTooLarge) } +func TestGet_DepthTooLarge(t *testing.T) { + depth := 2 + ap := NewTraceProvider("abc", uint64(depth)) + pos := types.NewPosition(depth+1, big.NewInt(0)) + _, err := ap.Get(context.Background(), pos) + require.ErrorIs(t, err, ErrIndexTooLarge) +} + // TestGet_Extends tests the Get function with an index that is larger // than the trace, but smaller than the maximum depth. func TestGet_Extends(t *testing.T) { diff --git a/op-challenger/game/fault/trace/outputs/split.go b/op-challenger/game/fault/trace/outputs/split.go new file mode 100644 index 000000000000..384e0044a349 --- /dev/null +++ b/op-challenger/game/fault/trace/outputs/split.go @@ -0,0 +1,89 @@ +package outputs + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" +) + +var ( + errRefClaimNotDeepEnough = errors.New("reference claim is not deep enough") +) + +type ProviderCreator func(ctx context.Context, pre types.Claim, post types.Claim) (types.TraceProvider, error) + +func newSplitProviderSelector(topProvider types.TraceProvider, topDepth int, bottomProviderCreator ProviderCreator) trace.ProviderSelector { + return func(ctx context.Context, game types.Game, ref types.Claim, pos types.Position) (types.TraceProvider, error) { + if pos.Depth() <= topDepth { + return topProvider, nil + } + if ref.Position.Depth() < topDepth { + return nil, fmt.Errorf("%w, claim depth: %v, depth required: %v", errRefClaimNotDeepEnough, ref.Position.Depth(), topDepth) + } + + // Find the ancestor claim at the leaf level for the top game. + topLeaf, err := findAncestorAtDepth(game, ref, topDepth) + if err != nil { + return nil, err + } + + var pre, post types.Claim + // If pos is to the right of the leaf from the top game, we must be defending that output root + // otherwise, we're attacking it. + if pos.TraceIndex(pos.Depth()).Cmp(topLeaf.TraceIndex(pos.Depth())) > 0 { + // Defending the top leaf claim, so use it as the pre-claim and find the post + pre = topLeaf + postTraceIdx := new(big.Int).Add(pre.TraceIndex(topDepth), big.NewInt(1)) + post, err = findAncestorWithTraceIndex(game, topLeaf, topDepth, postTraceIdx) + if err != nil { + return nil, fmt.Errorf("failed to find post claim: %w", err) + } + } else { + // Attacking the top leaf claim, so use it as the post-claim and find the pre + post = topLeaf + postTraceIdx := post.TraceIndex(topDepth) + if postTraceIdx.Cmp(big.NewInt(0)) == 0 { + pre = types.Claim{} + } else { + preTraceIdx := new(big.Int).Sub(postTraceIdx, big.NewInt(1)) + pre, err = findAncestorWithTraceIndex(game, topLeaf, topDepth, preTraceIdx) + if err != nil { + return nil, fmt.Errorf("failed to find pre claim: %w", err) + } + } + } + provider, err := bottomProviderCreator(ctx, pre, post) + if err != nil { + return nil, err + } + // Translate such that the root of the bottom game is the level below the top game leaf + return trace.Translate(provider, uint64(topDepth)+1), nil + } +} + +func findAncestorAtDepth(game types.Game, claim types.Claim, depth int) (types.Claim, error) { + for claim.Depth() > depth { + parent, err := game.GetParent(claim) + if err != nil { + return types.Claim{}, fmt.Errorf("failed to find ancestor at depth %v: %w", depth, err) + } + claim = parent + } + return claim, nil +} + +func findAncestorWithTraceIndex(game types.Game, ref types.Claim, depth int, traceIdx *big.Int) (types.Claim, error) { + candidate := ref + for candidate.TraceIndex(depth).Cmp(traceIdx) != 0 { + parent, err := game.GetParent(candidate) + if err != nil { + return types.Claim{}, fmt.Errorf("failed to get parent of claim %v: %w", candidate.ContractIndex, err) + } + candidate = parent + } + return candidate, nil +} diff --git a/op-challenger/game/fault/trace/outputs/split_test.go b/op-challenger/game/fault/trace/outputs/split_test.go new file mode 100644 index 000000000000..488cafee563e --- /dev/null +++ b/op-challenger/game/fault/trace/outputs/split_test.go @@ -0,0 +1,325 @@ +package outputs + +import ( + "context" + "fmt" + "math/big" + "testing" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/test" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "github.com/stretchr/testify/require" +) + +const ( + topDepth = 3 + bottomDepth = 4 +) + +func TestUseTopProvider(t *testing.T) { + ctx := context.Background() + topProvider, selector, gameBuilder := setupAlphabetSplitSelector(t) + + ref := gameBuilder.Game.Claims()[0] + + pos := ref.Position + for pos.Depth() <= topDepth { + provider, err := selector(ctx, gameBuilder.Game, ref, ref.Position) + require.NoError(t, err) + require.Same(t, topProvider, provider) + _, err = topProvider.Get(ctx, pos) + require.NoError(t, err, "should be able to use provider for position") + pos = pos.Attack() + } +} + +func TestErrorWhenRefAboveTopGameLeafButPositionInBottom(t *testing.T) { + ctx := context.Background() + _, selector, gameBuilder := setupAlphabetSplitSelector(t) + + // Generate claims at depths up to but not including the leaf of the top providers + createClaimsToDepth(gameBuilder, topDepth-1) + for _, ref := range gameBuilder.Game.Claims() { + pos := types.NewPosition(topDepth+1, big.NewInt(0)) + provider, err := selector(ctx, gameBuilder.Game, ref, pos) + require.ErrorIsf(t, err, errRefClaimNotDeepEnough, "should not get provider with ref claim at depth: %v", ref.Depth()) + require.Nil(t, provider) + } +} + +func TestTranslatePositionsForBottomProvider(t *testing.T) { + tests := []struct { + name string + setup func(t *testing.T, gameBuilder *test.GameBuilder) (ref types.Claim, pos types.Position, expectPre types.Claim, expectPost types.Claim) + }{ + // There are 4 leaf nodes that can be accessed in the top tree of depth 3: 8, 10, 12, 14 + // Then you can attack and defend any of those to challenge all blocks + {"attackTopLeafGIndex8", attackTopLeafGIndex8}, + {"defendTopLeafGIndex8", defendTopLeafGIndex8}, + {"attackTopLeafGIndex10", attackTopLeafGIndex10}, + {"defendTopLeafGIndex10", defendTopLeafGIndex10}, + {"attackTopLeafGIndex12", attackTopLeafGIndex12}, + {"defendTopLeafGIndex12", defendTopLeafGIndex12}, + {"attackTopLeafGIndex14", attackTopLeafGIndex14}, + {"attackTopLeafGIndex14", defendTopLeafGIndex14}, + } + for _, tCase := range tests { + tCase := tCase + t.Run(tCase.name, func(t *testing.T) { + _, selector, gameBuilder := setupAlphabetSplitSelector(t) + ref, pos, _, _ := tCase.setup(t, gameBuilder) + provider, err := selector(context.Background(), gameBuilder.Game, ref, pos) + require.NoError(t, err) + + claimPos := pos + localClaimPos := types.NewPositionFromGIndex(big.NewInt(1)) + requireSameValue(t, provider, claimPos, asBottomTraceProvider(t, provider).AlphabetTraceProvider, localClaimPos) + requireSameValue(t, provider, claimPos.Attack(), asBottomTraceProvider(t, provider).AlphabetTraceProvider, localClaimPos.Attack()) + requireSameValue(t, provider, claimPos.Attack().Defend(), asBottomTraceProvider(t, provider).AlphabetTraceProvider, localClaimPos.Attack().Defend()) + }) + } +} + +func requireSameValue(t *testing.T, a types.TraceProvider, aPos types.Position, b types.TraceProvider, bPos types.Position) { + // Check Get returns the same results + aValue, err := a.Get(context.Background(), aPos) + require.NoError(t, err) + bValue, err := b.Get(context.Background(), bPos) + require.NoError(t, err) + require.Equal(t, aValue, bValue) + + // Check GetStepData returns the same results + aPrestate, aProofData, aPreimageData, err := a.GetStepData(context.Background(), aPos) + require.NoError(t, err) + bPrestate, bProofData, bPreimageData, err := b.GetStepData(context.Background(), bPos) + require.NoError(t, err) + require.Equal(t, aPrestate, bPrestate) + require.Equal(t, aProofData, bProofData) + require.Equal(t, aPreimageData, bPreimageData) +} + +func TestBottomProviderAttackingTopLeaf(t *testing.T) { + tests := []struct { + name string + setup func(t *testing.T, gameBuilder *test.GameBuilder) (ref types.Claim, pos types.Position, expectPre types.Claim, expectPost types.Claim) + }{ + // There are 4 leaf nodes that can be accessed in the top tree of depth 3: 8, 10, 12, 14 + // Then you can attack and defend any of those to challenge all blocks + // We can then use these setups to test any other reference claim descending from what these setup since + // that whole subtree should have the same pre and post claim from the top provider. + {"attackTopLeafGIndex8", attackTopLeafGIndex8}, + {"defendTopLeafGIndex8", defendTopLeafGIndex8}, + {"attackTopLeafGIndex10", attackTopLeafGIndex10}, + {"defendTopLeafGIndex10", defendTopLeafGIndex10}, + {"attackTopLeafGIndex12", attackTopLeafGIndex12}, + {"defendTopLeafGIndex12", defendTopLeafGIndex12}, + {"attackTopLeafGIndex14", attackTopLeafGIndex14}, + {"attackTopLeafGIndex14", defendTopLeafGIndex14}, + } + for _, tCase := range tests { + tCase := tCase + t.Run(tCase.name, func(t *testing.T) { + _, selector, gameBuilder := setupAlphabetSplitSelector(t) + + ref, pos, expectedPre, expectedPost := tCase.setup(t, gameBuilder) + + runTest := func(ref types.Claim, pos types.Position) { + t.Run(fmt.Sprintf("Ref-d%vi%v_Pos-d%vi%v", ref.Depth(), ref.IndexAtDepth(), pos.Depth(), pos.IndexAtDepth()), func(t *testing.T) { + provider, err := selector(context.Background(), gameBuilder.Game, ref, pos) + require.NoError(t, err) + requireBottomProviderForClaims(t, provider, expectedPre, expectedPost) + }) + } + + // Check we get the same pre and post for any reference claim lower in the game + var testDescendantClaims func(ref types.Claim, pos types.Position) + testDescendantClaims = func(ref types.Claim, pos types.Position) { + // For each reference claim, check it works with the claim position, or attacking or defending the claim + runTest(ref, pos) + runTest(ref, pos.Attack()) + runTest(ref, pos.Defend()) + if pos.Depth() >= topDepth+bottomDepth { + return + } + + // If the ref is the leaf of the top claim, ensure we respect whether the test is setup + // to attack or defend the top leaf claim. + if ref.Depth() != topDepth || !pos.RightOf(ref.Position) { + gameBuilder.SeqFrom(ref).AttackCorrect() + attackRef := latestClaim(gameBuilder) + testDescendantClaims(attackRef, attackRef.Position) + } + if ref.Depth() != topDepth || pos.RightOf(ref.Position) { + gameBuilder.SeqFrom(ref).DefendCorrect() + defendRef := latestClaim(gameBuilder) + testDescendantClaims(defendRef, defendRef.Position) + } + } + testDescendantClaims(ref, pos) + }) + } +} + +func attackTopLeafGIndex8(_ *testing.T, gameBuilder *test.GameBuilder) (ref types.Claim, pos types.Position, expectPre types.Claim, expectPost types.Claim) { + // Generate claims down to the top provider's leaf + seq := gameBuilder.Seq() // gindex 1, trace 7 + seq = seq.AttackCorrect() // gindex 2, trace 3 + seq = seq.AttackCorrect() // gindex 4, trace 1 + seq.AttackCorrect() // gindex 8, trace 0 + expectPost = latestClaim(gameBuilder) + + // No pre-claim as the first output root is being challenged. + expectPre = types.Claim{} + + ref = latestClaim(gameBuilder) + pos = ref.Position.Attack() + return +} + +func defendTopLeafGIndex8(_ *testing.T, gameBuilder *test.GameBuilder) (ref types.Claim, pos types.Position, expectPre types.Claim, expectPost types.Claim) { + // Generate claims down to the top provider's leaf + seq := gameBuilder.Seq() // gindex 1, trace 7 + seq = seq.AttackCorrect() // gindex 2, trace 3 + seq = seq.AttackCorrect() // gindex 4, trace 1 + expectPost = latestClaim(gameBuilder) + seq.AttackCorrect() // gindex 8, trace 0 + expectPre = latestClaim(gameBuilder) + + ref = latestClaim(gameBuilder) + pos = ref.Position.Defend() + return +} + +func attackTopLeafGIndex10(_ *testing.T, gameBuilder *test.GameBuilder) (ref types.Claim, pos types.Position, expectPre types.Claim, expectPost types.Claim) { + seq := gameBuilder.Seq() // gindex 1, trace 7 + seq = seq.AttackCorrect() // gindex 2, trace 3 + seq = seq.AttackCorrect() // gindex 4, trace 1 + expectPre = latestClaim(gameBuilder) + seq.DefendCorrect() // gindex 10, trace 2 + expectPost = latestClaim(gameBuilder) + + ref = latestClaim(gameBuilder) + pos = ref.Position.Attack() + return +} + +func defendTopLeafGIndex10(_ *testing.T, gameBuilder *test.GameBuilder) (ref types.Claim, pos types.Position, expectPre types.Claim, expectPost types.Claim) { + seq := gameBuilder.Seq() // gindex 1, trace 7 + seq = seq.AttackCorrect() // gindex 2, trace 3 + expectPost = latestClaim(gameBuilder) + seq = seq.AttackCorrect() // gindex 4, trace 1 + seq.DefendCorrect() // gindex 10, trace 2 + expectPre = latestClaim(gameBuilder) + + ref = latestClaim(gameBuilder) + pos = ref.Position.Defend() + return +} + +func attackTopLeafGIndex12(_ *testing.T, gameBuilder *test.GameBuilder) (ref types.Claim, pos types.Position, expectPre types.Claim, expectPost types.Claim) { + seq := gameBuilder.Seq() // gindex 1, trace 7 + seq = seq.AttackCorrect() // gindex 2, trace 3 + expectPre = latestClaim(gameBuilder) + seq = seq.DefendCorrect() // gindex 6, trace 5 + seq.AttackCorrect() // gindex 12, trace 4 + expectPost = latestClaim(gameBuilder) + + ref = latestClaim(gameBuilder) + pos = ref.Position.Attack() + return +} + +func defendTopLeafGIndex12(_ *testing.T, gameBuilder *test.GameBuilder) (ref types.Claim, pos types.Position, expectPre types.Claim, expectPost types.Claim) { + seq := gameBuilder.Seq() // gindex 1, trace 7 + seq = seq.AttackCorrect() // gindex 2, trace 3 + seq = seq.DefendCorrect() // gindex 6, trace 5 + expectPost = latestClaim(gameBuilder) + seq.AttackCorrect() // gindex 12, trace 4 + expectPre = latestClaim(gameBuilder) + + ref = latestClaim(gameBuilder) + pos = ref.Position.Defend() + return +} + +func attackTopLeafGIndex14(_ *testing.T, gameBuilder *test.GameBuilder) (ref types.Claim, pos types.Position, expectPre types.Claim, expectPost types.Claim) { + seq := gameBuilder.Seq() // gindex 1, trace 7 + seq = seq.AttackCorrect() // gindex 2, trace 3 + seq = seq.DefendCorrect() // gindex 6, trace 5 + expectPre = latestClaim(gameBuilder) + seq.DefendCorrect() // gindex 14, trace 6 + expectPost = latestClaim(gameBuilder) + + ref = latestClaim(gameBuilder) + pos = ref.Position.Attack() + return +} + +func defendTopLeafGIndex14(_ *testing.T, gameBuilder *test.GameBuilder) (ref types.Claim, pos types.Position, expectPre types.Claim, expectPost types.Claim) { + seq := gameBuilder.Seq() // gindex 1, trace 7 + expectPost = latestClaim(gameBuilder) + seq = seq.AttackCorrect() // gindex 2, trace 3 + seq = seq.DefendCorrect() // gindex 6, trace 5 + seq.DefendCorrect() // gindex 14, trace 6 + expectPre = latestClaim(gameBuilder) + + ref = latestClaim(gameBuilder) + pos = ref.Position.Defend() + return +} + +func latestClaim(gameBuilder *test.GameBuilder) types.Claim { + return gameBuilder.Game.Claims()[len(gameBuilder.Game.Claims())-1] +} + +func createClaimsToDepth(gameBuilder *test.GameBuilder, depth int) { + seq := gameBuilder.Seq() + for i := 0; i < depth; i++ { + seq = seq.AttackCorrect() + } +} + +func requireBottomProviderForClaims(t *testing.T, actual types.TraceProvider, expectedPre types.Claim, expectedPost types.Claim) { + if expectedPre != (types.Claim{}) { + require.Equal(t, + new(big.Int).Add(expectedPre.TraceIndex(topDepth), big.NewInt(1)), + expectedPost.TraceIndex(topDepth), + "should expect adjacent top level trace indices") + } + + bottomProvider := asBottomTraceProvider(t, actual) + require.Equal(t, expectedPre, bottomProvider.pre, "Incorrect pre claim") + require.Equal(t, expectedPost, bottomProvider.post, "Incorrect post claim") +} + +func asBottomTraceProvider(t *testing.T, actual types.TraceProvider) *bottomTraceProvider { + translatingProvider, ok := actual.(*trace.TranslatingProvider) + require.True(t, ok) + bottomProvider, ok := translatingProvider.Original().(*bottomTraceProvider) + require.True(t, ok) + return bottomProvider +} + +func setupAlphabetSplitSelector(t *testing.T) (*alphabet.AlphabetTraceProvider, trace.ProviderSelector, *test.GameBuilder) { + top := alphabet.NewTraceProvider("abcdef", topDepth) + bottomCreator := func(ctx context.Context, pre types.Claim, post types.Claim) (types.TraceProvider, error) { + return &bottomTraceProvider{ + pre: pre, + post: post, + AlphabetTraceProvider: alphabet.NewTraceProvider(post.Value.Hex(), bottomDepth), + }, nil + } + selector := newSplitProviderSelector(top, topDepth, bottomCreator) + + claimBuilder := test.NewAlphabetClaimBuilder(t, topDepth+bottomDepth) + gameBuilder := claimBuilder.GameBuilder(true, true) + return top, selector, gameBuilder +} + +type bottomTraceProvider struct { + pre types.Claim + post types.Claim + *alphabet.AlphabetTraceProvider +} diff --git a/op-challenger/game/fault/trace/split/provider.go b/op-challenger/game/fault/trace/split/provider.go deleted file mode 100644 index eb77f198116c..000000000000 --- a/op-challenger/game/fault/trace/split/provider.go +++ /dev/null @@ -1,67 +0,0 @@ -package split - -import ( - "context" - - "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" -) - -var _ types.TraceProvider = (*SplitTraceProvider)(nil) - -// SplitTraceProvider is a [types.TraceProvider] implementation that -// routes requests to the correct internal trace provider based on the -// depth of the requested trace. -type SplitTraceProvider struct { - logger log.Logger - topProvider types.TraceProvider - bottomProvider types.TraceProvider - topDepth uint64 -} - -// NewTraceProvider creates a new [SplitTraceProvider] instance. -// The [topDepth] parameter specifies the depth at which the internal -// [types.TraceProvider] should be switched. -func NewTraceProvider(logger log.Logger, topProvider types.TraceProvider, bottomProvider types.TraceProvider, topDepth uint64) *SplitTraceProvider { - return &SplitTraceProvider{ - logger: logger, - topProvider: topProvider, - bottomProvider: bottomProvider, - topDepth: topDepth, - } -} - -func (s *SplitTraceProvider) providerForDepth(depth uint64) (uint64, types.TraceProvider) { - if depth <= s.topDepth { - return 0, s.topProvider - } - return s.topDepth, s.bottomProvider -} - -// Get routes the Get request to the internal [types.TraceProvider] that -// that serves the trace index at the depth. -func (s *SplitTraceProvider) Get(ctx context.Context, pos types.Position) (common.Hash, error) { - ancestorDepth, provider := s.providerForDepth(uint64(pos.Depth())) - relativePosition, err := pos.RelativeToAncestorAtDepth(ancestorDepth) - if err != nil { - return common.Hash{}, err - } - return provider.Get(ctx, relativePosition) -} - -// AbsolutePreStateCommitment returns the absolute prestate from the lowest internal [types.TraceProvider] -func (s *SplitTraceProvider) AbsolutePreStateCommitment(ctx context.Context) (hash common.Hash, err error) { - return s.bottomProvider.AbsolutePreStateCommitment(ctx) -} - -// GetStepData routes the GetStepData request to the lowest internal [types.TraceProvider]. -func (s *SplitTraceProvider) GetStepData(ctx context.Context, pos types.Position) (prestate []byte, proofData []byte, preimageData *types.PreimageOracleData, err error) { - ancestorDepth, provider := s.providerForDepth(uint64(pos.Depth())) - relativePosition, err := pos.RelativeToAncestorAtDepth(ancestorDepth) - if err != nil { - return nil, nil, nil, err - } - return provider.GetStepData(ctx, relativePosition) -} diff --git a/op-challenger/game/fault/trace/split/provider_test.go b/op-challenger/game/fault/trace/split/provider_test.go deleted file mode 100644 index b63ab6d93349..000000000000 --- a/op-challenger/game/fault/trace/split/provider_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package split - -import ( - "context" - "errors" - "math/big" - "testing" - - "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" - "github.com/ethereum-optimism/optimism/op-service/testlog" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" - "github.com/stretchr/testify/require" -) - -var ( - mockGetError = errors.New("mock get error") - mockOutput = common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - mockCommitment = common.HexToHash("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") -) - -func TestGet(t *testing.T) { - t.Run("ErrorBubblesUp", func(t *testing.T) { - mockOutputProvider := mockTraceProvider{getError: mockGetError} - splitProvider := newSplitTraceProvider(t, &mockOutputProvider, nil, 40) - _, err := splitProvider.Get(context.Background(), types.NewPosition(1, common.Big0)) - require.ErrorIs(t, err, mockGetError) - }) - - t.Run("ReturnsCorrectOutputFromTopProvider", func(t *testing.T) { - mockOutputProvider := mockTraceProvider{getOutput: mockOutput} - splitProvider := newSplitTraceProvider(t, &mockOutputProvider, &mockTraceProvider{}, 40) - output, err := splitProvider.Get(context.Background(), types.NewPosition(6, big.NewInt(3))) - require.NoError(t, err) - expectedGIndex := types.NewPosition(6, big.NewInt(3)).ToGIndex() - require.Equal(t, common.BigToHash(expectedGIndex), output) - }) - - t.Run("ReturnsCorrectOutputWithMultipleProviders", func(t *testing.T) { - bottomProvider := mockTraceProvider{getOutput: mockOutput} - splitProvider := newSplitTraceProvider(t, &mockTraceProvider{}, &bottomProvider, 40) - output, err := splitProvider.Get(context.Background(), types.NewPosition(42, big.NewInt(17))) - require.NoError(t, err) - expectedGIndex := types.NewPosition(2, big.NewInt(1)).ToGIndex() - require.Equal(t, common.BigToHash(expectedGIndex), output) - }) -} - -func TestAbsolutePreStateCommitment(t *testing.T) { - t.Run("ErrorBubblesUp", func(t *testing.T) { - mockOutputProvider := mockTraceProvider{absolutePreStateCommitmentError: mockGetError} - splitProvider := newSplitTraceProvider(t, nil, &mockOutputProvider, 40) - _, err := splitProvider.AbsolutePreStateCommitment(context.Background()) - require.ErrorIs(t, err, mockGetError) - }) - - t.Run("ReturnsCorrectOutput", func(t *testing.T) { - mockOutputProvider := mockTraceProvider{absolutePreStateCommitment: mockCommitment} - splitProvider := newSplitTraceProvider(t, nil, &mockOutputProvider, 40) - output, err := splitProvider.AbsolutePreStateCommitment(context.Background()) - require.NoError(t, err) - require.Equal(t, mockCommitment, output) - }) -} - -func TestGetStepData(t *testing.T) { - t.Run("ErrorBubblesUp", func(t *testing.T) { - mockOutputProvider := mockTraceProvider{getStepDataError: mockGetError} - splitProvider := newSplitTraceProvider(t, &mockOutputProvider, nil, 40) - _, _, _, err := splitProvider.GetStepData(context.Background(), types.NewPosition(0, common.Big0)) - require.ErrorIs(t, err, mockGetError) - }) - - t.Run("ReturnsCorrectStepData", func(t *testing.T) { - expectedStepData := []byte{1, 2, 3, 4} - mockOutputProvider := mockTraceProvider{stepPrestateData: expectedStepData} - splitProvider := newSplitTraceProvider(t, nil, &mockOutputProvider, 40) - output, _, _, err := splitProvider.GetStepData(context.Background(), types.NewPosition(41, common.Big0)) - require.NoError(t, err) - require.Equal(t, expectedStepData, output) - }) -} - -type mockTraceProvider struct { - getOutput common.Hash - getError error - absolutePreStateCommitmentError error - absolutePreStateCommitment common.Hash - absolutePreStateError error - preImageData []byte - getStepDataError error - stepPrestateData []byte -} - -func newSplitTraceProvider(t *testing.T, tp *mockTraceProvider, bp *mockTraceProvider, topDepth uint64) SplitTraceProvider { - return SplitTraceProvider{ - logger: testlog.Logger(t, log.LvlInfo), - topProvider: tp, - bottomProvider: bp, - topDepth: topDepth, - } -} - -func (m *mockTraceProvider) Get(ctx context.Context, pos types.Position) (common.Hash, error) { - if m.getError != nil { - return common.Hash{}, m.getError - } - return common.BigToHash(pos.ToGIndex()), nil -} - -func (m *mockTraceProvider) AbsolutePreStateCommitment(ctx context.Context) (hash common.Hash, err error) { - if m.absolutePreStateCommitmentError != nil { - return common.Hash{}, m.absolutePreStateCommitmentError - } - return m.absolutePreStateCommitment, nil -} - -func (m *mockTraceProvider) AbsolutePreState(ctx context.Context) (preimage []byte, err error) { - if m.absolutePreStateError != nil { - return []byte{}, m.absolutePreStateError - } - return m.preImageData, nil -} - -func (m *mockTraceProvider) GetStepData(ctx context.Context, pos types.Position) ([]byte, []byte, *types.PreimageOracleData, error) { - if m.getStepDataError != nil { - return nil, nil, nil, m.getStepDataError - } - return m.stepPrestateData, nil, nil, nil -} diff --git a/op-challenger/game/fault/trace/translate.go b/op-challenger/game/fault/trace/translate.go index fbe8b58c544f..9ba1356c7068 100644 --- a/op-challenger/game/fault/trace/translate.go +++ b/op-challenger/game/fault/trace/translate.go @@ -7,36 +7,43 @@ import ( "github.com/ethereum/go-ethereum/common" ) -type translatingProvider struct { - parentDepth uint64 - provider types.TraceProvider +type TranslatingProvider struct { + rootDepth uint64 + provider types.TraceProvider } -func Translate(provider types.TraceProvider, parentDepth uint64) types.TraceProvider { - return &translatingProvider{ - parentDepth: parentDepth, - provider: provider, +// Translate returns a new TraceProvider that translates any requested positions before passing them on to the +// specified provider. +// The translation is done such that the root node for provider is at rootDepth. +func Translate(provider types.TraceProvider, rootDepth uint64) types.TraceProvider { + return &TranslatingProvider{ + rootDepth: rootDepth, + provider: provider, } } -func (p translatingProvider) Get(ctx context.Context, pos types.Position) (common.Hash, error) { - relativePos, err := pos.RelativeToAncestorAtDepth(p.parentDepth) +func (p *TranslatingProvider) Original() types.TraceProvider { + return p.provider +} + +func (p *TranslatingProvider) Get(ctx context.Context, pos types.Position) (common.Hash, error) { + relativePos, err := pos.RelativeToAncestorAtDepth(p.rootDepth) if err != nil { return common.Hash{}, err } return p.provider.Get(ctx, relativePos) } -func (p translatingProvider) GetStepData(ctx context.Context, pos types.Position) (prestate []byte, proofData []byte, preimageData *types.PreimageOracleData, err error) { - relativePos, err := pos.RelativeToAncestorAtDepth(p.parentDepth) +func (p *TranslatingProvider) GetStepData(ctx context.Context, pos types.Position) (prestate []byte, proofData []byte, preimageData *types.PreimageOracleData, err error) { + relativePos, err := pos.RelativeToAncestorAtDepth(p.rootDepth) if err != nil { return nil, nil, nil, err } return p.provider.GetStepData(ctx, relativePos) } -func (p translatingProvider) AbsolutePreStateCommitment(ctx context.Context) (hash common.Hash, err error) { +func (p *TranslatingProvider) AbsolutePreStateCommitment(ctx context.Context) (hash common.Hash, err error) { return p.provider.AbsolutePreStateCommitment(ctx) } -var _ types.TraceProvider = (*translatingProvider)(nil) +var _ types.TraceProvider = (*TranslatingProvider)(nil) diff --git a/op-challenger/game/fault/types/position.go b/op-challenger/game/fault/types/position.go index 56beaa32de10..b1404b2109c4 100644 --- a/op-challenger/game/fault/types/position.go +++ b/op-challenger/game/fault/types/position.go @@ -9,7 +9,7 @@ import ( ) var ( - ErrPositionDepthTooSmall = errors.New("Position depth is too small") + ErrPositionDepthTooSmall = errors.New("position depth is too small") ) // Position is a golang wrapper around the dispute game Position type. @@ -32,6 +32,10 @@ func NewPositionFromGIndex(x *big.Int) Position { return NewPosition(depth, indexAtDepth) } +func (p Position) String() string { + return fmt.Sprintf("Position(depth: %v, indexAtDepth: %v)", p.depth, p.indexAtDepth) +} + func (p Position) MoveRight() Position { return Position{ depth: p.depth,