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

LayerResponsibilitySourceStatus
ClientGenerates secrets, builds Merkle trees, and proves with Noir + UltraHonk entirely in the browser.packages/circuitsTestnet-ready
CoordinationPrivacyVaultRegistry: commitments, policies, nullifiers, release logic. No admin, no funds, no msg.sender checks.packages/contracts-v2Testnet-ready
RelayerSubmits proofs on users’ behalf so wallets are never linked to vaults. Untrusted: cannot alter bound inputs.None listedIn development
KeepersThreshold signers (FROST, Safe module) that execute payouts or release key shares after Claimed events.None listedIn development
StorageClient-side-encrypted blobs on IPFS; drand timelock encryption for time-locked data.None listedIn 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.

CircuitPrivate inputsPublic inputsConstraints
owner_authowner_secret, saltvault_id, action, nullifiervault_id = H(DOMAIN_VAULT, secret, salt); nullifier = H(DOMAIN_OWNER_AUTH, secret, action)
claimbeneficiary_secret, leaf_index, path[8]vault_id, beneficiary_root, share_bps, recipient, nullifierleaf H(secret, share_bps) is in the tree; nullifier = H(DOMAIN_CLAIM, secret, vault_id); recipient ≠ 0
guardian_voteguardian_secret, leaf_index, path[8]vault_id, guardian_root, round, new_vault_id, nullifierguardian 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.solabridged
// 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.

TypeScriptpreview, not published
// 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 });