Skip to content

Release v0.20.1 - #16

Merged
jdogresorg merged 65 commits into
masterfrom
release/v0.20.1
Sep 24, 2026
Merged

jdogresorg merged 65 commits into
masterfrom
release/v0.20.1

Conversation

@jdogresorg

@jdogresorg jdogresorg commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Release v0.20.1

Coordinated platform release across the 13-repo v0.20.1 train.

  • Base: master
  • Head: release/v0.20.1

What changes

  • Expands BF5 and BF6 activation-family coverage for inert future-block stalls, hub-mirror readiness, mixed-hub provisioning, and live verdict reporting.
  • Proves that an absent admit_blocks map builds the legacy canonical without throwing.
  • Hardens attestation-mirror setup with fresh stacks, per-slot ports, listener and schema readiness, retry discipline, and pending-row census guards.
  • Exercises the DOGE bridge rail and miner-pause path, replacing timing sleeps with observable condition waits across activation and Byzantine scenarios.

Sibling release PRs

Filled in by open-release-prs.sh --execute once every PR above has a number:

Extract module-scope registration helpers while preserving titles, fixtures, assertions, comments, and registration order. Keep every helper under the structure limit and reduce the function-line finding count by two.
Extract synchronous registration helpers for hub connector behaviors while preserving test order, hooks, comments, and async bodies. Reduce the measured oversized-function count by two with no other structure-count changes.
Run each attest-mirror leg through a capped fresh-stack lifecycle with explicit readiness and teardown. Pin the armed ZC5 boundary and allow archived source trees to carry verified revision manifests.
Wait for the complete indexer schema before a leg begins and seed an adoptable attestation roster on every fresh stack. Keep the readiness and teardown lifecycle covered by the aggregate runner tests.
Gate fresh attest-mirror stacks on the concrete index tables used by initial checks. This prevents the runner from waiting on table names that are not part of the indexer schema.
Create a detached pre-upgrade hub checkout for the BF6 mixed-version cases and pass it through the isolated-stack driver. Remove the checkout during teardown and refuse archived worktree pointers as clone sources.
…s_5 twin across the fleet

The parity tier read SHARED registry part 2 and never part 5, so reverting a
TRAIN_ACTIVATION height in a consumer left it at 45 passing while the same
experiment on part 2's CHECKPOINT_COMMITMENT row went red. Add the missing
coverage in the shape part 2 already uses: value equality against the canonical
protocol constant, rule-set and network key shape, halt-verdict agreement driven
through both train gates over every registry's copy, and a whole-file byte grade
of shared_rows_5.js against the indexer canonical so every row in the part is
guarded rather than one row per cut.
… counted depth

The BF6 preparer case pointed at the hub's node_modules with a fixed number of
".." steps above the sibling base. That arithmetic only lands on a real path
when the checkout sits exactly three levels under the platform root, which is
true of the lane worktree it was written in and of nothing else: the platform
checkout resolved it outside the tree entirely, and the CI venue resolved it to
a directory that does not exist, so the case asserted its way to a red gate on
the commit that added it.

Take the path from BUILD_HUB, which the same case already uses as the clone
source and which the preparer itself uses as its default. Every layout keeps
the repo and the hub side by side, so the sibling row holds where the depth
does not.
…ta volume cannot deny db-init

setup-stack.sh mints a fresh random root password into the stack .env on every
boot, but MariaDB honours MARIADB_ROOT_PASSWORD only while the datadir is empty.
A db-data volume that outlives its stack therefore leaves the datadir on the old
password, db-init's single client call is refused with ERROR 1045, and the whole
up phase dies one second after db reports healthy.

A killed lane never reaches the down phase, and down -v skips any volume another
container still holds, so up now owns the cleanup: down -v --remove-orphans, a
label-scoped container and volume sweep that retries while holders drain, and a
hard refusal to boot if any volume survives.
… schema

start() returned as soon as the last indexer created its database schema, which
proves it reached MariaDB and not that its /status socket is bound. Indexers are
spawned serially, so the last one is exposed every time and a leg reading it in a
before all hook died on ECONNREFUSED at that one port rather than on a named
barrier. Any HTTP answer counts, 503 included: a stalled indexer that reports its
stall is listening, and a parked node is what these drills are about.
…acy row is a row a hub really wrote

BF2 and BF4 armed their venues with the genesis form of the regtest lever, which
resolves to height 0. On that venue every block is admission era, so their
"legacy" NULL-admission seeds were modern rows missing the map the producer
activation makes mandatory, and the canonical builder refused them and stalled
the drill block. The refusal is the product working, on both the binding side
and the canonical side: the builder rejects a NULL admission map on any row
whose era block is at or above the producer activation (adjudicated
2026-09-18). The seeding was the defect.

