lacuna
la·cu·na /ləˈkjuːnə/ n.
1a gap; the part of a record that is missing.
2a shielded pool on Solana. It goes in on the record. It comes out without a past.
la·cu·na /ləˈkjuːnə/ n.
1a gap; the part of a record that is missing.
2a shielded pool on Solana. It goes in on the record. It comes out without a past.
A deposit publishes a commitment and nothing else. A withdrawal publishes a proof that some commitment in the pool is yours — not which — and a nullifier that stops it being spent twice. The chain sees roots, proofs and nullifiers; the edge between them is the part of the record that is missing. $LAC is issued on one fixed curve, and a share of every withdrawal retires it.
$ lac verify --root latest → ok
Abstract
We describe lacuna, a fixed-denomination shielded pool on Solana, and $LAC, the token issued alongside it. A deposit publishes a commitment to a secret. A withdrawal publishes a proof that the prover knows the secret behind some commitment in the pool, without saying which, and a nullifier that prevents the same commitment from being withdrawn twice. The pool holds value; the chain holds only commitments, nullifiers and roots.
Privacy here is not a feature of the code. It is a property of the crowd: a withdrawal is exactly as private as the set of deposits it could plausibly have come from.1 Everything below is an attempt to make that set large, honest, and legible.
A proof is a way of saying yes without saying which.
Each depositor draws two random field elements: a secret s and a nullifier seed r. The commitment is C = H(r ‖ s), where H is Poseidon.2 The pair (s, r) — the note — never leaves the depositor. It is the only thing that can ever withdraw the funds.
deposit(C) moves one denomination into the pool vault and appends C to an incremental Merkle
tree of depth 20. The program keeps the last 32 roots, so a proof built against a slightly stale tree still
verifies.
withdraw(π, root, N, to, fee) checks four things: that root is among the stored roots;
that the nullifier N = H(r) has not been seen; that π is a valid Groth16
proof3 of the statement below; and that fee is less than the
denomination. It then records N and pays out.
// statement proven by π public root, N, to, fee private s, r, path, index assert N == H(r) assert root == merkle(H(r ‖ s), path, index)
to and fee are public inputs bound into π, so whoever submits the transaction cannot
redirect the payout or raise its own fee after the fact.
Four instructions. Proofs are 256 bytes: two G1 points and one G2 point on bn254, uncompressed.
deposit (commitment: [u8; 32]) → index: u32 withdraw (proof: [u8; 256], root: [u8; 32], nullifier: [u8; 32], to: Pubkey, fee: u64) → () is_spent (nullifier: [u8; 32]) → bool roots () → [[u8; 32]; 32]
$LAC has one issuance path. Tokens are minted when SOL is paid into the curve and burned when they are returned to it, so the reserve always equals the area underneath. There was no presale. There is no allocation outside the curve.
p(s) = p0 + k·s2(1)
R(s) = p0·s + k·s3 ⁄ 3(2)
Here s is circulating supply and R the SOL the curve holds. buy and
sell both take a min_out; the curve never fills worse than the caller asked.
The parameters are fixed at initialisation. No key can change them.
k is chosen so that k·s*2 = 6p0. Then R(s*) = 84 SOL, p(s*) = 7p0, and R(s*) ⁄ p(s*) is exactly 300,000,000 — the remainder of the cap. At close, the reserve and the remainder are paired in a constant-product pool that opens at the price the curve closed at, and the LP position is burned in the same transaction. After close, supply can only fall.
A withdrawing user rarely has an unlinked account holding SOL for the transaction fee. That is the point. So
withdrawals are submitted by relayers: anyone may run one, and any relayer can submit any proof. The relayer
pays the network fee and is repaid from fee, which the prover chose and bound into π.
From every withdrawal the program withholds 0.30% of the denomination. After close it is swapped for $LAC in the pool and burned in the same instruction. Before close it accrues in a fee vault that is emptied the same way in the first block after close. The program has no instruction that sends it anywhere else.
Settlement is final when the transaction is. The indexer is a convenience, not a source of truth; every row below can be recomputed from program logs.
| Time | Action | Δ | Hash |
|---|
What the construction does not do, stated plainly.
Amounts. Denominations are fixed at 1, 10 and 100 SOL. An arbitrary amount is a fingerprint; the pool refuses them.
Timing. The protocol cannot hide when you act. A deposit followed by a withdrawal a minute later, in a quiet hour, shrinks the set to roughly one.
Set size. Privacy scales with the unspent deposits in the same denomination. The indexer publishes the live count above. Read it before you rely on it.
Association sets. A withdrawal may attach a second proof: membership in a published subset of deposits, for example one that excludes deposits traced to known thefts.4 It proves what the funds are not, without revealing what they are. Optional at the protocol level; expected at most exits.
Setup. Groth16 needs a per-circuit trusted setup. Soundness holds if at least one ceremony participant discarded their contribution. The transcript is public.5
Keys. The program upgrade authority is a 3-of-5 multisig until rev 1.0, after which it is set to none. Until then, the code can change.
Audit. No audit is claimed on this page. Treat the code as unaudited until a report is linked here.
For a withdrawal in denomination d at time t, the set is the unspent deposits in d before t. An observer's best guess is uniform over it only if nothing else — timing, gas payer, habit — narrows it. ↑
L. Grassi, D. Khovratovich, C. Rechberger, A. Roy, M. Schofnegger. Poseidon: A New Hash Function for Zero-Knowledge Proof Systems. USENIX Security, 2021. ↑
J. Groth. On the Size of Pairing-Based Non-interactive Arguments. EUROCRYPT, 2016. Verification runs on-chain through Solana's alt_bn128 syscalls. ↑
V. Buterin, J. Illum, M. Nadler, F. Schär, A. Soleimani. Blockchain Privacy and Regulatory Compliance: Towards a Practical Equilibrium. 2023. ↑
Ceremony transcript, sha256: —— (to be published with rev 0.4). ↑