RougeChain Documentation
Welcome to RougeChain — a post-quantum secure Layer 1 blockchain built with real NIST-approved cryptography.
Current Status
| Component | Status |
|---|---|
| RougeChain mainnet (height 251 on 2026-10-06) | LIVE — 3 staked validators, one producing every block |
| Consensus Release 1 (tx-integrity @90, proposer selection @100) | LIVE |
| Verified BFT finality (FINALITY_V2) | LIVE since block 150 |
| Game-ready contracts (tokens, NFTs, randomness, payable calls) | LIVE since blocks 150–190 |
| Mintable tokens, contract royalty reads | LIVE since block 235 |
| Monetary-integrity rule; shielded / token-lock / token-staking / governance / delegation types suspended | LIVE since block 245 — see Status |
| Current node release | 1.6.3 (1.6.2 is the minimum to follow the chain) — upgrade notice, release notes |
| R1 production bridge (XRGE / qETH / qUSDC) | LIVE — ETH/USDC contract deposits auto-claimed (reopened 2026-10-05); operator-run, unaudited |
| Bitcoin bridge | Deposits LIVE; withdrawals switched off in the site |
| V3 XRGE bridge | AUDIT CANDIDATE / NOT ACTIVATED |
RougeChain is post-quantum-secured at the L1 level. The current production XRGE bridge remains on the hardened R1 architecture, which still relies on classical authorization on the Base side. A V3 XRGE bridge using ML-DSA-65 post-quantum authorization has been built and rehearsed; it is not deployed and awaits an external audit. Details: Status & Roadmap · Security Overview.
What is RougeChain?
RougeChain is among the first blockchains designed from the ground up to be resistant to quantum computer attacks. It uses:
- ML-DSA-65 (CRYSTALS-Dilithium) for digital signatures
- ML-KEM-768 (CRYSTALS-Kyber) for key encapsulation
- SHA-256 for hashing
All cryptographic primitives are NIST FIPS 204/203 compliant.
Key Features
| Feature | Description |
|---|---|
| Post-Quantum L1 | Accounts, transactions and block signatures use quantum-resistant ML-DSA-65 |
| Client-Side Signing | Private keys never leave your browser |
| AMM/DEX | Uniswap V2-style liquidity pools and token swaps |
| Token Burning | Official burn address with on-chain tracking |
| Proof of Stake | Validator staking; today one designated proposer (largest stake) produces every block |
| P2P Network | Decentralized peer-to-peer block and transaction propagation |
| Base Bridge (R1) | Bridge XRGE, ETH (qETH) and USDC (qUSDC) between Base mainnet and RougeChain — classical authorization on the Base side |
| Custom Tokens | Create your own tokens on the network |
| RC-721 NFTs | NFT collections with royalties, batch minting, and freezing |
| Encrypted Messenger | E2E encrypted messaging with PQC, media support, self-destruct |
| PQC Mail | Encrypted email with @rouge.quant addresses and threading |
| Browser Extensions | Chrome/Firefox wallet extensions with vault lock |
| PWA Support | Installable progressive web app for mobile and desktop |
| Social Layer | Posts, timeline, reposts, likes, follows, comments, tips |
| CLI Wallet | Command-line wallet with full chain access and social commands |
| SDK | @rougechain/sdk npm package for building dApps |
| EIP-1559 Dynamic Fees | Base fee auto-adjusts per block, fee burning for deflationary pressure |
| Token Mint Authority | Creator-only minting for tokens created mintable since block 235, with an optional supply cap |
| Validator Unbonding | Unbonding queue with 500-block delay. No slashing is active: missed-block auto-slashing has been frozen since block 100 and the legacy slash transaction is rejected since block 245; evidence-based slashing is part of the planned consensus redesign |
| Finality | Verified BFT finality (FINALITY_V2) is live since block 150: every block carries its parent's ≥⅔-stake certificate — see Finality |
| WebSocket Subscriptions | Topic-based real-time event streaming (blocks, txs, accounts, tokens) |
| HD Wallet Derivation | BIP-44-like PQC key derivation from master seed (HMAC-SHA256) |
| Open Source | Apache 2.0 licensed node software |
Quick Links
- Status & Roadmap
- Security Overview
- Getting Started
- Running a Node
- API Reference
- P2P Networking
- Staking & Validators
- Browser Extensions
- CLI Wallet
- SDK
- Architecture
- GitHub (Node Source)
Network Info
Mainnet is live (chain-id rougechain-mainnet-1) alongside the public testnet.
| Network | Chain ID | API Endpoint |
|---|---|---|
| Mainnet | rougechain-mainnet-1 | https://api.rougechain.io/api |
| Testnet | — | https://testnet.rougechain.io/api |
| Devnet (local) | — | http://127.0.0.1:5101/api |
A local node's default
--api-portis 5101.
Tokens
XRGE
XRGE is the native token that powers transactions, secures the network through staking, and fuels the post-quantum economy.
| Role | How it works |
|---|---|
| Gas Token | Every transaction pays fees in XRGE. 50% of the base fee is burned; the remaining tip pool is split proposer 20% / validators 70% (stake-weighted) / treasury 10%. |
| Staking Primitive | Validators must stake XRGE to propose blocks. Each block's designated proposer is the eligible validator with the most stake; every staked validator earns a stake-weighted share of fees, whoever proposes. |
| DeFi Base Pair | AMM liquidity pools trade against XRGE. It's the default quote currency on the built-in DEX. |
| Bridge Asset | XRGE exists on both RougeChain (native) and Base (ERC-20) via the cross-chain bridge. |
XRGE on Base: ERC-20 at 0x147120faEC9277ec02d957584CFCD92B56A24317 (Base mainnet, chain id 8453).
XRGE trades on Base DEXs (for example Aerodrome), so it can be swapped from any Base-compatible
wallet, including Coinbase Wallet / the Base app. Always verify the contract address above. XRGE
bought on Base is an ERC-20 until you move it to RougeChain through the XRGE Bridge.
The XRGE bridge in production is the hardened R1 bridge (BridgeVaultV2), with classical
authorization on the Base side. The post-quantum V3 XRGE bridge is built but
not activated.
qETH
qETH is a bridged representation of ETH on RougeChain. It uses 6 decimal places and can be bridged in from Base mainnet or withdrawn back.
| Property | Value |
|---|---|
| Decimals | 6 |
| Bridge Source | Base mainnet (chain id 8453) |
| Bridge Contract | RougeBridge at 0x0c09C764AdC024497729cd452ECfeE8869d35d83 |
qUSDC
qUSDC is a bridged representation of USDC (Base mainnet) on RougeChain, using the same
RougeBridge contract. See USDC Bridge.
qETH and qUSDC use classical Base-side authorization and are outside the scope of the V3 post-quantum XRGE bridge.
Fees
| Action | Fee |
|---|---|
| Transfer | 1 XRGE (fixed for wallet/API-signed transfers) |
| Token Creation | 100 XRGE |
| Pool Creation | 10 XRGE |
| Swap | 0.3% of input (to LPs) + 1 XRGE |
| Minimum Stake | 10,000 XRGE (enforced by the node API, not by consensus) |
| Unbonding Period | 500 blocks |
EIP-1559 Fee Model: The base fee adjusts ±12.5% per block based on transaction volume (target: 10 txs/block), floor 0.001 XRGE (where mainnet has sat since September 2026). 50% of the base fee is burned; the remaining tip pool is split proposer 20% / validators 70% (stake-weighted) / treasury 10%. The 0.1 XRGE minimum-tip floor in the code draws from a reserve that is empty on mainnet, so it pays nothing. Check current fees via
GET /api/fee.
Burn Address
Tokens can be permanently burned by sending to the official burn address:
XRGE_BURN_0x000000000000000000000000000000000000000000000000000000000000DEAD
Burned tokens are tracked on-chain and can be queried via the /api/burned endpoint.
Security
Client-Side Signing
RougeChain uses a secure v2 API where all transactions are signed client-side:
- Your wallet creates a transaction payload
- The payload is signed locally using ML-DSA-65
- Only the signature and public key are sent to the server
- Your private key never leaves your browser
This ensures maximum security even when interacting with untrusted nodes.
Status & Roadmap
This page is the single source of truth for what is live versus what is built but not activated. If another page disagrees with this one, this page wins — please open an issue.
Last reviewed: 2026-10-16 (mainnet height 261).
RougeChain is post-quantum-secured at the L1 level. The current production XRGE bridge remains on the hardened R1 architecture. A V3 XRGE bridge using ML-DSA-65 post-quantum authorization has been built and rehearsed; it is not deployed and awaits an external audit.
⚠️ Current node release: 1.6.8 (blocks 235, 245 and 260 have passed)
Mainnet activated mintable tokens and contract royalty reads at block 235, the monetary-integrity rule at block 245 (2026-10-05) and AMM_INTEGRITY at block 260 (2026-10-10). Every mainnet node must run release 1.6.7 or later to follow the chain (release 1.6.7). Release 1.6.8 (2026-10-15) fixes a bug that could stop a node producing and importing blocks while processing certain public NFT mints. It is not a consensus change, is recommended for every node, and is what the operator nodes run (release notes). Nodes installed with the one-line installer update themselves. The full schedule is in Upgrade schedule.
Suspended since block 245
The monetary-integrity rule makes a block invalid if it carries one of these transaction types:
shield, shielded_transfer, unshield, token_lock, token_unlock, create_staking_pool,
token_stake, token_unstake, create_proposal, cast_vote, execute_proposal, delegate,
undelegate, token_approve, token_transfer_from. The features behind them — shielded
transfers, token locking, token staking pools, on-chain governance and voting, vote delegation —
are therefore not available, however an older page or client describes them.
- Why. Their ledger effects were applied without the state checks they depend on. The V1 shielded pool's proof established value balance only and did not bind a withdrawal to a specific note, and its prover has no zero-knowledge mode, so it did not deliver privacy. The other types were never used on mainnet. The mainnet shielded pool was empty when it was suspended; no funds are affected.
- What replaces them. A redesigned shielded pool (V2) is specified and in development: a
Plonky3-based STARK with a hiding mode (proofs of about 190 KB, verify about 6 ms, prove about 3 s
on a phone), launching under a 1,000,000 XRGE pool cap before the external audit. It is not
audited, and nothing may claim audited privacy until it is. The other types are being rebuilt
with their checks in consensus; no activation height exists. Token allowances are unaffected
(
approve/transfer_fromstay enabled). Treasury decisions for Regenerate use the node-hosted votes in the meantime.
Earlier mandatory node upgrade (block 49)
If you run a RougeChain mainnet node or validator, you must upgrade. On 2026-09-18 mainnet
activated a protocol upgrade at block 49 (bridge security hardening and a fix to validator-stake
accounting). The upgraded code is on the public repository:
rougechain-node main.
A node running a build from before 2026-09-18 may show the correct block height, but it executes blocks with the old rules and ends up with incorrect balances and validator state. There is no partial fix: upgrade and resync. Stake is unaffected — an un-upgraded validator simply is not participating.
sudo systemctl stop rougechain-validator
cp -a ~/.quantum-vault/mainnet ~/.quantum-vault/mainnet.backup # keep a backup
cd ~/rougechain && git pull --ff-only
source ~/.cargo/env && cd core && cargo build --release --locked -p quantum-vault-daemon
mv ~/.quantum-vault/mainnet ~/.quantum-vault/mainnet.old
mkdir -p ~/.quantum-vault/mainnet
cp ~/.quantum-vault/mainnet.backup/node-keys.json ~/.quantum-vault/mainnet/
sudo systemctl start rougechain-validator
Paths and the service name are those of a source-build install (repository in ~/rougechain, data
in ~/.quantum-vault/mainnet); adjust them to your setup. A node installed with the current
install-validator.sh runs a signed release binary instead (/usr/local/bin/quantum-vault-daemon,
with the rougechain CLI from the same release in /usr/local/bin/rougechain; data in
/var/lib/rougechain/mainnet) and is upgraded by re-running the installer, which verifies
the release signature before installing anything — see Signed releases.
node-keys.json is your validator identity — keep it and never share it. A fresh sync takes under
a minute.
Verify you are on the canonical chain:
- block 49 has hash
4c7ec92d9fc4dd24146036474128072d07d146d5cde93a0d31c0462eea895abb(curl -s "localhost:5100/api/blocks?limit=100") state_rootfromcurl -s localhost:5100/api/statsequals the one athttps://api.rougechain.io/api/statsat the same height
Live today
| Component | Status | Notes |
|---|---|---|
RougeChain mainnet (rougechain-mainnet-1) | LIVE | ML-DSA-65 accounts, transactions and block signatures |
| R1 production bridge (Base mainnet ⇄ RougeChain) | LIVE | Hardened R1 architecture; see Bridge Security Model |
XRGE bridge (BridgeVaultV2) | TESTED / USABLE | Deposit, withdrawal and payout paths tested end-to-end on mainnet |
qETH bridge (RougeBridge) | TESTED / USABLE | Same |
qUSDC bridge (RougeBridge) | TESTED / USABLE | Same |
| Verified BFT finality (FINALITY_V2, Release 2a) | LIVE since block 150 | Every block carries its parent's ≥⅔-stake certificate; see Finality |
| Player-signed contracts (GAME_READY) | LIVE since block 150 | Deploy and call signed by the player; see Smart Contracts |
| Game-ready contracts (GAME_READY 2) | LIVE since block 160 | Tokens, NFTs, collections and minting, randomness, multi-hop moves; state root covers NFTs and contract state |
Grind-proof game rolls (GAME_READY 3, host_block_hash) | LIVE since block 170 | Commit-then-settle randomness; one-step host_random rolls can be ground by the sender |
Payable contract calls (attach) | LIVE since block 190 | Pay XRGE or tokens with a contract call; moves only if the call succeeds |
| Mintable custom tokens (creator-only minting, optional cap) | LIVE since block 235 | Tokens created before 235 stay fixed-supply; see Token creation |
Contracts read NFT royalties (host_nft_royalty_bps / host_nft_royalty_recipient) | LIVE since block 235 | Royalties above 100 % refused at collection creation; nft_marketplace example |
| Monetary-integrity rule | LIVE since block 245 | Fee range, no slash, no faucet on mainnet, suspended types above |
| Pool transactions all-or-nothing (AMM_INTEGRITY) | LIVE since block 260 | A pool transaction that cannot take effect changes nothing and pays no fee; one pool per pair; checked integer amounts |
| LP fee collection | LIVE | "Collect fees" on Pools withdraws only fee earnings |
ETH / USDC deposits through the bridge contract (depositETH / depositERC20, auto-claimed) | LIVE — reopened 2026-10-05 | The manual claim route is closed at the public endpoint |
| Bitcoin deposits (BTC → qBTC) | LIVE | Bitcoin withdrawals are switched off in the site while the relayer is stopped; see Bitcoin Bridge |
The production XRGE bridge still relies on classical (ECDSA / Safe multisig) authorization on the Base side. It is hardened, capped and monitored, but it is not post-quantum on Base. The ETH/USDC contract is owned by a single operator hot key. No bridge component has been externally audited.
Validators and staking, as they run today
| Fact | Today |
|---|---|
| Staked validators | 3 — about 100.09 M XRGE (the operator's producing validator), 10,000 XRGE (the operator's second node, non-producing) and 9,000 XRGE (an outside key) |
| Block production | One validator produces every block (designated proposer = largest stake, since block 100); no fallback proposer — if it is offline the chain stops |
| Finality | FINALITY_V2 since block 150; the largest validator's own vote reaches the ⅔ quorum |
| Minimum stake | 10,000 XRGE is enforced by the node API only (/api/v2/stake, CLI). Consensus accepts any positive stake — hence a 9,000 XRGE validator |
| Slashing | None active. Missed-block accounting frozen since block 100; the legacy slash transaction is rejected from block 245; evidence-based slashing does not exist yet |
| Unbonding | 500 blocks (a block count, not a duration) |
| Rewards | Fees only: half the base fee per transaction burned, tip pool 20 % proposer / 70 % all validators by stake (no participation check) / 10 % __treasury__. The 0.1 XRGE minimum-tip floor draws from __staking_rewards__, which holds 0 XRGE on mainnet — so it pays nothing |
Built and tested — NOT activated
| Component | Status |
|---|---|
V3 XRGE bridge (BridgeVaultV3) | AUDIT CANDIDATE / NOT ACTIVATED — not deployed to Base mainnet |
| ML-DSA-65 post-quantum root authorization (on-chain verifier) | Built, tested, frozen for audit — not deployed |
| Table-in-proof verifier architecture | Built, tested, frozen for audit — not deployed |
| Deterministic epoch / withdrawal-root pipeline | Built and tested — not running in production |
| Post-quantum root-authority signer | Built and tested — a 2-of-3 genesis authority set has been assembled offline; no production root has been signed |
| Post-quantum authority rotation + deterministic authority schedule | Built and tested, rehearsed end to end on a throwaway network — not in use |
| Reproducible V3 contract build / audit candidate | Complete |
| OP-stack deployment rehearsal (local devnet, throwaway keys and token) | Complete |
| Shielded pool V2 | Specified, in development — see "Suspended since block 245" |
| XRGE holder staking contract (WASM, fixed reward pot, 3-day unstake delay) | Deployed and tested on testnet only — not on mainnet |
Consensus redesign — decided 2026-10-06, NOT built
A redesign of consensus was decided on 2026-10-06. Nothing of it is implemented or scheduled; it is gated on a consensus simulator and an independent review, and will ship as staged forks A–E whose heights will be announced in advance. It supersedes the earlier "Release 2b" and "Release 3" plans.
| Element | Planned |
|---|---|
| Rounds | Tendermint-style rounds with ML-DSA-65 prevotes and precommits |
| Proposer | Rotating, stake-weighted, in place of the fixed largest-stake proposer |
| Active set | Capped at 32 validators |
| Minimum stake | 100,000 XRGE enforced by consensus, with a 30-day grace period for existing validators |
| Unbonding | 21 days |
| Slashing | Evidence-based: double-signing 100 % of stake; downtime jail only |
| Liveness | Hourly heartbeat blocks so the chain advances without transactions |
| Rewards | A per-time reserve subsidy in addition to fees |
| Admission | Temporary approval of new validators, with a written exit condition |
| Later | Validator key separation and delegation |
FINALITY_V2_ACTIVATION_HEIGHT is 150 (active). V3_BRIDGE_ACTIVATION_HEIGHT is None: no
activation height has been chosen for the V3 bridge.
Scope of V3
- V3 changes the XRGE withdrawal authorization path on Base to ML-DSA-65.
- qETH and qUSDC are outside the V3 scope. They stay on
RougeBridgewith classical Base-side authorization. - The Bitcoin bridge is a separate system and is not part of V3. See Bitcoin Bridge.
Roadmap
| Milestone | State |
|---|---|
| R1 bridge hardening | ✅ Complete (live) |
| V3 architecture | ✅ Complete |
| Reproducible audit candidate | ✅ Complete |
| OP-stack rehearsal | 🟡 Substantially complete |
| External review / audit | ⏳ Pending |
| Production deployment of V3 | ⏳ Pending |
| FINALITY_V2 activation | ✅ Live (block 150) |
| Game-ready contracts (GAME_READY 1 + 2 + 3, payable calls) | ✅ Live (blocks 150, 160, 170, 190) |
| Mintable tokens, contract royalty reads | ✅ Live (block 235) |
| Monetary-integrity rule | ✅ Live (block 245) |
| AMM_INTEGRITY (pool transactions all-or-nothing) | ✅ Live (block 260) |
| Shielded pool V2 | 🟡 In development, unaudited |
| Restoring the other suspended types | ⏳ Being rebuilt, no height |
| Consensus redesign (forks A–E) | ⏳ Decided, not built; simulator + independent review first |
| V3 activation | ⏳ Pending |
Activation will be announced in advance with the exact block heights. Until then, nothing on this page in the "not activated" table protects user funds.
Security Overview
An honest summary of what is post-quantum-secured today and what is not. For live/pending status see Status & Roadmap. Last reviewed: 2026-10-06.
RougeChain L1 — post-quantum-secured
- Accounts, transactions, validator block signatures and staking operations use ML-DSA-65 (FIPS 204). Messaging and mail use ML-KEM-768 (FIPS 203).
- Transactions are signed client-side; private keys do not leave the wallet.
- In the web wallet, setting a password (min 8 characters) is required to finish creating or importing a wallet, and keys are written to browser storage only encrypted (AES-256-GCM). A wallet an older version stored unencrypted must be secured with a password before it can be used again. See Create a Wallet.
- Messages and mail are encrypted to each participant's long-term ML-KEM-768 key. There is no forward secrecy: anyone who later obtains that key can decrypt the stored messages and mail it was used for, both received and sent.
- These algorithms are quantum-resistant under current cryptographic understanding. No system can promise more than that, and implementation bugs remain possible.
- Shielded transfers are not available. The V1 shielded pool is suspended since block 245: its STARK proof established value balance only, did not bind a withdrawal to a specific note, and its prover has no zero-knowledge mode, so it did not deliver privacy. The mainnet pool was empty. A redesigned pool (V2, Plonky3-based with a hiding mode, launching under a 1,000,000 XRGE cap) is in development and unaudited; nothing may claim audited privacy until an external audit covers it.
- Messenger and mail are node-hosted, not on-chain. Since node release 1.6.3 a directory entry can
be replaced only by the signing key that owns it, an encryption key registered to another wallet is
refused, and the unauthenticated legacy read routes return
410 Gone. Nodes before 1.6.3 do not enforce this.
Signatures commit to the network
Mainnet (rougechain-mainnet-1) and testnet (rougechain-devnet-1) accept the same keys, and the
web wallet uses one key on both. From node release 1.6.4 and the wallet releases listed in the
changelog, every payload a wallet signs for a node — transactions and signed requests
(mail, messenger, names, votes) — carries the chain id of the network it is meant for (chainId;
chain_id in a rougechain CLI envelope), inside the signed bytes. A signature therefore commits to
one network.
- Node rule (release 1.6.4, no fork). A node refuses a signed payload that names another chain id
(
CHAIN_ID_MISMATCH), at the API, at mempool admission and when producing. A payload that names no network is still accepted while wallets update; operators turn onREQUIRE_SIGNED_CHAIN_IDto refuse those too (CHAIN_ID_REQUIRED). - Wallets take the chain id from their network setting, compare it once per session with the
node's
GET /api/health→chain_id, and refuse to sign if the two differ. The extension and Qwalla show the network a request is for and refuse one for a network other than the selected one. - Consensus rule (CHAIN_ID_BINDING, not scheduled). From an activation height to be announced, a block is invalid if any transaction's signed bytes do not commit to the chain id; node-signed transactions switch to a network-bound signing format at the same height. Until then consensus is unchanged. See the upgrade schedule.
- Sign-in messages already carry a
Chain IDline; verifiers must check it (Wallet Authentication). - Contracts that verify signatures themselves can read the chain id with
host_get_chain_idfrom the CONTRACT_CHAIN_ID upgrade (not scheduled) — see Smart Contracts.
Bridges — mixed
| Path | Today |
|---|---|
| RougeChain side of every bridge | ML-DSA-65-signed withdrawals |
XRGE on Base (BridgeVaultV2) | Classical Base-side authorization (Safe multisig + capped relayer key). Hardened R1 architecture. |
qETH / qUSDC on Base (RougeBridge) | Classical Base-side authorization: single operator owner/release key, with a 2-of-3 Safe multisig guardian that can pause the bridge and cancel queued large releases |
| Bitcoin (qBTC) | Separate bridge; Bitcoin custody uses Bitcoin's classical signatures |
So: assets held on RougeChain are protected by post-quantum signatures. The Base-side
custody of bridged assets is not post-quantum today. The bridges are operator-run (deposit
watcher, relayers and the bridge_mint key), the ETH/USDC contract is owned by a single hot key, and
no bridge component has been externally audited. ETH/USDC deposits go through the contract's
depositETH / depositERC20 and are claimed automatically; the manual claim route is closed at the
public endpoint. Bitcoin deposits are live; Bitcoin withdrawals are switched off in the site.
What V3 will change (not activated)
The V3 XRGE bridge changes the XRGE authorization path on Base to on-chain-verified ML-DSA-65 signatures from an M-of-N authority. It has been built, frozen as an audit candidate and rehearsed, but is not deployed or activated, and it has not yet been externally audited.
- qETH and qUSDC remain outside the V3 post-quantum scope.
- The Bitcoin bridge is separate from the Base bridge architecture and from V3.
Finality
Verified BFT finality (FINALITY_V2) is live since block 150: every block carries a certificate of ML-DSA-65 precommits from more than two thirds of stake for its parent, and nodes reject blocks without one. Stake is still concentrated in few keys — three validators are staked and one holds more than 99.9 % of the stake, so its own vote is the quorum — which limits what any BFT guarantee means until the validator set broadens.
No slashing is active. Missed-block slashing is frozen since block 100, the legacy slash
transaction is rejected since block 245, and evidence-based slashing does not exist yet. The 10,000
XRGE minimum stake is enforced by the node API, not by consensus.
Decentralization
Validator stake and bridge operation are currently concentrated in a small operator set. This is a known limitation and part of the roadmap: a consensus redesign (rotating proposers, a capped validator set, a consensus-enforced minimum stake, evidence-based slashing) was decided on 2026-10-06 and is not built; see Status & Roadmap.
Getting Started
This guide will help you get up and running with RougeChain in minutes.
Prerequisites
- A modern web browser (Chrome, Firefox, Edge, Safari)
- For running a node: Rust toolchain (1.80+)
Quickest Path
- Visit rougechain.io
- Click "Wallet" in the sidebar
- Create (or import) a wallet and set a password when asked (required, min 8 chars) — your keys are encrypted at rest with AES-256-GCM (PBKDF2, 600k iterations) and are never stored unencrypted in the browser. Save a
.pqcbackupas well (see Create a Wallet) - Use the faucet to get free testnet XRGE
That's it! You now have a quantum-secure wallet.
What's Next?
- Quick Start Guide - Detailed walkthrough
- Create a Wallet - Understanding your keys
- Get Test Tokens - Using the faucet
- Running a Node - Participate in the network
Quick Start
Get started with RougeChain in 5 minutes.
Step 1: Access the Web App
Visit rougechain.io or run locally:
git clone https://github.com/cyberdreadx/rougechain-node
cd rougechain-node
npm install
npm run dev
Open http://localhost:5173 in your browser.
Step 2: Create Your Wallet
Create (or import) a wallet and set a password when asked (required, min 8 characters — the wallet is saved only once it's encrypted). It includes:
- Address (
rouge1...) — Your compact Bech32m address, share freely - Private Key (ML-DSA-65) — Never share this!
- Encryption Key (ML-KEM-768) — For secure messaging
On the web, your keys are encrypted at rest with AES-256-GCM (PBKDF2, 600k iterations): the password is required, and until you set it during create / import the wallet exists only in the tab's sessionStorage (it is never written to localStorage unencrypted). While unlocked, the decrypted wallet is kept in sessionStorage (cleared when the tab closes). In the browser extension, the decrypted key lives in chrome.storage.session and only the encrypted vault is persisted (legacy plaintext extension wallets are force-migrated to encrypted storage on next unlock). Your 24-word phrase restores your messaging/mail key only for wallets created or imported from the phrase on the website (since 2026-10-01) or in Qwalla; for older website wallets and extension wallets, export a .pqcbackup too (details).
Step 3: Get Test Tokens
- Click Wallet in the sidebar
- Click Request from Faucet
- Receive 10,000 XRGE instantly (24-hour cooldown per address)
Step 4: Send Your First Transaction
- Click Send
- Enter recipient address
- Enter amount
- Click Send XRGE
Transaction is signed with your ML-DSA-65 key and broadcast to the network.
Step 5: View on Blockchain
- Click Blockchain in sidebar
- See your transaction in the latest block
- Verify the PQC signature
What's Next?
- Run your own node
- Stake and become a validator
- Create custom tokens
- Use encrypted messenger
- Send encrypted mail
- Install the browser extension
- Use the SDK
Troubleshooting
"Failed to fetch" Error
- Check if you're connected to the right network (Testnet vs Devnet)
- Ensure the node is running if using local devnet
"Insufficient balance"
- Transaction requires amount + 1 XRGE fee
- Use faucet to get more tokens
Wallet not loading
- Clear browser cache (do not clear site data /
localStorageunless you have your seed phrase and.pqcbackup— it deletes a wallet stored in the browser) - Check browser console for errors
Create a Wallet
You explicitly create (or import) your RougeChain wallet, and onboarding then asks you to set a password (at least 8 characters). The password is required: on the web, the wallet is saved in your browser only once it's encrypted with it. Here's what you need to know about your wallet.
Wallet Components
| Component | Algorithm | Purpose |
|---|---|---|
| Signing Key | ML-DSA-65 | Sign transactions, prove ownership |
| Encryption Key | ML-KEM-768 | Encrypt/decrypt messages |
Your Address
RougeChain uses compact Bech32m addresses with the rouge1 prefix, derived from your ML-DSA-65 public key:
address = bech32m("rouge", SHA-256(signing_public_key))
Example address:
rouge1q8f3x7k2m4n9pvj5dz6ywl2cg8hs0kw9...
Addresses are ~63 characters — much shorter than the raw 3,904-char hex public key. The wallet, extension, and explorer all display this format.
Note: Some API endpoints still require the raw hex public key (see Address Format). The
rouge1address is for display, sharing, and QR codes.
Backup Your Wallet
CRITICAL: Your private key is only stored locally. If you clear browser data or lose your device, you lose access to your funds.
Encrypted Backup (.pqcbackup)
All RougeChain wallets use password-protected encrypted backups. Plaintext key exports are disabled across all platforms.
How to Export
| Platform | Steps |
|---|---|
| Web (rougechain.io) | Wallet → Settings → Backup → Enter password → Download .pqcbackup file |
| Browser Extension (RougeChain Wallet) | Settings → Export Encrypted Backup → Enter password → Download |
| Mobile (Qwalla) | Settings → Export Encrypted Backup → Enter password → Share/Save |
How to Restore
- Go to Wallet page (or Settings in the extension)
- Click Restore or Import Wallet
- Select your
.pqcbackupfile - Enter the password you used when exporting
- Your wallet keys are decrypted and restored
Seed Phrase (Mnemonic)
Wallets also support 24-word BIP-39 mnemonic backup (256-bit entropy for post-quantum security):
- Go to Settings → Reveal Seed Phrase
- Write down all 24 words in order
- Store securely — anyone with your seed phrase has full access to your wallet
Important: The seed phrase restores your signing key and address (and so your funds). Wallets created or imported from the phrase on rougechain.io (since 2026-10-01) or in Qwalla also get a messaging/mail encryption key derived from the phrase, so the phrase alone restores it — the same key in both apps. Wallets created on the website before that change have a random encryption key that the phrase cannot restore: keep their
.pqcbackupfile, which contains both keys, to read earlier messages and mail. The browser extension still generates a random encryption key, so keep a.pqcbackupfor extension wallets too.
.pqcbackup File Format
The encrypted backup file uses industry-standard cryptography:
| Component | Algorithm | Details |
|---|---|---|
| Key Derivation | PBKDF2-SHA256 | 600,000 iterations |
| Encryption | AES-256-GCM | 256-bit key, 96-bit IV |
| Salt | Random | 16 bytes per export |
| IV | Random | 12 bytes per export |
File Structure
{
"version": 1,
"salt": "<hex-encoded 16-byte random salt>",
"iv": "<hex-encoded 12-byte random IV>",
"ciphertext": "<hex-encoded AES-256-GCM encrypted wallet data>",
"algorithm": "PBKDF2-SHA256-AES-256-GCM"
}
How It Works
- Your password is stretched with PBKDF2-SHA256 (600,000 rounds) using a random salt
- The derived 256-bit key encrypts your wallet data with AES-256-GCM
- The salt, IV, and ciphertext are bundled into a
.pqcbackupJSON file - Without the correct password, the file cannot be decrypted
Warning: There is no password recovery. If you forget your backup password, you can restore your address and funds from your seed phrase; your old messages and mail come back too only if the wallet's encryption key is seed-derived (see above).
Security Best Practices
- Never share your private key or seed phrase
- Backup your wallet immediately after creation
- Use a strong, unique password for your
.pqcbackupfile - Store backups in multiple locations (password manager, USB, etc.)
- Never screenshot your seed phrase or keys
- Verify addresses before sending
Technical Details
Key Generation
// 24-word mnemonic (256-bit entropy — post-quantum safe)
const mnemonic = bip39.generateMnemonic(256);
// Derive 32-byte seed via HKDF-SHA256
const seed = hkdf(sha256, mnemonicToSeed(mnemonic), undefined, "rougechain-ml-dsa-65-v1", 32);
// Signing keypair (ML-DSA-65 / FIPS 204)
const signingKeypair = ml_dsa65.keygen(seed);
// Encryption keypair (ML-KEM-768 / FIPS 203) — derived from the phrase, identical to Qwalla.
// (No phrase, e.g. a private-key import: the signing private key hex is used instead.)
const kemSeed = sha512(utf8(`${mnemonic || signingPrivateKeyHex}|rougee-gram|kem-v1`)); // 64 bytes
const encryptionKeypair = ml_kem768.keygen(kemSeed);
// Wallets created on the website before 2026-10-01 keep their original random key.
Key Storage
Once you set a password, keys are encrypted at rest with AES-256-GCM (PBKDF2, 600,000 iterations).
On the web, a new or imported wallet is held only in the tab's sessionStorage until you set its password; nothing is written to localStorage before that, so if you close the tab during the recovery-phrase or password step the wallet is not kept (restore it from the recovery phrase). Setting the password stores the encrypted blob in localStorage. While the wallet is unlocked, the decrypted wallet is kept in sessionStorage, which the browser clears when the tab closes. Keep a .pqcbackup as well.
Older versions of the site saved a wallet without a password to localStorage unencrypted. If you still have such a wallet, rougechain.io asks you to set a password before you can keep using it (you can back it up first); the wallet is then encrypted and the unencrypted copy is deleted.
| Platform | Storage |
|---|---|
| Web (rougechain.io) | encrypted blob in localStorage; decrypted wallet in sessionStorage while unlocked (and before the password is set during create / import). Private keys are never written to localStorage unencrypted |
| Browser Extension (RougeChain Wallet) | encrypted AES-256-GCM vault in chrome.storage.local; decrypted key in chrome.storage.session, never written to disk |
| Mobile (Qwalla) | expo-secure-store → device keychain |
Terminal EVM Wallet (tools/evm-cli)
For Base/EVM operations there's a companion terminal wallet, rougechain-evm (tools/evm-cli). It derives your Base account from the same 24-word mnemonic (read from the ROUGECHAIN_MNEMONIC env var) and supports new, address, balance, send-eth, and send-token.
Get Test Tokens
The faucet distributes free XRGE tokens for testing on testnet.
Using the Web UI
- Go to the Wallet page
- Click Request from Faucet
- Receive 10,000 XRGE instantly
Using the API
curl -X POST https://testnet.rougechain.io/api/faucet \
-H "Content-Type: application/json" \
-d '{"recipientPublicKey": "your-hex-public-key-here"}'
The field is
recipientPublicKeyand must be a raw hex public key, not arouge1…address. The faucet is OFF by default and must be explicitly enabled with--faucet-enabled(orQV_FAUCET_ENABLED=1) — it is for dev/testnet only, and mainnet must never set it. When disabled, requests return"Faucet is disabled on this network."
Response:
{
"success": true,
"txId": "abc123...",
"tx": { "...": "the faucet transfer (TxV1)" },
"error": null
}
The signed POST /api/v2/faucet endpoint (used by the SDK) instead returns { "success": true, "message": "Faucet: 10000 XRGE sent" }, without a transaction id.
Rate Limits
| Condition | Limit |
|---|---|
| Per address | 1 request / 24 hours |
The cooldown is shared by POST /api/faucet and POST /api/v2/faucet (one claim per key per 24 hours, whichever endpoint is used) and starts only after a successful payout. There is no per-IP rate limit. A request is also rejected if the recipient already has a pending faucet transaction, or if their balance exceeds the faucet threshold.
Whitelisting
The whitelist gates faucet access — it does not grant a higher rate. When QV_FAUCET_WHITELIST is set, only the listed addresses may use the faucet (both /api/faucet and the signed /api/v2/faucet, which returns 403); everyone else is blocked. Leave it unset to allow any address (subject to the 24-hour cooldown).
# Start node with the faucet enabled and a whitelist
./quantum-vault-daemon --mine --faucet-enabled \
--faucet-whitelist "pubkey1,pubkey2,pubkey3"
# Or via environment variables
export QV_FAUCET_ENABLED=1
export QV_FAUCET_WHITELIST="pubkey1,pubkey2"
./quantum-vault-daemon --mine
Troubleshooting
"Rate limited"
Each address can request once every 24 hours. Wait for the cooldown to elapse or use a different address.
"Faucet disabled"
The node may not have faucet enabled. Check node configuration.
Transaction not appearing
- Check the block explorer for your tx
- Verify you're on the correct network
- Wait for the next block (1-2 seconds)
Running a Node
Run your own RougeChain node to sync the chain, validate transactions, serve an API to others, and — if you stake — produce blocks and earn rewards.
The node software is open source on GitHub.
Node Types
| Type | What it does | Key flags |
|---|---|---|
| Full node | Syncs and validates all blocks from peers | --peers |
| Mining / validator node | Votes on blocks once staked (≥ 10,000 XRGE); proposes only while it is the designated proposer (the validator with the most stake) | --mine |
| Public node | Serves an API/RPC to other users and apps | --public-url + reverse proxy |
A single node can be all three at once.
Minimum System Requirements
RougeChain's node is deliberately lightweight — the Rust daemon is a single ~23 MB binary and the live mainnet validator holds a steady ~40 MB of RAM. The heaviest part is building it, not running it.
To run a node
| Minimum | Recommended (public / validator) | |
|---|---|---|
| CPU | 1 vCPU | 2+ vCPU |
| RAM | 1 GB | 2–4 GB |
| Disk | 5 GB SSD | 20+ GB SSD (chain data grows over time; SSD matters for the embedded sled DB) |
| Network | Any stable broadband | ≥ 100 Mbps, static IP or a domain with TLS |
| OS | Linux (Ubuntu 22.04+), Windows, or Docker | Linux |
The daemon itself uses tens of MB of RAM; the extra headroom is for the OS, chain-data growth, and serving concurrent API traffic.
To build from source
| Requirement | |
|---|---|
| Rust | Recent stable toolchain (1.80+); the project builds on 1.94 |
| RAM during compile | ~4 GB |
Free disk for target/ | ~6 GB |
| Tools | git, a C toolchain (build-essential on Debian/Ubuntu) |
A clean release build takes roughly 2–5 minutes depending on cores. If you only want to run a node and not build it, you can also use the Docker image.
Quick Start
# Clone and enter the workspace
git clone https://github.com/cyberdreadx/rougechain-node
cd rougechain-node/core
# Build the daemon (release)
cargo build --release -p quantum-vault-daemon
Join Mainnet
Mainnet requires the mainnet genesis file (shipped in the repo) and the mainnet chain-id:
./target/release/quantum-vault-daemon \
--genesis daemon/genesis-mainnet.json \
--chain-id rougechain-mainnet-1 \
--api-port 5100 \
--data-dir ~/.quantum-vault/mainnet \
--peers "https://api.rougechain.io/api" \
--node-name my-node
Add --mine if you intend to validate. Your node only proposes once its key is staked (≥ 10,000 XRGE) and it is the designated proposer, i.e. it holds the most stake — see Staking.
Join Testnet
Testnet uses default parameters (no genesis file needed):
./target/release/quantum-vault-daemon \
--chain-id rougechain-devnet-1 \
--api-port 5101 \
--data-dir ~/.quantum-vault/testnet \
--peers "https://testnet.rougechain.io/api" \
--node-name my-testnet-node
Running a Public Node (for other people)
If you want your node to serve an API/RPC to users, wallets, or apps, run it as a public node. Do not expose the daemon port directly — put a TLS reverse proxy (nginx) in front and keep the daemon bound to localhost or firewalled.
Recommended operator setup:
- Bind and advertise — run with the default
--host 127.0.0.1and--api-port 5100, and set--public-url https://your-domain/apiso peers can reach you through the proxy. Peers use the same REST API; there is no separate P2P port. - Terminate TLS at nginx — proxy
https://your-domain/api→http://127.0.0.1:5100. A full nginx example is in Running a Public Node. - API keys (private nodes only) —
--api-keys "<key1>,<key2>"makes every route that is not exempt return401unless the request sendsX-API-KeyorAuthorization: Bearer. This covers reads too (e.g./api/blocks, balances) and the peer routes (/api/blocks/import,/api/peers/register,/api/tx/broadcast); peers never send a key, so a keyed node stops accepting pushed blocks and transactions. Exempt:/,/api/health,/api/stats,/api/v2/*,/api/bridge/*,/api/messenger/*,/api/mail/*,/api/names/*,/api/finality/*, the faucet and the bot. Signed routes check their own signatures. - Rate-limit — e.g.
--rate-limit-read-per-minute 600 --rate-limit-write-per-minute 60. All limits default to0(unlimited);--rate-limit-per-minuteis a deprecated alias for both when neither is set. Limits are keyed by the connecting IP, so behind a reverse proxy every client shares one bucket unless you pass--trust-proxy, which keys loopback (proxied) requests byX-Real-IP/ the rightmostX-Forwarded-Forentry. - Run under systemd — so it restarts on crash/reboot. Example unit and hardening in Public Node and Configuration.
- Monitor — poll
/api/health(liveness) and/api/stats(height, peers, mining) from your uptime checker.
Security note: legacy
v1write endpoints that accept a raw private key are development-only (enabled with--dev) and return410 Gonein a normal build — never run a public node with--dev.
Verify It's Working
curl http://127.0.0.1:5100/api/health
Expected response (mainnet):
{
"status": "ok",
"chain_id": "rougechain-mainnet-1",
"height": 12345
}
Check sync progress and peers:
curl http://127.0.0.1:5100/api/stats
Next Steps
- Installation — detailed setup, per-OS
- Configuration — every CLI flag and environment variable
- Running a Public Node — full nginx + systemd operator guide
- Mining — block production and validator selection
- Staking — the 10,000 XRGE minimum and rewards
Installation
System Requirements
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 1 vCPU | 2+ vCPU |
| RAM | 1 GB (run) · ~4 GB (build) | 2–4 GB |
| Disk | 5 GB SSD | 20 GB SSD |
| Network | 10 Mbps | 100 Mbps |
| OS | Linux (Ubuntu 22.04+, Debian 12), macOS, Windows | Ubuntu 22.04 LTS |
The daemon is a single Rust binary with low overhead. A $5/month VPS (Hetzner CX22, DigitalOcean Basic Droplet, Vultr Cloud Compute) is more than enough for testnet.
Docker (Fastest)
Run a node with a single command — no Rust toolchain needed:
docker run -d \
--name rougechain-node \
-p 127.0.0.1:5100:8900 \
-v qv-data:/data/rougechain \
rougechain/node \
--data-dir /data/rougechain --host 0.0.0.0 --api-port 8900 \
--mine --peers https://testnet.rougechain.io/api
Arguments after the image name replace the image's default command, so keep --data-dir /data/rougechain (the image's volume) and --api-port 8900 (the port the image's health check probes). See Docker.
Verify it's running:
curl http://127.0.0.1:5100/api/health
docker-compose
For a persistent setup, create a .env file:
API_PORT=5100
QV_PEERS=https://api.rougechain.io/api
The compose file always uses the mainnet genesis (chain id rougechain-mainnet-1; there is no chain-id setting) and always passes --mine, so point QV_PEERS at mainnet peers. The API is published on 127.0.0.1:${API_PORT} only; set API_BIND=0.0.0.0 to expose it on every interface.
Then start:
git clone https://github.com/cyberdreadx/rougechain-node
cd rougechain-node
docker compose up -d
View logs:
docker compose logs -f node
Build the image locally
git clone https://github.com/cyberdreadx/rougechain-node
cd rougechain-node
docker build -t rougechain/node .
Build from Source
Install and build manually if you prefer not to use Docker.
Prerequisites
Linux / macOS
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# Install build dependencies (Ubuntu/Debian)
sudo apt update && sudo apt install -y build-essential pkg-config libssl-dev
# Install build dependencies (macOS)
xcode-select --install
Windows
- Install Rust
- Install Visual Studio Build Tools
- Ensure
cargois in your PATH
Compile
# Clone the repository
git clone https://github.com/cyberdreadx/rougechain-node
cd rougechain-node/core
# Build release binary
cargo build --release -p quantum-vault-daemon
# Binary location
./target/release/quantum-vault-daemon --help
Verify Installation
./target/release/quantum-vault-daemon --version
Expected output:
quantum-vault-daemon 0.1.0
First Run
Option A: Connect to Testnet
./target/release/quantum-vault-daemon \
--api-port 5100 \
--peers "https://testnet.rougechain.io/api"
Option B: Start Local Devnet
./target/release/quantum-vault-daemon --mine --api-port 5100
Verify Node is Running
curl http://127.0.0.1:5100/api/health
Expected response:
{"status":"ok","chain_id":"rougechain-devnet-1","height":0}
Directory Structure
After first run, data is stored at:
| OS | Default Location |
|---|---|
| Linux/macOS | ~/.quantum-vault/core-node/ |
| Windows | C:\Users\<you>\.quantum-vault\core-node\ |
Updating
Bring your node up to the latest main with one command — it fetches, fast-forwards,
rebuilds the release binary, restarts the service, and health-checks:
cd rougechain-node
./scripts/update-node.sh
It refuses to run on a dirty working tree and only fast-forwards, so it never clobbers local changes. Override the defaults with env vars if your setup differs (systemd unit name, port, etc.):
ROUGECHAIN_SERVICE=my-node ROUGECHAIN_API_PORT=5100 ./scripts/update-node.sh
Manual equivalent
cd rougechain-node
git pull --ff-only
cd core
cargo build --release -p quantum-vault-daemon
sudo systemctl restart rougechain # your service name
Troubleshooting
"cargo not found"
source ~/.cargo/env
# or restart your terminal
"OpenSSL not found"
# Ubuntu/Debian
sudo apt install libssl-dev pkg-config
# Fedora/RHEL
sudo dnf install openssl-devel
# macOS
brew install openssl
"Address already in use"
Another process is using port 5100:
# Find and kill it
lsof -i :5100
kill -9 <PID>
# Or use a different port
./quantum-vault-daemon --api-port 5101
Build fails on Windows
Ensure you have Visual Studio Build Tools with C++ workload installed.
Environment Variables
Some CLI flags can also be set via environment variables (others, such as --host, --api-port, --data-dir, --chain-id, --mine and the --rate-limit-* flags, are CLI-only):
| Variable | CLI Flag | Default | Description |
|---|---|---|---|
QV_PEERS | --peers | — | Comma-separated peer URLs |
QV_PUBLIC_URL | --public-url | — | This node's public URL for peer discovery |
QV_CORS_ORIGINS | — | built-in list (local dev servers, rougechain.io, www.rougechain.io, testnet.rougechain.io, rougee.app, www.rougee.app) | Comma-separated allowed CORS origins |
QV_API_KEYS | --api-keys | — | Comma-separated API keys for authenticated access |
QV_TRUST_PROXY | --trust-proxy | false | Key rate limits by the client IP a local reverse proxy reports (loopback peers only) |
QV_BRIDGE_CUSTODY_ADDRESS | --bridge-custody-address | — | EVM custody address (enables bridge) |
QV_BASE_SEPOLIA_RPC | --base-sepolia-rpc | https://sepolia.base.org | Base RPC URL used for bridge verification. The name is historical: on mainnet nodes set it to a Base mainnet RPC (chain id 8453) |
Common CLI-only flags:
| Flag | Default | Description |
|---|---|---|
--host | 127.0.0.1 | Bind address. Keep 127.0.0.1 and put a reverse proxy in front for public nodes; 0.0.0.0 exposes the API on every interface (needed inside Docker) |
--api-port | 5101 | REST API port |
--mine | off | Enable block production |
--data-dir | ~/.quantum-vault/core-node/ | Chain data directory |
--chain-id | rougechain-devnet-1 | Network chain ID |
--block-time-ms | 400 | How often the miner checks for pending transactions, in milliseconds (blocks are only produced when there are transactions) |
Running a Node on Windows
A step-by-step guide for building and running a RougeChain node on Windows.
Prerequisites
1. Install Rust
Download and run the installer from rustup.rs. Use the default options.
After installation, open a new PowerShell window and verify:
cargo --version
rustc --version
2. Install Visual Studio Build Tools
Download Visual Studio Build Tools and install the "Desktop development with C++" workload.
This provides the MSVC compiler and Windows SDK needed to build native Rust projects.
3. Install Git (if needed)
Download from git-scm.com or install via winget:
winget install Git.Git
Build the Node
Open PowerShell and run:
git clone https://github.com/cyberdreadx/rougechain-node.git
cd rougechain-node\core
cargo build --release -p quantum-vault-daemon
The first build takes 5-10 minutes. The binary will be at:
target\release\quantum-vault-daemon.exe
Run the Node
Connect to Testnet
.\target\release\quantum-vault-daemon.exe --api-port 5100 --peers "https://testnet.rougechain.io/api"
Run a Mining Node
.\target\release\quantum-vault-daemon.exe --mine --api-port 5100 --peers "https://testnet.rougechain.io/api"
Name Your Node
.\target\release\quantum-vault-daemon.exe --api-port 5100 --node-name "MyWindowsNode" --peers "https://testnet.rougechain.io/api"
Verify It's Working
In a separate PowerShell window:
Invoke-RestMethod http://127.0.0.1:5100/api/health
Expected output:
{
"status": "ok",
"chain_id": "rougechain-devnet-1",
"height": 123
}
Visit http://localhost:5100 in your browser to see the built-in node dashboard.
Run as a Background Service
Option A: Task Scheduler
- Open Task Scheduler → Create Task
- General: Name it "RougeChain Node", check "Run whether user is logged on or not"
- Trigger: At startup
- Action: Start a program
- Program:
C:\path\to\quantum-vault-daemon.exe - Arguments:
--mine --api-port 5100 --peers "https://testnet.rougechain.io/api" - Start in:
C:\path\to\rougechain-node\core
- Program:
- Settings: Check "Restart if task fails", set to every 1 minute
Option B: NSSM (Non-Sucking Service Manager)
# Install NSSM
winget install NSSM.NSSM
# Create the service
nssm install rougechain "C:\path\to\quantum-vault-daemon.exe"
nssm set rougechain AppParameters "--mine --api-port 5100 --peers ""https://testnet.rougechain.io/api"""
nssm set rougechain AppDirectory "C:\path\to\rougechain-node\core"
# Start
nssm start rougechain
Windows Firewall
If you want your node to be publicly accessible, don't open the daemon's API port directly: the daemon binds to 127.0.0.1 by default, and a public node should sit behind a TLS reverse proxy (see Public Node). Open only the proxy's HTTPS port:
# Allow inbound HTTPS to the reverse proxy
New-NetFirewallRule -DisplayName "RougeChain Node (HTTPS)" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow
Docker Alternative
If you prefer Docker, install Docker Desktop for Windows and run:
docker run -d --name rougechain-node -p 127.0.0.1:5100:8900 -v qv-data:/data/rougechain rougechain/node --data-dir /data/rougechain --host 0.0.0.0 --api-port 8900 --mine --peers https://testnet.rougechain.io/api
Troubleshooting
"cargo not found"
Close and reopen PowerShell. Rust adds itself to PATH but it takes a new session to pick up.
Build errors about MSVC
Make sure Visual Studio Build Tools is installed with the C++ desktop workload. Restart your terminal after installing.
"Access denied" or antivirus blocking
- Run PowerShell as Administrator
- Add an antivirus exclusion for the
rougechain-nodedirectory - Windows Defender may flag the newly built binary — allow it through
Node doesn't respond
- Check Windows Firewall isn't blocking the port
- Make sure you're using
http://127.0.0.1:5100(notlocalhost, which may resolve to IPv6)
Updating
cd rougechain-node
git pull
cd core
cargo build --release -p quantum-vault-daemon
# Restart the node
Docker
Run a RougeChain node without installing Rust or any build dependencies.
Requirements
Any VPS or machine with Docker installed. Minimum specs:
- 1 vCPU, 1 GB RAM, 5 GB SSD, 10 Mbps
- Recommended: 2 vCPU, 1–2 GB RAM, 20 GB SSD
- A $5/month VPS is enough for testnet
Quick Start
docker run -d \
--name rougechain-node \
-p 127.0.0.1:5100:8900 \
-v qv-data:/data/rougechain \
rougechain/node \
--data-dir /data/rougechain --host 0.0.0.0 --api-port 8900 \
--mine --peers https://testnet.rougechain.io/api
Inside the container the node listens on port 8900 and stores data in /data/rougechain (the image's volume). Arguments after the image name replace the image's default command (--data-dir /data/rougechain --host 0.0.0.0 --api-port 8900 --mine), so always repeat --data-dir, --host and --api-port as above; otherwise the node binds to 127.0.0.1:5101 inside the container (unreachable), writes outside the volume, and fails the image's health check (which probes 8900). Publishing on 127.0.0.1 keeps the API off the public internet; put a reverse proxy in front if peers need to reach you (see Public Node).
Your node will:
- Sync with the testnet
- Produce blocks (
--mine) whenever it is the designated proposer - Persist chain data to the
qv-dataDocker volume - Serve the REST API on host port
5100
Verify:
curl http://127.0.0.1:5100/api/stats | python3 -m json.tool
docker-compose
For a persistent production setup, clone the repo and use docker-compose:
git clone https://github.com/cyberdreadx/rougechain-node
cd rougechain-node
Optionally create a .env file to override defaults:
API_PORT=5100
QV_PEERS=https://api.rougechain.io/api
QV_CORS_ORIGINS=https://yourdapp.com,https://rougechain.io
The compose file always starts the node from the mainnet genesis (--genesis /etc/rougechain/genesis.json), which sets the chain id to rougechain-mainnet-1 (there is no chain-id setting), so point QV_PEERS at mainnet peers; for another network use docker run with your own flags (see Custom Configuration). It also always passes --mine. API_PORT is the host port mapped to the container's 8900, published on 127.0.0.1 only; set API_BIND=0.0.0.0 to expose it on every interface (prefer a reverse proxy, see Public Node). The optional Prometheus service (--profile monitoring) is likewise published on 127.0.0.1:9090. If QV_CORS_ORIGINS is unset (or *), the node uses its built-in origin list.
Start:
docker compose up -d
View logs:
docker compose logs -f node
Stop:
docker compose down
Building the Image
Build locally instead of pulling from the registry:
docker build -t rougechain/node .
The Dockerfile uses a multi-stage build:
- Builder stage — compiles the Rust daemon in a full Rust image
- Runtime stage — copies the daemon (
rougechain-node) and therougechainCLI, plus the genesis files, into a minimal Debian image
Data Persistence
Chain data is stored at /data/rougechain inside the container (when the node runs with --data-dir /data/rougechain, as the image's default command and the examples here do). Mount a volume there to keep it across restarts:
# Named volume (recommended)
-v qv-data:/data/rougechain
# Host directory
-v /srv/rougechain-data:/data/rougechain
Data includes:
- Block database
- Validator state
- Pool/DEX state
- NFT collections
- Node keys (
node-keys.json)
Custom Configuration
Pass CLI flags after the image name:
docker run -d \
-p 127.0.0.1:5100:8900 \
-v qv-data:/data/rougechain \
rougechain/node \
--data-dir /data/rougechain --host 0.0.0.0 --api-port 8900 \
--mine \
--peers https://testnet.rougechain.io/api \
--chain-id rougechain-devnet-1 \
--block-time-ms 400
Set CORS origins via environment variable:
docker run -d \
-p 127.0.0.1:5100:8900 \
-v qv-data:/data/rougechain \
-e QV_CORS_ORIGINS="https://yourdapp.com" \
rougechain/node \
--data-dir /data/rougechain --host 0.0.0.0 --api-port 8900 \
--mine --peers https://testnet.rougechain.io/api
Becoming a Validator
Once your Docker node is running and synced:
- Fund your node's own key (
/data/rougechain/node-keys.json) with ≥ 10,000 XRGE plus the fee - Stake from that key with the bundled CLI, e.g.
docker exec rougechain-node rougechain --node-keys /data/rougechain/node-keys.json stake 10000(add--network testnetfor testnet). Staking from a browser wallet stakes the wallet's key, not the node's, and the node then never counts as a validator. See Becoming a Validator - Your node then votes on blocks, and proposes whenever it is the designated proposer (the validator with the most stake)
Your node earns:
- 20% of priority tips when it is the designated proposer
- A share of 70% of priority tips in every block, weighted by your stake
- A minimum tip floor of 0.1 XRGE/block is guaranteed from staking reserves
Health Checks
# Node health
curl http://127.0.0.1:5100/api/health
# Network stats (peers, height, mining status)
curl http://127.0.0.1:5100/api/stats
# Validator list
curl http://127.0.0.1:5100/api/validators
Updating
cd rougechain-node
git pull
docker compose build
docker compose up -d
Or if using docker run:
docker build -t rougechain/node .
docker stop rougechain-node
docker rm rougechain-node
docker run -d --name rougechain-node -p 127.0.0.1:5100:8900 -v qv-data:/data/rougechain rougechain/node --data-dir /data/rougechain --host 0.0.0.0 --api-port 8900 --mine --peers https://testnet.rougechain.io/api
The qv-data volume persists across container rebuilds, so your chain data is preserved.
Configuration
All node configuration is done via command-line flags or environment variables.
CLI Options
| Flag | Env Variable | Default | Description |
|---|---|---|---|
--host | - | 127.0.0.1 | Bind address for API/gRPC. For public nodes keep 127.0.0.1 behind a reverse proxy (see Public Node); 0.0.0.0 exposes the API on every interface |
--port | - | 4101 | gRPC port (client services; peers do not use it) |
--api-port | - | 5101 | HTTP API port (also used for all peer traffic) |
--chain-id | - | rougechain-devnet-1 | Chain identifier (ignored when --genesis is given; the genesis file's chain_id wins) |
--genesis | QV_GENESIS | - | Path to a genesis JSON file (e.g. genesis-mainnet.json) |
--block-time-ms | - | 400 | How often the miner checks the mempool (ms). Blocks are only produced when there are transactions |
--mine | - | false | Enable block production |
--node-name | QV_NODE_NAME | - | Human-readable name shown on the network globe |
--data-dir | - | ~/.quantum-vault/core-node | Data storage directory |
--peers | QV_PEERS | - | Comma-separated peer URLs |
--public-url | QV_PUBLIC_URL | - | This node's public URL for peer discovery |
--api-keys | QV_API_KEYS | - | Comma-separated API keys. When set, every non-exempt route (reads included) requires a key; see Running a Node |
--rate-limit-read-per-minute | - | 0 (unlimited) | Per-client limit for GET requests |
--rate-limit-write-per-minute | - | 0 (unlimited) | Per-client limit for all other methods |
--rate-limit-validator | - | 0 (unlimited) | Limit for requests carrying valid signed validator headers |
--rate-limit-peer | - | 0 (unlimited) | Limit for requests from registered peers (matched by IP) |
--rate-limit-per-minute | - | 0 | Deprecated alias: used for both read and write limits when neither is set |
--trust-proxy | QV_TRUST_PROXY | false | Key rate limits by X-Real-IP / rightmost X-Forwarded-For for requests from a loopback peer (a local reverse proxy); other peers are always keyed by socket IP |
--dev | - | false | Dev mode: enables legacy v1 unsigned write endpoints and allows any CORS origin |
| — | QV_CORS_ORIGINS | (built-in list) | Comma-separated origins allowed to call the API from a browser |
| — | QV_FAUCET_ENABLED | false | Enable the faucet endpoints (testnet/dev only — never set on mainnet) |
--require-signed-chain-id | QV_REQUIRE_SIGNED_CHAIN_ID | false | REQUIRE_SIGNED_CHAIN_ID. Refuse signed payloads (/api/v2 transactions, signed mail/messenger/name requests, CLI envelopes) that do not name this node's chain id (chainId; chain_id in a CLI envelope), with CHAIN_ID_REQUIRED. A payload naming another chain id is always refused (CHAIN_ID_MISMATCH), whatever this flag says. Leave off until wallets have shipped the field — see Security |
Browser access (CORS)
By default a node only accepts browser requests from a built-in allowlist (localhost:5173, localhost:4173, and the official rougechain.io / rougee.app origins). A dApp served from any other origin will get a CORS error in the browser (curl is unaffected). To allow your own front-end:
# Allowlist specific origins:
export QV_CORS_ORIGINS="https://mydapp.example.com,http://localhost:3000"
# Or, for local development, allow any origin:
./quantum-vault-daemon --dev --api-port 5100
QV_CORS_ORIGINStakes an explicit comma-separated list — setting it to"*"does not produce a wildcard. To serve a trueAccess-Control-Allow-Origin: *publicly (asapi.rougechain.iodoes), terminate at a reverse proxy (nginx) that strips the node's CORS header and emits*itself.
Examples
Local Development Node
./quantum-vault-daemon --mine --api-port 5100
Syncing Node (No Mining)
./quantum-vault-daemon \
--api-port 5100 \
--peers "https://testnet.rougechain.io/api"
Public Mining Node
./quantum-vault-daemon \
--mine \
--api-port 5100 \
--node-name "MyNode" \
--peers "https://testnet.rougechain.io/api" \
--public-url "https://mynode.example.com"
Keep the default --host 127.0.0.1 and serve --public-url through a reverse proxy with TLS (see Public Node). Once running, visit http://localhost:5100 in your browser to see the built-in node dashboard with live stats, peer list, and block height.
Multiple Peers
./quantum-vault-daemon \
--api-port 5100 \
--peers "https://node1.example.com,https://node2.example.com,https://node3.example.com"
Custom Data Directory
./quantum-vault-daemon \
--mine \
--api-port 5100 \
--data-dir "/var/lib/rougechain"
Environment Variables
You can also use environment variables:
export QV_PEERS="https://testnet.rougechain.io/api"
export QV_PUBLIC_URL="https://mynode.example.com"
export QV_NODE_NAME="MyNode"
export QV_API_KEYS="key1,key2,key3"
./quantum-vault-daemon --mine --api-port 5100
Data Directory Structure
~/.quantum-vault/core-node/
├── node-keys.json # Node / validator identity keypair
├── chain-db/ # Blocks (sled)
├── validators-db/ # Validator state (sled)
├── messenger-db/ # Messenger data (sled)
├── finality-db/ # Finality certificates (sled)
├── …-db/ # Other sled trees (mail, pools, NFTs, tokens, nonces, …)
├── replay_guard.json # Seen-signature replay guard
└── finality-signing-journal # Vote anti-equivocation journal
All databases are sled trees. A legacy chain.jsonl is only read once, to migrate an old data directory into chain-db (it is then renamed to chain.jsonl.bak).
Running with Systemd (Linux)
Create /etc/systemd/system/rougechain.service:
[Unit]
Description=RougeChain Node
After=network.target
[Service]
Type=simple
User=rougechain
ExecStart=/opt/rougechain/quantum-vault-daemon --mine --api-port 5100 --node-name "MyNode" --public-url "https://mynode.example.com" --peers "https://testnet.rougechain.io/api"
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl enable rougechain
sudo systemctl start rougechain
sudo journalctl -u rougechain -f # View logs
Mining (Block Production)
RougeChain uses Proof of Stake, so "mining" refers to block production by validators rather than proof-of-work mining.
Enable Mining
Start your node with the --mine flag:
./quantum-vault-daemon --mine --api-port 5100
This tells the node to try to seal a block whenever it has pending transactions. The miner checks the mempool every --block-time-ms (default 400 ms) and wakes early when a new transaction arrives. Empty blocks are never produced, so blocks only appear when there are transactions, and only the designated proposer seals them (see below).
Requirements
| Requirement | Value |
|---|---|
| Staked XRGE | Minimum 10,000 XRGE |
| Node uptime | Must be online to produce blocks |
| Network sync | Node must be synced to chain tip |
How Block Production Works
- Proposer selection — Since mainnet height 100, each height has exactly one designated proposer: the eligible validator (stake > 0, not jailed) with the most stake, ties going to the lowest raw public-key bytes. It is deterministic (no randomness, no rotation, no fallback). Every other node refuses to seal, and peers reject blocks from any other proposer. (
GET /api/selectionreports the same designated proposer for the next height.) - Block assembly — The designated proposer collects pending transactions from the mempool. Since height 150 it first waits for the commit certificate of the previous block (precommits from validators holding ⅔ of the stake)
- Signing — The block is signed with the validator's ML-DSA-65 key
- Voting — Validators automatically submit prevote and precommit attestations for each block
- Propagation — The signed block is broadcast to all peers via
POST /api/blocks/import - Verification — Receiving nodes verify the signature and block validity before accepting
Block Timing
| Parameter | Default | Flag |
|---|---|---|
| Miner poll interval | 400ms | --block-time-ms |
--block-time-ms is how often the miner checks for pending transactions, not a fixed block cadence: a new transaction wakes the miner immediately, and nothing is produced while the mempool is empty. So it is an upper bound on how long a pending transaction waits before the designated proposer tries to seal it. Mainnet nodes run with the default (the block_time_ms value in the genesis file is not used for timing).
# Check the mempool less often (every 5 seconds)
./quantum-vault-daemon --mine --block-time-ms 5000
Mining with Peers
To mine as part of the network (not solo):
./quantum-vault-daemon \
--mine \
--api-port 5100 \
--peers "https://testnet.rougechain.io/api" \
--public-url "https://mynode.example.com"
The --public-url flag is important — it lets other nodes discover and sync from you.
Monitoring
Check your validator status:
curl https://testnet.rougechain.io/api/validators
Check blocks produced:
curl https://testnet.rougechain.io/api/blocks?limit=10
Rewards
Every staked, non-jailed validator earns a stake-weighted share of the fees in every block; the proposer also gets a 20% share. Fees are credited when the block is applied to state. See Staking Rewards for details.
Troubleshooting
Node not producing blocks
- Ensure
--mineflag is set - Verify you have enough XRGE staked (min 10,000)
- Check that your node is synced:
curl http://127.0.0.1:5100/api/health - Check whether you are the designated proposer:
designated_proposer_nextincurl http://127.0.0.1:5100/api/stats. If it is another key, the log linenot the designated proposer … — not sealingis expected - If the log says
waiting for the commit certificate of block …, validators holding ⅔ of the stake have not yet voted for the tip
Blocks not propagating
- Ensure
--public-urlis set and accessible from the internet - Check firewall rules for your API port
- Verify peer connections:
curl http://127.0.0.1:5100/api/peers
Producing blocks but no rewards
- Confirm your staking transaction was included in a block
- Check validator status via the API
Signed releases
Node releases are published as a prebuilt quantum-vault-daemon binary and the rougechain CLI
(Linux x86_64), described by a signed release manifest. The installer, the
automatic updater and anyone verifying by hand check the same thing:
the manifest's signatures, then each file's sha256 and size against the manifest.
Where releases are published
| What | Primary | Mirror |
|---|---|---|
| Manifest + signatures | https://api.rougechain.io/releases/manifest-<network>.json (+ .ed25519.sig, .mldsa65.sig) | https://raw.githubusercontent.com/cyberdreadx/rougechain-node/main/releases/ |
| Node binary | binary.url in the manifest (https://api.rougechain.io/releases/<name>) | binary.mirrors: asset of the GitHub release v<version> |
rougechain CLI | cli.url in the manifest | cli.mirrors: asset of the GitHub release v<version> |
| Genesis file | genesis.url in the manifest | genesis.mirrors: GitHub release asset, and core/daemon/ in the public repository |
| Installer / updater script (releases from 1.6.1) | installer.url in the manifest | installer.mirrors: asset of the GitHub release v<version> |
| Public keys | releases/keys/ in the public repository | — |
<network> is mainnet or testnet. GitHub release assets are at
https://github.com/cyberdreadx/rougechain-node/releases/download/v<version>/<name> (GitHub answers
with a redirect to the file). The manifest and its signatures are mirrored as files of the
repository, not as release assets. A mirror cannot weaken anything: a file from any source is
used only if the manifest's signature verifies and the file's sha256 matches the manifest.
The manifest (schema: 1)
{
"schema": 1,
"network": "mainnet",
"chain_id": "rougechain-mainnet-1",
"version": "1.6.0",
"released": "2026-10-01",
"source_commit": "03613ef",
"public_commit": "e048bf1",
"binary": {
"name": "quantum-vault-daemon-mint-royalty-03613ef",
"url": "https://api.rougechain.io/releases/quantum-vault-daemon-mint-royalty-03613ef",
"mirrors": ["https://github.com/cyberdreadx/rougechain-node/releases/download/v1.6.0/quantum-vault-daemon-mint-royalty-03613ef"],
"sha256": "673bf7b1a469b7f9937b17228faab1e81ae48fb2a90aa32b2fd1b7fcc9cc2984",
"size": 28463856
},
"cli": { "name": "rougechain-03613ef", "url": "…", "mirrors": ["…"], "sha256": "…", "size": 0 },
"genesis": { "name": "genesis-mainnet.json", "url": "…", "mirrors": ["…"], "sha256": "…", "size": 4460 },
"installer": { "name": "install-validator-2.1.0-1a2b3c4d.sh", "url": "…", "mirrors": ["…"], "sha256": "…", "size": 0 },
"mandatory": true,
"upgrade_before_height": 235,
"activations": [ { "name": "token_minting", "height": 235 }, { "name": "contract_nft_royalty", "height": 235 } ],
"notes_url": "https://docs.rougechain.io/running-a-node/mandatory-upgrade-2026-10.html",
"min_installer_version": "2.0.0"
}
| Field | Type | Meaning |
|---|---|---|
schema | integer | Manifest format version. 1. |
network | string | mainnet or testnet. |
chain_id | string | rougechain-mainnet-1 or rougechain-devnet-1; must match network. |
version | string | Node release version, MAJOR.MINOR.PATCH. Increases with every release of a network. MINOR = a release with a new consensus activation; PATCH = everything else. |
released | string | ISO date (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ). |
source_commit | string | Git commit the binary was built from. |
public_commit | string or null | The commit of the public rougechain-node repository that contains that source. |
binary | object | The node (quantum-vault-daemon): name, url (primary), mirrors (array of URLs, tried in order after url), sha256 (lowercase hex), size (bytes). |
cli | object or null | Same shape as binary, for the rougechain CLI built from the same commit. null when a release ships no CLI. |
genesis | object or null | Same shape as binary, for the genesis file the node is started with. null when the network runs on default parameters (testnet). |
installer | object, optional | Same shape as binary, for install-validator.sh — the script nodes keep as their updater. A node replaces its updater only with the file named here. The field is either absent or a complete entry (never null). The 1.6.0 manifests were signed before it existed and do not have it. |
mandatory | boolean | true if every node of the network must install this release. |
upgrade_before_height | integer or null | Install before this block height (the first activation the release introduces). |
activations | array | The network's upgrade schedule carried by this binary: { "name", "height" }. Names are the upgrade_schedule fields of GET /api/stats. |
notes_url | string or null | Release / upgrade notes. |
min_installer_version | string | Oldest install-validator.sh that can install this release. |
The example is abridged (…).
All URLs are https://. Unknown fields are not allowed in schema 1; installer is the one
optional field (an addition that old installers ignore, so the schema number did not change). The current manifests (release 1.6.11) list the
full schedule in activations (mainnet: 49, 90, 100, 150, 150, 160, 170, 190, 235, 235, 245, 260; testnet
also carries validator_retirement at 1240 and, from 1.6.11, shield_v2 at 1480) — see the upgrade schedule. The
example above is the 1.6.0 manifest, kept because it shows upgrade_before_height and mandatory
set; the 1.6.11 mainnet manifest has mandatory: false and upgrade_before_height: null (no mainnet
rule changes), and the 1.6.11 testnet manifest has mandatory: true and upgrade_before_height: 1480
(SHIELD_V2 activates there).
Signatures
The signed object is the manifest file itself: both signatures are over its exact bytes (no canonicalisation — a manifest that is re-formatted no longer verifies).
| File | Algorithm | Encoding |
|---|---|---|
manifest-<network>.json.ed25519.sig | Ed25519 (RFC 8032) | base64 of the raw 64-byte signature |
manifest-<network>.json.mldsa65.sig | ML-DSA-65 (FIPS 204), empty context | base64 of the raw 3309-byte signature |
A release must carry both, and the installer and updater refuse a manifest without a
well-formed copy of each. They always verify the Ed25519 signature (stock OpenSSL 3.0 of Ubuntu
22.04/24.04/26.04 and Debian 12). They verify the ML-DSA-65 signature with the installed rougechain CLI
— rougechain release verify, CLI ≥ 1.2.0 — whenever one is installed, and refuse the release if it
does not verify; details in Automatic updates — trust model. The
release tooling and CI check both, and OpenSSL 3.5 or newer can check ML-DSA-65 by hand.
Public keys: releases/keys/release-ed25519.pub.pem (PEM) and releases/keys/release-mldsa65.pub
(hex of the raw 1952-byte key). The private keys are held offline by the release owner; they are
not on the release server.
| Key | Fingerprint (SHA-256 of the raw public key) |
|---|---|
| Ed25519 | 79bf33d554ee6d39e5819d94a04d941da9b0e4aeef42eb1106289c5030d4a040 |
| ML-DSA-65 | ac4497980205f2ccbd810048bcd5ffd819d82400730cfbead41157cc56781b3c |
Verify a release by hand
NET=mainnet
BASE=https://api.rougechain.io/releases
KEYS=https://raw.githubusercontent.com/cyberdreadx/rougechain-node/main/releases/keys
curl -fsSLO $BASE/manifest-$NET.json
curl -fsSLO $BASE/manifest-$NET.json.ed25519.sig
curl -fsSLO $KEYS/release-ed25519.pub.pem
# 1. The key is the one you expect (compare with the fingerprint table above)
openssl pkey -pubin -in release-ed25519.pub.pem -outform DER | tail -c 32 | sha256sum
# 2. The manifest is signed by that key → "Signature Verified Successfully"
base64 -d manifest-$NET.json.ed25519.sig > manifest.sig.bin
openssl pkeyutl -verify -pubin -inkey release-ed25519.pub.pem -rawin \
-in manifest-$NET.json -sigfile manifest.sig.bin
# 3. The files are the ones the manifest describes → "OK" and "size OK" for each
for f in binary cli; do # drop "cli" if the manifest has "cli": null
name=$(jq -r .$f.name manifest-$NET.json)
curl -fL -o "$name" "$(jq -r .$f.url manifest-$NET.json)" # or one of .$f.mirrors[]
echo "$(jq -r .$f.sha256 manifest-$NET.json) $name" | sha256sum -c
test "$(stat -c %s "$name")" = "$(jq -r .$f.size manifest-$NET.json)" && echo "size OK"
done
Get the public key from the Git repository (or from a copy you saved earlier), not from the same server that serves the manifest.
ML-DSA-65 signature — with the rougechain CLI (≥ 1.2.0; offline, no wallet or node needed):
curl -fsSLO $BASE/manifest-$NET.json.mldsa65.sig
curl -fsSLO $KEYS/release-mldsa65.pub
rougechain release verify --manifest manifest-$NET.json --sig manifest-$NET.json.mldsa65.sig --pubkey release-mldsa65.pub
# VERIFIED: ML-DSA-65 signature of manifest-mainnet.json (release key ac449798…) exit 0
# exit 1 = the signature does not verify, exit 2 = a file is missing or malformed
The fingerprint it prints is the SHA-256 of the raw public key — compare it with the table above.
or with OpenSSL ≥ 3.5 (e.g. Debian 13):
curl -fsSLO $BASE/manifest-$NET.json.mldsa65.sig
curl -fsSLO $KEYS/release-mldsa65.pub
tr -d '\n' < release-mldsa65.pub | xxd -r -p | sha256sum # fingerprint
# raw key → SubjectPublicKeyInfo (the hex prefix is the fixed ML-DSA-65 header)
{ printf '308207b2300b0609608648016503040312038207a100'; tr -d '\n' < release-mldsa65.pub; } | xxd -r -p > mldsa65.der
openssl pkey -pubin -inform DER -in mldsa65.der -out mldsa65.pem
base64 -d manifest-$NET.json.mldsa65.sig > manifest.mldsa.bin
openssl pkeyutl -verify -pubin -inkey mldsa65.pem -in manifest-$NET.json -sigfile manifest.mldsa.bin
or, on any machine with Node.js ≥ 20, both signatures and the binary in one step from a checkout of the repository:
cd scripts/release && npm ci
node verify-manifest.mjs --binary /path/to/the/binary --cli /path/to/rougechain ../../releases/manifest-mainnet.json # exit 0 = VERIFIED
What the installer does
scripts/install-validator.sh
(Ubuntu 22.04 / 24.04 / 26.04, Debian 12; x86_64; run as root):
- downloads
manifest-<network>.json, its.ed25519.sigand its.mldsa65.sigfrom every source (the primary and the mirror); - verifies each copy's Ed25519 signature with
opensslagainst the release key embedded in the script — it never downloads a key — and, when arougechainCLI withrelease verifyis already installed, its ML-DSA-65 signature against the embedded ML-DSA-65 key. It uses the newest release that verifies (a stale mirror cannot hold a node back) and stops if no source provides one; - checks the manifest is for the requested network, and is not older than the release already
installed (
ALLOW_DOWNGRADE=1overrides); - downloads the node binary, the
rougechainCLI (when the manifest has one), the genesis file and the updater script (when the manifest names one) — primary URL, then mirrors, following redirects — and requires size and sha256 of each to match the signed manifest; - installs, as described below, and keeps the previous binaries as
<file>.prev. If the ML-DSA-65 signature could not be checked in step 2 (no CLI yet), it is checked now with the CLI just installed, before the service is started; a release that fails is removed again; - sets up automatic updates: the updater,
rougechain-update, a systemd timer and/etc/rougechain/<network>/update.conf(AUTO_UPDATE=0to opt out).
It never overwrites node-keys.json or chain data. --dry-run performs steps 1–4 and changes
nothing.
| mainnet | testnet | |
|---|---|---|
| Service | rougechain-validator | rougechain-validator-testnet |
| Node binary | /usr/local/bin/quantum-vault-daemon | /usr/local/bin/quantum-vault-daemon-testnet |
| CLI | /usr/local/bin/rougechain | /usr/local/bin/rougechain-testnet |
Data + node-keys.json (DATA_DIR) | /var/lib/rougechain/mainnet | /var/lib/rougechain/testnet |
Genesis, installed manifest, optional node.env, update.conf | /etc/rougechain/mainnet/ | /etc/rougechain/testnet/ |
| Auto-update timer | rougechain-update.timer | rougechain-update-testnet.timer |
API (HOST:API_PORT) | 127.0.0.1:5100 | 127.0.0.1:5101 |
gRPC port (P2P_PORT) | 4100 | 4101 |
Peer (PEERS) | https://api.rougechain.io/api | https://testnet.rougechain.io/api |
The service runs as the system user rougechain (no login shell). The data directory is 0700,
node-keys.json is 0600, both owned by that user. The systemd unit lets the node write only to
its data directory (ProtectSystem=strict, ReadWritePaths=<data dir>) and removes privileges
(NoNewPrivileges, empty capability set, PrivateTmp, PrivateDevices, ProtectHome,
ProtectKernel*, RestrictAddressFamilies, UMask=0077). --mine is added only with
VALIDATOR=1. Extra QV_* settings go in /etc/rougechain/<network>/node.env; the unit file
itself is rewritten on every run.
The CLI and the node key. node-keys.json can be read only by the rougechain user (and
root), so run the CLI as that user:
sudo -u rougechain rougechain --node-keys /var/lib/rougechain/mainnet/node-keys.json whoami # also: stake 10000, validator-status
Without sudo, as root: runuser -u rougechain -- rougechain …. On testnet the commands are
rougechain-testnet --network testnet --node-keys /var/lib/rougechain/testnet/node-keys.json ….
The CLI submits to the network's public node (https://api.rougechain.io, or
https://testnet.rougechain.io with --network testnet) through the signed /api/v2 routes; see
CLI Wallet.
Settings: NETWORK, NODE_NAME, PUBLIC_URL, VALIDATOR, DATA_DIR, API_PORT, P2P_PORT,
HOST, PEERS, AUTO_UPDATE, NO_START, ALLOW_DOWNGRADE, REPLACE_LEGACY_UNIT,
RELEASE_BASE_URLS — see the header of the script.
Upgrading
Automatic — by default the node upgrades itself: see Automatic updates
(what it checks, how to make it notify-only, pin a version or turn it off, and what happens when an
update fails). rougechain-update status shows where a node stands.
By hand — re-run the installer; this always works, with auto-update on or off. It verifies the
new signed release, replaces the binary and the CLI, and restarts the
service (with NO_START=1 it installs without restarting, so you can restart at a time you
choose: systemctl restart rougechain-validator). Unlike the updater, a manual run does not
health-check the node afterwards or roll back. To go back to the previous binary:
sudo systemctl stop rougechain-validator
sudo cp -p /usr/local/bin/quantum-vault-daemon.prev /usr/local/bin/quantum-vault-daemon
sudo systemctl start rougechain-validator
A node installed with installer 2.0.0 (before auto-update existed) has no updater: re-run the installer once to get it. Whether that run can install the updater depends on the release — see where the updater comes from.
Nodes installed from source
A node set up with the earlier source-build installer (repository in ~/rougechain, data in
~/.quantum-vault/mainnet, unit rougechain-validator running as your login user) is not changed
silently: the installer stops when it finds a unit it did not write. To move such a node to signed
releases, keep its whole data directory — it holds node-keys.json (your validator identity) and
the vote journal:
sudo systemctl stop rougechain-validator
cp -a ~/.quantum-vault/mainnet ~/mainnet.backup-$(date +%Y%m%d) # backup, incl. node-keys.json
sudo useradd --system --user-group --home-dir /var/lib/rougechain --no-create-home --shell /usr/sbin/nologin rougechain
sudo mkdir -p /var/lib/rougechain
sudo mv ~/.quantum-vault/mainnet /var/lib/rougechain/mainnet
sudo chown -R rougechain:rougechain /var/lib/rougechain
curl -sSL https://raw.githubusercontent.com/cyberdreadx/rougechain-node/main/scripts/install-validator.sh \
| sudo REPLACE_LEGACY_UNIT=1 VALIDATOR=1 PUBLIC_URL=https://node.example.com bash
Use the VALIDATOR / PUBLIC_URL / NODE_NAME values your node ran with (VALIDATOR=1 only if it
ran with --mine). The old unit is kept as rougechain-validator.service.legacy. With
REPLACE_LEGACY_UNIT=1 the installer refuses to continue if there is no node-keys.json in the
data directory, so an existing node never ends up with a new identity. Never run two nodes with
the same node-keys.json.
For the release owner
The release procedure (build, make-manifest, sign on your own machine, verify, publish, sync) is
in scripts/release/README.md.
Automatic updates
A node installed with install-validator.sh keeps itself up to date: a systemd
timer looks for a newer signed release every hour, installs it, restarts the node, checks that
it is healthy, and puts the previous release back if it is not.
- It installs only signed releases — a manifest signed by the release keys, and files whose
sha256 matches that manifest. It never builds anything and never runs code from
main. - You stay in control:
MODE=notifyonly tells you about a release,PIN_VERSIONholds the node on one release,MODE=off(orAUTO_UPDATE=0at install time) turns it off.
rougechain-update status # installed / newest release, last result, next check
sudo rougechain-update check # look now; change nothing
sudo rougechain-update run --now # install a newer release now (no waiting)
journalctl -t rougechain-update # everything the updater did, and why
On testnet add --network testnet (timer rougechain-update-testnet.timer).
What is installed
/usr/local/bin/rougechain-update | the command (a two-line wrapper) |
/usr/local/lib/rougechain/install-validator.sh | the updater: the installer script itself, run as install-validator.sh updater … — one code path for installing and updating |
/etc/systemd/system/rougechain-update.timer | hourly, at a minute that is fixed per host (RandomizedDelaySec=30min, FixedRandomDelay), Persistent |
/etc/systemd/system/rougechain-update.service | Type=oneshot, runs rougechain-update run --network mainnet as root |
/etc/rougechain/<network>/update.conf | your settings (below) |
/var/lib/rougechain-updater/<network>/state | what the updater remembers: last check, pending release, failed releases. Owned by root — the node's user cannot write it |
The updater runs as root because it replaces files in /usr/local/bin and restarts the service.
What it downloads is data to it: manifests and API answers are read with jq and every value is
validated before use; nothing downloaded is executed before its sha256 has matched a signed
manifest; the ML-DSA-65 verifier and the --version probe of a new binary run as the unprivileged
rougechain user. Release sources must be https://.
Trust model
The trust anchor is the pair of release keys (Ed25519 and ML-DSA-65; fingerprints in Signed releases). Both public keys are embedded in the installer/updater script. The private keys are offline on the release owner's machine; the servers that host releases do not have them.
| Step | Check |
|---|---|
| Manifest | Ed25519 signature over the manifest file, verified with OpenSSL against the embedded key. Always. |
| Manifest | ML-DSA-65 signature, verified with the installed rougechain release verify (CLI ≥ 1.2.0) against the embedded key. Whenever that CLI is installed; if it fails, the release is refused. The signature file must be present and well-formed in any case. |
| Node binary, CLI, genesis | size and sha256 from the signed manifest |
| The updater itself | replaced only by the script whose sha256 is in a signed manifest (the optional installer entry) — never by an older one |
| Version | must be higher than the installed one: an old signed manifest cannot be replayed, a stale mirror cannot roll a node back |
A release server or a mirror that is compromised can therefore withhold a release, but it cannot make a node install anything the release owner did not sign.
ML-DSA-65 on a node whose CLI cannot verify it yet. The CLI shipped with node 1.6.0 has no
release verify. Such a node updates on the Ed25519 signature alone; as soon as the new release's
CLI is in place — and before the node is restarted — the updater verifies the ML-DSA-65
signature with it and undoes the installation if it does not verify. That second check uses a
program delivered by the release being checked, so it catches mistakes, not an attacker who can
forge Ed25519. From the first release whose CLI has release verify, every later manifest is
verified with a CLI that an earlier verified release installed, before anything is downloaded.
Where the updater comes from (bootstrap)
The updater is a root program that runs unattended, so the bytes that become it must have a known origin. There are exactly three cases:
- The release names it. Releases from 1.6.1 on carry an
installerentry in the signed manifest (name, URL, sha256, size). The installer downloads that file, checks the sha256, and installs it as the updater — whichever way the installer itself was started. This chains to the release keys. - The release does not name it (1.6.0), and you ran the installer from a file
(
sudo bash install-validator.sh). That file becomes the updater. It is trusted exactly as much as you already trusted it by running it as root — it is not covered by a release signature.rougechain-update statussays so (from: installer file run by the operator). The first update to a release that names an updater replaces it with the signed one. - The release does not name it, and the installer was piped (
curl … | sudo bash). A piped script cannot read its own bytes, and downloading "the installer" a second time could yield different bytes that nobody verified. No updater is installed; the installer says so and tells you what to do: re-run the same command once a release with a signed updater is out, or download the installer to a file and run that.
Nodes installed before auto-update existed (installer 2.0.0) have no updater either: re-run the installer once.
What a run does
- Ask every source (
api.rougechain.io/releasesand the GitHub mirror, orRELEASE_BASE_URLS) formanifest-<network>.jsonand both signature files. Verify each copy. - Take the newest valid one. A source that is down, stale, or serving something that does not verify is reported and ignored. A release that is not newer than the installed one is ignored.
- Decide (
MODE,PIN_VERSION, releases that failed before, the delay window — below). - If the release names a newer updater: verify it, replace the updater, continue as the new one.
- Download the binary and the CLI (primary URL, then mirrors) and check size + sha256. Nothing on the node has changed up to here; a failure is just retried at the next run.
- On a validator: wait if it is about to propose a block (below).
- Swap the files in. The previous binary and CLI stay as
<file>.prev. - Restart the service — only if the node binary changed and the service was running.
- Health check. If it fails: roll back.
The unit file, node-keys.json, chain data and node.env are never touched. A release that
changes the genesis file is refused (that is a different chain; do it by hand).
When (timing)
| Release | When it is installed |
|---|---|
Optional ("mandatory": false) | after a delay that is random but fixed per host, up to OPTIONAL_DELAY_MAX_SECS (6 h) after the node first saw the release — so the network's nodes do not all restart together |
| Mandatory, upgrade height far away | within MANDATORY_DELAY_MAX_SECS (10 min) of first seeing it, again at a per-host offset |
Mandatory, chain within DEADLINE_MARGIN_BLOCKS (10) of upgrade_before_height, or already past it | immediately: no delay, no proposer deferral |
rougechain-update run --now skips the delay. rougechain-update status shows a pending release
and the time it is scheduled for.
Validators. If the node produces blocks (--mine), is the designated proposer of the next
block (designated_proposer_next in /api/stats is its own key) and has pending transactions
(rougechain_mempool_size in /metrics), it is about to produce a block. The updater waits —
at most PROPOSER_DEFER_MAX_SECS (5 min), then restarts anyway — unless the upgrade height is
near. A restart takes a few seconds; on a network with one designated proposer that is a few
seconds without blocks.
The health check
After the restart the updater requires, using only the node's public API:
GET /api/healthanswers withinHEALTH_START_TIMEOUT_SECS(2 min);/api/healthand/api/statsreport the chain id of the release;/api/stats→upgrade_schedulehas every activation of the manifest at the manifest's height (the one exception iscanonical_ledger_fork, which nodes do not report);- within
HEALTH_DEADLINE_SECS(5 min) the node agrees with a reference node and is not behind it. Reference nodes are the node's own--peersplusREFERENCE_URLS.- Agrees: at the lower of the two tips,
c = min(local height, reference height), both nodes have blockc— its hash must be the same (GET /api/block/c). When both are at the same height, the state roots in/api/statsmust be equal too. A difference has to be seen twice in a row to count. - Not behind:
reference height − local height ≤ HEALTH_MAX_LAG_BLOCKS(2). A node that was still syncing before the update passes if it has advanced since. - An idle chain (RougeChain produces blocks only when there are transactions) needs nothing special: both nodes simply sit at the same height, and block and state root are compared there.
- Agrees: at the lower of the two tips,
A reference node that cannot be reached is not a reason to roll back. If no reference could be
compared with for the whole deadline, the node passes on checks 1–3 plus "its height did not go
backwards", and the result is logged as UNVERIFIED against a reference node. A reference node
that reports another chain id is ignored. If two references disagree with each other, agreeing
with one of them is enough (and the disagreement is logged).
Rollback
When a check fails — or a file cannot be written, or the new binary does not even run
(--version), or the ML-DSA-65 signature turns out not to verify — the updater:
- puts the previous binary, CLI and install record back (the failed files are kept as
<file>.failed); - restarts the service and waits for the API;
- records the release as failed — it is not tried again at the next run (no restart loop); a newer release is tried;
- exits non-zero and logs
UPDATE FAILED/ROLLING BACK/ROLLBACK:lines with the reason.
rougechain-update run exit code | Meaning |
|---|---|
0 | nothing to do, installed, scheduled for later, or notify-only |
1 | could not check (no source reachable, nothing validly signed) or refused (settings, genesis change, …). Nothing was changed |
3 | the update failed and the previous release was restored |
4 | …and the node did not answer after the rollback, or a file could not be put back — look at it now (journalctl -u rougechain-validator -n 100) |
After fixing the cause: sudo rougechain-update run --retry-failed. The systemd unit shows the
failure (systemctl status rougechain-update), and rougechain-update status names the failed
release and the reason.
A rollback restores programs, not data. If a release ever changes the on-disk format in a way the previous binary cannot read, its notes will say so.
Settings: /etc/rougechain/<network>/update.conf
Plain KEY=VALUE lines; the file is read, never executed. Written once by the installer — your
changes survive re-runs and updates. Something the updater does not understand is reported, and
the safe reading is used (a bad MODE or PIN_VERSION installs nothing).
| Setting | Default | |
|---|---|---|
MODE | auto | auto install signed releases · notify only report them · off do nothing |
PIN_VERSION | empty | stay on exactly this release (e.g. 1.6.1); nothing newer is installed while it is set, and the updater says so at every run |
OPTIONAL_DELAY_MAX_SECS | 21600 | upper bound of the per-host delay for optional releases |
MANDATORY_DELAY_MAX_SECS | 600 | …for mandatory releases |
DEADLINE_MARGIN_BLOCKS | 10 | this close to upgrade_before_height: install immediately |
PROPOSER_DEFER_MAX_SECS | 300 | longest wait for a proposing validator (0 = never wait) |
HEALTH_START_TIMEOUT_SECS | 120 | time for the API to answer after a restart |
HEALTH_DEADLINE_SECS | 300 | time to agree with a reference node |
HEALTH_MAX_LAG_BLOCKS | 2 | allowed distance behind the reference |
REFERENCE_URLS | empty | extra reference nodes, e.g. https://node.example.com/api (space-separated) |
RELEASE_BASE_URLS | empty | where manifests are fetched (space-separated). Empty = https://api.rougechain.io/releases and the GitHub mirror |
rougechain-update status:
RougeChain auto-update — mainnet
mode auto (/etc/rougechain/mainnet/update.conf)
pinned version none
installed release 1.6.1
latest seen 1.6.1
last check 2026-10-02 12:17:03 UTC
last result up to date (1.6.1)
last update 2026-10-02 09:17:41 UTC — 1.6.0 -> 1.6.1: healthy: agrees with https://api.rougechain.io/api (…)
failed releases none
timer rougechain-update.timer: enabled
next check Fri 2026-10-02 13:17:00 UTC
updater v2.1.0 (/usr/local/lib/rougechain/install-validator.sh; from: signed release 1.6.1)
Turning it off, pinning, removing
# notify only: releases are reported in the journal and in `status`, nothing is installed
sudo sed -i 's/^MODE=.*/MODE=notify/' /etc/rougechain/mainnet/update.conf
# off — either of
sudo sed -i 's/^MODE=.*/MODE=off/' /etc/rougechain/mainnet/update.conf
sudo systemctl disable --now rougechain-update.timer
# at install time (also on a re-run): AUTO_UPDATE=0 sets MODE=off and disables the timer
curl -sSL https://raw.githubusercontent.com/cyberdreadx/rougechain-node/main/scripts/install-validator.sh | sudo AUTO_UPDATE=0 bash
# back on
sudo sed -i 's/^MODE=.*/MODE=auto/' /etc/rougechain/mainnet/update.conf
sudo systemctl enable --now rougechain-update.timer
# remove the updater completely (the node keeps running; upgrade by re-running the installer
# with AUTO_UPDATE=0 — a re-run without it would set the timer up again unless update.conf says MODE=off)
sudo systemctl disable --now rougechain-update.timer
sudo sed -i 's/^MODE=.*/MODE=off/' /etc/rougechain/mainnet/update.conf
sudo rm -f /etc/systemd/system/rougechain-update.timer /etc/systemd/system/rougechain-update.service \
/usr/local/bin/rougechain-update && sudo systemctl daemon-reload
Re-running the installer without AUTO_UPDATE keeps your update.conf as it is — an opt-out
stays an opt-out. With auto-update off (or pinned) you are responsible for installing mandatory
releases before their upgrade height.
At a mandatory upgrade (fork)
A mandatory release has "mandatory": true and an upgrade_before_height. Every node must run it
before the chain reaches that height; a node that does not stops following the chain there.
MODE=auto: the node installs it within about an hour of publication (the timer) plus at most 10 minutes, and immediately once the chain is within 10 blocks of the height.MODE=notify/PIN_VERSION/ a release that failed on this node: the updater does not install it and repeats, at every run, that a mandatory release is waiting and at which height. Watch the journal, orrougechain-update status(latest seen … (mandatory, before block N)).- If the update fails its health check before the upgrade height, the node goes back to the
previous release and keeps working until that height — you have until then to find out why
(
journalctl -t rougechain-update). - Whatever the mode, the check to make yourself before the height is the one in each upgrade note:
/api/stats→upgrade_scheduleshows the new activation.
Not covered
- Nodes built from source, Docker nodes and Windows nodes: upgrade them as before.
- Changes to the systemd unit or to installer settings (
VALIDATOR,PUBLIC_URL, ports): re-run the installer. - The old git-pull "auto-deploy" script is retired: it built and ran whatever was on
main.
Network upgrade schedule
Each protocol upgrade turns on at a block height. Mainnet and testnet have separate schedules:
testnet ran old software through mainnet's upgrade heights, so it upgrades at its own, later heights.
The node picks the schedule from its chain id at startup, prints it in the log ([upgrades] …) and
reports it in GET /api/stats as upgrade_schedule. Source: core/daemon/src/upgrades.rs.
| Upgrade | Mainnet (rougechain-mainnet-1) | Testnet (rougechain-devnet-1) |
|---|---|---|
| Transaction integrity (tx uniqueness + signed-payload binding) | 90 | 1200 |
| Proposer selection, Release 1 | 100 | 1250 |
| Verified BFT finality (FINALITY_V2) | 150 | 1250 |
| Player-signed contracts (GAME_READY) | 150 | 1200 |
| Tokens, NFTs, randomness, multi-hop, state root v2 (GAME_READY 2) | 160 | 1200 |
host_block_hash commit-then-settle (GAME_READY 3) | 170 | 1200 |
Payable contract calls (attach) | 190 | 1200 |
| Mintable custom tokens (TOKEN_MINTING) | 235 | 1360 |
| Contracts read NFT royalty (CONTRACT_NFT_ROYALTY) | 235 | 1360 |
| Monetary-integrity rule (MONETARY_INTEGRITY) | 245 | 1390 |
| Signatures commit to the chain id (CHAIN_ID_BINDING) | not scheduled | not scheduled |
| Contracts read the chain id (CONTRACT_CHAIN_ID) | not scheduled | not scheduled |
| Pool transactions apply all-or-nothing (AMM_INTEGRITY) | 260 | 1440 |
| Shielded pool V2 (SHIELD_V2) | not scheduled | 1480 (release 1.6.11) |
Every height in this table has been reached on both networks, except the not scheduled rows
and SHIELD_V2 on testnet (mainnet passed 260 on 2026-10-10; testnet passed 1440 on 2026-10-09).
SHIELD_V2 activates on testnet at block 1480 (scheduled 2026-10-19): every testnet node must
run release 1.6.11 or later before that block (1.6.10 carried the height but was never published). It is not scheduled on mainnet.
The other next consensus changes — restoring the types suspended at 245 and the consensus redesign
decided on 2026-10-06 — have no heights yet and will be announced in advance (see
Status & Roadmap).
Testnet validator cleanup (block 1240, testnet only): 99% of testnet's stake sat on validator
4e094d21…, whose key nobody holds, so it could never sign finality votes. At block 1240 its stake is
returned to its balance and set to zero, and the testnet node's own key (staked beforehand) becomes the
largest validator. The finality validator replay starts from the validator set recorded at that block,
because testnet's earliest blocks predate stored receipts. Proposer selection and finality turn on at
1250. None of this exists on mainnet (validator_retirement is None there).
Rules for changing the schedule
- Mainnet heights are history and never change; a unit test (
mainnet_schedule_is_pinned) fails if they do. - To schedule an upgrade on either network: set its height in
upgrades.rsabove the current tip, rebuild, install on every node of that network before the height, and update this page. - Mainnet and testnet share one binary: a release carries both schedules, so installing it on the primary updates both services — each still follows its own schedule.
Runbook: activating TOKEN_MINTING (together with CONTRACT_NFT_ROYALTY)
Completed: testnet 1360, mainnet 235. Kept as the template for the next scheduled upgrade.
TOKEN_MINTING is a hard fork: from its height, create_token may carry token_mintable /
token_max_supply, mint_tokens is applied under the new rules, and once a mintable token exists the
header state root also commits the mint ledger. An old node drops the two new fields when it reads a
block and applies mint_tokens under the old rules, so from the first mintable token or mint it
computes a different state root and rejects the block. Every node must run the new binary before
the height.
Corrected 2026-10-06: an earlier version of this page said an old node rejects such a block because it "computes a different block
tx_hash". That was wrong: what an old node disagrees on is the state root.
CONTRACT_NFT_ROYALTY is planned for the same height. It is a smaller fork: from its height every
contract call links host_nft_royalty_bps / host_nft_royalty_recipient, so a call to a contract that
imports them succeeds where an old node fails it (an old node then rejects the block). From the same
height an nft_create_collection whose royaltyBps is above 10000 (over 100%) or not an exact
integer is invalid (refused by the API, mempool and producer; a block carrying one is rejected) — an
old node would accept that block, so this too requires the new binary everywhere. No transaction
or state-root format changes. Activating both at one height means one coordinated install. They are
two independent schedule fields — set both to the same H.
- Pick the height — well above the network's current tip (
curl -s localhost:5100/api/stats→network_height), leaving time to install everywhere (testnet first, then mainnet). - Set it in
core/daemon/src/upgrades.rs:token_minting: Some(H)andcontract_nft_royalty: Some(H)inTESTNET(and later inMAINNETvianode::TOKEN_MINTING_ACTIVATION_HEIGHTandnode::CONTRACT_NFT_ROYALTY_ACTIVATION_HEIGHT). Updatemainnet_schedule_is_pinned/testnet_schedule_is_pinnedto the chosen values, and this page's table. - Test in a clean worktree (never in the live build dir):
cargo test -p quantum-vault-daemon -j 2— includestoken_minting_tests, thenft_royalty/nft_marketplacetests ingame_ready_testsand the mainnet history replays —cargo test -p quantum-vault-vm -j 2, andcargo check(the default members;--workspaceis refused on purpose, seecore/Cargo.toml— and so is--all-targetswith the daemon selected, forbuild,checkandclippyalike, which is also what an editor runs by default: it builds the test targets next to the plain binary, the daemon's test dependencies bring in the shielded pool's prover, and the daemon refuses to compile with it; usecargo test -p quantum-vault-daemonfor the tests). - Build and install on every node of that network (primary, node #2, any outside validator)
before
H; restart each. Confirm the startup log line[upgrades] … token minting Some(H), contract NFT royalty Some(H)andGET /api/stats→upgrade_schedule.token_minting == Handupgrade_schedule.contract_nft_royalty == Hon every node. - At/after
H, verify:- every node reports the same
state_rootat the same height (/api/stats); - create a test token with
"mintable": true, "max_supply": …, thenGET /api/token/<SYM>/metadatashowsmintable: trueon every node; - mint once as the creator: balance and
total_mintedmove by exactly the amount on every node; - a mint by another key, a mint above the cap and a mint of a non-mintable token are refused;
- CONTRACT_NFT_ROYALTY:
POST /api/v2/nft/collection/createwith"royaltyBps": 10001is refused (royaltyBps must be an integer between 0 and 10000);10000is accepted. - CONTRACT_NFT_ROYALTY: publish
contracts/nft_marketplace/nft_marketplace.wasm(it can be published beforeH, but its calls only run fromH). On a test collection with a royalty,list, escrow the NFT withnft_transfer, thenbuywith the exact price attached: the royalty recipient gains exactlyfloor(price × bps / 10000)quanta, the seller the rest, the buyer owns the NFT; abuywith the wrong amount fails and is refunded (only gas is charged); - roots still agree after those blocks.
- every node reports the same
- Then enable the SDK / site UI for
mintable/max_supply/ mint, and tell marketplace developers the royalty host functions are live.
Rollback before H is just reinstalling the previous binary. After a mintable token exists (or a
call to a contract importing the royalty functions is included), a node without the upgrade cannot
follow the chain.
Upgrade notice: CHAIN_ID_BINDING and CONTRACT_CHAIN_ID (not scheduled)
What it is. From node release 1.6.4 every wallet signs the chain id into its payloads, and nodes
refuse a payload that names another network (node rule, no fork). CHAIN_ID_BINDING makes the same
property a consensus rule; CONTRACT_CHAIN_ID gives contracts host_get_chain_id. Both are hard forks
with no height on any network (upgrade_schedule.chain_id_binding / .contract_chain_id are
null).
The rule, from height H. A block is invalid if any transaction's signed bytes do not commit to
this chain's id:
| Signature format | Required from H |
|---|---|
/api/v2 signed payload (signed_payload, flat JSON) | the signature verifies over signed_payload only (no fallback to another format), and the JSON has chainId equal to the chain id (exact string) |
rougechain CLI envelope (signed_payload with tx_type + payload) | same, with chain_id in the envelope |
V1 format (no signed_payload; node-signed transactions: faucet, bridge mints, cosigned bridge withdrawals, legacy node routes) | signed over the network-bound encoding "rougechain/tx-v1/chain" ‖ 0x00 ‖ u64_be(len) ‖ chain id ‖ encode_tx_for_signing(tx); the plain V1 and legacy full-struct encodings are refused |
SHIELD_V2 (shield_v2, signer-less *_v2) | unchanged — the body already carries sha256(chain id) and is checked by the SHIELD_V2 rule |
Below H nothing changes: blocks are accepted, executed and stored exactly as by release 1.6.3, and
the network-bound V1 encoding is not accepted (an older node would refuse it). Nodes start signing
their own V1 transactions in the network-bound encoding at H automatically.
What must ship first, in this order.
- Node release 1.6.4 on every node of the network (node rule; harmless to older wallets).
- Wallets that sign
chainId: the site,@rougechain/sdk1.15.0 (and every dApp built on it, after a dependency bump), browser extension 1.9.0, Qwalla 1.3.0,rougechainCLI 1.3.0, the MCP server. Watch the node log / API errors for payloads still arriving without the field. - Operators turn on
REQUIRE_SIGNED_CHAIN_ID(testnet first). Only then is a payload without a network refused everywhere. - Pick
H(well above the tip), setchain_id_binding: Some(H)(and, if wanted at the same time,contract_chain_id: Some(H)) for testnet incore/daemon/src/upgrades.rs— mainnet viachain_binding::CHAIN_ID_BINDING_ACTIVATION_HEIGHT/node::CONTRACT_CHAIN_ID_ACTIVATION_HEIGHT— updatemainnet_schedule_is_pinned/testnet_schedule_is_pinnedand this page, build, and install on every node of that network beforeH. Confirm the startup line[upgrades] … chain id binding Some(H), contract chain id Some(H)andGET /api/stats→upgrade_schedule.chain_id_binding == Hon each node. - Any transaction still in a mempool at
Hwithout a network-bound signature is dropped by the producer and must be signed again.
Upgrade notice: AMM_INTEGRITY (mainnet 260, testnet 1440)
What it is. A hard fork that changes how the four pool transaction types — create_pool,
add_liquidity, remove_liquidity, swap — are executed. It activates on mainnet at height 260
(release 1.6.7; upgrade_schedule.amm_integrity is 260) and has been active on testnet since
height 1440 (release 1.6.6). Mainnet nodes must run release 1.6.7 or later before block 260. Source: core/daemon/src/amm_integrity.rs.
Node release 1.6.4 already keeps pool transactions that cannot take effect out of the blocks a node produces. That protects a network only while every block producer runs it. AMM_INTEGRITY makes the same outcome a rule of the chain, so it no longer depends on who produces the block.
The rule, from height H. A pool transaction either takes its full effect or changes nothing:
no fee is charged, no balance moves, no pool changes. It can never make its block invalid.
| Transaction | Takes effect only if |
|---|---|
swap | the input amount is greater than zero; the two tokens differ; the route (the signed swap_path, or the direct pair when absent) names 2 to 4 tokens, starts at the input token, ends at the output token, and uses no pool twice; every pool on the route exists and returns more than zero; the final amount is at least the signed min_amount_out; the sender holds the input and the fee |
create_pool | the two symbols are well formed and differ (compared without regard to letter case); both amounts are greater than zero; LP tokens would be minted; no pool exists for the pair, in either order or letter case; the sender holds both amounts and the fee |
add_liquidity | the pool exists; both amounts are greater than zero; more than zero LP tokens would be minted; the sender holds both amounts and the fee |
remove_liquidity | the pool exists; the sender holds the LP tokens; both sides return more than zero; the sender holds the fee |
When a transaction takes effect, all of its parts are written together: the sender's debit, the
pools on the route, and the credit. Amounts are computed with checked integer arithmetic; a value
out of range means the transaction has no effect. The fee is counted as collected whenever it is
charged (before H, the fee of a swap that pays out XRGE is taken from the sender but not
distributed).
What does not change. Prices (constant product, 0.3% of the input stays in the pool), the LP
minting and redemption formulas, XRGE pool amounts in whole XRGE, the state root layout, the
/api/v2 request formats, and every block below H, which replays exactly as before.
For wallets and apps. No change is required. Two visible differences from H:
- A swap that misses its
min_amount_outno longer costs the sender anything. BeforeHa node running 1.6.4 already leaves such a swap out of its blocks; fromHthe chain guarantees it. - A transaction that has no effect is still included in a block when a producer includes it, and
the chain does not record a per-transaction result. To learn whether a swap traded, compare
balances or read the pool events (
GET /api/pool/:pool_id/events).
Multi-pool routes are validated by the rule, but nodes do not yet accept a swap_path from the API
(no wallet signs one); that is a later, node-level change.
Not part of this upgrade (candidates for a later one): locking the minimum liquidity of a new
pool, taking only the matching amounts in add_liquidity with a signed minimum of LP tokens,
committing pool reserves in the state root, a deadline on swaps.
Activation.
- Pick
H(well above the tip), setamm_integrity: Some(H)for testnet incore/daemon/src/upgrades.rs— mainnet viaamm_integrity::AMM_INTEGRITY_ACTIVATION_HEIGHT— updatemainnet_schedule_is_pinned/testnet_schedule_is_pinnedand this page, build, and install on every node of that network beforeH. A node without the upgrade cannot follow the chain past the first block at or aboveHwhose pool transactions the two rule sets judge differently. - Confirm the startup line
[upgrades] … AMM integrity Some(H)andGET /api/stats→upgrade_schedule.amm_integrity == Hon each node. - After
H, on testnet: one swap that trades, one swap signed with a minimum it cannot meet (the sender's balances must be unchanged), one liquidity add and one removal.
RougeChain node release 1.6.11: testnet faucet fix; shielded pool V2 on testnet at block 1480 (2026-10)
Required for every testnet node before testnet block 1480. Recommended for mainnet nodes. Mainnet consensus is unchanged: the shielded pool V2 has no activation height on mainnet.
What changed
- Testnet: faucet payouts apply the same on every node. Testnet runs without a genesis file, so
a node decided whether a faucet payout creates new coins by comparing the sender with its own
key. Only the node holding the faucet key applied it as a mint; every other testnet node applied
it as an ordinary transfer and rejected the block on its state root (first seen at testnet block
1467). From 1.6.11 a fixed testnet mint-authority key (the project's testnet node,
41d7fc30…, which signed every testnet faucet payout) decides on every testnet node. The producing node's result is unchanged, so testnet history is unchanged. Mainnet keeps its genesis authority set. - Testnet: the shielded pool V2 (
shield_v2,shielded_transfer_v2,unshield_v2) activates at testnet block 1480. From that block testnet nodes accept, verify and apply these transactions under the rules of the shielded pool V2 specification, and thePOST /api/shield-v2/submitroute added in 1.6.9 starts admitting them. A testnet node on an older release will reject the first block that carries one, so upgrade before block 1480. - Mainnet: no change. The release replays mainnet history to the same state root as 1.6.9.
- Installer 2.1.1 (
install-validator.sh): adds Ubuntu 26.04 to the supported systems. Nodes with auto-update replace their updater with this signed version. No other behaviour changes. quantum-vault-daemon --versionreports1.6.11. TherougechainCLI is unchanged (1.3.0).
Everything in release 1.6.9 is included, and everything planned for 1.6.10 (which was never published).
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | 471567227439993159a169e5ab1c99b9a7373bb81cf7654cf2c5967ad59111b5 |
| Size | 29,531,048 bytes |
| Source | quantum-vault c208989; public rougechain-node |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-testnetfix-c208989 |
| CLI | rougechain 1.3.0, rougechain-290d4a2, sha256 0df5a526a120eeb12ac77a5a86b6b448bbdf80b371c63db0a4c1427ef22f6870 |
| Installer | install-validator-2.1.1-aec42822.sh, sha256 aec428220a10040e6ab73a78acccf47ddf9eb450d0e5e367ab119c1080b8b83f |
Built twice from clean; the two builds were byte-identical. The release is described by manifests
signed with the release keys (Signed releases). The testnet manifest marks the release
mandatory with upgrade_before_height 1480, so testnet nodes with auto-update install it within
minutes of seeing it.
Upgrade
Nodes installed with the one-line installer and auto-update install this release by themselves. Otherwise:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-testnetfix-c208989
echo "471567227439993159a169e5ab1c99b9a7373bb81cf7654cf2c5967ad59111b5 /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-1.6.11" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
Check after the restart: the node follows the chain as before, "$BIN" --version prints
quantum-vault-daemon 1.6.11, and on testnet GET /api/stats shows "shield_v2": 1480 in
upgrade_schedule.
To go back, stop the node, restore the .pre-1.6.11 binary and start it again (on testnet only
before block 1480).
RougeChain node release 1.6.10: shielded pool V2 on testnet at block 1480 (2026-10)
Superseded before publication by release 1.6.11. 1.6.10 was never published to the release server: a second testnet node rejected testnet block 1467 (a faucet payout) on its state root, and 1.6.11 fixes that. Install 1.6.11.
Required for every testnet node before testnet block 1480. Recommended for mainnet nodes. Mainnet consensus is unchanged: the shielded pool V2 has no activation height on mainnet.
What changed
- Testnet: the shielded pool V2 (
shield_v2,shielded_transfer_v2,unshield_v2) activates at testnet block 1480. From that block testnet nodes accept, verify and apply these transactions under the rules of the shielded pool V2 specification, and thePOST /api/shield-v2/submitroute added in 1.6.9 starts admitting them. A testnet node still on 1.6.9 or older will reject the first block that carries one, so upgrade before block 1480. - Mainnet: no change. The release replays mainnet history to the same state root as 1.6.9.
- Installer 2.1.1 (
install-validator.sh): adds Ubuntu 26.04 to the supported systems. Nodes with auto-update replace their updater with this signed version. No other behaviour changes. quantum-vault-daemon --versionreports1.6.10. TherougechainCLI is unchanged (1.3.0).
Everything in release 1.6.9 is included.
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | 09042df42c321aac660d07b7191b1dd7ba6389c6c55127730c368719e20722df |
| Size | 29,544,272 bytes |
| Source | quantum-vault 5afe534; public rougechain-node |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-shield1480-5afe534 |
| CLI | rougechain 1.3.0, rougechain-290d4a2, sha256 0df5a526a120eeb12ac77a5a86b6b448bbdf80b371c63db0a4c1427ef22f6870 |
| Installer | install-validator-2.1.1-aec42822.sh, sha256 aec428220a10040e6ab73a78acccf47ddf9eb450d0e5e367ab119c1080b8b83f |
Built twice from clean; the two builds were byte-identical. The release is described by manifests
signed with the release keys (Signed releases). The testnet manifest marks the release
mandatory with upgrade_before_height 1480, so testnet nodes with auto-update install it within
minutes of seeing it.
Upgrade
Nodes installed with the one-line installer and auto-update install this release by themselves. Otherwise:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-shield1480-5afe534
echo "09042df42c321aac660d07b7191b1dd7ba6389c6c55127730c368719e20722df /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-1.6.10" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
Check after the restart: the node follows the chain as before, "$BIN" --version prints
quantum-vault-daemon 1.6.10, and on testnet GET /api/stats shows "shield_v2": 1480 in
upgrade_schedule.
To go back, stop the node, restore the .pre-1.6.10 binary and start it again (on testnet only
before block 1480).
RougeChain node release 1.6.9: shielded pool V2 submit route (2026-10)
Recommended for every node, mainnet and testnet. No consensus rule changes and no new activation height.
What changed
- Adds
POST /api/shield-v2/submit, the route wallets use to send shielded pool V2 transactions (shield_v2,shielded_transfer_v2,unshield_v2). It accepts those three types only and turns everything else away. The shielded pool V2 has no activation height on either network, so until one is set the node refuses every transaction sent there, exactly as before. - A node that accepts such a transaction forwards it to its peers on the same route, so it reaches the block producer whichever node the wallet sent it to.
quantum-vault-daemon --versionreports1.6.9. TherougechainCLI is unchanged (1.3.0).
Everything in release 1.6.8 is included. The chain rules are unchanged; this binary replays mainnet and testnet history to the same state roots as 1.6.8.
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | d7e39beb1e8537182bebbf3f0f0bc94cf86615774970e73f90d763dc9020605f |
| Size | 29,545,824 bytes |
| Source | quantum-vault dc85da7; public rougechain-node |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-shieldsubmit-dc85da7 |
| CLI | rougechain 1.3.0, rougechain-290d4a2, sha256 0df5a526a120eeb12ac77a5a86b6b448bbdf80b371c63db0a4c1427ef22f6870 |
Built twice from clean; the two builds were byte-identical. The release is described by a manifest signed with the release keys (Signed releases).
Upgrade
Nodes installed with the one-line installer and auto-update install this release by themselves. Otherwise:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-shieldsubmit-dc85da7
echo "d7e39beb1e8537182bebbf3f0f0bc94cf86615774970e73f90d763dc9020605f /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-1.6.9" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
Check after the restart: the node follows the chain as before and "$BIN" --version prints
quantum-vault-daemon 1.6.9.
To go back, stop the node, restore the .pre-1.6.9 binary and start it again.
RougeChain node release 1.6.8 — NFT mint stability fix (2026-10)
Recommended for every node, mainnet and testnet. No consensus rule changes and no new activation height.
What changed
- Fixes a bug that could make a node stop producing and importing blocks, and stop answering its API, while it processed certain public NFT mints. A node that hit it stayed stuck until restarted, and would get stuck again on the same transaction. 1.6.8 processes those mints normally.
- Upgrade every node, including non-producing ones: a node still on an older release can get stuck importing a block that contains such a mint.
quantum-vault-daemon --versionreports1.6.8. TherougechainCLI is unchanged (1.3.0).
Everything in release 1.6.7 is included. The chain rules are unchanged; this binary replays mainnet and testnet history to the same state roots as 1.6.7.
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | b8c11945ef9c94017e2b1cb46472a4855f609b0ef16cb2fa1532d7a7dabf36e2 |
| Size | 29,400,544 bytes |
| Source | quantum-vault 290d4a2; public rougechain-node |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-nftfix-290d4a2 |
| CLI | rougechain 1.3.0, rougechain-290d4a2, sha256 0df5a526a120eeb12ac77a5a86b6b448bbdf80b371c63db0a4c1427ef22f6870 |
Built twice from clean; the two builds were byte-identical. The release is described by a manifest signed with the release keys (Signed releases).
Upgrade
Nodes installed with the one-line installer and auto-update install this release by themselves (node and CLI). Otherwise:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-nftfix-290d4a2
echo "b8c11945ef9c94017e2b1cb46472a4855f609b0ef16cb2fa1532d7a7dabf36e2 /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-1.6.8" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
Check after the restart: the node follows the chain as before and "$BIN" --version prints
quantum-vault-daemon 1.6.8.
To go back, stop the node, restore the .pre-1.6.8 binary and start it again.
RougeChain node release 1.6.7 — AMM_INTEGRITY on mainnet at block 260 (2026-10)
Mandatory for every mainnet node before mainnet block 260. On testnet nothing changes: AMM_INTEGRITY has been active there since block 1440, and a testnet node on this release validates and produces exactly as on 1.6.6.
What changed
- AMM_INTEGRITY activates on mainnet at height 260 (
upgrade_schedule.amm_integrityis260on mainnet and1440on testnet). From that block, pool transactions (create_pool,add_liquidity,remove_liquidity,swap) apply all-or-nothing: one that cannot take effect changes nothing, pays no fee and does not make the block invalid; swap routes are validated; a pair has one pool; amounts use checked integer arithmetic; the fee of a token-to-XRGE swap is counted as collected. See the upgrade schedule. - A mainnet node on an older release cannot follow the chain past the first block at or above 260 whose pool transactions the old and the new rules judge differently. Installing this release and restarting is enough to catch up.
quantum-vault-daemon --versionreports1.6.7. TherougechainCLI is unchanged (1.3.0).
Everything in release 1.6.6 is included. Below block 260 the mainnet rules are unchanged, and this binary replays mainnet history from block 1 to the same state root as 1.6.6.
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | e9b4fbf188e28ac3d963f35b28589442d2062890508c47c0b403bbab74aa1626 |
| Size | 29,405,112 bytes |
| Source | quantum-vault c64c90c; public rougechain-node |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-amm-mainnet-260-c64c90c |
| CLI | rougechain 1.3.0, rougechain-c64c90c, sha256 0df5a526a120eeb12ac77a5a86b6b448bbdf80b371c63db0a4c1427ef22f6870 |
Built twice from clean; the two builds were byte-identical. The release is described by a manifest signed with the release keys (Signed releases).
Upgrade
Nodes installed with the one-line installer and auto-update install this release by themselves (node and CLI). Otherwise:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-amm-mainnet-260-c64c90c
echo "e9b4fbf188e28ac3d963f35b28589442d2062890508c47c0b403bbab74aa1626 /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-1.6.7" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
Check after the restart: the node follows the chain as before, "$BIN" --version prints
quantum-vault-daemon 1.6.7, and curl -s localhost:5100/api/stats | jq .upgrade_schedule.amm_integrity
prints 260 on mainnet and 1440 on testnet.
To go back (mainnet before block 260 only), stop the node, restore the .pre-1.6.7 binary and
start it again.
RougeChain node release 1.6.6 — AMM_INTEGRITY on testnet at block 1440 (2026-10)
Mandatory for every testnet node before testnet block 1440. Recommended on mainnet, where nothing activates: AMM_INTEGRITY has no mainnet height, so a mainnet node on this release validates and produces exactly as on 1.6.4 and 1.6.5.
What changed
- AMM_INTEGRITY activates on testnet at height 1440 (
upgrade_schedule.amm_integrityis1440on testnet andnullon mainnet). From that block, pool transactions (create_pool,add_liquidity,remove_liquidity,swap) apply all-or-nothing: one that cannot take effect changes nothing, pays no fee and does not make the block invalid; swap routes are validated; a pair has one pool; amounts use checked integer arithmetic; the fee of a token-to-XRGE swap is counted as collected. See the upgrade schedule. - A testnet node on an older release cannot follow the chain past the first block at or above 1440 whose pool transactions the old and the new rules judge differently. Installing this release and restarting is enough to catch up.
quantum-vault-daemon --versionreports1.6.6. TherougechainCLI is unchanged (1.3.0).
Everything in release 1.6.5 is included. The mainnet schedule is unchanged, and this binary replays mainnet history from block 1 to the same state root as 1.6.5.
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | bdcfefda3db51fbfc8a100c5c6228c42e8836c96d4bec18dbf82e47d1c59f6db |
| Size | 29,405,144 bytes |
| Source | quantum-vault b7bf14b; public rougechain-node |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-amm-testnet-1440-b7bf14b |
| CLI | rougechain 1.3.0, rougechain-b7bf14b, sha256 0df5a526a120eeb12ac77a5a86b6b448bbdf80b371c63db0a4c1427ef22f6870 |
Built twice from clean; the two builds were byte-identical. The release is described by a manifest signed with the release keys (Signed releases).
Upgrade
Nodes installed with the one-line installer and auto-update install this release by themselves (node and CLI). Otherwise:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-amm-testnet-1440-b7bf14b
echo "bdcfefda3db51fbfc8a100c5c6228c42e8836c96d4bec18dbf82e47d1c59f6db /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-1.6.6" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
Check after the restart: the node follows the chain as before, "$BIN" --version prints
quantum-vault-daemon 1.6.6, and curl -s localhost:5100/api/stats | jq .upgrade_schedule.amm_integrity
prints 1440 on testnet and null on mainnet.
To go back (mainnet, or testnet before block 1440), stop the node, restore the .pre-1.6.6 binary
and start it again.
RougeChain node release 1.6.5 — AMM_INTEGRITY included, not scheduled (2026-10)
Recommended for every node. Nothing activates with this release: it carries the consensus rule AMM_INTEGRITY without an activation height, so a node that installs it validates and produces exactly as release 1.6.4 does, and a node that has not installed it keeps following the chain. Validators should install it now: the rule can only be given a height once every validator runs a release that contains it.
What changed
-
AMM_INTEGRITY is in the node, unscheduled. From its activation height, pool transactions (
create_pool,add_liquidity,remove_liquidity,swap) apply all-or-nothing:- a transaction that cannot take effect changes nothing, pays no fee and does not make the block invalid;
- a swap route must name 2 to 4 tokens, start at the input token, end at the output token, use no pool twice, and every pool on it must exist and pay out;
- a pair has one pool, whatever the order or letter case of the two symbols;
- amounts are computed with checked integer arithmetic;
- the fee of a token-to-XRGE swap is counted as collected.
The rule and what it does not cover are described in the upgrade schedule.
upgrade_schedule.amm_integrityinGET /api/statsisnullon every network. The height will come in a later, announced release — testnet first. -
quantum-vault-daemon --versionreports1.6.5. TherougechainCLI is unchanged (1.3.0), rebuilt from the same commit.
Everything in release 1.6.4 is included; the upgrade schedule is unchanged (mainnet 235 / 235 / 245, testnet 1360 / 1360 / 1390, all active). This binary replays mainnet history from block 1 to the same state root as 1.6.4.
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | d4ac78ef1082a0ff12af71734d67dab1caf9c5e267a7530c2264bd00f2a7da8f |
| Size | 29,404,488 bytes |
| Source | quantum-vault e433489; public rougechain-node |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-amm-integrity-e433489 |
| CLI | rougechain 1.3.0, rougechain-e433489, sha256 0df5a526a120eeb12ac77a5a86b6b448bbdf80b371c63db0a4c1427ef22f6870 |
Built twice from clean; the two builds were byte-identical. The release is described by a manifest signed with the release keys (Signed releases).
Upgrade
Nodes installed with the one-line installer and auto-update install this release by themselves (node and CLI). Otherwise:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-amm-integrity-e433489
echo "d4ac78ef1082a0ff12af71734d67dab1caf9c5e267a7530c2264bd00f2a7da8f /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-1.6.5" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
Check after the restart: the node follows the chain as before, "$BIN" --version prints
quantum-vault-daemon 1.6.5, and curl -s localhost:5100/api/stats | jq .upgrade_schedule shows
amm_integrity as null.
To go back, stop the node, restore the .pre-1.6.5 binary and start it again.
RougeChain node release 1.6.4 — signatures commit to the network (2026-10)
Recommended for every node. Not a consensus change: no activation height, no state change, and a node that has not installed it keeps following the chain. Nodes that serve the public API should install it promptly — it is step 1 of the network-binding rollout.
What changed
-
Signatures commit to the network. Wallets now put the network they sign for into the signed bytes (
chainIdin/api/v2payloads and in signed mail, messenger, name and vote requests;chain_idin arougechainCLI envelope). The node refuses a signed payload that names another network withCHAIN_ID_MISMATCH— at the API, in the mempool and when producing a block. A payload that names no network is still accepted. See Security. -
REQUIRE_SIGNED_CHAIN_ID(--require-signed-chain-id/QV_REQUIRE_SIGNED_CHAIN_ID, default off). When on, the node also refuses payloads that name no network (CHAIN_ID_REQUIRED). Leave it off for now: turn it on once the wallets your users sign with have shipped the field (site,@rougechain/sdk1.15.0, extension 1.9.0, Qwalla 1.3.0, CLI 1.3.0) and your node no longer receives payloads without it — testnet first, announced in the upgrade schedule (rollout step 3). -
Batch submissions get the same checks as single ones. Every item of
POST /api/v2/batch-submitnow goes through the checks of a single/api/v2submission: the 5-minute timestamp window, the signature replay guard (shared with the single routes and kept across restarts), the network rule,from= signer and the signedaccount_nonce. Verification is all-or-nothing: if any item fails, or the same signature appears twice, the whole batch is refused (success: false,accepted: 0, a result per item), nothing is queued and no signature is used up, so the valid items can be sent again unchanged. A batch that passes is offered to the mempool item by item, as before. -
Block producer and liquidity / swap requests. The block producer leaves out transactions that would fail and refuses malformed liquidity and swap requests:
- a transaction whose application fails is dropped (and logged) and the block is produced with the others — one transaction no longer holds back the rest of the queue;
- a pool, liquidity or swap transaction that would not take effect at its position in the block
(for example a swap whose output would be below its
min_amount_out) is left out instead of included, so it is not charged; - the node refuses, with
AMM_REQUEST_REFUSED, requests with zero amounts, a pool with the same token on both sides, a pool for a pair that already has one (in either order), an addition that would mint no LP tokens, a withdrawal that would return nothing on one side, and swaps with a multi-hop path (swap_path, which the public API never sets).
These are rules of the node that builds and admits transactions; what makes a block valid is unchanged, and blocks from nodes without this release are accepted exactly as before.
-
Balances.
GET /api/balance/:publicKey/XRGEreturns the native XRGE balance (native: true); it used to answer 0. Both balance routes also return the stored integers as decimal strings (balance_raw+decimals;balance_quanta+token_balances_raw) — use these for any threshold check (Units and precision). -
Present but not active. The shielded pool V2 code is included and inactive (
upgrade_schedule.shield_v2isnullon every network). The consensus rules CHAIN_ID_BINDING and CONTRACT_CHAIN_ID are included and unscheduled (chain_id_binding/contract_chain_idarenull); each will get its height in a later, announced release. -
quantum-vault-daemon --versionreports1.6.4(earlier binaries reported0.1.0).
Everything in release 1.6.3 is included; the upgrade schedule is unchanged (mainnet 235 / 235 / 245, testnet 1360 / 1360 / 1390, all active). This binary replays mainnet and testnet history to the same state roots as 1.6.3.
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | 48a43cad46388ede48179432a371a32819492aa59dbc2149653655a5f414e9c3 |
| Size | 29,396,176 bytes |
| Source | quantum-vault 447abca; public rougechain-node |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-network-binding-447abca |
| CLI | rougechain 1.3.0, rougechain-447abca, sha256 0df5a526a120eeb12ac77a5a86b6b448bbdf80b371c63db0a4c1427ef22f6870 |
Built twice from clean; the two builds were byte-identical. The release is described by a manifest signed with the release keys (Signed releases).
Upgrade
Nodes installed with the one-line installer and auto-update install this release by themselves (node and CLI). Otherwise:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-network-binding-447abca
echo "48a43cad46388ede48179432a371a32819492aa59dbc2149653655a5f414e9c3 /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-1.6.4" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
Check after the restart: the node follows the chain as before, "$BIN" --version prints
quantum-vault-daemon 1.6.4, and curl -s localhost:5100/api/stats | jq .upgrade_schedule shows
chain_id_binding, contract_chain_id and shield_v2 as null.
To go back, stop the node, restore the .pre-1.6.4 binary and start it again.
RougeChain node release 1.6.3 — messenger and mail hardening (2026-10)
Recommended for every node. Not a consensus change: no activation height, no state change, and a node that has not installed it keeps following the chain. Nodes that serve the public API should install it promptly.
What changed
- Messenger directory. A directory entry can only be replaced by the wallet that owns it, identified by the signing key that authenticates the request. An encryption key that is already registered to another wallet is refused. Mail names and mail folders follow the owning wallet only.
- Legacy read routes retired.
GET /api/mail/inbox,/api/mail/sent,/api/mail/trash,/api/mail/message/:id,/api/messenger/conversationsand/api/messenger/messagesreturn410 Gone(outside--dev). Clients use the signed routes:POST /api/v2/mail/folder,/api/v2/messenger/conversations/listand/api/v2/messenger/messages/list. - Suspended transaction types are refused by the shielded API routes before any request data
is processed, with
this transaction type is suspended.
Everything in release 1.6.2 is included; the upgrade schedule is unchanged (mainnet 235 / 235 / 245, testnet 1360 / 1360 / 1390, all active).
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | 58c0288fc0137f35f4b986be24b010589ff4783c58239bde229f6a7014ab6171 |
| Size | 28,441,912 bytes |
| Source | quantum-vault 8eacc0d; public rougechain-node |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-directory-hardening-8eacc0d |
Built twice from clean; the two builds were byte-identical. The release is described by a manifest signed with the release keys (Signed releases).
Upgrade
Nodes installed with the one-line installer and auto-update install this release by themselves. Otherwise:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-directory-hardening-8eacc0d
echo "58c0288fc0137f35f4b986be24b010589ff4783c58239bde229f6a7014ab6171 /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-1.6.3" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
Check after the restart: the node follows the chain as before and
curl -s -o /dev/null -w '%{http_code}\n' "localhost:5100/api/mail/inbox?walletId=x" prints 410.
To go back, stop the node, restore the .pre-1.6.3 binary and start it again.
RougeChain node release 1.6.2 — monetary-integrity rule (2026-10)
Mandatory upgrade for every mainnet validator and full node. Blocks 235 and 245 have both passed (mainnet was at height 251 on 2026-10-06), so a node on an older build is already stopped at the first block it cannot judge; installing this release — or 1.6.3, which contains it — and restarting is enough to catch up, no resync. This release contains the two changes that activated at 235 (see the previous notice) and one consensus rule that activated at 245.
| Change | Constant (core/daemon/src/node.rs) | Mainnet | Testnet |
|---|---|---|---|
| Mintable tokens (TOKEN_MINTING) | TOKEN_MINTING_ACTIVATION_HEIGHT | 235 | 1360 (active) |
| Contract royalty reads and royalty cap (CONTRACT_NFT_ROYALTY) | CONTRACT_NFT_ROYALTY_ACTIVATION_HEIGHT | 235 | 1360 (active) |
| Monetary-integrity rule (MONETARY_INTEGRITY) | MONETARY_INTEGRITY_ACTIVATION_HEIGHT | 245 | 1390 |
A node on an older build stops following mainnet at the first block from 235 that uses a new feature, or at the first block from 245 that the new rule judges differently: it stays at the height before it and applies nothing wrong. Installing this build and restarting is enough to continue — no resync.
What the monetary-integrity rule does (≥ 245)
From block 245 a block is invalid if it carries a transaction that:
- has a fee that is not a finite number of zero or more;
- is of type
slash; - sets the faucet flag (mainnet has no faucet; testnet keeps its faucet);
- is of a suspended type:
shield,shielded_transfer,unshield,token_lock,token_unlock,create_staking_pool,token_stake,token_unstake,create_proposal,cast_vote,execute_proposal,delegate,undelegate,token_approve,token_transfer_from.
Token allowances are not affected: /api/v2/token/approve and /api/v2/token/transfer-from use the
approve and transfer_from transaction types, which stay enabled; token_approve and
token_transfer_from are older types that no client sends.
The suspended features are switched off until a later release restores them. On mainnet the shielded pool is empty and none of the other types has ever been used, so no funds are affected. Transfers, validator staking, swaps and liquidity, tokens, minting and allowances, NFTs, contracts, the bridge, messaging and mail are unchanged. Nothing changes below the activation height; mainnet history replays identically.
Also in this build (no activation height): stricter verification on the manual bridge-deposit claim route, the gRPC faucet honours the faucet switch, and the node no longer accepts or produces transactions with an invalid fee.
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | c43773c898fa12897ace7d2dca80f95626da63d3bf5cf3c81fafb7a4a60f86a1 |
| Size | 28,441,848 bytes |
| Source | quantum-vault 2c96256; the public rougechain-node source follows once the release is active |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-monetary-integrity-2c96256 |
Built twice from clean; the two builds were byte-identical. The same binary carries testnet's schedule. The release is described by a manifest signed with the release keys (Signed releases).
Upgrade
Nodes installed with the one-line installer and auto-update install this release by
themselves. Check with rougechain-update status.
Otherwise back up node-keys.json and the data directory, then install the release binary:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-monetary-integrity-2c96256
echo "c43773c898fa12897ace7d2dca80f95626da63d3bf5cf3c81fafb7a4a60f86a1 /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-1.6.2" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
Check after the restart:
curl -s localhost:5100/api/stats | python3 -c "import json,sys;d=json.load(sys.stdin);u=d['upgrade_schedule'];print(d['network_height'], u['token_minting'], u['contract_nft_royalty'], u['monetary_integrity'])"
It should print the current height followed by 235 235 245, and the node's state_root should
equal https://api.rougechain.io/api/stats at the same height.
To go back, stop the node, restore the .pre-1.6.2 binary and start it again. That is safe only
below block 235.
RougeChain node release — mintable tokens and royalty-aware contracts (2026-10)
Mandatory upgrade for every mainnet validator and full node. Block 235 has passed; the current release is 1.6.3, which contains these changes and the block-245 rule. Two consensus changes activate at the same height:
| Change | Constant (core/daemon/src/node.rs) | Activation height |
|---|---|---|
| Mintable tokens: creator-only minting with an optional max supply (TOKEN_MINTING) | TOKEN_MINTING_ACTIVATION_HEIGHT | 235 |
| Contract royalty reads; royalties above 100% refused at creation (CONTRACT_NFT_ROYALTY) | CONTRACT_NFT_ROYALTY_ACTIVATION_HEIGHT | 235 |
A node on an older build stops following mainnet at the first block from 235 that uses either feature: it rejects that block and stays at the height before it. It does not apply anything wrong, so installing this build and restarting is enough to continue — no resync.
Both changes have been active on testnet since block 1360 (capped minting, creator-only minting, the royalty cap and a full marketplace sale paying exact royalty and seller amounts were all verified there).
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | 673bf7b1a469b7f9937b17228faab1e81ae48fb2a90aa32b2fd1b7fcc9cc2984 |
| Size | 28,463,856 bytes |
| Source | quantum-vault 4f8676d (release commit 03613ef); public rougechain-node main e048bf1 |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), cargo 1.94.0 (85eff7c80 2026-01-15), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-mint-royalty-03613ef |
This exact binary runs on the operator nodes. It was built twice from clean and the two builds were byte-identical. The same binary also carries testnet's schedule (both features at 1360).
What changed
Mintable tokens (≥ 235). create_token accepts "mintable": true and an optional integer
"max_supply" (at least the initial supply). The creator can then send signed mint_tokens
transactions (1 XRGE fee). The node refuses a mint by any other wallet or past the cap and records the
minted total in the chain state. Tokens created before 235 stay fixed-supply. See
Token creation.
Royalty-aware contracts (≥ 235). Two read-only host functions,
host_nft_royalty_bps(col) and host_nft_royalty_recipient(col, out, cap), let a contract read a
collection's royalty and pay it with host_transfer. From the same height nft_create_collection with
a royaltyBps above 10000 (or not an integer) is refused. See
Smart Contracts.
Also in this build (no activation height): v2 faucet whitelist and cooldown, --trust-proxy and the
--rate-limit-per-minute alias, rate-limited /api/finality/*, /api/selection showing the real
designated proposer, rouge1 nonce lookups, and token supply fields on the token endpoints.
Upgrade
Back up node-keys.json and the data directory first. Then either install the release binary:
curl -fLo /tmp/quantum-vault-daemon https://api.rougechain.io/releases/quantum-vault-daemon-mint-royalty-03613ef
echo "673bf7b1a469b7f9937b17228faab1e81ae48fb2a90aa32b2fd1b7fcc9cc2984 /tmp/quantum-vault-daemon" | sha256sum -c
systemctl cat rougechain-validator | grep ExecStart # the first path is the binary your node runs
BIN=/path/from/ExecStart/quantum-vault-daemon # set this to that path
sudo systemctl stop rougechain-validator
sudo cp -p "$BIN" "$BIN.pre-235" && sudo install -m 755 /tmp/quantum-vault-daemon "$BIN"
sudo systemctl start rougechain-validator
or build the public repository yourself:
sudo systemctl stop rougechain-validator
cp -a ~/.quantum-vault/mainnet ~/.quantum-vault/mainnet.backup-$(date +%Y%m%d)
cd ~/rougechain && git pull --ff-only # public rougechain-node main, e048bf1 or later
source ~/.cargo/env && cd core && cargo build --release --locked -p quantum-vault-daemon
sudo systemctl start rougechain-validator
(Your own build's sha256 differs from the release binary because build paths are embedded; the source commit is what matters.)
Check after the restart:
curl -s localhost:5100/api/stats | python3 -c "import json,sys;d=json.load(sys.stdin);u=d['upgrade_schedule'];print(d['network_height'], u['token_minting'], u['contract_nft_royalty'])"
It should print the current height followed by 235 235, and the node's state_root should equal
https://api.rougechain.io/api/stats at the same height.
RougeChain node release — finality and game-ready contracts (2026-09-28)
Mandatory upgrade for every mainnet validator and full node. Three consensus changes are already active on mainnet:
| Change | Constant (core/daemon/src/node.rs) | Activation height |
|---|---|---|
| Verified BFT finality — Release 2a | FINALITY_V2_ACTIVATION_HEIGHT | 150 |
| Player-signed contract calls and deployments (GAME_READY) | GAME_READY_ACTIVATION_HEIGHT | 150 |
| Game-ready contracts: tokens, NFTs, randomness, multi-hop moves, state root v2 (GAME_READY 2) | GAME_READY_2_ACTIVATION_HEIGHT | 160 |
Grind-proof rolls: host_block_hash (GAME_READY 3) | GAME_READY_3_ACTIVATION_HEIGHT | 170 |
Payable contract calls (attach) | PAYABLE_CALLS_ACTIVATION_HEIGHT | 190 — upgrade before block 190 |
A node without them cannot follow mainnet past block 150: from block 151 every block carries its parent's finality certificate, and from block 160 the header's state root also commits NFTs and contract state.
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | cc1aee1983e39a61a22995b85dba9e5d7c4f8ef2c3b0c9b7c4dd7cd7f86947bc (block-190 payable-calls release; supersedes 718200bc…, 5e5ca98c… and 46e29456…) |
| Size | 28,031,064 bytes |
| Source commit | cc6f9c3, branch feat/payable-calls (merged to main as #153) |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), cargo 1.94.0 (85eff7c80 2026-01-15), x86_64-unknown-linux-gnu |
This exact binary runs on both operator nodes. It was built twice from clean and the two builds were byte-identical.
Verify you are on the canonical chain:
- block 150 has hash
559e66ee9384aaeea91b5dc70285b3c1cbc2805dab1752632003c85007c81b52 - block 160 has hash
4592f38543195a6c619418f778e9897f09c99bab048a83afe29d8cdf5b5277c0 state_rootfromcurl -s localhost:5100/api/statsequalshttps://api.rougechain.io/api/statsat the same height
What changed
Exact JSON number parsing (required to follow block 177). A contract call's fee is
gasLimit × 0.000001; for about 18% of gas limits that number prints with many digits (block 177:
0.0018369999999999999) and the previous build's JSON parser read it back one unit off, so nodes that
receive blocks over the network rejected block 177. This build parses numbers exactly. It changes no
rule and needs no activation height.
Finality (≥ 150). Validators sign ML-DSA-65 precommits; a block is final once signers holding more
than two thirds of stake have signed it. From block 151 every block header carries parent_commit, the
verified certificate for its parent, and a node refuses to import or build on a block without one. See
Finality.
Player-signed contracts (≥ 150). contract_call and contract_deploy must be signed by the caller
(POST /api/v2/contract/execute and /api/v2/contract/publish); the contract sees the verified signer
as its caller, and the signer pays the fee (deploy 10 XRGE; call gasLimit × 0.000001 XRGE).
Node-signed contract transactions are invalid. Contracts can read their call arguments.
Game-ready contracts (≥ 160). Contracts can hold and send custom tokens and NFTs, create their own
NFT collection and mint to players, and draw per-transaction randomness (host_random). Moves made
inside cross-contract calls are applied. The block state root also covers NFT collections and ownership
and contract code and storage. See Smart Contracts.
API (no consensus change). POST /api/contract/:addr/query (free dry run), contract events paging
(?before=&tx=), WebSocket topic contract:<addr>, receipts that report Failed when a contract call
reverts, and the LP fee ledger (GET /api/pool/:id/earnings/:owner).
Upgrade
Stop the node, back up node-keys.json and the data directory, then build the public
rougechain-node main branch and restart:
sudo systemctl stop rougechain-validator
cp -a ~/.quantum-vault/mainnet ~/.quantum-vault/mainnet.backup-$(date +%Y%m%d)
cd ~/rougechain && git pull --ff-only
source ~/.cargo/env && cd core && cargo build --release --locked -p quantum-vault-daemon
sudo systemctl start rougechain-validator
After the restart, compare the block 150 and 160 hashes and the state_root above with the public
node. If anything differs, the node applied blocks under the old rules: resync from an empty data
directory (keep node-keys.json).
RougeChain node release — consensus Release 1 (2026-09-23)
Mandatory upgrade for every mainnet validator and full node.
This release contains two consensus changes that are already active on mainnet:
| Change | Activation height | Status |
|---|---|---|
Transaction-integrity rule (TX_UNIQUENESS_ACTIVATION_HEIGHT) | 90 | live since 2026-09-23 |
Deterministic proposer selection, Release 1 (PROPOSER_SELECTION_ACTIVATION_HEIGHT) | 100 | live since 2026-09-23 |
Both rules are compiled constants in core/daemon/src/node.rs. A node that does not enforce them is
not a mainnet node: it can accept blocks the network rejects, and if it mines it will fork itself off.
Production binary
| Item | Value |
|---|---|
| Binary | quantum-vault-daemon |
| sha256 | d50f7e5d34d03200f6efc45c8b7b63c61079e7f9d5b5c70f5b3c3c3544105eba |
| Size | 26,866,528 bytes |
| Source commit (this repository) | 203507e (consensus: read-only activation report), branch release/proposer-selection-r1 |
| Toolchain | rustc 1.94.0 (4a4ef493e 2026-03-02), cargo 1.94.0 (85eff7c80 2026-01-15), x86_64-unknown-linux-gnu |
| Download | https://api.rougechain.io/releases/quantum-vault-daemon-proposer-selection-b69d0c9 (+ .sha256) |
This exact binary runs on the operator validators. It was built twice from clean checkouts and the two builds were byte-identical; the recipe is in "Reproducible build" below.
Minimum required release
Minimum: this release (binary d50f7e5d…, source commit 203507e or a later commit on the
same line).
| Binary you are running | Result on mainnet today |
|---|---|
Any build before the transaction-integrity rule (before commit a005046) | Cannot validate blocks ≥ 90. Must upgrade and resync. |
Transaction-integrity build 9ad81dbf98e8e78a86b34be61af2360a34dba055f9f7a4be81b1a69c15942287 | Follows the chain but does not enforce proposer selection at ≥ 100. Not acceptable for a validator. Upgrade before reconnecting. |
d50f7e5d34d03200f6efc45c8b7b63c61079e7f9d5b5c70f5b3c3c3544105eba | Current. |
What changed (summary)
Transaction integrity (height ≥ 90). Every transaction has a single identity derived from the signer's public key and the exact bytes the signer signed. The fields a block executes must be the fields that were signed, and a transaction identity may be included in the chain at most once. This is enforced at the API, in the mempool, and as a block-validity rule, so a block that repeats or alters a signed transaction is rejected by every upgraded node.
Proposer selection, Release 1 (height ≥ 100). For each height H exactly one validator may
propose: the eligible validator (stake > 0, not jailed at H) with the greatest stake in the canonical
validator state after block H-1; ties go to the lowest raw public-key bytes. A correctly signed block
from any other validator is rejected. Producers refuse to seal a slot that is not theirs, and keep a
local, durable proposal journal so a restart can never sign two different blocks for the same slot.
Legacy missed-block accounting (auto-slash / auto-jail for not proposing) is frozen from height 100,
because non-designated validators are now required not to propose. There is no fallback proposer in
Release 1: if the designated proposer is offline, no block is produced until it returns. Fallback
selection is the subject of Release 2.
Read-only reporting. GET /api/stats now includes proposer_selection_activation_height,
designated_proposer_next_height, proposer_selection_active_next and designated_proposer_next;
the daemon logs one [consensus] proposer selection: … line at startup. Compare these across nodes.
Public API containment (unchanged by this release, still in force). POST /api/blocks/import,
POST /api/peers/register, POST /api/tx/broadcast, /api/v2/contract/deploy,
/api/v2/contract/call and /api/bridge/withdraw return 403 at the public edge
(api.rougechain.io). Nodes sync by pulling GET /api/blocks; peering is arranged with the operator.
Design and evidence: core/daemon/TX_INTEGRITY_FORK.md and core/daemon/PROPOSER_SELECTION_DESIGN.md.
Upgrade instructions (existing validator or full node)
An old validator must upgrade before reconnecting to mainnet. Do not start an old binary against
the network, with or without --mine.
- Stop the node.
systemctl stop <your-unit> # e.g. rougechain-validator - Back up keys and data.
node-keys.jsonin your data directory is your staked validator key.cp -a ~/.quantum-vault/mainnet ~/.quantum-vault/mainnet.backup-$(date +%Y%m%d) - Get the binary — download and verify, or build reproducibly (below).
curl -fsSLO https://api.rougechain.io/releases/quantum-vault-daemon-proposer-selection-b69d0c9 curl -fsSLO https://api.rougechain.io/releases/quantum-vault-daemon-proposer-selection-b69d0c9.sha256 sha256sum -c quantum-vault-daemon-proposer-selection-b69d0c9.sha256 # must print: OK install -m 755 quantum-vault-daemon-proposer-selection-b69d0c9 /path/to/quantum-vault-daemon - Decide whether you need a resync. If your node ever ran an older binary past height 48, or its
state root at any height differs from the network's, start from a fresh data directory (keep the
backup; copy only
node-keys.jsonback). If in doubt, resync. A resync from genesis replays the whole chain under the current rules and takes a few minutes at today's chain length. - Start without
--mine. Remove--minefrom your unit until the node is verified.systemctl start <your-unit> journalctl -u <your-unit> --since "-2min" | grep "proposer selection" # expect: [consensus] proposer selection: activation height Some(100); next height <tip+1> (rule ACTIVE); designated proposer for <tip+1>: 8ccf7878003b2668 - Verify against the network at the same height:
curl -s http://127.0.0.1:5101/api/stats | jq '{network_height, state_root, designated_proposer_next}' curl -s https://api.rougechain.io/api/stats | jq '{network_height, state_root, designated_proposer_next}'state_rootmust match at equal heights anddesignated_proposer_nextmust match. Only then is the node on mainnet. - Mining. Under Release 1 only the designated proposer may seal blocks, and the daemon refuses to
seal any other slot, so a non-designated validator gains nothing from
--mine. Leave it off unless your validator is the designated proposer. Staying staked and synchronized keeps your validator eligible for Release 2 fallback selection.
Reproducible build
git clone https://github.com/cyberdreadx/rougechain-node && cd rougechain-node
git checkout 203507e
export SOURCE_DATE_EPOCH=1790185445
export RUSTFLAGS="--remap-path-prefix=$PWD=/build/src --remap-path-prefix=$PWD/core/target=/build/target --remap-path-prefix=$HOME/.cargo=/build/cargo --remap-path-prefix=$HOME=/build/home"
cd core && cargo build --release --locked -p quantum-vault-daemon
sha256sum target/release/quantum-vault-daemon
# expected: d50f7e5d34d03200f6efc45c8b7b63c61079e7f9d5b5c70f5b3c3c3544105eba (rustc/cargo 1.94.0, x86_64 Linux)
Verified 2026-09-23: building commit 203507e of this repository with the recipe above (at the same source path as the release build) produced a byte-identical binary, sha256 d50f7e5d….
Cargo's metadata hash includes the package path, so two builds only match when they are made at the same absolute source path with the same remapping. A different absolute path yields a different, still valid, binary; compare against a second build at your own path if you want to check reproducibility locally.
Reporting problems
Open an issue in this repository with your /api/stats output, the startup [consensus] line and
the rejecting log line if any. Do not post private keys or node-keys.json.
P2P Networking
RougeChain uses a peer-to-peer network for block propagation, transaction broadcasting, and peer discovery. All peer traffic runs over each node's HTTP REST API (--api-port, usually behind an HTTPS proxy); there is no separate P2P port.
How It Works
┌─────────────┐ blocks/txs ┌─────────────┐
│ Node A │ ◄─────────────────► │ Node B │
│ (mining) │ │ (syncing) │
└─────────────┘ └─────────────┘
▲ ▲
│ peer discovery │
└───────────────┬───────────────────┘
│
▼
┌─────────────┐
│ Node C │
│ (new peer) │
└─────────────┘
Features
| Feature | Description |
|---|---|
| Block Sync | Nodes download and verify blocks from peers |
| Transaction Broadcast | Submitted txs propagate to all peers |
| Peer Discovery | Nodes share their peer lists with each other |
| Genesis Reset | New nodes automatically adopt the network's chain |
Connecting to the Network
As a Syncing Node
./quantum-vault-daemon \
--api-port 5100 \
--peers "https://testnet.rougechain.io/api"
As a Mining Node
./quantum-vault-daemon \
--mine \
--api-port 5100 \
--peers "https://testnet.rougechain.io/api"
Peer Discovery
Nodes automatically discover new peers every 30 seconds by:
- Querying known peers via
GET /api/peers - Adding any new peers to their list
- Optionally registering themselves via
POST /api/peers/register
Enable Self-Registration
./quantum-vault-daemon \
--api-port 5100 \
--peers "https://testnet.rougechain.io/api" \
--public-url "https://mynode.example.com"
This tells other nodes how to reach you.
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/peers | GET | List known peers |
/api/peers/register | POST | Register as a peer |
/api/blocks/import | POST | Import a block (peer-to-peer) |
/api/tx/broadcast | POST | Receive a broadcasted transaction |
Next Steps
Connecting to Peers
Connect your RougeChain node to the network to sync blocks and broadcast transactions.
Connect to Testnet
The simplest way to join the network:
./quantum-vault-daemon \
--api-port 5100 \
--peers "https://testnet.rougechain.io/api"
Your node will:
- Download all blocks from the peer
- Verify each block's PQC signatures
- Build the local chain state
- Start receiving new blocks in real-time
⚠️ Important: Your node is syncing but INVISIBLE to the network.
Without
--public-url, your node pulls blocks from peers but never registers itself. Other nodes won't know it exists, and it won't appear on the network globe.To be discoverable, add
--public-urlwith your node's reachable address — the HTTPS URL of the reverse proxy in front of it (see Firewall & Exposure):./quantum-vault-daemon \ --api-port 5100 \ --node-name "MyNode" \ --public-url "https://your-server.com/api" \ --peers "https://testnet.rougechain.io/api"If running locally without a public IP, your node works fine for personal use — it just won't be visible to the rest of the network.
Connect to Multiple Peers
For better reliability, connect to multiple peers:
./quantum-vault-daemon \
--api-port 5100 \
--peers "https://api.rougechain.io/api,https://node2.example.com,https://node3.example.com"
Or via environment variable:
export QV_PEERS="https://api.rougechain.io/api,https://node2.example.com"
./quantum-vault-daemon --api-port 5100
Verify Connection
Check that your node has peers:
curl http://127.0.0.1:5100/api/peers
Expected response:
{
"peers": [
"https://testnet.rougechain.io/api"
],
"count": 1
}
Sync Status
Check if your node is synced:
curl http://127.0.0.1:5100/api/health
{
"status": "ok",
"chain_id": "rougechain-devnet-1",
"height": 12345
}
Compare the height with the testnet to confirm you're in sync:
curl https://testnet.rougechain.io/api/health
Connection Flow
Your Node Peer Node
│ │
│── GET /api/blocks?from_height=0&limit=1000 ────►│ (fresh node; later polls
│◄─── { blocks: [ block0, block1, ... ] } ───────│ use ?limit=1000)
│ │
│ (verify signatures, apply) │
│ │
│── GET /api/finality/:height ───────────────────►│
│◄─── finality proof ────────────────────────────│
│ │
│── GET /api/peers ──────────────────────────────►│
│◄─── { peers: [...] } ──────────────────────────│
│ │
│── POST /api/peers/register ────────────────────►│ (only with --public-url)
│── GET /api/stats ──────────────────────────────►│ (peer name)
All peer traffic uses the same HTTP REST API that wallets use; there is no separate P2P protocol or port. New blocks and transactions are pushed with POST /api/blocks/import and POST /api/tx/broadcast.
Firewall & Exposure
Do not expose the daemon's REST API port (5100) to the public internet — it
serves state-changing endpoints. Bind the daemon to 127.0.0.1 and put a reverse
proxy (nginx + TLS) in front of it; open only the proxy's port 443. See
Public Node Security.
| Port | Bind | Exposure |
|---|---|---|
| 443 (nginx/TLS) | public | ✅ the only port open to the internet |
5100 (daemon REST API, --api-port; default 5101) | 127.0.0.1 | ❌ never open to the public — peers reach it through the proxy |
4101 (gRPC, --port) | 127.0.0.1 | ❌ client gRPC services only; peers do not use it |
# Expose ONLY the reverse proxy; keep the daemon API private:
sudo ufw allow 443/tcp
sudo ufw deny 5100/tcp
Troubleshooting
"Connection refused"
- Check that the peer URL is correct and reachable
- Verify the peer node is running
- Check for firewall restrictions
Node stuck syncing
- The initial sync may take time on long chains
- Monitor progress by watching the
heightincrease via/api/health - Try connecting to a different peer
"Chain ID mismatch"
- Ensure your
--chain-idmatches the network you're connecting to - Testnet uses
rougechain-devnet-1
Peer Discovery
RougeChain nodes automatically discover other nodes on the network through peer exchange over the HTTP API: each node polls GET /api/peers on the peers it knows.
How Discovery Works
1. Node A starts with seed peer(s) via --peers
2. Node A queries GET /api/peers on each known peer
3. Each peer returns its own list of known peers
4. Node A adds any new peers to its local list
5. If --public-url is set, Node A registers itself on peers
6. Repeat every 30 seconds
Over time, all nodes in the network discover each other, forming a mesh.
Discovery Interval
Peer discovery runs automatically every 30 seconds. No configuration is needed.
Seed Peers
The first peer(s) you connect to act as seeds for discovery. From them, your node learns about the rest of the network.
# Single seed
./quantum-vault-daemon --peers "https://testnet.rougechain.io/api"
# Multiple seeds for redundancy
./quantum-vault-daemon --peers "https://api.rougechain.io/api,https://backup.example.com"
Self-Registration
To make your node discoverable by others, set --public-url:
./quantum-vault-daemon \
--api-port 5100 \
--peers "https://testnet.rougechain.io/api" \
--public-url "https://mynode.example.com"
Your node will call POST /api/peers/register on all known peers, announcing its presence. Other nodes will then include your URL in their peer lists.
Discovery API
Get Known Peers
curl http://127.0.0.1:5100/api/peers
{
"peers": [
"https://testnet.rougechain.io/api",
"https://node2.example.com/api",
"https://node3.example.com/api"
],
"count": 3
}
Register as a Peer
curl -X POST https://testnet.rougechain.io/api/peers/register \
-H "Content-Type: application/json" \
-d '{"peerUrl": "https://mynode.example.com"}'
Discovery Diagram
Seed Node
┌─────────┐
│ Node A │ ◄── your --peers target
└────┬────┘
│
GET /api/peers
│
┌────────┼────────┐
▼ ▼ ▼
┌───────┐┌───────┐┌───────┐
│Node B ││Node C ││Node D │ ◄── discovered automatically
└───────┘└───────┘└───────┘
│ │ │
└─────────┼────────┘
│
More peers discovered
from B, C, D...
Privacy Considerations
- Your node's IP/URL is shared with all peers when using
--public-url - Without
--public-url, your node connects outward but is not discoverable by others - Consider using a reverse proxy or domain name instead of exposing raw IP addresses
Running a Public Node
Run a RougeChain node that is publicly accessible and participates fully in the network.
Requirements
| Requirement | Details |
|---|---|
| Server | VPS or dedicated server with a public IP |
| Domain (recommended) | Point a domain to your server |
| SSL certificate | Required for HTTPS (use Let's Encrypt) |
| Open port | 443 on the reverse proxy (the daemon's API port stays on 127.0.0.1) |
Setup
1. Build and Install
git clone https://github.com/cyberdreadx/rougechain-node
cd rougechain-node/core
cargo build --release -p quantum-vault-daemon
sudo cp target/release/quantum-vault-daemon /usr/local/bin/
2. Configure Reverse Proxy (nginx)
server {
listen 443 ssl;
server_name mynode.rougechain.example.com;
ssl_certificate /etc/letsencrypt/live/mynode.rougechain.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mynode.rougechain.example.com/privkey.pem;
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:5100;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Set client_max_body_size to at least 50M to support messenger media uploads.
By default the daemon keys rate limits by the connecting IP, which behind this proxy is always
127.0.0.1. Start it with--trust-proxyto key them by theX-Real-IP/X-Forwarded-Forthis proxy sets (honoured only for loopback connections). See Rate Limiting.
3. Start the Node
./quantum-vault-daemon \
--mine \
--host 127.0.0.1 \
--api-port 5100 \
--node-name "MyNode" \
--peers "https://testnet.rougechain.io/api" \
--public-url "https://mynode.rougechain.example.com"
Why
--public-urlis required: Without it, your node syncs blocks but never tells other nodes "I exist." It stays invisible — it won't appear on the network globe, other nodes can't sync from it, and it won't receive direct block broadcasts. The--public-urlmust be the URL that other nodes on the internet can reach.
Once running, visit http://127.0.0.1:5100 in your browser to see the built-in node dashboard with live block height, peers, mining status, and fees.
4. Run as a Service
Create /etc/systemd/system/rougechain.service:
[Unit]
Description=RougeChain Node
After=network.target
[Service]
Type=simple
User=rougechain
ExecStart=/usr/local/bin/quantum-vault-daemon --mine --host 127.0.0.1 --api-port 5100 --peers "https://testnet.rougechain.io/api" --public-url "https://mynode.rougechain.example.com"
Restart=always
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
sudo useradd -r -s /bin/false rougechain
sudo systemctl enable rougechain
sudo systemctl start rougechain
Verify Your Node
Health Check
curl https://mynode.rougechain.example.com/api/health
Check Peers Can See You
curl https://testnet.rougechain.io/api/peers
# Your node's URL should appear in the list
Security Hardening
API Keys
--api-keys requires an X-API-Key (or Authorization: Bearer) header on every route that is not exempt — reads such as /api/blocks and balances included, not just writes:
./quantum-vault-daemon \
--mine \
--api-port 5100 \
--api-keys "secret-key-1,secret-key-2"
Exempt routes: /, /api/health, /api/stats, /api/v2/*, /api/bridge/*, /api/messenger/*, /api/mail/*, /api/names/*, /api/finality/*, the faucet and the bot (signed routes check their own signatures). Peers never send an API key, so a keyed node also refuses the peer routes /api/blocks/import, /api/peers/register and /api/tx/broadcast — use API keys for a private node, not for a public one that should take part in peering.
Rate Limiting
The node has built-in rate limiting (disabled by default — every limit defaults to 0, unlimited). Set the read and write limits:
./quantum-vault-daemon --rate-limit-read-per-minute 600 --rate-limit-write-per-minute 60
GET requests count against the read limit and every other method against the write limit; --rate-limit-validator and --rate-limit-peer set separate limits for signed validator requests and registered peers. --rate-limit-per-minute is a deprecated alias that applies to both reads and writes when neither of those is set. Limits are keyed by the connecting IP, so behind the nginx proxy above all clients share one bucket unless you add --trust-proxy (env QV_TRUST_PROXY=true): then a request arriving from loopback is keyed by X-Real-IP (else the rightmost X-Forwarded-For entry), and any non-loopback connection still by its socket IP. Only enable it when the API port is reachable solely through that local proxy. All routes are limited, including /api/finality/*.
Firewall
Only expose the necessary port:
sudo ufw default deny incoming
sudo ufw allow ssh
sudo ufw allow 443/tcp
sudo ufw enable
Monitoring
Check Logs
sudo journalctl -u rougechain -f
Health Endpoint
Set up a monitoring tool (e.g., UptimeRobot) to poll:
https://mynode.rougechain.example.com/api/health
Metrics to Watch
| Metric | How to Check |
|---|---|
| Block height | GET /api/health — compare with testnet |
| Peer count | GET /api/peers — should be > 0 |
| Validator status | GET /api/validators — check your stake |
Staking & Validators
RougeChain uses Proof of Stake (PoS) for consensus. Validators stake XRGE tokens to participate in block production and earn rewards.
How It Works
- Stake tokens - Lock XRGE to become a validator
- Propose and vote - The designated proposer (the validator with the most stake) proposes blocks; staked validators vote to finalize them
- Earn rewards - Every staked validator earns a stake-weighted share of the fees in every block
- Unstake - Wait for unbonding period to withdraw
Requirements
| Requirement | Value |
|---|---|
| Minimum stake | 10,000 XRGE — enforced by the node API on every stake call (/api/v2/stake, CLI), not by consensus: block apply accepts any positive stake, and one mainnet validator holds 9,000 XRGE |
| Unbonding period | 500 blocks (a block count, not a duration) |
| Slashing | None active. The code carries 10 % per violation and a 20-block jail, but automatic missed-block slashing is frozen since height 100, the legacy slash transaction is rejected since height 245, and evidence-based slashing does not exist yet |
| Validators on mainnet (2026-10-06) | 3: about 100.09 M XRGE (producing), 10,000 XRGE and 9,000 XRGE (both non-producing) |
Become a Validator
Via Web UI
- Go to the Validators page
- Click Stake
- Enter amount (min 10,000 XRGE)
- Confirm transaction
Via SDK (Recommended)
import { RougeChain, Wallet } from '@rougechain/sdk';
const rc = new RougeChain('https://testnet.rougechain.io/api');
const wallet = Wallet.fromKeys(publicKey, privateKey);
await rc.stake(wallet, { amount: 10000 });
Via API (v2 Signed Request)
All write operations require ML-DSA-65 client-side signing. See Staking API for the full request format.
# The payload must be signed client-side with your ML-DSA-65 private key.
# Use the SDK or build the signed request manually:
curl -X POST https://testnet.rougechain.io/api/v2/stake \
-H "Content-Type: application/json" \
-d '{
"payload": {
"amount": 10000,
"from": "your-public-key-hex",
"timestamp": 1706745600000,
"nonce": "random-hex"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "your-public-key-hex"
}'
Check Your Stake
curl "https://testnet.rougechain.io/api/validators?publicKey=your-public-key"
Response:
{
"validators": [
{
"publicKey": "your-public-key",
"stake": 10000.0,
"status": "active",
"blocksProposed": 42
}
]
}
Unstake
Via SDK
await rc.unstake(wallet, { amount: 5000 });
Via API (v2 Signed Request)
curl -X POST https://testnet.rougechain.io/api/v2/unstake \
-H "Content-Type: application/json" \
-d '{
"payload": {
"amount": 5000,
"from": "your-public-key-hex",
"timestamp": 1706745600000,
"nonce": "random-hex"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "your-public-key-hex"
}'
Validator Selection
Since mainnet height 100, each block has exactly one designated proposer: the eligible validator (stake > 0, not jailed) with the most stake, ties going to the lowest raw public-key bytes. Selection is deterministic — no randomness and no rotation — and there is no fallback proposer yet. Blocks from any other validator are rejected. See Becoming a Validator → Proposer selection.
GET /api/selectionreports this designated proposer for the next height (rule: "designated_max_stake").
Rewards
Validators earn from an EIP-1559-inspired fee model:
| Component | Distribution |
|---|---|
| Base fee | Half of the base fee per transaction is burned (capped at the fees collected); everything else flows into the block's tip pool |
| Tip pool | Proposer 20% · validators 70% (stake-weighted) · treasury 10% |
| Minimum tip | The code tops the tip pool up to 0.1 XRGE per block from the __staking_rewards__ reserve — but on mainnet that reserve holds 0 XRGE (never funded), so the floor pays nothing |
Fees are credited when a block is applied to state. The validator share goes to every validator in the set by stake, with no check that the validator took part in finality. There is no block subsidy: with today's activity, rewards are small (about 541 XRGE of fees were collected over the whole chain up to 2026-10-06, before burning and splitting). See Validator Economics for detailed reward calculations.
PQC Security
All validator operations use ML-DSA-65 signatures:
- Block proposals are signed
- Stake/unstake transactions are signed
- Signatures are verified by all nodes
Block production and staking are therefore signed with quantum-resistant keys.
Finality: verified BFT finality (FINALITY_V2 — verified ML-DSA-65 votes, recomputed quorum, anti-equivocation journals) is active on mainnet since height 150. Every block from 151 carries a ⅔-stake commit certificate for its parent. See Finality.
Planned, not built: a consensus redesign decided on 2026-10-06 — rotating stake-weighted proposers, Tendermint-style rounds, an active set capped at 32, a consensus-enforced minimum of 100,000 XRGE (30-day grace for existing validators), 21-day unbonding and evidence-based slashing. Heights will be announced in advance; see Status & Roadmap. Until then, everything on this page describes the rules as they run today.
Becoming a Validator
Validators propose blocks and earn fees on RougeChain. This guide targets mainnet (rougechain-mainnet-1, real value, no faucet). To practice first, see Testing on testnet at the end.
Quick install (one command)
On a fresh Ubuntu 22.04 / 24.04 or Debian 12 server (x86_64; a ~$5/mo VPS is plenty), the
installer downloads the current signed release of the node — nothing is compiled — sets it up as
a systemd service under a dedicated user, and starts syncing:
curl -sSL https://raw.githubusercontent.com/cyberdreadx/rougechain-node/main/scripts/install-validator.sh | sudo bash
What it verifies. The installer fetches the release manifest and its signatures from
api.rougechain.io and from the GitHub mirror, checks the Ed25519 signature with openssl
against the release public key that is embedded in the script (and the ML-DSA-65 signature
with the rougechain CLI of the release), and only then downloads the
binary and checks its size and sha256 against the signed manifest. If anything does not match, it
stops and installs nothing. See Signed releases. To check a release yourself, or to see what the script would do
without changing anything:
curl -sSL https://raw.githubusercontent.com/cyberdreadx/rougechain-node/main/scripts/install-validator.sh | bash -s -- --dry-run
(--dry-run needs no root, but curl, openssl and jq must already be installed.) Verifying a
release by hand with openssl is described in Signed releases.
What it sets up. Node /usr/local/bin/quantum-vault-daemon and the CLI
/usr/local/bin/rougechain, both from the signed release; service rougechain-validator
running as the system user rougechain; data and node-keys.json in /var/lib/rougechain/mainnet
(key file mode 0600); API on 127.0.0.1:5100; and automatic updates
from signed releases (timer rougechain-update.timer; AUTO_UPDATE=0 to opt out). It generates a node key only if there is none and
never overwrites an existing key or chain data. Settings are environment variables placed after
sudo, for example NODE_NAME=my-validator, NETWORK=testnet, NO_START=1 — the full list is in
Signed releases.
It installs a full node, not yet a validator. Block production (--mine) stays off until you
ask for it, because a node should not claim to produce blocks with a key that is not staked:
-
Run the installer (above). Back up
/var/lib/rougechain/mainnet/node-keys.jsonoffline. -
Fund and stake ≥ 10,000 XRGE from that key with the installed
rougechainCLI. The key file is readable only by therougechainuser, so run the CLI as that user:K=/var/lib/rougechain/mainnet/node-keys.json sudo -u rougechain rougechain --node-keys $K whoami # your validator address # send ≥ 10,000 XRGE (+ 1 XRGE fee) to that address from your main wallet, then: sudo -u rougechain rougechain --node-keys $K stake 10000 sudo -u rougechain rougechain --node-keys $K validator-status(Without
sudo, as root:runuser -u rougechain -- rougechain ….) Details of each command are in Step 2 and Step 3 below. -
Once
validator-statusshows✓ Stakedand✓ In active set, turn on block production and give peers a URL they can reach over HTTPS (peers talk to each other over the REST API; there is no separate P2P port):curl -sSL https://raw.githubusercontent.com/cyberdreadx/rougechain-node/main/scripts/install-validator.sh \ | sudo VALIDATOR=1 PUBLIC_URL=https://node.example.com bash
The node then votes on blocks automatically and proposes whenever it is the
designated proposer. Upgrades are automatic: the node installs newer
signed releases by itself, checks its own health afterwards and rolls back if the check fails —
rougechain-update status shows where it stands, and
Automatic updates explains the checks and how to switch to
notify-only or pin a version. You can always upgrade by hand instead: re-run the installer (it
verifies the new signed release, keeps the previous binary as quantum-vault-daemon.prev, and
restarts the service; add NO_START=1 to restart later yourself). The rest of this page is the manual
walkthrough if you would rather do each step yourself.
The one thing you must understand
Your validator identity is a single ML-DSA-65 keypair that does two jobs:
- it holds your stake — staking from a key registers that key as a validator, and
- it signs the blocks your node proposes.
Your node stores this keypair at <data-dir>/node-keys.json. The network rejects any block whose proposer is not a staked validator (and, since height 100, any block whose proposer is not the designated proposer) — so your node's node-keys.json key and your staked key must be the same key. If you stake from one key but run your node with a different (freshly generated) key, your blocks are rejected by every peer and you earn nothing, with no obvious error. This is the most common way to get validator setup wrong.
Security: this key signs blocks on an always-online, internet-facing server. Use a dedicated key that holds only your stake — never your main treasury wallet. If the server is compromised, the blast radius is limited to the staked amount.
Prerequisites
- ≥ 10,000 XRGE (+ 1 XRGE fee) — see minimum stake.
- A server — see system requirements and installation.
- The daemon (
quantum-vault-daemon) and the CLI (rougechain).
Minimum stake
The 10,000 XRGE minimum is enforced by the node's API when it builds or accepts a stake request
(/api/v2/stake and the CLI stake command), not by consensus: block apply accepts any positive
stake that the balance covers, and one mainnet validator holds 9,000 XRGE. There are no validator
tiers, no commission and no delegation — the delegate transaction type belonged to the suspended
governance system (block 245) and never concerned staking. A consensus-enforced minimum of 100,000
XRGE, with a 30-day grace period for existing validators, is part of the consensus redesign decided on
2026-10-06, which is not built; see Status & Roadmap.
Mainnet has three staked validators (2026-10-06): about 100.09 M XRGE (the operator's producing validator), 10,000 XRGE and 9,000 XRGE. Only the largest produces blocks.
Step 1 — Install and generate your node identity
Install the daemon (installation guide), then start it once so it creates its identity keypair and begins syncing:
./quantum-vault-daemon \
--genesis daemon/genesis-mainnet.json \
--chain-id rougechain-mainnet-1 \
--peers https://api.rougechain.io/api \
--data-dir ~/.quantum-vault/mainnet \
--api-port 5100 --port 4100 \
--node-name my-validator
On first boot it logs:
[node] Generated and saved new node keys (pub: <your-node-pubkey>...)
Your identity keypair now lives at ~/.quantum-vault/mainnet/node-keys.json (fields: algorithm, public_key_hex, secret_key_hex). Back this file up, offline. Losing it means losing your validator identity — and the ability to unstake. Let the node sync to the chain tip before continuing.
Step 2 — Point the CLI at your node's key
No key copying needed — the CLI signs directly from your node's identity file with the --node-keys flag, so you act as exactly the key your node signs blocks with:
rougechain --node-keys ~/.quantum-vault/mainnet/node-keys.json whoami
This prints your validator's public key + rouge1… address — confirm it matches the key the daemon logged in Step 1. Every command below uses the same --node-keys flag. The CLI talks to the public mainnet node (https://api.rougechain.io) unless you pass --network testnet or --rpc <your node>; stake, unstake and transfer are submitted through the node's signed /api/v2 routes, so they work from any machine. This needs a CLI built from the current source (or the one the installer provides): older builds default to a retired host and post to a route the public node refuses.
Step 3 — Fund and stake
Send ≥ 10,000 XRGE (+ 1 XRGE fee) to your validator address (from whoami above) from your main wallet. Then stake it:
rougechain --node-keys ~/.quantum-vault/mainnet/node-keys.json stake 10000
The 10,000 minimum is enforced by the API on every stake call — a smaller top-up is rejected. Each additional stake must itself be ≥ 10,000; totals accumulate. It is not a consensus rule (see Minimum stake).
Verify with the built-in diagnostic — it checks funded / staked / active / producing in one shot:
rougechain --node-keys ~/.quantum-vault/mainnet/node-keys.json validator-status
You want ✓ Staked and ✓ In active set. (It prints the exact fix next to anything that's ✗.)
Step 4 — Start mining
Restart the daemon with --mine (same key, same data-dir), plus a public URL so peers can reach you:
./quantum-vault-daemon \
--mine \
--genesis daemon/genesis-mainnet.json \
--chain-id rougechain-mainnet-1 \
--peers https://api.rougechain.io/api \
--data-dir ~/.quantum-vault/mainnet \
--api-port 5100 --port 4100 \
--node-name my-validator \
--public-url https://my-validator.example.com
Because your node-keys.json key is now a staked validator, your node votes toward each block's commit certificate, and peers accept your blocks whenever you are the designated proposer. Check progress anytime with:
rougechain --node-keys ~/.quantum-vault/mainnet/node-keys.json validator-status
You're fully live once you're staked, in the active set and synced. ✓ Producing blocks counts blocks you have proposed, so it only turns green once you have been the designated proposer (the largest stake); see Proposer selection.
Proposer selection
Since mainnet height 100 (Release 1), each block has exactly one designated proposer: the eligible validator (stake > 0, not jailed) with the most stake; ties go to the lowest raw public-key bytes. Selection is deterministic: there is no randomness, no QRNG and no rotation, so a validator that does not hold the most stake does not propose blocks. Every staked, non-jailed validator still earns a stake-weighted share of the fees in every block (see Rewards). There is no fallback proposer yet: if the designated proposer is offline, no blocks are produced until it returns. See Adding a Validator for how this affects a new validator.
The designated proposer for the next height is
designated_proposer_nextinGET /api/stats, andproposerinGET /api/selection(which reportsrule: "designated_max_stake"and no entropy fields once the rule is active).
Security & slashing — read before you go live
- Dedicated key. Keep only the stake in your validator key; never use your treasury wallet. (Done, if you followed Step 1.)
- One node per key. Never run two nodes with the same key — two nodes can sign two different blocks or votes at one height (equivocation). Slashing on equivocation evidence is part of the planned consensus redesign and does not exist yet.
- Back up
node-keys.jsonoffline. It is the only copy of your validator identity. - Don't expose the daemon port. Bind to localhost, front it with nginx + TLS, and firewall the RPC/API port. See public-node security. Do not open port 5100 to the public internet.
- Stay online. No slashing is active today: automatic missed-block slashing is frozen since height 100 and the legacy
slashtransaction is rejected since height 245 (the 10 % / 20-block parameters remain in the code). But if you are the designated proposer and go offline, the chain stops producing blocks, and an offline validator's vote is missing from the ⅔-stake commit certificate. Alert on your node being offline or lagging the chain tip (/api/healthheight vs the network). - Never run
--devon a mainnet node — it enables unsafe key-accepting endpoints. - Upgrades. A node set up with the installer updates itself from signed releases only: it verifies the release keys' signatures, waits if your validator is the designated proposer with transactions pending, health-checks the node after the restart and rolls back if the check fails. If you would rather restart your validator yourself, set
MODE=notify(orPIN_VERSION) in/etc/rougechain/mainnet/update.conf— then you must install mandatory releases before their upgrade height. Never use anything that builds and runsmainunattended (the retired git-pull "auto-deploy" cron): that is unreviewed code, restarted at arbitrary times.
Increasing stake / leaving
- Add stake:
rougechain --node-keys ~/.quantum-vault/mainnet/node-keys.json stake 10000again (≥ 10,000 each time). - Leave:
rougechain --node-keys ~/.quantum-vault/mainnet/node-keys.json unstake <amount>enters the 500-block unbonding queue (wall-clock time depends on how often blocks are produced); dropping to 0 stake removes you from the active set. See Staking.
Testing on testnet first
Practice the whole flow with no real value: swap the mainnet flags for testnet —
--chain-id rougechain-devnet-1, --peers https://testnet.rougechain.io/api, --data-dir ~/.quantum-vault/testnet — point the CLI at it with rougechain --network testnet …, and get test XRGE from the faucet (rougechain --network testnet --node-keys <path> faucet, 10,000 per key per 24 h — staking 10,000 needs 10,001 with the fee, so top up with a transfer from a second key or the wallet). Everything else is identical.
With the installer: curl -sSL …/install-validator.sh | sudo NETWORK=testnet bash (service
rougechain-validator-testnet, data in /var/lib/rougechain/testnet, API on 127.0.0.1:5101, CLI
installed as rougechain-testnet; run it as sudo -u rougechain rougechain-testnet --network testnet --node-keys /var/lib/rougechain/testnet/node-keys.json …).
Adding a Validator to a Live Chain
This note explains how a new validator joins a chain that is already running, and clears up a common misreading of the sync code. If you just want the step-by-step operator walkthrough, read Becoming a Validator first — this page explains why those steps are ordered the way they are, and what happens at the network level when you get it wrong.
The one invariant: stake before you propose
A block is accepted by other nodes only if its proposer is a staked validator in the on-chain validator set. So a new validator must stake first, confirm it is in the active set, and only then run
--mine. Before you are staked-and-in-set, your node never holds a proposer slot.
Being staked is necessary but not sufficient to propose. Since mainnet height 100 (proposer selection, Release 1) exactly one validator may propose each block: the designated proposer, which is the eligible validator (stake > 0, not jailed) with the most stake, ties going to the lowest raw public-key bytes. The rule is deterministic: there is no randomness, no rotation and no fallback. A correctly signed block from any other validator is rejected, and a node that is not the designated proposer does not seal blocks at all. Since height 150 (FINALITY_V2) a block must also carry a commit certificate for its parent signed by validators holding ⅔ of the stake. See Finality.
Everything below is the mechanism behind these rules.
Myth vs. reality: "the chain becomes unjoinable after block 100"
You may hear — often from a tool reading the source — that RougeChain "becomes unjoinable the moment it passes block 100." This is false, and it comes from misreading one line in the P2P import path (core/daemon/src/node.rs, import_block):
#![allow(unused)] fn main() { let validators = self.list_validators().unwrap_or_default(); if !validators.is_empty() && block.header.height > PROPOSER_AUTH_ACTIVATION_HEIGHT { // proposer must be a staked validator (stake > 0), else reject this block } }
PROPOSER_AUTH_ACTIVATION_HEIGHT is 100. The rule this enforces is "past height 100, an imported block's proposer must be a staked validator" — not "past height 100, imported blocks are rejected." Whether it is a wall or a no-op depends entirely on who is producing the blocks:
- If blocks are produced by staked validators (genesis
initial_validators, or anyone added later via astaketx), then every syncing node rebuilds that same validator set from history as it replays the chain, recognizes the proposer, and accepts the block at any height. The chain stays fully joinable forever. - The check only ever rejects a block whose proposer is not a staked validator — i.e. a genuine misconfiguration (a node mining with an unstaked key). That is the check doing its job. (From height 100 the stricter designated-proposer rule above applies as well.)
So the "cliff" is really the stake-before-propose invariant stated backwards. Growing the validator set is not blocked by this check — it is governed by it: stake correctly and joining works at height 100, 10,000, or 10,000,000.
How proposer authorization actually works
Two things combine:
-
The validator set is deterministic from history. Genesis seeds
initial_validators(each with a stake), and every laterstake/unstaketransaction updates the on-chain validator store. Because a syncing node replays the exact same blocks in the exact same order, it derives the exact same validator-set membership at every height. There is no out-of-band "validator list" a joiner can be missing — it is a function of the chain it is already downloading. -
A bootstrap grace window below height
PROPOSER_AUTH_ACTIVATION_HEIGHT(100). Blocks at or below height 100 skip only the proposer-authorization check — they are not otherwise unvalidated. A block below 100 must still pass its proposer signature, block hash, andprev_hash, every transaction signature (verified at all heights), and — past the v2 fork height (18) — the committed state root and the contract-custody apply. Skipping proposer authorization just lets a brand-new network get off the ground before staking transactions have populated the set. Above height 100 the proposer check is enforced (once the set is non-empty — see below), so on a mature chain there is no grace period: a new validator must already be staked and in the set before its blocks will be accepted by peers.
Two edge cases in that gate. (a) The check is also short-circuited when the node's locally-derived validator set is empty (a defensive don't-brick fallback) — so it is meaningfully enforced only once the set is non-empty and height exceeds 100. (b) It gates imported blocks (the P2P sync path). Before proposer selection was active, a node's own freshly-produced block wasn't self-rejected, so an unstaked miner's height could climb locally while peers rejected the same blocks. Since height 100 that no longer happens: the miner refuses to seal any slot it is not the designated proposer for, so a misconfigured node simply stays at the network's height.
The grace window is a bootstrap aid, not something to rely on when adding a validator. On any chain past height 100 it does nothing for you. Always follow stake-before-propose regardless of current height.
Adding a validator to a running chain — the correct sequence
This mirrors Becoming a Validator, with the reason each step is load-bearing. Follow that guide for exact commands.
- Generate the node identity and sync to the tip. Your
node-keys.jsonkey is both your stake-holding key and your block-signing key — they must be the same key. Let the node fully sync first so yourstaketx builds on the current tip. - Fund the key, then stake ≥
min_stake(10,000 XRGE). The freshly-generated key starts empty — first send ≥ 10,000 XRGE (+ 1 XRGE fee) to the validator address, then submit thestaketx from that key and wait for it to be included in a block. (Staking from an unfunded key fails on the fee/balance check.) This is the step that puts your key into the on-chain validator set that every peer derives. - Confirm you are in the active set before mining:
You wantrougechain --node-keys ~/.quantum-vault/mainnet/node-keys.json validator-status✓ Stakedand✓ In active set. Do not skip this — it is the difference between "my blocks are accepted" and "my blocks are silently rejected by every peer." - Only now start
--mine— and make the node reachable. Start with--mineand a public URL (--public-url https://…) that peers can reach over HTTPS, exactly as Becoming a Validator Step 4 shows. Peers talk to each other over the REST API; there is no separate P2P port. Being staked makes you eligible: your node votes toward each block's commit certificate, and it proposes only while it is the designated proposer (the largest stake). Being reachable is what lets your votes and blocks propagate.
Do these out of order — mine with a key that is not staked, or not the designated proposer — and your node produces nothing: it logs not the designated proposer for height <h> … — not sealing and peers would reject any such block anyway. That, not a height cliff, is the real thing to understand.
Detecting the failure mode
Symptoms fall into two distinct buckets — don't conflate them:
Your key isn't staked / not in the active set (fix: stake it, or point the node at the staked key's node-keys.json):
validator-statusshows✗on Staked or In active set — the fix is printed next to it (usually: stake, top up, or wait for thestaketx to confirm).- Peers log the rejection on the P2P import path — the inner error is
Block <h> rejected: proposer <pk> not in validator set with active stake(where<pk>is the first 16 hex chars of the key), wrapped as[peer] Failed to import block <h>: ….
You are staked and in the active set, but your node never proposes. This is expected unless you hold the most stake. Your miner logs not the designated proposer for height <h> (designated: <pk>) — not sealing, and GET /api/stats shows designated_proposer_next. If a block from you does reach peers while another validator is designated, they reject it with proposer <pk> is not the designated proposer (<pk>). Staked validators still earn their stake-weighted share of fees in every block (see Rewards).
designated_proposer_nextin/api/statsandproposerinGET /api/selectionboth name the designated proposer for the next height.
You are the designated proposer but blocks don't appear, or your node falls behind. Check reachability and finality:
--public-urlis not set, or points somewhere peers can't reach.- The API port behind your proxy is firewalled, or you have no connected peers.
- Your miner logs
waiting for the commit certificate of block <h> before sealing …: validators holding ⅔ of the stake have not voted for the tip yet, so no node can extend it. - You've been jailed — jailed validators are out of the active set until the jail window passes. (Automatic missed-block slashing and jailing are frozen from height 100.)
Block acceptance itself has no reachability component (it's proposer-auth, the designated-proposer rule, the parent commit certificate, signatures, hash and state), so with green validator-status checks a node that is designated but not propagating points at the network, not the stake.
Constraints worth knowing
min_stake: 10,000 XRGE, enforced by thestakeCLI/RPC submission path (top-ups must each be ≥ 10,000; totals accumulate). Note this is a submission-time guard, not a consensus-level rejection.- Active-set membership is
stake > 0and not-jailed — there is no minimum-stake threshold for being in the set. The 10,000 figure is an API-side minimum, not a membership cutoff; a validator with0 < stake < 10,000is still in the set. Which member proposes is decided by the designated-proposer rule (most stake), not by membership alone. max_validators(100 in genesis) is a declared parameter but is not currently enforced in the selection/sync path — everystake > 0, non-jailed validator is in the active set. Do not assume a top-100 cutoff exists today.- One node per key. Two nodes signing with the same key is equivocation; slashing on equivocation evidence is planned, not active (see Becoming a Validator → Security & slashing).
- Unbonding is 500 blocks after
unstake(blocks are only produced when there are transactions, so the wall-clock time depends on network activity); dropping to 0 stake (or being jailed) is what removes you from the active set. - Slashing, key security, and public-node hardening are covered in Becoming a Validator → Security & slashing.
A note for anyone reading the source
The proposer key and the staking key are the same key (enforced by the gate above). If you're spelunking the daemon, ignore the stale comment in rebuild_proposer_counts that says "Block headers use node ephemeral keys as proposer, not validator staking keys" — that's legacy accounting for a misconfigured node, not a supported "node key ≠ stake key" mode. produce_block sets proposer_pub_key to the node's own key, and import_block requires that key to be staked (and, from height 100, to be the designated proposer).
Changing PROPOSER_AUTH_ACTIVATION_HEIGHT
The 100 is a protocol constant on the sync path, not a knob to tune per deployment. Changing its value changes which blocks a syncing node accepts, so a change must be rolled out like a coordinated fork — every node running the new value before any chain crosses the affected height — exactly as with V2_FORK_HEIGHT. Do not edit it casually.
See also
- Becoming a Validator — full operator walkthrough
- Staking Overview
- Running a Node
- Public-node security
Staking Rewards
Validators earn a share of the transaction fees in every block on RougeChain.
Reward Sources
| Source | Description |
|---|---|
| Transaction fees | Your share of the tip pool from transactions in each block (proposer + stake-weighted validator share) |
There is no fixed block reward: validator income comes only from fees. The minimum-tip subsidy described below exists in the code but pays nothing on mainnet, because its reserve is empty.
How Rewards Work
- The designated proposer (the eligible validator with the most stake) proposes a block when there are pending transactions
- The proposer assembles pending transactions; each transaction's full signed fee is debited from its sender
- Fees are split, not paid entirely to the proposer: up to half of the base fee per transaction is burned (
burned = min(total fees, base fee × tx count ÷ 2)), and the remaining tip pool is distributed 20% to the proposer, 70% among all staked, non-jailed validators (stake-weighted), and 10% to the treasury. If the tip pool is below 0.1 XRGE the code tops it up from the__staking_rewards__reserve — which holds 0 XRGE on mainnet (2026-10-06; it was never funded), so no top-up happens. Every validator in the set is paid its stake-weighted share whether or not it voted on the block. - Rewards are credited when the block is applied to state
Fee Structure
| Transaction Type | Fee |
|---|---|
| Transfer (wallet-signed) | 1 XRGE |
| Token creation | 100 XRGE |
| Pool creation | 10 XRGE |
| Swap | 1 XRGE network fee, plus 0.3% of the input to liquidity providers |
| Add / remove liquidity | 1 XRGE |
| Stake / Unstake | 1 XRGE |
| Bridge withdrawal | 0.1 XRGE |
These are the fees the node binds to each signed transaction type (core/daemon/src/v2_binding.rs).
The tip portion of these fees (after the base-fee burn) is split across the proposer, all validators (stake-weighted), and the treasury — see How Rewards Work. Swap fees go to liquidity providers, not validators.
Estimated Returns
Rewards depend on:
- Your stake relative to total staked — determines your share of the 70% validator pool in every block
- Network activity — more transactions = more fees per block
- Whether you are the designated proposer — only the validator with the most stake proposes, and it also receives the 20% proposer share
Example
| Scenario | Value |
|---|---|
| Your stake | 10,000 XRGE |
| Total staked | 100,000 XRGE (illustrative — mainnet's total is about 100.1 M XRGE, over 99.9 % of it in one validator, so a 10,000 XRGE stake earns about 0.01 % of the validator pool today) |
| Your share | 10% |
| Your share of the validator pool | 10% of 70% of each block's tip pool |
| Avg fee per block | varies with network activity |
Your earnings are your ~10% share of the 70% validator pool in every block, whoever proposes it (plus the 20% proposer share only if you are the designated proposer). Blocks are produced as transactions arrive, so daily volume tracks real network activity — on a quiet chain that is low, and there is no fixed "blocks per day." These figures are illustrative, not a yield promise; actual returns vary with usage.
Compounding
Rewards are added to your balance, not your stake. To compound:
- Periodically stake your accumulated rewards
- This increases your share of the validator pool
- Leading to more rewards (and, if your stake becomes the largest, the proposer slot)
Checking Rewards
Via Web UI
Go to the Validators page to see your validator stats including blocks proposed.
Via API
# Check your balance (includes accumulated rewards)
curl "https://testnet.rougechain.io/api/balance/your-public-key"
# Check blocks proposed
curl "https://testnet.rougechain.io/api/validators"
Tax Considerations
Staking rewards may be taxable income in your jurisdiction. Keep records of:
- Amount staked
- Rewards received (block by block)
- Token price at time of receipt
- Unstaking transactions
RougeChain does not provide tax advice. Consult a tax professional.
Finality
RougeChain uses verified BFT finality (FINALITY_V2) since block 150.
| Status | |
|---|---|
| FINALITY_V2 (Release 2a) | LIVE (FINALITY_V2_ACTIVATION_HEIGHT = Some(150)) |
| Legacy finality indicator | Blocks 0–149 only |
What mainnet enforces from block 151: every block header carries parent_commit, a finality
certificate for its parent block — ML-DSA-65 precommits from validators holding more than two thirds
of total stake, checked against the validator set for that height. A node rejects a block without a
valid certificate, and a producer waits for its parent's certificate before building on it. Check any
height with GET /api/finality/:height.
What Release 2a does not include: a fallback proposer, skip certificates, rotating proposers and
slashing on evidence. Those were planned as "Release 2b" and "Release 3"; on 2026-10-06 they were
folded into a broader consensus redesign (Tendermint-style rounds, rotating stake-weighted proposers,
a capped active set, a consensus-enforced minimum stake, evidence-based slashing) that is decided
but not built — see Status & Roadmap. If the designated proposer is offline, no
block is produced until it returns. Three validators are staked and the largest holds more than
99.9 % of the stake, so the ⅔ quorum is currently met by that validator alone. No slashing of any
kind is active; the legacy slash transaction is rejected since block 245.
Legacy behavior (blocks 0–149)
Blocks are proposed and signed with ML-DSA-65 by staked validators, and every node validates
blocks and state independently. The finalized_height value and the /api/finality and
/api/votes endpoints report a node-local, informational finality indicator. In the legacy
implementation, votes are not cryptographically verified as a stake-weighted quorum and are not
exchanged between validators.
Treat legacy "finalized" as a status display, not as a BFT guarantee. Nothing that moves funds (block validity, the bridge, payouts) depends on it. The production bridge relies on its own checks and confirmation depths instead.
How FINALITY_V2 works
FINALITY_V2 replaces the legacy indicator with verifiable finality:
- Verified votes. Each vote is an ML-DSA-65 signature by the validator's own staked key over a domain-separated message (chain id, vote type, height, round, block hash).
- Recomputed quorum. A finality proof is only valid if the verifier recomputes the stake of the signers against the validator set for that height and it exceeds ⅔ of total stake. Votes for different block hashes never combine. Proofs are self-verifying, so a node can check a proof received from any peer.
- Durable anti-equivocation journal. Before signing, a validator durably records the vote. After a crash or restart it will refuse to sign a conflicting vote for the same height and round.
- Validator-set provenance. The validator set used for a height is derived by deterministic replay from a pinned base, not taken from a peer's claim.
- Automatic vote propagation. Votes are gossiped between validators with bounded, rate-limited intake, and finality proofs are distributed to non-validators.
- Operator preflight.
--finality-v2-preflightchecks that a node's key is the staked validator key before activation.
FINALITY_V2 is a coordinated validator upgrade, tested on multi-node devnets before activation. It went live at block 150 (2026-09-28). It is a prerequisite of the V3 XRGE bridge, which only signs withdrawal roots for finalized blocks; V3 itself is not activated.
Bridge Overview
Status (2026-10-06): the R1 production bridge is LIVE on Base mainnet. XRGE, qETH and qUSDC paths have been tested end-to-end. ETH and USDC deposits through the site go through the RougeBridge contract's
depositETH/depositERC20and are claimed automatically (paused for a few days and reopened 2026-10-05); the manual claim route is closed at the public endpoint. The bridge is operator-run (deposit watcher, relayers,bridge_mintkey) and has not been externally audited. Base-side authorization is classical: XRGE uses a Safe multisig plus a capped relayer key; qETH/qUSDC (RougeBridge) use a single operator owner/release key, with a 2-of-3 Safe multisig guardian that can pause the bridge and cancel queued large releases. The post-quantum V3 XRGE bridge is built but NOT activated — see V3 Post-Quantum XRGE Bridge and Status & Roadmap.
RougeChain has two separate bridge systems:
| System | Assets | Counterpart chain | Page |
|---|---|---|---|
| Base bridge (R1) | XRGE, qETH, qUSDC | Base mainnet | this page, XRGE, qETH, qUSDC |
| Bitcoin bridge | qBTC | Bitcoin | Bitcoin Bridge — separate architecture, not part of the Base bridge or V3 |
The rest of this page describes the Base bridge.
RougeChain supports bridging assets between Base mainnet (EVM, chain id 8453) and the RougeChain L1 network. The bridge uses a lock-and-mint / burn-and-release model with a dedicated smart contract. (Base Sepolia, chain id 84532, is supported for testing via BASE_CHAIN=sepolia.)
Supported Assets
| EVM Asset | RougeChain Asset | Decimals | Direction |
|---|---|---|---|
| ETH | qETH | 6 (L1 units) | Both ways |
| XRGE | XRGE | 18 (EVM) / whole units (L1) | Both ways |
| USDC | qUSDC | 6 | Both ways |
XRGE uses the
BridgeVaultV2contract; ETH and USDC use theRougeBridgecontract.
How It Works
Deposit (EVM → RougeChain)
- User deposits ETH or USDC into RougeBridge, or XRGE into BridgeVaultV2, on Base
- The deposit watcher in the relayer detects the deposit event and auto-claims it on L1 — the node verifies the deposit on-chain and mints the L1 asset (qETH, qUSDC or XRGE) to the recipient encoded in the deposit. No manual step is required.
- The manual claim endpoint still exists in the node (and claims are deduped, so an auto- and a manual claim of one deposit cannot double-mint), but on the public mainnet endpoint it is closed at the reverse proxy; it is an operator fallback, not a user step.
Every claim is verified against the actual on-chain Base deposit (the
Transferto the custody contract / vault) — never a caller-supplied amount — and requires a valid EVM signature plus a confirmation depth (QV_BRIDGE_MIN_CONFIRMATIONS, default 6) before minting.
Withdrawal (RougeChain → EVM)
- User submits a signed bridge_withdraw transaction on RougeChain, burning the wrapped token
- The withdrawal is recorded in the pending withdrawals store with its owner key, token,
and
status - The bridge relayer polls for pending withdrawals and releases the corresponding asset on Base
- Each withdrawal is paid at most once, and fulfillment is re-verified on Base at confirmation
depth. If a release keeps failing, the relayer reports it and the withdrawal is handled by the operators. Automatic refunds are disabled in production as part of the R1 hardening; status (
pending/failed/refunded) is exposed via the API and the Bridge page.
Security
See Bridge Security Model for the full trust model. In short:
- Client-side signing — Private keys never leave the browser. Withdraw transactions are signed locally using ML-DSA-65
- Classical Base-side authorization — releases on Base are authorized by ECDSA keys (capped hot relayer under a Safe multisig). This is hardened but not post-quantum; V3 changes it for XRGE only, once activated
- RougeBridge contract — Pausable, with guardian role for emergencies, timelock on large withdrawals
- Relayer authentication — The relayer uses a
BRIDGE_RELAYER_SECRETfor API authentication - Replay protection — Claimed transaction hashes are persisted to prevent double-claims; refunds use a
refund:<txId>key so they can't be issued twice - EVM signature verification — manual ETH claims require an EVM
personal_sign; auto-claims re-verify the deposit tx on-chain (sender, amount, confirmations)
Architecture
Base mainnet (EVM) RougeChain L1
┌──────────────────┐ ┌──────────────────┐
│ RougeBridge.sol │ deposit │ Node Daemon │
│ - depositETH() │──watcher─▶│ - /deposit/ │
│ - depositERC20()│ │ auto-claim │
│ - releaseETH() │◀─relayer──│ - /bridge/withdraw│
│ - releaseERC20()│ release │ - withdraw store │
│ BridgeVaultV2 │ │ - /…/refund │
│ - deposit() │──watcher─▶│ - /bridge/xrge/* │
│ - release() │◀─relayer──│ │
└──────────────────┘ └──────────────────┘
deposit watcher: Base → L1 auto-claim
relayer release + refund: L1 → Base
XRGE Bridge
Bridge XRGE tokens between Base mainnet and RougeChain L1 using the BridgeVaultV2 contract.
Status: LIVE (hardened R1 bridge), tested end-to-end. Releases on Base are authorized with classical keys (a capped relayer key under a Safe 2-of-3 multisig). The post-quantum V3 XRGE bridge is built but not activated; nothing on this page uses it.
Overview
Unlike qETH/qUSDC (which are wrapped assets), XRGE is the native token of RougeChain. The XRGE bridge allows moving XRGE between its ERC-20 representation on Base and the L1 network.
XRGE token on Base mainnet: 0x147120faEC9277ec02d957584CFCD92B56A24317
BridgeVaultV2: 0x7BB10752E99e8872d7D2DE5D92bfd43cd935Cd2D
Amounts on RougeChain are whole XRGE; the ERC-20 uses 18 decimals.
Deposit (Base XRGE → L1 XRGE)
- Approve the BridgeVaultV2 contract to spend your XRGE
- Call
deposit(amount, rougechainPubkey)on the vault - The vault locks your XRGE and emits a
BridgeDepositevent carrying your L1 key - The relayer's deposit watcher detects the event and auto-claims it — XRGE is credited to your L1 wallet after the node verifies the on-chain transfer. No manual claim is needed.
The manual fallback endpoint /api/bridge/xrge/claim requires an evmSignature (sign
the claim message with the wallet that sent the XRGE) and ignores any caller-supplied
amount — the amount and depositor are derived from the actual on-chain
Transfer(from → vault) log emitted by the XRGE token, and the deposit must reach the
required confirmation depth (QV_BRIDGE_MIN_CONFIRMATIONS, default 6) before it mints.
Withdraw (L1 XRGE → Base XRGE)
- Go to the Bridge page and use the XRGE Bridge Out tab
- Enter the amount and your Base EVM address
- Submit the signed withdrawal
- The relayer calls
release()on the vault to unlock your XRGE on Base, within the vault's per-transaction and daily caps. Each withdrawal is paid at most once. - If a release can't be completed, it is reported and handled by the operators. Automatic refunds are disabled in production. Status is shown on the Bridge page.
BridgeVaultV2 Contract
A lock-and-release contract owned by a Safe multisig (2-of-3), with a separate capped relayer role:
deposit(amount, rougechainPubkey)— Lock XRGE, emit event for relayerrelease(to, amount, l1TxId)— relayer role only, within per-transaction and rolling daily caps, while unpausedvaultBalance()— View how much XRGE the vault holds- Emergency withdrawal — multisig only, behind a 48-hour timelock
Liquidity in the vault = total XRGE locked by depositors minus released amounts.
ETH Bridge (qETH)
Bridge ETH from Base mainnet to RougeChain as qETH, and back.
Status: LIVE (hardened R1 bridge), tested end-to-end; deposits reopened 2026-10-05 through the contract's
depositETHwith automatic claiming. qETH uses classical Base-side authorization (the RougeBridge owner and release key is a single operator hot key; unaudited) and is outside the scope of the V3 post-quantum XRGE bridge.
Deposit (ETH → qETH)
Step 1: Send ETH to the Bridge
Connect your MetaMask (or other EVM wallet) to Base and call depositETH(rougechainPubkey)
on the RougeBridge contract (the Bridge page does this for you), passing your RougeChain
public key. This emits a BridgeDepositETH event carrying your recipient key.
Step 2: Automatic claim
The relayer's deposit watcher sees the event and claims it for you — qETH is minted
to the RougeChain key you encoded in the deposit, once the deposit reaches the required
confirmation depth (QV_BRIDGE_MIN_CONFIRMATIONS, default 6 Base confirmations).
No manual claim step is needed.
The node verifies before minting:
- The transaction exists on Base and was sent to the correct contract
- The amount and sender match the deposit
- The transaction has at least the required confirmation depth (default 6)
- It hasn't been claimed before (auto-claim and manual claim share one dedup store)
Manual claim: the node's manual claim route (POST /api/bridge/claim, which needs a signature
from the depositing EVM wallet) is closed at the public mainnet endpoint. A deposit that the
watcher has not credited is handled by the operators; the Bridge page tracks the deposit until it is
credited.
Conversion Rate
1 ETH = 1,000,000 qETH units (6 decimal precision). Deposit amounts should be multiples of 0.000001 ETH (1e12 wei); smaller remainders cannot be represented on L1.
For example, depositing 0.01 ETH gives you 10,000 qETH units.
Withdraw (qETH → ETH)
- Go to the Bridge page and select the Bridge Out tab
- Select ETH as the token
- Enter the amount of qETH to bridge out
- Enter the Base address to receive ETH
- Click Bridge Out
The transaction is signed client-side (your private key never leaves the browser), then:
- qETH is burned on RougeChain
- A pending withdrawal is created
- The bridge relayer picks it up and sends ETH to your EVM address
- Release status (
pending/failed/refunded) shows on the Bridge page. Automatic refunds are disabled in production; a release that can't be completed is handled by the operators.
A 0.1 XRGE fee is charged for the withdrawal transaction.
Fees
| Operation | Fee |
|---|---|
| Deposit (ETH → qETH) | Gas on Base only |
| Withdraw (qETH → ETH) | 0.1 XRGE |
USDC Bridge (qUSDC)
Status: LIVE (hardened R1 bridge), tested end-to-end; deposits reopened 2026-10-05 through the contract's
depositERC20with automatic claiming (the manual claim route is closed at the public endpoint). qUSDC uses classical Base-side authorization (single operator hot key; unaudited) and is outside the scope of the V3 post-quantum XRGE bridge.
qUSDC is a 1:1 representation of USDC on Base mainnet on RougeChain, with 6 decimals. It uses
the same RougeBridge contract as qETH (0x0c09C764AdC024497729cd452ECfeE8869d35d83).
USDC on Base mainnet: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Deposit (USDC → qUSDC)
- Approve
RougeBridgeto spend your USDC. - Call
depositERC20(usdc, amount, rougechainPubkey)(the Bridge page does this for you). - The relayer's deposit watcher claims the deposit automatically once it reaches the required confirmation depth (default 6). The node verifies the on-chain deposit before minting qUSDC.
Withdraw (qUSDC → USDC)
- On the Bridge page choose Bridge Out and select USDC.
- Enter the amount and your Base address, then submit the signed withdrawal.
- qUSDC is burned on RougeChain and the relayer releases USDC on Base. Each withdrawal is paid at most once. Automatic refunds are disabled in production; failed releases are handled by the operators.
Bitcoin Bridge (BTC ⇄ qBTC)
Status (2026-10-06): deposits live, withdrawals switched off. The Bitcoin bridge is independent of the Base bridge (R1) and is not part of the V3 XRGE bridge. On mainnet, BTC → qBTC deposits are verified and credited. qBTC → BTC withdrawals are disabled in the site because the Bitcoin relayer has been stopped since the F49 upgrade while a hardening of its payout path awaits deployment; a withdrawal submitted directly to the API would wait unpaid until the relayer is reopened. Do not send significant value; check Status & Roadmap.
Bridges native Bitcoin to qBTC on RougeChain L1 and back. qBTC held on RougeChain is controlled by ML-DSA-65 keys. The BTC backing it sits in a Bitcoin custody address protected by Bitcoin's own (classical) signatures, so the bridge custody itself is not post-quantum.
qBTC uses 8 decimals — 1 on-chain unit = 1 satoshi.
Why it's built differently from the ETH/USDC bridges
Bitcoin has no event logs and no EVM signatures, so none of the eth_getLogs / ECDSA machinery
applies. Two design choices carry the security:
- Recipient binding. Every deposit is bound to a RougeChain recipient before any qBTC is
minted, in one of two ways:
- Per-user deposit address (default). Each RougeChain address is assigned its own Bitcoin
deposit address, stable per recipient. These addresses are derived (BIP84) from the BTC
relayer's HD wallet and registered with the daemon, which only stores the
address ↔ recipient binding and watches it. Send from any wallet, no
OP_RETURNneeded. OP_RETURNfallback. A depositor sends to the custody address and writes their RougeChain address into anOP_RETURNoutput of that same transaction. The binding is baked into a signed Bitcoin transaction, so knowing a txid does not let anyone redirect the mint.
- Per-user deposit address (default). Each RougeChain address is assigned its own Bitcoin
deposit address, stable per recipient. These addresses are derived (BIP84) from the BTC
relayer's HD wallet and registered with the daemon, which only stores the
address ↔ recipient binding and watches it. Send from any wallet, no
- Two-provider cross-check. Every deposit is verified against two independent Esplora providers (mempool.space + blockstream.info by default). Both must agree on the custody amount and the recipient and both must meet the confirmation depth, so no single API can fabricate a deposit. If a provider is unreachable the daemon fails closed (the claim is idempotent and pollable, so an outage only delays).
The daemon holds no Bitcoin key. It only watches the custody address and the assigned deposit addresses. The hot keys (custody and the HD deposit wallet) live only in the external BTC relayer, which sweeps deposit addresses into custody and pays withdrawals out; the daemon then re-verifies each payout on the Bitcoin chain before marking it fulfilled.
Deposit flow (BTC → qBTC)
Per-user deposit address
- The frontend calls
POST /api/bridge/btc/deposit-address{ recipient }with the user's RougeChain address and gets back that recipient's deposit address (the same one every time). - User sends BTC to it from any wallet — no
OP_RETURN. - The daemon polls assigned addresses, cross-checks each deposit on both providers, and once it
has the required confirmations mints qBTC (sats) to the bound recipient. No claim call is needed.
Dedupe key:
btcaddr:{txid}:{vout}.
OP_RETURN fallback
- User sends BTC to the custody address with an
OP_RETURNoutput containing their RougeChain address. (Wallets that support OP_RETURN: Sparrow, Electrum, BlueWallet advanced, bitcoinjs.) - User submits the txid to
POST /api/bridge/btc/claim{ btcTxid, recipientRougechainPubkey? }. The frontend polls this until it succeeds. - The daemon cross-checks both providers, then mints qBTC (sats) to the OP_RETURN recipient.
Dedupe key:
btc:{txid}.
Withdraw flow (qBTC → BTC)
- User burns qBTC via
POST /api/bridge/withdrawwithtokenSymbol: "qBTC",evmAddress: <destination BTC address>,amountUnits: <sats>(signed). Amounts below the minimum (btcMinWithdrawSats, default 2 000 sats) are rejected before anything is burned. - The withdrawal appears in
GET /api/bridge/btc/withdrawals. The BTC relayer (scripts/btc-bridge-relayer.ts) builds, signs, and broadcasts the Bitcoin payout from custody, sending the destination the burned amount minus the payout's Bitcoin network fee. - Once confirmed, the relayer calls
DELETE /api/bridge/btc/withdrawals/:txId{ btcTxid }. The daemon verifies the payout on-chain (custody-funded, enough confirmations, and paid the recipient the owed sats minus at most the capped network fee) before marking it fulfilled.
Fees and minimum
The withdrawer pays the Bitcoin network fee. For a withdrawal of N sats the relayer builds a
payout transaction and sends the destination N − fee, where fee is that transaction's real
network fee (its virtual size × the current fee rate, rounded up). Custody's change is
inputs − N, so custody's balance falls by exactly the N sats that were burned.
Example: a 5 000-sat withdrawal at 1 sat/vB. A typical payout (one custody input, destination + change outputs) is 141 vB, so the fee is 141 sats and the destination receives 4 859 sats.
| Rule | Value |
|---|---|
| Minimum withdrawal | btcMinWithdrawSats from GET /api/bridge/config (default 2 000 sats) |
| Network-fee cap | btcMaxNetworkFeeSats (default 10 000 sats). When fees are higher the payout waits until they drop — it is never paid with a larger fee. |
| Tiny remainders | If N − fee would be at or below the Bitcoin dust limit (546 sats) at current fees, the payout is not sent and the withdrawal is held for manual review — never silently lost. |
The site shows an estimate of the fee (mempool.space rate × ~141 vB) and "you receive ≈ amount − fee"; the exact fee depends on the transaction the relayer builds.
Node verification rule. A payout is accepted when it is funded by custody, has the required
confirmations, pays the destination a non-zero amount, and either pays the full owed amount
(the pre-fee-policy form, still accepted) or paid + fee ≥ owed with fee ≤ the cap. The node
computes fee itself from the Bitcoin transaction data (sum of inputs minus sum of outputs,
cross-checked against the provider's reported fee); it never takes the fee from the relayer.
Operators
Deployment and operations notes for the BTC relayer live in the repository's
scripts/README.md.
Bridge Security Model
This page states plainly who can authorize what, today and under V3. See Status & Roadmap for what is live.
Today: the hardened R1 bridge (LIVE)
| Layer | Protection |
|---|---|
| RougeChain side | Withdrawals are ML-DSA-65-signed transactions. A withdrawal either burns and records a payout atomically or fails with a receipt — it cannot burn without a payout record or create a payout without a burn. |
| Deposits | Every mint is verified against the actual Base transaction (contract, sender, amount) at a confirmation depth (default 6). Claims are de-duplicated, so a deposit cannot mint twice. |
| Payouts | Each withdrawal is paid at most once; fulfillment is re-verified on Base at confirmation depth before it is marked complete. |
XRGE vault (BridgeVaultV2) | Owned by a Safe multisig (2-of-3). A separate hot relayer key can only call release() within per-transaction and rolling daily caps, and only while unpaused. Emergency withdrawal is behind a 48-hour timelock. |
qETH / qUSDC (RougeBridge) | The RougeBridge owner is currently a single operator key, not a multisig; that key also performs releases. The guardian role is held by a 2-of-3 Safe multisig, which can pause the bridge and cancel queued large releases. Other protections include the 24-hour timelock on large releases and daemon-side controls. Migrating ownership to multisig control is a planned security improvement. Releases are also protected against replay by L1 transaction id. |
| Operations | Daemon-side kill switch and per-transaction cap; automatic refunds are disabled in production (failed releases are handled manually). |
The trust assumption: releases on Base are authorized by classical ECDSA keys (the relayer key and the Safe owners). These are conventional, quantum-vulnerable signatures. The R1 bridge limits the damage a compromised relayer key can do (caps, pause, multisig control), but it does not make the Base side post-quantum. The bridge is also operated by a small team, not a decentralized set.
V3 (NOT ACTIVATED): XRGE only
V3 moves XRGE release authorization to an M-of-N ML-DSA-65 authority verified on-chain, on top of verified RougeChain finality. Classical keys keep only limited roles (pausing, and actions that are themselves gated by the post-quantum authority). Details: V3 Post-Quantum XRGE Bridge, Authority Rotation.
| Asset | Today | After V3 activation |
|---|---|---|
| XRGE | R1, classical Base-side authorization | ML-DSA-65 authorization on Base |
| qETH | R1, classical Base-side authorization | Unchanged (outside V3 scope) |
| qUSDC | R1, classical Base-side authorization | Unchanged (outside V3 scope) |
| qBTC | Separate Bitcoin bridge | Unchanged (separate system) |
What V3 does not fix
- Base itself, the XRGE ERC-20, and users' EVM wallets use classical cryptography.
- qETH/qUSDC custody remains classical.
- Bitcoin custody uses Bitcoin's own (classical) signatures.
- V3 is unaudited until the external review completes.
V3 Post-Quantum XRGE Bridge
Status: AUDIT CANDIDATE — NOT ACTIVATED.
BridgeVaultV3is not deployed to Base mainnet and does not hold any XRGE. The production XRGE bridge is the R1 bridge described in XRGE Bridge. See Status & Roadmap.
What V3 changes
Today, XRGE is released on Base by a classical (ECDSA) relayer key under a Safe multisig. V3 replaces that authorization with ML-DSA-65 (FIPS 204) signatures verified on-chain on Base:
- RougeChain finalizes blocks with verified validator votes (FINALITY_V2).
- Finalized XRGE withdrawals are grouped into epochs. Each epoch has a deterministic Merkle root that any node can recompute from chain history.
- An M-of-N post-quantum root authority signs the epoch root with ML-DSA-65. Each authority member independently reconstructs the root from finalized history before signing — a member never signs a root just because another machine supplied the hash.
BridgeVaultV3accepts the root only if an on-chain verifier confirms the threshold of ML-DSA-65 signatures. No ECDSA signature, pairing check or SNARK wrapper is in the authorization path.- A user (or anyone on their behalf) releases XRGE with a Merkle proof against an accepted root. Each withdrawal can be released once.
The intended property: an attacker who holds every classical Base-side key (Safe, guardian, relayer, servers) but not the post-quantum authority threshold cannot get a forged root accepted, move XRGE, or weaken the vault's controls.
Verifier architecture (table-in-proof)
- The verifier contract is immutable and stateless (no owner, no upgrade path).
- Authority public keys live in immutable, content-addressed authority tables on Base. The proof names the table it uses; the vault stores only a commitment to the current authority set.
- Keys are held in canonical (sorted) order, and signer indexes must be strictly increasing, so a proof has one valid encoding.
What has been done
- Vault, verifier and ML-DSA-65 core are frozen as Audit Candidate 1 with pinned compilers, vendored dependencies and a byte-for-byte reproducible build.
- Test coverage includes NIST ACVP ML-DSA-65 vectors, cross-language differential tests (Rust ⇄ JavaScript ⇄ EVM), malformed-proof and attack suites, and end-to-end 2-of-3 and 3-of-5 runs with the real Rust signer.
- The exact audit-candidate bytecode was deployed and exercised on a local OP-stack devnet
(real
op-geth/op-node), using throwaway keys and a throwaway ERC-20. Measured gas is about 3.4M (2-of-3) to 5.0M (3-of-5) per root verification.
What has NOT been done
- No external audit yet.
- No deployment to Base mainnet (or any public network).
- No production authority keys; the authority size (e.g. 2-of-3 vs 3-of-5) is not decided.
- No activation height chosen; no XRGE migrated.
Out of scope
- qETH and qUSDC stay on
RougeBridgewith classical Base-side authorization. - Bitcoin / qBTC is a separate bridge and is not part of V3.
Known limitations (to be reviewed by auditors)
- The Keccak-f[1600] helper used by the verifier is third-party raw EVM bytecode with no high-level source; it is pinned by code hash and covered by behavioural tests only.
- The ML-DSA-65 core is third-party assembly close to the EVM contract size limit.
- RougeChain validator stake is currently concentrated, which limits what finality proves.
Authority Rotation (V3)
Status: BUILT AND TESTED — NOT IN USE. This describes the V3 XRGE bridge, which is not activated. No production authority exists yet.
The V3 post-quantum root authority is an M-of-N set of ML-DSA-65 keys. Rotation replaces that set without upgrading any contract.
Design
- The vault and verifier are immutable. The vault stores a commitment to the current authority set (version, threshold and member key hashes).
- A new set is published as a new immutable authority table on Base, together with the pre-expanded key data the verifier needs.
- The current authority signs a rotation to the new commitment with ML-DSA-65. The vault enforces a 48-hour timelock between proposing and executing a rotation.
- A rotation manifest binds the Base-side rotation to a RougeChain-side authority schedule record, so every node derives the same "which authority signs which epoch" answer from chain history.
Safety checks before anything is signed
- The rotation signer reconstructs the new set commitment from the actual keys and the on-chain table contents before signing; it does not sign a supplied hash.
- Every member of the new set must take part in the ceremony, and at least two independent checkers must confirm the on-chain tables match the intended keys.
- A durable signing journal prevents signing two different rotations for the same version.
- Root signing for an epoch is refused until the matching schedule record and Base-side state are both observed (publication gate).
Known hazard
The vault cannot check that a new commitment is well-formed. A rotation to a wrong commitment would leave the new authority unable to sign (the bridge fails closed — funds cannot move, but they are not exposed). The checks above exist to prevent this, and the design is flagged for external review.
Rehearsal status
Rotation has been rehearsed in tests and proposed on a local OP-stack devnet. Execution after the real 48-hour timelock is the remaining rehearsal step.
RougeBridge Contract
The RougeBridge.sol contract is a multi-asset bridge contract deployed on Base mainnet (0x0c09C764AdC024497729cd452ECfeE8869d35d83) that handles ETH and ERC-20 deposits/releases with enhanced security features.
Features
- Multi-token support — ETH and any ERC-20 token (USDC, etc.)
- Pausable — Guardian can pause all operations in emergencies
- Timelock — Large withdrawals require a delay period before execution
- Guardian role — Separate from owner; can pause but cannot withdraw
- Replay protection — Processed L1 transaction IDs are tracked to prevent double-releases
- Ownership — The RougeBridge owner is currently a single operator key, not a multisig; that key also performs releases. The guardian role is held by a 2-of-3 Safe multisig, which can pause the bridge and cancel queued large releases. Other protections include the 24-hour timelock on large releases and daemon-side controls. Migrating ownership to multisig control is a planned security improvement.
Key Functions
Deposits
function depositETH(string calldata rougechainPubkey) external payable;
function depositERC20(address token, uint256 amount, string calldata rougechainPubkey) external;
Users call these to lock assets in the contract. Events are emitted for the relayer to pick up.
Releases (Owner Only)
function releaseETH(address to, uint256 amount, bytes32 l1TxId) external;
function releaseERC20(address token, address to, uint256 amount, bytes32 l1TxId) external;
The relayer (owner) calls these to release assets when a user burns their wrapped tokens on L1. If the amount exceeds largeWithdrawalThreshold, a timelock is queued instead.
Timelock
function executeTimelock(uint256 requestId) external; // Owner, after delay
function cancelTimelock(uint256 requestId) external; // Guardian
Large releases are queued with a configurable delay (default 24 hours). The guardian can cancel suspicious requests during the delay window.
Admin
function pause() external; // Guardian or Owner
function unpause() external; // Owner only
function setGuardian(address newGuardian) external; // Owner
function setSupportedToken(address token, bool) external;// Owner
function setLargeWithdrawalThreshold(uint256) external; // Owner
Events
| Event | Description |
|---|---|
BridgeDepositETH | ETH deposited for bridging |
BridgeDepositERC20 | ERC-20 deposited for bridging |
BridgeReleaseETH | ETH released to user |
BridgeReleaseERC20 | ERC-20 released to user |
TimelockQueued | Large release queued with delay |
TimelockExecuted | Queued release executed |
TimelockCancelled | Queued release cancelled by guardian |
Deployment
The XRGE bridge is live on Base mainnet (chain ID 8453) as BridgeVaultV2 at 0x7BB10752E99e8872d7D2DE5D92bfd43cd935Cd2D. Its owner is a Gnosis Safe multisig (2-of-3) — no single key can move or drain the vault. Routine releases are signed by a separate hot relayer key that may only call release() within on-chain per-transaction and rolling daily caps and only while unpaused; the multisig alone can rotate that key, change the caps, pause, or (behind a 48-hour timelock) emergency-withdraw. The ETH/USDC custody contract is at 0x0c09C764AdC024497729cd452ECfeE8869d35d83. Base Sepolia (chain ID 84532) is used only for testing.
BridgeVaultV3(post-quantum XRGE vault) is not deployed on Base mainnet. See V3 Post-Quantum XRGE Bridge.
Migration note: the previous single-owner XRGE vault (
0xb3f5…bFAb5) is being retired in favor ofBridgeVaultV2; do not deposit to the old address.
Daemon Withdraw Guardrails
Independent of the on-chain contracts, the RougeChain daemon enforces its own withdraw controls (an operator pause, a per-transaction cap, and a required Base confirmation depth for deposit claims, default 6). Operator configuration is documented in the repository's scripts/README.md.
Bridge Relayer
The bridge relayer is an off-chain process that connects RougeChain L1 with Base. It fulfills withdrawals (L1 → Base), watches for deposits to auto-claim (Base → L1), and can refund withdrawals that cannot be released (disabled in production).
For the full configuration table and operational notes, see
scripts/README.md.
How It Works
- Polls the node for pending ETH, USDC and XRGE withdrawals and releases the corresponding asset on Base
- Marks each release fulfilled; on repeated failure, reports it. Auto-refund exists but is disabled in production (
AUTO_REFUND=false) - Deposit watcher: scans the bridge contracts for deposit events and auto-claims them on L1
- Alerts (console + optional webhook) on repeated failures
Running the Relayer
The relayer and the daemon share bridge-relayer.env (copy from bridge-relayer.env.example):
# Key variables (see bridge-relayer.env.example for the full list)
CORE_API_URL="http://localhost:5100"
BRIDGE_CUSTODY_PRIVATE_KEY="0x..." # EVM private key for the bridge wallet
BRIDGE_RELAYER_SECRET="your-secret" # Shared secret for API authentication
BASE_CHAIN="mainnet"
BASE_RPC_URL="https://mainnet.base.org" # NOTE: var name is BASE_RPC_URL
ROUGE_BRIDGE_ADDRESS="0x..." # RougeBridge contract address
XRGE_BRIDGE_VAULT="0x..." # BridgeVaultV2 contract address
AUTO_REFUND="false" # Production setting: failed withdrawals are handled manually
DEPOSIT_WATCHER="true" # Auto-claim deposits
# ALERT_WEBHOOK_URL= # Optional Slack/Discord webhook
# Run as a singleton under a process supervisor — never run two relayers against one wallet
npx tsx scripts/bridge-relayer.ts
Authentication
The relayer authenticates with the node using the BRIDGE_RELAYER_SECRET environment variable. This is sent as the x-bridge-relayer-secret HTTP header when marking withdrawals as fulfilled.
Set the same secret on both the relayer and the node:
# On the node
export BRIDGE_RELAYER_SECRET="your-secret"
# On the relayer
export BRIDGE_RELAYER_SECRET="your-secret"
Contract Mode vs Legacy Mode
- With
ROUGE_BRIDGE_ADDRESS— The relayer callsreleaseETH()/releaseERC20()on the RougeBridge contract - Without it — Falls back to raw ETH transfers from the custody wallet (legacy mode)
- With
XRGE_BRIDGE_VAULT— Enables XRGE bridge support via the BridgeVault contract
Daemon Withdraw Guardrails
Independent of the on-chain contracts, the RougeChain daemon enforces its own withdraw controls (an operator pause, a per-transaction cap, and a required Base confirmation depth for deposit claims, default 6). Operator configuration is documented in the repository's scripts/README.md.
Security Considerations
-
The relayer signs Base transactions with a classical ECDSA key. This is the R1 production design; the V3 XRGE bridge (not activated) removes this key from XRGE authorization
-
The relayer's EVM private key should be stored securely (not in code)
-
Use a dedicated wallet with limited funds for the relayer
-
The RougeBridge owner is currently a single operator key, not a multisig; that key also performs releases. The guardian role is held by a 2-of-3 Safe multisig, which can pause the bridge and cancel queued large releases. Other protections include the 24-hour timelock on large releases and daemon-side controls. Migrating ownership to multisig control is a planned security improvement.
-
The
BRIDGE_RELAYER_SECRETshould be a strong random string
DEX & AMM
RougeChain includes a built-in decentralized exchange (DEX) powered by an Automated Market Maker (AMM) using the constant product formula (x * y = k).
Overview
- Create pools for any token pair
- Swap between tokens with slippage protection
- Provide liquidity and earn fees from trades
- Multi-hop routing for tokens without direct pools
All operations are signed client-side with ML-DSA-65 — your private key never leaves the browser.
Fee Structure
| Operation | Fee |
|---|---|
| Create Pool | 10 XRGE |
| Swap | 0.3% of input + 1 XRGE tx fee |
| Add Liquidity | 1 XRGE |
| Remove Liquidity | 1 XRGE |
The 0.3% swap fee is distributed to liquidity providers proportional to their share of the pool.
Pool Mechanics
Pools use the Constant Product Market Maker model:
reserve_a × reserve_b = k (constant)
When a user swaps token A for token B:
- Token A is added to the pool
- Token B is removed, maintaining the constant product
- 0.3% fee is taken from the input amount
- Price impact increases with larger trades relative to pool size
LP Tokens
When you provide liquidity, you receive LP tokens representing your share of the pool. When you remove liquidity, you burn LP tokens and receive your proportional share of both tokens in the pool.
Liquidity Pools
Creating a Pool
To create a new trading pool:
- Go to the Swap page
- Click Create Pool
- Select two tokens (e.g., XRGE / qETH)
- Enter the initial amounts for each token
- Click Create Pool
The initial token ratio sets the starting price. A 10 XRGE fee is charged for pool creation.
Adding Liquidity
- Go to the pool page
- Click Add Liquidity
- Enter the amount of one token — the other amount auto-calculates based on the current ratio
- Confirm the transaction
You receive LP tokens proportional to your contribution.
Removing Liquidity
- Go to the pool page
- Click Remove Liquidity
- Enter the LP token amount to withdraw
- You receive your proportional share of both tokens
Impermanent Loss
If the price ratio changes significantly from when you deposited, you may experience impermanent loss compared to simply holding the tokens. This is standard AMM behavior.
Pool Stats
Each pool shows:
- Current reserves for both tokens
- Total LP tokens issued
- Your LP share percentage
- 24h volume and fees earned
- Price history chart
Swaps
Making a Swap
- Go to the Swap page
- Select the input token and output token
- Enter the amount to swap
- Review the quote (output amount, price impact, minimum received)
- Click Swap
Slippage Protection
Set your maximum slippage tolerance to protect against price movements during your transaction. If the actual output would be less than your minimum, the transaction is rejected.
Default slippage: 0.5%
Price Impact
Price impact shows how much your trade will move the pool price. Large trades relative to pool size have higher price impact.
| Price Impact | Severity |
|---|---|
| < 1% | Low |
| 1-5% | Medium |
| > 5% | High (warning shown) |
Multi-Hop Routing
If no direct pool exists between your tokens, the router automatically finds a path through XRGE:
TOKEN_A → XRGE → TOKEN_B
This uses two swaps internally but is handled in a single transaction.
Fees
- 0.3% of the input amount goes to liquidity providers
- 1 XRGE base transaction fee
Token Creation
Create custom tokens on RougeChain. Tokens can be traded on the built-in AMM/DEX.
Overview
| Property | Value |
|---|---|
| Creation fee | 100 XRGE |
| Supply | Set at creation — the full supply is minted to the creator. Fixed unless the token is created mintable (creator can mint more, optional cap; live since block 235) — see Mintable tokens |
| Decimals | Not configurable — user-created tokens are whole units (0 decimals) |
| Trading | Via AMM liquidity pools |
Create a Token
Via Web UI
- Navigate to the Token Explorer page
- Click Create Token
- Fill in token details:
- Name — Full name (e.g., "My Token")
- Symbol — Ticker symbol (e.g., "MTK")
- Total Supply — Full supply, minted to you at creation
- Logo — Upload an image or paste a URL (optional)
- Confirm and sign the transaction
Uploaded logos are compressed to WebP (max 256×256) and stored on-chain as base64 data URIs. The node rejects an inline data: logo larger than 32 KiB (and a logo URL longer than 2,048 bytes), so for a detailed image host it and paste a URL. Logos display across the wallet, swap, pools, and explorer.
Via SDK
import { RougeChain, Wallet } from "@rougechain/sdk";
const rc = new RougeChain("https://testnet.rougechain.io/api");
const wallet = Wallet.generate();
await rc.createToken(wallet, {
name: "My Token",
symbol: "MTK",
totalSupply: 1_000_000,
image: "https://example.com/logo.png", // or a data:image/webp;base64,... URI
});
Via v2 API
The request body is the signed envelope { payload, signature, public_key }. The from, timestamp, and nonce fields go inside payload, and from must equal the signing public key.
curl -X POST https://testnet.rougechain.io/api/v2/token/create \
-H "Content-Type: application/json" \
-d '{
"payload": {
"token_name": "My Token",
"token_symbol": "MTK",
"initial_supply": 1000000,
"image": "https://example.com/logo.png",
"from": "your-public-key-hex",
"timestamp": 1706745600000,
"nonce": "random-hex"
},
"signature": "your-ml-dsa65-signature-hex",
"public_key": "your-public-key-hex"
}'
Raw-HTTP field names are snake_case: token_name (1–64 chars), token_symbol (1–10 chars, no whitespace, must be unused), initial_supply (> 0), plus optional image and description. The 100 XRGE fee is set by the node regardless of any fee in the payload. There is no decimals field.
Response
{
"success": true,
"token_symbol": "MTK",
"message": "Token creation transaction submitted"
}
The token exists once the transaction is included in a block.
After Creation
Once created, the entire supply is credited to the creator's wallet. You can then:
- Transfer tokens to other wallets
- Create a liquidity pool to enable trading
- Burn tokens by sending to the burn address
Mintable tokens (active since block 235)
Status: LIVE on mainnet since block 235 and on testnet since block 1360 (
TOKEN_MINTING).GET /api/stats→upgrade_schedule.token_mintingshows the height. On a network where the height has not been reached the node refusesmintable/max_supplyon/api/v2/token/createwithtoken minting is not active yetand refuses every/api/v2/token/mint.
A token can be created mintable, with an optional cap. Add to the signed create payload:
| Field | Type | Meaning |
|---|---|---|
mintable | boolean | true: the creator may mint more later. Omit (or false) for a fixed-supply token. |
max_supply | integer | Optional cap on total issuance (initial supply + everything ever minted). Requires mintable: true; must be ≥ initial_supply. Omit for no cap. |
Rules (enforced by every node when the transaction is included in a block):
- Only the token's creator (the key that signed the
create_token) can mint. - Only tokens created mintable at or after the activation height can be minted; tokens created earlier stay fixed-supply.
amountandmax_supplyare JSON integers (a float or string is refused), at most 9,007,199,254,740,991 (2^53 − 1). A mintable token'sinitial_supplyhas the same bound.initial_supply + total_minted + amountmust stay ≤max_supplywhen a cap is set. Burning does not free room: the cap bounds issuance, not circulating supply.- A mint costs a 1 XRGE fee. A mint that breaks a rule when its block is built is skipped (no fee, no tokens); minting exactly up to the cap is allowed.
total_mintedis updated when the mint is included in a block, not when it is submitted.
# create (from activation)
curl -X POST https://testnet.rougechain.io/api/v2/token/create -H "Content-Type: application/json" -d '{
"payload": { "token_name": "My Token", "token_symbol": "MTK", "initial_supply": 1000000,
"mintable": true, "max_supply": 5000000,
"from": "your-public-key-hex", "timestamp": 1706745600000, "nonce": "random-hex" },
"signature": "...", "public_key": "your-public-key-hex" }'
# mint (creator only)
curl -X POST https://testnet.rougechain.io/api/v2/token/mint -H "Content-Type: application/json" -d '{
"payload": { "token_symbol": "MTK", "amount": 250000,
"from": "your-public-key-hex", "timestamp": 1706745600001, "nonce": "random-hex" },
"signature": "...", "public_key": "your-public-key-hex" }'
GET /api/token/:symbol/metadata and GET /api/tokens report mintable (true only for a token a
block created mintable), max_supply (null = uncapped), total_minted, and for a mintable token
initial_supply and mint_enabled_height. GET /api/token/:symbol/holders counts minted supply in
total_supply.
With the SDK (1.12.0+)
if (await rc.isTokenMintingActive()) { // reads /api/stats upgrade_schedule.token_minting
await rc.createToken(wallet, { name: "My Token", symbol: "MTK", totalSupply: 1_000_000,
mintable: true, maxSupply: 5_000_000 }); // maxSupply optional
await rc.mintTokens(wallet, { symbol: "MTK", amount: 250_000 }); // signed mint_tokens, 1 XRGE
}
The SDK validates the numbers before signing (integers, maxSupply ≥ totalSupply, at most
2^53 − 1) and returns { success: false, error } instead of posting an invalid request.
On rougechain.io
The Create a token dialog shows a Mintable option (and an optional Max supply) only on a network where the upgrade is active. On a mintable token's page in the Explorer, the token's creator sees Mint more (amount, room left under the cap, 1 XRGE fee); it signs with the local wallet or the connected extension / Qwalla. Everyone sees the initial, minted and max supply.
Creating a Liquidity Pool
To make your token tradeable on the DEX:
curl -X POST https://testnet.rougechain.io/api/v2/pool/create \
-H "Content-Type: application/json" \
-d '{
"payload": {
"token_a": "XRGE",
"token_b": "MTK",
"amount_a": 1000,
"amount_b": 10000,
"from": "your-public-key-hex",
"timestamp": 1706745600001,
"nonce": "random-hex"
},
"signature": "your-signature-hex",
"public_key": "your-public-key-hex"
}'
This creates an XRGE/MTK pool with an initial price of 0.1 XRGE per MTK.
Pool creation costs 10 XRGE.
Token Burning
Send tokens to the burn address to permanently remove them from circulation:
XRGE_BURN_0x000000000000000000000000000000000000000000000000000000000000DEAD
Burned amounts are tracked on-chain and queryable via GET /api/burned.
Listing on the DEX
Tokens are automatically listed on the DEX once a liquidity pool is created. Users can then:
- Swap between your token and XRGE
- Add/remove liquidity
- View price charts and pool stats
Token Metadata
Every token has on-chain metadata that the creator can manage:
| Field | Description |
|---|---|
image | Logo URL or base64 data URI |
description | Token description |
website | Project website |
twitter | X/Twitter handle |
discord | Discord invite link |
Updating Metadata
Only the original creator can update metadata:
await rc.updateTokenMetadata(wallet, {
symbol: "MTK",
image: "data:image/webp;base64,UklGR...",
description: "A community token for...",
website: "https://mytoken.io",
twitter: "@mytoken",
discord: "discord.gg/mytoken",
});
Logo images can be:
- URLs —
https://...,ipfs://... - Data URIs —
data:image/webp;base64,...(stored directly on-chain, persists forever)
The web UI provides an Upload button that compresses images to WebP and stores them as base64 on-chain.
Token Standards
RougeChain tokens are native protocol-level assets (not smart contract tokens). This means:
- No ERC-20 compatibility (different chain architecture)
- Transfers are first-class transactions
- All token operations are signed with ML-DSA-65
- Quantum-resistant by default
NFTs
RougeChain supports on-chain NFTs with collections, minting, transfers, and marketplace features — all secured by post-quantum cryptography.
Features
- Collections — Create NFT collections with configurable max supply, royalties, and metadata
- Minting — Mint single or batch NFTs with metadata URIs and custom attributes
- Transfers — Send NFTs to any RougeChain address with optional sale price tracking
- Burning — Permanently destroy NFTs
- Locking — Lock individual NFTs to prevent transfers
- Freezing — Freeze entire collections to prevent further minting
Fee Structure
| Operation | Fee (XRGE) |
|---|---|
| Create Collection | 50 |
| Mint NFT | 5 |
| Batch Mint | 5 per NFT |
| Transfer | 1 |
| Burn | 0.1 |
| Lock/Unlock | 0.1 |
| Freeze Collection | 0.1 |
Security
All NFT operations use the v2 signed transaction API — transactions are signed client-side with ML-DSA-65 and verified on-chain. Only collection creators can mint, and only token owners can transfer or burn.
NFT Collections
Creating a Collection
- Go to the NFT Explorer page
- Click Create Collection
- Fill in:
- Symbol — Short identifier (e.g., "ROGUE")
- Name — Full collection name
- Max Supply — Maximum number of NFTs (optional, 0 = unlimited)
- Royalty — Royalty percentage in basis points (e.g., 500 = 5%)
- Royalty recipient — (optional) wallet that receives royalties. Defaults to the creator when omitted.
- Image — Collection cover image URL
- Description — Collection description
- Click Create
The collection ID is generated from creator_pubkey:SYMBOL.
Royalties
Set a royaltyBps (basis points — 500 = 5%, 1000 = 10%) when creating a collection to earn on secondary sales. From the CONTRACT_NFT_ROYALTY upgrade, royaltyBps must be an integer from 0 to 10000 (100%); a higher value is rejected at creation.
- When it's paid: on an
nft_transferthat declares asalePrice > 0. The royalty issalePrice × royaltyBps / 10000, in XRGE. - Who pays: the sender (the current owner initiating the transfer). It is deducted from their XRGE balance on top of the 1 XRGE transfer fee.
- Who receives: the collection's
royaltyRecipient. If you didn't set one, this is the creator. If you set one at creation, it is that address — a wallet or a contract. The recipient is fixed at creation and cannot be changed afterward.
Routing royalties to another wallet or a contract
Pass royaltyRecipient at creation to send royalties somewhere other than the creator:
- A wallet — for example a shared payout wallet or a treasury. Anything the wallet's owner does with the royalty afterwards (such as splitting it) happens off-chain.
- A contract — for trustless on-chain splits. Since mainnet block 18 contracts hold XRGE and pay it out with their own code, so a royalty-splitter contract (see
contracts/royalty_splitterand Contract XRGE custody) can receive royalties and fan them out to collaborators by fixed shares. Deploy the contract first and use its address.
⚠️ The node does not check the recipient address. Royalties sent to a mistyped address, or to a contract that has no way to pay XRGE out, cannot be recovered — only a contract's own code can move its XRGE. Double-check the address, and only use contracts whose payout function you have tested.
Note on enforcement:
salePriceis self-declared in the transfer payload — there is no on-chain marketplace escrow. A transfer sent with nosalePrice(or0) pays no royalty. Royalties are honored by marketplaces/apps that populatesalePrice, not enforced against every possible transfer.
Collection Properties
| Property | Description |
|---|---|
id | Unique identifier (creator:symbol) |
symbol | Short token symbol |
name | Full collection name |
creator | Creator's public key |
max_supply | Max tokens (0 = unlimited) |
royalty_bps | Royalty in basis points |
royalty_recipient | Wallet that receives royalties (defaults to creator) |
frozen | Whether minting is frozen |
total_minted | Number of tokens minted |
Freezing a Collection
Collection creators can freeze their collections to permanently prevent further minting. This is irreversible and signals to holders that the supply is final.
Browsing Collections
The NFT Explorer page shows all collections with their stats, floor price, and total items. Click any collection to view its individual tokens.
Minting & Trading NFTs
Minting
Single Mint
- Open a collection page
- Click Mint NFT
- Enter the token name and optional metadata URI / attributes
- Click Mint
Only the collection creator can mint new tokens.
Batch Mint
Mint multiple NFTs at once:
- Open a collection page
- Click Batch Mint
- Provide a list of names (and optional URIs for each)
- Click Mint All
Fee is 5 XRGE per NFT in the batch.
Per-token attributes in a batch mint are applied once the client signs them as the payload's
attributesfield (one entry per name).@rougechain/sdk1.11.0 and later do this (rc.nft.batchMint(wallet, { ..., attributes: [...] })); SDK 1.10.0 and earlier, and site builds before this fix, sent them underbatchAttributes, which the node ignores, so those batch-minted tokens have no attributes. Upgrade the SDK (or mint individually) if a token needs on-chain attributes.
Token Properties
Each NFT has:
| Property | Description |
|---|---|
token_id | Sequential ID within the collection |
name | Token name |
owner | Current owner's address (rouge1...) |
creator | Original minter |
metadata_uri | Link to off-chain metadata (IPFS, HTTP) |
attributes | On-chain key-value attributes |
locked | Whether the token is locked (non-transferable) |
created_at | Timestamp of minting |
Transferring
- Open the NFT detail page
- Click Transfer
- Enter the recipient's RougeChain address (
rouge1...) - Optionally set a sale price (for marketplace tracking)
- Confirm
Locked NFTs cannot be transferred until unlocked by the owner.
Burning
Permanently destroy an NFT:
- Open the NFT detail page
- Click Burn
- Confirm
Only the current owner can burn. This action is irreversible.
Locking
Lock an NFT to prevent it from being transferred:
- Open the NFT detail page
- Toggle Lock
Only the owner can lock/unlock their NFTs.
Block Explorer
RougeChain includes a built-in block explorer at rougechain.io for browsing blocks, transactions, addresses, tokens, and NFTs.
Features
- Block details — View block height, hash, timestamp, proposer, and all transactions
- Transaction details — View transaction type, sender, recipient, amount, fee, and status
- Address pages — View any address's balances, token holdings, and transaction history
- Token list — Browse all tokens created on RougeChain
- NFT Explorer — Browse NFT collections and individual tokens
- Global search — Search by address, transaction hash, or block height
Navigation
The explorer is integrated into the main RougeChain web app:
| Page | URL | Description |
|---|---|---|
| Blockchain | /blockchain | Block list with live updates |
| Block Detail | /block/:height | Individual block |
| Transaction | /tx/:hash | Individual transaction |
| Address | /address/:pubkey | Address details |
| Transactions | /transactions | All transactions list |
| NFT Explorer | /nft-explorer | NFT collections browser |
| Tokens | /tokens | All tokens list |
Block Explorer — Blocks
Block List
The Blockchain page shows all blocks in reverse chronological order with:
- Block height (index)
- Block hash (truncated)
- Timestamp
- Number of transactions
- Proposer address
Click any block to view its full details.
Block Detail Page
URL: /block/:height
Shows:
| Field | Description |
|---|---|
| Height | Block number in the chain |
| Hash | SHA-256 hash of the block |
| Previous Hash | Hash of the parent block |
| Timestamp | When the block was created |
| Proposer | Public key of the block proposer |
| Transaction Count | Number of transactions in the block |
Below the header, all transactions in the block are listed with their type, sender, recipient, amount, and fee.
API
GET /api/blocks — List all blocks (paginated)
GET /api/block/:height — Get a specific block by height
GET /api/blocks/summary — Block summary for charts
Block Explorer — Transactions
Transaction List
The Transactions page shows all transactions across the network with:
- Transaction hash
- Type (transfer, swap, stake, bridge, NFT, etc.)
- Sender and recipient
- Amount
- Block number
- Timestamp
Transaction Detail Page
URL: /tx/:hash
Shows the full transaction details:
| Field | Description |
|---|---|
| Hash | Transaction hash (SHA-256) |
| Type | Transaction type |
| From | Sender public key |
| To | Recipient public key |
| Amount | Transaction amount |
| Fee | Transaction fee (XRGE) |
| Token | Token symbol (if applicable) |
| Block | Block height containing this tx |
| Timestamp | When the transaction was processed |
| Signature | PQC signature (ML-DSA-65) |
Transaction Types
| Type | Description |
|---|---|
transfer | Token transfer between addresses |
create_token | New token creation |
stake | Staking tokens |
unstake | Unstaking tokens |
create_pool | Creating a liquidity pool |
add_liquidity | Adding liquidity to a pool |
remove_liquidity | Removing liquidity from a pool |
swap | Token swap via AMM |
bridge_mint | Minting bridged tokens (qETH, qUSDC) |
bridge_withdraw | Burning bridged tokens for withdrawal |
nft_create_collection | Creating an NFT collection |
nft_mint | Minting an NFT |
nft_transfer | Transferring an NFT |
nft_burn | Burning an NFT |
API
GET /api/txs — List recent transactions
GET /api/tx/:hash — Get transaction by hash
GET /api/address/:public_key/transactions — Get transactions for an address
Block Explorer — Addresses
Address Detail Page
URL: /address/:pubkey
Shows comprehensive information for any RougeChain address. Addresses are displayed in the compact rouge1... Bech32m format throughout the UI, derived from the SHA-256 hash of the raw ML-DSA-65 public key.
Balances
- XRGE balance
- All token balances (qETH, qUSDC, custom tokens)
- Staked amount (if any)
Transaction History
Paginated list of all transactions where this address is the sender or recipient, including:
- Transfers
- Swaps
- Staking operations
- Bridge operations
- NFT operations
NFTs Owned
List of all NFTs currently owned by this address, grouped by collection.
API
GET /api/balance/:public_key — XRGE balance
GET /api/balance/:public_key/:token_symbol — Token balance
GET /api/address/:public_key/transactions — Transaction history (paginated; covers the most recent 500 blocks)
GET /api/nft/owner/:pubkey — NFTs owned (hex public key)
Contracts
The Contracts Explorer lets you browse all WASM smart contracts deployed on RougeChain.
Contracts List (/contracts)
The contracts page displays:
- Total Contracts — Number of deployed contracts
- Total WASM Size — Combined bytecode size
- Search — Filter by contract address or deployer
- Sort — By newest, oldest, or size
Each contract card shows its address, deployer, WASM size, and deploy block. Click to view details.
Contract Detail (/contract/:addr)
The contract detail page has three sections:
Contract Information
- Contract address (hex)
- Deployer identity
- Deploy block and transaction
- WASM bytecode size
State Viewer
Live key-value table showing all data in the contract's persistent storage. Updates after each contract call.
Execute Contract
Interactive form to call contract methods directly from the browser:
| Field | Description |
|---|---|
| Method | Function name (e.g. get_count, transfer) |
| Arguments | JSON object with method parameters |
| Caller | Caller identity (optional) |
| Gas Limit | Max fuel units (default: 100,000) |
Results display success/failure status, gas used, and return data.
Transaction Labels
Contract transactions appear throughout the explorer with distinct labels:
| Label | Description |
|---|---|
| Contract Deploy | WASM bytecode deployment |
| Contract Call | Method execution |
The transaction detail page shows a Contract Details card for these transactions with the contract address, method name, gas used, and WASM size.
API Reference
The RougeChain node exposes a REST API on the configured --api-port (default: 5101).
Important: All write operations (POST/PUT/DELETE) require v2 signed requests using ML-DSA-65 client-side signing. Legacy unsigned v1 write endpoints return
410 GONEin production. Private keys never leave your application.
Base URL
Local node:
http://127.0.0.1:5101/api
Public testnet:
https://testnet.rougechain.io/api
Public mainnet:
https://api.rougechain.io/api
Both public endpoints send Access-Control-Allow-Origin: *, so browser dApps and localhost dev servers can call them directly with no per-origin allowlisting. (The API has no cookie/session auth — every write is ML-DSA-65 signed — so a wildcard is safe, the same posture as public RPC providers.)
Address Format
RougeChain uses Bech32m addresses with the rouge1 prefix (e.g., rouge1q8f3x7k2m4n9p...). These are derived from the SHA-256 hash of the raw ML-DSA-65 public key.
Accepted formats differ per endpoint:
| Endpoint | Accepts |
|---|---|
/api/balance/:publicKey, /api/balance/:publicKey/:token | hex public key or rouge1… address |
/api/address/:pubkey/transactions | hex public key or rouge1… address |
/api/resolve/:input | hex public key or rouge1… address |
/api/nft/owner/:pubkey | exact match on the owner as recorded — normally the hex public key; a rouge1… address does not match a pubkey-owned NFT |
/api/account/:pubkey/nonce | hex public key or rouge1… address (resolved via the node's address index; an address that has never sent a transaction returns 0; a malformed one returns 400) |
When in doubt, pass the hex public key.
Tip: Use
GET /api/resolve/:inputto convert betweenrouge1…addresses and hex public keys.
v2 Signed Request Format
All write endpoints use a standard signed-request envelope:
{
"payload": {
"...endpoint-specific fields...",
"from": "your-signing-public-key-hex",
"timestamp": 1706745600000,
"nonce": "random-hex-string"
},
"signature": "ml-dsa65-signature-of-payload-hex",
"public_key": "your-signing-public-key-hex"
}
The payload is JSON-serialized with keys sorted alphabetically, then signed with your ML-DSA-65 private key. The server verifies the signature, checks the timestamp (must be within ±5 minutes of the server clock), and rejects a signature it has already seen within that window. Messenger, mail and name endpoints additionally require a unique nonce of 8–128 characters. The envelope may also carry an optional payload_bytes_hex (the exact bytes that were signed); when present the node verifies those bytes and checks that they decode to payload.
Endpoints Overview
System
| Endpoint | Method | Description |
|---|---|---|
/api/health | GET | Node health check |
/api/stats | GET | Network statistics |
/api/ws | GET | WebSocket for real-time events |
/api/price/xrge | GET | Current XRGE price |
Blockchain
| Endpoint | Method | Description |
|---|---|---|
/api/blocks | GET | Get recent blocks (max 100) or a page of blocks |
/api/blocks/summary | GET | Block summary for charts |
/api/block/:height | GET | Get block by height |
/api/txs | GET | Get transactions |
/api/tx/:hash | GET | Get transaction by hash |
/api/tx/:hash/receipt | GET | Get transaction receipt |
/api/events | GET | Get all events |
Wallet & Accounts
| Endpoint | Method | Description |
|---|---|---|
/api/balance/:publicKey | GET | Get XRGE balance |
/api/balance/:publicKey/:token | GET | Get token balance |
/api/account/:pubkey/nonce | GET | Get account nonce |
/api/resolve/:input | GET | Resolve address ↔ public key |
/api/address/:pubkey/transactions | GET | Get address transactions |
/api/v2/transfer | POST | Transfer tokens (signed) |
/api/v2/faucet | POST | Request testnet tokens (signed) |
Tokens
| Endpoint | Method | Description |
|---|---|---|
/api/tokens | GET | List all tokens |
/api/token/:symbol/metadata | GET | Get token metadata |
/api/token/:symbol/holders | GET | Get token holders |
/api/token/:symbol/transactions | GET | Get token transactions |
/api/burn-address | GET | Get official burn address |
/api/burned | GET | Get burned token stats |
/api/v2/token/create | POST | Create token (signed); mintable / max_supply since TOKEN_MINTING (mainnet block 235) |
/api/v2/token/mint | POST | Mint more of a mintable token, creator only (signed); live since mainnet block 235 |
/api/v2/token/metadata/update | POST | Update token metadata (signed) |
/api/v2/token/metadata/claim | POST | Claim metadata ownership (signed) |
Token Allowances (ERC-20 Style)
| Endpoint | Method | Description |
|---|---|---|
/api/v2/token/approve | POST | Approve spender allowance (signed) |
/api/v2/token/transfer-from | POST | Transfer using allowance (signed) |
/api/v2/token/freeze | POST | Freeze/unfreeze token transfers (signed) |
/api/token/allowance | GET | Check specific allowance |
/api/token/allowances | GET | List allowances by owner/spender |
/api/allowances/:pubkey | GET | List all allowances for a key |
/api/locks/:pubkey | GET | Get token locks |
Staking & Validators
| Endpoint | Method | Description |
|---|---|---|
/api/validators | GET | List validators |
/api/validators/stats | GET | Validator vote stats |
/api/selection | GET | Proposer for the next height. Once proposer selection is active, the consensus designated proposer (rule: "designated_max_stake", same as designated_proposer_next in /api/stats; no entropy fields); before activation, the legacy QRNG lottery (rule: "legacy_qrng_lottery") |
/api/finality | GET | Finality status (legacy, informational — see Finality) |
/api/votes | GET | Vote quorum info |
/api/v2/stake | POST | Stake tokens (signed) |
/api/v2/unstake | POST | Unstake tokens (signed) |
Token Staking Pools (suspended since block 245)
create_staking_pool,token_stakeandtoken_unstakeare suspended transaction types since mainnet block 245 (testnet 1390): no block may carry them. These read routes answer with empty data on mainnet (no pool was ever created). See Status.
| Endpoint | Method | Description |
|---|---|---|
/api/staking/pools | GET | List all staking pools |
/api/staking/pool/:pool_id | GET | Get staking pool details |
/api/staking/stakes/:pubkey | GET | Get stakes by owner |
/api/staking/pool/:pool_id/stakes | GET | Get stakes in a pool |
AMM/DEX
| Endpoint | Method | Description |
|---|---|---|
/api/pools | GET | List liquidity pools |
/api/pool/:pool_id | GET | Get pool details |
/api/pool/:pool_id/prices | GET | Get price history |
/api/pool/:pool_id/events | GET | Get pool events |
/api/pool/:pool_id/stats | GET | Get pool statistics |
/api/swap/quote | POST | Get swap quote |
/api/v2/pool/create | POST | Create liquidity pool (signed) |
/api/v2/pool/add-liquidity | POST | Add liquidity (signed) |
/api/v2/pool/remove-liquidity | POST | Remove liquidity (signed) |
/api/v2/swap/execute | POST | Execute swap (signed) |
NFTs
| Endpoint | Method | Description |
|---|---|---|
/api/nft/collections | GET | List collections |
/api/nft/collection/:id | GET | Get collection |
/api/nft/collection/:id/tokens | GET | Get collection tokens |
/api/nft/token/:coll/:id | GET | Get specific NFT |
/api/nft/owner/:pubkey | GET | Get NFTs by owner |
/api/v2/nft/collection/create | POST | Create collection (signed) |
/api/v2/nft/mint | POST | Mint NFT (signed) |
/api/v2/nft/batch-mint | POST | Batch mint (signed) |
/api/v2/nft/transfer | POST | Transfer NFT (signed) |
/api/v2/nft/burn | POST | Burn NFT (signed) |
/api/v2/nft/lock | POST | Lock/unlock NFT (signed) |
/api/v2/nft/freeze-collection | POST | Freeze collection (signed) |
Smart Contracts
| Endpoint | Method | Description |
|---|---|---|
/api/v2/contract/publish | POST | Publish (deploy) WASM contract (player-signed, 10 XRGE) |
/api/v2/contract/execute | POST | Call a contract method (player-signed, fee = gasLimit × 0.000001 XRGE) |
/api/contract/:address/query | POST | Read-only call (free, not signed) |
/api/contract/:address | GET | Get contract metadata |
/api/contract/:address/state | GET | Get contract state |
/api/contract/:address/events | GET | Get contract events (limit, before, tx) |
/api/contracts | GET | List contracts |
Bridge (ETH/USDC + XRGE)
| Endpoint | Method | Description |
|---|---|---|
/api/bridge/config | GET | ETH/USDC bridge config |
/api/bridge/claim | POST | Claim bridge deposit |
/api/bridge/withdraw | POST | Withdraw to EVM |
/api/bridge/withdrawals | GET | List pending withdrawals |
/api/bridge/activity | GET | Public bridge activity (deposits + withdrawals, paginated) |
/api/bridge/activity/:txId | GET | One bridge transfer by RougeChain tx id |
/api/bridge/withdrawals/:txId | DELETE | Fulfill withdrawal |
/api/bridge/btc/deposit-address | POST | Per-recipient BTC deposit address (qBTC) |
/api/bridge/xrge/config | GET | XRGE bridge config |
/api/bridge/xrge/claim | POST | Claim XRGE deposit |
/api/bridge/xrge/withdraw | POST | Withdraw XRGE to EVM |
/api/bridge/xrge/withdrawals | GET | List XRGE withdrawals |
/api/bridge/xrge/withdrawals/:txId | DELETE | Fulfill XRGE withdrawal |
Shielded Transactions (suspended since block 245)
The V1 shielded pool is suspended:
shield,shielded_transferandunshieldare refused (this transaction type is suspended) and a block carrying them is invalid. The mainnet pool is empty (shielded_supply0). A redesigned pool (V2) is in development and unaudited — see Status.
| Endpoint | Method | Description |
|---|---|---|
/api/v2/shielded/shield | POST | Refused: suspended |
/api/v2/shielded/transfer | POST | Refused: suspended |
/api/v2/shielded/unshield | POST | Refused: suspended |
/api/shielded/stats | GET | Shielded pool statistics (pool total 0 on mainnet) |
/api/shielded/nullifier/:hash | GET | Check nullifier |
Rollup (research prototype — not part of consensus)
The rollup accumulator batches transfers in memory and proves them with a STARK, but nothing in consensus reads a batch: a submitted transfer moves no funds and enters no block.
| Endpoint | Method | Description |
|---|---|---|
/api/v2/rollup/status | GET | Rollup status |
/api/v2/rollup/batch/:id | GET | Get rollup batch |
/api/v2/rollup/submit | POST | Submit rollup transfer |
Governance (suspended since block 245)
On-chain governance (
create_proposal,cast_vote,execute_proposal,delegate,undelegate) is suspended: no block may carry these types, and none was ever used on mainnet. These read routes return empty lists on mainnet. Treasury decisions for Regenerate use the node-hosted Regenerate votes in the meantime.
| Endpoint | Method | Description |
|---|---|---|
/api/governance/proposals | GET | List all proposals |
/api/governance/proposals/:token | GET | Proposals by token |
/api/governance/proposal/:id | GET | Get proposal details |
/api/governance/proposal/:id/votes | GET | Get proposal votes |
Social
| Endpoint | Method | Description |
|---|---|---|
/api/social/track/:trackId/stats | GET | Get track stats (plays, likes, comments) |
/api/social/track/:trackId/comments | GET | Get track comments |
/api/social/artist/:pubkey/stats | GET | Get artist stats (followers, following) |
/api/social/user/:pubkey/likes | GET | Get user's liked track/post IDs |
/api/social/user/:pubkey/following | GET | Get user's followed artists |
/api/social/user/:pubkey/posts | GET | Get user's posts |
/api/social/post/:postId | GET | Get a single post with stats |
/api/social/post/:postId/stats | GET | Get post stats (likes, reposts, replies) |
/api/social/post/:postId/replies | GET | Get replies to a post |
/api/social/timeline | GET | Global timeline (newest first) |
/api/v2/social/play | POST | Record a play (signed) |
/api/v2/social/like | POST | Toggle like (signed) |
/api/v2/social/comment | POST | Post a comment (signed) |
/api/v2/social/comment/delete | POST | Delete a comment (signed) |
/api/v2/social/follow | POST | Toggle follow (signed) |
/api/v2/social/post | POST | Create a post (signed) |
/api/v2/social/post/delete | POST | Delete a post (signed) |
/api/v2/social/repost | POST | Toggle repost (signed) |
/api/v2/social/feed | POST | Get following feed (signed) |
Messenger
| Endpoint | Method | Description |
|---|---|---|
/api/messenger/wallets | GET | List messenger wallets |
/api/v2/messenger/wallets/register | POST | Register wallet (signed) |
/api/v2/messenger/conversations | POST | Create conversation (signed) |
/api/v2/messenger/conversations/list | POST | List conversations (signed) |
/api/v2/messenger/conversations/delete | POST | Delete conversation (signed) |
/api/v2/messenger/messages | POST | Send message (signed) |
/api/v2/messenger/messages/list | POST | List messages (signed) |
/api/v2/messenger/messages/read | POST | Mark as read (signed) |
/api/v2/messenger/messages/delete | POST | Delete message (signed) |
| Endpoint | Method | Description |
|---|---|---|
/api/v2/names/register | POST | Register mail name (signed) |
/api/v2/names/release | POST | Release a name (signed) |
/api/names/resolve/:name | GET | Resolve name → wallet |
/api/names/reverse/:walletId | GET | Reverse lookup → name |
/api/v2/mail/send | POST | Send encrypted mail (signed) |
/api/v2/mail/folder | POST | Get inbox/sent/trash (signed) |
/api/v2/mail/message | POST | Get single mail item (signed) |
/api/v2/mail/read | POST | Mark as read (signed) |
/api/v2/mail/move | POST | Move to folder (signed) |
/api/v2/mail/delete | POST | Delete permanently (signed) |
Push Notifications
| Endpoint | Method | Description |
|---|---|---|
/api/push/register | POST | Register push token (PQC-signed) |
/api/push/unregister | POST | Unregister push token (PQC-signed) |
P2P
| Endpoint | Method | Description |
|---|---|---|
/api/peers | GET | List known peers |
/api/peers/register | POST | Register as peer |
/api/blocks/import | POST | Import block from peer |
/api/tx/broadcast | POST | Receive broadcasted tx |
Authentication
If a node is started with --api-keys, most REST routes require a key — reads included, not just writes. Exempt are /, /api/health, /api/stats, /api/v2/*, /api/bridge/*, /api/messenger/*, /api/mail/*, /api/names/* and the faucet. Send the key as X-API-Key or Authorization: Bearer:
curl -H "X-API-Key: your-api-key" http://127.0.0.1:5101/api/blocks
The public endpoints (api.rougechain.io, testnet.rougechain.io) do not require a key.
Rate Limiting
Rate limiting is disabled by default (all limits 0 = unlimited). Limits are per minute and tiered:
- Tier 1 (Validators): Proven via
X-Validator-Key/X-Validator-Sig/X-Validator-Tsheaders (the signed timestamp must be within 30 seconds) —--rate-limit-validator - Tier 2 (Registered peers): Recognized by IP —
--rate-limit-peer - Tier 3 (Public): GET requests use
--rate-limit-read-per-minute, all other methods--rate-limit-write-per-minute
--rate-limit-per-minute is a deprecated alias: when it is set and neither read nor write limit is, it applies to both (a deprecation warning is logged); explicit read/write limits take precedence.
Clients are keyed by the connecting IP. Behind a reverse proxy on the same host, start the node with --trust-proxy (env QV_TRUST_PROXY=true): requests whose TCP peer is loopback are then keyed by X-Real-IP, else the rightmost X-Forwarded-For entry; requests from any other peer always use the socket IP. Every route is rate-limited, including the peer routes /api/finality/* (which stay API-key exempt).
Health & Stats API
System endpoints for monitoring node status and network statistics.
Health Check
GET /api/health
Returns the node's current status.
Response
{
"status": "ok",
"chain_id": "rougechain-devnet-1",
"height": 12345
}
| Field | Type | Description |
|---|---|---|
status | string | "ok" if the node is healthy |
chain_id | string | The chain identifier |
height | number | Current block height |
Use Cases
- Monitoring node uptime
- Checking sync status (compare height with peers)
- Load balancer health checks
Network Statistics
GET /api/stats
Returns network-wide statistics.
Response
{
"connected_peers": 3,
"network_height": 12345,
"is_mining": true,
"node_id": "abc123...",
"total_fees_collected": 1234.5,
"fees_in_last_block": 0.6,
"chain_id": "rougechain-devnet-1",
"finalized_height": 12300,
"ws_clients": 12,
"node_name": "node-1",
"base_fee": 0.001,
"total_fees_burned": 5000.0
}
| Field | Type | Description |
|---|---|---|
connected_peers | number | Currently connected peers |
network_height | number | Current block height |
is_mining | boolean | Whether this node is mining |
node_id | string | This node's ID |
total_fees_collected | number | Total fees collected |
fees_in_last_block | number | Fees collected in the last block |
chain_id | string | Chain identifier |
finalized_height | number | Last finalized block height |
ws_clients | number | Active WebSocket clients |
node_name | string | null | Human-readable node name (optional) |
base_fee | number | Current EIP-1559 base fee |
total_fees_burned | number | Total fees (base fee) burned |
Burn Stats
GET /api/burned
Get burned token statistics.
Response
{
"burned": {
"XRGE": 5000.0,
"qETH": 1.5
},
"total_xrge_burned": 5000.0
}
| Field | Type | Description |
|---|---|---|
burned | object | Per-token burned totals, keyed by token symbol |
total_xrge_burned | number | Total XRGE burned |
Examples
Monitoring Script
#!/bin/bash
while true; do
HEIGHT=$(curl -s http://127.0.0.1:5101/api/health | jq '.height')
echo "$(date): Block height = $HEIGHT"
sleep 10
done
Compare with Testnet
LOCAL=$(curl -s http://127.0.0.1:5101/api/health | jq '.height')
TESTNET=$(curl -s https://testnet.rougechain.io/api/health | jq '.height')
echo "Local: $LOCAL, Testnet: $TESTNET, Behind: $((TESTNET - LOCAL))"
Blocks API
Endpoints for querying block data on RougeChain.
Get Blocks
GET /api/blocks?limit=50
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | - | Page number (1-based); enables paginated mode |
per_page | number | 10 | Blocks per page (used with page) |
limit | number | 100 (1000 with from_height) | Maximum blocks to return (max 100, up to 1000 with from_height) |
from_height | number | - | Start from this block height (used for P2P sync) |
Response
{
"blocks": [
{
"version": 1,
"header": {
"version": 1,
"chain_id": "rougechain-devnet-1",
"height": 42,
"time": 1706745600000,
"prev_hash": "abc123...",
"tx_hash": "def456...",
"proposer_pub_key": "ghi789...",
"state_root": "...",
"parent_commit": { "...": "..." }
},
"txs": [...],
"proposer_sig": "...",
"hash": "xyz..."
}
],
"total_height": 12345,
"page": 1,
"total_pages": 1235
}
total_height,pageandtotal_pagesare only returned in paginated mode (whenpageis supplied); withlimitorfrom_heightthe response contains onlyblocks.
Block Fields
| Field | Type | Description |
|---|---|---|
version | number | Block format version |
header.chain_id | string | Chain identifier |
header.height | number | Block number |
header.time | number | Timestamp (ms since epoch) |
header.prev_hash | string | Hash of previous block |
header.tx_hash | string | SHA-256 over the concatenated encoded transactions, in block order (not a Merkle root) |
header.proposer_pub_key | string | Validator who proposed this block |
header.state_root | string | Post-state root of the balance ledger (omitted before its activation height) |
header.parent_commit | object | Finality certificate for the parent block (omitted before FINALITY_V2 activation) |
txs | array | Transactions included in this block |
proposer_sig | string | ML-DSA-65 signature by the proposer |
hash | string | SHA-256 of the JSON-encoded header followed by proposer_sig |
Block Summary
GET /api/blocks/summary
Returns time-bucketed block/transaction counts, suitable for charts and dashboards.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
range | string | 24h | Time range: 1h, 24h, or 7d |
Response
{
"success": true,
"range": "24h",
"intervalMs": 3600000,
"startTime": 1706659200000,
"endTime": 1706745600000,
"points": [
{
"timestamp": 1706742000000,
"blocks": 42,
"transactions": 137
}
]
}
Import Block (P2P)
Used by peer nodes to propagate blocks.
POST /api/blocks/import
Content-Type: application/json
See Peers API for details.
Block Verification
Every block is verified by receiving nodes:
- Hash check — Recompute the block hash and compare
- Signature check — Verify ML-DSA-65 signature against the proposer's public key
- Height check — Must extend the current chain tip by exactly 1
- Previous hash — Must reference the current tip's hash
- Designated proposer — The proposer must be the designated proposer for that height (largest eligible stake; ties → lowest public key)
- Parent commit — The parent must carry a ⅔-stake finality certificate (
parent_commit) - Transaction validity — All transactions must have valid signatures and sufficient balances
- State root — The post-state must match
state_root
Transactions API
Transfer Tokens (v2)
Send XRGE or custom tokens to another address using client-side ML-DSA-65 signing.
POST /api/v2/transfer
Content-Type: application/json
Request Body
{
"payload": {
"to": "recipient-public-key-hex",
"amount": 100.0,
"token": "XRGE",
"from": "sender-public-key-hex",
"timestamp": 1706745600000,
"nonce": "random-hex-string"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "sender-public-key-hex"
}
| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Recipient's public key (hex) or rouge1 address |
amount | number | Yes | Amount to send |
token | string | No | Token symbol (default: "XRGE") |
Fee: The transaction fee is fixed at 1 XRGE and enforced by the server; any client-supplied
feein the payload is ignored.Security: Private keys never leave your application. The transaction is signed client-side using ML-DSA-65 and the server verifies the signature before processing.
Response
{
"success": true,
"message": "Transfer transaction submitted"
}
The response does not include a transaction id.
Error Response
{
"success": false,
"error": "insufficient XRGE balance: have 50.0000, need 101.0000"
}
Get Transactions
Retrieve recent transactions.
GET /api/txs?limit=50&offset=0
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 200 | Max transactions to return (max 1000) |
offset | number | 0 | Pagination offset |
Response
Each item wraps the transaction with its block context. The wrapper fields are camelCase
(txId, blockHeight, blockHash, blockTime); the nested tx (a TxV1) is
snake_case.
{
"txs": [
{
"txId": "abc123...",
"blockHeight": 42,
"blockHash": "xyz...",
"blockTime": 1706745600000,
"tx": {
"version": 1,
"tx_type": "transfer",
"from_pub_key": "abc...",
"nonce": 1234567890,
"payload": {
"to_pub_key_hex": "def...",
"amount": 100
},
"fee": 1.0,
"sig": "ghi..."
}
}
],
"total": 150
}
Get Transaction by Hash
GET /api/tx/:hash
Response
txId, blockHeight, blockHash and blockTime are top-level (not inside tx). The nested
tx is the stored TxV1 exactly as it was signed and accepted, in snake_case; receipt is
the execution receipt (present once the block is applied; null only for a transaction that is
known but not yet executed). The example below is a real mainnet transfer
(3c1b71bc…, block 110); unused payload fields are returned as null and omitted here.
{
"success": true,
"txId": "3c1b71bc59557f9b4d1191bc299e1bd2d8b6ad4058147efebe70d0f6c07daae1",
"blockHeight": 110,
"blockHash": "7ee2afd4eaebdcd4…",
"blockTime": 1790297628272,
"tx": {
"version": 1,
"tx_type": "transfer",
"from_pub_key": "df255dbd…",
"nonce": 4,
"payload": {
"to_pub_key_hex": "rouge1cd3mkuu6p89nm6uakpcchfmkvhj8x2z0xfx33gag9t5kma2s8fcs4m9mda",
"amount": 50,
"token_name": "XRGE"
},
"fee": 1.0,
"sig": "…",
"signed_payload": "{…}"
},
"receipt": {
"tx_hash": "3c1b71bc…",
"block_height": 110,
"block_hash": "7ee2afd4eaebdcd4…",
"index": 0,
"tx_type": "transfer",
"from": "df255dbd…",
"status": "Success",
"fee_paid": 1.0,
"timestamp": 1790297628272,
"logs": [
{ "event_type": "transfer", "data": { "amount": 50, "to": "rouge1cd3mkuu…", "token": "XRGE" } }
]
}
}
Field notes
tx.payload.to_pub_key_hexholds the recipient exactly as it was submitted — either arouge1…address (as in the example) or a hex public key. The name is historical; treat it as "recipient", not as a guarantee of a hex key. To convert either form to the other, callGET /api/resolve/:input(auto-detects the format and returnsaddress,publicKeyandbalance).receipt.logs[].data.tocarries the same recipient value;receipt.statusis"Success"or{"Failed": "<reason>"}.signed_payloadis the exact JSON the sender signed (V2 transactions); consensus binds the executed fields to it, sotx.*never differs from what was signed.- Stability: the
txandreceiptfield names are the serializedTxV1/TxReceipttypes and are stable; any change would ship as a new versioned endpoint, not as a rename.
Get Transaction Receipt
GET /api/tx/:hash/receipt
Returns execution receipt for contract calls and other complex transactions.
Request Faucet (v2)
Get free testnet XRGE tokens. The faucet is only enabled on test networks; elsewhere it returns 403.
POST /api/v2/faucet
Content-Type: application/json
Request Body
{
"payload": {
"from": "your-public-key-hex",
"timestamp": 1706745600000,
"nonce": "random-hex-string"
},
"signature": "your-signature-hex",
"public_key": "your-public-key-hex"
}
Response
{
"success": true,
"message": "Faucet: 10000 XRGE sent"
}
Limits
The faucet refuses a request with 429 while you already have a faucet transfer pending, if your balance is already above the faucet threshold, or during the 24-hour per-key cooldown (shared with POST /api/faucet; the response then also carries retryAfterSecs). When the node sets --faucet-whitelist, keys that are not listed get 403.
Transaction Types
| Type | Description |
|---|---|
transfer | Standard XRGE or token transfer (faucet payouts are transfers with payload.faucet = true) |
stake | Stake tokens to become validator |
unstake | Unstake tokens |
create_token | Create custom token (token_mintable / token_max_supply only from TOKEN_MINTING activation) |
mint_tokens | Creator mints more of a mintable token (token_total_supply = amount) — applied only from TOKEN_MINTING activation |
burn | Burn tokens permanently |
shield / shielded_transfer / unshield | Suspended since block 245 (V1 shielded pool; a block carrying one is invalid). Also suspended: token_lock, token_unlock, create_staking_pool, token_stake, token_unstake, create_proposal, cast_vote, execute_proposal, delegate, undelegate, token_approve, token_transfer_from; and slash is rejected. See Status |
contract_deploy | Deploy WASM smart contract |
contract_call | Call smart contract method |
Wallet API
Endpoints for balance queries, transfers, and token management.
Note: Wallets are created client-side using ML-DSA-65 + ML-KEM-768 key generation. Private keys never leave your application. See Create a Wallet for details.
Get Balance
GET /api/balance/:publicKey
Path Parameters
| Parameter | Type | Description |
|---|---|---|
publicKey | string | The wallet's ML-DSA-65 public key (hex) or its rouge1… address |
Response
{
"success": true,
"balance": 1500.5,
"token_balances": {
"qETH": 0.5
},
"lp_balances": {}
}
balance is the XRGE balance; token_balances maps token symbol → balance; lp_balances maps pool id → LP-token balance.
Transfer Tokens (v2)
POST /api/v2/transfer
Content-Type: application/json
Request Body
{
"payload": {
"to": "recipient-public-key-hex",
"amount": 100,
"token": "XRGE",
"from": "sender-public-key-hex",
"timestamp": 1706745600000,
"nonce": "random-hex-string"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "sender-public-key-hex"
}
The transaction is signed client-side using ML-DSA-65. The server verifies the signature before processing. The fee is fixed at 1 XRGE.
Response
{
"success": true,
"message": "Transfer transaction submitted"
}
The response does not include a transaction id; watch the sender's history (/api/address/:pubkey/transactions) or the WebSocket feed to see the transaction land in a block.
Request Faucet (v2)
POST /api/v2/faucet
Content-Type: application/json
Request Body
{
"payload": {
"from": "your-public-key-hex",
"timestamp": 1706745600000,
"nonce": "random-hex-string"
},
"signature": "your-signature-hex",
"public_key": "your-public-key-hex"
}
Response
{
"success": true,
"message": "Faucet: 10000 XRGE sent"
}
The faucet is only enabled on test networks; elsewhere it returns 403. It applies the node's --faucet-whitelist (403 for unlisted keys) and the same 24-hour per-key cooldown as POST /api/faucet (429). See Get Test Tokens for details.
Burn Address
GET /api/burn-address
Response
{
"burn_address": "XRGE_BURN_0x000000000000000000000000000000000000000000000000000000000000DEAD",
"description": "Official burn address. Tokens sent here are permanently destroyed and tracked on-chain."
}
Send tokens to this address to permanently burn them. Burned amounts are tracked on-chain.
Address Resolution
Resolve between compact rouge1… bech32 addresses and full hex public keys.
GET /api/resolve/:input
Input can be either a rouge1… address or a hex public key. The endpoint auto-detects the format.
Response
{
"success": true,
"address": "rouge1q8f3x7k2m4...",
"publicKey": "a1b2c3d4e5f6...",
"balance": 1000.5
}
Account Nonce
Get the current and next sequential nonce for a wallet. A v2 signed payload may include an optional account_nonce; when present it must equal next_nonce.
The path parameter is the hex public key or a rouge1… address. An address is resolved to its public key through the node's address index; an address that has never sent a transaction returns nonce 0, and a malformed rouge1… string returns 400 with {"success": false, "error": "..."}. The JSON-RPC rouge_getTransactionCount / eth_getTransactionCount methods resolve addresses the same way (-32602 for a malformed address).
GET /api/account/:publicKey/nonce
Response
{
"success": true,
"nonce": 5,
"next_nonce": 6
}
Staking API
Endpoints for validator staking operations. All write operations use v2 signed requests.
List Validators
GET /api/validators
Response
{
"success": true,
"validators": [
{
"publicKey": "abc123...",
"name": "my-node",
"stake": 20000,
"status": "active",
"slashCount": 0,
"jailedUntil": 0,
"entropyContributions": 0,
"blocksProposed": 142
},
{
"publicKey": "def456...",
"stake": 10000,
"status": "active",
"slashCount": 0,
"jailedUntil": 0,
"entropyContributions": 0,
"blocksProposed": 71
}
],
"totalStake": 30000
}
Validator Fields
| Field | Type | Description |
|---|---|---|
publicKey | string | Validator's ML-DSA-65 public key |
name | string | Node name, if known (omitted otherwise) |
stake | number | Amount of XRGE staked |
status | string | active, jailed or inactive (no stake) |
slashCount | number | Times this validator has been slashed (historical: no slashing is active since height 100, and the slash transaction is rejected since height 245) |
jailedUntil | number | Block height the validator is jailed until (0 if never) |
entropyContributions | number | Entropy contributions counter |
blocksProposed | number | Total blocks produced |
Stake Tokens
POST /api/v2/stake
Content-Type: application/json
Request Body
{
"payload": {
"amount": 10000,
"from": "your-public-key-hex",
"timestamp": 1706745600000,
"nonce": "random-hex-string"
},
"signature": "your-ml-dsa65-signature-hex",
"public_key": "your-public-key-hex"
}
Response
{
"success": true,
"message": "Stake transaction submitted"
}
No transaction id is returned; check /api/validators once the transaction is in a block.
Requirements
| Requirement | Value |
|---|---|
| Minimum stake | 10,000 XRGE — checked by this API (and the CLI), not by consensus; a block applying a smaller stake is valid |
| Fee | 1 XRGE (fixed for stake and unstake) |
Unstake Tokens
POST /api/v2/unstake
Content-Type: application/json
Request Body
{
"payload": {
"amount": 5000,
"from": "your-public-key-hex",
"timestamp": 1706745600000,
"nonce": "random-hex-string"
},
"signature": "your-ml-dsa65-signature-hex",
"public_key": "your-public-key-hex"
}
Response
{
"success": true,
"message": "Unstake transaction submitted"
}
Unbonding
After unstaking, tokens enter an unbonding period (500 blocks) before they become available in your balance.
Error Responses
| Error | Cause |
|---|---|
"insufficient XRGE balance: …" | Not enough XRGE to cover the stake plus the 1 XRGE fee |
"minimum stake is 10000 XRGE" | Amount is less than 10,000 XRGE |
"insufficient staked balance: …" | Unstaking more than is staked |
"Invalid signature" / "Signature verification failed: …" | ML-DSA-65 signature verification failed |
Peers API
Endpoints for P2P peer management and discovery.
List Peers
Get all known peers.
GET /api/peers
Response
{
"peers": [
"https://testnet.rougechain.io/api",
"https://node2.example.com/api"
],
"peer_details": [
{ "url": "https://testnet.rougechain.io/api", "node_name": "RougeChain Primary" },
{ "url": "https://node2.example.com/api", "node_name": "My Cool Node" }
],
"count": 2
}
Register Peer
Register this node with another peer (enables discovery).
POST /api/peers/register
Content-Type: application/json
Request Body
{
"peerUrl": "https://mynode.example.com",
"nodeName": "My Validator Node"
}
The nodeName field is optional. If provided, it will be displayed on the network globe at rougechain.io/blockchain.
Response
{
"success": true,
"message": "Peer registered"
}
If the peer is already known:
{
"success": true,
"message": "Peer already known"
}
Import Block (P2P)
Receive a block from a peer (used for block propagation).
POST /api/blocks/import
Content-Type: application/json
Request Body
The full block object. Field names are snake_case (a camelCase body is rejected):
{
"version": 1,
"header": {
"version": 1,
"chain_id": "rougechain-mainnet-1",
"height": 42,
"time": 1706745600000,
"prev_hash": "abc123...",
"tx_hash": "def456...",
"proposer_pub_key": "...",
"state_root": "...",
"parent_commit": { "...": "..." }
},
"txs": [...],
"proposer_sig": "...",
"hash": "..."
}
state_root and parent_commit are optional in the format but required on blocks at/after their activation heights (mainnet blocks carry both). Since proposer selection activated (mainnet height 100), a block is only accepted from the designated proposer for its height.
Response
{
"success": true,
"error": null
}
Error Response
{
"success": false,
"error": "Block height 42 doesn't extend tip height 40"
}
Broadcast Transaction (P2P)
Receive a transaction broadcast from a peer.
POST /api/tx/broadcast
Content-Type: application/json
Request Body
The full transaction object.
Response
{
"success": true
}
Peer Discovery Flow
1. Node A starts with --peers "https://seed.example.com"
2. Node A calls GET /api/peers on seed node
→ Receives list of other peers
3. Node A adds new peers to its list
4. If Node A has --public-url, it calls POST /api/peers/register
on all known peers to announce itself
5. Every 30 seconds, repeat steps 2-4
This creates a mesh network where all nodes eventually discover each other.
Messenger API
Endpoints for the end-to-end encrypted PQC messenger.
All messages are encrypted client-side using ML-KEM-768 key encapsulation and AES-GCM. The server only stores encrypted blobs — it cannot read message contents.
v2 API (March 2026): All write operations now use
/api/v2/endpoints that require ML-DSA-65 signed requests with timestamp validation and nonce-based anti-replay protection. Legacy unsigned endpoints return HTTP 410 (Gone) in production; since node release 1.6.3 (2026-10-05) so do the legacy read routesGET /api/messenger/conversationsandGET /api/messenger/messages— use the signedPOST …/conversations/listandPOST …/messages/listbelow. Messenger data is node-hosted, not on-chain.
Signed Request Format
All v2 write endpoints accept a signed request body:
{
"payload": {
"action": "register_wallet",
"from": "ml-dsa65-public-key-hex",
"timestamp": 1710100000000,
"nonce": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"...": "operation-specific fields"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "ml-dsa65-public-key-hex"
}
The server verifies: (1) the ML-DSA-65 signature, (2) the timestamp is within a 5-minute window, (3) the from field matches the signing key, and (4) the nonce has not been used before.
Register Messenger Wallet
POST /api/v2/messenger/wallets/register
Content-Type: application/json
Register your wallet's encryption public key so others can send you encrypted messages and mail. This is required before receiving mail from other apps. Requires a signed request.
Ownership (node release 1.6.3). The entry is owned by the signing key (
from/public_key). Re-registering replaces an entry only if the request is signed by that key —id,displayNameandencryptionPublicKeycan be updated by the owner, never by another wallet. IfencryptionPublicKeyis already registered to another wallet the request fails withThis encryption key is already registered to another wallet. Nodes before 1.6.3 do not enforce this.
Request Body (signed)
{
"payload": {
"id": "wallet-uuid-or-public-key",
"displayName": "Alice",
"signingPublicKey": "ml-dsa65-signing-public-key-hex",
"encryptionPublicKey": "ml-kem768-encryption-public-key-hex",
"from": "ml-dsa65-signing-public-key-hex",
"timestamp": 1710100000000,
"nonce": "random-hex-nonce"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "ml-dsa65-signing-public-key-hex"
}
| Field | Type | Description |
|---|---|---|
id | string | Unique wallet identifier |
displayName | string | Human-readable name (unique, case-insensitive) |
signingPublicKey | string | ML-DSA-65 public key (hex) |
encryptionPublicKey | string | ML-KEM-768 public key (hex) — needed for E2E encryption |
Response
{
"success": true
}
List Messenger Wallets
GET /api/messenger/wallets
Returns all registered messenger wallets.
Response
{
"success": true,
"wallets": [
{
"id": "wallet-uuid",
"display_name": "Alice",
"signing_public_key": "abc123...",
"encryption_public_key": "def456..."
}
]
}
Send Message
POST /api/v2/messenger/messages
Content-Type: application/json
Requires a signed request. The sender is authenticated via the ML-DSA-65 signature.
Request Body (signed)
The payload includes the message fields plus from, timestamp, and nonce:
{
"payload": {
"conversationId": "conv-uuid",
"senderPublicKey": "sender-pub-hex",
"recipientPublicKey": "recipient-pub-hex",
"senderEncrypted": "base64-encrypted-for-sender",
"recipientEncrypted": "base64-encrypted-for-recipient",
"senderDisplayName": "Alice",
"recipientDisplayName": "Bob",
"selfDestruct": false,
"destructSeconds": 0,
"from": "sender-pub-hex",
"timestamp": 1710100000000,
"nonce": "random-hex-nonce"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "sender-pub-hex"
}
Messages are encrypted twice — once for the sender (so they can read their sent messages) and once for the recipient. The server stores both ciphertexts.
Self-Destruct Messages
Set selfDestruct: true and destructSeconds to a value. After the recipient reads the message, it is marked as read and hidden from the UI.
Response
{
"success": true,
"messageId": "msg-uuid"
}
Get Messages
POST /api/v2/messenger/messages/list
Content-Type: application/json
Requires a signed request. The server verifies the caller is a participant in the conversation.
Request Body (signed)
{
"payload": {
"conversationId": "conv-uuid",
"from": "your-pub-hex",
"timestamp": 1710100000000,
"nonce": "random-hex-nonce"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "your-pub-hex"
}
Response
{
"messages": [
{
"id": "msg-uuid",
"conversationId": "conv-uuid",
"senderPublicKey": "abc...",
"senderDisplayName": "Alice",
"encrypted": "base64-ciphertext",
"timestamp": 1706745600000,
"selfDestruct": false,
"mediaType": null
}
]
}
Mark Message as Read
POST /api/v2/messenger/messages/read
Content-Type: application/json
Used for self-destruct messages. Requires a signed request with messageId and conversationId in the payload.
Create Conversation
POST /api/v2/messenger/conversations
Content-Type: application/json
Signed request with participantIds (wallet ids or public keys; the creator must be included),
optional name, optional isGroup.
1:1 conversations have a deterministic id and creation is an upsert. For two distinct
participants (and isGroup not set) the id is
"dm_" + hex(sha256(sorted([idA, idB]).join("\n"))) over the two canonical wallet ids, so
Alice→Bob and Bob→Alice, or the same person addressed by wallet id, signing key or encryption key,
all resolve to the same thread. If it already exists it is returned unchanged with
"existing": true (and, if the caller had trashed it, it comes back to their inbox). Groups keep
random ids and are never deduplicated.
Response
{ "success": true, "existing": false, "conversationId": "dm_9f2c…", "conversation": { "id": "dm_9f2c…", "participant_ids": ["…"], "is_group": false, "created_at": "…" } }
Delete Message
POST /api/v2/messenger/messages/delete
Content-Type: application/json
Signed request with messageId and conversationId. Only the sender may delete. The delete is
soft: the message disappears from listings for everyone but stays recoverable by the sender for
30 days via POST /api/v2/messenger/messages/restore (same payload). After 30 days the sweep
removes it permanently.
Delete Conversation
POST /api/v2/messenger/conversations/delete
Content-Type: application/json
Signed request with conversationId, optional "purge": true. The caller must be a participant.
Deleting affects only the caller's view (this changed from the previous global hard delete):
the conversation moves to the caller's trash and disappears from their …/list; every other
participant keeps the thread and all its messages. It stays recoverable for 30 days; purge: true
waives the caller's retention window. The record and its messages are removed permanently only once
every participant has deleted it and each share is purged or past retention.
Response
{ "success": true, "conversationId": "dm_9f2c…", "softDeleted": true, "purge": false, "recoverableUntil": "2026-10-25T14:00:00Z" }
Restore Conversation
POST /api/v2/messenger/conversations/restore
Content-Type: application/json
Signed request with conversationId. Clears the caller's delete; the thread and its messages
reappear in the caller's inbox. Fails if the conversation is not in the caller's trash (or was purged).
Restore Message
POST /api/v2/messenger/messages/restore
Content-Type: application/json
Signed request with conversationId and messageId; sender only; within the 30-day window.
Get Conversations
POST /api/v2/messenger/conversations/list
Content-Type: application/json
Signed request. Optional "folder": "inbox" (default — not deleted by the caller), "trash"
(deleted by the caller, still recoverable; use it for a "Recently deleted" view), or "all".
Response
{
"success": true,
"folder": "inbox",
"conversations": [
{
"id": "dm_9f2c…",
"participant_ids": ["…", "…"],
"is_group": false,
"name": null,
"created_at": "2026-09-25T12:00:00Z",
"deleted_by": {},
"last_message_at": "2026-09-25T12:30:00Z",
"last_sender_id": "…",
"last_message_preview": "[Encrypted message]",
"unread_count": 2
}
]
}
deleted_by maps the canonical wallet id of each participant who trashed the thread to the time
they did; a conversation in your trash folder has your id in it.
Real-time Events (WebSocket)
New messages are pushed over wss://api.rougechain.io/api/ws so clients don't need to poll. Message events are private. The node sends them only to sockets that proved they own a participant's signing key.
1. Authenticate by sending a signed request, the same format as the v2 REST endpoints, with action set to messenger_ws_subscribe:
{ "auth": { "payload": { "action": "messenger_ws_subscribe", "from": "<signing pubkey>", "timestamp": 1790000000000, "nonce": "<random hex>" },
"signature": "<ML-DSA-65 signature over the sorted-key JSON payload>",
"public_key": "<signing pubkey>" } }
The node replies {"type":"subscribed","topics":["messenger"]} or {"type":"auth_error","error":"..."}. Nonces are single-use and timestamps must be within 5 minutes, so re-sign on every reconnect. One socket can authenticate up to 8 identities.
2. Receive an event for every message stored in a conversation you belong to, including your own sends:
{ "type": "new_message", "conversation_id": "dm_…", "message_id": "…", "created_at": "2026-09-25T18:40:00Z",
"sender_wallet_id": "<sender signing pubkey>", "participant_ids": ["<signing pubkey>", "…"] }
Events never contain message content. Refetch the conversation with POST /api/v2/messenger/messages/list. Unauthenticated sockets never receive message events, and inbox:* topics cannot be joined with a plain subscribe. Public events (new_block, new_transaction, stats) are unchanged. Keep a slow fallback poll for when the socket is down.
Media Messages
The messenger supports image and video attachments. Media is encrypted and sent as base64 within the message payload.
| Limit | Value |
|---|---|
| Max upload size | 50 MB (before compression) |
| Compressed target | ~1.5 MB |
| Image format | Converted to WebP client-side |
| Video format | Converted to WebM (VP9) client-side |
The client automatically compresses large media before encryption and sending.
Encryption Flow
Sender Recipient
│ │
│ 1. Generate shared secret via ML-KEM-768 │
│ 2. Derive AES-256 key via HKDF │
│ 3. Encrypt message with AES-GCM │
│ 4. Encrypt for self (sender copy) │
│ 5. Encrypt for recipient │
│ │
│── POST /messages (both ciphertexts) ────►│
│ │
│ 6. Decapsulate shared secret
│ 7. Derive same AES key
│ 8. Decrypt message
Wallet Blocking
Users can block wallets client-side. Blocked wallets are filtered from conversations and contacts. The block list is stored in browser localStorage under pqc_blocked_wallets.
This is a client-side feature — the server is not involved. Blocked users can still send messages, but they won't appear in the blocking user's UI.
SDK Usage
The @rougechain/sdk provides a high-level API for messenger operations. All write operations now require a wallet parameter for ML-DSA-65 request signing:
import { RougeChain, Wallet } from "@rougechain/sdk";
const rc = new RougeChain("https://testnet.rougechain.io/api");
const wallet = Wallet.generate();
// Register wallet (signed request, required for receiving messages and mail)
await rc.messenger.registerWallet(wallet, {
id: wallet.publicKey,
displayName: "Alice",
signingPublicKey: wallet.publicKey,
encryptionPublicKey: encPubKey,
});
// Conversations (signed requests)
const convos = await rc.messenger.getConversations(wallet);
await rc.messenger.createConversation(wallet, [wallet.publicKey, recipientPubKey]);
// Messages (signed requests)
const msgs = await rc.messenger.getMessages(wallet, conversationId);
await rc.messenger.sendMessage(wallet, conversationId, encryptedContent, {
selfDestruct: true,
destructAfterSeconds: 30,
});
await rc.messenger.markRead(wallet, messageId);
await rc.messenger.deleteMessage(wallet, messageId);
TypeScript types: MessengerWallet, MessengerConversation, MessengerMessage
Regenerate Community Votes API
Interim governance for the RougeChain Regenerate treasury. Proposals and votes are hosted by the node, outside consensus, until on-chain governance ships. Every vote is a wallet-signed request, stored with its signature so anyone can recount.
Rules
Set per proposal when it opens, and never changed afterwards:
| Rule | Default | Node setting |
|---|---|---|
| Voting weight | XRGE balance when the proposal opened (snapshot) | — |
| Excluded wallets | Team, treasury and exchange wallets | QV_REGEN_EXCLUDED, QV_REGEN_TREASURY |
| Per-wallet cap | 5% of eligible supply | QV_REGEN_CAP_BPS |
| Turnout to pass | 10% of eligible supply (yes + no + abstain) | QV_REGEN_TURNOUT_BPS |
| Majority | More yes than no | — |
| Voting window | 7 days (1–30) | QV_REGEN_DEFAULT_DAYS |
| Opening a proposal | Curator, or 10,000 XRGE and one open proposal at a time | QV_REGEN_CURATORS, QV_REGEN_MIN_CREATOR_XRGE |
Balances acquired after a proposal opens carry no vote on it. A wallet can change its vote until the window closes; the latest vote counts.
Read endpoints
| Endpoint | Returns |
|---|---|
GET /api/regen/config | Treasury, cap, turnout, default window, excluded wallets, curators |
GET /api/regen/proposals | Every proposal, newest first, with tally and status |
GET /api/regen/proposals/:id | One proposal with every vote, including signedPayload and signature |
GET /api/regen/proposals/:id/weight/:who | A wallet's snapshot weight, eligibility and current vote (:who is a rouge1 address or public key) |
Status is one of open, passed, failed, paid or cancelled. Quanta amounts are decimal strings (1 XRGE = 10⁹ quanta); summaryXrge repeats the totals in XRGE.
Signed endpoints
All use the standard signed request format (payload with from, timestamp, nonce; ML-DSA-65 signature; public_key).
| Endpoint | Payload | Who |
|---|---|---|
POST /api/v2/regen/proposals | title, summary, optional territory, recipient (rouge1), requestedXrge, link (https), durationDays | Curator or eligible holder |
POST /api/v2/regen/votes | proposalId, choice (yes, no, abstain) | Any wallet with snapshot weight |
POST /api/v2/regen/proposals/payout | proposalId, txId | Curator, for a passed proposal |
POST /api/v2/regen/proposals/cancel | proposalId, optional reason | Curator, or the creator before any votes |
A payout is accepted only if txId is an on-chain XRGE transfer from the treasury, and to the proposal's recipient when one is set.
Recounting
Fetch GET /api/regen/proposals/:id. For each vote, verify signature over the sorted-key JSON of signedPayload against publicKey, derive the rouge1 address from publicKey, and sum weights by choice. Weights come from the snapshot taken at snapshotHeight.
Mail API
Endpoints for the PQC-encrypted mail system. Mail uses ML-KEM-768 encryption with a Content Encryption Key (CEK) pattern for multi-recipient support, ML-DSA-65 unified signatures, and name registry with atomic registration.
v2 API (March 2026): All write operations now use
/api/v2/endpoints that require ML-DSA-65 signed requests with timestamp validation and nonce-based anti-replay protection. Legacy unsigned endpoints return HTTP 410 (Gone) in production; since node release 1.6.3 (2026-10-05) so do the legacy read routesGET /api/mail/inbox,/api/mail/sent,/api/mail/trashand/api/mail/message/:id— use the signedPOST /api/v2/mail/folderand/api/v2/mail/messagebelow. Mail is node-hosted (stored by the node, not in blocks); a mailbox is owned by the ML-DSA-65 signing key that registered the wallet, and mail names and folders follow that wallet only.
Signed Request Format
All v2 write endpoints accept a signed request body:
{
"payload": {
"name": "alice",
"walletId": "wallet-uuid",
"from": "ml-dsa65-public-key-hex",
"timestamp": 1710100000000,
"nonce": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "ml-dsa65-public-key-hex"
}
The server verifies: (1) the ML-DSA-65 signature, (2) the timestamp is within a 5-minute window, (3) the from field matches the signing key, (4) the nonce has not been used before, and (5) the caller is authorized for the operation.
Name Registry
Important for third-party apps: Before a user can receive mail, two things must be registered on the node:
- Their wallet (via
/api/v2/messenger/wallets/register) — provides the encryption key - Their mail name (via
/api/v2/names/register) — maps a human-readable name to the wallet
Register a Name
POST /api/v2/names/register
Content-Type: application/json
Register a human-readable email address (e.g., alice@rouge.quant). Requires a signed request. Registration uses atomic compare-and-swap to prevent race conditions.
Request (signed):
{
"payload": {
"name": "alice",
"walletId": "wallet-uuid-or-public-key",
"from": "signing-public-key-hex",
"timestamp": 1710100000000,
"nonce": "random-hex-nonce"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "signing-public-key-hex"
}
Response:
{
"success": true,
"entry": {
"name": "alice",
"wallet_id": "wallet-uuid-or-public-key",
"registered_at": "2026-03-11T00:00:00Z"
}
}
Error:
{
"success": false,
"error": "Name already taken"
}
Resolve a Name
GET /api/names/resolve/:name
Returns the name entry and associated wallet (with encryption keys).
Response:
{
"success": true,
"entry": {
"name": "alice",
"wallet_id": "wallet-uuid",
"registered_at": "2026-03-11T00:00:00Z"
},
"wallet": {
"id": "wallet-uuid",
"display_name": "Alice",
"signing_public_key": "abc123...",
"encryption_public_key": "def456..."
}
}
Reverse Lookup (Wallet ID → Name)
GET /api/names/reverse/:walletId
Response:
{
"success": true,
"name": "alice"
}
Release a Name
POST /api/v2/names/release
Content-Type: application/json
Requires a signed request. The caller must own the name (verified via signing key).
{
"payload": {
"name": "alice",
"from": "signing-public-key-hex",
"timestamp": 1710100000000,
"nonce": "random-hex-nonce"
},
"signature": "ml-dsa65-signature-hex",
"public_key": "signing-public-key-hex"
}
Send Mail
POST /api/v2/mail/send
Content-Type: application/json
Requires a signed request. The sender is authenticated via ML-DSA-65 signature verification.
Request Body
{
"from": "alice",
"fromPublicKey": "sender-pub-hex",
"to": "bob",
"toPublicKey": "recipient-pub-hex",
"senderEncrypted": "base64-encrypted-for-sender",
"recipientEncrypted": "base64-encrypted-for-recipient",
"replyToId": null
}
The senderEncrypted and recipientEncrypted fields contain the encrypted subject and body. Both sender and recipient get their own copy, just like the messenger.
Set replyToId to the ID of the mail being replied to, enabling threading.
Response
{
"success": true,
"id": "mail-uuid"
}
Get Inbox
POST /api/v2/mail/folder
Content-Type: application/json
Requires a signed request with "folder": "inbox" in the payload. The caller's wallet is resolved from the signing key.
Response
{
"mail": [
{
"id": "mail-uuid",
"from": "alice",
"fromPublicKey": "abc...",
"to": "bob",
"toPublicKey": "def...",
"encrypted": "base64-ciphertext",
"timestamp": 1706745600000,
"read": false,
"replyToId": null
}
]
}
Get Sent Mail
POST /api/v2/mail/folder
Content-Type: application/json
Same as inbox but with "folder": "sent" in the payload. Requires a signed request.
Get Single Mail
POST /api/v2/mail/message
Content-Type: application/json
Requires a signed request with messageId in the payload.
Mark as Read
POST /api/v2/mail/read
Content-Type: application/json
Requires a signed request with messageId in the payload.
Move to Folder
POST /api/v2/mail/move
Content-Type: application/json
Requires a signed request with messageId and folder in the payload. Valid folders: inbox, sent, trash, starred, drafts.
Get Trash
POST /api/v2/mail/folder
Content-Type: application/json
Same as inbox/sent but with "folder": "trash" in the payload. Requires a signed request.
Delete Mail
POST /api/v2/mail/delete
Content-Type: application/json
Permanently deletes a mail item. Requires a signed request with messageId in the payload. The caller must be a participant (sender or recipient).
Email Domains
| Domain | Platform |
|---|---|
@rouge.quant | Website and browser extensions |
@qwalla.mail | QWalla mobile app (future) |
Threading
Mail threading is handled client-side by following the replyToId chain. When viewing a mail, the client:
- Fetches both inbox and sent mail
- Walks the
replyToIdchain to build the thread - Displays messages in chronological order
- Collapses older messages, expands the latest two
Encryption
Mail uses a Content Encryption Key (CEK) pattern for efficient multi-recipient support:
- Generate a random 256-bit AES key (the CEK)
- Encrypt subject, body, and attachment with the CEK via AES-256-GCM
- For each recipient (and the sender): encapsulate a shared secret via ML-KEM-768, derive a wrapping key via HKDF, and encrypt the CEK with AES-GCM
- Sign the concatenation of all encrypted parts (subject + body + attachment) with ML-DSA-65 (unified signature)
- Server stores the encrypted content with per-recipient wrapped keys
Attachments
Mail supports encrypted file attachments up to 2 MB. Attachments are encrypted client-side using the same ML-KEM-768 scheme as the message body.
Sending with Attachment
Include the attachmentEncrypted and hasAttachment fields in the send request:
{
"fromWalletId": "sender-wallet-id",
"toWalletIds": ["recipient-wallet-id"],
"subjectEncrypted": "ml-kem-encrypted-subject",
"bodyEncrypted": "ml-kem-encrypted-body",
"attachmentEncrypted": "ml-kem-encrypted-attachment-json",
"signature": "ml-dsa-65-signature",
"replyToId": null,
"hasAttachment": true
}
Attachment Payload
The attachmentEncrypted field contains an ML-KEM encrypted JSON string:
{
"name": "photo.jpg",
"type": "image/jpeg",
"data": "base64-encoded-file-data",
"size": 102400
}
| Field | Type | Description |
|---|---|---|
name | string | Original filename |
type | string | MIME type |
data | string | Base64-encoded file content |
size | number | File size in bytes |
Reading Attachments
When fetching mail (inbox/sent/trash), messages with attachments will include the attachment_encrypted field. The client must:
- Decrypt using the recipient's ML-KEM-768 private key
- Parse the JSON to extract the attachment metadata and data
- Display inline (images) or offer download (other types)
SDK Usage
The @rougechain/sdk provides a high-level API for name registry and mail operations. All write operations now require a wallet parameter for ML-DSA-65 request signing:
import { RougeChain, Wallet } from "@rougechain/sdk";
const rc = new RougeChain("https://testnet.rougechain.io/api");
const wallet = Wallet.generate();
// Register wallet + name (signed requests)
await rc.messenger.registerWallet(wallet, {
id: wallet.publicKey,
displayName: "Alice",
signingPublicKey: wallet.publicKey,
encryptionPublicKey: encPubKey,
});
await rc.mail.registerName(wallet, "alice", wallet.publicKey);
// Resolve a recipient before sending (public, no signing needed)
const recipient = await rc.mail.resolveName("bob");
// recipient.wallet.encryption_public_key → use for ML-KEM encryption
// Reverse lookup (public, no signing needed)
const name = await rc.mail.reverseLookup(wallet.publicKey); // "alice"
// Send, read, manage mail (all signed requests)
await rc.mail.send(wallet, { from, to, subject, body, encrypted_subject, encrypted_body });
const inbox = await rc.mail.getInbox(wallet);
await rc.mail.markRead(wallet, messageId);
await rc.mail.move(wallet, messageId, "trash");
await rc.mail.delete(wallet, messageId);
TypeScript types: NameEntry, ResolvedName, MailMessage, SendMailParams
Push Notifications API
Register and unregister Expo push tokens for real-time mobile notifications. Both endpoints require ML-DSA-65 signed payloads — only the wallet owner can register or unregister their token.
Register Push Token
POST /api/push/register
Content-Type: application/json
Request (SignedTransactionRequest)
{
"payload": {
"type": "push_register",
"from": "wallet-public-key",
"pushToken": "ExponentPushToken[xxx]",
"platform": "expo",
"timestamp": 1234567890123,
"nonce": "random-hex"
},
"signature": "ml-dsa-65-signature-hex",
"public_key": "wallet-public-key"
}
Response
{
"success": true
}
Unregister Push Token
POST /api/push/unregister
Content-Type: application/json
Request (SignedTransactionRequest)
{
"payload": {
"type": "push_unregister",
"from": "wallet-public-key",
"timestamp": 1234567890123,
"nonce": "random-hex"
},
"signature": "ml-dsa-65-signature-hex",
"public_key": "wallet-public-key"
}
Response
{
"success": true
}
Security
Push token registration is secured with the same SignedTransactionRequest pattern used by v2 API endpoints:
- The payload is signed client-side with the wallet's ML-DSA-65 private key
- The server verifies the signature against the
public_key - Timestamp must be within 5 minutes to prevent replay attacks
- Only the wallet owner can register a push token for their public key
This prevents spoofing — an attacker cannot register a rogue push token for someone else's wallet.
SDK Usage
import { RougeChain } from "@rougechain/sdk";
const rc = new RougeChain("https://testnet.rougechain.io/api");
// Register — pass the wallet, not raw keys (signing happens inside)
await rc.registerPushToken(wallet, "ExponentPushToken[xxx]"); // platform defaults to "expo"
// Unregister
await rc.unregisterPushToken(wallet);
API Reference — Bridge
ETH/USDC Bridge
Get Bridge Config
GET /api/bridge/config
Returns bridge status, custody address, chain ID, and supported tokens.
Response:
{
"enabled": true,
"custodyAddress": "0x...",
"chainId": 8453,
"supportedTokens": ["ETH", "USDC"]
}
enabledisfalsewhen the custody address is unset.chainIdis8453on Base mainnet (84532on Base Sepolia).ETH (→ qETH) and USDC (→ qUSDC) are claimable through this endpoint.
"BTC"also appears insupportedTokenswhen the separate Bitcoin bridge is configured. These endpoints are the R1 production bridge; no V3 endpoint is live.When the Bitcoin bridge is configured the response also carries
btcCustodyAddress,btcNetwork,btcMinWithdrawSats(minimum qBTC withdrawal, default 2000) andbtcMaxNetworkFeeSats(largest Bitcoin network fee deducted from a payout, default 10000). See Bitcoin Bridge → Fees.
Claim Bridge Deposit
POST /api/bridge/claim
Claim wrapped qETH or qUSDC after depositing ETH or USDC on Base. Closed at the public mainnet endpoint (api.rougechain.io refuses it at the reverse proxy): deposits made through the contract's depositETH / depositERC20 are claimed automatically — see the auto-claim note below. Documented for operators running their own node.
Body:
{
"evmTxHash": "0x...",
"evmAddress": "0x...",
"evmSignature": "0x...",
"recipientRougechainPubkey": "abc123...",
"token": "ETH"
}
The token field is "ETH" (default, mints qETH) or "USDC" (mints qUSDC).
The node verifies the on-chain deposit (the Transfer to custody, not a caller-supplied
amount), checks the EVM signature, requires the configured confirmation depth
(QV_BRIDGE_MIN_CONFIRMATIONS, default 6), and then mints qETH.
Auto-claim: with the deposit watcher enabled (default), the relayer detects
BridgeDepositETH/BridgeDepositERC20events on Base and claims them for you via/api/bridge/deposit/auto-claim— no manual claim needed. This endpoint remains in the node as an operator fallback but is not open to the public. Claims are deduped, so an auto-claim and a manual claim of the same deposit cannot double-mint.
Bridge Withdraw
POST /api/bridge/withdraw
Burn wrapped tokens and create a pending withdrawal for the relayer.
Body (signed):
{
"fromPublicKey": "abc123...",
"amountUnits": 10000,
"evmAddress": "0x...",
"signature": "...",
"payload": { "type": "bridge_withdraw", "..." }
}
List Pending Withdrawals
GET /api/bridge/withdrawals
Returns pending ETH/USDC withdrawals waiting for the relayer. XRGE withdrawals are
excluded here — they are served by /api/bridge/xrge/withdrawals.
Response:
{
"withdrawals": [
{
"txId": "0x...",
"evmAddress": "0x...",
"amountUnits": 10000,
"createdAt": 1717000000000,
"ownerPubkey": "abc123...",
"tokenSymbol": "qETH",
"status": "pending",
"attempts": 0,
"lastError": null
}
]
}
| Field | Meaning |
|---|---|
ownerPubkey | RougeChain L1 key of the withdrawer — the refund recipient |
tokenSymbol | qETH / qUSDC / XRGE; authoritative type discriminator (replaces the legacy xrge: tx-id prefix) |
status | pending · failed (relayer retrying) · fulfilled · refunded |
attempts | Failed relayer release attempts so far |
lastError | Last release error, when status is failed |
Fulfill Withdrawal
DELETE /api/bridge/withdrawals/:txId
Mark a withdrawal as fulfilled (relayer calls this after releasing on Base). Requires
x-bridge-relayer-secret header or a PQC-signed operator body.
Report Withdrawal Failure
POST /api/bridge/withdrawals/:txId/failure
Relayer reports a failed release attempt. Increments attempts, records lastError,
and sets status: failed. Auth: relayer secret or operator signature.
Body: { "error": "release tx reverted: 0x..." }
Response: { "success": true, "attempts": 3, "shouldRefund": false, "threshold": 5 }
When attempts reaches the threshold (5), shouldRefund becomes true and the
daemon logs an alert.
Refund Withdrawal
POST /api/bridge/withdrawals/:txId/refund
Refund a withdrawal that could not be released: re-mints the burned amountUnits of
tokenSymbol back to ownerPubkey via a bridge_mint, then clears the pending entry.
Deduped with a refund:<txId> claim-store key so it can never refund twice. Auth:
relayer secret or operator signature.
Response: { "success": true, "txId": "<l1-mint-tx>", "amount": 10000, "token": "qETH", "recipient": "abc123..." }
Deposit Auto-claim
POST /api/bridge/deposit/auto-claim
Used by the relayer's deposit watcher to mint a deposit discovered on Base without a browser claim. Re-verifies the EVM tx on-chain and dedupes against manual claims (idempotent). Auth: relayer secret or operator signature.
Body:
{
"evmTxHash": "0x...",
"recipientRougechainPubkey": "abc123...",
"token": "ETH"
}
token is ETH / USDC / XRGE, taken from the on-chain deposit event.
Admin Reclaim
POST /api/bridge/admin/reclaim
Manually process a missed deposit. Same verified mint path as auto-claim, but
authenticated with adminKey (requires QV_ADMIN_KEY set on the daemon).
Body: { "evmTxHash": "0x...", "recipientRougechainPubkey": "abc123...", "token": "ETH", "adminKey": "..." }
XRGE Bridge
Get XRGE Bridge Config
GET /api/bridge/xrge/config
Response:
{
"enabled": true,
"vaultAddress": "0x...",
"tokenAddress": "0x147120faEC9277ec02d957584CFCD92B56A24317",
"chainId": 8453
}
Claim XRGE Deposit
POST /api/bridge/xrge/claim
Body:
{
"evmTxHash": "0x...",
"evmAddress": "0x...",
"evmSignature": "0x...",
"recipientRougechainPubkey": "abc123..."
}
evmSignature is required — sign the claim message with the wallet that sent the
XRGE. The mint amount and depositor are derived from the on-chain
Transfer(from → vault) log emitted by the XRGE token; any caller-supplied amount
is ignored. The deposit must reach the required confirmation depth
(QV_BRIDGE_MIN_CONFIRMATIONS, default 6) before it mints.
XRGE Withdraw
POST /api/bridge/xrge/withdraw
Body (signed):
{
"fromPublicKey": "abc123...",
"amount": 100,
"evmAddress": "0x...",
"signature": "...",
"payload": { "..." }
}
List XRGE Withdrawals
GET /api/bridge/xrge/withdrawals
Returns pending XRGE withdrawals (filtered by tokenSymbol == "XRGE"). Each item
carries the same status / attempts / ownerPubkey / tokenSymbol fields as
the ETH listing.
Fulfill XRGE Withdrawal
DELETE /api/bridge/xrge/withdrawals/:txId
Failure / Refund
XRGE withdrawals reuse the shared withdrawal endpoints by txId:
POST /api/bridge/withdrawals/:txId/failure and .../refund (see above). The relayer
reports failures and the refund re-mints XRGE to ownerPubkey.
Bitcoin Bridge
Get BTC Deposit Address
POST /api/bridge/btc/deposit-address
Returns a Bitcoin deposit address bound to a RougeChain recipient. The address is stable per recipient: asking again for the same recipient returns the same address. BTC sent to it is credited as qBTC to that recipient. The node only stores addresses (watch-only); they are derived and swept by the relayer.
Request:
{ "recipient": "rouge1..." }
Response:
{ "success": true, "address": "bc1q...", "error": null }
On failure success is false, address is null and error explains why (bridge not
enabled, missing recipient, or no address available yet while the relayer refills its pool —
retry shortly). The custody address with an OP_RETURN memo (see
Bitcoin Bridge) remains a fallback.
POST /api/bridge/btc/deposit-pool and GET /api/bridge/btc/deposit-addresses are
relayer-only (relayer secret required).
Bridge Activity (public, read-only)
Every cross-chain bridge transfer (deposits into RougeChain and withdrawals out of it) with its status and the transaction on both chains. Built only from accepted blocks, the mempool and the node's existing bridge stores; nothing is written. No signature or relayer secret is involved, and no relayer internals are returned.
List Bridge Activity
GET /api/bridge/activity?limit=25&before=202-0
| Query | Meaning |
|---|---|
limit | 1–100, default 25. |
before | Pagination cursor: <height>-<index> (from cursor / nextCursor) or a bare <height> (everything below that block). Omit for the newest page. |
Newest first. Transactions still in the mempool lead the first page (with blockHeight,
timestamp and cursor = null); pages read with before contain only transactions in blocks.
{
"items": [
{
"kind": "withdrawal",
"asset": "qBTC",
"externalAsset": "BTC",
"amountUnits": 5000,
"decimals": 8,
"fromChain": "rougechain",
"toChain": "bitcoin",
"rougechainTxId": "d7df36b845ad0d147af6d717c2a8860026bbdb442c5375990a32e0efe6eb4c2c",
"rougechainAddress": "rouge1…",
"blockHeight": 202,
"externalChainId": "bitcoin",
"externalNetwork": "mainnet",
"externalAddress": "bc1qvt4r5dazmystwspgp62vh9ve5tutw5av4atjcz",
"externalTxHash": "91d306fcedfc15ce8cb8f1c547c5c2d403a20d138a74cc66fbb4a5e104259b84",
"status": "paid",
"statusReason": "payout_verified",
"timestamp": 1790716492488,
"statusUpdatedAt": 1790717000000,
"cursor": "202-0"
}
],
"nextCursor": "198-1",
"limit": 25
}
| Field | Meaning |
|---|---|
kind | deposit (a bridge_mint) or withdrawal (a bridge_withdraw). |
asset / externalAsset | Token on RougeChain (qETH, qUSDC, qBTC, XRGE) and on the external chain (ETH, USDC, BTC, XRGE; null for a non-bridge token). |
amountUnits / decimals | Raw on-chain amount and its decimals: qBTC 8 (1 unit = 1 sat), qETH / qUSDC 6, XRGE 0. |
fromChain / toChain | rougechain, base, base-sepolia, bitcoin, bitcoin-testnet, or unknown. |
rougechainTxId / blockHeight / timestamp | The RougeChain transaction, its block and block time (ms). |
rougechainAddress | rouge1 address of the withdrawal sender or the deposit recipient. |
externalChainId | "8453" (Base), "84532" (Base Sepolia), "bitcoin", or null when the node has no Base chain configured. |
externalNetwork | base, base-sepolia, or for Bitcoin mainnet / testnet. |
externalAddress | Withdrawals: the destination signed in the transaction (0x… or a Bitcoin address). Deposits: null. |
externalTxHash | Withdrawals: the payout transaction, only once the payout was verified (paid). Deposits: null. |
status / statusReason | See below. |
statusUpdatedAt | When the node's payout record last changed (withdrawals), else null. |
cursor | This item's pagination position (null in the mempool). |
Status
status | statusReason | Meaning |
|---|---|---|
pending | in_mempool | Submitted, not in a block yet. |
queued | awaiting_payout | Withdrawal accepted on-chain, waiting for the relayer to pay out. |
paid | payout_verified | Withdrawal paid; the payout was verified on the external chain. |
paid | minted | Deposit minted on RougeChain. |
failed | rejected_on_chain | The transaction's receipt is failed (nothing was burned or minted). |
failed | payout_retrying | Payout attempts have failed so far; the relayer keeps retrying (or refunds). |
refunded | refunded_on_rougechain | The payout could not complete and the tokens were minted back to the sender. |
unknown | no_payout_record | The node holds no payout record for this withdrawal (e.g. before the payout store existed). |
Always null (not recorded by the node, never guessed): the external source transaction
and sender of a deposit. The daemon's claim store keeps source hashes only as a replay-protection
set without a link to the mint transaction. Every bridge_mint is listed as a deposit, which
includes refund mints and testnet test mints.
Not exposed: relayer error strings, attempt counters, owner public keys, secrets. A payout
hash or address that is not in the expected format for its chain is returned as null.
Errors: 400 for a bad limit / before; 503 {"error":"bridge state degraded","degraded":true}
while the derived bridge payout state is degraded (the same fail-closed rule as
/api/bridge/health); 503 {"error":"bridge activity unavailable"} on an internal read failure.
Get One Bridge Transfer
GET /api/bridge/activity/:txId
txId is the 64-character lowercase hex RougeChain transaction id (an xrge: prefix is
accepted). Returns a single item (same fields as above), 404 when the transaction is not a
bridge transaction or is unknown, 400 for a malformed id, 503 as above.
API Reference — NFTs
All write operations use the v2 signed transaction API (client-side signing).
Read Endpoints
List Collections
GET /api/nft/collections
Get Collection
GET /api/nft/collection/:id
Get Collection Tokens
GET /api/nft/collection/:id/tokens
Get Token
GET /api/nft/token/:collection_id/:token_id
Get NFTs by Owner
GET /api/nft/owner/:pubkey
Matches the owner exactly as recorded — normally the owner's hex public key. Querying with a rouge1… address does not return NFTs owned by the public key.
Write Endpoints (v2 Signed)
All write endpoints accept a signed transaction body:
{
"payload": { "type": "nft_mint", "..." },
"signature": "...",
"public_key": "..."
}
Create Collection
POST /api/v2/nft/collection/create
Payload fields: symbol, name, maxSupply, royaltyBps, royaltyRecipient, image, description
Fee: 50 XRGE
royaltyBps— royalty in basis points (500= 5%). From CONTRACT_NFT_ROYALTY activation it must be an integer0–10000; anything else (above 100%, negative, fractional) returns400 royaltyBps must be an integer between 0 and 10000.royaltyRecipient— (optional) wallet that receives secondary-sale royalties. Defaults to the creator when omitted, and is fixed at creation. A wallet address or a deployed contract address (for example a royalty-splitter contract, which pays collaborators on-chain). ⚠️ The address is not validated: royalties sent to a mistyped address, or to a contract with no way to pay XRGE out, cannot be recovered.
Mint NFT
POST /api/v2/nft/mint
Payload fields: collectionId, name, metadataUri, attributes
Fee: 5 XRGE
Batch Mint
POST /api/v2/nft/batch-mint
Payload fields: collectionId, names, uris, attributes (array, one entry per NFT)
Fee: 5 XRGE per NFT
Note: the node reads per-NFT attributes only from
attributes.@rougechain/sdk1.11.0+ and the current site sign them there (batchMint({ ..., attributes }); the oldbatchAttributesoption is a deprecated alias). SDK ≤ 1.10.0 sent abatchAttributespayload field, which the node ignores, so attributes passed through those versions were not stored — upgrade the SDK. When signing payloads yourself, useattributes.
Transfer NFT
POST /api/v2/nft/transfer
Payload fields: collectionId, tokenId, to, salePrice
Fee: 1 XRGE
When salePrice > 0 and the collection has a royalty, the sender also pays salePrice × royaltyBps / 10000 XRGE to the collection's royaltyRecipient (on top of the fee). salePrice is self-declared — a transfer with no salePrice pays no royalty.
Burn NFT
POST /api/v2/nft/burn
Payload fields: collectionId, tokenId
Fee: 0.1 XRGE
Lock/Unlock NFT
POST /api/v2/nft/lock
Payload fields: collectionId, tokenId, locked
Fee: 0.1 XRGE
Freeze Collection
POST /api/v2/nft/freeze-collection
Payload fields: collectionId, frozen
Fee: 0.1 XRGE
API Reference — DEX / AMM
Read Endpoints
List Pools
GET /api/pools
Returns all liquidity pools with reserves, LP supply, and token info.
Response:
{
"success": true,
"pools": [
{
"pool_id": "XRGE-MTK",
"token_a": "MTK",
"token_b": "XRGE",
"reserve_a": 50000,
"reserve_b": 100000,
"total_lp_supply": 70710,
"fee_rate": 0.003
}
]
}
Get Pool
GET /api/pool/:pool_id
Pool ID format: TOKEN_A-TOKEN_B (alphabetically sorted).
Response:
{
"success": true,
"pool": {
"pool_id": "XRGE-MTK",
"token_a": "MTK",
"token_b": "XRGE",
"reserve_a": 50000,
"reserve_b": 100000,
"total_lp_supply": 70710,
"fee_rate": 0.003
}
}
Get Pool Events
GET /api/pool/:pool_id/events
Returns swap, add_liquidity, and remove_liquidity events for a pool.
Response:
{
"success": true,
"events": [
{
"id": "evt_abc123",
"pool_id": "XRGE-MTK",
"event_type": "Swap",
"user_pub_key": "abc123...",
"timestamp": 1710000000,
"block_height": 12345,
"tx_hash": "tx_abc...",
"token_in": "XRGE",
"token_out": "MTK",
"amount_in": 1000,
"amount_out": 490,
"reserve_a_after": 50490,
"reserve_b_after": 99000
}
]
}
Get Pool Price History
GET /api/pool/:pool_id/prices
Returns up to 500 price snapshots in chronological order. A snapshot is recorded after every swap, pool creation, and liquidity change.
Response:
{
"success": true,
"prices": [
{
"pool_id": "XRGE-MTK",
"timestamp": 1710000000,
"block_height": 12345,
"reserve_a": 50000,
"reserve_b": 100000,
"price_a_in_b": 2.0,
"price_b_in_a": 0.5
}
]
}
| Field | Description |
|---|---|
price_a_in_b | How many token_b for 1 token_a |
price_b_in_a | How many token_a for 1 token_b |
Get Pool Stats
GET /api/pool/:pool_id/stats
Response:
{
"success": true,
"stats": {
"pool_id": "XRGE-MTK",
"total_swaps": 142,
"total_volume_a": 500000,
"total_volume_b": 1000000,
"swap_count_24h": 12,
"volume_24h_a": 25000,
"volume_24h_b": 50000
}
}
Get Swap Quote
POST /api/swap/quote
Body:
{
"token_in": "XRGE",
"token_out": "qETH",
"amount_in": 1000
}
Response:
{
"success": true,
"amount_out": 95,
"price_impact": 0.5,
"path": ["XRGE", "qETH"],
"pools": ["XRGE-qETH"]
}
Get All Events
GET /api/events
Returns all DEX events across all pools.
Write Endpoints (v2 Signed)
Create Pool
POST /api/v2/pool/create
Payload fields: token_a, token_b, amount_a, amount_b
Fee: 10 XRGE
Add Liquidity
POST /api/v2/pool/add-liquidity
Payload fields: pool_id, amount_a, amount_b
Fee: 1 XRGE
Remove Liquidity
POST /api/v2/pool/remove-liquidity
Payload fields: pool_id, lp_amount
Fee: 1 XRGE
Execute Swap
POST /api/v2/swap/execute
Payload fields: token_in, token_out, amount_in, min_amount_out
Fee: 0.3% of input + 1 XRGE
SDK Usage
import { RougeChain, Wallet } from '@rougechain/sdk';
import type { PriceSnapshot, PoolEvent, PoolStats } from '@rougechain/sdk';
const rc = new RougeChain('https://testnet.rougechain.io/api');
// Read
const pools = await rc.dex.getPools();
const pool = await rc.dex.getPool('XRGE-MTK');
const prices: PriceSnapshot[] = await rc.dex.getPriceHistory('XRGE-MTK');
const stats: PoolStats = await rc.dex.getPoolStats('XRGE-MTK');
const events: PoolEvent[] = await rc.dex.getPoolEvents('XRGE-MTK');
// Quote & swap
const quote = await rc.dex.quote({ poolId: 'XRGE-MTK', tokenIn: 'XRGE', tokenOut: 'MTK', amountIn: 100 });
await rc.dex.swap(wallet, { tokenIn: 'XRGE', tokenOut: 'MTK', amountIn: 100, minAmountOut: 95 });
Smart Contracts API
Since mainnet block 150 every contract transaction is signed by the player: the signer of a
call is the caller the contract sees (host_get_caller) and pays the fee; the signer of a
deployment is the deployer. The old node-signed endpoints are retired:
POST /api/v2/contract/deploy returns 410 Gone and POST /api/v2/contract/call is a dry run
only (and is not reachable on the public mainnet edge). Use the endpoints below, or
rc.contracts in @rougechain/sdk 1.9.0+.
Signed requests use the standard /api/v2/* envelope { payload, signature, public_key }:
signature is ML-DSA-65 over the JSON of payload with keys sorted, payload.from must equal
public_key, and payload.timestamp (ms) must be within 5 minutes of the node clock.
Publish (Deploy) Contract
POST /api/v2/contract/publish
| Payload field | Type | Required | Description |
|---|---|---|---|
type | string | ✅ | "contract_deploy" |
from | string | ✅ | Deployer's signing public key (hex) |
wasm | string | ✅ | Base64-encoded WASM bytecode (max 1 MiB, must export memory) |
nonce | string | ✅ | Random string, at least 8 characters |
timestamp | number | ✅ | Milliseconds since the epoch |
Response:
{ "success": true, "txId": "…", "address": "<40 hex chars>", "fee": 10 }
Publish fee: 10 XRGE flat. The contract is installed when the tx is mined. Its address is fixed by the signed fields, so it is known before the block:
hex(sha256("rougechain/contract/v2" ‖ from ‖ 0x00 ‖ nonce ‖ 0x00 ‖ sha256(wasm))[0..20]).
Execute (Call) Contract
A state-changing call, included in a block.
POST /api/v2/contract/execute
| Payload field | Type | Required | Description |
|---|---|---|---|
type | string | ✅ | "contract_call" |
from | string | ✅ | Caller's signing public key (hex) |
contractAddr | string | ✅ | Contract address (hex, lower case) |
method | string | ✅ | Method name |
args | any JSON | ❌ | Arguments ({} when omitted) |
gasLimit | number | ✅ | Integer 1 – 10,000,000 |
attach | object | ❌ | From block 190: { "symbol": "XRGE" | "<TOKEN>", "amount": <positive integer> }. Pays the contract. See below |
timestamp | number | ✅ | Milliseconds since the epoch |
nonce | string | ✅ | Random string |
Response:
{
"success": true,
"txId": "…",
"fee": 0.05,
"preview": { "returnData": { }, "gasUsed": 31234, "events": [] }
}
Call fee:
gasLimit × 0.000001XRGE (the signed limit, charged up front). The node dry-runs the call first and refuses it ("call would fail: …", nothing charged) if it would fail or needs more gas than the limit. The authoritative run happens in the block: if the call reverts there (because state changed in between), it is still included and charged, and its receipt (GET /api/tx/:txId/receipt) reports"status": {"Failed": "<error>"}instead of"Success".
Payable calls: attach (from block 190)
"attach": { "symbol": "XRGE", "amount": 500000000 }
| Field | Type | Description |
|---|---|---|
attach.symbol | string | "XRGE" or a token symbol (1–32 letters, digits, _, -; upper-cased by the node) |
attach.amount | integer | Positive JSON integer: quanta for XRGE (1 XRGE = 1,000,000,000 quanta), raw units for a token. Decimals and strings are rejected |
attachis part of the signed payload.- The payment moves to the contract only if the call succeeds. If the call fails or traps, the payment stays with the caller, but the gas fee is still charged.
- The contract reads the payment with
host_get_attached_amount()andhost_get_attached_symbol(out, cap). It refuses a payment by failing the call. - The node refuses the request up front if the caller can't cover the gas fee plus an XRGE payment
(
"insufficient XRGE for the gas fee and the attached payment: …"), or the token amount ("insufficient <SYM> for the attached payment: …"), or if the dry run with the payment would fail. - Before block 190, the request fails with
"payable contract calls (attach) are not active yet". A malformedattachreturns"attach.amount must be a positive integer (quanta for XRGE, raw units for tokens)"or"attach.symbol must be a token symbol".
Query Contract (read-only)
A free dry run against the live state. Nothing is signed, charged or committed.
POST /api/contract/:addr/query
| Field | Type | Required | Description |
|---|---|---|---|
method | string | ✅ | Method name |
args | any JSON | ❌ | Arguments |
caller | string | ❌ | Public key the contract sees as host_get_caller. Required with attach |
attach | object | ❌ | Preview a paid call: same { symbol, amount } as execute (integer quanta for XRGE, raw token units). The contract sees the payment as it would in a block (from block 190; before that, the payment is ignored in the preview) |
Response:
{ "success": true, "returnData": { }, "gasUsed": 812, "events": [], "error": null }
Get Contract Metadata
GET /api/contract/:addr
Returns contract metadata: address, deployer, code hash, creation timestamp, WASM size.
{
"success": true,
"contract": {
"address": "86fe93e2...",
"deployer": "test-deployer",
"code_hash": "a1b2c3...",
"wasm_size": 711,
"created_at": 1774401569985
}
}
Read Contract Storage
Full State Dump
GET /api/contract/:addr/state
Returns all key-value pairs in the contract's persistent storage.
{
"success": true,
"state": {
"count": "42",
"owner": "alice"
},
"count": 2
}
Single Key Lookup
GET /api/contract/:addr/state?key=<hex_key>
Reads a single key from storage.
{
"success": true,
"key": "636f756e74",
"value": "3432",
"valueUtf8": "42"
}
Get Contract Events
GET /api/contract/:addr/events?limit=50&before=<height>&tx=<txhash>
Returns indexed events emitted by the contract, newest first. limit is 1–1000 (default 50),
before returns only events from blocks below that height (paging), and tx returns only the
events of one transaction.
{
"success": true,
"events": [
{
"contract_addr": "86fe93e2...",
"topic": "transfer",
"data": "{\"from\":\"alice\",\"to\":\"bob\",\"amount\":100}",
"block_height": 620,
"tx_hash": "c3d4e5f6..."
}
],
"count": 1
}
Live Events (WebSocket)
Connect to wss://<node>/api/ws and send { "subscribe": ["contract:<addr>"] }. After each block
is accepted you receive one frame per event:
{ "type": "contract_event", "contract_addr": "…", "topic": "move",
"data": "…", "block_height": 1234, "tx_hash": "…" }
List All Contracts
GET /api/contracts
Returns all deployed contracts with their metadata.
{
"success": true,
"contracts": [
{
"address": "86fe93e2...",
"deployer": "test-deployer",
"code_hash": "a1b2c3...",
"wasm_size": 711,
"created_at": 1774401569985
}
],
"count": 1
}
Architecture
An overview of RougeChain's system architecture.
High-Level Architecture
┌───────────────────────────────────────────────────────────┐
│ Clients │
│ │
│ ┌──────────┐ ┌──────────────────┐ ┌────────────────┐ │
│ │ Website │ │ Browser Extension│ │ @rougechain/sdk│ │
│ │ React │ │ Chrome / Firefox │ │ npm package │ │
│ └────┬─────┘ └────────┬─────────┘ └───────┬────────┘ │
│ │ │ │ │
│ │ Client-side ML-DSA-65 signing │ │
│ │ Client-side ML-KEM-768 encryption │ │
│ └────────────┬────┴─────────────────────┘ │
└────────────────────┼──────────────────────────────────────┘
│ HTTPS REST API
▼
┌───────────────────────────────────────────────────────────┐
│ Core Node (Rust) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ REST API │ │Blockchain│ │Validator │ │Messenger │ │
│ │ (Axum) │ │ Engine │ │ / PoS │ │ Server │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────┴──────────────┴──────────────┴──────────────┴────┐ │
│ │ Storage Layer │ │
│ │ sled: chain-db │ validators-db │ messenger-db │ … │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────┬──────────────────────────────────────┘
│ P2P (HTTP)
▼
┌───────────────────────────────────────────────────────────┐
│ Peer Nodes │
│ Block sync │ TX broadcast │ Peer discovery │
└───────────────────────────────────────────────────────────┘
Components
Core Node (Rust)
The backend is a single Rust binary (quantum-vault-daemon) that includes:
| Module | Responsibility |
|---|---|
| REST API | HTTP endpoints via Axum 0.6 (on hyper), default --api-port 5101. Peers sync over this same port — there is no separate P2P port |
| gRPC API | tonic client services (chain, wallet, validator, messenger) on --port, default 4101. Not used for peer traffic |
| Blockchain Engine | Block production, transaction processing, state management |
| Validator / PoS | Stake tracking, proposer selection, rewards |
| Messenger Server | Stores encrypted messages and wallet registrations |
| Mail Server | Stores encrypted mail, name registry |
| P2P Layer | HTTP polling of peers' /api/peers, /api/stats and /api/blocks; HTTP push of new blocks and txs |
| AMM/DEX | Liquidity pools, swap execution, price calculation |
| Bridge | R1 production bridge to Base mainnet (XRGE, qETH, qUSDC); separate Bitcoin bridge. V3 post-quantum XRGE bridge code is present but not activated |
Frontend (React + TypeScript)
The website at rougechain.io is a single-page application built with:
| Technology | Purpose |
|---|---|
| React 18 | UI framework |
| TypeScript | Type safety |
| Vite | Build tool |
| Tailwind CSS | Styling |
| shadcn/ui | Component library |
@noble/post-quantum | PQC cryptography (ML-DSA-65, ML-KEM-768) |
The frontend is a PWA (Progressive Web App) and can be installed on mobile and desktop.
Browser Extensions
Two Chrome extensions provide wallet functionality:
| Extension | Description |
|---|---|
| RougeChain Wallet | Primary browser extension |
| rougechain-wallet | Secondary extension |
Both inject a window.rougechain provider (similar to MetaMask's window.ethereum) for dApp integration.
SDK (@rougechain/sdk)
The npm package @rougechain/sdk provides a programmatic interface for interacting with RougeChain from Node.js or browser applications.
Cryptography Stack
┌─────────────────────────────────────────┐
│ Application Layer │
│ Transactions │ Messages │ Mail │ Auth │
└─────────────┬───────────────────────────┘
│
┌─────────────▼───────────────────────────┐
│ Cryptographic Primitives │
│ │
│ ML-DSA-65 (FIPS 204) │
│ └─ Signing: txs, blocks, stakes │
│ │
│ ML-KEM-768 (FIPS 203) │
│ └─ Key encapsulation: messenger, mail │
│ │
│ AES-256-GCM │
│ └─ Symmetric encryption of content │
│ │
│ HKDF (SHA-256) │
│ └─ Key derivation from shared secrets │
│ │
│ SHA-256 │
│ └─ Block hashes, tx hashes │
└─────────────────────────────────────────┘
Data Flow
Transaction Flow
1. User creates transaction in browser
2. Transaction payload is constructed
3. ML-DSA-65 signs the payload client-side
4. Signed transaction is sent to node via REST API
5. Node verifies signature
6. Transaction enters mempool
7. The designated proposer includes it in the next block
8. Block is signed and propagated to peers
Message Flow
1. Sender looks up recipient's ML-KEM-768 public key
2. ML-KEM-768 encapsulation generates shared secret
3. HKDF derives AES-256 key from shared secret
4. Message is encrypted with AES-GCM (for both sender and recipient)
5. The encrypted package is sent to the server
6. Recipient fetches encrypted blob
7. ML-KEM-768 decapsulation recovers shared secret
8. Message is decrypted client-side
Mail Flow
1. User registers a name (e.g., alice@rouge.quant) via Name Registry
2. Sender composes email, encrypts subject + body with PQC
3. Encrypted mail is stored on the server
4. Recipient fetches and decrypts client-side
5. Thread history is reconstructed via replyToId chain
Storage
Node Storage
All node state lives in sled embedded databases (*-db/ directories) under the data directory, plus a few JSON files.
| Store | Format | Content |
|---|---|---|
chain-db/ | sled | Block data (the chain tip is the last key) |
validators-db/ | sled | Validator stakes and state |
messenger-db/, mail-db/ | sled | Encrypted messages, mail, and wallet registrations |
other *-db/ | sled | Pools, NFTs, finality, nonces, social, and other state |
chain.jsonl | JSON lines | Legacy only: imported once into chain-db on first start, then renamed to chain.jsonl.bak |
Client Storage
| Store | Location | Content |
|---|---|---|
| Wallet keys | localStorage | ML-DSA-65 and ML-KEM-768 keys — only as an AES-256-GCM encrypted vault (password required); decrypted keys live in sessionStorage while unlocked |
| Block list | localStorage | Blocked wallet addresses |
| Mail settings | localStorage | Email signature preferences |
| Display name | localStorage | User's messenger display name |
Security Model
| Principle | Implementation |
|---|---|
| Keys never leave client | All signing/encryption happens in-browser |
| Server is untrusted | Messages and mail are stored only as ciphertext (the server still sees metadata such as sender, recipients, and timing) |
| Quantum-resistant L1 | NIST-approved PQC algorithms for L1 signatures and messaging. Base-side bridge custody is classical today — see Security Overview |
| BIP-39 mnemonics | Wallets derive from a 24-word BIP-39 mnemonic (256-bit entropy); the mnemonic is the primary backup for the signing key. The ML-KEM-768 messaging key is derived from the mnemonic (`ml_kem768.keygen(SHA-512(" |
| Signed v2 writes | /api/v2 writes require an ML-DSA-65 signature over a canonical payload; legacy v1 write endpoints return 410 Gone in production |
| Per-recipient encryption | Messages encrypted for the sender and every recipient |
v2 Signed Write Requirements
Every write through /api/v2 is authenticated by signature, not by a session:
- The request is a signed envelope
{ payload, signature, public_key }. - The signature is an ML-DSA-65 signature over the canonical
payloadbytes. payload.timestampmust fall within a ±5-minute window of server time.payload.frommust equal the signing public key (public_key).- A signature replay guard (
SEEN_SIGNATURES, keyed bysha256(signature), 5-minute window, persisted toreplay_guard.jsonacross restarts) rejects any signature already seen in-window. - Legacy v1 write endpoints return 410 Gone in production.
Post-Quantum Cryptography
RougeChain uses NIST-approved post-quantum cryptographic algorithms to protect against both classical and quantum computer attacks.
Why Post-Quantum?
Quantum computers threaten current cryptography:
| Algorithm | Quantum Threat |
|---|---|
| RSA | Broken by Shor's algorithm |
| ECDSA | Broken by Shor's algorithm |
| SHA-256 | Weakened (Grover's algorithm) |
| ML-DSA | Secure |
| ML-KEM | Secure |
Algorithms Used
ML-DSA-65 (Digital Signatures)
Formerly: CRYSTALS-Dilithium
Standard: FIPS 204
Security Level: NIST Level 3 (192-bit classical equivalent)
Used for:
- Transaction signatures
- Block proposal signatures
- Validator attestations
Key sizes:
| Component | Size |
|---|---|
| Public key | ~1,952 bytes |
| Private key | ~4,032 bytes |
| Signature | ~3,309 bytes |
ML-KEM-768 (Key Encapsulation)
Formerly: CRYSTALS-Kyber
Standard: FIPS 203
Security Level: NIST Level 3
Used for:
- Messenger encryption
- PQC Mail encryption
- Future: Encrypted transactions
Key sizes:
| Component | Size |
|---|---|
| Public key | ~1,184 bytes |
| Private key | ~2,400 bytes |
| Ciphertext | ~1,088 bytes |
SHA-256 (Hashing)
Used for:
- Block hashes
- Transaction hashes
- Merkle trees
While Grover's algorithm reduces SHA-256 security to ~128-bit equivalent against quantum computers, this is still considered secure.
Implementation
RougeChain uses the following libraries:
| Component | Library |
|---|---|
| Backend (Rust) | fips204 crate (ml_dsa_65) |
| Frontend (JS) | @noble/post-quantum |
All cryptographic operations happen locally - private keys never leave your device.
Key Generation
#![allow(unused)] fn main() { // Rust example — fips204 ml_dsa_65 (FIPS 204) use fips204::ml_dsa_65; use fips204::traits::{Signer, Verifier}; // Generate an ML-DSA-65 keypair let (pk, sk) = ml_dsa_65::try_keygen().unwrap(); // Sign a message (second arg is an optional context string) let signature = sk.try_sign(message, &[]).unwrap(); // Verify let valid = pk.verify(message, &signature, &[]); }
// TypeScript example
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
const { publicKey, secretKey } = ml_dsa65.keygen();
const signature = ml_dsa65.sign(message, secretKey);
const valid = ml_dsa65.verify(signature, message, publicKey);
Security Considerations
- Key storage - Private keys are persisted only encrypted with AES-256-GCM (PBKDF2, 600K iterations) under a required vault password (min 8 characters); they are never written to
localStoragein plaintext. Active session keys (and a new wallet before its password is set) are held insessionStorage. Wallets that older versions stored in plaintext must be secured with a password before further use, which deletes the plaintext copy. - Entropy - Keys use cryptographically secure random number generators
- Side channels - Library implementations are designed to be constant-time
- Hybrid approach - Consider adding classical signatures for defense-in-depth
STARKs (hash-based proofs)
RougeChain's crypto crate includes a STARK proof module. STARKs are quantum-resistant by design — they rely only on hash functions, not elliptic curves.
Status (2026-10-06). The circuits below are built on
winterfell0.13, which has no zero-knowledge mode: a proof shows the computation was done correctly but does not hide its inputs. They are integrity proofs, not privacy proofs, and no live feature uses them: the V1 shielded pool that did is suspended since block 245 (its proof established value balance only and did not bind a withdrawal to a specific note; the mainnet pool was empty), and the rollup below is a prototype that is not connected to consensus. A redesigned shielded pool (V2) is specified and in development on a Plonky3-based STARK with a hiding mode (proofs about 190 KB, verify about 6 ms, prove about 3 s on a phone), to launch under a 1,000,000 XRGE pool cap before the external audit. It is not audited; nothing may claim audited privacy until it is.
How It Works
The STARK module proves that a balance transfer is consistent (value is conserved, sender has sufficient funds). The public inputs are the final balances; because the prover has no hiding mode, the other values are not kept private by the proof.
| Property | Value |
|---|---|
| Library | winterfell 0.13 (Meta) — no zero-knowledge mode |
| Hash function | Blake3-256 |
| Quantum resistance | ✅ Hash-based (no EC) |
| Proof type | Balance transfer (value conservation) — integrity only |
Usage
#![allow(unused)] fn main() { use quantum_vault_crypto::stark::{ prove_balance_transfer, verify_balance_transfer, BalanceTransferInputs, }; use winterfell::math::{fields::f128::BaseElement, FieldElement}; // Prover (knows private balances) let proof = prove_balance_transfer(1000, 500, 250).unwrap(); // Verifier (only sees final balances) let public_inputs = BalanceTransferInputs { total_value: BaseElement::from(1500u64), final_sender_balance: BaseElement::from(750u64), final_receiver_balance: BaseElement::from(750u64), }; verify_balance_transfer(proof, public_inputs).unwrap(); }
STARK Rollup Batch Proofs (Phase 3) — research prototype
The rollup accumulator batches multiple transfers into a single STARK proof. It is not connected to consensus: a batch submitted through the API moves no funds and enters no block. It is kept as research code.
Rollup AIR (5-Column Trace)
sender_before | sender_after | receiver_after | amount | running_hash
Constraints:
- Value conservation:
sender_after = sender_before - amount - Running hash accumulation:
hash[i+1] = hash[i] + sender_before[i] * amount[i] - Cross-check redundancy
Boundary Assertions: running_hash transitions from pre_state_root to post_state_root
State Root
SHA-256 Merkle tree from sorted account balances with domain separation:
- Leaf:
SHA-256("ROUGECHAIN_STATE_V1" || address || balance_le_bytes) - Node:
SHA-256("ROUGECHAIN_NODE_V1" || left || right)
Rollup API
# Check rollup status
curl https://testnet.rougechain.io/api/v2/rollup/status
# Submit transfer to rollup batch
curl -X POST https://testnet.rougechain.io/api/v2/rollup/submit \
-H "Content-Type: application/json" \
-d '{"sender":"pubkey1","receiver":"pubkey2","amount":100,"fee":1}'
# Get completed batch result
curl https://testnet.rougechain.io/api/v2/rollup/batch/1
Bridge Deposit Verification (not STARK-based)
This section covers deposit verification on the RougeChain side. Releases on Base are still authorized with classical keys in production. The ML-DSA-65 on-chain authorization for XRGE (V3) is built but not activated — see Bridge Security Model.
Deposits from Base are cryptographically verified before minting:
- EVM Receipt Verification — tx status, recipient, sender, confirmations
- SHA-256 Commitment Nullifiers —
SHA-256("ROUGECHAIN_BRIDGE_V1" || tx_hash || address || amount)prevents double-claims - BridgeDeposit Event Verification — for XRGE vault deposits (log parsing)
Future Roadmap
- STARK proof system (Phase 1: balance transfer AIR) — integrity proofs, no zero-knowledge mode; unused by any live feature
- Shielded transactions: V1 suspended since block 245; V2 (Plonky3, hiding mode, 1,000,000 XRGE cap) in development, unaudited
- STARK rollup layer: prototype only, not connected to consensus
- Bridge deposit verification on the RougeChain side (EVM receipts + SHA-256 commitment nullifiers — not STARK proofs)
- V3 XRGE bridge with on-chain ML-DSA-65 authorization — built, audit pending, not activated (status)
- Fully trustless STARK bridge (Base light client)
- SLH-DSA (SPHINCS+) as alternative signature scheme
- Hybrid classical+PQC mode
- Hardware wallet support
- Threshold signatures for multi-sig
-
WASM-compiled STARK prover for browser (
core/wasm-prover/) — built; used by no live feature
PQC Messaging & Mail
RougeChain includes two built-in communication systems — both fully end-to-end encrypted with post-quantum cryptography.
Where the data lives. Messenger, mail and the name registry are node-hosted services, not chain state: every write is an ML-DSA-65-signed request, but the ciphertext and directory are stored in the node's own databases, are not in blocks and are not replicated by consensus. A wallet's messaging identity is its ML-DSA-65 signing key. Since node release 1.6.3 (2026-10-05) a directory entry can be replaced only by the wallet that owns it, an encryption key already registered to another wallet is refused, and the unauthenticated legacy read routes return
410 Gone(release notes).
Overview
| Feature | Messenger | |
|---|---|---|
| Purpose | Real-time chat | Async email |
| Encryption | ML-KEM-768 + AES-GCM | ML-KEM-768 + AES-GCM |
| Addresses | Wallet public keys | @rouge.quant / @qwalla.mail names |
| Media | Images, videos (auto-compressed) | Text + attachments (up to 3 MB) |
| Self-destruct | ✅ Configurable timer | ❌ |
| Folders | — | Inbox, Sent, Trash |
| Threading | Conversations | Reply chains |
| Server sees | Ciphertext + metadata (sender, conversation members, timing, size, message type) | Ciphertext + metadata (sender, recipients, timing, size) |
Why Post-Quantum?
Classical encrypted messengers (Signal, WhatsApp) use algorithms like X25519 and Ed25519. A sufficiently powerful quantum computer could break these with Shor's algorithm.
RougeChain's messaging uses ML-KEM-768 (CRYSTALS-Kyber, FIPS 203) for key encapsulation — resistant to both classical and quantum attacks. Messages encrypted today remain secure even if quantum computers arrive in the future.
How It Works
Messenger Encryption
Alice Bob
│ │
│ 1. Look up Bob's ML-KEM-768 public key │
│ 2. Encapsulate → shared secret │
│ 3. HKDF → AES-256 key │
│ 4. AES-GCM encrypt (for Bob) │
│ 5. AES-GCM encrypt (for self) │
│ │
│── Signed request to /api/v2/... ────────►│
│ │
│ 6. Decapsulate → same shared secret
│ 7. HKDF → same AES-256 key
│ 8. AES-GCM decrypt
Key principle: In a 1:1 conversation each message package holds two ciphertexts — one encapsulated to the recipient's ML-KEM key, one to the sender's own. Group conversations (2+ other members) use the same CEK pattern as mail: one ciphertext plus a per-member KEM-wrapped key (sender included). The server never has the private keys to decrypt any of them.
Your encryption key is derived from your seed phrase for wallets created or imported from the phrase on rougechain.io (since 2026-10-01) or in Qwalla: the ML-KEM-768 seed is SHA-512("<phrase>|rougee-gram|kem-v1"), so restoring from the 24-word phrase in either app restores the same key and your old messages and mail. Wallets created on the website before that change (and extension wallets) have a random key: restore those from their .pqcbackup file to keep old messages. A backup that contains encryption keys always keeps them as they are.
Mail Encryption (CEK Pattern)
Mail uses a Content Encryption Key (CEK) pattern for efficient multi-recipient support:
- Generate a random 256-bit AES key (the CEK)
- Encrypt all mail content (subject, body, attachment) once with the CEK via AES-256-GCM
- For each recipient (and the sender): KEM-wrap the CEK using their ML-KEM-768 public key
- Sign the concatenation of all encrypted parts with ML-DSA-65 (unified signature)
This design encrypts content only once regardless of recipient count.
Messenger
The real-time messenger is built into the RougeChain web app and browser extension.
Features
- E2E encrypted conversations between any two wallets
- Media sharing — images (auto-converted to WebP) and videos (VP9 WebM), compressed client-side
- Self-destruct messages — set a timer; message disappears after being read
- Display names — register a name that shows in conversations
- Contact blocking — client-side block list stored in localStorage
- Conversation management — create, view, and delete conversations
Getting Started
- Go to Messenger in the sidebar
- Your wallet is automatically registered
- Enter a recipient's public key to start a conversation
- Messages are encrypted before leaving your browser
API Endpoints
See Messenger API Reference for full endpoint documentation.
PQC Mail
Mail adds traditional email features on top of the same PQC encryption layer.
@rouge.quant Addresses
Instead of sharing long public keys, register a human-readable name:
| Action | Result |
|---|---|
| Register "alice" | You get alice@rouge.quant |
| Send to "bob" | System looks up Bob's public key automatically |
Names are unique and first-come-first-served.
Mail Features
- Subject lines — encrypted along with the body
- Folders — Inbox, Sent, Trash with move/delete operations
- Threading — reply chains built client-side via
replyToId - Mark as read — track read status per message
Email Domains
| Domain | Platform |
|---|---|
@rouge.quant | Website and browser extension |
@qwalla.mail | QWalla mobile app |
Both domains resolve against the same node-hosted name registry (not chain state) — the domain is a client-side display choice only.
Getting Started
- Go to Mail in the sidebar
- Click Register Name and choose your
@rouge.quantaddress - Send encrypted mail to any registered name
- All encryption/decryption happens in your browser
API Endpoints
See Mail API Reference for full endpoint documentation.
Authenticated Requests
All mail, messenger, and name registry write operations require ML-DSA-65 signed requests via /api/v2/ endpoints. Each request includes:
from— Sender's ML-DSA-65 public keytimestamp— Millisecond-precision timestamp (valid within a 5-minute window)nonce— 16 bytes of random hex (prevents replay attacks)signature— ML-DSA-65 signature over the canonical JSON payload
The server verifies the signature, validates the timestamp, confirms the sender owns the wallet, and rejects duplicate nonces. Legacy unsigned endpoints return HTTP 410 (Gone) in production — since release 1.6.3 this includes the legacy read routes (GET /api/mail/inbox, /sent, /trash, /api/mail/message/:id, /api/messenger/conversations, /api/messenger/messages); clients use the signed POST /api/v2/mail/folder, /api/v2/messenger/conversations/list and /api/v2/messenger/messages/list.
Directory ownership (release 1.6.3). The messenger directory maps a wallet to its display name, signing key and ML-KEM-768 encryption key. A re-registration replaces an entry only when it is signed by the key that owns it; a registration whose encryption key is already registered to another wallet is refused with This encryption key is already registered to another wallet. Mail names and mail folders follow the owning wallet only. Nodes before 1.6.3 do not enforce this.
Trust-on-First-Use (TOFU)
The messenger tracks public key fingerprints (SHA-256 hash) for contacts:
- On first interaction, the contact's key fingerprint is stored locally
- On subsequent interactions, the fingerprint is compared — a "Key Changed" warning appears if it differs
- The shortened fingerprint is shown in the chat header for manual verification
Security Properties
| Property | Details |
|---|---|
| Quantum-resistant | ML-KEM-768 key encapsulation (FIPS 203) |
| No forward secrecy | Each message uses a fresh encapsulation, but always to the recipient's (and sender's) long-term ML-KEM key — there is no ratchet. Anyone who later obtains that key can decrypt every stored message sent to or from it. Protect your encryption key and backups accordingly |
| Server can't read content | Server stores only ciphertext and cannot read messages, but it does see metadata (who talks to whom, when, and message size) |
| Client-side crypto | All encryption/decryption happens on the client |
| Per-member ciphertext | Sender and every recipient can each decrypt their own copy (two ciphertexts in 1:1, wrapped CEK per member in groups) |
| Signed requests | All API calls authenticated with ML-DSA-65 signatures |
| Anti-replay | Nonce + timestamp prevents request replay attacks |
| TOFU | Key fingerprint tracking with change detection |
| Unified signatures | Mail signed over all encrypted parts (subject + body + attachment) |
| CEK multi-recipient | Efficient per-recipient key wrapping without re-encryption |
| Atomic name registry | Compare-and-swap prevents race conditions on name claims |
| Directory ownership | Since node 1.6.3: only the owning signing key can replace a directory entry; a taken encryption key is refused |
| Node-hosted | Not on-chain: stored by the node you talk to, not replicated by consensus |
| Vaulted keys | Private keys persisted only as an AES-256-GCM encrypted blob (vault password required); active session keys in sessionStorage |
Notifications & Unread Badges
Browser Extension
The browser extension tracks unread counts for both Chat and Mail tabs:
| Feature | Details |
|---|---|
| Tab badges | Chat and Mail tabs display a numeric badge when unread items exist |
| Tooltips | Hovering a badged tab shows "3 unread messages" or "2 unread emails" |
| Extension icon badge | The combined unread total (chat + mail) is shown on the browser toolbar icon via chrome.action.setBadgeText |
| System notifications | Native OS notifications for new messages, new mail, received/sent tokens, contract events, staking, and balance changes |
| Badge clearing | Viewing a conversation or inbox marks items as read server-side and updates the badge immediately |
Notifications are powered by two channels:
- WebSocket — Real-time transaction and balance events via
wss://testnet.rougechain.io/api/ws - Polling — Unread messenger and mail counts checked every 15 seconds via signed
/api/v2/endpoints
QWalla Mobile App
QWalla provides the same unread badge experience:
| Feature | Details |
|---|---|
| Tab badges | Expo Router tabBarBadge on Chat and Mail tabs |
| Initial poll | On app launch, actual unread counts are fetched from the server so badges are accurate from the start |
| Real-time updates | WebSocket events increment the badge for new messages and mail |
| Push notifications | Expo push notifications for messages, mail, and transfers (requires registerPushToken) |
| Badge clearing | Navigating to the Chat or Mail tab clears the respective unread count |
Deriving Unread Counts
The server does not expose a dedicated "unread total" endpoint. Clients derive counts from existing data:
- Chat: Sum
unread_countfrom each conversation returned byPOST /api/v2/messenger/conversations/list - Mail: Count inbox items where the label's
is_readfield isfalsefromPOST /api/v2/mail/folder
SDK Usage
All write operations now require a wallet parameter for ML-DSA-65 request signing:
import { RougeChain, Wallet } from '@rougechain/sdk';
const rc = new RougeChain('https://testnet.rougechain.io/api');
const wallet = Wallet.generate();
// Step 1: Register wallet (signed request)
await rc.messenger.registerWallet(wallet, {
id: wallet.publicKey,
displayName: 'Alice',
signingPublicKey: wallet.publicKey,
encryptionPublicKey: encPubKey,
});
// Step 2: Register a mail name (signed request)
await rc.mail.registerName(wallet, 'alice', wallet.publicKey);
// Resolve a recipient's name → wallet + encryption key (public, no signing needed)
const bob = await rc.mail.resolveName('bob');
// bob.wallet.encryption_public_key → use for ML-KEM encryption
// Reverse lookup (public, no signing needed)
const name = await rc.mail.reverseLookup(wallet.publicKey); // "alice"
For the full messenger and mail APIs, see the SDK documentation.
Social Layer
RougeChain includes a built-in social layer for posts, likes, reposts, follows, comments, and play tracking. Social data is stored server-side in sled with ML-DSA-65 signed writes — tips settle on-chain via rc.transfer().
Features
| Feature | Description |
|---|---|
| Posts | Standalone text posts (max 4000 chars) with threaded replies via replyToId |
| Timeline | Global timeline (newest first) and personalized following feed |
| Likes | Toggle likes on posts or tracks — reuses the same endpoint |
| Reposts | Toggle reposts on any post |
| Comments | Track-level comments with pagination |
| Follows | Follow/unfollow any user; follower and following counts |
| Play counts | Record plays on tracks (debounced per session) |
| Tips | Send XRGE tips to creators via rc.transfer() — settles on-chain |
API Endpoints
Read (unsigned GET):
GET /api/social/timeline— Global timelineGET /api/social/post/:postId— Single post with statsGET /api/social/post/:postId/stats— Post engagement stats (likes, reposts, replies)GET /api/social/post/:postId/replies— Threaded repliesGET /api/social/user/:pubkey/posts— User's postsGET /api/social/track/:trackId/stats— Track stats (plays, likes, comments)GET /api/social/track/:trackId/comments— Track commentsGET /api/social/artist/:pubkey/stats— Artist stats (followers, following)GET /api/social/user/:pubkey/likes— User's liked IDsGET /api/social/user/:pubkey/following— User's followed artists
Write (v2 signed POST):
POST /api/v2/social/post— Create a postPOST /api/v2/social/post/delete— Delete your postPOST /api/v2/social/like— Toggle likePOST /api/v2/social/repost— Toggle repostPOST /api/v2/social/comment— Post a commentPOST /api/v2/social/comment/delete— Delete your commentPOST /api/v2/social/follow— Toggle followPOST /api/v2/social/play— Record a playPOST /api/v2/social/feed— Get following feed (authenticated)
SDK Usage
import { RougeChain, Wallet } from '@rougechain/sdk';
const rc = new RougeChain('https://testnet.rougechain.io/api');
const wallet = Wallet.generate();
// Create a post
const { post } = await rc.social.createPost(wallet, "Hello RougeChain!");
// Reply
await rc.social.createPost(wallet, "Great post!", post.id);
// Like, repost, follow
await rc.social.toggleLike(wallet, post.id);
await rc.social.toggleRepost(wallet, post.id);
await rc.social.toggleFollow(wallet, artistPubKey);
// Read timeline
const timeline = await rc.social.getGlobalTimeline();
const feed = await rc.social.getFollowingFeed(wallet);
CLI Usage
# Post
rougechain post "Hello from the CLI!"
# Reply
rougechain post "Nice post!" --reply-to <post-id>
# Timeline
rougechain timeline --limit 20
# Your feed (posts from people you follow)
rougechain feed
# Like / repost
rougechain like <post-or-track-id>
rougechain repost <post-id>
# Get a post
rougechain get-post <post-id>
WASM Smart Contracts
RougeChain includes a built-in WASM smart contract engine powered by wasmi — the same pure-Rust WASM interpreter used by Parity/Substrate.
v2 update: contracts can now custody and move XRGE.
host_transfer/host_get_balanceoperate in quanta (1 XRGE = 10^9 quanta), transfers are single-hop until block 160 (a contract moves only its own balance; from block 160 moves made inside cross-contract calls are applied too), and moves are enforced conserving and overdraft-free. See Contract XRGE Custody for the rules and examples.
Try it in the browser: the Contract IDE at rougechain.io/ide runs contracts on a local test chain (the node's own VM) and deploys them with your wallet.
Overview
Contracts are written in Rust (or any language that compiles to WASM), compiled to .wasm, and deployed on-chain. Execution is fuel-metered in a sandbox with host functions for chain interaction.
Architecture
Your Contract (Rust) → cargo build --target wasm32-unknown-unknown
→ .wasm bytecode (must export its memory as "memory", max 1 MiB)
→ Publish: a transaction YOU sign (POST /api/v2/contract/publish)
→ Execute: calls YOU sign (POST /api/v2/contract/execute), run in the wasmi sandbox
→ Host functions bridge to chain state
Since block 150 on mainnet, every contract transaction is player-signed: the
wallet that signs a call is the caller the contract sees (host_get_caller) and pays
the fee; the wallet that signs a deployment is the deployer. The old node-signed
endpoints are retired — POST /api/v2/contract/deploy returns 410 Gone, and
POST /api/v2/contract/call is only a dry run (it no longer submits anything).
Host Functions
Contracts import these from the env module:
| Function | Description |
|---|---|
host_log(ptr, len) | Debug logging |
host_get_caller(buf, len) | Caller's public key (the signer of the call) |
host_get_self_addr(buf, len) | The contract's own address |
host_get_chain_id(buf, len) → i32 | CONTRACT_CHAIN_ID (available from the upgrade; not scheduled yet): writes the chain id (rougechain-mainnet-1 / rougechain-devnet-1) and returns its length; -1 if len is too small. See Binding signed messages to the network |
host_get_args_len() → i32 | Length in bytes of the call's JSON arguments ({} when none) |
host_read_args(buf, len) → i32 | Copy the JSON arguments into memory; bytes written, or -1 if buf is too small |
host_get_block_height() | Current block height |
host_get_block_time() | Current block timestamp (seconds) |
host_get_balance(addr, len) | XRGE balance in quanta (1 XRGE = 10^9 quanta) |
host_transfer(to, len, amount) | Send XRGE (quanta) from the contract's own balance (single-hop until block 160) |
host_storage_read(key, klen, val, vlen) | Read persistent storage |
host_storage_write(key, klen, val, vlen) | Write persistent storage |
host_storage_delete(key, klen) | Delete from storage |
host_emit_event(topic, tlen, data, dlen) | Emit an indexed event (stored, and pushed over WebSocket) |
host_sha256(data, dlen, out) | Compute SHA-256 |
host_set_return(data, dlen) | Set the return value |
host_call_contract(addr, alen, method, mlen, args, argslen, gas) | Cross-contract call (returns call_id) |
host_get_call_result(call_id, buf, len) | Read a sub-call result |
host_pqc_verify(pk, pklen, msg, msglen, sig, siglen) | ML-DSA-65 signature verify |
host_pqc_pubkey_to_address(pk, pklen, out, outlen) | Derive a rouge1... address |
host_pqc_hash_pubkey(pk, pklen, out) | SHA-256 of a public key |
Game functions: tokens, NFTs, randomness (from block 160)
From mainnet block 160 (the GAME_READY 2 upgrade) contracts can also hold and move custom tokens and NFTs, run their own NFT collection, and roll dice. A contract only ever moves its own tokens and NFTs; players stock it by sending tokens/NFTs/XRGE to the contract address.
| Function | Result |
|---|---|
host_token_balance(sym, slen, addr, alen) → i64 | Token balance in the token's raw units (-1 invalid symbol) |
host_token_transfer(sym, slen, to, tlen, amount) → i32 | Send the contract's tokens: 0 ok, 1 insufficient, 2 invalid |
host_nft_owner(col, clen, id, out, cap) → i32 | Owner written to out; -1 not found, -2 buffer too small |
host_nft_transfer(col, clen, id, to, tlen) → i32 | Send an NFT the contract owns: 0 ok, 1 not the contract's, 2 not found/locked |
host_nft_create_collection(sym, slen, name, nlen, max_supply, out, cap) → i32 | Create the contract's own collection (max_supply 0 = unlimited). Writes the id col:<first 16 chars of the contract address>:<SYM>; -1 if it exists |
host_nft_mint(col, clen, to, tlen, name, nlen, meta, mlen) → i64 | Mint to a player (the contract must be the collection's creator). meta is optional JSON attributes. Returns the token id; -1 not creator, -2 sold out, -3 missing/frozen, -4 invalid |
host_random(out) → i32 | 32 pseudo-random bytes per call. The sender can grind it — see below |
host_block_hash(height, out) → i32 | From block 170: the 32-byte hash of a finished block up to 256 back; -1 otherwise. Use it to settle rolls |
host_get_attached_amount() → i64 | From block 190: the payment attached to this call, in quanta for XRGE or raw units for a token; 0 if none (always 0 in a cross-contract sub-call). See Payable calls |
host_get_attached_symbol(out, cap) → i32 | From block 190: writes the attached symbol (XRGE or a token symbol, upper-case). Returns bytes written, 0 if nothing is attached, -2 if cap is too small |
host_nft_royalty_bps(col, clen) → i32 | CONTRACT_NFT_ROYALTY (mainnet block 235, testnet 1360): the collection's royalty in basis points, 0–10000; -1 collection not found / invalid input. See Selling NFTs from a contract |
host_nft_royalty_recipient(col, clen, out, cap) → i32 | CONTRACT_NFT_ROYALTY: writes the royalty recipient in canonical ledger form (rouge1…, or a 40-hex contract address) and returns its length; -1 not found / invalid input, -2 cap too small |
Addresses a contract passes are normalised: paying the value host_get_caller returns (a public
key) credits the player's rouge1… wallet.
Randomness — use commit-then-settle for anything of value. host_random is fixed by the parent
block hash and the transaction hash. The parent hash is public before a player sends, and the player
controls their transaction's bytes, so a player can sign many variants of a one-step roll offline,
compute each result, and send only a winner. Never let host_random alone decide a prize.
From block 170, host_block_hash(height, out) → i32 returns the 32-byte hash of a finished block
up to 256 blocks back (-1 for the current block, a future block, or anything older). Games commit in
one call (record the block height H) and settle in a later call from host_block_hash(H + 1) mixed
with the player and H. That block did not exist when the player committed, and its hash covers the
producer's signature and the validators' finality signatures, so the player can't choose it. The
single block producer could still bias a result by withholding a block — acceptable at game stakes;
validator VRF randomness is planned.
Cross-contract calls and the state root (from block 160): token/NFT/XRGE moves made inside cross-contract calls are applied. Each sub-call sees the moves made before it; a failed sub-call's moves are dropped and the caller continues. The block state root commits NFT collections and ownership plus contract code and storage, so every node must agree on them.
A complete example — a loot box paying NFTs, tokens or XRGE — is in
contracts/loot_roll.
Selling NFTs from a contract: royalties (CONTRACT_NFT_ROYALTY)
Status: LIVE on mainnet since block 235 and on testnet since block 1360 (
GET /api/stats→upgrade_schedule.contract_nft_royalty). Below that height these two functions do not exist: a contract importing them can be published, but every call to it fails, exactly like a contract importing any unknown function.
host_nft_transfer moves an NFT without paying royalty, and the royalty a wallet nft_transfer
with a salePrice pays only applies to wallet sales. A contract that sells NFTs — an escrow
marketplace, an auction — reads the royalty and pays it itself:
| Function | Returns |
|---|---|
host_nft_royalty_bps(col, clen) → i32 | Basis points 0–10000 (250 = 2.5%); -1 if the collection doesn't exist. A stored value above 10000 is reported as 10000. |
host_nft_royalty_recipient(col, clen, out, cap) → i32 | Bytes of the recipient written to out; -1 not found, -2 cap too small. |
- Recipient form. The recipient is returned as the canonical ledger key the wallet royalty
path credits: a creator's public key comes back as its
rouge1…address, arouge1…address or a contract address (40 hex) unchanged.host_transfer(recipient, royalty)therefore credits exactly the balance a wallet sale would — including a royalty splitter contract used as the recipient. A 128-byte buffer is ample for these forms. - Contract-created collections (
host_nft_create_collection) have no royalty: bps0, recipient = the creating contract's address. A collection created earlier in the same call (or by the caller of a cross-contract call) is visible immediately. - Royalty is immutable: it is set once by
nft_create_collection(royaltyBps,royaltyRecipient, defaulting to the creator). From CONTRACT_NFT_ROYALTY activation, creating a collection withroyaltyBpsabove10000(more than 100%) — or one that is not an exact integer (negative, fractional, a string) — is rejected (400 royaltyBps must be an integer between 0 and 10000, and invalid in a block). Collections created before it keep their stored value; reads clamp it to 10000. - Integer math. Compute
royalty = price × bps / 10000in integer quanta (round down) and pay the sellerprice − royalty. Never use floats.
Example: escrow marketplace
contracts/nft_marketplace
is a small, commented reference (prebuilt nft_marketplace.wasm):
-
list {"collection","token_id","price"}— the seller must own the NFT when listing (the contract can't tell who deposited an NFT, so listing after the deposit would let anyone claim it). Returnslisted {"listing":N,"seller":…,"collection":…,"token_id":…,"price":…}.selleris the caller exactly as stored — for a wallet, its raw public key hex, not therouge1address — and is whatcancelcompares andbuypays. -
The seller escrows the NFT with a plain wallet
nft_transferto the contract address (no sale price). -
buy {"listing":N}withattach: {"symbol":"XRGE","amount":price}— the contract checks the payment is exactly the price (otherwise it traps, so the payment is refunded), moves the NFT to the buyer withhost_nft_transfer, then pays:#![allow(unused)] fn main() { let bps = unsafe { host_nft_royalty_bps(col.as_ptr(), col.len() as u32) }; if bps < 0 { revert(); } let royalty = (price as u128 * bps as u128 / 10_000) as u64; // floor, quanta if royalty > 0 { let n = unsafe { host_nft_royalty_recipient(col.as_ptr(), col.len() as u32, buf.as_mut_ptr(), buf.len() as u32) }; if n <= 0 || unsafe { host_transfer(buf.as_ptr(), n as u32, royalty as i64) } != 0 { revert(); } } if unsafe { host_transfer(seller.as_ptr(), seller.len() as u32, (price - royalty) as i64) } != 0 { revert(); } }and emits
sold {"listing":N,"price":…,"royalty":…,"seller_proceeds":…}. -
cancel {"listing":N}— seller only; returns the escrowed NFT (only for the NFT's current listing — a listing replaced by a newer listing of the same NFT never takes the escrow back).
A list/buy UI reads listings for free with the query endpoint (POST /api/contract/{addr}/query, no
signature, fee or caller). These methods only call host_set_return — no event, no storage write:
| Read method | Args | Returns |
|---|---|---|
listing | {"listing":N} | {"listing":N,"exists":true,"seller":…,"collection":…,"token_id":…,"price":…,"active":…,"escrowed":…} or {"listing":N,"exists":false} (unknown, sold or cancelled — no trap) |
listing_count | {} | {"next":N} — highest listing id issued (0 = none) |
active is false for a listing replaced by a newer one of the same NFT; escrowed means active and
the contract owns the NFT. Buyable = exists && escrowed.
const { returnData: { next } } = await rc.contracts.query(market, "listing_count", {});
for (let id = 1; id <= next; id++) {
const { returnData: l } = await rc.contracts.query(market, "listing", { listing: id });
if (l.exists && l.escrowed) render(l); // l.price is in quanta; l.seller is the seller's public key
}
For a 1.234567891 XRGE sale (1,234,567,891 quanta) of a 3% collection, the artist gets 37,037,036
quanta and the seller 1,197,530,855 — the node test
nft_marketplace_example_pays_royalty_and_seller_exactly_across_json_relay checks those exact numbers
on two nodes.
Binding signed messages to the network (CONTRACT_CHAIN_ID)
Status: available from the CONTRACT_CHAIN_ID upgrade, which is not scheduled on any network yet (
GET /api/stats→upgrade_schedule.contract_chain_idisnull). Until it activates the function does not exist: a contract importing it can be published, but every call to it fails, exactly like a contract importing any unknown function. The height will be announced in advance.
| Function | Returns |
|---|---|
host_get_chain_id(buf, len) → i32 | Writes the chain id (UTF-8) to buf and returns its length; -1 if len is too small. 64 bytes is ample. |
A contract that checks a signature itself with host_pqc_verify — an allowlist admission, a voucher,
an off-chain order — should make the signed message name the network, the contract and an expiry, so
a signature made for one network, one contract or one time window is not accepted anywhere else.
Build the message with a fixed domain tag and length-prefixed fields:
#![allow(unused)] fn main() { extern "C" { fn host_get_chain_id(buf: *mut u8, len: u32) -> i32; fn host_get_self_addr(buf: *mut u8, len: u32) -> i32; fn host_get_block_height() -> i64; fn host_pqc_verify(pk: *const u8, pklen: u32, msg: *const u8, msglen: u32, sig: *const u8, siglen: u32) -> i32; } /// "myapp/admit/v1" ‖ len‖chain id ‖ len‖contract address ‖ len‖player ‖ expiry height (u64 BE) fn admission_message(player: &[u8], expiry_height: u64) -> Vec<u8> { let (mut chain, mut me) = ([0u8; 64], [0u8; 64]); let c = unsafe { host_get_chain_id(chain.as_mut_ptr(), 64) }; let a = unsafe { host_get_self_addr(me.as_mut_ptr(), 64) }; if c <= 0 || a <= 0 { revert(); } let mut m = b"myapp/admit/v1".to_vec(); for part in [&chain[..c as usize], &me[..a as usize], player] { m.extend_from_slice(&(part.len() as u32).to_be_bytes()); m.extend_from_slice(part); } m.extend_from_slice(&expiry_height.to_be_bytes()); m } fn admit(player: &[u8], expiry_height: u64, issuer_pk: &[u8], sig: &[u8]) { if unsafe { host_get_block_height() } as u64 > expiry_height { revert(); } // expired let msg = admission_message(player, expiry_height); let ok = unsafe { host_pqc_verify(issuer_pk.as_ptr(), issuer_pk.len() as u32, msg.as_ptr(), msg.len() as u32, sig.as_ptr(), sig.len() as u32) }; if ok != 1 { revert(); } // …and record that this admission was used, if it must be single-use. } }
The issuer signs the same bytes off-chain, with the chain id of the network it means
(GET /api/health → chain_id).
Payable calls (from block 190)
From the payable-calls upgrade (block 190), a player can pay a contract in the same signed
call, for example an entry fee, a shop purchase or a stake. The signed contract_call payload
carries an optional attach:
"attach": { "symbol": "XRGE", "amount": 500000000 }
symbol:"XRGE"or a token symbol. The node upper-cases it. Allowed characters are 1–32 letters, digits,_and-.amountis a positive JSON integer: quanta for XRGE (1 XRGE = 1,000,000,000 quanta, so the example above is 0.5 XRGE) and raw units for a token (token decimals are not applied). Decimals, strings, zero and negative numbers are rejected. The SDK, extension and site keep it ≤ 2^53 − 1 so it round-trips exactly through JavaScript.attachis inside the signed payload, so nobody can change it after you sign.
Pay on success. While the call runs, the payment is already in the contract's balance
(host_get_balance of the contract's own address, or host_token_balance), so the contract can use
it. The payment becomes final only if the call succeeds. If the call fails or traps, all of its
effects are discarded and the payment stays with the player. The gas fee is still charged, as
for any failed call. A cross-contract sub-call sees no attachment.
Balance check. POST /api/v2/contract/execute refuses the call before it is broadcast if the
player can't cover gas fee + XRGE payment, or the token amount for a token payment. It also
refuses the call if the dry run with the payment would fail. In a block, a call whose attachment
the player can't cover is not executed. Before block 190, a call with attach is
invalid.
Refuse a payment by failing the call. A contract that doesn't want a payment should trap: wrong
symbol, too little, too much, or a method that isn't payable. Trapping returns the payment. A
method that ignores the attachment keeps whatever was sent, so non-payable methods should check
that host_get_attached_amount() is 0.
#![allow(unused)] fn main() { extern "C" { fn host_get_attached_amount() -> i64; fn host_get_attached_symbol(out_ptr: *mut u8, out_cap: u32) -> i32; } const ENTRY_FEE: i64 = 500_000_000; // 0.5 XRGE in quanta fn revert() -> ! { core::arch::wasm32::unreachable() } // fail the call: the payment goes back #[no_mangle] pub extern "C" fn roll() { let mut sym = [0u8; 8]; let n = unsafe { host_get_attached_symbol(sym.as_mut_ptr(), sym.len() as u32) }; if n != 4 || &sym[..4] != b"XRGE" || unsafe { host_get_attached_amount() } < ENTRY_FEE { revert(); // unpaid, underpaid or paid in the wrong token } // ... the fee is in the contract's balance; do the paid work ... } }
Players attach payments with the SDK
(rc.contracts.execute(wallet, addr, 'roll', {}, { attach: { symbol: 'XRGE', amount: xrgeToQuanta('0.5') } })),
the browser extension (v1.5.0+, which shows "Pays 0.5 XRGE to the contract (only if the call
succeeds)"), the explorer's contract page ("Attach payment"), or the MCP server
(attach: { symbol: "XRGE", amount_xrge: "0.5" }). loot_roll's roll requires a 0.5 XRGE entry
fee this way.
Gas Metering & Fees
Every WASM instruction costs 1 fuel unit. A call may use at most 10,000,000 fuel (≈10M instructions). If a contract runs out of fuel, execution halts and all state changes are reverted.
Fee Schedule
| Operation | Fee |
|---|---|
| Publish (deploy) | 10 XRGE flat |
| Call | gasLimit × 0.000001 XRGE — the signed gas limit, charged up front |
| Payable call | The call fee, plus the attached payment, which is paid only if the call succeeds (see Payable calls) |
| Query | Free (read-only, nothing is signed or committed) |
Because the fee is the signed gasLimit, not the gas used, pick a limit close to what
the call needs: query the call first and add headroom (the SDK does this for you:
ceil(gasUsed × 1.5) + 1000). The node dry-runs every call before accepting it and
refuses calls that would fail or that need more gas than the limit, so a failing call
costs nothing.
API
All write endpoints take the standard signed envelope used by every /api/v2/*
transaction: { payload, signature, public_key }, where signature is ML-DSA-65
over the JSON of payload with keys sorted. payload.from must be the signing key and
payload.timestamp (ms) must be within 5 minutes of the node's clock.
Publish a Contract
POST /api/v2/contract/publish
{
"payload": {
"type": "contract_deploy",
"from": "<your signing public key hex>",
"wasm": "<base64 WASM bytecode>",
"nonce": "<random string, at least 8 chars>",
"timestamp": 1790000000000
},
"signature": "<ML-DSA-65 signature hex>",
"public_key": "<your signing public key hex>"
}
→ { "success": true, "txId": "…", "address": "<40 hex chars>", "fee": 10 }
The contract is installed when the transaction is mined. Its address is fixed by what you signed, so you know it before the block and nobody else can take it:
address = hex( sha256( "rougechain/contract/v2" ‖ from ‖ 0x00 ‖ nonce ‖ 0x00 ‖ sha256(wasm) )[0..20] )
Signing the same code again needs a new nonce.
Call a Contract Method
POST /api/v2/contract/execute
{
"payload": {
"type": "contract_call",
"from": "<your signing public key hex>",
"contractAddr": "<contract address>",
"method": "my_method",
"args": { "key": "value" },
"gasLimit": 50000,
"attach": { "symbol": "XRGE", "amount": 500000000 }, // optional, from block 190
"timestamp": 1790000000000,
"nonce": "<random string>"
},
"signature": "…",
"public_key": "…"
}
→ { "success": true, "txId": "…", "fee": 0.05,
"preview": { "returnData": …, "gasUsed": 31234, "events": [ … ] } }
gasLimit must be an integer from 1 to 10,000,000. The preview is the node's dry run;
the authoritative execution happens when the transaction is mined. Its receipt
(GET /api/tx/{txId}/receipt) reports "status": "Success" when the call ran to completion,
or "status": {"Failed": "<error>"} when it reverted in the block (possible if the state
changed between the dry run and the block). A reverted call is still included and its fee is
charged; its state changes and events are discarded.
attach is optional (see Payable calls). amount is an
integer: quanta for XRGE, raw units for a token. The node refuses the call if you can't cover the
fee plus an XRGE payment, or the token amount. A reverted call keeps the payment with you.
Query (read-only, free)
POST /api/contract/{addr}/query
{ "method": "get_score", "args": { "player": "…" }, "caller": "<optional pubkey>" }
→ { "success": true, "returnData": …, "gasUsed": 812, "events": [], "error": null }
To preview a paid call, add the same attach plus the paying caller (required with attach):
{ "method": "roll", "args": {}, "caller": "<pubkey>", "attach": { "symbol": "XRGE", "amount": 500000000 } }.
The contract sees the payment exactly as it would in a block.
Read Contract Data
GET /api/contract/{addr} # metadata
GET /api/contract/{addr}/state # full state dump (all keys)
GET /api/contract/{addr}/state?key=x # one key (hex, or UTF-8 if not valid hex)
GET /api/contract/{addr}/events?limit=50 # event log
GET /api/contract/{addr}/events?before=12345 # older page: events below that block height
GET /api/contract/{addr}/events?tx=<txhash> # events emitted by one transaction
GET /api/contracts # list all contracts
Live Events (WebSocket)
Connect to wss://<node>/api/ws and send:
{ "subscribe": ["contract:<addr>"] }
After each block is accepted you receive one frame per event:
{ "type": "contract_event", "contract_addr": "…", "topic": "move",
"data": "…", "block_height": 1234, "tx_hash": "…" }
ERC-20 Token Standard
RougeChain includes a reference ERC-20 token contract at contracts/erc20_template/. This implements the standard fungible token interface:
| Method | Args | Description |
|---|---|---|
init | {name, symbol, decimals, total_supply, owner} | Initialize token, mint supply to owner |
name / symbol / decimals / total_supply | {} | Token metadata queries |
balance_of | {account} | Get account balance |
transfer | {to, amount} | Transfer tokens (caller → to) |
approve | {spender, amount} | Set allowance |
allowance | {owner, spender} | Get allowance |
transfer_from | {from, to, amount} | Transfer using allowance |
Storage Layout
meta:name/meta:symbol/meta:decimals/meta:total_supply— Token metadatabal:{account}— Account balancesallow:{owner}:{spender}— Allowances
Build & Deploy
cd contracts/erc20_template
cargo build --release --target wasm32-unknown-unknown
# Publish target/wasm32-unknown-unknown/release/*.wasm with rc.contracts.publish (see SDK below)
Explorer Integration
Deployed contracts are visible in the RougeChain explorer:
- Contracts Explorer (
/contracts) — List all deployed contracts with search/sort - Contract Detail (
/contract/{addr}) — Contract info, state viewer, free queries, wallet-signed calls (gas/fee preview, tx id, receipt Success/Failed) and a live events feed - Transaction Detail — Contract txs show: contract address, method, gas used, WASM size
SDK
@rougechain/sdk 1.9.0+ has a contracts namespace that signs, submits, queries and
subscribes. The constructor takes the API base URL (note the trailing /api):
import { RougeChain, Wallet } from '@rougechain/sdk';
import { readFileSync } from 'node:fs';
const rc = new RougeChain('https://api.rougechain.io/api');
const wallet = Wallet.fromMnemonic(process.env.MNEMONIC!);
// Publish (10 XRGE). The address is known before the block.
const wasm = readFileSync('target/wasm32-unknown-unknown/release/game.wasm');
const pub = await rc.contracts.publish(wallet, wasm);
console.log(pub.predictedAddress, pub.txId);
await rc.contracts.waitForReceipt(pub.txId!);
// Free read-only call
const q = await rc.contracts.query(pub.predictedAddress, 'get_score', { player: wallet.publicKey });
// Signed call. Without gasLimit the SDK queries first and signs ceil(gasUsed × 1.5) + 1000.
const r = await rc.contracts.execute(wallet, pub.predictedAddress, 'move', { x: 1, y: 2 });
if (!r.success) console.error(r.error); // e.g. "call would fail: not your turn"
const receipt = await rc.contracts.waitForReceipt(r.txId!);
if (receipt.status !== 'Success') console.error('reverted in block:', receipt.status.Failed);
// Live events (one shared socket, reconnects automatically)
const stop = rc.contracts.subscribe(pub.predictedAddress, (e) => console.log(e.topic, e.data));
// State, events, metadata
const all = await rc.contracts.state(pub.predictedAddress);
const one = await rc.contracts.state(pub.predictedAddress, new TextEncoder().encode('score'));
const older = await rc.contracts.events(pub.predictedAddress, { limit: 50, before: 12_000 });
For games there is a smaller handle:
const game = rc.contracts.game(address, wallet);
const off = game.on('move', (e) => render(JSON.parse(e.data))); // or '*' for every topic
await game.call('move', { x: 1, y: 2 });
const board = await game.query('board');
// Payable call (SDK 1.10.0+): 0.5 XRGE entry fee, paid only if the call succeeds
import { xrgeToQuanta } from '@rougechain/sdk';
await game.call('roll', {}, { attach: { symbol: 'XRGE', amount: xrgeToQuanta('0.5') } });
In the browser, dApps can have the RougeChain extension sign contract_call and
contract_deploy payloads via window.rougechain.signTransaction(payload). The
extension (v1.4.0+) shows the method, arguments, gas limit and maximum fee (or the
WASM size, predicted address and 10 XRGE fee) before signing. v1.5.0+ also shows any
attached payment and the max total cost.
MCP Server (AI Agents)
The RougeChain MCP server exposes smart contract operations as tools for AI agents:
| Tool | Description |
|---|---|
list_contracts | List all deployed contracts |
get_contract | Get contract metadata |
get_contract_state | Read state (single key or full dump) |
get_contract_events | Stored events (limit, before, tx) |
query_contract | Free read-only call (POST /api/contract/:addr/query); optional attach previews a paid call |
publish_contract | Write mode: sign and publish WASM with the server's wallet (10 XRGE) |
execute_contract | Write mode: sign a state-changing call with the server's wallet; optional attach (amount_xrge decimal for XRGE, integer amount for tokens) |
get_tx_receipt | Receipt of a transaction (Success or Failed) |
Security
WASM smart contracts maintain RougeChain's post-quantum security guarantees:
- All contract transactions are ML-DSA-65 signed by the player: the signer is the caller and pays the fee
- WASM execution is pure computation — no classical crypto involved
- Contract addresses are derived from the signed deployment via SHA-256, so they cannot be front-run
- Execution is sandboxed with no host OS access
Cross-Contract Calls
Contracts can call other contracts using host functions. Calls are queued during execution and processed recursively by the runtime after the primary call completes.
Host Functions
| Function | Returns | Description |
|---|---|---|
host_call_contract(addr, alen, method, mlen, args, argslen, gas) | call_id (i32) | Queue a call to another contract |
host_get_call_result(call_id, buf, len) | bytes written | Read the result of a sub-call |
Behavior
- Max depth: 8 nested calls (prevents infinite recursion)
- State merging: storage writes and events from sub-calls are merged atomically
- XRGE moves: a contract moves only its own balance. Until block 160 moves were single-hop —
host_transfercalls made by a sub-contract were not applied on-chain. From block 160 token/NFT/XRGE moves inside sub-calls are applied: each sub-call sees the moves made before it, and a failed sub-call's moves are dropped while the caller continues. See Contract XRGE Custody - Gas: sub-calls consume gas from the parent's remaining budget
- Failure: if a sub-call fails, it returns
-2fromhost_get_call_result; the parent can handle it gracefully
Example (Rust)
#![allow(unused)] fn main() { extern "C" { fn host_call_contract( addr: *const u8, alen: u32, method: *const u8, mlen: u32, args: *const u8, argslen: u32, gas: u64, ) -> i32; fn host_get_call_result(call_id: i32, buf: *mut u8, len: u32) -> i32; } #[no_mangle] pub extern "C" fn call_other() { let addr = b"contract_abc123..."; let method = b"get_value"; let args = b"{}"; let call_id = unsafe { host_call_contract( addr.as_ptr(), addr.len() as u32, method.as_ptr(), method.len() as u32, args.as_ptr(), args.len() as u32, 1_000_000, ) }; // Result is available after host processes the call queue let mut buf = [0u8; 1024]; let n = unsafe { host_get_call_result(call_id, buf.as_mut_ptr(), buf.len() as u32) }; // n > 0: success, n bytes of return data // n == -2: sub-call failed } }
PQC Precompiles
Native post-quantum cryptographic operations available as host functions — no need to implement ML-DSA in WASM.
| Function | Returns | Description |
|---|---|---|
host_pqc_verify(pk, pklen, msg, msglen, sig, siglen) | 1 valid, 0 invalid, -1 error | ML-DSA-65 signature verification |
host_pqc_pubkey_to_address(pk, pklen, out, outlen) | bytes written | Derive rouge1... bech32m address from raw pubkey |
host_pqc_hash_pubkey(pk, pklen, out) | 32 on success | SHA-256 hash of public key |
Key Sizes
- Public Key: 1,952 bytes (ML-DSA-65)
- Signature: 3,309 bytes (ML-DSA-65)
- Address: ~63 bytes (
rouge1...bech32m string) - Pubkey Hash: 32 bytes (SHA-256)
Use Cases
- On-chain auth: Verify a user's PQC signature inside a contract
- Address derivation: Convert a pubkey to a
rouge1...address for permission checks - Identity checks: Compare pubkey hashes for compact storage
EIP-1559 Dynamic Fees
RougeChain uses an EIP-1559-like fee model with base fee adjustment and fee burning:
How It Works
- Base fee adjusts ±12.5% per block based on block fullness (target: 10 txs/block)
- Floor: minimum base fee of 0.001 XRGE
- Fee burning: half of the base-fee portion (base fee × tx count ÷ 2, capped at the block's total fees) is burned (deflationary); everything else goes to the tip pool
- Priority fee (tip): 20% to block proposer, 70% to validators (stake-weighted), 10% to treasury
API
GET /api/fee-info
{
"baseFee": 0.0377,
"suggestedPriorityFee": 0.0038,
"suggestedTotalFee": 0.0415,
"totalBurned": 68.16,
"targetTxsPerBlock": 10,
"maxChangePercent": 12.5,
"blockHeight": 618
}
Contract IDE
rougechain.io/ide is a browser IDE for RougeChain WASM contracts. It lets you read and edit a contract's Rust source, run the contract on a local test chain inside the page, and deploy and call it on testnet or mainnet with your wallet.
The IDE doesn't compile Rust. You build the .wasm on your own machine with cargo and load the
file. Every template also ships a prebuilt .wasm, so you can run it right away.
Templates
| Template | What it shows |
|---|---|
| Counter | The smallest useful contract: storage, JSON arguments, return values, events, the caller |
| NFT marketplace | Escrow listings, buying with attached XRGE, royalties paid by the contract |
| XRGE staking | Staking with a reward pot that streams over time, a 3-day unstake delay |
| Loot roll | A game: pay to commit, settle two blocks later from a block hash |
| Royalty splitter | Receiving XRGE and paying it out to fixed recipients by share |
Sources are in contracts/ of
the node repository. To build one after editing it:
cd rougechain-node/contracts/counter
rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown
# → target/wasm32-unknown-unknown/release/rougechain_counter.wasm
Then choose Load your .wasm… in the IDE.
The local test chain
The local chain is the node's own contract engine (core/vm, the same wasmi VM and host functions
mainnet runs) compiled to WebAssembly. It runs in the page: nothing is sent anywhere and nothing
costs XRGE. It starts with three funded test accounts (Alice, Bob, Carol). It applies a call's
results the way a node does: an attached payment moves only if the call succeeds, and XRGE, token
and NFT moves are checked for overdrafts.
It also has tools for testing:
- send XRGE or an NFT to an account or a contract
- create a demo NFT collection (5 % royalty) and mint into it
- create a token
- top up the current account
- move the clock forward, for unstake delays and reward streams
Each call is its own block. Fees, signatures and other people's transactions don't exist there, and contract addresses differ from the ones a real network assigns.
Checks before you deploy
When you load a .wasm, the IDE lists its methods (its exported functions) and checks it against
what the node accepts:
- it must export its memory as
memory(the node refuses a contract that doesn't) - it must be at most 1 MiB
- every function it imports must be a host function the node provides — otherwise it can be deployed, but every call fails
Deploying with your wallet
Switch to Testnet or Mainnet (the network your wallet is on). Deploying costs 10 XRGE
and is signed by your wallet (POST /api/v2/contract/publish). The contract's address is known
before the transaction is mined. Calls are checked first with a free read
(POST /api/contract/:addr/query) and sent only if they would succeed, with a gas limit set from that
check. A deployed contract can't be changed or removed, so test it on the local chain and on
testnet first.
Contract XRGE Custody (RougeChain v2)
Live on mainnet since v2 (block 18). WASM contracts can now hold and move XRGE.
Before v2, a contract could receive XRGE but never spend it — funds sent to a contract address were locked forever. As of the v2 fork, contracts are first-class holders of XRGE: they can custody a balance and pay it out from within their own execution.
The unit: quanta
XRGE is counted internally in quanta, its smallest indivisible unit:
1 XRGE = 1,000,000,000 quanta (10^9)
This is the wei ↔ ETH model. Contracts do integer math in quanta; wallets and
UIs render the decimal XRGE. There are no floating-point amounts anywhere in a
contract — everything is exact integers, so a payout can never drift or round
ambiguously. A 1-XRGE three-way split is 333_333_333 quanta to each recipient
with a 1-quanta remainder — exact, conserved to the atom.
Host functions
host_get_balance(addr_ptr, addr_len) -> i64 // returns the address's balance in QUANTA
host_transfer(to_ptr, to_len, amount_i64) -> i32 // moves `amount` QUANTA from THIS contract to `to`
// returns 0 on success, 1 on refusal
amountis in quanta. A negative amount is rejected.- A contract can only transfer its own balance —
host_transferalways debits the executing contract's address. - If the contract's balance can't cover
amount, the transfer returns1and nothing moves (no partial transfer, no delta recorded).
The two rules the chain enforces
Every set of moves a contract makes in a call is checked before any balance changes, all-or-nothing:
- Conservation — the moves must net to exactly zero. A contract can never mint or burn XRGE, only move it.
- No overdraft — no account may go below zero. A contract can only move quanta it actually holds.
Break either and the whole call's balance effects are rejected.
v1 limitation: single-hop (until block 160)
Until block 160, XRGE custody is single-hop: a contract moves its own XRGE. Cross-contract XRGE moves (contract A calls contract B, and B pays out) are not applied before block 160 — cross-calls still work for compute/storage, but XRGE does not move across a hop. Design payout logic so each contract moves only its own balance.
From block 160 (the GAME_READY 2 upgrade), token/NFT/XRGE moves made inside cross-contract calls are applied: each sub-call sees the moves made before it, and a failed sub-call's moves are dropped while the caller continues. A contract still only moves its own balance.
Determinism & deployment
- Deploy transactions now carry the contract bytecode on-chain; every node installs it on import and re-executes identically. No separate code distribution step.
- Call arguments are recorded on the transaction, so on-chain re-execution matches what the caller signed.
- A call a node cannot execute deterministically (missing code, VM error) is rejected — the network fails closed rather than silently diverging. A contract that simply reverts or runs out of gas is normal: no moves, the block proceeds.
Example: a royalty splitter
(module
(import "env" "host_transfer" (func $tr (param i32 i32 i64) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "alice")
(data (i32.const 16) "bob")
(data (i32.const 32) "carol")
;; send 1 XRGE (1e9 quanta) to each of three recipients
(func (export "split") (result i32)
(drop (call $tr (i32.const 0) (i32.const 5) (i64.const 1000000000)))
(drop (call $tr (i32.const 16) (i32.const 3) (i64.const 1000000000)))
(call $tr (i32.const 32) (i32.const 5) (i64.const 1000000000))))
Fund the contract, call split, and 1 XRGE lands in each of three wallets —
conserved exactly, agreed by every node.
What this changes for NFT royalties (qRougee)
Previously the NFT royaltyRecipient had to be a wallet — routing royalties
to a contract address would lock them. With v2 custody that constraint relaxes:
a splitter contract can receive royalties and fan them to collaborators on-chain
(single-hop until block 160). Off-chain payout wallets are no longer the only safe option.
Multi-Signature Wallets
RougeChain supports M-of-N multi-signature wallets with PQC-safe ML-DSA-65 aggregate verification.
Overview
A multi-sig wallet requires M out of N co-signers to approve a transaction before it executes. This enables:
- Shared treasury management — DAOs, teams, and organizations
- Secure cold storage — 2-of-3 schemes (e.g. hardware + phone + backup)
- Escrow — Buyer + seller + arbitrator
Transaction Types
Create a Wallet
{
"tx_type": "multisig_create",
"payload": {
"multisig_signers": ["pubkey_hex_1", "pubkey_hex_2", "pubkey_hex_3"],
"multisig_threshold": 2,
"multisig_label": "Team Treasury"
}
}
- Minimum 2 signers
- Threshold must be ≥ 1 and ≤ number of signers
- Wallet ID is auto-generated if not provided
Submit a Proposal
{
"tx_type": "multisig_submit",
"payload": {
"multisig_wallet_id": "ms-abc123...",
"multisig_proposal_tx_type": "transfer",
"multisig_proposal_payload": {
"to_pub_key_hex": "recipient_pubkey_hex",
"amount": 1000
},
"multisig_proposal_fee": 0.1
}
}
- Only signers of the wallet can submit proposals
- The submitter's signature counts as the first approval
Approve a Proposal
{
"tx_type": "multisig_approve",
"payload": {
"multisig_proposal_id": "mp-def456..."
}
}
- Only signers who haven't already approved can approve
- When threshold is reached, the proposal auto-executes
- For
transferproposals: funds move from the wallet creator's balance
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/multisig/wallets | List all multi-sig wallets |
| GET | /api/multisig/wallet/:id | Get wallet details |
| GET | /api/multisig/wallet/:id/proposals | List proposals for a wallet |
| GET | /api/multisig/wallets/:pubkey | Find wallets where pubkey is a signer |
Response Format
{
"success": true,
"wallet": {
"wallet_id": "ms-abc123...",
"creator": "pubkey_hex",
"signers": ["pubkey1", "pubkey2", "pubkey3"],
"threshold": 2,
"created_at_height": 500,
"label": "Team Treasury"
}
}
SDK
import { RougeChain } from '@rougechain/sdk';
// The constructor takes the API base URL as a string (note the trailing /api)
const rc = new RougeChain('https://api.rougechain.io/api');
// Query multi-sig wallets and proposals (public reads)
const wallets = await rc.get('/multisig/wallets');
const myWallets = await rc.get(`/multisig/wallets/${myPubKey}`);
const wallet_ = await rc.get(`/multisig/wallet/${walletId}`);
const proposals = await rc.get(`/multisig/wallet/${walletId}/proposals`);
Writes are not yet exposed. The
multisig_create/multisig_submit/multisig_approvetransaction types above document the on-chain payload structure, but the node currently serves only the multi-sig read routes — there is no public submit endpoint and no SDK method (rc.sendTransactiondoes not exist). Multi-sig proposal submission is tracked for a future release. See the SDK reference.
MCP Server (AI Agent Integration)
RougeChain is the first blockchain with native MCP (Model Context Protocol) integration. AI agents like Claude, ChatGPT, and custom agents can interact with the blockchain using standardized MCP tools.
What is MCP?
The Model Context Protocol is an open standard for AI agents to interact with external services. RougeChain's MCP server exposes 63 blockchain tools — read the chain and, with a wallet configured, sign and submit real transactions. Every write is signed locally with ML-DSA-65 via @rougechain/sdk; private keys never leave the server process.
Two modes
| Mode | How | What the agent can do |
|---|---|---|
| Read-only (default) | no wallet env | All query tools. Safe to expose anywhere. |
| Read + write | set a wallet env (below) | Everything above plus signed transactions from that wallet. |
Write tools are only registered when a wallet is configured — with no wallet, the transaction tools don't even appear.
Setup
No install needed — the server is published on npm as @rougechain/mcp-server and runs via npx.
Claude Desktop — read-only
Add to ~/.config/claude/claude_desktop_config.json:
{
"mcpServers": {
"rougechain": {
"command": "npx",
"args": ["-y", "@rougechain/mcp-server"],
"env": {
"ROUGECHAIN_URL": "https://api.rougechain.io"
}
}
}
}
Claude Desktop — read + write
Add a wallet to unlock the signing tools:
{
"mcpServers": {
"rougechain": {
"command": "npx",
"args": ["-y", "@rougechain/mcp-server"],
"env": {
"ROUGECHAIN_URL": "https://api.rougechain.io",
"ROUGECHAIN_MNEMONIC": "your twenty four word seed phrase here ..."
}
}
}
}
⚠️ The mnemonic controls real funds. Use a dedicated low-balance agent wallet — generate one with the
generate_wallettool — and keep it out of shared configs. Alternatively supply raw hex keys viaROUGECHAIN_PRIVATE_KEY+ROUGECHAIN_PUBLIC_KEY.
Host note:
ROUGECHAIN_URLmust be the API host (api.rougechain.io), notrougechain.io, which serves the web app for every/api/*path.
Available Tools (63)
Wallet (always available)
| Tool | Description |
|---|---|
generate_wallet | Create a fresh ML-DSA-65 wallet (mnemonic + address); not persisted |
wallet_info | Show the configured signer, address, live balance, and whether writes are enabled |
Read tools (always available)
| Category | Tools |
|---|---|
| Chain | get_chain_stats, get_block, get_latest_blocks |
| Wallet | get_balance, get_transaction |
| Tokens | list_tokens, get_token, get_token_holders |
| DeFi | list_pools, get_swap_quote |
| NFTs | list_nft_collections, get_nft_collection |
| Validators | list_validators |
| Contracts | list_contracts, get_contract, get_contract_state, get_contract_events, query_contract (free read-only dry run), get_tx_receipt |
| Social | get_global_timeline, get_post, get_user_posts, get_post_replies, get_track_stats, get_artist_stats |
| Mail & Messaging | resolve_name, reverse_lookup_name, list_messenger_wallets |
| Governance & Fees | list_proposals (on-chain governance is suspended since block 245; returns an empty list on mainnet), get_fee_info |
✍️ Write tools (write mode only — signed with ML-DSA-65)
| Category | Tools |
|---|---|
| Value | send_transaction, burn_tokens, stake, unstake, request_faucet (testnet) |
| Tokens | create_token, mint_tokens, update_token_metadata, claim_token_metadata |
| DeFi | swap, create_pool, add_liquidity, remove_liquidity |
| NFTs | nft_create_collection, nft_mint, nft_batch_mint, nft_transfer, nft_burn, nft_lock, nft_freeze_collection |
| Names | register_name, release_name |
| Social | create_post, delete_post, repost, follow, like_track, comment_on_track |
| Bridge | bridge_withdraw |
| Contracts | publish_contract, execute_contract |
Architecture
AI Agent (Claude / GPT / Custom)
↕ stdio (MCP protocol)
RougeChain MCP Server
↕ HTTPS
RougeChain Node API
↕ PQC-signed transactions
RougeChain L1 (ML-DSA + ML-KEM)
All operations maintain post-quantum security guarantees.
Resources
The MCP server also exposes a rougechain://info resource with static context about RougeChain's technology stack, features, and API endpoints.
Browser Extensions
RougeChain provides browser extensions that serve as quantum-resistant wallets, similar to how MetaMask works for Ethereum — but using post-quantum cryptography.
Available Extensions
| Extension | Description | Store |
|---|---|---|
| RougeChain Wallet | Primary browser extension | Chrome Web Store |
Features
- Wallet — View balances, send/receive XRGE, claim faucet, custom token support
- Encrypted Messenger — E2E encrypted chat using ML-KEM-768 + ML-DSA-65
- PQC Mail — Encrypted email with
@rouge.quantaddresses - Vault Lock — AES-256-GCM encryption with PBKDF2 key derivation and auto-lock timer
- Cross-browser — Chrome, Edge, Brave, Opera, Arc, Firefox (Manifest V3)
Installation
From Chrome Web Store
- Visit the extension page on the Chrome Web Store
- Click Add to Chrome
- The extension icon appears in your toolbar
From Source
cd apps/extension
npm install
npm run build
- Open
chrome://extensions(oredge://extensions,brave://extensions) - Enable Developer mode
- Click Load unpacked
- Select the
apps/extension/distfolder
Firefox
cd apps/extension
npm install
npm run build
- Open
about:debugging#/runtime/this-firefox - Click Load Temporary Add-on
- Select
apps/extension/dist/manifest.json
DApp Integration
The extension (and the Qwalla mobile app's dApp browser) inject a window.rougechain
provider object, similar to MetaMask's window.ethereum — but the API is
RougeChain-specific, not the EVM/ethers API. In particular there is no
getAddress() and no getNetwork(); you obtain the user's public key by calling
connect().
Which sites get the provider: from extension 1.6.0, every https:// site and
http://localhost / http://127.0.0.1 — you don't need to ask us to add your domain. The first
connect() from a site opens a "Connect to this site?" prompt; nothing is shared until the user
approves, and every signature or send is approved separately. Users can review and disconnect sites
under Settings → Connected Sites. Plain http:// public sites are not supported. (Extension
1.5.x and earlier injected only on rougechain.io, rougee.app and localhost.)
⚠️ Common mistake: calling
window.rougechain.getAddress()throwse.getAddress is not a function. That method does not exist — RougeChain is not EVM-compatible. Useconnect()(below) and read.publicKey.
Detecting the extension and connecting
// 1. Detect
if (!window.rougechain?.isRougeChain) {
throw new Error("RougeChain wallet not found — install the extension or open in Qwalla.");
}
// 2. Connect (prompts the user, returns their public key)
const { publicKey, displayName, encryptionPublicKey } = await window.rougechain.connect();
console.log("Connected:", publicKey);
The provider may not be injected the instant your script runs. Either check for it
after the rougechain#initialized event, or poll briefly:
function getProvider(timeoutMs = 3000) {
return new Promise((resolve, reject) => {
if (window.rougechain?.isRougeChain) return resolve(window.rougechain);
const onReady = () => resolve(window.rougechain);
window.addEventListener("rougechain#initialized", onReady, { once: true });
setTimeout(() => reject(new Error("RougeChain wallet not detected")), timeoutMs);
});
}
Deriving a rouge1… address
connect() returns the public key (hex). If you need the short Bech32m
rouge1… address, derive it with the SDK:
import { pubkeyToAddress } from "@rougechain/sdk";
const { publicKey } = await window.rougechain.connect();
const address = await pubkeyToAddress(publicKey); // rouge1...
Provider API
| Method | Signature | Description |
|---|---|---|
isRougeChain | boolean | Property — true on the authentic provider. Use it to detect the wallet. |
connect() | → { publicKey, displayName?, encryptionPublicKey? } | Prompt the user to connect. Returns their public key. This replaces getAddress(). |
getBalance() | → { balance, tokens } | Get the connected wallet's XRGE balance and token balances. |
signTransaction(payload) | → { signature, signedPayload, publicKey, payload } | Sign a transaction payload with ML-DSA-65 (key never leaves the wallet). signedPayload is the exact JSON signed. |
signMessage({ message }) | → { signature, publicKey, address } | Extension 1.8.0+; Qwalla after its next update. Sign a text message (at most 4,096 bytes) to prove control of the wallet: logins and token gating. Never a transaction. The user approves every request. See Wallet Authentication. |
sendTransaction(payload) | → { txId, fee?, address?, preview? } | Sign and broadcast. A transfer by default; contract_call / contract_deploy payloads go to the contract endpoints. |
on(event, cb) | void | Subscribe to events (e.g. account/connection changes). |
removeListener(event, cb) | void | Remove an event listener. |
Signing a message (v1.8.0+)
signMessage proves that the visitor controls the connected wallet without sending anything to
the chain. Feature-detect it, because older extension versions and Qwalla builds do not have it:
if (typeof window.rougechain.signMessage !== "function") {
throw new Error("Update your RougeChain wallet to sign in.");
}
const { signature, publicKey, address } = await window.rougechain.signMessage({
message: "I agree to the rules of this raffle.",
});
// Server: verifyMessage(publicKey, message, signature) from @rougechain/sdk 1.13.0+
The site must be connected, and the wallet asks every time, showing your site and the whole
message. The signature is over "\x19RougeChain Signed Message:\n" + byte length + "\n" + message,
which a node never accepts as a transaction; a message that looks like a transaction payload
(JSON with a type / tx_type field) is refused, so keep using signTransaction for those.
For logins use the sign-in message format, which lets the wallet show the domain, nonce and
expiry and warn when the domain is not your site:
Wallet Authentication has the format and a complete token-gating
server.
Smart contract transactions (v1.4.0+)
The extension signs contract_call and contract_deploy payloads and shows a
contract-specific approval: the contract address, method, arguments, gas limit and
maximum fee for a call; the WASM size, predicted contract address and 10 XRGE fee for a
deployment. Missing from, timestamp and nonce are filled in, and the filled
payload is returned so you can submit it:
const { payload, signature, publicKey } = await window.rougechain.signTransaction({
type: "contract_call",
contractAddr: "<40-hex address>",
method: "move",
args: { x: 1, y: 2 },
gasLimit: 50000, // fee = gasLimit × 0.000001 XRGE
});
await fetch("https://api.rougechain.io/api/v2/contract/execute", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload, signature, public_key: publicKey }),
});
// or sign and submit in one step:
const { txId, preview } = await window.rougechain.sendTransaction({
type: "contract_call", contractAddr, method: "move", args: { x: 1 }, gasLimit: 50000,
});
Size the gas limit from a free query (POST /api/contract/<addr>/query) first. See
WASM Smart Contracts.
Payable calls (v1.5.0+; the node accepts them from block 190). A
contract_call may carry attach: { symbol, amount }. amount is a positive integer JSON
number: quanta for XRGE (1 XRGE = 1,000,000,000 quanta), raw units for tokens. The approval
view shows "Pays amount SYMBOL to the contract (only if the call succeeds)", with XRGE
formatted from quanta. For XRGE it also shows the max total cost, which is the gas fee plus the
payment. A non-integer, zero, negative or unsafe (> 2^53 − 1) amount is refused before the
approval opens.
await window.rougechain.sendTransaction({
type: "contract_call", contractAddr, method: "roll", args: {}, gasLimit: 50000,
attach: { symbol: "XRGE", amount: 500000000 }, // 0.5 XRGE, in quanta
});
Authenticity: the genuine provider also sets a non-enumerable
Symbol.for("rougechain:authentic") to true, so a dApp can guard against a page
that pre-defines a fake window.rougechain:
const authentic = window.rougechain?.[Symbol.for("rougechain:authentic")] === true;
Security
| Feature | Implementation |
|---|---|
| Vault encryption | AES-256-GCM with a PBKDF2-derived key (600k iterations); a password (min 8 chars) is mandatory at wallet creation |
| Auto-lock | Configurable timer via background service worker |
| Key storage | Encrypted blob in chrome.storage.local; the decrypted key is held only in chrome.storage.session (memory) and never written to disk. Legacy plaintext wallets are force-migrated to encrypted storage. |
| Signing | ML-DSA-65 (FIPS 204) — quantum-resistant |
| Encryption | ML-KEM-768 (FIPS 203) — quantum-resistant |
Permissions
The extensions request minimal permissions:
| Permission | Purpose |
|---|---|
storage | Store encrypted wallet data |
alarms | Auto-lock timer |
notifications | Transaction alerts |
| Host permissions | Connect to RougeChain nodes (rougechain.io, testnet.rougechain.io, localhost) |
Why Not MetaMask?
MetaMask and all EVM wallets use secp256k1 / ECDSA cryptography. RougeChain uses ML-DSA-65 / ML-KEM-768 (post-quantum). The key formats, signature schemes, and transaction structures are fundamentally incompatible. RougeChain's extensions are purpose-built for quantum-safe operations.
See PQC Cryptography for details on the algorithms used.
Wallet Authentication (Sign a Message)
A website can ask a RougeChain wallet to sign a message to prove the visitor controls that wallet. Nothing is sent to the chain and no funds can move. Use it for logins and for token gating: "only holders of token X or an NFT from collection Y may enter".
const { signature, publicKey, address } = await window.rougechain.signMessage({ message });
The site verifies the result on its server with verifyMessage or verifySignIn from
@rougechain/sdk (1.13.0 or later), then looks up what the wallet holds on a node.
Which wallets support it
| Wallet | signMessage |
|---|---|
| RougeChain Wallet browser extension | from version 1.8.0 |
| Qwalla in-app browser | after its next update (not in the builds released before this page) |
| rougechain.io built-in wallet | has no window.rougechain for other sites; it signs for first-party pages only |
Always feature-detect, and tell the user to update their wallet when the method is missing:
const provider = window.rougechain;
if (!provider?.isRougeChain) throw new Error("Install the RougeChain wallet or open this page in Qwalla.");
if (typeof provider.signMessage !== "function") throw new Error("Update your RougeChain wallet to sign in.");
Do not fall back to signTransaction for logins. A transaction signature is a different
thing (see Domain separation) and verifyMessage will not accept it.
What the wallet does
window.rougechain.signMessage({ message })
messageis a string of at most 4,096 bytes (UTF-8). It must be well-formed Unicode and not empty.- The site must already be connected (
connect()), otherwise the call rejects withSite not connected. Call connect() first. - The wallet asks the user every time. There is no "remember this site" for signatures.
- The approval shows the requesting site, and the whole message: scrollable, never cut, with characters that are normally invisible (carriage return, zero-width and right-to-left marks, and so on) drawn as symbols so text cannot be hidden or reordered.
- When the message is a sign-in message, the wallet also shows its domain, address, nonce and expiry, and a red warning if the domain in the message is not the site that is asking.
- A message that parses as a transaction payload (a JSON object with a
typeortx_typefield, or withfromandtimestamp) is refused withsignMessage refuses a message that looks like a transaction payload. Use signTransaction to sign a transaction. - It resolves to
{ signature, publicKey, address }(hex signature, hex public key,rouge1…address), or rejects if the user declines.
The signed bytes
The wallet does not sign the bare message. It signs:
UTF8("\x19RougeChain Signed Message:\n") ‖ decimal(len(message_bytes)) ‖ "\n" ‖ message_bytes
\x19is the single byte0x19.message_bytesis the UTF-8 encoding of the string.decimal(len(...))is the byte length in ASCII decimal digits, without leading zeros.- The signature scheme is ML-DSA-65 (FIPS 204) with an empty context, the same parameters the wallet uses for transactions.
For the message hello the signed bytes are, in hex:
19 526f756765436861696e205369676e6564204d6573736167653a 0a 35 0a 68656c6c6f
"RougeChain Signed Message:" \n "5" \n "hello"
ML-DSA signing is randomized, so signing the same message twice gives two different signatures. Both verify.
Sizes. An ML-DSA-65 signature is 3,309 bytes (6,618 hex characters, about 3.3 KB) and a
public key is 1,952 bytes (3,904 hex characters, about 2 KB). Send them in a POST body, never
in a URL or a cookie, and keep the short rouge1… address (about 64 characters) in your session, not
the key.
Domain separation
A signed message can never be used as a transaction, and a transaction signature can never pass as a signed message:
- Every byte string a RougeChain node verifies a transaction signature over is a JSON
document that starts with
{: the legacy encodings (serde_jsonof the transaction struct), the signed payload of the/api/v2/*endpoints (must parse as a JSON object equal to the payload), and the CLI envelope ({"tx_type",…,"payload":{…}}). The node never verifies a transaction signature over raw caller-chosen bytes. - Signed-message bytes start with
0x19, a control character that cannot appear at the start of (or unescaped anywhere in) a JSON document.
So the two sets of byte strings do not overlap. The same construction, with a different prefix,
is what Ethereum's personal_sign uses.
Sign-in message
For logins, sign a message in this exact format (modelled on Sign-In with Ethereum, EIP-4361). Wallets recognise it and show its fields.
tickets.example.com wants you to sign in with your RougeChain account:
rouge1ayekjlm684n3hrpfg3fyv53s6n2r85eh4lf9hxwm4zzpw42p4p2srzg20p
Sign in to see your tickets.
URI: https://tickets.example.com/login
Version: 1
Chain ID: rougechain-mainnet-1
Nonce: 4f9c2d1e8a7b6c5d0011
Issued At: 2026-10-06T12:00:00.000Z
Expiration Time: 2026-10-06T12:10:00.000Z
Resources:
- https://tickets.example.com/events/42
| Line | Rule |
|---|---|
| 1 | <domain> wants you to sign in with your RougeChain account: — fixed wording. domain is your site's host, with the port if it is not the default (localhost:5173). No scheme, no path. |
| 2 | The signer's rouge1… address, lower case. |
| 3 | Empty. |
| 4 | Optional statement: one line of text. |
| 5 | Empty. (Without a statement the address is followed by two empty lines.) |
URI: | The page or API the sign-in is for. No whitespace. |
Version: | Always 1. |
Chain ID: | rougechain-mainnet-1 (mainnet) or rougechain-devnet-1 (testnet). A node reports its own in GET /api/health. |
Nonce: | From your server, single use, 16 to 128 characters of A-Z a-z 0-9 - _. |
Issued At: | ISO 8601 time with a time zone, e.g. new Date().toISOString(). |
Expiration Time: | Optional, same format. Recommended: a few minutes after Issued At. |
Resources: | Optional. Then one - <uri> line per resource. |
Lines are separated by a single \n. There is no trailing newline. The order is fixed. There is
exactly one valid text for a given set of fields: parseSignInMessage returns null for
anything else (extra spaces, \r\n, another order, unknown lines).
Build it with createSignInMessage rather than by hand:
import { createSignInMessage, pubkeyToAddress } from "@rougechain/sdk";
const message = createSignInMessage({
domain: window.location.host,
address: await pubkeyToAddress(publicKey),
uri: window.location.origin + "/login",
statement: "Sign in to see your tickets.",
nonce, // from your server
issuedAt: new Date().toISOString(),
expirationTime: new Date(Date.now() + 10 * 60_000).toISOString(),
chainId: "rougechain-mainnet-1",
});
SDK reference
All of these are exported by @rougechain/sdk from 1.13.0.
| Function | Returns | Notes |
|---|---|---|
signMessage(privateKeyHex, message) | signature (hex) | message is a string or a Uint8Array. For scripts and servers that hold a key; browsers use the wallet. |
verifyMessage(publicKeyHex, message, signatureHex) | boolean | false for anything that is not a valid signature of exactly that message by that key, including malformed input. Never throws. |
messageSigningBytes(message) | Uint8Array | The exact bytes that are signed. |
createSignInMessage(fields) | string | Throws on an invalid field. |
parseSignInMessage(text) | fields or null | null unless the text is exactly canonical. |
verifySignIn(params) | Promise<{ valid: true, address, publicKey, fields } | { valid: false, error }> | See below. Never throws or rejects. |
verifySignIn({ message, signature, publicKey, expectedDomain, expectedNonce, expectedChainId, now?, maxClockSkewMs?, maxAgeMs? })
checks, in this order, and reports the first failure in error:
error | Meaning |
|---|---|
malformed_message | The text is not a canonical sign-in message. |
invalid_signature | The signature is not valid for this text and public key. |
address_mismatch | The address in the message is not the address of publicKey. |
domain_mismatch | The message is for another site than expectedDomain (compared case-insensitively). |
nonce_mismatch | The nonce is not expectedNonce. |
chain_id_mismatch | The chain id is not expectedChainId. |
issued_in_future | Issued At is later than now plus maxClockSkewMs (default 60 seconds). |
expired | now is at or after Expiration Time. |
too_old | Only with maxAgeMs: Issued At is longer ago than that. |
Token gating: a complete example
The flow:
- The browser asks your server for a nonce.
- The browser connects the wallet, builds the sign-in message and calls
signMessage. - Your server runs
verifySignIn, marks the nonce used, and starts a session for the address. - Your server reads the wallet's holdings from a node and decides.
- At the door (ticket scan, download, entry to the room) your server reads the holdings again.
Browser
import { createSignInMessage, pubkeyToAddress } from "@rougechain/sdk";
export async function signIn() {
const provider = window.rougechain;
if (!provider?.isRougeChain) throw new Error("Install the RougeChain wallet or open this page in Qwalla.");
if (typeof provider.signMessage !== "function") throw new Error("Update your RougeChain wallet to sign in.");
const { publicKey } = await provider.connect();
const { nonce } = await (await fetch("/api/auth/nonce", { method: "POST", credentials: "include" })).json();
const message = createSignInMessage({
domain: window.location.host,
address: await pubkeyToAddress(publicKey),
uri: window.location.origin + "/login",
statement: "Sign in to see your tickets.",
nonce,
issuedAt: new Date().toISOString(),
expirationTime: new Date(Date.now() + 10 * 60_000).toISOString(),
chainId: "rougechain-mainnet-1",
});
const signed = await provider.signMessage({ message }); // the wallet asks the user
const res = await fetch("/api/auth/verify", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, signature: signed.signature, publicKey: signed.publicKey }),
});
if (!res.ok) throw new Error((await res.json()).error);
return res.json(); // { address, allowed }
}
Server (Node 20+, Express)
import express from "express";
import crypto from "node:crypto";
import { parseSignInMessage, verifySignIn } from "@rougechain/sdk";
const DOMAIN = "tickets.example.com"; // your site's host, exactly as in the browser's address bar
const CHAIN_ID = "rougechain-mainnet-1";
const NODE_API = "https://api.rougechain.io/api"; // a node YOU choose to trust; your own node is best
const GATE = { token: "TICKET", minBalance: 1, nftCollection: null }; // set nftCollection to a collection id to gate on an NFT
const app = express();
app.use(express.json({ limit: "32kb" })); // a signature + public key are about 10.5 KB of hex
// Use a shared store with expiry (Redis, a database table) when you run more than one process.
const nonces = new Map(); // nonce -> expiry (ms)
const sessions = new Map(); // session id -> { address, publicKey }
const NONCE_TTL_MS = 10 * 60_000;
// 1. Issue a nonce. 16 random bytes as hex = 32 characters.
app.post("/api/auth/nonce", (req, res) => {
const nonce = crypto.randomBytes(16).toString("hex");
nonces.set(nonce, Date.now() + NONCE_TTL_MS);
res.json({ nonce });
});
// 3. Verify the sign-in.
app.post("/api/auth/verify", async (req, res) => {
const { message, signature, publicKey } = req.body ?? {};
// The nonce must be one WE issued, not expired, and it is consumed NOW — before any other
// check — so that a signed message can be presented exactly once.
const nonce = parseSignInMessage(message)?.nonce;
const expiry = nonce ? nonces.get(nonce) : undefined;
if (nonce) nonces.delete(nonce);
if (!expiry || expiry < Date.now()) return res.status(401).json({ error: "unknown or expired nonce" });
const result = await verifySignIn({
message, signature, publicKey,
expectedDomain: DOMAIN,
expectedNonce: nonce,
expectedChainId: CHAIN_ID,
maxAgeMs: NONCE_TTL_MS,
});
if (!result.valid) return res.status(401).json({ error: result.error });
const sid = crypto.randomBytes(32).toString("hex");
sessions.set(sid, { address: result.address, publicKey: result.publicKey });
res.cookie("sid", sid, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 3_600_000 });
res.json({ address: result.address, allowed: await holdsGate(result.publicKey) });
});
// 4. Read holdings from the node.
async function getJson(path) {
const r = await fetch(NODE_API + path, { signal: AbortSignal.timeout(8_000) });
if (!r.ok) throw new Error(`node returned ${r.status} for ${path}`);
return r.json();
}
async function holdsGate(publicKey) {
if (GATE.token) {
// GET /api/balance/:publicKey/:tokenSymbol -> { success, token_symbol, balance }
const { balance } = await getJson(`/balance/${publicKey}/${encodeURIComponent(GATE.token)}`);
if (balance >= GATE.minBalance) return true;
}
if (GATE.nftCollection) {
// GET /api/nft/owner/:pubkey -> { nfts: [{ collection_id, token_id, owner, locked, ... }] }
const { nfts } = await getJson(`/nft/owner/${publicKey}`);
if (nfts.some((n) => n.collection_id === GATE.nftCollection)) return true;
}
return false;
}
// 5. Check again at the door. A session proves who signed in, not what they hold now.
app.post("/api/tickets/:id/admit", async (req, res) => {
const sid = /(?:^|;\s*)sid=([0-9a-f]{64})/.exec(req.headers.cookie ?? "")?.[1];
const session = sid && sessions.get(sid);
if (!session) return res.status(401).json({ error: "sign in first" });
if (!(await holdsGate(session.publicKey))) return res.status(403).json({ error: "this wallet no longer holds the required token" });
res.json({ admitted: true, address: session.address });
});
app.listen(3000);
The node endpoints used
These are public, unauthenticated GET endpoints of every RougeChain node
(see Wallet API and NFTs API):
| Endpoint | Returns | Use it for |
|---|---|---|
GET /api/balance/:publicKey/:tokenSymbol | { success, token_symbol, balance } | One token's balance. balance is 0 when the wallet holds none. The symbol is case-sensitive (TICKET, qETH). |
GET /api/balance/:publicKey | { success, balance, token_balances, lp_balances } | XRGE (balance) and every token at once (token_balances maps symbol to balance). |
GET /api/nft/owner/:pubkey | { nfts: [ { collection_id, token_id, owner, name, locked, … } ] } | Every NFT a wallet owns. |
GET /api/nft/token/:collectionId/:tokenId | the NFT (owner is the holder's public key), or 404 | Who owns one specific NFT, for example a numbered ticket. |
GET /api/health | { status, chain_id, height } | Checking you are talking to the network you expect. |
Pass the wallet's hex public key (the publicKey that verifySignIn returned). The NFT
endpoints match the owner exactly as recorded, which is the public key; a rouge1… address does
not find NFTs there. Token balances are in the units the node reports for that token.
Units and precision
Use the exact fields for any threshold check. The float fields (balance, token_balances) remain
for display and cannot represent every amount exactly above 2^53 units.
| Endpoint | Exact fields | Meaning |
|---|---|---|
GET /api/balance/:publicKey/:tokenSymbol | balance_raw (string), decimals | The balance in integer base units; balance_raw / 10^decimals is the displayed amount |
GET /api/balance/:publicKey | balance_quanta (string), token_balances_raw (symbol → string) | XRGE in quanta (1 XRGE = 10^9 quanta) and every token in integer units |
- Tokens. Tokens created on RougeChain have
decimals: 0: one unit is one whole token, so a gate of "at least 1 TICKET" isBigInt(balance_raw) >= 1n. The bridged assets differ:qUSDCandqETHuse 6 decimals andqBTCuses 8.GET /api/tokenslists every token'sdecimals. Convert your threshold to base units once (minimum × 10^decimals) and compare integers. - XRGE. XRGE is the chain's native coin. It is held in the native ledger, not the token ledger,
and
XRGEis a reserved symbol that no token can use.GET /api/balance/:publicKey/XRGEreturns the native balance withnative: true,decimals: 9andbalance_rawin quanta. Nodes before this change answer0forXRGEon the per-token endpoint and have no exact fields; on those, read XRGE frombalanceinGET /api/balance/:publicKey(display units). A client can tell the two apart by the presence ofbalance_raw. - Symbols are case-sensitive, and an unknown symbol returns
0rather than an error, so check the symbol againstGET /api/tokenswhen you configure a gate.
What a verifier must check
verifySignIn does the cryptography. The rest is yours:
- Domain. Pass your own host as
expectedDomain, from configuration, never from the request. Without this check a signature given to another site could be replayed to yours. - Nonce, single use. Issue it on your server, keep it for a few minutes, and delete it the
first time it is presented, whether or not verification succeeds.
verifySignIncannot know if a nonce was used before. - Expiry. Put an
Expiration Timea few minutes ahead in the message, or passmaxAgeMs. Keep your server's clock synchronised. - Chain ID — required. A verifier MUST check the
Chain IDline: pass the network you gate on asexpectedChainId(verifySignInrequires it and fails withchain_id_mismatchotherwise), so a sign-in made for one network is never accepted for another. - Use the address
verifySignInreturns. Do not trust an address or public key sent alongside the message. - Holdings come from a node you choose. The signature proves control of a key; it says
nothing about balances. Whoever runs the node you query can answer anything, so use your own
node, or one you trust, and check
chain_idin/api/healthif the URL is configurable. - Re-check holdings at entry time, not only at login. Tokens and NFTs can be transferred a
second after the login. A gate that only checks at login can be passed by lending the token to
one wallet after another. Check when the benefit is delivered (the door scan, the download, the
mint), and for a high-value gate consider admitting each NFT
token_idonly once.
A sign-in is not a transaction approval and gives your site no power over the wallet. It also does not prove the visitor is a person, or that one person has only one wallet.
For wallet developers
A wallet that implements signMessage must:
- sign exactly the bytes above, and in
signTransactiononly sign bytes that are the canonical JSON of the payload it shows; - ask the user on every request and show the origin and the complete message;
- warn when a sign-in message names another domain than the requesting origin.
@rougechain/sdk exports what the reference wallets use: reviewSignMessageRequest(message, origin, walletAddress),
visibleMessageText, looksLikeTransactionPayload and MAX_SIGN_MESSAGE_BYTES. A shared test
vector (fixed key, message, signed bytes and one valid signature) is in
sdk/test/fixtures/sign-message-vector.json.
Verifying Finality
A node's API answers "this transaction succeeded" because the node says so. If you accept
deposits, settle trades or release goods on that answer, you are trusting that one node.
verifyTxFinalized from @rougechain/sdk (1.14.0 or later) checks the claim against the chain
itself: it proves that a transaction is included in a finalized block, using only hashes and
signatures it recomputes.
import { verifyTxFinalized } from "@rougechain/sdk";
const result = await verifyTxFinalized({
txHash: "72952313b71c7fea36a4f553e0eeb3b5e5e09b95e49a5ea33c326d0dd9c25cf1",
nodes: ["https://api.rougechain.io/api", "https://node-b.example/api", "https://node-c.example/api"],
chainId: "rougechain-mainnet-1",
trustedValidatorSet: PINNED_SET, // see "Pin the validator set" below
});
if (result.status === "finalized") {
// Included and final. Whether it SUCCEEDED is a separate question: see result.outcome.
}
Results
status | Meaning |
|---|---|
finalized | The transaction is in a block whose integrity, proposer signature and finality certificate all verified, and a strict majority of nodes serve that same block. |
included-not-finalized | The transaction is in a valid block, but there is no finality certificate yet (normally the newest block), too few of nodes answered, no validator set could be obtained, or minConfirmations is not met. Ask again later. |
not-found | No node knows the transaction, the block at the height a node named does not contain it, or the block could not be fetched. |
invalid | Something failed verification: a hash, a signature, the chain id, the certificate, or two nodes serving different blocks at the same height. Do not accept the payment. Read checks to see which part failed. |
The result also contains:
height,blockHash(recomputed, not copied from the node),txIndex.trust:"pinned validator set"or"node-reported validator set".proof: the header fields, where the certificate came from, the verified certificate (signers, voting stake, total stake, quorum) and the nodes that agreed.outcome:{ source: "node receipt", verified: false, value }. See below.checks: every check performed, each{ name, ok, critical, detail, node }. A check withcritical: falseis informational and does not changestatus.
What is verified
The verifier recomputes every hash it relies on. It never uses a node's hash, txId,
success or finalizedHeight field as an input.
- The transaction is in the block. It fetches the block (
GET /api/blocks?from_height=H), hashes each transaction exactly as the node does (SHA-256 of the transaction's canonical JSON) and looks fortxHash. The height a node reports for the transaction is only a hint. - The block's transaction list matches its header. It recomputes the header's
tx_hashfrom the full transaction list, so a transaction added, removed, altered or reordered is detected. - The header is the one the proposer signed. It recomputes the block hash,
SHA-256(header JSON ‖ proposer signature), verifies the proposer's ML-DSA-65 signature over the header, and checks that the header'schain_idis thechainIdyou passed. - The block is finalized. It takes the finality certificate for that height (from the next
block's
parent_commit, or fromGET /api/finality/:height) and checks every precommit: its ML-DSA-65 signature overROUGECHAIN_FINALITY_VOTE_V2|chain=<chain id>|type=precommit|height=<H>|round=0|block=<block hash>, that it is for this exact block, and that each signer is a distinct validator in the set used. The signers' stake must reach the quorumfloor(2 × total stake / 3) + 1. The voting stake, total stake and quorum written inside the certificate are compared with the recomputed values afterwards; they are never used as inputs. - The nodes agree. Every node in
nodesis asked for the block at that height. A strict majority must serve the same (recomputed) block hash. Any node serving a different block at that height makes the resultinvalid. Unreachable nodes count as neither.
fetchAndVerifyBlock(height, { node, chainId }) runs steps 2 and 3 for one block and returns the
verified block and the next block. verifyBlock and verifyFinalityCertificate work on data you
already have.
What is NOT verified
Whether the transaction succeeded
Inclusion and finality do not prove that a transaction succeeded. A transaction that fails during execution, for example a transfer from an account with too little balance, is still included in the block and still finalized. Whether it succeeded is recorded in the node's receipt, and the receipt is not committed in the block header. No signature or certificate covers it.
verifyTxFinalized therefore returns the receipt status separately and marks it unverified:
result.outcome; // { source: "node receipt", verified: false, value: "Success" }
// or { ..., value: { Failed: "reason" } }
Treat outcome.value as the node's word. To reduce that trust, ask several independent nodes
and compare, or check the resulting balance on several nodes. A planned protocol upgrade on the
roadmap will commit each transaction's outcome to the block header; once it is live, the
verifier can check outcomes the same way it checks inclusion.
The validator set, unless you pin it
A certificate is only as good as the validator set it is checked against. If you do not pass
trustedValidatorSet, the verifier fetches the set from the node that served the block
(GET /api/validators) and reports trust: "node-reported validator set". A dishonest node can
then invent a validator set containing keys it controls, sign a certificate with them, and the
check would pass. Without a pinned set, you trust that node for the validator set.
Nodes only expose the current validator set, not the set that applied at a past height. A
stake change since then can make a certificate's written totals differ from the current set
(reported as informational cert.claim.* checks) or, if a past signer is no longer a
validator, make an older certificate fail against the current set.
State
Balances, token supplies and contract state are not checked. Only inclusion and finality are.
Pin the validator set
For real security, obtain the validator public keys and stakes from a source you trust (your own node, several operators, a published list you check) and pass them:
const PINNED_SET = {
validators: [
{ publicKey: "8ccf7878…", stake: 100090000n },
{ publicKey: "21e0ed0a…", stake: 9000n },
{ publicKey: "c97f59a2…", stake: 10000n },
],
// optional: the height this set applies to. When it equals the block's height, the
// certificate's written totals must match exactly.
height: undefined,
};
List only eligible validators (staked and not jailed). Stakes are in the node's raw stake units,
as /api/validators reports them. Update the pinned set when the validator set changes; a stale
set makes new certificates fail (invalid), which is the safe direction.
Background
- Finality exists since block 150. From block 151 every block header carries the verified
finality certificate of its parent (see Finality). Blocks before 150
have no certificate and return
included-not-finalized. - One block proposer today. Mainnet currently has a single designated proposer, and the largest validator holds more than two thirds of the stake, so its signature alone meets the quorum. The verifier checks the rule as written; it does not make the validator set more decentralized than it is.
minConfirmations(default 1) additionally requirestip − height + 1blocks. It is optional: a verified certificate already makes a block final.- Pass
fetchto use your own HTTP client, andtimeoutMs(default 15,000) per request.
SDK (@rougechain/sdk)
The official JavaScript/TypeScript SDK for building on RougeChain.
Installation
npm install @rougechain/sdk
Quick Start
import { RougeChain, Wallet } from '@rougechain/sdk';
const rc = new RougeChain('https://testnet.rougechain.io/api');
// Generate a new post-quantum wallet (24-word BIP-39 mnemonic + keypair)
const wallet = Wallet.generate();
console.log('Public key:', wallet.publicKey);
console.log('Recovery phrase:', wallet.mnemonic); // save this to restore the wallet
// Request testnet tokens
await rc.faucet(wallet);
// Check balance
const balance = await rc.getBalance(wallet.publicKey);
console.log('Balance:', balance);
// Transfer tokens
await rc.transfer(wallet, { to: recipientPubKey, amount: 100 });
Wallet
All signing happens client-side. Private keys never leave your application.
import { Wallet } from '@rougechain/sdk';
// Generate a new wallet WITH a 24-word BIP-39 recovery phrase (default, recommended)
const wallet = Wallet.generate();
console.log(wallet.mnemonic); // "word1 word2 … word24"
// 12-word phrase instead of 24
const wallet12 = Wallet.generate(128);
// Generate from pure entropy — no mnemonic, cannot be seed-restored
const random = Wallet.generateRandom();
// Restore from a BIP-39 recovery phrase (optionally with a 25th-word passphrase)
const fromSeed = Wallet.fromMnemonic('word1 word2 … word24');
// Restore from saved hex keys
const restored = Wallet.fromKeys(publicKey, privateKey);
// Derive the short Bech32m address (rouge1…) from the public key
const address = await wallet.address();
// Export for storage — includes `mnemonic` when the wallet has one
const keys = wallet.toJSON(); // { publicKey, privateKey, mnemonic? }
// Verify keypair integrity
const valid = wallet.verify(); // true
The mnemonic model is what the MCP server uses via
ROUGECHAIN_MNEMONIC. Store the phrase securely — anyone with it controls the wallet.
Client
import { RougeChain } from '@rougechain/sdk';
// Basic connection
const rc = new RougeChain('https://testnet.rougechain.io/api');
// With API key
const rc = new RougeChain('https://testnet.rougechain.io/api', {
apiKey: 'your-api-key',
});
Available Methods
Queries
await rc.getHealth();
await rc.getStats();
await rc.getBlocks({ limit: 10 });
await rc.getBalance(publicKey);
await rc.getValidators();
await rc.getTokens();
await rc.getBurnedTokens();
Transactions
await rc.transfer(wallet, { to: recipient, amount: 100 });
await rc.transfer(wallet, { to: recipient, amount: 50, token: 'MYTOKEN' });
await rc.createToken(wallet, { name: 'My Token', symbol: 'MTK', totalSupply: 1_000_000, image: 'https://example.com/logo.png' });
await rc.burn(wallet, 500, 1, 'XRGE');
await rc.faucet(wallet);
Mintable tokens (SDK 1.12.0, node TOKEN_MINTING upgrade)
Mintable tokens need the node's TOKEN_MINTING upgrade, active on mainnet since block 235 and on
testnet since block 1360. On a network where it is not active the node refuses mintable /
maxSupply and every mint with "token minting is not active yet". To check:
import { tokenMintingActive } from '@rougechain/sdk';
await rc.isTokenMintingActive(); // GET /api/stats → upgrade_schedule.token_minting
tokenMintingActive(await rc.getStats()); // the same check on stats you already have
// Creator can mint more later; maxSupply (optional) caps initial + minted supply
await rc.createToken(wallet, { name: 'My Token', symbol: 'MTK', totalSupply: 1_000_000, mintable: true, maxSupply: 5_000_000 });
// Signed mint_tokens → POST /api/v2/token/mint (creator only, 1 XRGE fee)
const r = await rc.mintTokens(wallet, { symbol: 'MTK', amount: 250_000 });
if (!r.success) console.error(r.error); // not creator / not mintable / exceeds max supply / not active
// Supply info
const meta = await rc.getTokenMetadata('MTK'); // mintable, max_supply, total_minted, initial_supply
mintable: trueandmax_supplyare signed only whenmintableis true, so a fixed-supply token's payload is the same as before. AmaxSupplywithoutmintable, amaxSupplybelowtotalSupply, a non-integer value or anything above 2^53 − 1 returns{ success: false, error }without sending anything.tokenMintFields(initialSupply, { mintable, maxSupply })runs the same validation and returns the fields to sign.- Low-level builders:
createSignedTokenCreation(wallet, name, symbol, supply, fee?, image?, { mintable, maxSupply, description })andcreateSignedTokenMint(wallet, symbol, amount, fee = 1, accountNonce?). - Constants:
TOKEN_MINT_MAX_AMOUNT(2^53 − 1),TOKEN_MINT_FEE_XRGE(1),TOKEN_CREATE_FEE_XRGE(100). - Before 1.12.0,
mintTokenssent an unsigned request that the node always rejected, andcreateTokendroppedmintable/maxSupply.
Staking
await rc.stake(wallet, { amount: 1000 });
await rc.unstake(wallet, { amount: 500 });
DEX (rc.dex)
// Pool queries
await rc.dex.getPools();
await rc.dex.getPool('XRGE-MTK');
await rc.dex.getPriceHistory('XRGE-MTK'); // PriceSnapshot[] — for building charts
await rc.dex.getPoolStats('XRGE-MTK'); // volume, swap counts (total + 24h)
await rc.dex.getPoolEvents('XRGE-MTK'); // swap/add/remove event history
// Quote & swap
await rc.dex.quote({ poolId: 'XRGE-MTK', tokenIn: 'XRGE', tokenOut: 'MTK', amountIn: 100 });
await rc.dex.swap(wallet, { tokenIn: 'XRGE', tokenOut: 'MTK', amountIn: 100, minAmountOut: 95 });
// Liquidity
await rc.dex.createPool(wallet, { tokenA: 'XRGE', tokenB: 'MTK', amountA: 10000, amountB: 5000 });
await rc.dex.addLiquidity(wallet, { poolId: 'XRGE-MTK', amountA: 1000, amountB: 500 });
await rc.dex.removeLiquidity(wallet, { poolId: 'XRGE-MTK', lpAmount: 100 });
NFTs (rc.nft)
await rc.nft.createCollection(wallet, { symbol: 'ART', name: 'My Art', royaltyBps: 500, maxSupply: 10000 });
// Route royalties to another wallet or a contract (defaults to the creator). Only use a contract with a tested XRGE payout path — see Contract XRGE custody.
await rc.nft.createCollection(wallet, { symbol: 'ART2', name: 'Split Art', royaltyBps: 1000, royaltyRecipient: payoutWalletPubKey });
await rc.nft.mint(wallet, { collectionId: 'abc123', name: 'Piece #1', metadataUri: '...' });
await rc.nft.batchMint(wallet, { collectionId: 'abc123', names: ['#1', '#2'], uris: ['...', '...'] });
await rc.nft.transfer(wallet, { collectionId: 'abc123', tokenId: 1, to: buyerPubKey, salePrice: 100 });
await rc.nft.getCollections();
await rc.nft.getByOwner(wallet.publicKey);
Bridge (rc.bridge)
await rc.bridge.getConfig();
await rc.bridge.withdraw(wallet, { amount: 500000, evmAddress: '0x...' });
await rc.bridge.claim({ evmTxHash: '0x...', evmAddress: '0x...', evmSignature: '0x...', recipientPubkey: wallet.publicKey });
await rc.bridge.getWithdrawals();
Mail Name Registry (rc.mail)
Register human-readable names and resolve recipients before sending encrypted mail. Third-party apps must call these for cross-app mail to work.
All write operations require a wallet parameter for ML-DSA-65 signed requests via /api/v2/ endpoints with anti-replay nonce protection.
// Register wallet on the node first (signed request)
await rc.messenger.registerWallet(wallet, {
id: wallet.publicKey,
displayName: "Alice",
signingPublicKey: wallet.publicKey,
encryptionPublicKey: encPubKey,
});
// Register a mail name (signed request)
await rc.mail.registerName(wallet, "alice", wallet.publicKey);
// Resolve a name → wallet info (public, no signing needed)
const resolved = await rc.mail.resolveName("bob");
// { entry: { name, wallet_id }, wallet: { id, encryption_public_key, ... } }
// Reverse lookup: wallet ID → name (public, no signing needed)
const name = await rc.mail.reverseLookup(wallet.publicKey); // "alice"
// Release a name (signed request)
await rc.mail.releaseName(wallet, "alice");
// Send mail (signed request, multi-recipient CEK encryption)
await rc.mail.send(wallet, { from, to, subject, body, encrypted_subject, encrypted_body });
// Read inbox / sent (signed requests)
const inbox = await rc.mail.getInbox(wallet);
const sent = await rc.mail.getSent(wallet);
// Manage mail (signed requests)
await rc.mail.markRead(wallet, messageId);
await rc.mail.move(wallet, messageId, "trash");
await rc.mail.delete(wallet, messageId);
Messenger (rc.messenger)
All operations use ML-DSA-65 signed requests with nonce-based anti-replay protection.
await rc.messenger.getWallets();
await rc.messenger.registerWallet(wallet, { id, displayName, signingPublicKey, encryptionPublicKey });
await rc.messenger.getConversations(wallet);
await rc.messenger.createConversation(wallet, [pubKeyA, pubKeyB]);
await rc.messenger.getMessages(wallet, conversationId);
await rc.messenger.sendMessage(wallet, conversationId, encryptedContent, { selfDestruct: true, destructAfterSeconds: 30 });
await rc.messenger.deleteMessage(wallet, messageId, conversationId);
await rc.messenger.deleteConversation(wallet, conversationId);
Social (rc.social)
Posts, replies, reposts, follows, likes, and track comments. These are node-hosted — stored in the node's own database, not in blocks. Writes are ML-DSA-65 signed; reads are public.
// Reads
await rc.social.getGlobalTimeline(50, 0); // newest-first feed
await rc.social.getPost(postId, viewerPubKey); // single post + stats
await rc.social.getUserPosts(pubKey);
await rc.social.getPostReplies(postId);
await rc.social.getTrackStats(trackId, viewerPubKey);
await rc.social.getArtistStats(artistPubKey, viewerPubKey);
// Writes (signed)
await rc.social.createPost(wallet, 'gm from RougeChain');
await rc.social.createPost(wallet, 'nice!', parentPostId); // reply
await rc.social.deletePost(wallet, postId);
await rc.social.toggleRepost(wallet, postId);
await rc.social.toggleFollow(wallet, artistPubKey);
await rc.social.toggleLike(wallet, trackId);
await rc.social.postComment(wallet, trackId, 'great track');
Push Notifications
// Register for push notifications (PQC-signed) — pass the wallet, not raw keys
await rc.registerPushToken(wallet, 'ExponentPushToken[xxx]'); // platform defaults to "expo"
await rc.registerPushToken(wallet, 'ExponentPushToken[xxx]', 'expo');
// Unregister
await rc.unregisterPushToken(wallet);
Address Resolution
// Resolve rouge1… address to public key, or vice versa
const result = await rc.resolveAddress('rouge1q8f3x...');
// → { address, publicKey, balance }
Account Nonce
const nonce = await rc.getNonce(publicKey);
// → { nonce, next_nonce }
Token Allowances (ERC-20 style approve / transferFrom)
There are no rc.approveAllowance / rc.transferFrom convenience methods. Build the
signed transaction with the exported signer helpers, then submit it with
rc.submitTx(endpoint, signedTx):
import {
createSignedTokenApproval,
createSignedTokenTransferFrom,
} from '@rougechain/sdk';
// Approve a spender to move up to `amount` of your MTK
await rc.submitTx(
'/v2/token/approve',
createSignedTokenApproval(wallet, spenderPubKey, 'MTK', 1000),
);
// Spend an existing allowance on the owner's behalf
await rc.submitTx(
'/v2/token/transfer-from',
createSignedTokenTransferFrom(wallet, ownerPubKey, recipientPubKey, 'MTK', 1000),
);
// Read current allowances (public)
await rc.get('/token/allowances');
await rc.get('/token/allowance');
Freeze/unfreeze a token you created is a creator-only signed request to
/api/v2/token/freeze(payload{ tokenSymbol, frozen }). There is no dedicated SDK helper — sign it with the genericsignRequest(wallet, payload)and post it.
Message Signing and Sign-In (SDK 1.13.0)
Prove control of a wallet to a website without a transaction. Full guide, including the exact signed bytes and a token-gating server: Wallet Authentication.
import {
signMessage, verifyMessage,
createSignInMessage, parseSignInMessage, verifySignIn,
pubkeyToAddress,
} from "@rougechain/sdk";
// Any message (string or Uint8Array)
const signature = signMessage(wallet.privateKey, "hello");
verifyMessage(wallet.publicKey, "hello", signature); // true; false on anything else, never throws
// Sign-in message
const message = createSignInMessage({
domain: "tickets.example.com",
address: await pubkeyToAddress(wallet.publicKey),
uri: "https://tickets.example.com/login",
statement: "Sign in to see your tickets.", // optional
nonce, // server-issued, single use, >= 16 characters
issuedAt: new Date().toISOString(),
expirationTime: new Date(Date.now() + 10 * 60_000).toISOString(), // optional
chainId: "rougechain-mainnet-1",
resources: ["https://tickets.example.com/events/42"], // optional
});
const result = await verifySignIn({
message, signature: signMessage(wallet.privateKey, message), publicKey: wallet.publicKey,
expectedDomain: "tickets.example.com",
expectedNonce: nonce,
expectedChainId: "rougechain-mainnet-1",
});
// { valid: true, address, publicKey, fields } or { valid: false, error }
| Function | Description |
|---|---|
signMessage(privateKeyHex, message) | ML-DSA-65 signature (hex) over "\x19RougeChain Signed Message:\n" + decimal(byte length) + "\n" + message bytes. |
verifyMessage(publicKeyHex, message, signatureHex) | boolean. Never throws. |
messageSigningBytes(message) | The exact signed bytes. |
createSignInMessage(fields) / parseSignInMessage(text) | Build / strictly parse the canonical sign-in text. |
verifySignIn(params) | Signature, address-of-key, domain, nonce, chain id, issued-at and expiry checks. Resolves to a result object, never rejects. |
In a browser the private key stays in the wallet: call window.rougechain.signMessage({ message })
(extension 1.8.0+, Qwalla after its next update) and verify the result on your server. A message
signature is never a transaction signature and signTransaction / verifyTransaction /
signRequest are unchanged.
Verifying Finality (SDK 1.14.0)
Check that a transaction is in a finalized block without trusting a node's "success" answer. Full guide, including what it cannot prove: Verifying Finality.
import { verifyTxFinalized } from '@rougechain/sdk';
const r = await verifyTxFinalized({ txHash, nodes: [nodeA, nodeB, nodeC], chainId: 'rougechain-mainnet-1', trustedValidatorSet });
r.status; // "finalized" | "included-not-finalized" | "not-found" | "invalid"
r.checks; // every check, pass/fail
r.outcome; // { source: "node receipt", verified: false, value } — success is NOT proven
Multi-Sig Wallets
Multi-sig queries are available today:
const wallets = await rc.get('/multisig/wallets');
const myWallets = await rc.get(`/multisig/wallets/${myPubKey}`);
const wallet_ = await rc.get(`/multisig/wallet/${walletId}`);
const proposals = await rc.get(`/multisig/wallet/${walletId}/proposals`);
Note: creating a multi-sig wallet and submitting/approving proposals (
multisig_create/multisig_submit/multisig_approve) are on-chain transaction types, but they are not yet exposed through a stable SDK method or public submit endpoint — the node currently serves the multi-sig read routes above only. Do not callrc.sendTransaction(...); that method does not exist. Multi-sig write support is tracked for a future release.
Smart Contracts (rc.contracts)
Since 1.9.0. Contract transactions are signed by your wallet: the signer is the caller the contract sees and pays the fee. See WASM Smart Contracts for the host functions and the raw API.
// Publish WASM (Uint8Array, ArrayBuffer or base64). Fee: 10 XRGE flat.
const pub = await rc.contracts.publish(wallet, wasmBytes);
// → { success, txId, address, predictedAddress, nonce, fee }
await rc.contracts.waitForReceipt(pub.txId!); // resolves once the tx is in a block
// The address is fixed by what you sign; compute it offline:
rc.contracts.predictContractAddress(wallet.publicKey, pub.nonce, wasmBytes);
// Read-only call (free, no signature)
const q = await rc.contracts.query(addr, 'get_score', { player: wallet.publicKey });
// → { success, returnData, gasUsed, events, error? }
// Signed call. Fee = gasLimit × 0.000001 XRGE.
// Without gasLimit the SDK queries first and signs min(ceil(gasUsed × 1.5) + 1000, 10M).
const r = await rc.contracts.execute(wallet, addr, 'move', { x: 1, y: 2 });
const r2 = await rc.contracts.execute(wallet, addr, 'move', { x: 3 }, { gasLimit: 80_000 });
// → { success, txId, fee, gasLimit, preview: { returnData, gasUsed, events } }
// The node dry-runs first; a call that would fail returns { success: false, error } and costs nothing.
// The block run is authoritative: a call that reverts there is still included and charged,
// and its receipt says so.
const receipt = await rc.contracts.waitForReceipt(r.txId!);
// receipt.status === "Success", or { Failed: "<error>" } when the call reverted in the block
// Metadata, storage, events, list
const meta = await rc.contracts.get(addr); // null if none
const state = await rc.contracts.state(addr); // whole storage
const one = await rc.contracts.state(addr, 'score'); // hex key, else UTF-8
const events = await rc.contracts.events(addr, { limit: 50, before: 12_000 });
const byTx = await rc.contracts.events(addr, { tx: r.txId }); // events of one tx
const all = await rc.contracts.list();
// Live events after each accepted block (shared socket, auto-reconnect + resubscribe)
const stop = rc.contracts.subscribe(addr, (e) => console.log(e.topic, e.data, e.block_height));
stop();
Game helper — a handle bound to one contract (and optionally a wallet):
const game = rc.contracts.game(addr, wallet);
const off = game.on('move', (e) => render(JSON.parse(e.data))); // '*' = every topic
await game.call('move', { x: 1, y: 2 }); // signed; needs the wallet
const board = await game.query('board'); // free; caller = wallet key
const s = await game.state();
off();
Payable calls (SDK 1.10.0; the node accepts them from the payable-calls upgrade, block 190). A call can pay the contract in XRGE or a token. The amount is an integer: quanta for XRGE (1 XRGE = 1,000,000,000 quanta), raw units for tokens. The payment moves only if the call succeeds. A failing or trapping call leaves it with you, but the gas fee is still charged.
import { xrgeToQuanta, quantaToXrge } from '@rougechain/sdk';
const attach = { symbol: 'XRGE', amount: xrgeToQuanta('0.5') }; // 500_000_000n (exact, no floats)
// Free preview of the paid call. The contract sees the payment. Needs a caller.
const p = await rc.contracts.query(addr, 'roll', {}, wallet.publicKey, { attach });
// Signed, paid call. Without gasLimit, gas is sized from a preview that includes the payment.
const r = await rc.contracts.execute(wallet, addr, 'roll', {}, { attach });
// → { success, txId, fee, gasLimit, attach: { symbol: 'XRGE', amount: 500000000 } }
await game.call('buy', { item: 3 }, { attach: { symbol: 'GOLD', amount: 25 } }); // token units
quantaToXrge(1_500_000_000n); // "1.5"
amount may be a bigint, a safe-integer number or a digit string. The SDK throws, or returns
{ success: false, error }, for a non-integer, zero, negative or > Number.MAX_SAFE_INTEGER amount,
because the node reads it as an exact JSON integer. The node refuses the call if you can't cover
fee + XRGE payment, or the token amount.
Lower-level builders are exported too: createSignedContractCall,
createSignedContractPublish, predictContractAddress, suggestGasLimit,
contractCallFee, normalizeContractAttach, xrgeToQuanta, quantaToXrge. On Node versions without a global WebSocket, pass one:
new RougeChain(url, { WebSocket: (await import('ws')).default }).
The old
rc.shielded.deployContract/callContracthelpers used node-signed endpoints that are retired (deploy returns 410; call is a dry run only). They are deprecated; userc.contracts.
EIP-1559 Fee Info
const feeInfo = await rc.getFeeInfo();
// → { base_fee, total_fee_suggestion, total_fees_burned, block_height }
Environment Support
| Environment | Requirements |
|---|---|
| Browser | Any bundler (Vite, webpack) |
| Node.js 18+ | Works out of the box |
| Node.js < 18 | Provide node-fetch polyfill |
| React Native | Install react-native-get-random-values |
TypeScript
Full type declarations are included:
import type {
Block, Transaction, Validator, LiquidityPool, NftCollection,
NameEntry, ResolvedName, MailMessage, SendMailParams,
PriceSnapshot, PoolStats, SwapQuote,
} from '@rougechain/sdk';
Security
- All signatures use ML-DSA-65 (FIPS 204), resistant to quantum attacks
- Private keys never leave your application
- The SDK does not store keys — persistence is your responsibility
- All mail, messenger, and name registry operations use ML-DSA-65 signed requests with timestamp validation and nonce-based anti-replay protection
- Mail uses a CEK pattern for efficient multi-recipient encryption via ML-KEM-768
- TOFU key fingerprint tracking for contact key-change detection
Source
The SDK source code is in the sdk/ directory of the quantum-vault repository.
Troubleshooting
Common issues and how to fix them.
Node Issues
Node won't start
"Address already in use"
Another process is using the port. Find it and stop it, or use a different port:
# Linux/macOS
lsof -i :5100
kill -9 <PID>
# Windows (PowerShell)
Get-Process -Id (Get-NetTCPConnection -LocalPort 5100).OwningProcess
Stop-Process -Id <PID>
# Or just use a different port
./quantum-vault-daemon --api-port 5102
"cargo not found"
Rust isn't in your PATH. Fix with:
source ~/.cargo/env
# Or restart your terminal
"OpenSSL not found" (build error)
Install OpenSSL development headers:
# Ubuntu/Debian
sudo apt install libssl-dev pkg-config
# Fedora/RHEL
sudo dnf install openssl-devel
# macOS
brew install openssl
# Windows — install Visual Studio Build Tools with C++ workload
Node won't sync
| Check | How |
|---|---|
| Node is running | curl http://127.0.0.1:5100/api/health |
| Peers are correct | Make sure --peers includes /api (e.g. mainnet --peers "https://api.rougechain.io/api") |
| Genesis + chain-id match the network | Mainnet needs --genesis daemon/genesis-mainnet.json --chain-id rougechain-mainnet-1. A wrong/missing genesis or chain-id makes a fresh node fail to join — its /api/health chain_id must read rougechain-mainnet-1 |
| Firewall isn't blocking | Peers reach your node through its --public-url (the API port behind your reverse proxy); there is no separate P2P port |
| Network is reachable | mainnet curl https://api.rougechain.io/api/health (or testnet https://testnet.rougechain.io/api/health) |
Peers value of 0
Your node isn't connected to anyone. Check:
- You passed
--peerswith the correct URL (including/api) - Your internet connection is working
- The testnet node is online:
curl https://testnet.rougechain.io/api/stats
Height stuck at 0
If your node's chain height isn't advancing:
- Ensure
--peersis set (solo nodes without peers don't receive blocks) - If you're mining solo (
--minewithout--peers), blocks are only produced locally - Check logs for sync errors
Blocks not propagating
If you're mining but other nodes don't see your blocks:
- Set
--public-url— Without this, your node is invisible to the network. Other nodes can't sync from you. - Check your firewall — Your
--public-url(the HTTPS reverse proxy in front of your API port) must be reachable from the internet - Check you are the designated proposer — only the validator with the most stake proposes blocks (see Staking Issues)
- Verify with peers API:
curl https://testnet.rougechain.io/api/peers # Your node's URL should appear in the list
Transaction Issues
"Insufficient balance"
Transaction amount + fee must not exceed your balance. The fee for a wallet- or API-signed transfer is 1 XRGE, and the full fee is debited.
Required: amount + 1 XRGE
On testnet, use the faucet to get more tokens:
- Website: Go to the Wallet page and click "Request Faucet"
- API:
POST /api/v2/faucet
"Transaction rejected"
Common causes:
| Cause | Fix |
|---|---|
| Wrong signature | Ensure you're signing with the correct private key |
| Duplicate transaction | Wait a moment and retry — the previous tx may still be processing |
| Node not synced | Check GET /api/health — your node's height should match the network |
| Stale nonce | Refresh your wallet state and retry |
"Failed to fetch" in the web app
- Check you're connected to the right network (Testnet vs local Devnet)
- If using local devnet, make sure the daemon is running
- Check browser console (
F12) for the actual error - Verify CORS: a self-hosted node only allows a built-in set of origins by default — set
QV_CORS_ORIGINSor run with--dev(see Configuration → Browser access). The publictestnet/api.rougechain.ioendpoints already sendAccess-Control-Allow-Origin: *, so if you're hitting those, CORS is not the cause.
Wallet Issues
Wallet not loading
- Check the browser console for errors (
F12→ Console) - Try a different browser
- If using the extension, check it's enabled and not suspended
- As a last resort, clear the site's browser data — only after you have your seed phrase or a
.pqcbackupfile, because the web wallet's keys are stored in the browser and clearing it deletes them
Lost private key
Private keys are stored locally in your browser, so clearing browser data deletes them. You can restore the wallet from:
- your 24-word seed phrase — restores your signing key, address and funds; it also restores your messaging/mail encryption key if the wallet was created or imported from the phrase on the website since 2026-10-01 or in Qwalla (older website wallets and extension wallets have a random key), or
- an encrypted
.pqcbackupfile and its password — restores both keys.
See Create a Wallet → Backup. Without either, the keys cannot be recovered.
Best practice: Write down your seed phrase and export an encrypted .pqcbackup right after creating a wallet.
Extension not connecting
- Check the extension is enabled in your browser
- Ensure you're on a supported page (the extension injects on pages that need it)
- Try disabling and re-enabling the extension
- Check if another wallet extension is conflicting
Staking Issues
Can't stake — "insufficient balance"
You need at least 10,000 XRGE plus the transaction fee — the 10,000 XRGE minimum is enforced on every stake call. Use the faucet if needed.
Staked but not producing blocks
- Most common cause: your node's key is not your staked key. Your node signs
blocks with the keypair in
<data-dir>/node-keys.json, and peers reject blocks whose proposer isn't a staked validator. If you staked from one key but the node runs with a different (auto-generated) key, your blocks are silently rejected. The node'snode-keys.jsonkey and your staked key must be the same key — see Becoming a Validator. - Ensure the
--mineflag is set on your node - Your node must be synced (height matches the network)
- Only the designated proposer produces blocks. Since mainnet height 100 that is the eligible validator with the most stake; every other staked validator never proposes (its log shows
not the designated proposer … — not sealing), but it still votes and earns a stake-weighted share of fees in every block. Checkdesignated_proposer_nextinGET /api/statsorproposerinGET /api/selection - Check your validator status:
GET /api/validators— your node's public key should appear with active stake
Unstaked but balance not returned
After unstaking, the tokens enter a 500-block unbonding period and are then returned to your wallet address. Blocks are only produced when there are transactions, so how long this takes depends on network activity. Check your balance:
curl "https://testnet.rougechain.io/api/balance/YOUR_PUBLIC_KEY"
Bridge Issues
Bridge deposit not credited
- Confirm the EVM transaction was confirmed on Base mainnet (Base Sepolia only if you are on testnet)
- Deposits are credited after the required confirmation depth (default 6 Base blocks), then the deposit watcher claims them
- Check the bridge config:
GET /api/bridge/config - Verify the custody address matches your bridge target
Withdrawal pending
Withdrawals require the bridge relayer to process them. Check status:
curl "https://api.rougechain.io/api/bridge/withdrawals" # mainnet
curl "https://testnet.rougechain.io/api/bridge/withdrawals" # testnet
DEX Issues
Swap failed — "slippage exceeded"
The price moved between your quote and execution. Increase your slippage tolerance or retry.
Pool creation failed
- Both tokens must exist on the network
- You need sufficient balance of both tokens
- Pool creation fee is 10 XRGE
Platform-Specific
Windows: Build fails
- Install Visual Studio Build Tools with the C++ desktop workload
- Install Rust — make sure
cargois in your PATH - Restart your terminal after installation
Windows: Permission denied
Run PowerShell as Administrator, or check that your antivirus isn't blocking the daemon.
macOS: "developer cannot be verified"
xattr -d com.apple.quarantine ./target/release/quantum-vault-daemon
Still Stuck?
- Check the GitHub Issues
- Visit the built-in node dashboard at
http://localhost:5100for live diagnostics - Review daemon logs (stderr output) for detailed error messages
Changelog
All notable changes to RougeChain.
Node release 1.6.11: testnet faucet fix; shielded pool V2 on testnet at block 1480 — 2026-10-20
- Testnet: every node now applies faucet payouts the same way (a fixed testnet mint-authority key replaces the local node key), so non-producing testnet nodes follow the chain. Testnet nodes must run 1.6.11 before testnet block 1480, where SHIELD_V2 activates. Mainnet consensus unchanged.
- Includes everything planned for 1.6.10 (installer 2.1.1 with Ubuntu 26.04).
- See the release notes.
Node release 1.6.10 (not published) — 2026-10-19
- Testnet: SHIELD_V2 activates at block 1480 (testnet nodes must upgrade before it). Mainnet consensus unchanged (SHIELD_V2 not scheduled there).
- Installer 2.1.1: Ubuntu 26.04 supported.
- See the release notes.
Node release 1.6.9: shielded pool V2 submit route — 2026-10-19
- Adds
POST /api/shield-v2/submitfor shielded pool V2 transactions only, forwarded between nodes. Inert until the pool's activation height is set (none yet). No consensus change. - See the release notes.
Node release 1.6.8: NFT mint stability fix — 2026-10-15
- Fixes a bug that could make a node stop producing and importing blocks while processing certain public NFT mints. Recommended for every node; no consensus change.
- See the release notes.
Node release 1.6.7: AMM_INTEGRITY on mainnet at block 260 — 2026-10-10
- AMM_INTEGRITY activates on mainnet at height 260 (active on testnet since 1440). Mainnet nodes must run 1.6.7 before that block. On testnet the release behaves as 1.6.6.
- See the release notes.
Node release 1.6.6: AMM_INTEGRITY on testnet at block 1440 — 2026-10-09
- AMM_INTEGRITY activates on testnet at height 1440; mainnet stays unscheduled. Testnet nodes must run 1.6.6 before that block. On mainnet the release behaves as 1.6.5.
- See the release notes.
Node release 1.6.5: AMM_INTEGRITY included, not scheduled — 2026-10-09
- The node carries the consensus rule AMM_INTEGRITY with no activation height on any network
(
upgrade_schedule.amm_integrityisnull). From its height, pool transactions (create_pool,add_liquidity,remove_liquidity,swap) apply all-or-nothing: one that cannot take effect changes nothing, pays no fee and does not make the block invalid; swap routes are validated; a pair has one pool; amounts use checked integer arithmetic; the fee of a token-to-XRGE swap is counted as collected. - Until the height is set, nodes on 1.6.5 validate and produce exactly as on 1.6.4.
- See the upgrade schedule and the release notes.
Node release 1.6.4: signatures commit to the network — 2026-10-08
- Every payload a wallet signs for a node now names the network it is for:
chainId(the exact chain id, e.g.rougechain-mainnet-1) inside the signed bytes of/api/v2transactions and of signed mail, messenger, name and vote requests;chain_idinside arougechainCLI envelope. Wallets take it from their network setting, check it once per session against the node's/api/healthchain_id, and refuse to sign when they differ. - Node 1.6.4 (no fork): a signed payload naming another chain id is refused with
CHAIN_ID_MISMATCHat the API, mempool and producer. New operator flagREQUIRE_SIGNED_CHAIN_ID(--require-signed-chain-id/QV_REQUIRE_SIGNED_CHAIN_ID, default off) also refuses payloads naming none (CHAIN_ID_REQUIRED); it is turned on once wallets have shipped. - Consensus (not scheduled): CHAIN_ID_BINDING — from a height to be announced, every
transaction's signed bytes must commit to the chain id (V1-format, node-signed transactions use a
new network-bound signing encoding from that height). CONTRACT_CHAIN_ID — contracts can read the
chain id with
host_get_chain_id. Both areNoneon every network; history replays identically. - Clients: site (rougechain.io),
@rougechain/sdk1.15.0, browser extension 1.9.0 (approval screen shows the network and refuses another one),rougechainCLI 1.3.0 (--chain-id), Qwalla 1.3.0 (patch). Sign-in verification (verifySignIn) already required the chain id; the docs now say verifiers must check it. - Node 1.6.4, also: every item of
POST /api/v2/batch-submitgets the checks of a single/api/v2submission (5-minute timestamp window, the shared signature replay guard, the network rule,from= signer, signedaccount_nonce); a batch with any failing item is refused as a whole and uses up no signature.GET /api/balance/:publicKey/XRGEreturns the native balance, and both balance routes return exact integer fields (balance_raw+decimals,balance_quanta+token_balances_raw). The block producer leaves out transactions that would fail and refuses malformed liquidity and swap requests (AMM_REQUEST_REFUSED; node rules, block validity unchanged). The shielded pool V2 code is present and inactive.quantum-vault-daemon --versionreports1.6.4. - See Security and the release notes.
Wallet message signing (signMessage) for logins and token gating — 2026-10-06
- New provider method
window.rougechain.signMessage({ message })→{ signature, publicKey, address }in the browser extension 1.8.0 (built from source; not yet in the Chrome Web Store at the time of this entry). Qwalla gets it with its next update. Sites must feature-detect. - The signature is ML-DSA-65 over
"\x19RougeChain Signed Message:\n" + byte length + "\n" + message. A node only verifies transaction signatures over JSON documents, so a signed message can never be a transaction, and the reverse. @rougechain/sdk1.13.0 (in the repository; not yet on npm) addssignMessage,verifyMessage,createSignInMessage,parseSignInMessageandverifySignIn.- New page: Wallet Authentication, with the exact formats and a
complete token-gating server. No node change;
signTransactionis unchanged.
Mainnet: blocks 235 and 245 active; documentation brought up to date — 2026-10-06
- Mainnet passed block 235 (mintable tokens, contract royalty reads) and block 245
(monetary-integrity rule) in early October 2026; both operator nodes run release 1.6.3 and report
upgrade_schedule235 / 235 / 245. The height-245 rule is in force: a block carrying aslashtransaction, a faucet-flagged transaction, an invalid fee or one of the fifteen suspended types is invalid. Shielded transfers, token locking, token staking pools, on-chain governance and vote delegation are therefore not available — see Status for why and what replaces them (shielded pool V2 in development, unaudited). - Documentation revised to match the running chain:
WHITEPAPER.md(repository root) v2.4 (upgrade table with 235 and 245; STARK and shielded-pool corrections; staking, fee, bridge and messenger facts; the consensus redesign decided 2026-10-06 recorded as planned, not built), Status, staking, bridge, messaging and API pages. Live figures (3 validators, base fee at the 0.001 floor,__staking_rewards__= 0,__treasury__≈ 52 XRGE) were read from the node API on 2026-10-06. The whitepaper PDF shipped with the web apps still reflects v2.3. - No code change.
Node release 1.6.3: messenger and mail hardening — 2026-10-05
- Not a consensus change (no activation height). A messenger directory entry can only be replaced by the wallet that owns it (signing key); an encryption key registered to another wallet is refused; mail names and folders follow the owning wallet only.
- The unauthenticated legacy read routes for mail and messenger return
410 Gone; clients use the signed/api/v2routes. - The shielded API routes refuse a suspended transaction type before processing request data.
- Release notes.
Node release 1.6.2: monetary-integrity rule — mainnet 245, testnet 1390 — 2026-10-04
- Consensus upgrade (
upgrade_schedule.monetary_integrity). From its height a block is invalid if it carries a transaction with a fee that is not a finite number ≥ 0, aslashtransaction, a faucet-flagged transaction on a network without a faucet, or a transaction of a suspended type (shielded, token lock/stake, governance, delegation and the legacytoken_approve/token_transfer_fromtypes; allowances throughapprove/transfer_fromare unaffected). The suspended features are off until a later release restores them. Below the height nothing changes; mainnet history replays identically. - No activation height: stricter verification on the manual bridge-deposit claim route; the gRPC faucet honours the faucet switch; invalid fees are refused at the mempool and by the producer.
- Active on testnet since block 1390 (2026-10-04): transfers and the faucet finalize as before; a
suspended type is refused with
this transaction type is suspended. - Mandatory: upgrade notice.
Node: contracts can read NFT royalties (CONTRACT_NFT_ROYALTY) — built, activation not scheduled — 2026-10-01
- Consensus upgrade, not active on any network (
upgrade_schedule.contract_nft_royaltyisnull). From its height contracts get two read-only host functions:host_nft_royalty_bps(col, clen) → i32(0–10000,-1not found) andhost_nft_royalty_recipient(col, clen, out, cap) → i32(length,-1not found,-2buffer too small). The recipient is the canonical ledger key the wallet royalty path credits, so a contract that sells NFTs can pay royalty withhost_transfer. Planned to activate at the same height as TOKEN_MINTING. - Before activation nothing changes: a contract importing them fails exactly like one importing an unknown function. No new transaction fields, no state-root change; mainnet replay 0–137 identical.
- Same activation:
nft_create_collectionwithroyaltyBpsabove10000(over 100%) or not an exact integer (negative, fractional, a string, or wider than 16 bits — previously truncated) is rejected at creation: the API returns400 royaltyBps must be an integer between 0 and 10000, the mempool and producer refuse it, and a block carrying one is invalid. Before activation creation behaves exactly as before (history replays unchanged). - New example:
contracts/nft_marketplace— an escrow marketplace (list → escrow → buy with an attached XRGE payment → royalty + seller paid in integer quanta → NFT to the buyer; cancel).
Node: mintable tokens (TOKEN_MINTING) — built, activation not scheduled — 2026-10-01
- Consensus upgrade, not active on any network (
upgrade_schedule.token_mintingisnull). From its height a token can be created withmintable: trueand an optional integermax_supply(≥initial_supply; cap on total issuance), and its creator can mint more through/api/v2/token/mint. Block apply enforces creator-only, mintable-at-creation and the cap, and advancestotal_minteddeterministically; once a mintable token exists the state root commits the mint ledger. - Before activation nothing changes on-chain. The API now refuses
mintable/max_supplyon create (they used to be silently ignored, giving a fixed-supply token) and refuses mint requests, instead of accepting a mint that every block then dropped. - History is unaffected: the new transaction fields are omitted when unset, so every existing transaction and block encodes and hashes exactly as before (pinned by tests, mainnet replay 0–137).
Web wallet: messaging key derived from the recovery phrase — 2026-10-01
- New wallets created on rougechain.io, and wallets imported from a recovery phrase (or private
key), now get an ML-KEM-768 messaging/mail key derived from the phrase, exactly as Qwalla
does:
ml_kem768.keygen(SHA-512("<phrase>|rougee-gram|kem-v1"))(signing private key hex in place of the phrase when there is none). Restoring from the 24-word phrase on the website or in Qwalla now gives the same messaging key, so old messages and mail stay readable. - Existing wallets keep their current key — nothing stored is re-derived. Wallets created on the
website before this change have a random key: keep their
.pqcbackupto read old messages. Importing such a wallet from its phrase gives it the derived key (registered on next messenger / mail visit), as a phrase import already gave it a new key before. - A
.pqcbackupthat contains encryption keys keeps them exactly; one without gets the derived pair.
SDK 1.11.0: NFT batch-mint attributes, create-token fee — 2026-10-01
- Batch mints now sign per-NFT attributes as
attributes, the field the node reads (SDK, site and core client). Earlier clients sentbatchAttributes, which the node ignored, so batch-minted NFTs got no attributes. Requires@rougechain/sdk1.11.0; no node change. - The SDK / core
create_tokenbuilders now default the signed fee to 100 XRGE, what the node charges (it was 10).
Web wallet: keys never stored unencrypted — 2026-10-01
- Security fix (rougechain.io). A new or imported wallet was saved to
localStorageunencrypted before the password step, and stayed there if you left onboarding (the password was optional). Now the wallet is held only in the tab'ssessionStorageuntil you set its password, which is required (min 8 characters, also during onboarding); only the AES-256-GCM vault is persisted. - Existing wallets that an older version stored unencrypted keep working, but the site asks you to set a password before using them (with a backup option); the plaintext copy is then deleted.
Payable contract calls (block 190) — 2026-09-28
- A contract call can carry a signed payment:
attach: {"symbol": "XRGE" | TOKEN, "amount": N}(integer: quanta for XRGE, raw units for tokens). It moves to the contract only if the call succeeds; a contract refuses a payment by failing the call. Contracts read it withhost_get_attached_amount/host_get_attached_symbol. Enables entry fees and shops. Mandatory node upgrade before block 190. loot_roll'srollnow costs 0.5 XRGE, so its treasury can't be drained by free rolls.- SDK 1.10.0 (
attach,xrgeToQuanta), extension 1.5.0 (approval shows the payment), MCP server 1.3.0, site contract page "Attach payment".
Node fix: exact JSON number parsing — 2026-09-28
- Contract-call fees (
gasLimit × 0.000001) could print with more digits than the previous JSON parser read back exactly, so peers rejected mainnet block 177 while the producer accepted it. Nodes now parse numbers exactly (binary718200bc…); every node must run it to follow the chain past block 176.
Grind-proof contract randomness (block 170) — 2026-09-28
- Security fix. One-step
host_randomrolls could be ground: the seed is fixed by the parent block (public) and the transaction (chosen by the sender), so a player could sign many variants offline and send only a winner. The earlier docs wrongly implied this was safe. No contracts were deployed. host_block_hash(height)from block 170 (GAME_READY 3): the hash of a finished block up to 256 back. Games commit, then settle from the hash of the next block.contracts/loot_rollnow usesroll+settle. Mandatory node upgrade before block 170 — see the upgrade guide.
Finality, game-ready contracts and LP fee collection — 2026-09-28
Node release dbe0fc0 (binary sha256 46e29456…), mandatory:
upgrade guide.
- Verified BFT finality (block 150). Every block from 151 carries its parent's ≥⅔-stake precommit certificate; nodes reject blocks without one. See Finality.
- Player-signed contracts (block 150). Deploy (
/api/v2/contract/publish, 10 XRGE) and call (/api/v2/contract/execute,gasLimit × 0.000001XRGE) are signed by the player, who is the caller the contract sees. Contracts read their arguments. - Game-ready contracts (block 160). Contracts hold and send custom tokens and NFTs, create their
own collection and mint to players, roll
host_random; moves inside cross-contract calls apply; the state root covers NFTs and contract code and storage. Example:contracts/loot_roll. - Contract API. Free query endpoint, events paging and WebSocket
contract:<addr>events,Failedreceipts for reverted calls. SDK 1.9.0rc.contracts, extension 1.4.0 contract signing, MCP server 1.2.0. - LP fee collection. Pools shows each position's uncollected fees; Collect fees withdraws
exactly the fee growth and leaves the deposit (node ledger
GET /api/pool/:id/earnings/:owner). - Explorer. Swap amounts are labelled with the input token (1,000 QTEK no longer shows as XRGE).
Messenger — deterministic threads and recoverable delete — 2026-09-25
Node release 59478e6 (binary sha256 9a93d68b…, non-consensus; installed on the primary 2026-09-25).
- 1:1 conversations now have a deterministic id (
dm_…, derived from the two participants' canonical wallet ids), and creating one that already exists returns it withexisting: trueinstead of a duplicate thread. Groups are unchanged. - Deleting a conversation is now per participant and recoverable: it moves to your trash for 30 days (
conversations/listwithfolder: "trash"),conversations/restorebrings it back,purge: truewaives your window; the thread is removed for good only once every participant has deleted it. Message delete works the same way for the sender (messages/restore). - Details: Messenger API. Addresses public issue rougechain-node #73.
Docs status sync — 2026-09-21
- Added Status & Roadmap, Security Overview, Finality, Bridge Security Model, V3 Post-Quantum XRGE Bridge and Authority Rotation.
- Clarified what is live (mainnet, hardened R1 bridge for XRGE / qETH / qUSDC) versus built but not activated (FINALITY_V2, V3 XRGE bridge with ML-DSA-65 authorization).
- Corrected stale claims: Base Sepolia → Base mainnet, qUSDC "planned" → live, auto-refund → disabled in production, generic BFT finality wording.
Security Hardening — 2026-08-19
Security Hardening
- On-chain bridge verification — The bridge now verifies the actual on-chain Base deposit instead of trusting caller-supplied messages. An XRGE claim requires an EVM signature and derives the credited amount from the on-chain
Transferlog. The qETH EntryPoint/balance-delta claim path was removed in favor of a direct-to-custody, recipient-bound ECDSA claim only - Signature replay guard — Signed transactions are now protected by a process-global replay guard keyed by
sha256(signature)with a 5-minute window, closing the in-window replay hole left by the timestamp check alone - Bridge confirmation depth & limits — Default bridge confirmation depth raised to 6; added a withdraw kill-switch and a per-transaction withdrawal cap
- Token freeze authorization — Token freeze now cryptographically verifies the request signature and requires
signer == creator - Wallet keys encrypted at rest — Wallet keys are encrypted with AES-256-GCM (PBKDF2, 600k iterations) behind a mandatory password (min 8 chars); the decrypted key is kept only in memory and never written to disk, and legacy plaintext wallets are force-migrated
Testnet v0.2.4 — March 2026
Added
- Social layer — Node-hosted social features (not on-chain) with plays, likes, comments, follows, and tips. Data is stored server-side in sled with ML-DSA-65 signed writes; tips settle on-chain via
rc.transfer() - Standalone posts — Create, delete, and fetch posts (max 4000 chars) with threaded replies via
replyToId. Global timeline and personalized following feed endpoints - Reposts — Toggle repost on any post; repost counts aggregated per post with viewer state
- Post stats — Aggregate endpoint returns likes, reposts, reply count, and viewer's liked/reposted state for any post
- Following feed — Authenticated endpoint returns posts from users the viewer follows, sorted newest-first
- qRougee social integration — TrackDetail shows play counts, like button, tip modal, and comments. TrackCard badges show plays/likes. ArtistProfile shows followers and follow button. Library includes a "Liked" tab. Home page sorts discovery by popularity
- SDK v1.0.0 —
rc.socialnamespace with 19 methods:createPost,deletePost,toggleRepost,getPost,getPostStats,getPostReplies,getUserPosts,getGlobalTimeline,getFollowingFeed, plus existing play/like/comment/follow methods - WASM STARK prover — Browser-side STARK proof generation via
core/wasm-prover/compiled to WebAssembly. Unshield and shielded transfer operations now generate real winterfell STARK proofs client-side without relying on a trusted server - Groq-powered Quantum Bot — Messenger AI bot proxied through the node using Groq's
llama-3.1-8b-instantmodel with a comprehensive RougeChain knowledge base - Mail & messenger unread badges — Browser extension and QWalla app show unread count badges on both Chat and Mail tabs with hover tooltips (e.g. "3 unread emails"). The browser extension icon badge displays the combined unread total
- Native browser notifications — Browser extension fires system notifications for new messages, new mail, received/sent tokens, contract deployments, staking events, and balance changes via WebSocket and periodic polling
- Mail reply pre-fill — Clicking "Reply" in the browser extension auto-populates the recipient, subject (with "Re:"), and quoted original message body
- Mail attachments (extension) — Browser extension mail compose and read views now support file attachments with upload, preview, and download, matching the website's feature set
- Initial unread count polling (QWalla) — App fetches actual unread chat and mail counts from the server on launch so tab badges are accurate immediately, not just after a real-time event arrives
Fixed
- Custom token transfers —
v2_transferhandler now correctly setstoken_symbolonTxPayload(previously only settoken_name), which the balance engine actually reads. Without this, all custom token transfers silently moved XRGE instead of the selected token - Browser extension token send — Added token picker dropdown to the send form so users can send any held token, not just XRGE. Upgraded from legacy v1
/tx/submit(which sent the private key to the server) to v2 signed endpoint with client-side ML-DSA-65 signing - Browser extension import token — Added MetaMask-style "Import Custom Token" flow: enter a symbol, validates on-chain, persists to tracked list. Replaced the "All Tokens on Chain" dump with a curated held + imported view
- Qwalla custom token balance validation — Send screen now fetches and validates the correct token balance when sending a custom token, instead of always checking XRGE balance
- Website send dialog pre-selection — Clicking "Send" from a token's detail view now pre-selects that token instead of always defaulting to XRGE
- Swap pre-fill from Trade button — "Trade on this Pool" now passes
tokenIn/tokenOutURL params to the swap page, correctly pre-filling both sides of the swap - PWA wallet persistence — Wallet private keys now persist in
localStoragewhen no vault password is set, preventing wallet loss on PWA/tab restart. Password-protected vaults continue to use encrypted storage only - Browser extension key regeneration — Removed aggressive version-based key regeneration that was replacing existing valid keys and causing loss of on-chain identity (faucet funds)
- Quantum Bot registration — Bot wallets use unique per-browser IDs and register as non-discoverable to prevent display name conflicts
- Browser extension messenger — All 8 messenger endpoints migrated from legacy unsigned v1 to ML-DSA-65 signed v2 endpoints, fixing "Registration failed" errors in production
- Browser extension mail decryption — Ported v2 multi-recipient CEK encryption/decryption to the extension, fixing "[Unable to decrypt]" on mail sent from the website
- QWalla mail encryption — Added v2 CEK encryption/decryption for cross-client mail compatibility with the website and browser extension
- QWalla message signing — Messages now include ML-DSA-65 content signatures; fixed
ml_dsa65.signargument order that caused "secretKey expected Uint8Array of length 4032" errors - Message signature display — Three-state indicator: green check (verified), red X (failed), grey shield (no signature) — prevents false negatives on unsigned legacy messages
- Shielded note badge — Browser extension wallet tab now shows shielded notes for the current wallet only (per-wallet
getActiveNotes) instead of the global chain stat - Unread badge persistence — Badge clears correctly after viewing messages;
lastKnownUnreadpersisted tochrome.storage.localto survive service worker restarts - QWalla
@qwalla.maildomain — Mail addresses now display as@qwalla.mailthroughout the app instead of@rouge.quant - QWalla mail name resolution — Mail list and detail views resolve and display registered names instead of raw wallet IDs
Changed
- Validator economics — Base fee burn reduced from 100% to 50%; remaining 50% flows into the tip pool for validator rewards. A 0.1 XRGE/block minimum tip floor is guaranteed from the staking reserves allocation
- Minimum stake — Enforced at 10,000 XRGE (previously unenforced). Staking requests below this threshold are rejected
- Entropy prefetch — ANU QRNG entropy is now fetched in a background thread and cached, eliminating per-block blocking HTTP calls that could stall block production for up to 20 seconds
- Service worker cache bumped to
rougechain-v2to invalidate stale assets on existing PWA installs - Session-only private keys policy updated:
localStorageused for unprotected wallets,sessionStorage-only when vault passphrase is configured
Testnet v0.2.3 — March 2026
Security Hardening
- Signed API requests — All 16 mail, messenger, and name registry endpoints now require ML-DSA-65 signed requests via
/api/v2/routes. Legacy unsigned endpoints return HTTP 410 (Gone) - Anti-replay nonces — Each signed request includes a cryptographically random nonce; duplicates within the timestamp window are rejected server-side
- Multi-recipient CEK encryption — Mail content encrypted once with a random AES-256 CEK, KEM-wrapped individually per recipient via ML-KEM-768
- Unified mail signatures — Single ML-DSA-65 signature over concatenation of all encrypted parts (subject + body + attachment) prevents partial content substitution
- TOFU key verification — Public key fingerprints (SHA-256) tracked on first use with key-change warnings displayed in messenger UI
- Atomic name registration — Name registry uses sled compare-and-swap (CAS) to prevent TOCTOU race conditions during name claims
- Sled messenger storage — Messenger data migrated from JSON file to sled embedded database with per-record atomic operations and automatic migration from legacy format
- Server-side input validation — Length limits enforced on all fields (display names: 50 chars, message content: 2 MB, mail subject: 10 KB, mail body: 512 KB, attachments: 3 MB, max 50 recipients)
- Session-only private keys — Web app stores private keys in
sessionStorage(cleared on tab close) instead oflocalStorage; encrypted wallet blob persists inlocalStorage(superseded in v0.2.4: localStorage used when no vault password is set for PWA persistence) - Legacy decryption removal — Pre-v2 mail and messenger decryption fallbacks removed to reduce attack surface
Changed
- Messenger, mail, and name registry SDK methods now require a
walletparameter for request signing WHITEPAPER.mdupdated to v1.7 with full security hardening documentation
Testnet v0.2.2 — March 2026
Added
- SDK v0.8.4 — Name registry methods:
rc.mail.registerName(),rc.mail.resolveName(),rc.mail.reverseLookup(),rc.mail.releaseName() - SDK types —
NameEntry,ResolvedNameexported for TypeScript consumers - Browser Extension — BIP-39 seed phrase support: generate, view, and import 24-word mnemonic phrases
Fixed
- API docs corrected: name registry endpoints now show actual routes (
/names/resolve/:name,/names/reverse/:walletId) instead of non-existent query-param URLs - Blockchain explorer chain validation no longer fails on descending block order from API
- Tamper detection demo works correctly with real blocks from the API
- SDK
SwapQuoteParamsnow includes requiredtokenOutfield
Testnet v0.2.1 — March 2026
Added
- Mail Attachments — Encrypted file attachments (up to 2 MB) via ML-KEM-768
- Push Notifications — PQC-signed Expo push token registration (
/api/push/register) - Address Resolution — Convert between
rouge1…and hex via/api/resolve/:input - Account Nonce API —
GET /api/account/:pubkey/noncefor replay protection - SDK v0.8.2 —
registerPushToken(),unregisterPushToken(),resolveAddress(),getNonce()
Fixed
- Auto-migration of stale timestamp-based nonces to sequential nonces on node startup
Testnet v0.2.0 — March 2026
Added
- PQC Mail — Encrypted email with
@rouge.quantaddresses, threading, and folder management - RC-721 NFTs — Collections, batch minting, royalties, transferring, burning, and freezing
- AMM/DEX — Uniswap V2-style liquidity pools, swaps, and price charts
- XRGE Bridge — Bidirectional bridge for XRGE between RougeChain and Base (ERC-20)
- Shielded Transactions — Private transfers using STARK proofs with on-chain nullifiers
- Token Staking Pools — Stake custom tokens with configurable reward rates
- Governance — On-chain proposal creation and weighted voting
- Token Locking — Time-locked and vesting token locks
- Token Allowances — Approve and spend-from delegation
- ZK Rollup (Phase 3) — Batch transaction accumulation with proof submission
- Tiered Rate Limiting — Validators and peers get separate rate limit tiers
- Network Globe — 3D visualization of connected nodes on the blockchain page
- SDK —
@rougechain/sdknpm package for building dApps - Docker Support — One-command node deployment with
docker run - Node Dashboard — Built-in web dashboard at
http://localhost:5100when running a node - Name Registry — Register human-readable names for wallets (
alice@rouge.quant) - Browser Extensions — Chrome/Firefox wallet extensions with vault lock
Changed
- Default block time reduced from 1000ms to 400ms
--peersURL now requires/apisuffix (e.g.,https://testnet.rougechain.io/api)- v1 endpoints that accept private keys are disabled by default (use
--devto enable) - Secure v2 API with client-side signing is the default for all write operations
Security
- All signatures use ML-DSA-65 (FIPS 204)
- All key encapsulation uses ML-KEM-768 (FIPS 203)
- Client-side signing — private keys never leave your browser
- Validator-proven rate limiting with PQC signature verification
Testnet v0.1.0 — February 2026
Added
- Core blockchain — Proof of Stake L1 with PQC cryptography
- Wallet — Generate ML-DSA-65 keypairs, send/receive XRGE
- Faucet — Request Testnet tokens
- Validator staking — Stake XRGE to become a block proposer
- Encrypted Messenger — E2E encrypted messaging with ML-KEM-768 and AES-GCM
- Self-destruct messages — Messages that auto-delete after being read
- Token creation — Create custom tokens with metadata
- Token burning — Official burn address with on-chain tracking
- P2P networking — Peer discovery, block propagation, and automatic sync
- ETH Bridge (qETH) — Bridge ETH from Base Sepolia with 6-decimal precision
- Block Explorer — Browse blocks, transactions, and addresses
- gRPC API — Chain, wallet, validator, and messenger services
- REST API — Full HTTP API for all blockchain operations