Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

v0 source pure

Package vmkit is the host ABI shared by every guest virtual machine that runs inside a gno realm: a stepping contract...

Readme View source

p/moul/x/vm/vmkit

The host ABI shared by every guest virtual machine that runs inside a gno realm: a stepping contract with an explicit fuel budget, a capability-scoped Host, and a snapshot format that lets a guest program outlive the transaction that started it.

A guest VM implements Machine. Everything else here is the machinery a VM should not have to write twice: the Meter, the canonical Writer/Reader snapshots are built from, an in-memory TestHost, and the avl-backed Store a realm keeps its instances in.

First consumer: p/moul/x/vm/bf. Live demo: r/moul/x/vm/bfdemo.

1type Machine interface {
2	Step(h Host, fuel int64) (used int64, status Status)
3	Snapshot() []byte
4	Restore(b []byte) error
5}

Status is one of Running, Halted, Trapped, OutOfFuel.

What the ABI is for

Fuel is the interface, not the backstop. Running out of fuel yields OutOfFuel plus a machine that can be snapshotted, never a panic. Gas still bounds the transaction; it is just not the thing a guest program is written against.

Continuations. Snapshot and Restore mean a program runs across blocks. The realm stores the bytes, the next caller pays for the next slice. Gno realm code cannot pause itself; a guest can. The property that makes this real is tested rather than asserted: five slices of one fuel unit must produce exactly what one slice of five produces, for every program in bf's corpus.

Capabilities, not ambient authority. A guest gets exactly the Host it was handed. Send returns ErrNotGranted unless the deploying realm funded a budget, storage is scoped to the instance, and nothing is looked up. Gno itself has ambient authority through the realm frame, so the guest is where the capability-secure version can actually be tried.

Meter is not a hot-path type

Meter exists for the API boundary: compute a slice budget, charge it once, report what was used. Calling Meter.Charge once per guest instruction was measured at +86% on top of an entire interpreter dispatch loop, and more than tripled its allocation count. A machine should count fuel in a local and settle up with the caller. The numbers are in bf's README.

Charge spends nothing when the budget cannot cover the request, so a machine that stops for lack of fuel is exactly at the instruction it could not pay for, and resuming re-executes that instruction and no other.

The snapshot codec

Writer and Reader are fixed-width big-endian with length-prefixed bytes, so the encoding is canonical: the same machine state always produces the same bytes, on every node. A snapshot is consensus state, so two nodes encoding it differently is a fork, and a short read is a hard error rather than a zero value.

Machines are not required to use it, but a machine that invents its own layout owes the zoo an explanation: the cross-VM snapshot cost comparison only means something when the encodings match.

Instance and Store

Instance is one guest program as a realm stores it: code, snapshot between slices, status, and the fuel accounting that survives the transaction. It holds bytes rather than a Machine on purpose, which is what makes the storage cost of a paused program measurable.

Instance.Run folds one slice back into the instance and leaves it untouched when Restore fails, so a snapshot that cannot be decoded costs the caller gas but never corrupts the stored program.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

gno.land/p/moul/x/vm/vmkit/v0 dependency graph

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.

Overview

Package vmkit is the host ABI shared by every guest virtual machine that runs inside a gno realm: a stepping contract with an explicit fuel budget, a capability-scoped Host, and a snapshot format that lets a guest program outlive the transaction that started it.

A guest VM implements Machine. Everything else in this package is the machinery a VM should not have to write twice: the Meter, the [Codec] the snapshots are built from, an in-memory TestHost, and the avl-backed Store a realm keeps its instances in.

The three things the ABI exists to fix:

  • Fuel is the interface, not the backstop. Running out of fuel yields OutOfFuel plus a machine that can be snapshotted, never a panic. Gas still bounds the transaction, but a guest that stops is a normal outcome.
  • Continuations. Snapshot and Restore mean a program runs across blocks. The realm stores the bytes, and the next caller pays for the next slice. Gno realm code itself cannot pause; a guest can.
  • Capabilities, not ambient authority. A guest gets exactly the Host it was handed. No Send without a grant, no storage outside its own scope.

