diff --git a/cannon/cmd/run.go b/cannon/cmd/run.go
index f4d57a114d5aeca7ea1e8c1995b896ecf56dd020..df1870d73554cb70151b63bd786fe3cb35353f31 100644
--- a/cannon/cmd/run.go
+++ b/cannon/cmd/run.go
@@ -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,21 +436,14 @@ 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)
-			}
 			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)
 			}
+			_, postStateHash := state.EncodeWitness()
 			proof := &Proof{
 				Step:      step,
-				Pre:       preStateHash,
+				Pre:       witness.StateHash,
 				Post:      postStateHash,
 				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,
diff --git a/cannon/cmd/witness.go b/cannon/cmd/witness.go
index c2f38daa362199d705c1beb2ff3511ea9822ed06..c869f2b4cf86bac10be4ba7ed0ff3a6b90f1ae12 100644
--- a/cannon/cmd/witness.go
+++ b/cannon/cmd/witness.go
@@ -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)
diff --git a/cannon/mipsevm/evm_test.go b/cannon/mipsevm/evm_test.go
index 14c53220c432e9fb3cab88f15ad603b8a11fd314..edc6675f55815e82ca137b2857aca7238f470406 100644
--- a/cannon/mipsevm/evm_test.go
+++ b/cannon/mipsevm/evm_test.go
@@ -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")
 	}
diff --git a/cannon/mipsevm/fuzz_evm_test.go b/cannon/mipsevm/fuzz_evm_test.go
index 9b11f9363c931ae9a11798ba19db9680b1119784..06b77a10247fd20d376f53386538ad92aeb76026 100644
--- a/cannon/mipsevm/fuzz_evm_test.go
+++ b/cannon/mipsevm/fuzz_evm_test.go
@@ -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")
 	})
diff --git a/cannon/mipsevm/iface.go b/cannon/mipsevm/iface.go
new file mode 100644
index 0000000000000000000000000000000000000000..2cc8f939fcebdec9138c5a1cc9f613c2719066ad
--- /dev/null
+++ b/cannon/mipsevm/iface.go
@@ -0,0 +1,39 @@
+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
+}
diff --git a/cannon/mipsevm/instrumented.go b/cannon/mipsevm/instrumented.go
index 011c7c4d06309f90b2a2686fd14b622a7464453a..fad3e541c1a185783a408d4b47e6d6daa2a4d773 100644
--- a/cannon/mipsevm/instrumented.go
+++ b/cannon/mipsevm/instrumented.go
@@ -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,
 	}
 }
 
diff --git a/cannon/mipsevm/state.go b/cannon/mipsevm/state.go
index c8a681f23e37eb9436722f89e4c454fa8a115f13..474f800219690ae65f4cc9a774235481eca9d4a1 100644
--- a/cannon/mipsevm/state.go
+++ b/cannon/mipsevm/state.go
@@ -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 {
diff --git a/cannon/mipsevm/state_test.go b/cannon/mipsevm/state_test.go
index dfb90674ec78ae902fcc339e2988ecc83bc2cf8d..fe36267926cacbb6683650ebc2743c84c4a91cc7 100644
--- a/cannon/mipsevm/state_test.go
+++ b/cannon/mipsevm/state_test.go
@@ -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)
diff --git a/cannon/mipsevm/witness.go b/cannon/mipsevm/witness.go
index 58039a86ea32ca22a0358dd0c233f004c31129a5..ef75db69c65c39bb3286488a6e5c907816b91a33 100644
--- a/cannon/mipsevm/witness.go
+++ b/cannon/mipsevm/witness.go
@@ -6,7 +6,8 @@ type LocalContext common.Hash
 
 type StepWitness struct {
 	// encoded state witness
-	State []byte
+	State     []byte
+	StateHash common.Hash
 
 	MemProof []byte
 
diff --git a/op-challenger/game/fault/trace/cannon/prestate.go b/op-challenger/game/fault/trace/cannon/prestate.go
index 67d7c55389c2756e5ef3a5941dd9dac1c27fe8b7..3853e619567ba99169fdf919e997b881be65a9b9 100644
--- a/op-challenger/game/fault/trace/cannon/prestate.go
+++ b/op-challenger/game/fault/trace/cannon/prestate.go
@@ -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
 }
diff --git a/op-challenger/game/fault/trace/cannon/prestate_test.go b/op-challenger/game/fault/trace/cannon/prestate_test.go
index a8616f1711808af738ff7ec8e401677dd4c1c556..9dadbbc94ff8dc9cd9ffa7256884cd2f33a6ed7e 100644
--- a/op-challenger/game/fault/trace/cannon/prestate_test.go
+++ b/op-challenger/game/fault/trace/cannon/prestate_test.go
@@ -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)
 	})
 
diff --git a/op-challenger/game/fault/trace/cannon/provider.go b/op-challenger/game/fault/trace/cannon/provider.go
index c565b9b39b75df40161b8a7dc679a2292469ca97..7b55035aa190141e3dd2e2b524bd59ec63ea0032 100644
--- a/op-challenger/game/fault/trace/cannon/provider.go
+++ b/op-challenger/game/fault/trace/cannon/provider.go
@@ -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),
diff --git a/op-challenger/game/fault/trace/cannon/provider_test.go b/op-challenger/game/fault/trace/cannon/provider_test.go
index 94277ed88399b1ef6e90860f3b693ff47ffa657b..87f6105c4279b14eab639c5a460760d1d0bbef0f 100644
--- a/op-challenger/game/fault/trace/cannon/provider_test.go
+++ b/op-challenger/game/fault/trace/cannon/provider_test.go
@@ -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)
 	})