Run a node
xrs-node 0.1.1Rustports 4000 · 56001 · 50008
One binary, three ports, one keypair file.
Build
git clone https://github.com/ZZachWWins/xeriscointestnet && cd xeriscointestnet
cargo build --release
| Binary | Does |
|---|---|
xrs-node | the node |
keypair_gen | writes keypair.json |
wallet | builds transactions, prints the curl to submit them |
| Needs | Why |
|---|---|
rustc | 1.94.1 in Xeris’s last remediation report; no pin |
cc, perl, make | vendored OpenSSL, bundled SQLite |
Keys
# keypair.json; --mainnet writes keypair_mainnet.json
RUST_LOG=info ./target/release/keypair_gen
| File | Content | Mode |
|---|---|---|
keypair.json | JSON array of 64 bytes, the Ed25519 secret key | 0600 |
dilithium3_sk.bin | ML-DSA-65 secret key, 4,032 bytes; written at first start in the working directory; self-tested on every start | 0600 |
dilithium3_pk.bin | public key, 1,952 bytes | umask |
keypair.json loads with XerisKeypair.fromJsonFile.
Start
| Flag | Default | Meaning |
|---|---|---|
--validator <seed-ip> <keypair.json> <data-dir> | — | join through a seed; a missing keypair is generated |
--bootstrap <keypair.json> <data-dir> | — | first node of a chain; needs the treasury key |
--mainnet | off | chain id xeris-mainnet-v1; roster required; <seed-ip> ignored |
--genesis | off | write a genesis descriptor and exit |
--p2p-port | 4000 | P2P |
--rpc-port | 56001 | RPC |
--explorer-port | 50008 | Explorer API |
--genesis-pre-stake | — | deprecated, ignored |
RUST_LOG=info ./target/release/xrs-node \
--validator 138.197.116.81 keypair.json ./data
# mainnet: three P2P addresses
export XRS_FEDERATED_PRODUCERS='<pk1>@<ip1>:4000,<pk2>@<ip2>:4000,<pk3>@<ip3>:4000'
RUST_LOG=info ./target/release/xrs-node --mainnet \
--validator 138.197.116.81 keypair_mainnet.json ./data
Environment
| Variable | Effect |
|---|---|
XRS_FEDERATED_PRODUCERS | 3 pubkey@ipv4:port entries, the P2P addresses; required with --mainnet |
XRS_LOCAL_ONLY=1 | P2P and RPC bind 127.0.0.1, no DNS seeds; the explorer still binds 0.0.0.0 |
XRS_SEED_PEERS | ip[:port] comma-separated, ≤ 200, trusted; testnet only |
XRS_MAX_DEEP_SYNC_BYTES | deep-sync disk budget; default 512 GiB, minimum 1 GiB |
RUST_LOG | info shows the banner and block log; unset prints errors only |
Ports
| Port | Service | Protocol | Bind |
|---|---|---|---|
| 4000 | P2P | TCP, Ed25519 challenge-response | 0.0.0.0, or 127.0.0.1 local-only |
| 56001 | RPC | HTTP/1.1, no TLS | 0.0.0.0, or 127.0.0.1 local-only |
| 50008 | Explorer API | HTTP/1.1, no TLS | 0.0.0.0 always |
Terminate TLS on a reverse proxy. The write limiter keys on the TCP peer: one proxy, one 30-per-minute budget.
Files
| Path | Purpose |
|---|---|
<data-dir>/ledger.dat | blocks, one JSON line each, fsynced before apply |
<data-dir>/tx_receipts.sqlite | explorer history, schema 2; rebuilt by replay when the version differs; required on every network |
<data-dir>/ledger.dat.recovered-txs.sqlite | recovery journal; created on every start |
<data-dir>/price_history_<pool>.json | ≤ 10,080 snapshots per pool, one per 100 blocks |
<data-dir>/ledger_snapshot.json, contract_state.json | written, never loaded |
<cwd>/dilithium3_*.bin | the PQ keys |
Two nodes started from one working directory share one PQ key.
Peers
| Source | Rule |
|---|---|
--validator <seed-ip> | dialled first; port 4000 when none is given |
XRS_SEED_PEERS | trusted |
| DNS seed list | one HTTPS URL fetched at start, 5 s timeout, unless local-only |
| built-in list | empty |
| mainnet | the roster replaces every source above |
| address | IPv4, optional port; hostnames refused |
Stake and produce
| Step | Do |
|---|---|
| 1 | generate keypair.json |
| 2 | start with --validator; the log reads needs 1000 to mine |
| 3 | fund the address: ≥ 1,000 XRS plus fees; no faucet |
| 4 | wallet stake → POST /stake |
| 5 | wallet pq-register → POST /pq-register, before the first block |
# each prints the curl to submit; run it
./target/release/wallet stake keypair.json 1000
./target/release/wallet pq-register keypair.json
import { readFileSync } from 'node:fs';
import { XerisClient, XerisKeypair } from 'xeris-sdk';
const client = new XerisClient('http://127.0.0.1',
{ rpcPort: 56001, explorerPort: 50008 });
const kp = XerisKeypair.fromJsonFile('keypair.json');
const pk = readFileSync('dilithium3_pk.bin');
await client.stakeXrs(kp, 1000); // POST /stake
await client.pqKeyRegister(kp, pk, 'dilithium3', 3); // POST /pq-register
| Rule | Value |
|---|---|
| produce blocks | stake ≥ 1,000 XRS; counts from the next block |
first Stake | resulting stake ≥ 1,000 XRS; /stake does not pre-check it |
| staking rewards | stake ≥ 100 XRS; 7 % a year, every 900 blocks, to the liquid balance |
| unstake | all, or keep ≥ 1,000 XRS; partial ≥ 1 XRS, POST /unstake |
| unbonding | 151,200 slots (7 days); slashable; released without a claim, GET /account/{address}/unstaking |
| attestations | stake ≥ 100 XRS; 0.01 XRS, one per 10 blocks, within 200 slots, hash of 32 bytes, ValidatorAttestation |
| slashing | 10 % of stake per proven double-sign; 5 % of that to the reporter, the rest burned, SlashReport |
| PQ key | one per node; blocks need it from slot 2 |
mainnet Stake | roster keys only; others: Public self-staking is closed during federated beta |
Mainnet: the node key must be in the 3-entry
XRS_FEDERATED_PRODUCERS roster or startup panics.Health
curl -s http://127.0.0.1:56001/health
curl -s http://127.0.0.1:50008/v2/stats
curl -s http://127.0.0.1:56001/stake/<address>
| Signal | Route | SDK |
|---|---|---|
| up | GET /health | getHealth() |
| height, TPS estimate | GET /v2/stats | getStats() |
stake, isValidator, blocksProposed | GET /stake/{address} | getStakeInfo(address) |
| PQ key | GET /pq/keys/{address} | getPqKey(address) |
| block reward, fees, halving | GET /network/economics | getNetworkEconomics() |
| peers | the node log | — |
| metrics | none | — |