Runners#
Runners specify execution environments for GenVM contracts.
Runner Architecture#
Identification and Packaging#
A runner is referenced by a runner id, which has one of the following
forms (see the runner-id definition in the runner.json schema):
<human-readable-id>:<hash>— a packaged runner.human-readable-idis provided for convenience;hashis a hash of its contents (see Hash Format).contract— the runner of the contract currently being executed.chain:<address>[:<d|f>[:<slot>]]— a runner code blob read from a storage slot of an arbitrary contract (d= decided,f= finalized).<address>is a0x20 byte hex address and<slot>is a SlotID encoded with GVM32 (Base32). Both<d|f>and<slot>are optional:<d|f>defaults todand<slot>to the target contract’s root code slot.custom:<hash>— a runner registered at runtime via theRegisterRunnergl_call, looked up by its hash.
contract, chain and custom are reserved prefixes and cannot be used
as human-readable ids.
chain: State Visibility#
A chain: id is code, but its resolution is an ordinary
Deterministic Mode storage read and is subject to the same rules:
The read resolves against the state the executing transaction was fixed to by consensus, not against whatever a node’s chain tip happens to be. Every validator of the transaction therefore reads the same octets, whichever Host serves them.
f(finalized) reads the state at the last finalized block;d(decided, the default) reads the decided state — the same view a read-only CallContract Message child observes by default (see Contract Execution Flow). Neither view includes the executing transaction’s own uncommitted writes, so a contract cannot deploy code and load it as a runner in the same transaction.The choice between the two is part of the id string and thus part of the runner graph, never a node-local default.
A resolution that finds no code, or code that is not a valid runner, fails like any other malformed runner rather than falling back to another view.
Hash Format#
hash is a 256-bit hash of the runner’s contents, encoded with
GVM32 (Base32) (GVM32, a lowercase Crockford
Base32). This keeps it free of filesystem-illegal characters and
case-insensitive.
The algorithm differs per id form:
<human-readable-id>:<hash>— SHA-256, matching the hash the runner is packaged and distributed under.custom:<hash>— SHA3-256 of the registered blob.
Runner Layout#
For any of the layouts a file list is constructed. Each entry name:
Must not be empty or start with
/Must use
/as the path separator; backslashes are forbiddenMust not contain empty,
.or..path componentsMust not end with
/unless the entry is a directory
Each file also has contents as a raw byte slice
1. ZIP Archive#
Used if runner bytes represent a ZIP archive
If successful, extracts the archive contents and processes it as a structured runner package
This format supports complex runners with multiple files, dependencies, and configuration
Only allowed compression is
stored(no compression), declared by both the central directory and the entry’s local headerA stored entry’s compressed and uncompressed sizes must be equal
A stored file’s contents must match its declared CRC-32
The local header must agree with the central directory on the entry name, and, unless the entry has a data descriptor (general-purpose flag bit 3), on the CRC-32 and the sizes. The name is compared as raw bytes and must be valid UTF-8, so an entry no two readers would decode alike is rejected rather than silently resolved
Encrypted entries (general-purpose flag bit 0) are rejected
An entry ending with
/is a directory; its size and CRC-32 must be 0, and it is omitted from the file listIf several entries share a name, the file list holds the last of them
2. Raw WASM#
Used if runner bytes represent a wasm file (magic matches)
Creates a minimal runner configuration
version = v0.1.0
runner.json = { "StartWasm": "file" }
file = # source bytes
Both defaults can be overridden by the module itself, through custom sections:
genvm.version supplies the version string and genvm.runner.json the
whole runner.json. This is how a single wasm file declares dependencies
without being repackaged as a ZIP
3. Text-based#
Used if neither of previous worked. Must be a valid utf-8 encoded string
Resulting structure#
version = # first line if started with version, else default
runner.json = # consequent comment lines with removed comment prefix. All whitespaces are kept as-is
file = # source bytes
Example#
# v1.0.0
# {
# "Depends": "python:latest",
# "StartWasm": "python.wasm"
# }
exit(30)
version file#
This file must contain a single line with the version of genvm in v<major>.<minor>.<patch> format.
If this file is not present, the default version is used.
runner.json File#
The runner.json file defines a recursive structure of initialization actions that configure the execution environment for a contract.
Schema is available in runner.json JSON Schema.
It must be a valid JSON object with described below structure
Each action object accepts exactly the fields shown below. The top-level object may also contain a string $schema
annotation
Seq, When and With nest actions. Nesting deeper than
init_action_depth, or a NUL octet in
any action string, is rejected with
invalid_contract runner malformed.
AddEnv#
Adds an environment variable to the GenVM environment with variable interpolation support using ${} syntax.
The name is at most env_name_len
octets and consists of ASCII characters other than =, whitespace and control
characters. The value after interpolation is at most
env_value_len octets. Violating either
is rejected with
invalid_contract runner malformed.
The interpolated value’s length in octets is charged as RAM Consumption; exceeding the budget results in out_of memory.
Example#
{
"AddEnv": {
"name": "DEBUG",
"val": "true"
}
}
MapFile#
Maps files or directories from an archive to specific paths in the GenVM filesystem.
Properties#
file(string): Path within the archive. If ending with/, recursively maps all files in the directoryto(string): Absolute destination path in the GenVM filesystem
Example#
{
"MapFile": {
"file": "config/",
"to": "/etc/myapp/"
}
}
Creating a single file mapping implies RAM Consumption of
file name length in octets
SetArgs#
Sets process arguments for the GenVM environment.
Type: Array of strings
Example#
{
"SetArgs": ["exe-name", "--verbose", "--config", "/path/to/config"]
}
LinkWasm#
Links a WebAssembly file to make it available in GenVM.
Type: String (path to WebAssembly file)
{
"LinkWasm": "path/in/arch/to/module.wasm"
}
If function _initialize is present, it will be called immediately after linking.
StartWasm#
Starts a specific WebAssembly file in GenVM.
Type: String (path to WebAssembly file)
Example#
{
"StartWasm": "path/in/arch/to/module.wasm"
}
This is a terminal action in the runner configuration. It results in linking the module and calling _start function.
Depends#
Specifies a dependency on another runner by its ID and hash.
Example#
{
"Depends": "cpython:123"
}
Dependencies are processed only once, for the first request
Seq#
Executes a sequence of initialization actions.
{
"Seq": [
{ "SetArgs": ["exe-name", "--verbose", "--config", "/path/to/config"] },
{ "StartWasm": "path/in/arch/to/module.wasm" }
]
}
When#
Conditionally executes an action based on WebAssembly execution mode.
Properties#
cond: WebAssembly mode, eitherdet(deterministic) or!det(non-deterministic)action: Action to execute when condition is met
Example#
{
"When": {
"cond": "det",
"action": { "AddEnv": {"name": "MODE", "val": "deterministic"} }
}
}
With#
Sets a runner as current without executing its action, useful for reusing files or creating runner locks.
Example#
{
"With": {
"runner": "base-environment",
"action": { "MapFile": {"file": "patched.foo", "to": "foo" } }
}
}
Startup#
Runner actions are executed left-recursively, until StartWasm is reached.
If it was not reached, it will result in a VMError with
invalid_contract runner malformed code.
Loading a runner goes through a single load action, defined per sub-VM. Each sub-VM owns a loaded-runner set: the runner ids it has already loaded. The load action for an id is:
if the id is already in the sub-VM’s loaded set, nothing is charged;
otherwise, when the loaded set already holds max_runners ids, the load fails with out_of memory and nothing is charged — a count cap is refused exactly like an exhausted RAM budget;
otherwise runner_load_cost plus the runner’s raw size and metadata cost in octets is charged as RAM Consumption against the sub-VM’s RAM budget, and the id is then added to the loaded set.
The raw size is the length of the runner’s code or archive bytes. For a ZIP runner, the metadata cost is the sum of zip_file_cost plus the UTF-8 filename length in octets for each distinct non-directory entry. Repeated filenames count once, using the last entry. Non-ZIP runners have no additional metadata charge. Insufficient RAM for either charge exits the sub-VM with out_of memory
Whether the executor has the archive cached internally is not observable: the charge depends only on the sub-VM’s own load history, never on cache state. The same runner loaded by different sub-VM instances is charged once per sub-VM. Deterministic Mode and Non-Deterministic Mode have separate RAM budgets and separate loaded sets.
A load action occurs when:
spawning the sub-VM’s main (entry-point) runner;
resolving a
DependsorWithaction in arunner.json;executing the
MapFilegl_call;registering a runner via the
RegisterRunnergl_call;receiving a custom-runner grant at sub-VM creation (see Custom-Runner Grants).
For a chain: runner the raw size is the length of the code blob read from
storage. The same content has the same load charge regardless of its source;
there is no doubled charge and no separate fee component
Custom Runner Loading#
A custom:<hash> id resolves iff <hash> is in the resolving
sub-VM’s own loaded set; otherwise loading it fails with a
VMError. There is no separate registry lookup at resolution — a
sub-VM can use exactly the custom runners it has loaded, whether by
registering them itself via the RegisterRunner gl_call or by receiving
grants from its parent at creation time
(Custom-Runner Grants).
Registered content lives while at least one sub-VM has it loaded; once
no loaded set holds it, it is freed. Registering the same code again while
it is still loaded somewhere is deduplicated by hash; re-registering it after it
has been freed re-parses and charges again.
Comment Header Format#
The contract source code must begin with comment lines using one of the supported comment syntaxes:
//(C-style comments)#(Shell/Python-style comments)--(SQL/Lua/Haskell-style comments)The comment header consists of:
Version Line (first comment line): Must start with
vfollowed by version information:term:`Runner` Configuration (subsequent comment lines): JSON configuration for the runner