Commit 5209734b authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-program: Refactor derivation task to be separate from bootstrapping the...

op-program: Refactor derivation task to be separate from bootstrapping the program and verifying outputs. (#13612)

Makes the prepare, derive, verify steps more clearly separate.
parent 663d9303
package claim
import (
"context"
"errors"
"fmt"
......@@ -12,20 +11,7 @@ import (
var ErrClaimNotValid = errors.New("invalid claim")
type L2Source interface {
L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
L2OutputRoot(uint64) (eth.Bytes32, error)
}
func ValidateClaim(log log.Logger, l2ClaimBlockNum uint64, claimedOutputRoot eth.Bytes32, src L2Source) error {
l2Head, err := src.L2BlockRefByLabel(context.Background(), eth.Safe)
if err != nil {
return fmt.Errorf("cannot retrieve safe head: %w", err)
}
outputRoot, err := src.L2OutputRoot(min(l2ClaimBlockNum, l2Head.Number))
if err != nil {
return fmt.Errorf("calculate L2 output root: %w", err)
}
func ValidateClaim(log log.Logger, l2Head eth.L2BlockRef, claimedOutputRoot eth.Bytes32, outputRoot eth.Bytes32) error {
log.Info("Validating claim", "head", l2Head, "output", outputRoot, "claim", claimedOutputRoot)
if claimedOutputRoot != outputRoot {
return fmt.Errorf("%w: claim: %v actual: %v", ErrClaimNotValid, claimedOutputRoot, outputRoot)
......
package claim
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/require"
......@@ -13,101 +11,22 @@ import (
"github.com/ethereum-optimism/optimism/op-service/testlog"
)
type mockL2 struct {
safeL2 eth.L2BlockRef
safeL2Err error
outputRoot eth.Bytes32
outputRootErr error
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) (eth.Bytes32, error) {
m.requestedOutputRoot = u
if m.outputRootErr != nil {
return eth.Bytes32{}, m.outputRootErr
}
return m.outputRoot, nil
}
var _ L2Source = (*mockL2)(nil)
func TestValidateClaim(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
expected := eth.Bytes32{0x11}
l2 := &mockL2{
outputRoot: expected,
}
logger := testlog.Logger(t, log.LevelError)
err := ValidateClaim(logger, uint64(0), expected, l2)
require.NoError(t, err)
})
t.Run("Valid-PriorToSafeHead", func(t *testing.T) {
expected := eth.Bytes32{0x11}
l2 := &mockL2{
outputRoot: expected,
safeL2: eth.L2BlockRef{
Number: 10,
},
}
actual := eth.Bytes32{0x11}
l2Head := eth.L2BlockRef{Number: 42}
logger := testlog.Logger(t, log.LevelError)
err := ValidateClaim(logger, uint64(20), expected, l2)
err := ValidateClaim(logger, l2Head, expected, actual)
require.NoError(t, err)
require.Equal(t, uint64(10), l2.requestedOutputRoot)
})
t.Run("Invalid", func(t *testing.T) {
l2 := &mockL2{
outputRoot: eth.Bytes32{0x22},
}
logger := testlog.Logger(t, log.LevelError)
err := ValidateClaim(logger, uint64(0), eth.Bytes32{0x11}, l2)
require.ErrorIs(t, err, ErrClaimNotValid)
})
t.Run("Invalid-PriorToSafeHead", func(t *testing.T) {
l2 := &mockL2{
outputRoot: eth.Bytes32{0x22},
safeL2: eth.L2BlockRef{Number: 10},
}
expected := eth.Bytes32{0x11}
actual := eth.Bytes32{0x22}
l2Head := eth.L2BlockRef{Number: 42}
logger := testlog.Logger(t, log.LevelError)
err := ValidateClaim(logger, uint64(20), eth.Bytes32{0x55}, l2)
err := ValidateClaim(logger, l2Head, expected, actual)
require.ErrorIs(t, err, ErrClaimNotValid)
require.Equal(t, uint64(10), l2.requestedOutputRoot)
})
t.Run("Error-safe-head", func(t *testing.T) {
expectedErr := errors.New("boom")
l2 := &mockL2{
outputRoot: eth.Bytes32{0x11},
safeL2: eth.L2BlockRef{Number: 10},
safeL2Err: expectedErr,
}
logger := testlog.Logger(t, log.LevelError)
err := ValidateClaim(logger, uint64(0), eth.Bytes32{0x11}, l2)
require.ErrorIs(t, err, expectedErr)
})
t.Run("Error-output-root", func(t *testing.T) {
expectedErr := errors.New("boom")
l2 := &mockL2{
outputRoot: eth.Bytes32{0x11},
outputRootErr: expectedErr,
safeL2: eth.L2BlockRef{Number: 10},
}
logger := testlog.Logger(t, log.LevelError)
err := ValidateClaim(logger, uint64(0), eth.Bytes32{0x11}, l2)
require.ErrorIs(t, err, expectedErr)
})
}
......@@ -2,20 +2,16 @@ package client
import (
"errors"
"fmt"
"io"
"os"
"github.com/ethereum-optimism/optimism/op-node/rollup"
preimage "github.com/ethereum-optimism/optimism/op-preimage"
"github.com/ethereum-optimism/optimism/op-program/client/claim"
cldr "github.com/ethereum-optimism/optimism/op-program/client/driver"
"github.com/ethereum-optimism/optimism/op-program/client/l1"
"github.com/ethereum-optimism/optimism/op-program/client/l2"
"github.com/ethereum-optimism/optimism/op-program/client/tasks"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)
// Main executes the client program in a detached context and exits the current process.
......@@ -45,33 +41,18 @@ func RunProgram(logger log.Logger, preimageOracle io.ReadWriter, preimageHinter
bootInfo := NewBootstrapClient(pClient).BootInfo()
logger.Info("Program Bootstrapped", "bootInfo", bootInfo)
return runDerivation(
safeHead, outputRoot, err := tasks.RunDerivation(
logger,
bootInfo.RollupConfig,
bootInfo.L2ChainConfig,
bootInfo.L1Head,
bootInfo.L2OutputRoot,
bootInfo.L2Claim,
bootInfo.L2ClaimBlockNumber,
l1PreimageOracle,
l2PreimageOracle,
)
}
// runDerivation executes the L2 state transition, given a minimal interface to retrieve data.
func runDerivation(logger log.Logger, cfg *rollup.Config, l2Cfg *params.ChainConfig, l1Head common.Hash, l2OutputRoot common.Hash, l2Claim common.Hash, l2ClaimBlockNum uint64, l1Oracle l1.Oracle, l2Oracle l2.Oracle) error {
l1Source := l1.NewOracleL1Client(logger, l1Oracle, l1Head)
l1BlobsSource := l1.NewBlobFetcher(logger, l1Oracle)
engineBackend, err := l2.NewOracleBackedL2Chain(logger, l2Oracle, l1Oracle /* kzg oracle */, l2Cfg, l2OutputRoot)
if err != nil {
return fmt.Errorf("failed to create oracle-backed L2 chain: %w", err)
}
l2Source := l2.NewOracleEngine(cfg, logger, engineBackend)
logger.Info("Starting derivation")
d := cldr.NewDriver(logger, cfg, l1Source, l1BlobsSource, l2Source, l2ClaimBlockNum)
if err := d.RunComplete(); err != nil {
return fmt.Errorf("failed to run program to completion: %w", err)
return err
}
return claim.ValidateClaim(logger, l2ClaimBlockNum, eth.Bytes32(l2Claim), l2Source)
return claim.ValidateClaim(logger, safeHead, eth.Bytes32(bootInfo.L2Claim), outputRoot)
}
package tasks
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-node/rollup"
cldr "github.com/ethereum-optimism/optimism/op-program/client/driver"
"github.com/ethereum-optimism/optimism/op-program/client/l1"
"github.com/ethereum-optimism/optimism/op-program/client/l2"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)
type L2Source interface {
L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
L2OutputRoot(uint64) (eth.Bytes32, error)
}
// RunDerivation executes the L2 state transition, given a minimal interface to retrieve data.
// Returns the L2BlockRef of the safe head reached and the output root at l2ClaimBlockNum or
// the final safe head when l1Head is reached if l2ClaimBlockNum is not reached.
// Derivation may stop prior to l1Head if the l2ClaimBlockNum has already been reached though
// this is not guaranteed.
func RunDerivation(
logger log.Logger,
cfg *rollup.Config,
l2Cfg *params.ChainConfig,
l1Head common.Hash,
l2OutputRoot common.Hash,
l2ClaimBlockNum uint64,
l1Oracle l1.Oracle,
l2Oracle l2.Oracle) (eth.L2BlockRef, eth.Bytes32, error) {
l1Source := l1.NewOracleL1Client(logger, l1Oracle, l1Head)
l1BlobsSource := l1.NewBlobFetcher(logger, l1Oracle)
engineBackend, err := l2.NewOracleBackedL2Chain(logger, l2Oracle, l1Oracle /* kzg oracle */, l2Cfg, l2OutputRoot)
if err != nil {
return eth.L2BlockRef{}, eth.Bytes32{}, fmt.Errorf("failed to create oracle-backed L2 chain: %w", err)
}
l2Source := l2.NewOracleEngine(cfg, logger, engineBackend)
logger.Info("Starting derivation")
d := cldr.NewDriver(logger, cfg, l1Source, l1BlobsSource, l2Source, l2ClaimBlockNum)
if err := d.RunComplete(); err != nil {
return eth.L2BlockRef{}, eth.Bytes32{}, fmt.Errorf("failed to run program to completion: %w", err)
}
return loadOutputRoot(l2ClaimBlockNum, l2Source)
}
func loadOutputRoot(l2ClaimBlockNum uint64, src L2Source) (eth.L2BlockRef, eth.Bytes32, error) {
l2Head, err := src.L2BlockRefByLabel(context.Background(), eth.Safe)
if err != nil {
return eth.L2BlockRef{}, eth.Bytes32{}, fmt.Errorf("cannot retrieve safe head: %w", err)
}
outputRoot, err := src.L2OutputRoot(min(l2ClaimBlockNum, l2Head.Number))
if err != nil {
return eth.L2BlockRef{}, eth.Bytes32{}, fmt.Errorf("calculate L2 output root: %w", err)
}
return l2Head, outputRoot, nil
}
package tasks
import (
"context"
"errors"
"testing"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/stretchr/testify/require"
)
func TestLoadOutputRoot(t *testing.T) {
t.Run("Success", func(t *testing.T) {
expected := eth.Bytes32{0x11}
l2 := &mockL2{
outputRoot: expected,
safeL2: eth.L2BlockRef{Number: 65},
}
safeHead, outputRoot, err := loadOutputRoot(uint64(0), l2)
require.NoError(t, err)
require.Equal(t, l2.safeL2, safeHead)
require.Equal(t, expected, outputRoot)
})
t.Run("Success-PriorToSafeHead", func(t *testing.T) {
expected := eth.Bytes32{0x11}
l2 := &mockL2{
outputRoot: expected,
safeL2: eth.L2BlockRef{
Number: 10,
},
}
safeHead, outputRoot, err := loadOutputRoot(uint64(20), l2)
require.NoError(t, err)
require.Equal(t, uint64(10), l2.requestedOutputRoot)
require.Equal(t, l2.safeL2, safeHead)
require.Equal(t, expected, outputRoot)
})
t.Run("Error-SafeHead", func(t *testing.T) {
expectedErr := errors.New("boom")
l2 := &mockL2{
outputRoot: eth.Bytes32{0x11},
safeL2: eth.L2BlockRef{Number: 10},
safeL2Err: expectedErr,
}
_, _, err := loadOutputRoot(uint64(0), l2)
require.ErrorIs(t, err, expectedErr)
})
t.Run("Error-OutputRoot", func(t *testing.T) {
expectedErr := errors.New("boom")
l2 := &mockL2{
outputRoot: eth.Bytes32{0x11},
outputRootErr: expectedErr,
safeL2: eth.L2BlockRef{Number: 10},
}
_, _, err := loadOutputRoot(uint64(0), l2)
require.ErrorIs(t, err, expectedErr)
})
}
type mockL2 struct {
safeL2 eth.L2BlockRef
safeL2Err error
outputRoot eth.Bytes32
outputRootErr error
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) (eth.Bytes32, error) {
m.requestedOutputRoot = u
if m.outputRootErr != nil {
return eth.Bytes32{}, m.outputRootErr
}
return m.outputRoot, nil
}
var _ L2Source = (*mockL2)(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