diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 123f00f6..4b034a20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ on: # so the merge commit a release tag points at has its own green run. branches: [master, develop] pull_request: - branches: [master] + branches: [master, develop] # Protected branches (master, develop) are exempt from cancellation, and that # needs both lines rather than the second alone. `cancel-in-progress: false` @@ -107,7 +107,14 @@ jobs: # ones that are absent, and the env-var coverage suite applies its fleet # floor only when every sibling is present, so a partial set is judged on # what is here rather than accused of a broken scanner. + # Skipped on a fork pull request. This checkout is the one step in the + # job that reaches for cross-repo access rather than reading only the + # repo the workflow itself runs in, so it is the one a fork's more + # limited token could fail on. Skipping it there still runs the rest of + # the suite with siblings absent, per the near-hermeticity note above, + # rather than failing the whole job over this one step. - name: Check out xchain-indexer (flag-day registry read by the literals suite) + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository uses: actions/checkout@v4 with: repository: XChain-Platform/xchain-indexer diff --git a/CHANGELOG.md b/CHANGELOG.md index 56be39f8..6614a3b7 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 + +### Changed +- Published re-slid BTC and DOGE testnet activation maps with LTC inert and corrected protocol behavior across the reference documentation. + + ## [0.20.0] - 2026-09-17 ### Added diff --git a/README.md b/README.md index 02bfb11b..0f049bb2 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ A blockchain-agnostic token protocol currently running on Bitcoin, Litecoin, and | [**xchain-utxo-tracker**](https://github.com/XChain-Platform/xchain-utxo-tracker/) | Real-time UTXO indexer powering balance queries and transaction construction | | [**xchain-vm**](https://github.com/XChain-Platform/xchain-vm/) | Sandboxed JavaScript virtual machine for on-chain smart contracts with gas metering, deterministic execution, and reorg-safe state | | [**xchain-contracts**](https://github.com/XChain-Platform/xchain-contracts/) | MIT-licensed template library: audited example smart contracts, reusable patterns, a no-code policy generator, and a CLI, deployed via the ordinary `DEPLOY` action | -| [**xchain-sdk**](https://github.com/XChain-Platform/xchain-sdk/) | Developer SDK: builders for all 31 developer-invocable actions, 100+ explorer query methods, smart contract support, live WebSocket events, batch builder, PSBT generation | +| [**xchain-sdk**](https://github.com/XChain-Platform/xchain-sdk/) | Developer SDK: builders for all 32 developer-invocable actions, 100+ explorer query methods, smart contract support, live WebSocket events, batch builder, PSBT generation | | [**xchain-wallet**](https://github.com/XChain-Platform/xchain-wallet/) | Reference self-custodial multi-chain wallet: browser, Chrome extension, Electron desktop, and Capacitor mobile (Android shipped, iOS later) from a single codebase; software + Trezor + Ledger + remote + multisig signers; full DEX, messaging, contracts, staking, and `window.xchain` dApp bridge | | [**xchain-regtest-miner**](https://github.com/XChain-Platform/xchain-regtest-miner/) | Auto-mines blocks for regtest development environments | | [**xchain-e2e-test**](https://github.com/XChain-Platform/xchain-e2e-test/) | Full-stack Mocha test suite running against a live regtest deployment | @@ -50,7 +50,7 @@ This project is licensed under the **GNU Affero General Public License v3.0 (AGP | [**LICENSE**](./LICENSE.md) | Full license text | | [**NOTICE**](./NOTICE.md) | Required attribution, license summary, and third-party notices | -Any redistribution or modification must include the attribution notice specified in [NOTICE.md](./NOTICE.md). You may run and modify XChain for free under the AGPL-3.0, including inside a for-profit company, provided you share any modifications under the AGPL; a commercial license from Dankest, LLC is required only to keep modifications private or to embed XChain in a closed-source product (see [legal/licensing.md](./legal/licensing.md) for details). +Any redistribution or modification must include the attribution notice specified in [NOTICE.md](./NOTICE.md). You may run and modify XChain for free under the AGPL-3.0, including inside a for-profit company, provided you share any modifications under the AGPL; a commercial license from Dankest, LLC is required only to keep modifications private or to embed XChain in a closed-source product; the MIT-licensed `xchain-contracts` templates need neither (see [legal/licensing.md](./legal/licensing.md) for details). --- diff --git a/SECURITY.md b/SECURITY.md index e506ff35..4beb66cc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,7 +25,7 @@ Email **security@dankest.llc** with: - A concrete scenario: which implementer behavior the spec permits or encourages, and why that behavior is insecure. - Any proposed corrected text you'd like considered. -For sensitive reports, encrypt the email body to our PGP key. Its fingerprint is published at , which is currently the only place a public reader can read it, and the key itself is served beside it. If you would rather make first contact before sending anything sensitive, the email channel is fine for that and we will coordinate an encrypted exchange before you share details. +For sensitive reports, the GitHub advisory channel above is the confidential path we recommend: it is private until we publish it and needs no key exchange. If you would rather use email, send a first message without the sensitive details and we will coordinate an encrypted exchange before you share them. We do not publish a PGP key for this address. The keys published at and in [operations/release-signing.md](./operations/release-signing.md) are release signing keys, there so you can verify that a download is genuine; do not encrypt a report to them. We do not currently offer a paid bug bounty. We do offer public credit in release notes and the advisory itself, unless you prefer to remain anonymous. @@ -86,4 +86,4 @@ We ship security fixes against the latest revision on `master`. The current vers --- -Last reviewed: 2026-06-16. +Last reviewed: 2026-09-22. diff --git a/architecture/component-map.md b/architecture/component-map.md index e348de03..0ecada94 100644 --- a/architecture/component-map.md +++ b/architecture/component-map.md @@ -259,7 +259,7 @@ See [`../components/node/`](../components/node/) for full documentation. | | | |---|---| | **Purpose** | Auto-mines mempool transactions for regtest development environments | -| **Inputs** | Coin node JSON-RPC (mempool polling every 1 second) | +| **Inputs** | Coin node JSON-RPC (mempool polling every 100 ms) | | **Outputs** | Mined blocks via `generatetoaddress` | | **Storage** | None | | **Communication** | Outbound JSON-RPC to coin node; inbound JSON-RPC control API | @@ -314,7 +314,7 @@ Key technical details: - Non-deterministic globals (`Date`, `Math.random`, `fetch`, `eval`, etc.) are stripped before any contract code runs; `Math` is replaced by a frozen deterministic subset. - Gas is metered by AST instrumentation (acorn parse + astring regenerate) rather than wall-clock time, so cost is a deterministic function of code structure. - A per-block compilation cache (keyed by contract index plus code hash, bounded to 1,000 entries) avoids recompiling the same contract across multiple calls in a block. -- Requires Node.js 22 exactly; `isolated-vm` does not build on Node.js 24. +- Requires Node.js 22 exactly: `xchain-vm` pins the consensus runtime to Node ABI 127, so Node.js 24 fails `checkConsensusRuntime()` even though `isolated-vm` installs there from a prebuilt binding. See [`../components/vm/`](../components/vm/) for full documentation. @@ -334,7 +334,7 @@ See [`../components/vm/`](../components/vm/) for full documentation. Key technical details: -- Built on xchain-sdk; all action construction goes through the SDK's 31 developer-invocable ACTION methods. +- Built on xchain-sdk; all action construction goes through the SDK's 32 developer-invocable ACTION methods. - Supports every chain the platform runs on, today Bitcoin, Litecoin, and Dogecoin (mainnet, testnet, regtest), from the same codebase. - Deployed as a web SPA (served from a static docroot), a Chrome MV3 extension (packaged from the same source), an Electron desktop application, and a Capacitor mobile app wrapping the same web build (Android shipped, iOS later). - Private keys never leave the client; signing happens locally before broadcast. diff --git a/architecture/data-pipeline.md b/architecture/data-pipeline.md index a9b3833f..d920c97c 100644 --- a/architecture/data-pipeline.md +++ b/architecture/data-pipeline.md @@ -208,7 +208,7 @@ The cost is latency: a transaction confirmed in a block will not appear in the e In a local development environment, the full pipeline runs identically but with two additions: -- **xchain-regtest-miner** polls the coin node's mempool every 1 second. When it detects pending transactions, it waits up to 30 seconds (resetting to 5 seconds on each new arrival) and then calls `generatetoaddress` to mine a block. This means developers do not have to manually mine blocks. +- **xchain-regtest-miner** polls the coin node's mempool every 100 ms. When it detects pending transactions, it waits up to 30 seconds (resetting to 5 seconds on each new arrival) and then calls `generatetoaddress` to mine a block. This means developers do not have to manually mine blocks. - **xchain-e2e-test** drives the entire stack using a Mocha test suite. Tests construct actions via BIP39/BIP32 wallets, broadcast them, wait for the pipeline to process them, and assert the resulting explorer state. Tests run in order and share state across the suite; each test builds on the blockchain and indexer state left by the previous one. diff --git a/architecture/platform-map-app.html b/architecture/platform-map-app.html index 927dbe49..7cfe93a3 100644 --- a/architecture/platform-map-app.html +++ b/architecture/platform-map-app.html @@ -245,7 +245,7 @@

Flows

{ "id": "indexer", "label": "xchain-indexer", "type": "service", "group": "index", "tech": "Node.js, MariaDB", "description": "Reads the decoder DB and applies ACTION semantics: double-entry ledger, XCHAIN gas fees, orders/swaps/dispensers, staking capabilities, attestation validation, system-injected expiries. Executes contracts through xchain-vm and follows decoder reorgs." }, { "id": "indexerdb", "label": "Indexer DB", "type": "database", "group": "index", "tech": "MariaDB", "description": "Canonical protocol state: balances, tokens, orders, contracts, contract_state (append-only), staking, attestation and SPV tables." }, { "id": "vm", "label": "xchain-vm", "type": "library", "group": "index", "tech": "Node.js, isolated-vm", "description": "Deterministic smart-contract engine: sandboxed V8 isolates, host-side gas metering, 30s CPU / 8MB memory / call depth 4 limits, hardened sandbox (constructor neutering, RegExp removal). Emits actions (XCALL, ATTEST) back to the indexer." }, - { "id": "hub", "label": "xchain-hub", "type": "service", "group": "hub", "tech": "Node.js, MariaDB", "description": "Config oracle and cross-chain coordinator. PBFT federation (stake-weighted source-deduped quorum at/above STAKE_WEIGHTED_QUORUM_ACTIVATION, majority-floored count max(2f+1, ceil((N+1)/2)) below it), five stake-qualified capabilities, price-oracle rounds, attestation engine (http_get / llm providers), reorg-retraction co-signing, governance/slash, and StateAnchorPublisher (ANCHOR v7 bundle on DOGE)." }, + { "id": "hub", "label": "xchain-hub", "type": "service", "group": "hub", "tech": "Node.js, MariaDB", "description": "Config oracle and cross-chain coordinator. PBFT federation (stake-weighted source-deduped quorum at/above STAKE_WEIGHTED_QUORUM_ACTIVATION, majority-floored count max(2f+1, ceil((N+1)/2)) below it), five stake-qualified capabilities, price-oracle rounds, attestation engine (http_get / llm providers), reorg-retraction co-signing, governance/slash, and StateAnchorPublisher (ANCHOR v0 bundle on DOGE)." }, { "id": "hubdb", "label": "Hub DB", "type": "database", "group": "hub", "tech": "MariaDB", "description": "About 20 tables: configs, validators, consensus state, cross-chain calls, price_snapshots, oracle_prices, attestation stats." }, { "id": "explorer", "label": "xchain-explorer", "type": "service", "group": "serve", "tech": "Node.js, Express, WS", "description": "Read-only REST + JSON-RPC + WebSocket API and web UI over indexer state (60+ endpoints, /{COIN} prefixed). Runs a hub-mirror sync for consensus tables, ABI introspection, and an optional sandboxed contract-simulation endpoint." }, { "id": "sync", "label": "xchain-sync", "type": "service", "group": "serve", "tech": "Node.js, WS", "description": "Replicates indexer + decoder DBs to validators: REST snapshots plus a WebSocket block feed, transactional apply with rollback, merkle transparency log (sync_meta, merkle_epochs) and pinned-validator checkpoint quorum verification." }, @@ -399,10 +399,10 @@

Flows

