Skip to content

Release v0.17.0 - xchain-node - #43

Merged
jdogresorg merged 20 commits into
masterfrom
release/v0.17.0
Sep 11, 2026
Merged

jdogresorg merged 20 commits into
masterfrom
release/v0.17.0

Conversation

@jdogresorg

Copy link
Copy Markdown
Contributor

Bumps the package and lockfile to 0.17.0 and dates the 0.17.0 changelog entry. The release manifest, README badge and platform release notes are written on this branch once the sibling tags exist, so this PR is the last of the train to merge. Platform notes ride this repo's Release.

The explorer's batch balance and coinpay-obligation routes share one limiter
with its own knob; it rides the same by-name passthrough as the other eight
explorer rate-limit variables so a deployment can size it from the host env.
…de is reset

A regtest chain reset is a re-genesis, and the hub's cross_chain_matches,
cross_chain_calls and capability_snapshots rows are scoped by network alone,
so every fresh indexer re-mirrored the dead chain's finalized matches and
refused them at every block (spec cross-settle-relic-matches.md, leg 3).

`reset node`/`reset all` on regtest now purges them beside the price-fence
delete, named in the confirmation, never fatal (prints the statements and the
hub restart on failure). A Bitcoin reset clears all three tables for the
network; a non-Bitcoin reset clears only matches and calls with a leg on that
chain. capability_snapshots has no network column, so its unscoped delete
runs only when the co-located hub is proven to be the regtest stack's.
…ugh with the other three, so an armed regtest venue no longer holds every block 60s behind a frozen match mirror
… rides the regtest pass-through, since each mirrored table's barrier holds blocks on its own
Every install points its coin indexers at the hub container on the same host,
and that hub judges the on-chain PRICE batches they read. A non-validator
install gave it neither the network the batch rules are keyed on nor a BTC
indexer to read capability snapshots from, so it failed closed on every batch
with insufficient signer stake and the price mirror never filled.

HUB_NETWORK is now derived from the single network this deployment's stacks
run on, or from the network the running install names when no stack is
registered yet; a deployment spanning two networks stays unset and says so.
BTC_INDEXER_API_URL composes the co-located bitcoin indexer for that network
when one exists, and warns naming the consequence when none can be composed.
Host env wins in both cases, and the composition is gated on the indexer
actually existing so it cannot shadow a working configs-table push.

Measured on a stock 0.16.2 install in an isolated docker-in-docker sandbox:
before, the hub container carried no HUB_NETWORK at all and an empty
BTC_INDEXER_API_URL; after, HUB_NETWORK=testnet and the bitcoin case composes.
…eanup, node first under all

Four operator findings from one mainnet Pi install, each fixed where it lives:

- XCHAIN_NODE_STOP_TIMEOUT_SECONDS overrides the 600 s flush budget a coin
  node gets before docker kills it, on update and as the container's own
  stop timeout. The stop now reads the container's exit code, prints how
  long the daemon took, and warns when it ran out of budget and was killed,
  because a killed mainnet bitcoind came back 17000 blocks lower and
  re-validated for four hours while the update said nothing.
- ps marks a decoder or tracker whose coin node is not answering as NODE
  UNREACHABLE with the duration and a note saying it is not the IBD wait,
  from the node_last_ok_at and node_unreachable fields the services publish.
- A bootstrap restore removes latest.tgz and its .sig after success or a
  node-behind refusal (the download is unconditional, so a kept copy buys
  nothing), keeps it after a failure and says so with the size, and the
  install summary carries what came back. A 151 GiB tracker archive had sat
  in a bootstrap volume for a week.
- The coin node leads the all expansion, so install creates it before the
  services that poll it: a decoder was created four and a half hours before
  its node existed and logged ENOTFOUND the whole time.
…t activation positions

It parses addChange() mainnet arguments, mainnet map entries and *MAINNET* constants for 9999999999 or 999999999 instead of substring-matching 1798761600, which is CROSS_CHAIN_ROYALTY's ARMED instant (ruled 2026-09-09), not a placeholder.
…rom where

getValidatorEnv() answers {} for three situations a hub deploy could not tell
apart: a genuine standalone node, a validator deliberately disabled, and a
validator whose files are intact but not under the configDir this process
resolved. VALIDATOR_DIR is path.join(configDir, 'validator') and configDir
defaults to <xchain-node>/config, so a config directory holding only the .local
sidecars makes isInitialized() false and silently drops P2P_VALIDATOR_ADDR,
SIGNING_PRIVKEY_HEX, HUB_CAPABILITY_CONFIG and the /validator mount.

The failure was invisible in every direction: no error, no warning, a healthy
container, and getanchorstatus reporting active:true from hub-local effector
flags while the pubkey was in no capability set at all and the anchor publisher
never ran. A reset only restarts containers so it never fired; an update
recreates them so it did.

validatorModeReport() separates the three, and the hub config now states the
mode and the directory it came from every time. A HALF-present state (one of
validator.json / signing.key) is called incomplete and warned about, because that
is never a standalone node, it is a broken install.

