Skip to content

Commit

Permalink
Merge pull request #1174 from privacy-scaling-explorations/refactor/t…
Browse files Browse the repository at this point in the history
…ally-votes-non-qv

refactor(nonqv): optimize tally votes non qv circuit and contracts
  • Loading branch information
ctrlc03 authored Feb 14, 2024
2 parents fa4313f + ea632a9 commit e2e1031
Show file tree
Hide file tree
Showing 35 changed files with 1,159 additions and 201 deletions.
106 changes: 87 additions & 19 deletions circuits/circom/tallyVotesNonQv.circom
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,7 @@ template TallyVotesNonQv(
signal input currentSpentVoiceCreditSubtotal;
signal input currentSpentVoiceCreditSubtotalSalt;

signal input currentPerVOSpentVoiceCredits[numVoteOptions];
signal input currentPerVOSpentVoiceCreditsRootSalt;

signal input newResultsRootSalt;
signal input newPerVOSpentVoiceCreditsRootSalt;
signal input newSpentVoiceCreditSubtotalSalt;

// -----------------------------------------------------------------------
Expand Down Expand Up @@ -163,18 +159,8 @@ template TallyVotesNonQv(
}
}

// Tally the spent voice credits per vote option
component newPerVOSpentVoiceCredits[numVoteOptions];
for (var i = 0; i < numVoteOptions; i++) {
newPerVOSpentVoiceCredits[i] = CalculateTotal(batchSize + 1);
newPerVOSpentVoiceCredits[i].nums[batchSize] <== currentPerVOSpentVoiceCredits[i] * iz.out;
for (var j = 0; j < batchSize; j++) {
newPerVOSpentVoiceCredits[i].nums[j] <== votes[j][i];
}
}

// Verify the current and new tally
component rcv = ResultCommitmentVerifier(voteOptionTreeDepth);
component rcv = ResultCommitmentNonQvVerifier(voteOptionTreeDepth);
rcv.isFirstBatch <== isFirstBatch.out;
rcv.currentTallyCommitment <== currentTallyCommitment;
rcv.newTallyCommitment <== newTallyCommitment;
Expand All @@ -184,13 +170,95 @@ template TallyVotesNonQv(
rcv.currentSpentVoiceCreditSubtotalSalt <== currentSpentVoiceCreditSubtotalSalt;
rcv.newSpentVoiceCreditSubtotal <== newSpentVoiceCreditSubtotal.sum;
rcv.newSpentVoiceCreditSubtotalSalt <== newSpentVoiceCreditSubtotalSalt;
rcv.currentPerVOSpentVoiceCreditsRootSalt <== currentPerVOSpentVoiceCreditsRootSalt;
rcv.newPerVOSpentVoiceCreditsRootSalt <== newPerVOSpentVoiceCreditsRootSalt;

for (var i = 0; i < numVoteOptions; i++) {
rcv.currentResults[i] <== currentResults[i];
rcv.newResults[i] <== resultCalc[i].sum;
rcv.currentPerVOSpentVoiceCredits[i] <== currentPerVOSpentVoiceCredits[i];
rcv.newPerVOSpentVoiceCredits[i] <== newPerVOSpentVoiceCredits[i].sum;
}
}

