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
JavaScript
'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({});