Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

op-challenger: Begin implementing super root trace provider #13777

Merged
merged 3 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions op-challenger/game/fault/trace/super/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package super

import (
"context"
"errors"
"fmt"

"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"
)

var (
ErrGetStepData = errors.New("GetStepData not supported")
ErrIndexTooBig = errors.New("trace index is greater than max uint64")

InvalidTransition = []byte("invalid")
InvalidTransitionHash = crypto.Keccak256Hash(InvalidTransition)
)

const (
StepsPerTimestamp = 1024
)

type RootProvider interface {
SuperRootAtTimestamp(timestamp uint64) (eth.SuperRootResponse, error)
}

type SuperTraceProvider struct {
types.PrestateProvider
logger log.Logger
rootProvider RootProvider
prestateTimestamp uint64
poststateTimestamp uint64
l1Head eth.BlockID
gameDepth types.Depth
}

func NewSuperTraceProvider(logger log.Logger, prestateProvider types.PrestateProvider, rootProvider RootProvider, l1Head eth.BlockID, gameDepth types.Depth, prestateTimestamp, poststateTimestamp uint64) *SuperTraceProvider {
return &SuperTraceProvider{
PrestateProvider: prestateProvider,
logger: logger,
rootProvider: rootProvider,
prestateTimestamp: prestateTimestamp,
poststateTimestamp: poststateTimestamp,
l1Head: l1Head,
gameDepth: gameDepth,
}
}

func (s *SuperTraceProvider) Get(ctx context.Context, pos types.Position) (common.Hash, error) {
// Find the timestamp and step at position
timestamp, step, err := s.ComputeStep(pos)
if err != nil {
return common.Hash{}, err
}
s.logger.Info("Getting claim", "pos", pos.ToGIndex(), "timestamp", timestamp, "step", step)
if step == 0 {
root, err := s.rootProvider.SuperRootAtTimestamp(timestamp)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to retrieve super root at timestamp %v: %w", timestamp, err)
}
return common.Hash(root.SuperRoot), nil
}
// Fetch the super root at the next timestamp since we are part way through the transition to it
prevRoot, err := s.rootProvider.SuperRootAtTimestamp(timestamp)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to retrieve super root at timestamp %v: %w", timestamp, err)
}
nextTimestamp := timestamp + 1
nextRoot, err := s.rootProvider.SuperRootAtTimestamp(nextTimestamp)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to retrieve super root at timestamp %v: %w", nextTimestamp, err)
}
prevChainOutputs := make([]eth.ChainIDAndOutput, 0, len(prevRoot.Chains))
for _, chain := range prevRoot.Chains {
prevChainOutputs = append(prevChainOutputs, eth.ChainIDAndOutput{ChainID: chain.ChainID.ToBig().Uint64(), Output: chain.Canonical})
}
expectedState := interopTypes.TransitionState{
SuperRoot: eth.NewSuperV1(prevRoot.Timestamp, prevChainOutputs...).Marshal(),
PendingProgress: make([]interopTypes.OptimisticBlock, 0, step),
Step: step,
}
for i := uint64(0); i < min(step, uint64(len(prevChainOutputs))); i++ {
Inphi marked this conversation as resolved.
Show resolved Hide resolved
rawOutput, err := eth.UnmarshalOutput(nextRoot.Chains[i].Pending)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to unmarshal pending output %v at timestamp %v: %w", i, nextTimestamp, err)
}
output, ok := rawOutput.(*eth.OutputV0)
if !ok {
return common.Hash{}, fmt.Errorf("unsupported output version %v at timestamp %v", output.Version(), nextTimestamp)
}
expectedState.PendingProgress = append(expectedState.PendingProgress, interopTypes.OptimisticBlock{
BlockHash: output.BlockHash,
OutputRoot: eth.OutputRoot(output),
})
}
return expectedState.Hash(), nil
}

func (s *SuperTraceProvider) 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 *SuperTraceProvider) GetStepData(_ context.Context, _ types.Position) (prestate []byte, proofData []byte, preimageData *types.PreimageOracleData, err error) {
return nil, nil, nil, ErrGetStepData
}

func (s *SuperTraceProvider) GetL2BlockNumberChallenge(_ context.Context) (*types.InvalidL2BlockNumberChallenge, error) {
// Never need to challenge L2 block number for super root games.
return nil, types.ErrL2BlockNumberValid
}

var _ types.TraceProvider = (*SuperTraceProvider)(nil)
249 changes: 249 additions & 0 deletions op-challenger/game/fault/trace/super/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package super

import (
"context"
"math/big"
"math/rand"
"testing"

"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"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)

var (
gameDepth = types.Depth(30)
prestateTimestamp = uint64(1000)
poststateTimestamp = uint64(5000)
)