// Verifies the commitment to the current results. Also computes and outputs a
// commitment to the new results. Works for non quadratic voting
// - so no need for perVOSpentCredits as they would just match
// the results.
template ResultCommitmentNonQvVerifier(voteOptionTreeDepth) {
var TREE_ARITY = 5;
var numVoteOptions = TREE_ARITY ** voteOptionTreeDepth;

// 1 if this is the first batch, and 0 otherwise
signal input isFirstBatch;
signal input currentTallyCommitment;
signal input newTallyCommitment;

// Results
signal input currentResults[numVoteOptions];
signal input currentResultsRootSalt;

signal input newResults[numVoteOptions];
signal input newResultsRootSalt;

// Spent voice credits
signal input currentSpentVoiceCreditSubtotal;
signal input currentSpentVoiceCreditSubtotalSalt;

signal input newSpentVoiceCreditSubtotal;
signal input newSpentVoiceCreditSubtotalSalt;

// Compute the commitment to the current results
component currentResultsRoot = QuinCheckRoot(voteOptionTreeDepth);
for (var i = 0; i < numVoteOptions; i++) {
currentResultsRoot.leaves[i] <== currentResults[i];
}

component currentResultsCommitment = HashLeftRight();
currentResultsCommitment.left <== currentResultsRoot.root;
currentResultsCommitment.right <== currentResultsRootSalt;

// Compute the commitment to the current spent voice credits
component currentSpentVoiceCreditsCommitment = HashLeftRight();
currentSpentVoiceCreditsCommitment.left <== currentSpentVoiceCreditSubtotal;
currentSpentVoiceCreditsCommitment.right <== currentSpentVoiceCreditSubtotalSalt;

// Commit to the current tally
component currentTallyCommitmentHasher = HashLeftRight();
currentTallyCommitmentHasher.left <== currentResultsCommitment.hash;
currentTallyCommitmentHasher.right <== currentSpentVoiceCreditsCommitment.hash;

// Check if the current tally commitment is correct only if this is not the first batch
component iz = IsZero();
iz.in <== isFirstBatch;
// iz.out is 1 if this is not the first batch
// iz.out is 0 if this is the first batch

// hz is 0 if this is the first batch
// currentTallyCommitment should be 0 if this is the first batch

// hz is 1 if this is not the first batch
// currentTallyCommitment should not be 0 if this is the first batch
signal hz;
hz <== iz.out * currentTallyCommitmentHasher.hash;

hz === currentTallyCommitment;

// Compute the root of the new results
component newResultsRoot = QuinCheckRoot(voteOptionTreeDepth);
for (var i = 0; i < numVoteOptions; i++) {
newResultsRoot.leaves[i] <== newResults[i];
}

component newResultsCommitment = HashLeftRight();
newResultsCommitment.left <== newResultsRoot.root;
newResultsCommitment.right <== newResultsRootSalt;

// Compute the commitment to the new spent voice credits value
component newSpentVoiceCreditsCommitment = HashLeftRight();
newSpentVoiceCreditsCommitment.left <== newSpentVoiceCreditSubtotal;
newSpentVoiceCreditsCommitment.right <== newSpentVoiceCreditSubtotalSalt;

// Commit to the new tally
component newTallyCommitmentHasher = HashLeftRight();
newTallyCommitmentHasher.left <== newResultsCommitment.hash;
newTallyCommitmentHasher.right <== newSpentVoiceCreditsCommitment.hash;

newTallyCommitmentHasher.hash === newTallyCommitment;
}
9 changes: 5 additions & 4 deletions circuits/ts/__tests__/TallyVotes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,14 @@ describe("TallyVotes circuit", function test() {
});

it("should produce the correct result commitments", async () => {
const generatedInputs = poll.tallyVotes() as unknown as ITallyVotesInputs;
const witness = await circuit.calculateWitness(generatedInputs);
await circuit.expectConstraintPass(witness);
const generatedInputs = poll.tallyVotesNonQv() as unknown as ITallyVotesInputs;

const witness = await circuitNonQv.calculateWitness(generatedInputs);
await circuitNonQv.expectConstraintPass(witness);
});

