# FLAX Privacy Layer Working Spec (Archival version) Kyle Charbonnet and Wei Dai Sept 24 ## Introduction ![](https://i.imgur.com/2KMdwZ9.png) FLAX is an EVM compatible dApp that allows users to anonymously interact with any on-chain application that work with ERC20, 721, or 1155 assets. FLAX overcomes two shortcomings of existing privacy solutions for Ethereum: 1. Backwards incomaptibility -- requirement of changes made to existing DeFi smart contracts (Ex. Aztec) 2. Requirement of leaving and rejoining the anonymity pool in separate transactions (Ex. TornadoCash, Zkopru) With FLAX, users are able to use the FLAX Wallet contract as if it is their own smart contract wallet. There is no need to leave the anonymity pool as users are still able to interact with current DeFi and NFT applications. ## How does FLAX add privacy? ### Current State of Privacy on the Blockchain Ethereum, along with most other blockchains, every single transaction in the chain where everyone can view them. The transaction is stored along with its data so that everyone can see: 1. The address sending the transaction 2. The address receiving the transaction 3. Amount of Ether sent 4. In case of a smart contract call: which function was called and the input parameters 5. And more... Therefore, whenever a user sends ether, swaps ERC20 tokens, or mints an NFT, everyone can see which address sent the assets and which address received them. The following image is taken from Etherscan, a site that clearly illustrates the details of every transaction. ![](https://i.imgur.com/KnyJ0O1.png) One can regain some privacy by ensuring to use a new address for each transaction, but this comes with the following challenges: 1. Having enough balance to pay for gas for each address used 2. Managing all of the different addresses used For the above reasons, this strategy is too difficult for many to use. **For example...** Let's take an NFT collector who wants to receive a payment from someone they don't know or trust. In order to receive the payment, the collector will have to give up their address. Then, the payment sender can see all of the transaction history and assets that the collector owns. Even though many NFT collectors want to showcase their NFTs, it is also common for users to want to keep their assets private. The NFT collector essentially becomes doxxed every time they want to send or receive payments. Imagine if every time you ordered a coffee, the clerk would easily be able to see your total bank account numbers and payment history. **Coin Mixers** Another popular way to achieve some extent of privacy is the use of coin mixers. Coin mixers utilize zkSNARKs to enable users to anonymously transfer their assets. The most popular example of this is TornadoCash. However, there are some caveats. A coin mixer works by having users who want to make anonymous transfers all deposit their assets into a single smart contract (pool). Then, with the help of the zkSNARK, only the intended reciever is able to retrieve the asset. So, a common strategy to achieve anonymity is to transfer funds to the mixer, and then withdraw with a brand new address. If done correctly with enough users using the pool, no one can determine who the original sender was. However, the main limitations to this strategy are the same as listed above: 1. Users must have ether in the receiving address to be able to retrieve and use the assets 2. A user should use a new receiving address every time they want to perform an anonymous transaction - this involves address management **Unbanked?** Many view cryptocurrencies as an alternative solution to banks. Why save your money in a bank if you can instead have full control over it as a cryptocurrency? Especially with the surge of DeFi, cryptocurrencies are looking more and more competitive with existing institutions. However, this vision falls short when you consider the current lack of privacy available to cryptocurrency users. ### Enter FLAX ... ## High Level Architecture FLAX uses one large multi-asset pool, known as the Vault, to store every user's funds. All user interactions with dApps are initiated from the FLAX Wallet contract without revealing the identity of the user. * Vault.sol - responsible for holding all of the funds and is only callable by the Wallet. * Wallet.sol - * Manages all of the note commitments and therefore keeps track (cryptographically) of which users own which notes. * Makes the external contract calls for the user. * The entry point for users when they want to perform a DeFi transaction or a smart contract call. The Wallet will request the funds needed from the Vault, and then perform the contract call. Once the contract call is made, the Wallet will send back any remaining or new funds back to the Vault. The Wallet will then create new note commitments for these values for the user. The following diagram illustrates a high level view of the workflow: ![](https://i.imgur.com/Rq7h5S0.png) ### Batching In order to get the lowest gas cost per user interaction, FLAX will batch multiple transactions into one and submit them on-chain via a sequencer. This also enables the sequencer to pay gas in Ether for users, and the users can refund the sequencer with allowed ERC20 notes. The sequencer therefore must bundle all of the users' transaction data into one transaction call. A function call of multiple user transactions to the Vault is structured in the following format: 1. Bundle - Contains an Operation for each user's message 2. Operation - Contains the spending and refund note commitment information and a series of actions for the user 3. Action - An external function call (ex. DeFi Swap) These data types are represented as structs and are explained in more detail in later sections. ### FLAX API (External Functions) #### Wallet.sol // TODO: limit these APIs to only the designated sequencer? Or open to anyone? These are the exposed functions that the sequencer and users can use to interact with FLAX. **[1] ProcessBundle(Bundle bundle) returns (bool[] successes, bytes[][] results)** * Performs every Operation inside the bundle * For each Operation, handles: * Spend Txs - Verifies user owns the notes listed in the spend tx. Gathers specified funds from the Vault for each valid spend tx. * Actions - Makes an external call to the user-defined smart contract with the user-defined data for each Action. Note: An action can not call the Vault. * Refund Txs - Creates new note commitments for the user for each asset type with positive balance. Sends the funds to the Vault. Asset types must be known in advance. * The Wallet will contain 0 funds before this call, and 0 funds after. **[2] DepositFunds(Deposit deposit)** * Creates new note commitments for the deposited amount for the user calling this function. * Caller must be the owner of the deposit tokens. * This is to prevent any user from making your deposits for you if you have approved the vault to spend the tokens * User must first approve the **Vault** to spend the deposited amount. * The Vault is the one making the transfer to prevent users from using processBundle with a malicious action to transfer the funds how they like * Calls the Vault.makeDeposit(deposit), and the Vault will call (assetType).transfer(deposit.spender, address(this), deposit.value). **[3] BatchDepositFunds(Deposit[] deposits, Signature[] sigs)** * Makes a series of deposits in one call. Meant to be called by the sequencer. * The signatures must be from the address that owns the tokens being deposited and they must have signed their respective deposit. * Users must first approve the **Vault** to spend the deposited amount. * Calls the Vault.makeBatchDeposit(deposits), and the Vault will call Vault.makeDeposit(deposits[i]) for each deposit. **[4] Commit8FromQueue()** * Commits 8 leaves in the leafQueue to the noteCommitmentTree. The sequencer is responsible for calling this function periodically. Note: User's cannot spend note commitments in the queue. They must be commited to the tree via this function in order to be spent. #### Vault.sol These are the exposed functions that only the Wallet can call. **[1] RequestFunds(uint256[] values, address[] assetTypes) onlyWallet** * Transfers the given value for each asset type to the Wallet. * Called by the Wallet after the Wallet verified valid SpendTxs corresponding to the given values and asset types. * Not callable by processBundles since actions cannot call the Vault. **[2] makeDeposit(IFLAXTeller.Deposit calldata deposit) onlyWallet returns (bool)** * Calls assetType.transfer(deposit.spender, address(this), deposit.value) - essentially transfering approved funds to this contract. * Returns the success status of the transfer call. **[3] makeBatchDeposit(Deposit[] deposits, uint256 numApprovedDeposits) onlyWallet returns (uint256[], uint256)** * Calls Vault.makeDeposit(deposits[i]) for each deposit. * Returns an array of the indexes of the successful deposits, and the number of successful deposits. ### Front-End The Front-End design for FLAX can be found in a separate document - https://hackmd.io/BR7AHGViQcmICGzw1bY6Sw?edit ## FLAX Contracts ### Wallet ```javascript= contract Wallet { /** A single tree for all assets will get pretty large. Zcash uses a 2^32 sized tree. Haven't looked into it too much, but maybe use a verkle tree? */ IncrementalBinaryTree noteCommitmentTree; /** Past noteCommitmentTree roots. Used so that actions can use any past root instead of the most current one. */ mapping(uint256 => bool) pastRoots; /** Each note is tied to a nullifier. Once a note is spent, nullifierSet[nullifier] is set to true. This prevents double spending of notes. */ mapping(uint256 => bool) nullifierSet; /** Verifier contract for snark proofs. */ Verifier verifier; /** The contract that holds all of the funds. */ IVault vault; /** Calls a sequence of external DeFi functions (actions) using funds from spendTxs, each revealing a token type and an exact amount. Un-used and output tokens are refunded using refundTx. Before and after a call to processBundle, all ERC20/721/1155 balances of Wallet are 0. Phase 1: Fund - Verify that spendAuthProof authorizes all operations. - Spend Txs are procesed. Wallet requests each funding tokens (ERC20/721/1155 transfer) from Vault - ERC20/721/1155 approves are called on each token with non-zero funding balance Phase 2: DeFi interaction - Execute all functions encoded in payload Phase 3: Refund - Obtain ids of newly minted tokens - Compute refund amounts for each token (current balance) - Process refunds using RefundTxs */ function processBundle(Bundle bundle) external returns (bool[] successes, bytes[][] results) { } /** Users must approve the Vault/POOL (not the wallet) for the deposit amount first. Then, a sequencer will batch a group of deposits and the user signatures. The signature will be verified against deposits[i].spender, and pool.makeBatchDeposit will be called. Can add a second nullifier map to prevent replays. */ function batchDepositFunds(Deposit[] deposits, Signature[] sigs) external { } /** A user can call this to quickly deposit on their own. Msg.sender must approve the amount of tokens to the POOL before calling this. */ function depositFunds(Deposit deposit) external { } /** Commits 8 notes from the leaf queue. */ function commit8FromQueue() external { } /** This function is external only so that it can be used in a try/catch. There is a 'onlyThis' modifier so that only this contract can call it. 1. Handles all spend transactions, and updates balances[spendTx.assetType] += spendTx.value 2. Gathers the funds from the pool 3. Loops through the actions and makes the external calls 4. Handles the refunds */ function performOperation( uint256[2] validatingKey, Operation op ) external { } /** 1. For each spend token, get current balance of wallet and handle refund 2. For each refund token, get current balance of wallet and handle refund */ function handleAllRefunds( address[] spendTokens, address[] refundTokens, RefundTransaction refundTx ) internal { } /** 1. Verify spendTx.commitmentTreeRoot is a past root 2. Verify spendTx.nullifier is not in the nullifier set 2. Validate the spendTx.proof 3. Add spendTx.noteCommitment to the noteCommitmentTree queue 4. Set nullifiers[spendTx.nullifier] = true */ function handleSpendTx(SpendTransaction spendTx, uint256[2] validatingKey) { } /** 1. Create full note commitment = Commit(Commit(refundTx.baseNoteCommitment, type), value) 2. Add note commitment to noteCommitmentTree queue */ function handleRefundTx(RefundTransaction refundTx, address assetType, uint256 value) { } } ``` ### Vault ```javascript= /* Holds all deposited funds. Transfer temporary funds to Wallet contract. */ contract Vault { /** FLAX wallet address. */ address wallet; /** Only the wallet can access functions with this modifier. */ modifier onlyWallet() { } /** Only the wallet can call this function. Transfer the funds of the asset types to the wallet contract. */ function requestFunds(uint256[] values, address[] assetTypes) external onlyWallet { } /** Transfers the assetType from deposit.spender to this contract for each deposit. */ function makeBatchDeposit( Deposit[] deposits, uint256 numApprovedDeposits ) external onlyWallet { } /** Transfers the assetType from deposit.spender to this contract */ function makeDeposit(Deposit deposit) { } } ``` ### Notes on Confidential Transfers (TBD) * Approach 1: Make action circuit spend two notes and create two notes. Circuit complexity is doubled. * Approach 2: Make action circuit output committed type and value. Making them public amounts require revealing the commitment randomness. More calldata and gas cost. * Approach 3: Just don't support confidential transfers. ## Structs ```javascript= struct Bundle { Operation[] operations; uint256[2][5] validatingKeys; uint256[8] spendAuthProof; } struct Operation { SpendTransaction[] spendTxs; FLAXAddress refundAddr; Tokens tokens; Action[] actions; uint256 gasLimit; } struct Action { address contractAddress; bytes encodedFunction; } struct Tokens { address[] spendTokens; address[] refundTokens; } struct Deposit { FLAXAddress addr; address spender; address assetType; // ERC20/721/1155 contract addr uint256 value; uint256 id; // NFT id } struct Signature { uint8 v; bytes32 r; bytes32 s; } ``` #### Operation Digest The digest of an operation is the hash of necessary components of the operation. It shall include `commitmentTreeRoot, nullifier, noteCommitment, value, type, id` of each SpendTransaction as well as `refundAddr, tokens, actions, gasLimit`. ### Spend ```javascript= /** ZCash Orchard design, burns one note, creates one note, net value difference is publically revealed */ struct SpendTransaction { // Zcash params uint256 commitmentTreeRoot; uint256 nullifier; uint256 noteCommitment; uint256[8] proof; uint256 value; address assetType; } ``` Rules: * The notes spent must exist in the note commitment tree => handled by proof * The note nullifiers must not be in the note nullifier set => handled with nullifier * The nullifier set must be updated if the tx is successful * The transaction must have been authorized by the owner of the notes => handled by spendAuthSig * The net value change is correctly declared => handled by proof * The asset type is correctly declared => handled by proof ## Cryptography - We will utilize Groth16 proof system over the BN254 curve supported natively on Ethereum, using either Circom or ZoKrates. - Let $\mathbb F_r$ be the base field of BN254 and let $p$ be the group order of BN254. Most data elements will be encoded as elements of $\mathbb F_p$ for efficiency. - We use $\mathbb G$ to denote the Baby-Jubjub group of order $q$ whose base field is $\mathbb F_p$, and fix a generator $g \in \mathbb G$. - We use Poseidon hash, denoted `H`, over the scalar field of BN254. ### Rerandomizable keys ```javavascript= // TBD: Store compressed point or non-compressed point(2x size)? struct FLAXAddress { uint256 H1; // Compressed Babyjub point h1 uint256 H2; // Compressed Babyjub point h2 uint256 H3; // Compressed Babyjub point h3 } ``` * `pk` - FLAXAddress, aka public key, an array of three group elements from $\mathbb G$ * `vk` - Viewing / nullifier key, an element of $\mathbb F_p$ * `sk` - Spending / validating key, an element of $\mathbb F_p$ **Key relation** $$\{((H_1, H_2, H_3), (sk, vk)) \in \mathbb G^3 \times \mathbb F_q^2 \mid (vk \cdot H_1 = H_2) \mbox{ and } (sk \cdot H_1 = H_3)\}$$ A public key `pk` is canonical if `pk[0]` is the fixed generator $g$. * To support anonymous transactions, users will rerandomize their public key inside every `Operation` to receive output funds. * With access to $vk$, one can detect if a rerandomized public key is the one associated with $vk$. $vk$ also allows one to compute and prove nullifier of a note. **Design rationale** * Separate viewing authority ($vk$) and spending authority ($sk$) * Note scanning requires viewing key but is otherwise stateless **Signature** We use a variant of Schnorr signature over BabyJub curve with Poseidon hash where base generator is set to `pk[0]`. Verification is efficient inside a circuit (that is BN254-based). $$Verify(pk, m, (R, z)) : (z * pk[0] = R + H(pk, R, m) \cdot pk[2]).$$ ### Notes ```javavascript= // Each element, besides FLAXAddress, interpreted as an element of F_p struct Note { FLAXAddress owner; uint256 nonce; uint256 type; uint256 id; uint256 value; } ``` A note is a fundamental unit of data that is logically kept track inside the Merkle tree of the FLAX contract. It encodes three pieces of information, owner, type, and value. #### Note Commitment * `NoteCommit(Note) = H(owner, nonce, type, id, value)` #### Nullifier Derivation * `DeriveNullifier(sk, Note) = H(NoteCommit(Note), sk)` ### Circuits Each Action will include its own SNARK proof. Later on, we could potentially utilize proof aggregation (e.g. https://eprint.iacr.org/2021/529.pdf) or recursion (e.g. https://github.com/fluidex/plonkit) to "compress" proofs for multiple actions into one. #### Proof system candidates We use [Circom](https://github.com/iden3/circom) for the current prototype. Other candidates: * [PlonkIt](https://github.com/fluidex/plonkit) * [ZoKrates](https://zokrates.github.io/) #### Spend Circuit The semantics circuit is: "I own and spend some note with `nullifier` which is part of tree with `anchor` and created a new note that commits to `newNoteCommit`, which results in net difference of `value` for `type` asset with NFT id `id`, and I authorize operationDigest". Primary (public) inputs: * `(newNoteCommitment, nullifier, type, id, value, anchor, operationDigest)` Auxilliary (private) inputs: * `(vk, oldNote, oldNotePath, newNote, authSig)` The ActionTransaction proof must prove: 1. Old NoteCommitment integrity: `oldNoteCommitment == NoteCommit(oldNote)` 2. Old NoteCommitment is merkle tree with root `anchor` via `oldNotePath` 4. Nullifier integrity: `nullifier == DeriveNullifier(vk, oldNote)` 5. Viewing key validity: `vk * oldNote.address[0] = oldNote.address[1]` 3. Value integrity: `value == val_old - val_new` 3. Type integrity: `oldNote.type == newNote.type == type` 3. Id integrity: `oldNote.id == newNote.id == id` 5. AuthSig validity: `Verify(oldNote.address, operationDigest, authSig)` 6. New NoteCommitment integrity: `newNoteCommitment == NoteCommit(newNote)` The `authSig` param is used to prove that the owner of the input note has authorized the entire operation containing the note. ## Privacy Anyone on the blockchain can find out: * Amounts associated with Spend and Refund Users cannot find out: * Who owns a note * Which note is spent during Spend ## Roadmap / feature list - Backend and circuits - Prototyping and gas / prover cost estimates and analysis - Implement prototype Spend and signature aggregation circuits in Circom and do benchmarks - Spend circuit (prototyped - Wei) - Sig aggregation circuit (compare cost and see if we need it - Wei) - Implement a prototype FLAX contract (Started - Kyle) - Sequencer infra - Frontend: user wallet - Note management (storage / backup / restore / scanning) - Read (dApp obtaining data from user wallet) - Balance reading API - Asset ownership proofs - NFT Gallery - Write (dApp requesting transactions) - Contract call API - Prove - Verifiable selective disclosure of information. - Circuits - Frontend - Schnorr multi-sig - Branding - Partnerships - Swaps - NFT projects - NFT-based lending & asset vaults ## Design TODOS ### In-Band Distribution vs Remove cTransfer? To allow confidential transfer we also need to support in-band distribution so that note receivers can see when a note is transferred to them. For a MVP, we should consider possibility of removing confidential transfer, i.e. cTransfer. Note that anonymous transfers can still be done via performOperations. - cTransfer requires a different circuit and in-band secret distribution mechanism. Overall supporting cTransfer creates a lot of complexity. - In-band secret distribution also increases calldata costs - Regulation compliance: potentially easier on regulation compliance if protocol does not support confidential transfer. ### Gas - Outline how users will pay gas anonymously - [EIP 4337](https://eips.ethereum.org/EIPS/eip-4337) ## Appendix ### Design Choices #### Spending Authorization Signature vs ZK Snark Technically, proof of the spending key can be included into the zk-snark of an action. We should consider this option if it reduces gas costs. Zcash does not include proof of the spending key inside the snark, and chose to use a spending authorization signature instead. Their reasoning (p54 of Zcash protocol paper): > Knowledge of the spending key could have been proven directly in the Spend statement or Action statement, similar to the check in § 4.17.1 ‘JoinSplit Statement (Sprout)’ on p. 57 that is part of the JoinSplit statement. The motivation for a separate signature is to allow devices that are limited in memory and computational capacity, such as hardware wallets, to authorize a Sapling or Orchard shielded Spend. Typically such devices cannot create, and may not be able to verify, zk-SNARK proofs for a statement of the size needed using the BCTV14, Groth16, or Halo 2 proving systems. ### Future Features * Allow exchanges between different asset types in the private asset pool. Something similar - https://ethresear.ch/t/private-binding-negotiations/12426 * Bundle multiple snarks into one - https://eprint.iacr.org/2021/529.pdf ### Confidential Bridging Allow users to transfer tokens from one chain to another without revealing the amount transferred. This can be done via a confidential bridge. The method is similar to the FLAX asset pool, where a note is burned on one end of the bridge, and then created on the other end of the bridge. The major piece to figure out here is determining how to coordinate both ends of the bridge. ### Gas Estimates Note: Got these from Wei Jie's slides from 0xPARC group - https://docs.google.com/presentation/d/1G1zQjTKPKclUtwaYidek07eceL7BB1rZ0nMH39dU3yw/edit#slide=id.p https://ethresear.ch/t/gas-and-circuit-constraint-benchmarks-of-binary-and-quinary-incremental-merkle-trees-using-the-poseidon-hash-function/7446 * Groth16 proof verification: ~200k gas + ~21k gas per public input * Poseidon (2 inputs): 53189 gas, 240 constraints * Poseidon (5 inputs): 121984 gas, ??? constraints * SHA256 (2 inputs): 2179 gas, 409926 constraints * Proof generation (1.1M constraints): * ~ 7 seconds on an Intel i7 1.80GHz laptop with rapidsnark (optimised for Intel x86) * ~ 17 seconds with zkutil (Rust/bellman) * Minutes with snarkjs (Nodejs) * Binary Merkle Tree (Poseidon Hash) * Depth = 32, Gas Cost = 1.32 million (for entire transaction) * Quinary Merkle Tree (Poseidon Hash) * Depth = 15, Gas Cost = 1.78 million (for entire transaction) Gas costs for performOperation: * 1 merkle tree insert per spend ~1.3 million gas * 1 proof verification per spend ~200k gas + 21k per pub input * 1 Poseidon hash per refund tx ~50k gas * 1 merkle tree insert per refund tx ~1.3 million gas * 2 contract.balanceOf calls per ERC20 interacted with Gas costs for "rolled" performOperation consisting of n ops, m spends, and p refunds: * 1 Merkle tree insertion for 8 note commitments ~1.4 mil * Note: 1 Merkle tree insertion for 16 note commitments is ~1.6mil * 1 proof verification per spend ~m*(200k + 6*21k) * 1 proof verification for rolled signatures ~(200k + 3n * 21k) * 1 poseidon hash per refund ~p*50k * 2 transfer and balance calls per asset involved ~4.1k for transfer, ~1.4k for balanceOf (depends on ERC20 impl) * Total estimated overhead (up to 8 note commitments, (m + p) <= 8): ~1.4mil + m*(200k + 6*21k) + (200k + 3n*21k) + p*50k * Assuming 4 operations batched (n = 4), 1 spend and 1 refund per operation (8 total potential note commitments commited in a batch to the merkle tree), overhead: * 1.4 million + 4*(200k + 126k) + (200k + 3*4*21k) + 4*50k * = ~3.356 million gas for the bundle * = ~839k gas per user operation * Assuming 8 operations batched (n = 8), 1 spend and 1 refund per operation (16 total potential note commitments commited in a batch to the merkle tree), overhead: * 1.6 million + 8*(200k + 126k) + (200k + 3*8*21k) + 8*50k * = ~5.312 million gas for the bundle * = ~332k gas per user operation ### Proving time estimates Approach 1: 12k constraints, <5s with wasmsnark, per numbers provided here https://github.com/tornadocash/tornado-core/tree/master/circuits Approach 2: 16k constraints, ~5s with wasmsnark ### Links * FLAX Paper - https://eprint.iacr.org/2021/1249.pdf * Zcash protocol (whitepaper, updated) - https://zips.z.cash/protocol/protocol.pdf * (Separate L2) Aztec yellowpaper (2021) - https://hackmd.io/@aztec-network/ByzgNxBfd - BN254 / Grumpkin cycle * (EVM) Aztec doc - https://docs.aztecprotocol.com/ * Yellowpaper - https://hackmd.io/@aztec-network/ByzgNxBfd * (Separate L2) zkopru - https://docs.zkopru.network/ * (Separate L2) Polygon Nightfall - https://polygon.technology/solutions/polygon-nightfall/ * (EVM) Espresso - https://docs.cape.tech/espresso-systems/cape-technical-documentation/introduction * (Private Bridge Txs) Mystiko - https://docs.mystiko.network/ * Info on curve choices - https://docs.gnark.consensys.net/en/latest/Concepts/schemes_curves/ * Write contracts in FE or Vyper? * FE - https://fe-lang.org/ * Vyper - https://vyper.readthedocs.io/en/stable/ * Security Concern - https://arxiv.org/abs/1805.03180