Skip to content

Release v0.19.0 - xchain-decoder - #20

Merged
jdogresorg merged 156 commits into
masterfrom
release/v0.19.0
Sep 16, 2026
Merged

jdogresorg merged 156 commits into
masterfrom
release/v0.19.0

Conversation

@jdogresorg

Copy link
Copy Markdown
Contributor

Bumps the package and lockfile to 0.19.0 and folds the Unreleased block into a dated 0.19.0 entry. The train carries the XBRIDGE cross-chain bridge, armed on testnet by the ladder and on mainnet at the sentinel, with the token bridge and policy code dark behind their sentinels, the rollcall and tracker fixes, the code-structure passes with the consensus identity pinned, and the activation registry.

Platform notes ride xchain-node's Release.

Re-vendors the shared coin bundles with the bridge escrow addresses and the XBRIDGE gas entry.
Refreshes the lockfile so every package the security audit flags resolves to its patched release inside the existing version ranges. No source change.
Mixed-carrier envelope recognition is now height-gated by an activation map, clearing a reorg halt refuses when a newer halt has landed, dispenser cancel grace anchors on the expiry mark's header time, and the purge-safety probe also covers batch sub-commands. A first-seen source pubkey is carried through transaction insertion, the shutdown timer is injectable, and the fuzz invariants read the dispenser field offsets from the decoder itself instead of restating them.
Two comments pointed at the indexer's dispenser divergence metrics module by its old file name. They now name the snake_case file the indexer renamed it to.
A restructure that renames test files has to prove it changed nothing about
what runs, and a passing count proves nothing, so the pin is the set of full
test titles per file for the test:unit tier: 84 files, 1622 titles. The
identity pin is the other half, sha256 of the five vendored coin files and
the two conformance fixtures whose canonicals live in other repos, so drift
in a copy this repo may not edit fails here instead of in a consumer's CI.

The reachability and reference-map tools come across from the indexer, with
the repo name they hardcoded now taken from the checkout directory and the
dynamic-require table emptied: every require under this repo's src/ is a
literal. Suite wall time recorded at 21.4 s, which keeps the tier per-step.
One source file is reachable from no require in the tree, src/bufferutils.js,
and a platform-wide text sweep across every checkout proves it live: the
Dockerfile copies it over bitcoinjs-lib at image build time to replace a
64-bit reader that throws above 2^53-1, which a single Dogecoin output
exceeds. Deleting it would have wedged block decode in every container with
nothing failing here, so the sweep output is committed instead of a deletion.
The style rules had a pre-push gate but no editor-level check, so a writer
only learned about a naming or require-placement violation at push time. The
preset is vendored by copy rather than imported, because a public clone of
this repo has no platform tree beside it to import from.

Two local additions over the master copy: the two vendored trees are ignored,
since they are refreshed from another repo and an edit here would be drift;
and src/clear-reorg-halt.js joins the entry-point list beside the api and the
migrator, because its output is its product. It reports 535 findings under
src/ today; a later cleanup drains those rather than suppressing them.
Six environment names were read at seven sites across the connector and the
database layer, so nothing could say what this service is configured by
without reading every file, and two sites reading one name could disagree on
its fallback without anyone noticing.

The home exports accessors rather than a snapshot, which is the whole of the
design: several of these knobs are documented and tested as retunable without
rebuilding the object that reads them, and a require-time copy looks identical
while silently freezing that. Five reads stay where they are, four because
they coerce at the read site and the coerced type is a decision rather than a
move, one because it is a bare process.env reference with no name to move.
… home

Twelve source files carried three naming schemes between them, and seven
protocol-rule modules sat loose at the top of src/ beside the infrastructure,
which is the one thing the top level is for. The rule modules move into the
existing src/protocol/ home; the rest are renames.

Forced by the moves, in this commit because they break otherwise: require
and path.join references across src and test, a regex in the chain-identity
gate test that matched the old require specifier through escaped characters,
two path.join segment lists that name a source file by parts, and four
comments naming a moved path. Two body requires hoisted in src; the codemod
also hoisted 56 in test files, and those were put back: the rule is scoped to
src, and a require inside an it() block is frequently the assertion itself.

One declared test-title change, and no other: a title naming chainIdentity.js
now names chain_identity.js. Same 84 files, same 1622 titles otherwise.

Not moved, each for a reason: bufferutils.js and its in-process twin, which
the image build copies over a dependency by literal path; the decoder metrics,
because the observability directory is vendored and grading stops at its
prefix; the connector and the network table, which three sibling repos hold at
the same depth and one cross-repo test resolves from a single path template;
and the reorg-halt clear, which xchain-node runs by its literal path inside
this repo's container, so it keeps its hyphenated name at src/ rather than
following the snake_case sweep.
A leading underscore is neither privacy nor a plain name, and every one of
these is called from outside the class it lives in, including from the startup
assertion registry, so the prefix said the opposite of what was true.

