Back

🌀Huracan

Written by FilosofiaCodigo

Sep 04, 2024 · 15 min read

Prefer to see the complete code? Head to Github to find all the code mentioned in this guide.

Learning about ZK today is not an easy task. It's a new technology with limited documentation. Huracan was born out of my own need to learn about ZK in a practical way, geared towards developers and engineers.

Huracan is a fully functional project capable of performing private transactions on Ethereum and EVM blockchains. It's based on battle tested privacy projects but includes only the minimal code to simplify the learning process. We will cover how this technology can be adapted to new use cases and future regulations. Additionally, at the end of the article, I share what is needed to take this project from testnet to real production use.

By the end of this guide, you'll be able to research other projects of the same nature and understand how they are built.

How Huracán is Built:

  • Circom circuits
  • Poseidon hash
  • Deposit and withdrawal logic in Solidity
  • Merkle tree generation in JS and Solidity, verification in Circom
  • HTML and JS vanilla Frontend
  • web3.js for web3 interaction and snarkjs for browser proving (zk-WASM)
  • Relayer with ethers.js 6 and Express to preserve user anonymity

1. How Huracán Works

Huracán is a DeFi tool that protects the identity of its users using a technique known as anonymous inclusion proofs to perform what is commonly called a mixer. This system can prove that a user has deposited ether into a contract without revealing which one he is.

Depositing in Huracán

Each user who deposits Ether into Huracán is added as a leaf in a Merkle tree within the contract

To achieve this, we need a smart contract where funds are deposited, which will generate a Merkle tree where each leaf represents a depositor. Additionally, we will need a circuit that generates inclusion proofs to keep the user anonymous when withdrawing funds. We also need a relayer that will execute the transaction on behalf of the anonymous user to protect their privacy.

Withdrawing from Huracán

Users can later withdraw their funds by proving they are part of the Merkle tree without revealing which leaf belongs to them

Below is the code, a brief explanation, and the supporting materials necessary to build and launch your own privacy-focused project.

2. The Circuit

Supporting Material: Private Smart Contracts with Solidity and Circom

The circuit is responsible for proving that you are part of the Merkle tree, meaning you are one of the depositors without revealing which one you are since you keep the parameters private while generating an inclusion proof that can be verified by a smart contract. Which parameters are kept private? During the deposit, we hash a private key and a nullifier to create a new leaf in the tree. The private key is a private parameter that will later serve to prove that you are the owner of that leaf. The nullifier is another parameter whose hash will be passed to the Solidity contract when redeeming funds, preventing a user from withdrawing funds twice in a row (double spend). The rest of the private parameters help the circuit reconstruct the tree and verify that you are part of it.

We start by installing the circomlib library, which contains the Poseidon circuits we will be using in this tutorial.

git clone https://github.com/iden3/circomlib.git

Now we create our proveWithdrawal circuit that proves we have deposited in the contract without revealing who we are.

proveWithdrawal.circom

pragma circom 2.0.0; include "circomlib/circuits/poseidon.circom"; template switchPosition() { signal input in[2]; signal input s; signal output out[2]; s * (1 - s) === 0; out[0] <== (in[1] - in[0])*s + in[0]; out[1] <== (in[0] - in[1])*s + in[1]; } template commitmentHasher() { signal input privateKey; signal input nullifier; signal output commitment; signal output nullifierHash; component commitmentHashComponent; commitmentHashComponent = Poseidon(2); commitmentHashComponent.inputs[0] <== privateKey; commitmentHashComponent.inputs[1] <== nullifier; commitment <== commitmentHashComponent.out; component nullifierHashComponent; nullifierHashComponent = Poseidon(1); nullifierHashComponent.inputs[0] <== nullifier; nullifierHash <== nullifierHashComponent.out; } template proveWithdrawal(levels) { signal input root; signal input recipient; signal input privateKey; signal input nullifier; signal input pathElements[levels]; signal input pathIndices[levels]; signal output nullifierHash; signal leaf; component commitmentHasherComponent; commitmentHasherComponent = commitmentHasher(); commitmentHasherComponent.privateKey <== privateKey; commitmentHasherComponent.nullifier <== nullifier; leaf <== commitmentHasherComponent.commitment; nullifierHash <== commitmentHasherComponent.nullifierHash; component selectors[levels]; component hashers[levels]; signal computedPath[levels]; for (var i = 0; i < levels; i++) { selectors[i] = switchPosition(); selectors[i].in[0] <== i == 0 ? leaf : computedPath[i - 1]; selectors[i].in[1] <== pathElements[i]; selectors[i].s <== pathIndices[i]; hashers[i] = Poseidon(2); hashers[i].inputs[0] <== selectors[i].out[0]; hashers[i].inputs[1] <== selectors[i].out[1]; computedPath[i] <== hashers[i].out; } root === computedPath[levels - 1]; } component main {public [root, recipient]} = proveWithdrawal(2);

