Skip to content

Commit

Permalink
merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
souradeep-das committed Jul 15, 2024
2 parents ff5ac90 + a68fe7c commit 3fff770
Show file tree
Hide file tree
Showing 7 changed files with 697 additions and 5 deletions.
4 changes: 4 additions & 0 deletions contracts/IPseudoRand.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ interface IPseudoRand {
bytes32 value;
}

function hashToG1(
bytes memory message
) external returns (Pairing.G1Point memory);

function verifyPartialEvalFast(
Pairing.G1Point memory h,
Pairing.G1Point memory sigma,
Expand Down
5 changes: 5 additions & 0 deletions contracts/PseudoRand.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ contract PseudoRand is IPseudoRand{
bytes public constant DOMAIN = bytes("DVRF pseudorandom generation 2023");
uint public constant R = 21888242871839275222246405745257275088548364400416034343698204186575808495617;

function hashToG1(bytes memory message) public view returns (Pairing.G1Point memory) {
Pairing.G1Point memory h = Hash.hashToG1(DOMAIN, message);
return h;
}

// verify partial eval without computing hash to point
function verifyPartialEvalFast(
Pairing.G1Point memory h,
Expand Down
1 change: 0 additions & 1 deletion contracts/zkdvrf.sol
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ contract zkdvrf is Ownable {
// submit the final pseudorandom value which is computed by combining t partial evaluations offchain
function submitRandom(IPseudoRand.PseudoRandom memory pseudo) public onlyOwner {
require(roundToRandom[currentRoundNum].value == bytes32(0), "Answer for round already exists");
require(roundSubmissionCount[currentRoundNum] >= threshold, "Partial evaluation threshold not reached");
require(IPseudoRand(pseudoRand).verifyPseudoRand(bytes(roundInput[currentRoundNum]), pseudo.proof, gpkVal), "Incorrect random submitted");
bytes32 value = keccak256(abi.encodePacked(pseudo.proof.x, pseudo.proof.y));
require(pseudo.value == value, "Incorrect pseudorandom value");
Expand Down
286 changes: 286 additions & 0 deletions contracts/zkdvrf_pre.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Halo2Verifier} from "./Halo2Verifier.sol";
import {GlobalPublicParams} from "./GlobalPublicParams.sol";
import {Pairing} from "./libs/Pairing.sol";
import {IPseudoRand} from "./IPseudoRand.sol";
import {Grumpkin} from "./libs/Grumpkin.sol";

import "@openzeppelin/contracts/utils/Strings.sol";
import '@openzeppelin/contracts/access/Ownable.sol';

// zkdvrf with precomputation for hash2curve
contract zkdvrf_pre is Ownable {
using Strings for uint256;
using Grumpkin for *;

event RegistrationCompleted(uint32 count);
event NidkgStarted();
event NidkgCompleted(uint32 count);
event GlobalPublicParamsCreated();
event RandomInitiated(uint roundNum, string input);
event RandomThresholdReached(uint roundNum, string input);
event RandomReady(uint roundNum, string input);

struct dvrfNode {
address nodeAddress;
bool status;
uint256 deposit;
bool statusPP;
uint32 pkIndex;
}

enum Status {
Unregistered,
Nidkg,
NidkgComplete,
Ready
}

string public constant INPUT_PREFIX = "zkRand-v1-2024:";

uint32 public memberCount;
uint32 public threshold;
uint32 public ppLength;
// current count of members added
uint32 internal currentIndex;
// current count of members deposited and registered
uint32 internal registeredCount;
uint32 internal ppSubmissionCount;

uint256 public currentRoundNum;
uint256 public minNodeDeposit;

uint32 public pkListIndex;
Grumpkin.Point[] public pkList;
address[] public pkListOrder;

uint256[][] public ppList;
// address[] public ppListOrder;

// The order in vkList is the same as pkList
Pairing.G1Point[] public vkList;
Pairing.G2Point internal gpkVal;

Status public contractPhase;
address public halo2Verifier;
address public halo2VerifyingKey;
address public globalPublicParams;
address public pseudoRand;

mapping (uint32 => address) public nodes;
mapping (address => dvrfNode) public addrToNode;
mapping (uint256 => string) public roundInput;
mapping (uint256 => Pairing.G1Point) public roundHash;
mapping (address => uint256) public lastSubmittedRound;
mapping (uint256 => mapping (uint32 => IPseudoRand.PartialEval)) public roundToEval;
mapping (uint256 => uint32) public roundSubmissionCount;
mapping (uint256 => IPseudoRand.PseudoRandom) public roundToRandom;


constructor(uint32 thresholdValue, uint32 numberValue, address halo2VerifierAddress, address halo2VerifyingKeyAddress, address globalPublicParamsAddress, address pseudoRandAddress, uint256 minDeposit) Ownable(msg.sender) {
require (halo2VerifierAddress != address(0) && globalPublicParamsAddress != address(0) && pseudoRandAddress != address(0), "Cannot be zero addresses");
memberCount = numberValue;
threshold = thresholdValue;
ppLength = 7 * memberCount + 14;
halo2Verifier = halo2VerifierAddress;
halo2VerifyingKey = halo2VerifyingKeyAddress;
globalPublicParams = globalPublicParamsAddress;
pseudoRand = pseudoRandAddress;
minNodeDeposit = minDeposit;
}


// works until all members added,
// to move to the next phase registeredCount has to be equal to memberCount
function addPermissionedNodes(address nodeAddress) public onlyOwner {
require(currentIndex < memberCount, "All members added");
require(nodeAddress != address(0), "Node cannot be zero address");
require(addrToNode[nodeAddress].nodeAddress == address(0), "Node has already been added");

addrToNode[nodeAddress] = dvrfNode(nodeAddress, false, 0, false, 0);
currentIndex++;
}

// each member registers with deposit and confirms
function registerNode(Grumpkin.Point memory pubKey) public payable {
require(contractPhase == Status.Unregistered, "Registration has already been completed");
require(msg.sender == addrToNode[msg.sender].nodeAddress, "Unauthorized call");
require(!addrToNode[msg.sender].status, "Node Already registered");
require(msg.value >= minNodeDeposit, "Must provide enough node deposit");
require(Grumpkin.isOnCurve(pubKey), "Invalid Public Key submitted");

nodes[registeredCount] = msg.sender;
addrToNode[msg.sender].deposit = msg.value;
addrToNode[msg.sender].status = true;
addrToNode[msg.sender].pkIndex = pkListIndex;
pkList.push(pubKey);
// pkListOrder is unutilized but added for public visibility
pkListOrder.push(msg.sender);
pkListIndex++;
registeredCount++;

// all the permitted nodes have registered
if (registeredCount == memberCount) {
emit RegistrationCompleted(registeredCount);
}
}

// owner starts nidkg protocol
// can't add members after this process
function startNidkg() public onlyOwner {
require(contractPhase == Status.Unregistered, "NIDKG has already been completed");
require(registeredCount == memberCount, "Not all Members are ready");
contractPhase = Status.Nidkg;

emit NidkgStarted();
}

// each member can submit pp_i, zk_i
// contract validates zk_i here for each submission and then accepts it
function submitPublicParams(uint256[] calldata pp, bytes calldata zkProof) public {
require(msg.sender == addrToNode[msg.sender].nodeAddress, "Unauthorized call");
require(contractPhase == Status.Nidkg, "Contract not in NIDKG phase");
require(!addrToNode[msg.sender].statusPP, "Node already submitted");
require(checkPublicParams(pp), "Invalid public parameters");
require(Halo2Verifier(halo2Verifier).verifyProof(halo2VerifyingKey, zkProof, pp), "SNARK proof verification failed");

addrToNode[msg.sender].statusPP = true;

ppList.push(pp);
// ppListOrder is unutilized but added for public visibility
// ppListOrder.push(msg.sender);
ppSubmissionCount++;

if (ppSubmissionCount == memberCount) {
contractPhase = Status.NidkgComplete;
emit NidkgCompleted(ppSubmissionCount);
}
}

// compute gpk and vk and store on the contract
function computeVk(Pairing.G2Point calldata gpk) public {
require(contractPhase == Status.NidkgComplete, "Partial Parameter submission not complete");
(Pairing.G2Point memory gpkRet, Pairing.G1Point[] memory vk) = GlobalPublicParams(globalPublicParams).createGpp(memberCount, gpk, ppList);
for (uint i = 0; i < vk.length; i++) {
vkList.push(vk[i]);
}
gpkVal = gpkRet;
contractPhase = Status.Ready;

emit GlobalPublicParamsCreated();
}

// initiate public inputs for generating randoms
function initiateRandom() public onlyOwner {
require(contractPhase == Status.Ready, "Contract not ready");

if (currentRoundNum != 0) {
require(roundToRandom[currentRoundNum].value != bytes32(0), "Earlier round not completed");
}

currentRoundNum++;
bytes memory input = abi.encodePacked(INPUT_PREFIX, currentRoundNum.toString());
roundInput[currentRoundNum] = string(input);
roundHash[currentRoundNum] = IPseudoRand(pseudoRand).hashToG1(input);

emit RandomInitiated(currentRoundNum, roundInput[currentRoundNum]);
}

// each member can submit their partial evaluation.
// this function can be taken offchain. The onchain storage and verification can help determine which node to reward or punish.
function submitPartialEval(IPseudoRand.PartialEval memory pEval) public {
require(msg.sender == addrToNode[msg.sender].nodeAddress, "Unauthorized call");
// check valid round
require(roundToRandom[currentRoundNum].value == bytes32(0), "Round already computed");
// this will help revert calls if the contract status is not Ready and the first initiateRandom() is not called
require (lastSubmittedRound[msg.sender] < currentRoundNum, "Already submitted for round");
uint32 pkIndex = addrToNode[msg.sender].pkIndex;
require(pEval.indexPlus == pkIndex + 1);
Pairing.G1Point memory vkStored = vkList[pkIndex];
require(IPseudoRand(pseudoRand).verifyPartialEvalFast(roundHash[currentRoundNum], pEval.value, pEval.proof, vkStored), "Verification of partial eval failed");
lastSubmittedRound[msg.sender] = currentRoundNum;
roundToEval[currentRoundNum][pkIndex] = pEval;
roundSubmissionCount[currentRoundNum]++;

if (roundSubmissionCount[currentRoundNum] == threshold) {
emit RandomThresholdReached(currentRoundNum, roundInput[currentRoundNum]);
}
}

// submit the final pseudorandom value which is computed by combining t partial evaluations offchain
function submitRandom(IPseudoRand.PseudoRandom memory pseudo) public onlyOwner {
require(roundToRandom[currentRoundNum].value == bytes32(0), "Answer for round already exists");
require(roundSubmissionCount[currentRoundNum] >= threshold, "Partial evaluation threshold not reached");
require(IPseudoRand(pseudoRand).verifyPseudoRandFast(roundHash[currentRoundNum], pseudo.proof, gpkVal), "Incorrect random submitted");
bytes32 value = keccak256(abi.encodePacked(pseudo.proof.x, pseudo.proof.y));
require(pseudo.value == value, "Incorrect pseudorandom value");
roundToRandom[currentRoundNum] = pseudo;

emit RandomReady(currentRoundNum, roundInput[currentRoundNum]);
}

function getLatestRandom() public view returns (IPseudoRand.PseudoRandom memory pseudo) {
if (roundToRandom[currentRoundNum].value != bytes32(0)) {
return roundToRandom[currentRoundNum];
}

if (currentRoundNum == 1) {
revert("Answer does not exist for the round yet");
}

return roundToRandom[currentRoundNum - 1];
}

function getRandomAtRound(uint256 roundNum) public view returns (IPseudoRand.PseudoRandom memory pseudo) {
if (roundToRandom[roundNum].value != bytes32(0)) {
return roundToRandom[roundNum];
}

revert("Answer does not exist for the round yet");
}

function checkPublicParams(uint256[] calldata pp) public view returns (bool) {
require(pkList.length == memberCount, "Not enough member public keys");

require(pp.length == ppLength, "Wrong size of public parameters");
if (pp.length != ppLength) {
return false;
}

// check if the last 2n elements in pp are public keys
uint j = pp.length - 2 * memberCount;
for (uint i = 0; i < memberCount; i++) {
require(pp[j] == pkList[i].x, "Wrong public key x");
require(pp[j+1] == pkList[i].y, "Wrong public key y");
if (pp[j] != pkList[i].x || pp[j+1] != pkList[i].y) {
return false;
}
j = j+2;
}

return true;
}

function getIndexPlus(address nodeAdress) public view returns (uint32) {
uint32 pkIndex = addrToNode[nodeAdress].pkIndex;
return pkIndex + 1;
}

function getPkList() public view returns (Grumpkin.Point[] memory) {
return pkList;
}

function getPpList() public view returns (uint256[][] memory) {
return ppList;
}

function getGpk() public view returns (Pairing.G2Point memory) {
return gpkVal;
}

function getVkList() public view returns (Pairing.G1Point[] memory) {
return vkList;
}
}
1 change: 1 addition & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module.exports = {
}
}],
overrides: {
'contracts/GlobalPublicParams.sol': altCompilerSettings,
'contracts/PseudoRand.sol': altCompilerSettings,
},
gasReporter: {
Expand Down
4 changes: 0 additions & 4 deletions test/zkdvrf.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,6 @@ describe('ZKDVRF on-chain tests', async () => {
await expect(Zkdvrf.submitPartialEval(pEvals[0])).to.be.revertedWith('Already submitted for round')
})

it('should not be able to generate random below threshold', async () => {
await expect(Zkdvrf.submitRandom(pseudoRandom)).to.be.revertedWith('Partial evaluation threshold not reached')
})

it('all nodes should be able to submit partial eval', async () => {
await Zkdvrf.connect(account2).submitPartialEval(pEvals[1])
await Zkdvrf.connect(account3).submitPartialEval(pEvals[2])
Expand Down
Loading

0 comments on commit 3fff770

Please sign in to comment.