Two populations the rename tool leaves alone on purpose, both rewritten here
because they are reflective calls rather than prose: the startup-assertion
registry maps each migration file to its assertion method BY NAME as a string
and invokes it through that string, and the suite stubs five of these methods
by name through sinon, which refuses to stub a property that no longer exists.
Fifty-one such occurrences across seven files.

Forty-two test titles change, all of them the same underscore dropped from a
method name inside the title. No suite is added or removed: same 84 files.
…suite

The tree carried four schemes at once: camelCase, kebab-case, a dotted kind
infix, and a kind suffix that is not .test.js at all. Every suite is now
<unit>.test.js in snake_case, a kind infix is dropped where the directory
already says it (security, boundary, regression, unit) and becomes a word
where it does not (extra, queries), and the fuzz harnesses keep .fuzz.js
because the fuzz glob keys on it.

The 52 files that are not suites move into a support/ directory inside their
own kind: each kind's setup and helpers, the fuzz mutators, the benchmark
harness with its mocks and scenarios. The batch-limit generator was never a
test at all and moves to bin/ beside the other operator tools.

Rewritten in this commit because a rename without them collects zero files
and reads green forever: five kind globs, every --require path across 20 npm
scripts, the tier manifest the enforcement suite reads, the two docker
wrappers, the mutation configs, the workflow, and the benchmark scenario names
in both the harness list and the stored baseline. Also three sibling pins that
now name the tracker's renamed files: without them the AuxPoW parity and
safe-depth suites do not fail, they SKIP, which is nine assertions going quiet.

Same 84 unit files through the rename map, same titles, no suite added or lost.
The 150 raw console calls outside the three process entry points now go
through the shared logger. A call with one argument passes it straight
through; a call with two or more is folded through node's formatter under a
name that cannot collide with this repo's own util module, which is what the
vendored console patch already does at runtime, so a folded line renders
exactly what it rendered before.

The two files that exported a class and then attached named helpers to it
line by line now attach them in one Object.assign. module.exports IS the class
in both, so this is the same assignment written once, and no call site in this
repo or any consumer changes.

Two requires move out of function bodies: the coin registry, which the
startup pin reads, and dotenv, whose CALL stays inside main because the
environment must be read at run time. One source-text assertion and one
halt-diagnostics assertion follow: the halt line now reaches the logger the
suite already installs rather than console.error, so the test reads the sink.
Driven: with the message text changed, that assertion goes red, and back to
green when it is restored.
Two sweeps cut comment lines out of this repo, 21 lines across 2 files in June
and 630 across 101 files in August, and nothing had been restored. The
documentation layer is a product requirement here: the owner wrote these
comments so a reader who does not program can follow a file top to bottom, and
a suite cannot notice their absence because a comment cannot fail a test.

260 comment runs come back, each anchored to the line of code it was written
about rather than to a line number, so a run lands above its own statement
wherever the restructure moved it. Not a revert: the same sweep also rewrote
surviving lines while scrubbing internal references, and reverting a file
would duplicate the rewritten text and put back a reference the push gate
refuses.

Comment-only, and proven so rather than asserted: with comments and blank
lines stripped, all 34 files are byte-identical to their committed versions.
…ool out

Top-level src/ is for cross-cutting infrastructure, and three files were not
that. The three modules that talk to or parse a coin node (the RPC connector,
the network-parameter table and the block decoder) become src/chain/, which is
the feature they share.

The reorg-halt clear stays at src/clear-reorg-halt.js despite being an
operator command rather than service code: xchain-node execs it inside the
decoder container by that literal path, and the Dockerfile copies only
./src, so joining the other one-off tools in bin/ would leave the container
image without it and break every `xchain-node clear-reorg-halt` call.

Two sibling pins move with them, and both were failing OPEN rather than loud:
the encoder parity suites resolve that repo's validator, which moved into a
directory of its own there, so six assertions had gone quiet as skips.

Top-level source files fall from 14 to 11. What stays is named with a reason:
the api, the main class, the database and the config home are the declared
layout; util.js is the style doc's own top-level example; the migrator is an
entry point the shared lint preset pins by that path; the reorg-halt clear is
named by path from outside this repo; the metrics module cannot join the
vendored observability tree, which is excluded from grading by prefix and
would hide it; and the bufferutils pair is copied over a dependency by a
literal path in the image build, which this change does not touch.
…ss exemplary

