Skip to content

Commit

Permalink
Merge pull request #835 from clrfund/fix/verify-tally-on-chain
Browse files Browse the repository at this point in the history
Add verifyTallyResult, verifyPerVOSpentVoiceCredits, verifySpentVoiceCredits
  • Loading branch information
ctrlc03 authored Dec 8, 2023
2 parents 00a5078 + 8165968 commit 2ce6f5c
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 0 deletions.
46 changes: 46 additions & 0 deletions cli/ts/commands/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Contract } from "ethers";
import { existsSync, readFileSync } from "fs";
import { hash2, hash3, genTreeCommitment } from "maci-crypto";
import { TallyData } from "../utils/interfaces";
import { verifyPerVOSpentVoiceCredits, verifyTallyResults } from "../utils/verifiers";

/**
* Verify the results of a poll and optionally the subsidy results
Expand Down Expand Up @@ -117,6 +118,51 @@ export const verify = async (
if (onChainTallycomment !== newTallyCommitment) logError("The on-chain tally commitment does not match.");
logGreen(quiet, success("The on-chain tally commitment matches."));

// verify total spent voice credits on-chain
const isValid = await tallyContract.verifySpentVoiceCredits(
tallyResults.totalSpentVoiceCredits.spent,
tallyResults.totalSpentVoiceCredits.salt,
newResultsCommitment,
newPerVOSpentVoiceCreditsCommitment,
);
if (isValid) {
logGreen(quiet, success("The on-chain verification of total spent voice credits passed."));
} else {
logError("The on-chain verification of total spent voice credits failed.");
}

// verify per vote option voice credits on-chain
const failedSpentCredits = await verifyPerVOSpentVoiceCredits(
tallyContract,
tallyResults,
voteOptionTreeDepth,
newSpentVoiceCreditsCommitment,
newResultsCommitment,
);
if (failedSpentCredits.length === 0) {
logGreen(quiet, success("The on-chain verification of per vote option spent voice credits passed"));
} else {
logError(
`At least one tally result failed the on-chain verification. Please check your Tally data at these indexes: ${failedSpentCredits}`,
);
}

// verify tally result on-chain for each vote option
const failedPerVOSpentCredits = await verifyTallyResults(
tallyContract,
tallyResults,
voteOptionTreeDepth,
newSpentVoiceCreditsCommitment,
newPerVOSpentVoiceCreditsCommitment,
);
if (failedPerVOSpentCredits.length === 0) {
logGreen(quiet, success("The on-chain verification of tally results passed"));
} else {
logError(
`At least one spent voice credits entry in the tally results failed the on-chain verification. Please check your tally results at these indexes: ${failedPerVOSpentCredits}`,
);
}

// verify subsidy result if subsidy file is provided
if (subsidyFile) {
const onChainSubsidyCommitment = BigInt(await subsidyContract.subsidyCommitment());
Expand Down
90 changes: 90 additions & 0 deletions cli/ts/utils/verifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Contract } from "ethers";
import { genTreeProof } from "maci-crypto";
import { TallyData } from "./interfaces";

/**
* Loop through each per vote option spent voice credits and verify it on-chain
*
* @param tallyContract The tally contract
* @param tallyData The tally.json file data
* @param voteOptionTreeDepth The vote option tree depth
* @param newSpentVoiceCreditsCommitment The total spent voice credits commitment
* @param newResultsCommitment The tally result commitment
*
* @returns list of the indexes of the tally result that failed on-chain verification
*/
export const verifyPerVOSpentVoiceCredits = async (
tallyContract: Contract,
tallyData: TallyData,
voteOptionTreeDepth: number,
newSpentVoiceCreditsCommitment: bigint,
newResultsCommitment: bigint,
): Promise<number[]> => {
const failedIndices: number[] = [];

for (let i = 0; i < tallyData.perVOSpentVoiceCredits.tally.length; i += 1) {
const proof = genTreeProof(
i,
tallyData.perVOSpentVoiceCredits.tally.map((x) => BigInt(x)),
voteOptionTreeDepth,
);

const isValid = await tallyContract.verifyPerVOSpentVoiceCredits(
i,
tallyData.perVOSpentVoiceCredits.tally[i],
proof,
tallyData.perVOSpentVoiceCredits.salt,
voteOptionTreeDepth,
newSpentVoiceCreditsCommitment,
newResultsCommitment,
);
if (!isValid) {
failedIndices.push(i);
}
}

return failedIndices;
};

/**
* Loop through each tally result and verify it on-chain
* @param tallyContract The tally contract
* @param tallyData The tally.json file data
* @param voteOptionTreeDepth The vote option tree depth
* @param newSpentVoiceCreditsCommitment The total spent voice credits commitment
* @param newPerVOSpentVoiceCreditsCommitment The per vote option voice credits commitment
* @returns list of the indexes of the tally result that failed on-chain verification
*/
export const verifyTallyResults = async (
tallyContract: Contract,
tallyData: TallyData,
voteOptionTreeDepth: number,
newSpentVoiceCreditsCommitment: bigint,
newPerVOSpentVoiceCreditsCommitment: bigint,
): Promise<number[]> => {
const failedIndices: number[] = [];

for (let i = 0; i < tallyData.results.tally.length; i += 1) {
const proof = genTreeProof(
i,
tallyData.results.tally.map((x) => BigInt(x)),
voteOptionTreeDepth,
);

const isValid = await tallyContract.verifyTallyResult(
i,
tallyData.results.tally[i],
proof,
tallyData.results.salt,
voteOptionTreeDepth,
newSpentVoiceCreditsCommitment,
newPerVOSpentVoiceCreditsCommitment,
);

if (!isValid) {
failedIndices.push(i);
}
}

return failedIndices;
};
87 changes: 87 additions & 0 deletions contracts/contracts/Tally.sol
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,91 @@ contract Tally is Ownable, SnarkCommon, CommonUtilities, Hasher {
}
return current;
}

/**
* @notice Verify the number of spent voice credits from the tally.json
* @param _totalSpent spent field retrieved in the totalSpentVoiceCredits object
* @param _totalSpentSalt the corresponding salt in the totalSpentVoiceCredit object
* @param _resultCommitment hashLeftRight(merkle root of the results.tally, results.salt) in tally.json file
* @param _perVOSpentVoiceCreditsHash hashLeftRight(merkle root of the no spent voice credits per vote option, perVOSpentVoiceCredits salt)
* @return a boolean representing the status of the verification (could be either true or false)
*/
function verifySpentVoiceCredits(
uint256 _totalSpent,
uint256 _totalSpentSalt,
uint256 _resultCommitment,
uint256 _perVOSpentVoiceCreditsHash
) public view returns (bool) {
uint256[3] memory tally;
tally[0] = _resultCommitment;
tally[1] = hashLeftRight(_totalSpent, _totalSpentSalt);
tally[2] = _perVOSpentVoiceCreditsHash;

return hash3(tally) == tallyCommitment;
}

/**
* @notice Verify the number of spent voice credits per vote option from the tally.json
* @param _voteOptionIndex the index of the vote option where credits were spent
* @param _spent the spent voice credits for a given vote option index
* @param _spentProof proof generated for the perVOSpentVoiceCredits
* @param _spentSalt the corresponding salt given in the tally perVOSpentVoiceCredits object
* @param _voteOptionTreeDepth depth of the vote option tree
* @param _spentVoiceCreditsHash hashLeftRight(number of spent voice credits, spent salt)
* @param _resultCommitment hashLeftRight(merkle root of the results.tally, results.salt) in tally.json file
* @return a boolean representing the status of the verification (could be either true or false)
*/
function verifyPerVOSpentVoiceCredits(
uint256 _voteOptionIndex,
uint256 _spent,
uint256[][] memory _spentProof,
uint256 _spentSalt,
uint8 _voteOptionTreeDepth,
uint256 _spentVoiceCreditsHash,
uint256 _resultCommitment
) public view returns (bool) {
uint256 computedRoot = computeMerkleRootFromPath(_voteOptionTreeDepth, _voteOptionIndex, _spent, _spentProof);

uint256[3] memory tally;
tally[0] = _resultCommitment;
tally[1] = _spentVoiceCreditsHash;
tally[2] = hashLeftRight(computedRoot, _spentSalt);

return hash3(tally) == tallyCommitment;
}

/**
* @notice Verify the result generated from the tally.json
* @param _voteOptionIndex the index of the vote option to verify the correctness of the tally
* @param _tallyResult Flattened array of the tally
* @param _tallyResultProof Corresponding proof of the tally result
* @param _tallyResultSalt the respective salt in the results object in the tally.json
* @param _voteOptionTreeDepth depth of the vote option tree
* @param _spentVoiceCreditsHash hashLeftRight(number of spent voice credits, spent salt)
* @param _perVOSpentVoiceCreditsHash hashLeftRight(merkle root of the no spent voice credits per vote option, perVOSpentVoiceCredits salt)
* @return a boolean representing the status of the verification (could be either true or false)
*/
function verifyTallyResult(
uint256 _voteOptionIndex,
uint256 _tallyResult,
uint256[][] memory _tallyResultProof,
uint256 _tallyResultSalt,
uint8 _voteOptionTreeDepth,
uint256 _spentVoiceCreditsHash,
uint256 _perVOSpentVoiceCreditsHash
) public view returns (bool) {
uint256 computedRoot = computeMerkleRootFromPath(
_voteOptionTreeDepth,
_voteOptionIndex,
_tallyResult,
_tallyResultProof
);

uint256[3] memory tally;
tally[0] = hashLeftRight(computedRoot, _tallyResultSalt);
tally[1] = _spentVoiceCreditsHash;
tally[2] = _perVOSpentVoiceCreditsHash;

return hash3(tally) == tallyCommitment;
}
}
1 change: 1 addition & 0 deletions crypto/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {
deepCopyBigIntArray,
calcDepthFromNumLeaves,
genTreeCommitment,
genTreeProof,
} from "./utils";

export { SNARK_FIELD_SIZE, NOTHING_UP_MY_SLEEVE, babyJubMaxValue } from "./constants";
Expand Down
17 changes: 17 additions & 0 deletions crypto/ts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,20 @@ export const genTreeCommitment = (leaves: bigint[], salt: bigint, depth: number)

return hashLeftRight(tree.root, salt);
};

/**
* A helper function to generate the tree proof for the value at the given index in the leaves
* @param index The index of the value to generate the proof for
* @param leaves A list of values
* @param depth The tree depth
* @returns The proof
*/
export const genTreeProof = (index: number, leaves: bigint[], depth: number): bigint[][] => {
const tree = new IncrementalQuinTree(depth, BigInt(0), 5, hash5);
leaves.forEach((leaf) => {
tree.insert(leaf);
});

const proof = tree.genMerklePath(index);
return proof.pathElements;
};

0 comments on commit 2ce6f5c

Please sign in to comment.