To compile the circuit, we need to have both Circom and snarkjs installed. If you don't have them installed, follow the Circom installation guide.

Circom installation guide

Run the following commands to install circom and snarkjs.

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh git clone https://github.com/iden3/circom.git cd circom cargo build --release cargo install --path circom npm install -g snarkjs

Generate the trusted setup and zk artifacts we will need later on on the frontend.

circom proveWithdrawal.circom --r1cs --wasm --sym snarkjs powersoftau new bn128 12 pot12_0000.ptau -v snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v snarkjs groth16 setup proveWithdrawal.r1cs pot12_final.ptau proveWithdrawal_0000.zkey snarkjs zkey contribute proveWithdrawal_0000.zkey proveWithdrawal_0001.zkey --name="1st Contributor Name" -v snarkjs zkey export verificationkey proveWithdrawal_0001.zkey verification_key.json

We can now generate the verifier.sol contract.

snarkjs zkey export solidityverifier proveWithdrawal_0001.zkey verifier.sol

3. The Contracts

The contracts serve as a guarantee that everything was run correctly. They allow us to keep track of how much has been deposited and verify that the proofs are valid in order to release the funds. It’s important to note that everything happening in the smart contracts is public; this is the part of our system that is not anonymous.

We will use three contracts. The first is the verifier contract that we just generated in the verifier.sol file, so go ahead and deploy it now. For example you can use foundry to deploy it on Scroll Sepolia.

forge create --rpc-url https://scroll-testnet-public.unifra.io --private-key <PRIVATE_KEY> verifier.sol:Groth16Verifier

The second contract is the Poseidon contract. If you are on Scroll Sepolia, you can simply use the one I've already deployed at 0x52f28FEC91a076aCc395A8c730dCa6440B6D9519. If you want to use another blockchain, expand and follow the steps:

Deploy the Poseidon contract

The version of Poseidon we use in our circuit and contract must be exactly compatible. Therefore, we use the version in circomlibjs as shown. Just make sure to insert your private key and RPC URL in place of YOURPRIVATEKEY and YOURRPCURL.

git clone https://github.com/iden3/circomlibjs.git cd circomlibjs npm install cd .. node --input-type=module --eval "import { writeFileSync } from 'fs'; import('./circomlibjs/src/poseidon_gencontract.js').then(({ createCode }) => { const output = createCode(2); writeFileSync('poseidonBytecode', output); })" cast send --rpc-url TUURLRPC --private-key TULLAVEPRIVADA --create $(cat bytecode)

On Scroll, I added --legacy --gas-price 5000000000, probably when you see this video you won't need to add it. In any case you shouldn't need this in other chains. Regardless, this is the command just as I sent it.

cast send --rpc-url TUURLRPC --legacy --gas-price 5000000000 --private-key TULLAVEPRIVADA --create $(cat bytecode)

Now deploy the Huracan contract by passing the verifier and poseidon addresses as parameters.

// SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; interface IPoseidon { function poseidon(uint[2] memory inputs) external returns(uint[1] memory output); } interface ICircomVerifier { function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[3] calldata _pubSignals) external view returns (bool); } contract Huracan { ICircomVerifier circomVerifier; uint nextIndex; uint public constant LEVELS = 2; uint public constant MAX_SIZE = 4; uint public NOTE_VALUE = 0.001 ether; uint[] public filledSubtrees = new uint[](LEVELS); uint[] public emptySubtrees = new uint[](LEVELS); address POSEIDON_ADDRESS; uint public root; mapping(uint => uint) public commitments; mapping(uint => bool) public nullifiers; event Deposit(uint index, uint commitment); constructor(address poseidonAddress, address circomVeriferAddress) { POSEIDON_ADDRESS = poseidonAddress; circomVerifier = ICircomVerifier(circomVeriferAddress); for (uint32 i = 1; i < LEVELS; i++) { emptySubtrees[i] = IPoseidon(POSEIDON_ADDRESS).poseidon([ emptySubtrees[i-1], 0 ])[0]; } } function deposit(uint commitment) public payable { require(msg.value == NOTE_VALUE, "Invalid value sent"); require(nextIndex != MAX_SIZE, "Merkle tree is full. No more leaves can be added"); uint currentIndex = nextIndex; uint currentLevelHash = commitment; uint left; uint right; for (uint32 i = 0; i < LEVELS; i++) { if (currentIndex % 2 == 0) { left = currentLevelHash; right = emptySubtrees[i]; filledSubtrees[i] = currentLevelHash; } else { left = filledSubtrees[i]; right = currentLevelHash; } currentLevelHash = IPoseidon(POSEIDON_ADDRESS).poseidon([left, right])[0]; currentIndex /= 2; } root = currentLevelHash; emit Deposit(nextIndex, commitment); commitments[nextIndex] = commitment; nextIndex = nextIndex + 1; } function withdraw(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[3] calldata _pubSignals) public { circomVerifier.verifyProof(_pA, _pB, _pC, _pubSignals); uint nullifierHash = _pubSignals[0]; uint rootPublicInput = _pubSignals[1]; address recipient = address(uint160(_pubSignals[2])); require(root == rootPublicInput, "Invalid merke root"); require(!nullifiers[nullifierHash], "Vote already casted"); nullifiers[nullifierHash] = true; (bool sent, bytes memory data) = recipient.call{value: NOTE_VALUE}(""); require(sent, "Failed to send Ether"); data; } }

Now deploy it on-chain. If you're using forge on Scroll Sepolia you can do it with the following command.

forge create Huracan.sol:Huracan --rpc-url https://scroll-testnet-public.unifra.io --private-key <PRIVATE_KEY> --constructor-args <VERIFIER_ADDRESS>

4. The Frontend

Supporting Material: Privacy Interfaces with Solidity and zk-WASM

The frontend is the graphical interface we will be interacting with. In this demonstration, we will be using HTML and vanilla JS so that developers can adapt it to any frontend framework they are using. Something very important is that the frontend must be capable of producing zk proofs without leaking private information over the internet. This is why zk-WASM is crucial, as it allows us to efficiently generate proofs directly in our browser.

Now, create the following file structure:

js/
  blockchain_stuff.js
  snarkjs.min.js
json_abi/
  Huracan.json
  Poseidon.json
zk_artifacts/
  proveWithdrawal_final.zkey
  proveWithdrawal.wasm
index.html
  • js/snarkjs.min.js: Download snarkjs-0.7.4.zip, which contains the snarkjs.min.js library under the build/ directory.
  • json_abi/Huracan.json: The ABI of the CircomCustomLogic contract we just deployed (e.g., in Remix). You can obtain it by clicking the "ABI" button in the compilation tab.
  • json_abi/Poseidon.json: Use this file.
  • zk_artifacts: Place the previously generated artifacts in this folder. Note: Rename proveWithdrawal_0001.zkey to proveWithdrawal_final.zkey.
  • index.html, js/blockchain_stuff.js, and js/zk_stuff.js: These will be detailed below.

