Zk Ledger

Technical reference

How the vault holds nothing.

This document describes the Zk Ledger program as specified for Solana devnet. Field names, account layouts and figures are from the current draft and will be pinned against an audit before mainnet. Nothing here is deployed.

01

Overview

Zk Ledger is a fixed denomination shielded vault. A deposit inserts a commitment into an append-only tree and transfers SOL to a program derived address. A withdrawal submits a zero knowledge proof that the caller knows a note whose commitment is in the tree, and that the note's spend marker has not been recorded before. The program never learns which leaf was spent.

PropertyValue
ChainSolana (devnet first)
Denominations0.1 · 1 · 10 SOL
Tree height20 (1,048,576 leaves per denomination)
HashPoseidon (BN254 scalar field)
Proof systemGroth16, one trusted setup per circuit
Proof size256 bytes
Verify cost≈ 195k compute units
Upgrade authorityRevoked before mainnet

02

Accounts

One vault account exists per denomination. Every account below is a PDA derived from the program id, so no keypair controls any of them.

AccountSeedsSizeWritable by
Vault["vault", denom]1,096 Bdeposit, withdraw
TreeState["tree", denom]3,328 Bdeposit
SpentMarker["spent", marker]41 Bwithdraw
FeeConfig["fees"]72 Bgovernance (revoked at mainnet)

03

Note construction

The note is drawn in your browser and never leaves it. Only the commitment is published.

secret   = random_32_bytes()      // proves ownership
marker   = random_32_bytes()      // becomes the spend marker
commit   = poseidon(secret, marker)

note     = "zkledger:v1:" + denom + ":" + base58(secret || marker)
// deposit publishes commit only
// withdraw reveals marker, never secret

A note string is the entire claim on a deposit. There is no server copy, so a lost note means permanently sealed funds.

04

Commitment tree

Commitments are appended to a height-20 incremental Merkle tree. The program keeps the last 64 roots so a proof built against a slightly stale root still verifies, which matters because deposits keep arriving while you assemble a withdrawal.

insert(commit):
  require(next_index < 2^20)
  path = filled_subtrees(next_index)
  root = hash_up(commit, path)
  roots.push_ring(root)          // 64-slot ring buffer
  next_index += 1
  emit Deposited { commit, next_index - 1, slot }

05

Withdrawal circuit

Public inputs are the root, the spend marker, the recipient, the relayer and the relayer fee. Private inputs are the secret and the Merkle path. Binding the recipient and fee into the proof is what stops a relayer from redirecting funds or inflating its own cut.

SignalVisibilityConstraint
rootpublicmust equal one of 64 stored roots
markerpublicmust not exist as a SpentMarker PDA
recipientpublichashed into proof; cannot be swapped
relayer_feepublic≤ fee_cap for the denomination
secretprivateposeidon(secret, marker) == leaf
path[20]privatehash_up(leaf, path) == root

06

Guarantees

Requirements written so they can be checked against the code. Not deployed, not audited.

Nobody custodies the deposits.
Only a valid proof pays out.
Value cannot be minted.
A deposit cannot be withdrawn twice.
A relayer cannot alter a withdrawal.
The pause key cannot block an exit.

07

Relayer contract

Every Solana transaction records a fee payer in the clear. If you pay it, that wallet is the link you came here to remove. Relayers submit proofs on behalf of withdrawers and are compensated inside the withdrawal itself.

POST /relay
{
  "denom": "1",
  "proof": "<256 bytes base64>",
  "root": "0x…",
  "marker": "0x…",
  "recipient": "9fQ…T4a",
  "relayer_fee": "0.0035"
}
-> 200 { "signature": "…", "slot": 318294112 }
-> 409 { "error": "marker_spent" }
-> 422 { "error": "stale_root" }

08

Fee table

DenominationNetwork feeRelayer capNet receivedEffective cost
0.1 SOL0.0000050.00120.0987951.21%
1 SOL0.0000050.00350.9964950.35%
10 SOL0.0000050.01209.9879950.12%

Example figures. Caps are enforced in-circuit, so a relayer cannot take more than the row above.

09

$ZKLEDGER

$ZKLEDGER gates the vault pages today, and its creator fees pay the rent Solana charges to store the program. It carries no claim on anything inside the vault: deposits are claimed by notes only, and the program has no concept of a token balance.

ItemDetail
Ticker$ZKLEDGER
ContractCA : pump
UsePage access · rent treasury
Claim on depositsNone
Mint authorityRevoked

10

Threat model

AdversaryCan seeCannot seeMitigation
Chain observerDeposits, withdrawals, amountsWhich deposit paid which exitFixed denominations, shared tree
RelayerRecipient and feeYour deposit, your noteRecipient bound in-circuit
Timing analystDeposit and exit slotsNothing, if you waitDeep sets, delayed exits
This websiteYour public address, if connectedYour note, everNote generated and stored client-side

The honest limit: a vault is weakest the day it opens, and an exit taken minutes after a deposit narrows the crowd to whoever else moved in those minutes. Privacy here is arithmetic, and arithmetic needs company.