Deliberately a statement and a warning, never a throw. Some hubs are
legitimately standalone, and this repo's own config dir already carries a
sidecar-without-base for bitcoin-testnet, bitcoin-mainnet and dogecoin-testnet,
so a fatal guard would have wedged those deploys rather than informing them.

Falsified by deleting the incomplete branch: the half-present cases then report
standalone, which is the original defect, and two cases go red. Restored
byte-exact, sha256 equal either side. Driven against real directories rather than
only fixtures: this machine's config dir reports validator, an empty dir reports
standalone with both files missing, a half-populated one reports incomplete
naming the one missing file, and enabled:false reports disabled.

The three ConfigService ValidatorService stubs gain the new function. A stub that
omits it is not a standalone machine, it is an incomplete module, and leaving
production code to guard around that would have hidden the same class of gap
this change exists to expose.
…work

A reset cleared the hub's price-ingest fence by chain, so resetting one
network could drop the fence row another network was relying on.

The obvious fix, scoping the delete by network, is not available and would be
worse than the bug: the fence table is keyed on the source chain alone and
has no network column, so the clause would fail the statement outright. The
caller catches and warns, so the fence would then never clear at all, which
is the silent price-rail outage this function exists to prevent.

So the guard is where the information actually is. When the co-located hub
declares a network and it is not the one being reset, the clear is refused
and the statement to run by hand is printed. A hub that declares no network
contradicts nothing and behaves exactly as before. This mirrors the guard the
sibling capability-snapshot clear in this file already uses.

Separating the fences properly needs a network column on the hub table and
its accessors, which is a fleet migration and is tracked as its own item.
…repairs it

When the hub container was restart-looping, the pre-check pushed
configuration to it, failed after nine connection refusals and aborted every
command, including the update that would have rebuilt it. The only way out
was removing the container by hand.

The pre-check now asks once whether the hub answers, with a single request
and no retries or restart attempts, because only a request distinguishes a
crash-looping container from a healthy one: a looping container is still
registered, still has an id and still reports a status. When the hub is down
and the command about to run would put it back, the configuration push is
skipped with a warning and the command proceeds. When the hub is down and the
command is anything else, it still aborts, and the message now names the
command that repairs it and points at the container logs. A healthy hub keeps
the previous behaviour byte for byte, including the existing error text.

A command counts as repairing only when the verb replaces or restarts
container state and the target actually resolves to the hub, using the same
argument resolver the actions use. That includes the update that carries the
shared services hub-first, the documented teardown that removes the hub, and
the autoheal sweep whose whole job is restarting unhealthy containers. It
excludes stop, everything that works through a hub it needs running, a
repairing verb aimed at another service, and the bulk forms whose expansion
never includes the hub.

The hub update takes a flag that keeps the shared-container network attaches,
which work on a container that is not serving, and drops only the push. The
key sidecar load stays ahead of everything that talks to the hub, and a
regression test pins that on the new unhealthy path, where an ordering
mistake would hide.
- fix(validator): probe what a host resolves as its capability set against what the indexer answers
- fix(node): forward LEVELDB_CACHE_BYTES and LEVELDB_WRITE_BUFFER_BYTES to the utxo-tracker container
- fix(node): read the LevelDB passthrough variables by name so the doc-coverage gate can see them
- fix(node): bitcoin testnet chain subdir is testnet4, dogecoin stays testnet3
- test(node): seal e2e-env call-time requires so proxyquire stubs are not bypassed

Squashed from the local landing commits: 2407a6a9 4662d7fc ab19c0c4 00ca21e5 d671a9eb
The lines those commits added were pushed with their leading whitespace
collapsed to one space by a message-scrubbing step; content is unchanged.
… docker kill on update, recreate, uninstall and stop

Every CLI path that took a service container down used `docker kill` (update,
recreate, uninstall) or a bare `docker stop` with docker's ten seconds (stop),
so no service ever received a bounded drain and the indexer's SIGTERM handler
was dead code. An operator's `docker stop -t 180` on a BTC mainnet stack read
encoder 143, indexer 0, tracker 1, decoder 137.

StopBudgetService gives each service a budget (decoder and utxo-tracker 120 s,
others 30 s, the coin node its own 600 s resolver), overridable through
XCHAIN_NODE_MODULE_STOP_TIMEOUT_SECONDS_<SERVICE>, stamps it on the container
as --stop-timeout so a plain docker stop honours it, and prints a clean stop
with its duration or a warning naming the override when the process was killed.
R-LIF-003 and R-LIF-008 stubbed stopContainer only, so once stopModules
stops with a budget the call fell through proxyquire to the real docker on
the CI venue and nothing was recorded.
Bumps the package and lockfile to 0.17.0 and dates the 0.17.0 changelog entry. The
manifest, README badge and release notes are written separately from the tagged sibling
SHAs.
@CLAassistant

CLAassistant commented Sep 10, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Pins the twelve component repos at their signed v0.17.0 master merge commits,
resolved against origin by the platform manifest generator rather than by hand.
Also sets the README version badge to 0.17.0, which the stats refresher cannot
do before the carrier's own tag exists.
@jdogresorg
jdogresorg merged commit be5ce31 into master Sep 11, 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.

2 participants