RougeChain Documentation

Welcome to RougeChain — a post-quantum secure Layer 1 blockchain built with real NIST-approved cryptography.

Current Status

ComponentStatus
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 readsLIVE since block 235
Monetary-integrity rule; shielded / token-lock / token-staking / governance / delegation types suspendedLIVE since block 245 — see Status
Current node release1.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 bridgeDeposits LIVE; withdrawals switched off in the site
V3 XRGE bridgeAUDIT 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

FeatureDescription
Post-Quantum L1Accounts, transactions and block signatures use quantum-resistant ML-DSA-65
Client-Side SigningPrivate keys never leave your browser
AMM/DEXUniswap V2-style liquidity pools and token swaps
Token BurningOfficial burn address with on-chain tracking
Proof of StakeValidator staking; today one designated proposer (largest stake) produces every block
P2P NetworkDecentralized 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 TokensCreate your own tokens on the network
RC-721 NFTsNFT collections with royalties, batch minting, and freezing
Encrypted MessengerE2E encrypted messaging with PQC, media support, self-destruct
PQC MailEncrypted email with @rouge.quant addresses and threading
Browser ExtensionsChrome/Firefox wallet extensions with vault lock
PWA SupportInstallable progressive web app for mobile and desktop
Social LayerPosts, timeline, reposts, likes, follows, comments, tips
CLI WalletCommand-line wallet with full chain access and social commands
SDK@rougechain/sdk npm package for building dApps
EIP-1559 Dynamic FeesBase fee auto-adjusts per block, fee burning for deflationary pressure
Token Mint AuthorityCreator-only minting for tokens created mintable since block 235, with an optional supply cap
Validator UnbondingUnbonding 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
FinalityVerified BFT finality (FINALITY_V2) is live since block 150: every block carries its parent's ≥⅔-stake certificate — see Finality
WebSocket SubscriptionsTopic-based real-time event streaming (blocks, txs, accounts, tokens)
HD Wallet DerivationBIP-44-like PQC key derivation from master seed (HMAC-SHA256)
Open SourceApache 2.0 licensed node software

Network Info

Mainnet is live (chain-id rougechain-mainnet-1) alongside the public testnet.

NetworkChain IDAPI Endpoint
Mainnetrougechain-mainnet-1https://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-port is 5101.

Tokens

XRGE

XRGE is the native token that powers transactions, secures the network through staking, and fuels the post-quantum economy.

RoleHow it works
Gas TokenEvery 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 PrimitiveValidators 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 PairAMM liquidity pools trade against XRGE. It's the default quote currency on the built-in DEX.
Bridge AssetXRGE 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.

PropertyValue
Decimals6
Bridge SourceBase mainnet (chain id 8453)
Bridge ContractRougeBridge 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

ActionFee
Transfer1 XRGE (fixed for wallet/API-signed transfers)
Token Creation100 XRGE
Pool Creation10 XRGE
Swap0.3% of input (to LPs) + 1 XRGE
Minimum Stake10,000 XRGE (enforced by the node API, not by consensus)
Unbonding Period500 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:

  1. Your wallet creates a transaction payload
  2. The payload is signed locally using ML-DSA-65
  3. Only the signature and public key are sent to the server
  4. 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_from stay 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_root from curl -s localhost:5100/api/stats equals the one at https://api.rougechain.io/api/stats at the same height

Live today

ComponentStatusNotes
RougeChain mainnet (rougechain-mainnet-1)LIVEML-DSA-65 accounts, transactions and block signatures
R1 production bridge (Base mainnet ⇄ RougeChain)LIVEHardened R1 architecture; see Bridge Security Model
XRGE bridge (BridgeVaultV2)TESTED / USABLEDeposit, withdrawal and payout paths tested end-to-end on mainnet
qETH bridge (RougeBridge)TESTED / USABLESame
qUSDC bridge (RougeBridge)TESTED / USABLESame
Verified BFT finality (FINALITY_V2, Release 2a)LIVE since block 150Every block carries its parent's ≥⅔-stake certificate; see Finality
Player-signed contracts (GAME_READY)LIVE since block 150Deploy and call signed by the player; see Smart Contracts
Game-ready contracts (GAME_READY 2)LIVE since block 160Tokens, 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 170Commit-then-settle randomness; one-step host_random rolls can be ground by the sender
Payable contract calls (attach)LIVE since block 190Pay XRGE or tokens with a contract call; moves only if the call succeeds
Mintable custom tokens (creator-only minting, optional cap)LIVE since block 235Tokens created before 235 stay fixed-supply; see Token creation
Contracts read NFT royalties (host_nft_royalty_bps / host_nft_royalty_recipient)LIVE since block 235Royalties above 100 % refused at collection creation; nft_marketplace example
Monetary-integrity ruleLIVE since block 245Fee range, no slash, no faucet on mainnet, suspended types above
Pool transactions all-or-nothing (AMM_INTEGRITY)LIVE since block 260A pool transaction that cannot take effect changes nothing and pays no fee; one pool per pair; checked integer amounts
LP fee collectionLIVE"Collect fees" on Pools withdraws only fee earnings
ETH / USDC deposits through the bridge contract (depositETH / depositERC20, auto-claimed)LIVE — reopened 2026-10-05The manual claim route is closed at the public endpoint
Bitcoin deposits (BTC → qBTC)LIVEBitcoin 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

FactToday
Staked validators3 — 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 productionOne validator produces every block (designated proposer = largest stake, since block 100); no fallback proposer — if it is offline the chain stops
FinalityFINALITY_V2 since block 150; the largest validator's own vote reaches the ⅔ quorum
Minimum stake10,000 XRGE is enforced by the node API only (/api/v2/stake, CLI). Consensus accepts any positive stake — hence a 9,000 XRGE validator
SlashingNone active. Missed-block accounting frozen since block 100; the legacy slash transaction is rejected from block 245; evidence-based slashing does not exist yet
Unbonding500 blocks (a block count, not a duration)
RewardsFees 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

ComponentStatus
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 architectureBuilt, tested, frozen for audit — not deployed
Deterministic epoch / withdrawal-root pipelineBuilt and tested — not running in production
Post-quantum root-authority signerBuilt 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 scheduleBuilt and tested, rehearsed end to end on a throwaway network — not in use
Reproducible V3 contract build / audit candidateComplete
OP-stack deployment rehearsal (local devnet, throwaway keys and token)Complete
Shielded pool V2Specified, 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.

ElementPlanned
RoundsTendermint-style rounds with ML-DSA-65 prevotes and precommits
ProposerRotating, stake-weighted, in place of the fixed largest-stake proposer
Active setCapped at 32 validators
Minimum stake100,000 XRGE enforced by consensus, with a 30-day grace period for existing validators
Unbonding21 days
SlashingEvidence-based: double-signing 100 % of stake; downtime jail only
LivenessHourly heartbeat blocks so the chain advances without transactions
RewardsA per-time reserve subsidy in addition to fees
AdmissionTemporary approval of new validators, with a written exit condition
LaterValidator 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 RougeBridge with classical Base-side authorization.
  • The Bitcoin bridge is a separate system and is not part of V3. See Bitcoin Bridge.

Roadmap

MilestoneState
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 on REQUIRE_SIGNED_CHAIN_ID to 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 ID line; verifiers must check it (Wallet Authentication).
  • Contracts that verify signatures themselves can read the chain id with host_get_chain_id from the CONTRACT_CHAIN_ID upgrade (not scheduled) — see Smart Contracts.

Bridges — mixed

PathToday
RougeChain side of every bridgeML-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

  1. Visit rougechain.io
  2. Click "Wallet" in the sidebar
  3. 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 .pqcbackup as well (see Create a Wallet)
  4. Use the faucet to get free testnet XRGE

That's it! You now have a quantum-secure wallet.

What's Next?

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

  1. Click Wallet in the sidebar
  2. Click Request from Faucet
  3. Receive 10,000 XRGE instantly (24-hour cooldown per address)

Step 4: Send Your First Transaction

  1. Click Send
  2. Enter recipient address
  3. Enter amount
  4. Click Send XRGE

Transaction is signed with your ML-DSA-65 key and broadcast to the network.

Step 5: View on Blockchain

  1. Click Blockchain in sidebar
  2. See your transaction in the latest block
  3. Verify the PQC signature

What's Next?

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 / localStorage unless 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

ComponentAlgorithmPurpose
Signing KeyML-DSA-65Sign transactions, prove ownership
Encryption KeyML-KEM-768Encrypt/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 rouge1 address 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

PlatformSteps
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

  1. Go to Wallet page (or Settings in the extension)
  2. Click Restore or Import Wallet
  3. Select your .pqcbackup file
  4. Enter the password you used when exporting
  5. 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):

  1. Go to Settings → Reveal Seed Phrase
  2. Write down all 24 words in order
  3. 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 .pqcbackup file, which contains both keys, to read earlier messages and mail. The browser extension still generates a random encryption key, so keep a .pqcbackup for extension wallets too.

.pqcbackup File Format

The encrypted backup file uses industry-standard cryptography:

ComponentAlgorithmDetails
Key DerivationPBKDF2-SHA256600,000 iterations
EncryptionAES-256-GCM256-bit key, 96-bit IV
SaltRandom16 bytes per export
IVRandom12 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

  1. Your password is stretched with PBKDF2-SHA256 (600,000 rounds) using a random salt
  2. The derived 256-bit key encrypts your wallet data with AES-256-GCM
  3. The salt, IV, and ciphertext are bundled into a .pqcbackup JSON file
  4. 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

  1. Never share your private key or seed phrase
  2. Backup your wallet immediately after creation
  3. Use a strong, unique password for your .pqcbackup file
  4. Store backups in multiple locations (password manager, USB, etc.)
  5. Never screenshot your seed phrase or keys
  6. 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.

PlatformStorage
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

  1. Go to the Wallet page
  2. Click Request from Faucet
  3. 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 recipientPublicKey and must be a raw hex public key, not a rouge1… address. The faucet is OFF by default and must be explicitly enabled with --faucet-enabled (or QV_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

ConditionLimit
Per address1 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

  1. Check the block explorer for your tx
  2. Verify you're on the correct network
  3. 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

TypeWhat it doesKey flags
Full nodeSyncs and validates all blocks from peers--peers
Mining / validator nodeVotes on blocks once staked (≥ 10,000 XRGE); proposes only while it is the designated proposer (the validator with the most stake)--mine
Public nodeServes 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

MinimumRecommended (public / validator)
CPU1 vCPU2+ vCPU
RAM1 GB2–4 GB
Disk5 GB SSD20+ GB SSD (chain data grows over time; SSD matters for the embedded sled DB)
NetworkAny stable broadband≥ 100 Mbps, static IP or a domain with TLS
OSLinux (Ubuntu 22.04+), Windows, or DockerLinux

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
RustRecent stable toolchain (1.80+); the project builds on 1.94
RAM during compile~4 GB
Free disk for target/~6 GB
Toolsgit, 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:

  1. Bind and advertise — run with the default --host 127.0.0.1 and --api-port 5100, and set --public-url https://your-domain/api so peers can reach you through the proxy. Peers use the same REST API; there is no separate P2P port.
  2. 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.
  3. API keys (private nodes only) — --api-keys "<key1>,<key2>" makes every route that is not exempt return 401 unless the request sends X-API-Key or Authorization: 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.
  4. Rate-limit — e.g. --rate-limit-read-per-minute 600 --rate-limit-write-per-minute 60. All limits default to 0 (unlimited); --rate-limit-per-minute is 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 by X-Real-IP / the rightmost X-Forwarded-For entry.
  5. Run under systemd — so it restarts on crash/reboot. Example unit and hardening in Public Node and Configuration.
  6. Monitor — poll /api/health (liveness) and /api/stats (height, peers, mining) from your uptime checker.

Security note: legacy v1 write endpoints that accept a raw private key are development-only (enabled with --dev) and return 410 Gone in 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

System Requirements

ResourceMinimumRecommended
CPU1 vCPU2+ vCPU
RAM1 GB (run) · ~4 GB (build)2–4 GB
Disk5 GB SSD20 GB SSD
Network10 Mbps100 Mbps
OSLinux (Ubuntu 22.04+, Debian 12), macOS, WindowsUbuntu 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

  1. Install Rust
  2. Install Visual Studio Build Tools
  3. Ensure cargo is 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:

OSDefault Location
Linux/macOS~/.quantum-vault/core-node/
WindowsC:\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):

VariableCLI FlagDefaultDescription
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-proxyfalseKey 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-rpchttps://sepolia.base.orgBase 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:

