14 / 19Module 2 – DappAdvanced
Dapp operations overview
This guide walks through each operation in the zkp-demo workspace. The workspace shows how to build a private token transfer system on Chromia with zero-knowledge proofs.
Overview
With the ZKP demo, you can:
- Shield tokens, turning public tokens into private ones
- Send private tokens without revealing who sent them
- Unshield tokens, turning private tokens back into public ones
- Keep a registry of private addresses
Core operations
Private transfer operation
operation transfer_private_tokens( encrypted_sender_note: byte_array, encrypted_recipient_note: byte_array) { val public_signals = zkp.extract_signals_from_preceeding_proof_op("private_transfer"); zkp.check_plonk_proof("private_transfer", public_signals); // Extract public signals require(public_signals.size() == 5, "Invalid public signals size"); val commitment_input = public_signals[0]; val commitment_output_sender = public_signals[1]; val commitment_output_recipient = public_signals[2]; val recipient_private_address = public_signals[3]; val nullifier = public_signals[4]; // Check input commitment exists val sender_unspent_commitment = require( unspent_commitment @? { .commitment_hash == commitment_input }, "Input commitment not found in unspent list" ); // Check nullifier has not been spent require( spent_nullifier @? { .nullifier_hash == nullifier } == null, "Nullifier already spent" ); // Update the blockchain state create spent_nullifier ( nullifier_hash = nullifier ); delete sender_unspent_commitment; create unspent_commitment ( commitment_hash = commitment_output_sender ); create unspent_commitment ( commitment_hash = commitment_output_recipient ); // Log the transfer event with encrypted notes create private_transfer_event ( commitment_input, commitment_output_sender, commitment_output_recipient, recipient_private_address, nullifier, encrypted_sender_note, encrypted_recipient_note );}Purpose: Performs the main private transfer, backed by a zero-knowledge proof.
How it works:
-
ZKP verification: Reads the public signals from a "private_transfer" proof and validates them.
-
Input validation: Requires exactly 5 public signals:
commitment_input: the input commitment you are spendingcommitment_output_sender: the sender's new output commitment (the change)commitment_output_recipient: the recipient's output commitmentrecipient_private_address: the recipient's private addressnullifier: blocks double-spending
-
Security checks:
- Confirms the input commitment is in the unspent list
- Confirms the nullifier has never been used, so the same funds can't be spent twice
-
State updates:
- Records the nullifier as spent
- Deletes the input commitment from the unspent list
- Inserts two output commitments: the sender's change and the recipient's share
-
Event logging: Logs the transfer together with encrypted notes that wallets use to sync.
Shield operation
operation shield_tokens( encrypted_note: byte_array) { val account = auth.authenticate(); val public_signals = zkp.extract_signals_from_preceeding_proof_op("shield_operation"); // Extract public signals val commitment = public_signals[0]; val amount = public_signals[2]; // Verify the ZKP zkp.check_plonk_proof("shield_operation", public_signals); // Subtract amount from user's public balance assets.deduct_balance(account, zkp_demo.init.test_asset, amount); // Add commitment to unspent commitments create zkp_demo.unspent_commitment(commitment); // Log the shield operation create shield_log( account_id = account.id, commitment = commitment, amount = amount, encrypted_note = encrypted_note );}Purpose: Turns public FT4 tokens into private tokens.
Process:
- Authenticates your account
- Reads the public signals from the "shield_operation" ZKP:
commitment: the new private commitmentamount: how much you are shielding
- Verifies the zero-knowledge proof
- Subtracts the tokens from your public FT4 balance
- Adds a new unspent commitment to the private system
- Logs the shield operation along with an encrypted note
Unshield operation
operation unshield_tokens() { val account = auth.authenticate(); val public_signals = zkp.extract_signals_from_preceeding_proof_op("unshield_operation"); // Extract public signals val commitment = public_signals[0]; val nullifier = public_signals[1]; val amount = public_signals[3]; // Verify the ZKP zkp.check_plonk_proof("unshield_operation", public_signals); // Check if commitment exists val unspent_commitment = require(zkp_demo.unspent_commitment @? { commitment }, "Commitment does not exist"); // Check if nullifier has been spent require(zkp_demo.spent_nullifier @? { nullifier } == null, "Nullifier already spent"); // Mark nullifier as spent create zkp_demo.spent_nullifier(nullifier); // Remove commitment from unspent list delete unspent_commitment; // Add amount to user's public balance assets.increase_balance(account, zkp_demo.init.test_asset, amount); // Log the unshield operation create unshield_log( account_id = account.id, nullifier = nullifier, commitment = commitment, amount = amount );}Purpose: Turns private tokens back into public FT4 tokens.
Process:
- Authenticates your account
- Reads the public signals from the "unshield_operation" ZKP:
commitment: the commitment you are unshieldingnullifier: blocks double-spendingamount: how much you are unshielding
- Verifies the zero-knowledge proof
- Checks that the commitment exists and that the nullifier is still unspent
- Records the nullifier as spent and deletes the commitment
- Credits the tokens to your public FT4 balance
- Logs the unshield operation
Private address registration
operation register_private_address(private_address: big_integer, public_encryption_key: text) { val account = auth.authenticate(); // Check if already registered val existing = private_address_registry @? { account.id }; if (existing != null) { // Update existing registration update existing ( private_address = private_address, public_encryption_key = public_encryption_key, registered_at = op_context.last_block_time ); } else { // Create new registration create private_address_registry( account_id = account.id, private_address = private_address, public_encryption_key = public_encryption_key, registered_at = op_context.last_block_time ); }}Purpose: Links an FT4 account to a private address and an encryption key.
Features:
- Maps each FT4 account ID to a private address
- Saves public encryption keys so notes can be encrypted securely
- Lets you update a registration that already exists
- Allows users to communicate privately with each other