func TestGet(t *testing.T) {
t.Run("AtPostState", func(t *testing.T) {
provider, stubSupervisor := createProvider(t)
superRoot := eth.Bytes32{0xaa}
stubSupervisor.Add(eth.SuperRootResponse{
Timestamp: poststateTimestamp,
SuperRoot: superRoot,
Chains: []eth.ChainRootInfo{
{
ChainID: eth.ChainIDFromUInt64(1),
Canonical: eth.Bytes32{0xbb},
Pending: []byte{0xcc},
},
},
})
claim, err := provider.Get(context.Background(), types.RootPosition)
require.NoError(t, err)
require.Equal(t, common.Hash(superRoot), claim)
})

t.Run("AtNewTimestamp", func(t *testing.T) {
provider, stubSupervisor := createProvider(t)
superRoot := eth.Bytes32{0xaa}
stubSupervisor.Add(eth.SuperRootResponse{
Timestamp: prestateTimestamp + 1,
SuperRoot: superRoot,
Chains: []eth.ChainRootInfo{
{
ChainID: eth.ChainIDFromUInt64(1),
Canonical: eth.Bytes32{0xbb},
Pending: []byte{0xcc},
},
},
})
claim, err := provider.Get(context.Background(), types.NewPosition(gameDepth, big.NewInt(StepsPerTimestamp-1)))
require.NoError(t, err)
require.Equal(t, common.Hash(superRoot), claim)
})

t.Run("FirstTimestamp", func(t *testing.T) {
rng := rand.New(rand.NewSource(1))
provider, stubSupervisor := createProvider(t)
outputA1 := testutils.RandomOutputV0(rng)
outputA2 := testutils.RandomOutputV0(rng)
outputB1 := testutils.RandomOutputV0(rng)
outputB2 := testutils.RandomOutputV0(rng)
superRoot1 := eth.NewSuperV1(
prestateTimestamp,
eth.ChainIDAndOutput{ChainID: 1, Output: eth.OutputRoot(outputA1)},
eth.ChainIDAndOutput{ChainID: 2, Output: eth.OutputRoot(outputB1)})
superRoot2 := eth.NewSuperV1(prestateTimestamp+1,
eth.ChainIDAndOutput{ChainID: 1, Output: eth.OutputRoot(outputA2)},
eth.ChainIDAndOutput{ChainID: 2, Output: eth.OutputRoot(outputB2)})
stubSupervisor.Add(eth.SuperRootResponse{
Timestamp: prestateTimestamp,
SuperRoot: eth.SuperRoot(superRoot1),
Chains: []eth.ChainRootInfo{
{
ChainID: eth.ChainIDFromUInt64(1),
Canonical: eth.OutputRoot(outputA1),
Pending: outputA1.Marshal(),
},
{
ChainID: eth.ChainIDFromUInt64(2),
Canonical: eth.OutputRoot(outputB1),
Pending: outputB1.Marshal(),
},
},
})
stubSupervisor.Add(eth.SuperRootResponse{
Timestamp: prestateTimestamp + 1,
SuperRoot: eth.SuperRoot(superRoot2),
Chains: []eth.ChainRootInfo{
{
ChainID: eth.ChainIDFromUInt64(1),
Canonical: eth.OutputRoot(outputA2),
Pending: outputA2.Marshal(),
},
{
ChainID: eth.ChainIDFromUInt64(1),
Canonical: eth.OutputRoot(outputB2),
Pending: outputB2.Marshal(),
},
},
})

expectedFirstStep := &interopTypes.TransitionState{
SuperRoot: superRoot1.Marshal(),
PendingProgress: []interopTypes.OptimisticBlock{
{BlockHash: outputA2.BlockHash, OutputRoot: eth.OutputRoot(outputA2)},
},
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: superRoot1.Marshal(),
PendingProgress: []interopTypes.OptimisticBlock{
{BlockHash: outputA2.BlockHash, OutputRoot: eth.OutputRoot(outputA2)},
{BlockHash: outputB2.BlockHash, OutputRoot: eth.OutputRoot(outputB2)},
},
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: superRoot1.Marshal(),
PendingProgress: []interopTypes.OptimisticBlock{
{BlockHash: outputA2.BlockHash, OutputRoot: eth.OutputRoot(outputA2)},
{BlockHash: outputB2.BlockHash, OutputRoot: eth.OutputRoot(outputB2)},
},
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)
}
})
}

func TestGetStepDataReturnsError(t *testing.T) {
provider, _ := createProvider(t)
_, _, _, err := provider.GetStepData(context.Background(), types.RootPosition)
require.ErrorIs(t, err, ErrGetStepData)
}

func TestGetL2BlockNumberChallengeReturnsError(t *testing.T) {
provider, _ := createProvider(t)
_, err := provider.GetL2BlockNumberChallenge(context.Background())
require.ErrorIs(t, err, types.ErrL2BlockNumberValid)
}

func TestComputeStep(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 := NewSuperTraceProvider(testlog.Logger(t, log.LvlInfo), nil, &stubRootProvider{}, 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, _ := createProvider(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, _ := createProvider(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, _ := createProvider(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, _ := createProvider(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(1023), prevStep, "Should only loop back to step 0 after the consolidation step")
}
prevTimestamp = timestamp
prevStep = step
}
})
}

func createProvider(t *testing.T) (*SuperTraceProvider, *stubRootProvider) {
logger := testlog.Logger(t, log.LvlInfo)
stubSupervisor := &stubRootProvider{
rootsByTimestamp: make(map[uint64]eth.SuperRootResponse),
}
return NewSuperTraceProvider(logger, nil, stubSupervisor, eth.BlockID{}, gameDepth, prestateTimestamp, poststateTimestamp), stubSupervisor
}

type stubRootProvider struct {
rootsByTimestamp map[uint64]eth.SuperRootResponse
}

func (s *stubRootProvider) Add(root eth.SuperRootResponse) {
if s.rootsByTimestamp == nil {
s.rootsByTimestamp = make(map[uint64]eth.SuperRootResponse)
}
s.rootsByTimestamp[root.Timestamp] = root
}

func (s *stubRootProvider) SuperRootAtTimestamp(timestamp uint64) (eth.SuperRootResponse, error) {
root, ok := s.rootsByTimestamp[timestamp]
if !ok {
return eth.SuperRootResponse{}, ethereum.NotFound
}
return root, nil
}
Loading