Commit 34101091 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

challenger: Introduce StateConverter to abstract loading VM states (#11715)

parent 31f408bf
......@@ -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)
},
}
......
......@@ -27,6 +27,7 @@ type AsteriscTraceProvider struct {
generator utils.ProofGenerator
gameDepth types.Depth
preimageLoader *utils.PreimageLoader
stateConverter vm.StateConverter
types.PrestateProvider
......@@ -46,6 +47,7 @@ func NewTraceProvider(logger log.Logger, m vm.Metricer, cfg vm.Config, vmCfg vm.
return kvstore.NewFileKV(vm.PreimageDir(dir))
}),
PrestateProvider: prestateProvider,
stateConverter: NewStateConverter(),
}
}
......@@ -120,31 +122,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)
}
}
}
......@@ -160,14 +154,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 {
......@@ -194,14 +180,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
}
......@@ -226,6 +226,7 @@ func setupWithTestData(t *testing.T, dataDir string, prestate string) (*Asterisc
generator: generator,
prestate: filepath.Join(dataDir, prestate),
gameDepth: 63,
stateConverter: &StateConverter{},
}, generator
}
......
......@@ -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
}
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
}
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)
}
......@@ -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
......@@ -49,6 +48,7 @@ func NewTraceProvider(logger log.Logger, m vm.Metricer, cfg vm.Config, vmCfg vm.
return kvstore.NewFileKV(vm.PreimageDir(dir))
}),
PrestateProvider: prestateProvider,
stateConverter: &StateConverter{},
}
}
......@@ -122,33 +122,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)
}
}
}
......@@ -164,14 +153,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 {
......@@ -198,14 +179,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
}
......@@ -244,6 +244,7 @@ func setupWithTestData(t *testing.T, dataDir string, prestate string) (*CannonTr
generator: generator,
prestate: filepath.Join(dataDir, prestate),
gameDepth: 63,
stateConverter: &StateConverter{},
}, generator
}
......
......@@ -6,9 +6,35 @@ import (
"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 {
......
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)
}
package asterisc
package vm
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
)
var _ types.PrestateProvider = (*AsteriscPreStateProvider)(nil)
var _ types.PrestateProvider = (*PrestateProvider)(nil)
type AsteriscPreStateProvider struct {
type PrestateProvider struct {
prestate string
stateConverter StateConverter
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)
func NewPrestateProvider(prestate string, converter StateConverter) *PrestateProvider {
return &PrestateProvider{
prestate: prestate,
stateConverter: converter,
}
return state, nil
}
func (p *AsteriscPreStateProvider) AbsolutePreStateCommitment(_ context.Context) (common.Hash, error) {
func (p *PrestateProvider) AbsolutePreStateCommitment(_ context.Context) (common.Hash, error) {
if p.prestateCommitment != (common.Hash{}) {
return p.prestateCommitment, nil
}
state, err := p.absolutePreState()
proof, _, _, err := p.stateConverter.ConvertStateToProof(p.prestate)
if err != nil {
return common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err)
}
p.prestateCommitment = state.StateHash
return state.StateHash, nil
p.prestateCommitment = proof.ClaimValue
return proof.ClaimValue, nil
}
func (p *AsteriscPreStateProvider) PrestatePath() string {
func (p *PrestateProvider) PrestatePath() string {
return p.prestate
}
package asterisc
package vm
import (
"context"
"os"
"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"
)
func newAsteriscPrestateProvider(dataDir string, prestate string) *AsteriscPreStateProvider {
return &AsteriscPreStateProvider{
prestate: filepath.Join(dataDir, prestate),
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 TestAbsolutePreStateCommitment(t *testing.T) {
dataDir := t.TempDir()
func newPrestateProvider(prestate common.Hash) *PrestateProvider {
return NewPrestateProvider("state.json", &stubConverter{hash: prestate})
}
prestate := "state.json"
func TestAbsolutePreStateCommitment(t *testing.T) {
prestate := common.Hash{0xaa, 0xbb}
t.Run("StateUnavailable", func(t *testing.T) {
provider := newAsteriscPrestateProvider("/dir/does/not/exist", prestate)
expectedErr := errors.New("kaboom")
provider := NewPrestateProvider("foo", &stubConverter{err: expectedErr})
_, err := provider.AbsolutePreStateCommitment(context.Background())
require.ErrorIs(t, err, os.ErrNotExist)
require.ErrorIs(t, err, expectedErr)
})
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("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) {
setupPreState(t, dataDir, prestate)
provider := newAsteriscPrestateProvider(dataDir, prestate)
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
require.NoError(t, os.Remove(provider.prestate))
converter.err = errors.New("no soup for you")
// Value should still be available from cache
cached, err := provider.AbsolutePreStateCommitment(context.Background())
......@@ -48,12 +61,3 @@ func TestAbsolutePreStateCommitment(t *testing.T) {
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)
}
......@@ -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")
......
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