Interfaces con privacidad en Solidity y zk-WASM
Ahmed Castro
Posted on June 25, 2024
Los usuarios de blockchain necesitan privacidad en sus finanzas, identidad, redes sociales y más. Pero web3 es transparente y público. Entonces, ¿cómo pueden los usuarios proteger su anonimato en un ambiente con estas características?
La clave es crear pruebas de computación en un lugar donde únicamente el usuario tiene acceso, donde los datos del usuario estén seguros. Ese lugar es precisamente el navegador, antes que los datos del usuarios toquen el internet. A esto le llamamos generación de pruebas a nivel de cliente, "client side proving", o "browser proving" en inglés.
Para mantener los parámetros privados, estos nunca deben salir de nuestro navegador
Conozcamos, con un ejemplo práctico y sencillo, cómo crear interfaces que hagan uso de zk-wasm, la tecnología que hace esto posible.
Dependencias
Si aún no lo tienes, instala NPM, yo estaré usando la versión 20.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash nvm install 20
Para este ejemplo usaremos circom, si no lo tienes instalado puedes hacerlo con los comandos a continuación.
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
1. Crea un circuito
Haremos un ejemplo muy sencillo: generar una prueba de computación de una multiplicación a*b=c
pero manteniendo privados a
y b
. Si te interesa un ejemplo más avanzado con un caso de uso real visita mi artículo anterior.
Circom nos permite crear circuitos, que permiten generar pruebas de ejecución ofuscando los parámetros.
Comienza creando el circuito a continuación.
myCircuit.circom
pragma circom 2.0.0;
template Multiplier() {
signal input a;
signal input b;
signal output c;
c <== a*b;
}
component main = Multiplier();
Ahora compílalo y genera los artefactos que usaremos más adelante.
circom myCircuit.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 myCircuit.r1cs pot12_final.ptau myCircuit_0000.zkey
snarkjs zkey contribute myCircuit_0000.zkey myCircuit_0001.zkey --name="1st Contributor Name" -v
snarkjs zkey export verificationkey myCircuit_0001.zkey verification_key.json
2. Lanza los contratos
El siguiente comando generará un contrato verificador en el archivo de verifier.sol
. Lánzalo en un blockchain de preferencia. Este contrato contiene la función verifyProof()
que recibe por parámetro una prueba de computación hecha con nuestro circuito y devuelve true
si la prueba es correcta.
Nota: Este contrato es compatible con L1 EVMs, L2 optimistas, pero en términos de L2 ZK, actualmente únicamente es compatible con Scroll.
snarkjs zkey export solidityverifier myCircuit_0001.zkey verifier.sol
Ahora lanza el siguiente contrato de lógica personalizada pasando como parámetro de constructor el address del contrato verificador que lanzamos anteriormente. En este contrato puedes agregar la lógica en solidity que desees, por ejemplo el conteo de votos de un sistema de votación o la recepción o envío de tokens ERC20 en un sistema DeFi anónimo. En este ejemplo únicamente almacenaremos el resultado de la multiplicación que hicimos en nuestro circuito.
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
interface ICircomVerifier {
function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[1] calldata _pubSignals) external view returns (bool);
}
contract CircomCustomLogic {
ICircomVerifier circomVerifier;
uint public publicInput;
constructor(address circomVeriferAddress) {
circomVerifier = ICircomVerifier(circomVeriferAddress);
}
function sendProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[1] calldata _pubSignals) public {
// ZK verification
circomVerifier.verifyProof(_pA, _pB, _pC, _pubSignals);
// Your custom logic
publicInput = _pubSignals[0];
}
}
3. Construye un frontend
Ahora crea la siguiente estructura de archivos:
js/
blockchain_stuff.js
snarkjs.min.js
json_abi/
MyContract.json
zk_artifacts/
myCircuit_final.zkey
myCircuit.wasm
verification_key.json
index.html
-
js/snarkjs.min.js
: descarga este archivo que contiene la librería de snark.js -
json_abi/MyContract.json
: el ABI del contratoCircomCustomLogic
que recién lanzamos, por ejemplo en Remix, lo puedes hacer dando clic en el botón "ABI" en la pestaña de compilación. -
zk_artifacts
: coloca en esta carpeta los artefactos generados anteriormente. Nota: Cambia el nombre demyCircuit_0002.zkey
pormyCircuit_final.zkey
-
index.html
yjs/blockchain_stuff.js
los detallo a continuación
El archivo HTML a continuación describe la interfaz gráfica donde colocaremos los números a multiplicar. En un ambiente de producción recomendaría usar un frontend framework como react, vue o angular. Este ejemplo es con fines didácticos.
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="a"></input>
<input type="input" value="" id="b"></input>
<input type="button" value="Send Proof" onclick="_sendProof()"></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/blockchain_stuff.js"></script>
<script type="text/javascript" src="js/snarkjs.min.js"></script>
</body>
</html>
<script>
function _sendProof()
{
a = document.getElementById("a").value
b = document.getElementById("b").value
sendProof(a, b)
}
</script>
Nuestro archivo de javascript contiene tanto la lógica de generación de pruebas zk usando la librería snark.js como la lógica de blockchain usando la librería web3.js. En un ambiente de producción recomendaría usar typescript y no javascript vainilla, este ejemplo es con fines didácticos.
js/blockchain_stuff.js
const NETWORK_ID = 534351
const MY_CONTRACT_ADDRESS = "0xFdAFc996a60bC5fEB307AAF81b1eD0A34a954F06"
const MY_CONTRACT_ABI_PATH = "./json_abi/MyContract.json"
var my_contract
var accounts
var web3
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 () {
my_contract = await getContract(web3, MY_CONTRACT_ADDRESS, MY_CONTRACT_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 Scroll Testnet";
}
});
};
awaitWeb3();
}
async function connectWallet() {
await window.ethereum.request({ method: "eth_requestAccounts" })
accounts = await web3.eth.getAccounts()
onWalletConnectedCallback()
}
loadDapp()
const onContractInitCallback = async () => {
var publicInput = await my_contract.methods.publicInput().call()
var contract_state = "Public input: " + publicInput
document.getElementById("contract_state").textContent = contract_state;
}
const onWalletConnectedCallback = async () => {
}
//// Functions ////
const sendProof = async (a, b) => {
document.getElementById("web3_message").textContent="Generating proof...";
const { proof, publicSignals } = await snarkjs.groth16.fullProve( { a: a, b: b}, "../zk_artifacts/myCircuit.wasm", "../zk_artifacts/myCircuit_final.zkey");
const vkey = await fetch("../zk_artifacts/verification_key.json").then( function(res) {
return res.json();
});
const res = await snarkjs.groth16.verify(vkey, publicSignals, proof);
pA = proof.pi_a
pA.pop()
pB = proof.pi_b
pB.pop()
pC = proof.pi_c
pC.pop()
document.getElementById("web3_message").textContent="Proof generated please confirm transaction.";
const result = await my_contract.methods.sendProof(pA, pB, pC, publicSignals)
.send({ from: accounts[0], gas: 0, value: 0 })
.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)
});
}
4. Prueba la aplicación
Antes de probar, debes adaptar las variables NETWORK_ID
y MY_CONTRACT_ADDRESS
en js/blockchain_stuff.js
. NETWORK_ID
es el identificador único de la chain que estés usando, en este ejemplo estoy usando 534351
que representa a Scroll Sepolia Testnet, si deseas usar otro te recomiendo buscar el identificador en chainlist. Por otro coloca el address del contrato CircomCustomLogic
que recién lanzaste en la variable MY_CONTRACT_ADDRESS
.
Ahora estás listo para probar la aplicación en cualquier servidor web. Usualmente yo uso lite-server
para desarrollar, así lo instalas y levantas un servidor, solo asegúrate de estar en la carpeta del proyecto:
npm install -g lite-server #para instalar
lite-server #para levantar el servidor
Una vez todo esté listo, tu aplicación debería de verse de esta manera
¿Qué más necesito aprender para desarrollar una zkDapp?
Para desarrollar una aplicación descentralizada y anónima necesitarás una combinación de conocimientos en Circom, Solidity y programación web. Dependiendo de los casos de uso también ocuparás un poco de programación backend en el caso de necesitar un Relayer. Estaré creando guías sobre estos temas así que te invito a que te suscribas.
También te dejo un par de materiales de aprendizaje como siguiente paso:
¡Gracias por leer esta guía!
Sígueme en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.
Posted on June 25, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.