XChain VM: Architecture
Execution Pipeline
flowchart TD
SRC["Contract Source Code"]
ACORN["acorn (parse AST, ES2020)"]
METER["metering.js (inject __gas() calls<br>at control flow points)"]
ASTR["astring (regenerate source<br>from modified AST)"]
ISOLATE["isolated-vm (V8 isolate)"]
SANDBOX["sandbox.js (strip non-deterministic globals)"]
GATEWAY["gateway.js (inject xchain object<br>via JSON bridge protocol)"]
GASHOST["gas.js (__gas -> chargeComputation on host side)"]
RUNSYNC["script.runSync() (execute with wall-clock timeout)"]
COLLECT["Collect results"]
STATE["state.js -> stateChanges, stateDeletes"]
COLLECTOR["collector.js -> emittedActions, logs"]
GASRESULT["gas.js -> gasUsed"]
RETURN["Return to indexer (execute.js)"]
SRC --> ACORN --> METER --> ASTR --> ISOLATE
ISOLATE --> SANDBOX
ISOLATE --> GATEWAY
ISOLATE --> GASHOST
ISOLATE --> RUNSYNC
SANDBOX --> COLLECT
GATEWAY --> COLLECT
GASHOST --> COLLECT
RUNSYNC --> COLLECT
COLLECT --> STATE
COLLECT --> COLLECTOR
COLLECT --> GASRESULT
STATE --> RETURN
COLLECTOR --> RETURN
GASRESULT --> RETURN
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: 19 action types (SEND through MESSAGE and VOTE, plus execute for cross-contract calls and crossExecute for cross-chain calls), 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 21 allowed action types (SEND, DESTROY, ISSUE, MINT, ORDER, DISPENSER, DIVIDEND, AIRDROP, CALLBACK, FILE, LIST, COINPAY, SWEEP, LINK, BROADCAST, MESSAGE, ATTEST, SLASH, EXECUTE, XCALL, VOTE) and checks params shape |
lint-core.js |
Dependency-light (no isolated-vm) acorn-only consensus validation, exporting lintSource(), analyzeContract(), and per-rule detectors: findBannedMathCalls, findBannedLiterals, findBannedAsync, findBannedGenerator (function*/yield), findBannedWasm (global WebAssembly reference), findBannedExponentiation (**/**=, hardened-only), findBannedProtoMethods (advisory, name-only match, never consensus), findReservedControlBinding (CONTRACT_WRAPPER bindings, hardened-only), and codeSizeBytes/MAX_CODE_SIZE for the 64KiB deploy cap. Under VM_LINT_HARDENING, the Math ban widens from the five named transcendentals to the complement of the SAFE_MATH_MEMBERS whitelist. Holds CONSENSUS_RULES (the 8-member set of deploy-blocking rule names: invalid-type, unsupported-syntax, reserved-identifier, banned-math, banned-literal, banned-async, banned-generator, banned-wasm) and lintSource(). Vendored byte-for-byte into xchain-sdk/src/contract/lint-core.js; a CI SHA-256 parity guard fails on drift. |
syntax.js |
Deploy-time validation: V8 syntax check (throwaway isolate, the one step requiring isolated-vm; blocks via a separate early return and is not itself a CONSENSUS_RULES member), then delegates all acorn-coverable consensus rules to lint-core.lintSource(). Re-exports findBannedMathCalls, findBannedLiterals, findBannedAsync, findBannedGenerator, and findBannedWasm for backward compatibility. validateSyntax(code, opts) threads four independent consensus gates: enforceBannedAsync (the VM_BANNED_ASYNC flag-day), enforceLintHardening (the VM_LINT_HARDENING flag-day, widening the Math ban and adding the exponentiation/reserved-binding/dynamic-import rules), and enforceBannedGenerator/enforceBannedWasm (the Package 3 per-coin sandbox height gate, active from genesis on testnet/regtest). Each defaults to true for author-facing callers so a from-genesis replay reproduces the historical deploy verdict at every activation. |
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
sequenceDiagram
participant Contract as Contract (isolate)
participant Wrap as wrap() (isolate)
participant Bridge as bridge() (host)
participant Gateway as gateway function (host)
Contract->>Wrap: xchain.state.get(key)
Wrap->>Wrap: JSON-serialize the arguments
Wrap->>Bridge: applySync sends the JSON string
Bridge->>Bridge: JSON.parse the arguments
Bridge->>Gateway: call the gateway function
Gateway-->>Bridge: return value
Bridge-->>Wrap: JSON-encode, tagged with the 0x01 prefix
Wrap-->>Contract: JSON.parse the result
alt gateway method throws a typed error
Gateway-->>Bridge: throws ContractRevertError or GasExhaustedError
Bridge-->>Wrap: re-throw tagged with the 0x03 prefix, REVERT or GAS
end
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,Promise(consensus-gated: stripped at/after theVM_BANNED_ASYNCflag-day block time; present on earlier blocks for replay fidelity) - Memory:
WeakRef,FinalizationRegistry - Proxies:
Proxy,Reflect - Network:
fetch,XMLHttpRequest,WebSocket - Concurrency:
SharedArrayBuffer,Atomics - Big integers:
BigInt(super-linear native cost, unmeterable by the AST meter; usexchain.math.*instead;BigIntliterals also rejected at deploy time) - Regex:
RegExp(set toundefined; prevents catastrophic backtracking / ReDoS) - Locale/time:
Intl,Temporal,structuredClone,performance - Eval/constructors:
eval,Function(global reference set toundefined) - System:
console,process,require,importScripts
Preserved:
Array,Object,String,Number,Boolean,JSON,Map,Set,Symbol,Error,parseInt,parseFloat
Replaced:
-
Math: replaced with a frozen deterministic subset:floor,ceil,round,abs,min,max,sign,trunc, plus constantsPIandE. The object is frozen withObject.freeze()to prevent mutation.The transcendentals
sqrt,pow,log,log2, andlog10are intentionally absent: IEEE 754 only mandates correctly-rounded results forsqrt(not forpow,log,log2, orlog10) so hostlibmcan differ by 1 ULP across CPU architectures, producing divergent state hashes across a heterogeneous validator fleet. These five functions are also banned at deploy time (xchain-vm/src/syntax.jsfindBannedMathCalls): contracts that attempt to use them are rejected before they can be stored. Contracts that need high-precision transcendentals must usexchain.math.*(mathjs bignumber, pure software arithmetic, identical on every platform).
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
flowchart TD
A["Create module and exports object"]
B["Compile metered contract code as a function body<br>(module, exports, xchain params)"]
C["Execute the compiled function"]
D{"Export type?"}
E["Function export: call the function directly with xchain<br>(method param ignored)"]
F["Object export: look up the named method on the object<br>(throws unknown method if not found)"]
G["Other: throws contract must export a function or object"]
H["JSON-serialize return value with \x02 prefix, cross isolate boundary"]
A --> B --> C --> D
D -->|"function"| E --> H
D -->|"object"| F --> H
D -->|"other"| G
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.