diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b15e1..56be39f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/bin/generate-flag-days.js b/bin/generate-flag-days.js index cc515b2..1284ec0 100644 --- a/bin/generate-flag-days.js +++ b/bin/generate-flag-days.js @@ -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. */ @@ -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; @@ -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. diff --git a/components/explorer/architecture.md b/components/explorer/architecture.md index b0f7341..4480a89 100644 --- a/components/explorer/architecture.md +++ b/components/explorer/architecture.md @@ -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) | diff --git a/components/hub/architecture.md b/components/hub/architecture.md index b4ff216..c897f50 100644 --- a/components/hub/architecture.md +++ b/components/hub/architecture.md @@ -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 diff --git a/components/hub/configuration.md b/components/hub/configuration.md index e015c92..1ed8453 100644 --- a/components/hub/configuration.md +++ b/components/hub/configuration.md @@ -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. | diff --git a/components/hub/decentralization.md b/components/hub/decentralization.md index a5a60e4..937c8a2 100644 --- a/components/hub/decentralization.md +++ b/components/hub/decentralization.md @@ -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. diff --git a/components/indexer/architecture.md b/components/indexer/architecture.md index 0c937d0..48e0166 100644 --- a/components/indexer/architecture.md +++ b/components/indexer/architecture.md @@ -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 ` 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 | diff --git a/components/indexer/configuration.md b/components/indexer/configuration.md index 72a61d5..99ce758 100644 --- a/components/indexer/configuration.md +++ b/components/indexer/configuration.md @@ -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 diff --git a/components/sync/architecture.md b/components/sync/architecture.md index aad4da2..17c6745 100644 --- a/components/sync/architecture.md +++ b/components/sync/architecture.md @@ -105,19 +105,19 @@ flowchart TD | `server/cooldown_credits.js` | `collectMaturedCooldownCredits` | Collects backdated cooldown-refund credits that the action-scoped join cannot reach; source side | | `util/wire_codec.js` | `encodeRow`, `decodeValue` | Binary-safe row serialization: tags BLOB/Buffer column values with a `__xbin__` sentinel so they survive JSON round-trip intact | | `client/block_hasher.js` | `BlockHasher` | Independently recomputes a block's consensus hashes (ledger/actions/contract) from the replicated rows; the source of VERIFY_RECOMPUTE | -| `stateHash.js` | `buildStateHashData` | Builds the canonical preimage for the fourth per-block replication-integrity hash (`state_hash`), covering in-place mutations and backdated credits not captured by the three consensus hashes | +| `consensus/state_hash.js` | `buildStateHashData` | Builds the canonical preimage for the fourth per-block replication-integrity hash (`state_hash`), covering in-place mutations and backdated credits not captured by the three consensus hashes | | `stateCommitment.js` | `computeFollowerRoots` | Follower twin of the indexer's SPV state-commitment engine; recomputes per-block SMT roots (balances, stakes, state) for VERIFY_STATE_COMMITMENT | | `merkle.js` | None | Consensus-critical SPV Merkle primitives (SHA-256 SMT, block Merkle root, state root); byte-aligned with the indexer twin | | `server/merkle_tree.js` | `MerkleTree` | Binary SHA-256 Merkle tree used by TransparencyLog for epoch proof construction | | `db/balance_helpers.js` | None | Shared SQL helpers for rebuilding the `balances` aggregate after a block apply or rollback | | `checkpoint.js` | None | Client-side verifier for quorum-signed state checkpoints (SPV spec §6.1/§6.3) | -| `stake_weighted_quorum.js` | None | Canonical stake-weighted quorum predicate; vendored byte-identically from xchain-documentation | +| `consensus/stake_weighted_quorum.js` | None | Canonical stake-weighted quorum predicate; vendored byte-identically from xchain-documentation | | `client/pinned_validators.js` | None | Out-of-band pinned validator sets used by VERIFY_CHECKPOINT_QUORUM to anchor checkpoint signatures | | `consensus-constants.js` | None | Frozen per-chain consensus constants (e.g. `ACTIVATION_DELAY_BLOCKS`) shared across modules | | `schema/version.js` | None | Snapshot schema version constant used to detect incompatible snapshot formats | -| `state_commitment_activation.js` | `isStateCommitmentActive` | Flag-day gate: returns whether the SPV state-commitment feature is active for a given block and network | -| `checkpoint_commitment_activation.js` | None | Flag-day gate for quorum-signed checkpoint commitment (SPV spec §6.1/§6.3 Phase 2) | -| `equivocation_header.js` | None | Consensus-critical implementation of the uniform signed equivocation header (WI-2 bump 2) | +| `consensus/gates/state_commitment_gate.js` | `isStateCommitmentActive` | Flag-day gate: returns whether the SPV state-commitment feature is active for a given block and network | +| `consensus/gate_registry/` (row `checkpoint_commitment_activation.CHECKPOINT_COMMITMENT_ACTIVATION`) | None | Flag-day gate for quorum-signed checkpoint commitment (SPV spec §6.1/§6.3 Phase 2) | +| `consensus/equivocation_header.js` | None | Consensus-critical implementation of the uniform signed equivocation header (WI-2 bump 2) | | `client/sync.js` | `ClientSync` | Client-mode orchestrator: bootstrap, catch-up, live sync loop per chain/network | | `client/applier.js` | `ClientApplier` | Applies block payloads and snapshots to local replica DB via INSERT IGNORE | | `client/rollback.js` | `ClientRollback` | Rollback logic mirroring indexer's Rollback.js table lists | @@ -203,7 +203,7 @@ Each hash includes `block_index` and `previous_hash` (from the prior block's cor A fourth per-block field, `state_hash`, covers the in-place mutations and backdated cooldown-refund credits that the three consensus hashes structurally cannot reach. Those hashes scope rows by `actions.block_index = B` (new, immutable rows only). They cannot see a mutation the indexer applies to a surviving row from an earlier block, nor a refund credit that reuses an earlier action_index. A follower that silently fails to apply one of those mutations therefore diverges with no mismatch on the three hashes to flag it. -`state_hash` is computed by `stateHash.js` and stored in `blocks.state_hash_id`. `ServerPoller` reads it via the `getBlockHashRow` JOIN on `state_hash_id` and attaches it as a top-level field on every indexer block payload. It is NOT written to `sync_meta`, NOT included in Merkle leaves, and NOT part of the hub-signed checkpoint; it is a replication-integrity field only. +`state_hash` is computed by `consensus/state_hash.js` and stored in `blocks.state_hash_id`. `ServerPoller` reads it via the `getBlockHashRow` JOIN on `state_hash_id` and attaches it as a top-level field on every indexer block payload. It is NOT written to `sync_meta`, NOT included in Merkle leaves, and NOT part of the hub-signed checkpoint; it is a replication-integrity field only. On the client side, when `VERIFY_STATE_HASH=true` (the default), `ClientSync` recomputes `state_hash` from the replica's rows at apply time and halts durably on mismatch. A `NULL` `state_hash` (block indexed before the feature shipped) is skipped, so enabling this check can never false-halt against a back-level source. diff --git a/concepts/actions.md b/concepts/actions.md index 67b77eb..94317a9 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 `rollcall_activation.js`, `xchain_bridge_activation.js` and `token_bridge_activation.js` 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; 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). ### Token Lifecycle diff --git a/concepts/block-hashes.md b/concepts/block-hashes.md index 030b2aa..97dfe35 100644 --- a/concepts/block-hashes.md +++ b/concepts/block-hashes.md @@ -102,7 +102,7 @@ The three consensus hashes cover only rows whose `block_index` equals the curren ### What It Covers -The state hash preimage (built in `xchain-indexer/src/stateHash.js`, mirrored verbatim in `xchain-sync/src/stateHash.js`) includes, for each block: +The state hash preimage (built in `xchain-indexer/src/consensus/state_hash.js`, mirrored verbatim in `xchain-sync/src/consensus/state_hash.js`) includes, for each block: | Row class | Tables | Trigger | |---|---|---| diff --git a/lib/env-var-doc-coverage.js b/lib/env-var-doc-coverage.js index e554f27..1ef240f 100644 --- a/lib/env-var-doc-coverage.js +++ b/lib/env-var-doc-coverage.js @@ -1232,8 +1232,16 @@ function checkDivergentDefaults(survey) { 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: 7, node: 5, + decoder: 4, encoder: 4, explorer: 6, hub: 5, indexer: 8, node: 5, 'regtest-miner': 7, sdk: 4, sync: 10, 'utxo-tracker': 7, vm: 0, + // 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, + // which landed with four computed reads. Three became literal reads (the side-process now + // reads XC_MIRROR_ADMISSION_ACTIVATION by name, documented in components/indexer/ + // configuration.md) and one stays: process.env[o.db.passEnv] in explicitDbParams(). Raised + // deliberately for that one: --db-pass-env NAMES the password variable at run time so the + // password never reaches argv or the tool's output, so the key cannot be a literal. // hub 6 -> 5, indexer 8 -> 7 and sync 11 -> 10 (activation registry W4, row P1/P3): the // carrier logic pin's one computed read (bin/lib/carrier_logic_pin.js:119, the // XCHAIN__DIR key built from the repo name) became three reads by literal name, diff --git a/package-lock.json b/package-lock.json index 40e3813..684e3ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xchain-documentation", - "version": "0.19.0", + "version": "0.20.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "xchain-documentation", - "version": "0.19.0", + "version": "0.20.0", "license": "AGPL-3.0-or-later", "devDependencies": { "mathjs": "15.2.0" diff --git a/package.json b/package.json index 4240df9..e4193bb 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.19.0", + "version": "0.20.0", "license": "AGPL-3.0-or-later", "repository": { "type": "git", diff --git a/protocol/actions/anchor.md b/protocol/actions/anchor.md index 42f1ddf..66af3ef 100644 --- a/protocol/actions/anchor.md +++ b/protocol/actions/anchor.md @@ -346,7 +346,7 @@ exact bytes): snapshot set summed over distinct sources. A source whose snapshot weight is missing fails closed. Three equally weighted sources therefore need all three signatures; two are not enough. The predicate is `meetsStakeThreshold` in - [`protocol/reference-impl/stake_weighted_quorum.js`](../reference-impl/stake_weighted_quorum.js). + [`protocol/reference-impl/consensus/stake_weighted_quorum.js`](../reference-impl/consensus/stake_weighted_quorum.js). - **Below activation** the quorum is the legacy signer COUNT `max(2f+1, ceil((N+1)/2))`, `f = floor((N-1)/3)`: PBFT `2f+1` floored at a simple majority, so N=3 requires 2 and single-validator sets require 1. The floor is what stops bare `2f+1` degenerating to a diff --git a/protocol/actions/rollcall.md b/protocol/actions/rollcall.md index 0c8d44c..b727f4a 100644 --- a/protocol/actions/rollcall.md +++ b/protocol/actions/rollcall.md @@ -123,7 +123,7 @@ The window is a **height cut**, not a per-block time filter, so every honest nod Miner timestamp slack (~2 hours either way, on either chain) moves the edge. It cuts both ways and a wider window can only *reduce* evictions, so the residual is bounded and named: a signature landed in the last couple of hours of the window on a back-dated DOGE block may fall outside the cut. Publishers stay clear of the edge by self-publishing well before it. ## Activation and constants -All eight values are **consensus** and frozen in `protocol/constants.js`, with byte-identical copies in `xchain-{indexer,hub}/src/rollcall_activation.js`. None may be read from the coin registry or from env. +All eight values are **consensus** and frozen in `protocol/constants.js`, with byte-identical copies in `xchain-{indexer,hub}/src/consensus/gates/rollcall_gate.js`. None may be read from the coin registry or from env. | Constant | mainnet | testnet | regtest | Unit | |---|---|---|---|---| diff --git a/protocol/actions/xbridge.md b/protocol/actions/xbridge.md index 7b819eb..55e84b7 100644 --- a/protocol/actions/xbridge.md +++ b/protocol/actions/xbridge.md @@ -61,7 +61,7 @@ Burns 250 of the bridged BTC.PEPECASH row on DOGE; a matching XBRIDGE v5 release ``` ## Rules -- Below `XCHAIN_BRIDGE_ACTIVATION` for this chain (v0 to v2; the map is keyed `':'`, falling back to the bare network key) or `TOKEN_BRIDGE_ACTIVATION` for this network (v3 to v5), every version returns `invalid: XBRIDGE before activation`. The height compared is always the block index of the chain being parsed, never a transfer's snapshot block. +- Below `XCHAIN_BRIDGE_ACTIVATION` for v0 to v2 or `TOKEN_BRIDGE_ACTIVATION` for v3 to v5 on this chain (both maps are keyed `':'`, falling back to the bare network key), every version returns `invalid: XBRIDGE before activation`. The height compared is always the block index of the chain being parsed, never a transfer's snapshot block. - **Version 0 (lock XCHAIN).** BTC only; on any other chain, `invalid: XBRIDGE (BTC only)`. `DEST_COIN` must be a supported coin other than BTC (`invalid: DEST_COIN`). `DEST_ADDRESS` is validated with the coin-and-network-aware address check for `DEST_COIN` (`invalid: DEST_ADDRESS`). `AMOUNT` must be a positive decimal at up to 8 fractional digits and no more than the source's XCHAIN balance (`invalid: AMOUNT`, `invalid: insufficient funds`). Debits the source and credits `ADDRESS.BRIDGE_`; no supply change on BTC. Fee: `XBRIDGE_BASE` (5,000 gas). - **Version 1 (burn XCHAIN).** Non-BTC only; on BTC, `invalid: XBRIDGE v1 is not valid on BTC`. `BTC_ADDRESS` is validated as a BTC address (`invalid: BTC_ADDRESS`). Debits the source and lowers this chain's XCHAIN supply by `AMOUNT`. Fee: `XBRIDGE_BASE`, paid in native coin. - **Version 2 (settle XCHAIN).** System-injected only from a finalized `bridge_transfers` row; a broadcast v2 is refused with `invalid: XBRIDGE v2 is system-injected`. On the destination (from a lock): credits `DEST_ADDRESS` and raises this chain's XCHAIN supply, creating the token row on first use if it does not yet exist. On BTC (from a burn): debits `ADDRESS.BRIDGE_` and credits the named BTC address; an escrow that would go negative is refused outright and logged once, applying nothing. Pays no fee. diff --git a/protocol/constants.js b/protocol/constants.js index 8778f42..966f5f6 100644 --- a/protocol/constants.js +++ b/protocol/constants.js @@ -372,7 +372,7 @@ const STAKE_WEIGHTED_QUORUM_ACTIVATION = { // `EQUIV|||||`. This is consensus-breaking (it changes the // signed preimage of every settlement/checkpoint/price/attestation signature + the config-change // PBFT canonical), so it is gated, kept byte-identical to the local copies in -// xchain-{hub,indexer,sdk,explorer,sync}/src/equivocation_header.js by the +// xchain-{hub,indexer,sdk,explorer,sync}/src/consensus/equivocation_header.js by the // cross-service regression suite, and must deploy hub + ALL indexers atomically. Its sole // consumer is the SLASH v0 equivocation-slashing action, which is only constructible from // post-flag-day (header-carrying) messages. Same ARMED height and deploy-by convention as @@ -397,7 +397,7 @@ const EQUIV_HEADER_ACTIVATION = { // indexed mainnet history is ISSUE and ANCHOR only (0 validators, 0 stakes, 0 quorum-signed // artifacts measured), so burying reinterprets nothing there and a from-genesis replay is the // witness. Kept byte-identical to the local copies in -// xchain-{hub,indexer,sdk}/src/snapshot_reorg_buffer.js by the cross-service regression suite. +// xchain-{hub,indexer,sdk}/src/consensus/snapshot_reorg_buffer.js by the cross-service regression suite. const CANONICAL_REORG_BUFFER = 6; const SNAPSHOT_BURIAL_ACTIVATION = { mainnet: 0, // ARMED at genesis by the 2026-09-09 ruling: identity on the indexed mainnet history (ISSUE and ANCHOR only, measured 2026-09-09) @@ -416,8 +416,8 @@ const SNAPSHOT_BURIAL_ACTIVATION = { // the xchain-sync follower recomputes and HALTS on if they diverge. UNLIKE the two maps above, // this gates on the chain's OWN local block_index (each chain starts committing its own per-block // root at its own height); the Phase 2 checkpoint/ANCHOR extension that SIGNS these roots gates on -// snapshot_block. Kept byte-identical to the local copies in xchain-indexer/src/ -// state_commitment_activation.js + xchain-sync/src/state_commitment_activation.js (and xchain-hub +// snapshot_block. Kept byte-identical to the local copies in xchain-indexer/src/consensus/gates/ +// state_commitment_gate.js + xchain-sync/src/consensus/gates/state_commitment_gate.js (and xchain-hub // at Phase 2) by the cross-service regression suite. ARMED MID-CHAIN 2026-07-07 with per-chain // ':' keys (one shared height cannot fit BTC ~957k and DOGE ~6.28M at once; bare // network key remains for regtest; coin-less mainnet/testnet lookups stay inert). Same heights @@ -448,9 +448,10 @@ const STATE_COMMITMENT_ACTIVATION = { // checkpoint canonical, exactly like STAKE_WEIGHTED_QUORUM_ACTIVATION / EQUIV_HEADER_ACTIVATION, so the // hub and the BTC/LTC/DOGE indexers all flip the SIGNED shape on the same anchor. The operator MUST pick // a snapshot_block at/after which every checkpointed chain is already past its own STATE_COMMITMENT -// flag-day (else the engine would have no roots to sign). Kept byte-identical to the local copies in -// xchain-{hub,indexer,sdk,explorer,sync}/src/checkpoint_commitment_activation.js (sync consumes it at -// checkpoint.js to decide whether to expect the roots) by the cross-service regression suite. Same +// flag-day (else the engine would have no roots to sign). Kept equal to the registry row +// checkpoint_commitment_activation.CHECKPOINT_COMMITMENT_ACTIVATION every repo's registry parts carry +// (sync reads it at checkpoint.js to decide whether to expect the roots) by the cross-service +// regression suite. Same // ARMED height and deploy-by convention as the maps above: mainnet is armed to 961000 // (2026-07-07; BTC anchor ~2026-08-04), not a disabled placeholder. const CHECKPOINT_COMMITMENT_ACTIVATION = { @@ -471,7 +472,7 @@ const CHECKPOINT_COMMITMENT_ACTIVATION = { // credited reward becomes a COLLECT-spendable per-block ledger row), so it must deploy hub + ALL // indexers atomically. Like CHECKPOINT_COMMITMENT_ACTIVATION / STAKE_WEIGHTED_QUORUM_ACTIVATION it gates // on the BTC-anchored `snapshot_block` carried by every ANCHOR canonical. Kept byte-identical to the -// local copies in xchain-{hub,indexer}/src/anchor_reward_activation.js by the cross-service regression +// local copies in xchain-{hub,indexer}/src/consensus/gates/anchor_reward_gate.js by the cross-service regression // suite. Same ARMED height and deploy-by convention as the maps above: mainnet is armed to 961000 // (2026-07-07; BTC anchor ~2026-08-04), not a disabled placeholder. const ANCHOR_REWARD_ACTIVATION = { @@ -493,7 +494,7 @@ const ANCHOR_REWARD_AMOUNT = '10.00000000'; // stands and an archive head's PUBLISHER tail earns no derived credit. Consensus-relevant, same // deploy rules and snapshot_block gating as ANCHOR_REWARD_ACTIVATION; kept byte-identical to the // local copies in -// xchain-{hub,indexer}/src/anchor_reward_activation.js by the cross-service regression suite. +// xchain-{hub,indexer}/src/consensus/gates/anchor_reward_gate.js by the cross-service regression suite. const ARCHIVE_REWARD_ACTIVATION = { mainnet: 963000, // ARMED 2026-07-16, RE-PINNED 2026-08-12 off 969500 onto the shared pre-freeze train boundary (tip 959,853 on 07-27 at ~144 blocks/day + 21d); deploy every consumer before this era testnet: 0, @@ -541,7 +542,7 @@ const ARCHIVE_REWARD_AMOUNT = '10.00000000'; // and atomic-deploy rules as ANCHOR_REWARD_ACTIVATION. It CANNOT ride the 961000/963000 boundaries // (already live on testnet/regtest, so no coordinated flip window; and one gate must cover both // the `anchor_bundle` and `anchor_archive` reward families). Kept byte-identical to the local copies in -// xchain-{hub,indexer}/src/anchor_reward_activation.js by the cross-service regression suite. +// xchain-{hub,indexer}/src/consensus/gates/anchor_reward_gate.js by the cross-service regression suite. // Active from genesis on every network. Testnet was armed at 0 by the 2026-08-11 operator // ruling: it was re-genesised with no pre-flag history, so there is no legacy set to diverge // from and no mid-upgrade window to protect, and it is where the relocated derive path gets @@ -564,7 +565,7 @@ const ANCHOR_REWARD_DERIVE_ACTIVATION = { // node whose attestation mirror is not provably caught up defers the block rather than // deriving a partial set. It moves the block a COLLECT-spendable reward materializes at, so // it is a hashed value frozen with the map above; changing it needs its own flag-day. Kept -// byte-identical to xchain-{hub,indexer}/src/anchor_reward_activation.js. +// byte-identical to xchain-{hub,indexer}/src/consensus/gates/anchor_reward_gate.js. const ANCHOR_REWARD_MIRROR_MATURITY = 144; // ~24h of BTC blocks // ROLLCALL (validator liveness eviction). A roll call is a signed proof of presence bound to a @@ -581,7 +582,7 @@ const ANCHOR_REWARD_MIRROR_MATURITY = 144; // ~24h of BTC blocks // maturity and burial depths. By operator ruling 2026-09-01 that rule is SCOPED to networks with a // shared ledger: a regtest chain is private, no two regtest venues validate the same blocks, and // refusing a venue-pinned height only left the AT1-AT10 acceptance suite with nowhere to run. Kept -// byte-identical to xchain-{indexer,hub}/src/rollcall_activation.js by the cross-service suite. +// byte-identical to xchain-{indexer,hub}/src/consensus/gates/rollcall_gate.js by the cross-service suite. // // Keyed on the carried BTC EPOCH_HEIGHT on BOTH chains (the snapshot_block convention of // STAKE_WEIGHTED_QUORUM_ACTIVATION), never on either chain's local height, so a pre-activation @@ -630,7 +631,7 @@ const ROLLCALL_ACTIVATION = { // roll that lands between them. Regtest is env-derived on the ROLLCALL precedent and ships inert: // a venue that arms the ROLLCALL rail opts in here separately with XC_ROLLCALL_GATES_REGTEST_ACTIVATION. // The mainnet and testnet heights are kept value-identical to the local copies in -// xchain-{hub,indexer}/src/rollcall_gates_activation.js by the parity suite; regtest is not. +// xchain-{hub,indexer}/src/consensus/gates/rollcall_gates_gate.js by the parity suite; regtest is not. const ROLLCALL_GATES_REGTEST_ARMED_HEIGHT = 0; const ROLLCALL_GATES_REGTEST_ENV = 'XC_ROLLCALL_GATES_REGTEST_ACTIVATION'; function resolveRegtestGatesActivation(env){ @@ -707,8 +708,8 @@ const RETRACTION_SIGNING_ACTIVATION = { // gated separately by the CROSS_CHAIN_ROYALTY entry in the indexer's protocol_changes.js; the // operator MUST flip this canonical gate first or together with it, NEVER create-side first // (create-side ON with canonical OFF would put the legs in unsigned mirror fields, the exact -// tamper hole the legs-in-canonical design closes). Kept byte-identical to the local copies in -// xchain-{hub,indexer}/src/cross_chain_royalty_activation.js by the cross-service regression +// tamper hole the legs-in-canonical design closes). Kept equal to the registry row +// cross_chain_royalty_activation.CROSS_CHAIN_ROYALTY_ACTIVATION by the cross-service regression // suite. Same ARMED height and deploy-by convention as the maps above: mainnet is armed to // 961000 (2026-07-07; BTC anchor ~2026-08-04), not a disabled placeholder. const CROSS_CHAIN_ROYALTY_ACTIVATION = { @@ -728,7 +729,7 @@ const CROSS_CHAIN_ROYALTY_ACTIVATION = { // the legacy accept-then-expire behavior is preserved verbatim. Keyed on the request's own // block_index + network like STAKE_WEIGHTED_QUORUM_ACTIVATION (the shrink this closes comes // from that gate's source-dedupe), and armed to the SAME anchor so both rules flip together. -// Kept value-identical to the local copy in xchain-indexer/src/attest_admission_activation.js +// Kept value-identical to the registry row attest_admission_activation.ATTEST_ADMISSION_ACTIVATION // by the activation-constants parity suite. const ATTEST_ADMISSION_ACTIVATION = { mainnet: 961000, // ARMED: BTC anchor ~2026-08-04 (same anchor as STAKE_WEIGHTED_QUORUM); deploy ALL indexers before this height @@ -769,7 +770,7 @@ const ATTEST_ADMISSION_ACTIVATION = { // 2026-09-09 ruling on its own measurement: the explorer reports 0 attestation rows ever recorded // on BTC, LTC and DOGE mainnet (measured 2026-09-09), so no mainnet block can have exceeded the // cap and arming from genesis reinterprets nothing. -// Kept value-identical to the local copy in xchain-indexer/src/attest_request_cap_activation.js +// Kept value-identical to the local copy in xchain-indexer/src/actions/attest/attest_request_cap_gate.js // by the activation-constants parity suite. const ATTEST_REQUEST_CAP_ACTIVATION = { mainnet: 0, // ARMED at genesis by the 2026-09-09 ruling: identity on the indexed mainnet history (0 attestations, measured 2026-09-09) @@ -811,8 +812,8 @@ const ATTEST_REQUEST_CAPS = { // they expire on their own deadline (the pre-Phase-5 outcome); BTC-first leaves no origin request // to relay. // -// Armed on the shared mainnet cohort anchor. Kept value-identical to the local copies in -// xchain-{hub,indexer}/src/attest_relay_activation.js by the activation-constants parity suite. +// Armed on the shared mainnet cohort anchor. Kept value-identical to the registry row +// attest_relay_activation.ATTEST_RELAY_ACTIVATION by the activation-constants parity suite. const ATTEST_RELAY_ACTIVATION = { mainnet: 963000, // ARMED 2026-07-30 on the shared BTC anchor, RE-PINNED 2026-08-12 off 969500 with the rest of that cohort; deploy every indexer + hub before this height testnet: 0, @@ -856,7 +857,7 @@ const ATTEST_RELAY_ACTIVATION = { // below the height never widens and one admitted above always may: the rule for a given request // is fixed the moment it is admitted and cannot change mid-window. // -// Kept value-identical to the local copies in xchain-{hub,indexer}/src/attest_responsible_widening_activation.js +// Kept value-identical to the local copies in xchain-{hub,indexer}/src/consensus/gates/attest_responsible_widening_gate.js // by the activation-constants parity suite. const ATTEST_RESPONSIBLE_WIDENING_ACTIVATION = { mainnet: 0, // ARMED at genesis by the 2026-09-09 ruling: identity on the indexed mainnet history (0 attestations, measured 2026-09-09) @@ -908,7 +909,7 @@ const ATTEST_RESPONSIBLE_WIDENING = { // response. The height is therefore armed past a SYNCHRONIZED fleet window (hubs, indexers and // explorer together, the HUB_SCHEMA_VERSION 4->5 flip) with no request straddling it. // -// Kept value-identical to the local copies in xchain-{hub,indexer}/src/attest_response_mirror_activation.js +// Kept value-identical to the registry row attest_response_mirror_activation.ATTEST_RESPONSE_MIRROR_ACTIVATION // by the activation-constants parity suite. const ATTEST_RESPONSE_MIRROR_ACTIVATION = { mainnet: null, // INERT: operator-owned height, unratified. The legacy on-chain response path runs byte for byte. @@ -932,7 +933,8 @@ const ATTEST_RESPONSE_MIRROR_ACTIVATION = { // wave is confirmed complete, because change C is indexer-only and an old indexer strands a request a // new one binds. Floor on testnet: 151324. // -// Kept value-identical to the local copies in xchain-{hub,indexer}/src/attest_zero_conf_activation.js +// Kept value-identical to the registry row attest_zero_conf_activation.ATTEST_ZERO_CONF_ACTIVATION +// (the hub's src/attestation/attest_zero_conf_gate.js reads it) // by the activation-constants parity suite. const ATTEST_ZERO_CONF_ACTIVATION = { mainnet: null, // INERT: operator-owned height, unratified. Ratified only after the mirror arms there. @@ -948,7 +950,7 @@ const ATTEST_ZERO_CONF_ACTIVATION = { // all. maxSlots 2 is kept, so the pool can reach redundancy + 3. Headroom widens who may EARN, never // who is CHARGED: the persisted assignment and the missed_count charge stay on the unwidened slice. // -// Kept value-identical to the local copies in xchain-{hub,indexer}/src/attest_responsible_widening_activation.js +// Kept value-identical to the local copies in xchain-{hub,indexer}/src/consensus/gates/attest_responsible_widening_gate.js // by the activation-constants parity suite. const ATTEST_RESPONSIBLE_WIDENING_V2 = { startOffset: 0, @@ -974,7 +976,7 @@ const ATTEST_RESPONSIBLE_WIDENING_V2 = { // UNALLOCATED. The four sub-decisions (denomination, broadcaster identity, height, amount bound) // were pinned by the operator on 2026-08-11 with the HEIGHT explicitly reserved to the operator, // so the implementation ships inert. Nearby cohort anchors in use are 961000, 962500 and 969500. -// Kept value-identical to xchain-indexer/src/attest_broadcast_fee_activation.js by the +// Kept value-identical to xchain-indexer/src/actions/attest/attest_broadcast_fee_gate.js by the // activation-constants parity suite. // TESTNET ARMED AT 0, operator-ratified 2026-08-18 under the standing ruling that every platform // feature must be ACTIVE on testnet. Safe by MEASUREMENT, not assumption: this gate only changes how @@ -1384,7 +1386,7 @@ const PRICE_PAIR_WIDEN_ACTIVATION = { // whose height has already passed. Deploy every indexer AND every hub before // this height; they are peers here, not producer and consumer, so a split fleet // has the hub finalizing rounds the chain rejects. Kept value-identical to the -// local copies in xchain-{indexer,hub}/src/price_sig_tally_activation.js by the +// registry row price_sig_tally_activation.PRICE_SIG_TALLY_ACTIVATION by the // activation-constants parity suite. const PRICE_SIG_TALLY_ACTIVATION = { mainnet: 963000, // ARMED, RE-PINNED 2026-08-12 off 969500 onto the train boundary shared with RETRACTION_SIGNING; deploy ALL indexers + hubs before this height @@ -1415,8 +1417,8 @@ const PRICE_SIG_TALLY_ACTIVATION = { // ceiling holds, so each network arms at a coordinated future height once that is // proven against its own publisher; a regtest stack publishes no batch at all, so its // seeded rounds carry no landing clock and arming there would leave every USD-priced -// action unpriceable. Kept value-identical to the local copy in -// xchain-indexer/src/price_fee_batch_landed_activation.js by the activation-constants +// action unpriceable. Kept value-identical to the registry row +// price_fee_batch_landed_activation.PRICE_FEE_BATCH_LANDED_ACTIVATION by the activation-constants // parity suite. const PRICE_FEE_BATCH_LANDED_ACTIVATION = { mainnet: null, @@ -1502,11 +1504,25 @@ const TRAIN_ACTIVATION = { // bridge height below sits above it on the same BTC clock, so a node lacking this rule // set halts before it can grade a bridge action. '0.19.0': { mainnet: 9999999999, testnet: 152787, regtest: 0 }, + // The mirror-admission rule set, armed at the v0.20.0 cut: the producer and consumer + // admission maps and the anchor-attest barrier replace the effective_time binding, so a + // node without them grades an admission-stamped row under the rule it replaced. Mainnet + // holds the house sentinel because the whole family is null on mainnet under the + // 2026-08-29 write hold. Testnet: SIZED 2026-09-17 22:45Z, chain_tip TBTC 152,891 + 225 + // blocks, which is ceil(36 h / 576.7 s per block), about 36.0 h. The cadence is measured + // over a trailing window as long as the lead being sized (53 h here), never the last 99 + // blocks: a 99-block window on a testnet difficulty burst is noise, and it is what pulled + // 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 }, }; // STAKE v1 signing-key REUSE flag day, keyed on the processing chain's OWN -// block_index, per network AND coin. Canonical authority for the local copy in -// xchain-indexer/src/stake_key_reuse_activation.js, which carries the full rationale; +// block_index, per network AND coin. Canonical authority for the registry row +// stake_key_reuse_activation.STAKE_KEY_REUSE_ACTIVATION, which carries the full rationale; // the indexer's activation-constant parity suite holds the two value-identical, and a // one-sided edit forks STAKE v1 admission at the boundary. // @@ -1543,8 +1559,8 @@ const STAKE_KEY_REUSE_ACTIVATION = { }; // SWEEP zero-amount leg flag day, keyed on the processing chain's OWN block_index, -// per network AND coin. Canonical authority for the local copy in -// xchain-indexer/src/sweep_zero_leg_activation.js, which carries the full rationale; +// per network AND coin. Canonical authority for the registry row +// sweep_zero_leg_activation.SWEEP_ZERO_LEG_ACTIVATION, which carries the full rationale; // the indexer's activation-constant parity suite holds the two value-identical, and a // one-sided edit forks the per-block ledger hash at the boundary. // @@ -1576,8 +1592,8 @@ const SWEEP_ZERO_LEG_ACTIVATION = { }; // XCHAIN bridge flag day, keyed ':' on the block_index of the chain being -// parsed, with the bare network key as the fallback. Canonical authority for the local copy -// in xchain-indexer/src/xchain_bridge_activation.js, which carries the full rationale; the +// parsed, with the bare network key as the fallback. Canonical authority for the registry +// row xchain_bridge_activation.XCHAIN_BRIDGE_ACTIVATION, which carries the full rationale; the // indexer's activation-constant parity suite holds the two value-identical, and a one-sided // edit forks the bridge at the boundary. // @@ -1618,7 +1634,7 @@ const XCHAIN_BRIDGE_ACTIVATION = { }; // General token-bridge flag day (XBRIDGE v3/v4/v5 and ISSUE format 7), keyed the same way. -// Canonical authority for xchain-indexer/src/token_bridge_activation.js. +// Canonical authority for the registry row token_bridge_activation.TOKEN_BRIDGE_ACTIVATION. // // ORDERING INVARIANT, asserted by the indexer's parity suite over this map: // TOKEN_BRIDGE_ACTIVATION >= XCHAIN_BRIDGE_ACTIVATION per network. The general formats ride @@ -1635,8 +1651,8 @@ const TOKEN_BRIDGE_ACTIVATION = { regtest: 0, }; -// Token-policy inheritance flag day, keyed the same way. Canonical authority for -// xchain-indexer/src/token_policy_activation.js, which carries the full rationale. +// Token-policy inheritance flag day, keyed the same way. Canonical authority for the registry +// row token_policy_activation.TOKEN_POLICY_INHERITANCE_ACTIVATION, which carries the full rationale. // // At and above a network's height a token's origin-row policy binds every bridged copy: // the milestone-1 mutual exclusion in ISSUE lifts, LIST address items validate against any @@ -1663,7 +1679,7 @@ const TOKEN_POLICY_INHERITANCE_ACTIVATION = { }; // LIST owner-check flag day, keyed on the block_index of the chain being parsed. -// Canonical authority for xchain-indexer/src/list_owner_activation.js. +// Canonical authority for the registry row list_owner_activation.LIST_OWNER_ACTIVATION. // // At and above a network's height a LIST format 1 whose source is not the address that // created the list it names is 'invalid: LIST_ACTION_INDEX (not owner)'. Below it the edit @@ -1680,7 +1696,7 @@ const LIST_OWNER_ACTIVATION = { }; // Tick-namespace flag day (R8), keyed on the block_index of the chain being -// parsed. Canonical authority for xchain-indexer/src/tick_namespace_activation.js, which +// parsed. Canonical authority for the registry row tick_namespace_activation.TICK_NAMESPACE_ACTIVATION, which // carries the full rationale. // // At and above a network's height two rules bind in the ISSUE handler, beside the reserved @@ -1750,7 +1766,7 @@ const RESERVED_FUTURE_ROOTS = Object.freeze([ // watermark against B rather than a clock against t(B). Heights do not move with stamps, // so a block stamped 7200 s ahead is height B like any other. // -// Kept value-identical to the local copies in xchain-{hub,indexer}/src/mirror_admission_activation.js +// Kept value-identical to the local copies in xchain-{hub,indexer}/src/consensus/gates/mirror_admission_gate.js // by the activation-constants parity suite. // ADMIT_MARGIN_BLOCKS: how far ahead of the producer's observed admission tip a row is @@ -1858,13 +1874,64 @@ function resolveMirrorAdmissionRegtest(env){ // completes BEFORE any network's activation height: the heights map rides frames that carry // no schema_version, so a v7 indexer above the activation against a v6 hub would see no // heights at all and defer forever under the fail-closed rule. +// +// TESTNET SIZED 2026-09-16 20:41Z, from the three chain tips and their last-99-block cadences +// read in one sitting, read-only, off the public explorer's status and block pages: TBTC +// 152,756 at 498.7 s per block, TLTC 4,887,745 at 146.6 s, TDOGE 67,901,335 at 24.5 s. +// +// LTC AND DOGE RE-CUT 2026-09-17 22:45Z onto the same BTC instant, same method, because that +// 99-block window is not an estimator. LTC testnet then ran at about 82 s per block against +// the 146.6 s assumed, which pulled its producer boundary to 3.2 h out while BTC's stayed +// 53.0 h out, and DOGE's drifted 7.5 h early. Two legs of one cross-chain match would have +// crossed the flag day about two days apart, which is the failure the same-instant rule below +// exists to prevent. BTC is NOT re-cut: its height is keyed to an epoch close and the re-size +// trigger below has not fired. Re-cut tips and cadences: TBTC 152,891 at 576.7 s per block, +// TLTC 4,889,190 at 82.5 s, TDOGE 67,904,912 at 27.7 s. +// +// CADENCE WINDOW. Measure each chain over a TRAILING WALL-CLOCK WINDOW at least as long as the +// lead being sized, never over a fixed block count. At the 2026-09-17 re-cut one LTC tip read +// 8.0 s per block over the last 99 blocks, 43.5 s over 12 h, 82.5 s over 53 h and 111.7 s over +// 168 h; only the window that spans the lead answers the question being asked, and a short +// window sampled during a testnet difficulty burst is noise that ships as a consensus constant. +// +// The BTC PRODUCER height is the first roll-call epoch close at least 24 h above that tip +// (151,200 + 2 x 1008 = 153,216), plus one CANONICAL_REORG_BUFFER of 6. The epoch close is what +// makes the roll legal: a GATES membership change rolls BETWEEN epoch closes and never across +// one, and this places the whole roll, the v7 schema roll included, inside epoch +// [152208, 153216). The extra 6 keeps the rule change off the close block itself, so no hub +// grades that epoch's roll call while its own gate set is changing. The result is a 466-block, +// about 64.5 h lead, which covers the 90-minute roll budget many times over. +// +// LTC and DOGE take the SAME WALL-CLOCK INSTANT converted at their own measured cadence, never +// one shared number: a single height is an LTC height on an LTC indexer and a BTC height on a +// BTC indexer, so the two legs of one cross-chain match would cross the flag day at unrelated +// instants. Each CONSUMER height is its own producer plus 6 h on the same chain, which is the +// ordering rule above in its only safe direction. +// +// RE-SIZE RULE, PER CHAIN and not BTC alone. The v0.19.0 cut's first sizing was overrun by the +// chain while the train waited on its e2e matrix and had to be re-cut from a fresh tip, and the +// v0.20.0 sizing was overrun on LTC by a cadence that nearly halved while BTC's slowed. Before +// the carrying train rolls, re-measure all three tips and project each producer height forward +// at its own window cadence. Re-cut when EITHER holds: +// 1. The fleet has not rolled by TBTC 153,211 (this producer height less the 11 blocks a +// 90-minute roll takes at the measured cadence). Re-cut every testnet key here and in both +// registry twins from the NEXT epoch close, rather than shipping a height the chain passed. +// 2. Any chain's projected crossing sits more than 6 h from the BTC producer's. Re-cut that +// chain onto the BTC instant. Six hours is the consumer gap below and so the entire +// ordering margin this family has, in BOTH directions: a leg that crosses early leaves its +// consumer reading an admission column the other chain's producer has not written yet, and +// a leg that crosses late leaves the other chain's consumer reading one this leg's producer +// has not written yet. Both bind nothing. +// Trigger 1 is mechanical, a height comparison. Trigger 2 is a measurement, and LTC testnet's +// cadence varies by more than 2x across windows, so it is re-checked at the cut AND again +// immediately before the roll, not once. 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': null, // SIZED AT THE CUT: tip + roll window + slack, strictly below the consumer height - 'LTC:testnet': null, - 'DOGE:testnet': 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:regtest': resolveMirrorAdmissionRegtest(process.env), 'LTC:regtest': resolveMirrorAdmissionRegtest(process.env), 'DOGE:regtest': resolveMirrorAdmissionRegtest(process.env), @@ -1874,9 +1941,9 @@ const MIRROR_ADMISSION_CONSUMER_ACTIVATION = Object.freeze({ 'BTC:mainnet': null, 'LTC:mainnet': null, 'DOGE:mainnet': null, - 'BTC:testnet': null, // SIZED AT THE CUT, strictly ABOVE the producer height for the same key - 'LTC:testnet': null, - 'DOGE:testnet': 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:regtest': resolveMirrorAdmissionRegtest(process.env), 'LTC:regtest': resolveMirrorAdmissionRegtest(process.env), 'DOGE:regtest': resolveMirrorAdmissionRegtest(process.env), @@ -1898,7 +1965,7 @@ const MIRROR_ADMISSION_CONSUMER_ACTIVATION = Object.freeze({ // watermark at or past horizonTime + ANCHOR_ATTEST_ARRIVAL_MARGIN_S certifies the node holds // every row that pass will read, which is the completeness property in full. // -// Kept value-identical to the local copies in xchain-{hub,indexer}/src/anchor_reward_activation.js +// Kept value-identical to the local copies in xchain-{hub,indexer}/src/consensus/gates/anchor_reward_gate.js // by the activation-constants parity suite. // ANCHOR_ATTEST_ARRIVAL_MARGIN_S: sized against the hub's whole MEASURED write-lag envelope, @@ -1923,7 +1990,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 - testnet: null, // SIZED AT THE CUT from the measured tip plus the roll window + // 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, regtest: resolveMirrorAdmissionRegtest(process.env), // shares the family's arming seam so one venue lever arms both }); diff --git a/protocol/controller-bound-tokens.md b/protocol/controller-bound-tokens.md index ede0021..88115f0 100644 --- a/protocol/controller-bound-tokens.md +++ b/protocol/controller-bound-tokens.md @@ -345,7 +345,7 @@ the funds on both chains. Regtest note: BTC/LTC/DOGE regtest share base58 prefix re-encoding is a no-op there; address-level tests must use mainnet parameters. The canonical format flip is keyed on the BTC-anchored `snapshot_block` -(`cross_chain_royalty_activation.js`, a hub/indexer twin module), while the create-side +(the activation-registry row `cross_chain_royalty_activation.CROSS_CHAIN_ROYALTY_ACTIVATION`, carried by the hub and indexer registry parts), while the create-side acceptance rule is keyed on the local block (`protocol_changes.js`). Operators must 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 diff --git a/protocol/cross-chain-dex.md b/protocol/cross-chain-dex.md index 69b2d67..5f479ae 100644 --- a/protocol/cross-chain-dex.md +++ b/protocol/cross-chain-dex.md @@ -113,7 +113,7 @@ XMATCH|match_id|snapshot_block|a_chain|a_action_index|a_tick|a_amount|a_ownershi `0` for a whole-offer match. `a` is the canonical-lower chain. The four fill fields are **appended after `network`** so a `swap` record's leading bytes are unchanged from Phase 1. At/above the `CROSS_CHAIN_ROYALTY` flag-day, keyed on the BTC-anchored `snapshot_block` via -`cross_chain_royalty_activation.js`, two further fields `|a_payout_legs|b_payout_legs` are appended, +the activation-registry row `cross_chain_royalty_activation.CROSS_CHAIN_ROYALTY_ACTIVATION`, two further fields `|a_payout_legs|b_payout_legs` are appended, empty when a side has none: putting the royalty legs inside the signed bytes is what stops a colluding hub from stripping a royalty at settlement. Below the flag-day the canonical is byte-identical to the pre-royalty format.) A signature counts only if diff --git a/protocol/protocol-activation.md b/protocol/protocol-activation.md index bea9b86..ba440e4 100644 --- a/protocol/protocol-activation.md +++ b/protocol/protocol-activation.md @@ -246,17 +246,17 @@ inventoried on this page, the armed ones in this table and the mainnet-unarmed V | Gate | Keyed on | Mainnet threshold | Straggler | Lives in | |---|---|---|---|---| -| **SWQ source cap** (`SWQ_SOURCE_CAP_ACTIVATION`, caps `STAKE_WEIGHT_MAX_SOURCES=1000`, `STAKE_WEIGHT_MAX_KEYS_PER_SOURCE=64`) | BTC height | `BTC:mainnet` 960000 (after state commitment 958500, before stake-weighted quorum 961000; LTC/DOGE inert) | forks | `xchain-indexer` / `xchain-sync` `src/swq_source_cap_activation.js` | +| **SWQ source cap** (`SWQ_SOURCE_CAP_ACTIVATION`, caps `STAKE_WEIGHT_MAX_SOURCES=1000`, `STAKE_WEIGHT_MAX_KEYS_PER_SOURCE=64`) | BTC height | `BTC:mainnet` 960000 (after state commitment 958500, before stake-weighted quorum 961000; LTC/DOGE inert) | forks | `xchain-indexer` / `xchain-sync` `src/consensus/gates/swq_source_cap_gate.js` | | **Slash burns pending stake** (`SLASH_BURNS_PENDING_STAKE`) | BTC height | 961000 (Cohort-B anchor) | forks | `xchain-indexer/src/protocol_changes.js` | | **Slash oracle-round discriminated** (`SLASH_ORACLE_ROUND_DISCRIMINATED`, the sibling registry entry one row from slash-burns) | BTC height | 961000 (Cohort-B anchor) | forks | `xchain-indexer/src/protocol_changes.js` | | **VM deploy-lint Pkg 3** (`VM_DEPLOY_LINT_PKG3_ACTIVATION`, adds the two Package-3 deploy-blocking `CONSENSUS_RULES`, so it changes which contracts the chain accepts) | per-chain local height | `BTC:mainnet` 961000, `LTC:mainnet` 3154250, `DOGE:mainnet` 6319000 (armed 2026-07-22) | forks | registry row `vm_deploy_lint_pkg3_activation.VM_DEPLOY_LINT_PKG3_ACTIVATION` in `xchain-indexer/src/protocol_changes/gates_3.js`; the runtime half is `PKG3_SANDBOX_ACTIVATION` in `xchain-vm/src/index.js`, pinned to the same three heights so the deploy-time and execution-time halves stay coherent | | **Oracle snapshot-age causality** (`ORACLE_SNAPSHOT_AGE_CAUSALITY_ACTIVATION`, caps the snapshot-age query at the processing block; the uncapped value is VM-visible and forks `contract_hash` between a synced and a catching-up node) | per-chain local height | `BTC:mainnet` 961000, `LTC:mainnet` 3154250, `DOGE:mainnet` 6319000 (armed 2026-07-22) | forks | registry row `oracle_snapshot_age_causality_activation.ORACLE_SNAPSHOT_AGE_CAUSALITY_ACTIVATION` in `xchain-indexer/src/protocol_changes/gates_2.js` | | **Dispenser freshness** (`DISPENSER_FRESHNESS_ACTIVATION`, redefines freshness against indexer-local chain state instead of the external utxo tracker, changing which historical DISPENSER creates were valid) | per-chain local height | `BTC:mainnet` 961000, `LTC:mainnet` 3154250, `DOGE:mainnet` 6319000 (armed 2026-07-22) | forks | registry row `dispenser_freshness_activation.DISPENSER_FRESHNESS_ACTIVATION` in `xchain-indexer/src/protocol_changes/gates_1.js` | -| **List-edit resolution** (`LIST_EDIT_RESOLUTION_ACTIVATION`, resolves a list to its newest valid edit; `getList` gates BET place, ORDER/SWAP match, DISPENSE, DIVIDEND, CALLBACK and AIRDROP, so action acceptance changes) | per-chain local height | `BTC:mainnet` 963000, `LTC:mainnet` 3162000, `DOGE:mainnet` 6338000 | forks | `xchain-indexer` / `xchain-explorer` `src/list_edit_resolution_activation.js` | +| **List-edit resolution** (`LIST_EDIT_RESOLUTION_ACTIVATION`, resolves a list to its newest valid edit; `getList` gates BET place, ORDER/SWAP match, DISPENSE, DIVIDEND, CALLBACK and AIRDROP, so action acceptance changes) | per-chain local height | `BTC:mainnet` 963000, `LTC:mainnet` 3162000, `DOGE:mainnet` 6338000 | forks | `xchain-indexer` / `xchain-explorer` the registry row `list_edit_resolution_activation.LIST_EDIT_RESOLUTION_ACTIVATION` | | **Caret-ref strict** (`CARET_REF_STRICT_ACTIVATION`, makes an unresolvable address reference a hard reject at three sites that previously failed open, which moves the block's credits and debits) | per-chain local height | `BTC:mainnet` 963000, `LTC:mainnet` 3162000, `DOGE:mainnet` 6338000 (kept value-equal to list-edit resolution) | forks | `xchain-indexer/src/db/database/caret_ref_strict_gate.js` | | **Oracle stale-round visibility** (`ORACLE_STALE_ROUND_VISIBILITY_ACTIVATION`, keeps a stale tip round in the `getPrice()` view with its price withheld instead of dropping the round outright, so a contract can tell an oracle stall apart from an oracle that never ran; VM-visible, so it changes `contract_hash`) | per-chain local height | `BTC:mainnet` 966500, `LTC:mainnet` 3175500, `DOGE:mainnet` 6370000 (pinned ahead of the tip the first release carrying the gate deploys at, so the flag day has no retroactive window; it does NOT share the list-edit resolution boundary, which rides an earlier release) | forks | registry row `oracle_stale_round_visibility_activation.ORACLE_STALE_ROUND_VISIBILITY_ACTIVATION` in `xchain-indexer/src/protocol_changes/gates_2.js` | | **Ledger amount precision** (`LEDGER_AMOUNT_PRECISION_ACTIVATION`, quantizes every ledger write at 18 dp, the finest precision a tick can be issued with, instead of the written tick's own `decimals`; `db.createLedgerChangeRecord` takes the scale from `ledgerWriteScale` and applies it as `bcadd(amount, 0, decimals)`, so the persisted amounts and the balances projected from them both move) | per-chain local height | `BTC:mainnet` 966500, `LTC:mainnet` 3175500, `DOGE:mainnet` 6370000 (pinned to the same boundary as oracle stale-round visibility so both arm in one fleet deploy, and above each chain's tip at pinning so the flag day has no retroactive window) | forks | `xchain-indexer/src/consensus/ledger_amount_precision_gate.js` (indexer-only: the rule sits on the ledger write path, which `xchain-sync` never re-runs, so it has no twin and no twin drift guard) | -| **State-key collation** (`STATE_KEY_COLLATION_ACTIVATION`) | per-chain local height | `BTC:mainnet` 962500, `LTC:mainnet` 3160000, `DOGE:mainnet` 6335000 (armed 2026-07-10, ~10 days past Cohort-B) | halts, recoverable | `xchain-indexer` / `xchain-sync` `src/state_key_collation_activation.js` | +| **State-key collation** (`STATE_KEY_COLLATION_ACTIVATION`) | per-chain local height | `BTC:mainnet` 962500, `LTC:mainnet` 3160000, `DOGE:mainnet` 6335000 (armed 2026-07-10, ~10 days past Cohort-B) | halts, recoverable | `xchain-indexer` / `xchain-sync` the registry row `state_key_collation_activation.STATE_KEY_COLLATION_ACTIVATION` | | **DISPENSE cancelling-dispenser match** (`DISPENSE_CANCELLING_MATCH_ACTIVATION`, corrects the `db.findMatchingDispensers` latest-status correlation on the native-coin DISPENSE trigger path) | block time | the coordinated 2.0.0 [contract-era flag day](./flag-days.md#contract-era-flag-day); deploy all indexers before it | forks | registry row `dispense_cancelling_match_activation.DISPENSE_CANCELLING_MATCH_ACTIVATION` in `xchain-indexer/src/protocol_changes/gates_1.js` | The SWQ source cap, slash-burns and slash-oracle-round gates are BTC-height forking rules that belong diff --git a/protocol/reference-impl/equivocation_header.js b/protocol/reference-impl/consensus/equivocation_header.js similarity index 98% rename from protocol/reference-impl/equivocation_header.js rename to protocol/reference-impl/consensus/equivocation_header.js index 6d262cd..48e1199 100644 --- a/protocol/reference-impl/equivocation_header.js +++ b/protocol/reference-impl/consensus/equivocation_header.js @@ -43,7 +43,7 @@ * ********************************************************************/ -const { get, copy, activeAt } = require('./consensus/gate_registry'); +const { get, copy, activeAt } = require('./gate_registry'); const EQUIV_HEADER_ACTIVATION = copy('equivocation_header.EQUIV_HEADER_ACTIVATION'); 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 59c78a9..bf311be 100644 --- a/protocol/reference-impl/consensus/gate_registry/shared_rows_1.js +++ b/protocol/reference-impl/consensus/gate_registry/shared_rows_1.js @@ -295,7 +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 - testnet: null, // SIZED AT THE CUT from the measured tip plus the roll window + // 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, 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 3b03a0b..c7fd765 100644 --- a/protocol/reference-impl/consensus/gate_registry/shared_rows_2.js +++ b/protocol/reference-impl/consensus/gate_registry/shared_rows_2.js @@ -204,19 +204,21 @@ addGate('mirror_admission_activation.ADMIT_MAX_FUTURE_BLOCKS', 'constant', { * height on a BTC indexer, so the two legs of one cross-chain match would cross the flag day at * unrelated instants. The 'COIN:network' key shape is established precedent. * - * Mainnet is null under the 2026-08-29 write hold. Testnet is sized at the release cut from the - * measured tip plus the roll window plus slack, per key. The v7 HUB_SCHEMA_VERSION roll - * completes BEFORE any network's activation height: the heights map rides frames carrying no - * schema_version, so a v7 indexer above the activation against a v6 hub would see no heights at - * all and defer forever under the fail-closed rule. + * Mainnet is null under the 2026-08-29 write hold. TESTNET SIZED 2026-09-16 20:41Z, LTC and DOGE + * RE-CUT 2026-09-17 22:45Z onto the BTC instant after their cadences drifted off it; the measured + * tips, the formula, the cadence-window rule, the epoch-close rule and the per-chain re-size rule + * are written once in the canon (xchain-documentation/protocol/constants.js), which this row is + * held value-identical to. The v7 HUB_SCHEMA_VERSION roll completes BEFORE any of these heights: + * the heights map rides frames carrying no schema_version, so a v7 indexer above the activation + * against a v6 hub would see no heights at all and defer forever under the fail-closed rule. */ addGate('mirror_admission_activation.MIRROR_ADMISSION_ACTIVATION', 'height', { 'BTC:mainnet': null, 'LTC:mainnet': null, 'DOGE:mainnet': null, - 'BTC:testnet': null, // SIZED AT THE CUT, strictly below the consumer height for this key - 'LTC:testnet': null, - 'DOGE:testnet': 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: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 @@ -226,9 +228,9 @@ addGate('mirror_admission_activation.MIRROR_ADMISSION_CONSUMER_ACTIVATION', 'hei 'BTC:mainnet': null, 'LTC:mainnet': null, 'DOGE:mainnet': null, - 'BTC:testnet': null, // SIZED AT THE CUT, strictly above the producer height for this key - 'LTC:testnet': null, - 'DOGE:testnet': 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: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 92f3979..cbf2897 100644 --- a/protocol/reference-impl/consensus/gate_registry/shared_rows_5.js +++ b/protocol/reference-impl/consensus/gate_registry/shared_rows_5.js @@ -97,6 +97,20 @@ addGate('train_activation.TRAIN_ACTIVATION', 'ruleset', { // bridge height below sits above it on the same BTC clock, so a node lacking this rule // set halts before it can grade a bridge action. '0.19.0': { mainnet: 9999999999, testnet: 152787, regtest: 0 }, + // The mirror-admission rule set, armed at the v0.20.0 cut: the producer and consumer + // admission maps and the anchor-attest barrier replace the effective_time binding, so a + // node without them grades an admission-stamped row under the rule it replaced. Mainnet + // holds the house sentinel because the whole family is null on mainnet under the + // 2026-08-29 write hold. Testnet: SIZED 2026-09-17 22:45Z, chain_tip TBTC 152,891 + 225 + // blocks, which is ceil(36 h / 576.7 s per block), about 36.0 h. The cadence is measured + // over a trailing window as long as the lead being sized (53 h here), never the last 99 + // blocks: a 99-block window on a testnet difficulty burst is noise, and it is what pulled + // 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 }, }); // xchain_bridge_activation diff --git a/protocol/reference-impl/snapshot_reorg_buffer.js b/protocol/reference-impl/consensus/snapshot_reorg_buffer.js similarity index 98% rename from protocol/reference-impl/snapshot_reorg_buffer.js rename to protocol/reference-impl/consensus/snapshot_reorg_buffer.js index 7e2bb9e..28ea9da 100644 --- a/protocol/reference-impl/snapshot_reorg_buffer.js +++ b/protocol/reference-impl/consensus/snapshot_reorg_buffer.js @@ -72,7 +72,7 @@ 'use strict'; -const { get, copy, activeAt } = require('./consensus/gate_registry'); +const { get, copy, activeAt } = require('./gate_registry'); const CANONICAL_REORG_BUFFER = copy('snapshot_reorg_buffer.CANONICAL_REORG_BUFFER'); diff --git a/protocol/reference-impl/stake_weighted_quorum.js b/protocol/reference-impl/consensus/stake_weighted_quorum.js similarity index 99% rename from protocol/reference-impl/stake_weighted_quorum.js rename to protocol/reference-impl/consensus/stake_weighted_quorum.js index 493d7f1..76fdd4e 100644 --- a/protocol/reference-impl/stake_weighted_quorum.js +++ b/protocol/reference-impl/consensus/stake_weighted_quorum.js @@ -32,7 +32,7 @@ * ********************************************************************/ -const { get, copy, activeAt } = require('./consensus/gate_registry'); +const { get, copy, activeAt } = require('./gate_registry'); const mathjs = require('mathjs'); diff --git a/test/contract-state-proof-availability.test.js b/test/contract-state-proof-availability.test.js index 1b0a7a1..2117eb7 100644 --- a/test/contract-state-proof-availability.test.js +++ b/test/contract-state-proof-availability.test.js @@ -52,7 +52,7 @@ const path = require('node:path'); const { sibling } = require('./helpers/sibling_checkout.js'); const DOC_ROOT = path.resolve(__dirname, '..'); -const ACTIVATION = path.resolve(__dirname, '../../xchain-explorer/src/state_subtree_activation.js'); +const ACTIVATION = path.resolve(__dirname, '../../xchain-explorer/src/consensus/gates/state_subtree_gate.js'); // Skips by name on a bare clone; throws under XCHAIN_REQUIRE_SIBLINGS=1 when the armed map is unreadable. const explorer = sibling('xchain-explorer', [ACTIVATION]); diff --git a/test/flag-day-literals.test.js b/test/flag-day-literals.test.js index 5ee6b67..492b823 100644 --- a/test/flag-day-literals.test.js +++ b/test/flag-day-literals.test.js @@ -140,24 +140,23 @@ test('all three gate-collection paths still find their gates', { skip: noIndexer 'DEPLOY_BASE64_CODE is absent from collectGates(): the addChange(...) parse in ' + 'bin/generate-flag-days.js stopped matching, so protocol/flag-days.md is short a row.'); - // Sibling `const NAME_ACTIVATION = { mainnet: ... }` path - // (collectSiblingGates). The two anchors above read one named file; this - // path reads a DIRECTORY listing filtered on the `_activation.js` suffix, - // so a module renamed off that suffix, moved out of src/, or restyled into - // one of the shapes the scan is deliberately quiet about (`mainnet: - // SOME_CONSTANT`, a bare `null`) drops its row with nothing loud. The - // fixture tests below prove the scan's logic against a throwaway tree and - // by construction cannot see that drift in the real one. Today this path - // alone sources three published rows. + // Registry-row path (the `addGate(...)` parts). This gate once rode the + // sibling `const NAME_ACTIVATION = { mainnet: ... }` scan (collectSiblingGates), + // which reads a DIRECTORY listing filtered on the `_activation.js` suffix; since + // W5 the indexer keeps no such file and the map is a registry row, so the row + // reaches the page through the registry arm alone. A row the arm stops + // reading drops out with nothing loud; the fixture tests below prove the + // scan's logic against a throwaway tree and by construction cannot see that + // drift in the real one. assert.ok(byName.has('DISPENSER_CAPS_ACTIVATION'), - 'DISPENSER_CAPS_ACTIVATION is absent from collectGates(): the sibling `*_activation.js` ' - + 'scan in bin/generate-flag-days.js stopped reaching its module, so protocol/flag-days.md ' - + 'is short a row. It is declared only in xchain-indexer/src/dispenser_caps_activation.js.'); + 'DISPENSER_CAPS_ACTIVATION is absent from collectGates(): the registry arm in ' + + 'bin/generate-flag-days.js stopped reading its row, so protocol/flag-days.md ' + + 'is short a row. It is declared only as the registry row dispenser_caps_activation.DISPENSER_CAPS_ACTIVATION.'); // All four ride the coordinated instant. Asserted as equality against a // value read from the registry, never as a literal: a repin is legitimate // and must not have to edit this file. The sibling gate belongs in this - // list on its own module's authority: dispenser_caps_activation.js pins + // list on its own row's authority: the dispenser_caps_activation row pins // mainnet to "the coordinated 2.0.0 contract-era flag-day" in the comment // above the map. const anchor = gen.coordinatedFlagDay(gen.collectGates()).time; diff --git a/test/vectors.test.js b/test/vectors.test.js index 224c608..572e8b9 100644 --- a/test/vectors.test.js +++ b/test/vectors.test.js @@ -25,9 +25,9 @@ const assert = require('node:assert/strict'); const { test, describe } = require('node:test'); const constants = require('../protocol/constants.js'); -const swq = require('../protocol/reference-impl/stake_weighted_quorum.js'); -const eqh = require('../protocol/reference-impl/equivocation_header.js'); -const srb = require('../protocol/reference-impl/snapshot_reorg_buffer.js'); +const swq = require('../protocol/reference-impl/consensus/stake_weighted_quorum.js'); +const eqh = require('../protocol/reference-impl/consensus/equivocation_header.js'); +const srb = require('../protocol/reference-impl/consensus/snapshot_reorg_buffer.js'); const swqVectors = require('../protocol/test-vectors/stake_weighted_quorum.json'); const eqhVectors = require('../protocol/test-vectors/equivocation_header.json');