ZK es una tecnología que trae nuevos beneficios al blockchain. Tanto para escalar como para privacidad. En este workshop vamos a hacer un primer circuito ZK que nos servirá para aprender los conocimientos básicos sobre el tema. Haremos un juego llamado ZK Sudoku, enviaremos una prueba que resolvimos el rompecabezas a un smart contract y este nos dará un token como premio por hacerlo.
Antes de comenzar
Para este tutorial ocuparásNodeJs que recomiendo descargarlo en Linux viaNVM, y también necesitarásMetamask u otra wallet compatible con fondos en Goerli que puedes obtener desde unfaucet.
Obtener fondos en Scroll Testnet
Puedes ir albridge de Scroll para mover fondos de Goerli testnet a Scroll Alpha. Coloca tu wallet en Goerli y desde ahí mueve fondos a Scroll L2.
💡 Este post muestra como lanzar el proyecto en Scroll. Mas adelante mostraremos los cambios necesarios para hacerlo en otras chains.
Ceremonia Inicial
Esta ceremonia inicializa losParámetros Globales que son necesarios para asegurar la computación de la generación de pruebas. La ceremonia nos devolvera lallave de comprobación (PK) y lallave de verificación (VK). Estlo lo haremos a partir de un circuito escrito en el lenguaje Zokrates.
Comenzamos creando el siguiente archivo html.
generateVerifier.html
<!DOCTYPE html><htmllang="en"><head><metacharset="utf-8"></head><body><textareacols="40"rows="5"id="_source">def main(private field a, field b){ assert(a * a == b); return;}</textarea><inputtype="button"value="Generate Verifier"onclick="_generate()"></input><h3>Verifier</h3><textareacols="40"rows="5"id="verifier"></textarea><br><h3>Prover key</h3><textareacols="40"rows="5"id="pk"></textarea><br></body></html><scriptsrc="https://unpkg.com/zokrates-js@latest/umd.min.js"></script><script>function_generate(){zokrates.initialize().then((zokratesProvider)=>{varsource=document.getElementById("_source").valueconstartifacts=zokratesProvider.compile(source);constkeypair=zokratesProvider.setup(artifacts.program);constverifier=zokratesProvider.exportSolidityVerifier(keypair.vk);document.getElementById("verifier").textContent=verifier;document.getElementById("pk").textContent='{"pk":['+keypair.pk+']}';});}</script>
Ahora lo corremos en un servidor web. Puede ser cualquier servidor de archivos estáticos. En este caso vamos a usarlite-server
.
Para instalar el servidor corremos lo siguiente.
npminstall-g lite-server
Y esto para levantar el servidor.
lite-server
Accedemos al servidor web desde la urlhttp://localhost:3000/generateVerifier.html.
Una vez lista la interfaz, colocamos el circuito en el primer cuadro de texto. Y luego hacemos clic en "Generate Verifier".
// Sudoku of format// | a11 | a12 || b11 | b12 |// --------------------------// | a21 | a22 || b21 | b22 |// ==========================// | c11 | c12 || d11 | d12 |// --------------------------// | c21 | c22 || d21 | d22 |// We use a naive encoding of the values as `[1, 2, 3, 4]` and rely on if-else statements to detect duplicatesdefcountDuplicates(fielde11,fielde12,fielde21,fielde22)->field{fieldmutduplicates=e11==e12?1:0;duplicates=duplicates+(e11==e21?1:0);duplicates=duplicates+(e11==e22?1:0);duplicates=duplicates+(e12==e21?1:0);duplicates=duplicates+(e12==e22?1:0);duplicates=duplicates+(e21==e22?1:0);returnduplicates;}// returns 0 for x in (1..4)defvalidateInput(fieldx)->bool{return(x-1)*(x-2)*(x-3)*(x-4)==0;}// variables naming: box'row''column'defmain(fielda21,fieldb11,fieldb22,fieldc11,fieldc22,fieldd21,privatefielda11,privatefielda12,privatefielda22,privatefieldb12,privatefieldb21,privatefieldc12,privatefieldc21,privatefieldd11,privatefieldd12,privatefieldd22)->bool{// validate inputsassert(validateInput(a11));assert(validateInput(a12));assert(validateInput(a21));assert(validateInput(a22));assert(validateInput(b11));assert(validateInput(b12));assert(validateInput(b21));assert(validateInput(b22));assert(validateInput(c11));assert(validateInput(c12));assert(validateInput(c21));assert(validateInput(c22));assert(validateInput(d11));assert(validateInput(d12));assert(validateInput(d21));assert(validateInput(d22));fieldmutduplicates=0;// globally counts duplicate entries in boxes, rows and columns// check box correctness// no duplicatesduplicates=duplicates+countDuplicates(a11,a12,a21,a22);duplicates=duplicates+countDuplicates(b11,b12,b21,b22);duplicates=duplicates+countDuplicates(c11,c12,c21,c22);duplicates=duplicates+countDuplicates(d11,d12,d21,d22);// check row correctnessduplicates=duplicates+countDuplicates(a11,a12,b11,b12);duplicates=duplicates+countDuplicates(a21,a22,b21,b22);duplicates=duplicates+countDuplicates(c11,c12,d11,d12);duplicates=duplicates+countDuplicates(c21,c22,d21,d22);// check column correctnessduplicates=duplicates+countDuplicates(a11,a21,c11,c21);duplicates=duplicates+countDuplicates(a12,a22,c12,c22);duplicates=duplicates+countDuplicates(b11,b21,d11,d21);duplicates=duplicates+countDuplicates(b12,b22,d12,d22);// the solution is correct if and only if there are no duplicatesassert(duplicates==0);returntrue;}
La llave de comprobación, será generada en formato json. En una de las cajas posteriores en la UI. Colocamos esta llave en el archivoassets/pk.json
en la misma carpeta de proyecto. También guardemos el circuito de Zokrates en el archivoassets/sudoku.zok
.
El smart contract de tokens
La llave de verificación, o VK, nos es devuelta incrustada en un contrato inteligente en Solidity.
Vamos a modificar el contrato generado para que nos otorgue un token por completar la respuesta.
Para eso vamos a hacer lo siguientes cambios en el contrato.
Primero importamos las librerías de OpenZeppelin necesarias para lanzar tokens ERC20. Y luego el contrato que hereda del contrato verificador que fue generado anteriormente. Para hacer esto puedes ya sea importar el contrato verificador como un nuevo archivo o colocar este código luego del contrato verificador generado.
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";// SPDX-License-Identifier: MITcontract ZKSudokuToken is Verifier, ERC20 { constructor() ERC20("ZK Sudoku Token", "ZKST") { } function mintWithProof(Proof memory proof, uint[7] memory input) public { require(verifyTx(proof, input)); _mint(msg.sender, 10 ether); }}
Lanza el smart contract y agrega el ABI en el archivoassets/VerifierABI.json
.
El frontend
Agregamos el archivo HTML del frontend que contiene todo el UI.
<!DOCTYPE html><htmllang="en"><head><metacharset="utf-8"></head><body><inputid="connect_button"type="button"value="Connect"onclick="connectWallet()"style="display: none"></input><pid="account_address"style="display: none"></p><pid="web3_message"></p><inputtype="input"value="1"id="a11"size="2"disabled></input><inputtype="input"value="2"id="a12"size="2"disabled></input> |<inputtype="input"value=""id="b11"size="2"></input><inputtype="input"value="4"id="b12"size="2"disabled></input><br><inputtype="input"value="3"id="a21"size="2"disabled></input><inputtype="input"value=""id="a22"size="2"></input> |<inputtype="input"value="1"id="b21"size="2"disabled></input><inputtype="input"value="2"id="b22"size="2"disabled></input><br> ----------------------------------<br><inputtype="input"value="2"id="c11"size="2"disabled></input><inputtype="input"value="1"id="c12"size="2"disabled></input> |<inputtype="input"value="4"id="d11"size="2"disabled></input><inputtype="input"value="3"id="d12"size="2"disabled></input><br><inputtype="input"value="4"id="c21"size="2"disabled></input><inputtype="input"value="3"id="c22"size="2"disabled></input> |<inputtype="input"value="2"id="d21"size="2"disabled></input><inputtype="input"value=""id="d22"size="2"></input><br><inputtype="button"value="Generate Proof"onclick="_verify()"></input><inputtype="button"value="Mint"onclick="_mint()"></input><br><pid="proof"></p><scripttype="text/javascript"src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js"></script><scriptsrc="https://unpkg.com/zokrates-js@latest/umd.min.js"></script><scripttype="text/javascript"src="blockchain_stuff.js"></script></body></html><script>function_verify(){verify()}function_mint(){mintWithProof()}</script>
Finalmente agregamos el archivoblockchain_stuff.js
que contiene la lógica del blockchain. Solo asegúrate de actualizarMY_CONTRACT_ADDRESS
con el address del contrato que recién lanzamos.
constNETWORK_ID=534353constMY_CONTRACT_ADDRESS="0x31379Cff7f2846fa34DA42265F1C50B827653DE9"constMY_CONTRACT_ABI_PATH="./assets/VerifierABI.json"constPK_PATH="./assets/pk.json"constZOK_PATH="./assets/sudoku.zok"varmy_contractvarzokratesProvidervaraccountsvarweb3functionmetamaskReloadCallback(){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()})}constgetWeb3=async()=>{returnnewPromise((resolve,reject)=>{if(document.readyState=="complete"){if(window.ethereum){constweb3=newWeb3(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){constweb3=newWeb3(window.ethereum)resolve(web3)}else{reject("must install MetaMask")document.getElementById("web3_message").textContent="Error: Please install Metamask";}});}});};constgetContract=async(web3,address,abi_path)=>{constresponse=awaitfetch(abi_path);constdata=awaitresponse.json();constnetId=awaitweb3.eth.net.getId();contract=newweb3.eth.Contract(data,address);returncontract}asyncfunctionloadDapp(){//zokratesProvider = await zokrates.initialize()//metamaskReloadCallback()document.getElementById("web3_message").textContent="Please connect to Metamask"varawaitWeb3=asyncfunction(){web3=awaitgetWeb3()web3.eth.net.getId((err,netId)=>{if(netId==NETWORK_ID){varawaitContract=asyncfunction(){my_contract=awaitgetContract(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=_accountsif(err!=null){console.error("An error occurred:"+err)}elseif(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 Alpha";}});};awaitWeb3();}asyncfunctionconnectWallet(){awaitwindow.ethereum.request({method:"eth_requestAccounts"})accounts=awaitweb3.eth.getAccounts()onWalletConnectedCallback()}loadDapp()constonContractInitCallback=async()=>{// Now the contracts are initialized}constonWalletConnectedCallback=async()=>{// Now the account is initialized}asyncfunctiongetProof(){a11=document.getElementById("a11").valuea12=document.getElementById("a12").valuea21=document.getElementById("a21").valuea22=document.getElementById("a22").valueb11=document.getElementById("b11").valueb12=document.getElementById("b12").valueb21=document.getElementById("b21").valueb22=document.getElementById("b22").valuec11=document.getElementById("c11").valuec12=document.getElementById("c12").valuec21=document.getElementById("c21").valuec22=document.getElementById("c22").valued11=document.getElementById("d11").valued12=document.getElementById("d12").valued21=document.getElementById("d21").valued22=document.getElementById("d22").valuezokratesProvider=awaitzokrates.initialize()constresponse=awaitfetch(ZOK_PATH);constsource=awaitresponse.text();constartifacts=zokratesProvider.compile(source);const{witness,output}=zokratesProvider.computeWitness(artifacts,[a21,b11,b22,c11,c22,d21,a11,a12,a22,b12,b21,c12,c21,d11,d12,d22]);pkFile=awaitfetch(PK_PATH)pkJson=awaitpkFile.json()pk=pkJson.pkconstproof=zokratesProvider.utils.formatProof(zokratesProvider.generateProof(artifacts.program,witness,pk));returnproof}constverify=async()=>{varproof=awaitgetProof()document.getElementById("proof").textContent="Proof:"+JSON.stringify(proof);varverificationResult=awaitmy_contract.methods.verifyTx(proof[0],proof[1]).call()if(verificationResult){alert("Success!");}}constmintWithProof=async()=>{varproof=awaitgetProof()constresult=awaitmy_contract.methods.mintWithProof(proof[0],proof[1]).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)});}
💡 En este caso estamos usando elNETWORK_ID
534353 que representa a Scroll Alpha. Si no estás usando Scroll cambialo al que estés utilizando.
Ahora podemos correr la interfaz web.
lite-server
Completamos el sudoku y hacemos click en "Mint".
Una vez confirmada la transacción podemos ver el token premio en Metamask si agregamos el contrato lanzado como asset ERC20.
¡Gracias por ver este tutorial!
Sígueme en dev.to y enYoutube para todo lo relacionado al desarrollo en Blockchain en Español.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse