XChain VM — Architecture
Execution Pipeline
Contract Source Code
|
acorn (parse AST, ES2020)
|
metering.js (inject __gas() calls at control flow points)
|
astring (regenerate source from modified AST)
|
isolated-vm (V8 isolate)
|-- sandbox.js (strip non-deterministic globals)
|-- gateway.js (inject xchain object via JSON bridge protocol)
|-- gas.js (__gas -> chargeComputation on host side)
'-- script.runSync() (execute with wall-clock timeout)
|
Collect results
|-- state.js -> stateChanges, stateDeletes
|-- collector.js -> emittedActions, logs
'-- gas.js -> gasUsed
|
Return to indexer (execute.js)
Internal Components
| File | Purpose |
|---|---|
index.js |
XChainVM class — main entry point, orchestrates isolate lifecycle, injects gateway, compiles and executes contracts |
isolate.js |
V8 isolate creation (createIsolate), throwaway isolate for syntax validation (createThrowawayIsolate), compilation (compileScript), cached data extraction, safe disposal |
sandbox.js |
Strips non-deterministic globals (Date, Math.random, setTimeout, etc.), replaces Math with frozen deterministic subset, preserves Function reference for contract wrapper |
metering.js |
AST-based gas injection — parses source with acorn, injects __gas() at control flow points, regenerates with astring. Also provides hasGasIdentifier() for deploy-time validation |
gas.js |
GasTracker class — validates gas schedule (non-negative integers), accumulates gas charges per operation, enforces ceiling, throws GasExhaustedError on overflow |
gateway.js |
Builds the xchain gateway object — context accessors, state CRUD, ledger queries, oracle, cross-chain, external attestation (xchain.attestation.*), contract-targeted staking (xchain.contract.*), emit API, math, control flow, logging |
gateway-emit.js |
Emit API builder — 16 action types (SEND through MESSAGE), parameter validation, gas charging |
math.js |
Deterministic math wrapping mathjs bignumber — all inputs/outputs are strings, wrapped in safeMath for ContractRevertError on failures |
state.js |
StateManager — reads from initial snapshot, tracks writes/deletes in dirty map, enforces key count, key size, and value size limits, provides getChanges() for result collection |
collector.js |
EmissionCollector — queues emitted actions (with emission cap), collects debug logs (100 entries, 1 KB UTF-8 each, with byte-aware truncation) |
validator.js |
ActionValidator — pre-validates emitted actions against the 16 allowed action types and checks params shape |
syntax.js |
Deploy-time validation — V8 syntax check (throwaway isolate), acorn metering pass (ES2020 ceiling), reserved __gas identifier detection, float literal warnings |
errors.js |
ContractRevertError (thrown by revert()/require()) and GasExhaustedError (thrown when gas ceiling exceeded) |
JSON Bridge Protocol
The V8 isolate boundary in isolated-vm only allows primitive values (strings, numbers, booleans, null) to cross via applySync. Objects, arrays, and functions cannot be transferred directly. The VM uses a JSON-based bridge protocol to work around this limitation.
How It Works
Each host-side gateway function is wrapped in a bridge() helper and injected into the isolate as an ivm.Reference. Inside the isolate, a corresponding wrap() helper converts each Reference into a callable function.
Calling a gateway method (isolate → host):
- Contract calls
xchain.state.get('key') - The isolate-side
wrap()function JSON-serializes the arguments:'["key"]' ref.applySync(undefined, ['["key"]'])sends the JSON string to the host- The host-side
bridge()function parses the arguments:JSON.parse('["key"]')→['key'] - The host calls the actual gateway function:
stateManager.get('key') - The return value is JSON-encoded with a
\x01prefix:'\x01{"count":"5"}'(all non-null/undefined returns use this encoding to prevent user data containing control characters from being misinterpreted as protocol markers) - The isolate-side
wrap()detects the\x01prefix andJSON.parses the result
Return value encoding:
| Prefix | Meaning | Example |
|---|---|---|
\x01 |
JSON-encoded return value from gateway method (any type) | '\x01"hello"', '\x01["a","b"]', '\x0142' |
\x02 |
JSON-encoded contract return value (from CONTRACT_WRAPPER) | '\x02{"count":"5"}' |
\x03 |
Typed error encoding | '\x03REVERT:not enough tokens' |
Error encoding:
When a gateway method throws a typed error, the bridge() wrapper catches it and re-throws with a type prefix so _classifyError can identify it after the error loses its class crossing the boundary:
| Error Type | Encoded Message |
|---|---|
ContractRevertError |
\x03REVERT:<reason> |
GasExhaustedError |
\x03GAS:<used>:<ceiling> |
Error classification hardening: To prevent contracts from spoofing error types by throwing new Error('\x03GAS:...') directly, _classifyError verifies the \x03 prefix against authoritative state: \x03GAS is only trusted when gasTracker.used > gasTracker.ceiling, and \x03REVERT is only trusted when the gateway’s revert() or require() function was actually called (tracked via an execution context flag). Unverified \x03-prefixed errors fall through to generic error classification.
Why Not ExternalCopy?
ivm.ExternalCopy can clone plain data objects across the boundary, but it cannot clone function closures. The math API (buildMathAPI()) returns functions wrapped in safeMath() closures, making ExternalCopy fail with “could not be cloned”. The JSON bridge avoids this by injecting each math method as an individual bridge() Reference.
AST-Based Gas Metering
The VM uses deterministic, structure-based gas metering rather than wall-clock timing. Before execution, the contract source is transformed in three phases:
Phase 1: Control Flow Injection
The AST is walked with acorn-walk and __gas(1) calls are injected at:
- Function entry — declarations, expressions, and arrow functions (after directive prologue)
- Loop iterations —
for,while,do-while,for-in,for-of(top of body). Indexedforloops additionally inject a charge into the update expression (for (…; i++)→for (…; (__gas(1), i++))), so eachforiteration costs 2 ×VM_COMPUTATION(body + update) whilewhile/do-while/for-in/for-ofcost 1× - Branches —
if/elseblocks,switchcases (non-empty), ternary operators (wraps test) - Exception handling —
try,catch,finallyblocks - Single-statement bodies are wrapped in
BlockStatementto safely prepend the gas call
Phase 2: Deep Binary Expression Handling
Binary expression chains deeper than 10 levels (e.g., a + b + c + d + ... with 11+ terms) get a gas injection at the depth-10 boundary. This prevents deeply nested expressions from consuming unbounded computation without being metered.
Phase 3: Call Expression Injection
Every CallExpression (except __gas calls themselves) is wrapped as (__gas(1), originalCall()). This uses acorn-walk.ancestor to find the parent node and replace the call in-place.
The injected __gas() function is an ivm.Reference callback that calls GasTracker.chargeComputation() on the host side, with typed error encoding for GasExhaustedError.
Sandbox Security
The V8 isolate provides hardware-level isolation (separate heap, no shared objects). The sandbox script runs inside the isolate before any contract code and strips non-deterministic APIs:
Stripped:
- Timers:
Date,setTimeout,setInterval,setImmediate,clearTimeout,clearInterval,clearImmediate - Async:
queueMicrotask - Memory:
WeakRef,FinalizationRegistry - Proxies:
Proxy - Network:
fetch,XMLHttpRequest,WebSocket - Concurrency:
SharedArrayBuffer,Atomics - Eval/constructors:
eval,Function(global reference set toundefined) - System:
console,process,require,importScripts
Preserved:
Array,Object,String,Number,Boolean,BigInt,JSON,Map,Set,Symbol,Error,RegExp,parseInt,parseFloat
Replaced:
Math— replaced with a frozen deterministic subset:floor,ceil,round,abs,min,max,sqrt,pow,sign,trunc,log,log2,log10, plus constantsPIandE. The object is frozen withObject.freeze()to prevent mutation.
Function Constructor Preservation
The sandbox strips Function from the global scope and kills Function.prototype.constructor to prevent escape via new Function('return process')(). However, the CONTRACT_WRAPPER needs Function to compile contract code. The sandbox saves a private reference as globalThis.__Function before stripping. The contract wrapper consumes and deletes this reference immediately, so contract code never has access to it.
Compilation Cache
The VM maintains a per-block compilation cache to avoid redundant V8 compilation for contracts called multiple times in the same block:
vm.beginBlock()— initializes a newMapfor the cachevm.endBlock()— clears the cache
Cache key: contractIndex:codeHash where codeHash is the SHA-256 of the original (pre-metering) source code.
Cache value: V8 cached compilation data extracted via script.createCachedData(). On cache hit, this data is passed to compileScriptSync() to skip full compilation.
Cache bound: The cache is limited to maxBlockCacheSize entries (default 1,000) per block. When the limit is reached, new unique contracts skip the cache but still execute normally. This prevents memory exhaustion from blocks with thousands of unique contracts.
This eliminates redundant compilation for hot contracts (e.g., a popular AMM called 50 times in one block).
Contract Wrapper
The CONTRACT_WRAPPER script runs the contract code inside the isolate and routes method calls:
- Creates a
moduleandexportsobject - Uses the preserved
__Functionreference to compile the metered contract code as a function body withmodule,exports, andxchainparameters - Executes the compiled function
- Routes based on export type:
- Function export: calls the function directly with
xchain(method param is ignored) - Object export: looks up the named method on the object, throws
'unknown method'if not found - Other: throws
'contract must export a function or object'
- Function export: calls the function directly with
- JSON-serializes the return value with a
\x02prefix before crossing the isolate boundary
Copyright © 2025–2026 Dankest, LLC
Based on XChain Platform by Dankest, LLC – https://dankest.llc
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later) with a commercial license available for proprietary use.
You may use, modify, and distribute this material under the terms of the License. See LICENSE and NOTICE for full terms. See the licensing overview.