The removal sweep did two things, and a line count only sees one. It deleted
comment runs, which a restore pass puts back and a coverage floor proves. It
also REWROTE runs it kept, scrubbing internal references, and some of those
rewrites took the explanation along with the reference: the file ends up with
a comment in the same place, shorter, saying less, and nothing notices.

bin/comment-run-pairs.js finds those. It pairs each before-and-after comment
run by token overlap, at a floor of 0.3, rather than by line, because
rewrapping a paragraph changes every line in it and line pairing reports a
pure rewrap as a total loss. 1370 runs paired across this repo's sweep, 134
lost words, and reading every one of those shows the large majority are the
scrub working: the reference went and the sentence around it stayed, often
tighter than before. Two in the main class were real losses and are merged
back without the reference: the worked example showing an 8192-byte payload
stored as 8197 bytes after alias expansion, and the field-by-field account of
what the canonicalizer returns.

The main class is also the file a human reads first for this service, so the validation
checks in it now each carry a plain-language line: the whole envelope-grammar
walk, the stall gates, the size cap, the unknown-action rule, the one-carrier
rule and the source-attribution rule. Comment-only, proven by stripping
comments and blank lines and comparing bytes.
The rename of the in-process bufferutils patch left this line naming a file
that no longer exists. The COPY under it is unchanged and still names
src/bufferutils.js, which did not move: the image build pins that path
literally, which is why it stayed at the top of src/.
xchain-decoder named xchain-encoder/src/validator.js and
xchain-utxo-tracker/src/BlockchainConnector.js and undo-blocks.js in
comments and computed sibling-path literals; those files moved during a
cross-repo directory reorganization. Forced by xchain-encoder and
xchain-utxo-tracker.
Comments, docs and message strings still named indexer files by paths that its feature-directory layout and snake_case renames retired. Each mention now names the file that holds that code, and no executable line changes.
The canonical coin registry repointed a consensus note at the decoder's
renamed crypto networks module. This re-vendors that byte change so the
copy stays identical to canonical.
bin/suite-title-map.js now reads a structured {paths, titles} rename map as well as a flat one, and bin/pins/suite-title-renames.json declares the 80 unit test files and 45 titles the layout pass renamed, so the compare against bin/pins/at1-suite-titles.json holds with zero undeclared differences.
… file

CODE-STYLE requires require() at the top of a file, not inside a function
body, so a computed or side-effecting import is the only kind allowed to
stay inline. The Database require carried no such reason: db.js only
defines the class, and hoisting it changes nothing about when a connection
opens. require('dotenv').config() stays in main(), since it is not a plain
`const X = require(path)` statement the layout codemod can safely hoist.
…checks

COMMENT-STYLE.md requires a plain-language line above every validation
check, and dbVerified/tablesVerified had none even though the surrounding
boot sequence is otherwise well commented. State in ordinary words what
each check protects against, matching the neighbouring stanzas' style.
Comments only; no code line changed.
…ugh config.js

The five remaining environment reads outside src/config.js now take their raw
value from it. Coercion, defaults and the injectable env parameter of
resolveTimeoutMs stay at each read site, so every value is unchanged.
dotenv now loads before db.js, as it did before the db.js require was hoisted,
and only when the file runs as the process, so a test requiring it for its
exports still sees no .env.
Only the chain block decoder applies the BigInt-safe reader, so the patch and
the file the Dockerfile copies over bitcoinjs-lib now live beside it, as they
do in xchain-utxo-tracker. The Dockerfile COPY source moves with it.
…_halt.js

The operator script takes the snake_case file name. The npm script keeps its
clear-reorg-halt name and xchain-node execs the new path in the same change.
An installed xchain-node execs `node src/clear-reorg-halt.js` inside the decoder container. The file at that path now runs src/clear_reorg_halt.js as the main module when executed, so the tool loads the service .env and exits exactly as before, and re-exports the same module when required. It is deleted once the fleet runs an xchain-node that execs the new path.
… config read note plainly

The config home added one require line to XChainDecoder.js; one of three consecutive blank lines goes, so the file does not grow. The note in config.js now says what a raw read returns without a narrative marker.
…tle blocks by behaviour

(cherry picked from commit 84f02f024ca4517ae20ea119a048805c56cd0110)
…n suites into same-title blocks by behaviour

(cherry picked from commit eb2fa4cdc5addd9031e31c81772abd8923cc3420)
A dry run only reads state and never calls clearReorgHalt, so gating it on the same 8-character --reason check as a real clear made the read-only path fail for no reason. The reason validation now runs only when a real clear will happen, and the dry-run message reports the checks either way, printing a hint to pass --reason instead of the reason text when none was given.
…nd /live route into a part beside the entry

