XerisCoin
Protocol specification

Revision 2.0.0October 2026xrs-node 0.1.1Mainnet candidate · federated beta

XerisCoin is a Layer 1 whose blocks are produced every 4 seconds by one pipeline: Proof-of-History orders locally, a stake-weighted draw elects the leader, and Scrypt Proof-of-Work produces the block. Finality is 64 blocks deep. Every block carries a hybrid Ed25519 + ML-DSA-65 (Dilithium3) proposer signature from slot 1. The runtime executes 62 typed instructions over 23 contract types, 15 of them user-deployable, including the Alexandria real-world-asset layer and the Ari agent layer. Mainnet will launch as a federated beta: three roster producers, public self-staking closed. This revision records the CertiK module reviews of May to October 2026; Xeris has answered every round and deferred two consensus items, dependency upgrades and the fee model (§12.3).

1Introduction

XerisCoin is a Layer 1 in Rust (xrs-node 0.1.1). One 4 s slot runs three mechanisms (§2): a Proof-of-History hash chain orders events locally and is not a consensus input; a stake-weighted draw over validators holding at least 1,000 XRS selects the leader; Scrypt Proof-of-Work (N = 4,096, r = 4, p = 1; 2 MiB per hash) produces the block.

The runtime executes 62 native instruction variants (0–61) under one fee and replay model (§3, Appendix B). Contracts are 23 typed templates, no bytecode; 15 are user-deployable, 8 protocol-managed singletons (§5, Appendix C). SubDelegate, ZkPrivateTransfer, ZkIdentityProof and PqSignedTransfer are refused at the dispatcher (§12.2).

From slot 1 every block carries a hybrid Ed25519 + ML-DSA-65 (Dilithium3, FIPS 204) proposer signature; both components must verify (§9.1). Transactions are Ed25519 only. Finality is 64 blocks, ≈ 4.3 min (§2.4). Mainnet will launch as a federated beta: three roster keys, public self-staking closed (§12).

2Consensus

Each 4 s slot runs one pipeline: a Proof-of-History recorder ticks once and supplies the local slot number, a hash and a wall-clock timestamp; a stake-weighted draw over validators holding at least 1,000 XRS names the leader; the producer searches for a Scrypt Proof-of-Work solution for at most 3.9 s, under the leader target or, as a non-leader, under a target four times harder. The block carries a hybrid Ed25519 + ML-DSA-65 proposer signature and is broadcast. Validators recompute the leader, the target and the work; they do not recompute the PoH chain. Fork choice is by summed work and a reorganisation replaces at most 64 blocks.

Figure 1. The slot pipeline
PROOF-OF-HISTORYSHA-256 chain+ subsec_nanoslocal slot clockLEADER ELECTIONstake-weightedpubkey sortparent-hash seedSCRYPT MININGN=4096 r=4 p=13.9 s deadline4x after 2 slotsHYBRID SIGNATUREEd25519 + ML-DSA-65merkle rootP2P broadcast4-SECOND SLOT

2.1Proof-of-History

The recorder is a SHA-256 chain seeded with SHA-256("XERIS_V1_MAINNET_GENESIS_SEED_2026"). The producer loop ticks it once per 4 s iteration, not continuously, and the tick counter is the local slot clock. Each tick mixes the elapsed nanoseconds of the monotonic Instant clock into the preimage, so two nodes never produce the same hash for the same slot (XWC-27).

hash_0 = SHA-256("XERIS_V1_MAINNET_GENESIS_SEED_2026") tick: hash_n = SHA-256(hash_{n-1} ‖ "{count}:tick:{nanos}") // nanos = Instant::elapsed().subsec_nanos() count += 1 // count is the local slot time = max(time, wall_clock_ms) // non-decreasing block: poh_hash = hash_n at proposal · poh_timestamp = time (ms)

Validators do not replay the chain. Consensus checks the PoH fields of a block as listed below and nothing else. The poh_hash value is bound into the PoW preimage and the proposer signature; it is proposer-chosen bytes committed by work and signature, never recomputed by a peer.

Table 1. Consensus rules on the PoH fields.
FieldRuleWhere
poh_hashnon-emptyevery admission path
poh_timestampnot zeroevery admission path
poh_timestamp>= parent.poh_timestampevery admission path
poh_timestamp - parent.poh_timestamp>= 2,000 ms; equality failsevery admission path
poh_timestamp - wall clock<= 2,000 ms (MAX_FUTURE_TIMESTAMP_DRIFT_MS)live admission and fork ingest

2.2Proof-of-Work

Every block is mined and verified with Scrypt N = 4,096, r = 4, p = 1, empty salt, 32-byte output (2 MiB per hash). Two earlier parameter sets remain in scrypt_params_for_slot and cannot fire: SCRYPT_V2_SLOT and SCRYPT_UPGRADE_SLOT are both 0.

Table 2. Scrypt parameter sets in the node.
GenerationNrpMemory per hashStatus
Legacy1,02411128 KiBunreachable
v1.116,3848116 MiBunreachable
v1.24,096412 MiBevery block

The preimage is domain-tagged and length-prefixed from POW_PREIMAGE_V2_SLOT = 1, so one solution binds one parent, one timestamp and one transaction set (XWC-05). The target is 32 bytes of which only target[0] is non-zero; a larger byte is an easier target. The nonce starts at a random u64 and increments.

preimage = "XRS_POW_V2" ‖ slot u64 LE ‖ proposer 32 B ‖ len(prev_hash) u32 LE ‖ prev_hash ‖ poh_timestamp u128 LE ‖ len(poh_hash) u32 LE ‖ poh_hash ‖ nonce u64 BE ‖ len(merkle_root) u32 LE ‖ merkle_root hash = scrypt(preimage, salt = "", N = 4096, r = 4, p = 1) → 32 B valid ⇔ hash < target // 32-byte lexicographic compare

Mining stops at 3.9 s. The producer forfeits the slot and the template's transactions return to the mempool. The difficulty target is recomputed before every block from poh_timestamp values and the running difficulty committed after the parent, in integer arithmetic. Startup replay rebuilds it from 0x1f block by block; reorg simulation seeds it from the ancestor's recorded verdict.

Table 3. Difficulty adjustment.
RuleValue
Genesis target byte0x1f, also while fewer than 10 blocks exist
Windownewest min(len, 20) blocks
Target interval4,000 ms
Stepbase × clamp(avg_ms / 4,000, 0.75, 1.25), fixed-point 1/10,000
Clamp0x10 (hardest) to 0x3f (easiest)
Stall easinglast gap > 12,000 ms adds min((gap - 12,000) / 4,000, 0x10), capped at 0x3f

Slot continuity is exact. On every admission path (live, peer, replay, reorg) block.slot == parent.slot + 1, or the block is rejected (XWC-10). A proposer cannot choose among candidate child slots or advance slot-gated state such as unbonding.

A block whose proposer is not the expected leader must meet the non-leader target: the base target shifted right by two bits (base >> 2, 4× harder). Validators enforce this from LEADER_ENFORCEMENT_ACTIVATION_SLOT = 1: a block passes under the leader target if its proposer is the leader, under the non-leader target otherwise, and fails under neither (XWC-04). The producer mines as a non-leader only after NON_LEADER_GRACE_SLOTS = 2, that is 8,000 ms of chain silence; this is a liveness convention of the producer, not a consensus rule.

2.3Leader election

The leader of a slot is a deterministic function of the stake table after the parent block, the parent hash and the slot number. One function computes it on the producer, at live and peer admission, in startup replay, in reorg simulation and in the recovery scorer.

eligible = { (pk, stake) : stake ≥ 1,000 XRS } // MIN_STAKE_TO_MINE; the only rule if eligible = ∅ → leader = 0x00…00 // no leader: every producer mines at base >> 2 sort eligible by pk bytes, ascending total = Σ stake // u128 seed = SHA-256("XRS_LEADER_V1" ‖ parent.hash ‖ slot u64 LE) raw = u128_be(seed[0..16]) limit = (u128::MAX / total) · total target = raw < limit ? raw mod total : u128_be(SHA-256("XRS_LEADER_V1_RETRY" ‖ seed)[0..16]) mod total leader = first pk in order with target < cumulative stake

Eligibility is stake of at least 1,000 XRS and nothing else; there is no activity window. The seed uses the parent's PoW hash, not a PoH hash, so predicting a leader requires mining the parent (C-1). Admission re-checks the proposer's stake against the same threshold from slot 1 on every path. On mainnet only the three roster keys can hold stake, so the draw selects among them (§12.1).

2.4Finality and fork choice

There is no vote-based finality. Fork choice is by summed work, and a reorganisation replaces at most MAX_REORG_DEPTH = 64 blocks (≈ 4.3 min at 4 s). A block 64 or more below the tip is final on the public path; the producer roster of §12.1 is an operational gate, not a consensus vote.

Figure 2. Fork ingest and finality. A competing branch within 64 blocks is replayed in isolation and adopted only if its summed work is strictly greater.
FINAL (not revertible)tipCANONICALPEER BRANCH64 blocks · MAX_REORG_DEPTHwork = Σ 2^96 / (target_byte + 1)exact tip child → applywithin 64 → buffer, replay, comparebeyond 64 → reject
Table 4. Peer block ingest, in order.
Peer blockAction
proposer not in the roster (mainnet)rejected
serialized size > 4 MiBrejected
already canonicalignored
previous_hash == tip.hash and slot == tip.slot + 1validated and applied directly
slot outside [tip - 64, tip + 64]rejected
poh_timestamp > wall clock + 2,000 msrejected, retried later
otherwiseheader gate, then ForkBuffer

The header gate checks dimensions, PoH fields, Merkle root and hybrid signature and recomputes the PoW before a block enters the buffer. ForkBuffer holds at most 256 blocks and 260 MiB. A recovery thread assembles branches of at most 65 blocks from a canonical ancestor within 64 blocks and replays the candidate and the reference chain into isolated scratch ledgers through the production block validator.

work(block) = 2^96 / (effective_target_byte + 1) effective = target[0] if proposer == expected leader = (target >> 2)[0] otherwise adopt ⇔ Σ work(candidate) > Σ work(reference) tie → the branch whose first child after the ancestor has the smaller recovery identity

Work is scored from the target each block was validated against, leader or non-leader, not from realised hash values or height. A candidate is adopted only if it is strictly heavier. Publication renames the candidate ledger over the canonical file, fsyncs the directory, swaps the in-memory ledger, journals the displaced transactions and resets the PoH recorder to the new tip.

A separate deep-sync path (§10.2) replays a roster-supplied full history under the same work comparison and can replace blocks of any depth. The 64-block bound holds against the public network; it does not hold against the roster.

3Blocks, transactions and execution

A block is a signed header over a Merkle-committed list of at most 40,000 transactions. A transaction is a Solana-format message, Ed25519-signed, carrying account keys, a recent blockhash and at most 16 instructions. One admission gate runs at RPC and P2P ingress; block validation runs the same structural caps again. Every transaction pays one flat fee before its first instruction (§3.4) and its instructions commit one at a time (§3.5).

3.1Block structure

