Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
91dba39
config(validator): default the mainnet federation oracle epoch to its…
jdogresorg Sep 3, 2026
a25085a
fix(node): resolve the reset datadir from the container bind mount an…
jdogresorg Sep 3, 2026
d91f1f0
feat(node): snapshot the tracker volume by hardlink and declare an en…
jdogresorg Sep 3, 2026
15520cb
fix(node): derive the hub consensus-env guard key list per network
jdogresorg Sep 3, 2026
f5fd1e1
fix(node): pass HUB_RATE_LIMIT_EXEMPT_LOCAL through to the hub container
jdogresorg Sep 3, 2026
8459eda
fix(node): stop validator init minting a hub API key on a re-run
jdogresorg Sep 2, 2026
2559fea
fix(node): send the hub API key that validator init generated when th…
jdogresorg Sep 2, 2026
df934eb
config: let a private explorer set its own serving limits
jdogresorg Sep 3, 2026
f75ad9e
chore(coins): sync the vendored coin registry
jdogresorg Sep 3, 2026
f504307
feat(node): force a bootstrap republish after a reindex
jdogresorg Sep 3, 2026
d19f7d9
fix(node): honour an explicitly injected null validator settings object
jdogresorg Sep 3, 2026
570ccca
fix(regtest): lower the block-assembly fee floor beside the relay floor
jdogresorg Sep 3, 2026
3ace65a
feat(node): pass the rollcall rail env through to the indexer and hub
jdogresorg Sep 3, 2026
43ecedf
feat(node): pass the oracle batch landing reserve through to the hub
jdogresorg Sep 3, 2026
2a1cc20
test(node): keep the credential-report cases inside their own describe
jdogresorg Sep 3, 2026
40db31c
feat(node): arm the indexer's hub mirror on regtest and pass the atte…
jdogresorg Sep 3, 2026
54ffed4
fix(node): stop a chain daemon gracefully on update and stage its rel…
jdogresorg Sep 4, 2026
609aa45
fix(node): guard the reset and DDL paths against half-destroying a stack
jdogresorg Sep 4, 2026
ee99294
node: pass the encoder trust-proxy and rate-limit settings and the ex…
jdogresorg Sep 4, 2026
5fa9684
fix(node): derive dependent healthcheck start periods from the depend…
jdogresorg Sep 5, 2026
67d6e37
ci: run the e2e gate nightly, across all three coins
jdogresorg Sep 5, 2026
f7a09bf
ci(e2e): title each run with the chain it is actually testing
jdogresorg Sep 5, 2026
5919769
fix(e2e): let a regtest venue size the anchor-attest barrier to its o…
jdogresorg Sep 6, 2026
c379a53
fix(node): destructive provisioning paths fail closed on an unreadabl…
jdogresorg Sep 6, 2026
c5b7678
fix(e2e): make a deferred block cost a tenth as much on a fast venue
jdogresorg Sep 6, 2026
b71644f
fix(e2e): stop the COINPay clock jump from stalling the bitcoin leg f…
jdogresorg Sep 6, 2026
8683e1f
Merge remote-tracking branch 'origin/master' into HEAD
jdogresorg Sep 6, 2026
d2fc571
release: v0.15.0
jdogresorg Sep 4, 2026
4e2c90c
docs(changelog): fold the stray unreleased entry into 0.15.0
jdogresorg Sep 6, 2026
1eb2180
feat(activation): arm the ATTEST response mirror on Bitcoin testnet
jdogresorg Sep 7, 2026
a2e2f95
release: pin the v0.15.0 component set from the signed tags
jdogresorg Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 114 additions & 7 deletions .github/workflows/nightly-e2e.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
name: E2E (regtest)

# The RUN title, which is a different string from `name` above and is the one
# that actually distinguishes runs in the Actions list. Without it every leg of
# a three-coin matrix renders as the identical "E2E (regtest)" and the only way
# to tell which chain a run is testing is to open it - which is exactly what you
# are doing when a release matrix is in flight and you want the one that failed.
#
# A dispatch names its coin. A scheduled run covers all three in one run (see the
# matrix on the job), so it says so rather than naming a coin it is not limited
# to. The ref is included because the same workflow grades develop, a release
# branch and a published tag, and "which code" is the second question after
# "which chain". A suite filter is appended only when one is set, so the common
# full-suite run stays short.
run-name: >-
E2E ${{ github.event_name == 'schedule' && 'all coins' || inputs.coin }} regtest @ ${{ inputs.ref || 'develop' }}${{ inputs.suite && format(' [{0} only]', inputs.suite) || '' }}

