mnrMonero Network Relay
Get a free token

mnr — Protocol & Development Architecture

an RPC network for Monero

The protocol and component design behind Stage 2, the permissionless operator network. Rendered from the architecture document in the code repository with the build plan and internal decisions left out. See the roadmap for the principles in brief.


1. What is being built

A permissionless network in which independent operators run monerod behind a small agent, relayers aggregate many operators into one verified, cached, metered endpoint, and clients (wallets, swap backends, autonomous agents) pay in XMR — either to a relayer for one URL, or, later, directly to operators. The protocol, operator agent, relayer, and client library are open source (AGPL-3.0 for binaries, CC-BY for the spec). The founders participate as the first operators and the reference relayer; they do not own the network.

Design principles, in priority order:

  1. Verify, don't trust. Wherever Monero data is self-authenticating (block hashes, tx hashes, header chains), the relayer or client checks it. Trust is used only where verification is impossible (mempool, node info), and there it is replaced by agreement across independent operators.
  2. No token, no stake, no slashing. Operators are paid in XMR for verified work; bad work is recorded in a public, cryptographically verifiable fault log that anyone can weigh. Nothing is locked, nothing is confiscated.
  3. Stock wallets must work. A wallet must be able to use the network with nothing more than --daemon-address. Direct multi-operator verification is a client-library feature, not a requirement.
  4. Hobbyist operators must be able to join in ten minutes with a one-line install, no view key, no public IP if they use Tor.
  5. The reference relayer must be replaceable. Everything a relayer knows that others would need (operator directory, fault log, settlement statements) is published in a form another relayer can consume.
  6. Both sides of the market are paid from day one. Operators through the pool with a probation lane for newcomers; distributors (wallets, agents, interfaces) through an affiliate share. These are the two THORChain mechanisms that demonstrably bootstrapped an anonymous-operator network, and they are protocol features here, not marketing programmes.

Out of scope, permanently: monero-wallet-rpc methods, mixnets / decoy traffic, any on-chain protocol changes to Monero. Out of scope for the first 18 months: I2P ingress (planned, not built), payment channels of any kind, cross-relayer traffic sharing.


2. Roles, identities and trust boundaries

2.1 Roles

