Skip to content

Release v0.15.0 - xchain-hub - #17

Merged
jdogresorg merged 61 commits into
masterfrom
release/v0.15.0
Sep 7, 2026
Merged

Release v0.15.0 - xchain-hub#17
jdogresorg merged 61 commits into
masterfrom
release/v0.15.0

Conversation

@jdogresorg

Copy link
Copy Markdown
Contributor

Cuts xchain-hub for the v0.15.0 release train. 58 commits from master.

Added

  • ATTEST responses are written to a hub mirror table, gossiped to every hub, verified before they are stored, and served over the snapshot route and stream; inert until the response-mirror activation height, which is armed on regtest only.
  • Each hour's finalized responses are published as a signed ATTEST batch on Dogecoin, keyed on the signed effective time, and landed batches are accepted back.
  • The response's effective time is inside the signed canonical and the body is capped before anyone signs it.
  • getattestationresponsibleset answers which validators a request drew.
  • A lost-round watchdog per seat, oracle round presence over RPC, and a stake-share watcher that warns before the weighted commit gate halts rounds.
  • A frozen bitcoin tip that pins the checkpoint cadence slot is metered and reported.
  • A regtest venue can arm ROLLCALL from its environment.
  • SWEEP and CALLBACK are priced on the unified fee schedule.

Fixed

  • A price window that closed while the hub was down is published on restart.
  • Oracle takeover holds off while a co-signed batch may still be in flight.
  • Local callers are exempt from the per-IP cap and a 429 is answered as JSON-RPC.
  • getallconfigs redacts secret-bearing params; include_secrets needs its own key tier.
  • Out-of-band oracle round numbers are refused at ingest and stored ones are reported through diagnostics.
  • The hub refuses to start when a canonical capability has no configured stake floor.
  • The LLM credential gate reports ready only once a real token is present.
  • hub_url stays on the pushed checkpoint block instead of being dropped with self_sync.
  • The derive block is carried through the anchor reward activation copy.
  • The attestation forward-margin override is read by its documented name.
  • Closing the hub stops its attestation and consensus engines and unsubscribes them.
  • The snapshot routes and the WS stream serialize BIGINT the same way.
  • mariadb moved off the cleartext-credential advisory range with the floor pinned in the dependency gate.

…table

Two additions, both inert until the height is armed.

attest_response_mirror_activation.js is the value-identical twin of the
indexer's copy and of the canonical map in the documentation repo. The
null sentinel needs its explicit test: `req >= null` coerces to `req >= 0`
and would arm the mirror on every block of a network nobody ratified.

attestation_responses.sql is the table a finalized response is written to
instead of being broadcast. It is insert-only and hub-authored, and every
hub that holds the artifact writes its own row, so the id is hub-local and
the consumer strips it on apply. The network column is what lets a mirror
scope its cursor and purge rows a previous hub served. The mirror is
transport, never authority: the consumer re-verifies the signatures
against the responsible set it resolves from its own request row.
… above the height

Three pieces of the response mirror, all inert until a height is armed.

attest_response_canonical.js is the twin of the indexer's copy and holds the
one thing both sides must agree on byte for byte. The legacy canonical is
five fields concatenated with no separator, which was safe only because the
free-form meta field was last and nothing followed it. Appending the
effective time is exactly what that accident was protecting: concatenated
bare, meta "X" with 1234 and meta "X1" with 234 produce identical bytes, so
one honest quorum's signature would validate against two different effective
times and the producer would get to pick which. A separator plus a canonical
integer spelling requirement makes the encoding injective again.

The publisher now declines a request whose response rides the mirror. The
gate is a per-request early return rather than a conditional listener,
because the listener is process-global; returning early skips the queue
append, the broadcast, the spend reservation and the durable intent at once.
The on-disk sweep is independent, so entries queued before a flag day still
drain.

The schema version and the shared-gate registry move with them. The registry
row changes the rules digest that rides the heartbeat, so upgraded hubs will
report a mismatch against un-upgraded peers for the length of a deploy wave.
A snapshot route for the bootstrap and a max_ids entry for the live stream.

The route lists its columns explicitly rather than selecting everything,
because a mirror bootstrapped over REST and one streamed over the websocket
must deliver the same column set: a wildcard drifts the moment the hub gains
a column the broadcaster does not send.

