Technical Whitepaper — v0.9 draft

Lethe Network: Private,
Trustless Digital Cash

Lethe Network Contributors  ·  2026
Abstract

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.

§ 1

Introduction

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.

Goals

Non-goals

§ 2

Design overview

The Lethe protocol is built around four core primitives:

PrimitiveFunctionInstantiation
NoteEncodes a unit of value with its owner and randomnessStruct (value: u64, owner: Fq, rho: Fq, rcm: Fq)
CommitmentCryptographic fingerprint of a note; inserted into the Merkle treePoseidon2(value, owner, rcm) over BN254 scalar field
NullifierOne-time tag derived from spending key and note; prevents double-spendPoseidon2(spending_key, note.rho)
ZK proofProves ownership and correct construction without revealing private dataUltraHonk 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.

§ 3

Cryptographic primitives

3.1 Hash function: Poseidon2

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:

hash_2(a, b) = Poseidon2_permutation([a, b, 0, 0], 4)[0]

3.2 Note commitment

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:

cm = Poseidon2(note.value, note.owner, note.rcm)

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.

3.3 Nullifier

To spend a note, the sender reveals a nullifier derived from their spending key and the note's one-time random tag (rho):

nf = Poseidon2(spending_key, note.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.

3.4 Key derivation

Lethe uses a simple three-level key hierarchy derived entirely from the spending key sk (a uniformly-random 32-byte secret):

vk = hash_2(sk, 0)     (viewing key)
pk_d = hash_2(vk, 0)     (payment address)

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.

3.5 Note encryption

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.

3.6 Merkle accumulator

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:

parent = hash_2(left_child, right_child)

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

§ 4

Transaction protocol

4.1 Transaction structure

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
}

4.2 Spend circuit

For each input note, the prover generates a spend proof. The circuit has the following interface:

VisibilityNameDescription
PrivatenoteThe full Note struct being spent
Privatespending_keysk — authorises the spend
Privatemerkle_path32 sibling hashes from leaf to root
Publicmerkle_rootThe Merkle root used; must match the path
Publicnf_publicPoseidon2(sk, note.rho) — the nullifier
PublicfeeTransaction fee; circuit asserts note.value ≥ fee
Publicvalue_publicnote.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.

4.3 Output circuit

For each output note, the prover generates an output proof. The circuit proves correct construction of the note commitment:

VisibilityNameDescription
PrivatenoteThe new Note struct (value, owner, rho, rcm)
Publiccm_publicPoseidon2(value, owner, rcm) — asserted equal to circuit-computed cm
Publicvalue_publicnote.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.

4.4 Value conservation

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:

∑ spend.value_public = ∑ output.value_public + fee

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.

4.5 Proof generation

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.

§ 5

Consensus mechanism

5.1 Proof of work

Lethe uses SHA-256d (double SHA-256), the same proof-of-work function as Bitcoin. A block is valid when:

SHA256(SHA256(bincode(header))) ≤ target

