Skip to content

Release v0.20.0 - #47

Merged
jdogresorg merged 19 commits into
masterfrom
release/v0.20.0
Sep 19, 2026
Merged

jdogresorg merged 19 commits into
masterfrom
release/v0.20.0

Conversation

@jdogresorg

@jdogresorg jdogresorg commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Title: Release v0.20.0

Summary

Added

  • XC_MIRROR_ADMISSION_ACTIVATION passes through to the hub and regtest indexer containers.

Changed

  • Nightly Litecoin and Dogecoin jobs provision validator configuration, API access, database headroom, and a Bitcoin gas rail.

Fixed

  • Confirmation overrides pass through to the hub for nightly bridged-gas finalization.
  • Litecoin and Dogecoin indexers receive the Bitcoin indexer URL needed for bridge escrow proofs.
  • Bootstrap restores resolve the signing public key from its relocated configuration path.
  • Migration preflight accepts the v0.19.0 and later db/migration CLI layout.
  • Coverage checks derive sibling requirements from the repository roster.

Sibling release PRs

Merge order

Merge order: merge last, after all twelve sibling PRs are merged and tagged and the release manifest names those tags.

CI note

GitHub CI on this branch clones sibling repos at develop, not at this release branch, unless a workflow call passes siblings-ref for release branches. A cross-repo guard failure here can reflect that develop-only comparison rather than a real disagreement with a sibling still on this release branch; check the venue pre-push gate (which ships each companion at its own release branch) before treating a red guard as a defect.

…atrix legs

Since xchain-e2e-test 8df86bb the only XCHAIN gas path onto a non-BTC chain is
the bridge: the suite MINTs on BTC and locks it across with an XBRIDGE v0
(gasHelper.bridgeGasIn). A single-coin stack has no bitcoin hub config, so the
litecoin and dogecoin legs of every dispatch since 2026-09-12 failed in
initialCheck's beforeAll with "[chainRail] hub has no config for
bitcoin/regtest" (runs 35102837148 and 35102839982; last green legs
34690792431, three hours before that commit).