FlagDefaultDescription
--host127.0.0.1Bind 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-port5101REST API port
--mineoffEnable block production
--data-dir~/.quantum-vault/core-node/Chain data directory
--chain-idrougechain-devnet-1Network chain ID
--block-time-ms400How 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

  1. Open Task Scheduler → Create Task
  2. General: Name it "RougeChain Node", check "Run whether user is logged on or not"
  3. Trigger: At startup
  4. 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
  5. 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-node directory
  • 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 (not localhost, 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-data Docker 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:

  1. Builder stage — compiles the Rust daemon in a full Rust image
  2. Runtime stage — copies the daemon (rougechain-node) and the rougechain CLI, 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:

  1. Fund your node's own key (/data/rougechain/node-keys.json) with ≥ 10,000 XRGE plus the fee
  2. 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 testnet for 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
  3. 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

FlagEnv VariableDefaultDescription
--host-127.0.0.1Bind 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-4101gRPC port (client services; peers do not use it)
--api-port-5101HTTP API port (also used for all peer traffic)
--chain-id-rougechain-devnet-1Chain identifier (ignored when --genesis is given; the genesis file's chain_id wins)
--genesisQV_GENESIS-Path to a genesis JSON file (e.g. genesis-mainnet.json)
--block-time-ms-400How often the miner checks the mempool (ms). Blocks are only produced when there are transactions
--mine-falseEnable block production
--node-nameQV_NODE_NAME-Human-readable name shown on the network globe
--data-dir-~/.quantum-vault/core-nodeData storage directory
--peersQV_PEERS-Comma-separated peer URLs
--public-urlQV_PUBLIC_URL-This node's public URL for peer discovery
--api-keysQV_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-0Deprecated alias: used for both read and write limits when neither is set
--trust-proxyQV_TRUST_PROXYfalseKey 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-falseDev 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_ENABLEDfalseEnable the faucet endpoints (testnet/dev only — never set on mainnet)
--require-signed-chain-idQV_REQUIRE_SIGNED_CHAIN_IDfalseREQUIRE_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_ORIGINS takes an explicit comma-separated list — setting it to "*" does not produce a wildcard. To serve a true Access-Control-Allow-Origin: * publicly (as api.rougechain.io does), 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

RequirementValue
Staked XRGEMinimum 10,000 XRGE
Node uptimeMust be online to produce blocks
Network syncNode must be synced to chain tip

How Block Production Works

  1. 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/selection reports the same designated proposer for the next height.)
  2. 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)
  3. Signing — The block is signed with the validator's ML-DSA-65 key
  4. Voting — Validators automatically submit prevote and precommit attestations for each block
  5. Propagation — The signed block is broadcast to all peers via POST /api/blocks/import
  6. Verification — Receiving nodes verify the signature and block validity before accepting

Block Timing

ParameterDefaultFlag
Miner poll interval400ms--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 --mine flag 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_next in curl http://127.0.0.1:5100/api/stats. If it is another key, the log line not the designated proposer … — not sealing is 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-url is 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

WhatPrimaryMirror
Manifest + signatureshttps://api.rougechain.io/releases/manifest-<network>.json (+ .ed25519.sig, .mldsa65.sig)https://raw.githubusercontent.com/cyberdreadx/rougechain-node/main/releases/
Node binarybinary.url in the manifest (https://api.rougechain.io/releases/<name>)binary.mirrors: asset of the GitHub release v<version>
rougechain CLIcli.url in the manifestcli.mirrors: asset of the GitHub release v<version>
Genesis filegenesis.url in the manifestgenesis.mirrors: GitHub release asset, and core/daemon/ in the public repository
Installer / updater script (releases from 1.6.1)installer.url in the manifestinstaller.mirrors: asset of the GitHub release v<version>
Public keysreleases/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"
}
FieldTypeMeaning
schemaintegerManifest format version. 1.
networkstringmainnet or testnet.
chain_idstringrougechain-mainnet-1 or rougechain-devnet-1; must match network.
versionstringNode release version, MAJOR.MINOR.PATCH. Increases with every release of a network. MINOR = a release with a new consensus activation; PATCH = everything else.
releasedstringISO date (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ).
source_commitstringGit commit the binary was built from.
public_commitstring or nullThe commit of the public rougechain-node repository that contains that source.
binaryobjectThe node (quantum-vault-daemon): name, url (primary), mirrors (array of URLs, tried in order after url), sha256 (lowercase hex), size (bytes).
cliobject or nullSame shape as binary, for the rougechain CLI built from the same commit. null when a release ships no CLI.
genesisobject or nullSame shape as binary, for the genesis file the node is started with. null when the network runs on default parameters (testnet).
installerobject, optionalSame 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.
mandatorybooleantrue if every node of the network must install this release.
upgrade_before_heightinteger or nullInstall before this block height (the first activation the release introduces).
activationsarrayThe network's upgrade schedule carried by this binary: { "name", "height" }. Names are the upgrade_schedule fields of GET /api/stats.
notes_urlstring or nullRelease / upgrade notes.
min_installer_versionstringOldest 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).

FileAlgorithmEncoding
manifest-<network>.json.ed25519.sigEd25519 (RFC 8032)base64 of the raw 64-byte signature
manifest-<network>.json.mldsa65.sigML-DSA-65 (FIPS 204), empty contextbase64 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.

KeyFingerprint (SHA-256 of the raw public key)
Ed2551979bf33d554ee6d39e5819d94a04d941da9b0e4aeef42eb1106289c5030d4a040
ML-DSA-65ac4497980205f2ccbd810048bcd5ffd819d82400730cfbead41157cc56781b3c

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):

  1. downloads manifest-<network>.json, its .ed25519.sig and its .mldsa65.sig from every source (the primary and the mirror);
  2. verifies each copy's Ed25519 signature with openssl against the release key embedded in the script — it never downloads a key — and, when a rougechain CLI with release verify is 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;
  3. checks the manifest is for the requested network, and is not older than the release already installed (ALLOW_DOWNGRADE=1 overrides);
  4. downloads the node binary, the rougechain CLI (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;
  5. 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;
  6. sets up automatic updates: the updater, rougechain-update, a systemd timer and /etc/rougechain/<network>/update.conf (AUTO_UPDATE=0 to opt out).

It never overwrites node-keys.json or chain data. --dry-run performs steps 1–4 and changes nothing.

mainnettestnet
Servicerougechain-validatorrougechain-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 timerrougechain-update.timerrougechain-update-testnet.timer
API (HOST:API_PORT)127.0.0.1:5100127.0.0.1:5101
gRPC port (P2P_PORT)41004101
Peer (PEERS)https://api.rougechain.io/apihttps://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=notify only tells you about a release, PIN_VERSION holds the node on one release, MODE=off (or AUTO_UPDATE=0 at 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-updatethe command (a two-line wrapper)
/usr/local/lib/rougechain/install-validator.shthe updater: the installer script itself, run as install-validator.sh updater … — one code path for installing and updating
/etc/systemd/system/rougechain-update.timerhourly, at a minute that is fixed per host (RandomizedDelaySec=30min, FixedRandomDelay), Persistent
/etc/systemd/system/rougechain-update.serviceType=oneshot, runs rougechain-update run --network mainnet as root
/etc/rougechain/<network>/update.confyour settings (below)
/var/lib/rougechain-updater/<network>/statewhat 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.

StepCheck
ManifestEd25519 signature over the manifest file, verified with OpenSSL against the embedded key. Always.
ManifestML-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, genesissize and sha256 from the signed manifest
The updater itselfreplaced only by the script whose sha256 is in a signed manifest (the optional installer entry) — never by an older one
Versionmust 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:

  1. The release names it. Releases from 1.6.1 on carry an installer entry 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.
  2. 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 status says so (from: installer file run by the operator). The first update to a release that names an updater replaces it with the signed one.
  3. 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

  1. Ask every source (api.rougechain.io/releases and the GitHub mirror, or RELEASE_BASE_URLS) for manifest-<network>.json and both signature files. Verify each copy.
  2. 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.
  3. Decide (MODE, PIN_VERSION, releases that failed before, the delay window — below).
  4. If the release names a newer updater: verify it, replace the updater, continue as the new one.
  5. 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.
  6. On a validator: wait if it is about to propose a block (below).
  7. Swap the files in. The previous binary and CLI stay as <file>.prev.
  8. Restart the service — only if the node binary changed and the service was running.
  9. 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)

ReleaseWhen 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 awaywithin 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 itimmediately: 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:

  1. GET /api/health answers within HEALTH_START_TIMEOUT_SECS (2 min);
  2. /api/health and /api/stats report the chain id of the release;
  3. /api/stats → upgrade_schedule has every activation of the manifest at the manifest's height (the one exception is canonical_ledger_fork, which nodes do not report);
  4. 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 --peers plus REFERENCE_URLS.
    • Agrees: at the lower of the two tips, c = min(local height, reference height), both nodes have block c — its hash must be the same (GET /api/block/c). When both are at the same height, the state roots in /api/stats must 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.

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:

  1. puts the previous binary, CLI and install record back (the failed files are kept as <file>.failed);
  2. restarts the service and waits for the API;
  3. records the release as failed — it is not tried again at the next run (no restart loop); a newer release is tried;
  4. exits non-zero and logs UPDATE FAILED / ROLLING BACK / ROLLBACK: lines with the reason.
rougechain-update run exit codeMeaning
0nothing to do, installed, scheduled for later, or notify-only
1could not check (no source reachable, nothing validly signed) or refused (settings, genesis change, …). Nothing was changed
3the 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).

SettingDefault
MODEautoauto install signed releases · notify only report them · off do nothing
PIN_VERSIONemptystay 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_SECS21600upper bound of the per-host delay for optional releases
MANDATORY_DELAY_MAX_SECS600…for mandatory releases
DEADLINE_MARGIN_BLOCKS10this close to upgrade_before_height: install immediately
PROPOSER_DEFER_MAX_SECS300longest wait for a proposing validator (0 = never wait)
HEALTH_START_TIMEOUT_SECS120time for the API to answer after a restart
HEALTH_DEADLINE_SECS300time to agree with a reference node
HEALTH_MAX_LAG_BLOCKS2allowed distance behind the reference
REFERENCE_URLSemptyextra reference nodes, e.g. https://node.example.com/api (space-separated)
RELEASE_BASE_URLSemptywhere 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, or rougechain-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_schedule shows 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.

UpgradeMainnet (rougechain-mainnet-1)Testnet (rougechain-devnet-1)
Transaction integrity (tx uniqueness + signed-payload binding)901200
Proposer selection, Release 11001250
Verified BFT finality (FINALITY_V2)1501250
Player-signed contracts (GAME_READY)1501200
Tokens, NFTs, randomness, multi-hop, state root v2 (GAME_READY 2)1601200
host_block_hash commit-then-settle (GAME_READY 3)1701200
Payable contract calls (attach)1901200
Mintable custom tokens (TOKEN_MINTING)2351360
Contracts read NFT royalty (CONTRACT_NFT_ROYALTY)2351360
Monetary-integrity rule (MONETARY_INTEGRITY)2451390
Signatures commit to the chain id (CHAIN_ID_BINDING)not schedulednot scheduled
Contracts read the chain id (CONTRACT_CHAIN_ID)not schedulednot scheduled
Pool transactions apply all-or-nothing (AMM_INTEGRITY)2601440
Shielded pool V2 (SHIELD_V2)not scheduled1480 (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.rs above 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.

  1. 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).
  2. Set it in core/daemon/src/upgrades.rs: token_minting: Some(H) and contract_nft_royalty: Some(H) in TESTNET (and later in MAINNET via node::TOKEN_MINTING_ACTIVATION_HEIGHT and node::CONTRACT_NFT_ROYALTY_ACTIVATION_HEIGHT). Update mainnet_schedule_is_pinned / testnet_schedule_is_pinned to the chosen values, and this page's table.
  3. Test in a clean worktree (never in the live build dir): cargo test -p quantum-vault-daemon -j 2 — includes token_minting_tests, the nft_royalty / nft_marketplace tests in game_ready_tests and the mainnet history replays — cargo test -p quantum-vault-vm -j 2, and cargo check (the default members; --workspace is refused on purpose, see core/Cargo.toml — and so is --all-targets with the daemon selected, for build, check and clippy alike, 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; use cargo test -p quantum-vault-daemon for the tests).
  4. 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) and GET /api/stats → upgrade_schedule.token_minting == H and upgrade_schedule.contract_nft_royalty == H on every node.
  5. At/after H, verify:
    • every node reports the same state_root at the same height (/api/stats);
    • create a test token with "mintable": true, "max_supply": …, then GET /api/token/<SYM>/metadata shows mintable: true on every node;
    • mint once as the creator: balance and total_minted move 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/create with "royaltyBps": 10001 is refused (royaltyBps must be an integer between 0 and 10000); 10000 is accepted.
    • CONTRACT_NFT_ROYALTY: publish contracts/nft_marketplace/nft_marketplace.wasm (it can be published before H, but its calls only run from H). On a test collection with a royalty, list, escrow the NFT with nft_transfer, then buy with the exact price attached: the royalty recipient gains exactly floor(price × bps / 10000) quanta, the seller the rest, the buyer owns the NFT; a buy with the wrong amount fails and is refunded (only gas is charged);
    • roots still agree after those blocks.
  6. 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 formatRequired 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.

  1. Node release 1.6.4 on every node of the network (node rule; harmless to older wallets).
  2. Wallets that sign chainId: the site, @rougechain/sdk 1.15.0 (and every dApp built on it, after a dependency bump), browser extension 1.9.0, Qwalla 1.3.0, rougechain CLI 1.3.0, the MCP server. Watch the node log / API errors for payloads still arriving without the field.
  3. Operators turn on REQUIRE_SIGNED_CHAIN_ID (testnet first). Only then is a payload without a network refused everywhere.
  4. Pick H (well above the tip), set chain_id_binding: Some(H) (and, if wanted at the same time, contract_chain_id: Some(H)) for testnet in core/daemon/src/upgrades.rs — mainnet via chain_binding::CHAIN_ID_BINDING_ACTIVATION_HEIGHT / node::CONTRACT_CHAIN_ID_ACTIVATION_HEIGHT — update mainnet_schedule_is_pinned / testnet_schedule_is_pinned and this page, build, and install on every node of that network before H. Confirm the startup line [upgrades] … chain id binding Some(H), contract chain id Some(H) and GET /api/stats → upgrade_schedule.chain_id_binding == H on each node.
  5. Any transaction still in a mempool at H without 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.

