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 (
"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/outputs"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs/source"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/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/types"
"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/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
......@@ -29,7 +29,7 @@ type Registry interface {
}
type RollupClient interface {
source.OutputRollupClient
outputs.OutputRollupClient
SyncStatusProvider
}
......@@ -48,7 +48,6 @@ func RegisterGameTypes(
) (CloseFunc, error) {
var closer CloseFunc
var l2Client *ethclient.Client
var outputSourceCreator *source.OutputSourceCreator
if cfg.TraceTypeEnabled(config.TraceTypeCannon) || cfg.TraceTypeEnabled(config.TraceTypePermissioned) {
l2, err := ethclient.DialContext(ctx, cfg.CannonL2)
if err != nil {
......@@ -56,17 +55,16 @@ func RegisterGameTypes(
}
l2Client = l2
closer = l2Client.Close
outputSourceCreator = source.NewOutputSourceCreator(logger, rollupClient, l1HeaderSource)
}
syncValidator := newSyncStatusValidator(rollupClient)
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)
}
}
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)
}
}
......@@ -85,7 +83,7 @@ func registerAlphabet(
logger log.Logger,
m metrics.Metricer,
syncValidator SyncValidator,
rollupClient source.OutputRollupClient,
rollupClient RollupClient,
txSender types.TxSender,
gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller,
......@@ -104,10 +102,13 @@ func registerAlphabet(
if err != nil {
return nil, err
}
outputSource := source.NewUnrestrictedOutputSource(rollupClient)
prestateProvider := outputs.NewPrestateProvider(outputSource, prestateBlock)
l1Head, err := loadL1Head(contract, ctx, l1HeaderSource)
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) {
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 {
return nil, err
}
......@@ -155,7 +156,7 @@ func registerCannon(
m metrics.Metricer,
cfg *config.Config,
syncValidator SyncValidator,
outputSourceCreator *source.OutputSourceCreator,
rollupClient outputs.OutputRollupClient,
txSender types.TxSender,
gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller,
......@@ -175,17 +176,13 @@ func registerCannon(
if err != nil {
return nil, fmt.Errorf("failed to load split depth: %w", err)
}
l1Head, err := contract.GetL1Head(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load L1 head: %w", err)
}
rollupClient, err := outputSourceCreator.ForL1Head(ctx, l1Head)
l1HeadID, err := loadL1Head(contract, ctx, l1HeaderSource)
if err != nil {
return nil, fmt.Errorf("failed to create output root source: %w", err)
return nil, err
}
prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock)
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 {
return nil, err
}
......@@ -207,3 +204,15 @@ func registerCannon(
registry.RegisterBondContract(gameType, contractCreator)
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
if err != nil {
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)
if err != nil {
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)
if err != nil {
return LocalGameInputs{}, fmt.Errorf("fetch L2 block header %v: %w", agreedOutput.L2BlockNumber, err)
......
......@@ -52,9 +52,7 @@ func TestFetchLocalInputsFromProposals(t *testing.T) {
L2BlockNumber: big.NewInt(3333),
OutputRoot: common.Hash{0xee},
}
contract := &mockGameInputsSource{
l1Head: common.Hash{0xcc},
}
l1Head := common.Hash{0xcc}
l2Client := &mockL2DataSource{
chainID: big.NewInt(88422),
header: ethtypes.Header{
......@@ -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.Equal(t, contract.l1Head, inputs.L1Head)
require.Equal(t, l1Head, inputs.L1Head)
require.Equal(t, l2Client.header.Hash(), inputs.L2Head)
require.EqualValues(t, agreed.OutputRoot, inputs.L2OutputRoot)
require.EqualValues(t, claimed.OutputRoot, inputs.L2Claim)
......
......@@ -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/types"
"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/log"
)
......@@ -17,12 +18,13 @@ func NewOutputAlphabetTraceAccessor(
logger log.Logger,
m metrics.Metricer,
prestateProvider types.PrestateProvider,
rollupClient OutputRootProvider,
rollupClient OutputRollupClient,
l1Head eth.BlockID,
splitDepth types.Depth,
prestateBlock uint64,
poststateBlock uint64,
) (*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) {
provider := alphabet.NewTraceProvider(agreed.L2BlockNumber, depth)
return provider, nil
......
......@@ -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/types"
"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/log"
)
......@@ -21,19 +22,19 @@ func NewOutputCannonTraceAccessor(
m metrics.Metricer,
cfg *config.Config,
l2Client cannon.L2HeaderSource,
contract cannon.L1HeadSource,
prestateProvider types.PrestateProvider,
rollupClient OutputRootProvider,
rollupClient OutputRollupClient,
dir string,
l1Head eth.BlockID,
splitDepth types.Depth,
prestateBlock uint64,
poststateBlock uint64,
) (*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) {
logger := logger.New("pre", agreed.OutputRoot, "post", claimed.OutputRoot, "localContext", localContext)
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 {
return nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err)
}
......
......@@ -12,10 +12,10 @@ var _ types.PrestateProvider = (*OutputPrestateProvider)(nil)
type OutputPrestateProvider struct {
prestateBlock uint64
rollupClient OutputRootProvider
rollupClient OutputRollupClient
}
func NewPrestateProvider(rollupClient OutputRootProvider, prestateBlock uint64) *OutputPrestateProvider {
func NewPrestateProvider(rollupClient OutputRollupClient, prestateBlock uint64) *OutputPrestateProvider {
return &OutputPrestateProvider{
prestateBlock: prestateBlock,
rollupClient: rollupClient,
......@@ -27,9 +27,9 @@ func (o *OutputPrestateProvider) AbsolutePreStateCommitment(ctx context.Context)
}
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 {
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 (
"context"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs/source"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/stretchr/testify/require"
)
func newOutputPrestateProvider(t *testing.T, prestateBlock uint64) (*OutputPrestateProvider, *stubRollupClient) {
rollupClient := stubRollupClient{
rollupClient := &stubRollupClient{
outputs: map[uint64]*eth.OutputResponse{
prestateBlock: {
OutputRoot: eth.Bytes32(prestateOutputRoot),
......@@ -24,9 +23,9 @@ func newOutputPrestateProvider(t *testing.T, prestateBlock uint64) (*OutputPrest
},
}
return &OutputPrestateProvider{
rollupClient: source.NewUnrestrictedOutputSource(&rollupClient),
rollupClient: rollupClient,
prestateBlock: prestateBlock,
}, &rollupClient
}, rollupClient
}
func TestAbsolutePreStateCommitment(t *testing.T) {
......
......@@ -6,6 +6,7 @@ import (
"fmt"
"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/log"
)
......@@ -17,8 +18,9 @@ var (
var _ types.TraceProvider = (*OutputTraceProvider)(nil)
type OutputRootProvider interface {
OutputAtBlock(ctx context.Context, blockNum uint64) (common.Hash, error)
type OutputRollupClient interface {
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
......@@ -26,28 +28,33 @@ type OutputRootProvider interface {
type OutputTraceProvider struct {
types.PrestateProvider
logger log.Logger
rollupProvider OutputRootProvider
rollupProvider OutputRollupClient
prestateBlock uint64
poststateBlock uint64
l1Head eth.BlockID
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{
PrestateProvider: prestateProvider,
logger: logger,
rollupProvider: rollupProvider,
prestateBlock: prestateBlock,
poststateBlock: poststateBlock,
l1Head: l1Head,
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)
if !traceIndex.IsUint64() {
return 0, fmt.Errorf("%w: %v", ErrIndexTooBig, traceIndex)
}
outputBlock := traceIndex.Uint64() + o.prestateBlock + 1
if outputBlock > o.poststateBlock {
outputBlock = o.poststateBlock
......@@ -55,8 +62,27 @@ func (o *OutputTraceProvider) BlockNumber(pos types.Position) (uint64, error) {
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) {
outputBlock, err := o.BlockNumber(pos)
outputBlock, err := o.HonestBlockNumber(ctx, pos)
if err != nil {
return common.Hash{}, err
}
......@@ -69,9 +95,9 @@ func (o *OutputTraceProvider) GetStepData(_ context.Context, _ types.Position) (
}
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 {
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 (
"context"
"errors"
"fmt"
"math"
"math/big"
"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-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog"
......@@ -63,26 +63,74 @@ func TestGet(t *testing.T) {
})
}
func TestGetBlockNumber(t *testing.T) {
func TestHonestBlockNumber(t *testing.T) {
tests := []struct {
name string
pos types.Position
expected uint64
maxSafeHead uint64
}{
{"FirstBlockAfterPrestate", types.NewPosition(gameDepth, big.NewInt(0)), prestateBlock + 1},
{"PostStateBlock", types.NewPositionFromGIndex(big.NewInt(228)), poststateBlock},
{"AfterPostStateBlock", types.NewPositionFromGIndex(big.NewInt(229)), poststateBlock},
{"Root", types.NewPositionFromGIndex(big.NewInt(1)), poststateBlock},
{"MiddleNode1", types.NewPosition(gameDepth-1, big.NewInt(2)), 106},
{"MiddleNode2", types.NewPosition(gameDepth-1, big.NewInt(3)), 108},
{"Leaf1", types.NewPosition(gameDepth, big.NewInt(1)), prestateBlock + 2},
{"Leaf2", types.NewPosition(gameDepth, big.NewInt(2)), prestateBlock + 3},
{"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 + 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, _ := setupWithTestData(t, prestateBlock, poststateBlock)
actual, err := provider.BlockNumber(test.pos)
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 {
test := test
t.Run(test.name, func(t *testing.T) {
provider, stubRollupClient := setupWithTestData(t, prestateBlock, poststateBlock)
stubRollupClient.maxSafeHead = test.maxSafeHead
actual, err := provider.ClaimedBlockNumber(test.pos)
require.NoError(t, err)
require.Equal(t, test.expected, actual)
})
......@@ -92,7 +140,7 @@ func TestGetBlockNumber(t *testing.T) {
deepGame := types.Depth(164)
provider, _ := setupWithTestData(t, prestateBlock, poststateBlock, deepGame)
pos := types.NewPosition(0, big.NewInt(0))
_, err := provider.BlockNumber(pos)
_, err := provider.ClaimedBlockNumber(pos)
require.ErrorIs(t, err, ErrIndexTooBig)
})
}
......@@ -104,7 +152,7 @@ func TestGetStepData(t *testing.T) {
}
func setupWithTestData(t *testing.T, prestateBlock, poststateBlock uint64, customGameDepth ...types.Depth) (*OutputTraceProvider, *stubRollupClient) {
rollupClient := stubRollupClient{
rollupClient := &stubRollupClient{
outputs: map[uint64]*eth.OutputResponse{
prestateBlock: {
OutputRoot: eth.Bytes32(prestateOutputRoot),
......@@ -116,6 +164,7 @@ func setupWithTestData(t *testing.T, prestateBlock, poststateBlock uint64, custo
OutputRoot: eth.Bytes32(poststateOutputRoot),
},
},
maxSafeHead: math.MaxUint64,
}
inputGameDepth := gameDepth
if len(customGameDepth) > 0 {
......@@ -123,16 +172,17 @@ func setupWithTestData(t *testing.T, prestateBlock, poststateBlock uint64, custo
}
return &OutputTraceProvider{
logger: testlog.Logger(t, log.LevelInfo),
rollupProvider: source.NewUnrestrictedOutputSource(&rollupClient),
rollupProvider: rollupClient,
prestateBlock: prestateBlock,
poststateBlock: poststateBlock,
gameDepth: inputGameDepth,
}, &rollupClient
}, rollupClient
}
type stubRollupClient struct {
errorsOnPrestateFetch bool
outputs map[uint64]*eth.OutputResponse
maxSafeHead uint64
}
func (s *stubRollupClient) OutputAtBlock(_ context.Context, blockNum uint64) (*eth.OutputResponse, error) {
......@@ -144,5 +194,10 @@ func (s *stubRollupClient) OutputAtBlock(_ context.Context, blockNum uint64) (*e
}
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
OutputRoot: prestateRoot,
}
} else {
preBlockNum, err := topProvider.BlockNumber(pre.Position)
preBlockNum, err := topProvider.HonestBlockNumber(ctx, pre.Position)
if err != nil {
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
OutputRoot: pre.Value,
}
}
postBlockNum, err := topProvider.BlockNumber(post.Position)
postBlockNum, err := topProvider.ClaimedBlockNumber(post.Position)
if err != nil {
return contracts.Proposal{}, contracts.Proposal{}, fmt.Errorf("unable to calculate post-claim block number: %w", err)
}
......
......@@ -8,7 +8,6 @@ import (
"testing"
"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/types"
"github.com/ethereum-optimism/optimism/op-service/eth"
......@@ -125,17 +124,22 @@ func setupAdapterTest(t *testing.T, topDepth types.Depth) (split.ProviderCreator
prestateBlock := uint64(20)
poststateBlock := uint64(40)
creator := &capturingCreator{}
l1Head := eth.BlockID{
Hash: common.Hash{0x11, 0x11},
Number: 11,
}
rollupClient := &stubRollupClient{
outputs: map[uint64]*eth.OutputResponse{
prestateBlock: {
OutputRoot: eth.Bytes32(prestateOutputRoot),
},
},
maxSafeHead: math.MaxUint64,
}
prestateProvider := &stubPrestateProvider{
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)
return adapter, creator
}
......
......@@ -12,7 +12,6 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer"
"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/source"
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/disputegame/preimage"
......@@ -20,6 +19,7 @@ import (
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"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/testlog"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
......@@ -59,6 +59,7 @@ func (s Status) String() string {
}
type gameCfg struct {
allowFuture bool
allowUnsafe bool
}
type GameOpt interface {
......@@ -76,6 +77,12 @@ func WithUnsafeProposal() GameOpt {
})
}
func WithFutureProposal() GameOpt {
return gameOptFn(func(c *gameCfg) {
c.allowFuture = true
})
}
type DisputeSystem interface {
L1BeaconEndpoint() string
NodeEndpoint(name string) string
......@@ -177,15 +184,17 @@ func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, l2Node string
game, err := bindings.NewFaultDisputeGame(createdEvent.DisputeProxy, h.client)
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")
poststateBlock, err := game.L2BlockNumber(&bind.CallOpts{Context: ctx})
poststateBlock, err := game.L2BlockNumber(callOpts)
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")
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock.Uint64())
provider := outputs.NewTraceProvider(logger, prestateProvider, outputRootProvider, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
l1Head := h.getL1Head(ctx, game)
prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock.Uint64())
provider := outputs.NewTraceProvider(logger, prestateProvider, rollupClient, l1Head, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
return &OutputCannonGameHelper{
OutputGameHelper: OutputGameHelper{
......@@ -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 {
cfg := newGameCfg(opts...)
h.waitForBlock(l2Node, l2BlockNumber, cfg)
......@@ -232,15 +250,17 @@ func (h *FactoryHelper) StartOutputAlphabetGame(ctx context.Context, l2Node stri
game, err := bindings.NewFaultDisputeGame(createdEvent.DisputeProxy, h.client)
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")
poststateBlock, err := game.L2BlockNumber(&bind.CallOpts{Context: ctx})
poststateBlock, err := game.L2BlockNumber(callOpts)
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")
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock.Uint64())
provider := outputs.NewTraceProvider(logger, prestateProvider, outputRootProvider, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
l1Head := h.getL1Head(ctx, game)
prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock.Uint64())
provider := outputs.NewTraceProvider(logger, prestateProvider, rollupClient, l1Head, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
return &OutputAlphabetGameHelper{
OutputGameHelper: OutputGameHelper{
......@@ -266,6 +286,11 @@ func (h *FactoryHelper) createBisectionGameExtraData(l2Node string, l2BlockNumbe
}
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)
if cfg.allowUnsafe {
_, err := geth.WaitForBlock(new(big.Int).SetUint64(l2BlockNumber), l2Client, 1*time.Minute)
......
......@@ -5,7 +5,6 @@ import (
"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/source"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
......@@ -44,10 +43,10 @@ func (g *OutputAlphabetGameHelper) CreateHonestActor(ctx context.Context, l2Node
prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx)
g.require.NoError(err, "Get block range")
splitDepth := g.SplitDepth(ctx)
l1Head := g.getL1Head(ctx)
rollupClient := g.system.RollupClient(l2Node)
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock)
correctTrace, err := outputs.NewOutputAlphabetTraceAccessor(logger, metrics.NoopMetrics, prestateProvider, outputRootProvider, splitDepth, prestateBlock, poststateBlock)
prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock)
correctTrace, err := outputs.NewOutputAlphabetTraceAccessor(logger, metrics.NoopMetrics, prestateProvider, rollupClient, l1Head, splitDepth, prestateBlock, poststateBlock)
g.require.NoError(err, "Create trace accessor")
return &OutputHonestHelper{
t: g.t,
......
......@@ -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/cannon"
"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/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
......@@ -64,10 +63,10 @@ func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node s
dir := filepath.Join(cfg.Datadir, "honest")
splitDepth := g.SplitDepth(ctx)
rollupClient := g.system.RollupClient(l2Node)
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock)
prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock)
l1Head := g.getL1Head(ctx)
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")
return &OutputHonestHelper{
t: g.t,
......@@ -231,15 +230,15 @@ func (g *OutputCannonGameHelper) createCannonTraceProvider(ctx context.Context,
prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx)
g.require.NoError(err, "Failed to load block range")
rollupClient := g.system.RollupClient(l2Node)
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock)
outputProvider := outputs.NewTraceProvider(logger, prestateProvider, outputRootProvider, splitDepth, prestateBlock, poststateBlock)
prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock)
l1Head := g.getL1Head(ctx)
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) {
agreed, disputed, err := outputs.FetchProposals(ctx, outputProvider, pre, post)
g.require.NoError(err)
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")
localContext := outputs.CreateLocalContext(pre, post)
dir := filepath.Join(cfg.Datadir, "cannon-trace")
......
......@@ -16,6 +16,7 @@ import (
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
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/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
......@@ -109,7 +110,7 @@ func (g *OutputGameHelper) DisputeBlock(ctx context.Context, disputeBlockNum uin
}
pos := types.NewPositionFromGIndex(big.NewInt(1))
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")
if claimBlockNum < disputeBlockNum {
// 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
claim := g.RootClaim(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")
if parentClaimBlockNum >= disputeBlockNum {
pos = pos.Attack()
......@@ -671,7 +672,7 @@ func (g *OutputGameHelper) gameData(ctx context.Context) string {
pos := types.NewPositionFromGIndex(claim.Position)
extra := ""
if pos.Depth() <= splitDepth {
blockNum, err := g.correctOutputProvider.BlockNumber(pos)
blockNum, err := g.correctOutputProvider.ClaimedBlockNumber(pos)
if err != nil {
} else {
extra = fmt.Sprintf("Block num: %v", blockNum)
......@@ -697,3 +698,12 @@ func (g *OutputGameHelper) Credit(ctx context.Context, addr common.Address) *big
g.require.NoError(err)
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) {
}
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)
ctx := context.Background()
......@@ -695,7 +693,7 @@ func TestInvalidateUnsafeProposal(t *testing.T) {
test := test
t.Run(test.name, func(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
sys, l1Client := startFaultDisputeSystem(t, withSequencerWindowSize(1000))
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
......@@ -715,6 +713,71 @@ func TestInvalidateUnsafeProposal(t *testing.T) {
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
game.StartChallenger(ctx, "sequencer", "Honest", challenger.WithPrivKey(sys.Cfg.Secrets.Bob))
......
......@@ -83,11 +83,12 @@ func (d *Driver) SafeHead() eth.L2BlockRef {
}
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 {
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 {
return fmt.Errorf("%w: claim: %v actual: %v", ErrClaimNotValid, claimedOutputRoot, outputRoot)
}
......
......@@ -80,6 +80,19 @@ func TestValidateClaim(t *testing.T) {
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) {
driver := createDriver(t, io.EOF)
driver.l2OutputRoot = func(_ uint64) (eth.Bytes32, error) {
......@@ -89,6 +102,19 @@ func TestValidateClaim(t *testing.T) {
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) {
driver := createDriver(t, io.EOF)
expectedErr := errors.New("boom")
......
......@@ -40,10 +40,12 @@ func (s *L2Client) OutputByRoot(ctx context.Context, l2OutputRoot common.Hash) (
if err != nil {
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
// 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
}
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