Building the Contracts

Get started by deploying the game contracts.

Project Setup

To get started, clone the starting template into a folder called zk-game:

git clone https://github.com/sarahschwartz/zk-game-tutorial-template.git zk-game

In the template, you will find the basic game utilities set up for you, including:

  • a backend folder which contains an API to handle the requests for proofs,
  • a frontend folder with most of the frontend application set up already,
  • and a game_lib folder, which contains a Rust library for the game mechanics.

What's missing from the template are the contracts, prover program, SSO wallet, and onchain interactions. Let's get started by adding the contracts first.

Adding Contracts

Move into the zk-game folder and create a new contracts folder using the hardhat_solidity template:

cd zk-game
npx zksync-cli create contracts --template hardhat_solidity

Once that is ready, move into the new contracts folder.

cd contracts

Once that is ready, move into the new contracts folder, and run the command below to move the GeneralPaymaster contract to the contracts/contracts folder, and delete the template files we won't need.

mv ./contracts/paymasters/GeneralPaymaster.sol ./contracts/
find ./contracts -mindepth 1 ! -name 'GeneralPaymaster.sol' -delete
rm -rf ./scripts/*
rm -rf ./test/*

We will be testing our game on a local node. To deploy the contracts to our local node, modify the defaultNetwork on line 9 inside the hardhat.config.ts to set it to anvilZKsync.

  defaultNetwork: 'anvilZKsync',

Game Contract Code

Next, create a new file for our game contract called Brickles.sol.

touch contracts/Brickles.sol

Copy and paste the full contract below into the file.

Understanding the Contract

Let's break down what's in this contract, and how it works.

PublicValuesStruct

At the top of the contract file is the PublicValuesStruct. This struct defines the public values that will be produced by the proof.

Remember: our prover program will have private inputs and public outputs. The private inputs will include the player's actions and final game score. The public outputs will include whether or not the action inputs produce the same final game score that they claimed as well as the score itself.

Storage Values

There are several persistent storage variables in our contract:

  • verifier: stores the address of the verifier contract.
  • gameProgramVKey: stores the program verification key for the prover program.
  • verifiedProofs: stores all of the verified proofs used.
  • topScores: keeps track of top 10 high scores across all players.
  • playerBestScores: stores the high score for each player.

Constructor Function

When the contract is deployed, it requires that contract address of the verifier and the gameProgramVKey in order to initialize those values. There are no methods to change these variables, so if they need to be changed, a new deployment would be required.

verifyProof Function

The only writable function available for players to call is the verifyProof function. It takes the proof and public values hashes, verifies them with the verifier contract, and if valid, saves the player's score.

This function first checks to make sure the submitted proof isn't empty and hasn't been used by another player before. Then, it calls the verifyProof function from the verifier contract. Once verified, it checks to make sure the result of the program was valid, and saves the high score and proof in persistent storage.

Read Functions

The getTopScores and getPlayerScore functions are available to get the top 10 high scores and get the highest score for a given player respectively.

Verifier Contract Code

We will also need the verifier contract in order to verify the game proofs. These contracts are provided by SP1. We will be using version 4.0.0-rc.3 of the SP1 verifier contracts.

There are three contracts we need to copy:

  1. ISP1Verifier.sol
  2. Groth16Verifier.sol
  3. SP1VerifierGroth16.sol

Add each of these contracts into your contracts/contracts folder.

Make sure to update the import path for ISP1Verifier.sol at the top of the SP1VerifierGroth16.sol contract. With each of these added, your contracts/contracts folder should look like this:

├── Brickles.sol
├── GeneralPaymaster.sol
├── Groth16Verifier.sol
├── ISP1Verifier.sol
└── SP1VerifierGroth16.sol

1 directory, 5 files

Deploying Contracts

Create three new files inside the contracts/scripts folder for the verifier, paymaster, and game contract deploy scripts.

touch scripts/deploy-verifier.ts scripts/deploy-paymaster.ts scripts/deploy-game.ts

Edit your contracts/package.json file so we can easily run the scripts later:

contracts/package.json
"deploy:game": "hardhat run ./scripts/deploy-game.ts",
"deploy:verifier": "hardhat run ./scripts/deploy-verifier.ts",
"deploy:paymaster": "hardhat run ./scripts/deploy-paymaster.ts",

Starting a Local Node

Open Docker Desktop so the Docker daemon is running in the background, and use zksync-cli to start a local in-memory node:

npx zksync-cli dev start

Then, copy of one the private keys from the pre-configured rich wallets list, and update the contracts/.env file with the private key like the example below:

contracts/.env
WALLET_PRIVATE_KEY=0x3d3cbc973389cb26f657686445bcc75662b415b656078503592ac8c1abb8810e

Deploying the Verifier Contracts

Let's start with the verifier contract. Copy and paste the deploy script:

contracts/scripts/deploy-verifier.ts
import { deployer } from 'hardhat';

async function main() {
  const artifact = await deployer.loadArtifact('SP1Verifier');
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const constructorArguments: any[] = [];
  const contract = await deployer.deploy(artifact, constructorArguments);
  console.log('DEPLOYED VERIFIER CONTRACT ADDRESS: ', await contract.getAddress());
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Next, compile and deploy the contract from inside the zk-game/contracts folder:

npm run compile
npm run deploy:verifier

Save the deployed contract address somewhere so we can use it later.

Deploying the Paymaster Contract

Next, let's add the script to deploy and fund the paymaster contract.

contracts/scripts/deploy-paymaster.ts
import { deployer, ethers } from 'hardhat';

async function main() {
  const artifact = await deployer.loadArtifact('GeneralPaymaster');
  const [signer] = await ethers.getSigners();
  const constructorArguments = [signer.address];
  const contract = await deployer.deploy(artifact, constructorArguments);
  const address = await contract.getAddress();
  console.log('DEPLOYED PAYMASTER CONTRACT ADDRESS: ', address);

  // Fund the paymaster contract
  const tx = await signer.sendTransaction({
    to: address,
    value: ethers.parseEther('0.5'),
  });

  await tx.wait();
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Then, run the script, and save this contract address as well.

npm run deploy:paymaster

Deploying the Game Contract

Finally, let's set up the deploy script for the game contract.

contracts/scripts/deploy-game.ts
import { deployer } from 'hardhat';

async function main() {
  // sp1 verifier contract v4.0.0-rc.3
  const VERIFIER_CONTRACT_ADDRESS = process.env.VERIFIER_CONTRACT_ADDRESS ?? '0x...';
  // generated in our sp1 script
  const PROGRAM_VERIFICATION_KEY = process.env.PROGRAM_VERIFICATION_KEY ?? '0x...';

  const artifact = await deployer.loadArtifact('Brickles');
  const constructorArguments = [VERIFIER_CONTRACT_ADDRESS, PROGRAM_VERIFICATION_KEY];
  const contract = await deployer.deploy(artifact, constructorArguments);
  console.log('DEPLOYED GAME CONTRACT ADDRESS: ', await contract.getAddress());
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

In our deploy script for the game, we need both the verifier contract address and the verification key for our prover program.

Update the VERIFIER_CONTRACT_ADDRESS variable with the deployed address from earlier. However, we need to wait to deploy this contract until we have the PROGRAM_VERIFICATION_KEY. We will do that in the next section.


Made with ❤️ by the ZKsync Community