# Cross-component integration gate. Boots the FULL XChain stack on regtest via
# xchain-node - which clones every service repo at the REF THIS RUN NAMES and
# runs them as Docker containers - then runs the live xchain-e2e-test suites against it
Expand Down Expand Up @@ -28,12 +43,23 @@ name: E2E (regtest)
# ─────────────────────────────────────────────────────────────────────────────

on:
# Manual / on-demand gate. The nightly schedule is intentionally DISABLED while the
# sub-repos are PRIVATE - a scheduled run would fail at the first private clone and
# spam nightly failures. Re-enable it once the repos are public per the launch plan
# (anonymous HTTPS clones just work then, and no SUBREPO_CLONE_TOKEN is needed):
# schedule:
# - cron: '0 7 * * *' # 07:00 UTC nightly
# Nightly gate, RE-ENABLED 2026-09-05. It was disabled only because the sub-repos
# were PRIVATE and a scheduled run would fail at the first clone; they are public
# now, so anonymous HTTPS clones work and no SUBREPO_CLONE_TOKEN is needed.
#
# WHY A NIGHTLY IS WORTH ITS RUNNER TIME. This gate is the only thing that
# exercises consensus and money movement end to end, and a full pass is ~1h50m,
# so a release cut that meets it for the first time discovers a whole train's
# worth of breakage against a two-hour clock and has to restart it. Running the
# same pass every night against develop moves that discovery to a day when
# nobody is waiting. The v0.15.0 cut is the worked example: at cut time the last
# matrix evidence was three days old AND had been taken against master, so the
# entire attestation-mirror and roll-call surface had never once met this gate.
#
# Hosted minutes are free on a public repo, so the cost of the nightly is zero
# and the cost of not having it is a restarted release.
schedule:
- cron: '0 7 * * *' # 07:00 UTC nightly, against the `ref` default below
#
# NOTE for whoever re-enables the cron: a scheduled workflow runs from the
# DEFAULT branch's file, so once develop is the default the nightly boots
Expand Down Expand Up @@ -107,6 +133,17 @@ permissions:
jobs:
e2e:
runs-on: ubuntu-latest
# WHY THE COIN LIST IS EVENT-DEPENDENT. A scheduled run carries NO inputs, so
# without this it would silently test bitcoin alone and the nightly would be
# two thirds blind: the litecoin and dogecoin legs are where the chain-specific
# breakage actually lands. A dispatch keeps exactly its old behaviour, one
# chosen coin, because a subset proves a fix and must not pretend to be a train.
# The three legs are independent stacks, so fail-fast would throw away two
# answers to report one; a release needs all three verdicts, not the first.
strategy:
fail-fast: false
matrix:
coin: ${{ fromJSON(github.event_name == 'schedule' && '["bitcoin","litecoin","dogecoin"]' || format('["{0}"]', github.event.inputs.coin || 'bitcoin')) }}
# A BTC full action suite alone runs ~1h50m of wall clock, and the security
# and performance suites are sequenced AFTER it, so at 120 the two of them
# shared whatever minutes the action suite happened to leave - usually none.
Expand All @@ -126,7 +163,77 @@ jobs:
# legitimately long pass.
timeout-minutes: 360
env:
COIN: ${{ github.event.inputs.coin || 'bitcoin' }}
COIN: ${{ matrix.coin }}
# The anchor-reward attestation barrier holds each block until the hub-mirror
# stream watermark is 120s past that block's own timestamp. On a shared ledger
# that is free, because blocks are ten minutes apart and the watermark is long
# past by the time one is processed. Here blocks are stamped at about wall clock
# and the watermark tracks wall clock, so a freshly mined block can never be
# 120s behind it: the barrier cannot be satisfied and every affected block burns
# its full 60s timeout before proceeding anyway.
#
# Measured on the 2026-09-06 release matrix, before this line existed: the BTC
# leg parsed 367 blocks in six hours and was killed by the job budget, against
# 2013 blocks in 1h52m on the pre-mirror build. 160 deferrals, ~2.7 hours spent
# waiting for a condition that could not arrive. The litecoin and dogecoin legs
# were green in ~2h25m throughout, because this barrier is BTC-only, which is
# exactly what made it look like a bitcoin-specific defect rather than a venue
# constant sized for a different block cadence.
#
# 2s, not 0: the barrier still has to MEAN something here, or the venue stops
# exercising the ordering it exists to enforce and a real mirror-lag defect would
# ride through green. The indexer accepts this ONLY on regtest and ignores it with
# a warning anywhere else, because a watermark grace is a consensus input and a
# per-node value forks settlement - so this cannot leak onto a shared ledger even
# if it is copied somewhere it does not belong.
HUB_SYNC_ANCHOR_ATTEST_GRACE_S: "2"
# Bounds ONE mirror-barrier attempt. On expiry the block is DEFERRED and
# retried, never committed uncertified - the indexer says so outright
# ("purely operational: it opens no barrier and commits no block") - so a
# shorter attempt trades away no safety at all, only the cost of a failure.
#
# It is here because the grace above fixed the barrier that could never be
# satisfied, and revealed a second cost behind it. Measured at the 60s
# default on 2026-09-06: 119 deferrals cost 119 minutes of a 289-minute
# bitcoin leg, 41% of the wall clock. Ten seconds keeps the barrier honest
# while making a deferral cost a tenth as much.
#
# CORRECTION, measured 2026-09-06 off run 34015867460's own artifact: those
# 119 deferrals were NOT a lagging mirror. They all name the SAME block, and
# the stream watermark tracked wall clock throughout (1-6s behind, advancing
# at 0.9999 of real time) while the hub logged no late heartbeat and no
# backpressure close. The block was future-stamped by the COINPay clock jump
# (see XCHAIN_COINPAY_EXPIRATION_S below), so the barrier was waiting on real
# time, not on delivery. This knob therefore makes a failed attempt cheaper
# but recovers no wall clock on its own; the window below is what does.
#
# NOT a consensus input, unlike the grace, so it carries no fork risk - but
# it is still passed through on regtest only, because a shared ledger wants
# the long attempt: there a lagging mirror is a fault worth waiting on.
HUB_PRICE_SYNC_TIMEOUT_MS: "10000"
# COINPay obligation expiration window, in seconds. The shared-ledger value is
# 7200 (two hours), which no e2e suite can wait out, so the COINPay expiry case
# freezes the node clock past the deadline and mines. That stamps the mined
# blocks two hours into the FUTURE, and the anchor-attest barrier above compares
# a block's own timestamp against a wall-clock watermark: the indexer then waits
# those two hours in REAL time on that one block, with every other block behind
# it deferred.
#
# Measured on run 34015867460 (bitcoin, 2026-09-06): blocks 516 and 517 were
# mined at 07:08:10Z and stamped 09:18:01Z, and the indexer deferred block 516
# 119 times over 2h08m50s. That single stall is 44% of the 289-minute leg and is
# the reason thirty downstream waits gave up. Nothing was lagging.
#
# 300s, not seconds: the case still has to assert the obligation is PENDING
# before it expires it, and a window shorter than that setup window would expire
# the obligation underneath the assertion and make the case flaky in the other
# direction. 300 leaves the clock jump at about five minutes instead of 2h10m.
#
# Consensus input, so the indexer accepts it ONLY on regtest and ignores it with
# a warning anywhere else (resolveCoinpayExpiration); the node's passthrough is
# gated on regtest a second time. Neither gate alone can carry it onto a shared
# ledger, which is what makes a venue-local window safe to set at all.
XCHAIN_COINPAY_EXPIRATION_S: "300"
# Drives BOTH the xchain-node checkout below and the `install` boot
# argument, so the CLI running the install is the same version as the
# stack it installs. Splitting those two was how "we tested the release"
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.15.0] - 2026-09-07

