Commit 1078f86f authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Binary search for guaranteed safe block (#9538)

parent 830cdc64
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
}
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