Live demo: r/moul/x/vm/bfdemo(/r/moul/x/vm/bfdemo/v0), running the p/moul/x/vm/bf(/p/moul/x/vm/bf/v0) guest.

Constants 2

const Unmetered

1const Unmetered int64 = -1
source

Unmetered is the fuel value that disables the budget: a Meter built with it never reports exhaustion. It exists so a benchmark can measure the dispatch loop without the meter in it, and so a trusted caller can run a program to completion in one slice. Gas remains the real backstop.

const Running, Halted, Trapped, OutOfFuel

 1const (
 2	// Running means the machine stopped because it ran out of the fuel
 3	// handed to this slice, but the program has not finished. It is
 4	// resumable: snapshot it, and step it again later.
 5	Running Status = iota
 6
 7	// Halted means the program reached its end. Terminal.
 8	Halted
 9
10	// Trapped means the guest did something the machine refuses to do:
11	// an invalid instruction, an out-of-range access, a capability it was
12	// not granted. Terminal.
13	Trapped
14
15	// OutOfFuel means the instance exhausted its total budget, not just the
16	// fuel for this slice. Terminal unless the owner raises the budget.
17	OutOfFuel
18)
source

Variables 5

var ErrBadSnapshot

1var ErrBadSnapshot = errors.New("vmkit: snapshot is not for this machine")
source

ErrBadSnapshot is returned when a snapshot is well-formed but not for this machine: wrong magic, wrong version, or a field outside its legal range.

var ErrBudgetExhausted

1var ErrBudgetExhausted = errors.New("vmkit: instance fuel budget exhausted")
source

ErrBudgetExhausted is returned when an instance has already spent its total fuel budget, so there is nothing left to step.

var ErrNoInstance

1var ErrNoInstance = errors.New("vmkit: no such instance")
source

ErrNoInstance is returned when an id names nothing.

var ErrNotGranted

1var ErrNotGranted = errors.New("vmkit: capability not granted")
source

ErrNotGranted is returned by a Host for a capability the instance was never given. A guest that ignores the error and keeps going is a guest bug; a machine should turn it into a Trapped status.

var ErrTruncated

1var ErrTruncated = errors.New("vmkit: truncated snapshot")
source

ErrTruncated is returned by every Reader method that runs past the end of the buffer. A snapshot is consensus state, so a short read is always a hard error and never a zero value.

Functions 7

func TrapReason

1func TrapReason(m Machine) string
source

TrapReason returns m's trap reason when m implements Trapper, else "".

func NewInstance

1func NewInstance(id string, owner address, vm string, program []byte, budget int64) *Instance
source

NewInstance returns an instance ready for its first slice.

func NewMeter

1func NewMeter(budget int64) *Meter
source

NewMeter returns a meter good for `budget` units, or an unmetered one when budget is Unmetered. A budget of zero is a meter with nothing to spend, which is a legitimate way to ask "is this instance still runnable".

func NewReader

1func NewReader(b []byte) *Reader
source

NewReader returns a Reader over b.

func NewStore

1func NewStore() *Store
source

NewStore returns an empty store.

func NewTestHost

1func NewTestHost() *TestHost
source

NewTestHost returns a host with no capabilities granted, height 1, time 0, and empty input.

func NewWriter

1func NewWriter(n int) *Writer
source

NewWriter returns a Writer with room for n bytes reserved up front.

Types 11

type Host

interface
 1type Host interface {
 2	// Caller is the address that called the realm running this guest.
 3	Caller() address
 4	// Origin is the address that signed the transaction.
 5	Origin() address
 6	// Now is block time in Unix seconds, never wall time.
 7	Now() int64
 8	// Height is the block height.
 9	Height() int64
10
11	// Get reads from storage scoped to this instance. A miss is nil.
12	Get(key []byte) []byte
13	// Set writes to storage scoped to this instance.
14	Set(key, val []byte)
15
16	// Input is the immutable call input for this slice of execution: the
17	// guest's calldata, argv, or stdin depending on the machine. A machine
18	// tracks its own read cursor, in its snapshot, so that resuming reads
19	// the byte it had not read yet.
20	Input() []byte
21	// Output appends to the guest's output buffer.
22	Output(p []byte)
23
24	// Emit writes a chain event. kv is a flat list of alternating keys and
25	// values; an odd trailing element is dropped.
26	Emit(typ string, kv ...string)
27	// Send transfers amount ugnot to `to`, and returns [ErrNotGranted]
28	// unless the deploying realm funded this instance with a budget.
29	Send(to address, amount int64) error
30	// Log records a diagnostic line. Never consensus-relevant.
31	Log(msg string)
32}
source

