XChain Platform Hub: Configuration

Environment Variables

Silent-failure variables (read this first)

Two variables degrade security when left empty. The hub starts and appears healthy either way, so the misconfiguration is easy to miss.

HUB_API_KEY: empty disables API authentication. When HUB_API_KEY is set, authentication fails closed: mutating methods (updateconfig, registervalidator, propose, vote, requestattestation, reportreorg, initiateswap, the oracle/price push methods) and the hub-DB WebSocket upgrade return 401 unless the caller presents the configured key.

When it is unset or empty, those paths are open, so the hub refuses to boot unless keyless operation is declared with HUB_ALLOW_UNAUTHENTICATED=true. Keyless remains supported (single-host regtest, a hub reachable only on a private network or behind an authenticating proxy), but it has to be a stated choice rather than the result of a forgotten variable. xchain-node sets the declaration automatically when it deploys a hub with no key in the host environment, except on mainnet, where the refusal stands.

Always set a strong, random HUB_API_KEY in production. Generate one with:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Clients then send it on each request (the indexer reads the same value as HUB_API_KEY, the encoder-facing services as their own *_ENCODER_API_KEY, and so on).

SIGNING_PRIVKEY_SECRET: empty means unsigned P2P messages and no federation identity. SIGNING_PRIVKEY_SECRET is the Ed25519 private key (a 32-byte seed, encoded as 64 hex characters) that authenticates this hub’s P2P messages to the rest of the validator federation. When the P2P cluster is enabled (see P2P_VALIDATOR_ADDR) but this key is empty, the hub loads no validator identity: outbound messages go out unsigned, and this hub has no verifiable identity among its peers. Nothing fails loudly; the hub simply never participates as an authenticated validator, and depending on peers’ REQUIRE_SIGNATURES its messages may be silently dropped.

Generate a key pair (the private seed is what you set; keep it secret):

# Private seed (set this as SIGNING_PRIVKEY_SECRET):
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

The corresponding public key is derived automatically at startup from the seed (the hub logs the first 16 hex characters of the pubkey when the identity loads). Each validator’s pubkey must be registered in the cluster so peers can verify its signatures.

The key format is validated: it must be exactly 64 hex characters. An invalid non-empty value throws at startup; only the empty case is silent.

Secret variable naming

Automatic secret redaction, in terminals, CI logs and assistant transcripts, keys on the variable name and matches _SECRET, _KEY, or _TOKEN. A name like HUB_DB_PASS matches nothing, so the value prints in full every time someone reads the env file.

Three hub secrets accept a _SECRET name, with the historical name kept as a deprecated fallback so no running deployment breaks:

Preferred Deprecated
HUB_DB_SECRET HUB_DB_PASS
XCHAIN_PRICE_INDEXER_DB_SECRET XCHAIN_PRICE_INDEXER_DB_PASS
SIGNING_PRIVKEY_SECRET SIGNING_PRIVKEY_HEX

The hub logs a warning at startup for each secret still supplied under the deprecated name. Setting both names to different values is a startup error, not a precedence rule: that shape is a half-finished rename, and picking a winner is how a hub keeps authenticating with the credential it was supposed to rotate away from. Renaming does not un-leak anything by itself: a credential that has already been read out loud still has to be rotated.

Core (Required)

These variables are required regardless of operating mode.

Variable Required Default Description
HUB_HOST No 0.0.0.0 Host to bind the API server
HUB_PORT Yes None Port for the JSON-RPC API
HUB_DB_HOST Yes None MariaDB host
HUB_DB_PORT Yes None MariaDB port
HUB_DB_NAME Yes None MariaDB database name (e.g., XChain_Hub)
HUB_DB_USER Yes None MariaDB username
HUB_DB_SECRET Yes None MariaDB password. Deprecated name HUB_DB_PASS is still read; see Secret variable naming above.
HUB_DB_KEEPALIVE_INTERVAL No 30000 Interval (ms) between no-op keepalive queries sent to the MariaDB pool to prevent idle-connection drops
HUB_TRUST_PROXY No loopback, uniquelocal Express trust proxy setting. A containerized hub behind a local reverse proxy works with the default. Set to false to disable, a hop count (e.g. 1), or a CIDR list for other topologies. See Express docs.
HUB_ALLOW_UNAUTHENTICATED No false A hub in validator mode (P2P_VALIDATOR_ADDR set) with no HUB_API_KEY refuses to boot, because its write methods would let anyone drive consensus-affecting writes. Set to true to explicitly acknowledge running keyless (regtest/dev only). See OPERATIONS.md → Authentication.

Telemetry Collector

The hub is the single collector for anonymous node-operator telemetry (the telemetry_pings table; see Database Schema below). The raw client IP is never stored: at ingest the hub derives a coarse country/region and a keyed one-way hash, then discards the IP.

Variable Required Default Description
TELEMETRY_ENABLED No true Accept telemetry pings. Set false on a private/local hub.
TELEMETRY_RETENTION_DAYS No 90 Prune telemetry rows older than N days.
TELEMETRY_IP_SALT No None Secret salt for the one-way IP hash. Without it, ip_hash is left null (an unsalted hash would be trivially reversible).
TELEMETRY_ADMIN_KEY No None x-api-key gate for the telemetry admin/query surface (empty leaves it fail-closed). Must match the value the dashboard service is configured with.

Metrics and Log Shipping

The shared observability module adds a Prometheus scrape endpoint and a structured log shim. Both are off unless set here: with no variables the hub registers no extra route, starts no timer, and opens no socket.

