Skip to content

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 ZKDocumentRegistry
  • token_address: Address of RWA token
  • commitment: Cryptographic commitment (Poseidon hash)
  • verifying_key_id: ID of stored verifying key
  • access_key: Encrypted access key
  • ctx: Transaction context

Effects:

  • Creates ZKCommitment record
  • Increments total_commitments
  • Emits ZKCommitmentRegistered event

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
): bool

Parameters:

  • registry: Mutable reference to ZKDocumentRegistry
  • token_address: Address of RWA token
  • verifying_key: Groth16 verification key bytes
  • public_inputs: Public inputs to the proof
  • proof_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 ZKProofVerified event

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
): ID

Returns: ID of stored verifying key

Effects:

  • Stores verifying key for reuse
  • Emits VerifyingKeyRegistered event

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 document
  • ownerAddress: Token owner address
  • nonce: Random nonce for privacy

Proof Structure

The proof demonstrates knowledge of:

  1. A document hash that matches the commitment
  2. Ownership of the RWA token
  3. 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 10000000

Verify 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 20000000

TypeScript 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

FeatureRegular VaultZK Vault
Hash Storage✅ Stored on-chain❌ Never stored
VerificationHash comparisonZK proof
PrivacyLimitedFull privacy
Gas CostLowerHigher (proof verification)
ComplexitySimpleMore complex

Best Practices

  1. Use for Sensitive Documents: ZK vault for highly sensitive assets
  2. Circuit Design: Design secure ZK circuits
  3. Key Management: Store verifying keys securely
  4. Proof Generation: Generate proofs off-chain
  5. Privacy First: Use commitments even if not required

Next Steps