Skip to content

Commit

Permalink
adding guess random number game
Browse files Browse the repository at this point in the history
  • Loading branch information
poppyseedDev committed Jan 13, 2025
1 parent 67bf4f6 commit 99017e6
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 93 deletions.
21 changes: 0 additions & 21 deletions hardhat/contracts/confidentialCounter/sample0.sol

This file was deleted.

12 changes: 1 addition & 11 deletions hardhat/contracts/confidentialCounter/sample1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,10 @@ import "fhevm/config/ZamaFHEVMConfig.sol";
/// for understanding how to implement basic FHE operations in Solidity
contract EncryptedCounter1 is SepoliaZamaFHEVMConfig {
euint8 internal counter;
euint8 internal immutable CONST_ONE;

constructor() {
// Initialize counter with an encrypted zero value
counter = TFHE.asEuint8(0);
TFHE.allowThis(counter);
// Save on gas by computing the constant here
CONST_ONE = TFHE.asEuint8(1);
TFHE.allowThis(CONST_ONE);
}

function increment() public {
// Perform encrypted addition to increment the counter
counter = TFHE.add(counter, CONST_ONE);
counter = TFHE.add(counter, 1);
TFHE.allowThis(counter);
}
}
111 changes: 79 additions & 32 deletions hardhat/contracts/guessRandomNumberGame/GuessRandomNumberGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,52 @@ import "fhevm/gateway/GatewayCaller.sol";
/// @title Guess Random Number Game
/// @notice A decentralized game where players submit their encrypted guesses to guess the random number
contract GuessRandomNumberGame is SepoliaZamaFHEVMConfig, SepoliaZamaGatewayConfig, GatewayCaller {
uint64 public constant MAX_VAlUE = 1024; // Upper bound must be a power of 2
euint64 encryptedTarget;
uint64 public decryptedTarget;
// uint16 is best suited for this task since -> uint16: 0 to 65,535 (2^16 - 1)
uint16 public constant MAX_VAlUE = 1024; // Upper bound must be a power of 2
uint8 public immutable MIN_PLAYERS;
euint16 encryptedTarget;
uint16 public decryptedPreviousTarget; // Decrypted previous encrypted target
euint16 closestValue; // Closest winning value
uint16 public closestPreviousWinningValueDecrypted; // Closest previous winning value decrypted
eaddress closestOwner; // Closest winning owner
address public closestPreviousWinnerDecrypted; // Closest previous winning address decrypted

// Adding player scores to make it more fun
mapping(address => uint256) public playerScores;

struct Secret {
euint64 encryptedValue;
euint16 encryptedValue;
address owner;
}
Secret[] public secrets;

/// @notice Event emitted when a secret is submitted
event SecretSubmitted(address indexed player, euint64 encryptedValue);
event SecretSubmitted(address indexed player, euint16 encryptedValue);

/// @notice Event emitted when a winner is determined
event WinnerDeclared(address indexed winner, euint64 winningValue);
event WinnerDeclared(address indexed winner, uint16 winningValue);

/// @notice Event emitted when game reset
event GameReset();

/// @notice Constructor sets a random encrypted target value
constructor() {
encryptedTarget = TFHE.randEuint64(MAX_VAlUE);
constructor(uint8 _minPlayers) {
MIN_PLAYERS = _minPlayers;
encryptedTarget = TFHE.randEuint16(MAX_VAlUE);
// Encrypt and store the target value
TFHE.allowThis(encryptedTarget);
}

/// @notice Function to submit an encrypted secret
/// @notice Function to submit the encrypted guess
/// @param secretValue The encrypted secret value
/// @param inputProof The ZK proof for the secret
function submitSecret(einput secretValue, bytes calldata inputProof) public {
euint64 validatedSecret = TFHE.asEuint64(secretValue, inputProof);
function submitGuess(einput secretValue, bytes calldata inputProof) public {
// Check if address has already participated
for (uint i = 0; i < secrets.length; i++) {
require(secrets[i].owner != msg.sender, "Address has already participated");
}

euint16 validatedSecret = TFHE.asEuint16(secretValue, inputProof);
// allowing the contract and the user to access this contract later
TFHE.allowThis(validatedSecret);
TFHE.allow(validatedSecret, msg.sender);
Expand All @@ -50,7 +68,7 @@ contract GuessRandomNumberGame is SepoliaZamaFHEVMConfig, SepoliaZamaGatewayConf

/// @notice Get the encrypted secret value for the caller
/// @return The encrypted secret value for the caller
function getMySecret() public view returns (euint64) {
function getMySecret() public view returns (euint16) {
for (uint i = 0; i < secrets.length; i++) {
if (secrets[i].owner == msg.sender) {
return secrets[i].encryptedValue;
Expand All @@ -60,16 +78,19 @@ contract GuessRandomNumberGame is SepoliaZamaFHEVMConfig, SepoliaZamaGatewayConf
}

/// @notice Compare all secrets to the encrypted target and determine the winner
/// @return winner The address of the winner
function determineWinner() public returns (address winner) {
require(secrets.length > 0, "No secrets submitted");
function determineWinner() public {
require(secrets.length >= MIN_PLAYERS, "Not enough players have submitted guesses");

euint64 closestValue = TFHE.asEuint64(0); // Start with 0 as the closest value
euint64 smallestDifference = TFHE.asEuint64(MAX_VAlUE); // Start with max value
address closestOwner;
closestValue = TFHE.asEuint16(0); // Start with 0 as the closest value
euint16 smallestDifference = TFHE.asEuint16(MAX_VAlUE); // Start with max value
closestOwner = TFHE.asEaddress(address(this)); // Begin closest owner with this address

for (uint i = 0; i < secrets.length; i++) {
euint64 difference = TFHE.sub(secrets[i].encryptedValue, encryptedTarget);
euint16 difference = TFHE.select(
TFHE.gt(secrets[i].encryptedValue, encryptedTarget),
TFHE.sub(secrets[i].encryptedValue, encryptedTarget),
TFHE.sub(encryptedTarget, secrets[i].encryptedValue)
);
ebool diff_smaller = TFHE.lt(difference, smallestDifference);

// Use TFHE.select to find the smallest difference
Expand All @@ -78,27 +99,53 @@ contract GuessRandomNumberGame is SepoliaZamaFHEVMConfig, SepoliaZamaGatewayConf
// Update the closest value and owner if the difference is smaller
closestValue = TFHE.select(diff_smaller, secrets[i].encryptedValue, closestValue);

// closestOwner = TFHE.select(diff_smaller, secrets[i].owner, closestOwner);
// Update the closest owner
closestOwner = TFHE.select(diff_smaller, TFHE.asEaddress(secrets[i].owner), closestOwner);
}

closestOwner = msg.sender;
// Emit winner information
// emit WinnerDeclared(closestOwner, closestValue);
return closestOwner;
requestDecrypt();
}

/// @notice Request decryption of the encrypted target value
function requestDecryptEncryptedTarget() public {
uint256[] memory cts = new uint256[](1);
/// @notice Request decryption of the encrypted target value, closest value and the closest owner
function requestDecrypt() internal {
uint256[] memory cts = new uint256[](3); // 3 indicates how many items do we want to decrypt
cts[0] = Gateway.toUint256(encryptedTarget);
cts[1] = Gateway.toUint256(closestValue);
cts[2] = Gateway.toUint256(closestOwner);
Gateway.requestDecryption(cts, this.callbackEncryptedTarget.selector, 0, block.timestamp + 100, false);
}

/// @notice Callback function for encrypted target decryption
/// @param decryptedInput The decrypted target value
/// @return The decrypted value
function callbackEncryptedTarget(uint256, uint64 decryptedInput) public onlyGateway returns (uint64) {
decryptedTarget = decryptedInput;
return decryptedInput;
/// @param _decryptedTarget The decrypted target value
/// @param _closestValueDecrypted The decrypted owner address
/// @param _closestOwnerDecrypted The decrypted owner address
function callbackEncryptedTarget(
uint256,
uint16 _decryptedTarget,
uint16 _closestValueDecrypted,
address _closestOwnerDecrypted
) public onlyGateway {
decryptedPreviousTarget = _decryptedTarget;
closestPreviousWinningValueDecrypted = _closestValueDecrypted;
closestPreviousWinnerDecrypted = _closestOwnerDecrypted;

// Emit winner information
emit WinnerDeclared(closestPreviousWinnerDecrypted, closestPreviousWinningValueDecrypted);

resetGame();
}

/// @notice Reset the game state for a new round
function resetGame() internal {
// Reset the secrets array
delete secrets;

// Reset encrypted values
closestValue = TFHE.asEuint16(0);
closestOwner = TFHE.asEaddress(address(0));

encryptedTarget = TFHE.randEuint16(MAX_VAlUE);

emit GameReset();
}
}
68 changes: 39 additions & 29 deletions hardhat/test/guessRandomNumberGame/GuessRandomNumberGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ethers } from "hardhat";
import type { GuessRandomNumberGame } from "../../types";
import { awaitAllDecryptionResults, initGateway } from "../asyncDecrypt";
import { createInstance } from "../instance";
import { reencryptEuint16 } from "../reencrypt";
import { getSigners, initSigners } from "../signers";

describe("EncryptedSecretKeeper", function () {
Expand All @@ -17,78 +18,87 @@ describe("EncryptedSecretKeeper", function () {

beforeEach(async function () {
const CounterFactory = await ethers.getContractFactory("GuessRandomNumberGame");
contract = await CounterFactory.connect(this.signers.alice).deploy();
contract = await CounterFactory.connect(this.signers.alice).deploy(2); // minPlayers: 2
await contract.waitForDeployment();
this.contractAddress = await contract.getAddress();
this.instances = await createInstance();
});

it("should allow players to submit encrypted secrets", async function () {
const input = this.instances.createEncryptedInput(this.contractAddress, this.signers.alice.address);
input.add64(500); // Add the secret value
input.add16(500); // Add the secret value
const encryptedInput = await input.encrypt();

const tx = await contract
.connect(this.signers.alice)
.submitSecret(encryptedInput.handles[0], encryptedInput.inputProof);
.submitGuess(encryptedInput.handles[0], encryptedInput.inputProof);
await tx.wait();

const secrets = await contract.secrets(0);
expect(secrets.owner).to.equal(this.signers.alice.address);
expect(secrets.encryptedValue).to.not.be.null;
});

it("should request decryption of the encrypted target", async function () {
const tx = await contract.requestDecryptEncryptedTarget();
await tx.wait();

await awaitAllDecryptionResults();

const decryptedValue = await contract.decryptedTarget();
expect(decryptedValue).to.equal(5);
it("should request decryption and revert", async function () {
await expect(contract.determineWinner()).to.be.revertedWith("Not enough players have submitted guesses");
});

it.only("should determine the winner based on the closest secret to the target", async function () {
// Simulate player 1 submitting a secret
const input1 = this.instances.createEncryptedInput(this.contractAddress, this.signers.alice.address);
input1.add64(450);
input1.add16(450);
const encryptedInput1 = await input1.encrypt();

await contract.connect(this.signers.alice).submitSecret(encryptedInput1.handles[0], encryptedInput1.inputProof);
await contract.connect(this.signers.alice).submitGuess(encryptedInput1.handles[0], encryptedInput1.inputProof);

// Simulate player 2 submitting a secret
const input2 = this.instances.createEncryptedInput(this.contractAddress, this.signers.bob.address);
input2.add64(700);
input2.add16(700);
const encryptedInput2 = await input2.encrypt();

await contract.connect(this.signers.bob).submitSecret(encryptedInput2.handles[0], encryptedInput2.inputProof);
await contract.connect(this.signers.bob).submitGuess(encryptedInput2.handles[0], encryptedInput2.inputProof);

// Call determineWinner and get transaction response
const tx = await contract.connect(this.signers.alice).determineWinner();
const receipt = await tx.wait();
await tx.wait();

// Get the data from the transaction
const data = tx.data;
console.log("Transaction data:", data);
await awaitAllDecryptionResults();

// Verify the transaction was successful
expect(receipt.status).to.equal(1);
// expect(data).to.equal("0x33b16d93"); // The function selector for determineWinner()
const decryptedValue = await contract.decryptedPreviousTarget();
expect(decryptedValue).to.be.within(0, 1024);
console.log("decrypted Value: ", decryptedValue);
const closestValue = await contract.closestPreviousWinningValueDecrypted();
console.log("closest value: ", closestValue);
const closestOwnerDecrypted = await contract.closestPreviousWinnerDecrypted();
console.log("closest Owner: ", closestOwnerDecrypted);
expect(closestOwnerDecrypted).to.be.oneOf([this.signers.bob.address, this.signers.alice.address]);
});

it("should emit events on secret submission and winner declaration", async function () {
it("should emit events on secret submission", async function () {
// Simulate player submitting a secret
const input = this.instances.createEncryptedInput(this.contractAddress, this.signers.alice.address);
input.add64(350);
input.add16(350);
const encryptedInput = await input.encrypt();

await expect(
contract.connect(this.signers.alice).submitSecret(encryptedInput.handles[0], encryptedInput.inputProof),
)
await expect(contract.connect(this.signers.alice).submitGuess(encryptedInput.handles[0], encryptedInput.inputProof))
.to.emit(contract, "SecretSubmitted")
.withArgs(this.signers.alice.address, encryptedInput.handles[0]);
});

it("should allow retrieving encrypted secret for caller", async function () {
// Submit a secret first
const input = this.instances.createEncryptedInput(this.contractAddress, this.signers.alice.address);
input.add16(123);
const encryptedInput = await input.encrypt();

await contract.connect(this.signers.alice).submitGuess(encryptedInput.handles[0], encryptedInput.inputProof);

// Get the secret back
const mySecret = await contract.connect(this.signers.alice).getMySecret();
expect(mySecret).to.not.be.null;

const decryptedValue = await reencryptEuint16(this.signers.alice, this.instances, mySecret, this.contractAddress);

// Call determineWinner and check event
await expect(contract.connect(this.signers.alice).determineWinner()).to.emit(contract, "WinnerDeclared");
expect(decryptedValue).to.equal(123);
});
});

0 comments on commit 99017e6

Please sign in to comment.