diff --git a/.github/workflows/vm-differential-fuzz.yml b/.github/workflows/vm-differential-fuzz.yml
index 984cdf3..987fbd8 100644
--- a/.github/workflows/vm-differential-fuzz.yml
+++ b/.github/workflows/vm-differential-fuzz.yml
@@ -109,6 +109,13 @@ jobs:
with:
node-version: "22"
+ # compare only diffs already-recorded JSON manifests (no VM execution,
+ # so isolated-vm need not build from source here), but differential.js
+ # itself requires fast-check/acorn/astring, so those still need to be
+ # on disk before the compare step below can run at all.
+ - name: Install deps
+ run: npm install --ignore-scripts
+
- name: Download all manifests
uses: actions/download-artifact@v4
with:
@@ -121,14 +128,27 @@ jobs:
set -e
REF="manifests/differential.linux-x64-glibc.json"
test -f "$REF" || { echo "reference manifest missing"; exit 1; }
- status=0
+ divergence=0
+ crash=0
for m in manifests/differential.*.json; do
[ "$m" = "$REF" ] && continue
echo "::group::compare $(basename "$m") vs reference"
- node test/fuzz/helpers/differential_run.js compare "$REF" "$m" || status=1
+ out="$(node test/fuzz/helpers/differential_run.js compare "$REF" "$m" 2>&1)" && rc=0 || rc=$?
+ printf '%s\n' "$out"
+ if [ "$rc" -ne 0 ]; then
+ if printf '%s\n' "$out" | grep -q 'DIFFERENTIAL DIVERGENCE:'; then
+ divergence=1
+ else
+ crash=1
+ fi
+ fi
echo "::endgroup::"
done
- if [ "$status" -ne 0 ]; then
+ if [ "$crash" -ne 0 ]; then
+ echo "::error::VM differential fuzz tool crashed before it could render a verdict (see the compare group's output above); this is not a consensus divergence"
+ exit 1
+ fi
+ if [ "$divergence" -ne 0 ]; then
echo "::error::VM differential fuzz found a cross-build consensus divergence"
exit 1
fi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6271e98..0d252f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.20.1] - 2026-09-23
+
+### Fixed
+- Aligned simulator deploy gating, oracle round floors, and balance scoping with chain execution behavior.
+
+
## [0.20.0] - 2026-09-17
### Changed
diff --git a/README.md b/README.md
index 24416a0..c754b80 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
# XChain Platform Virtual Machine (VM)
-
+
@@ -201,8 +201,10 @@ runGate(source); // { ok, errors, advisories, warnings, g
// string ("invalid: CONTRACT_MANIFEST (meta required)"). A meta the static walk
// cannot read (computed, spread, non-literal) is an advisory, not a block.
-const sim = new ContractSimulator({ coin: 'BTC' });
-sim.setBalance('alice', 'GOLD', '1000'); // seed read-only ledger/oracle state
+const sim = new ContractSimulator({ coin: 'BTC', defaultCaller: 'alice' });
+// Seed read-only ledger/oracle state. A node preloads balances for the caller and
+// the contract's own address only, so seed those two; any other address reads null on chain.
+sim.setBalance('alice', 'GOLD', '1000');
sim.setAttestationResponse('req-1', '{"ok":true}'); // and the async read surfaces
const { contractIndex } = await sim.deploy(source, { constructorParams: ['5'] });
const res = await sim.call(contractIndex, 'increment', ['3']); // state persists across calls
@@ -310,7 +312,7 @@ Cross-run and cross-process determinism guarantees in `test/determinism/`: golde
Latency/throughput assertions in `test/performance/` (`npm run test:performance`), distinct from the `bench/` scenario scripts below.
-### Toolkit Tests (52)
+### Toolkit Tests (141)
`xchain-foundry` / `create-xchain-contract` developer-toolkit coverage: gate, scaffold, and TypeScript-strip logic run on any OS; simulator-backed cases need the isolated-vm binding (Node 22 / Linux).
@@ -413,7 +415,7 @@ xchain-vm/
| |-- smoke/ (10 tests)
| |-- determinism/ (79 tests, incl. a known-red probe subset)
| |-- performance/ (5 tests, distinct from bench/ below)
-| |-- toolkit/ (52 tests: xchain-foundry gate/scaffold/simulate)
+| |-- toolkit/ (141 tests: xchain-foundry gate/scaffold/simulate)
| |-- e2e/ (10 E2E test files, 64 tests + helpers + contracts)
| |-- fuzz/ (9 fuzz test files, 57 tests + harness + generators)
| |-- chaos/ (3-phase chaos tests, 76 tests + helpers + contracts)
diff --git a/bin/ci-full.sh b/bin/ci-full.sh
index 3c4eb30..5673a9c 100755
--- a/bin/ci-full.sh
+++ b/bin/ci-full.sh
@@ -46,7 +46,35 @@ SELF="$(pwd)"
SIB="$(cd .. && pwd)"
FAILED=""
+# >>> ci-tier (generated block; re-run the tier wirer to update) >>>
+# Tier classes. A push grades the FAST tier only: the unit job, the pin and
+# drift guards, and the structure and hygiene checks the hook runs before it
+# dispatches. The tiers named below (coverage re-runs, perf scenarios) are
+# skipped when the gate sets CI_TIER=fast, and each skip is recorded so the
+# closing verdict can never claim a green it did not earn. Nothing stops
+# being graded: a scheduled sweep re-runs this same script with CI_TIER=full
+# on every repo every three hours and before any release or deploy, and a
+# red there is tracked down and fixed first. CI_TIER is unset for a hand
+# run, so a bare `npm run ci:full` still runs every tier as it always did.
+CI_TIER_FULL_ONLY=(
+ "coverage ratchet (coverage:check)"
+)
+DEFERRED=""
+ci_tier_deferred() {
+ [ "${CI_TIER:-full}" = "fast" ] || return 1
+ local t
+ for t in ${CI_TIER_FULL_ONLY[@]+"${CI_TIER_FULL_ONLY[@]}"}; do
+ if [ "$t" = "$1" ]; then
+ DEFERRED="$DEFERRED [$1]"
+ echo; echo "ci:full ===== $1 DEFERRED (CI_TIER=fast, runs in the full sweep) ====="
+ return 0
+ fi
+ done
+ return 1
+}
+# <<< ci-tier <<<
run_tier() {
+ ci_tier_deferred "$1" && return 0 # ci-tier guard (generated)
local name="$1"; shift
echo; echo "ci:full ===== $name ====="
if "$@"; then
@@ -106,8 +134,20 @@ run_tier "identity pin (vendored lint trio)" identity_pin_check
run_tier "coverage ratchet (coverage:check)" npm run coverage:check
echo
+# >>> ci-tier summary (generated) >>>
+echo "ci:full: tier class ${CI_TIER:-full}"
+if [ -n "${DEFERRED:-}" ]; then
+ echo "ci:full: DEFERRED to the full sweep:$DEFERRED"
+fi
+# <<< ci-tier summary <<<
if [ -n "$FAILED" ]; then
echo "ci:full: RED tiers:$FAILED"
exit 1
fi
-echo "ci:full: all tiers green (same set GitHub CI runs)"
+# >>> ci-tier verdict (generated) >>>
+if [ "${CI_TIER:-full}" = "fast" ]; then
+ echo "ci:full: all FAST tiers green; the DEFERRED tiers above were NOT graded here"
+else
+ echo "ci:full: all tiers green (same set GitHub CI runs)"
+fi
+# <<< ci-tier verdict <<<
diff --git a/package-lock.json b/package-lock.json
index 159fcbd..38ce2f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "xchain-vm",
- "version": "0.20.0",
+ "version": "0.20.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "xchain-vm",
- "version": "0.20.0",
+ "version": "0.20.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"acorn": "8.16.0",
diff --git a/package.json b/package.json
index 4eaf011..37fe2ee 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "xchain-vm",
"description": "XChain Platform Virtual Machine (VM) runtime - deterministic smart contract execution in sandboxed V8 isolates",
- "version": "0.20.0",
+ "version": "0.20.1",
"ciPlatform": "linux",
"license": "AGPL-3.0-or-later",
"main": "src/index.js",
diff --git a/src/gateway.d.ts b/src/gateway.d.ts
index 75b5cb7..9aa3800 100644
--- a/src/gateway.d.ts
+++ b/src/gateway.d.ts
@@ -113,6 +113,12 @@ export interface PollResult {
total_voters: number;
decided_early: boolean;
options: Array<{ index: number; weight: string; voters: number }>;
+ /**
+ * The poll's electorate token tick (null for a poll bound to no token).
+ * Present only once the host enables VOTE_POLL_TICK_VISIBLE; absent below it.
+ * Electorate-pinning contracts (treasury.executeProposal) read it.
+ */
+ tick?: string | null;
}
/** Terminal outcome of a cross-chain call this chain originated. */
diff --git a/src/index.js b/src/index.js
index 8a2bc5d..e4b1ba3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1716,7 +1716,7 @@ class XChainVM {
// Map.
// meterCode() is a pure AST transform (acorn parse + walk + astring regen over
// up to maxCodeSize bytes), the single most expensive step of a warm execute,
- // and its output depends ONLY on the contract source plus the two consensus
+ // and its output depends ONLY on the contract source plus the consensus
// gate flags (specEvalOrder, meterCallSpread, meterRestPattern), all baked into
// the key. So a
// hit returns byte-identical metered source to a fresh call: no consensus
@@ -1731,7 +1731,7 @@ class XChainVM {
this.limits.maxMeteredCacheSize = this.limits.maxBlockCacheSize || 1000;
// Execute-time lint-verdict cache:
- // Map.
+ // Map, {valid, error?}>.
// INVARIANT: the key carries the code digest plus EVERY consensus flag the
// verdict depends on, with no count written down here that a new flag can
// falsify. Adding a flag to getLintVerdict without adding its bit lets a warm
@@ -2614,7 +2614,8 @@ module.exports.isSlashTokenDelimGuardActive = isSlashTokenDelimGuardActive;
// exported so a test can pin it against the indexer's MAX_TOKEN_DECIMALS.
module.exports.isSlashAmountPrecisionActive = isSlashAmountPrecisionActive;
module.exports.MAX_SLASH_AMOUNT_DECIMALS = MAX_SLASH_AMOUNT_DECIMALS;
-// Cross-CHAIN call (XCALL) protocol constants, same canonical source.
+// Export the cross-chain call protocol limits so validator hosts can pin the
+// VM's bounds against the values they revalidate while processing emissions.
module.exports.XCALL_MIN_GAS = XCALL_MIN_GAS;
module.exports.XCALL_MAX_GAS = XCALL_MAX_GAS;
module.exports.XCALL_MAX_HOPS = XCALL_MAX_HOPS;
diff --git a/src/index/gateway_injection.js b/src/index/gateway_injection.js
index c47ceef..44677c7 100644
--- a/src/index/gateway_injection.js
+++ b/src/index/gateway_injection.js
@@ -23,6 +23,120 @@
const ivm = require('isolated-vm');
const { ContractRevertError, GasExhaustedError } = require('../errors.js');
+function injectAccessors(g, gateway, bridge) {
+ // Context accessors (0 gas)
+ g.setSync('__getBlockHeight', bridge(gateway.getBlockHeight));
+ g.setSync('__getBlockTimestamp', bridge(gateway.getBlockTimestamp));
+ g.setSync('__getBlockHash', bridge(gateway.getBlockHash));
+ g.setSync('__getSourceAddress', bridge(gateway.getSourceAddress));
+ g.setSync('__getContractAddress', bridge(gateway.getContractAddress));
+ g.setSync('__getInputParams', bridge(gateway.getInputParams));
+ g.setSync('__getInputParam', bridge(gateway.getInputParam));
+ g.setSync('__getInputParamCount', bridge(gateway.getInputParamCount));
+ g.setSync('__getCallDepth', bridge(gateway.getCallDepth));
+ g.setSync('__getCrossHops', bridge(gateway.getCrossHops));
+}
+
+function injectLedger(g, gateway, bridge) {
+ // Ledger queries (metered)
+ g.setSync('__getBalance', bridge(gateway.getBalance));
+ g.setSync('__getTokenInfo', bridge(gateway.getTokenInfo));
+ g.setSync('__getPollResult', bridge(gateway.getPollResult));
+}
+
+function injectState(g, gateway, bridge) {
+ // State (metered)
+ g.setSync('__state_get', bridge(gateway.state.get));
+ g.setSync('__state_has', bridge(gateway.state.has));
+ g.setSync('__state_set', bridge(gateway.state.set));
+ g.setSync('__state_delete', bridge(gateway.state.delete));
+}
+
+function injectOracle(g, gateway, bridge) {
+ // Oracle (metered)
+ g.setSync('__oracle_getPrice', bridge(gateway.oracle.getPrice));
+ g.setSync('__oracle_getPriceAtRound', bridge(gateway.oracle.getPriceAtRound));
+ g.setSync('__oracle_getSnapshotAge', bridge(gateway.oracle.getSnapshotAge));
+}
+
+function injectCrossChain(g, gateway, bridge) {
+ // Cross-chain (metered)
+ g.setSync('__crossChain_getAttestation', bridge(gateway.crossChain.getAttestation));
+ g.setSync('__crossChain_isSettled', bridge(gateway.crossChain.isSettled));
+ g.setSync('__crossChain_getCallResult', bridge(gateway.crossChain.getCallResult));
+
+ g.setSync('__attestation_request', bridge(gateway.attestation.request));
+ g.setSync('__attestation_getResponse', bridge(gateway.attestation.getResponse));
+}
+
+function injectStaking(g, gateway, bridge) {
+ // Contract-targeted staking (metered)
+ g.setSync('__contract_getStake', bridge(gateway.contract.getStake));
+ g.setSync('__contract_getTotalStaked', bridge(gateway.contract.getTotalStaked));
+ g.setSync('__contract_getStakers', bridge(gateway.contract.getStakers));
+ g.setSync('__contract_slash', bridge(gateway.contract.slash));
+}
+
+function injectEmit(g, gateway, bridge) {
+ // Emit (metered)
+ g.setSync('__emit_send', bridge(gateway.emit.send));
+ g.setSync('__emit_destroy', bridge(gateway.emit.destroy));
+ g.setSync('__emit_issue', bridge(gateway.emit.issue));
+ g.setSync('__emit_mint', bridge(gateway.emit.mint));
+ g.setSync('__emit_order', bridge(gateway.emit.order));
+ g.setSync('__emit_dispenser', bridge(gateway.emit.dispenser));
+ g.setSync('__emit_dividend', bridge(gateway.emit.dividend));
+ g.setSync('__emit_airdrop', bridge(gateway.emit.airdrop));
+ g.setSync('__emit_callback', bridge(gateway.emit.callback));
+ g.setSync('__emit_file', bridge(gateway.emit.file));
+ g.setSync('__emit_list', bridge(gateway.emit.list));
+ g.setSync('__emit_coinpay', bridge(gateway.emit.coinpay));
+ g.setSync('__emit_sweep', bridge(gateway.emit.sweep));
+ g.setSync('__emit_link', bridge(gateway.emit.link));
+ g.setSync('__emit_broadcast', bridge(gateway.emit.broadcast));
+ g.setSync('__emit_message', bridge(gateway.emit.message));
+ g.setSync('__emit_vote', bridge(gateway.emit.vote));
+ g.setSync('__emit_execute', bridge(gateway.emit.execute));
+ g.setSync('__emit_crossExecute', bridge(gateway.emit.crossExecute));
+}
+
+function injectMath(g, gateway, bridge) {
+ // Math
+ g.setSync('__math_add', bridge(gateway.math.add));
+ g.setSync('__math_subtract', bridge(gateway.math.subtract));
+ g.setSync('__math_multiply', bridge(gateway.math.multiply));
+ g.setSync('__math_divide', bridge(gateway.math.divide));
+ g.setSync('__math_mod', bridge(gateway.math.mod));
+ g.setSync('__math_compare', bridge(gateway.math.compare));
+ g.setSync('__math_gt', bridge(gateway.math.gt));
+ g.setSync('__math_gte', bridge(gateway.math.gte));
+ g.setSync('__math_lt', bridge(gateway.math.lt));
+ g.setSync('__math_lte', bridge(gateway.math.lte));
+ g.setSync('__math_eq', bridge(gateway.math.eq));
+ g.setSync('__math_min', bridge(gateway.math.min));
+ g.setSync('__math_max', bridge(gateway.math.max));
+ g.setSync('__math_abs', bridge(gateway.math.abs));
+ g.setSync('__math_isZero', bridge(gateway.math.isZero));
+ g.setSync('__math_sqrt', bridge(gateway.math.sqrt));
+ g.setSync('__math_pow', bridge(gateway.math.pow));
+ g.setSync('__math_log', bridge(gateway.math.log));
+ g.setSync('__math_log2', bridge(gateway.math.log2));
+ g.setSync('__math_log10', bridge(gateway.math.log10));
+}
+
+function injectControlFlow(g, gateway, bridge) {
+ // Control flow (gas-free)
+ g.setSync('__revert', bridge(gateway.revert));
+ g.setSync('__require', bridge(gateway.require));
+}
+
+function injectLogging(g, gateway, bridge) {
+ // Logging (gas-free)
+ g.setSync('__log', bridge(gateway.log));
+ g.setSync('__isLogFull', bridge(gateway.isLogFull));
+ g.setSync('__getLogCount', bridge(gateway.getLogCount));
+}
+
module.exports = {
/**
* Inject gateway methods into the isolate context as ivm.Reference objects.
@@ -53,98 +167,15 @@ module.exports = {
}
});
- // Context accessors (0 gas)
- g.setSync('__getBlockHeight', bridge(gateway.getBlockHeight));
- g.setSync('__getBlockTimestamp', bridge(gateway.getBlockTimestamp));
- g.setSync('__getBlockHash', bridge(gateway.getBlockHash));
- g.setSync('__getSourceAddress', bridge(gateway.getSourceAddress));
- g.setSync('__getContractAddress', bridge(gateway.getContractAddress));
- g.setSync('__getInputParams', bridge(gateway.getInputParams));
- g.setSync('__getInputParam', bridge(gateway.getInputParam));
- g.setSync('__getInputParamCount', bridge(gateway.getInputParamCount));
- g.setSync('__getCallDepth', bridge(gateway.getCallDepth));
- g.setSync('__getCrossHops', bridge(gateway.getCrossHops));
-
- // Ledger queries (metered)
- g.setSync('__getBalance', bridge(gateway.getBalance));
- g.setSync('__getTokenInfo', bridge(gateway.getTokenInfo));
- g.setSync('__getPollResult', bridge(gateway.getPollResult));
-
- // State (metered)
- g.setSync('__state_get', bridge(gateway.state.get));
- g.setSync('__state_has', bridge(gateway.state.has));
- g.setSync('__state_set', bridge(gateway.state.set));
- g.setSync('__state_delete', bridge(gateway.state.delete));
-
- // Oracle (metered)
- g.setSync('__oracle_getPrice', bridge(gateway.oracle.getPrice));
- g.setSync('__oracle_getPriceAtRound', bridge(gateway.oracle.getPriceAtRound));
- g.setSync('__oracle_getSnapshotAge', bridge(gateway.oracle.getSnapshotAge));
-
- // Cross-chain (metered)
- g.setSync('__crossChain_getAttestation', bridge(gateway.crossChain.getAttestation));
- g.setSync('__crossChain_isSettled', bridge(gateway.crossChain.isSettled));
- g.setSync('__crossChain_getCallResult', bridge(gateway.crossChain.getCallResult));
-
- g.setSync('__attestation_request', bridge(gateway.attestation.request));
- g.setSync('__attestation_getResponse', bridge(gateway.attestation.getResponse));
-
- // Contract-targeted staking (metered)
- g.setSync('__contract_getStake', bridge(gateway.contract.getStake));
- g.setSync('__contract_getTotalStaked', bridge(gateway.contract.getTotalStaked));
- g.setSync('__contract_getStakers', bridge(gateway.contract.getStakers));
- g.setSync('__contract_slash', bridge(gateway.contract.slash));
-
- // Emit (metered)
- g.setSync('__emit_send', bridge(gateway.emit.send));
- g.setSync('__emit_destroy', bridge(gateway.emit.destroy));
- g.setSync('__emit_issue', bridge(gateway.emit.issue));
- g.setSync('__emit_mint', bridge(gateway.emit.mint));
- g.setSync('__emit_order', bridge(gateway.emit.order));
- g.setSync('__emit_dispenser', bridge(gateway.emit.dispenser));
- g.setSync('__emit_dividend', bridge(gateway.emit.dividend));
- g.setSync('__emit_airdrop', bridge(gateway.emit.airdrop));
- g.setSync('__emit_callback', bridge(gateway.emit.callback));
- g.setSync('__emit_file', bridge(gateway.emit.file));
- g.setSync('__emit_list', bridge(gateway.emit.list));
- g.setSync('__emit_coinpay', bridge(gateway.emit.coinpay));
- g.setSync('__emit_sweep', bridge(gateway.emit.sweep));
- g.setSync('__emit_link', bridge(gateway.emit.link));
- g.setSync('__emit_broadcast', bridge(gateway.emit.broadcast));
- g.setSync('__emit_message', bridge(gateway.emit.message));
- g.setSync('__emit_vote', bridge(gateway.emit.vote));
- g.setSync('__emit_execute', bridge(gateway.emit.execute));
- g.setSync('__emit_crossExecute', bridge(gateway.emit.crossExecute));
-
- // Math
- g.setSync('__math_add', bridge(gateway.math.add));
- g.setSync('__math_subtract', bridge(gateway.math.subtract));
- g.setSync('__math_multiply', bridge(gateway.math.multiply));
- g.setSync('__math_divide', bridge(gateway.math.divide));
- g.setSync('__math_mod', bridge(gateway.math.mod));
- g.setSync('__math_compare', bridge(gateway.math.compare));
- g.setSync('__math_gt', bridge(gateway.math.gt));
- g.setSync('__math_gte', bridge(gateway.math.gte));
- g.setSync('__math_lt', bridge(gateway.math.lt));
- g.setSync('__math_lte', bridge(gateway.math.lte));
- g.setSync('__math_eq', bridge(gateway.math.eq));
- g.setSync('__math_min', bridge(gateway.math.min));
- g.setSync('__math_max', bridge(gateway.math.max));
- g.setSync('__math_abs', bridge(gateway.math.abs));
- g.setSync('__math_isZero', bridge(gateway.math.isZero));
- g.setSync('__math_sqrt', bridge(gateway.math.sqrt));
- g.setSync('__math_pow', bridge(gateway.math.pow));
- g.setSync('__math_log', bridge(gateway.math.log));
- g.setSync('__math_log2', bridge(gateway.math.log2));
- g.setSync('__math_log10', bridge(gateway.math.log10));
-
- // Control flow (gas-free)
- g.setSync('__revert', bridge(gateway.revert));
- g.setSync('__require', bridge(gateway.require));
-
- // Logging (gas-free)
- g.setSync('__log', bridge(gateway.log));
- g.setSync('__isLogFull', bridge(gateway.isLogFull));
- g.setSync('__getLogCount', bridge(gateway.getLogCount));
+ injectAccessors(g, gateway, bridge);
+ injectLedger(g, gateway, bridge);
+ injectState(g, gateway, bridge);
+ injectOracle(g, gateway, bridge);
+ injectCrossChain(g, gateway, bridge);
+ injectStaking(g, gateway, bridge);
+ injectEmit(g, gateway, bridge);
+ injectMath(g, gateway, bridge);
+ injectControlFlow(g, gateway, bridge);
+ injectLogging(g, gateway, bridge);
},
};
diff --git a/src/index/lint_and_metering.js b/src/index/lint_and_metering.js
index 311bcdf..3a51471 100644
--- a/src/index/lint_and_metering.js
+++ b/src/index/lint_and_metering.js
@@ -81,11 +81,10 @@ module.exports = {
* Return the execute-time consensus source-lint verdict for `code` under the
* resolved ban flags, from the verdict cache when possible.
*
- * validateSyntax() is a pure function of (code, enforceBannedAsync,
- * enforceLintHardening, enforceBannedGenerator === enforceBannedWasm,
- * enforceLintGlobalAlias), all folded into the key, so a hit returns the verdict a
- * fresh call would produce. The two Pkg 3 rules share ONE activation and are
- * therefore threaded as one bit; the global-alias refinement rides its own.
+ * validateSyntax() is a pure function of the code plus every flag parameter of this
+ * method, all folded into the key, so a hit returns the verdict a fresh call would
+ * produce. The two Pkg 3 rules share ONE activation and are therefore threaded as
+ * one bit; the global-alias and banned-rest refinements each ride their own.
*
* This exists because validateSyntax spawns an ivm.Isolate for its V8 syntax check
* and then acorn-parses the source; paying that on every execute of a hot contract
diff --git a/src/readonly_accessors.js b/src/readonly_accessors.js
index 6a1b06e..3cf059d 100644
--- a/src/readonly_accessors.js
+++ b/src/readonly_accessors.js
@@ -145,7 +145,10 @@ function buildPollAccessor(snap) {
// Frozen result of a finalized VOTE poll, or null if the poll does not
// exist or has not finalized yet. Keys are poll indices (the VOTE v0
// action_index). Shape: { status, winning_option, total_weight,
- // total_voters, decided_early, options:[{index,weight,voters}] }.
+ // total_voters, decided_early, options:[{index,weight,voters}], tick? },
+ // where `tick` is present only once VOTE_POLL_TICK_VISIBLE is enabled.
+ // Return the host entry verbatim: never project or whitelist its fields
+ // (dropping a flag-gated field makes an electorate-pinned contract revert).
getPollResult: (pollIndex) => {
const p = polls[String(pollIndex)];
return p != null ? p : null;
diff --git a/src/toolkit/authoring/authoring_knowledge.js b/src/toolkit/authoring/authoring_knowledge.js
index c50da3c..dd2c0c9 100644
--- a/src/toolkit/authoring/authoring_knowledge.js
+++ b/src/toolkit/authoring/authoring_knowledge.js
@@ -22,7 +22,7 @@ const MODEL_SHIFTS = [
const NATIVE_PRIMITIVES = [
{ want: 'A fungible token', solidity: 'write an ERC-20', xchain: 'ISSUE action (tokens are first-class); no contract' },
{ want: 'Transfer a token', solidity: 'transfer()', xchain: 'SEND action' },
- { want: 'Check a balance', solidity: 'balanceOf()', xchain: 'explorer/SDK query, or in-contract getBalance(addr, tick)' },
+ { want: 'Check a balance', solidity: 'balanceOf()', xchain: 'explorer/SDK query, or in-contract getBalance(addr, tick) for the caller or the contract itself only (any other address reads null)' },
{ want: 'An NFT', solidity: 'ERC-721', xchain: 'ISSUE with DECIMALS=0 + LOCK_MAX_SUPPLY=1' },
{ want: 'Token sale', solidity: 'crowdsale contract', xchain: 'DISPENSER action, or the crowdsale template' },
{ want: 'Swap two tokens', solidity: 'a DEX pair', xchain: 'ORDER / SWAP actions, or the amm template' },
diff --git a/src/toolkit/gate/meta_validation.js b/src/toolkit/gate/meta_validation.js
index fc34c8f..6985f3d 100644
--- a/src/toolkit/gate/meta_validation.js
+++ b/src/toolkit/gate/meta_validation.js
@@ -14,6 +14,7 @@
const acorn = require('acorn');
const walk = require('acorn-walk');
+const { CONTRACT_ECMA_VERSION } = require('../../metering.js');
// --- Contract identity (`meta`) ------------------------------------------
//
@@ -34,9 +35,14 @@ const walk = require('acorn-walk');
// contract the chain would accept on a shape it merely cannot see.
// Frozen consensus tokens, copied verbatim from spec 2.3 so the author reads the
-// same string the chain will write into the action's status.
+// same string the chain will write into the action's status. READ_FAILED,
+// NOT_OBJECT and OVERSIZE need the evaluated read, so only the simulator's deploy
+// gate reaches them; the static walk here never does.
const META_VERDICTS = {
+ READ_FAILED: 'invalid: CONTRACT_MANIFEST (manifest read failed)',
REQUIRED: 'invalid: CONTRACT_MANIFEST (meta required)',
+ NOT_OBJECT: 'invalid: CONTRACT_MANIFEST (meta must be a plain object)',
+ OVERSIZE: 'invalid: CONTRACT_MANIFEST (meta exceeds 4096 characters)',
NAME: 'invalid: CONTRACT_MANIFEST (meta.name must be a string of 1..64 bytes, printable, trimmed)',
DESCRIPTION: 'invalid: CONTRACT_MANIFEST (meta.description must be a string of 1..512 bytes, printable, trimmed)',
VERSION: 'invalid: CONTRACT_MANIFEST (meta.version must be a string of 1..32 bytes, printable, trimmed)'
@@ -195,7 +201,7 @@ function getExportedMeta(source) {
try {
// acorn is a hard dependency of lint_core, already required above, so an
// unparseable source is the only way to land here.
- ast = acorn.parse(String(source), { ecmaVersion: 2020, sourceType: 'script', locations: true });
+ ast = acorn.parse(String(source), { ecmaVersion: CONTRACT_ECMA_VERSION, sourceType: 'script', locations: true });
} catch (e) {
return { status: 'undecidable' };
}
diff --git a/src/toolkit/simulator.js b/src/toolkit/simulator.js
index a005d1f..e7b6f5a 100644
--- a/src/toolkit/simulator.js
+++ b/src/toolkit/simulator.js
@@ -50,7 +50,10 @@
* What it does NOT do (out of scope; that is the real indexer + regtest):
* - process emitted ACTIONs against the ledger (SEND/ISSUE/... are captured
* for assertions, never applied to balances). getBalance reads only what
- * the author seeds.
+ * the author seeds. A node also SCOPES that snapshot: it preloads balances
+ * for the action's SOURCE and the contract's own address only, so a seed
+ * for any third address reads back here and is null on chain. call() warns
+ * once when such a seed is present (see warnIfBalanceOutOfScope).
* - resolve emit.execute / emit.crossExecute call trees. Those emissions are
* captured; the callee is not auto-run.
* - ASSIGN the identity discriminators a real node assigns. txHash,
@@ -59,10 +62,16 @@
* matches chain only when the caller supplies the real values.
* - populate the read-only snapshots. They start empty and read back
* null / '0' / [] until seeded, which is the same answer a node gives for
- * data that genuinely does not exist; a stale seed is the author's.
+ * data that genuinely does not exist; a stale seed is the author's. The
+ * oracle snapshot hides no history by default (roundFloor 0), while a node
+ * ships a bounded window; setOracleRoundFloor(n) simulates that window, so
+ * getPriceAtRound on an unseeded round below n returns the chain's
+ * { price: null, outsideWindow: true } row instead of plain null.
* - REFUSE a deploy the chain's deploy gate would reject. deploy() runs that
- * gate (code size + validateSyntax, resolved at the configured network /
- * coin / block) and hands the verdict back as `deployGate`, warning once on a
+ * gate's three legs (code size, validateSyntax, then the manifest read with
+ * its permissions / maxTakeBps rows and the CONTRACT_META_REQUIRED meta
+ * ladder, all resolved at the configured network / coin / block) and hands
+ * the verdict back as `deployGate`, warning once on a
* reject, but it still registers the contract: simulating a source the chain
* would not accept is a legitimate move, and this repo's own fixtures do it to
* measure the runtime strips. A `deployGate.valid === false` means the later
@@ -197,6 +206,8 @@ function initializeBlockContext(opts) {
this._preHeightGateWarned = false;
// Likewise for the deploy-gate rejection warning (see warnDeployGate).
this._deployGateWarned = false;
+ // Likewise for the balance-scope warning (see warnIfBalanceOutOfScope).
+ this._balanceScopeWarned = false;
}
class ContractSimulator {
@@ -254,7 +265,8 @@ class ContractSimulator {
// Read-only snapshots the author seeds.
this.balances = {}; // address -> tick -> amountStr
this.tokenInfo = {}; // tick -> info object
- this.oracle = { snapshotAge: 0, prices: {}, rounds: {} };
+ // roundFloor: oldest round the snapshot guarantees; 0 means no history is hidden.
+ this.oracle = { snapshotAge: 0, prices: {}, rounds: {}, roundFloor: 0 };
this.crossChainData = { attestations: {}, settled: {}, calls: {} };
// The remaining read-only snapshots the gateway reads. Shapes are the
// ones src/readonly_accessors.js documents; an empty snapshot is
diff --git a/src/toolkit/simulator/constants.js b/src/toolkit/simulator/constants.js
index f662d3b..1f00281 100644
--- a/src/toolkit/simulator/constants.js
+++ b/src/toolkit/simulator/constants.js
@@ -96,11 +96,24 @@ const GUARD_PARAM_ORDER = Object.freeze([
'actionType', 'from', 'to', 'tick', 'amount', 'price', 'proceedsTick'
]);
+// Activation instants of CONTRACT_META_REQUIRED, the indexer's deploy rule that a
+// contract export a conforming `meta`. The rule is an indexer registry row with no
+// VM twin, so these are a deliberate second home for its flag times
+// (xchain-indexer/src/protocol_changes/flag_times.js), compared against that file by
+// test/determinism/simulator_manifest_gate_cross_repo.test.js. Mainnet and regtest are
+// genesis-active; any network not named here resolves like mainnet.
+const CONTRACT_META_REQUIRED_TIMES = Object.freeze({
+ mainnet: 0,
+ testnet: 1789257600,
+ regtest: 0
+});
+
module.exports = {
GATE_BLOCK_TIMES,
GATE_FALLBACK_BLOCK_TIME,
HEIGHT_GATES,
GENESIS_ACTIVE_NETWORKS,
+ CONTRACT_META_REQUIRED_TIMES,
GUARD_GAS_CEILING,
GUARD_METHOD,
GUARD_PARAM_ORDER
diff --git a/src/toolkit/simulator/execution.js b/src/toolkit/simulator/execution.js
index 94dee20..ef3e758 100644
--- a/src/toolkit/simulator/execution.js
+++ b/src/toolkit/simulator/execution.js
@@ -53,17 +53,20 @@ module.exports = {
* @param {string[]} [opts.constructorParams] - if present, runs `initialize`
* @param {string} [opts.caller]
* @returns {Promise<{contractIndex, contractAddress, initResult, deployGate}>}
- * deployGate is the chain's deploy verdict, `{ valid: true }` or
- * `{ valid: false, error }`. Advisory: a reject warns once and the
- * contract is still registered (see deployGateVerdict).
+ * deployGate is the chain's deploy verdict over all three legs (size
+ * cap, validateSyntax, manifest + meta), `{ valid: true }` or
+ * `{ valid: false, error }`, or `valid: null` when a leg could not run
+ * on this host. Advisory: a reject warns once and the contract is
+ * still registered (see deployGateVerdict, manifestGateVerdict).
*/
async deploy(code, opts = {}) {
const src = toContractJs(code, opts.filename || '');
- const deployGate = this.deployGateVerdict(src);
- if (deployGate.valid !== true) this.warnDeployGate(deployGate);
const index = (opts.contractIndex != null) ? Number(opts.contractIndex) : this._nextIndex;
if (index >= this._nextIndex) this._nextIndex = index + 1;
const address = opts.contractAddress || ('C:' + this.coin + ':' + index);
+ let deployGate = this.deployGateVerdict(src);
+ if (deployGate.valid === true) deployGate = await this.manifestGateVerdict(src, address);
+ if (deployGate.valid !== true) this.warnDeployGate(deployGate);
this.contracts.set(index, {
code: src,
@@ -144,15 +147,17 @@ module.exports = {
if (!contract) {
throw new Error('no contract deployed at index ' + contractIndex);
}
+ const caller = opts.caller || this.defaultCaller;
this.warnIfPreGate();
this.warnIfPreHeightGate(contract.address);
+ this.warnIfBalanceOutOfScope(caller, contract.address);
const execOpts = {
code: contract.code,
state: contract.state,
method: method || 'default',
params: Array.isArray(params) ? params : [],
- caller: opts.caller || this.defaultCaller,
+ caller,
contractAddress: contract.address,
contractIndex: Number(contractIndex),
network: this.network,
diff --git a/src/toolkit/simulator/gate_warnings.js b/src/toolkit/simulator/gate_warnings.js
index 7eae04a..3437264 100644
--- a/src/toolkit/simulator/gate_warnings.js
+++ b/src/toolkit/simulator/gate_warnings.js
@@ -16,6 +16,11 @@
const XChainVM = require('../../index.js');
const { HEIGHT_GATES, GENESIS_ACTIVE_NETWORKS } = require('./constants.js');
const { heightGateThreshold, defaultBlockHeight } = require('./block_time_gates.js');
+const {
+ isContractMetaRequiredActive,
+ manifestPolicyError,
+ contractMetaError
+} = require('./manifest_gate.js');
module.exports = {
// ---- block control ------------------------------------------------------
@@ -95,8 +100,32 @@ module.exports = {
},
/**
- * Run the chain's DEPLOY gate over already-transpiled source and return its
- * verdict as `{ valid, error }`. ADVISORY: deploy() reports it and warns once,
+ * Warn once per simulator when a seeded balance sits outside the two addresses
+ * a node's snapshot carries. xchain-indexer builds the getBalance snapshot for
+ * the action's SOURCE and the contract's own address only (run_vm.js, and the
+ * constructor and controller-guard paths alike), so any other seeded address
+ * reads a value here and null on chain. Advisory: a suite that seeds several
+ * callers is legitimate, and the seed is left in place.
+ */
+ warnIfBalanceOutOfScope(caller, contractAddress) {
+ if (this._balanceScopeWarned) return;
+ const outside = Object.keys(this.balances)
+ .filter((a) => a !== caller && a !== contractAddress);
+ if (!outside.length) return;
+ this._balanceScopeWarned = true;
+ console.warn(
+ '[xchain-vm simulator] balances are seeded for ' + JSON.stringify(outside) +
+ ', outside this call\'s snapshot scope (caller ' + JSON.stringify(caller) +
+ ', contract ' + JSON.stringify(contractAddress) + '). A node preloads only ' +
+ 'those two addresses, so a contract reading getBalance for any other one ' +
+ 'gets null on chain whatever it reads here.'
+ );
+ },
+
+ /**
+ * Run the first two legs of the chain's DEPLOY gate (size cap, validateSyntax)
+ * over already-transpiled source and return their verdict as `{ valid, error }`;
+ * manifestGateVerdict is the third leg. ADVISORY: deploy() reports it and warns once,
* it never refuses, because simulating a source the chain would reject is a
* legitimate move (this repo's own fixtures deploy a WebAssembly probe to
* measure the runtime strip) and a public toolkit API that started throwing
@@ -143,6 +172,36 @@ module.exports = {
}
},
+ /**
+ * Run the third leg of the chain's DEPLOY gate: read the contract's manifest the
+ * way xchain-indexer/src/actions/deploy/manifest.js does (same call, same block
+ * context, the real contract address the sandbox gate derives its coin from),
+ * then judge the policy rows and, once CONTRACT_META_REQUIRED is armed at this
+ * block, the meta ladder. A module top level that throws is a real reject (the
+ * chain records `manifest read failed`), so only a read that throws, a host
+ * fault rather than a verdict, returns the null "gate did not run" verdict.
+ * @param {string} src - transpiled source that already passed the first two legs
+ * @param {string} contractAddress
+ * @returns {Promise<{valid: (boolean|null), error?: string}>}
+ */
+ async manifestGateVerdict(src, contractAddress) {
+ let read;
+ try {
+ read = await this.vm.readManifest(src, {
+ network: this.network,
+ contractAddress,
+ blockContext: { height: this.block.height, timestamp: this.block.timestamp }
+ });
+ } catch (e) {
+ return { valid: null, error: 'deploy gate could not run on this host: ' + e.message };
+ }
+ const policy = manifestPolicyError(read);
+ if (policy) return { valid: false, error: policy };
+ if (!isContractMetaRequiredActive(this.network, this.block.timestamp)) return { valid: true };
+ const meta = contractMetaError(read);
+ return meta ? { valid: false, error: meta } : { valid: true };
+ },
+
/**
* Warn once per simulator when the deploy gate rejects a source. Fires only on
* a REJECT, so a clean contract keeps the simulator silent, and once per
@@ -158,9 +217,12 @@ module.exports = {
'the acorn half of the same gate without an isolate.');
return;
}
+ // A manifest-leg error is already the status the chain writes; a lint-leg one is wrapped.
+ const status = /^invalid: /.test(String(verdict.error))
+ ? verdict.error : 'invalid: CODE_ENCODING (...)';
console.warn(
'[xchain-vm simulator] the DEPLOY gate rejects this contract: ' + verdict.error +
- '. On chain xchain-indexer records `invalid: CODE_ENCODING (...)` and the contract ' +
+ '. On chain xchain-indexer records `' + status + '` and the contract ' +
'never exists, so any call() result below is simulation-only. The verdict rides ' +
'back on deploy() as `deployGate`; `xchain-foundry lint` reports it without an isolate.'
);
diff --git a/src/toolkit/simulator/manifest_gate.js b/src/toolkit/simulator/manifest_gate.js
new file mode 100644
index 0000000..f6e46d8
--- /dev/null
+++ b/src/toolkit/simulator/manifest_gate.js
@@ -0,0 +1,114 @@
+/*********************************************************************
+ *
+ * Copyright © 2025–2026 Dankest, LLC
+ * Based on XChain Platform by Dankest, LLC – https://dankest.llc
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * This file is part of XChain Platform. Licensed under the GNU Affero
+ * General Public License v3.0 or later; see LICENSE.md. A commercial
+ * license (without AGPL source-disclosure terms) is available -
+ * contact legal@dankest.llc.
+ *
+ **********************************************************************
+ * The third leg of the chain's DEPLOY gate, judged over a vm.readManifest()
+ * result: the permissions / maxTakeBps policy rows and the
+ * CONTRACT_META_REQUIRED verdict ladder, in the order the indexer walks them
+ * (xchain-indexer/src/actions/deploy/manifest.js and contract_meta.js). The
+ * verdict strings are consensus tokens the chain writes into the action's
+ * status, so each one here is the indexer's byte for byte;
+ * test/determinism/simulator_manifest_gate_cross_repo.test.js compares them.
+ ********************************************************************/
+// @ts-nocheck
+
+const {
+ META_VERDICTS,
+ META_NAME_MAX_BYTES,
+ META_DESCRIPTION_MAX_BYTES,
+ META_VERSION_MAX_BYTES,
+ isValidMetaText
+} = require('../gate/meta_validation.js');
+const { CONTRACT_META_REQUIRED_TIMES } = require('./constants.js');
+
+// Policy-row rejections, written verbatim into the DEPLOY status by applyManifestPolicy.
+const MANIFEST_POLICY_VERDICTS = Object.freeze({
+ PERMISSIONS_ARRAY: 'invalid: CONTRACT_MANIFEST (permissions must be an array)',
+ PERMISSIONS_STRINGS: 'invalid: CONTRACT_MANIFEST (permissions must be action-type strings)',
+ MAX_TAKE_BPS: 'invalid: CONTRACT_MANIFEST (maxTakeBps must be an integer in [0, 10000])'
+});
+
+/**
+ * Whether CONTRACT_META_REQUIRED is armed at this block time. The indexer reads
+ * median-time-past off mainnet; the simulator has only the block's own stamp.
+ * @param {string} network
+ * @param {number} blockTime - seconds
+ * @returns {boolean}
+ */
+function isContractMetaRequiredActive(network, blockTime) {
+ const known = Object.prototype.hasOwnProperty.call(CONTRACT_META_REQUIRED_TIMES, network);
+ const at = CONTRACT_META_REQUIRED_TIMES[known ? network : 'mainnet'];
+ return Number(blockTime) >= at;
+}
+
+/**
+ * The permissions and maxTakeBps rows. Ungated on chain, and judged only over a
+ * read that succeeded, exactly as applyManifestPolicy is.
+ * @param {?object} read - vm.readManifest() result
+ * @returns {?string} the chain's status string, or null when both rows pass
+ */
+function manifestPolicyError(read) {
+ if (!read || !read.success || !read.manifest) return null;
+ const m = read.manifest;
+ if (m.permissionsType !== 'undefined') {
+ if (m.permissionsType !== 'array' || !Array.isArray(m.permissions))
+ return MANIFEST_POLICY_VERDICTS.PERMISSIONS_ARRAY;
+ if (!m.permissions.every((p) => typeof p === 'string'))
+ return MANIFEST_POLICY_VERDICTS.PERMISSIONS_STRINGS;
+ }
+ if (m.maxTakeBpsType !== 'undefined') {
+ const mtb = m.maxTakeBps;
+ if (m.maxTakeBpsType !== 'number' || !Number.isInteger(mtb) || mtb < 0 || mtb > 10000)
+ return MANIFEST_POLICY_VERDICTS.MAX_TAKE_BPS;
+ }
+ return null;
+}
+
+/**
+ * Rows 1 to 4 of the meta ladder, then the host's own parse of the reported
+ * JSON; first failure wins, as in evaluateContractMeta.
+ * @param {?object} read - vm.readManifest() result
+ * @returns {?string} the chain's status string, or null for a conforming meta
+ */
+function contractMetaError(read) {
+ if (!read || read.success !== true || !read.manifest) return META_VERDICTS.READ_FAILED;
+ const m = read.manifest;
+ const metaType = (m.metaType === undefined || m.metaType === null) ? 'undefined' : m.metaType;
+ if (metaType === 'undefined') return META_VERDICTS.REQUIRED;
+ if (metaType !== 'object' || m.metaError === true) return META_VERDICTS.NOT_OBJECT;
+ if (m.metaOversize === true) return META_VERDICTS.OVERSIZE;
+ let parsed = null;
+ if (typeof m.metaJson === 'string') {
+ try { parsed = JSON.parse(m.metaJson); } catch (e) { parsed = null; }
+ }
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
+ return META_VERDICTS.NOT_OBJECT;
+ return metaFieldsError(parsed);
+}
+
+// Rows 5 to 7: name, description, then version when the key is present at all.
+function metaFieldsError(parsed) {
+ if (!isValidMetaText(parsed.name, META_NAME_MAX_BYTES, false)) return META_VERDICTS.NAME;
+ if (!isValidMetaText(parsed.description, META_DESCRIPTION_MAX_BYTES, true))
+ return META_VERDICTS.DESCRIPTION;
+ if (Object.prototype.hasOwnProperty.call(parsed, 'version') &&
+ !isValidMetaText(parsed.version, META_VERSION_MAX_BYTES, false))
+ return META_VERDICTS.VERSION;
+ return null;
+}
+
+module.exports = {
+ MANIFEST_POLICY_VERDICTS,
+ isContractMetaRequiredActive,
+ manifestPolicyError,
+ contractMetaError
+};
diff --git a/src/toolkit/simulator/world_state_setters.js b/src/toolkit/simulator/world_state_setters.js
index 4886307..f303249 100644
--- a/src/toolkit/simulator/world_state_setters.js
+++ b/src/toolkit/simulator/world_state_setters.js
@@ -60,6 +60,18 @@ module.exports = {
return this;
},
+ /**
+ * Set the oldest oracle round this simulated host guarantees. 0 (the default)
+ * hides no history. A positive floor is the bounded window a node ships: an
+ * unseeded round below it reads back from oracle.getPriceAtRound as
+ * { price: null, outsideWindow: true } rather than as plain null.
+ */
+ setOracleRoundFloor(roundFloor) {
+ const n = Number(roundFloor);
+ this.oracle.roundFloor = (Number.isFinite(n) && n > 0) ? n : 0;
+ return this;
+ },
+
/**
* Seed a settled ATTEST response (read by attestation.getResponse in a
* callback method). Keys are request_ids; the simulator does not derive
@@ -74,7 +86,9 @@ module.exports = {
* Seed a finalized VOTE poll result (read by poll.getPollResult).
* @param {number|string} pollIndex - the VOTE v0 action_index
* @param {object} result - { status, winning_option, total_weight,
- * total_voters, decided_early, options:[{index,weight,voters}] }
+ * total_voters, decided_early, options:[{index,weight,voters}],
+ * tick? } (tick: the electorate token, present once the host
+ * enables VOTE_POLL_TICK_VISIBLE)
*/
setPollResult(pollIndex, result) {
this.pollData.polls[String(pollIndex)] = result;
diff --git a/src/vm_worker.js b/src/vm_worker.js
index fbf77a6..5a30747 100644
--- a/src/vm_worker.js
+++ b/src/vm_worker.js
@@ -67,80 +67,98 @@ function send(msg) {
}
}
-process.on('message', (msg) => {
+function handleInitMessage(msg) {
+ // Force in-process mode in the child (never recurse into another fork).
+ vm = new XChainVM(Object.assign({}, msg.config, { execution: 'in-process' }));
+ send({ type: 'ready' });
+}
+
+function handleBeginBlockMessage() {
+ enqueue(() => { if (vm) vm.beginBlock(); });
+}
+
+function handleEndBlockMessage() {
+ enqueue(() => { if (vm) { vm.endBlock(); flushCoverage(); } });
+}
+
+// vm.execute() throwing is a HOST fault, not a contract outcome, and
+// the two host-fault shapes need OPPOSITE handling.
+//
+// A HostFaultError says THIS MACHINE cannot run the contract at all:
+// index.js raises it when isolateManager.createIsolate() fails, and
+// syntax.js raises it when the execute-time lint isolate cannot be
+// spawned (reaching execute() through getLintVerdict, outside its own
+// try block). Both are properties of this host's memory/thread budget,
+// not of the contract, so every healthy peer commits a normal result
+// for the same execution. Dying here handed the parent's crash clamp a
+// dispatched entry, which resolved a committed
+// 'out_of_resource: execution host terminated' at gasUsed = ceiling --
+// a unilateral fork, and the exact laundering index.js:classifyError
+// re-throws HostFaultError to prevent. Report it instead; the parent
+// rejects the request so the caller HALTS and retries, which is the
+// rule process_executor already enforces for queued and shutdown work.
+// Tested with instanceof, never e.code or the message: an error that
+// crossed the isolate boundary arrives as a plain host Error built from
+// contract-controlled text, and a name/code match would let a contract
+// spoof a chain-wide halt (same anti-spoof rule as classifyError).
+//
+// Every OTHER throw still dies into the parent's deterministic
+// host-termination machinery (onExit -> hostTerminatedResult), which
+// clamps the request to its caller-funded ceiling identically on every
+// validator, exactly as an in-isolate resource failure would.
+function handleExecuteMessage(msg) {
+ enqueue(async () => {
+ let result;
+ try {
+ result = await vm.execute(msg.opts);
+ } catch (e) {
+ if (e instanceof HostFaultError) {
+ send({
+ type: 'hostfault',
+ id: msg.id,
+ reason: String((e && e.message) || 'executor unavailable').slice(0, 200)
+ });
+ // The isolate never came up, so worker state is intact (execute()'s
+ // finally disposes nothing when createIsolate threw). Staying alive
+ // avoids a respawn for what is usually a transient pressure blip.
+ flushCoverage();
+ return;
+ }
+ process.exit(1);
+ return;
+ }
+ send({ type: 'result', id: msg.id, result });
+ // Persist this execution's coverage before the parent can SIGKILL the
+ // worker (inert unless a coverage harness set NODE_V8_COVERAGE).
+ flushCoverage();
+ });
+}
+
+function dispatchMessage(msg) {
if (!msg) return;
if (msg.type === 'init') {
- // Force in-process mode in the child (never recurse into another fork).
- vm = new XChainVM(Object.assign({}, msg.config, { execution: 'in-process' }));
- send({ type: 'ready' });
+ handleInitMessage(msg);
return;
}
if (msg.type === 'beginBlock') {
- enqueue(() => { if (vm) vm.beginBlock(); });
+ handleBeginBlockMessage();
return;
}
if (msg.type === 'endBlock') {
- enqueue(() => { if (vm) { vm.endBlock(); flushCoverage(); } });
+ handleEndBlockMessage();
return;
}
if (msg.type === 'execute') {
- enqueue(async () => {
- let result;
- try {
- result = await vm.execute(msg.opts);
- } catch (e) {
- // vm.execute() throwing is a HOST fault, not a contract outcome, and
- // the two host-fault shapes need OPPOSITE handling.
- //
- // A HostFaultError says THIS MACHINE cannot run the contract at all:
- // index.js raises it when isolateManager.createIsolate() fails, and
- // syntax.js raises it when the execute-time lint isolate cannot be
- // spawned (reaching execute() through getLintVerdict, outside its own
- // try block). Both are properties of this host's memory/thread budget,
- // not of the contract, so every healthy peer commits a normal result
- // for the same execution. Dying here handed the parent's crash clamp a
- // dispatched entry, which resolved a committed
- // 'out_of_resource: execution host terminated' at gasUsed = ceiling --
- // a unilateral fork, and the exact laundering index.js:classifyError
- // re-throws HostFaultError to prevent. Report it instead; the parent
- // rejects the request so the caller HALTS and retries, which is the
- // rule process_executor already enforces for queued and shutdown work.
- // Tested with instanceof, never e.code or the message: an error that
- // crossed the isolate boundary arrives as a plain host Error built from
- // contract-controlled text, and a name/code match would let a contract
- // spoof a chain-wide halt (same anti-spoof rule as classifyError).
- //
- // Every OTHER throw still dies into the parent's deterministic
- // host-termination machinery (onExit -> hostTerminatedResult), which
- // clamps the request to its caller-funded ceiling identically on every
- // validator, exactly as an in-isolate resource failure would.
- if (e instanceof HostFaultError) {
- send({
- type: 'hostfault',
- id: msg.id,
- reason: String((e && e.message) || 'executor unavailable').slice(0, 200)
- });
- // The isolate never came up, so worker state is intact (execute()'s
- // finally disposes nothing when createIsolate threw). Staying alive
- // avoids a respawn for what is usually a transient pressure blip.
- flushCoverage();
- return;
- }
- process.exit(1);
- return;
- }
- send({ type: 'result', id: msg.id, result });
- // Persist this execution's coverage before the parent can SIGKILL the
- // worker (inert unless a coverage harness set NODE_V8_COVERAGE).
- flushCoverage();
- });
+ handleExecuteMessage(msg);
return;
}
-});
+}
+
+process.on('message', dispatchMessage);
// If the parent disconnects (shutdown / crash), flush any pending coverage
// (best-effort; races the parent's SIGKILL) and exit cleanly.
diff --git a/test/determinism/simulator_manifest_gate_cross_repo.test.js b/test/determinism/simulator_manifest_gate_cross_repo.test.js
new file mode 100644
index 0000000..54d282a
--- /dev/null
+++ b/test/determinism/simulator_manifest_gate_cross_repo.test.js
@@ -0,0 +1,97 @@
+'use strict';
+
+/*********************************************************************
+ *
+ * Copyright © 2025–2026 Dankest, LLC
+ * Based on XChain Platform by Dankest, LLC – https://dankest.llc
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * This file is part of XChain Platform. Licensed under the GNU Affero
+ * General Public License v3.0 or later; see LICENSE.md. A commercial
+ * license (without AGPL source-disclosure terms) is available -
+ * contact legal@dankest.llc.
+ *
+ **********************************************************************
+ * Parity gate for the simulator's third DEPLOY leg against the indexer that
+ * owns the rule. The toolkit carries second copies of three consensus facts:
+ * the CONTRACT_MANIFEST verdict strings, the policy-row strings, and the
+ * CONTRACT_META_REQUIRED flag times. Each is compared here against the
+ * sibling xchain-indexer, so a change on that side reddens this repo instead
+ * of leaving the simulator quoting a status the chain no longer writes.
+ *
+ * Sibling reads skip when the sibling is absent, except where
+ * XCHAIN_REQUIRE_SIBLINGS=1 is exported (bin/ci-all.sh), where a missing
+ * sibling is a hard failure rather than a green-by-skip.
+ ********************************************************************/
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const { META_VERDICTS } = require('../../src/toolkit/gate/meta_validation.js');
+
+// constants.js loads the VM, so the flag-time and policy checks skip with it.
+let simConstants = null;
+let manifestGate = null;
+try {
+ simConstants = require('../../src/toolkit/simulator/constants.js');
+ manifestGate = require('../../src/toolkit/simulator/manifest_gate.js');
+} catch (e) {
+ console.log('Skipping simulator manifest-gate parity (isolated-vm unavailable):', e.message);
+}
+
+const PLATFORM_ROOT = path.resolve(__dirname, '..', '..', '..');
+const REQUIRE_SIBLINGS = process.env.XCHAIN_REQUIRE_SIBLINGS === '1';
+
+// Resolves a sibling path, skipping (or failing under XCHAIN_REQUIRE_SIBLINGS) when absent.
+function siblingOrSkip(ctx, rel) {
+ const abs = path.join(PLATFORM_ROOT, rel);
+ if (fs.existsSync(abs)) return abs;
+ if (REQUIRE_SIBLINGS) {
+ assert.fail('manifest-gate parity cannot run: ' + rel + ' missing at ' + abs +
+ '; XCHAIN_REQUIRE_SIBLINGS=1 forbids the green-by-skip');
+ }
+ ctx.skip();
+ return null;
+}
+
+describe('simulator deploy-gate manifest leg agrees with the indexer', function () {
+
+ it('carries the indexer contract_meta VERDICTS byte for byte', function () {
+ const file = siblingOrSkip(this, path.join('xchain-indexer', 'src', 'actions', 'deploy', 'contract_meta.js'));
+ if (!file) return;
+ const { VERDICTS } = require(file);
+ assert.deepStrictEqual(Object.assign({}, META_VERDICTS), Object.assign({}, VERDICTS),
+ 'toolkit META_VERDICTS drifted from the consensus strings the indexer writes');
+ });
+
+ it('carries every manifest policy string the indexer writes', function () {
+ if (!manifestGate) this.skip();
+ const file = siblingOrSkip(this, path.join('xchain-indexer', 'src', 'actions', 'deploy', 'manifest.js'));
+ if (!file) return;
+ const src = fs.readFileSync(file, 'utf8');
+ const written = src.match(/'invalid: CONTRACT_MANIFEST \([^']*\)'/g) || [];
+ assert.ok(written.length > 0, 'no CONTRACT_MANIFEST literal found in ' + file + '; re-point this scrape');
+ const ours = Object.values(manifestGate.MANIFEST_POLICY_VERDICTS).map((s) => "'" + s + "'");
+ assert.deepStrictEqual(ours.slice().sort(), written.slice().sort(),
+ 'the policy strings in manifest.js and MANIFEST_POLICY_VERDICTS differ');
+ });
+
+ it('arms CONTRACT_META_REQUIRED at the indexer flag times', function () {
+ if (!simConstants) this.skip();
+ const flagFile = siblingOrSkip(this, path.join('xchain-indexer', 'src', 'protocol_changes', 'flag_times.js'));
+ if (!flagFile) return;
+ const rowFile = siblingOrSkip(this, path.join('xchain-indexer', 'src', 'protocol_changes', 'changes_4.js'));
+ if (!rowFile) return;
+ const flags = require(flagFile);
+ const row = fs.readFileSync(rowFile, 'utf8').match(
+ /\['CONTRACT_META_REQUIRED',\s*'[^']*',\s*CONTRACT_META_REQUIRED_MAINNET_TIME,\s*CONTRACT_META_REQUIRED_TESTNET_TIME,\s*(\d+)/);
+ assert.ok(row, 'CONTRACT_META_REQUIRED row not found in ' + rowFile + '; re-point this scrape');
+ assert.deepStrictEqual(Object.assign({}, simConstants.CONTRACT_META_REQUIRED_TIMES), {
+ mainnet: flags.CONTRACT_META_REQUIRED_MAINNET_TIME,
+ testnet: flags.CONTRACT_META_REQUIRED_TESTNET_TIME,
+ regtest: Number(row[1])
+ }, 'the simulator arms the meta rule at a different instant than the chain');
+ });
+});
diff --git a/test/e2e/helpers/MockLedger.js b/test/e2e/helpers/MockLedger.js
index a078187..411b0c1 100644
--- a/test/e2e/helpers/MockLedger.js
+++ b/test/e2e/helpers/MockLedger.js
@@ -268,7 +268,8 @@ class MockLedger {
// Seed a finalized poll readable via xchain.getPollResult. `result` mirrors
// the indexer's getPollResultsForVM map entries: { status, winning_option,
- // total_weight, total_voters, decided_early, options:[{index,weight,voters}] }.
+ // total_weight, total_voters, decided_early, options:[{index,weight,voters}],
+ // tick? } (tick present once the host enables VOTE_POLL_TICK_VISIBLE).
seedPollResult(pollIndex, result) {
this.pollResults[String(pollIndex)] = result;
}
diff --git a/test/integration/process_executor.test/07_a_queued_request_never_times_out_on_queue_wait_watchdog_starts_at_dispatch.test.js b/test/integration/process_executor.test/07_a_queued_request_never_times_out_on_queue_wait_watchdog_starts_at_dispatch.test.js
index 0c37f9c..2236358 100644
--- a/test/integration/process_executor.test/07_a_queued_request_never_times_out_on_queue_wait_watchdog_starts_at_dispatch.test.js
+++ b/test/integration/process_executor.test/07_a_queued_request_never_times_out_on_queue_wait_watchdog_starts_at_dispatch.test.js
@@ -45,15 +45,24 @@ const { assert, hashResult, GAS_SCHEDULE, LIMITS, GAS_CEILING, makeVM, BASE, HAV
// Shrink the watchdog so queue wait spans several windows, then
// hold dispatch closed (exactly the backed-up/respawning state).
+ const defaultWatchdogMs = exec._watchdogMs;
exec._watchdogMs = 250;
exec._sawReady = false;
const queued = exec.execute({ ...BASE, code: `module.exports = function(){ return 'queued'; };` });
+ // Observation window, not a synchronization point: do not convert to
+ // a poll. The claim is that the queue does NOT drain while 3+ shrunk
+ // windows elapse, and a timer armed at acceptance fires before this
+ // one does, so host load cannot turn a regression green.
await new Promise((r) => setTimeout(r, 800));
assert.strictEqual(exec._queue.length, 1,
'request must still be queued after 3+ watchdog windows, not resolved');
+ // Restore the real bound before dispatch so a loaded host cannot trip
+ // the shrunk window while the contract itself runs.
+ exec._watchdogMs = defaultWatchdogMs;
+
// Worker becomes dispatchable again → the contract must RUN.
exec._sawReady = true;
exec.flush();
diff --git a/test/toolkit/meta_validation_codepoints.test.js b/test/toolkit/meta_validation_codepoints.test.js
new file mode 100644
index 0000000..342031b
--- /dev/null
+++ b/test/toolkit/meta_validation_codepoints.test.js
@@ -0,0 +1,54 @@
+/*********************************************************************
+ *
+ * Copyright © 2025–2026 Dankest, LLC
+ * Based on XChain Platform by Dankest, LLC – https://dankest.llc
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * This file is part of XChain Platform. Licensed under the GNU Affero
+ * General Public License v3.0 or later; see LICENSE.md. A commercial
+ * license (without AGPL source-disclosure terms) is available -
+ * contact legal@dankest.llc.
+ *
+ **********************************************************************
+ * Toolkit gate: the meta-text code-point tables (isBannedMetaCodePoint,
+ * isMetaEdgeCodePoint) in isolation from the rest of the gate.
+ *
+ * Requires meta_validation.js directly, never the toolkit index, so this
+ * suite stays runnable where isolated-vm cannot dlopen.
+ ********************************************************************/
+// @ts-nocheck
+
+const assert = require('assert');
+const { isBannedMetaCodePoint, isMetaEdgeCodePoint } = require('../../src/toolkit/gate/meta_validation.js');
+
+describe('Toolkit gate: meta text code-point tables', function () {
+
+ describe('isBannedMetaCodePoint', function () {
+ it('bans LF (U+000A) when allowLf is false', function () {
+ assert.strictEqual(isBannedMetaCodePoint(0x0A, false), true);
+ });
+
+ it('allows LF (U+000A) when allowLf is true', function () {
+ assert.strictEqual(isBannedMetaCodePoint(0x0A, true), false);
+ });
+
+ it('bans the word joiner (U+2060)', function () {
+ assert.strictEqual(isBannedMetaCodePoint(0x2060, false), true);
+ });
+
+ it('does not ban an ordinary ASCII letter (U+0041)', function () {
+ assert.strictEqual(isBannedMetaCodePoint(0x41, false), false);
+ });
+ });
+
+ describe('isMetaEdgeCodePoint', function () {
+ it('treats the ASCII space (U+0020) as an edge code point', function () {
+ assert.strictEqual(isMetaEdgeCodePoint(0x20, false), true);
+ });
+
+ it('does not treat an ordinary ASCII letter (U+0041) as an edge code point', function () {
+ assert.strictEqual(isMetaEdgeCodePoint(0x41, false), false);
+ });
+ });
+});
diff --git a/test/toolkit/simulator.test.js b/test/toolkit/simulator.test.js
index d81a85a..6fb9037 100644
--- a/test/toolkit/simulator.test.js
+++ b/test/toolkit/simulator.test.js
@@ -30,6 +30,7 @@ try {
const COUNTER = `
module.exports = {
+ meta: { name: 'Counter', description: 'A persisted counter' },
initialize: function(xchain) {
var start = xchain.getInputParam(0);
if (start === null || start === undefined) start = '0';
@@ -83,7 +84,8 @@ module.exports = {
});
it('exposes seeded balances to getBalance', async function() {
- const sim = new ContractSimulator({ coin: 'BTC' });
+ // alice is the caller, one of the two addresses a node's snapshot carries.
+ const sim = new ContractSimulator({ coin: 'BTC', defaultCaller: 'alice' });
sim.setBalance('alice', 'GOLD', '1000');
try {
const dep = await sim.deploy(
@@ -136,14 +138,16 @@ module.exports = {
} finally { await sim.close(); }
});
- it('reports the chain deploy verdict, on mainnet too, where nothing else lints', async function() {
+ it('reports the mainnet deploy verdict and rejects banned syntax again at call time', async function() {
// This gate is what surfaces a chain-rejected source at DEPLOY, where the
// author sees it. The 2026-09-09 ruling armed EXEC_LINT_ACTIVATION on mainnet,
// so a mainnet simulator now re-lints at call() time too, but only for a source
// that is actually called; a deploy-rejected source that is never called stays
// silent without this gate. The case asserted here is the readable one because
// banned-math also strips Math.sqrt, so the rejection is observable either way.
- const sim = new ContractSimulator({ coin: 'BTC', network: 'mainnet' });
+ const sim = new ContractSimulator({
+ coin: 'BTC', network: 'mainnet', block: { height: 0 }
+ });
const warned = [];
const real = console.warn;
console.warn = (...a) => warned.push(a.join(' '));
@@ -156,6 +160,9 @@ module.exports = {
// Advisory by design: the contract is still registered, so a fixture that
// deliberately simulates a chain-rejected source keeps working.
assert.strictEqual(sim.contracts.size, 1);
+ const res = await sim.call(dep.contractIndex, 'run', []);
+ assert.strictEqual(res.success, false);
+ assert.match(String(res.error), /^error: banned syntax: .*Math\.sqrt/);
// Warned once, not once per deploy.
await sim.deploy(
'module.exports = { run: function(xchain){ return String(Math.pow(2, 3)); } };');
@@ -187,7 +194,8 @@ module.exports = {
// accept a source whose only violation rides that unarmed gate, exactly as
// the chain accepted it at that height. Hardcoding the flags would reject it
// and teach the author their historical contract was never deployable.
- const WASM = 'module.exports = { probe: function(xchain){ return typeof WebAssembly; } };';
+ const WASM = 'module.exports = { meta: { name: "Wasm probe", description: "Reads WebAssembly" }, ' +
+ 'probe: function(xchain){ return typeof WebAssembly; } };';
const pre = new ContractSimulator({ coin: 'BTC', network: 'mainnet', block: { height: 1 } });
const at = new ContractSimulator({ coin: 'BTC', network: 'mainnet' });
const real = console.warn;
@@ -216,7 +224,8 @@ module.exports = {
// predicate on both sides of the activation.
const XChainVM = require('../../src/index.js');
const GATE = XChainVM.REST_PATTERN_METER_GATE_BLOCK_TIME;
- const REST = 'module.exports = { run: function(xchain, ...rest){ return String(rest.length); } };';
+ const REST = 'module.exports = { meta: { name: "Rest probe", description: "Counts rest params" }, ' +
+ 'run: function(xchain, ...rest){ return String(rest.length); } };';
const pre = new ContractSimulator({ coin: 'BTC', network: 'mainnet', block: { timestamp: GATE - 1 } });
const at = new ContractSimulator({ coin: 'BTC', network: 'mainnet', block: { timestamp: GATE } });
const real = console.warn;
diff --git a/test/toolkit/simulator.test/02_read_surface_scope.test.js b/test/toolkit/simulator.test/02_read_surface_scope.test.js
new file mode 100644
index 0000000..b73ede3
--- /dev/null
+++ b/test/toolkit/simulator.test/02_read_surface_scope.test.js
@@ -0,0 +1,119 @@
+/*********************************************************************
+ *
+ * Copyright © 2025–2026 Dankest, LLC
+ * Based on XChain Platform by Dankest, LLC – https://dankest.llc
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * This file is part of XChain Platform. Licensed under the GNU Affero
+ * General Public License v3.0 or later; see LICENSE.md. A commercial
+ * license (without AGPL source-disclosure terms) is available -
+ * contact legal@dankest.llc.
+ *
+ **********************************************************************
+ * Toolkit: ContractSimulator read-surface scope. Needs the isolated-vm
+ * binding, so the require is guarded and the suite SKIPS where it cannot
+ * dlopen. Runs for real on Node 22 / Linux (CI).
+ ********************************************************************/
+// @ts-nocheck
+
+const assert = require('assert');
+
+let ContractSimulator = null;
+try {
+ ({ ContractSimulator } = require('../../../src/toolkit/simulator.js'));
+} catch (e) {
+ console.log('Skipping toolkit simulator tests (isolated-vm unavailable):', e.message);
+}
+
+// Reads the round named by param 0 and classifies the answer the way a price-bet settle branch does.
+const ROUND_READER =
+ 'module.exports = function(xchain){ var r = xchain.oracle.getPriceAtRound("BTC/USD", ' +
+ 'Number(xchain.getInputParam(0))); return r ? (r.outsideWindow === true ? "outside" : "row") : "null"; };';
+
+const BALANCE_READER =
+ 'module.exports = function(xchain){ return xchain.getBalance(xchain.getInputParam(0), "GOLD"); };';
+
+// Captures console.warn lines for the duration of fn, restoring it even on a throw.
+async function captureWarnings(fn) {
+ const lines = [];
+ const real = console.warn;
+ console.warn = (...a) => lines.push(a.join(' '));
+ try { await fn(); } finally { console.warn = real; }
+ return lines;
+}
+
+(ContractSimulator ? describe : describe.skip)('Toolkit ContractSimulator: oracle round floor', function() {
+ this.timeout(30000);
+
+ it('carries roundFloor 0 by default, so a missing old round reads plain null', async function() {
+ const sim = new ContractSimulator();
+ sim.setPrice('BTC/USD', { price: '65000', roundNumber: 100 });
+ try {
+ assert.strictEqual(sim.oracle.roundFloor, 0);
+ const dep = await sim.deploy(ROUND_READER);
+ const res = await sim.call(dep.contractIndex, 'default', ['10']);
+ assert.strictEqual(JSON.parse(res.returnValue), 'null', res.error);
+ } finally { await sim.close(); }
+ });
+
+ it('answers outsideWindow below a floor set with setOracleRoundFloor', async function() {
+ const sim = new ContractSimulator();
+ sim.setPrice('BTC/USD', { price: '65000', roundNumber: 100 }).setOracleRoundFloor(50);
+ try {
+ const dep = await sim.deploy(ROUND_READER);
+ const below = await sim.call(dep.contractIndex, 'default', ['10']);
+ assert.strictEqual(JSON.parse(below.returnValue), 'outside', below.error);
+ const seeded = await sim.call(dep.contractIndex, 'default', ['100']);
+ assert.strictEqual(JSON.parse(seeded.returnValue), 'row', seeded.error);
+ const aboveFloor = await sim.call(dep.contractIndex, 'default', ['60']);
+ assert.strictEqual(JSON.parse(aboveFloor.returnValue), 'null', aboveFloor.error);
+ } finally { await sim.close(); }
+ });
+
+ it('normalizes a non-positive or non-numeric floor to 0', async function() {
+ const sim = new ContractSimulator();
+ try {
+ assert.strictEqual(sim.setOracleRoundFloor(-5).oracle.roundFloor, 0);
+ assert.strictEqual(sim.setOracleRoundFloor('x').oracle.roundFloor, 0);
+ assert.strictEqual(sim.setOracleRoundFloor('7').oracle.roundFloor, 7);
+ } finally { await sim.close(); }
+ });
+});
+
+(ContractSimulator ? describe : describe.skip)('Toolkit ContractSimulator: balance snapshot scope', function() {
+ this.timeout(30000);
+
+ it('warns once, naming the address, when a balance is seeded outside caller + contract', async function() {
+ const sim = new ContractSimulator({ coin: 'BTC' });
+ sim.setBalance('alice', 'GOLD', '1000');
+ let first = null;
+ const lines = await captureWarnings(async () => {
+ try {
+ const dep = await sim.deploy(BALANCE_READER);
+ first = await sim.call(dep.contractIndex, 'default', ['alice']);
+ await sim.call(dep.contractIndex, 'default', ['alice']);
+ } finally { await sim.close(); }
+ });
+ // Advisory only: the seed still reads back here.
+ assert.strictEqual(JSON.parse(first.returnValue), '1000', first.error);
+ const scope = lines.filter((l) => /outside this call's snapshot scope/.test(l));
+ assert.strictEqual(scope.length, 1, JSON.stringify(lines));
+ assert.match(scope[0], /"alice"/);
+ assert.match(scope[0], /"sim_caller"/);
+ });
+
+ it('stays silent when only the caller and the contract address are seeded', async function() {
+ const sim = new ContractSimulator({ coin: 'BTC', defaultCaller: 'alice' });
+ sim.setBalance('alice', 'GOLD', '1000').setBalance('C:BTC:0', 'GOLD', '5');
+ const lines = await captureWarnings(async () => {
+ try {
+ const dep = await sim.deploy(BALANCE_READER);
+ assert.strictEqual(dep.contractAddress, 'C:BTC:0');
+ await sim.call(dep.contractIndex, 'default', ['C:BTC:0']);
+ } finally { await sim.close(); }
+ });
+ const scope = lines.filter((l) => /outside this call's snapshot scope/.test(l));
+ assert.deepStrictEqual(scope, []);
+ });
+});
diff --git a/test/toolkit/simulator.test/03_deploy_gate_manifest_leg.test.js b/test/toolkit/simulator.test/03_deploy_gate_manifest_leg.test.js
new file mode 100644
index 0000000..3cc8490
--- /dev/null
+++ b/test/toolkit/simulator.test/03_deploy_gate_manifest_leg.test.js
@@ -0,0 +1,123 @@
+/*********************************************************************
+ *
+ * Copyright © 2025–2026 Dankest, LLC
+ * Based on XChain Platform by Dankest, LLC – https://dankest.llc
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * This file is part of XChain Platform. Licensed under the GNU Affero
+ * General Public License v3.0 or later; see LICENSE.md. A commercial
+ * license (without AGPL source-disclosure terms) is available -
+ * contact legal@dankest.llc.
+ *
+ **********************************************************************
+ * Toolkit: the third leg of the simulator's DEPLOY gate (manifest read,
+ * policy rows, CONTRACT_META_REQUIRED ladder). Needs the isolated-vm
+ * binding, so the require is guarded and the suite SKIPS where it cannot
+ * dlopen. Runs for real on Node 22 / Linux (CI).
+ ********************************************************************/
+// @ts-nocheck
+
+const assert = require('assert');
+
+let ContractSimulator = null;
+let gate = null;
+try {
+ ({ ContractSimulator } = require('../../../src/toolkit/simulator.js'));
+ gate = require('../../../src/toolkit/simulator/manifest_gate.js');
+} catch (e) {
+ console.log('Skipping toolkit simulator tests (isolated-vm unavailable):', e.message);
+}
+const { META_VERDICTS } = require('../../../src/toolkit/gate/meta_validation.js');
+
+const META = 'meta: { name: "Probe", description: "A probe contract" }, ';
+const withBody = (exportsBody) => 'module.exports = { ' + exportsBody + 'run: function(){ return "ok"; } };';
+
+// Deploys src on a fresh simulator, with warnings silenced, and returns the verdict.
+async function verdictOf(src, opts) {
+ const sim = new ContractSimulator(opts);
+ const real = console.warn;
+ console.warn = () => {};
+ try { return (await sim.deploy(src)).deployGate; } finally { console.warn = real; await sim.close(); }
+}
+
+(ContractSimulator ? describe : describe.skip)('Toolkit ContractSimulator: deploy gate manifest leg', function() {
+ this.timeout(30000);
+
+ it('rejects a contract with no meta on the default regtest network, and still registers it', async function() {
+ const sim = new ContractSimulator();
+ const lines = [];
+ const real = console.warn;
+ console.warn = (...a) => lines.push(a.join(' '));
+ try {
+ const dep = await sim.deploy(withBody(''));
+ assert.deepStrictEqual(dep.deployGate, { valid: false, error: META_VERDICTS.REQUIRED });
+ const res = await sim.call(dep.contractIndex, 'run', []);
+ assert.strictEqual(res.success, true, res.error);
+ assert.ok(lines.some((l) => l.includes('records `' + META_VERDICTS.REQUIRED + '`')),
+ JSON.stringify(lines));
+ } finally { console.warn = real; await sim.close(); }
+ });
+
+ it('accepts a conforming literal meta and a meta computed at module load', async function() {
+ assert.deepStrictEqual(await verdictOf(withBody(META)), { valid: true });
+ const computed = 'var n = ["Pro", "be"].join(""); module.exports = { meta: { name: n, ' +
+ 'description: "Built at load" }, run: function(){ return "ok"; } };';
+ assert.deepStrictEqual(await verdictOf(computed), { valid: true });
+ });
+
+ it('rejects a module top level that throws as a failed manifest read', async function() {
+ const src = 'throw new Error("boom"); module.exports = { ' + META + 'run: function(){ return 1; } };';
+ assert.deepStrictEqual(await verdictOf(src), { valid: false, error: META_VERDICTS.READ_FAILED });
+ });
+
+ it('rejects meta rows in ladder order with the chain strings', async function() {
+ const cases = [
+ ['meta: "Probe", ', META_VERDICTS.NOT_OBJECT],
+ ['meta: { name: " Probe", description: "d" }, ', META_VERDICTS.NAME],
+ ['meta: { name: "Probe", description: "" }, ', META_VERDICTS.DESCRIPTION],
+ ['meta: { name: "Probe", description: "d", version: 3 }, ', META_VERDICTS.VERSION]
+ ];
+ for (const [body, error] of cases) {
+ assert.deepStrictEqual(await verdictOf(withBody(body)), { valid: false, error }, body);
+ }
+ });
+});
+
+(ContractSimulator ? describe : describe.skip)('Toolkit ContractSimulator: deploy gate manifest leg', function() {
+ this.timeout(30000);
+
+ it('rejects malformed permissions and maxTakeBps ahead of the meta rows', async function() {
+ const P = gate.MANIFEST_POLICY_VERDICTS;
+ const cases = [
+ ['permissions: "SEND", ', P.PERMISSIONS_ARRAY],
+ ['permissions: [1], ', P.PERMISSIONS_STRINGS],
+ ['maxTakeBps: 20000, ', P.MAX_TAKE_BPS],
+ ['maxTakeBps: 1.5, ', P.MAX_TAKE_BPS]
+ ];
+ for (const [body, error] of cases) {
+ // No meta either: the policy row wins, as it does on chain.
+ assert.deepStrictEqual(await verdictOf(withBody(body)), { valid: false, error }, body);
+ }
+ assert.deepStrictEqual(
+ await verdictOf(withBody(META + 'permissions: ["SEND"], maxTakeBps: 500, ')), { valid: true });
+ });
+
+ it('arms the meta rule on testnet at its flag time and keeps the policy rows ungated', async function() {
+ const at = 1789257600;
+ const before = { network: 'testnet', block: { timestamp: at - 1 } };
+ assert.deepStrictEqual(await verdictOf(withBody(''), before), { valid: true });
+ assert.deepStrictEqual(await verdictOf(withBody(''), { network: 'testnet', block: { timestamp: at } }),
+ { valid: false, error: META_VERDICTS.REQUIRED });
+ assert.deepStrictEqual(await verdictOf(withBody('maxTakeBps: -1, '), before),
+ { valid: false, error: gate.MANIFEST_POLICY_VERDICTS.MAX_TAKE_BPS });
+ });
+
+ it('resolves the activation per network, unknown networks like mainnet', function() {
+ assert.strictEqual(gate.isContractMetaRequiredActive('mainnet', 0), true);
+ assert.strictEqual(gate.isContractMetaRequiredActive('regtest', 0), true);
+ assert.strictEqual(gate.isContractMetaRequiredActive('testnet', 1789257599), false);
+ assert.strictEqual(gate.isContractMetaRequiredActive('testnet', 1789257600), true);
+ assert.strictEqual(gate.isContractMetaRequiredActive('signet', 0), true);
+ });
+});
diff --git a/test/toolkit/simulator_block_time.test.js b/test/toolkit/simulator_block_time.test.js
index 007ef77..1c7581a 100644
--- a/test/toolkit/simulator_block_time.test.js
+++ b/test/toolkit/simulator_block_time.test.js
@@ -53,9 +53,11 @@ try {
// Allocates a large typed array: unmetered below the binary-alloc flag-day,
// charged at byte length at/after it. The state write keeps the call from being
-// optimized into nothing.
+// optimized into nothing. The meta keeps the deploy gate clean, so the only
+// warning a test sees is the one it is about.
const ALLOC = `
module.exports = {
+ meta: { name: 'Alloc probe', description: 'Allocates a large typed array' },
alloc: function (xchain) {
var buf = new Uint8Array(100000);
xchain.state.set('n', String(buf.length));
@@ -201,7 +203,8 @@ const PRE_GATE_TIME = 1700000000; // the old default: 2023-11-14, below every
// fixed verdict, so it keeps its teeth on both sides of the flag day instead of
// going vacuous the day the gate elapses.
it('gives a default mainnet simulation the live chain\'s banned-rest verdict', async function () {
- const REST = 'module.exports = { run: function(xchain, ...rest){ return String(rest.length); } };';
+ const REST = 'module.exports = { meta: { name: "Rest probe", description: "Counts rest params" }, ' +
+ 'run: function(xchain, ...rest){ return String(rest.length); } };';
const now = Math.floor(Date.now() / 1000);
const restEnforcedNow = XChainVM.isRestPatternMeterActive('mainnet', now);
const sim = new ContractSimulator({ coin: 'BTC', network: 'mainnet' });
diff --git a/test/unit/lint/exec_lint_enforcement.test.js b/test/unit/lint/exec_lint_enforcement.test.js
index 0f27c2e..b890e83 100644
--- a/test/unit/lint/exec_lint_enforcement.test.js
+++ b/test/unit/lint/exec_lint_enforcement.test.js
@@ -303,6 +303,19 @@ describe('execute-time consensus source-lint enforcement @regression @tier1', fu
// Correctness survives eviction: the evicted entry recomputes to the same verdict.
assert.strictEqual(vm.getLintVerdict(CLEAN, true, true, true, true, true).valid, true);
});
+
+ it('carries one key bit per flag parameter of getLintVerdict', function () {
+ // Signature is (code, ...flags, codeHash); a flag given a default drops out of
+ // Function.length, and this count must then be updated deliberately.
+ const flagCount = newVm().getLintVerdict.length - 2;
+ const vm = newVm();
+ vm.getLintVerdict(CLEAN, ...new Array(flagCount).fill(false));
+ const bits = [...vm._lintVerdictCache.keys()][0].split(':')[1];
+ assert.match(bits, /^[01]+$/);
+ assert.strictEqual(bits.length, flagCount,
+ 'a new flag parameter must append its bit to the key, or a warm node can answer ' +
+ 'from a verdict computed under the other setting');
+ });
});
});
diff --git a/test/unit/metering/contract_language_version.test.js b/test/unit/metering/contract_language_version.test.js
index ede25c0..97f58e8 100644
--- a/test/unit/metering/contract_language_version.test.js
+++ b/test/unit/metering/contract_language_version.test.js
@@ -20,14 +20,31 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
-const { CONTRACT_ECMA_VERSION, meterCode } = require('../../../src/metering.js');
+const { CONTRACT_ECMA_VERSION, meterCode } = require('../../../src/metering');
+
+const SRC_DIR = path.join(__dirname, '../../../src');
+
+// Every .js file under src/, recursively, so a new parse site (wherever it
+// lands) is caught without the list needing to be kept in sync by hand.
+function listSourceFiles(dir) {
+ let out = [];
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) out = out.concat(listSourceFiles(full));
+ else if (entry.isFile() && entry.name.endsWith('.js')) out.push(full);
+ }
+ return out;
+}
+
+const sourcePaths = listSourceFiles(SRC_DIR);
// validateSyntax needs isolated-vm (V8 pre-check); skip those cases cleanly
// where the binding doesn't load; the preflight suite is the loud guard.
+// Only the binding probe may skip: a syntax.js that fails to load for any
+// other reason (a moved import, say) must fail here, not skip.
let HAVE_IVM = true;
-let validateSyntax = null;
-try { validateSyntax = require('../../../src/syntax.js').validateSyntax; }
-catch (e) { HAVE_IVM = false; }
+try { require('isolated-vm'); } catch (e) { HAVE_IVM = false; }
+const validateSyntax = HAVE_IVM ? require('../../../src/syntax').validateSyntax : null;
describe('Contract language version (frozen consensus pin)', function () {
@@ -39,12 +56,19 @@ describe('Contract language version (frozen consensus pin)', function () {
});
it('no parse site hardcodes an ecmaVersion outside the shared constant', function () {
- for (const file of ['metering.js', 'syntax.js']) {
- const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'src', file), 'utf8');
+ let pinnedSites = 0;
+ for (const file of sourcePaths) {
+ const src = fs.readFileSync(file, 'utf8');
const hardcoded = src.match(/ecmaVersion:\s*\d+/g) || [];
assert.deepStrictEqual(hardcoded, [],
file + ' has a hardcoded ecmaVersion; use CONTRACT_ECMA_VERSION: ' + hardcoded.join(', '));
+ pinnedSites += (src.match(/ecmaVersion:\s*CONTRACT_ECMA_VERSION/g) || []).length;
}
+ // A re-export shim left behind by a move would pass the check above
+ // without scanning any parse site, so require at least one pinned site.
+ assert.ok(pinnedSites > 0,
+ 'no CONTRACT_ECMA_VERSION parse site found under src/; point this test at the ' +
+ 'file that now holds the acorn parse');
});
it('meterCode accepts ES2020 syntax (optional chaining, nullish coalescing)', function () {
diff --git a/test/unit/state/readonly_accessors.test.js b/test/unit/state/readonly_accessors.test.js
index c45660a..858ba8f 100644
--- a/test/unit/state/readonly_accessors.test.js
+++ b/test/unit/state/readonly_accessors.test.js
@@ -20,6 +20,7 @@ const {
buildContractStakeAccessor,
buildCrossChainAccessor,
buildAttestationAccessor,
+ buildPollAccessor,
resolveAccessors,
} = require('../../../src/readonly_accessors.js');
@@ -202,4 +203,23 @@ describe('Read-only accessors', function () {
assert.strictEqual(r.contractStakeData.getTotalStaked('T'), '5');
});
});
+
+ describe('buildPollAccessor', function () {
+ it('returns null for a null snapshot and for an unknown poll', function () {
+ assert.strictEqual(buildPollAccessor(null), null);
+ assert.strictEqual(buildPollAccessor({ polls: {} }).getPollResult(7), null);
+ });
+ it('returns the host entry verbatim, keeping the flag-gated tick and unknown fields', function () {
+ const entry = {
+ status: 'finalized', winning_option: 0, total_weight: '4000',
+ total_voters: 12, decided_early: false,
+ options: [{ index: 0, weight: '4000', voters: 12 }],
+ tick: 'GOVTOK', futureField: 'kept'
+ };
+ const expected = JSON.parse(JSON.stringify(entry));
+ const got = buildPollAccessor({ polls: { '7': entry } }).getPollResult(7);
+ assert.deepStrictEqual(got, expected);
+ assert.strictEqual(got.tick, 'GOVTOK');
+ });
+ });
});