Commit e3235f46 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Limit output roots to safe head at L1 block (#9598)

* op-challenger: Load maximum safe head from op-node RPC

* op-challenger: Remove no longer required code to find guaranteed safe head.

* op-e2e: Enable safe db
parent fdf5bd06
......@@ -48,6 +48,7 @@ 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 {
......@@ -55,8 +56,8 @@ func RegisterGameTypes(
}
l2Client = l2
closer = l2Client.Close
outputSourceCreator = source.NewOutputSourceCreator(logger, rollupClient, l1HeaderSource)
}
outputSourceCreator := source.NewOutputSourceCreator(logger, rollupClient)
syncValidator := newSyncStatusValidator(rollupClient)
if cfg.TraceTypeEnabled(config.TraceTypeCannon) {
......
......@@ -22,7 +22,7 @@ func NewOutputAlphabetTraceAccessor(
prestateBlock uint64,
poststateBlock uint64,
) (*trace.Accessor, error) {
outputProvider := NewTraceProviderFromInputs(logger, prestateProvider, rollupClient, splitDepth, prestateBlock, poststateBlock)
outputProvider := NewTraceProvider(logger, prestateProvider, rollupClient, 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
......
......@@ -29,7 +29,7 @@ func NewOutputCannonTraceAccessor(
prestateBlock uint64,
poststateBlock uint64,
) (*trace.Accessor, error) {
outputProvider := NewTraceProviderFromInputs(logger, prestateProvider, rollupClient, splitDepth, prestateBlock, poststateBlock)
outputProvider := NewTraceProvider(logger, prestateProvider, rollupClient, 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())
......
......@@ -32,7 +32,7 @@ type OutputTraceProvider struct {
gameDepth types.Depth
}
func NewTraceProviderFromInputs(logger log.Logger, prestateProvider types.PrestateProvider, rollupProvider OutputRootProvider, gameDepth types.Depth, prestateBlock, poststateBlock uint64) *OutputTraceProvider {
func NewTraceProvider(logger log.Logger, prestateProvider types.PrestateProvider, rollupProvider OutputRootProvider, gameDepth types.Depth, prestateBlock, poststateBlock uint64) *OutputTraceProvider {
return &OutputTraceProvider{
PrestateProvider: prestateProvider,
logger: logger,
......
......@@ -142,3 +142,7 @@ func (s *stubRollupClient) OutputAtBlock(_ context.Context, blockNum uint64) (*e
}
return output, nil
}
func (s *stubRollupClient) SafeHeadAtL1Block(_ context.Context, l1BlockNum uint64) (*eth.SafeHeadResponse, error) {
return nil, errors.New("not supported")
}
......@@ -2,30 +2,41 @@ package source
import (
"context"
"math"
"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) *OutputSourceCreator {
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) {
// TODO(client-pod#416): Run op-program to detect the latest safe head supported by l1Head
return NewRestrictedOutputSource(l.rollupClient, math.MaxUint64), nil
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
}
......@@ -5,26 +5,34 @@ import (
"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
maxSafeHead uint64
l1Head eth.BlockID
}
func NewRestrictedOutputSource(rollupClient OutputRollupClient, maxSafeHead uint64) *RestrictedOutputSource {
func NewRestrictedOutputSource(rollupClient OutputRollupClient, l1Head eth.BlockID) *RestrictedOutputSource {
return &RestrictedOutputSource{
rollupClient: rollupClient,
unrestricted: NewUnrestrictedOutputSource(rollupClient),
maxSafeHead: maxSafeHead,
l1Head: l1Head,
}
}
func (l *RestrictedOutputSource) OutputAtBlock(ctx context.Context, blockNum uint64) (common.Hash, error) {
if blockNum > l.maxSafeHead {
return common.Hash{}, fmt.Errorf("%w, requested: %v max: %v", ErrExceedsL1Head, blockNum, l.maxSafeHead)
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)
}
......@@ -64,7 +64,11 @@ func TestRestrictedOutputLoader(t *testing.T) {
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
loader := NewRestrictedOutputSource(&stubOutputRollupClient{}, test.maxSafeHead)
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)
......@@ -72,26 +76,56 @@ func TestRestrictedOutputLoader(t *testing.T) {
} else {
require.ErrorIs(t, err, test.expectedErr)
}
require.Equal(t, l1Head.Number, rollupClient.requestedL1BlockNum)
})
}
}
func TestRestrictedOutputLoader_ReturnsError(t *testing.T) {
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")
loader := NewRestrictedOutputSource(&stubOutputRollupClient{err: expectedErr}, 6)
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 {
err error
outputErr error
safeHeadErr error
safeHead uint64
requestedL1BlockNum uint64
}
func (s *stubOutputRollupClient) OutputAtBlock(_ context.Context, blockNum uint64) (*eth.OutputResponse, error) {
if s.err != nil {
return nil, s.err
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-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/eth"
)
type L2Source interface {
L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error)
}
// FindGuaranteedSafeHead finds a L2 block where the L1 origin is the most recent L1 block closes to l1BlockNum
// where the block is guaranteed to now be safe because the sequencer window has expired.
// That is: block.origin.Number + sequencerWindowSize < l1BlockNum
// Note that the derivation rules guarantee that there is at least 1 L2 block for each L1 block.
// Otherwise deposits from the skipped L1 block would be missed.
func FindGuaranteedSafeHead(ctx context.Context, rollupCfg *rollup.Config, l1BlockNum uint64, l2Client L2Source) (eth.BlockID, error) {
if l1BlockNum <= rollupCfg.SeqWindowSize {
// The sequencer window hasn't completed yet, so the only guaranteed safe block is L2 genesis
return rollupCfg.Genesis.L2, nil
}
safeHead, err := l2Client.L2BlockRefByLabel(ctx, eth.Safe)
if err != nil {
return eth.BlockID{}, fmt.Errorf("failed to load local safe head: %w", err)
}
safeL1BlockNum := l1BlockNum - rollupCfg.SeqWindowSize - 1
start := rollupCfg.Genesis.L2.Number
end := safeHead.Number
for start <= end {
mid := (start + end) / 2
l2Block, err := l2Client.L2BlockRefByNumber(ctx, mid)
if err != nil {
return eth.BlockID{}, fmt.Errorf("failed to retrieve l2 block %v: %w", mid, err)
}
if l2Block.L1Origin.Number == safeL1BlockNum {
return l2Block.ID(), nil
} else if l2Block.L1Origin.Number < safeL1BlockNum {
start = mid + 1
} else {
end = mid - 1
}
}
return rollupCfg.Genesis.L2, nil
}
package source
import (
"context"
"errors"
"math"
"testing"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestFindGuaranteedSafeHead_ErrorWhenSafeHeadNotAvailable(t *testing.T) {
cfg := &rollup.Config{
SeqWindowSize: 100,
Genesis: rollup.Genesis{
L2: eth.BlockID{
Hash: common.Hash{0x1},
Number: 1343,
},
},
}
expectedErr := errors.New("boom")
l2Source := &stubL2Source{byLabelError: expectedErr}
_, err := FindGuaranteedSafeHead(context.Background(), cfg, 248249, l2Source)
require.Error(t, err)
}
func TestFindGuaranteedSafeHead_L2GenesisWhenL1HeadNotPastSequenceWindow(t *testing.T) {
cfg := &rollup.Config{
SeqWindowSize: 100,
Genesis: rollup.Genesis{
L2: eth.BlockID{
Hash: common.Hash{0x1},
Number: 1343,
},
},
}
l2Source := &stubL2Source{}
actual, err := FindGuaranteedSafeHead(context.Background(), cfg, 99, l2Source)
require.NoError(t, err)
require.Equal(t, cfg.Genesis.L2, actual)
}
func TestFindGuaranteedSafeHead_L2GenesisWhenL1HeadEqualToSequenceWindow(t *testing.T) {
cfg := &rollup.Config{
SeqWindowSize: 100,
Genesis: rollup.Genesis{
L2: eth.BlockID{
Hash: common.Hash{0x1},
Number: 1343,
},
},
}
l2Source := &stubL2Source{}
actual, err := FindGuaranteedSafeHead(context.Background(), cfg, 100, l2Source)
require.NoError(t, err)
require.Equal(t, cfg.Genesis.L2, actual)
}
func TestFindGuaranteedSafeHead_SafeHeadIsGuaranteedSafe(t *testing.T) {
cfg := &rollup.Config{
SeqWindowSize: 100,
Genesis: rollup.Genesis{
L2: eth.BlockID{
Hash: common.Hash{0x1},
Number: 1343,
},
},
}
safeHead := eth.L2BlockRef{
Hash: common.Hash{0xaa},
Number: 1000,
L1Origin: eth.BlockID{
Number: 499,
},
}
l2Source := &stubL2Source{
safe: safeHead,
}
actual, err := FindGuaranteedSafeHead(context.Background(), cfg, 500, l2Source)
require.NoError(t, err)
require.Equal(t, cfg.Genesis.L2, actual)
}
func TestFindGuaranteedSafeHead_SearchBackwardFromSafeHead(t *testing.T) {
cfg := &rollup.Config{
SeqWindowSize: 100,
Genesis: rollup.Genesis{
L2: eth.BlockID{
Hash: common.Hash{0x1},
Number: 500,
},
},
}
safeHead := eth.L2BlockRef{
Hash: common.Hash{0xaa},
Number: 1500,
L1Origin: eth.BlockID{
Number: 5000,
},
}
l2Source := &stubL2Source{
safe: safeHead,
blocks: make(map[uint64]eth.L2BlockRef),
}
for i := cfg.Genesis.L2.Number + 1; i < safeHead.Number; i++ {
block := eth.L2BlockRef{
Hash: common.Hash{byte(i)},
Number: i,
L1Origin: eth.BlockID{
Number: 2000 + i, // Make it different from L2 block number
},
}
l2Source.blocks[block.Number] = block
}
expected := l2Source.blocks[1260]
actual, err := FindGuaranteedSafeHead(context.Background(), cfg, expected.L1Origin.Number+cfg.SeqWindowSize+1, l2Source)
require.NoError(t, err)
require.Equal(t, expected.ID(), actual)
maxQueries := int(math.Log2(float64(len(l2Source.blocks))) + 1)
require.LessOrEqual(t, l2Source.byNumCount, maxQueries, "Should use an efficient search")
}
type stubL2Source struct {
safe eth.L2BlockRef
byLabelError error
blocks map[uint64]eth.L2BlockRef
byNumCount int
}
func (s *stubL2Source) L2BlockRefByLabel(_ context.Context, _ eth.BlockLabel) (eth.L2BlockRef, error) {
return s.safe, s.byLabelError
}
func (s *stubL2Source) L2BlockRefByNumber(_ context.Context, blockNum uint64) (eth.L2BlockRef, error) {
s.byNumCount++
ref, ok := s.blocks[blockNum]
if !ok {
return eth.L2BlockRef{}, errors.New("not found")
}
return ref, nil
}
......@@ -135,7 +135,7 @@ func setupAdapterTest(t *testing.T, topDepth types.Depth) (split.ProviderCreator
prestateProvider := &stubPrestateProvider{
absolutePrestate: prestateOutputRoot,
}
topProvider := NewTraceProviderFromInputs(testlog.Logger(t, log.LevelInfo), prestateProvider, source.NewUnrestrictedOutputSource(rollupClient), topDepth, prestateBlock, poststateBlock)
topProvider := NewTraceProvider(testlog.Logger(t, log.LevelInfo), prestateProvider, source.NewUnrestrictedOutputSource(rollupClient), topDepth, prestateBlock, poststateBlock)
adapter := OutputRootSplitAdapter(topProvider, creator.Create)
return adapter, creator
}
......
......@@ -157,7 +157,7 @@ func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, l2Node string
h.require.NoError(err, "Failed to load split depth")
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock.Uint64())
provider := outputs.NewTraceProviderFromInputs(logger, prestateProvider, outputRootProvider, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
provider := outputs.NewTraceProvider(logger, prestateProvider, outputRootProvider, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
return &OutputCannonGameHelper{
OutputGameHelper: OutputGameHelper{
......@@ -210,7 +210,7 @@ func (h *FactoryHelper) StartOutputAlphabetGame(ctx context.Context, l2Node stri
h.require.NoError(err, "Failed to load split depth")
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock.Uint64())
provider := outputs.NewTraceProviderFromInputs(logger, prestateProvider, outputRootProvider, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
provider := outputs.NewTraceProvider(logger, prestateProvider, outputRootProvider, faultTypes.Depth(splitDepth.Uint64()), prestateBlock.Uint64(), poststateBlock.Uint64())
return &OutputAlphabetGameHelper{
OutputGameHelper: OutputGameHelper{
......
......@@ -233,7 +233,7 @@ func (g *OutputCannonGameHelper) createCannonTraceProvider(ctx context.Context,
rollupClient := g.system.RollupClient(l2Node)
outputRootProvider := source.NewUnrestrictedOutputSource(rollupClient)
prestateProvider := outputs.NewPrestateProvider(outputRootProvider, prestateBlock)
outputProvider := outputs.NewTraceProviderFromInputs(logger, prestateProvider, outputRootProvider, splitDepth, prestateBlock, poststateBlock)
outputProvider := outputs.NewTraceProvider(logger, prestateProvider, outputRootProvider, 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)
......
......@@ -47,6 +47,7 @@ func startFaultDisputeSystem(t *testing.T, opts ...faultDisputeConfigOpts) (*op_
for _, opt := range opts {
opt(&cfg)
}
cfg.Nodes["sequencer"].SafeDBPath = t.TempDir()
cfg.DeployConfig.SequencerWindowSize = 4
cfg.DeployConfig.FinalizationPeriodSeconds = 2
cfg.SupportL1TimeTravel = true
......
......@@ -406,6 +406,7 @@ func TestMixedWithdrawalValidity(t *testing.T) {
// Create our system configuration, funding all accounts we created for L1/L2, and start it
cfg := DefaultSystemConfig(t)
cfg.Nodes["sequencer"].SafeDBPath = t.TempDir()
cfg.DeployConfig.L2BlockTime = 2
require.LessOrEqual(t, cfg.DeployConfig.FinalizationPeriodSeconds, uint64(6))
require.Equal(t, cfg.DeployConfig.FundDevAccounts, true)
......
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