{ "id": "f8", "name": "State anchoring (ANCHOR on DOGE)", - "description": "Quorum-signed per-chain state checkpoints are anchored on Dogecoin as one ANCHOR v7 bundle per network, for chain-parse recoverability.", + "description": "Quorum-signed per-chain state checkpoints are anchored on Dogecoin as one ANCHOR v0 bundle per network, for chain-parse recoverability.", "steps": [ { "n": 1, "node": "hub", "edge": "r13", "action": "StateAnchorPublisher runs one publisher election per bundle by hash ordering and assembles every checkpointed chain's quorum-signed checkpoint as its own bundle section, plus the compressed cross-chain match archive." }, - { "n": 2, "node": "hub", "edge": "r14", "action": "ONE ANCHOR v7 checkpoint bundle per network per publishing cycle is built and submitted through the DOGE encoder from the elected publisher's own DOGE wallet." }, + { "n": 2, "node": "hub", "edge": "r14", "action": "ONE ANCHOR v0 checkpoint bundle per network per publishing cycle is built and submitted through the DOGE encoder from the elected publisher's own DOGE wallet." }, { "n": 3, "node": "encoder", "edge": "r4", "action": "Anchor transaction is broadcast on Dogecoin." }, { "n": 4, "node": "decoder", "edge": "r6", "action": "DOGE decoder picks the ANCHOR out of the block." }, { "n": 5, "node": "indexer", "edge": "r8", "action": "Indexers verify and record the checkpoint; RewardTracker records publisher rewards." } diff --git a/architecture/platform-map.json b/architecture/platform-map.json index 5ea8c2d4..caa1eaa0 100644 --- a/architecture/platform-map.json +++ b/architecture/platform-map.json @@ -98,7 +98,7 @@ "type": "service", "group": "hub", "tech": "Node.js, MariaDB", - "description": "Config oracle and cross-chain coordinator. PBFT federation (stake-weighted source-deduped quorum at/above STAKE_WEIGHTED_QUORUM_ACTIVATION, majority-floored count max(2f+1, ceil((N+1)/2)) below it), five stake-qualified capabilities, price-oracle rounds, attestation engine (http_get / llm providers), reorg-retraction co-signing, governance/slash, and StateAnchorPublisher (ANCHOR v7 bundle on DOGE)." + "description": "Config oracle and cross-chain coordinator. PBFT federation (stake-weighted source-deduped quorum at/above STAKE_WEIGHTED_QUORUM_ACTIVATION, majority-floored count max(2f+1, ceil((N+1)/2)) below it), five stake-qualified capabilities, price-oracle rounds, attestation engine (http_get / llm providers), reorg-retraction co-signing, governance/slash, and StateAnchorPublisher (ANCHOR v0 bundle on DOGE)." }, { "id": "hubdb", @@ -794,7 +794,7 @@ { "id": "f8", "name": "State anchoring (ANCHOR on DOGE)", - "description": "Quorum-signed per-chain state checkpoints are anchored on Dogecoin as one ANCHOR v7 bundle per network, for chain-parse recoverability.", + "description": "Quorum-signed per-chain state checkpoints are anchored on Dogecoin as one ANCHOR v0 bundle per network, for chain-parse recoverability.", "steps": [ { "n": 1, @@ -806,7 +806,7 @@ "n": 2, "node": "hub", "edge": "r14", - "action": "ONE ANCHOR v7 checkpoint bundle per network per publishing cycle is built and submitted through the DOGE encoder from the elected publisher's own DOGE wallet." + "action": "ONE ANCHOR v0 checkpoint bundle per network per publishing cycle is built and submitted through the DOGE encoder from the elected publisher's own DOGE wallet." }, { "n": 3, diff --git a/bin/complete_run_reporter.js b/bin/complete_run_reporter.js index 8e25ec93..a7182254 100644 --- a/bin/complete_run_reporter.js +++ b/bin/complete_run_reporter.js @@ -1,9 +1,21 @@ +/********************************************************************* + * + * Copyright © 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. + * + ********************************************************************** + * + * Refuses a node --test run in which a file's child exited before its event + * stream was whole: the runner trusts exit codes alone, so a lost stream tail + * otherwise grades green with the tests in it never counted. Node 22.10+. + */ 'use strict'; -// Refuses a node --test run in which a file's child exited before its event -// stream was whole: the runner trusts exit codes alone, so a lost stream tail -// otherwise grades green with the tests in it never counted. Node 22.10+. - const fs = require('node:fs'); const path = require('node:path'); diff --git a/bin/generate-flag-days.js b/bin/generate-flag-days.js index 1284ec01..2fc8997e 100644 --- a/bin/generate-flag-days.js +++ b/bin/generate-flag-days.js @@ -11,7 +11,8 @@ * ********************************************************************** * - * Generates protocol/flag-days.md from the indexer's activation registry. + * Generates protocol/flag-days.md from the indexer's activation registry and + * the documentation canon's published activation maps. * * WHY. Five doc pages and the whitepaper quoted the coordinated * contract-era flag-day as a literal DATE. A flag-day date is not a fact about @@ -67,6 +68,7 @@ const DOC_ROOT = path.resolve(__dirname, '..'); const INDEXER_SRC = path.resolve(DOC_ROOT, '../xchain-indexer/src'); const REGISTRY = path.join(INDEXER_SRC, 'protocol_changes.js'); const OUTPUT = path.join(DOC_ROOT, 'protocol', 'flag-days.md'); +const CANONICAL_CONSTANTS = path.join(DOC_ROOT, 'protocol', 'constants.js'); // The registry's own files as ONE text, comments blanked, with the map back to // the part file and line an offset came from. Every registry pass below reads @@ -673,6 +675,15 @@ function collectMainnetUnarmed(indexerSrc = INDEXER_SRC) { return [...found.values()].sort((a, b) => a.gate.localeCompare(b.gate)); } +/** Every activation map the documentation canon publishes, sorted for stable output. */ +function collectCanonicalActivationMaps(constantsPath = CANONICAL_CONSTANTS) { + const resolved = require.resolve(constantsPath); + delete require.cache[resolved]; + return Object.keys(require(resolved)) + .filter((name) => name.endsWith('_ACTIVATION')) + .sort(); +} + /** * The coordinated contract-era flag day: the timestamp the most gates ride. * Derived rather than named, because naming it here would reintroduce exactly @@ -695,7 +706,7 @@ function coordinatedFlagDay(gates) { return { time: ranked[0][0], count: ranked[0][1] }; } -function render(gates, testnetArms = [], testnetUnarmed = [], mainnetUnarmed = []) { +function render(gates, testnetArms = [], testnetUnarmed = [], mainnetUnarmed = [], canonicalActivationMaps = []) { const anchor = coordinatedFlagDay(gates); const others = gates.filter((g) => g.time !== anchor.time); @@ -703,6 +714,7 @@ function render(gates, testnetArms = [], testnetUnarmed = [], mainnetUnarmed = [ const note = g.time === anchor.time ? 'contract-era flag day' : 'own date'; return `| \`${g.gate}\` | \`${g.time}\` | ${utcInstant(g.time)} | ${note} | \`${g.source}\` |`; }); + const canonicalRows = canonicalActivationMaps.map((name) => `- \`${name}\``); const outliers = others.length === 0 ? 'Every mainnet time-keyed gate rides the coordinated instant; none carries a date of its own.' @@ -769,8 +781,9 @@ function render(gates, testnetArms = [], testnetUnarmed = [], mainnetUnarmed = [ # Flag-Day Values **This page is generated** from \`xchain-indexer/src/protocol_changes.js\`, its part files -under \`src/protocol_changes/\`, and the time-keyed activation modules beside them. Do not -edit it by hand: run \`node bin/generate-flag-days.js\` from the repository root and commit the result. +under \`src/protocol_changes/\`, the time-keyed activation modules beside them, and +\`protocol/constants.js\`. Do not edit it by hand: run \`node bin/generate-flag-days.js\` +from the repository root and commit the result. Every other page in this documentation set names the **gate** and links here instead of quoting a date, because a flag-day value is not a fact about the @@ -781,6 +794,14 @@ For what a flag day is, how \`isEnabled\` evaluates it, which cohort a gate belongs to, and what happens to a node that misses one, see [Protocol Activation](./protocol-activation.md). +## Canonical activation maps + +These names are exported by [\`protocol/constants.js\`](./constants.js). The index includes +scheduled, inert, genesis-active, time-keyed, and height-keyed maps so a gate remains +discoverable here even when it has no mainnet date for the table below. + +${canonicalRows.join('\n')} + ## Contract-era flag day The coordinated instant that the **Cohort A** contract-era rules switch on, @@ -813,7 +834,8 @@ here; they are inventoried on function generate(indexerSrc = INDEXER_SRC) { return render(collectGates(indexerSrc), collectTestnetArms(indexerSrc), - collectTestnetUnarmed(indexerSrc), collectMainnetUnarmed(indexerSrc)); + collectTestnetUnarmed(indexerSrc), collectMainnetUnarmed(indexerSrc), + collectCanonicalActivationMaps()); } if (require.main === module) { @@ -833,6 +855,7 @@ if (require.main === module) { } module.exports = { - collectGates, collectTestnetArms, collectTestnetUnarmed, collectMainnetUnarmed, coordinatedFlagDay, render, generate, utcInstant, utcDate, - DOC_ROOT, INDEXER_SRC, REGISTRY, OUTPUT, TIMESTAMP_FLOOR, SENTINEL_FLOOR, + collectGates, collectTestnetArms, collectTestnetUnarmed, collectMainnetUnarmed, + collectCanonicalActivationMaps, coordinatedFlagDay, render, generate, utcInstant, utcDate, + DOC_ROOT, INDEXER_SRC, REGISTRY, OUTPUT, CANONICAL_CONSTANTS, TIMESTAMP_FLOOR, SENTINEL_FLOOR, }; diff --git a/blockchains.md b/blockchains.md index 7517f380..9c2ede4c 100644 --- a/blockchains.md +++ b/blockchains.md @@ -21,6 +21,8 @@ Each chain supports three network types: | **Testnet** | Public test network: free test coins, mirrors mainnet behavior | | **Regtest** | Local regression testing network: instant block generation, fully controlled environment | +> **BTC testnet4 contract limitation:** Contract deploys on BTC testnet4 cannot be mined at current miner block sizes. Use LTC or DOGE testnet for contract deployment and testing. + ## Chain-Specific Differences The same protocol specification applies across all chains. Chain-specific differences are limited to: diff --git a/components/decoder/README.md b/components/decoder/README.md index 50d77fcd..2d796116 100644 --- a/components/decoder/README.md +++ b/components/decoder/README.md @@ -20,7 +20,7 @@ The decoder's job is extraction only; it does not interpret action semantics. It - **DISPENSER protocol**: parses DISPENSER actions and tracks active dispensers with expiration for real-time payment detection - **Mempool tracking**: maintains an index of unconfirmed transactions, updated every 60 seconds when synced - **Normalized storage**: addresses and transaction hashes stored in index tables with integer IDs for join efficiency -- **ACTION name validation**: 36-name whitelist (SEND, ISSUE, MINT, ORDER, ANCHOR, NODEPROOF, ROLLCALL, SLASH, VOTE, BET, etc.) enforced before database writes +- **ACTION name validation**: 37-name whitelist (SEND, ISSUE, MINT, ORDER, ANCHOR, NODEPROOF, ROLLCALL, SLASH, VOTE, BET, XBRIDGE, etc.) enforced before database writes - **Graceful shutdown**: SIGTERM/SIGINT handlers complete in-flight work before exiting - **1,333 tests** (measured 2026-07-27): unit, integration, e2e, boundary, security, fuzz, chaos, regression, benchmarks, and mutation testing diff --git a/components/decoder/configuration.md b/components/decoder/configuration.md index 2e9b5f80..8351d930 100644 --- a/components/decoder/configuration.md +++ b/components/decoder/configuration.md @@ -32,14 +32,14 @@ Configuration is loaded from a `.env` file via `dotenv`. All variables are read | `GETMEMPOOL_CACHE_MS` | How long the `getmempool` JSON-RPC response is cached, in milliseconds. That method reads the database, so without a cache a burst of unauthenticated requests contends with the block loop; the window is read once and sliced per request, and the underlying snapshot only changes every 60 seconds. | `5000` | | `FEE_DESTINATION` | Native-coin protocol fee destination override for this coin+network. The decoder persists outputs paying the resolved address to `transaction_outputs` so the indexer can validate native-coin fee payments. By default the address comes from the bundled coin registry (`src/coins`, pinned per coin/network), so capture is on for a stock install. This variable overrides the default on testnet/regtest only; on mainnet it is ignored with a warning, because fee acceptance is consensus and must not depend on operator environment. | _(coin registry)_ | | `DB_QUERY_TIMEOUT` | MariaDB query timeout in milliseconds (passed to the connection pool `queryTimeout` option) | `30000` | -| `SHUTDOWN_TIMEOUT_MS` | Hard-exit budget for the SIGTERM drain, in milliseconds. On `docker stop` the decoder marks itself not-running, lets the parse loop break at its next block boundary, closes the API listener and both database pools, and exits 0; if that has not finished within the budget it exits 1 instead of lingering until docker's SIGKILL. Sized under the 120 s stop budget `xchain-node` gives a decoder. | `100000` | +| `SHUTDOWN_TIMEOUT_MS` | Hard-exit budget for the SIGTERM drain, in milliseconds. On `docker stop` the decoder marks itself not-running, lets the parse loop break at its next block boundary, closes the API listener and both database pools, and exits 0; if that has not finished within the budget it exits 1 instead of lingering until docker's SIGKILL. `xchain-node` sets it when it creates the container, to the decoder's stop budget less 20 s (`100000` from the default 120 s), unless the module config sets it; see `XCHAIN_NODE_MODULE_STOP_TIMEOUT_SECONDS_` in the [node configuration](../node/configuration.md). | `100000` | | `NODE_RPC_TIMEOUT` | HTTP timeout in milliseconds for all JSON-RPC calls to the coin node (sets `axios.defaults.timeout` at startup) | `30000` | | `NODE_URL_FALLBACK` | Comma-separated list of additional coin-node endpoints. The connector rotates round-robin to the next endpoint after `NODE_FAILOVER_THRESHOLD` consecutive connection-level failures, so a recovered primary is retried again if the fallback also dies. Each fallback reuses `NODE_PORT`. | _(unset, single endpoint)_ | | `NODE_FAILOVER_THRESHOLD` | Consecutive connection-level failures before rotating to the next endpoint in `NODE_URL_FALLBACK`. Floored at 1. | `3` | | `RPC_TIMEOUT_RETRY_DELAY_MS` | Backoff in milliseconds between timeout (`ECONNABORTED`) retries in the block-path RPC methods. Each attempt has already burned the full RPC timeout before aborting, so an instant re-fire stacks retries onto a node that is timing out because it is overloaded. Set to `0` to disable (tests do). | `500` | | `DECODER_RPC_CONCURRENCY` | Maximum concurrent outbound JSON-RPC calls to the coin node. Floored at 1. | `50` | | `DECODER_RPC_MAX_BATCH` | Maximum number of calls permitted in one inbound JSON-RPC batch. The router runs `Promise.all` over a batch while the rate limiter counts the batch as a single request, so this bound is what stops one array from fanning out into thousands of concurrent handlers. | `20` | -| `MIGRATION_STRICT_CHECKSUM` | Set to `1` to make a schema-checksum mismatch fail closed at startup instead of logging and continuing. Off by default so a diverged schema does not cause a surprise fleet-wide boot failure; CI and operators running `node src/migrate.js` get the strict path anyway. | _(unset, non-fatal)_ | +| `MIGRATION_STRICT_CHECKSUM` | Set to `1` to make a schema-checksum mismatch fail closed at startup instead of logging and continuing. Off by default so a diverged schema does not cause a surprise fleet-wide boot failure; CI and operators running `node src/db/migrate.js` get the strict path anyway. | _(unset, non-fatal)_ | | `COIN` | Cosmetic label only, reported in the `/status` response. The decoder takes its chain identity from the node it is pointed at, so it has no coin setting of its own; the label stays empty unless a deploy sets one. | _(unset, empty label)_ | | `DECODER_POLL_SILENT_MS` | How long the block loop may go without completing a single iteration before `/live` reports unhealthy (503) and the container restart policy recycles the process. Measures the loop, not the chain: the stall window `DECODER_STALL_ALERT_MS` (default `900000` ms, documented under Operations) asks whether the chain is advancing, and a caught-up decoder advances nothing for hours while being perfectly healthy, so only an iteration count separates "idle" from "the loop is gone". Defaults to twice the stall window, because every normal path through the loop, the node-outage retry included, returns to the loop top far inside it. | `1800000` (30 minutes) | | `XCHAIN_INDEXER_DIR` | Path override for the sibling `xchain-indexer` checkout that `bin/sync-batch-limits.js` reads to regenerate the vendored BATCH limit tables (`src/protocol/indexer_batch_limits.js`) from the indexer's own `src/actions/batch.js` and `src/protocol_changes.js`. A maintenance-tool setting only; the decoder service itself never reads it. | `../../xchain-indexer` relative to `bin/` (the sibling checkout layout) | @@ -143,7 +143,7 @@ The decoder begins parsing from a preconfigured block height per network to skip ## Valid ACTION Names -The decoder accepts only these 36 ACTION names after deobfuscation. Transactions with unrecognized action names are logged and skipped: +The decoder accepts only these 37 ACTION names after deobfuscation. Transactions with unrecognized action names are logged and skipped: ``` ADDRESS, AIRDROP, ANCHOR, ATTEST, @@ -151,9 +151,11 @@ BATCH, BET, BROADCAST, CALLBACK, COINPAY, COLLECT, DELEGATE, DEPLOY, DEPOSIT, DESTROY, DISPENSER, DIVIDEND, EXECUTE, FILE, ISSUE, LINK, LIST, MESSAGE, MINT, NODEPROOF, ORDER, PRICE, ROLLCALL, SEND, SLASH, SLEEP, STAKE, SWAP, -SWEEP, UNSTAKE, VOTE, WITHDRAW +SWEEP, UNSTAKE, VOTE, WITHDRAW, XBRIDGE ``` +XBRIDGE arrives on the wire only in its user-broadcast versions (0, 1, 3, 4). Its settle versions (2, 5) are mirror-injected by the indexer and refused when broadcast, so they share the one name. XCALL is never wire-decoded and is not on this list. + --- **Copyright © 2025–2026 Dankest, LLC** diff --git a/components/decoder/database.md b/components/decoder/database.md index d476afa8..dd182423 100644 --- a/components/decoder/database.md +++ b/components/decoder/database.md @@ -177,7 +177,7 @@ The decoder applies schema changes via a tracked migration system. Two paths exi - **Manual (operator):** migrations tagged `mode=manual` (destructive column-type changes, data backfills, dedup-then-unique) run only when an operator explicitly invokes: ```bash - node src/migrate.js + node src/db/migrate.js # or: npm run migrate ``` diff --git a/components/decoder/operations.md b/components/decoder/operations.md index 1b013d31..f9b559c8 100644 --- a/components/decoder/operations.md +++ b/components/decoder/operations.md @@ -198,7 +198,7 @@ The decoder ships with a migration system that tracks and applies schema changes To apply pending manual migrations: ```bash -node src/migrate.js +node src/db/migrate.js # or: npm run migrate ``` @@ -295,7 +295,9 @@ Two recoveries: npm run clear-reorg-halt -- --reason "" ``` - The clear checks that every rolled-back block above the tip has been re-parsed (cannot be forced; wait for the decoder to catch up) and that the database holds no dispenser rows and never decoded a `DISPENSER` action (so the purge could not have lost anything). A database that has held dispensers is refused unless you pass `--force` after comparing its `dispensers` table against a known-good replica; the clear is then recorded as forced. `--dry-run` reports the verdict without writing and needs no `--reason`, so run it first to see what a clear would do. + The clear checks that every rolled-back block above the tip has been re-parsed (cannot be forced) and that the database holds no dispenser rows and never decoded a `DISPENSER` action (so the purge could not have lost anything). A database that has held dispensers is refused unless you pass `--force` after comparing its `dispensers` table against a known-good replica; the clear is then recorded as forced. `--dry-run` reports the verdict without writing and needs no `--reason`, so run it first to see what a clear would do. + + When the re-parse check refuses, `reorg_halt_parked` decides what to do next. A decoder still parsing forward on a dormant marker (`reorg_halt_parked: false`) re-parses the range on its own: wait for it to pass the halt height and run the clear again. A parked decoder (`reorg_halt_parked: true`) parses nothing and never re-parses the range, so the clear cannot pass and recovery 1, a full resync, is the path. The clear writes a `REORG_HALT_CLEARED` event carrying the reason, the check results and the halt it supersedes. The halt row stays for the audit trail, `health` reports `reorg_halted: false` with `reorg_halt_cleared_at` set on its next probe, and the bootstrap health gate accepts the database again. diff --git a/components/encoder/README.md b/components/encoder/README.md index 40fae982..aec52ee0 100644 --- a/components/encoder/README.md +++ b/components/encoder/README.md @@ -161,6 +161,7 @@ npm run api | `ENCODER_MAINTENANCE_FILE` | No | `/tmp/xchain-encoder-maintenance.json` | Path, inside the encoder container, to the maintenance-window sentinel that `GET /status` reads before reporting an unreachable UTXO tracker. When xchain-node's bootstrap stops the tracker for a scheduled publish, it drops a small JSON file here declaring the outage planned; `/status` then folds that in as context alongside the unchanged readiness fields, so the public status board can show "Maintenance" instead of "Degraded" without ever making an unready encoder read ready. Must be set to the same path as xchain-node's `XCHAIN_NODE_ENCODER_MAINTENANCE_FILE`, since that variable is what writes and removes the file this one points at | | `ENCODER_REPLICAS` | No | `1` (unset) | Deploy-manifest declaration of the horizontal replica count, checked at boot. Any value above `1` is refused: the UTXO outpoint-reservation double-spend guard, the recent-build duplicate refusal and the rate limiter are all in-process, so two replicas could build PSBTs spending the same UTXO or journal one byte-identical transaction as two successes. Unset or empty passes as the default single-replica deploy | | `ENCODER_INSTANCE_LOCK_FILE` | No | `/xchain-encoder-.lock` | Path to the same-host PID lockfile the encoder takes exclusively at boot, so two encoder processes accidentally started on one host fail fast instead of racing UTXO selections. Does not see replicas on other hosts or containers; `ENCODER_REPLICAS` is the cross-host declaration | +| `NODE_OPTIONS` | No | None | Node flags carried into the mocha processes the suite-title pin tooling starts when it records or compares this repo's test titles. Whatever an operator has set is kept and the pin run's own sibling-resolver require is added to it, so an inspector, heap-size or loader flag already in force still applies to the child runs. It configures that tooling only; the encoder API reads none of it | ## Testing diff --git a/components/explorer/api.md b/components/explorer/api.md index 959d0a27..49d18bb3 100644 --- a/components/explorer/api.md +++ b/components/explorer/api.md @@ -1811,6 +1811,8 @@ GET /BTC/api/proof/validator-set?height={snapshotBlock}[&capabilities=oracle_pub | 400 | `STAKES_BTC_ONLY` | Must call on a BTC coin prefix | | 409 | `SNAPSHOT_NOT_YET_CHECKPOINTED` | No BTC checkpoint at this height yet | | 409 | `CHECKPOINT_PRE_COMMITMENT` | Checkpoint predates state-commitment activation | +| 409 | `STAKE_SNAPSHOT_TRUNCATED` | The indexer truncated a capability's stake snapshot at its query cap; no proof is served until operators raise the cap | +| 500 | `STAKE_SNAPSHOT_MALFORMED` | A capability's stake snapshot cannot yield a stake total (blank or missing source, or a bad weight) | | 501 | `NO_STATE_TREE` | Server does not hold the state tree | | 501 | `INDEXER_NOT_CONFIGURED` | No indexer API URL configured for this coin/network | | 502 | `INDEXER_UNAVAILABLE` | Indexer API did not respond | diff --git a/components/explorer/configuration.md b/components/explorer/configuration.md index 27de20ff..c2b4a722 100644 --- a/components/explorer/configuration.md +++ b/components/explorer/configuration.md @@ -243,7 +243,7 @@ also shape startup output. ## Local Configuration File -The `src/config.json` file provides database connection details when xchain-hub is not available. Structure: +The `src/config.json` file provides database connection details when xchain-hub is not available. It is operator-created runtime state and is intentionally gitignored because it contains database credentials, so a source-only checkout will not include it. Copy the tracked `src/config.json.example` template to `src/config.json`, keep the populated file out of Git, and use this structure: ```json { @@ -268,7 +268,7 @@ The `src/config.json` file provides database connection details when xchain-hub Each coin/network entry specifies both the Indexer database (primary data source) and the Decoder database (for raw transaction lookups). -An example template is provided at `src/config.json.example`. +The tracked template remains at `src/config.json.example`; only the populated `src/config.json` is expected to be absent from Git. ## Checkpoint Schema (Hub-Mirror Tables) diff --git a/components/explorer/operations.md b/components/explorer/operations.md index 2d9201c0..167402a7 100644 --- a/components/explorer/operations.md +++ b/components/explorer/operations.md @@ -6,7 +6,7 @@ ## Prerequisites -- **Node.js** 22 (22.x LTS), pinned in `.nvmrc`. Node 24 cannot build the native `isolated-vm` module that this component's vendored `xchain-vm` dependency pulls in. +- **Node.js** 22 (22.x LTS), pinned in `.nvmrc`. Node 24 is outside the consensus runtime this component's vendored `xchain-vm` pins (Node ABI 127, which `checkConsensusRuntime()` enforces); `isolated-vm` itself installs there from a prebuilt binding. - **MariaDB** server with an existing Indexer database (populated by xchain-indexer) - **xchain-hub** (optional), for centralized config discovery - **SSL certificates** (optional), for HTTPS diff --git a/components/hub/configuration.md b/components/hub/configuration.md index 1ed8453a..eb0aa8ad 100644 --- a/components/hub/configuration.md +++ b/components/hub/configuration.md @@ -98,6 +98,7 @@ These variables are required regardless of operating mode. | `HUB_DB_SECRET` | Yes | None | MariaDB password. Deprecated name `HUB_DB_PASS` is still read; see Secret variable naming above. | | `HUB_DB_KEEPALIVE_INTERVAL` | No | `30000` | Interval (ms) between no-op keepalive queries sent to the MariaDB pool to prevent idle-connection drops | | `HUB_RATE_LIMIT_RPM` | No | `100` | Requests allowed per IP per 60-second window on every route except the mirror-bootstrap family `/hub-db/snapshot/*`, which `HUB_SNAPSHOT_RATE_LIMIT_RPM` below meters in its own bucket. Over the limit the request returns HTTP 429 with a JSON-RPC error body (code `-32029`) naming the limit, the window and the seconds to wait, plus `Retry-After` and `RateLimit-*` headers. Behind a reverse proxy the limiter keys on `X-Forwarded-For`, which is what `HUB_TRUST_PROXY` below governs. | +| `HUB_AUTH_RATE_LIMIT_RPM` | No | `60000` | Requests allowed per IP per 60-second window on the JSON-RPC surface for callers whose `x-api-key` header matches a key this hub has configured, metered in their own bucket so the fleet's own indexers and replay traffic keep a high budget while keyless callers stay at `HUB_RATE_LIMIT_RPM`. A wrong key lands in the public bucket, and a keyless hub has no authenticated bucket, so every caller is public there. A request is charged to one bucket only, and a batch is charged one token per call. The mirror-bootstrap routes `/hub-db/snapshot/*` stay in the `HUB_SNAPSHOT_RATE_LIMIT_RPM` bucket, and the `HUB_RATE_LIMIT_EXEMPT_LOCAL` exemption applies here too. An unset, unparseable or non-positive value keeps the default, raised to `HUB_RATE_LIMIT_RPM` if that is higher. | | `HUB_RATE_LIMIT_EXEMPT_LOCAL` | No | `true` | Exempts callers whose resolved client IP is loopback or private-range (RFC1918, IPv6 unique-local and link-local) from the per-IP limit above. This is what lets a node's own indexer rebuild price history from the chain at the shipped default: it replays one `pushpricebatch` per batch-bearing block, far faster than 100/min, and reaches the hub over the container bridge. The check runs on the post-`trust proxy` client IP, so a public caller arriving through a private-IP reverse proxy is still limited. Set to `false` to enforce the cap on every caller. | | `HUB_SNAPSHOT_RATE_LIMIT_RPM` | No | `600` | Requests allowed per IP per 60-second window on the mirror-bootstrap routes `/hub-db/snapshot/*`, metered in their own bucket so a mirroring indexer draining from id 0 and ordinary polling cannot starve each other: on the 2026-09-16 fleet roll three indexers behind one public address spent the shared 100 req/min, 429ed part way and stayed wedged until `HUB_RATE_LIMIT_RPM` was raised by hand. Sized to the measured drain (about 32 page reads for one mirror's full bootstrap, about 96 for three mirrors behind one address). A request here is charged to this bucket only, never to `HUB_RATE_LIMIT_RPM`. Over the limit the hub answers `429` in the `{ "error": ... }` shape these REST routes already use. The `HUB_RATE_LIMIT_EXEMPT_LOCAL` exemption above applies to this bucket too. An unset, unparseable or non-positive value keeps the default. | | `HUB_MAX_RPC_BATCH` | No | `20` | Maximum call objects in one JSON-RPC batch array. The rate limiter above charges one token per HTTP request while the dispatcher runs every element of the batch, so without this cap one request amplifies past the limit. Over the cap the hub answers `400` with JSON-RPC error `-32600`. Every hub connector sends a single call object, so the cap breaks no existing client. | @@ -178,7 +179,9 @@ and **hot-reloads** on file change. It supplies two things: - Per-capability self-test config blocks, checked locally so the hub only participates when it can actually serve: - `price`: `{ "sources": [...], "fiats": [...] }` - - `cross_chain`: `{ "chains": { "BTC": { "rpc": "..." }, ... } }` + - `cross_chain`: `{ "chains": { "BTC": { "rpc": "..." }, ... } }`. The `rpc` value is the + chain's coin-node RPC, and the `full_node` self-test reads the same BTC entry; matching + itself resolves the chain's indexer through `_INDEXER_API_URL` - `oracle_publish`: `{ "doge_address": "...", "doge_wallet": "..." }` - `attestation`: `{ "providers": { "": false } }` (omit a key to enable it) - `DISABLED_CAPABILITIES`: array of capabilities to opt out of even when qualified. @@ -211,6 +214,7 @@ mounts it into the hub container automatically. See OPERATIONS.md → Validator | `XCHAIN_HUB_SKIP_ZERO_CONF_ASSERT` | No | _(unset)_ | Set to `1` to bypass the boot assertion that `ATTEST_ZERO_CONF_ACTIVATION` is at or above both `ATTEST_RESPONSE_MIRROR_ACTIVATION` and `ATTEST_RESPONSIBLE_WIDENING_ACTIVATION` on every network where it is armed. Only for a venue where **every** hub runs the same maps: below the mirror height a round would run at zero confirmations and burn a broadcast fee against a reorged request, and with widening unarmed the flip would remove the wait with no headroom behind it. On `mainnet` and `testnet` a violation otherwise refuses to start (`ZERO_CONF_ORDERING`); standalone and regtest warn instead. The bypass logs a warning every time it is taken. | | `XCHAIN_HUB_SKIP_REORG_BUFFER_ASSERT` | No | _(unset)_ | Set to `1` to bypass the assertion that `HUB_SNAPSHOT_REORG_BUFFER` equals the canonical federation value. Only for a venue where **every** hub runs the same override: each hub subtracts this buffer before resolving a snapshot, so hubs disagreeing on it lock different blocks for the same round and produce divergent validator sets and quorum N. On `mainnet` and `testnet` a mismatch otherwise refuses to start (`REORG_BUFFER_MISMATCH`); standalone and regtest warn instead. The bypass logs a warning every time it is taken. | | `XCHAIN_HUB_SKIP_MIN_STAKE_ASSERT` | No | _(unset)_ | Set to `1` to skip the minimum-stake assertion at startup. Test and bring-up seam; leaving it set on a real deployment disables a safety check. | +| `XCHAIN_HUB_ORACLE_TICK_MIRRORS_WIDENED` | No | _(unset, widen skipped)_ | Set to `1` once every `oracle_prices` mirror (indexer and explorer) has widened `tick` to `VARCHAR(250)`; the hub then widens its own column at boot. Unset, the hub logs and skips its widen so a long-tick PRICE never reaches a mirror that would refuse it. | | `ATTEST_RESPONSE_FORWARD_S_OVERRIDE` | No | _(unset)_ | Overrides `ATTEST_RESPONSE_FORWARD_S` (120), the seconds a round leader adds to now when stamping the effective time an attestation response becomes applicable at. **Honoured on `regtest` only**; on any other network a differing value is ignored with a warning latched once per process, and standalone mode (no network) counts as not-regtest and keeps the frozen value. On `regtest` a value that is not a whole number of seconds throws at resolve time rather than defaulting, because a silent fallback to 120 leaves an acceptance run waiting two minutes per attestation with nothing in the log to explain it. The seam exists because regtest blocks are stamped at roughly now, so without it no mirrored response could bind for 120 real seconds. Also readable from the validator config table under the same key, which takes precedence over the environment. | | `ATTEST_BATCH_WINDOW_S_OVERRIDE` | No | _(unset)_ | Overrides `ATTEST_BATCH_WINDOW_S` (3600), the length of the window an attestation batch closes on. Same seam and same rules as `ATTEST_RESPONSE_FORWARD_S_OVERRIDE` above, with one difference: on `regtest` the value must be a **positive** whole number of seconds, because a window of zero is not a faster cadence but a division by zero in the alignment arithmetic, and a bad spelling throws at resolve time rather than defaulting. **Honoured on `regtest` only**; elsewhere a differing value is ignored with a warning latched once per process, since the window bounds are part of the batch key and of the signed batch canonical, so a hub running its own cadence proposes batches no peer can co-sign. Also readable from the validator config table under the same key, which takes precedence over the environment. | @@ -292,9 +296,9 @@ Controls `OraclePublisher`, which broadcasts finalized price rounds on-chain as | `ORACLE_BATCH_GRACE_MS` | No | `300000` | How long after a window closes the elected leader waits before assembling it, giving late-finalizing peers time to agree on its contents. Armed once per window and never extended, so a trickle of stragglers cannot postpone a window indefinitely. | | `ORACLE_BATCH_SIGN_TIMEOUT_MS` | No | `60000` | How long the leader waits for a signing quorum on an assembled window. No quorum means no publication for that window: it stays buffered and a later leader can propose it again. One name, two rails, two defaults: here `OracleBatchSigner` reads it from the validator config table only and defaults to `60000`, while the attestation batch rail reads the same name from the environment first and defaults to `15000` (see Attestation Publishing below). Setting it in the environment therefore moves the attestation rail and leaves this one unchanged. | | `ORACLE_BATCH_BUFFER_MAX_ROUNDS` | No | `4032` | Upper bound on buffered rounds, so a hub that never leads a window cannot grow its buffer without limit. Reached only if publication has been failing for a long time; the oldest rounds are dropped first. | -| `HUB_PRICE_CAPABILITY_DERIVE` | No | `on` | Whether a hub that does NOT run oracle consensus derives `price` capability snapshots from its own Bitcoin view. On such a hub nothing else writes them, because the only other writer is the round-finalization path, so without this its indexer refuses every on-chain PRICE batch with `invalid: insufficient signer stake`. Set to `off` to disable, which is logged loudly. A hub running oracle consensus disarms the pass automatically and is unaffected. | -| `HUB_PRICE_CAPABILITY_DERIVE_LOOKBACK_BLOCKS` | No | `144` | How many Bitcoin blocks back the derivation pass covers, newest first, about a day. A node following the tip needs no more; a node catching up across a longer gap needs this raised, at a cost of one Bitcoin indexer call per uncovered height. | -| `HUB_PRICE_CAPABILITY_DERIVE_INTERVAL_S` | No | `60` | Seconds between derivation passes. Each pass covers at most 64 uncovered heights and skips heights already written, so a steady-state hub spends nothing. | +| `HUB_PRICE_CAPABILITY_DERIVE` | No | `on` | Whether a hub derives capability snapshots from its own Bitcoin view for each of the four capabilities whose consensus writer does not run locally: `price`, `oracle_publish`, `cross_chain`, and `attestation`. Each unit of derivation is one capability-and-height pair. Without these snapshots, the indexer cannot resolve the signer set for the affected on-chain actions. Set to `off` to disable all derived pairs, which is logged loudly. A hub skips each capability covered by a local consensus writer and disarms the pass only when local writers cover all four. | +| `HUB_PRICE_CAPABILITY_DERIVE_LOOKBACK_BLOCKS` | No | `144` | How many Bitcoin heights back the derivation pass covers, newest first, for each capability it must derive. The default is about a day and can represent up to 576 capability-and-height pairs when all four capabilities need derivation. A node following the tip needs no more; a node catching up across a longer gap needs this raised, at a cost of one Bitcoin indexer call per uncovered pair. | +| `HUB_PRICE_CAPABILITY_DERIVE_INTERVAL_S` | No | `60` | Seconds between derivation passes. With the default per-pass budget, each pass covers at most 64 uncovered capability-and-height pairs, finishing all required capabilities at a newer height before moving backward, and skips pairs already written. | | `PUBLISHER_QUEUE_PATH` | No | `./data/publisher-queue.jsonl` | Durable queue file for pending publishes. Point at persistent storage so a restart does not lose queued rows. | | `PUBLISHER_MAX_ATTEMPTS` | No | `5` | Attempts before a queued publish is abandoned. | | `DOGE_PUBKEY_HEX` | No | _(from config table)_ | Public key, hex, of the DOGE publishing wallet. | diff --git a/components/indexer/configuration.md b/components/indexer/configuration.md index 99ce7581..fd81b720 100644 --- a/components/indexer/configuration.md +++ b/components/indexer/configuration.md @@ -45,7 +45,7 @@ Configuration is loaded from a `.env` file and environment variables. Copy the ` | `DB_ACQUIRE_TIMEOUT` | Time to wait for a free pooled connection, in milliseconds | `10000` | | `DB_QUERY_TIMEOUT` | MariaDB query execution timeout in milliseconds | `30000` | | `MIGRATION_STRICT_CHECKSUM` | Set to `1` to make a schema-checksum mismatch fail closed at startup instead of logging and continuing. Off by default so a diverged schema does not cause a surprise fleet-wide boot failure; the operator path (`node src/db/migration/migrate.js`) fails closed regardless. | _(unset, non-fatal)_ | -| `SHUTDOWN_TIMEOUT_MS` | Hard-exit budget for the SIGTERM/SIGINT drain, in milliseconds. On `docker stop` the indexer stops reporting itself running on `/status`, lets the block loop break at its next block boundary (never mid-transaction), drains the API listener, closes its database pools and exits 0; if that has not finished within the budget it logs the overrun and exits 1 instead of lingering until docker's SIGKILL. The default sits under docker's 10 s stop grace because `xchain-node` issues a bare `docker stop`; raise it for a chain whose blocks take longer to apply. A non-numeric or non-positive value keeps the default. | `8000` | +| `SHUTDOWN_TIMEOUT_MS` | Hard-exit budget for the SIGTERM/SIGINT drain, in milliseconds. On `docker stop` the indexer stops reporting itself running on `/status`, lets the block loop break at its next block boundary (never mid-transaction), drains the API listener, closes its database pools and exits 0; if that has not finished within the budget it logs the overrun and exits 1 instead of lingering until docker's SIGKILL. Sized under the 30 s stop budget `xchain-node` gives the indexer, and under the ten seconds docker allows a container created before that budget existed; raise it, keeping it under the stop budget, for a chain whose blocks take longer to apply. When `XCHAIN_NODE_MODULE_STOP_TIMEOUT_SECONDS_XCHAIN_INDEXER` overrides that budget, `xchain-node` sets this to the budget less 20 s (half the budget below 40 s) unless the module config sets it. A non-numeric or non-positive value keeps the default. | `8000` | ### Migration compatibility harness @@ -83,12 +83,14 @@ Configuration is loaded from a `.env` file and environment variables. Copy the ` | `HUB_SYNC_WATERMARK_STALL_S` | How long the hub mirror's stream watermark may stay frozen while the hub's own heartbeat tip runs ahead of it before the mirror forces a fresh subscribe-then-bootstrap. This is the mirror's own bound, distinct from `HUB_SYNC_BARRIER_HOLD_CEILING_S`, which the block loop drives and only while a block is deferring: heartbeats keep arriving during such a stall, so the transport watchdog stays satisfied while the mirror certifies nothing. Suppressed where a frozen watermark is correct: poll mode, an outstanding hub schema-version mismatch, and a mirror that has not yet certified a first watermark. Operational only: it opens no barrier and commits no block early. Seconds; `0` disables the detector. | `180` | | `HUB_SYNC_WATERMARK_STALL_EXIT_S` | How long after that forced resync the watermark still has to stay frozen before the process logs a named fatal and exits non-zero so its supervisor restarts it (the indexer wires the exit; the explorer's vendored copy logs and re-drives). Sized above a full re-bootstrap drain, so an ordinary slow drain finishes and moves the watermark inside the window; any real advance cancels it. Seconds; `0` keeps the forced resync but never exits. | `300` | +**Bridge proof wiring is directional and required for every two-stack deployment.** Every destination indexer crediting a bridged transfer must have the origin chain's indexer API URL configured as `_INDEXER_URL` or `_INDEXER_API_URL`. Configure each direction independently: for example, a DOGE indexer crediting a transfer from BTC needs the BTC indexer URL, while a BTC indexer crediting a transfer from DOGE needs the DOGE indexer URL. Without either URL, that destination holds silently at the bridge proof barrier until the default 900-second (15-minute) hold ceiling; reaching the ceiling can re-drive the wait but never credits an unproven transfer. This is separate from the DOGE-specific ROLLCALL read below, even when both reads use the same DOGE URL. + **The same DOGE wiring is what ROLLCALL runs on, and it becomes required a second time.** From `ROLLCALL_ACTIVATION` onward, every **BTC** indexer closes each roll-call epoch by asking its DOGE indexer for the epoch's signers (`getrollcallsigners`, a federation-read method served off the committed view). It reuses `DOGE_INDEXER_API_URL` → `DOGE_INDEXER_URL` → config, the `DOGE_INDEXER_API_KEY` header, and `ANCHOR_PROOF_TIMEOUT_MS`; there is no separate env knob for it. | Variable | Description | Default | |---|---|---| | `XC_ROLLCALL_GATES_REGTEST_ACTIVATION` | **Regtest only.** Arms ROLLCALL v1 on this private venue: from the armed epoch height on, roll calls must carry the publisher's consensus-gate list, the epoch close records each verified signer's list in `rollcall_gates`, and the attestation capability set drops a validator whose recorded list lacks a rule active at the request block. Same grammar as `XC_ROLLCALL_REGTEST_ACTIVATION`; read **once at startup**; set identically on every hub and BTC indexer in the venue. mainnet and testnet are fixed in source. | _(unset: inert)_ | -| `XC_MIRROR_ADMISSION_ACTIVATION` | **Regtest only.** Arms the per-coin `regtest` entries of `MIRROR_ADMISSION_ACTIVATION` and `MIRROR_ADMISSION_CONSUMER_ACTIVATION` (the mirror-admission heights) and the `regtest` entry of `ANCHOR_ATTEST_BARRIER_ACTIVATION` in the indexer's activation registry (`src/protocol_changes/shared_rows.js`), one variable for the whole barrier family. Same grammar and inert default as `XC_ROLLCALL_REGTEST_ACTIVATION`; the armed form arms at height `0`. Applied when a row is read, from the environment as it stands then; set identically on every hub, indexer, sync and explorer process in the venue. mainnet and testnet are fixed in source. | _(unset: inert)_ | +| `XC_MIRROR_ADMISSION_ACTIVATION` | **Regtest only.** Arms the per-coin `regtest` entries of `MIRROR_ADMISSION_ACTIVATION` and `MIRROR_ADMISSION_CONSUMER_ACTIVATION` (the mirror-admission heights) and the `regtest` entry of `ANCHOR_ATTEST_BARRIER_ACTIVATION` in the indexer's activation registry (`src/protocol_changes/shared_rows.js`), one variable for the whole barrier family. Same grammar and inert default as `XC_ROLLCALL_REGTEST_ACTIVATION`; the armed form arms at height `0`. The indexer's gate modules snapshot these activation maps when they are loaded, so set the variable before startup and restart the indexer after changing it. Set it identically on every hub, indexer, sync and explorer process in the venue. mainnet and testnet are fixed in source. | _(unset: inert)_ | | `XC_ROLLCALL_REGTEST_ACTIVATION` | **Regtest only.** Arms ROLLCALL on this private venue. `armed` (or `genesis`/`on`/`true`/`yes`) activates at BTC height `0`; a bare non-negative integer activates at that height, for a venue whose epochs should begin above an already-indexed prefix; `off`/`inert`/`false` and anything unrecognised leave it inert, and an unrecognised value is logged. Read **once at startup**, so a change needs a restart. mainnet and testnet are fixed in source and cannot be moved from the environment. | _(unset: inert)_ | Regtest ships inert on purpose: arming a network commits every BTC indexer on it to a wired DOGE peer, so a hardcoded height wedged every single-coin BTC venue at its first close. Set this on **every** BTC indexer and hub in a two-chain acceptance venue, alongside `DOGE_INDEXER_API_URL`. A venue that arms its hubs and forgets its indexer shows up as a consensus-rules digest mismatch, because `ROLLCALL_ACTIVATION` is one of the shared gates that digest covers. @@ -236,6 +238,24 @@ actually resolved. |---|---|---| | `MA_SIDE_KEY` | **Harness only.** Side label the side-process reports its hash chain under, set by the parent for its side-processes: `off`, `boundary` or `on` | `boundary` | +### List-owner replay witness + +Read only by `bin/verify-list-owner-replay-equivalence.js` (the below-the-flag +replay witness for `LIST_OWNER_ACTIVATION`, which replays one mainnet or testnet +corpus through a LEGACY tree with the general list-owner check removed and the +OFF tree with its natural inert gate, and compares the resolved four-hash chain +at every block; regtest is refused because its gate is active from height zero); +never by the indexer service itself. Database credentials are loaded by dotenv +from the project `.env` and read through `src/config.js`, never from argv. For +each side-process the parent sets `INDEXER_COIN`, `INDEXER_NETWORK`, +`TEST_DECODER_DB`, `TEST_INDEXER_DB` and the `TEST_DB_*` coordinates documented +for the A7 harness above. + +| Variable | Description | Example | +|---|---|---| +| `LO_SIDE_ROOT` | **Harness only.** Materialized tree the forked side-process replays from (the LEGACY tree or the OFF tree under the harness workdir) | `/tmp/xchain-lo-witness-btc/off` | +| `LO_SIDE_KEY` | **Harness only.** Side label the side-process reports its hash chain under, set by the parent for its side-processes: `legacy` or `off` | `off` | + ### BATCH cost-measurement harness Read only by `bin/measure-batch-execute-cost.js`, which measures the block-loop diff --git a/components/indexer/operations.md b/components/indexer/operations.md index d23a002e..7d516646 100644 --- a/components/indexer/operations.md +++ b/components/indexer/operations.md @@ -5,7 +5,7 @@ ## Prerequisites -- Node.js 22 (22.x LTS), pinned in `.nvmrc`. Node 24 cannot build the native `isolated-vm` module that this component's vendored `xchain-vm` dependency pulls in. +- Node.js 22 (22.x LTS), pinned in `.nvmrc`. Node 24 is outside the consensus runtime this component's vendored `xchain-vm` pins (Node ABI 127, which `checkConsensusRuntime()` enforces); `isolated-vm` itself installs there from a prebuilt binding. - MariaDB server (for both Decoder and Indexer databases) - A running xchain-decoder instance (populating the Decoder database) diff --git a/components/node/configuration.md b/components/node/configuration.md index c4223c87..7ea06a3f 100644 --- a/components/node/configuration.md +++ b/components/node/configuration.md @@ -117,7 +117,7 @@ These variables are read by xchain-node itself at startup. They control runtime | `XCHAIN_NODE_EXTERNAL_DB_ROOT_PASSWORD` | MariaDB root password for the host-native (non-Docker) database, used alongside `XCHAIN_NODE_EXTERNAL_DB=1`. Avoids an interactive password prompt in headless installs. Supply alongside `XCHAIN_NODE_EXTERNAL_DB_HOST`, `XCHAIN_NODE_EXTERNAL_DB_PORT`, and `XCHAIN_NODE_EXTERNAL_DB_ROOT_USER`. | | `XCHAIN_NODE_MODULE_MEMORY_MB_` | Explicit container memory limit in MB for one service, e.g. `XCHAIN_NODE_MODULE_MEMORY_MB_XCHAIN_UTXO_TRACKER=4096` or `XCHAIN_NODE_MODULE_MEMORY_MB_XCHAIN_DECODER=1536` (the service name upper-cased with `-` as `_`). Applied as `--memory` and an equal `--memory-swap` at the next `install`, `update` or `recreate`. `0` disables the derived tracker limit. Without it, only the utxo-tracker is limited, to half the host RAM divided by the number of installed trackers (floor 1024 MB, ceiling 16384 MB); other services run unlimited because they do not size themselves to a cgroup limit. A host whose kernel has no memory cgroup controller accepts the flag and discards it, so confirm the limit landed after the create; [Memory on a multi-chain host](../../operations/deployment.md#memory-on-a-multi-chain-host) has the check and the Raspberry Pi OS fix. | | `XCHAIN_NODE_STOP_TIMEOUT_SECONDS` | Seconds a coin node daemon is given to exit cleanly before docker kills it, on `update` and `recreate` and as the container's own `--stop-timeout` (default: `600`). A daemon flushes its chainstate only on a clean exit; a killed one re-validates from its last flushed block when it returns. Raise it on a host where a large `dbcache` flushes slowly (a Pi writing to a USB SSD). The update prints how long the daemon took and warns when the budget ran out. Applied at the next `update` or `recreate`. | -| `XCHAIN_NODE_MODULE_STOP_TIMEOUT_SECONDS_` | Seconds one service container is given to exit cleanly before docker kills it, e.g. `XCHAIN_NODE_MODULE_STOP_TIMEOUT_SECONDS_XCHAIN_DECODER=300` (the service name upper-cased with `-` as `_`). Used by `stop`, `update`, `recreate` and `uninstall` and stamped on the container as `--stop-timeout`. Defaults: 120 for `xchain-decoder` and `xchain-utxo-tracker`, which break their loops at a block boundary, 30 for every other service. The coin daemon keeps `XCHAIN_NODE_STOP_TIMEOUT_SECONDS`. Applied at the next `update` or `recreate`. See [Stopping](operations.md#stopping). | +| `XCHAIN_NODE_MODULE_STOP_TIMEOUT_SECONDS_` | Seconds one service container is given to exit cleanly before docker kills it, e.g. `XCHAIN_NODE_MODULE_STOP_TIMEOUT_SECONDS_XCHAIN_DECODER=300` (the service name upper-cased with `-` as `_`). Used by `stop`, `update`, `recreate` and `uninstall` and stamped on the container as `--stop-timeout`. Defaults: 120 for `xchain-decoder` and `xchain-utxo-tracker`, which break their loops at a block boundary, 30 for every other service. The coin daemon keeps `XCHAIN_NODE_STOP_TIMEOUT_SECONDS`. The node also derives the service's own drain timer from it, passing `SHUTDOWN_TIMEOUT_MS` as the budget less 20 seconds (half the budget below 40 seconds) so the service always gives up before docker's kill: the default 120 gives the decoder's and tracker's own `100000`. It does this for the decoder and utxo-tracker always and for any other service whose budget is overridden, and never when the module config sets `SHUTDOWN_TIMEOUT_MS` itself. Applied at the next `update` or `recreate`; until then `stop` warns that the running container still carries the timer it was created with. See [Stopping](operations.md#stopping). | | `XCHAIN_NODE_NO_TELEMETRY` | Set to `1` to disable anonymous usage telemetry. Opt-out is also available via the `--no-telemetry` CLI flag or a persisted preference in `~/.xchain-node/telemetry.json`. | | `XCHAIN_NODE_TELEMETRY_URL` | Override the telemetry collector endpoint (default: `https://hub.xchain.io/telemetry`). Useful for self-hosted collectors or test environments. | | `HUB_API_KEY` | API key xchain-node sends as `x-api-key` when talking to the hub, and forwards into the generated service `.env` files. Required whenever the hub runs keyed. Treat as a credential. | @@ -266,6 +266,8 @@ These env vars override where xchain-node stores its filesystem state on the hos > | LTC testnet | `testnet4/blocks/` | > | DOGE / LTC regtest | `regtest/blocks/` | > +> The `testnet4/` path above is Litecoin's testnet directory. BTC testnet4 is a separate network, where contract deploys cannot be mined at current miner block sizes. Use LTC or DOGE testnet for contract deployment and testing. +> > This matters if you ever try to free up disk by hand-mounting *only* the bare `blocks/` path, e.g. `-v /misc/dogecoin/testnet/blocks:/root/.dogecoin/blocks`. On testnet/regtest the daemon writes to `testnet3/blocks/` (etc.), which that bind does **not** cover, so the mount silently catches nothing and blocks keep accumulating on the default disk. No error is raised. > > `XCHAIN_NODE_BLOCKS_DIR` avoids this trap entirely: xchain-node starts the daemon with `-blocksdir=/blocks`, which the daemon honours on every network, so all per-network subdirectories land inside the mounted path (`/blocks/testnet3/blocks/`, `/blocks/regtest/blocks/`, …). A single host bind therefore covers mainnet, testnet, and regtest uniformly. See [Disk Management](../../operations/disk-management.md) for the full disk-offload guide. diff --git a/components/node/operations.md b/components/node/operations.md index f7feb13c..7d51dacf 100644 --- a/components/node/operations.md +++ b/components/node/operations.md @@ -9,7 +9,7 @@ If you are setting up a node for the first time, start with the [Node Operator Q ## Prerequisites -- **Node.js** 22 (22.x LTS), the runtime every component repo pins in its `.nvmrc`. Node 24 cannot build the native `isolated-vm` module that the modules this CLI installs (indexer, explorer) depend on; Node 18 and earlier fail on the ESM-only `mariadb` driver. +- **Node.js** 22 (22.x LTS), the runtime every component repo pins in its `.nvmrc`. Node 24 is outside the consensus runtime the `xchain-vm` inside the modules this CLI installs (indexer, explorer) pins (Node ABI 127, which `checkConsensusRuntime()` enforces); Node 18 and earlier fail on the ESM-only `mariadb` driver. - **Docker** installed and running (`docker --version` and `docker ps` must both succeed) - **npm** for dependency installation @@ -180,7 +180,7 @@ xchain-node stop xchain-decoder dogecoin mainnet # one service xchain-node stop all dogecoin mainnet # everything on that chain ``` -Every stop is a SIGTERM followed by a budget in which the process may finish what it is doing, and docker kills it only when the budget runs out. The coin daemon gets `XCHAIN_NODE_STOP_TIMEOUT_SECONDS` (default 600) because it flushes its chainstate only on a clean exit; the decoder and utxo-tracker get 120 seconds, because each breaks its loop at a block boundary; every other service gets 30. `update`, `recreate` and `uninstall` stop a container the same way before replacing or removing it, and the command prints how long the process took or a warning when it was killed. The budget is also stamped on the container as `--stop-timeout`, so a plain `docker stop` or `docker restart` on the container honours it without `-t`. A container created by a CLI that predates its budget (v0.15.0 for the daemon; the services gained theirs after v0.16.3) carries no stamp and is killed by docker after ten seconds under a plain `docker stop`; `update` or `recreate` replaces it with a stamped one. +Every stop is a SIGTERM followed by a budget in which the process may finish what it is doing, and docker kills it only when the budget runs out. The coin daemon gets `XCHAIN_NODE_STOP_TIMEOUT_SECONDS` (default 600) because it flushes its chainstate only on a clean exit; the decoder and utxo-tracker get 120 seconds, because each breaks its loop at a block boundary; every other service gets 30. `update`, `recreate` and `uninstall` stop a container the same way before replacing or removing it, and the command prints how long the process took, or a warning when it was killed or exited non-zero inside the budget. A service that exits non-zero on its own ran out of its own drain timer or its drain failed: the decoder, utxo-tracker, indexer, sync and explorer each bound their drain with their own `SHUTDOWN_TIMEOUT_MS`, which must stay below this budget. The node derives it from the budget when it creates the container (the budget less 20 seconds, so 100000 for the default 120), for the decoder and utxo-tracker always and for any other service whose budget is overridden, unless the module config sets it; raising the budget therefore gives that drain more time once the container is recreated. `stop` warns when a running container was created under a different budget, or before the node passed the timer, and so still carries an old one. The budget is also stamped on the container as `--stop-timeout`, so a plain `docker stop` or `docker restart` on the container honours it without `-t`. A container created by a CLI that predates its budget (v0.15.0 for the daemon; the services gained theirs after v0.16.3) carries no stamp and is killed by docker after ten seconds under a plain `docker stop`; `update` or `recreate` replaces it with a stamped one. Do not stop a coin daemon from inside its container (`bitcoin-cli stop`, `dogecoin-cli stop`). The daemon exits cleanly, and about two seconds later docker's `unless-stopped` restart policy starts it again, so `docker ps` shows a healthy container and the stop looks as if it failed. The policy exists so a crashed daemon comes back, and it cannot tell a clean exit from a crash; only `docker stop` (which `xchain-node stop` issues) marks the container as deliberately stopped. diff --git a/components/regtest-miner/README.md b/components/regtest-miner/README.md index 2f22ece2..501ba1fe 100644 --- a/components/regtest-miner/README.md +++ b/components/regtest-miner/README.md @@ -5,7 +5,7 @@ ## What is xchain-regtest-miner -xchain-regtest-miner is an auto-mining service for XChain Platform regtest development environments. In regtest mode, Bitcoin-family coin nodes (bitcoind, litecoind, dogecoind) do not mine blocks automatically, developers must mine manually via `generatetoaddress` or script the calls themselves. The regtest miner eliminates this friction by polling the mempool every second and mining blocks automatically whenever transactions are detected. +xchain-regtest-miner is an auto-mining service for XChain Platform regtest development environments. In regtest mode, Bitcoin-family coin nodes (bitcoind, litecoind, dogecoind) do not mine blocks automatically, developers must mine manually via `generatetoaddress` or script the calls themselves. The regtest miner eliminates this friction by polling the mempool every 100 ms and mining blocks automatically whenever transactions are detected. The miner uses an adaptive dual-timer system: when the first unconfirmed transaction appears, a 30-second max timer starts; each additional transaction resets a 5-second extension timer. Mining triggers when either timer expires. This batching strategy groups related transactions (such as a P2SH fund transaction and its corresponding spend) into the same block, which is important for correct platform behavior since some XChain encoding formats require both transactions to be confirmed together. @@ -17,12 +17,12 @@ This service is testing infrastructure. It must not be run against mainnet or te - **Automatic wallet management**: creates, loads, and funds a regtest wallet on startup; mines 101 bootstrap blocks on a fresh chain for coinbase maturity - **JSON-RPC control API**: 9 endpoints for health checks, status reporting, fund transfers, mempool stress testing, mining pause/resume, timer configuration, and block generation - **Mempool stress testing**: `fill_mempool` constructs and broadcasts thousands of raw Bitcoin transactions using BIP32/BIP39 key derivation and PSBT signing for load testing -- **Exponential backoff**: automatic retry with capped exponential backoff (1s to 30s) on RPC connection failures, with counter reset on success +- **Exponential backoff**: automatic retry with capped exponential backoff (200 ms to 30s) on RPC connection failures, with counter reset on success - **Graceful shutdown**: SIGTERM handler allows the current mining loop iteration to complete before exiting - **Input validation**: rejects invalid addresses, amounts, timer values, and transaction quantities before any RPC call is made - **Error sanitization**: RPC credentials are never exposed in error messages or console output - **Concurrent call protection**: `fillMempool` mutex prevents overlapping stress test runs, with automatic `keepMining` flag restoration in a finally block -- **Docker-ready**: Alpine Node 22 image with non-root user, healthcheck via JSON-RPC ping, and hardened security headers (Helmet, CORS) +- **Docker-ready**: Alpine Node 22 image with non-root user, healthcheck via JSON-RPC `health` (503 when the miner is stalled), and hardened security headers (Helmet, CORS) - **1,007 tests** (measured 2026-07-27): unit, integration, e2e, smoke, boundary, security, fuzz, chaos, performance, mutation, and regression testing ## Documentation @@ -66,7 +66,7 @@ On startup, the miner: 1. Validates all 6 required environment variables 2. Creates or loads the `xchain_regtest_wallet` wallet 3. Mines 101 bootstrap blocks if the chain is fresh (coinbase maturity) -4. Begins the 1-second mempool polling loop +4. Begins the 100 ms mempool polling loop 5. Starts the Express JSON-RPC API server ## Scripts diff --git a/components/regtest-miner/architecture.md b/components/regtest-miner/architecture.md index 54165e27..bff8cdf9 100644 --- a/components/regtest-miner/architecture.md +++ b/components/regtest-miner/architecture.md @@ -48,7 +48,7 @@ flowchart TD ## Mining Loop -The miner's core loop runs every 1 second (`CHECK_BLOCK_DELAY_MS`): +The miner's core loop runs every 100 ms (`CHECK_BLOCK_DELAY_MS`): 1. Poll `getrawmempool` to check for unconfirmed transactions 2. If new transactions are detected (mempool length increased): @@ -61,7 +61,7 @@ The loop skips mempool polling when `keepMining` is `false`, allowing external c ```mermaid flowchart TD - SLEEP["Sleep
1 second"] + SLEEP["Sleep
100 ms"] CHECK["Check keepMining flag"] POLL["getrawmempool"] NEWTX{"New txs detected?"} diff --git a/components/regtest-miner/configuration.md b/components/regtest-miner/configuration.md index a83290d6..461c0196 100644 --- a/components/regtest-miner/configuration.md +++ b/components/regtest-miner/configuration.md @@ -22,11 +22,11 @@ All configuration is via environment variables (loaded from `.env` by dotenv). T | Variable | Description | |---|---| -| `MINER_API_KEY` | When set, every JSON-RPC request must carry a matching `X-API-Key` header (401 otherwise). `ping` and `status` are exempt so healthchecks keep working. Unset by default (no auth), mirroring the encoder/hub opt-in pattern. | +| `MINER_API_KEY` | When set, every JSON-RPC request must carry a matching `X-API-Key` header (401 otherwise). The read-only methods `ping`, `status` and `health` are exempt so healthchecks keep working; the bundled Docker healthcheck posts `health` with no key. Only read-only methods belong in this exempt set. Unset by default (no auth), mirroring the encoder/hub opt-in pattern. | | `MINER_STALL_ERROR_THRESHOLD` | Consecutive failed mining cycles before the `health` probe reports the miner stalled. Defaults to `5`. A deliberate pause is never counted as a stall. | | `MINER_WALLET_GRACE_MS` | Cold-start grace period before a wallet that never became ready is reported as a stall by the `health` probe. Defaults to `60000` (60 seconds). | -| `NODE_RPC_TIMEOUT` | HTTP timeout in milliseconds for all JSON-RPC calls to the coin node (sets `axios.defaults.timeout` at startup). Defaults to `60000`, which is **not** the decoder's default for the same variable name (`30000`); if you export it globally for a whole stack, both services pick up your value. | -| `IDLE_MINE_INTERVAL_MS` | Mine one empty block whenever the mempool has been empty this long. Unset or `0` (the default) keeps the mining loop purely mempool-driven, which means an idle chain never advances a block and anything gated on HEIGHT stalls: stake activation delays, confirmation depth, time-locked expiries. Set it on venues whose tests wait out a height window with no transactions in flight. Same bounds as the mining timers (1,000 to 3,600,000 ms); changeable at runtime with `set_idle_mine_interval`. | +| `NODE_RPC_TIMEOUT` | HTTP timeout in milliseconds for all JSON-RPC calls to the coin node (sets `axios.defaults.timeout` at startup). Defaults to `60000`, and a value that is not a plain non-negative integer falls back to it (`0` disables the timeout). That default is **not** the decoder's default for the same variable name (`30000`); if you export it globally for a whole stack, both services pick up your value. | +| `IDLE_MINE_INTERVAL_MS` | Mine one empty block whenever the mempool has been empty this long. Defaults to `60000` when unset, so a rebooted chain with an empty mempool still advances height on its own. `0` keeps the mining loop purely mempool-driven, which means an idle chain never advances a block and anything gated on HEIGHT stalls: stake activation delays, confirmation depth, time-locked expiries. Set `0` on venues whose tests count blocks (reorg or confirmation-depth assertions), since each heartbeat block is a real block. Same bounds as the mining timers (1,000 to 3,600,000 ms); changeable at runtime with `set_idle_mine_interval`. | ### Validation Rules @@ -40,16 +40,19 @@ All configuration is via environment variables (loaded from `.env` by dotenv). T | Constant | Value | Description | |---|---|---| -| `CHECK_BLOCK_DELAY_MS` | 1000 | Mempool polling interval (1 second) | +| `CHECK_BLOCK_DELAY_MS` | 100 | Mempool polling interval (100 ms) | | `DEFAULT_MAX_TIME_TO_MINE_TXS` | 30000 | Max time before mining after first tx (30 seconds) | | `DEFAULT_ADDED_TIME_TO_MINE_TXS` | 5000 | Extension time on each new tx (5 seconds) | | `MIN_MINING_TIME` | 1000 | Minimum allowed timer value via API (1 second) | | `MAX_MINING_TIME` | 3600000 | Maximum allowed timer value via API (1 hour) | | `MAX_FILL_MEMPOOL_QUANTITY` | 50000 | Maximum transactions for `fill_mempool` | +| `MAX_GENERATE_BLOCKS` | 10000 | Maximum blocks per `generate_blocks` call (a larger `count` is rejected) | | `MAX_SEND_RETRIES` | 50 | Maximum retry attempts for funding in fillMempool | | `OUTPUTS_QUANTITY_PER_TX` | 2500 | Maximum outputs per PSBT in fillMempool | | `MAX_BACKOFF_MS` | 30000 | Maximum exponential backoff delay (30 seconds) | +The 100 ms poll is deliberately independent of the 1-second `MIN_MINING_TIME` floor: it lets the loop notice an expired timer within about 100 ms instead of up to a full second late. Moving one value does not imply moving the other. + ### Timer Behavior The dual-timer system uses two independent timers that run simultaneously: @@ -64,9 +67,11 @@ Both timers can be reconfigured at runtime via the `set_mining_time` JSON-RPC me When the coin node is unreachable, the miner retries with exponential backoff: ``` -delay = min(1000 * 2^attempts, MAX_BACKOFF_MS) +delay = min(CHECK_BLOCK_DELAY_MS * 2^attempts, MAX_BACKOFF_MS) ``` +`attempts` counts consecutive failures including the current one, so the first retry waits 200 ms and the delay doubles up to the 30-second ceiling. + The attempt counter resets to zero on the first successful RPC call. --- diff --git a/components/regtest-miner/operations.md b/components/regtest-miner/operations.md index 785ec2b6..fade5a76 100644 --- a/components/regtest-miner/operations.md +++ b/components/regtest-miner/operations.md @@ -21,7 +21,7 @@ On startup, the miner: 2. Connects to the coin node via JSON-RPC 3. Creates or loads the `xchain_regtest_wallet` wallet 4. Mines 101 bootstrap blocks if the chain is fresh (coinbase maturity) -5. Begins the 1-second mempool polling loop +5. Begins the 100 ms mempool polling loop 6. Starts the Express JSON-RPC API server on `REGTEST_MINER_API_PORT` ## Docker @@ -29,7 +29,7 @@ On startup, the miner: When managed by xchain-node, the regtest miner runs as a Docker container: - **Image**: Alpine Node 22 with non-root user -- **Healthcheck**: JSON-RPC `ping` call +- **Healthcheck**: JSON-RPC `health` call, which answers 503 when the miner is stalled (`ping` always answers 200) - **Security headers**: Helmet (CSP, X-Frame-Options, etc.) - **CORS**: Enabled for cross-origin access @@ -45,7 +45,7 @@ The miner exposes a JSON-RPC 2.0 API via Express for test orchestration. All met ### Authentication -By default the API is open (no auth), matching the encoder/hub opt-in pattern. Setting the `MINER_API_KEY` environment variable requires a matching `X-API-Key` header on every request; a missing or wrong key returns HTTP 401. The read-only health methods `ping` and `status` always bypass the key gate, so Docker healthchecks and uptime monitors keep working on keyed deployments. +By default the API is open (no auth), matching the encoder/hub opt-in pattern. Setting the `MINER_API_KEY` environment variable requires a matching `X-API-Key` header on every request; a missing or wrong key returns HTTP 401. The read-only health methods `ping`, `status` and `health` always bypass the key gate, so Docker healthchecks and uptime monitors keep working on keyed deployments. Only read-only methods belong in this exempt set. Note that the miner refuses to start when `NETWORK` is `mainnet` (only `regtest` and `testnet` are accepted): `send_funds` would otherwise expose a default-unauthenticated way to spend the node wallet. @@ -270,7 +270,7 @@ Mine a specific number of empty blocks immediately, regardless of mempool state. | Parameter | Type | Required | Description | |---|---|---|---| -| `count` | number | Yes | Number of blocks to mine (must be a positive integer) | +| `count` | number | Yes | Number of blocks to mine (a positive integer, at most 10000; a larger value is rejected with an error) | **Response:** @@ -316,7 +316,7 @@ Remove a block from the invalid set so the node can re-evaluate chain selection. ### Exponential Backoff -When the coin node is unreachable (ECONNREFUSED, timeout, DNS failure), the miner retries with capped exponential backoff from 1 second to 30 seconds. The attempt counter resets on the first successful call. +When the coin node is unreachable (ECONNREFUSED, timeout, DNS failure), the miner retries with capped exponential backoff from 200 ms to 30 seconds. The attempt counter resets on the first successful call. ### Pinned Wallet Fee Rate diff --git a/components/sdk/README.md b/components/sdk/README.md index 7a33930b..cb4990e4 100644 --- a/components/sdk/README.md +++ b/components/sdk/README.md @@ -9,7 +9,7 @@ xchain-sdk is the developer-facing Software Development Kit for the XChain Platf ## Features -- Generate all 31 XChain ACTION command strings (SEND, ISSUE, MINT, DESTROY, ORDER, DISPENSER, DIVIDEND, SWEEP, SWAP, CALLBACK, SLEEP, AIRDROP, MESSAGE, LIST, LINK, FILE, BROADCAST, ADDRESS, BATCH, DEPLOY, EXECUTE, DEPOSIT, WITHDRAW, COINPAY, STAKE, UNSTAKE, DELEGATE, COLLECT, PRICE, VOTE, BET) +- Generate all 32 XChain ACTION command strings (SEND, ISSUE, MINT, DESTROY, ORDER, DISPENSER, DIVIDEND, SWEEP, SWAP, CALLBACK, SLEEP, AIRDROP, MESSAGE, LIST, LINK, FILE, BROADCAST, ADDRESS, BATCH, DEPLOY, EXECUTE, DEPOSIT, WITHDRAW, COINPAY, STAKE, UNSTAKE, DELEGATE, COLLECT, PRICE, VOTE, BET, XBRIDGE) - **Transaction Lifecycle Manager** (`submitAction`): full encode → sign → broadcast → wait pipeline in a single call, with automatic P2SH two-phase handling and progress callbacks - **Wallet Sessions** (`sdk.session(wif)`): bound wallet object that bundles address/key/UTXO state with action convenience methods: "I am this address, do things" - **Fee Estimation** (`estimateFees`): dry-run fee calculation via encoder, returns fee in satoshis plus reusable PSBT @@ -55,7 +55,7 @@ xchain-sdk is the developer-facing Software Development Kit for the XChain Platf | Document | Description | |---|---| | [Configuration](configuration.md) | Constructor options, environment variables, hub discovery, retry and pooling config | -| [Actions](actions.md) | All 31 supported ACTION types, parameters, and version formats | +| [Actions](actions.md) | All 32 supported ACTION types, parameters, and version formats | | [Transaction Lifecycle](lifecycle.md) | `submitAction`, fee estimation, UTXO chaining, P2SH two-phase handling | | [Wallet Sessions](sessions.md) | Bound wallet sessions, convenience methods, UTXO cache | | [Workflows](workflows.md) | High-level recipes: issueAndDistribute, deployAndFund, stakeAndDelegate | diff --git a/components/sdk/contracts.md b/components/sdk/contracts.md index ad1054de..088c2405 100644 --- a/components/sdk/contracts.md +++ b/components/sdk/contracts.md @@ -215,10 +215,11 @@ Checks for: - Code size limit (64 KB) - JavaScript syntax errors and unsupported syntax (ES2020 maximum, via acorn) - Reserved identifier usage (`__gas`, the allocator metering helpers, and the call-depth metering helpers `__depth_enter`/`__depth_exit`) -- Banned transcendental Math calls (`Math.sqrt`, `Math.pow`, `Math.log`, etc.) +- Banned transcendental Math calls (`Math.sqrt`, `Math.pow`, `Math.log`, etc.), including global-object-qualified spellings such as `globalThis.Math.pow` and `this.Math.pow` - Banned native-DoS literals (BigInt and RegExp literals) - Banned async surface (`async`, `await`, `Promise`) - Float literal warnings (advisory; does not block deployment) +- Sandbox-removal warnings (advisory; do not block deployment): `banned-proto-method` for a call to a prototype method the sandbox removes (`.match()`, `.localeCompare()`, ...) and `banned-stripped-global` for a read of a global it deletes (`Date`, `fetch`, ...); both throw at runtime. See [Deploy-Time Validation](../../developer-guide/smart-contract-development.md#deploy-time-validation) for the full name lists. - Logic-level advisories: `crossCallable` integrity, unbounded loops, unchecked `state.get` dereferences, missing input validation ```js diff --git a/components/sync/configuration.md b/components/sync/configuration.md index 31a6baff..9f5381b5 100644 --- a/components/sync/configuration.md +++ b/components/sync/configuration.md @@ -78,6 +78,7 @@ In client mode, the service connects to remote sync servers and replicates their | `VERIFY_STATE_COMMITMENT` | No | `true` | **Security-critical. Default ON.** When `true`, the per-block SPV state-commitment roots (balances, block Merkle root) are recomputed over the replica and compared to the source's committed values. A mismatch triggers a durable halt. NULL roots (blocks before the flag-day) are skipped. **Set to `false` on truncated replicas** seeded via `SYNC_BOOTSTRAP_DEPTH_*`, because their incomplete balances history would produce wrong roots. | | `INDEX_MAP_PARITY_CHECK` | No | `false` | **Advisory only; never halts.** When `true`, periodically checks that the replica's `index_addresses` id-to-address mapping matches the source's deterministic-subset checksum. A mismatch is logged and counted but never halted on. Off by default because computing it scans `index_addresses` (an index on `block_index` is advisable before enabling on a high-volume chain). | | `TABLE_CONTENT_PARITY_CHECK` | No | `false` | **Advisory only; never halts.** When `true`, compares per-table content checksums between source and replica, not just the row counts published beside them: an equal-count content substitution in a table that no consensus hash reads passes every other check. Read on both sides (the server publishes the checksums on `/status`, a client at the same height recomputes and compares). A mismatch is logged and durably counted. Off by default because it reads a window of about 93 indexer tables per status poll. | +| `TOKEN_FOLD_PARITY_CHECK` | No | `false` | **Advisory only; never halts.** When `true`, compares a digest of the `tokens` metadata columns that an `ISSUE` edit folds in place. No consensus hash covers these columns, and the content-parity windows leave them out as in-place state, so this is the only check that sees a replica missing an edit or its reorg reversal. Read on both sides (the server publishes the digest on `/status`, a client at the same height recomputes and compares). A mismatch is logged and durably counted. Off by default because it scans `tokens` on the source once per status poll. | | `TABLE_CONTENT_PARITY_WINDOW` | No | `100` | How many blocks each content checksum spans, and for append-only lookups that carry no block column, how many ids. Server-side setting: the source publishes the window it used and a follower recomputes over that same span, so the two can never compare different spans and only the source needs tuning. Clamped to `[1, 10000]`, since `0` or a negative would silently disable the check while still reporting passes, and an unbounded value would read a table's whole history every poll. | | `COMPLETENESS_CHECK_INTERVAL` | No | `3600000` | **Advisory only; never halts.** How often, in milliseconds, a live client re-runs the replica-completeness sweep against its primary source: the per-table row counts the source publishes on `/status`, compared against its own, which is the only check that sees a follower missing rows the consensus hashes cannot cover. `0` disables it. Runs only when the replica and the source are at the same height, because a shortfall while behind is ordinary lag. Deliberately slow by default: the sweep makes the source run a `COUNT(*)` per replicated table. | | `REPLICA_GAP_ALERT_SWEEPS` | No | `2` | How many consecutive equal-height completeness sweeps a table must stay short before the client escalates it from the ordinary per-sweep shortfall line to the distinct, rate-limited `REPLICA_GAP_PERSISTENT` alert and records the gap in sync state for monitors. Clamped to at least `1`. Read from the client config first, then the environment. | @@ -100,6 +101,8 @@ In client mode, the service connects to remote sync servers and replicates their | `CLIENT_SOURCE_STALE_MS` | No | `180000` | How long (ms) without any WebSocket event from the source (block pushes or the periodic status heartbeat) before the replica's `/status` reports `source_height_stale: true`. | | `DISPENSERS_RECONCILE_EVERY` | No | `20` | Reconcile the decoder-replica `dispensers` table (which converges by snapshot, not the block stream) every Nth catch-up in steady state. | | `DISPENSERS_RECONCILE_MAX_INTERVAL_MS` | No | `1800000` | Upper bound (ms, default 30 minutes) on time between `dispensers` reconciles regardless of catch-up cadence. | +| `LOOKUP_PAGE_SIZE` | No | `50000` | Rows per page when a client pages the append-only lookup tables by id cursor (at least `1`; the client clamps it to `100000`). | +| `GAP_LOG_INTERVAL_MS` | No | `30000` | Throttle window (ms) for the client's catch-up gap log summaries (at least `1`). | | `CHECKPOINT_ANCHOR_URL` | No | None | Dedicated source URL for checkpoint fetches when `VERIFY_CHECKPOINT_QUORUM=true`; falls back to the first `SYNC_SOURCES` entry. The client rejects checkpoint sequence rollback and withholding from this source (advisory, does not halt). | | `CHECKPOINT_SEED__` | No | None | JSON override for the pinned checkpoint-quorum validator seed set (fail-closed: malformed JSON is an error, not a fallback). Used with `VERIFY_CHECKPOINT_QUORUM` to bootstrap trust on chains without a compiled-in pinned set. | | `CHECKPOINT_FRESHNESS_BLOCKS` | No | `500` | Freshness bound, in applied blocks, for the checkpoint anchor. When the newest quorum checkpoint trails the replica tip by more than this, the anchor can no longer catch a forged tail, so the gap is logged. Advisory by default: withholding is not proof of forgery, and halting on absence would hand an attacker a DoS-halt. | diff --git a/components/utxo-tracker/architecture.md b/components/utxo-tracker/architecture.md index 60bb7c1a..b4b52e24 100644 --- a/components/utxo-tracker/architecture.md +++ b/components/utxo-tracker/architecture.md @@ -57,7 +57,7 @@ flowchart TD | `src/chain/crypto_networks.js` | `CryptoNetworks` | Network parameter lookup: maps network names to bitcoinjs-lib network objects for 9 network variants | | `src/common/util.js` | None | Utility functions: timing, hex/uint8 conversion, formatting | | `src/chain/bufferutils.js` | `BufferReader`, `BufferWriter` | Binary buffer reading/writing: UInt8/16/32/64LE, VarInt, slices | -| `src/bulk-sync/` | (multiple) | Bulk-sync pipeline: offline parallel parse and load for initial database population on an empty DB (orchestrator, parse worker, merger, writers, loader, validator, and supporting utilities) | +| `src/bulk_sync/` | (multiple) | Bulk-sync pipeline: offline parallel parse and load for initial database population on an empty DB (orchestrator, parse worker, merger, writers, loader, validator, and supporting utilities) | ## LevelDB Key Schema diff --git a/components/utxo-tracker/configuration.md b/components/utxo-tracker/configuration.md index 9760d66d..97131ef0 100644 --- a/components/utxo-tracker/configuration.md +++ b/components/utxo-tracker/configuration.md @@ -24,7 +24,7 @@ BULK_SYNC_RAM_BUDGET=768 | `NODE_USER` | Coin node RPC username | `rpc` | | `NODE_PASSWORD` | Coin node RPC password | `rpc` | | `UTXO_TRACKER_API_PORT` | API server listening port | `3001` | -| `SHUTDOWN_TIMEOUT_MS` | Hard-exit budget for the SIGTERM drain, in milliseconds. On `docker stop` the tracker stops its block loop at the next block boundary, closes the API listener and the LevelDB store, and exits 0; if that has not finished within the budget it exits 1 instead of lingering until docker's SIGKILL. Sized under the 120 s stop budget `xchain-node` gives a tracker. | `100000` | +| `SHUTDOWN_TIMEOUT_MS` | Hard-exit budget for the SIGTERM drain, in milliseconds. On `docker stop` the tracker stops its block loop at the next block boundary, closes the API listener and the LevelDB store, and exits 0; if that has not finished within the budget it exits 1 instead of lingering until docker's SIGKILL. `xchain-node` sets it when it creates the container, to the tracker's stop budget less 20 s (`100000` from the default 120 s), unless the module config sets it; see `XCHAIN_NODE_MODULE_STOP_TIMEOUT_SECONDS_` in the [node configuration](../node/configuration.md). | `100000` | ### Optional Variables diff --git a/components/utxo-tracker/operations.md b/components/utxo-tracker/operations.md index fb1d17a7..60f28751 100644 --- a/components/utxo-tracker/operations.md +++ b/components/utxo-tracker/operations.md @@ -84,7 +84,7 @@ Returns all unspent outputs for an address, including both confirmed and mempool "value": "100000000", "height": 119, "confirmations": 13, - "amount": 1, + "amount": "1.00000000", "scriptPubKey": "76a914...7988ac" } ] diff --git a/components/vm/README.md b/components/vm/README.md index ba078339..bd60e5d2 100644 --- a/components/vm/README.md +++ b/components/vm/README.md @@ -38,7 +38,7 @@ cd xchain-vm npm install ``` -`isolated-vm` requires native C++ compilation. On Debian/Ubuntu: `build-essential`, `python3`, `libnghttp2-dev`, `libicu-dev`, `libbrotli-dev`, `libc-ares-dev`. +`isolated-vm` installs from a prebuilt binding on linux x64/arm64 (glibc and musl), darwin-arm64 and win32-x64, so no compiler is needed there. Elsewhere npm falls back to a source build, which needs `build-essential`, `python3`, `libnghttp2-dev`, `libicu-dev`, `libbrotli-dev`, `libc-ares-dev` on Debian/Ubuntu. ## Quick Start diff --git a/components/vm/architecture.md b/components/vm/architecture.md index 4f68208d..ed9bd3b8 100644 --- a/components/vm/architecture.md +++ b/components/vm/architecture.md @@ -54,8 +54,8 @@ flowchart TD | `state.js` | StateManager: reads from initial snapshot, tracks writes/deletes in dirty map, enforces key count, key size, and value size limits, provides `getChanges()` for result collection | | `collector.js` | EmissionCollector: queues emitted actions (with emission cap), collects debug logs (100 entries, 1 KB UTF-8 each, with byte-aware truncation) | | `validator.js` | ActionValidator: pre-validates emitted actions against the 21 allowed action types (`SEND`, `DESTROY`, `ISSUE`, `MINT`, `ORDER`, `DISPENSER`, `DIVIDEND`, `AIRDROP`, `CALLBACK`, `FILE`, `LIST`, `COINPAY`, `SWEEP`, `LINK`, `BROADCAST`, `MESSAGE`, `ATTEST`, `SLASH`, `EXECUTE`, `XCALL`, `VOTE`) and checks params shape | -| `lint_core.js` | Dependency-light (no isolated-vm) acorn-only consensus validation, exporting `lintSource()`, `analyzeContract()`, and per-rule detectors: `findBannedMathCalls`, `findBannedLiterals`, `findBannedAsync`, `findBannedGenerator` (`function*`/`yield`), `findBannedWasm` (global `WebAssembly` reference), `findBannedRest` (unmeterable rest positions), `findBannedExponentiation` (`**`/`**=`, hardened-only), `findBannedProtoMethods` (advisory, name-only match, never consensus), `findReservedControlBinding` (`CONTRACT_WRAPPER` bindings, hardened-only), and `codeSizeBytes`/`MAX_CODE_SIZE` for the 64KiB deploy cap. Under `VM_LINT_HARDENING`, the Math ban widens from the five named transcendentals to the complement of the `SAFE_MATH_MEMBERS` whitelist. Holds `CONSENSUS_RULES` (the 9-member set of deploy-blocking rule names: `invalid-type`, `unsupported-syntax`, `reserved-identifier`, `banned-math`, `banned-literal`, `banned-async`, `banned-generator`, `banned-rest`, `banned-wasm`) and `lintSource()`. Vendored byte-for-byte into `xchain-sdk/src/contract/lint_core.js`; a CI SHA-256 parity guard fails on drift. | -| `syntax.js` | Deploy-time validation: V8 syntax check (throwaway isolate, the one step requiring isolated-vm; blocks via a separate early return and is not itself a `CONSENSUS_RULES` member), then delegates all acorn-coverable consensus rules to `lint-core.lintSource()`. Re-exports `findBannedMathCalls`, `findBannedLiterals`, `findBannedAsync`, `findBannedGenerator`, `findBannedWasm`, and `findBannedRest` for backward compatibility. `validateSyntax(code, opts)` threads five independent consensus gates: `enforceBannedAsync` (the `VM_BANNED_ASYNC` flag-day), `enforceLintHardening` (the `VM_LINT_HARDENING` flag-day, widening the Math ban and adding the exponentiation/reserved-binding/dynamic-import rules), `enforceBannedGenerator`/`enforceBannedWasm` (the Package 3 per-coin sandbox height gate, active from genesis on testnet/regtest), and `enforceBannedRest` (the separate [`REST_PATTERN_METER`](../../protocol/flag-days.md) gate, likewise active from genesis on testnet/regtest). Each defaults to `true` for author-facing callers so a from-genesis replay reproduces the historical deploy verdict at every activation. | +| `lint_core.js` | Dependency-light (no isolated-vm) acorn-only consensus validation, exporting `lintSource()`, `analyzeContract()`, and per-rule detectors: `findBannedMathCalls`, `findBannedLiterals`, `findBannedAsync`, `findBannedGenerator` (`function*`/`yield`), `findBannedWasm` (global `WebAssembly` reference), `findBannedRest` (unmeterable rest positions), `findBannedExponentiation` (`**`/`**=`, hardened-only), `findBannedProtoMethods` (advisory, name-only match, never consensus), `findBannedStrippedGlobals` (advisory, reads of sandbox-deleted globals, never consensus), `findReservedControlBinding` (`CONTRACT_WRAPPER` bindings, hardened-only), and `codeSizeBytes`/`MAX_CODE_SIZE` for the 64KiB deploy cap. Under `VM_LINT_HARDENING`, the Math ban widens from the five named transcendentals to the complement of the `SAFE_MATH_MEMBERS` whitelist. Under `LINT_GLOBAL_ALIAS` (the `globalAlias` option), the banned-math, banned-async and banned-wasm detectors also treat sloppy-mode `this` and the `globalThis.globalThis…` self-reference chain as the global object. Holds `CONSENSUS_RULES` (the 9-member set of deploy-blocking rule names: `invalid-type`, `unsupported-syntax`, `reserved-identifier`, `banned-math`, `banned-literal`, `banned-async`, `banned-generator`, `banned-rest`, `banned-wasm`) and `lintSource()`. Vendored byte-for-byte into `xchain-sdk/src/contract/lint_core.js`; a CI SHA-256 parity guard fails on drift. | +| `syntax.js` | Deploy-time validation: V8 syntax check (throwaway isolate, the one step requiring isolated-vm; blocks via a separate early return and is not itself a `CONSENSUS_RULES` member), then delegates all acorn-coverable consensus rules to `lint-core.lintSource()`. Re-exports `findBannedMathCalls`, `findBannedLiterals`, `findBannedAsync`, `findBannedGenerator`, `findBannedWasm`, and `findBannedRest` for backward compatibility. `validateSyntax(code, opts)` threads six independent consensus gates: `enforceBannedAsync` (the `VM_BANNED_ASYNC` flag-day), `enforceLintHardening` (the `VM_LINT_HARDENING` flag-day, widening the Math ban and adding the exponentiation/reserved-binding/dynamic-import rules), `enforceLintGlobalAlias` (the per-coin [`LINT_GLOBAL_ALIAS_ACTIVATION`](../../protocol/protocol-activation.md#vm-gates-service-carried) height gate, armed at genesis on every network, making banned-math, banned-async and banned-wasm resolve sloppy-mode `this` and the `globalThis` self-reference chain as the global object), `enforceBannedGenerator`/`enforceBannedWasm` (the Package 3 per-coin sandbox height gate, active from genesis on testnet/regtest), and `enforceBannedRest` (the separate [`REST_PATTERN_METER`](../../protocol/flag-days.md) gate, likewise active from genesis on testnet/regtest). Each defaults to `true` for author-facing callers so a from-genesis replay reproduces the historical deploy verdict at every activation. | | `errors.js` | ContractRevertError (thrown by `revert()`/`require()`) and GasExhaustedError (thrown when gas ceiling exceeded) | ## JSON Bridge Protocol diff --git a/components/vm/operations.md b/components/vm/operations.md index b1ec9f23..6d0e39f7 100644 --- a/components/vm/operations.md +++ b/components/vm/operations.md @@ -5,8 +5,8 @@ ## Prerequisites -- **Node.js** v22 exactly: `isolated-vm` requires 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-vm` compilation: `build-essential`, `python3`, `libnghttp2-dev`, `libicu-dev`, `libbrotli-dev`, `libc-ares-dev` (Debian/Ubuntu) +- **Node.js** v22 exactly: `src/consensus_runtime.js` pins the Node ABI to 127, so Node 24 fails `checkConsensusRuntime()` (below Node 22 tests silently skip rather than fail, producing false greens) +- **Native build tools** only where `isolated-vm` has no prebuilt binding (it ships them for linux x64/arm64 glibc and musl, darwin-arm64 and win32-x64) and npm falls back to a source build: `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 @@ -16,7 +16,7 @@ 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. +`npm install` resolves a prebuilt `isolated-vm` binding for the running Node ABI. If it falls back to a source build and that fails, install the native build prerequisites above. ## Running Tests @@ -149,12 +149,12 @@ Before a contract is deployed, `vm.validateSyntax(code)` runs the following chec 1. **V8 syntax check**: compiles the code in a throwaway 8 MB isolate to catch syntax errors (the only step requiring `isolated-vm`) 2. **Acorn metering pass**: runs `meterCode()` to ensure acorn can parse the source (effective ES2020 ceiling) 3. **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. Under `VM_LINT_HARDENING` this also covers the `CONTRACT_WRAPPER`'s injected control bindings (`__contractCode`, `__methodName`, `__isCrossCall`, `__readManifest`). -4. **Banned transcendental Math check**: rejects calls to `Math.sqrt`, `Math.pow`, `Math.log`, `Math.log2`, and `Math.log10` in 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. Under `VM_LINT_HARDENING` the ban widens to the complement of the deterministic SafeMath whitelist (`floor`, `ceil`, `round`, `abs`, `min`, `max`, `sign`, `trunc`, `PI`, `E`), plus the `**`/`**=` exponentiation operator. Use `xchain.math.*` (mathjs bignumber) instead. +4. **Banned transcendental Math check**: rejects calls to `Math.sqrt`, `Math.pow`, `Math.log`, `Math.log2`, and `Math.log10` in dotted (`Math.pow`), computed-string (`Math['pow']`) and global-object-qualified (`globalThis.Math.pow`, `this.Math.pow`; see [Global-object spellings](#global-object-spellings)) 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. Under `VM_LINT_HARDENING` the ban widens to the complement of the deterministic SafeMath whitelist (`floor`, `ceil`, `round`, `abs`, `min`, `max`, `sign`, `trunc`, `PI`, `E`), plus the `**`/`**=` exponentiation operator. Use `xchain.math.*` (mathjs bignumber) instead. 5. **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. -6. **Banned async check** (consensus-gated): rejects `async` functions, `await` expressions, and `Promise` references after the `VM_BANNED_ASYNC` flag-day. The CONTRACT_WRAPPER invokes exports synchronously; an async export returns a pending Promise whose post-`await` effects depend on isolated-vm's version-dependent microtask-drain timing, which is outside the consensus-runtime pin and can diverge across validators. Under `VM_LINT_HARDENING` this also rejects dynamic `import(...)` (it evaluates to a Promise). +6. **Banned async check** (consensus-gated): rejects `async` functions, `await` expressions, and `Promise` references (bare or [global-object-qualified](#global-object-spellings)) after the `VM_BANNED_ASYNC` flag-day. The CONTRACT_WRAPPER invokes exports synchronously; an async export returns a pending Promise whose post-`await` effects depend on isolated-vm's version-dependent microtask-drain timing, which is outside the consensus-runtime pin and can diverge across validators. Under `VM_LINT_HARDENING` this also rejects dynamic `import(...)` (it evaluates to a Promise). 7. **Banned generator check** (consensus-gated, Pkg 3 sandbox): rejects `function*`, generator methods, and any `yield`; live from genesis on testnet/regtest. 8. **Banned rest-pattern check** (consensus-gated, its own [`REST_PATTERN_METER`](../../protocol/flag-days.md) gate, not the Pkg 3 one): rejects a rest pattern in the four positions the metering transform cannot charge, because it charges a rest destructure by wrapping the source expression and these have none: a rest parameter in a function parameter list, a nested rest inside a destructuring pattern, a catch-clause rest, and a rest in a `for-of`/`for-in` loop head. Live from genesis on testnet/regtest, and on mainnet at/after that gate's block time; the `xchain-lint` CLI and the SDK linter enforce it today by default. -9. **Banned WebAssembly check** (consensus-gated, Pkg 3 sandbox): rejects any reference to the global `WebAssembly`; live from genesis on testnet/regtest. +9. **Banned WebAssembly check** (consensus-gated, Pkg 3 sandbox): rejects any reference to the global `WebAssembly`, bare or [global-object-qualified](#global-object-spellings); live from genesis on testnet/regtest. ```mermaid flowchart TD @@ -192,13 +192,17 @@ flowchart TD S9 -->|"clean"| ACCEPT ``` -`vm.checkFloatWarnings(code)` additionally scans for non-integer number literals and returns warnings (non-blocking). +`vm.checkFloatWarnings(code)` additionally scans for non-integer number literals and returns warnings (non-blocking). `lintSource` also returns two advisory warning families that never affect the verdict above: `banned-proto-method` (a call to a prototype method the sandbox neuters, such as `.match()` or `.localeCompare()`, which throws `TypeError` at runtime) and `banned-stripped-global` (a read of a global the sandbox deletes, such as `Date` or `fetch`, which throws `ReferenceError` at runtime); see [Deploy-Time Validation](../../developer-guide/smart-contract-development.md#deploy-time-validation). + +### Global-object spellings + +Checks 4, 6 and 9 match a banned global read through the global object as well as by its bare name: `globalThis.Math.pow(...)`, `globalThis['Promise']` and `` globalThis[`WebAssembly`] `` are rejected exactly like `Math.pow(...)`, `Promise` and `WebAssembly`. Under the [`LINT_GLOBAL_ALIAS_ACTIVATION`](../../protocol/protocol-activation.md#vm-gates-service-carried) gate (`enforceLintGlobalAlias`), two more spellings count as the global object: sloppy-mode `this` (`this.Math.pow(2, 3)`, `this.Promise`) and the self-reference chain at any depth (`globalThis.globalThis.Math.log(x)`). That gate is armed at genesis on BTC, LTC and DOGE mainnet and on testnet and regtest, so every such spelling is rejected at deploy on every network. The `this` match fails closed: a `this.Promise` inside a method of the contract's own object is rejected too, so give such a property a different name. ## Troubleshooting ### isolated-vm won't compile -**Symptoms:** `npm install` fails with C++ compilation errors. +**Symptoms:** `npm install` fails with C++ compilation errors. This happens only where no prebuilt binding matches the platform, so npm falls back to a source build. **Fix:** Install native build tools: ```bash diff --git a/components/wallet/README.md b/components/wallet/README.md index c8e7ef09..72375b47 100644 --- a/components/wallet/README.md +++ b/components/wallet/README.md @@ -7,12 +7,12 @@ xchain-wallet is the reference self-custodial wallet for the XChain Platform. It runs as a browser web app, a Chrome MV3 extension (popup + full-screen + side panel), a desktop application (Windows / macOS / Linux), and a Capacitor mobile app (Android now, iOS later), all from a single React codebase published as a pnpm workspace. The wallet supports Bitcoin, Litecoin, and Dogecoin at launch, with additional chains added as the platform adds them. It consumes [xchain-sdk](../sdk/) as its only data and signing layer and never duplicates SDK functionality. -The wallet implements every XChain feature exposed by the platform: all 31 user-encodable ACTION types, a built-in DEX surface, encrypted messaging (ECIES / ECDH / AES), smart contracts, BTC staking + delegation, classical n-of-m + MuSig2 multisig, cross-chain flows, dispensers, parimutuel betting markets, on-chain governance voting, a `window.xchain` dApp bridge, and air-gapped PSBT signing via animated QR transport. +The wallet implements every XChain feature exposed by the platform: all 32 user-encodable ACTION types, a built-in DEX surface, encrypted messaging (ECIES / ECDH / AES), smart contracts, BTC staking + delegation, classical n-of-m + MuSig2 multisig, cross-chain flows, dispensers, parimutuel betting markets, on-chain governance voting, a `window.xchain` dApp bridge, and air-gapped PSBT signing via animated QR transport. ## Features - **Four shells, one codebase**: `@xchain-wallet/web` (Vite SPA, mobile-responsive), `@xchain-wallet/extension` (Chrome MV3 popup + full-screen + side panel + service worker), `@xchain-wallet/desktop` (Electron, main-process signing isolation), `@xchain-wallet/mobile` (Capacitor wrapper of the built web SPA; Android shipped, iOS later); all share `@xchain-wallet/core` for routes, components, flows, and signers -- **All 31 user-encodable ACTIONs** (every action has an authoring form; `PROTOCOL_ONLY_ACTIONS` is empty): ADDRESS, AIRDROP, BATCH, BET, BROADCAST, CALLBACK, COINPAY, COLLECT, DELEGATE, DEPLOY, DEPOSIT, DESTROY, DISPENSER, DIVIDEND, EXECUTE, FILE, ISSUE, LINK, LIST, MESSAGE, MINT, ORDER, PRICE, SEND, SLEEP, STAKE, SWAP, SWEEP, UNSTAKE, VOTE, WITHDRAW. COLLECT, DEPLOY, and EXECUTE are BTC-only; the other 28 are offered on all three chains +- **All 32 user-encodable ACTIONs** (every action has an authoring form; `PROTOCOL_ONLY_ACTIONS` is empty): ADDRESS, AIRDROP, BATCH, BET, BROADCAST, CALLBACK, COINPAY, COLLECT, DELEGATE, DEPLOY, DEPOSIT, DESTROY, DISPENSER, DIVIDEND, EXECUTE, FILE, ISSUE, LINK, LIST, MESSAGE, MINT, ORDER, PRICE, SEND, SLEEP, STAKE, SWAP, SWEEP, UNSTAKE, VOTE, WITHDRAW, XBRIDGE. COLLECT is BTC-only; the other 31 are offered on all three chains - **Self-custodial key management**: BIP39 mnemonic + optional 25th-word passphrase (captured once at setup and stored encrypted alongside the mnemonic), BIP32 HD derivation per chain, AES-256-GCM vault encrypted with an Argon2id-derived master key (calibrated per device), Counterwallet legacy mnemonic import - **Pluggable signer interface**: four concrete signers, `SoftwareSigner` (in-vault keys), `TrezorSigner` (Trezor Connect), `LedgerSigner` (WebHID), and `RemoteSigner` (cross-shell pairing). Multisig (classical n-of-m + MuSig2) is orchestrated by flows over these signers rather than by a signer of its own; a dedicated `MultisigSigner` is planned but not implemented - **Token issuance suite**: issue / mint / destroy / distribute / dividend / dispenser / broadcast / airdrop / sweep, with parsed-recipient previews and dry-run review diff --git a/components/wallet/release/extension/test-dapp-runbook.md b/components/wallet/release/extension/test-dapp-runbook.md index 5763192f..2f6280f1 100644 --- a/components/wallet/release/extension/test-dapp-runbook.md +++ b/components/wallet/release/extension/test-dapp-runbook.md @@ -10,7 +10,7 @@ Run this before tagging a release candidate, and after any change that touches t ## Prerequisites -- **Node.js** 22 (22.x LTS); Node 18 fails on the `mariadb` ESM package (`ERR_REQUIRE_ESM`); Node 24 cannot build `isolated-vm`. Node 22 is required. +- **Node.js** 22 (22.x LTS); Node 18 fails on the `mariadb` ESM package (`ERR_REQUIRE_ESM`); Node 24 fails the Node ABI 127 consensus-runtime pin in `xchain-vm`. Node 22 is required. - pnpm at the major the wallet repo pins in its root `package.json` `packageManager` field (`pnpm@11.8.0` as of 2026-08-24). pnpm 9 cannot re-resolve that workspace, so the `pnpm install` below fails on it. - A regtest XChain stack running locally, so the local coin-node endpoints the wallet talks to respond. - A Chromium-family browser: Chrome, Edge, Brave, and Arc all work, since the Manifest V3 contract is the same across them. diff --git a/components/wallet/release/mobile/android-play.md b/components/wallet/release/mobile/android-play.md index eae34678..dc806d5a 100644 --- a/components/wallet/release/mobile/android-play.md +++ b/components/wallet/release/mobile/android-play.md @@ -503,6 +503,12 @@ itself does not exist before API 31) and the Play Store present. A plain matter how long you wait or how many times you re-verify - and that reads exactly like a failure when it is really an absent verdict, never taken. +**The LG K51 (API 30) is not an App Links test venue.** Its `pm` carries no +domain-verification verdict to read, so it cannot prove whether App Links are +verified. Use AVD `xc36play` for that coverage. Keep the handset in the release +matrix only for the two hardware-only checks it can still settle: biometric +unlock and real-camera QR scanning. + A `google_apis_playstore` image carries both, and building one is four commands. Both SDK tools need `JAVA_HOME` set first (on this Mac, `/opt/homebrew/opt/openjdk@21`), or they fail with "Unable to locate a Java @@ -866,9 +872,11 @@ printf %s "" | wc -c > and a QR code. "Send" builds a transaction and asks for confirmation before > broadcasting anything. -If a future review does need funded balances, use a wallet on a public test -network and never a funded mainnet wallet, and rotate it after the review -cycle. Nothing in the current submission needs one. +If a future review needs funded balances, run +`node tools/release/verify-demo-endpoints.mjs`; fund only TBTC when the gate +reports it fundable, confirm the balance appears in the app, never use a funded +mainnet wallet, and rotate the test wallet after review. The current submission +needs no funded wallet. ### Graphics diff --git a/components/wallet/reproducible-builds.md b/components/wallet/reproducible-builds.md index 55fa372e..6b051bc0 100644 --- a/components/wallet/reproducible-builds.md +++ b/components/wallet/reproducible-builds.md @@ -32,7 +32,7 @@ Reproducibility breaks down into two enforceable halves: The audit catches regressions automatically on every commit. The run-twice verification catches subtler drift, such as a build-tool version bump that quietly loses determinism, but it requires a clean Docker host to run. Splitting the work this way makes both halves independently checkable. -## Non-determinism sources addressed across every shell +## Non-determinism sources addressed across desktop, extension, and web - **`pnpm install --frozen-lockfile`** rejects builds against an out-of-sync lockfile. Any dependency-tree change requires a lockfile update and commit before a release tag is cut. - **Pinned Node version**, declared in the toolchain configuration and honored by every reproduction container. Tooling outside the container (a verifier's own local Node or pnpm) does not affect the build. @@ -44,6 +44,12 @@ The audit catches regressions automatically on every commit. The run-twice verif Each shell's section below covers what is specific to it on top of this shared floor. +## iOS + +iOS does not carry the run-twice artifact reproduction claim made for the other release targets. Two exports of the same `.xcarchive` have different SHA-256 hashes, so `RELEASE_HASHES.txt` identifies only the submitted export and is not reproducible evidence of a uniquely determined IPA. After submission, Apple re-signs, thins, and FairPlay-encrypts the delivery for each device. + +For those reasons, run-twice IPA reproduction must not be extended to iOS. The narrower possibility of reproducing the pre-export archive has not yet been established: archive reproducibility remains unproven. + ## Verifying from an arm64 machine Every shell builds in a container pinned to `linux/amd64`, because the release lane runs on an amd64 runner and the pinned base image resolves to amd64 only. On an Apple Silicon Mac or an arm64 Linux box the whole build therefore runs under emulation, and the two emulators that can answer that platform flag do not behave the same way. diff --git a/concepts/actions.md b/concepts/actions.md index 94317a94..34b0b163 100644 --- a/concepts/actions.md +++ b/concepts/actions.md @@ -55,7 +55,7 @@ Invalid ACTIONs are recorded as failed; they are not silently ignored. This make ## The ACTION Set -The platform defines 38 named ACTIONs; 36 ACTION types are decoded from the wire. Of those 36, 31 are user-submittable (available via the SDK) across ten categories; the remaining 5 (ANCHOR, ATTEST, NODEPROOF, ROLLCALL, SLASH) are validator-broadcast or system-synthesized and are not SDK-invocable. XCALL is a related but separate case: it is mirror-injected into the destination chain's index rather than decoded from a wire transaction, so it is not counted among the 36 wire-decoded ACTION types, though it is documented below alongside the validator/system actions since it is also not user-submittable. XBRIDGE is the newest addition and sits outside that 36/31 split the same way: it decodes from the wire for its user-broadcast versions but its settle versions are system-injected, so it is counted separately (see the Cross-Chain section below). Every ACTION is gated by the **indexer's protocol version**, not by a block height: 21 are registered at version `0.1.0` and 17 at `0.2.0`, and all 38 carry an activation block and timestamp of `0` on every network. ROLLCALL and XBRIDGE additionally carry a per-network height gate on top of the version gate above; those gates live in `src/consensus/gates/rollcall_gate.js` and the activation-registry rows `xchain_bridge_activation.XCHAIN_BRIDGE_ACTIVATION` and `token_bridge_activation.TOKEN_BRIDGE_ACTIVATION` rather than in the protocol-version registry. An indexer processes an action once its own version is at least the registered one. Block-height and timestamp flag-days do exist, but they gate *changes in behaviour* to already-live actions (fee rules, validation tightening, hash-preimage ordering), not the arrival of the actions themselves. See [Protocol Activation](../protocol/protocol-activation.md). +The platform defines 38 named ACTIONs; 37 ACTION types are decoded from the wire. Of those 37, 32 are user-submittable (available via the SDK) across eleven categories; the remaining 5 (ANCHOR, ATTEST, NODEPROOF, ROLLCALL, SLASH) are validator-broadcast or system-synthesized and are not SDK-invocable. XCALL is a related but separate case: it is mirror-injected into the destination chain's index rather than decoded from a wire transaction, so it is not counted among the 37 wire-decoded ACTION types, though it is documented below alongside the validator/system actions since it is also not user-submittable. XBRIDGE is the newest addition and sits inside both counts: its user-broadcast versions decode from the wire under one decoder name and the SDK builds them, while its settle versions are system-injected by the indexer rather than broadcast (see the Cross-Chain Bridge section below). Every ACTION is gated by the **indexer's protocol version**, not by a block height: 21 are registered at version `0.1.0` and 17 at `0.2.0`, and all 38 carry an activation block and timestamp of `0` on every network. ROLLCALL and XBRIDGE additionally carry a per-network height gate on top of the version gate above; those gates live in `src/consensus/gates/rollcall_gate.js` and the activation-registry rows `xchain_bridge_activation.XCHAIN_BRIDGE_ACTIVATION` and `token_bridge_activation.TOKEN_BRIDGE_ACTIVATION` rather than in the protocol-version registry. An indexer processes an action once its own version is at least the registered one. Block-height and timestamp flag-days do exist, but they gate *changes in behaviour* to already-live actions (fee rules, validation tightening, hash-preimage ordering), not the arrival of the actions themselves. See [Protocol Activation](../protocol/protocol-activation.md). ### Token Lifecycle @@ -149,7 +149,7 @@ This section contains 2 entries: `PRICE` (user-submittable) and `ATTEST` (includ ### Validator / System Actions -The following actions are not user-submittable and are not in the SDK. They are listed here so readers can find their descriptions when browsing this page. (The full set of non-user-submittable, wire-decoded actions is ANCHOR, ATTEST, NODEPROOF, ROLLCALL, and SLASH; ATTEST is documented in the Oracles section above. XCALL is also not user-submittable, but unlike the other five it is mirror-injected rather than wire-decoded, so it falls outside the 36-ACTION wire-decoded count.) +The following actions are not user-submittable and are not in the SDK. They are listed here so readers can find their descriptions when browsing this page. (The full set of non-user-submittable, wire-decoded actions is ANCHOR, ATTEST, NODEPROOF, ROLLCALL, and SLASH; ATTEST is documented in the Oracles section above. XCALL is also not user-submittable, but unlike the other five it is mirror-injected rather than wire-decoded, so it falls outside the 37-ACTION wire-decoded count.) | ACTION | What it does | |---|---| diff --git a/concepts/cross-chain.md b/concepts/cross-chain.md index 774e7be9..fd23d1f4 100644 --- a/concepts/cross-chain.md +++ b/concepts/cross-chain.md @@ -120,7 +120,7 @@ Adding a new Bitcoin-compatible chain to XChain requires only a configuration fi --- -*See also: [Actions](./actions.md) | [Security Model](./security-model.md) | [SWAP spec](../protocol/actions/swap.md) | [LINK spec](../protocol/actions/link.md) | [Cross-Chain Contract Calls](../protocol/cross-chain-calls.md)* +*See also: [Actions](./actions.md) | [Security Model](./security-model.md) | [Token Bridge](./token-bridge.md) for moving a token itself between chains | [SWAP spec](../protocol/actions/swap.md) | [LINK spec](../protocol/actions/link.md) | [Cross-Chain Contract Calls](../protocol/cross-chain-calls.md)* --- diff --git a/concepts/gas.md b/concepts/gas.md index cb3ab044..4acc2204 100644 --- a/concepts/gas.md +++ b/concepts/gas.md @@ -68,7 +68,7 @@ The fee destination address is the per-network `ADDRESS.FEE_DESTINATION` value f | **Cross-chain call request** | 2,000 | 0.02 | Additional fee on top of action emission for `emit.crossExecute()`; the federation relay work. The call also pre-pays its remote `gasLimit` plus the cross-chain callback ceiling, with no refund of unused remote gas | | **Cross-chain callback ceiling** | 20,000 | 0.2 | Fixed gas ceiling the result/expiry callback runs against on the source chain, pre-paid at `emit.crossExecute()` time | | **Computation** | 1/instruction | None | Metered by isolated-vm; one charge per control-flow point | -| **Controller-guard ceiling** (`VM_GUARD_GAS_CEILING`) | 200,000 | 2.0 | Fixed ceiling the controller guard runs against, reserved and billed against `SOURCE` from the `CONTROLLER_GUARD` flag day. Consensus-critical: it is committed into the ledger and contract hashes, so an indexer whose schedule omits or mistypes it throws at the guard-fee site rather than billing a phantom default | +| **Controller-guard ceiling** (`VM_GUARD_GAS_CEILING`) | 200,000 | 2.0 | Fixed ceiling the controller guard runs against on every chain, billed against `SOURCE` from the `CONTROLLER_GUARD` flag day and, on BTC only, reserved up front (see [Controller-Bound Tokens](../protocol/controller-bound-tokens.md#gas)). Consensus-critical: it is committed into the ledger and contract hashes, so an indexer whose schedule omits or mistypes it throws at the guard-fee site rather than billing a phantom default | > **Indexed `for` loops are charged twice per iteration.** The gas meter injects a control-flow charge at the top of the loop body and a second charge into the update expression, so a `for` loop of N iterations costs `2 × N` computation gas. `while`, `do-while`, `for-in`, and `for-of` loops have no update expression and cost 1 per iteration. Account for the doubled cost when budgeting a gas ceiling for contracts that use indexed `for` loops. diff --git a/concepts/tokens.md b/concepts/tokens.md index 60237056..f840722c 100644 --- a/concepts/tokens.md +++ b/concepts/tokens.md @@ -38,17 +38,19 @@ This allows an issuer to, for example, open a mint window by setting `MINT_START ## Locking Parameters -Any parameter can be **locked**, once locked, it cannot be changed by any subsequent `ISSUE`, even from the token owner. Locks are permanent and irreversible. +Several parameters can be **locked**: once a lock is set, what it covers cannot be changed again, even by the token owner, and the lock itself can never be unset. Locks are permanent and irreversible. -Locks are applied by including a lock flag in the `ISSUE` ACTION. Separate locks exist for: +Locks are applied by including a lock flag in the `ISSUE` ACTION. The lock flags are: -- Supply locks (max supply, mint supply, max mint) -- Description lock -- Allow list / block list locks -- Callback lock -- Owner transfer lock (prevents ownership from ever being transferred) +- Supply locks: `LOCK_MAX_SUPPLY` (the ceiling), `LOCK_MAX_MINT` (the per-mint cap), `LOCK_MINT` (the public `MINT` command) and `LOCK_MINT_SUPPLY` (the issuer's own `MINT_SUPPLY`) +- `LOCK_DESCRIPTION` +- `LOCK_SLEEP` (the owner can never pause the token with `SLEEP`) +- `LOCK_CALLBACK` (the token can never be recalled) +- `LOCK_BRIDGE` (the token's bridge settings) -Locking is a trust mechanism. A token with a locked max supply provably cannot be inflated. A token with a locked allow list provably cannot have its access rules changed. Users and integrations can rely on locked parameters without trusting the issuer. +There is no lock for a token's allow list or block list, for a controller binding, or against transferring ownership, so none of these can be frozen the way a locked parameter is. + +Locking is a trust mechanism. A token with a locked max supply provably cannot have its ceiling raised. Users and integrations can rely on locked parameters without trusting the issuer; for anything without a lock, they are still relying on the owner. ## Supply Management diff --git a/developer-guide/adding-a-blockchain.md b/developer-guide/adding-a-blockchain.md index 9c7c80a5..32d96124 100644 --- a/developer-guide/adding-a-blockchain.md +++ b/developer-guide/adding-a-blockchain.md @@ -91,6 +91,8 @@ confirmations: 6, // default cross-chain attestation depth Each network block carries: +> **BTC testnet4 contract limitation:** Contract deploys on BTC testnet4 cannot be mined at current miner block sizes. Use LTC or DOGE testnet when validating contract support. + - **`net`**: the bitcoinjs-lib network object (`messagePrefix`, `bech32`, `bip32`, `pubKeyHash`, `scriptHash`, `wif`, `dustThreshold`, `minStandardTxNonWitnessSize`, `singleOpReturnPolicy`). The decoder, encoder, diff --git a/developer-guide/smart-contract-development.md b/developer-guide/smart-contract-development.md index 0d451d86..df4202c8 100644 --- a/developer-guide/smart-contract-development.md +++ b/developer-guide/smart-contract-development.md @@ -271,7 +271,7 @@ The VM performs the following checks before deployment: 8. **Banned rest-pattern check** (consensus-gated, its own `REST_PATTERN_METER` gate): a rest pattern is rejected in the four positions the gas-metering transform cannot charge, because it charges a rest destructure by wrapping the source expression and these positions have none. Rewrite each one: a **rest parameter** in a function parameter list (read `arguments.length` or index the parameters instead), a **nested rest** inside a destructuring pattern (destructure in two steps so the rest reads a named binding), a **catch-clause rest** (destructure the caught binding on a following line), and a **rest in a `for-of`/`for-in` loop head** (bind the iteration value and destructure it in the loop body). See the [`REST_PATTERN_METER` flag day](../protocol/flag-days.md). 9. **Banned WebAssembly check** (consensus-gated, Pkg 3 sandbox): any reference to the global `WebAssembly` is rejected. -Checks 7 and 9 are live today on testnet/regtest (the Pkg 3 sandbox gate is unconditionally active on those networks) even though their mainnet activation is a separate flag-day; a regtest deploy that trips either one is rejected now. Check 8 rides its own `REST_PATTERN_METER` gate rather than the Pkg 3 one, and is likewise unconditionally active on testnet/regtest, so the `xchain-lint` CLI, the SDK linter and a regtest deploy all reject an unmeterable rest pattern today. The `VM_LINT_HARDENING` widenings under checks 3, 4, and 6 activate at the same instant as `VM_BANNED_ASYNC` and are default-on for the SDK and CLI already. +Checks 7 and 9 are live today on testnet/regtest (the Pkg 3 sandbox gate is unconditionally active on those networks) even though their mainnet activation is a separate flag-day; a regtest deploy that trips either one is rejected now. Check 8 rides its own `REST_PATTERN_METER` gate rather than the Pkg 3 one, and is likewise unconditionally active on testnet/regtest, so the `xchain-lint` CLI, the SDK linter and a regtest deploy all reject an unmeterable rest pattern today. The `VM_LINT_HARDENING` widenings under checks 3, 4, and 6 activate at the same instant as `VM_BANNED_ASYNC` and are default-on for the SDK and CLI already. Checks 4, 6, and 9 also match the global-object spellings of `Math`, `Promise`, and `WebAssembly` (`globalThis.Math.pow(...)`, and under the genesis-armed `LINT_GLOBAL_ALIAS_ACTIVATION` gate `this.Math.pow(...)` and `globalThis.globalThis.Promise`), so qualifying the name does not get it past the check; see [Global-object spellings](../components/vm/operations.md#global-object-spellings). A non-blocking **float warning** is also generated if decimal number literals are detected in the code. This warning appears in the execution record but does not prevent deployment. @@ -298,6 +298,8 @@ Beyond the deploy-time rules above, the linter adds **logic-level** checks. None - **`contract-meta`**: a source whose export object provably carries no `meta`, or whose literal `meta.name` / `meta.description` fails the byte grammar, is refused **before** the transaction is built, with the same consensus string the chain would have answered. The check is a static read of the source, so a *computed* `meta` field is undecidable and downgrades to an advisory rather than blocking (see [Contract identity](#contract-identity) for why literals are worth it). The foundry's `runGate` carries the same rule under the same id. - **`crossCallable` integrity**: a *non-array* `crossCallable` makes **every** cross-chain call to your contract fail at runtime (`XCALL_NOT_CALLABLE`). This is reported as a linter **error**: it fails `xchain-lint` and, by default, `sdk.deploy` (`{ lint: 'block' }`), even though the chain itself accepts the contract. A `crossCallable` entry that names no exported method is a **warning** (likely a typo; that method stays uncallable cross-chain). - **Warnings** (advisory, never block): structurally unbounded loops, bulk allocations, a `state.get(...)` result dereferenced without a null guard, and methods that read call inputs without any `require()` validation. +- **`banned-proto-method`** (warning): a call to a prototype method the sandbox removes: `match`, `matchAll`, `search` (they coerce their argument to a RegExp, so catastrophic backtracking would run for almost no gas), and `normalize`, `localeCompare`, `toLocaleLowerCase`, `toLocaleUpperCase`, `toLocaleString` (their output depends on the host's ICU build). The call throws `TypeError` the first time it runs. It is a warning rather than a deploy rule because the match is by method name alone, so the linter cannot tell `someString.search(x)` from your own `myIndex.search(x)`; rename your own method if the warning is a false positive. +- **`banned-stripped-global`** (warning): a read of a global the sandbox deletes so every validator computes the same result: `Date`, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `WeakRef`, `FinalizationRegistry`, `Proxy`, `Reflect`, `fetch`, `XMLHttpRequest`, `WebSocket`, `SharedArrayBuffer`, `Atomics`, `queueMicrotask`, `BigInt`, `Intl`, `Temporal`, `structuredClone`, and `performance`, bare or [global-object-qualified](../components/vm/operations.md#global-object-spellings). The contract deploys but throws `ReferenceError` the first time that line runs. Use the `xchain.*` gateway instead: `xchain.attestation.request` to read a URL, `xchain.getBlockTimestamp()` for time, `xchain.math.*` for big numbers. `Promise` and `WebAssembly` are stripped too but are reported by the error-severity `banned-async` and `banned-wasm` checks above instead. `sdk.deploy(params, encoder, { lint })` runs this automatically. The default `lint: 'block'` **throws before building the transaction** if the contract has errors, so a guaranteed-to-fail deploy never reaches the chain. Pass `lint: 'warn'` to log and proceed, or `lint: 'off'` to skip. Chunked deploys (`sdk.deployContract`) lint the fully-assembled source once, before chunking. diff --git a/developer-guide/testing.md b/developer-guide/testing.md index 2c9880bc..875cbf92 100644 --- a/developer-guide/testing.md +++ b/developer-guide/testing.md @@ -173,7 +173,7 @@ All XChain Platform tests use the following infrastructure: | **Toxiproxy** | Network fault injection for chaos tests (explorer) | | **Docker Compose** | Test environment orchestration (integration, chaos, E2E) | -All suites require **Node 22 exactly**. Node 18 fails outright; Node 24 cannot build the `isolated-vm` native module, and on some suites an unsupported Node silently skips tests rather than failing, which reads as a false green. +All suites require **Node 22 exactly**. Node 18 fails outright; Node 24 fails the Node ABI 127 consensus-runtime pin in `xchain-vm`, and on some suites an unsupported Node silently skips tests rather than failing, which reads as a false green. ### Running Tests diff --git a/getting-started/key-terms.md b/getting-started/key-terms.md index 312179d4..ee65d5c5 100644 --- a/getting-started/key-terms.md +++ b/getting-started/key-terms.md @@ -55,7 +55,7 @@ A reference glossary of XChain terminology, organized by category. **xchain-regtest-miner**: A service that automatically mines pending mempool transactions in regtest environments, producing instant block confirmations for development and testing. -**xchain-sdk**: The developer SDK for the XChain platform. Provides methods for all 31 developer-invocable actions, 100+ explorer query methods, a batch builder, PSBT generation, and typed error classes. +**xchain-sdk**: The developer SDK for the XChain platform. Provides methods for all 32 developer-invocable actions, 100+ explorer query methods, a batch builder, PSBT generation, and typed error classes. **xchain-utxo-tracker**: A service that indexes all UTXOs from the coin node into LevelDB. Used by the encoder to look up available UTXOs for a given address. diff --git a/getting-started/quickstart-developer.md b/getting-started/quickstart-developer.md index 463b82eb..3d639cf2 100644 --- a/getting-started/quickstart-developer.md +++ b/getting-started/quickstart-developer.md @@ -7,7 +7,7 @@ This guide walks you through creating your first XChain token using the SDK. You ## Prerequisites -- **Node.js** 22 (22.x LTS); Node 18 fails on the `mariadb` ESM package (`ERR_REQUIRE_ESM`); Node 24 cannot build `isolated-vm`. Node 22 is required. +- **Node.js** 22 (22.x LTS); Node 18 fails on the `mariadb` ESM package (`ERR_REQUIRE_ESM`); Node 24 fails the Node ABI 127 consensus-runtime pin in `xchain-vm`. Node 22 is required. - A running XChain platform (either local via [regtest](../developer-guide/regtest-development.md) or a public node) - A Bitcoin/Litecoin/Dogecoin wallet with a funded address (for mainnet/testnet), or use regtest for free development @@ -259,9 +259,9 @@ See [Regtest Development](../developer-guide/regtest-development.md) for setup i | JSON-RPC microservice | `npm run api` in xchain-sdk | Any language via HTTP | | Browser bundle | `dist/xchain_sdk.min.js` | Client-side web apps | -### All 31 User-Submittable Actions Are Covered +### All 32 User-Submittable Actions Are Covered -The SDK covers all 31 user-submittable actions. Thirty of them have convenience methods: `sdk.issue()`, `sdk.mint()`, `sdk.send()`, `sdk.sweep()`, `sdk.airdrop()`, `sdk.dividend()`, `sdk.order()`, `sdk.coinpay()`, `sdk.dispenser()`, `sdk.swap()`, `sdk.broadcast()`, `sdk.message()`, `sdk.file()`, `sdk.address()`, `sdk.link()`, `sdk.list()`, `sdk.sleep()`, `sdk.callback()`, `sdk.destroy()`, `sdk.price()`, `sdk.bet()`, `sdk.stake()`, `sdk.unstake()`, `sdk.delegate()`, `sdk.collect()`, `sdk.deploy()`, `sdk.execute()`, `sdk.deposit()`, `sdk.withdraw()`, `sdk.vote()`. `sdk.transfer()` is an alias for `sdk.send()`. The thirty-first, BATCH, has no convenience method: `sdk.batch()` returns a builder for composing BATCH actions. The 6 remaining actions (ANCHOR, ATTEST, NODEPROOF, ROLLCALL, SLASH, and XCALL) are validator-broadcast, VM-emitted, or permissionless-proof actions and are not user-submittable; see [concepts/ACTIONS.md](../concepts/actions.md) for the full taxonomy. +The SDK covers all 32 user-submittable actions. Thirty-one of them have convenience methods: `sdk.issue()`, `sdk.mint()`, `sdk.send()`, `sdk.sweep()`, `sdk.airdrop()`, `sdk.dividend()`, `sdk.order()`, `sdk.coinpay()`, `sdk.dispenser()`, `sdk.swap()`, `sdk.broadcast()`, `sdk.message()`, `sdk.file()`, `sdk.address()`, `sdk.link()`, `sdk.list()`, `sdk.sleep()`, `sdk.callback()`, `sdk.destroy()`, `sdk.price()`, `sdk.bet()`, `sdk.stake()`, `sdk.unstake()`, `sdk.delegate()`, `sdk.collect()`, `sdk.deploy()`, `sdk.execute()`, `sdk.deposit()`, `sdk.withdraw()`, `sdk.vote()`, `sdk.xbridge()`. `sdk.transfer()` is an alias for `sdk.send()`. The thirty-second, BATCH, has no convenience method: `sdk.batch()` returns a builder for composing BATCH actions. The 6 remaining actions (ANCHOR, ATTEST, NODEPROOF, ROLLCALL, SLASH, and XCALL) are validator-broadcast, VM-emitted, or permissionless-proof actions and are not user-submittable; see [concepts/ACTIONS.md](../concepts/actions.md) for the full taxonomy. --- diff --git a/getting-started/quickstart-node-operator.md b/getting-started/quickstart-node-operator.md index 00444d1f..ae80c52d 100644 --- a/getting-started/quickstart-node-operator.md +++ b/getting-started/quickstart-node-operator.md @@ -8,7 +8,7 @@ This guide walks you through installing and running the full XChain platform sta ## Prerequisites - **Docker** (Engine 20.10+) and **Docker Compose**, all XChain services run as Docker containers -- **Node.js** 22 (22.x LTS), required to run the `xchain-node` CLI. Node 18 fails on the `mariadb` ESM package (`ERR_REQUIRE_ESM`); Node 24 cannot build `isolated-vm`. Node 22 is required. +- **Node.js** 22 (22.x LTS), required to run the `xchain-node` CLI. Node 18 fails on the `mariadb` ESM package (`ERR_REQUIRE_ESM`); Node 24 fails the Node ABI 127 consensus-runtime pin in `xchain-vm`. Node 22 is required. - **Disk space**: blockchain data is large; plan for at least 600 GB for Bitcoin mainnet, or use testnet/regtest for development - **Internet access**: the installer downloads service images and blockchain binaries from GitHub diff --git a/getting-started/quickstart-validator.md b/getting-started/quickstart-validator.md index 8a4dcd14..6b027421 100644 --- a/getting-started/quickstart-validator.md +++ b/getting-started/quickstart-validator.md @@ -68,6 +68,10 @@ somewhere off this machine now.** There is no recovery if they are lost. - Send **0.02 testnet DOGE** to the second address, from any Dogecoin testnet faucet. +The BTC testnet4 funding above supports validator setup. Contract deploys on +BTC testnet4 cannot be mined at current miner block sizes; use +LTC or DOGE testnet for contract deployment and testing. + ## Step 4: stake ```bash diff --git a/getting-started/running-a-validator.md b/getting-started/running-a-validator.md index 8b623815..33fa8a01 100644 --- a/getting-started/running-a-validator.md +++ b/getting-started/running-a-validator.md @@ -14,7 +14,8 @@ There are two ways to run one, and the cheaper one is a first-class citizen rath | Runs a coin node (`bitcoind` and friends) | No | Yes | | Runs its own decoder + indexer | No, replicates them | Yes | | Gets chain data from | `xchain-sync` replication | Its own node, decoded locally | -| Can claim `price`, `attestation`, `cross_chain`, `oracle_publish` | Yes | Yes | +| Can claim `price`, `attestation`, `oracle_publish` | Yes | Yes | +| Can claim `cross_chain` | No (its self-test needs a BTC coin-node RPC) | Yes | | Can claim `full_node` | No | Yes | | Base oracle reward | Yes | Yes | | Full-node reward tranche | No | Yes, once armed | @@ -44,7 +45,7 @@ indexer of your own. Note what is *not* on that list: a coin node. The protocol assigns no tiers by decree. Capabilities qualify automatically when your total effective stake clears each capability's floor, and most capabilities never touch a coin node at all. `price` needs a price feed. `attestation` needs a reachable model provider. `cross_chain` verifies source actions against indexer APIs. `oracle_publish` needs a broadcast wallet. -Only one capability requires a coin node, and it is the one named after it. +Two capabilities require a coin node. `full_node` is named after it. `cross_chain` matches against the BTC indexer, but the hub's self-test for it requires `cross_chain.chains.BTC.rpc`, the same setting the hub reads as its BTC coin-node RPC for `full_node`. A validator without the BTC stack lists `cross_chain` under `DISABLED_CAPABILITIES` (see [Step 6](../operations/run-a-validator.md#step-6-decide-your-capabilities)). ## Capabilities and their stake floors @@ -56,7 +57,7 @@ Capabilities are not applied for. Any key whose total effective stake clears a f | `price` | 1,000 | Signs price rounds | No | | `attestation` | 1,000 | Attests to off-chain facts | No | | `full_node` | 2,000 | Proves possession of the chain | **Yes** | -| `cross_chain` | 5,000 | Matches actions across chains | No | +| `cross_chain` | 5,000 | Matches actions across chains | **Yes** (its self-test RPC) | ## How the two tiers get their chain data diff --git a/getting-started/what-is-xchain.md b/getting-started/what-is-xchain.md index 24b0e138..4f180951 100644 --- a/getting-started/what-is-xchain.md +++ b/getting-started/what-is-xchain.md @@ -35,7 +35,7 @@ In practice, XChain works by embedding small pieces of data inside ordinary bloc ## What Can You Do with XChain? -XChain is built around 35 commands (called **ACTIONs**) that cover the full lifecycle of a digital asset ecosystem. +XChain is built around 37 commands (called **ACTIONs**) that cover the full lifecycle of a digital asset ecosystem. ### Create and Manage Tokens @@ -105,7 +105,7 @@ XChain supports **staking** for hub validation. Validators stake XCHAIN tokens t ### Validator and System Actions -Five of the 36 ACTIONs are written by the validator federation or synthesized by the indexer. They are not user-broadcast and are not accessible through the SDK, but they appear on-chain and in the explorer, so it is worth knowing what they do. +Five of the 37 ACTIONs are written by the validator federation or synthesized by the indexer. They are not user-broadcast and are not accessible through the SDK, but they appear on-chain and in the explorer, so it is worth knowing what they do. - **ANCHOR** is written by validators to commit a quorum-signed state checkpoint to the anchor chain (Dogecoin on all networks). It records the per-block ledger, action, and contract hash triple so light clients can verify indexer state against a threshold of validator signatures without trusting any single operator. Later versions of ANCHOR also archive cross-chain match records and SPV light-client roots, making the full platform state reconstructible from chain data alone. - **ATTEST** is written by validators when they answer a smart contract's request for outside-world data (an HTTP call, an LLM query, etc.). Validators fetch the answer independently, reach quorum, and broadcast the signed response back on-chain; a system-synthesized expiry version is written by the indexer if the deadline passes before quorum is reached. @@ -113,7 +113,7 @@ Five of the 36 ACTIONs are written by the validator federation or synthesized by - **ROLLCALL** is a liveness roll call published on Dogecoin. Validators sign a message bound to a Bitcoin epoch block's ledger hash, which cannot be signed before that block is mined, so a signature proves the validator was actually running at the time. Any number of roll calls may land per epoch from anyone and the present set is their union, so no publisher can leave a rival out. The Bitcoin indexer closes each epoch and evicts a validator that has been absent for two consecutive epochs: its stake is deactivated and refunded after the normal cooldown, never burned, because being offline is not an offense. - **SLASH** is submitted by anyone who catches a validator signing two conflicting values for the same consensus slot (equivocation). When the proof is valid, the offending validator's entire capability bond is burned automatically. The submitter receives a governance-configured bounty. -XCALL, the platform's cross-chain contract call, works differently: it's emitted by the VM when a smart contract calls `emit.crossExecute(...)` to invoke a contract on a different chain, then mirror-injected into the destination chain's index by the validator federation rather than decoded from a wire transaction, so it isn't counted among the 36 wire-decoded ACTIONs above. The validator federation relays the call and delivers the result back through a callback on the originating chain; a system-synthesized version is written by the indexer if the deadline passes before a result arrives. +XCALL, the platform's cross-chain contract call, works differently: it's emitted by the VM when a smart contract calls `emit.crossExecute(...)` to invoke a contract on a different chain, then mirror-injected into the destination chain's index by the validator federation rather than decoded from a wire transaction, so it isn't counted among the 37 wire-decoded ACTIONs above. The validator federation relays the call and delivers the result back through a callback on the originating chain; a system-synthesized version is written by the indexer if the deadline passes before a result arrives. --- @@ -133,7 +133,7 @@ A lot of layer-2 and sidechain systems require you to trust a separate set of va ### Multi-Chain by Design -XChain runs natively on every supported chain simultaneously, which today means Bitcoin, Litecoin, and Dogecoin. A token on one chain is distinct from a token on another chain; they have separate ledgers. But the XChain software supports all three chains with the same protocol, the same 36 actions, and the same tooling. A single deployment of the platform can index and serve data for all three chains at once. +XChain runs natively on every supported chain simultaneously, which today means Bitcoin, Litecoin, and Dogecoin. A token on one chain is distinct from a token on another chain; they have separate ledgers. But the XChain software supports all three chains with the same protocol, the same 37 actions, and the same tooling. A single deployment of the platform can index and serve data for all three chains at once. ### AI-Callable Smart Contracts @@ -145,14 +145,15 @@ The XChain platform is open source software. Anyone can run their own XChain nod --- -## The 36 ACTIONs: The Building Blocks +## The 37 ACTIONs: The Building Blocks -Every operation on XChain is expressed as one of 36 ACTION commands. Think of them as the vocabulary of the protocol; a complete set of verbs for working with digital assets. +Every operation on XChain is expressed as one of 37 ACTION commands. Think of them as the vocabulary of the protocol; a complete set of verbs for working with digital assets. | Category | ACTIONs | |---|---| | Token lifecycle | ISSUE, MINT, DESTROY, CALLBACK, SLEEP | | Transfers | SEND, SWEEP, AIRDROP, DIVIDEND | +| Cross-chain | XBRIDGE | | Trading | ORDER, COINPAY, DISPENSER, SWAP | | Smart contracts | DEPLOY, EXECUTE, DEPOSIT, WITHDRAW | | Outside-world data | PRICE, ATTEST | @@ -161,7 +162,7 @@ Every operation on XChain is expressed as one of 36 ACTION commands. Think of th | Configuration | ADDRESS, BATCH, LINK, LIST | | Governance | VOTE | | Betting | BET | -| Validator / system | ANCHOR, NODEPROOF, SLASH | +| Validator / system | ANCHOR, NODEPROOF, ROLLCALL, SLASH | Each ACTION has a versioned format, as the protocol evolves and adds new fields, old versions remain valid so that existing software doesn't break. @@ -181,7 +182,7 @@ XCHAIN is itself just a token on XChain, issued via `ISSUE` by a designated addr ### Developers Building Token Platforms -XChain provides a complete SDK (`xchain-sdk`) with methods for all 31 of the 36 actions that are developer-invocable, 100+ explorer queries, smart contract deployment and execution, a batch builder, live WebSocket event streaming, and PSBT generation. If you want to build a token platform, a DEX, an NFT marketplace, a DeFi protocol with smart contracts, or any application involving digital assets on Bitcoin-family chains, XChain gives you the full stack. +XChain provides a complete SDK (`xchain-sdk`) with methods for all 32 of the 37 actions that are developer-invocable, 100+ explorer queries, smart contract deployment and execution, a batch builder, live WebSocket event streaming, and PSBT generation. If you want to build a token platform, a DEX, an NFT marketplace, a DeFi protocol with smart contracts, or any application involving digital assets on Bitcoin-family chains, XChain gives you the full stack. ### Organizations Wanting Private Deployments diff --git a/legal/README.md b/legal/README.md index 7c035603..6b2c745b 100644 --- a/legal/README.md +++ b/legal/README.md @@ -3,7 +3,7 @@ # Legal -XChain Platform is **open source**, dual-licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0-or-later) with a separate **commercial license** for proprietary use. +XChain Platform is **open source**, dual-licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0-or-later) with a separate **commercial license** for proprietary use. The one carve-out is the `xchain-contracts` template library, which is **MIT-licensed** because it exists to be copied into user contracts (see [NOTICE](../NOTICE.md)). | Document | What it covers | |---|---| diff --git a/legal/commercial-license.md b/legal/commercial-license.md index 29e1c25e..24c212e2 100644 --- a/legal/commercial-license.md +++ b/legal/commercial-license.md @@ -10,6 +10,8 @@ XChain Platform is dual-licensed: You may use XChain Platform under **either** license. This page explains when you need the commercial one. +The `xchain-contracts` template library is carved out of that dual license: it is **MIT-licensed**, because it exists to be copied into user contracts (see [NOTICE](../NOTICE.md)). + --- ## Do I need a commercial license? @@ -19,6 +21,7 @@ You may use XChain Platform under **either** license. This page explains when yo - You use XChain for personal, hobby, research, or educational purposes. - You run XChain **unmodified**, for any purpose, including inside a company. - You modify XChain and you are willing to **release your modified source code** to everyone who interacts with it over a network, under the AGPL. +- You copy a template from the MIT-licensed `xchain-contracts` library into your own contract, closed-source or not. **You DO need a commercial license** if: diff --git a/legal/licensing.md b/legal/licensing.md index ac7ef2bb..1073115a 100644 --- a/legal/licensing.md +++ b/legal/licensing.md @@ -10,6 +10,8 @@ XChain Platform is **open source**, dual-licensed under: You may use XChain under **either** license, whichever fits you. +One first-party component is carved out of that dual license: the [`xchain-contracts`](https://github.com/XChain-Platform/xchain-contracts/) template library is licensed under the **MIT License**, because it exists to be copied into user contracts. Its terms are in that repository's own `LICENSE` file and are not affected by the AGPL's source-disclosure requirements (see [NOTICE](../NOTICE.md)). + --- ## The short version @@ -21,6 +23,7 @@ You may use XChain under **either** license, whichever fits you. | Modify XChain and **share your changes** under AGPL | AGPL-3.0 | **Free** | | Modify XChain and want to keep your changes **private/proprietary** | Commercial | **Paid** | | Embed XChain in a closed-source product | Commercial | **Paid** | +| Copy an `xchain-contracts` template into your own contract, including a closed-source one | MIT | **Free** | --- @@ -61,6 +64,9 @@ No. That's free under the AGPL. **We want to fork XChain, add proprietary features, and run it as our own private service. Is that free?** You can, under the AGPL, *if* you publish your modified source to your users. If you want to keep those modifications private, you need a [commercial license](./commercial-license.md). +**I copied a contract template from `xchain-contracts` into a proprietary product. Do I need a commercial license?** +No. That repository is MIT-licensed, so keep the copyright and permission notice from its `LICENSE` file and the AGPL's source-disclosure requirement does not apply to what you copied. The rest of the platform stays under the AGPL-or-commercial terms above. + **Can I call my deployment "XChain"?** The software license lets you run the code; it does not grant rights to the **XChain name or brand**. See [TRADEMARK.md](./trademark.md). diff --git a/lib/env-var-doc-coverage.js b/lib/env-var-doc-coverage.js index 1ef240f7..8e86b118 100644 --- a/lib/env-var-doc-coverage.js +++ b/lib/env-var-doc-coverage.js @@ -1233,7 +1233,19 @@ const COMPUTED_READ_BASELINE = { // Measured 2026-08-11 against the committed trees of all 11 gated // components: 95 sites in 37 files across 10 of them. decoder: 4, encoder: 4, explorer: 6, hub: 5, indexer: 8, node: 5, - 'regtest-miner': 7, sdk: 4, sync: 10, 'utxo-tracker': 7, vm: 0, + 'regtest-miner': 4, sdk: 4, sync: 8, 'utxo-tracker': 7, vm: 0, + // sync 10 -> 8 on 2026-09-20, re-measured at xchain-sync origin/develop 8fb8e4d5 by the + // gate itself: two computed reads became named reads since the 2026-09-15 count, so the + // ratchet drops to the eight remaining sites (src/api.js:72, src/coins/index.js:120, src/coins/index.js:182, src/coins/index.js:202, src/coins/index.js:399, src/config.js:146, src/config.js:171, src/config.js:259). Lowered because the + // blind spot shrank, the only direction this table may move. + // regtest-miner 7 -> 4 on 2026-09-18, re-measured against xchain-regtest-miner + // origin/develop 02a8ac9: the API split moved src/api.js's three computed reads (the + // REQUIRED_ENV_VARS presence and trim pair at :127 and the parseInt at :134) onto named + // reads, leaving only the four coin-registry sites (src/coins/index.js:120, :182, :202, + // :399). Lowered because the blind spot shrank, which is the only direction this ratchet + // may move. The stale 7 was measured against a local checkout that origin had moved past, + // so every venue run reddened for every pusher while a local run stayed green: measure this + // table at the sibling's origin ref, never at whatever a working checkout happens to hold. // indexer 7 -> 8 on 2026-09-17, re-derived against the v0.20.0 landing set (xchain-indexer // daf70507 plus the edit that reads the witness lever by name): the mirror-admission barrier // family (xchain-indexer 8c50c9d4) added bin/verify-mirror-admission-replay-equivalence.js, diff --git a/lib/indexer-source.js b/lib/indexer-source.js index 0c541956..3e86fd93 100644 --- a/lib/indexer-source.js +++ b/lib/indexer-source.js @@ -134,4 +134,4 @@ function locatedModuleSource(entry) { return { text: parts.map((p) => p.text).join('\n'), parts, where }; } -module.exports = { moduleEntry, moduleExists, modulePaths, readModuleSource, locatedModuleSource }; +module.exports = { moduleEntry, moduleExists, readModuleSource, locatedModuleSource }; diff --git a/operations/deployment.md b/operations/deployment.md index 039f6b4a..13507fbb 100644 --- a/operations/deployment.md +++ b/operations/deployment.md @@ -12,7 +12,7 @@ This guide covers deploying the XChain Platform from scratch, from a single-chai ### Software - **Docker**: Engine 20.10 or later. Docker must be accessible to the current user (add user to `docker` group or run as root). -- **Node.js**: 22 (22.x LTS) exactly, required to run the xchain-node CLI. Node 18 and earlier fail on the ESM-only `mariadb` driver (`ERR_REQUIRE_ESM`); Node 24 cannot build the native `isolated-vm` module the indexer and explorer pull in. +- **Node.js**: 22 (22.x LTS) exactly, required to run the xchain-node CLI. Node 18 and earlier fail on the ESM-only `mariadb` driver (`ERR_REQUIRE_ESM`); Node 24 fails the Node ABI 127 consensus-runtime pin in the `xchain-vm` the indexer and explorer pull in. - **Git**: to clone xchain-node. Verify Docker is working before proceeding: diff --git a/operations/disk-management.md b/operations/disk-management.md index ab794aaa..e852a946 100644 --- a/operations/disk-management.md +++ b/operations/disk-management.md @@ -24,6 +24,10 @@ behaviour, inherited by both forks: | LTC testnet | `testnet4/blocks/` | | DOGE / LTC regtest | `regtest/blocks/` | +The `testnet4/` path above is Litecoin's testnet directory. BTC testnet4 is a +separate network, where contract deploys cannot be mined at current miner block +sizes. Use LTC or DOGE testnet for contract deployment and testing. + Any disk-offload approach has to account for the network subdirectory. The two safe options below do; the anti-pattern does not. diff --git a/operations/release-process.md b/operations/release-process.md index 8e0a5eb5..67a3f550 100644 --- a/operations/release-process.md +++ b/operations/release-process.md @@ -10,17 +10,23 @@ This is the runbook. It assumes you are the person driving the release. ## The shape of it -XChain Platform ships as a **release train**: nine components that move together +XChain Platform ships as a **release train**: thirteen components that move together under one version number, so that "XChain 0.9.0" names an exact, reproducible set of software rather than a rough era. | | | |---|---| -| **Train members** | `xchain-vm`, `xchain-decoder`, `xchain-indexer`, `xchain-hub`, `xchain-sync`, `xchain-node`, `xchain-encoder`, `xchain-utxo-tracker`, `xchain-explorer` | +| **Train members** | `xchain-vm`, `xchain-decoder`, `xchain-indexer`, `xchain-hub`, `xchain-sync`, `xchain-node`, `xchain-encoder`, `xchain-utxo-tracker`, `xchain-explorer`, `xchain-sdk`, `xchain-e2e-test`, `xchain-regtest-miner`, `xchain-contracts` | | **Version scheme** | one stream, `MAJOR.MINOR.PATCH`, shared by every member | | **Where work lands** | the `develop` branch of each repo | | **Where releases live** | the `master` branch, which only ever receives release merges | +The members are the components the release manifest in `xchain-node` names, plus +`xchain-node` itself. The repos a given cut freezes, merges and tags are those +members minus any the train leaves unchanged (see Sparse lockstep below), plus +`xchain-documentation`, which is tagged with the train but carries no component +version of its own. + ### Sparse lockstep A component is tagged only for the trains it actually changes in. A gap in a diff --git a/operations/releases.md b/operations/releases.md index dad45364..ab24b939 100644 --- a/operations/releases.md +++ b/operations/releases.md @@ -21,7 +21,10 @@ block cannot hold an indexer behind an unbounded time barrier. The hub emits the messages and bootstrap pages, the indexer enforces the same rule in live processing and in replay, and the hub mirror schema moves from 6 to 7. Because the barriers change what a node derives from a block, the manifest is classified major and carries a `trainActivation` block -arming the 0.20.0 rule set on testnet at Bitcoin height 153116. +that armed the 0.20.0 rule set on testnet at Bitcoin height 153116 at cut. Testnet's live tip +outran that boundary twice before the fleet rolled to it, so an in-flight v0.20.1 patch has +re-slid it (see below); v0.20.0 itself, its manifest and every non-height behavior described +here, is unchanged. | Component | Version | |---|---| @@ -39,16 +42,25 @@ arming the 0.20.0 rule set on testnet at Bitcoin height 153116. | xchain-contracts | 0.17.0 (unchanged) | | xchain-regtest-miner | 0.20.0 | -**Every testnet node must run v0.20.0 before Bitcoin testnet height 153116**, the train -boundary: a node without this rule set halts at that boundary instead of processing a block -under the older rules, and below it the new binary runs the old rules, which is the -rolling-upgrade window. The barrier family arms per chain on testnet in two stages. The mirror -admission producer (`MIRROR_ADMISSION_ACTIVATION`) arms at BTC 153222, LTC 4891504 and DOGE -67911796; the consumer (`MIRROR_ADMISSION_CONSUMER_ACTIVATION`) arms later at BTC 153266, LTC -4891766 and DOGE 67912575, so writers publish admission metadata before readers require it. The -anchor-attestation completeness barrier (`ANCHOR_ATTEST_BARRIER_ACTIVATION`) arms at the Bitcoin -consumer height, 153266, and uses the same bounded view of admitted rows. Mainnet stays unarmed -on every row, as do token-bridge ISSUE and policy inheritance on every network. +**At v0.20.0's 2026-09-18 cut, every testnet node had to run it before Bitcoin testnet height +153116**, the train boundary: a node without this rule set halts at that boundary instead of +processing a block under the older rules, and below it the new binary runs the old rules, which +is the rolling-upgrade window. The barrier family armed per chain on testnet in two stages. The +mirror admission producer (`MIRROR_ADMISSION_ACTIVATION`) armed at BTC 153222, LTC 4891504 and +DOGE 67911796; the consumer (`MIRROR_ADMISSION_CONSUMER_ACTIVATION`) armed later at BTC 153266, +LTC 4891766 and DOGE 67912575, so writers publish admission metadata before readers require it. +The anchor-attestation completeness barrier (`ANCHOR_ATTEST_BARRIER_ACTIVATION`) armed at the +Bitcoin consumer height, 153266, and used the same bounded view of admitted rows. + +**v0.20.1 arms (currently in flight, not yet released): train boundary Bitcoin testnet height +154074**, re-slid twice (2026-09-19, then again 2026-09-23 to a 40-hour margin at each coin's +measured cadence) after the live testnet tip outran the boundary v0.20.0 shipped, before the +fleet ever rolled to it. The mirror admission producer now arms at BTC 154234 and DOGE 67936053; +the consumer now arms later at BTC 154291 and DOGE 67936888. LTC:testnet, which v0.20.0 shipped +at producer 4891504 / consumer 4891766, ships null under both re-slides (disabled for v0.20.1, 2026-09-18) and +arms on a later train instead. The anchor-attestation completeness barrier now arms at the +Bitcoin consumer height, 154291. Mainnet stays unarmed on every row in both v0.20.0 and v0.20.1, +as do token-bridge ISSUE and policy inheritance on every network. Roll the hub first and apply the admission-height migration before rolling mirror readers: seven mirror tables gain admission-height columns, including the lifecycle and attestation data the diff --git a/operations/run-a-validator.md b/operations/run-a-validator.md index 8b8bd6b4..cb44012c 100644 --- a/operations/run-a-validator.md +++ b/operations/run-a-validator.md @@ -156,6 +156,10 @@ So a first-time stake from zero (three mints plus the stake) runs about XCHAIN action on testnet is a Bitcoin transaction, so this is also the balance to top up if you ever want to change your stake. +This BTC testnet4 path is suitable for validator setup, but contract deploys on +BTC testnet4 cannot be mined at current miner block sizes. Use +LTC or DOGE testnet for contract deployment and testing. + **DOGE address** (TDOGE): send testnet dogecoin. This is the wallet your hub **spends from** when it is the elected publisher for a price round or a state anchor, which is what the `oracle_publish` capability means. Qualifying for @@ -288,6 +292,32 @@ replica that has fallen behind makes your epoch close wait longer rather than judge the roll call on stale data. If you run a Dogecoin indexer of your own on the same network, point at it instead. +### Wire each bridge destination to its origin indexer + +Bridge crediting has a separate directional read from the DOGE-specific +ROLLCALL read above. Every destination indexer crediting a bridged transfer +must have the origin chain's indexer API URL configured as +`_INDEXER_URL` or `_INDEXER_API_URL`. The `_API_URL` name takes +precedence. Configure both directions when transfers can travel both ways. +For example, the DOGE indexer receiving from BTC needs `BTC_INDEXER_API_URL`, +and the BTC indexer receiving from DOGE needs `DOGE_INDEXER_API_URL`: + +``` +# On the DOGE destination indexer +BTC_INDEXER_API_URL=http://:3004 +BTC_INDEXER_API_KEY= + +# On the BTC destination indexer +DOGE_INDEXER_API_URL=http://:3004 +DOGE_INDEXER_API_KEY= +``` + +Without either URL, that destination holds silently at the bridge proof +barrier until the default 900-second (15-minute) hold ceiling; reaching the +ceiling can re-drive the wait but never credits an unproven transfer. Apply +this rule to every origin and destination pair in any two-stack deployment, +not only to BTC and DOGE. + ## Step 6: decide your capabilities `config/validator/hub-caps/capabilities.json` is ready to go for `price`, diff --git a/package-lock.json b/package-lock.json index 684e3ed9..000e5736 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xchain-documentation", - "version": "0.20.0", + "version": "0.20.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "xchain-documentation", - "version": "0.20.0", + "version": "0.20.1", "license": "AGPL-3.0-or-later", "devDependencies": { "mathjs": "15.2.0" diff --git a/package.json b/package.json index e4193bb1..5a933400 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xchain-documentation", "description": "XChain Platform protocol specification, architecture guides, and developer documentation", - "version": "0.20.0", + "version": "0.20.1", "license": "AGPL-3.0-or-later", "repository": { "type": "git", diff --git a/protocol/action-manifest.json b/protocol/action-manifest.json index 43a87125..a8343544 100644 --- a/protocol/action-manifest.json +++ b/protocol/action-manifest.json @@ -2,8 +2,8 @@ "$schema_note": "Authoritative registry of every XChain protocol ACTION and which repos must wire it. Single source of truth for the cross-repo action lockstep. Adding an action = add one entry here, re-vendor the copies, and the per-repo ActionManifestConformance guards force every repo to wire it (or fail CI). Generated from live wiring at HEAD; it codifies what IS, not an aspiration.", "authority": "xchain-documentation/protocol/action-manifest.json is authoritative. xchain-{decoder,encoder,indexer,sdk,wallet,explorer}/test/fixtures/action-manifest.json vendor byte-identical copies so each repo CI asserts its slice without a sibling checkout. Keep all copies identical.", "flags": { - "wireDecoded": "top-level ACTION-encoded on-chain tx the DECODER must decode (xchain-decoder VALID_ACTION_NAMES); also enforced pre-broadcast by the ENCODER's own VALID_ACTION_NAMES/ACTION_ALIASES gate (xchain-encoder src/validator.js)", - "indexerHandled": "the INDEXER dispatches a handler for it (xchain-indexer src/actions.js action== switch)", + "wireDecoded": "top-level ACTION-encoded on-chain tx the DECODER must decode (xchain-decoder VALID_ACTION_NAMES); also enforced pre-broadcast by the ENCODER's own VALID_ACTION_NAMES/ACTION_ALIASES gate (xchain-encoder src/common/validator/constants.js, applied in src/common/validator/action_data_checks.js)", + "indexerHandled": "the INDEXER dispatches a handler for it (xchain-indexer src/actions/actions_class/dispatch.js action== switch)", "userEncodable": "the SDK can author it (xchain-sdk Formats keys)", "userEncodableVersions": "REQUIRED on every userEncodable action, forbidden on the rest: the exact list of FORMAT versions a user may author (xchain-sdk Formats[ACTION] keys). Present because userEncodable alone is action-level, so a version the indexer accepts only when it synthesizes it could be added to the SDK Formats without any guard noticing. Each entry is audited against the indexer handler's own this.formats map plus its system-only gates, so a version listed here is one a user-broadcast tx can legitimately carry.", "explorerRender": "the EXPLORER renders it (xchain-explorer getActionData)", diff --git a/protocol/actions/anchor.md b/protocol/actions/anchor.md index 66af3ef9..acf01cdd 100644 --- a/protocol/actions/anchor.md +++ b/protocol/actions/anchor.md @@ -217,6 +217,8 @@ XCHECKPOINT|CHAIN|NETWORK|BLOCK_INDEX|BLOCK_HASH|LEDGER_HASH|ACTIONS_HASH|CONTRA `NETWORK` here is the bundle header's network and `SECTION_SNAPSHOT_BLOCK` the section's own snapshot block, so a section's signed bytes are byte-identical to the checkpoint canonical the hub `StateCheckpointEngine` signs and the SDK / explorer verifiers reconstruct (the publisher reuses the checkpoint row's signatures verbatim, it does not re-sign anything to build a bundle). +That identity holds at or above `CHECKPOINT_COMMITMENT_ACTIVATION`, evaluated on the header network at `SECTION_SNAPSHOT_BLOCK` (see [flag days](../flag-days.md)). Below it the hub and the SDK / explorer verifiers omit the root suffix while the indexer rebuilds every v0 section with it, so no bundle may carry a v0 section whose own snapshot block sits below that height. Such a section fails signature verification rather than being accepted, and because the bundle verdict is all-or-nothing it takes the whole bundle with it; the verifier never adopts roots the quorum did not sign. + `ARCHIVE_B64` is **not** part of the signed bytes; the blob is bound to the signed structure by `BATCH_CRC32`, computed over the uncompressed JSON. (CRC over uncompressed bytes keeps verification independent of the zlib version that produced the gzip stream.) Chain/network are diff --git a/protocol/actions/deploy.md b/protocol/actions/deploy.md index 48f665c6..ba965ab5 100644 --- a/protocol/actions/deploy.md +++ b/protocol/actions/deploy.md @@ -78,7 +78,9 @@ Final slice of the same group; a later DEPLOY|2 (or DEPLOY|3) then assembles by 5. Banned literal check, rejects `BigInt` and `RegExp` literals 6. Banned async check (consensus-gated), rejects `async`/`await`/`Promise` references after the `VM_BANNED_ASYNC` flag-day 7. Banned generator check (consensus-gated), rejects `function*`, generator methods, and `yield`; live from genesis on testnet/regtest - 8. Banned WebAssembly check (consensus-gated), rejects any reference to the global `WebAssembly`; live from genesis on testnet/regtest + 8. Banned rest-pattern check (consensus-gated on its own [`REST_PATTERN_METER`](../flag-days.md) gate), rejects a rest pattern the gas meter cannot charge: a rest parameter, a nested rest inside a destructuring pattern, a catch-clause rest, and a rest in a `for-of`/`for-in` loop head; live from genesis on testnet/regtest + 9. Banned WebAssembly check (consensus-gated), rejects any reference to the global `WebAssembly`; live from genesis on testnet/regtest +- Checks 4, 6 and 9 also reject the global-object-qualified spellings (`globalThis.Math.pow`, `this.Promise`, `globalThis.globalThis.WebAssembly`); see [Global-object spellings](../../components/vm/operations.md#global-object-spellings) - If syntax validation fails, the deployment is rejected with `invalid: CODE_ENCODING ()` and no gas is charged ```mermaid @@ -97,7 +99,9 @@ flowchart TD Async -->|"fail"| Reject Async -->|"pass"| Generator{"7. Banned generator check, function*, generator methods, yield"} Generator -->|"fail"| Reject - Generator -->|"pass"| Wasm{"8. Banned WebAssembly check, global WebAssembly reference"} + Generator -->|"pass"| Rest{"8. Banned rest-pattern check, unmeterable rest positions, REST_PATTERN_METER gate"} + Rest -->|"fail"| Reject + Rest -->|"pass"| Wasm{"9. Banned WebAssembly check, global WebAssembly reference"} Wasm -->|"fail"| Reject Wasm -->|"pass"| Charge["Gas charged, deployment proceeds"] ``` diff --git a/protocol/actions/file.md b/protocol/actions/file.md index 0f439d2d..efaf6433 100644 --- a/protocol/actions/file.md +++ b/protocol/actions/file.md @@ -61,6 +61,7 @@ This example uploads an encrypted ZIP gated by the PEPECREATURE token. `ENCRYPTI - When `GATE_TICKER` is non-empty, `rawData` is the ciphertext: `[12-byte nonce][16-byte GCM authentication tag][ciphertext]`. - `GATE_MIN_AMOUNT`, when present, must be a decimal amount strictly greater than zero (every zero form is invalid), at most 40 characters, digits with at most one `.`, no leading zeros unless the integer part is exactly `0`, a non-empty fractional part whenever a `.` is present, and no more decimal places than min(the gate token's divisibility, 18). A present-but-invalid value makes the FILE invalid rather than being ignored: a FILE is immutable, so a dropped threshold would leave the publisher believing one was in force while the chain recorded none. - `GATE_MIN_AMOUNT` is only meaningful with a `GATE_TICKER`; on a non-gated FILE it is invalid, since there is no balance to weigh it against. +- `GATE_TICKER` takes the ticker name; see [Index ID References](../index-id-references.md). **Never compact `GATE_TICKER` to `^`: write the ticker in full.** A caret resolves during validation, so the FILE is accepted, but the value is stored verbatim and the `SEND` key-handoff rule matches it against each `SEND`'s `TICK` as exact text, so a `SEND` that writes the ticker name never triggers the handoff and the file is in effect not gated. ## Cost and storage diff --git a/protocol/actions/rollcall.md b/protocol/actions/rollcall.md index b727f4a2..5b9c8a47 100644 --- a/protocol/actions/rollcall.md +++ b/protocol/actions/rollcall.md @@ -105,7 +105,7 @@ The close for epoch `E` runs at `C = E + ROLLCALL_ACCEPT_WINDOW_BLOCKS + ROLLCAL The membership predicate itself does not change. The stamp is the whole effect: the source leaves through the predicate's existing terms, and the validator set shrinks exactly the way it shrinks for any UNSTAKE. ## Gates: what a v1 roll teaches the attestation capability set -For a ROLLED epoch at or above `ROLLCALL_GATES_ACTIVATION`, the close additionally records every verified v1 signer's `GATES` list, keyed by pubkey (19 `.` keys at this revision, the same shared consensus-gate set the hub and indexer's own rules digest hashes). An unrolled epoch, or a v0-only rolled epoch, records nothing here. +For a ROLLED epoch at or above `ROLLCALL_GATES_ACTIVATION`, the close additionally records every verified v1 signer's `GATES` list, keyed by pubkey (33 `.` keys as measured 2026-09-22, the same shared consensus-gate set the hub and indexer's own rules digest hashes; the set grows every time a gate is appended to it, so the count is a dated measurement, not a constant). An unrolled epoch, or a v0-only rolled epoch, records nothing here. The `attestation` capability set then drops a validator whose most recently recorded list is not a superset of the gates active at the request's own block: a validator that has never rolled a v1, or whose recorded list has fallen behind a gate armed since it last rolled, is still served, since it has simply never proven what it knows; only a validator that positively named a list missing a gate now active is dropped. A pubkey with no recorded list at all is never dropped by this rule, since liveness eviction (above) already owns the never-rolled case. diff --git a/protocol/actions/vote.md b/protocol/actions/vote.md index 39b32f3b..7c3821cd 100644 --- a/protocol/actions/vote.md +++ b/protocol/actions/vote.md @@ -186,7 +186,7 @@ A poll is *binding* when its v0 sets `CALLBACK_CONTRACT`: finalization then call - **When it fires.** At v2, after the tally is frozen and the deposit settled, the callback fires if `CALLBACK_ON` permits the outcome: `pass` only on a `finalized` win, `always` on `finalized` or `failed_quorum`. A poll that does not meet its gate under `pass` never calls the contract. - **Timelock.** A poll created with `CALLBACK_DELAY_BLOCKS > 0` defers the firing: the v2 stamps a due block (`finalize block + delay`) and the per-block sweep injects the callback EXECUTE there, reconstructing the frozen result from the terminal poll row. Everything else about finalization (tally freeze, deposit settlement, escrow release) still happens at the v2. The delay is the holders' and guardians' reaction window between a hostile pass and value moving; a callback contract can use it to honor a veto armed in the interim. - **How it runs.** The callback is a system-synthesized EXECUTE injected in the same block as the v2, mirroring ATTEST's callback. Its `SOURCE` is the callback contract itself (`C::`), it is marked as an emission, and gas is bounded by `GAS_CEILING`. The injected EXECUTE's `action_index` is recorded on the poll (`callback_execute_action_index`). -- **What the method receives.** The poll result is delivered as positional arguments the contract reads with `xchain.getInputParam`: poll id, status, winning option, total counted weight, total voters, quorum-met flag, min-voters-met flag, then any `CALLBACK_PARAMS` elements. The result is passed in rather than read via `xchain.getPollResult` because the callback runs in the poll's own finalization block, before the poll is visible to the result accessor (which only exposes polls resolved in an earlier block). +- **What the method receives.** The poll result is delivered as positional arguments the contract reads with `xchain.getInputParam`: poll id, status, winning option, total counted weight, total voters, quorum-met flag, min-voters-met flag, then (at/after the `VOTE_POLL_TICK_VISIBLE` flag-day) the poll's electorate `TICK` as a ticker string (empty when the poll names no token), then any `CALLBACK_PARAMS` elements. Before the flag-day there is no tick slot and `CALLBACK_PARAMS` start at the eighth position, so a callback that reads fixed offsets must know which side of the flag-day it runs on. The tick lets a binding-poll contract verify which token decided the poll; the treasury template's `arm` pins it to its governance tick and reverts otherwise. The result is passed in rather than read via `xchain.getPollResult` because the callback runs in the poll's own finalization block, before the poll is visible to the result accessor (which only exposes polls resolved in an earlier block). - **Isolation.** The callback runs inside a savepoint. If it throws, only the callback is rolled back; the poll stays terminal with its frozen tally and settled deposit. A binding callback never un-decides a poll. - **Funding.** `GAS_ESCROW` funds the callback's execution and is escrowed alongside `DEPOSIT` at creation; both are released at finalize (see Deposit and callback flow). diff --git a/protocol/constants.js b/protocol/constants.js index 966f5f66..c3f5868e 100644 --- a/protocol/constants.js +++ b/protocol/constants.js @@ -1083,6 +1083,20 @@ const ORACLE_FEE_SET_CAPTURE_ACTIVATION = { regtest: 0, }; +// AMOUNT_REPRESENTABILITY_ACTIVATION: the block-time boundary at/above which an +// amount must be a plain unsigned decimal numeral whose integer part fits the +// ledger's DECIMAL(60,18) capacity. Below it the legacy text-shape validator is +// preserved so replay does not re-grade committed actions. +// +// Mainnet is held under the standing write hold. Testnet is also unarmed because +// it has live history and needs a measured old-vs-on replay witness before this +// stricter rule can be scheduled. Regtest is genesis-active so replay exercises it. +const AMOUNT_REPRESENTABILITY_ACTIVATION = { + mainnet: 9999999999, + testnet: 9999999999, + regtest: 0, +}; + // DISPENSER_EXPIRY_REALIGN_ACTIVATION (dispenser soft-expire measurement point): the flag-day // at/above which the DECODER soft-expires open dispensers AFTER the block's transaction loop // instead of before it, putting its measurement point where the INDEXER's already is. Keyed on @@ -1185,6 +1199,23 @@ const DISPENSER_CANCEL_GRACE_ACTIVATION = { regtest: 0, }; +// DISPENSER_FRESHNESS_SHAPE_ACTIVATION: the processing chain's own height +// at/above which a non-null get_first_seen result with the wrong shape is fatal +// instead of degrading to the legacy fail-open null. +// +// Each mainnet chain remains operator-owned and must be armed below its existing +// freshness boundary. The bare mainnet entry keeps unknown coins inert. Testnet +// is genesis-active because the tracker path is unreachable there, while regtest +// is genesis-active so the strict path is exercised. +const DISPENSER_FRESHNESS_SHAPE_ACTIVATION = { + 'BTC:mainnet': null, + 'LTC:mainnet': null, + 'DOGE:mainnet': null, + mainnet: null, + testnet: 0, + regtest: 0, +}; + // BATCH_SUBCOMMAND_OUTPUT_CAPTURE_ACTIVATION (payment-output capture through a BATCH): the // flag-day at/above which the DECODER decides which native-coin outputs to persist by looking // at a BATCH's SUB-COMMANDS instead of only at the top-level ACTION name. Keyed on BLOCK TIME @@ -1247,8 +1278,10 @@ const BATCH_SUBCOMMAND_OUTPUT_CAPTURE_ACTIVATION = { // at/above which the decoder recognizes Taproot-envelope reveals as // action-bearing transactions, per host chain and network. Recognition (and // the §3.8 mixed-carrier/multi-envelope rejections, which activate at the same -// height) is fleet-deterministic: every decoder for a chain+network MUST flip -// at the same height or the fleet forks on the first envelope. Keyed on each +// height, except the payload-free carrier case, which waits for +// ENVELOPE_CARRIER_RECOGNITION_ACTIVATION below) is fleet-deterministic: every +// decoder for a chain+network MUST flip at the same height or the fleet forks +// on the first envelope. Keyed on each // chain's OWN local block height (like STATE_COMMITMENT_ACTIVATION) because // recognition happens while parsing that chain's blocks. DOGE has no segwit, // hence no envelope: its entry is null (never active) and must stay null. @@ -1271,6 +1304,29 @@ const ENVELOPE_RECOGNITION_ACTIVATION = { DOGE: { mainnet: null, testnet: null, regtest: null }, }; +// ENVELOPE_CARRIER_RECOGNITION_ACTIVATION (spec §3.8): the LOCAL block height +// at/above which the decoder counts a RECOGNIZED but payload-free carrier as a +// mixed carrier. Below it, arbitration infers a co-present carrier from the +// payload bytes it contributes (or a chunk marker), so an XCHN OP_RETURN that +// deobfuscates to exactly the magic and nothing after it adds zero bytes and +// the envelope is still accepted as an action. Its own height, separate from +// ENVELOPE_RECOGNITION_ACTIVATION, because that gate is already armed on BTC +// and LTC mainnet: changing what §3.8 refuses is a second recognition change +// that every decoder must flip at the same height, and below it the decoder +// behaves exactly as shipped, so replay of indexed history is byte-identical. +// Mainnet is deliberately unpinned (null = never active); pinning it is an +// operator deploy-train decision. testnet/regtest are genesis-active, and DOGE +// is null everywhere (no envelope). Once pinned, every decoder on that +// chain+network MUST run the value before its height or the fleet forks on +// the first envelope beside a marker-only XCHN OP_RETURN; verify by reading +// the armed map out of each RUNNING container. Vendored byte-equal into +// xchain-decoder/src/protocol/constants.js. +const ENVELOPE_CARRIER_RECOGNITION_ACTIVATION = { + BTC: { mainnet: null, testnet: 0, regtest: 0 }, + LTC: { mainnet: null, testnet: 0, regtest: 0 }, + DOGE: { mainnet: null, testnet: null, regtest: null }, +}; + // ── FILE payload compression (spec Part B) ─────────────────────────────────── // // COMPRESSION is a trailing optional field on FILE v0: @@ -1426,6 +1482,27 @@ const PRICE_FEE_BATCH_LANDED_ACTIVATION = { regtest: null, }; +// PRICE_ZERO_VALIDITY_ACTIVATION: the action block-time boundary at/above which +// PRICE values must lie strictly inside (0, PRICE_MAX), matching the hub's bound. +// Mainnet remains operator-owned. Public testnet uses a future instant so existing +// history is not re-graded, and regtest is genesis-active for venue coverage. +const PRICE_ZERO_VALIDITY_ACTIVATION = { + mainnet: null, + testnet: 1790812800, + regtest: 0, +}; + +// PRICE_BATCHING_FLOOR_ACTIVATION: the earliest block time for which price sync +// barriers apply. A positive floor may only be armed at or below the network's +// first finalized price round; 0 preserves the existing barrier on every block. +// Mainnet has no rail start yet, testnet still needs that measured first-round +// timestamp, and regtest deliberately keeps no pre-batch era for seeded rounds. +const PRICE_BATCHING_FLOOR_ACTIVATION = { + mainnet: 0, + testnet: 0, + regtest: 0, +}; + // VALID_FIAT_CODES: the accepted FIAT_CODE allow-list for PRICE actions. The indexer's // config['FIATS'] keys (xchain-indexer/src/config.js) are the on-chain arbiter; this list // mirrors them in the indexer's insertion order. The SDK validator (VALID_FIAT_CODES) must @@ -1515,9 +1592,16 @@ const TRAIN_ACTIVATION = { // the LTC leg of this family two days off its BTC counterpart a day after the first cut. // That lead is the rolling-upgrade window the fleet roll must finish inside (24x the 90 // minute roll budget), and every testnet mirror-admission height sits above it on the same - // BTC clock (the BTC producer at 153,222 is 106 blocks and about 17.0 h further up), so a - // node lacking this rule set halts before it can grade an admission-stamped row. - '0.20.0': { mainnet: 9999999999, testnet: 153116, regtest: 0 }, + // BTC clock, so a node lacking this rule set halts before it can grade an admission-stamped + // row. + // RE-SLID 2026-09-23 for the v0.20.1 patch train, after the live tips overran the + // 2026-09-19 slide before the freeze: margin is 40 h to the nearest armed height, converted + // at each coin's fastest defensible cadence. Chain_tip TBTC 153,698 at 2026-09-23T15:55Z + // + 376 blocks, ceil(40 h / 383.04 s per block, the least-squares bound). The + // mirror-admission family below re-slides onto the same instant plus its own 17 h and 6 h + // offsets. LTC:testnet mirror admission ships disabled on this train and is + // untouched by this reslide; it arms on a later train. + '0.20.0': { mainnet: 9999999999, testnet: 154074, regtest: 0 }, }; // STAKE v1 signing-key REUSE flag day, keyed on the processing chain's OWN @@ -1929,9 +2013,9 @@ const MIRROR_ADMISSION_ACTIVATION = Object.freeze({ 'BTC:mainnet': null, // INERT under the 2026-08-29 mainnet write hold 'LTC:mainnet': null, 'DOGE:mainnet': null, - 'BTC:testnet': 153222, // epoch close 153,216 + 6 buried; tip 152,756 + 466 at 498.7 s/blk, about 64.5 h - 'LTC:testnet': 4891504, // RE-CUT 2026-09-17 22:45Z onto that instant: tip 4,889,190 + 2314 at 82.5 s/blk - 'DOGE:testnet': 67911796, // RE-CUT 2026-09-17 22:45Z onto that instant: tip 67,904,912 + 6884 at 27.7 s/blk + 'BTC:testnet': 154234, // RE-SLID 2026-09-23: train 154,074 + 160 blocks (17 h at the 383.04 s/blk bound, 25.6 h at the 575.89 s/blk 84 h trailing mean), the v0.20.1 patch reslide + 'LTC:testnet': null, // disabled for v0.20.1, 2026-09-18: LTC:testnet mirror admission ships null on this train; arms on a later train + 'DOGE:testnet': 67936053, // RE-SLID 2026-09-23: tip 67,924,397 at 17:48Z + 11656 blocks (83.8 h at 25.89 s/blk, the 84 h trailing mean, the same instant as the BTC producer), the v0.20.1 patch reslide 'BTC:regtest': resolveMirrorAdmissionRegtest(process.env), 'LTC:regtest': resolveMirrorAdmissionRegtest(process.env), 'DOGE:regtest': resolveMirrorAdmissionRegtest(process.env), @@ -1941,9 +2025,9 @@ const MIRROR_ADMISSION_CONSUMER_ACTIVATION = Object.freeze({ 'BTC:mainnet': null, 'LTC:mainnet': null, 'DOGE:mainnet': null, - 'BTC:testnet': 153266, // its producer + 44 blocks, about 6 h: strictly above, never equal - 'LTC:testnet': 4891766, // its producer + 262 blocks, about 6 h at 82.5 s/blk - 'DOGE:testnet': 67912575, // its producer + 779 blocks, about 6 h at 27.7 s/blk + 'BTC:testnet': 154291, // RE-SLID 2026-09-23: its producer + 57 blocks (6 h at the 383.04 s/blk bound, 9.1 h at the 575.89 s/blk 84 h trailing mean), strictly above, never equal + 'LTC:testnet': null, // disabled for v0.20.1, 2026-09-18: LTC:testnet mirror admission ships null on this train; arms on a later train + 'DOGE:testnet': 67936888, // RE-SLID 2026-09-23: its producer + 835 blocks (6 h at 25.89 s/blk, the 84 h trailing mean), strictly above, never equal 'BTC:regtest': resolveMirrorAdmissionRegtest(process.env), 'LTC:regtest': resolveMirrorAdmissionRegtest(process.env), 'DOGE:regtest': resolveMirrorAdmissionRegtest(process.env), @@ -1990,11 +2074,11 @@ const ANCHOR_ATTEST_ARRIVAL_MARGIN_S = 64800; // 18 h // guarantee, and one map removes that window. const ANCHOR_ATTEST_BARRIER_ACTIVATION = Object.freeze({ mainnet: null, // INERT under the 2026-08-29 mainnet write hold - // SIZED 2026-09-16 20:41Z on the BTC clock, the same instant as the family's BTC CONSUMER - // height, so the one member that keeps BOTH completeness certificates gains them together - // instead of carrying a lone extra rule for 6 h. Above the same roll and the same epoch - // close; the measurement, the formula and the re-size rule are with the maps above. - testnet: 153266, + // SIZED 2026-09-16 20:41Z, RE-SLID 2026-09-19 and 2026-09-23 on the BTC clock, the same + // instant as the family's BTC CONSUMER height, so the one member that keeps BOTH + // completeness certificates gains them together instead of carrying a lone extra rule for + // 6 h. The measurement, the formula and the re-size rule are with the maps above. + testnet: 154291, regtest: resolveMirrorAdmissionRegtest(process.env), // shares the family's arming seam so one venue lever arms both }); @@ -2076,10 +2160,13 @@ module.exports = { ATTEST_RESPONSIBLE_WIDENING_V2, ORACLE_FEE_OUTPUT_ACTIVATION, ORACLE_FEE_SET_CAPTURE_ACTIVATION, + AMOUNT_REPRESENTABILITY_ACTIVATION, DISPENSER_EXPIRY_REALIGN_ACTIVATION, DISPENSER_CANCEL_GRACE_ACTIVATION, + DISPENSER_FRESHNESS_SHAPE_ACTIVATION, BATCH_SUBCOMMAND_OUTPUT_CAPTURE_ACTIVATION, ENVELOPE_RECOGNITION_ACTIVATION, + ENVELOPE_CARRIER_RECOGNITION_ACTIVATION, COMPRESSION_CODE_DEFLATE_RAW, COMPRESSION_MAX_RATIO, COMPRESSION_MAX_INPUT_BYTES, @@ -2088,6 +2175,8 @@ module.exports = { PRICE_PAIR_WIDEN_ACTIVATION, PRICE_SIG_TALLY_ACTIVATION, PRICE_FEE_BATCH_LANDED_ACTIVATION, + PRICE_ZERO_VALIDITY_ACTIVATION, + PRICE_BATCHING_FLOOR_ACTIVATION, VALID_FIAT_CODES, GAS_TICK, PRICE_MAX, diff --git a/protocol/controller-bound-tokens.md b/protocol/controller-bound-tokens.md index 88115f05..1a857506 100644 --- a/protocol/controller-bound-tokens.md +++ b/protocol/controller-bound-tokens.md @@ -69,8 +69,8 @@ may have several legs. A direct `SEND` runs the token's `transfer` guard, then t account's outbound `transfer` guard, then the `DESTINATION` account's inbound one (see [Account controllers](#account-address-controllers)); bulk actions (`AIRDROP` / `DIVIDEND` / `SWEEP`) repeat the applicable guards per tick or leg. Each run is metered separately against -`GAS_SCHEDULE.VM_GUARD_GAS_CEILING` and the reservations are cumulative (see [Gas](#gas)), so -budget `GAS` for every guard an action can invoke, not for one. +`GAS_SCHEDULE.VM_GUARD_GAS_CEILING` and, on BTC, the reservations are cumulative (see +[Gas](#gas)), so budget `GAS` for every guard an action can invoke, not for one. To layer several policies on the *same* subject and class, put them inside that controller's `guard`. Note that a controller does not re-enter its own guard for the moves that guard @@ -122,6 +122,17 @@ Rules: `(token, class)` is the latest event at or below the current block: a `bind` gates; an `unbind` gates only until its cooldown elapses. There is no `LOCK_CONTROLLER` flag; the drop-cooldown is the only friction on changing a binding. +- **Bindings and [bridging](./token-bridge.md#issuer-opt-in) exclude each other.** A token that + is bridgeable (a live `BRIDGE_CHAINS`) or has ever bridged (the sticky `BRIDGED` bit) cannot + use `ISSUE` v6 at all, bind or unbind: + `invalid: TICK (bridged tokens cannot be policy-bound yet)`. Clearing `BRIDGE_CHAINS` with + `-` reopens binding only for a token that never actually bridged. In the other direction, a + token with any effective binding (including one still inside its drop-cooldown) cannot opt + into bridging: `invalid: TICK (policy-bound tokens are not bridgeable yet)`. So the order is + a choice: bind first and the token forfeits bridging until every binding is fully dropped; + bridge first and it forfeits controllers. Unlike allow/block lists and sleep, this half of + the exclusion does not lift at policy inheritance, because a controller names a contract on + this chain's VM (see [What does not generalize](./token-bridge.md#what-does-not-generalize)). - A token with no binding for a class behaves exactly as before (one NULL check, zero overhead). ```mermaid @@ -137,7 +148,8 @@ stateDiagram-v2 An action is always **routed** to exactly one of six concrete classes by a static map from the action name. The class is never derived from user-supplied data, so a future action -cannot accidentally fall into a controlled class. +cannot accidentally fall into a controlled class. A few actions refuse a bound token without +running any guard; see [Actions refused outright](#actions-refused-outright-on-a-bound-token). | Class | Gates | Guard `action_type` | |---|---|---| @@ -177,6 +189,21 @@ Cooldown and unbind semantics are identical for `all` (it is just another `actio value with its own append-only events). `all` participates in resolution only; routing is unchanged. +### Actions refused outright on a bound token + +The class table lists the actions that run a guard. A few actions never invoke the guard ABI; +instead they refuse a bound token while a qualifying binding is effective, whatever the +controller would have decided: + +| Action | Refused when | Verdict | +|---|---|---| +| [`BET`](./actions/bet.md) v0 (create a market) | `TICK` has an effective `trade` binding, or failing that an `all` binding (the same most-specific-wins resolution the `ORDER` guard uses) | `invalid: TICK (controller-bound)` | +| [`ISSUE`](./actions/issue.md) v7 (bridge opt-in) | `TICK` has any effective binding | `invalid: TICK (policy-bound tokens are not bridgeable yet)` | + +So binding `trade` or `all` also makes a token un-bettable (a market would otherwise route the +token around the `trade` veto and royalty legs), and any binding blocks bridging; the reverse +direction of the bridge rule is under [Binding a controller](#binding-a-controller-issue-v6). + --- ## The guard ABI @@ -288,7 +315,8 @@ transfer ownership rather than a balance, so no proceeds split applies to that l > routed around by vending the token through a dispenser instead of listing it. A guard that > means to enforce a cut must `revert` on `action_type === 'DISPENSER_CREATE'` (or on the > dispenser price it will not be paid a share of). This is a known engine gap, not a design -> rule: it is recorded as a `KNOWN GAP` at the call site in the indexer's `dispenser.js`. +> rule: it is recorded as a `KNOWN GAP` at the call site in +> `xchain-indexer/src/actions/dispenser/controller_guard.js`. ### Cross-chain sales (`CROSS_CHAIN_ROYALTY`) @@ -350,9 +378,10 @@ acceptance rule is keyed on the local block (`protocol_changes.js`). Operators m coordinate the two: flip the canonical gate first or together with the create-side gate, never create-side first. Both mainnet values are armed: the canonical flip is set to `snapshot_block` height `961000` (BTC anchor ~2026-08-04; hub and every indexer must deploy -before that height), and the create-side acceptance gate (`CROSS_CHAIN_ROYALTY`) is keyed on a -block time listed on [Flag-Day Values](./flag-days.md#mainnet-time-keyed-gates), one quarter -after the rest of Cohort A. +before that height), and the create-side acceptance gate (`CROSS_CHAIN_ROYALTY`) is keyed on +its own, later block time, listed on +[Flag-Day Values](./flag-days.md#mainnet-time-keyed-gates) under its own date rather than the +Cohort A instant. --- @@ -437,9 +466,10 @@ self-imposed spending controls such as velocity limits, allowlists, or complianc **`DESTINATION`** (an *inbound* gate: refuse an unsolicited incoming transfer). The guard distinguishes direction from its `from` / `to` (`from === subject` ⇒ outbound). The enforcement order is: the token's own `transfer` guard, then the source's outbound `transfer` -guard, then the destination's inbound `transfer` guard. `SOURCE` pays the guard gas, and the -reservations are cumulative so `GAS` can never be driven negative. DEX and dispenser -deliveries are *solicited pulls*, not direct sends, so they are never gated this way. +guard, then the destination's inbound `transfer` guard. `SOURCE` pays the guard gas, and on +BTC the reservations are cumulative so `GAS` can never be driven negative (see [Gas](#gas)). +DEX and dispenser deliveries are *solicited pulls*, not direct sends, so they are never gated +this way. Where a token controller makes the rules travel with the *asset*, an address controller makes them travel with the *account*. @@ -503,10 +533,17 @@ Running the guard costs VM gas, billed to the action's `SOURCE` in `XCHAIN` at `fee = gasBilled × GAS_PRICE`: - The guard runs against a bounded ceiling, `GAS_SCHEDULE.VM_GUARD_GAS_CEILING` - (default 200,000). -- `SOURCE` must hold the **full ceiling fee** as a reservation before the guard runs (this - mirrors the cross-contract-call gas reservation); insufficient `XCHAIN` rejects the action - before any VM work. The actual metered fee (≤ reservation) is what is charged. + (default 200,000), on every chain. +- On BTC, `SOURCE` must hold the **full ceiling fee** as a reservation before the guard runs + (this mirrors the cross-contract-call gas reservation); insufficient `XCHAIN` rejects the + action with `insufficient funds (guard gas)` before any VM work. The actual metered fee + (≤ reservation) is what is charged. +- **The reservation is BTC-only today.** It is keyed on the chain, not on whether an `XCHAIN` + token row exists there, so on LTC and DOGE no reservation is taken and + `insufficient funds (guard gas)` is never the verdict, including after the + [XChain bridge](./xchain-bridge.md#the-xchain-token-off-btc) creates an `XCHAIN` row on + that chain. Extending the reservation to every chain would be a separate, future flag-day + change. - **v1 charges guard gas on ALLOW only.** A denied action records no ledger change (preserving the ledger/balance invariant). The denial-spam vector is bounded by the real on-chain transaction cost of each attempt; charge-on-deny is a possible later refinement. @@ -594,6 +631,8 @@ For the current protocol activation heights, see [Protocol Activation](./protoco - [`MINT`](./actions/mint.md), [`STAKE`](./actions/stake.md), [`SWEEP`](./actions/sweep.md), [`DESTROY`](./actions/destroy.md), [`ORDER`](./actions/order.md), [`SWAP`](./actions/swap.md), [`DISPENSER`](./actions/dispenser.md): the guarded actions. +- [`BET`](./actions/bet.md): market creation refuses a `trade`- or `all`-bound token. +- [Token Bridge](./token-bridge.md): the bridge/controller mutual exclusion. - [`DEPLOY`](./actions/deploy.md) and [Contract ABI](./contract-abi.md): deploying a controller and declaring its manifest. - [Smart Contracts](../concepts/smart-contracts.md): the VM the guard runs in. diff --git a/protocol/cross-chain-dex.md b/protocol/cross-chain-dex.md index 5f479aec..3c59c147 100644 --- a/protocol/cross-chain-dex.md +++ b/protocol/cross-chain-dex.md @@ -86,7 +86,7 @@ A finalized match is symmetric and describes both legs: | `network` | `mainnet`/`testnet`/`regtest`; signed into the canonical so a match can only settle on the network it was matched on | | `a_chain,a_action_index,a_kind,a_tick,a_amount,a_filled_before,a_ownership,a_payout_addr` | order A (canonical-lower side); payout = A's receive address on B's chain | | `b_*` | order B | -| `a_payout_legs`/`b_payout_legs` | each order's controller-guard royalty split (JSON `[{to,bps}]` in that order's OWN chain encoding; NULL = none). Applied to the released proceeds at settlement, re-encoded to the local chain; signed into the canonical at/above the `CROSS_CHAIN_ROYALTY` flag-day (see `Controller_Bound_Tokens.md` §Cross-chain sales) | +| `a_payout_legs`/`b_payout_legs` | each order's controller-guard royalty split (JSON `[{to,bps}]` in that order's OWN chain encoding; NULL = none). Applied to the released proceeds at settlement, re-encoded to the local chain; signed into the canonical at/above the `CROSS_CHAIN_ROYALTY` flag-day (see [Controller-Bound Tokens](./controller-bound-tokens.md#cross-chain-sales-cross_chain_royalty)) | | `effective_time` | wall-clock instant indexers apply at (the only shared clock across chains) | | `validator_signatures` | JSON `[{pubkey,sig}]` over the canonical match, meeting the `cross_chain` quorum at `snapshot_block` (see [Trust model](#trust-model)) | | `status` | `finalized` / `retracted` | diff --git a/protocol/error-codes.md b/protocol/error-codes.md index 143638e8..16a52d11 100644 --- a/protocol/error-codes.md +++ b/protocol/error-codes.md @@ -29,6 +29,7 @@ Errors are JSON objects: | `RELAY_DENIED` | 403 | `/relay` refuses private/loopback/metadata destinations (SSRF guard) | No | | `UNKNOWN_COIN` | 404 | The `{COIN}` prefix is not served by this explorer | No: check `/{COIN}/api/status` | | `NOT_FOUND` | 404 | No row for that lookup | No | +| `ACTION_NOT_YET_INDEXED` | 404 | The action index lies above what this explorer's indexer has committed so far; the body carries `indexed_through` and the response a `Retry-After` header | Yes: the indexer is catching up; honor `Retry-After` | | `CHECKPOINT_NOT_FOUND` | 404 | No quorum-signed checkpoint at that height | Maybe: checkpoints lag the tip | | `RATE_LIMITED` | 429 | Per-IP request budget exhausted (default 500/min) | Yes: back off; honor `RateLimit-*` headers | | `SERVER_ERROR` | 500 | Unexpected internal failure | Yes: with backoff | @@ -53,6 +54,7 @@ Errors are JSON objects: | `CHECKPOINT_PRE_COMMITMENT` | 409 | The checkpoint predates the state-commitment flag day, so it carries no committed roots to prove against | No: choose a later height | | `ACTION_BLOCK_NOT_CHECKPOINTED` | 409 | The action's block is not checkpointed yet, so there is no signed `block_merkle_root` to bind the proof to | Yes: after a checkpoint covers the block | | `SNAPSHOT_NOT_YET_CHECKPOINTED` | 409 | No BTC checkpoint exists at the snapshot height yet (validator-set proof) | Yes: after the chain advances | +| `STAKE_SNAPSHOT_TRUNCATED` | 409 | The indexer marked a capability's stake snapshot at this height truncated (the qualifying validator set overflowed its query cap), so the stake total would be under-counted and no proof is served (validator-set proof) | No: the same height keeps answering this until operators raise the indexer's query cap | | `CONTRACT_STATE_NOT_COMMITTED` | 409 | `contract_state_root` is not committed at this height, so absence cannot be proven | No: choose a later height | | `ESCROW_LEAF_NOT_COMMITTED` | 409 | The locked-balance leaf is not committed at this height, so absence cannot be proven | No: choose a later height | | `STATE_TOO_LARGE` | 413 | Contract simulation: the contract's state exceeds the simulation limits | No | @@ -63,6 +65,7 @@ Errors are JSON objects: | `PROOF_STATE_ROOT_MISMATCH` | 500 | The committed `state_root` does not match this server's local state tree | No: the operator must investigate | | `ACTION_LEAF_NOT_FOUND` | 500 | The action row is not present in its block's leaf set (action proof) | No: the operator must investigate | | `PROOF_BLOCK_MERKLE_MISMATCH` | 500 | The committed `block_merkle_root` does not match this server's local block tree (action proof) | No: the operator must investigate | +| `STAKE_SNAPSHOT_MALFORMED` | 500 | A capability's stake snapshot at this height cannot yield a stake total (a validator with a blank or missing source, or a missing, non-numeric or negative weight), so no proof is served (validator-set proof) | No: the operator must investigate | | `NO_STATE_TREE` | 501 | This server does not hold the state tree (proof routes need a full indexer database) | No: use another instance | | `INDEXER_UNAVAILABLE` | 502 | The indexer API behind a validator-set proof is unreachable | Yes: with backoff | | `INDEXER_AUTH_REQUIRED` | 503 | The indexer API behind a validator-set proof requires a key this explorer does not carry | No: operator configuration | @@ -73,6 +76,9 @@ Errors are JSON objects: | `VM_QUERY_DISABLED` | 503 | Contract simulation is disabled on this explorer | No: use another instance | | `VM_QUERY_VM_DRIFT` | 503 | Contract simulation is disabled because the deployed VM is not the canonical one | No: operator action | | `VM_MODULE_UNAVAILABLE` | 503 | Contract simulation: the VM module is not available on this host | No: use another instance | +| `INVALID_TICK` | 400 | Bridge panel routes (`GET /{COIN}/api/bridge-invariant/{tick}`, `GET /{COIN}/api/bridge-transfers/{tick}`): the `tick` path segment is empty, longer than 250 characters, or contains a control character | No: fix the request | +| `BRIDGE_INVARIANT_UNAVAILABLE` | 503 | `GET /{COIN}/api/bridge-invariant/{tick}`: this explorer has no hub endpoint, or the hub's bridge-invariant read failed or returned nothing | Maybe: a failed hub read may be transient; an explorer without a hub endpoint answers this every time | +| `BRIDGE_TRANSFERS_UNAVAILABLE` | 503 | `GET /{COIN}/api/bridge-transfers/{tick}`: this explorer holds no hub-mirror source for the coin, or the bridge-transfer read failed | Maybe: a failed read may be transient; a coin with no mirror source answers this every time | | `INVALID_ADDRESSES` | 400 | Batch address routes (`POST /{COIN}/api/balances`, `POST /{COIN}/api/coinpay_obligations`): the body's `addresses` field is missing, not an array, empty, or holds a non-string entry, and nothing is read. The SDK's client-side pre-flight throws the same code before a request is sent (see [SDK explorer methods](../components/sdk/explorer.md)) | No: fix the request | | `TOO_MANY_ADDRESSES` | 400 | Batch address routes: more than 20 addresses in one body, counted before duplicates are collapsed, so a body of repeats is refused rather than quietly served | No: send at most 20 per request | | `INVALID_ADDRESS` | 400 | Batch address routes: one entry is not a well-formed address; the offending entry is echoed (truncated) in the `error` text. Unrelated to the SDK library's own `INVALID_ADDRESS` typed error, which is a separate surface (see [SDK errors](../components/sdk/errors.md)) | No | diff --git a/protocol/flag-days.md b/protocol/flag-days.md index 5e97ced9..56c7be91 100644 --- a/protocol/flag-days.md +++ b/protocol/flag-days.md @@ -5,8 +5,9 @@ # Flag-Day Values **This page is generated** from `xchain-indexer/src/protocol_changes.js`, its part files -under `src/protocol_changes/`, and the time-keyed activation modules beside them. Do not -edit it by hand: run `node bin/generate-flag-days.js` from the repository root and commit the result. +under `src/protocol_changes/`, the time-keyed activation modules beside them, and +`protocol/constants.js`. Do not edit it by hand: run `node bin/generate-flag-days.js` +from the repository root and commit the result. Every other page in this documentation set names the **gate** and links here instead of quoting a date, because a flag-day value is not a fact about the @@ -17,6 +18,58 @@ For what a flag day is, how `isEnabled` evaluates it, which cohort a gate belongs to, and what happens to a node that misses one, see [Protocol Activation](./protocol-activation.md). +## Canonical activation maps + +These names are exported by [`protocol/constants.js`](./constants.js). The index includes +scheduled, inert, genesis-active, time-keyed, and height-keyed maps so a gate remains +discoverable here even when it has no mainnet date for the table below. + +- `AMOUNT_REPRESENTABILITY_ACTIVATION` +- `ANCHOR_ACTIVATION` +- `ANCHOR_ATTEST_BARRIER_ACTIVATION` +- `ANCHOR_REWARD_ACTIVATION` +- `ANCHOR_REWARD_DERIVE_ACTIVATION` +- `ARCHIVE_REWARD_ACTIVATION` +- `ATTEST_ADMISSION_ACTIVATION` +- `ATTEST_BROADCAST_FEE_ACTIVATION` +- `ATTEST_RELAY_ACTIVATION` +- `ATTEST_REQUEST_CAP_ACTIVATION` +- `ATTEST_RESPONSE_MIRROR_ACTIVATION` +- `ATTEST_RESPONSIBLE_WIDENING_ACTIVATION` +- `ATTEST_ZERO_CONF_ACTIVATION` +- `BATCH_SUBCOMMAND_OUTPUT_CAPTURE_ACTIVATION` +- `CHECKPOINT_COMMITMENT_ACTIVATION` +- `CROSS_CHAIN_ROYALTY_ACTIVATION` +- `DISPENSER_CANCEL_GRACE_ACTIVATION` +- `DISPENSER_EXPIRY_REALIGN_ACTIVATION` +- `DISPENSER_FRESHNESS_SHAPE_ACTIVATION` +- `ENVELOPE_CARRIER_RECOGNITION_ACTIVATION` +- `ENVELOPE_RECOGNITION_ACTIVATION` +- `EQUIV_HEADER_ACTIVATION` +- `LIST_OWNER_ACTIVATION` +- `MIRROR_ADMISSION_ACTIVATION` +- `MIRROR_ADMISSION_CONSUMER_ACTIVATION` +- `ORACLE_FEE_OUTPUT_ACTIVATION` +- `ORACLE_FEE_SET_CAPTURE_ACTIVATION` +- `PRICE_BATCHING_FLOOR_ACTIVATION` +- `PRICE_FEE_BATCH_LANDED_ACTIVATION` +- `PRICE_PAIR_WIDEN_ACTIVATION` +- `PRICE_SIG_TALLY_ACTIVATION` +- `PRICE_ZERO_VALIDITY_ACTIVATION` +- `RETRACTION_SIGNING_ACTIVATION` +- `ROLLCALL_ACTIVATION` +- `ROLLCALL_GATES_ACTIVATION` +- `SNAPSHOT_BURIAL_ACTIVATION` +- `STAKE_KEY_REUSE_ACTIVATION` +- `STAKE_WEIGHTED_QUORUM_ACTIVATION` +- `STATE_COMMITMENT_ACTIVATION` +- `SWEEP_ZERO_LEG_ACTIVATION` +- `TICK_NAMESPACE_ACTIVATION` +- `TOKEN_BRIDGE_ACTIVATION` +- `TOKEN_POLICY_INHERITANCE_ACTIVATION` +- `TRAIN_ACTIVATION` +- `XCHAIN_BRIDGE_ACTIVATION` + ## Contract-era flag day The coordinated instant that the **Cohort A** contract-era rules switch on, diff --git a/protocol/index-id-references.md b/protocol/index-id-references.md index d9d02a34..f21f1c12 100644 --- a/protocol/index-id-references.md +++ b/protocol/index-id-references.md @@ -52,6 +52,15 @@ the reference SDK does not compact it, so a client that wants the shorter form w `^` itself. See [LIST](./actions/list.md). +`FILE.GATE_TICKER` is also resolved on input, but clients must write it in full. The +indexer checks it through the same ticker lookup as the fields above, so a `^` +resolves and the `FILE` is accepted, but the value is then stored verbatim as the file's +gate, and the `SEND` key-handoff rule finds a token's gated packs by comparing that stored +value to the `SEND`'s `TICK` as exact text. A compacted gate is therefore missed by every +`SEND` that writes the ticker name: no handoff is required for it, so the file is accepted +but not gated. The reference SDK keeps its existence check on this field and never +compacts it. See [FILE](./actions/file.md). + **Address fields that receive an index id:** the destination/transfer/get-address style fields of an action: `SEND.DESTINATION`, `MINT.DESTINATION`, `MESSAGE.DESTINATION`, `SWEEP.DESTINATION`, diff --git a/protocol/protocol-activation.md b/protocol/protocol-activation.md index ba440e4e..d4a34e05 100644 --- a/protocol/protocol-activation.md +++ b/protocol/protocol-activation.md @@ -73,7 +73,7 @@ date in prose, this one included: a flag-day value is the current setting of a c repinned before, and prose cannot notice its source moving. Pages name the gate and link there. [`constants.js`](constants.js) in this repository is the canonical source for the **validator-era -(Cohort B)** and **state-commitment (Cohort C)** gates below, and for the five **decoder-carried** +(Cohort B)** and **state-commitment (Cohort C)** gates below, and for the six **decoder-carried** gates ([below](#decoder-carried-gates)), which sit outside all three cohorts because they are evaluated in the decoder rather than the indexer. Each consuming service carries a **byte-identical twin** of the maps it needs, and a cross-repo conformance gate fails CI if a twin @@ -95,7 +95,7 @@ re-runs an action handler, a deploy validator, or the VM. | `xchain-indexer` | `protocol_changes.js` (contract-era gates) + the state-commitment and validator-era activation modules | | `xchain-vm` | the seven contract-era VM gate constants (async ban, binary-alloc metering, deploy-linter hardening, state-key NUL-reject, state-key type normalization, metering eval-order fix, call-spread metering) plus five constant-less contract-era riders that key on the binary-alloc instant instead of minting a constant ([Cohort A riders that mint no constant](#cohort-a-riders-that-mint-no-constant)) plus three per-coin height-keyed maps: `PKG3_SANDBOX_ACTIVATION` (the armed runtime half of VM deploy-lint Pkg 3, [below](#additional-armed-gates-service-carried)), and the genesis-armed `EXEC_LINT_ACTIVATION` and `LINT_GLOBAL_ALIAS_ACTIVATION` ([VM gates](#vm-gates-service-carried)) | | `xchain-hub` | the nine validator-era gate modules it consumes (checkpoint, equivocation header, stake-weighted quorum, anchor reward, archive reward, cross-chain royalty canonical, retraction signing, attestation relay, price signature tally). The tenth Cohort B gate, attestation admission, is indexer-only | -| `xchain-decoder` | the five activation maps consumed in the decoder's own parse path: `ORACLE_FEE_OUTPUT_ACTIVATION`, `ORACLE_FEE_SET_CAPTURE_ACTIVATION`, `DISPENSER_EXPIRY_REALIGN_ACTIVATION` and `BATCH_SUBCOMMAND_OUTPUT_CAPTURE_ACTIVATION` (block-time-keyed) plus `ENVELOPE_RECOGNITION_ACTIVATION` (per-chain local height) | +| `xchain-decoder` | the six activation maps consumed in the decoder's own parse path: `ORACLE_FEE_OUTPUT_ACTIVATION`, `ORACLE_FEE_SET_CAPTURE_ACTIVATION`, `DISPENSER_EXPIRY_REALIGN_ACTIVATION` and `BATCH_SUBCOMMAND_OUTPUT_CAPTURE_ACTIVATION` (block-time-keyed) plus `ENVELOPE_RECOGNITION_ACTIVATION` and `ENVELOPE_CARRIER_RECOGNITION_ACTIVATION` (per-chain local height) | | `xchain-sync`, `xchain-explorer`, `xchain-sdk` | the subset each needs to verify or display | Because the values are byte-identical everywhere, a heterogeneous fleet and any from-genesis replay @@ -291,12 +291,12 @@ BOTH copies is a flag day under the [notice policy](./upgrade-notice-policy.md). ## Decoder-carried gates -**Five gates belong to no cohort**, and the difference is worth stating because everything above -describes the **indexer's** `isEnabled` check. These five are evaluated in the **decoder**, while it +**Six gates belong to no cohort**, and the difference is worth stating because everything above +describes the **indexer's** `isEnabled` check. These six are evaluated in the **decoder**, while it parses a block, so they decide what the indexer is ever shown: which transactions count as -action-bearing, and which native-coin outputs are persisted at all. All five are canonical in +action-bearing, and which native-coin outputs are persisted at all. All six are canonical in [`constants.js`](constants.js) and vendored byte-equal into -`xchain-decoder/src/protocol/constants.js`; four are keyed on block time and one on per-chain local +`xchain-decoder/src/protocol/constants.js`; four are keyed on block time and two on per-chain local height. | Gate | Keyed on | Thresholds | Straggler | Lives in | @@ -306,6 +306,7 @@ height. | **Dispenser expiry realignment** (`DISPENSER_EXPIRY_REALIGN_ACTIVATION`, soft-expires open dispensers *after* the block's transaction loop, where the indexer's measurement point already is) | block time | mainnet **armed at genesis** (0, ruled 2026-09-09 under the [mainnet genesis arm](#the-mainnet-genesis-arm)); testnet and regtest genesis-active (0) | forks | `protocol/constants.js`, vendored into `xchain-decoder/src/protocol/constants.js` | | **BATCH sub-command output capture** (`BATCH_SUBCOMMAND_OUTPUT_CAPTURE_ACTIVATION`, decides which native-coin outputs to persist from a BATCH's sub-commands instead of only its top-level ACTION name, so a batched COINPAY or Mode B DISPENSER stops spending a coin and settling nothing) | block time | mainnet **armed**, at the same instant the indexer's `BATCH_ISSUANCE_LIMITS` carries; testnet and regtest genesis-active (0) | forks | `protocol/constants.js`, vendored into `xchain-decoder/src/protocol/constants.js` | | **Taproot envelope recognition** (`ENVELOPE_RECOGNITION_ACTIVATION`, and with it every [envelope consensus rule](./taproot-envelope.md): end-indexed witness parsing, annex refusal, input-0 binding, mixed-carrier and multi-envelope rejection) | per-chain **local height** | `BTC:mainnet` 960850, `LTC:mainnet` 3153500 (both crossed 2026-08-03), `DOGE` **null on every network**; testnet and regtest genesis-active (0) | forks | `xchain-decoder/src/XChainDecoder.js`, mirrored in `xchain-encoder/src/build/crypto_networks.js` | +| **Taproot envelope carrier recognition** (`ENVELOPE_CARRIER_RECOGNITION_ACTIVATION`, makes the [mixed-carrier rule](./taproot-envelope.md#rules-that-decide-whether-an-envelope-is-an-action) count a recognized carrier that contributes no payload, such as an `XCHN` OP_RETURN carrying only the magic; below it such a carrier does not block the envelope) | per-chain **local height** | mainnet **null on every chain** (unpinned; pinning is a deploy-train decision), `DOGE` **null on every network**; BTC and LTC testnet and regtest genesis-active (0) | forks, once pinned | `protocol/constants.js`, vendored into `xchain-decoder/src/protocol/constants.js` | **Neither armed decoder gate appears by name on [Flag-Day Values](./flag-days.md).** That page is generated from the indexer's registry and the activation modules beside it, so a gate declared only @@ -338,7 +339,7 @@ Three further properties of this gate differ from the cohorts above: constant stays in the tree now that both mainnet heights have passed: it is history, not a control. Within **Cohort A**, the **cross-chain royalty create-side** gate is the one rule that does not share -the single contract-era timestamp: it is deliberately armed one quarter later (both values are on +the single contract-era timestamp: it is deliberately armed on its own, later date (both values are on [Flag-Day Values](./flag-days.md)), so the deny window between the two dates is the safe interim while the fleet upgrades to legs-in-canonical. Its match-canonical partner is a Cohort-B gate (`CROSS_CHAIN_ROYALTY_ACTIVATION`, armed months earlier at BTC anchor 961000), preserving the canonical-first ordering. So Cohort A is diff --git a/protocol/reference-impl/consensus/equivocation_header.js b/protocol/reference-impl/consensus/equivocation_header.js index 48e11991..c043bdec 100644 --- a/protocol/reference-impl/consensus/equivocation_header.js +++ b/protocol/reference-impl/consensus/equivocation_header.js @@ -18,11 +18,15 @@ * EQUIV||||| * * The canonical source of record is - * xchain-documentation/protocol/reference-impl/equivocation_header.js; it is - * vendored BYTE-IDENTICALLY into xchain-hub, xchain-indexer, xchain-explorer, - * xchain-sdk and xchain-sync. Every PBFT/consensus engine prefixes its canonical - * through here, the settlement gates (cross_settle, xexec, xcall, anchor, price, - * attest) and the recovery verifier re-derive it to re-verify quorum signatures, + * xchain-indexer/src/consensus/equivocation_header.js; it is vendored + * BYTE-IDENTICALLY into xchain-hub, xchain-explorer, xchain-sdk, xchain-sync and + * xchain-documentation/protocol/reference-impl/consensus/equivocation_header.js. + * Edit the indexer copy only and re-run reconcile-twins.sh to re-vendor every + * other copy; never edit a vendored copy. + * + * Every PBFT/consensus engine prefixes its canonical through here, the settlement + * gates (cross_settle, xexec, xcall, anchor, price, attest) and the recovery + * verifier re-derive it to re-verify quorum signatures, * and the SLASH v0 action verifies equivocation proofs against it. Adding `` * makes equivocation provable WITHOUT false-positiving honest view changes (which * re-sign different content for the same round under a different view). Equivocation @@ -38,8 +42,9 @@ * the same anchor. The cross-service conformance suite * (ConsensusPrimitiveConformance.test.js, driven by * xchain-documentation/protocol/test-vectors) runs in every repo and asserts BOTH the - * behavior (canonical vectors) AND byte-identity of the local copy to this canonical - * source, so any unmirrored edit fails CI everywhere (a divergence forks the chain). + * behavior (canonical vectors) AND byte-identity of the local copy to the + * xchain-documentation copy, so any unmirrored edit fails CI everywhere (a + * divergence forks the chain). * ********************************************************************/ diff --git a/protocol/reference-impl/consensus/gate_registry/shared_rows_1.js b/protocol/reference-impl/consensus/gate_registry/shared_rows_1.js index bf311be0..568d8ff5 100644 --- a/protocol/reference-impl/consensus/gate_registry/shared_rows_1.js +++ b/protocol/reference-impl/consensus/gate_registry/shared_rows_1.js @@ -295,11 +295,11 @@ addGate('anchor_reward_activation.ANCHOR_ATTEST_ARRIVAL_MARGIN_S', 'constant', 6 // that window. addGate('anchor_reward_activation.ANCHOR_ATTEST_BARRIER_ACTIVATION', 'height', { mainnet: null, // INERT under the 2026-08-29 mainnet write hold - // SIZED 2026-09-16 20:41Z, on the BTC clock because this member is BTC-only: the same - // instant as the family's BTC CONSUMER height, so the one member that keeps BOTH - // certificates gains them together rather than carrying a lone extra rule for 6 h. - // Above the same roll and the same epoch close; the canon carries the measurement. - testnet: 153266, + // SIZED 2026-09-16 20:41Z, RE-SLID 2026-09-19 and 2026-09-23, on the BTC clock because this + // member is BTC-only: the same instant as the family's BTC CONSUMER height, so the one + // member that keeps BOTH certificates gains them together rather than carrying a lone extra + // rule for 6 h. The canon carries the measurement. + testnet: 154291, regtest: UNPINNED, // shares the family's arming seam so one venue lever arms both }); diff --git a/protocol/reference-impl/consensus/gate_registry/shared_rows_2.js b/protocol/reference-impl/consensus/gate_registry/shared_rows_2.js index c7fd765d..0063a165 100644 --- a/protocol/reference-impl/consensus/gate_registry/shared_rows_2.js +++ b/protocol/reference-impl/consensus/gate_registry/shared_rows_2.js @@ -216,9 +216,9 @@ addGate('mirror_admission_activation.MIRROR_ADMISSION_ACTIVATION', 'height', { 'BTC:mainnet': null, 'LTC:mainnet': null, 'DOGE:mainnet': null, - 'BTC:testnet': 153222, // SIZED 2026-09-16 20:41Z: epoch close 153,216 + 6 buried; tip 152,756 + 466 at 498.7 s/blk, about 64.5 h - 'LTC:testnet': 4891504, // RE-CUT 2026-09-17 22:45Z onto that instant: tip 4,889,190 + 2314 at 82.5 s/blk - 'DOGE:testnet': 67911796, // RE-CUT 2026-09-17 22:45Z onto that instant: tip 67,904,912 + 6884 at 27.7 s/blk + 'BTC:testnet': 154234, // RE-SLID 2026-09-23: train 154,074 + 160 blocks (17 h at the 383.04 s/blk bound, 25.6 h at the 575.89 s/blk 84 h trailing mean), the v0.20.1 patch reslide + 'LTC:testnet': null, // disabled for v0.20.1, 2026-09-18: LTC:testnet mirror admission ships null on this train; arms on a later train + 'DOGE:testnet': 67936053, // RE-SLID 2026-09-23: tip 67,924,397 at 17:48Z + 11656 blocks (83.8 h at 25.89 s/blk, the 84 h trailing mean, the same instant as the BTC producer), the v0.20.1 patch reslide 'BTC:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration 'LTC:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration 'DOGE:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration @@ -228,9 +228,9 @@ addGate('mirror_admission_activation.MIRROR_ADMISSION_CONSUMER_ACTIVATION', 'hei 'BTC:mainnet': null, 'LTC:mainnet': null, 'DOGE:mainnet': null, - 'BTC:testnet': 153266, // its producer + 44 blocks, about 6 h: strictly above, never equal - 'LTC:testnet': 4891766, // its producer + 262 blocks, about 6 h at 82.5 s/blk - 'DOGE:testnet': 67912575, // its producer + 779 blocks, about 6 h at 27.7 s/blk + 'BTC:testnet': 154291, // RE-SLID 2026-09-23: its producer + 57 blocks (6 h at the 383.04 s/blk bound, 9.1 h at the 575.89 s/blk 84 h trailing mean), strictly above, never equal + 'LTC:testnet': null, // disabled for v0.20.1, 2026-09-18: LTC:testnet mirror admission ships null on this train; arms on a later train + 'DOGE:testnet': 67936888, // RE-SLID 2026-09-23: its producer + 835 blocks (6 h at 25.89 s/blk, the 84 h trailing mean), strictly above, never equal 'BTC:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration 'LTC:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration 'DOGE:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration diff --git a/protocol/reference-impl/consensus/gate_registry/shared_rows_5.js b/protocol/reference-impl/consensus/gate_registry/shared_rows_5.js index cbf28976..fde6a96c 100644 --- a/protocol/reference-impl/consensus/gate_registry/shared_rows_5.js +++ b/protocol/reference-impl/consensus/gate_registry/shared_rows_5.js @@ -108,9 +108,16 @@ addGate('train_activation.TRAIN_ACTIVATION', 'ruleset', { // the LTC leg of this family two days off its BTC counterpart a day after the first cut. // That lead is the rolling-upgrade window the fleet roll must finish inside (24x the 90 // minute roll budget), and every testnet mirror-admission height sits above it on the same - // BTC clock (the BTC producer at 153,222 is 106 blocks and about 17.0 h further up), so a - // node lacking this rule set halts before it can grade an admission-stamped row. - '0.20.0': { mainnet: 9999999999, testnet: 153116, regtest: 0 }, + // BTC clock, so a node lacking this rule set halts before it can grade an admission-stamped + // row. + // RE-SLID 2026-09-23 for the v0.20.1 patch train, after the live tips overran the + // 2026-09-19 slide before the freeze: margin is 40 h to the nearest armed height, converted + // at each coin's fastest defensible cadence. Chain_tip TBTC 153,698 at 2026-09-23T15:55Z + // + 376 blocks, ceil(40 h / 383.04 s per block, the least-squares bound). The + // mirror-admission family below re-slides onto the same instant plus its own 17 h and 6 h + // offsets. LTC:testnet mirror admission ships disabled on this train and is + // untouched by this reslide; it arms on a later train. + '0.20.0': { mainnet: 9999999999, testnet: 154074, regtest: 0 }, }); // xchain_bridge_activation diff --git a/protocol/reference-impl/consensus/snapshot_reorg_buffer.js b/protocol/reference-impl/consensus/snapshot_reorg_buffer.js index 28ea9dad..0f000387 100644 --- a/protocol/reference-impl/consensus/snapshot_reorg_buffer.js +++ b/protocol/reference-impl/consensus/snapshot_reorg_buffer.js @@ -62,11 +62,14 @@ * difference must not be "corrected" without its own flag-day. * * The canonical source of record is - * xchain-documentation/protocol/reference-impl/snapshot_reorg_buffer.js; it is - * vendored BYTE-IDENTICALLY into xchain-hub, xchain-indexer and xchain-sdk. The - * cross-service conformance suite (ConsensusPrimitiveConformance.test.js) runs in - * every one of those repos and asserts byte-identity of the local copy to this - * source, so an unmirrored edit fails CI everywhere. + * xchain-indexer/src/consensus/snapshot_reorg_buffer.js; it is vendored + * BYTE-IDENTICALLY into xchain-hub, xchain-sdk and + * xchain-documentation/protocol/reference-impl/consensus/snapshot_reorg_buffer.js. + * Edit the indexer copy only and re-run reconcile-twins.sh to re-vendor every + * other copy; never edit a vendored copy. The cross-service conformance suite + * (ConsensusPrimitiveConformance.test.js) runs in every one of those repos and + * asserts byte-identity of the local copy to the xchain-documentation copy, so + * an unmirrored edit fails CI everywhere. * ********************************************************************/ @@ -74,8 +77,23 @@ const { get, copy, activeAt } = require('./gate_registry'); +// The reorg-depth buffer every party in a federation must resolve capability +// snapshots at. 6 = the BTC confirmation depth the platform already treats as +// buried (XCHAIN_CONFIRMATIONS_BTC). CONSENSUS-CRITICAL: the hub subtracts this +// before every snapshot lookup and refuses to boot on mainnet/testnet when a local +// override diverges (CapabilitySnapshot._resolveReorgBuffer), so a verifier that +// buries by a different depth resolves a different set than the signer. const CANONICAL_REORG_BUFFER = copy('snapshot_reorg_buffer.CANONICAL_REORG_BUFFER'); +// Per-network activation height (LOCAL COPY of the canonical map maintained +// upstream, kept equal by the cross-service regression suite). Keyed on the +// BTC-anchored declared snapshot_block. +// +// ARMED at genesis on mainnet, testnet and regtest per the gate table: every +// network resolves through the buried height from block 0. Arming changes +// acceptance itself, so a one-sided or partially-rolled-out arm would fork the +// fleet rather than fix it; the gate table records the per-network ruling and +// the evidence for arming each network at genesis instead of a later height. const SNAPSHOT_BURIAL_ACTIVATION = copy('snapshot_reorg_buffer.SNAPSHOT_BURIAL_ACTIVATION'); // Whether a verifier must bury the declared snapshot_block before re-deriving the diff --git a/protocol/reference-impl/consensus/stake_weighted_quorum.js b/protocol/reference-impl/consensus/stake_weighted_quorum.js index 76fdd4ed..11665534 100644 --- a/protocol/reference-impl/consensus/stake_weighted_quorum.js +++ b/protocol/reference-impl/consensus/stake_weighted_quorum.js @@ -14,17 +14,20 @@ * * THE single, CONSENSUS-CRITICAL implementation of the stake-weighted quorum * predicate. The canonical source of record is - * xchain-documentation/protocol/reference-impl/stake_weighted_quorum.js; it is - * vendored BYTE-IDENTICALLY into xchain-hub, xchain-indexer, xchain-explorer, - * xchain-sdk and xchain-sync. Every PBFT tally engine, every settlement gate - * (cross_settle, xexec, xcall, anchor, price, attest), the recovery verifier and - * the client/explorer checkpoint verifiers resolve through this predicate so they - * can never drift; a divergence forks the chain. + * xchain-indexer/src/consensus/stake_weighted_quorum.js; it is vendored + * BYTE-IDENTICALLY into xchain-hub, xchain-explorer, xchain-sdk, xchain-sync and + * xchain-documentation/protocol/reference-impl/consensus/stake_weighted_quorum.js. + * Edit the indexer copy only and re-run reconcile-twins.sh to re-vendor every + * other copy; never edit a vendored copy. Every PBFT tally engine, every + * settlement gate (cross_settle, xexec, xcall, anchor, price, attest), the + * recovery verifier and the client/explorer checkpoint verifiers resolve through + * this predicate so they can never drift; a divergence forks the chain. * * The cross-service conformance suite (ConsensusPrimitiveConformance.test.js, * driven by xchain-documentation/protocol/test-vectors) runs in every repo and * asserts BOTH the behavior (canonical vectors) AND byte-identity of the local - * copy to this canonical source, so any unmirrored edit fails CI everywhere. + * copy to the xchain-documentation copy, so any unmirrored edit fails CI + * everywhere. * * Self-contained on mathjs bignumber (exact, never a JS double): the predicate is * a pure function with no injected utility instance, so it is identical in every diff --git a/protocol/taproot-envelope.md b/protocol/taproot-envelope.md index 2580016d..b61f27fe 100644 --- a/protocol/taproot-envelope.md +++ b/protocol/taproot-envelope.md @@ -64,13 +64,13 @@ An envelope whose format byte is not `0x00` is **not recognized at all** - invis ## Rules that decide whether an envelope is an action -These are consensus-relevant: every implementation must agree, or the fleet forks. All of them activate at the network's recognition height, and below that height a transaction parses exactly as it always did. +These are consensus-relevant: every implementation must agree, or the fleet forks. All of them activate at the network's recognition height (`ENVELOPE_RECOGNITION_ACTIVATION`), and below that height a transaction parses exactly as it always did. The one exception is the payload-free carrier case of the mixed-carrier rule, which has a second height of its own (`ENVELOPE_CARRIER_RECOGNITION_ACTIVATION`), described in that bullet below. Both maps are listed on [Flag-Day Values](./flag-days.md#canonical-activation-maps), and their values are in [`constants.js`](./constants.js). - The witness is read **from the end** of the stack (control block last, script second-to-last), per BIP341. - A reveal carrying a **BIP341 annex is never an envelope.** Policy rejects annexes today but consensus does not, so a miner-included annexed reveal must not be able to split implementations. - The envelope input must be **input 0**. Anywhere else, it is not an action. - **Two or more envelope inputs** in one transaction: not an action. -- An envelope **mixed with any other carrier** (an `XCHN` OP_RETURN, a chunk marker, MULTISIGN outputs): not an action. Deterministic refusal, not a preference between carriers. +- An envelope **mixed with any other carrier** (an `XCHN` OP_RETURN, a chunk marker, MULTISIGN outputs): not an action. Deterministic refusal, not a preference between carriers. Between the recognition height and the carrier height (`ENVELOPE_CARRIER_RECOGNITION_ACTIVATION`), a co-present carrier is detected by the payload bytes it contributes or by its chunk marker, so an `XCHN` OP_RETURN that deobfuscates to exactly the magic and nothing after it contributes nothing and does **not** block the envelope: the envelope is still the action. At and above the carrier height, any recognized carrier blocks the action whether or not it carries payload. The carrier height is unpinned (never active) on every mainnet, and genesis-active on testnet and regtest. - **Every payload element must be a data push, never a bare opcode.** A one-byte element in `0x01`-`0x10` or `0x81` canonicalizes to `OP_1`-`OP_16` / `OP_1NEGATE` and breaks the pattern, so the reveal is not an envelope. Encoders avoid producing that shape by rebalancing the final two pushes to `(n-1, 2)`; see [No payload push may canonicalize to a bare opcode](#no-payload-push-may-canonicalize-to-a-bare-opcode). - The reassembled payload has its own ceiling, `ENVELOPE_MAX_PAYLOAD` (390,000 bytes), measured **before** parse and **excluding** the envelope's own push framing. The ceiling is sized against transaction **weight**, not chosen as a round byte count. A payload filling the larger cap this constant carried before 2026-07-31 compiles to a reveal of 402,789 WU, over Bitcoin Core's `MAX_STANDARD_TX_WEIGHT` of 400,000 WU: the encoder builds it, the validator accepts it, and no node relays it. The current value leaves 7,050 WU of margin under the worst reveal shape, so an implementer sizing a cap of their own should derive it from weight rather than copy a byte count. Note this measures a different quantity from `MAX_ACTION_DATA_LENGTH`, which is framing-inclusive and still governs every legacy lane. diff --git a/protocol/test-vectors/activation_predicates.json b/protocol/test-vectors/activation_predicates.json new file mode 100644 index 00000000..ebc539ce --- /dev/null +++ b/protocol/test-vectors/activation_predicates.json @@ -0,0 +1,28 @@ +{ + "_description": "Canonical conformance vectors for the activation predicates used by stake-weighted quorum, equivocation headers, and snapshot burial. The expected results pin the boundaries and fail-closed behavior specified by protocol/constants.js and protocol/protocol-activation.md. A snapshotBlock object with special 'nan' or 'undefined' represents the corresponding JavaScript value, which JSON cannot encode directly.", + "isStakeWeightedQuorumActive": [ + { "name": "mainnet is inactive one block below the flag day", "snapshotBlock": 960999, "network": "mainnet", "expected": false }, + { "name": "mainnet activates at the flag day", "snapshotBlock": 961000, "network": "mainnet", "expected": true }, + { "name": "testnet is active from genesis", "snapshotBlock": 0, "network": "testnet", "expected": true }, + { "name": "a NaN snapshot block fails closed", "snapshotBlock": { "special": "nan" }, "network": "mainnet", "expected": false }, + { "name": "an unknown network fails closed", "snapshotBlock": 961000, "network": "unknown", "expected": false } + ], + "isEquivHeaderActive": [ + { "name": "mainnet is inactive one block below the flag day", "snapshotBlock": 960999, "network": "mainnet", "expected": false }, + { "name": "mainnet activates at the flag day", "snapshotBlock": 961000, "network": "mainnet", "expected": true }, + { "name": "regtest is active from genesis", "snapshotBlock": 0, "network": "regtest", "expected": true }, + { "name": "a NaN snapshot block fails closed", "snapshotBlock": { "special": "nan" }, "network": "mainnet", "expected": false }, + { "name": "an unknown network fails closed", "snapshotBlock": 961000, "network": "unknown", "expected": false } + ], + "snapshotBurial": [ + { "name": "mainnet is active at its genesis boundary and clamps the buried height to zero", "snapshotBlock": 0, "network": "mainnet", "active": true, "buriedSnapshotBlock": 0 }, + { "name": "an active height inside the buffer clamps to zero", "snapshotBlock": 5, "network": "mainnet", "active": true, "buriedSnapshotBlock": 0 }, + { "name": "an active height above the buffer subtracts the canonical depth", "snapshotBlock": 7, "network": "mainnet", "active": true, "buriedSnapshotBlock": 1 }, + { "name": "null fails closed and is returned verbatim", "snapshotBlock": null, "network": "mainnet", "active": false, "buriedSnapshotBlock": null }, + { "name": "an empty string fails closed and is returned verbatim", "snapshotBlock": "", "network": "mainnet", "active": false, "buriedSnapshotBlock": "" }, + { "name": "a boolean fails closed and is returned verbatim", "snapshotBlock": false, "network": "mainnet", "active": false, "buriedSnapshotBlock": false }, + { "name": "undefined fails closed and is returned verbatim", "snapshotBlock": { "special": "undefined" }, "network": "mainnet", "active": false, "buriedSnapshotBlock": { "special": "undefined" } }, + { "name": "a NaN snapshot block fails closed and is returned verbatim", "snapshotBlock": { "special": "nan" }, "network": "mainnet", "active": false, "buriedSnapshotBlock": { "special": "nan" } }, + { "name": "an unknown network fails closed and returns the declared height", "snapshotBlock": 7, "network": "unknown", "active": false, "buriedSnapshotBlock": 7 } + ] +} diff --git a/protocol/test-vectors/taproot_envelope.json b/protocol/test-vectors/taproot_envelope.json index 241e0943..21ab9694 100644 --- a/protocol/test-vectors/taproot_envelope.json +++ b/protocol/test-vectors/taproot_envelope.json @@ -27,7 +27,7 @@ "control_block_hex": "c079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" }, "envelope_chunking": { - "description": "1,246-byte compiled payload split into 520-byte pushes, in order; reassembly by concatenation is byte-identical.", + "description": "1,253-byte compiled payload split into 520-byte pushes, in order; reassembly by concatenation is byte-identical.", "payload_generation": "action \"FILE|0|chunks.bin|application/octet-stream|||||||\" + 1200 rawData bytes where byte[i] = (i*7+13) & 0xff", "compiled_payload_sha256": "ae00e58a01ed2978a7c0226399cb944f3736874114103f62b8471aced34308a0", "compiled_payload_length": 1253, @@ -114,9 +114,14 @@ }, { "name": "mixed_carrier_envelope_plus_op_return", - "description": "A tx containing an envelope reveal input AND an OP_RETURN XCHN payload is NOT a valid action (spec §3.8, height-gated: below the recognition flag height it replays exactly as the fleet indexed it, i.e. as the OP_RETURN action).", + "description": "A tx containing an envelope reveal input AND an OP_RETURN XCHN payload is NOT a valid action (spec §3.8, height-gated: below the recognition flag height it replays exactly as the fleet indexed it, i.e. as the OP_RETURN action). The OP_RETURN here carries payload bytes after the magic; the marker-only shape is the next vector, under its own height.", "expect": "no_action_post_flag__shipped_behavior_pre_flag" }, + { + "name": "mixed_carrier_envelope_plus_marker_only_op_return", + "description": "Envelope reveal input plus an XCHN OP_RETURN that deobfuscates to exactly the magic and nothing after it. The OP_RETURN contributes zero payload bytes, so from the recognition height up to ENVELOPE_CARRIER_RECOGNITION_ACTIVATION the envelope is still accepted as the action; at and above the carrier height the tx is NOT a valid action (spec §3.8). Carrier height: unpinned on every mainnet, genesis-active on BTC/LTC testnet and regtest.", + "expect": "no_action_post_carrier_flag__envelope_action_pre_carrier_flag" + }, { "name": "mixed_carrier_envelope_plus_chunk_marker", "description": "Envelope input plus P2SH/P2WSH chunk marker: no action post-flag.", diff --git a/protocol/token-information-standard.md b/protocol/token-information-standard.md index 2a329a88..6d99410f 100644 --- a/protocol/token-information-standard.md +++ b/protocol/token-information-standard.md @@ -59,13 +59,17 @@ marked *(since v1.1.0)* are absent from the v1.0.0 schema. #### File Entry Fields -Entries inside the `files`, `audio`, `video`, and `images` arrays can carry the following fields: +Entries inside the `files`, `audio`, `video`, and `images` arrays can carry the following fields. +A row marked *(images only)* is declared on `images` entries alone; every other row applies to +all four arrays. | Field | Type | Description | :--- | :--- | :--- | data | String | URL to the file (off-chain). Used for non-gated content. | data_ref | String | *(since v1.1.0)* Reference to an on-chain [`FILE`](./actions/file.md) action by `ACTION_INDEX`: `action:` (same chain as the token) or `action::` (sibling chain: base coin ticker `BTC`/`LTC`/`DOGE`, network tier implied by the token's network, same convention as [`LINK`](./actions/link.md)'s `COIN1`/`COIN2`). Lets cheap chains carry the bytes for tokens on expensive ones: e.g. a BTC token whose artwork FILE lives on DOGE. When both `data` and `data_ref` are present, clients prefer `data_ref`. | name | String | Filename +| hash | String | (Optional) A sha256 hash of the file. 64 characters max. +| size | String | *(images only)* (Optional) The image's pixel dimensions, such as `48x48`, or `svg` for an SVG image. Read together with `type` to pick a token icon. | type | String | Entry classification, drawn from the vocabulary of the array the entry sits in, and NOT a MIME type. `images`: display role, one of `icon`, `standard`, `large`, `hires`, paired with `size` so clients can pick a token icon. `audio`: container, one of `m4a`, `mp3`, `wav`. `video`: container, one of `mp4`, `mov`, `wmv`. `files`: free-form category such as `doc`, `pdf`, `xls`, `other`. The schema pins the first three lists as enums and leaves the `files` vocabulary open. The media type of the bytes comes from elsewhere: a `data_ref` entry inherits it from the referenced [`FILE`](./actions/file.md) action's `TYPE`, and a `data` URL from the server's `Content-Type`. | title | String | *(since v1.1.0)* Display title | locked | Boolean | *(since v1.1.0)* `true` if the file is encrypted and gated. Clients use this to render locked/unlocked states without first fetching the FILE action. diff --git a/protocol/xchain-bridge.md b/protocol/xchain-bridge.md index 93429ad2..86b88837 100644 --- a/protocol/xchain-bridge.md +++ b/protocol/xchain-bridge.md @@ -133,9 +133,11 @@ distributing XCHAIN to another chain is an ordinary treasury operation on the br (mint on BTC, lock to an operator-held address on the destination, `AIRDROP` there), not a protocol-level concern. -Once the row exists, handlers that resolve XCHAIN unconditionally (guard-gas reservations, fee -mode detection) start seeing it where they previously saw nothing; see -[Gas and Fees](../concepts/gas.md) for the fee side of that boundary. +Once the row exists, handlers that resolve XCHAIN unconditionally (fee mode detection) start +seeing a row that did not exist until the first credit; see [Gas and Fees](../concepts/gas.md) +for the fee side of that boundary. The controller-guard gas reservation is not one of them: it is keyed on +the chain, so it stays BTC-only and the new row moves no guard verdict (see +[Controller-Bound Tokens](./controller-bound-tokens.md#gas)). ## Reads diff --git a/test/action-count-claims.test.js b/test/action-count-claims.test.js index 124402fa..55142ffa 100644 --- a/test/action-count-claims.test.js +++ b/test/action-count-claims.test.js @@ -20,14 +20,14 @@ * the SDK's own ACTIONS and SESSIONS pages, which claimed the SDK supported 30 * action types while documenting all 31 of them on the same page. * - * The trap this guard is built around: 36 and 37 are BOTH correct, for + * The trap this guard is built around: 37 and 38 are BOTH correct, for * different sets, and a sweep that flattens them makes the docs worse. * - * 37 named ACTIONs every spec in protocol/actions/ - * 36 wire-decoded ACTIONs the above minus XCALL, which is mirror-injected + * 38 named ACTIONs every spec in protocol/actions/ + * 37 wire-decoded ACTIONs the above minus XCALL, which is mirror-injected * into the destination chain's index rather than * decoded from a transaction - * 31 user-submittable the above minus the five validator/system + * 32 user-submittable the above minus the five validator/system * actions; equals the SDK's builder methods * * WHAT IT CHECKS: @@ -81,11 +81,16 @@ function namedActions() { * handlers), which is what the old "51 handler files" tally was counting. */ /* - * The wire-decoded count (37 minus XCALL) is correct only where the text is - * genuinely talking about what comes off a transaction. Everywhere else a 35 is - * a pre-XCALL leftover, which is what it was on four pages in the 07-29 sweep. - * So this count is allowed only where somebody has said why, rather than being - * a default. A page that means "every ACTION" should say 37. + * The wire-decoded count (named minus XCALL) is correct only where the text is + * genuinely talking about what comes off a transaction. Everywhere else it is + * usually a leftover from before the newest ACTION, which is what it was on four + * pages in the 07-29 sweep. So this count is allowed only where somebody has + * said why, rather than being a default. A page that means "every ACTION" should + * use the named count. + * + * Each claim's leading number must equal the derived wire count, so a + * registered sentence goes red when a new ACTION moves that count instead of + * blessing the old number by exact string. * * Registered per CLAIM and per occurrence count, not per file. A file-level * allowlist would let a NEW bare 35 slip into any page that already had a @@ -95,19 +100,19 @@ function namedActions() { * itself an error, so a claim that gets deleted takes its entry with it. */ const WIRE_SCOPED = [ - { file: 'components/decoder/configuration.md', claim: '36 ACTION names', count: 1, + { file: 'components/decoder/configuration.md', claim: '37 ACTION names', count: 1, why: 'the decoder only ever sees the wire-decoded names' }, - { file: 'concepts/actions.md', claim: '36 ACTION types', count: 1, + { file: 'concepts/actions.md', claim: '37 ACTION types', count: 1, why: 'defines the wire-decoded set, and says so on the same line' }, - { file: 'concepts/actions.md', claim: '36 wire-decoded ACTION types', count: 1, + { file: 'concepts/actions.md', claim: '37 wire-decoded ACTION types', count: 1, why: 'explains XCALL sitting outside that set' }, - { file: 'getting-started/what-is-xchain.md', claim: '36 ACTION commands', count: 1, + { file: 'getting-started/what-is-xchain.md', claim: '37 ACTION commands', count: 1, why: 'the page presents the wire-decoded set and documents XCALL separately' }, - { file: 'getting-started/what-is-xchain.md', claim: '36 ACTIONs', count: 2, + { file: 'getting-started/what-is-xchain.md', claim: '37 ACTIONs', count: 2, why: 'the section heading, and the five validator/system actions within that set' }, - { file: 'getting-started/what-is-xchain.md', claim: '36 actions', count: 2, - why: 'chain parity, and the 31-of-36 developer-invocable split' }, - { file: 'getting-started/what-is-xchain.md', claim: '36 wire-decoded ACTIONs', count: 1, + { file: 'getting-started/what-is-xchain.md', claim: '37 actions', count: 2, + why: 'chain parity, and the 32-of-37 developer-invocable split' }, + { file: 'getting-started/what-is-xchain.md', claim: '37 wire-decoded ACTIONs', count: 1, why: 'names the scope explicitly where XCALL is introduced' }, ]; @@ -121,7 +126,7 @@ const SCOPED = [ why: 'handler classes in xchain-indexer src/actions/index.js, not the ACTION set; ' + 'requires, instantiations and dispatch cases all counted 48 on 2026-08-06' }, { file: 'components/indexer/actions.md', claim: '21 actions', count: 1, - why: 'the subset registered at protocol version 1.0.0; 21 + 16 = 37 on the same page' }, + why: 'the subset registered at protocol version 0.1.0; 21 + 17 = 38 on the same page' }, { file: 'components/vm/architecture.md', claim: '19 action types', count: 1, why: 'emit-API types the VM can construct; verified against gateway-emit.js 2026-07-29' }, { file: 'components/wallet/ux.md', claim: '5 actions', count: 1, @@ -137,7 +142,7 @@ const SCOPED = [ // ORDER of the noun alternatives: they are longest-first, because `ACTIONs?` // placed first would match inside "36 ACTION names" and every claim would // report as the uninformative "35 ACTION". -const CLAIM = /\b(\d{1,3}(?:,\d{3})*)\s+(?:standard |named |core |on-chain |protocol |wire-decoded |different |registered )?(?:ACTION commands?|ACTION definitions?|ACTION types?|ACTION names?|action types?|ACTIONs?|actions?)\b/g; +const CLAIM = /\b(\d{1,3}(?:,\d{3})*)\s+(?:standard |named |core |on-chain |protocol |wire-decoded |different |registered |developer-invocable |user-submittable |user-encodable |supported )?(?:ACTION commands?|ACTION definitions?|ACTION types?|ACTION names?|action types?|ACTIONs?|actions?)\b/g; function markdownFiles() { const out = []; @@ -166,6 +171,12 @@ test('every ACTION count in the prose refers to a set that exists', () => { const wire = named - MIRROR_INJECTED.length; const submittable = wire - NOT_USER_SUBMITTABLE.length; const allowed = new Set([named, submittable]); + // Pin every wire-scoped claim to the derived wire count, never to a typed number. + const offCount = WIRE_SCOPED.filter((s) => Number(s.claim.match(/^\d+/)[0]) !== wire) + .map((s) => `${s.file}|${s.claim}`); + assert.deepStrictEqual(offCount, [], + `WIRE_SCOPED claims must carry the wire-decoded count ${wire}; update the prose and the entry:\n` + + offCount.join('\n')); // Each registered wire-decoded claim is spent as it is matched, so an extra // occurrence of an otherwise-legitimate sentence is still caught. const budget = new Map([...WIRE_SCOPED, ...SCOPED].map((s) => [`${s.file}|${s.claim}`, s.count])); diff --git a/test/bridge-proof-origin-indexer-doc.test.js b/test/bridge-proof-origin-indexer-doc.test.js new file mode 100644 index 00000000..9a1c578a --- /dev/null +++ b/test/bridge-proof-origin-indexer-doc.test.js @@ -0,0 +1,49 @@ +/********************************************************************* + * + * 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. + * + **********************************************************************/ + +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); +const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8').replace(/\s+/g, ' '); + +const DOCS = [ + 'components/indexer/configuration.md', + 'operations/run-a-validator.md', +]; + +const DIRECTIONAL_REQUIREMENT = + "Every destination indexer crediting a bridged transfer must have the origin chain's indexer API URL configured as `_INDEXER_URL` or `_INDEXER_API_URL`."; +const HOLD_BEHAVIOR = + 'Without either URL, that destination holds silently at the bridge proof barrier until the default 900-second (15-minute) hold ceiling; reaching the ceiling can re-drive the wait but never credits an unproven transfer.'; + +for (const file of DOCS) { + test(`${file} states the directional origin-indexer bridge proof requirement`, () => { + const text = read(file); + assert.ok( + text.includes(DIRECTIONAL_REQUIREMENT), + `${file} must state that every crediting destination indexer needs the origin indexer URL` + ); + }); + + test(`${file} states the silent 900-second bridge proof hold`, () => { + const text = read(file); + assert.ok( + text.includes(HOLD_BEHAVIOR), + `${file} must state that a missing origin URL holds silently at the bridge proof barrier until the 900-second ceiling without crediting` + ); + }); +} diff --git a/test/complete_run_reporter.test.js b/test/complete_run_reporter.test.js index bd2ec48c..4c2f854d 100644 --- a/test/complete_run_reporter.test.js +++ b/test/complete_run_reporter.test.js @@ -1,9 +1,21 @@ +/********************************************************************* + * + * Copyright © 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 run in which a file's child exited 0 before its event stream was whole + * must not grade green: proved over synthetic streams and by driving a real + * runner over a throwaway file that exits mid-suite. + */ 'use strict'; -// A run in which a file's child exited 0 before its event stream was whole -// must not grade green: proved over synthetic streams and by driving a real -// runner over a throwaway file that exits mid-suite. - const assert = require('node:assert/strict'); const { test, describe } = require('node:test'); const fs = require('node:fs'); diff --git a/test/error-code-registry-coverage.test.js b/test/error-code-registry-coverage.test.js index 8f6e5d4e..0ecdbf2e 100644 --- a/test/error-code-registry-coverage.test.js +++ b/test/error-code-registry-coverage.test.js @@ -23,9 +23,14 @@ * This test re-derives the emitted set from the explorer source on every run and * fails naming any code that has no registry row. * - * Collection is by `code:` line rather than by a bare literal scan, so the - * ternary fallback form (`code: cond ? x : 'READ_FAILED'`) is caught alongside - * the plain `code: 'X'` form. Only the three REST-side sources are read: + * Collection is by emit shape rather than by a bare literal scan. Three shapes + * are read: a `code:` line, so the ternary fallback form + * (`code: cond ? x : 'READ_FAILED'`) is caught alongside the plain `code: 'X'` + * form; a status-map row (`CODE: [409, 'message']`), which the proof routes + * declare and then send by reference as `code: code`; and a positional helper + * call (`error(res, 503, 'message', 'CODE')`), which the bridge panel routes + * use. A new emit shape needs its own rule here, or its codes go unchecked. + * Only the three REST-side sources are read: * src/ws/ carries the WebSocket channel codes, which are a separate surface * documented in components/explorer/websocket.md and explicitly excluded by the * registry page's own closing note. @@ -92,8 +97,17 @@ const noExplorer = sibling('xchain-explorer').skip; // otherwise read that event name as a REST code the registry owes a row. const LOGGER_EVENT = /\blog\.(?:trace|debug|info|warn|error|fatal)\(\s*'[A-Z][A-Z0-9_]*'/g; -// Every SCREAMING_SNAKE string literal on a line that also carries `code:`, -// minus a logger event name leading that line's log call. +// Key of a `CODE: [status, 'message']` status-map row, wherever it sits on the line +// (the first key shares its line with `let map = {`). The three-digit status keeps +// non-code constants such as `ROLES: ['EXPLORER']` out. +const STATUS_MAP_KEY = /\b([A-Z][A-Z0-9_]{2,})\s*:\s*\[\s*[0-9]{3}\s*,/g; + +// A positional error helper called as `helper(res, status, message, 'CODE')`. +const STATUS_HELPER_CALL = /\(\s*res\s*,\s*[0-9]{3}\s*,/; + +// Every SCREAMING_SNAKE string literal on a line that carries `code:` or a +// positional helper call, plus every status-map key, minus a logger event name +// leading that line's log call. function emittedCodes() { const found = new Map(); const files = SOURCES.slice(); @@ -108,13 +122,13 @@ function emittedCodes() { path.relative(EXPLORER, file) + ' is gone from xchain-explorer; repoint SOURCES at the file the emit site moved to'); const lines = fs.readFileSync(file, 'utf8').split('\n'); lines.forEach((line, i) => { - if (!line.includes('code:')) return; + const where = `${path.basename(file)}:${i + 1}`; + const record = (code) => { if (!found.has(code)) found.set(code, where); }; const emitted = line.replace(LOGGER_EVENT, ''); - for (const quoted of emitted.match(/'([A-Z][A-Z0-9_]{2,})'/g) || []) { - const code = quoted.slice(1, -1); - if (!found.has(code)) - found.set(code, `${path.basename(file)}:${i + 1}`); - } + for (const m of emitted.matchAll(STATUS_MAP_KEY)) record(m[1]); + if (!line.includes('code:') && !STATUS_HELPER_CALL.test(line)) return; + for (const quoted of emitted.match(/'([A-Z][A-Z0-9_]{2,})'/g) || []) + record(quoted.slice(1, -1)); }); } return found; diff --git a/test/flag-day-literals.test.js b/test/flag-day-literals.test.js index 492b8237..20f30c8f 100644 --- a/test/flag-day-literals.test.js +++ b/test/flag-day-literals.test.js @@ -117,6 +117,38 @@ test('the generated flag-day page matches the indexer registry', { skip: noIndex ); }); +test('the canonical activation-map index names every newly published gate', () => { + const names = gen.collectCanonicalActivationMaps(); + for (const name of [ + 'AMOUNT_REPRESENTABILITY_ACTIVATION', + 'DISPENSER_FRESHNESS_SHAPE_ACTIVATION', + 'PRICE_ZERO_VALIDITY_ACTIVATION', + 'PRICE_BATCHING_FLOOR_ACTIVATION', + ]) { + assert.ok(names.includes(name), `${name} is missing from the canonical activation-map index`); + } +}); + +test('every newly published activation map is value-identical to the indexer registry', { skip: noIndexer }, () => { + const canonical = require('../protocol/constants.js'); + const registry = require(gen.REGISTRY); + const rows = { + AMOUNT_REPRESENTABILITY_ACTIVATION: + 'amount_representability_activation.AMOUNT_REPRESENTABILITY_ACTIVATION', + DISPENSER_FRESHNESS_SHAPE_ACTIVATION: + 'dispenser_freshness_shape_activation.DISPENSER_FRESHNESS_SHAPE_ACTIVATION', + PRICE_ZERO_VALIDITY_ACTIVATION: + 'price_zero_validity_activation.PRICE_ZERO_VALIDITY_ACTIVATION', + PRICE_BATCHING_FLOOR_ACTIVATION: + 'price_batching_floor_activation.PRICE_BATCHING_FLOOR_ACTIVATION', + }; + + for (const [name, key] of Object.entries(rows)) { + assert.deepStrictEqual(canonical[name], registry.get(key), + `${name} has drifted from the indexer activation registry`); + } +}); + test('all three gate-collection paths still find their gates', { skip: noIndexer }, () => { // collectGates reads the registry with two independent regexes and then // scans the sibling `*_activation.js` modules, and the check above cannot diff --git a/test/layout-doc-currency.test.js b/test/layout-doc-currency.test.js index 7fbe3b46..a2beefa5 100644 --- a/test/layout-doc-currency.test.js +++ b/test/layout-doc-currency.test.js @@ -135,7 +135,7 @@ function markdownFiles() { * its exemption with it. Empty today, and that is a fact about the repo rather * than a shortcut: every digit-form component claim currently counts the * documented set. Release-train and library counts are spelled out in words - * ("nine components", "five component libraries"), which the digit-form regex + * ("thirteen components", "five component libraries"), which the digit-form regex * below does not reach, and they count release scope rather than the docs set. */ const SCOPED = []; diff --git a/test/node-runtime-rationale.test.js b/test/node-runtime-rationale.test.js new file mode 100644 index 00000000..4055cdf5 --- /dev/null +++ b/test/node-runtime-rationale.test.js @@ -0,0 +1,61 @@ +/********************************************************************* + * + * 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. + * + ********************************************************************** + * + * Node 22 pin rationale. + * + * WHY. isolated-vm 6.2.0 ships prebuilt bindings per Node ABI, so Node 24 + * installs it without a compiler. What holds the platform on Node 22 is the + * consensus runtime: xchain-vm's src/consensus_runtime.js pins the Node ABI to + * 127 and checkConsensusRuntime() rejects any other. Pages that blame a + * compile failure send operators after a toolchain the install does not need + * and hide the check that actually fails them. + * + * WHAT IT CHECKS. No living page restates the compile rationale. CHANGELOG.md + * and operations/releases.md are history and are skipped. + */ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); +const HISTORY = new Set(['CHANGELOG.md', path.join('operations', 'releases.md')]); +const STALE = /cannot build (the (native )?)?`isolated-vm`|breaks native compilation|`isolated-vm` does not build on Node|`isolated-vm` requires native C\+\+ compilation/; + +function markdownFiles() { + const out = []; + (function walk(dir) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + if (e.name === 'node_modules' || e.name === '.git' || e.name === 'dist') continue; + const f = path.join(dir, e.name); + if (e.isDirectory()) walk(f); + else if (e.name.endsWith('.md')) out.push(f); + } + })(ROOT); + return out; +} + +test('no living page blames the Node 22 pin on an isolated-vm build failure', () => { + const files = markdownFiles(); + assert.ok(files.length > 50, 'found almost no markdown, so this guard is inert'); + const bad = []; + for (const file of files) { + const rel = path.relative(ROOT, file); + if (HISTORY.has(rel)) continue; + fs.readFileSync(file, 'utf8').split('\n').forEach((line, i) => { + if (STALE.test(line)) bad.push(`${rel}:${i + 1}`); + }); + } + assert.deepEqual(bad, [], + 'isolated-vm installs from a prebuilt binding on Node 22 and 24; the pin is the Node ABI 127 ' + + 'consensus-runtime check in xchain-vm (checkConsensusRuntime()). Say that instead:\n' + bad.join('\n')); +}); diff --git a/test/platform-map-integrity.test.js b/test/platform-map-integrity.test.js index 9a2b2644..0bab124b 100644 --- a/test/platform-map-integrity.test.js +++ b/test/platform-map-integrity.test.js @@ -113,6 +113,20 @@ describe('platform-map scrub', () => { } }); +describe('platform-map ANCHOR wire versions', () => { + // protocol/actions/anchor.md closes the version space at 0, 1 and 2; the + // retired wires reused those bytes, so a higher number names a shape that no longer exists. + for (const file of [HTML, JSON_FILE]) { + test(`${path.basename(file)} names only ANCHOR versions 0, 1 and 2`, () => { + const text = fs.readFileSync(file, 'utf8'); + const bad = [...text.matchAll(/ANCHOR (v[\d/v]+)/g)] + .filter((m) => m[1].split('/').some((v) => Number(v.replace(/^v/, '')) > 2)) + .map((m) => m[0]); + assert.deepEqual(bad, [], `${path.basename(file)}: retired ANCHOR versions ${bad.join(', ')}`); + }); + } +}); + describe('platform-map doc page wiring', () => { // The sidebar is generated from markdown pages only, so the map is // discoverable exactly as long as platform-map.md exists and embeds diff --git a/test/protocol-constant-claims.test.js b/test/protocol-constant-claims.test.js index 8df07a07..6912b046 100644 --- a/test/protocol-constant-claims.test.js +++ b/test/protocol-constant-claims.test.js @@ -211,8 +211,8 @@ const COMMAND_QUANTITY = /\b(\d{1,3}(?:,\d{3})+|\d+)\s*-?\s*commands?\b/gi; const COMMAND_SCOPED = [ { file: 'components/node/architecture.md', count: 21, times: 2, why: 'the xchain-node Commander CLI verb count, nothing to do with BATCH' }, - { file: 'getting-started/what-is-xchain.md', count: 35, times: 1, - why: 'the size of the ACTION set, nothing to do with BATCH' }, + { file: 'getting-started/what-is-xchain.md', count: 37, times: 1, + why: 'the size of the wire-decoded ACTION set, nothing to do with BATCH' }, ]; test('prose command counts for the BATCH cap match the canonical value', () => { diff --git a/test/reference-impl-gate-registry-entry.test.js b/test/reference-impl-gate-registry-entry.test.js new file mode 100644 index 00000000..3e3c1c7a --- /dev/null +++ b/test/reference-impl-gate-registry-entry.test.js @@ -0,0 +1,124 @@ +/********************************************************************* + * + * Copyright © 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. + * + ********************************************************************** + * + * The reference registry ENTRY (protocol/reference-impl/consensus/ + * gate_registry.js) assembles itself from load-for-effect part requires that + * nothing else validates, and it is not a twin, so no reconciler covers it. + * The vector suite reads only a handful of keys, all from two of the parts, so + * a dropped, duplicated or unwired part would ship a reference registry + * missing whole row families with every other test green. This file pins the + * assembled key set to the union the part files declare, plus the miss and + * regtest-arming read paths the entry re-exports. + * + * Run: node --test test/reference-impl-gate-registry-entry.test.js (Node 22) + */ +'use strict'; + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const CONSENSUS = path.join(__dirname, '..', 'protocol', 'reference-impl', 'consensus'); +const ENTRY_PATH = path.join(CONSENSUS, 'gate_registry.js'); +const PART_DIR = path.join(CONSENSUS, 'gate_registry'); +const PART_RE = /^shared_rows_\d+\.js$/; +const REQUIRE_RE = /^require\('\.\/gate_registry\/(shared_rows_\d+\.js)'\);$/gm; +const ADD_GATE_RE = /^addGate\('([^']+)'/gm; + +const entry = require(ENTRY_PATH); + +/** Part files on disk, in name order. */ +function partFiles() { + return fs.readdirSync(PART_DIR).filter((f) => PART_RE.test(f)).sort(); +} + +/** Keys a part file registers through top-level addGate() calls. */ +function partKeys(file) { + const src = fs.readFileSync(path.join(PART_DIR, file), 'utf8'); + return [...src.matchAll(ADD_GATE_RE)].map((m) => m[1]); +} + +/** Symmetric difference, for a failure message that names families, not ~90 strings. */ +function diff(actual, expected) { + const a = new Set(actual); + const e = new Set(expected); + return { + missing: expected.filter((k) => !a.has(k)), + unexpected: actual.filter((k) => !e.has(k)), + }; +} + +describe('reference registry ENTRY assembly', () => { + test('requires every part file on disk exactly once, and no part that is absent', () => { + const parts = partFiles(); + assert.ok(parts.length >= 1, `no shared_rows_N.js found under ${PART_DIR}: the part scan broke`); + const src = fs.readFileSync(ENTRY_PATH, 'utf8'); + const required = [...src.matchAll(REQUIRE_RE)].map((m) => m[1]); + const seen = new Map(); + for (const r of required) seen.set(r, (seen.get(r) || 0) + 1); + for (const [file, n] of seen) { + assert.equal(n, 1, `gate_registry.js requires ${file} ${n} times`); + assert.ok(parts.includes(file), `gate_registry.js requires ${file}, which is not on disk`); + } + for (const file of parts) { + assert.ok(seen.has(file), `${file} is on disk but gate_registry.js never requires it`); + } + }); + + test('the assembled keys are exactly the union the part files register', () => { + const owner = new Map(); + for (const file of partFiles()) { + const keys = partKeys(file); + assert.ok(keys.length > 0, `${file} yielded no addGate keys: the scan matched nothing`); + for (const key of keys) { + assert.ok(!owner.has(key), `${key} is registered by both ${owner.get(key)} and ${file}`); + owner.set(key, file); + } + } + const expected = [...owner.keys()].sort(); + const actual = [...entry.keys()].sort(); + const { missing, unexpected } = diff(actual, expected); + const families = [...new Set(missing.map((k) => owner.get(k)))]; + assert.deepEqual( + { missing, unexpected }, { missing: [], unexpected: [] }, + `the reference registry drifted from its part files; missing keys come from ${families.join(', ') || 'none'}`, + ); + assert.equal(actual.length, expected.length); + }); +}); + +describe('reference registry ENTRY reads', () => { + test('a miss throws RegistryMissError naming the key, and has() says false', () => { + const key = 'no_such_stem.NO_SUCH_KEY'; + assert.equal(entry.has(key), false); + assert.throws(() => entry.get(key), (err) => err instanceof entry.RegistryMissError && err.message.includes(key)); + }); + + test('a regtest-armed row follows process.env at read time', () => { + const key = 'rollcall_gates_activation.ROLLCALL_GATES_ACTIVATION'; + const envName = 'XC_ROLLCALL_GATES_REGTEST_ACTIVATION'; + const had = Object.prototype.hasOwnProperty.call(process.env, envName); + const saved = process.env[envName]; + try { + delete process.env[envName]; + assert.equal(entry.get(key).regtest, entry.UNPINNED, 'an unset venue must leave regtest unpinned'); + process.env[envName] = 'armed'; + assert.equal(entry.get(key).regtest, 0, 'the armed form resolves to the rule height'); + process.env[envName] = '7'; + assert.equal(entry.get(key).regtest, 7, 'a numeric form resolves to that height'); + } finally { + if (had) process.env[envName] = saved; + else delete process.env[envName]; + } + }); +}); diff --git a/test/release-train-roster.test.js b/test/release-train-roster.test.js new file mode 100644 index 00000000..57371593 --- /dev/null +++ b/test/release-train-roster.test.js @@ -0,0 +1,75 @@ +/********************************************************************* + * + * 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. + * + ********************************************************************** + * + * Release-train roster drift. + * + * WHY. operations/release-process.md is the page an operator reads while + * freezing and tagging a train, and its Train members row is the list the + * repo-scoped steps resolve through. The count there is spelled as a word, + * which the digit-form count guards do not reach, so a roster short of the + * real train stays green everywhere else. + * + * WHAT IT CHECKS. The Train members row names exactly the components in the + * newest version table of operations/releases.md, and the spelled-out count + * in the page's opening sentence matches the roster length. + */ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); +const PROCESS = path.join(ROOT, 'operations', 'release-process.md'); +const RELEASES = path.join(ROOT, 'operations', 'releases.md'); + +const WORDS = { + eight: 8, nine: 9, ten: 10, eleven: 11, twelve: 12, thirteen: 13, + fourteen: 14, fifteen: 15, sixteen: 16, seventeen: 17, eighteen: 18, +}; + +function rosterRow(md) { + const row = md.split('\n').find((l) => l.startsWith('| **Train members** |')); + assert.ok(row, 'release-process.md has no Train members row'); + return [...row.matchAll(/`(xchain-[a-z0-9-]+)`/g)].map((m) => m[1]).sort(); +} + +function newestVersionTable(md) { + const lines = md.split('\n'); + const start = lines.findIndex((l) => /^\| Component \| Version \|/.test(l)); + assert.ok(start >= 0, 'releases.md has no Component/Version table'); + const out = []; + for (let i = start + 2; i < lines.length && lines[i].startsWith('|'); i++) { + const m = lines[i].match(/^\| (xchain-[a-z0-9-]+) \|/); + if (m) out.push(m[1]); + } + return out.sort(); +} + +test('the Train members row matches the newest release table', () => { + const roster = rosterRow(fs.readFileSync(PROCESS, 'utf8')); + const table = newestVersionTable(fs.readFileSync(RELEASES, 'utf8')); + assert.ok(roster.length > 0, 'parsed an empty Train members row, so this guard is inert'); + assert.ok(table.length > 0, 'parsed an empty release table, so this guard is inert'); + const missing = table.filter((c) => !roster.includes(c)); + const extra = roster.filter((c) => !table.includes(c)); + assert.deepEqual({ missing, extra }, { missing: [], extra: [] }, + 'release-process.md Train members disagrees with the newest table in releases.md'); +}); + +test('the spelled-out train size matches the roster', () => { + const md = fs.readFileSync(PROCESS, 'utf8'); + const m = md.match(/ships as a \*\*release train\*\*: (\w+) components/); + assert.ok(m, 'release-process.md no longer states the train size in its opening sentence'); + const n = WORDS[m[1].toLowerCase()]; + assert.ok(n, `unrecognised count word "${m[1]}"; extend WORDS rather than pass vacuously`); + assert.equal(n, rosterRow(md).length, `"${m[1]} components" does not match the Train members row`); +}); diff --git a/test/settlement-and-delivery-claims.test.js b/test/settlement-and-delivery-claims.test.js index 4944da12..bf8ceb3d 100644 --- a/test/settlement-and-delivery-claims.test.js +++ b/test/settlement-and-delivery-claims.test.js @@ -35,6 +35,14 @@ * native-coin fee output is rejected. bet.js resolves the payment mode * only when the computed fee is above zero, and a market inside the * duration-fee free window computes to zero. + * 6. An open order's escrow was said to reach only two addresses, the + * counterparty's or your own. order_match settles each side's released + * escrow as the OTHER order's proceeds and applies that order's stored + * royalty/fee legs to it, so a buyer of a controller-bound token pays + * part of the escrowed price to the legs' addresses. + * 7. The cross-chain guide dismissed bridges and never named XBRIDGE. Its + * bridge section says XCHAIN-only, off on mainnet, and hub-trusted, each + * true only while the activation gates it cites stay where they are. * * WHAT IT CHECKS. Both halves of every claim: the SOURCE fact the corrected * wording rests on, read out of the sibling indexer, and the PROSE, which must @@ -310,3 +318,58 @@ test('the betting guide ties the fee-output requirement to a fee being owed', () assert.match(betting, /free window owes nothing/, 'betting.md no longer says a market inside the free window needs no fee output'); }); + +/* 7. A released escrow funds the counterparty's royalty/fee legs. */ + +test('the escrow-destination source facts still hold', { skip: skipNoIndexer }, () => { + const match = readSrc('actions/order_match.js'); + + assert.match(match, /escrows\.push\(\[matchInfo\['GET_TICK'\],[^\n]*give_amount[^\n]*matchInfo\['GET_ADDRESS'\]\]\)/, + 'order_match no longer releases the order\'s give-side escrow to the matching order'); + assert.match(match, /applyProceedsSplit\(matchInfo\['GET_TICK'\],\s*give_amount,\s*matchInfo\['GET_ADDRESS'\],\s*matchInfo\['PAYOUT_LEGS'\]/, + 'order_match no longer applies the matching order\'s PAYOUT_LEGS to the escrow it releases; ' + + 'the guide\'s royalty-split destination wording may no longer be accurate'); + assert.match(match, /applyProceedsSplit\(orderInfo\['GET_TICK'\],\s*get_amount,\s*orderInfo\['GET_ADDRESS'\],\s*orderInfo\['PAYOUT_LEGS'\]/, + 'order_match no longer applies the order\'s PAYOUT_LEGS on the mirror side'); +}); + +test('the guide names the royalty split as an escrow destination', () => { + const safety = section(trading, '### Safety During a Trade', 'trading.md'); + const answer = section(faq, '### Are my tokens safe while a trade is in progress?', 'faq.md'); + for(const [label, text] of [['trading.md "Safety During a Trade"', safety], + ['faq.md "Are my tokens safe"', answer]]){ + assert.ok(!/only two addresses|[Nn]obody else can be paid|only be released in two ways/.test(text), + `${label} again says an escrow reaches only the counterparty or you. order_match applies ` + + 'the counterparty\'s stored PAYOUT_LEGS to the escrow it releases.'); + assert.match(text, /royalty or fee split/, + `${label} no longer names the royalty or fee split as a destination of the escrow`); + } +}); + +/* 8. The bridge section's availability and trust wording. */ + +test('the bridge availability source facts still hold', { skip: skipNoIndexer }, () => { + const changes = readSrc('protocol_changes.js'); + const xchainGate = changes.match(/addGate\('xchain_bridge_activation\.XCHAIN_BRIDGE_ACTIVATION'[\s\S]*?\}\);/); + const tokenGate = changes.match(/addGate\('token_bridge_activation\.TOKEN_BRIDGE_ACTIVATION'[\s\S]*?\}\);/); + + assert.ok(xchainGate && tokenGate, 'protocol_changes no longer declares both bridge gates via addGate'); + for(const key of ['BTC:mainnet', 'LTC:mainnet', 'DOGE:mainnet']){ + assert.match(xchainGate[0], new RegExp(`'${key}':\\s*9999999999`), + `XCHAIN_BRIDGE_ACTIVATION ${key} is no longer the sentinel. cross-chain.md says the ` + + 'bridge is not active on mainnet and must change in the same commit.'); + } + assert.match(tokenGate[0], /testnet:\s*9999999999/, + 'TOKEN_BRIDGE_ACTIVATION is armed on testnet. cross-chain.md says bridging other tokens ' + + 'is not yet switched on for testnet and must change in the same commit.'); +}); + +test('the cross-chain guide covers the bridge with its scope and trust stated', () => { + const bridge = section(crossChain, '## Moving a Token to Another Chain (XBRIDGE)', 'cross-chain.md'); + assert.match(bridge, /not active on mainnet/, + 'cross-chain.md bridge section no longer says the bridge is off on mainnet'); + assert.match(bridge, /hub-trusted mint/, + 'cross-chain.md bridge section no longer states the hub-trusted mint assumption'); + assert.match(bridge, /cannot be undone or redirected/, + 'cross-chain.md bridge section no longer says an applied credit is final'); +}); diff --git a/test/sibling-source-path-existence.test.js b/test/sibling-source-path-existence.test.js index 8998371e..44294801 100644 --- a/test/sibling-source-path-existence.test.js +++ b/test/sibling-source-path-existence.test.js @@ -348,6 +348,9 @@ describe('sibling path resolution', () => { fs.mkdirSync(path.join(root, 'src', 'batch'), { recursive: true }); fs.writeFileSync(path.join(root, 'src', 'batch', 'index.js'), 'a\nb\nc\n'); fs.writeFileSync(path.join(root, 'toolkit.js'), ''); + fs.writeFileSync(path.join(root, 'package.json'), '{}\n'); + fs.writeFileSync(path.join(root, '.gitignore'), 'src/config.json\n'); + execFileSync('git', ['init', '-q', root]); }); test.after(() => fs.rmSync(root, { recursive: true, force: true })); @@ -372,6 +375,14 @@ describe('sibling path resolution', () => { assert.match(r.why, /src\/gone\.js/); assert.match(r.why, /src\/gone\/index\.js/); }); + + test('a missing gitignored runtime file is expected absent, not a dead source citation', () => { + const roots = () => root; + const ignored = checkReference({ rel: 'src/config.json', line: null, candidates: ['x'] }, roots); + const untracked = checkReference({ rel: 'src/missing.json', line: null, candidates: ['x'] }, roots); + assert.deepEqual(ignored, { ok: true, why: 'gitignored in the sibling' }); + assert.equal(untracked.ok, false, 'an arbitrary missing file must still fail'); + }); }); /* ------------------------------------------------------------------ * diff --git a/test/supply-lock-claims.test.js b/test/supply-lock-claims.test.js index f81abe81..817aefe2 100644 --- a/test/supply-lock-claims.test.js +++ b/test/supply-lock-claims.test.js @@ -37,6 +37,10 @@ * so the flag does not preserve a usable recall, it removes recall * entirely. It was the only lock in the list not phrased as a * prohibition, which is how the consequence went unstated. + * 5. The allow and block lists were described as lockable ("unless you + * choose to lock them permanently"). The ISSUE lock set has no list + * entry, format 5 carries no lock field, and the edit rules guard no + * list, so the membership gate stays editable for a token's whole life. * * WHAT IT CHECKS. Both sides, because either one alone is a half guard: * @@ -219,3 +223,32 @@ test('the LOCK_CALLBACK bullet is phrased as a prohibition, not an assurance', ( 'the LOCK_CALLBACK bullet does not tell the issuer that recall becomes impossible, ' + 'which is the irreversible consequence of setting it'); }); + +test('the source facts the no-list-lock wording rests on still hold', { skip: skipNoIndexer }, () => { + const issue = readSrc('actions/issue.js'); + const lockSet = issue.match(/this\.fieldList\['LOCK'\]\s*=\s*\[([^\]]*)\]/); + + assert.ok(lockSet, 'issue.js no longer declares fieldList[\'LOCK\'], so the lock set cannot be read'); + assert.doesNotMatch(lockSet[1], /LIST/, + 'the ISSUE lock set now names a list lock. If ALLOW_LIST/BLOCK_LIST can be frozen, the ' + + 'guide\'s "no lock flag covers the lists" wording is no longer accurate'); + assert.match(issue, /this\.formats\[5\]\s*=\s*'VERSION\|TICK\|ALLOW_LIST\|BLOCK_LIST\|MEMO'/, + 'ISSUE format 5 no longer carries only the two lists; re-check whether a list lock was added'); + assert.doesNotMatch(issue, /'invalid: (ALLOW|BLOCK)_LIST \(locked\)'/, + 'issue.js now refuses a locked list edit, so the lists can be frozen after all'); +}); + +test('the guide does not promise a lock on the allow and block lists', () => { + const accessSection = section(guide, '## Access Control'); + for(const [page, text, phrase] of [ + ['creating-tokens.md', accessSection, 'lock them permanently'], + ['use-cases.md', useCases, 'lock the membership rules permanently'], + ]){ + assert.ok(!text.includes(phrase), + `user-guide/${page} states "${phrase}". No lock flag covers ALLOW_LIST or BLOCK_LIST: ` + + 'the ISSUE lock set has no list entry and the edit rules guard no list.'); + } + assert.match(lockSection, /allow and block lists/i, + 'creating-tokens.md "## Building Trust: Locking Parameters" no longer names the lists ' + + 'among what the lock flags do not cover, which is where a reader looks for it'); +}); diff --git a/test/tis-schema-field-coverage.test.js b/test/tis-schema-field-coverage.test.js index 91e4531f..1702d2ce 100644 --- a/test/tis-schema-field-coverage.test.js +++ b/test/tis-schema-field-coverage.test.js @@ -24,7 +24,9 @@ * * 1. Every row of the two TIS field tables is declared in the CURRENT schema * (top-level rows as top-level properties; file-entry rows in all four of - * the images/audio/video/files definitions). + * the images/audio/video/files definitions, or only in the one a leading + * `*(images only)*` marker names), and every property the schema declares + * has a row, so a field cannot drift out of the tables in either direction. * 2. Every character bound the prose states equals the schema's maxLength. * 3. The worked example parses and uses no key the schema does not declare. * 4. v1.0.0 and v1.1.0 stay frozen: each still stamped with its own version, @@ -84,6 +86,13 @@ function statedBound(description) { return m ? Number(m[1]) : null; } +// A leading `*(images only)*` narrows a file-entry row to that one media +// definition; an unmarked row applies to all four. +function rowScope(row) { + const m = /^\*\((\w+) only\)\*/.exec(row.description); + return m ? [m[1]] : MEDIA; +} + function readJson(name) { return JSON.parse(fs.readFileSync(path.join(JSON_DIR, name), 'utf8')); } @@ -127,6 +136,20 @@ describe('TIS field table / schema coverage', () => { assert.equal(statedBound('A link. 255 characters max.'), 255); assert.equal(statedBound('A link. 100 characters max.'), 100); assert.equal(statedBound('The TICK of the token'), null); + + const [scoped, plain, since] = fieldRows([ + '#### Scope', + '', + '| Field | Type | Description', + '| :--- | :--- | :---', + '| size | String | *(images only)* Pixels.', + '| hash | String | A hash.', + '| title | String | *(since v1.1.0)* A title.', + '', + ].join('\n'), '#### Scope'); + assert.deepEqual(rowScope(scoped), ['images'], 'the images-only marker was not read'); + assert.deepEqual(rowScope(plain), MEDIA, 'an unmarked row covers all four arrays'); + assert.deepEqual(rowScope(since), MEDIA, 'a since-version marker is not a scope'); }); test('both field tables were actually read', () => { @@ -134,7 +157,7 @@ describe('TIS field table / schema coverage', () => { `only ${TOP_ROWS.length} top-level field rows parsed out of ` + 'protocol/token-information-standard.md; the table format changed and this gate ' + 'is no longer reading it'); - assert.ok(ENTRY_ROWS.length >= 5, + assert.ok(ENTRY_ROWS.length >= 9, `only ${ENTRY_ROWS.length} file-entry field rows parsed; the table format changed`); }); @@ -147,17 +170,43 @@ describe('TIS field table / schema coverage', () => { undeclared.join('\n ')); }); - test(`every documented file-entry field is declared in all four media definitions`, () => { + test('every documented file-entry field is declared in each media definition its row covers', () => { const undeclared = []; - for (const def of MEDIA) { - const props = schema.definitions[def].properties; - for (const row of ENTRY_ROWS) - if (!(row.name in props)) undeclared.push(`${def}.${row.name}`); + for (const row of ENTRY_ROWS) { + const scope = rowScope(row); + for (const def of MEDIA) { + const declared = row.name in schema.definitions[def].properties; + if (scope.includes(def) && !declared) undeclared.push(`${def}.${row.name}`); + if (!scope.includes(def) && declared) + undeclared.push(`${def}.${row.name} is declared, but the row is marked ${scope} only`); + } + for (const def of scope) + if (!MEDIA.includes(def)) undeclared.push(`${row.name}: scope "${def}" is no media array`); } assert.deepEqual(undeclared, [], - 'the file-entry table says it applies to files, audio, video and images alike, so a ' + - 'field missing from any one of them is a contract that differs by array:\n ' + - undeclared.join('\n ')); + 'an unmarked file-entry row applies to files, audio, video and images alike, and a ' + + 'marked one to its named array alone, so a mismatch is a contract that differs by ' + + 'array:\n ' + undeclared.join('\n ')); + }); + + test(`every v${CURRENT} top-level property has a row in the field table`, () => { + const documented = new Set(TOP_ROWS.map((r) => r.name)); + const missing = Object.keys(schema.properties).filter((n) => !documented.has(n)); + assert.deepEqual(missing, [], + 'schema properties an implementer reading the field table never sees:\n ' + + missing.join('\n ')); + }); + + test(`every v${CURRENT} media property has a file-entry row covering its array`, () => { + const missing = []; + for (const def of MEDIA) + for (const name of Object.keys(schema.definitions[def].properties)) + if (!ENTRY_ROWS.some((r) => r.name === name && rowScope(r).includes(def))) + missing.push(`${def}.${name}`); + assert.deepEqual(missing, [], + 'media-entry properties the schema declares and the file-entry table omits, so a ' + + 'third party implementing from the table neither emits nor reads them:\n ' + + missing.join('\n ')); }); test('every character bound the prose states matches the schema maxLength', () => { @@ -169,6 +218,15 @@ describe('TIS field table / schema coverage', () => { if (declared !== stated) drifted.push(`${row.name}: prose says ${stated}, schema says ${declared}`); } + for (const row of ENTRY_ROWS) { + const stated = statedBound(row.description); + if (stated === null) continue; + for (const def of rowScope(row)) { + const declared = ((schema.definitions[def] || {}).properties || {})[row.name]; + if ((declared || {}).maxLength !== stated) + drifted.push(`${def}.${row.name}: prose says ${stated}, schema says ${(declared || {}).maxLength}`); + } + } assert.deepEqual(drifted, [], 'a publisher truncating to the documented bound and a validator enforcing the ' + 'schema disagree about what is valid:\n ' + drifted.join('\n ')); diff --git a/test/vectors.test.js b/test/vectors.test.js index 572e8b9d..98407c7a 100644 --- a/test/vectors.test.js +++ b/test/vectors.test.js @@ -31,6 +31,14 @@ const srb = require('../protocol/reference-impl/consensus/snapshot_reorg_buffer. const swqVectors = require('../protocol/test-vectors/stake_weighted_quorum.json'); const eqhVectors = require('../protocol/test-vectors/equivocation_header.json'); +const activationVectors = require('../protocol/test-vectors/activation_predicates.json'); + +function decodeSnapshotBlock(value) { + if (!value || typeof value !== 'object') return value; + if (value.special === 'nan') return Number.NaN; + if (value.special === 'undefined') return undefined; + throw new Error('unknown snapshotBlock vector encoding: ' + JSON.stringify(value)); +} describe('constants.js <-> reference-impl activation parity (consensus-critical)', () => { test('STAKE_WEIGHTED_QUORUM_ACTIVATION matches between constants.js and the reference impl', () => { @@ -110,14 +118,25 @@ describe('reference-impl/equivocation_header.js (EQUIV_HEADER / WI-2 bump 2)', ( }); }); -// The two activation predicates have zero vectors today. A vector authored from -// the implementation under test would only prove the implementation equals -// itself, so synthesizing one here is deliberately out of scope; these named, -// skipped placeholders keep the gap visible in every test run instead of letting -// it disappear silently. See the observability/verification-gate review pass -// (2026-07-09) that added this harness for the full rationale. -describe('activation predicates (KNOWN GAP: no normative vectors exist yet)', () => { - test('isStakeWeightedQuorumActive: no vectors for the mainnet 960999/961000 boundary, NaN snapshotBlock, or an unknown network', { skip: true }, () => {}); - test('isEquivHeaderActive: no vectors for the mainnet 960999/961000 boundary, NaN snapshotBlock, or an unknown network', { skip: true }, () => {}); - test('isSnapshotBurialActive / buriedSnapshotBlock: no vectors for the inert mainnet threshold, the empty-ish height guard, or the clamp to 0', { skip: true }, () => {}); +describe('activation predicates (canonical boundary vectors)', () => { + test('isStakeWeightedQuorumActive: mainnet boundary, NaN snapshotBlock, and unknown network', () => { + for (const v of activationVectors.isStakeWeightedQuorumActive) { + assert.equal(swq.isStakeWeightedQuorumActive(decodeSnapshotBlock(v.snapshotBlock), v.network), v.expected, v.name); + } + }); + + test('isEquivHeaderActive: mainnet boundary, NaN snapshotBlock, and unknown network', () => { + for (const v of activationVectors.isEquivHeaderActive) { + assert.equal(eqh.isEquivHeaderActive(decodeSnapshotBlock(v.snapshotBlock), v.network), v.expected, v.name); + } + }); + + test('isSnapshotBurialActive / buriedSnapshotBlock: genesis boundary, empty-ish height guard, and clamp to 0', () => { + for (const v of activationVectors.snapshotBurial) { + const snapshotBlock = decodeSnapshotBlock(v.snapshotBlock); + const expectedBuried = decodeSnapshotBlock(v.buriedSnapshotBlock); + assert.equal(srb.isSnapshotBurialActive(snapshotBlock, v.network), v.active, v.name + ': activation'); + assert.deepEqual(srb.buriedSnapshotBlock(snapshotBlock, v.network), expectedBuried, v.name + ': buried height'); + } + }); }); diff --git a/user-guide/creating-tokens.md b/user-guide/creating-tokens.md index ca4f847c..af99b2b6 100644 --- a/user-guide/creating-tokens.md +++ b/user-guide/creating-tokens.md @@ -84,7 +84,7 @@ You can restrict which addresses are allowed to interact with your token. The li Because the sending address is checked too, taking a holder off the allow list (or adding them to the block list) freezes the balance they already hold: they keep it, but they cannot move it until the lists change. -Both lists reference named lists you define on-chain using the LIST action. You can update these lists at any time, unless you choose to lock them permanently. +Both lists reference named lists you define on-chain using the LIST action. You can update these lists at any time, and there is no lock flag that freezes them: unlike a locked max supply, who may send or receive the token can always be changed later. --- @@ -140,7 +140,7 @@ No single flag forecloses all supply creation. Set **LOCK_MINT** and **LOCK_MINT Locking is a one-way door. Think carefully before locking anything. Once it is done, there is no going back. Not even for you. -One thing the lock flags do not cover is a **controller binding**. There is no `LOCK_CONTROLLER`, so a binding cannot be frozen the way a max supply can, and one can be added to a token after it has been issued. The drop-cooldown you commit to at bind time is the only friction on changing or removing one. Anyone weighing up a token's guarantees should read its bindings alongside its locks. +Two things the lock flags do not cover. The first is a token's **allow and block lists**. There is no `LOCK_ALLOW_LIST` or `LOCK_BLOCK_LIST`, so the lists a token points at can be changed, and those lists edited, at any time; the rules on who may hold the token can never be made permanent the way a max supply can. The second is a **controller binding**. There is no `LOCK_CONTROLLER`, so a binding cannot be frozen either, and one can be added to a token after it has been issued. The drop-cooldown you commit to at bind time is the only friction on changing or removing one. Anyone weighing up a token's guarantees should read its lists and bindings alongside its locks. --- diff --git a/user-guide/cross-chain.md b/user-guide/cross-chain.md index 7fec630c..947e3508 100644 --- a/user-guide/cross-chain.md +++ b/user-guide/cross-chain.md @@ -5,7 +5,7 @@ XChain runs on every supported chain simultaneously, today Bitcoin, Litecoin, and Dogecoin; but these are separate blockchains. A token created on Bitcoin exists on Bitcoin. A token created on Litecoin exists on Litecoin. Normally, trading between them would require a bridge, a centralized exchange, or a complex multi-step process involving trust in a third party. -XChain solves this with **SWAP**; a cross-chain exchange that lets you trade tokens on one blockchain for tokens on another, without any intermediary holding your assets. +XChain solves this with **SWAP**; a cross-chain exchange that lets you trade tokens on one blockchain for tokens on another, without any intermediary holding your assets. To move a token you already hold onto another chain without trading it, XChain also has a native bridge, covered in [Moving a Token to Another Chain](#moving-a-token-to-another-chain-xbridge). --- @@ -13,7 +13,7 @@ XChain solves this with **SWAP**; a cross-chain exchange that lets you trade tok Imagine you hold a token on Bitcoin and want to trade it for a token on Litecoin. On a centralized exchange, you would deposit your Bitcoin token, trust the exchange to hold it, find a counterparty, execute the trade, and then withdraw your Litecoin token. At every step, you are trusting the exchange not to lose your funds, freeze your account, or disappear. -With cross-chain bridges, you lock your asset on one chain and mint a representative version on another. The security of your asset depends entirely on the bridge's security; a single point of failure that has been exploited for billions of dollars across the industry. +With cross-chain bridges, you lock your asset on one chain and mint a representative version on another. The security of your asset depends entirely on the bridge's security; a single point of failure that has been exploited for billions of dollars across the industry. XChain's own bridge uses the same lock-and-mint shape, so its section below states exactly what it trusts. XChain offers a different path. @@ -37,7 +37,7 @@ Cross-chain swaps on XChain follow a straightforward flow: 2. **Someone accepts.** A counterparty on the other chain sees your offer and agrees to the terms. They record their acceptance on their blockchain. -3. **The match settles on each chain.** The hub records a match signed by a supermajority of validators, and only after each side's escrow has reached that chain's required confirmation depth. Each chain's indexer then independently checks those signatures before releasing the escrowed tokens to the counterparty, so the two legs settle separately rather than in one step. If no match is signed, both sides get their tokens back automatically at the deadline. +3. **The match settles on each chain.** The hub records a match signed by a supermajority of validators, and only after each side's escrow has reached that chain's required confirmation depth. Each chain's indexer then independently checks those signatures before releasing the escrowed tokens to the counterparty, less any royalty or fee split the counterparty's listing carries (see [Available Pairs](#available-pairs)), so the two legs settle separately rather than in one step. If no match is signed, both sides get their tokens back automatically at the deadline. ```mermaid sequenceDiagram @@ -113,7 +113,25 @@ Use SWAP when you want a precise exchange with a single counterparty. Use a cros The DEX order book is best when you are trading two tokens that both exist on the same blockchain. Swaps are for when the tokens you want to exchange live on different blockchains. -You can combine both: use the order book to trade on a single chain, and use SWAP or cross-chain ORDER when you need to move value across chains. +You can combine both: use the order book to trade on a single chain, and use SWAP or cross-chain ORDER when you need to move value across chains. To move a token you hold to another chain without trading it for anything, use the bridge below. + +--- + +## Moving a Token to Another Chain (XBRIDGE) + +SWAP and ORDER exchange one token for a different token with a counterparty. The bridge does something else: it moves a token you already hold to another chain, as the same asset, with no counterparty and no trade. + +**How it works.** You lock the token on the chain it was issued on, in a protocol-owned escrow address that nobody holds a key for. Once the lock has the source chain's required confirmations (by default 6 on Bitcoin, 12 on Litecoin and 60 on Dogecoin), the hub's validator federation signs a record of the transfer, and the destination chain credits the same amount to the address you named. To come back, you burn the copy on the destination chain, and after that chain's confirmations the same amount is released from the escrow to the address you name on the origin chain. Every unit is either held in the escrow or circulating as exactly one copy, so the supply never doubles. + +In the wallet, **Move across chains** performs the move, and **Bridge settings** is where an issuer opts a token in. + +**What is live today.** Only XCHAIN can be bridged, between Bitcoin and Litecoin or Dogecoin, and only on testnet and regtest. The bridge is not active on mainnet. Bridging other tokens through the issuer opt-in is built but not yet switched on for testnet or mainnet. [Flag-Day Values](../protocol/flag-days.md) lists where each activation stands. + +**What it trusts.** The credit on the destination chain relies on the hub's validator federation: a compromised hub could supply both the transfer record and the validator roster that checks it, so this is a hub-trusted mint, not a trustless one. The bridge stays off on mainnet until each credit must also agree with a validator-signed checkpoint of the Bitcoin ledger. + +**Finality.** Once a credit has been applied on the destination chain, it cannot be undone or redirected. A lock reversed by a reorg before its credit is applied is withdrawn and never applied; the confirmation depth is what makes a deeper reorg expensive. + +See [Token Bridge](../concepts/token-bridge.md) for the general model and the issuer opt-in, [Cross-Chain Bridge](../protocol/xchain-bridge.md) for XCHAIN's own bridge and its trust model, and [XBRIDGE](../protocol/actions/xbridge.md) for the action itself. --- @@ -152,7 +170,7 @@ sequenceDiagram - You want one chain's contract to trigger another chain's contract as part of a multi-chain application. - You are building cross-chain automation, oracles, or governance where the outcome of a call on one chain drives behavior on another. -XCALL is a system-level mechanism used by contract authors, not an action end users submit directly. The DEX (SWAP and ORDER) remains the right tool for cross-chain token trading between addresses. +XCALL is a system-level mechanism used by contract authors, not an action end users submit directly. The DEX (SWAP and ORDER) remains the right tool for cross-chain token trading between addresses, and the bridge for moving your own tokens between chains. --- diff --git a/user-guide/faq.md b/user-guide/faq.md index 173a54d9..1dafabed 100644 --- a/user-guide/faq.md +++ b/user-guide/faq.md @@ -89,7 +89,7 @@ Yes. The SWAP action allows cross-chain exchanges: you can trade a token on Bitc ### Are my tokens safe while a trade is in progress? -Yes. When you place a sell order or set up a swap, your tokens are moved into protocol-level escrow. This is not a company holding your tokens; it is the protocol itself locking them against your order. They can only be released in two ways: to the counterparty when the trade completes, or back to you when the order expires or you cancel it. There is no third party who can access or misappropriate them. +Yes. When you place a sell order or set up a swap, your tokens are moved into protocol-level escrow. This is not a company holding your tokens; it is the protocol itself locking them against your order. They are released only by the protocol's settlement rules: to the counterparty when the trade completes, or back to you when the order expires or you cancel it. The one addition is a royalty or fee split: when you buy a token bound to a `trade`-class controller, part of your escrowed payment goes to the addresses its listing's split names, fixed when that listing was created, and the seller gets the rest. Beyond that, no company, operator or other third party can access or misappropriate them. See [Safety During a Trade](./trading.md#safety-during-a-trade). ### Can I cancel an order once it is placed? diff --git a/user-guide/trading.md b/user-guide/trading.md index 83be3a81..1eea86de 100644 --- a/user-guide/trading.md +++ b/user-guide/trading.md @@ -47,7 +47,7 @@ Coin payments are the one exception. When a buyer takes your order and pays in b ### Safety During a Trade -Your tokens are never at risk during an open order. They sit in protocol-level escrow. Not on a company's server, not in a wallet someone else controls. The protocol guarantees there are only two addresses they can ever reach: a matching buyer's, or your own on cancellation or expiration. Nobody else can be paid out of that escrow. What can vary is the timing of the return, not the destination: if a buyer still owes you a coin payment, the release waits for that payment to settle or lapse, as described above. +Your tokens are never at risk during an open order. They sit in protocol-level escrow. Not on a company's server, not in a wallet someone else controls. The protocol releases that escrow only by its own settlement rules: to the counterparty whose order matches yours, or back to you on cancellation or expiration. One case adds destinations. If the token you are buying is bound to a `trade`-class controller, the seller's listing carries a royalty or fee split, fixed when that listing was created, and part of what you escrowed as payment is credited to the addresses that split names while the seller receives the rest. You still pay only the quoted price, and nobody can add to or redirect that split once the listing exists; see [Fees](#fees) and [Proceeds split](../protocol/controller-bound-tokens.md#proceeds-split-royalty-fee-payout_legs). The timing of a return can also vary: if a buyer still owes you a coin payment, the release waits for that payment to settle or lapse, as described above. --- @@ -122,7 +122,7 @@ That listing fee is what creating or editing a listing costs: placing an order a **Listings that hand over ownership cost extra.** A listing that escrows a token's **ownership** instead of a balance (the ownership dispenser described above, and the equivalent order or swap) also pays a flat **ownership-escrow premium of 50,000 gas, 0.5 XCHAIN at the current gas price**. It is charged on top of any duration fee, and it applies inside the 90-day window too, so an ownership listing is never free. The premium is charged when the listing is created; editing an existing ownership listing does not pay it again, and cancelling is still free. It is the same amount on Bitcoin, Litecoin, and Dogecoin. -**Controller-bound tokens can take a share of the seller's proceeds.** If a token binds a `trade`-class controller, that contract's guard runs when the listing is created and may attach a split that routes part of the **seller's** proceeds to addresses it names. This is how enforced royalties, marketplace fees and revenue share work on XChain. The split is fixed when the listing is created, is capped by a protocol limit that the token's own contract can tighten further, and comes out of what the seller receives; the buyer still pays the quoted price. The same guard can also refuse the listing outright, so a listing on a controller-bound token is not guaranteed to be accepted. Your escrowed tokens are unaffected either way: the split applies to the proceeds you are paid, not to the balance you put up. See [Proceeds split](../protocol/controller-bound-tokens.md#proceeds-split-royalty-fee-payout_legs). +**Controller-bound tokens can take a share of the seller's proceeds.** If a token binds a `trade`-class controller, that contract's guard runs when the listing is created and may attach a split that routes part of the **seller's** proceeds to addresses it names. This is how enforced royalties, marketplace fees and revenue share work on XChain. The split is fixed when the listing is created, is capped by a protocol limit that the token's own contract can tighten further, and comes out of what the seller receives; the buyer still pays the quoted price. The same guard can also refuse the listing outright, so a listing on a controller-bound token is not guaranteed to be accepted. As a seller, the balance you put up is unaffected: the split applies to the proceeds you are paid. As a buyer of a controller-bound token, it works the other way round: the split is carved out of the payment you escrowed, so part of it reaches the token's royalty or fee addresses instead of the seller, while your total outlay stays the quoted price. See [Proceeds split](../protocol/controller-bound-tokens.md#proceeds-split-royalty-fee-payout_legs). Check the current fee schedule through the XChain Explorer or your wallet software for the latest amounts. diff --git a/user-guide/use-cases.md b/user-guide/use-cases.md index 0f244de6..42363e7e 100644 --- a/user-guide/use-cases.md +++ b/user-guide/use-cases.md @@ -17,7 +17,7 @@ XChain actions involved: ISSUE (to create and lock the supply), SEND (to distrib ### Community and Fan Club Tokens -Issue a token to represent membership in a community, fan club, or organization. Members who hold the token can be granted access to events, content, or voting rights. You can update who qualifies at any time by adjusting the allow list, or lock the membership rules permanently for a more formal structure. +Issue a token to represent membership in a community, fan club, or organization. Members who hold the token can be granted access to events, content, or voting rights. You can update who qualifies at any time by adjusting the allow list. No lock flag covers the lists, so the membership rules stay editable for the life of the token. XChain actions involved: ISSUE, LIST (to define eligible members), SEND (to distribute memberships). diff --git a/whitepaper.md b/whitepaper.md index 63e262e7..4bed6709 100644 --- a/whitepaper.md +++ b/whitepaper.md @@ -419,7 +419,7 @@ flowchart TD A `trade`-class guard may additionally return a basis-point **proceeds split** (`payoutLegs`) when a listing is created. The split is validated and stored on the order or swap as declarative data and applied at match with exact conservation (remainder to the seller first, then each leg); no guard runs on the system-triggered fill path, so matching stays deterministic and gas-free. This one primitive expresses enforced royalties, marketplace fees, and revenue share; there is no royalty-specific code path. On cross-chain sales the legs travel inside the validator-signed match canonical and are applied by the settling chain, so a corrupted mirror cannot strip a royalty (§9.2). -The mechanism is bounded by design. A controller is a gate, never an agent: it cannot move user funds on its own initiative, and it may not call the asynchronous frameworks (attestation, XCALL) or emit `SLASH`. A contract may declare an immutable **permissions manifest** at deploy time (an emission allowlist, and a royalty cap tighter than the global ceiling). Guard gas is billed to the action's source against a bounded ceiling (200,000 gas by default, reserved up front and charged on allow only). Bindings are droppable, subject to a per-binding cooldown the owner commits at bind time. Bulk distributions are guarded once, sender-side, per tick (never per recipient, which keeps guard cost independent of recipient count and un-griefable). A token or account with no binding takes a single NULL check: zero VM work, zero added fee, unchanged behavior. *(gated on the 2.0.0 contract-era flag day, whose armed instant has passed; mainnet availability follows the network launch. Cross-chain royalty acceptance follows its own later gate after the match-canonical flag-day, with royalty-bearing cross-chain listings denied fail-closed in the interim.)* +The mechanism is bounded by design. A controller is a gate, never an agent: it cannot move user funds on its own initiative, and it may not call the asynchronous frameworks (attestation, XCALL) or emit `SLASH`. A contract may declare an immutable **permissions manifest** at deploy time (an emission allowlist, and a royalty cap tighter than the global ceiling). Guard gas is billed to the action's source against a bounded ceiling (200,000 gas by default, reserved up front on BTC and charged on allow only). Bindings are droppable, subject to a per-binding cooldown the owner commits at bind time. Bulk distributions are guarded once, sender-side, per tick (never per recipient, which keeps guard cost independent of recipient count and un-griefable). A token or account with no binding takes a single NULL check: zero VM work, zero added fee, unchanged behavior. *(gated on the 2.0.0 contract-era flag day, whose armed instant has passed; mainnet availability follows the network launch. Cross-chain royalty acceptance follows its own later gate after the match-canonical flag-day, with royalty-bearing cross-chain listings denied fail-closed in the interim.)* --- @@ -722,7 +722,7 @@ XChain demonstrates that a complete digital-asset platform, including tokens, an | Attestation request | 5,000 gas (plus the 500 emission) | | Cross-chain call (XCALL) request / callback | 2,000 / up to 20,000 gas | | Ownership-escrow premium (ORDER/SWAP/DISPENSER with give-ownership) | 50,000 gas | -| Controller guard ceiling (per guard run) | 200,000 gas, reserved up front, charged on allow only | +| Controller guard ceiling (per guard run) | 200,000 gas, reserved up front on BTC, charged on allow only | | AIRDROP / DIVIDEND | 100 gas/recipient | | Order/dispenser/swap expiration; betting-market duration | first 90 days free, then ~550 gas/day | | VM gas ceiling / memory | 1,000,000 gas / 8 MB per execution |