5/5
## Understanding Panagram: A Decentralized Word Game with Zero-Knowledge Proofs Welcome to this technical exploration of Panagram, an innovative decentralized word-guessing game. Panagram distinguishes itself by leveraging Zero-Knowledge Proofs (ZKPs), a cryptographic technique that allows players to prove they know the secret word without actually revealing the word itself. Successful players are rewarded with Non-Fungible Tokens (NFTs), adding a layer of collectibility and achievement. This lesson will dissect the architecture, core components, and operational flow of Panagram. ## Panagram's Architecture: A Three-Tier System The Panagram application is built upon a robust three-component architecture, ensuring a separation of concerns and efficient operation: 1. **Frontend (React/TypeScript):** This is the user-facing layer responsible for all player interactions, proof generation, and communication with the blockchain. 2. **ZK Circuits (Noir):** The cryptographic heart of Panagram, these circuits define the logic for proving knowledge of the secret word in a zero-knowledge manner. 3. **Smart Contracts (Solidity):** Deployed on the blockchain, these contracts manage game state, verify proofs on-chain, and handle the minting of NFT rewards. Let's delve into each component. ## The User Experience: Guessing Words in Panagram Players interact with Panagram through a web interface built with React and TypeScript. The key elements of the user interface (UI) include: * **Word Puzzle:** Jumbled letters of the current secret word are displayed, challenging the player. * **Input Field:** A dedicated field where players can type their guess. * **Submit Guess Button:** Triggers the proof generation and submission process. * **NFT Collection Display:** Shows the player's earned NFTs, categorized as: * "Times Won": Awarded for being the first to guess the secret word correctly in a round. * "Times got Correct (but not won)": Awarded for guessing correctly after another player has already won the round. ## Under the Hood: Generating a Zero-Knowledge Proof When a player submits a guess, a sophisticated process unfolds on the frontend to generate a ZK proof: 1. **Input Processing:** The player's guess, entered into the UI, is captured by the frontend code (primarily within `src/components/Input.tsx`). 2. **Hashing for Privacy:** The raw guess is hashed (e.g., using `keccak256`). This hash is then reduced modulo a field prime to ensure compatibility with the ZK circuit. This hashed guess becomes a *private input* – it's known to the user but not revealed publicly. 3. **Witness Generation with NoirJS:** The frontend utilizes **NoirJS** (`@noir-lang/noir_js`) to execute a pre-compiled Noir circuit. This circuit, in its Abstract Circuit Intermediate Representation (ACIR) format (typically `panagram.json`), takes the private input (the hashed guess) and several *public inputs*. These public inputs can include the user's Ethereum address (for associating the proof with the prover) and the hash of the correct answer for the current game round. The execution of the circuit with these inputs produces a **witness**. A witness is essentially a complete set of assignments for all wires in the circuit that satisfy the circuit's constraints. 4. **Proof Generation with BB.js:** The generated witness, along with the circuit's bytecode, is then passed to **BB.js** (`@aztec/bb.js`), specifically its `UltraHonkBackend`. This backend is responsible for constructing the actual ZK proof, a compact cryptographic assurance that the user possesses a valid witness (and thus knows the secret word) without revealing the witness itself. 5. **Off-Chain Verification:** Before incurring gas costs by sending the proof to the blockchain, an off-chain verification step is performed using BB.js. This provides immediate feedback to the user and prevents the submission of invalid proofs, thereby saving on transaction fees. This entire client-side proof generation logic is primarily orchestrated within `src/utils/generateProof.ts`. ## The Heart of Privacy: Noir ZK Circuits The core privacy-preserving logic of Panagram resides in its Zero-Knowledge circuits, written in the Noir language. These circuits are typically located in a `circuits` folder, with the main logic in a file like `circuits/src/main.nr`. The Panagram circuit is designed with the following characteristics: * **Private Input:** It accepts the hash of the user's guess. This is the secret information the user wants to prove knowledge of without revealing. * **Public Inputs:** It takes several public inputs, critically: * The hash of the correct secret word for the current round. * The user's Ethereum address. This inclusion is vital to prevent proof replay attacks (where one user's valid proof is stolen and submitted by another) and to ensure that any rewards are directed to the legitimate prover. * **Assertion Logic:** The fundamental operation within the circuit is an assertion: it checks if the privately provided hashed guess matches the publicly provided hash of the correct answer. If they match, the circuit is satisfied. Once written, the Noir circuit is compiled into its ACIR and Application Binary Interface (ABI), which are stored, for instance, in `circuits/target/panagram.json`. This compiled artifact is what NoirJS uses on the frontend. ## On-Chain Trust: Solidity Smart Contracts The Panagram game's integrity and reward system are managed by smart contracts written in Solidity and deployed on an Ethereum-compatible blockchain. These are found in the `contracts` folder. Two primary contracts are involved: 1. **Verifier Contract (e.g., `Verifier.sol`):** This contract is not manually written but is automatically generated by the Barretenberg Command Line Interface (BB CLI) directly from the compiled Noir circuit. Its sole purpose is to verify ZK proofs on-chain. It contains the mathematical logic, derived from the circuit structure, to check the validity of a submitted proof. 2. **Main Game Contract (`Panagram.sol`):** This is the central smart contract orchestrating the game. Its responsibilities include: * **State Management:** Tracking crucial game data like the current round number, the hash of the correct secret word for the active round, and a record of which players have already won the current round. * **`makeGuess` Function:** This public function is the entry point for players to submit their guesses. It accepts the ZK proof (generated by the frontend) and the associated public inputs. * **On-Chain Verification:** Upon receiving a guess, the `makeGuess` function calls the `verify` function on the deployed Verifier Contract, passing it the proof and public inputs. This is the critical on-chain verification step. * **Reward Distribution:** If the Verifier Contract confirms the proof's validity: * The `Panagram.sol` contract mints ERC-1155 NFTs to the player's address. Different Token IDs within the ERC-1155 standard are used to represent different achievements. For example, Token ID 0 might be minted if the user is the first to solve the puzzle in a round (signifying "Times Won"). Token ID 1 could be minted for users who guess correctly but are not the first (signifying "Times got Correct (but not won)"). * The user's NFT balance for the corresponding Token ID is updated. * **Round Advancement:** The contract includes logic to manage game rounds, such as changing the secret word (by updating its hash) once a round is won and resetting relevant state for the new round. ## Code Deep Dive: Proof Generation and Submission Let's examine the key TypeScript code snippets that handle proof generation and submission to the smart contract. **Proof Generation in `src/utils/generateProof.ts`:** The `generateProof` function orchestrates the client-side ZK proof creation: ```typescript import { UltraHonkBackend } from "@aztec/bb.js"; import circuit from "../../circuits/target/panagram.json"; // Compiled ACIR + ABI import { Noir } from "@noir-lang/noir_js"; import { ANSWER_HASH } from "../constants"; // Public hash of the correct word for the current round export async function generateProof(guess: string, address: string, /*...other params like showLog callback */) { // 1. Initialize Noir with the compiled circuit's ACIR // 'circuit' is imported from the JSON file containing the ACIR and ABI. const noir = new Noir(circuit as CompiledCircuit); // CompiledCircuit is a type definition // 2. Initialize the Barretenberg backend (UltraHonk) with the circuit's bytecode // The circuit's bytecode is part of the compiled 'circuit' object. const honk = new UltraHonkBackend(circuit.bytecode); // 3. Prepare inputs for the circuit // These must match the inputs defined in the Noir circuit (main.nr). const inputs = { guess: guess, // The user's hashed guess (private input) address: address, // The user's Ethereum address (public input) expected_hash: ANSWER_HASH // Hash of the correct answer for the current round (public input) }; // 4. Generate the witness using NoirJS // This step executes the circuit logic with the provided inputs. // console.log("Generating witness... ⌛️"); // Example logging const witness = await noir.execute(inputs); // console.log("Generated witness... ✔️"); // 5. Generate the ZK proof using the backend and witness // The 'honk' backend takes the witness and produces the cryptographic proof. // console.log("Generating proof... ⌛️"); const { proof, publicInputs } = await honk.generateProof(witness, { keccak: true }); // Options can tailor proof generation // console.log("Generated proof... ✔️"); // 6. Perform off-chain verification // This step verifies the just-generated proof locally before any on-chain submission. // While the example shows re-generating for verification, ideally, you verify the 'proof' from step 5. // console.log("Verifying proof off-chain... ⌛️"); // For a more efficient off-chain verification, use the 'proof' object directly: // const isValid = await honk.verifyProof({ proof, publicInputs }); // The example's re-generation: const offChainProofObject = await honk.generateProof(witness); // Re-generates, less efficient const isValid = await honk.verifyProof(offChainProofObject); // console.log(`Proof is valid (off-chain): ${isValid} ✔️`); // Note on proof formatting: Older versions or specific backends might require proof byte manipulation. // The comment "no longer needed for bb: const cleanProof = proof.slice(4)" suggests such a historical requirement. // Always refer to the latest library documentation for correct proof handling. return { proof, publicInputs }; // Return the raw proof bytes and the public inputs used } ``` **Submitting the Guess in `src/components/Input.tsx` (within `handleSubmit` function):** Once the proof is generated and verified off-chain, the `handleSubmit` function in the React component takes over: 1. The user's raw guess string is retrieved from the input field. 2. This guess is then hashed client-side (e.g., `keccak256(toUtf8Bytes(guessInput))`) to produce the `guessHash` that will be used as the private input for proof generation. 3. The `generateProof` function is called: `const { proof } = await generateProof(guessHash, userAddress, ...);`. 4. The `proof` (which is typically a `Uint8Array`) is then formatted (e.g., converted to a hex string) and sent to the `makeGuess` function of the Panagram smart contract via a library like Viem or Ethers.js: ```typescript // Assuming 'writeContract' is a function from a library like Viem for smart contract interaction // and 'PANAGRAM_CONTRACT_ADDRESS' and 'panagramAbi' are defined elsewhere. // 'uint8ArrayToHex' is a utility function to convert the proof byte array to a hex string. await writeContract({ address: PANAGRAM_CONTRACT_ADDRESS, // Address of the deployed Panagram.sol contract abi: panagramAbi, // ABI of the Panagram.sol contract functionName: "makeGuess", args: [`0x${uint8ArrayToHex(proof)}`], // The proof, formatted as a hex string prefixed with '0x' // Public inputs might also be passed if the contract expects them directly // alongside the proof, depending on Verifier.sol integration. }); ``` This transaction, when mined, will trigger the on-chain verification and potential NFT minting. ## Witnessing Panagram in Action: A Typical Gameplay Scenario Consider a player attempting to guess the secret word, which happens to be "outnumber." 1. The player types "outnumber" into the input field and clicks "Submit Guess." 2. The frontend initiates the proof generation sequence. Logs might display: * "Generating witness... ⌛️" * "Generated witness... ✔️" * "Generating proof... ⌛️" * "Generated proof... ✔️" * "Verifying proof off-chain... ⌛️" * "Proof is valid (off-chain): true ✔️" 3. Since the off-chain verification is successful, the frontend prompts the player to sign a transaction using their connected wallet (e.g., MetaMask). 4. The player signs, and the transaction containing the proof is submitted to the blockchain. 5. Upon successful transaction confirmation, the Panagram smart contract verifies the proof. If valid and the player is the first winner: * An NFT (e.g., Token ID 0 for "Times Won") is minted to the player's address. * The UI updates to reflect the increased NFT balance. * The jumbled letters on the screen change, indicating a new round has begun with a new secret word. ## Key Learnings from Panagram The Panagram project offers several important insights into building advanced Web3 applications: * **Synergistic Technologies:** It effectively combines modern frontend development (React/TypeScript) with specialized ZK circuit design (Noir) and robust smart contract development (Solidity). * **Privacy through ZKPs:** Zero-Knowledge Proofs are a powerful tool for enabling privacy-preserving interactions. Users can prove specific claims (like knowing a word) without revealing the underlying sensitive data. * **User Experience and Efficiency:** Off-chain proof generation and verification are paramount. They significantly improve the user experience by providing fast feedback and reduce costs by preventing the submission of invalid proofs to the blockchain. * **Trustless On-Chain Verification:** The ultimate source of truth and trust resides in the on-chain verification performed by the smart contract. This step is essential for executing state changes, like minting NFTs, in a decentralized and verifiable manner. * **Flexible Tokenization with ERC-1155:** The ERC-1155 token standard is well-suited for applications like Panagram that require multiple types of tokens or collectibles (e.g., "Won" NFTs vs. "Correct Guess" NFTs) under a single contract. ## Essential Tools and Resources To build or understand applications like Panagram, familiarity with the following tools and libraries is beneficial: * **Noir Language & NoirJS:** * `@noir-lang/noir_js`: The JavaScript library for interacting with Noir circuits (compiling, executing, generating witnesses) from a JavaScript/TypeScript environment. * **Barretenberg & BB.js:** * `@aztec/bb.js`: The JavaScript implementation of the Barretenberg ZK proving system. Essential for generating and verifying proofs (using backends like UltraHonk) in the browser or Node.js. * **Source Code (Example):** * The concepts discussed are often demonstrated in repositories like `github.com/cyfrin/zk-panagram` (based on typical educational project structures). Reviewing such codebases provides practical understanding. By understanding these components and principles, developers can begin to explore and build their own privacy-preserving decentralized applications.
An architectural guide to Understanding Panagram: A Decentralized Word Game with Zero-Knowledge Proofs - Unpack Panagram's three-tier design, encompassing its React frontend, Noir ZK circuits, and Solidity smart contracts that manage game state and NFT rewards. This lesson details how ZKPs facilitate private word guessing through client-side proof generation and on-chain verification.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025
Duration: 6min
Duration: 1h 11min
Duration: 2h 12min
Duration: 3h 19min
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025