Commit ffe301ca authored by vicotor's avatar vicotor

change data struct to nebula

parent 0e07951f
node_modules
artifacts
cache
.*.swp
venv
.idea
*.log
testdata/example/bin
contracts/out
*.pprof
*.out
bin
multicannon/embeds/cannon*
FROM golang:1.22.7-alpine3.20 AS builder
RUN apk add --no-cache make bash
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
WORKDIR /app
RUN echo "go mod cache: $(go env GOMODCACHE)"
RUN echo "go build cache: $(go env GOCACHE)"
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build go mod download
COPY . /app
# We avoid copying the full .git dir into the build for just some metadata.
# Instead, specify:
# --build-arg GIT_COMMIT=$(git rev-parse HEAD)
# --build-arg GIT_DATE=$(git show -s --format='%ct')
ARG GIT_COMMIT
ARG GIT_DATE
ARG TARGETOS TARGETARCH
FROM --platform=$BUILDPLATFORM us-docker.pkg.dev/oplabs-tools-artifacts/images/cannon:v1.1.0-alpha.4 AS cannon-v2
FROM --platform=$BUILDPLATFORM builder AS cannon-verify
COPY --from=cannon-v2 /usr/local/bin/cannon /usr/local/bin/cannon-v2
# verify the latest singlethreaded VM behavior against cannon-v2
RUN cd cannon && make diff-singlethreaded-2-cannon -e OTHER_CANNON=/usr/local/bin/cannon-v2
RUN --mount=type=cache,target=/root/.cache/go-build cd cannon && \
make diff-singlethreaded-2-cannon -e OTHER_CANNON=/usr/local/bin/cannon-v2 \
GOOS=$TARGETOS GOARCH=$TARGETARCH GITCOMMIT=$GIT_COMMIT GITDATE=$GIT_DATE
MIT License
Copyright (c) 2021 Optimism
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
GITCOMMIT ?= $(shell git rev-parse HEAD)
GITDATE ?= $(shell git show -s --format='%ct')
VERSION ?= v0.0.0
LDFLAGSSTRING +=-X main.GitCommit=$(GITCOMMIT)
LDFLAGSSTRING +=-X main.GitDate=$(GITDATE)
LDFLAGSSTRING +=-X github.com/ethereum-optimism/optimism/cannon/multicannon/version.Version=$(VERSION)
LDFLAGSSTRING +=-X github.com/ethereum-optimism/optimism/cannon/multicannon/version.Meta=$(VERSION_META)
LDFLAGS := -ldflags "$(LDFLAGSSTRING)"
# Use the old Apple linker to workaround broken xcode - https://github.com/golang/go/issues/65169
ifeq ($(shell uname),Darwin)
FUZZLDFLAGS := -ldflags=-extldflags=-Wl,-ld_classic
endif
.DEFAULT_GOAL := cannon
# The MIPS64 r1 opcodes not supported by cannon. This list does not include coprocess-specific opcodes.
UNSUPPORTED_OPCODES := (dclo|dclz)
CANNON32_FUZZTIME := 10s
CANNON64_FUZZTIME := 20s
cannon32-impl:
env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build --tags=cannon32 -v $(LDFLAGS) -o ./bin/cannon32-impl .
cannon64-impl:
env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build --tags=cannon64 -v $(LDFLAGS) -o ./bin/cannon64-impl .
# Note: This target is used by ./scripts/build-legacy-cannons.sh
# It should build the individual versions of cannons and copy them into place in hte multicannon/embeds directory
# Ideally, preserve backwards compatibility with this behaviour but if it needs to change, build-legacy-cannons.sh will
# need to be updated to account for different behaviours in different versions.
# Each embed is suffixed with the latest `StateVersion` number corresponding to the target VM and architecture.
cannon-embeds: cannon32-impl cannon64-impl
# 32-bit singlethreaded vm
@cp bin/cannon32-impl ./multicannon/embeds/cannon-2
# 32-bit multithreaded vm
@cp bin/cannon32-impl ./multicannon/embeds/cannon-5
# 64-bit multithreaded vm
@cp bin/cannon64-impl ./multicannon/embeds/cannon-6
cannon: cannon-embeds
env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build -v $(LDFLAGS) -o ./bin/cannon ./multicannon/
clean:
rm -rf bin multicannon/embeds/cannon*
elf:
make -C ./testdata/example elf
sanitize-program:
@if ! { mips-linux-gnu-objdump -d -j .text $$GUEST_PROGRAM | awk '{print $3}' | grep -Ew -m1 "$(UNSUPPORTED_OPCODES)"; }; then \
echo "guest program is sanitized for unsupported instructions"; \
else \
echo "found unsupported instructions in the guest program"; \
exit 1; \
fi
contract:
cd ../packages/contracts-bedrock && forge build
test: elf contract test64
go test -v ./...
test64: elf contract
go test -tags=cannon64 -run '(TestEVM.*64|TestHelloEVM|TestClaimEVM)' ./mipsevm/tests
diff-%-cannon: cannon elf
$$OTHER_CANNON load-elf --type $* --path ./testdata/example/bin/hello.elf --out ./bin/prestate-other.bin.gz --meta ""
./bin/cannon load-elf --type $* --path ./testdata/example/bin/hello.elf --out ./bin/prestate.bin.gz --meta ""
@cmp ./bin/prestate-other.bin.gz ./bin/prestate.bin.gz
@if [ $$? -eq 0 ]; then \
echo "Generated identical prestates"; \
else \
echo "Generated different prestates"; \
exit 1; \
fi
$$OTHER_CANNON run --proof-at '=0' --stop-at '=100000000' --input=./bin/prestate.bin.gz --output ./bin/out-other.bin.gz --meta ""
./bin/cannon run --proof-at '=0' --stop-at '=100000000' --input=./bin/prestate.bin.gz --output ./bin/out.bin.gz --meta ""
@cmp ./bin/out-other.bin.gz ./bin/out.bin.gz
@if [ $$? -eq 0 ]; then \
echo "Generated identical states"; \
else \
echo "Generated different prestates"; \
exit 1; \
fi
cannon-stf-verify:
@docker build --progress plain -f Dockerfile.diff ../
fuzz:
printf "%s\n" \
"go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime $(CANNON32_FUZZTIME) -fuzz=FuzzStateSyscallBrk ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime $(CANNON32_FUZZTIME) -fuzz=FuzzStateSyscallMmap ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime $(CANNON32_FUZZTIME) -fuzz=FuzzStateSyscallExitGroup ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime $(CANNON32_FUZZTIME) -fuzz=FuzzStateSyscallFcntl ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime $(CANNON32_FUZZTIME) -fuzz=FuzzStateHintRead ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 20s -fuzz=FuzzStatePreimageRead ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime $(CANNON32_FUZZTIME) -fuzz=FuzzStateHintWrite ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 20s -fuzz=FuzzStatePreimageWrite ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime $(CANNON32_FUZZTIME) -fuzz=FuzzStateSyscallCloneST ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime $(CANNON32_FUZZTIME) -fuzz=FuzzStateSyscallCloneMT ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStateConsistencyMulOp ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStateConsistencyMultOp ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) -tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStateConsistencyMultuOp ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) --tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStateSyscallBrk ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) --tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStateSyscallMmap ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) --tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStateSyscallExitGroup ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) --tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStateSyscallFcntl ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) --tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStateHintRead ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) --tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStatePreimageRead ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) --tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStateHintWrite ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) --tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStatePreimageWrite ./mipsevm/tests" \
"go test $(FUZZLDFLAGS) --tags=cannon64 -run NOTAREALTEST -v -fuzztime $(CANNON64_FUZZTIME) -fuzz=FuzzStateSyscallCloneMT ./mipsevm/tests" \
| parallel -j 8 {}
.PHONY: \
cannon32-impl \
cannon64-impl \
cannon-embeds \
cannon \
clean \
test \
lint \
fuzz \
diff-%-cannon \
cannon-stf-verify
<!--![cannon](https://upload.wikimedia.org/wikipedia/commons/8/80/Cannon%2C_Château_du_Haut-Koenigsbourg%2C_France.jpg)-->
<!--![cannon](https://cdn1.epicgames.com/ue/product/Featured/SCIFIWEAPONBUNDLE_featured-894x488-83fbc936b6d86edcbbe892b1a6780224.png)-->
<!--![cannon](https://static.wikia.nocookie.net/ageofempires/images/8/80/Bombard_cannon_aoe2DE.png/revision/latest/top-crop/width/360/height/360?cb=20200331021834)-->
<!--![cannon](https://paradacreativa.es/wp-content/uploads/2021/05/Canon-orbital-GTA-01.jpg)-->
---
Cannon *(cannon cannon cannon)* is an onchain MIPS instruction emulator.
Cannon supports EVM-equivalent fault proofs by enabling Geth to run onchain,
one instruction at a time, as part of an interactive dispute game.
* It's Go code
* ...that runs an EVM
* ...emulating a MIPS machine
* ...running compiled Go code
* ...that runs an EVM
For more information, see [Docs](./docs/README.md).
## Usage
```shell
# Build op-program server-mode and MIPS-client binaries.
cd ../op-program
make op-program # build
# Switch back to cannon, and build the CLI
cd ../cannon
make cannon
# Transform MIPS op-program client binary into first VM state.
# This outputs state.bin.gz (VM state) and meta.json (for debug symbols).
./bin/cannon load-elf --type singlethreaded-2 --path=../op-program/bin/op-program-client.elf
# Run cannon emulator (with example inputs)
# Note that the server-mode op-program command is passed into cannon (after the --),
# it runs as sub-process to provide the pre-image data.
#
# Note:
# - The L2 RPC is an archive L2 node on OP MAINNET.
# - The L1 RPC is a non-archive RPC, also change `--l1.rpckind` to reflect the correct L1 RPC type.
# - The network flag is only suitable for specific networks(https://github.com/ethereum-optimism/superchain-registry/blob/main/chainList.json). If you are running on the devnet, please use '--l2.genesis' to supply a path to the L2 devnet genesis file.
./bin/cannon run \
--pprof.cpu \
--info-at '%10000000' \
--proof-at '=<TRACE_INDEX>' \
--stop-at '=<STOP_INDEX>' \
--snapshot-at '%1000000000' \
--input ./state.bin.gz \
-- \
../op-program/bin/op-program \
--network <network name> \
--l1 <L1_URL> \
--l2 <L2_URL> \
--l1.head <L1_HEAD> \
--l2.claim <L2_CLAIM> \
--l2.head <L2_HEAD> \
--l2.blocknumber <L2_BLOCK_NUMBER> \
--l2.outputroot <L2_OUTPUT_ROOT>
--datadir /tmp/fpp-database \
--log.format terminal \
--server
# Add --proof-at '=12345' (or pick other pattern, see --help)
# to pick a step to build a proof for (e.g. exact step, every N steps, etc.)
# Also see `./bin/cannon run --help` for more options
```
## Contracts
The Cannon contracts:
- `MIPS.sol`: A MIPS emulator implementation, to run a single instruction onchain, with merkleized VM memory.
- `PreimageOracle.sol`: implements the pre-image oracle ABI, to support the instruction execution pre-image requests.
The smart-contracts are integrated into the Optimism monorepo contracts:
[`../packages/contracts-bedrock/src/cannon`](../packages/contracts-bedrock/src/cannon)
## `mipsevm`
`mipsevm` is Go tooling to test the onchain MIPS implementation, and generate proof data.
## `example`
Example programs that can be run and proven with Cannon.
Optional dependency, but required for `mipsevm` Go tests.
See [`testdata/example/Makefile`](./testdata/example/Makefile) for building the example MIPS binaries.
## License
MIT, see [`LICENSE`](./LICENSE) file.
**Note: This code is unaudited.**
In NO WAY should it be used to secure any monetary value before testing and auditing.
This is experimental software, and should be treated as such.
The authors of this project make no guarantees of security of ANY KIND.
package cmd
import (
"debug/elf"
"fmt"
"github.com/urfave/cli/v2"
"github.com/exchain/go-exchain/cannon/mipsevm"
"github.com/exchain/go-exchain/cannon/mipsevm/multithreaded"
"github.com/exchain/go-exchain/cannon/mipsevm/program"
"github.com/exchain/go-exchain/cannon/mipsevm/singlethreaded"
"github.com/exchain/go-exchain/cannon/mipsevm/versions"
openum "github.com/exchain/go-exchain/op-service/enum"
"github.com/exchain/go-exchain/op-service/ioutil"
"github.com/exchain/go-exchain/op-service/jsonutil"
"github.com/exchain/go-exchain/op-service/serialize"
)
var (
LoadELFVMTypeFlag = &cli.StringFlag{
Name: "type",
Usage: "VM type to create state for. Valid options: " + openum.EnumString(stateVersions()),
Required: true,
}
LoadELFPathFlag = &cli.PathFlag{
Name: "path",
Usage: "Path to 32/64-bit big-endian MIPS ELF file",
TakesFile: true,
Required: true,
}
LoadELFOutFlag = &cli.PathFlag{
Name: "out",
Usage: "Output path to write state to. State is dumped to stdout if set to '-'. Not written if empty. Use file extension '.bin', '.bin.gz', or '.json' for binary, compressed binary, or JSON formats.",
Value: "state.bin.gz",
Required: false,
}
LoadELFMetaFlag = &cli.PathFlag{
Name: "meta",
Usage: "Write metadata file, for symbol lookup during program execution. None if empty.",
Value: "meta.json",
Required: false,
}
)
func stateVersions() []string {
vers := make([]string, len(versions.StateVersionTypes))
for i, v := range versions.StateVersionTypes {
vers[i] = v.String()
}
return vers
}
func LoadELF(ctx *cli.Context) error {
elfPath := ctx.Path(LoadELFPathFlag.Name)
elfProgram, err := elf.Open(elfPath)
if err != nil {
return fmt.Errorf("failed to open ELF file %q: %w", elfPath, err)
}
if elfProgram.Machine != elf.EM_MIPS {
return fmt.Errorf("ELF is not big-endian MIPS R3000, but got %q", elfProgram.Machine.String())
}
var createInitialState func(f *elf.File) (mipsevm.FPVMState, error)
var patcher = program.PatchStack
ver, err := versions.ParseStateVersion(ctx.String(LoadELFVMTypeFlag.Name))
if err != nil {
return err
}
switch ver {
case versions.VersionSingleThreaded2:
createInitialState = func(f *elf.File) (mipsevm.FPVMState, error) {
return program.LoadELF(f, singlethreaded.CreateInitialState)
}
patcher = func(state mipsevm.FPVMState) error {
err := program.PatchGoGC(elfProgram, state)
if err != nil {
return err
}
return program.PatchStack(state)
}
case versions.VersionMultiThreaded_v2, versions.VersionMultiThreaded64_v3:
createInitialState = func(f *elf.File) (mipsevm.FPVMState, error) {
return program.LoadELF(f, multithreaded.CreateInitialState)
}
default:
return fmt.Errorf("unsupported state version: %d (%s)", ver, ver.String())
}
state, err := createInitialState(elfProgram)
if err != nil {
return fmt.Errorf("failed to load ELF data into VM state: %w", err)
}
err = patcher(state)
if err != nil {
return fmt.Errorf("failed to patch state: %w", err)
}
meta, err := program.MakeMetadata(elfProgram)
if err != nil {
return fmt.Errorf("failed to compute program metadata: %w", err)
}
if err := jsonutil.WriteJSON[*program.Metadata](meta, ioutil.ToStdOutOrFileOrNoop(ctx.Path(LoadELFMetaFlag.Name), OutFilePerm)); err != nil {
return fmt.Errorf("failed to output metadata: %w", err)
}
// Ensure the state is written with appropriate version information
versionedState, err := versions.NewFromState(state)
if err != nil {
return fmt.Errorf("failed to create versioned state: %w", err)
}
return serialize.Write(ctx.Path(LoadELFOutFlag.Name), versionedState, OutFilePerm)
}
func CreateLoadELFCommand(action cli.ActionFunc) *cli.Command {
return &cli.Command{
Name: "load-elf",
Usage: "Load ELF file into Cannon state",
Description: "Load ELF file into Cannon state",
Action: action,
Flags: []cli.Flag{
LoadELFVMTypeFlag,
LoadELFPathFlag,
LoadELFOutFlag,
LoadELFMetaFlag,
},
}
}
var LoadELFCommand = CreateLoadELFCommand(LoadELF)
package cmd
import (
"io"
"log/slog"
"os"
"golang.org/x/term"
"github.com/ethereum/go-ethereum/log"
)
func Logger(w io.Writer, lvl slog.Level) log.Logger {
if term.IsTerminal(int(os.Stdout.Fd())) {
return log.NewLogger(log.LogfmtHandlerWithLevel(w, lvl))
} else {
return log.NewLogger(rawLogHandler(w, lvl))
}
}
// rawLogHandler returns a handler that strips out the time attribute
func rawLogHandler(wr io.Writer, lvl slog.Level) slog.Handler {
return slog.NewTextHandler(wr, &slog.HandlerOptions{
ReplaceAttr: replaceAttr,
Level: &leveler{lvl},
})
}
type leveler struct{ minLevel slog.Level }
func (l *leveler) Level() slog.Level {
return l.minLevel
}
func replaceAttr(_ []string, attr slog.Attr) slog.Attr {
if attr.Key == slog.TimeKey {
return slog.Attr{}
}
return attr
}
package cmd
import (
"fmt"
"strconv"
"strings"
)
type VMState interface {
GetStep() uint64
}
type StepMatcher func(st VMState) bool
type StepMatcherFlag struct {
repr string
matcher StepMatcher
}
func MustStepMatcherFlag(pattern string) *StepMatcherFlag {
out := new(StepMatcherFlag)
if err := out.Set(pattern); err != nil {
panic(err)
}
return out
}
func (m *StepMatcherFlag) Set(value string) error {
m.repr = value
if value == "" || value == "never" {
m.matcher = func(st VMState) bool {
return false
}
} else if value == "always" {
m.matcher = func(st VMState) bool {
return true
}
} else if strings.HasPrefix(value, "=") {
when, err := strconv.ParseUint(value[1:], 0, 64)
if err != nil {
return fmt.Errorf("failed to parse step number: %w", err)
}
m.matcher = func(st VMState) bool {
return st.GetStep() == when
}
} else if strings.HasPrefix(value, "%") {
when, err := strconv.ParseUint(value[1:], 0, 64)
if err != nil {
return fmt.Errorf("failed to parse step interval number: %w", err)
}
m.matcher = func(st VMState) bool {
return st.GetStep()%when == 0
}
} else {
return fmt.Errorf("unrecognized step matcher: %q", value)
}
return nil
}
func (m *StepMatcherFlag) String() string {
return m.repr
}
func (m *StepMatcherFlag) Matcher() StepMatcher {
if m.matcher == nil { // Set(value) is not called for omitted inputs, default to never matching.
return func(st VMState) bool {
return false
}
}
return m.matcher
}
func (m *StepMatcherFlag) Clone() any {
var out StepMatcherFlag
if err := out.Set(m.repr); err != nil {
panic(fmt.Errorf("invalid repr: %w", err))
}
return &out
}
This diff is collapsed.
package cmd
import (
"fmt"
"os"
factory "github.com/exchain/go-exchain/cannon/mipsevm/versions"
"github.com/exchain/go-exchain/op-service/ioutil"
"github.com/exchain/go-exchain/op-service/jsonutil"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/urfave/cli/v2"
)
var (
WitnessInputFlag = &cli.PathFlag{
Name: "input",
Usage: "path of input JSON state.",
TakesFile: true,
Required: true,
}
WitnessOutputFlag = &cli.PathFlag{
Name: "output",
Usage: "path to write binary witness.",
TakesFile: true,
}
)
type response struct {
WitnessHash common.Hash `json:"witnessHash"`
Witness hexutil.Bytes `json:"witness"`
Step uint64 `json:"step"`
Exited bool `json:"exited"`
ExitCode uint8 `json:"exitCode"`
}
func Witness(ctx *cli.Context) error {
input := ctx.Path(WitnessInputFlag.Name)
witnessOutput := ctx.Path(WitnessOutputFlag.Name)
state, err := factory.LoadStateFromFile(input)
if err != nil {
return fmt.Errorf("invalid input state (%v): %w", input, err)
}
witness, h := state.EncodeWitness()
if witnessOutput != "" {
if err := os.WriteFile(witnessOutput, witness, 0755); err != nil {
return fmt.Errorf("writing output to %v: %w", witnessOutput, err)
}
}
output := response{
WitnessHash: h,
Witness: witness,
Step: state.GetStep(),
Exited: state.GetExited(),
ExitCode: state.GetExitCode(),
}
if err := jsonutil.WriteJSON(output, ioutil.ToStdOut()); err != nil {
return fmt.Errorf("failed to write response: %w", err)
}
return nil
}
func CreateWitnessCommand(action cli.ActionFunc) *cli.Command {
return &cli.Command{
Name: "witness",
Usage: "Convert a Cannon JSON state into a binary witness",
Description: "Convert a Cannon JSON state into a binary witness. Basic data about the state is printed to stdout in JSON format.",
Action: action,
Flags: []cli.Flag{
WitnessInputFlag,
WitnessOutputFlag,
},
}
}
var WitnessCommand = CreateWitnessCommand(Witness)
# Cannon Overview
Cannon has two main components:
- Onchain `MIPS.sol`: EVM implementation to verify execution of a single MIPS instruction.
- Offchain `mipsevm`: Go implementation to produce a proof for any MIPS instruction to verify onchain.
Between these two modes of operation, the [witness data](#witness-data) is essential,
to reproduce the same instruction onchain as offchain.
## Onchain `MIPS.sol`
This is an onchain implementation of big-endian 32-bit MIPS instruction execution.
This covers MIPS III, R3000, as required by the `mips` Go compiler/runtime target.
The syscall instruction is implemented to simulate a minimal subset of the Linux kernel,
just enough to serve the needs of a basic Go program:
allocate memory, read/write to certain file-descriptors, and exit.
Note that this does not include concurrency related system calls: when running Go programs,
the GC has to be disabled, since it runs concurrently.
This is done by patching out specific runtime functions that start the GC,
by simply inserting jumps to the return-address, before the functions spin up any additional threads.
## Offchain `mipsevm`
This is an instrumented emulator, also running big-endian 32-bit MIPS, that executes one step at a time,
and maintains the same state as used for onchain execution.
The difference is that it has access to the full memory, and pre-image oracle.
And as it executes each step, it can optionally produce the witness data for the step, to repeat it onchain.
The Cannon CLI is used to load a program into an initial state,
transition it to N steps quickly without witness generation, and 1 step while producing a witness.
`mipsevm` is instrumented for proof generation and handles delay-slots by isolating each individual instruction
and tracking `nextPC` to emulate the delayed `PC` changes after delay-slot execution.
## Witness Data
There are 3 types of witness data involved in onchain execution:
- [Packed State](#packed-state)
- [Memory proofs](#memory-proofs)
- [Pre-image data](#pre-image-data)
### Packed State
The Packed State is provided in every executed onchain instruction.
See [Cannon VM Specs](https://github.com/ethereum-optimism/specs/blob/main/specs/fault-proof/cannon-fault-proof-vm.md#state) for
details on the state structure.
The packed state is small! The `State` data can be packed in such a small amount of EVM words,
that it is more efficient to fully provide it, than to create a proof for each individual part involved in execution.
The program stops changing the state when `exited` is set to true. The exit-code is remembered,
to determine if the program is successful, or panicked/exited in some unexpected way.
This outcome can be used to determine truthiness of claims that are verified as part of the program execution.
### Memory proofs
Simplicity is key here. Previous versions of Cannon used to overlap memory and register state,
and represent it with a Merkle Patricia Trie.
This added a lot of complexity, as there would be multiple dynamically-sized MPT proofs,
with read and write operations, during a single instruction.
Instead, memory in Cannon is now represented as a binary merkle tree:
The tree has a fixed-depth of 27 levels, with leaf values of 32 bytes each.
This spans the full 32-bit address space: `2**27 * 32 = 2**32`.
Each leaf contains the memory at that part of the tree.
The tree is efficiently allocated, since the root of fully zeroed sub-trees
can be computed without actually creating the full-subtree: `zero_hash[d] = hash(zero_hash[d-1], zero_hash[d-1])`,
until the base-case of `zero_hash[0] == bytes32(0)`.
Nodes in this memory tree are combined as: `out = keccak256(left ++ right)`, where `++` is concatenation,
and `left` and `right` are the 32-byte outputs of the respective sub-trees or the leaf values themselves.
Individual leaf nodes are not hashed.
The proof format is a concatenated byte array of 28 `bytes32`. The first `bytes32` is the leaf value,
and the remaining 27 `bytes32` are the sibling nodes up till the root of the tree,
along the branch of the leaf value, starting from the bottom.
To verify the proof, start with the leaf value as `node`, and combine it with respective sibling values:
`node = keccak256(node ++ sibling)` or `node = keccak256(sibling ++ node)`,
depending on the position of `node` at that level of the tree.
During the onchain execution, each instruction only executes 1 or 2 memory reads, followed by 0 or 1 writes,
where the write is over the same memory as was last read.
The memory access is specifically:
- instruction (4 byte) read at `PC`
- load or syscall mem read, always aligned 4 bytes, read at any `addr`
- store or syscall mem write, always aligned 4 bytes, at the same `addr`
Writing only once, at the last read leaf, also means that the leaf can be safely updated and the same proof-data
that was used to verify the read, can be used to reconstruct the new `memRoot` of the memory tree,
since all sibling data that is combined with the new leaf value was already authenticated during the read part.
### Pre-image data
Pre-image data is accessed through syscalls exclusively.
The OP-stack fault-proof [Pre-image Oracle specs](https://github.com/ethereum-optimism/specs/blob/main/specs/fault-proof/index.md#pre-image-oracle)
define the ABI for communicating pre-images.
This ABI is implemented by the VM by intercepting the `read`/`write` syscalls to specific file descriptors. See [Cannon VM Specs](https://github.com/ethereum-optimism/specs/blob/main/specs/fault-proof/cannon-fault-proof-vm.md#io) for more details.
The data is loaded into `PreimageOracle.sol` using the respective loading function based on the pre-image type.
And then retrieved during execution of the `read` syscall.
Note that although the oracle provides up to 32 bytes of the pre-image,
Cannon only supports reading at most 4 bytes at a time, to unify the memory operations with regular load/stores.
## Usage in Dispute Game
The onchain MIPS execution functions as base-case of an interactive dispute game.
The dispute game narrows down a sequence of disputed instructions to a single disputed instruction.
This instruction can then be executed onchain to determine objective truth.
The Dispute Game itself is out of scope for Cannon: Cannon is just one example of a fault-proof VM that
can be used to resolve disputes.
package main
import (
"context"
"errors"
"fmt"
"os"
"github.com/exchain/go-exchain/op-service/ctxinterrupt"
"github.com/urfave/cli/v2"
"github.com/exchain/go-exchain/cannon/cmd"
)
func main() {
app := cli.NewApp()
app.Name = os.Args[0]
app.Usage = "MIPS Fault Proof tool"
app.Description = "MIPS Fault Proof tool"
app.Commands = []*cli.Command{
cmd.LoadELFCommand,
cmd.WitnessCommand,
cmd.RunCommand,
}
ctx := ctxinterrupt.WithSignalWaiterMain(context.Background())
err := app.RunContext(ctx, os.Args)
if err != nil {
if errors.Is(err, ctx.Err()) {
_, _ = fmt.Fprintf(os.Stderr, "command interrupted")
os.Exit(130)
} else {
_, _ = fmt.Fprintf(os.Stderr, "error: %v", err)
os.Exit(1)
}
}
}
mipsevm
mipsevm.test
*.prof
# `mipsevm`
Supported 63 instructions:
| Category | Instruction | Description |
|----------------------|---------------|----------------------------------------------|
| `Arithmetic` | `add` | Add. |
| `Arithmetic` | `addi` | Add immediate (with sign-extension). |
| `Arithmetic` | `addiu` | Add immediate unsigned. |
| `Arithmetic` | `addu` | Add unsigned. |
| `Logical` | `and` | Bitwise AND. |
| `Logical` | `andi` | Bitwise AND immediate. |
| `Conditional Branch` | `beq` | Branch on equal. |
| `Conditional Branch` | `bgez` | Branch on greater than or equal to zero. |
| `Conditional Branch` | `bgtz` | Branch on greater than zero. |
| `Conditional Branch` | `bgezal` | Branch and link on greater than or equal to zero. |
| `Conditional Branch` | `blez` | Branch on less than or equal to zero. |
| `Conditional Branch` | `bltz` | Branch on less than zero. |
| `Conditional Branch` | `bltzal` | Branch and link on less than zero. |
| `Conditional Branch` | `bne` | Branch on not equal. |
| `Logical` | `clo` | Count leading ones. |
| `Logical` | `clz` | Count leading zeros. |
| `Arithmetic` | `div` | Divide. |
| `Arithmetic` | `divu` | Divide unsigned. |
| `Unconditional Jump` | `j` | Jump. |
| `Unconditional Jump` | `jal` | Jump and link. |
| `Unconditional Jump` | `jalr` | Jump and link register. |
| `Unconditional Jump` | `jr` | Jump register. |
| `Data Transfer` | `lb` | Load byte. |
| `Data Transfer` | `lbu` | Load byte unsigned. |
| `Data Transfer` | `lh` | Load halfword. |
| `Data Transfer` | `lhu` | Load halfword unsigned. |
| `Data Transfer` | `ll` | Load linked word. |
| `Data Transfer` | `lui` | Load upper immediate. |
| `Data Transfer` | `lw` | Load word. |
| `Data Transfer` | `lwl` | Load word left. |
| `Data Transfer` | `lwr` | Load word right. |
| `Data Transfer` | `mfhi` | Move from HI register. |
| `Data Transfer` | `mflo` | Move from LO register. |
| `Data Transfer` | `movn` | Move conditional on not zero. |
| `Data Transfer` | `movz` | Move conditional on zero. |
| `Data Transfer` | `mthi` | Move to HI register. |
| `Data Transfer` | `mtlo` | Move to LO register. |
| `Arithmetic` | `mul` | Multiply (to produce a word result). |
| `Arithmetic` | `mult` | Multiply. |
| `Arithmetic` | `multu` | Multiply unsigned. |
| `Logical` | `nor` | Bitwise NOR. |
| `Logical` | `or` | Bitwise OR. |
| `Logical` | `ori` | Bitwise OR immediate. |
| `Data Transfer` | `sb` | Store byte. |
| `Data Transfer` | `sc` | Store conditional. |
| `Data Transfer` | `sh` | Store halfword. |
| `Logical` | `sll` | Shift left logical. |
| `Logical` | `sllv` | Shift left logical variable. |
| `Comparison` | `slt` | Set on less than (signed). |
| `Comparison` | `slti` | Set on less than immediate. |
| `Comparison` | `sltiu` | Set on less than immediate unsigned. |
| `Comparison` | `sltu` | Set on less than unsigned. |
| `Logical` | `sra` | Shift right arithmetic. |
| `Logical` | `srav` | Shift right arithmetic variable. |
| `Logical` | `srl` | Shift right logical. |
| `Logical` | `srlv` | Shift right logical variable. |
| `Arithmetic` | `sub` | Subtract. |
| `Arithmetic` | `subu` | Subtract unsigned. |
| `Data Transfer` | `sw` | Store word. |
| `Data Transfer` | `swl` | Store word left. |
| `Data Transfer` | `swr` | Store word right. |
| `Serialization` | `sync` | Synchronize shared memory. |
| `System Calls` | `syscall` | System call. |
| `Logical` | `xor` | Bitwise XOR. |
| `Logical` | `xori` | Bitwise XOR immediate. |
To run:
1. Load a program into a state, e.g. using `LoadELF`.
2. Patch the program if necessary: e.g. using `PatchGo` for Go programs, `PatchStack` for empty initial stack, etc.
4. Implement the `PreimageOracle` interface
5. Instrument the emulator with the state, and pre-image oracle, using `NewInstrumentedState`
6. Step through the instrumented state with `Step(proof)`,
where `proof==true` if witness data should be generated. Steps are faster with `proof==false`.
7. Optionally repeat the step on-chain by calling `MIPS.sol` and `PreimageOracle.sol`, using the above witness data.
package arch
// This file contains stuff common to both arch32 and arch64
const (
IsMips32 = WordSize == 32
WordSizeBytes = WordSize >> 3
PageAddrSize = 12
PageKeySize = WordSize - PageAddrSize
MemProofLeafCount = WordSize - 4
MemProofSize = MemProofLeafCount * 32
)
//go:build !cannon64
// +build !cannon64
package arch
import "encoding/binary"
type (
// Word differs from the tradditional meaning in MIPS. The type represents the *maximum* architecture specific access length and value sizes.
Word = uint32
// SignedInteger specifies the maximum signed integer type used for arithmetic.
SignedInteger = int32
)
const (
AddressMask = 0xFFffFFfc
WordSize = 32
ExtMask = 0x3
HeapStart = 0x05_00_00_00
HeapEnd = 0x60_00_00_00
ProgramBreak = 0x40_00_00_00
HighMemoryStart = 0x7f_ff_d0_00
)
// 32-bit Syscall codes
const (
SysMmap = 4090
SysBrk = 4045
SysClone = 4120
SysExitGroup = 4246
SysRead = 4003
SysWrite = 4004
SysFcntl = 4055
SysExit = 4001
SysSchedYield = 4162
SysGetTID = 4222
SysFutex = 4238
SysOpen = 4005
SysNanosleep = 4166
SysClockGetTime = 4263
SysGetpid = 4020
)
// Noop Syscall codes
const (
SysMunmap = 4091
SysGetAffinity = 4240
SysMadvise = 4218
SysRtSigprocmask = 4195
SysSigaltstack = 4206
SysRtSigaction = 4194
SysPrlimit64 = 4338
SysClose = 4006
SysPread64 = 4200
SysStat = 4106
SysFstat = 4108
SysFstat64 = 4215
SysOpenAt = 4288
SysReadlink = 4085
SysReadlinkAt = 4298
SysIoctl = 4054
SysEpollCreate1 = 4326
SysPipe2 = 4328
SysEpollCtl = 4249
SysEpollPwait = 4313
SysGetRandom = 4353
SysUname = 4122
SysStat64 = 4213
SysGetuid = 4024
SysGetgid = 4047
SysLlseek = 4140
SysMinCore = 4217
SysTgkill = 4266
SysGetRLimit = 4076
SysLseek = 4019
// Profiling-related syscalls
SysSetITimer = 4104
SysTimerCreate = 4257
SysTimerSetTime = 4258
SysTimerDelete = 4261
)
var ByteOrderWord = byteOrder32{}
type byteOrder32 struct{}
func (bo byteOrder32) Word(b []byte) Word {
return binary.BigEndian.Uint32(b)
}
func (bo byteOrder32) AppendWord(b []byte, v uint32) []byte {
return binary.BigEndian.AppendUint32(b, v)
}
func (bo byteOrder32) PutWord(b []byte, v uint32) {
binary.BigEndian.PutUint32(b, v)
}
//go:build cannon64
// +build cannon64
package arch
import "encoding/binary"
type (
// Word differs from the traditional meaning in MIPS. The type represents the *maximum* architecture specific access length and value sizes
Word = uint64
// SignedInteger specifies the maximum signed integer type used for arithmetic.
SignedInteger = int64
)
const (
AddressMask = 0xFFFFFFFFFFFFFFF8
WordSize = 64
ExtMask = 0x7
// Ensure virtual address is limited to 48-bits as many user programs assume such to implement packed pointers
// limit 0x00_00_FF_FF_FF_FF_FF_FF
HeapStart = 0x00_00_10_00_00_00_00_00
HeapEnd = 0x00_00_60_00_00_00_00_00
ProgramBreak = 0x00_00_40_00_00_00_00_00
HighMemoryStart = 0x00_00_7F_FF_FF_FF_F0_00
)
// MIPS64 syscall table - https://github.com/torvalds/linux/blob/3efc57369a0ce8f76bf0804f7e673982384e4ac9/arch/mips/kernel/syscalls/syscall_n64.tbl. Generate the syscall numbers using the Makefile in that directory.
// See https://gpages.juszkiewicz.com.pl/syscalls-table/syscalls.html for the generated syscalls
// 64-bit Syscall numbers - new
const (
SysMmap = 5009
SysBrk = 5012
SysClone = 5055
SysExitGroup = 5205
SysRead = 5000
SysWrite = 5001
SysFcntl = 5070
SysExit = 5058
SysSchedYield = 5023
SysGetTID = 5178
SysFutex = 5194
SysOpen = 5002
SysNanosleep = 5034
SysClockGetTime = 5222
SysGetpid = 5038
)
// Noop Syscall numbers
const (
// UndefinedSysNr is the value used for 32-bit syscall numbers that aren't supported for 64-bits
UndefinedSysNr = ^Word(0)
SysMunmap = 5011
SysGetAffinity = 5196
SysMadvise = 5027
SysRtSigprocmask = 5014
SysSigaltstack = 5129
SysRtSigaction = 5013
SysPrlimit64 = 5297
SysClose = 5003
SysPread64 = 5016
SysStat = 5004
SysFstat = 5005
SysFstat64 = UndefinedSysNr
SysOpenAt = 5247
SysReadlink = 5087
SysReadlinkAt = 5257
SysIoctl = 5015
SysEpollCreate1 = 5285
SysPipe2 = 5287
SysEpollCtl = 5208
SysEpollPwait = 5272
SysGetRandom = 5313
SysUname = 5061
SysStat64 = UndefinedSysNr
SysGetuid = 5100
SysGetgid = 5102
SysLlseek = UndefinedSysNr
SysMinCore = 5026
SysTgkill = 5225
SysGetRLimit = 5095
SysLseek = 5008
// Profiling-related syscalls
SysSetITimer = 5036
SysTimerCreate = 5216
SysTimerSetTime = 5217
SysTimerDelete = 5220
)
var ByteOrderWord = byteOrder64{}
type byteOrder64 struct{}
func (bo byteOrder64) Word(b []byte) Word {
return binary.BigEndian.Uint64(b)
}
func (bo byteOrder64) AppendWord(b []byte, v uint64) []byte {
return binary.BigEndian.AppendUint64(b, v)
}
func (bo byteOrder64) PutWord(b []byte, v uint64) {
binary.BigEndian.PutUint64(b, v)
}
package arch
type ByteOrder interface {
Word([]byte) Word
AppendWord([]byte, Word) []byte
PutWord([]byte, Word)
}
package mipsevm
import "github.com/ethereum/go-ethereum/common/hexutil"
type DebugInfo struct {
Pages int `json:"pages"`
MemoryUsed hexutil.Uint64 `json:"memory_used"`
NumPreimageRequests int `json:"num_preimage_requests"`
TotalPreimageSize int `json:"total_preimage_size"`
TotalSteps uint64 `json:"total_steps"`
// Multithreading-related stats below
RmwSuccessCount uint64 `json:"rmw_success_count"`
RmwFailCount uint64 `json:"rmw_fail_count"`
MaxStepsBetweenLLAndSC uint64 `json:"max_steps_between_ll_and_sc"`
ReservationInvalidationCount uint64 `json:"reservation_invalidation_count"`
ForcedPreemptionCount uint64 `json:"forced_preemption_count"`
IdleStepCountThread0 uint64 `json:"idle_step_count_thread_0"`
}
package mipsevm
import (
"math"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/exchain/go-exchain/op-service/ioutil"
"github.com/exchain/go-exchain/op-service/jsonutil"
)
func TestDebugInfo_Serialization(t *testing.T) {
debugInfo := &DebugInfo{
Pages: 1,
MemoryUsed: 2,
NumPreimageRequests: 3,
TotalPreimageSize: 4,
TotalSteps: 123456,
RmwSuccessCount: 5,
RmwFailCount: 6,
MaxStepsBetweenLLAndSC: 7,
ReservationInvalidationCount: 8,
ForcedPreemptionCount: 9,
IdleStepCountThread0: math.MaxUint64,
}
// Serialize to file
dir := t.TempDir()
path := filepath.Join(dir, "debug-info-test.txt")
err := jsonutil.WriteJSON(debugInfo, ioutil.ToAtomicFile(path, 0o644))
require.NoError(t, err)
// Deserialize
fromJson, err := jsonutil.LoadJSON[DebugInfo](path)
require.NoError(t, err)
require.Equal(t, debugInfo, fromJson)
}
package exec
import (
"fmt"
"github.com/exchain/go-exchain/cannon/mipsevm/arch"
"github.com/exchain/go-exchain/cannon/mipsevm/memory"
)
type MemTracker interface {
TrackMemAccess(addr Word)
}
type MemoryTrackerImpl struct {
memory *memory.Memory
lastMemAccess Word
memProofEnabled bool
// proof of first unique memory access
memProof [memory.MemProofSize]byte
// proof of second unique memory access
memProof2 [memory.MemProofSize]byte
}
func NewMemoryTracker(memory *memory.Memory) *MemoryTrackerImpl {
return &MemoryTrackerImpl{memory: memory}
}
func (m *MemoryTrackerImpl) TrackMemAccess(effAddr Word) {
if m.memProofEnabled && m.lastMemAccess != effAddr {
if m.lastMemAccess != ^Word(0) {
panic(fmt.Errorf("unexpected different mem access at %08x, already have access at %08x buffered", effAddr, m.lastMemAccess))
}
m.lastMemAccess = effAddr
m.memProof = m.memory.MerkleProof(effAddr)
}
}
// TrackMemAccess2 creates a proof for a memory access following a call to TrackMemAccess
// This is used to generate proofs for contiguous memory accesses within the same step
func (m *MemoryTrackerImpl) TrackMemAccess2(effAddr Word) {
if m.memProofEnabled && m.lastMemAccess+arch.WordSizeBytes != effAddr {
panic(fmt.Errorf("unexpected disjointed mem access at %08x, last memory access is at %08x buffered", effAddr, m.lastMemAccess))
}
m.lastMemAccess = effAddr
m.memProof2 = m.memory.MerkleProof(effAddr)
}
func (m *MemoryTrackerImpl) Reset(enableProof bool) {
m.memProofEnabled = enableProof
m.lastMemAccess = ^Word(0)
}
func (m *MemoryTrackerImpl) MemProof() [memory.MemProofSize]byte {
return m.memProof
}
func (m *MemoryTrackerImpl) MemProof2() [memory.MemProofSize]byte {
return m.memProof2
}
type NoopMemoryTracker struct{}
func (n *NoopMemoryTracker) TrackMemAccess(Word) {}
This diff is collapsed.
// These tests target architectures that are 32-bit or larger
package exec
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/exchain/go-exchain/cannon/mipsevm/arch"
"github.com/exchain/go-exchain/cannon/mipsevm/memory"
)
// TestLoadSubWord_32bits validates LoadSubWord with 32-bit offsets (up to 3 bytes)
func TestLoadSubWord_32bits(t *testing.T) {
cases := []struct {
name string
byteLength Word
addr uint32
memVal uint32
signExtend bool
shouldSignExtend bool
expectedValue uint32
}{
{name: "32-bit", byteLength: 4, addr: 0xFF00_0000, memVal: 0x1234_5678, expectedValue: 0x1234_5678},
{name: "32-bit, extra bits", byteLength: 4, addr: 0xFF00_0001, memVal: 0x1234_5678, expectedValue: 0x1234_5678},
{name: "32-bit, extra bits", byteLength: 4, addr: 0xFF00_0002, memVal: 0x1234_5678, expectedValue: 0x1234_5678},
{name: "32-bit, extra bits", byteLength: 4, addr: 0xFF00_0003, memVal: 0x1234_5678, expectedValue: 0x1234_5678},
{name: "16-bit, offset=0", byteLength: 2, addr: 0x00, memVal: 0x1234_5678, expectedValue: 0x1234},
{name: "16-bit, offset=0, extra bit set", byteLength: 2, addr: 0x01, memVal: 0x1234_5678, expectedValue: 0x1234},
{name: "16-bit, offset=2", byteLength: 2, addr: 0x02, memVal: 0x1234_5678, expectedValue: 0x5678},
{name: "16-bit, offset=2, extra bit set", byteLength: 2, addr: 0x03, memVal: 0x1234_5678, expectedValue: 0x5678},
{name: "16-bit, sign extend positive val", byteLength: 2, addr: 0x02, memVal: 0x1234_5678, expectedValue: 0x5678, signExtend: true, shouldSignExtend: false},
{name: "16-bit, sign extend negative val", byteLength: 2, addr: 0x02, memVal: 0x1234_F678, expectedValue: 0xFFFF_F678, signExtend: true, shouldSignExtend: true},
{name: "16-bit, do not sign extend negative val", byteLength: 2, addr: 0x02, memVal: 0x1234_F678, expectedValue: 0xF678, signExtend: false},
{name: "8-bit, offset=0", byteLength: 1, addr: 0x1230, memVal: 0x1234_5678, expectedValue: 0x12},
{name: "8-bit, offset=1", byteLength: 1, addr: 0x1231, memVal: 0x1234_5678, expectedValue: 0x34},
{name: "8-bit, offset=2", byteLength: 1, addr: 0x1232, memVal: 0x1234_5678, expectedValue: 0x56},
{name: "8-bit, offset=3", byteLength: 1, addr: 0x1233, memVal: 0x1234_5678, expectedValue: 0x78},
{name: "8-bit, sign extend positive", byteLength: 1, addr: 0x1233, memVal: 0x1234_5678, expectedValue: 0x78, signExtend: true, shouldSignExtend: false},
{name: "8-bit, sign extend negative", byteLength: 1, addr: 0x1233, memVal: 0x1234_5688, expectedValue: 0xFFFF_FF88, signExtend: true, shouldSignExtend: true},
{name: "8-bit, do not sign extend neg value", byteLength: 1, addr: 0x1233, memVal: 0x1234_5688, expectedValue: 0x88, signExtend: false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
mem := memory.NewMemory()
memTracker := NewMemoryTracker(mem)
effAddr := Word(c.addr) & arch.AddressMask
// Shift memval for consistency across architectures
memVal := Word(c.memVal) << (arch.WordSize - 32)
mem.SetWord(effAddr, memVal)
retVal := LoadSubWord(mem, Word(c.addr), c.byteLength, c.signExtend, memTracker)
// If sign extending, make sure retVal is consistent across architectures
expected := Word(c.expectedValue)
if c.shouldSignExtend {
signedBits := ^Word(0xFFFF_FFFF)
expected = expected | signedBits
}
require.Equal(t, expected, retVal)
})
}
}
// TestStoreSubWord_32bits validates LoadSubWord with 32-bit offsets (up to 3 bytes)
func TestStoreSubWord_32bits(t *testing.T) {
memVal := 0xFFFF_FFFF
value := 0x1234_5678
cases := []struct {
name string
byteLength Word
addr uint32
expectedValue uint32
}{
{name: "32-bit", byteLength: 4, addr: 0xFF00_0000, expectedValue: 0x1234_5678},
{name: "32-bit, extra bits", byteLength: 4, addr: 0xFF00_0001, expectedValue: 0x1234_5678},
{name: "32-bit, extra bits", byteLength: 4, addr: 0xFF00_0002, expectedValue: 0x1234_5678},
{name: "32-bit, extra bits", byteLength: 4, addr: 0xFF00_0003, expectedValue: 0x1234_5678},
{name: "16-bit, subword offset=0", byteLength: 2, addr: 0x00, expectedValue: 0x5678_FFFF},
{name: "16-bit, subword offset=0, extra bit set", byteLength: 2, addr: 0x01, expectedValue: 0x5678_FFFF},
{name: "16-bit, subword offset=2", byteLength: 2, addr: 0x02, expectedValue: 0xFFFF_5678},
{name: "16-bit, subword offset=2, extra bit set", byteLength: 2, addr: 0x03, expectedValue: 0xFFFF_5678},
{name: "8-bit, offset=0", byteLength: 1, addr: 0x1230, expectedValue: 0x78FF_FFFF},
{name: "8-bit, offset=1", byteLength: 1, addr: 0x1231, expectedValue: 0xFF78_FFFF},
{name: "8-bit, offset=2", byteLength: 1, addr: 0x1232, expectedValue: 0xFFFF_78FF},
{name: "8-bit, offset=3", byteLength: 1, addr: 0x1233, expectedValue: 0xFFFF_FF78},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
mem := memory.NewMemory()
memTracker := NewMemoryTracker(mem)
effAddr := Word(c.addr) & arch.AddressMask
// Shift memval for consistency across architectures
memVal := Word(memVal) << (arch.WordSize - 32)
mem.SetWord(effAddr, memVal)
StoreSubWord(mem, Word(c.addr), c.byteLength, Word(value), memTracker)
newMemVal := mem.GetWord(effAddr)
// Make sure expectation is consistent across architectures
expected := Word(c.expectedValue) << (arch.WordSize - 32)
require.Equal(t, expected, newMemVal)
})
}
}
//go:build cannon64
// +build cannon64
// These tests target architectures that are 64-bit or larger
package exec
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/exchain/go-exchain/cannon/mipsevm/arch"
"github.com/exchain/go-exchain/cannon/mipsevm/memory"
)
// TestLoadSubWord_64bits extends TestLoadSubWord_32bits by testing up to 64-bits (7 byte) offsets
func TestLoadSubWord_64bits(t *testing.T) {
memVal := uint64(0x1234_5678_9876_5432)
cases := []struct {
name string
byteLength Word
addr uint64
memVal uint64
signExtend bool
expectedValue uint64
}{
{name: "64-bit", byteLength: 8, addr: 0xFF00_0000, memVal: 0x8234_5678_9876_5432, expectedValue: 0x8234_5678_9876_5432},
{name: "64-bit w sign extension", byteLength: 8, addr: 0xFF00_0000, memVal: 0x8234_5678_9876_5432, expectedValue: 0x8234_5678_9876_5432, signExtend: true},
{name: "32-bit, offset=0", byteLength: 4, addr: 0xFF00_0000, memVal: memVal, expectedValue: 0x1234_5678},
{name: "32-bit, offset=0, extra bits", byteLength: 4, addr: 0xFF00_0001, memVal: memVal, expectedValue: 0x1234_5678},
{name: "32-bit, offset=0, extra bits", byteLength: 4, addr: 0xFF00_0002, memVal: memVal, expectedValue: 0x1234_5678},
{name: "32-bit, offset=0, extra bits", byteLength: 4, addr: 0xFF00_0003, memVal: memVal, expectedValue: 0x1234_5678},
{name: "32-bit, offset=4", byteLength: 4, addr: 0xFF00_0004, memVal: memVal, expectedValue: 0x9876_5432},
{name: "32-bit, offset=4, extra bits", byteLength: 4, addr: 0xFF00_0005, memVal: memVal, expectedValue: 0x9876_5432},
{name: "32-bit, offset=4, extra bits", byteLength: 4, addr: 0xFF00_0006, memVal: memVal, expectedValue: 0x9876_5432},
{name: "32-bit, offset=4, extra bits", byteLength: 4, addr: 0xFF00_0007, memVal: memVal, expectedValue: 0x9876_5432},
{name: "32-bit, sign extend negative", byteLength: 4, addr: 0xFF00_0006, memVal: 0x1234_5678_F1E2_A1B1, expectedValue: 0xFFFF_FFFF_F1E2_A1B1, signExtend: true},
{name: "32-bit, sign extend positive", byteLength: 4, addr: 0xFF00_0007, memVal: 0x1234_5678_7876_5432, expectedValue: 0x7876_5432, signExtend: true},
{name: "16-bit, subword offset=4", byteLength: 2, addr: 0x04, memVal: memVal, expectedValue: 0x9876},
{name: "16-bit, subword offset=4, extra bit set", byteLength: 2, addr: 0x05, memVal: memVal, expectedValue: 0x9876},
{name: "16-bit, subword offset=6", byteLength: 2, addr: 0x06, memVal: memVal, expectedValue: 0x5432},
{name: "16-bit, subword offset=6, extra bit set", byteLength: 2, addr: 0x07, memVal: memVal, expectedValue: 0x5432},
{name: "16-bit, sign extend negative val", byteLength: 2, addr: 0x04, memVal: 0x1234_5678_8BEE_CCDD, expectedValue: 0xFFFF_FFFF_FFFF_8BEE, signExtend: true},
{name: "16-bit, sign extend positive val", byteLength: 2, addr: 0x04, memVal: 0x1234_5678_7876_5432, expectedValue: 0x7876, signExtend: true},
{name: "8-bit, offset=4", byteLength: 1, addr: 0x1234, memVal: memVal, expectedValue: 0x98},
{name: "8-bit, offset=5", byteLength: 1, addr: 0x1235, memVal: memVal, expectedValue: 0x76},
{name: "8-bit, offset=6", byteLength: 1, addr: 0x1236, memVal: memVal, expectedValue: 0x54},
{name: "8-bit, offset=7", byteLength: 1, addr: 0x1237, memVal: memVal, expectedValue: 0x32},
{name: "8-bit, sign extend positive", byteLength: 1, addr: 0x1237, memVal: memVal, expectedValue: 0x32, signExtend: true},
{name: "8-bit, sign extend negative", byteLength: 1, addr: 0x1237, memVal: 0x1234_5678_8764_4381, expectedValue: 0xFFFF_FFFF_FFFF_FF81, signExtend: true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
mem := memory.NewMemory()
memTracker := NewMemoryTracker(mem)
effAddr := Word(c.addr) & arch.AddressMask
mem.SetWord(effAddr, c.memVal)
retVal := LoadSubWord(mem, Word(c.addr), c.byteLength, c.signExtend, memTracker)
require.Equal(t, c.expectedValue, retVal)
})
}
}
// TestStoreSubWord_64bits extends TestStoreSubWord_32bits by testing up to 64-bits (7 byte) offsets
func TestStoreSubWord_64bits(t *testing.T) {
memVal := uint64(0xFFFF_FFFF_FFFF_FFFF)
value := uint64(0x1234_5678_9876_5432)
cases := []struct {
name string
byteLength Word
addr uint64
expectedValue uint64
}{
{name: "64-bit", byteLength: 8, addr: 0xFF00_0000, expectedValue: value},
{name: "32-bit, offset 0", byteLength: 4, addr: 0xFF00_0000, expectedValue: 0x9876_5432_FFFF_FFFF},
{name: "32-bit, offset 0, extra addr bits", byteLength: 4, addr: 0xFF00_0001, expectedValue: 0x9876_5432_FFFF_FFFF},
{name: "32-bit, offset 0, extra addr bits", byteLength: 4, addr: 0xFF00_0002, expectedValue: 0x9876_5432_FFFF_FFFF},
{name: "32-bit, offset 0, extra addr bits", byteLength: 4, addr: 0xFF00_0003, expectedValue: 0x9876_5432_FFFF_FFFF},
{name: "32-bit, offset 4", byteLength: 4, addr: 0xFF00_0004, expectedValue: 0xFFFF_FFFF_9876_5432},
{name: "32-bit, offset 4, extra addr bits", byteLength: 4, addr: 0xFF00_0005, expectedValue: 0xFFFF_FFFF_9876_5432},
{name: "32-bit, offset 4, extra addr bits", byteLength: 4, addr: 0xFF00_0006, expectedValue: 0xFFFF_FFFF_9876_5432},
{name: "32-bit, offset 4, extra addr bits", byteLength: 4, addr: 0xFF00_0007, expectedValue: 0xFFFF_FFFF_9876_5432},
{name: "16-bit, offset=4", byteLength: 2, addr: 0x04, expectedValue: 0xFFFF_FFFF_5432_FFFF},
{name: "16-bit, offset=4, extra bit set", byteLength: 2, addr: 0x05, expectedValue: 0xFFFF_FFFF_5432_FFFF},
{name: "16-bit, offset=6", byteLength: 2, addr: 0x06, expectedValue: 0xFFFF_FFFF_FFFF_5432},
{name: "16-bit, offset=6, extra bit set", byteLength: 2, addr: 0x07, expectedValue: 0xFFFF_FFFF_FFFF_5432},
{name: "8-bit, offset=4", byteLength: 1, addr: 0x1234, expectedValue: 0xFFFF_FFFF_32FF_FFFF},
{name: "8-bit, offset=5", byteLength: 1, addr: 0x1235, expectedValue: 0xFFFF_FFFF_FF32_FFFF},
{name: "8-bit, offset=6", byteLength: 1, addr: 0x1236, expectedValue: 0xFFFF_FFFF_FFFF_32FF},
{name: "8-bit, offset=7", byteLength: 1, addr: 0x1237, expectedValue: 0xFFFF_FFFF_FFFF_FF32},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
mem := memory.NewMemory()
memTracker := NewMemoryTracker(mem)
effAddr := Word(c.addr) & arch.AddressMask
mem.SetWord(effAddr, memVal)
StoreSubWord(mem, Word(c.addr), c.byteLength, Word(value), memTracker)
newMemVal := mem.GetWord(effAddr)
require.Equal(t, c.expectedValue, newMemVal)
})
}
}
package exec
import (
"encoding/binary"
"io"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/exchain/go-exchain/cannon/mipsevm"
"github.com/exchain/go-exchain/cannon/mipsevm/arch"
"github.com/exchain/go-exchain/cannon/mipsevm/memory"
"github.com/exchain/go-exchain/cannon/mipsevm/program"
"github.com/exchain/go-exchain/cannon/mipsevm/register"
)
type Word = arch.Word
const (
AddressMask = arch.AddressMask
)
// File descriptors
const (
FdStdin = 0
FdStdout = 1
FdStderr = 2
FdHintRead = 3
FdHintWrite = 4
FdPreimageRead = 5
FdPreimageWrite = 6
)
// Errors
const (
SysErrorSignal = ^Word(0)
MipsEBADF = 0x9
MipsEINVAL = 0x16
MipsEAGAIN = 0xb
MipsETIMEDOUT = 0x91
)
// SysFutex-related constants
const (
FutexWaitPrivate = 128
FutexWakePrivate = 129
)
// SysClone flags
// Handling is meant to support go runtime use cases
// Pulled from: https://github.com/golang/go/blob/go1.21.3/src/runtime/os_linux.go#L124-L158
const (
CloneVm = 0x100
CloneFs = 0x200
CloneFiles = 0x400
CloneSighand = 0x800
ClonePtrace = 0x2000
CloneVfork = 0x4000
CloneParent = 0x8000
CloneThread = 0x10000
CloneNewns = 0x20000
CloneSysvsem = 0x40000
CloneSettls = 0x80000
CloneParentSettid = 0x100000
CloneChildCleartid = 0x200000
CloneUntraced = 0x800000
CloneChildSettid = 0x1000000
CloneStopped = 0x2000000
CloneNewuts = 0x4000000
CloneNewipc = 0x8000000
ValidCloneFlags = CloneVm |
CloneFs |
CloneFiles |
CloneSighand |
CloneSysvsem |
CloneThread
)
// Other constants
const (
// SchedQuantum is the number of steps dedicated for a thread before it's preempted. Effectively used to emulate thread "time slices"
SchedQuantum = 100_000
// HZ is the assumed clock rate of an emulated MIPS32 CPU.
// The value of HZ is a rough estimate of the Cannon instruction count / second on a typical machine.
// HZ is used to emulate the clock_gettime syscall used by guest programs that have a Go runtime.
// The Go runtime consumes the system time to determine when to initiate gc assists and for goroutine scheduling.
// A HZ value that is too low (i.e. lower than the emulation speed) results in the main goroutine attempting to assist with GC more often.
// Adjust this value accordingly as the emulation speed changes. The HZ value should be within the same order of magnitude as the emulation speed.
HZ = 10_000_000
// ClockGettimeRealtimeFlag is the clock_gettime clock id for Linux's realtime clock: https://github.com/torvalds/linux/blob/ad618736883b8970f66af799e34007475fe33a68/include/uapi/linux/time.h#L49
ClockGettimeRealtimeFlag = 0
// ClockGettimeMonotonicFlag is the clock_gettime clock id for Linux's monotonic clock: https://github.com/torvalds/linux/blob/ad618736883b8970f66af799e34007475fe33a68/include/uapi/linux/time.h#L50
ClockGettimeMonotonicFlag = 1
)
func GetSyscallArgs(registers *[32]Word) (syscallNum, a0, a1, a2 Word) {
syscallNum = registers[register.RegSyscallNum] // v0
a0 = registers[register.RegSyscallParam1]
a1 = registers[register.RegSyscallParam2]
a2 = registers[register.RegSyscallParam3]
return syscallNum, a0, a1, a2
}
func HandleSysMmap(a0, a1, heap Word) (v0, v1, newHeap Word) {
v1 = Word(0)
newHeap = heap
sz := a1
if sz&memory.PageAddrMask != 0 { // adjust size to align with page size
sz += memory.PageSize - (sz & memory.PageAddrMask)
}
if a0 == 0 {
v0 = heap
//fmt.Printf("mmap heap 0x%x size 0x%x\n", v0, sz)
newHeap += sz
// Fail if new heap exceeds memory limit, newHeap overflows around to low memory, or sz overflows
if newHeap > program.HEAP_END || newHeap < heap || sz < a1 {
v0 = SysErrorSignal
v1 = MipsEINVAL
return v0, v1, heap
}
} else {
v0 = a0
//fmt.Printf("mmap hint 0x%x size 0x%x\n", v0, sz)
}
return v0, v1, newHeap
}
func HandleSysRead(
a0, a1, a2 Word,
preimageKey [32]byte,
preimageOffset Word,
preimageReader PreimageReader,
memory *memory.Memory,
memTracker MemTracker,
) (v0, v1, newPreimageOffset Word, memUpdated bool, memAddr Word) {
// args: a0 = fd, a1 = addr, a2 = count
// returns: v0 = read, v1 = err code
v0 = Word(0)
v1 = Word(0)
newPreimageOffset = preimageOffset
switch a0 {
case FdStdin:
// leave v0 and v1 zero: read nothing, no error
case FdPreimageRead: // pre-image oracle
effAddr := a1 & AddressMask
memTracker.TrackMemAccess(effAddr)
mem := memory.GetWord(effAddr)
dat, datLen := preimageReader.ReadPreimage(preimageKey, preimageOffset)
//fmt.Printf("reading pre-image data: addr: %08x, offset: %d, datLen: %d, data: %x, key: %s count: %d\n", a1, preimageOffset, datLen, dat[:datLen], preimageKey, a2)
alignment := a1 & arch.ExtMask
space := arch.WordSizeBytes - alignment
if space < datLen {
datLen = space
}
if a2 < datLen {
datLen = a2
}
var outMem [arch.WordSizeBytes]byte
arch.ByteOrderWord.PutWord(outMem[:], mem)
copy(outMem[alignment:], dat[:datLen])
memory.SetWord(effAddr, arch.ByteOrderWord.Word(outMem[:]))
memUpdated = true
memAddr = effAddr
newPreimageOffset += datLen
v0 = datLen
//fmt.Printf("read %d pre-image bytes, new offset: %d, eff addr: %08x mem: %08x\n", datLen, m.state.PreimageOffset, effAddr, outMem)
case FdHintRead: // hint response
// don't actually read into memory, just say we read it all, we ignore the result anyway
v0 = a2
default:
v0 = ^Word(0)
v1 = MipsEBADF
}
return v0, v1, newPreimageOffset, memUpdated, memAddr
}
func HandleSysWrite(a0, a1, a2 Word,
lastHint hexutil.Bytes,
preimageKey [32]byte,
preimageOffset Word,
oracle mipsevm.PreimageOracle,
memory *memory.Memory,
memTracker MemTracker,
stdOut, stdErr io.Writer,
) (v0, v1 Word, newLastHint hexutil.Bytes, newPreimageKey common.Hash, newPreimageOffset Word) {
// args: a0 = fd, a1 = addr, a2 = count
// returns: v0 = written, v1 = err code
v1 = Word(0)
newLastHint = lastHint
newPreimageKey = preimageKey
newPreimageOffset = preimageOffset
switch a0 {
case FdStdout:
_, _ = io.Copy(stdOut, memory.ReadMemoryRange(a1, a2))
v0 = a2
case FdStderr:
_, _ = io.Copy(stdErr, memory.ReadMemoryRange(a1, a2))
v0 = a2
case FdHintWrite:
hintData, _ := io.ReadAll(memory.ReadMemoryRange(a1, a2))
lastHint = append(lastHint, hintData...)
for len(lastHint) >= 4 { // process while there is enough data to check if there are any hints
hintLen := binary.BigEndian.Uint32(lastHint[:4])
if hintLen <= uint32(len(lastHint[4:])) {
hint := lastHint[4 : 4+hintLen] // without the length prefix
lastHint = lastHint[4+hintLen:]
oracle.Hint(hint)
} else {
break // stop processing hints if there is incomplete data buffered
}
}
newLastHint = lastHint
v0 = a2
case FdPreimageWrite:
effAddr := a1 & arch.AddressMask
memTracker.TrackMemAccess(effAddr)
mem := memory.GetWord(effAddr)
key := preimageKey
alignment := a1 & arch.ExtMask
space := arch.WordSizeBytes - alignment
if space < a2 {
a2 = space
}
copy(key[:], key[a2:])
var tmp [arch.WordSizeBytes]byte
arch.ByteOrderWord.PutWord(tmp[:], mem)
copy(key[32-a2:], tmp[alignment:])
newPreimageKey = key
newPreimageOffset = 0
//fmt.Printf("updating pre-image key: %s\n", m.state.PreimageKey)
v0 = a2
default:
v0 = ^Word(0)
v1 = MipsEBADF
}
return v0, v1, newLastHint, newPreimageKey, newPreimageOffset
}
func HandleSysFcntl(a0, a1 Word) (v0, v1 Word) {
// args: a0 = fd, a1 = cmd
v1 = Word(0)
if a1 == 1 { // F_GETFD: get file descriptor flags
switch a0 {
case FdStdin, FdStdout, FdStderr, FdPreimageRead, FdHintRead, FdPreimageWrite, FdHintWrite:
v0 = 0 // No flags set
default:
v0 = ^Word(0)
v1 = MipsEBADF
}
} else if a1 == 3 { // F_GETFL: get file status flags
switch a0 {
case FdStdin, FdPreimageRead, FdHintRead:
v0 = 0 // O_RDONLY
case FdStdout, FdStderr, FdPreimageWrite, FdHintWrite:
v0 = 1 // O_WRONLY
default:
v0 = ^Word(0)
v1 = MipsEBADF
}
} else {
v0 = ^Word(0)
v1 = MipsEINVAL // cmd not recognized by this kernel
}
return v0, v1
}
func HandleSyscallUpdates(cpu *mipsevm.CpuScalars, registers *[32]Word, v0, v1 Word) {
registers[register.RegSyscallRet1] = v0
registers[register.RegSyscallErrno] = v1
cpu.PC = cpu.NextPC
cpu.NextPC = cpu.NextPC + 4
}
package exec
import (
"encoding/binary"
"github.com/exchain/go-exchain/cannon/mipsevm"
)
type PreimageReader interface {
ReadPreimage(key [32]byte, offset Word) (dat [32]byte, datLen Word)
}
// TrackingPreimageOracleReader wraps around a PreimageOracle, implements the PreimageOracle interface, and adds tracking functionality.
// It also implements the PreimageReader interface
type TrackingPreimageOracleReader struct {
po mipsevm.PreimageOracle
totalPreimageSize int
numPreimageRequests int
// cached pre-image data, including 8 byte length prefix
lastPreimage []byte
// key for above preimage
lastPreimageKey [32]byte
// offset we last read from, or max Word if nothing is read this step
lastPreimageOffset Word
}
func NewTrackingPreimageOracleReader(po mipsevm.PreimageOracle) *TrackingPreimageOracleReader {
return &TrackingPreimageOracleReader{po: po}
}
func (p *TrackingPreimageOracleReader) Reset() {
p.lastPreimageOffset = ^Word(0)
}
func (p *TrackingPreimageOracleReader) Hint(v []byte) {
p.po.Hint(v)
}
func (p *TrackingPreimageOracleReader) GetPreimage(k [32]byte) []byte {
p.numPreimageRequests++
preimage := p.po.GetPreimage(k)
p.totalPreimageSize += len(preimage)
return preimage
}
func (p *TrackingPreimageOracleReader) ReadPreimage(key [32]byte, offset Word) (dat [32]byte, datLen Word) {
preimage := p.lastPreimage
if key != p.lastPreimageKey {
p.lastPreimageKey = key
data := p.GetPreimage(key)
// add the length prefix
preimage = make([]byte, 0, 8+len(data))
preimage = binary.BigEndian.AppendUint64(preimage, uint64(len(data)))
preimage = append(preimage, data...)
p.lastPreimage = preimage
}
p.lastPreimageOffset = offset
if offset >= Word(len(preimage)) {
panic("Preimage offset out-of-bounds")
}
datLen = Word(copy(dat[:], preimage[offset:]))
return
}
func (p *TrackingPreimageOracleReader) LastPreimage() ([32]byte, []byte, Word) {
return p.lastPreimageKey, p.lastPreimage, p.lastPreimageOffset
}
func (p *TrackingPreimageOracleReader) TotalPreimageSize() int {
return p.totalPreimageSize
}
func (p *TrackingPreimageOracleReader) NumPreimageRequests() int {
return p.numPreimageRequests
}
package exec
import (
"errors"
"fmt"
"github.com/exchain/go-exchain/cannon/mipsevm"
)
type StackTracker interface {
PushStack(caller Word, target Word)
PopStack()
}
type TraceableStackTracker interface {
StackTracker
Traceback()
}
type NoopStackTracker struct{}
func (n *NoopStackTracker) PushStack(caller Word, target Word) {}
func (n *NoopStackTracker) PopStack() {}
func (n *NoopStackTracker) Traceback() {}
type StackTrackerImpl struct {
state mipsevm.FPVMState
stack []Word
caller []Word
meta mipsevm.Metadata
}
func NewStackTracker(state mipsevm.FPVMState, meta mipsevm.Metadata) (*StackTrackerImpl, error) {
if meta == nil {
return nil, errors.New("metadata is nil")
}
return NewStackTrackerUnsafe(state, meta), nil
}
// NewStackTrackerUnsafe creates a new TraceableStackTracker without verifying meta is not nil
func NewStackTrackerUnsafe(state mipsevm.FPVMState, meta mipsevm.Metadata) *StackTrackerImpl {
return &StackTrackerImpl{state: state, meta: meta}
}
func (s *StackTrackerImpl) PushStack(caller Word, target Word) {
s.caller = append(s.caller, caller)
s.stack = append(s.stack, target)
}
func (s *StackTrackerImpl) PopStack() {
if len(s.stack) != 0 {
fn := s.meta.LookupSymbol(s.state.GetPC())
topFn := s.meta.LookupSymbol(s.stack[len(s.stack)-1])
if fn != topFn {
// most likely the function was inlined. Snap back to the last return.
i := len(s.stack) - 1
for ; i >= 0; i-- {
if s.meta.LookupSymbol(s.stack[i]) == fn {
s.stack = s.stack[:i]
s.caller = s.caller[:i]
break
}
}
} else {
s.stack = s.stack[:len(s.stack)-1]
s.caller = s.caller[:len(s.caller)-1]
}
} else {
fmt.Printf("ERROR: stack underflow at pc=%x. step=%d\n", s.state.GetPC(), s.state.GetStep())
}
}
func (s *StackTrackerImpl) Traceback() {
fmt.Printf("traceback at pc=%x. step=%d\n", s.state.GetPC(), s.state.GetStep())
for i := len(s.stack) - 1; i >= 0; i-- {
jumpAddr := s.stack[i]
idx := len(s.stack) - i - 1
fmt.Printf("\t%d %x in %s caller=%08x\n", idx, jumpAddr, s.meta.LookupSymbol(jumpAddr), s.caller[i])
}
}
package mipsevm
import "fmt"
// HexU32 to lazy-format integer attributes for logging
type HexU32 uint32
func (v HexU32) String() string {
return fmt.Sprintf("%08x", uint32(v))
}
func (v HexU32) MarshalText() ([]byte, error) {
return []byte(v.String()), nil
}
package mipsevm
import (
"io"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
"github.com/exchain/go-exchain/cannon/mipsevm/arch"
"github.com/exchain/go-exchain/cannon/mipsevm/memory"
"github.com/exchain/go-exchain/op-service/serialize"
)
type FPVMState interface {
serialize.Serializable
GetMemory() *memory.Memory
// GetHeap returns the current memory address at the top of the heap
GetHeap() arch.Word
// GetPreimageKey returns the most recently accessed preimage key
GetPreimageKey() common.Hash
// GetPreimageOffset returns the current offset into the current preimage
GetPreimageOffset() arch.Word
// GetPC returns the currently executing program counter
GetPC() arch.Word
// GetCpu returns the currently active cpu scalars, including the program counter
GetCpu() CpuScalars
// GetRegistersRef returns a pointer to the currently active registers
GetRegistersRef() *[32]arch.Word
// 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
// GetLastHint returns optional metadata which is not part of the VM state itself.
// It is used to remember the last pre-image hint,
// so a VM can start from any state without fetching prior pre-images,
// and instead just repeat the last hint on setup,
// to make sure pre-image requests can be served.
// The first 4 bytes are a Word length prefix.
// Warning: the hint MAY NOT BE COMPLETE. I.e. this is buffered,
// and should only be read when len(LastHint) > 4 && Word(LastHint[:4]) <= len(LastHint[4:])
GetLastHint() hexutil.Bytes
// EncodeWitness returns the witness for the current state and the state hash
EncodeWitness() (witness []byte, hash common.Hash)
// CreateVM creates a FPVM that can operate on this state.
CreateVM(logger log.Logger, po PreimageOracle, stdOut, stdErr io.Writer, meta Metadata) FPVM
}
type SymbolMatcher func(addr arch.Word) bool
type Metadata interface {
LookupSymbol(addr arch.Word) string
CreateSymbolMatcher(name string) SymbolMatcher
}
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)
// CheckInfiniteLoop returns true if the vm is stuck in an infinite loop
CheckInfiniteLoop() bool
// LastPreimage returns the last preimage accessed by the VM
LastPreimage() (preimageKey [32]byte, preimage []byte, preimageOffset arch.Word)
// Traceback prints a traceback of the program to the console
Traceback()
// GetDebugInfo returns debug information about the VM
GetDebugInfo() *DebugInfo
// InitDebug initializes the debug mode of the VM
InitDebug() error
// EnableStats if supported by the VM, enables some additional statistics that can be retrieved via GetDebugInfo()
EnableStats()
// LookupSymbol returns the symbol located at the specified address.
// May return an empty string if there's no symbol table available.
LookupSymbol(addr arch.Word) string
}
package mipsevm
import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
)
// LoggingWriter is a simple util to wrap a logger,
// and expose an io Writer interface,
// for the program running within the VM to write to.
type LoggingWriter struct {
Log log.Logger
}
func logAsText(b string) bool {
for _, c := range b {
if (c < 0x20 || c >= 0x7F) && (c != '\n' && c != '\t') {
return false
}
}
return true
}
func (lw *LoggingWriter) Write(b []byte) (int, error) {
t := string(b)
if logAsText(t) {
lw.Log.Info("", "text", t)
} else {
lw.Log.Info("", "data", hexutil.Bytes(b))
}
return len(b), nil
}
This diff is collapsed.
//go:build cannon64
// +build cannon64
package memory
import (
"bytes"
"crypto/rand"
"encoding/binary"
"encoding/json"
"io"
"strings"
"testing"
"github.com/exchain/go-exchain/cannon/mipsevm/arch"
"github.com/stretchr/testify/require"
)
// These tests are mostly copied from memory_test.go. With a few tweaks for 64-bit.
func TestMemory64MerkleProof(t *testing.T) {
t.Run("nearly empty tree", func(t *testing.T) {
m := NewMemory()
m.SetWord(0x10000, 0xAABBCCDD_EEFF1122)
proof := m.MerkleProof(0x10000)
require.Equal(t, uint64(0xAABBCCDD_EEFF1122), binary.BigEndian.Uint64(proof[:8]))
for i := 0; i < 64-5; i++ {
require.Equal(t, zeroHashes[i][:], proof[32+i*32:32+i*32+32], "empty siblings")
}
})
t.Run("fuller tree", func(t *testing.T) {
m := NewMemory()
m.SetWord(0x10000, 0xaabbccdd)
m.SetWord(0x80008, 42)
m.SetWord(0x13370000, 123)
root := m.MerkleRoot()
proof := m.MerkleProof(0x80008)
require.Equal(t, uint64(42), binary.BigEndian.Uint64(proof[8:16]))
node := *(*[32]byte)(proof[:32])
path := uint32(0x80008) >> 5
for i := 32; i < len(proof); i += 32 {
sib := *(*[32]byte)(proof[i : i+32])
if path&1 != 0 {
node = HashPair(sib, node)
} else {
node = HashPair(node, sib)
}
path >>= 1
}
require.Equal(t, root, node, "proof must verify")
})
}
func TestMemory64MerkleRoot(t *testing.T) {
t.Run("empty", func(t *testing.T) {
m := NewMemory()
root := m.MerkleRoot()
require.Equal(t, zeroHashes[64-5], root, "fully zeroed memory should have expected zero hash")
})
t.Run("empty page", func(t *testing.T) {
m := NewMemory()
m.SetWord(0xF000, 0)
root := m.MerkleRoot()
require.Equal(t, zeroHashes[64-5], root, "fully zeroed memory should have expected zero hash")
})
t.Run("single page", func(t *testing.T) {
m := NewMemory()
m.SetWord(0xF000, 1)
root := m.MerkleRoot()
require.NotEqual(t, zeroHashes[64-5], root, "non-zero memory")
})
t.Run("repeat zero", func(t *testing.T) {
m := NewMemory()
m.SetWord(0xF000, 0)
m.SetWord(0xF008, 0)
root := m.MerkleRoot()
require.Equal(t, zeroHashes[64-5], root, "zero still")
})
t.Run("two empty pages", func(t *testing.T) {
m := NewMemory()
m.SetWord(PageSize*3, 0)
m.SetWord(PageSize*10, 0)
root := m.MerkleRoot()
require.Equal(t, zeroHashes[64-5], root, "zero still")
})
t.Run("random few pages", func(t *testing.T) {
m := NewMemory()
m.SetWord(PageSize*3, 1)
m.SetWord(PageSize*5, 42)
m.SetWord(PageSize*6, 123)
p3 := m.MerkleizeSubtree((1 << PageKeySize) | 3)
p5 := m.MerkleizeSubtree((1 << PageKeySize) | 5)
p6 := m.MerkleizeSubtree((1 << PageKeySize) | 6)
z := zeroHashes[PageAddrSize-5]
r1 := HashPair(
HashPair(
HashPair(z, z), // 0,1
HashPair(z, p3), // 2,3
),
HashPair(
HashPair(z, p5), // 4,5
HashPair(p6, z), // 6,7
),
)
r2 := m.MerkleizeSubtree(1 << (PageKeySize - 3))
require.Equal(t, r1, r2, "expecting manual page combination to match subtree merkle func")
})
t.Run("invalidate page", func(t *testing.T) {
m := NewMemory()
m.SetWord(0xF000, 0)
require.Equal(t, zeroHashes[64-5], m.MerkleRoot(), "zero at first")
m.SetWord(0xF008, 1)
require.NotEqual(t, zeroHashes[64-5], m.MerkleRoot(), "non-zero")
m.SetWord(0xF008, 0)
require.Equal(t, zeroHashes[64-5], m.MerkleRoot(), "zero again")
})
}
func TestMemory64ReadWrite(t *testing.T) {
t.Run("large random", func(t *testing.T) {
m := NewMemory()
data := make([]byte, 20_000)
_, err := rand.Read(data[:])
require.NoError(t, err)
require.NoError(t, m.SetMemoryRange(0, bytes.NewReader(data)))
for _, i := range []Word{0, 8, 1000, 20_000 - 8} {
v := m.GetWord(i)
expected := binary.BigEndian.Uint64(data[i : i+8])
require.Equalf(t, expected, v, "read at %d", i)
}
})
t.Run("repeat range", func(t *testing.T) {
m := NewMemory()
data := []byte(strings.Repeat("under the big bright yellow sun ", 40))
require.NoError(t, m.SetMemoryRange(0x1337, bytes.NewReader(data)))
res, err := io.ReadAll(m.ReadMemoryRange(0x1337-10, Word(len(data)+20)))
require.NoError(t, err)
require.Equal(t, make([]byte, 10), res[:10], "empty start")
require.Equal(t, data, res[10:len(res)-10], "result")
require.Equal(t, make([]byte, 10), res[len(res)-10:], "empty end")
})
t.Run("empty range", func(t *testing.T) {
m := NewMemory()
addr := Word(0xAABBCC00)
r := bytes.NewReader(nil)
pre := m.MerkleRoot()
preJSON, err := m.MarshalJSON()
require.NoError(t, err)
var preSerialized bytes.Buffer
require.NoError(t, m.Serialize(&preSerialized))
require.NoError(t, m.SetMemoryRange(addr, r))
v := m.GetWord(0)
require.Equal(t, Word(0), v)
post := m.MerkleRoot()
require.Equal(t, pre, post)
// Assert that there are no extra zero pages in serialization
postJSON, err := m.MarshalJSON()
require.NoError(t, err)
require.Equal(t, preJSON, postJSON)
var postSerialized bytes.Buffer
require.NoError(t, m.Serialize(&postSerialized))
require.Equal(t, preSerialized.Bytes(), postSerialized.Bytes())
})
t.Run("range page overlap", func(t *testing.T) {
m := NewMemory()
data := bytes.Repeat([]byte{0xAA}, PageAddrSize)
require.NoError(t, m.SetMemoryRange(0, bytes.NewReader(data)))
for i := 0; i < PageAddrSize/arch.WordSizeBytes; i++ {
addr := Word(i * arch.WordSizeBytes)
require.Equal(t, Word(0xAAAAAAAA_AAAAAAAA), m.GetWord(addr))
}
data = []byte{0x11, 0x22, 0x33, 0x44}
require.NoError(t, m.SetMemoryRange(0, bytes.NewReader(data)))
require.Equal(t, Word(0x11223344_AAAAAAAA), m.GetWord(0))
for i := 1; i < PageAddrSize/arch.WordSizeBytes; i++ {
addr := Word(i * arch.WordSizeBytes)
require.Equal(t, Word(0xAAAAAAAA_AAAAAAAA), m.GetWord(addr))
}
})
t.Run("read-write", func(t *testing.T) {
m := NewMemory()
m.SetWord(16, 0xAABBCCDD_EEFF1122)
require.Equal(t, Word(0xAABBCCDD_EEFF1122), m.GetWord(16))
m.SetWord(16, 0xAABB1CDD_EEFF1122)
require.Equal(t, Word(0xAABB1CDD_EEFF1122), m.GetWord(16))
m.SetWord(16, 0xAABB1CDD_EEFF1123)
require.Equal(t, Word(0xAABB1CDD_EEFF1123), m.GetWord(16))
})
t.Run("unaligned read", func(t *testing.T) {
m := NewMemory()
m.SetWord(16, Word(0xAABBCCDD_EEFF1122))
m.SetWord(24, 0x11223344_55667788)
for i := Word(17); i < 24; i++ {
require.Panics(t, func() {
m.GetWord(i)
})
}
require.Equal(t, Word(0x11223344_55667788), m.GetWord(24))
require.Equal(t, Word(0), m.GetWord(32))
require.Equal(t, Word(0xAABBCCDD_EEFF1122), m.GetWord(16))
})
t.Run("unaligned write", func(t *testing.T) {
m := NewMemory()
m.SetWord(16, 0xAABBCCDD_EEFF1122)
require.Panics(t, func() {
m.SetWord(17, 0x11223344)
})
require.Panics(t, func() {
m.SetWord(18, 0x11223344)
})
require.Panics(t, func() {
m.SetWord(19, 0x11223344)
})
require.Panics(t, func() {
m.SetWord(20, 0x11223344)
})
require.Panics(t, func() {
m.SetWord(21, 0x11223344)
})
require.Panics(t, func() {
m.SetWord(22, 0x11223344)
})
require.Panics(t, func() {
m.SetWord(23, 0x11223344)
})
require.Equal(t, Word(0xAABBCCDD_EEFF1122), m.GetWord(16))
})
}
func TestMemory64JSON(t *testing.T) {
m := NewMemory()
m.SetWord(8, 0xAABBCCDD_EEFF1122)
dat, err := json.Marshal(m)
require.NoError(t, err)
var res Memory
require.NoError(t, json.Unmarshal(dat, &res))
require.Equal(t, Word(0xAABBCCDD_EEFF1122), res.GetWord(8))
}
func TestMemory64Copy(t *testing.T) {
m := NewMemory()
m.SetWord(0xAABBCCDD_8000, 0x000000_AABB)
mcpy := m.Copy()
require.Equal(t, Word(0xAABB), mcpy.GetWord(0xAABBCCDD_8000))
require.Equal(t, m.MerkleRoot(), mcpy.MerkleRoot())
}
//go:build !cannon64
// +build !cannon64
package memory
import (
"bytes"
"crypto/rand"
"encoding/binary"
"encoding/json"
"io"
"strings"
"testing"
"github.com/exchain/go-exchain/cannon/mipsevm/arch"
"github.com/stretchr/testify/require"
)
func TestMemoryMerkleProof(t *testing.T) {
t.Run("nearly empty tree", func(t *testing.T) {
m := NewMemory()
m.SetWord(0x10000, 0xaabbccdd)
proof := m.MerkleProof(0x10000)
require.Equal(t, uint32(0xaabbccdd), binary.BigEndian.Uint32(proof[:4]))
for i := 0; i < 32-5; i++ {
require.Equal(t, zeroHashes[i][:], proof[32+i*32:32+i*32+32], "empty siblings")
}
})
t.Run("fuller tree", func(t *testing.T) {
m := NewMemory()
m.SetWord(0x10000, 0xaabbccdd)
m.SetWord(0x80004, 42)
m.SetWord(0x13370000, 123)
root := m.MerkleRoot()
proof := m.MerkleProof(0x80004)
require.Equal(t, uint32(42), binary.BigEndian.Uint32(proof[4:8]))
node := *(*[32]byte)(proof[:32])
path := uint32(0x80004) >> 5
for i := 32; i < len(proof); i += 32 {
sib := *(*[32]byte)(proof[i : i+32])
if path&1 != 0 {
node = HashPair(sib, node)
} else {
node = HashPair(node, sib)
}
path >>= 1
}
require.Equal(t, root, node, "proof must verify")
})
}
func TestMemoryMerkleRoot(t *testing.T) {
t.Run("empty", func(t *testing.T) {
m := NewMemory()
root := m.MerkleRoot()
require.Equal(t, zeroHashes[32-5], root, "fully zeroed memory should have expected zero hash")
})
t.Run("empty page", func(t *testing.T) {
m := NewMemory()
m.SetWord(0xF000, 0)
root := m.MerkleRoot()
require.Equal(t, zeroHashes[32-5], root, "fully zeroed memory should have expected zero hash")
})
t.Run("single page", func(t *testing.T) {
m := NewMemory()
m.SetWord(0xF000, 1)
root := m.MerkleRoot()
require.NotEqual(t, zeroHashes[32-5], root, "non-zero memory")
})
t.Run("repeat zero", func(t *testing.T) {
m := NewMemory()
m.SetWord(0xF000, 0)
m.SetWord(0xF004, 0)
root := m.MerkleRoot()
require.Equal(t, zeroHashes[32-5], root, "zero still")
})
t.Run("two empty pages", func(t *testing.T) {
m := NewMemory()
m.SetWord(PageSize*3, 0)
m.SetWord(PageSize*10, 0)
root := m.MerkleRoot()
require.Equal(t, zeroHashes[32-5], root, "zero still")
})
t.Run("random few pages", func(t *testing.T) {
m := NewMemory()
m.SetWord(PageSize*3, 1)
m.SetWord(PageSize*5, 42)
m.SetWord(PageSize*6, 123)
p3 := m.MerkleizeSubtree((1 << PageKeySize) | 3)
p5 := m.MerkleizeSubtree((1 << PageKeySize) | 5)
p6 := m.MerkleizeSubtree((1 << PageKeySize) | 6)
z := zeroHashes[PageAddrSize-5]
r1 := HashPair(
HashPair(
HashPair(z, z), // 0,1
HashPair(z, p3), // 2,3
),
HashPair(
HashPair(z, p5), // 4,5
HashPair(p6, z), // 6,7
),
)
r2 := m.MerkleizeSubtree(1 << (PageKeySize - 3))
require.Equal(t, r1, r2, "expecting manual page combination to match subtree merkle func")
})
t.Run("invalidate page", func(t *testing.T) {
m := NewMemory()
m.SetWord(0xF000, 0)
require.Equal(t, zeroHashes[32-5], m.MerkleRoot(), "zero at first")
m.SetWord(0xF004, 1)
require.NotEqual(t, zeroHashes[32-5], m.MerkleRoot(), "non-zero")
m.SetWord(0xF004, 0)
require.Equal(t, zeroHashes[32-5], m.MerkleRoot(), "zero again")
})
}
func TestMemoryReadWrite(t *testing.T) {
t.Run("large random", func(t *testing.T) {
m := NewMemory()
data := make([]byte, 20_000)
_, err := rand.Read(data[:])
require.NoError(t, err)
require.NoError(t, m.SetMemoryRange(0, bytes.NewReader(data)))
for _, i := range []Word{0, 4, 1000, 20_000 - 4} {
v := m.GetWord(i)
expected := binary.BigEndian.Uint32(data[i : i+4])
require.Equalf(t, expected, v, "read at %d", i)
}
})
t.Run("repeat range", func(t *testing.T) {
m := NewMemory()
data := []byte(strings.Repeat("under the big bright yellow sun ", 40))
require.NoError(t, m.SetMemoryRange(0x1337, bytes.NewReader(data)))
res, err := io.ReadAll(m.ReadMemoryRange(0x1337-10, Word(len(data)+20)))
require.NoError(t, err)
require.Equal(t, make([]byte, 10), res[:10], "empty start")
require.Equal(t, data, res[10:len(res)-10], "result")
require.Equal(t, make([]byte, 10), res[len(res)-10:], "empty end")
})
t.Run("empty range", func(t *testing.T) {
m := NewMemory()
addr := Word(0xAABBCC00)
r := bytes.NewReader(nil)
pre := m.MerkleRoot()
preJSON, err := m.MarshalJSON()
require.NoError(t, err)
var preSerialized bytes.Buffer
require.NoError(t, m.Serialize(&preSerialized))
require.NoError(t, m.SetMemoryRange(addr, r))
v := m.GetWord(0)
require.Equal(t, Word(0), v)
post := m.MerkleRoot()
require.Equal(t, pre, post)
// Assert that there are no extra zero pages in serialization
postJSON, err := m.MarshalJSON()
require.NoError(t, err)
require.Equal(t, preJSON, postJSON)
var postSerialized bytes.Buffer
require.NoError(t, m.Serialize(&postSerialized))
require.Equal(t, preSerialized.Bytes(), postSerialized.Bytes())
})
t.Run("range page overlap", func(t *testing.T) {
m := NewMemory()
data := bytes.Repeat([]byte{0xAA}, PageAddrSize)
require.NoError(t, m.SetMemoryRange(0, bytes.NewReader(data)))
for i := 0; i < PageAddrSize/arch.WordSizeBytes; i++ {
addr := Word(i * arch.WordSizeBytes)
require.Equal(t, Word(0xAAAAAAAA), m.GetWord(addr))
}
data = []byte{0x11, 0x22}
require.NoError(t, m.SetMemoryRange(0, bytes.NewReader(data)))
require.Equal(t, Word(0x1122_AAAA), m.GetWord(0))
for i := 1; i < PageAddrSize/arch.WordSizeBytes; i++ {
addr := Word(i * arch.WordSizeBytes)
require.Equal(t, Word(0xAAAAAAAA), m.GetWord(addr))
}
})
t.Run("read-write", func(t *testing.T) {
m := NewMemory()
m.SetWord(12, 0xAABBCCDD)
require.Equal(t, uint32(0xAABBCCDD), m.GetWord(12))
m.SetWord(12, 0xAABB1CDD)
require.Equal(t, uint32(0xAABB1CDD), m.GetWord(12))
})
t.Run("unaligned read", func(t *testing.T) {
m := NewMemory()
m.SetWord(12, 0xAABBCCDD)
m.SetWord(16, 0x11223344)
require.Panics(t, func() {
m.GetWord(13)
})
require.Panics(t, func() {
m.GetWord(14)
})
require.Panics(t, func() {
m.GetWord(15)
})
require.Equal(t, uint32(0x11223344), m.GetWord(16))
require.Equal(t, uint32(0), m.GetWord(20))
require.Equal(t, uint32(0xAABBCCDD), m.GetWord(12))
})
t.Run("unaligned write", func(t *testing.T) {
m := NewMemory()
m.SetWord(12, 0xAABBCCDD)
require.Panics(t, func() {
m.SetWord(13, 0x11223344)
})
require.Panics(t, func() {
m.SetWord(14, 0x11223344)
})
require.Panics(t, func() {
m.SetWord(15, 0x11223344)
})
require.Equal(t, uint32(0xAABBCCDD), m.GetWord(12))
})
}
func TestMemoryJSON(t *testing.T) {
m := NewMemory()
m.SetWord(8, 0xAABBCCDD)
dat, err := json.Marshal(m)
require.NoError(t, err)
var res Memory
require.NoError(t, json.Unmarshal(dat, &res))
require.Equal(t, uint32(0xAABBCCDD), res.GetWord(8))
}
func TestMemoryCopy(t *testing.T) {
m := NewMemory()
m.SetWord(0x8000, 123)
mcpy := m.Copy()
require.Equal(t, Word(123), mcpy.GetWord(0x8000))
require.Equal(t, m.MerkleRoot(), mcpy.MerkleRoot())
}
package memory
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"sync"
"github.com/ethereum/go-ethereum/crypto"
)
var zlibWriterPool = sync.Pool{
New: func() any {
var buf bytes.Buffer
return zlib.NewWriter(&buf)
},
}
type Page [PageSize]byte
func (p *Page) MarshalJSON() ([]byte, error) { // nosemgrep
var out bytes.Buffer
w := zlibWriterPool.Get().(*zlib.Writer)
defer zlibWriterPool.Put(w)
w.Reset(&out)
if _, err := w.Write(p[:]); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return json.Marshal(out.Bytes())
}
func (p *Page) UnmarshalJSON(dat []byte) error {
// Strip off the `"` characters at the start & end.
dat = dat[1 : len(dat)-1]
// Decode b64 then decompress
r, err := zlib.NewReader(base64.NewDecoder(base64.StdEncoding, bytes.NewReader(dat)))
if err != nil {
return err
}
defer r.Close()
if n, err := r.Read(p[:]); n != PageSize {
return fmt.Errorf("epxeted %d bytes, but got %d", PageSize, n)
} else if err == io.EOF {
return nil
} else {
return err
}
}
func (p *Page) UnmarshalText(dat []byte) error {
if len(dat) != PageSize*2 {
return fmt.Errorf("expected %d hex chars, but got %d", PageSize*2, len(dat))
}
_, err := hex.Decode(p[:], dat)
return err
}
type CachedPage struct {
Data *Page
// intermediate nodes only
Cache [PageSize / 32][32]byte
// true if the intermediate node is valid
Ok [PageSize / 32]bool
}
func (p *CachedPage) invalidate(pageAddr Word) {
if pageAddr >= PageSize {
panic("invalid page addr")
}
k := (1 << PageAddrSize) | pageAddr
// first cache layer caches nodes that has two 32 byte leaf nodes.
k >>= 5 + 1
for k > 0 {
p.Ok[k] = false
k >>= 1
}
}
func (p *CachedPage) InvalidateFull() {
p.Ok = [PageSize / 32]bool{} // reset everything to false
}
func (p *CachedPage) MerkleRoot() [32]byte {
// hash the bottom layer
for i := uint64(0); i < PageSize; i += 64 {
j := PageSize/32/2 + i/64
if p.Ok[j] {
continue
}
p.Cache[j] = crypto.Keccak256Hash(p.Data[i : i+64])
//fmt.Printf("0x%x 0x%x -> 0x%x\n", p.Data[i:i+32], p.Data[i+32:i+64], p.Cache[j])
p.Ok[j] = true
}
// hash the cache layers
for i := PageSize/32 - 2; i > 0; i -= 2 {
j := i >> 1
if p.Ok[j] {
continue
}
p.Cache[j] = HashPair(p.Cache[i], p.Cache[i+1])
p.Ok[j] = true
}
return p.Cache[1]
}
func (p *CachedPage) MerkleizeSubtree(gindex uint64) [32]byte {
_ = p.MerkleRoot() // fill cache
if gindex >= PageSize/32 {
if gindex >= PageSize/32*2 {
panic("gindex too deep")
}
// it's pointing to a bottom node
nodeIndex := gindex & (PageAddrMask >> 5)
return *(*[32]byte)(p.Data[nodeIndex*32 : nodeIndex*32+32])
}
return p.Cache[gindex]
}
package memory
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestCachedPage(t *testing.T) {
p := &CachedPage{Data: new(Page)}
p.Data[42] = 0xab
gindex := ((uint64(1) << PageAddrSize) | 42) >> 5
node := common.Hash(p.MerkleizeSubtree(gindex))
expectedLeaf := common.Hash{10: 0xab}
require.Equal(t, expectedLeaf, node, "leaf nodes should not be hashed")
node = p.MerkleizeSubtree(gindex >> 1)
expectedParent := common.Hash(HashPair(zeroHashes[0], expectedLeaf))
require.Equal(t, expectedParent, node, "can get the parent node")
node = p.MerkleizeSubtree(gindex >> 2)
expectedParentParent := common.Hash(HashPair(expectedParent, zeroHashes[1]))
require.Equal(t, expectedParentParent, node, "and the parent of the parent")
pre := p.MerkleRoot()
p.Data[42] = 0xcd
post := p.MerkleRoot()
require.Equal(t, pre, post, "no change expected until cache is invalidated")
p.invalidate(42)
post2 := p.MerkleRoot()
require.NotEqual(t, post, post2, "change after cache invalidation")
p.Data[2000] = 0xef
p.invalidate(42)
post3 := p.MerkleRoot()
require.Equal(t, post2, post3, "local invalidation is not global invalidation")
p.invalidate(2000)
post4 := p.MerkleRoot()
require.NotEqual(t, post3, post4, "can see the change now")
p.Data[1000] = 0xff
p.InvalidateFull()
post5 := p.MerkleRoot()
require.NotEqual(t, post4, post5, "and global invalidation works regardless of changed data")
}
package multithreaded
import (
"io"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
"github.com/exchain/go-exchain/cannon/mipsevm"
"github.com/exchain/go-exchain/cannon/mipsevm/arch"
"github.com/exchain/go-exchain/cannon/mipsevm/exec"
)
type InstrumentedState struct {
state *State
log log.Logger
stdOut io.Writer
stdErr io.Writer
memoryTracker *exec.MemoryTrackerImpl
stackTracker ThreadedStackTracker
statsTracker StatsTracker
preimageOracle *exec.TrackingPreimageOracleReader
meta mipsevm.Metadata
}
var _ mipsevm.FPVM = (*InstrumentedState)(nil)
func NewInstrumentedState(state *State, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger, meta mipsevm.Metadata) *InstrumentedState {
return &InstrumentedState{
state: state,
log: log,
stdOut: stdOut,
stdErr: stdErr,
memoryTracker: exec.NewMemoryTracker(state.Memory),
stackTracker: &NoopThreadedStackTracker{},
statsTracker: NoopStatsTracker(),
preimageOracle: exec.NewTrackingPreimageOracleReader(po),
meta: meta,
}
}
func (m *InstrumentedState) InitDebug() error {
stackTracker, err := NewThreadedStackTracker(m.state, m.meta)
if err != nil {
return err
}
m.stackTracker = stackTracker
return nil
}
func (m *InstrumentedState) EnableStats() {
m.statsTracker = NewStatsTracker()
}
func (m *InstrumentedState) Step(proof bool) (wit *mipsevm.StepWitness, err error) {
m.preimageOracle.Reset()
m.memoryTracker.Reset(proof)
if proof {
proofData := make([]byte, 0)
threadProof := m.state.EncodeThreadProof()
insnProof := m.state.Memory.MerkleProof(m.state.GetPC())
proofData = append(proofData, threadProof[:]...)
proofData = append(proofData, insnProof[:]...)
encodedWitness, stateHash := m.state.EncodeWitness()
wit = &mipsevm.StepWitness{
State: encodedWitness,
StateHash: stateHash,
ProofData: proofData,
}
}
err = m.mipsStep()
if err != nil {
return nil, err
}
if proof {
memProof := m.memoryTracker.MemProof()
memProof2 := m.memoryTracker.MemProof2()
wit.ProofData = append(wit.ProofData, memProof[:]...)
wit.ProofData = append(wit.ProofData, memProof2[:]...)
lastPreimageKey, lastPreimage, lastPreimageOffset := m.preimageOracle.LastPreimage()
if lastPreimageOffset != ^arch.Word(0) {
wit.PreimageOffset = lastPreimageOffset
wit.PreimageKey = lastPreimageKey
wit.PreimageValue = lastPreimage
}
}
return
}
func (m *InstrumentedState) CheckInfiniteLoop() bool {
return false
}
func (m *InstrumentedState) LastPreimage() ([32]byte, []byte, arch.Word) {
return m.preimageOracle.LastPreimage()
}
func (m *InstrumentedState) GetState() mipsevm.FPVMState {
return m.state
}
func (m *InstrumentedState) GetDebugInfo() *mipsevm.DebugInfo {
debugInfo := &mipsevm.DebugInfo{
Pages: m.state.Memory.PageCount(),
MemoryUsed: hexutil.Uint64(m.state.Memory.UsageRaw()),
NumPreimageRequests: m.preimageOracle.NumPreimageRequests(),
TotalPreimageSize: m.preimageOracle.TotalPreimageSize(),
TotalSteps: m.state.GetStep(),
}
m.statsTracker.populateDebugInfo(debugInfo)
return debugInfo
}
func (m *InstrumentedState) Traceback() {
m.stackTracker.Traceback()
}
func (m *InstrumentedState) LookupSymbol(addr arch.Word) string {
if m.meta == nil {
return ""
}
return m.meta.LookupSymbol(addr)
}
package multithreaded
import (
"bytes"
"io"
"os"
"testing"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
"github.com/exchain/go-exchain/cannon/mipsevm"
"github.com/exchain/go-exchain/cannon/mipsevm/memory"
"github.com/exchain/go-exchain/cannon/mipsevm/program"
"github.com/exchain/go-exchain/cannon/mipsevm/testutil"
)
func vmFactory(state *State, po mipsevm.PreimageOracle, stdOut, stdErr io.Writer, log log.Logger, meta *program.Metadata) mipsevm.FPVM {
return NewInstrumentedState(state, po, stdOut, stdErr, log, meta)
}
func TestInstrumentedState_OpenMips(t *testing.T) {
t.Parallel()
testutil.RunVMTests_OpenMips(t, CreateEmptyState, vmFactory, "clone.bin")
}
func TestInstrumentedState_Hello(t *testing.T) {
t.Parallel()
testutil.RunVMTest_Hello(t, CreateInitialState, vmFactory, false)
}
func TestInstrumentedState_Claim(t *testing.T) {
t.Parallel()
testutil.RunVMTest_Claim(t, CreateInitialState, vmFactory, false)
}
func TestInstrumentedState_UtilsCheck(t *testing.T) {
// Sanity check that test running utilities will return a non-zero exit code on failure
t.Parallel()
cases := []struct {
name string
expectedOutput string
}{
{name: "utilscheck", expectedOutput: "Test failed: ShouldFail"},
{name: "utilscheck2", expectedOutput: "Test failed: ShouldFail (subtest 2)"},
{name: "utilscheck3", expectedOutput: "Test panicked: ShouldFail (panic test)"},
{name: "utilscheck4", expectedOutput: "Test panicked: ShouldFail"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
state, meta := testutil.LoadELFProgram(t, testutil.ProgramPath(c.name), CreateInitialState, false)
oracle := testutil.StaticOracle(t, []byte{})
var stdOutBuf, stdErrBuf bytes.Buffer
us := NewInstrumentedState(state, oracle, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), testutil.CreateLogger(), meta)
for i := 0; i < 1_000_000; i++ {
if us.GetState().GetExited() {
break
}
_, err := us.Step(false)
require.NoError(t, err)
}
t.Logf("Completed in %d steps", state.Step)
require.True(t, state.Exited, "must complete program")
require.Equal(t, uint8(1), state.ExitCode, "exit with 1")
require.Contains(t, stdOutBuf.String(), c.expectedOutput)
require.NotContains(t, stdOutBuf.String(), "Passed test that should have failed")
require.Equal(t, "", stdErrBuf.String(), "should not print any errors")
})
}
}
func TestInstrumentedState_MultithreadedProgram(t *testing.T) {
if os.Getenv("SKIP_SLOW_TESTS") == "true" {
t.Skip("Skipping slow test because SKIP_SLOW_TESTS is enabled")
}
t.Parallel()
cases := []struct {
name string
expectedOutput []string
programName string
steps int
}{
{
name: "general concurrency test",
expectedOutput: []string{
"waitgroup result: 42",
"channels result: 1234",
"GC complete!",
},
programName: "mt-general",
steps: 5_000_000,
},
{
name: "atomic test",
expectedOutput: []string{
"Atomic tests passed",
},
programName: "mt-atomic",
steps: 350_000_000,
},
{
name: "waitgroup test",
expectedOutput: []string{
"WaitGroup tests passed",
},
programName: "mt-wg",
steps: 15_000_000,
},
{
name: "mutex test",
expectedOutput: []string{
"Mutex test passed",
},
programName: "mt-mutex",
steps: 5_000_000,
},
{
name: "cond test",
expectedOutput: []string{
"Cond test passed",
},
programName: "mt-cond",
steps: 5_000_000,
},
{
name: "rwmutex test",
expectedOutput: []string{
"RWMutex test passed",
},
programName: "mt-rwmutex",
steps: 5_000_000,
},
{
name: "once test",
expectedOutput: []string{
"Once test passed",
},
programName: "mt-once",
steps: 5_000_000,
},
{
name: "oncefunc test",
expectedOutput: []string{
"OnceFunc tests passed",
},
programName: "mt-oncefunc",
steps: 15_000_000,
},
{
name: "map test",
expectedOutput: []string{
"Map test passed",
},
programName: "mt-map",
steps: 150_000_000,
},
{
name: "pool test",
expectedOutput: []string{
"Pool test passed",
},
programName: "mt-pool",
steps: 50_000_000,
},
{
name: "value test",
expectedOutput: []string{
"Value tests passed",
},
programName: "mt-value",
steps: 3_000_000,
},
}
for _, test := range cases {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
state, meta := testutil.LoadELFProgram(t, testutil.ProgramPath(test.programName), CreateInitialState, false)
oracle := testutil.StaticOracle(t, []byte{})
var stdOutBuf, stdErrBuf bytes.Buffer
us := NewInstrumentedState(state, oracle, io.MultiWriter(&stdOutBuf, os.Stdout), io.MultiWriter(&stdErrBuf, os.Stderr), testutil.CreateLogger(), meta)
for i := 0; i < test.steps; i++ {
if us.GetState().GetExited() {
break
}
_, err := us.Step(false)
require.NoError(t, err)
}
t.Logf("Completed in %d steps", state.Step)
require.True(t, state.Exited, "must complete program")
require.Equal(t, uint8(0), state.ExitCode, "exit with 0")
for _, expected := range test.expectedOutput {
require.Contains(t, stdOutBuf.String(), expected)
}
require.Equal(t, "", stdErrBuf.String(), "should not print any errors")
})
}
}
func TestInstrumentedState_Alloc(t *testing.T) {
if os.Getenv("SKIP_SLOW_TESTS") == "true" {
t.Skip("Skipping slow test because SKIP_SLOW_TESTS is enabled")
}
const MiB = 1024 * 1024
cases := []struct {
name string
numAllocs int
allocSize int
maxMemoryUsageCheck int
}{
{name: "10 32MiB allocations", numAllocs: 10, allocSize: 32 * MiB, maxMemoryUsageCheck: 256 * MiB},
{name: "5 64MiB allocations", numAllocs: 5, allocSize: 64 * MiB, maxMemoryUsageCheck: 256 * MiB},
{name: "5 128MiB allocations", numAllocs: 5, allocSize: 128 * MiB, maxMemoryUsageCheck: 128 * 3 * MiB},
}
for _, test := range cases {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
state, meta := testutil.LoadELFProgram(t, testutil.ProgramPath("alloc"), CreateInitialState, false)
oracle := testutil.AllocOracle(t, test.numAllocs, test.allocSize)
us := NewInstrumentedState(state, oracle, os.Stdout, os.Stderr, testutil.CreateLogger(), meta)
require.NoError(t, us.InitDebug())
// emulation shouldn't take more than 20 B steps
for i := 0; i < 20_000_000_000; i++ {
if us.GetState().GetExited() {
break
}
_, err := us.Step(false)
require.NoError(t, err)
if state.Step%10_000_000 == 0 {
t.Logf("Completed %d steps", state.Step)
}
}
memUsage := state.Memory.PageCount() * memory.PageSize
t.Logf("Completed in %d steps. cannon memory usage: %d KiB", state.Step, memUsage/1024/1024.0)
require.True(t, state.Exited, "must complete program")
require.Equal(t, uint8(0), state.ExitCode, "exit with 0")
require.Less(t, memUsage, test.maxMemoryUsageCheck, "memory allocation is too large")
})
}
}
This diff is collapsed.
package multithreaded
import (
"errors"
"github.com/exchain/go-exchain/cannon/mipsevm"
"github.com/exchain/go-exchain/cannon/mipsevm/exec"
)
type ThreadedStackTracker interface {
exec.TraceableStackTracker
DropThread(threadId Word)
}
type NoopThreadedStackTracker struct {
exec.NoopStackTracker
}
var _ ThreadedStackTracker = (*ThreadedStackTrackerImpl)(nil)
func (n *NoopThreadedStackTracker) DropThread(threadId Word) {}
type ThreadedStackTrackerImpl struct {
meta mipsevm.Metadata
state *State
trackersByThreadId map[Word]exec.TraceableStackTracker
}
var _ ThreadedStackTracker = (*ThreadedStackTrackerImpl)(nil)
func NewThreadedStackTracker(state *State, meta mipsevm.Metadata) (*ThreadedStackTrackerImpl, error) {
if meta == nil {
return nil, errors.New("metadata is nil")
}
return &ThreadedStackTrackerImpl{
state: state,
meta: meta,
trackersByThreadId: make(map[Word]exec.TraceableStackTracker),
}, nil
}
func (t *ThreadedStackTrackerImpl) PushStack(caller Word, target Word) {
t.getCurrentTracker().PushStack(caller, target)
}
func (t *ThreadedStackTrackerImpl) PopStack() {
t.getCurrentTracker().PopStack()
}
func (t *ThreadedStackTrackerImpl) Traceback() {
t.getCurrentTracker().Traceback()
}
func (t *ThreadedStackTrackerImpl) getCurrentTracker() exec.TraceableStackTracker {
thread := t.state.GetCurrentThread()
tracker, exists := t.trackersByThreadId[thread.ThreadId]
if !exists {
tracker = exec.NewStackTrackerUnsafe(t.state, t.meta)
t.trackersByThreadId[thread.ThreadId] = tracker
}
return tracker
}
func (t *ThreadedStackTrackerImpl) DropThread(threadId Word) {
delete(t.trackersByThreadId, threadId)
}
This diff is collapsed.
This diff is collapsed.
package multithreaded
import (
lru "github.com/hashicorp/golang-lru/v2/simplelru"
"github.com/exchain/go-exchain/cannon/mipsevm"
)
// Define stats interface
type StatsTracker interface {
trackLL(threadId Word, step uint64)
trackSCSuccess(threadId Word, step uint64)
trackSCFailure(threadId Word, step uint64)
trackReservationInvalidation()
trackForcedPreemption()
trackThreadActivated(tid Word, step uint64)
populateDebugInfo(debugInfo *mipsevm.DebugInfo)
}
// Noop implementation for when tracking is disabled
type noopStatsTracker struct{}
func NoopStatsTracker() StatsTracker {
return &noopStatsTracker{}
}
func (s *noopStatsTracker) trackLL(threadId Word, step uint64) {}
func (s *noopStatsTracker) trackSCSuccess(threadId Word, step uint64) {}
func (s *noopStatsTracker) trackSCFailure(threadId Word, step uint64) {}
func (s *noopStatsTracker) trackReservationInvalidation() {}
func (s *noopStatsTracker) trackForcedPreemption() {}
func (s *noopStatsTracker) trackThreadActivated(tid Word, step uint64) {}
func (s *noopStatsTracker) populateDebugInfo(debugInfo *mipsevm.DebugInfo) {}
var _ StatsTracker = (*noopStatsTracker)(nil)
// Actual implementation
type statsTrackerImpl struct {
// State
lastLLStepByThread *lru.LRU[Word, uint64]
activeThreadId Word
lastActiveStepThread0 uint64
// Stats
rmwSuccessCount uint64
rmwFailCount uint64
maxStepsBetweenLLAndSC uint64
// Tracks RMW reservation invalidation due to reserved memory being accessed outside of the RMW sequence
reservationInvalidationCount uint64
forcedPreemptionCount uint64
idleStepCountThread0 uint64
}
func (s *statsTrackerImpl) populateDebugInfo(debugInfo *mipsevm.DebugInfo) {
debugInfo.RmwSuccessCount = s.rmwSuccessCount
debugInfo.RmwFailCount = s.rmwFailCount
debugInfo.MaxStepsBetweenLLAndSC = s.maxStepsBetweenLLAndSC
debugInfo.ReservationInvalidationCount = s.reservationInvalidationCount
debugInfo.ForcedPreemptionCount = s.forcedPreemptionCount
debugInfo.IdleStepCountThread0 = s.idleStepCountThread0
}
func (s *statsTrackerImpl) trackLL(threadId Word, step uint64) {
s.lastLLStepByThread.Add(threadId, step)
}
func (s *statsTrackerImpl) trackSCSuccess(threadId Word, step uint64) {
s.rmwSuccessCount += 1
s.recordStepsBetweenLLAndSC(threadId, step)
}
func (s *statsTrackerImpl) trackSCFailure(threadId Word, step uint64) {
s.rmwFailCount += 1
s.recordStepsBetweenLLAndSC(threadId, step)
}
func (s *statsTrackerImpl) recordStepsBetweenLLAndSC(threadId Word, scStep uint64) {
// Track rmw steps if we have the last ll step in our cache
if llStep, ok := s.lastLLStepByThread.Get(threadId); ok {
diff := scStep - llStep
if diff > s.maxStepsBetweenLLAndSC {
s.maxStepsBetweenLLAndSC = diff
}
// Purge ll step since the RMW seq is now complete
s.lastLLStepByThread.Remove(threadId)
}
}
func (s *statsTrackerImpl) trackReservationInvalidation() {
s.reservationInvalidationCount += 1
}
func (s *statsTrackerImpl) trackForcedPreemption() {
s.forcedPreemptionCount += 1
}
func (s *statsTrackerImpl) trackThreadActivated(tid Word, step uint64) {
if s.activeThreadId == Word(0) && tid != Word(0) {
// Thread 0 has been deactivated, start tracking to capture idle steps
s.lastActiveStepThread0 = step
} else if s.activeThreadId != Word(0) && tid == Word(0) {
// Thread 0 has been activated, record idle steps
idleSteps := step - s.lastActiveStepThread0
s.idleStepCountThread0 += idleSteps
}
s.activeThreadId = tid
}
func NewStatsTracker() StatsTracker {
return newStatsTracker(5)
}
func newStatsTracker(cacheSize int) StatsTracker {
llStepCache, err := lru.NewLRU[Word, uint64](cacheSize, nil)
if err != nil {
panic(err) // negative size parameter may produce an error
}
return &statsTrackerImpl{
lastLLStepByThread: llStepCache,
}
}
var _ StatsTracker = (*statsTrackerImpl)(nil)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
package testutil
import (
"github.com/stretchr/testify/require"
"github.com/exchain/go-exchain/cannon/mipsevm"
"github.com/exchain/go-exchain/cannon/mipsevm/multithreaded"
)
func GetMtState(t require.TestingT, vm mipsevm.FPVM) *multithreaded.State {
state := vm.GetState()
mtState, ok := state.(*multithreaded.State)
if !ok {
require.Fail(t, "Failed to cast FPVMState to multithreaded State type")
}
return mtState
}
func RandomState(seed int) *multithreaded.State {
state := multithreaded.CreateEmptyState()
mut := StateMutatorMultiThreaded{state}
mut.Randomize(int64(seed))
return state
}
This diff is collapsed.
This diff is collapsed.
package mipsevm
type PreimageOracle interface {
Hint(v []byte)
GetPreimage(k [32]byte) []byte
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
package register
// FYI: https://en.wikibooks.org/wiki/MIPS_Assembly/Register_File
//
// https://refspecs.linuxfoundation.org/elf/mipsabi.pdf
const (
// syscall number; 1st return value
RegV0 = 2
// syscall arguments; returned unmodified
RegA0 = 4
RegA1 = 5
RegA2 = 6
// 4th syscall argument; set to 0/1 for success/error
RegA3 = 7
// Stack pointer
RegSP = 29
)
// FYI: https://web.archive.org/web/20231223163047/https://www.linux-mips.org/wiki/Syscall
const (
RegSyscallNum = RegV0
RegSyscallErrno = RegA3
RegSyscallRet1 = RegV0
RegSyscallParam1 = RegA0
RegSyscallParam2 = RegA1
RegSyscallParam3 = RegA2
)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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