diff --git a/src/ZKLottoGame.test.ts b/src/ZKLottoGame.test.ts index 9240f7b..4b2374a 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 { offchainState, 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 @@ -33,6 +33,18 @@ describe('ZKLottoGame', () => { zkAppPrivateKey = PrivateKey.random(); zkAppAddress = zkAppPrivateKey.toPublicKey(); zkApp = new ZKLottoGame(zkAppAddress); + offchainState.setContractInstance(zkApp); + + // compile Offchain state program + if (proofsEnabled) { + console.time('compile program'); + await offchainState.compile(); + console.timeEnd('compile program'); + console.time('compile contract'); + await ZKLottoGame.compile(); + console.timeEnd('compile contract'); + } + }); async function localDeploy() { @@ -45,23 +57,78 @@ describe('ZKLottoGame', () => { await txn.sign([deployerKey, zkAppPrivateKey]).send(); } + // let initialCommitment: Field = Field(0); + // async function makeGuess(name: Names, index: bigint, guess: number) { + // let account = Accounts.get(name)!; + // let w = Tree.getWitness(index); + // let witness = new MyMerkleWitness(w); + + // let tx = await Mina.transaction(feePayer, async () => { + // await contract.guessPreimage(Field(guess), account, witness); + // }); + // await tx.prove(); + // await tx.sign([feePayer.key, contractAccount.key]).send(); + + // // if the transaction was successful, we can update our off-chain storage as well + // account.points = account.points.add(1); + // Tree.setLeaf(index, account.hash()); + // contract.commitment.get().assertEquals(Tree.getRoot()); + // } + it('generates and deploys the `ZKLottoGame` smart contract', async () => { await localDeploy(); - const num = zkApp.lottogameWeek.get(); - expect(num).toEqual(Field(1)); + + const num = await zkApp.settleWeek(); + expect(num).toEqual(Field(0)); }); - it('correctly start Lotto Week on the `ZKLottoGame` smart contract', async () => { - await localDeploy(); + // it('correctly start Lotto Week on the `ZKLottoGame` smart contract', async () => { + // await localDeploy(); + // console.time('settlement proof 1'); + // let proof = await offchainState.createSettlementProof(); + // console.timeEnd('settlement proof 1'); - // update transaction - const txn = await Mina.transaction(senderAccount, async () => { - await zkApp.startLottoWeek(); - }); - await txn.prove(); - await txn.sign([senderKey]).send(); + // console.time('settle 1'); + // await Mina.transaction(sender, () => contract.settle(proof)) + // .sign([sender.key]) + // .prove() + // .send(); + // console.timeEnd('settle 1'); + - const updatedNum = zkApp.lottogameWeek.get(); - expect(updatedNum).toEqual(Field(2)); - }); + // // update transaction + // const txn = await Mina.transaction(senderAccount, async () => { + // await zkApp.startLottoWeek(); + // }); + // await txn.prove(); + // await txn.sign([senderKey]).send(); + + // const GameWeekNum = zkApp.lottogameWeek.get(); + // expect(GameWeekNum).toEqual(Field(1)); + // }); + + // it('Should fail the start of a new game week when one is running on the `ZKLottoGame` smart contract', async () => { + // await localDeploy(); + + // // Start new Lotto Game Week + // const txn1 = await Mina.transaction(senderAccount, async () => { + // await zkApp.startLottoWeek(); + // }); + // await txn1.prove(); + // await txn1.sign([senderKey]).send(); + + // //start Lotto Game Week again + // try { + // const txn2 = await Mina.transaction(senderAccount, async () => { + // await zkApp.startLottoWeek(); + // }); + // await txn2.prove(); + // await txn2.sign([senderKey]).send(); + // }catch(e){ + // console.log(e) + // } + + // const GameWeekNum = zkApp.lottogameWeek.get(); + // expect(GameWeekNum).toEqual(Field(1)); + // }); }); diff --git a/src/ZKLottoGame.ts b/src/ZKLottoGame.ts index 1af3a05..71387cc 100644 --- a/src/ZKLottoGame.ts +++ b/src/ZKLottoGame.ts @@ -22,7 +22,9 @@ import { MerkleWitness, } from 'o1js'; -export { LottoNumbers, GameBoard, ZKLottoGame }; +const { OffchainState, OffchainStateCommitments } = Experimental; + +export { offchainState, LottoNumbers, GameBoard, ZKLottoGame }; export class MerkleWitness4 extends MerkleWitness(4) {} @@ -35,55 +37,57 @@ 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 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); + // 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]; + // 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); + // // 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); - } + // // calculate the new root after setting the leaf + // let newLeafHash = Provable.if( + // newLeafIsEmpty, + // emptyLeaf, + // Poseidon.hash(newLeaf) + // ); + // currentRoot = leafWitness.calculateRoot(newLeafHash); + // } - const storedNewRoot = currentRoot; + // const storedNewRoot = currentRoot; - // check the server is storing the stored new root - storedNewRootSignature - .verify(serverPublicKey, [storedNewRoot, storedNewRootNumber]) - .assertTrue(); - rootNumber.assertLessThan(storedNewRootNumber); + // // check the server is storing the stored new root + // storedNewRootSignature + // .verify(serverPublicKey, [storedNewRoot, storedNewRootNumber]) + // .assertTrue(); + // rootNumber.assertLessThan(storedNewRootNumber); - return storedNewRoot; - }; + // return storedNewRoot; + // }; // ============================================================================== @@ -100,19 +104,19 @@ export class MerkleWitness256 extends MerkleWitness(256) {} } - function Optional<T>(type: Provable<T>) { - return class Optional_ extends Struct({ isSome: Bool, value: type }) { - constructor(isSome: boolean | Bool, value: T) { - super({ isSome: Bool(isSome), value }); - } + // function Optional<T>(type: Provable<T>) { + // return class Optional_ extends Struct({ isSome: Bool, value: type }) { + // constructor(isSome: boolean | Bool, value: T) { + // super({ isSome: Bool(isSome), value }); + // } - toFields() { - return Optional_.toFields(this); - } - }; - } + // toFields() { + // return Optional_.toFields(this); + // } + // }; + // } - class OptionalBool extends Optional(GameBoard) {} + // class OptionalBool extends Optional(GameBoard) {} @@ -177,179 +181,280 @@ class LottoNumbers extends Struct({ return Poseidon.hash([this.gmWeek, numbersHash]); } } +class LottoGameMerkleWitness extends MerkleWitness(8) {} + +/* ===================================================== + OFFCHAIN STATE + ===================================================== +*/ + +// //lotto game states +// @state(Bool) lottoGameDone = State<Bool>(); +// @state(Field) lottogameWeek = State<Field>(); +// @state(Field) currentGameStartTime = State<UInt64>(); +// @state(Field) currentGameEndTime = State<UInt64>(); +// @state(Field) gameduration = State<UInt64>(); + +// //Lotto Winning numbers Details +// @state(Field) LottoWeekWinningNumbers = State<LottoNumbers>(); +// @state(Field) LottoWinHistory = State<LottoNumbers[]>(); +// @state(Field) LottoWinHash = State<Field>(); + +const offchainState = OffchainState({ + lottoGameDone: OffchainState.Field(Bool), + lottogameWeek: OffchainState.Field(Field), + currentGameStartTime: OffchainState.Field(UInt64), + currentGameEndTime: OffchainState.Field(UInt64), + gameduration: OffchainState.Field(UInt64), + LottoWeekWinningNumbers: OffchainState.Field(LottoNumbers), + LottoWinHistory: OffchainState.Map(Field, LottoNumbers), + LottoWinHash: OffchainState.Field(LottoNumbers), +}); + +class StateProof extends offchainState.Proof {} + +// 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 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 { + //offchainState + @state(OffchainStateCommitments) offchainState = State( + OffchainStateCommitments.empty() + ); // The board is serialized as a single field element @state(Field) lottoboard = State<GameBoard>(); - //lotto game states - @state(Bool) lottoGameDone = State<Bool>(); - @state(Field) lottogameWeek = State<Field>(); - @state(Field) currentGameTimeStart = State<UInt64>(); - @state(Field) currentGameTimeEnd = State<UInt64>(); - @state(Field) gameduration = State<UInt64>(); + + + //Game Commit Witness + @state(Field) GameStorageTreeRoot = State<Field>(); + @state(Field) PlayersStorageTreeRoot = State<Field>(); - //Lotto Winning numbers Details - @state(Field) LottoWeekWinningNumbers = State<LottoNumbers>(); - @state(Field) LottoWinHistory = State<LottoNumbers[]>(); - @state(Field) LottoWinHash = State<Field>(); + //Lotto Play Entries + @state(Field) LastLottoEntryHash = State<Field>(); - @state(Field) storageTreeRoot = State<Field>(); init() { super.init(); - this.lottoGameDone.set(Bool(true)); - this.lottogameWeek.set(Field(0)); - this.currentGameTimeStart.set(UInt64.from(0)); - 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 + offchainState.fields.lottoGameDone.update({ + from: undefined, + to: true, + }); + offchainState.fields.lottogameWeek.update({ + from: undefined, + to: Field(0), + }); + offchainState.fields.currentGameStartTime.update({ + from: undefined, + to: UInt64.from(0), + }); + offchainState.fields.currentGameEndTime.update({ + from: undefined, + to: this.network.timestamp.get(), + }); + offchainState.fields.gameduration.update({ + from: undefined, + to: UInt64.from(518400), //game duration is 6 days, winning lotto numbers generated on day 7 + }); + + // this.lottoGameDone.set(Bool(true)); + // this.lottogameWeek.set(Field(0)); + // this.currentGameStartTime.set(UInt64.from(0)); + // this.currentGameEndTime.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 = Field(0); + this.GameStorageTreeRoot.set(emptyTreeRoot); + this.PlayersStorageTreeRoot.set(emptyTreeRoot); } - @method async startLottoWeek() { - //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)); - 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.set(newGameEndTime); - - // you can only start a new game if the current game is done - this.lottoGameDone.requireEquals(Bool(true)); - this.lottoGameDone.set(Bool(false)); + @method.returns(Field) + async settleWeek() { + let currentWeek = await offchainState.fields.lottogameWeek.get(); + return (currentWeek).orElse(0n); + } + + // @method async startLottoWeek() { + // //start lotto game week by increasing by 1 week + // //ensure current game week is at least 1 week past previous game week + // const currentGameStartTime = this.currentGameStartTime.get(); + // this.network.timestamp.get().assertGreaterThan(currentGameStartTime.add(86400)); + // this.currentGameStartTime.set(this.network.timestamp.get()) + // //game ends 6 days after new game start. //could round-up timestamp to the hour + // const newGameEndTime = this.currentGameStartTime.get().add(this.gameduration.get()); + // this.currentGameEndTime.set(newGameEndTime); + + // // you can only start a new game if the current game is done + // this.lottoGameDone.requireEquals(Bool(true)); + // this.lottoGameDone.set(Bool(false)); - //set new game week - let gameWeek = this.lottogameWeek.get(); - this.lottogameWeek.requireEquals(gameWeek); - gameWeek = gameWeek.add(Field(1)); - this.lottogameWeek.set(gameWeek); + // //set new game week + // let gameWeek = this.lottogameWeek.get(); + // this.lottogameWeek.requireEquals(gameWeek); + // 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()); - */ + // /*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.currentGameStartTime.get(), + // this.currentGameEndTime.get(), + // this.lottoGameDone.get() + // ); + // this.lottoboard.set(gameBoard); + + // /*let lottoboard = new Lotto(this.lottoboard.get()); + // lottoboard.startNewLotto( + // gameWeek, + // this.currentGameStartTime, + // this.currentGameEndTime, 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); - - //end GameWeek - this.lottoGameDone.requireEquals(Bool(false)); - this.lottoGameDone.set(Bool(true)); - - /*generate lotto winning numbers - random six numbers and set as Field array - Ideally, we are to a more secure verifiable means to generate the winning numbers - possibly using VRF. But for this PoC, we manually set the winning numbers - */ + // } + + // @method async endLottoWeek(winningNums: LottoNumbers, leafWitness: LottoGameMerkleWitness) { + // //start lotto game week by increasing by 1 week + // //ensure current game week is at least 1 week past previous game week + // const currentGameEndTime = this.currentGameEndTime.get(); + // this.network.timestamp.get().assertGreaterThanOrEqual(currentGameEndTime); + + // //end GameWeek + // this.lottoGameDone.requireEquals(Bool(false)); + // this.lottoGameDone.set(Bool(true)); + + // /*generate lotto winning numbers + // random six numbers and set as Field array + // Ideally, we are to a more secure verifiable means to generate the winning numbers + // possibly using VRF. But for this PoC, we manually set the winning numbers + // */ + + // // verify the lotto week to end is same as current week + // this.lottogameWeek.requireEquals(winningNums.gmWeek); + // //set winning details + // this.LottoWeekWinningNumbers.set(winningNums); - // 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); - //add to winning game lotto numbers array - const winHistory = this.LottoWinHistory.get(); - winHistory.push(winningNums); + // // this.LottoWeekWinningNumbers.set(winningHash); - // this.LottoWeekWinningNumbers.set(winningHash); + // //@notice MerkleMap might be a better option? + + // const currentWinningHash = this.LottoWinHash.get(); + // //hash week winning numbers and set to LottoWinHash + // this.LottoWinHash.requireEquals(currentWinningHash); + // const NewWinningHash = winningNums.hash(); + // this.LottoWinHash.set(NewWinningHash); + + // // we fetch the on-chain commitment + // let gameRoot = this.GameStorageTreeRoot.get(); + // this.GameStorageTreeRoot.requireEquals(gameRoot); + + // // we check that the winning numbers for the week is within the committed Merkle Tree + // leafWitness.calculateRoot(currentWinningHash).assertEquals(gameRoot); + + // // we calculate the new Merkle Root, based on new lotto winning numbers + // let newGameRoot = leafWitness.calculateRoot(NewWinningHash); + // this.GameStorageTreeRoot.set(newGameRoot); - //@notice MerkleMap might be a better option? - //hash week winning numbers and set to LottoWinHash - this.LottoWinHash.requireEquals(this.LottoWinHash.get()); - const winningHash = winningNums.hash(); - this.LottoWinHash.set(winningHash); - } + // } - // Lotto Game: - // ---- ---- ---- ---- ---- ---- - // | X | | X | | X | | X | | X | | X | - // ---- ---- ---- ---- ---- ---- - - @method async play( - pubkey: PublicKey, - signature: Signature, - week_: Field, - lottoEntry: LottoNumbers, - ) { - //require game week is active - this.lottogameWeek.requireEquals(week_); - this.lottoGameDone.requireEquals(Bool(false)); + // // Lotto Game: + // // ---- ---- ---- ---- ---- ---- + // // | X | | X | | X | | X | | X | | X | + // // ---- ---- ---- ---- ---- ---- + // @method async play( + // pubkey: PublicKey, + // signature: Signature, + // week_: Field, + // lottoEntry: LottoNumbers, + // leafWitness: LottoGameMerkleWitness, + // ) { + // //require game week is active + // this.lottogameWeek.requireEquals(week_); + // this.lottoGameDone.requireEquals(Bool(false)); + + + // //verify lotto entry is signed by user + // const newLottoEntryHash = lottoEntry.hash(); + // const newLeaf = pubkey.toGroup().toFields().concat(newLottoEntryHash.toFields()); + // signature.verify(pubkey, newLeaf).assertTrue(); + + // /*TO-DO + // add user's lotto numbers entry to merkleTree for the Game week + // */ + // const currentLottoEntryHash = this.LastLottoEntryHash.get(); + // this.LastLottoEntryHash.requireEquals(currentLottoEntryHash); - //verify lotto entry is signed by user - const lottoEntryHash = lottoEntry.hash(); - const newLeaf = pubkey.toGroup().toFields().concat(lottoEntryHash.toFields()); - signature.verify(pubkey, newLeaf).assertTrue(); - /*TO-DO - add user's lotto numbers entry to merkleTree for the Game week - */ + // // we fetch the on-chain root for players lotto entry + // let CurrentPlayerEntryRoot = this.PlayersStorageTreeRoot.get(); + // this.PlayersStorageTreeRoot.requireEquals(CurrentPlayerEntryRoot); + // // we check that the winning numbers for the week is within the committed Merkle Tree + // leafWitness.calculateRoot(currentLottoEntryHash).assertEquals(CurrentPlayerEntryRoot); + // // we calculate the new Player Lotto Entries Merkle Root when a player submits an entry + // let newGameRoot = leafWitness.calculateRoot(newLottoEntryHash); + // this.PlayersStorageTreeRoot.set(newGameRoot); - // 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); - + // //update last lotto entry hash with the new entry hash + // this.LastLottoEntryHash.set(newLottoEntryHash); + // } - } - @method async ClaimWinning( - pubkey: PublicKey, - signature: Signature, - path: MerkleWitness8, - week_: Field, - winningNums: LottoNumbers, - ) { - //proof user is winner of the claim week's lotto + // @method async ClaimWinning( + // pubkey: PublicKey, + // signature: Signature, + // leafWitness: MerkleWitness8, + // week_: Field, + // winningNums: LottoNumbers, + // ) { + // //verify claim request is signed by caller + // const WinningHash = winningNums.hash(); + // const newLeaf = pubkey.toGroup().toFields().concat(WinningHash.toFields()); + // signature.verify(pubkey, newLeaf).assertTrue(); + + + // //verify the user's entry is in the Player Players Storage Tree + // try{ + + // }catch(e){ + // console.log(e); + // } - //transfer winnings to user after successful proof verification + // //transfer winnings to user after successful proof verification + // } + + @method + async settle(proof: StateProof) { + await offchainState.settle(proof); } + + } diff --git a/src/index.ts b/src/index.ts index a47e3b5..48a13cc 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..f1081af 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, LottoGameMerkleWitness, ZKLottoGame } from './ZKLottoGame'; // check command line arg let deployAlias = process.argv[2];