import Aside from "../../../../../components/Aside.astro" import ClickToZoom from "../../../../../components/ClickToZoom.astro" import ToggleElement from "../../../../../components/ToggleElement.astro" import paymentDApp from "../../../../../assets/images/developers/stablecoin-payments-tutorial/payment-dapp.png" import nftRecipt from "../../../../../assets/images/developers/stablecoin-payments-tutorial/nft-recipt.png"
123
# install noir
curl -L https://raw.githubusercontent.com/noir-lang/noirup/refs/heads/main/install | bash
noirup
# install barremberg
curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/master/barretenberg/bbup/install | bash
bbup
nargo new circuits --name hello_world
cd circuits
nargo build
bb write_vk -b ./target/hello_world.json -o ./target --oracle_hash keccak
bb write_solidity_verifier -k ./target/vk -o Verifier.sol
Deploy the Verifier.sol and pass it as param to
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
// ZK verifier interface
interface IVerifier {
function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool);
}
// Custom logic contract for ZK verifier demo
contract ZKHelloWorld {
uint public publicInput;
IVerifier verifier;
// Pass the address of the Noir verifier contract on the constructor
constructor(address verifierAddress) {
verifier = IVerifier(verifierAddress);
}
// Proof verifier with custom logic
function sendProof(bytes calldata _proof, bytes32[] calldata _publicInputs) public {
require(verifier.verify( _proof, _publicInputs), "Invalid proof");
publicInput = uint(_publicInputs[0]);
}
}
cd ..
curl -fsSL https://bun.sh/install | bash
bun i @noir-lang/[email protected] @noir-lang/[email protected] @aztec/[email protected]
vite.config.js
export default { optimizeDeps: { esbuildOptions: { target: "esnext" } } };
index.html
<!DOCTYPE html>
<head>
<style>
.outer {
display: flex;
justify-content: space-between;
width: 100%;
}
.inner {
width: 45%;
border: 1px solid black;
padding: 10px;
word-wrap: break-word;
}
#connected_section, #proof_section {
display: none;
}
</style>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js"></script>
</head>
<body>
<script type="module" src="/index.js"></script>
<h1>ZK Proof Demo</h1>
<div id="web3_message"></div>
<button id="connect_button" onclick="connectWallet()" style="display: none;">Connect Wallet</button>
<div id="connected_section">
<div id="wallet_address"></div>
<div id="proof_section">
<h3>Generate and Submit Proof</h3>
<div>
<label for="private_input">Private Input (x):</label>
<input type="number" id="private_input" placeholder="Enter private number">
</div>
<div>
<label for="public_input">Public Input (y):</label>
<input type="number" id="public_input" placeholder="Enter public number">
</div>
<button id="submit_proof">Submit Proof</button>
</div>
</div>
<div class="outer">
<div id="logs" class="inner"><h2>Proof Generation Logs</h2></div>
<div id="results" class="inner"><h2>Verification Results</h2></div>
</div>
</body>
</html>
index.js
import { loadDapp, submitProof } from './web3_stuff.js';
import { generateProof, show } from './zk_stuff.js';
// Initialize web3
loadDapp();
// Event listener for proof submission
document.getElementById("submit_proof").addEventListener("click", async () => {
const privateInput = document.getElementById("private_input").value;
const publicInput = document.getElementById("public_input").value;
if (!privateInput || !publicInput) {
show("results", "Please enter both private and public inputs");
return;
}
try {
const { proofBytes, publicInputs } = await generateProof(privateInput, publicInput);
await submitProof(proofBytes, publicInputs);
show("results", "Proof submitted and verified successfully!");
} catch (error) {
show("results", "Error: " + error.message);
}
});
zk_stuff.js
import { UltraHonkBackend } from '@aztec/bb.js';
import { Noir } from '@noir-lang/noir_js';
import circuit from './circuit/target/hello_world.json';
// Initialize Noir and backend
const noir = new Noir(circuit);
const backend = new UltraHonkBackend(circuit.bytecode);
export const show = (id, content) => {
const container = document.getElementById(id);
container.appendChild(document.createTextNode(content));
container.appendChild(document.createElement("br"));
};
export async function generateProof(privateInput, publicInput) {
show("logs", "Generating witness... ⏳");
const { witness } = await noir.execute({
x: privateInput,
y: publicInput
});
show("logs", "Generated witness... ✅");
show("logs", "Generating proof... ⏳");
const proof = await backend.generateProof(witness, { keccak: true });
show("logs", "Generated proof... ✅");
// Verify the proof before sending to contract
show('logs', 'Verifying proof... ⌛');
const isValid = await backend.verifyProof(proof, { keccak: true });
show("logs", `Proof is ${isValid ? "valid" : "invalid"}... ✅`);
if (!isValid) {
throw new Error("Generated proof is invalid");
}
const proofBytes = '0x' + Array.from(Object.values(proof.proof))
.map(n => n.toString(16).padStart(2, '0'))
.join('');
return {
proofBytes,
publicInputs: proof.publicInputs
};
}
web3_stuff.js
const NETWORK_ID = 534352; // Scroll Mainnet
const CONTRACT_ADDRESS = "YOUR_CONTRACT_ADDRESS";
const CONTRACT_ABI = [
{
inputs: [
{
internalType: "bytes",
name: "_proof",
type: "bytes"
},
{
internalType: "bytes32[]",
name: "_publicInputs",
type: "bytes32[]"
}
],
name: "sendProof",
outputs: [],
stateMutability: "nonpayable",
type: "function"
}
];
let web3;
let accounts;
let contract;
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 Scroll Mainnet";
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("proof_section").style.display = "block";
}
async function submitProof(proofBytes, publicInputs) {
console.log(proofBytes);
console.log(publicInputs);
try {
await contract.methods.sendProof(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 proof:", error);
document.getElementById("web3_message").textContent = "Transaction failed";
}
}
export { loadDapp, connectWallet, submitProof };
bunx vite