Commit 50682ad4 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

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

* 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
parent 984bae91
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)
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
}
......@@ -314,12 +314,6 @@ func TestInteropFaultProofs(gt *testing.T) {
end, err := source.CreateSuperRoot(ctx, endTimestamp)
require.NoError(t, err)
serializeIntermediateRoot := func(root *types.TransitionState) []byte {
data, err := root.Marshal()
require.NoError(t, err)
return data
}
endBlockNumA, err := actors.ChainA.RollupCfg.TargetBlockNumber(endTimestamp)
require.NoError(t, err)
chain1End, err := chainAClient.OutputAtBlock(ctx, endBlockNumA)
......@@ -330,32 +324,32 @@ func TestInteropFaultProofs(gt *testing.T) {
chain2End, err := chainBClient.OutputAtBlock(ctx, endBlockNumB)
require.NoError(t, err)
step1Expected := serializeIntermediateRoot(&types.TransitionState{
step1Expected := (&types.TransitionState{
SuperRoot: start.Marshal(),
PendingProgress: []types.OptimisticBlock{
{BlockHash: chain1End.BlockRef.Hash, OutputRoot: chain1End.OutputRoot},
},
Step: 1,
})
}).Marshal()
step2Expected := serializeIntermediateRoot(&types.TransitionState{
step2Expected := (&types.TransitionState{
SuperRoot: start.Marshal(),
PendingProgress: []types.OptimisticBlock{
{BlockHash: chain1End.BlockRef.Hash, OutputRoot: chain1End.OutputRoot},
{BlockHash: chain2End.BlockRef.Hash, OutputRoot: chain2End.OutputRoot},
},
Step: 2,
})
}).Marshal()
paddingStep := func(step uint64) []byte {
return serializeIntermediateRoot(&types.TransitionState{
return (&types.TransitionState{
SuperRoot: start.Marshal(),
PendingProgress: []types.OptimisticBlock{
{BlockHash: chain1End.BlockRef.Hash, OutputRoot: chain1End.OutputRoot},
{BlockHash: chain2End.BlockRef.Hash, OutputRoot: chain2End.OutputRoot},
},
Step: step,
})
}).Marshal()
}
tests := []*transitionTest{
......
......@@ -78,11 +78,7 @@ func stateTransition(logger log.Logger, bootInfo *boot.BootInfoInterop, l1Preima
PendingProgress: expectedPendingProgress,
Step: transitionState.Step + 1,
}
expected, err := finalState.Hash()
if err != nil {
return common.Hash{}, err
}
return expected, nil
return finalState.Hash(), nil
}
func parseAgreedState(bootInfo *boot.BootInfoInterop, l2PreimageOracle l2.Oracle) (*types.TransitionState, *eth.SuperV1, error) {
......
......@@ -66,9 +66,7 @@ func TestDeriveBlockForFirstChainFromSuperchainRoot(t *testing.T) {
Step: 1,
}
expectedClaim, err := expectedIntermediateRoot.Hash()
require.NoError(t, err)
expectedClaim := expectedIntermediateRoot.Hash()
verifyResult(t, logger, tasksStub, configSource, l2PreimageOracle, agreedSuperRoot, outputRootHash, expectedClaim)
}
......@@ -82,8 +80,7 @@ func TestDeriveBlockForSecondChainFromTransitionState(t *testing.T) {
},
Step: 1,
}
outputRootHash, err := agreedTransitionState.Hash()
require.NoError(t, err)
outputRootHash := agreedTransitionState.Hash()
l2PreimageOracle, _ := test.NewStubOracle(t)
l2PreimageOracle.TransitionStates[outputRootHash] = agreedTransitionState
expectedIntermediateRoot := &types.TransitionState{
......@@ -95,8 +92,7 @@ func TestDeriveBlockForSecondChainFromTransitionState(t *testing.T) {
Step: 2,
}
expectedClaim, err := expectedIntermediateRoot.Hash()
require.NoError(t, err)
expectedClaim := expectedIntermediateRoot.Hash()
verifyResult(t, logger, tasksStub, configSource, l2PreimageOracle, agreedSuperRoot, outputRootHash, expectedClaim)
}
......@@ -111,15 +107,13 @@ func TestNoOpStep(t *testing.T) {
},
Step: 2,
}
outputRootHash, err := agreedTransitionState.Hash()
require.NoError(t, err)
outputRootHash := agreedTransitionState.Hash()
l2PreimageOracle, _ := test.NewStubOracle(t)
l2PreimageOracle.TransitionStates[outputRootHash] = agreedTransitionState
expectedIntermediateRoot := *agreedTransitionState // Copy agreed state
expectedIntermediateRoot.Step = 3
expectedClaim, err := expectedIntermediateRoot.Hash()
require.NoError(t, err)
expectedClaim := expectedIntermediateRoot.Hash()
verifyResult(t, logger, tasksStub, configSource, l2PreimageOracle, agreedSuperRoot, outputRootHash, expectedClaim)
}
......
......@@ -32,20 +32,17 @@ func (i *TransitionState) Version() byte {
return IntermediateTransitionVersion
}
func (i *TransitionState) Marshal() ([]byte, error) {
func (i *TransitionState) Marshal() []byte {
rlpData, err := rlp.EncodeToBytes(i)
if err != nil {
panic(err)
}
return append([]byte{IntermediateTransitionVersion}, rlpData...), nil
return append([]byte{IntermediateTransitionVersion}, rlpData...)
}
func (i *TransitionState) Hash() (common.Hash, error) {
data, err := i.Marshal()
if err != nil {
return common.Hash{}, err
}
return crypto.Keccak256Hash(data), nil
func (i *TransitionState) Hash() common.Hash {
data := i.Marshal()
return crypto.Keccak256Hash(data)
}
func UnmarshalTransitionState(data []byte) (*TransitionState, error) {
......
......@@ -25,8 +25,7 @@ func TestTransitionStateCodec(t *testing.T) {
},
Step: 2,
}
data, err := state.Marshal()
require.NoError(t, err)
data := state.Marshal()
actual, err := UnmarshalTransitionState(data)
require.NoError(t, err)
require.Equal(t, state, actual)
......
package eth
import (
"cmp"
"encoding/binary"
"encoding/json"
"errors"
"slices"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
......@@ -44,6 +46,16 @@ func (c *ChainIDAndOutput) Marshal() []byte {
return d
}
func NewSuperV1(timestamp uint64, chains ...ChainIDAndOutput) *SuperV1 {
slices.SortFunc(chains, func(a, b ChainIDAndOutput) int {
return cmp.Compare(a.ChainID, b.ChainID)
})
return &SuperV1{
Timestamp: timestamp,
Chains: chains,
}
}
type SuperV1 struct {
Timestamp uint64
Chains []ChainIDAndOutput
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment