Appearance
Cryptographic Foundations
Three primitives carry a Veilix note: a Poseidon commitment, a Merkle tree of those commitments, and a Groth16 proof that you know one of them.
The note
A note is an unspent output. It has:
| Field | Meaning |
|---|---|
| Amount | Base units of one asset |
| Spending key | A random field element. Its public key is Poseidon(privateKey). This key is not a Solana keypair. |
| Blinding | A second random field element, so equal amounts do not share a commitment |
| Mint | The asset. Empty means SOL |
The commitment is:
text
Poseidon(amount, publicKey, blinding, mintField)mintField is 0 for SOL. For a token it is the first 31 bytes of the mint address, interpreted as a field element. The commitment does not include the leaf index. The same note has the same commitment wherever it sits.
Once the note is inserted at index i, its nullifier is:
text
Poseidon(commitment, i, Poseidon(privateKey, commitment, i))The inner hash is a signature by the spending key over the commitment and the index. An observer who sees the nullifier cannot walk backward to the commitment. The holder of the spending key can recompute the nullifier and watch whether it has been published.
The note string packs the amount, the spending key, and the blinding. The mint is appended for tokens. Anyone who has the string can spend the note.
The tree
Each asset has one Poseidon Merkle tree of height 26, so it can hold 2²⁶ leaves. Empty leaves use a fixed zero value that matches the program. Internal nodes are Poseidon(left, right).
The program stores the latest 100 roots. A proof must use one of them. That window lets a client prove against a root that is a few updates behind the tip, which matters when other people deposit while the proof is being built. A root older than the window is rejected.
Spending a note requires the Merkle path from its leaf to the root. The client gets the ordered leaves from the indexer and rebuilds the tree locally. The path is a private input. The root is public.
The circuit
Every Veilix transaction is a 2-input, 2-output proof.
Public inputs:
- the Merkle root
- the public amount (positive for a deposit into the pool, negative for a withdrawal out of it)
- a hash of the external data
- the mint field
- two input nullifiers
- two output commitments
Private inputs include the input amounts, spending keys, blindings, leaf indexes, and Merkle paths, and the output amounts, public keys, and blindings.
The circuit checks, in substance:
- Each real input’s commitment opens under the spending key and sits at the claimed index on a path to the public root.
- Each input nullifier matches that note and index.
- Each output commitment matches the output amount, key, blinding, and mint.
- Value is conserved: inputs plus the public amount equal outputs.
- The external-data hash matches the recipient, relayer, fee, mint, public amount, and encrypted outputs.
Zero-amount padding fills unused inputs and outputs, so a deposit and a withdrawal share one circuit.
The external-data hash is SHA-256 of those fields, with the first byte cleared so the digest fits in the BN254 scalar field. The program recomputes that hash from the accounts and arguments. A proof built for one recipient will not verify for another.
Proofs are Groth16. The client runs the proving key locally. The program verifies. Generating a proof takes a few seconds on a normal device.
Encrypted outputs
Next to each commitment the program stores up to 256 bytes of ciphertext. That blob is not an input to the commitment. It exists so the owner can find the note again.
When a backup is requested, the client seals the amount, spending key, and blinding with a secret-box key. The key is the first 32 bytes of the hash of a wallet signature over the message veilix:notes:v1. Only a wallet that can produce that signature can open those blobs. The chain never sees the plaintext.
A deposit usually writes one ciphertext, on the real output. A partial withdrawal writes one ciphertext on the change note, when the encryption key is loaded. Empty outputs carry empty ciphertext.