XChain VM — Operations
Prerequisites
- Node.js v18+ (V8 engine compatibility)
- 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 (580 tests, 30s timeout)
npm run test:all # unit + E2E (644 tests)
npm run test:regression:core # P0+P1 regression — security + smoke (45 tests, < 200ms)
npm run test:regression:full # P0-P3 regression — full suite (152 tests, < 1s)
npm run test:fuzz # property-based / fuzz tests (86 tests)
npm run test:chaos # chaos engineering (92 tests)
npm run mutation # mutation testing (Stryker)
The VM maintains 974 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
Data Flow
Indexer (execute.js)
|
|-- Loads contract code + state from DB
|-- Builds balances, tokenInfo, oracleData, crossChainData
|
'-> vm.execute({ code, state, method, params, caller, ... })
|
'-> Returns { success, error, gasUsed, returnValue,
stateChanges, stateDeletes, emittedActions, logs }
|
|-- On success: apply state changes, process emissions, charge gas
'-- On failure: discard state/emissions, charge gas up to failure point
Error Classification
The VM classifies execution failures into five 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 |
| 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 three checks:
- V8 syntax check — compiles the code in a throwaway 8 MB isolate to catch syntax errors
- Acorn metering pass — runs
meterCode()to ensure acorn can parse the source (effective ES2020 ceiling) - Reserved identifier check — rejects code containing the
__gasidentifier (used internally by the gas metering system)
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.