# 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](https://github.com/mnrnetwork/mnr/blob/main/docs/stage2-network-protocol-architecture.md) with the build plan and internal decisions left out. See the [roadmap](/docs/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

- Every operator and relayer has a long-lived **Ed25519 identity key**. The public key *is* the identity (`op1…` / `rl1…` bech32m encoded). Keys are generated by the binary on first run and stored in the config directory; loss of key = new identity with zero history, which is the only "penalty" the network has, and it is enough.
- Operators sign their **directory record**. Relayers sign **session challenges**, **settlement statements** and **fault records**. Directory signers sign the **directory snapshot**.
- Transport identity is separate: clearnet endpoints use TLS with either a public CA cert or a self-signed cert whose SPKI hash is in the directory record (pinning); `.onion` endpoints get identity from Tor. Both are acceptable; pinning is preferred because it does not depend on the CA system.

### 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

- Weekly epoch (Monday 00:00 UTC). For each operator with which the relayer had sessions, the relayer publishes a **settlement statement**: `{relayer_id, op_id, epoch, wu_served, wu_faulted, share, pool_xmr, amount_xmr, txid?, sig}`.
- The pool: `pool_xmr = op_share × relayer_client_revenue_xmr(epoch)` where `op_share` is the relayer's published rate (reference relayer: **0.60**). Each operator's `share = wu_served_i / Σ wu_served`, with `wu_faulted` (requests whose responses failed verification) excluded and counted against reputation.
- Amounts below the operator's `min_payout_xmr` roll over. Payouts are batched into one Monero transaction per epoch (up to 16 outputs per tx; more epochs → more txs). Relayer's `monero-wallet-rpc` (spend-capable hot wallet with a **capped float**; the storefront's incoming wallet is view-only and swept to cold) sends them.
- Operators compare `wu_served` in the statement to their own session counters. The agent exposes `GET /mnr/v1/statements` showing both numbers side by side; discrepancies > 2% are flagged locally and, optionally, published as a signed **dispute record** to the fault log. Nothing is automatically enforced; the point is visibility.
- **Dev fee:** the reference relayer's settlement code adds a line `{op_id: <maintainers' key>, share: 0.02}` to the pool by default, minus any portion of the pool that went to the relayer's own operators (we do not pay ourselves a fee on ourselves). It is a config flag (`dev_fee = 0.02`), disclosed in the relayer's directory record, and visible in every statement.
- **Affiliate share (borrowed from THORChain's interface fee):** a client token may carry an `affiliate_id` (an Ed25519 key registered in the directory like an operator, with a payout address) set at purchase time by the wallet, agent framework, swap interface or integrator that brought the client. The relayer pays the affiliate **15% of that client's revenue** (reference default; a relayer config value) out of its own 40%, in the same weekly settlement, with a signed statement. Self-referral is allowed and harmless — it is merely a discount. The `op_share` pool is unaffected, so operators never subsidise distribution.
- **Probation lane (borrowed from THORChain's churn):** each epoch the dispatcher reserves **10% of traffic weight** for operators with the least verified history in the last 30 days (new joiners and returners), spread evenly among them, subject to the normal on-tip and fault-rate gates. This guarantees that a new operator earns and is audited from its first week instead of being starved by the ranking. The share is a snapshot parameter (`probation_weight`).
- **`op_share` band (THORChain's incentive pendulum, deferred):** `op_share` is a snapshot parameter with a permitted band of 0.50–0.70. v1 pins it at 0.60; a later version may move it with network utilisation (scarce supply → higher; abundant supply → lower). Declaring it a parameter now means changing it later is not a protocol break.

### 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 |

---
