Skip to content

Commit

Permalink
Added smart contract interaction on player submitting commitments (#14)
Browse files Browse the repository at this point in the history
* wip

* Added test for submit-rangecheck
  • Loading branch information
jimmychu0807 authored Sep 10, 2024
1 parent c55f8e0 commit 984556b
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 82 deletions.
73 changes: 32 additions & 41 deletions apps/contracts/contracts/GuessingGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ pragma solidity ^0.8.23;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IGuessingGame} from "./interfaces/IGuessingGame.sol";
import {ISubmitRangeCheckVerifier} from "./interfaces/ISubmitRangeCheckVerifier.sol";
import {MIN_NUM, MAX_NUM, ROUND_TO_WIN} from "./base/Constants.sol";

contract GuessingGame is IGuessingGame, Ownable {
ISubmitRangeCheckVerifier public submitRangeCheckVerifier;

// Storing all the game info. Refer to the interface to see the game struct
Game[] public games;
uint32 public nextGameId = 0;

// Constructor
constructor() Ownable(msg.sender) {
// @param srAddr: submit-rangecheck verifier address
constructor(ISubmitRangeCheckVerifier srAddr) Ownable(msg.sender) {
// Initialization happens here
submitRangeCheckVerifier = srAddr;
}

// Modifiers declaration
Expand Down Expand Up @@ -40,15 +46,15 @@ contract GuessingGame is IGuessingGame, Ownable {
}
}
if (!found) {
revert GuessingGame__SenderNotOneOfPlayers();
revert GuessingGame__NotOneOfPlayers();
}
_;
}

modifier gameStateEq(uint32 gameId, GameState gs) {
Game storage game = games[gameId];
if (game.state != gs) {
revert GuessingGame__UnexpectedGameState(game.state);
revert GuessingGame__UnexpectedGameState(gs, game.state);
}
_;
}
Expand All @@ -57,14 +63,7 @@ contract GuessingGame is IGuessingGame, Ownable {
Game storage game = games[gameId];
address host = game.players[0];
if (host != msg.sender) {
revert GuessingGame__SenderIsNotGameHost();
}
_;
}

modifier BidInRange(uint8 bid) {
if (bid < MIN_NUM || bid > MAX_NUM) {
revert GuessingGame__BidOutOfRange(msg.sender, bid);
revert GuessingGame__NotGameHost(gameId, msg.sender);
}
_;
}
Expand All @@ -89,27 +88,19 @@ contract GuessingGame is IGuessingGame, Ownable {
});
}

function getGameHost(uint32 gameId) public view validGameId(gameId) returns (address) {
function getPlayerCommitment(
uint32 gameId,
uint8 round,
address player
) public view validGameId(gameId) returns (Bid memory) {
Game storage game = games[gameId];
return game.players[0];
}

/**
* Helpers functions
**/
return game.bids[round][player];
}

