ZK Vault Module
The zk_vault module provides privacy-preserving document verification using Groth16 zero-knowledge proofs. Instead of storing document hashes on-chain, it stores cryptographic commitments that can be verified without revealing the document content.
Overview
The ZK vault enables privacy-preserving document verification where:
- Document hashes are never stored on-chain
- Only cryptographic commitments are stored
- Proofs can verify authenticity without revealing content
- Uses Sui's native Groth16 verifier
Key Structures
ZKDocumentRegistry
Registry for zero-knowledge commitments:
public struct ZKDocumentRegistry has key {
id: UID,
commitments: Table<address, ZKCommitment>, // Maps token address to commitment
verifying_keys: Table<ID, StoredVerifyingKey>, // Reusable verification keys
total_commitments: u64,
}ZKCommitment
Cryptographic commitment to document:
public struct ZKCommitment has store {
token_address: address,
commitment: vector<u8>, // Commitment hash (not document hash!)
owner: address, // Original owner
verifying_key_id: ID, // Verification key reference
proof_count: u64, // Number of proofs verified
last_verified_at: u64, // Last verification timestamp
access_key: vector<u8>, // Encrypted access key
created_at: u64,
}StoredVerifyingKey
Reusable verification key for circuits:
public struct StoredVerifyingKey has store {
id: ID,
vk_bytes: vector<u8>, // Verification key bytes
curve_id: u8, // 0 = BLS12-381, 1 = BN254
}Main Functions
register_zk_commitment()
Register document commitment without revealing content:
public entry fun register_zk_commitment(
registry: &mut ZKDocumentRegistry,
token_address: address,
commitment: vector<u8>,
verifying_key_id: ID,
access_key: vector<u8>,
ctx: &mut TxContext
)Parameters:
registry: Mutable reference to ZKDocumentRegistrytoken_address: Address of RWA tokencommitment: Cryptographic commitment (Poseidon hash)verifying_key_id: ID of stored verifying keyaccess_key: Encrypted access keyctx: Transaction context
Effects:
- Creates ZKCommitment record
- Increments total_commitments
- Emits
ZKCommitmentRegisteredevent
verify_groth16_proof()
Verify ownership proof using Sui's native Groth16 verifier:
public entry fun verify_groth16_proof(
registry: &mut ZKDocumentRegistry,
token_address: address,
verifying_key: vector<u8>,
public_inputs: vector<vector<u8>>,
proof_points: vector<u8>,
curve_id: u8,
ctx: &mut TxContext
): boolParameters:
registry: Mutable reference to ZKDocumentRegistrytoken_address: Address of RWA tokenverifying_key: Groth16 verification key bytespublic_inputs: Public inputs to the proofproof_points: Proof points (A, B, C)curve_id: Curve ID (0 = BLS12-381, 1 = BN254)ctx: Transaction context
Returns: true if proof is valid
Effects:
- Verifies proof using Sui's native verifier
- Updates proof_count and last_verified_at
- Emits
ZKProofVerifiedevent
register_verifying_key()
Store reusable verification keys on-chain:
public entry fun register_verifying_key(
registry: &mut ZKDocumentRegistry,
vk_bytes: vector<u8>,
curve_id: u8,
ctx: &mut TxContext
): IDReturns: ID of stored verifying key
Effects:
- Stores verifying key for reuse
- Emits
VerifyingKeyRegisteredevent
get_access_key()
Retrieve encrypted access key (requires token ownership):
public fun get_access_key(
registry: &ZKDocumentRegistry,
token: &RwaToken,
token_address: address
): vector<u8>Requirements:
- Caller must own the RWA token
- Commitment must exist
Returns: Encrypted access key
ZK Proof Scheme
Commitment Generation
The commitment is computed off-chain:
commitment = PoseidonHash(documentHash || ownerAddress || nonce)Where:
documentHash: SHA-256 hash of documentownerAddress: Token owner addressnonce: Random nonce for privacy
Proof Structure
The proof demonstrates knowledge of:
- A document hash that matches the commitment
- Ownership of the RWA token
- Without revealing the document hash
Verification
The proof is verified on-chain using Sui's native Groth16 verifier:
- Public inputs: commitment, token address
- Private inputs: document hash, owner address, nonce
- Circuit: Proves commitment computation without revealing private inputs
Events
ZKCommitmentRegistered
public struct ZKCommitmentRegistered has copy, drop {
token_address: address,
commitment: vector<u8>,
timestamp: u64,
}ZKProofVerified
public struct ZKProofVerified has copy, drop {
token_address: address,
commitment: vector<u8>,
verified: bool,
timestamp: u64,
}VerifyingKeyRegistered
public struct VerifyingKeyRegistered has copy, drop {
verifying_key_id: ID,
curve_id: u8,
timestamp: u64,
}Privacy Guarantees
What's Public
- Commitment hash (not reversible)
- Token address
- Verification status
- Proof count
What's Private
- Document hash (never stored on-chain)
- Document content
- Private inputs to proof
- Access keys (encrypted)
Usage Examples
Register ZK Commitment
sui client call \
--package <PACKAGE_ID> \
--module zk_vault \
--function register_zk_commitment \
--args <ZK_REGISTRY_ID> <TOKEN_ADDRESS> "[<COMMITMENT_BYTES>]" <VK_ID> "[<ACCESS_KEY>]" \
--gas-budget 10000000Verify ZK Proof
sui client call \
--package <PACKAGE_ID> \
--module zk_vault \
--function verify_groth16_proof \
--args <ZK_REGISTRY_ID> <TOKEN_ADDRESS> 0 "[<VK_BYTES>]" "[<PUBLIC_INPUTS>]" "[<PROOF_POINTS>]" \
--gas-budget 20000000TypeScript Example
// Generate commitment off-chain
const commitment = await generateZKCommitment(documentHash, ownerAddress, nonce);
// Register commitment
const tx = new TransactionBlock();
tx.moveCall({
target: `${PACKAGE_ID}::zk_vault::register_zk_commitment`,
arguments: [
tx.object(ZK_REGISTRY_ID),
tx.pure.address(tokenAddress),
tx.pure.vector('u8', commitment),
tx.pure.id(verifyingKeyId),
tx.pure.vector('u8', encryptedAccessKey),
],
});
// Generate proof off-chain
const proof = await generateOwnershipProof(documentHash, ownerAddress, nonce, commitment);
// Verify proof on-chain
const verifyTx = new TransactionBlock();
verifyTx.moveCall({
target: `${PACKAGE_ID}::zk_vault::verify_groth16_proof`,
arguments: [
tx.object(ZK_REGISTRY_ID),
tx.pure.address(tokenAddress),
tx.pure.vector('u8', verifyingKey),
tx.pure.vector('vector<u8>', publicInputs),
tx.pure.vector('u8', proofPoints),
tx.pure.u8(curveId), // 0 for BLS12-381
],
});Supported Curves
BLS12-381 (curve_id: 0)
- More efficient for verification
- Better for production use
- Recommended for most cases
BN254 (curve_id: 1)
- Legacy curve support
- Compatibility with older circuits
Security Considerations
Commitment Security
- Commitments are one-way (cannot reverse to document hash)
- Nonce prevents rainbow table attacks
- Poseidon hash is ZK-friendly
Proof Security
- Uses Groth16 (proven secure zk-SNARK)
- On-chain verification (no trust required)
- Public inputs prevent replay attacks
Privacy Guarantees
- Document hash never stored on-chain
- Proof doesn't reveal private inputs
- Commitment alone doesn't reveal content
Comparison with Regular Vault
| Feature | Regular Vault | ZK Vault |
|---|---|---|
| Hash Storage | ✅ Stored on-chain | ❌ Never stored |
| Verification | Hash comparison | ZK proof |
| Privacy | Limited | Full privacy |
| Gas Cost | Lower | Higher (proof verification) |
| Complexity | Simple | More complex |
Best Practices
- Use for Sensitive Documents: ZK vault for highly sensitive assets
- Circuit Design: Design secure ZK circuits
- Key Management: Store verifying keys securely
- Proof Generation: Generate proofs off-chain
- Privacy First: Use commitments even if not required
Next Steps
- Learn about Access Control
- Understand Regular Vault for comparison
- Review Frontend ZK Services