Both legs now arm producers and consumers at the chain's own tip + 1, so every
block the stack already has is legacy era and every block the venue mines is
admission era, and they seed their legacy rows below that height. The crossing
is one the venue walks through rather than one the seeds manufacture.

- barrierFamilyFixture: armValue, crossingArmHeight and legacyEraBlock, with the
  arming overlay carrying a height; the evidence records it.
- barrierFamilyDrive: bootFamilyVenue takes armAtCrossing, reads the tip before
  the venue starts, and arms the hubs at the same height as the indexers.
- barrierFamilyRows: armedLegacyApplyHazards is keyed on the row's era rather
  than on its table alone, and an unreadable era block fails closed.
  CANONICAL_BEFORE_QUORUM_TABLES survives as that guard's scope; it is no longer
  a seeding rule, so admissionSeedRows now seeds a legacy row in every member
  table instead of skipping bridge_transfers and policy_snapshots.
- The unit tier pins the new line against the indexer's own builder in a child
  process armed the way a venue arms one.
…base

Concurrent legs probed one port base and were handed the same ports. Only the
barrier-family drive read the base the stack runner exports per slot; every
other leg built the venue directly, fell through to the historical 41000, and
three concurrent legs then raced between probe and bind. Two of them lost their
whole before-all hook to listen EADDRINUSE on 127.0.0.1:61017 and :61030 and
reported 0 passing, which reads as broken legs rather than as an allocation
that was never slot-aware.

The venue now resolves its own base: an explicit basePort first, else the
runner's per-slot base from the environment, else 41000. One resolver instead
of sixteen call sites, so a leg added later is slot-isolated without being
told to be, and a caller that places its own venues by hand keeps its base.
A base that is set but unusable throws rather than falling back, because the
fallback is the shared window this leaves.
…atus

statusOf was one unretried axios.get, so a single transport failure threw out of the
venue and killed the leg that was reading. Across two aggregate drives it threw in four
legs: at0, at0b and venue.smoke on 2026-09-17 (ECONNREFUSED at bring-up, since cured by
waiting for the listener) and ab1 on 2026-09-18 (socket hang up, mid leg, on an indexer
that had already answered many times). Waiting for the listener could not cure the second
shape because the socket drops long after boot.

The retry keys on error.response being absent. validateStatus accepts every status on
purpose, since a stalled indexer reporting 503 is an answer and is usually the state the
drill is asserting, so any status still returns on the first attempt, unretried. Only the
absence of an answer is repeated, three times over two seconds, and an indexer that is
genuinely gone still fails the leg with a message naming the attempts and the last cause.

FALSIFIED 2026-09-18 against the real class, offline, 8 of 8 claims: a 503 and a 404 each
return on the FIRST attempt with the body intact and exactly one request served, which is
the claim that matters since a barrier drill asserts on 503; a socket hung up once is
retried and the second answer returned; a port with nothing behind it still fails, in
3009 ms, naming "in 3 attempts" and ECONNREFUSED and carrying the original on .cause; and
an indexer index the venue does not have throws rather than returning undefined.
…le 5

AT0b asserted the hub serves schema_version 5 as its proof that the hub still holds the
row it withheld from the starved indexer. The platform has since rolled the schema to 7,
so the leg red on 7 !== 5 at a clause that has nothing to do with barrier attribution,
and it did so on the 2026-09-18 aggregate where it was one of the seven legs holding
clause 1 back.

venue.smoke already had this right: it reads HUB_SCHEMA_VERSION from the indexer's own
hub_schema_version.js and stayed green straight through the same bump. That resolution
moves into test/attestMirror/helpers/hubSchemaVersion.js so there is one copy rather than
a second literal waiting to go stale, and both legs now read it.

The assertion is unchanged in kind. It is still an equality against a number sourced
independently of the response, so a hub serving the wrong version still reds the leg; it
just is not a number typed into the test. On empty input, an indexer sibling that is not
there, the helper throws MODULE_NOT_FOUND naming the path it wanted rather than yielding
undefined, which would have compared Number(7) against NaN and failed for a third
unrelated reason.

Falsified offline: the helper resolves 7 from the indexer sibling, which is exactly what
the hub was serving when the leg red at 5.
…ore reading the row

ZC1 read its v0 request row straight after broadcasting, so the read raced the venue
indexer's commit of the block carrying the request and the leg red on "the venue indexer
holds no v0 request row", which says nothing about zero-conf serving and everything about
when the read fired. It was one of the seven legs holding clause 1 back on the 2026-09-18
aggregate.

waitForLocalRequestAtBlock waits for the venue indexer to commit the block the standing
indexer placed the request at, then reads the row, then asserts the row really is at that
block. The last part is what keeps this a fix and not a sleep: a row that turns up at a
different height now fails loudly instead of being accepted because the wait happened to
be long enough.

