Skip to content

Commit

Permalink
refactor(nonqv): optimize tally votes non qv circuit and contracts
Browse files Browse the repository at this point in the history
reduce the number of constraints for the tally votes non qv circuit, and amend the repo code to
support the changes. This includes a new Tally contract with reduced logic which has been removed
from the circuit.
  • Loading branch information
ctrlc03 committed Feb 14, 2024
1 parent fa4313f commit ea632a9
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 ea632a9

Please sign in to comment.