The HTML file contains the interface necessary for users to interact with Huracán.

index.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> </head> <body> <input id="connect_button" type="button" value="Connect" onclick="connectWallet()" style="display: none"></input> <p id="account_address" style="display: none"></p> <p id="web3_message"></p> <p id="contract_state"></p> <input type="input" value="" id="depositPrivateKey" placeholder="private key"></input> <input type="input" value="" id="depositNullifier" placeholder="nullifier"></input> <input type="button" value="Deposit" onclick="_deposit()"></input> <br> <input type="input" value="" id="withdrawPrivateKey" placeholder="private key"></input> <input type="input" value="" id="withdrawNullifier" placeholder="nullifier"></input> <input type="input" value="" id="withdrawRecipient" placeholder="recipient"></input> <input type="button" value="Withdraw" onclick="_withdraw()"></input> <br> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js"></script> <script type="text/javascript" src="js/zk_stuff.js"></script> <script type="text/javascript" src="js/blockchain_stuff.js"></script> <script type="text/javascript" src="js/snarkjs.min.js"></script> </body> </html> <script> function _deposit() { depositPrivateKey = document.getElementById("depositPrivateKey").value depositNullifier = document.getElementById("depositNullifier").value deposit(depositPrivateKey, depositNullifier) } function _withdraw() { withdrawPrivateKey = document.getElementById("withdrawPrivateKey").value withdrawNullifier = document.getElementById("withdrawNullifier").value withdrawRecipient = document.getElementById("withdrawRecipient").value withdraw(withdrawPrivateKey, withdrawNullifier, withdrawRecipient) } </script>

Al logic related to web3 is placed in the following file. This includes browser and wallet connection, state read and contract function calls.

js/blockchain_stuff.js