It refuses to wait at all without a usable height from the standing indexer, so an absent
or unparseable blockIndex fails naming that rather than waiting on Number(undefined) and
timing out with an unrelated message.

The hunk is the ZC1 third of the held branch pb/at1-legs-rebase-2026-09-18 (8a96d5f1),
taken alone. The AT2b and ZC3 thirds of that branch are deliberately NOT taken: both legs
read green on the landed tip with their files untouched on the 2026-09-18 drive, so
landing them would rewrite passing legs from versions never driven green.

Falsified offline via the leg's own ZC1_UNIT_ONLY mode, 3 passing / 0 failing: the row
read is proven to run after the commit and not before, a row at a different block is
refused, and a missing request height is refused.
… base

resolveVenueBasePort falls back to 41000 when AB_VENUE_BASE_PORT is unset, which is right
for someone running one leg by hand and wrong for a leg the runner launched as one of
several concurrent slots. If that export ever goes missing, every slot silently shares one
probe window, _pickFreePorts bounces it out of the ephemeral range into 61000, and the run
comes back as intermittently flaky barrier legs rather than as a missing export. That is
the shape the 2026-09-18 concurrency-3 attempt failed in, at :61017 and :61030.

The slot is the evidence that a base was owed. run-attest-mirror.js puts ATTEST_MIRROR_SLOT
in the driver's environment, the stack driver derives the base from it, run-leg.sh exports
the base, and nothing in that chain scrubs the environment, so both reach the mocha process.
Verified on the rail host by replaying the exec chain: slot 2 with base 63600 and slot 0
with base 63400 both arrive in the child. A slot present with no base is therefore a broken
runner, and it now throws instead of defaulting.

SLOT 0 IS A REAL SLOT, and it is the first one allocated. The check is presence rather than
truthiness for exactly that reason, since the first slot arrives as the string "0" and a
truthiness test would wave through the one case that collides with every other slot.

Nothing else changes. An explicit basePort still wins before the check is reached, so the
bridge rail's hand-placed pair is untouched, and a leg run by hand with neither variable set
still gets 41000.

Falsified both directions: with the guard the venue unit suite reads 87 passing / 0 failing
/ 0 pending; with the guard removed and the same tests run, 86 passing / 1 failing, failing
first on "defaulted slot 0", which is the truthiness trap the check was written against.
…sertions

BF5's two failing assertions printed the /status JSON and nothing else, and /status names
the last barrier the block DEFERRED on, which is not the same thing as the reason it is
still uncommitted. A canonical-build refusal or a parse rollback loop never reaches
/status at all, so the leg reported match_sync_barrier for twenty minutes after that
barrier had cleared and two aggregate drives failed to attribute it from its own output.

Both assertions now carry venue.logTail, the idiom the armed-node assertion beside them
and at0b already use. This changes what a failure PRINTS and nothing about what it
concludes, so it cannot move a verdict: the counts on a green BF5 are unchanged and a red
BF5 now carries the evidence that explains it.

The preserved venue logs from the 2026-09-18 drive show what these assertions were
hiding: the match-sync barrier cleared on schedule and forty-six seconds later block 278
began throwing in the indexer's mirror admission gate, "legacy-era row at block 278 on
regtest was handed admit_blocks {BTC:280}; refusing to build an admission-era canonical",
repeating 534 times on a five-second rollback and resume loop until the leg was torn down.
None of that reached the failure message.
Verifies local sibling repo paths resolve correctly before a rail
drive, catching dangling symlinks, absolute Users targets and missing
spawned sibling APIs early.
ping() and getFileRaw() called axios with no timeout, so an explorer that
accepts the socket and never answers (the "503 with zero DB pools" venue
with no pool left to answer from) left ping() pending forever. initialCheck
requires that ping before any action test runs, and the suite runs under
mocha --timeout 0, so nothing else in this stack ever timed the call out:
a stuck explorer silently stalled CI instead of failing it. Same fix, same
reasoning already applied to RegtestMinerConnector's PING_TIMEOUT_MS.
…uard

Adds check-mirror-pendings.js to census skipped/pending attest-mirror
tests with a stated reason, and check-request-row-wait.js to detect
request-row reads not guarded by a height wait. Extends the
attest-mirror aggregate runner with concurrency-capped leg allocation,
readiness-gate teardown and abort-on-consecutive-ready-failure
handling, with matching harness coverage in the BF2/BF4 barrier family
fixtures.
withMiningPaused() read BRIDGE_RAIL_MINER_PAUSE_FILE and nothing else, so
asking it to pause DOGE silently did not pause DOGE: the external DOGE mine
loop kept moving the tip while the drive believed it was held. Row 8's AT8
cap split (25 then 5) is only meaningful if that loop honours its own flag
file, so the absent handling read as success.

Honour BRIDGE_RAIL_DOGE_MINER_PAUSE_FILE alongside the BTC variable, writing
and removing each configured file for the span. The flag writes move inside
the try so a failed write now resumes mining instead of stranding the miner
paused; the BTC file naming, contents and lifetime are otherwise unchanged.

New unit suite covers DOGE pausing via its own variable, BTC still pausing
via its own, the BTC path unmoved, neither variable set, an unwritable DOGE
target, a throwing body, and flag files removed underneath it.

GATE-REGISTRATION: pending, package.json locked by the v0.20.1 bump lane.
Follow-on for whoever unlocks it: insert
'test/unit/helpers/bridgeRailMinerPause.test.js' as the first spec in the ci
script's mocha argument list, leaving every other entry in place.
…shallow

The CI venue's hub sibling is a depth-1 clone, so cloning from it never
carried 529ebe7a and the BF6 preparer's checkout failed with "unable to
read tree", holding develop red since 2026-09-19. When the pinned commit
is absent after the clone, fetch that one commit from the hub's declared
upstream.
…tine suites with condition waits

Lowers the sleep-flake baseline to match.
Replace the racy one-shot inert status read with a bounded status poll. Preserve the B-1 height assertion after observing the expected stall class.
…OLICY_MAX_MEMBERS, fix the phase2 mutation scope
Print parseable passing and failing counts for the live tier, preserve the Docker-only path when requested, and classify a pure before-all database privilege failure as a venue error. Mixed failures and unknown-database errors remain red commit failures.
The bare-SEND rejection test can only conclude "rejected" by letting
waitForSend exhaust its own give-up on every healthy run, not just on
failure. That give-up is bounded by db.js's adaptive-wait budget
(timeMax x (1 + WAIT_MAX_EXTENSIONS) = 60000 x 4 = 240000ms), sitting
after the P2SH 2-tx confirm and utxo-tracker prefix the same test pays.
The test's own mocha timeout was a second, separately maintained copy
of that budget (240000ms), set before _waitFor grew extension support
and never revisited after. With zero margin between the two, a run
where the indexer used all three extensions (nightly-e2e run
35829816064, bitcoin leg, test/actions/gated_file.test.js:204) hit
mocha's timer first and turned a correct rejection into a false
"Timeout of 240000ms exceeded" failure.

Disable the outer bound for this one negative check instead of
raising the number again, matching how every other waitFor-driven
test in test/actions already relies on _waitFor's own bounded give-up
rather than a duplicate mocha timeout.
…dge per address

Every funded address on the litecoin and dogecoin legs bridged its own gas from
BTC and waited out the destination relay margin (LTC 600 s, DOGE 240 s). On
nightly run 35829816064 the lock-to-credit leg averaged 518 s on litecoin and
404 s on dogecoin per address, and the suite funds a few hundred addresses, so
both legs finished 8 and 15 tests in the 360-minute job budget while bitcoin,
whose faucet is a one-block local MINT, finished 272.

ensureGasBalance off BTC now SENDs the gas tick from a per-chain reservoir that
is bridged once (500 000 XCHAIN, E2E_GAS_RESERVOIR_AMOUNT to override) and
refilled on a shortfall; initialCheck fills it where it used to bridge a
throwaway 1. bridgeGasIn mints above MAX_MINT in chunks and still locks the
whole amount in one XBRIDGE. Suites that prove the bridge call it directly.
Preserve the useful performance and timeout rationale without
publishing the specific internal run identifiers. Comment text only;
tests and runtime behavior unchanged.
…ent prices and feequote, and re-seed a displaced seed
…nd cannot rewrite them

hub_db_sync keeps the hub's wire id for price_snapshots and upserts ON DUPLICATE
KEY UPDATE without touching round_number or coin_pair. A fixture inserted with
AUTO_INCREMENT took MAX(id)+1, the id the hub's next round arrives with, so each
finalized round overwrote the seed rows' prices in place while they kept their
sentinel rounds. The indexer then priced fees off another pair's live price until
the next re-seed: MT3 DOGE shard 2 (run 35925455269, an ISSUE quoted at 17.5 DOGE)
and LTC shard 1 (a flat-fee DEPLOY that owed 7.8 LTC).

priceSnapshotHelper.seedSnapshot, native_fee_live and native_fee_dispenser now
delete the (round, pair) row and insert at GREATEST(MAX(id)+1, 1e12). As defence,
nativeFeeHelper treats its own seed round carrying a non-seed pair as displaced,
forces one re-seed when an over-flat quote is displaced or above the sender's
balance, and refuses such a quote with every price it saw.
Pinning by digest keeps the build on the exact V8/ICU build xchain-vm's consensus runtime check expects, since the floating tag can advance to a patch that fails it.
@jdogresorg
jdogresorg merged commit 2da7650 into master Sep 24, 2026
7 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