LogoLogo
  • WELCOME TO ALEPH ZERO
  • EXPLORE
    • About Aleph Zero
    • AlephBFT Consensus
    • The Economy of Aleph Zero
    • Where to Buy AZERO
    • Decentralized Governance on Aleph Zero
    • Ecosystem
    • Aleph Zero Foundation Treasury Management
    • Community
    • Glossary
    • Audit & Research Papers
  • USE
    • Wallets
    • Explorer
    • Ledger
    • Telegram Notifications
    • Aleph Zero Signer
      • General introduction
      • What does Signer do?
      • What are Sub-accounts and Sub-account paths?
      • Why is it critical to store your Secret Phrase in a safe place?
      • How to forget and restore accounts?
      • What are Networks?
      • What are Trusted apps?
    • Dashboard
      • Dashboard basics
      • Overview
    • Stake
      • Staking Basics
      • Staking Menu Overview
      • How to Start Staking with the Aleph Zero Dashboard
      • How to Start Staking With the Developer Wallet
      • How to start staking using Ledger hardware wallet
      • How to Change Nominations
      • How to Stop Staking
      • Staking Rewards
      • Validators
      • Commission and Foundation Nodes
      • Proxy Accounts
    • Validate
      • Validating Overview
      • Hardware requirements
      • Running an Aleph Node on Testnet
        • Downloading and running the node
        • Verifying your setup
        • Customizing your setup
        • Building and running from source [advanced]
          • Building from source
          • Set environment variables
          • Download DB snapshot
          • Running the binary
        • Appendix: Ports, addresses, validators, and archivists
      • Running an Aleph Node on Mainnet
        • Running the node
        • Building and running from source [advanced]
      • Setting your identity
      • Making the node validate
      • Securing your validator
      • Troubleshooting
      • Elections and Rewards Math
      • Testnet Validator Airdrop
      • Foundation Nomination Program
    • Using the EVM-layer
    • Governance
      • Token
      • Multisig Accounts
  • BUILD
    • Aleph Zero smart contracts basics
      • Setting up a Testnet account
      • Installing required tools
      • Creating your first contract
      • Deploying your contract to Aleph Zero Testnet
      • Extending your contract
    • Cross contract calls
      • Using references
      • Using dynamic calls
    • Migrating from Solidity
    • Writing e2e tests with ink-wrapper
    • Aleph Zero Signer integration
    • Front-end app: smart contract interaction
    • Security Course by Kudelski Security
      • ink! Developers Security Guideline
      • Lesson 1 - Getting started with ink!
      • Lesson 2 - Threat Assessment
      • Lesson 3 - Integer Overflow
      • Lesson 4 - Signed-integer
      • Lesson 5 - Role-Based Access Control
      • Lesson 6 - Address Validation
      • Lesson 7 - Smart Contract Control
    • Development on EVM-layer
  • PROTOCOL DETAILS
    • Shielder
      • Overview
      • Design against Bad Actors
      • Preliminaries - ZK-relations
      • Notes and Accounts
      • ZK-ID and Registrars
      • Anonymity Revokers
      • PoW Anonymity Revoking
      • Relayers
      • Deterministic Secret Management
      • SNARK-friendly Symmetric Encryption
      • SNARK-friendly Asymmetric Encryption
      • Cryptography
      • Token shortlist
      • User Wallet
      • Versioning
      • PoC
      • Version 0.1.0
      • Version 0.2.0
    • Common DEX
      • Common Whitepaper - Differences
      • Dutch Auctions
  • FAQ
  • Tutorials
    • Withdrawing coins from exchanges
      • How to withdraw your AZERO coins from KuCoin
      • How to withdraw your AZERO coins from MEXC Global
      • How to withdraw your AZERO coins from HTX
  • Setting up or restoring a wallet
    • How to set up or recover your AZERO account using Aleph Zero Signer
    • How to set up or recover your AZERO account using the official mainnet web wallet
    • How to set up or recover your AZERO account using Nova Wallet
    • How to set up or recover your AZERO account using SubWallet
    • How to set up or recover your AZERO account using Talisman
  • Staking
    • How to stake via a direct nomination using the Aleph Zero Dashboard
    • How to stake via a nomination pool using the Aleph Zero Dashboard
    • How to destroy a nomination pool via the Aleph Zero Dashboard
Powered by GitBook
On this page
  • Notes
  • Accounts
  • Operations
  • Updating Notes
  • Transactions updating Notes

Was this helpful?

  1. PROTOCOL DETAILS
  2. Shielder

Notes and Accounts

Here we describe the basic shielder design, then: in section ZK-ID and Registrars we enrich it with ZK-IDs for sybil resistance and in section Anonymity Revokers we propose an improvement that helps in fighting bad actors (see also Design against Bad Actors).

The shielder is smart contract that holds:

  • notes -- a binary Merkle Tree of a fixed depth H -- each node in this tree is a Scalar element. The leaves in the tree hold hashes of user Notes (see below).

  • nullifier_set -- a set of elements of type Scalar whose purpose is to invalidate old notes

  • roots -- a list of all historical Merkle roots, needed for technical reasons

  • Other less relevant storage items that we omit for brevity.