The ready frame's max_ids entry is what lets a consumer run its gap
catch-up for the table at all. Leaving it out fails silently, with no error
and no log line, so it is asserted against a real frame rather than a mock.
The count is unfiltered: unlike the cross-chain tables, nothing here is ever
retracted, so a filtered maximum would understate.
Above the activation height the responsible set signs the time its answer
becomes effective, so the block every indexer fires the callback at is a
function of signed data rather than of whichever hub happened to write the
row. Below the height the canonical is unchanged, byte for byte, and a
captured literal from before this change pins that.

Two things the design needed that were not obvious until it was built.

The time has to ride the proposal, not only the prepare, because every
responsible hub establishes its own winner independently. If each kept its
own stamp, every hub would sign a canonical no peer could rebuild and an
honest round that agrees on the body would still run to timeout. Each hub
therefore settles on the elected leader's stamp, which it already holds.
With the leader's proposal missing they fall back to their own and the
round stalls and retries, which is the liveness profile a missing leader
already has, and a stall rather than a divergence.

The candidate key had to gain the stamp for the same reason: two
corroborators that stamped different times signed different canonicals, and
merging them would have carried a non-verifying signature into the winner's
set and inflated the count.

The follower checks the spelling before the bounds, deliberately. A time
spelled with a leading zero is numerically in range, so a bounds-first
order would admit it, collect an honest quorum, and produce a row whose
canonical nobody can rebuild.

The margin is 120 seconds, not the relay's 2400: a relayed row has to land
on another chain before its effective time, and nothing here lands
anywhere, so this only has to beat gossip, stream lag and clock skew. It is
overridable on regtest alone, because regtest blocks are stamped at about
now and the acceptance tests would otherwise be undrivable.
It was read by indexing process.env through a constant that holds that
exact string, which bought nothing and hid the variable from the
environment-variable coverage gate. That gate scans for literal reads, so
the knob was operator-settable, documented nowhere, and visible to the gate
only as the anonymous computed-read count going up.

Spelled out, it is scannable and now has a row in the hub's configuration
table. The constant stays for the config-table lookup and the messages,
where it is a real deduplication.
…e slot

Leader election is a function of the BTC height, so a tip that stops
advancing pins the slot to one constant and every other hub returns from
the not-my-slot branch forever with no stall recorded. Consecutive
not-my-slot ticks at the same height now report as a cadence stall.
…ironment

Regtest ships inert and arms from XC_ROLLCALL_REGTEST_ACTIVATION, so a
single-coin venue with no DOGE peer is not wedged at its first epoch close.
Mainnet and testnet stay literal and are unreachable from the environment.
…he body before anyone signs it

The request:finalized listener materializes a mirror-era response into
attestation_responses, selects it back for the autoincrement the consumer
pages on, and broadcasts the stored row. A legacy-era round (null effective
time) is refused outright rather than coerced to the epoch.

The 8189-byte body cap is now enforced where a signature can be produced:
the leader before proposing, and followers on both PROPOSE and PREPARE,
measured on the decoded body rather than the assembled wire.
… before writing

A hub that newly writes a mirror row broadcasts it once as an ATTEST_RESULT
envelope. A receiving hub resolves the request from its own indexer,
checks the responsible-set signatures against its own capability snapshot
at the buried request height, and only then inserts and streams the row.
An unknown request parks for one cycle and is retried once; duplicates
are no-ops and are never re-gossiped, so five hubs converge without a
storm.
The mirror table gains a nullable batch_action_index column, the one field
set after insert: when the ATTEST v5 batch carrying a row lands on DOGE the
hub records that batch action on the row and re-broadcasts it, and the
explorer's batch link reads it downstream. The mirrored column list, the
snapshot route and the insert carry it; the gossip wire deliberately does
not, because the value is set hours after finalization and reaches every
hub through the chain-to-hub push rather than through a peer's claim.
…d accept landed batches back

An hourly window of mirrored responses is signed by the attestation set
over the batch canonical, encoded with the shared wire codec (vendored
byte-identical from the indexer) and published on the Dogecoin rail as a
head plus continuations, with its own buffer, dead-letter path and durable
markers so a restart never pays twice and an empty window still lands a
zero-row head that proves coverage. A hash-order election over the batch
key picks one publisher per window with rank-staggered fallback.

The receive method accepts a batch the Dogecoin indexer parsed: it re-verifies
the batch quorum, inserts any rows this hub lacks, records the landing
batch action on each row once, and re-broadcasts so every mirror carries the
link. The method is documented in the RPC spec and listed for the feed.
…y a window defers

