XChain VM: Operations
Prerequisites
- Node.js v22 exactly:
isolated-vmrequires Node 22 to build (Node 24 breaks native compilation; below Node 22 tests silently skip rather than fail, producing false greens) - Native build tools for
isolated-vmcompilation:build-essential,python3,libnghttp2-dev,libicu-dev,libbrotli-dev,libc-ares-dev(Debian/Ubuntu) - The VM is a library dependency of
xchain-indexer; it is not run as a standalone process
Installation
cd xchain-vm
npm install
If isolated-vm fails to compile, ensure the native build prerequisites are installed. The module requires C++ compilation against the system’s V8 headers.
Running Tests
npm test # unit tests (500+ tests, 30s timeout)
npm run test:all # all suites (2,028 tests)
npm run test:regression:core # P0+P1 regression: security + smoke (31 tests, < 200ms)
npm run test:regression:full # P0-P3 regression: full suite (100+ tests, < 1s)
npm run test:fuzz # property-based / fuzz tests (53 tests)
npm run test:chaos # chaos engineering (76 tests)
npm run mutation # mutation testing (Stryker)
The VM maintains 1,250+ total tests across unit, E2E, security, fuzz, chaos, boundary, regression, and smoke suites. Tests that require isolated-vm are automatically skipped in the standard suite if the native module is not available. The regression suite (test/regression/) uses a fail-loud check; it throws a fatal error instead of silently skipping, since regression tests must never pass vacuously.
Integration with the Indexer
The VM is instantiated once in the indexer’s actions.js and shared across all action handlers for the lifetime of the indexer process.
Lifecycle
- Startup: Indexer creates
new XChainVM({ gasSchedule, gasCeiling, limits })from its configuration - Per block: Indexer calls
vm.beginBlock()before processing transactions,vm.endBlock()after - DEPLOY action:
deploy.jscallsvm.validateSyntax(code)to validate contract source, thenvm.execute()to run the constructor - EXECUTE action:
execute.jscallsvm.execute()with the contract code, current state, method name, parameters, and block context - Result processing: The indexer applies
stateChangesandstateDeletesto the database, processesemittedActionsthrough standard action handlers, and recordsgasUsedfor fee charging
sequenceDiagram
participant Indexer
participant VM
Note over Indexer: Startup
Indexer->>VM: new XChainVM({ gasSchedule, gasCeiling, limits })
Note over Indexer: Per block
Indexer->>VM: vm.beginBlock()
Indexer->>VM: vm.endBlock()
Note over Indexer: DEPLOY action (deploy.js)
Indexer->>VM: vm.validateSyntax(code)
Indexer->>VM: vm.execute() (run constructor)
Note over Indexer: EXECUTE action (execute.js)
Indexer->>VM: vm.execute(code, state, method, params, block context)
Note over Indexer: Result processing
Indexer->>Indexer: apply stateChanges/stateDeletes, process emittedActions, record gasUsed
Data Flow
flowchart TD
INDEXER["Indexer (execute.js)"]
LOAD["Loads contract code + state from DB"]
BUILD["Builds balances, tokenInfo,<br>oracleData, crossChainData"]
EXEC["vm.execute({ code, state, method,<br>params, caller, ... })"]
RETURNS["Returns { success, error, gasUsed, returnValue,<br>stateChanges, stateDeletes, emittedActions, logs }"]
DECIDE{"execution succeeded?"}
SUCCESS["On success: apply state changes,<br>process emissions, charge gas"]
FAILURE["On failure: discard state/emissions,<br>charge gas up to failure point"]
INDEXER --> LOAD
INDEXER --> BUILD
LOAD --> EXEC
BUILD --> EXEC
EXEC --> RETURNS
RETURNS --> DECIDE
DECIDE -->|success| SUCCESS
DECIDE -->|failure| FAILURE
Error Classification
The VM classifies execution failures into six categories:
| Error Type | Result Prefix | Cause |
|---|---|---|
| Contract revert | revert: <reason> |
xchain.revert(reason) or xchain.require(false, reason) |
| Gas exhaustion | out_of_gas: used X of Y |
Gas ceiling exceeded during execution |
| Wall-clock timeout | timeout: wall-clock safety net triggered |
Execution exceeded maxCpuTimeMs (safety net only) |
| Memory exhaustion | out_of_memory: isolate memory limit exceeded |
V8 isolate heap exceeded maxMemory |
| Stack depth exceeded | out_of_stack: maximum call depth exceeded |
Intra-contract recursion hit the deterministic depth limit, injected by the host as __DEPTH_LIMIT: 512 by default, tightened to 256 once the Package 3 VM-sandbox bundle gate is active for that (network, coin). The gate is active unconditionally on testnet and regtest today, and on mainnet at/above the per-coin heights BTC 961000 / LTC 3154250 / DOGE 6319000. The metering-injected __depth_enter/__depth_exit hooks throw when this limit is reached, producing a deterministic fault instead of relying on V8’s architecture-variable native stack limit. gasUsed is clamped to the ceiling. |
| Runtime error | error: <message> |
Any other JavaScript error (undefined variable, type error, etc.) |
Errors thrown inside the V8 isolate lose their JavaScript class identity when crossing the isolate boundary. The VM uses a message-encoding scheme (see ARCHITECTURE.md) to preserve error type information.
Atomicity Guarantees
On any failure (revert, gas exhaustion, timeout, memory, runtime error):
- State changes are discarded:
stateChangesandstateDeletesare empty arrays - Emitted actions are discarded:
emittedActionsis an empty array - Logs are preserved:
logscontains allxchain.log()output up to the failure point - Gas is charged:
gasUsedreflects consumption up to the failure point
The indexer uses database savepoints to ensure these guarantees extend to the persistent state.
Syntax Validation (Deploy-Time)
Before a contract is deployed, vm.validateSyntax(code) runs the following checks in order. The V8 syntax check blocks a deploy via a separate early return and is not itself a lint-core.CONSENSUS_RULES member; checks 2-8 below are all deploy-blocking consensus rules (invalid-type, unsupported-syntax, reserved-identifier, banned-math, banned-literal, banned-async, banned-generator, banned-wasm), and all must pass or the DEPLOY action is rejected:
- V8 syntax check: compiles the code in a throwaway 8 MB isolate to catch syntax errors (the only step requiring
isolated-vm) - Acorn metering pass: runs
meterCode()to ensure acorn can parse the source (effective ES2020 ceiling) - Reserved identifier check: rejects code containing
__gas, the allocator metering helpers (__concat,__setconcat,__setconcatL,__tmpl,__tmpltag,__tmpltagm,__arrspread,__objspread,__objspreadmeter), or the call-depth metering helpers (__depth_enter,__depth_exit); referencing these could bypass or forge size/depth metering. UnderVM_LINT_HARDENINGthis also covers theCONTRACT_WRAPPER’s injected control bindings (__contractCode,__methodName,__isCrossCall,__readManifest). - Banned transcendental Math check: rejects calls to
Math.sqrt,Math.pow,Math.log,Math.log2, andMath.log10in both dotted (Math.pow) and computed-string (Math['pow']) forms. These five are IEEE 754 transcendentals whose results differ by up to 1 ULP across CPU architectures, producing divergent state hashes on a heterogeneous validator fleet. UnderVM_LINT_HARDENINGthe ban widens to the complement of the deterministic SafeMath whitelist (floor,ceil,round,abs,min,max,sign,trunc,PI,E), plus the**/**=exponentiation operator. Usexchain.math.*(mathjs bignumber) instead. - Banned literal check: rejects BigInt literals (e.g.
10n) and RegExp literals (e.g./foo/). BigInt arithmetic is unmetered native computation; catastrophic RegExp backtracking is unmetered and can burn heavy CPU for near-zero gas. - Banned async check (consensus-gated): rejects
asyncfunctions,awaitexpressions, andPromisereferences after theVM_BANNED_ASYNCflag-day. The CONTRACT_WRAPPER invokes exports synchronously; an async export returns a pending Promise whose post-awaiteffects depend on isolated-vm’s version-dependent microtask-drain timing, which is outside the consensus-runtime pin and can diverge across validators. UnderVM_LINT_HARDENINGthis also rejects dynamicimport(...)(it evaluates to a Promise). - Banned generator check (consensus-gated, Pkg 3 sandbox): rejects
function*, generator methods, and anyyield; live from genesis on testnet/regtest. - Banned WebAssembly check (consensus-gated, Pkg 3 sandbox): rejects any reference to the global
WebAssembly; live from genesis on testnet/regtest.
flowchart TD
START["DEPLOY action: code"]
S1{"1. V8 syntax check (throwaway isolate)"}
S2{"2. Acorn metering pass"}
S3{"3. Reserved identifier check"}
S4{"4. Banned transcendental Math check"}
S5{"5. Banned literal check"}
S6{"6. Banned async check (VM_BANNED_ASYNC flag-day)"}
S7{"7. Banned generator check (live from genesis, testnet/regtest)"}
S8{"8. Banned WebAssembly check (live from genesis, testnet/regtest)"}
ACCEPT["DEPLOY accepted"]
REJECT["DEPLOY rejected"]
START --> S1
S1 -->|"syntax error"| REJECT
S1 -->|"compiles"| S2
S2 -->|"parse failure"| REJECT
S2 -->|"parses"| S3
S3 -->|"reserved identifier found"| REJECT
S3 -->|"clean"| S4
S4 -->|"banned Math call found"| REJECT
S4 -->|"clean"| S5
S5 -->|"BigInt or RegExp literal found"| REJECT
S5 -->|"clean"| S6
S6 -->|"async, await, or Promise found, flag-day active"| REJECT
S6 -->|"clean, or flag-day not active"| S7
S7 -->|"generator or yield found"| REJECT
S7 -->|"clean"| S8
S8 -->|"WebAssembly reference found"| REJECT
S8 -->|"clean"| ACCEPT
vm.checkFloatWarnings(code) additionally scans for non-integer number literals and returns warnings (non-blocking).
Troubleshooting
isolated-vm won’t compile
Symptoms: npm install fails with C++ compilation errors.
Fix: Install native build tools:
# Debian/Ubuntu
sudo apt-get install build-essential python3 libnghttp2-dev libicu-dev libbrotli-dev libc-ares-dev
# macOS
xcode-select --install
Contract hits gas limit unexpectedly
Symptoms: Contract returns out_of_gas with relatively simple logic.
Cause: Gas is charged at every control flow point, function call, and loop iteration. Deeply nested loops or many function calls accumulate gas quickly. Note that indexed for loops are charged twice per iteration (once for the loop body and once for the update expression (i++)) so a for loop costs double what an equivalent while loop costs. Deeply nested for loops are the most common cause of unexpected exhaustion.
Fix: Optimize contract logic: reduce loop iterations, minimize function call depth, batch state reads.
Contract returns “unknown method”
Symptoms: error: unknown method: <name> in execution result.
Cause: The contract exports an object but the method name in the EXECUTE action doesn’t match any property on the exported object.
Fix: Verify the method name matches a property on module.exports. Method names are case-sensitive.
Contract returns “contract must export a function or object”
Symptoms: error: contract must export a function or object in execution result.
Cause: The contract code doesn’t assign a function or object to module.exports.
Fix: Ensure the contract sets module.exports = function(xchain) { ... } or module.exports = { methodName: function(xchain) { ... } }.
Sandbox escape attempt detected
Symptoms: Contract tries to access process, require, Date, Math.random, or Function and gets undefined or an error.
Cause: The sandbox strips all non-deterministic and dangerous APIs. This is working as intended.
Note: Contracts should use xchain.getBlockTimestamp() instead of Date, xchain.math.* instead of native arithmetic operators, and xchain.log() instead of console.log.
State value rejected
Symptoms: Contract execution fails with “state value cannot be null or undefined”, “state value cannot be NaN or Infinity”, “state value must be JSON-serializable”, or “state value exceeds max size”.
Fix: Ensure all values passed to xchain.state.set() are non-null, finite, JSON-serializable, and under 64 KB when serialized. Use xchain.state.delete() to remove keys instead of setting to null.
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.