Role Runs Holds Sees Paid by
Operator monerod + mnr-agent Ed25519 operator key; Monero payout subaddress; no view key needed Request bodies from relayers (not client IPs, unless the relayer is misconfigured) Relayers (settlement pool), later clients directly
Relayer mnr-relay (+ mnr-store storefront, monero-wallet-rpc view-only) Ed25519 relayer key; client tokens (hashed); its own header chain; cache; metering DB Client IPs (unless client uses the relayer's .onion), request bodies, everything Clients (subscriptions / XMR402 credit)
Client Any Monero software, optionally mnr-client A bearer token (relayer mode) or an XMR402 credit (direct mode) — —
Affiliate Nothing (a wallet, agent framework, swap interface or integrator that puts its id on tokens it brings) Ed25519 affiliate key; payout address — Relayers (15% of referred client revenue, from the relayer's share)
Directory signers mnr-directory signer tooling Ed25519 signer keys (threshold t-of-n, initially 1-of-1 = founders, target 3-of-5 by month 15) Operator/relayer registration records Nobody (public good)

2.2 Identity and keys

2.3 Trust boundaries (who can hurt whom)

Threat Mitigation
Operator serves wrong block/tx/header Relayer verifies hash; issues signed fault record; operator is deprioritised network-wide.
Operator serves wrong get_info/fee/mempool Majority agreement across ≥3 operators for consensus state; mempool is annotated as per-operator and never treated as authoritative.
Operator under-reports or relayer under-pays Both meter; settlement statements are signed and published; discrepancy > 2% is visible to everyone. There is no proof-of-serving; the remedy is reputation and exit.
Relayer logs or de-anonymises clients Cannot be prevented by protocol; mitigated by open source, .onion ingress, direct mode, and multiple relayers. Stated plainly in docs.
Sybil operators (one person, many identities) Pool payout is pro-rata by verified served work, so Sybils earn nothing extra per unit of capacity; diversity scoring (ASN, /24, latency fingerprint) limits how many "independent" votes one host can have in agreement checks.
Relayer and operator are the same entity (self-dealing) Allowed and expected (we are both). Settlement statements show it; clients can prefer relayers with more external supply.
Directory signers are compromised or coerced Records are individually operator-signed, so a malicious snapshot can omit operators but not forge them; multiple mirrors; threshold signing from month 15; relayers cache the last good snapshot and continue on stale data with a warning.
Hard-fork desync across independent operators Agent enforces a minimum monerod version published in the directory snapshot; operators below it are marked stale 2 weeks before a fork height and excluded from consensus checks after it.

3. Protocol specification (v0 — what the spec document will contain)

The spec is a separate, versioned document (mnr-spec). This section fixes the decisions so engineering can start; wire formats are finalised in the spec.

3.1 Directory

Operator record (CBOR, signed by operator key):

{ v: 1, id: <op pubkey>, endpoints: [{kind: "https"|"onion"|"i2p", url, spki?}],
  node: {pruned: bool, restricted: true, version: "0.18.x", chain: "mainnet"|"stagenet"},
  capacity: {rps_light: u32, mbps_stream: u32},         // self-declared, verified by relayers
  payout: [{addr: <monero subaddress or integrated addr>, bps: u16}, …],   // splits must sum to 10000; see "pooled operators" below
  min_payout_xmr: f64,
  accepts_direct: bool,                                  // phase 3
  votes: {param_name: value, …},                         // optional; §3.7
  contact: <optional onion/simplex/matrix string>, ts: unix, sig }

Pooled operators (borrowed from THORChain's pooled nodes): payout is a list of splits, so a person with capital can fund a box that a skilled operator runs, with the split (e.g. operator 2,000 bps, funder 8,000 bps) declared in the signed record. Relayers pay each address its share in the same settlement transaction. No protocol state beyond the record; the two parties' agreement is their own business.

Directory snapshot (CBOR, threshold-signed): {v, seq, ts, min_monerod_version, fork_height?, operators: [record…], relayers: [record…], fault_log_urls: [], sigs: []}. Distributed over: HTTPS mirrors (≥3, different hosts), .onion, and a DNS TXT seed carrying the current mirror list and snapshot hash (like Monero's seed nodes). Snapshot size at 500 operators ≈ 250 KB; refreshed hourly; sequence numbers prevent rollback.

Registration: the agent POSTs its signed record to any directory mirror; the record is queued; a relayer (initially ours) probes the endpoint for correctness (height, restricted RPC, version, TLS pin) and the signer includes it in the next snapshot. There is no fee and no bond. Removal is by the operator (signed tombstone) or by the signers (record omitted; reason published in the fault log).

3.2 Relayer ↔ operator session

  1. Relayer opens TLS (pinned) or Tor connection to the agent's endpoint and requests /mnr/v1/session with {relayer_id, nonce_r}.
  2. Agent replies {nonce_a}; relayer returns sig_relayer(nonce_a || nonce_r || op_id); agent verifies against the relayer's published key (agent fetches the directory too) or, for unknown relayers, its own allow_unknown_relayers setting (default true, so new relayers can bootstrap; operators may restrict).
  3. Agent issues a session id (random 128-bit) valid 24 h; all subsequent RPC requests carry Mnr-Session: <id>. Requests without a valid session hit the agent's public policy: by default deny (private operator), optionally free-tier (small unauthenticated allowance — how public nodes join without changing their habits).
  4. Both sides count per-session: light_requests, stream_bytes, errors, first_ts, last_ts. Counters are persisted every 10 s (SQLite) so restarts do not lose more than 10 s.

Metering unit: work units (WU) = light_requests × 1 + stream_MB × 20 (one MB of get_blocks.bin costs an operator roughly what 20 light calls do; the coefficient is a directory-snapshot parameter so it can be tuned network-wide without a release).

3.3 Settlement

3.4 Verification rules (the heart of the network)

The relayer maintains its own header chain: for every height, (hash, prev_hash, timestamp, height) — 32 + 32 + 8 + 8 bytes ≈ 80 B × ~3.5 M blocks ≈ 280 MB on disk, built once from operators by majority and then extended at the tip by agreement. RandomX proof-of-work is not verified (too expensive, and irrelevant: an operator that fabricates a PoW-valid alternate chain is a 51% attacker, which is not our threat model); linkage and majority are.

Data How verified On failure
get_block / get_block_header_by_hash Recompute block hash from the returned blob (Keccak over hashing blob: header ‖ tx-tree-hash ‖ tx count). Must equal the requested hash, or the header-chain hash at the requested height. Fault record; retry on next operator.
get_block_header_by_height, get_block_headers_range Header hash must equal the relayer's header chain at that height; if the relayer's chain is shorter, extend by majority first. Fault record.
/get_transactions For each tx: Keccak(tx blob) = txid (pruned txs: verify the pruned-hash form). block_height/in_pool fields must be consistent with header chain (height ≤ tip). Fault record.
/get_blocks.bin (stream) Relayer parses the epee stream incrementally, hashes every block, and checks linkage (prev_id) and equality with the header chain. CPU cost is real (§6.2); the relayer may sample-verify (every block header linkage, full hash on 1-in-k blocks, k configurable, default 4) when overloaded and record the verification level in metrics. Abort stream, fault record, restart from last verified height on another operator (clients handle short reads).
/get_outs.bin, get_output_distribution, get_output_histogram Immutable but not self-authenticating from a single response. Two-operator agreement on first request; cached with the tip−10 rule afterwards. Both answers cached as "disputed", third operator breaks tie; loser gets fault record.
get_info, get_height, get_last_block_header, get_fee_estimate, hard_fork_info Majority of ≥3 operators on height and top_block_hash (SWR-cached, §3.6); fee estimate = median. Node-specific fields normalised (see gateway plan §3.3). Degraded mode: serve highest-height operator, suspend immutable cache writes, alert.
Mempool methods Not verifiable. Annotated Mnr-Operator: <id>; never cached. —
send_raw_transaction Broadcast to all healthy operators; success if ≥1 OK. Result header Mnr-Relayed: k/n. If all reject with the same reason (e.g. double spend), return it verbatim.

Fault record (CBOR, signed by relayer): {relayer_id, op_id, ts, method, request_hash, response_hash, expected, got, evidence_url?, sig}. The evidence (request + response blobs) is stored by the relayer for 30 days so anyone can reproduce the check. Fault logs are append-only files served over HTTPS/onion; relayers and mnr-client fetch the logs of relayers they choose to believe (initially the reference relayer's; a relayer can mark others' logs as trusted in config). An operator's fault rate = faults / verified requests over 30 days, decayed; ranking multiplies capacity weight by (1 − fault_rate)^8, so 5% faults roughly halves an operator's traffic and 20% removes them from the pool for practical purposes.

3.5 Client access

Relayer mode (v1): exactly the token scheme from the gateway plan §3.1 — path token or Basic auth, 256-bit, hashed at rest, rotation with grace — but issued by the relayer's storefront. Purchases via invoice page or XMR402: the storefront responds 402 with {amount_xmr, address, memo, expires}; the client pays; on 10 confirmations (or a relayer-chosen lower threshold for small credits) the storefront issues the token. Credits are denominated in WU (same unit as settlement) so that a client's balance and an operator's payout are commensurable. Purchase requests may carry affiliate_id (query parameter on the invoice page, header in the XMR402 flow); it is bound to the token for its lifetime and shown on the client's statement.

Direct mode (phase 3): mnr-client fetches the directory, picks k operators by diversity + fault rate + latency, and performs the verification rules itself. Payment: XMR402 credit purchased from an operator whose accepts_direct is true (the agent gets an optional view-only wallet for this; hobbyists stay relayer-only). Stock wallets never use direct mode; it is for backends, agents and wallet developers who embed the library.

3.6 Caching and policy

Identical to the gateway plan §3.3 (tip−10 immutable rule, SWR 1/5/15 for consensus state, per-tx cache, never cache mempool, method allow-list, per-method timeouts) with one change: cache keys include the verification level (full | sampled | agreed) so that a sampled-verified stream never satisfies a request that asked for full verification.

3.7 Network parameters and work-weighted votes (THORChain's "mimir", without a token)

The directory snapshot carries a small set of network parameters: wu_stream_per_mb (20), op_share (0.60, band 0.50–0.70), probation_weight (0.10), affiliate_default (0.15), min_monerod_version, fork_height, tip_safety_depth (10). Until month 15 the directory signers set them. From then on, an operator may include a votes map in its signed record; when the signers build a snapshot they compute, per parameter, the median of votes weighted by each operator's verified WU over the trailing 90 days (from the published settlement statements of all relayers the signers recognise) and adopt it if at least 40% of network-wide verified WU has voted. Voting weight is work actually served and verified, not capital and not identity — the only Sybil-resistant quantity this network has. Relayers are not obliged to follow the snapshot's op_share, but the reference relayer does, and their published value is visible to operators choosing where to serve.


4. Component design

All core components are Rust (tokio, hyper/axum, rustls). Rationale: epee parsing and hashing on streamed data is CPU-bound and must be memory-safe; one language across agent, relayer and client library; existing crates (monero-serai/monero-rs families) for serialization and hashing; arti for Tor as a later option. TypeScript is used only for the storefront UI and the optional wallet-plugin glue.

4.1 mnr-core (library crate)

Modules: wire (epee binary + JSON-RPC types for the daemon API, hand-verified against monerod 0.18.x fixtures), hash (Keccak-256, block hashing blob, tx-tree hash, pruned tx hash), verify (rules of §3.4 as pure functions over bytes), policy (method table; the single source of truth rendered into docs), directory (record/snapshot types, signing, validation), metering (WU accounting), identity (Ed25519 keys, bech32m ids), settlement (statement types, pool math). No I/O in this crate; it must be fuzzable (cargo fuzz targets for every parser).

4.2 mnr-agent (operator binary)

[ clearnet TLS :443 ] ─┐
[ tor hidden svc    ] ─┼─▶ ingress (axum) ─▶ session auth ─▶ local policy (restricted-only) ─▶ proxy ─▶ monerod 127.0.0.1:18081
[ i2p (later)       ] ─┘        │                 │                          │
                               │                 └── unknown-session policy (deny | free-tier bucket)
                               ├── metering (per-session counters → SQLite, 10 s flush)
                               ├── health (self-probe monerod every 5 s: height, sync, peers, disk)
                               ├── directory client (fetch snapshot hourly; enforce min version; refresh own record daily)
                               └── statements viewer (GET /mnr/v1/statements; compares relayer statements to own counters)

Properties: single static binary (musl), ~10 MB; config is one TOML file; mnr-agent init generates keys, detects monerod, writes config, registers with the directory, and prints the operator id; mnr-agent doctor checks restricted RPC, version, disk, port reachability, Tor status. It does not manage monerod (operators keep their setup) but ships a recommended systemd unit and flags. Memory budget < 100 MB; it must run on the same box as a pruned node on a 4 GB VPS. Streaming responses are proxied without buffering. No view key, no wallet, no payments in v1 — the agent is deliberately boring.

4.3 mnr-relay (relayer binary)

client ─▶ ingress (axum; clearnet + onion) ─▶ auth (token hash → SQLite/Postgres; isolate cache) ─▶ rate limit (governor; per-token bucket + WU balance)
      ─▶ policy (mnr-core::policy) ─▶ cache (in-memory LRU + on-disk immutable store, key includes verification level)
      ─▶ dispatcher (operator pool: rank by on_tip, ema_latency, fault_rate, diversity; retry-once; broadcast for writes)
      ─▶ verifier (mnr-core::verify; streaming for .bin; sample-verify under load) ─▶ response (+ Mnr-* headers)

background tasks: header-chain sync (majority extend at tip; reorg detection → epoch bump of cache)
                  operator prober (every 10 s: get_info + last header; EMA; on_tip)
                  directory sync (hourly) + fault-log publisher (append-only, signed)
                  settlement (weekly: pool math, statements, payout via wallet-rpc hot wallet, dev fee line)
                  metrics (Prometheus /metrics, no per-client labels except hashed prefix in error samples)

State: SQLite for a single-node relayer (our v1); Postgres option for larger relayers. Cache: moka in-memory (bounded, e.g. 2 GB) + rocksdb on-disk store for immutable blocks/txs (bounded by size, LRU eviction). The relayer runs on 2 boxes in active/active behind DNS with health-checked failover in v1 (no Cloudflare required; optional Cloudflare in front for those who want it — off by default in the reference deployment). Each box also runs Tor for the .onion ingress; both boxes publish the same onion via OnionBalance.

4.4 mnr-store (storefront + billing, part of the relayer repo)

Axum routes + a small server-rendered UI: tier/credit purchase, invoice page with live confirmations, XMR402 endpoints, token issuance/rotation, statements for clients (WU balance). Watches a view-only monero-wallet-rpc for incoming payments; sweeps to cold on a schedule. A separate spend-capable monero-wallet-rpc with a capped float (refilled manually from cold) performs operator payouts; it is the one hot key in the system and is treated as such (own box, firewalled, cap = one epoch's pool + 10%).

4.5 mnr-client (library) and integrations

Rust library with C ABI, plus thin TypeScript and Python packages generated from it. Provides: directory fetch + selection, relayer-mode helper (token handling, rotation), direct-mode verified calls (phase 3), XMR402 purchase flow. A local proxy mode (mnr-client proxy --listen 127.0.0.1:18089) lets stock wallets use direct mode without embedding the library — the wallet points at localhost and the proxy does selection and verification. This is how Feather/Cake/CLI users get direct mode without those projects changing anything.

4.6 mnr-directory

Signer CLI (mnr-dir sign, mnr-dir add, mnr-dir tombstone) producing snapshots; a static-file publisher for mirrors; a registration intake (small axum service) that queues records for probing. Threshold signing uses FROST (Ed25519) via an existing Rust crate when the signer set expands (month 15); until then a single key with an offline backup.

4.7 mnr-sim (test network harness)

Docker-compose bringing up N stagenet monerod + agents, one relayer, a fault injector (serve wrong block, lag height, drop streams, return alt-chain), synthetic clients (wallet-sync replay, get_info storm, broadcast). Used in CI nightly and for every release. This is not optional; a network protocol without a simulator will ship bugs that only appear with strangers' nodes.

4.8 Repository layout

mnr/
├── spec/                 protocol spec (markdown, versioned; wire formats, verification rules, settlement)
├── crates/core/          mnr-core
├── crates/agent/         mnr-agent
├── crates/relay/         mnr-relay (+ store/ module)
├── crates/client/        mnr-client (+ ffi/, bindings/ts, bindings/py)
├── crates/directory/     mnr-directory
├── sim/                  mnr-sim docker harness, fault injector, k6/replay clients
├── deploy/               ansible for our operators + relayer boxes; systemd units; Tor/OnionBalance config
├── docs/                 operator guide, relayer guide, client guide, wallet how-tos, generated method table
└── .github/              CI: fmt, clippy, tests, fuzz smoke, sim nightly, release builds (musl, arm64)

4.9 Hostnames

Hostname Role Notes
mnr.network Protocol site: spec, docs, relayer list, transparency reports Neutral; lists all relayers, ours first only while it is the only one
rpc.mnr.network Reference relayer client endpoint (/v1/<token>/json_rpc) Run by the founders' entity; disclosed as such on the site. Health-checked DNS across the two relayer boxes; .onion published beside it via OnionBalance
stagenet.rpc.mnr.network Stagenet endpoint Handed to THORChain-side teams in week 1
node.mnr.network Operator onboarding: agent download, mnr-agent init instructions, earnings dashboard "Run a node, get paid"
dir1. / dir2. / dir3.mnr.network Directory mirrors Different hosts; each with a .onion; deliberately separate from rpc. so the neutral part is visibly not the commercial part
pay.mnr.network Storefront, invoices, XMR402 endpoints, token rotation Reference relayer's; other relayers run their own
status.mnr.network Status page (phase 2) Static host, outside the relayer boxes