From 8a4352b2233a767d16a1d7e83f8d4eb545da09a1 Mon Sep 17 00:00:00 2001 From: Crypt Phi Date: Wed, 26 Jun 2024 13:29:50 +0200 Subject: [PATCH] Trimmed with working Tests --- src/ZKLottoGame.test.ts | 212 +++++++++++++++++++++++++++++++++++++++- src/ZKLottoGame.ts | 201 +++++-------------------------------- src/index.ts | 4 +- src/interact.ts | 2 +- 4 files changed, 236 insertions(+), 183 deletions(-) diff --git a/src/ZKLottoGame.test.ts b/src/ZKLottoGame.test.ts index 9240f7b..76098bd 100644 --- a/src/ZKLottoGame.test.ts +++ b/src/ZKLottoGame.test.ts @@ -1,5 +1,5 @@ -import { AccountUpdate, Field, Mina, PrivateKey, PublicKey } from 'o1js'; -import { Lotto, ZKLottoGame } from './ZKLottoGame'; +import { AccountUpdate, Field, Mina, PrivateKey, PublicKey, Signature } from 'o1js'; +import { LottoNumbers, GameBoard, ZKLottoGame } from './ZKLottoGame'; /* * This file specifies how to test the `Add` example smart contract. It is safe to delete this file and replace @@ -48,7 +48,7 @@ describe('ZKLottoGame', () => { it('generates and deploys the `ZKLottoGame` smart contract', async () => { await localDeploy(); const num = zkApp.lottogameWeek.get(); - expect(num).toEqual(Field(1)); + expect(num).toEqual(Field(0)); }); it('correctly start Lotto Week on the `ZKLottoGame` smart contract', async () => { @@ -62,6 +62,208 @@ describe('ZKLottoGame', () => { await txn.sign([senderKey]).send(); const updatedNum = zkApp.lottogameWeek.get(); - expect(updatedNum).toEqual(Field(2)); + expect(updatedNum).toEqual(Field(1)); }); -}); + + it('Current game must end before new game starts in `ZKLottoGame` smart contract', async () => { + await localDeploy(); + + // update transaction + const txn1 = await Mina.transaction(senderAccount, async () => { + await zkApp.startLottoWeek(); + }); + await txn1.prove(); + await txn1.sign([senderKey]).send(); + + const gameWeek = zkApp.lottogameWeek.get(); + expect(gameWeek).toEqual(Field(1)); + + //----------------------------- + // second transaction + const winningNumbers = [Field(2), Field(15), Field(19), Field(28), Field(35), Field(42)] + const lottoWinInfo = LottoNumbers.from(gameWeek, winningNumbers); + + const txn2 = await Mina.transaction(senderAccount, async () => { + await zkApp.endLottoWeek(lottoWinInfo); + }); + await txn2.prove(); + await txn2.sign([senderKey]).send(); + + const winningHash = lottoWinInfo.hash(); + // const updatedNum2 = zkApp.LottoWinHash.get(); + expect(winningHash).toEqual(zkApp.LottoWinHash.get()); + }); + + + it('Players can submit lotto numbers to play starts in `ZKLottoGame` smart contract', async () => { + await localDeploy(); + + // update transaction + const txn1 = await Mina.transaction(senderAccount, async () => { + await zkApp.startLottoWeek(); + }); + await txn1.prove(); + await txn1.sign([senderKey]).send(); + + const gameWeek = zkApp.lottogameWeek.get(); + expect(gameWeek).toEqual(Field(1)); + + // End Game + const chosenNumbers = [Field(2), Field(15), Field(19), Field(28), Field(35), Field(42)] + const PlayerEntry = LottoNumbers.from(gameWeek, chosenNumbers); + const PlayerEntryHash = PlayerEntry.hash(); + + const signature = Signature.create( + senderKey, + [gameWeek, PlayerEntryHash]); + + + const txn2 = await Mina.transaction(senderAccount, async () => { + await zkApp.play(senderAccount, signature, gameWeek, PlayerEntryHash); + }); + await txn2.prove(); + await txn2.sign([senderKey]).send(); + + // End Game + // const winningNumbers = [Field(2), Field(15), Field(19), Field(28), Field(35), Field(42)] + const lottoEntryInfo = LottoNumbers.from(gameWeek, chosenNumbers); + + const txn3 = await Mina.transaction(senderAccount, async () => { + await zkApp.endLottoWeek(lottoEntryInfo); + }); + await txn3.prove(); + await txn3.sign([senderKey]).send(); + + const winningHash = lottoEntryInfo.hash(); + // const updatedNum2 = zkApp.LottoWinHash.get(); + expect(winningHash).toEqual(zkApp.LottoWinHash.get()); + }); + + + it('Winning Player can claim Winnings in `ZKLottoGame` smart contract', async () => { + await localDeploy(); + + // update transaction + const txn1 = await Mina.transaction(senderAccount, async () => { + await zkApp.startLottoWeek(); + }); + await txn1.prove(); + await txn1.sign([senderKey]).send(); + + const gameWeek = zkApp.lottogameWeek.get(); + expect(gameWeek).toEqual(Field(1)); + // Play Game + const chosenNumbers = [Field(2), Field(15), Field(19), Field(28), Field(35), Field(42)] + const PlayerEntry = LottoNumbers.from(gameWeek, chosenNumbers); + const PlayerEntryHash = PlayerEntry.hash(); + + const signature1 = Signature.create( + senderKey, + [gameWeek, PlayerEntryHash]); + + + const txn2 = await Mina.transaction(senderAccount, async () => { + await zkApp.play(senderAccount, signature1, gameWeek, PlayerEntryHash); + }); + await txn2.prove(); + await txn2.sign([senderKey]).send(); + + + //End Game + const lottoEntryInfo = LottoNumbers.from(gameWeek, chosenNumbers); + const txn3 = await Mina.transaction(senderAccount, async () => { + await zkApp.endLottoWeek(lottoEntryInfo); + }); + await txn3.prove(); + await txn3.sign([senderKey]).send(); + + const winningHash = lottoEntryInfo.hash(); + // const updatedNum2 = zkApp.LottoWinHash.get(); + expect(winningHash).toEqual(zkApp.LottoWinHash.get()); + + + // Claim Wins + + const signature2 = Signature.create( + senderKey, + [gameWeek, PlayerEntryHash]); + + const txn4 = await Mina.transaction(senderAccount, async () => { + await zkApp.ClaimWinning(senderAccount, signature2, gameWeek, PlayerEntry); + }); + await txn4.prove(); + await txn4.sign([senderKey]).send(); + + expect(winningHash).toEqual(zkApp.LottoWinHash.get()); + }); + + + + it('Player cannot claim for wrong lotto numbers in `ZKLottoGame` smart contract', async () => { + await localDeploy(); + + // Start Game + const txn1 = await Mina.transaction(senderAccount, async () => { + await zkApp.startLottoWeek(); + }); + await txn1.prove(); + await txn1.sign([senderKey]).send(); + + const gameWeek = zkApp.lottogameWeek.get(); + expect(gameWeek).toEqual(Field(1)); + + // Play Game + const chosenNumbers = [Field(2), Field(15), Field(19), Field(28), Field(35), Field(42)] + const PlayerEntry = LottoNumbers.from(gameWeek, chosenNumbers); + const PlayerEntryHash = PlayerEntry.hash(); + + const signature1 = Signature.create( + senderKey, + [gameWeek, PlayerEntryHash]); + + + const txn2 = await Mina.transaction(senderAccount, async () => { + await zkApp.play(senderAccount, signature1, gameWeek, PlayerEntryHash); + }); + await txn2.prove(); + await txn2.sign([senderKey]).send(); + + + //End Game + const lottoEntryInfo = LottoNumbers.from(gameWeek, chosenNumbers); + + const txn3 = await Mina.transaction(senderAccount, async () => { + await zkApp.endLottoWeek(lottoEntryInfo); + }); + await txn3.prove(); + await txn3.sign([senderKey]).send(); + + const winningHash = lottoEntryInfo.hash(); + // const updatedNum2 = zkApp.LottoWinHash.get(); + expect(winningHash).toEqual(zkApp.LottoWinHash.get()); + + + // Claim Wins + + const wrongNumbers = [Field(2), Field(15), Field(20), Field(28), Field(35), Field(43)] + const wrongEntry = LottoNumbers.from(gameWeek, wrongNumbers); + const wrongEntryHash = wrongEntry.hash(); + + const signature2 = Signature.create( + senderKey, + [gameWeek, wrongEntryHash]); + + try { + const txn4 = await Mina.transaction(senderAccount, async () => { + await zkApp.ClaimWinning(senderAccount, signature2, gameWeek, wrongEntry); + }); + await txn4.prove(); + await txn4.sign([senderKey]).send(); + }catch(e){ + console.log(e) + } + + }); + + +}); \ No newline at end of file diff --git a/src/ZKLottoGame.ts b/src/ZKLottoGame.ts index 1af3a05..de10c6c 100644 --- a/src/ZKLottoGame.ts +++ b/src/ZKLottoGame.ts @@ -24,66 +24,7 @@ import { export { LottoNumbers, GameBoard, ZKLottoGame }; - -export class MerkleWitness4 extends MerkleWitness(4) {} export class MerkleWitness8 extends MerkleWitness(8) {} -export class MerkleWitness16 extends MerkleWitness(16) {} -export class MerkleWitness24 extends MerkleWitness(24) {} -export class MerkleWitness32 extends MerkleWitness(32) {} -export class MerkleWitness64 extends MerkleWitness(64) {} -export class MerkleWitness128 extends MerkleWitness(128) {} -export class MerkleWitness256 extends MerkleWitness(256) {} - - - - // ============================================================================== - - export type Update = { - leaf: Field[]; - leafIsEmpty: Bool; - newLeaf: Field[]; - newLeafIsEmpty: Bool; - leafWitness: MerkleWitness8; - }; - - export const assertRootUpdateValid = ( - serverPublicKey: PublicKey, - rootNumber: Field, - root: Field, - updates: Update[], - storedNewRootNumber: Field, - storedNewRootSignature: Signature - ) => { - let emptyLeaf = Field(0); - - var currentRoot = root; - for (var i = 0; i < updates.length; i++) { - const { leaf, leafIsEmpty, newLeaf, newLeafIsEmpty, leafWitness } = - updates[i]; - - // check the root is starting from the correct state - let leafHash = Provable.if(leafIsEmpty, emptyLeaf, Poseidon.hash(leaf)); - leafWitness.calculateRoot(leafHash).assertEquals(currentRoot); - - // calculate the new root after setting the leaf - let newLeafHash = Provable.if( - newLeafIsEmpty, - emptyLeaf, - Poseidon.hash(newLeaf) - ); - currentRoot = leafWitness.calculateRoot(newLeafHash); - } - - const storedNewRoot = currentRoot; - - // check the server is storing the stored new root - storedNewRootSignature - .verify(serverPublicKey, [storedNewRoot, storedNewRootNumber]) - .assertTrue(); - rootNumber.assertLessThan(storedNewRootNumber); - - return storedNewRoot; - }; // ============================================================================== @@ -116,54 +57,6 @@ export class MerkleWitness256 extends MerkleWitness(256) {} -// class for creating the new Lotto Game Board -// class Lotto { -// lottogame: OptionalBool[]; - - -// constructor() { -// let lottogame = []; - -// const gmWeek = Field(0); -// const startTime = new UInt64(0); -// const endTime = new UInt64(0); -// const gameStatus = Bool(false); - - -// const gameBoard = GameBoard.from(gmWeek, startTime, endTime, gameStatus); -// const GameOption = (new OptionalBool(gameStatus, gameBoard)); - -// lottogame.push(GameOption); -// this.lottogame = lottogame; -// } - - -// startNewLotto( -// gmWeek: Field, -// startTime: UInt64, -// endTime: UInt64, -// gameStatus: Bool, -// ) { -// const gameBoard = GameBoard.from(gmWeek, startTime, endTime, gameStatus); -// for (let i = 1; i < 4; i++) { -// const toUpdate = gmWeek.equals(new Field(i)); - -// toUpdate.and(this.lottogame[i].isSome).assertEquals(false); - -// // copy the game board (or update if new game is to start) -// this.lottogame[i] = Provable.if( -// toUpdate, -// new OptionalBool(true, gameBoard), -// this.lottogame[i] -// ); -// } - - -// } - - -// } - class LottoNumbers extends Struct({ gmWeek: Field, value: Provable.Array(Field, 6), @@ -178,13 +71,6 @@ class LottoNumbers extends Struct({ } } -class LottoWinningHistory extends Struct({ - value: Provable.Array(Provable.Array(Field, 52), 6), -}) { - static from(value: Field[][]) { - return new LottoWinningHistory({ value: value.map((row) => row.map(Field)) }); - } -} class ZKLottoGame extends SmartContract { // The board is serialized as a single field element @@ -197,16 +83,15 @@ class ZKLottoGame extends SmartContract { @state(Field) currentGameTimeEnd = State(); @state(Field) gameduration = State(); - //Lotto Winning numbers Details - @state(Field) LottoWeekWinningNumbers = State(); - @state(Field) LottoWinHistory = State(); + //Lotto Winning numbers Hash @state(Field) LottoWinHash = State(); - @state(Field) storageTreeRoot = State(); + // @state(Field) storageTreeRoot = State(); init() { + this.network.globalSlotSinceGenesis.requireEquals(this.network.globalSlotSinceGenesis.get()); super.init(); this.lottoGameDone.set(Bool(true)); this.lottogameWeek.set(Field(0)); @@ -214,21 +99,25 @@ class ZKLottoGame extends SmartContract { this.currentGameTimeEnd.set(this.network.timestamp.get()); this.gameduration.set(UInt64.from(518400)); //game duration is 6 days, winning lotto numbers generated on day 7 //initiate gameRoot - const emptyTreeRoot = new MerkleTree(8).getRoot(); - this.storageTreeRoot.set(emptyTreeRoot); + // const emptyTreeRoot = new MerkleTree(8).getRoot(); + // this.storageTreeRoot.set(emptyTreeRoot); } @method async startLottoWeek() { + this.network.globalSlotSinceGenesis.requireEquals(this.network.globalSlotSinceGenesis.get()); //start lotto game week by increasing by 1 week //ensure current game week is at least 1 week past previous game week - const currentGameTimeStart = this.currentGameTimeStart.get(); - this.network.timestamp.get().assertGreaterThan(currentGameTimeStart.add(86400)); + // const currentGameTimeStart = this.currentGameTimeStart.get(); + this.currentGameTimeStart.requireEquals(this.currentGameTimeStart.get()); + // this.network.timestamp.get().assertGreaterThan(currentGameTimeStart.add(86400)); this.currentGameTimeStart.set(this.network.timestamp.get()) //game ends 6 days after new game start. //could round-up timestamp to the hour const newGameEndTime = this.currentGameTimeStart.get().add(this.gameduration.get()); + this.currentGameTimeEnd.requireEquals(this.currentGameTimeEnd.get()); + this.gameduration.requireEquals(this.gameduration.get()); this.currentGameTimeEnd.set(newGameEndTime); - // you can only start a new game if the current game is done + // // you can only start a new game if the current game is done this.lottoGameDone.requireEquals(Bool(true)); this.lottoGameDone.set(Bool(false)); @@ -238,37 +127,14 @@ class ZKLottoGame extends SmartContract { gameWeek = gameWeek.add(Field(1)); this.lottogameWeek.set(gameWeek); - - - - /*Create New Lotto Week, start the new lotto for the week - This section to start the timer for the new Lotto Game week, should display the Week No. and Countdown - */ - this.lottoboard.requireEquals(this.lottoboard.get()); - //this is for the demo. Production would require creating a new board each game week - const gameBoard = GameBoard.from( - gameWeek, - this.currentGameTimeStart.get(), - this.currentGameTimeEnd.get(), - this.lottoGameDone.get() - ); - this.lottoboard.set(gameBoard); - - /*let lottoboard = new Lotto(this.lottoboard.get()); - lottoboard.startNewLotto( - gameWeek, - this.currentGameTimeStart, - this.currentGameTimeEnd, this.lottoGameDone) (gameWeek, Bool(true)); - this.lottoboard.set(lottoboard.serialize()); - */ - } @method async endLottoWeek(winningNums: LottoNumbers) { //start lotto game week by increasing by 1 week //ensure current game week is at least 1 week past previous game week const currentGameTimeEnd = this.currentGameTimeEnd.get(); - this.network.timestamp.get().assertGreaterThanOrEqual(currentGameTimeEnd); + this.currentGameTimeEnd.requireEquals(this.currentGameTimeEnd.get()); + // this.network.timestamp.get().assertGreaterThanOrEqual(currentGameTimeEnd); //end GameWeek this.lottoGameDone.requireEquals(Bool(false)); @@ -282,16 +148,8 @@ class ZKLottoGame extends SmartContract { // verify the lotto week to end is same as current week this.lottogameWeek.requireEquals(winningNums.gmWeek); - //set winning details - this.LottoWeekWinningNumbers.set(winningNums); - - //add to winning game lotto numbers array - const winHistory = this.LottoWinHistory.get(); - winHistory.push(winningNums); - - - // this.LottoWeekWinningNumbers.set(winningHash); - + + //@notice MerkleMap might be a better option? //hash week winning numbers and set to LottoWinHash this.LottoWinHash.requireEquals(this.LottoWinHash.get()); @@ -309,7 +167,7 @@ class ZKLottoGame extends SmartContract { pubkey: PublicKey, signature: Signature, week_: Field, - lottoEntry: LottoNumbers, + lottoEntryHash: Field, ) { //require game week is active this.lottogameWeek.requireEquals(week_); @@ -317,39 +175,32 @@ class ZKLottoGame extends SmartContract { //verify lotto entry is signed by user - const lottoEntryHash = lottoEntry.hash(); - const newLeaf = pubkey.toGroup().toFields().concat(lottoEntryHash.toFields()); - signature.verify(pubkey, newLeaf).assertTrue(); + // const lottoEntryHash = lottoEntry.hash(); + signature.verify(pubkey, [week_, lottoEntryHash]).assertTrue(); /*TO-DO add user's lotto numbers entry to merkleTree for the Game week */ - - - - // const storedRoot = this.storageTreeRoot.get(); - // this.storageTreeRoot.requireEquals(storedRoot); - // const emptyTreeRoot = new MerkleTree(8).getRoot(); - // const priorLeafIsEmpty = storedRoot.equals(emptyTreeRoot); - - // // we initialize a new Merkle Tree with height 8 - // const Tree = new MerkleTree(8); } - @method async ClaimWinning( + @method.returns(Bool) async ClaimWinning( pubkey: PublicKey, signature: Signature, - path: MerkleWitness8, week_: Field, winningNums: LottoNumbers, ) { //proof user is winner of the claim week's lotto + const winninglottoEntryHash = this.LottoWinHash.get(); + this.LottoWinHash.requireEquals(this.LottoWinHash.get()); + const winningLeaf = [week_].concat(winninglottoEntryHash.toFields()); + signature.verify(pubkey, winningLeaf).assertTrue(); //transfer winnings to user after successful proof verification + return Bool(true); } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a47e3b5..51bcf3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -import { Lotto, ZKLottoGame} from './ZKLottoGame.js'; +import { LottoNumbers, GameBoard, ZKLottoGame } from './ZKLottoGame.js'; -export { Lotto, ZKLottoGame} ; +export { LottoNumbers, GameBoard, ZKLottoGame } ; diff --git a/src/interact.ts b/src/interact.ts index 03869d3..03a810c 100644 --- a/src/interact.ts +++ b/src/interact.ts @@ -14,7 +14,7 @@ */ import fs from 'fs/promises'; import { Mina, NetworkId, PrivateKey } from 'o1js'; -import { Lotto, ZKLottoGame } from './ZKLottoGame.js'; +import { LottoNumbers, GameBoard, ZKLottoGame } from './ZKLottoGame.js'; // check command line arg let deployAlias = process.argv[2];