Skip to content

Instantly share code, notes, and snippets.

@Turupawn
Created April 20, 2025 17:44
Show Gist options
  • Save Turupawn/640decece5304f78ee97efebd7e53faa to your computer and use it in GitHub Desktop.
Save Turupawn/640decece5304f78ee97efebd7e53faa to your computer and use it in GitHub Desktop.
valiste verga
title published description tags
Privacidad sin ZK ¿Es posible a puro Solidity?
false

Ahorcado sin ZK

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

// Contrato demostración de equemas de commit-reveal
contract SimpleHangman {
    // El commitment de la palabra por adivinar, este se calcula con keccak256(word)
    // Aunque esto esconde una palabra, no provee ninguna garantía que es una palabra válida
    bytes32 public wordHash;
    // El address del ganador, quien adivinó la palabra
    address public winner;

    // Al iniciar el juego, almacenamos un commitment a la palabra con la que jugaremos
    constructor(bytes32 _wordHash) payable {
        wordHash = _wordHash;
    }

    // Intenta adivinar la palabra oculta
    // Esto es sujeto a un ataque de frontrunn, el atacante puede ver la palabra en la mempool y pagar mas gas para ganar
    function playWord(string memory word) public {
        require(winner != address(0), "Game already played");
        require(hashFunction(word) == wordHash, "Invalid word");
        winner = msg.sender;
    }

    // Nos ayuda hashear la palabra, puedes usarla para calcular el commitment antes de lanzar el contrato
    function hashFunction(string memory value) public pure returns(bytes32)
    {
        return keccak256(abi.encodePacked(value));
    }
}

Versión ZK

nargo new zk_hangman
cd zk_hangman

src/main.nr

use dep::std;

// Circuito de ahorcado que evita frontrunn y verifica que la palabra sea valida
fn main(
    word: [Field; 10], // Palabra a adivinar, un maximo de 10 caracteres
    word_length: pub Field, // Longitud de la palabra, cantidad de caracteres
    winner: pub Field // Wallet del ganador, debe estar integrado en los parametros de la transacción para evitar frontrunn
) -> pub Field {

    // Convierte la palabra a bytes para ser compatible con la implementación de la librería de keccak256 que usaremos
    // Además, verificamos que la palabra no contenga caracteres no alfabéticos
    let mut word_bytes = [0; 10];
    for i in 0..10 {
        if i < word_length as u8 {
            let current_char = word[i] as u8;
            let is_uppercase = (current_char >= 65) & (current_char <= 90);
            let is_lowercase = (current_char >= 97) & (current_char <= 122);
            assert(is_uppercase | is_lowercase);
        }
        word_bytes[i] = word[i] as u8;
    }

    // Obtenemos el hash de la palabra
    let hash_bytes = std::hash::keccak256(word_bytes, 10);

    // Convertimos el hash a un numero de 256 bits para ahorrar el tamaño de la prueba
    let mut computed_hash = 0 as Field;
    for i in 0..30 {
        computed_hash = computed_hash * 256 + (hash_bytes[i] as Field);
    }

    println(computed_hash);
    println(hash_bytes);
    // Devolvemos el hash de la palabra, recuerda que los valores de retorno son publicos y pueden ser vistos desde el contrato
    computed_hash
}
nargo execute zk_hangman
bb write_vk -b ./target/zk_hangman.json -o ./target --oracle_hash keccak
bb write_solidity_verifier -k ./target/vk -o Verifier.sol

Prover.toml

winner = "642224319606688042440426533877338891159607336304"
word = ["72", "101", "108", "108", "111", "0", "0", "0", "0", "0"]
word_length = "5"
bb prove -b ./target/zk_hangman.json -w ./target/zk_hangman.gz -o ./target --oracle_hash keccak

serialize_proof.sh

PROOF_HEX=$(cat ./target/proof | od -An -v -t x1 | tr -d $' \n' | sed 's/^.\{8\}//')

NUM_PUBLIC_INPUTS=3
HEX_PUBLIC_INPUTS=${PROOF_HEX:0:$((32 * $NUM_PUBLIC_INPUTS * 2))}
SPLIT_HEX_PUBLIC_INPUTS=$(sed -e 's/.\{64\}/0x&,/g' <<<$HEX_PUBLIC_INPUTS)

PROOF_WITHOUT_PUBLIC_INPUTS="${PROOF_HEX:$((NUM_PUBLIC_INPUTS * 32 * 2))}"