Variable Required Default Description
METRICS_ENABLED No off Serve the Prometheus scrape endpoint.
METRICS_PATH No /metrics Scrape path.
METRICS_TOKEN No None Require Authorization: Bearer <token> on the scrape. Set this (or keep the path behind the fronting proxy) on any internet-reachable box.
METRICS_HTTP No true when metrics are on Per-request counters and a latency histogram. Set 0 for endpoint-only.
LOG_FORMAT No text json emits one NDJSON record per log line.
LOG_LEVEL No info debug, info, warn, or error.
LOG_SHIP_ENABLED No off POST batched NDJSON to a collector. Needs LOG_SHIP_URL too; either alone stays off.
LOG_SHIP_URL No None Collector endpoint (http/https).
LOG_SHIP_TOKEN No None Bearer token for the collector. Never logged or echoed.
LOG_SHIP_BATCH_SIZE No 100 Lines per POST.
LOG_SHIP_INTERVAL_MS No 5000 Flush interval.
LOG_SHIP_MAX_BUFFER No 5000 Bounded buffer; the oldest lines are dropped and counted, never grown without limit.
LOG_SHIP_TIMEOUT_MS No 5000 Per-batch POST timeout.

P2P Gossip Layer

Validator mode is activated when P2P_VALIDATOR_ADDR is set. All P2P-dependent subsystems (consensus, oracle, cross-chain, reorg, governance) are no-ops without it.

Variable Required Default Description
P2P_VALIDATOR_ADDR No None This validator’s public address. Setting this activates validator mode.
P2P_PORT No 10001 WebSocket P2P listen port
P2P_HOST No 0.0.0.0 P2P bind address
SEED_NODES No None Comma-separated list of peer addresses (e.g., peer1.example.com:10001,peer2.example.com:10001)
SIGNING_PRIVKEY_SECRET No None 64-hex-char Ed25519 private key seed for message signing. Deprecated name SIGNING_PRIVKEY_HEX is still read; see Secret variable naming above and Silent-failure variables for what an empty value does.
HUB_NETWORK Yes (validator mode) None Deployment network: mainnet, testnet, or regtest. Required when P2P_VALIDATOR_ADDR is set; process.exit(1) if absent or invalid. Must match the INDEXER_NETWORK of the chains this hub federates. Controls consensus activation gating (e.g. STAKE_WEIGHTED_QUORUM activation height is per-network).
REQUIRE_SIGNATURES No true When true, reject unsigned P2P messages. Defaults to true in validator mode; pass false to bootstrap a new federation before all nodes have keys.
P2P_HEARTBEAT_INTERVAL No 15000 Milliseconds between heartbeat broadcasts
P2P_RECONNECT_BASE No 2000 Base delay for reconnect backoff (ms)
P2P_RECONNECT_MAX No 60000 Maximum delay for reconnect backoff (ms)
P2P_MSG_DEDUP_TTL No 60000 Message deduplication cache TTL (ms)
P2P_MAX_PAYLOAD No 1048576 Maximum WebSocket message size in bytes (1 MB)
P2P_DEDUP_PRUNE_INTERVAL No 30000 Interval (ms) at which the seen-message deduplication cache is pruned
P2P_WS_PING_INTERVAL No 30000 Interval (ms) for WebSocket ping/pong keepalive (dead-connection detection)
P2P_MAX_CONNECTIONS_PER_IP No 3 Maximum simultaneous inbound connections from a single IP (anti-DoS). Increase for co-located federations where multiple validators share one IP.
HUB_CAPABILITY_CONFIG No None Path to the capability config JSON (see below). Required for capability qualification + self-tests.

Capability Configuration

Capability staking decides which of the five capabilities (price, cross_chain, oracle_publish, attestation, full_node) a validator is qualified and ready to serve. The hub loads this from the JSON file at HUB_CAPABILITY_CONFIG, applies it on startup, and hot-reloads on file change. It supplies two things:

  • CAPABILITIES.<cap>.MIN_STAKE: the stake threshold a pubkey must meet (queried from the indexer) to qualify. If a capability has no configured MIN_STAKE, the hub treats it as not qualified (fail-closed); it does not default to 0.
  • Per-capability self-test config blocks, checked locally so the hub only participates when it can actually serve:
    • price: { "sources": [...], "fiats": [...] }
    • cross_chain: { "chains": { "BTC": { "rpc": "..." }, ... } }
    • oracle_publish: { "doge_address": "...", "doge_wallet": "..." }
    • attestation: { "providers": { "<id>": false } } (omit a key to enable it)
  • DISABLED_CAPABILITIES: array of capabilities to opt out of even when qualified.
{
  "CAPABILITIES": {
    "price":          { "MIN_STAKE": "1000.00000000" },
    "oracle_publish": { "MIN_STAKE": "500.00000000" }
  },
  "DISABLED_CAPABILITIES": ["cross_chain", "attestation"],
  "price": { "sources": ["coingecko"], "fiats": ["USD"] },
  "oracle_publish": { "doge_address": "D...", "doge_wallet": "/data/.dogecoin/wallet.dat" }
}

xchain-node validator init generates a starter file and xchain-node install mounts it into the hub container automatically. See OPERATIONS.md → Validator Mode.

PBFT Consensus

Variable Required Default Description
PBFT_TIMEOUT No 30000 Consensus round timeout in milliseconds. Triggers view change on expiry.
MIN_VALIDATORS No 1 Minimum validators required before a consensus round may run.
HUB_CONSENSUS_INPUT_ALERT_AFTER No (built-in default) Consecutive consensus-input failures before the alarm fires. A non-integer or non-positive value logs an error and falls back to the default rather than disabling the alarm, so a typo cannot silently restore fail-closed-and-silent behaviour.
HUB_SNAPSHOT_REORG_BUFFER No 6 Blocks of reorg buffer applied when building a capability snapshot. Consensus-critical: it must match across the federation. A malformed value logs an error and falls back to 6 rather than forking the federation on a typo.
XCHAIN_HUB_SKIP_MIN_STAKE_ASSERT No (unset) Set to 1 to skip the minimum-stake assertion at startup. Test and bring-up seam; leaving it set on a real deployment disables a safety check.