Notes

Each leaf of the notes Merkle Tree is a hash of a note. The Note is a data structure

struct Note {
    id: Scalar, // the ZK-ID of a user
    trapdoor: Scalar, // a secret needed to prove ownership of the note
    nullifier: Scalar, // a secret used to invalidate the note
    account_hash: Scalar, // the hash of the user's Account state
} 

We note that because 1) we store hashes of Note in the Merkle Tree, and 2) because trapdoor stays secret forever (only the user knows it), the id and the account stay private even if nullifier is revealed.

The ZK-ID is discussed in more detail in ZK-ID and Registrars however you can just think of it as the private key of the user.

Accounts

Instead of describing concretely what the Account structure is, we instead abstractly define the operations/properties that accounts should have. By adopting Rust terminology, we define the Account "trait", i.e., specify all the methods that should be defined on accounts.

  • fn new() -> Account Creates a new account.

  • fn hash(acc: Account) -> Scalar Hashing to a Scalar (field element)

  • fn update(acc: Account, op: Operation) -> Account update is a state transition function for Accounts, given an Operation such as add 2 ETH or subtract 5 AZERO

The set of operations depends on what do we want to support exactly. But one should have in mind something akin to:

enum OperationSimple {
    depositFT(Amount, TokenId, AccountId),
    withdrawFT(Amount, TokenId, AccountId),
    depositNFT(Id, AccountId),
    withdrawNFT(Id, AccountId),
}

There are additional technical details regarding the description of operations that arise because of details on how accounts are represented and accessed, but they are not essential for high-level understanding.

The simplest possible account structure that supports just a fixed list of fungible tokens would look as follows:

AccountSimple {
    balance_AZERO: Scalar,
    balance_USDT: Scalar,
    balance_USDC: Scalar,
    balance_wETH: Scalar,    
}

this structure is very simple and allows to implement all the required methods assuming that there are just two possible operations depositFT and withdrawFT . The downside is that it's not easily extendable to more token types and/or NFTs. So it might be beneficial to use a more complex structure, like below

AccountAdvanced {
    balance_AZERO: Scalar,
    other: Array<Scalar, 256>,
}

The other field is meant to be an array of 256 entries, each of which is an asset, either FT or NFT, represented as a hash, for instance, hash(ETH, 4) would represent 4 ETH. This account structure is certainly more flexible but it poses an issue when it comes to hashing it and proving correct updates efficiently. More specifically, we are interested in efficiently proving ZK-relations of the following form:

relation R_update_account_op
// op is a particular operation, like withdraw or deposit, along with all
// required arguments, like amount or tokenId

inputs:
    - h_acc_old: Scalar, 
    - h_acc_new: Scalar,
    
witnesses:
    - acc_new: Account,
    - acc_old: Account,

constraints:
    1. acc_new = Account::update(acc_old, op)
    2. h_acc_old = Account::hash(acc_old) // Account::hash is the hash method of the Account trait
    3. h_acc_new = Account::hash(acc_new) 

What matters to us is that for each operation opthe relation R_update_account_op should be possible to write as a small arithmetic circuit so that SNARKs for R_update_account_op can be generated efficiently (prover efficiency). It is also not a coincidence that the public inputs of R_update_account_op are hashes of acc_old and acc_new and not the values itself. The account size might be significant (as in AccountAdvanced) and hence we don't want them explicitly included in the circuit. Even though the constraints mention acc_old and acc_new the circuit does not have to unpack whole accounts as long as the Account::hash is smart enough (for instance it can Merklize other in AccountAdvanced). This way there is hope to make the size of the R_update_account_op circuit logarithmic (or even constant) in the size of Account.

Operations

For maximum flexibility and to enable certain less trivial use patterns we introduce an abstraction layer on Operation. Namely we assume that each operation op: Operation can be broken into:

  • op_priv: OpPriv - the "private" part of the operation that the user does not reveal

  • op_pub: OpPub - the "public" part of the operation that is visible in the transaction

Moreover we assume there is a function

fn combine(op_priv: OpPriv, op_pub: OpPub) -> Option<Operation>

which allows to extract an Operation like above given the public and private counterparts. Note that the output of combine is Option<Operation> and not Operation to signify that it can fail -- it will be apparent from the subsequent examples why is that.

The intuition to keep in mind is that op_pub in plaintext will be attached to a transaction the user sends (part of calldata) whereas op_priv will be only part of the witness of a ZK-relation that the user proves when executing the transaction. Typically op_priv is used to hold one of:

  • Data that the user wants to keep hidden. For instance when transferring funds to a different user the op_priv might contain the recipient "address" and transferred amount.

  • Data that is not necessary for public execution (see below in the description of transaction) of the operation and is just a technical detail related to how Accounts are represented. For instance we might want to include details on which index of the other Array is used to save data about a particular asset when using AccountAdvanced