echo 0x$PROOF_WITHOUT_PUBLIC_INPUTS
echo [$SPLIT_HEX_PUBLIC_INPUTS]
sh serialize_proof.sh

Contratos

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

// Interfaz de contrato verificador ZK
interface IVerifier {
    function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool);
}

// Contrato demostración de equemas de commit-reveal
contract SimpleHangman {
    // El commitment de la palablra por adivinar, este se calcula con keccak256(word)
    bytes32 public wordHash;
    // La cantidad de letras en la palabra secreta
    uint public wordLength;
    // El address de quien adivinó la palablra
    address public winner;
    // Contrato verificador de las pruebas ZK
    IVerifier verifier;

    constructor(address verifierAddress) {
        verifier = IVerifier(verifierAddress);
    }

    // Al iniciar el juego, almacenamos un commitment a la palabra con la que jugaremos
    function init(bytes calldata _proof, bytes32[] calldata _publicInputs) public {
        require(verifier.verify( _proof, _publicInputs), "Invalid proof");
        wordLength = uint(_publicInputs[0]);
        wordHash = _publicInputs[2];
    }

    // Intenta adivinar la palabra oculta
    function playWord(bytes calldata _proof, bytes32[] calldata _publicInputs) public {
        require(wordLength > 0, "Game hasn't been initialized");
        require(verifier.verify( _proof, _publicInputs), "Invalid proof");
        require(winner == address(0), "Game already played");
        bytes32 _wordHash = _publicInputs[2];
        require(_wordHash == wordHash, "Invalid word");
        winner = address(uint160(uint256(_publicInputs[1])));
    }

    // Nos ayuda hashear la palabra, puedes usarla para calcular el commitment antes de lanzar el contrato
    function hashFunction(string memory value) public pure returns(bytes32)
    {
        return keccak256(abi.encodePacked(value));
    }
}

Frontend

package.json

{
  "dependencies": {
    "@aztec/bb.js": "0.82.2",
    "@noir-lang/noir_js": "1.0.0-beta.3",
    "@noir-lang/noir_wasm": "1.0.0-beta.3"
  }
}

vite.config.js

export default { optimizeDeps: { esbuildOptions: { target: "esnext" } } };

index.js

import { loadDapp, submitAdminProof, submitPlayerProof } from './web3_stuff.js';
import { generateProof, show } from './zk_stuff.js';

// Initialize dapp
loadDapp();

document.getElementById("admin_submit").addEventListener("click", async () => {
  const word = document.getElementById("admin_word").value;
  const { proofBytes, publicInputs, rawProof } = await generateProof(
    word, 
    "0x0000000000000000000000000000000000000000"
  );
  
  await submitAdminProof(proofBytes, publicInputs);
  show("results", rawProof);
});

document.getElementById("player_submit").addEventListener("click", async () => {
  const word = document.getElementById("player_word").value;
  const winnerAddress = document.getElementById("winner-address").value;
  const { proofBytes, publicInputs, rawProof } = await generateProof(
    word,
    winnerAddress
  );
  
  await submitPlayerProof(proofBytes, publicInputs);
  show("results", rawProof);
});

web3_stuff.js

const NETWORK_ID = 17000 // Holesky
const CONTRACT_ADDRESS = "0xfb89Fb2a693e71B237cE2E6A4CC2EEbFb59034c9"

// Define ABI directly instead of loading from file
const CONTRACT_ABI = [
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "_proof",
        "type": "bytes"
      },
      {
        "internalType": "bytes32[]",
        "name": "_publicInputs",
        "type": "bytes32[]"
      }
    ],
    "name": "init",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "_proof",
        "type": "bytes"
      },
      {
        "internalType": "bytes32[]",
        "name": "_publicInputs",
        "type": "bytes32[]"
      }
    ],
    "name": "playWord",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
];

let web3;
let accounts;
let contract;
let isAdmin = false;

function metamaskReloadCallback() {
  window.ethereum.on('accountsChanged', () => {
    window.location.reload();
  });
  window.ethereum.on('chainChanged', () => {
    window.location.reload();
  });
}

const getWeb3 = async () => {
  if (!window.ethereum) {
    throw new Error("Please install MetaMask");
  }
  return new Web3(window.ethereum);
};

const getContract = async (web3) => {
  return new web3.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS);
};