Hub-DB WebSocket (GET /hub-db/subscribe)

Caps on the live-update channel indexers subscribe to. See API.

Variable Required Default Description
WS_MAX_SUBSCRIBERS No 1000 Maximum simultaneous subscribers across all IPs
WS_MAX_PER_IP No 100 Maximum simultaneous subscribers from a single IP
WS_BACKPRESSURE_LIMIT No 50 Buffered messages a slow subscriber may accumulate before its connection is dropped
WS_WATERMARK_INTERVAL_MS No 10000 Interval between watermark heartbeats, which let a subscriber tell “the mirror is behind” apart from “no rows are being produced”
WS_WATERMARK_LATE_FACTOR No 2 Multiple of WS_WATERMARK_INTERVAL_MS after which a heartbeat gap counts as late and is logged; getWatermarkStats() exposes the tally on /health. A value below 1 would mark an exactly-on-time tick late, so anything under 1 falls back to the default rather than raising permanent false alarms

Indexer tip freshness

The hub reads the BTC chain tip to anchor consensus rounds. These gates stop a stale tip from locking an out-of-date validator set into a round.

Variable Required Default Description
BTC_INDEXER_URL No (from config table) BTC indexer JSON-RPC URL used by the full-node challenge round.
BTC_INDEXER_API_KEY No (from config table) API key presented to that indexer’s fail-closed federation-read gate. Treat as a credential.
BTC_INDEXER_API_URL No None BTC indexer JSON-RPC URL for the validator-mode price oracle’s block-height anchor (getlatestblock). Set it when the hub is not co-located with a BTC indexer, e.g. a master hub box whose BTC stack lives elsewhere. Empty falls back to local resolution. xchain-node forwards this from the host environment.
MAX_INDEXER_LAG_BLOCKS No 200 Maximum blocks the BTC indexer may lag before its tip is treated as untrustworthy and ignored, degrading gracefully instead of locking in a stale validator set.
MAX_TIP_AGE_S No 2 × ORACLE_ROUND_INTERVAL (seconds) Maximum age of the indexer-pushed BTC tip before it is considered stale.
INDEXER_COIN_CHECK No enabled Set to 0 to disable the per-coin indexer reachability check.

Oracle

Variable Required Default Description
ORACLE_ROUND_INTERVAL No 600000 Milliseconds between oracle rounds (default: 10 minutes)
ORACLE_SUBMISSION_WINDOW No 180000 Milliseconds to collect validator price submissions (default: 3 minutes)
ORACLE_FINALIZATION_TIMEOUT No 120000 Timeout for oracle PBFT finalization round (default: 2 minutes)
ORACLE_MIN_SUBMISSIONS No 2 Minimum distinct-validator price submissions required before a round can be finalized. Consensus-critical: a single-hub or regtest deployment stalls every oracle round unless this is set to 1, because the default of 2 requires a second submitter that will never arrive.
ORACLE_MAX_SUBMISSIONS_PER_ROUND No 200 Cap on the number of price submissions accepted per round. Submissions beyond this limit are discarded to bound memory and consensus payload size.
ORACLE_STALENESS_THRESHOLD_S No 2 x ORACLE_ROUND_INTERVAL Seconds since the last finalized price snapshot before the GET /health endpoint reports oracle_stale: true (and returns HTTP 503). Defaults to twice the round interval; override for slow-start or custom round cadences.
ORACLE_EARLY_MSG_MAX_ROUNDS No 256 Cap on the number of distinct future consensus rounds the oracle buffers early messages for. Bounds memory against a peer flooding fabricated round numbers; messages for rounds beyond the cap are dropped.
COINGECKO_API_KEY No None CoinGecko API key (optional, improves rate limits)
COINMARKETCAP_API_KEY No None CoinMarketCap API key (enables a third price source; CoinGecko and Kraken are both keyless and always active)
PRICE_FETCH_TIMEOUT No 10000 HTTP timeout for external price API calls (ms)
ORACLE_LEADER_TIMEOUT_MS No 30000 How long a round waits on its leader before failover. Kept below the finalization window.
ORACLE_FINALIZED_MAX No 10000 Cap on retained finalized-round records held in memory.
ORACLE_SUBMISSIONS_RETENTION_ROUNDS No (unset) Number of past rounds of raw price submissions to retain. Unset keeps the built-in retention.
ORACLE_ALLOW_UNVERIFIED_PAIRS No false Set to true to accept price pairs that have not been verified. Loosens a fail-closed check; intended for bring-up, not production.
ORACLE_MAX_PRICE_AGE_SECONDS No (coin registry, per pair) Maximum age of an oracle price before it is treated as stale. Resolution order is p2pConfig → this variable → the per-pair value pinned in the coin registry. The registry value is never a hardcoded literal, so a coordinated release that changes the pin cannot silently diverge the hub’s advisory from the indexer’s gate. Setting this per-host overrides that pin: do it deliberately, and match it across the federation.

Oracle Publishing

Controls OraclePublisher, which broadcasts finalized price rounds on-chain as DOGE PRICE actions.