src/api.js keeps its require path and every export; the moved units live in
src/api/probe_routes.js byte for byte, with the /live handler's probe state
and response split into two helpers under the function limit. The source
text scans that count the three health payload sites now read the entry and
the part together, and the /live scan reads the part.
src/db.js keeps its require path, the constructor and the exported class;
every other method moves byte for byte into a part under src/db/ installed
onto Database.prototype, and the checksum rebaselines and migration
preconditions attach as side-effect parts. The oversized table
verification, block delete, transaction and dispenser inserts, mempool
reconcile, migration runner, expiration type check and destructive
statement check split into helpers under the function limit. The security
source scans read the entry and the parts it requires.
… the entry

src/api.js keeps its require path, its exports and every route in the same
registration order. The start-failure and process crash records move to
src/api/crash_reporting.js, the health method's DB and halt probes to
src/api/health_probe.js, and the metrics and log shim wiring to
src/api/observability_wiring.js, byte for byte. The entry keeps the health
and /status payloads, getmempool and the middleware in named functions under
the length limit, reading the running flag and start error through getters.
Two source scans follow the moved text into its part, and three comments
that pointed at moved text now name its new home.
…spenser fee and mempool methods into parts beside the class

The decoder constants and payload helpers move to their own modules, and six method
groups install onto the class prototype from src/XChainDecoder/. The entry path,
its exported names and the prototype method set are unchanged. getSourceFromOutput,
detectEnvelopeWitness and updateMempool are split into in-file helpers as they move.
Source-reading tests follow the constant and mempool code to the parts that hold it.
…tializers

Identity and connector wiring, parse progress, fetch mode, reorg counters and the
halt park state each get an initializer the constructor calls in the same order,
so every field is assigned exactly as before.
…side the class

verifyReorg moves to src/XChainDecoder/reorg_verification.js and the haltReorg
closure to src/XChainDecoder/reorg_halt.js, with every statement and comment
moved verbatim. The walk, the above-tip and fork deletes, the tip refresh and
the prior-depth read become helpers called with .call(this, ...). Each helper
that awaits suspends on the same leaf await the method did before it can
return, and the refusal and record steps stay synchronous.

The two source-reading tests follow the code: the reorg counter guard reads
the part, and the chain-tier gate reads the entry with its parts.
parseTransaction moves to src/XChainDecoder/transaction_parsing.js, and its
per-output carrier reads and payload decompile to
src/XChainDecoder/carrier_extraction.js, with every statement and comment moved
verbatim.

The steps are generators driven by one loop in parseTransaction, so each
node or database wait is still awaited exactly once, in parseTransaction
itself, and a transaction with no carrier still resolves without suspending.
A step spells its waits `yield` where the method spelled them `await`.
…e class

start moves to src/XChainDecoder/startup.js, and the steps of its block loop to
sync_loop.js, tip_refresh.js, block_ingest.js, transaction_ingest.js and
dispenser_registration.js beside it, with every statement and comment moved
verbatim. The loop-carried cursors, counters and latches live on one object the
steps share. A step that ended the iteration returns 'continue', and one that
rolled the block back returns 'rollback', so the loop runs resetAfterRollback
after every write the step made, as before.

Each step that awaits suspends on the same node or database wait the loop did
before it can return, and every other step stays synchronous. The consensus
pin check stays the first statement of start, so its refusal is still thrown
before anything is awaited.

The source-reading tests follow the code into the parts. The chain-identity
require check accepts the parts' ../ path.
…penser/index.js

The indexer M3 move split src/actions/dispenser.js into src/actions/dispenser/,
with this.formats now in index.js, so the sibling guard read "sibling not found"
under XCHAIN_REQUIRE_SIBLINGS=1 on CI (run 35021438054). The pinned offsets are
unchanged; only the cited path moved.
…parts, not the loader alone

The indexer registry push made src/protocol_changes.js a loader over
src/protocol_changes/*.js, so the FIX_OUTPUT_FANOUT and BATCH_* conformance
guards found no row in the single file and run 35053183286 read four failures.
Both guards now read the loader plus every part, accept the row in its
addChange(...) call form and its registry tuple form, and resolve the mainnet
arm's numeric const from the same corpus; under XCHAIN_REQUIRE_SIBLINGS=1 a
loader with no parts directory is an error, never a skip.
Version 0.19.0, the 0.19.0 changelog section and the README badges and script-table counts measured on Linux.
@jdogresorg jdogresorg closed this Sep 16, 2026
@jdogresorg jdogresorg reopened this Sep 16, 2026
@jdogresorg
jdogresorg merged commit bb75868 into master Sep 16, 2026
15 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.

1 participant