Skip to content

Release v0.15.0 - xchain-node - #32

Open
jdogresorg wants to merge 30 commits into
masterfrom
release/v0.15.0
Open

Release v0.15.0 - xchain-node#32
jdogresorg wants to merge 30 commits into
masterfrom
release/v0.15.0

Conversation

@jdogresorg

Copy link
Copy Markdown
Contributor

Cuts xchain-node for the v0.15.0 release train. 29 commits from master.

Added

  • The indexer's hub mirror is armed on regtest, and the attest response, roll-call rail and oracle batch landing-reserve knobs pass through to the hub and indexer.
  • A private explorer can set its own serving limits.
  • A reindex forces a bootstrap republish.
  • The tracker volume is snapshotted by hardlink and an encoder maintenance window is declared around it.
  • ENCODER_TRUST_PROXY, ENCODER_RATE_LIMIT_RPM, and five explorer per-route rate-limit knobs now pass through from the host env, so a container recreate no longer drops them.

Fixed

  • A chain daemon is stopped gracefully on update and its release tree is staged before the swap.
  • validator init no longer mints a hub API key on a re-run, and the CLI sends the key it generated when it pushes config to the hub.
  • HUB_RATE_LIMIT_EXEMPT_LOCAL passes through to the hub container.
  • The hub consensus-env guard derives its key list per network.
  • Reset resolves the datadir from the container bind mount and fails closed instead of skipping the chain wipe.
  • An explicitly injected null validator settings object is honoured.
  • The regtest block-assembly fee floor is lowered beside the relay floor.
  • The mainnet federation oracle epoch defaults to its ruled past instant.

…d fail closed instead of skipping the chain wipe
Re-running init no longer mints a HUB_API_KEY that was not asked for;
--mint-hub-api-key is the explicit opt-in.
…e CLI pushes config to the hub

`validator init` writes HUB_API_KEY to config/hub.local and the hub container
deploys keyed from that sidecar, but the CLI only sent the key it found in
.env. On a validator host provisioned per the runbook the config push that
follows `install xchain-hub` was therefore keyless against a keyed hub and
failed with HTTP 401, and so did every state-changing command after it.
preCheck now fills the CLI's own env from the same sidecar before the hub is
installed or pushed to. A host-env key still wins and nothing is ever minted,
so a host with no sidecar stays keyless as before.
The explorer's request budgets and its tip-age freshness gate both default to
values chosen for a public deployment, and a shared service has no per-venue
config file to override them in. Pass EXPLORER_*RATE_LIMIT_RPM and
EXPLORER_TIP_MAX_AGE_S (including the per-coin form) through from host env, the
same injection point the published ports and the CORS origin already use.

Both defaults misfire on a private venue. One tunnelled dev box is a single IP,
so every browser and every test run shares one 500/min bucket. And a regtest
chain only advances when someone mines it, so a chain nobody is driving crosses
the six-hour age gate and the explorer starts refusing every read for it with
COIN_DATA_STALE while its lag is zero.
A reset rebuilds a store on a new lineage, so every bootstrap already
published for that combo describes the old one and a fresh install that
restores it halts. A reset now records the combos it wiped, bootstrap
create clears them, and the new bootstrap-republish-due command lists
the difference so the publisher can pull due combos into its plan even
when the schedule or the tracker opt-in would have skipped them.
An explicit null says there is no validator, so it must not fall through
to the real validator directory the way an absent injection does.
Litecoin and Dogecoin regtest nodes keep a separate blockmintxfee, so a
low-fee transaction is accepted into the mempool and never mined unless
both floors come down together.
An epoch close needs a reachable DOGE indexer on every network, and the
regtest arming opt-in has to reach the indexer and the container hub
together or the venue reports a rules mismatch. The arming variable is
gated on regtest here as well as in the activation module itself.
The time budgeted between a window closing and its batch being readable
on chain varies by venue, so it belongs beside the other batch knobs
rather than clamped to the fleet default.
The filterCommandParameters() block opened one describe too early, which
listed the hub credential-report cases under the wrong suite.
…st response knobs through

Regtest stacks now get the same hub-DB mirror pointer mainnet and testnet
always had, including the password reconciliation that a second gate had
kept off, and default the mirror grace windows to zero unless the host
sets them. The hub receives the attest response forward-margin and batch
window overrides when the host provides them.
…ease tree

Pin Bitcoin Core 31.1 and Litecoin Core 0.21.5.6, and fix the two ways
`update node` damaged a live daemon on the way there.

