From 5536ab87a339df5b97e115e733304dc1fe0234bd Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Mon, 10 Jun 2024 11:38:05 -0400 Subject: [PATCH] challenger: Introduce StateConverter to abstract loading VM states --- op-challenger/game/fault/register_task.go | 8 +- .../game/fault/trace/asterisc/prestate.go | 45 ---------- .../fault/trace/asterisc/prestate_test.go | 59 ------------ .../game/fault/trace/asterisc/provider.go | 34 +++---- .../fault/trace/asterisc/provider_test.go | 11 +-- .../asterisc/{state.go => state_converter.go} | 25 ++++++ .../game/fault/trace/cannon/prestate.go | 47 ---------- .../game/fault/trace/cannon/prestate_test.go | 89 ------------------- .../game/fault/trace/cannon/provider.go | 43 +++------ .../game/fault/trace/cannon/provider_test.go | 11 +-- .../game/fault/trace/cannon/state.go | 27 ------ .../fault/trace/cannon/state_converter.go | 53 +++++++++++ op-challenger/game/fault/trace/vm/iface.go | 9 ++ op-challenger/game/fault/trace/vm/prestate.go | 42 +++++++++ .../game/fault/trace/vm/prestate_test.go | 63 +++++++++++++ op-challenger/runner/factory.go | 6 +- 16 files changed, 233 insertions(+), 339 deletions(-) delete mode 100644 op-challenger/game/fault/trace/asterisc/prestate.go delete mode 100644 op-challenger/game/fault/trace/asterisc/prestate_test.go rename op-challenger/game/fault/trace/asterisc/{state.go => state_converter.go} (73%) delete mode 100644 op-challenger/game/fault/trace/cannon/prestate.go delete mode 100644 op-challenger/game/fault/trace/cannon/prestate_test.go delete mode 100644 op-challenger/game/fault/trace/cannon/state.go create mode 100644 op-challenger/game/fault/trace/cannon/state_converter.go create mode 100644 op-challenger/game/fault/trace/vm/iface.go create mode 100644 op-challenger/game/fault/trace/vm/prestate.go create mode 100644 op-challenger/game/fault/trace/vm/prestate_test.go diff --git a/op-challenger/game/fault/register_task.go b/op-challenger/game/fault/register_task.go index b9b2db6b4951..40acfecce22b 100644 --- a/op-challenger/game/fault/register_task.go +++ b/op-challenger/game/fault/register_task.go @@ -57,7 +57,7 @@ func NewCannonRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m c cfg.CannonAbsolutePreState, filepath.Join(cfg.Datadir, "cannon-prestates"), func(path string) faultTypes.PrestateProvider { - return cannon.NewPrestateProvider(path) + return vm.NewPrestateProvider(path, cannon.NewStateConverter()) }), newTraceAccessor: func( logger log.Logger, @@ -71,7 +71,7 @@ func NewCannonRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m c splitDepth faultTypes.Depth, prestateBlock uint64, poststateBlock uint64) (*trace.Accessor, error) { - provider := vmPrestateProvider.(*cannon.CannonPrestateProvider) + provider := vmPrestateProvider.(*vm.PrestateProvider) return outputs.NewOutputCannonTraceAccessor(logger, m, cfg.Cannon, serverExecutor, l2Client, prestateProvider, provider.PrestatePath(), rollupClient, dir, l1Head, splitDepth, prestateBlock, poststateBlock) }, } @@ -87,7 +87,7 @@ func NewAsteriscRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m cfg.AsteriscAbsolutePreState, filepath.Join(cfg.Datadir, "asterisc-prestates"), func(path string) faultTypes.PrestateProvider { - return asterisc.NewPrestateProvider(path) + return vm.NewPrestateProvider(path, asterisc.NewStateConverter()) }), newTraceAccessor: func( logger log.Logger, @@ -101,7 +101,7 @@ func NewAsteriscRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m splitDepth faultTypes.Depth, prestateBlock uint64, poststateBlock uint64) (*trace.Accessor, error) { - provider := vmPrestateProvider.(*asterisc.AsteriscPreStateProvider) + provider := vmPrestateProvider.(*vm.PrestateProvider) return outputs.NewOutputAsteriscTraceAccessor(logger, m, cfg.Asterisc, serverExecutor, l2Client, prestateProvider, provider.PrestatePath(), rollupClient, dir, l1Head, splitDepth, prestateBlock, poststateBlock) }, } diff --git a/op-challenger/game/fault/trace/asterisc/prestate.go b/op-challenger/game/fault/trace/asterisc/prestate.go deleted file mode 100644 index f65e4d9c901c..000000000000 --- a/op-challenger/game/fault/trace/asterisc/prestate.go +++ /dev/null @@ -1,45 +0,0 @@ -package asterisc - -import ( - "context" - "fmt" - - "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" - "github.com/ethereum/go-ethereum/common" -) - -var _ types.PrestateProvider = (*AsteriscPreStateProvider)(nil) - -type AsteriscPreStateProvider struct { - prestate string - - prestateCommitment common.Hash -} - -func NewPrestateProvider(prestate string) *AsteriscPreStateProvider { - return &AsteriscPreStateProvider{prestate: prestate} -} - -func (p *AsteriscPreStateProvider) absolutePreState() (*VMState, error) { - state, err := parseState(p.prestate) - if err != nil { - return nil, fmt.Errorf("cannot load absolute pre-state: %w", err) - } - return state, nil -} - -func (p *AsteriscPreStateProvider) AbsolutePreStateCommitment(_ context.Context) (common.Hash, error) { - if p.prestateCommitment != (common.Hash{}) { - return p.prestateCommitment, nil - } - state, err := p.absolutePreState() - if err != nil { - return common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err) - } - p.prestateCommitment = state.StateHash - return state.StateHash, nil -} - -func (p *AsteriscPreStateProvider) PrestatePath() string { - return p.prestate -} diff --git a/op-challenger/game/fault/trace/asterisc/prestate_test.go b/op-challenger/game/fault/trace/asterisc/prestate_test.go deleted file mode 100644 index 38bc6d360e7b..000000000000 --- a/op-challenger/game/fault/trace/asterisc/prestate_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package asterisc - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -func newAsteriscPrestateProvider(dataDir string, prestate string) *AsteriscPreStateProvider { - return &AsteriscPreStateProvider{ - prestate: filepath.Join(dataDir, prestate), - } -} - -func TestAbsolutePreStateCommitment(t *testing.T) { - dataDir := t.TempDir() - - prestate := "state.json" - - t.Run("StateUnavailable", func(t *testing.T) { - provider := newAsteriscPrestateProvider("/dir/does/not/exist", prestate) - _, err := provider.AbsolutePreStateCommitment(context.Background()) - require.ErrorIs(t, err, os.ErrNotExist) - }) - - t.Run("InvalidStateFile", func(t *testing.T) { - setupPreState(t, dataDir, "invalid.json") - provider := newAsteriscPrestateProvider(dataDir, prestate) - _, err := provider.AbsolutePreStateCommitment(context.Background()) - require.ErrorContains(t, err, "invalid asterisc VM state") - }) - - t.Run("CacheAbsolutePreState", func(t *testing.T) { - setupPreState(t, dataDir, prestate) - provider := newAsteriscPrestateProvider(dataDir, prestate) - first, err := provider.AbsolutePreStateCommitment(context.Background()) - require.NoError(t, err) - - // Remove the prestate from disk - require.NoError(t, os.Remove(provider.prestate)) - - // Value should still be available from cache - cached, err := provider.AbsolutePreStateCommitment(context.Background()) - require.NoError(t, err) - require.Equal(t, first, cached) - }) -} - -func setupPreState(t *testing.T, dataDir string, filename string) { - srcDir := filepath.Join("test_data") - path := filepath.Join(srcDir, filename) - file, err := testData.ReadFile(path) - require.NoErrorf(t, err, "reading %v", path) - err = os.WriteFile(filepath.Join(dataDir, "state.json"), file, 0o644) - require.NoErrorf(t, err, "writing %v", path) -} diff --git a/op-challenger/game/fault/trace/asterisc/provider.go b/op-challenger/game/fault/trace/asterisc/provider.go index c4647234c64f..86b7de21aa85 100644 --- a/op-challenger/game/fault/trace/asterisc/provider.go +++ b/op-challenger/game/fault/trace/asterisc/provider.go @@ -27,6 +27,7 @@ type AsteriscTraceProvider struct { generator utils.ProofGenerator gameDepth types.Depth preimageLoader *utils.PreimageLoader + stateConverter vm.StateConverter types.PrestateProvider @@ -44,6 +45,7 @@ func NewTraceProvider(logger log.Logger, m vm.Metricer, cfg vm.Config, vmCfg vm. gameDepth: gameDepth, preimageLoader: utils.NewPreimageLoader(kvstore.NewDiskKV(vm.PreimageDir(dir)).Get), PrestateProvider: prestateProvider, + stateConverter: NewStateConverter(), } } @@ -118,31 +120,23 @@ func (p *AsteriscTraceProvider) loadProof(ctx context.Context, i uint64) (*utils file, err = ioutil.OpenDecompressed(path) if errors.Is(err, os.ErrNotExist) { // Expected proof wasn't generated, check if we reached the end of execution - state, err := p.finalState() + proof, step, exited, err := p.stateConverter.ConvertStateToProof(filepath.Join(p.dir, vm.FinalState)) if err != nil { return nil, err } - if state.Exited && state.Step <= i { - p.logger.Warn("Requested proof was after the program exited", "proof", i, "last", state.Step) + if exited && step <= i { + p.logger.Warn("Requested proof was after the program exited", "proof", i, "last", 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 + p.lastStep = step - 1 // Extend the trace out to the full length using a no-op instruction that doesn't change any state // No execution is done, so no proof-data or oracle values are required. - proof := &utils.ProofData{ - ClaimValue: state.StateHash, - StateData: state.Witness, - ProofData: []byte{}, - OracleKey: nil, - OracleValue: nil, - OracleOffset: 0, - } if err := utils.WriteLastStep(p.dir, proof, p.lastStep); err != nil { p.logger.Warn("Failed to write last step to disk cache", "step", p.lastStep) } return proof, nil } else { - return nil, fmt.Errorf("expected proof not generated but final state was not exited, requested step %v, final state at step %v", i, state.Step) + return nil, fmt.Errorf("expected proof not generated but final state was not exited, requested step %v, final state at step %v", i, step) } } } @@ -158,14 +152,6 @@ func (p *AsteriscTraceProvider) loadProof(ctx context.Context, i uint64) (*utils return &proof, nil } -func (c *AsteriscTraceProvider) finalState() (*VMState, error) { - state, err := parseState(filepath.Join(c.dir, vm.FinalState)) - if err != nil { - return nil, fmt.Errorf("cannot read final state: %w", err) - } - return state, nil -} - // AsteriscTraceProviderForTest is a AsteriscTraceProvider that can find the step referencing the preimage read // Only to be used for testing type AsteriscTraceProviderForTest struct { @@ -190,14 +176,14 @@ func (p *AsteriscTraceProviderForTest) FindStep(ctx context.Context, start uint6 return 0, fmt.Errorf("generate asterisc trace (until preimage read): %w", err) } // Load the step from the state asterisc finished with - state, err := p.finalState() + _, step, exited, err := p.stateConverter.ConvertStateToProof(filepath.Join(p.dir, vm.FinalState)) if err != nil { return 0, fmt.Errorf("failed to load final state: %w", err) } // Check we didn't get to the end of the trace without finding the preimage read we were looking for - if state.Exited { + if exited { return 0, fmt.Errorf("preimage read not found: %w", io.EOF) } // The state is the post-state so the step we want to execute to read the preimage is step - 1. - return state.Step - 1, nil + return step - 1, nil } diff --git a/op-challenger/game/fault/trace/asterisc/provider_test.go b/op-challenger/game/fault/trace/asterisc/provider_test.go index 939a27decc30..d467bbb668dc 100644 --- a/op-challenger/game/fault/trace/asterisc/provider_test.go +++ b/op-challenger/game/fault/trace/asterisc/provider_test.go @@ -221,11 +221,12 @@ func setupTestData(t *testing.T) (string, string) { func setupWithTestData(t *testing.T, dataDir string, prestate string) (*AsteriscTraceProvider, *stubGenerator) { generator := &stubGenerator{} return &AsteriscTraceProvider{ - logger: testlog.Logger(t, log.LevelInfo), - dir: dataDir, - generator: generator, - prestate: filepath.Join(dataDir, prestate), - gameDepth: 63, + logger: testlog.Logger(t, log.LevelInfo), + dir: dataDir, + generator: generator, + prestate: filepath.Join(dataDir, prestate), + gameDepth: 63, + stateConverter: &StateConverter{}, }, generator } diff --git a/op-challenger/game/fault/trace/asterisc/state.go b/op-challenger/game/fault/trace/asterisc/state_converter.go similarity index 73% rename from op-challenger/game/fault/trace/asterisc/state.go rename to op-challenger/game/fault/trace/asterisc/state_converter.go index e30ebf8702d8..29c9f8b2ea50 100644 --- a/op-challenger/game/fault/trace/asterisc/state.go +++ b/op-challenger/game/fault/trace/asterisc/state_converter.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum-optimism/optimism/cannon/mipsevm" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" "github.com/ethereum-optimism/optimism/op-service/ioutil" ) @@ -74,3 +75,27 @@ func parseStateFromReader(in io.ReadCloser) (*VMState, error) { } return &state, nil } + +type StateConverter struct { +} + +func NewStateConverter() *StateConverter { + return &StateConverter{} +} + +func (c *StateConverter) ConvertStateToProof(statePath string) (*utils.ProofData, uint64, bool, error) { + state, err := parseState(statePath) + if err != nil { + return nil, 0, false, fmt.Errorf("cannot read final state: %w", err) + } + // Extend the trace out to the full length using a no-op instruction that doesn't change any state + // No execution is done, so no proof-data or oracle values are required. + return &utils.ProofData{ + ClaimValue: state.StateHash, + StateData: state.Witness, + ProofData: []byte{}, + OracleKey: nil, + OracleValue: nil, + OracleOffset: 0, + }, state.Step, state.Exited, nil +} diff --git a/op-challenger/game/fault/trace/cannon/prestate.go b/op-challenger/game/fault/trace/cannon/prestate.go deleted file mode 100644 index 6fcb8f50f662..000000000000 --- a/op-challenger/game/fault/trace/cannon/prestate.go +++ /dev/null @@ -1,47 +0,0 @@ -package cannon - -import ( - "context" - "fmt" - - "github.com/ethereum/go-ethereum/common" - - "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" -) - -var _ types.PrestateProvider = (*CannonPrestateProvider)(nil) - -type CannonPrestateProvider struct { - prestate string - - prestateCommitment common.Hash -} - -func NewPrestateProvider(prestate string) *CannonPrestateProvider { - return &CannonPrestateProvider{prestate: prestate} -} - -func (p *CannonPrestateProvider) absolutePreState() ([]byte, common.Hash, error) { - state, err := parseState(p.prestate) - if err != nil { - return nil, common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err) - } - witness, hash := state.EncodeWitness() - return witness, hash, nil -} - -func (p *CannonPrestateProvider) AbsolutePreStateCommitment(_ context.Context) (common.Hash, error) { - if p.prestateCommitment != (common.Hash{}) { - return p.prestateCommitment, nil - } - _, hash, err := p.absolutePreState() - if err != nil { - return common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err) - } - p.prestateCommitment = hash - return hash, nil -} - -func (p *CannonPrestateProvider) PrestatePath() string { - return p.prestate -} diff --git a/op-challenger/game/fault/trace/cannon/prestate_test.go b/op-challenger/game/fault/trace/cannon/prestate_test.go deleted file mode 100644 index 9566e9d0664c..000000000000 --- a/op-challenger/game/fault/trace/cannon/prestate_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package cannon - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/require" - - "github.com/ethereum-optimism/optimism/cannon/mipsevm" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/memory" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded" -) - -func newCannonPrestateProvider(dataDir string, prestate string) *CannonPrestateProvider { - return &CannonPrestateProvider{ - prestate: filepath.Join(dataDir, prestate), - } -} - -func TestAbsolutePreStateCommitment(t *testing.T) { - dataDir := t.TempDir() - - prestate := "state.json" - - t.Run("StateUnavailable", func(t *testing.T) { - provider := newCannonPrestateProvider("/dir/does/not/exist", prestate) - _, err := provider.AbsolutePreStateCommitment(context.Background()) - require.ErrorIs(t, err, os.ErrNotExist) - }) - - t.Run("InvalidStateFile", func(t *testing.T) { - setupPreState(t, dataDir, "invalid.json") - provider := newCannonPrestateProvider(dataDir, prestate) - _, err := provider.AbsolutePreStateCommitment(context.Background()) - require.ErrorContains(t, err, "invalid mipsevm state") - }) - - t.Run("ExpectedAbsolutePreState", func(t *testing.T) { - setupPreState(t, dataDir, "state.json") - provider := newCannonPrestateProvider(dataDir, prestate) - actual, err := provider.AbsolutePreStateCommitment(context.Background()) - require.NoError(t, err) - state := singlethreaded.State{ - Memory: memory.NewMemory(), - PreimageKey: common.HexToHash("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), - PreimageOffset: 0, - Cpu: mipsevm.CpuScalars{ - PC: 0, - NextPC: 1, - LO: 0, - HI: 0, - }, - Heap: 0, - ExitCode: 0, - Exited: false, - Step: 0, - Registers: [32]uint32{}, - } - _, expected := state.EncodeWitness() - require.Equal(t, expected, actual) - }) - - t.Run("CacheAbsolutePreState", func(t *testing.T) { - setupPreState(t, dataDir, prestate) - provider := newCannonPrestateProvider(dataDir, prestate) - first, err := provider.AbsolutePreStateCommitment(context.Background()) - require.NoError(t, err) - - // Remove the prestate from disk - require.NoError(t, os.Remove(provider.prestate)) - - // Value should still be available from cache - cached, err := provider.AbsolutePreStateCommitment(context.Background()) - require.NoError(t, err) - require.Equal(t, first, cached) - }) -} - -func setupPreState(t *testing.T, dataDir string, filename string) { - srcDir := filepath.Join("test_data") - path := filepath.Join(srcDir, filename) - file, err := testData.ReadFile(path) - require.NoErrorf(t, err, "reading %v", path) - err = os.WriteFile(filepath.Join(dataDir, "state.json"), file, 0o644) - require.NoErrorf(t, err, "writing %v", path) -} diff --git a/op-challenger/game/fault/trace/cannon/provider.go b/op-challenger/game/fault/trace/cannon/provider.go index 7262ec918040..c97d5ea9a9ad 100644 --- a/op-challenger/game/fault/trace/cannon/provider.go +++ b/op-challenger/game/fault/trace/cannon/provider.go @@ -11,10 +11,8 @@ import ( "path/filepath" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded" "github.com/ethereum-optimism/optimism/op-challenger/config" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/vm" @@ -30,6 +28,7 @@ type CannonTraceProvider struct { generator utils.ProofGenerator gameDepth types.Depth preimageLoader *utils.PreimageLoader + stateConverter vm.StateConverter types.PrestateProvider @@ -47,6 +46,7 @@ func NewTraceProvider(logger log.Logger, m vm.Metricer, cfg vm.Config, vmCfg vm. gameDepth: gameDepth, preimageLoader: utils.NewPreimageLoader(kvstore.NewDiskKV(vm.PreimageDir(dir)).Get), PrestateProvider: prestateProvider, + stateConverter: &StateConverter{}, } } @@ -120,33 +120,22 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*utils.P // Try opening the file again now and it should exist. file, err = ioutil.OpenDecompressed(path) if errors.Is(err, os.ErrNotExist) { - // Expected proof wasn't generated, check if we reached the end of execution - state, err := p.finalState() + proof, stateStep, exited, err := p.stateConverter.ConvertStateToProof(filepath.Join(p.dir, vm.FinalState)) if err != nil { - return nil, err + return nil, fmt.Errorf("cannot create proof from 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) + + if exited && stateStep <= i { + p.logger.Warn("Requested proof was after the program exited", "proof", i, "last", stateStep) // 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 - // Extend the trace out to the full length using a no-op instruction that doesn't change any state - // No execution is done, so no proof-data or oracle values are required. - witness, witnessHash := state.EncodeWitness() - proof := &utils.ProofData{ - ClaimValue: witnessHash, - StateData: hexutil.Bytes(witness), - ProofData: []byte{}, - OracleKey: nil, - OracleValue: nil, - OracleOffset: 0, - } + p.lastStep = stateStep - 1 if err := utils.WriteLastStep(p.dir, proof, p.lastStep); err != nil { p.logger.Warn("Failed to write last step to disk cache", "step", p.lastStep) } return proof, nil } else { - return nil, fmt.Errorf("expected proof not generated but final state was not exited, requested step %v, final state at step %v", i, state.Step) + return nil, fmt.Errorf("expected proof not generated but final state was not exited, requested step %v, final state at step %v", i, stateStep) } } } @@ -162,14 +151,6 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*utils.P return &proof, nil } -func (c *CannonTraceProvider) finalState() (*singlethreaded.State, error) { - state, err := parseState(filepath.Join(c.dir, vm.FinalState)) - if err != nil { - return nil, fmt.Errorf("cannot read final state: %w", err) - } - return state, nil -} - // CannonTraceProviderForTest is a CannonTraceProvider that can find the step referencing the preimage read // Only to be used for testing type CannonTraceProviderForTest struct { @@ -194,14 +175,14 @@ func (p *CannonTraceProviderForTest) FindStep(ctx context.Context, start uint64, return 0, fmt.Errorf("generate cannon trace (until preimage read): %w", err) } // Load the step from the state cannon finished with - state, err := p.finalState() + _, step, exited, err := p.stateConverter.ConvertStateToProof(filepath.Join(p.dir, vm.FinalState)) if err != nil { return 0, fmt.Errorf("failed to load final state: %w", err) } // Check we didn't get to the end of the trace without finding the preimage read we were looking for - if state.Exited { + if exited { return 0, fmt.Errorf("preimage read not found: %w", io.EOF) } // The state is the post-state so the step we want to execute to read the preimage is step - 1. - return state.Step - 1, nil + return step - 1, nil } diff --git a/op-challenger/game/fault/trace/cannon/provider_test.go b/op-challenger/game/fault/trace/cannon/provider_test.go index 3343b581f1c1..c650bd6ef818 100644 --- a/op-challenger/game/fault/trace/cannon/provider_test.go +++ b/op-challenger/game/fault/trace/cannon/provider_test.go @@ -239,11 +239,12 @@ func setupTestData(t *testing.T) (string, string) { func setupWithTestData(t *testing.T, dataDir string, prestate string) (*CannonTraceProvider, *stubGenerator) { generator := &stubGenerator{} return &CannonTraceProvider{ - logger: testlog.Logger(t, log.LevelInfo), - dir: dataDir, - generator: generator, - prestate: filepath.Join(dataDir, prestate), - gameDepth: 63, + logger: testlog.Logger(t, log.LevelInfo), + dir: dataDir, + generator: generator, + prestate: filepath.Join(dataDir, prestate), + gameDepth: 63, + stateConverter: &StateConverter{}, }, generator } diff --git a/op-challenger/game/fault/trace/cannon/state.go b/op-challenger/game/fault/trace/cannon/state.go deleted file mode 100644 index cf781c8bd428..000000000000 --- a/op-challenger/game/fault/trace/cannon/state.go +++ /dev/null @@ -1,27 +0,0 @@ -package cannon - -import ( - "encoding/json" - "fmt" - "io" - - "github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded" - "github.com/ethereum-optimism/optimism/op-service/ioutil" -) - -func parseState(path string) (*singlethreaded.State, error) { - file, err := ioutil.OpenDecompressed(path) - if err != nil { - return nil, fmt.Errorf("cannot open state file (%v): %w", path, err) - } - return parseStateFromReader(file) -} - -func parseStateFromReader(in io.ReadCloser) (*singlethreaded.State, error) { - defer in.Close() - var state singlethreaded.State - if err := json.NewDecoder(in).Decode(&state); err != nil { - return nil, fmt.Errorf("invalid mipsevm state: %w", err) - } - return &state, nil -} diff --git a/op-challenger/game/fault/trace/cannon/state_converter.go b/op-challenger/game/fault/trace/cannon/state_converter.go new file mode 100644 index 000000000000..e11b0ee53caa --- /dev/null +++ b/op-challenger/game/fault/trace/cannon/state_converter.go @@ -0,0 +1,53 @@ +package cannon + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + "github.com/ethereum-optimism/optimism/op-service/ioutil" +) + +type StateConverter struct { +} + +func NewStateConverter() *StateConverter { + return &StateConverter{} +} + +func (c *StateConverter) ConvertStateToProof(statePath string) (*utils.ProofData, uint64, bool, error) { + state, err := parseState(statePath) + if err != nil { + return nil, 0, false, fmt.Errorf("cannot read final state: %w", err) + } + // Extend the trace out to the full length using a no-op instruction that doesn't change any state + // No execution is done, so no proof-data or oracle values are required. + witness, witnessHash := state.EncodeWitness() + return &utils.ProofData{ + ClaimValue: witnessHash, + StateData: witness, + ProofData: []byte{}, + OracleKey: nil, + OracleValue: nil, + OracleOffset: 0, + }, state.Step, state.Exited, nil +} + +func parseState(path string) (*singlethreaded.State, error) { + file, err := ioutil.OpenDecompressed(path) + if err != nil { + return nil, fmt.Errorf("cannot open state file (%v): %w", path, err) + } + return parseStateFromReader(file) +} + +func parseStateFromReader(in io.ReadCloser) (*singlethreaded.State, error) { + defer in.Close() + var state singlethreaded.State + if err := json.NewDecoder(in).Decode(&state); err != nil { + return nil, fmt.Errorf("invalid mipsevm state: %w", err) + } + return &state, nil +} diff --git a/op-challenger/game/fault/trace/vm/iface.go b/op-challenger/game/fault/trace/vm/iface.go new file mode 100644 index 000000000000..188f19e0c8e2 --- /dev/null +++ b/op-challenger/game/fault/trace/vm/iface.go @@ -0,0 +1,9 @@ +package vm + +import "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + +type StateConverter interface { + // ConvertStateToProof reads the state snapshot at the specified path and converts it to ProofData. + // Returns the proof data, the VM step the state is from and whether or not the VM had exited. + ConvertStateToProof(statePath string) (*utils.ProofData, uint64, bool, error) +} diff --git a/op-challenger/game/fault/trace/vm/prestate.go b/op-challenger/game/fault/trace/vm/prestate.go new file mode 100644 index 000000000000..bbb4a9437d9a --- /dev/null +++ b/op-challenger/game/fault/trace/vm/prestate.go @@ -0,0 +1,42 @@ +package vm + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" +) + +var _ types.PrestateProvider = (*PrestateProvider)(nil) + +type PrestateProvider struct { + prestate string + stateConverter StateConverter + + prestateCommitment common.Hash +} + +func NewPrestateProvider(prestate string, converter StateConverter) *PrestateProvider { + return &PrestateProvider{ + prestate: prestate, + stateConverter: converter, + } +} + +func (p *PrestateProvider) AbsolutePreStateCommitment(_ context.Context) (common.Hash, error) { + if p.prestateCommitment != (common.Hash{}) { + return p.prestateCommitment, nil + } + proof, _, _, err := p.stateConverter.ConvertStateToProof(p.prestate) + if err != nil { + return common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err) + } + p.prestateCommitment = proof.ClaimValue + return proof.ClaimValue, nil +} + +func (p *PrestateProvider) PrestatePath() string { + return p.prestate +} diff --git a/op-challenger/game/fault/trace/vm/prestate_test.go b/op-challenger/game/fault/trace/vm/prestate_test.go new file mode 100644 index 000000000000..69498e323c59 --- /dev/null +++ b/op-challenger/game/fault/trace/vm/prestate_test.go @@ -0,0 +1,63 @@ +package vm + +import ( + "context" + "errors" + "path/filepath" + "testing" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +type stubConverter struct { + err error + hash common.Hash +} + +func (s *stubConverter) ConvertStateToProof(statePath string) (*utils.ProofData, uint64, bool, error) { + if s.err != nil { + return nil, 0, false, s.err + } + return &utils.ProofData{ + ClaimValue: s.hash, + }, 0, false, nil +} + +func newPrestateProvider(prestate common.Hash) *PrestateProvider { + return NewPrestateProvider("state.json", &stubConverter{hash: prestate}) +} + +func TestAbsolutePreStateCommitment(t *testing.T) { + prestate := common.Hash{0xaa, 0xbb} + + t.Run("StateUnavailable", func(t *testing.T) { + expectedErr := errors.New("kaboom") + provider := NewPrestateProvider("foo", &stubConverter{err: expectedErr}) + _, err := provider.AbsolutePreStateCommitment(context.Background()) + require.ErrorIs(t, err, expectedErr) + }) + + t.Run("ExpectedAbsolutePreState", func(t *testing.T) { + provider := newPrestateProvider(prestate) + actual, err := provider.AbsolutePreStateCommitment(context.Background()) + require.NoError(t, err) + require.Equal(t, prestate, actual) + }) + + t.Run("CacheAbsolutePreState", func(t *testing.T) { + converter := &stubConverter{hash: prestate} + provider := NewPrestateProvider(filepath.Join("state.json"), converter) + first, err := provider.AbsolutePreStateCommitment(context.Background()) + require.NoError(t, err) + + // Remove the prestate from disk + converter.err = errors.New("no soup for you") + + // Value should still be available from cache + cached, err := provider.AbsolutePreStateCommitment(context.Background()) + require.NoError(t, err) + require.Equal(t, first, cached) + }) +} diff --git a/op-challenger/runner/factory.go b/op-challenger/runner/factory.go index dae0a3fce146..59188210d8e5 100644 --- a/op-challenger/runner/factory.go +++ b/op-challenger/runner/factory.go @@ -33,7 +33,7 @@ func createTraceProvider( if err != nil { return nil, err } - prestateProvider := cannon.NewPrestateProvider(prestate) + prestateProvider := vm.NewPrestateProvider(prestate, cannon.NewStateConverter()) return cannon.NewTraceProvider(logger, m, cfg.Cannon, vmConfig, prestateProvider, prestate, localInputs, dir, 42), nil case types.TraceTypeAsterisc: vmConfig := vm.NewOpProgramServerExecutor() @@ -41,7 +41,7 @@ func createTraceProvider( if err != nil { return nil, err } - prestateProvider := asterisc.NewPrestateProvider(prestate) + prestateProvider := vm.NewPrestateProvider(prestate, asterisc.NewStateConverter()) return asterisc.NewTraceProvider(logger, m, cfg.Asterisc, vmConfig, prestateProvider, prestate, localInputs, dir, 42), nil case types.TraceTypeAsteriscKona: vmConfig := vm.NewKonaServerExecutor() @@ -49,7 +49,7 @@ func createTraceProvider( if err != nil { return nil, err } - prestateProvider := asterisc.NewPrestateProvider(prestate) + prestateProvider := vm.NewPrestateProvider(prestate, asterisc.NewStateConverter()) return asterisc.NewTraceProvider(logger, m, cfg.Asterisc, vmConfig, prestateProvider, prestate, localInputs, dir, 42), nil } return nil, errors.New("invalid trace type")