const NETWORK_ID = 534351 const HURACAN_ADDRESS = "0x8BD32BDC921f5239c0f5d9eaf093B49A67C3b9d0" const HURACAN_ABI_PATH = "./json_abi/Huracan.json" const POSEIDON_ADDRESS = "0x52f28FEC91a076aCc395A8c730dCa6440B6D9519" const POSEIDON_ABI_PATH = "./json_abi/Poseidon.json" const RELAYER_URL = "http://localhost:8080" var huracanContract var poseidonContract var accounts var web3 let leaves function metamaskReloadCallback() { window.ethereum.on('accountsChanged', (accounts) => { document.getElementById("web3_message").textContent="Se cambió el account, refrescando..."; window.location.reload() }) window.ethereum.on('networkChanged', (accounts) => { document.getElementById("web3_message").textContent="Se el network, refrescando..."; window.location.reload() }) } const getWeb3 = async () => { return new Promise((resolve, reject) => { if(document.readyState=="complete") { if (window.ethereum) { const web3 = new Web3(window.ethereum) window.location.reload() resolve(web3) } else { reject("must install MetaMask") document.getElementById("web3_message").textContent="Error: Porfavor conéctate a Metamask"; } }else { window.addEventListener("load", async () => { if (window.ethereum) { const web3 = new Web3(window.ethereum) resolve(web3) } else { reject("must install MetaMask") document.getElementById("web3_message").textContent="Error: Please install Metamask"; } }); } }); }; const getContract = async (web3, address, abi_path) => { const response = await fetch(abi_path); const data = await response.json(); const netId = await web3.eth.net.getId(); contract = new web3.eth.Contract( data, address ); return contract } async function loadDapp() { metamaskReloadCallback() document.getElementById("web3_message").textContent="Please connect to Metamask" var awaitWeb3 = async function () { web3 = await getWeb3() web3.eth.net.getId((err, netId) => { if (netId == NETWORK_ID) { var awaitContract = async function () { huracanContract = await getContract(web3, HURACAN_ADDRESS, HURACAN_ABI_PATH) poseidonContract = await getContract(web3, POSEIDON_ADDRESS, POSEIDON_ABI_PATH) document.getElementById("web3_message").textContent="You are connected to Metamask" onContractInitCallback() web3.eth.getAccounts(function(err, _accounts){ accounts = _accounts if (err != null) { console.error("An error occurred: "+err) } else if (accounts.length > 0) { onWalletConnectedCallback() document.getElementById("account_address").style.display = "block" } else { document.getElementById("connect_button").style.display = "block" } }); }; awaitContract(); } else { document.getElementById("web3_message").textContent="Please connect to Goerli"; } }); }; awaitWeb3(); } async function connectWallet() { await window.ethereum.request({ method: "eth_requestAccounts" }) accounts = await web3.eth.getAccounts() onWalletConnectedCallback() } loadDapp() const onContractInitCallback = async () => { document.getElementById("web3_message").textContent="Reading merkle tree data..."; leaves = [] let i =0 let maxSize = await huracanContract.methods.MAX_SIZE().call() for(let i=0; i<maxSize; i++) { leaves.push(await huracanContract.methods.commitments(i).call()) } document.getElementById("web3_message").textContent="All ready!"; } const onWalletConnectedCallback = async () => { } //// Functions //// const deposit = async (depositPrivateKey, depositNullifier) => { let commitment = await poseidonContract.methods.poseidon([depositPrivateKey,depositNullifier]).call() let value = await huracanContract.methods.NOTE_VALUE().call() document.getElementById("web3_message").textContent="Please confirm transaction."; const result = await huracanContract.methods.deposit(commitment) .send({ from: accounts[0], gas: 0, value: value }) .on('transactionHash', function(hash){ document.getElementById("web3_message").textContent="Executing..."; }) .on('receipt', function(receipt){ document.getElementById("web3_message").textContent="Success."; }) .catch((revertReason) => { console.log("ERROR! Transaction reverted: " + revertReason.receipt.transactionHash) }); } const withdraw = async (privateKey, nullifier, recipient) => { document.getElementById("web3_message").textContent="Generating proof..."; let commitment = await poseidonContract.methods.poseidon([privateKey,nullifier]).call() let index = null for(let i=0; i<leaves.length;i++) { if(commitment == leaves[i]) { index = i } } if(index == null) { console.log("Commitment not found in merkle tree") return } let root = await huracanContract.methods.root().call() let proof = await getWithdrawalProof(index, privateKey, nullifier, recipient, root) await sendProofToRelayer(proof.pA, proof.pB, proof.pC, proof.publicSignals) } const sendProofToRelayer = async (pA, pB, pC, publicSignals) => { fetch(RELAYER_URL + "/relay?pA=" + pA + "&pB=" + pB + "&pC=" + pC + "&publicSignals=" + publicSignals) .then(res => res.json()) .then(out => console.log(out)) .catch(); }

Finally, the file that contains all ZK logic. Capable of generating ZK proofs.

js/zk_stuff.js

async function getMerklePath(leaves) { if (leaves.length === 0) { throw new Error('Leaves array is empty'); } let layers = [leaves]; // Build the Merkle tree while (layers[layers.length - 1].length > 1) { const currentLayer = layers[layers.length - 1]; const nextLayer = []; for (let i = 0; i < currentLayer.length; i += 2) { const left = currentLayer[i]; const right = currentLayer[i + 1] ? currentLayer[i + 1] : left; // Handle odd number of nodes nextLayer.push(await poseidonContract.methods.poseidon([left,right]).call()) } layers.push(nextLayer); } const root = layers[layers.length - 1][0]; function getPath(leafIndex) { let pathElements = []; let pathIndices = []; let currentIndex = leafIndex; for (let i = 0; i < layers.length - 1; i++) { const currentLayer = layers[i]; const isLeftNode = currentIndex % 2 === 0; const siblingIndex = isLeftNode ? currentIndex + 1 : currentIndex - 1; pathIndices.push(isLeftNode ? 0 : 1); pathElements.push(siblingIndex < currentLayer.length ? currentLayer[siblingIndex] : currentLayer[currentIndex]); currentIndex = Math.floor(currentIndex / 2); } return { PathElements: pathElements, PathIndices: pathIndices }; } // You can get the path for any leaf index by calling getPath(leafIndex) return { getMerklePathForLeaf: getPath, root: root }; } function addressToUint(address) { const hexString = address.replace(/^0x/, ''); const uint = BigInt('0x' + hexString); return uint; } async function getWithdrawalProof(index, privateKey, nullifier, recipient, root) { let merklePath = await getMerklePath(leaves) let pathElements = merklePath.getMerklePathForLeaf(index).PathElements; let pathIndices = merklePath.getMerklePathForLeaf(index).PathIndices; let proverParams = { "privateKey": privateKey, "nullifier": nullifier, "recipient": addressToUint(recipient), "root": root, "pathElements": pathElements, "pathIndices": pathIndices } const { proof, publicSignals } = await snarkjs.groth16.fullProve( proverParams, "../zk_artifacts/proveWithdrawal.wasm", "../zk_artifacts/proveWithdrawal_final.zkey" ); let pA = proof.pi_a pA.pop() let pB = proof.pi_b pB.pop() let pC = proof.pi_c pC.pop() document.getElementById("web3_message").textContent="Proof generated please confirm transaction."; return { pA: pA, pB: pB, pC: pC, publicSignals: publicSignals } }

5. The Relayer

Generating ZK anonymity proofs makes no sense if we end up posting the transaction ourselves. Doing so would compromise privacy, as everything on Ethereum is public. This is why we need a relayer, an intermediary that executes the transaction on-chain on behalf of the anonymous user.

Let's start by creating the backend file.

relayer.mjs

import fs from "fs" import cors from "cors" import express from "express" import { ethers } from 'ethers'; const app = express() app.use(cors()) const JSON_CONTRACT_PATH = "./json_abi/Huracan.json" const CHAIN_ID = "534351" const PORT = 8080 var contract var provider var signer const { RPC_URL, HURACAN_ADDRESS, RELAYER_PRIVATE_KEY, RELAYER_ADDRESS } = process.env; const loadContract = async (data) => { data = JSON.parse(data); contract = new ethers.Contract(HURACAN_ADDRESS, data, signer); } async function initAPI() { provider = new ethers.JsonRpcProvider(RPC_URL); signer = new ethers.Wallet(RELAYER_PRIVATE_KEY, provider); fs.readFile(JSON_CONTRACT_PATH, 'utf8', function (err,data) { if (err) { return console.log(err); } loadContract(data) }); app.listen(PORT, () => { console.log(\`Listening to port \${PORT}\`) }) } async function relayMessage(pA, pB, pC, publicSignals) { console.log(pA) console.log(pB) console.log(pC) console.log(publicSignals) const transaction = { from: RELAYER_ADDRESS, to: HURACAN_ADDRESS, value: '0', gasPrice: "700000000", // 0.7 gwei nonce: await provider.getTransactionCount(RELAYER_ADDRESS), chainId: CHAIN_ID, data: contract.interface.encodeFunctionData( "withdraw",[pA, pB, pC, publicSignals] ) }; const signedTransaction = await signer.populateTransaction(transaction); const transactionResponse = await signer.sendTransaction(signedTransaction); console.log('🎉 The hash of your transaction is:', transactionResponse.hash); } app.get('/relay', (req, res) => { console.log(req) var pA = req.query["pA"].split(',') var pBTemp = req.query["pB"].split(',') const pB = [ [pBTemp[0], pBTemp[1]], [pBTemp[2], pBTemp[3]] ]; var pC = req.query["pC"].split(',') var publicSignals = req.query["publicSignals"].split(',') relayMessage(pA, pB, pC, publicSignals) res.setHeader('Content-Type', 'application/json'); res.send({ "message": "the proof was relayed" }) }) initAPI()

Install the coors library to run the relayer locally.

npm install cors express ethers

Now deploy the server by replacing TUURLRPC, TUHURACANADDRESS, TULLAVEPRIVADA, TUADDRESS in the following command.

RPC_URL=TUURLRPC HURACAN_ADDRESS=TUHURACANADDRESS RELAYER_PRIVATE_KEY=TULLAVEPRIVADA RELAYER_ADDRESS=TUADDRESS node relayer.mjs

You are ready to deposit and withdraw funds on Huracan from the web interface.

Huracan web

Once everything is ready this is how your app should look like

6. How to Take Huracan to Production?

a. Store Historical Roots On-Chain

By only storing the most recent root, the generated proof must use this root. This means that if someone deposits right after generating a withdrawal proof, and thus modifies the root, the proof will become invalid, and a new one will need to be generated.

Necessary Changes: Store the entire historical record of roots on-chain, for example, using a mapping like mapping(uint id => uint root) public roots; and use the most recent root when generating a proof. If someone makes a deposit and changes the root, there will be no problem as the verification will be done against any historically saved root using a function like isKnownRoot(uint root).

b. Index the Merkle Tree in an Accessible Place

To generate an inclusion proof, we need to read the current state of the tree. Currently, we read it from the commitments variable, but this process is slow and requires many RPC calls if the tree size is large.

Necessary Changes: Store and index the entire tree in an accessible location. An ideal place for this might be a subgraph.

c. Incentivize the Relayer

It’s necessary to offer a reward to the relayer, as they cover the on-chain transaction fees.

Necessary Changes: When generating the proof, allocate a percentage of the note to the relayer. You can do this by adding an extra parameter in the circuits, like signal input fee;, and in Solidity, send this value to msg.sender or to the address determined by the relayer.

d. Use Appropriate Libraries

In the web app, instead of vanilla HTML and JS, you should use a frontend framework like React, Angular, or Vue to provide a better experience for users and developers.

For the relayer, instead of Express, use a more robust backend and host it on a machine equipped to handle a high number of transactions with anti-DoS mechanisms and a suitable firewall, as the relayer’s funds used for gas are a target for hacking.

e. Define the Size of the Merkle Tree

This example works for 4 depositors; you’ll need to reflect changes in the circuit and contract to accommodate a larger tree.

Necessary Changes: Start by changing the number of levels in the circuit; it is currently set to 2, which is enough for a tree with 4 leaves. Also, update the constants LEVELS and MAX_SIZE in the contract. If your tree is very large, you can save gas on deployment by hardcoding the default values for an empty tree instead of using a loop as shown.

f. Remember, Everything We Used Is in Experimental Stages

The circuits and contracts in this guide are not properly audited, as are the libraries used. For example, Poseidon is a new hashing function that is promising and used instead of the traditional Pedersen.

Also, remember, this tutorial does not cover a secure trusted setup. It is recommended to conduct an open ceremony with sufficient time for participation.

7. Ideas for Further Exploration

a. Exclusion Proofs

Just as we handle inclusion proofs in this example, we can create exclusion proofs that demonstrate that we are not part of a blacklisted group. This could help in complying with future regulations that determine states.

b. Use ERC20s Instead of Ether

Instead of using Ether as the native currency in Solidity, you can use a specific ERC20 token. The changes would only be in the Solidity contracts and web app, while the circuits could remain the same.

c. Experiment with Re-Staking

Once you integrate ERC20s, a good next step might be to experiment with generating passive income using LSTs (Liquid Staking Tokens).

d. Think of Other Use Cases!

Anonymous inclusion proofs have many use cases, even beyond DeFi. Think about how you can apply what you’ve learned to voting systems, governance, social networks, video games, etc.

© 2024 Scroll Foundation | All rights reserved

Terms of UsePrivacy Policy