TransactionTakes effect only if
swapthe 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_poolthe 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_liquiditythe 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_liquiditythe 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_out no longer costs the sender anything. Before H a node running 1.6.4 already leaves such a swap out of its blocks; from H the 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.

  1. Pick H (well above the tip), set amm_integrity: Some(H) for testnet in core/daemon/src/upgrades.rs — mainnet via amm_integrity::AMM_INTEGRITY_ACTIVATION_HEIGHT — update mainnet_schedule_is_pinned / testnet_schedule_is_pinned and this page, build, and install on every node of that network before H. A node without the upgrade cannot follow the chain past the first block at or above H whose pool transactions the two rule sets judge differently.
  2. Confirm the startup line [upgrades] … AMM integrity Some(H) and GET /api/stats → upgrade_schedule.amm_integrity == H on each node.
  3. 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 the POST /api/shield-v2/submit route 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 --version reports 1.6.11. The rougechain CLI 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

ItemValue
Binaryquantum-vault-daemon
sha256471567227439993159a169e5ab1c99b9a7373bb81cf7654cf2c5967ad59111b5
Size29,531,048 bytes
Sourcequantum-vault c208989; public rougechain-node
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu
Downloadhttps://api.rougechain.io/releases/quantum-vault-daemon-testnetfix-c208989
CLIrougechain 1.3.0, rougechain-290d4a2, sha256 0df5a526a120eeb12ac77a5a86b6b448bbdf80b371c63db0a4c1427ef22f6870
Installerinstall-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 the POST /api/shield-v2/submit route 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 --version reports 1.6.10. The rougechain CLI is unchanged (1.3.0).

Everything in release 1.6.9 is included.

Production binary

ItemValue
Binaryquantum-vault-daemon
sha25609042df42c321aac660d07b7191b1dd7ba6389c6c55127730c368719e20722df
Size29,544,272 bytes
Sourcequantum-vault 5afe534; public rougechain-node
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu
Downloadhttps://api.rougechain.io/releases/quantum-vault-daemon-shield1480-5afe534
CLIrougechain 1.3.0, rougechain-290d4a2, sha256 0df5a526a120eeb12ac77a5a86b6b448bbdf80b371c63db0a4c1427ef22f6870
Installerinstall-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 --version reports 1.6.9. The rougechain CLI 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

ItemValue
Binaryquantum-vault-daemon
sha256d7e39beb1e8537182bebbf3f0f0bc94cf86615774970e73f90d763dc9020605f
Size29,545,824 bytes
Sourcequantum-vault dc85da7; public rougechain-node
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu
Downloadhttps://api.rougechain.io/releases/quantum-vault-daemon-shieldsubmit-dc85da7
CLIrougechain 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 --version reports 1.6.8. The rougechain CLI 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

ItemValue
Binaryquantum-vault-daemon
sha256b8c11945ef9c94017e2b1cb46472a4855f609b0ef16cb2fa1532d7a7dabf36e2
Size29,400,544 bytes
Sourcequantum-vault 290d4a2; public rougechain-node
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu
Downloadhttps://api.rougechain.io/releases/quantum-vault-daemon-nftfix-290d4a2
CLIrougechain 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_integrity is 260 on mainnet and 1440 on 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 --version reports 1.6.7. The rougechain CLI 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

ItemValue
Binaryquantum-vault-daemon
sha256e9b4fbf188e28ac3d963f35b28589442d2062890508c47c0b403bbab74aa1626
Size29,405,112 bytes
Sourcequantum-vault c64c90c; public rougechain-node
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu
Downloadhttps://api.rougechain.io/releases/quantum-vault-daemon-amm-mainnet-260-c64c90c
CLIrougechain 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_integrity is 1440 on testnet and null on 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 --version reports 1.6.6. The rougechain CLI 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

ItemValue
Binaryquantum-vault-daemon
sha256bdcfefda3db51fbfc8a100c5c6228c42e8836c96d4bec18dbf82e47d1c59f6db
Size29,405,144 bytes
Sourcequantum-vault b7bf14b; public rougechain-node
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu
Downloadhttps://api.rougechain.io/releases/quantum-vault-daemon-amm-testnet-1440-b7bf14b
CLIrougechain 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_integrity in GET /api/stats is null on every network. The height will come in a later, announced release — testnet first.

  • quantum-vault-daemon --version reports 1.6.5. The rougechain CLI 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

ItemValue
Binaryquantum-vault-daemon
sha256d4ac78ef1082a0ff12af71734d67dab1caf9c5e267a7530c2264bd00f2a7da8f
Size29,404,488 bytes
Sourcequantum-vault e433489; public rougechain-node
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu
Downloadhttps://api.rougechain.io/releases/quantum-vault-daemon-amm-integrity-e433489
CLIrougechain 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 (chainId in /api/v2 payloads and in signed mail, messenger, name and vote requests; chain_id in a rougechain CLI envelope). The node refuses a signed payload that names another network with CHAIN_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/sdk 1.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-submit now goes through the checks of a single /api/v2 submission: 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 signed account_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/XRGE returns 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_v2 is null on every network). The consensus rules CHAIN_ID_BINDING and CONTRACT_CHAIN_ID are included and unscheduled (chain_id_binding / contract_chain_id are null); each will get its height in a later, announced release.

  • quantum-vault-daemon --version reports 1.6.4 (earlier binaries reported 0.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

ItemValue
Binaryquantum-vault-daemon
sha25648a43cad46388ede48179432a371a32819492aa59dbc2149653655a5f414e9c3
Size29,396,176 bytes
Sourcequantum-vault 447abca; public rougechain-node
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu
Downloadhttps://api.rougechain.io/releases/quantum-vault-daemon-network-binding-447abca
CLIrougechain 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/conversations and /api/messenger/messages return 410 Gone (outside --dev). Clients use the signed routes: POST /api/v2/mail/folder, /api/v2/messenger/conversations/list and /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

ItemValue
Binaryquantum-vault-daemon
sha25658c0288fc0137f35f4b986be24b010589ff4783c58239bde229f6a7014ab6171
Size28,441,912 bytes
Sourcequantum-vault 8eacc0d; public rougechain-node
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu
Downloadhttps://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.

ChangeConstant (core/daemon/src/node.rs)MainnetTestnet
Mintable tokens (TOKEN_MINTING)TOKEN_MINTING_ACTIVATION_HEIGHT2351360 (active)
Contract royalty reads and royalty cap (CONTRACT_NFT_ROYALTY)CONTRACT_NFT_ROYALTY_ACTIVATION_HEIGHT2351360 (active)
Monetary-integrity rule (MONETARY_INTEGRITY)MONETARY_INTEGRITY_ACTIVATION_HEIGHT2451390

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

ItemValue
Binaryquantum-vault-daemon
sha256c43773c898fa12897ace7d2dca80f95626da63d3bf5cf3c81fafb7a4a60f86a1
Size28,441,848 bytes
Sourcequantum-vault 2c96256; the public rougechain-node source follows once the release is active
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), x86_64-unknown-linux-gnu
Downloadhttps://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:

ChangeConstant (core/daemon/src/node.rs)Activation height
Mintable tokens: creator-only minting with an optional max supply (TOKEN_MINTING)TOKEN_MINTING_ACTIVATION_HEIGHT235
Contract royalty reads; royalties above 100% refused at creation (CONTRACT_NFT_ROYALTY)CONTRACT_NFT_ROYALTY_ACTIVATION_HEIGHT235

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

ItemValue
Binaryquantum-vault-daemon
sha256673bf7b1a469b7f9937b17228faab1e81ae48fb2a90aa32b2fd1b7fcc9cc2984
Size28,463,856 bytes
Sourcequantum-vault 4f8676d (release commit 03613ef); public rougechain-node main e048bf1
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), cargo 1.94.0 (85eff7c80 2026-01-15), x86_64-unknown-linux-gnu
Downloadhttps://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:

ChangeConstant (core/daemon/src/node.rs)Activation height
Verified BFT finality — Release 2aFINALITY_V2_ACTIVATION_HEIGHT150
Player-signed contract calls and deployments (GAME_READY)GAME_READY_ACTIVATION_HEIGHT150
Game-ready contracts: tokens, NFTs, randomness, multi-hop moves, state root v2 (GAME_READY 2)GAME_READY_2_ACTIVATION_HEIGHT160
Grind-proof rolls: host_block_hash (GAME_READY 3)GAME_READY_3_ACTIVATION_HEIGHT170
Payable contract calls (attach)PAYABLE_CALLS_ACTIVATION_HEIGHT190 — 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

ItemValue
Binaryquantum-vault-daemon
sha256cc1aee1983e39a61a22995b85dba9e5d7c4f8ef2c3b0c9b7c4dd7cd7f86947bc (block-190 payable-calls release; supersedes 718200bc…, 5e5ca98c… and 46e29456…)
Size28,031,064 bytes
Source commitcc6f9c3, branch feat/payable-calls (merged to main as #153)
Toolchainrustc 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_root from curl -s localhost:5100/api/stats equals https://api.rougechain.io/api/stats at 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:

ChangeActivation heightStatus
Transaction-integrity rule (TX_UNIQUENESS_ACTIVATION_HEIGHT)90live since 2026-09-23
Deterministic proposer selection, Release 1 (PROPOSER_SELECTION_ACTIVATION_HEIGHT)100live 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

ItemValue
Binaryquantum-vault-daemon
sha256d50f7e5d34d03200f6efc45c8b7b63c61079e7f9d5b5c70f5b3c3c3544105eba
Size26,866,528 bytes
Source commit (this repository)203507e (consensus: read-only activation report), branch release/proposer-selection-r1
Toolchainrustc 1.94.0 (4a4ef493e 2026-03-02), cargo 1.94.0 (85eff7c80 2026-01-15), x86_64-unknown-linux-gnu
Downloadhttps://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 runningResult on mainnet today
Any build before the transaction-integrity rule (before commit a005046)Cannot validate blocks ≥ 90. Must upgrade and resync.
Transaction-integrity build 9ad81dbf98e8e78a86b34be61af2360a34dba055f9f7a4be81b1a69c15942287Follows the chain but does not enforce proposer selection at ≥ 100. Not acceptable for a validator. Upgrade before reconnecting.
d50f7e5d34d03200f6efc45c8b7b63c61079e7f9d5b5c70f5b3c3c3544105ebaCurrent.

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.

  1. Stop the node.
    systemctl stop <your-unit>      # e.g. rougechain-validator
    
  2. Back up keys and data. node-keys.json in your data directory is your staked validator key.
    cp -a ~/.quantum-vault/mainnet ~/.quantum-vault/mainnet.backup-$(date +%Y%m%d)
    
  3. 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
    
  4. 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.json back). 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.
  5. Start without --mine. Remove --mine from 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
    
  6. 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_root must match at equal heights and designated_proposer_next must match. Only then is the node on mainnet.
  7. 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

FeatureDescription
Block SyncNodes download and verify blocks from peers
Transaction BroadcastSubmitted txs propagate to all peers
Peer DiscoveryNodes share their peer lists with each other
Genesis ResetNew 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:

  1. Querying known peers via GET /api/peers
  2. Adding any new peers to their list
  3. 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

EndpointMethodDescription
/api/peersGETList known peers
/api/peers/registerPOSTRegister as a peer
/api/blocks/importPOSTImport a block (peer-to-peer)
/api/tx/broadcastPOSTReceive 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:

  1. Download all blocks from the peer
  2. Verify each block's PQC signatures
  3. Build the local chain state
  4. 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-url with 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.

PortBindExposure
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 height increase via /api/health
  • Try connecting to a different peer

"Chain ID mismatch"

  • Ensure your --chain-id matches 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

RequirementDetails
ServerVPS or dedicated server with a public IP
Domain (recommended)Point a domain to your server
SSL certificateRequired for HTTPS (use Let's Encrypt)
Open port443 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-proxy to key them by the X-Real-IP / X-Forwarded-For this 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-url is 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-url must 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

MetricHow to Check
Block heightGET /api/health — compare with testnet
Peer countGET /api/peers — should be > 0
Validator statusGET /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

  1. Stake tokens - Lock XRGE to become a validator
  2. Propose and vote - The designated proposer (the validator with the most stake) proposes blocks; staked validators vote to finalize them
  3. Earn rewards - Every staked validator earns a stake-weighted share of the fees in every block
  4. Unstake - Wait for unbonding period to withdraw

Requirements

RequirementValue
Minimum stake10,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 period500 blocks (a block count, not a duration)
SlashingNone 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

  1. Go to the Validators page
  2. Click Stake
  3. Enter amount (min 10,000 XRGE)
  4. Confirm transaction
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/selection reports this designated proposer for the next height (rule: "designated_max_stake").

Rewards

Validators earn from an EIP-1559-inspired fee model:

ComponentDistribution
Base feeHalf of the base fee per transaction is burned (capped at the fees collected); everything else flows into the block's tip pool
Tip poolProposer 20% · validators 70% (stake-weighted) · treasury 10%
Minimum tipThe 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:

  1. Run the installer (above). Back up /var/lib/rougechain/mainnet/node-keys.json offline.

  2. Fund and stake ≥ 10,000 XRGE from that key with the installed rougechain CLI. The key file is readable only by the rougechain user, 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.

  3. Once validator-status shows ✓ Staked and ✓ 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:

  1. it holds your stake — staking from a key registers that key as a validator, and
  2. 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

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_next in GET /api/stats, and proposer in GET /api/selection (which reports rule: "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.json offline. 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 slash transaction 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/health height vs the network).
  • Never run --dev on 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 (or PIN_VERSION) in /etc/rougechain/mainnet/update.conf — then you must install mandatory releases before their upgrade height. Never use anything that builds and runs main unattended (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 10000 again (≥ 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 a stake tx), 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:

  1. The validator set is deterministic from history. Genesis seeds initial_validators (each with a stake), and every later stake / unstake transaction 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.

  2. 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, and prev_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.

  1. Generate the node identity and sync to the tip. Your node-keys.json key is both your stake-holding key and your block-signing key — they must be the same key. Let the node fully sync first so your stake tx builds on the current tip.
  2. 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 the stake tx 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.
  3. Confirm you are in the active set before mining:
    rougechain --node-keys ~/.quantum-vault/mainnet/node-keys.json validator-status
    
    You want ✓ Staked and ✓ 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."
  4. Only now start --mine — and make the node reachable. Start with --mine and 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-status shows ✗ on Staked or In active set — the fix is printed next to it (usually: stake, top up, or wait for the stake tx 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_next in /api/stats and proposer in GET /api/selection both 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-url is 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 the stake CLI/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 > 0 and 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 with 0 < stake < 10,000 is 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 — every stake > 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

Staking Rewards

Validators earn a share of the transaction fees in every block on RougeChain.

Reward Sources

SourceDescription
Transaction feesYour 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

  1. The designated proposer (the eligible validator with the most stake) proposes a block when there are pending transactions
  2. The proposer assembles pending transactions; each transaction's full signed fee is debited from its sender
  3. 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.
  4. Rewards are credited when the block is applied to state

Fee Structure

Transaction TypeFee
Transfer (wallet-signed)1 XRGE
Token creation100 XRGE
Pool creation10 XRGE
Swap1 XRGE network fee, plus 0.3% of the input to liquidity providers
Add / remove liquidity1 XRGE
Stake / Unstake1 XRGE
Bridge withdrawal0.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

ScenarioValue
Your stake10,000 XRGE
Total staked100,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 share10%
Your share of the validator pool10% of 70% of each block's tip pool
Avg fee per blockvaries 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:

  1. Periodically stake your accumulated rewards
  2. This increases your share of the validator pool
  3. 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 indicatorBlocks 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-preflight checks 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 / depositERC20 and 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_mint key) 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:

SystemAssetsCounterpart chainPage
Base bridge (R1)XRGE, qETH, qUSDCBase mainnetthis page, XRGE, qETH, qUSDC
Bitcoin bridgeqBTCBitcoinBitcoin 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 AssetRougeChain AssetDecimalsDirection
ETHqETH6 (L1 units)Both ways
XRGEXRGE18 (EVM) / whole units (L1)Both ways
USDCqUSDC6Both ways

XRGE uses the BridgeVaultV2 contract; ETH and USDC use the RougeBridge contract.

How It Works

Deposit (EVM → RougeChain)

  1. User deposits ETH or USDC into RougeBridge, or XRGE into BridgeVaultV2, on Base
  2. 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.
  3. 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 Transfer to 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)

  1. User submits a signed bridge_withdraw transaction on RougeChain, burning the wrapped token
  2. The withdrawal is recorded in the pending withdrawals store with its owner key, token, and status
  3. The bridge relayer polls for pending withdrawals and releases the corresponding asset on Base
  4. 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_SECRET for 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)

  1. Approve the BridgeVaultV2 contract to spend your XRGE
  2. Call deposit(amount, rougechainPubkey) on the vault
  3. The vault locks your XRGE and emits a BridgeDeposit event carrying your L1 key
  4. 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)

  1. Go to the Bridge page and use the XRGE Bridge Out tab
  2. Enter the amount and your Base EVM address
  3. Submit the signed withdrawal
  4. 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.
  5. 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 relayer
  • release(to, amount, l1TxId) — relayer role only, within per-transaction and rolling daily caps, while unpaused
  • vaultBalance() — 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 depositETH with 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)

  1. Go to the Bridge page and select the Bridge Out tab
  2. Select ETH as the token
  3. Enter the amount of qETH to bridge out
  4. Enter the Base address to receive ETH
  5. 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

OperationFee
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 depositERC20 with 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)

  1. Approve RougeBridge to spend your USDC.
  2. Call depositERC20(usdc, amount, rougechainPubkey) (the Bridge page does this for you).
  3. 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)

  1. On the Bridge page choose Bridge Out and select USDC.
  2. Enter the amount and your Base address, then submit the signed withdrawal.
  3. 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.

See Bridge Security Model.

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:

  1. 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_RETURN needed.
    • OP_RETURN fallback. A depositor sends to the custody address and writes their RougeChain address into an OP_RETURN output of that same transaction. The binding is baked into a signed Bitcoin transaction, so knowing a txid does not let anyone redirect the mint.
  2. 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

  1. 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).
  2. User sends BTC to it from any wallet — no OP_RETURN.
  3. 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

  1. User sends BTC to the custody address with an OP_RETURN output containing their RougeChain address. (Wallets that support OP_RETURN: Sparrow, Electrum, BlueWallet advanced, bitcoinjs.)
  2. User submits the txid to POST /api/bridge/btc/claim { btcTxid, recipientRougechainPubkey? }. The frontend polls this until it succeeds.
  3. The daemon cross-checks both providers, then mints qBTC (sats) to the OP_RETURN recipient. Dedupe key: btc:{txid}.

Withdraw flow (qBTC → BTC)

  1. User burns qBTC via POST /api/bridge/withdraw with tokenSymbol: "qBTC", evmAddress: <destination BTC address>, amountUnits: <sats> (signed). Amounts below the minimum (btcMinWithdrawSats, default 2 000 sats) are rejected before anything is burned.
  2. 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.
  3. 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.

RuleValue
Minimum withdrawalbtcMinWithdrawSats from GET /api/bridge/config (default 2 000 sats)
Network-fee capbtcMaxNetworkFeeSats (default 10 000 sats). When fees are higher the payout waits until they drop — it is never paid with a larger fee.
Tiny remaindersIf 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)

LayerProtection
RougeChain sideWithdrawals 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.
DepositsEvery 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.
PayoutsEach 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.
OperationsDaemon-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.

AssetTodayAfter V3 activation
XRGER1, classical Base-side authorizationML-DSA-65 authorization on Base
qETHR1, classical Base-side authorizationUnchanged (outside V3 scope)
qUSDCR1, classical Base-side authorizationUnchanged (outside V3 scope)
qBTCSeparate Bitcoin bridgeUnchanged (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. BridgeVaultV3 is 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:

  1. RougeChain finalizes blocks with verified validator votes (FINALITY_V2).
  2. Finalized XRGE withdrawals are grouped into epochs. Each epoch has a deterministic Merkle root that any node can recompute from chain history.
  3. 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.
  4. BridgeVaultV3 accepts 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.
  5. 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 RougeBridge with 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

EventDescription
BridgeDepositETHETH deposited for bridging
BridgeDepositERC20ERC-20 deposited for bridging
BridgeReleaseETHETH released to user
BridgeReleaseERC20ERC-20 released to user
TimelockQueuedLarge release queued with delay
TimelockExecutedQueued release executed
TimelockCancelledQueued 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 of BridgeVaultV2; 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

  1. Polls the node for pending ETH, USDC and XRGE withdrawals and releases the corresponding asset on Base
  2. Marks each release fulfilled; on repeated failure, reports it. Auto-refund exists but is disabled in production (AUTO_REFUND=false)
  3. Deposit watcher: scans the bridge contracts for deposit events and auto-claims them on L1
  4. 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 calls releaseETH() / 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_SECRET should 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

OperationFee
Create Pool10 XRGE
Swap0.3% of input + 1 XRGE tx fee
Add Liquidity1 XRGE
Remove Liquidity1 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:

  1. Token A is added to the pool
  2. Token B is removed, maintaining the constant product
  3. 0.3% fee is taken from the input amount
  4. 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:

  1. Go to the Swap page
  2. Click Create Pool
  3. Select two tokens (e.g., XRGE / qETH)
  4. Enter the initial amounts for each token
  5. Click Create Pool

The initial token ratio sets the starting price. A 10 XRGE fee is charged for pool creation.

Adding Liquidity

  1. Go to the pool page
  2. Click Add Liquidity
  3. Enter the amount of one token — the other amount auto-calculates based on the current ratio
  4. Confirm the transaction

You receive LP tokens proportional to your contribution.

Removing Liquidity

  1. Go to the pool page
  2. Click Remove Liquidity
  3. Enter the LP token amount to withdraw
  4. 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

  1. Go to the Swap page
  2. Select the input token and output token
  3. Enter the amount to swap
  4. Review the quote (output amount, price impact, minimum received)
  5. 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 ImpactSeverity
< 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

PropertyValue
Creation fee100 XRGE
SupplySet 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
DecimalsNot configurable — user-created tokens are whole units (0 decimals)
TradingVia AMM liquidity pools

Create a Token

Via Web UI

  1. Navigate to the Token Explorer page
  2. Click Create Token
  3. 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)
  4. 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:

  1. Transfer tokens to other wallets
  2. Create a liquidity pool to enable trading
  3. 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_minting shows the height. On a network where the height has not been reached the node refuses mintable / max_supply on /api/v2/token/create with token minting is not active yet and refuses every /api/v2/token/mint.

A token can be created mintable, with an optional cap. Add to the signed create payload:

FieldTypeMeaning
mintablebooleantrue: the creator may mint more later. Omit (or false) for a fixed-supply token.
max_supplyintegerOptional 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.
  • amount and max_supply are JSON integers (a float or string is refused), at most 9,007,199,254,740,991 (2^53 − 1). A mintable token's initial_supply has the same bound.
  • initial_supply + total_minted + amount must stay ≤ max_supply when 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_minted is 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:

FieldDescription
imageLogo URL or base64 data URI
descriptionToken description
websiteProject website
twitterX/Twitter handle
discordDiscord 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

OperationFee (XRGE)
Create Collection50
Mint NFT5
Batch Mint5 per NFT
Transfer1
Burn0.1
Lock/Unlock0.1
Freeze Collection0.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

  1. Go to the NFT Explorer page
  2. Click Create Collection
  3. 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
  4. 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_transfer that declares a salePrice > 0. The royalty is salePrice × 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_splitter and 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: salePrice is self-declared in the transfer payload — there is no on-chain marketplace escrow. A transfer sent with no salePrice (or 0) pays no royalty. Royalties are honored by marketplaces/apps that populate salePrice, not enforced against every possible transfer.

Collection Properties

PropertyDescription
idUnique identifier (creator:symbol)
symbolShort token symbol
nameFull collection name
creatorCreator's public key
max_supplyMax tokens (0 = unlimited)
royalty_bpsRoyalty in basis points
royalty_recipientWallet that receives royalties (defaults to creator)
frozenWhether minting is frozen
total_mintedNumber 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

  1. Open a collection page
  2. Click Mint NFT
  3. Enter the token name and optional metadata URI / attributes
  4. Click Mint

Only the collection creator can mint new tokens.

Batch Mint

Mint multiple NFTs at once:

  1. Open a collection page
  2. Click Batch Mint
  3. Provide a list of names (and optional URIs for each)
  4. 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 attributes field (one entry per name). @rougechain/sdk 1.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 under batchAttributes, 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:

PropertyDescription
token_idSequential ID within the collection
nameToken name
ownerCurrent owner's address (rouge1...)
creatorOriginal minter
metadata_uriLink to off-chain metadata (IPFS, HTTP)
attributesOn-chain key-value attributes
lockedWhether the token is locked (non-transferable)
created_atTimestamp of minting

Transferring

  1. Open the NFT detail page
  2. Click Transfer
  3. Enter the recipient's RougeChain address (rouge1...)
  4. Optionally set a sale price (for marketplace tracking)
  5. Confirm

Locked NFTs cannot be transferred until unlocked by the owner.

Burning

Permanently destroy an NFT:

  1. Open the NFT detail page
  2. Click Burn
  3. Confirm