Window membership read the hub's own wall clock, which the schema declares
may differ between hubs, so two hubs could partition a boundary row
differently and cost the window its quorum. A skew band papered over that.
The effective time is inside the signed canonical, so every hub holding a
row agrees on it byte for byte, and the band is gone with it: a disagreement
about membership is now always a real one. The signed field also gives the
window its own completeness deadline, because a row is written a full
forward margin before the window holding it can close.

The publisher needs a Bitcoin chain tip for its anchor and deferred every
window in silence without one. It now names the cause once per distinct
failure, points at the tip push that fixes it, and says coverage is missing
meanwhile. The batch rail's configuration is documented, and the doc
coverage gate now scans the attestation prefixes so the next knob cannot
arrive undocumented.
Closing a hub stopped none of the five attestation engines it started, so a
hub closed and reopened inside one process kept every previous lifetime
listening and one finalized response fanned out to all of them, each copy
racing for the same spend reservation. Close now stops them all.

That alone was not enough: the publisher subscribed with an anonymous
closure it kept no reference to, so its own subscription was structurally
unremovable and the listener count never returned to zero. It now stores the
handler and detaches it on stop, the way the spot checker already did.

A new guard asserts the two eras partition every finalized request: exactly
one of the publisher and the mirror serves each one, so a request served by
neither can no longer lose its response in silence.
…now use

The snapshot routes stopped handing the payload to res.json when they took over
serializing a BIGINT column themselves, and ship the finished string through
res.type().send() instead. This suite's response double implements only status()
and json(), so type() was undefined, the success path threw, and the route's own
catch answered 500: the case asserting a valid since_id is accepted read that as
the route rejecting its input.

The double now carries type() and send() as well, so it covers the same slice of
the express response the routes actually call.

The case had been masked rather than passing. The security tier runs behind the
unit tier in one && chain, so the unit failures that came with the serialization
change stopped it from running at all.
RollcallRound re-emits a stored signature after a restart without
re-signing, and the loader took any line for the epoch regardless of
which identity wrote it. A log carrying another hub's line therefore
made the restarted hub broadcast a signature no peer could verify under
its pubkey, record nothing of its own, and read as absent while it
believed it had signed. Both loaders now keep only records naming this
hub's pubkey; spend records gain the field, and a record predating it
is treated as this hub's own.
…poch

A hub broadcasts its signature once, when it signs, and never again, and
_onSign dropped any signature for an epoch whose round was not open yet.
The hub that signed last therefore lost every earlier signer for good:
elected leader, it published a partial set for the whole accept window
and left the rest to sweepers and self-publishes. Early gossip is now
held per epoch, unverified, and judged by the ordinary rule when the
round opens; the holding area covers a few epochs ahead and drops
anything at or behind an open round.
… cadence config

One wave of the review round on the xchain-platform board.
Every change was re-derived from the code rather than applied from the
finding recommended option, and each carries a control that reproduces
the original failure.

Review findings: 6425 6426 6427 6428 6438 6440 6465 6466 6489 6490 6491 6515
…header

