01. Why Single-Primitive Privacy Leaks in Production
Most "private" payroll or transfer protocols in Web3 rely on a single cryptographic primitive, usually a ZK commitment pool, and assume the privacy problem is solved. In practice, an adversary observing a public high-throughput chain like Solana does not need to break your Groth16 circuit to deanonymize your users.
If an employer uploads a plaintext payroll CSV to a centralized backend to build the Merkle tree, the server operator sees every salary. If an employee claims their exact salary amount in a single transfer 12 seconds after the root is posted, simple amount-and-timing heuristics link the employer vault directly to the employee wallet. Real institutional privacy requires defense in depth across storage, tree construction, verification, and settlement.
02. Unifying Everything Over the BN254 Scalar Field
In Civitas, we unified every cryptographic operation over the BN254 scalar field F_p so that browser witnesses (Circom 2.1.6 / snarkjs), confidential enclave tree builders (Nillion nilCC V4), and onchain verifiers (Solana groth16-solana via alt_bn128 syscalls) speak the exact same mathematical language without expensive bit-packing or endianness bugs.
To prevent replay attacks and cross-run collisions without revealing which leaf in the depth-20 Poseidon Merkle tree is being spent, our Voucher.circom circuit enforces a two-step deterministic nullifier derivation bound to a public-input Poseidon sponge.
// 1. Leaf Commitment inside depth-20 Poseidon Merkle Tree
commitment <== Poseidon(4)([credential_nonce, employee_tag, amount, epoch_id]);
// 2. Two-Step Deterministic Nullifier (prevents double-claim without revealing leaf index)
nullifier_secret <== Poseidon(2)([credential_nonce, epoch_id]);
nullifier_hash <== Poseidon(2)([nullifier_secret, merkle_root]);
// 3. Public-Input Binding Sponge (locks recipient & amount against mempool tampering)
public_inputs_hash <== Poseidon(5)([merkle_root, nullifier_hash, amount, epoch_id, recipient_pubkey]);03. Defeating Timing & Amount Correlation with MagicBlock TEE
Even after the Solana program verifies the 256-byte Groth16 proof and locks the Nullifier PDA, sending a single lump-sum USDC transfer to the recipient would leak the exact salary figure on Solscan.
To close that final side channel, Civitas dispatches the verified settlement instruction to a MagicBlock Ephemeral Rollup TEE, which fragments the payout into 5 randomized sub-transfers executed across a jittered [500ms, 30s] window. Every layer of the 4-layer stack eliminates a specific real-world inference vector.