The two non-BTC legs now install the bitcoin regtest stack after the coin under
test, at the same ref. Two stacks on one runner need distinct host ports (the
default block collides at the second install's port check), so the leg writes
config/<coin>-regtest and config/bitcoin-regtest with the blocks chainRail's
DEFAULT_PORTS already expects, and BTC_SERVICE_HOST points the e2e container at
the docker bridge gateway, the address the spawned containers already reach the
runner's MariaDB on. Ports and that host only; credentials keep coming from the
install's own .local sidecars. The bitcoin leg is unchanged.

The test drives both step scripts under bash with a recording node stub and
asserts the install calls, their order and the file contents, per leg.
Every file under config/ is gitignored, so actions/checkout materializes no
such directory and the ports step died on its first dispatch (run 35108669606:
"config/litecoin-regtest: No such file or directory"). The test had created the
directory in its scratch checkout, which is why it stayed green; it now runs
the step in a checkout with no config/, exactly as the runner does, and the
step creates the directory itself.
mariadb:11 defaults max_connections to 151. Every service holds a pool of about
ten (indexer, decoder, hub, the explorer's per-coin pools, the e2e runner's pool
per rail); one coin stack fits under that and two do not. The first two-stack
litecoin leg (run 35109600216) booted both stacks, the hub served bitcoin and
litecoin, chainRail built the BTC rail and funded the BTC address, then died
opening the BTC rail's indexer pool with "Too many connections". The CI DB is
per-run and runner-local, so the cap protected nothing; 1000 is headroom.
…seeded local validator

The two-stack legs gas in over the bridge lock from BTC, and a standalone hub
never finalizes that transfer: startCrossChain returns before constructing
CrossChainBridgeEngine without a peerManager, so runs 35115449692 and
35115452598 got the lock valid and debited with no credit on the destination.
The validator init step now runs for every non-bitcoin leg without the input,
and exports XDEX_SEED_LOCAL_VALIDATOR=1 for those legs only, because an
identity alone signs against a cross_chain capability set that is empty on a
fresh regtest. The bitcoin leg keeps its opt-in shape and local MINT gas path.
The harness discovers every rail's node and indexer credentials through the
hub's getallconfigs, which a keyed hub gates behind HUB_API_KEY. A validator-mode
host is keyed (validator init mints the key into the hub sidecar), and the hub,
the indexer and the shared services all resolved it, but the e2e container was
still deployed keyless: the litecoin and dogecoin matrix legs 401'd in
initialCheck's beforeAll as "[chainRail] hub has no config for bitcoin/regtest"
(run 35120852486) while the standalone bitcoin leg never noticed. Same
resolution order as the indexer: host env first, then the sidecar, and a keyless
host stays keyless. Three unit cases, the first two red without the passthrough.

(cherry picked from commit 973cc2c)
…ks exist

The hub's cross-chain engines resolve each chain's indexer URL once at engine
start, from env or from the configs table, and skip a chain with none on every
later tick. On the runner the hub boots before either coin stack, so the table
is empty at that moment and the seeded validator never polled the BTC lock: run
35122297316 got the lock valid and debited with "CrossChainBridge: no indexer
URL for chain BTC/LTC" and the credit never landed. The validator init step now
exports the in-network indexer container names on port 3004 for the leg's coin
and for bitcoin, which the hub passthrough carries, the same shape the rail's
.env gives its hub. Unit case extended on both legs.

(cherry picked from commit f4f0504)
…pinned to 1 on the nightly two-stack legs

The hub's cross-chain engines propose a source leg only once it sits at the
per-coin confirmation depth (coins/index.js resolveConfirmations), six blocks
on BTC by default, and nothing on the nightly runner mines six BTC blocks
behind a bridge lock: run 35124072478 held the litecoin leg's gas lock at
"CrossChainBridge: not proposing BTC:3 (below depth 6)" until the suite gave
up on the credit. The three names now ride the hub passthrough
(hubPassthroughVars and the HUB_PASSTHROUGH_ENV view, which must mirror each
other or the read throws), and the validator init step exports them at 1 for
the litecoin and dogecoin legs, the way a regtest rail pins them. Inert on
mainnet and testnet: the hub clamps a value below the per-coin default up to
that default off regtest, so the passthrough can only raise the depth there.
# Conflicts:
#	.github/workflows/nightly-e2e.yml
#	test/unit/nightly_e2e_workflow.test/two_stack_legs.test.js
…ridge escrow proof on the two-stack legs

With the depth pinned to 1 the hub finalized the gas lock (run 35140173657:
"CrossChainBridge: finalized transfer ... BTC:3 -> DOGE 1 XCHAIN"), and the
next layer showed: before it credits a bridged transfer the destination indexer
fetches the escrow proof from the origin chain's indexer, resolved from its own
env as BTC_INDEXER_API_URL, and with nothing wired it holds the block at the
bridge proof barrier ("no origin-chain indexer endpoint is configured for the
escrow proof") for the 900 s ceiling while the chain ran 143 blocks ahead. The
ports step now writes that route into the coin's config file beside
BTC_SERVICE_HOST, which reaches every container of the coin, the indexer
included; the install already attaches the bitcoin indexer to the coin's docker
network, so its container name on the indexer's own port serves.
…nreleased after the v0.19.0 level

Levelling develop with master (2d4b33b) placed the 0.19.0 section above the
Unreleased line that was already on develop, which left that line under a
second "### Fixed" heading inside 0.19.0 beside a repeat of the hub API key
entry. The passthrough is not in v0.19.0; it and the escrow-proof route sit
under Unreleased, and the duplicate block is gone.
…service moved into src/services/bootstrap_service/

The code-structure pass moved BootstrapService.js into
src/services/bootstrap_service/archive_signing.js but kept the key path one
level short, so DEFAULT_BOOTSTRAP_PUBKEY_PATH aimed at
src/services/config/bootstrap_signing_pubkey.pem, which does not exist.
loadBootstrapPublicKey() therefore returned null unless
XCHAIN_NODE_BOOTSTRAP_PUBKEY was set: a signed-bootstrap restore refused as
"unsigned" and the auto-bootstrap install path fell back to a full sync from
genesis.

The signing suite never caught it because every case writes its own key pair
to a tmpdir and pins it through the env override, so the repo-pinned default
was never exercised. Four cases now drive the default path with real fs and no
override: the key ships where the module looks, it parses as an ed25519 public
key, it is byte-identical to the file in the tree, and checkBootstrapSignature()
consults it (a signature from a foreign key is refused for the right reason).
MIGRATE_CLI_PATHS only knew the two pre-v0.19.0 layouts, so the deploy
precondition guard read null and refused with no scoped command against
a container running the current indexer layout. The newest path is now
checked first, and the two older ones stay for older still-supported
builds. A pinned-list test fails if a future indexer move drops a path
this guard needs again.
…sh empty dir

The 'exits non-zero when the listing fails' test also proves the listing
command succeeds against a healthy directory, using os.tmpdir() directly.
A long-lived dev box's TMPDIR can accumulate tens of thousands of entries,
so `ls -A` against it runs past mocha's 2000ms budget and times out for a
reason that has nothing to do with the code under test. Use a directory
this test creates and removes instead, which keeps both assertions (the
listing fails on a nonexistent path, succeeds on a healthy one) intact.
bin/ci-full.sh hard-coded need_sib to xchain-hub alone, so a hand run
missing xchain-indexer sailed past the gate and only broke later,
confusingly, inside npm run ci. It now reads .ci-siblings (the same
roster ci-reusable.yml's sibling-checkout step reads) so the two
cannot drift, and arms XCHAIN_REQUIRE_SIBLINGS for the ci tier the
same way ci-reusable.yml arms it once siblings are checked out.

The coverage CI job re-ran the unit suite with no siblings checked
out and no XCHAIN_REQUIRE_SIBLINGS, so every cross-repo guard
silently skipped there: green by absence, not by the guard actually
running. Mirrors ci-reusable.yml's own sibling-checkout step verbatim
so coverage now measures the same suite the ci job runs, and only
arms the flag when the checkout genuinely happened.
…hub and indexer containers

Same shape as the rollcall gates lever: regtest only, absent on testnet and mainnet. Without it the admission-height crossing could not be armed on the rail.
Pins eleven components at their signed, origin-verified v0.20.0 tags and carries
xchain-contracts forward at v0.17.0, which this train does not touch. Arms the
0.20.0 rule set at BTC testnet 153116, computed from tip 153018.

Generated by bin/write-release-manifest.js against origin, never by hand.
release: regenerate the v0.20.0 manifest from the signed sibling tags
@jdogresorg
jdogresorg merged commit 280223e into master Sep 19, 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