Only the current owner can burn. This action is irreversible.

Locking

Lock an NFT to prevent it from being transferred:

  1. Open the NFT detail page
  2. 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

The explorer is integrated into the main RougeChain web app:

PageURLDescription
Blockchain/blockchainBlock list with live updates
Block Detail/block/:heightIndividual block
Transaction/tx/:hashIndividual transaction
Address/address/:pubkeyAddress details
Transactions/transactionsAll transactions list
NFT Explorer/nft-explorerNFT collections browser
Tokens/tokensAll 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:

FieldDescription
HeightBlock number in the chain
HashSHA-256 hash of the block
Previous HashHash of the parent block
TimestampWhen the block was created
ProposerPublic key of the block proposer
Transaction CountNumber 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:

FieldDescription
HashTransaction hash (SHA-256)
TypeTransaction type
FromSender public key
ToRecipient public key
AmountTransaction amount
FeeTransaction fee (XRGE)
TokenToken symbol (if applicable)
BlockBlock height containing this tx
TimestampWhen the transaction was processed
SignaturePQC signature (ML-DSA-65)

Transaction Types

TypeDescription
transferToken transfer between addresses
create_tokenNew token creation
stakeStaking tokens
unstakeUnstaking tokens
create_poolCreating a liquidity pool
add_liquidityAdding liquidity to a pool
remove_liquidityRemoving liquidity from a pool
swapToken swap via AMM
bridge_mintMinting bridged tokens (qETH, qUSDC)
bridge_withdrawBurning bridged tokens for withdrawal
nft_create_collectionCreating an NFT collection
nft_mintMinting an NFT
nft_transferTransferring an NFT
nft_burnBurning 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:

FieldDescription
MethodFunction name (e.g. get_count, transfer)
ArgumentsJSON object with method parameters
CallerCaller identity (optional)
Gas LimitMax 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:

LabelDescription
Contract DeployWASM bytecode deployment
Contract CallMethod 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 GONE in 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:

EndpointAccepts
/api/balance/:publicKey, /api/balance/:publicKey/:tokenhex public key or rouge1… address
/api/address/:pubkey/transactionshex public key or rouge1… address
/api/resolve/:inputhex public key or rouge1… address
/api/nft/owner/:pubkeyexact match on the owner as recorded — normally the hex public key; a rouge1… address does not match a pubkey-owned NFT
/api/account/:pubkey/noncehex 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/:input to convert between rouge1… 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

EndpointMethodDescription
/api/healthGETNode health check
/api/statsGETNetwork statistics
/api/wsGETWebSocket for real-time events
/api/price/xrgeGETCurrent XRGE price

Blockchain

EndpointMethodDescription
/api/blocksGETGet recent blocks (max 100) or a page of blocks
/api/blocks/summaryGETBlock summary for charts
/api/block/:heightGETGet block by height
/api/txsGETGet transactions
/api/tx/:hashGETGet transaction by hash
/api/tx/:hash/receiptGETGet transaction receipt
/api/eventsGETGet all events

Wallet & Accounts

EndpointMethodDescription
/api/balance/:publicKeyGETGet XRGE balance
/api/balance/:publicKey/:tokenGETGet token balance
/api/account/:pubkey/nonceGETGet account nonce
/api/resolve/:inputGETResolve address ↔ public key
/api/address/:pubkey/transactionsGETGet address transactions
/api/v2/transferPOSTTransfer tokens (signed)
/api/v2/faucetPOSTRequest testnet tokens (signed)

Tokens

EndpointMethodDescription
/api/tokensGETList all tokens
/api/token/:symbol/metadataGETGet token metadata
/api/token/:symbol/holdersGETGet token holders
/api/token/:symbol/transactionsGETGet token transactions
/api/burn-addressGETGet official burn address
/api/burnedGETGet burned token stats
/api/v2/token/createPOSTCreate token (signed); mintable / max_supply since TOKEN_MINTING (mainnet block 235)
/api/v2/token/mintPOSTMint more of a mintable token, creator only (signed); live since mainnet block 235
/api/v2/token/metadata/updatePOSTUpdate token metadata (signed)
/api/v2/token/metadata/claimPOSTClaim metadata ownership (signed)

Token Allowances (ERC-20 Style)

EndpointMethodDescription
/api/v2/token/approvePOSTApprove spender allowance (signed)
/api/v2/token/transfer-fromPOSTTransfer using allowance (signed)
/api/v2/token/freezePOSTFreeze/unfreeze token transfers (signed)
/api/token/allowanceGETCheck specific allowance
/api/token/allowancesGETList allowances by owner/spender
/api/allowances/:pubkeyGETList all allowances for a key
/api/locks/:pubkeyGETGet token locks

Staking & Validators

EndpointMethodDescription
/api/validatorsGETList validators
/api/validators/statsGETValidator vote stats
/api/selectionGETProposer 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/finalityGETFinality status (legacy, informational — see Finality)
/api/votesGETVote quorum info
/api/v2/stakePOSTStake tokens (signed)
/api/v2/unstakePOSTUnstake tokens (signed)

Token Staking Pools (suspended since block 245)

create_staking_pool, token_stake and token_unstake are 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.

EndpointMethodDescription
/api/staking/poolsGETList all staking pools
/api/staking/pool/:pool_idGETGet staking pool details
/api/staking/stakes/:pubkeyGETGet stakes by owner
/api/staking/pool/:pool_id/stakesGETGet stakes in a pool

AMM/DEX

EndpointMethodDescription
/api/poolsGETList liquidity pools
/api/pool/:pool_idGETGet pool details
/api/pool/:pool_id/pricesGETGet price history
/api/pool/:pool_id/eventsGETGet pool events
/api/pool/:pool_id/statsGETGet pool statistics
/api/swap/quotePOSTGet swap quote
/api/v2/pool/createPOSTCreate liquidity pool (signed)
/api/v2/pool/add-liquidityPOSTAdd liquidity (signed)
/api/v2/pool/remove-liquidityPOSTRemove liquidity (signed)
/api/v2/swap/executePOSTExecute swap (signed)

NFTs

EndpointMethodDescription
/api/nft/collectionsGETList collections
/api/nft/collection/:idGETGet collection
/api/nft/collection/:id/tokensGETGet collection tokens
/api/nft/token/:coll/:idGETGet specific NFT
/api/nft/owner/:pubkeyGETGet NFTs by owner
/api/v2/nft/collection/createPOSTCreate collection (signed)
/api/v2/nft/mintPOSTMint NFT (signed)
/api/v2/nft/batch-mintPOSTBatch mint (signed)
/api/v2/nft/transferPOSTTransfer NFT (signed)
/api/v2/nft/burnPOSTBurn NFT (signed)
/api/v2/nft/lockPOSTLock/unlock NFT (signed)
/api/v2/nft/freeze-collectionPOSTFreeze collection (signed)

Smart Contracts

EndpointMethodDescription
/api/v2/contract/publishPOSTPublish (deploy) WASM contract (player-signed, 10 XRGE)
/api/v2/contract/executePOSTCall a contract method (player-signed, fee = gasLimit × 0.000001 XRGE)
/api/contract/:address/queryPOSTRead-only call (free, not signed)
/api/contract/:addressGETGet contract metadata
/api/contract/:address/stateGETGet contract state
/api/contract/:address/eventsGETGet contract events (limit, before, tx)
/api/contractsGETList contracts

Bridge (ETH/USDC + XRGE)

EndpointMethodDescription
/api/bridge/configGETETH/USDC bridge config
/api/bridge/claimPOSTClaim bridge deposit
/api/bridge/withdrawPOSTWithdraw to EVM
/api/bridge/withdrawalsGETList pending withdrawals
/api/bridge/activityGETPublic bridge activity (deposits + withdrawals, paginated)
/api/bridge/activity/:txIdGETOne bridge transfer by RougeChain tx id
/api/bridge/withdrawals/:txIdDELETEFulfill withdrawal
/api/bridge/btc/deposit-addressPOSTPer-recipient BTC deposit address (qBTC)
/api/bridge/xrge/configGETXRGE bridge config
/api/bridge/xrge/claimPOSTClaim XRGE deposit
/api/bridge/xrge/withdrawPOSTWithdraw XRGE to EVM
/api/bridge/xrge/withdrawalsGETList XRGE withdrawals
/api/bridge/xrge/withdrawals/:txIdDELETEFulfill XRGE withdrawal

Shielded Transactions (suspended since block 245)

The V1 shielded pool is suspended: shield, shielded_transfer and unshield are refused (this transaction type is suspended) and a block carrying them is invalid. The mainnet pool is empty (shielded_supply 0). A redesigned pool (V2) is in development and unaudited — see Status.

EndpointMethodDescription
/api/v2/shielded/shieldPOSTRefused: suspended
/api/v2/shielded/transferPOSTRefused: suspended
/api/v2/shielded/unshieldPOSTRefused: suspended
/api/shielded/statsGETShielded pool statistics (pool total 0 on mainnet)
/api/shielded/nullifier/:hashGETCheck 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.

EndpointMethodDescription
/api/v2/rollup/statusGETRollup status
/api/v2/rollup/batch/:idGETGet rollup batch
/api/v2/rollup/submitPOSTSubmit 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.

EndpointMethodDescription
/api/governance/proposalsGETList all proposals
/api/governance/proposals/:tokenGETProposals by token
/api/governance/proposal/:idGETGet proposal details
/api/governance/proposal/:id/votesGETGet proposal votes

Social

EndpointMethodDescription
/api/social/track/:trackId/statsGETGet track stats (plays, likes, comments)
/api/social/track/:trackId/commentsGETGet track comments
/api/social/artist/:pubkey/statsGETGet artist stats (followers, following)
/api/social/user/:pubkey/likesGETGet user's liked track/post IDs
/api/social/user/:pubkey/followingGETGet user's followed artists
/api/social/user/:pubkey/postsGETGet user's posts
/api/social/post/:postIdGETGet a single post with stats
/api/social/post/:postId/statsGETGet post stats (likes, reposts, replies)
/api/social/post/:postId/repliesGETGet replies to a post
/api/social/timelineGETGlobal timeline (newest first)
/api/v2/social/playPOSTRecord a play (signed)
/api/v2/social/likePOSTToggle like (signed)
/api/v2/social/commentPOSTPost a comment (signed)
/api/v2/social/comment/deletePOSTDelete a comment (signed)
/api/v2/social/followPOSTToggle follow (signed)
/api/v2/social/postPOSTCreate a post (signed)
/api/v2/social/post/deletePOSTDelete a post (signed)
/api/v2/social/repostPOSTToggle repost (signed)
/api/v2/social/feedPOSTGet following feed (signed)

Messenger

EndpointMethodDescription
/api/messenger/walletsGETList messenger wallets
/api/v2/messenger/wallets/registerPOSTRegister wallet (signed)
/api/v2/messenger/conversationsPOSTCreate conversation (signed)
/api/v2/messenger/conversations/listPOSTList conversations (signed)
/api/v2/messenger/conversations/deletePOSTDelete conversation (signed)
/api/v2/messenger/messagesPOSTSend message (signed)
/api/v2/messenger/messages/listPOSTList messages (signed)
/api/v2/messenger/messages/readPOSTMark as read (signed)
/api/v2/messenger/messages/deletePOSTDelete message (signed)

Mail

EndpointMethodDescription
/api/v2/names/registerPOSTRegister mail name (signed)
/api/v2/names/releasePOSTRelease a name (signed)
/api/names/resolve/:nameGETResolve name → wallet
/api/names/reverse/:walletIdGETReverse lookup → name
/api/v2/mail/sendPOSTSend encrypted mail (signed)
/api/v2/mail/folderPOSTGet inbox/sent/trash (signed)
/api/v2/mail/messagePOSTGet single mail item (signed)
/api/v2/mail/readPOSTMark as read (signed)
/api/v2/mail/movePOSTMove to folder (signed)
/api/v2/mail/deletePOSTDelete permanently (signed)

Push Notifications

EndpointMethodDescription
/api/push/registerPOSTRegister push token (PQC-signed)
/api/push/unregisterPOSTUnregister push token (PQC-signed)

P2P

EndpointMethodDescription
/api/peersGETList known peers
/api/peers/registerPOSTRegister as peer
/api/blocks/importPOSTImport block from peer
/api/tx/broadcastPOSTReceive 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-Ts headers (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
}
FieldTypeDescription
statusstring"ok" if the node is healthy
chain_idstringThe chain identifier
heightnumberCurrent 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
}
FieldTypeDescription
connected_peersnumberCurrently connected peers
network_heightnumberCurrent block height
is_miningbooleanWhether this node is mining
node_idstringThis node's ID
total_fees_collectednumberTotal fees collected
fees_in_last_blocknumberFees collected in the last block
chain_idstringChain identifier
finalized_heightnumberLast finalized block height
ws_clientsnumberActive WebSocket clients
node_namestring | nullHuman-readable node name (optional)
base_feenumberCurrent EIP-1559 base fee
total_fees_burnednumberTotal 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
}
FieldTypeDescription
burnedobjectPer-token burned totals, keyed by token symbol
total_xrge_burnednumberTotal 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

