Commit 85ff49b8 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Use proof from last step (#6652)

* op-challenger: Increase the default cannon snapshot frequency

Generating snapshots is very slow and uses a lot of disk space so they should be relatively infrequent.

* op-challenger: Use proof from last step when required trace index is beyond end of trace
parent beeffe9c
......@@ -58,7 +58,7 @@ func ValidTraceType(value TraceType) bool {
return false
}
const DefaultCannonSnapshotFreq = uint(10_000)
const DefaultCannonSnapshotFreq = uint(1_000_000_000)
// Config is a well typed config that is parsed from the CLI params.
// This also contains config options for auxiliary services.
......
package cannon
import (
"encoding/json"
"fmt"
"os"
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
)
func parseState(path string) (*mipsevm.State, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("cannot open state file (%v): %w", path, err)
}
defer file.Close()
var state mipsevm.State
err = json.NewDecoder(file).Decode(&state)
if err != nil {
return nil, fmt.Errorf("invalid mipsevm state (%v): %w", path, err)
}
return &state, nil
}
......@@ -20,6 +20,7 @@ import (
const (
snapsDir = "snapshots"
preimagesDir = "preimages"
finalState = "final.json"
)
var snapshotNameRegexp = regexp.MustCompile(`^[0-9]+\.json$`)
......@@ -71,15 +72,21 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro
}
proofDir := filepath.Join(dir, proofsDir)
dataDir := filepath.Join(e.dataDir, preimagesDir)
lastGeneratedState := filepath.Join(dir, finalState)
args := []string{
"run",
"--input", start,
"--output", filepath.Join(dir, "out.json"),
"--output", lastGeneratedState,
"--meta", "",
"--proof-at", "=" + strconv.FormatUint(i, 10),
"--proof-fmt", filepath.Join(proofDir, "%d.json"),
"--snapshot-at", "%" + strconv.FormatUint(uint64(e.snapshotFreq), 10),
"--snapshot-fmt", filepath.Join(snapshotDir, "%d.json"),
}
if i < math.MaxUint64 {
args = append(args, "--stop-at", "="+strconv.FormatUint(i+1, 10))
}
args = append(args,
"--",
e.server, "--server",
"--l1", e.l1,
......@@ -90,10 +97,7 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro
"--l2.outputroot", e.inputs.l2OutputRoot.Hex(),
"--l2.claim", e.inputs.l2Claim.Hex(),
"--l2.blocknumber", e.inputs.l2BlockNumber.Text(10),
}
if i < math.MaxUint64 {
args = append(args, "--stop-at", "="+strconv.FormatUint(i+1, 10))
}
)
if e.network != "" {
args = append(args, "--network", e.network)
}
......
......@@ -36,7 +36,7 @@ func TestGenerateProof(t *testing.T) {
l2Claim: common.Hash{0x44},
l2BlockNumber: big.NewInt(3333),
}
captureExec := func(cfg config.Config, proofAt uint64) (string, string, map[string]string) {
captureExec := func(t *testing.T, cfg config.Config, proofAt uint64) (string, string, map[string]string) {
executor := NewExecutor(testlog.Logger(t, log.LvlInfo), &cfg, inputs)
executor.selectSnapshot = func(logger log.Logger, dir string, absolutePreState string, i uint64) (string, error) {
return input, nil
......@@ -67,7 +67,7 @@ func TestGenerateProof(t *testing.T) {
cfg.CannonNetwork = "mainnet"
cfg.CannonRollupConfigPath = ""
cfg.CannonL2GenesisPath = ""
binary, subcommand, args := captureExec(cfg, 150_000_000)
binary, subcommand, args := captureExec(t, cfg, 150_000_000)
require.DirExists(t, filepath.Join(cfg.CannonDatadir, preimagesDir))
require.DirExists(t, filepath.Join(cfg.CannonDatadir, proofsDir))
require.DirExists(t, filepath.Join(cfg.CannonDatadir, snapsDir))
......@@ -76,7 +76,7 @@ func TestGenerateProof(t *testing.T) {
require.Equal(t, input, args["--input"])
require.Contains(t, args, "--meta")
require.Equal(t, "", args["--meta"])
require.Equal(t, filepath.Join(cfg.CannonDatadir, "out.json"), args["--output"])
require.Equal(t, filepath.Join(cfg.CannonDatadir, finalState), args["--output"])
require.Equal(t, "=150000000", args["--proof-at"])
require.Equal(t, "=150000001", args["--stop-at"])
require.Equal(t, "%500", args["--snapshot-at"])
......@@ -105,7 +105,7 @@ func TestGenerateProof(t *testing.T) {
cfg.CannonNetwork = ""
cfg.CannonRollupConfigPath = "rollup.json"
cfg.CannonL2GenesisPath = "genesis.json"
_, _, args := captureExec(cfg, 150_000_000)
_, _, args := captureExec(t, cfg, 150_000_000)
require.NotContains(t, args, "--network")
require.Equal(t, cfg.CannonRollupConfigPath, args["--rollup.config"])
require.Equal(t, cfg.CannonL2GenesisPath, args["--l2.genesis"])
......@@ -115,7 +115,7 @@ func TestGenerateProof(t *testing.T) {
cfg.CannonNetwork = "mainnet"
cfg.CannonRollupConfigPath = "rollup.json"
cfg.CannonL2GenesisPath = "genesis.json"
_, _, args := captureExec(cfg, math.MaxUint64)
_, _, args := captureExec(t, cfg, math.MaxUint64)
// stop-at would need to be one more than the proof step which would overflow back to 0
// so expect that it will be omitted. We'll ultimately want cannon to execute until the program exits.
require.NotContains(t, args, "--stop-at")
......
......@@ -15,6 +15,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
......@@ -38,9 +39,14 @@ type ProofGenerator interface {
}
type CannonTraceProvider struct {
logger log.Logger
dir string
prestate string
generator ProofGenerator
// lastStep stores the last step in the actual trace if known. 0 indicates unknown.
// Cached as an optimisation to avoid repeatedly attempting to execute beyond the end of the trace.
lastStep uint64
}
func NewTraceProvider(ctx context.Context, logger log.Logger, cfg *config.Config, l1Client bind.ContractCaller) (*CannonTraceProvider, error) {
......@@ -58,6 +64,7 @@ func NewTraceProvider(ctx context.Context, logger log.Logger, cfg *config.Config
return nil, fmt.Errorf("fetch local game inputs: %w", err)
}
return &CannonTraceProvider{
logger: logger,
dir: cfg.CannonDatadir,
prestate: cfg.CannonAbsolutePreState,
generator: NewExecutor(logger, cfg, l1Head),
......@@ -65,7 +72,7 @@ func NewTraceProvider(ctx context.Context, logger log.Logger, cfg *config.Config
}
func (p *CannonTraceProvider) GetOracleData(ctx context.Context, i uint64) (*types.PreimageOracleData, error) {
proof, err := p.loadProof(ctx, i)
proof, err := p.loadProofData(ctx, i)
if err != nil {
return nil, err
}
......@@ -74,10 +81,14 @@ func (p *CannonTraceProvider) GetOracleData(ctx context.Context, i uint64) (*typ
}
func (p *CannonTraceProvider) Get(ctx context.Context, i uint64) (common.Hash, error) {
proof, err := p.loadProof(ctx, i)
proof, state, err := p.loadProof(ctx, i)
if err != nil {
return common.Hash{}, err
}
if proof == nil && state != nil {
// Use the hash from the final state
return crypto.Keccak256Hash(state.EncodeWitness()), nil
}
value := common.BytesToHash(proof.ClaimValue)
if value == (common.Hash{}) {
......@@ -87,7 +98,7 @@ func (p *CannonTraceProvider) Get(ctx context.Context, i uint64) (common.Hash, e
}
func (p *CannonTraceProvider) GetPreimage(ctx context.Context, i uint64) ([]byte, []byte, error) {
proof, err := p.loadProof(ctx, i)
proof, err := p.loadProofData(ctx, i)
if err != nil {
return nil, nil, err
}
......@@ -104,37 +115,77 @@ func (p *CannonTraceProvider) GetPreimage(ctx context.Context, i uint64) ([]byte
func (p *CannonTraceProvider) AbsolutePreState(ctx context.Context) ([]byte, error) {
path := filepath.Join(p.dir, p.prestate)
file, err := os.Open(path)
state, err := parseState(path)
if err != nil {
return []byte{}, fmt.Errorf("cannot open state file (%v): %w", path, err)
return []byte{}, fmt.Errorf("cannot load absolute pre-state: %w", err)
}
defer file.Close()
var state mipsevm.State
err = json.NewDecoder(file).Decode(&state)
return state.EncodeWitness(), nil
}
// loadProofData loads the proof data for the specified step.
// If the requested index is beyond the end of the actual trace, the proof data from the last step is returned.
// Cannon will be executed a second time if required to generate the full proof data.
func (p *CannonTraceProvider) loadProofData(ctx context.Context, i uint64) (*proofData, error) {
proof, state, err := p.loadProof(ctx, i)
if err != nil {
return []byte{}, fmt.Errorf("invalid mipsevm state (%v): %w", path, err)
return nil, err
} else if proof == nil && state != nil {
p.logger.Info("Re-executing to generate proof for last step", "step", state.Step)
proof, _, err = p.loadProof(ctx, state.Step)
if err != nil {
return nil, err
}
if proof == nil {
return nil, fmt.Errorf("proof at step %v was not generated", i)
}
return proof, nil
}
return state.EncodeWitness(), nil
return proof, nil
}
func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofData, error) {
// loadProof will attempt to load or generate the proof data at the specified index
// If the requested index is beyond the end of the actual trace:
// - When the actual trace length is known, the proof data from the last step is returned with nil state
// - When the actual trace length is not yet know, the state from after the last step is returned with nil proofData
// and the actual trace length is cached for future runs
func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofData, *mipsevm.State, error) {
if p.lastStep != 0 && i > p.lastStep {
// If the requested index is after the last step in the actual trace, use the last step
i = p.lastStep
}
path := filepath.Join(p.dir, proofsDir, fmt.Sprintf("%d.json", i))
file, err := os.Open(path)
if errors.Is(err, os.ErrNotExist) {
if err := p.generator.GenerateProof(ctx, p.dir, i); err != nil {
return nil, fmt.Errorf("generate cannon trace with proof at %v: %w", i, err)
return nil, nil, fmt.Errorf("generate cannon trace with proof at %v: %w", i, err)
}
// Try opening the file again now and it should exist.
file, err = os.Open(path)
if errors.Is(err, os.ErrNotExist) {
// Expected proof wasn't generated, check if we reached the end of execution
state, err := parseState(filepath.Join(p.dir, finalState))
if err != nil {
return nil, nil, fmt.Errorf("cannot read final state: %w", err)
}
if state.Exited && state.Step < i {
p.logger.Warn("Requested proof was after the program exited", "proof", i, "last", state.Step)
// The final instruction has already been applied to this state, so the last step we can execute
// is one before its Step value.
p.lastStep = state.Step - 1
return nil, state, nil
} else {
return nil, nil, fmt.Errorf("expected proof not generated but final state was not exited, requested step %v, final state at step %v", i, state.Step)
}
}
}
if err != nil {
return nil, fmt.Errorf("cannot open proof file (%v): %w", path, err)
return nil, nil, fmt.Errorf("cannot open proof file (%v): %w", path, err)
}
defer file.Close()
var proof proofData
err = json.NewDecoder(file).Decode(&proof)
if err != nil {
return nil, fmt.Errorf("failed to read proof (%v): %w", path, err)
return nil, nil, fmt.Errorf("failed to read proof (%v): %w", path, err)
}
return &proof, nil
return &proof, nil, nil
}
......@@ -4,12 +4,17 @@ import (
"context"
"embed"
_ "embed"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
......@@ -19,29 +24,35 @@ var testData embed.FS
func TestGet(t *testing.T) {
dataDir, prestate := setupTestData(t)
t.Run("ExistingProof", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
provider, generator := setupWithTestData(t, dataDir, prestate)
value, err := provider.Get(context.Background(), 0)
require.NoError(t, err)
require.Equal(t, common.HexToHash("0x45fd9aa59768331c726e719e76aa343e73123af888804604785ae19506e65e87"), value)
require.Empty(t, generator.generated)
})
t.Run("ProofUnavailable", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
_, err := provider.Get(context.Background(), 7)
require.ErrorIs(t, err, os.ErrNotExist)
require.Contains(t, generator.generated, 7, "should have tried to generate the proof")
t.Run("ProofAfterEndOfTrace", func(t *testing.T) {
provider, generator := setupWithTestData(t, dataDir, prestate)
generator.finalState = &mipsevm.State{
Memory: &mipsevm.Memory{},
Step: 10,
Exited: true,
}
value, err := provider.Get(context.Background(), 7000)
require.NoError(t, err)
require.Contains(t, generator.generated, 7000, "should have tried to generate the proof")
require.Equal(t, crypto.Keccak256Hash(generator.finalState.EncodeWitness()), value)
})
t.Run("MissingPostHash", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
provider, generator := setupWithTestData(t, dataDir, prestate)
_, err := provider.Get(context.Background(), 1)
require.ErrorContains(t, err, "missing post hash")
require.Empty(t, generator.generated)
})
t.Run("IgnoreUnknownFields", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
provider, generator := setupWithTestData(t, dataDir, prestate)
value, err := provider.Get(context.Background(), 2)
require.NoError(t, err)
expected := common.HexToHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
......@@ -53,7 +64,7 @@ func TestGet(t *testing.T) {
func TestGetOracleData(t *testing.T) {
dataDir, prestate := setupTestData(t)
t.Run("ExistingProof", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
provider, generator := setupWithTestData(t, dataDir, prestate)
oracleData, err := provider.GetOracleData(context.Background(), 420)
require.NoError(t, err)
require.False(t, oracleData.IsLocal)
......@@ -64,15 +75,32 @@ func TestGetOracleData(t *testing.T) {
require.Empty(t, generator.generated)
})
t.Run("ProofUnavailable", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
_, err := provider.GetOracleData(context.Background(), 7)
require.ErrorIs(t, err, os.ErrNotExist)
require.Contains(t, generator.generated, 7, "should have tried to generate the proof")
t.Run("ProofAfterEndOfTrace", func(t *testing.T) {
provider, generator := setupWithTestData(t, dataDir, prestate)
generator.finalState = &mipsevm.State{
Memory: &mipsevm.Memory{},
Step: 10,
Exited: true,
}
generator.proof = &proofData{
ClaimValue: common.Hash{0xaa}.Bytes(),
StateData: []byte{0xbb},
ProofData: []byte{0xcc},
OracleKey: common.Hash{0xdd}.Bytes(),
OracleValue: []byte{0xdd},
OracleOffset: 10,
}
oracleData, err := provider.GetOracleData(context.Background(), 7000)
require.NoError(t, err)
require.Contains(t, generator.generated, 7000, "should have tried to generate the proof")
require.Contains(t, generator.generated, 9, "should have regenerated proof from last step")
require.False(t, oracleData.IsLocal)
require.EqualValues(t, generator.proof.OracleKey, oracleData.OracleKey)
require.EqualValues(t, generator.proof.OracleValue, oracleData.OracleData)
})
t.Run("IgnoreUnknownFields", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
provider, generator := setupWithTestData(t, dataDir, prestate)
oracleData, err := provider.GetOracleData(context.Background(), 421)
require.NoError(t, err)
require.False(t, oracleData.IsLocal)
......@@ -87,7 +115,7 @@ func TestGetOracleData(t *testing.T) {
func TestGetPreimage(t *testing.T) {
dataDir, prestate := setupTestData(t)
t.Run("ExistingProof", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
provider, generator := setupWithTestData(t, dataDir, prestate)
value, proof, err := provider.GetPreimage(context.Background(), 0)
require.NoError(t, err)
expected := common.Hex2Bytes("b8f068de604c85ea0e2acd437cdb47add074a2d70b81d018390c504b71fe26f400000000000000000000000000000000000000000000000000000000000000000000000000")
......@@ -97,22 +125,38 @@ func TestGetPreimage(t *testing.T) {
require.Empty(t, generator.generated)
})
t.Run("ProofUnavailable", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
_, _, err := provider.GetPreimage(context.Background(), 7)
require.ErrorIs(t, err, os.ErrNotExist)
require.Contains(t, generator.generated, 7, "should have tried to generate the proof")
t.Run("ProofAfterEndOfTrace", func(t *testing.T) {
provider, generator := setupWithTestData(t, dataDir, prestate)
generator.finalState = &mipsevm.State{
Memory: &mipsevm.Memory{},
Step: 10,
Exited: true,
}
generator.proof = &proofData{
ClaimValue: common.Hash{0xaa}.Bytes(),
StateData: []byte{0xbb},
ProofData: []byte{0xcc},
OracleKey: common.Hash{0xdd}.Bytes(),
OracleValue: []byte{0xdd},
OracleOffset: 10,
}
preimage, proof, err := provider.GetPreimage(context.Background(), 7000)
require.NoError(t, err)
require.Contains(t, generator.generated, 7000, "should have tried to generate the proof")
require.Contains(t, generator.generated, 9, "should have regenerated proof from last step")
require.EqualValues(t, generator.proof.StateData, preimage)
require.EqualValues(t, generator.proof.ProofData, proof)
})
t.Run("MissingStateData", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
provider, generator := setupWithTestData(t, dataDir, prestate)
_, _, err := provider.GetPreimage(context.Background(), 1)
require.ErrorContains(t, err, "missing state data")
require.Empty(t, generator.generated)
})
t.Run("IgnoreUnknownFields", func(t *testing.T) {
provider, generator := setupWithTestData(dataDir, prestate)
provider, generator := setupWithTestData(t, dataDir, prestate)
value, proof, err := provider.GetPreimage(context.Background(), 2)
require.NoError(t, err)
expected := common.Hex2Bytes("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
......@@ -130,21 +174,21 @@ func TestAbsolutePreState(t *testing.T) {
prestate := "state.json"
t.Run("StateUnavailable", func(t *testing.T) {
provider, _ := setupWithTestData("/dir/does/not/exist", prestate)
provider, _ := setupWithTestData(t, "/dir/does/not/exist", prestate)
_, err := provider.AbsolutePreState(context.Background())
require.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("InvalidStateFile", func(t *testing.T) {
setupPreState(t, dataDir, "invalid.json")
provider, _ := setupWithTestData(dataDir, prestate)
provider, _ := setupWithTestData(t, dataDir, prestate)
_, err := provider.AbsolutePreState(context.Background())
require.ErrorContains(t, err, "invalid mipsevm state")
})
t.Run("ExpectedAbsolutePreState", func(t *testing.T) {
setupPreState(t, dataDir, "state.json")
provider, _ := setupWithTestData(dataDir, prestate)
provider, _ := setupWithTestData(t, dataDir, prestate)
preState, err := provider.AbsolutePreState(context.Background())
require.NoError(t, err)
state := mipsevm.State{
......@@ -190,9 +234,10 @@ func setupTestData(t *testing.T) (string, string) {
return dataDir, "state.json"
}
func setupWithTestData(dataDir string, prestate string) (*CannonTraceProvider, *stubGenerator) {
func setupWithTestData(t *testing.T, dataDir string, prestate string) (*CannonTraceProvider, *stubGenerator) {
generator := &stubGenerator{}
return &CannonTraceProvider{
logger: testlog.Logger(t, log.LvlInfo),
dir: dataDir,
generator: generator,
prestate: prestate,
......@@ -200,10 +245,28 @@ func setupWithTestData(dataDir string, prestate string) (*CannonTraceProvider, *
}
type stubGenerator struct {
generated []int // Using int makes assertions easier
generated []int // Using int makes assertions easier
finalState *mipsevm.State
proof *proofData
}
func (e *stubGenerator) GenerateProof(ctx context.Context, dir string, i uint64) error {
e.generated = append(e.generated, int(i))
if e.finalState != nil && e.finalState.Step <= i {
// Requesting a trace index past the end of the trace
data, err := json.Marshal(e.finalState)
if err != nil {
return err
}
return os.WriteFile(filepath.Join(dir, finalState), data, 0644)
}
if e.proof != nil {
proofFile := filepath.Join(dir, proofsDir, fmt.Sprintf("%d.json", i))
data, err := json.Marshal(e.proof)
if err != nil {
return err
}
return os.WriteFile(proofFile, data, 0644)
}
return nil
}
......@@ -33,7 +33,7 @@ func (g *FaultGameHelper) GameDuration(ctx context.Context) time.Duration {
}
func (g *FaultGameHelper) WaitForClaimCount(ctx context.Context, count int64) {
ctx, cancel := context.WithTimeout(ctx, time.Minute)
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
err := utils.WaitFor(ctx, time.Second, func() (bool, error) {
actual, err := g.game.ClaimDataLen(&bind.CallOpts{Context: ctx})
......
......@@ -145,7 +145,6 @@ func TestChallengerCompleteDisputeGame(t *testing.T) {
}
func TestCannonDisputeGame(t *testing.T) {
t.Skip("CLI-4290: op-challenger doesn't handle trace extension correctly for cannon")
InitParallel(t)
ctx := context.Background()
......
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