function _verifyBidProof(
bytes32 proof,
uint8 bid,
uint256 nullifier
) internal pure returns (bool) {
/**
* TODO: verify proof
**/
proof;
bid;
nullifier;
return true;
function getGameHost(uint32 gameId) public view validGameId(gameId) returns (address) {
Game storage game = games[gameId];
return game.players[0];
}

function _updateGameState(
Expand Down Expand Up @@ -195,8 +186,8 @@ contract GuessingGame is IGuessingGame, Ownable {

function submitCommitment(
uint32 gameId,
bytes32 bid_null_commitment,
bytes32 null_commitment
uint256[24] calldata proof,
uint256[2] calldata pubSignals
)
external
override
Expand All @@ -207,14 +198,20 @@ contract GuessingGame is IGuessingGame, Ownable {
// each player submit a bid. The last player that submit a bid will change the game state
Game storage game = games[gameId];
uint8 round = game.currentRound;
game.bids[round][msg.sender] = Bid(bid_null_commitment, null_commitment);

// Verify the computation and proof
if (!submitRangeCheckVerifier.verifyProof(proof, pubSignals)) {
revert GuessingGame__InvalidSubmitRangeCheckProof(gameId, round, msg.sender);
}

game.bids[round][msg.sender] = Bid(pubSignals[0], pubSignals[1]);
emit BidSubmitted(gameId, round, msg.sender);

// If all players have submitted bid, update game state
bool notYetBid = false;
for (uint i = 0; i < game.players.length; ++i) {
address p = game.players[i];
if (game.bids[round][p].bid_null_commitment == bytes32(0)) {
if (game.bids[round][p].nullifier == 0) {
notYetBid = true;
break;
}
Expand All @@ -225,26 +222,20 @@ contract GuessingGame is IGuessingGame, Ownable {
}
}

function revealCommitment(
function openCommitment(
uint32 gameId,
bytes32 proof,
uint8 bid,
uint256 nullifier
uint16 bid
)
external
override
validGameId(gameId)
oneOfPlayers(gameId)
gameStateEq(gameId, GameState.RoundReveal)
BidInRange(bid)
{
Game storage game = games[gameId];

// each player reveal a bid. The last player that reveal a bid will change the game state
bool proofVerified = _verifyBidProof(proof, bid, nullifier);
if (!proofVerified) {
revert GuessingGame__BidProofRejected(msg.sender, gameId, game.currentRound);
}

uint8 round = game.currentRound;
game.revelations[round][msg.sender] = bid;
Expand Down
6 changes: 3 additions & 3 deletions apps/contracts/contracts/base/Constants.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

uint8 constant MIN_NUM = 1;
uint8 constant MAX_NUM = 100;
uint8 constant ROUND_TO_WIN = 5;
uint16 constant MIN_NUM = 1;
uint16 constant MAX_NUM = 100;
uint8 constant ROUND_TO_WIN = 3;
23 changes: 13 additions & 10 deletions apps/contracts/contracts/interfaces/IGuessingGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ pragma solidity ^0.8.23;

interface IGuessingGame {
struct Bid {
bytes32 bid_null_commitment;
bytes32 null_commitment;
uint256 submission;
uint256 nullifier;
}

struct Game {
Expand All @@ -15,7 +15,7 @@ interface IGuessingGame {
GameState state;
// player bid list
mapping(uint8 => mapping(address => Bid)) bids;
mapping(uint8 => mapping(address => uint8)) revelations;
mapping(uint8 => mapping(address => uint16)) revelations;
address finalWinner;
uint256 startTime;
uint256 lastUpdate;
Expand Down Expand Up @@ -46,12 +46,11 @@ interface IGuessingGame {
error GuessingGame__InvalidGameId();
error GuessingGame__NotEnoughPlayers(uint32 gameId);
error GuessingGame__GameHasEnded();
error GuessingGame__UnexpectedGameState(GameState actual);
error GuessingGame__UnexpectedGameState(GameState expected, GameState actual);
error GuessingGame__PlayerAlreadyJoin(address p);
error GuessingGame__SenderIsNotGameHost();
error GuessingGame__SenderNotOneOfPlayers();
error GuessingGame__BidProofRejected(address, uint32, uint8);
error GuessingGame__BidOutOfRange(address, uint8);
error GuessingGame__NotGameHost(uint32 gameId, address addr);
error GuessingGame__InvalidSubmitRangeCheckProof(uint32 gameId, uint8 round, address addr);
error GuessingGame__NotOneOfPlayers();

// Emitted Events
event NewGame(uint32 indexed gameId, address indexed sender);
Expand All @@ -67,7 +66,11 @@ interface IGuessingGame {
function newGame() external returns (uint32 gameId);
function joinGame(uint32 gameId) external;
function startGame(uint32 gameId) external;
function submitCommitment(uint32 gameId, bytes32, bytes32) external;
function revealCommitment(uint32 gameId, bytes32 proof, uint8 bid, uint256 nullifier) external;
function submitCommitment(
uint32 gameId,
uint256[24] calldata _proof,
uint256[2] calldata _pubSignals
) external;
function openCommitment(uint32 gameId, bytes32 proof, uint16 bid) external;
function endRound(uint32 gameId) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

interface ISubmitRangeCheckVerifier {
function verifyProof(
uint256[24] calldata _proof,
uint256[2] calldata _pubSignals
) external view returns (bool);
}
21 changes: 13 additions & 8 deletions apps/contracts/tasks/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import { task, types } from "hardhat/config";

task("deploy", "Deploy a contract")
task("deploy", "Deploy all Number Guessing Game contracts")
.addOptionalParam("logs", "Print the logs", true, types.boolean)
.setAction(async ({ logs }, { ethers, run }) => {
const game = await run("deploy:game", { logs });
const verifiers = await run("deploy:verifiers", { logs });
return { game, ...verifiers };
const verifiers = await run("deploy:game-verifiers", { logs });

const rcVerifier = await verifiers.rcContract.getAddress();
const gameContract = await run("deploy:game", { logs, rcVerifier });

return { gameContract, ...verifiers };
});

task("deploy:game", "Deploy a GuessingGame contract")
task("deploy:game", "Deploy Number Guessing Game main contract")
.addOptionalParam("logs", "Print the logs", true, types.boolean)
.setAction(async ({ logs }, { ethers, run }) => {
.addParam("rcVerifier", "submit-rangecheck verifier address", undefined, types.string)
.setAction(async ({ logs, rcVerifier }, { ethers, run }) => {
const factory = await ethers.getContractFactory("GuessingGame");
const contract = await factory.deploy();

const contract = await factory.deploy(rcVerifier);
await contract.waitForDeployment();

logs && console.info(`GuessingGame contract: ${await contract.getAddress()}`);

return contract;
});

task("deploy:verifiers", "Deploy all verifier contracts")
task("deploy:game-verifiers", "Deploy all Number Guessing Game verifier contracts")
.addOptionalParam("logs", "Print the logs", true, types.boolean)
.setAction(async ({ logs }, { ethers, run }) => {
const rcFactory = await ethers.getContractFactory(
Expand Down
67 changes: 50 additions & 17 deletions apps/contracts/test/GuessingGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,40 @@ import { GuessingGame } from "../typechain-types";
chai.use(chaiAsPromised);
const expect = chai.expect;

describe("GuessingGame", () => {
let contracts;
let host;
let bob, charlie;
// Defining circuit base paths
const SUBMIT_RANGECHECK_CIRCUIT_BASEPATH = "./artifacts/circuits/submit-rangecheck-1-100";

describe("GuessingGame", () => {
async function deployContractsCleanSlate() {
contracts = await run("deploy", { logs: false });
[host, bob, charlie] = await hre.ethers.getSigners();
const contracts = await run("deploy", { logs: false });
const [host, bob, charlie] = await hre.ethers.getSigners();
Object.values(contracts).map((c) => c.connect(host));

return { contracts, players: { host, bob, charlie } };
}

async function deployContractsGameStarted() {
contracts = await run("deploy", { logs: false });
[host, bob, charlie] = await hre.ethers.getSigners();
const contracts = await run("deploy", { logs: false });
const [host, bob, charlie, dave] = await hre.ethers.getSigners();
Object.values(contracts).map((c) => c.connect(host));

return { contracts, players: { host, bob, charlie } };
const { gameContract } = contracts;

const GAME_ID = 0;
await gameContract.newGame();
await Promise.all([
gameContract.connect(bob).joinGame(GAME_ID),
gameContract.connect(charlie).joinGame(GAME_ID),
]);
await gameContract.startGame(GAME_ID);

return { contracts, players: { host, bob, charlie, dave } };
}

describe("L New Game", () => {
it("should create a new game", async () => {
const { contracts, players } = await loadFixture(deployContractsCleanSlate);
const { game: gameContract } = contracts;
const { gameContract } = contracts;
const { host } = players;

await gameContract.newGame();
Expand All @@ -50,7 +59,7 @@ describe("GuessingGame", () => {

it("host can't join the game again, but other players can", async () => {
const { contracts, players } = await loadFixture(deployContractsCleanSlate);
const { game: gameContract } = contracts;
const { gameContract } = contracts;
const { host, bob } = players;

await gameContract.newGame();
Expand All @@ -71,7 +80,7 @@ describe("GuessingGame", () => {

it("can start game by host once there are more than two players", async () => {
const { contracts, players } = await loadFixture(deployContractsCleanSlate);
const { game: gameContract } = contracts;
const { gameContract } = contracts;
const { host, bob, charlie } = players;

await gameContract.newGame();
Expand All @@ -93,7 +102,34 @@ describe("GuessingGame", () => {
});
});

describe("L Players submitting and revealing bid for a round", () => {});
describe("L After a game started", () => {
it("only players can submit a commitment, non-players cannot", async () => {
const { contracts, players } = await loadFixture(deployContractsGameStarted);
const { gameContract } = contracts;
const { host, dave } = players;

const GAME_ID = 0;
const rand = randomInt(281474976710655);
// generate proof
const input = { in: 99, rand };
const { proof, publicSignals } = await prove(input, SUBMIT_RANGECHECK_CIRCUIT_BASEPATH);

// host can submit a commitment
await expect(gameContract.submitCommitment(GAME_ID, toOnChainProof(proof), publicSignals))
.to.emit(gameContract, "BidSubmitted")
.withArgs(GAME_ID, 0, host.address);

// check the relevant game state on-chain
const bid = await gameContract.getPlayerCommitment(GAME_ID, 0, host.address);
expect(bid).to.deep.equal(publicSignals);

// dave couldn't submit a commitment
const daveGameContract = gameContract.connect(dave);
await expect(
daveGameContract.submitCommitment(GAME_ID, toOnChainProof(proof), publicSignals)
).to.be.revertedWithCustomError(gameContract, "GuessingGame__NotOneOfPlayers");
});
});

describe("L Range check: genarate proof offchain, verify proof onchain", () => {
it("should create a range proof and be verified", async () => {
Expand All @@ -104,10 +140,7 @@ describe("GuessingGame", () => {

// generate proof
const input = { in: 99, rand };
const { proof, publicSignals } = await prove(
input,
`./artifacts/circuits/submit-rangecheck-1-100`
);
const { proof, publicSignals } = await prove(input, SUBMIT_RANGECHECK_CIRCUIT_BASEPATH);
const result = await rcContract.verifyProof(toOnChainProof(proof), publicSignals);
expect(result).to.be.true;
});
Expand Down
Loading

0 comments on commit 984556b

Please sign in to comment.