ParameterTypeDefaultDescription
pagenumber-Page number (1-based); enables paginated mode
per_pagenumber10Blocks per page (used with page)
limitnumber100 (1000 with from_height)Maximum blocks to return (max 100, up to 1000 with from_height)
from_heightnumber-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, page and total_pages are only returned in paginated mode (when page is supplied); with limit or from_height the response contains only blocks.

Block Fields

FieldTypeDescription
versionnumberBlock format version
header.chain_idstringChain identifier
header.heightnumberBlock number
header.timenumberTimestamp (ms since epoch)
header.prev_hashstringHash of previous block
header.tx_hashstringSHA-256 over the concatenated encoded transactions, in block order (not a Merkle root)
header.proposer_pub_keystringValidator who proposed this block
header.state_rootstringPost-state root of the balance ledger (omitted before its activation height)
header.parent_commitobjectFinality certificate for the parent block (omitted before FINALITY_V2 activation)
txsarrayTransactions included in this block
proposer_sigstringML-DSA-65 signature by the proposer
hashstringSHA-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

ParameterTypeDefaultDescription
rangestring24hTime 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:

  1. Hash check — Recompute the block hash and compare
  2. Signature check — Verify ML-DSA-65 signature against the proposer's public key
  3. Height check — Must extend the current chain tip by exactly 1
  4. Previous hash — Must reference the current tip's hash
  5. Designated proposer — The proposer must be the designated proposer for that height (largest eligible stake; ties → lowest public key)
  6. Parent commit — The parent must carry a ⅔-stake finality certificate (parent_commit)
  7. Transaction validity — All transactions must have valid signatures and sufficient balances
  8. 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"
}
FieldTypeRequiredDescription
tostringYesRecipient's public key (hex) or rouge1 address
amountnumberYesAmount to send
tokenstringNoToken symbol (default: "XRGE")

Fee: The transaction fee is fixed at 1 XRGE and enforced by the server; any client-supplied fee in 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

ParameterTypeDefaultDescription
limitnumber200Max transactions to return (max 1000)
offsetnumber0Pagination 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_hex holds the recipient exactly as it was submitted — either a rouge1… 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, call GET /api/resolve/:input (auto-detects the format and returns address, publicKey and balance).
  • receipt.logs[].data.to carries the same recipient value; receipt.status is "Success" or {"Failed": "<reason>"}.
  • signed_payload is the exact JSON the sender signed (V2 transactions); consensus binds the executed fields to it, so tx.* never differs from what was signed.
  • Stability: the tx and receipt field names are the serialized TxV1 / TxReceipt types 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

TypeDescription
transferStandard XRGE or token transfer (faucet payouts are transfers with payload.faucet = true)
stakeStake tokens to become validator
unstakeUnstake tokens
create_tokenCreate custom token (token_mintable / token_max_supply only from TOKEN_MINTING activation)
mint_tokensCreator mints more of a mintable token (token_total_supply = amount) — applied only from TOKEN_MINTING activation
burnBurn tokens permanently
shield / shielded_transfer / unshieldSuspended 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_deployDeploy WASM smart contract
contract_callCall 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

ParameterTypeDescription
publicKeystringThe 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

FieldTypeDescription
publicKeystringValidator's ML-DSA-65 public key
namestringNode name, if known (omitted otherwise)
stakenumberAmount of XRGE staked
statusstringactive, jailed or inactive (no stake)
slashCountnumberTimes this validator has been slashed (historical: no slashing is active since height 100, and the slash transaction is rejected since height 245)
jailedUntilnumberBlock height the validator is jailed until (0 if never)
entropyContributionsnumberEntropy contributions counter
blocksProposednumberTotal 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

RequirementValue
Minimum stake10,000 XRGE — checked by this API (and the CLI), not by consensus; a block applying a smaller stake is valid
Fee1 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

ErrorCause
"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 routes GET /api/messenger/conversations and GET /api/messenger/messages — use the signed POST …/conversations/list and POST …/messages/list below. 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, displayName and encryptionPublicKey can be updated by the owner, never by another wallet. If encryptionPublicKey is already registered to another wallet the request fails with This 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"
}
FieldTypeDescription
idstringUnique wallet identifier
displayNamestringHuman-readable name (unique, case-insensitive)
signingPublicKeystringML-DSA-65 public key (hex)
encryptionPublicKeystringML-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.

LimitValue
Max upload size50 MB (before compression)
Compressed target~1.5 MB
Image formatConverted to WebP client-side
Video formatConverted 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:

RuleDefaultNode setting
Voting weightXRGE balance when the proposal opened (snapshot)—
Excluded walletsTeam, treasury and exchange walletsQV_REGEN_EXCLUDED, QV_REGEN_TREASURY
Per-wallet cap5% of eligible supplyQV_REGEN_CAP_BPS
Turnout to pass10% of eligible supply (yes + no + abstain)QV_REGEN_TURNOUT_BPS
MajorityMore yes than no—
Voting window7 days (1–30)QV_REGEN_DEFAULT_DAYS
Opening a proposalCurator, or 10,000 XRGE and one open proposal at a timeQV_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

EndpointReturns
GET /api/regen/configTreasury, cap, turnout, default window, excluded wallets, curators
GET /api/regen/proposalsEvery proposal, newest first, with tally and status
GET /api/regen/proposals/:idOne proposal with every vote, including signedPayload and signature
GET /api/regen/proposals/:id/weight/:whoA 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).

EndpointPayloadWho
POST /api/v2/regen/proposalstitle, summary, optional territory, recipient (rouge1), requestedXrge, link (https), durationDaysCurator or eligible holder
POST /api/v2/regen/votesproposalId, choice (yes, no, abstain)Any wallet with snapshot weight
POST /api/v2/regen/proposals/payoutproposalId, txIdCurator, for a passed proposal
POST /api/v2/regen/proposals/cancelproposalId, optional reasonCurator, 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 routes GET /api/mail/inbox, /api/mail/sent, /api/mail/trash and /api/mail/message/:id — use the signed POST /api/v2/mail/folder and /api/v2/mail/message below. 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:

  1. Their wallet (via /api/v2/messenger/wallets/register) — provides the encryption key
  2. 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

DomainPlatform
@rouge.quantWebsite and browser extensions
@qwalla.mailQWalla mobile app (future)

Threading

Mail threading is handled client-side by following the replyToId chain. When viewing a mail, the client:

  1. Fetches both inbox and sent mail
  2. Walks the replyToId chain to build the thread
  3. Displays messages in chronological order
  4. Collapses older messages, expands the latest two

Encryption

Mail uses a Content Encryption Key (CEK) pattern for efficient multi-recipient support:

  1. Generate a random 256-bit AES key (the CEK)
  2. Encrypt subject, body, and attachment with the CEK via AES-256-GCM
  3. 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
  4. Sign the concatenation of all encrypted parts (subject + body + attachment) with ML-DSA-65 (unified signature)
  5. 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
}
FieldTypeDescription
namestringOriginal filename
typestringMIME type
datastringBase64-encoded file content
sizenumberFile size in bytes

Reading Attachments

When fetching mail (inbox/sent/trash), messages with attachments will include the attachment_encrypted field. The client must:

  1. Decrypt using the recipient's ML-KEM-768 private key
  2. Parse the JSON to extract the attachment metadata and data
  3. 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:

  1. The payload is signed client-side with the wallet's ML-DSA-65 private key
  2. The server verifies the signature against the public_key
  3. Timestamp must be within 5 minutes to prevent replay attacks
  4. 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"]
}

enabled is false when the custody address is unset. chainId is 8453 on Base mainnet (84532 on Base Sepolia).

ETH (→ qETH) and USDC (→ qUSDC) are claimable through this endpoint. "BTC" also appears in supportedTokens when 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) and btcMaxNetworkFeeSats (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 / BridgeDepositERC20 events 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
    }
  ]
}
FieldMeaning
ownerPubkeyRougeChain L1 key of the withdrawer — the refund recipient
tokenSymbolqETH / qUSDC / XRGE; authoritative type discriminator (replaces the legacy xrge: tx-id prefix)
statuspending · failed (relayer retrying) · fulfilled · refunded
attemptsFailed relayer release attempts so far
lastErrorLast 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
QueryMeaning
limit1–100, default 25.
beforePagination 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
}
FieldMeaning
kinddeposit (a bridge_mint) or withdrawal (a bridge_withdraw).
asset / externalAssetToken on RougeChain (qETH, qUSDC, qBTC, XRGE) and on the external chain (ETH, USDC, BTC, XRGE; null for a non-bridge token).
amountUnits / decimalsRaw on-chain amount and its decimals: qBTC 8 (1 unit = 1 sat), qETH / qUSDC 6, XRGE 0.
fromChain / toChainrougechain, base, base-sepolia, bitcoin, bitcoin-testnet, or unknown.
rougechainTxId / blockHeight / timestampThe RougeChain transaction, its block and block time (ms).
rougechainAddressrouge1 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.
externalNetworkbase, base-sepolia, or for Bitcoin mainnet / testnet.
externalAddressWithdrawals: the destination signed in the transaction (0x… or a Bitcoin address). Deposits: null.
externalTxHashWithdrawals: the payout transaction, only once the payout was verified (paid). Deposits: null.
status / statusReasonSee below.
statusUpdatedAtWhen the node's payout record last changed (withdrawals), else null.
cursorThis item's pagination position (null in the mempool).

Status

statusstatusReasonMeaning
pendingin_mempoolSubmitted, not in a block yet.
queuedawaiting_payoutWithdrawal accepted on-chain, waiting for the relayer to pay out.
paidpayout_verifiedWithdrawal paid; the payout was verified on the external chain.
paidmintedDeposit minted on RougeChain.
failedrejected_on_chainThe transaction's receipt is failed (nothing was burned or minted).
failedpayout_retryingPayout attempts have failed so far; the relayer keeps retrying (or refunds).
refundedrefunded_on_rougechainThe payout could not complete and the tokens were minted back to the sender.
unknownno_payout_recordThe 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 integer 0–10000; anything else (above 100%, negative, fractional) returns 400 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/sdk 1.11.0+ and the current site sign them there (batchMint({ ..., attributes }); the old batchAttributes option is a deprecated alias). SDK ≤ 1.10.0 sent a batchAttributes payload field, which the node ignores, so attributes passed through those versions were not stored — upgrade the SDK. When signing payloads yourself, use attributes.

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
    }
  ]
}
FieldDescription
price_a_in_bHow many token_b for 1 token_a
price_b_in_aHow 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 fieldTypeRequiredDescription
typestring✅"contract_deploy"
fromstring✅Deployer's signing public key (hex)
wasmstring✅Base64-encoded WASM bytecode (max 1 MiB, must export memory)
noncestring✅Random string, at least 8 characters
timestampnumber✅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 fieldTypeRequiredDescription
typestring✅"contract_call"
fromstring✅Caller's signing public key (hex)
contractAddrstring✅Contract address (hex, lower case)
methodstring✅Method name
argsany JSON❌Arguments ({} when omitted)
gasLimitnumber✅Integer 1 – 10,000,000
attachobject❌From block 190: { "symbol": "XRGE" | "<TOKEN>", "amount": <positive integer> }. Pays the contract. See below
timestampnumber✅Milliseconds since the epoch
noncestring✅Random string

Response:

{
  "success": true,
  "txId": "…",
  "fee": 0.05,
  "preview": { "returnData": { }, "gasUsed": 31234, "events": [] }
}

Call fee: gasLimit × 0.000001 XRGE (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 }
FieldTypeDescription
attach.symbolstring"XRGE" or a token symbol (1–32 letters, digits, _, -; upper-cased by the node)
attach.amountintegerPositive JSON integer: quanta for XRGE (1 XRGE = 1,000,000,000 quanta), raw units for a token. Decimals and strings are rejected
  • attach is 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() and host_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 malformed attach returns "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

FieldTypeRequiredDescription
methodstring✅Method name
argsany JSON❌Arguments
callerstring❌Public key the contract sees as host_get_caller. Required with attach
attachobject❌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:

ModuleResponsibility
REST APIHTTP endpoints via Axum 0.6 (on hyper), default --api-port 5101. Peers sync over this same port — there is no separate P2P port
gRPC APItonic client services (chain, wallet, validator, messenger) on --port, default 4101. Not used for peer traffic
Blockchain EngineBlock production, transaction processing, state management
Validator / PoSStake tracking, proposer selection, rewards
Messenger ServerStores encrypted messages and wallet registrations
Mail ServerStores encrypted mail, name registry
P2P LayerHTTP polling of peers' /api/peers, /api/stats and /api/blocks; HTTP push of new blocks and txs
AMM/DEXLiquidity pools, swap execution, price calculation
BridgeR1 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:

TechnologyPurpose
React 18UI framework
TypeScriptType safety
ViteBuild tool
Tailwind CSSStyling
shadcn/uiComponent library
@noble/post-quantumPQC 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:

ExtensionDescription
RougeChain WalletPrimary browser extension
rougechain-walletSecondary 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.

