Commit 8d59c7a1 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Restrict the safe head in output provider BlockNumber (#9664)

* op-challenger: Restrict the safe head in output provider BlockNumber instead of OutputRootProvider

This ensures the restriction is applied both to fetching output roots, and identifying the L2 block number the agreed output root comes from. The disputed block number remains restricted only by the claimed block number.

* go mod tidy cannon examples.

* op-e2e: Add test that proposals for future blocks are invalidated
parent c96fa366
...@@ -10,12 +10,12 @@ import ( ...@@ -10,12 +10,12 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs/source"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types" keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum-optimism/optimism/op-challenger/game/scheduler" "github.com/ethereum-optimism/optimism/op-challenger/game/scheduler"
"github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -29,7 +29,7 @@ type Registry interface { ...@@ -29,7 +29,7 @@ type Registry interface {
} }
type RollupClient interface { type RollupClient interface {
source.OutputRollupClient outputs.OutputRollupClient
SyncStatusProvider SyncStatusProvider
} }
...@@ -48,7 +48,6 @@ func RegisterGameTypes( ...@@ -48,7 +48,6 @@ func RegisterGameTypes(
) (CloseFunc, error) { ) (CloseFunc, error) {
var closer CloseFunc var closer CloseFunc
var l2Client *ethclient.Client var l2Client *ethclient.Client
var outputSourceCreator *source.OutputSourceCreator
if cfg.TraceTypeEnabled(config.TraceTypeCannon) || cfg.TraceTypeEnabled(config.TraceTypePermissioned) { if cfg.TraceTypeEnabled(config.TraceTypeCannon) || cfg.TraceTypeEnabled(config.TraceTypePermissioned) {
l2, err := ethclient.DialContext(ctx, cfg.CannonL2) l2, err := ethclient.DialContext(ctx, cfg.CannonL2)
if err != nil { if err != nil {
...@@ -56,17 +55,16 @@ func RegisterGameTypes( ...@@ -56,17 +55,16 @@ func RegisterGameTypes(
} }
l2Client = l2 l2Client = l2
closer = l2Client.Close closer = l2Client.Close
outputSourceCreator = source.NewOutputSourceCreator(logger, rollupClient, l1HeaderSource)
} }
syncValidator := newSyncStatusValidator(rollupClient) syncValidator := newSyncStatusValidator(rollupClient)
if cfg.TraceTypeEnabled(config.TraceTypeCannon) { if cfg.TraceTypeEnabled(config.TraceTypeCannon) {
if err := registerCannon(faultTypes.CannonGameType, registry, ctx, cl, logger, m, cfg, syncValidator, outputSourceCreator, txSender, gameFactory, caller, l2Client, l1HeaderSource); err != nil { if err := registerCannon(faultTypes.CannonGameType, registry, ctx, cl, logger, m, cfg, syncValidator, rollupClient, txSender, gameFactory, caller, l2Client, l1HeaderSource); err != nil {
return nil, fmt.Errorf("failed to register cannon game type: %w", err) return nil, fmt.Errorf("failed to register cannon game type: %w", err)
} }
} }
if cfg.TraceTypeEnabled(config.TraceTypePermissioned) { if cfg.TraceTypeEnabled(config.TraceTypePermissioned) {
if err := registerCannon(faultTypes.PermissionedGameType, registry, ctx, cl, logger, m, cfg, syncValidator, outputSourceCreator, txSender, gameFactory, caller, l2Client, l1HeaderSource); err != nil { if err := registerCannon(faultTypes.PermissionedGameType, registry, ctx, cl, logger, m, cfg, syncValidator, rollupClient, txSender, gameFactory, caller, l2Client, l1HeaderSource); err != nil {
return nil, fmt.Errorf("failed to register permissioned cannon game type: %w", err) return nil, fmt.Errorf("failed to register permissioned cannon game type: %w", err)
} }
} }
...@@ -85,7 +83,7 @@ func registerAlphabet( ...@@ -85,7 +83,7 @@ func registerAlphabet(
logger log.Logger, logger log.Logger,
m metrics.Metricer, m metrics.Metricer,
syncValidator SyncValidator, syncValidator SyncValidator,
rollupClient source.OutputRollupClient, rollupClient RollupClient,
txSender types.TxSender, txSender types.TxSender,
gameFactory *contracts.DisputeGameFactoryContract, gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller, caller *batching.MultiCaller,
...@@ -104,10 +102,13 @@ func registerAlphabet( ...@@ -104,10 +102,13 @@ func registerAlphabet(
if err != nil { if err != nil {
return nil, err return nil, err
} }
outputSource := source.NewUnrestrictedOutputSource(rollupClient) l1Head, err := loadL1Head(contract, ctx, l1HeaderSource)
prestateProvider := outputs.NewPrestateProvider(outputSource, prestateBlock) if err != nil {
return nil, err
}
prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock)
creator := func(ctx context.Context, logger log.Logger, gameDepth faultTypes.Depth, dir string) (faultTypes.TraceAccessor, error) { creator := func(ctx context.Context, logger log.Logger, gameDepth faultTypes.Depth, dir string) (faultTypes.TraceAccessor, error) {
accessor, err := outputs.NewOutputAlphabetTraceAccessor(logger, m, prestateProvider, outputSource, splitDepth, prestateBlock, poststateBlock) accessor, err := outputs.NewOutputAlphabetTraceAccessor(logger, m, prestateProvider, rollupClient, l1Head, splitDepth, prestateBlock, poststateBlock)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -155,7 +156,7 @@ func registerCannon( ...@@ -155,7 +156,7 @@ func registerCannon(
m metrics.Metricer, m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
syncValidator SyncValidator, syncValidator SyncValidator,
outputSourceCreator *source.OutputSourceCreator, rollupClient outputs.OutputRollupClient,
txSender types.TxSender, txSender types.TxSender,
gameFactory *contracts.DisputeGameFactoryContract, gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller, caller *batching.MultiCaller,
...@@ -175,17 +176,13 @@ func registerCannon( ...@@ -175,17 +176,13 @@ func registerCannon(
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load split depth: %w", err) return nil, fmt.Errorf("failed to load split depth: %w", err)
} }
l1Head, err := contract.GetL1Head(ctx) l1HeadID, err := loadL1Head(contract, ctx, l1HeaderSource)
if err != nil {
return nil, fmt.Errorf("failed to load L1 head: %w", err)
}
rollupClient, err := outputSourceCreator.ForL1Head(ctx, l1Head)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create output root source: %w", err) return nil, err
} }
prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock) prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock)
creator := func(ctx context.Context, logger log.Logger, gameDepth faultTypes.Depth, dir string) (faultTypes.TraceAccessor, error) { creator := func(ctx context.Context, logger log.Logger, gameDepth faultTypes.Depth, dir string) (faultTypes.TraceAccessor, error) {
accessor, err := outputs.NewOutputCannonTraceAccessor(logger, m, cfg, l2Client, contract, prestateProvider, rollupClient, dir, splitDepth, prestateBlock, poststateBlock) accessor, err := outputs.NewOutputCannonTraceAccessor(logger, m, cfg, l2Client, prestateProvider, rollupClient, dir, l1HeadID, splitDepth, prestateBlock, poststateBlock)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -207,3 +204,15 @@ func registerCannon( ...@@ -207,3 +204,15 @@ func registerCannon(
registry.RegisterBondContract(gameType, contractCreator) registry.RegisterBondContract(gameType, contractCreator)
return nil return nil
} }
func loadL1Head(contract *contracts.FaultDisputeGameContract, ctx context.Context, l1HeaderSource L1HeaderSource) (eth.BlockID, error) {
l1Head, err := contract.GetL1Head(ctx)
if err != nil {
return eth.BlockID{}, fmt.Errorf("failed to load L1 head: %w", err)
}
l1Header, err := l1HeaderSource.HeaderByHash(ctx, l1Head)
if err != nil {
return eth.BlockID{}, fmt.Errorf("failed to load L1 header: %w", err)
}
return eth.HeaderBlockID(l1Header), nil
}
...@@ -36,15 +36,15 @@ func FetchLocalInputs(ctx context.Context, caller GameInputsSource, l2Client L2H ...@@ -36,15 +36,15 @@ func FetchLocalInputs(ctx context.Context, caller GameInputsSource, l2Client L2H
if err != nil { if err != nil {
return LocalGameInputs{}, fmt.Errorf("fetch proposals: %w", err) return LocalGameInputs{}, fmt.Errorf("fetch proposals: %w", err)
} }
return FetchLocalInputsFromProposals(ctx, caller, l2Client, agreedOutput, claimedOutput)
}
func FetchLocalInputsFromProposals(ctx context.Context, caller L1HeadSource, l2Client L2HeaderSource, agreedOutput contracts.Proposal, claimedOutput contracts.Proposal) (LocalGameInputs, error) {
l1Head, err := caller.GetL1Head(ctx) l1Head, err := caller.GetL1Head(ctx)
if err != nil { if err != nil {
return LocalGameInputs{}, fmt.Errorf("fetch L1 head: %w", err) return LocalGameInputs{}, fmt.Errorf("fetch L1 head: %w", err)
} }
return FetchLocalInputsFromProposals(ctx, l1Head, l2Client, agreedOutput, claimedOutput)
}
func FetchLocalInputsFromProposals(ctx context.Context, l1Head common.Hash, l2Client L2HeaderSource, agreedOutput contracts.Proposal, claimedOutput contracts.Proposal) (LocalGameInputs, error) {
agreedHeader, err := l2Client.HeaderByNumber(ctx, agreedOutput.L2BlockNumber) agreedHeader, err := l2Client.HeaderByNumber(ctx, agreedOutput.L2BlockNumber)
if err != nil { if err != nil {
return LocalGameInputs{}, fmt.Errorf("fetch L2 block header %v: %w", agreedOutput.L2BlockNumber, err) return LocalGameInputs{}, fmt.Errorf("fetch L2 block header %v: %w", agreedOutput.L2BlockNumber, err)
......
...@@ -52,9 +52,7 @@ func TestFetchLocalInputsFromProposals(t *testing.T) { ...@@ -52,9 +52,7 @@ func TestFetchLocalInputsFromProposals(t *testing.T) {
L2BlockNumber: big.NewInt(3333), L2BlockNumber: big.NewInt(3333),
OutputRoot: common.Hash{0xee}, OutputRoot: common.Hash{0xee},
} }
contract := &mockGameInputsSource{ l1Head := common.Hash{0xcc}
l1Head: common.Hash{0xcc},
}
l2Client := &mockL2DataSource{ l2Client := &mockL2DataSource{
chainID: big.NewInt(88422), chainID: big.NewInt(88422),
header: ethtypes.Header{ header: ethtypes.Header{
...@@ -62,10 +60,10 @@ func TestFetchLocalInputsFromProposals(t *testing.T) { ...@@ -62,10 +60,10 @@ func TestFetchLocalInputsFromProposals(t *testing.T) {
}, },
} }
inputs, err := FetchLocalInputsFromProposals(ctx, contract, l2Client, agreed, claimed) inputs, err := FetchLocalInputsFromProposals(ctx, l1Head, l2Client, agreed, claimed)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, contract.l1Head, inputs.L1Head) require.Equal(t, l1Head, inputs.L1Head)
require.Equal(t, l2Client.header.Hash(), inputs.L2Head) require.Equal(t, l2Client.header.Hash(), inputs.L2Head)
require.EqualValues(t, agreed.OutputRoot, inputs.L2OutputRoot) require.EqualValues(t, agreed.OutputRoot, inputs.L2OutputRoot)
require.EqualValues(t, claimed.OutputRoot, inputs.L2Claim) require.EqualValues(t, claimed.OutputRoot, inputs.L2Claim)
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/split" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/split"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -17,12 +18,13 @@ func NewOutputAlphabetTraceAccessor( ...@@ -17,12 +18,13 @@ func NewOutputAlphabetTraceAccessor(
logger log.Logger, logger log.Logger,
m metrics.Metricer, m metrics.Metricer,
prestateProvider types.PrestateProvider, prestateProvider types.PrestateProvider,
rollupClient OutputRootProvider, rollupClient OutputRollupClient,
l1Head eth.BlockID,
splitDepth types.Depth, splitDepth types.Depth,
prestateBlock uint64, prestateBlock uint64,
poststateBlock uint64, poststateBlock uint64,
) (*trace.Accessor, error) { ) (*trace.Accessor, error) {
outputProvider := NewTraceProvider(logger, prestateProvider, rollupClient, splitDepth, prestateBlock, poststateBlock) outputProvider := NewTraceProvider(logger, prestateProvider, rollupClient, l1Head, splitDepth, prestateBlock, poststateBlock)
alphabetCreator := func(ctx context.Context, localContext common.Hash, depth types.Depth, agreed contracts.Proposal, claimed contracts.Proposal) (types.TraceProvider, error) { alphabetCreator := func(ctx context.Context, localContext common.Hash, depth types.Depth, agreed contracts.Proposal, claimed contracts.Proposal) (types.TraceProvider, error) {
provider := alphabet.NewTraceProvider(agreed.L2BlockNumber, depth) provider := alphabet.NewTraceProvider(agreed.L2BlockNumber, depth)
return provider, nil return provider, nil
......
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/split" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/split"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -21,19 +22,19 @@ func NewOutputCannonTraceAccessor( ...@@ -21,19 +22,19 @@ func NewOutputCannonTraceAccessor(
m metrics.Metricer, m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
l2Client cannon.L2HeaderSource, l2Client cannon.L2HeaderSource,
contract cannon.L1HeadSource,
prestateProvider types.PrestateProvider, prestateProvider types.PrestateProvider,
rollupClient OutputRootProvider, rollupClient OutputRollupClient,
dir string, dir string,
l1Head eth.BlockID,
splitDepth types.Depth, splitDepth types.Depth,
prestateBlock uint64, prestateBlock uint64,
poststateBlock uint64, poststateBlock uint64,
) (*trace.Accessor, error) { ) (*trace.Accessor, error) {
outputProvider := NewTraceProvider(logger, prestateProvider, rollupClient, splitDepth, prestateBlock, poststateBlock) outputProvider := NewTraceProvider(logger, prestateProvider, rollupClient, l1Head, splitDepth, prestateBlock, poststateBlock)
cannonCreator := func(ctx context.Context, localContext common.Hash, depth types.Depth, agreed contracts.Proposal, claimed contracts.Proposal) (types.TraceProvider, error) { cannonCreator := func(ctx context.Context, localContext common.Hash, depth types.Depth, agreed contracts.Proposal, claimed contracts.Proposal) (types.TraceProvider, error) {
logger := logger.New("pre", agreed.OutputRoot, "post", claimed.OutputRoot, "localContext", localContext) logger := logger.New("pre", agreed.OutputRoot, "post", claimed.OutputRoot, "localContext", localContext)
subdir := filepath.Join(dir, localContext.Hex()) subdir := filepath.Join(dir, localContext.Hex())
localInputs, err := cannon.FetchLocalInputsFromProposals(ctx, contract, l2Client, agreed, claimed) localInputs, err := cannon.FetchLocalInputsFromProposals(ctx, l1Head.Hash, l2Client, agreed, claimed)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err) return nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err)
} }
......
...@@ -12,10 +12,10 @@ var _ types.PrestateProvider = (*OutputPrestateProvider)(nil) ...@@ -12,10 +12,10 @@ var _ types.PrestateProvider = (*OutputPrestateProvider)(nil)
type OutputPrestateProvider struct { type OutputPrestateProvider struct {
prestateBlock uint64 prestateBlock uint64
rollupClient OutputRootProvider rollupClient OutputRollupClient
} }
func NewPrestateProvider(rollupClient OutputRootProvider, prestateBlock uint64) *OutputPrestateProvider { func NewPrestateProvider(rollupClient OutputRollupClient, prestateBlock uint64) *OutputPrestateProvider {
return &OutputPrestateProvider{ return &OutputPrestateProvider{
prestateBlock: prestateBlock, prestateBlock: prestateBlock,
rollupClient: rollupClient, rollupClient: rollupClient,
...@@ -27,9 +27,9 @@ func (o *OutputPrestateProvider) AbsolutePreStateCommitment(ctx context.Context) ...@@ -27,9 +27,9 @@ func (o *OutputPrestateProvider) AbsolutePreStateCommitment(ctx context.Context)
} }
func (o *OutputPrestateProvider) outputAtBlock(ctx context.Context, block uint64) (common.Hash, error) { func (o *OutputPrestateProvider) outputAtBlock(ctx context.Context, block uint64) (common.Hash, error) {
root, err := o.rollupClient.OutputAtBlock(ctx, block) output, err := o.rollupClient.OutputAtBlock(ctx, block)
if err != nil { if err != nil {
return common.Hash{}, fmt.Errorf("failed to fetch output at block %v: %w", block, err) return common.Hash{}, fmt.Errorf("failed to fetch output at block %v: %w", block, err)
} }
return root, nil return common.Hash(output.OutputRoot), nil
} }
...@@ -4,13 +4,12 @@ import ( ...@@ -4,13 +4,12 @@ import (
"context" "context"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs/source"
"github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func newOutputPrestateProvider(t *testing.T, prestateBlock uint64) (*OutputPrestateProvider, *stubRollupClient) { func newOutputPrestateProvider(t *testing.T, prestateBlock uint64) (*OutputPrestateProvider, *stubRollupClient) {
rollupClient := stubRollupClient{ rollupClient := &stubRollupClient{
outputs: map[uint64]*eth.OutputResponse{ outputs: map[uint64]*eth.OutputResponse{
prestateBlock: { prestateBlock: {
OutputRoot: eth.Bytes32(prestateOutputRoot), OutputRoot: eth.Bytes32(prestateOutputRoot),
...@@ -24,9 +23,9 @@ func newOutputPrestateProvider(t *testing.T, prestateBlock uint64) (*OutputPrest ...@@ -24,9 +23,9 @@ func newOutputPrestateProvider(t *testing.T, prestateBlock uint64) (*OutputPrest
}, },
} }
return &OutputPrestateProvider{ return &OutputPrestateProvider{
rollupClient: source.NewUnrestrictedOutputSource(&rollupClient), rollupClient: rollupClient,
prestateBlock: prestateBlock, prestateBlock: prestateBlock,
}, &rollupClient }, rollupClient
} }
func TestAbsolutePreStateCommitment(t *testing.T) { func TestAbsolutePreStateCommitment(t *testing.T) {
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -17,8 +18,9 @@ var ( ...@@ -17,8 +18,9 @@ var (
var _ types.TraceProvider = (*OutputTraceProvider)(nil) var _ types.TraceProvider = (*OutputTraceProvider)(nil)
type OutputRootProvider interface { type OutputRollupClient interface {
OutputAtBlock(ctx context.Context, blockNum uint64) (common.Hash, error) OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error)
SafeHeadAtL1Block(ctx context.Context, l1BlockNum uint64) (*eth.SafeHeadResponse, error)
} }
// OutputTraceProvider is a [types.TraceProvider] implementation that uses // OutputTraceProvider is a [types.TraceProvider] implementation that uses
...@@ -26,28 +28,33 @@ type OutputRootProvider interface { ...@@ -26,28 +28,33 @@ type OutputRootProvider interface {
type OutputTraceProvider struct { type OutputTraceProvider struct {
types.PrestateProvider types.PrestateProvider
logger log.Logger logger log.Logger
rollupProvider OutputRootProvider rollupProvider OutputRollupClient
prestateBlock uint64 prestateBlock uint64
poststateBlock uint64 poststateBlock uint64
l1Head eth.BlockID
gameDepth types.Depth gameDepth types.Depth
} }
func NewTraceProvider(logger log.Logger, prestateProvider types.PrestateProvider, rollupProvider OutputRootProvider, gameDepth types.Depth, prestateBlock, poststateBlock uint64) *OutputTraceProvider { func NewTraceProvider(logger log.Logger, prestateProvider types.PrestateProvider, rollupProvider OutputRollupClient, l1Head eth.BlockID, gameDepth types.Depth, prestateBlock, poststateBlock uint64) *OutputTraceProvider {
return &OutputTraceProvider{ return &OutputTraceProvider{
PrestateProvider: prestateProvider, PrestateProvider: prestateProvider,
logger: logger, logger: logger,
rollupProvider: rollupProvider, rollupProvider: rollupProvider,
prestateBlock: prestateBlock, prestateBlock: prestateBlock,
poststateBlock: poststateBlock, poststateBlock: poststateBlock,
l1Head: l1Head,
gameDepth: gameDepth, gameDepth: gameDepth,
} }
} }
func (o *OutputTraceProvider) BlockNumber(pos types.Position) (uint64, error) { // ClaimedBlockNumber returns the block number for a position restricted only by the claimed L2 block number.
// The returned block number may be after the safe head reached by processing batch data up to the game's L1 head
func (o *OutputTraceProvider) ClaimedBlockNumber(pos types.Position) (uint64, error) {
traceIndex := pos.TraceIndex(o.gameDepth) traceIndex := pos.TraceIndex(o.gameDepth)
if !traceIndex.IsUint64() { if !traceIndex.IsUint64() {
return 0, fmt.Errorf("%w: %v", ErrIndexTooBig, traceIndex) return 0, fmt.Errorf("%w: %v", ErrIndexTooBig, traceIndex)
} }
outputBlock := traceIndex.Uint64() + o.prestateBlock + 1 outputBlock := traceIndex.Uint64() + o.prestateBlock + 1
if outputBlock > o.poststateBlock { if outputBlock > o.poststateBlock {
outputBlock = o.poststateBlock outputBlock = o.poststateBlock
...@@ -55,8 +62,27 @@ func (o *OutputTraceProvider) BlockNumber(pos types.Position) (uint64, error) { ...@@ -55,8 +62,27 @@ func (o *OutputTraceProvider) BlockNumber(pos types.Position) (uint64, error) {
return outputBlock, nil return outputBlock, nil
} }
// HonestBlockNumber returns the block number for a position in the game restricted to the minimum of the claimed L2
// block number or the safe head reached by processing batch data up to the game's L1 head.
// This is used when posting honest output roots to ensure that only roots supported by L1 data are posted
func (o *OutputTraceProvider) HonestBlockNumber(ctx context.Context, pos types.Position) (uint64, error) {
outputBlock, err := o.ClaimedBlockNumber(pos)
if err != nil {
return 0, err
}
resp, err := o.rollupProvider.SafeHeadAtL1Block(ctx, o.l1Head.Number)
if err != nil {
return 0, fmt.Errorf("failed to get safe head at L1 block %v: %w", o.l1Head, err)
}
maxSafeHead := resp.SafeHead.Number
if outputBlock > maxSafeHead {
outputBlock = maxSafeHead
}
return outputBlock, nil
}
func (o *OutputTraceProvider) Get(ctx context.Context, pos types.Position) (common.Hash, error) { func (o *OutputTraceProvider) Get(ctx context.Context, pos types.Position) (common.Hash, error) {
outputBlock, err := o.BlockNumber(pos) outputBlock, err := o.HonestBlockNumber(ctx, pos)
if err != nil { if err != nil {
return common.Hash{}, err return common.Hash{}, err
} }
...@@ -69,9 +95,9 @@ func (o *OutputTraceProvider) GetStepData(_ context.Context, _ types.Position) ( ...@@ -69,9 +95,9 @@ func (o *OutputTraceProvider) GetStepData(_ context.Context, _ types.Position) (
} }
func (o *OutputTraceProvider) outputAtBlock(ctx context.Context, block uint64) (common.Hash, error) { func (o *OutputTraceProvider) outputAtBlock(ctx context.Context, block uint64) (common.Hash, error) {
root, err := o.rollupProvider.OutputAtBlock(ctx, block) output, err := o.rollupProvider.OutputAtBlock(ctx, block)
if err != nil { if err != nil {
return common.Hash{}, fmt.Errorf("failed to fetch output at block %v: %w", block, err) return common.Hash{}, fmt.Errorf("failed to fetch output at block %v: %w", block, err)
} }
return root, err return common.Hash(output.OutputRoot), nil
} }
...@@ -4,10 +4,10 @@ import ( ...@@ -4,10 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"math/big" "math/big"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs/source"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testlog"
...@@ -63,26 +63,74 @@ func TestGet(t *testing.T) { ...@@ -63,26 +63,74 @@ func TestGet(t *testing.T) {
}) })
} }
func TestGetBlockNumber(t *testing.T) { func TestHonestBlockNumber(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
pos types.Position pos types.Position
expected uint64 expected uint64
maxSafeHead uint64
}{ }{
{"FirstBlockAfterPrestate", types.NewPosition(gameDepth, big.NewInt(0)), prestateBlock + 1}, {"FirstBlockAfterPrestate", types.NewPosition(gameDepth, big.NewInt(0)), prestateBlock + 1, math.MaxUint64},
{"PostStateBlock", types.NewPositionFromGIndex(big.NewInt(228)), poststateBlock}, {"PostStateBlock", types.NewPositionFromGIndex(big.NewInt(228)), poststateBlock, math.MaxUint64},
{"AfterPostStateBlock", types.NewPositionFromGIndex(big.NewInt(229)), poststateBlock}, {"AfterPostStateBlock", types.NewPositionFromGIndex(big.NewInt(229)), poststateBlock, math.MaxUint64},
{"Root", types.NewPositionFromGIndex(big.NewInt(1)), poststateBlock}, {"Root", types.NewPositionFromGIndex(big.NewInt(1)), poststateBlock, math.MaxUint64},
{"MiddleNode1", types.NewPosition(gameDepth-1, big.NewInt(2)), 106}, {"MiddleNode1", types.NewPosition(gameDepth-1, big.NewInt(2)), 106, math.MaxUint64},
{"MiddleNode2", types.NewPosition(gameDepth-1, big.NewInt(3)), 108}, {"MiddleNode2", types.NewPosition(gameDepth-1, big.NewInt(3)), 108, math.MaxUint64},
{"Leaf1", types.NewPosition(gameDepth, big.NewInt(1)), prestateBlock + 2}, {"Leaf1", types.NewPosition(gameDepth, big.NewInt(1)), prestateBlock + 2, math.MaxUint64},
{"Leaf2", types.NewPosition(gameDepth, big.NewInt(2)), prestateBlock + 3}, {"Leaf2", types.NewPosition(gameDepth, big.NewInt(2)), prestateBlock + 3, math.MaxUint64},
{"RestrictedHead-UnderLimit", types.NewPosition(gameDepth, big.NewInt(48)), prestateBlock + 49, prestateBlock + 50},
{"RestrictedHead-EqualLimit", types.NewPosition(gameDepth, big.NewInt(49)), prestateBlock + 50, prestateBlock + 50},
{"RestrictedHead-OverLimit", types.NewPosition(gameDepth, big.NewInt(50)), prestateBlock + 50, prestateBlock + 50},
{"RestrictedHead-PastPostState", types.NewPosition(gameDepth, big.NewInt(1000)), prestateBlock + 50, prestateBlock + 50},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
provider, stubRollupClient := setupWithTestData(t, prestateBlock, poststateBlock)
stubRollupClient.maxSafeHead = test.maxSafeHead
actual, err := provider.HonestBlockNumber(context.Background(), test.pos)
require.NoError(t, err)
require.Equal(t, test.expected, actual)
})
}
t.Run("ErrorsTraceIndexOutOfBounds", func(t *testing.T) {
deepGame := types.Depth(164)
provider, _ := setupWithTestData(t, prestateBlock, poststateBlock, deepGame)
pos := types.NewPosition(0, big.NewInt(0))
_, err := provider.HonestBlockNumber(context.Background(), pos)
require.ErrorIs(t, err, ErrIndexTooBig)
})
}
func TestClaimedBlockNumber(t *testing.T) {
tests := []struct {
name string
pos types.Position
expected uint64
maxSafeHead uint64
}{
{"FirstBlockAfterPrestate", types.NewPosition(gameDepth, big.NewInt(0)), prestateBlock + 1, math.MaxUint64},
{"PostStateBlock", types.NewPositionFromGIndex(big.NewInt(228)), poststateBlock, math.MaxUint64},
{"AfterPostStateBlock", types.NewPositionFromGIndex(big.NewInt(229)), poststateBlock, math.MaxUint64},
{"Root", types.NewPositionFromGIndex(big.NewInt(1)), poststateBlock, math.MaxUint64},
{"MiddleNode1", types.NewPosition(gameDepth-1, big.NewInt(2)), 106, math.MaxUint64},
{"MiddleNode2", types.NewPosition(gameDepth-1, big.NewInt(3)), 108, math.MaxUint64},
{"Leaf1", types.NewPosition(gameDepth, big.NewInt(1)), prestateBlock + 2, math.MaxUint64},
{"Leaf2", types.NewPosition(gameDepth, big.NewInt(2)), prestateBlock + 3, math.MaxUint64},
{"RestrictedHead-UnderLimit", types.NewPosition(gameDepth, big.NewInt(48)), prestateBlock + 49, prestateBlock + 50},
{"RestrictedHead-EqualLimit", types.NewPosition(gameDepth, big.NewInt(49)), prestateBlock + 50, prestateBlock + 50},
{"RestrictedHead-OverLimit", types.NewPosition(gameDepth, big.NewInt(50)), prestateBlock + 51, prestateBlock + 50},
{"RestrictedHead-PastPostState", types.NewPosition(gameDepth, big.NewInt(300)), poststateBlock, prestateBlock + 50},
} }
for _, test := range tests { for _, test := range tests {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
provider, _ := setupWithTestData(t, prestateBlock, poststateBlock) provider, stubRollupClient := setupWithTestData(t, prestateBlock, poststateBlock)
actual, err := provider.BlockNumber(test.pos) stubRollupClient.maxSafeHead = test.maxSafeHead
actual, err := provider.ClaimedBlockNumber(test.pos)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, test.expected, actual) require.Equal(t, test.expected, actual)
}) })
...@@ -92,7 +140,7 @@ func TestGetBlockNumber(t *testing.T) { ...@@ -92,7 +140,7 @@ func TestGetBlockNumber(t *testing.T) {
deepGame := types.Depth(164) deepGame := types.Depth(164)
provider, _ := setupWithTestData(t, prestateBlock, poststateBlock, deepGame) provider, _ := setupWithTestData(t, prestateBlock, poststateBlock, deepGame)
pos := types.NewPosition(0, big.NewInt(0)) pos := types.NewPosition(0, big.NewInt(0))
_, err := provider.BlockNumber(pos) _, err := provider.ClaimedBlockNumber(pos)
require.ErrorIs(t, err, ErrIndexTooBig) require.ErrorIs(t, err, ErrIndexTooBig)
}) })
} }
...@@ -104,7 +152,7 @@ func TestGetStepData(t *testing.T) { ...@@ -104,7 +152,7 @@ func TestGetStepData(t *testing.T) {
} }
func setupWithTestData(t *testing.T, prestateBlock, poststateBlock uint64, customGameDepth ...types.Depth) (*OutputTraceProvider, *stubRollupClient) { func setupWithTestData(t *testing.T, prestateBlock, poststateBlock uint64, customGameDepth ...types.Depth) (*OutputTraceProvider, *stubRollupClient) {
rollupClient := stubRollupClient{ rollupClient := &stubRollupClient{
outputs: map[uint64]*eth.OutputResponse{ outputs: map[uint64]*eth.OutputResponse{
prestateBlock: { prestateBlock: {
OutputRoot: eth.Bytes32(prestateOutputRoot), OutputRoot: eth.Bytes32(prestateOutputRoot),
...@@ -116,6 +164,7 @@ func setupWithTestData(t *testing.T, prestateBlock, poststateBlock uint64, custo ...@@ -116,6 +164,7 @@ func setupWithTestData(t *testing.T, prestateBlock, poststateBlock uint64, custo
OutputRoot: eth.Bytes32(poststateOutputRoot), OutputRoot: eth.Bytes32(poststateOutputRoot),
}, },
}, },
maxSafeHead: math.MaxUint64,
} }
inputGameDepth := gameDepth inputGameDepth := gameDepth
if len(customGameDepth) > 0 { if len(customGameDepth) > 0 {
...@@ -123,16 +172,17 @@ func setupWithTestData(t *testing.T, prestateBlock, poststateBlock uint64, custo ...@@ -123,16 +172,17 @@ func setupWithTestData(t *testing.T, prestateBlock, poststateBlock uint64, custo
} }
return &OutputTraceProvider{ return &OutputTraceProvider{
logger: testlog.Logger(t, log.LevelInfo), logger: testlog.Logger(t, log.LevelInfo),
rollupProvider: source.NewUnrestrictedOutputSource(&rollupClient), rollupProvider: rollupClient,
prestateBlock: prestateBlock, prestateBlock: prestateBlock,
poststateBlock: poststateBlock, poststateBlock: poststateBlock,
gameDepth: inputGameDepth, gameDepth: inputGameDepth,
}, &rollupClient }, rollupClient
} }
type stubRollupClient struct { type stubRollupClient struct {
errorsOnPrestateFetch bool errorsOnPrestateFetch bool
outputs map[uint64]*eth.OutputResponse outputs map[uint64]*eth.OutputResponse
maxSafeHead uint64
} }
func (s *stubRollupClient) OutputAtBlock(_ context.Context, blockNum uint64) (*eth.OutputResponse, error) { func (s *stubRollupClient) OutputAtBlock(_ context.Context, blockNum uint64) (*eth.OutputResponse, error) {
...@@ -144,5 +194,10 @@ func (s *stubRollupClient) OutputAtBlock(_ context.Context, blockNum uint64) (*e ...@@ -144,5 +194,10 @@ func (s *stubRollupClient) OutputAtBlock(_ context.Context, blockNum uint64) (*e
} }
func (s *stubRollupClient) SafeHeadAtL1Block(_ context.Context, l1BlockNum uint64) (*eth.SafeHeadResponse, error) { func (s *stubRollupClient) SafeHeadAtL1Block(_ context.Context, l1BlockNum uint64) (*eth.SafeHeadResponse, error) {
return nil, errors.New("not supported") return &eth.SafeHeadResponse{
SafeHead: eth.BlockID{
Number: s.maxSafeHead,
Hash: common.Hash{0x11},
},
}, nil
} }
package source
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
type OutputRollupClient interface {
OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error)
SafeHeadAtL1Block(ctx context.Context, l1BlockNum uint64) (*eth.SafeHeadResponse, error)
}
type L1HeaderSource interface {
HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error)
}
type OutputSourceCreator struct {
log log.Logger
rollupClient OutputRollupClient
l1Client L1HeaderSource
}
func NewOutputSourceCreator(logger log.Logger, rollupClient OutputRollupClient, l1Client L1HeaderSource) *OutputSourceCreator {
return &OutputSourceCreator{
log: logger,
rollupClient: rollupClient,
l1Client: l1Client,
}
}
func (l *OutputSourceCreator) ForL1Head(ctx context.Context, l1Head common.Hash) (*RestrictedOutputSource, error) {
head, err := l.l1Client.HeaderByHash(ctx, l1Head)
if err != nil {
return nil, fmt.Errorf("failed to get L1 head %v: %w", l1Head, err)
}
return NewRestrictedOutputSource(l.rollupClient, eth.HeaderBlockID(head)), nil
}
package source
import (
"context"
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
)
var ErrExceedsL1Head = errors.New("output root beyond safe head for L1 head")
type RestrictedOutputSource struct {
rollupClient OutputRollupClient
unrestricted *UnrestrictedOutputSource
l1Head eth.BlockID
}
func NewRestrictedOutputSource(rollupClient OutputRollupClient, l1Head eth.BlockID) *RestrictedOutputSource {
return &RestrictedOutputSource{
rollupClient: rollupClient,
unrestricted: NewUnrestrictedOutputSource(rollupClient),
l1Head: l1Head,
}
}
func (l *RestrictedOutputSource) OutputAtBlock(ctx context.Context, blockNum uint64) (common.Hash, error) {
resp, err := l.rollupClient.SafeHeadAtL1Block(ctx, l.l1Head.Number)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to get safe head at L1 block %v: %w", l.l1Head, err)
}
maxSafeHead := resp.SafeHead.Number
if blockNum > maxSafeHead {
return common.Hash{}, fmt.Errorf("%w, requested: %v max: %v", ErrExceedsL1Head, blockNum, maxSafeHead)
}
return l.unrestricted.OutputAtBlock(ctx, blockNum)
}
package source
import (
"context"
"errors"
"testing"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestRestrictedOutputLoader(t *testing.T) {
tests := []struct {
name string
maxSafeHead uint64
blockNum uint64
expectedErr error
}{
{
name: "GenesisNotRestricted",
maxSafeHead: 1000,
blockNum: 0,
expectedErr: nil,
},
{
name: "BothAtGenesis",
maxSafeHead: 0,
blockNum: 0,
expectedErr: nil,
},
{
name: "RestrictedToGenesis",
maxSafeHead: 0,
blockNum: 1,
expectedErr: ErrExceedsL1Head,
},
{
name: "JustBelowMaxHead",
maxSafeHead: 1000,
blockNum: 999,
expectedErr: nil,
},
{
name: "EqualMaxHead",
maxSafeHead: 1000,
blockNum: 1000,
expectedErr: nil,
},
{
name: "JustAboveMaxHead",
maxSafeHead: 1000,
blockNum: 1001,
expectedErr: ErrExceedsL1Head,
},
{
name: "WellAboveMaxHead",
maxSafeHead: 1000,
blockNum: 99001,
expectedErr: ErrExceedsL1Head,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
l1Head := eth.BlockID{Number: 3428}
rollupClient := &stubOutputRollupClient{
safeHead: test.maxSafeHead,
}
loader := NewRestrictedOutputSource(rollupClient, l1Head)
result, err := loader.OutputAtBlock(context.Background(), test.blockNum)
if test.expectedErr == nil {
require.NoError(t, err)
require.Equal(t, common.Hash{byte(test.blockNum)}, result)
} else {
require.ErrorIs(t, err, test.expectedErr)
}
require.Equal(t, l1Head.Number, rollupClient.requestedL1BlockNum)
})
}
}
func TestRestrictedOutputLoader_GetOutputRootErrors(t *testing.T) {
expectedErr := errors.New("boom")
client := &stubOutputRollupClient{outputErr: expectedErr, safeHead: 884}
loader := NewRestrictedOutputSource(client, eth.BlockID{Number: 1234})
_, err := loader.OutputAtBlock(context.Background(), 4)
require.ErrorIs(t, err, expectedErr)
}
func TestRestrictedOutputLoader_SafeHeadAtL1BlockErrors(t *testing.T) {
expectedErr := errors.New("boom")
client := &stubOutputRollupClient{safeHeadErr: expectedErr, safeHead: 884}
loader := NewRestrictedOutputSource(client, eth.BlockID{Number: 1234})
_, err := loader.OutputAtBlock(context.Background(), 4)
require.ErrorIs(t, err, expectedErr)
}
type stubOutputRollupClient struct {
outputErr error
safeHeadErr error
safeHead uint64
requestedL1BlockNum uint64
}
func (s *stubOutputRollupClient) OutputAtBlock(_ context.Context, blockNum uint64) (*eth.OutputResponse, error) {
if s.outputErr != nil {
return nil, s.outputErr
}
return &eth.OutputResponse{
OutputRoot: eth.Bytes32{byte(blockNum)},
}, nil
}
func (s *stubOutputRollupClient) SafeHeadAtL1Block(_ context.Context, l1BlockNum uint64) (*eth.SafeHeadResponse, error) {
s.requestedL1BlockNum = l1BlockNum
if s.safeHeadErr != nil {
return nil, s.safeHeadErr
}
return &eth.SafeHeadResponse{
L1Block: eth.BlockID{
Hash: common.Hash{0x11},
Number: 4824,
},
SafeHead: eth.BlockID{
Hash: common.Hash{0x22},
Number: s.safeHead,
},
}, nil
}
package source
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/common"
)
type UnrestrictedOutputSource struct {
rollupClient OutputRollupClient
}
func NewUnrestrictedOutputSource(rollupClient OutputRollupClient) *UnrestrictedOutputSource {
return &UnrestrictedOutputSource{rollupClient: rollupClient}
}
func (l *UnrestrictedOutputSource) OutputAtBlock(ctx context.Context, blockNum uint64) (common.Hash, error) {
output, err := l.rollupClient.OutputAtBlock(ctx, blockNum)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to fetch output at block %v: %w", blockNum, err)
}
return common.Hash(output.OutputRoot), nil
}
...@@ -38,7 +38,7 @@ func FetchProposals(ctx context.Context, topProvider *OutputTraceProvider, pre t ...@@ -38,7 +38,7 @@ func FetchProposals(ctx context.Context, topProvider *OutputTraceProvider, pre t
OutputRoot: prestateRoot, OutputRoot: prestateRoot,
} }
} else { } else {
preBlockNum, err := topProvider.BlockNumber(pre.Position) preBlockNum, err := topProvider.HonestBlockNumber(ctx, pre.Position)
if err != nil { if err != nil {
return contracts.Proposal{}, contracts.Proposal{}, fmt.Errorf("unable to calculate pre-claim block number: %w", err) return contracts.Proposal{}, contracts.Proposal{}, fmt.Errorf("unable to calculate pre-claim block number: %w", err)
} }
...@@ -47,7 +47,7 @@ func FetchProposals(ctx context.Context, topProvider *OutputTraceProvider, pre t ...@@ -47,7 +47,7 @@ func FetchProposals(ctx context.Context, topProvider *OutputTraceProvider, pre t
OutputRoot: pre.Value, OutputRoot: pre.Value,
} }
} }
postBlockNum, err := topProvider.BlockNumber(post.Position) postBlockNum, err := topProvider.ClaimedBlockNumber(post.Position)
if err != nil { if err != nil {
return contracts.Proposal{}, contracts.Proposal{}, fmt.Errorf("unable to calculate post-claim block number: %w", err) return contracts.Proposal{}, contracts.Proposal{}, fmt.Errorf("unable to calculate post-claim block number: %w", err)
} }
......
...@@ -8,7 +8,6 @@ import ( ...@@ -8,7 +8,6 @@ import (
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs/source"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/split" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/split"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/eth"
...@@ -125,17 +124,22 @@ func setupAdapterTest(t *testing.T, topDepth types.Depth) (split.ProviderCreator ...@@ -125,17 +124,22 @@ func setupAdapterTest(t *testing.T, topDepth types.Depth) (split.ProviderCreator
prestateBlock := uint64(20) prestateBlock := uint64(20)
poststateBlock := uint64(40) poststateBlock := uint64(40)
creator := &capturingCreator{} creator := &capturingCreator{}
l1Head := eth.BlockID{
Hash: common.Hash{0x11, 0x11},
Number: 11,
}
rollupClient := &stubRollupClient{ rollupClient := &stubRollupClient{
outputs: map[uint64]*eth.OutputResponse{ outputs: map[uint64]*eth.OutputResponse{
prestateBlock: { prestateBlock: {
OutputRoot: eth.Bytes32(prestateOutputRoot), OutputRoot: eth.Bytes32(prestateOutputRoot),
}, },
}, },
maxSafeHead: math.MaxUint64,
} }
prestateProvider := &stubPrestateProvider{ prestateProvider := &stubPrestateProvider{
absolutePrestate: prestateOutputRoot, absolutePrestate: prestateOutputRoot,
} }
topProvider := NewTraceProvider(testlog.Logger(t, log.LevelInfo), prestateProvider, source.NewUnrestrictedOutputSource(rollupClient), topDepth, prestateBlock, poststateBlock) topProvider := NewTraceProvider(testlog.Logger(t, log.LevelInfo), prestateProvider, rollupClient, l1Head, topDepth, prestateBlock, poststateBlock)
adapter := OutputRootSplitAdapter(topProvider, creator.Create) adapter := OutputRootSplitAdapter(topProvider, creator.Create)
return adapter, creator return adapter, creator
} }
......
...@@ -12,7 +12,6 @@ import ( ...@@ -12,7 +12,6 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer" "github.com/ethereum-optimism/optimism/op-chain-ops/deployer"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs/source"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame/preimage" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame/preimage"
...@@ -20,6 +19,7 @@ import ( ...@@ -20,6 +19,7 @@ import (
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
...@@ -59,6 +59,7 @@ func (s Status) String() string { ...@@ -59,6 +59,7 @@ func (s Status) String() string {
} }
type gameCfg struct { type gameCfg struct {
allowFuture bool
allowUnsafe bool allowUnsafe bool
} }
type GameOpt interface { type GameOpt interface {
...@@ -76,6 +77,12 @@ func WithUnsafeProposal() GameOpt { ...@@ -76,6 +77,12 @@ func WithUnsafeProposal() GameOpt {
}) })
} }
func WithFutureProposal() GameOpt {
return gameOptFn(func(c *gameCfg) {
c.allowFuture = true
})
}
type DisputeSystem interface { type DisputeSystem interface {
L1BeaconEndpoint() string L1BeaconEndpoint() string
NodeEndpoint(name string) string NodeEndpoint(name string) string
...@@ -177,15 +184,17 @@ func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, l2Node string ...@@ -177,15 +184,17 @@ func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, l2Node string
game, err := bindings.NewFaultDisputeGame(createdEvent.DisputeProxy, h.client) game, err := bindings.NewFaultDisputeGame(createdEvent.DisputeProxy, h.client)
h.require.NoError(err) h.require.NoError(err)
prestateBlock, err := game.GenesisBlockNumber(&bind.CallOpts{Context: ctx}) callOpts := &bind.CallOpts{Context: ctx}
prestateBlock, err := game.GenesisBlockNumber(callOpts)
h.require.NoError(err, "Failed to load genesis block number") h.require.NoError(err, "Failed to load genesis block number")
poststateBlock, err := game.L2BlockNumber(&bind.CallOpts{Context: ctx}) poststateBlock, err := game.L2BlockNumber(callOpts)
h.require.NoError(err, "Failed to load l2 block number") h.require.NoError(err, "Failed to load l2 block number")
splitDepth, err := game.SplitDepth(&bind.CallOpts{Context: ctx}) splitDepth, err := game.SplitDepth(callOpts)
h.require.NoError(err, "Failed to load split depth") h.require.NoError(err, "Failed to load split depth")
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient) l1Head := h.getL1Head(ctx, game)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock.Uint64())
provider := outputs.NewTraceProvider(logger, prestateProvider, outputRootProvider, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64()) prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock.Uint64())
provider := outputs.NewTraceProvider(logger, prestateProvider, rollupClient, l1Head, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
return &OutputCannonGameHelper{ return &OutputCannonGameHelper{
OutputGameHelper: OutputGameHelper{ OutputGameHelper: OutputGameHelper{
...@@ -202,6 +211,15 @@ func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, l2Node string ...@@ -202,6 +211,15 @@ func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, l2Node string
} }
} }
func (h *FactoryHelper) getL1Head(ctx context.Context, game *bindings.FaultDisputeGame) eth.BlockID {
l1HeadHash, err := game.L1Head(&bind.CallOpts{Context: ctx})
h.require.NoError(err, "Failed to load L1 head")
l1Header, err := h.client.HeaderByHash(ctx, l1HeadHash)
h.require.NoError(err, "Failed to load L1 header")
l1Head := eth.HeaderBlockID(l1Header)
return l1Head
}
func (h *FactoryHelper) StartOutputAlphabetGameWithCorrectRoot(ctx context.Context, l2Node string, l2BlockNumber uint64, opts ...GameOpt) *OutputAlphabetGameHelper { func (h *FactoryHelper) StartOutputAlphabetGameWithCorrectRoot(ctx context.Context, l2Node string, l2BlockNumber uint64, opts ...GameOpt) *OutputAlphabetGameHelper {
cfg := newGameCfg(opts...) cfg := newGameCfg(opts...)
h.waitForBlock(l2Node, l2BlockNumber, cfg) h.waitForBlock(l2Node, l2BlockNumber, cfg)
...@@ -232,15 +250,17 @@ func (h *FactoryHelper) StartOutputAlphabetGame(ctx context.Context, l2Node stri ...@@ -232,15 +250,17 @@ func (h *FactoryHelper) StartOutputAlphabetGame(ctx context.Context, l2Node stri
game, err := bindings.NewFaultDisputeGame(createdEvent.DisputeProxy, h.client) game, err := bindings.NewFaultDisputeGame(createdEvent.DisputeProxy, h.client)
h.require.NoError(err) h.require.NoError(err)
prestateBlock, err := game.GenesisBlockNumber(&bind.CallOpts{Context: ctx}) callOpts := &bind.CallOpts{Context: ctx}
prestateBlock, err := game.GenesisBlockNumber(callOpts)
h.require.NoError(err, "Failed to load genesis block number") h.require.NoError(err, "Failed to load genesis block number")
poststateBlock, err := game.L2BlockNumber(&bind.CallOpts{Context: ctx}) poststateBlock, err := game.L2BlockNumber(callOpts)
h.require.NoError(err, "Failed to load l2 block number") h.require.NoError(err, "Failed to load l2 block number")
splitDepth, err := game.SplitDepth(&bind.CallOpts{Context: ctx}) splitDepth, err := game.SplitDepth(callOpts)
h.require.NoError(err, "Failed to load split depth") h.require.NoError(err, "Failed to load split depth")
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient) l1Head := h.getL1Head(ctx, game)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock.Uint64()) prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock.Uint64())
provider := outputs.NewTraceProvider(logger, prestateProvider, outputRootProvider, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
provider := outputs.NewTraceProvider(logger, prestateProvider, rollupClient, l1Head, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
return &OutputAlphabetGameHelper{ return &OutputAlphabetGameHelper{
OutputGameHelper: OutputGameHelper{ OutputGameHelper: OutputGameHelper{
...@@ -266,6 +286,11 @@ func (h *FactoryHelper) createBisectionGameExtraData(l2Node string, l2BlockNumbe ...@@ -266,6 +286,11 @@ func (h *FactoryHelper) createBisectionGameExtraData(l2Node string, l2BlockNumbe
} }
func (h *FactoryHelper) waitForBlock(l2Node string, l2BlockNumber uint64, cfg *gameCfg) { func (h *FactoryHelper) waitForBlock(l2Node string, l2BlockNumber uint64, cfg *gameCfg) {
if cfg.allowFuture {
// Proposing a block that doesn't exist yet, so don't perform any checks
return
}
l2Client := h.system.NodeClient(l2Node) l2Client := h.system.NodeClient(l2Node)
if cfg.allowUnsafe { if cfg.allowUnsafe {
_, err := geth.WaitForBlock(new(big.Int).SetUint64(l2BlockNumber), l2Client, 1*time.Minute) _, err := geth.WaitForBlock(new(big.Int).SetUint64(l2BlockNumber), l2Client, 1*time.Minute)
......
...@@ -5,7 +5,6 @@ import ( ...@@ -5,7 +5,6 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs/source"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
...@@ -44,10 +43,10 @@ func (g *OutputAlphabetGameHelper) CreateHonestActor(ctx context.Context, l2Node ...@@ -44,10 +43,10 @@ func (g *OutputAlphabetGameHelper) CreateHonestActor(ctx context.Context, l2Node
prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx) prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx)
g.require.NoError(err, "Get block range") g.require.NoError(err, "Get block range")
splitDepth := g.SplitDepth(ctx) splitDepth := g.SplitDepth(ctx)
l1Head := g.getL1Head(ctx)
rollupClient := g.system.RollupClient(l2Node) rollupClient := g.system.RollupClient(l2Node)
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient) prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock) correctTrace, err := outputs.NewOutputAlphabetTraceAccessor(logger, metrics.NoopMetrics, prestateProvider, rollupClient, l1Head, splitDepth, prestateBlock, poststateBlock)
correctTrace, err := outputs.NewOutputAlphabetTraceAccessor(logger, metrics.NoopMetrics, prestateProvider, outputRootProvider, splitDepth, prestateBlock, poststateBlock)
g.require.NoError(err, "Create trace accessor") g.require.NoError(err, "Create trace accessor")
return &OutputHonestHelper{ return &OutputHonestHelper{
t: g.t, t: g.t,
......
...@@ -11,7 +11,6 @@ import ( ...@@ -11,7 +11,6 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs/source"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/split" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/split"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
...@@ -64,10 +63,10 @@ func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node s ...@@ -64,10 +63,10 @@ func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node s
dir := filepath.Join(cfg.Datadir, "honest") dir := filepath.Join(cfg.Datadir, "honest")
splitDepth := g.SplitDepth(ctx) splitDepth := g.SplitDepth(ctx)
rollupClient := g.system.RollupClient(l2Node) rollupClient := g.system.RollupClient(l2Node)
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient) prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock) l1Head := g.getL1Head(ctx)
accessor, err := outputs.NewOutputCannonTraceAccessor( accessor, err := outputs.NewOutputCannonTraceAccessor(
logger, metrics.NoopMetrics, cfg, l2Client, contract, prestateProvider, outputRootProvider, dir, splitDepth, prestateBlock, poststateBlock) logger, metrics.NoopMetrics, cfg, l2Client, prestateProvider, rollupClient, dir, l1Head, splitDepth, prestateBlock, poststateBlock)
g.require.NoError(err, "Failed to create output cannon trace accessor") g.require.NoError(err, "Failed to create output cannon trace accessor")
return &OutputHonestHelper{ return &OutputHonestHelper{
t: g.t, t: g.t,
...@@ -231,15 +230,15 @@ func (g *OutputCannonGameHelper) createCannonTraceProvider(ctx context.Context, ...@@ -231,15 +230,15 @@ func (g *OutputCannonGameHelper) createCannonTraceProvider(ctx context.Context,
prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx) prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx)
g.require.NoError(err, "Failed to load block range") g.require.NoError(err, "Failed to load block range")
rollupClient := g.system.RollupClient(l2Node) rollupClient := g.system.RollupClient(l2Node)
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient) prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock) l1Head := g.getL1Head(ctx)
outputProvider := outputs.NewTraceProvider(logger, prestateProvider, outputRootProvider, splitDepth, prestateBlock, poststateBlock) outputProvider := outputs.NewTraceProvider(logger, prestateProvider, rollupClient, l1Head, splitDepth, prestateBlock, poststateBlock)
selector := split.NewSplitProviderSelector(outputProvider, splitDepth, func(ctx context.Context, depth types.Depth, pre types.Claim, post types.Claim) (types.TraceProvider, error) { selector := split.NewSplitProviderSelector(outputProvider, splitDepth, func(ctx context.Context, depth types.Depth, pre types.Claim, post types.Claim) (types.TraceProvider, error) {
agreed, disputed, err := outputs.FetchProposals(ctx, outputProvider, pre, post) agreed, disputed, err := outputs.FetchProposals(ctx, outputProvider, pre, post)
g.require.NoError(err) g.require.NoError(err)
g.t.Logf("Using trace between blocks %v and %v\n", agreed.L2BlockNumber, disputed.L2BlockNumber) g.t.Logf("Using trace between blocks %v and %v\n", agreed.L2BlockNumber, disputed.L2BlockNumber)
localInputs, err := cannon.FetchLocalInputsFromProposals(ctx, contract, l2Client, agreed, disputed) localInputs, err := cannon.FetchLocalInputsFromProposals(ctx, l1Head.Hash, l2Client, agreed, disputed)
g.require.NoError(err, "Failed to fetch local inputs") g.require.NoError(err, "Failed to fetch local inputs")
localContext := outputs.CreateLocalContext(pre, post) localContext := outputs.CreateLocalContext(pre, post)
dir := filepath.Join(cfg.Datadir, "cannon-trace") dir := filepath.Join(cfg.Datadir, "cannon-trace")
......
...@@ -16,6 +16,7 @@ import ( ...@@ -16,6 +16,7 @@ import (
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types" keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
preimage "github.com/ethereum-optimism/optimism/op-preimage" preimage "github.com/ethereum-optimism/optimism/op-preimage"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -109,7 +110,7 @@ func (g *OutputGameHelper) DisputeBlock(ctx context.Context, disputeBlockNum uin ...@@ -109,7 +110,7 @@ func (g *OutputGameHelper) DisputeBlock(ctx context.Context, disputeBlockNum uin
} }
pos := types.NewPositionFromGIndex(big.NewInt(1)) pos := types.NewPositionFromGIndex(big.NewInt(1))
getClaimValue := func(parentClaim *ClaimHelper, claimPos types.Position) common.Hash { getClaimValue := func(parentClaim *ClaimHelper, claimPos types.Position) common.Hash {
claimBlockNum, err := g.correctOutputProvider.BlockNumber(claimPos) claimBlockNum, err := g.correctOutputProvider.ClaimedBlockNumber(claimPos)
g.require.NoError(err, "failed to calculate claim block number") g.require.NoError(err, "failed to calculate claim block number")
if claimBlockNum < disputeBlockNum { if claimBlockNum < disputeBlockNum {
// Use the correct output root for all claims prior to the dispute block number // Use the correct output root for all claims prior to the dispute block number
...@@ -127,7 +128,7 @@ func (g *OutputGameHelper) DisputeBlock(ctx context.Context, disputeBlockNum uin ...@@ -127,7 +128,7 @@ func (g *OutputGameHelper) DisputeBlock(ctx context.Context, disputeBlockNum uin
claim := g.RootClaim(ctx) claim := g.RootClaim(ctx)
for !claim.IsOutputRootLeaf(ctx) { for !claim.IsOutputRootLeaf(ctx) {
parentClaimBlockNum, err := g.correctOutputProvider.BlockNumber(pos) parentClaimBlockNum, err := g.correctOutputProvider.ClaimedBlockNumber(pos)
g.require.NoError(err, "failed to calculate parent claim block number") g.require.NoError(err, "failed to calculate parent claim block number")
if parentClaimBlockNum >= disputeBlockNum { if parentClaimBlockNum >= disputeBlockNum {
pos = pos.Attack() pos = pos.Attack()
...@@ -671,7 +672,7 @@ func (g *OutputGameHelper) gameData(ctx context.Context) string { ...@@ -671,7 +672,7 @@ func (g *OutputGameHelper) gameData(ctx context.Context) string {
pos := types.NewPositionFromGIndex(claim.Position) pos := types.NewPositionFromGIndex(claim.Position)
extra := "" extra := ""
if pos.Depth() <= splitDepth { if pos.Depth() <= splitDepth {
blockNum, err := g.correctOutputProvider.BlockNumber(pos) blockNum, err := g.correctOutputProvider.ClaimedBlockNumber(pos)
if err != nil { if err != nil {
} else { } else {
extra = fmt.Sprintf("Block num: %v", blockNum) extra = fmt.Sprintf("Block num: %v", blockNum)
...@@ -697,3 +698,12 @@ func (g *OutputGameHelper) Credit(ctx context.Context, addr common.Address) *big ...@@ -697,3 +698,12 @@ func (g *OutputGameHelper) Credit(ctx context.Context, addr common.Address) *big
g.require.NoError(err) g.require.NoError(err)
return amt return amt
} }
func (g *OutputGameHelper) getL1Head(ctx context.Context) eth.BlockID {
l1HeadHash, err := g.game.L1Head(&bind.CallOpts{Context: ctx})
g.require.NoError(err, "Failed to load L1 head")
l1Header, err := g.client.HeaderByHash(ctx, l1HeadHash)
g.require.NoError(err, "Failed to load L1 header")
l1Head := eth.HeaderBlockID(l1Header)
return l1Head
}
...@@ -662,8 +662,6 @@ func TestDisputeOutputRoot_ChangeClaimedOutputRoot(t *testing.T) { ...@@ -662,8 +662,6 @@ func TestDisputeOutputRoot_ChangeClaimedOutputRoot(t *testing.T) {
} }
func TestInvalidateUnsafeProposal(t *testing.T) { func TestInvalidateUnsafeProposal(t *testing.T) {
// TODO(client-pod#540) Fix and enable TestInvalidateUnsafeProposal
t.Skip("Agreed head not correctly restricted yet")
op_e2e.InitParallel(t, op_e2e.UsesCannon) op_e2e.InitParallel(t, op_e2e.UsesCannon)
ctx := context.Background() ctx := context.Background()
...@@ -695,7 +693,7 @@ func TestInvalidateUnsafeProposal(t *testing.T) { ...@@ -695,7 +693,7 @@ func TestInvalidateUnsafeProposal(t *testing.T) {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon) op_e2e.InitParallel(t, op_e2e.UsesCannon)
sys, l1Client := startFaultDisputeSystem(t, withSequencerWindowSize(1000)) sys, l1Client := startFaultDisputeSystem(t, withSequencerWindowSize(100000))
t.Cleanup(sys.Close) t.Cleanup(sys.Close)
// Wait for the safe head to advance at least one block to init the safe head database // Wait for the safe head to advance at least one block to init the safe head database
...@@ -715,6 +713,71 @@ func TestInvalidateUnsafeProposal(t *testing.T) { ...@@ -715,6 +713,71 @@ func TestInvalidateUnsafeProposal(t *testing.T) {
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Alice)) correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
// Start the honest challenger
game.StartChallenger(ctx, "sequencer", "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Bob))
game.DefendClaim(ctx, game.RootClaim(ctx), func(parent *disputegame.ClaimHelper) *disputegame.ClaimHelper {
if parent.IsBottomGameRoot(ctx) {
return correctTrace.AttackClaim(ctx, parent)
}
return test.strategy(correctTrace, parent)
})
// Time travel past when the game will be resolvable.
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
game.LogGameData(ctx)
})
}
}
func TestInvalidateProposalForFutureBlock(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
ctx := context.Background()
tests := []struct {
name string
strategy func(correctTrace *disputegame.OutputHonestHelper, parent *disputegame.ClaimHelper) *disputegame.ClaimHelper
}{
{
name: "Attack",
strategy: func(correctTrace *disputegame.OutputHonestHelper, parent *disputegame.ClaimHelper) *disputegame.ClaimHelper {
return correctTrace.AttackClaim(ctx, parent)
},
},
{
name: "Defend",
strategy: func(correctTrace *disputegame.OutputHonestHelper, parent *disputegame.ClaimHelper) *disputegame.ClaimHelper {
return correctTrace.DefendClaim(ctx, parent)
},
},
{
name: "Counter",
strategy: func(correctTrace *disputegame.OutputHonestHelper, parent *disputegame.ClaimHelper) *disputegame.ClaimHelper {
return correctTrace.CounterClaim(ctx, parent)
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
sys, l1Client := startFaultDisputeSystem(t, withSequencerWindowSize(100000))
t.Cleanup(sys.Close)
// Wait for the safe head to advance at least one block to init the safe head database
require.NoError(t, wait.ForSafeBlock(ctx, sys.RollupClient("sequencer"), 1))
farFutureBlockNum := uint64(10_000_000)
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
// Root claim is _dishonest_ because the required data is not available on L1
game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", farFutureBlockNum, common.Hash{0xaa}, disputegame.WithFutureProposal())
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
// Start the honest challenger // Start the honest challenger
game.StartChallenger(ctx, "sequencer", "Honest", challenger.WithPrivKey(sys.Cfg.Secrets.Bob)) game.StartChallenger(ctx, "sequencer", "Honest", challenger.WithPrivKey(sys.Cfg.Secrets.Bob))
......
...@@ -83,11 +83,12 @@ func (d *Driver) SafeHead() eth.L2BlockRef { ...@@ -83,11 +83,12 @@ func (d *Driver) SafeHead() eth.L2BlockRef {
} }
func (d *Driver) ValidateClaim(l2ClaimBlockNum uint64, claimedOutputRoot eth.Bytes32) error { func (d *Driver) ValidateClaim(l2ClaimBlockNum uint64, claimedOutputRoot eth.Bytes32) error {
outputRoot, err := d.l2OutputRoot(l2ClaimBlockNum) l2Head := d.SafeHead()
outputRoot, err := d.l2OutputRoot(min(l2ClaimBlockNum, l2Head.Number))
if err != nil { if err != nil {
return fmt.Errorf("calculate L2 output root: %w", err) return fmt.Errorf("calculate L2 output root: %w", err)
} }
d.logger.Info("Validating claim", "head", d.SafeHead(), "output", outputRoot, "claim", claimedOutputRoot) d.logger.Info("Validating claim", "head", l2Head, "output", outputRoot, "claim", claimedOutputRoot)
if claimedOutputRoot != outputRoot { if claimedOutputRoot != outputRoot {
return fmt.Errorf("%w: claim: %v actual: %v", ErrClaimNotValid, claimedOutputRoot, outputRoot) return fmt.Errorf("%w: claim: %v actual: %v", ErrClaimNotValid, claimedOutputRoot, outputRoot)
} }
......
...@@ -80,6 +80,19 @@ func TestValidateClaim(t *testing.T) { ...@@ -80,6 +80,19 @@ func TestValidateClaim(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
}) })
t.Run("Valid-PriorToSafeHead", func(t *testing.T) {
driver := createDriverWithNextBlock(t, io.EOF, 10)
expected := eth.Bytes32{0x11}
requestedOutputRoot := uint64(0)
driver.l2OutputRoot = func(blockNum uint64) (eth.Bytes32, error) {
requestedOutputRoot = blockNum
return expected, nil
}
err := driver.ValidateClaim(uint64(20), expected)
require.NoError(t, err)
require.Equal(t, uint64(10), requestedOutputRoot)
})
t.Run("Invalid", func(t *testing.T) { t.Run("Invalid", func(t *testing.T) {
driver := createDriver(t, io.EOF) driver := createDriver(t, io.EOF)
driver.l2OutputRoot = func(_ uint64) (eth.Bytes32, error) { driver.l2OutputRoot = func(_ uint64) (eth.Bytes32, error) {
...@@ -89,6 +102,19 @@ func TestValidateClaim(t *testing.T) { ...@@ -89,6 +102,19 @@ func TestValidateClaim(t *testing.T) {
require.ErrorIs(t, err, ErrClaimNotValid) require.ErrorIs(t, err, ErrClaimNotValid)
}) })
t.Run("Invalid-PriorToSafeHead", func(t *testing.T) {
driver := createDriverWithNextBlock(t, io.EOF, 10)
expected := eth.Bytes32{0x11}
requestedOutputRoot := uint64(0)
driver.l2OutputRoot = func(blockNum uint64) (eth.Bytes32, error) {
requestedOutputRoot = blockNum
return expected, nil
}
err := driver.ValidateClaim(uint64(20), eth.Bytes32{0x55})
require.ErrorIs(t, err, ErrClaimNotValid)
require.Equal(t, uint64(10), requestedOutputRoot)
})
t.Run("Error", func(t *testing.T) { t.Run("Error", func(t *testing.T) {
driver := createDriver(t, io.EOF) driver := createDriver(t, io.EOF)
expectedErr := errors.New("boom") expectedErr := errors.New("boom")
......
...@@ -40,10 +40,12 @@ func (s *L2Client) OutputByRoot(ctx context.Context, l2OutputRoot common.Hash) ( ...@@ -40,10 +40,12 @@ func (s *L2Client) OutputByRoot(ctx context.Context, l2OutputRoot common.Hash) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
if eth.OutputRoot(output) != eth.Bytes32(l2OutputRoot) { actualOutputRoot := eth.OutputRoot(output)
if actualOutputRoot != eth.Bytes32(l2OutputRoot) {
// For fault proofs, we only reference outputs at the l2 head at boot time // For fault proofs, we only reference outputs at the l2 head at boot time
// The caller shouldn't be requesting outputs at any other block // The caller shouldn't be requesting outputs at any other block
return nil, fmt.Errorf("unknown output root") // If they are, there is no chance of recovery and we should panic to avoid retrying forever
panic(fmt.Errorf("output root %v from specified L2 block %v does not match requested output root %v", actualOutputRoot, s.l2Head, l2OutputRoot))
} }
return output, nil return output, nil
} }
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