Block { slot: u64 hash: Vec<u8> // 32-byte Scrypt PoW hash nonce: u64 transactions: Vec<Transaction> // <= 40,000 merkle_root: Vec<u8> proposer: Pubkey // Ed25519 poh_timestamp: u128 // wall-clock ms previous_hash: Vec<u8> poh_hash: Vec<u8> proposer_sig: Vec<u8> // empty from slot 1 hybrid_proposer_sig: Option<HybridSignature> // { version: 1, ed25519_sig: 64 B, dilithium3_sig: 3,309 B } proposer_dilithium3_pk: Vec<u8> // 1,952 B, inline }

A block travels as bincode inside a 4-byte big-endian length frame of at most 5 MiB and is stored as one JSON line per block in ledger.dat, fsynced before the block is committed. The signed header is about 5.6 KiB; the producer reserves that footprint before filling the transaction list. hybrid_proposer_sig and the inline proposer_dilithium3_pk are mandatory from slot 1 (§9.1; registry binding from slot 2, §8.2). Slot 0 is not mined: the PoH recorder ticks from 0 to 1 before the first proposal, and a slot-0 block must carry zero transactions.

3.2Limits

Table 5. Block and transaction limits.
ConstantValueEnforced at
MAX_TXS_PER_BLOCK40,000block validation
MAX_BLOCK_SIZE_BYTES4 MiB serialisedblock validation; producer assembly
MAX_IX_PER_TX16ingress; block validation
MAX_ACCOUNTS_PER_TX64ingress; block validation
MAX_IX_DATA_SIZE8 KiB per instructioningress; block validation
MAX_SLASH_IX_DATA_SIZE65,535 B, SlashReport onlyingress; block validation
MAX_GROTH16_VERIFICATIONS_PER_BLOCK64block validation
MAX_RWA_POLICY_WORK_PER_BLOCK256 holder scansexecution; instruction rejected
MAX_MODEL_MUTATIONS_PER_BLOCK16execution; instruction rejected

The per-transaction caps are one set of constants shared by ingress and block validation, so a producer cannot place in a block what the mempool would refuse (XWC-15).

3.3Transaction admission

Figure 3. Transaction admission. Every gate runs at RPC and P2P ingress and again in block validation.
STRUCTUREsanitize()≤16 ix · ≤64 keys≤8 KiB per ixmalformedSIGNATURESEd25519 verifycount == requiredsemantics gatebad signatureSTATEnot processedblockhash ≤150 oldpayer ≥ 0.001 XRSreplay / expiredMEMPOOL50,000 / 64 MiB256 tx per payerflat priorityquotaBLOCK≤40,000 tx · 4 MiBfee before ix 0per-ix commitskipped, fee keptreceipt: confirmed · partial · failed

Every ingress path, POST /submit and both P2P transaction handlers, runs one gate in a fixed order. sanitize() must pass; num_required_signatures is at least 1; signatures.len() equals it; the instruction list is non-empty and within the caps of Table 5; every instruction decodes as a XerisInstruction or a SystemInstruction. Then tx.verify() checks the Ed25519 signatures. Then a stateless semantics gate rejects the legacy SystemInstruction::Transfer, a NativeTransfer whose to is not a canonical public key or names a reserved __* key, a zero amount on NativeTransfer, TokenMint or RWATransfer, an AgentExecute or ConditionalOrder that nests another, and SubDelegate (XWC-82).

State checks follow. The first signature must be absent from the processed set and from the mempool. recent_blockhash must be one of the last 150 block hashes, evaluated at the slot of the next block. A payer whose balance is below BASE_TX_FEE is routed to the underfunded population cap rather than the normal queue, unless the transaction is a fee-exempt attestation (§4.4). The mainnet Stake gate is in §12.1.

Table 6. Mempool bounds.
Mempool boundValue
Entries50,000
Bytes64 MiB
Per transaction128 KiB
Per payer256 transactions; 4 MiB
SlashReport transactions128 in the pool; 4 per reporter
Underfunded payers5,000 (10 % of entries)
Priorityflat; BASE_TX_FEE for every transaction
Evictions per admission64, cheapest first, only below the incoming fee

Transactions in an in-flight proposal are reserved for the mine-to-commit window and count as present for deduplication (XWC-17). Ordering and eviction are in §10.4; write-RPC limits in §10.5.

3.4Fees and replay

The fee is BASE_TX_FEE = 1,000,000 lamports (0.001 XRS) per transaction, independent of instruction count or size. It is debited from account_keys[0] before instruction 0 and credited to the block proposer. Fees are not burned; XRS leaves supply only through slashing. A transaction whose payer cannot cover the fee is skipped without execution, the block stays valid, and its signature is still recorded as processed. The only exemption is a transaction whose sole instruction is a ValidatorAttestation that passes every rule of Table 10 (XWC-60).

recent_blockhash in { hash(b) : b in last 150 blocks } // BLOCKHASH_EXPIRY_WINDOW or 0x00..00 (32 bytes) iff the chain is empty signatures[0] not in block // duplicate within the block signatures[0] not in processed_signatures // <= 10,000,000 entries, FIFO by slot SystemInstruction::Transfer // rejected from slot 0

The blockhash is the block's Scrypt hash, served by getLatestBlockhash (Table 21). The processed-signature set is written into every snapshot (§10.3).

3.5Execution semantics

Instructions execute in message order. Each produces one outcome (signature, index, ok) that defaults to failed and is set to ok only when the handler commits. A failed instruction changes no state and the next instruction still runs: commit is per instruction, with no transaction-level rollback. Inside a handler every check precedes the first mutation (contract calls: §5.1). The per-block work budgets of Table 5 can reject an otherwise valid instruction, which then fails like any other.

Table 7. Transaction status.
Transaction statusRule
confirmedevery instruction committed
partialsome committed; first_failed_index names the first that did not
failednone committed, or the transaction never executed

A transaction skipped at the fee gate is not recorded and is never reported as history. Outcomes and receipts live in a SQLite store derived from execution; consensus never reads them (§10.5). An integrator that needs atomicity sends one instruction per transaction.

4Economics

XRS has 9 decimals; 1 XRS is 1,000,000,000 lamports. Genesis credits the treasury 8evPjjozSHNcoGRcv7zzxwan9sf3ubJ8q9CFzms6AK97 with 200,000,000 XRS, of which 1,000 XRS is staked and 199,999,000 XRS is liquid. Every other XRS is minted as a reward against one budget, MAX_EMISSION_SUPPLY = 500,000,000 XRS, shared by mining, staking and attestation. The nominal total is 700,000,000 XRS.

Table 8. Supply.
AllocationXRSShareMechanism
Treasury200,000,00028.6 %Genesis balance; 1,000 XRS of it staked at genesis
Emission budget500,000,00071.4 %Mining + staking + attestation rewards; MAX_EMISSION_SUPPLY
Total (nominal)700,000,000100 %Treasury + emission budget
Only the emission budget is consensus-enforced. The treasury key is the mint authority of the wrapped token xrs_native (wXRS, max supply 700,000,000), and UnwrapXrs (14) credits native XRS 1:1 without touching total_mined. The 700,000,000 XRS total therefore rests on the treasury not minting wXRS, not on a protocol check.

4.1Emission

Every accepted block credits its proposer with a mining reward. The reward is BASE_BLOCK_REWARD = 10 XRS right-shifted once per 25,000,000 slots (HALVING_INTERVAL), the shift capped at 63. One epoch is 25,000,000 slots × 4 s ≈ 3.17 years. The reward is then clamped to the emission remaining after all earlier rewards and the attestation rewards already granted in the same block.

reward(slot) = 10 XRS >> min(slot / 25,000,000, 63) reward = min(reward, MAX_EMISSION_SUPPLY − (total_mined + block_attestation_rewards)) epoch 0 slots 1 – 24,999,999 10 XRS / block 249,999,990 XRS (no slot-0 block) epoch 1 slots 25,000,000 – 49,999,999 5 XRS / block 125,000,000 XRS epoch 2 slots 50,000,000 – 74,999,999 2.5 XRS / block 62,500,000 XRS epoch 3 slots 75,000,000 – 99,999,999 1.25 XRS / block 31,250,000 XRS epoch 33 reward 1 lamport · epoch 34 onward reward 0 series limit 500,000,000 XRS · exact integer mining total 499,999,989.725 XRS
Figure 4. Cumulative emission
0M100M200M300M400M500M600M700MH1H2H3H4CAPTREASURY0M25M50M75M100MBLOCK HEIGHT
Mining, staking and attestation rewards draw on one 500,000,000 XRS budget. The curve is the mining series alone; the nominal total is 700,000,000 XRS (§4).

Within one block the budget is consumed in a fixed order: attestation rewards as transactions execute, then the mining reward, then staking rewards. Each step is capped by what the earlier steps left. total_mined is the sum of all three and is the only counter the cap reads.

4.2Staking rewards

At every slot that is a multiple of 900 (STAKING_REWARD_INTERVAL, one hour) each account with at least 100 XRS staked receives 7 % per year pro rata on its own stake. BLOCKS_PER_YEAR is 7,884,000, so there are exactly 8,760 payouts per year. The reward is credited to the liquid balance; it does not compound. Stakers are paid in Pubkey byte order; when the emission remainder cannot cover all of them, earlier keys are paid in full and the last payable key receives the truncated remainder (XWC-52).

trigger slot > 0 and slot % 900 == 0 eligible stake >= 100 XRS r = floor(stake × 7 × 900 / (100 × 7,884,000)) lamports // 1,000 XRS → 7,990,867 lamports per payout budget MAX_EMISSION_SUPPLY − (total_mined + block_attestation_rewards + mining_reward)
A new Stake must bring the account to at least 1,000 XRS and a partial Unstake must leave 0 or at least 1,000 XRS. A stake of 100 to 999 XRS can therefore exist only as the remainder after a slash. Rewards need no claim (§12.2).

4.3Stake and unbonding

Stake (9) and Unstake (10) require the signer to equal pubkey. Stake moves lamports from the liquid balance to the stake table and counts at the next leader draw and payout. Unstake moves them to an unbonding entry that matures 151,200 slots later (UNBONDING_PERIOD_SLOTS, 7 days). After each applied block the node releases every matured entry to the liquid balance; no instruction claims it. Unbonding stake earns nothing, elects nothing and remains slashable. A rejected Stake or Unstake is a failed instruction (§3.5); its fee is still charged. On mainnet a Stake naming a key outside the three-producer roster is rejected at every ingress and validation path (§12.1).

Table 9. Stake and unbonding rules.
RuleValue
Stake resulting stake≥ 1,000 XRS (MIN_STAKE_TO_MINE); a lower first stake or top-up is rejected
Unstake partial amount≥ 1 XRS (MIN_UNSTAKE_AMOUNT); a full exit is always allowed
Unstake remainder0 or ≥ 1,000 XRS
Pending entries per account≤ 10; an 11th Unstake is rejected and the stake refunded
Unbonding queue≤ 10,000 entries (MAX_UNBONDING_QUEUE); overflow is rejected and refunded
Maturitycompletion_slot = start_slot + 151,200; released after the first block at or past it

4.4Light-client attestation

ValidatorAttestation (12) pays 0.01 XRS (ATTESTATION_REWARD) to a staked validator that names a recent block by slot and full 32-byte hash. The reward is credited to the liquid balance and counted against the emission budget. A transaction whose only instruction is a valid attestation pays no fee (XWC-60).

Table 10. Attestation rules, in handler order.
RuleValue
Signerequals validator
Stake≥ 100 XRS (MIN_ATTESTOR_STAKE)
Ageblock.slot − block_slot ≤ 200 (ATTESTATION_SLOT_WINDOW)
Hashblock_hash_prefix is the 32-byte hash of the block at block_slot in recent_blocks (last 1,000 blocks)
Dedup(block_slot, validator) is paid once
Rateno prior attestation by the validator for a slot > block_slot − 10; one reward per 10 slots
Rewardmin(0.01 XRS, MAX_EMISSION_SUPPLY − (total_mined + block_attestation_rewards))

4.5Slashing

SlashReport (38) is the only slashing path. The offence is a double-sign: two distinct block headers at one slot by one proposer. Any account other than the offender may report. The evidence is a JSON array of exactly two Block values with empty transaction lists; the handler rebuilds each canonical signing payload and verifies the proposer signature under the regime active at that slot, the hybrid Ed25519 + ML-DSA-65 check using the PQ key bound to the owner at that slot (XWC-53). Evidence older than 302,400 slots (PQ_KEY_HISTORY_RETENTION_SLOTS, 14 days) is rejected; the evidence limit is in Table 5.

offence = two distinct, validly signed headers at one slot by one proposer window = block.slot − violation_slot <= 302,400 slashable = active stake + unbonding entries with completion_slot > block.slot slash = slashable / 10 // active stake first, then unbonding oldest-first reporter = slash / 20 // 5 % of the slash, to the reporter's liquid balance burned = slash − reporter // 95 %, credited nowhere dedup = SHA-256("XRS_SLASH_OFFENSE_V2" ‖ owner ‖ violation_slot_le64 ‖ lo_digest ‖ hi_digest)

The dedup key is recorded in the protocol contract xeris_slashing_registry, so one offence slashes once regardless of how the evidence is encoded or labelled (XWC-30). The slashed share of an unbonding entry is reduced in place and is never released. A report whose applied slash is zero fails and records no key, so the offence stays reportable (XWC-13). This path has no minimum slash amount.

5The contract engine

A contract is a typed state machine. The engine defines 23 ContractType variants, each with a fixed state struct and a fixed method set; there is no bytecode, no virtual machine and no user-supplied code. ContractDeploy (5) creates an instance from a type string and a JSON parameter object; ContractCall (4) invokes one method by name. Fifteen types are user-deployable. Eight are protocol-managed singletons under reserved xeris_* ids, created by the protocol and driven only by their dedicated instructions: DeviceRegistry, ZkVerifierRegistry, PqKeyRegistry, ConditionalOrderBook, DisputeRegistry, DealRegistry, TaskBoard, StateChannelRegistry. Appendix C lists every type with its aliases.

Figure 5. The 23 contract types by group. * protocol-managed singleton, not user-deployable.
XerisInstruction runtimeBASICTimeLockEscrow / MultiSigVestingStateChannelRegistry*DEFISwap (x·y=k)Launchpad (curve)LimitOrder / DcaOrderConditionalOrderBook*ALEXANDRIA (RWA)RealWorldAssetlegal_doc_hashapproved_holdersaccredited_onlyARIAgentRegistryIdentityRegistryCapabilityRegistryOracleRegistry / ModelRegistryARI (PROTOCOL)TaskBoard*DisputeRegistry*DealRegistry*DeviceRegistry*CRYPTO / GOVERNANCEZkVerifierRegistry*PqKeyRegistry*GovernanceGroth16 · ML-DSA-65

5.1Deploy and call rules

Every deployment and every call passes the rules below before type-specific logic runs. The caller a contract sees is the transaction fee payer. A transaction pays the flat 0.001 XRS fee and nothing more; there is no deploy or call fee. Mutating calls execute against a clone of the contract and the clone replaces the stored contract only when the method returns Ok.

Table 11. Rules shared by every contract type.
RuleValue
Contract id1–128 characters; alphanumeric (Unicode is_alphanumeric), _ or -
Existing idnever overwritten; the deploy is skipped
Reserved id namespacesxeris_*, identity_*, agent_registry_*, __*, *_xrs_pool: not user-deployable
Protocol-managed typesthe eight singleton types are refused by ContractDeploy
Deploy parametersparams_json parsed as a JSON object; unparseable input becomes {}
Calleraccount_keys[0], the fee payer
Call argumentsa UTF-8 JSON object; current_slot is overwritten with the block slot
Binary exceptionSwap swap_a_to_b / swap_b_to_a with exactly 16 bytes: u64 LE amount ‖ u64 LE min_output
Protected methodssingleton internals (e.g. place_order, cancel_order, evaluate on xeris_conditional_orders) refused on generic, delegated and conditional-order paths
Inactive contractevery call fails with "Contract is not active"
Registry readspaged: at most 32 items and 16 KiB per query

5.2TimeLock, Escrow, Vesting, MultiSig

Four custody types hold a token balance and release it under one rule each. They move token_balances only, so native XRS enters them as wrapped xrs_native (WrapXrs, 13). TimeLock, Escrow and Vesting refuse an RWA token at deploy; a MultiSig transfer of one passes the compliance gate of §7.2. Timestamps are block poh_timestamp values in milliseconds; TimeLock does not range-check unlock_timestamp, and a value in the past is releasable at once.

Table 12. The four custody types.
TypeDeploy parametersMethodsRule
TimeLockbeneficiary, token_id, amount, unlock_timestamp (ms); amount debited from the deployerreleaseanyone may call once the block timestamp reaches unlock_timestamp; pays the beneficiary; no cancel, extend or reassign
Escrowparty_a (must equal the signer), party_b (distinct), token_id, amountconfirm, cancelboth parties confirm → amount to party_b; party_a may cancel until completion, even after party_b confirmed; no expiry
Vestingbeneficiary, token_id, total_amount, cliff_timestamp, end_timestamp (ms); cliff ≥ now, end > now, cliff ≤ endclaimbeneficiary only; vested = total × (now − start) / (end − start), linear from the deploy timestamp; the cliff gates the first claim; no revoke
MultiSigsigners[] (distinct public keys), threshold (1 ≤ t ≤ n); no funds are lockedpropose, approve, rejectone pending proposal at a time, numbered by a nonce that approve and reject must name; at threshold approvals a transfer action moves the deployer's own native (XRS, xrs_native) or token balance; min(t, n − t + 1) rejections clear it

5.3Swap

A Swap is a constant-product pool of two distinct non-RWA tokens. Deploy debits amount_a and amount_b from the deployer and mints isqrt(amount_a × amount_b) shares, which must exceed 1,000; 1,000 shares go to a dead address and are never redeemable. fee_bps defaults to 30 and may be set from 0 to 10,000 at deploy. The fee stays in the reserves and accrues to liquidity providers; the protocol takes no cut of swaps. add_liquidity and remove_liquidity take signed minimums and fail below them. Shares are entries in the pool state, not a token.

deploy shares_0 = isqrt(amount_a × amount_b) require shares_0 > 1,000 lp[dead] = 1,000 · lp[owner] = shares_0 − 1,000 · T = shares_0 swap fee = floor(in × fee_bps / 10,000) // fee_bps default 30 in_net = in − fee out = floor(R_out × in_net / (R_in + in_net)) require in > 0 · balance ≥ in · 0 < out ≤ R_out · out ≥ min_output R_in += in // fee remains in the pool R_out −= out add shares = min(a × T / R_a, b × T / R_b) · accepted_x = ceil(shares × R_x / T) remove out_x = floor(shares × R_x / T)

5.4Launchpad

A Launchpad creates the token lp_<id> and sells part of its supply on a constant-product curve with virtual reserves. The token is LaunchpadManaged: its supply is minted only by curve buys and TokenMint is refused. Nothing is debited at deploy. liquidity_bps of the supply (default 2,000; clamped to 500–4,000) is reserved for the graduation pool and the curve sells the remainder; the virtual reserves are chosen so the curve price at sellout equals the opening price of the pool. Every buy and sell pays 1 % to the creator, accrued until claim_rewards, and 0.77 % to the treasury address, credited in the same call. Buyers pay in wrapped xrs_native. A launchpad contract id is at most 116 characters so that the pool id fits in 128.

defaults total_supply = 1,000,000,000 tokens (9 decimals) T = target_liquidity_xrs = 10,000 XRS · liquidity_bps = 2,000 (500–4,000) split L = floor(total_supply × liquidity_bps / 10,000) // pool reserve s = total_supply − L // curve inventory; require L > 0, s > L x_v = ceil(L × T / (s − L)) y_v = floor(L × s / (s − L)) reserves x = xrs_collected + x_v y = tokens_remaining + y_v buy net = xrs − floor(xrs × 100 / 10,000) − floor(xrs × 77 / 10,000) tokens_out = min(floor(y × net / (x + net)), tokens_remaining) sell raw = min(floor(x × t / (y + t)), xrs_collected) xrs_out = raw − floor(raw × 100 / 10,000) − floor(raw × 77 / 10,000) graduate require xrs_collected ≥ T Swap lp_<id>_xrs_pool { token_a: lp_<id>, token_b: xrs_native, L, xrs_collected, fee_bps: 30 } owner = __launchpad_pool__

finalize_curve is permissionless once xrs_collected ≥ T. The ledger credits the pseudo-account __launchpad_pool__ with the collected XRS and the tranche L and deploys the Swap above; the shares belong to that non-signable account, so the liquidity cannot be withdrawn. If the pool id already exists or seeding fails, the finalisation reverts and the launchpad stays open. Launchpad methods execute only through a direct ContractCall: AgentExecute and conditional-order inner calls to a launchpad are refused. vesting_enabled: true is rejected at deploy.

5.5Standing orders

ConditionalOrder (23) places a standing order in the singleton xeris_conditional_orders. The order escrows locked_amount from the signer's native balance into __escrow_order_<id>; the minimum is the refundable 0.01 XRS storage bond, and an inner NativeTransfer must be covered by it. An order lives at most 650,000 slots (≈ 30 days); an owner holds at most 100 live orders, the book at most 10,000; the inner instruction is at most 2,048 bytes of bincode. The protocol evaluates every live order once per block after all transactions and executes triggered orders in order_id order; there are no keepers. A failed or expired order is cancelled and its escrow refunded. CancelConditionalOrder (24) refunds the owner.

Table 13. Condition types and their trigger rules.
ConditionSourceFires when
slot_reached—current slot ≥ threshold
price_above / price_belowa deployed Swap contract idpool median price ≥ / ≤ threshold, in base units of token_b per 10^9 base units of token_a
balance_above / balance_below—the owner's native balance ≥ / ≤ threshold; a missing entry counts as 0
oracle_valuean active oracle feedlatest value ≥ threshold; feed owner and type bound at placement; value no older than min(update_interval_slots, 900) slots

A pool price is observed once per block, after transactions, for the at most 64 pools referenced by live price orders: spot = reserve_b × 10^9 / reserve_a, skipped and the window cleared when either reserve is below 1,000. The trigger price is the median of the last 61 observations and exists only after 20. Moving the median requires holding an off-market price for 31 consecutive blocks against arbitrage.

LimitOrder and DcaOrder deploy and escrow but never fill: execute_limit_order and execute_dca_tick return an error. cancel_limit_order and cancel_dca_order refund the escrow in full.

6The Ari agent layer

Ari (Autonomous Runtime Infrastructure) is a set of registries reached by dedicated instructions: self-sovereign identities with per-category reputation, bounded delegation from an owner key to agent keys, capability listings, escrowed tasks, stake-weighted dispute panels, two-party deals, payment channels, staked oracle feeds, attested devices, model records and liveness heartbeats. Each registry is a contract created on first use under the id in Table 14; Appendix C marks which types are protocol-managed. SubDelegate (22) is refused at ingress and skipped at the dispatcher; delegation is one tier deep and there is no agent hierarchy (XWC-82).

Figure 6. Owner-to-agent delegation. SubDelegate (22) is disabled; there is no second tier.
OWNER (Ed25519)RegisterAgentAGENT REGISTRYmax_per_tx | max_daily | allowed_contractsallowed_operations | expires_at_slot | revokedSubDelegate: disabled (XWC-82)IDENTITYreputation by categorycredentials ≤ 100CAPABILITYcategory · tags · regionprice_per_unitTRADING AGENTmax_per_tx 50 XRS · daily 500 XRSDATA AGENTallowed_operations: [ContractCall]AgentHeartbeat · liveness ≤ 5,400 slots
Table 14. Ari registries and their fixed contract ids.
RegistryContract idCreated by
Identity rootxeris_identitiesCreateIdentity (18)
Identityidentity_<sha256(pubkey)[..32]>CreateIdentity (18), one per public key
Agent registryagent_registry_<sha256(owner)[..32]>RegisterAgent (15), one per owner
Oraclesxeris_oraclesRegisterOracle (25)
Devicesxeris_devicesHardwareAttest (27), after the proof verifies
Capabilitiesxeris_capabilitiesRegisterCapability (28)
Tasksxeris_tasksPostTask (31)
Modelsxeris_modelsRegisterModel (34)
Disputesxeris_disputesOpenDispute (36) or DisputeDeal (58)
Dealsxeris_dealsCreateDeal (54)
Channelsxeris_channelsOpenChannel (42)
Heartbeatsxeris_heartbeatsAgentHeartbeat (45)

6.1Agent delegation

RegisterAgent (15) is signed by the owner and writes an AgentEntry into the registry of that owner. A registry holds at most 50 agents. UpdateAgent (16) changes any limit and sets or clears revoked; revocation is the kill switch and the owner may reverse it.

AgentEntry { agent_pubkey // the key the agent signs with max_per_tx // lamports per delegated instruction max_daily // lamports per 21,600-slot window (DAILY_SLOTS) allowed_contracts // contract ids; empty = all allowed_operations // operation names; empty = all expires_at_slot // 0 = never revoked // set and cleared by the owner daily_spent · daily_window_start · total_txs · total_spent }

AgentExecute (17) is signed by the agent and carries a bincode-encoded inner instruction that executes as the owner. The inner instruction must be one of NativeTransfer, TokenTransfer, ContractCall, WrapXrs, UnwrapXrs, Stake, Unstake, TokenMint, TokenBurn; any other variant is refused. The spend of a transfer, wrap, stake, mint or burn is its amount. The spend of a ContractCall is derived from the method, and a method whose spend cannot be bounded is refused.

Table 15. Spend derivation for a delegated ContractCall.
Delegated ContractCall methodSpend charged to the budget
buy_tokens · sell_tokensxrs_amount · token_amount
swapamount_in + input_amount + amount
add_liquidityquoted accepted_a + accepted_b, else amount_a + amount_b
remove_liquidity · create_dca_order · place_ordershares · total_amount · locked_amount
post · open · createreward · deposit · amount
cancel · reclaim · claim_rewards · redeem · amend · list · status · get_stats · get_key0
confirm · verify · any other methodrefused: spend lives in contract state (XWC-87)

Validation runs in this order: the agent is registered, not revoked and not expired; the spend is at most max_per_tx; the window resets after 21,600 slots and the spend is at most max_daily minus daily_spent; the contract allowlist applies to a ContractCall target; the operation allowlist applies to the operation name. The budget is recorded only after the inner instruction succeeds. A delegated call to an agent registry, a sealed protocol method, a Launchpad contract or an RWA contract is refused. A nested AgentExecute or ConditionalOrder is refused at ingress.

6.2Identity and reputation

CreateIdentity (18) creates one contract per public key; the signer must be the identity. identity_type is one of agent, device, service, human; display_name is at most 128 B and metadata_json at most 4,096 B. A non-empty parent_identity must co-sign the transaction, and the child is appended to the parent, which holds at most 200 children. UpdateIdentity (19) changes name and metadata; deactivated: true is one-way and there is no reactivation path. An active identity gates capability listings, task claims, model records, heartbeats and bound devices.

AttestReputation (20) requires an active attestor identity and refuses self-attestation. The category is one of reliability, accuracy, speed, honesty, safety, general; the score is clamped to 0–100 and the identity keeps a running average per category. An identity holds at most 100 credentials. AgentMessage (21) is checked for type, payload size (≤ 8,192 B) and an active sender, and writes no contract state; the message exists only in the block.

6.3Capabilities and tasks

RegisterCapability (28) and UpdateCapability (29) are signed by the provider_identity, which must be active. A listing is keyed provider:category and holds tags, region (default global), description ≤ 2,048 B, price_per_unit, max_concurrent, metadata_json ≤ 4,096 B and reputation_snapshot, the reliability average of the provider at registration. A provider holds at most 100 listings; 50,000 may be active. QueryCapabilities (30) is a no-op in a block; discovery is GET /capabilities/search.

PostTask (31) escrows reward from the poster. min_reputation must be 0; expires_at_slot lies within 648,000 slots (≈ 30 days) of the posting slot; verification is poster_confirm or oracle, where the oracle is a named approver key, and automatic is removed (XWC-74). At most 100,000 tasks are live. ClaimTask (32) requires the claimant to be the signer, to hold an active identity and, when required_category is set, an active listing in that category carrying at least one required_tag. ResolveTask (33) carries one of the resolutions below.

post poster escrows reward → open claim claimant == signer; listing in required_category → claimed (when max_claimants is reached) complete a claimant submits proof before the deadline → completed verify poster | verification_oracle; escrow paid once → verified reject same approver; rejections < 3; proof cleared → claimed reject third rejection; escrow refunded to the poster → rejected cancel poster, from open only; escrow refunded → cancelled expiry every block; open | claimed past deadline; refunded → expired

6.4Oracles, devices, models and heartbeats

Four further registries follow the same pattern: a dedicated instruction, a signer bound to the record and a fixed cap. The device registry is protocol-managed and is created only after the first attestation proof verifies.

Table 16. Oracle, device, model and heartbeat rules.
RegistryInstructionRule
OracleRegistryRegisterOracle (25)feed_type in price, event, sensor, weather, custom; stake >= 1 XRS locked from the caller; description <= 512 B; at most 1,000 active feeds
OracleRegistryOracleSubmit (26)owner only; metadata <= 1,024 B; the feed keeps the last 1,000 (slot, value) points
DeviceRegistryHardwareAttest (27)device_type in humanoid, terminal, iot, mobile, secure_element; signer is the device or its bound identity; proof is a 64-byte Ed25519 signature by the device key over the challenge below; at most 100,000 active devices
ModelRegistryRegisterModel (34) · UpdateModel (35)identity_pubkey == signer with an active identity; a record is at most 2,048 B; at most 10,000 live models; 16 model mutations per block
HeartbeatsAgentHeartbeat (45)identity_pubkey == signer with an active identity; records older than 21,600 slots are dropped; at most 10,000 records; alive = last beat within 5,400 slots (≈ 6 h)
challenge = "XRS_HW_ATTEST_V2" ‖ identity(device_pubkey) ‖ identity(bound_identity) // 0x01 ‖ 32 B for an Ed25519 key, else 0x00 ‖ len ‖ bytes ‖ len ‖ device_type ‖ len ‖ manufacturer ‖ len ‖ model ‖ len ‖ firmware_version ‖ slot u64 LE // the block slot the transaction lands in proof = Ed25519(device_key, challenge) // 64 B

6.5Disputes

OpenDispute (36) names a defendant and posts a bond; the bond is at least 1 XRS for a deal dispute and may be 0 otherwise. The panel is a snapshot of every validator staked at least 1,000 XRS at the opening slot, minus the two parties, weighted by stake; an empty panel is refused. At most 10,000 disputes are open.

ResolveDispute (37) carries one action. During the 21,600-slot challenge window (≈ 1 day) only evidence from the disputer and defendant_evidence from the defendant are accepted. After it, vote_disputer, vote_defendant and vote_dismiss are accepted from panel members whose stake is still at least 1,000 XRS; one vote per validator, the latest counts. The dispute is ruled when one option reaches an absolute majority of the snapshot weight. A ruling moves only the bond: refunded when the disputer wins, forfeited otherwise. expire after 648,000 slots (≈ 30 days) refunds the bond.

6.6Deals

A deal escrows the same amount from each party; the pot is deposit_a + deposit_b. Every instruction after CreateDeal carries the deal instance, a counter that is never reused. At most 100,000 deals are live.

CreateDeal (54) A escrows amount; counterparty distinct; amount > 0 → proposed CancelDeal (57) A only, while proposed; A refunded → cancelled AcceptDeal (55) B only; instance, party_a, amount, sha256(terms) must match; B escrows amount → active ConfirmDeal (56) A or B; when both have confirmed, each deposit is refunded → completed ReclaimDeal (60) A or B, while active, after created_slot + 648,000 slots; both deposits refunded → completed DisputeDeal (58) A or B, while active; bond >= 1 XRS; escrow frozen; opens dispute deal_<instance>_<deal_id> against the other → disputed SettleDeal (59) anyone, after the ruling; winner takes the pot; dismissed | expired → each deposit refunded → settled

6.7State channels

OpenChannel (42) escrows the deposit of the opener; the counterparty joins through ContractCall join. CloseChannel (43) carries the 64-byte Ed25519 signature of the other party over the close message below, and the final balances must sum exactly to the deposits. ForceCloseChannel (44) with state_sequence 0 claims the original split; with a newer signed state it opens a 1,000-slot challenge (≈ 67 min), during which challenge_update supersedes it with a newer signed state and after which finalize_dispute pays the recorded split. Expired channels settle automatically, at most 256 per block. At most 100,000 channels are unsettled.

close = "XRS_CLOSE_CH_V4" ‖ len ‖ chain_id ‖ len ‖ channel_id ‖ generation u64 LE ‖ created_slot u64 LE ‖ identity(party_a) ‖ identity(party_b) ‖ final_balance_a u64 LE ‖ final_balance_b u64 LE ‖ message_count u64 LE state = "XRS_CH_STATE_V3" ‖ same prefix ‖ balance_a u64 LE ‖ balance_b u64 LE ‖ state_sequence u64 LE sig = Ed25519(counterparty_key, message) // 64 B; balances always in (A, B) order

7Alexandria real-world assets

An Alexandria token is a token-registry entry whose rwa_metadata binds it to an off-chain Ricardian document by SHA-256 hash. TokenCreateRWA (6) creates the entry; the signer must equal mint_authority; the registry seeds approved_holders with the issuer. The compliance gate reads this entry, not the issuer contract of §7.3, before every credit.

7.1Token metadata

RWAMetadata { asset_type: String // real_estate | equity | debt | commodity | ip | collectible | fund | bond legal_doc_hash: String // SHA-256 of the Ricardian document; must be non-empty, otherwise unvalidated legal_doc_uri: String // IPFS, Arweave or HTTPS; unvalidated jurisdiction: String // free-form, e.g. "US-WY"; recorded, never enforced status: String // active | frozen | redeemed | disputed | revoked; "active" at creation transfer_restricted: bool accredited_only: bool valuation: u64 // USD cents approved_holders: Vec<String> // canonical Ed25519 public keys, sorted; <= 1,024 (MAX_RWA_APPROVED_HOLDERS) status_history: Vec<(u64, String, String)> // (slot, old_status, new_status); first entry (created_slot, "created", "active") }

RWAUpdateStatus (7), signed by mint_authority, writes any of the five statuses from any current one, appends a status_history tuple and replaces valuation, legal_doc_hash and legal_doc_uri when supplied.

7.2Compliance gate

enforce_rwa_credit runs before every credit of an RWA token: TokenMint (0), TokenTransfer (1), RWATransfer (8) and a MultiSig-approved transfer. A token without rwa_metadata passes. The checks run in this order; the first failure rejects the instruction.

  • Roster larger than 1,024 entries: refused.
  • status other than active: no mint, no transfer.
  • transfer_restricted: the recipient must be an approved holder.
  • accredited_only: every recipient, minted or transferred, must be an approved holder.

The fourth check applies even when transfer_restricted is false: the roster is the accreditation roster. RWATransfer adds amount > 0, from != to and signer == from; it is otherwise identical to TokenTransfer.

TimeLock, Escrow, Swap, Vesting, LimitOrder and DcaOrder refuse an RWA token on any leg. A block carries at most 256 units of RWA policy work (MAX_RWA_POLICY_WORK_PER_BLOCK): each RWA TokenMint or TokenTransfer, each RWATransfer and each approve_holder or revoke_holder call costs 1, also as a delegated or conditional inner instruction. A block over the limit fails preflight; an instruction that would exceed the total is rejected, and a triggered conditional order is deferred.

7.3Issuer contract

ContractDeploy (5) with type rwa by the token's mint_authority creates a RealWorldAsset contract for one token_id. asset_type, legal_doc_hash, legal_doc_uri, jurisdiction and approved_holders are copied from the registry; caller-supplied values are ignored. Every method is issuer-only and accepts a direct ContractCall (4) only; AgentExecute inner calls and conditional-order calls to an RWA contract are refused.

Table 17. Methods of the RealWorldAsset contract.
MethodArgumentsEffect
approve_holder{"address"}adds a canonical public key to the contract roster and the registry roster in one write; both rosters must already match; refused when the key is present, the roster holds 1,024 entries or the asset is redeemed
revoke_holder{"address"}removes the key from both rosters; the issuer cannot be revoked
amend_legal_doc{"legal_doc_hash", "legal_doc_uri"}replaces the hash and URI in the contract and the registry; refused after redemption
redeemnonesets redeemed_at, deactivates the contract and sets the registry status to redeemed
distribute, toggle_distributionsanydisabled; the call fails

There are no on-chain distributions; distributions_enabled and distribution_history are never written. Either redeem or RWAUpdateStatus with redeemed redeems the asset.

8ZK and post-quantum cryptography

Three primitives are reachable from consensus: Ed25519, ML-DSA-65 (Dilithium3) and Groth16 on BN254; §9 gives the signing contexts. Groth16 verifies proofs against verification keys registered by staked validators and is classically secure only. crypto.rs also holds Pedersen commitments, a Schnorr proof, a range proof and a WOTS+/XMSS verifier; no dispatcher arm calls them.

Table 18. Cryptographic primitives in the node.
PrimitiveAlgorithmUseStatus
Ed25519Curve25519Transactions; P2P authentication; channel, device and slash messages; classical half of the block signatureactive
ML-DSA-65 (Dilithium3)Lattice, FIPS 204, pqcrypto-mldsaPost-quantum half of the block signature; xeris_pq_keys registry; rotation proofsactive; mandatory from slot 1
Groth16BN254 pairing, arkworksProof verification against registered VKs (ZkProofSubmit)active; classical security only
Schnorr sigma, Pedersen commitments, range proofRistretto255Confidential transfersreserve; unreachable
WOTS+/XMSSSHA-256 hash-basedFallback post-quantum signaturesreserve; unreachable
PqSignedTransfer (52)ML-DSA-65Transfer authorised by a post-quantum signaturedisabled; fee charged, nothing executes

8.1Groth16 verification

The registry xeris_zk_verifier is a protocol-managed singleton. A proof is accepted only against a verification key already registered in it. The verifier is arkworks Groth16::<Bn254>; a result other than Ok(true) is a failure.

ZkVkRegister (61) signer stake >= 1,000 XRS (MIN_STAKE_TO_MINE) vk_base64 -> canonical compressed BN254 VerifyingKey, 1..=16 KiB, exact consumption vk_id, claim_type, description: no post-quantum token first write wins; registry cap 10,000 keys stored: {vk_base64, claim_type, description, registered_by, registered_slot} ZkProofSubmit (46) proof_system == "groth16" verification_key_hash == a registered vk_id proof_data <= 512 B; public_inputs <= 64 x 32-byte LE Fr; exact consumption proof_system, proof_type, metadata_json: no post-quantum token failing proof -> not stored; duplicate proof_id -> error stored: proof_type := VK claim_type (caller value ignored), proof_data_hash, public_inputs_hash (SHA-256 hex), proof_size, verified = true ZkProofVerify (47) read-only; returns {proof_id, verified, verification_slot, proof_system} per block <= 64 ZkProofSubmit with proof_system == "groth16", counted whether or not they verify

The per-block cap of 64 Groth16 submissions is enforced by the validator and mirrored by the producer, which defers the transaction that would exceed it. A stored record carries hashes, not proof bytes, so verified cannot change after submission.

The ZK path refuses any post-quantum claim. A case-insensitive substring match over vk_id, claim_type, description, proof_system, proof_type and metadata_json against pq, post-quantum, post_quantum, postquantum, post quantum, dilithium, mldsa, ml-dsa and ml_dsa rejects the instruction (crypto-module XWC-13). A field containing the two letters pq in any word is rejected. PqAttest (53) is a self-asserted adoption marker: the node performs no cryptographic check, stores the record in the separate pq_attestations map with verified = false, and records the caller's flag under self_asserted.

ZkPrivateTransfer (48) and ZkIdentityProof (49) are disabled at the dispatcher (§12.2); no receipt is written. The nullifier table in the registry is never written by any live path.

8.2Post-quantum key registry

xeris_pq_keys maps an Ed25519 address to its current ML-DSA-65 public key and to the history of keys it has held. The only accepted algorithm string is dilithium3; a public key is 1,952 bytes, a secret key 4,032 bytes and a detached signature 3,309 bytes. The crate is pqcrypto-mldsa, which replaced pqcrypto-dilithium (RUSTSEC-2024-0380) with the same parameter set and byte lengths (crypto-module XWC-12).

PqKeyRegister (50) signer == ed25519_pubkey pq_algorithm == "dilithium3"; security_level == 3 pq_public_key: exactly 1,952 B, parses as an ML-DSA-65 key, not all-zero first registration only; an existing entry is rejected registry cap 10,000,000 entries key_history := [ {key, valid_from_slot: slot, valid_until_slot: None} ] PqKeyRotate (51) signer == ed25519_pubkey; an entry must exist new key: "dilithium3", structurally valid, exactly 1,952 B rotation_proof = ML-DSA-65 signature by the CURRENT key over "xrs_pq_rotate_v5" || chain_id || old_pk (1,952) || new_pk (1,952) || rotation_count u64 LE (no length prefixes; chain_id = "xeris-mainnet-v1" or "xeris-testnet-v1") current binding closes at slot + 1; new binding [slot + 1, None) rotation_count += 1; last_rotation_slot = slot

Bindings in key_history are half-open intervals [valid_from_slot, valid_until_slot); a key installed in block R is admission-active from R + 1. Closed bindings are retained for 302,400 slots (14 days, twice the unbonding period) so slash evidence for a past slot is checked against the key active at that slot. An Ed25519-only compromise cannot replace a registered key: registration is one-shot and rotation requires the current ML-DSA-65 key.

From slot 2 (PQ_REGISTRY_BINDING_ACTIVATION_SLOT) the inline proposer_dilithium3_pk of every block must byte-equal the registry entry for block.proposer; a missing or mismatched entry rejects the block on live and replay paths and is advisory only on the reorg pre-filter. The slot-1 block auto-registers its proposer's inline key, with the capitalised string Dilithium3 as pq_algorithm, and seeds its history. Every other producer registers through PqKeyRegister before its first block.

A node loads dilithium3_sk.bin and dilithium3_pk.bin from its working directory, validates the public key, signs and verifies the fixed challenge XRS_DILITHIUM_LOAD_CHECK_V1, and regenerates the pair on any failure. The secret key is written with mode 0o600 and fsynced; a write failure is fatal. A node without both keys does not produce blocks.

9Signatures, hashing and the transaction format

Three signing contexts exist. A transaction carries Ed25519 signatures only. A block carries one hybrid Ed25519 + ML-DSA-65 (Dilithium3) proposer signature. A peer authenticates with an Ed25519 signature over a server nonce. Every message the node signs or hashes begins with a fixed ASCII domain tag, and every chain-bound message also mixes in the chain id xeris-mainnet-v1 or xeris-testnet-v1.

9.1The hybrid block signature

From HYBRID_SIG_ACTIVATION_SLOT = 1 every block carries proposer_dilithium3_pk and hybrid_proposer_sig. The signed message is built by hybrid_canonical_message with the purpose block. Every variable field is length-prefixed, so two field sets cannot share one byte string, and a signature under one purpose does not verify under another.

canonical = "XRS_HYBRID_V1" // 13 B ‖ u8 5 ‖ "block" // purpose ‖ u8 16 ‖ chain_id // "xeris-mainnet-v1" | "xeris-testnet-v1" ‖ u8 9 // field count ‖ for each field: u32 LE len ‖ bytes fields = slot u64 LE · hash · previous_hash · merkle_root · proposer (32 B) · poh_timestamp u128 LE · nonce u64 LE · poh_hash · proposer_dilithium3_pk (1,952 B) HybridSignature { version: u8 = 1, ed25519_sig: 64 B, dilithium3_sig: 3,309 B } valid ⇔ version == 1 ∧ Ed25519.verify(proposer, canonical) ∧ ML-DSA-65.verify(proposer_dilithium3_pk, canonical)

Both halves must verify; there is no classical-only path for a mined block. The ML-DSA-65 half is checked against the key carried in the block, and from slot 2 that key must byte-equal the xeris_pq_keys entry for the proposer (§8.2). The mining worker receives only the proposer's public key; the block is signed on the validator's owning thread after the search completes (XWC-26).

9.2Merkle root

leaf = SHA-256(0x00 ‖ bincode(tx)) // the whole transaction: signatures and message node = SHA-256(0x01 ‖ left ‖ right) odd pad = SHA-256(0x01 ‖ "XRS_MERKLE_ODD_PAD_V2") // right sibling of an unpaired node empty = SHA-256(0x01 ‖ "XRS_MERKLE_EMPTY_V2") // block with no transactions root = 32 B

A leaf commits to the whole canonical transaction from MERKLE_FULLTX_ACTIVATION_SLOT = 1 (XWC-07). A transaction with an empty signature vector is an error, never a sentinel: the producer abandons the template and requeues its transactions, and a validator rejects the block (XWC-08). An unpaired node is hashed with the tagged pad, not with a copy of itself, which removes the CVE-2012-2459 duplicate-leaf ambiguity.

9.3Domain-separated messages

Table 19 lists every tag reachable from consensus or the network layer. lp(x) is u32 LE len ‖ x. id(s) is 0x01 ‖ 32-byte key when s parses as a public key, otherwise 0x00 ‖ lp(s). Integers are little-endian unless marked.

Table 19. Domain tags reachable from consensus and the network layer.
TagSigner or useLayout after the tag
XRS_HYBRID_V1block proposer, Ed25519 + ML-DSA-65§9.1
XRS_POW_V2Scrypt preimage§2.2
XRS_LEADER_V1, XRS_LEADER_V1_RETRYelection seed, SHA-256last_block_hash ‖ slot u64; retry: seed
XRS_P2P_AUTH_V2\0peer, Ed25519nonce (32 B) ‖ peer_version; signed raw, not pre-hashed
XRS_CH_STATE_V3channel counterparty, Ed25519lp(chain_id) ‖ lp(channel_id) ‖ generation ‖ created_slot ‖ id(party_a) ‖ id(party_b) ‖ balance_a ‖ balance_b ‖ state_sequence
XRS_CLOSE_CH_V4channel counterparty, Ed25519as above, ending final_balance_a ‖ final_balance_b ‖ message_count
XRS_HW_ATTEST_V2device key, Ed25519id(device_pubkey) ‖ id(bound_identity) ‖ lp(device_type) ‖ lp(manufacturer) ‖ lp(model) ‖ lp(firmware_version) ‖ slot u64
xrs_pq_rotate_v5current PQ key, ML-DSA-65chain_id ‖ old_pk ‖ new_pk ‖ rotation_count u64; no length prefixes
XRS_SLASH_ID_V2SHA-256, header digestcanonical (the §9.1 message)
XRS_SLASH_OFFENSE_V2SHA-256, offence idowner_pubkey ‖ violation_slot u64 ‖ min(d_a, d_b) ‖ max(d_a, d_b)
XRS_FEDERATION_V1\0SHA-256, roster domainchain_id ‖ producer keys, sorted
XRS_FEDERATION_TIP_V2\0roster producer, Ed25519domain (32 B) ‖ nonce (32 B) ‖ height u64 ‖ lp(hash) ‖ lp(identity)

A chain-bound signature from one network does not verify on the other. The chain id changes on every hard fork.

9.4Transaction wire format

A transaction is a legacy solana_sdk::transaction::Transaction serialised with bincode 1.x: fixed-width little-endian integers and short_vec (compact-u16) lengths. Versioned (v0) messages are not accepted. account_keys[0] is the signer and the fee payer. The node ignores program_id_index and accounts for a XerisInstruction; the reference wallet sets the program id to the all-zero key.

01 ShortU16: 1 signature sig 64 B Ed25519 over the message bytes below 01 00 01 header: 1 required signer, 0 readonly signed, 1 readonly unsigned 02 ShortU16: 2 account keys payer 32 B account_keys[0]: signer and fee payer 00 × 32 account_keys[1]: program id, ignored blockhash 32 B recent_blockhash, valid for 150 blocks 01 ShortU16: 1 instruction 01 program_id_index 00 ShortU16: 0 account indexes len ‖ data ShortU16 data = bincode(XerisInstruction), ≤ 8,192 B

Instruction data is bincode of the XerisInstruction enum: a u32 LE variant index, then the fields in declaration order. String and Vec carry a u64 LE length; Option is 0x00 or 0x01 ‖ T; bool is one byte; [u8; 32] is raw. The decoder tolerates trailing bytes, but they change the signature and the Merkle leaf; a client emits canonical bincode.

NativeTransfer { from: "Alice", to: "Bob", amount: 5_000_000_000 } // 5 XRS; the strings stand for base58 keys 0b000000 variant 11 0500000000000000 416c696365 from: u64 LE len 5 ‖ "Alice" (must equal account_keys[0] in base58) 0300000000000000 426f62 to: u64 LE len 3 ‖ "Bob" 00f2052a01000000 amount: u64 LE lamports

getLatestBlockhash returns the hash as hex; the client converts it to 32 bytes before signing. A signed transaction is submitted as POST /submit with the body {"tx_base64"} (limits in §10.5). Every rejection is returned as HTTP 200 with {"error"}.

9.5Peer authentication

Peers exchange bincode NetworkMessage frames over plain TCP. The rustls, tokio-rustls and openssl crates in Cargo.toml are not referenced by the source.

connect : "XRS1" // 4-byte magic, once per connection frame : u32 BE len ‖ bincode(NetworkMessage) // len ≤ 5 MiB server → : AuthChallenge { nonce: 32 B CSPRNG, server_version } client → : AuthResponse { pubkey: 32 B, signature: 64 B, peer_version } // within 15 s signature = Ed25519(pubkey, "XRS_P2P_AUTH_V2\0" ‖ nonce ‖ peer_version)

The server admits a peer only after the signature verifies against the claimed key. The server does not prove its identity to the client; a roster producer does, through the signed tip of §10.2. On mainnet a non-roster peer is dropped after authentication (§12.1).

10Network, mempool and sync

Nodes exchange length-prefixed bincode frames over plain TCP. Every connection opens with the Ed25519 challenge and response of §9.5.

Table 20. Transport and sync constants.
ParameterValue
MagicXRS1, 4 raw bytes, within 5 s
Frame≤ 5 MiB
Block≤ 4 MiB, strictly below the frame
Handshake15 s for the challenge and for the response
Peers3,000
Inbound per subnet8 per IPv4 /24 or IPv6 /64, authenticated
Pre-auth per IP4
Pending handshakes256
Message rate600 per 45 s per connection; solicited sync blocks exempt
Idle45 s
Connection lifetime1 h
Send deadline10 s per write
Sync turn100 blocks or 16 MiB
GetBlocks cadenceone per 2 s per connection
Retained sync bytes64 MiB per lane (serve, seed, public)
Recent blocks in memory1,000
Snapshot interval10,000 blocks
PortsP2P 4000; RPC 56001; explorer 50008
Peer discovery is operator-configured: XRS_SEED_PEERS takes up to 200 IPv4 host[:port] entries. The built-in DNS seed is a GitHub gist and the fallback list is empty, marked "RESTORE BEFORE ANY DEPLOYMENT". On mainnet the seed list is the roster; a dialer outside it is refused.

10.1Gossip and relay

The wire carries eight NetworkMessage variants; the legacy AuthRequest is rejected on receipt.

0 Transaction(tx) gossip, both directions 1 Block(block) gossip push and sync turns 2 AuthRequest(sig, version) legacy; rejected 3 AuthChallenge { nonce[32], server_version } accepting side → dialer 4 AuthResponse { pubkey[32], signature[64], peer_version } 5 GetBlocks(start_slot) history request; ends a duplex turn 6 FederationTipRequest { nonce[32], domain[32] } roster peers only, ≤ 4 per connection 7 FederationTip { nonce, height, hash, identity, signature[64] }

A mined block or an admitted transaction is relayed to ceil(sqrt(n)) outbound peers, clamped to 3–15, each over a fresh connection and handshake that carries one message and closes. Received blocks are not re-relayed. active_peers holds outbound addresses only, so an inbound-only peer is never a relay target.

10.2Sync

start = forks.want ?? tip + 1, clamped to [tip − 64, tip + 1] turn = blocks with slot ≥ start, ≤ 100 blocks, ≤ 16 MiB, each ≤ 4 MiB empty and start > tip → the serving tip alone

The accepting side advertises XRS-V2-SYNC-TURNS. A dialer that echoes it gets a duplex connection: the server sends one turn, then its own GetBlocks, which closes the turn and hands over the write side. A dialer answering XRS-V2 polls one way. History comes from the 1,000 blocks in memory or from a byte-indexed scan of ledger.dat under a 5 s budget.

A received block enters the live ledger only as the exact child of the tip; every other block goes to the recovery coordinator of §2.4 (Table 4). Networking never calls the live Ledger::reorg.

Deep sync is roster-only. A producer that proves a fresh signed tip can have the node spool its branch from slot 1 into blocks.jsonl, replay both chains from genesis and, if the branch wins on work, rename the candidate and exit with code 75 for restart. Spools share XRS_MAX_DEEP_SYNC_BYTES, default 512 GiB.

10.3State, snapshots and replay

ledger.dat is the authoritative state: one JSON block per line, appended and fsynced before the block counts as committed. Startup replays it from genesis and re-validates every block under the full rule set. Replay fails closed: a read error or undecodable record stops the node. Only a trailing record without a line terminator is tolerated and truncated (XWC-57).

ledger_snapshot.json is written every 10,000 blocks with balances, stakes, tokens, contracts, processed signatures and governance maps. It is not read at startup; replay is the only load path, and a reorganisation deletes it. contract_state.json is a diagnostic dump with no consensus role.

Recovery clones the ledger with cp --reflink=always on Linux and clonefile(2) on macOS, at startup and before every candidate replay. On a filesystem without reflinks, ext4 among them, the node refuses to start. XFS with reflink, btrfs and APFS qualify.

10.4Mempool

The mempool is a max-heap keyed by fee, and the fee is flat: mempool_priority_fee returns BASE_TX_FEE for every transaction. Order among residents is heap order, and there is no fee market. Eviction needs a strictly higher incoming fee, so a full pool admits nothing until pruning frees space. Every rejection happens before any mutation. Bounds are in Table 6.

Pruning runs after every accepted block: entries whose blockhash left the 150-block window or whose signature is committed are dropped, committed reservations are released and the underfunded set is reconciled. Transactions displaced by a reorganisation are journaled in SQLite, re-admitted 256 at a time and relayed 16 per 2 s until they commit or the chain passes reorg_height + 64.

10.5Node interfaces

A node runs three listeners: P2P, the write RPC and the explorer API with JSON-RPC (Table 20). XRS_LOCAL_ONLY=1 binds P2P and RPC to loopback; the explorer always binds 0.0.0.0 and allows any origin. Write endpoints take a body of at most 256 KiB, 30 requests per minute per IP, and reject a signature seen within 120 s.

Table 21. JSON-RPC methods on the explorer port (POST /).
JSON-RPC methodResult
getBalancelamports of the address
getAccountInfolamports, stake, isValidator
getSlottip slot
getBlockHeightchain_height
getLatestBlockhash, alias getRecentBlockhashtip hash as 64 hex characters; lastValidBlockHeight = slot + 150
getBlockheader fields of a block among the 1,000 in memory, else null
getTransactionthe transaction, from the blocks in memory, with the persisted outcome
getSignaturesForAddressreceipt-store history, default 20 entries
getHealth"ok"
getVersiona hard-coded string unrelated to the build
RPC 56001 POST /submit /stake /unstake /pq-register GET /network/economics /stake/:addr /contract/:id /contracts /launchpads /governance/proposals /capabilities/search /tasks /zk/stats Explorer 50008 GET /v2/stats /v2/blocks /v2/block/slot/:n /v2/block/hash/:h /v2/transactions /v2/tx/:sig /v2/account/:a /v2/account/:a/transactions /v2/tokens /v2/token/:id/holders /v2/contracts /v2/contract/:id /v2/pools /v2/rwa /v2/rwa/:id /v2/validators /v2/search?q=

Transaction history and receipts are derived data in tx_receipts.sqlite, never read by consensus. On testnet a node without a store keeps validating and serves no history; on mainnet an open failure stops startup and an indexing failure writes a rebuild marker and exits with code 75. Endpoints that return 501 are listed in §12.2.

11Governance

Governance is the protocol-managed Governance contract at xeris_governance. The first CreateProposal creates it with min_proposal_stake = 100 XRS. Three instructions reach it: CreateProposal (39), CastVote (40) and ExecuteProposal (41). The generic contract-call path cannot invoke propose, vote or execute; the instructions are the only entry.

11.1Proposals and votes

propose (CreateProposal, 39) stakes[signer] >= min_proposal_stake (100 XRS); no bond voting_period_slots 21,600 <= n <= 1,296,000 (1 day .. 60 days) quorum 0 -> DEFAULT_PROPOSAL_QUORUM (5,000 XRS); else 0 < q <= MAX_EMISSION_SUPPLY voting_end_slot = current_slot + voting_period_slots (checked add) title <= 256 bytes; proposal_type free-form, default "text"; parameter_json stored verbatim live proposals <= 1,000 in "voting"; terminal records pruned oldest-first beyond 1,000 vote (CastVote, 40) weight = stakes[signer] at the slot the vote is processed vote yes | no | abstain one vote per address; slot <= voting_end_slot; status must be "voting" execute (ExecuteProposal, 41; any signer) slot > voting_end_slot yes + no + abstain < quorum -> "rejected" yes > no -> "passed", total_executed += 1 otherwise -> "rejected"

Vote weight is consensus stake, not a lock. An address without stake cannot propose, and its vote adds zero weight. A vote after voting_end_slot is rejected without changing the proposal. Quorum counts abstentions; passage needs quorum and a strict yes majority. Finalisation happens in the first ExecuteProposal after the window closes: there is no scheduling, no timelock and no second step.

A passed proposal records an outcome and changes nothing on-chain. parameter_json is stored and never read; no handler applies a parameter change. proposal_type is a free-form string with no enumeration. Statuses are voting, passed and rejected; executed and expired are named in the struct comment and never assigned.

Delegation and vote locking are inactive. The ledger persists governance_delegates and governance_locks, but no instruction writes either map and the vote path reads neither.

11.2Activation slots

Consensus rules are gated on slot constants compiled into the node, not on proposals. Every gate is active from the first mined block of this chain (slot 0 is the genesis marker; the first block is slot 1), so the table states the launch posture, not a migration path. A rule change with a new slot gate ships as a node release.

Table 22. Slot-gated rules; all active from the first mined block.
ConstantSlotRule from that slot
FEE_ACTIVATION_SLOT1Transaction fees are required.
STRICT_ADMISSION_ACTIVATION_SLOT1The full admission set, including exact slot continuity, applies to every non-genesis block.
MERKLE_FULLTX_ACTIVATION_SLOT1The Merkle leaf commits to the full transaction, not its first signature.
LEADER_ENFORCEMENT_ACTIVATION_SLOT1Leader election and the 4x non-leader difficulty penalty are consensus-enforced.
HYBRID_SIG_ACTIVATION_SLOT1Every block carries a non-empty proposer_dilithium3_pk and a valid hybrid_proposer_sig.
POW_PREIMAGE_V2_SLOT1The PoW preimage binds parent_hash and poh_timestamp.
HW_ATTEST_V2_ACTIVATION_SLOT1Hardware attestations are Ed25519 signatures by the device key.
PQ_REGISTRY_BINDING_ACTIVATION_SLOT2The inline proposer_dilithium3_pk must equal the xeris_pq_keys entry; slot 1 auto-registers the bootstrap key.
SCRYPT_V2_SLOT0Every block is mined and verified under Scrypt v1.2 (N = 4,096, r = 4, p = 1).
SCRYPT_UPGRADE_SLOT0The Scrypt v1.1 branch never fires.
LEGACY_TRANSFER_SUNSET_SLOT0A block carrying SystemInstruction::Transfer is rejected.

11.3RPC surface

GET /governance/proposals reads xeris_governance and returns { proposals, total_proposals, total_executed }; it maps voting to Active, passed to Passed and rejected to Failed, and omits votes_abstain and voters. GET /governance/lock/{address} reads the unwritten maps and returns locked_amount 0 and delegate null. The four POST /governance/* routes return HTTP 501 (§12.2); governance writes are instructions 39–41 submitted to POST /submit.

12Mainnet, federation and the audit record

Mainnet is chain id xeris-mainnet-v1, run by xrs-node 0.1.1 with --mainnet. With it the node runs as a federated beta: three roster producers propose blocks, public self-staking is closed, and the surfaces in §12.2 are refused. The consensus rules of §2 are unchanged; participation is gated. The build is a mainnet candidate; mainnet has not launched as of this revision.

12.1The producer roster

XRS_FEDERATED_PRODUCERS=<pubkey>@<ipv4>:<port>,<pubkey>@<ipv4>:<port>,<pubkey>@<ipv4>:<port> entries exactly 3; keys distinct; addresses distinct; IPv4; port != 0 --mainnet variable unset -> panic at startup --testnet variable unset -> no roster; every gate below is off roster hash SHA-256("XRS_FEDERATION_V1\0" ‖ chain_id ‖ keys sorted by bytes) tip message "XRS_FEDERATION_TIP_V2\0" ‖ roster hash ‖ nonce(32) ‖ height_le ‖ len(hash) ‖ hash ‖ len(identity) ‖ identity

The roster is an operational beta gate, not a consensus vote. A fixed set of three keys is admitted to every path that produces, relays or stakes; the table lists each gate. The roster is fixed for the chain: changing keys requires a reviewed epoch migration, not an edit of the variable on restart.

Table 23. Federation gates under --mainnet.
GateRule
Startup--mainnet without XRS_FEDERATED_PRODUCERS panics. A malformed entry, a duplicate key or address, a non-IPv4 address or port 0 panics.
GenesisA fresh mainnet (height 0) starts only if a roster key holds genesis stake of at least 1,000 XRS.
BlocksEvery block on disk and every block from a peer must name a roster key as proposer; federation_block_gate runs over the stored chain file at startup and over each peer block.
StakeA Stake instruction naming a non-roster key is rejected at ingress, relay and replay: "Public self-staking is closed during federated beta".
PeersAn inbound peer that authenticates with a non-roster key is dropped after the handshake. Outbound dials go only to roster addresses. The seed list is the roster.
ProposalA roster producer proposes on a parent only after another roster peer has been observed at that height and hash within 3 s. This is a 2-of-3 liveness gate, "not finality"; finality stays 64 blocks.
Deep syncOnly a freshly signed tip from a roster producer authorises an unbounded history download. The 64-block fast path stays public.

12.2Disabled surfaces

The surfaces below exist in the build and are refused. An instruction refused at ingress never enters the mempool. An instruction skipped at the dispatcher is included in the block, charged the base fee, and executes nothing. Every RPC write that once mutated state outside consensus answers with an error; the replacement is a signed transaction to POST /submit.

Table 24. Surfaces refused by xrs-node 0.1.1; the empty fallback seed list is in §10.
SurfaceStatusReplacement
SubDelegate (22)Rejected at ingress and skipped at the dispatcher (XWC-82).None.
ZkPrivateTransfer (48)Skipped at the dispatcher; fee charged (NEW-CRIT-3).None.
ZkIdentityProof (49)Skipped at the dispatcher; fee charged (NEW-CRIT-1).None.
PqSignedTransfer (52)Skipped at the dispatcher; fee charged (NEW-CRIT-4).None. PqKeyRotate is active.
QueryCapabilities (30)No-op in a block.GET /capabilities/search.
Legacy SystemInstruction::TransferRejected at ingress and in blocks from slot 0 (LEGACY_TRANSFER_SUNSET_SLOT).NativeTransfer.
execute_limit_order, execute_dca_tickReturn an error (CRIT-1, CRIT-2).cancel_limit_order, cancel_dca_order recover the escrow.
RWA distribute, toggle_distributionsReturn an error (contracts-module XWC-69).None.
Launchpad vesting_enabledDeployment rejected (contracts-module XWC-65).Unvested launch.
/airdrop/:addr/:amountJSON body with status: 501 under HTTP 200 (NEW-HIGH-7).None.
POST /stake/claimHTTP 501 (NEW-CRIT-6).None; staking rewards pay every 900 blocks without a claim.
POST /governance/{propose, vote, lock, delegate}HTTP 501 (NEW-CRIT-6).Instructions 39 to 41 (§11). No lock or delegate instruction exists.
POST /token/{mint, transfer, burn, create}, /contract/{deploy, call}, /rwa/{create, update_status}JSON error under HTTP 200 naming the instruction to submit.Instructions 0 to 7 via POST /submit.

12.3The audit record

CertiK reviewed the node as three separately numbered modules. Each module starts at XWC-01, so an id identifies a finding only with its module name: consensus XWC-22 is reorg signature validation, crypto-module XWC-22 is mempool capacity. Id ranges: consensus to XWC-88, crypto to XWC-22, contracts and network to XWC-81. Xeris has deferred consensus XWC-36 (dependency upgrades) and XWC-86 (fee model, on CertiK's recommendation that gas pricing be redesigned rather than patched).

Every CertiK document the repository answers is titled "Preliminary Comments". No CertiK-authored report is in the repository; CertiK's interim statuses are known as relayed in Xeris's responses. The first comments are dated 2026-05-01 and the latest Xeris response 2026-10-07. This paper cites an id inline where a rule exists because of that finding.

Table 25. CertiK modules. Id ranges include later rounds answered by commit.
ModuleFilesFinding idsComments and Xeris responsesOpen
Consensusledger.rs, pow.rs, poh.rs, main.rsXWC-01 to XWC-88Comments 2026-05-01; responses 2026-05-18, 2026-06-09, 2026-07-27.XWC-36 dependencies, XWC-86 fee model: deferred.
Crypto, Merkle, tx poolcrypto.rs, merkle.rs, tx_pool.rsXWC-01 to XWC-22Responses 2026-06-22, 2026-07-03, 2026-07-27.None pending; XWC-12 acknowledged: pqcrypto-mldsa replaced pqcrypto-dilithium, pqcrypto-traits remains.
Contracts and networkcontracts.rs, network.rs, token.rs, genesis.rsXWC-01 to XWC-81Response 2026-09-12; commit-only responses 2026-09-29 and 2026-10-07.None recorded in the repository.
Id prefixes in this paper: XWC-nn, a CertiK finding, module named when ambiguous; AH-n, a consensus issue Xeris disclosed from its own two-node exercise; NEW-CRIT-n, NEW-HIGH-n, CRIT-n, C-n, H-n, L-n, M-n, tags from Xeris's pre-audit internal review that the code still carries. Only XWC ids are CertiK findings.

AAppendix A. Constants

Every value below is read from xrs-node 0.1.1 at commit 243046d; paths are src/<file>:<line>. The lamport is the base unit: 1 XRS = 1,000,000,000 lamports (9 decimals). Values the code stores in lamports are shown in XRS.

Table A.1. Protocol constants and operational bounds, xrs-node 0.1.1 at 243046d.
ConstantValueFile:line
SLOT_DURATION_MS4,000 ms (a block every 4 s)main.rs:33
MAX_REORG_DEPTH64 blocks (≈ 4.3 min to finality)ledger.rs:4555
mining_deadline3,900 ms per slotpow.rs:539
NON_LEADER_GRACE_SLOTS2 slots (8,000 ms of leader silence)main.rs:404-405
non_leader_targetbase target >> 2 (4× harder)ledger.rs:407-421
scrypt_params_for_slotN = 4,096, r = 4, p = 1 (2 MiB per hash)pow.rs:41-49
SCRYPT_UPGRADE_SLOT0pow.rs:22
SCRYPT_V2_SLOT0pow.rs:31
MAX_FUTURE_TIMESTAMP_DRIFT_MS2,000 msledger.rs:244
BLOCKHASH_EXPIRY_WINDOW150 blocks (≈ 10 min)ledger.rs:239
MAX_RECENT_BLOCKS1,000 blocks in RAMledger.rs:36
SNAPSHOT_INTERVAL10,000 blocksledger.rs:261
MAX_PROCESSED_SIGS10,000,000 signaturesledger.rs:39
MAINNET_INITIAL_SUPPLY200,000,000 XRS (treasury)ledger.rs:27
MAX_EMISSION_SUPPLY500,000,000 XRS (mining + staking + attestation)ledger.rs:65
MAINNET_INITIAL_SUPPLY + MAX_EMISSION_SUPPLY700,000,000 XRSledger.rs:27, 65
BASE_BLOCK_REWARD10 XRSledger.rs:70
HALVING_INTERVAL25,000,000 blocks (≈ 3.17 years)ledger.rs:75
BASE_TX_FEE0.001 XRS (1,000,000 lamports)ledger.rs:58
STAKING_APY_NUMERATOR / STAKING_APY_DENOMINATOR7 / 100 (7 % per year)ledger.rs:5144-5145
STAKING_REWARD_INTERVAL900 blocksledger.rs:5138
BLOCKS_PER_YEAR7,884,000ledger.rs:5141
MIN_STAKE_TO_MINE1,000 XRSpow.rs:16; ledger.rs:232
staking reward floor100 XRS stakedledger.rs:9399
MIN_ATTESTOR_STAKE100 XRSledger.rs:1285
ATTESTATION_REWARD0.01 XRSledger.rs:214
ATTESTATION_SLOT_WINDOW200 slotsledger.rs:218
attestation rate limit1 reward per 10 blocks per validatorledger.rs:6287-6296
UNBONDING_PERIOD_SLOTS151,200 slots (7 days)ledger.rs:201
MAX_UNBONDING_QUEUE10,000 entries (10 per account)ledger.rs:205, 5792
MIN_UNSTAKE_AMOUNT1 XRS (partial unstake floor)ledger.rs:210
slash_amount10 % of slashable balanceledger.rs:8219
reporter_reward5 % of the slash; 95 % burnedledger.rs:8252-8255
MAX_TXS_PER_BLOCK40,000 transactionsledger.rs:78
MAX_BLOCK_SIZE_BYTES4 MiBledger.rs:196
MAX_IX_PER_TX16 instructionsledger.rs:94
MAX_ACCOUNTS_PER_TX64 account keysledger.rs:95
MAX_IX_DATA_SIZE8 KiB per instructionledger.rs:93
MAX_SLASH_IX_DATA_SIZE65,535 B (SlashReport only)ledger.rs:119
MAX_GROTH16_VERIFICATIONS_PER_BLOCK64ledger.rs:154
MAX_GROTH16_PROOF_BYTES512 Bcrypto.rs:1041
MAX_GROTH16_VK_BYTES16 KiBcrypto.rs:1042
MAX_GROTH16_PUBLIC_INPUTS64 field elementscrypto.rs:1043
MAX_MEMPOOL_SIZE50,000 entriestx_pool.rs:160
MAX_MEMPOOL_BYTES64 MiBtx_pool.rs:175
MAX_TX_BYTES128 KiB per transactiontx_pool.rs:183
MAX_TXS_PER_ACCOUNT256 per payertx_pool.rs:186
MAX_BYTES_PER_ACCOUNT4 MiB per payertx_pool.rs:197
MAX_UNDERFUNDED_TXS5,000 (MAX_MEMPOOL_SIZE / 10)tx_pool.rs:218
XERIS_CHAIN_ID_MAINNETxeris-mainnet-v1ledger.rs:286
SUPPORTED_PQ_ALGORITHMdilithium3 (ML-DSA-65, FIPS 204)crypto.rs:924
dilithium3_public_key_len()1,952 Bcrypto.rs:1184-1186
dilithium3_signature_len()3,309 Bcrypto.rs:1178-1180
ML-DSA-65 secret key4,032 Bcrypto.rs:1164
PQ_KEY_HISTORY_RETENTION_SLOTS302,400 slots (14 days)contracts.rs:1224
FEE_ACTIVATION_SLOT1ledger.rs:54
STRICT_ADMISSION_ACTIVATION_SLOT1ledger.rs:1073
MERKLE_FULLTX_ACTIVATION_SLOT1ledger.rs:1099
LEADER_ENFORCEMENT_ACTIVATION_SLOT1ledger.rs:1111
HYBRID_SIG_ACTIVATION_SLOT1ledger.rs:281
POW_PREIMAGE_V2_SLOT1pow.rs:109
HW_ATTEST_V2_ACTIVATION_SLOT1ledger.rs:7311
PQ_REGISTRY_BINDING_ACTIVATION_SLOT2ledger.rs:1137
LEGACY_TRANSFER_SUNSET_SLOT0ledger.rs:229
MAGIC_BYTESXRS1network.rs:23
MAX_MSG_SIZE5 MiB per framenetwork.rs:28
MAX_PEERS3,000network.rs:29
CONN_TIMEOUT45 s idlenetwork.rs:30
MAX_PEERS_PER_SUBNET8 per /24network.rs:34
MAX_PREAUTH_PER_IP4 pre-auth connections per IPnetwork.rs:40
MAX_CONN_LIFETIME_SECS3,600 snetwork.rs:46
PEER_SEND_TIMEOUT_SECS10 snetwork.rs:50
MAX_MSGS_PER_WINDOW600 messages per 45 snetwork.rs:55
CHALLENGE_TIMEOUT_SECS15 s handshakenetwork.rs:447
GETBLOCKS_MIN_INTERVAL2 snetwork.rs:560
MAX_SYNC_TURN_BLOCKS100 blocks per turnnetwork.rs:561
MAX_SYNC_TURN_BYTES16 MiB per turnnetwork.rs:562
MAX_RETAINED_SYNC_BYTES64 MiBnetwork.rs:563
MAX_PENDING_HANDSHAKES256network.rs:3786
MAGIC_TIMEOUT_SECS5 snetwork.rs:3791
min_proposal_stake100 XRSledger.rs:8275
MIN_VOTING_PERIOD21,600 slots (ingress check)ledger.rs:8267
MIN_VOTING_PERIOD_SLOTS21,600 slots (≈ 1 day)contracts.rs:5527
MAX_VOTING_PERIOD_SLOTS1,296,000 slots (≈ 60 days)contracts.rs:5528
DEFAULT_PROPOSAL_QUORUM5,000 XRS of cast weightcontracts.rs:163
MAX_LIVE_PROPOSALS1,000contracts.rs:5505
DISPUTE_CHALLENGE_PERIOD_SLOTS21,600 slotscontracts.rs:995
DISPUTE_MAX_LIFETIME_SLOTS648,000 slotscontracts.rs:1000
arbitration_panelvalidators with stake ≥ MIN_STAKE_TO_MINEledger.rs:5286-5291
MIN_DEAL_DISPUTE_BOND1 XRScontracts.rs:1008
DEAL_TIMEOUT_SLOTS648,000 slotscontracts.rs:1079
MAX_TASK_LIFETIME_SLOTS648,000 slotscontracts.rs:916
MAX_TASK_REJECTIONS3contracts.rs:914
DAILY_SLOTS21,600 slots (24 h spend window)contracts.rs:3439
agents per registry50contracts.rs:3334
HEARTBEAT_RETENTION_SLOTS21,600 slotscontracts.rs:5947
MAX_HEARTBEAT_RECORDS10,000contracts.rs:5948
stale_threshold5,400 slots (≈ 6 h)contracts.rs:5984
ORDER_STORAGE_BOND0.01 XRSledger.rs:1290
MAX_ORDER_LIFETIME_SLOTS650,000 slotsledger.rs:1295
MAX_ACTIVE_ORDERS_PER_OWNER100ledger.rs:1325
MAX_ORACLE_VALUE_AGE_SLOTS900 slotsledger.rs:1300
PRICE_WINDOW_BLOCKS61 blocks (median)ledger.rs:1318
PRICE_MIN_OBSERVATIONS20ledger.rs:1319
MAX_TRACKED_PRICE_POOLS64contracts.rs:170
fee_bps (Swap default)30 bpscontracts.rs:1396
MINIMUM_LIQUIDITY1,000 sharescontracts.rs:6
creator_reward_bps100 bpscontracts.rs:1600
XERIS_FEE_BPS77 bpscontracts.rs:308
total_supply (Launchpad default)1,000,000,000 tokenscontracts.rs:1603-1604
target_liquidity_xrs (default)10,000 XRScontracts.rs:1608-1609
liquidity_bps2,000 (clamp 500–4,000)contracts.rs:1631-1633
MAX_RWA_APPROVED_HOLDERS1,024token.rs:7
valuation (RealWorldAsset)USD centstoken.rs:872
challenge_period_slots (state channels)1,000 slotsledger.rs:8308
MAX_CHANNELS100,000contracts.rs:2105

BAppendix B. The instruction set

XerisInstruction has 62 variants. The bincode discriminant is the declaration index, encoded as u32 little-endian, so the index column is the wire tag. Signer rule names the field that must equal the first account key, or the rule the handler applies. Status is what the block dispatcher does with the variant; disabled arms skip the instruction and still charge the fee.

Table B.1. XerisInstruction variants in bincode declaration order
IndexVariantSigner ruleStatus
0TokenMintmint authorityactive
1TokenTransfersigner == fromactive
2TokenBurnsigner == fromactive
3TokenCreatemint authorityactive
4ContractCallper methodactive
5ContractDeployowneractive
6TokenCreateRWAmint authorityactive
7RWAUpdateStatusmint authorityactive
8RWATransfersigner == fromactive
9Stakesigner == pubkeyactive
10Unstakesigner == pubkeyactive
11NativeTransfersigner == fromactive
12ValidatorAttestationsigner == validatoractive
13WrapXrssigneractive
14UnwrapXrssigneractive
15RegisterAgentowneractive
16UpdateAgentowneractive
17AgentExecuteagent (executes as owner)active
18CreateIdentitysigner == identity_pubkeyactive
19UpdateIdentitysigner == identity_pubkeyactive
20AttestReputationactive identityactive
21AgentMessageactive identityactive (no state)
22SubDelegaten/adisabled (XWC-82)
23ConditionalOrderowneractive
24CancelConditionalOrderowneractive
25RegisterOracleowneractive
26OracleSubmitowneractive
27HardwareAttestsigner == device_pubkey or bound_identityactive
28RegisterCapabilitysigner == provider_identityactive
29UpdateCapabilitysigner == provider_identityactive
30QueryCapabilitiesn/ano-op
31PostTaskanyoneactive
32ClaimTasksigner == claimant_identityactive
33ResolveTaskpartyactive
34RegisterModelsigner == identity_pubkeyactive
35UpdateModelowneractive
36OpenDisputeanyoneactive
37ResolveDisputestake ≥ 1,000 XRS or partyactive
38SlashReportanyoneactive
39CreateProposalstake ≥ 100 XRSactive
40CastVoteanyone (weight = stake)active
41ExecuteProposalanyoneactive
42OpenChannelpartyactive
43CloseChannelpartyactive
44ForceCloseChannelpartyactive
45AgentHeartbeatsigner == identity_pubkeyactive
46ZkProofSubmitanyoneactive
47ZkProofVerifyanyoneactive (read-only)
48ZkPrivateTransfern/adisabled (NEW-CRIT-3)
49ZkIdentityProofn/adisabled (NEW-CRIT-1)
50PqKeyRegistersigner == ed25519_pubkeyactive
51PqKeyRotatesigner == ed25519_pubkeyactive
52PqSignedTransfern/adisabled (NEW-CRIT-4)
53PqAttestanyoneactive (marker)
54CreateDealpartyactive
55AcceptDealpartyactive
56ConfirmDealpartyactive
57CancelDealpartyactive
58DisputeDealpartyactive
59SettleDealanyoneactive
60ReclaimDealpartyactive
61ZkVkRegisterstake ≥ 1,000 XRSactive

CAppendix C. Contract types

ContractType has 23 variants. ContractDeploy resolves contract_type_str through ContractType::from_str, which lower-cases the input, so every alias is case-insensitive. Eight types are protocol-managed: user deploy is refused and the protocol creates each singleton under a fixed xeris_* id on first use.

Table C.1. The 23 ContractType variants, their from_str aliases and how each is created and driven.
TypeAliasesDeployableSingleton idDriven by
TimeLocktimelock, time_lockyes—ContractDeploy, ContractCall
Escrowescrowyes—ContractDeploy, ContractCall
Swapswapyes—ContractDeploy, ContractCall
Vestingvestingyes—ContractDeploy, ContractCall
MultiSigmultisig, multi_sigyes—ContractDeploy, ContractCall
RealWorldAssetrwa, real_world_asset, realworldassetmint authority of the RWA token only—ContractDeploy, ContractCall
Launchpadlaunchpad, launch_padyes—ContractDeploy, ContractCall
AgentRegistryagent_registry, agent, agentsyesagent_registry_<sha256(owner)[..32]> per ownerRegisterAgent, UpdateAgent, AgentExecute
IdentityRegistryidentity, identity_registryyesxeris_identities; identity_<sha256(pubkey)[..32]> per identityCreateIdentity, UpdateIdentity, AttestReputation
ConditionalOrderBookconditional, conditional_orders, ordersnoxeris_conditional_ordersConditionalOrder, CancelConditionalOrder
LimitOrderlimit, limit_order, limit_ordersyes—ContractDeploy, ContractCall
DcaOrderdca, dca_order, dollar_cost_averagingyes—ContractDeploy, ContractCall
OracleRegistryoracle, oracle_registry, oraclesyesxeris_oraclesRegisterOracle, OracleSubmit
DeviceRegistrydevice, device_registry, hardwarenoxeris_devicesHardwareAttest
CapabilityRegistrycapability, capabilities, cap_registryyesxeris_capabilitiesRegisterCapability, UpdateCapability
TaskBoardtask, tasks, task_board, bountynoxeris_tasksPostTask, ClaimTask, ResolveTask
ModelRegistrymodel, model_registry, modelsyesxeris_modelsRegisterModel, UpdateModel
DisputeRegistrydispute, disputes, arbitrationnoxeris_disputesOpenDispute, ResolveDispute, DisputeDeal
Governancegovernance, gov, daoyes; the instructions target xeris_governance onlyxeris_governanceCreateProposal, CastVote, ExecuteProposal
StateChannelRegistrychannel, channels, state_channelno; also refused inside deploy_contractxeris_channelsOpenChannel, CloseChannel, ForceCloseChannel
ZkVerifierRegistryzk, zk_verifier, zero_knowledgenoxeris_zk_verifierZkVkRegister, ZkProofSubmit, ZkProofVerify, PqAttest
PqKeyRegistrypq, pq_keys, post_quantum, quantumnoxeris_pq_keysPqKeyRegister, PqKeyRotate; slot-1 proposer bootstrap
DealRegistrydeal, deals, escrow_dealnoxeris_dealsCreateDeal … ReclaimDeal (54–60)
xeris_heartbeats (AgentHeartbeat) and xeris_slashing_registry (SlashReport) are stored with contract_type: IdentityRegistry although their state is Heartbeats; GET /contracts reports them as IdentityRegistry.