Skip to content

Commit

Permalink
op-challenger: Begin implementing super root trace provider (#13777)
Browse files Browse the repository at this point in the history
* op-challenger: Begin implementing super root trace provider

* op-challenger: Remove first attempt at handling unsafe proposals.

Will replace with a proper implementation as a follow up

* op-challenger: Update for move to eth package
  • Loading branch information
ajsutton authored Jan 15, 2025
1 parent 984bae9 commit 50682ad
Show file tree
Hide file tree
Showing 8 changed files with 411 additions and 38 deletions.
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++ {
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

0 comments on commit 50682ad

Please sign in to comment.