Skip to content

Commit a7fe2e1

Browse files
authored
Merge pull request #25 from XChain-Platform/release/v0.23.0
Release v0.23.0
2 parents d204894 + abf8167 commit a7fe2e1

151 files changed

Lines changed: 7204 additions & 1598 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.23.0] - 2026-10-11
11+
12+
### Added
13+
- Added dispenser address collapse, purge grace and expiration extension undo, each behind an activation.
14+
- Enforced newest-first protocol time windows and the protocol time history bound.
15+
16+
### Changed
17+
- Required unsigned BIGINT transaction id columns and asserted them at startup.
18+
- Pinned the BIGINT migration checksum rebaseline so the widened ids migration heals in place.
19+
20+
### Fixed
21+
- Preserved stale mempool read metadata and kept a known reorg halt on a database ping failure.
22+
- Validated block hashes from the node and carried JSON-RPC error codes on HTTP 200 errors.
23+
1024
## [0.21.0] - 2026-09-29
1125

1226
### Added

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ CI runs the no-external-services gate, the docker tiers, the cross-repo drift gu
125125
2. Update `CHANGELOG.md` with a terse entry for your change.
126126
3. Make sure `git status` is clean apart from intended changes (no `node_modules/`, no editor leftovers, no `.env`).
127127
4. Open the PR with a clear title and a description of what changed and why.
128+
5. Agree to the Contributor License Agreement when the CLA Assistant bot links it on your first PR. Its `license/cla` check must pass before merge, so a PR from a contributor who has not signed cannot be merged. One signature covers every XChain Platform repository.
128129

129130
For non-security bugs, open an issue at <https://github.com/XChain-Platform/xchain-decoder/issues/new>. For security bugs, see [`SECURITY.md`](./SECURITY.md).
130131

‎Dockerfile‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
FROM node:25.9.0
2-
3-
RUN mkdir /XChainDecoder/
1+
FROM node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 AS dependencies
2+
WORKDIR /XChainDecoder
43
COPY ./package.json /XChainDecoder/package.json
54
COPY ./package-lock.json /XChainDecoder/package-lock.json
6-
WORKDIR /XChainDecoder
75
RUN npm ci --omit=dev
86

7+
FROM node:25.9.0@sha256:78839ac448c23517f8eab2e8f7943d9b4f73979eb7f8bed2c73dbf72ff869e7b AS decoder
8+
WORKDIR /XChainDecoder
9+
COPY ./package.json /XChainDecoder/package.json
10+
COPY ./package-lock.json /XChainDecoder/package-lock.json
11+
COPY --from=dependencies /XChainDecoder/node_modules /XChainDecoder/node_modules
912
COPY ./src /XChainDecoder/src
1013
# Patch bitcoinjs-lib's bufferutils with a BigInt-aware 64-bit reader. The stock
1114
# readUInt64 throws "RangeError: value out of range" for output values above
@@ -24,4 +27,4 @@ COPY ./src/chain/bufferutils.js /XChainDecoder/node_modules/bitcoinjs-lib/src/bu
2427
# xchain-encoder/Dockerfile). This image registers real drain work on SIGTERM
2528
# (src/api.js: flip decoderRunning, then decoder.stop()), which only runs when
2629
# node is PID 1 and receives the signal itself.
27-
CMD ["node", "./src/api.js"]
30+
CMD ["node", "./src/api.js"]

‎NOTICE.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ For commercial licensing, contact **legal@dankest.llc**.
2121

2222
## Attribution
2323

24-
Any redistribution or modification of this software must retain the following
25-
attribution, per the AGPL and the project's trademark policy:
24+
As an additional term under section 7(b) of the GNU Affero General Public
25+
License v3.0, any redistribution or modification of this software must preserve
26+
the following author attribution in its source and in the Appropriate Legal
27+
Notices displayed by any work containing it:
2628