it("should produce the correct result if the inital tally is not zero", async () => {
const generatedInputs = poll.tallyVotes(false) as unknown as ITallyVotesInputs;
const generatedInputs = poll.tallyVotesNonQv() as unknown as ITallyVotesInputs;

// Start the tally from non-zero value
let randIdx = generateRandomIndex(Object.keys(generatedInputs).length);
Expand Down
5 changes: 3 additions & 2 deletions cli/tests/e2e/e2e.nonQv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { cleanVanilla, isArm } from "../utils";
Test scenarios:
1 signup, 1 message with quadratic voting disabled
*/
describe("e2e tests", function test() {
describe("e2e tests with non quadratic voting", function test() {
const useWasm = isArm();
this.timeout(900000);

Expand All @@ -69,6 +69,7 @@ describe("e2e tests", function test() {
processWasm: testProcessMessagesNonQvWasmPath,
tallyWasm: testTallyVotesNonQvWasmPath,
useWasm,
useQuadraticVoting: false,
};

// before all tests we deploy the vk registry contract and set the verifying keys
Expand All @@ -90,7 +91,7 @@ describe("e2e tests", function test() {

before(async () => {
// deploy the smart contracts
maciAddresses = await deploy({ ...deployArgs, signer });
maciAddresses = await deploy({ ...deployArgs, signer, useQv: false });
// deploy a poll contract
await deployPoll({ ...deployPollArgs, signer });
});
Expand Down
6 changes: 3 additions & 3 deletions cli/tests/e2e/keyChange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe("keyChange tests", function test() {
it("should confirm the tally is correct", () => {
const tallyData = JSON.parse(fs.readFileSync(testTallyFilePath).toString()) as TallyData;
expect(tallyData.results.tally[0]).to.equal(expectedTally.toString());
expect(tallyData.perVOSpentVoiceCredits.tally[0]).to.equal(expectedPerVoteOptionTally.toString());
expect(tallyData.perVOSpentVoiceCredits?.tally[0]).to.equal(expectedPerVoteOptionTally.toString());
});
});

Expand Down Expand Up @@ -215,7 +215,7 @@ describe("keyChange tests", function test() {
it("should confirm the tally is correct", () => {
const tallyData = JSON.parse(fs.readFileSync(testTallyFilePath).toString()) as TallyData;
expect(tallyData.results.tally[0]).to.equal(expectedTally.toString());
expect(tallyData.perVOSpentVoiceCredits.tally[0]).to.equal(expectedPerVoteOptionTally.toString());
expect(tallyData.perVOSpentVoiceCredits?.tally[0]).to.equal(expectedPerVoteOptionTally.toString());
});
});

Expand Down Expand Up @@ -283,7 +283,7 @@ describe("keyChange tests", function test() {
it("should confirm the tally is correct", () => {
const tallyData = JSON.parse(fs.readFileSync(testTallyFilePath).toString()) as TallyData;
expect(tallyData.results.tally[2]).to.equal(expectedTally.toString());
expect(tallyData.perVOSpentVoiceCredits.tally[2]).to.equal(expectedPerVoteOptionTally.toString());
expect(tallyData.perVOSpentVoiceCredits?.tally[2]).to.equal(expectedPerVoteOptionTally.toString());
});
});
});
2 changes: 2 additions & 0 deletions cli/ts/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const deploy = async ({
poseidonT4Address,
poseidonT5Address,
poseidonT6Address,
useQv = true,
signer,
quiet = true,
}: DeployArgs): Promise<DeployedContracts> => {
Expand Down Expand Up @@ -95,6 +96,7 @@ export const deploy = async ({
signer,
stateTreeDepth,
quiet: true,
useQv,
});

const [maciContractAddress, stateAqContractAddress, pollFactoryContractAddress] = await Promise.all([
Expand Down
48 changes: 30 additions & 18 deletions cli/ts/commands/genProofs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,9 @@ export const genProofs = async ({
// tally all ballots for this poll
while (poll.hasUntalliedBallots()) {
// tally votes in batches
tallyCircuitInputs = poll.tallyVotes(useQuadraticVoting) as unknown as CircuitInputs;
tallyCircuitInputs = useQuadraticVoting
? (poll.tallyVotes() as unknown as CircuitInputs)
: (poll.tallyVotesNonQv() as unknown as CircuitInputs);

try {
// generate the proof
Expand Down Expand Up @@ -467,29 +469,19 @@ export const genProofs = async ({
BigInt(asHex(tallyCircuitInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish)),
);

// Compute newPerVOSpentVoiceCreditsCommitment
const newPerVOSpentVoiceCreditsCommitment = genTreeCommitment(
poll.perVOSpentVoiceCredits,
BigInt(asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish)),
poll.treeDepths.voteOptionTreeDepth,
);

// Compute newTallyCommitment
const newTallyCommitment = hash3([
newResultsCommitment,
newSpentVoiceCreditsCommitment,
newPerVOSpentVoiceCreditsCommitment,
]);

// get the tally contract address
const tallyContractAddress = tallyAddress || readContractAddress(`Tally-${pollId}`, network?.name);

let newPerVOSpentVoiceCreditsCommitment: bigint | undefined;
let newTallyCommitment: bigint;

// create the tally file data to store for verification later
const tallyFileData: TallyData = {
maci: maciContractAddress,
pollId: pollId.toString(),
network: network?.name,
chainId: network?.chainId.toString(),
isQuadratic: useQuadraticVoting,
tallyAddress: tallyContractAddress,
newTallyCommitment: asHex(tallyCircuitInputs!.newTallyCommitment as BigNumberish),
results: {
Expand All @@ -502,12 +494,32 @@ export const genProofs = async ({
salt: asHex(tallyCircuitInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish),
commitment: asHex(newSpentVoiceCreditsCommitment),
},
perVOSpentVoiceCredits: {
};

if (useQuadraticVoting) {
// Compute newPerVOSpentVoiceCreditsCommitment
newPerVOSpentVoiceCreditsCommitment = genTreeCommitment(
poll.perVOSpentVoiceCredits,
BigInt(asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish)),
poll.treeDepths.voteOptionTreeDepth,
);

// Compute newTallyCommitment
newTallyCommitment = hash3([
newResultsCommitment,
newSpentVoiceCreditsCommitment,
newPerVOSpentVoiceCreditsCommitment,
]);

// update perVOSpentVoiceCredits in the tally file data
tallyFileData.perVOSpentVoiceCredits = {
tally: poll.perVOSpentVoiceCredits.map((x) => x.toString()),
salt: asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish),
commitment: asHex(newPerVOSpentVoiceCreditsCommitment),
},
};
};
} else {
newTallyCommitment = hashLeftRight(newResultsCommitment, newSpentVoiceCreditsCommitment);
}

fs.writeFileSync(tallyFile, JSON.stringify(tallyFileData, null, 4));

Expand Down
Loading

0 comments on commit e2e1031

Please sign in to comment.