Imagine you're on a treasure hunt with a map that leads you to a hidden box. The map doesn't tell you the exact location, but it gives you clues about where to dig. You follow the instructions, carefully measuring distances and angles, until finally, you find the treasure.
But here's the twist: no one ever sees the map or knows where you dug. You simply prove, using a special technique, that you followed the correct steps and found the treasure without revealing any of the secret details. This is the essence of zero-knowledge proofs (zk-proofs) in a nutshell.
They allow you to prove you possess certain information without actually sharing that information itself. Just like the treasure hunter demonstrating they reached the right spot without showing the map or the answer to the riddle.

In this article we will construct a zero-knowledge (zk) treasure hunt game on Scroll, utilizing Noir's domain-specific language. Players navigate a grid map guided by riddles, attempting to pinpoint the hidden treasure's location.
A walkthrough of the completed treasure hunt application.
We'll also leverage Noir Codegen to generate a Solidity verifier contract, forming the core of our interactive treasure hunt experience.
The Treasure Hunt Smart Contract is deployed at https://sepolia.scrollscan.dev/address/0xfa36ffd97f50c8dE61242c7D42EcCaCfab725d7F (opens in a new tab)
There is also a demo game available live at https://treasure-hunt-scroll.vercel.app/ (opens in a new tab) where you can play the simple implementation of the above contracts.
You can access the entire source code for the tutorial at:
github.com
envoy1084/treasure-hunt-scroll
Source code for the ZK Treasure Hunt application.
(opens in a new tab)- Noir: Ensure you have Noir installed and ready to use. Follow the official installation guide (opens in a new tab) to get started.
- Forge: Familiarity with Forge is essential. Make sure you have it installed (opens in a new tab) and configured correctly.
- Having a basic understanding of Solidity will be beneficial as we create our treasure hunt smart contract.
Let's create a directory named treasure-hunt-scroll and initialize a new Noir project within it. Run the following command in your terminal:
This generates a circuits directory within your project. Inside the src folder, create a file named treasure.nr (opens in a new tab). This will be our central script for defining the game's logic.
Our Circuit will take some public as well as private inputs such as
Public Inputs -
Array of Riddles, each riddle contains the following:
question: A unique hash representing the riddle itself.answerHash: A combined hash of the answer and its corresponding location.position: A hash of the hidden treasure's location.
there will be two private inputs to the circuit
Private Inputs -
The circuit also utilizes two private inputs:
position: An array containing the x and y coordinates of the treasure's location.answerHash: The keccak256 hash of the user's provided answer.
First, we define a Riddle struct to encapsulate these riddle components:
Next, we create a public function named getRiddleForPos. This function takes the user's guessed position and the array of riddles as input and returns the riddle associated with that specific position.
This function iterates through the riddles and checks if the position hash of any riddle matches the user's guess. If a match is found, that riddle is returned.
Now that we have the relevant riddle, we can verify if the user's answer is correct. The solveRiddle function takes the retrieved riddle and the user's answer hash as input and returns true if the answer hash matches the riddle's answerHash.
This function simply compares the two hashes, and if they are equal, the function returns true.
To retrieve the solution hash, which combines the answer and location, we create the getSolutionHash function. This function takes the answer as a byte array and the position as [x,y] coordinates, returning the combined hash
This function performs the following steps:
- Converts the answer byte array to a Field array using the
u8ArrToFieldArrutility function (explained later). - Computes individual hashes for the answer and position using the
pedersen_hashfunction. - Combines the individual hashes and calculates the final solution hash using
pedersen_hash.
The u8ArrToFieldArr function serves as a helper, this function iterates through the byte array and converts each element to a Field element, suitable for further mathematical operations within the circuit.
With these tools in hand, we introduce the constructRiddle function. This function takes the riddle's question, position, and actual answer hash as input and returns the complete riddle structure.
This function performs the following tasks:
- Hashes the question using
u8ArrToFieldArrandpedersen_hash. - Employs
getSolutionHashto calculate the combined answer and position hash. - Hashes the position using
pedersen_hash. - Creates the
Riddlestruct with the computed hashes and returns it.
Now that we have our treasure hunt logic solidified, let's dive into the main.nr (opens in a new tab) file within the src directory. Here, we'll pass the core logic.
First, we import the treasure module containing our riddle-related code and the std module for standard functionalities:
The main function serves as the entrypoint of the treasure hunt experience. It takes three arguments:
-
riddles: A public array containing all riddles for the game. -
position: The private array representing the treasure's location (x, y coordinates). -
answerHash: The private hash of the user's provided answer. -
Verifying the Position:
- The function calculates the
posHashfrom the givenposition. - It then retrieves the riddle associated with that position using the
getRiddleForPosfunction from thetreasuremodule. - An assertion ensures that a riddle actually exists for the chosen position, preventing invalid attempts.
- The function calculates the
-
Solving the Riddle:
- The
solveRiddlefunction is called, passing the retrieved riddle and the calculatedanswerHash(combined hash of answer and position). - Another assertion verifies if the answer hash matches the riddle's solution, confirming if the user solved it correctly.
- The
But how do we get the riddles for the circuit? Remember our constructRiddle function? It's time to put it to work!
We create a getRiddles function that returns the pre-filled riddle array. Each riddle is constructed using constructRiddle, providing the question, position, and solution hash (pre-computed using keccak256).
now lest write a test for out code, first we will get all the riddles using the getRiddles function and using the position of the treasure and the answer we will create the private inputs for the circuits and then we will call the main function with the parameters.
Now that we've crafted the zk-circuits, let's test them to ensure everything functions as intended. Use the following commands in your terminal:
If all goes well, you'll see a clean console, signaling success!
Congratulations, you've built the foundational zk-circuits for your treasure hunt!
Next, we'll translate this circuit logic into a Solidity verifier contract. This contract interacts with users and verifies their proofs without revealing sensitive information. To generate the Solidity code, run the following command:
This creates a new file called treasure-hunt-scroll/circuits/contract/circuits/plonk_vk.sol. This file contains the Solidity code for your verifier contract, ready to be integrated into your application.
With the circuits built and the verifier contract generated, we're well-equipped to move forward in the next part of the tutorial.
Now, let's craft the Solidity contract that brings your zk-powered treasure hunt to life. Navigate to the root of your project and initialize a new Forge project
Integrating the Verifier:
Copy the circuits directory from your treasure-hunt-scroll/circuits/contracts folder to contracts/src. This directory contains the generated Solidity verifier code.
Next, create a new file named TreasureHunt.sol within the src directory. This file will be the contract logic.
We will start by adding the license identifier and compiler version:
Import the UltraVerifier contract from the generated verifier code:
Then we will define the TreasureHunt contract and declare state variables, These variables manage:
verifier: Stores the address of the generated verifier contract.COST_PER_DIG: Defines the cost in tokens for each attempt to find the treasure.REWARD_PER_DIG: Specifies the reward in tokens for successfully solving a riddle.hasStarted: Maps addresses to booleans, indicating if they've started the game.balances: Maps addresses to their in-game token balances.
then we will implement events to log player actions, these events will be emitted when players start the game and attempt to dig for the treasure.
and we will also set the verifier address in the constructor.
To start the game we will define functions for players to begin or restart their treasure hunt journey.
These functions:
- Ensure players haven't already started before initiating the game.
- Reset player state (started and balance) for a fresh start.
- Emit events to signal the game start or restart.
Now it's time to implement the heart of the game: the dig function. This function allows players to attempt to find the treasure by submitting a zk-proof. Here's a breakdown of its steps:
Preparing the Public Inputs:
- Constructing the Public Input Array: The function creates a
bytes32array namedpublicInputswith nine elements. These elements represent the public inputs for the circuit, which are essentially the three riddles. Each riddle is represented by a combination of three hashes: question, answer hash, and position hash. - Hardcoded Riddles: Currently, the function has hardcoded values for the public inputs. These values represent pre-defined riddles for testing purposes. In a real-world scenario, you'd dynamically generate or store these riddles elsewhere.
Verifying the Proof:
- Calling the Verifier: The function calls the
verifyfunction on theverifiercontract. This function takes the user's submitted proof (_proof) and the constructed public inputs (publicInputs) as arguments. - Proof Evaluation: The
verifyfunction within the verifier contract evaluates the zk-proof against the circuit and the public inputs. If the proof is valid, it means the user has successfully solved the riddles without revealing the answers or location.
Rewarding Success:
-
Balance Update: Based on the proof verification result (
verified):- If the proof is valid, the user's balance increases by the
REWARD_PER_DIGminus theCOST_PER_DIG. - If the proof is invalid, the user's balance decreases by the
COST_PER_DIG.
- If the proof is valid, the user's balance increases by the
-
Emitting Event: The
Digevent is emitted, logging the player's attempt and outcome.
Retrieving Public Inputs for Testing:
To obtain the correct riddle hashes, follow these steps:
-
Add the following code in the test circuit function:
-
Run the following command to print the riddles in the test function.
-
Extract the question, answer hash, and position hash for each riddle.
-
Replace the placeholder values in the
publicInputsarray with the actual extracted hashes.
Now it's time to deploy your treasure hunt contract to the Scroll network! Here's a breakdown of the process:
Setting Up Environment Variables:
- Private Key: Store your private key securely in an environment variable named
PRIVATE_KEY. - Scroll RPC URL: Define the Scroll Sepolia RPC URL in the
SCROLL_RPC_URLenvironment variable. - Scroll Etherscan API Key: Obtain an API key for Scroll Etherscan and store it in
SCROLL_ETHERSCAN_API_KEY.
Configuring Foundry:
Update your foundry.toml file with the following entries:
Next create a new file named Deploy.s.sol in the contracts/script directory.
We will start by importing necessary modules
Then we will implement the run function, this function deploys the UltraVerifier contract and then the TreasureHunt contract, passing the verifier address as an argument.
-
Load the environment variables
-
Run the deployment script:
-
Verify the contract on Scroll Etherscan
- Replace
0xfa36ffd97f50c8dE61242c7D42EcCaCfab725d7Fwith the actual deployed address.
Now that your contract is deployed on Scroll, let's put it to the test and see if adventurers can claim their treasure!
Generating Proofs:
- Head to the
circuitsfolder and open theProver.tomlfile. - Paste the following details into the file, replacing placeholders with actual values from your test case
- Run the following command in your terminal:
This will generate a proof file called circuits.proof in the circuits/proofs directory.
Calling Functions
-
Open your deployed contract on Scroll Etherscan.
-
Execute the "startGame" function and verify that your balance starts at 1000 (assuming your starting balance).

-
Copy the value from the
circuits.proofsfile, ensuring it starts with0xto represent bytes.
-
Paste the copied proof into the "dig" function input on the contract page.
-
Click "Dig."
If the execution completes successfully, check your balance again. It should now be 1050, signifying that you've successfully proven you know the treasure location and riddle answers without revealing the actual values!

This demonstrates how users can interact with your treasure hunt contract, using zk-proofs to verify their knowledge without exposing sensitive information. Remember to replace the placeholders with your actual values for a functional test.
Congratulations! You've built and deployed a zk-powered treasure hunt, ready to challenge and reward adventurous players on the Scroll network.
Dynamic Riddles: Instead of hardcoded questions, create a system that generates riddles dynamically. This could involve randomizing elements, incorporating external data feeds, or even user-submitted content.
Security Enhancements: Introduce features like EdDSA signatures and commitment schemes. Signatures can verify the authenticity of proofs, while commitments lock in values before revealing them, preventing double spending or manipulation.
There is also a demo game available live at https://treasure-hunt-scroll.vercel.app/ (opens in a new tab) where you can play the simple implementation of the above contracts.


