VM(Py) ABI — Call/Data Encoding & Event Topics
The canonical byte encoding for function calls, return values and event topics, so SDKs and nodes agree on every contract interaction.
5 min read 1,134 words
View docs/vm/ABI.md on GitHub
Source: docs/vm/ABI.md — this page mirrors the repository documentation.
Status: Stable (v1)
Audience: Contract authors, SDK implementers, node/runtime integrators
Scope: Canonical, deterministic byte encoding for function calls/returns and event topics used by VM(Py).
TL;DR: Calls and returns use a small, self-contained codec (no JSON/CBOR). Scalars are length-prefixed where variable, big-endian for integers, and strictly bounded. Events have a stable topic scheme and carry canonical data bytes.
1) Design Goals
- Deterministic & canonical. Identical inputs → identical bytes and gas everywhere.
- Small surface. A few scalar types + tuples/arrays, no dynamic reflection or schemas at runtime.
- Language-agnostic. Friendly to Python/TS/Rust SDKs; no dependency on host endianness.
- Stable function selection. Collision-resistant selector tied to the ABI signature.
2) Primitives
This ABI defines a compact wire format with the following building blocks:
2.1 Unsigned Varint (uvarint)
- Format: LEB128 (little-endian base-128) for non-negative integers.
- Range: 0…2^64-1 for lengths/counts; larger values are invalid.
- Determinism: Minimal form only (no leading 0x80 “continuations” beyond necessity).
2.2 Integers (int)
- Domain: Unsigned, 0…2^256-1 (cap enforced by validator and runtime).
- Encoding:
uvarint(L)followed by big-endianLbytes.- Zero is encoded with
L=0and no payload bytes. - Canonical: No leading zero byte permitted (except zero above).
- Zero is encoded with
2.3 Boolean (bool)
- Single byte:
0x00(False) or0x01(True). Any other value invalid.
2.4 Bytes (bytes)
uvarint(len)+ raw bytes;lenmay be 0. Max length is policy-bound (see caps).
2.5 Address (address)
- Logical form: (alg_id:
u8, pubkey_hash:sha3_25632-byte). - Binary payload: 33 bytes =
alg_id || pubkey_hash. - Encoding: As bytes with
len=33.- SDKs map bech32m
anim1…⇄ 33-byte payload; on-chain is always the 33-byte binary.
- SDKs map bech32m
2.6 Arrays / Tuples
- Tuple:
uvarint(n)thenencode(v0) … encode(v{n-1})(positional; heterogeneous allowed). - Array[T]:
uvarint(n)thenencode_Trepeated (homogeneous).- Canonical:
nmust match actual element count; nested caps apply recursively.
- Canonical:
No strings at the ABI level; use
byteswith a known text encoding if needed (e.g., UTF-8).
3) Function Selectors & Call Data
3.1 Function Selector (selector)
- Signature string:
name "(" comma_join(param_types) ")" "->" comma_join(return_types)
Examples:inc()->get()->inttransfer(address,int)->bool
- Selector: First 8 bytes of
sha3_256( b"fn:" || signature_utf8 ).- 64-bit truncation balances space with negligible collision risk in practice.
- Stable across language/toolchain updates if the ABI signature is constant.
3.2 Call Data
call_data := selector(8) || args_tuple args_tuple := tuple encoded via §2.6 with each argument encoded by its type rules
- No additional length prefix around the whole call.
- Empty args →
tuple(n=0)(encoded as a singleuvarint(0)).
3.3 Return Data
ret_data := results_tuple
results_tuplefollows the same tuple rules as arguments.
3.4 Errors & Reverts
abi.require(cond, msg)andabi.revert(msg)cause the VM to signal REVERT.- Return bytes on REVERT:
bytes(msg_utf8)(ABI-encoded bytes) are available to callers/simulators. - On-chain, REVERT status is reflected in receipts; payload may appear in debug/SIM RPCs.
4) Events: Topics & Data
Contracts emit events with:
events.emit(name: bytes, args: dict[str,
4.1 Topic Scheme
To keep topics compact and filterable while avoiding oversized logs:
- topic[0] = sha3_256( b”event:” || name )[:32]
- topic[1] = sha3_256( canonical_args_bytes )[:32]
Where:
- canonical_args_bytes = ABI encoding of a stable map representation:
- Sort args by key (bytewise ascending of key.encode(‘utf-8’)).
- Encode as a tuple of pairs: uvarint(k) entries of (bytes(key_utf8), encode(value)).
Rationale: topic[0] gives a stable identity per event kind; topic[1] enables quick bloom/filtering for “this exact payload” without exploding topic count. Full details live in log data (below).
4.2 Event Data
- data = the same canonical_args_bytes described above.
- Determinism: Keys must be valid UTF-8; duplicate keys are invalid.
Bloom and receipts layout are specified in docs/spec/RECEIPTS_EVENTS.md; this doc only fixes event ABI bytes.
5) Examples & Test Vectors
5.1 Scalars
- int = 0
- L=0 → 00
- int = 0x01
- L=1 → 01 | 01
- int = 0x0102
- L=2 → 02 | 01 02
- bool True / False
- 01 / 00
- bytes(b””) → 00
- bytes(b”\xDE\xAD”) → 02 | DE AD
- address (alg_id=0x01, hash=00…0F 32 bytes) → 21 | 01 00 00 … 0F (0x21 = 33)
5.2 inc() (no args, no return)
- selector: sha3_256(“fn:inc()->”)[:8]
- Suppose selector = 12 34 56 78 9A BC DE F0
- args_tuple: uvarint(0) → 00
- call_data hex: 12 34 56 78 9A BC DE F0 00
5.3 get()->int (no args, returns an int)
- selector: sha3_256(“fn:get()->int”)[:8] → e.g., AA…
- args_tuple: 00
- ret_data (e.g., returns 1): int(1) → 01 | 01
5.4 Event
events.emit(b”Inc”, {“value”: 1})
- topic[0] = sha3_256(b”event:Inc”)
- Canonical args bytes:
- Sorted pairs: only (“value”, 1)
- Tuple count uvarint(1) = 01
- key bytes(“value”) = 05 | 76 61 6C 75 65
- value int(1) = 01 | 01
- data hex: 01 05 76 61 6C 75 65 01 01
- topic[1] = sha3_256(data)
6) Caps & Limits (enforced by validator/runtime)
- int: ≤ 256 bits.
- bytes: max length configurable; defaults recommended ≤ 64 KiB per value.
- tuples/arrays: max element count (e.g., 1024) and nesting depth (e.g., 8).
- event args total encoded size: bounded by receipt/logs policy.
Exact values are network policy; see execution/specs/GAS.md and docs/spec/CAPABILITIES.md for gas implications.
7) SDK Mapping (Guidance)
- Python:
- int ⇄ Python int with bound checks.
- bytes ⇄ bytes.
- address ⇄ helper struct with alg_id and hash, plus bech32m codec.
- TypeScript:
- int as bigint; encode to BE bytes without leading zero; zero → empty bytes w/ L=0.
- bytes as Uint8Array.
- address as { alg: number; hash: Uint8Array(32) } plus bech32m conversion.
- Rust:
- int as Uint<256> or biguint checked; serialize BE.
- address as struct { alg: u8, hash: [u8;32] }.
8) Compatibility Notes
- ABI is not EVM ABI: no 32-byte slot alignment, no function name hashing to 4 bytes, no packed/strict sizing quirks.
- Function selector uses 8 bytes from SHA3-256 and includes returns in the signature to minimize accidental reuse.
- The ABI is stable at v1; any breaking change will bump the major version and gate via network upgrades.
9) Reference Implementations
- Encoder/decoder utilities in:
- vm_py/runtime/abi.py, vm_py/abi/{encoding,decoding}.py
- SDKs: sdk/python/omni_sdk/tx/encode.py, sdk/typescript/src/tx/encode.ts, sdk/rust/src/tx/encode.rs
- Event helpers: execution/state/events.py, execution/receipts/logs_hash.py
10) Validation Checklist
- Integers minimally encoded (no leading zeros, zero uses L=0).
- Arrays/tuples counts correct and within caps.
- Address is exactly 33 bytes (alg_id + hash).
- Function selector matches the signature including returns.
- Event args keys are unique, UTF-8, lexicographically sorted in the canonical payload.
This page mirrors a file in the animicaorg/all repository. If the repository and this page ever disagree, the repository is authoritative. For long-form explainers written for newcomers, see Learn.