We present Lethe Network, a fully-shielded digital cash system that achieves strong payment privacy without a trusted setup ceremony. Lethe combines a note-based UTXO model with Poseidon2-based commitments and nullifiers, UltraHonk zero-knowledge proofs (compiled from Noir circuits), SHA-256d proof-of-work consensus, and a Merkle accumulator for the note commitment set. Value conservation is enforced inside the ZK circuits themselves — neither the wallet nor the node can inflate or deflate amounts. An optional compliance module allows users to prove, in zero knowledge, that their address is not present in a published sanctions list, enabling exchange listings without compromising privacy.
Unlike Zcash, Lethe uses a transparent proof system (no Powers-of-Tau ceremony), so there is no trapdoor that could allow a silent counterfeiting attack. Unlike Monero, Lethe uses succinct ZK proofs rather than ring signatures, providing constant-size proofs and efficient verification. The system is implemented as a Rust workspace with Noir circuits, a libp2p peer-to-peer network, a Tauri desktop wallet, and a Solidity compliance registry.
Existing privacy cryptocurrencies make one of two compromises: they either require a trusted setup ceremony whose security cannot be verified (Zcash Sapling / Groth16), or they use ring signatures whose proof size grows linearly with the anonymity set (Monero). Neither approach is satisfactory for a system intended to be both minimal-trust and scalable.
Lethe takes a different path. The proof system is UltraHonk — an interactive oracle proof compiled to a non-interactive argument via the Fiat-Shamir transform. UltraHonk does not require a trusted setup of circuit-specific parameters; the only global parameter is the BN254 elliptic curve, which is a widely-deployed, publicly-audited curve. Proof size is constant and small (~16 KB), and verification is fast enough to run in a smart contract.
The Lethe protocol is built around four core primitives:
| Primitive | Function | Instantiation |
|---|---|---|
| Note | Encodes a unit of value with its owner and randomness | Struct (value: u64, owner: Fq, rho: Fq, rcm: Fq) |
| Commitment | Cryptographic fingerprint of a note; inserted into the Merkle tree | Poseidon2(value, owner, rcm) over BN254 scalar field |
| Nullifier | One-time tag derived from spending key and note; prevents double-spend | Poseidon2(spending_key, note.rho) |
| ZK proof | Proves ownership and correct construction without revealing private data | UltraHonk via Barretenberg, circuits in Noir |
The public chain stores only commitments, nullifiers, and block headers. Amounts, addresses, and transaction graphs are not visible on-chain. A recipient scans the chain by attempting to decrypt each encrypted note with their viewing key; notes that decrypt successfully belong to them.
All in-circuit hashing uses Poseidon2 [GHL+23] instantiated over the BN254 scalar field (Fr, 254-bit prime). Poseidon2 is a ZK-friendly algebraic hash designed to have a low gate count in arithmetic circuits. The t=4 permutation is used throughout (state width 4 field elements). The external Poseidon2 implementation in lethe-core matches the Noir standard library's poseidon2_permutation output exactly, verified by a known-answer test.
For binary uses (2-input hash), we use:
A note commitment commits to the note's value, owner, and randomness (rcm), but not to rho (the one-time nullifier randomness). The commitment scheme is binding and hiding:
The value is cast to a field element before hashing. The owner is a BN254 public key (pk_d, 32 bytes). The rcm is a uniformly-random field element chosen at note creation time.
To spend a note, the sender reveals a nullifier derived from their spending key and the note's one-time random tag (rho):
Since rho is unique per note and spending_key is only known to the owner, no two spend operations can produce the same nullifier unless the same note is spent twice. The ZK proof demonstrates, without revealing spending_key, that the nullifier was computed correctly and that the prover holds the spending key corresponding to the note's owner.
Lethe uses a simple three-level key hierarchy derived entirely from the spending key sk (a uniformly-random 32-byte secret):
The viewing key vk allows a third party (e.g., a tax auditor) to scan the chain and identify incoming notes without being able to spend them. The payment address pk_d is the public identifier shared to receive funds.
Each output note is encrypted to the recipient's viewing key using AES-256-GCM with a key derived from an ephemeral ECDH exchange (or in the current implementation, a symmetric key derived from the viewing key). The encrypted blob is stored in the block header (for coinbase) or in the transaction (for user outputs). All encrypted notes are served to all callers — the server cannot distinguish which wallet owns which note.
Note commitments are accumulated in a 32-level incremental Merkle tree (capacity 2³² ≈ 4 billion notes). The tree uses Poseidon2 as the internal hash function:
Empty positions are filled with precomputed zero hashes (EMPTY_ROOTS[depth]). The root of the tree is included in every block header as note_root. Spend proofs reference a recent root; the node accepts any root that appears in the valid chain (with a configurable lookback window).
A Lethe transaction spends one or more existing notes and creates one or more new notes. The on-chain representation contains:
struct Transaction {
nullifiers: Vec<[u8; 32]>, // one per spent note
output_commitments: Vec<[u8; 32]>, // one per new note
encrypted_outputs: Vec<EncryptedNote>, // for recipient scanning
fee: u64, // atoms to miner
spend_proofs: Vec<Vec<u8>>, // one spend proof per nullifier
output_proofs: Vec<Vec<u8>>, // one output proof per commitment
anchor: [u8; 32], // Merkle root used for membership
}
For each input note, the prover generates a spend proof. The circuit has the following interface:
| Visibility | Name | Description |
|---|---|---|
| Private | note | The full Note struct being spent |
| Private | spending_key | sk — authorises the spend |
| Private | merkle_path | 32 sibling hashes from leaf to root |
| Public | merkle_root | The Merkle root used; must match the path |
| Public | nf_public | Poseidon2(sk, note.rho) — the nullifier |
| Public | fee | Transaction fee; circuit asserts note.value ≥ fee |
| Public | value_public | note.value — asserted equal; used for conservation |
The circuit asserts: (1) the note commitment lies on the Merkle path and the computed root equals merkle_root; (2) the nullifier is computed correctly from sk and note.rho; (3) the owner is derived from sk via the key derivation chain; (4) note.value ≥ fee; (5) note.value == value_public.
For each output note, the prover generates an output proof. The circuit proves correct construction of the note commitment:
| Visibility | Name | Description |
|---|---|---|
| Private | note | The new Note struct (value, owner, rho, rcm) |
| Public | cm_public | Poseidon2(value, owner, rcm) — asserted equal to circuit-computed cm |
| Public | value_public | note.value — asserted equal; note.value must be > 0 |
The circuit asserts: (1) cm_public equals the Poseidon2 commitment of the private note; (2) note.value > 0 (no dust notes); (3) note.owner ≠ 0 (no burn-to-zero); (4) note.value == value_public.
The value_public field in both circuits exposes each note's value as a plain integer on the proof's public input list. The node — or any verifier — can check conservation without running the ZK proof:
This is a two-layer defence. First, each circuit independently asserts that its value_public matches the private note value — so a prover cannot lie about the value of any individual note without producing an invalid proof. Second, the node checks the summation before admitting a transaction to the mempool. Both layers must pass for a transaction to be included in a block. Inflation is impossible.
Note: the note value is not hidden in the current protocol. Observers can read value_public from the public inputs. A future upgrade may use a range proof or a blinded value scheme to hide per-note amounts while still proving conservation, at the cost of larger proofs.
Proofs are generated by the Barretenberg UltraHonk prover (bb prove). The prover takes a compiled circuit artifact (.json), a witness file (.gz), and a verification key, and produces a proof binary (~16 KB) plus a public inputs file (32 bytes per field element). The wallet calls NargoProver, which invokes nargo execute to generate the witness from a Prover.toml, then bb prove to generate the proof.
Lethe uses SHA-256d (double SHA-256), the same proof-of-work function as Bitcoin. A block is valid when:
The target is encoded in compact nBits format (identical to Bitcoin's). Miners increment a 64-bit nonce until a valid hash is found.
Difficulty retargets every 504 blocks (approximately one week at the 2-minute block target). The actual interval is measured from the timestamp of the retarget block and the block 504 blocks earlier. The new target is:
Clamped to ±4× per interval to prevent wild swings. The initial target at genesis is set to 0x207fffff (trivially easy) for development; mainnet genesis will use a harder target.
struct BlockHeader {
prev_hash: [u8; 32], // SHA256d of the previous block header
height: u64, // genesis = 0
timestamp: u64, // Unix seconds
note_root: [u8; 32], // Merkle root after all commitments in this block
coinbase_cm: [u8; 32], // commitment of the miner's reward note
coinbase_enc: Vec<u8>, // encrypted coinbase note for miner scanning
nonce: u64, // incremented during mining
}
The note_root commits to every note commitment appended by this block (coinbase first, then outputs in transaction order). Nodes reject any block whose note_root does not match the root computed after applying all commitments.
Lethe uses the heaviest-chain rule: the valid chain with the highest cumulative proof of work is canonical. Work per block is:
Orphan blocks are stored in a fork buffer (up to 32 deep). When a fork's cumulative work exceeds the main chain, the node reorgs: it rolls back to the common ancestor and replays the fork chain. All rolled-back transactions re-enter the mempool.
All LTH is created by mining — there is no pre-mine, no developer allocation, and no ICO. The emission follows Bitcoin's halving model with a tail emission floor:
| Parameter | Value |
|---|---|
| Initial block reward | 50 LTH (5,000,000,000 atoms) |
| Halving interval | 210,000 blocks (~1.6 years) |
| Tail emission | 0.01 LTH (permanent floor) |
| Approx. total supply | ~21 million LTH |
The Lethe P2P layer is built on libp2p (Rust implementation). Each node establishes outgoing and incoming TCP connections, upgrades them with the Noise protocol for authenticated encryption, and multiplexes streams with Yamux.
Two application protocols run on top:
lethe/blocks/1 and lethe/mempool/1.Node identities are ed25519 keypairs persisted to disk on first startup. The public key is the node's peer ID (a multihash of the public key). Bootstrap peers are provided via the LETHE_BOOTSTRAP_PEERS environment variable as libp2p Multiaddr strings.
The node exposes a minimal HTTP API (axum) for wallet communication:
| Method | Path | Description |
|---|---|---|
| POST | /v1/transactions | Submit a transaction; returns 200 on acceptance, 422 on validation failure |
| GET | /v1/state | Chain tip hash, height, note Merkle root |
The API enforces per-IP rate limiting (token bucket, default 60 capacity / 10 tokens per second). Requests exceeding the limit receive a 429 response.
Cryptocurrency exchanges operating under AML/KYC regulations must screen users against sanctions lists (e.g., the OFAC SDN list). For privacy coins, this creates a fundamental tension: the exchange needs to verify that the user is not sanctioned, but the user needs to avoid revealing their address.
Lethe resolves this with a ZK non-membership proof. The process:
ComplianceRegistry Solidity contract.ComplianceRegistry.verifyCompliance(proof, root) on-chain. The contract verifies the proof. If valid, it concludes the user is not sanctioned. It learns nothing about the user's address.The compliance circuit implements a ZK sorted-set non-membership proof. Non-membership is proved by exhibiting a neighbour leaf in the sorted sanctions tree such that the user's address falls strictly between the neighbour's value and its next pointer:
If the neighbour is correctly placed in the tree (its Merkle path verifies against sanctions_root), then the address cannot be a leaf in the tree. The circuit derives the address from the spending key internally via hash_2(hash_2(sk, 0), 0), so the raw address is never an input — only the spending key is required.
The LetheAnchor Solidity contract provides a trustless path for verifying spend proofs on-chain. Calling submitRootWithProof(root, nullifier, fee, valuePublic, proof) invokes the pluggable ILetheVerifier interface. If the verifier accepts the proof, the root is marked valid on-chain. The verifier address is upgradeable by the contract owner via setVerifier().
Creating LTH from nothing requires producing a valid spend proof for a note commitment that does not exist in the Merkle tree, or an output proof whose value_public is larger than the private note's value. Both require breaking the soundness of the UltraHonk proof system — which would require breaking the discrete-log hardness of BN254. No such attack is known.
Spending the same note twice requires producing two valid nullifiers from the same note. Since nf = Poseidon2(sk, rho), and both sk and rho are fixed for a given note, a second spend of the same note would produce the same nullifier. The node maintains a nullifier set and rejects any transaction containing a nullifier already in the set.
On-chain data consists of note commitments, nullifiers, and encrypted notes. Commitments reveal nothing about value, owner, or randomness (Poseidon2 is a one-way function). Nullifiers reveal nothing (derived from private data). Encrypted notes are indistinguishable from random bytes to anyone without the viewing key.
Transaction graph analysis is thwarted because there is no public link between nullifiers and commitments — the link exists only inside the ZK proof, which reveals nothing beyond its public inputs. The note Merkle tree grows monotonically; old commitments are never removed, so the anonymity set grows over time.
Current limitation: value_public leaks the per-note value. A sophisticated observer can correlate transactions by value. A future version will add blinded value commitments.
UltraHonk (the proof system underlying Barretenberg) is a polynomial IOP compiled via the Fiat-Shamir transform into a non-interactive argument. It does not require a circuit-specific trusted setup. The only global parameter is the BN254 elliptic curve, which has been independently verified and is used widely (Ethereum's BN254 precompile, Zcash). There is no trapdoor.
As a PoW chain, Lethe is vulnerable to a majority hashrate attack. An attacker controlling more than 50% of the hashrate can rewrite recent chain history. This is a known limitation of all PoW systems, mitigated by: (a) finality by depth — merchants can wait for N confirmations; (b) the chain selection rule uses cumulative work, making deep reorgs prohibitively expensive; (c) as hashrate grows with adoption, the cost of a 51% attack rises proportionally.
Miners select transactions by fee. A minimum-fee policy (enforced in TxBuilder) prevents zero-fee spam. The API rate limiter provides a secondary defence against mempool flooding from a single source.
The Lethe reference implementation is a Rust workspace with the following crates:
| Crate | Description |
|---|---|
| lethe-core | Cryptographic primitives, note types, Merkle tree, ZK proof types, prover/verifier wrappers, wallet logic |
| lethe-node | Full node: P2P (libp2p), block production, mempool, chain state, HTTP API |
| lethe-wallet | Desktop wallet built with Tauri (Rust backend + React frontend) |
| lethe-circuits | Noir circuits: spend, output, compliance; compiled artifacts and VKs |
| lethe-contracts | Solidity: ComplianceRegistry, LetheAnchor, ILetheVerifier interface |
As of this writing, the test suite includes:
lethe-core unit tests (hash KATs, Merkle tree, commitment/nullifier, prover stubs, key derivation, encryption)lethe-node integration tests (block production, PoW mining, chain state, fork buffer, block builder)A GitHub Actions workflow runs on every push: Rust build + nextest, Clippy lints, rustfmt, Noir circuit tests, Hardhat contract tests, cargo-audit for dependency vulnerabilities, and a bb prove/bb verify end-to-end test when Barretenberg is available.
The following items are not yet implemented and are required before a mainnet launch:
| Item | Priority | Description |
|---|---|---|
| Persistent chain state | Critical | Chain state is currently in-memory only. A sled/rocksdb backend is needed for node restarts to survive without restarting from genesis. |
| Initial block download | Critical | New nodes cannot sync from peers — only the genesis block is initialised. A block download protocol over libp2p is needed. |
| Wallet-node sync | Critical | The wallet has no mechanism to poll the node for new blocks and update its note store incrementally. |
| Real ZK proofs in wallet | Critical | The wallet uses a stub prover. Real Barretenberg proofs must be wired to the Tauri send flow before mainnet. |
| Note encryption upgrade | High | Current encryption ties to the viewing key directly. A proper ECDH-based ephemeral key scheme provides forward secrecy and sender anonymity. |
| Blinded value commitments | High | value_public leaks per-note amounts. Pedersen/homomorphic commitments with range proofs would hide amounts while still proving conservation. |
| Light client protocol | Medium | Wallets currently need a trusted full node. A compact block filter or FlyClient-style protocol would enable trustless light clients. |
| Mainnet genesis parameters | High | Genesis difficulty, initial bits, and bootstrap peers must be set. A public testnet should run for at least 3 months before mainnet. |
| Independent security audit | Critical | The cryptographic protocols, circuit constraints, and consensus logic should be reviewed by an independent security firm before mainnet launch. |
Lethe Network demonstrates that it is possible to build a fully-shielded digital cash system without a trusted setup, using modern transparent proof systems. The key insight is that UltraHonk (via Barretenberg and Noir) provides sufficiently compact proofs — comparable to Groth16 in size, without the ceremony.
The compliance module addresses the practical barrier that has prevented privacy coins from achieving exchange listings: exchanges can verify user compliance with sanctions requirements without learning any identifying information about the user. This property is unique to cryptographic systems; no traditional KYC approach can offer it.
The system is not yet ready for mainnet deployment. The items in §10 — particularly persistent chain state, IBD, wallet-node sync, and a security audit — must be completed first. However, the cryptographic foundations and core consensus rules are implemented, tested, and working. The path to mainnet is a software engineering challenge, not a cryptographic one.
noir-lang.org.github.com/AztecProtocol/barretenberg.libp2p.io.