### 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.

### Activation
- The ATTEST response mirror activates on Bitcoin testnet at block 151324 and on regtest from genesis. Mainnet is unratified and the legacy on-chain response path runs there byte for byte.
- ROLLCALL activates on Bitcoin testnet at block 151200, which the chain has already passed, so it is live from the moment a node updates. Mainnet is unratified.
- Both change state derived from existing bytes on testnet, so an updated node and one still on 0.14.0 judge a mirrored response differently once one lands. Update every indexer and hub together.

## [0.14.0] - 2026-09-02

### Fixed
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# XChain Platform Node

<p align="center">
<img src="https://img.shields.io/badge/version-0.12.3-blue" alt="Version">
<img src="https://img.shields.io/badge/tests-2%2C680%2B%20passing-brightgreen" alt="Tests">
<img src="https://img.shields.io/badge/version-0.15.0-blue" alt="Version">
<img src="https://img.shields.io/badge/tests-2%2C827%2B%20passing-brightgreen" alt="Tests">
<img src="https://img.shields.io/badge/node-%3E%3D22-green" alt="Node">
<img src="https://img.shields.io/badge/license-AGPL--3.0--or--later-blue" alt="License">
</p>
Expand Down Expand Up @@ -156,19 +156,19 @@ Turn it off with any of: `--no-telemetry` on any command (sticks for future runs

| Command | Description |
|---|---|
| `npm test` | Unit tests (1,787 tests) |
| `npm run test:integration` | Integration tests (103 tests) |
| `npm test` | Unit tests (1,930 tests) |
| `npm run test:integration` | Integration tests (105 tests) |
| `npm run test:smoke` | Smoke tests (159 tests) |
| `npm run test:boundary` | Boundary condition tests (57 tests) |
| `npm run test:security` | Security tests (74 tests) |
| `npm run test:security` | Security tests (76 tests) |
| `npm run test:e2e` | End-to-end tests (57 tests) |
| `npm run test:fuzz` | Fuzz tests (264 tests) |
| `npm run test:chaos` | Chaos engineering tests (121 tests) |
| `npm run test:regression` | Regression tests (58 tests) |
| `npm run test:regression:p0` | Regression P0: critical gate (33 tests) |
| `npm run test:regression:p0p1` | Regression P0+P1: standard gate (51 tests) |
| `npm run test:mutation` | Mutation testing (Stryker Mutator) |
| `npm run test:all` | All tests (~2,549 tests; excludes security/boundary) |
| `npm run test:all` | All tests (~2,694 tests; excludes security/boundary) |
| `npm run benchmark` | Performance benchmarks (5 scenarios) |
| `npm run benchmark:quick` | Quick benchmarks |

Expand Down
4 changes: 3 additions & 1 deletion crypto_nodes/bitcoin/bitcoin-regtest.conf
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ rpcbind=0.0.0.0
rpcallowip=0.0.0.0/0
rpcport=18444
rpctimeout=30
# Bitcoin Core 31 replaced the ancestor/descendant SIZE limits with cluster
# limits (limitclustercount/limitclustersize) and no longer parses
# limitdescendantsize, so only the count limit is kept here.
limitdescendantcount=2500
limitdescendantsize=3000
port=18443
bind=0.0.0.0:18443
7 changes: 7 additions & 0 deletions crypto_nodes/dogecoin/dogecoin-regtest.conf
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,12 @@ limitdescendantsize=3000
# kick in. acceptnonstdtxn=1 (regtest default but explicit here) accepts
# the non-standard scripts the encoder emits.
minrelaytxfee=0.00000001
# A regtest node must MINE whatever it accepts. minrelaytxfee lowers only the
# RELAY floor; block assembly keeps its own blockmintxfee (default 0.00001/kB),
# so the two have to be lowered together or the node accepts a transaction into
# its mempool and never puts it in a block. Chunked bodies of ~11kB pay a flat
# 5460-sat fee (0.49 sat/vB) and sit below that assembly floor. dogecoind -help
# carries the option on v1.14.
blockmintxfee=0.00000001
limitfreerelay=99999
acceptnonstdtxn=1
8 changes: 8 additions & 0 deletions crypto_nodes/litecoin/litecoin-regtest.conf
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ rpcthreads=16
# ineffective on the deployed node - keep it flat.)
dustrelayfee=0
minrelaytxfee=0.00000001
# A regtest node must MINE whatever it accepts. minrelaytxfee above lowers only
# the RELAY floor; block assembly keeps its own blockmintxfee (default 0.00001/kB
# = 1 sat/vB), so the two have to be lowered together or the node accepts a
# transaction into its mempool and never puts it in a block. Chunked bodies of
# ~11kB pay a flat 5460-sat fee (0.49 sat/vB) and sit below that assembly floor,
# which stalls the chunk runner and keeps the miner producing blocks forever
# against a mempool that never empties.
blockmintxfee=0.00000001
limitfreerelay=99999
datacarriersize=1000
acceptnonstdtxn=1
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xchain-node",
"version": "0.14.0",
"version": "0.15.0",
"description": "xchain-node allows users to install, configure and run XChain platform nodes.",
"license": "AGPL-3.0-or-later",
"repository": {
Expand All @@ -12,7 +12,6 @@
"@dankest-llc/xchain-sdk": "^0.11.1",
"axios": "^1.18.1",
"blessed": "^0.1.81",
"chalk": "^5.6.0",
"commander": "^14.0.2",
"dotenv": "^16.4.5",
"enquirer": "^2.4.1",
Expand Down Expand Up @@ -62,7 +61,8 @@
"tmp": "^0.2.7",
"js-yaml": "^4.3.1",
"form-data": "^4.0.6",
"ip-address": "^10.3.1"
"ip-address": "^10.3.1",
"decimal.js": "10.4.3"
},
"bin": {
"xchain-node": "./src/index.js"
Expand Down
Loading
Loading