The target is encoded in compact nBits format (identical to Bitcoin's). Miners increment a 64-bit nonce until a valid hash is found.

5.2 Difficulty adjustment

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:

new_target = old_target × (actual_time / expected_time)

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.

5.3 Block structure

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.

5.4 Chain selection and reorganisation

Lethe uses the heaviest-chain rule: the valid chain with the highest cumulative proof of work is canonical. Work per block is:

block_work = 2128 / (target_as_u128 + 1)

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.

5.5 Emission schedule

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:

reward(height) = max( 50 LTH ÷ 2⌊height / 210000⌋, 0.01 LTH )
ParameterValue
Initial block reward50 LTH (5,000,000,000 atoms)
Halving interval210,000 blocks (~1.6 years)
Tail emission0.01 LTH (permanent floor)
Approx. total supply~21 million LTH
§ 6

Peer-to-peer network

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:

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.

REST API

The node exposes a minimal HTTP API (axum) for wallet communication:

MethodPathDescription
POST/v1/transactionsSubmit a transaction; returns 200 on acceptance, 422 on validation failure
GET/v1/stateChain 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.

§ 7

Compliance module

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:

  1. The exchange builds a sorted Merkle tree of sanctioned addresses and publishes the root on-chain via the ComplianceRegistry Solidity contract.
  2. The user runs the compliance circuit locally. Given their spending key (private), the circuit derives their address internally and proves that the address does not appear in the sanctions tree — without revealing the address to the prover or any observer.
  3. The exchange calls 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.

Non-membership proof

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:

neighbour.value < address < neighbour.next

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.

On-chain anchor

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().

§ 8

Security analysis

8.1 Counterfeiting

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.

8.2 Double spending

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.

8.3 Privacy

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.

8.4 Trusted setup

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.

8.5 51% attack

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.

8.6 Fee market

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.

§ 9

Implementation overview

The Lethe reference implementation is a Rust workspace with the following crates:

CrateDescription
lethe-coreCryptographic primitives, note types, Merkle tree, ZK proof types, prover/verifier wrappers, wallet logic
lethe-nodeFull node: P2P (libp2p), block production, mempool, chain state, HTTP API
lethe-walletDesktop wallet built with Tauri (Rust backend + React frontend)
lethe-circuitsNoir circuits: spend, output, compliance; compiled artifacts and VKs
lethe-contractsSolidity: ComplianceRegistry, LetheAnchor, ILetheVerifier interface

Test coverage

As of this writing, the test suite includes:

CI/CD

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.

§ 10

Road map

The following items are not yet implemented and are required before a mainnet launch:

ItemPriorityDescription
Persistent chain stateCriticalChain state is currently in-memory only. A sled/rocksdb backend is needed for node restarts to survive without restarting from genesis.
Initial block downloadCriticalNew nodes cannot sync from peers — only the genesis block is initialised. A block download protocol over libp2p is needed.
Wallet-node syncCriticalThe wallet has no mechanism to poll the node for new blocks and update its note store incrementally.
Real ZK proofs in walletCriticalThe wallet uses a stub prover. Real Barretenberg proofs must be wired to the Tauri send flow before mainnet.
Note encryption upgradeHighCurrent encryption ties to the viewing key directly. A proper ECDH-based ephemeral key scheme provides forward secrecy and sender anonymity.
Blinded value commitmentsHighvalue_public leaks per-note amounts. Pedersen/homomorphic commitments with range proofs would hide amounts while still proving conservation.
Light client protocolMediumWallets currently need a trusted full node. A compact block filter or FlyClient-style protocol would enable trustless light clients.
Mainnet genesis parametersHighGenesis difficulty, initial bits, and bootstrap peers must be set. A public testnet should run for at least 3 months before mainnet.
Independent security auditCriticalThe cryptographic protocols, circuit constraints, and consensus logic should be reviewed by an independent security firm before mainnet launch.
§ 11

Conclusion

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.


References

  1. [BCGGMTV14] Eli Ben-Sasson, Alessandro Chiesa, Christina Garman, Matthew Green, Ian Miers, Eran Tromer, Madars Virza. "Zerocash: Decentralized Anonymous Payments from Bitcoin." IEEE S&P 2014.
  2. [GHL+23] Lorenzo Grassi, Dmitry Khovratovich, Reinhard Lüftenegger, Christian Rechberger, Markus Schofnegger, Roman Walch. "Poseidon2: A Faster Version of the Poseidon Hash Function." AFRICACRYPT 2023.
  3. [KZG10] Aniket Kate, Gregory Zaverucha, Ian Goldberg. "Constant-Size Commitments to Polynomials and Their Applications." ASIACRYPT 2010.
  4. [MBKM19] Mary Maller, Sean Bowe, Markulf Kohlweiss, Sarah Meiklejohn. "Sonic: Zero-Knowledge SNARKs from Linear-Size Universal and Updateable Structured Reference Strings." CCS 2019.
  5. [Nakamoto08] Satoshi Nakamoto. "Bitcoin: A Peer-to-Peer Electronic Cash System." 2008.
  6. [Noir] Aztec Network. "Noir: A Domain Specific Language for SNARK Proving Systems." noir-lang.org.
  7. [BB] Aztec Network. "Barretenberg: An Elliptic Curve Library." github.com/AztecProtocol/barretenberg.
  8. [libp2p] Protocol Labs. "libp2p: Modular peer-to-peer networking stack." libp2p.io.