Variable Required Default Description
ORACLE_PUBLISH_ENABLED No true Set to false to stop this hub publishing oracle rounds on-chain. Consensus participation is unaffected.
PUBLISHER_QUEUE_PATH No ./data/publisher-queue.jsonl Durable queue file for pending publishes. Point at persistent storage so a restart does not lose queued rows.
PUBLISHER_MAX_ATTEMPTS No 5 Attempts before a queued publish is abandoned.
DOGE_PUBKEY_HEX No (from config table) Public key, hex, of the DOGE publishing wallet.
DOGE_ENCODER_URL No (from config table) Encoder URL used to build DOGE publish transactions.
DOGE_ENCODER_API_KEY No (from config table) API key presented to that encoder when it runs keyed. Treat as a credential.
DOGE_LOW_BALANCE_THRESHOLD No 10 DOGE balance below which the publisher warns that it is running out of funds.

Rewards and Slashing

Variable Required Default Description
ORACLE_REWARD_PER_ROUND No "10.00000000" XCHAIN distributed per finalized oracle round
SLASH_DEVIATION_THRESHOLD No "0.05" Price deviation threshold (5%) for slash detection
SLASH_MISSED_ROUNDS_THRESHOLD No "30" Consecutive missed rounds before non-participation slash

ANCHOR Publishing

Controls StateAnchorPublisher (commits checkpoints and the cross-chain match archive on-chain via the DOGE ANCHOR action) and RewardTracker (anchor-publish reward amount).

Variable Required Default Description
ANCHOR_INTERVAL_MS No 86400000 Milliseconds between ANCHOR publish cycles (default: 24 hours)
ANCHOR_MATCH_BATCH_SIZE No 200 Maximum cross_chain_matches rows to include per ANCHOR archive chunk
ANCHOR_CHUNK_RETRY_MS No 2500 Delay before retrying a failed archive chunk upload (ms)
ANCHOR_ELECTION_TOLERANCE_BLOCKS No 36 BTC blocks a non-leader hub waits before the next eligible rank may take over
ANCHOR_REWARD_PER_PUBLISH No "10.00000000" XCHAIN distributed to the elected ANCHOR publisher per successful publish cycle
ANCHOR_CHECKPOINT_EVERY_N No 1 Anchor only every Nth checkpoint_seq on-chain (per chain). Decouples on-chain ANCHOR spend from checkpoint production cadence: skipped (off-multiple) seqs remain in the off-chain hub-DB mirror and are still verifiable via the explorer. 1 anchors every checkpoint (original behaviour).
ANCHOR_ENABLED No true Set to false to stop this hub publishing ANCHORs.
ANCHOR_MAX_BATCH No 1000 Maximum cross_chain_matches rows drained into one publish cycle.
ANCHOR_CHUNK_MAX_BYTES No 6000 Maximum payload bytes per ANCHOR archive chunk.
ANCHOR_ROUND_TIMEOUT_MS No 120000 Timeout for one ANCHOR signing round.
ANCHOR_AMBIGUOUS_POLL_ATTEMPTS No 3 Re-polls before an ambiguous publish result (broadcast may or may not have landed) is resolved.
ANCHOR_AMBIGUOUS_POLL_MS No 5000 Delay between those re-polls.
ANCHOR_ANNOUNCE_RETRY_MS No 300000 Delay between retries of the anchor announcement (5 minutes).
ANCHOR_ANNOUNCE_RETRY_TTL_MS No 21600000 How long announcement retries continue before the entry is dropped (6 hours, roughly six times the 60-confirmation DOGE window).
ANCHOR_ANNOUNCE_QUEUE_MAX No 500 Maximum queued anchor announcements, bounding memory.

Why those magnitudes (before you retune them)

None of the ANCHOR knobs is consensus data: hubs on different values still produce mutually verifiable anchors. Three of them do encode a real bound, and the full derivation is in ANCHOR.md:

  • ANCHOR_CHUNK_MAX_BYTES = 6000 reserves head room under the protocol’s 8192-byte MAX_ACTION_DATA_LENGTH ceiling, because chunk 0 shares its action with the checkpoint prefix (~322 bytes) and the signature lists (194 bytes per (PUBKEY, SIG) pair, doubled on a v6). What is left is about nine signature pairs on a v1, or 4+4 on a v6, so lower this as the federation grows: a 5+5 v6 quorum needs ~5860 or less, a 7+7 quorum ~5080. Exceeding the ceiling is silent, since the decoder simply drops the action.
  • ANCHOR_MATCH_BATCH_SIZE = 200 is an early-flush latency trigger and ANCHOR_MAX_BATCH = 1000 is the per-cycle DOGE spend bound. Archive rows are signature-dominated and barely compress (~0.55 KB of gzip+base64 per settled match), so 1000 rows is ~550 KB, ~93 chunks, ~93 DOGE transactions in a cycle; 200 rows is ~19.
  • ANCHOR_ELECTION_TOLERANCE_BLOCKS = 36 is ~6 hours of BTC blocks per failover rank, counted in blocks rather than wall clock so every hub agrees on the unlock without clock sync. The ordering carries the meaning: signing round (120s) + DOGE burial (60 confs, ~1h) << 36 blocks (~6h) << ANCHOR_INTERVAL_MS (24h). Roughly 6 to 144 blocks keeps both bounds; a wrong value costs duplicate DOGE spend or delayed anchoring, never a divergence.

Attestation Publishing

Controls AttestationPublisher, which writes the validator network’s answers to contract attestation requests back on-chain.

Variable Required Default Description
ATTEST_ENABLED No true Set to false to stop this hub publishing attestation results.
ATTESTATION_QUEUE_PATH No ./data/attestation-queue.jsonl Durable queue file for pending attestation publishes. Point at persistent storage.
ATTESTATION_FAILOVER_WINDOW_BLOCKS No 2 Blocks of leader silence before the rank-1 hub steps in.
ATTESTATION_FAILOVER_POLL_MS No 30000 Failover sweep cadence.
ATTESTATION_LEADER_RETRY_MS No 60000 Grace period before the sweep retries a leader’s entry.
ATTESTATION_BLOCK_MS No 600000 Nominal block interval used to translate the failover window from blocks into time. Defaults to the BTC ~10 minute interval.
ATTESTATION_AMBIGUOUS_COOLDOWN_MS No ATTESTATION_FAILOVER_WINDOW_BLOCKS × ATTESTATION_BLOCK_MS Cooldown after an ambiguous publish result before another hub may retry.
BTC_ADDRESS No (from config table) BTC address of this hub’s publishing wallet.

