title | published | description | tags |
---|---|---|---|
Privacidad sin ZK ¿Es posible a puro Solidity? |
false |
// 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));
}
}
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
// 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));
}
}
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
};
}