Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/attest-mirror-leg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ jobs:
node src/index.js install "$STACK_REF" all dogecoin regtest
# The standing stacks stay up: legs read the standing
# explorer (18080) and the standing LTC/DOGE indexers (at0-anti-wedge reads 3224).
docker pull -q mariadb:10.11 && docker tag mariadb:10.11 xchain-node-database
docker pull -q mariadb:11.8 && docker tag mariadb:11.8 xchain-node-database
docker images --format '{{.Repository}}' | grep '^xchain-node-' | sort
docker ps --format '{{.Names}} {{.Status}}'

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.25.0] - 2026-10-18

### Changed
- Raised the provisioned MariaDB image to the 11 line.
- Split the config path and naming helpers into their own modules.
- Clarified the train activation halt stall comments and added tests for its stall handling and for service restarts.

## [0.24.0] - 2026-10-17

### Added
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xchain-node",
"version": "0.24.0",
"version": "0.25.0",
"private": true,
"description": "xchain-node allows users to install, configure and run XChain platform nodes.",
"license": "AGPL-3.0-or-later",
Expand Down
42 changes: 21 additions & 21 deletions src/release-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"_comment": [
"Pinned component set for XChain Platform v0.24.0.",
"Pinned component set for XChain Platform v0.25.0.",
"Generated by bin/write-release-manifest.js from the ACTUAL tagged master merge commits.",
"xchain-node is the carrier and is not listed: checking out its tag IS this manifest.",
"A train tags only the repos it touches. This one moves xchain-vm, xchain-decoder, xchain-indexer, xchain-hub, xchain-sync, xchain-encoder, xchain-utxo-tracker, xchain-explorer and xchain-sdk.",
Expand All @@ -12,44 +12,44 @@
"repo's origin/master, and is a GPG-signed annotated tag verifying against the",
"platform release key (fingerprint 1DA7C4896F56EA22CF491EDF4361611A82F90B70)."
],
"platform_version": "0.24.0",
"released": "2026-10-17",
"platform_version": "0.25.0",
"released": "2026-10-19",
"components": {
"xchain-vm": {
"tag": "v0.24.0",
"commit": "67b41add8817db1523e0add492a872746952a874"
"tag": "v0.25.0",
"commit": "a415f304c02207763d4bd4565a4be9e749846331"
},
"xchain-decoder": {
"tag": "v0.24.0",
"commit": "800cb43bac94d5d8b7632181e935fb3668e0c12c"
"tag": "v0.25.0",
"commit": "509374321ab7076f22662f9768e01556d856d37d"
},
"xchain-indexer": {
"tag": "v0.24.0",
"commit": "0b399defc7f44e2794379e6ddf37b803e282847b"
"tag": "v0.25.0",
"commit": "64a281981203572c3e1f85f43150b9dcf224f2f5"
},
"xchain-hub": {
"tag": "v0.24.0",
"commit": "fea89a7ff6bfa94ad6dede2318696acc7a2f66e8"
"tag": "v0.25.0",
"commit": "86f959e6d4683b0c1f79cab97af72f5aab4ced10"
},
"xchain-sync": {
"tag": "v0.24.0",
"commit": "0ef3ba957b8f7418f0e54e80df831270c010d665"
"tag": "v0.25.0",
"commit": "9a52ba21dd9f0de92b7875ef1854e4c2ceeca8f8"
},
"xchain-encoder": {
"tag": "v0.24.0",
"commit": "c246a23ba17710337724fb45fee941cb8e58ca6d"
"tag": "v0.25.0",
"commit": "9253069e64aaf5b69f6e1c01fad743b6b46167c8"
},
"xchain-utxo-tracker": {
"tag": "v0.24.0",
"commit": "8e15897e25ab483acd0981dda193227b7a276402"
"tag": "v0.25.0",
"commit": "9c6941bdddaa53a71f4f2fe273753da4389e17e2"
},
"xchain-explorer": {
"tag": "v0.24.0",
"commit": "689949a8dcbdf79b037288f3efffecc0537857c0"
"tag": "v0.25.0",
"commit": "166f9f17edf2fd02881840a6fa4fe66f085c75ee"
},
"xchain-sdk": {
"tag": "v0.24.0",
"commit": "b521fa41587495529df50e5b7f1e3c6a911e4a15"
"tag": "v0.25.0",
"commit": "0e34a9da6ac6875bb2cc4184b1ee744d42dde927"
},
"xchain-e2e-test": {
"tag": "v0.21.0",
Expand Down
5 changes: 3 additions & 2 deletions src/services/bootstrap_archive_meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,9 @@ async function readBootstrapArchiveMeta(archivePath) {

const ARCHIVE_IDENTITY_FIELDS = ['module', 'coin', 'network']

// An identity value trimmed and lowercased, or null when it is absent or blank: the rule the tracker's
// restore_validation.js applies to the same bootstrap.json, so both readers judge one archive alike.
// An identity value trimmed and lowercased, or null when it is absent or blank: the same normalization the tracker's
// restore_validation.js applies to bootstrap.json. Only the normalization is shared: a target lacking a value for a
// declared field is a mismatch here but unchecked in the tracker, so the two readers can reach different verdicts.
function identityValue(value) {
if (typeof value !== 'string') return null
const v = value.trim().toLowerCase()
Expand Down
8 changes: 5 additions & 3 deletions src/services/bootstrap_health_gate/status_evaluation.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,11 @@ function stallReasons(payload) {
// The indexer's own single-field verdict on its block counter: 'none' |
// 'future_block_wait' | 'bridge_proof_wait' | 'price_landing_wait' | 'barrier_defer' |
// 'wedged', plus the published refinements 'upstream_halted' (only beside
// decoderReorgHalted: true, refused above) | 'rollcall_proof_wait' | 'rollcall_proof_stuck'.
// The set is defined in xchain-indexer src/XChainIndexer/stall_health.js (stallClassOf,
// publishedStallClass). Only 'wedged' is a refusal, and it needs its own
// decoderReorgHalted: true, refused above) | 'rollcall_proof_wait' | 'rollcall_proof_stuck' |
// 'train_activation_halt'. The set is defined in xchain-indexer
// src/XChainIndexer/stall_health.js (stallClassOf, publishedStallClass). Only 'wedged'
// is a refusal by itself; 'train_activation_halt' never is because the
// train_activation.status === 'halt' leg below already refuses it. 'wedged' needs its own
// leg: the indexer reports status "healthy"
// whenever its process is up and its DB circuit is closed, so a freshly-wedged
// indexer whose lag is still inside the ceiling passes every other check here.
Expand Down
133 changes: 18 additions & 115 deletions src/services/config_service.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,124 +46,27 @@ const database = require('./config_service/database')
const networks = require('./config_service/networks')
const filters = require('./config_service/filters')
const argumentsService = require('./config_service/arguments')
const paths = require('./config_service/paths')
const naming = require('./config_service/naming')
const { validatePort } = require('./config_service/validation')

function getModuleDir(module) {
return moduleDir + "/" + module
}

function getModuleTmpDir(module) {
return tmpDir + "/" + module
}

function getCryptoNodeDir(coin) {
if (!(coin in Coin)) {
coin = stringToCoin(coin)
}
return cryptoNodesDir + "/" + Coin[coin]
}

function removeModuleDir(module) {
fs.rmSync(getModuleDir(module), { recursive: true })
}

function removeModuleTmpDir(module) {
const dir = getModuleTmpDir(module)
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true })
}
}

function createModuleTmpDir(module) {
const dir = getModuleTmpDir(module)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
}

function moduleDirExists(module) {
return fs.existsSync(getModuleDir(module))
}

function checkIfModuleExists(module) {
const dir = getModuleDir(module)
return fs.existsSync(dir)
&& fs.existsSync(dir + "/Dockerfile")
&& fs.existsSync(dir + "/src")
&& fs.existsSync(dir + "/package.json")
}

function checkIfCryptoNodeSourceExists(coin) {
const dir = getCryptoNodeDir(coin)
return fs.existsSync(dir)
&& fs.existsSync(dir + "/Dockerfile")
&& fs.existsSync(dir + "/src")
}

function getDockerContainerImageNamePrefix(module, coin, network) {
if (module === DB_MODULE_NAME || module === HUB_MODULE_NAME || module === EXPLORER_MODULE_NAME || module === SYNC_MODULE_NAME) {
return NODE_PREFIX
}
return NODE_PREFIX + SEP + coin + SEP + network
}

function getDockerContainerImageName(module, coin, network) {
return getDockerContainerImageNamePrefix(module, coin, network) + SEP + module
}

// Single source of truth for the UTXO tracker's Docker volume name. Two
// NODE_PREFIX stacks of the same coin+network must not share one volume, so
// non-default prefixes get a prefixed volume name; the default prefix keeps
// the legacy unprefixed name (renaming it would orphan every existing
// deployment's tracker data, forcing a fleet-wide resync). Previously this
// rule lived only in ModuleService.buildAndUp; moduleOperations.resetModules
// and all three BootstrapService sites re-derived the unprefixed name
// independently, so a non-default NODE_PREFIX made reset/bootstrap/restore
// silently operate on the wrong stack's volume (uuid:7523dd94, uuid:a61fc673).
function getUtxoTrackerVolumeName(coin, network) {
const prefix = NODE_PREFIX === DEFAULT_NODE_PREFIX ? '' : `${NODE_PREFIX}${SEP}`
return `${prefix}${XChainService.XCHAIN_UTXO_TRACKER}${SEP}${coin}-${network}-data`
}

function getDockerNetwork(coin, network) {
return NODE_PREFIX
+ (coin !== "" ? SEP + coin : "")
+ (network !== "" ? SEP + network : "")
}

function getModuleDatabaseName(module, coin, network) {
// Defense in depth: an unknown coin (e.g. 'all' or a typo passed straight
// from a raw CLI arg) yields CoinTickerSymbol[coin] === undefined, which
// would otherwise produce a junk `XChain_undefined_*` name that reaches
// CREATE DATABASE on the live MariaDB while the command still exits 0.
// Fail loud so an unresolved coin never materializes a database.
if (coin !== "" && CoinTickerSymbol[coin] === undefined) {
throw new Error("Unknown coin '" + coin + "'; cannot derive a database name")
}
// Mirror the coin guard above: a typo'd network (e.g. from a raw CLI arg
// on the reset/bootstrap paths, which bypass filterCommandParameters)
// would otherwise derive a well-formed-but-bogus name that still reaches
// CREATE DATABASE while the command exits 0.
if (network !== "" && !Object.values(Network).includes(network)) {
throw new Error("Unknown network '" + network + "'; cannot derive a database name")
}
const moduleName = module.slice("xchain-".length)
return "XChain" + DB_SEP
+ scopedNodePrefixSegment(NODE_PREFIX, DEFAULT_NODE_PREFIX, DB_SEP)
+ CoinTickerSymbol[coin] + DB_SEP
+ network.charAt(0).toUpperCase() + network.slice(1) + DB_SEP
+ moduleName.charAt(0).toUpperCase() + moduleName.slice(1)
}
paths.configure({ fs, moduleDir, tmpDir, cryptoNodesDir, Coin, stringToCoin })
naming.configure({
NODE_PREFIX, DEFAULT_NODE_PREFIX, SEP, DB_SEP,
DB_MODULE_NAME, HUB_MODULE_NAME, EXPLORER_MODULE_NAME, SYNC_MODULE_NAME,
CoinTickerSymbol, Network, XChainService, scopedNodePrefixSegment
})

// Module DB account names carry the NODE_PREFIX like the database names, so two
// stacks sharing one MariaDB never contend for one account. Default prefix: unchanged.
function applyPrefixedModuleDbUsers(defaultValues, coin, network) {
const segment = scopedNodePrefixSegment(NODE_PREFIX, DEFAULT_NODE_PREFIX, DB_SEP)
if (segment === '') return
for (const [key, name] of [["DECODER_DB_USER", "decoder"], ["INDEXER_DB_USER", "indexer"]]) {
defaultValues[key] = "xchain" + DB_SEP + segment + name + DB_SEP + coin + DB_SEP + network
}
}
const {
getModuleDir, getModuleTmpDir, getCryptoNodeDir, removeModuleDir,
removeModuleTmpDir, createModuleTmpDir, moduleDirExists,
checkIfModuleExists, checkIfCryptoNodeSourceExists
} = paths
const {
getDockerContainerImageNamePrefix, getDockerContainerImageName,
getUtxoTrackerVolumeName, getDockerNetwork, getModuleDatabaseName,
applyPrefixedModuleDbUsers
} = naming

sidecars.configure({
crypto, fs, path, readline, configDir, preferredSecretEnvName,
Expand Down
92 changes: 92 additions & 0 deletions src/services/config_service/naming.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*********************************************************************
*
* Copyright © 2025–2026 Dankest, LLC
* Based on XChain Platform by Dankest, LLC – https://dankest.llc
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* This file is part of XChain Platform. Licensed under the GNU Affero
* General Public License v3.0 or later; see LICENSE.md. A commercial
* license (without AGPL source-disclosure terms) is available -
* contact legal@dankest.llc.
*
**********************************************************************
* Config Service Naming Helpers
********************************************************************/

'use strict'

function createContainerNames(dependencies) {
const {
NODE_PREFIX, DEFAULT_NODE_PREFIX, SEP,
DB_MODULE_NAME, HUB_MODULE_NAME, EXPLORER_MODULE_NAME, SYNC_MODULE_NAME,
XChainService
} = dependencies

function getDockerContainerImageNamePrefix(module, coin, network) {
if ([DB_MODULE_NAME, HUB_MODULE_NAME, EXPLORER_MODULE_NAME, SYNC_MODULE_NAME].includes(module)) {
return NODE_PREFIX
}
return NODE_PREFIX + SEP + coin + SEP + network
}

function getDockerContainerImageName(module, coin, network) {
return getDockerContainerImageNamePrefix(module, coin, network) + SEP + module
}

function getUtxoTrackerVolumeName(coin, network) {
const prefix = NODE_PREFIX === DEFAULT_NODE_PREFIX ? '' : `${NODE_PREFIX}${SEP}`
return `${prefix}${XChainService.XCHAIN_UTXO_TRACKER}${SEP}${coin}-${network}-data`
}

function getDockerNetwork(coin, network) {
return NODE_PREFIX
+ (coin !== "" ? SEP + coin : "")
+ (network !== "" ? SEP + network : "")
}

return {
getDockerContainerImageNamePrefix,
getDockerContainerImageName,
getUtxoTrackerVolumeName,
getDockerNetwork
}
}

function createDatabaseNames(dependencies) {
const {
NODE_PREFIX, DEFAULT_NODE_PREFIX, DB_SEP,
CoinTickerSymbol, Network, scopedNodePrefixSegment
} = dependencies

const getModuleDatabaseName = function (module, coin, network) {
if (coin !== "" && CoinTickerSymbol[coin] === undefined) {
throw new Error("Unknown coin '" + coin + "'; cannot derive a database name")
}
if (network !== "" && !Object.values(Network).includes(network)) {
throw new Error("Unknown network '" + network + "'; cannot derive a database name")
}
const moduleName = module.slice("xchain-".length)
return "XChain" + DB_SEP
+ scopedNodePrefixSegment(NODE_PREFIX, DEFAULT_NODE_PREFIX, DB_SEP)
+ CoinTickerSymbol[coin] + DB_SEP
+ network.charAt(0).toUpperCase() + network.slice(1) + DB_SEP
+ moduleName.charAt(0).toUpperCase() + moduleName.slice(1)
}

function applyPrefixedModuleDbUsers(defaultValues, coin, network) {
const segment = scopedNodePrefixSegment(NODE_PREFIX, DEFAULT_NODE_PREFIX, DB_SEP)
if (segment === '') return
for (const [key, name] of [["DECODER_DB_USER", "decoder"], ["INDEXER_DB_USER", "indexer"]]) {
defaultValues[key] = "xchain" + DB_SEP + segment + name + DB_SEP + coin + DB_SEP + network
}
}

return { getModuleDatabaseName, applyPrefixedModuleDbUsers }
}

function configure(dependencies) {
Object.assign(module.exports, createContainerNames(dependencies), createDatabaseNames(dependencies))
}

module.exports = { configure }
Loading
Loading