2729
> Based on XChain Platform by Dankest, LLC - https://dankest.llc
2830

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Transaction extraction service for the XChain Platform. Polls the coin nodes of
2929
- **ACTION validation**: 34-name allowlist plus 5 short-form aliases (e.g. TRANSFER -> SEND) expanded before database writes
3030
- **Parse-failure quarantine**: tx-level decode failures retry up to 3 times, then are quarantined as PARSE_ERROR events rather than halting the block
3131
- **Source pubkey capture**: records the source address pubkey per transaction in a dedicated table for downstream use by the indexer
32-
- **Native-coin fee tracking**: when FEE_DESTINATION is set, outputs paying that address are persisted to transaction_outputs for indexer fee validation
32+
- **Native-coin fee tracking**: outputs paying the coin registry's FEE_DESTINATION are persisted to transaction_outputs for indexer fee validation; on regtest only, `XCHAIN_FEE_DESTINATION_<COIN>_REGTEST` redirects it for the decoder and indexer alike, and a bare `FEE_DESTINATION` env is ignored with a warning
3333
- **Graceful shutdown**: SIGTERM/SIGINT handlers complete in-flight work
3434
- **Node RPC failover**: rotates through `NODE_URL_FALLBACK` endpoints after `NODE_FAILOVER_THRESHOLD` consecutive connection failures, round-robin, so a recovered primary is retried again if the fallback also dies
3535
- **1300+ tests**: unit, integration, e2e, security, fuzz, chaos, mutation, regression, benchmarks, smoke

