Release v0.20.1 - #16
Merged
Merged
Conversation
…nts above the code they describe
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.
…ore reading its row
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
…token where the MINT is the subject
This was referenced Sep 23, 2026
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release v0.20.1
Coordinated platform release across the 13-repo v0.20.1 train.
masterrelease/v0.20.1What changes
admit_blocksmap builds the legacy canonical without throwing.Sibling release PRs
Filled in by open-release-prs.sh --execute once every PR above has a number: