lac_una

▤ working paper · rev 0.3

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.

Doc
LAC-WP-003
Rev
0.3 · 2026-09
Status
draft
Token
$LAC
Chain
solana

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

Program ID
mainnet-beta · click to copy
Market cap
——
——
Supply
——
——
relayer · verifier · indexer
slot —— · set —— · ——

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.

Fig. 1 The pool drawn as a lacuna: the field is dense everywhere except where notes are held. Orange marks commitments added this epoch. Nothing that leaves on the right can be traced to what entered on the left.
01

mechanism

Each depositor draws two random field elements: a secret s and a nullifier seed r. The commitment is C = H(rs), 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.

deposit(C) 1 · 10 · 100 SOL root leaves C = H(r ‖ s) π ⊢ C ∈ tree(root) withdraw(π, N) N = H(r) → to, − fee RECORDED GAP UNLINKED
Fig. 2 A commitment enters as a leaf (orange). The withdrawal proves membership against the root. No edge connects it to the leaf.

1.1 interface

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]
02

the curve

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

p₀
4.0 × 10⁻⁸ SOL
opening price
k
6 p₀ ⁄ s*²
≈ 4.898 × 10⁻²⁵
s*
700,000,000
close
cap
1,000,000,000
hard

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.

Fig. 3 Price against supply. The hatched area is R(s); the dot is the current supply, read live from the indexer.
03

settle

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.

Log · program events stream
TimeActionΔHash
04

limits

What the construction does not do, stated plainly.

  1. 4.1

    Amounts. Denominations are fixed at 1, 10 and 100 SOL. An arbitrary amount is a fingerprint; the pool refuses them.

  2. 4.2

    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.

  3. 4.3

    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.

  4. 4.4

    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.

  5. 4.5

    Setup. Groth16 needs a per-circuit trusted setup. Soundness holds if at least one ceremony participant discarded their contribution. The transcript is public.5

  6. 4.6

    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.

  7. 4.7

    Audit. No audit is claimed on this page. Treat the code as unaudited until a report is linked here.

05

notes

  1. 1

    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.

  2. 2

    L. Grassi, D. Khovratovich, C. Rechberger, A. Roy, M. Schofnegger. Poseidon: A New Hash Function for Zero-Knowledge Proof Systems. USENIX Security, 2021.

  3. 3

    J. Groth. On the Size of Pairing-Based Non-interactive Arguments. EUROCRYPT, 2016. Verification runs on-chain through Solana's alt_bn128 syscalls.

  4. 4

    V. Buterin, J. Illum, M. Nadler, F. Schär, A. Soleimani. Blockchain Privacy and Regulatory Compliance: Towards a Practical Equilibrium. 2023.

  5. 5

    Ceremony transcript, sha256: —— (to be published with rev 0.4).