From ae18f995ec1938331461895290908327a27d3817 Mon Sep 17 00:00:00 2001 From: J-Dog Date: Wed, 16 Sep 2026 02:48:00 -0700 Subject: [PATCH 1/9] refactor(activation): W5 consolidation, twin gate modules and carriers at the src/consensus tails from the indexer's bytes, citations and requirers repointed (activation registry row 21c) --- bin/generate-flag-days.js | 11 ++- components/explorer/architecture.md | 6 +- components/hub/architecture.md | 2 +- components/hub/decentralization.md | 2 +- components/indexer/architecture.md | 6 +- components/sync/architecture.md | 12 +-- concepts/actions.md | 2 +- concepts/block-hashes.md | 2 +- protocol/actions/anchor.md | 2 +- protocol/actions/rollcall.md | 2 +- protocol/constants.js | 82 ++++++++++--------- protocol/controller-bound-tokens.md | 2 +- protocol/cross-chain-dex.md | 2 +- protocol/protocol-activation.md | 6 +- .../{ => consensus}/equivocation_header.js | 2 +- .../{ => consensus}/snapshot_reorg_buffer.js | 2 +- .../{ => consensus}/stake_weighted_quorum.js | 2 +- .../contract-state-proof-availability.test.js | 2 +- test/flag-day-literals.test.js | 25 +++--- test/vectors.test.js | 6 +- 20 files changed, 91 insertions(+), 87 deletions(-) rename protocol/reference-impl/{ => consensus}/equivocation_header.js (98%) rename protocol/reference-impl/{ => consensus}/snapshot_reorg_buffer.js (98%) rename protocol/reference-impl/{ => consensus}/stake_weighted_quorum.js (99%) 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/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/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/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/constants.js b/protocol/constants.js index 71a5450..32fce00 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, @@ -1493,8 +1495,8 @@ const TRAIN_ACTIVATION = { }; // 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. // @@ -1531,8 +1533,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. // @@ -1564,8 +1566,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. // @@ -1599,7 +1601,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 @@ -1616,8 +1618,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 @@ -1644,7 +1646,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 @@ -1661,7 +1663,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 @@ -1731,7 +1733,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 @@ -1879,7 +1881,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, 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/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'); From 9fde8cd1815899ba8894d2f4c8a13044fd525d3b Mon Sep 17 00:00:00 2001 From: J-Dog Date: Wed, 16 Sep 2026 20:30:10 -0700 Subject: [PATCH 2/9] docs(protocol): barrier family canon, testnet mirror admission and anchor-attest barrier heights in constants.js --- protocol/constants.js | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/protocol/constants.js b/protocol/constants.js index 3c33367..ce18596 100644 --- a/protocol/constants.js +++ b/protocol/constants.js @@ -1860,13 +1860,37 @@ 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. +// +// 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. 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. If the carrying train has not rolled +// the fleet by TBTC 153,211 (this producer height less the 11 blocks a 90-minute roll takes at +// the measured cadence), re-measure all three tips and re-cut every testnet key here and in both +// registry twins from the NEXT epoch close, rather than shipping a height the chain has passed. 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': 4889331, // the same instant: tip 4,887,745 + 1586 at 146.6 s/blk + 'DOGE:testnet': 67910821, // the same instant: tip 67,901,335 + 9486 at 24.5 s/blk 'BTC:regtest': resolveMirrorAdmissionRegtest(process.env), 'LTC:regtest': resolveMirrorAdmissionRegtest(process.env), 'DOGE:regtest': resolveMirrorAdmissionRegtest(process.env), @@ -1876,9 +1900,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': 4889479, // its producer + 148 blocks, about 6 h + 'DOGE:testnet': 67911703, // its producer + 882 blocks, about 6 h 'BTC:regtest': resolveMirrorAdmissionRegtest(process.env), 'LTC:regtest': resolveMirrorAdmissionRegtest(process.env), 'DOGE:regtest': resolveMirrorAdmissionRegtest(process.env), @@ -1925,7 +1949,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 }); From f6fc5a1c7a0555666b609917d1533933fdedca44 Mon Sep 17 00:00:00 2001 From: J-Dog Date: Wed, 16 Sep 2026 20:31:21 -0700 Subject: [PATCH 3/9] chore(twins): vendor the barrier-family registry rows (shared_rows_1 and shared_rows_2 from the indexer) --- .../consensus/gate_registry/shared_rows_1.js | 6 ++++- .../consensus/gate_registry/shared_rows_2.js | 23 ++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) 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..eda6ba3 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,20 @@ 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; the measured + * tips, the formula, the epoch-close rule and the 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': 4889331, // the same instant: tip 4,887,745 + 1586 at 146.6 s/blk + 'DOGE:testnet': 67910821, // the same instant: tip 67,901,335 + 9486 at 24.5 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 +227,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': 4889479, // its producer + 148 blocks, about 6 h + 'DOGE:testnet': 67911703, // its producer + 882 blocks, about 6 h '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 From 84c1811df9400c83a196ce7a3bae334665a3775e Mon Sep 17 00:00:00 2001 From: J-Dog Date: Wed, 16 Sep 2026 20:36:23 -0700 Subject: [PATCH 4/9] docs(protocol): key the token bridge, policy and tick namespace maps per chain and arm testnet --- CHANGELOG.md | 3 + protocol/constants.js | 106 ++++++++++++++---- .../consensus/gate_registry/shared_rows_4.js | 35 +++--- .../consensus/gate_registry/shared_rows_5.js | 44 ++++++-- 4 files changed, 143 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b15e1..9999669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- `TOKEN_BRIDGE_ACTIVATION` and `TOKEN_POLICY_INHERITANCE_ACTIVATION` are keyed by coin and network and arm testnet at BTC 153160, LTC 4888478 and DOGE 67906525. + ## [0.19.0] - 2026-09-16 ### Added diff --git a/protocol/constants.js b/protocol/constants.js index ce18596..d393675 100644 --- a/protocol/constants.js +++ b/protocol/constants.js @@ -1623,18 +1623,51 @@ const XCHAIN_BRIDGE_ACTIVATION = { // 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 +// TOKEN_BRIDGE_ACTIVATION >= XCHAIN_BRIDGE_ACTIVATION per chain key. The general formats ride // the same hub engine, the same mirrored transfer table and the same settle pass as // XCHAIN's, so a train that armed v3 without the XCHAIN bridge behind it would admit locks // that nothing can ever finalize and that no burn can ever return. // -// Testnet is NOT armed alongside the XCHAIN bridge: no third-party token can be offered on -// a hub-trusted mint, so this gate waits on the checkpoint cross-check landing on that -// network. Regtest is genesis-active. +// KEYED ':' since the v0.20.0 arming train, with the bare network key as the +// fallback. That invariant is per chain key and XCHAIN_BRIDGE_ACTIVATION is three different +// testnet heights on three chains whose tips differ by orders of magnitude, so a single +// testnet number here is either unreachable on two of them or already passed on two. +// +// Mainnet holds the house sentinel on every key: milestone 1 is a hub-trusted mint, no +// third-party token can be offered on that basis, and nothing arms on mainnet before the +// checkpoint cross-check lands there. Regtest is genesis-active. +// +// THE TESTNET SIZING, v0.20.0 cut. Tips and cadences read in ONE sitting at +// 2026-09-17 02:42:30Z from the public explorer status endpoint and its per-block +// timestamps, cadence measured over the 99 blocks below each tip: +// +// chain tip cadence lead blocks height +// TBTC 152,780 487.2 s 51.3 h +380 153160 +// TLTC 4,887,866 184.2 s 31.3 h +612 4888478 +// TDOGE 67,902,163 25.8 s 31.3 h +4362 67906525 +// +// Each lead runs from the earliest plausible v0.20.0 fleet-roll completion, 2026-09-18 +// 00:00Z or 21.3 h after that read, plus the cut kit's own lead after it: 10 h for the two +// destinations and 30 h for BTC. BTC is the ORIGIN of the v3 lock and so arms LAST in wall +// clock, because the lock handler never checks the destination's own activation; a +// destination arming later would admit a lock nothing can mint, and the 3x gap is the band +// a destination cadence can slow by before that ordering breaks. Nothing is rounded, in +// either direction: rounding a destination height up can push its instant past the +// origin's. Every slot clears its own XCHAIN_BRIDGE_ACTIVATION slot (152929 / 4887898 / +// 67902062) and must also sit above the v0.20.0 TRAIN_ACTIVATION testnet boundary on the +// BTC clock, which the cut kit sizes and which has to land below the LTC and DOGE instants +// here. A cut that slips past those instants re-sizes them from fresh tips, as the v0.19.0 +// cut re-sized its own. const TOKEN_BRIDGE_ACTIVATION = { - mainnet: 9999999999, - testnet: 9999999999, - regtest: 0, + 'BTC:mainnet': 9999999999, + 'LTC:mainnet': 9999999999, + 'DOGE:mainnet': 9999999999, + mainnet: 9999999999, // fallback for a coin with no entry above + 'BTC:testnet': 153160, // SIZED 2026-09-17 02:42Z: chain_tip 152,780 + 380 (51.3 h at 487.2 s/blk), the origin, last + 'LTC:testnet': 4888478, // SIZED 2026-09-17 02:42Z: chain_tip 4,887,866 + 612 (31.3 h at 184.2 s/blk) + 'DOGE:testnet': 67906525, // SIZED 2026-09-17 02:42Z: chain_tip 67,902,163 + 4362 (31.3 h at 25.8 s/blk) + testnet: 9999999999, // fallback: a testnet coin with no entry above stays dark + regtest: 0, // genesis-active so the e2e rail exercises the armed rule }; // Token-policy inheritance flag day, keyed the same way. Canonical authority for the registry @@ -1648,7 +1681,7 @@ const TOKEN_BRIDGE_ACTIVATION = { // and no snapshot is signed, so pre-activation block hashes are unchanged on every chain. // // TWO ORDERING INVARIANTS, asserted by the indexer's parity suite over this map: -// TOKEN_POLICY_INHERITANCE_ACTIVATION >= TOKEN_BRIDGE_ACTIVATION per network. Inheritance +// TOKEN_POLICY_INHERITANCE_ACTIVATION >= TOKEN_BRIDGE_ACTIVATION per chain key. Inheritance // has nothing to inherit onto until bridged copies can exist. // TOKEN_POLICY_INHERITANCE_ACTIVATION >= LIST_EDIT_RESOLUTION_ACTIVATION per chain and // network (that map is indexer-local: BTC 963000, LTC 3162000, DOGE 6338000 on mainnet, @@ -1656,12 +1689,29 @@ const TOKEN_BRIDGE_ACTIVATION = { // walking the edit chain; below that gate the legacy create-index read runs, and the // membership the federation signs would not be the membership the chain enforced. // -// Both public networks hold at the house sentinel until the train that arms them sizes a -// dated instant. Regtest is genesis-active. +// KEYED ':' since the v0.20.0 arming train, tracking the token bridge's own +// re-keying: the first invariant is per chain key and the bridge is three testnet heights, +// so a single testnet number here could satisfy at most one of the three chains. +// +// Mainnet holds the house sentinel on every key until the checkpoint cross-check lands +// there. Regtest is genesis-active. +// +// Testnet is armed AT THE SAME HEIGHT as the token bridge on each chain, not above it. Both +// flag days ship on the v0.20.0 train and the milestone-2 code is already in the binary, so +// a gap between them would buy nothing and cost something real: between the two heights +// bridged copies could exist with no policy to inherit and no in-leg barrier holding a v5 +// credit until the tick has one, which is the unpoliced window the barrier exists to close. +// Equality satisfies the first invariant, which is `>=`. const TOKEN_POLICY_INHERITANCE_ACTIVATION = { - mainnet: 9999999999, - testnet: 9999999999, - regtest: 0, + 'BTC:mainnet': 9999999999, + 'LTC:mainnet': 9999999999, + 'DOGE:mainnet': 9999999999, + mainnet: 9999999999, // fallback for a coin with no entry above + 'BTC:testnet': 153160, // == TOKEN_BRIDGE_ACTIVATION BTC:testnet, sized 2026-09-17 02:42Z + 'LTC:testnet': 4888478, // == TOKEN_BRIDGE_ACTIVATION LTC:testnet + 'DOGE:testnet': 67906525, // == TOKEN_BRIDGE_ACTIVATION DOGE:testnet + testnet: 9999999999, // fallback: a testnet coin with no entry above stays dark + regtest: 0, // genesis-active so the e2e rail exercises the armed rule }; // LIST owner-check flag day, keyed on the block_index of the chain being parsed. @@ -1685,7 +1735,7 @@ const LIST_OWNER_ACTIVATION = { // 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 +// At and above a chain's height two rules bind in the ISSUE handler, beside the reserved // guard: a top-level ISSUE that would CREATE a tick shorter than four characters is // 'invalid: TICK (length)' (creation only, so every short token already issued keeps its // owner and its admin surface), and every ticker in RESERVED_FUTURE_ROOTS below is @@ -1698,13 +1748,29 @@ const LIST_OWNER_ACTIVATION = { // ahead of the fee and budget checks, so a mined ISSUE of a listed name that is refused // today on fee would flip its verdict string on replay. // -// Both public networks hold at the house sentinel until the train that arms them sizes a -// dated instant; mainnet additionally waits on a replica measurement of zero mined ISSUEs -// of a short or listed name, valid or invalid. Regtest is genesis-active. +// ORDERING INVARIANT, asserted by the indexer's parity suite over this map: +// TICK_NAMESPACE_ACTIVATION <= TOKEN_BRIDGE_ACTIVATION per chain key, so the bridge never +// roots a foreign asset on a chain whose matching root is still on sale. +// +// KEYED ':' since the v0.20.0 arming train, with the bare network key as the +// fallback, because TOKEN_BRIDGE_ACTIVATION is. A single testnet number at or below BTC's +// 153160 would sit millions of blocks under the TLTC and TDOGE tips and re-verdict every +// short or reserved ISSUE already mined on those two chains. +// +// Testnet arms AT the token bridge height on each chain (153160 / 4888478 / 67906525). The +// tips read 2026-09-17 03:08:30Z were 152,781 / 4,887,868 / 67,902,208, so every slot is +// above its chain's tip and no mined ISSUE changes verdict; and each slot keeps the post-roll +// lead the bridge was sized with, which an earlier height would give up for a few hours of +// earlier closure the invariant does not ask for. Mainnet holds the house sentinel until a +// replica measurement of zero mined ISSUEs of a short or listed name, valid or invalid. +// Regtest is genesis-active. const TICK_NAMESPACE_ACTIVATION = { - mainnet: 9999999999, - testnet: 9999999999, - regtest: 0, + mainnet: 9999999999, + 'BTC:testnet': 153160, // == TOKEN_BRIDGE_ACTIVATION BTC:testnet, sized 2026-09-17 02:42Z + 'LTC:testnet': 4888478, // == TOKEN_BRIDGE_ACTIVATION LTC:testnet + 'DOGE:testnet': 67906525, // == TOKEN_BRIDGE_ACTIVATION DOGE:testnet + testnet: 9999999999, // fallback: a testnet coin with no entry above stays dark + regtest: 0, }; // Chain tickers held free for chains XChain has not integrated yet, refused as diff --git a/protocol/reference-impl/consensus/gate_registry/shared_rows_4.js b/protocol/reference-impl/consensus/gate_registry/shared_rows_4.js index 4a5063c..81f498d 100644 --- a/protocol/reference-impl/consensus/gate_registry/shared_rows_4.js +++ b/protocol/reference-impl/consensus/gate_registry/shared_rows_4.js @@ -373,21 +373,28 @@ addGate('swq_source_cap_activation.SWQ_SOURCE_CAP_ACTIVATION', 'height', { }); // token_bridge_activation -// TOKEN_BRIDGE_ACTIVATION: the height (per network) on the chain being parsed at/above -// which XBRIDGE v3/v4 and ISSUE format 7 are legal. Below it v3 and v4 return the base -// spec's own string 'invalid: XBRIDGE before activation', v5 is never injected, and an -// ISSUE|7 keeps the parse verdict 'invalid: VERSION (unknown)' so no historical ISSUE on -// any chain changes status on replay. +// TOKEN_BRIDGE_ACTIVATION: the height on the chain being parsed at/above which XBRIDGE +// v3/v4 and ISSUE format 7 are legal. Below it v3 and v4 return the base spec's own +// 'invalid: XBRIDGE before activation', v5 is never injected and an ISSUE|7 keeps the +// verdict 'invalid: VERSION (unknown)', so no historical ISSUE changes status on replay. // -// Keyed on the chain's OWN block_index, as XCHAIN_BRIDGE_ACTIVATION. -// -// Mainnet and testnet sit at the house sentinel 9999999999. Testnet is NOT armed with the -// XCHAIN bridge: no third-party token can be offered on a hub-trusted mint, so this gate -// waits on the base spec's D2 checkpoint cross-check being built and armed on that -// network. Regtest is 0 so the e2e rail exercises the armed rule from genesis. +// KEYED ':' since the v0.20.0 arming train, bare network as the fallback: +// the shape XCHAIN_BRIDGE_ACTIVATION uses, and for its reason. This map sits at or above +// that one on EVERY chain key and the three testnet tips differ by orders of magnitude, +// so one testnet number is either unreachable on two chains or already passed on two. +// Mainnet holds the sentinel on every key until the base spec's D2 checkpoint cross-check +// arms there. Testnet was sized per chain at the v0.20.0 cut, the two destinations first +// and BTC (the v3 lock's origin) last, because the lock handler never reads the +// destination's own activation; the arithmetic is in the canonical constants.js. addGate('token_bridge_activation.TOKEN_BRIDGE_ACTIVATION', 'height', { - mainnet: 9999999999, - testnet: 9999999999, - regtest: 0, + 'BTC:mainnet': 9999999999, + 'LTC:mainnet': 9999999999, + 'DOGE:mainnet': 9999999999, + mainnet: 9999999999, // fallback for a coin with no entry above + 'BTC:testnet': 153160, // SIZED 2026-09-17 02:42Z: chain_tip 152,780 + 380 (51.3 h at 487.2 s/blk), the origin, last + 'LTC:testnet': 4888478, // SIZED 2026-09-17 02:42Z: chain_tip 4,887,866 + 612 (31.3 h at 184.2 s/blk) + 'DOGE:testnet': 67906525, // SIZED 2026-09-17 02:42Z: chain_tip 67,902,163 + 4362 (31.3 h at 25.8 s/blk) + testnet: 9999999999, // fallback: a testnet coin with no entry above stays dark + regtest: 0, // genesis-active so the e2e rail exercises the armed rule }); // SHARED-GATES END 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..e7c2b48 100644 --- a/protocol/reference-impl/consensus/gate_registry/shared_rows_5.js +++ b/protocol/reference-impl/consensus/gate_registry/shared_rows_5.js @@ -52,23 +52,45 @@ const { addGate, UNARMED, UNPINNED } = require('./shared_rows.js'); // Below it every milestone-1 verdict stands unchanged, so the replay corpus is // hash-identical on every chain with this code present. // -// Mainnet and testnet are the house sentinel 9999999999: this rides the same MAJOR -// train as the two bridges and the operator sizes the dated instant at the cut. A -// height in the map ahead of the fleet's deploy tip is the operator's act, not a -// build's. Regtest is 0 so the e2e rail exercises the armed rule from genesis. +// KEYED ':' since the v0.20.0 arming train, with the bare network key as +// the fallback, tracking TOKEN_BRIDGE_ACTIVATION's own re-keying on that train. The +// reason is the first invariant below: inheritance has to sit at or above the token +// bridge, the token bridge is now three testnet heights on three chains whose tips differ +// by orders of magnitude, and a single testnet number here could satisfy at most one of +// them. A coin with no entry of its own inherits the bare network key, so an unlisted +// chain is inert rather than undecided. // -// TWO ORDERING INVARIANTS, asserted by test/unit/activationConstantsParity.test.js -// over the canonical constants.js rather than over this copy: -// - >= TOKEN_BRIDGE_ACTIVATION per network. Inheritance has nothing to inherit onto +// Mainnet is the house sentinel 9999999999 on every key: this rides the same MAJOR train +// as the two bridges and nothing arms on mainnet before the base spec's D2 checkpoint +// cross-check lands there. +// +// Testnet is armed AT THE SAME HEIGHT as the token bridge on each chain, not above it. +// Both flag days ship on the v0.20.0 train and the milestone-2 code is already in the +// binary, so a gap between them would buy nothing and cost something real: between the +// two heights bridged copies could exist with no policy to inherit and no in-leg barrier +// holding a v5 credit until the tick has one, which is exactly the unpoliced window the +// barrier exists to close. Equality satisfies the first invariant, which is `>=`. +// +// TWO ORDERING INVARIANTS, asserted by the activation-constant parity suite over the +// canonical constants.js rather than over this copy: +// - >= TOKEN_BRIDGE_ACTIVATION per chain key. Inheritance has nothing to inherit onto // before bridged copies can exist. // - >= LIST_EDIT_RESOLUTION_ACTIVATION per chain and network. The snapshot read // resolves a list AS OF origin_block through getListAtBlock, which walks the edit // chain; below that gate the legacy create-index read runs and the membership the -// federation signs would not be the membership the chain actually held. +// federation signs would not be the membership the chain actually held. That map is +// 0 on all three testnet chains since the 2026-08-10 fresh genesis, so every testnet +// height here clears it. addGate('token_policy_activation.TOKEN_POLICY_INHERITANCE_ACTIVATION', 'height', { - mainnet: 9999999999, - testnet: 9999999999, - regtest: 0, + 'BTC:mainnet': 9999999999, + 'LTC:mainnet': 9999999999, + 'DOGE:mainnet': 9999999999, + mainnet: 9999999999, // fallback for a coin with no entry above + 'BTC:testnet': 153160, // == TOKEN_BRIDGE_ACTIVATION BTC:testnet, sized 2026-09-17 02:42Z + 'LTC:testnet': 4888478, // == TOKEN_BRIDGE_ACTIVATION LTC:testnet + 'DOGE:testnet': 67906525, // == TOKEN_BRIDGE_ACTIVATION DOGE:testnet + testnet: 9999999999, // fallback: a testnet coin with no entry above stays dark + regtest: 0, // genesis-active so the e2e rail exercises the armed rule }); // train_activation From b91596aaab2257dc851965559626671bd8a9cf13 Mon Sep 17 00:00:00 2001 From: J-Dog Date: Wed, 16 Sep 2026 20:59:51 -0700 Subject: [PATCH 5/9] docs(changelog): v0.20.0 barrier family, per-chain token and tick namespace arming, carrier re-pin --- CHANGELOG.md | 3 ++- protocol/actions/xbridge.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9999669..a0257af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed -- `TOKEN_BRIDGE_ACTIVATION` and `TOKEN_POLICY_INHERITANCE_ACTIVATION` are keyed by coin and network and arm testnet at BTC 153160, LTC 4888478 and DOGE 67906525. +- `TOKEN_BRIDGE_ACTIVATION`, `TOKEN_POLICY_INHERITANCE_ACTIVATION` and `TICK_NAMESPACE_ACTIVATION` are keyed by coin and network and arm testnet at BTC 153160, LTC 4888478 and DOGE 67906525. +- Mirrored rows carry per-chain admission heights so a future-dated block no longer stalls indexer processing while the mirror proves completeness by height. ## [0.19.0] - 2026-09-16 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. From 0d73f45eeeebdede8d82f63de7cb8dfa6c35aef1 Mon Sep 17 00:00:00 2001 From: J-Dog Date: Wed, 16 Sep 2026 21:17:24 -0700 Subject: [PATCH 6/9] docs(config): document MA_SIDE_KEY and HUB_SNAPSHOT_RATE_LIMIT_RPM, indexer computed-read baseline 7 to 8 --- components/hub/configuration.md | 3 ++- components/indexer/configuration.md | 20 ++++++++++++++++++++ lib/env-var-doc-coverage.js | 10 +++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) 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/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/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, From 91c99b64ec4696a9022882d6adcc629ed15b336f Mon Sep 17 00:00:00 2001 From: J-Dog Date: Thu, 17 Sep 2026 05:40:19 -0700 Subject: [PATCH 7/9] revert(token-bridge): drop the per-chain testnet arming from the v0.20.0 train (dq3 (a)) This reverts commit 84c1811df9400c83a196ce7a3bae334665a3775e; the token bridge and policy gates stay dark on v0.20.0 and arm on the next train. --- CHANGELOG.md | 1 - protocol/constants.js | 106 ++++-------------- .../consensus/gate_registry/shared_rows_4.js | 35 +++--- .../consensus/gate_registry/shared_rows_5.js | 44 ++------ 4 files changed, 45 insertions(+), 141 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0257af..c0298a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed -- `TOKEN_BRIDGE_ACTIVATION`, `TOKEN_POLICY_INHERITANCE_ACTIVATION` and `TICK_NAMESPACE_ACTIVATION` are keyed by coin and network and arm testnet at BTC 153160, LTC 4888478 and DOGE 67906525. - Mirrored rows carry per-chain admission heights so a future-dated block no longer stalls indexer processing while the mirror proves completeness by height. ## [0.19.0] - 2026-09-16 diff --git a/protocol/constants.js b/protocol/constants.js index d393675..ce18596 100644 --- a/protocol/constants.js +++ b/protocol/constants.js @@ -1623,51 +1623,18 @@ const XCHAIN_BRIDGE_ACTIVATION = { // 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 chain key. The general formats ride +// TOKEN_BRIDGE_ACTIVATION >= XCHAIN_BRIDGE_ACTIVATION per network. The general formats ride // the same hub engine, the same mirrored transfer table and the same settle pass as // XCHAIN's, so a train that armed v3 without the XCHAIN bridge behind it would admit locks // that nothing can ever finalize and that no burn can ever return. // -// KEYED ':' since the v0.20.0 arming train, with the bare network key as the -// fallback. That invariant is per chain key and XCHAIN_BRIDGE_ACTIVATION is three different -// testnet heights on three chains whose tips differ by orders of magnitude, so a single -// testnet number here is either unreachable on two of them or already passed on two. -// -// Mainnet holds the house sentinel on every key: milestone 1 is a hub-trusted mint, no -// third-party token can be offered on that basis, and nothing arms on mainnet before the -// checkpoint cross-check lands there. Regtest is genesis-active. -// -// THE TESTNET SIZING, v0.20.0 cut. Tips and cadences read in ONE sitting at -// 2026-09-17 02:42:30Z from the public explorer status endpoint and its per-block -// timestamps, cadence measured over the 99 blocks below each tip: -// -// chain tip cadence lead blocks height -// TBTC 152,780 487.2 s 51.3 h +380 153160 -// TLTC 4,887,866 184.2 s 31.3 h +612 4888478 -// TDOGE 67,902,163 25.8 s 31.3 h +4362 67906525 -// -// Each lead runs from the earliest plausible v0.20.0 fleet-roll completion, 2026-09-18 -// 00:00Z or 21.3 h after that read, plus the cut kit's own lead after it: 10 h for the two -// destinations and 30 h for BTC. BTC is the ORIGIN of the v3 lock and so arms LAST in wall -// clock, because the lock handler never checks the destination's own activation; a -// destination arming later would admit a lock nothing can mint, and the 3x gap is the band -// a destination cadence can slow by before that ordering breaks. Nothing is rounded, in -// either direction: rounding a destination height up can push its instant past the -// origin's. Every slot clears its own XCHAIN_BRIDGE_ACTIVATION slot (152929 / 4887898 / -// 67902062) and must also sit above the v0.20.0 TRAIN_ACTIVATION testnet boundary on the -// BTC clock, which the cut kit sizes and which has to land below the LTC and DOGE instants -// here. A cut that slips past those instants re-sizes them from fresh tips, as the v0.19.0 -// cut re-sized its own. +// Testnet is NOT armed alongside the XCHAIN bridge: no third-party token can be offered on +// a hub-trusted mint, so this gate waits on the checkpoint cross-check landing on that +// network. Regtest is genesis-active. const TOKEN_BRIDGE_ACTIVATION = { - 'BTC:mainnet': 9999999999, - 'LTC:mainnet': 9999999999, - 'DOGE:mainnet': 9999999999, - mainnet: 9999999999, // fallback for a coin with no entry above - 'BTC:testnet': 153160, // SIZED 2026-09-17 02:42Z: chain_tip 152,780 + 380 (51.3 h at 487.2 s/blk), the origin, last - 'LTC:testnet': 4888478, // SIZED 2026-09-17 02:42Z: chain_tip 4,887,866 + 612 (31.3 h at 184.2 s/blk) - 'DOGE:testnet': 67906525, // SIZED 2026-09-17 02:42Z: chain_tip 67,902,163 + 4362 (31.3 h at 25.8 s/blk) - testnet: 9999999999, // fallback: a testnet coin with no entry above stays dark - regtest: 0, // genesis-active so the e2e rail exercises the armed rule + mainnet: 9999999999, + testnet: 9999999999, + regtest: 0, }; // Token-policy inheritance flag day, keyed the same way. Canonical authority for the registry @@ -1681,7 +1648,7 @@ const TOKEN_BRIDGE_ACTIVATION = { // and no snapshot is signed, so pre-activation block hashes are unchanged on every chain. // // TWO ORDERING INVARIANTS, asserted by the indexer's parity suite over this map: -// TOKEN_POLICY_INHERITANCE_ACTIVATION >= TOKEN_BRIDGE_ACTIVATION per chain key. Inheritance +// TOKEN_POLICY_INHERITANCE_ACTIVATION >= TOKEN_BRIDGE_ACTIVATION per network. Inheritance // has nothing to inherit onto until bridged copies can exist. // TOKEN_POLICY_INHERITANCE_ACTIVATION >= LIST_EDIT_RESOLUTION_ACTIVATION per chain and // network (that map is indexer-local: BTC 963000, LTC 3162000, DOGE 6338000 on mainnet, @@ -1689,29 +1656,12 @@ const TOKEN_BRIDGE_ACTIVATION = { // walking the edit chain; below that gate the legacy create-index read runs, and the // membership the federation signs would not be the membership the chain enforced. // -// KEYED ':' since the v0.20.0 arming train, tracking the token bridge's own -// re-keying: the first invariant is per chain key and the bridge is three testnet heights, -// so a single testnet number here could satisfy at most one of the three chains. -// -// Mainnet holds the house sentinel on every key until the checkpoint cross-check lands -// there. Regtest is genesis-active. -// -// Testnet is armed AT THE SAME HEIGHT as the token bridge on each chain, not above it. Both -// flag days ship on the v0.20.0 train and the milestone-2 code is already in the binary, so -// a gap between them would buy nothing and cost something real: between the two heights -// bridged copies could exist with no policy to inherit and no in-leg barrier holding a v5 -// credit until the tick has one, which is the unpoliced window the barrier exists to close. -// Equality satisfies the first invariant, which is `>=`. +// Both public networks hold at the house sentinel until the train that arms them sizes a +// dated instant. Regtest is genesis-active. const TOKEN_POLICY_INHERITANCE_ACTIVATION = { - 'BTC:mainnet': 9999999999, - 'LTC:mainnet': 9999999999, - 'DOGE:mainnet': 9999999999, - mainnet: 9999999999, // fallback for a coin with no entry above - 'BTC:testnet': 153160, // == TOKEN_BRIDGE_ACTIVATION BTC:testnet, sized 2026-09-17 02:42Z - 'LTC:testnet': 4888478, // == TOKEN_BRIDGE_ACTIVATION LTC:testnet - 'DOGE:testnet': 67906525, // == TOKEN_BRIDGE_ACTIVATION DOGE:testnet - testnet: 9999999999, // fallback: a testnet coin with no entry above stays dark - regtest: 0, // genesis-active so the e2e rail exercises the armed rule + mainnet: 9999999999, + testnet: 9999999999, + regtest: 0, }; // LIST owner-check flag day, keyed on the block_index of the chain being parsed. @@ -1735,7 +1685,7 @@ const LIST_OWNER_ACTIVATION = { // parsed. Canonical authority for the registry row tick_namespace_activation.TICK_NAMESPACE_ACTIVATION, which // carries the full rationale. // -// At and above a chain's height two rules bind in the ISSUE handler, beside the reserved +// At and above a network's height two rules bind in the ISSUE handler, beside the reserved // guard: a top-level ISSUE that would CREATE a tick shorter than four characters is // 'invalid: TICK (length)' (creation only, so every short token already issued keeps its // owner and its admin surface), and every ticker in RESERVED_FUTURE_ROOTS below is @@ -1748,29 +1698,13 @@ const LIST_OWNER_ACTIVATION = { // ahead of the fee and budget checks, so a mined ISSUE of a listed name that is refused // today on fee would flip its verdict string on replay. // -// ORDERING INVARIANT, asserted by the indexer's parity suite over this map: -// TICK_NAMESPACE_ACTIVATION <= TOKEN_BRIDGE_ACTIVATION per chain key, so the bridge never -// roots a foreign asset on a chain whose matching root is still on sale. -// -// KEYED ':' since the v0.20.0 arming train, with the bare network key as the -// fallback, because TOKEN_BRIDGE_ACTIVATION is. A single testnet number at or below BTC's -// 153160 would sit millions of blocks under the TLTC and TDOGE tips and re-verdict every -// short or reserved ISSUE already mined on those two chains. -// -// Testnet arms AT the token bridge height on each chain (153160 / 4888478 / 67906525). The -// tips read 2026-09-17 03:08:30Z were 152,781 / 4,887,868 / 67,902,208, so every slot is -// above its chain's tip and no mined ISSUE changes verdict; and each slot keeps the post-roll -// lead the bridge was sized with, which an earlier height would give up for a few hours of -// earlier closure the invariant does not ask for. Mainnet holds the house sentinel until a -// replica measurement of zero mined ISSUEs of a short or listed name, valid or invalid. -// Regtest is genesis-active. +// Both public networks hold at the house sentinel until the train that arms them sizes a +// dated instant; mainnet additionally waits on a replica measurement of zero mined ISSUEs +// of a short or listed name, valid or invalid. Regtest is genesis-active. const TICK_NAMESPACE_ACTIVATION = { - mainnet: 9999999999, - 'BTC:testnet': 153160, // == TOKEN_BRIDGE_ACTIVATION BTC:testnet, sized 2026-09-17 02:42Z - 'LTC:testnet': 4888478, // == TOKEN_BRIDGE_ACTIVATION LTC:testnet - 'DOGE:testnet': 67906525, // == TOKEN_BRIDGE_ACTIVATION DOGE:testnet - testnet: 9999999999, // fallback: a testnet coin with no entry above stays dark - regtest: 0, + mainnet: 9999999999, + testnet: 9999999999, + regtest: 0, }; // Chain tickers held free for chains XChain has not integrated yet, refused as diff --git a/protocol/reference-impl/consensus/gate_registry/shared_rows_4.js b/protocol/reference-impl/consensus/gate_registry/shared_rows_4.js index 81f498d..4a5063c 100644 --- a/protocol/reference-impl/consensus/gate_registry/shared_rows_4.js +++ b/protocol/reference-impl/consensus/gate_registry/shared_rows_4.js @@ -373,28 +373,21 @@ addGate('swq_source_cap_activation.SWQ_SOURCE_CAP_ACTIVATION', 'height', { }); // token_bridge_activation -// TOKEN_BRIDGE_ACTIVATION: the height on the chain being parsed at/above which XBRIDGE -// v3/v4 and ISSUE format 7 are legal. Below it v3 and v4 return the base spec's own -// 'invalid: XBRIDGE before activation', v5 is never injected and an ISSUE|7 keeps the -// verdict 'invalid: VERSION (unknown)', so no historical ISSUE changes status on replay. +// TOKEN_BRIDGE_ACTIVATION: the height (per network) on the chain being parsed at/above +// which XBRIDGE v3/v4 and ISSUE format 7 are legal. Below it v3 and v4 return the base +// spec's own string 'invalid: XBRIDGE before activation', v5 is never injected, and an +// ISSUE|7 keeps the parse verdict 'invalid: VERSION (unknown)' so no historical ISSUE on +// any chain changes status on replay. // -// KEYED ':' since the v0.20.0 arming train, bare network as the fallback: -// the shape XCHAIN_BRIDGE_ACTIVATION uses, and for its reason. This map sits at or above -// that one on EVERY chain key and the three testnet tips differ by orders of magnitude, -// so one testnet number is either unreachable on two chains or already passed on two. -// Mainnet holds the sentinel on every key until the base spec's D2 checkpoint cross-check -// arms there. Testnet was sized per chain at the v0.20.0 cut, the two destinations first -// and BTC (the v3 lock's origin) last, because the lock handler never reads the -// destination's own activation; the arithmetic is in the canonical constants.js. +// Keyed on the chain's OWN block_index, as XCHAIN_BRIDGE_ACTIVATION. +// +// Mainnet and testnet sit at the house sentinel 9999999999. Testnet is NOT armed with the +// XCHAIN bridge: no third-party token can be offered on a hub-trusted mint, so this gate +// waits on the base spec's D2 checkpoint cross-check being built and armed on that +// network. Regtest is 0 so the e2e rail exercises the armed rule from genesis. addGate('token_bridge_activation.TOKEN_BRIDGE_ACTIVATION', 'height', { - 'BTC:mainnet': 9999999999, - 'LTC:mainnet': 9999999999, - 'DOGE:mainnet': 9999999999, - mainnet: 9999999999, // fallback for a coin with no entry above - 'BTC:testnet': 153160, // SIZED 2026-09-17 02:42Z: chain_tip 152,780 + 380 (51.3 h at 487.2 s/blk), the origin, last - 'LTC:testnet': 4888478, // SIZED 2026-09-17 02:42Z: chain_tip 4,887,866 + 612 (31.3 h at 184.2 s/blk) - 'DOGE:testnet': 67906525, // SIZED 2026-09-17 02:42Z: chain_tip 67,902,163 + 4362 (31.3 h at 25.8 s/blk) - testnet: 9999999999, // fallback: a testnet coin with no entry above stays dark - regtest: 0, // genesis-active so the e2e rail exercises the armed rule + mainnet: 9999999999, + testnet: 9999999999, + regtest: 0, }); // SHARED-GATES END 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 e7c2b48..92f3979 100644 --- a/protocol/reference-impl/consensus/gate_registry/shared_rows_5.js +++ b/protocol/reference-impl/consensus/gate_registry/shared_rows_5.js @@ -52,45 +52,23 @@ const { addGate, UNARMED, UNPINNED } = require('./shared_rows.js'); // Below it every milestone-1 verdict stands unchanged, so the replay corpus is // hash-identical on every chain with this code present. // -// KEYED ':' since the v0.20.0 arming train, with the bare network key as -// the fallback, tracking TOKEN_BRIDGE_ACTIVATION's own re-keying on that train. The -// reason is the first invariant below: inheritance has to sit at or above the token -// bridge, the token bridge is now three testnet heights on three chains whose tips differ -// by orders of magnitude, and a single testnet number here could satisfy at most one of -// them. A coin with no entry of its own inherits the bare network key, so an unlisted -// chain is inert rather than undecided. +// Mainnet and testnet are the house sentinel 9999999999: this rides the same MAJOR +// train as the two bridges and the operator sizes the dated instant at the cut. A +// height in the map ahead of the fleet's deploy tip is the operator's act, not a +// build's. Regtest is 0 so the e2e rail exercises the armed rule from genesis. // -// Mainnet is the house sentinel 9999999999 on every key: this rides the same MAJOR train -// as the two bridges and nothing arms on mainnet before the base spec's D2 checkpoint -// cross-check lands there. -// -// Testnet is armed AT THE SAME HEIGHT as the token bridge on each chain, not above it. -// Both flag days ship on the v0.20.0 train and the milestone-2 code is already in the -// binary, so a gap between them would buy nothing and cost something real: between the -// two heights bridged copies could exist with no policy to inherit and no in-leg barrier -// holding a v5 credit until the tick has one, which is exactly the unpoliced window the -// barrier exists to close. Equality satisfies the first invariant, which is `>=`. -// -// TWO ORDERING INVARIANTS, asserted by the activation-constant parity suite over the -// canonical constants.js rather than over this copy: -// - >= TOKEN_BRIDGE_ACTIVATION per chain key. Inheritance has nothing to inherit onto +// TWO ORDERING INVARIANTS, asserted by test/unit/activationConstantsParity.test.js +// over the canonical constants.js rather than over this copy: +// - >= TOKEN_BRIDGE_ACTIVATION per network. Inheritance has nothing to inherit onto // before bridged copies can exist. // - >= LIST_EDIT_RESOLUTION_ACTIVATION per chain and network. The snapshot read // resolves a list AS OF origin_block through getListAtBlock, which walks the edit // chain; below that gate the legacy create-index read runs and the membership the -// federation signs would not be the membership the chain actually held. That map is -// 0 on all three testnet chains since the 2026-08-10 fresh genesis, so every testnet -// height here clears it. +// federation signs would not be the membership the chain actually held. addGate('token_policy_activation.TOKEN_POLICY_INHERITANCE_ACTIVATION', 'height', { - 'BTC:mainnet': 9999999999, - 'LTC:mainnet': 9999999999, - 'DOGE:mainnet': 9999999999, - mainnet: 9999999999, // fallback for a coin with no entry above - 'BTC:testnet': 153160, // == TOKEN_BRIDGE_ACTIVATION BTC:testnet, sized 2026-09-17 02:42Z - 'LTC:testnet': 4888478, // == TOKEN_BRIDGE_ACTIVATION LTC:testnet - 'DOGE:testnet': 67906525, // == TOKEN_BRIDGE_ACTIVATION DOGE:testnet - testnet: 9999999999, // fallback: a testnet coin with no entry above stays dark - regtest: 0, // genesis-active so the e2e rail exercises the armed rule + mainnet: 9999999999, + testnet: 9999999999, + regtest: 0, }); // train_activation From 779056b2eeb1bf37766503b3a20e2b8c1be00e84 Mon Sep 17 00:00:00 2001 From: J-Dog Date: Thu, 17 Sep 2026 13:58:55 -0700 Subject: [PATCH 8/9] chore(release): bump to 0.20.0 and record the changelog --- CHANGELOG.md | 11 ++++++++++- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0298a7..56be39f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,17 @@ 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 while the mirror proves completeness by height. +- 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 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", From 9929cd2084ab6f436ee05c006ba0f3f3bcd223ca Mon Sep 17 00:00:00 2001 From: J-Dog Date: Thu, 17 Sep 2026 15:52:11 -0700 Subject: [PATCH 9/9] fix(consensus): re-cut the LTC and DOGE mirror-admission heights onto the BTC instant The v0.20.0 family was sized 2026-09-16 20:41Z from last-99-block cadences. LTC testnet then ran at about 82 s per block against the 146.6 s that sizing assumed, pulling 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 what the same-wall-clock-instant rule exists to prevent. Re-measured 2026-09-17 22:45Z over a trailing window as long as the lead being sized: 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. BTC is unchanged, still keyed to epoch close 153,216 plus 6 buried; LTC and DOGE are converted onto that same instant, and each consumer is its own producer plus six hours at its own measured cadence rather than a block count carried over from the first sizing. Arms the TRAIN_ACTIVATION 0.20.0 row at testnet 153,116, 106 blocks and about 17 h below the BTC producer. resolveRuleSet reads only the local map, so with no row every block above the boundary keeps resolving under 0.19.0, and a manifest naming 0.20.0 halts a fleet in which no build implements it. Records the cadence-window rule the re-cut used and makes the re-size rule per chain instead of BTC-keyed, so an LTC or DOGE drift past the six-hour ordering margin forces a re-cut the way an overrun BTC height already does. (cherry picked from commit c80423afa277f292bc3c8844911c52012c8b09ae) --- protocol/constants.js | 59 ++++++++++++++++--- .../consensus/gate_registry/shared_rows_2.js | 21 +++---- .../consensus/gate_registry/shared_rows_5.js | 14 +++++ 3 files changed, 75 insertions(+), 19 deletions(-) diff --git a/protocol/constants.js b/protocol/constants.js index ce18596..966f5f6 100644 --- a/protocol/constants.js +++ b/protocol/constants.js @@ -1504,6 +1504,20 @@ 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 @@ -1865,6 +1879,21 @@ function resolveMirrorAdmissionRegtest(env){ // 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 @@ -1879,18 +1908,30 @@ function resolveMirrorAdmissionRegtest(env){ // 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. 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. If the carrying train has not rolled -// the fleet by TBTC 153,211 (this producer height less the 11 blocks a 90-minute roll takes at -// the measured cadence), re-measure all three tips and re-cut every testnet key here and in both -// registry twins from the NEXT epoch close, rather than shipping a height the chain has passed. +// 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': 153222, // epoch close 153,216 + 6 buried; tip 152,756 + 466 at 498.7 s/blk, about 64.5 h - 'LTC:testnet': 4889331, // the same instant: tip 4,887,745 + 1586 at 146.6 s/blk - 'DOGE:testnet': 67910821, // the same instant: tip 67,901,335 + 9486 at 24.5 s/blk + '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), @@ -1901,8 +1942,8 @@ const MIRROR_ADMISSION_CONSUMER_ACTIVATION = Object.freeze({ 'LTC:mainnet': null, 'DOGE:mainnet': null, 'BTC:testnet': 153266, // its producer + 44 blocks, about 6 h: strictly above, never equal - 'LTC:testnet': 4889479, // its producer + 148 blocks, about 6 h - 'DOGE:testnet': 67911703, // its producer + 882 blocks, about 6 h + '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), 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 eda6ba3..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,20 +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 SIZED 2026-09-16 20:41Z; the measured - * tips, the formula, the epoch-close rule and the 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. + * 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': 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': 4889331, // the same instant: tip 4,887,745 + 1586 at 146.6 s/blk - 'DOGE:testnet': 67910821, // the same instant: tip 67,901,335 + 9486 at 24.5 s/blk + '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 @@ -228,8 +229,8 @@ addGate('mirror_admission_activation.MIRROR_ADMISSION_CONSUMER_ACTIVATION', 'hei 'LTC:mainnet': null, 'DOGE:mainnet': null, 'BTC:testnet': 153266, // its producer + 44 blocks, about 6 h: strictly above, never equal - 'LTC:testnet': 4889479, // its producer + 148 blocks, about 6 h - 'DOGE:testnet': 67911703, // its producer + 882 blocks, about 6 h + '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