Security Notes
Operational risks and cryptographic boundaries
Overview#
Mini Veil is a privacy protocol that handles real value. These security notes document operational risks, cryptographic boundaries, and best practices for safe usage.
Use at Your Own Risk
Mini Veil is experimental software. It has not undergone a formal security audit. Use only on devnet for testing purposes. Mainnet deployment should follow a comprehensive audit.
Key Management#
Key Loss Is Permanent#
Protocol keys are derived from your wallet's Ed25519 signing key. If you lose access to your wallet:
- All funds in the anonymity pool become inaccessible
- Unclaimed stealth payments become unclaimable
- There is no recovery or social recovery mechanism
- No one, including the protocol developers, can help recover your keys
Key Exposure#
- Protocol keys are stored in
localStoragewhen derived - Do not share your spending private key
- You may share your viewing private key to allow others to scan for your payments
- The
generateVeilKeysfunction produces deterministic keys — anyone who knows your master seed can derive all sub-keys
Key Registration#
// Cost: ~0.005 SOL
Keys are registered on-chain via register_keys.
If a wallet changes (e.g., hardware wallet rotation), old keys remain registered.
Unregistering old keys is not supported.
Key Rotation
If you rotate your wallet keys, previously registered KeyRegistry entries remain on-chain. Anyone with the old spending key could claim stealth payments sent to the old meta-address. Always ensure old wallets are monitored or drained before rotation.
Cryptographic Boundaries#
Nullifier Collisions#
If two notes from different spending keys produce the same nullifier, the second unshield will be rejected by the on-chain NullifierAlreadySpent check. However, the collision probability is:
Same key: Counter ensures uniqueness
Different keys: Poseidon collision resistance + 254-bit random secret
Collision probability: Negligible (~2^-254 per pair)
Stealth Address Collisions#
If two senders generate the same stealth address for the same recipient simultaneously:
- The first claim succeeds
- The second fails (account already drained)
- The sender loses the transaction fee but funds remain in the vault
The ephemeral key contains 254 bits of entropy. Collision probability is negligible.
BN254 Field Overflow#
~62% of Ed25519 public keys exceed the BN254 scalar field modulus. The generateStealthAddress function retries up to 40 times with new ephemeral keys, giving approximately 3×10⁻⁹ failure probability per address generation.
Relayer Security#
Multisig Root Updates#
The Merkle root update requires 2-of-3 oracle signatures. This prevents:
- A single compromised oracle from falsifying the root
- Root rollback attacks (old roots cannot be re-submitted without 2 oracle signatures)
- Censorship (any 2 of 3 oracles can authorize a root update)
Relayer Trust Model#
The relayer is permissionless. Anyone can run one. The trust model:
- Liveness. If all relayers go offline, new deposits cannot be proven. However, existing commitments remain valid and can be proven once the relayer returns.
- Correctness. The relayer cannot steal funds. It only manages the Merkle tree state. The vault is controlled by PDA seeds, not the relayer admin key.
- Front-running. A malicious relayer could front-run root updates. This is mitigated by the multisig — 2 of 3 oracles would need to collude.
Relayer Independence
For production, run multiple independent relayers with different oracle sets. Each denomination's tree can be managed by a different relayer instance.
On-chain Risks#
Vault Solvency#
The total deposits (MixerState.total_shielded) should equal the vault's SOL balance. If there is a discrepancy (e.g., due to forced SOL transfers), withdrawals may fail with insufficient vault funds.
Nullifier Account Rent#
Each unshield creates a nullifier PDA at cost ~0.00089 SOL (or ~0.000015 SOL for compressed). The fee payer is the sender's wallet, not the vault. Ensure adequate SOL for both the denomination and the nullifier rent.
Compute Limits#
Groth16 verification requires 700,000 compute units — significantly more than typical Solana transactions. The ComputeBudgetProgram.setComputeUnitLimit({ units: 700_000 }) instruction is required before the unshield instruction. Without it, the transaction will exceed the default compute budget and fail.
Test Suite#
The protocol is tested across 10 test files in the tests/ directory:
| Test suite | Coverage |
|---|---|
| day1.ts | Key generation, registration, multisig root updates |
| day2.ts | Full shield → unshield lifecycle, double-spend prevention |
| day3.ts | ETA encrypted balance updates |
| day4.ts | Stealth addresses, scanning, claiming |
| day4-compressed.ts | Compressed nullifier variant |
| day5.ts | SPL token mixer |
| day5-compressed.ts | Compressed + SPL tokens |
| day5-webhook.ts | Relayer event processing |
Run with:
yarn test
# or
anchor test