Build private releases into anything
A commitment-only registry on an EVM coordination chain, three Noir circuits, and a custody layer that only acts on verified proofs. The Vault ZK app is built on it, and other apps can be too. The registry is live on BSC Testnet.
Five layers
| Layer | Responsibility | Source | Status |
|---|---|---|---|
| Client | Generates secrets, builds Merkle trees, and proves with Noir + UltraHonk entirely in the browser. | packages/circuits | Testnet-ready |
| Coordination | PrivacyVaultRegistry: commitments, policies, nullifiers, release logic. No admin, no funds, no msg.sender checks. | packages/contracts-v2 | Testnet-ready |
| Relayer | Submits proofs on users’ behalf so wallets are never linked to vaults. Untrusted: cannot alter bound inputs. | None listed | In development |
| Keepers | Threshold signers (FROST, Safe module) that execute payouts or release key shares after Claimed events. | None listed | In development |
| Storage | Client-side-encrypted blobs on IPFS; drand timelock encryption for time-locked data. | None listed | In development |
Noir circuits
All hashes are Poseidon2 with domain separation constants. Merkle trees have depth 8, so up to 256 beneficiaries or guardians per vault. Proofs are UltraHonk; verifiers are generated with Barretenberg and deployed as Solidity contracts.
| Circuit | Private inputs | Public inputs | Constraints |
|---|---|---|---|
| owner_auth | owner_secret, salt | vault_id, action, nullifier | vault_id = H(DOMAIN_VAULT, secret, salt); nullifier = H(DOMAIN_OWNER_AUTH, secret, action) |
| claim | beneficiary_secret, leaf_index, path[8] | vault_id, beneficiary_root, share_bps, recipient, nullifier | leaf H(secret, share_bps) is in the tree; nullifier = H(DOMAIN_CLAIM, secret, vault_id); recipient ≠ 0 |
| guardian_vote | guardian_secret, leaf_index, path[8] | vault_id, guardian_root, round, new_vault_id, nullifier | guardian leaf is in the tree; nullifier = H(DOMAIN_VOTE, secret, vault_id, round) |
Owner action values are keccak hashes of the operation, chain ID, registry address, vault and parameters, reduced into the BN254 field. A proof for one action, chain or deployment is therefore useless for any other.
PrivacyVaultRegistry
Every state-changing function takes a proof and a nullifier. Nothing reads msg.sender, so any account can submit, including a relayer.
isReleasable returns true when the inactivity period has elapsed since the last check-in, or the unlock time has passed. Check-ins accept the current or previous one-day epoch.
After the first claim, reconfigure, checkIn and voteRecovery revert with AlreadyReleased.
// PrivacyVaultRegistry.sol, Solidity ^0.8.27struct Policy { uint64 inactivityPeriod; // seconds without check-in before release (0 = off) uint64 unlockTime; // unix time after which release is allowed (0 = off) bytes32 beneficiaryRoot; // Merkle root of Poseidon2(DOMAIN_BENEFICIARY, secret, shareBps) bytes32 guardianRoot; // Merkle root of Poseidon2(DOMAIN_GUARDIAN, secret) (0 = no recovery) uint16 guardianThreshold; // votes needed to rotate to a new owner commitment}// owner: authorised by an owner_auth proof bound to the action hashfunction createVault(bytes32 vaultId, Policy policy, bytes32 nullifier, bytes proof)function reconfigure(bytes32 vaultId, Policy policy, bytes32 nullifier, bytes proof)function checkIn(bytes32 vaultId, uint256 epoch, bytes32 nullifier, bytes proof)// beneficiary: claim proof; recipient is a field-encoded hash of the payout destinationfunction claim(bytes32 vaultId, uint16 shareBps, bytes32 recipient, bytes32 nullifier, bytes proof)// guardian: anonymous k-of-n vote for a new owner commitmentfunction voteRecovery(bytes32 vaultId, bytes32 newVaultId, bytes32 nullifier, bytes proof)// viewsfunction getVault(bytes32 vaultId) view returns (Vault)function isReleasable(bytes32 vaultId) view returns (bool)function createAction(bytes32 vaultId, Policy policy) view returns (bytes32)function reconfigureAction(bytes32 vaultId, uint32 nonce, Policy policy) view returns (bytes32)function checkinAction(bytes32 vaultId, uint256 epoch) view returns (bytes32)function nullifierUsed(bytes32) view returns (bool)// eventsevent VaultCreated(bytes32 indexed vaultId, Policy policy)event VaultReconfigured(bytes32 indexed vaultId, uint32 nonce, Policy policy)event CheckedIn(bytes32 indexed vaultId, uint256 epoch)event Claimed(bytes32 indexed vaultId, bytes32 indexed nullifier, uint16 shareBps, bytes32 recipient)event RecoveryVote(bytes32 indexed vaultId, uint32 round, bytes32 indexed newVaultId, uint256 votes)event RecoveryCancelled(bytes32 indexed vaultId, uint32 round)event VaultRecovered(bytes32 indexed vaultId, bytes32 indexed newVaultId)SDK preview
The SDK is in development and not published. This sketch shows the intended shape of the API; names will change.
// PREVIEW: illustrative interface, not yet published. Names will change.import { createVaultClient } from '@vault/privacy-sdk'; // package name not finalconst vault = createVaultClient({ registry, relayer });// 1. Secrets are generated locally and never sent anywhere.const owner = vault.newOwner(); // { ownerSecret, salt, vaultId }const heirs = vault.beneficiaries([ { shareBps: 6000 }, // each gets a fresh secret to hand over { shareBps: 4000 },]);// 2. Prove and submit. Proving runs in the browser (Noir + UltraHonk).await vault.create(owner, { inactivityPeriod: 180 * 86_400, beneficiaryRoot: heirs.root,});// 3. Stay alive.await vault.checkIn(owner); // owner_auth proof for today's epoch// 4. Later, a beneficiary claims with only their own secret.await vault.claim({ vaultId: owner.vaultId, secret: heirs.secrets[1], recipient });