StoreFormatContent
chain-db/sledBlock data (the chain tip is the last key)
validators-db/sledValidator stakes and state
messenger-db/, mail-db/sledEncrypted messages, mail, and wallet registrations
other *-db/sledPools, NFTs, finality, nonces, social, and other state
chain.jsonlJSON linesLegacy only: imported once into chain-db on first start, then renamed to chain.jsonl.bak

Client Storage

StoreLocationContent
Wallet keyslocalStorageML-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 listlocalStorageBlocked wallet addresses
Mail settingslocalStorageEmail signature preferences
Display namelocalStorageUser's messenger display name

Security Model

PrincipleImplementation
Keys never leave clientAll signing/encryption happens in-browser
Server is untrustedMessages and mail are stored only as ciphertext (the server still sees metadata such as sender, recipients, and timing)
Quantum-resistant L1NIST-approved PQC algorithms for L1 signatures and messaging. Base-side bridge custody is classical today — see Security Overview
BIP-39 mnemonicsWallets 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 encryptionMessages encrypted for the sender and every recipient

v2 Signed Write Requirements

Every write through /api/v2 is authenticated by signature, not by a session:

  1. The request is a signed envelope { payload, signature, public_key }.
  2. The signature is an ML-DSA-65 signature over the canonical payload bytes.
  3. payload.timestamp must fall within a ±5-minute window of server time.
  4. payload.from must equal the signing public key (public_key).
  5. A signature replay guard (SEEN_SIGNATURES, keyed by sha256(signature), 5-minute window, persisted to replay_guard.json across restarts) rejects any signature already seen in-window.
  6. 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:

AlgorithmQuantum Threat
RSABroken by Shor's algorithm
ECDSABroken by Shor's algorithm
SHA-256Weakened (Grover's algorithm)
ML-DSASecure
ML-KEMSecure

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:

ComponentSize
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:

ComponentSize
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:

ComponentLibrary
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

  1. 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 localStorage in plaintext. Active session keys (and a new wallet before its password is set) are held in sessionStorage. Wallets that older versions stored in plaintext must be secured with a password before further use, which deletes the plaintext copy.
  2. Entropy - Keys use cryptographically secure random number generators
  3. Side channels - Library implementations are designed to be constant-time
  4. 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 winterfell 0.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.

PropertyValue
Librarywinterfell 0.13 (Meta) — no zero-knowledge mode
Hash functionBlake3-256
Quantum resistance✅ Hash-based (no EC)
Proof typeBalance 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:

  1. Value conservation: sender_after = sender_before - amount
  2. Running hash accumulation: hash[i+1] = hash[i] + sender_before[i] * amount[i]
  3. 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:

  1. EVM Receipt Verification — tx status, recipient, sender, confirmations
  2. SHA-256 Commitment Nullifiers — SHA-256("ROUGECHAIN_BRIDGE_V1" || tx_hash || address || amount) prevents double-claims
  3. 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

FeatureMessengerMail
PurposeReal-time chatAsync email
EncryptionML-KEM-768 + AES-GCMML-KEM-768 + AES-GCM
AddressesWallet public keys@rouge.quant / @qwalla.mail names
MediaImages, videos (auto-compressed)Text + attachments (up to 3 MB)
Self-destruct✅ Configurable timer❌
Folders—Inbox, Sent, Trash
ThreadingConversationsReply chains
Server seesCiphertext + 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:

  1. Generate a random 256-bit AES key (the CEK)
  2. Encrypt all mail content (subject, body, attachment) once with the CEK via AES-256-GCM
  3. For each recipient (and the sender): KEM-wrap the CEK using their ML-KEM-768 public key
  4. 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

  1. Go to Messenger in the sidebar
  2. Your wallet is automatically registered
  3. Enter a recipient's public key to start a conversation
  4. 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:

ActionResult
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

DomainPlatform
@rouge.quantWebsite and browser extension
@qwalla.mailQWalla 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

  1. Go to Mail in the sidebar
  2. Click Register Name and choose your @rouge.quant address
  3. Send encrypted mail to any registered name
  4. 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 key
  • timestamp — 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

PropertyDetails
Quantum-resistantML-KEM-768 key encapsulation (FIPS 203)
No forward secrecyEach 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 contentServer stores only ciphertext and cannot read messages, but it does see metadata (who talks to whom, when, and message size)
Client-side cryptoAll encryption/decryption happens on the client
Per-member ciphertextSender and every recipient can each decrypt their own copy (two ciphertexts in 1:1, wrapped CEK per member in groups)
Signed requestsAll API calls authenticated with ML-DSA-65 signatures
Anti-replayNonce + timestamp prevents request replay attacks
TOFUKey fingerprint tracking with change detection
Unified signaturesMail signed over all encrypted parts (subject + body + attachment)
CEK multi-recipientEfficient per-recipient key wrapping without re-encryption
Atomic name registryCompare-and-swap prevents race conditions on name claims
Directory ownershipSince node 1.6.3: only the owning signing key can replace a directory entry; a taken encryption key is refused
Node-hostedNot on-chain: stored by the node you talk to, not replicated by consensus
Vaulted keysPrivate 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:

FeatureDetails
Tab badgesChat and Mail tabs display a numeric badge when unread items exist
TooltipsHovering a badged tab shows "3 unread messages" or "2 unread emails"
Extension icon badgeThe combined unread total (chat + mail) is shown on the browser toolbar icon via chrome.action.setBadgeText
System notificationsNative OS notifications for new messages, new mail, received/sent tokens, contract events, staking, and balance changes
Badge clearingViewing 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:

FeatureDetails
Tab badgesExpo Router tabBarBadge on Chat and Mail tabs
Initial pollOn app launch, actual unread counts are fetched from the server so badges are accurate from the start
Real-time updatesWebSocket events increment the badge for new messages and mail
Push notificationsExpo push notifications for messages, mail, and transfers (requires registerPushToken)
Badge clearingNavigating 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_count from each conversation returned by POST /api/v2/messenger/conversations/list
  • Mail: Count inbox items where the label's is_read field is false from POST /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

FeatureDescription
PostsStandalone text posts (max 4000 chars) with threaded replies via replyToId
TimelineGlobal timeline (newest first) and personalized following feed
LikesToggle likes on posts or tracks — reuses the same endpoint
RepostsToggle reposts on any post
CommentsTrack-level comments with pagination
FollowsFollow/unfollow any user; follower and following counts
Play countsRecord plays on tracks (debounced per session)
TipsSend XRGE tips to creators via rc.transfer() — settles on-chain

API Endpoints

Read (unsigned GET):

  • GET /api/social/timeline — Global timeline
  • GET /api/social/post/:postId — Single post with stats
  • GET /api/social/post/:postId/stats — Post engagement stats (likes, reposts, replies)
  • GET /api/social/post/:postId/replies — Threaded replies
  • GET /api/social/user/:pubkey/posts — User's posts
  • GET /api/social/track/:trackId/stats — Track stats (plays, likes, comments)
  • GET /api/social/track/:trackId/comments — Track comments
  • GET /api/social/artist/:pubkey/stats — Artist stats (followers, following)
  • GET /api/social/user/:pubkey/likes — User's liked IDs
  • GET /api/social/user/:pubkey/following — User's followed artists

Write (v2 signed POST):

  • POST /api/v2/social/post — Create a post
  • POST /api/v2/social/post/delete — Delete your post
  • POST /api/v2/social/like — Toggle like
  • POST /api/v2/social/repost — Toggle repost
  • POST /api/v2/social/comment — Post a comment
  • POST /api/v2/social/comment/delete — Delete your comment
  • POST /api/v2/social/follow — Toggle follow
  • POST /api/v2/social/play — Record a play
  • POST /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_balance operate 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:

FunctionDescription
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) → i32CONTRACT_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() → i32Length in bytes of the call's JSON arguments ({} when none)
host_read_args(buf, len) → i32Copy 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.

FunctionResult
host_token_balance(sym, slen, addr, alen) → i64Token balance in the token's raw units (-1 invalid symbol)
host_token_transfer(sym, slen, to, tlen, amount) → i32Send the contract's tokens: 0 ok, 1 insufficient, 2 invalid
host_nft_owner(col, clen, id, out, cap) → i32Owner written to out; -1 not found, -2 buffer too small
host_nft_transfer(col, clen, id, to, tlen) → i32Send 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) → i32Create 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) → i64Mint 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) → i3232 pseudo-random bytes per call. The sender can grind it — see below
host_block_hash(height, out) → i32From 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() → i64From 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) → i32From 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) → i32CONTRACT_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) → i32CONTRACT_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:

FunctionReturns
host_nft_royalty_bps(col, clen) → i32Basis 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) → i32Bytes 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, a rouge1… 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: bps 0, 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 with royaltyBps above 10000 (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 / 10000 in integer quanta (round down) and pay the seller price − royalty. Never use floats.

Example: escrow marketplace

contracts/nft_marketplace is a small, commented reference (prebuilt nft_marketplace.wasm):

  1. 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). Returns listed {"listing":N,"seller":…,"collection":…,"token_id":…,"price":…}. seller is the caller exactly as stored — for a wallet, its raw public key hex, not the rouge1 address — and is what cancel compares and buy pays.

  2. The seller escrows the NFT with a plain wallet nft_transfer to the contract address (no sale price).

  3. buy {"listing":N} with attach: {"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 with host_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":…}.

  4. 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 methodArgsReturns
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_id is null). 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.

FunctionReturns
host_get_chain_id(buf, len) → i32Writes 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 -.
  • amount is 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.
  • attach is 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