‎bin/ci-full.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ ci_tier_deferred() {
8989
# >>> ci-tier timer (generated block; re-run the tier wirer to update) >>>
9090
run_tier() {
9191
ci_tier_deferred "$1" && return 0 # ci-tier guard (generated)
92+
if [ "${CI_TIER:-full}" = "fast" ] && [ -n "${FAILED:-}" ]; then echo; echo "ci:full ===== $1 NOT RUN (a push stops at its first red tier, its verdict already red; the full sweep runs it) ====="; return 0; fi # ci-tier stop (generated)
9293
local name="$1"; shift
9394
local __ci_tier_t0=$SECONDS
9495
echo; echo "ci:full ===== $name ====="

‎bin/lib/migration_residue_pin.js‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env node
2+
/*********************************************************************
3+
*
4+
* Copyright © 2025–2026 Dankest, LLC
5+
* Based on XChain Platform by Dankest, LLC – https://dankest.llc
6+
*
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*
9+
* This file is part of XChain Platform. Licensed under the GNU Affero
10+
* General Public License v3.0 or later; see LICENSE.md. A commercial
11+
* license (without AGPL source-disclosure terms) is available -
12+
* contact legal@dankest.llc.
13+
*
14+
**********************************************************************
15+
*
16+
* The migration immutability pin: two sha256 values per committed migration in
17+
* test/fixtures/migration-executable-residue.json, so an in-place edit to an
18+
* applied file fails in CI instead of surfacing on a fleet as `content CHANGED`.
19+
* Ported from the indexer's pin of the same name; the decoder's runner hashes
20+
* migrations the same way (whole-file sha256 in schema_migrations).
21+
*
22+
* residue the file with every `--` line and blank line dropped (the basis the
23+
* MIGRATION_CHECKSUM_REBASELINES entries are verified on). A change
24+
* here is an executable edit and always fails.
25+
* sha256 the whole file, exactly what schema_migrations records. A change here
26+
* passes only when a reviewed rebaseline heals the pinned hash to the
27+
* current one, since that is the only case a deployed DB survives.
28+
*
29+
* A file the pin does not hold yet passes when its name sorts after the newest pinned
30+
* migration, so a new migration needs no fixture edit; `--add` pins it. A file that
31+
* sorts before the newest pinned migration fails because it was missed or backdated.
32+
* An existing entry moves only through `--accept <file>`, which is legitimate solely
33+
* for a file no database has applied.
34+
*
35+
* USAGE (from the repo root)
36+
* node bin/lib/migration_residue_pin.js check; exit 1 on a violation
37+
* node bin/lib/migration_residue_pin.js --add pin every unpinned file
38+
* node bin/lib/migration_residue_pin.js --accept <file> [--accept <file>]
39+
*
40+
********************************************************************/
41+
42+
'use strict';
43+
44+
const crypto = require('crypto');
45+
const fs = require('fs');
46+
const path = require('path');
47+
48+
// Loads the rebaseline table only; requiring src/db opens no connection and reads no env.
49+
const { MIGRATION_CHECKSUM_REBASELINES } = require('../../src/db');
50+
51+
const ROOT = path.join(__dirname, '..', '..');
52+
const MIG_DIR = path.join(ROOT, 'src', 'sql', 'migrations');
53+
const FIXTURE = path.join(ROOT, 'test', 'fixtures', 'migration-executable-residue.json');
54+
55+
const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex');
56+
57+
function executableResidue(raw){
58+
return String(raw).split('\n').filter((l) => !/^\s*--/.test(l) && l.trim() !== '').join('\n');
59+
}
60+
61+
function pinOf(raw){
62+
return { residue: sha256(executableResidue(raw)), sha256: sha256(raw) };
63+
}
64+
65+
// Every violation of the pin over `files` (name -> raw text), as { file, kind, message }.
66+
function findViolations({ files, fixture, rebaselines }){
67+
const out = [];
68+
const newest = Object.keys(fixture).sort().pop() || '';
69+
for(const file of Object.keys(files).filter((f) => !Object.hasOwn(fixture, f) && f < newest).sort()){
70+
out.push({ file, kind: 'unpinned', message: file + ' sorts before the newest pinned migration ' + newest +
71+
' but is not pinned: run node bin/lib/migration_residue_pin.js --add and commit the fixture.' });
72+
}
73+
for(const file of Object.keys(fixture).sort()){
74+
const pinned = fixture[file];
75+
// Deleting or renaming an applied migration is forbidden: the decoder has no ledger rename map.
76+
if(!Object.hasOwn(files, file)){
77+
out.push({ file, kind: 'missing', message: file + ' is pinned but gone: deleting or renaming an applied ' +
78+
'migration breaks every ledger that recorded it.' });
79+
continue;
80+
}
81+
const now = pinOf(files[file]);
82+
// A changed statement is never healable; it needs a new dated migration.
83+
if(now.residue !== pinned.residue){
84+
out.push({ file, kind: 'residue', message: file + ': the executable statements of a pinned migration ' +
85+
'changed. Revert and write a new dated migration; MIGRATION_CHECKSUM_REBASELINES heals the ledger only.' });
86+
continue;
87+
}
88+
// A comment-only change passes only under a rebaseline healing exactly the pinned hash to the current one.
89+
const heal = rebaselines && rebaselines[file];
90+
const healed = heal && heal.to === now.sha256 && [].concat(heal.from).includes(pinned.sha256);
91+
if(now.sha256 !== pinned.sha256 && !healed){
92+
out.push({ file, kind: 'content', message: file + ': content changed with no rebaseline from ' +
93+
pinned.sha256.slice(0, 12) + ' to ' + now.sha256.slice(0, 12) + ', so every DB that applied it logs ' +
94+
'`content CHANGED` and migrate.js fails closed there. Revert, or add a reviewed comment-only rebaseline.' });
95+
}
96+
}
97+
return out;
98+
}
99+
100+
// Every committed migration (`.sql` only; the README beside them is not one), name -> raw text.
101+
function readTree(){
102+
const files = {};
103+
for(const f of fs.readdirSync(MIG_DIR).filter((n) => n.endsWith('.sql'))) files[f] = fs.readFileSync(path.join(MIG_DIR, f), 'utf8');
104+
return files;
105+
}
106+
107+
function writeFixture(fixture){
108+
const sorted = {};
109+
for(const k of Object.keys(fixture).sort()) sorted[k] = fixture[k];
110+
fs.writeFileSync(FIXTURE, JSON.stringify(sorted, null, 2) + '\n');
111+
}
112+
113+
function main(argv){
114+
const files = readTree();
115+
const fixture = JSON.parse(fs.readFileSync(FIXTURE, 'utf8'));
116+
const accept = argv.flatMap((a, i) => (a === '--accept' ? [argv[i + 1]] : []));
117+
if(argv.includes('--add') || accept.length){
118+
const added = Object.keys(files).filter((f) => !fixture[f]);
119+
if(argv.includes('--add')) for(const f of added) fixture[f] = pinOf(files[f]);
120+
for(const f of accept){
121+
if(!files[f]) throw new Error('--accept ' + f + ': no such migration');
122+
console.warn('ACCEPTED a new pin for ' + f + ': legitimate only if no database has applied it.');
123+
fixture[f] = pinOf(files[f]);
124+
}
125+
writeFixture(fixture);
126+
console.log('pinned ' + (argv.includes('--add') ? added.length : 0) + ' new, accepted ' + accept.length +
127+
'. Next: node bin/lib/migration_residue_pin.js');
128+
return 0;
129+
}
130+
const bad = findViolations({ files, fixture, rebaselines: MIGRATION_CHECKSUM_REBASELINES });
131+
for(const v of bad) console.log(v.kind + ': ' + v.message);
132+
const unpinned = Object.keys(files).filter((f) => !fixture[f]).length;
133+
console.log(bad.length + ' violation(s), ' + unpinned + ' unpinned file(s).' +
134+
(unpinned ? ' Next: node bin/lib/migration_residue_pin.js --add' : ''));
135+
return bad.length ? 1 : 0;
136+
}
137+
138+
module.exports = { executableResidue, pinOf, findViolations, readTree, FIXTURE };
139+
140+
if(require.main === module) process.exitCode = main(process.argv.slice(2));

0 commit comments

Comments
 (0)