Attestation Relay

Controls AttestationRelay, the driver that carries attestation requests to the validator network and the response leg back. Attestation staking lives on Bitcoin, so an ATTEST request made on an origin chain (LTC, DOGE) is relayed to BTC as a v3 request, and the finalized answer is relayed back to the origin chain as a v4 response. The relay therefore needs a broadcast rail on every origin chain it serves, not just on the home chain.

Variable Required Default Description
ATTEST_RELAY_ENABLED No 0 Set to 1 to run the relay. Opt-in rather than a kill switch, deliberately: a fleet that has merely deployed this code must run nothing until an operator turns it on.
ATTEST_RELAY_POLL_MS No 15000 How often the relay sweeps for work.
ATTEST_RELAY_FAILOVER_MS No 1200000 Leader silence tolerated before another hub takes the relay over. Twenty minutes.
ATTEST_RELAY_QUEUE_PATH No ./data/attest-relay-queue.jsonl Durable at-most-once record of v3 broadcasts. Point it at persistent storage: replaying a v3 is rejected on-chain but still burns a real BTC fee, and this file is what stops a restart from doing that.
BTC_PUBKEY_HEX No (from config table) Public key, hex, of that wallet.
BTC_ENCODER_URL No (from config table) Encoder URL used to build BTC publish transactions.
BTC_ENCODER_API_KEY No (from config table) API key presented to that encoder when it runs keyed. Treat as a credential.
ATTESTATION_HTTP_GET_ALLOW_PRIVATE No (unset, fails closed) Set to 1 to let the http_get attestation provider reach private and loopback addresses. Off by default and security-relevant: the provider normally resolves the hostname once and pins the request to that public address, which is what stops a contract-supplied URL being used for SSRF against the validator’s own network. Enable only on an isolated test venue.

Per-origin-chain broadcast rail

The variables below are read dynamically (process.env[coin + '_ENCODER_URL'] and friends) for each origin chain, so a static scan of the source will not list them; <COIN> is any allowed coin other than BTC, so today LTC and DOGE. Each also resolves from the hub’s p2pConfig; the env var wins.

Leaving an origin chain’s rail unconfigured does not fail loudly. The v4 response is still consensus-finalized and then held forever, behind one startup warning (no <COIN> broadcast rail). Responses are held rather than dropped, so the recovery is to configure the rail and restart, but nothing re-warns in the meantime. Check attest_relay on /health to see the hold: a rising awaiting_broadcast with a flat responses_relayed is this misconfiguration.

Variable Required Default Description
<COIN>_ENCODER_URL If relaying to <COIN> None xchain-encoder endpoint used to build the v4 response transaction on origin chain <COIN> (e.g. LTC_ENCODER_URL). Empty means no rail; see the hold warning above.
<COIN>_ENCODER_API_KEY No None API key for that origin chain’s encoder.
<COIN>_ADDRESS If relaying to <COIN> None Wallet address on <COIN> that pays for and publishes v4 responses (e.g. LTC_ADDRESS).
<COIN>_PUBKEY_HEX If relaying to <COIN> None Public key (hex) for that address.
<COIN>_INDEXER_API_URL If relaying to <COIN> None Indexer endpoint the relay reads <COIN>-origin ATTEST requests from. Also accepted as <COIN>_INDEXER_URL, or pushed via xchain-node updateconfig. A chain with no indexer URL is skipped every tick, with a startup warning.
<COIN>_INDEXER_API_KEY No None API key for that indexer.

The home (BTC) rail reuses the BTC attestation publisher variables above (BTC_ENCODER_URL, BTC_ADDRESS, BTC_PUBKEY_HEX, BTC_INDEXER_API_URL). Origin rails are deliberately kept separate from it: an operator broadcast hook configured for one chain would put an LTC payload on BTC, where it is rejected outright after burning a real BTC fee. Spend limits come from the shared spend guard under the ATTEST prefix (see Effector Spend Policy below), and confirmation depths from XCHAIN_CONFIRMATIONS_<COIN>.

Effector Spend Policy

Every hub effector that spends real coin on-chain runs behind a shared spend guard: a balance floor, a rolling per-window spend ceiling (hard-clamped at a $2000 admission ceiling), and a per-capability runtime pause. The knobs below take a per-effector <PREFIX>; the four prefixes are ORACLE_PUBLISH, ATTEST, ANCHOR, and FULLNODE. Each variable also resolves from p2pConfig; the env var wins. The spend ceiling is default-enabled: unset config yields the $2000 clamp, never “off”.

Variable Required Default Description
<PREFIX>_MAX_SPEND_USD_CENTS_PER_WINDOW No 200000 ($2000) Rolling per-window spend budget in USD cents. Clamped to <= 200000; an operator can only lower it.
<PREFIX>_EST_SPEND_USD_CENTS No 100 ($1) Per-broadcast cost estimate charged against the window budget when the caller does not supply a real fee.
<PREFIX>_MAX_PUBLISHES_PER_WINDOW No 0 (off) Optional per-window broadcast count cap, defense in depth alongside the USD budget. <=0 disables the count cap.
<PREFIX>_SPEND_WINDOW_MS No 3600000 (1h) Rolling window length (ms) for both the count and USD ceilings.
<PREFIX>_MIN_BALANCE No 0 Wallet floor (native coin). A balance below the floor, or an unreadable (null) balance, skips the spend fail-closed.

