Documentation
Wallet setup, node operation, mining, the REST API, and everything you need to run Lethe locally or on a server.
Getting started
The fastest way to run Lethe locally. You need Rust (stable), cargo, and Node.js ≥ 18.
# 1. Clone the repository
git clone https://github.com/lethe-network/lethe
cd lethe
# 2. Build the node
cargo build -p lethe-node --release
# 3. Start the node + wallet together
./scripts/dev.sh
dev.sh builds the node, starts it on port 9000 (P2P) and 9001 (API), waits for the REST API to respond, then launches the Tauri wallet.
To mine, set your coinbase address before running: LETHE_COINBASE_ADDRESS=<your-address> ./scripts/dev.sh. The address comes from your wallet's address chip.
Install via rustup. The workspace uses stable Rust — no nightly needed for the node or wallet.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Tauri needs the platform's WebView library and a few system tools.
# macOS
xcode-select --install
# Ubuntu / Debian
sudo apt install libwebkit2gtk-4.0-dev build-essential \
libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
The wallet frontend is React + Vite. Node.js is only needed to build the UI — not to run the node.
# via nvm
nvm install 20 && nvm use 20
Only needed if you're working on the Noir ZK circuits in lethe-circuits/.
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
noirup
# Build the entire Rust workspace
cargo build --workspace
# Build just the node (faster)
cargo build -p lethe-node
# Build the wallet (Tauri compiles the Rust backend + bundles the React UI)
cd lethe-wallet && npm install && npm run tauri build
# All Rust tests (requires cargo-nextest)
cargo nextest run --workspace
# Install nextest if you don't have it
cargo install cargo-nextest --locked
# Noir circuit tests
cd lethe-circuits && nargo test --workspace
# Solidity contract tests
cd lethe-contracts && npx hardhat test
Wallet
The Lethe wallet is a native desktop application (Tauri). Your keys are generated locally and stored in an encrypted keystore — nothing is sent to a server.
Launch the app via ./scripts/dev.sh or run the built binary from lethe-wallet/src-tauri/target/release/. The Setup screen appears on first run.
Click Create, enter a strong password, and click Generate. A 24-word BIP39 seed phrase is shown. Write it down and store it offline. This is your only backup.
The wallet shows the seed phrase one more time. Confirm you've written it down. The keystore is then encrypted with Argon2id + your password and saved locally.
The wallet opens to the Dashboard. Your address (a 64-char hex string) appears in the top navigation chip. Share this address to receive LTH.
The keystore is encrypted with Argon2id (memory-hard KDF) + AES-256-GCM. Your spending key never leaves the device in plaintext. There is no account recovery — the seed phrase is the only backup mechanism.
If a keystore already exists on disk, creating or importing a new wallet requires explicit confirmation (force: true via the Tauri API). The wallet UI displays a warning before overwriting. This prevents accidental loss of an existing wallet.
Your Lethe address is the 64-character hex string shown in the top navigation bar. It is your pk_d — the BN254 public key from which all note ownership is derived.
To receive LTH:
Copy your address from the navigation chip. Give it to the sender, or use it as your mining coinbase address.
After the sender's transaction is mined, click Scan for notes on the Dashboard. The wallet fetches all notes from the node and tries to decrypt each one with your viewing key. Notes that decrypt successfully are yours.
Scanning is completely private. The node serves all encrypted notes to anyone who asks — it cannot tell which wallet is scanning or which notes belong to which wallet.
To send LTH, navigate to Send. The wallet will:
The wallet picks unspent notes from your local store that cover the amount + fee. This is done locally — the node never sees which notes you're spending until the transaction is broadcast.
A spend proof is generated for each input note, and an output proof for each new note. Each proof exposes a value_public field that the node uses to verify conservation (∑ inputs = ∑ outputs + fee) without learning amounts. This runs locally using the Barretenberg prover.
The signed transaction (proofs + nullifiers + encrypted output notes) is posted to the node's mempool via POST /v1/transactions. The node verifies the proofs and propagates the transaction over P2P gossip.
If your selected notes exceed the amount + fee, a change output note is automatically created back to your own address. This change note appears in your balance after the next scan.
Lethe does not track your balance on-chain. Your balance is computed locally by scanning all encrypted notes on the chain and trying to decrypt each one with your viewing key.
Click Scan for notes on the Dashboard to trigger a scan. The wallet:
GET /v1/nullifiers and removes any matching notes from your local storeEncryptedNote objects from GET /v1/notesephem_bytes for key derivationNote struct with matching commitment are added to your note storeGET /v1/mempool/notes for notes in the mempool (not yet confirmed)Scanning is idempotent. Already-known notes are skipped. The node cannot determine which wallet is scanning — all encrypted notes are served to all callers, by design.
Navigate to Compliance in the wallet to generate a ZK non-membership proof for an exchange listing or regulatory requirement.
Paste the Merkle root hash provided by the exchange or compliance authority. This is a 64-char hex string published by the exchange (on-chain or in their docs).
Click Generate. The wallet runs the compliance Noir circuit locally, producing a ZK proof that your address is not in the sanctions tree.
Export the proof and submit it to the exchange. They call ComplianceRegistry.verifyCompliance() on-chain to confirm it's valid.
Node
Running a node validates transactions, propagates blocks over P2P, and (optionally) mines new blocks.
# Build the node binary
cargo build -p lethe-node --release
# Start with defaults (P2P :9000, API :9001, data in ~/.local/share/lethe)
./target/release/lethe-node
# With custom port and data directory
LETHE_PORT=9000 \
LETHE_DATA_DIR=/var/lib/lethe \
RUST_LOG=info \
./target/release/lethe-node
The node starts listening for P2P connections on TCP port 9000 and exposes a REST API on port 9001. The genesis block is created automatically if no chain state exists.
Mining is controlled by a single environment variable. If it is not set, the node runs in relay-only mode.
# Get your address from the wallet's navigation chip, then:
LETHE_COINBASE_ADDRESS=a8b38ec87209511e9088a80add9e0c684c7e9d5375349ab88a4d9e9cab91752a \
RUST_LOG=info \
./target/release/lethe-node
The coinbase address is your wallet's pk_d — the 64-character hex string shown in the top navigation bar. Every block mined creates a new encrypted note locked to this address. Scan for notes in your wallet to see the rewards.
The genesis difficulty (0x207fffff) is intentionally easy — roughly 50% of hashes satisfy the target. Blocks mine in milliseconds on any hardware at genesis. Difficulty retargets every 504 blocks toward the 2-minute target.
All configuration is via environment variables. No config file needed.
| Variable | Default | Description |
|---|---|---|
| LETHE_PORT | 9000 | TCP port for P2P connections. The REST API listens on port+1 (default 9001). |
| LETHE_DATA_DIR | ~/.local/share/lethe | Directory for chain state, nullifier set, and node keypair. |
| LETHE_COINBASE_ADDRESS | — | Hex-encoded 32-byte pk_d (64 chars). Required to enable mining. If unset, node is relay-only. |
| LETHE_BLOCK_TX_LIMIT | 100 | Maximum transactions included per block. |
| LETHE_BOOTSTRAP_PEERS | — | Comma-separated list of Multiaddr bootstrap peers. |
| LETHE_INITIAL_BITS | 0x207fffff | Override the genesis difficulty (compact nBits format). Use 0x1e00ffff for mainnet-level genesis. |
| LETHE_NODE_URL | http://127.0.0.1:9001 | Used by the wallet to locate the node REST API. |
| LETHE_RATE_CAPACITY | 60 | Token bucket capacity per IP address for API rate limiting. |
| LETHE_RATE_REFILL | 10 | Tokens refilled per second per IP. Requests above capacity get 429. |
| RUST_LOG | warn | Log level. Use info or debug for verbose output. |
To join an existing network, provide bootstrap peer addresses in libp2p Multiaddr format.
LETHE_BOOTSTRAP_PEERS=/ip4/192.168.1.10/tcp/9000/p2p/12D3KooW...,/ip4/10.0.0.5/tcp/9000/p2p/12D3KooW... \
./target/release/lethe-node
The node will dial each bootstrap peer on startup. Peer IDs are derived from the node's ed25519 keypair, which is generated on first run and stored in LETHE_DATA_DIR/node.key. Your node's peer ID and Multiaddr are logged on startup.
Example startup log:
[INFO] node listening listen_addr=/ip4/0.0.0.0/tcp/9000
[INFO] dialing bootstrap peer addr=/ip4/10.0.0.5/tcp/9000/p2p/12D3KooW...
[INFO] connection established peer_id=12D3KooW...
[INFO] ⛏ starting mining job height=507 bits=520164710
REST API
Returns the current chain state. No authentication required.
GET http://127.0.0.1:9001/v1/state
| Field | Type | Description |
|---|---|---|
| height | u64 | Current chain height. 0 = genesis. |
| merkle_root | hex string | Poseidon2 Merkle root of all note commitments, compressed BN254 Fq (32 bytes). |
| mempool_size | usize | Number of unconfirmed transactions in the mempool. |
| note_count | u64 | Total number of note commitments in the tree (= number of notes ever created). |
Returns all encrypted notes stored on-chain. The wallet scanner calls this to find received notes.
GET http://127.0.0.1:9001/v1/notes
| Field | Type | Description |
|---|---|---|
| enc.ephem_bytes | [u8; 32] | Ephemeral public key used for ECDH key agreement. Combined with the recipient's viewing key to derive the decryption key. |
| enc.nonce | [u8; 12] | ChaCha20-Poly1305 nonce (random 96-bit value). |
| enc.ciphertext | Vec<u8> | Bincode-serialised Note encrypted with ChaCha20-Poly1305. Includes a 16-byte Poly1305 authentication tag. |
| leaf_index | u64 | Position of this note's commitment in the Merkle tree. Used to fetch the Merkle auth path for spending (GET /v1/auth_path/:leaf_index). |
All notes are served to all callers. The node cannot determine which notes belong to which wallet. This is by design — differential serving would leak information.
Returns all spent nullifiers currently in the on-chain nullifier set. The wallet calls this during a scan to detect and remove notes that have already been spent.
GET http://127.0.0.1:9001/v1/nullifiers
| Field | Type | Description |
|---|---|---|
| (array element) | hex string | Compressed 32-byte Fq nullifier value. |
Returns a Merkle authentication path and the corresponding tree root for a given note commitment leaf. The wallet uses this to obtain the witness needed to generate a spend proof.
GET http://127.0.0.1:9001/v1/auth_path/42
| Field | Type | Description |
|---|---|---|
| siblings | Vec<hex string> | 32 sibling hashes from the leaf to the root (compressed Fq, 32 bytes each). |
| position | u64 | Leaf index — same as the request parameter. Use as the path position in the spend circuit. |
| merkle_root | hex string | The tree root at the moment the path was snapshotted. Use this as the spend circuit anchor — it is guaranteed to be consistent with the returned siblings. |
Always use the merkle_root from this response as your spend proof anchor. The root and path are returned as an atomic snapshot, so they are always consistent with each other.
Returns all encrypted output notes from transactions currently in the mempool (not yet confirmed on-chain). The wallet uses this to compute the pending balance shown in the dashboard.
GET http://127.0.0.1:9001/v1/mempool/notes
The response is a flat list of EncryptedNote objects — the same format as the enc field in GET /v1/notes, but without a leaf_index (mempool notes have not yet been assigned a tree position). The wallet scans these with the viewing key to estimate the pending balance.
Submit a transaction to the node's mempool. The body is the hex-encoded bincode serialisation of a Transaction.
POST http://127.0.0.1:9001/v1/transactions
Content-Type: text/plain
7b22737065... (hex-encoded bincode Transaction)
| Status | Meaning |
|---|---|
| 200 | Transaction accepted into mempool. Returns hex tx_id. |
| 400 | Malformed request — hex decode failed or bincode deserialise failed. |
| 422 | Transaction rejected — double spend, invalid proof, or balance mismatch. |