Host is everything a guest program can reach outside its own memory.

It is deliberately small, and deliberately handed in rather than looked up: a guest has exactly the authority its Host carries. Gno realm code has ambient authority through the realm frame, so the guest is where the capability-secure version can actually be tried.

Every method must be deterministic across replays of the same block. Now and Height come from the chain, never from a wall clock.

type Instance

struct
 1type Instance struct {
 2	ID    string
 3	Owner address
 4	VM    string // which guest machine the Program is for
 5
 6	Program  []byte
 7	Snapshot []byte
 8
 9	Status Status
10	Trap   string
11
12	// FuelUsed is the total across every slice run so far.
13	FuelUsed int64
14	// FuelBudget caps FuelUsed across the instance's whole life, or is
15	// [Unmetered] for no cap.
16	FuelBudget int64
17
18	// Slices counts how many transactions have stepped this instance.
19	Slices int64
20
21	Output []byte
22}
source

Instance is one guest program as a realm stores it: the code, the machine state between slices, and the accounting that survives the transaction.

It holds a snapshot rather than a Machine, on purpose. A realm keeps bytes; the machine is rebuilt for the slice that needs it and thrown away after. That is what makes the storage cost of a paused program measurable, which is the whole kill criterion for continuations: if resuming costs more than re-running, continuations are theater.

Methods on Instance

func Remaining

method on Instance
1func (i *Instance) Remaining() int64
source

Remaining reports the fuel left in the instance's total budget, or Unmetered.

func Run

method on Instance
1func (i *Instance) Run(m Machine, h Host, fuel int64) error
source

Run steps m for one slice of at most `fuel` units and folds the result back into the instance: the new snapshot, the status, the fuel spent, and anything the guest wrote to h.

m must be a fresh machine loaded from i.Program; Run restores i.Snapshot into it when there is one, so the caller never has to remember the order. The instance is left untouched when Restore fails, which means a snapshot that cannot be decoded costs the caller gas but never corrupts the stored program.

func Slice

method on Instance
1func (i *Instance) Slice(want int64) int64
source

Slice returns how much fuel the next call to Instance.Run may spend when the caller asks for `want`: `want` clamped to what the budget still allows.

type Machine

interface
1type Machine interface {
2	Step(h Host, fuel int64) (used int64, status Status)
3	Snapshot() []byte
4	Restore(b []byte) error
5}
source

Machine is one guest virtual machine, mid-execution.

Step runs until the program halts, traps, or burns `fuel` units, whichever comes first, and reports how much fuel it actually used. A Machine must charge at least one unit per guest instruction so that a fuel budget is a real bound on work; beyond that the unit is the VM's own business, and r/moul/x/vm/bfdemo(/r/moul/x/vm/bfdemo/v0) compares them by measurement rather than by trusting the number.

Snapshot must round-trip through Restore: a machine stepped to exhaustion, snapshotted, restored and stepped again must produce exactly what the same machine stepped in one go would have. That property is what makes a guest program a contract instead of a function call.

type Meter

struct
1type Meter struct {
2	budget int64 // Unmetered, or the number of units this slice may spend
3	used   int64
4}
source

Meter is a fuel budget being spent. It allocates nothing after construction, because it sits in the hot path of every guest instruction.

Methods on Meter

func Charge

method on Meter
1func (m *Meter) Charge(n int64) bool
source

Charge spends n units and reports whether they were available. On false nothing is spent, so the caller can stop before executing the instruction it could not pay for. That ordering is what makes a resumed machine re-execute exactly the instruction it stopped at, and no other.

func Exhausted

method on Meter
1func (m *Meter) Exhausted() bool
source

Exhausted reports whether the next single unit would fail.

func Remaining