Runtime pause is operator-driven via JSON-RPC (auth-gated): pauseeffectorspend / resumeeffectorspend take { label } (the effector’s guard label, e.g. OraclePublisher), and geteffectorspendstatus lists every effector’s live state. A pause halts the effector’s primary/leader spend path immediately, with no restart.

State Checkpoints

Controls StateCheckpointEngine, which produces the quorum-signed per-block state-hash checkpoints that light clients and xchain-sync replicas verify against.

Variable Required Default Description
CHECKPOINT_ENABLED No true Set to false to stop this hub participating in checkpoint rounds.
CHECKPOINT_CHAINS No all supported coins Comma-separated list of chains to checkpoint. Entries outside the supported set are dropped.
CHECKPOINT_INTERVAL_BLOCKS No 6 Blocks between checkpoints. Raising it cuts checkpoint and anchor spend at the cost of a coarser recovery point; 144 is roughly daily on BTC.
CHECKPOINT_CONFIRMATIONS No 6 Confirmations required before a block is eligible for checkpointing.
CHECKPOINT_POLL_MS No 60000 Interval between checkpoint eligibility polls.
CHECKPOINT_ROUND_TIMEOUT_MS No 60000 Timeout for one checkpoint signing round.
CHECKPOINT_COSIGN_TOLERANCE_BLOCKS No 144 Fail-closed co-sign gate: a SIGN_REQ whose snapshot_block deviates from this hub’s own BTC tip by more than this many blocks is declined. The default is roughly a day of BTC blocks.
CHECKPOINT_STALL_LOG_MS No 3600000 (1 h) Throttle for the “cadence stalled” log line. The eligibility poll runs far more often than the checkpoint cadence, so the reason is logged at most this often and the counter carries the true rate.

Full-Node Challenge

Controls FullNodeChallengeRound, the periodic possession challenge proving a validator runs a real coin full node rather than mirroring the decoder and indexer databases. Feeds the full-node verified reward tier and the on-chain NODEPROOF action.

Variable Required Default Description
FULLNODE_ENABLED No true Set to false to stop this hub running full-node challenges.
FULLNODE_BTC_RPC No (from coin config) BTC node JSON-RPC URL used to pose the challenge.
FULLNODE_CHALLENGE_INTERVAL_BLOCKS No 144 Blocks between challenge rounds (about daily on BTC).
FULLNODE_SPEND_LOG_PATH No ./data/fullnode-verdict.spend.jsonl JSONL record written and fsynced BEFORE a verdict fee is committed, so a crash mid-flight leaves a durable trace instead of only stdout.
FULLNODE_CONFIRM_DEPTH No 100 Depth behind the tip from which challenge material is drawn.
FULLNODE_VERDICT_ACCEPT_WINDOW_BLOCKS No 24 Blocks during which a verdict remains acceptable.
FULLNODE_POLL_MS No 30000 Poll cadence for challenge progress.
FULLNODE_COLLECT_MS No 20000 Window for collecting challenge answers.
FULLNODE_COLLECT_DEPTH_BLOCKS No 3 Blocks past the collection point before the round closes.
FULLNODE_PROOF_WINDOW_BLOCKS No per-coin Blocks a challenged node has to submit a proof.
FULLNODE_REWARD_PASS_WINDOW_BLOCKS No per-coin Blocks in the reward-pass window.
FULLNODE_MIN_PASS_RATE_BPS No per-coin Minimum pass rate, in basis points, for a node to be treated as passing.
FULLNODE_REWARD_SHARE No per-coin Reward share for passing full nodes.
FULLNODE_GENESIS_VERIFIERS No per-coin Comma-separated genesis verifier pubkeys (lowercased).
XCHAIN_HUB_SKIP_FULLNODE_ASSERT No (unset, assertion active) Set to 1 to skip the canonical-FULLNODE assertion at startup and warn instead. Intended as a loud one-off bypass for a venue running its own challenge cadence and verifier set: divergent NODEPROOF knobs fork the challenge schedule, so this is not a setting to leave on.

Retraction Consensus

Variable Required Default Description
RETRACT_ROUND_TIMEOUT_MS No 180000 Timeout for one retraction round.
RETRACT_SIGN_RETRY_MS No 15000 Delay before re-sending a retraction SIGN_REQ.
RETRACT_INTENT_TTL_MS No 3600000 How long a retraction intent stays live before expiring (1 hour).

XCHAIN Price Derivation

The XCHAIN/USD price is derived from platform-realized fills rather than an external feed. These read the indexer database holding those fills.

Variable Required Default Description
XCHAIN_PRICE_INDEXER_DB_HOST No None Host of the indexer DB the fills are read from
XCHAIN_PRICE_INDEXER_DB_PORT No None Port of that database
XCHAIN_PRICE_INDEXER_DB_NAME No None Database name
XCHAIN_PRICE_INDEXER_DB_USER No None Database user
XCHAIN_PRICE_INDEXER_DB_SECRET No None Database password. Deprecated name XCHAIN_PRICE_INDEXER_DB_PASS is still read; see Secret variable naming above. Treat as a credential: supply it from the deployment environment, never a checked-in file.
XCHAIN_PRICE_INDEXER_DB_COIN No BTC Chain whose fills the price is derived from
XCHAIN_PRICE_WINDOW_BLOCKS No (built-in) Rolling window, in blocks, over which fills are aggregated
XCHAIN_PRICE_MIN_BTC_VOLUME No (built-in) Minimum BTC-notional volume in the window before a derived price is considered valid
XCHAIN_PRICE_CONFIRMATION_BUFFER No (built-in) Confirmations a fill needs before it counts toward the derived price
XCHAIN_PRICE_BOOTSTRAP_SATS No 1000 Bootstrap XCHAIN price in SATOSHIS, used before enough on-platform volume exists to derive one. Converted to USD at round time with the consensus BTC/USD, so it is never a USD pin. Consensus-critical: a per-operator value forks fee acceptance