async function loadDapp() {
  try {
    metamaskReloadCallback();
    web3 = await getWeb3();
    
    const netId = await web3.eth.net.getId();
    if (netId !== NETWORK_ID) {
      document.getElementById("web3_message").textContent = "Please connect to Holesky network";
      return;
    }

    contract = await getContract(web3);
    
    accounts = await web3.eth.getAccounts();
    if (accounts.length > 0) {
      onWalletConnected();
    } else {
      document.getElementById("web3_message").textContent = "Please connect wallet";
      document.getElementById("connect_button").style.display = "block";
      document.getElementById("connected_section").style.display = "none";
    }
  } catch (error) {
    console.error("Error loading dapp:", error);
    document.getElementById("web3_message").textContent = error.message;
  }
}

async function connectWallet() {
  try {
    accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
    onWalletConnected();
  } catch (error) {
    console.error("Error connecting wallet:", error);
  }
}

function onWalletConnected() {
  document.getElementById("connect_button").style.display = "none";
  document.getElementById("web3_message").textContent = "Connected!";
  document.getElementById("wallet_address").textContent = `Wallet: ${accounts[0]}`;
  document.getElementById("connected_section").style.display = "block";
  document.getElementById("forms").style.display = "block";
}

// Contract interaction functions
async function submitAdminProof(proofBytes, publicInputs) {
  console.log(proofBytes);
  console.log(publicInputs);
  try {
    await contract.methods.init(proofBytes, publicInputs)
      .send({ from: accounts[0] })
      .on('transactionHash', (hash) => {
        document.getElementById("web3_message").textContent = "Transaction pending...";
      })
      .on('receipt', (receipt) => {
        document.getElementById("web3_message").textContent = "Success!";
      });
  } catch (error) {
    console.error("Error submitting admin proof:", error);
    document.getElementById("web3_message").textContent = "Transaction failed";
  }
}

async function submitPlayerProof(proofBytes, publicInputs) {
  try {
    await contract.methods.playWord(proofBytes, publicInputs)
      .send({ from: accounts[0] })
      .on('transactionHash', (hash) => {
        document.getElementById("web3_message").textContent = "Transaction pending...";
      })
      .on('receipt', (receipt) => {
        document.getElementById("web3_message").textContent = "Success!";
      });
  } catch (error) {
    console.error("Error submitting player proof:", error);
    document.getElementById("web3_message").textContent = "Transaction failed";
  }
}

export { loadDapp, connectWallet, submitAdminProof, submitPlayerProof };

zk_stuff.js

import { UltraHonkBackend } from '@aztec/bb.js';
import { Noir } from '@noir-lang/noir_js';
import circuit from './circuit/target/zk_hangman.json';

import initNoirC from "@noir-lang/noirc_abi";
import initACVM from "@noir-lang/acvm_js";
import acvm from "@noir-lang/acvm_js/web/acvm_js_bg.wasm?url";
import noirc from "@noir-lang/noirc_abi/web/noirc_abi_wasm_bg.wasm?url";

// Initialize Noir
await Promise.all([initACVM(fetch(acvm)), initNoirC(fetch(noirc))]);

export const show = (id, content) => {
  const container = document.getElementById(id);
  container.appendChild(document.createTextNode(content));
  container.appendChild(document.createElement("br"));
};

export async function generateProof(word, winnerAddress) {
  // Initialize noir with precompiled circuit
  const noir = new Noir(circuit);
  const backend = new UltraHonkBackend(circuit.bytecode);
  
  // Convert word to array
  const wordArray = Array.from(word)
    .map(char => char.charCodeAt(0).toString())
    .concat(Array(10 - word.length).fill("0"));
  
  show("logs", "Generating witness... ⏳");
  const { witness } = await noir.execute({ 
    word: wordArray,
    word_length: word.length,
    winner: winnerAddress
  });
  show("logs", "Generated witness... ✅");
  
  show("logs", "Generating proof... ⏳");
  const proof = await backend.generateProof(witness, { keccak: true });
  show("logs", "Generated proof... ✅");

  show('logs', 'Verifying proof... ⌛');
  const isValid = await backend.verifyProof(proof, { keccak: true });
  show("logs", `Proof is ${isValid ? "valid" : "invalid"}... ✅`);
  
  const proofBytes = '0x' + Array.from(Object.values(proof.proof))
    .map(n => n.toString(16).padStart(2, '0'))
    .join('');
  
  return {
    proofBytes,
    publicInputs: proof.publicInputs,
    rawProof: proof.proof
  };
} 

Playing th zk hangman

ZK hangman result

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment