Skip to content

destroyersrt/Rock-Paper-Scissor-ZKP

Repository files navigation

Rock-Paper-Scissors (ZKP Version):

This is the zero knowledge proof version of the Rock-Paper-Scissors game.

We are going to utilize the zk-snarks for proving. The library that we are going to use to build proofs and verifer is Zokrates.

Zokrates : ZoKrates is a toolbox for zkSNARKs on Ethereum. It helps you use verifiable computation in your DApp, from the specification of your program in a high level language to generating proofs of computation to verifying those proofs in Solidity.

Procedure

Setting up zokrates:

For Linux, Macos Installation: curl -LSfs get.zokrat.es | sh


Zokrates Commands

compile - zokrates compile -i filName.zok. This step generates abi.json and out(executable) files.

perform the setup phase - zokrates setup. This step generates proving.key and verification.key

execute the program - zokrates compute-witness -a 337 113569. This generates a witness file.

generate a proof of computation - zokrates generate-proof

export a solidity verifier - zokrates export-verifier

or verify natively - zokrates verify


Zokrates Procedure:

  • mkdir circuits
  • vim EncodingChoice.zok
import "hashes/keccak/256bit" as keccak256
import "utils/casts/field_to_u64" as field_to_u64

def main(private u64 num, private u64[4] salt) -> (u64[4]): // take choice and salt as the array of four numbers
    assert(num >= 1 && num <= 3)

    u64[5] salt_u64 = [0; 5]
    for u32 i in 0..4 do
        salt_u64[i] = salt[i]
    endfor
    salt_u64[4] = num
    return keccakHash = keccak256(salt_u64)
  • compile the file - zokrates compile -i EncodingChoice.zok -o EncodingChoice
  • setup - zokrates setup -i EncodingChoice
  • compute witness - here 1 is the choice which is rock
zokrates compute-witness -i EncodingChoice -a 1 123 456 789 321 -o encodingWitness
  • fetch the out values from the encodingWitness
~out_3 3059451574168111488
~out_2 8892337613839764846
~out_1 14252626103636100779
~out_0 5455366589862775137
  • Submit these four values 0 to 4 as the encodedChoice in the submitEncodedChoice function.

Now, follow similar procedure for the circuit given below. This time while computing the witness also provide the out values you got from the above.

zokrtates compute-witness -i {outFileName} 1 123 456 789 321 5455366589862775137 14252626103636100779 8892337613839764846 3059451574168111488 -o {witnessFileName}

  • Now generate a proof file using the zokrates generate-proof command. The proof.json file contains proof.a, proof.b, proof.c values should be provided in the submitProof function.

Writing a Circuit:

This is the circuit used for the verification process:

import "hashes/keccak/256bit" as keccak256
import "utils/casts/field_to_u64" as field_to_u64

def main(private u64 num, private u64[4] salt, u64[4] hash): // take choice and salt as the array of four numbers
    assert(num >= 1 && num <= 3)

    u64[5] salt_u64 = [0; 5]
    for u32 i in 0..4 do
        salt_u64[i] = salt[i]
    endfor
    salt_u64[4] = num
    u64[4] keccakHash = keccak256(salt_u64)

    assert(keccakHash[0] == hash[0])
    assert(keccakHash[1] == hash[1])
    assert(keccakHash[2] == hash[2])
    assert(keccakHash[3] == hash[3])
    return

This circuit takes 3 arguments:

  • num/choice private
  • salt private
  • hash(keccakHash) public

The hash is the value to which the generated hash of the choice and salt would be matched.


Smart Contract Flow

  • Player1 starts the game, with either some bet or no
  • Player2 joins the game, matching the bet if any
  • Now, game is in InProgress state
  • submitEncodedChoice - Both Players submits a encodedChoice which is generated by computing a keccak256 of uint64[4] salt and choice which is in the range of 1 <= choice <= 3
  • submitProof - Both Players submits a ZKP to the contract that verifies if the encodedChoice is a valid choice or not. It checks if the choice is between the [1,3] range, and the player knows the salt to generate the submitted encodedChoice.
  • revealChoice - Players submits their choice and their respective salts. This function matches the hash generated by their choice and salt with the encodedChoice
function hashing(uint64 choice, uint64[4] memory salt) external pure returns(bytes32) {
   
   return keccak256(abi.encodePacked(salt[0],salt[1],salt[2],salt[3], choice));
   
}
  • computeWinner - This function can be called by anybody until the gameState is set to Result. This function settles the rewards for the players, if their was any bet included in the game.

  • Note

    • Almost each stage has a timelimit of 1 day. If any one of the player completes that stage before that timelimit and other player fails to do so. Then the 2 * bet amount is transferred to the player who completed the stage.
    • If no players completes the stage before the timelimit then the amount is transferred to the contract owner.
    • This doesn't restrict the attacker to not use the same inputs as the other player. Thus drawing the game but restricts the attacker to not know the exact choice before the revealChoice function.

Commands to run the project:

Run npm install then npx hardhat test

Things that have been improved in this game:

  • Concurrent Games
  • Storage Optimizations
  • Pull reward mechanism
  • usage of reentrancy guard

Verifier.sol

// This file is MIT Licensed.
//
// Copyright 2017 Christian Reitwiessner
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
pragma solidity ^0.8.0;
library Pairing {
    struct G1Point {
        uint X;
        uint Y;
    }
    // Encoding of field elements is: X[0] * z + X[1]
    struct G2Point {
        uint[2] X;
        uint[2] Y;
    }
    /// @return the generator of G1
    function P1() pure internal returns (G1Point memory) {
        return G1Point(1, 2);
    }
    /// @return the generator of G2
    function P2() pure internal returns (G2Point memory) {
        return G2Point(
            [10857046999023057135944570762232829481370756359578518086990519993285655852781,
             11559732032986387107991004021392285783925812861821192530917403151452391805634],
            [8495653923123431417604973247489272438418190587263600148770280649306958101930,
             4082367875863433681332203403145435568316851327593401208105741076214120093531]
        );
    }
    /// @return the negation of p, i.e. p.addition(p.negate()) should be zero.
    function negate(G1Point memory p) pure internal returns (G1Point memory) {
        // The prime q in the base field F_q for G1
        uint q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
        if (p.X == 0 && p.Y == 0)
            return G1Point(0, 0);
        return G1Point(p.X, q - (p.Y % q));
    }
    /// @return r the sum of two points of G1
    function addition(G1Point memory p1, G1Point memory p2) internal view returns (G1Point memory r) {
        uint[4] memory input;
        input[0] = p1.X;
        input[1] = p1.Y;
        input[2] = p2.X;
        input[3] = p2.Y;
        bool success;
        assembly {
            success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60)
            // Use "invalid" to make gas estimation work
            switch success case 0 { invalid() }
        }
        require(success);
    }


    /// @return r the product of a point on G1 and a scalar, i.e.
    /// p == p.scalar_mul(1) and p.addition(p) == p.scalar_mul(2) for all points p.
    function scalar_mul(G1Point memory p, uint s) internal view returns (G1Point memory r) {
        uint[3] memory input;
        input[0] = p.X;
        input[1] = p.Y;
        input[2] = s;
        bool success;
        assembly {
            success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60)
            // Use "invalid" to make gas estimation work
            switch success case 0 { invalid() }
        }
        require (success);
    }
    /// @return the result of computing the pairing check
    /// e(p1[0], p2[0]) *  .... * e(p1[n], p2[n]) == 1
    /// For example pairing([P1(), P1().negate()], [P2(), P2()]) should
    /// return true.
    function pairing(G1Point[] memory p1, G2Point[] memory p2) internal view returns (bool) {
        require(p1.length == p2.length);
        uint elements = p1.length;
        uint inputSize = elements * 6;
        uint[] memory input = new uint[](inputSize);
        for (uint i = 0; i < elements; i++)
        {
            input[i * 6 + 0] = p1[i].X;
            input[i * 6 + 1] = p1[i].Y;
            input[i * 6 + 2] = p2[i].X[1];
            input[i * 6 + 3] = p2[i].X[0];
            input[i * 6 + 4] = p2[i].Y[1];
            input[i * 6 + 5] = p2[i].Y[0];
        }
        uint[1] memory out;
        bool success;
        assembly {
            success := staticcall(sub(gas(), 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20)
            // Use "invalid" to make gas estimation work
            switch success case 0 { invalid() }
        }
        require(success);
        return out[0] != 0;
    }
    /// Convenience method for a pairing check for two pairs.
    function pairingProd2(G1Point memory a1, G2Point memory a2, G1Point memory b1, G2Point memory b2) internal view returns (bool) {
        G1Point[] memory p1 = new G1Point[](2);
        G2Point[] memory p2 = new G2Point[](2);
        p1[0] = a1;
        p1[1] = b1;
        p2[0] = a2;
        p2[1] = b2;
        return pairing(p1, p2);
    }
    /// Convenience method for a pairing check for three pairs.
    function pairingProd3(
            G1Point memory a1, G2Point memory a2,
            G1Point memory b1, G2Point memory b2,
            G1Point memory c1, G2Point memory c2
    ) internal view returns (bool) {
        G1Point[] memory p1 = new G1Point[](3);
        G2Point[] memory p2 = new G2Point[](3);
        p1[0] = a1;
        p1[1] = b1;
        p1[2] = c1;
        p2[0] = a2;
        p2[1] = b2;
        p2[2] = c2;
        return pairing(p1, p2);
    }
    /// Convenience method for a pairing check for four pairs.
    function pairingProd4(
            G1Point memory a1, G2Point memory a2,
            G1Point memory b1, G2Point memory b2,
            G1Point memory c1, G2Point memory c2,
            G1Point memory d1, G2Point memory d2
    ) internal view returns (bool) {
        G1Point[] memory p1 = new G1Point[](4);
        G2Point[] memory p2 = new G2Point[](4);
        p1[0] = a1;
        p1[1] = b1;
        p1[2] = c1;
        p1[3] = d1;
        p2[0] = a2;
        p2[1] = b2;
        p2[2] = c2;
        p2[3] = d2;
        return pairing(p1, p2);
    }
}

