XChain Platform Action - ANCHOR
On-chain commitment of federation-signed state, checkpoints and the cross-chain match archive, in a single action with seven version-discriminated phases:
- v0: Checkpoint. Validator-broadcast. Commits one chain’s quorum-signed state checkpoint
(the per-block
ledger/actions/contracthash triple) to the anchor chain. - v1: Checkpoint + match archive. Validator-broadcast. A v0 checkpoint plus a compressed
batch of full
cross_chain_matchesrecords (including their validator signatures and thecapability_snapshotsrows needed to re-verify them), making cross-chain match data recoverable from chain parse alone. - v2: Archive continuation. Validator-broadcast. Carries overflow chunks when a v1 archive payload exceeds the per-action data limit. Authenticated by its parent v1 (carries no signatures of its own).
- v3: Checkpoint + light-client roots. Validator-broadcast. A v0 checkpoint plus the additive
SPV light-client roots (
STATE_ROOT+BLOCK_MERKLE_ROOTand their version bytes), gated by theCHECKPOINT_COMMITMENTflag-day. Post-flag-day the publisher emits v3 instead of v0; the federation signatures cover the roots (they are part of the signed checkpoint canonical), so an on-chain-anchored state root is recoverable from chain parse alone. - v4 / v5: Checkpoint + publisher attestation. Validator-broadcast. A v0 (v4, rootless) / v3
(v5, root-bearing) checkpoint plus the elected
PUBLISHERpubkey and a second 2f+1oracle_publishattestation (theXANCPUBcanonical) binding which validator earns the fixed anchor reward, gated by theANCHOR_REWARDflag-day. The indexer re-derives the reward from these bytes, retiring the previously trusted (and forgeable)pushvalidatorrewardspush. Post-flag-day the publisher emits v4/v5 in place of v0/v3; a degraded federation that cannot reach the attestation quorum falls back to a legacy v0/v3 so the anchor still lands (no reward). - v6: Archive + publisher attestation. Validator-broadcast. The v1 archive anchor plus the
elected archive leader’s
PUBLISHERpubkey and the same 2f+1oracle_publishattestation tail as v4/v5, attested over ananchor_archiveXANCPUBcanonical keyed onMATCH_BATCH_SEQand gated by theARCHIVE_REWARDflag-day (XC-459). The indexer re-derives the archive reward from these bytes, retiring the last key-authenticatedpushvalidatorrewardsrail. Post-flag-day the archive leader emits v6 in place of v1; a degraded federation falls back to a legacy v1 so the archive still lands (no reward).
flowchart TD
v0["v0: Checkpoint"]
v1["v1: Checkpoint + match archive"]
v2["v2: Archive continuation<br>(overflow chunks, authenticated by parent v1)"]
v3["v3: Checkpoint + light-client roots"]
v4["v4: v0 + publisher attestation<br>(rootless)"]
v5["v5: v3 + publisher attestation<br>(root-bearing)"]
v6["v6: v1 archive + publisher attestation"]
v0 -->|"add match archive"| v1
v1 -->|"overflow chunks"| v2
v0 -->|"add SPV light-client roots"| v3
v0 -->|"add publisher attestation"| v4
v3 -->|"add publisher attestation"| v5
v1 -->|"add publisher attestation"| v6
ANCHOR is valid only on the anchor chain; DOGE (all networks). Indexers on other chains
reject it. BTC and LTC state is still covered: each v0/v1/v3/v4/v5/v6 names the CHAIN it
checkpoints, so one cheap chain carries the commitments for all three.
ANCHOR supersedes the hub’s legacy raw XDEXANCHOR payload (which was not a protocol action and
was invisible to the decoder). The XDEXANCHOR publisher (CrossChainDexAnchor) was removed
from the hub on 2026-06-11 after ANCHOR verified end-to-end on mainnet; rows it stamped
(batch_root) remain readable but nothing publishes the legacy payload anymore.
Purpose
- Verifiable state. Light clients verify any indexer/explorer response against a
checkpoint signed by
2f+1oracle_publishvalidators, without trusting a single operator. - Full-parse recoverability. Cross-chain match records are the only consensus-relevant dataset not natively on-chain (they are mirror-delivered; see Cross-Chain DEX). The v1/v2 archive places the records themselves on-chain, so the entire platform state is reconstructible from a full parse of the three blockchains with no surviving hub database.
PARAMS
| Name | Type | Versions | Description |
|---|---|---|---|
VERSION |
Integer | all | Format version (0=checkpoint, 1=checkpoint+archive, 2=continuation, 3=checkpoint+SPV roots, 4=v0+publisher, 5=v3+publisher, 6=v1+publisher) |
CHAIN |
String | 0, 1, 3 | Chain being checkpointed: BTC | LTC | DOGE |
NETWORK |
String | 0, 1, 3 | mainnet | testnet | regtest |
BLOCK_INDEX |
Integer | 0, 1, 3 | Checkpointed block height on CHAIN |
BLOCK_HASH |
String | 0, 1, 3 | 64-hex block hash of CHAIN at BLOCK_INDEX |
LEDGER_HASH |
String | 0, 1, 3 | 64-hex chained ledger hash (blocks.ledger_hash at BLOCK_INDEX) |
ACTIONS_HASH |
String | 0, 1, 3 | 64-hex chained actions hash |
CONTRACT_HASH |
String | 0, 1, 3 | 64-hex chained contract hash |
CHECKPOINT_SEQ |
Integer | 0, 1, 3 | Monotonic checkpoint counter per (CHAIN,NETWORK) |
SNAPSHOT_BLOCK |
Integer | 0, 1, 3 | BTC block selecting the oracle_publish validator set for the sigs |
STATE_ROOT |
String | 3 | 64-hex SPV state root (SMT over balances+stakes) at BLOCK_INDEX |
STATE_ROOT_VERSION |
Integer | 3 | Merkle scheme version the STATE_ROOT was computed under |
BLOCK_MERKLE_ROOT |
String | 3 | 64-hex SPV per-block content Merkle root at BLOCK_INDEX |
BLOCK_MERKLE_VERSION |
Integer | 3 | Merkle scheme version the BLOCK_MERKLE_ROOT was computed under |
MATCH_BATCH_SEQ |
Integer | 1, 2 | Monotonic archive-batch counter (ties v2 chunks to their v1) |
MATCH_COUNT |
Integer | 1 | Number of match records in this archive batch |
BATCH_CRC32 |
String | 1 | 8-hex CRC32 of the uncompressed archive JSON bytes |
ARCHIVE_B64 |
String | 1 | base64url of gzip(archive JSON): chunk 0 when the batch is chunked |
CHUNK_INDEX |
Integer | 2 | 1-based continuation index (the v1 itself carries chunk 0) |
TOTAL_CHUNKS |
Integer | 1, 2 | Total chunks in the batch (1 = unchunked, v1-only) |
ARCHIVE_B64_CHUNK |
String | 2 | This continuation’s slice of the base64url payload |
SIG_COUNT |
Integer | 0, 1, 3, 4, 5 | Number of (pubkey, sig) pairs that follow |
PUBKEY_n |
String | 0, 1, 3, 4, 5 | 64-hex Ed25519 pubkey, in the oracle_publish set at SNAPSHOT_BLOCK |
SIG_n |
String | 0, 1, 3, 4, 5 | 128-hex Ed25519 signature over the canonical checkpoint message |
PUBLISHER |
String | 4, 5, 6 | 64-hex Ed25519 pubkey of the elected publisher that earns the anchor reward |
ATTEST_SIG_COUNT |
Integer | 4, 5, 6 | Number of (pubkey, sig) attestation pairs that follow |
APUBKEY_n |
String | 4, 5, 6 | 64-hex pubkey in the oracle_publish set at SNAPSHOT_BLOCK (attestation signer) |
ASIG_n |
String | 4, 5, 6 | 128-hex Ed25519 signature over the XANCPUB canonical |
Formats
Version 0: Checkpoint (validator-broadcast)
ANCHOR|0|CHAIN|NETWORK|BLOCK_INDEX|BLOCK_HASH|LEDGER_HASH|ACTIONS_HASH|CONTRACT_HASH|CHECKPOINT_SEQ|SNAPSHOT_BLOCK|SIG_COUNT|PUBKEY1|SIG1|PUBKEY2|SIG2|...
Version 1: Checkpoint + match archive (validator-broadcast)
ANCHOR|1|CHAIN|NETWORK|BLOCK_INDEX|BLOCK_HASH|LEDGER_HASH|ACTIONS_HASH|CONTRACT_HASH|CHECKPOINT_SEQ|SNAPSHOT_BLOCK|MATCH_BATCH_SEQ|MATCH_COUNT|BATCH_CRC32|TOTAL_CHUNKS|ARCHIVE_B64|SIG_COUNT|PUBKEY1|SIG1|...
Version 2: Archive continuation (validator-broadcast; no signatures)
ANCHOR|2|MATCH_BATCH_SEQ|CHUNK_INDEX|TOTAL_CHUNKS|ARCHIVE_B64_CHUNK
Version 3: Checkpoint + light-client roots (validator-broadcast)
ANCHOR|3|CHAIN|NETWORK|BLOCK_INDEX|BLOCK_HASH|LEDGER_HASH|ACTIONS_HASH|CONTRACT_HASH|CHECKPOINT_SEQ|SNAPSHOT_BLOCK|STATE_ROOT|STATE_ROOT_VERSION|BLOCK_MERKLE_ROOT|BLOCK_MERKLE_VERSION|SIG_COUNT|PUBKEY1|SIG1|...
Version 4: Checkpoint + publisher attestation (validator-broadcast)
ANCHOR|4|CHAIN|NETWORK|BLOCK_INDEX|BLOCK_HASH|LEDGER_HASH|ACTIONS_HASH|CONTRACT_HASH|CHECKPOINT_SEQ|SNAPSHOT_BLOCK|SIG_COUNT|PUBKEY1|SIG1|...|PUBLISHER|ATTEST_SIG_COUNT|APUBKEY1|ASIG1|...- The rootless v0 checkpoint with the
PUBLISHER+ attestation list appended after the root signature list (never inserted mid-string, so old positional parsers are unaffected). Emitted in place of v0 at/above theANCHOR_REWARDflag-day.
Version 5: Checkpoint + light-client roots + publisher attestation (validator-broadcast)
ANCHOR|5|CHAIN|NETWORK|BLOCK_INDEX|BLOCK_HASH|LEDGER_HASH|ACTIONS_HASH|CONTRACT_HASH|CHECKPOINT_SEQ|SNAPSHOT_BLOCK|STATE_ROOT|STATE_ROOT_VERSION|BLOCK_MERKLE_ROOT|BLOCK_MERKLE_VERSION|SIG_COUNT|PUBKEY1|SIG1|...|PUBLISHER|ATTEST_SIG_COUNT|APUBKEY1|ASIG1|...- The root-bearing v3 checkpoint with the same
PUBLISHER+ attestation tail as v4. Emitted in place of v3 at/above theANCHOR_REWARDflag-day (so v5 also requires theCHECKPOINT_COMMITMENTflag-day). Set the mainnetANCHOR_REWARDflag-day>=CHECKPOINT_COMMITMENTto keep mainnet on v5-only (always root-bearing) and avoid the rootless v4. - The two roots + version bytes are appended after
SNAPSHOT_BLOCK(never inserted mid-string, so old positional parsers are unaffected). Emitted in place of v0 at/above theCHECKPOINT_COMMITMENTflag-day.
Version 6: Checkpoint + match archive + publisher attestation (validator-broadcast)
ANCHOR|6|CHAIN|NETWORK|BLOCK_INDEX|BLOCK_HASH|LEDGER_HASH|ACTIONS_HASH|CONTRACT_HASH|CHECKPOINT_SEQ|SNAPSHOT_BLOCK|MATCH_BATCH_SEQ|MATCH_COUNT|BATCH_CRC32|TOTAL_CHUNKS|ARCHIVE_B64|SIG_COUNT|PUBKEY1|SIG1|...|PUBLISHER|ATTEST_SIG_COUNT|APUBKEY1|ASIG1|...- The v1 archive anchor with the
PUBLISHER+ attestation list appended after the wrapper signature list (never inserted mid-string). Emitted in place of v1 at/above theARCHIVE_REWARDflag-day (XC-459). Continuation chunks stay v2, tied byMATCH_BATCH_SEQexactly as for a v1 head.
Examples
ANCHOR|0|BTC|mainnet|900123|00000000...|3f9a...|b81c...|44d0...|417|900120|3|a1b2...|c3d4...|e5f6...|0718...|292a...|3b4c...
Quorum-signed checkpoint of BTC mainnet block 900123, published on DOGE
ANCHOR|1|BTC|mainnet|900123|00000000...|3f9a...|b81c...|44d0...|418|900120|42|17|9c4e1b22|1|H4sIAAAA...|3|a1b2...|c3d4...|...
Checkpoint plus archive batch 42 (17 match records, single chunk)
ANCHOR|2|42|1|3|AAAB7Rxe...
Continuation chunk 1 of 3 for archive batch 42
Canonical signing message (v0 / v1 / v3)
Each SIG_n covers the UTF-8 bytes of:
XCHECKPOINT|CHAIN|NETWORK|BLOCK_INDEX|BLOCK_HASH|LEDGER_HASH|ACTIONS_HASH|CONTRACT_HASH|CHECKPOINT_SEQ|SNAPSHOT_BLOCK
and for v1, with the archive structure appended:
XCHECKPOINT|...|SNAPSHOT_BLOCK|MATCH_BATCH_SEQ|MATCH_COUNT|BATCH_CRC32|TOTAL_CHUNKS
and for v3, with the SPV light-client roots appended (the byte the federation signs, so the roots are covered by the quorum, not merely transported):
XCHECKPOINT|...|SNAPSHOT_BLOCK|STATE_ROOT|STATE_ROOT_VERSION|BLOCK_MERKLE_ROOT|BLOCK_MERKLE_VERSION
The v3 suffix is byte-identical to the post-flag-day checkpoint canonical the hub StateCheckpointEngine signs and the SDK / explorer verifiers reconstruct (the publisher reuses the checkpoint row’s signatures verbatim). Gated on SNAPSHOT_BLOCK by CHECKPOINT_COMMITMENT_ACTIVATION; a v3 below the flag-day is rejected.
ARCHIVE_B64 is not part of the signed bytes; the blob is bound to the signed structure by
BATCH_CRC32, computed over the uncompressed JSON. (CRC over uncompressed bytes keeps
verification independent of the zlib version that produced the gzip stream.) Chain/network are
uppercase/lowercase exactly as on the wire; numerics are decimal with no leading zeros; hashes
are lowercase hex. A signature counts only if its pubkey is in the oracle_publish capability
snapshot at SNAPSHOT_BLOCK and the Ed25519 signature verifies.
The v4/v5 root signatures (SIG_n) cover the SAME canonical as v0/v3 respectively (rootless for
v4, root-bearing for v5), and the v6 wrapper signatures cover the SAME archive canonical as v1;
the publisher attestation below is a SEPARATE signature list.
Publisher-attestation canonical (XANCPUB, v4 / v5 / v6)
Each ASIG_n covers the UTF-8 bytes of the reward tuple:
XANCPUB|anchor_<CHAIN>|CHECKPOINT_SEQ|SNAPSHOT_BLOCK|PUBLISHER|ANCHOR_REWARD_AMOUNT
ANCHOR_REWARD_AMOUNT is the frozen consensus constant 10.00000000 (read from the
ANCHOR_REWARD_ACTIVATION twin module, NEVER taken from the wire; changing it is itself a
flag-day). PUBLISHER is lowercase hex. At/above the EQUIV_HEADER flag-day the bytes are wrapped
once in the uniform equivocation header, with a distinct XANCPUB|... round id so this attestation
forms its own equivocation family (a validator that signs both the checkpoint root canonical and
this reward attestation in the same round is never falsely slashable):
EQUIV|XCHECKPOINT|XANCPUB|CHAIN|NETWORK|CHECKPOINT_SEQ|SNAPSHOT_BLOCK|0||XANCPUB|anchor_<CHAIN>|CHECKPOINT_SEQ|SNAPSHOT_BLOCK|PUBLISHER|ANCHOR_REWARD_AMOUNT
The v6 archive attestation (XC-459) uses the same shape keyed on the archive batch, with the
frozen ARCHIVE_REWARD_AMOUNT (10.00000000, from the same twin module) and an
XANCPUB|archive|... round id disjoint from every per-chain round id (the two attestation
families can never equivocation-collide):
XANCPUB|anchor_archive|MATCH_BATCH_SEQ|SNAPSHOT_BLOCK|PUBLISHER|ARCHIVE_REWARD_AMOUNT
and at/above the EQUIV_HEADER flag-day:
EQUIV|XCHECKPOINT|XANCPUB|archive|NETWORK|MATCH_BATCH_SEQ|SNAPSHOT_BLOCK|0||XANCPUB|anchor_archive|MATCH_BATCH_SEQ|SNAPSHOT_BLOCK|PUBLISHER|ARCHIVE_REWARD_AMOUNT
These bytes are byte-identical across the hub producer (StateAnchorPublisher._attestationCanonical),
the indexer verifier (actions/anchor.js _rewardCanonical) and this spec; a divergence forks the
derived reward row. An ASIG_n counts only if its pubkey is in the SAME oracle_publish snapshot at
SNAPSHOT_BLOCK used for the root quorum and the Ed25519 signature verifies.
Archive JSON (v1/v2 payload, after gunzip)
A single JSON object with fixed key order (required: BATCH_CRC32 is computed over these
exact bytes):
{
"v": 1,
"network": "mainnet",
"batch_seq": 42,
"matches": [ { ...full cross_chain_matches row... } ],
"calls": [ { ...cross_chain_calls relay row... } ],
"rewards": [ { "validator_pubkey": "...", "source": "1Stake...", "round_number": 17, "reward_type": "anchor_BTC", "amount": "10.00000000", "block_index": 900120 } ],
"capability_snapshots": [ { "snapshot_block": 900120, "capability": "cross_chain", "signing_pubkey": "...", "amount": "..." } ]
}
matches[]rows carry every wire-relevantcross_chain_matchescolumn:match_id,snapshot_block,network, both legs (a_*/b_*includingkind/filled_before),effective_time,validator_signatures(the raw JSON string, verbatim), andstatus(finalizedorretracted). Hub-side audit columns (batch_root,anchor_txid) are excluded.capability_snapshots[]carries thecross_chainsnapshot rows for every distinctsnapshot_blockreferenced bymatches[](to re-verify match signatures) plus theoracle_publishrows at the wrapper checkpoint’sSNAPSHOT_BLOCK(to re-verify the v1 anchor’s own signatures). They are included because historicalmin_stakegovernance values are not on-chain, archiving the snapshot rows makes signature re-verification self-contained during recovery. Recovery additionally cross-checks archived pubkeys against on-chain BTC stakes (a fabricated snapshot row cannot survive, staking is on-chain), so the chain remains the root of trust.rewards[]carries the anchor-publish reward rows (reward_typeanchor_<chain>/anchor_archiveonly) that have not yet ridden an archive. These are the onevalidator_rewardsrail a chain parse cannot re-derive (oracle_roundandattest_feerows are derived deterministically from PRICE/ATTEST actions and are never archived: recovery rejects an archive that claims them). Reward rows carry no per-row signatures; every co-signing hub instead re-derives each field before signing: the pubkey must be in its ownoracle_publishresolution atblock_index, the amount must equal its configured publish reward, andsourcemust match its own block-scoped indexer resolution of the earn-time staking address (pinned into the archive because recovery restores rewards into an EMPTY BTC DB, and a later re-stake of the pubkey must not move the credit).block_indexis the quorum-agreedSNAPSHOT_BLOCKof the rewarded checkpoint, so every hub records identical row bytes.- All amounts are decimal strings (full precision, as stored).
- A match retracted after it was archived is re-published in a later batch with
status:"retracted". Recovery applies latest-status-wins ordered bybatch_seq. callsandrewardsare additive keys, archives published before each existed simply omit them, and recovery treats a missing key as an empty list.
Rules
All versions
- Valid only where
COIN = DOGE, indexers on other chains mark the action invalid. - No XCHAIN fee and no native-coin protocol fee (validator protocol action, same fee treatment
as
PRICEv0). The publisher pays only the DOGE miner fee.
Version 0 / 1 / 3 / 4 / 5 / 6
CHAINmust be one ofBTC/LTC/DOGE;NETWORKmust equal the indexer’s own network.- Each
PUBKEY_nis checked against theoracle_publishcapability snapshot atSNAPSHOT_BLOCK(a BTC height; non-BTC indexers resolve it from the hub-mirroredcapability_snapshotstable, exactly as cross-chain settlement resolvescross_chain). - Each
SIG_nmust Ed25519-verify against the canonical message. - Valid signatures must reach
max(2f+1, ceil((N+1)/2))of the snapshot set; PBFT2f+1floored at a simple majority, so N=3 requires 2 (single-validator sets require 1). CHECKPOINT_SEQmust be ≥ any previously accepted seq for (CHAIN,NETWORK), replays of older checkpoints are recorded but flaggedstale, nevervalid. Equal-seq records are accepted: a v0 and its v1 share the same wrapper seq by design, and an exact replay is signature-bound to identical content (harmless duplicate). The same ≥ rule applies toMATCH_BATCH_SEQon v1/v6. The checkpoint replay watermark counts v0/v1/v3/v4/v5/v6 together.
Version 3 only
- Valid only at/above the
CHECKPOINT_COMMITMENTflag-day (gated onSNAPSHOT_BLOCK); a v3 below it is invalid (its signed canonical would have no root suffix, so the sigs could never verify). Post-flag-day the publisher emits v3 in place of v0. STATE_ROOTandBLOCK_MERKLE_ROOTmust be 64-hex; the two version bytes must be integers. Both roots are part of the signed canonical, so a swapped root fails the signature check.
Version 4 / 5 only
- Valid only at/above the
ANCHOR_REWARDflag-day (gated onSNAPSHOT_BLOCK); a v4/v5 below it is invalid. v5 additionally requires theCHECKPOINT_COMMITMENTflag-day (it carries roots). Post-flag-day the publisher emits v4/v5 in place of v0/v3. PUBLISHERmust be 64-hex. The attestation list (APUBKEY_n/ASIG_n) is verified as a SECOND quorum over theXANCPUBcanonical against the SAMEoracle_publishsnapshot atSNAPSHOT_BLOCKused for the root quorum, reaching the samemax(2f+1, ceil((N+1)/2))(stake-weighted at/aboveSTAKE_WEIGHTED_QUORUM) threshold.- The anchor reward is credited (a COLLECT-spendable
validator_rewardsrow keyed(CHECKPOINT_SEQ, anchor_<CHAIN>), amount = the frozenANCHOR_REWARD_AMOUNT, never the wire) only when the root quorum passed, the attestation quorum is met, andPUBLISHERis in the snapshot set. A failed, short, or forged attestation never invalidates the anchor: the checkpoint still records asvalidand only the reward is skipped, deterministically across the fleet. A failover double-publish converges to the smallest-pubkey winner (the same reconcile the retired push + recovery use), so the COLLECT rail stays single-winner. - The trusted, unauthenticated
pushvalidatorrewardsreward push is retired foranchor_<chain>at/above the flag-day: every indexer DERIVES the reward from these bytes instead.
Version 6 only
- Valid only at/above the
ARCHIVE_REWARDflag-day (gated onSNAPSHOT_BLOCK); a v6 below it is invalid. Post-flag-day the archive leader emits v6 in place of v1. - All v1 rules apply unchanged (archive integrity,
MATCH_BATCH_SEQmonotonicity, v2 chunk tie). PUBLISHER+ attestation verification follows the v4/v5 rules verbatim, over the archiveXANCPUBcanonical. The credited row is keyed(MATCH_BATCH_SEQ, anchor_archive), amount = the frozenARCHIVE_REWARD_AMOUNT, never the wire. A failed, short, or forged attestation never invalidates the archive anchor; only the reward is skipped.- The
pushvalidatorrewardspush is retired foranchor_archiveat/above the flag-day: every indexer DERIVES the archive reward from these bytes instead (XC-459; closes the insider-with-key forge surface the per-chain flag-day left open).
Version 1 only
MATCH_COUNTmust equalmatches.lengthafter decompression (whenTOTAL_CHUNKS= 1; otherwise checked at reassembly).BATCH_CRC32must match the CRC32 of the uncompressed JSON (checked at reassembly when chunked).MATCH_BATCH_SEQmust be ≥ any previously accepted batch seq for the network.
Version 2 only
MATCH_BATCH_SEQmust reference a previously indexed v1 withTOTAL_CHUNKS> 1 (out-of-order arrival within the same block is tolerated; the batch assembles when all chunks are present).CHUNK_INDEXmust be in[1, TOTAL_CHUNKS-1]and not a duplicate.- The chunk’s source address must equal the head’s: “authenticated by its parent v1” is enforced, so only the publisher whose batch it is can fill a slot.
- Carries no signatures; a v2 is meaningful only joined to its authenticated v1; orphan or
CRC-failing batches are flagged
invalid_archiveand ignored by recovery.
Archive batch identity (flag-day gated)
MATCH_BATCH_SEQ is not unique: an equal seq is accepted (re-broadcast, failover
double-publish), so more than one head can carry it. At/after the per-network
ARCHIVE_BATCH_AUTHOR flag-day an archive batch is identified by
(MATCH_BATCH_SEQ, head source address), not by the seq alone:
- a chunk’s parent is the earliest head for that seq authored by the same address,
TOTAL_CHUNKS, slot occupancy andBATCH_CRC32reassembly are all evaluated within that publisher’s own batch, and- a chunk whose publisher has no head for the seq is an orphan.
Before the flag-day the parent is the earliest head for the seq regardless of author, which
lets anyone deny a batch by broadcasting a junk head at the next seq: its TOTAL_CHUNKS
invalidates every legitimate chunk, and its address becomes the only one whose chunks count.
A publisher publishing one head per seq sees no difference between the two rules.
Effects
- Persists into
anchor_actions(action-indexed; rolled back on reorg like any data table). - A
validv0/v1 records the checkpoint; the indexer’s mirroredstate_checkpointscopy is the live source for verification APIs, whileanchor_actionsis the permanent on-chain record. - No ledger effect. ANCHOR never credits, debits, escrows, or alters token state. A bad or missing anchor can never corrupt balances; the worst failure mode is a missing audit/recovery record.
Publisher
- Published by the hub’s
StateAnchorPublisher. Per-chain publisher election: each pending checkpoint elects its own publisher from theoracle_publishcapability snapshot at the checkpoint’ssnapshot_block, ordered bySHA256(election key ‖ pubkey)ascending (the attestation responsible-set idiom; the key binds chain/network/seq/snapshot_block, so a different validator typically wins each chain’s anchor in a cycle). Rank 0 publishes from its own funded DOGE wallet; each further rank unlocks afterANCHOR_ELECTION_TOLERANCE_BLOCKSmore BTC blocks elapse without a publish (deterministic failover ladder; a gossipedXANC_V0_DONEback-fill stops peers from re-anchoring a checkpoint someone already paid for). The v1/v2 archive round elects a single leader the same way, keyed per election block. A single-validator federation degenerates to today’s serialized single-wallet behavior.
flowchart TD
A["Pending checkpoint (per chain)"] --> B["Elect publisher: oracle_publish snapshot at snapshot_block,<br>ordered by SHA256(election key ‖ pubkey) ascending"]
B --> C["Rank 0 publishes<br>from its own funded DOGE wallet"]
B -.->|"same election, keyed per election block"| I["v1/v2 archive round<br>elects a single leader"]
C --> D{"XANC_V0_DONE gossiped<br>before next rank unlocks?"}
D -->|"yes"| E["Peers stand down,<br>no re-anchor"]
D -->|"no, after ANCHOR_ELECTION_TOLERANCE_BLOCKS<br>more BTC blocks"| F["Rank 1 publishes"]
F --> G{"XANC_V0_DONE gossiped?"}
G -->|"yes"| E
G -->|"no, after ANCHOR_ELECTION_TOLERANCE_BLOCKS<br>more BTC blocks"| H["Rank 2+ unlocks<br>(ladder continues)"]
- Each successful publish records an
anchor_<chain>(round =checkpoint_seq) oranchor_archive(round =batch_seq) reward ofANCHOR_REWARD_PER_PUBLISHXCHAIN (default 10) on thevalidator_rewardsrail, collectable on BTC viaCOLLECTlike oracle-round rewards. - P2SH encoding via the standard encoder pipeline.
- Default cadence: one v0 per chain plus pending v1/v2 archive batches per anchor interval
(
ANCHOR_INTERVAL_MS, default daily), or early whenANCHOR_MATCH_BATCH_SIZEmatches are pending. Checkpoint signing happens more often (hourly, mirror-only, no chain writes); the anchor commits the latest signed checkpoint at publish time. Operators can also trigger an immediate flush via the hub’s authenticatedanchorflushJSON-RPC method.
Where the publisher constants come from
None of these is consensus data: they are per-hub operator knobs, and two hubs running
different values still produce mutually verifiable anchors. What follows is the derivation of
each magnitude, so a tuner can tell what is load-bearing from what is merely a round number.
The arithmetic is pinned by xchain-hub/test/unit/StateAnchorPublisher.constant-derivations.test.js.
ANCHOR_CHUNK_MAX_BYTES (6000). The hard ceiling is MAX_ACTION_DATA_LENGTH = 8192
compiled bytes (protocol/constants.js). The decoder is the arbiter and silently drops any
action above it, so an oversize anchor is lost on every node rather than rejected loudly. Chunk
0 never travels alone: it sits inside the v1/v6 head beside the checkpoint prefix (four 64-hex
hashes plus the chain/network/seq/index fields, about 322 bytes at mainnet heights) and the
signature lists, which cost 194 bytes per (PUBKEY, SIG) pair. A v6 adds roughly 67 bytes for
PUBLISHER + ATTEST_SIG_COUNT and another 194 bytes per attesting signer. So 8192 - 6000 - 322 leaves about 1870 bytes of head budget, which is about nine signature pairs on a v1, or
four wrapper plus four attestation pairs on a v6. That reserve is the reason for 6000 rather
than a figure nearer 8000. It constrains chunk 0 only (a v2 continuation carries about 30 bytes
of overhead and could hold far more), but a single uniform slice keeps the splitter trivial.
This is the knob to lower as the federation grows: a v6 with a 5+5 quorum needs
ANCHOR_CHUNK_MAX_BYTES at or below about 5860, a 7+7 quorum about 5080. Raising it saves v2
transactions but overflows the head first, and the encoder rejects the oversize v1/v6.
ANCHOR_MATCH_BATCH_SIZE (200) and ANCHOR_MAX_BATCH (1000). The first is a latency
trigger, not a size cap: 200 pending rows flush an archive early instead of waiting out
ANCHOR_INTERVAL_MS, which bounds how much settled cross-chain state exists only in hub
databases. The second is the per-cycle SQL LIMIT and therefore the DOGE spend bound. Archived
rows are dominated by validator signatures and do not compress: roughly 0.55 KB of gzip+base64
per settled match, so 1000 rows is about 550 KB, about 93 chunks, about 93 DOGE transactions in
one cycle, while 200 rows is about 19. Both trade cost against archive latency and neither
changes what the archive means; too large spends more DOGE per cycle, too small drains the
backlog more slowly.
ANCHOR_ELECTION_TOLERANCE_BLOCKS (36). The unit is BTC blocks, not wall clock, precisely
so every hub computes the same rank unlock without clock synchronisation. 36 BTC blocks is about
6 hours at the 10-minute target, so each failover rank waits about 6 hours of elected-publisher
silence. The ordering is the load-bearing property:
signing round (120s) + DOGE burial (60 confs, ~1h) << 36 blocks (~6h) << ANCHOR_INTERVAL_MS (24h)
The left inequality keeps a healthy but slow rank-0 publisher from being overtaken, so the
federation never pays DOGE twice for the same checkpoint, and it keeps the on-chain
confirmation wait in the XANC_V0_DONE path well inside a single rank. The right inequality
means ranks 1 to 3 unlock at about 6, 12 and 18 hours, so up to three backups still get a slot
inside one publishing cycle and a dead rank 0 cannot cost a whole day of anchoring. Anything in
roughly 6 to 144 blocks (1 to 24 hours) preserves both bounds: below the DOGE burial window the
ladder burns DOGE on duplicate anchors, above about 144 a dead leader stalls a cycle. Neither
direction is a divergence risk, because concurrently unlocked publishers build byte-identical
archives; the cost of getting it wrong is DOGE or delay, never a fork. The same value bounds how
far a peer’s claimed election block may sit from the receiver’s own BTC tip when co-signing,
which is anti-spam only.
Recovery procedure (full-parse)
- Sync DOGE through the decoder/indexer from genesis:
anchor_actionspopulates from the chain alone. - Run
xchain-indexer/src/recovery.js --skip-stake-verification --i-understand-unverified: reassembles chunked batches byMATCH_BATCH_SEQ, gunzips, verifiesBATCH_CRC32, verifies each archived match’s/call’svalidator_signaturesagainst the archivedcapability_snapshots, rebuildscross_chain_matches+cross_chain_calls+capability_snapshots(latest-status-wins), and restores archivedrewards[]rows into the BTC indexer DB’svalidator_rewards(seeding the id maps is safe pre-reindex, they are append-only get-or-create). - Reindex BTC/LTC/DOGE from genesis against the recovered tables, cross-chain settlements,
XCALL injections,
oracle_round/attest_feerewards, and historical COLLECT claims all re-derive identically; finalblockshash triples must match the anchored checkpoints.
flowchart TD
S1["Step 1: Sync DOGE via decoder/indexer from genesis<br>anchor_actions populates from the chain alone"]
S2["Step 2: Run recovery.js with --skip-stake-verification --i-understand-unverified<br>reassemble chunked batches, verify CRC32 and signatures,<br>restore rewards into the BTC indexer DB's validator_rewards"]
S3["Step 3: Reindex BTC/LTC/DOGE from genesis<br>against the recovered tables"]
S1 --> S2
S2 -->|"must complete first, COLLECT validation reads validator_rewards at parse time"| S3
Ordering is load-bearing: the reward restore (step 2) MUST complete before the BTC reindex
(step 3); COLLECT validation reads validator_rewards synchronously at parse time, so a
reindex that reaches a historical COLLECT before its anchor rewards are restored replays it
invalid: no unclaimed rewards and the recovered ledger diverges. The stake cross-check is ON by
default and requires BTC_INDEXER_DB_NAME; against the empty pre-reindex BTC DB it would fail
every batch, so the writing reward-restore pass explicitly opts out with
--skip-stake-verification --i-understand-unverified (a bare --skip-stake-verification is forced
to a dry run). AFTER the BTC reindex, run a verifying pass with --dry-run (stake cross-check
default-on) to confirm every archived validator set is backed by real on-chain stake.
Notes
SNAPSHOT_BLOCKis distinct fromBLOCK_INDEX:BLOCK_INDEXis the checkpointed height onCHAIN;SNAPSHOT_BLOCKis always a BTC height (capability staking is BTC-only).- The handler reads
BLOCK_INDEX/SNAPSHOT_BLOCKfrom the wire payload. Not from the DOGE block the ANCHOR lands in. - Checkpoints for a reorged height are simply re-signed and re-anchored for the canonical
chain; the superseding record has a higher
CHECKPOINT_SEQ.
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.