XChain Platform Decoder: Database Schema
The decoder writes to a single MariaDB database. The indexer reads from this database (read-only from the indexer’s perspective).
Database Naming
XChain_{CHAIN}_{NETWORK}_Decoder
Examples: XChain_BTC_Mainnet_Decoder, XChain_LTC_Regtest_Decoder, XChain_DOGE_Testnet_Decoder
The database and all tables are auto-created on startup if they don’t exist. Schema definitions live in src/sql/*.sql.
Table Overview
| Table | Purpose | Primary Key |
|---|---|---|
blocks |
One row per parsed block | block_index |
transactions |
Confirmed XChain transactions | tx_index |
mempool_transactions |
Unconfirmed XChain transactions (raw strings, no FK ids) | tx_hash (unique) |
dispensers |
Open and soft-expired dispensers | (tx_index, address_id) |
transaction_outputs |
Outputs that paid to a dispenser address | (tx_index, vout) |
index_addresses |
Address string to integer ID lookup | id (auto-increment) |
index_transactions |
Transaction/block hash to integer ID lookup | id (auto-increment) |
events |
System events (reorgs, errors) | id (auto-increment) |
pubkeys |
Source-address public keys captured during parsing | address_id |
Core Tables
erDiagram
index_transactions ||--o{ blocks : "block_hash_id, previous_block_hash_id"
index_transactions ||--o{ transactions : "tx_hash_id"
index_addresses ||--o{ transactions : "source_id, destination_id"
index_addresses ||--o{ dispensers : "address_id"
index_addresses ||--o{ transaction_outputs : "destination_id"
index_addresses ||--o{ pubkeys : "address_id"
blocks
Stores one row per block that the decoder has processed.
| Column | Type | Description |
|---|---|---|
block_index |
BIGINT UNSIGNED |
Block height (primary key) |
block_time |
BIGINT UNSIGNED |
Block timestamp (unix epoch) |
block_hash_id |
BIGINT UNSIGNED |
FK to index_transactions.id |
previous_block_hash_id |
BIGINT UNSIGNED |
FK to index_transactions.id |
Indexes: block_hash_id, previous_block_hash_id
transactions
Stores confirmed XChain transactions (transactions whose deobfuscated payload starts with XCHN).
| Column | Type | Description |
|---|---|---|
tx_index |
BIGINT UNSIGNED |
Transaction index (primary key, auto-assigned) |
tx_hash_id |
BIGINT UNSIGNED |
FK to index_transactions.id (unique) |
block_index |
BIGINT UNSIGNED |
Block height containing this transaction |
source_id |
BIGINT UNSIGNED |
FK to index_addresses.id (sender) |
destination_id |
BIGINT UNSIGNED |
FK to index_addresses.id (receiver) |
amount |
BIGINT |
BTC/LTC/DOGE amount in satoshis |
fee |
BIGINT |
Transaction fee in satoshis |
data |
MEDIUMTEXT |
Decoded ACTION string (e.g., SEND|0|TOKEN|1000) |
raw_data |
MEDIUMBLOB |
Raw payload bytes (preserved for binary payloads such as gated-FILE ciphertext) |
Indexes: tx_hash_id (unique), block_index, source_id, destination_id
mempool_transactions
Holds unconfirmed XChain transactions. Rows are created when a transaction enters the mempool and deleted when it confirms (promoted to transactions) or drops out of the mempool.
Important: this table stores raw strings rather than FK integer IDs. Mempool arrival order is node-local and non-deterministic, while index_addresses and index_transactions are part of the replicated decoder set. Pre-allocating lookup IDs during mempool observation would let two nodes assign different AUTO_INCREMENT IDs to the same address or hash, causing silent divergence. IDs are allocated only during deterministic block-confirmation processing (see insertTransaction). This table is intentionally excluded from xchain-sync replication.
| Column | Type | Description |
|---|---|---|
tx_hash |
VARCHAR(250) |
Raw transaction hash (NOT an index_transactions id) |
source |
VARCHAR(120) |
Raw source address (NOT an index_addresses id) |
destination |
VARCHAR(120) |
Raw destination address (NOT an index_addresses id) |
amount |
BIGINT |
Amount in satoshis |
fee |
BIGINT |
Fee in satoshis |
data |
MEDIUMTEXT |
Decoded ACTION string |
Indexes: tx_hash (unique), source, destination
DISPENSER Tables
dispensers
Tracks open and soft-expired dispensers. A dispenser is created when the decoder encounters a DISPENSER|0|... action with valid fields. Rather than deleting a dispenser row on expiry, the decoder sets expired_block_index to the current block height (soft-expire), which allows a subsequent reorg to restore the dispenser by clearing that mark. Rows are hard-deleted once they are reorg-safe-deep, per the exported DISPENSER_EXPIRE_SAFE_DEPTH constant in xchain-decoder/src/XChainDecoder.js (currently 126 blocks: 120-block DOGE undo window + 6-block margin), bounding table growth.
| Column | Type | Description |
|---|---|---|
tx_index |
BIGINT UNSIGNED |
Transaction that created the dispenser |
address_id |
BIGINT UNSIGNED |
FK to index_addresses.id (dispenser owner) |
expiration |
BIGINT UNSIGNED |
Expiration as a raw Unix timestamp in seconds. Stored as an integer (not via FROM_UNIXTIME()) because FROM_UNIXTIME() caps at 2147483647 (Y2038) and returns NULL above that value, which would silently drop dispensers with far-future expirations. The raw BIGINT UNSIGNED column supports values up to 4294967295 (year 2106), matching the protocol’s accepted range. |
expired_block_index |
BIGINT UNSIGNED DEFAULT NULL |
NULL for open dispensers. Set to the block height that soft-expired this row instead of hard-deleting it; a reorg’s deleteBlockByIndex clears the mark for orphaned heights, restoring any dispenser that an orphaned block’s non-monotonic timestamp expired. |
Primary Key: (tx_index, address_id)
Indexes: dispensers_expired_block_index, dispensers_expiration
transaction_outputs
Records outputs that paid to an active dispenser’s address. These are detected during transaction parsing when isThereADispenserForAddress() returns true. Also used to record outputs paying the native-coin fee destination (when FEE_DESTINATION is set), so the indexer can validate native-coin fee payments.
| Column | Type | Description |
|---|---|---|
tx_index |
BIGINT UNSIGNED |
Transaction containing the output |
vout |
BIGINT UNSIGNED |
Output index within the transaction |
destination_id |
BIGINT UNSIGNED |
FK to index_addresses.id (dispenser or fee address) |
amount |
VARCHAR(250) |
Coin amount in decimal (e.g., 0.01000000) |
Primary Key: (tx_index, vout)
Index Tables
Index tables normalize frequently-repeated strings (addresses, transaction hashes) into integer IDs for storage efficiency and join performance. Both tables reserve id=1 for a blank/null sentinel value.
index_addresses
| Column | Type | Description |
|---|---|---|
id |
BIGINT UNSIGNED NOT NULL AUTO_INCREMENT |
Address ID (primary key) |
address |
VARCHAR(120) |
Address string |
Indexes: address (full-column UNIQUE index; enforces one row per address and makes INSERT IGNORE upserts race-safe)
Row id=1 is reserved as a blank/null sentinel (inserted at table creation with address='').
index_transactions
| Column | Type | Description |
|---|---|---|
id |
BIGINT UNSIGNED NOT NULL AUTO_INCREMENT |
Hash ID (primary key) |
hash |
VARCHAR(250) |
Transaction or block hash |
Indexes: hash (full-column UNIQUE index; enforces one row per hash and makes INSERT IGNORE upserts race-safe)
Row id=1 is reserved as a blank/null sentinel (inserted at table creation with hash='').
Events Table
events
Records system events such as chain reorganizations.
| Column | Type | Description |
|---|---|---|
id |
BIGINT UNSIGNED AUTO_INCREMENT |
Event ID (primary key) |
time |
DATETIME |
Event timestamp |
code |
VARCHAR(32) |
Event type code (e.g., REORG) |
data |
MEDIUMTEXT |
Event payload (JSON). MEDIUMTEXT (16 MB cap) rather than TEXT (64 KB): a deep reorg serializes ~100 bytes per rolled-back block, so a >650-block reorg walk would overflow TEXT and silently truncate the only audit record. |
pubkeys
Records the source-address public key captured when parsing a transaction whose first input exposes it in the scriptSig or witness. Used by xchain-sync to replicate the source_pubkey field per transaction to validators.
| Column | Type | Description |
|---|---|---|
address_id |
BIGINT UNSIGNED NOT NULL |
PK, FK to index_addresses.id |
pubkey |
VARCHAR(66) NOT NULL |
Compressed public key (hex) |
Foreign key: address_id references index_addresses(id)
Schema Migrations
The decoder applies schema changes via a tracked migration system. Two paths exist:
-
Automatic (startup): migrations tagged
-- xchain:migration mode=autoin their header are applied by the normal startup sequence. These must be additive and idempotent. -
Manual (operator): migrations tagged
mode=manual(destructive column-type changes, data backfills, dedup-then-unique) run only when an operator explicitly invokes:node src/migrate.js # or: npm run migrate
Each migration is recorded in the schema_migrations table (created automatically) and is applied at most once regardless of how many times the command is run. An advisory lock prevents concurrent migration runs. Migration files live in src/sql/migrations/ and are applied in filename sort order.
Notable applied migrations:
| File | Mode | Effect |
|---|---|---|
2026-05-28-unique-index-tables.sql |
manual | De-duplicates index_addresses/index_transactions rows, then adds UNIQUE indexes |
2026-06-02-widen-ids-to-bigint.sql |
manual | Widens all INTEGER UNSIGNED id/key columns to BIGINT UNSIGNED across seven tables (index_addresses, index_transactions, pubkeys, blocks, transactions, dispensers, transaction_outputs). mempool_transactions is not included; the 2026-06-15-mempool-raw-strings.sql migration later drops and recreates that table entirely with the correct types. |
2026-06-13-dispensers-expiration-bigint.sql |
manual | Changes dispensers.expiration from DATETIME to BIGINT UNSIGNED |
2026-06-15-events-data-mediumtext.sql |
auto | Widens events.data from TEXT to MEDIUMTEXT |
2026-06-15-mempool-raw-strings.sql |
manual | Drops and recreates mempool_transactions with raw VARCHAR columns instead of FK integer IDs |
2026-06-17-pubkeys-add-monotonic-id.sql |
auto | Adds id BIGINT UNSIGNED AUTO_INCREMENT UNIQUE to pubkeys for stable replication paging |
Data Flow
flowchart TD
NODE["Coin Node"]
CONN["BlockchainConnector"]
PARSE["XChainDecoder.parseTransaction()"]
BLOCKS[("blocks table")]
TXN[("transactions table")]
IDXTX[("index_transactions")]
IDXADDR[("index_addresses")]
DISP[("dispensers")]
TXOUT[("transaction_outputs")]
EVENTS[("events (reorgs)")]
NODE --> CONN --> PARSE
PARSE --> BLOCKS
PARSE --> TXN
BLOCKS --> IDXTX
TXN --> IDXADDR
IDXTX --> DISP
IDXADDR --> TXOUT
DISP --> EVENTS
Satoshi Conversion
The bigIntSatoshiToDecimalsString() method converts satoshi values (integers or BigInts) to decimal strings with 8 decimal places:
| Input | Output |
|---|---|
100000000 |
"1.00000000" |
50000000 |
"0.50000000" |
1 |
"0.00000001" |
0 |
"0.00000000" |
-100000000 |
"-1.00000000" |
This is used for the amount column in transaction_outputs.
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.