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.
| Property | Value |
|---|---|
| Chain | Solana (devnet first) |
| Denominations | 0.1 · 1 · 10 SOL |
| Tree height | 20 (1,048,576 leaves per denomination) |
| Hash | Poseidon (BN254 scalar field) |
| Proof system | Groth16, one trusted setup per circuit |
| Proof size | 256 bytes |
| Verify cost | ≈ 195k compute units |
| Upgrade authority | Revoked 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.
| Account | Seeds | Size | Writable by |
|---|---|---|---|
| Vault | ["vault", denom] | 1,096 B | deposit, withdraw |
| TreeState | ["tree", denom] | 3,328 B | deposit |
| SpentMarker | ["spent", marker] | 41 B | withdraw |
| FeeConfig | ["fees"] | 72 B | governance (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 secretA 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.
| Signal | Visibility | Constraint |
|---|---|---|
| root | public | must equal one of 64 stored roots |
| marker | public | must not exist as a SpentMarker PDA |
| recipient | public | hashed into proof; cannot be swapped |
| relayer_fee | public | ≤ fee_cap for the denomination |
| secret | private | poseidon(secret, marker) == leaf |
| path[20] | private | hash_up(leaf, path) == root |
06
Guarantees
Requirements written so they can be checked against the code. Not deployed, not audited.
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
| Denomination | Network fee | Relayer cap | Net received | Effective cost |
|---|---|---|---|---|
| 0.1 SOL | 0.000005 | 0.0012 | 0.098795 | 1.21% |
| 1 SOL | 0.000005 | 0.0035 | 0.996495 | 0.35% |
| 10 SOL | 0.000005 | 0.0120 | 9.987995 | 0.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.
| Item | Detail |
|---|---|
| Ticker | $ZKLEDGER |
| Contract | CA : pump |
| Use | Page access · rent treasury |
| Claim on deposits | None |
| Mint authority | Revoked |
10
Threat model
| Adversary | Can see | Cannot see | Mitigation |
|---|---|---|---|
| Chain observer | Deposits, withdrawals, amounts | Which deposit paid which exit | Fixed denominations, shared tree |
| Relayer | Recipient and fee | Your deposit, your note | Recipient bound in-circuit |
| Timing analyst | Deposit and exit slots | Nothing, if you wait | Deep sets, delayed exits |
| This website | Your public address, if connected | Your note, ever | Note 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.