The update path force-removed the node container before rebuilding. That
is SIGKILL, so the daemon came back at its last flushed block index: a
regtest rehearsal lost 16 mined blocks, and a mainnet node would face a
long replay instead. The running node is now left alone until
buildCryptoNode stops it with a 600 second flush budget, immediately
before the removal and re-run it already performs. Containers are created
with the same stop timeout so an operator's own restart is safe too.

The release downloader extracted an archive into the previous version's
tree. The archive's top-level directory is flattened only when it is the
sole entry, so a second release landed nested beside the old bin/ and
share/, and the image was built from the OLD binaries under a NEW version
file. Downloads now extract into a staging sibling that is swapped in
once complete, which also leaves the previous tree intact when a download
or hash check fails.

Bitcoin Core 31 no longer parses limitdescendantsize, replaced by the
cluster limits, so drop it from the regtest config.

The 28.1 and 0.21.4 hashes stay: a pinned install of either still verifies.
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: 6411 6533 6534 6535 6536 6537
…plorer's five per-route caps through from the host env

The encoder behind a cross-box reverse proxy trusts only loopback and
private peers, so its per-IP limiter keyed every visitor on the proxy's
egress address. ENCODER_TRUST_PROXY names that address and now survives
update and recreate like the hub's settings do. The explorer's five
per-route limiters were unreachable on a node-managed explorer; all eight
are now host-env settable. Read by name so the env-var coverage gate sees
each variable.
…ency they probe

Three healthcheck startPeriod literals were each chosen per service instead of
being derived from the step they actually wait on, so the encoder, hub and
explorer could each be declared unhealthy while their dependency was still
starting normally.

A new DEPENDENCY_HEALTH_START_PERIOD in src/config/constants.js now feeds those
three descriptors in ModuleService.js and MariaDB's own --health-start-period
push in DatabaseService.js, so the two sides cannot drift apart in a one-sided
edit. Self-judging boots (decoder /live, indexer, tracker, miner, sync's hub
wait) deliberately keep their own literals.

The DatabaseService assertion pins the EMITTED --health-start-period value
rather than only the flag's presence, so putting a literal back on the DB side
is caught. Negative controls executed: forcing the constant to 45s and to 30s
reddens the encoder assertion.

