Commit bdce0bfa authored by Inphi's avatar Inphi Committed by GitHub

cannon: Generic FPVM interface (#10993)

parent 73d56a9d
......@@ -23,6 +23,13 @@ import (
)
var (
RunType = &cli.StringFlag{
Name: "type",
Usage: "VM type to run. Options are 'cannon' (default)",
Value: "cannon",
// TODO(client-pod#903): This should be required once we have additional vm types
Required: false,
}
RunInputFlag = &cli.PathFlag{
Name: "input",
Usage: "path of input JSON state. Stdin if left empty.",
......@@ -250,14 +257,20 @@ func Guard(proc *os.ProcessState, fn StepFn) StepFn {
var _ mipsevm.PreimageOracle = (*ProcessPreimageOracle)(nil)
type VMType string
var cannonVMType VMType = "cannon"
func Run(ctx *cli.Context) error {
if ctx.Bool(RunPProfCPU.Name) {
defer profile.Start(profile.NoShutdownHook, profile.ProfilePath("."), profile.CPUProfile).Stop()
}
state, err := jsonutil.LoadJSON[mipsevm.State](ctx.Path(RunInputFlag.Name))
if err != nil {
return err
var vmType VMType
if vmTypeStr := ctx.String(RunType.Name); vmTypeStr == string(cannonVMType) {
vmType = cannonVMType
} else {
return fmt.Errorf("unknown VM type %q", vmType)
}
guestLogger := Logger(os.Stderr, log.LevelInfo)
......@@ -349,53 +362,65 @@ func Run(ctx *cli.Context) error {
}
}
us := mipsevm.NewInstrumentedState(state, po, outLog, errLog)
debugProgram := ctx.Bool(RunDebugFlag.Name)
if debugProgram {
if metaPath := ctx.Path(RunMetaFlag.Name); metaPath == "" {
return fmt.Errorf("cannot enable debug mode without a metadata file")
var vm mipsevm.FPVM
var debugProgram bool
if vmType == cannonVMType {
cannon, err := mipsevm.NewInstrumentedStateFromFile(ctx.Path(RunInputFlag.Name), po, outLog, errLog)
if err != nil {
return err
}
if err := us.InitDebug(meta); err != nil {
return fmt.Errorf("failed to initialize debug mode: %w", err)
debugProgram = ctx.Bool(RunDebugFlag.Name)
if debugProgram {
if metaPath := ctx.Path(RunMetaFlag.Name); metaPath == "" {
return fmt.Errorf("cannot enable debug mode without a metadata file")
}
if err := cannon.InitDebug(meta); err != nil {
return fmt.Errorf("failed to initialize debug mode: %w", err)
}
}
vm = cannon
} else {
return fmt.Errorf("unknown VM type %q", vmType)
}
proofFmt := ctx.String(RunProofFmtFlag.Name)
snapshotFmt := ctx.String(RunSnapshotFmtFlag.Name)
stepFn := us.Step
stepFn := vm.Step
if po.cmd != nil {
stepFn = Guard(po.cmd.ProcessState, stepFn)
}
start := time.Now()
startStep := state.Step
state := vm.GetState()
startStep := state.GetStep()
// avoid symbol lookups every instruction by preparing a matcher func
sleepCheck := meta.SymbolMatcher("runtime.notesleep")
for !state.Exited {
if state.Step%100 == 0 { // don't do the ctx err check (includes lock) too often
for !state.GetExited() {
step := state.GetStep()
if step%100 == 0 { // don't do the ctx err check (includes lock) too often
if err := ctx.Context.Err(); err != nil {
return err
}
}
step := state.Step
if infoAt(state) {
delta := time.Since(start)
l.Info("processing",
"step", step,
"pc", mipsevm.HexU32(state.Cpu.PC),
"insn", mipsevm.HexU32(state.Memory.GetMemory(state.Cpu.PC)),
"pc", mipsevm.HexU32(state.GetPC()),
"insn", mipsevm.HexU32(state.GetMemory().GetMemory(state.GetPC())),
"ips", float64(step-startStep)/(float64(delta)/float64(time.Second)),
"pages", state.Memory.PageCount(),
"mem", state.Memory.Usage(),
"name", meta.LookupSymbol(state.Cpu.PC),
"pages", state.GetMemory().PageCount(),
"mem", state.GetMemory().Usage(),
"name", meta.LookupSymbol(state.GetPC()),
)
}
if sleepCheck(state.Cpu.PC) { // don't loop forever when we get stuck because of an unexpected bad program
if sleepCheck(state.GetPC()) { // don't loop forever when we get stuck because of an unexpected bad program
return fmt.Errorf("got stuck in Go sleep at step %d", step)
}
......@@ -411,22 +436,15 @@ func Run(ctx *cli.Context) error {
}
if proofAt(state) {
preStateHash, err := state.EncodeWitness().StateHash()
if err != nil {
return fmt.Errorf("failed to hash prestate witness: %w", err)
}
_, preStateHash := state.EncodeWitness()
witness, err := stepFn(true)
if err != nil {
return fmt.Errorf("failed at proof-gen step %d (PC: %08x): %w", step, state.Cpu.PC, err)
}
postStateHash, err := state.EncodeWitness().StateHash()
if err != nil {
return fmt.Errorf("failed to hash poststate witness: %w", err)
return fmt.Errorf("failed at proof-gen step %d (PC: %08x): %w", step, state.GetPC(), err)
}
proof := &Proof{
Step: step,
Pre: preStateHash,
Post: postStateHash,
Post: witness.StateHash,
StateData: witness.State,
ProofData: witness.MemProof,
}
......@@ -441,11 +459,11 @@ func Run(ctx *cli.Context) error {
} else {
_, err = stepFn(false)
if err != nil {
return fmt.Errorf("failed at step %d (PC: %08x): %w", step, state.Cpu.PC, err)
return fmt.Errorf("failed at step %d (PC: %08x): %w", step, state.GetPC(), err)
}
}
lastPreimageKey, lastPreimageValue, lastPreimageOffset := us.LastPreimage()
lastPreimageKey, lastPreimageValue, lastPreimageOffset := vm.LastPreimage()
if lastPreimageOffset != ^uint32(0) {
if stopAtAnyPreimage {
l.Info("Stopping at preimage read")
......@@ -464,16 +482,16 @@ func Run(ctx *cli.Context) error {
}
}
}
l.Info("Execution stopped", "exited", state.Exited, "code", state.ExitCode)
l.Info("Execution stopped", "exited", state.GetExited(), "code", state.GetExitCode())
if debugProgram {
us.Traceback()
vm.Traceback()
}
if err := jsonutil.WriteJSON(ctx.Path(RunOutputFlag.Name), state, OutFilePerm); err != nil {
return fmt.Errorf("failed to write state output: %w", err)
}
if debugInfoFile := ctx.Path(RunDebugInfoFlag.Name); debugInfoFile != "" {
if err := jsonutil.WriteJSON(debugInfoFile, us.GetDebugInfo(), OutFilePerm); err != nil {
if err := jsonutil.WriteJSON(debugInfoFile, vm.GetDebugInfo(), OutFilePerm); err != nil {
return fmt.Errorf("failed to write benchmark data: %w", err)
}
}
......@@ -486,6 +504,7 @@ var RunCommand = &cli.Command{
Description: "Run VM step(s) and generate proof data to replicate onchain. See flags to match when to output a proof, a snapshot, or to stop early.",
Action: Run,
Flags: []cli.Flag{
RunType,
RunInputFlag,
RunOutputFlag,
RunProofAtFlag,
......
......@@ -30,11 +30,7 @@ func Witness(ctx *cli.Context) error {
if err != nil {
return fmt.Errorf("invalid input state (%v): %w", input, err)
}
witness := state.EncodeWitness()
h, err := witness.StateHash()
if err != nil {
return fmt.Errorf("failed to compute witness hash: %w", err)
}
witness, h := state.EncodeWitness()
if output != "" {
if err := os.WriteFile(output, witness, 0755); err != nil {
return fmt.Errorf("writing output to %v: %w", output, err)
......
......@@ -196,7 +196,7 @@ func TestEVM(t *testing.T) {
evmPost := evm.Step(t, stepWitness)
// verify the post-state matches.
// TODO: maybe more readable to decode the evmPost state, and do attribute-wise comparison.
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equalf(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM at step %d", state.Step)
}
......@@ -243,7 +243,7 @@ func TestEVMSingleStep(t *testing.T) {
evm := NewMIPSEVM(contracts, addrs)
evm.SetTracer(tracer)
evmPost := evm.Step(t, stepWitness)
goPost := us.state.EncodeWitness()
goPost, _ := us.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......@@ -421,7 +421,7 @@ func TestEVMSysWriteHint(t *testing.T) {
evm := NewMIPSEVM(contracts, addrs)
evm.SetTracer(tracer)
evmPost := evm.Step(t, stepWitness)
goPost := us.state.EncodeWitness()
goPost, _ := us.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......@@ -459,8 +459,9 @@ func TestEVMFault(t *testing.T) {
require.Panics(t, func() { _, _ = us.Step(true) })
insnProof := initialState.Memory.MerkleProof(0)
encodedWitness, _ := initialState.EncodeWitness()
stepWitness := &StepWitness{
State: initialState.EncodeWitness(),
State: encodedWitness,
MemProof: insnProof[:],
}
input := encodeStepInput(t, stepWitness, LocalContext{}, contracts.MIPS)
......@@ -509,7 +510,7 @@ func TestHelloEVM(t *testing.T) {
evmPost := evm.Step(t, stepWitness)
// verify the post-state matches.
// TODO: maybe more readable to decode the evmPost state, and do attribute-wise comparison.
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
}
......@@ -560,7 +561,7 @@ func TestClaimEVM(t *testing.T) {
evm.SetTracer(tracer)
evmPost := evm.Step(t, stepWitness)
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
}
......
......@@ -61,7 +61,7 @@ func FuzzStateSyscallBrk(f *testing.F) {
evm := NewMIPSEVM(contracts, addrs)
evmPost := evm.Step(t, stepWitness)
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......@@ -112,7 +112,7 @@ func FuzzStateSyscallClone(f *testing.F) {
evm := NewMIPSEVM(contracts, addrs)
evmPost := evm.Step(t, stepWitness)
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......@@ -173,7 +173,7 @@ func FuzzStateSyscallMmap(f *testing.F) {
evm := NewMIPSEVM(contracts, addrs)
evmPost := evm.Step(t, stepWitness)
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......@@ -223,7 +223,7 @@ func FuzzStateSyscallExitGroup(f *testing.F) {
evm := NewMIPSEVM(contracts, addrs)
evmPost := evm.Step(t, stepWitness)
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......@@ -288,7 +288,7 @@ func FuzzStateSyscallFcntl(f *testing.F) {
evm := NewMIPSEVM(contracts, addrs)
evmPost := evm.Step(t, stepWitness)
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......@@ -340,7 +340,7 @@ func FuzzStateHintRead(f *testing.F) {
evm := NewMIPSEVM(contracts, addrs)
evmPost := evm.Step(t, stepWitness)
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......@@ -406,7 +406,7 @@ func FuzzStatePreimageRead(f *testing.F) {
evm := NewMIPSEVM(contracts, addrs)
evmPost := evm.Step(t, stepWitness)
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......@@ -466,7 +466,7 @@ func FuzzStateHintWrite(f *testing.F) {
evm := NewMIPSEVM(contracts, addrs)
evmPost := evm.Step(t, stepWitness)
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......@@ -521,7 +521,7 @@ func FuzzStatePreimageWrite(f *testing.F) {
evm := NewMIPSEVM(contracts, addrs)
evmPost := evm.Step(t, stepWitness)
goPost := goState.state.EncodeWitness()
goPost, _ := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
......
package mipsevm
import "github.com/ethereum/go-ethereum/common"
type FPVMState interface {
GetMemory() *Memory
// GetPC returns the currently executing program counter
GetPC() uint32
// GetStep returns the current VM step
GetStep() uint64
// GetExited returns whether the state exited bit is set
GetExited() bool
// GetExitCode returns the exit code
GetExitCode() uint8
// EncodeWitness returns the witness for the current state and the state hash
EncodeWitness() (witness []byte, hash common.Hash)
}
type FPVM interface {
// GetState returns the current state of the VM. The FPVMState is updated by successive calls to Step
GetState() FPVMState
// Step executes a single instruction and returns the witness for the step
Step(includeProof bool) (*StepWitness, error)
// LastPreimage returns the last preimage accessed by the VM
LastPreimage() (preimageKey [32]byte, preimage []byte, preimageOffset uint32)
// Traceback prints a traceback of the program to the console
Traceback()
// GetDebugInfo returns debug information about the VM
GetDebugInfo() *DebugInfo
}
......@@ -3,6 +3,8 @@ package mipsevm
import (
"errors"
"io"
"github.com/ethereum-optimism/optimism/op-service/jsonutil"
)
type PreimageOracle interface {
......@@ -48,6 +50,19 @@ func NewInstrumentedState(state *State, po PreimageOracle, stdOut, stdErr io.Wri
}
}
func NewInstrumentedStateFromFile(stateFile string, po PreimageOracle, stdOut, stdErr io.Writer) (*InstrumentedState, error) {
state, err := jsonutil.LoadJSON[State](stateFile)
if err != nil {
return nil, err
}
return &InstrumentedState{
state: state,
stdOut: stdOut,
stdErr: stdErr,
preimageOracle: &trackingOracle{po: po},
}, nil
}
func (m *InstrumentedState) InitDebug(meta *Metadata) error {
if meta == nil {
return errors.New("metadata is nil")
......@@ -64,9 +79,11 @@ func (m *InstrumentedState) Step(proof bool) (wit *StepWitness, err error) {
if proof {
insnProof := m.state.Memory.MerkleProof(m.state.Cpu.PC)
encodedWitness, stateHash := m.state.EncodeWitness()
wit = &StepWitness{
State: m.state.EncodeWitness(),
MemProof: insnProof[:],
State: encodedWitness,
StateHash: stateHash,
MemProof: insnProof[:],
}
}
err = m.mipsStep()
......@@ -89,11 +106,15 @@ func (m *InstrumentedState) LastPreimage() ([32]byte, []byte, uint32) {
return m.lastPreimageKey, m.lastPreimage, m.lastPreimageOffset
}
func (d *InstrumentedState) GetDebugInfo() *DebugInfo {
func (m *InstrumentedState) GetState() FPVMState {
return m.state
}
func (m *InstrumentedState) GetDebugInfo() *DebugInfo {
return &DebugInfo{
Pages: d.state.Memory.PageCount(),
NumPreimageRequests: d.preimageOracle.numPreimageRequests,
TotalPreimageSize: d.preimageOracle.totalPreimageSize,
Pages: m.state.Memory.PageCount(),
NumPreimageRequests: m.preimageOracle.numPreimageRequests,
TotalPreimageSize: m.preimageOracle.totalPreimageSize,
}
}
......
......@@ -104,13 +104,23 @@ func (s *State) UnmarshalJSON(data []byte) error {
return nil
}
func (s *State) GetPC() uint32 { return s.Cpu.PC }
func (s *State) GetExitCode() uint8 { return s.ExitCode }
func (s *State) GetExited() bool { return s.Exited }
func (s *State) GetStep() uint64 { return s.Step }
func (s *State) VMStatus() uint8 {
return vmStatus(s.Exited, s.ExitCode)
}
func (s *State) EncodeWitness() StateWitness {
func (s *State) GetMemory() *Memory {
return s.Memory
}
func (s *State) EncodeWitness() ([]byte, common.Hash) {
out := make([]byte, 0)
memRoot := s.Memory.MerkleRoot()
out = append(out, memRoot[:]...)
......@@ -131,7 +141,7 @@ func (s *State) EncodeWitness() StateWitness {
for _, r := range s.Registers {
out = binary.BigEndian.AppendUint32(out, r)
}
return out
return out, stateHashFromWitness(out)
}
type StateWitness []byte
......@@ -147,14 +157,20 @@ func (sw StateWitness) StateHash() (common.Hash, error) {
if len(sw) != 226 {
return common.Hash{}, fmt.Errorf("Invalid witness length. Got %d, expected 226", len(sw))
}
return stateHashFromWitness(sw), nil
}
func stateHashFromWitness(sw []byte) common.Hash {
if len(sw) != 226 {
panic("Invalid witness length")
}
hash := crypto.Keccak256Hash(sw)
offset := 32*2 + 4*6
exitCode := sw[offset]
exited := sw[offset+1]
status := vmStatus(exited == 1, exitCode)
hash[0] = status
return hash, nil
return hash
}
func vmStatus(exited bool, exitCode uint8) uint8 {
......
......@@ -104,9 +104,7 @@ func TestStateHash(t *testing.T) {
ExitCode: c.exitCode,
}
actualWitness := state.EncodeWitness()
actualStateHash, err := StateWitness(actualWitness).StateHash()
require.NoError(t, err, "Error hashing witness")
actualWitness, actualStateHash := state.EncodeWitness()
require.Equal(t, len(actualWitness), StateWitnessSize, "Incorrect witness size")
expectedWitness := make(StateWitness, 226)
......@@ -118,7 +116,7 @@ func TestStateHash(t *testing.T) {
exited = 1
}
expectedWitness[exitedOffset+1] = uint8(exited)
require.Equal(t, expectedWitness[:], actualWitness[:], "Incorrect witness")
require.EqualValues(t, expectedWitness[:], actualWitness[:], "Incorrect witness")
expectedStateHash := crypto.Keccak256Hash(actualWitness)
expectedStateHash[0] = vmStatus(c.exited, c.exitCode)
......
......@@ -6,7 +6,8 @@ type LocalContext common.Hash
type StepWitness struct {
// encoded state witness
State []byte
State []byte
StateHash common.Hash
MemProof []byte
......
......@@ -6,7 +6,6 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
)
......@@ -22,26 +21,23 @@ func NewPrestateProvider(prestate string) *CannonPrestateProvider {
return &CannonPrestateProvider{prestate: prestate}
}
func (p *CannonPrestateProvider) absolutePreState() ([]byte, error) {
func (p *CannonPrestateProvider) absolutePreState() ([]byte, common.Hash, error) {
state, err := parseState(p.prestate)
if err != nil {
return nil, fmt.Errorf("cannot load absolute pre-state: %w", err)
return nil, common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err)
}
return state.EncodeWitness(), nil
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
}
state, err := p.absolutePreState()
_, hash, err := p.absolutePreState()
if err != nil {
return common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err)
}
hash, err := mipsevm.StateWitness(state).StateHash()
if err != nil {
return common.Hash{}, fmt.Errorf("cannot hash absolute pre-state: %w", err)
}
p.prestateCommitment = hash
return hash, nil
}
......@@ -56,8 +56,7 @@ func TestAbsolutePreStateCommitment(t *testing.T) {
Step: 0,
Registers: [32]uint32{},
}
expected, err := state.EncodeWitness().StateHash()
require.NoError(t, err)
_, expected := state.EncodeWitness()
require.Equal(t, expected, actual)
})
......
......@@ -132,11 +132,7 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*utils.P
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 := state.EncodeWitness()
witnessHash, err := mipsevm.StateWitness(witness).StateHash()
if err != nil {
return nil, fmt.Errorf("cannot hash witness: %w", err)
}
witness, witnessHash := state.EncodeWitness()
proof := &utils.ProofData{
ClaimValue: witnessHash,
StateData: hexutil.Bytes(witness),
......
......@@ -57,8 +57,7 @@ func TestGet(t *testing.T) {
value, err := provider.Get(context.Background(), PositionFromTraceIndex(provider, big.NewInt(7000)))
require.NoError(t, err)
require.Contains(t, generator.generated, 7000, "should have tried to generate the proof")
stateHash, err := generator.finalState.EncodeWitness().StateHash()
require.NoError(t, err)
_, stateHash := generator.finalState.EncodeWitness()
require.Equal(t, stateHash, value)
})
......@@ -149,7 +148,7 @@ func TestGetStepData(t *testing.T) {
require.NoError(t, err)
require.Contains(t, generator.generated, 7000, "should have tried to generate the proof")
witness := generator.finalState.EncodeWitness()
witness, _ := generator.finalState.EncodeWitness()
require.EqualValues(t, witness, preimage)
require.Equal(t, []byte{}, proof)
require.Nil(t, data)
......@@ -190,7 +189,8 @@ func TestGetStepData(t *testing.T) {
require.NoError(t, err)
require.Empty(t, generator.generated, "should not have to generate the proof again")
require.EqualValues(t, initGenerator.finalState.EncodeWitness(), preimage)
encodedWitness, _ := initGenerator.finalState.EncodeWitness()
require.EqualValues(t, encodedWitness, preimage)
require.Empty(t, proof)
require.Nil(t, data)
})
......
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