OperationFee
Publish (deploy)10 XRGE flat
CallgasLimit × 0.000001 XRGE — the signed gas limit, charged up front
Payable callThe call fee, plus the attached payment, which is paid only if the call succeeds (see Payable calls)
QueryFree (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:

MethodArgsDescription
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 metadata
  • bal:{account} — Account balances
  • allow:{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:

ToolDescription
list_contractsList all deployed contracts
get_contractGet contract metadata
get_contract_stateRead state (single key or full dump)
get_contract_eventsStored events (limit, before, tx)
query_contractFree read-only call (POST /api/contract/:addr/query); optional attach previews a paid call
publish_contractWrite mode: sign and publish WASM with the server's wallet (10 XRGE)
execute_contractWrite mode: sign a state-changing call with the server's wallet; optional attach (amount_xrge decimal for XRGE, integer amount for tokens)
get_tx_receiptReceipt 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

FunctionReturnsDescription
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 writtenRead 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_transfer calls 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 -2 from host_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.

FunctionReturnsDescription
host_pqc_verify(pk, pklen, msg, msglen, sig, siglen)1 valid, 0 invalid, -1 errorML-DSA-65 signature verification
host_pqc_pubkey_to_address(pk, pklen, out, outlen)bytes writtenDerive rouge1... bech32m address from raw pubkey
host_pqc_hash_pubkey(pk, pklen, out)32 on successSHA-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

  1. Base fee adjusts ±12.5% per block based on block fullness (target: 10 txs/block)
  2. Floor: minimum base fee of 0.001 XRGE
  3. 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
  4. 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

TemplateWhat it shows
CounterThe smallest useful contract: storage, JSON arguments, return values, events, the caller
NFT marketplaceEscrow listings, buying with attached XRGE, royalties paid by the contract
XRGE stakingStaking with a reward pot that streams over time, a 3-day unstake delay
Loot rollA game: pay to commit, settle two blocks later from a block hash
Royalty splitterReceiving 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
  • amount is in quanta. A negative amount is rejected.
  • A contract can only transfer its own balance — host_transfer always debits the executing contract's address.
  • If the contract's balance can't cover amount, the transfer returns 1 and 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:

  1. Conservation — the moves must net to exactly zero. A contract can never mint or burn XRGE, only move it.
  2. 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 transfer proposals: funds move from the wallet creator's balance

API Endpoints

MethodEndpointDescription
GET/api/multisig/walletsList all multi-sig wallets
GET/api/multisig/wallet/:idGet wallet details
GET/api/multisig/wallet/:id/proposalsList proposals for a wallet
GET/api/multisig/wallets/:pubkeyFind 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_approve transaction 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.sendTransaction does 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

ModeHowWhat the agent can do
Read-only (default)no wallet envAll query tools. Safe to expose anywhere.
Read + writeset 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_wallet tool — and keep it out of shared configs. Alternatively supply raw hex keys via ROUGECHAIN_PRIVATE_KEY + ROUGECHAIN_PUBLIC_KEY.

Host note: ROUGECHAIN_URL must be the API host (api.rougechain.io), not rougechain.io, which serves the web app for every /api/* path.

Available Tools (63)

Wallet (always available)

ToolDescription
generate_walletCreate a fresh ML-DSA-65 wallet (mnemonic + address); not persisted
wallet_infoShow the configured signer, address, live balance, and whether writes are enabled

Read tools (always available)

CategoryTools
Chainget_chain_stats, get_block, get_latest_blocks
Walletget_balance, get_transaction
Tokenslist_tokens, get_token, get_token_holders
DeFilist_pools, get_swap_quote
NFTslist_nft_collections, get_nft_collection
Validatorslist_validators
Contractslist_contracts, get_contract, get_contract_state, get_contract_events, query_contract (free read-only dry run), get_tx_receipt
Socialget_global_timeline, get_post, get_user_posts, get_post_replies, get_track_stats, get_artist_stats
Mail & Messagingresolve_name, reverse_lookup_name, list_messenger_wallets
Governance & Feeslist_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)

CategoryTools
Valuesend_transaction, burn_tokens, stake, unstake, request_faucet (testnet)
Tokenscreate_token, mint_tokens, update_token_metadata, claim_token_metadata
DeFiswap, create_pool, add_liquidity, remove_liquidity
NFTsnft_create_collection, nft_mint, nft_batch_mint, nft_transfer, nft_burn, nft_lock, nft_freeze_collection
Namesregister_name, release_name
Socialcreate_post, delete_post, repost, follow, like_track, comment_on_track
Bridgebridge_withdraw
Contractspublish_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

ExtensionDescriptionStore
RougeChain WalletPrimary browser extensionChrome 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.quant addresses
  • 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

  1. Visit the extension page on the Chrome Web Store
  2. Click Add to Chrome
  3. The extension icon appears in your toolbar

From Source

cd apps/extension
npm install
npm run build
  1. Open chrome://extensions (or edge://extensions, brave://extensions)
  2. Enable Developer mode
  3. Click Load unpacked
  4. Select the apps/extension/dist folder

Firefox

cd apps/extension
npm install
npm run build
  1. Open about:debugging#/runtime/this-firefox
  2. Click Load Temporary Add-on
  3. 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() throws e.getAddress is not a function. That method does not exist — RougeChain is not EVM-compatible. Use connect() (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

MethodSignatureDescription
isRougeChainbooleanProperty — 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)voidSubscribe to events (e.g. account/connection changes).
removeListener(event, cb)voidRemove 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

FeatureImplementation
Vault encryptionAES-256-GCM with a PBKDF2-derived key (600k iterations); a password (min 8 chars) is mandatory at wallet creation
Auto-lockConfigurable timer via background service worker
Key storageEncrypted 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.
SigningML-DSA-65 (FIPS 204) — quantum-resistant
EncryptionML-KEM-768 (FIPS 203) — quantum-resistant

Permissions

The extensions request minimal permissions:

PermissionPurpose
storageStore encrypted wallet data
alarmsAuto-lock timer
notificationsTransaction alerts
Host permissionsConnect 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

WalletsignMessage
RougeChain Wallet browser extensionfrom version 1.8.0
Qwalla in-app browserafter its next update (not in the builds released before this page)
rougechain.io built-in wallethas 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 })

  • message is 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 with Site 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 type or tx_type field, or with from and timestamp) is refused with signMessage 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
  • \x19 is the single byte 0x19.
  • message_bytes is 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_json of 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
LineRule
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.
2The signer's rouge1… address, lower case.
3Empty.
4Optional statement: one line of text.
5Empty. (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.

FunctionReturnsNotes
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)booleanfalse for anything that is not a valid signature of exactly that message by that key, including malformed input. Never throws.
messageSigningBytes(message)Uint8ArrayThe exact bytes that are signed.
createSignInMessage(fields)stringThrows on an invalid field.
parseSignInMessage(text)fields or nullnull 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:

errorMeaning
malformed_messageThe text is not a canonical sign-in message.
invalid_signatureThe signature is not valid for this text and public key.
address_mismatchThe address in the message is not the address of publicKey.
domain_mismatchThe message is for another site than expectedDomain (compared case-insensitively).
nonce_mismatchThe nonce is not expectedNonce.
chain_id_mismatchThe chain id is not expectedChainId.
issued_in_futureIssued At is later than now plus maxClockSkewMs (default 60 seconds).
expirednow is at or after Expiration Time.
too_oldOnly with maxAgeMs: Issued At is longer ago than that.

Token gating: a complete example

The flow:

  1. The browser asks your server for a nonce.
  2. The browser connects the wallet, builds the sign-in message and calls signMessage.
  3. Your server runs verifySignIn, marks the nonce used, and starts a session for the address.
  4. Your server reads the wallet's holdings from a node and decides.
  5. 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):

EndpointReturnsUse 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/:tokenIdthe NFT (owner is the holder's public key), or 404Who 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.

EndpointExact fieldsMeaning
GET /api/balance/:publicKey/:tokenSymbolbalance_raw (string), decimalsThe balance in integer base units; balance_raw / 10^decimals is the displayed amount
GET /api/balance/:publicKeybalance_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" is BigInt(balance_raw) >= 1n. The bridged assets differ: qUSDC and qETH use 6 decimals and qBTC uses 8. GET /api/tokens lists every token's decimals. 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 XRGE is a reserved symbol that no token can use. GET /api/balance/:publicKey/XRGE returns the native balance with native: true, decimals: 9 and balance_raw in quanta. Nodes before this change answer 0 for XRGE on the per-token endpoint and have no exact fields; on those, read XRGE from balance in GET /api/balance/:publicKey (display units). A client can tell the two apart by the presence of balance_raw.
  • Symbols are case-sensitive, and an unknown symbol returns 0 rather than an error, so check the symbol against GET /api/tokens when you configure a gate.

What a verifier must check

verifySignIn does the cryptography. The rest is yours:

  1. 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.
  2. 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. verifySignIn cannot know if a nonce was used before.
  3. Expiry. Put an Expiration Time a few minutes ahead in the message, or pass maxAgeMs. Keep your server's clock synchronised.
  4. Chain ID — required. A verifier MUST check the Chain ID line: pass the network you gate on as expectedChainId (verifySignIn requires it and fails with chain_id_mismatch otherwise), so a sign-in made for one network is never accepted for another.
  5. Use the address verifySignIn returns. Do not trust an address or public key sent alongside the message.
  6. 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_id in /api/health if the URL is configurable.
  7. 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_id only 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 signTransaction only 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

statusMeaning
finalizedThe 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-finalizedThe 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-foundNo node knows the transaction, the block at the height a node named does not contain it, or the block could not be fetched.
invalidSomething 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 with critical: false is informational and does not change status.

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.

  1. 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 for txHash. The height a node reports for the transaction is only a hint.
  2. The block's transaction list matches its header. It recomputes the header's tx_hash from the full transaction list, so a transaction added, removed, altered or reordered is detected.
  3. 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's chain_id is the chainId you passed.
  4. The block is finalized. It takes the finality certificate for that height (from the next block's parent_commit, or from GET /api/finality/:height) and checks every precommit: its ML-DSA-65 signature over ROUGECHAIN_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 quorum floor(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.
  5. The nodes agree. Every node in nodes is 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 result invalid. 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 requires tip − height + 1 blocks. It is optional: a verified certificate already makes a block final.
  • Pass fetch to use your own HTTP client, and timeoutMs (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: true and max_supply are signed only when mintable is true, so a fixed-supply token's payload is the same as before. A maxSupply without mintable, a maxSupply below totalSupply, 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 }) and createSignedTokenMint(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, mintTokens sent an unsigned request that the node always rejected, and createToken dropped mintable / 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 generic signRequest(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 }
FunctionDescription
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 call rc.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 / callContract helpers used node-signed endpoints that are retired (deploy returns 410; call is a dry run only). They are deprecated; use rc.contracts.

EIP-1559 Fee Info

const feeInfo = await rc.getFeeInfo();
// → { base_fee, total_fee_suggestion, total_fees_burned, block_height }

Environment Support

EnvironmentRequirements
BrowserAny bundler (Vite, webpack)
Node.js 18+Works out of the box
Node.js < 18Provide node-fetch polyfill
React NativeInstall 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

CheckHow
Node is runningcurl http://127.0.0.1:5100/api/health
Peers are correctMake sure --peers includes /api (e.g. mainnet --peers "https://api.rougechain.io/api")
Genesis + chain-id match the networkMainnet 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 blockingPeers reach your node through its --public-url (the API port behind your reverse proxy); there is no separate P2P port
Network is reachablemainnet 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 --peers with 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 --peers is set (solo nodes without peers don't receive blocks)
  • If you're mining solo (--mine without --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:

  1. Set --public-url — Without this, your node is invisible to the network. Other nodes can't sync from you.
  2. Check your firewall — Your --public-url (the HTTPS reverse proxy in front of your API port) must be reachable from the internet
  3. Check you are the designated proposer — only the validator with the most stake proposes blocks (see Staking Issues)
  4. 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:

CauseFix
Wrong signatureEnsure you're signing with the correct private key
Duplicate transactionWait a moment and retry — the previous tx may still be processing
Node not syncedCheck GET /api/health — your node's height should match the network
Stale nonceRefresh 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_ORIGINS or run with --dev (see Configuration → Browser access). The public testnet/api.rougechain.io endpoints already send Access-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 .pqcbackup file, 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 .pqcbackup file 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's node-keys.json key and your staked key must be the same key — see Becoming a Validator.
  • Ensure the --mine flag 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. Check designated_proposer_next in GET /api/stats or proposer in GET /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

  1. Confirm the EVM transaction was confirmed on Base mainnet (Base Sepolia only if you are on testnet)
  2. Deposits are credited after the required confirmation depth (default 6 Base blocks), then the deposit watcher claims them
  3. Check the bridge config: GET /api/bridge/config
  4. 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

  1. Install Visual Studio Build Tools with the C++ desktop workload
  2. Install Rust — make sure cargo is in your PATH
  3. 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:5100 for 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/submit for 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_integrity is null). 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/v2 transactions and of signed mail, messenger, name and vote requests; chain_id inside a rougechain CLI envelope. Wallets take it from their network setting, check it once per session against the node's /api/health chain_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_MISMATCH at the API, mempool and producer. New operator flag REQUIRE_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 are None on every network; history replays identically.
  • Clients: site (rougechain.io), @rougechain/sdk 1.15.0, browser extension 1.9.0 (approval screen shows the network and refuses another one), rougechain CLI 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-submit gets the checks of a single /api/v2 submission (5-minute timestamp window, the shared signature replay guard, the network rule, from = signer, signed account_nonce); a batch with any failing item is refused as a whole and uses up no signature. GET /api/balance/:publicKey/XRGE returns 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 --version reports 1.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/sdk 1.13.0 (in the repository; not yet on npm) adds signMessage, verifyMessage, createSignInMessage, parseSignInMessage and verifySignIn.
  • New page: Wallet Authentication, with the exact formats and a complete token-gating server. No node change; signTransaction is 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_schedule 235 / 235 / 245. The height-245 rule is in force: a block carrying a slash transaction, 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/v2 routes.
  • 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, a slash transaction, 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 legacy token_approve / token_transfer_from types; allowances through approve / transfer_from are 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_royalty is null). From its height contracts get two read-only host functions: host_nft_royalty_bps(col, clen) → i32 (0–10000, -1 not found) and host_nft_royalty_recipient(col, clen, out, cap) → i32 (length, -1 not found, -2 buffer too small). The recipient is the canonical ledger key the wallet royalty path credits, so a contract that sells NFTs can pay royalty with host_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_collection with royaltyBps above 10000 (over 100%) or not an exact integer (negative, fractional, a string, or wider than 16 bits — previously truncated) is rejected at creation: the API returns 400 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_minting is null). From its height a token can be created with mintable: true and an optional integer max_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 advances total_minted deterministically; once a mintable token exists the state root commits the mint ledger.
  • Before activation nothing changes on-chain. The API now refuses mintable / max_supply on 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 .pqcbackup to 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 .pqcbackup that 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 sent batchAttributes, which the node ignored, so batch-minted NFTs got no attributes. Requires @rougechain/sdk 1.11.0; no node change.
  • The SDK / core create_token builders 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 localStorage unencrypted before the password step, and stayed there if you left onboarding (the password was optional). Now the wallet is held only in the tab's sessionStorage until 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 with host_get_attached_amount / host_get_attached_symbol. Enables entry fees and shops. Mandatory node upgrade before block 190.
  • loot_roll's roll now 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 (binary 718200bc…); 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_random rolls 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_roll now uses roll + 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.000001 XRGE) 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, Failed receipts for reverted calls. SDK 1.9.0 rc.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 with existing: true instead 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/list with folder: "trash"), conversations/restore brings it back, purge: true waives 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

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 Transfer log. 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.social namespace 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-instant model 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_transfer handler now correctly sets token_symbol on TxPayload (previously only set token_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/tokenOut URL params to the swap page, correctly pre-filling both sides of the swap
  • PWA wallet persistence — Wallet private keys now persist in localStorage when 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.sign argument 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; lastKnownUnread persisted to chrome.storage.local to survive service worker restarts
  • QWalla @qwalla.mail domain — Mail addresses now display as @qwalla.mail throughout 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-v2 to invalidate stale assets on existing PWA installs
  • Session-only private keys policy updated: localStorage used 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 of localStorage; encrypted wallet blob persists in localStorage (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 wallet parameter for request signing
  • WHITEPAPER.md updated 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, ResolvedName exported 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 SwapQuoteParams now includes required tokenOut field

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/nonce for 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.quant addresses, 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/sdk npm package for building dApps
  • Docker Support — One-command node deployment with docker run
  • Node Dashboard — Built-in web dashboard at http://localhost:5100 when 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
  • --peers URL now requires /api suffix (e.g., https://testnet.rougechain.io/api)
  • v1 endpoints that accept private keys are disabled by default (use --dev to 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