method on Meter
1func (m *Meter) Remaining() int64
source

Remaining reports what is left, or Unmetered.

func Used

method on Meter
1func (m *Meter) Used() int64
source

Used reports how much fuel has been spent.

type Reader

struct
1type Reader struct {
2	buf []byte
3	pos int
4	err error
5}
source

Reader consumes a snapshot written by Writer.

It latches the first error it hits: a caller may decode a whole struct and check Reader.Err once at the end, instead of after every field. Every method returns a zero value once the Reader is in error.

Methods on Reader

func Byte

method on Reader
1func (r *Reader) Byte() byte
source

Byte reads one byte.

func Bytes

method on Reader
1func (r *Reader) Bytes() []byte
source

Bytes reads a length-prefixed byte slice. The result is a copy, so a machine can keep it without aliasing the snapshot it was handed.

func Err

method on Reader
1func (r *Reader) Err() error
source

Err returns the first error hit, or nil.

func Fail

method on Reader
1func (r *Reader) Fail(err error)
source

Fail latches err, so a machine can reject a field its own rules forbid and have it surface through Reader.Err like any decoding failure.

func Int

method on Reader
1func (r *Reader) Int() int64
source

Int reads a zig-zag encoded signed value written by Writer.Int.

func Remaining

method on Reader
1func (r *Reader) Remaining() int
source

Remaining reports how many bytes are left unread.

func String

method on Reader
1func (r *Reader) String() string
source

String reads a length-prefixed string.

func Uint32

method on Reader
1func (r *Reader) Uint32() uint32
source

Uint32 reads a 4-byte big-endian value.

func Uint64

method on Reader
1func (r *Reader) Uint64() uint64
source

Uint64 reads an 8-byte big-endian value.

type Status

ident
1type Status int
source

Status is the outcome of a call to [Machine.Step].

Methods on Status

func Done

method on Status
1func (s Status) Done() bool
source

Done reports whether the status is terminal, i.e. stepping again is pointless without operator intervention.

func String

method on Status
1func (s Status) String() string
source

String renders the status as the lowercase word used in realm output.

type Store

struct
1type Store struct {
2	tree *avl.Tree
3}
source

Store is the avl-backed set of instances a realm owns, keyed by id.

Methods on Store

func Get

method on Store
1func (s *Store) Get(id string) *Instance
source

Get returns the instance with this id, or nil.

func Iterate

method on Store
1func (s *Store) Iterate(fn func(*Instance) bool)
source

Iterate walks every instance in key order, stopping early when fn returns true.

func Remove

method on Store
1func (s *Store) Remove(id string) bool
source

Remove deletes an instance and reports whether it existed.

func ReverseIterate

method on Store
1func (s *Store) ReverseIterate(fn func(*Instance) bool)
source

ReverseIterate walks every instance in descending key order, which is how a realm renders newest-first when ids are zero-padded and ascending.

func Set

method on Store
1func (s *Store) Set(i *Instance)
source

Set writes an instance, replacing any instance with the same id.

func Size

method on Store
1func (s *Store) Size() int
source

Size returns how many instances are stored.

type TestHost

struct
 1type TestHost struct {
 2	caller address
 3	origin address
 4	now    int64
 5	height int64
 6
 7	store *avl.Tree // hex key -> []byte
 8
 9	in     []byte
10	out    []byte
11	events []string
12	logs   []string
13
14	sendBudget int64
15	sends      []Transfer
16}
source

TestHost is a deterministic in-memory Host for unit tests and for measuring a machine without a chain under it.

Nothing here reads the chain, so a test that uses it produces the same numbers on every host. Storage is an avl tree rather than a map because gno map iteration order is unspecified, and TestHost.Keys has to be stable for a test to assert on it.

Send is denied unless TestHost.Grant funded a budget, which is how the capability rule gets tested: a guest that tries to send without a grant must come back Trapped, not silently succeed.

Methods on TestHost

func Caller

method on TestHost
1func (h *TestHost) Caller() address
source

func Emit

method on TestHost
1func (h *TestHost) Emit(typ string, kv ...string)
source

func Events

method on TestHost
1func (h *TestHost) Events() []string
source