The review round corrected this shared module's header in the indexer copy
(#6494): it claimed the indexer flips the signed shape on this anchor, when the
indexer has no call site at all. The file is vendored in five repos under a
byte-identity gate that permits only the "live in xchain-{...}" line to differ,
so the correction has to land in all five or the gate reds.

Mechanical sync from the indexer copy, each repo keeping its own live-in line.
…letters

EncoderClient's default 30s axios timeout had zero margin over the encoder's own
upstream RPC budget, whose uncached worst case is three 30s hops, so a healthy
broadcast could time out client-side and be recorded as an ambiguous send. The
budget is now derived rather than guessed; the recommended 90000 was rejected
because it merely equals that worst case rather than exceeding it.

Also: SlashDetector silently discarded an explicit SLASH_DEVIATION_THRESHOLD=0
override (0 is falsy, so the coercion chain fell back to the 0.05 default), and
the judge_equivalence_threshold governance knob was defined but consumed by
nothing.

One rationale comment in SlashDetector.js is corrected rather than added: a NaN
deviation band does NOT disable slashing as previously written. Executed,
exceedsBand returns true for everything, so a NaN band would slash the entire
honest federation, which is why the finiteness guard belongs there.

Review round 7 findings #6860, #6862, #6871, #6881, #6852. Also carries review
round 6's hub work.
isResponseMirrorActive returned false for two conditions that mean different
things. A null threshold is the deliberate unratified sentinel: the operator has
not armed that network and the legacy on-chain path is correct. An absent entry
is a misconfiguration, and it disables the mirror totally and silently, so every
request is answered on the legacy path and every delivered mirror row is
declined with no log line anywhere. Downstream that is indistinguishable from a
mirror that never delivered at all.

It is reachable because the two sides of the applier's join take the network
string from different places, and nothing in the code forces them to agree, so
whichever side is wrong the only symptom is silence.

Only the absent-entry case warns, once per distinct string, because this sits on
a per-request per-block path. Return values are untouched, so the era decision
and every signature over it are byte-identical.
…s capability snapshot mirrored at its anchor

Two defects the regtest ladder's batch drill found, neither reachable
before a window carrying a real response reached the co-sign step.

- Mirror row: AttestationConsensus hands the finalize list over in the
  order signatures ARRIVED (a Map walked in insertion order), which differs
  hub to hub for one quorum, and the batch co-sign compares signer_pubkeys
  and signatures byte for byte across hubs. Every window with a real
  response was refused `differs on signer_pubkeys` while all four
  responsible hubs held the same three signers. The row now sorts the
  pairs by pubkey; the pairing is kept, and the indexer verifies the pairs
  as a set, so nothing downstream reads the order.
- Batch anchor: an off-BTC verifier judges a v5 head against
  capability_snapshots at exactly the batch anchor, mirrored from the hub
  it follows, and nobody wrote those rows: the publisher resolved the set
  from the BTC indexer and kept it in memory, and the relay's writer had no
  caller. Every v5 head on DOGE read `invalid: insufficient signer stake`.
  The publisher now persists the attestation set at its anchor on the
  leader after the election resolves and on every follower after its own
  capability check, the PRICE batch's contract: INSERT IGNORE on the
  natural key, truncated sets never mirrored, rows broadcast, once per
  anchor per process.

Driven: a window of two responses co-signed 3 of 5, broadcast as a v5
head plus a v6 continuation, and judged valid on an indexer that mirrors
the federation.
Review-round fixes, scoped by pathspec: a concurrent lane's oracle and consensus
work is live in this same tree and is deliberately NOT in this commit.

Money-path effectors: a failed send consumes no budget while a POSSIBLY-PAID one
does, so the attestation publisher and the idempotent broadcast helper now
commit on an ambiguous send instead of releasing, matching the four exemplars in
this repo that already did. Roll-call reserves one guard token per CHUNK rather
than once per batch, since the pair list splits at 41 and each chunk is its own
transaction and its own fee, and it marks delivery per chunk so a failing second
chunk cannot re-broadcast the first on every tick.

Checkpoints: a same-sequence fence refuses a conflicting write instead of
letting an INSERT IGNORE drop it, which also stops a phantom resync that dumped
every mirror subscriber and emitted a finalized event for a row the hub does not
hold. The Byzantine half is left open: proving its liveness needs a live regtest
venue.

The LLM path classifies on HTTP status rather than JSON body shape, so a vendor
failure stops being booked as a decided judgement, and only the resolved
credential reaches the spawned child.

Suite: 5370 passing, 0 failing.
The tests for this landed without the implementation, so the suite has been red
since: three cases in test/unit/OraclePublisher.test.js assert that a pre-send
failure requeues, and nothing tagged pre-send failures.

_defaultBroadcast now splits into _buildSignedTx (fetch, build, sign) and the
broadcast call. Every failure in the build-and-sign stage is tagged
oraclePreSend, because that stage definitively cannot have sent anything, and
the dead-letter branch excludes it. Only the broadcast call keeps ambiguity
classification, where a timeout or mid-flight reset really can mean the DOGE
node accepted the transaction and a blind retry would spend a second fee and
double-anchor the round.

Without the tag the shared classifier decides: it answers "ambiguous" for any
error it does not recognise, which is right for a broadcaster it knows nothing
about and wrong for a stage it knows cannot send. A get_utxos timeout was
therefore dead-lettered, permanently removing a never-broadcast round from
automatic retry. Same convention as AttestationRelay's _relayPreSend.

Verified: test/unit/OraclePublisher.test.js 102 passing, 0 failing. The control
is the gate's own run of the committed tree, where exactly these three failed.
Nothing bound a hub to the body it had already signed at a given sequence, so a
second request for the same (chain, network, checkpoint_seq) carrying a different
canonical was signed as readily as the first. Two conflicting signatures over one
sequence are equivocation, and the co-signers cannot tell which body the hub meant.

The engine now remembers the canonical it signed per sequence and refuses a second
signature over a different one, while a repeat of the SAME canonical still returns
its signature so an ordinary retry is unaffected.
…-signed

The back-fill trusted the member ids it was handed rather than the ones in the
archive it had actually decompressed and co-signed, so a request could name
members the signed body never contained and have them written as finalized.

The sign path now records the member ids of the archive it verified (matches,
calls, rewards), and the back-fill writes only those. A member outside the
co-signed body is refused instead of trusted.
…success

The durable at-most-once marker keyed only the request, so a first attempt that
failed for a retryable reason claimed the key and the successful response that
followed was discarded as a duplicate.

Rather than widening the primary key, which would auto-apply a key change on
already-deployed hubs, the row gains two nullable columns recording which
outcomes have been sent. Identity moves INSIDE the row: the marker still admits
one send per outcome, a retryable failure no longer consumes the slot a success
needs, and existing rows read correctly with both columns null.
…pts writes

_loadState's header states that all of its rules are fail-closed, and the
corrupt-file rule beside it seeds the window CONSUMED because a broken store must
never read as a green light. The absent-file rule did not follow it: it returned
an empty window, which is right for a genuine first run and wrong for a store
that cannot be written at all.

Those two raise the same ENOENT. On a read-only disk _persist() lands no byte, so
the file never appears, every restart reads ENOENT and takes a full fresh
allowance, and the per-window ceiling is unbounded across restarts.

An absent file now seeds CONSUMED unless the store actually accepts writes. The
probe is permission-only, creating and writing nothing, so the write path keeps
its single call site; it walks to the nearest existing ancestor because _persist()
mkdirs the tree it needs, so a missing directory under a writable parent is still
a first run.

This is the half that is decidable without a policy call. Whether reserve() itself
must refuse when _persist() fails is a separate liveness trade-off, since failing
closed there can halt every hub effector on a read-only disk, and it is not
changed here.

Control: the new test is 2 passing / 1 failing against the previous guard and
3 passing / 0 failing with this one, and the two cases that must NOT change (a
genuine first run, and an absent directory under a writable parent) pass both
ways. All spend-guard suites together: 51 passing, 0 failing.
…cannot pin the socket

_post cleared its abort timer when the response HEADERS arrived and never released
res.body, so a collector that answers 200 and then stalls held the socket open with
nothing bounding it. Measured against a stalling collector, the socket was still
open at three seconds and the batch was counted as shipped.

That is worse than a leak, because the timeout was configured and did nothing: at a
400ms ship timeout the timer cleared at 22ms, so the stall outlived its only bound
and the shipper reported success.

The body is now cancelled inside the same then, within the abort timer's window,
rather than after finally has cleared it. The stream is cancelled and never read,
and a cancel on an already-errored body is swallowed.

Every existing test injected a transport and bypassed the real fetch path, which is
why this survived. The new case drives _post itself.

Canonical copy. The vendored consumers are regenerated from it by the sync
script and are never hand-edited.
…ss as fresh

_resolveBtcLatestBlock fell back to a direct getlatestblock call when no tip push
was available, and took whatever height came back. The pushed-tip path is
age-gated; the direct path was not, so an indexer stuck at a stale height served
that height and the hub treated it as current.

The direct path is now dated against the newest block_time the hub holds, and a
height it dates as frozen is refused rather than served. A rejected pushed tip is
kept for exactly that purpose: it is still the only block_time available, so it
remains useful as a reference even when it is too old to serve.

Both paths failing returns null, unchanged.
…cepts

attestation_responses.response_payload and meta hold provider bytes, and the
on-chain twins they stand in for are utf8mb4. These sat on the table's utf8mb3
tail, so a 4-byte character failed the mirror INSERT with errno 1366 under
STRICT_TRANS_TABLES: a body the ATTEST v1 path would have carried never reached
any indexer, and the request it answered expired unresolved.

Both columns now declare utf8mb4. The definition edit alone reaches only fresh
installs, because the drift reconciler adds a missing column and never restates
an existing one, so a charset migration runs for deployments that already have
the table.
…ready emits

PRICE v0 ingest accepted unbounded-precision price strings that the v1 lane and
every v0 producer reject, so a value no producer emits could enter through the
older path and reach consumers that assume the bounded form.

Ingest now applies the same scale on both lanes.

Gated, because refusing a value the chain currently accepts moves consensus.
Mainnet ships UNARMED on the house sentinel and naming the activation instant is
a separate operator act; testnet and regtest run from genesis, so both sides of
the gate are exercised and historical replay stays byte-identical.

The activation is duplicated across the two services and the copies are
byte-identical, so a partial landing cannot arm one side alone.
…oduces

One request can finalize more than once. Every responsible hub runs its own
agreement, the leader slot follows the chain tip that hub polled, and two hubs
polling different tips finalize under different leaders whose signed stamps
differ. Both rows carry an honest quorum over their own canonical, yet the
mirror keyed rows on the request alone, so the second variant was a silent
duplicate on some hubs and the first on others, and no window carrying such a
request could ever reach batch quorum.

Row identity is now the request plus its signed effective_time. The already-
held check, the batch-link stamp, the re-broadcast read and the select-back are
keyed on that triple, the startup key widen rebuilds the unique index on an
existing database, and the batch co-sign compares windows on the same identity
with the window read ordered by the stamp last so the signed bytes agree on
every hub. The indexer binds the smaller stamp, which it already did.
A round with no deterministic price capability snapshot is now skipped and
recorded as skipped, rather than sized from this hub's live validator set, which
peers do not share and cannot reproduce. Sizing quorum from a local set is a
consensus input that differs per hub, so the round it produced was unverifiable
by design.

Carried with it:

- The validator normalization both consensus paths perform moves into one helper
  and preserves `truncated`, so a truncated snapshot stays marked as truncated
  through the carry instead of reading as a complete set.
- The attestation finalizer re-checks its own pending entry before acting, so a
  second finalize for the same request cannot act on a superseded entry.
- The price sources become a declarative table carrying each source's key
  requirement and pair coverage, in place of the per-source branching.
- A fee config row that is inert against a pinned value warns once per
  coin/network/param instead of on every read, and the config lookup it needs is
  a single per-module query.
- The stake gauges reset before they are set, so a removed series does not
  linger at its last value.

Tests: new deterministic-snapshot gate and snapshot-truncation-carry suites, plus
boundary coverage for the fee quote and the trimmed median, a constants
conformance suite, and hub metrics coverage.
@jdogresorg jdogresorg changed the title Release v0.15.0 Release v0.15.0 - xchain-hub Sep 7, 2026
Operator ruling 2026-09-07: the point of this train is to exercise the response
mirror and roll call on testnet, and a train that ships them dark there is not
worth cutting. The mirror is armed at block 151324, the chain tip when the
ruling was made, so it is active the moment a node updates rather than waiting
on a future height. Roll call needed no change: it was already armed at 151200,
which the chain passed some time ago.

Mainnet stays unratified for both, so its behaviour is byte for byte unchanged.
On testnet this changes state derived from existing bytes, so the changelogs now
carry an Activation section saying so, and every hub and the indexers following
it must update together rather than one at a time.

The activation map is mirrored in five places and all five move together: both
service copies, the documented canonical, the vendored copy the test helper
reads, and the assertion that used testnet as its example of an unratified
network, which it no longer is.
…network

Two suites used testnet as their example of a network with no ratified height,
which stopped being true when this train armed it. Both drove a request far
above any plausible height and asserted the legacy path still ran, so arming
testnet turned them red for the right reason.

Mainnet is the unratified example in both files already, so the testnet cases
are repointed at what they can still prove: below the activation height the
legacy path owns the response. That keeps the coercion guard the originals
existed for, since a null read as zero would arm every block.
The shared workflow pins every sibling at develop for any pull request, and this
train's activation arming lives only on the release branches, so
consensus_rules_digest compared an armed activation map here against an indexer
still carrying the unarmed one and reported a gate disagreement that exists
nowhere outside CI. The venue gate never saw it because it ships each sibling at
its release branch already.

The coverage ratchet re-runs the same unit suite, so it needs the same
expression or it measures a suite whose cross-repo guards resolve differently
from the ones the gate ran. xchain-indexer already passes both; this is the hub
adopting them.
@jdogresorg
jdogresorg merged commit 053cc98 into master Sep 7, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant