diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fa49a3..f7d9615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.18.0] - 2026-09-11 + +### Fixed +- `GET /status` publishes a `lag` field, so the node's bootstrap health gate no longer refuses the payload when it falls back to this route. +- The Docker image no longer tries to bake a `.env` file, so the build succeeds on the legacy builder and configuration reaches the container as environment only. + ## [0.17.0] - 2026-09-10 ### Added diff --git a/Dockerfile b/Dockerfile index dc0baae..f014665 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,9 @@ COPY ./src /XChainDecoder/src # is also applied in-process at require time (src/applyBufferutilsPatch.js), so # non-Docker runs and node_modules refreshes are covered even without this COPY. COPY ./src/bufferutils.js /XChainDecoder/node_modules/bitcoinjs-lib/src/bufferutils.js -COPY ./.en[v] /XChainDecoder/.env +# No .env is baked in: configuration reaches the container as environment +# (xchain-node at `docker run`, docker-compose.yml via env_file). An optional +# `COPY ./.en[v]` glob here builds only under BuildKit. # Exec-form node, not `npm run api` (which is this exact command). npm builds an # npm -> sh -c -> node tree and no wrapper forwards signals, so `docker stop` diff --git a/README.md b/README.md index aa586f0..5f112fb 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ # XChain Platform Decoder

- Version - Tests + Version + Tests Node License