Events returns the rendered events, in emission order.

func Get

method on TestHost
1func (h *TestHost) Get(key []byte) []byte
source

func Grant

method on TestHost
1func (h *TestHost) Grant(budget int64) *TestHost
source

Grant funds the send capability with a budget in ugnot. Without it, Send returns ErrNotGranted.

func Height

method on TestHost
1func (h *TestHost) Height() int64
source

func Input

method on TestHost
1func (h *TestHost) Input() []byte
source

func Keys

method on TestHost
1func (h *TestHost) Keys() []string
source

Keys returns every storage key the guest wrote, hex-encoded, in sorted order.

func Log

method on TestHost
1func (h *TestHost) Log(msg string)
source

func Logs

method on TestHost
1func (h *TestHost) Logs() []string
source

Logs returns the diagnostic lines, in emission order.

func Now

method on TestHost
1func (h *TestHost) Now() int64
source

func Origin

method on TestHost
1func (h *TestHost) Origin() address
source

func Out

method on TestHost
1func (h *TestHost) Out() []byte
source

Out returns everything the guest has written, as bytes.

func OutString

method on TestHost
1func (h *TestHost) OutString() string
source

OutString returns everything the guest has written, as a string.

func Output

method on TestHost
1func (h *TestHost) Output(p []byte)
source

func ResetOut

method on TestHost
1func (h *TestHost) ResetOut()
source

ResetOut discards the output buffer, so one host can measure several runs.

func Send

method on TestHost
1func (h *TestHost) Send(to address, amount int64) error
source

func Sends

method on TestHost
1func (h *TestHost) Sends() []Transfer
source

Sends returns the transfers that succeeded, in order.

func Set

method on TestHost
1func (h *TestHost) Set(key, val []byte)
source

func WithCaller

method on TestHost
1func (h *TestHost) WithCaller(a address) *TestHost
source

WithCaller sets the address the guest sees as its caller and origin.

func WithHeight

method on TestHost
1func (h *TestHost) WithHeight(n int64) *TestHost
source

WithHeight sets the block height.

func WithInput

method on TestHost
1func (h *TestHost) WithInput(p []byte) *TestHost
source

WithInput sets the guest's call input.

func WithOrigin

method on TestHost
1func (h *TestHost) WithOrigin(a address) *TestHost
source

WithOrigin overrides the origin separately from the caller.

func WithTime

method on TestHost
1func (h *TestHost) WithTime(t int64) *TestHost
source

WithTime sets the block time in Unix seconds.

type Trapper

interface
1type Trapper interface {
2	// Trap returns the reason for a [Trapped] status, or "" when the
3	// machine has not trapped.
4	Trap() string
5}
source

Trapper is an optional refinement of Machine: a machine that can explain why it trapped. Kept out of Machine so the core ABI stays three methods.

type Writer

struct
1type Writer struct {
2	buf []byte
3}
source

Writer builds a snapshot. Every integer is fixed-width big-endian and every byte slice is length-prefixed, so the encoding is canonical: the same machine state always produces the same bytes, on every node.

Machines are not required to use it, but a machine that invents its own layout owes the zoo an explanation, because the cross-VM snapshot cost comparison only means something when the encodings match.

Methods on Writer

func Byte

method on Writer
1func (w *Writer) Byte(b byte)
source

Byte appends one byte.

func Bytes

method on Writer
1func (w *Writer) Bytes(p []byte)
source

Bytes appends a length-prefixed byte slice.

func Int

method on Writer
1func (w *Writer) Int(v int64)
source

Int appends a signed value, zig-zag encoded into a Uint64 so that small negative numbers do not cost eight 0xff bytes.

func Out

method on Writer
1func (w *Writer) Out() []byte
source

Out returns the encoded bytes.

func String

method on Writer
1func (w *Writer) String(s string)
source

String appends a length-prefixed string.

func Uint32

method on Writer
1func (w *Writer) Uint32(v uint32)
source

Uint32 appends a 4-byte big-endian value.

func Uint64

method on Writer
1func (w *Writer) Uint64(v uint64)
source

Uint64 appends an 8-byte big-endian value.

Imports 2

Source Files 9