Review round 7 cluster 2675ab894ab8 (findings #6814, #6815, #6816) plus #6856.
Also carries review round 6's node work.
The gate was on-demand only, disabled because the sub-repos were private and a
scheduled run would have failed at the first clone. They are public now, so
anonymous clones work and the stated reason is gone.

Leaving it on-demand has a cost that a release pays. A full pass is about an
hour and fifty minutes and it is the only gate that exercises consensus and
money movement end to end, so a cut that meets it for the first time discovers a
whole train's worth of breakage against a two-hour clock, and every failure
restarts that clock. Run nightly against develop, the same discovery happens on
a day when nobody is waiting on it.

The coin list is event-dependent because a scheduled run carries no inputs, so
without it the nightly would silently test one coin and be two thirds blind: the
litecoin and dogecoin legs are where chain-specific breakage lands. A dispatch
still runs exactly the single coin it names. The legs are independent stacks, so
fail-fast is off: a release needs all three verdicts, not whichever failed first.

Verified by dispatch on a throwaway branch: selecting litecoin produced exactly
one job, e2e (litecoin), so the matrix resolves and the input is not falling
back to the default.
Every leg of a three-coin matrix rendered as the identical workflow name in the
run list, so the only way to tell which chain a run was grading was to open it.
That is precisely the moment you cannot afford it: a release matrix is in
flight, one leg has gone red, and finding it costs three clicks.

The run title is a different string from the workflow name and is the one the
list renders, so it carries the coin. It also carries the ref, because the same
workflow grades develop, a release branch and a published tag, and during a cut
the list holds runs against more than one of them at once; "which chain" stops
being enough. A suite filter appears only when one is set, which keeps an
ordinary full pass short while making a single-suite investigation obvious next
to the full matrix it was cut from.

A scheduled run says "all coins" rather than naming one, because the schedule
covers all three in a single run and naming a coin there would be a lie.
…wn block cadence

The anchor-reward attestation barrier holds a block until the hub-mirror stream
watermark is 120 seconds past that block's own timestamp. On a shared ledger
that costs nothing, because blocks are ten minutes apart and the watermark is
long past by the time one is processed. On regtest, blocks are stamped at about
wall clock and the watermark tracks wall clock, so a freshly mined block can
never be 120 seconds behind it. The condition cannot be satisfied, and every
affected block waits out its full minute before proceeding anyway.

Measured on the release matrix: the bitcoin leg parsed 367 blocks in six hours
and was killed by the job budget, against 2013 blocks in under two hours on the
build before the mirror was armed. A hundred and sixty deferrals, about two and
three quarter hours spent waiting for something that could not arrive. The other
two coins were green throughout, because the barrier is bitcoin-only, which is
what made a venue constant sized for the wrong block cadence look like a defect
on one chain.

The indexer already had the seam for this and it is deliberately one-sided: the
grace is honoured on regtest and ignored with a warning anywhere else, because a
watermark grace is a consensus input and a per-node value forks settlement. This
adds the matching passthrough, gated on regtest a second time so neither gate
alone can carry a host variable onto a shared ledger, and sets it for the venue.

Two seconds rather than zero, so the barrier still enforces the ordering it
exists for and a genuine mirror-lag defect cannot ride through green.
…e state

Review-round fixes.

Three provisioning paths conflated "the inspection failed" with "the object is
absent or empty", so a transient container, registry or database error let a
destructive step proceed: a swallowed volume-wipe failure that then dropped the
decoder and indexer databases, a registry error letting reset drop databases
without stopping their services, and a transient database failure triggering
destructive bootstrap during an update. A failed inspection is now an error
rather than an emptiness verdict.

Suite: 1988 passing, 0 failing.
The grace added earlier fixed a barrier that could never be satisfied. It
revealed one that legitimately cannot be satisfied yet: on a venue that mines in
seconds the hub mirror runs a minute or two behind the chain, so blocks defer
for real and the barrier is right to hold them.

What a fast venue cannot afford is the price of each hold. The attempt budget
bounds one try, and on expiry the block is deferred and retried rather than
committed uncertified, which the indexer states outright. So shortening it
trades away no safety whatsoever; it only stops a correct decision from costing
a minute every time it is taken. Measured at the default: a hundred and nineteen
deferrals burned a hundred and nineteen minutes of a two hundred and eighty-nine
minute bitcoin leg, two fifths of the wall clock, and left the indexer far
enough behind that thirty end-to-end waits gave up on rows that had simply not
landed.

Ten seconds keeps the barrier honest and makes a deferral cost a tenth as much.
Unlike the grace this is not a consensus input and carries no fork risk, but it
is still passed through on regtest alone, because a shared ledger wants the long
attempt: there a lagging mirror is a fault worth waiting on rather than a
mismatch between block cadence and delivery rate.
…or two hours

The e2e COINPay expiry case cannot wait out a two-hour obligation deadline, so
it freezes the node clock past it and mines two blocks. Those blocks are then
stamped two hours in the future, and the indexer's anchor-attest barrier holds a
block until the hub's wall-clock watermark reaches its timestamp. The case
avoids waiting two real hours and the indexer waits them instead.

Measured on run 34015867460: every one of the 119 deferrals in the bitcoin leg
named the same block, held for 2h08m50s out of a 289-minute run. Nothing was
lagging. The watermark stayed within six seconds of wall clock the whole time
and advanced at 0.9999 of real time, and the hub logged no late heartbeat, no
backpressure close and no socket churn.

The venue now sizes the window itself, at 300 seconds rather than 7200, which
takes the clock jump from 2h10m to about six minutes. 300 and not less because
the case still has to observe the obligation PENDING before expiring it, and a
window shorter than that setup would expire it underneath the assertion.

The window is a consensus input, so the indexer honours the override only on
regtest and ignores it with a warning elsewhere; this passthrough is gated on
regtest a second time, so neither gate alone can carry a host value onto a
shared ledger.

Also corrects the note on the attempt-budget knob above it, which claimed the
mirror ran a minute or two behind this venue. It did not, and leaving that
sentence in place would hand the next reader the wrong cause.
# Conflicts:
#	CHANGELOG.md
#	package-lock.json
A rebase onto develop brought an [Unreleased] section back above the release
block, carrying one entry for host-env passthrough of the encoder and explorer
rate-limit knobs. That code is in this release, so a changelog that files it
under "unreleased" is wrong in the one place a reader checks to find out what
shipped.

Folding rather than deleting: the entry is real, it just belongs in the version
that carries it. This is the step the recut leaves until the fold pass on
purpose, because during a re-cut both sections are legitimately present and
resolving it early would drop whichever side the rebase replayed second.
@jdogresorg jdogresorg changed the title Release v0.15.0 Release v0.15.0 - xchain-node 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.
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