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)

354 lines 16.5 kB
import { AccountUpdate, Cache, Field, Mina, Poseidon, PrivateKey, UInt32, UInt64, } from 'o1js'; import { generateNumbersSeed, PLottery } from '../PLottery'; import { Ticket } from '../Structs/Ticket'; import { NumberPacked, convertToUInt64 } from '../util'; import { BLOCK_PER_ROUND, COMMISSION, PRECISION, TICKET_PRICE, treasury, } from '../constants'; import { dummyBase64Proof } from 'o1js/dist/node/lib/proof-system/zkprogram'; import { Pickles } from 'o1js/dist/node/snarky'; import { TicketReduceProgram } from '../Proofs/TicketReduceProof'; import { CommitValue } from '../Random/RandomManager'; import { MockedRandomManager } from './MockedContracts/MockedRandomManager'; import { FactoryManager } from '../StateManager/FactoryStateManager'; import { MerkleMap20 } from '../Structs/CustomMerkleMap'; import { MockedPlotteryFactory } from './MockedContracts/MockedFactory'; export async function mockProof(publicOutput, ProofType, publicInput) { const [, proof] = Pickles.proofOfBase64(await dummyBase64Proof(), 2); return new ProofType({ proof: proof, maxProofsVerified: 2, publicInput, publicOutput, }); } const testCommitValue = { v1: new CommitValue({ value: Field(123), salt: Field(456), }), v2: new CommitValue({ value: Field(654), salt: Field(117), }), }; const testWinningCombination = generateNumbersSeed(Poseidon.hash([testCommitValue.v1.value, testCommitValue.v2.value])); const ROUNDS = 10; let proofsEnabled = false; describe('Add', () => { let deployerAccount, deployerKey, senderAccount, restAccs, users, senderKey, factoryPrivateKey, factoryAddress, factory, factoryManager, plotteries, randomManagers, checkConsistency, mineNBlocks, commitValue, produceResultInRM, deployRound, getWinningCombinationPacked; beforeAll(async () => { if (proofsEnabled) { // console.log(`Compiling distribution program proof`); // await DistributionProgram.compile({ // cache: Cache.FileSystem('./cache'), // }); console.log(`Compiling reduce program proof`); await TicketReduceProgram.compile({ cache: Cache.FileSystem('./cache'), }); console.log(`Compiling MockLottery`); throw Error('Currently there is no option to compile PLottery'); } }); beforeEach(async () => { const Local = await Mina.LocalBlockchain({ proofsEnabled }); Local.addAccount(treasury, '100'); Mina.setActiveInstance(Local); [deployerAccount, senderAccount, ...restAccs] = Local.testAccounts; users = restAccs.slice(0, 7); deployerKey = deployerAccount.key; senderKey = senderAccount.key; factoryPrivateKey = PrivateKey.random(); factoryAddress = factoryPrivateKey.toPublicKey(); factory = new MockedPlotteryFactory(factoryAddress); plotteries = {}; randomManagers = {}; // rmStateManager = new RandomManagerManager(); factoryManager = new FactoryManager(true, true); mineNBlocks = (n) => { let curAmount = Local.getNetworkState().globalSlotSinceGenesis; Local.setGlobalSlot(curAmount.add(n)); }; checkConsistency = () => { // expect(lottery.ticketRoot.get()).toEqual(state.ticketMap.getRoot()); // expect(lottery.ticketNullifier.get()).toEqual( // state.ticketNullifierMap.getRoot() // ); // expect(lottery.bankRoot.get()).toEqual(state.bankMap.getRoot()); // expect(lottery.roundResultRoot.get()).toEqual( // state.roundResultMap.getRoot() // ); }; commitValue = async (round) => { let randomManager = randomManagers[round]; let rmStateManager = factoryManager.randomManagers[round]; let tx = await Mina.transaction(deployerAccount, async () => { await randomManager.firstPartyCommit(testCommitValue.v1); }); await tx.prove(); await tx.sign([deployerKey]).send(); let tx2 = await Mina.transaction(deployerAccount, async () => { await randomManager.secondPartyCommit(testCommitValue.v2); }); await tx2.prove(); await tx2.sign([deployerKey]).send(); // rmStateManager.addCommit(testCommitValue); }; produceResultInRM = async (round) => { let randomManager = randomManagers[round]; let tx2 = await Mina.transaction(deployerAccount, async () => { await randomManager.revealFirstCommit(testCommitValue.v1); }); await tx2.prove(); await tx2.sign([deployerKey]).send(); let tx3 = await Mina.transaction(deployerAccount, async () => { await randomManager.revealSecondCommit(testCommitValue.v2); }); await tx3.prove(); await tx3.sign([deployerKey]).send(); }; deployRound = async (round) => { const roundWitness = factoryManager.roundsMap.getWitness(Field(round)); const randomManagerKeypair = PrivateKey.randomKeypair(); const plotteryKeypair = PrivateKey.randomKeypair(); const tx = await Mina.transaction(deployerAccount, async () => { AccountUpdate.fundNewAccount(deployerAccount); AccountUpdate.fundNewAccount(deployerAccount); await factory.deployRound(roundWitness, randomManagerKeypair.publicKey, plotteryKeypair.publicKey); }); await tx.prove(); await tx .sign([ deployerKey, randomManagerKeypair.privateKey, plotteryKeypair.privateKey, ]) .send(); factoryManager.addDeploy(round, randomManagerKeypair.publicKey, plotteryKeypair.publicKey); const plotteryContract = new PLottery(plotteryKeypair.publicKey); const randomManagerContract = new MockedRandomManager(randomManagerKeypair.publicKey); return { plotteryContract, randomManagerContract, }; }; getWinningCombinationPacked = (round) => { const rmResult = randomManagers[round].result.get(); const winningNumbers = generateNumbersSeed(rmResult); const winningNumbersPacked = NumberPacked.pack(winningNumbers); return winningNumbersPacked; }; }); async function localDeploy() { const txn = await Mina.transaction(deployerAccount, async () => { AccountUpdate.fundNewAccount(deployerAccount); await factory.deploy(); }); await txn.prove(); // this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization await txn.sign([deployerKey, factoryPrivateKey]).send(); for (let i = 0; i < ROUNDS; i++) { const { plotteryContract, randomManagerContract } = await deployRound(i); plotteries[i] = plotteryContract; randomManagers[i] = randomManagerContract; } } it('check plottery initial values', async () => { await localDeploy(); for (let i = 0; i < ROUNDS; i++) { let plottery = plotteries[i]; expect(plottery.randomManager.get()).toEqual(randomManagers[i].address); expect(plottery.startSlot.get()).toEqual(UInt32.from(BLOCK_PER_ROUND * i)); expect(plottery.ticketRoot.get()).toEqual(new MerkleMap20().getRoot()); expect(plottery.ticketNullifier.get()).toEqual(new MerkleMap20().getRoot()); expect(plottery.bank.get()).toEqual(Field(0)); expect(plottery.result.get()).toEqual(Field(0)); expect(plottery.totalScore.get()).toEqual(UInt64.from(0)); } }); it('one user case', async () => { await localDeploy(); let curRound = 0; const balanceBefore = Mina.getBalance(senderAccount); // Buy ticket const ticket = Ticket.from(testWinningCombination, senderAccount, 1); let state = factoryManager.plotteryManagers[curRound]; let lottery = plotteries[curRound]; console.log('Buying ticket'); let tx = await Mina.transaction(senderAccount, async () => { await lottery.buyTicket(ticket); }); await tx.prove(); await tx.sign([senderKey]).send(); const balanceAfter = Mina.getBalance(senderAccount); expect(balanceBefore.sub(balanceAfter)).toEqual(TICKET_PRICE); checkConsistency(); console.log('Commiting value'); // Wait next round mineNBlocks(BLOCK_PER_ROUND); // Commit value for random await commitValue(curRound); console.log('Revealing'); await produceResultInRM(curRound); // Produce result console.log('Produced in RM'); let winningNumbers = getWinningCombinationPacked(curRound); console.log(`Before reduce`); let reduceProof = await state.reduceTickets(winningNumbers); console.log(`Here`); let tx2 = await Mina.transaction(senderAccount, async () => { await lottery.reduceTicketsAndProduceResult(reduceProof); }); await tx2.prove(); await tx2.sign([senderKey]).send(); checkConsistency(); // Get reward const rp = await state.getReward(curRound, ticket); let faultTicket = Ticket.fromFields(Ticket.toFields(ticket)); faultTicket.owner = restAccs[0]; await expect(Mina.transaction(restAccs[0], async () => { await lottery.getReward(faultTicket, rp.ticketWitness, rp.nullifierWitness); })).rejects.toThrow('Field.assertEquals()'); // Valid transaction let tx3 = await Mina.transaction(senderAccount, async () => { await lottery.getReward(ticket, rp.ticketWitness, rp.nullifierWitness); }); await tx3.prove(); await tx3.sign([senderKey]).send(); checkConsistency(); }); it('Refund check', async () => { await localDeploy(); let curRound = 0; let lottery = plotteries[curRound]; let state = factoryManager.plotteryManagers[curRound]; const balanceBefore = Mina.getBalance(senderAccount); // Buy ticket const ticket = Ticket.from(testWinningCombination, senderAccount, 1); let tx = await Mina.transaction(senderAccount, async () => { await lottery.buyTicket(ticket); }); await tx.prove(); await tx.sign([senderKey]).send(); const balanceAfter = Mina.getBalance(senderAccount); expect(balanceBefore.sub(balanceAfter)).toEqual(TICKET_PRICE); checkConsistency(); // Buy second ticket let tx1_1 = await Mina.transaction(senderAccount, async () => { await lottery.buyTicket(ticket); }); await tx1_1.prove(); await tx1_1.sign([senderKey]).send(); // Wait 3 more rounds mineNBlocks(3 * BLOCK_PER_ROUND + 1); // Commit value for random await commitValue(curRound); // Reduce tickets let reduceProof = await state.reduceTickets(Field(0)); let tx2_1 = await Mina.transaction(senderAccount, async () => { await lottery.emergencyReduceTickets(reduceProof); }); await tx2_1.prove(); await tx2_1.sign([senderKey]).send(); checkConsistency(); // Get refund let { ticketWitness } = await state.getRefund(0, ticket); const balanceBefore2 = Mina.getBalance(senderAccount); let tx3 = await Mina.transaction(senderAccount, async () => { await lottery.refund(ticket, ticketWitness); }); await tx3.prove(); await tx3.sign([senderKey]).send(); checkConsistency(); const balanceAfter2 = Mina.getBalance(senderAccount); expect(balanceAfter2.sub(balanceBefore2)).toEqual(TICKET_PRICE); // Produce random value await produceResultInRM(curRound); // Produce result state.ticketMap = new MerkleMap20(); state.lastTicketInRound = 0; let winningNumbers = getWinningCombinationPacked(curRound); reduceProof = await state.reduceTickets(winningNumbers); let tx4 = await Mina.transaction(senderAccount, async () => { await lottery.reduceTicketsAndProduceResult(reduceProof); }); await tx4.prove(); await tx4.sign([senderKey]).send(); checkConsistency(); const balanceBefore3 = Mina.getBalance(senderAccount); // Get reward for second transaction const rp = await state.getReward(curRound, ticket); let tx5 = await Mina.transaction(senderAccount, async () => { await lottery.getReward(ticket, rp.ticketWitness, rp.nullifierWitness); }); await tx5.prove(); await tx5.sign([senderKey]).send(); checkConsistency(); const balanceAfter3 = Mina.getBalance(senderAccount); expect(balanceAfter3.sub(balanceBefore3)).toEqual(TICKET_PRICE.mul(PRECISION - COMMISSION).div(PRECISION)); }); it('Multiple round test', async () => { await localDeploy(); const amountOfRounds = 5; const amountOfTickets = 10; for (let round = 0; round < amountOfRounds; round++) { let lottery = plotteries[round]; let state = factoryManager.plotteryManagers[round]; console.log(`Process: ${round} round`); let tickets = []; for (let j = 0; j < amountOfTickets; j++) { let ticket = Ticket.random(users[j % users.length]); tickets.push({ owner: users[j % users.length], ticket, }); } // For each ticket - buy ticket for (let j = 0; j < amountOfTickets; j++) { let ticket = tickets[j]; const balanceBefore = Mina.getBalance(ticket.owner); let tx = await Mina.transaction(ticket.owner, async () => { await lottery.buyTicket(ticket.ticket); }); await tx.prove(); await tx.sign([ticket.owner.key]).send(); const balanceAfter = Mina.getBalance(ticket.owner); expect(balanceBefore.sub(balanceAfter)).toEqual(TICKET_PRICE); checkConsistency(); } // Wait for the end of round mineNBlocks(BLOCK_PER_ROUND); // Commit value for random await commitValue(round); // Produce random value await produceResultInRM(round); // Produce result let winningNumbers = getWinningCombinationPacked(round); let reduceProof = await state.reduceTickets(winningNumbers); let tx2 = await Mina.transaction(senderAccount, async () => { await lottery.reduceTicketsAndProduceResult(reduceProof); }); await tx2.prove(); await tx2.sign([senderKey]).send(); checkConsistency(); const bank = convertToUInt64(lottery.bank.get()); // Get rewards for (let j = 0; j < amountOfTickets; j++) { const ticketInfo = tickets[j]; const balanceBefore = Mina.getBalance(ticketInfo.owner); const ticket = ticketInfo.ticket; const score = ticket.getScore(testWinningCombination.map((val) => UInt32.from(val))); const rp = await state.getReward(round, ticket); let tx3 = await Mina.transaction(ticketInfo.owner, async () => { await lottery.getReward(ticket, rp.ticketWitness, rp.nullifierWitness); }); await tx3.prove(); await tx3.sign([ticketInfo.owner.key]).send(); checkConsistency(); const balanceAfter = Mina.getBalance(ticketInfo.owner); const totalScore = await lottery.totalScore.get(); expect(balanceAfter.sub(balanceBefore)).toEqual(bank.mul(score).div(totalScore)); } } }); }); //# sourceMappingURL=PLottery.test.js.map