UTXO indexing service for the XChain Platform. Continuously polls the coin nodes of every supported chain (Bitcoin, Litecoin, and Dogecoin today) via JSON-RPC, decodes every block, indexes all unspent transaction outputs in LevelDB using compact binary encoding, and serves balance and UTXO queries through REST and JSON-RPC APIs. The encoder depends on this service to find spendable inputs when constructing transactions.
- Full UTXO index: every unspent output indexed by SHA-256 scriptPubKey hash for fast address lookups
- Compact binary encoding: 11 LevelDB key prefix types stored as raw binary Buffers, reducing DB size ~50% vs hex strings
- Truncated txid keys: 8-byte transaction ID truncations in most index keys for further space savings; JSON-RPC
get_tx_blockinstead resolves against a separate index keyed on the full 64-hex txid, so two txids that happen to share their 8-byte prefix never contend for the same record and a rollback of one cannot delete the other's mapping - Active-UTXO-only storage: only unspent outputs in the live index; spent outputs archived temporarily for reorg recovery
- Real-time mempool tracking: unconfirmed transactions in a separate in-memory LevelDB, updated every 60 seconds
- BigInt precision: JSON-RPC
get_inforeturns full-precision balance strings viasatoshiToDecimalString(); the REST/balanceendpoint returns a float (useget_infowhen precision matters) - Reorg handling: per-chain, per-network undo history (mainnet BTC: 12, LTC: 120, DOGE: 120 blocks; LTC testnet: 5000; other testnets: 120; regtest as mainnet) with K/M archive records and automatic rollback on chain reorganization; depth overridable via
XCHAIN_UNDO_BLOCKS_<COIN> - Concurrent block prefetch: up to 10 blocks pre-fetched concurrently via JSON-RPC batch requests with HTTP keep-alive
- Batch writes: LevelDB writes batched in groups of 200 blocks with atomic commit
- Two-pass transaction processing: outputs inserted before inputs within each block, correctly handling intra-block spends
- Multi-chain support: Bitcoin, Litecoin, and Dogecoin today on mainnet, testnet, and regtest
- AuxPoW/HogEx header stripping: Dogecoin AuxPoW headers and Litecoin HogEx witness flag stripped before block decoding
- Bootstrap support: compressed tar archive backup and restore for fast initial sync
- REST + JSON-RPC API: dual interface for UTXO queries, balance lookups, address info, and bootstrap operations
- 990+ tests: unit, integration, e2e, boundary, security, fuzz, chaos, mutation, regression, performance, and smoke testing
Full UTXO tracker documentation is available in the xchain-documentation repository:
| Document | Description |
|---|---|
| README | Overview, features, installation, quick start, scripts, dependencies |
| Architecture | Data pipeline position, LevelDB key schema, block processing loop, reorg handling, mempool tracking |
| Configuration | Environment variables, internal constants, database paths |
| Operations | Running, Docker, REST and JSON-RPC API reference, resilience, troubleshooting |
Requires Node.js >= 22.
git clone https://github.com/XChain-Platform/xchain-utxo-tracker.git
cd xchain-utxo-tracker
npm installStorage is LevelDB-backed via
classic-level, which ships prebuilt binaries for Node 22.npm installneeds no compiler toolchain or build flags.
Create a .env file:
NETWORK=bitcoin-regtest
NODE_URL=127.0.0.1
NODE_PORT=18443
NODE_USER=rpc
NODE_PASSWORD=rpc
UTXO_TRACKER_API_PORT=3000Start the tracker:
npm run apiImportant
Pre-migration LevelDB snapshots must be re-indexed.
The on-disk output (O) record format was extended to carry the full 32-byte
transaction hash. Records written before that field was added store a zero hash
instead, which decodes to a missing txid. The tracker cannot derive a valid,
spendable transaction id from such a record: only the 8-byte key prefix is
available, and a 16-character prefix is not a usable txid.
If your LevelDB was first populated by a version of the tracker that predates the full-hash output format, wipe the data directory and re-sync from the coin node before serving address/UTXO queries with the current version. Without a re-index, any UTXO drawn from a pre-migration record raises an explicit "missing a fullTxHash ... re-index this LevelDB" error on the first spend attempt rather than silently producing an invalid transaction. A fresh sync, or any DB already synced under the current format, needs no action.
Important
get_tx_block needs a re-index against pre-migration history too.
get_tx_block is served from an exact-match index keyed on the full
32-byte txid (not the T record's 8-byte-prefix key), so two txids sharing
an 8-byte prefix each get their own record and neither can overwrite or
delete the other's mapping. This index is written by insertTransaction
alongside the T record, so a T record written before this index
existed has no matching entry: get_tx_block treats it as unindexed and
returns null rather than falling back to the collision-prone T key.
Blocks indexed via bulk-sync (the initial-sync fast path) never populate
this index either: with the default removeSpent setting bulk-sync calls
neither insertTransaction nor its exact-match write. get_tx_block
returns null for any txid from bulk-synced history until bulk-sync's
writer is updated to populate it; until then, wipe the data directory and
re-sync live (not via bulk-sync) to backfill get_tx_block for existing
history. Address, UTXO and balance queries are unaffected either way,
since they never depended on the T record or this index.
A Prometheus /metrics endpoint and a structured log shim ship with this
service and stay inert unless switched on: with no env set, no route is
registered, no timer starts and no socket opens. Turn the endpoint on with
METRICS_ENABLED=1 (add METRICS_TOKEN to gate the scrape on a reachable
box), and ship logs with LOG_SHIP_ENABLED=1 plus LOG_SHIP_URL. Full
variable list and the exported metric names are in
src/observability/README.md.
The module is vendored byte-identically from xchain-hub. Edit it there
and re-run xchain-hub/bin/sync-observability.sh; a local edit fails the
parity check CI runs across the vendored copies.
These four names configure the shim itself. The fleet deploy path carries them
into the container: xchain-node forwards any of them set in the module config
store or in the deploy host's environment (ModuleService.resolveObservabilityEnv),
and the validator compose files name them outright. Nothing is fabricated when
neither source sets one, so these defaults hold on an unconfigured box:
| Variable | Default | Effect |
|---|---|---|
LOG_LEVEL |
info |
Lowest level emitted. debug | info | warn | error; an unrecognised value falls back to info. |
LOG_FORMAT |
text |
text emits <iso-ts> <level> [<service>] <msg> key=value; json emits one NDJSON record per line. |
METRICS_ENABLED |
false |
Registers the /metrics route. The counter registry is built either way, so counters are collected whether or not the route is exposed. |
XCHAIN_LOG_PATCH |
1 |
Routes bare console.* calls through the shim so they carry the level and service prefix. 0 leaves console untouched, which is what the test bootstrap sets. |
| Command | Description |
|---|---|
npm run api |
Start the tracker and API server |
npm test |
Unit tests (~961 tests) |
npm run test:smoke |
Smoke tests (9 tests) |
npm run test:integration |
Integration tests (~69 tests) |
npm run test:e2e |
End-to-end tests (~36 tests) |
npm run test:boundary |
Boundary condition tests (~29 tests) |
npm run test:security |
Security tests (~78 tests) |
npm run test:regression |
Regression tests (~172 tests) |
npm run test:fuzz |
Fuzz tests (13 campaigns, 1000 iterations each, 106 tests) |
npm run test:fuzz:quick |
Quick fuzz (100 iterations, 106 tests) |
npm run test:fuzz:deep |
Deep fuzz (10,000 iterations, 106 tests) |
npm run test:perf |
Performance tests (~32 tests) |
npm run test:perf:quick |
Quick performance (small scale, 32 tests) |
npm run test:perf:deep |
Deep performance (large scale, 4 GB heap, 32 tests) |
npm run test:chaos |
Chaos engineering tests (~31 tests) |
npm run test:all |
All unit + integration + e2e tests (1,365 tests) |
npm run mutate |
Mutation testing (Stryker Mutator) |
npm run mutate:quick |
Quick mutation testing |
npm run mutate:p1 |
P1 priority mutation testing |
npm run mutate:p2 |
P2 priority mutation testing |
npm run mutate:p3 |
P3 priority mutation testing |
| Type | Tests | Description |
|---|---|---|
| Unit | ~544 | level_up_db.test.js, xchain_utxo_tracker.test.js, blockchain_connector.test.js, api.test.js, xchain_block_decoder.test.js, bufferutils.test.js, crypto_networks.test.js, util.test.js, boundary.test.js |
| Integration | ~69 | core_indexing.test.js, reorg.test.js, mempool.test.js, api_queries.test.js, batch_boundaries.test.js, boundary.test.js |
| E2E | ~36 | lifecycle.test.js, persistence.test.js, reorg.test.js, api.test.js, mempool.test.js |
| Smoke | 9 | smoke.test.js: module loading, config, API liveness |
| Boundary | ~20 | confirmations.test.js, key_range_scan.test.js, varint_thresholds.test.js |
| Security | ~27 | address_validation.test.js, concurrency_gate.test.js, key_pattern_injection.test.js, rest_route_surface.test.js |
| Fuzz | ~105 | 13 campaigns: blockDecoder, txProcessing, connector, addressValidation, balanceCalc, outputEncoding, leveldbKeys, apiEndpoints, bootstrap, config, reorgHandling, mempool, keySchema |
| Regression | ~120 | Curated critical-path suite: bulk-sync chain continuity/coinbase/merkle/resume, reorg/undo-window, coinbase maturity, connector cred scrub, satoshi precision |
| Performance | ~29 | Indexing throughput, query load, mempool stress, DB growth, reorg under load, mainnet-scale queries |
| Chaos | ~31 | RPC faults, storage faults, concurrency, state corruption |
| Mutation | (Stryker) | Stryker Mutator: P1/P2/P3 tiers + custom Buffer/encoding mutations |
| Total | 990+ |
Copyright © 2025-2026 Dankest, LLC
Based on XChain Platform by Dankest, LLC – https://dankest.llc
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later) with a commercial license available for proprietary use.
You may use, modify, and distribute this material under the terms of the License. See LICENSE and NOTICE for full terms. See the licensing overview.