# Dapp operations overview · Build with zero-knowledge proofs on Chromia · Learn Chromia

> 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…

Source: https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-operations/

Build with zero-knowledge proofs on ChromiaLesson 14 of 19

1. [01Introduction to the course](https://chromia.com/learn/zero-knowledge-proof/introduction/)
2. [02Zero-knowledge proof](https://chromia.com/learn/zero-knowledge-proof/zero-knowledge-proof/)
3. [03Project setup](https://chromia.com/learn/zero-knowledge-proof/setup/)
4. [04Architecture overview](https://chromia.com/learn/zero-knowledge-proof/architecture-overview/)

Module 1: Writing Circom circuits

1. [05Overview](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/)
2. [06Circom circuits: introduction](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-introduction/)
3. [07Circom files overview](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-project/)
4. [08Circom circuits: compile](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-compile/)

Module 2 – Dapp

1. [09Overview](https://chromia.com/learn/zero-knowledge-proof/dapp/)
2. [10Dapp: setup and run](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-setup-run/)
3. [11Dapp overview](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-overview/)
4. [12Dapp entities](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-entities/)
5. [13PLONK verification](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-verification/)
6. [14Dapp operations overview](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-operations/)
7. [15Dapp queries overview](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-queries/)

Module 3 – Frontend

1. [16Overview](https://chromia.com/learn/zero-knowledge-proof/frontend/)
2. [17Frontend: setup and run](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-setup-run/)
3. [18Frontend architecture](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-explore/)
4. [19Frontend: test](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-test/)

[Learn](https://chromia.com/learn/)[Build with zero-knowledge proofs on Chromia](https://chromia.com/learn/zero-knowledge-proof/)

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

rell/src/zkp\_demo/operations.rell

```
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**:

1. **ZKP verification**: Reads the public signals from a "private\_transfer" proof and validates them.
2. **Input validation**: Requires exactly 5 public signals:

   * `commitment_input`: the input commitment you are spending
   * `commitment_output_sender`: the sender's new output commitment (the change)
   * `commitment_output_recipient`: the recipient's output commitment
   * `recipient_private_address`: the recipient's private address
   * `nullifier`: blocks double-spending
3. **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
4. **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
5. **Event logging**: Logs the transfer together with encrypted notes that wallets use to sync.

### Shield operation

rell/src/zkp\_demo/private\_token.rell

```
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**:

1. Authenticates your account
2. Reads the public signals from the "shield\_operation" ZKP:
   * `commitment`: the new private commitment
   * `amount`: how much you are shielding
3. Verifies the zero-knowledge proof
4. Subtracts the tokens from your public FT4 balance
5. Adds a new unspent commitment to the private system
6. Logs the shield operation along with an encrypted note

### Unshield operation

rell/src/zkp\_demo/operations.rell

```
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**:

1. Authenticates your account
2. Reads the public signals from the "unshield\_operation" ZKP:
   * `commitment`: the commitment you are unshielding
   * `nullifier`: blocks double-spending
   * `amount`: how much you are unshielding
3. Verifies the zero-knowledge proof
4. Checks that the commitment exists and that the nullifier is still unspent
5. Records the nullifier as spent and deletes the commitment
6. Credits the tokens to your public FT4 balance
7. Logs the unshield operation

### Private address registration

rell/src/zkp\_demo/private\_address\_registry.rell

```
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

Written for this site from Chromia’s public course. Code samples are the course’s own.

[← Previous**PLONK verification**](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-verification/)[Next →**Dapp queries overview**](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-queries/)

On this page

* [Overview](#overview)
* [Core operations](#core-operations)