@@ -113,7 +113,7 @@ neither source sets one, so these defaults hold on an unconfigured box: | `npm run migrate` | Apply pending database migrations (auto + manual; `--file ` scopes to specific migration(s)) | | `npm run ci` | The full no-external-services gate: unit, security, smoke, regression, chaos, and a 100-iteration fuzz pass (about a minute) | | `npm run test:smoke` | Smoke tests (58 tests, no external services) | -| `npm run test:unit` | Unit tests (1,593 tests, no external services) | +| `npm run test:unit` | Unit tests (1,596 tests, no external services) | | `npm run test:security` | Security tests (83 tests, no external services) | | `npm run test:integration` | Integration tests (30 tests; brings up its own throwaway regtest node and MariaDB, requires Docker) | | `npm run test:e2e` | End-to-end tests (72 tests; brings up its own throwaway regtest node and MariaDB on separate ports, requires Docker) | diff --git a/docker-compose.yml b/docker-compose.yml index 0ce0e24..e83bd00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,9 @@ version: '3' services: xchain_decoder: build: . + # The image bakes no .env (Dockerfile); the README's .env is handed to the + # container here instead. + env_file: .env volumes: - xchain_decoder_data:/data/ networks: diff --git a/package-lock.json b/package-lock.json index 80f213e..1124c84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xchain-decoder", - "version": "0.17.0", + "version": "0.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "xchain-decoder", - "version": "0.17.0", + "version": "0.18.0", "license": "AGPL-3.0-or-later", "dependencies": { "axios": "^1.18.1", diff --git a/package.json b/package.json index d669edc..c3f9791 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xchain-decoder", "description": "xchain-decoder decodes XChain platform transactions from a given blockchain and populates a database with the decoded data.", - "version": "0.17.0", + "version": "0.18.0", "license": "AGPL-3.0-or-later", "repository": { "type": "git", diff --git a/src/api.js b/src/api.js index 890a4cc..e82ce45 100644 --- a/src/api.js +++ b/src/api.js @@ -536,11 +536,18 @@ async function startApi(){ if (dbOk && typeof decoder.checkReorgHalt === 'function'){ try { reorgHalt = await decoder.checkReorgHalt() } catch (e) { noteProbeFailure('reorg_halt', '/status', e) } } + // RULED 2026-09-01: xchain-node's BootstrapHealthGate refuses any + // /status payload with no lag key (lagKeys: lag_blocks, blockLag, lag) once it + // falls back to this route. getSyncStatus() already reports the same + // node-height-minus-processed-height gap the JSON-RPC health method and /live + // publish, null before the first processed block rather than a false zero. + const syncStatus = decoder.getSyncStatus() const healthy = decoderRunning && dbOk res.status(healthy ? 200 : 503).json({ status: healthy ? 'healthy' : 'unhealthy', db: dbOk, running: decoderRunning, + lag: syncStatus.lag, reorg_halted: reorgHalt.halted, reorg_halt_reason: reorgHalt.reason, reorg_halted_at: reorgHalt.at, diff --git a/src/coins/BTC.js b/src/coins/BTC.js index c846405..caf3614 100644 --- a/src/coins/BTC.js +++ b/src/coins/BTC.js @@ -347,9 +347,14 @@ module.exports = { // capability SLASH block. Mainnet-inert until the EQUIV_HEADER flag-day. CONFIG_SLASH: { BOUNTY_BPS: 500, BOUNTY_FLOOR: '50.00000000', BOUNTY_CAP: '1000.00000000' }, - // Full-node possession-proof / verified-validator tier (NODEPROOF.md). The - // regtest-only env/sidecar overrides are applied by index.js for regtest ONLY; - // mainnet/testnet keep these frozen defaults with no env surface. + // Full-node possession-proof / verified-validator tier; see + // xchain-documentation/protocol/actions/nodeproof.md for the challenge/verdict + // action and xchain-documentation/getting-started/running-a-validator.md for the + // tier walkthrough. REWARD_SHARE 0 and an empty GENESIS_VERIFIERS below are the + // deliberate pre-activation state: the tier ships inert until a later flag-day + // arms it, not a bug. The regtest-only env/sidecar overrides are applied by + // index.js for regtest ONLY; mainnet/testnet keep these frozen defaults with no + // env surface. FULLNODE: { CHALLENGE_INTERVAL_BLOCKS: 144, CONFIRM_DEPTH: 100, diff --git a/src/coins/consensus_pin.js b/src/coins/consensus_pin.js index 53a2113..1adccc8 100644 --- a/src/coins/consensus_pin.js +++ b/src/coins/consensus_pin.js @@ -68,13 +68,15 @@ module.exports = { // untouched and were re-verified as unchanged by this edit. // REGENERATED 2026-09-01: GAS_SCHEDULE gains SWEEP_BASE, // SWEEP_PER_ITEM, CALLBACK_BASE and CALLBACK_PER_RECIPIENT, the unified prices - // SWEEP and CALLBACK move onto at the UNIFIED_FEES_SWEEP_CALLBACK flag day (both - // networks UNARMED; regtest genesis-active). GAS_SCHEDULE is hashed whole by - // consensusSubset(), so ADDING a key moves every hash even while the flag that - // reads it is unarmed, and the same one-wave rollout rule as every regeneration - // above applies in full: every service bundling these must ship the new values - // together, and a straggler fail-closes on verifyConsensusPin() at boot rather - // than forking. Mainnet stays null (Phase 6 arms it). + // SWEEP and CALLBACK move onto at the UNIFIED_FEES_SWEEP_CALLBACK flag day + // (mainnet armed at genesis since the genesis arm; testnet armed + // 2026-10-01T00:00:00Z; regtest genesis-active). GAS_SCHEDULE is hashed whole by + // consensusSubset(), so ADDING a key moves every hash regardless of which + // networks have the flag armed, and the same one-wave rollout rule as every + // regeneration above applies in full: every service bundling these must ship + // the new values together, and a straggler fail-closes on verifyConsensusPin() + // at boot rather than forking. CONSENSUS_CONFIG_PIN.mainnet above stays null + // regardless (Phase 6 arms that separate pin). testnet: { BTC: 'd3c66a4fb288b2666a2a4fad85200bbeac162bb36fed8a3eddcfc7b2d4d48070', LTC: 'ae94a951a838e64f9c36e503b978d9b9ad5ea74f7b443465baaabca8f675ea0d', diff --git a/test/unit/statusLagField.test.js b/test/unit/statusLagField.test.js new file mode 100644 index 0000000..6443e8c --- /dev/null +++ b/test/unit/statusLagField.test.js @@ -0,0 +1,65 @@ +/********************************************************************* + * + * 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. + * + ********************************************************************** + * GET /status carried no lag field, so xchain-node's + * BootstrapHealthGate refused every payload it fell back to reading from this + * decoder (lagKeys checked there: lag_blocks, blockLag, lag - see + * BootstrapHealthGate.js). RULED 2026-09-01: publish a lag field on /status, + * Proposal B (a bespoke computation) not adopted; reuse the same + * decoder.getSyncStatus().lag the JSON-RPC health method and /live already + * publish, so all three surfaces report the identical gap. + * + * api.js registers GET /status inside startApi(), which opens a real listening + * socket and is not reachable from a unit test (see decoderHaltDiagnostics.test.js's + * "api.js GET /status halt surface (source pin)" block, which pins reorg_halted + * the same way). This file pins the lag field at the source for the same reason. + */ + +'use strict' + +const assert = require('assert') +const fs = require('fs') +const path = require('path') + +const src = fs.readFileSync(path.join(__dirname, '../../src/api.js'), 'utf8') + +function statusRouteBody() { + const at = src.indexOf("app.get('/status'") + assert.ok(at > -1, 'GET /status route missing from api.js') + return src.slice(at, at + 3000) +} + +describe('api.js GET /status publishes a lag field', function () { + it('reads sync status via decoder.getSyncStatus() before building the response', function () { + const body = statusRouteBody() + assert.ok(/const\s+syncStatus\s*=\s*decoder\.getSyncStatus\(\)/.test(body), + 'GET /status must call decoder.getSyncStatus() to know its own lag') + }) + + it('publishes lag on the JSON body, matching one of the keys BootstrapHealthGate checks', function () { + const body = statusRouteBody() + // BootstrapHealthGate.js lagKeys = ['lag_blocks', 'blockLag', 'lag']; any one + // satisfies the gate, and this route uses the same 'lag' name getSyncStatus() + // and the JSON-RPC health method already publish. + assert.ok(/lag:\s*syncStatus\.lag/.test(body), + 'GET /status still has no lag key, so BootstrapHealthGate refuses its fallback payload') + }) + + it('computes syncStatus before the res.json() call, not after', function () { + const body = statusRouteBody() + const syncAt = body.search(/const\s+syncStatus\s*=\s*decoder\.getSyncStatus\(\)/) + const statusAt = body.search(/status:\s*healthy\s*\?\s*'healthy'/) + assert.ok(syncAt > -1 && statusAt > -1 && syncAt < statusAt, + 'syncStatus must be computed before it is spread into the response body') + }) +})