One interesting option would be to set op_priv = op and op_pub = hash(op) -- this makes the size of op_pub just 1 Scalar which is good for the verifier complexity. Using some salt, to randomize the hash can even allow us to gain full privacy. This however might not be viable for operations like Deposit where the shielder contract is required to accept a public token transfer of a particular amount, and thus couldn't be done when amount is private.

Examples:

  • If we use AccountSimple as Account and the Operation type is similar to OperationSimple then we could just use OpPub = Operation and OpPriv = () (unit type -- "empty").

  • If we use AccountAdvanced as Account then the Operation type needs to contain more details than just OperationSimple -- indeed if the user makes deposit operation with +10 ETH then op must contain information which cell of the other Array should be modified and how. So one can think that op_priv specifies the non-deterministic details of op while op_pub is just a "human readable" representation of op.

Updating Notes

Using R_update_account_op as a black box, we can formulate the relation that's needed to update notes with respect to the operation op

relation R_update_note_op
// op_pub: OpPub is the public part of  the operation op to be performed

inputs:
    - h_note_new: Scalar,
    - merkle_root: Scalar,
    - h_nullifier_old: Scalar,
    
witnesses:
    - note_new, note_old: Note,
    - trapdoor_new, trapdor_old: Scalar
    - nullifier_new, nullifier_old: Scalar,
    - proof: MerkleProof
    - op_priv: OpPriv
    - id: Scalar
    
constraints:
    1. h_note_new = hash(note_new)
    2. note_new = Note { id, trapdoor_new, nullifier_new, h_acc_new }
    3. h_note_old = hash(note_old)
    4. note_old = Note { id, trapdoor_old, nullifier_old, h_acc_old }
    5. h_nullifier_old = hash(nullifier_old)
    6. verify_merkle_proof(merkle_root, h_note_old, proof)
    7. op = combine(op_pub, op_priv)
    8. R_update_account(op, h_acc_old, h_acc_new)

The hash of the nullifier is published, so that the contract can add it to nullifier_set, which prevents spending the same note again. The reason for not publishing the nullifier itself is to prevent a frontrunning attack. Specifically, a bad actor could intercept the user's nullifier, create their own note with that nullifier, and spend it before the user manages to spend it – thereby invalidating the user's note.

Note: our relations are parametrized by operation types (there is one for deposit, one for withdraw: generally one for each variant of `Operation`). Depending on the set of operations and their inputs, it's sometimes possible to define generic relations that can handle multiple different operations. This way it might be possible to hide the type of the performed operation at the cost of being required to build large, generic ZK-circuits that handle a few operations at once.

Transactions updating Notes

Finally we are able to write the pseudocode for a transaction the user sends to update its note

transaction update_note_op

inputs:
    - op_pub: OpPub,
    - proof: ZkProof,
    - h_nullifier_old: Scalar,
    - merkle_root: Scalar,
    - h_note_new: Scalar,
    
execution:
    - shielder.public_exec_op(op_pub)
    - assert: merkle_root is the current or historical root of shielder.notes
    - assert: h_nullifier_old not in shielder.nullifier_set
    - v = ZK-Verifier(R_update_note_op) // initialize verifier for the relation R_update_note_op
    - assert: v.verify(proof; (op_pub, h_note_new, merkle_root, h_nullifier_old))
    - shielder.notes.add_leaf(h_note_new)
    - shielder.nullifier_set.add(h_nullifier_old)

The above should be familiar for those who have studied privacy systems like ZCash.

The first instruction shielder.public_exec_op(op_pub) performs all operations on the "public state" that the operation op with public params op_pub requires. Below we give some examples. It is important to note that even though public_exec_op(op_pub) might perform some token transfers etc. as a first operation in the transaction, it will be rolled back in case some later operation fails (such as proof verification), so in fact it does not matter much where is this instruction placed within the function body.

Example: depositFT operation

// Below implementation for the depositFT variant
fn public_exec_depositFT(op_pub: OpPub) {
    let depositFT { amount, token_id, user } = op_pub;
    assert allowance(user, shielder) >= amount;
    transfer amount of token_id token from user to shielder;
}

Note that in the above if the user has not given enough allowance to the shielder contract, then the assert fails and hence the execution of update_note fails too.

The above is what the "public" part of the operation does. As mentioned before, the "full" version op: Operation that arises when op_pub is combined with the op_priv part is used to update the user's private account. What Account::update(acc, op) should do in this case is quite straightforward, depending on the specifics of Account this might be:

  • either incrementing one of the hardcoded fields (AccountSimple), or,

  • adding the tokens to one of the cells in acc.other (AccountAdvanced).

Example: withdrawFT operation

// Below implementation for the withdrawFT variant
fn public_exec_withdrawFT(op: Operation) {
    let withdrawFT { amount, token_id, user } = op_pub;
    transfer amount of token_id token from shielder to user;
}
PreviousPreliminaries - ZK-relationsNextZK-ID and Registrars

Last updated 2 months ago

Was this helpful?