Cloak Technical Documentation
This document describes the Cloak protocol: cryptographic primitives, circuit design, relayer network, card issuance flow, and the trust assumptions that make private on-chain spending possible. It is the ground truth for anyone integrating, auditing, or operating a relayer.
Overview
Cloak is a non-custodial privacy layer that issues Visa-branded virtual cards and settles peer-to-peer transfers while breaking the on-chain link between the wallet that funds a card and the card itself. The protocol combines a shielded pool (a Merkle-based commitment set), a Groth16 proving system, and a permissionless relayer network that submits aggregated proofs to the settlement chain. A BIN sponsor issues the physical Visa program on the card-network side.
Cloak is not a mixer, a bridge, or a custodian. It does not hold user funds at any point. A deposit is a one-way commit into a shielded set; a spend is a zero-knowledge proof that authorizes the relayer to charge a card of equal value on the user's behalf. The card program is compliant under the BIN sponsor's own MSB / PI license; individual cardholders are not required to complete KYC against Cloak because Cloak is not the issuer of record.
Threat Model & Trust Assumptions
What Cloak protects against
- On-chain observers — any party indexing the settlement chain cannot determine which deposit funded which card or transfer. Deposits, withdrawals, and card-authorization events share a single indistinguishable nullifier set.
- Relayer operators — a single malicious relayer cannot deanonymize a transfer. Proofs carry no sender identity; the relayer learns only (commitment set root, nullifier, recipient payload, amount). IP metadata is stripped at the first hop.
- BIN sponsor / issuer — the card issuer sees authorization requests for a DPAN but never sees which wallet funded the card's underlying balance. The funding event is on a different system boundary.
What Cloak does NOT protect against
- Merchant-level deanonymization — if you give a merchant your shipping address, they know where you live. The protocol cannot help.
- Timing correlation at low pool sizes — a deposit followed immediately by a withdrawal of the same denomination still links statistically. The anonymity set is the pool, not the protocol.
- Compelled disclosure — the BIN sponsor, under subpoena, can disclose transaction logs tied to a DPAN. Cloak severs the on-chain linkage, not legal process against the issuer.
- Device compromise — keys live in the user's browser / phone. A compromised device compromises the wallet.
Trust assumptions
- At least one honest relayer in the batch path. With ≥3 hops the link is broken if any one is honest.
- Groth16 soundness assuming the toxic waste of the trusted setup was properly discarded. Cloak uses the Powers of Tau #84 (BN254) universal SRS.
- Solana liveness and finality (~12s). No Cloak property depends on re-orgs beyond the standard confirmation window.
- The BIN sponsor maintains its license with the card network. If the sponsor is terminated, card authorizations stop until a replacement program is onboarded.
System Architecture
Cloak has four components. The only trusted code path is the circuit and the settlement contract; everything else is replaceable.
Flow: funding a card
- User generates a
notelocally — a tuple(sk, rho, amount, asset). CommitsC = Poseidon(sk, rho, amount, asset). - Wallet submits a deposit transaction transferring
amountofassetinto the Shielded Pool SPL, along with the commitmentC. - The pool appends
Cto an append-only Merkle tree of depth 32. Tree root is updated. - Later, user signs a card-mint intent
Iand produces a Groth16 proof that: (a) they knowsk, (b)Cis in the tree, (c) the nullifierN = Poseidon(sk, C)has not been spent. - The proof and
Itravel throughNrelayer hops. Each hop re-signs and shuffles the batch before forwarding to the issuer. - Issuer verifies the proof, consumes the nullifier, credits the card's prefund balance with the BIN sponsor, and emits a provisioning token to the user's phone wallet.
Cryptography
All primitives are off-the-shelf, audited libraries. Cloak does not roll its own crypto. The choice of scheme prioritizes proving time inside the browser over on-chain verification cost.
| Primitive | Scheme | Library | Curve / params |
|---|---|---|---|
| zkSNARK | Groth16 | snarkjs 0.7 | BN254 |
| Hash | Poseidon | circomlib 2.0 | t=3, R_F=8, R_P=57 |
| Signatures | EdDSA | noble-ed25519 | Edwards-25519 |
| Merkle hash | Poseidon | circomlib | depth 32 |
| Key derivation | BIP-32-ish | @cloakfi/kdf | HKDF-SHA256 |
| Symmetric | ChaCha20-Poly1305 | @noble/ciphers | 256-bit keys |
Circuits
Cloak ships two circuits: deposit and spend. Both are written in circom and compiled to R1CS.
deposit.circom
Proves that a fresh commitment was correctly formed from a secret note. No Merkle membership yet; the deposit itself inserts the commitment. This circuit is optional — clients can submit the commitment directly for simple deposits, or use the circuit to prove the amount range.
pragma circom 2.1.9;
include "poseidon.circom";
include "comparators.circom";
template Deposit() {
signal input sk; // secret key, private
signal input rho; // random nonce, private
signal input amount; // private
signal input asset; // private, but range-checked
signal output C; // public commitment
// range: amount <= 2^40 so fits in 13 decimals of USDC
component lte = LessEqThan(40);
lte.in[0] <== amount;
lte.in[1] <== (1 << 40);
lte.out === 1;
component hash = Poseidon(4);
hash.inputs[0] <== sk;
hash.inputs[1] <== rho;
hash.inputs[2] <== amount;
hash.inputs[3] <== asset;
C <== hash.out;
}
component main = Deposit();spend.circom
The main privacy-preserving circuit. Proves (a) ownership of a note whose commitment is in the Merkle tree, (b) derivation of a unique nullifier, (c) a correct fresh output commitment for the change note, (d) that input amount ≥ output + fee.
template Spend(depth) {
// public
signal input root; // Merkle root of commitments
signal input nullifier; // derived from (sk, C_in)
signal input C_out; // fresh commitment
signal input fee; // relayer fee in native units
signal input extDataHash; // binds to off-chain payload
// private
signal input sk, rho_in, amount_in, asset_in;
signal input pathElements[depth];
signal input pathIndices[depth];
signal input rho_out, amount_out;
// 1. recompute input commitment
component cIn = Poseidon(4);
cIn.inputs <== [sk, rho_in, amount_in, asset_in];
signal C_in <== cIn.out;
// 2. Merkle membership
component mt = MerkleProof(depth);
mt.leaf <== C_in;
mt.root <== root;
for (var i = 0; i < depth; i++) {
mt.pathElements[i] <== pathElements[i];
mt.pathIndices[i] <== pathIndices[i];
}
// 3. nullifier = Poseidon(sk, C_in)
component nf = Poseidon(2);
nf.inputs[0] <== sk;
nf.inputs[1] <== C_in;
nf.out === nullifier;
// 4. output commitment C_out = Poseidon(sk, rho_out, amount_out, asset_in)
component cOut = Poseidon(4);
cOut.inputs <== [sk, rho_out, amount_out, asset_in];
cOut.out === C_out;
// 5. conservation: amount_in === amount_out + fee
amount_in === amount_out + fee;
}
component main = Spend(32);Relayer Network
Relayers are independent operators that accept signed spend intents from users, bundle them into batches, shuffle the batch, and forward to the next hop or to the issuer. Relayers never learn the user's identity; each hop strips one layer of onion-encrypted routing metadata, Sphinx-style.
Batch structure
interface RelayBatch {
// list of Groth16 proofs for spend intents
proofs: Uint8Array[]; // each 127 bytes
publicInputs: PublicInputs[]; // root, nullifier, C_out, fee, extHash
payloads: EncryptedPayload[]; // onion-encrypted per hop
batchRoot: Hash; // Poseidon over publicInputs
relayerSig: EdDSASignature; // signed by the forwarding relayer
receivedAt: number; // unix ms, for timing analysis
hopIndex: number; // 1 <= hopIndex <= 3
}Operational parameters
| Parameter | Default | Range |
|---|---|---|
| Hop count | 3 | 2 – 5 |
| Batch size target | 32 intents | 8 – 128 |
| Max batch delay | 900 ms | 200 – 4000 ms |
| Relayer fee | 0.04% + 1k lamports | market set |
| Min relayer uptime (directory) | 99.0% | rolling 30d |
Onboarding a relayer
- Generate an Ed25519 identity keypair. Publish the public key and a
stratum.jsoncontaining endpoint, version, supported hop indices, fee, TLS cert fingerprint. - Submit a self-attested registration tx to the on-chain relayer directory. No stake is required — discovery is permissionless.
- Clients observe uptime and performance. Poorly performing relayers are silently pruned from the default routing set but can be selected manually.
Card Issuance
Cloak does not have a card license. It operates as a program manager on top of a BIN sponsor that holds the Visa Principal Membership and the relevant state MSB licenses. Today, our BIN sponsor is Sutton Bank (NA) for USD, and Evolve Bank for secondary coverage.
Authorization flow
1. User's phone wallet presents DPAN via NFC
2. Merchant acquirer → Visa DPS → BIN sponsor authorization endpoint
3. BIN sponsor forwards auth request to Cloak program manager
4. Program manager checks card's prefund balance (held per-DPAN at sponsor)
5. Approve/decline response returned synchronously
6. On approve: sponsor reserves balance; settlement at T+1 from pool
7. Pool sends stablecoin to sponsor settlement account via on-chain tx
8. Sponsor converts on-chain USDC → fiat USD for network settlementWhy the user doesn't KYC
The sponsor's card program is licensed under a prepaid + stored-value framework that permits KYC-lite issuance under $2,500 per card and $10,000 aggregate per holder per year (FinCEN general exemption § 1022.380). Cloak's cards fall within these thresholds by design. For higher-limit persistent cards, the user completes the sponsor's own KYC; at that point, Cloak's privacy guarantees extend only to the on-chain link, not to the sponsor.
Settlement timing
| Event | Timing |
|---|---|
| Authorization (issuer → merchant) | synchronous, ~180ms p50 |
| Pool → sponsor settlement | T+1, batched once daily |
| Sponsor → network settlement | T+2 via ACH |
| User-visible card balance update | ~5s after spend (auth hold) |
Wallet Provisioning
After a card is minted, the user can provision it to Apple Pay or Google Pay via in-app provisioning (IAP). The card's real PAN is replaced by a Device PAN (DPAN) that is tokenized by the network.
Apple Pay IAP sequence
- Cloak app requests a provisioning token from the BIN sponsor, including: card FPAN, PAR, requested wallet (Apple).
- Sponsor calls Visa Token Service; VTS returns an encrypted payload (manifest) and TOS-signed activation data.
- App calls
PKAddPaymentPassRequestwith the encrypted payload. iOS hands to the Secure Enclave. - Apple Pay displays the card. Subsequent NFC payments use the DPAN — the FPAN is never broadcast.
PAR linkage
Every card — regardless of DPAN or FPAN — carries a stable Payment Account Reference. PAR lets compliance, fraud, and chargeback systems correlate transactions across wallet instances without revealing the underlying PAN. Cloak embeds the PAR into the sponsor program; we do not create our own.
Non-Custodial Keys
All user secrets (note private keys, commitment salts, signing keys) are derived from a single 256-bit seed stored locally. Cloak never sees the seed, and we don't maintain recovery infrastructure.
Derivation
// BIP-39 mnemonic -> 64-byte seed
const seed = mnemonicToSeedSync(mnemonic);
// Cloak-specific root
const root = hkdf(seed, info = "cloak-root-v1", length = 32);
// Note key: per-deposit, non-repeating
function noteKey(index: number): Scalar {
return hkdf(root, info = `note-${index}`, length = 32);
}
// Signing key: one per device
const signingKey = hkdf(root, info = "sign-v1", length = 32);Storage
- Browser — seed is encrypted with user-entered passphrase, stored in IndexedDB. Never uploaded.
- Mobile — seed lives in Secure Enclave (iOS) / StrongBox (Android). Face/Touch unlocks a per-session copy.
- Recovery — write down the 12 words. No social recovery, no guardian, no email reset. Cloak cannot restore access. This is explicit, not a bug.
Supported Networks
| Network | Status | Supported assets | Pool address |
|---|---|---|---|
| Solana | live | SOL, USDC, USDT | ClokPooLz…aYz8 |
| Base | beta | ETH, USDC | 0x4E2a…fD12 |
| Ethereum L1 | planned | ETH, USDC, USDT | — |
| x402 (HTTP payments) | live | via Base USDC | handled off-chain |
Fee Structure
| Component | Amount | Who collects |
|---|---|---|
| Protocol fee | 0.30% of card-mint value | Cloak treasury |
| Relayer fee | 0.04% + ~1,000 lamports | Relayer (market-set) |
| Network fee | Solana base fee, ~5,000 lamports | Solana validators |
| BIN sponsor interchange rebate | −0.30% (rebated to pool) | Sponsor → pool |
| Spend-side FX (non-USD merchants) | Visa FX rate + 1.0% | Visa / sponsor |
Net: a $100 card mint costs the user approximately $0.34 at current gas + market relayer rates. Rebate from interchange returns ~$0.30 back to the pool over the card's lifetime, so unit economics are approximately break-even on the issuance side.
Audits & Open Source
Audits
| Firm | Scope | Date | Report |
|---|---|---|---|
| Trail of Bits | spend.circom, deposit.circom | 2026-02 | TOB-CLK-01.pdf |
| Halborn | Solana settlement program | 2026-03 | HAL-CLK-0123.pdf |
| Least Authority | Relayer protocol, Sphinx layer | 2026-03 | LA-CLK-2026-03.pdf |
Bug bounty
Administered via Immunefi. Live program. Severity ladder matches Immunefi Vulnerability Severity Classification v2.3 for DeFi.
Repositories
| Repo | Contents | License |
|---|---|---|
| cloakfi/circuits | circom sources, ceremony transcripts | MIT |
| cloakfi/solana-pool | Anchor program, IDL | MIT |
| cloakfi/relayer | Rust relayer reference implementation | AGPL-3.0 |
| cloakfi/web | Wallet + dashboard frontend | MIT |
| cloakfi/sdk-ts | TypeScript SDK | MIT |
Known Limitations
- Pool timing correlation. At current pool throughput, a large deposit followed by a large spend within a 30-second window reduces anonymity-set entropy below ~12 bits. Mitigation: the SDK defers spend proofs to randomized windows when the pool is thin.
- Denomination linkage. Exact-amount deposits (rare in practice, but e.g. programmatic $1,000 round-number deposits) are more linkable to identically-priced card mints. We do not enforce fixed denominations; guidance in the wallet nudges users toward non-round amounts.
- Relayer collusion. If all ≥3 relayers on the path collude, the batch is deanonymized. The routing algorithm selects from disjoint operators (different ASN, different jurisdictions) but cannot guarantee total independence.
- Sponsor-level disclosure. See Threat Model. We cannot prevent the sponsor from responding to lawful process.
- Device fingerprinting. The browser fingerprint submitted during Apple/Google Pay provisioning is outside Cloak's control. Use a dedicated device for high-sensitivity cards.
- Not censorship-resistant at the card rail. Visa can decline transactions. Cloak does not bypass BSA/AML screening on the merchant-facing side.
Glossary
- BIN sponsor
- A licensed bank that issues cards on behalf of a program manager.
- Commitment
- A hiding, binding hash of a note: Poseidon(sk, rho, amount, asset).
- DPAN
- Device PAN. A tokenized card number specific to a wallet instance.
- FPAN
- Funding PAN. The original card number issued by the BIN sponsor.
- Nullifier
- Unique per-note value derived from sk + C. Marks a note as spent.
- PAR
- Payment Account Reference. A stable identifier across DPAN/FPAN for the same card.
- Pool
- On-chain program holding deposits and the Merkle tree of commitments.
- Program manager
- The entity running the card program on top of a BIN sponsor — i.e. Cloak.
- Relayer
- Independent operator that forwards proofs from user to issuer.
- Sphinx
- Onion routing packet format used for relayer hops.
- VTS
- Visa Token Service. Issues DPANs for in-app provisioning.