XChain Platform Hub: Architecture
Position in the Data Pipeline
flowchart TD
NODE["Coin Node<br>(bitcoind / litecoind / dogecoind)"]
DECODER["xchain-decoder"]
DECDB[("Decoder DB (MariaDB)")]
INDEXER["xchain-indexer"]
IDXDB[("Indexer DB (MariaDB)")]
EXPLORER["xchain-explorer"]
SYNC["xchain-sync"]
REPLICAS["Validator replicas"]
HUB["xchain-hub<br>- Config oracle (all services poll)<br>- Price oracle (CoinGecko, Kraken, CoinMarketCap)<br>- Cross-chain attestation<br>- SWAP lifecycle tracking<br>- Reorg propagation<br>- Governance"]
P2P["P2P Validator Network"]
NODE -->|JSON-RPC| DECODER
DECODER --> DECDB
DECDB --> INDEXER
INDEXER --> IDXDB
IDXDB --> EXPLORER
IDXDB --> SYNC
SYNC --> REPLICAS
HUB --> SYNC
HUB <--> P2P
The hub sits at the center of the platform. All other services depend on it for configuration discovery. In validator mode, it also serves as the decentralized coordination layer for pricing, cross-chain operations, and governance.
Operating Modes
flowchart LR
subgraph STANDALONE["STANDALONE MODE<br>(P2P_VALIDATOR_ADDR not set)"]
direction TB
SA_API["api.js<br>Express + JSON-RPC"]
SA_HUB["XChainHub<br>(orchestrator)"]
SA_DB["db.js<br>MariaDB pool + circuit breaker"]
SA_NOTE["Config writes go directly to MariaDB.<br>No P2P, no consensus."]
SA_API --> SA_HUB --> SA_DB
SA_DB -.-> SA_NOTE
end
subgraph VALIDATOR["VALIDATOR MODE<br>(P2P_VALIDATOR_ADDR set)"]
direction TB
VA_API["api.js<br>Express + JSON-RPC"]
VA_HUB["XChainHub<br>(orchestrator)"]
VA_PEER["PeerManager<br>P2P gossip layer"]
VA_CONS["Consensus"]
VA_ORACLE["OracleRound"]
VA_CROSS["CrossChainEngine"]
VA_REORG["ReorgHandler"]
VA_GOV["Governance"]
VA_REWARD["RewardTracker"]
VA_SLASH["SlashDetector"]
VA_SWAP["SwapTracker"]
VA_DB["db.js<br>MariaDB pool + circuit breaker"]
VA_API --> VA_HUB --> VA_PEER
VA_PEER --> VA_CONS --> VA_GOV
VA_PEER --> VA_ORACLE --> VA_REWARD
VA_PEER --> VA_CROSS --> VA_SLASH
VA_PEER --> VA_REORG --> VA_SWAP
VA_GOV --> VA_DB
VA_REWARD --> VA_DB
VA_SLASH --> VA_DB
VA_SWAP --> VA_DB
end
In standalone mode, the hub is a simple config oracle. In validator mode, the XChainHub orchestrator wires together all subsystems via event-driven architecture.
Internal Components
Event-Driven Wiring
Subsystems communicate via Node.js EventEmitter events rather than direct method calls:
flowchart TD
ORACLECONS["OracleConsensus"]
REWARD["RewardTracker"]
SLASH["SlashDetector"]
ORACLEPUB["OraclePublisher<br>(queues for DOGE broadcast)"]
PRICEAGG["PriceAggregator"]
CHECKPOINT["StateCheckpointEngine"]
DEXENGINE["CrossChainDexEngine"]
CALLENGINE["CrossChainCallEngine"]
BROADCASTER["HubDbBroadcaster<br>(forwards to WebSocket subscribers)"]
ANCHORPUB["StateAnchorPublisher<br>(queues for DOGE ANCHOR broadcast)"]
CROSSCHAIN["CrossChainEngine"]
SWAP["SwapTracker"]
REORG["ReorgHandler"]
DOWNSTREAM["downstream indexer notification"]
GOV["Governance"]
PARAMAPPLY["parameter application"]
ORACLECONS -->|round:finalized| REWARD
ORACLECONS -->|round:finalized| SLASH
ORACLECONS -->|round:finalized| ORACLEPUB
PRICEAGG -->|row:inserted| BROADCASTER
CHECKPOINT -->|row:inserted| BROADCASTER
DEXENGINE -->|row:inserted| BROADCASTER
CALLENGINE -->|row:inserted| BROADCASTER
CHECKPOINT -->|checkpoint:finalized| ANCHORPUB
CROSSCHAIN -->|attestation:finalized| SWAP
REORG -->|reorg:confirmed| DOWNSTREAM
GOV -->|proposal:passed| PARAMAPPLY
Source Files
| File | Class/Module | Role |
|---|---|---|
api.js |
None | Entry point: Express app, JSON-RPC routes, env var validation, starts XChainHub |
XChainHub.js |
XChainHub |
Orchestrator: wires all subsystems, exposes JSON-RPC method handlers |
db.js |
Database |
MariaDB connection pool with circuit breaker and exponential backoff |
PeerManager.js |
PeerManager |
WebSocket P2P gossip layer: peer connections, message signing, heartbeats |
Consensus.js |
Consensus |
PBFT consensus for config writes: PRE_PREPARE → PREPARE → COMMIT |
ValidatorIdentity.js |
ValidatorIdentity |
Ed25519 key management: signing, verification, key generation |
OracleRound.js |
OracleRound |
Oracle round lifecycle: timer, price fetching, submission broadcast |
OracleConsensus.js |
OracleConsensus |
PBFT consensus for price finalization: trimmed median, propose/prepare/commit |
PriceFetcher.js |
PriceFetcher |
External price API client: CoinGecko and Kraken (both keyless, always active) plus CoinMarketCap (optional, when COINMARKETCAP_API_KEY is set) |
CrossChainEngine.js |
CrossChainEngine |
PBFT attestation for cross-chain actions with per-chain-pair validators |
SwapTracker.js |
SwapTracker |
Cross-chain SWAP lifecycle tracking: initiated → attested → executed → settled |
ReorgHandler.js |
ReorgHandler |
Blockchain reorg detection, PBFT consensus, and hub state rollback |
Governance.js |
Governance |
Off-chain PBFT voting for parameter changes |
RewardTracker.js |
RewardTracker |
Per-round XCHAIN reward distribution to oracle participants; pushes rewards to BTC indexer for COLLECT |
SlashDetector.js |
SlashDetector |
Validator misbehavior detection: price deviation, non-participation |
PriceAggregator.js |
PriceAggregator |
Receives validated PRICE v0/v1 actions from indexers, deduplicates by round_number (v0) or (source, action_index) (v1), writes to price_snapshots/oracle_prices. EventEmitter: emits row:inserted for hub DB sync. |
OraclePublisher.js |
OraclePublisher |
oracle_publish capability publisher: deterministic leader rotation, persistent JSONL queue, builds PRICE v0 wire format, broadcasts to DOGE via the encoder pipeline, monitors DOGE balance |
EncoderClient.js |
EncoderClient |
Minimal JSON-RPC client for talking to xchain-encoder (get_utxos, create_tx, broadcast_tx): used by OraclePublisher |
HubDbBroadcaster.js |
HubDbBroadcaster |
WebSocket subscriber registry; broadcasts row:inserted events from PriceAggregator, StateCheckpointEngine, CrossChainDexEngine, and CrossChainCallEngine to all connected indexers’ HubDbSync clients |
StateCheckpointEngine.js |
StateCheckpointEngine |
Quorum-signed per-chain ledger/actions/contract hash checkpoints: cadence-leader reads each chain’s block-hash triple, collects XCHK_SIGN from peers, finalizes at 2f+1 signatures, writes to state_checkpoints, streams via HubDbBroadcaster, emits checkpoint:finalized |
StateAnchorPublisher.js |
StateAnchorPublisher |
Per-chain publisher-election anchor: listens for checkpoint:finalized, batches cross_chain_matches archive, and commits checkpoints + archive on-chain via the DOGE ANCHOR action on ANCHOR_INTERVAL_MS cadence |
FullNodeChallengeRound.js |
FullNodeChallengeRound |
Challenge-response rounds that verify full_node capability claimants. The elected leader issues a block-hash challenge; each claimant broadcasts its computed answer (XNODE_ANSWER); the leader proposes the pass list (XNODE_SIGN_REQ); verifiers independently recompute and co-sign (XNODE_SIGN); results are finalized on-chain via XNODE_DONE. Pass rate feeds into the full-node reward tier. |
AttestationPublisher.js |
AttestationPublisher |
Subscribes to AttestationConsensus request:finalized events and ships the on-chain ATTEST v1 (response) wire payload via an operator-provided hook. Writes a durable JSONL write-ahead log before any broadcast; the leader broadcasts immediately, followers step in after failoverWindowBlocks blocks using a rank-staggered backoff. |
AttestationRound.js |
AttestationRound |
Event-driven per-request lifecycle for the external attestation framework. Polls the indexer for new ATTEST v0 (request) rows, selects the responsible validator set via SHA-256 ordering at block_index, fetches the payload via the provider module, and gossips ATTEST_PROPOSE for AttestationConsensus to drive to quorum. |
AttestationSpotChecker.js |
AttestationSpotChecker |
Spot-checker for synthetic ATTEST v0 requests injected to verify validator honesty. When AttestationConsensus finalizes a round, compares the published response against the expected pattern using a provider’s judge_model comparator. Repeated failures within a 24-hour window trigger a slash proposal via SlashDetector. |
CapabilityRegistry.js |
CapabilityRegistry |
Tracks per-validator capability state in the validator_capabilities table. A capability is active when all three conditions hold: qualified (stake >= configured MIN_STAKE), self_test_ok (local self-test passed), and enabled (operator has not opted out). Hot-reloads the capability config file on change. |
CapabilitySnapshot.js |
CapabilitySnapshot |
Locks the validator set for a capability at a block boundary so every hub in the federation computes the same PBFT quorum for a given round. Queries the BTC indexer at the target blockIndex; stake state at a given block is on-chain-deterministic, making the snapshot cross-hub identical. Self-test and enabled flags are excluded (those are local per hub). |
CrossChainDexEngine.js |
CrossChainDexEngine |
Matches cross-chain ORDER/SWAP offers across chain-isolated indexer order books. Polls each chain’s getopencrosschainorders RPC, pairs compatible offers, drives PBFT finalization via CrossChainDexConsensus, writes validator-signed match rows to cross_chain_matches, and broadcasts them to indexers via HubDbBroadcaster. |
CrossChainDexConsensus.js |
CrossChainDexConsensus |
PBFT consensus engine for cross-chain DEX match finalization. Each peer independently re-derives and validates the canonical match before co-signing. Drives a 3-phase PBFT round (PROPOSE, PREPARE, COMMIT) with VIEW_CHANGE / NEW_VIEW leader failover. Reused as the base engine for CrossChainCallEngine with parameterized message types. |
ProviderRegistry.js |
ProviderRegistry |
Hub-authoritative registry of governance-approved attestation providers. Loads provider definitions from the configs table under module='ATTESTATION_PROVIDER'; falls back to a built-in http_get default so a fresh hub works without prior governance configuration. Hot-reloads on governance proposal:passed events. |
constants.js |
None | Shared protocol/consensus constants for the oracle price pipeline. Exports PRICE_MAX (the per-pair price ceiling enforced during ingestion and aggregation) and ORACLE_DEVIATION_THRESHOLD (used by OracleConsensus). |
bcmath.js |
None | Big-number helpers for cross-chain DEX partial-fill matching. A faithful port of the bignumber utilities in xchain-indexer (mathjs bignumber, bcmul at precision 18, bcsub at precision 64) so hub match quantities stay byte-identical to the indexer’s local fill math. |
stake_weighted_quorum.js |
None | Consensus-critical stake-weighted quorum predicate (WI-1). Vendored byte-identically from xchain-documentation/protocol/reference-impl/ into the hub and every service that tallies PBFT votes or verifies settlement gates. A CI gate (ConsensusPrimitiveConformance) asserts byte-identity across all repos. |
equivocation_header.js |
None | Consensus-critical equivocation header builder (WI-2). Exports the key, prefix, and canonical builders only; consumers match the literal `EQUIV |
checkpoint_commitment_activation.js |
None | Flag-day gate for the SPV light-client checkpoint-commitment (Phase 2). At and above the activation snapshot_block, the checkpoint signing string gains STATE_ROOT and BLOCK_MERKLE_ROOT fields and ANCHOR v3 carries them on DOGE. Gates on the BTC-anchored snapshot_block so the hub and all indexers flip the signed shape on the same anchor. |
sql/*.sql |
None | MariaDB table schemas (configs, validators, price_snapshots, oracle_prices, state_checkpoints, capability_snapshots, cross_chain_matches, cross_chain_calls, validator_rewards, governance, telemetry_pings, etc.) |
P2P Gossip Layer
The PeerManager provides the transport layer for all consensus protocols.
flowchart LR
subgraph A["Validator A"]
direction TB
PMA["PeerManager"]
SUBA["Consensus<br>Oracle<br>CrossChain<br>Governance"]
PMA -->|message events| SUBA
SUBA -->|message events| PMA
end
subgraph B["Validator B"]
direction TB
PMB["PeerManager"]
SUBB["Consensus<br>Oracle<br>CrossChain<br>Governance"]
PMB -->|message events| SUBB
SUBB -->|message events| PMB
end
PMA -->|WebSocket outbound| PMB
PMB -->|WebSocket inbound| PMA
Message Flow
- Subsystem calls
peerManager.broadcast(type, data)(orsendToPeer(addr, type, data)for a directed message). - PeerManager wraps
datain an envelope, assigns a unique ID, signs with Ed25519 (if identity configured), and adds toseenIdsdedup cache. - Message sent to all connected peers.
- Receiving PeerManager checks
seenIds, drops duplicates. - Verifies Ed25519 signature against registered validator pubkeys (if
REQUIRE_SIGNATURES=true). - Emits
messageevent; relays to other peers (flood-fill gossip).
Connection Management
- Outbound connections to
SEED_NODESwith exponential backoff (2s base, 60s max). - Inbound connections on
P2P_PORT(default 10001). - Deduplicates bidirectional connections (if both A→B and B→A connect, one is dropped).
- Heartbeat broadcasts every 15 seconds (includes hub software version for upgrade coordination).
- WS ping/pong every 30 seconds to detect dead connections.
- Peer records persisted to
p2p_peerstable.
Envelope Wire Format
Every message on the gossip layer (regardless of which subsystem produced it) is a single JSON object with the same envelope shape. The subsystem-specific payload lives entirely inside data; everything else is transport metadata.
{
"type": "CAPABILITY_ACTIVATED",
"id": "v1:<sender-addr>:1743690000000:550e8400-e29b-41d4-a716-446655440000",
"sender": "<originating-validator-addr>",
"timestamp": 1743690000000,
"data": { },
"sig": "<128-hex-char Ed25519 signature>"
}
| Field | Type | Description |
|---|---|---|
type |
string |
Message type (see table below). Required; messages without a string type are dropped. |
id |
string |
Globally unique message ID, format v1:<sender-addr>:<unix-ms>:<uuid>. Used for flood-fill deduplication. Required. |
sender |
string |
Address of the original publisher (does not change as the message is relayed). Required. |
timestamp |
number |
Unix epoch milliseconds when the envelope was built. Required (must be a number). |
data |
object |
Subsystem-specific payload. Defaults to {} when omitted by the sender. |
sig |
string |
Optional Ed25519 signature, hex-encoded. Present when the sender has a configured validator identity. |
Signature canonicalization. The signature covers a deterministic JSON serialization of exactly five fields, in this fixed key order (id, type, sender, timestamp, data) with sig itself excluded:
JSON.stringify({ id, type, sender, timestamp, data })
A verifier must reconstruct this exact string to check the signature. The signing key is the sender’s Ed25519 validator key; the verifier looks up sender in the validator registry to obtain the 64-hex-char public key. When REQUIRE_SIGNATURES=true, unsigned messages and messages from unknown senders are rejected; otherwise they are accepted (bootstrap mode).
Inbound processing order (in _handleInbound): JSON parse → reject non-object/array values → validate type/id/sender/timestamp → self-connection guard (drop messages whose sender is this node) → dedup against seenIds → per-peer rate limit → signature verification → emit message (and type-specific events) → relay to all peers except the source connection and the original sender.
Message Types
All types below ride the envelope above; only the data payload differs. Every node emits a generic message event for any valid inbound envelope; some types additionally emit a typed event.
Liveness:
| Type | data shape |
Purpose |
|---|---|---|
HEARTBEAT |
{ "version": "<hub-version>" } |
Broadcast every P2P_HEARTBEAT_INTERVAL (default 15s). Carries the hub software version for upgrade coordination; emits a heartbeat event with (sender, timestamp). |
Capability gossip (advertises which staking-backed capabilities a validator is running; emits a capability event). The receiver additionally requires that data.pubkey match the sender’s registered validator pubkey; a validator cannot advertise capabilities on behalf of another pubkey.
| Type | data shape |
Purpose |
|---|---|---|
CAPABILITY_ACTIVATED |
{ "pubkey": "<hex>", "capability": "<name>", "block_at": <block-index> } |
Sender advertises that it has activated capability (one of price, cross_chain, oracle_publish, attestation, full_node). The receiver verifies the stake-backed qualification against the indexer stake snapshot at block_at before trusting it; if the snapshot is unavailable it falls back to accepting for liveness. |
CAPABILITY_DEACTIVATED |
{ "pubkey": "<hex>", "capability": "<name>", "block_at": <block-index>, "reason": "<string, optional>" } |
Sender reports that capability is no longer active (failed self-test or lost qualification). reason carries the self-test failure message when present. |
CAPABILITY_SELF_TEST |
{ "pubkey": "<hex>", "capability": "<name>", "ok": <bool>, "reason": "<string, optional>" } |
Carries a capability self-test result (ok true/false, with optional reason on failure). Recognized and applied on receipt to the local capability registry. |
Consensus and coordination (carried over the same gossip layer; payloads are defined by their respective engines):
| Type | Producer | Purpose |
|---|---|---|
PBFT_PRE_PREPARE / PBFT_PREPARE / PBFT_COMMIT |
Consensus |
Three-phase PBFT for config writes. |
PBFT_VIEW_CHANGE / PBFT_NEW_VIEW |
Consensus |
Leader view-change protocol. |
ORACLE_PRICE_SUBMIT |
OracleRound |
Per-round price submission for oracle aggregation. |
ORACLE_PROPOSE / ORACLE_PREPARE / ORACLE_COMMIT |
OracleConsensus |
PBFT finalization of the trimmed-median price. |
ATTEST_PROPOSE / ATTEST_PREPARE / ATTEST_COMMIT |
AttestationConsensus |
PBFT-style consensus over external attestation responses. |
XCHAIN_ATTEST_PROPOSE / XCHAIN_ATTEST_PREPARE / XCHAIN_ATTEST_COMMIT |
CrossChainEngine |
Consensus over cross-chain action confirmations. |
XCALL_RELAY_PROPOSE / XCALL_RELAY_PREPARE / XCALL_RELAY_COMMIT / XCALL_RELAY_VIEW_CHANGE / XCALL_RELAY_NEW_VIEW / XCALL_RELAY_FINAL_SYNC |
CrossChainCallEngine |
PBFT consensus to quorum-sign cross-chain contract call relay rows (cross_chain_calls). Reuses the DEX consensus engine with parameterized message types. |
XCHK_SIGN_REQ / XCHK_SIGN / XCHK_FINALIZED |
StateCheckpointEngine |
Collect 2f+1 validator signatures over per-chain ledger/actions/contract hash checkpoints. |
XANC_SIGN_REQ / XANC_SIGN / XANC_FINALIZED / XANC_V0_DONE |
StateAnchorPublisher |
Co-sign the on-chain ANCHOR payload (checkpoints + archive). XANC_V0_DONE back-fills peers’ anchor_txid to prevent duplicate anchoring. |
XNODE_ANSWER / XNODE_SIGN_REQ / XNODE_SIGN / XNODE_DONE |
FullNodeChallengeRound |
Full-node challenge-response protocol. Claimants broadcast their computed answer (XNODE_ANSWER); the elected leader proposes the pass list (XNODE_SIGN_REQ); eligible verifiers co-sign after recomputing independently (XNODE_SIGN); the leader finalizes and broadcasts results (XNODE_DONE). |
Unrecognized types are still deduplicated, signature-checked, and relayed (so the gossip mesh forwards message types a given node may not handle), but produce no local side effect beyond the generic message event.
PBFT Consensus
The consensus engine implements simplified PBFT for config writes:
sequenceDiagram
participant L as Leader
participant V as Validators (2f+1 required)
L->>V: PRE_PREPARE
Note right of V: Leader proposes config write
V-->>L: PREPARE
Note left of L: Validators acknowledge (collect 2f+1)
L->>V: COMMIT
Note right of V: Leader broadcasts commit
V-->>L: COMMIT
Note left of L: Validators confirm (collect 2f+1)
Note over L,V: Apply config to MariaDB
Leader Selection
Leader for sequence N = validatorSet[(N + view) % validatorCount], where validators are sorted by pubkey.
View Change
If the leader fails to drive consensus within PBFT_TIMEOUT (default 30s):
- Validators broadcast
PBFT_VIEW_CHANGEforview + 1. - Once 2f+1 view-change votes are collected, the new view is adopted.
- The next leader (per the new view number) takes over.
Quorum
max(2f+1, ceil((N+1)/2)) where f = floor((N-1)/3), tolerates f Byzantine validators
out of N total. The simple-majority floor matters for small federations: bare 2f+1
degenerates to a quorum of 1 at N=3 (f=0), which would let a single validator finalize
alone. With the floor, N=3 requires 2 votes and N=2 requires both.
Oracle Pipeline
flowchart TD
subgraph ROUND["Every ORACLE_ROUND_INTERVAL (default 10 min)"]
S1["1. CHAIN TIP<br>OracleRound reads BTC chain tip from configs table<br>→ currentBtcBlockHeight, currentBtcBlockTime"]
S2["2. FETCH<br>PriceFetcher queries CoinGecko + Kraken (keyless) + CoinMarketCap (if key set)<br>→ 3 coins x 12 fiats = 36 pairs per source<br>→ compute local median across sources"]
S3["3. SUBMIT<br>Broadcast ORACLE_PRICE_SUBMIT via gossip<br>→ stored in oracle_submissions table"]
S4["4. COLLECT<br>Wait ORACLE_SUBMISSION_WINDOW (default 3 min)<br>→ accumulate other validators' submissions"]
S5["5. AGGREGATE<br>Round leader computes trimmed median:<br>→ sort submissions, discard top/bottom 15%<br>→ median of remaining values"]
S6["6. SIGN<br>Each validator signs the canonical PRICE v0 payload<br>→ JSON.stringify({round, timestamp, sortedPairs})<br>→ Ed25519 via ValidatorIdentity"]
S7["7. PROPOSE<br>Leader broadcasts ORACLE_PROPOSE (with sig)"]
S8["8. PREPARE<br>Validators verify and send ORACLE_PREPARE (with their sig)<br>→ sigs stored on pending.signatures Map<br>→ collect 2f+1 prepares"]
S9["9. COMMIT<br>Leader broadcasts ORACLE_COMMIT (with sig)<br>→ collect 2f+1 commits"]
S10["10. FINALIZE<br>Store in price_snapshots (status='finalized')<br>→ reference_block = btcBlockHeight (not 0)<br>→ emit round:finalized event with collected sigs<br>→ RewardTracker distributes XCHAIN (pushes to BTC indexer)<br>→ SlashDetector checks for misbehavior<br>→ OraclePublisher queues for DOGE broadcast (if leader)"]
S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7 --> S8 --> S9 --> S10
end
oracle_publish Capability Publishing Pipeline
After consensus finalizes a round, the OraclePublisher takes over:
flowchart TD
S1["1. ROTATION<br>Leader = SHA256(round_number || pubkey) ordering<br>oracle_publish validators sorted deterministically"]
S2["2. ENQUEUE<br>If local node is the leader, append to JSONL queue (fsync)"]
S3["3. BROADCAST<br>EncoderClient.getUtxos(DOGE_ADDRESS)<br>→ EncoderClient.createTx(payload, P2SH encoding)<br>→ walletSignFn(psbtHex) [operator-provided signer]<br>→ EncoderClient.broadcastTx(signedHex)"]
S4["4. CONFIRM<br>Remove from queue on successful broadcast"]
S5["5. FAILOVER<br>If leader misses by 1 BTC block, next oracle_publish validator in rotation takes over and batches all missed rounds in a single tx"]
S1 --> S2 --> S3 --> S4 --> S5
Price Sources
| Source | Coverage | Requires API Key |
|---|---|---|
| CoinGecko | 3 coins x 12 fiats (single API call via vs_currencies) |
Optional (rate limits apply without a key) |
| Kraken | Subset of the 36 pairs that Kraken lists for these three coins (pairs not listed fall back to CoinGecko) | No (public ticker, always active) |
| CoinMarketCap | 3 coins x 12 fiats (single API call via convert) |
Yes (COINMARKETCAP_API_KEY) |
Supported fiat currencies: USD, CAD, AUD, MXN, GBP, JPY, CNY, CHF, BRL, INR, EUR, KRW.
CoinGecko and Kraken are both keyless, so every hub has two uncorrelated upstreams out of the box. CoinMarketCap is added as a third source only when COINMARKETCAP_API_KEY is configured. The local price for each pair is the median of values from all available sources.
Hub DB Sync Channel
For geographically distributed deployments, the hub broadcasts row inserts to indexers’ local hub DB copies via a WebSocket channel separate from the per-chain sync.
flowchart LR
subgraph HUB["Hub"]
direction TB
PA["PriceAggregator<br>receiveValidatedRound/OraclePrice"]
EMIT["emit row:inserted"]
HDB["HubDbBroadcaster<br>WebSocket subs"]
PA --> EMIT --> HDB
end
subgraph IDX["Indexer (HubDbSync client)"]
direction TB
XI["XChainIndexer<br>- hubDb (local)<br>- HubDbSync"]
APPLY["Apply row to local hub DB<br>via INSERT IGNORE"]
XI --> APPLY
end
HDB -->|ws send| XI
REST Bootstrap Endpoints
| Endpoint | Returns |
|---|---|
GET /hub-db/snapshot/price_snapshots?since_id=N&limit=10000 |
Rows from price_snapshots table after since_id |
GET /hub-db/snapshot/oracle_prices?since_id=N&limit=10000 |
Rows from oracle_prices table after since_id |
GET /hub-db/snapshot/cross_chain_matches?since_id=N&limit=10000 |
Rows from cross_chain_matches after since_id; retracted rows excluded |
GET /hub-db/snapshot/capability_snapshots?since_id=N&limit=10000 |
Rows from capability_snapshots after since_id |
GET /hub-db/snapshot/cross_chain_calls?since_id=N&limit=10000 |
Rows from cross_chain_calls after since_id; retracted rows excluded |
GET /hub-db/snapshot/state_checkpoints?since_id=N&limit=10000 |
Rows from state_checkpoints after since_id |
WebSocket Channel
| Path | Auth | Messages |
|---|---|---|
/hub-db/subscribe |
Authorization: Bearer <HUB_API_KEY> |
{type: 'row:inserted', table, row} per inserted row |
Indexers bootstrap by fetching the REST snapshots (paginated by since_id) then subscribe to the WebSocket for live updates. Failed sends apply backpressure handling, connections exceeding WS_BACKPRESSURE_LIMIT buffered messages are dropped.
Trimmed Median
Given N validator submissions for a coin pair:
- Sort all submissions by price.
- Discard the top 15% and bottom 15%.
- Compute the median of the remaining values.
This resists manipulation: an attacker would need to control >30% of validators to significantly shift the price.
Cross-Chain Attestation
flowchart TD
S1["1. REQUEST<br>requestattestation(source_chain, source_action_index, dest_chain)"]
S2["2. PROPOSE<br>Leader broadcasts XCHAIN_ATTEST_PROPOSE<br>→ includes attestation_id: '{source_chain}:{action_index}:{dest_chain}'"]
S3["3. PREPARE<br>Validators send XCHAIN_ATTEST_PREPARE<br>→ only validators supporting BOTH chains participate<br>→ collect 2f+1 from eligible validator subset"]
S4["4. COMMIT<br>Leader broadcasts XCHAIN_ATTEST_COMMIT<br>→ collect 2f+1 commits"]
S5["5. FINALIZE<br>Store in attestations table (status='attested')<br>→ emit attestation:finalized event<br>→ SwapTracker auto-progresses matching swaps"]
S1 --> S2 --> S3 --> S4 --> S5
Confirmation Thresholds
Higher on the lower-hashpower chains to approach Bitcoin-comparable settlement assurance. Per-chain, configurable via XCHAIN_CONFIRMATIONS_<COIN>.
| Chain | Required Confirmations |
|---|---|
| Bitcoin | 6 |
| Litecoin | 12 |
| Dogecoin | 60 |
Supported Chain Pairs
BTC-LTC, BTC-DOGE, LTC-DOGE.
Per-Chain-Pair Validators
Validators declare which chains they support via the chains column. Only validators supporting both chains in a pair participate in attestation consensus. Validators with NULL chains support all chain-pairs (backward compatible).
Reorg Handling
flowchart TD
S1["1. REPORT<br>reportreorg(chain, reorg_height, timestamp, old_hash, new_hash)<br>→ old_hash/new_hash = the reporter's observed block hash at reorg_height before and after the reorg<br>→ receiving hub verifies new_hash against its OWN indexer (getblockhashes) before broadcasting"]
S2["2. ALERT<br>Broadcast REORG_ALERT via gossip (carries the hash pair)"]
S3["3. CONSENSUS<br>PBFT round: XCHAIN_REORG_PREPARE / XCHAIN_REORG_COMMIT<br>→ the digest binds reorgId, chain, height, timestamp AND the hash pair, so a Byzantine leader cannot swap hashes per-follower<br>→ each hub co-signs ONLY after its own indexer serves new_hash at reorg_height, within height bounds (≤ own tip, ≥ tip - REORG_MAX_DEPTH) and on the federation network; otherwise it abstains<br>→ 2f+1 agreement = 2f+1 independent observations"]
S4["4. ROLLBACK<br>Hub state cleanup:<br>→ DELETE attestations after reorg timestamp for affected chain<br>→ Mark price_snapshots as 'disputed'"]
S5["5. NOTIFY<br>Store in reorg_attestations table<br>→ emit reorg:confirmed event"]
S1 --> S2 --> S3 --> S4 --> S5
Governance
flowchart TD
S1["1. PROPOSE<br>propose(parameter, current_value, proposed_value, rationale)<br>→ only active validators can propose<br>→ stored in governance_proposals (status='voting')"]
S2["2. GOSSIP<br>Broadcast GOV_PROPOSE via P2P"]
S3["3. VOTE<br>vote(proposal_id, vote) [approve/reject]<br>→ stored in governance_votes<br>→ broadcast GOV_VOTE via P2P"]
S4["4. TALLY<br>Automatic tally every 60 seconds:<br>→ check if voting period (7 days) has ended<br>→ quorum: 50% minimum participation<br>→ approval: 2/3+ of validator set<br>→ broadcast GOV_RESULT via P2P"]
S5["5. APPLY<br>If passed: emit proposal:passed event<br>→ downstream parameter application"]
S1 --> S2 --> S3 --> S4 --> S5
Constraints
| Rule | Value |
|---|---|
| Voting period | 7 days (GOV_VOTING_PERIOD) |
| Quorum | 50% of active validators |
| Approval threshold | 2/3+ of validator set |
| General param change bounds | Max +50% / −33% |
| Slashing param change bounds | Max +25% / −20% |
| Cooldown after rejection | 14 days before re-proposing same parameter |
Reward and Slash System
Rewards
On each finalized oracle round, ORACLE_REWARD_PER_ROUND (default “10.00000000”) XCHAIN is distributed equally among validators who submitted a price in that round. Rewards are recorded in the validator_rewards table with claimed=0 and are collectable via a COLLECT action on the BTC chain (handled by the indexer, not the hub).
Slash Detection
Three offense types are monitored:
| Offense | Trigger | Description |
|---|---|---|
price_deviation |
>5% from consensus | Submission deviates more than SLASH_DEVIATION_THRESHOLD from the finalized price |
repeated_deviation |
3+ in 24 hours | Three or more deviations within a rolling 24-hour window |
non_participation |
30+ missed rounds | SLASH_MISSED_ROUNDS_THRESHOLD consecutive rounds without a submission |
Detection is recorded in the slash_proposals table. Actual stake slashing is executed by the indexer, not the hub.
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.