UNPKG

l1-lottery-contracts

Version:

This repo contains smart contracts and related scripts for ZkNoid L1 Lottery proposed [here](https://forums.minaprotocol.com/t/zknoid-l1-lottery/6269)

1,199 lines (1,188 loc) 77.2 kB
'use strict'; var o1js = require('o1js'); const NUMBERS_IN_TICKET = 6; const TICKET_PRICE = o1js.UInt64.from(10 * 10 ** 9); const BLOCK_PER_ROUND = 480; // Approximate blocks per 1 day const SCORE_COEFFICIENTS = [0, 90, 324, 2187, 26244, 590490, 31886460]; // Should be updated with appropriate probability const PRECISION = 1000; const COMMISSION = 100; // 10% commission const mockWinningCombination = [1, 1, 1, 1, 1, 1]; const treasury = o1js.PublicKey.fromBase58('B62qm9d3Ff7DQMpc59wNv9d6R9mSqRKbtHsPs53ZBGr27Y7Cj1poEmc'); function getRandomInt(max) { return Math.floor(Math.random() * max); } class Ticket extends o1js.Struct({ numbers: o1js.Provable.Array(o1js.UInt32, NUMBERS_IN_TICKET), owner: o1js.PublicKey, amount: o1js.UInt64, }) { static from(numbers, owner, amount) { if (numbers.length != NUMBERS_IN_TICKET) { throw new Error(`Wrong amount of numbers. Got: ${numbers.length}, expect: ${NUMBERS_IN_TICKET}`); } return new Ticket({ numbers: numbers.map((number) => o1js.UInt32.from(number)), owner, amount: o1js.UInt64.from(amount), }); } static random(owner) { return new Ticket({ numbers: [...Array(NUMBERS_IN_TICKET)].map(() => o1js.UInt32.from(getRandomInt(9) + 1)), owner, amount: o1js.UInt64.from(1), }); } check() { return this.numbers.reduce((acc, val) => acc.and(val.lessThan(o1js.UInt32.from(10)).and(val.greaterThan(o1js.UInt32.from(0)))), o1js.Bool(true)); } hash() { return o1js.Poseidon.hash(this.numbers .map((number) => number.value) .concat(this.owner.toFields()) .concat(this.amount.value)); } getScore(winningCombination) { let result = o1js.UInt64.from(0); for (let i = 0; i < NUMBERS_IN_TICKET; i++) { result = result.add(o1js.Provable.if(winningCombination[i].equals(this.numbers[i]), o1js.UInt64.from(1), o1js.UInt64.from(0))); } const conditions = [...Array(NUMBERS_IN_TICKET + 1)].map((val, index) => result.equals(o1js.UInt64.from(index))); const values = SCORE_COEFFICIENTS.map((val) => o1js.UInt64.from(val).mul(this.amount)); return o1js.Provable.switch(conditions, o1js.UInt64, values); } } // import * as fs from 'fs'; // import { PackedUInt32Factory } from 'o1js-pack'; const MAX_BITS_PER_FIELD = 254n; const L = 7; // 7 32-bit uints fit in one Field const SIZE_IN_BITS = 32n; function PackingPlant(elementType, l, bitSize) { if (bitSize * BigInt(l) > MAX_BITS_PER_FIELD) { throw new Error(`The Packing Plant is only accepting orders that can fit into one Field, try using MultiPackingPlant`); } class Packed_ extends o1js.Struct({ packed: o1js.Field, }) { constructor(packed) { super({ packed }); } // Must implement these in type-specific implementation static extractField(input) { throw new Error('Must implement extractField'); } static sizeInBits() { throw new Error('Must implement sizeInBits'); } static unpack(f) { throw new Error('Must implement unpack'); } // End /** * * @param unpacked Array of the implemented packed type * @throws if the length of the array is longer than the length of the implementing factory config */ static checkPack(unpacked) { if (unpacked.length > l) { throw new Error(`Input of size ${unpacked.length} is larger than expected size of ${l}`); } } /** * * @param unpacked Array of the implemented packed type, must be shorter than the max allowed, which varies by type, will throw if the input is too long * @returns Field, packed with the information from the unpacked input */ static pack(unpacked) { this.checkPack(unpacked); let f = this.extractField(unpacked[0]); const n = Math.min(unpacked.length, l); for (let i = 1; i < n; i++) { const c = o1js.Field((2n ** this.sizeInBits()) ** BigInt(i)); f = f.add(this.extractField(unpacked[i]).mul(c)); } return f; } /** * * @param f Field, packed with the information, as returned by #pack * @returns Array of bigints, which can be decoded by the implementing class into the final type */ static unpackToBigints(f) { let unpacked = new Array(l); unpacked.fill(0n); let packedN; if (f) { packedN = f.toBigInt(); } else { throw new Error('No Packed Value Provided'); } for (let i = 0; i < l; i++) { unpacked[i] = packedN & ((1n << this.sizeInBits()) - 1n); packedN >>= this.sizeInBits(); } return unpacked; } // NOTE: adding to fields here breaks the proof generation. Probably not overriding it correctly /** * @returns array of single Field element which constitute the packed object */ toFields() { return [this.packed]; } assertEquals(other) { this.packed.assertEquals(other.packed); } } Packed_.type = o1js.provable({ packed: o1js.Field }, {}); Packed_.l = l; Packed_.bitSize = bitSize; return Packed_; } function PackedUInt32Factory(l = L) { class PackedUInt32_ extends PackingPlant(o1js.UInt32, l, SIZE_IN_BITS) { static extractField(input) { return input.value; } static sizeInBits() { return SIZE_IN_BITS; } /** * * @param f Field, packed with the information, as returned by #pack * @returns Array of UInt32 */ static unpack(f) { const unpacked = o1js.Provable.witness(o1js.Provable.Array(o1js.UInt32, l), () => { const unpacked = this.unpackToBigints(f); return unpacked.map((x) => o1js.UInt32.from(x)); }); f.assertEquals(PackedUInt32_.pack(unpacked)); return unpacked; } /** * * @param uint32s Array of UInt32s to be packed * @returns Instance of the implementing class */ static fromUInt32s(uint32s) { const packed = PackedUInt32_.pack(uint32s); return new PackedUInt32_(packed); } /** * * @param bigints Array of bigints to be packed * @returns Instance of the implementing class */ static fromBigInts(bigints) { const uint32s = bigints.map((x) => o1js.UInt32.from(x)); return PackedUInt32_.fromUInt32s(uint32s); } toBigInts() { return PackedUInt32_.unpack(this.packed).map((x) => x.toBigint()); } } return PackedUInt32_; } class NumberPacked extends PackedUInt32Factory() { } function convertToUInt64(value) { let val = o1js.UInt64.Unsafe.fromField(value); o1js.UInt64.check(val); return val; } function convertToUInt32(value) { let val = o1js.UInt32.Unsafe.fromField(value); o1js.UInt32.check(val); return val; } // Copied from https://github.com/o1-labs/o1js/blob/main/src/lib/provable/merkle-map.ts // With a change of merkle tree height. const LENGTH20 = 20; class MerkleMap20Witness extends o1js.Struct({ isLefts: o1js.Provable.Array(o1js.Bool, LENGTH20 - 1), siblings: o1js.Provable.Array(o1js.Field, LENGTH20 - 1), }) { /** * computes the merkle tree root for a given value and the key for this witness * @param value The value to compute the root for. * @returns A tuple of the computed merkle root, and the key that is connected to the path updated by this witness. */ computeRootAndKey(value) { let hash = value; const isLeft = this.isLefts; const siblings = this.siblings; let key = o1js.Field(0); for (let i = 0; i < LENGTH20 - 1; i++) { const left = o1js.Provable.if(isLeft[i], hash, siblings[i]); const right = o1js.Provable.if(isLeft[i], siblings[i], hash); hash = o1js.Poseidon.hash([left, right]); const bit = o1js.Provable.if(isLeft[i], o1js.Field(0), o1js.Field(1)); key = key.mul(2).add(bit); } return [hash, key]; } /** * Computes the merkle tree root for a given value and the key for this witness * @param value The value to compute the root for. * @returns A tuple of the computed merkle root, and the key that is connected to the path updated by this witness. */ computeRootAndKeyV2(value) { // Check that the computed key is less than 2^254, in order to avoid collisions since the Pasta field modulus is smaller than 2^255 this.isLefts[0].assertTrue(); let hash = value; const isLeft = this.isLefts; const siblings = this.siblings; let key = o1js.Field(0); for (let i = 0; i < LENGTH20 - 1; i++) { const left = o1js.Provable.if(isLeft[i], hash, siblings[i]); const right = o1js.Provable.if(isLeft[i], siblings[i], hash); hash = o1js.Poseidon.hash([left, right]); const bit = o1js.Provable.if(isLeft[i], o1js.Field(0), o1js.Field(1)); key = key.mul(2).add(bit); } return [hash, key]; } } class MerkleMap20 { /** * Creates a new, empty Merkle Map. * @returns A new MerkleMap */ constructor() { this.tree = new o1js.MerkleTree(LENGTH20); } _keyToIndex(key) { // console.log('Key: ', key.toString()); // the bit map is reversed to make reconstructing the key during proving more convenient // let bits = BinableFp.toBits(key.toBigInt()).reverse(); // original version let bits = key.toBits(LENGTH20 - 1).reverse(); // Can we just use BinableFP? It is used for constants anyway // console.log(bits.map((bit) => bit.toString())); // console.log(bits.map((bit) => bit.toField().toString())); let n = 0n; for (let i = bits.length - 1; i >= 0; i--) { n = (n << 1n) | BigInt(bits[i].toField().toString()); } return n; } /** * Sets a key of the merkle map to a given value. * @param key The key to set in the map. * @param value The value to set. */ set(key, value) { const index = this._keyToIndex(key); this.tree.setLeaf(index, value); } /** * Returns a value given a key. Values are by default Field(0). * @param key The key to get the value from. * @returns The value stored at the key. */ get(key) { const index = this._keyToIndex(key); return this.tree.getNode(0, index); } /** * Returns the root of the Merkle Map. * @returns The root of the Merkle Map. */ getRoot() { return this.tree.getRoot(); } /** * Returns a circuit-compatible witness (also known as [Merkle Proof or Merkle Witness](https://computersciencewiki.org/index.php/Merkle_proof)) for the given key. * @param key The key to make a witness for. * @returns A MerkleMapWitness, which can be used to assert changes to the MerkleMap, and the witness's key. */ getWitness(key) { const index = this._keyToIndex(key); class MyMerkleWitness extends o1js.MerkleWitness(LENGTH20) { } const witness = new MyMerkleWitness(this.tree.getWitness(index)); // console.log('Witness in getWitness: '); // console.log(witness); return new MerkleMap20Witness({ isLefts: witness.isLeft, siblings: witness.path, }); } } const emptyMap20 = new MerkleMap20(); // https://github.com/o1-labs/o1js-bindings/blob/71f2e698dadcdfc62c76a72248c0df71cfd39d4c/lib/binable.ts#L317 let encoder = new TextEncoder(); function stringToBytes(s) { return [...encoder.encode(s)]; } function prefixToField( // Field: GenericSignableField<Field>, Field, prefix) { let fieldSize = Field.sizeInBytes; if (prefix.length >= fieldSize) throw Error('prefix too long'); let stringBytes = stringToBytes(prefix); return Field.fromBytes(stringBytes.concat(Array(fieldSize - stringBytes.length).fill(0))); } // hashing helpers taken from https://github.com/o1-labs/o1js/blob/72a2779c6728e80e0c9d1462020347c954a0ffb5/src/lib/mina/events.ts#L28 function initialState() { return [o1js.Field(0), o1js.Field(0), o1js.Field(0)]; } function salt(prefix) { return o1js.Poseidon.update(initialState(), [prefixToField(o1js.Field, prefix)]); } function emptyHashWithPrefix(prefix) { return salt(prefix)[0]; } class LotteryAction extends o1js.Struct({ ticket: Ticket, }) { } const actionListAdd = (hash, action) => { return o1js.Poseidon.hashWithPrefix('MinaZkappSeqEvents**', [ hash, o1js.Poseidon.hashWithPrefix('MinaZkappEvent******', LotteryAction.toFields(action)), ]); }; class ActionList extends o1js.MerkleList.create(LotteryAction, actionListAdd, emptyHashWithPrefix('MinaZkappActionsEmpty')) { } const merkleActionsAdd = (hash, actionsHash) => { return o1js.Poseidon.hashWithPrefix('MinaZkappSeqEvents**', [hash, actionsHash]); }; class MerkleActions extends o1js.MerkleList.create(ActionList.provable, (hash, x) => merkleActionsAdd(hash, x.hash), emptyHashWithPrefix('MinaZkappActionStateEmptyElt')) { } class TicketReduceProofPublicInput extends o1js.Struct({ action: LotteryAction, ticketWitness: MerkleMap20Witness, }) { } class TicketReduceProofPublicOutput extends o1js.Struct({ finalState: o1js.Field, newTicketRoot: o1js.Field, newBank: o1js.Field, totalScore: o1js.UInt64, processedActionList: o1js.Field, lastProcessedTicketId: o1js.Field, winningNumbersPacked: o1js.Field, }) { } const init$1 = async (input, winningNumbersPacked) => { return new TicketReduceProofPublicOutput({ finalState: o1js.Reducer.initialActionState, newTicketRoot: emptyMap20.getRoot(), newBank: o1js.Field(0), totalScore: o1js.UInt64.from(0), processedActionList: ActionList.emptyHash, lastProcessedTicketId: o1js.Field(-1), winningNumbersPacked, }); }; const addTicket$1 = async (input, prevProof) => { prevProof.verify(); let [prevTicketRoot, ticketId] = input.ticketWitness.computeRootAndKeyV2(o1js.Field(0)); const expectedTicketId = prevProof.publicOutput.lastProcessedTicketId.add(1); ticketId.assertEquals(expectedTicketId, 'Wrong id for ticket'); prevTicketRoot.assertEquals(prevProof.publicOutput.newTicketRoot, 'Wrong ticket root'); // Update root let [newTicketRoot] = input.ticketWitness.computeRootAndKeyV2(input.action.ticket.hash()); let newBank = prevProof.publicOutput.newBank.add(TICKET_PRICE.mul(input.action.ticket.amount).value); let processedActionList = actionListAdd(prevProof.publicOutput.processedActionList, input.action); let ticketScore = input.action.ticket.getScore(NumberPacked.unpack(prevProof.publicOutput.winningNumbersPacked)); let newTotalScore = prevProof.publicOutput.totalScore.add(ticketScore); return new TicketReduceProofPublicOutput({ finalState: prevProof.publicOutput.finalState, newTicketRoot, newBank, totalScore: newTotalScore, lastProcessedTicketId: expectedTicketId, processedActionList, winningNumbersPacked: prevProof.publicOutput.winningNumbersPacked, }); }; const cutActions = async (input, prevProof) => { prevProof.verify(); let finalState = merkleActionsAdd(prevProof.publicOutput.finalState, prevProof.publicOutput.processedActionList); let processedActionList = ActionList.emptyHash; return new TicketReduceProofPublicOutput({ finalState, newTicketRoot: prevProof.publicOutput.newTicketRoot, newBank: prevProof.publicOutput.newBank, totalScore: prevProof.publicOutput.totalScore, processedActionList, lastProcessedTicketId: prevProof.publicOutput.lastProcessedTicketId, winningNumbersPacked: prevProof.publicOutput.winningNumbersPacked, }); }; const refund = async (input, prevProof) => { prevProof.verify(); const ticket = input.action.ticket; let [prevTicketRoot] = input.ticketWitness.computeRootAndKeyV2(o1js.Field(ticket.hash())); prevTicketRoot.assertEquals(prevProof.publicOutput.newTicketRoot, 'Wrong ticket witness for refund'); let newBank = prevProof.publicOutput.newBank.sub(TICKET_PRICE.mul(ticket.amount).value); let ticketScore = input.action.ticket.getScore(NumberPacked.unpack(prevProof.publicOutput.winningNumbersPacked)); let newTotalScore = prevProof.publicOutput.totalScore.sub(ticketScore); let [newTicketRoot] = input.ticketWitness.computeRootAndKeyV2(o1js.Field(0)); return new TicketReduceProofPublicOutput({ finalState: prevProof.publicOutput.finalState, newTicketRoot, newBank, totalScore: newTotalScore, processedActionList: prevProof.publicOutput.processedActionList, lastProcessedTicketId: prevProof.publicOutput.lastProcessedTicketId, winningNumbersPacked: prevProof.publicOutput.winningNumbersPacked, }); }; /* init: simple initializer, create empty proof addTicket: process next ticket, updates roots of merkle tries. Add actions to processedActionList merkleList cutActions: updates finalState by adding processedActionList to finalState merkle list */ const TicketReduceProgram = o1js.ZkProgram({ name: 'ticket-reduce-program', publicInput: TicketReduceProofPublicInput, publicOutput: TicketReduceProofPublicOutput, methods: { init: { privateInputs: [o1js.Field], async method(input, winningCombinationPacked) { return init$1(input, winningCombinationPacked); }, }, addTicket: { privateInputs: [o1js.SelfProof], async method(input, prevProof) { return addTicket$1(input, prevProof); }, }, cutActions: { privateInputs: [o1js.SelfProof], async method(input, prevProof) { return cutActions(input, prevProof); }, }, refund: { privateInputs: [o1js.SelfProof], async method(input, prevProof) { return refund(input, prevProof); }, }, }, }); class TicketReduceProof extends o1js.ZkProgram.Proof(TicketReduceProgram) { } var __decorate$2 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata$2 = (undefined && undefined.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; class CommitValue extends o1js.Struct({ value: o1js.Field, salt: o1js.Field, }) { hash() { return o1js.Poseidon.hash([this.value, this.salt]); } } // #TODO change to actual address const firstPartyAddress = o1js.PublicKey.fromBase58('B62qryLwDWH5TM4N65Cs9S3jWqyDgC9JYXr5kc87ua8NFB5enpiuT1Y'); const secondPartyAddress = o1js.PublicKey.fromBase58('B62qnSztu3Gp49AR6AWAEUERBVSnoWDFJTki5taYUY7ig9hR1ut1a6r'); class RandomManager extends o1js.SmartContract { constructor() { super(...arguments); // Do not change order of storage, as it would affect deployment via factory this.startSlot = o1js.State(); this.firstCommit = o1js.State(); this.secondCommit = o1js.State(); this.firstValue = o1js.State(); this.secondValue = o1js.State(); this.result = o1js.State(); } async firstPartyCommit(value) { this.firstCommit.getAndRequireEquals().assertEquals(o1js.Field(0)); value.value.assertGreaterThan(0, 'Value should be > 0'); this.checkPermission(firstPartyAddress); this.firstCommit.set(value.hash()); } async secondPartyCommit(value) { this.secondCommit.getAndRequireEquals().assertEquals(o1js.Field(0)); value.value.assertGreaterThan(0, 'Value should be > 0'); this.checkPermission(secondPartyAddress); this.secondCommit.set(value.hash()); } async revealFirstCommit(value) { this.checkRoundPass(); const storedCommit = this.firstCommit.getAndRequireEquals(); storedCommit.assertEquals(value.hash(), 'Reveal failed: Commit does not match stored value.'); this.firstValue.set(value.value); const secondValue = this.secondValue.getAndRequireEquals(); this.produceResultIfAllRevealed(value.value, secondValue); } async revealSecondCommit(value) { this.checkRoundPass(); const storedCommit = this.secondCommit.getAndRequireEquals(); storedCommit.assertEquals(value.hash(), 'Reveal failed: Commit does not match stored value.'); const firstValue = this.firstValue.getAndRequireEquals(); this.secondValue.set(value.value); this.produceResultIfAllRevealed(firstValue, value.value); } produceResultIfAllRevealed(firstValue, secondValue) { const allRevealed = firstValue .greaterThan(o1js.Field(0)) .and(secondValue.greaterThan(o1js.Field(0))); const result = o1js.Poseidon.hash([firstValue, secondValue]); const resultToStore = o1js.Provable.if(allRevealed, result, o1js.Field(0)); this.result.set(resultToStore); } checkPermission(targetAddress) { this.sender.getAndRequireSignatureV2().assertEquals(targetAddress); } checkRoundPass() { const startSlot = this.startSlot.getAndRequireEquals(); this.network.globalSlotSinceGenesis.requireBetween(startSlot.add(BLOCK_PER_ROUND), o1js.UInt32.MAXINT()); } } __decorate$2([ o1js.state(o1js.UInt32), __metadata$2("design:type", Object) ], RandomManager.prototype, "startSlot", void 0); __decorate$2([ o1js.state(o1js.Field), __metadata$2("design:type", Object) ], RandomManager.prototype, "firstCommit", void 0); __decorate$2([ o1js.state(o1js.Field), __metadata$2("design:type", Object) ], RandomManager.prototype, "secondCommit", void 0); __decorate$2([ o1js.state(o1js.Field), __metadata$2("design:type", Object) ], RandomManager.prototype, "firstValue", void 0); __decorate$2([ o1js.state(o1js.Field), __metadata$2("design:type", Object) ], RandomManager.prototype, "secondValue", void 0); __decorate$2([ o1js.state(o1js.Field), __metadata$2("design:type", Object) ], RandomManager.prototype, "result", void 0); __decorate$2([ o1js.method, __metadata$2("design:type", Function), __metadata$2("design:paramtypes", [CommitValue]), __metadata$2("design:returntype", Promise) ], RandomManager.prototype, "firstPartyCommit", null); __decorate$2([ o1js.method, __metadata$2("design:type", Function), __metadata$2("design:paramtypes", [CommitValue]), __metadata$2("design:returntype", Promise) ], RandomManager.prototype, "secondPartyCommit", null); __decorate$2([ o1js.method, __metadata$2("design:type", Function), __metadata$2("design:paramtypes", [CommitValue]), __metadata$2("design:returntype", Promise) ], RandomManager.prototype, "revealFirstCommit", null); __decorate$2([ o1js.method, __metadata$2("design:type", Function), __metadata$2("design:paramtypes", [CommitValue]), __metadata$2("design:returntype", Promise) ], RandomManager.prototype, "revealSecondCommit", null); var __decorate$1 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata$1 = (undefined && undefined.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; const mockResult = NumberPacked.pack(mockWinningCombination.map((v) => o1js.UInt32.from(v))); const generateNumbersSeed = (seed) => { let bits = seed.toBits(); const numbers = [...Array(6)].map((_, i) => { let res64 = o1js.UInt64.from(0); res64.value = o1js.Field.fromBits(bits.slice(42 * i, 42 * (i + 1))); res64 = res64.mod(9).add(1); let res = o1js.UInt32.from(0); res.value = res64.value; return res; }); return numbers; }; const emptyMap20Root = new MerkleMap20().getRoot(); class BuyTicketEvent extends o1js.Struct({ ticket: Ticket, }) { } class ProduceResultEvent extends o1js.Struct({ result: o1js.Field, totalScore: o1js.UInt64, bank: o1js.Field, }) { } class GetRewardEvent extends o1js.Struct({ ticket: Ticket, ticketId: o1js.Field, }) { } class RefundEvent extends o1js.Struct({ ticketId: o1js.Field, ticket: Ticket, }) { } class ReduceEvent extends o1js.Struct({}) { } class PLottery extends o1js.SmartContract { constructor() { super(...arguments); this.reducer = o1js.Reducer({ actionType: LotteryAction }); this.events = { 'buy-ticket': BuyTicketEvent, 'produce-result': ProduceResultEvent, 'get-reward': GetRewardEvent, 'get-refund': RefundEvent, reduce: ReduceEvent, }; // Do not change order of storage, as it would affect deployment via factory // !!!!First slot for outer initializer this.randomManager = o1js.State(); // Stores block of deploy this.startSlot = o1js.State(); // Stores merkle map with all tickets, that user have bought this.ticketRoot = o1js.State(); // Stores nullifier tree root for tickets, so one ticket can't be used twice this.ticketNullifier = o1js.State(); // Stores merkle map with total bank for each round. this.bank = o1js.State(); // Stores merkle map with wining combination for each rounds this.result = o1js.State(); this.totalScore = o1js.State(); } init() { super.init(); /// !!!! This contracts is deployed from factory. No init call there this.ticketRoot.set(emptyMap20Root); this.ticketNullifier.set(emptyMap20Root); this.startSlot.set(this.network.globalSlotSinceGenesis.getAndRequireEquals()); this.account.permissions.set({ ...o1js.Permissions.default(), setVerificationKey: o1js.Permissions.VerificationKey.impossibleDuringCurrentVersion(), }); } /** * @notice Set verification key for account * @dev verification key can be updated only if Mina hardfork happen. It allows zkApp to be live after Mina hardfork * @param vk Verification key */ async updateVerificationKey(vk) { this.account.verificationKey.set(vk); } /** * @notice Allows a user to buy a lottery ticket for a specific round. * @dev No ticket merkle tree update happens here. Only action is dispatched. * * @param ticket The lottery ticket being purchased. * * @require The ticket must be valid as per the ticket validity check. * @require The specified round must be the current lottery round. * * @event buy-ticket Emitted when a ticket is successfully purchased. */ async buyTicket(ticket) { // Ticket validity check ticket.check().assertTrue(); ticket.amount.assertGreaterThan(o1js.UInt64.from(0), 'Ticket amount should be positive'); // Round check this.checkCurrentRound(); // Take ticket price from user let senderUpdate = o1js.AccountUpdate.createSigned(this.sender.getAndRequireSignatureV2()); senderUpdate.send({ to: this, amount: TICKET_PRICE.mul(ticket.amount) }); // Dispatch action and emit event this.reducer.dispatch(new LotteryAction({ ticket, })); this.emitEvent('buy-ticket', new BuyTicketEvent({ ticket, })); } /** * @notice Reduce tickets that lies as actions. * @dev This function verifies the proof and ensures that the contract's state matches the state described in the proof. * It then updates the tickets merkle tree, populating it with new tickets. * * @param reduceProof The proof that validates the ticket reduction process and contains the new contract state. * * @require The proof must be valid and successfully verified. * @require The processed action list in the proof must be empty, indicating that all actions have been processed. * @require The contract's last processed state must match the initial state in the proof. * @require The contract's action state must match the final state in the proof. * @require The contract's last processed ticket ID must match the initial ticket ID in the proof. * * @event reduce Emitted when the tickets are successfully reduced and the contract state is updated. */ async reduceTicketsAndProduceResult(reduceProof) { this.result .getAndRequireEquals() .assertEquals(o1js.Field(0), 'Already produced'); // Only after round is passed this.checkRoundPass(o1js.UInt32.from(1)); // Get random value const RM = new RandomManager(this.randomManager.getAndRequireEquals()); const rmValue = RM.result.getAndRequireEquals(); rmValue.assertGreaterThan(o1js.Field(0), 'Random value was not generated yet'); this.approve(RM.self); let winningNumbers = generateNumbersSeed(rmValue); let winningNumbersPacked = NumberPacked.pack(winningNumbers); this.checkReduceProof(reduceProof, winningNumbersPacked); const bankValue = reduceProof.publicOutput.newBank; this.send({ to: treasury, amount: convertToUInt64(bankValue.mul(COMMISSION).div(PRECISION)), }); const newBankValue = bankValue.mul(PRECISION - COMMISSION).div(PRECISION); // Update onchain values this.ticketRoot.set(reduceProof.publicOutput.newTicketRoot); this.bank.set(newBankValue); this.totalScore.set(reduceProof.publicOutput.totalScore); this.result.set(winningNumbersPacked); // Emit event this.emitEvent('produce-result', new ProduceResultEvent({ result: winningNumbersPacked, bank: newBankValue, totalScore: reduceProof.publicOutput.totalScore, })); } // If random manager can't produce value async emergencyReduceTickets(reduceProof) { // Allow only after 2 round pass this.checkRoundPass(o1js.UInt32.from(2)); // And no result produce this.result.getAndRequireEquals().assertEquals(o1js.Field(0)); this.checkReduceProof(reduceProof, o1js.Field(0)); this.ticketRoot.set(reduceProof.publicOutput.newTicketRoot); } checkReduceProof(reduceProof, winningNumbersPacked) { // Check proof validity reduceProof.verify(); // Check that all actions was processed. reduceProof.publicOutput.processedActionList.assertEquals(ActionList.emptyHash, 'Proof is not complete. Call cutActions first'); // Check random value reduceProof.publicOutput.winningNumbersPacked.assertEquals(winningNumbersPacked, 'Wrong winning combination used in proof'); // If emergency reduce was previously call then check that ticketRoot is equal to newTicketRoot const currentRoot = this.ticketRoot.getAndRequireEquals(); currentRoot .equals(new MerkleMap20().getRoot()) .or(currentRoot.equals(reduceProof.publicOutput.newTicketRoot)) .assertTrue('Wrong ticket root'); // Check that actionState is equal to actionState on proof this.account.actionState .getAndRequireEquals() .assertEquals(reduceProof.publicOutput.finalState); } // Update refund natspec /** * @notice Processes a refund for a lottery ticket if the result for the round was not generated within 2 days. * @dev This function ensures that the ticket owner is the one requesting the refund, verifies the ticket's validity * in the Merkle maps, checks that the result for the round is zero, and processes the refund after verifying * and updating the necessary states. * * @param ticket The lottery ticket for which the refund is being requested. * @param ticketWitness Witness of the ticket in the ticketMap tree. * * @require The sender must be the owner of the ticket. * @require The ticket must exist in the Merkle map as verified by the round and ticket witnesses. * @require The result for the round must be zero to be eligible for a refund. * @require The refund can only be processed after approximately two days since the round finished. * * @event get-refund Emitted when a refund is successfully processed and the ticket price is returned to the user. */ async refund(ticket, ticketWitness) { // Check that owner trying to claim ticket.owner.assertEquals(this.sender.getAndRequireSignatureV2()); const result = this.result.getAndRequireEquals(); result.assertEquals(o1js.Field(0), 'Result for this round is not zero'); // Check ticket in merkle map and set ticket to zero const [ticketRoot, ticketId] = ticketWitness.computeRootAndKeyV2(ticket.hash()); this.ticketRoot .getAndRequireEquals() .assertEquals(ticketRoot, 'Wrong ticket witness'); const [newTicketRoot] = ticketWitness.computeRootAndKeyV2(o1js.Field(0)); this.ticketRoot.set(newTicketRoot); // Can call refund after ~ 2 days after round finished this.checkRoundPass(o1js.UInt32.from(2)); // Check and update bank witness const totalTicketPrice = ticket.amount.mul(TICKET_PRICE); const bankValue = this.bank.getAndRequireEquals(); const newBankValue = bankValue.sub(totalTicketPrice.value); this.bank.set(newBankValue); // Send ticket price back to user this.send({ to: ticket.owner, amount: totalTicketPrice, }); this.emitEvent('get-refund', new RefundEvent({ ticketId, ticket, })); } /** * @notice Claims the reward for a winning lottery ticket. * @dev This function calculate ticket score, totalScore is obtained from DistributionProof, * and then sends appropriate potion of bank to ticket owner. Finally it nullify the ticket. * * @param ticket The lottery ticket for which the reward is being claimed. * @param ticketWitness Witness of the ticket in the ticketMap tree. * @param nullifierWitness The Merkle proof witness for the nullifier tree. * * @require The sender must be the owner of the ticket. * @require The distribution proof must be valid and match the round's ticket root and winning numbers. * @require The ticket must exist in the Merkle map as verified by the round and ticket witnesses. * @require The actions for the round must be reduced before claiming the reward. * * @event get-reward Emitted when the reward is successfully claimed and transferred to the ticket owner. */ async getReward(ticket, ticketWitness, nullifierWitness) { // Check ticket in tree const [ticketRoot, ticketId] = ticketWitness.computeRootAndKeyV2(ticket.hash()); this.ticketRoot .getAndRequireEquals() .assertEquals(ticketRoot, 'Wrong ticket witness'); const winningNumbers = this.result.getAndRequireEquals(); winningNumbers.assertGreaterThan(o1js.Field(0), 'Winning number is not generated yet'); // Compute score using winning ticket const score = ticket.getScore(NumberPacked.unpack(winningNumbers)); const totalScore = this.totalScore.getAndRequireEquals(); const bank = this.bank.getAndRequireEquals(); const payAmount = convertToUInt64(bank).mul(score).div(totalScore); this.send({ to: ticket.owner, amount: payAmount, }); // Add ticket to nullifier this.checkAndUpdateNullifier(nullifierWitness, ticketId, o1js.Field(0), o1js.Field.from(1)); this.emitEvent('get-reward', new GetRewardEvent({ ticket, ticketId, })); } /** * @notice Check that execution is happening within provided round * * * @require globalSlotSinceGenesis to be within range of round */ checkCurrentRound() { const startSlot = this.startSlot.getAndRequireEquals(); this.network.globalSlotSinceGenesis.requireBetween(startSlot, startSlot.add(BLOCK_PER_ROUND).sub(1)); } /** * @notice Check that execution is happening after provided round * * @param amount Amounts of rounds to pass to check * * @require globalSlotSinceGenesis to be greater then last slot of given number */ checkRoundPass(amount) { const startSlot = this.startSlot.getAndRequireEquals(); this.network.globalSlotSinceGenesis.requireBetween(startSlot.add(amount.mul(BLOCK_PER_ROUND)), o1js.UInt32.MAXINT()); } /** * @notice Check validity of merkle map witness for nullifier tree and then updates tree with new value. * * @param witness Merkle map witness for nullifier tree. * @param key Round number, that will be compared with <witness> key. * @param curValue Value of nullifier to be checked. * @param newValue New value that should be store in tree. * * @returns key of <witness> */ checkAndUpdateNullifier(witness, key, curValue, newValue) { return this.checkAndUpdateMap(this.ticketNullifier, witness, key, curValue, newValue); } /** * @notice General method that allows to check and update onchain merkle trees roots * * @param state On-chain state, that should be updated. * @param witness Merkle map witness. * @param key Key that will be compared with <witness> key. * @param curValue Value to be checked. * @param newValue New value that should be store in tree. * * @returns key of <witness> */ checkAndUpdateMap(state, witness, key, curValue, newValue) { let checkRes = this.checkMap(state, witness, key, curValue); const [newRoot] = witness.computeRootAndKeyV2(newValue); state.set(newRoot); return checkRes; } /** * @notice General method that allows to check onchain merkle trees roots * * @param state On-chain state, that should be updated. * @param witness Merkle map witness. * @param key Key that will be compared with <witness> key. * @param curValue Value to be checked. * * @returns key of <witness> */ checkMap(state, witness, key, curValue) { const curRoot = state.getAndRequireEquals(); const [prevRoot, witnessKey] = witness.computeRootAndKeyV2(curValue); curRoot.assertEquals(prevRoot, 'Wrong witness'); witnessKey.assertEquals(key, 'Wrong key'); return { key: witnessKey, }; } } __decorate$1([ o1js.state(o1js.PublicKey), __metadata$1("design:type", Object) ], PLottery.prototype, "randomManager", void 0); __decorate$1([ o1js.state(o1js.UInt32), __metadata$1("design:type", Object) ], PLottery.prototype, "startSlot", void 0); __decorate$1([ o1js.state(o1js.Field), __metadata$1("design:type", Object) ], PLottery.prototype, "ticketRoot", void 0); __decorate$1([ o1js.state(o1js.Field), __metadata$1("design:type", Object) ], PLottery.prototype, "ticketNullifier", void 0); __decorate$1([ o1js.state(o1js.Field), __metadata$1("design:type", Object) ], PLottery.prototype, "bank", void 0); __decorate$1([ o1js.state(o1js.Field), __metadata$1("design:type", Object) ], PLottery.prototype, "result", void 0); __decorate$1([ o1js.state(o1js.UInt64), __metadata$1("design:type", Object) ], PLottery.prototype, "totalScore", void 0); __decorate$1([ o1js.method, __metadata$1("design:type", Function), __metadata$1("design:paramtypes", [o1js.VerificationKey]), __metadata$1("design:returntype", Promise) ], PLottery.prototype, "updateVerificationKey", null); __decorate$1([ o1js.method, __metadata$1("design:type", Function), __metadata$1("design:paramtypes", [Ticket]), __metadata$1("design:returntype", Promise) ], PLottery.prototype, "buyTicket", null); __decorate$1([ o1js.method, __metadata$1("design:type", Function), __metadata$1("design:paramtypes", [TicketReduceProof]), __metadata$1("design:returntype", Promise) ], PLottery.prototype, "reduceTicketsAndProduceResult", null); __decorate$1([ o1js.method, __metadata$1("design:type", Function), __metadata$1("design:paramtypes", [TicketReduceProof]), __metadata$1("design:returntype", Promise) ], PLottery.prototype, "emergencyReduceTickets", null); __decorate$1([ o1js.method, __metadata$1("design:type", Function), __metadata$1("design:paramtypes", [Ticket, MerkleMap20Witness]), __metadata$1("design:returntype", Promise) ], PLottery.prototype, "refund", null); __decorate$1([ o1js.method, __metadata$1("design:type", Function), __metadata$1("design:paramtypes", [Ticket, MerkleMap20Witness, MerkleMap20Witness]), __metadata$1("design:returntype", Promise) ], PLottery.prototype, "getReward", null); class BaseStateManager { constructor(contract, isMock = true, shouldUpdateState = false) { this.contract = contract; this.ticketMap = new MerkleMap20(); this.lastTicketInRound = 0; this.roundTickets = []; this.ticketNullifierMap = new MerkleMap20(); this.isMock = isMock; this.shouldUpdateState = shouldUpdateState; } addTicket(ticket) { throw Error('Add ticket is not implemented'); } // async getDP(round: number): Promise<DistributionProof> { // if (this.dpProof) { // return this.dpProof; // } // const reducedTickets = (await this.contract.reducer.fetchActions()).flat(1); // const winningCombination = this.contract.result.get(); // let curMap = new MerkleMap20(); // let input = new DistributionProofPublicInput({ // winningCombination, // ticket: Ticket.random(PublicKey.empty()), // valueWitness: this.ticketMap.getWitness(Field(0)), // }); // let curProof = this.isMock // ? await mockProof(await init(input), DistributionProof, input) // : await DistributionProgram.init(input); // for (let i = 0; i < reducedTickets.length; i++) { // const ticket = reducedTickets[i].ticket; // if (!ticket) { // // Skip refunded tickets // continue; // } // const input = new DistributionProofPublicInput({ // winningCombination, // ticket: ticket, // valueWitness: curMap.getWitness(Field(i)), // }); // curMap.set(Field(i), ticket.hash()); // if (this.isMock) { // curProof = await mockProof( // await addTicket(input, curProof), // DistributionProof, // input // ); // } else { // curProof = await DistributionProgram.addTicket(input, curProof); // } // } // this.dpProof = curProof; // return curProof; // } // Changes value of nullifier! async getReward(round, ticket, roundDP = undefined, ticketIndex = 1 // If two or more same tickets presented ) { const ticketHash = ticket.hash(); let ticketWitness; // Find ticket in tree let ticketId = 0; for (; ticketId < this.lastTicketInRound; ticketId++) { if (this.ticketMap.get(o1js.Field(ticketId)).equals(ticketHash).toBoolean()) { ticketIndex--; if (ticketIndex == 0) { ticketWitness = this.ticketMap.getWitness(o1js.Field.from(ticketId)); break; } } } if (!ticketWitness) { throw Error(`No such ticket in round ${round}`); } // const dp = !roundDP // ? await this.getDP(round) // : await DistributionProof.fromJSON(roundDP); const nullifierWitness = this.ticketNullifierMap.getWitness(o1js.Field.from(ticketId)); if (this.shouldUpdateState) { this.ticketNullifierMap.set(o1js.Field.from(ticketId), o1js.Field(1)); } return { ticketWitness, // dp, nullifierWitness, }; } async getRewardByTicketId(ticketId) { const ticketWitness = this.ticketMap.getWitness(o1js.Field.from(ticketId)); const nullifierWitness = this.ticketNullifierMap.getWitness(o1js.Field.from(ticketId)); if (this.shouldUpdateState) { this.ticketNullifierMap.set(o1js.Field.from(ticketId), o1js.Field(1)); } return { ticketWitness, nullifierWitness, }; } async getRefund(round, ticket) { const ticketHash = ticket.hash(); let ticketWitness; // Find ticket in tree let ticketId = 0; for (; ticketId < this.lastTicketInRound; ticketId++) { if (this.ticketMap.get(o1js.Field(ticketId)).equals(ticketHash).toBoolean()) { ticketWitness = this.ticketMap.getWitness(o1js.Field.from(ticketId)); break; } } if (!ticketWitness) { throw Error(`No such ticket in round ${round}`); } if (this.shouldUpdateState) { console.log('Update state'); this.ticketMap.set(o1js.Field(ticketId), o1js.Field(0)); this.roundTickets[ticketId] = undefined; } return { ticketWitness, }; } } async function mockProof(publicOutput, ProofType, publicInput) { // const [, proof] = Pickles.proofOfBase64(await dummyBase64Proof(), 2); return new ProofType({ proof: null, maxProofsVerified: 2, publicInput, publicOutput, }); } class PStateManager extends BaseStateManager { constructor(plottery, isMock = true, shouldUpdateState = false) { super(plottery, isMock, shouldUpdateState); this.contract = plottery; this.processedTicketData = { ticketId: -1, round: 0, }; } addTicket(ticket, forceUpdate = false) { if (this.shouldUpdateState || forceUpdate) { this.ticketMap.set(o1js.Field.from(this.lastTicketInRound), ticket.hash()); } this.roundTickets.push(ticket); this.lastTicketInRound++; } async removeLastTicket() { this.roundTickets.pop(); this.lastTicketInRound--; this.ticketMap.set(o1js.Field.from(this.lastTicketInRound - 1), o1js.Field(0)); // ? } async reduceTickets(winningNumberPacked, actionLists, updateState = true) { let addedTicketInfo = []; if (!actionLists) { actionLists = await this.contract.reducer.fetchActions({}); } // All this params can be random for init function, because init do not use them let input = new TicketReduceProofPublicInput({ action: new LotteryAction({ ticket: Ticket.random(this.contract.address), }), ticketWitness: new MerkleMap20().getWitness(o1js.Field(0)), }); let curProof = this.isMock ? await mockProof(await init$1(input, winningNumberPacked), TicketReduceProof, input) : await TicketReduceProgram.init(input, winningNumberPacked); let ticketId = 0; for (let actionList of actionLists) { for (let action of actionList) { console.log(`Process ticket: <${ticketId}>`); input = new TicketReduceProofPublicInput({ action: action, ticketWitness: this.ticketMap.getWitness(o1js.Field(ticketId)), }); curProof = this.isMock ? await mockProof(await addTicket$1(input, curProof), TicketReduceProof, input) : await TicketReduceProgram.addTicket(input, curProof); this.addTicket(action.ticket, true); addedTicketInfo.push({});