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.
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:
🧪 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.
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.
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. 5RunningStatus=iota 6 7// Halted means the program reached its end. Terminal. 8Halted 910// Trapped means the guest did something the machine refuses to do:11// an invalid instruction, an out-of-range access, a capability it was12// not granted. Terminal.13Trapped1415// OutOfFuel means the instance exhausted its total budget, not just the16// fuel for this slice. Terminal unless the owner raises the budget.17OutOfFuel18)
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.
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.
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".
1typeHostinterface{ 2// Caller is the address that called the realm running this guest. 3Caller()address 4// Origin is the address that signed the transaction. 5Origin()address 6// Now is block time in Unix seconds, never wall time. 7Now()int64 8// Height is the block height. 9Height()int641011// Get reads from storage scoped to this instance. A miss is nil.12Get(key[]byte)[]byte13// Set writes to storage scoped to this instance.14Set(key,val[]byte)1516// Input is the immutable call input for this slice of execution: the17// guest's calldata, argv, or stdin depending on the machine. A machine18// tracks its own read cursor, in its snapshot, so that resuming reads19// the byte it had not read yet.20Input()[]byte21// Output appends to the guest's output buffer.22Output(p[]byte)2324// Emit writes a chain event. kv is a flat list of alternating keys and25// values; an odd trailing element is dropped.26Emit(typstring,kv...string)27// Send transfers amount ugnot to `to`, and returns [ErrNotGranted]28// unless the deploying realm funded this instance with a budget.29Send(toaddress,amountint64)error30// Log records a diagnostic line. Never consensus-relevant.31Log(msgstring)32}
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.
1typeInstancestruct{ 2IDstring 3Owneraddress 4VMstring// which guest machine the Program is for 5 6Program[]byte 7Snapshot[]byte 8 9StatusStatus10Trapstring1112// FuelUsed is the total across every slice run so far.13FuelUsedint6414// FuelBudget caps FuelUsed across the instance's whole life, or is15// [Unmetered] for no cap.16FuelBudgetint641718// Slices counts how many transactions have stepped this instance.19Slicesint642021Output[]byte22}
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.
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.
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.
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.
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.
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.
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.