Commit 9881eda4 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-program: Support agreed prestate hint in host (#13703)

* op-program: Support agreed prestate hint in host

* op-program: Fix transition state serialization in test

* op-program: Fix prefetcher_test.go

* op-program: Prevent specifying both --l2.outputroot and --l2.agreed-prestate flags

Check config is consistent

* op-program: Return safe or local safe head from derivation

Avoids relying on the safe label which won't be updated on interop chains.

* op-program: Only stop derivation if local safe reaches the target block.

* op-program: Fix safe head trace extension

* op-program: Remove unused field.
parent e86cc35a
......@@ -9,7 +9,7 @@ import (
"github.com/ethereum-optimism/optimism/op-program/client/claim"
"github.com/ethereum-optimism/optimism/op-program/client/interop/types"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-e2e/actions/helpers"
......@@ -239,8 +239,8 @@ func TestInteropFaultProofs(gt *testing.T) {
chainBClient := actors.ChainB.Sequencer.RollupClient()
ctx := context.Background()
startTimestamp := actors.ChainA.RollupCfg.Genesis.L2Time
endTimestamp := startTimestamp + actors.ChainA.RollupCfg.BlockTime
endTimestamp := actors.ChainA.RollupCfg.Genesis.L2Time + actors.ChainA.RollupCfg.BlockTime
startTimestamp := endTimestamp - 1
source, err := NewSuperRootSource(ctx, chainAClient, chainBClient)
require.NoError(t, err)
start, err := source.CreateSuperRoot(ctx, startTimestamp)
......@@ -249,19 +249,19 @@ func TestInteropFaultProofs(gt *testing.T) {
require.NoError(t, err)
serializeIntermediateRoot := func(root *types.TransitionState) []byte {
data, err := rlp.EncodeToBytes(root)
data, err := root.Marshal()
require.NoError(t, err)
return data
}
num, err := actors.ChainA.RollupCfg.TargetBlockNumber(endTimestamp)
endBlockNumA, err := actors.ChainA.RollupCfg.TargetBlockNumber(endTimestamp)
require.NoError(t, err)
chain1End, err := chainAClient.OutputAtBlock(ctx, num)
chain1End, err := chainAClient.OutputAtBlock(ctx, endBlockNumA)
require.NoError(t, err)
num, err = actors.ChainB.RollupCfg.TargetBlockNumber(endTimestamp)
endBlockNumB, err := actors.ChainB.RollupCfg.TargetBlockNumber(endTimestamp)
require.NoError(t, err)
chain2End, err := chainBClient.OutputAtBlock(ctx, num)
chain2End, err := chainBClient.OutputAtBlock(ctx, endBlockNumB)
require.NoError(t, err)
step1Expected := serializeIntermediateRoot(&types.TransitionState{
......@@ -297,7 +297,6 @@ func TestInteropFaultProofs(gt *testing.T) {
agreedClaim: start.Marshal(),
disputedClaim: start.Marshal(),
expectValid: false,
skip: true,
},
{
name: "ClaimDirectToNextTimestamp",
......@@ -305,7 +304,6 @@ func TestInteropFaultProofs(gt *testing.T) {
agreedClaim: start.Marshal(),
disputedClaim: end.Marshal(),
expectValid: false,
skip: true,
},
{
name: "FirstChainOptimisticBlock",
......@@ -313,7 +311,6 @@ func TestInteropFaultProofs(gt *testing.T) {
agreedClaim: start.Marshal(),
disputedClaim: step1Expected,
expectValid: true,
skip: true,
},
{
name: "SecondChainOptimisticBlock",
......@@ -364,6 +361,9 @@ func TestInteropFaultProofs(gt *testing.T) {
chain1End.BlockRef.Number,
checkResult,
fpHelpers.WithInteropEnabled(),
fpHelpers.WithAgreedPrestate(test.agreedClaim),
fpHelpers.WithL2Claim(crypto.Keccak256Hash(test.disputedClaim)),
fpHelpers.WithL2BlockNumber(endBlockNumA),
)
})
}
......
......@@ -6,6 +6,7 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
e2ecfg "github.com/ethereum-optimism/optimism/op-e2e/config"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
altda "github.com/ethereum-optimism/optimism/op-alt-da"
......@@ -151,6 +152,13 @@ func WithL2Claim(claim common.Hash) FixtureInputParam {
}
}
func WithAgreedPrestate(prestate []byte) FixtureInputParam {
return func(f *FixtureInputs) {
f.AgreedPrestate = prestate
f.L2OutputRoot = crypto.Keccak256Hash(prestate)
}
}
func WithL2BlockNumber(num uint64) FixtureInputParam {
return func(f *FixtureInputs) {
f.L2BlockNumber = num
......@@ -213,6 +221,9 @@ func NewOpProgramCfg(
dfault.DataDir = t.TempDir()
dfault.DataFormat = hostTypes.DataFormatPebble
}
if fi.InteropEnabled {
dfault.AgreedPrestate = fi.AgreedPrestate
}
dfault.InteropEnabled = fi.InteropEnabled
return dfault
}
......@@ -44,6 +44,7 @@ type FixtureInputs struct {
L2OutputRoot common.Hash `toml:"l2-output-root"`
L2ChainID uint64 `toml:"l2-chain-id"`
L1Head common.Hash `toml:"l1-head"`
AgreedPrestate []byte `toml:"agreed-prestate"`
InteropEnabled bool `toml:"use-interop"`
}
......
......@@ -83,7 +83,7 @@ func RunFaultProofProgram(t helpers.Testing, logger log.Logger, l1 *helpers.L1Mi
l2DebugCl := hostcommon.NewL2SourceWithClient(logger, l2Client, sources.NewDebugClient(l2RPC.CallContext))
executor := host.MakeProgramExecutor(logger, programCfg)
return prefetcher.NewPrefetcher(logger, l1Cl, l1BlobFetcher, l2DebugCl, kv, l2ChainConfig.Config, executor), nil
return prefetcher.NewPrefetcher(logger, l1Cl, l1BlobFetcher, l2DebugCl, kv, executor, cfg.AgreedPrestate), nil
})
err = hostcommon.FaultProofProgram(t.Ctx(), logger, programCfg, withInProcessPrefetcher)
checkResult(t, err)
......
......@@ -4,6 +4,7 @@ import (
"context"
"errors"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/log"
altda "github.com/ethereum-optimism/optimism/op-alt-da"
......@@ -17,7 +18,7 @@ import (
type EndCondition interface {
Closing() bool
Result() error
Result() (eth.L2BlockRef, error)
}
type Driver struct {
......@@ -51,7 +52,7 @@ func NewDriver(logger log.Logger, cfg *rollup.Config, l1Source derive.L1Fetcher,
logger: logger,
Emitter: d,
closing: false,
result: nil,
result: eth.L2BlockRef{},
targetBlockNum: targetBlockNum,
}
......@@ -73,7 +74,7 @@ func (d *Driver) Emit(ev event.Event) {
d.events = append(d.events, ev)
}
func (d *Driver) RunComplete() error {
func (d *Driver) RunComplete() (eth.L2BlockRef, error) {
// Initial reset
d.Emit(engine.ResetEngineRequestEvent{})
......@@ -83,7 +84,7 @@ func (d *Driver) RunComplete() error {
return d.end.Result()
}
if len(d.events) > 10000 { // sanity check, in case of bugs. Better than going OOM.
return errors.New("way too many events queued up, something is wrong")
return eth.L2BlockRef{}, errors.New("way too many events queued up, something is wrong")
}
ev := d.events[0]
d.events = d.events[1:]
......
......@@ -4,6 +4,7 @@ import (
"errors"
"testing"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/log"
......@@ -21,8 +22,8 @@ func (d *fakeEnd) Closing() bool {
return d.closing
}
func (d *fakeEnd) Result() error {
return d.result
func (d *fakeEnd) Result() (eth.L2BlockRef, error) {
return eth.L2BlockRef{}, d.result
}
func TestDriver(t *testing.T) {
......@@ -44,7 +45,8 @@ func TestDriver(t *testing.T) {
d := newTestDriver(t, func(d *Driver, end *fakeEnd, ev event.Event) {
end.closing = true
})
require.NoError(t, d.RunComplete())
_, err := d.RunComplete()
require.NoError(t, err)
})
t.Run("insta error", func(t *testing.T) {
......@@ -53,7 +55,8 @@ func TestDriver(t *testing.T) {
end.closing = true
end.result = mockErr
})
require.ErrorIs(t, mockErr, d.RunComplete())
_, err := d.RunComplete()
require.ErrorIs(t, mockErr, err)
})
t.Run("success after a few events", func(t *testing.T) {
......@@ -66,7 +69,8 @@ func TestDriver(t *testing.T) {
count += 1
d.Emit(TestEvent{})
})
require.NoError(t, d.RunComplete())
_, err := d.RunComplete()
require.NoError(t, err)
})
t.Run("error after a few events", func(t *testing.T) {
......@@ -81,7 +85,8 @@ func TestDriver(t *testing.T) {
count += 1
d.Emit(TestEvent{})
})
require.ErrorIs(t, mockErr, d.RunComplete())
_, err := d.RunComplete()
require.ErrorIs(t, mockErr, err)
})
t.Run("exhaust events", func(t *testing.T) {
......@@ -93,7 +98,8 @@ func TestDriver(t *testing.T) {
count += 1
})
// No further processing to be done so evaluate if the claims output root is correct.
require.NoError(t, d.RunComplete())
_, err := d.RunComplete()
require.NoError(t, err)
})
t.Run("queued events", func(t *testing.T) {
......@@ -105,7 +111,8 @@ func TestDriver(t *testing.T) {
}
count += 1
})
require.NoError(t, d.RunComplete())
_, err := d.RunComplete()
require.NoError(t, err)
// add 1 for initial event that RunComplete fires
require.Equal(t, 1+3*2, count, "must have queued up 2 events 3 times")
})
......
......@@ -3,6 +3,7 @@ package driver
import (
"fmt"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-node/rollup"
......@@ -21,7 +22,8 @@ type ProgramDeriver struct {
Emitter event.Emitter
closing bool
result error
result eth.L2BlockRef
resultError error
targetBlockNum uint64
}
......@@ -29,8 +31,8 @@ func (d *ProgramDeriver) Closing() bool {
return d.closing
}
func (d *ProgramDeriver) Result() error {
return d.result
func (d *ProgramDeriver) Result() (eth.L2BlockRef, error) {
return d.result, d.resultError
}
func (d *ProgramDeriver) OnEvent(ev event.Event) bool {
......@@ -58,20 +60,35 @@ func (d *ProgramDeriver) OnEvent(ev event.Event) bool {
// and continue with the next.
d.Emitter.Emit(engine.PendingSafeRequestEvent{})
case engine.ForkchoiceUpdateEvent:
// Track latest head.
if x.SafeL2Head.Number >= d.result.Number {
d.result = x.SafeL2Head
}
// Stop if we have reached the target block
if x.SafeL2Head.Number >= d.targetBlockNum {
d.logger.Info("Derivation complete: reached L2 block", "head", x.SafeL2Head)
d.logger.Info("Derivation complete: reached L2 block as safe", "head", x.SafeL2Head)
d.closing = true
}
case engine.LocalSafeUpdateEvent:
// Track latest head.
if x.Ref.Number >= d.result.Number {
d.result = x.Ref
}
// Stop if we have reached the target block
if x.Ref.Number >= d.targetBlockNum {
d.logger.Info("Derivation complete: reached L2 block as local safe", "head", x.Ref)
d.closing = true
}
case derive.DeriverIdleEvent:
// We dont't close the deriver yet, as the engine may still be processing events to reach
// We don't close the deriver yet, as the engine may still be processing events to reach
// the target. A ForkchoiceUpdateEvent will close the deriver when the target is reached.
d.logger.Info("Derivation complete: no further L1 data to process")
case rollup.ResetEvent:
d.closing = true
d.result = fmt.Errorf("unexpected reset error: %w", x.Err)
d.resultError = fmt.Errorf("unexpected reset error: %w", x.Err)
case rollup.L1TemporaryErrorEvent:
d.closing = true
d.result = fmt.Errorf("unexpected L1 error: %w", x.Err)
d.resultError = fmt.Errorf("unexpected L1 error: %w", x.Err)
case rollup.EngineTemporaryErrorEvent:
// (Legacy case): While most temporary errors are due to requests for external data failing which can't happen,
// they may also be returned due to other events like channels timing out so need to be handled
......@@ -79,7 +96,7 @@ func (d *ProgramDeriver) OnEvent(ev event.Event) bool {
d.Emitter.Emit(engine.PendingSafeRequestEvent{})
case rollup.CriticalErrorEvent:
d.closing = true
d.result = x.Err
d.resultError = x.Err
default:
// Other events can be ignored safely.
// They are broadcast, but only consumed by the other derivers,
......
......@@ -36,9 +36,9 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(engine.EngineResetConfirmedEvent{})
m.AssertExpectations(t)
require.False(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
require.False(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
})
// step 2: more derivation work, triggered when pending safe data is published
t.Run("pending safe update", func(t *testing.T) {
......@@ -48,7 +48,7 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(engine.PendingSafeUpdateEvent{PendingSafe: ref})
m.AssertExpectations(t)
require.False(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
})
// step 3: if no attributes are generated, loop back to derive more.
t.Run("deriver more", func(t *testing.T) {
......@@ -57,7 +57,7 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(derive.DeriverMoreEvent{})
m.AssertExpectations(t)
require.False(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
})
// step 4: if attributes are derived, pass them to the engine.
t.Run("derived attributes", func(t *testing.T) {
......@@ -68,7 +68,7 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(derive.DerivedAttributesEvent{Attributes: attrib})
m.AssertExpectations(t)
require.False(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
})
// step 5: if attributes were invalid, continue with derivation for new attributes.
t.Run("invalid payload", func(t *testing.T) {
......@@ -77,7 +77,7 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(engine.InvalidPayloadAttributesEvent{Attributes: &derive.AttributesWithParent{}})
m.AssertExpectations(t)
require.False(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
})
// step 6: if attributes were valid, we may have reached the target.
// Or back to step 2 (PendingSafeUpdateEvent)
......@@ -87,21 +87,21 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(engine.ForkchoiceUpdateEvent{SafeL2Head: eth.L2BlockRef{Number: 42 + 1}})
m.AssertExpectations(t)
require.True(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
})
t.Run("completed", func(t *testing.T) {
p, m := newProgram(t, 42)
p.OnEvent(engine.ForkchoiceUpdateEvent{SafeL2Head: eth.L2BlockRef{Number: 42}})
m.AssertExpectations(t)
require.True(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
})
t.Run("incomplete", func(t *testing.T) {
p, m := newProgram(t, 42)
p.OnEvent(engine.ForkchoiceUpdateEvent{SafeL2Head: eth.L2BlockRef{Number: 42 - 1}})
m.AssertExpectations(t)
require.False(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
})
})
// Do not stop processing when the deriver is idle, the engine may still be busy and create further events.
......@@ -110,7 +110,7 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(derive.DeriverIdleEvent{})
m.AssertExpectations(t)
require.False(t, p.closing)
require.Nil(t, p.result)
require.NoError(t, p.resultError)
})
// on inconsistent chain data: stop with error
t.Run("reset event", func(t *testing.T) {
......@@ -118,7 +118,7 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(rollup.ResetEvent{Err: errors.New("reset test err")})
m.AssertExpectations(t)
require.True(t, p.closing)
require.NotNil(t, p.result)
require.Error(t, p.resultError)
})
// on L1 temporary error: stop with error
t.Run("L1 temporary error event", func(t *testing.T) {
......@@ -126,7 +126,7 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(rollup.L1TemporaryErrorEvent{Err: errors.New("temp test err")})
m.AssertExpectations(t)
require.True(t, p.closing)
require.NotNil(t, p.result)
require.Error(t, p.resultError)
})
// on engine temporary error: continue derivation (because legacy, not all connection related)
t.Run("engine temp error event", func(t *testing.T) {
......@@ -135,7 +135,7 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(rollup.EngineTemporaryErrorEvent{Err: errors.New("temp test err")})
m.AssertExpectations(t)
require.False(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
})
// on critical error: stop
t.Run("critical error event", func(t *testing.T) {
......@@ -143,14 +143,14 @@ func TestProgramDeriver(t *testing.T) {
p.OnEvent(rollup.ResetEvent{Err: errors.New("crit test err")})
m.AssertExpectations(t)
require.True(t, p.closing)
require.NotNil(t, p.result)
require.Error(t, p.resultError)
})
t.Run("unknown event", func(t *testing.T) {
p, m := newProgram(t, 1000)
p.OnEvent(TestEvent{})
m.AssertExpectations(t)
require.False(t, p.closing)
require.NoError(t, p.result)
require.NoError(t, p.resultError)
})
}
......
......@@ -91,7 +91,7 @@ func runInteropProgram(logger log.Logger, bootInfo *boot.BootInfo, l1PreimageOra
if !validateClaim {
return nil
}
return claim.ValidateClaim(logger, derivationResult.SafeHead, eth.Bytes32(bootInfo.L2Claim), eth.Bytes32(expected))
return claim.ValidateClaim(logger, derivationResult.Head, eth.Bytes32(bootInfo.L2Claim), eth.Bytes32(expected))
}
type interopTaskExecutor struct {
......
......@@ -75,7 +75,7 @@ func (t *stubTasks) RunDerivation(
_ l1.Oracle,
_ l2.Oracle) (tasks.DerivationResult, error) {
return tasks.DerivationResult{
SafeHead: t.l2SafeHead,
Head: t.l2SafeHead,
BlockHash: t.blockHash,
OutputRoot: t.outputRoot,
}, t.err
......
......@@ -48,7 +48,7 @@ func (i *TransitionState) Hash() (common.Hash, error) {
return crypto.Keccak256Hash(data), nil
}
func UnmarshalProofsState(data []byte) (*TransitionState, error) {
func UnmarshalTransitionState(data []byte) (*TransitionState, error) {
if len(data) == 0 {
return nil, eth.ErrInvalidSuperRoot
}
......
......@@ -27,7 +27,7 @@ func TestTransitionStateCodec(t *testing.T) {
}
data, err := state.Marshal()
require.NoError(t, err)
actual, err := UnmarshalProofsState(data)
actual, err := UnmarshalTransitionState(data)
require.NoError(t, err)
require.Equal(t, state, actual)
})
......@@ -44,7 +44,7 @@ func TestTransitionStateCodec(t *testing.T) {
SuperRoot: superRoot.Marshal(),
}
data := superRoot.Marshal()
actual, err := UnmarshalProofsState(data)
actual, err := UnmarshalTransitionState(data)
require.NoError(t, err)
require.Equal(t, expected, actual)
})
......
......@@ -124,7 +124,7 @@ func (p *PreimageOracle) BlockDataByHash(agreedBlockHash, blockHash common.Hash,
func (p *PreimageOracle) TransitionStateByRoot(root common.Hash) *interopTypes.TransitionState {
p.hint.Hint(AgreedPrestateHint(root))
data := p.oracle.Get(preimage.Keccak256Key(root))
output, err := interopTypes.UnmarshalProofsState(data)
output, err := interopTypes.UnmarshalTransitionState(data)
if err != nil {
panic(fmt.Errorf("invalid agreed prestate data for root %s: %w", root, err))
}
......
......@@ -25,5 +25,5 @@ func RunPreInteropProgram(logger log.Logger, bootInfo *boot.BootInfo, l1Preimage
if err != nil {
return err
}
return claim.ValidateClaim(logger, result.SafeHead, eth.Bytes32(bootInfo.L2Claim), result.OutputRoot)
return claim.ValidateClaim(logger, result.Head, eth.Bytes32(bootInfo.L2Claim), result.OutputRoot)
}
package tasks
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-node/rollup"
......@@ -15,12 +14,11 @@ import (
)
type L2Source interface {
L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
L2OutputRoot(uint64) (common.Hash, eth.Bytes32, error)
}
type DerivationResult struct {
SafeHead eth.L2BlockRef
Head eth.L2BlockRef
BlockHash common.Hash
OutputRoot eth.Bytes32
}
......@@ -49,23 +47,20 @@ func RunDerivation(
logger.Info("Starting derivation")
d := cldr.NewDriver(logger, cfg, l1Source, l1BlobsSource, l2Source, l2ClaimBlockNum)
if err := d.RunComplete(); err != nil {
result, err := d.RunComplete()
if err != nil {
return DerivationResult{}, fmt.Errorf("failed to run program to completion: %w", err)
}
return loadOutputRoot(l2ClaimBlockNum, l2Source)
return loadOutputRoot(l2ClaimBlockNum, result, l2Source)
}
func loadOutputRoot(l2ClaimBlockNum uint64, src L2Source) (DerivationResult, error) {
l2Head, err := src.L2BlockRefByLabel(context.Background(), eth.Safe)
if err != nil {
return DerivationResult{}, fmt.Errorf("cannot retrieve safe head: %w", err)
}
blockHash, outputRoot, err := src.L2OutputRoot(min(l2ClaimBlockNum, l2Head.Number))
func loadOutputRoot(l2ClaimBlockNum uint64, head eth.L2BlockRef, src L2Source) (DerivationResult, error) {
blockHash, outputRoot, err := src.L2OutputRoot(min(l2ClaimBlockNum, head.Number))
if err != nil {
return DerivationResult{}, fmt.Errorf("calculate L2 output root: %w", err)
}
return DerivationResult{
SafeHead: l2Head,
Head: head,
BlockHash: blockHash,
OutputRoot: outputRoot,
}, nil
......
package tasks
import (
"context"
"errors"
"testing"
......@@ -12,66 +11,51 @@ import (
func TestLoadOutputRoot(t *testing.T) {
t.Run("Success", func(t *testing.T) {
safeHead := eth.L2BlockRef{Number: 65}
l2 := &mockL2{
blockHash: common.Hash{0x24},
outputRoot: eth.Bytes32{0x11},
safeL2: eth.L2BlockRef{Number: 65},
}
result, err := loadOutputRoot(uint64(0), l2)
result, err := loadOutputRoot(uint64(0), safeHead, l2)
require.NoError(t, err)
assertDerivationResult(t, result, l2.safeL2, l2.blockHash, l2.outputRoot)
assertDerivationResult(t, result, safeHead, l2.blockHash, l2.outputRoot)
})
t.Run("Success-PriorToSafeHead", func(t *testing.T) {
expected := eth.Bytes32{0x11}
safeHead := eth.L2BlockRef{
Number: 10,
}
l2 := &mockL2{
blockHash: common.Hash{0x24},
outputRoot: expected,
safeL2: eth.L2BlockRef{
Number: 10,
},
}
result, err := loadOutputRoot(uint64(20), l2)
result, err := loadOutputRoot(uint64(20), safeHead, l2)
require.NoError(t, err)
require.Equal(t, uint64(10), l2.requestedOutputRoot)
assertDerivationResult(t, result, l2.safeL2, l2.blockHash, l2.outputRoot)
})
t.Run("Error-SafeHead", func(t *testing.T) {
expectedErr := errors.New("boom")
l2 := &mockL2{
blockHash: common.Hash{0x24},
outputRoot: eth.Bytes32{0x11},
safeL2: eth.L2BlockRef{Number: 10},
safeL2Err: expectedErr,
}
_, err := loadOutputRoot(uint64(0), l2)
require.ErrorIs(t, err, expectedErr)
assertDerivationResult(t, result, safeHead, l2.blockHash, l2.outputRoot)
})
t.Run("Error-OutputRoot", func(t *testing.T) {
expectedErr := errors.New("boom")
safeHead := eth.L2BlockRef{Number: 10}
l2 := &mockL2{
blockHash: common.Hash{0x24},
outputRoot: eth.Bytes32{0x11},
outputRootErr: expectedErr,
safeL2: eth.L2BlockRef{Number: 10},
}
_, err := loadOutputRoot(uint64(0), l2)
_, err := loadOutputRoot(uint64(0), safeHead, l2)
require.ErrorIs(t, err, expectedErr)
})
}
func assertDerivationResult(t *testing.T, actual DerivationResult, safeHead eth.L2BlockRef, blockHash common.Hash, outputRoot eth.Bytes32) {
require.Equal(t, safeHead, actual.SafeHead)
require.Equal(t, safeHead, actual.Head)
require.Equal(t, blockHash, actual.BlockHash)
require.Equal(t, outputRoot, actual.OutputRoot)
}
type mockL2 struct {
safeL2 eth.L2BlockRef
safeL2Err error
blockHash common.Hash
outputRoot eth.Bytes32
outputRootErr error
......@@ -79,16 +63,6 @@ type mockL2 struct {
requestedOutputRoot uint64
}
func (m *mockL2) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) {
if label != eth.Safe {
panic("unexpected usage")
}
if m.safeL2Err != nil {
return eth.L2BlockRef{}, m.safeL2Err
}
return m.safeL2, nil
}
func (m *mockL2) L2OutputRoot(u uint64) (common.Hash, eth.Bytes32, error) {
m.requestedOutputRoot = u
if m.outputRootErr != nil {
......
......@@ -14,6 +14,7 @@ import (
"github.com/ethereum-optimism/optimism/op-program/host/types"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
......@@ -202,7 +203,11 @@ func TestL2Head(t *testing.T) {
func TestL2OutputRoot(t *testing.T) {
t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag l2.outputroot is required", addRequiredArgsExcept("--l2.outputroot"))
verifyArgsInvalid(t, "flag l2.outputroot or l2.agreed-prestate is required", addRequiredArgsExcept("--l2.outputroot"))
})
t.Run("NotRequiredWhenAgreedPrestateProvided", func(t *testing.T) {
configForArgs(t, addRequiredArgsExcept("--l2.outputroot", "--l2.agreed-prestate", "0x1234"))
})
t.Run("Valid", func(t *testing.T) {
......@@ -215,6 +220,33 @@ func TestL2OutputRoot(t *testing.T) {
})
}
func TestL2AgreedPrestate(t *testing.T) {
t.Run("NotRequiredWhenL2OutputRootProvided", func(t *testing.T) {
configForArgs(t, addRequiredArgsExcept("--l2.outputroot", "--l2.outputroot", "0x1234"))
})
t.Run("Valid", func(t *testing.T) {
prestate := "0x1234"
prestateBytes := common.FromHex(prestate)
expectedOutputRoot := crypto.Keccak256Hash(prestateBytes)
cfg := configForArgs(t, addRequiredArgsExcept("--l2.outputroot", "--l2.agreed-prestate", prestate))
require.Equal(t, expectedOutputRoot, cfg.L2OutputRoot)
require.Equal(t, prestateBytes, cfg.AgreedPrestate)
})
t.Run("MustNotSpecifyWithL2OutputRoot", func(t *testing.T) {
verifyArgsInvalid(t, "flag l2.outputroot and l2.agreed-prestate must not be specified together", addRequiredArgs("--l2.agreed-prestate", "0x1234"))
})
t.Run("Invalid", func(t *testing.T) {
verifyArgsInvalid(t, config.ErrInvalidAgreedPrestate.Error(), addRequiredArgsExcept("--l2.outputroot", "--l2.agreed-prestate", "something"))
})
t.Run("ZeroLength", func(t *testing.T) {
verifyArgsInvalid(t, config.ErrInvalidAgreedPrestate.Error(), addRequiredArgsExcept("--l2.outputroot", "--l2.agreed-prestate", "0x"))
})
}
func TestL1Head(t *testing.T) {
t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag l1.head is required", addRequiredArgsExcept("--l1.head"))
......
......@@ -12,6 +12,7 @@ import (
"github.com/ethereum-optimism/optimism/op-program/chainconfig"
"github.com/ethereum-optimism/optimism/op-program/client/boot"
"github.com/ethereum-optimism/optimism/op-program/host/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-program/host/flags"
......@@ -29,12 +30,14 @@ var (
ErrInvalidL1Head = errors.New("invalid l1 head")
ErrInvalidL2Head = errors.New("invalid l2 head")
ErrInvalidL2OutputRoot = errors.New("invalid l2 output root")
ErrInvalidAgreedPrestate = errors.New("invalid l2 agreed prestate")
ErrL1AndL2Inconsistent = errors.New("l1 and l2 options must be specified together or both omitted")
ErrInvalidL2Claim = errors.New("invalid l2 claim")
ErrInvalidL2ClaimBlock = errors.New("invalid l2 claim block number")
ErrDataDirRequired = errors.New("datadir must be specified when in non-fetching mode")
ErrNoExecInServerMode = errors.New("exec command must not be set when in server mode")
ErrInvalidDataFormat = errors.New("invalid data format")
ErrMissingAgreedPrestate = errors.New("missing agreed prestate")
)
type Config struct {
......@@ -80,6 +83,8 @@ type Config struct {
// InteropEnabled enables interop fault proof rules when running the client in-process
InteropEnabled bool
// AgreedPrestate is the preimage of the agreed prestate claim. Required for interop.
AgreedPrestate []byte
}
func (c *Config) Check() error {
......@@ -116,6 +121,14 @@ func (c *Config) Check() error {
if c.DataDir != "" && !slices.Contains(types.SupportedDataFormats, c.DataFormat) {
return ErrInvalidDataFormat
}
if c.InteropEnabled {
if len(c.AgreedPrestate) == 0 {
return ErrMissingAgreedPrestate
}
if crypto.Keccak256Hash(c.AgreedPrestate) != c.L2OutputRoot {
return fmt.Errorf("%w: must be preimage of L2 output root", ErrInvalidAgreedPrestate)
}
}
return nil
}
......@@ -162,7 +175,18 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) {
if l2Head == (common.Hash{}) {
return nil, ErrInvalidL2Head
}
l2OutputRoot := common.HexToHash(ctx.String(flags.L2OutputRoot.Name))
var l2OutputRoot common.Hash
var agreedPrestate []byte
if ctx.IsSet(flags.L2OutputRoot.Name) {
l2OutputRoot = common.HexToHash(ctx.String(flags.L2OutputRoot.Name))
} else if ctx.IsSet(flags.L2AgreedPrestate.Name) {
prestateStr := ctx.String(flags.L2AgreedPrestate.Name)
agreedPrestate = common.FromHex(prestateStr)
if len(agreedPrestate) == 0 {
return nil, ErrInvalidAgreedPrestate
}
l2OutputRoot = crypto.Keccak256Hash(agreedPrestate)
}
if l2OutputRoot == (common.Hash{}) {
return nil, ErrInvalidL2OutputRoot
}
......@@ -238,6 +262,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) {
L2ChainConfig: l2ChainConfig,
L2Head: l2Head,
L2OutputRoot: l2OutputRoot,
AgreedPrestate: agreedPrestate,
L2Claim: l2Claim,
L2ClaimBlockNumber: l2ClaimBlockNum,
L1Head: l1Head,
......
......@@ -11,6 +11,7 @@ import (
"github.com/ethereum-optimism/optimism/op-program/client/boot"
"github.com/ethereum-optimism/optimism/op-program/host/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require"
)
......@@ -174,7 +175,45 @@ func TestCustomL2ChainID(t *testing.T) {
cfg := NewConfig(validRollupConfig, customChainConfig, validL1Head, validL2Head, validL2OutputRoot, validL2Claim, validL2ClaimBlockNum)
require.Equal(t, cfg.L2ChainID, boot.CustomChainIDIndicator)
})
}
func TestAgreedPrestate(t *testing.T) {
t.Run("requiredWithInterop-nil", func(t *testing.T) {
cfg := validConfig()
cfg.InteropEnabled = true
cfg.AgreedPrestate = nil
err := cfg.Check()
require.ErrorIs(t, err, ErrMissingAgreedPrestate)
})
t.Run("requiredWithInterop-empty", func(t *testing.T) {
cfg := validConfig()
cfg.InteropEnabled = true
cfg.AgreedPrestate = []byte{}
err := cfg.Check()
require.ErrorIs(t, err, ErrMissingAgreedPrestate)
})
t.Run("notRequiredWithoutInterop", func(t *testing.T) {
cfg := validConfig()
cfg.AgreedPrestate = nil
require.NoError(t, cfg.Check())
})
t.Run("valid", func(t *testing.T) {
cfg := validConfig()
cfg.InteropEnabled = true
cfg.AgreedPrestate = []byte{1}
cfg.L2OutputRoot = crypto.Keccak256Hash(cfg.AgreedPrestate)
require.NoError(t, cfg.Check())
})
t.Run("mustMatchL2OutputRoot", func(t *testing.T) {
cfg := validConfig()
cfg.InteropEnabled = true
cfg.AgreedPrestate = []byte{1}
cfg.L2OutputRoot = common.Hash{0xaa}
require.ErrorIs(t, cfg.Check(), ErrInvalidAgreedPrestate)
})
}
func TestDBFormat(t *testing.T) {
......
......@@ -72,9 +72,15 @@ var (
}
L2OutputRoot = &cli.StringFlag{
Name: "l2.outputroot",
Usage: "Agreed L2 Output Root to start derivation from",
Usage: "Agreed L2 Output Root to start derivation from. Used for non-interop games.",
EnvVars: prefixEnvVars("L2_OUTPUT_ROOT"),
}
L2AgreedPrestate = &cli.StringFlag{
Name: "l2.agreed-prestate",
Usage: "Agreed L2 pre state pre-image to start derivation from. " +
"l2.outputroot will be automatically set to the hash of the prestate. Used for interop-enabled games.",
EnvVars: prefixEnvVars("L2_AGREED_PRESTATE"),
}
L2Claim = &cli.StringFlag{
Name: "l2.claim",
Usage: "Claimed L2 output root to validate",
......@@ -133,12 +139,13 @@ var Flags []cli.Flag
var requiredFlags = []cli.Flag{
L1Head,
L2Head,
L2OutputRoot,
L2Claim,
L2BlockNumber,
}
var programFlags = []cli.Flag{
L2OutputRoot,
L2AgreedPrestate,
L2Custom,
RollupConfig,
Network,
......@@ -184,5 +191,11 @@ func CheckRequired(ctx *cli.Context) error {
return fmt.Errorf("flag %s is required", flag.Names()[0])
}
}
if !ctx.IsSet(L2OutputRoot.Name) && !ctx.IsSet(L2AgreedPrestate.Name) {
return fmt.Errorf("flag %s or %s is required", L2OutputRoot.Name, L2AgreedPrestate.Name)
}
if ctx.IsSet(L2OutputRoot.Name) && ctx.IsSet(L2AgreedPrestate.Name) {
return fmt.Errorf("flag %s and %s must not be specified together", L2OutputRoot.Name, L2AgreedPrestate.Name)
}
return nil
}
......@@ -95,7 +95,7 @@ func makeDefaultPrefetcher(ctx context.Context, logger log.Logger, kv kvstore.KV
}
executor := MakeProgramExecutor(logger, cfg)
return prefetcher.NewPrefetcher(logger, l1Cl, l1BlobFetcher, l2Client, kv, cfg.L2ChainConfig, executor), nil
return prefetcher.NewPrefetcher(logger, l1Cl, l1BlobFetcher, l2Client, kv, executor, cfg.AgreedPrestate), nil
}
type programExecutor struct {
......
......@@ -27,6 +27,8 @@ import (
var (
precompileSuccess = [1]byte{1}
precompileFailure = [1]byte{0}
ErrAgreedPrestateUnavailable = errors.New("agreed prestate unavailable")
)
var acceleratedPrecompiles = []common.Address{
......@@ -56,6 +58,7 @@ type Prefetcher struct {
// Used to run the program for native block execution
executor ProgramExecutor
agreedPrestate []byte
}
func NewPrefetcher(
......@@ -64,8 +67,8 @@ func NewPrefetcher(
l1BlobFetcher L1BlobSource,
l2Fetcher hosttypes.L2Source,
kvStore kvstore.KV,
l2ChainConfig *params.ChainConfig,
executor ProgramExecutor,
agreedPrestate []byte,
) *Prefetcher {
return &Prefetcher{
logger: logger,
......@@ -74,6 +77,7 @@ func NewPrefetcher(
l2Fetcher: NewRetryingL2Source(logger, l2Fetcher),
kvStore: kvStore,
executor: executor,
agreedPrestate: agreedPrestate,
}
}
......@@ -312,6 +316,12 @@ func (p *Prefetcher) prefetch(ctx context.Context, hint string) error {
return fmt.Errorf("failed to re-execute block: %w", err)
}
return p.kvStore.Put(BlockDataKey(blockHash).Key(), []byte{1})
case l2.HintAgreedPrestate:
if len(p.agreedPrestate) == 0 {
return ErrAgreedPrestateUnavailable
}
hash := crypto.Keccak256Hash(p.agreedPrestate)
return p.kvStore.Put(preimage.Keccak256Key(hash).PreimageKey(), p.agreedPrestate)
}
return fmt.Errorf("unknown hint type: %v", hintType)
}
......
......@@ -567,6 +567,28 @@ func TestFetchL2BlockData(t *testing.T) {
})
}
func TestFetchAgreedPrestate(t *testing.T) {
t.Run("unavailable", func(t *testing.T) {
prefetcher, _, _, _, _ := createPrefetcher(t)
hash := common.Hash{0xaa}
hint := l2.AgreedPrestateHint(hash).Hint()
require.NoError(t, prefetcher.Hint(hint))
_, err := prefetcher.GetPreimage(context.Background(), hash)
require.ErrorIs(t, err, ErrAgreedPrestateUnavailable)
})
t.Run("available", func(t *testing.T) {
prestate := []byte{1, 2, 3, 6}
prefetcher, _, _, _, _ := createPrefetcherWithAgreedPrestate(t, prestate)
hash := crypto.Keccak256Hash(prestate)
hint := l2.AgreedPrestateHint(hash).Hint()
require.NoError(t, prefetcher.Hint(hint))
actual, err := prefetcher.GetPreimage(context.Background(), preimage.Keccak256Key(hash).PreimageKey())
require.NoError(t, err)
require.Equal(t, prestate, actual)
})
}
func TestBadHints(t *testing.T) {
prefetcher, _, _, _, kv := createPrefetcher(t)
hash := common.Hash{0xad}
......@@ -666,6 +688,9 @@ func (m *l2Client) ExpectOutputByRoot(root common.Hash, output eth.Output, err e
}
func createPrefetcher(t *testing.T) (*Prefetcher, *testutils.MockL1Source, *testutils.MockBlobsFetcher, *l2Client, kvstore.KV) {
return createPrefetcherWithAgreedPrestate(t, nil)
}
func createPrefetcherWithAgreedPrestate(t *testing.T, agreedPrestate []byte) (*Prefetcher, *testutils.MockL1Source, *testutils.MockBlobsFetcher, *l2Client, kvstore.KV) {
logger := testlog.Logger(t, log.LevelDebug)
kv := kvstore.NewMemKV()
......@@ -676,7 +701,7 @@ func createPrefetcher(t *testing.T) (*Prefetcher, *testutils.MockL1Source, *test
MockDebugClient: new(testutils.MockDebugClient),
}
prefetcher := NewPrefetcher(logger, l1Source, l1BlobSource, l2Source, kv, nil, nil)
prefetcher := NewPrefetcher(logger, l1Source, l1BlobSource, l2Source, kv, nil, agreedPrestate)
return prefetcher, l1Source, l1BlobSource, l2Source, kv
}
......
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