LLM Attestation Provider

Backs the ATTEST path where a contract asks an approved model a question. See Attestation.

Variable Required Default Description
LLM_PROVIDER_ENABLED No true Set to false to disable the LLM attestation provider on this hub.
LLM_MAX_BUDGET_USD No (built-in cap) Spend ceiling in USD for LLM attestation calls. A kill-switch against runaway cost.
CLAUDE_BIN No claude Path to the Claude CLI binary the provider spawns. Override when it is not on PATH.

Cost note. Each on-chain checkpoint anchor spends real DOGE on three transactions (BTC + LTC + DOGE checkpoints all broadcast on the DOGE chain). State recovery (recovery.js) only needs the latest anchored checkpoint per chain, so anchoring every intermediate checkpoint_seq is optional. With daily checkpoints (CHECKPOINT_INTERVAL_BLOCKS=144), ANCHOR_CHECKPOINT_EVERY_N=2 halves anchor spend (on-chain recovery point then trails the tip by up to ~2 checkpoint intervals). checkpoint_seq is consensus data, so the gate is deterministic across every hub.

Operator Signer

Variable Required Default Description
HUB_SIGNER_MODULE No None Path to a CommonJS module exporting walletSign(psbtHex) → Promise<txHex>. Used by OraclePublisher and AttestationPublisher to sign DOGE transactions; StateAnchorPublisher borrows the same hooks via _resolveSigner(). Optional: without it the publishers stay idle. Set-but-unloadable throws at startup (fail loudly). Falls back to setWalletSignHook / setBroadcastHook if the module is not provided.

Cross-Chain

Variable Required Default Description
ATTESTATION_TIMEOUT No 60000 Cross-chain attestation consensus timeout (ms)
CROSS_CHAIN_INDEXER_TIMEOUT No 15000 HTTP timeout (ms) for the hub’s federation-read calls to indexers during cross-chain verification
XCALL_POLL_MS No 15000 Poll cadence of the cross-chain call relay
XCALL_RELAY_MARGIN_BLOCKS No 4 Margin, in blocks of the gating chain, stamped onto every relayed row’s effective_time. Sized by that chain’s nominal block interval.
XDEX_POLL_MS No 15000 Poll cadence of the cross-chain DEX settlement engine
XDEX_MIN_CONFIRMATIONS No (per-coin config) Flat confirmation floor for cross-chain DEX settlement, overriding the per-coin values
XDEX_MIN_CONFIRMATIONS_<COIN> No per-coin (BTC 6, LTC 12, DOGE 60) Per-coin confirmation depth override (e.g. XDEX_MIN_CONFIRMATIONS_DOGE). Takes precedence over the flat XDEX_MIN_CONFIRMATIONS variable. Consensus-affecting.
XCHAIN_CONFIRMATIONS_<COIN> No per-coin Cross-chain attestation/swap confirmation depth for <COIN> (e.g. XCHAIN_CONFIRMATIONS_BTC), read by the cross-chain and cross-chain-call engines. Also resolves from p2pConfig and falls back to per-coin defaults; the env var is the highest-precedence override. Consensus-affecting.
XCHAIN_ATTEST_FINALIZED_MAX No 10000 Cap on retained finalized cross-chain attestation records held in memory

Regtest-only seams. Both engines honour these only when the hub’s network is regtest, and read them as NaN/false everywhere else, so a stray environment variable or config row can never reach the signed snapshot anchor or seed a validator on mainnet or testnet. They deliberately share names between the DEX and XCALL engines so a no-BTC regtest stack is configured once.

Variable Required Default Description
XDEX_SNAPSHOT_BLOCK No None Pin the snapshot anchor block on a regtest stack that has no BTC chain
XDEX_SEED_LOCAL_VALIDATOR No None Set to 1 to seed the local hub as a validator on a single-node regtest stack

Genesis and Regtest Binding

Regtest-only genesis overrides, ignored on mainnet and testnet, which always use the frozen bundled values. Consensus-relevant: they bind the local chain’s genesis anchor.

Variable Required Default Description
XCHAIN_GENESIS_BLOCK Regtest only per-coin Genesis block height for a regtest chain.
XCHAIN_GENESIS_LEDGER_HASH Regtest only per-coin Genesis ledger-hash pin for a regtest chain.
XCHAIN_GENESIS_DUMP_HASH Regtest only per-coin Genesis dump-hash pin for a regtest chain.

Fee Destination Override

Variable Required Default Description
XCHAIN_FEE_DESTINATION_<COIN>_<NETWORK> Regtest only bundled Overrides the native-fee destination address for <COIN> on <NETWORK>. Honored on regtest only: on mainnet and testnet it is ignored (and logged), because the fee destination is consensus-pinned and an env override would escape the freeze and fork the block-hashed ledger.

Reorg

Variable Required Default Description
REORG_TIMEOUT No 60000 Reorg consensus timeout (ms)
REORG_MAX_LOOKBACK_MS No 86400000 How far back (24 hours) a reorg may be considered
REORG_MAX_PENDING No 64 Maximum concurrently pending reorg records, bounding memory
REORG_TIMESTAMP_SKEW_MS No 10800000 Tolerated clock skew (3 hours) when comparing reorg block timestamps
REORG_ALLOW_UNRECORDED_OLDHASH No (unset, abstain) Set to 1 to co-sign a reorg whose recorded orphaned block hash is null (unrecorded), so its claimed old hash cannot be verified against local history. Off by default: the hub abstains from such rounds rather than co-sign a claim it cannot check. An escape hatch for operators knowingly running against history with unrecorded orphan hashes, at the cost of co-signing unverified claims.

