UniNet

Start here

What runs today

A three-way split of the Rust workspace: what the node binary actually calls, what exists only as a library, and what is an interface with no implementor.

AvailableValidator production deploymentAvailablePublic network access for external developersAvailableDistributable client (CLI / SDK)AvailableUNP private addressing and routingAvailableDynamic chain spawning

What this page is

The implementation is a Rust workspace whose members are fourteen library crates under crates/, two binaries - uninet-node and uninet-cli - and three bench and test crates, all listed in the workspace Cargo.toml. This page sorts that workspace into three categories, because "implemented" is not one state:

  1. Called by the node binary. A route, a handler or an executor path reaches the code when the node is running.
  2. A library with no caller outside its own crate and its tests. The code is written and tested; no binary invokes it.
  3. An interface with no implementor. A trait exists, the call site exists, and nothing outside that crate's own test module implements it.

The distinction matters because the second and third categories look identical to anyone reading a module list, and they behave very differently when you try to deploy.

One thing to know before reading further: this codebase contains no todo!(), no unimplemented!(), and no // TODO or // FIXME markers. Gaps are recorded in module-level doc comments instead - crates/net/src/onion/mod.rs carries a "# Not built" section, and crates/net/src/unp/resolver.rs opens with "This module is an interface with no backend." An absent marker is not evidence of completeness here, and the prose is the place to look.

What a running node serves

Start from the surface, because it bounds everything else. bin/uninet-node/src/api/unp_gateway.rs documents three entrypoints on the external HTTP listener once UNP enforcement is live:

  GET  /unp/resolve     node identity + a signed bootstrap nonce
  POST /unp/handshake   X25519 ECDH  ->  AES-256-GCM session
  POST /unp             the tunneled request dispatcher