contract Verifier {
    using Pairing for *;
    struct VerifyingKey {
        Pairing.G1Point alpha;
        Pairing.G2Point beta;
        Pairing.G2Point gamma;
        Pairing.G2Point delta;
        Pairing.G1Point[] gamma_abc;
    }
    struct Proof {
        Pairing.G1Point a;
        Pairing.G2Point b;
        Pairing.G1Point c;
    }
    function verifyingKey() pure internal returns (VerifyingKey memory vk) {
        vk.alpha = Pairing.G1Point(uint256(0x2d4fb1c81728a79afbfa9d78e789af3c86f0a7582e90b89c24f4859be78322be), uint256(0x035b8913ca135eb417e03603d703f0110a4c6589f5fbdaa54d168af636e01b20));
        vk.beta = Pairing.G2Point([uint256(0x16d56021f4bf5b948eb2d5d2c2bdbb2d09e9daad7b18154107ceea411e748083), uint256(0x0c5ef8b83ab5d33fa4641c06745660572cce27adad4ba22c65f00eb699b5ebb5)], [uint256(0x1af9331f3c7420f8f463db2bb74dca5989c7375bcd22cf81ca2959bc79a21985), uint256(0x02402d196855fe2366510548702109cf00dbd99f030a95639f3c0606d47f66e8)]);
        vk.gamma = Pairing.G2Point([uint256(0x28802d6ae229220af61576e67da96746c572e14a77d40b53b168b7969a86f094), uint256(0x1c83a04716f2287bb9a4530df939f76f66c7407c95369470ec696f9b85d6e50c)], [uint256(0x0f140feaee05f3f4231cc6aa6da1f571e84f334579406f3530342e6e790fba9d), uint256(0x0676a28ecd081d51ba029172d0550cac106c40bb73ee59581e58eb79f38585d8)]);
        vk.delta = Pairing.G2Point([uint256(0x265758770e84c0ec11a4a726e57a53b01f68f138f5ab57ce99849f175da06dc6), uint256(0x2a26a0147007950763d758873663ce8f3244fb0e582884f66150a6172a71c2b9)], [uint256(0x21eba645d80aaf4ff1578ccf3df55e692e2a11d62202eaad0f7814937ede00bc), uint256(0x2c8e0f71400a157a7a55cd201dbbbe98151f2b172e8ae3844b9ca6b4867e5ac8)]);
        vk.gamma_abc = new Pairing.G1Point[](5);
        vk.gamma_abc[0] = Pairing.G1Point(uint256(0x203186449a0dcd0f899b1ae66c82218f9e1b6ad49c59b613c0dc58148a59cabc), uint256(0x0a74661234b713af6f88ea00dfc4550f9283f7bfa2ae7e4d1e0e9befeebc752d));
        vk.gamma_abc[1] = Pairing.G1Point(uint256(0x016e9bd9f91639d79c8c56b11285d9f3bdafcfa855e8d798f0731ef83815b886), uint256(0x1f014b03cca5f2e4de459a5b548b8ecb6aa0b48762e6d7f7089a48f7aa24e8bf));
        vk.gamma_abc[2] = Pairing.G1Point(uint256(0x181fb918d261dcc66483ed94a0378cb04fe4fb70f185351cb1c25e71bf2d072e), uint256(0x2963a61f7116ffa9004ac298b3cc208d3c59e0654eafd66eb7c9fffa695ac606));
        vk.gamma_abc[3] = Pairing.G1Point(uint256(0x148c48c1c9b05b739bfe6719edb2fdb56160da69bfd8c4d918af1996d52d3014), uint256(0x26e64a1bdc7880c4dc261b69abe21b81392b844a2427ef5067ef029f2f57cf08));
        vk.gamma_abc[4] = Pairing.G1Point(uint256(0x24a89bdbf4f449ee56e7ea431a6096d4ad4ef2dd5d2b86c68947a18e4ba4c97a), uint256(0x122f2007b47314200912b190d103f3ae695f0d49b642b976ee85c801eefd18c7));
    }
    function verify(uint[] memory input, Proof memory proof) internal view returns (uint) {
        uint256 snark_scalar_field = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
        VerifyingKey memory vk = verifyingKey();
        require(input.length + 1 == vk.gamma_abc.length);
        // Compute the linear combination vk_x
        Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0);
        for (uint i = 0; i < input.length; i++) {
            require(input[i] < snark_scalar_field);
            vk_x = Pairing.addition(vk_x, Pairing.scalar_mul(vk.gamma_abc[i + 1], input[i]));
        }
        vk_x = Pairing.addition(vk_x, vk.gamma_abc[0]);
        if(!Pairing.pairingProd4(
             proof.a, proof.b,
             Pairing.negate(vk_x), vk.gamma,
             Pairing.negate(proof.c), vk.delta,
             Pairing.negate(vk.alpha), vk.beta)) return 1;
        return 0;
    }
    function verifyTx(
            Proof memory proof, uint[4] memory input
        ) public view returns (bool r) {
        uint[] memory inputValues = new uint[](4);
        
        for(uint i = 0; i < input.length; i++){
            inputValues[i] = input[i];
        }
        if (verify(inputValues, proof) == 0) {
            return true;
        } else {
            return false;
        }
    }
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published