Chromia
Start Building

12 / 19Module 2 – DappAdvanced

Dapp entities

A handful of entities cooperate to make private token transfers possible. Below you'll find what each one stores and the job it performs in the privacy-preserving token system.

Core privacy entities

spent_nullifier

rell/src/zkp_demo/entities.rell
entity spent_nullifier {    key nullifier_hash: big_integer;}

Purpose: Records nullifiers that have already been used, which blocks double-spending in the privacy system.

How it works:

  • Spending a private token produces a unique nullifier derived from its commitment
  • That nullifier is written to spent_nullifier, flagging it as consumed
  • Every transaction is checked against this entity first to see whether its nullifier is already present
  • As a result, a private token can never be spent more than once

unspent_commitment

rell/src/zkp_demo/entities.rell
entity unspent_commitment {    key commitment_hash: big_integer;}

Purpose: Holds every commitment that has not yet been spent, i.e. the private tokens currently available.

How it works:

  • A private token takes the form of a commitment, which is a cryptographic hash
  • Shielding tokens (making them private) or receiving them through a private transfer adds new commitments
  • Spending or unshielding tokens deletes the corresponding commitments
  • Together, these records serve as the "UTXO set" for private tokens

private_transfer_event

rell/src/zkp_demo/entities.rell
@logentity private_transfer_event {    commitment_input: big_integer;    commitment_output_sender: big_integer;    commitment_output_recipient: big_integer;    recipient_private_address: big_integer;    nullifier: big_integer;    encrypted_sender_note: byte_array;    encrypted_recipient_note: byte_array;}

Purpose: Keeps a log of every private transfer along with its encrypted details.

How it works:

  • Writes an on-chain record for each private transfer transaction
  • Stores the input commitment being consumed plus two output commitments: one for the sender's change and one for the recipient
  • Carries encrypted notes that can be decrypted only by the sender and the recipient
  • Because of the @log annotation, you can query these records for wallet scanning
  • Wallets scan these events to discover private transactions addressed to them

Token shielding/unshielding entities

shield_log

rell/src/zkp_demo/private_token.rell
@logentity shield_log {    account_id: byte_array;    commitment: big_integer;    amount: big_integer;    encrypted_note: byte_array;}

Purpose: Records each conversion of public tokens into private tokens (shielding).

How it works:

  • Captures the moment a user turns public FT4 tokens into private commitments
  • Ties the public account to the private commitment that was produced
  • Includes an encrypted note describing the transaction
  • Gives users a way to review their shielding history

unshield_log

rell/src/zkp_demo/private_token.rell
@logentity unshield_log {    account_id: byte_array;    nullifier: big_integer;    commitment: big_integer;    amount: big_integer;}

Purpose: Records each conversion of private tokens back into public tokens (unshielding).

How it works:

  • Captures the moment a user turns private commitments back into public FT4 tokens
  • Ties the nullifier and commitment to the public account that receives the tokens
  • Gives users a way to review their unshielding history
  • Supplies an audit trail for regulatory compliance

User management entity

private_address_registry

rell/src/zkp_demo/private_address_registry.rell
entity private_address_registry {    key account_id: byte_array;    mutable private_address: big_integer;    mutable public_encryption_key: text;    mutable registered_at: timestamp;}

Purpose: Associates each public FT4 account with its private address and encryption key.

How it works:

  • You register a private address, derived from your private key, against your public FT4 account
  • Your public encryption key is stored here so notes can be encrypted securely for you
  • Senders look up a recipient's private address here before sending private tokens
  • In effect, it is a "phone book" for private transactions
  • You can update your keys whenever needed

Privacy model summary

Taken together, these entities form a UTXO-based privacy model:

  1. Shielding turns public tokens into private commitments
  2. Private commitments move between users privately with the help of zero-knowledge proofs
  3. Nullifiers stop double-spending while keeping the spent commitment hidden
  4. Encrypted notes let parties exchange transaction details securely
  5. The address registry makes it possible for users to receive private payments

You get transaction privacy, with amounts and recipients concealed, while auditability is preserved through the logged events and regulatory compliance is supported by shield/unshield logs that connect to public identities.

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