Commit b02dba0b authored by Minhyuk Kim's avatar Minhyuk Kim Committed by GitHub

Add binary deserialization to asterisc's state converter trace function (#12238)

* Move serialize from cannon to op-service

* Add binary deserialization to asterisc's state converter trace function

* Use Asterisc's witness cmd to generate state data

* Fix op-challenger tests

* remove unnecessary logic from state_converter, and only test witness subcommand
parent 711bc7cf
...@@ -85,7 +85,7 @@ func NewCannonRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m c ...@@ -85,7 +85,7 @@ func NewCannonRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m c
} }
func NewAsteriscRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m caching.Metrics, serverExecutor vm.OracleServerExecutor) *RegisterTask { func NewAsteriscRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m caching.Metrics, serverExecutor vm.OracleServerExecutor) *RegisterTask {
stateConverter := asterisc.NewStateConverter() stateConverter := asterisc.NewStateConverter(cfg.Asterisc)
return &RegisterTask{ return &RegisterTask{
gameType: gameType, gameType: gameType,
getPrestateProvider: cachePrestates( getPrestateProvider: cachePrestates(
...@@ -117,7 +117,7 @@ func NewAsteriscRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m ...@@ -117,7 +117,7 @@ func NewAsteriscRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m
} }
func NewAsteriscKonaRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m caching.Metrics, serverExecutor vm.OracleServerExecutor) *RegisterTask { func NewAsteriscKonaRegisterTask(gameType faultTypes.GameType, cfg *config.Config, m caching.Metrics, serverExecutor vm.OracleServerExecutor) *RegisterTask {
stateConverter := asterisc.NewStateConverter() stateConverter := asterisc.NewStateConverter(cfg.Asterisc)
return &RegisterTask{ return &RegisterTask{
gameType: gameType, gameType: gameType,
getPrestateProvider: cachePrestates( getPrestateProvider: cachePrestates(
......
...@@ -49,7 +49,7 @@ func NewTraceProvider(logger log.Logger, m vm.Metricer, cfg vm.Config, vmCfg vm. ...@@ -49,7 +49,7 @@ func NewTraceProvider(logger log.Logger, m vm.Metricer, cfg vm.Config, vmCfg vm.
return kvstore.NewDiskKV(logger, vm.PreimageDir(dir), kvtypes.DataFormatFile) return kvstore.NewDiskKV(logger, vm.PreimageDir(dir), kvtypes.DataFormatFile)
}), }),
PrestateProvider: prestateProvider, PrestateProvider: prestateProvider,
stateConverter: NewStateConverter(), stateConverter: NewStateConverter(cfg),
cfg: cfg, cfg: cfg,
} }
} }
...@@ -173,7 +173,7 @@ func NewTraceProviderForTest(logger log.Logger, m vm.Metricer, cfg *config.Confi ...@@ -173,7 +173,7 @@ func NewTraceProviderForTest(logger log.Logger, m vm.Metricer, cfg *config.Confi
preimageLoader: utils.NewPreimageLoader(func() (utils.PreimageSource, error) { preimageLoader: utils.NewPreimageLoader(func() (utils.PreimageSource, error) {
return kvstore.NewDiskKV(logger, vm.PreimageDir(dir), kvtypes.DataFormatFile) return kvstore.NewDiskKV(logger, vm.PreimageDir(dir), kvtypes.DataFormatFile)
}), }),
stateConverter: NewStateConverter(), stateConverter: NewStateConverter(cfg.Asterisc),
cfg: cfg.Asterisc, cfg: cfg.Asterisc,
} }
return &AsteriscTraceProviderForTest{p} return &AsteriscTraceProviderForTest{p}
......
...@@ -23,6 +23,7 @@ import ( ...@@ -23,6 +23,7 @@ import (
//go:embed test_data //go:embed test_data
var testData embed.FS var testData embed.FS
var asteriscWitnessLen = 362
func PositionFromTraceIndex(provider *AsteriscTraceProvider, idx *big.Int) types.Position { func PositionFromTraceIndex(provider *AsteriscTraceProvider, idx *big.Int) types.Position {
return types.NewPosition(provider.gameDepth, idx) return types.NewPosition(provider.gameDepth, idx)
...@@ -226,7 +227,7 @@ func setupWithTestData(t *testing.T, dataDir string, prestate string) (*Asterisc ...@@ -226,7 +227,7 @@ func setupWithTestData(t *testing.T, dataDir string, prestate string) (*Asterisc
generator: generator, generator: generator,
prestate: filepath.Join(dataDir, prestate), prestate: filepath.Join(dataDir, prestate),
gameDepth: 63, gameDepth: 63,
stateConverter: &StateConverter{}, stateConverter: generator,
}, generator }, generator
} }
...@@ -234,6 +235,20 @@ type stubGenerator struct { ...@@ -234,6 +235,20 @@ type stubGenerator struct {
generated []int // Using int makes assertions easier generated []int // Using int makes assertions easier
finalState *VMState finalState *VMState
proof *utils.ProofData proof *utils.ProofData
finalStatePath string
}
func (e *stubGenerator) ConvertStateToProof(ctx context.Context, statePath string) (*utils.ProofData, uint64, bool, error) {
if statePath == e.finalStatePath {
return &utils.ProofData{
ClaimValue: e.finalState.StateHash,
StateData: e.finalState.Witness,
ProofData: []byte{},
}, e.finalState.Step, e.finalState.Exited, nil
} else {
return nil, 0, false, fmt.Errorf("loading unexpected state: %s, only support: %s", statePath, e.finalStatePath)
}
} }
func (e *stubGenerator) GenerateProof(ctx context.Context, dir string, i uint64) error { func (e *stubGenerator) GenerateProof(ctx context.Context, dir string, i uint64) error {
...@@ -244,6 +259,7 @@ func (e *stubGenerator) GenerateProof(ctx context.Context, dir string, i uint64) ...@@ -244,6 +259,7 @@ func (e *stubGenerator) GenerateProof(ctx context.Context, dir string, i uint64)
if e.finalState != nil && e.finalState.Step <= i { if e.finalState != nil && e.finalState.Step <= i {
// Requesting a trace index past the end of the trace // Requesting a trace index past the end of the trace
proofFile = vm.FinalStatePath(dir, false) proofFile = vm.FinalStatePath(dir, false)
e.finalStatePath = proofFile
data, err = json.Marshal(e.finalState) data, err = json.Marshal(e.finalState)
if err != nil { if err != nil {
return err return err
......
package asterisc package asterisc
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "os/exec"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils"
"github.com/ethereum-optimism/optimism/op-service/ioutil" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/vm"
) )
var asteriscWitnessLen = 362
// The state struct will be read from json. // The state struct will be read from json.
// other fields included in json are specific to FPVM implementation, and not required for trace provider. // other fields included in json are specific to FPVM implementation, and not required for trace provider.
type VMState struct { type VMState struct {
PC uint64 `json:"pc"` PC uint64 `json:"pc"`
Exited bool `json:"exited"` Exited bool `json:"exited"`
Step uint64 `json:"step"` Step uint64 `json:"step"`
Witness []byte `json:"witness"` Witness hexutil.Bytes `json:"witness"`
StateHash common.Hash `json:"stateHash"` StateHash common.Hash `json:"stateHash"`
}
func (state *VMState) validateStateHash() error {
exitCode := state.StateHash[0]
if exitCode >= 4 {
return fmt.Errorf("invalid stateHash: unknown exitCode %d", exitCode)
}
if (state.Exited && exitCode == mipsevm.VMStatusUnfinished) || (!state.Exited && exitCode != mipsevm.VMStatusUnfinished) {
return fmt.Errorf("invalid stateHash: invalid exitCode %d", exitCode)
}
return nil
} }
func (state *VMState) validateWitness() error { type StateConverter struct {
witnessLen := len(state.Witness) vmConfig vm.Config
if witnessLen != asteriscWitnessLen { cmdExecutor func(ctx context.Context, binary string, args ...string) (stdOut string, stdErr string, err error)
return fmt.Errorf("invalid witness: Length must be 362 but got %d", witnessLen)
}
return nil
} }
// validateState performs verification of state; it is not perfect. func NewStateConverter(vmConfig vm.Config) *StateConverter {
// It does not recalculate whether witness nor stateHash is correctly set from state. return &StateConverter{
func (state *VMState) validateState() error { vmConfig: vmConfig,
if err := state.validateStateHash(); err != nil { cmdExecutor: runCmd,
return err
}
if err := state.validateWitness(); err != nil {
return err
} }
return nil
} }
// parseState parses state from json and goes on state validation func (c *StateConverter) ConvertStateToProof(ctx context.Context, statePath string) (*utils.ProofData, uint64, bool, error) {
func parseState(path string) (*VMState, error) { stdOut, stdErr, err := c.cmdExecutor(ctx, c.vmConfig.VmBin, "witness", "--input", statePath)
file, err := ioutil.OpenDecompressed(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot open state file (%v): %w", path, err) return nil, 0, false, fmt.Errorf("state conversion failed: %w (%s)", err, stdErr)
}
return parseStateFromReader(file)
}
func parseStateFromReader(in io.ReadCloser) (*VMState, error) {
defer in.Close()
var state VMState
if err := json.NewDecoder(in).Decode(&state); err != nil {
return nil, fmt.Errorf("invalid asterisc VM state %w", err)
} }
if err := state.validateState(); err != nil { var data VMState
return nil, fmt.Errorf("invalid asterisc VM state %w", err) if err := json.Unmarshal([]byte(stdOut), &data); err != nil {
} return nil, 0, false, fmt.Errorf("failed to parse state data: %w", err)
return &state, nil
}
type StateConverter struct {
}
func NewStateConverter() *StateConverter {
return &StateConverter{}
}
func (c *StateConverter) ConvertStateToProof(_ context.Context, 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 // 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. // No execution is done, so no proof-data or oracle values are required.
return &utils.ProofData{ return &utils.ProofData{
ClaimValue: state.StateHash, ClaimValue: data.StateHash,
StateData: state.Witness, StateData: data.Witness,
ProofData: []byte{}, ProofData: []byte{},
OracleKey: nil, OracleKey: nil,
OracleValue: nil, OracleValue: nil,
OracleOffset: 0, OracleOffset: 0,
}, state.Step, state.Exited, nil }, data.Step, data.Exited, nil
}
func runCmd(ctx context.Context, binary string, args ...string) (stdOut string, stdErr string, err error) {
var outBuf bytes.Buffer
var errBuf bytes.Buffer
cmd := exec.CommandContext(ctx, binary, args...)
cmd.Stdout = &outBuf
cmd.Stderr = &errBuf
err = cmd.Run()
stdOut = outBuf.String()
stdErr = errBuf.String()
return
} }
package asterisc package asterisc
import ( import (
"compress/gzip" "context"
_ "embed"
"encoding/json" "encoding/json"
"os" "errors"
"path/filepath"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/vm"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
//go:embed test_data/state.json const testBinary = "./somewhere/asterisc"
var testState []byte
func TestLoadState(t *testing.T) { func TestStateConverter(t *testing.T) {
t.Run("Uncompressed", func(t *testing.T) { setup := func(t *testing.T) (*StateConverter, *capturingExecutor) {
dir := t.TempDir() vmCfg := vm.Config{
path := filepath.Join(dir, "state.json") VmBin: testBinary,
require.NoError(t, os.WriteFile(path, testState, 0644)) }
executor := &capturingExecutor{}
state, err := parseState(path) converter := NewStateConverter(vmCfg)
require.NoError(t, err) converter.cmdExecutor = executor.exec
return converter, executor
var expected VMState }
require.NoError(t, json.Unmarshal(testState, &expected))
require.Equal(t, &expected, state)
})
t.Run("Gzipped", func(t *testing.T) { t.Run("Valid", func(t *testing.T) {
dir := t.TempDir() converter, executor := setup(t)
path := filepath.Join(dir, "state.json.gz") data := VMState{
f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) Witness: []byte{1, 2, 3, 4},
require.NoError(t, err) StateHash: common.Hash{0xab},
defer f.Close() Step: 42,
writer := gzip.NewWriter(f) Exited: true,
_, err = writer.Write(testState) PC: 11,
}
ser, err := json.Marshal(data)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, writer.Close()) executor.stdOut = string(ser)
proof, step, exited, err := converter.ConvertStateToProof(context.Background(), "foo.json")
state, err := parseState(path)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, data.Exited, exited)
require.Equal(t, data.Step, step)
require.Equal(t, data.StateHash, proof.ClaimValue)
require.Equal(t, data.Witness, proof.StateData)
require.NotNil(t, proof.ProofData, "later validations require this to be non-nil")
var expected VMState require.Equal(t, testBinary, executor.binary)
require.NoError(t, json.Unmarshal(testState, &expected)) require.Equal(t, []string{"witness", "--input", "foo.json"}, executor.args)
require.Equal(t, &expected, state)
}) })
t.Run("InvalidStateWitness", func(t *testing.T) { t.Run("CommandError", func(t *testing.T) {
invalidWitnessLen := asteriscWitnessLen - 1 converter, executor := setup(t)
state := &VMState{ executor.err = errors.New("boom")
Step: 10, _, _, _, err := converter.ConvertStateToProof(context.Background(), "foo.json")
Exited: true, require.ErrorIs(t, err, executor.err)
Witness: make([]byte, invalidWitnessLen),
}
err := state.validateState()
require.ErrorContains(t, err, "invalid witness")
}) })
t.Run("InvalidStateHash", func(t *testing.T) { t.Run("InvalidOutput", func(t *testing.T) {
state := &VMState{ converter, executor := setup(t)
Step: 10, executor.stdOut = "blah blah"
Exited: true, _, _, _, err := converter.ConvertStateToProof(context.Background(), "foo.json")
Witness: make([]byte, asteriscWitnessLen), require.ErrorContains(t, err, "failed to parse state data")
}
// Unknown exit code
state.StateHash[0] = 37
err := state.validateState()
require.ErrorContains(t, err, "invalid stateHash: unknown exitCode")
// Exited but ExitCode is VMStatusUnfinished
state.StateHash[0] = 3
err = state.validateState()
require.ErrorContains(t, err, "invalid stateHash: invalid exitCode")
// Not Exited but ExitCode is not VMStatusUnfinished
state.Exited = false
for exitCode := 0; exitCode < 3; exitCode++ {
state.StateHash[0] = byte(exitCode)
err = state.validateState()
require.ErrorContains(t, err, "invalid stateHash: invalid exitCode")
}
}) })
} }
type capturingExecutor struct {
binary string
args []string
stdOut string
stdErr string
err error
}
func (c *capturingExecutor) exec(_ context.Context, binary string, args ...string) (string, string, error) {
c.binary = binary
c.args = args
return c.stdOut, c.stdErr, c.err
}
...@@ -40,7 +40,7 @@ func createTraceProvider( ...@@ -40,7 +40,7 @@ func createTraceProvider(
return cannon.NewTraceProvider(logger, m, cfg.Cannon, serverExecutor, prestateProvider, prestate, localInputs, dir, 42), nil return cannon.NewTraceProvider(logger, m, cfg.Cannon, serverExecutor, prestateProvider, prestate, localInputs, dir, 42), nil
case types.TraceTypeAsterisc: case types.TraceTypeAsterisc:
serverExecutor := vm.NewOpProgramServerExecutor() serverExecutor := vm.NewOpProgramServerExecutor()
stateConverter := asterisc.NewStateConverter() stateConverter := asterisc.NewStateConverter(cfg.Asterisc)
prestate, err := getPrestate(ctx, prestateHash, cfg.AsteriscAbsolutePreStateBaseURL, cfg.AsteriscAbsolutePreState, dir, stateConverter) prestate, err := getPrestate(ctx, prestateHash, cfg.AsteriscAbsolutePreStateBaseURL, cfg.AsteriscAbsolutePreState, dir, stateConverter)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -49,7 +49,7 @@ func createTraceProvider( ...@@ -49,7 +49,7 @@ func createTraceProvider(
return asterisc.NewTraceProvider(logger, m, cfg.Asterisc, serverExecutor, prestateProvider, prestate, localInputs, dir, 42), nil return asterisc.NewTraceProvider(logger, m, cfg.Asterisc, serverExecutor, prestateProvider, prestate, localInputs, dir, 42), nil
case types.TraceTypeAsteriscKona: case types.TraceTypeAsteriscKona:
serverExecutor := vm.NewKonaExecutor() serverExecutor := vm.NewKonaExecutor()
stateConverter := asterisc.NewStateConverter() stateConverter := asterisc.NewStateConverter(cfg.Asterisc)
prestate, err := getPrestate(ctx, prestateHash, cfg.AsteriscKonaAbsolutePreStateBaseURL, cfg.AsteriscKonaAbsolutePreState, dir, stateConverter) prestate, err := getPrestate(ctx, prestateHash, cfg.AsteriscKonaAbsolutePreStateBaseURL, cfg.AsteriscKonaAbsolutePreState, dir, stateConverter)
if err != nil { if err != nil {
return nil, err return nil, err
......
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