Governance

Variable Required Default Description
GOV_VOTING_PERIOD No 604800000 Governance voting period in milliseconds (default: 7 days)
GOVERNANCE_TALLY_INTERVAL No 60000 Interval between governance tally sweeps

Database Schema

The hub uses 20 MariaDB tables, auto-created on startup from src/sql/:

Config Storage

Table Purpose
configs Service config parameters: (coin, network, module, param_name, param_value)

Unique constraint on (coin, network, module, param_name) for upsert behavior.

Validator Management

Table Purpose
validators Active validators: (signing_pubkey, addr, status, chains): capabilities are derived from each pubkey’s aggregate stake, not stored here (the tier column was dropped in the capability-staking refactor)
consensus_state PBFT sequence number persistence
p2p_peers Known P2P peers and last-seen timestamps

Oracle

Table Purpose
oracle_submissions Raw per-validator price submissions per round: (round_number, coin_pair, validator_pubkey, price)
price_snapshots Finalized/skipped/disputed price snapshots: (round_number, coin_pair, price, status, consensus_proof)
oracle_prices User-published PRICE v1 oracle prices: (source_address, coin, tick, fiat, value, effective_at) with 24-hour delay on updates

Cross-Chain

Table Purpose
attestations Cross-chain attestation records: (attestation_id, source_chain, source_action_index, dest_chain, status, consensus_proof): status: pending, attested, rejected, expired
swap_records SWAP lifecycle tracking: (source_chain, source_action_index, dest_chain, dest_action_index, status)
reorg_attestations Confirmed blockchain reorg events: (chain, reorg_height, timestamp, consensus_proof)
cross_chain_matches Cross-chain DEX match records mirrored across the federation and to indexers via hub DB sync
cross_chain_calls Cross-chain contract call relay rows (XCALL dispatch + result) mirrored to indexers via hub DB sync

State Checkpoints and Capability Snapshots

Table Purpose
state_checkpoints Quorum-signed per-chain ledger/actions/contract hash checkpoints produced by StateCheckpointEngine; streamed to indexers via hub DB sync and committed on-chain via ANCHOR
capability_snapshots Block-boundary per-capability validator-set snapshots locked by CapabilitySnapshot for deterministic quorum; mirrored to indexers

Governance

Table Purpose
governance_proposals Parameter change proposals: (parameter, current_value, proposed_value, rationale, proposer, status)
governance_votes Validator votes: (proposal_id, signing_pubkey, vote, signature)

Rewards and Slashing

Table Purpose
validator_rewards Per-round validator rewards: (validator_pubkey, round_number, reward_type, amount, block_index, batch_seq, claimed): reward_type distinguishes oracle_round, attest_fee, anchor_<chain> etc.; batch_seq links anchor-publish batch rows; block_index pins the earn block
slash_proposals Detected validator offenses: (signing_pubkey, offense_type, evidence, round_number)

Telemetry

Table Purpose
telemetry_pings Anonymous node-operator usage pings: (install_id, hub_version, services, os_info, country, region, ip_hash): raw IP is never stored

Capability Registry

Table Purpose
validator_capabilities Per-pubkey capability activation/deactivation records written by CapabilityRegistry

Config Table Detail

Column Type Description
coin VARCHAR(16) Coin identifier (BTC, LTC, DOGE)
network VARCHAR(16) Network (mainnet, testnet, regtest)
module VARCHAR(64) Service name (xchain-decoder, xchain-indexer, etc.)
param_name VARCHAR(32) Parameter name (host, port, db_host, db_port, name, user, pass, service_port)
param_value TEXT Parameter value
updated_at TIMESTAMP Last update timestamp

Config is served as a nested object: { coin: { network: { module: { param: value } } } }.

Connection Pool

Parameter Value Description
connectionLimit 10 Maximum simultaneous connections
connectTimeout 10000 Connection timeout (ms); override with DB_CONNECT_TIMEOUT
acquireTimeout 10000 Time to wait for a free pooled connection (ms); override with DB_ACQUIRE_TIMEOUT
queryTimeout 30000 Query execution timeout (ms); override with DB_QUERY_TIMEOUT
idleTimeout 60000 Idle connection timeout (ms)

DB_CONNECT_TIMEOUT, DB_ACQUIRE_TIMEOUT, and DB_QUERY_TIMEOUT are read by the indexer’s pool with the same names and the same defaults.

Circuit Breaker

Parameter Value Description
Threshold 10 Consecutive failures before opening the circuit
Cooldown 30000 Milliseconds before attempting a half-open retry
Max retries 30 Maximum retry attempts with backoff
Backoff range 500ms–15s Delay range with jitter

When the circuit opens, all database queries fail fast until the cooldown period expires. Retries use exponential backoff with jitter to prevent thundering herd.

Validator Identity

Ed25519 keys are used for P2P message signing and verification:

  • Private key: 32-byte seed from SIGNING_PRIVKEY_SECRET (64 hex chars), wrapped in PKCS8 DER for Node.js crypto.
  • Public key: extracted as raw 32-byte SPKI, stored as 64 hex chars.
  • Signing: canonical payload is JSON with sorted fields (id, type, sender, timestamp, data).
  • Generation: ValidatorIdentity.generate() produces a random keypair.
const { ValidatorIdentity } = require('./src/ValidatorIdentity');
const { privkey, pubkey } = ValidatorIdentity.generate();
// privkey: 64-char hex string for SIGNING_PRIVKEY_SECRET
// pubkey:  64-char hex string for registervalidator

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.

Edit this page on GitHub ↗