Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.20.0] - 2026-09-17

### Added
- Published the mirror-admission and anchor-attestation activation heights for the barrier family.

### Changed
- Mirrored rows carry per-chain admission heights so a future-dated block no longer stalls indexer processing.
- Canonical activation modules and their vendored barrier rows now share the consolidated consensus layout.
- Release documentation mirrors v0.19.0 and v0.19.1, and CI resolves master-bound siblings from master.
- Configuration documentation covers `MA_SIDE_KEY` and `HUB_SNAPSHOT_RATE_LIMIT_RPM`.
- The indexer computed-read baseline advances to 8.

## [0.19.0] - 2026-09-16

### Added
Expand Down
11 changes: 7 additions & 4 deletions bin/generate-flag-days.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const TIMESTAMP_FLOOR = 1_000_000_000;

/**
* Upper bound past which a value is an UNARMED sentinel rather than a date
* anybody scheduled. `price_pair_activation.js` parks 9999999999 (year 2286)
* anybody scheduled. `consensus/gates/price_pair_gate.js` parks 9999999999 (year 2286)
* exactly so no operator reads it as a plan. Publishing it as a flag day would
* put a fake commitment on a page implementers read.
*/
Expand Down Expand Up @@ -166,7 +166,7 @@ function mainnetTimeLiteral(text, open) {
* or null when the braces never close.
*
* Brace-counted rather than matched, because an activation map nests:
* `state_subtree_activation.js` keys three per-slot maps inside one const.
* `consensus/gates/state_subtree_gate.js` keys three per-slot maps inside one const.
*/
function objectBody(text, open) {
let depth = 0;
Expand Down Expand Up @@ -203,12 +203,15 @@ function readMainnetSlot(raw) {

/**
* Every time-keyed gate the sibling `*_activation.js` modules declare, and the
* slots this scan refuses to guess at.
* slots this scan refuses to guess at. Since W5 the indexer keeps no top-level
* `*_activation.js` (every map is a registry row and the logic modules sit under
* `src/consensus/gates/`), so the scan finds nothing there; it stays as the
* pre-W3 fallback the registry arm supersedes.
*
* READ WITH THE REGISTRY ARM'S RIGOR, which it once lacked in three ways. It
* scanned raw text, so a retired map parked in a block comment was published as
* a live row; it took the FIRST `mainnet:` per file, so the second map in a
* multi-map module could never enter the page (`anchor_reward_activation.js`
* multi-map module could never enter the page (the anchor-reward module
* alone declares three); and it named the gate after the FILE, which cannot
* name more than one map. Comments are stripped, every map is scanned, and each
* gate is named by its enclosing const.
Expand Down
6 changes: 3 additions & 3 deletions components/explorer/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ flowchart TD
| `src/connectors/indexer.js` | `XChainIndexerConnector` | JSON-RPC client for xchain-indexer; proxies read-only `feequote` and `feeschedule` endpoints so fee logic stays single-sourced in the indexer |
| `src/http/proof_server.js` | `ProofServer` | SPV light-client proof server (spec §8.1): builds Merkle balance/state proofs from the indexer's `state_tree_nodes` table for client-side verification against quorum-signed checkpoint roots |
| `src/consensus/merkle.js` | None | Consensus-critical, DB-free Merkle primitives for the additive state commitment, per-block content root, and top-level state root; shared byte-identically with xchain-indexer and xchain-sdk |
| `src/checkpoint_commitment_activation.js` | None | Flag-day gate (SPV Phase 2, spec §6.1/§6.3): determines at which BTC block the signed checkpoint canonical gains `state_root` and `block_merkle_root` fields; consensus-critical, vendored across hub/indexer/explorer |
| `src/equivocation_header.js` | None | Consensus-critical equivocation header (`EQUIV|ENGINE|ROUND|VIEW||content`) that prefixes every PBFT canonical at/above its activation height; vendored byte-identically across all consensus-bearing services |
| `src/stake_weighted_quorum.js` | None | Consensus-critical source-deduplicated stake predicate (3 x tally > 2 x total stake) used by every settlement gate and the checkpoint verifier; the 2f+1 signer count is the separate pre-activation rule, not this one; vendored byte-identically across all consensus-bearing services |
| `src/consensus/gate_registry/` (row `checkpoint_commitment_activation.CHECKPOINT_COMMITMENT_ACTIVATION`) | None | Flag-day gate (SPV Phase 2, spec §6.1/§6.3): determines at which BTC block the signed checkpoint canonical gains `state_root` and `block_merkle_root` fields; consensus-critical, vendored across hub/indexer/explorer |
| `src/consensus/equivocation_header.js` | None | Consensus-critical equivocation header (`EQUIV|ENGINE|ROUND|VIEW||content`) that prefixes every PBFT canonical at/above its activation height; vendored byte-identically across all consensus-bearing services |
| `src/consensus/stake_weighted_quorum.js` | None | Consensus-critical source-deduplicated stake predicate (3 x tally > 2 x total stake) used by every settlement gate and the checkpoint verifier; the 2f+1 signer count is the separate pre-activation rule, not this one; vendored byte-identically across all consensus-bearing services |
| `src/icons/downloader.js` | `IconDownloader` | In-process worker that downloads, resizes, and caches token icons from the indexer's `icons` table |
| `src/icons/resolver.js` | `IconResolver` | Pure icon URL resolution logic; mirrors the priority chain used in the web UI's `xchain.js` so server and browser select the same source |
| `src/coin-config/BTC.js` | None | Bitcoin-specific: chain info, network addresses (burn, gas, protocol, community) |
Expand Down
2 changes: 1 addition & 1 deletion components/hub/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ Each voting validator's signing pubkey resolves to its stake source in the feder
snapshot, each source counts at most once however many of its keys vote, and the summed
stake must satisfy `3 x tally > 2 x S`, where `S` is the snapshot's total stake over
distinct sources. Three equally weighted sources therefore need all three votes. See
[`protocol/reference-impl/stake_weighted_quorum.js`](../../protocol/reference-impl/stake_weighted_quorum.js).
[`protocol/reference-impl/consensus/stake_weighted_quorum.js`](../../protocol/reference-impl/consensus/stake_weighted_quorum.js).

**Below activation:** the legacy signer COUNT `max(2f+1, ceil((N+1)/2))` where
`f = floor((N-1)/3)`, tolerating `f` Byzantine validators out of `N` total. The
Expand Down
3 changes: 2 additions & 1 deletion components/hub/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ These variables are required regardless of operating mode.
| `HUB_DB_USER` | Yes | None | MariaDB username |
| `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 across the whole API. 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_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_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. |
| `HUB_TRUST_PROXY` | No | `loopback, uniquelocal` | Express `trust proxy` setting. A containerized hub behind a local reverse proxy works with the default. Set to `false` to disable, a hop count (e.g. `1`), or a CIDR list for other topologies. See [Express docs](https://expressjs.com/en/guide/behind-proxies.html). |
| `HUB_ALLOW_UNAUTHENTICATED` | No | `false` | A hub in validator mode (`P2P_VALIDATOR_ADDR` set) with no `HUB_API_KEY` refuses to boot, because its write methods would let anyone drive consensus-affecting writes. Set to `true` to explicitly acknowledge running keyless (regtest/dev only). See OPERATIONS.md → Authentication. |
Expand Down
2 changes: 1 addition & 1 deletion components/hub/decentralization.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Each validator runs the full hub stack. Communication happens via WebSocket-base

The rule is keyed on the round's BTC-anchored snapshot block and network, so every hub and every indexer flips on the same anchor.

**At or above `STAKE_WEIGHTED_QUORUM_ACTIVATION`:** stake-weighted and source-deduplicated. Each voting validator's pubkey resolves to its stake source in the federation snapshot, each source counts at most once however many of its keys vote, and the summed stake must satisfy `3 x tally > 2 x S`, where `S` is the snapshot's total stake over distinct sources. Three equally weighted sources therefore need all three votes. See [`protocol/reference-impl/stake_weighted_quorum.js`](../../protocol/reference-impl/stake_weighted_quorum.js).
**At or above `STAKE_WEIGHTED_QUORUM_ACTIVATION`:** stake-weighted and source-deduplicated. Each voting validator's pubkey resolves to its stake source in the federation snapshot, each source counts at most once however many of its keys vote, and the summed stake must satisfy `3 x tally > 2 x S`, where `S` is the snapshot's total stake over distinct sources. Three equally weighted sources therefore need all three votes. See [`protocol/reference-impl/consensus/stake_weighted_quorum.js`](../../protocol/reference-impl/consensus/stake_weighted_quorum.js).

**Below activation:** the legacy signer count `max(2f+1, ceil((N+1)/2))` where `f = floor((N-1)/3)`, tolerating `f` Byzantine validators out of `N` total. The simple-majority floor matters for small federations: bare `2f+1` degenerates to a quorum of 1 at N=3 (f=0), which would let a single validator finalize alone. With the floor, N=3 requires 2 votes and N=2 requires both.

Expand Down
6 changes: 3 additions & 3 deletions components/indexer/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,11 @@ The VM maintains a per-block cache of V8 compiled script data (`beginBlock()`/`e
| `src/hub/hub_push_queue.js` | `HubPushQueue` | Durable retry queue for PRICE pushes to the hub; backs the `pending_hub_pushes` table |
| `src/consensus/ed25519.js` | None | Ed25519 signature verification using Node built-in crypto; mirrors `xchain-hub/src/validators/identity.js` format |
| `src/consensus/merkle.js` | None | Consensus-critical SPV light-client Merkle primitives: additive state SMT, per-block content root, fixed top-level state root. Vendored byte-identically into `xchain-sync` |
| `src/stateHash.js` | None | Builds the `state_hash` preimage covering in-place mutations (deactivation stamps, slash debits, status flips, cooldown maturities) that the three standard block hashes cannot see |
| `src/consensus/state_hash.js` | None | Builds the `state_hash` preimage covering in-place mutations (deactivation stamps, slash debits, status flips, cooldown maturities) that the three standard block hashes cannot see |
| `src/state_commitment/index.js` | None | Computes per-block `state_tree_roots` (balances SMT + stakes SMT + state root + block Merkle root) and writes them to the DB |
| `src/stake_weighted_quorum.js` | None | Consensus-critical stake-weighted quorum predicate (WI-1). Vendored byte-identically across hub, indexer, explorer, sync, and SDK |
| `src/consensus/stake_weighted_quorum.js` | None | Consensus-critical stake-weighted quorum predicate (WI-1). Vendored byte-identically across hub, indexer, explorer, sync, and SDK |
| `bin/recovery.js` | None | CLI for rebuilding the cross-chain match mirror from on-chain ANCHOR archive data, with no surviving hub database |
| `src/equivocation_header.js` | None | Builds EQUIV-header canonicals for the WI-2 equivocation slashing protocol, one per engine tag |
| `src/consensus/equivocation_header.js` | None | Builds EQUIV-header canonicals for the WI-2 equivocation slashing protocol, one per engine tag |
| `src/db/migration/migrate.js` | None | Operator-initiated CLI that applies pending SQL migrations from `src/sql/migrations/`, including the `manual`-tagged ones startup skips (startup auto-applies only `auto`-tagged migrations). A bare run applies every pending migration; `--file <name.sql>` scopes the run to named files. There is no `--help` and no dry-run flag |
| `xchain-vm` (external) | `XChainVM` | Standalone module: V8 isolate sandbox, AST-based gas metering, gateway API; loaded by `src/actions/index.js`, called by DEPLOY and EXECUTE handlers |

Expand Down
20 changes: 20 additions & 0 deletions components/indexer/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,26 @@ pipeline rather than imported.
| `GA_SIDE_ROOT` | **Harness only.** Materialized tree the forked side-process replays from (the HEAD archive, or the HEAD archive with the arm commit reverted) | `/tmp/xchain-ga-witness-btc/indexer-old` |
| `GA_SIDE_KEY` | **Harness only.** Side label the side-process reports under and prefixes its progress lines with: `OLD` or `ON` | `ON` |

### Mirror-admission replay witness

Read only by `bin/verify-mirror-admission-replay-equivalence.js` (the
below-the-flag replay witness for the mirror-admission barrier family, which
replays one decoder corpus with the lever OFF, armed at the boundary height and
armed at genesis, and compares the consensus hash chain); never by the indexer
service itself. It takes its database coordinates from `--db-host`, `--db-port`
and `--db-user` or from the `TEST_DB_*` variables documented for the A7 harness
above, and it never falls back to `.env`. The password comes from the variable
NAMED by `--db-pass-env` (for example `MA_DB_PASS`), or from `TEST_DB_PASS` when
that option is absent, so it never reaches a process list. For each side-process
the parent sets `INDEXER_COIN`, `INDEXER_NETWORK`, `TEST_DECODER_DB`,
`TEST_INDEXER_DB` and `XC_MIRROR_ADMISSION_ACTIVATION` (unset, the boundary
height, or `0`), and the side-process reads the lever back to prove the era it
actually resolved.

| Variable | Description | Example |
|---|---|---|
| `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` |

### BATCH cost-measurement harness

Read only by `bin/measure-batch-execute-cost.js`, which measures the block-loop
Expand Down
Loading
Loading