Skip to content

ZkNoid/L1-lottery

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mina L1 lottery

This repo contains smart contracts and related scripts for ZkNoid L1 Lottery proposed here

Technical overview

Pre-Round Phase

  • Before a round begins, the only action permitted is committing a value for randomness.

Active Round

  • A round is considered active if the current chain slot falls within the [roundStartBlock, roundEndBlock] range.

During the Round

  • Users are allowed to buyTickets.

Post-Round Actions

  • Once the round concludes, the following steps must be completed before users can claim rewards for their tickets:
    1. Ticket Reduction:

      • ticketReduce must be invoked on the PLottery contract. This function will reduce all tickets from the target round and at least one from the subsequent round.
    2. Randomness Reveal:

      • reveal should be called on the RandomManager for the target round. This will generate the seed required for winning ticket generation.
    3. Result Production:

      • produceResult must be executed on the PLottery contract to determine the winning ticket for the round.

Reward Collection

  • After the above steps are completed, users can call getReward to claim their rewards for the tickets.

Fallback for Result Generation

  • If the result is not produced within the next two rounds, users will be eligible to request a refund for their tickets.

Storage usage

    @state(Field) ticketRoot = State<Field>();
    @state(Field) ticketNullifier = State<Field>();
    @state(Field) bankRoot = State<Field>();
    @state(Field) roundResultRoot = State<Field>();
    @state(UInt32) startBlock = State<UInt32>();
    @state(Field) lastProcessedState = State<Field>();
    @state(Field) lastReduceInRound = State<Field>();
    @state(Field) lastProcessedTicketId = State<Field>();

We are using all 8 avaliable slots:

  1. ticketRoot - to store merkle tree root for ticket
  2. ticketNullifier - to store merkle tree root for nullifer, so one ticket could be redeemed only once
  3. bankRoot - to store bank merkle tree root for each round
  4. roundResultRoot - to store winning combination for each round
  5. startBlock - stores slot of deployment
  6. lastProcessedState - stores last processed actions state
  7. lastReduceInRound - stores round in wich last actions reduce was called. We can garantee, that in this case all tickets from previous rounds was processed by reducer
  8. lastProcessedTicketId - id of last processed ticket by reducer

Ticket purchase

For ticket purchase buyTicket method is used

@method async buyTicket(ticket: Ticket, round: Field) {
  ...
}

It do not update ticket merkle tree, but add action to actionList.

this.reducer.dispatch(
  new LotteryAction({
    ticket,
    round,
  })
);

Also it fires event:

this.emitEvent(
  'buy-ticket',
  new BuyTicketEvent({
    ticket,
    round: round,
  })
);

Ticket reduce

As mentioned earlier buyTicket do not update ticket merkle tree, but add action to actionList. We can't use it directly, so we need to convert actions list to merkle tree. We do it using ZkProgramm:

export const TicketReduceProgram = ZkProgram({
  name: 'ticket-reduce-program',
  publicInput: TicketReduceProofPublicInput,
  publicOutput: TicketReduceProofPublicOutput,
  methods: {
    init: {
      ...
    },
    addTicket: {
      ...
    },
    cutActions: {
      ...
    },
  },
});

Winning number generation

For random generation we have separate contract. It utilize ZKOn zk oracle and commit-reveal technique.

First we commit our hidden value:

    @method async commit(
      commitValue: CommitValue,
      commitWitness: MerkleMapWitness
    ) {
      ...
    }

Then send request for ZkOn oracle, with request that lies on IPFS(cid can be found here). It sends request on quantum-random.com to get random number.

@method async callZkon() {
  ...
  const requestId = await coordinator.sendRequest(
    this.address,
    hashPart1,
    hashPart2
  );
  ...
}

After we receive random number from ZKOn, we mix it with out hidden number, and store result on merkle tree.

    @method async reveal(
      commitValue: CommitValue,
      commitWitness: MerkleMapWitness,
      resultWitness: MerkleMapWitness
    ) {
      ...
      const resultValue = Poseidon.hash([commitValue.value, curRandomValue]);

      // Update result
      const [newResultRoot] = resultWitness.computeRootAndKey(resultValue);
      this.resultRoot.set(newResultRoot);
      ...
    }

Later we will use this number to generate winning combination

    @method async produceResult(
      resultWiness: MerkleMap20Witness,
      result: Field,
      bankValue: Field,
      bankWitness: MerkleMap20Witness,
      rmWitness: MerkleMapWitness,
      rmValue: Field
    ) {
      ...
      this.checkRandomResultValue(rmWitness, rmValue, round);

      let winningNumbers = generateNumbersSeed(rmValue);
      ...
    }


    public checkRandomResultValue(
      roundResultWitness: MerkleMapWitness,
      roundResulValue: Field,
      round: Field
    ) {
      ...
      const rm = new RandomManager(randomManagerAddress);
      const resultRoot = rm.resultRoot.getAndRequireEquals();
      ...
    }

Bank distribution

Bank is distrubuted among all players fairly, according to amount of rightly guessed number. To do so, after winning numbers generation we match score for it ticket:

  • 0 numbers guessed - 0
  • 1 number guessed - 90
  • 2 numbers guessed - 324
  • 3 numbers guessed - 2187
  • 4 numbers guessed - 26244
  • 5 numbers guessed - 590490
  • 6 numbers guessed - 31886460

Then we compute total score for round in a provable way using proof.

Each ticket can get roundBank * ticketScore / totalScore tokens.

Ticket redemption

To get winning for ticket getReward function exist. It will compute score for ticket, portion of bank, which it owns, transfer to to user, and update nullifier merkle map, so this ticket can't be used in future.

    @method async getReward(
      ticket: Ticket,
      roundWitness: MerkleMap20Witness,
      roundTicketWitness: MerkleMap20Witness,
      dp: DistributionProof,
      winningNumbers: Field,
      resutWitness: MerkleMap20Witness,
      bankValue: Field,
      bankWitness: MerkleMap20Witness,
      nullifierWitness: MerkleMapWitness
    ) {
    }

Preparation

Install npm modules:

npm install

Prepare mina-fungible-token module:

npm run token_prepare
npm run build

How to run tests

npm run test

How to run coverage

npm run coverage

How to deploy

To deploy you first need to set evironments variables:

DEPLOYER_KEY = ""
RANDOM_MANAGER_OWNER_ADDRESS = ""

Then you can deploy it using

npm run deploy

It will generate private and public keys for RandomManager and Lottery. Deploy them on corresponding addresses and will store addresses on deploy/addresses and store keys on keys/auto

License

Apache-2.0