Every other /api/* route is reachable only through the third one. AppState holds the inner axum::Router, and the dispatcher decodes a TunneledRequest and re-dispatches it into that router. /unp/resolve returns the node's Ed25519 public key, its wallet address, its X25519 public key, a freshly generated 32-byte nonce and an Ed25519 signature over the nonce, the timestamp, the node id and the X25519 key, so a client can check that the response, the key included, came from the holder of the key it was handed. /unp/handshake derives the session key from that exchange, signs the transcript, and returns a session id and an expiry. Alongside these, /unp/:domain and /unp/:domain/*rest serve names registered on the node's own DNS chain, and the router registers the routing endpoints /unp/relay, /unp/circuit, /unp/deliver and /unp/onion, whose behaviour the next section covers.

The CLI speaks this protocol. It also carries a --dev-insecure flag documented as dev-and-test only, which bypasses the tunnel and talks plain HTTP to a dev node's loopback listener.

This is the shape of what you can exercise on one node: its hosted child chains, and a client that tunnels into it. Everything in the table below sits behind that surface, and the hosted testnet runs the same binary on five validators.

The inventory

SubsystemWhat existsWhat calls it
Chain executioncrates/chain/src/executor.rs - seventeen transaction variants, including transfer, element deploy with in-transaction supply minting, contract execution, chain spawn, token bind and dissolve, governance vote, module swap and the storage set. Receipts are hashed into a receipts_root.The node's /api/tx/* routes, and block application on every node that receives a block.
WASM executioncrates/vm/src/runtime/wasmtime_backend.rs: Wasmtime JIT, gas metering, StoreLimits bounding memory and table size, and twelve env host functions, each checked by name and signature at deploy. Storage reads and writes, balance reads, transfers from the contract's own wallet, hashing, Ed25519 signature checks, block info and events run against the chain's own state backend. A contract importing anything else (the former deploy, dissolve and cross-chain placeholders included) is refused when it is deployed.executor.rs:1081 calls execute_with_state. This is the only engine the chain executor uses.
Interpreter runtimecrates/vm/src/runtime/mod.rs - InterpreterRuntime, whose simulate_execution echoes the call arguments back as return data. Its compile step requires the magic bytes UNVM and rejects anything else, so it cannot parse a WASM module.Nothing outside crates/vm. Every reference to it is that crate's own tests.
PBFT consensuscrates/consensus: three-phase PBFT, Ed25519-signed votes, quorum arithmetic, view change, and a commit certificate stapled into the block so a node that was not present for the round can check it afterwards.Every engine the node builds verifies message signatures, the single-node path included. Quorums stay safe at any validator count, and validators journal what they sign so a restart cannot make them sign twice.
Blocks and state rootscrates/chain/src/block.rs - the header commits to the state root the block executes against, and Chain::apply_block checks it before executing, so a diverged node refuses rather than executing onto a different base. A zero root is accepted as unverifiable.Primary-chain block production (AppState::build_block) and application on every node. Two limits to read with it: the header commits the pre-execution root, and nothing commits a post-execution root into a block; and AppState::build_child_block, the child-chain proposer, writes a zero root, so the check is inert on child chains.
Storage and Merkle proofscrates/storage: a sparse trie. On the primary chain DiskBackend is wrapped in EncryptedBackend for both the state trie and the block store (state.rs:751, state.rs:761); child chains wrap theirs the same way (AppState::open_child_chain).POST /api/storage/proof serves a real trie proof from chain.state().prove(key) and verifies it against the node's current root. The handler withholds the proof path when the walk would end on another caller's value. Read the root carefully: it is this node's trie root, so the proof shows what this node stores, not what a validator set agreed.
Identity, RBAC, delegationbin/uninet-node/src/rbac.rs - owner, root, admin, per-field grants, a parent cascade bounded at MAX_PARENT_DEPTH = 16, token gates evaluated against real ledger balances, and a delegation chain walk bounded at MAX_DELEGATION_DEPTH = 8. Revocation marks rather than deletes, so the record of who once held a right survives.The thirteen /api/identity/{admin,permission,delegation}/* routes. A regression test asserts that the eleven mutating ones answer an unauthenticated caller 401 and specifically not 501, that a refused call writes nothing, and that the two verdict routes - permission/check and delegation/check - answer false rather than 501.
Governancecrates/ao - organization types, roles, a voting protocol with quorum, payment and inactivity state machines, and a transition validator.The node's /api/aos/* routes - create, campaign, vote, tally, payment, governance action, status - and the /api/governance/* set, over gov_state.rs.
Onion routing and SURBscrates/net/src/onion: one AES-256-GCM layer per hop, keyed by a fresh ephemeral X25519 exchange against that relay's published onion key; fixed cell size per privacy tier; every layer authenticating its own ciphertext, the cleartext frame header and the filler region; cover traffic; key rotation with a grace window for frames already in flight; and single-use reply blocks with a byte-exact wire format. Unit tests across its packet, circuit, reply, key-schedule, parameter, cover-traffic and rotation modules, plus the integration suite in crates/net/tests/privacy_tiers.rs.Cross-node routing carries real requests over circuits built from it (onion::circuit, POST /unp/circuit). Reply blocks carry nothing, and the cover that runs is requests over those circuits, not the crate's cover packets. See below.
UNP confidential resolutioncrates/net/src/unp/resolver.rs: the ThresholdResolver trait, and a four-point specification of what an implementation has to guarantee.Nothing: the node resolves from signed records instead. See below.
.unp domainscrates/net/src/registry/domain.rs, bin/uninet-node/src/dns_chain.rs, and a 2,311-line HTTP API. Registration charges payment server-side and unconditionally, and records expiry and ownership; the NFT element is minted only when the request sets mint_nft./unp/:domain and /unp/:domain/*rest serve registered domains, and lookup_active treats a lapsed registration as absent. One caveat: lookup returns vec![leaf_hash], a single element the source itself comments as "Simplified". That is not a verifiable inclusion proof and should not be read as one.
Chain spawningTwo separate things share the name. crates/multichain/src/spawner holds a nine-step protocol with node allocation and hosting tokens. SpawnChain records the chain on the primary (uninet_chain::child_chains: owner, validators, status), and every validator it names starts the chain from that record when it applies the block: its own PBFT on a multi-node network, one proposer on a single node, and the primary's fee schedule in the chain's own token, paid to its validators./api/tx/spawn-chain goes through the executor, which mints the hosting NFT and records the chain under an owner-scoped id; its hosts start it from that record. /api/chains/:id/validators (invite, accept, remove, leave) and /api/chains/:id/freeze and /unfreeze let its owner change who runs it and stop it. ChainSpawner is referenced only inside crates/multichain and in tests/integration/src/multi_chain.rs; neither binary constructs one.
Ethereum bridgecrates/interop - bridge registry, a validator set with epochs and thresholds, and escrow with a seven-day lock TTL and a refund path for a mint that never lands.Partly. /api/bridges/* drives the registry, the validator set and a relay log held in the node's own process; a "relayed" message is a hash appended to that log and nothing leaves the machine. The escrow and verifier modules have no reference in either binary. The crate's Cargo.toml declares no HTTP client, no RPC client and no light client - there is no transport in it at all.
Capacity reservationcrates/chain/src/child_chains.rs: what a chain reserves on each node that runs it, recorded with a grant signed by each of those nodes, which the executor checks against the node's key. crates/node/src/vm.rs: each node's ledger of holds, one per chain pool, per promise of room and per container.A spawn, or its owner's later change, through /api/tx/spawn-chain/* and /api/chains/:id/resources. Each node asked sets the room aside before it signs, and every node that runs the chain holds the recorded amount from the block that records it and again after a restart (bin/uninet-node/src/api/apps.rs). The chain records what each node signed for and does not measure the hardware behind it.
Command-line clientbin/uninet-cli, a thin binary over the library crates/client: read commands (status, health, block, receipt, chains, nodes, relays, and raw for any route), two verifiers (dns-verify, relays-verify), key and identity commands, and signing commands (transfer, name registration and renewal, stake, unbond, withdraw, and tx sign for any other transaction a node prepares).It reads through the tunnel, and it writes with a key of its own: it makes the key, registers its identity and signs. The node hands it the bytes to sign, and it shows them first and holds a transfer to the recipient and amount asked for. It uploads no files and builds no circuits.

The UNP split

UNP is where the difference between the three categories decides whether anything works end to end, so it is worth stating precisely.

Cross-node routing runs. With unp_routing configured on a real multi-validator network, a site hosted on one validator is fetched through another over an onion circuit of one, two or three relays (Standard, Private, HighSecurity): the stronger of the tier the domain's owner sets in its route binding on the chain and the one the visitor asks for. The serving node comes from service records each hosting validator signs and gossips, authorized against the resolving node's own registry and chain state (bin/uninet-node/src/unp_routes.rs). The relays come from the relay list on the chain, which any node with 1,000 UNE staked behind it may join, each used only after it proves its onion key at its address, and at Private and up only relays under distinct operators, each with a stake behind it, in distinct networks. A path a node draws runs over validators' relays; a tier that cannot be drawn is refused with 503 PRIVACY_DOWNGRADE_REFUSED, never carried over fewer relays (bin/uninet-node/src/unp_circuit.rs). One fixed-size cell names the relays (crates/net/src/onion/circuit.rs). The request, sealed end to end to the serving node, and its reply carry one layer per relay in power-of-two size buckets (padded inside the seal too, so the last link shows a bucket as well), and each relay posts on to the next one's POST /unp/circuit until the last delivers to the serving node's POST /unp/deliver. A relay that cannot pass a request on reports why under its own layer, so the visitor's node knows which relay said so, and the retry goes around it.

The desktop app builds its own circuits. The client app's desktop shell resolves a name itself, from the service records validators sign and the proofs of the name's claim and route binding (GET /api/unp/directory), and draws the relays from the relay list, all checked against the genesis validators' keys. The list is the chain's own count of relays and every one below it, so none can be withheld; a relay outside the validator set may enter a path or sit in its middle, and the last relay is always a validator, the only kind of node that knows where a host is reached. It seals the request and the cell as a node would, and its node only carries the opaque request to the entry relay (POST /api/unp/forward), so that node learns the entry relay, the tier and the sizes, not the name, the host or the page. The app's TypeScript is checked byte for byte against vectors the Rust code exports, and the five-node test fetches a page this way. A site keeps one path: the relays drawn for its first request carry its later ones for ten minutes, unless one fails or leaves the relay list, and each site draws its own. A request no circuit carried is refused, and goes through the visitor's node only while the visitor has turned that on. A page shown in the app may load only UNP addresses, so what it asks for goes through the app's handler or nowhere, and a page that came over a High Security circuit runs no script. In a web browser on localhost the app does the same without a handler of its own: each site is shown on an origin of its own, named by a random label, where a service worker hands the site's requests to the app, which carries them over its circuits (src/lib/unp/browserBridge.ts, public/unp-sw.js in the client app). The app says there that this is weaker than the desktop shell. It has not been run in a browser yet: its worker, its frame page and the app's side are driven by tests that stand in for the browser, and the built app is asked over HTTP what it serves at a site's address.

A site can be hosted hidden. Its owner binds the name with hidden: true (POST /api/dns/bind, site mode): the node that is to host makes a service key and picks up to three meeting relays, and the binding the owner signs names that key and those relays, and no node. The host sends no route record for the name and answers for it at its own gateway as every other node does. It keeps requests open at each meeting relay over a circuit of its own; a visitor's circuit ends at one of them; the relay hands the visit to the waiting host and the answer back (POST /unp/meet, bin/uninet-node/src/hidden.rs, crates/net/src/unp/meet.rs). The request is sealed to the service key and each message to the relay, so the relay learns that the site is asked for, not who asks or who answers. A five-node test binds a site this way and fetches it through another node, through the host's own gateway and over a circuit the app builds, then kills a meeting relay and fetches it again.

High Security adds holds and cover. At HighSecurity a request and its reply are never under 4 KiB, every relay holds a cell for up to 150 ms before it passes it on and again on the way back, and every node sends a cover circuit about every ten seconds: a request for nothing to a node drawn at random, built and carried as a visit is, which that node answers with nothing in a size it draws (unp_circuit::send_cover, route_seal::cover_target). A five-node test runs cover beside a HighSecurity visit, sees it cross every relay and come back, and sees a request that is not padded refused.

A user's transactions leave over circuits. A validator with routing on does not gossip what its users sign. It sends it over a circuit to another validator, which takes it into its pool and gossips it as its own (bin/uninet-node/src/tx_circuit.rs), and it keeps the transaction in its own pool, out of its proposals, until it comes back. What has not come back after five seconds it gossips itself, as it does at once when no circuit can be drawn. The two transactions of a hidden site's bind are never gossiped by the host, and the validator that takes them keeps them 5 to 30 seconds first. A five-node test binds a hidden site this way and counts every one of the owner's transactions leaving the host over a circuit.

What it is not. In a browser that has no origin of its own for a site, and in either app once the visitor has allowed it for requests no circuit carried, the node the visitor talks to draws the circuit, so it sees the visitor, the domain, the host and every relay, and when it fetches pages for the visitor it reads them. Every validator can see which node hosts a domain unless its owner bound it hidden, and a hidden host is still known to the nodes that store the owner's files. Holds and cover run at HighSecurity only and cover is sparse, so a party watching both ends of a circuit can still link them by timing. This is relayed hosting with layered routing, not an anonymity system.

Confidential resolution has no backend. ThresholdResolver has no non-test implementor anywhere in the workspace, and UnpResolver::resolve refuses with EnclaveUnavailable rather than handing back ciphertext. Resolution is a verified lookup over signed records, not a private one: nothing on this path uses threshold, MPC, enclave or zero-knowledge cryptography.

Reply blocks and cover packets are library code. onion::reply (single-use reply blocks) and onion::cover (cover packets a last hop drops, and a constant-rate scheduler) are implemented and tested, and no handler routes a reply frame or emits such a packet: a circuit brings its reply back over the same chain of relays, and the cover that runs is requests for nothing over those circuits.

The hosted testnet

A testnet runs on five validators: four rented servers, which also run other services and give the node one core and 1 GB of memory each, and a laptop behind a home router, which reaches the servers over a private overlay network and opens no port. All five run the signed v0.1.0 release, and four of them make a quorum. Measured on 2026-10-03 with the repository's scripts/gate1-measure.sh and scripts/testnet-acceptance.sh:

  • Finality. A transfer is final on every server a median of 75 ms after the first node receives it (p95 92 ms, max 141 ms, 20 samples), timed on the servers' own NTP-synchronized clocks.
  • A validator down. With one server stopped, the other three and the laptop finalized the next block in 0.37 s, and the server caught up within a second of restarting. With the laptop stopped, the four servers did in 0.45 s.
  • The leader down. The others changed view and finalized the next block in 10.6 to 10.9 s, of which the first view-change timeout is 10 s.
  • A burst. 200 transfers from one signer landed as fast as the script sent them, in 5.7 s: a figure for the script, not a throughput.
  • Circuits. A site on one server was served through another at Standard (one relay) and Private (two relays, under distinct staked operators in distinct networks), and the client app's own code built a Private circuit to a second site while the entry node's log never named it. HighSecurity needs three relays besides the entry and the host, which four public servers cannot give, so the node refuses it rather than use fewer.
  • Joining. The laptop joined by vote, as any operator does. A node that knew only the network file (uninet.technology/network.json) synced to the newest block from the internet in 6 s, and the one-line install in running a node installed the signed release, checked its signature and synced.

This shows that consensus survives real links and a lost validator. It does not show behaviour under sustained load, across continents, or among operators who do not trust each other: one operator runs all five validators.

What was removed, and why that is the outcome

Five hand-rolled primitives were deleted from crates/crypto on 2026-08-27. The reasons are recorded in crates/crypto/src/lib.rs under "# Deliberately absent", which lists them so nobody re-adds an approximation:

  • Zero-knowledge proofs. SchnorrZkp::verify recomputed the Fiat-Shamir challenge, discarded it, and returned whether both fields contained a non-zero byte - so any 64 non-zero bytes verified against any statement. Its three dependants inherited that.
  • Threshold signatures, and the DistributedSigningSession that consumed them, which collected secret shares in one process rather than partial signatures on a wire.
  • Feldman verifiable secret sharing, whose commitments were a hash of the share, chosen by the same dealer. Shamir shares in this tree are documented as unverifiable.
  • Pkcs11Hsm, six methods that all returned an error, behind a feature flag that made it read as an available backend. SoftwareHsm is now the only backend and says so.

These were unsound rather than incomplete, and removing an unsound primitive is the correct engineering outcome. The routes that used to be backed by them - /api/crypto/zkp/*, /api/crypto/threshold/*, /api/crypto/smpc/* - return 501 with an error field and deliberately no field shaped like a result: no proof, no signature, no valid. A regression test in api/crypto.rs asserts none of them can return a stub value again. UniNet claims no zero-knowledge proof system, no verifiable secret sharing and no threshold signature scheme.

What the design does not address

Design-level limits, taken from the modules that implement the mechanism rather than from a policy statement:

  • A global passive adversary. Fixed cells and constant-rate cover raise the cost of correlating flows. An observer who watches every link at once defeats low-latency onion routing. This is not a mixnet and makes no mixnet claim.
  • End-to-end confirmation. A party observing both the entry and the exit of a circuit can correlate timing and volume. Relay diversity is the defence, and it is only as good as the attribution behind it: a relay counts under whoever has a stake behind its node (the node itself, or the identity the chain binds to it as operator), so one party looks like several operators only by staking several times, and there is no AS data.
  • Directory agreement. Relay records and their onion keys are on the chain, agreed by the validators, and a node fills its directory from its own chain once each key is proven at the relay's address. A client can check the whole list with state proofs against the genesis validators' keys (GET /api/relays/list, GET /api/validators/epochs); uninet-cli relays-verify and the client app's desktop shell check them, and in a browser the app still takes routing from its node.
  • The Standard tier, in the relay-anonymity sense. One hop means the single relay is both entry and exit and does see both the client and the destination. It is a latency tier and is documented as one.
  • The exit relay's view of the payload. Onion layers protect routing metadata. Payload confidentiality end to end is the tunnel's job, not the onion module's.
  • The host operator. Isolation at the endpoint is container-level, and only on a Linux host with crun or runc installed (the Docker image ships crun). Elsewhere only the node admin's and trusted owners' apps run, as plain processes, and a chain with firewall rules runs its apps sandboxed or not at all. An operator with root on the machine is inside the trust boundary either way.

Where to check each claim

The load-bearing claims above are read out of these files. They are listed so that disagreeing with this page is a matter of opening a file rather than taking a position. The inventory table names the rest.

ClaimFile
The external surface and the tunnel dispatcherbin/uninet-node/src/api/unp_gateway.rs
Cross-node routing over circuits, and what each party learnsbin/uninet-node/src/unp_routes.rs, bin/uninet-node/src/unp_circuit.rs, crates/net/src/onion/circuit.rs, bin/uninet-node/DOCUMENTATION.md
No implementor for ThresholdResolver, and what one must guaranteecrates/net/src/unp/resolver.rs
Onion threat model, and the "# Not built" listcrates/net/src/onion/mod.rs
The reply-block wire format and who sees whatcrates/net/src/onion/reply.rs
The five deleted primitives, and whycrates/crypto/src/lib.rs
501 answers with no result-shaped field, plus the regression testbin/uninet-node/src/api/crypto.rs
Wasmtime host functions, limits, and the imports refused at deploycrates/vm/src/runtime/wasmtime_backend.rs, crates/vm/src/sandbox/imports.rs
The interpreter's UNVM magic bytes and echo behaviourcrates/vm/src/runtime/mod.rs, crates/vm/src/compiler.rs
Pre-execution state-root commitment, checked before executingcrates/chain/src/block.rs, crates/chain/src/chain.rs
Depth bounds, token gates and the delegation walkbin/uninet-node/src/rbac.rs
The single-leaf domain "proof"crates/net/src/registry/domain.rs
Container isolation, and where it does not activatebin/uninet-node/src/sandbox.rs
What a chain reserves, the grants behind it and who holds itcrates/chain/src/child_chains.rs, bin/uninet-node/src/api/apps.rs, bin/uninet-node/tests/multinode_bft.rs
The non-verifying default consensus enginebin/uninet-node/src/state.rs, crates/consensus/src/engine.rs

Where any other document in the repository disagrees with these files, the files are correct. Nothing on this page is derived from a capability summary.

How it composes

Read this page against private networking, which describes the routing model the onion modules implement, and against execution for what runs at the far end of a route. Identity and authority cover the two subsystems whose node-side code carries explicit regression tests asserting their routes cannot return to stubs.

For the graded view of every subsystem, and the limitation attached to each one, go to build status. This page describes what the source does. That table is the single place the site records how far along each piece is, and every status chip on this page is rendered from it.