made the deployment scripts for the maci contracts

This commit is contained in:
Yash Goyal
2024-03-23 07:23:34 +05:30
parent f88f28a2b8
commit cbee4e33f9
111 changed files with 16784 additions and 293 deletions

View File

@@ -15,3 +15,5 @@ artifacts-zk
cache-zk
deployments/localhost
zkeys

View File

@@ -0,0 +1,17 @@
const USE_QUADRADIC_VOTING = true;
export const InitialVoiceCreditProxyContractName = "ConstantInitialVoiceCreditProxy";
export const GatekeeperContractName = "FreeForAllGatekeeper";
export const VerifierContractName = "Verifier";
export const TopupCreditContractName = "TopupCredit";
export const TallyFactoryContractName = USE_QUADRADIC_VOTING ? "TallyFactory" : "TallyNonQvFactory";
// zk registry config
export const stateTreeDepth = 10;
export const intStateTreeDepth = 1;
export const messageTreeDepth = 2;
export const voteOptionTreeDepth = 2;
export const messageBatchDepth = 1;
export const processMessagesZkeyPath = "./zkeys/ProcessMessages_6-9-2-3/processMessages_6-9-2-3.zkey";
export const tallyVotesZkeyPath = "./zkeys/TallyVotes_6-2-3/tallyVotes_6-2-3.zkey";
export const subsidyZkeyPath: string | null = null;

View File

@@ -1,44 +0,0 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { Contract } from "ethers";
/**
* Deploys a contract named "YourContract" using the deployer account and
* constructor arguments set to the deployer address
*
* @param hre HardhatRuntimeEnvironment object.
*/
const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
/*
On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account
should have sufficient balance to pay for the gas fees for contract creation.
You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY
with a random private key in the .env file (then used on hardhat.config.ts)
You can run the `yarn account` command to check your balance in every network.
*/
const { deployer } = await hre.getNamedAccounts();
const { deploy } = hre.deployments;
await deploy("YourContract", {
from: deployer,
// Contract constructor arguments
args: [deployer],
log: true,
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
// automatically mining the contract deployment transaction. There is no effect on live networks.
autoMine: true,
});
// Get the deployed contract to interact with it after deploying.
const yourContract = await hre.ethers.getContract<Contract>("YourContract", deployer);
console.log("👋 Initial greeting:", await yourContract.greeting());
};
export default deployYourContract;
// Tags are useful if you have multiple deploy files and only want to run one of them.
// e.g. yarn deploy --tags YourContract
deployYourContract.tags = ["YourContract"];

View File

@@ -0,0 +1,23 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { InitialVoiceCreditProxyContractName } from "../constants";
const DEFAULT_INITIAL_VOICE_CREDITS = 99;
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
await hre.deployments.deploy(InitialVoiceCreditProxyContractName, {
from: deployer,
args: [DEFAULT_INITIAL_VOICE_CREDITS],
log: true,
autoMine: true,
});
const initialVoiceCreditProxy = await hre.ethers.getContract(InitialVoiceCreditProxyContractName, deployer);
console.log(`The initial voice credit proxy is deployed at ${await initialVoiceCreditProxy.getAddress()}`);
};
export default deployContracts;
deployContracts.tags = ["InitialVoiceCreditProxy"];

View File

@@ -0,0 +1,21 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { GatekeeperContractName } from "../constants";
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
await hre.deployments.deploy(GatekeeperContractName, {
from: deployer,
args: [],
log: true,
autoMine: true,
});
const gatekeeper = await hre.ethers.getContract(GatekeeperContractName, deployer);
console.log(`The gatekeeper is deployed at ${await gatekeeper.getAddress()}`);
};
export default deployContracts;
deployContracts.tags = ["Gatekeeper"];

View File

@@ -0,0 +1,21 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { VerifierContractName } from "../constants";
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
await hre.deployments.deploy(VerifierContractName, {
from: deployer,
args: [],
log: true,
autoMine: true,
});
const verifier = await hre.ethers.getContract(VerifierContractName, deployer);
console.log(`The verifier is deployed at ${await verifier.getAddress()}`);
};
export default deployContracts;
deployContracts.tags = ["Verifier"];

View File

@@ -0,0 +1,21 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { TopupCreditContractName } from "../constants";
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
await hre.deployments.deploy(TopupCreditContractName, {
from: deployer,
args: [],
log: true,
autoMine: true,
});
const topupCredit = await hre.ethers.getContract(TopupCreditContractName, deployer);
console.log(`The topupCredit is deployed at ${await topupCredit.getAddress()}`);
};
export default deployContracts;
deployContracts.tags = ["TopupCredit"];

View File

@@ -0,0 +1,37 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
async function deployPoseidenContract(
name: "PoseidonT3" | "PoseidonT4" | "PoseidonT5" | "PoseidonT6",
hre: HardhatRuntimeEnvironment,
) {
const { deployer } = await hre.getNamedAccounts();
await hre.deployments.deploy(name, {
from: deployer,
args: [],
log: true,
autoMine: true,
});
const poseidon = await hre.ethers.getContract(name, deployer);
return poseidon;
}
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const poseidonT3 = await deployPoseidenContract("PoseidonT3", hre);
console.log(`The poseidonT3 is deployed at ${await poseidonT3.getAddress()}`);
const poseidonT4 = await deployPoseidenContract("PoseidonT4", hre);
console.log(`The poseidonT4 is deployed at ${await poseidonT4.getAddress()}`);
const poseidonT5 = await deployPoseidenContract("PoseidonT5", hre);
console.log(`The poseidonT5 is deployed at ${await poseidonT5.getAddress()}`);
const poseidonT6 = await deployPoseidenContract("PoseidonT6", hre);
console.log(`The poseidonT6 is deployed at ${await poseidonT6.getAddress()}`);
};
export default deployContracts;
deployContracts.tags = ["Poseidon"];

View File

@@ -0,0 +1,32 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
const poseidonT3 = await hre.ethers.getContract("PoseidonT3", deployer);
const poseidonT4 = await hre.ethers.getContract("PoseidonT4", deployer);
const poseidonT5 = await hre.ethers.getContract("PoseidonT5", deployer);
const poseidonT6 = await hre.ethers.getContract("PoseidonT6", deployer);
await hre.deployments.deploy("PollFactory", {
from: deployer,
args: [],
log: true,
libraries: {
PoseidonT3: await poseidonT3.getAddress(),
PoseidonT4: await poseidonT4.getAddress(),
PoseidonT5: await poseidonT5.getAddress(),
PoseidonT6: await poseidonT6.getAddress(),
},
autoMine: true,
});
const pollFactory = await hre.ethers.getContract("PollFactory", deployer);
console.log(`The poll factory is deployed at ${await pollFactory.getAddress()}`);
};
export default deployContracts;
deployContracts.tags = ["PollFactory"];

View File

@@ -0,0 +1,32 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
const poseidonT3 = await hre.ethers.getContract("PoseidonT3", deployer);
const poseidonT4 = await hre.ethers.getContract("PoseidonT4", deployer);
const poseidonT5 = await hre.ethers.getContract("PoseidonT5", deployer);
const poseidonT6 = await hre.ethers.getContract("PoseidonT6", deployer);
await hre.deployments.deploy("MessageProcessorFactory", {
from: deployer,
args: [],
log: true,
libraries: {
PoseidonT3: await poseidonT3.getAddress(),
PoseidonT4: await poseidonT4.getAddress(),
PoseidonT5: await poseidonT5.getAddress(),
PoseidonT6: await poseidonT6.getAddress(),
},
autoMine: true,
});
const messageProcessorFactory = await hre.ethers.getContract("MessageProcessorFactory", deployer);
console.log(`The message processor factory is deployed at ${await messageProcessorFactory.getAddress()}`);
};
export default deployContracts;
deployContracts.tags = ["MessageProcessorFactory"];

View File

@@ -0,0 +1,33 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { TallyFactoryContractName } from "../constants";
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
const poseidonT3 = await hre.ethers.getContract("PoseidonT3", deployer);
const poseidonT4 = await hre.ethers.getContract("PoseidonT4", deployer);
const poseidonT5 = await hre.ethers.getContract("PoseidonT5", deployer);
const poseidonT6 = await hre.ethers.getContract("PoseidonT6", deployer);
await hre.deployments.deploy(TallyFactoryContractName, {
from: deployer,
args: [],
log: true,
libraries: {
PoseidonT3: await poseidonT3.getAddress(),
PoseidonT4: await poseidonT4.getAddress(),
PoseidonT5: await poseidonT5.getAddress(),
PoseidonT6: await poseidonT6.getAddress(),
},
autoMine: true,
});
const tallyFactory = await hre.ethers.getContract(TallyFactoryContractName, deployer);
console.log(`The tally factory is deployed at ${await tallyFactory.getAddress()}`);
};
export default deployContracts;
deployContracts.tags = ["TallyFactory"];

View File

@@ -0,0 +1,32 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
const poseidonT3 = await hre.ethers.getContract("PoseidonT3", deployer);
const poseidonT4 = await hre.ethers.getContract("PoseidonT4", deployer);
const poseidonT5 = await hre.ethers.getContract("PoseidonT5", deployer);
const poseidonT6 = await hre.ethers.getContract("PoseidonT6", deployer);
await hre.deployments.deploy("SubsidyFactory", {
from: deployer,
args: [],
log: true,
libraries: {
PoseidonT3: await poseidonT3.getAddress(),
PoseidonT4: await poseidonT4.getAddress(),
PoseidonT5: await poseidonT5.getAddress(),
PoseidonT6: await poseidonT6.getAddress(),
},
autoMine: true,
});
const subsidyFactory = await hre.ethers.getContract("SubsidyFactory", deployer);
console.log(`The subsidy factory is deployed at ${await subsidyFactory.getAddress()}`);
};
export default deployContracts;
deployContracts.tags = ["SubsidyFactory"];

View File

@@ -0,0 +1,59 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import {
GatekeeperContractName,
InitialVoiceCreditProxyContractName,
TopupCreditContractName,
stateTreeDepth,
} from "../constants";
import { SignUpGatekeeper } from "../typechain-types";
// const STATE_TREE_SUBDEPTH = 2;
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
const poseidonT3 = await hre.ethers.getContract("PoseidonT3", deployer);
const poseidonT4 = await hre.ethers.getContract("PoseidonT4", deployer);
const poseidonT5 = await hre.ethers.getContract("PoseidonT5", deployer);
const poseidonT6 = await hre.ethers.getContract("PoseidonT6", deployer);
const initialVoiceCreditProxy = await hre.ethers.getContract(InitialVoiceCreditProxyContractName, deployer);
const gatekeeper = await hre.ethers.getContract<SignUpGatekeeper>(GatekeeperContractName, deployer);
const topupCredit = await hre.ethers.getContract(TopupCreditContractName, deployer);
const pollFactory = await hre.ethers.getContract("PollFactory", deployer);
const messageProcessorFactory = await hre.ethers.getContract("MessageProcessorFactory", deployer);
const tallyFactory = await hre.ethers.getContract("TallyFactory", deployer);
const subsidyFactory = await hre.ethers.getContract("SubsidyFactory", deployer);
await hre.deployments.deploy("MACI", {
from: deployer,
args: [
await pollFactory.getAddress(),
await messageProcessorFactory.getAddress(),
await tallyFactory.getAddress(),
await subsidyFactory.getAddress(),
await gatekeeper.getAddress(),
await initialVoiceCreditProxy.getAddress(),
await topupCredit.getAddress(),
stateTreeDepth,
],
log: true,
libraries: {
PoseidonT3: await poseidonT3.getAddress(),
PoseidonT4: await poseidonT4.getAddress(),
PoseidonT5: await poseidonT5.getAddress(),
PoseidonT6: await poseidonT6.getAddress(),
},
autoMine: true,
});
const maci = await hre.ethers.getContract("MACI", deployer);
console.log(`The MACI contract is deployed at ${await maci.getAddress()}`);
await gatekeeper.setMaciInstance(await maci.getAddress());
};
export default deployContracts;
deployContracts.tags = ["MACI"];

View File

@@ -0,0 +1,60 @@
import { extractVk } from "../maci-ts/circuits";
import { VerifyingKey } from "../maci-ts/domainobjs";
import type { IVerifyingKeyStruct } from "../maci-ts/ts/types";
import type { VkRegistry } from "../typechain-types";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import {
intStateTreeDepth,
messageBatchDepth,
messageTreeDepth,
processMessagesZkeyPath,
stateTreeDepth,
subsidyZkeyPath,
tallyVotesZkeyPath,
voteOptionTreeDepth,
} from "../constants";
const deployContracts: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();
await hre.deployments.deploy("VkRegistry", {
from: deployer,
args: [],
log: true,
autoMine: true,
});
const vkRegistry = await hre.ethers.getContract<VkRegistry>("VkRegistry", deployer);
console.log(`The Vk Registry is deployed at ${await vkRegistry.getAddress()}`);
const [processVk, tallyVk, subsidyVk] = await Promise.all([
extractVk(processMessagesZkeyPath),
extractVk(tallyVotesZkeyPath),
subsidyZkeyPath ? extractVk(subsidyZkeyPath) : null,
]).then(vks => vks.map(vk => (vk ? VerifyingKey.fromObj(vk) : null)));
await vkRegistry.setVerifyingKeys(
stateTreeDepth,
intStateTreeDepth,
messageTreeDepth,
voteOptionTreeDepth,
5 ** messageBatchDepth,
processVk!.asContractParam() as IVerifyingKeyStruct,
tallyVk!.asContractParam() as IVerifyingKeyStruct,
);
if (subsidyVk)
await vkRegistry.setSubsidyKeys(
stateTreeDepth,
intStateTreeDepth,
voteOptionTreeDepth,
subsidyVk.asContractParam() as IVerifyingKeyStruct,
);
};
export default deployContracts;
deployContracts.tags = ["VkRegistry"];

View File

@@ -1,5 +1,6 @@
import * as dotenv from "dotenv";
dotenv.config();
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-ethers";
import "@nomicfoundation/hardhat-chai-matchers";
@@ -9,6 +10,7 @@ import "solidity-coverage";
import "@nomicfoundation/hardhat-verify";
import "hardhat-deploy";
import "hardhat-deploy-ethers";
import "hardhat-artifactor";
// If not set, it uses ours Alchemy's default API key.
// You can get your own at https://dashboard.alchemyapi.io
@@ -21,7 +23,7 @@ const etherscanApiKey = process.env.ETHERSCAN_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z
const config: HardhatUserConfig = {
solidity: {
version: "0.8.17",
version: "0.8.10",
settings: {
optimizer: {
enabled: true,

View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -o pipefail
cd "$(dirname "$0")"
cd ..
# Delete old files
rm -rf ./artifacts/*
rm -rf ./cache/*
rm -rf ./typechain-types/*
echo 'Writing Merkle zeros contracts'
bash ./maci-scripts/writeMerkleZeroesContracts.sh
echo 'Writing empty ballot tree root contract'
pnpm exec ts-node maci-ts/ts/genEmptyBallotRootsContract.ts
echo 'Building contracts with Hardhat'
TS_NODE_TRANSPILE_ONLY=1 pnpm exec hardhat compile
echo 'Building Poseidon libraries from bytecode'
pnpm exec ts-node maci-ts/ts/buildPoseidon.ts

View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"
cd ..
# The nothing-up-my-sleeve value
maciNums="8370432830353022751713833565135785980866757267633941821328460903436894336785"
# The hash of a blank state leaf
blankSl="6769006970205099520508948723718471724660867171122235270773600567925038008762"
# Binary tree with zero = 0
pnpm exec ts-node maci-ts/ts/genZerosContract.ts \
MerkleBinary0 0 2 33 "Binary tree zeros (0)" 0 0 \
> contracts/maci-contracts/trees/zeros/MerkleBinary0.sol
# Binary tree with zero = maciNums
pnpm exec ts-node maci-ts/ts/genZerosContract.ts \
MerkleBinaryMaci $maciNums 2 33 "Binary tree zeros (Keccak hash of 'Maci')" 0 0 \
> contracts/maci-contracts/trees/zeros/MerkleBinaryMaci.sol
# Quinary tree with zero = 0
pnpm exec ts-node maci-ts/ts/genZerosContract.ts \
MerkleQuinary0 0 5 33 "Quinary tree zeros (0)" 0 0 \
> contracts/maci-contracts/trees/zeros/MerkleQuinary0.sol
# Quinary tree with zero = maciNums
pnpm exec ts-node maci-ts/ts/genZerosContract.ts \
MerkleQuinaryMaci $maciNums 5 33 "Quinary tree zeros (Keccak hash of 'Maci')" 0 0 \
> contracts/maci-contracts/trees/zeros/MerkleQuinaryMaci.sol
# Quinary tree with zero = blank state leaf
pnpm exec ts-node maci-ts/ts/genZerosContract.ts \
MerkleQuinaryBlankSl $blankSl 5 33 "Quinary tree zeros (hash of a blank state leaf)" 0 0 \
> contracts/maci-contracts/trees/zeros/MerkleQuinaryBlankSl.sol
## Quinary tree with SHA256 for subtrees and zero = maciNums
#pnpm exec ts-node maci-ts/ts/genZerosContract.ts \
#MerkleQuinaryMaciWithSha256 $maciNums 5 33 "Quinary tree (with SHA256) zeros (Keccak hash of 'Maci')" 1 2 \
#> contracts/maci-contracts/trees/zeros/MerkleQuinaryMaciWithSha256.sol

View File

@@ -0,0 +1,30 @@
import { type WitnessTester } from "circomkit";
import { circomkitInstance } from "./utils/utils";
describe("CalculateTotal circuit", () => {
let circuit: WitnessTester<["nums"], ["sum"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("calculateTotal", {
file: "trees/calculateTotal",
template: "CalculateTotal",
params: [6],
});
});
it("should correctly sum a list of values", async () => {
const nums: number[] = [];
for (let i = 0; i < 6; i += 1) {
nums.push(Math.floor(Math.random() * 100));
}
const sum = nums.reduce((a, b) => a + b, 0);
const circuitInputs = {
nums,
};
await circuit.expectPass(circuitInputs, { sum });
});
});

View File

@@ -0,0 +1,354 @@
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { MaciState, Poll, packProcessMessageSmallVals, STATE_TREE_ARITY } from "../../core";
import { hash5, IncrementalQuinTree, NOTHING_UP_MY_SLEEVE, AccQueue } from "../../crypto";
import { PrivKey, Keypair, PCommand, Message, Ballot } from "../../domainobjs";
import { IProcessMessagesInputs, ITallyVotesInputs } from "../types";
import { generateRandomIndex, getSignal, circomkitInstance } from "./utils/utils";
describe("Ceremony param tests", () => {
const params = {
// processMessages and Tally
stateTreeDepth: 6,
// processMessages
messageTreeDepth: 9,
// processMessages
messageBatchTreeDepth: 2,
// processMessages and Tally
voteOptionTreeDepth: 3,
// Tally
stateLeafBatchDepth: 2,
};
const maxValues = {
maxUsers: STATE_TREE_ARITY ** params.stateTreeDepth,
maxMessages: STATE_TREE_ARITY ** params.messageTreeDepth,
maxVoteOptions: STATE_TREE_ARITY ** params.voteOptionTreeDepth,
};
const treeDepths = {
intStateTreeDepth: params.messageBatchTreeDepth,
messageTreeDepth: params.messageTreeDepth,
messageTreeSubDepth: params.messageBatchTreeDepth,
voteOptionTreeDepth: params.voteOptionTreeDepth,
};
const messageBatchSize = STATE_TREE_ARITY ** params.messageBatchTreeDepth;
const voiceCreditBalance = BigInt(100);
const duration = 30;
const coordinatorKeypair = new Keypair();
describe("ProcessMessage circuit", function test() {
this.timeout(900000);
let circuit: WitnessTester<
[
"inputHash",
"packedVals",
"pollEndTimestamp",
"msgRoot",
"msgs",
"msgSubrootPathElements",
"coordPrivKey",
"coordPubKey",
"encPubKeys",
"currentStateRoot",
"currentStateLeaves",
"currentStateLeavesPathElements",
"currentSbCommitment",
"currentSbSalt",
"newSbCommitment",
"newSbSalt",
"currentBallotRoot",
"currentBallots",
"currentBallotsPathElements",
"currentVoteWeights",
"currentVoteWeightsPathElements",
]
>;
let hasherCircuit: WitnessTester<
["packedVals", "coordPubKey", "msgRoot", "currentSbCommitment", "newSbCommitment", "pollEndTimestamp"],
["maxVoteOptions", "numSignUps", "batchStartIndex", "batchEndIndex", "hash"]
>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("processMessages", {
file: "processMessages",
template: "ProcessMessages",
params: [6, 9, 2, 3],
});
hasherCircuit = await circomkitInstance.WitnessTester("processMessageInputHasher", {
file: "processMessages",
template: "ProcessMessagesInputHasher",
});
});
describe("1 user, 2 messages", () => {
const maciState = new MaciState(params.stateTreeDepth);
const voteWeight = BigInt(9);
const voteOptionIndex = BigInt(0);
let stateIndex: bigint;
let pollId: bigint;
let poll: Poll;
const messages: Message[] = [];
const commands: PCommand[] = [];
before(() => {
// Sign up and publish
const userKeypair = new Keypair(new PrivKey(BigInt(1)));
stateIndex = BigInt(
maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))),
);
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
poll = maciState.polls.get(pollId)!;
// update the state
poll.updatePoll(BigInt(maciState.stateLeaves.length));
// First command (valid)
const command = new PCommand(
stateIndex, // BigInt(1),
userKeypair.pubKey,
voteOptionIndex, // voteOptionIndex,
voteWeight, // vote weight
BigInt(2), // nonce
BigInt(pollId),
);
const signature = command.sign(userKeypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
messages.push(message);
commands.push(command);
poll.publishMessage(message, ecdhKeypair.pubKey);
// Second command (valid)
const command2 = new PCommand(
stateIndex,
userKeypair.pubKey,
voteOptionIndex, // voteOptionIndex,
BigInt(1), // vote weight
BigInt(1), // nonce
BigInt(pollId),
);
const signature2 = command2.sign(userKeypair.privKey);
const ecdhKeypair2 = new Keypair();
const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey);
const message2 = command2.encrypt(signature2, sharedKey2);
messages.push(message2);
commands.push(command2);
poll.publishMessage(message2, ecdhKeypair2.pubKey);
// Use the accumulator queue to compare the root of the message tree
const accumulatorQueue: AccQueue = new AccQueue(
params.messageTreeDepth,
STATE_TREE_ARITY,
NOTHING_UP_MY_SLEEVE,
);
accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey));
accumulatorQueue.enqueue(message2.hash(ecdhKeypair2.pubKey));
accumulatorQueue.mergeSubRoots(0);
accumulatorQueue.merge(params.messageTreeDepth);
expect(poll.messageTree.root.toString()).to.be.eq(
accumulatorQueue.getMainRoots()[params.messageTreeDepth].toString(),
);
});
it("should produce the correct state root and ballot root", async () => {
// The current roots
const emptyBallot = new Ballot(poll.maxValues.maxVoteOptions, poll.treeDepths.voteOptionTreeDepth);
const emptyBallotHash = emptyBallot.hash();
const ballotTree = new IncrementalQuinTree(params.stateTreeDepth, emptyBallot.hash(), STATE_TREE_ARITY, hash5);
ballotTree.insert(emptyBallot.hash());
poll.stateLeaves.forEach(() => {
ballotTree.insert(emptyBallotHash);
});
const currentStateRoot = poll.stateTree?.root;
const currentBallotRoot = ballotTree.root;
const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs;
// Calculate the witness
const witness = await circuit.calculateWitness(inputs);
await circuit.expectConstraintPass(witness);
// The new roots, which should differ, since at least one of the
// messages modified a Ballot or State Leaf
const newStateRoot = poll.stateTree?.root;
const newBallotRoot = poll.ballotTree?.root;
expect(newStateRoot?.toString()).not.to.be.eq(currentStateRoot?.toString());
expect(newBallotRoot?.toString()).not.to.be.eq(currentBallotRoot.toString());
const packedVals = packProcessMessageSmallVals(
BigInt(maxValues.maxVoteOptions),
BigInt(poll.maciStateRef.numSignUps),
0,
2,
);
// Test the ProcessMessagesInputHasher circuit
const hasherCircuitInputs = {
packedVals,
coordPubKey: inputs.coordPubKey,
msgRoot: inputs.msgRoot,
currentSbCommitment: inputs.currentSbCommitment,
newSbCommitment: inputs.newSbCommitment,
pollEndTimestamp: inputs.pollEndTimestamp,
};
const hasherWitness = await hasherCircuit.calculateWitness(hasherCircuitInputs);
await hasherCircuit.expectConstraintPass(hasherWitness);
const hash = await getSignal(hasherCircuit, hasherWitness, "hash");
expect(hash.toString()).to.be.eq(inputs.inputHash.toString());
});
});
describe("TallyVotes circuit", function test() {
this.timeout(900000);
let testCircuit: WitnessTester<
[
"stateRoot",
"ballotRoot",
"sbSalt",
"packedVals",
"sbCommitment",
"currentTallyCommitment",
"newTallyCommitment",
"inputHash",
"ballots",
"ballotPathElements",
"votes",
"currentResults",
"currentResultsRootSalt",
"currentSpentVoiceCreditSubtotal",
"currentSpentVoiceCreditSubtotalSalt",
"currentPerVOSpentVoiceCredits",
"currentPerVOSpentVoiceCreditsRootSalt",
"newResultsRootSalt",
"newPerVOSpentVoiceCreditsRootSalt",
"newSpentVoiceCreditSubtotalSalt",
]
>;
before(async () => {
testCircuit = await circomkitInstance.WitnessTester("tallyVotes", {
file: "tallyVotes",
template: "TallyVotes",
params: [6, 2, 3],
});
});
describe("1 user, 2 messages", () => {
let stateIndex: bigint;
let pollId: bigint;
let poll: Poll;
let maciState: MaciState;
const voteWeight = BigInt(9);
const voteOptionIndex = BigInt(0);
beforeEach(() => {
maciState = new MaciState(params.stateTreeDepth);
const messages: Message[] = [];
const commands: PCommand[] = [];
// Sign up and publish
const userKeypair = new Keypair();
stateIndex = BigInt(
maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))),
);
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
poll = maciState.polls.get(pollId)!;
// update the state
poll.updatePoll(BigInt(maciState.stateLeaves.length));
// First command (valid)
const command = new PCommand(
stateIndex,
userKeypair.pubKey,
voteOptionIndex, // voteOptionIndex,
voteWeight, // vote weight
BigInt(1), // nonce
BigInt(pollId),
);
const signature = command.sign(userKeypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
messages.push(message);
commands.push(command);
poll.publishMessage(message, ecdhKeypair.pubKey);
// Use the accumulator queue to compare the root of the message tree
const accumulatorQueue: AccQueue = new AccQueue(
params.messageTreeDepth,
STATE_TREE_ARITY,
NOTHING_UP_MY_SLEEVE,
);
accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey));
accumulatorQueue.mergeSubRoots(0);
accumulatorQueue.merge(params.messageTreeDepth);
expect(poll.messageTree.root.toString()).to.be.eq(
accumulatorQueue.getMainRoots()[params.messageTreeDepth].toString(),
);
// Process messages
poll.processMessages(pollId);
});
it("should produce the correct result commitments", async () => {
const generatedInputs = poll.tallyVotes() as unknown as ITallyVotesInputs;
const witness = await testCircuit.calculateWitness(generatedInputs);
await testCircuit.expectConstraintPass(witness);
});
it("should produce the correct result if the initial tally is not zero", async () => {
const generatedInputs = poll.tallyVotes() as unknown as ITallyVotesInputs;
// Start the tally from non-zero value
let randIdx = generateRandomIndex(Object.keys(generatedInputs).length);
while (randIdx === 0) {
randIdx = generateRandomIndex(Object.keys(generatedInputs).length);
}
generatedInputs.currentResults[randIdx] = 1n;
const witness = await testCircuit.calculateWitness(generatedInputs);
await testCircuit.expectConstraintPass(witness);
});
});
});
});
});

View File

@@ -0,0 +1,67 @@
import chai, { expect } from "chai";
import chaiAsPromised from "chai-as-promised";
import { type WitnessTester } from "circomkit";
import { Keypair } from "../domainobjs";
import { circomkitInstance } from "./utils/utils";
chai.use(chaiAsPromised);
describe("Public key derivation circuit", () => {
let circuit: WitnessTester<["privKey", "pubKey"], ["sharedKey"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("ecdh", {
file: "ecdh",
template: "Ecdh",
});
});
it("correctly computes a public key", async () => {
const keypair = new Keypair();
const keypair2 = new Keypair();
const ecdhSharedKey = Keypair.genEcdhSharedKey(keypair.privKey, keypair2.pubKey);
const circuitInputs = {
privKey: BigInt(keypair.privKey.asCircuitInputs()),
pubKey: keypair2.pubKey.rawPubKey as [bigint, bigint],
};
await circuit.expectPass(circuitInputs, { sharedKey: [ecdhSharedKey[0], ecdhSharedKey[1]] });
});
it("should generate the same ECDH key given the same inputs", async () => {
const keypair = new Keypair();
const keypair2 = new Keypair();
const circuitInputs = {
privKey: BigInt(keypair.privKey.asCircuitInputs()),
pubKey: keypair2.pubKey.asCircuitInputs() as unknown as bigint[],
};
// calculate first time witness and check constraints
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
// read out
const out = await circuit.readWitnessSignals(witness, ["sharedKey"]);
// calculate again
await circuit.expectPass(circuitInputs, { sharedKey: out.sharedKey });
});
it("should throw when given invalid inputs (pubKey too short)", async () => {
const keypair = new Keypair();
const keypair2 = new Keypair();
const circuitInputs = {
privKey: BigInt(keypair.privKey.asCircuitInputs()),
pubKey: keypair2.pubKey.asCircuitInputs().slice(0, 1) as unknown as [bigint, bigint],
};
await expect(circuit.calculateWitness(circuitInputs)).to.be.rejectedWith(
"Not enough values for input signal pubKey",
);
});
});

View File

@@ -0,0 +1,304 @@
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { genRandomSalt, sha256Hash, hashLeftRight, hash13, hash5, hash4, hash3 } from "../../crypto";
import { PCommand, Keypair } from "../../domainobjs";
import { getSignal, circomkitInstance } from "./utils/utils";
describe("Poseidon hash circuits", function test() {
this.timeout(30000);
describe("SHA256", () => {
describe("Sha256HashLeftRight", () => {
let circuit: WitnessTester<["left", "right"], ["hash"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("sha256HashLeftRight", {
file: "hasherSha256",
template: "Sha256HashLeftRight",
});
});
it("should correctly hash two random values", async () => {
const left = genRandomSalt();
const right = genRandomSalt();
const circuitInputs = { left, right };
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output = await getSignal(circuit, witness, "hash");
const outputJS = sha256Hash([left, right]);
expect(output.toString()).to.be.eq(outputJS.toString());
});
});
describe("Sha256Hasher4", () => {
let circuit: WitnessTester<["in"], ["hash"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("sha256Hasher4", {
file: "hasherSha256",
template: "Sha256Hasher4",
});
});
it("should correctly hash 4 random values", async () => {
const preImages: bigint[] = [];
for (let i = 0; i < 4; i += 1) {
preImages.push(genRandomSalt());
}
const circuitInputs = {
in: preImages,
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output = await getSignal(circuit, witness, "hash");
const outputJS = sha256Hash(preImages);
expect(output.toString()).to.be.eq(outputJS.toString());
});
});
describe("Sha256Hasher6", () => {
let circuit: WitnessTester<["in"], ["hash"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("sha256Hasher6", {
file: "hasherSha256",
template: "Sha256Hasher6",
});
});
it("should correctly hash 6 random values", async () => {
const preImages: bigint[] = [];
for (let i = 0; i < 6; i += 1) {
preImages.push(genRandomSalt());
}
const circuitInputs = {
in: preImages,
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output = await getSignal(circuit, witness, "hash");
const outputJS = sha256Hash(preImages);
expect(output.toString()).to.be.eq(outputJS.toString());
});
});
});
describe("Poseidon", () => {
describe("Hasher5", () => {
let circuit: WitnessTester<["in"], ["hash"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("hasher5", {
file: "hasherPoseidon",
template: "Hasher5",
});
});
it("correctly hashes 5 random values", async () => {
const preImages: bigint[] = [];
for (let i = 0; i < 5; i += 1) {
preImages.push(genRandomSalt());
}
const circuitInputs = {
in: preImages,
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output = await getSignal(circuit, witness, "hash");
const outputJS = hash5(preImages);
expect(output.toString()).to.be.eq(outputJS.toString());
});
});
describe("Hasher4", () => {
let circuit: WitnessTester<["in"], ["hash"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("hasher4", {
file: "hasherPoseidon",
template: "Hasher4",
});
});
it("correctly hashes 4 random values", async () => {
const preImages: bigint[] = [];
for (let i = 0; i < 4; i += 1) {
preImages.push(genRandomSalt());
}
const circuitInputs = {
in: preImages,
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output = await getSignal(circuit, witness, "hash");
const outputJS = hash4(preImages);
expect(output.toString()).to.be.eq(outputJS.toString());
});
});
describe("Hasher3", () => {
let circuit: WitnessTester<["in"], ["hash"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("hasher3", {
file: "hasherPoseidon",
template: "Hasher3",
});
});
it("correctly hashes 3 random values", async () => {
const preImages: bigint[] = [];
for (let i = 0; i < 3; i += 1) {
preImages.push(genRandomSalt());
}
const circuitInputs = {
in: preImages,
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output = await getSignal(circuit, witness, "hash");
const outputJS = hash3(preImages);
expect(output.toString()).to.be.eq(outputJS.toString());
});
});
describe("Hasher13", () => {
let circuit: WitnessTester<["in"], ["hash"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("hasher13", {
file: "hasherPoseidon",
template: "Hasher13",
});
});
it("should correctly hash 13 random values", async () => {
const preImages: bigint[] = [];
for (let i = 0; i < 13; i += 1) {
preImages.push(genRandomSalt());
}
const circuitInputs = {
in: preImages,
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output = await getSignal(circuit, witness, "hash");
const outputJS = hash13(preImages);
expect(output.toString()).to.be.eq(outputJS.toString());
});
});
describe("HashLeftRight", () => {
let circuit: WitnessTester<["left", "right"], ["hash"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("hashLeftRight", {
file: "hasherPoseidon",
template: "HashLeftRight",
});
});
it("should correctly hash two random values", async () => {
const left = genRandomSalt();
const right = genRandomSalt();
const circuitInputs = { left, right };
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output = await getSignal(circuit, witness, "hash");
const outputJS = hashLeftRight(left, right);
expect(output.toString()).to.be.eq(outputJS.toString());
});
it("should produce consistent results", async () => {
const left = genRandomSalt();
const right = genRandomSalt();
const circuitInputs = { left, right };
let witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output = await getSignal(circuit, witness, "hash");
witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output2 = await getSignal(circuit, witness, "hash");
expect(output.toString()).to.be.eq(output2.toString());
});
});
});
describe("MessageHasher", () => {
let circuit: WitnessTester<["in", "encPubKey"], ["hash"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("messageHasher", {
file: "messageHasher",
template: "MessageHasher",
});
});
it("should correctly hash a message", async () => {
const k = new Keypair();
const random50bitBigInt = (): bigint =>
// eslint-disable-next-line no-bitwise
((BigInt(1) << BigInt(50)) - BigInt(1)) & BigInt(genRandomSalt().toString());
const command: PCommand = new PCommand(
random50bitBigInt(),
k.pubKey,
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
genRandomSalt(),
);
const { privKey } = new Keypair();
const ecdhSharedKey = Keypair.genEcdhSharedKey(privKey, k.pubKey);
const signature = command.sign(privKey);
const message = command.encrypt(signature, ecdhSharedKey);
const messageHash = message.hash(k.pubKey);
const circuitInputs = {
in: message.asCircuitInputs(),
encPubKey: k.pubKey.asCircuitInputs() as unknown as [bigint, bigint],
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const output = await getSignal(circuit, witness, "hash");
expect(output.toString()).to.be.eq(messageHash.toString());
});
});
});

View File

@@ -0,0 +1,152 @@
import chai, { expect } from "chai";
import chaiAsPromised from "chai-as-promised";
import { type WitnessTester } from "circomkit";
import { IncrementalQuinTree, hash5 } from "../../crypto";
import { getSignal, circomkitInstance } from "./utils/utils";
chai.use(chaiAsPromised);
describe("IncrementalQuinTree circuit", function test() {
this.timeout(50000);
const leavesPerNode = 5;
const treeDepth = 3;
let circuitLeafExists: WitnessTester<["leaf", "path_elements", "path_index", "root"]>;
let circuitGeneratePathIndices: WitnessTester<["in"], ["out"]>;
let circuitQuinSelector: WitnessTester<["in", "index"], ["out"]>;
let splicerCircuit: WitnessTester<["in", "leaf", "index"], ["out"]>;
before(async () => {
circuitLeafExists = await circomkitInstance.WitnessTester("quinLeafExists", {
file: "./trees/incrementalQuinTree",
template: "QuinLeafExists",
params: [3],
});
circuitGeneratePathIndices = await circomkitInstance.WitnessTester("quinGeneratePathIndices", {
file: "./trees/incrementalQuinTree",
template: "QuinGeneratePathIndices",
params: [4],
});
circuitQuinSelector = await circomkitInstance.WitnessTester("quinSelector", {
file: "./trees/incrementalQuinTree",
template: "QuinSelector",
params: [5],
});
splicerCircuit = await circomkitInstance.WitnessTester("splicer", {
file: "./trees/incrementalQuinTree",
template: "Splicer",
params: [4],
});
});
describe("QuinSelector", () => {
it("should return the correct value", async () => {
const circuitInputs = {
index: 0n,
in: [1n, 2n, 3n, 4n, 5n],
};
const witness = await circuitQuinSelector.calculateWitness(circuitInputs);
await circuitQuinSelector.expectConstraintPass(witness);
const out = await getSignal(circuitQuinSelector, witness, "out");
expect(out.toString()).to.be.eq("1");
});
it("should throw when the index is out of range", async () => {
const circuitInputs = {
index: 5n,
in: [1n, 2n, 3n, 4n, 5n],
};
await expect(circuitQuinSelector.calculateWitness(circuitInputs)).to.be.rejectedWith("Assert Failed.");
});
});
describe("Splicer", () => {
it("should insert a value at the correct index", async () => {
const circuitInputs = {
in: [5n, 3n, 20n, 44n],
leaf: 0n,
index: 2n,
};
const witness = await splicerCircuit.calculateWitness(circuitInputs);
await splicerCircuit.expectConstraintPass(witness);
const out1 = await getSignal(splicerCircuit, witness, "out[0]");
const out2 = await getSignal(splicerCircuit, witness, "out[1]");
const out3 = await getSignal(splicerCircuit, witness, "out[2]");
const out4 = await getSignal(splicerCircuit, witness, "out[3]");
const out5 = await getSignal(splicerCircuit, witness, "out[4]");
expect(out1.toString()).to.eq("5");
expect(out2.toString()).to.eq("3");
expect(out3.toString()).to.eq("0");
expect(out4.toString()).to.eq("20");
expect(out5.toString()).to.eq("44");
});
});
describe("QuinGeneratePathIndices", () => {
it("should generate the correct path indices", async () => {
const circuitInputs = {
in: 30n,
};
const witness = await circuitGeneratePathIndices.calculateWitness(circuitInputs);
await circuitGeneratePathIndices.expectConstraintPass(witness);
const out1 = await getSignal(circuitGeneratePathIndices, witness, "out[0]");
const out2 = await getSignal(circuitGeneratePathIndices, witness, "out[1]");
const out3 = await getSignal(circuitGeneratePathIndices, witness, "out[2]");
const out4 = await getSignal(circuitGeneratePathIndices, witness, "out[3]");
expect(out1.toString()).to.be.eq("0");
expect(out2.toString()).to.be.eq("1");
expect(out3.toString()).to.be.eq("1");
expect(out4.toString()).to.be.eq("0");
});
});
describe("QuinLeafExists", () => {
it("should exit correctly when provided the correct leaf", async () => {
const leaves = [1n, 2n, 3n, 4n, 5n];
const tree = new IncrementalQuinTree(treeDepth, 0n, leavesPerNode, hash5);
leaves.forEach(leaf => {
tree.insert(leaf);
});
const proof = tree.genProof(2);
const circuitInputs = {
root: tree.root,
leaf: 3n,
path_elements: proof.pathElements,
path_index: proof.pathIndices,
};
const witness = await circuitLeafExists.calculateWitness(circuitInputs);
await circuitLeafExists.expectConstraintPass(witness);
});
it("should throw when provided an incorrect leaf", async () => {
const circuitInputs = {
root: 30n,
leaf: 0n,
path_elements: [
[1n, 1n, 0n, 0n],
[1n, 1n, 0n, 1n],
[1n, 1n, 1n, 0n],
],
path_index: [0n, 1n, 1n],
};
await expect(circuitLeafExists.calculateWitness(circuitInputs)).to.be.rejectedWith("Assert Failed.");
});
});
});

View File

@@ -0,0 +1,137 @@
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { genRandomSalt, genPrivKey } from "../../crypto";
import { Keypair, PCommand, PrivKey } from "../../domainobjs";
import { circomkitInstance, getSignal } from "./utils/utils";
describe("MessageToCommand circuit", () => {
let circuit: WitnessTester<
["message", "encPubKey", "encPubKey"],
[
"stateIndex",
"newPubKey",
"voteOptionIndex",
"newVoteWeight",
"nonce",
"pollId",
"salt",
"sigR8",
"sigS",
"packedCommandOut",
]
>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("messageToCommand", {
file: "messageToCommand",
template: "MessageToCommand",
});
});
it("should decrypt a Message and output the fields of a Command", async () => {
const { privKey } = new Keypair();
const k = new Keypair();
const pubKey1 = k.pubKey;
const newPubKey = k.pubKey;
const ecdhSharedKey = Keypair.genEcdhSharedKey(privKey, pubKey1);
const random50bitBigInt = (): bigint =>
// eslint-disable-next-line no-bitwise
((BigInt(1) << BigInt(50)) - BigInt(1)) & BigInt(genRandomSalt().toString());
const command: PCommand = new PCommand(
random50bitBigInt(),
newPubKey,
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
// genRandomSalt(),
BigInt(123),
);
const signature = command.sign(privKey);
const message = command.encrypt(signature, ecdhSharedKey);
const circuitInputs = {
message: message.asCircuitInputs(),
encPrivKey: privKey.asCircuitInputs() as unknown as bigint,
encPubKey: pubKey1.asCircuitInputs() as unknown as bigint[],
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const stateIndexOut = await getSignal(circuit, witness, "stateIndex");
expect(command.stateIndex.toString()).to.be.eq(stateIndexOut.toString());
const newPubKey0 = await getSignal(circuit, witness, "newPubKey[0]");
expect(command.newPubKey.rawPubKey[0].toString()).to.be.eq(newPubKey0.toString());
const newPubKey1 = await getSignal(circuit, witness, "newPubKey[1]");
expect(command.newPubKey.rawPubKey[1].toString()).to.be.eq(newPubKey1.toString());
const voteOptionIndex = await getSignal(circuit, witness, "voteOptionIndex");
expect(command.voteOptionIndex.toString()).to.be.eq(voteOptionIndex.toString());
const newVoteWeight = await getSignal(circuit, witness, "newVoteWeight");
expect(command.newVoteWeight.toString()).to.be.eq(newVoteWeight.toString());
const nonce = await getSignal(circuit, witness, "nonce");
expect(command.nonce.toString()).to.be.eq(nonce.toString());
const pollId = await getSignal(circuit, witness, "pollId");
expect(command.pollId.toString()).to.be.eq(pollId.toString());
const salt = await getSignal(circuit, witness, "salt");
expect(command.salt.toString()).to.be.eq(salt.toString());
const sigR80 = await getSignal(circuit, witness, "sigR8[0]");
expect(signature.R8[0].toString()).to.be.eq(sigR80.toString());
const sigR81 = await getSignal(circuit, witness, "sigR8[1]");
expect(signature.R8[1].toString()).to.be.eq(sigR81.toString());
const sigS = await getSignal(circuit, witness, "sigS");
expect(signature.S.toString()).to.be.eq(sigS.toString());
});
it("should not throw when given an invalid key which cannot decrypt a Message", async () => {
const { privKey } = new Keypair();
const k = new Keypair();
const pubKey1 = k.pubKey;
const newPubKey = k.pubKey;
const ecdhSharedKey = Keypair.genEcdhSharedKey(privKey, pubKey1);
const random50bitBigInt = (): bigint =>
// eslint-disable-next-line no-bitwise
((BigInt(1) << BigInt(50)) - BigInt(1)) & BigInt(genRandomSalt().toString());
const command: PCommand = new PCommand(
random50bitBigInt(),
newPubKey,
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
// genRandomSalt(),
BigInt(123),
);
const signature = command.sign(privKey);
const message = command.encrypt(signature, ecdhSharedKey);
const circuitInputs = {
message: message.asCircuitInputs(),
// invalid private key
encPrivKey: new PrivKey(genPrivKey()).asCircuitInputs() as unknown as bigint,
encPubKey: pubKey1.asCircuitInputs() as unknown as [bigint, bigint],
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
});
});

View File

@@ -0,0 +1,156 @@
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { SignalValueType } from "circomkit/dist/types/circuit";
import { genRandomSalt } from "../../crypto";
import { PCommand, Keypair } from "../../domainobjs";
import { type IMessageValidatorCircuitInputs } from "./utils/types";
import { getSignal, circomkitInstance } from "./utils/utils";
describe("MessageValidator circuit", function test() {
this.timeout(90000);
let circuitInputs: IMessageValidatorCircuitInputs;
let circuit: WitnessTester<
[
"stateTreeIndex",
"numSignUps",
"voteOptionIndex",
"maxVoteOptions",
"originalNonce",
"nonce",
"cmd",
"pubKey",
"sigR8",
"sigS",
"currentVoiceCreditBalance",
"currentVotesForOption",
"voteWeight",
"slTimestamp",
"pollEndTimestamp",
],
["isValid"]
>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("messageValidator", {
file: "messageValidator",
template: "MessageValidator",
});
});
before(() => {
const { privKey, pubKey } = new Keypair();
// Note that the command fields don't matter in this test
const command: PCommand = new PCommand(
BigInt(1),
pubKey,
BigInt(2),
BigInt(3),
BigInt(4),
BigInt(5),
genRandomSalt(),
);
const signature = command.sign(privKey);
circuitInputs = {
stateTreeIndex: 0n as SignalValueType,
numSignUps: 1n,
voteOptionIndex: 0n,
maxVoteOptions: 1n,
originalNonce: 1n,
nonce: 2n,
cmd: command.asCircuitInputs(),
pubKey: pubKey.asCircuitInputs() as unknown as [bigint, bigint],
sigR8: signature.R8 as unknown as bigint,
sigS: signature.S as bigint,
currentVoiceCreditBalance: 100n,
currentVotesForOption: 0n,
voteWeight: 9n,
slTimestamp: 1n,
pollEndTimestamp: 2n,
};
});
it("should pass if all inputs are valid", async () => {
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("1");
});
it("should be invalid if the signature is invalid", async () => {
const circuitInputs2 = circuitInputs;
circuitInputs2.sigS = 0n;
const witness = await circuit.calculateWitness(circuitInputs2);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("0");
});
it("should be invalid if the pubkey is invalid", async () => {
const circuitInputs2 = circuitInputs;
circuitInputs2.pubKey = [0n, 1n];
const witness = await circuit.calculateWitness(circuitInputs2);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("0");
});
it("should be invalid if there are insufficient voice credits", async () => {
const circuitInputs2 = circuitInputs;
circuitInputs2.voteWeight = 11n;
const witness = await circuit.calculateWitness(circuitInputs2);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("0");
});
it("should be invalid if the nonce is invalid", async () => {
const circuitInputs2 = circuitInputs;
circuitInputs2.nonce = 3n;
const witness = await circuit.calculateWitness(circuitInputs2);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("0");
});
it("should be invalid if the state leaf index is invalid", async () => {
const circuitInputs2 = circuitInputs;
circuitInputs2.stateTreeIndex = 2n;
const witness = await circuit.calculateWitness(circuitInputs2);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("0");
});
it("should be invalid if the vote option index is invalid", async () => {
const circuitInputs2 = circuitInputs;
circuitInputs2.voteOptionIndex = 1n;
const witness = await circuit.calculateWitness(circuitInputs2);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("0");
});
it("should be invalid if the vote option index is invalid", async () => {
const circuitInputs2 = circuitInputs;
circuitInputs2.voteOptionIndex = 6049261729n;
const witness = await circuit.calculateWitness(circuitInputs2);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("0");
});
it("should be invalid if the state leaf timestamp is too high", async () => {
const circuitInputs2 = circuitInputs;
circuitInputs2.slTimestamp = 3n;
const witness = await circuit.calculateWitness(circuitInputs2);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("0");
});
});

View File

@@ -0,0 +1,51 @@
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { SNARK_FIELD_SIZE } from "../../crypto";
import { Keypair } from "../../domainobjs";
import { circomkitInstance, getSignal } from "./utils/utils";
describe("Public key derivation circuit", function test() {
this.timeout(90000);
let circuit: WitnessTester<["privKey"], ["pubKey"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("privToPubKey", {
file: "privToPubKey",
template: "PrivToPubKey",
});
});
it("should correctly compute a public key", async () => {
const keypair = new Keypair();
const circuitInputs = {
privKey: keypair.privKey.asCircuitInputs() as unknown as bigint,
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const derivedPubkey0 = await getSignal(circuit, witness, "pubKey[0]");
const derivedPubkey1 = await getSignal(circuit, witness, "pubKey[1]");
expect(derivedPubkey0.toString()).to.be.eq(keypair.pubKey.rawPubKey[0].toString());
expect(derivedPubkey1.toString()).to.be.eq(keypair.pubKey.rawPubKey[1].toString());
});
it("should produce an output that is within the baby jubjub curve", async () => {
const keypair = new Keypair();
const circuitInputs = {
privKey: keypair.privKey.asCircuitInputs() as unknown as bigint,
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const derivedPubkey0 = await getSignal(circuit, witness, "pubKey[0]");
const derivedPubkey1 = await getSignal(circuit, witness, "pubKey[1]");
expect(derivedPubkey0 < SNARK_FIELD_SIZE).to.eq(true);
expect(derivedPubkey1 < SNARK_FIELD_SIZE).to.eq(true);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
import chai, { expect } from "chai";
import chaiAsPromised from "chai-as-promised";
import { type WitnessTester } from "circomkit";
import { IncrementalQuinTree, hash5 } from "../../crypto";
import { getSignal, circomkitInstance } from "./utils/utils";
chai.use(chaiAsPromised);
describe("QuinCheckRoot circuit", function test() {
this.timeout(50000);
const leavesPerNode = 5;
const treeDepth = 3;
let circuit: WitnessTester<["leaves"], ["root"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("checkRoot", {
file: "trees/checkRoot",
template: "QuinCheckRoot",
params: [3],
});
});
it("should compute the correct merkle root", async () => {
const leaves = Array<bigint>(leavesPerNode ** treeDepth).fill(5n);
const circuitInputs = {
leaves,
};
const tree = new IncrementalQuinTree(3, 0n, 5, hash5);
leaves.forEach(leaf => {
tree.insert(leaf);
});
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const circuitRoot = await getSignal(circuit, witness, "root");
expect(circuitRoot.toString()).to.be.eq(tree.root.toString());
});
it("should not accept less leaves than a full tree", async () => {
const leaves = Array<bigint>(leavesPerNode ** treeDepth - 1).fill(5n);
const circuitInputs = {
leaves,
};
await expect(circuit.calculateWitness(circuitInputs)).to.be.rejectedWith(
"Not enough values for input signal leaves",
);
});
});

View File

@@ -0,0 +1,140 @@
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { genRandomSalt } from "../../crypto";
import { PCommand, Keypair } from "../../domainobjs";
import { getSignal, circomkitInstance } from "./utils/utils";
describe("StateLeafAndBallotTransformer circuit", function test() {
this.timeout(90000);
// variables needed for testing
const keypair = new Keypair();
const stateIndex = BigInt(1);
const newPubKey = keypair.pubKey;
const voteOptionIndex = BigInt(0);
const newVoteWeight = BigInt(9);
const nonce = BigInt(1);
const pollId = BigInt(0);
const salt = genRandomSalt();
const numSignUps = 25n;
const maxVoteOptions = 25n;
const slKeypair = new Keypair();
const slPubKey = slKeypair.pubKey;
const slVoiceCreditBalance = BigInt(100);
const ballotNonce = BigInt(0);
const ballotCurrentVotesForOption = BigInt(0);
const slTimestamp = 1n;
const pollEndTimestamp = 2n;
const command: PCommand = new PCommand(stateIndex, newPubKey, voteOptionIndex, newVoteWeight, nonce, pollId, salt);
const signature = command.sign(slKeypair.privKey);
let circuit: WitnessTester<
[
"numSignUps",
"maxVoteOptions",
"slPubKey",
"slVoiceCreditBalance",
"slTimestamp",
"pollEndTimestamp",
"ballotNonce",
"ballotCurrentVotesForOption",
"cmdStateIndex",
"cmdNewPubKey",
"cmdVoteOptionIndex",
"cmdNewVoteWeight",
"cmdNonce",
"cmdPollId",
"cmdSalt",
"cmdSigR8",
"cmdSigS",
"packedCommand",
],
["newSlPubKey", "newBallotNonce", "isValid"]
>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("stateLeafAndBallotTransformer", {
file: "stateLeafAndBallotTransformer",
template: "StateLeafAndBallotTransformer",
});
});
it("should output new state leaf and ballot values if the command is valid", async () => {
const circuitInputs = {
numSignUps,
maxVoteOptions,
slPubKey: slPubKey.asCircuitInputs() as unknown as [bigint, bigint],
slVoiceCreditBalance,
slTimestamp,
pollEndTimestamp,
ballotNonce,
ballotCurrentVotesForOption,
cmdStateIndex: command.stateIndex,
cmdNewPubKey: command.newPubKey.asCircuitInputs() as unknown as [bigint, bigint],
cmdVoteOptionIndex: command.voteOptionIndex,
cmdNewVoteWeight: command.newVoteWeight,
cmdNonce: command.nonce,
cmdPollId: command.pollId,
cmdSalt: command.salt,
cmdSigR8: signature.R8 as [bigint, bigint],
cmdSigS: signature.S as bigint,
packedCommand: command.asCircuitInputs(),
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const newSlPubKey0 = await getSignal(circuit, witness, "newSlPubKey[0]");
const newSlPubKey1 = await getSignal(circuit, witness, "newSlPubKey[1]");
const newBallotNonce = await getSignal(circuit, witness, "newBallotNonce");
expect(newSlPubKey0.toString()).to.be.eq(command.newPubKey.rawPubKey[0].toString());
expect(newSlPubKey1.toString()).to.be.eq(command.newPubKey.rawPubKey[1].toString());
expect(newBallotNonce.toString()).to.be.eq(command.nonce.toString());
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("1");
});
it("should output existing state leaf and ballot values if the command is invalid", async () => {
const circuitInputs = {
numSignUps,
maxVoteOptions,
slPubKey: slPubKey.asCircuitInputs() as unknown as [bigint, bigint],
slVoiceCreditBalance,
slTimestamp,
pollEndTimestamp,
ballotNonce,
ballotCurrentVotesForOption,
cmdStateIndex: command.stateIndex,
cmdNewPubKey: command.newPubKey.asCircuitInputs() as unknown as [bigint, bigint],
cmdVoteOptionIndex: command.voteOptionIndex,
cmdNewVoteWeight: command.newVoteWeight,
cmdNonce: 2n, // invalid
cmdPollId: command.pollId,
cmdSalt: command.salt,
cmdSigR8: signature.R8 as [bigint, bigint],
cmdSigS: signature.S as bigint,
packedCommand: command.asCircuitInputs(),
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const newSlPubKey0 = await getSignal(circuit, witness, "newSlPubKey[0]");
const newSlPubKey1 = await getSignal(circuit, witness, "newSlPubKey[1]");
const newBallotNonce = await getSignal(circuit, witness, "newBallotNonce");
expect(newSlPubKey0.toString()).to.be.eq(slPubKey.rawPubKey[0].toString());
expect(newSlPubKey1.toString()).to.be.eq(slPubKey.rawPubKey[1].toString());
expect(newBallotNonce.toString()).to.be.eq("0");
const isValid = await getSignal(circuit, witness, "isValid");
expect(isValid.toString()).to.be.eq("0");
});
});

View File

@@ -0,0 +1,304 @@
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { MaciState, Poll, STATE_TREE_ARITY } from "../../core";
import { AccQueue, NOTHING_UP_MY_SLEEVE } from "../../crypto";
import { Keypair, PCommand, Message } from "../../domainobjs";
import { ITallyVotesInputs } from "../types";
import { STATE_TREE_DEPTH, duration, maxValues, messageBatchSize, voiceCreditBalance } from "./utils/constants";
import { generateRandomIndex, circomkitInstance } from "./utils/utils";
describe("TallyVotes circuit", function test() {
this.timeout(900000);
const treeDepths = {
intStateTreeDepth: 1,
messageTreeDepth: 2,
messageTreeSubDepth: 1,
voteOptionTreeDepth: 2,
};
const coordinatorKeypair = new Keypair();
type TallyVotesCircuitInputs = [
"stateRoot",
"ballotRoot",
"sbSalt",
"packedVals",
"sbCommitment",
"currentTallyCommitment",
"newTallyCommitment",
"inputHash",
"ballots",
"ballotPathElements",
"votes",
"currentResults",
"currentResultsRootSalt",
"currentSpentVoiceCreditSubtotal",
"currentSpentVoiceCreditSubtotalSalt",
"currentPerVOSpentVoiceCredits",
"currentPerVOSpentVoiceCreditsRootSalt",
"newResultsRootSalt",
"newPerVOSpentVoiceCreditsRootSalt",
"newSpentVoiceCreditSubtotalSalt",
];
let circuit: WitnessTester<TallyVotesCircuitInputs>;
let circuitNonQv: WitnessTester<TallyVotesCircuitInputs>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("tallyVotes", {
file: "tallyVotes",
template: "TallyVotes",
params: [10, 1, 2],
});
circuitNonQv = await circomkitInstance.WitnessTester("tallyVotesNonQv", {
file: "tallyVotesNonQv",
template: "TallyVotesNonQv",
params: [10, 1, 2],
});
});
describe("1 user, 2 messages", () => {
let stateIndex: bigint;
let pollId: bigint;
let poll: Poll;
let maciState: MaciState;
const voteWeight = BigInt(9);
const voteOptionIndex = BigInt(0);
beforeEach(() => {
maciState = new MaciState(STATE_TREE_DEPTH);
const messages: Message[] = [];
const commands: PCommand[] = [];
// Sign up and publish
const userKeypair = new Keypair();
stateIndex = BigInt(
maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))),
);
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
poll = maciState.polls.get(pollId)!;
poll.updatePoll(stateIndex);
// First command (valid)
const command = new PCommand(
stateIndex,
userKeypair.pubKey,
voteOptionIndex, // voteOptionIndex,
voteWeight, // vote weight
BigInt(1), // nonce
BigInt(pollId),
);
const signature = command.sign(userKeypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
messages.push(message);
commands.push(command);
poll.publishMessage(message, ecdhKeypair.pubKey);
// Use the accumulator queue to compare the root of the message tree
const accumulatorQueue: AccQueue = new AccQueue(
treeDepths.messageTreeSubDepth,
STATE_TREE_ARITY,
NOTHING_UP_MY_SLEEVE,
);
accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey));
accumulatorQueue.mergeSubRoots(0);
accumulatorQueue.merge(treeDepths.messageTreeDepth);
expect(poll.messageTree.root.toString()).to.be.eq(
accumulatorQueue.getMainRoots()[treeDepths.messageTreeDepth].toString(),
);
// Process messages
poll.processMessages(pollId);
});
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);
});
it("should produce the correct result if the initial tally is not zero", async () => {
const generatedInputs = poll.tallyVotes() as unknown as ITallyVotesInputs;
// Start the tally from non-zero value
let randIdx = generateRandomIndex(Object.keys(generatedInputs).length);
while (randIdx === 0) {
randIdx = generateRandomIndex(Object.keys(generatedInputs).length);
}
generatedInputs.currentResults[randIdx] = 1n;
const witness = await circuit.calculateWitness(generatedInputs);
await circuit.expectConstraintPass(witness);
});
});
describe("1 user, 2 messages (non qv)", () => {
let stateIndex: bigint;
let pollId: bigint;
let poll: Poll;
let maciState: MaciState;
const voteWeight = BigInt(9);
const voteOptionIndex = BigInt(0);
beforeEach(() => {
maciState = new MaciState(STATE_TREE_DEPTH);
const messages: Message[] = [];
const commands: PCommand[] = [];
// Sign up and publish
const userKeypair = new Keypair();
stateIndex = BigInt(
maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))),
);
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
poll = maciState.polls.get(pollId)!;
poll.updatePoll(stateIndex);
// First command (valid)
const command = new PCommand(
stateIndex,
userKeypair.pubKey,
voteOptionIndex, // voteOptionIndex,
voteWeight, // vote weight
BigInt(1), // nonce
BigInt(pollId),
);
const signature = command.sign(userKeypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
messages.push(message);
commands.push(command);
poll.publishMessage(message, ecdhKeypair.pubKey);
// Use the accumulator queue to compare the root of the message tree
const accumulatorQueue: AccQueue = new AccQueue(
treeDepths.messageTreeSubDepth,
STATE_TREE_ARITY,
NOTHING_UP_MY_SLEEVE,
);
accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey));
accumulatorQueue.mergeSubRoots(0);
accumulatorQueue.merge(treeDepths.messageTreeDepth);
expect(poll.messageTree.root.toString()).to.be.eq(
accumulatorQueue.getMainRoots()[treeDepths.messageTreeDepth].toString(),
);
// Process messages
poll.processMessages(pollId, false);
});
it("should produce the correct result commitments", async () => {
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 initial tally is not zero", async () => {
const generatedInputs = poll.tallyVotesNonQv() as unknown as ITallyVotesInputs;
// Start the tally from non-zero value
let randIdx = generateRandomIndex(Object.keys(generatedInputs).length);
while (randIdx === 0) {
randIdx = generateRandomIndex(Object.keys(generatedInputs).length);
}
generatedInputs.currentResults[randIdx] = 1n;
const witness = await circuitNonQv.calculateWitness(generatedInputs);
await circuitNonQv.expectConstraintPass(witness);
});
});
const NUM_BATCHES = 2;
const x = messageBatchSize * NUM_BATCHES;
describe(`${x} users, ${x} messages`, () => {
it("should produce the correct state root and ballot root", async () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
const userKeypairs: Keypair[] = [];
for (let i = 0; i < x; i += 1) {
const k = new Keypair();
userKeypairs.push(k);
maciState.signUp(k.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000) + duration));
}
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const poll = maciState.polls.get(pollId)!;
poll.updatePoll(BigInt(maciState.stateLeaves.length));
const numMessages = messageBatchSize * NUM_BATCHES;
for (let i = 0; i < numMessages; i += 1) {
const command = new PCommand(
BigInt(i),
userKeypairs[i].pubKey,
BigInt(i), // vote option index
BigInt(1), // vote weight
BigInt(1), // nonce
BigInt(pollId),
);
const signature = command.sign(userKeypairs[i].privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
}
for (let i = 0; i < NUM_BATCHES; i += 1) {
poll.processMessages(pollId);
}
for (let i = 0; i < NUM_BATCHES; i += 1) {
const generatedInputs = poll.tallyVotes() as unknown as ITallyVotesInputs;
// For the 0th batch, the circuit should ignore currentResults,
// currentSpentVoiceCreditSubtotal, and
// currentPerVOSpentVoiceCredits
if (i === 0) {
generatedInputs.currentResults[0] = 123n;
generatedInputs.currentSpentVoiceCreditSubtotal = 456n;
generatedInputs.currentPerVOSpentVoiceCredits[0] = 789n;
}
// eslint-disable-next-line no-await-in-loop
const witness = await circuit.calculateWitness(generatedInputs);
// eslint-disable-next-line no-await-in-loop
await circuit.expectConstraintPass(witness);
}
});
});
});

View File

@@ -0,0 +1,75 @@
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { genRandomSalt } from "../../crypto";
import { getSignal, circomkitInstance } from "./utils/utils";
describe("UnpackElement circuit", () => {
let circuit: WitnessTester<["in"], ["out"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("unpackElement", {
file: "unpackElement",
template: "UnpackElement",
params: [5],
});
});
it("should unpack a field element with 5 packed values correctly", async () => {
const elements: string[] = [];
for (let i = 0; i < 5; i += 1) {
let e = (BigInt(genRandomSalt().toString()) % BigInt(2 ** 50)).toString(2);
while (e.length < 50) {
e = `0${e}`;
}
elements.push(e);
}
const circuitInputs = {
in: BigInt(`0b${elements.join("")}`),
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
for (let i = 0; i < 5; i += 1) {
// eslint-disable-next-line no-await-in-loop
const out = await getSignal(circuit, witness, `out[${i}]`);
expect(BigInt(`0b${BigInt(out).toString(2)}`).toString()).to.be.eq(BigInt(`0b${elements[i]}`).toString());
}
});
describe("unpackElement4", () => {
before(async () => {
circuit = await circomkitInstance.WitnessTester("unpackElement", {
file: "unpackElement",
template: "UnpackElement",
params: [4],
});
});
it("should unpack a field element with 4 packed values correctly", async () => {
const elements: string[] = [];
for (let i = 0; i < 4; i += 1) {
let e = (BigInt(genRandomSalt().toString()) % BigInt(2 ** 50)).toString(2);
while (e.length < 50) {
e = `0${e}`;
}
elements.push(e);
}
const circuitInputs = {
in: BigInt(`0b${elements.join("")}`),
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
for (let i = 0; i < 4; i += 1) {
// eslint-disable-next-line no-await-in-loop
const out = await getSignal(circuit, witness, `out[${i}]`);
expect(BigInt(`0b${BigInt(out).toString(2)}`).toString()).to.be.eq(BigInt(`0b${elements[i]}`).toString());
}
});
});
});

View File

@@ -0,0 +1,101 @@
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { verifySignature, hash4 } from "../../crypto";
import { Keypair, PCommand } from "../../domainobjs";
import { getSignal, circomkitInstance } from "./utils/utils";
describe("Signature verification circuit", function test() {
this.timeout(90000);
let circuit: WitnessTester<["pubKey", "R8", "S", "preimage"], ["valid"]>;
before(async () => {
circuit = await circomkitInstance.WitnessTester("verifySignature", {
file: "verifySignature",
template: "VerifySignature",
});
});
it("should verify a valid signature", async () => {
const keypair = new Keypair();
const command = new PCommand(BigInt(0), keypair.pubKey, BigInt(123), BigInt(123), BigInt(1), BigInt(2), BigInt(3));
const signer = new Keypair();
const sig = command.sign(signer.privKey);
const plaintext = hash4(command.asArray());
expect(verifySignature(plaintext, sig, signer.pubKey.rawPubKey)).to.eq(true);
const circuitInputs = {
pubKey: signer.pubKey.asCircuitInputs() as unknown as [bigint, bigint],
R8: sig.R8 as [bigint, bigint],
S: sig.S as bigint,
preimage: command.asCircuitInputs(),
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "valid");
expect(isValid.toString()).to.be.eq("1");
});
it("should reject an invalid signature (wrong signer)", async () => {
const keypair = new Keypair();
const command = new PCommand(BigInt(0), keypair.pubKey, BigInt(123), BigInt(123), BigInt(1), BigInt(2), BigInt(3));
const signer = new Keypair();
const wrongSigner = new Keypair();
expect(signer.privKey.rawPrivKey).not.to.be.eq(wrongSigner.privKey.rawPrivKey);
const sig = command.sign(signer.privKey);
const plaintext = hash4(command.asArray());
// The signature is signed by `signer`
expect(verifySignature(plaintext, sig, signer.pubKey.rawPubKey)).to.eq(true);
// The signature is not signed by `wrongSigner`
expect(verifySignature(plaintext, sig, wrongSigner.pubKey.rawPubKey)).to.eq(false);
const circuitInputs = {
pubKey: wrongSigner.pubKey.asCircuitInputs() as unknown as [bigint, bigint],
R8: sig.R8 as [bigint, bigint],
S: sig.S as bigint,
preimage: command.asCircuitInputs(),
};
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "valid");
expect(isValid.toString()).to.be.eq("0");
expect((await getSignal(circuit, witness, "verifier.isCcZero.out")).toString()).to.be.eq("1");
});
it("should reject an invalid signature (wrong S)", async () => {
const keypair = new Keypair();
const command = new PCommand(BigInt(0), keypair.pubKey, BigInt(123), BigInt(123), BigInt(1), BigInt(2), BigInt(3));
const signer = new Keypair();
const sig = command.sign(signer.privKey);
const plaintext = hash4(command.asArray());
expect(verifySignature(plaintext, sig, signer.pubKey.rawPubKey)).to.eq(true);
const circuitInputs = {
pubKey: signer.pubKey.asCircuitInputs() as unknown as [bigint, bigint],
R8: sig.R8 as [bigint, bigint],
S: BigInt("2736030358979909402780800718157159386076813972158567259200215660948447373040") + BigInt(1),
preimage: command.asCircuitInputs(),
};
expect(verifySignature(plaintext, sig, signer.pubKey.rawPubKey)).to.eq(true);
const witness = await circuit.calculateWitness(circuitInputs);
await circuit.expectConstraintPass(witness);
const isValid = await getSignal(circuit, witness, "valid");
expect(isValid.toString()).to.be.eq("0");
expect((await getSignal(circuit, witness, "verifier.isCcZero.out")).toString()).to.be.eq("0");
});
});

View File

@@ -0,0 +1,15 @@
export const STATE_TREE_DEPTH = 10;
export const voiceCreditBalance = BigInt(100);
export const duration = 30;
export const maxValues = {
maxUsers: 25,
maxMessages: 25,
maxVoteOptions: 25,
};
export const treeDepths = {
intStateTreeDepth: 2,
messageTreeDepth: 2,
messageTreeSubDepth: 1,
voteOptionTreeDepth: 2,
};
export const messageBatchSize = 5;

View File

@@ -0,0 +1,22 @@
import { type SignalValueType } from "circomkit/dist/types/circuit";
/**
* Circuit inputs for testing the MessageValidator circuit
*/
export interface IMessageValidatorCircuitInputs {
stateTreeIndex: SignalValueType;
numSignUps: SignalValueType;
voteOptionIndex: SignalValueType;
maxVoteOptions: SignalValueType;
originalNonce: SignalValueType;
nonce: SignalValueType;
cmd: SignalValueType;
pubKey: SignalValueType;
sigR8: SignalValueType;
sigS: SignalValueType;
currentVoiceCreditBalance: SignalValueType;
currentVotesForOption: SignalValueType;
voteWeight: SignalValueType;
slTimestamp: SignalValueType;
pollEndTimestamp: SignalValueType;
}

View File

@@ -0,0 +1,45 @@
import { Circomkit, type WitnessTester, type CircomkitConfig } from "circomkit";
import fs from "fs";
import path from "path";
const configFilePath = path.resolve(__dirname, "..", "..", "..", "circomkit.json");
const config = JSON.parse(fs.readFileSync(configFilePath, "utf-8")) as CircomkitConfig;
export const circomkitInstance = new Circomkit({
...config,
verbose: false,
});
/**
* Convert a string to a bigint
* @param s - the string to convert
* @returns the bigint representation of the string
*/
export const str2BigInt = (s: string): bigint => BigInt(parseInt(Buffer.from(s).toString("hex"), 16));
/**
* Generate a random number within a certain threshold
* @param upper - the upper bound
* @returns the random index
*/
export const generateRandomIndex = (upper: number): number => Math.floor(Math.random() * (upper - 1));
// @note thanks https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/test/utils.ts
// for the code below (modified version)
/**
* Get a signal from the circuit
* @param circuit - the circuit object
* @param witness - the witness
* @param name - the name of the signal
* @returns the signal value
*/
export const getSignal = async (tester: WitnessTester, witness: bigint[], name: string): Promise<bigint> => {
const prefix = "main";
// E.g. the full name of the signal "root" is "main.root"
// You can look up the signal names using `circuit.getDecoratedOutput(witness))`
const signalFullName = `${prefix}.${name}`;
const out = await tester.readWitness(witness, [signalFullName]);
return BigInt(out[signalFullName]);
};

View File

@@ -0,0 +1,91 @@
import { type CircomkitConfig, type CircuitConfig, Circomkit } from "circomkit";
import { execFileSync } from "child_process";
import fs from "fs";
import path from "path";
import type { CircuitConfigWithName } from "./types";
/**
* Compile MACI's circuits using circomkit
* and setup the dir structure
* @param cWitness - whether to compile to the c witness generator
* or not
* @param outputPath - the path to the output folder
* @returns the build directory
*/
export const compileCircuits = async (cWitness?: boolean, outputPath?: string): Promise<string> => {
// read circomkit config files
const configFilePath = path.resolve(__dirname, "..", "circomkit.json");
const circomKitConfig = JSON.parse(fs.readFileSync(configFilePath, "utf-8")) as CircomkitConfig;
const circuitsConfigPath = path.resolve(__dirname, "..", "circom", "circuits.json");
const circuitsConfigContent = JSON.parse(fs.readFileSync(circuitsConfigPath, "utf-8")) as unknown as Record<
string,
CircuitConfig
>;
const circuitsConfigs: CircuitConfigWithName[] = Object.entries(circuitsConfigContent).map(([name, config]) => ({
name,
...config,
}));
// generate the absolute path to the output folder
const outputPathUpdated = outputPath ? path.resolve(outputPath) : undefined;
// set the config based on whether to compile the c witness or no
if (cWitness) {
circomKitConfig.cWitness = true;
} else {
circomKitConfig.cWitness = false;
}
// update the build directory if we have an output path
if (outputPathUpdated) {
circomKitConfig.dirBuild = outputPathUpdated;
circomKitConfig.dirPtau = outputPathUpdated;
}
// create an instance of circomkit with a custom config
const circomkitInstance = new Circomkit({
...circomKitConfig,
verbose: false,
});
// loop through each circuit config and compile them
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < circuitsConfigs.length; i += 1) {
const circuit = circuitsConfigs[i];
// eslint-disable-next-line no-console
console.log(`Compiling ${circuit.name}...`);
// eslint-disable-next-line no-await-in-loop
const outPath = await circomkitInstance.compile(circuit.name, circuit);
// if the circuit is compiled with a c witness, then let's run make in the directory
if (cWitness) {
try {
// build
execFileSync("bash", ["-c", `cd ${outPath}/${circuit.name}_cpp && make`]);
} catch (error) {
throw new Error(`Failed to compile the c witness for ${circuit.name}`);
}
}
}
// return the build directory
return circomKitConfig.dirBuild;
};
if (require.main === module) {
(async () => {
// check if we want to compile the c witness or not
const cWitness = process.argv.includes("--cWitness");
// the output path is the next argument after the --outPath flag
// and is not mandatory
const outputPathIndex = process.argv.indexOf("--outPath");
if (outputPathIndex === -1) {
await compileCircuits(cWitness);
} else {
const outputFolder = process.argv[outputPathIndex + 1];
await compileCircuits(cWitness, outputFolder);
}
})();
}

View File

@@ -0,0 +1,72 @@
import { type CircomkitConfig, type CircuitConfig, Circomkit } from "circomkit";
import fs from "fs";
import path from "path";
import type { CircuitConfigWithName } from "./types";
import { cleanThreads } from "./utils";
/**
* Generate the zkeys for MACI's circuits using circomkit
* @dev This should only be used for testing purposes, or to generate the genesis zkey
* for a new trusted setup ceremony. Never use zkeys that have not undergone a ceremony
* in production.
* @param outPath - the path to the output folder
*/
export const generateZkeys = async (outputPath?: string): Promise<void> => {
// read circomkit config files
const configFilePath = path.resolve(__dirname, "..", "circomkit.json");
const circomKitConfig = JSON.parse(fs.readFileSync(configFilePath, "utf-8")) as CircomkitConfig;
const circuitsConfigPath = path.resolve(__dirname, "..", "circom", "circuits.json");
const circuitsConfigContent = JSON.parse(fs.readFileSync(circuitsConfigPath, "utf-8")) as unknown as Record<
string,
CircuitConfig
>;
const circuitsConfigs: CircuitConfigWithName[] = Object.entries(circuitsConfigContent).map(([name, config]) => ({
name,
...config,
}));
const outPath = outputPath ? path.resolve(outputPath) : undefined;
// update the output directory
if (outPath) {
circomKitConfig.dirBuild = outPath;
circomKitConfig.dirPtau = outPath;
}
const circomkitInstance = new Circomkit({
...circomKitConfig,
verbose: false,
});
// loop through each circuit config and compile them
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < circuitsConfigs.length; i += 1) {
const circuit = circuitsConfigs[i];
// eslint-disable-next-line no-console
console.log(`Generating zKey for ${circuit.name}...`);
// eslint-disable-next-line no-await-in-loop
const { proverKeyPath } = await circomkitInstance.setup(circuit.name);
// rename the zkey
const zkeyPath = path.resolve(circomKitConfig.dirBuild, circuit.name, `${circuit.name}.0.zkey`);
fs.renameSync(proverKeyPath, zkeyPath);
}
// clean up the threads so we can exit
await cleanThreads();
};
if (require.main === module) {
(async () => {
const outputPathIndex = process.argv.indexOf("--outPath");
if (outputPathIndex === -1) {
await generateZkeys();
} else {
const outputFolder = process.argv[process.argv.indexOf("--outPath") + 1];
await generateZkeys(outputFolder);
}
})();
}

View File

@@ -0,0 +1,3 @@
export { genProof, verifyProof, extractVk } from "./proofs";
export { cleanThreads } from "./utils";
export type { ISnarkJSVerificationKey } from "./types";

View File

@@ -0,0 +1,136 @@
import { stringifyBigInts } from "../crypto";
import {
zKey,
groth16,
type FullProveResult,
type PublicSignals,
type Groth16Proof,
type ISnarkJSVerificationKey,
} from "snarkjs";
import { execFileSync } from "child_process";
import fs from "fs";
import { tmpdir } from "os";
import path from "path";
import type { IGenProofOptions } from "./types";
import { cleanThreads, isArm } from "./utils";
/**
* Generate a zk-SNARK proof
* @dev if running on a intel chip we use rapidsnark for
* speed - on the other hand if running on ARM we need to use
* snark and a WASM witness
* @param inputs - the inputs to the circuit
* @param zkeyPath - the path to the zkey
* @param useWasm - whether we want to use the wasm witness or not
* @param rapidsnarkExePath - the path to the rapidnsark binary
* @param witnessExePath - the path to the compiled witness binary
* @param wasmPath - the path to the wasm witness
* @param silent - whether we want to print to the console or not
* @returns the zk-SNARK proof and public signals
*/
export const genProof = async ({
inputs,
zkeyPath,
useWasm,
rapidsnarkExePath,
witnessExePath,
wasmPath,
silent = false,
}: IGenProofOptions): Promise<FullProveResult> => {
// if we want to use a wasm witness we use snarkjs
if (useWasm) {
if (!wasmPath) {
throw new Error("wasmPath must be specified");
}
if (!fs.existsSync(wasmPath)) {
throw new Error(`wasmPath ${wasmPath} does not exist`);
}
const { proof, publicSignals } = await groth16.fullProve(inputs, wasmPath, zkeyPath);
return { proof, publicSignals };
}
if (isArm()) {
throw new Error("To use rapidnsnark you currently need to be running on an intel chip");
}
// intel chip flow (use rapidnsark)
// Create tmp directory
const tmpPath = path.resolve(tmpdir(), `tmp-${Date.now()}`);
fs.mkdirSync(tmpPath, { recursive: true });
const inputJsonPath = path.resolve(tmpPath, "input.json");
const outputWtnsPath = path.resolve(tmpPath, "output.wtns");
const proofJsonPath = path.resolve(tmpPath, "proof.json");
const publicJsonPath = path.resolve(tmpPath, "public.json");
// Write input.json
const jsonData = JSON.stringify(stringifyBigInts(inputs));
fs.writeFileSync(inputJsonPath, jsonData);
// Generate the witness
execFileSync(witnessExePath!, [inputJsonPath, outputWtnsPath], { stdio: silent ? "ignore" : "pipe" });
if (!fs.existsSync(outputWtnsPath)) {
throw new Error(`Error executing ${witnessExePath} ${inputJsonPath} ${outputWtnsPath}`);
}
// Generate the proof
execFileSync(rapidsnarkExePath!, [zkeyPath, outputWtnsPath, proofJsonPath, publicJsonPath], {
stdio: silent ? "ignore" : "pipe",
});
if (!fs.existsSync(proofJsonPath)) {
throw new Error(
`Error executing ${rapidsnarkExePath} ${zkeyPath} ${outputWtnsPath} ${proofJsonPath} ${publicJsonPath}`,
);
}
// Read the proof and public inputs
const proof = JSON.parse(fs.readFileSync(proofJsonPath).toString()) as Groth16Proof;
const publicSignals = JSON.parse(fs.readFileSync(publicJsonPath).toString()) as PublicSignals;
// remove all artifacts
[proofJsonPath, publicJsonPath, inputJsonPath, outputWtnsPath].forEach(f => {
if (fs.existsSync(f)) {
fs.unlinkSync(f);
}
});
// remove tmp directory
fs.rmdirSync(tmpPath);
return { proof, publicSignals };
};
/**
* Verify a zk-SNARK proof using snarkjs
* @param publicInputs - the public inputs to the circuit
* @param proof - the proof
* @param vk - the verification key
* @returns whether the proof is valid or not
*/
export const verifyProof = async (
publicInputs: PublicSignals,
proof: Groth16Proof,
vk: ISnarkJSVerificationKey,
): Promise<boolean> => {
const isValid = await groth16.verify(vk, publicInputs, proof);
await cleanThreads();
return isValid;
};
/**
* Extract the Verification Key from a zKey
* @param zkeyPath - the path to the zKey
* @returns the verification key
*/
export const extractVk = async (zkeyPath: string): Promise<ISnarkJSVerificationKey> => {
const vk = await zKey.exportVerificationKey(zkeyPath);
await cleanThreads();
return vk;
};

View File

@@ -0,0 +1,78 @@
import type { CircuitConfig } from "circomkit";
import type { CircuitInputs } from "../core";
import type { ISnarkJSVerificationKey } from "snarkjs";
/**
* Parameters for the genProof function
*/
export interface IGenProofOptions {
inputs: CircuitInputs;
zkeyPath: string;
useWasm?: boolean;
rapidsnarkExePath?: string;
witnessExePath?: string;
wasmPath?: string;
silent?: boolean;
}
/**
* Inputs for circuit ProcessMessages
*/
export interface IProcessMessagesInputs {
inputHash: bigint;
packedVals: bigint;
pollEndTimestamp: bigint;
msgRoot: bigint;
msgs: bigint[];
msgSubrootPathElements: bigint[][];
coordPrivKey: bigint;
coordPubKey: [bigint, bigint];
encPubKeys: bigint[];
currentStateRoot: bigint;
currentStateLeaves: bigint[];
currentStateLeavesPathElements: bigint[][];
currentSbCommitment: bigint;
currentSbSalt: bigint;
newSbCommitment: bigint;
newSbSalt: bigint;
currentBallotRoot: bigint;
currentBallots: bigint[];
currentBallotsPathElements: bigint[][];
currentVoteWeights: bigint[];
currentVoteWeightsPathElements: bigint[][];
}
/**
* Inputs for circuit TallyVotes
*/
export interface ITallyVotesInputs {
stateRoot: bigint;
ballotRoot: bigint;
sbSalt: bigint;
packedVals: bigint;
sbCommitment: bigint;
currentTallyCommitment: bigint;
newTallyCommitment: bigint;
inputHash: bigint;
ballots: bigint[];
ballotPathElements: bigint[];
votes: bigint[][];
currentResults: bigint[];
currentResultsRootSalt: bigint;
currentSpentVoiceCreditSubtotal: bigint;
currentSpentVoiceCreditSubtotalSalt: bigint;
currentPerVOSpentVoiceCredits: bigint[];
currentPerVOSpentVoiceCreditsRootSalt: bigint;
newResultsRootSalt: bigint;
newPerVOSpentVoiceCreditsRootSalt: bigint;
newSpentVoiceCreditSubtotalSalt: bigint;
}
/**
* Extend CircuitConfig type to include the name of the circuit
*/
export interface CircuitConfigWithName extends CircuitConfig {
name: string;
}
export type { ISnarkJSVerificationKey };

View File

@@ -0,0 +1,50 @@
declare module "snarkjs" {
export type NumericString = string;
export type PublicSignals = Record<string, string | bigint | bigint[] | string[] | bigint[][] | bigint[][][]>;
export type BigNumberish = number | string | bigint;
export interface ISnarkJSVerificationKey {
protocol: BigNumberish;
curve: BigNumberish;
nPublic: BigNumberish;
vk_alpha_1: BigNumberish[];
vk_beta_2: BigNumberish[][];
vk_gamma_2: BigNumberish[][];
vk_delta_2: BigNumberish[][];
vk_alphabeta_12: BigNumberish[][][];
IC: BigNumberish[][];
}
export interface FullProveResult {
proof: Groth16Proof;
publicSignals: PublicSignals;
}
export interface Groth16Proof {
pi_a: NumericString[];
pi_b: NumericString[][];
pi_c: NumericString[];
protocol: string;
curve: string;
}
export namespace zKey {
function exportVerificationKey(zkeyName: string, logger?: unknown): Promise<ISnarkJSVerificationKey>;
}
export namespace groth16 {
function verify(
vk_verifier: ISnarkJSVerificationKey,
publicSignals: PublicSignals,
proof: Groth16Proof,
logger?: unknown,
): Promise<boolean>;
function fullProve(
input: PublicSignals,
wasmFile: string,
zkeyFileName: string,
logger?: unknown,
): Promise<FullProveResult>;
}
}

View File

@@ -0,0 +1,35 @@
import os from "os";
declare global {
interface ITerminatable {
terminate: () => Promise<unknown>;
}
// eslint-disable-next-line vars-on-top, no-var, camelcase
var curve_bn128: ITerminatable | undefined;
// eslint-disable-next-line vars-on-top, no-var, camelcase
var curve_bls12381: ITerminatable | undefined;
}
/**
* Check if we are running on an arm chip
* @returns whether we are running on an arm chip
*/
export const isArm = (): boolean => os.arch().includes("arm");
/*
* https://github.com/iden3/snarkjs/issues/152
* Need to cleanup the threads to avoid stalling
*/
export const cleanThreads = async (): Promise<void> => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!globalThis) {
return;
}
const curves = ["curve_bn128", "curve_bls12381"];
await Promise.all(
curves.map(curve => globalThis[curve as "curve_bn128" | "curve_bls12381"]?.terminate()).filter(Boolean),
);
};

View File

@@ -0,0 +1,175 @@
import { type PubKey, type Keypair, StateLeaf, blankStateLeaf } from "../domainobjs";
import type { IJsonMaciState, IJsonPoll, IMaciState, MaxValues, TreeDepths } from "./utils/types";
import { Poll } from "./Poll";
import { STATE_TREE_ARITY } from "./utils/constants";
/**
* A representation of the MACI contract.
*/
export class MaciState implements IMaciState {
// a MaciState can hold multiple polls
polls: Map<bigint, Poll> = new Map<bigint, Poll>();
// the leaves of the state tree
stateLeaves: StateLeaf[] = [];
// how deep the state tree is
stateTreeDepth: number;
numSignUps = 0;
// to keep track if a poll is currently being processed
pollBeingProcessed?: boolean;
currentPollBeingProcessed?: bigint;
/**
* Constructs a new MaciState object.
* @param stateTreeDepth - The depth of the state tree.
*/
constructor(stateTreeDepth: number) {
this.stateTreeDepth = stateTreeDepth;
// we put a blank state leaf to prevent a DoS attack
this.stateLeaves.push(blankStateLeaf);
// we need to increase the number of signups by one given
// that we already added the blank leaf
this.numSignUps += 1;
}
/**
* Sign up a user with the given public key, initial voice credit balance, and timestamp.
* @param pubKey - The public key of the user.
* @param initialVoiceCreditBalance - The initial voice credit balance of the user.
* @param timestamp - The timestamp of the sign-up.
* @returns The index of the newly signed-up user in the state tree.
*/
signUp(pubKey: PubKey, initialVoiceCreditBalance: bigint, timestamp: bigint): number {
this.numSignUps += 1;
const stateLeaf = new StateLeaf(pubKey, initialVoiceCreditBalance, timestamp);
return this.stateLeaves.push(stateLeaf.copy()) - 1;
}
/**
* Deploy a new poll with the given parameters.
* @param pollEndTimestamp - The Unix timestamp at which the poll ends.
* @param maxValues - The maximum number of values for each vote option.
* @param treeDepths - The depths of the tree.
* @param messageBatchSize - The batch size for processing messages.
* @param coordinatorKeypair - The keypair of the MACI round coordinator.
* @returns The index of the newly deployed poll.
*/
deployPoll(
pollEndTimestamp: bigint,
maxValues: MaxValues,
treeDepths: TreeDepths,
messageBatchSize: number,
coordinatorKeypair: Keypair,
): bigint {
const poll: Poll = new Poll(
pollEndTimestamp,
coordinatorKeypair,
treeDepths,
{
messageBatchSize,
subsidyBatchSize: STATE_TREE_ARITY ** treeDepths.intStateTreeDepth,
tallyBatchSize: STATE_TREE_ARITY ** treeDepths.intStateTreeDepth,
},
maxValues,
this,
);
this.polls.set(BigInt(this.polls.size), poll);
return BigInt(this.polls.size - 1);
}
/**
* Deploy a null poll.
*/
deployNullPoll(): void {
this.polls.set(BigInt(this.polls.size), null as unknown as Poll);
}
/**
* Create a deep copy of the MaciState object.
* @returns A new instance of the MaciState object with the same properties.
*/
copy = (): MaciState => {
const copied = new MaciState(this.stateTreeDepth);
copied.stateLeaves = this.stateLeaves.map((x: StateLeaf) => x.copy());
copied.polls = new Map(Array.from(this.polls, ([key, value]) => [key, value.copy()]));
return copied;
};
/**
* Check if the MaciState object is equal to another MaciState object.
* @param m - The MaciState object to compare.
* @returns True if the two MaciState objects are equal, false otherwise.
*/
equals = (m: MaciState): boolean => {
const result =
this.stateTreeDepth === m.stateTreeDepth &&
this.polls.size === m.polls.size &&
this.stateLeaves.length === m.stateLeaves.length;
if (!result) {
return false;
}
for (let i = 0; i < this.polls.size; i += 1) {
if (!this.polls.get(BigInt(i))?.equals(m.polls.get(BigInt(i))!)) {
return false;
}
}
for (let i = 0; i < this.stateLeaves.length; i += 1) {
if (!this.stateLeaves[i].equals(m.stateLeaves[i])) {
return false;
}
}
return true;
};
/**
* Serialize the MaciState object to a JSON object.
* @returns A JSON object representing the MaciState object.
*/
toJSON(): IJsonMaciState {
return {
stateTreeDepth: this.stateTreeDepth,
polls: Array.from(this.polls.values()).map(poll => poll.toJSON()),
stateLeaves: this.stateLeaves.map(leaf => leaf.toJSON()),
pollBeingProcessed: Boolean(this.pollBeingProcessed),
currentPollBeingProcessed: this.currentPollBeingProcessed ? this.currentPollBeingProcessed.toString() : "",
numSignUps: this.numSignUps,
};
}
/**
* Create a new MaciState object from a JSON object.
* @param json - The JSON object representing the MaciState object.
* @returns A new instance of the MaciState object with the properties from the JSON object.
*/
static fromJSON(json: IJsonMaciState): MaciState {
const maciState = new MaciState(json.stateTreeDepth);
// assign the json values to the new instance
maciState.stateLeaves = json.stateLeaves.map(leaf => StateLeaf.fromJSON(leaf));
maciState.pollBeingProcessed = json.pollBeingProcessed;
maciState.currentPollBeingProcessed = BigInt(json.currentPollBeingProcessed);
maciState.numSignUps = json.numSignUps;
// re-generate the polls and set the maci state reference
maciState.polls = new Map(
json.polls.map((jsonPoll: IJsonPoll, index) => [BigInt(index), Poll.fromJSON(jsonPoll, maciState)]),
);
return maciState;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
import benny from "benny";
import { Keypair, PCommand } from "../../domainobjs";
import { MaciState } from "..";
import {
COORDINATOR_KEYPAIR,
DURATION,
MAX_VALUES,
MESSAGE_BATCH_SIZE,
STATE_TREE_DEPTH,
TREE_DEPTHS,
VOICE_CREDIT_BALANCE,
} from "./utils/constants";
const NAME = "maci-core";
export default function runCore(): void {
benny.suite(
NAME,
benny.add(`maci-core - Generate circuit inputs for 10 signups and 50 messages`, () => {
const voteWeight = 9n;
const users: Keypair[] = [];
const maciState = new MaciState(STATE_TREE_DEPTH);
// Sign up and vote
for (let i = 0; i < MESSAGE_BATCH_SIZE - 1; i += 1) {
const userKeypair = new Keypair();
users.push(userKeypair);
maciState.signUp(userKeypair.pubKey, VOICE_CREDIT_BALANCE, BigInt(Math.floor(Date.now() / 1000)));
}
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + DURATION),
MAX_VALUES,
TREE_DEPTHS,
MESSAGE_BATCH_SIZE,
COORDINATOR_KEYPAIR,
);
const poll = maciState.polls.get(pollId)!;
poll.updatePoll(BigInt(maciState.stateLeaves.length));
// 24 valid votes
for (let i = 0; i < MESSAGE_BATCH_SIZE - 1; i += 1) {
const userKeypair = users[i];
const command = new PCommand(
BigInt(i + 1),
userKeypair.pubKey,
BigInt(i), // vote option index
voteWeight,
1n,
BigInt(pollId),
);
const signature = command.sign(userKeypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, COORDINATOR_KEYPAIR.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
}
// 24 invalid votes
for (let i = 0; i < MESSAGE_BATCH_SIZE - 1; i += 1) {
const userKeypair = users[i];
const command = new PCommand(
BigInt(i + 1),
userKeypair.pubKey,
BigInt(i), // vote option index
VOICE_CREDIT_BALANCE * 2n, // invalid vote weight
1n,
BigInt(pollId),
);
const signature = command.sign(userKeypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, COORDINATOR_KEYPAIR.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
}
// Process messages
poll.processMessages(pollId);
// Process messages
poll.processMessages(pollId);
// Test processAllMessages
poll.processAllMessages();
}),
benny.cycle(),
benny.complete(results => {
results.results.forEach(result => {
// eslint-disable-next-line no-console
console.log(`${result.name}: mean time: ${result.details.mean.toFixed(2)}`);
});
}),
benny.save({ folder: "ts/__benchmarks__/results", file: NAME, version: "1.0.0", details: true }),
benny.save({ folder: "ts/__benchmarks__/results", file: NAME, format: "chart.html", details: true }),
benny.save({ folder: "ts/__benchmarks__/results", file: NAME, format: "table.html", details: true }),
);
}
runCore();

View File

@@ -0,0 +1,19 @@
import { Keypair } from "../../../domainobjs";
export const VOICE_CREDIT_BALANCE = 100n;
export const DURATION = 30;
export const MESSAGE_BATCH_SIZE = 25;
export const COORDINATOR_KEYPAIR = new Keypair();
export const STATE_TREE_DEPTH = 10;
export const MAX_VALUES = {
maxUsers: 25,
maxMessages: 25,
maxVoteOptions: 25,
};
export const TREE_DEPTHS = {
intStateTreeDepth: 2,
messageTreeDepth: 3,
messageTreeSubDepth: 2,
voteOptionTreeDepth: 4,
};

View File

@@ -0,0 +1,190 @@
import { expect } from "chai";
import { PCommand, Message, Keypair } from "../../domainobjs";
import fs from "fs";
import { MaciState } from "../MaciState";
import { STATE_TREE_DEPTH } from "../utils/constants";
import { IJsonMaciState } from "../utils/types";
import {
coordinatorKeypair,
duration,
maxValues,
messageBatchSize,
treeDepths,
voiceCreditBalance,
} from "./utils/constants";
describe("MaciState", function test() {
this.timeout(100000);
describe("copy", () => {
let pollId: bigint;
let m1: MaciState;
const userKeypair = new Keypair();
const stateFile = "./state.json";
after(() => {
if (fs.existsSync(stateFile)) {
fs.unlinkSync(stateFile);
}
});
before(() => {
m1 = new MaciState(STATE_TREE_DEPTH);
m1.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000)));
pollId = m1.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const command = new PCommand(0n, userKeypair.pubKey, 0n, 0n, 0n, BigInt(pollId), 0n);
const encKeypair = new Keypair();
const signature = command.sign(encKeypair.privKey);
const sharedKey = Keypair.genEcdhSharedKey(encKeypair.privKey, coordinatorKeypair.pubKey);
const message: Message = command.encrypt(signature, sharedKey);
m1.polls.get(pollId)!.publishMessage(message, encKeypair.pubKey);
m1.polls.get(pollId)!.publishMessage(message, encKeypair.pubKey);
});
it("should correctly deep-copy a MaciState object", () => {
const m2 = m1.copy();
// modify stateTreeDepth
m2.stateTreeDepth += 1;
expect(m1.equals(m2)).not.to.eq(true);
// modify user.pubKey
const m3 = m1.copy();
m3.stateLeaves[0].pubKey = new Keypair().pubKey;
expect(m1.equals(m3)).not.to.eq(true);
// modify user.voiceCreditBalance
const m4 = m1.copy();
m4.stateLeaves[0].voiceCreditBalance = BigInt(m4.stateLeaves[0].voiceCreditBalance) + 1n;
expect(m1.equals(m4)).not.to.eq(true);
// modify poll.coordinatorKeypair
const m6 = m1.copy();
m6.polls.get(pollId)!.coordinatorKeypair = new Keypair();
expect(m1.equals(m6)).not.to.eq(true);
// modify poll.treeDepths.intStateTreeDepth
const m9 = m1.copy();
m9.polls.get(pollId)!.treeDepths.intStateTreeDepth += 1;
expect(m1.equals(m9)).not.to.eq(true);
// modify poll.treeDepths.messageTreeDepth
const m10 = m1.copy();
m10.polls.get(pollId)!.treeDepths.messageTreeDepth += 1;
expect(m1.equals(m10)).not.to.eq(true);
// modify poll.treeDepths.messageTreeSubDepth
const m11 = m1.copy();
m11.polls.get(pollId)!.treeDepths.messageTreeSubDepth += 1;
expect(m1.equals(m11)).not.to.eq(true);
// modify poll.treeDepths.voteOptionTreeDepth
const m12 = m1.copy();
m12.polls.get(pollId)!.treeDepths.voteOptionTreeDepth += 1;
expect(m1.equals(m12)).not.to.eq(true);
// modify poll.batchSizes.tallyBatchSize
const m13 = m1.copy();
m13.polls.get(pollId)!.batchSizes.tallyBatchSize += 1;
expect(m1.equals(m13)).not.to.eq(true);
// modify poll.batchSizes.messageBatchSize
const m14 = m1.copy();
m14.polls.get(pollId)!.batchSizes.messageBatchSize += 1;
expect(m1.equals(m14)).not.to.eq(true);
// modify poll.maxValues.maxMessages
const m16 = m1.copy();
m16.polls.get(pollId)!.maxValues.maxMessages += 1;
expect(m1.equals(m16)).not.to.eq(true);
// modify poll.maxValues.maxVoteOptions
const m17 = m1.copy();
m17.polls.get(pollId)!.maxValues.maxVoteOptions += 1;
expect(m1.equals(m17)).not.to.eq(true);
// modify poll.messages
const m20 = m1.copy();
m20.polls.get(pollId)!.messages[0].data[0] = BigInt(m20.polls.get(pollId)!.messages[0].data[0]) + 1n;
expect(m1.equals(m20)).not.to.eq(true);
// modify poll.encPubKeys
const m21 = m1.copy();
m21.polls.get(pollId)!.encPubKeys[0] = new Keypair().pubKey;
expect(m1.equals(m21)).not.to.eq(true);
});
it("should create a JSON object from a MaciState object", () => {
// test loading a topup message
m1.polls.get(pollId)!.topupMessage(new Message(2n, [0n, 5n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]));
// mock a message with invalid message type
m1.polls.get(pollId)!.messages[1].msgType = 3n;
const json = m1.toJSON();
fs.writeFileSync(stateFile, JSON.stringify(json, null, 4));
const content = JSON.parse(fs.readFileSync(stateFile).toString()) as IJsonMaciState;
const state = MaciState.fromJSON(content);
state.polls.forEach(poll => {
poll.setCoordinatorKeypair(coordinatorKeypair.privKey.serialize());
expect(poll.coordinatorKeypair.equals(coordinatorKeypair)).to.eq(true);
});
expect(state.equals(m1)).to.eq(true);
});
});
describe("deployNullPoll ", () => {
it("should deploy a Poll that is null", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
maciState.deployNullPoll();
expect(maciState.polls.get(0n)).to.eq(null);
});
});
describe("topup", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const poll = maciState.polls.get(pollId)!;
it("should allow to publish a topup message", () => {
const user1Keypair = new Keypair();
// signup the user
const user1StateIndex = maciState.signUp(
user1Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
const message = new Message(2n, [BigInt(user1StateIndex), 50n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]);
poll.topupMessage(message);
expect(poll.messages.length).to.eq(1);
});
it("should throw if the message has an invalid message type", () => {
const message = new Message(1n, [1n, 50n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]);
expect(() => {
poll.topupMessage(message);
}).to.throw("A Topup message must have msgType 2");
});
});
});

View File

@@ -0,0 +1,667 @@
import { expect } from "chai";
import { PCommand, Message, Keypair, StateLeaf, PrivKey, Ballot } from "../../domainobjs";
import { MaciState } from "../MaciState";
import { Poll } from "../Poll";
import { STATE_TREE_DEPTH } from "../utils/constants";
import {
coordinatorKeypair,
duration,
maxValues,
messageBatchSize,
treeDepths,
voiceCreditBalance,
} from "./utils/constants";
describe("Poll", function test() {
this.timeout(90000);
describe("processMessage", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const poll = maciState.polls.get(pollId)!;
const user1Keypair = new Keypair();
// signup the user
const user1StateIndex = maciState.signUp(
user1Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
// copy the state from the MaciState ref
poll.updatePoll(BigInt(maciState.stateLeaves.length));
it("should throw if a message has an invalid state index", () => {
const command = new PCommand(
// invalid state index as it is one more than the number of state leaves
BigInt(user1StateIndex + 1),
user1Keypair.pubKey,
0n,
1n,
0n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
expect(() => {
poll.processMessage(message, ecdhKeypair.pubKey);
}).to.throw("invalid state leaf index");
});
it("should throw if a message has an invalid nonce", () => {
const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, 0n, 0n, 0n, BigInt(pollId));
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
expect(() => {
poll.processMessage(message, ecdhKeypair.pubKey);
}).to.throw("invalid nonce");
});
it("should throw if a message has an invalid signature", () => {
const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, 0n, 0n, 0n, BigInt(pollId));
const signature = command.sign(new PrivKey(0n));
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
expect(() => {
poll.processMessage(message, ecdhKeypair.pubKey);
}).to.throw("invalid signature");
});
it("should throw if a message consumes more than the available voice credits for a user", () => {
const command = new PCommand(
BigInt(user1StateIndex),
user1Keypair.pubKey,
0n,
// voice credits spent would be this value ** this value
BigInt(Math.sqrt(Number.parseInt(voiceCreditBalance.toString(), 10)) + 1),
1n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
expect(() => {
poll.processMessage(message, ecdhKeypair.pubKey);
}).to.throw("insufficient voice credits");
});
it("should throw if a message has an invalid vote option index (>= max vote options)", () => {
const command = new PCommand(
BigInt(user1StateIndex),
user1Keypair.pubKey,
BigInt(maxValues.maxVoteOptions),
// voice credits spent would be this value ** this value
1n,
1n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
expect(() => {
poll.processMessage(message, ecdhKeypair.pubKey);
}).to.throw("invalid vote option index");
});
it("should throw if a message has an invalid vote option index (< 0)", () => {
const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, -1n, 1n, 1n, BigInt(pollId));
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
expect(() => {
poll.processMessage(message, ecdhKeypair.pubKey);
}).to.throw("invalid vote option index");
});
it("should throw when passed a message that cannot be decrypted (wrong encPubKey)", () => {
const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, 0n, 1n, 1n, BigInt(pollId));
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(new Keypair().privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
expect(() => {
poll.processMessage(message, user1Keypair.pubKey);
}).to.throw("failed decryption due to either wrong encryption public key or corrupted ciphertext");
});
it("should throw when passed a corrupted message", () => {
const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, 0n, 1n, 1n, BigInt(pollId));
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(user1Keypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
message.data[0] = 0n;
expect(() => {
poll.processMessage(message, user1Keypair.pubKey);
}).to.throw("failed decryption due to either wrong encryption public key or corrupted ciphertext");
});
it("should throw when going over the voice credit limit (non qv)", () => {
const command = new PCommand(
// invalid state index as it is one more than the number of state leaves
BigInt(user1StateIndex),
user1Keypair.pubKey,
0n,
voiceCreditBalance + 1n,
1n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
expect(() => {
poll.processMessage(message, ecdhKeypair.pubKey, false);
}).to.throw("insufficient voice credits");
});
it("should work when submitting a valid message (voteWeight === voiceCreditBalance and non qv)", () => {
const command = new PCommand(
// invalid state index as it is one more than the number of state leaves
BigInt(user1StateIndex),
user1Keypair.pubKey,
0n,
voiceCreditBalance,
1n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
poll.processMessage(message, ecdhKeypair.pubKey, false);
});
});
describe("processMessages", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const poll = maciState.polls.get(pollId)!;
poll.updatePoll(BigInt(maciState.stateLeaves.length));
const user1Keypair = new Keypair();
// signup the user
const user1StateIndex = maciState.signUp(
user1Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
it("should throw if this is the first batch and currentMessageBatchIndex is defined", () => {
const command = new PCommand(BigInt(user1StateIndex), user1Keypair.pubKey, 0n, 1n, 0n, BigInt(pollId));
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
// mock
poll.currentMessageBatchIndex = 0;
expect(() => poll.processMessages(pollId)).to.throw(
"The current message batch index should not be defined if this is the first batch",
);
poll.currentMessageBatchIndex = undefined;
});
it("should throw if the state has not been copied prior to calling processMessages", () => {
const tmpPoll = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
expect(() => maciState.polls.get(tmpPoll)?.processMessages(pollId)).to.throw(
"You must update the poll with the correct data first",
);
});
it("should succeed even if we send an invalid message", () => {
const command = new PCommand(
// we only signed up one user so the state index is invalid
BigInt(user1StateIndex + 1),
user1Keypair.pubKey,
0n,
1n,
0n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
poll.updatePoll(BigInt(maciState.stateLeaves.length));
expect(() => {
poll.processMessage(message, ecdhKeypair.pubKey);
}).to.throw("invalid state leaf index");
// keep this call to complete processing
// eslint-disable-next-line no-unused-expressions
expect(() => poll.processMessages(pollId)).to.not.throw;
});
it("should correctly process a topup message and increase an user's voice credit balance", () => {
const topupMessage = new Message(2n, [BigInt(user1StateIndex), 50n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]);
poll.topupMessage(topupMessage);
const balanceBefore = poll.stateLeaves[user1StateIndex].voiceCreditBalance;
poll.processMessages(pollId);
// check balance
expect(poll.stateLeaves[user1StateIndex].voiceCreditBalance.toString()).to.eq((balanceBefore + 50n).toString());
});
it("should throw when called after all messages have been processed", () => {
expect(() => poll.processMessages(pollId)).to.throw("No more messages to process");
});
});
describe("processAllMessages", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const poll = maciState.polls.get(pollId)!;
const user1Keypair = new Keypair();
// signup the user
const user1StateIndex = maciState.signUp(
user1Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
poll.updatePoll(BigInt(maciState.stateLeaves.length));
it("it should succeed even if send an invalid message", () => {
const command = new PCommand(
// we only signed up one user so the state index is invalid
BigInt(user1StateIndex + 1),
user1Keypair.pubKey,
0n,
1n,
0n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
expect(() => {
poll.processMessage(message, ecdhKeypair.pubKey);
}).to.throw("invalid state leaf index");
expect(() => poll.processAllMessages()).to.not.throw();
});
it("should return the correct state leaves and ballots", () => {
const command = new PCommand(BigInt(user1StateIndex + 1), user1Keypair.pubKey, 0n, 1n, 0n, BigInt(pollId));
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
expect(() => {
poll.processMessage(message, ecdhKeypair.pubKey);
}).to.throw("invalid state leaf index");
const { stateLeaves, ballots } = poll.processAllMessages();
stateLeaves.forEach((leaf: StateLeaf, index: number) => expect(leaf.equals(poll.stateLeaves[index])).to.eq(true));
ballots.forEach((ballot: Ballot, index: number) => expect(ballot.equals(poll.ballots[index])).to.eq(true));
});
it("should have processed all messages", () => {
const command = new PCommand(BigInt(user1StateIndex + 1), user1Keypair.pubKey, 0n, 1n, 0n, BigInt(pollId));
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
// publish batch size + 1
for (let i = 0; i <= messageBatchSize; i += 1) {
poll.publishMessage(message, ecdhKeypair.pubKey);
}
poll.processAllMessages();
expect(poll.hasUnprocessedMessages()).to.eq(false);
});
});
describe("tallyVotes", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const poll = maciState.polls.get(pollId)!;
const user1Keypair = new Keypair();
const user2Keypair = new Keypair();
// signup the user
const user1StateIndex = maciState.signUp(
user1Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
const user2StateIndex = maciState.signUp(
user2Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
poll.updatePoll(BigInt(maciState.stateLeaves.length));
const voteWeight = 5n;
const voteOption = 0n;
const command = new PCommand(
BigInt(user1StateIndex),
user1Keypair.pubKey,
voteOption,
voteWeight,
1n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
it("should throw if called before all messages have been processed", () => {
expect(() => poll.tallyVotes()).to.throw("You must process the messages first");
});
it("should generate the correct results", () => {
poll.processAllMessages();
poll.tallyVotes();
const spentVoiceCredits = poll.totalSpentVoiceCredits;
const results = poll.tallyResult;
expect(spentVoiceCredits).to.eq(voteWeight * voteWeight);
expect(results[Number.parseInt(voteOption.toString(), 10)]).to.eq(voteWeight);
expect(poll.perVOSpentVoiceCredits[Number.parseInt(voteOption.toString(), 10)]).to.eq(voteWeight * voteWeight);
});
it("should generate the correct results (non-qv)", () => {
// deploy a second poll
const secondPollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const secondPoll = maciState.polls.get(secondPollId)!;
secondPoll.updatePoll(BigInt(maciState.stateLeaves.length));
const secondVoteWeight = 10n;
const secondVoteOption = 1n;
const secondCommand = new PCommand(
BigInt(user2StateIndex),
user2Keypair.pubKey,
secondVoteOption,
secondVoteWeight,
1n,
secondPollId,
);
const secondSignature = secondCommand.sign(user2Keypair.privKey);
const secondEcdhKeypair = new Keypair();
const secondSharedKey = Keypair.genEcdhSharedKey(secondEcdhKeypair.privKey, coordinatorKeypair.pubKey);
const secondMessage = secondCommand.encrypt(secondSignature, secondSharedKey);
secondPoll.publishMessage(secondMessage, secondEcdhKeypair.pubKey);
secondPoll.processAllMessages();
secondPoll.tallyVotesNonQv();
const spentVoiceCredits = secondPoll.totalSpentVoiceCredits;
const results = secondPoll.tallyResult;
// spent voice credit is not vote weight * vote weight
expect(spentVoiceCredits).to.eq(secondVoteWeight);
expect(results[Number.parseInt(secondVoteOption.toString(), 10)]).to.eq(secondVoteWeight);
});
it("should throw when there are no more ballots to tally", () => {
expect(() => poll.tallyVotes()).to.throw("No more ballots to tally");
});
});
describe("subsidy", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const poll = maciState.polls.get(pollId)!;
const user1Keypair = new Keypair();
// signup the user
const user1StateIndex = maciState.signUp(
user1Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
const voteWeight = 5n;
const voteOption = 0n;
const command = new PCommand(
BigInt(user1StateIndex),
user1Keypair.pubKey,
voteOption,
voteWeight,
1n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
before(() => {
poll.updatePoll(BigInt(maciState.stateLeaves.length));
poll.publishMessage(message, ecdhKeypair.pubKey);
poll.processAllMessages();
poll.tallyVotes();
});
it("should calculate the subsidy", () => {
const { rbi, cbi } = poll;
expect(() => poll.subsidyPerBatch()).to.not.throw();
const { rbi: newRbi, cbi: newCbi } = poll;
expect(newRbi).to.eq(rbi + 1);
expect(newCbi).to.eq(cbi + 1);
});
it("should throw when the subsidy was already calculated", () => {
expect(() => poll.subsidyPerBatch()).to.throw("No more subsidy batches to calculate");
});
});
describe("setCoordinatorKeypair", () => {
it("should update the coordinator's Keypair", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const poll = maciState.polls.get(pollId)!;
const newCoordinatorKeypair = new Keypair();
poll.setCoordinatorKeypair(newCoordinatorKeypair.privKey.serialize());
expect(poll.coordinatorKeypair.privKey.serialize()).to.deep.eq(newCoordinatorKeypair.privKey.serialize());
expect(poll.coordinatorKeypair.pubKey.serialize()).to.deep.eq(newCoordinatorKeypair.pubKey.serialize());
});
});
describe("setNumSignups", () => {
it("should update the number of signups", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
maciState.signUp(new Keypair().pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000)));
const poll = maciState.polls.get(pollId)!;
poll.updatePoll(BigInt(maciState.stateLeaves.length));
expect(poll.getNumSignups()).to.eq(2n);
// update it again
poll.setNumSignups(3n);
expect(poll.getNumSignups()).to.eq(3n);
});
});
describe("toJSON", () => {
it("should return the correct JSON", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
const pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
const poll = maciState.polls.get(pollId)!;
const json = poll.toJSON();
const pollFromJson = Poll.fromJSON(json, maciState);
pollFromJson.setCoordinatorKeypair(coordinatorKeypair.privKey.serialize());
expect(pollFromJson.equals(poll)).to.eq(true);
});
});
});

View File

@@ -0,0 +1,894 @@
import { expect } from "chai";
import { hash5, NOTHING_UP_MY_SLEEVE, IncrementalQuinTree, AccQueue } from "../../crypto";
import { PCommand, Keypair, StateLeaf, blankStateLeafHash } from "../../domainobjs";
import { MaciState } from "../MaciState";
import { Poll } from "../Poll";
import { STATE_TREE_DEPTH, STATE_TREE_ARITY } from "../utils/constants";
import { packProcessMessageSmallVals, unpackProcessMessageSmallVals } from "../utils/utils";
import {
coordinatorKeypair,
duration,
maxValues,
messageBatchSize,
treeDepths,
voiceCreditBalance,
} from "./utils/constants";
import { TestHarness, calculateTotal } from "./utils/utils";
describe("MaciState/Poll e2e", function test() {
this.timeout(300000);
describe("key changes", () => {
const user1Keypair = new Keypair();
const user2Keypair = new Keypair();
const user1SecondKeypair = new Keypair();
const user2SecondKeypair = new Keypair();
let pollId: bigint;
let user1StateIndex: number;
let user2StateIndex: number;
const user1VoteOptionIndex = 0n;
const user2VoteOptionIndex = 1n;
const user1VoteWeight = 9n;
const user2VoteWeight = 3n;
const user1NewVoteWeight = 5n;
const user2NewVoteWeight = 7n;
describe("only user 1 changes key", () => {
const maciState: MaciState = new MaciState(STATE_TREE_DEPTH);
before(() => {
// Sign up
user1StateIndex = maciState.signUp(
user1Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
user2StateIndex = maciState.signUp(
user2Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
// deploy a poll
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
maciState.polls.get(pollId)?.updatePoll(BigInt(maciState.stateLeaves.length));
});
it("should submit a vote for each user", () => {
const poll = maciState.polls.get(pollId)!;
const command1 = new PCommand(
BigInt(user1StateIndex),
user1Keypair.pubKey,
user1VoteOptionIndex,
user1VoteWeight,
1n,
BigInt(pollId),
);
const signature1 = command1.sign(user1Keypair.privKey);
const ecdhKeypair1 = new Keypair();
const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey);
const message1 = command1.encrypt(signature1, sharedKey1);
poll.publishMessage(message1, ecdhKeypair1.pubKey);
const command2 = new PCommand(
BigInt(user2StateIndex),
user2Keypair.pubKey,
user2VoteOptionIndex,
user2VoteWeight,
1n,
BigInt(pollId),
);
const signature2 = command2.sign(user2Keypair.privKey);
const ecdhKeypair2 = new Keypair();
const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey);
const message2 = command2.encrypt(signature2, sharedKey2);
poll.publishMessage(message2, ecdhKeypair2.pubKey);
});
it("user1 sends a keychange message with a new vote", () => {
const poll = maciState.polls.get(pollId)!;
const command = new PCommand(
BigInt(user1StateIndex),
user1SecondKeypair.pubKey,
user1VoteOptionIndex,
user1NewVoteWeight,
1n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
});
it("should perform the processing and tallying correctly", () => {
const poll = maciState.polls.get(pollId)!;
poll.processMessages(pollId);
poll.tallyVotes();
expect(poll.perVOSpentVoiceCredits[0].toString()).to.eq((user1NewVoteWeight * user1NewVoteWeight).toString());
expect(poll.perVOSpentVoiceCredits[1].toString()).to.eq((user2VoteWeight * user2VoteWeight).toString());
});
it("should confirm that the user key pair was changed (user's 2 one has not)", () => {
const poll = maciState.polls.get(pollId)!;
const stateLeaf1 = poll.stateLeaves[user1StateIndex];
const stateLeaf2 = poll.stateLeaves[user2StateIndex];
expect(stateLeaf1.pubKey.equals(user1SecondKeypair.pubKey)).to.eq(true);
expect(stateLeaf2.pubKey.equals(user2Keypair.pubKey)).to.eq(true);
});
});
describe("both users change key", () => {
const maciState: MaciState = new MaciState(STATE_TREE_DEPTH);
let poll: Poll;
before(() => {
// Sign up
user1StateIndex = maciState.signUp(
user1Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
user2StateIndex = maciState.signUp(
user2Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
// deploy a poll
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
poll = maciState.polls.get(pollId)!;
poll.updatePoll(BigInt(maciState.stateLeaves.length));
});
it("should submit a vote for each user", () => {
const command1 = new PCommand(
BigInt(user1StateIndex),
user1Keypair.pubKey,
user1VoteOptionIndex,
user1VoteWeight,
1n,
BigInt(pollId),
);
const signature1 = command1.sign(user1Keypair.privKey);
const ecdhKeypair1 = new Keypair();
const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey);
const message1 = command1.encrypt(signature1, sharedKey1);
poll.publishMessage(message1, ecdhKeypair1.pubKey);
const command2 = new PCommand(
BigInt(user2StateIndex),
user2Keypair.pubKey,
user2VoteOptionIndex,
user2VoteWeight,
1n,
BigInt(pollId),
);
const signature2 = command2.sign(user2Keypair.privKey);
const ecdhKeypair2 = new Keypair();
const sharedKey2 = Keypair.genEcdhSharedKey(ecdhKeypair2.privKey, coordinatorKeypair.pubKey);
const message2 = command2.encrypt(signature2, sharedKey2);
poll.publishMessage(message2, ecdhKeypair2.pubKey);
});
it("user1 sends a keychange message with a new vote", () => {
const command = new PCommand(
BigInt(user1StateIndex),
user1SecondKeypair.pubKey,
user1VoteOptionIndex,
user1NewVoteWeight,
1n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
});
it("user2 sends a keychange message with a new vote", () => {
const command = new PCommand(
BigInt(user2StateIndex),
user2SecondKeypair.pubKey,
user2VoteOptionIndex,
user2NewVoteWeight,
1n,
BigInt(pollId),
);
const signature = command.sign(user2Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
});
it("should perform the processing and tallying correctly", () => {
poll.processMessages(pollId);
poll.tallyVotes();
expect(poll.perVOSpentVoiceCredits[0].toString()).to.eq((user1NewVoteWeight * user1NewVoteWeight).toString());
expect(poll.perVOSpentVoiceCredits[1].toString()).to.eq((user2NewVoteWeight * user2NewVoteWeight).toString());
});
it("should confirm that the users key pairs were changed", () => {
const stateLeaf1 = poll.stateLeaves[user1StateIndex];
const stateLeaf2 = poll.stateLeaves[user2StateIndex];
expect(stateLeaf1.pubKey.equals(user1SecondKeypair.pubKey)).to.eq(true);
expect(stateLeaf2.pubKey.equals(user2SecondKeypair.pubKey)).to.eq(true);
});
});
describe("user1 changes key, but messages are in different batches", () => {
const maciState = new MaciState(STATE_TREE_DEPTH);
let poll: Poll;
before(() => {
// Sign up
user1StateIndex = maciState.signUp(
user1Keypair.pubKey,
voiceCreditBalance,
BigInt(Math.floor(Date.now() / 1000)),
);
// deploy a poll
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
poll = maciState.polls.get(pollId)!;
poll.updatePoll(BigInt(maciState.stateLeaves.length));
});
it("should submit a vote for one user in one batch", () => {
const command1 = new PCommand(
BigInt(user1StateIndex),
user1Keypair.pubKey,
user1VoteOptionIndex,
user1VoteWeight,
1n,
BigInt(pollId),
);
const signature1 = command1.sign(user1Keypair.privKey);
const ecdhKeypair1 = new Keypair();
const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey);
const message1 = command1.encrypt(signature1, sharedKey1);
poll.publishMessage(message1, ecdhKeypair1.pubKey);
});
it("should fill the batch with random messages", () => {
for (let i = 0; i < messageBatchSize - 1; i += 1) {
const command = new PCommand(
1n,
user1Keypair.pubKey,
user1VoteOptionIndex,
user1VoteWeight,
2n,
BigInt(pollId),
);
const signature = command.sign(user1Keypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
}
});
it("should submit a new message in a new batch", () => {
const command1 = new PCommand(
BigInt(user1StateIndex),
user1SecondKeypair.pubKey,
user1VoteOptionIndex,
user1NewVoteWeight,
1n,
BigInt(pollId),
);
const signature1 = command1.sign(user1Keypair.privKey);
const ecdhKeypair1 = new Keypair();
const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey);
const message1 = command1.encrypt(signature1, sharedKey1);
poll.publishMessage(message1, ecdhKeypair1.pubKey);
});
it("should perform the processing and tallying correctly", () => {
poll.processAllMessages();
poll.tallyVotes();
expect(poll.perVOSpentVoiceCredits[0].toString()).to.eq((user1NewVoteWeight * user1NewVoteWeight).toString());
});
it("should confirm that the user key pair was changed", () => {
const stateLeaf1 = poll.stateLeaves[user1StateIndex];
expect(stateLeaf1.pubKey.equals(user1SecondKeypair.pubKey)).to.eq(true);
});
});
});
describe("Process and tally 1 message from 1 user", () => {
let maciState: MaciState;
let pollId: bigint;
let poll: Poll;
let msgTree: IncrementalQuinTree;
let stateTree: IncrementalQuinTree;
const voteWeight = 9n;
const voteOptionIndex = 0n;
let stateIndex: number;
const userKeypair = new Keypair();
before(() => {
maciState = new MaciState(STATE_TREE_DEPTH);
msgTree = new IncrementalQuinTree(treeDepths.messageTreeDepth, NOTHING_UP_MY_SLEEVE, 5, hash5);
stateTree = new IncrementalQuinTree(STATE_TREE_DEPTH, blankStateLeafHash, STATE_TREE_ARITY, hash5);
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
poll = maciState.polls.get(pollId)!;
});
// The end result should be that option 0 gets 3 votes
// because the user spends 9 voice credits on it
it("the state root should be correct", () => {
const timestamp = BigInt(Math.floor(Date.now() / 1000));
const stateLeaf = new StateLeaf(userKeypair.pubKey, voiceCreditBalance, timestamp);
stateIndex = maciState.signUp(userKeypair.pubKey, voiceCreditBalance, timestamp);
stateTree.insert(blankStateLeafHash);
stateTree.insert(stateLeaf.hash());
poll.updatePoll(BigInt(maciState.stateLeaves.length));
expect(stateIndex.toString()).to.eq("1");
expect(stateTree.root.toString()).to.eq(poll.stateTree?.root.toString());
});
it("the message root should be correct", () => {
const command = new PCommand(
BigInt(stateIndex),
userKeypair.pubKey,
voteOptionIndex,
voteWeight,
1n,
BigInt(pollId),
);
const signature = command.sign(userKeypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
msgTree.insert(message.hash(ecdhKeypair.pubKey));
// Use the accumulator queue to compare the root of the message tree
const accumulatorQueue: AccQueue = new AccQueue(
treeDepths.messageTreeSubDepth,
STATE_TREE_ARITY,
NOTHING_UP_MY_SLEEVE,
);
accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey));
accumulatorQueue.mergeSubRoots(0);
accumulatorQueue.merge(treeDepths.messageTreeDepth);
expect(accumulatorQueue.getRoot(treeDepths.messageTreeDepth)?.toString()).to.eq(msgTree.root.toString());
});
it("packProcessMessageSmallVals and unpackProcessMessageSmallVals", () => {
const maxVoteOptions = 1n;
const numUsers = 2n;
const batchStartIndex = 5;
const batchEndIndex = 10;
const packedVals = packProcessMessageSmallVals(maxVoteOptions, numUsers, batchStartIndex, batchEndIndex);
const unpacked = unpackProcessMessageSmallVals(packedVals);
expect(unpacked.maxVoteOptions.toString()).to.eq(maxVoteOptions.toString());
expect(unpacked.numUsers.toString()).to.eq(numUsers.toString());
expect(unpacked.batchStartIndex.toString()).to.eq(batchStartIndex.toString());
expect(unpacked.batchEndIndex.toString()).to.eq(batchEndIndex.toString());
});
it("Process a batch of messages (though only 1 message is in the batch)", () => {
poll.processMessages(pollId);
// Check the ballot
expect(poll.ballots[1].votes[Number(voteOptionIndex)].toString()).to.eq(voteWeight.toString());
// Check the state leaf in the poll
expect(poll.stateLeaves[1].voiceCreditBalance.toString()).to.eq(
(voiceCreditBalance - voteWeight * voteWeight).toString(),
);
});
it("Tally ballots", () => {
const initialTotal = calculateTotal(poll.tallyResult);
expect(initialTotal.toString()).to.eq("0");
expect(poll.hasUntalliedBallots()).to.eq(true);
poll.tallyVotes();
const finalTotal = calculateTotal(poll.tallyResult);
expect(finalTotal.toString()).to.eq(voteWeight.toString());
});
});
describe(`Process and tally ${messageBatchSize * 2} messages from ${messageBatchSize} users`, () => {
let maciState: MaciState;
let pollId: bigint;
let poll: Poll;
const voteWeight = 9n;
const users: Keypair[] = [];
before(() => {
maciState = new MaciState(STATE_TREE_DEPTH);
// Sign up and vote
for (let i = 0; i < messageBatchSize - 1; i += 1) {
const userKeypair = new Keypair();
users.push(userKeypair);
maciState.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000)));
}
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
poll = maciState.polls.get(pollId)!;
poll.updatePoll(BigInt(maciState.stateLeaves.length));
});
it("should process votes correctly", () => {
// 24 valid votes
for (let i = 0; i < messageBatchSize - 1; i += 1) {
const userKeypair = users[i];
const command = new PCommand(
BigInt(i + 1),
userKeypair.pubKey,
BigInt(i), // vote option index
voteWeight,
1n,
BigInt(pollId),
);
const signature = command.sign(userKeypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
}
expect(poll.messages.length).to.eq(messageBatchSize - 1);
// 24 invalid votes
for (let i = 0; i < messageBatchSize - 1; i += 1) {
const userKeypair = users[i];
const command = new PCommand(
BigInt(i + 1),
userKeypair.pubKey,
BigInt(i), // vote option index
voiceCreditBalance * 2n, // invalid vote weight
1n,
BigInt(pollId),
);
const signature = command.sign(userKeypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
}
// 48 messages in total
expect(poll.messages.length).to.eq(2 * (messageBatchSize - 1));
expect(poll.currentMessageBatchIndex).to.eq(undefined);
expect(poll.numBatchesProcessed).to.eq(0);
// Process messages
poll.processMessages(pollId);
// currentMessageBatchIndex is 0 because the current batch starts
// with index 0.
expect(poll.currentMessageBatchIndex).to.eq(0);
expect(poll.numBatchesProcessed).to.eq(1);
// Process messages
poll.processMessages(pollId);
expect(poll.currentMessageBatchIndex).to.eq(0);
expect(poll.numBatchesProcessed).to.eq(2);
for (let i = 1; i < messageBatchSize; i += 1) {
const leaf = poll.ballots[i].votes[i - 1];
expect(leaf.toString()).to.eq(voteWeight.toString());
}
// Test processAllMessages
const r = poll.processAllMessages();
expect(r.stateLeaves.length).to.eq(poll.stateLeaves.length);
expect(r.ballots.length).to.eq(poll.ballots.length);
expect(r.ballots.length).to.eq(r.stateLeaves.length);
for (let i = 0; i < r.stateLeaves.length; i += 1) {
expect(r.stateLeaves[i].equals(poll.stateLeaves[i])).to.eq(true);
expect(r.ballots[i].equals(poll.ballots[i])).to.eq(true);
}
});
it("should tally ballots correctly", () => {
// Start with tallyResult = [0...0]
const total = calculateTotal(poll.tallyResult);
expect(total.toString()).to.eq("0");
// Check that there are untallied results
expect(poll.hasUntalliedBallots()).to.eq(true);
// First batch tally
poll.tallyVotes();
// Recall that each user `i` cast the same number of votes for
// their option `i`
for (let i = 0; i < poll.tallyResult.length - 1; i += 1) {
expect(poll.tallyResult[i].toString()).to.eq(voteWeight.toString());
}
expect(poll.hasUntalliedBallots()).to.eq(false);
expect(() => {
poll.tallyVotes();
}).to.throw();
});
});
describe("Process and tally with non quadratic voting", () => {
let maciState: MaciState;
let pollId: bigint;
let poll: Poll;
let msgTree: IncrementalQuinTree;
let stateTree: IncrementalQuinTree;
const voteWeight = 9n;
const voteOptionIndex = 0n;
let stateIndex: number;
const userKeypair = new Keypair();
const useQv = false;
before(() => {
maciState = new MaciState(STATE_TREE_DEPTH);
msgTree = new IncrementalQuinTree(treeDepths.messageTreeDepth, NOTHING_UP_MY_SLEEVE, 5, hash5);
stateTree = new IncrementalQuinTree(STATE_TREE_DEPTH, blankStateLeafHash, STATE_TREE_ARITY, hash5);
pollId = maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
coordinatorKeypair,
);
poll = maciState.polls.get(pollId)!;
const timestamp = BigInt(Math.floor(Date.now() / 1000));
const stateLeaf = new StateLeaf(userKeypair.pubKey, voiceCreditBalance, timestamp);
stateIndex = maciState.signUp(userKeypair.pubKey, voiceCreditBalance, timestamp);
stateTree.insert(blankStateLeafHash);
stateTree.insert(stateLeaf.hash());
poll.updatePoll(BigInt(maciState.stateLeaves.length));
const command = new PCommand(
BigInt(stateIndex),
userKeypair.pubKey,
voteOptionIndex,
voteWeight,
1n,
BigInt(pollId),
);
const signature = command.sign(userKeypair.privKey);
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
poll.publishMessage(message, ecdhKeypair.pubKey);
msgTree.insert(message.hash(ecdhKeypair.pubKey));
});
it("Process a batch of messages (though only 1 message is in the batch)", () => {
poll.processMessages(pollId, useQv);
// Check the ballot
expect(poll.ballots[1].votes[Number(voteOptionIndex)].toString()).to.eq(voteWeight.toString());
// Check the state leaf in the poll
expect(poll.stateLeaves[1].voiceCreditBalance.toString()).to.eq((voiceCreditBalance - voteWeight).toString());
});
it("Tally ballots", () => {
const initialTotal = calculateTotal(poll.tallyResult);
expect(initialTotal.toString()).to.eq("0");
expect(poll.hasUntalliedBallots()).to.eq(true);
poll.tallyVotesNonQv();
const finalTotal = calculateTotal(poll.tallyResult);
expect(finalTotal.toString()).to.eq(voteWeight.toString());
// check that the totalSpentVoiceCredits is correct
expect(poll.totalSpentVoiceCredits.toString()).to.eq(voteWeight.toString());
});
});
describe("Sanity checks", () => {
let testHarness: TestHarness;
let poll: Poll;
beforeEach(() => {
testHarness = new TestHarness();
poll = testHarness.poll;
});
it("should process a valid message", () => {
const voteOptionIndex = 0n;
const voteWeight = 9n;
const nonce = 1n;
const users = testHarness.createUsers(1);
testHarness.vote(users[0], testHarness.getStateIndex(users[0]), voteOptionIndex, voteWeight, nonce);
testHarness.finalizePoll();
const messageLengthResult = poll.messages.length;
const expectedNumVotes = users.length;
expect(messageLengthResult).to.eq(expectedNumVotes);
const tallyResult = poll.tallyResult[0];
const expectedTallyResult = 9n;
expect(tallyResult).to.eq(expectedTallyResult);
});
it("should not process messages twice", () => {
const voteOptionIndex = 0n;
const voteWeight = 9n;
const nonce = 1n;
const users = testHarness.createUsers(1);
testHarness.vote(users[0], testHarness.getStateIndex(users[0]), voteOptionIndex, voteWeight, nonce);
poll.updatePoll(BigInt(testHarness.maciState.stateLeaves.length));
poll.processMessages(testHarness.pollId);
expect(() => {
poll.processMessages(testHarness.pollId);
}).to.throw("No more messages to process");
poll.tallyVotes();
const messageLengthResult = poll.messages.length;
const expectedNumVotes = users.length;
expect(messageLengthResult).to.eq(expectedNumVotes);
const tallyResult = poll.tallyResult[0];
const expectedTallyResult = 9n;
expect(tallyResult).to.eq(expectedTallyResult);
});
it("should not process a message with an incorrect nonce", () => {
const voteOptionIndex = 0n;
const voteWeight = 9n;
const users = testHarness.createUsers(5);
// generate a bunch of invalid votes with nonces that are not 1
let nonce: bigint;
users.forEach(user => {
do {
nonce = BigInt(Math.floor(Math.random() * 100) - 50);
} while (nonce === 1n);
testHarness.vote(user, testHarness.getStateIndex(user), voteOptionIndex, voteWeight, nonce);
});
testHarness.finalizePoll();
const messageLengthResult = poll.messages.length;
const expectedNumVotes = users.length;
expect(messageLengthResult).to.eq(expectedNumVotes);
const tallyResult = poll.tallyResult[0];
const expectedTallyResult = 0n;
expect(tallyResult).to.eq(expectedTallyResult);
});
// note: When voting, the voice credit is used. The amount of voice credit used is
// the square of the vote weight. Since the maximum voice credit is 100 here,
// the vote weight can only be a value between 1 and 10
// (as these are the square roots of numbers up to 100).
it("should not process a message with an incorrect vote weight", () => {
const voteOptionIndex = 0n;
const nonce = 1n;
const users = testHarness.createUsers(5);
// generate a bunch of invalid votes with vote weights that are not between 1 and 10
let voteWeight: bigint;
users.forEach(user => {
do {
voteWeight = BigInt(Math.floor(Math.random() * 100) - 50);
} while (voteWeight >= 1n && voteWeight <= 10n);
testHarness.vote(user, testHarness.getStateIndex(user), voteOptionIndex, voteWeight, nonce);
});
testHarness.finalizePoll();
const messageLengthResult = poll.messages.length;
const expectedNumVotes = users.length;
expect(messageLengthResult).to.eq(expectedNumVotes);
const tallyResult = poll.tallyResult[0];
const expectedTallyResult = 0n;
expect(tallyResult).to.eq(expectedTallyResult);
});
it("should not process a message with an incorrect state tree index", () => {
const voteOptionIndex = 0n;
const nonce = 1n;
const voteWeight = 9n;
const numVotes = 5;
const users = testHarness.createUsers(5);
users.forEach(user => {
// generate a bunch of invalid votes with incorrect state tree index
testHarness.vote(user, testHarness.getStateIndex(user) + 1, voteOptionIndex, voteWeight, nonce);
});
testHarness.finalizePoll();
const messageLengthResult = poll.messages.length;
const expectedNumVotes = numVotes;
expect(messageLengthResult).to.eq(expectedNumVotes);
const tallyResult = poll.tallyResult[0];
const expectedTallyResult = 0n;
expect(tallyResult).to.eq(expectedTallyResult);
});
it("should not process a message with an incorrect signature", () => {
const voteOptionIndex = 0n;
const voteWeight = 9n;
const nonce = 1n;
const users = testHarness.createUsers(2);
const { command } = testHarness.createCommand(
users[0],
testHarness.getStateIndex(users[0]),
voteOptionIndex,
voteWeight,
nonce,
);
// create an invalid signature
const { signature: invalidSignature } = testHarness.createCommand(
users[1],
testHarness.getStateIndex(users[0]),
voteOptionIndex,
voteWeight,
nonce,
);
// sign the command with the invalid signature
const { message, encPubKey } = testHarness.createMessage(
command,
invalidSignature,
testHarness.coordinatorKeypair,
);
testHarness.poll.publishMessage(message, encPubKey);
testHarness.finalizePoll();
const messageLengthResult = poll.messages.length;
const expectedNumVotes = users.length - 1;
expect(messageLengthResult).to.eq(expectedNumVotes);
const tallyResult = poll.tallyResult[0];
const expectedTallyResult = 0n;
expect(tallyResult).to.eq(expectedTallyResult);
});
it("should not process a message with an invalid coordinator key", () => {
const voteOptionIndex = 0n;
const voteWeight = 9n;
const nonce = 1n;
const users = testHarness.createUsers(1);
const { command, signature } = testHarness.createCommand(
users[0],
testHarness.getStateIndex(users[0]),
voteOptionIndex,
voteWeight,
nonce,
);
const { message, encPubKey } = testHarness.createMessage(command, signature, new Keypair());
testHarness.poll.publishMessage(message, encPubKey);
testHarness.finalizePoll();
const messageLengthResult = poll.messages.length;
const expectedNumVotes = users.length;
expect(messageLengthResult).to.eq(expectedNumVotes);
const tallyResult = poll.tallyResult[0];
const expectedTallyResult = 0n;
expect(tallyResult).to.eq(expectedTallyResult);
});
});
});

View File

@@ -0,0 +1,62 @@
import { expect } from "chai";
import {
genProcessVkSig,
genTallyVkSig,
genSubsidyVkSig,
packProcessMessageSmallVals,
unpackProcessMessageSmallVals,
packTallyVotesSmallVals,
unpackTallyVotesSmallVals,
packSubsidySmallVals,
} from "../utils/utils";
describe("Utils", () => {
it("genProcessVkSig should work", () => {
const result = genProcessVkSig(1, 2, 3, 4);
expect(result).to.equal(25108406941546723055683440059751604127909689873435325366275n);
});
it("genTallyVkSig should work", () => {
const result = genTallyVkSig(1, 2, 3);
expect(result).to.equal(340282366920938463500268095579187314691n);
});
it("genSubsidyVkSig should work", () => {
const result = genSubsidyVkSig(1, 2, 3);
expect(result).to.equal(340282366920938463500268095579187314691n);
});
it("packProcessMessageSmallVals should work", () => {
const result = packProcessMessageSmallVals(1n, 2n, 3, 4);
expect(result).to.equal(5708990770823843327184944562488436835454287873n);
});
it("unpackProcessMessageSmallVals should work", () => {
const result = unpackProcessMessageSmallVals(5708990770823843327184944562488436835454287873n);
expect(result).to.deep.equal({
maxVoteOptions: 1n,
numUsers: 2n,
batchStartIndex: 3n,
batchEndIndex: 4n,
});
});
it("packTallyVotesSmallVals should work", () => {
const result = packTallyVotesSmallVals(1, 2, 3);
expect(result).to.equal(3377699720527872n);
});
it("unpackTallyVotesSmallVals should work", () => {
const result = unpackTallyVotesSmallVals(3377699720527872n);
expect(result).to.deep.equal({
numSignUps: 3n,
batchStartIndex: 0n,
});
});
it("packSubsidySmallVals should work", () => {
const result = packSubsidySmallVals(1, 2, 3);
expect(result).to.equal(3802951800684689330390016458754n);
});
});

View File

@@ -0,0 +1,18 @@
import { Keypair } from "../../../domainobjs";
export const voiceCreditBalance = 100n;
export const duration = 30;
export const messageBatchSize = 25;
export const coordinatorKeypair = new Keypair();
export const maxValues = {
maxUsers: 25,
maxMessages: 25,
maxVoteOptions: 25,
};
export const treeDepths = {
intStateTreeDepth: 2,
messageTreeDepth: 3,
messageTreeSubDepth: 2,
voteOptionTreeDepth: 4,
};

View File

@@ -0,0 +1,154 @@
import { Signature } from "../../../crypto";
import { PCommand, Message, Keypair, PubKey } from "../../../domainobjs";
import { MaciState } from "../../MaciState";
import { Poll } from "../../Poll";
import { STATE_TREE_DEPTH } from "../../utils/constants";
import { duration, maxValues, messageBatchSize, treeDepths, voiceCreditBalance } from "./constants";
/**
* Calculates the total of a tally result
* @param tallyResult - the tally result
* @returns the total of the tally result
*/
export const calculateTotal = (tallyResult: bigint[]): bigint => tallyResult.reduce((acc, v) => acc + v, 0n);
/**
* A test harness for the MACI contract.
*/
export class TestHarness {
maciState = new MaciState(STATE_TREE_DEPTH);
coordinatorKeypair = new Keypair();
poll: Poll;
pollId: bigint;
users: Keypair[] = [];
stateIndices = new Map<Keypair, number>();
/**
* Constructs a new TestHarness object.
*/
constructor() {
this.pollId = this.maciState.deployPoll(
BigInt(Math.floor(Date.now() / 1000) + duration),
maxValues,
treeDepths,
messageBatchSize,
this.coordinatorKeypair,
);
this.poll = this.maciState.polls.get(this.pollId)!;
}
/**
* Creates a number of users and signs them up to the MACI state tree.
* @param numUsers - The number of users to create.
* @returns The keypairs of the newly created users.
*/
createUsers = (numUsers: number): Keypair[] => {
for (let i = 0; i < numUsers; i += 1) {
const user = new Keypair();
this.users.push(user);
const stateIndex = this.signup(user);
this.stateIndices.set(user, stateIndex);
}
return this.users;
};
/**
* Signs up a user to the MACI state tree.
* @param user - The keypair of the user.
* @returns The index of the newly signed-up user in the state tree.
*/
signup = (user: Keypair): number => {
const timestamp = BigInt(Math.floor(Date.now() / 1000));
const stateIndex = this.maciState.signUp(user.pubKey, voiceCreditBalance, timestamp);
return stateIndex;
};
/**
* Publishes a message to the MACI poll instance.
* @param user - The keypair of the user.
* @param stateIndex - The index of the user in the state tree.
* @param voteOptionIndex - The index of the vote option.
* @param voteWeight - The weight of the vote.
* @param nonce - The nonce of the vote.
*/
vote = (user: Keypair, stateIndex: number, voteOptionIndex: bigint, voteWeight: bigint, nonce: bigint): void => {
const { command, signature } = this.createCommand(user, stateIndex, voteOptionIndex, voteWeight, nonce);
const { message, encPubKey } = this.createMessage(command, signature, this.coordinatorKeypair);
this.poll.publishMessage(message, encPubKey);
};
/**
* Creates a message from a command and signature.
* @param command - The command to be encrypted.
* @param signature - The signature of the command signer.
* @param coordinatorKeypair - The keypair of the MACI round coordinator.
* @returns The message and the ephemeral public key used to encrypt the message.
*/
createMessage = (
command: PCommand,
signature: Signature,
coordinatorKeypair: Keypair,
): { message: Message; encPubKey: PubKey } => {
const ecdhKeypair = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey);
const message = command.encrypt(signature, sharedKey);
return { message, encPubKey: ecdhKeypair.pubKey };
};
/**
* Creates a command and signature.
* @param user - The keypair of the user.
* @param stateIndex - The index of the user in the state tree.
* @param voteOptionIndex - The index of the vote option.
* @param voteWeight - The weight of the vote.
* @param nonce - The nonce of the vote.
* @returns The command and signature of the command.
*/
createCommand = (
user: Keypair,
stateIndex: number,
voteOptionIndex: bigint,
voteWeight: bigint,
nonce: bigint,
): { command: PCommand; signature: Signature } => {
const command = new PCommand(
BigInt(stateIndex),
user.pubKey,
voteOptionIndex,
voteWeight,
nonce,
BigInt(this.pollId),
);
const signature = command.sign(user.privKey);
return { command, signature };
};
/**
* Finalizes the poll.
* This processes all messages and tallies the votes.
* This should be called after all votes have been cast.
*/
finalizePoll = (): void => {
this.poll.updatePoll(BigInt(this.maciState.stateLeaves.length));
this.poll.processMessages(this.pollId);
this.poll.tallyVotes();
};
/**
* Returns the state index of a signed-up user.
* @param user - The keypair of the user.
* @returns The state index of the user.
*/
getStateIndex = (user: Keypair): number => this.stateIndices.get(user) || -1;
}

View File

@@ -0,0 +1,27 @@
export { MaciState } from "./MaciState";
export { Poll } from "./Poll";
export {
genProcessVkSig,
genTallyVkSig,
genSubsidyVkSig,
packProcessMessageSmallVals,
unpackProcessMessageSmallVals,
packTallyVotesSmallVals,
unpackTallyVotesSmallVals,
packSubsidySmallVals,
} from "./utils/utils";
export type {
ITallyCircuitInputs,
IProcessMessagesCircuitInputs,
ISubsidyCircuitInputs,
CircuitInputs,
MaxValues,
TreeDepths,
BatchSizes,
IJsonMaciState,
} from "./utils/types";
export { STATE_TREE_ARITY } from "./utils/constants";

View File

@@ -0,0 +1,5 @@
export const STATE_TREE_DEPTH = 10;
export const STATE_TREE_ARITY = 5;
export const STATE_TREE_SUBDEPTH = 2;
export const MESSAGE_TREE_ARITY = 5;
export const VOTE_OPTION_TREE_ARITY = 5;

View File

@@ -0,0 +1,29 @@
/**
* An enum describing the possible errors that can occur
* in Poll.processMessage()
*/
export enum ProcessMessageErrors {
InvalidCommand = "invalid command",
InvalidStateLeafIndex = "invalid state leaf index",
InvalidSignature = "invalid signature",
InvalidNonce = "invalid nonce",
InsufficientVoiceCredits = "insufficient voice credits",
InvalidVoteOptionIndex = "invalid vote option index",
FailedDecryption = "failed decryption due to either wrong encryption public key or corrupted ciphertext",
}
/**
* A class which extends the Error class
* which is to be used when an error occurs
* in Poll.processMessage()
*/
export class ProcessMessageError extends Error {
/**
* Generate a new instance of the ProcessMessageError class
* @param code - the error code
*/
constructor(public code: ProcessMessageErrors) {
super(code);
this.name = this.constructor.name;
}
}

View File

@@ -0,0 +1,224 @@
import type { MaciState } from "../MaciState";
import type { Poll } from "../Poll";
import type { PathElements } from "../../crypto";
import type {
Ballot,
IJsonBallot,
IJsonCommand,
IJsonPCommand,
IJsonStateLeaf,
IJsonTCommand,
Keypair,
Message,
PCommand,
PubKey,
StateLeaf,
TCommand,
} from "../../domainobjs";
/**
* A circuit inputs for the circom circuit
*/
export type CircuitInputs = Record<string, string | bigint | bigint[] | bigint[][] | string[] | bigint[][][]>;
/**
* This interface defines the tree depths.
* @property intStateTreeDepth - The depth of the intermediate state tree.
* @property messageTreeDepth - The depth of the message tree.
* @property messageTreeSubDepth - The depth of the message tree sub.
* @property voteOptionTreeDepth - The depth of the vote option tree.
*/
export interface TreeDepths {
intStateTreeDepth: number;
messageTreeDepth: number;
messageTreeSubDepth: number;
voteOptionTreeDepth: number;
}
/**
* This interface defines the batch sizes.
* @property tallyBatchSize - The size of the tally batch.
* @property messageBatchSize - The size of the message batch.
* @property subsidyBatchSize - The size of the subsidy batch.
*/
export interface BatchSizes {
tallyBatchSize: number;
messageBatchSize: number;
subsidyBatchSize: number;
}
/**
* This interface defines the maximum values that the circuit can handle.
* @property maxMessages - The maximum number of messages.
* @property maxVoteOptions - The maximum number of vote options.
*/
export interface MaxValues {
maxMessages: number;
maxVoteOptions: number;
}
/**
* Represents the public API of the MaciState class.
*/
export interface IMaciState {
// This method is used for signing up users to the state tree.
signUp(pubKey: PubKey, initialVoiceCreditBalance: bigint, timestamp: bigint): number;
// This method is used for deploying poll.
deployPoll(
pollEndTimestamp: bigint,
maxValues: MaxValues,
treeDepths: TreeDepths,
messageBatchSize: number,
coordinatorKeypair: Keypair,
): bigint;
// These methods are helper functions.
deployNullPoll(): void;
copy(): MaciState;
equals(m: MaciState): boolean;
toJSON(): IJsonMaciState;
}
/**
* An interface which represents the public API of the Poll class.
*/
export interface IPoll {
// These methods are used for sending a message to the poll from user
publishMessage(message: Message, encPubKey: PubKey): void;
topupMessage(message: Message): void;
// These methods are used to generate circuit inputs
processMessages(pollId: bigint): IProcessMessagesCircuitInputs;
tallyVotes(): ITallyCircuitInputs;
// These methods are helper functions
hasUnprocessedMessages(): boolean;
processAllMessages(): { stateLeaves: StateLeaf[]; ballots: Ballot[] };
hasUntalliedBallots(): boolean;
hasUnfinishedSubsidyCalculation(): boolean;
subsidyPerBatch(): ISubsidyCircuitInputs;
copy(): Poll;
equals(p: Poll): boolean;
toJSON(): IJsonPoll;
setCoordinatorKeypair(serializedPrivateKey: string): void;
}
/**
* This interface defines the JSON representation of a Poll
*/
export interface IJsonPoll {
pollEndTimestamp: string;
treeDepths: TreeDepths;
batchSizes: BatchSizes;
maxValues: MaxValues;
messages: unknown[];
commands: IJsonCommand[] | IJsonTCommand[] | IJsonPCommand[];
ballots: IJsonBallot[];
encPubKeys: string[];
currentMessageBatchIndex: number;
stateLeaves: IJsonStateLeaf[];
results: string[];
numBatchesProcessed: number;
numSignups: string;
}
/**
* This interface defines the JSON representation of a MaciState
*/
export interface IJsonMaciState {
stateTreeDepth: number;
polls: IJsonPoll[];
stateLeaves: IJsonStateLeaf[];
pollBeingProcessed: boolean;
currentPollBeingProcessed: string;
numSignUps: number;
}
/**
* An interface describing the output of the processMessage function
*/
export interface IProcessMessagesOutput {
stateLeafIndex?: number;
newStateLeaf?: StateLeaf;
originalStateLeaf?: StateLeaf;
originalStateLeafPathElements?: PathElements;
originalVoteWeight?: bigint;
originalVoteWeightsPathElements?: PathElements;
newBallot?: Ballot;
originalBallot?: Ballot;
originalBallotPathElements?: PathElements;
command?: PCommand | TCommand;
}
/**
* An interface describing the circuit inputs to the ProcessMessage circuit
*/
export interface IProcessMessagesCircuitInputs {
pollEndTimestamp: string;
packedVals: string;
msgRoot: string;
msgs: string[];
msgSubrootPathElements: string[][];
coordPrivKey: string;
coordPubKey: string;
encPubKeys: string[];
currentStateRoot: string;
currentBallotRoot: string;
currentSbCommitment: string;
currentSbSalt: string;
currentStateLeaves: string[];
currentStateLeavesPathElements: string[][];
currentBallots: string[];
currentBallotsPathElements: string[][];
currentVoteWeights: string[];
currentVoteWeightsPathElements: string[][];
inputHash: string;
newSbSalt: string;
newSbCommitment: string;
}
/**
* An interface describing the circuit inputs to the TallyVotes circuit
*/
export interface ITallyCircuitInputs {
stateRoot: string;
ballotRoot: string;
sbSalt: string;
sbCommitment: string;
currentTallyCommitment: string;
newTallyCommitment: string;
packedVals: string;
inputHash: string;
ballots: string[];
ballotPathElements: PathElements;
votes: string[][];
currentResults: string[];
currentResultsRootSalt: string;
currentSpentVoiceCreditSubtotal: string;
currentSpentVoiceCreditSubtotalSalt: string;
currentPerVOSpentVoiceCredits?: string[];
currentPerVOSpentVoiceCreditsRootSalt?: string;
newResultsRootSalt: string;
newPerVOSpentVoiceCreditsRootSalt?: string;
newSpentVoiceCreditSubtotalSalt: string;
}
/**
* An interface describing the circuit inputs to the Subsidy circuit
*/
export interface ISubsidyCircuitInputs {
stateRoot: string;
ballotRoot: string;
sbSalt: string;
currentSubsidySalt: string;
newSubsidySalt: string;
sbCommitment: string;
currentSubsidyCommitment: string;
newSubsidyCommitment: string;
currentSubsidy: string[];
packedVals: string;
inputHash: string;
ballots1: string[];
ballots2: string[];
votes1: number[];
votes2: number[];
ballotPathElements1: string[];
ballotPathElements2: string[];
}

View File

@@ -0,0 +1,155 @@
/* eslint-disable no-bitwise */
import assert from "assert";
/**
* This function generates the signature of a ProcessMessage Verifying Key(VK).
* This can be used to check if a ProcessMessages' circuit VK is registered
* in a smart contract that holds several VKs.
* @param stateTreeDepth - The depth of the state tree.
* @param messageTreeDepth - The depth of the message tree.
* @param voteOptionTreeDepth - The depth of the vote option tree.
* @param batchSize - The size of the batch.
* @returns Returns a signature for querying if a verifying key with the given parameters is already registered in the contract.
*/
export const genProcessVkSig = (
stateTreeDepth: number,
messageTreeDepth: number,
voteOptionTreeDepth: number,
batchSize: number,
): bigint =>
(BigInt(batchSize) << 192n) +
(BigInt(stateTreeDepth) << 128n) +
(BigInt(messageTreeDepth) << 64n) +
BigInt(voteOptionTreeDepth);
/**
* This function generates the signature of a Tally Verifying Key(VK).
* This can be used to check if a TallyVotes' circuit VK is registered
* in a smart contract that holds several VKs.
* @param _stateTreeDepth - The depth of the state tree.
* @param _intStateTreeDepth - The depth of the intermediate state tree.
* @param _voteOptionTreeDepth - The depth of the vote option tree.
* @returns Returns a signature for querying if a verifying key with
* the given parameters is already registered in the contract.
*/
export const genTallyVkSig = (
_stateTreeDepth: number,
_intStateTreeDepth: number,
_voteOptionTreeDepth: number,
): bigint => (BigInt(_stateTreeDepth) << 128n) + (BigInt(_intStateTreeDepth) << 64n) + BigInt(_voteOptionTreeDepth);
/**
* This function generates the signature of a Subsidy Verifying Key(VK).
* This can be used to check if a SubsidyCalculations' circuit VK is registered
* in a smart contract that holds several VKs.
* @param _stateTreeDepth - The depth of the state tree.
* @param _intStateTreeDepth - The depth of the intermediate state tree.
* @param _voteOptionTreeDepth - The depth of the vote option tree.
* @returns Returns a signature for querying if a verifying key with
* the given parameters is already registered in the contract.
*/
export const genSubsidyVkSig = (
_stateTreeDepth: number,
_intStateTreeDepth: number,
_voteOptionTreeDepth: number,
): bigint => (BigInt(_stateTreeDepth) << 128n) + (BigInt(_intStateTreeDepth) << 64n) + BigInt(_voteOptionTreeDepth);
/**
* This function packs it's parameters into a single bigint.
* @param maxVoteOptions - The maximum number of vote options.
* @param numUsers - The number of users.
* @param batchStartIndex - The start index of the batch.
* @param batchEndIndex - The end index of the batch.
* @returns Returns a single bigint that contains the packed values.
*/
export const packProcessMessageSmallVals = (
maxVoteOptions: bigint,
numUsers: bigint,
batchStartIndex: number,
batchEndIndex: number,
): bigint => {
const packedVals =
// Note: the << operator has lower precedence than +
BigInt(`${maxVoteOptions}`) +
(BigInt(`${numUsers}`) << 50n) +
(BigInt(batchStartIndex) << 100n) +
(BigInt(batchEndIndex) << 150n);
return packedVals;
};
/**
* This function unpacks partial values for the ProcessMessages circuit from a single bigint.
* @param packedVals - The single bigint that contains the packed values.
* @returns Returns an object that contains the unpacked values.
*/
export const unpackProcessMessageSmallVals = (
packedVals: bigint,
): {
maxVoteOptions: bigint;
numUsers: bigint;
batchStartIndex: bigint;
batchEndIndex: bigint;
} => {
let asBin = packedVals.toString(2);
assert(asBin.length <= 200);
while (asBin.length < 200) {
asBin = `0${asBin}`;
}
const maxVoteOptions = BigInt(`0b${asBin.slice(150, 200)}`);
const numUsers = BigInt(`0b${asBin.slice(100, 150)}`);
const batchStartIndex = BigInt(`0b${asBin.slice(50, 100)}`);
const batchEndIndex = BigInt(`0b${asBin.slice(0, 50)}`);
return {
maxVoteOptions,
numUsers,
batchStartIndex,
batchEndIndex,
};
};
/**
* This function packs it's parameters into a single bigint.
* @param batchStartIndex - The start index of the batch.
* @param batchSize - The size of the batch.
* @param numSignUps - The number of signups.
* @returns Returns a single bigint that contains the packed values.
*/
export const packTallyVotesSmallVals = (batchStartIndex: number, batchSize: number, numSignUps: number): bigint => {
// Note: the << operator has lower precedence than +
const packedVals = BigInt(batchStartIndex) / BigInt(batchSize) + (BigInt(numSignUps) << 50n);
return packedVals;
};
/**
* This function unpacks partial values for the TallyVotes circuit from a single bigint.
* @param packedVals - The single bigint that contains the packed values.
* @returns Returns an object that contains the unpacked values.
*/
export const unpackTallyVotesSmallVals = (packedVals: bigint): { numSignUps: bigint; batchStartIndex: bigint } => {
let asBin = packedVals.toString(2);
assert(asBin.length <= 100);
while (asBin.length < 100) {
asBin = `0${asBin}`;
}
const numSignUps = BigInt(`0b${asBin.slice(0, 50)}`);
const batchStartIndex = BigInt(`0b${asBin.slice(50, 100)}`);
return { numSignUps, batchStartIndex };
};
/**
* This function packs it's parameters into a single bigint.
* @param row - The row.
* @param col - The column.
* @param numSignUps - The number of signups.
* @returns Returns a single bigint that contains the packed values.
*/
export const packSubsidySmallVals = (row: number, col: number, numSignUps: number): bigint => {
// Note: the << operator has lower precedence than +
const packedVals = (BigInt(numSignUps) << 100n) + (BigInt(row) << 50n) + BigInt(col);
return packedVals;
};

View File

@@ -0,0 +1,627 @@
import assert from "assert";
import type { Leaf, Queue, StringifiedBigInts } from "./types";
import { deepCopyBigIntArray, stringifyBigInts, unstringifyBigInts } from "./bigIntUtils";
import { sha256Hash, hashLeftRight, hash5 } from "./hashing";
import { IncrementalQuinTree } from "./quinTree";
import { calcDepthFromNumLeaves } from "./utils";
/**
* An Accumulator Queue which conforms to the implementation in AccQueue.sol.
* Each enqueue() operation updates a subtree, and a merge() operation combines
* all subtrees into a main tree.
* @notice It supports 2 or 5 elements per leaf.
*/
export class AccQueue {
private MAX_DEPTH = 32;
// The depth per subtree
private subDepth: number;
// The number of inputs per hash function
private hashLength: number;
// The default value for empty leaves
private zeroValue: bigint;
// The current subtree index. e.g. the first subtree has index 0, the
// second has 1, and so on
private currentSubtreeIndex = 0;
// The number of leaves across all subtrees
private numLeaves = 0;
// The current subtree
private leafQueue: Queue = {
levels: new Map(),
indices: [],
};
// For merging subtrees into the smallest tree
private nextSRindexToQueue = 0;
private smallSRTroot = 0n;
private subRootQueue: Queue = {
levels: new Map(),
indices: [],
};
// The root of each complete subtree
private subRoots: Leaf[] = [];
// The root of merged subtrees
private mainRoots: Leaf[] = [];
// The zero value per level. i.e. zeros[0] is zeroValue,
// zeros[1] is the hash of leavesPerNode zeros, and so on.
private zeros: bigint[] = [];
// Whether the subtrees have been merged
private subTreesMerged = false;
// The hash function to use for the subtrees
readonly subHashFunc: (leaves: Leaf[]) => bigint;
// The hash function to use for rest of the tree (above the subroots)
readonly hashFunc: (leaves: Leaf[]) => bigint;
/**
* Create a new instance of AccQueue
* @param subDepth - the depth of the subtrees
* @param hashLength - the number of leaves per node
* @param zeroValue - the default value for empty leaves
*/
constructor(subDepth: number, hashLength: number, zeroValue: bigint) {
// This class supports either 2 leaves per node, or 5 leaves per node.
// 5 is largest number of inputs which circomlib's Poseidon EVM hash
// function implementation supports.
assert(hashLength === 2 || hashLength === 5);
assert(subDepth > 0);
this.hashLength = hashLength;
this.subDepth = subDepth;
this.zeroValue = zeroValue;
// Set this.hashFunc depending on the number of leaves per node
if (this.hashLength === 2) {
// Uses PoseidonT3 under the hood, which accepts 2 inputs
this.hashFunc = (inputs: bigint[]) => hashLeftRight(inputs[0], inputs[1]);
} else {
// Uses PoseidonT6 under the hood, which accepts up to 5 inputs
this.hashFunc = hash5;
}
this.subHashFunc = sha256Hash;
let hashed = this.zeroValue;
for (let i = 0; i < this.MAX_DEPTH; i += 1) {
this.zeros.push(hashed);
let e: bigint[] = [];
if (this.hashLength === 2) {
e = [0n];
hashed = this.hashFunc([hashed, hashed]);
} else {
e = [0n, 0n, 0n, 0n];
hashed = this.hashFunc([hashed, hashed, hashed, hashed, hashed]);
}
const levels = new Map(Object.entries(e).map(([key, value]) => [Number(key), value]));
this.leafQueue.levels.set(this.leafQueue.levels.size, levels);
this.leafQueue.indices[i] = 0;
this.subRootQueue.levels.set(this.subRootQueue.levels.size, levels);
this.subRootQueue.indices[i] = 0;
}
}
/**
* Get the small SRT root
* @returns small SRT root
*/
getSmallSRTroot(): bigint {
return this.smallSRTroot;
}
/**
* Get the subroots
* @returns subroots
*/
getSubRoots(): Leaf[] {
return this.subRoots;
}
/**
* Get the subdepth
* @returns subdepth
*/
getSubDepth(): number {
return this.subDepth;
}
/**
* Get the root of merged subtrees
* @returns the root of merged subtrees
*/
getMainRoots(): Leaf[] {
return this.mainRoots;
}
/**
* Get the zero values per level. i.e. zeros[0] is zeroValue,
* zeros[1] is the hash of leavesPerNode zeros, and so on.
* @returns zeros
*/
getZeros(): bigint[] {
return this.zeros;
}
/**
* Get the subroot at a given index
* @param index - The index of the subroot
* @returns the subroot
*/
getSubRoot(index: number): Leaf {
return this.subRoots[index];
}
/**
* Get the number of inputs per hash function
*
* @returns the number of inputs
*/
getHashLength(): number {
return this.hashLength;
}
/**
* Enqueue a leaf into the current subtree
* @param leaf The leaf to insert.
* @returns The index of the leaf
*/
enqueue(leaf: Leaf): number {
// validation
assert(this.numLeaves < this.hashLength ** this.MAX_DEPTH, "AccQueue is full");
this.enqueueOp(leaf, 0);
// the index is the number of leaves (0-index)
const leafIndex = this.numLeaves;
// increase the number of leaves
this.numLeaves += 1;
// we set merged false because there are new leaves
this.subTreesMerged = false;
// reset the smallSRTroot because it is obsolete
this.smallSRTroot = 0n;
// @todo this can be moved in the constructor rather than computing every time
const subTreeCapacity = this.hashLength ** this.subDepth;
// If the current subtree is full
if (this.numLeaves % subTreeCapacity === 0) {
// store the subroot
const subRoot = this.leafQueue.levels.get(this.subDepth)?.get(0) ?? 0n;
this.subRoots[this.currentSubtreeIndex] = subRoot;
this.currentSubtreeIndex += 1;
// reset the current subtree
this.leafQueue.levels.get(this.subDepth)?.set(0, 0n);
for (let i = 0; i < this.MAX_DEPTH; i += 1) {
this.leafQueue.indices[i] = 0;
}
}
return leafIndex;
}
/**
* Private function that performs the actual enqueue operation
* @param leaf - The leaf to insert
* @param level - The level of the subtree
*/
private enqueueOp = (leaf: Leaf, level: number) => {
// small validation, do no throw
if (level > this.subDepth) {
return;
}
// get the index to determine where to insert the next leaf
const n = this.leafQueue.indices[level];
// we check that the index is not the last one (1 or 4 depending on the hash length)
if (n !== this.hashLength - 1) {
// Just store the leaf
this.leafQueue.levels.get(level)?.set(n, leaf);
this.leafQueue.indices[level] += 1;
} else {
// if not we compute the root
let hashed: bigint;
if (this.hashLength === 2) {
const subRoot = this.leafQueue.levels.get(level)?.get(0) ?? 0n;
hashed = this.hashFunc([subRoot, leaf]);
this.leafQueue.levels.get(level)?.set(0, 0n);
} else {
const levelSlice = this.leafQueue.levels.get(level) ?? new Map<number, bigint>();
hashed = this.hashFunc(Array.from(levelSlice.values()).concat(leaf));
for (let i = 0; i < 4; i += 1) {
this.leafQueue.levels.get(level)?.set(i, 0n);
}
}
this.leafQueue.indices[level] = 0;
// Recurse
this.enqueueOp(hashed, level + 1);
}
};
/**
* Fill any empty leaves of the last subtree with zeros and store the
* resulting subroot.
*/
fill(): void {
// The total capacity of the subtree
const subTreeCapacity = this.hashLength ** this.subDepth;
if (this.numLeaves % subTreeCapacity === 0) {
// If the subtree is completely empty, then the subroot is a
// precalculated zero value
this.subRoots[this.currentSubtreeIndex] = this.zeros[this.subDepth];
} else {
this.fillOp(0);
// Store the subroot
const subRoot = this.leafQueue.levels.get(this.subDepth)?.get(0) ?? 0n;
this.subRoots[this.currentSubtreeIndex] = subRoot;
// Blank out the subtree data
for (let i = 0; i < this.subDepth + 1; i += 1) {
if (this.hashLength === 2) {
this.leafQueue.levels.get(i)?.set(0, 0n);
} else {
const levels = new Map(Object.entries([0n, 0n, 0n, 0n]).map(([key, value]) => [Number(key), value]));
this.leafQueue.levels.set(i, levels);
}
}
}
// Update the subtree index
this.currentSubtreeIndex += 1;
// Update the number of leaves
this.numLeaves = this.currentSubtreeIndex * subTreeCapacity;
this.subTreesMerged = false;
this.smallSRTroot = 0n;
}
/**
* Private function that performs the actual fill operation
* @param level - The level of the subtree
*/
private fillOp(level: number) {
if (level > this.subDepth) {
return;
}
const n = this.leafQueue.indices[level];
if (n !== 0) {
// Fill the subtree level and hash it
let hashed: bigint;
if (this.hashLength === 2) {
hashed = this.hashFunc([this.leafQueue.levels.get(level)?.get(0) ?? 0n, this.zeros[level]]);
} else {
for (let i = n; i < this.hashLength; i += 1) {
this.leafQueue.levels.get(level)?.set(i, this.zeros[level]);
}
const levelSlice = this.leafQueue.levels.get(level) ?? new Map<number, bigint>();
hashed = this.hashFunc(Array.from(levelSlice.values()));
}
// Update the subtree from the next level onwards with the new leaf
this.enqueueOp(hashed, level + 1);
// Reset the current level
this.leafQueue.indices[level] = 0;
}
// Recurse
this.fillOp(level + 1);
}
/**
* Calculate the depth of the smallest possible Merkle tree which fits all
* @returns the depth of the smallest possible Merkle tree which fits all
*/
calcSRTdepth(): number {
// Calculate the SRT depth
let srtDepth = this.subDepth;
const subTreeCapacity = this.hashLength ** this.subDepth;
while (this.hashLength ** srtDepth < this.subRoots.length * subTreeCapacity) {
srtDepth += 1;
}
return srtDepth;
}
/**
* Insert a subtree into the queue. This is used when the subtree is
* already computed.
* @param subRoot - The root of the subtree
*/
insertSubTree(subRoot: bigint): void {
// If the current subtree is not full, fill it.
const subTreeCapacity = this.hashLength ** this.subDepth;
this.subRoots[this.currentSubtreeIndex] = subRoot;
// Update the subtree index
this.currentSubtreeIndex += 1;
// Update the number of leaves
this.numLeaves += subTreeCapacity;
// Reset the subroot tree root now that it is obsolete
this.smallSRTroot = 0n;
this.subTreesMerged = false;
}
/**
* Merge all the subroots into a tree of a specified depth.
* It requires this.mergeSubRoots() to be run first.
*/
merge(depth: number): void {
assert(this.subTreesMerged);
assert(depth <= this.MAX_DEPTH);
const srtDepth = this.calcSRTdepth();
assert(depth >= srtDepth);
if (depth === srtDepth) {
this.mainRoots[depth] = this.smallSRTroot;
} else {
let root = this.smallSRTroot;
// Calculate the main root
for (let i = srtDepth; i < depth; i += 1) {
const inputs: bigint[] = [root];
const z = this.zeros[i];
for (let j = 1; j < this.hashLength; j += 1) {
inputs.push(z);
}
root = this.hashFunc(inputs);
}
this.mainRoots[depth] = root;
}
}
/**
* Merge all the subroots into a tree of a specified depth.
* Uses an IncrementalQuinTree instead of the two-step method that
* AccQueue.sol uses.
*/
mergeDirect(depth: number): void {
// There must be subtrees to merge
assert(this.numLeaves > 0);
const srtDepth = this.calcSRTdepth();
// The desired tree must be deep enough
assert(depth >= srtDepth);
if (depth === this.subDepth) {
// If there is only 1 subtree, and the desired depth is the subtree
// depth, the subroot is the result
assert(this.numLeaves === this.hashLength ** this.subDepth);
const [subRoot] = this.subRoots;
this.mainRoots[depth] = subRoot;
this.subTreesMerged = true;
return;
}
// The desired main tree must be deep enough to fit all leaves
assert(BigInt(depth ** this.hashLength) >= this.numLeaves);
// Fill any empty leaves in the last subtree with zeros
if (this.numLeaves % this.hashLength ** this.subDepth > 0) {
this.fill();
}
const tree = new IncrementalQuinTree(
depth - this.subDepth,
this.zeros[this.subDepth],
this.hashLength,
this.hashFunc,
);
this.subRoots.forEach(subRoot => {
tree.insert(subRoot);
});
this.mainRoots[depth] = tree.root;
}
/**
* Merge all subroots into the smallest possible Merkle tree which fits
* them. e.g. if there are 5 subroots and hashLength == 2, the tree depth
* is 3 since 2 ** 3 = 8 which is the next power of 2.
* @param numSrQueueOps - The number of subroots to queue into the SRT
*/
mergeSubRoots(numSrQueueOps = 0): void {
// This function can only be called once unless a new subtree is created
assert(!this.subTreesMerged);
// There must be subtrees to merge
assert(this.numLeaves > 0);
// Fill any empty leaves in the last subtree with zeros
if (this.numLeaves % this.hashLength ** this.subDepth !== 0) {
this.fill();
}
// If there is only 1 subtree, use its root
if (this.currentSubtreeIndex === 1) {
this.smallSRTroot = this.getSubRoot(0);
this.subTreesMerged = true;
return;
}
// Compute the depth and maximum capacity of the smallMainTreeRoot
const depth = calcDepthFromNumLeaves(this.hashLength, this.currentSubtreeIndex);
let numQueueOps = 0;
for (let i = this.nextSRindexToQueue; i < this.currentSubtreeIndex; i += 1) {
// Stop if the limit has been reached
if (numSrQueueOps !== 0 && numQueueOps === numSrQueueOps) {
return;
}
// Queue the next subroot
const subRoot = this.getSubRoot(this.nextSRindexToQueue);
this.queueSubRoot(subRoot, 0, depth);
// Increment the next subroot counter
this.nextSRindexToQueue += 1;
numQueueOps += 1;
}
// Queue zeros to get the SRT. `m` is the number of leaves in the
// main tree, which already has `this.currentSubtreeIndex` leaves
const m = this.hashLength ** depth;
if (this.nextSRindexToQueue === this.currentSubtreeIndex) {
for (let i = this.currentSubtreeIndex; i < m; i += 1) {
const z = this.zeros[this.subDepth];
this.queueSubRoot(z, 0, depth);
}
}
// Store the root
const subRoot = this.subRootQueue.levels.get(depth)?.get(0) ?? 0n;
this.smallSRTroot = subRoot;
this.subTreesMerged = true;
}
/**
* Queues the leaf (a subroot) into queuedSRTlevels
* @param leaf - The leaf to insert
* @param level - The level of the subtree
* @param maxDepth - The maximum depth of the tree
*/
private queueSubRoot(leaf: bigint, level: number, maxDepth: number) {
if (level > maxDepth) {
return;
}
const n = this.subRootQueue.indices[level];
if (n !== this.hashLength - 1) {
// Just store the leaf
this.subRootQueue.levels.get(level)?.set(n, leaf);
this.subRootQueue.indices[level] += 1;
} else {
// Hash the elements in this level and queue it in the next level
const inputs: bigint[] = [];
for (let i = 0; i < this.hashLength - 1; i += 1) {
inputs.push(this.subRootQueue.levels.get(level)?.get(i) ?? 0n);
}
inputs.push(leaf);
const hashed = this.hashFunc(inputs);
// Recurse
this.subRootQueue.indices[level] = 0;
this.queueSubRoot(hashed, level + 1, maxDepth);
}
}
/**
* Get the root at a certain depth
* @param depth - The depth of the tree
* @returns the root
*/
getRoot(depth: number): bigint | null | undefined {
return this.mainRoots[depth];
}
/**
* Check if the root at a certain depth exists (subtree root)
* @param depth - the depth of the tree
* @returns whether the root exists
*/
hasRoot(depth: number): boolean {
const root = this.getRoot(depth);
return !(root === null || root === undefined);
}
/**
* @notice Deep-copies this object
* @returns a deep copy of this object
*/
copy(): AccQueue {
const newAccQueue = new AccQueue(this.subDepth, this.hashLength, this.zeroValue);
newAccQueue.currentSubtreeIndex = JSON.parse(JSON.stringify(this.currentSubtreeIndex)) as number;
newAccQueue.numLeaves = JSON.parse(JSON.stringify(this.numLeaves)) as number;
const arrayLeafLevels = unstringifyBigInts(
JSON.parse(JSON.stringify(stringifyBigInts(this.mapToArray(this.leafQueue.levels)))) as StringifiedBigInts,
) as bigint[][];
newAccQueue.leafQueue.levels = this.arrayToMap(arrayLeafLevels);
newAccQueue.leafQueue.indices = JSON.parse(JSON.stringify(this.leafQueue.indices)) as number[];
newAccQueue.subRoots = deepCopyBigIntArray(this.subRoots);
newAccQueue.mainRoots = deepCopyBigIntArray(this.mainRoots);
newAccQueue.zeros = deepCopyBigIntArray(this.zeros);
newAccQueue.subTreesMerged = !!this.subTreesMerged;
newAccQueue.nextSRindexToQueue = Number(this.nextSRindexToQueue.toString());
newAccQueue.smallSRTroot = BigInt(this.smallSRTroot.toString());
newAccQueue.subRootQueue.indices = JSON.parse(JSON.stringify(this.subRootQueue.indices)) as number[];
const arraySubRootLevels = unstringifyBigInts(
JSON.parse(JSON.stringify(stringifyBigInts(this.mapToArray(this.subRootQueue.levels)))) as StringifiedBigInts,
) as bigint[][];
newAccQueue.subRootQueue.levels = this.arrayToMap(arraySubRootLevels);
return newAccQueue;
}
/**
* Convert map to 2D array
*
* @param map - map representation of 2D array
* @returns 2D array
*/
private mapToArray(map: Map<number, Map<number, bigint>>): bigint[][] {
return Array.from(map.values()).map(v => Array.from(v.values()));
}
/**
* Convert 2D array to its map representation
*
* @param array - 2D array
* @returns map representation of 2D array
*/
private arrayToMap(array: bigint[][]): Map<number, Map<number, bigint>> {
return new Map(array.map((level, i) => [i, new Map(level.map((leaf, j) => [j, leaf]))]));
}
/**
* Hash an array of leaves
* @param leaves - The leaves to hash
* @returns the hash value of the leaves
*/
hash(leaves: bigint[]): bigint {
assert(leaves.length === this.hashLength);
return this.hashFunc(leaves);
}
}

View File

@@ -0,0 +1,3 @@
import runTrees from "./suites/trees";
runTrees();

View File

@@ -0,0 +1,40 @@
import benny from "benny";
import { IncrementalQuinTree, hash5 } from "../../index";
const NAME = "merkle-trees";
export default function runTrees(): void {
const treeDepth = 2;
const numberOfLeaves = 5 ** treeDepth;
benny.suite(
NAME,
benny.add(`MACI - insert, update, generate and verify proof for ${numberOfLeaves} leaves`, () => {
const tree5 = new IncrementalQuinTree(treeDepth, BigInt(0), 5, hash5);
for (let i = 0; i < numberOfLeaves; i += 1) {
tree5.insert(BigInt(i));
}
for (let i = 0; i < numberOfLeaves; i += 1) {
tree5.update(i, BigInt(0));
}
tree5.verifyProof(tree5.genProof(5));
}),
benny.cycle(),
benny.complete(results => {
results.results.forEach(result => {
// eslint-disable-next-line no-console
console.log(`${result.name}: mean time: ${result.details.mean.toFixed(2)}`);
});
}),
benny.save({ folder: "ts/__benchmarks__/results", file: NAME, version: "1.0.0", details: true }),
benny.save({ folder: "ts/__benchmarks__/results", file: NAME, format: "chart.html", details: true }),
benny.save({ folder: "ts/__benchmarks__/results", file: NAME, format: "table.html", details: true }),
);
}

View File

@@ -0,0 +1,256 @@
import { expect } from "chai";
import { IncrementalQuinTree, AccQueue } from "..";
import { testMerge, testMergeExhaustive, testMergeShortest, testMergeShortestOne } from "./utils";
describe("AccQueue", function test() {
this.timeout(100000);
describe("Enqueue", () => {
describe("Binary AccQueue", () => {
const HASH_LENGTH = 2;
const SUB_DEPTH = 2;
const ZERO = BigInt(0);
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
it("should enqueue leaves into a subtree", () => {
const tree0 = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
const subtreeCapacity = HASH_LENGTH ** SUB_DEPTH;
for (let i = 0; i < subtreeCapacity; i += 1) {
const leaf = BigInt(i + 1);
tree0.insert(leaf);
aq.enqueue(leaf);
}
expect(aq.getSubRoot(0).toString()).to.eq(tree0.root.toString());
});
it("should enqueue another subtree", () => {
const tree1 = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
const subtreeCapacity = HASH_LENGTH ** SUB_DEPTH;
for (let i = 0; i < subtreeCapacity; i += 1) {
const leaf = BigInt(i + 1);
tree1.insert(leaf);
aq.enqueue(leaf);
}
expect(aq.getSubRoot(1).toString()).to.eq(tree1.root.toString());
});
});
describe("Quinary AccQueue", () => {
const HASH_LENGTH = 5;
const SUB_DEPTH = 2;
const ZERO = BigInt(0);
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
it("should enqueue leaves into a subtree", () => {
const tree0 = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
const subtreeCapacity = HASH_LENGTH ** SUB_DEPTH;
for (let i = 0; i < subtreeCapacity; i += 1) {
const leaf = BigInt(i + 1);
tree0.insert(leaf);
aq.enqueue(leaf);
}
expect(aq.getSubRoot(0).toString()).to.eq(tree0.root.toString());
const tree1 = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
for (let i = 0; i < subtreeCapacity; i += 1) {
const leaf = BigInt(i + 1);
tree1.insert(leaf);
aq.enqueue(leaf);
}
expect(aq.getSubRoot(1).toString()).to.eq(tree1.root.toString());
});
});
});
describe("Fill", () => {
describe("Binary AccQueue", () => {
const HASH_LENGTH = 2;
const SUB_DEPTH = 2;
const ZERO = BigInt(0);
it("Filling an empty subtree should create the correct subroot", () => {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const tree = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
aq.fill();
expect(aq.getSubRoot(0).toString()).to.eq(tree.root.toString());
});
it("should fill an incomplete subtree", () => {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const tree = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
const leaf = BigInt(1);
aq.enqueue(leaf);
tree.insert(leaf);
aq.fill();
expect(aq.getSubRoot(0).toString()).to.eq(tree.root.toString());
});
it("Filling an empty subtree again should create the correct subroot", () => {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const leaf = BigInt(1);
// Create the first subtree with one leaf
aq.enqueue(leaf);
aq.fill();
// Fill the second subtree with zeros
aq.fill();
const tree = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
expect(aq.getSubRoot(1).toString()).to.eq(tree.root.toString());
});
it("fill() should be correct for every number of leaves in an incomplete subtree", () => {
for (let i = 0; i < 2; i += 1) {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const tree = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
for (let j = 0; j < i; j += 1) {
const leaf = BigInt(i + 1);
aq.enqueue(leaf);
tree.insert(leaf);
}
aq.fill();
expect(aq.getSubRoot(0).toString()).to.eq(tree.root.toString());
}
});
});
describe("Quinary AccQueue", () => {
const HASH_LENGTH = 5;
const SUB_DEPTH = 2;
const ZERO = BigInt(0);
it("Filling an empty subtree should create the correct subroot", () => {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const tree = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
aq.fill();
expect(aq.getSubRoot(0).toString()).to.eq(tree.root.toString());
});
it("should fill one incomplete subtree", () => {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const tree = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
const leaf = BigInt(1);
aq.enqueue(leaf);
tree.insert(leaf);
aq.fill();
expect(aq.getSubRoot(0).toString()).to.eq(tree.root.toString());
});
it("Filling an empty subtree again should create the correct subroot", () => {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const leaf = BigInt(1);
// Create the first subtree with one leaf
aq.enqueue(leaf);
aq.fill();
// Fill the second subtree with zeros
aq.fill();
const tree = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
expect(aq.getSubRoot(1).toString()).to.eq(tree.root.toString());
});
it("fill() should be correct for every number of leaves in an incomplete subtree", () => {
const capacity = HASH_LENGTH ** SUB_DEPTH;
for (let i = 1; i < capacity - 1; i += 1) {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const tree = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
for (let j = 0; j < i; j += 1) {
const leaf = BigInt(i + 1);
aq.enqueue(leaf);
tree.insert(leaf);
}
aq.fill();
expect(aq.getSubRoot(0).toString()).to.eq(tree.root.toString());
}
});
});
});
describe("Merge", () => {
const SUB_DEPTH = 2;
const ZERO = BigInt(0);
const NUM_SUBTREES = 5;
const MAIN_DEPTH = 5;
describe("Binary AccQueue", () => {
const HASH_LENGTH = 2;
describe("merge()", () => {
it("should produce the correct main root", () => {
testMerge(SUB_DEPTH, HASH_LENGTH, ZERO, NUM_SUBTREES, MAIN_DEPTH);
});
});
describe("mergeSubRoots()", () => {
it("should work progressively", () => {
testMergeShortest(SUB_DEPTH, HASH_LENGTH, ZERO, NUM_SUBTREES);
});
it("should fail if there are 0 leaves", () => {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
expect(() => {
aq.mergeSubRoots(0);
}).to.throw();
});
it("should a generate the same smallMainTreeRoot root from 1 subroot", () => {
testMergeShortestOne(SUB_DEPTH, HASH_LENGTH, ZERO);
});
it("Exhaustive test from 2 to 16 subtrees", () => {
const MAX = 16;
testMergeExhaustive(SUB_DEPTH, HASH_LENGTH, ZERO, MAX);
});
});
});
describe("Quinary AccQueue", () => {
const HASH_LENGTH = 5;
describe("merge()", () => {
it("should produce the correct main root", () => {
testMerge(SUB_DEPTH, HASH_LENGTH, ZERO, NUM_SUBTREES, MAIN_DEPTH);
});
});
describe("mergeSubRoots()", () => {
it("should work progressively", () => {
testMergeShortest(SUB_DEPTH, HASH_LENGTH, ZERO, NUM_SUBTREES);
});
it("should fail if there are 0 leaves", () => {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
expect(() => {
aq.mergeSubRoots(0);
}).to.throw();
});
it("should a generate the same smallMainTreeRoot root from 1 subroot", () => {
testMergeShortestOne(SUB_DEPTH, HASH_LENGTH, ZERO);
});
it("Exhaustive test from 2 to 16 subtrees", () => {
const MAX = 16;
testMergeExhaustive(SUB_DEPTH, HASH_LENGTH, ZERO, MAX);
});
});
});
});
});

View File

@@ -0,0 +1,578 @@
import { expect } from "chai";
import { G1Point, G2Point, genRandomBabyJubValue } from "../babyjub";
import { SNARK_FIELD_SIZE } from "../constants";
import {
sha256Hash,
hash2,
hash3,
hash4,
hash5,
hash13,
hashLeftRight,
hashN,
hashOne,
poseidonT3,
poseidonT4,
poseidonT5,
poseidonT6,
} from "../hashing";
import { genPubKey, genKeypair, genEcdhSharedKey, genRandomSalt, genPrivKey, packPubKey, unpackPubKey } from "../keys";
describe("Crypto", function test() {
this.timeout(100000);
describe("G1Point", () => {
it("should create a new G1Point", () => {
const g1 = new G1Point(BigInt(1), BigInt(2));
expect(g1.x).to.eq(BigInt(1));
expect(g1.y).to.eq(BigInt(2));
});
it("equals should return true for equal G1Point instances", () => {
const g1 = new G1Point(BigInt(1), BigInt(2));
const g2 = new G1Point(BigInt(1), BigInt(2));
expect(g1.equals(g2)).to.eq(true);
});
it("equals should return false for different G1Point instances", () => {
const g1 = new G1Point(BigInt(1), BigInt(2));
const g2 = new G1Point(BigInt(2), BigInt(1));
expect(g1.equals(g2)).to.eq(false);
});
it("asContractParam should return the G1Point instance as an object with x and y properties", () => {
const g1 = new G1Point(BigInt(1), BigInt(2));
const g1Obj = g1.asContractParam();
expect(g1Obj.x).to.eq("1");
expect(g1Obj.y).to.eq("2");
expect(Object.keys(g1Obj).length).to.eq(2);
expect(Object.keys(g1Obj)).to.deep.eq(["x", "y"]);
});
});
describe("G2Point", () => {
it("should create a new G2Point", () => {
const g2 = new G2Point([BigInt(1)], [BigInt(2)]);
expect(g2.x).to.deep.eq([BigInt(1)]);
expect(g2.y).to.deep.eq([BigInt(2)]);
});
it("equals should return true for equal G2Point instances", () => {
const g1 = new G2Point([BigInt(1)], [BigInt(2)]);
const g2 = new G2Point([BigInt(1)], [BigInt(2)]);
expect(g1.equals(g2)).to.eq(true);
});
it("equals should return false for different G2Point instances", () => {
const g1 = new G2Point([BigInt(1)], [BigInt(2)]);
const g2 = new G2Point([BigInt(2)], [BigInt(1)]);
expect(g1.equals(g2)).to.eq(false);
});
it("asContractParam should return the G2Point instance as an object with x and y properties", () => {
const g2 = new G2Point([BigInt(1)], [BigInt(2)]);
const g2Obj = g2.asContractParam();
expect(g2Obj.x).to.deep.eq(["1"]);
expect(g2Obj.y).to.deep.eq(["2"]);
expect(Object.keys(g2Obj).length).to.eq(2);
expect(Object.keys(g2Obj)).to.deep.eq(["x", "y"]);
});
});
describe("sha256Hash", () => {
it("should return a hash of the input", () => {
const res = sha256Hash([BigInt(1), BigInt(2)]);
expect(res).to.not.eq(BigInt(0));
});
it("should produce the same hash for the same input", () => {
const res1 = sha256Hash([BigInt(1), BigInt(2)]);
const res2 = sha256Hash([BigInt(1), BigInt(2)]);
expect(res1).to.eq(res2);
});
it("should produce different hashes for different inputs", () => {
const res1 = sha256Hash([BigInt(1), BigInt(2)]);
const res2 = sha256Hash([BigInt(2), BigInt(1)]);
expect(res1).to.not.eq(res2);
});
it("should produce an output smaller than the snark field size", () => {
const hash = sha256Hash([BigInt(1), BigInt(2)]);
expect(hash < SNARK_FIELD_SIZE).to.eq(true);
});
it("should produce the correct output", () => {
const s = sha256Hash([BigInt(0), BigInt(1)]);
expect(s.toString()).to.eq("21788914573420223731318033363701224062123674814818143146813863227479480390499");
});
});
describe("poseidon", () => {
describe("poseidonT3", () => {
it("should produce the same output for the same input", () => {
const res1 = poseidonT3([BigInt(1), BigInt(2)]);
const res2 = poseidonT3([BigInt(1), BigInt(2)]);
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = poseidonT3([BigInt(1), BigInt(2)]);
const res2 = poseidonT3([BigInt(2), BigInt(1)]);
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = poseidonT3([BigInt(1), BigInt(2)]);
expect(hash).to.not.eq(BigInt(0));
});
it("should only accept two inputs", () => {
expect(() => poseidonT3([BigInt(1), BigInt(2), BigInt(3)])).to.throw();
});
});
describe("poseidonT4", () => {
it("should produce the same output for the same input", () => {
const res1 = poseidonT4([BigInt(1), BigInt(2), BigInt(3)]);
const res2 = poseidonT4([BigInt(1), BigInt(2), BigInt(3)]);
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = poseidonT4([BigInt(1), BigInt(2), BigInt(3)]);
const res2 = poseidonT4([BigInt(2), BigInt(1), BigInt(3)]);
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = poseidonT4([BigInt(1), BigInt(2), BigInt(3)]);
expect(hash).to.not.eq(BigInt(0));
});
it("should only accept three inputs", () => {
expect(() => poseidonT4([BigInt(1), BigInt(2)])).to.throw();
expect(() => poseidonT4([BigInt(1), BigInt(2), BigInt(3), BigInt(4)])).to.throw();
});
});
describe("poseidonT5", () => {
it("should produce the same output for the same input", () => {
const res1 = poseidonT5([BigInt(1), BigInt(2), BigInt(3), BigInt(4)]);
const res2 = poseidonT5([BigInt(1), BigInt(2), BigInt(3), BigInt(4)]);
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = poseidonT5([BigInt(1), BigInt(2), BigInt(3), BigInt(4)]);
const res2 = poseidonT5([BigInt(2), BigInt(1), BigInt(3), BigInt(4)]);
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = poseidonT5([BigInt(1), BigInt(2), BigInt(3), BigInt(4)]);
expect(hash).to.not.eq(BigInt(0));
});
it("should only accept four inputs", () => {
expect(() => poseidonT5([BigInt(1), BigInt(2)])).to.throw();
expect(() => poseidonT5([BigInt(1), BigInt(2), BigInt(3)])).to.throw();
expect(() => poseidonT5([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)])).to.throw();
});
});
describe("poseidonT6", () => {
it("should produce the same output for the same input", () => {
const res1 = poseidonT6([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
const res2 = poseidonT6([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = poseidonT6([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
const res2 = poseidonT6([BigInt(2), BigInt(1), BigInt(3), BigInt(4), BigInt(5)]);
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = poseidonT6([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
expect(hash).to.not.eq(BigInt(0));
});
it("should only accept five inputs", () => {
expect(() => poseidonT6([BigInt(1), BigInt(2)])).to.throw();
expect(() => poseidonT6([BigInt(1), BigInt(2), BigInt(3)])).to.throw();
expect(() => poseidonT6([BigInt(1), BigInt(2), BigInt(3), BigInt(4)])).to.throw();
expect(() => poseidonT6([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5), BigInt(6)])).to.throw();
});
});
describe("hashLeftRight", () => {
it("should produce the same output for the same input", () => {
const res1 = hashLeftRight(BigInt(1), BigInt(2));
const res2 = hashLeftRight(BigInt(1), BigInt(2));
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = hashLeftRight(BigInt(1), BigInt(2));
const res2 = hashLeftRight(BigInt(2), BigInt(1));
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = hashLeftRight(BigInt(1), BigInt(2));
expect(hash).to.not.eq(BigInt(0));
});
});
describe("hashN", () => {
it("should produce the same output for the same input", () => {
const res1 = hashN(5, [BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
const res2 = hashN(5, [BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = hashN(5, [BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
const res2 = hashN(5, [BigInt(2), BigInt(1), BigInt(3), BigInt(4), BigInt(5)]);
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = hashN(5, [BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
expect(hash).to.not.eq(BigInt(0));
});
it("should throw when elements is more than numElement", () => {
expect(() => hashN(5, [BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5), BigInt(6)])).to.throw(
"the length of the elements array should be at most 5; got 6",
);
});
it("should work (and apply padding) when passed less than numElement elements", () => {
const hash = hashN(5, [BigInt(1), BigInt(2), BigInt(3), BigInt(4)]);
expect(hash).to.not.eq(BigInt(0));
});
});
describe("hash2", () => {
it("should produce the same output for the same input", () => {
const res1 = hash2([BigInt(1), BigInt(2)]);
const res2 = hash2([BigInt(1), BigInt(2)]);
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = hash2([BigInt(1), BigInt(2)]);
const res2 = hash2([BigInt(2), BigInt(1)]);
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = hash2([BigInt(1), BigInt(2)]);
expect(hash).to.not.eq(BigInt(0));
});
it("should throw when elements is more than numElement", () => {
expect(() => hash2([BigInt(1), BigInt(2), BigInt(3)])).to.throw(
"the length of the elements array should be at most 2; got 3",
);
});
it("should work (and apply padding) when passed less than numElement elements", () => {
const hash = hash2([BigInt(1)]);
expect(hash).to.not.eq(BigInt(0));
});
});
describe("hash3", () => {
it("should produce the same output for the same input", () => {
const res1 = hash3([BigInt(1), BigInt(2), BigInt(3)]);
const res2 = hash3([BigInt(1), BigInt(2), BigInt(3)]);
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = hash3([BigInt(1), BigInt(2), BigInt(3)]);
const res2 = hash3([BigInt(2), BigInt(1), BigInt(3)]);
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = hash3([BigInt(1), BigInt(2), BigInt(3)]);
expect(hash).to.not.eq(BigInt(0));
});
it("should throw when elements is more than numElement", () => {
expect(() => hash3([BigInt(1), BigInt(2), BigInt(3), BigInt(4)])).to.throw(
"the length of the elements array should be at most 3; got 4",
);
});
it("should work (and apply padding) when passed less than numElement elements", () => {
const hash = hash3([BigInt(1), BigInt(2)]);
expect(hash).to.not.eq(BigInt(0));
});
});
describe("hash4", () => {
it("should produce the same output for the same input", () => {
const res1 = hash4([BigInt(1), BigInt(2), BigInt(3), BigInt(4)]);
const res2 = hash4([BigInt(1), BigInt(2), BigInt(3), BigInt(4)]);
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = hash4([BigInt(1), BigInt(2), BigInt(3), BigInt(4)]);
const res2 = hash4([BigInt(2), BigInt(1), BigInt(3), BigInt(4)]);
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = hash4([BigInt(1), BigInt(2), BigInt(3), BigInt(4)]);
expect(hash).to.not.eq(BigInt(0));
});
it("should throw when elements is more than numElement", () => {
expect(() => hash4([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)])).to.throw(
"the length of the elements array should be at most 4; got 5",
);
});
it("should work (and apply padding) when passed less than numElement elements", () => {
const hash = hash4([BigInt(1), BigInt(2)]);
expect(hash).to.not.eq(BigInt(0));
});
});
describe("hash5", () => {
it("should produce the same output for the same input", () => {
const res1 = hash5([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
const res2 = hash5([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = hash5([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
const res2 = hash5([BigInt(2), BigInt(1), BigInt(3), BigInt(4), BigInt(5)]);
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = hash5([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)]);
expect(hash).to.not.eq(BigInt(0));
});
it("should throw when elements is more than numElement", () => {
expect(() => hash5([BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5), BigInt(6)])).to.throw(
"the length of the elements array should be at most 5; got 6",
);
});
it("should work (and apply padding) when passed less than numElement elements", () => {
const hash = hash5([BigInt(1), BigInt(2)]);
expect(hash).to.not.eq(BigInt(0));
});
});
describe("hash13", () => {
it("should produce the same output for the same input", () => {
const res1 = hash13([
BigInt(1),
BigInt(2),
BigInt(3),
BigInt(4),
BigInt(5),
BigInt(6),
BigInt(7),
BigInt(8),
BigInt(9),
BigInt(10),
BigInt(11),
BigInt(12),
BigInt(13),
]);
const res2 = hash13([
BigInt(1),
BigInt(2),
BigInt(3),
BigInt(4),
BigInt(5),
BigInt(6),
BigInt(7),
BigInt(8),
BigInt(9),
BigInt(10),
BigInt(11),
BigInt(12),
BigInt(13),
]);
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = hash13([
BigInt(1),
BigInt(2),
BigInt(3),
BigInt(4),
BigInt(5),
BigInt(6),
BigInt(7),
BigInt(8),
BigInt(9),
BigInt(10),
BigInt(11),
BigInt(12),
BigInt(13),
]);
const res2 = hash13([
BigInt(2),
BigInt(1),
BigInt(3),
BigInt(4),
BigInt(5),
BigInt(6),
BigInt(7),
BigInt(8),
BigInt(9),
BigInt(10),
BigInt(11),
BigInt(12),
BigInt(13),
]);
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = hash13([
BigInt(1),
BigInt(2),
BigInt(3),
BigInt(4),
BigInt(5),
BigInt(6),
BigInt(7),
BigInt(8),
BigInt(9),
BigInt(10),
BigInt(11),
BigInt(12),
BigInt(13),
]);
expect(hash).to.not.eq(BigInt(0));
});
it("should throw when elements is more than numElement", () => {
expect(() =>
hash13([
BigInt(1),
BigInt(2),
BigInt(3),
BigInt(4),
BigInt(5),
BigInt(6),
BigInt(7),
BigInt(8),
BigInt(9),
BigInt(10),
BigInt(11),
BigInt(12),
BigInt(13),
BigInt(14),
]),
).to.throw("the length of the elements array should be at most 13; got 14");
});
it("should work (and apply padding) when passed less than numElement elements", () => {
const hash = hash13([BigInt(1), BigInt(2)]);
expect(hash).to.not.eq(BigInt(0));
});
});
describe("hashOne", () => {
it("should produce the same output for the same input", () => {
const res1 = hashOne(BigInt(1));
const res2 = hashOne(BigInt(1));
expect(res1).to.eq(res2);
});
it("should produce different outputs for different inputs", () => {
const res1 = hashOne(BigInt(1));
const res2 = hashOne(BigInt(2));
expect(res1).to.not.eq(res2);
});
it("should produce a non zero value", () => {
const hash = hashOne(BigInt(1));
expect(hash).to.not.eq(BigInt(0));
});
});
});
describe("utils", () => {
describe("genRandomSalt", () => {
it("should produce a random salt", () => {
const salt1 = genRandomSalt();
const salt2 = genRandomSalt();
expect(salt1).to.not.eq(salt2);
});
it("should produce a salt smaller than the snark field size", () => {
const salt = genRandomSalt();
expect(salt < SNARK_FIELD_SIZE).to.eq(true);
});
it("should produce a non zero value", () => {
const salt = genRandomSalt();
expect(salt).to.not.eq(BigInt(0));
});
});
});
describe("babyjub", () => {
describe("genRandomBabyJubValue", () => {
it("should generate a value what is < SNARK_FIELD_SIZE", () => {
const p = genRandomBabyJubValue();
expect(p < SNARK_FIELD_SIZE).to.eq(true);
});
it("should generate a random value", () => {
const p1 = genRandomBabyJubValue();
const p2 = genRandomBabyJubValue();
expect(p1).to.not.eq(p2);
});
it("should generate a non zero value", () => {
const p = genRandomBabyJubValue();
expect(p).to.not.eq(BigInt(0));
});
});
describe("genPrivKey", () => {
it("should generate a random private key", () => {
const sk1 = genPrivKey();
const sk2 = genPrivKey();
expect(sk1).to.not.eq(sk2);
});
it("should generate a non zero private key", () => {
const sk = genPrivKey();
expect(sk).to.not.eq(BigInt(0));
});
});
describe("genRandomSalt", () => {
it("should generate a salt that is < SNARK_FIELD_SIZE", () => {
const salt = genRandomSalt();
expect(salt < SNARK_FIELD_SIZE).to.eq(true);
});
it("should generate a random salt", () => {
const salt1 = genRandomSalt();
const salt2 = genRandomSalt();
expect(salt1).to.not.eq(salt2);
});
it("should generate a non zero salt", () => {
const salt = genRandomSalt();
expect(salt).to.not.eq(BigInt(0));
});
});
describe("packPubKey", () => {
it("should pack a public key into a bigint", () => {
const pk = genPubKey(genPrivKey());
const pkBuff = packPubKey(pk);
expect(typeof pkBuff).to.eq("bigint");
});
});
describe("unpackPubKey", () => {
it("should unpack a Buffer into a public key", () => {
const pk = genPubKey(genPrivKey());
const pkBuff = packPubKey(pk);
const pkUnpacked = unpackPubKey(pkBuff);
expect(pkUnpacked).to.deep.eq(pk);
});
it("should produce a result which is < SNARK_FIELD_SIZE", () => {
const pk = genPubKey(genPrivKey());
const pkBuff = packPubKey(pk);
const pkUnpacked = unpackPubKey(pkBuff);
expect(pkUnpacked[0] < SNARK_FIELD_SIZE).to.eq(true);
expect(pkUnpacked[1] < SNARK_FIELD_SIZE).to.eq(true);
});
});
describe("genPubKey", () => {
it("should produce a public key which is < SNARK_FIELD_SIZE", () => {
const pk = genPubKey(genPrivKey());
expect(pk[0] < SNARK_FIELD_SIZE).to.eq(true);
expect(pk[1] < SNARK_FIELD_SIZE).to.eq(true);
});
});
describe("genKeypair", () => {
it("should produce a public key which is < SNARK_FIELD_SIZE", () => {
const { pubKey } = genKeypair();
expect(pubKey[0] < SNARK_FIELD_SIZE).to.eq(true);
expect(pubKey[1] < SNARK_FIELD_SIZE).to.eq(true);
});
});
describe("genEcdhSharedKey", () => {
it("should produce a shared key which is < SNARK_FIELD_SIZE", () => {
const { privKey, pubKey } = genKeypair();
const sharedKey = genEcdhSharedKey(privKey, pubKey);
expect(sharedKey[0] < SNARK_FIELD_SIZE).to.eq(true);
expect(sharedKey[1] < SNARK_FIELD_SIZE).to.eq(true);
});
it("should generate a key which is different than the both privKey and pubKey", () => {
const { privKey, pubKey } = genKeypair();
const sharedKey = genEcdhSharedKey(privKey, pubKey);
expect(sharedKey[0]).to.not.eq(privKey);
expect(sharedKey[1]).to.not.eq(privKey);
expect(sharedKey[0]).to.not.eq(pubKey[0]);
expect(sharedKey[1]).to.not.eq(pubKey[1]);
});
it("should generate non zero points", () => {
const { privKey, pubKey } = genKeypair();
const sharedKey = genEcdhSharedKey(privKey, pubKey);
expect(sharedKey[0]).to.not.eq(BigInt(0));
expect(sharedKey[1]).to.not.eq(BigInt(0));
});
it("should produce consistent results", () => {
const { privKey: privKey1, pubKey: pubKey1 } = genKeypair();
const { privKey: privKey2, pubKey: pubKey2 } = genKeypair();
const sharedKey1 = genEcdhSharedKey(privKey1, pubKey2);
const sharedKey2 = genEcdhSharedKey(privKey2, pubKey1);
expect(sharedKey1[0]).to.eq(sharedKey2[0]);
expect(sharedKey1[1]).to.eq(sharedKey2[1]);
});
});
});
});

View File

@@ -0,0 +1,167 @@
import { expect } from "chai";
import { hash5 } from "../hashing";
import { IncrementalQuinTree } from "../quinTree";
describe("IMT comparison", () => {
describe("constructor", () => {
it("should calculate initial root and zero values", () => {
const mt1 = new IncrementalQuinTree(5, 0n, 5, hash5);
expect(mt1.root).to.not.eq(null);
expect(mt1.zeros.length).to.be.gt(0);
});
});
describe("insert", () => {
it("should update the root after one insertion", () => {
const mt1 = new IncrementalQuinTree(5, 0n, 5, hash5);
const rootBefore = mt1.root;
mt1.insert(1n);
expect(mt1.root).to.not.eq(rootBefore);
});
});
describe("genProof", () => {
it("should generate a proof", () => {
const mt1 = new IncrementalQuinTree(5, 0n, 5, hash5);
mt1.insert(1n);
const proof1 = mt1.genProof(0);
expect(proof1.leaf).to.eq(mt1.getNode(0));
expect(proof1.pathElements.length).to.be.gt(0);
expect(proof1.pathIndices.length).to.be.gt(0);
expect(proof1.root).to.eq(mt1.root);
});
it("should throw when trying to generate a proof for an index < 0", () => {
const mt1 = new IncrementalQuinTree(5, 0n, 5, hash5);
expect(() => mt1.genProof(-1)).to.throw("The leaf index must be greater or equal to 0");
});
it("should throw when trying to generate a proof for an index > tree size", () => {
const mt1 = new IncrementalQuinTree(5, 0n, 5, hash5);
const capacity = 5 ** 5;
expect(() => mt1.genProof(capacity + 1)).to.throw("The leaf index must be less than the tree capacity");
});
});
describe("genSubrootProof", () => {
it("should generate a valid proof for a subtree", () => {
const mt1 = new IncrementalQuinTree(5, 0n, 5, hash5);
for (let i = 0; i < 100; i += 1) {
mt1.insert(BigInt(i));
}
const proof1 = mt1.genSubrootProof(5, 10);
expect(mt1.verifyProof(proof1)).to.eq(true);
});
it("should throw when trying to generate a subroot proof and providing an end index > start index", () => {
const mt1 = new IncrementalQuinTree(5, 0n, 5, hash5);
expect(() => mt1.genSubrootProof(5, 4)).to.throw("The start index must be less than the end index");
});
it("should throw when providing a start index < 0", () => {
const mt1 = new IncrementalQuinTree(5, 0n, 5, hash5);
expect(() => mt1.genSubrootProof(-1, 5)).to.throw("The start index must be greater or equal to 0");
});
it("should throw when providing a leaves range not multiple of arity", () => {
const arity = 5;
const mt1 = new IncrementalQuinTree(arity, 0n, 5, hash5);
expect(() => mt1.genSubrootProof(0, arity + 1)).to.throw(
"The number of leaves must be a multiple of the tree arity",
);
});
it("should throw when the number of leaves is larger than the capacity", () => {
const arity = 5;
const mt1 = new IncrementalQuinTree(arity, 0n, 5, hash5);
expect(() => mt1.genSubrootProof(0, arity ** 5)).to.throw(
"The number of leaves must be less than the tree capacity",
);
});
});
describe("verifyProof", () => {
it("should validate a proof", () => {
const mt1 = new IncrementalQuinTree(5, 0n, 5, hash5);
mt1.insert(1n);
const proof1 = mt1.genProof(0);
expect(mt1.verifyProof(proof1)).to.eq(true);
});
});
describe("calcParentIndices", () => {
it("should throw when the index is out of bounds", () => {
const arity = 5;
const mt1 = new IncrementalQuinTree(arity, 0n, 5, hash5);
expect(() => mt1.calcParentIndices(arity ** 5)).to.throw(
`Index ${arity ** 5} is out of bounds. Can only get parents of leaves`,
);
});
});
describe("calcChildIndices", () => {
it("should throw when the index is out of bounds", () => {
const arity = 5;
const mt1 = new IncrementalQuinTree(arity, 0n, 5, hash5);
const index = 2;
expect(() => mt1.calcChildIndices(index)).to.throw(
`Index ${index} is out of bounds. Can only get children of subroots`,
);
});
});
describe("setNode", () => {
it("should throw when trying to set the root directly", () => {
const arity = 5;
const mt1 = new IncrementalQuinTree(arity, 0n, 5, hash5);
expect(() => {
mt1.setNode(mt1.numNodes, 1n);
}).to.throw("Index out of bounds");
});
it("should throw when the index is out of bounds", () => {
const arity = 5;
const mt1 = new IncrementalQuinTree(arity, 0n, 5, hash5);
expect(() => {
mt1.setNode(mt1.numNodes + 5, 1n);
}).to.throw("Index out of bounds");
});
});
describe("copy", () => {
it("should produce a copy of the tree", () => {
const arity = 5;
const mt1 = new IncrementalQuinTree(arity, 0n, 5, hash5);
const mt2 = mt1.copy();
expect(mt2.root).to.eq(mt1.root);
expect(mt2.zeros).to.deep.eq(mt1.zeros);
expect(mt2.numNodes).to.eq(mt1.numNodes);
expect(mt2.arity).to.eq(mt1.arity);
});
});
});

View File

@@ -0,0 +1,241 @@
import { expect } from "chai";
import { bigInt2Buffer, fromRprLE, fromString, shiftRight, stringifyBigInts, unstringifyBigInts } from "../bigIntUtils";
import { SNARK_FIELD_SIZE } from "../constants";
import { genTreeCommitment, genTreeProof } from "../utils";
describe("Utils", () => {
describe("stringifyBigInts", () => {
it("should work on a BigInt input", () => {
expect(stringifyBigInts(BigInt(1))).to.eq("1");
});
it("should work on a BigInt[] input", () => {
expect(stringifyBigInts([BigInt(1), BigInt(2)])).to.deep.eq(["1", "2"]);
});
it("should work on a BigInt[][] input", () => {
expect(
stringifyBigInts([
[BigInt(1), BigInt(2)],
[BigInt(3), BigInt(4)],
]),
).to.deep.eq([
["1", "2"],
["3", "4"],
]);
});
it("should work on a BigInt[][][] input", () => {
expect(
stringifyBigInts([
[
[BigInt(1), BigInt(2)],
[BigInt(3), BigInt(4)],
],
[
[BigInt(5), BigInt(6)],
[BigInt(7), BigInt(8)],
],
]),
).to.deep.eq([
[
["1", "2"],
["3", "4"],
],
[
["5", "6"],
["7", "8"],
],
]);
});
it("should work on a { [key: string]: BigInt } input", () => {
expect(stringifyBigInts({ a: BigInt(1), b: BigInt(2) })).to.deep.eq({ a: "1", b: "2" });
});
it("should work on a null input", () => {
expect(stringifyBigInts(null)).to.eq(null);
});
it("should return the input if it is not a valid value", () => {
expect(stringifyBigInts("A")).to.eq("A");
});
it("should work on a Uint8Array input", () => {
const input = new Uint8Array([1, 2, 3, 4]);
expect(stringifyBigInts(input)).to.eq("67305985");
});
});
describe("unstringifyBigInts", () => {
it("should work on a string input with decimal numbers", () => {
expect(unstringifyBigInts("1")).to.eq(BigInt(1));
});
it("should work on a string input with hex number", () => {
expect(unstringifyBigInts("0xA")).to.eq(BigInt(10));
});
it("should work on a string[] input", () => {
expect(unstringifyBigInts(["1", "2"])).to.deep.eq([BigInt(1), BigInt(2)]);
});
it("should work on a string[][] input", () => {
expect(
unstringifyBigInts([
["1", "2"],
["3", "4"],
]),
).to.deep.eq([
[BigInt(1), BigInt(2)],
[BigInt(3), BigInt(4)],
]);
});
it("should work on a string[][][] input", () => {
expect(
unstringifyBigInts([
[
["1", "2"],
["3", "4"],
],
[
["5", "6"],
["7", "8"],
],
]),
).to.deep.eq([
[
[BigInt(1), BigInt(2)],
[BigInt(3), BigInt(4)],
],
[
[BigInt(5), BigInt(6)],
[BigInt(7), BigInt(8)],
],
]);
});
it("should work on a { [key: string]: string } input", () => {
expect(unstringifyBigInts({ a: "1", b: "2" })).to.deep.eq({ a: BigInt(1), b: BigInt(2) });
});
it("should work on a null input", () => {
expect(unstringifyBigInts(null)).to.eq(null);
});
it("should return the input if it is not a valid value", () => {
expect(unstringifyBigInts("A")).to.eq("A");
});
});
describe("bigInt2Buffer", () => {
it("should convert a BigInt to a Buffer", () => {
const bigInt = BigInt(123456789);
const buffer = bigInt2Buffer(bigInt);
expect(buffer).to.be.instanceOf(Buffer);
});
it("should produce a Buffer with the correct value", () => {
const bigInt = BigInt(123456789);
const buffer = bigInt2Buffer(bigInt);
let hex = bigInt.toString(16);
// Ensure even length.
if (hex.length % 2 !== 0) {
hex = `0${hex}`;
}
const expectedBuffer = Buffer.from(hex, "hex");
expect(buffer.equals(expectedBuffer)).to.eq(true);
});
it("should produce a Buffer with the correct value even if not even length", () => {
const bigInt = BigInt(15);
const buffer = bigInt2Buffer(bigInt);
const expectedBuffer = Buffer.from("0f", "hex");
expect(buffer.equals(expectedBuffer)).to.eq(true);
});
});
describe("genTreeCommitment", () => {
const leaves = [BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)];
const salt = BigInt(6);
const depth = 3;
it("should generate a commitment to the tree root using the provided salt", () => {
const commitment = genTreeCommitment(leaves, salt, depth);
expect(commitment).to.satisfy((num: bigint) => num > 0);
expect(commitment).to.satisfy((num: bigint) => num < SNARK_FIELD_SIZE);
});
it("should always generate the same commitment for the same inputs", () => {
const commitment = genTreeCommitment(leaves, salt, depth);
expect(commitment).to.satisfy((num: bigint) => num > 0);
expect(commitment).to.satisfy((num: bigint) => num < SNARK_FIELD_SIZE);
const commitment2 = genTreeCommitment(leaves, salt, depth);
expect(commitment2).to.satisfy((num: bigint) => num > 0);
expect(commitment2).to.satisfy((num: bigint) => num < SNARK_FIELD_SIZE);
expect(commitment).to.eq(commitment2);
});
});
describe("fromString", () => {
it("should convert a string with radix 10 to a bigint", () => {
expect(fromString("123456789", 10)).to.eq(BigInt(123456789));
});
it("should convert a string with radix 16 to a bigint", () => {
expect(fromString("123456789", 16)).to.eq(BigInt(0x123456789));
});
it("should convert a string with radix 16 and starting with 0x to a bigint", () => {
expect(fromString("0x123456789", 16)).to.eq(BigInt(0x123456789));
});
it("should convert a string with radix != 10 && != 16 to a bigint", () => {
expect(fromString("123456789", 2)).to.eq(BigInt(123456789));
});
});
describe("genTreeProof", () => {
it("should return the path elements for the given index", () => {
const leaves = [BigInt(1), BigInt(2), BigInt(3), BigInt(4), BigInt(5)];
const depth = 3;
const proof = genTreeProof(2, leaves, depth);
expect(proof.length).to.be.gt(0);
});
});
describe("fromRprLE", () => {
it("should correctly parse a buffer with Little Endian Representation", () => {
const { buffer } = new Uint8Array([1, 2, 3, 4]);
const view = new DataView(buffer);
const expected = fromString("04030201", 16).toString();
expect(fromRprLE(view)).to.eq(expected);
});
it("should correctly parse a buffer with Little Endian Representation with offset", () => {
const { buffer } = new Uint8Array([0, 0, 0, 0, 1, 2, 3, 4, 0, 0, 0, 0]);
const view = new DataView(buffer);
const expected = fromString("04030201", 16).toString();
expect(fromRprLE(view, 4, 4)).to.eq(expected);
});
it("should correctly parse a buffer with Little Endian Representation with byte length", () => {
const { buffer } = new Uint8Array([1, 2, 3, 4, 5, 6]);
const view = new DataView(buffer);
const expected = fromString("04030201", 16).toString();
expect(fromRprLE(view, 0, 4)).to.eq(expected);
});
});
describe("shiftRight", () => {
it("should shift a bigint to the right by n bits", () => {
expect(shiftRight(16n, 2n)).to.eq(4n);
});
});
});

View File

@@ -0,0 +1,131 @@
import { expect } from "chai";
import { AccQueue, IncrementalQuinTree, calcDepthFromNumLeaves } from "..";
/**
* Test a full merge
* @param SUB_DEPTH
* @param HASH_LENGTH
* @param ZERO
* @param NUM_SUBTREES
* @param MAIN_DEPTH
*/
export const testMerge = (
SUB_DEPTH: number,
HASH_LENGTH: number,
ZERO: bigint,
NUM_SUBTREES: number,
MAIN_DEPTH: number,
): void => {
// const hashFunc = HASH_LENGTH === 5 ? hash5 : hash2
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const aq2 = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const tree = new IncrementalQuinTree(MAIN_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
for (let i = 0; i < NUM_SUBTREES; i += 1) {
for (let j = 0; j < HASH_LENGTH ** SUB_DEPTH; j += 1) {
const leaf = BigInt(j + 1);
tree.insert(leaf);
aq.enqueue(leaf);
aq2.enqueue(leaf);
}
}
// The main root should not exist yet
expect(aq.hasRoot(MAIN_DEPTH)).to.eq(false);
expect(aq2.hasRoot(MAIN_DEPTH)).to.eq(false);
aq2.mergeSubRoots(0);
aq2.merge(MAIN_DEPTH);
// For reference only
aq.mergeDirect(MAIN_DEPTH);
// merge and mergeDirect should produce the same root
expect(aq.hasRoot(MAIN_DEPTH)).to.eq(true);
expect(aq2.hasRoot(MAIN_DEPTH)).to.eq(true);
expect(aq.getRoot(MAIN_DEPTH)!.toString()).to.eq(aq2.getRoot(MAIN_DEPTH)!.toString());
// merge and mergeDirect should produce the correct root
expect(aq.getRoot(MAIN_DEPTH)!.toString()).to.eq(tree.root.toString());
};
/**
* Test merging the shortest subtree
* @param SUB_DEPTH
* @param HASH_LENGTH
* @param ZERO
* @param NUM_SUBTREES
*/
export const testMergeShortest = (SUB_DEPTH: number, HASH_LENGTH: number, ZERO: bigint, NUM_SUBTREES: number): void => {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const aq2 = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
for (let i = 0; i < NUM_SUBTREES; i += 1) {
for (let j = 0; j < HASH_LENGTH ** SUB_DEPTH; j += 1) {
const leaf = BigInt(j + 1);
aq.enqueue(leaf);
aq2.enqueue(leaf);
}
}
// Merge all subroots in aq
aq.mergeSubRoots(0);
// Merge all but one subroot in aq2
aq2.mergeSubRoots(2);
expect(aq.getSmallSRTroot().toString()).not.to.eq(aq2.getSmallSRTroot().toString());
aq2.mergeSubRoots(2);
expect(aq.getSmallSRTroot().toString()).not.to.eq(aq2.getSmallSRTroot().toString());
// Merge the last subroot in aq2
aq2.mergeSubRoots(1);
expect(aq.getSmallSRTroot().toString()).to.eq(aq2.getSmallSRTroot().toString());
};
/**
* Insert one leaf, then run mergeSubRoots
*/
export const testMergeShortestOne = (SUB_DEPTH: number, HASH_LENGTH: number, ZERO: bigint): void => {
const leaf = BigInt(123);
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
const smallTree = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, aq.hashFunc);
aq.enqueue(leaf);
smallTree.insert(leaf);
aq.mergeSubRoots(0);
expect(aq.getSmallSRTroot().toString()).to.eq(smallTree.root.toString());
expect(aq.getSubRoot(0).toString()).to.eq(smallTree.root.toString());
};
/**
* Create a number of subtrees, and merge them all
*/
export const testMergeExhaustive = (SUB_DEPTH: number, HASH_LENGTH: number, ZERO: bigint, MAX: number): void => {
for (let numSubtrees = 2; numSubtrees <= MAX; numSubtrees += 1) {
const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO);
// Create numSubtrees subtrees
for (let i = 0; i < numSubtrees; i += 1) {
for (let j = 0; j < HASH_LENGTH ** SUB_DEPTH; j += 1) {
const leaf = BigInt(j + 1);
aq.enqueue(leaf);
}
}
// Merge subroots
aq.mergeSubRoots(0);
const depth = calcDepthFromNumLeaves(HASH_LENGTH, numSubtrees);
const smallTree = new IncrementalQuinTree(depth, aq.getZeros()[aq.getSubDepth()], HASH_LENGTH, aq.hashFunc);
aq.getSubRoots().forEach(subRoot => {
smallTree.insert(subRoot);
});
expect(aq.getSmallSRTroot().toString()).to.eq(smallTree.root.toString());
}
};

View File

@@ -0,0 +1,132 @@
import assert from "assert";
import { randomBytes } from "crypto";
import type { PrivKey } from "./types";
import { SNARK_FIELD_SIZE } from "./constants";
/**
* @notice A class representing a point on the first group (G1)
* of the Jubjub curve
*/
export class G1Point {
x: bigint;
y: bigint;
/**
* Create a new instance of G1Point
* @param x the x coordinate
* @param y the y coordinate
*/
constructor(x: bigint, y: bigint) {
assert(x < SNARK_FIELD_SIZE && x >= 0, "G1Point x out of range");
assert(y < SNARK_FIELD_SIZE && y >= 0, "G1Point y out of range");
this.x = x;
this.y = y;
}
/**
* Check whether two points are equal
* @param pt the point to compare with
* @returns whether they are equal or not
*/
equals(pt: G1Point): boolean {
return this.x === pt.x && this.y === pt.y;
}
/**
* Return the point as a contract param in the form of an object
* @returns the point as a contract param
*/
asContractParam(): { x: string; y: string } {
return {
x: this.x.toString(),
y: this.y.toString(),
};
}
}
/**
* @notice A class representing a point on the second group (G2)
* of the Jubjub curve. This is usually an extension field of the
* base field of the curve.
*/
export class G2Point {
x: bigint[];
y: bigint[];
/**
* Create a new instance of G2Point
* @param x the x coordinate
* @param y the y coordinate
*/
constructor(x: bigint[], y: bigint[]) {
this.checkPointsRange(x, "x");
this.checkPointsRange(y, "y");
this.x = x;
this.y = y;
}
/**
* Check whether two points are equal
* @param pt the point to compare with
* @returns whether they are equal or not
*/
equals(pt: G2Point): boolean {
return this.x[0] === pt.x[0] && this.x[1] === pt.x[1] && this.y[0] === pt.y[0] && this.y[1] === pt.y[1];
}
/**
* Return the point as a contract param in the form of an object
* @returns the point as a contract param
*/
asContractParam(): { x: string[]; y: string[] } {
return {
x: this.x.map(n => n.toString()),
y: this.y.map(n => n.toString()),
};
}
/**
* Check whether the points are in range
* @param x the x coordinate
* @param type the type of the coordinate
*/
private checkPointsRange(x: bigint[], type: "x" | "y") {
assert(
x.every(n => n < SNARK_FIELD_SIZE && n >= 0),
`G2Point ${type} out of range`,
);
}
}
/**
* Returns a BabyJub-compatible random value. We create it by first generating
* a random value (initially 256 bits large) modulo the snark field size as
* described in EIP197. This results in a key size of roughly 253 bits and no
* more than 254 bits. To prevent modulo bias, we then use this efficient
* algorithm:
* http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/lib/libc/crypt/arc4random_uniform.c
* @returns A BabyJub-compatible random value.
*/
export const genRandomBabyJubValue = (): bigint => {
// Prevent modulo bias
// const lim = BigInt('0x10000000000000000000000000000000000000000000000000000000000000000')
// const min = (lim - SNARK_FIELD_SIZE) % SNARK_FIELD_SIZE
const min = BigInt("6350874878119819312338956282401532410528162663560392320966563075034087161851");
let privKey: PrivKey = SNARK_FIELD_SIZE;
do {
const rand = BigInt(`0x${randomBytes(32).toString("hex")}`);
if (rand >= min) {
privKey = rand % SNARK_FIELD_SIZE;
}
} while (privKey >= SNARK_FIELD_SIZE);
return privKey;
};

View File

@@ -0,0 +1,135 @@
import type { BigIntVariants, StringifiedBigInts } from "./types";
/**
* Given an input containing string values, convert them
* to bigint
* @param input - The input to convert
* @returns the input with string values converted to bigint
*/
export const unstringifyBigInts = (input: StringifiedBigInts): BigIntVariants => {
if (typeof input === "string" && /^[0-9]+$/.test(input)) {
return BigInt(input);
}
if (typeof input === "string" && /^0x[0-9a-fA-F]+$/.test(input)) {
return BigInt(input);
}
if (Array.isArray(input)) {
return input.map(unstringifyBigInts);
}
if (input === null) {
return null;
}
if (typeof input === "object") {
return Object.entries(input).reduce<Record<string, bigint>>((acc, [key, value]) => {
acc[key] = unstringifyBigInts(value) as bigint;
return acc;
}, {});
}
return input;
};
/**
* Converts a string to a bigint using the given radix
* @param str - The string to convert
* @param radix - The radix to use
* @returns The converted string as a bigint
*/
export const fromString = (str: string, radix: number): bigint => {
if (!radix || radix === 10) {
return BigInt(str);
}
if (radix === 16) {
if (str.startsWith("0x")) {
return BigInt(str);
}
return BigInt(`0x${str}`);
}
return BigInt(str);
};
/**
* Parses a buffer with Little Endian Representation
* @param buff - The buffer to parse
* @param o - The offset to start from
* @param n8 - The byte length
* @returns The parsed buffer as a string
*/
export const fromRprLE = (buff: ArrayBufferView, o = 0, n8: number = buff.byteLength): string => {
const v = new Uint32Array(buff.buffer, buff.byteOffset + o, n8 / 4);
const a: string[] = new Array<string>(n8 / 4);
v.forEach((ch, i) => {
a[a.length - i - 1] = ch.toString(16).padStart(8, "0");
});
return fromString(a.join(""), 16).toString();
};
/**
* Given an input of bigint values, convert them to their string representations
* @param input - The input to convert
* @returns The input with bigint values converted to string
*/
export const stringifyBigInts = (input: BigIntVariants): StringifiedBigInts => {
if (typeof input === "bigint") {
return input.toString();
}
if (input instanceof Uint8Array) {
return fromRprLE(input, 0);
}
if (Array.isArray(input)) {
return input.map(stringifyBigInts);
}
if (input === null) {
return null;
}
if (typeof input === "object") {
return Object.entries(input).reduce<Record<string, StringifiedBigInts>>((acc, [key, value]) => {
acc[key] = stringifyBigInts(value);
return acc;
}, {});
}
return input;
};
/**
* Create a copy of a bigint array
* @param arr - the array of bigints to copy
* @returns a deep copy of the array
*/
export const deepCopyBigIntArray = (arr: bigint[]): bigint[] => arr.map(x => BigInt(x.toString()));
/**
* Sihft a left by n bits
* @param a - The first bigint
* @param n - The second bigint
* @returns The result of shifting a right by n
*/
export const shiftRight = (a: bigint, n: bigint): bigint =>
// eslint-disable-next-line no-bitwise
a >> n;
/**
* Convert a BigInt to a Buffer
* @param i - the bigint to convert
* @returns the buffer
*/
export const bigInt2Buffer = (i: bigint): Buffer => {
let hex = i.toString(16);
// Ensure even length.
if (hex.length % 2 !== 0) {
hex = `0${hex}`;
}
return Buffer.from(hex, "hex");
};

View File

@@ -0,0 +1,12 @@
import { r } from "@zk-kit/baby-jubjub";
import { keccak256, toUtf8Bytes } from "ethers";
import assert from "assert";
export const SNARK_FIELD_SIZE = r;
// A nothing-up-my-sleeve zero value
// Should be equal to 8370432830353022751713833565135785980866757267633941821328460903436894336785
export const NOTHING_UP_MY_SLEEVE = BigInt(keccak256(toUtf8Bytes("Maci"))) % SNARK_FIELD_SIZE;
assert(NOTHING_UP_MY_SLEEVE === BigInt("8370432830353022751713833565135785980866757267633941821328460903436894336785"));

View File

@@ -0,0 +1,160 @@
import { poseidonPerm } from "@zk-kit/poseidon-cipher";
import { solidityPackedSha256 } from "ethers";
import assert from "assert";
import type { Plaintext, PoseidonFuncs } from "./types";
import { SNARK_FIELD_SIZE } from "./constants";
/**
* Hash an array of uint256 values the same way that the EVM does.
* @param input - the array of values to hash
* @returns a EVM compatible sha256 hash
*/
export const sha256Hash = (input: bigint[]): bigint => {
const types: string[] = [];
input.forEach(() => {
types.push("uint256");
});
return (
BigInt(
solidityPackedSha256(
types,
input.map(x => x.toString()),
),
) % SNARK_FIELD_SIZE
);
};
/**
* Generate the poseidon hash of the inputs provided
* @param inputs The inputs to hash
* @returns the hash of the inputs
*/
export const poseidon = (inputs: bigint[]): bigint => poseidonPerm([BigInt(0), ...inputs.map(x => BigInt(x))])[0];
/**
* Hash up to 2 elements
* @param inputs The elements to hash
* @returns the hash of the elements
*/
export const poseidonT3 = (inputs: bigint[]): bigint => {
assert(inputs.length === 2);
return poseidon(inputs);
};
/**
* Hash up to 3 elements
* @param inputs The elements to hash
* @returns the hash of the elements
*/
export const poseidonT4 = (inputs: bigint[]): bigint => {
assert(inputs.length === 3);
return poseidon(inputs);
};
/**
* Hash up to 4 elements
* @param inputs The elements to hash
* @returns the hash of the elements
*/
export const poseidonT5 = (inputs: bigint[]): bigint => {
assert(inputs.length === 4);
return poseidon(inputs);
};
/**
* Hash up to 5 elements
* @param inputs The elements to hash
* @returns the hash of the elements
*/
export const poseidonT6 = (inputs: bigint[]): bigint => {
assert(inputs.length === 5);
return poseidon(inputs);
};
/**
* Hash two BigInts with the Poseidon hash function
* @param left The left-hand element to hash
* @param right The right-hand element to hash
* @returns The hash of the two elements
*/
export const hashLeftRight = (left: bigint, right: bigint): bigint => poseidonT3([left, right]);
// hash functions
const funcs: PoseidonFuncs = {
2: poseidonT3,
3: poseidonT4,
4: poseidonT5,
5: poseidonT6,
};
/**
* Hash up to N elements
* @param numElements The number of elements to hash
* @param elements The elements to hash
* @returns The hash of the elements
*/
export const hashN = (numElements: number, elements: Plaintext): bigint => {
const elementLength = elements.length;
if (elements.length > numElements) {
throw new TypeError(`the length of the elements array should be at most ${numElements}; got ${elements.length}`);
}
const elementsPadded = elements.slice();
if (elementLength < numElements) {
for (let i = elementLength; i < numElements; i += 1) {
elementsPadded.push(BigInt(0));
}
}
return funcs[numElements](elementsPadded);
};
// hash functions
export const hash2 = (elements: Plaintext): bigint => hashN(2, elements);
export const hash3 = (elements: Plaintext): bigint => hashN(3, elements);
export const hash4 = (elements: Plaintext): bigint => hashN(4, elements);
export const hash5 = (elements: Plaintext): bigint => hashN(5, elements);
/**
* A convenience function to use Poseidon to hash a Plaintext with
* no more than 13 elements
* @param elements The elements to hash
* @returns The hash of the elements
*/
export const hash13 = (elements: Plaintext): bigint => {
const max = 13;
const elementLength = elements.length;
if (elementLength > max) {
throw new TypeError(`the length of the elements array should be at most ${max}; got ${elements.length}`);
}
const elementsPadded = elements.slice();
if (elementLength < max) {
for (let i = elementLength; i < max; i += 1) {
elementsPadded.push(BigInt(0));
}
}
return poseidonT6([
elementsPadded[0],
poseidonT6(elementsPadded.slice(1, 6)),
poseidonT6(elementsPadded.slice(6, 11)),
elementsPadded[11],
elementsPadded[12],
]);
};
/**
* Hash a single BigInt with the Poseidon hash function
* @param preImage The element to hash
* @returns The hash of the element
*/
export const hashOne = (preImage: bigint): bigint => poseidonT3([preImage, BigInt(0)]);

View File

@@ -0,0 +1,43 @@
export { AccQueue } from "./AccQueue";
export { calcDepthFromNumLeaves, genTreeCommitment, genTreeProof } from "./utils";
export { IncrementalQuinTree } from "./quinTree";
export { bigInt2Buffer, stringifyBigInts, unstringifyBigInts, deepCopyBigIntArray } from "./bigIntUtils";
export { NOTHING_UP_MY_SLEEVE, SNARK_FIELD_SIZE } from "./constants";
export {
genPrivKey,
genRandomSalt,
formatPrivKeyForBabyJub,
genPubKey,
genKeypair,
genEcdhSharedKey,
packPubKey,
unpackPubKey,
} from "./keys";
export { G1Point, G2Point, genRandomBabyJubValue } from "./babyjub";
export { sha256Hash, hashLeftRight, hashN, hash2, hash3, hash4, hash5, hash13, hashOne } from "./hashing";
export { poseidonDecrypt, poseidonDecryptWithoutCheck, poseidonEncrypt } from "@zk-kit/poseidon-cipher";
export { verifySignature, signMessage as sign } from "@zk-kit/eddsa-poseidon";
export type {
PrivKey,
PubKey,
Point,
EcdhSharedKey,
Plaintext,
Ciphertext,
Queue,
Keypair,
Signature,
PoseidonFuncs,
Leaf,
PathElements,
} from "./types";

View File

@@ -0,0 +1,77 @@
import { mulPointEscalar } from "@zk-kit/baby-jubjub";
import { derivePublicKey, deriveSecretScalar, packPublicKey, unpackPublicKey } from "@zk-kit/eddsa-poseidon";
import { randomBytes } from "crypto";
import { genRandomBabyJubValue } from "./babyjub";
import { EcdhSharedKey, Keypair, Point, PrivKey, PubKey } from "./types";
/**
* Generate a private key
* @returns A random seed for a private key.
*/
export const genPrivKey = (): bigint => BigInt(`0x${randomBytes(32).toString("hex")}`);
/**
* Generate a random value
* @returns A BabyJub-compatible salt.
*/
export const genRandomSalt = (): bigint => genRandomBabyJubValue();
/**
* An internal function which formats a random private key to be compatible
* with the BabyJub curve. This is the format which should be passed into the
* PubKey and other circuits.
* @param privKey A private key generated using genPrivKey()
* @returns A BabyJub-compatible private key.
*/
export const formatPrivKeyForBabyJub = (privKey: PrivKey): bigint => BigInt(deriveSecretScalar(privKey));
/**
* Losslessly reduces the size of the representation of a public key
* @param pubKey The public key to pack
* @returns A packed public key
*/
export const packPubKey = (pubKey: PubKey): bigint => BigInt(packPublicKey(pubKey));
/**
* Restores the original PubKey from its packed representation
* @param packed The value to unpack
* @returns The unpacked public key
*/
export const unpackPubKey = (packed: bigint): PubKey => {
const pubKey = unpackPublicKey(packed);
return pubKey.map((x: any) => BigInt(x)) as PubKey;
};
/**
* @param privKey A private key generated using genPrivKey()
* @returns A public key associated with the private key
*/
export const genPubKey = (privKey: PrivKey): PubKey => {
const key = derivePublicKey(privKey);
return [BigInt(key[0]), BigInt(key[1])];
};
/**
* Generates a keypair.
* @returns a keypair
*/
export const genKeypair = (): Keypair => {
const privKey = genPrivKey();
const pubKey = genPubKey(privKey);
const keypair: Keypair = { privKey, pubKey };
return keypair;
};
/**
* Generates an Elliptic-Curve DiffieHellman (ECDH) shared key given a private
* key and a public key.
* @param privKey A private key generated using genPrivKey()
* @param pubKey A public key generated using genPubKey()
* @returns The ECDH shared key.
*/
export const genEcdhSharedKey = (privKey: PrivKey, pubKey: PubKey): EcdhSharedKey =>
mulPointEscalar(pubKey as Point<bigint>, formatPrivKeyForBabyJub(privKey));

View File

@@ -0,0 +1,377 @@
import type { Leaf, Node, IMerkleProof } from "./types";
/**
* An implementation of an incremental Merkle tree
* @dev adapted from https://github.com/weijiekoh/optimisedmt
*/
export class IncrementalQuinTree {
// how many levels
depth: number;
// the zero value
zeroValue: bigint;
// the number of leaves per node
arity: number;
// the hash function used in the tree
hashFunc: (leaves: Leaf[]) => bigint;
// The the smallest empty leaf index
nextIndex = 0;
// Contains the zero value per level. i.e. zeros[0] is zeroValue,
// zeros[1] is the hash of leavesPerNode zeros, and so on.
zeros: bigint[] = [];
root: bigint;
nodes: Node;
numNodes: number;
capacity: number;
/**
* Create a new instance of the MaciQuinTree
* @param depth The depth of the tree
* @param zeroValue The zero value of the tree
* @param arity The arity of the tree
* @param hashFunc The hash function of the tree
*/
constructor(depth: number, zeroValue: bigint, arity: number, hashFunc: (leaves: bigint[]) => bigint) {
this.depth = depth;
this.zeroValue = zeroValue;
this.arity = arity;
this.hashFunc = hashFunc;
// calculate the initial values
const { zeros, root } = this.calcInitialVals(this.arity, this.depth, this.zeroValue, this.hashFunc);
this.zeros = zeros;
this.root = root;
// calculate the number of nodes
this.numNodes = (this.arity ** (this.depth + 1) - 1) / (this.arity - 1);
// initialize the nodes
this.nodes = {};
// set the root node
this.nodes[this.numNodes - 1] = root;
// calculate the capacity
this.capacity = this.arity ** this.depth;
}
/**
* Insert a leaf at the next available index
* @param value The value to insert
*/
insert(value: Leaf): void {
// update the node with this leaf
this.update(this.nextIndex, value);
this.nextIndex += 1;
}
/**
* Update a leaf at a given index
* @param index The index of the leaf to update
* @param value The value to update the leaf with
*/
update(index: number, value: Leaf): void {
// Set the leaf value
this.setNode(index, value);
// Set the parent leaf value
// Get the parent indices
const parentIndices = this.calcParentIndices(index);
parentIndices.forEach(parentIndex => {
const childIndices = this.calcChildIndices(parentIndex);
const elements: Leaf[] = [];
childIndices.forEach(childIndex => {
elements.push(this.getNode(childIndex));
});
this.nodes[parentIndex] = this.hashFunc(elements);
});
this.root = this.nodes[this.numNodes - 1];
}
/**
* Calculate the indices of the leaves in the path to the root
* @param index The index of the leaf
* @returns The indices of the leaves in the path to the root
*/
calcLeafIndices(index: number): number[] {
const indices = new Array<number>(this.depth);
let r = index;
for (let i = 0; i < this.depth; i += 1) {
indices[i] = r % this.arity;
r = Math.floor(r / this.arity);
}
return indices;
}
/**
* Generate a proof for a given leaf index
* @param index The index of the leaf to generate a proof for
* @returns The proof
*/
genProof(index: number): IMerkleProof {
if (index < 0) {
throw new Error("The leaf index must be greater or equal to 0");
}
if (index >= this.capacity) {
throw new Error("+The leaf index must be less than the tree capacity");
}
const pathElements: bigint[][] = [];
const indices = this.calcLeafIndices(index);
// Calculate path elements
let leafIndex = index;
let offset = 0;
for (let i = 0; i < this.depth; i += 1) {
const elements: bigint[] = [];
const start = leafIndex - (leafIndex % this.arity) + offset;
for (let j = 0; j < this.arity; j += 1) {
if (j !== indices[i]) {
const node = this.getNode(start + j);
elements.push(node);
}
}
pathElements.push(elements);
leafIndex = Math.floor(leafIndex / this.arity);
offset += this.arity ** (this.depth - i);
}
return {
pathElements,
pathIndices: indices,
root: this.root,
leaf: this.getNode(index),
};
}
/**
* Generates a Merkle proof from a subroot to the root.
* @param startIndex The index of the first leaf
* @param endIndex The index of the last leaf
* @returns The Merkle proof
*/
genSubrootProof(
// inclusive
startIndex: number,
// exclusive
endIndex: number,
): IMerkleProof {
// The end index must be greater than the start index
if (startIndex >= endIndex) {
throw new Error("The start index must be less than the end index");
}
if (startIndex < 0) {
throw new Error("The start index must be greater or equal to 0");
}
// count the number of leaves
const numLeaves = endIndex - startIndex;
// The number of leaves must be a multiple of the tree arity
if (numLeaves % this.arity !== 0) {
throw new Error("The number of leaves must be a multiple of the tree arity");
}
// The number of leaves must be lower than the maximum tree capacity
if (numLeaves >= this.capacity) {
throw new Error("The number of leaves must be less than the tree capacity");
}
// Calculate the subdepth
let subDepth = 0;
while (numLeaves !== this.arity ** subDepth && subDepth < this.depth) {
subDepth += 1;
}
const subTree = new IncrementalQuinTree(subDepth, this.zeroValue, this.arity, this.hashFunc);
for (let i = startIndex; i < endIndex; i += 1) {
subTree.insert(this.getNode(i));
}
const fullPath = this.genProof(startIndex);
fullPath.pathIndices = fullPath.pathIndices.slice(subDepth, this.depth);
fullPath.pathElements = fullPath.pathElements.slice(subDepth, this.depth);
fullPath.leaf = subTree.root;
return fullPath;
}
/**
* Verify a proof
* @param proof The proof to verify
* @returns Whether the proof is valid
*/
verifyProof = (proof: IMerkleProof): boolean => {
const { pathElements, leaf, root, pathIndices } = proof;
// Hash the first level
const firstLevel: bigint[] = pathElements[0].map(BigInt);
firstLevel.splice(Number(pathIndices[0]), 0, leaf);
let currentLevelHash: bigint = this.hashFunc(firstLevel);
// Verify the proof
for (let i = 1; i < pathElements.length; i += 1) {
const level: bigint[] = pathElements[i].map(BigInt);
level.splice(Number(pathIndices[i]), 0, currentLevelHash);
currentLevelHash = this.hashFunc(level);
}
// the path is valid if the root matches the calculated root
return currentLevelHash === root;
};
/**
* Calculate the indices of the parent
* @param index The index of the leaf
* @returns The indices of the parent
*/
calcParentIndices(index: number): number[] {
// can only calculate the parent for leaves not subroots
if (index >= this.capacity || index < 0) {
throw new Error(`Index ${index} is out of bounds. Can only get parents of leaves`);
}
const indices = new Array<number>(this.depth);
let r = index;
let levelCapacity = 0;
for (let i = 0; i < this.depth; i += 1) {
levelCapacity += this.arity ** (this.depth - i);
r = Math.floor(r / this.arity);
indices.push(levelCapacity + r);
}
return indices;
}
/**
* Calculate the indices of the children of a node
* @param index The index of the node
* @returns The indices of the children
*/
calcChildIndices(index: number): number[] {
// cannot get the children of a leaf
if (index < this.capacity || index < 0) {
throw new Error(`Index ${index} is out of bounds. Can only get children of subroots`);
}
// find the level
let level = 0;
let r = this.arity ** level;
do {
level += 1;
r += this.arity ** level;
} while (index >= r);
const start = (index - this.arity ** level) * this.arity;
const indices = Array<number>(this.arity)
.fill(0)
.map((_, i) => start + i);
return indices;
}
/**
* Get a node at a given index
* @param index The index of the node
* @returns The node
*/
getNode(index: number): Leaf {
// if we have it, just return it
if (this.nodes[index]) {
return this.nodes[index];
}
// find the zero value at that level
// first need to find the level
let runningTotal = 0;
let level = this.depth;
while (level >= 0) {
runningTotal += this.arity ** level;
if (index < runningTotal) {
break;
}
level -= 1;
}
return this.zeros[this.depth - level];
}
/**
* Set a node (not the root)
* @param index the index of the node
* @param value the value of the node
*/
setNode(index: number, value: Leaf): void {
if (index > this.numNodes - 1 || index < 0) {
throw new Error("Index out of bounds");
}
this.nodes[index] = value;
}
/**
* Copy the tree to a new instance
* @returns The new instance
*/
copy(): IncrementalQuinTree {
const newTree = new IncrementalQuinTree(this.depth, this.zeroValue, this.arity, this.hashFunc);
newTree.nodes = this.nodes;
newTree.numNodes = this.numNodes;
newTree.zeros = this.zeros;
newTree.root = this.root;
newTree.nextIndex = this.nextIndex;
return newTree;
}
/**
* Calculate the zeroes and the root of a tree
* @param arity The arity of the tree
* @param depth The depth of the tree
* @param zeroValue The zero value of the tree
* @param hashFunc The hash function of the tree
* @returns The zeros and the root
*/
private calcInitialVals = (
arity: number,
depth: number,
zeroValue: bigint,
hashFunc: (leaves: bigint[]) => bigint,
): { zeros: bigint[]; root: bigint } => {
const zeros: bigint[] = [];
let currentLevelHash = zeroValue;
for (let i = 0; i < depth; i += 1) {
zeros.push(currentLevelHash);
const z = Array<bigint>(arity).fill(currentLevelHash);
currentLevelHash = hashFunc(z);
}
return { zeros, root: currentLevelHash };
};
}

View File

@@ -0,0 +1,95 @@
// we define a bignumber as either a bigint or a string
// which is what we use the most in MACI
export type SnarkBigNumber = bigint | string;
// a private key is a single BigNumber
export type PrivKey = SnarkBigNumber;
// a public key is a pair of BigNumbers
export type PubKey<N = bigint> = [N, N];
// a shared key is a pair of BigNumbers
export type EcdhSharedKey<N = bigint> = [N, N];
// a point is a pair of BigNumbers
export type Point<N = SnarkBigNumber> = [N, N];
// a plaintext is an array of BigNumbers
export type Plaintext<N = bigint> = N[];
// a ciphertext is an array of BigNumbers
export type Ciphertext<N = bigint> = N[];
// a merkle tree path elements
export type PathElements = bigint[][];
/**
* A acc queue
*/
export interface Queue {
levels: Map<number, Map<number, bigint>>;
indices: number[];
}
/**
* A private key and a public key
*/
export interface Keypair {
privKey: PrivKey;
pubKey: PubKey;
}
// An EdDSA signature.
// R8 is a Baby Jubjub elliptic curve point and S is an element of the finite
// field of order `l` where `l` is the large prime number dividing the order of
// Baby Jubjub: see
// https://iden3-docs.readthedocs.io/en/latest/_downloads/a04267077fb3fdbf2b608e014706e004/Ed-DSA.pdf
export interface Signature<N = SnarkBigNumber> {
R8: Point<N>;
S: N;
}
/**
* A interface for poseidon hash functions
*/
export interface PoseidonFuncs {
[key: number]: (inputs: bigint[]) => bigint;
2: (inputs: bigint[]) => bigint;
3: (inputs: bigint[]) => bigint;
4: (inputs: bigint[]) => bigint;
5: (inputs: bigint[]) => bigint;
}
// a leaf is a single BigNumber
export type Leaf = bigint;
// a node is a leaf or subroot in a quinary merkle tree
export type Node = Record<number, Leaf>;
// a merkle proof is a set of path elements, path indices, and a root
export interface IMerkleProof {
pathElements: Leaf[][];
pathIndices: number[];
root: Leaf;
leaf: Leaf;
}
export type StringifiedBigInts =
| StringifiedBigInts[]
| string
| string[]
| string[][]
| string[][][]
| { [key: string]: StringifiedBigInts }
| null;
export type BigIntVariants =
| BigIntVariants[]
| StringifiedBigInts
| bigint
| bigint[]
| bigint[][]
| bigint[][][]
| { [key: string]: BigIntVariants }
| Uint8Array
| null;

View File

@@ -0,0 +1,55 @@
import { hash5, hashLeftRight } from "./hashing";
import { IncrementalQuinTree } from "./quinTree";
/**
* Calculate the depth of a tree given the number of leaves
* @param hashLength the hashing function param length
* @param numLeaves how many leaves
* @returns the depth
*/
export const calcDepthFromNumLeaves = (hashLength: number, numLeaves: number): number => {
let depth = 1;
let max = hashLength ** depth;
while (BigInt(max) < numLeaves) {
depth += 1;
max = hashLength ** depth;
}
return depth;
};
/**
* A helper function which hashes a list of results with a salt and returns the
* hash.
* @param leaves A list of values
* @param salt A random salt
* @param depth The tree depth
* @returns The hash of the leaves and the salt, with the salt last
*/
export const genTreeCommitment = (leaves: bigint[], salt: bigint, depth: number): bigint => {
const tree = new IncrementalQuinTree(depth, 0n, 5, hash5);
leaves.forEach(leaf => {
tree.insert(leaf);
});
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, 0n, 5, hash5);
leaves.forEach(leaf => {
tree.insert(leaf);
});
const proof = tree.genProof(index);
return proof.pathElements;
};

View File

@@ -0,0 +1,119 @@
import { expect } from "chai";
import { Ballot } from "..";
describe("Ballot", () => {
describe("constructor", () => {
it("should create an empty ballot", () => {
const b = new Ballot(0, 2);
expect(b.votes.length).to.eq(0);
});
it("should create a ballot with 1 vote", () => {
const b = new Ballot(1, 2);
expect(b.votes.length).to.eq(1);
expect(b.votes[0]).to.eq(BigInt(0));
});
});
describe("hash", () => {
it("should produce an hash of the ballot", () => {
const b = new Ballot(0, 2);
const h = b.hash();
expect(h).to.not.eq(null);
});
});
describe("copy", () => {
it("should produce a deep copy", () => {
const b1 = Ballot.genRandomBallot(2, 2);
const b2 = b1.copy();
expect(b1.voteOptionTreeDepth).to.eq(b2.voteOptionTreeDepth);
expect(b1.nonce).to.eq(b2.nonce);
expect(b1.votes.length).to.eq(b2.votes.length);
expect(b1.votes).to.deep.eq(b2.votes);
expect(b1.equals(b2)).to.eq(true);
});
});
describe("asCircuitInputs", () => {
it("should produce an array", () => {
const len = 2;
const b1 = Ballot.genRandomBallot(len, 2);
const arr = b1.asCircuitInputs();
expect(arr).to.be.instanceOf(Array);
expect(arr.length).to.eq(len);
});
});
describe("isEqual", () => {
it("should return false for ballots that are not equal (different votes length)", () => {
const b1 = Ballot.genRandomBallot(2, 2);
const b2 = Ballot.genRandomBallot(2, 3);
expect(b1.equals(b2)).to.eq(false);
});
it("should return true for ballots that are equal", () => {
const b1 = new Ballot(0, 2);
const b2 = new Ballot(0, 2);
expect(b1.equals(b2)).to.eq(true);
});
it("should return false for ballots that are not equal (different nonce)", () => {
const b1 = Ballot.genRandomBallot(3, 2);
const b2 = Ballot.genRandomBallot(2, 2);
b2.nonce = BigInt(1);
expect(b1.equals(b2)).to.eq(false);
});
});
describe("asArray", () => {
it("should produce a valid result", () => {
const b1 = Ballot.genRandomBallot(2, 2);
b1.votes[0] = BigInt(1);
b1.votes[1] = BigInt(2);
b1.votes[2] = BigInt(3);
const arr = b1.asArray();
expect(arr[0]).to.eq(b1.nonce);
});
});
describe("genRandomBallot", () => {
it("should generate a ballot with a random nonce", () => {
const b1 = Ballot.genRandomBallot(2, 2);
const b2 = Ballot.genRandomBallot(2, 2);
expect(b1.nonce).to.not.eq(b2.nonce);
});
});
describe("genBlankBallot", () => {
it("should generate a ballot with all votes set to 0", () => {
const b1 = Ballot.genBlankBallot(2, 2);
expect(b1.votes.every(v => v === BigInt(0))).to.eq(true);
});
});
describe("serialization/deserialization", () => {
describe("toJSON", () => {
it("toJSON should produce a JSON object representing the ballot", () => {
const b1 = Ballot.genBlankBallot(2, 2);
const json = b1.toJSON();
expect(json).to.have.property("votes");
expect(json).to.have.property("nonce");
expect(json).to.have.property("voteOptionTreeDepth");
expect(json.votes.length).to.eq(b1.votes.length);
expect(json.nonce).to.eq(b1.nonce.toString());
expect(json.voteOptionTreeDepth).to.eq(b1.voteOptionTreeDepth.toString());
});
});
describe("fromJSON", () => {
it("should create a ballot from a JSON object", () => {
const b1 = Ballot.genBlankBallot(2, 2);
const json = b1.toJSON();
const b2 = Ballot.fromJSON(json);
expect(b1.equals(b2)).to.eq(true);
});
});
});
});

View File

@@ -0,0 +1,164 @@
import { expect } from "chai";
import { genRandomSalt } from "../../crypto";
import { PCommand, Keypair, TCommand } from "..";
describe("Commands", () => {
const { privKey, pubKey } = new Keypair();
const ecdhSharedKey = Keypair.genEcdhSharedKey(privKey, pubKey);
// eslint-disable-next-line no-bitwise
const random50bitBigInt = (): bigint => ((BigInt(1) << BigInt(50)) - BigInt(1)) & BigInt(genRandomSalt().toString());
describe("constructor", () => {
it("should create a PCommand", () => {
const command: PCommand = new PCommand(
random50bitBigInt(),
pubKey,
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
genRandomSalt(),
);
expect(command).to.not.eq(null);
});
it("should create a TCommand", () => {
const command: TCommand = new TCommand(random50bitBigInt(), random50bitBigInt(), random50bitBigInt());
expect(command).to.not.eq(null);
});
});
describe("signature", () => {
const command: PCommand = new PCommand(
random50bitBigInt(),
pubKey,
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
genRandomSalt(),
);
it("should produce a valid signature", () => {
const signature = command.sign(privKey);
expect(command.verifySignature(signature, pubKey)).to.eq(true);
});
});
describe("encryption", () => {
const command: PCommand = new PCommand(
random50bitBigInt(),
pubKey,
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
random50bitBigInt(),
genRandomSalt(),
);
const signature = command.sign(privKey);
describe("encrypt", () => {
it("should encrypt a command", () => {
const message = command.encrypt(signature, ecdhSharedKey);
expect(message).to.not.eq(null);
});
});
describe("decrypt", () => {
const message = command.encrypt(signature, ecdhSharedKey);
const decrypted = PCommand.decrypt(message, ecdhSharedKey);
it("should decrypt a message and keep the correct values", () => {
expect(decrypted).to.not.eq(null);
expect(decrypted.command.equals(command)).to.eq(true);
expect(decrypted.signature.R8[0].toString()).to.eq(signature.R8[0].toString());
expect(decrypted.signature.R8[1].toString()).to.eq(signature.R8[1].toString());
expect(decrypted.signature.S.toString()).to.eq(signature.S.toString());
});
it("should have a valid signature after decryption", () => {
const decryptedForce = PCommand.decrypt(message, ecdhSharedKey, true);
const isValid = decrypted.command.verifySignature(decrypted.signature, pubKey);
expect(isValid).to.eq(true);
const isValidForce = decryptedForce.command.verifySignature(decryptedForce.signature, pubKey);
expect(isValidForce).to.eq(true);
});
});
});
describe("copy", () => {
it("should produce a deep copy for PCommand", () => {
const c1: PCommand = new PCommand(BigInt(10), pubKey, BigInt(0), BigInt(9), BigInt(1), BigInt(123));
// shallow copy
const c2 = c1;
c1.nonce = BigInt(9999);
expect(c1.nonce.toString()).to.eq(c2.nonce.toString());
// deep copy
const c3 = c1.copy();
c1.nonce = BigInt(8888);
expect(c1.nonce.toString()).not.to.eq(c3.nonce.toString());
});
it("should produce a deep copy for TCommand", () => {
const c1: TCommand = new TCommand(BigInt(10), BigInt(0), BigInt(9));
// shallow copy
const c2 = c1;
c1.amount = BigInt(9999);
expect(c1.amount.toString()).to.eq(c2.amount.toString());
// deep copy
const c3 = c1.copy();
c1.amount = BigInt(8888);
expect(c1.amount.toString()).not.to.eq(c3.amount.toString());
});
});
describe("deserialization/serialization", () => {
describe("toJSON", () => {
it("should produce a JSON object with valid values", () => {
const c1: TCommand = new TCommand(BigInt(10), BigInt(0), BigInt(9));
const json = c1.toJSON();
expect(json).to.not.eq(null);
expect(json.cmdType).to.eq("2");
expect(json.stateIndex).to.eq("10");
expect(json.amount).to.eq("0");
expect(json.pollId).to.eq("9");
});
it("should produce a JSON object with valid values", () => {
const c1: PCommand = new PCommand(BigInt(10), pubKey, BigInt(0), BigInt(9), BigInt(1), BigInt(123));
const json = c1.toJSON();
expect(json).to.not.eq(null);
expect(json.stateIndex).to.eq("10");
expect(json.voteOptionIndex).to.eq("0");
expect(json.newVoteWeight).to.eq("9");
expect(json.nonce).to.eq("1");
expect(json.pollId).to.eq("123");
expect(json.cmdType).to.eq("1");
});
});
describe("fromJSON", () => {
it("should produce a TCommand from a JSON object", () => {
const c1: TCommand = new TCommand(BigInt(10), BigInt(0), BigInt(9));
const json = c1.toJSON();
const c2 = TCommand.fromJSON(json);
expect(c2.equals(c1)).to.eq(true);
});
it("should produce a PCommand from a JSON object", () => {
const c1: PCommand = new PCommand(BigInt(10), pubKey, BigInt(0), BigInt(9), BigInt(1), BigInt(123));
const json = c1.toJSON();
const c2 = PCommand.fromJSON(json);
expect(c2.equals(c1)).to.eq(true);
});
});
});
});

View File

@@ -0,0 +1,119 @@
import { expect } from "chai";
import { genKeypair, genPrivKey } from "../../crypto";
import { Keypair, PrivKey } from "..";
describe("keypair", () => {
describe("constructor", () => {
it("should generate a random keypair if not provided a private key", () => {
const k1 = new Keypair();
const k2 = new Keypair();
expect(k1.equals(k2)).to.eq(false);
expect(k1.privKey.rawPrivKey).not.to.eq(k2.privKey.rawPrivKey);
});
it("should generate the correct public key given a private key", () => {
const rawKeyPair = genKeypair();
const k = new Keypair(new PrivKey(rawKeyPair.privKey));
expect(rawKeyPair.pubKey[0]).to.eq(k.pubKey.rawPubKey[0]);
expect(rawKeyPair.pubKey[1]).to.eq(k.pubKey.rawPubKey[1]);
});
});
describe("equals", () => {
it("should return false for two completely different keypairs", () => {
const k1 = new Keypair();
const k2 = new Keypair();
expect(k1.equals(k2)).to.eq(false);
});
it("should return false for two keypairs with different private keys", () => {
const privateKey = new PrivKey(genPrivKey());
const privateKey2 = new PrivKey(genPrivKey());
const k1 = new Keypair(privateKey);
const k2 = new Keypair(privateKey2);
expect(k1.equals(k2)).to.eq(false);
});
it("should throw when the private keys are equal but the public keys are not", () => {
const privateKey = new PrivKey(genPrivKey());
const k1 = new Keypair(privateKey);
const k2 = new Keypair(privateKey);
k2.pubKey.rawPubKey[0] = BigInt(9);
expect(() => k1.equals(k2)).to.throw();
});
it("should return true for two identical keypairs", () => {
const k1 = new Keypair();
const k2 = k1.copy();
expect(k1.equals(k2)).to.eq(true);
});
});
describe("copy", () => {
it("should produce a deep copy", () => {
const k1 = new Keypair();
// shallow copy
const k2 = k1;
expect(k1.privKey.rawPrivKey.toString()).to.eq(k2.privKey.rawPrivKey.toString());
k1.privKey.rawPrivKey = BigInt(0);
expect(k1.privKey.rawPrivKey.toString()).to.eq(k2.privKey.rawPrivKey.toString());
// deep copy
const k3 = new Keypair();
const k4 = k3.copy();
expect(k3.privKey.rawPrivKey.toString()).to.eq(k4.privKey.rawPrivKey.toString());
k3.privKey.rawPrivKey = BigInt(0);
expect(k3.privKey.rawPrivKey.toString()).not.to.eq(k4.privKey.rawPrivKey.toString());
});
});
describe("genEcdhSharedKey", () => {
it("should produce a shared key", () => {
const k1 = new Keypair();
const k2 = new Keypair();
const sharedKey = Keypair.genEcdhSharedKey(k1.privKey, k2.pubKey);
expect(sharedKey).to.not.eq(null);
});
});
describe("serialization/deserialization", () => {
describe("toJSON", () => {
it("should produce a JSON object", () => {
const k1 = new Keypair();
const json = k1.toJSON();
expect(json).to.not.eq(null);
});
it("should produce a JSON object with the correct keys", () => {
const k1 = new Keypair();
const json = k1.toJSON();
expect(Object.keys(json)).to.deep.eq(["privKey", "pubKey"]);
});
it("should preserve the data correctly", () => {
const k1 = new Keypair();
const json = k1.toJSON();
expect(k1.privKey.serialize()).to.eq(json.privKey);
expect(k1.pubKey.serialize()).to.eq(json.pubKey);
});
});
describe("fromJSON", () => {
it("should produce a Keypair instance", () => {
const k1 = new Keypair();
const json = k1.toJSON();
const k2 = Keypair.fromJSON(json);
expect(k2).to.be.instanceOf(Keypair);
});
it("should preserve the data correctly", () => {
const k1 = new Keypair();
const json = k1.toJSON();
const k2 = Keypair.fromJSON(json);
expect(k1.equals(k2)).to.eq(true);
});
});
});
});

View File

@@ -0,0 +1,120 @@
import { expect } from "chai";
import { Message, Keypair } from "..";
describe("message", () => {
describe("constructor", () => {
it("should create a new message", () => {
const msg = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
expect(msg).to.not.eq(null);
});
it("should throw an error if the data length is not 10", () => {
expect(() => new Message(BigInt(0), Array<bigint>(9).fill(BigInt(0)))).to.throw();
});
});
describe("asCircuitInputs", () => {
it("should produce an array", () => {
const msg = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const arr = msg.asCircuitInputs();
expect(arr).to.be.instanceOf(Array);
expect(arr.length).to.eq(11);
expect(arr).to.deep.eq([BigInt(0), ...Array<bigint>(10).fill(BigInt(0))]);
});
});
describe("asContractParam", () => {
it("should produce an object", () => {
const msg = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const obj = msg.asContractParam();
expect(obj).to.be.instanceOf(Object);
expect(Object.keys(obj)).to.deep.eq(["msgType", "data"]);
});
it("should produce an object with the correct values", () => {
const msg = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const obj = msg.asContractParam();
expect(obj.msgType).to.eq("0");
expect(obj.data).to.deep.eq(Array<string>(10).fill("0"));
});
});
describe("hash", () => {
const keypair = new Keypair();
it("should produce a hash", () => {
const msg = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const h = msg.hash(keypair.pubKey);
expect(h).to.not.eq(null);
});
it("should produce the same hash for the same ballot", () => {
const msg1 = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const msg2 = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const h1 = msg1.hash(keypair.pubKey);
const h2 = msg2.hash(keypair.pubKey);
expect(h1).to.eq(h2);
});
});
describe("copy", () => {
it("should produce a deep copy", () => {
const msg1 = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const msg2 = msg1.copy();
expect(msg1.equals(msg2)).to.eq(true);
expect(msg1.data).to.deep.eq(msg2.data);
expect(msg1.msgType).to.eq(msg2.msgType);
});
});
describe("equals", () => {
it("should return false for messages that are not equal (different length)", () => {
const msg1 = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const msg2 = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
msg1.data[10] = BigInt(1);
expect(msg1.equals(msg2)).to.eq(false);
});
it("should return true for messages that are equal", () => {
const msg1 = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const msg2 = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
expect(msg1.equals(msg2)).to.eq(true);
});
it("should return false when the message type is not equal", () => {
const msg1 = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const msg2 = new Message(BigInt(1), Array<bigint>(10).fill(BigInt(0)));
expect(msg1.equals(msg2)).to.eq(false);
});
});
describe("serialization/deserialization", () => {
describe("toJSON", () => {
it("should produce a JSON object", () => {
const msg = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const json = msg.toJSON();
expect(json).to.not.eq(null);
});
it("should produce a JSON object with the correct keys", () => {
const msg = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const json = msg.toJSON();
expect(Object.keys(json)).to.deep.eq(["msgType", "data"]);
});
it("should preserve the data correctly", () => {
const msg = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const json = msg.toJSON();
expect(msg.msgType.toString()).to.eq(json.msgType);
expect(msg.data.map((x: bigint) => x.toString())).to.deep.eq(json.data);
});
});
describe("fromJSON", () => {
it("should produce a Message instance", () => {
const msg = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const json = msg.toJSON();
const msg2 = Message.fromJSON(json);
expect(msg2).to.be.instanceOf(Message);
});
it("should preserve the data correctly", () => {
const msg = new Message(BigInt(0), Array<bigint>(10).fill(BigInt(0)));
const json = msg.toJSON();
const msg2 = Message.fromJSON(json);
expect(msg.equals(msg2)).to.eq(true);
});
});
});
});

View File

@@ -0,0 +1,128 @@
import { expect } from "chai";
import { SNARK_FIELD_SIZE, genPrivKey } from "../../crypto";
import { Keypair, PrivKey } from "..";
describe("privateKey", function test() {
this.timeout(90000);
describe("constructor", () => {
it("should create a private key", () => {
const priv = genPrivKey();
const k = new PrivKey(priv);
expect(k).to.not.eq(null);
});
it("should create the same private key object for the same raw key", () => {
const priv = genPrivKey();
const k1 = new PrivKey(priv);
const k2 = new PrivKey(priv);
expect(k1.rawPrivKey.toString()).to.eq(k2.rawPrivKey.toString());
});
});
describe("serialization", () => {
describe("serialize", () => {
it("should serialize the private key", () => {
const priv = genPrivKey();
const k = new PrivKey(priv);
const s = k.serialize();
expect(s.startsWith("macisk.")).to.eq(true);
const d = `0x${s.slice(7)}`;
expect(priv.toString()).to.eq(BigInt(d).toString());
});
it("should always return a key with the same length", () => {
for (let i = 0; i < 100; i += 1) {
const k = new Keypair();
const s = k.privKey.serialize();
expect(s.length).to.eq(71);
}
});
});
describe("deserialize", () => {
it("should deserialize the private key", () => {
const priv = genPrivKey();
const k = new PrivKey(priv);
const s = k.serialize();
const k2 = PrivKey.deserialize(s);
expect(k.rawPrivKey.toString()).to.eq(k2.rawPrivKey.toString());
});
});
describe("isValidSerializedPrivKey", () => {
it("should return true for a valid serialized private key", () => {
const priv = genPrivKey();
const k = new PrivKey(priv);
const s = k.serialize();
expect(PrivKey.isValidSerializedPrivKey(s)).to.eq(true);
});
it("should return false for an invalid serialized private key", () => {
const s = "macisk.0x1234567890";
expect(PrivKey.isValidSerializedPrivKey(s)).to.eq(false);
});
});
describe("toJSON", () => {
it("should produce a JSON object", () => {
const priv = genPrivKey();
const k = new PrivKey(priv);
const json = k.toJSON();
expect(json).to.not.eq(null);
});
it("should produce a JSON object with the correct keys", () => {
const priv = genPrivKey();
const k = new PrivKey(priv);
const json = k.toJSON();
expect(Object.keys(json)).to.deep.eq(["privKey"]);
});
it("should preserve the data correctly", () => {
const priv = genPrivKey();
const k = new PrivKey(priv);
const json = k.toJSON();
expect(k.serialize()).to.eq(json.privKey);
});
});
describe("fromJSON", () => {
it("should produce a PrivKey instance", () => {
const priv = genPrivKey();
const k = new PrivKey(priv);
const json = k.toJSON();
const k2 = PrivKey.fromJSON(json);
expect(k2).to.be.instanceOf(PrivKey);
});
});
});
describe("copy", () => {
it("should produce a deep copy", () => {
const k = new Keypair();
const sk1 = k.privKey;
// shallow copy
const sk2 = sk1;
expect(sk1.rawPrivKey.toString()).to.eq(sk2.rawPrivKey.toString());
sk1.rawPrivKey = BigInt(0);
expect(sk1.rawPrivKey.toString()).to.eq(sk2.rawPrivKey.toString());
// deep copy
const k1 = new Keypair();
const sk3 = k1.privKey;
const sk4 = sk3.copy();
expect(sk3.rawPrivKey.toString()).to.eq(sk4.rawPrivKey.toString());
sk4.rawPrivKey = BigInt(0);
expect(sk3.rawPrivKey.toString()).not.to.eq(sk4.rawPrivKey.toString());
});
});
describe("asCircuitInputs", () => {
it("should generate a value that is < SNARK_FIELD_SIZE", () => {
const k = new Keypair();
const sk = k.privKey;
const circuitInputs = sk.asCircuitInputs();
expect(BigInt(circuitInputs) < SNARK_FIELD_SIZE).to.eq(true);
});
});
});

View File

@@ -0,0 +1,190 @@
import { expect } from "chai";
import { SNARK_FIELD_SIZE, unpackPubKey } from "../../crypto";
import { Keypair, PubKey } from "..";
describe("public key", () => {
describe("constructor", () => {
it("should create a public key", () => {
const k = new Keypair();
const pk = new PubKey(k.pubKey.rawPubKey);
expect(pk).to.not.eq(null);
});
it("should create the same public key object for the same raw key", () => {
const k = new Keypair();
const pk1 = new PubKey(k.pubKey.rawPubKey);
const pk2 = new PubKey(k.pubKey.rawPubKey);
expect(pk1.rawPubKey.toString()).to.eq(pk2.rawPubKey.toString());
});
it("should fail to create a public key if the raw key is invalid", () => {
expect(() => new PubKey([BigInt(0), BigInt(SNARK_FIELD_SIZE)])).to.throw();
expect(() => new PubKey([BigInt(SNARK_FIELD_SIZE), BigInt(0)])).to.throw();
expect(() => new PubKey([BigInt(SNARK_FIELD_SIZE), BigInt(SNARK_FIELD_SIZE)])).to.throw();
});
});
describe("copy", () => {
it("should produce a deep copy", () => {
const k = new Keypair();
const pk1 = k.pubKey;
// shallow copy
const pk2 = pk1;
expect(pk1.rawPubKey.toString()).to.eq(pk2.rawPubKey.toString());
pk1.rawPubKey = [BigInt(0), BigInt(0)];
expect(pk1.rawPubKey.toString()).to.eq(pk2.rawPubKey.toString());
// deep copy
const k1 = new Keypair();
const pk3 = k1.pubKey;
const pk4 = pk3.copy();
expect(pk3.rawPubKey.toString()).to.eq(pk4.rawPubKey.toString());
pk4.rawPubKey = [BigInt(0), BigInt(0)];
expect(pk3.rawPubKey.toString()).not.to.eq(pk4.rawPubKey.toString());
});
});
describe("serialization", () => {
describe("serialize", () => {
it("should serialize into a string", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const s = pk1.serialize();
expect(s.startsWith("macipk.")).to.eq(true);
const unpacked = unpackPubKey(BigInt(`0x${s.slice(7).toString()}`));
expect(unpacked[0].toString()).to.eq(pk1.rawPubKey[0].toString());
expect(unpacked[1].toString()).to.eq(pk1.rawPubKey[1].toString());
});
it("should serialize into an invalid serialized key when the key is [bigint(0), bigint(0)]", () => {
const pk1 = new PubKey([BigInt(0), BigInt(0)]);
const s = pk1.serialize();
expect(s).to.eq("macipk.z");
});
});
describe("deserialize", () => {
it("should deserialize the public key", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const s = pk1.serialize();
const pk2 = PubKey.deserialize(s);
expect(pk1.rawPubKey.toString()).to.eq(pk2.rawPubKey.toString());
});
it("should deserialize into an invalid serialized key when the key is equal to macipk.z", () => {
const pk1 = PubKey.deserialize("macipk.z");
expect(pk1.rawPubKey.toString()).to.eq([BigInt(0), BigInt(0)].toString());
});
});
describe("isValidSerializedPubKey", () => {
const k = new Keypair();
const s = k.pubKey.serialize();
it("should return true for keys that are serialized in the correct format", () => {
expect(PubKey.isValidSerializedPubKey(s)).to.eq(true);
});
it("should return false for keys that are not serialized in the correct format", () => {
expect(PubKey.isValidSerializedPubKey(`${s}ffffffffffffffffffffffffffffff`)).to.eq(false);
expect(PubKey.isValidSerializedPubKey(s.slice(1))).to.eq(false);
});
});
describe("toJSON", () => {
it("should produce a JSON object", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const json = pk1.toJSON();
expect(json).to.not.eq(null);
});
it("should produce a JSON object with the correct keys", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const json = pk1.toJSON();
expect(Object.keys(json)).to.deep.eq(["pubKey"]);
});
it("should preserve the data correctly", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const json = pk1.toJSON();
expect(pk1.serialize()).to.eq(json.pubKey);
});
});
describe("fromJSON", () => {
it("should produce a public key", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const json = pk1.toJSON();
const pk2 = PubKey.fromJSON(json);
expect(pk2).to.not.eq(null);
});
it("should produce the same public key object for the same raw key", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const json = pk1.toJSON();
const pk2 = PubKey.fromJSON(json);
expect(pk1.rawPubKey.toString()).to.eq(pk2.rawPubKey.toString());
});
});
});
describe("asContractParam", () => {
it("should produce an object with the correct values", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const obj = pk1.asContractParam();
expect(obj.x).to.eq(pk1.rawPubKey[0].toString());
expect(obj.y).to.eq(pk1.rawPubKey[1].toString());
});
});
describe("asCircuitInputs", () => {
it("should produce an array with the two points of the public key", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const arr = pk1.asCircuitInputs();
expect(arr).to.be.instanceOf(Array);
expect(arr.length).to.eq(2);
});
});
describe("asArray", () => {
it("should produce an array with the two points of the public key", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const arr = pk1.asArray();
expect(arr).to.be.instanceOf(Array);
expect(arr.length).to.eq(2);
expect(arr).to.deep.eq(pk1.rawPubKey);
});
});
describe("hash", () => {
it("should produce a hash", () => {
const k = new Keypair();
const pk1 = k.pubKey;
const h = pk1.hash();
expect(h).to.not.eq(null);
});
});
describe("equals", () => {
it("should return false for public keys that are not equal", () => {
const k1 = new Keypair();
const pk1 = k1.pubKey;
const k2 = new Keypair();
const pk2 = k2.pubKey;
expect(pk1.equals(pk2)).to.eq(false);
});
it("should return true for public keys that are equal", () => {
const k1 = new Keypair();
const pk1 = k1.pubKey;
const pk2 = pk1.copy();
expect(pk1.equals(pk2)).to.eq(true);
});
});
});

View File

@@ -0,0 +1,140 @@
import { expect } from "chai";
import { Keypair, StateLeaf } from "..";
describe("stateLeaf", () => {
const { pubKey } = new Keypair();
describe("constructor", () => {
it("should create a state leaf", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
expect(stateLeaf).to.not.eq(null);
expect(stateLeaf.pubKey.equals(pubKey)).to.eq(true);
expect(stateLeaf.voiceCreditBalance).to.eq(BigInt(123));
expect(stateLeaf.timestamp).to.eq(BigInt(1231267));
});
});
describe("copy", () => {
it("should create an exact copy of the state leaf", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
const copy = stateLeaf.copy();
expect(stateLeaf.equals(copy)).to.eq(true);
});
});
describe("genBlankLeaf", () => {
it("should return a blank leaf", () => {
const blankLeaf = StateLeaf.genBlankLeaf();
expect(blankLeaf.pubKey.rawPubKey[0]).to.eq(
BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"),
);
expect(blankLeaf.pubKey.rawPubKey[1]).to.eq(
BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"),
);
expect(blankLeaf.voiceCreditBalance).to.eq(BigInt(0));
expect(blankLeaf.timestamp).to.eq(BigInt(0));
});
});
describe("genRandomLeaf", () => {
it("should return a random leaf", () => {
const randomLeaf = StateLeaf.genRandomLeaf();
const randomLeaf2 = StateLeaf.genRandomLeaf();
expect(randomLeaf.equals(randomLeaf2)).to.eq(false);
});
});
describe("equals", () => {
it("should return true when comparing two equal state leaves", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
const stateLeaf2 = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
expect(stateLeaf.equals(stateLeaf2)).to.eq(true);
});
it("should return false when comparing two different state leaves", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
const stateLeaf2 = new StateLeaf(pubKey, BigInt(123), BigInt(1231268));
expect(stateLeaf.equals(stateLeaf2)).to.eq(false);
});
});
describe("serialization", () => {
describe("serialize", () => {
it("should work correctly", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
const serialized = stateLeaf.serialize();
expect(serialized).to.not.eq(null);
});
});
describe("deserialize", () => {
it("should work correctly", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
const serialized = stateLeaf.serialize();
const deserialized = StateLeaf.deserialize(serialized);
expect(deserialized.equals(stateLeaf)).to.eq(true);
});
});
describe("toJSON", () => {
it("should produce an object with the correct properties", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
const json = stateLeaf.toJSON();
expect(json).to.not.eq(null);
expect(Object.keys(json)).to.deep.eq(["pubKey", "voiceCreditBalance", "timestamp"]);
});
});
describe("fromJSON", () => {
it("should produce a state leaf from a JSON object", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
const json = stateLeaf.toJSON();
const deserialized = StateLeaf.fromJSON(json);
expect(deserialized.equals(stateLeaf)).to.eq(true);
});
});
});
describe("asCircuitInputs", () => {
it("should return an array", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
const arr = stateLeaf.asCircuitInputs();
expect(arr).to.be.instanceOf(Array);
expect(arr.length).to.eq(4);
});
});
describe("asContractParam", () => {
it("should return an object with the correct properties and values", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
const obj = stateLeaf.asContractParam();
expect(obj).to.not.eq(null);
expect(Object.keys(obj)).to.deep.eq(["pubKey", "voiceCreditBalance", "timestamp"]);
expect(obj.pubKey).to.deep.eq(pubKey.asContractParam());
expect(obj.voiceCreditBalance).to.eq("123");
expect(obj.timestamp).to.eq("1231267");
});
});
describe("hash", () => {
it("should hash into a single bigint value which is not null", () => {
const stateLeaf = new StateLeaf(pubKey, BigInt(123), BigInt(1231267));
const hash = stateLeaf.hash();
expect(hash).to.not.eq(null);
});
});
});

View File

@@ -0,0 +1,135 @@
import { expect } from "chai";
import { G1Point } from "../../crypto";
import fs from "fs";
import path from "path";
import { IVkObjectParams, VerifyingKey } from "..";
describe("verifyingKey", () => {
describe("fromJSON", () => {
it("should convert a JSON file from snarkjs to a VerifyingKey", () => {
const file = path.join(__dirname, "./artifacts/test_vk.json");
const j = fs.readFileSync(file).toString();
const d = JSON.parse(j) as IVkObjectParams;
const vk = VerifyingKey.fromJSON(j);
expect(d.vk_alpha_1[0]).to.eq(vk.alpha1.x.toString());
expect(d.vk_alpha_1[1]).to.eq(vk.alpha1.y.toString());
expect(d.vk_beta_2[0][0]).to.eq(vk.beta2.x[1].toString());
expect(d.vk_beta_2[0][1]).to.eq(vk.beta2.x[0].toString());
expect(d.vk_beta_2[1][0]).to.eq(vk.beta2.y[1].toString());
expect(d.vk_beta_2[1][1]).to.eq(vk.beta2.y[0].toString());
expect(d.vk_gamma_2[0][0]).to.eq(vk.gamma2.x[1].toString());
expect(d.vk_gamma_2[0][1]).to.eq(vk.gamma2.x[0].toString());
expect(d.vk_gamma_2[1][0]).to.eq(vk.gamma2.y[1].toString());
expect(d.vk_gamma_2[1][1]).to.eq(vk.gamma2.y[0].toString());
expect(d.vk_delta_2[0][0]).to.eq(vk.delta2.x[1].toString());
expect(d.vk_delta_2[0][1]).to.eq(vk.delta2.x[0].toString());
expect(d.vk_delta_2[1][0]).to.eq(vk.delta2.y[1].toString());
expect(d.vk_delta_2[1][1]).to.eq(vk.delta2.y[0].toString());
expect(d.IC.length).to.eq(vk.ic.length);
for (let i = 0; i < d.IC.length; i += 1) {
expect(d.IC[i][0]).to.eq(vk.ic[i].x.toString());
expect(d.IC[i][1]).to.eq(vk.ic[i].y.toString());
}
});
});
describe("copy", () => {
it("Copy should generate a deep copy", () => {
const file = path.join(__dirname, "./artifacts/test_vk.json");
const j = fs.readFileSync(file).toString();
const vk = VerifyingKey.fromJSON(j);
const vk2 = vk.copy();
expect(vk.equals(vk2)).to.eq(true);
});
});
describe("equals", () => {
it("should return true for equal verifying keys", () => {
const file = path.join(__dirname, "./artifacts/test_vk.json");
const j = fs.readFileSync(file).toString();
const vk = VerifyingKey.fromJSON(j);
const vk2 = vk.copy();
expect(vk.equals(vk2)).to.eq(true);
});
it("should return false for unequal verifying keys", () => {
const file = path.join(__dirname, "./artifacts/test_vk.json");
const j = fs.readFileSync(file).toString();
const vk = VerifyingKey.fromJSON(j);
const vk2 = vk.copy();
vk2.alpha1.x = BigInt(123);
expect(vk.equals(vk2)).to.eq(false);
});
it("should return false for unequal verifying keys (different ic)", () => {
const file = path.join(__dirname, "./artifacts/test_vk.json");
const j = fs.readFileSync(file).toString();
const vk = VerifyingKey.fromJSON(j);
const vk2 = vk.copy();
vk2.ic[15] = {} as unknown as G1Point;
expect(vk.equals(vk2)).to.eq(false);
});
});
describe("fromObj", () => {
it("should convert an object to a VerifyingKey", () => {
const file = path.join(__dirname, "./artifacts/test_vk.json");
const j = fs.readFileSync(file).toString();
const d = JSON.parse(j) as IVkObjectParams;
const vk = VerifyingKey.fromObj(d);
expect(d.vk_alpha_1[0]).to.eq(vk.alpha1.x.toString());
expect(d.vk_alpha_1[1]).to.eq(vk.alpha1.y.toString());
expect(d.vk_beta_2[0][0]).to.eq(vk.beta2.x[1].toString());
expect(d.vk_beta_2[0][1]).to.eq(vk.beta2.x[0].toString());
expect(d.vk_beta_2[1][0]).to.eq(vk.beta2.y[1].toString());
expect(d.vk_beta_2[1][1]).to.eq(vk.beta2.y[0].toString());
expect(d.vk_gamma_2[0][0]).to.eq(vk.gamma2.x[1].toString());
expect(d.vk_gamma_2[0][1]).to.eq(vk.gamma2.x[0].toString());
expect(d.vk_gamma_2[1][0]).to.eq(vk.gamma2.y[1].toString());
expect(d.vk_gamma_2[1][1]).to.eq(vk.gamma2.y[0].toString());
expect(d.vk_delta_2[0][0]).to.eq(vk.delta2.x[1].toString());
expect(d.vk_delta_2[0][1]).to.eq(vk.delta2.x[0].toString());
expect(d.vk_delta_2[1][0]).to.eq(vk.delta2.y[1].toString());
expect(d.vk_delta_2[1][1]).to.eq(vk.delta2.y[0].toString());
expect(d.IC.length).to.eq(vk.ic.length);
for (let i = 0; i < d.IC.length; i += 1) {
expect(d.IC[i][0]).to.eq(vk.ic[i].x.toString());
expect(d.IC[i][1]).to.eq(vk.ic[i].y.toString());
}
});
});
describe("asContractParam", () => {
it("should produce an object with the correct properties", () => {
const file = path.join(__dirname, "./artifacts/test_vk.json");
const j = fs.readFileSync(file).toString();
const vk = VerifyingKey.fromJSON(j);
const obj = vk.asContractParam();
expect(Object.keys(obj)).to.deep.eq(["alpha1", "beta2", "gamma2", "delta2", "ic"]);
});
});
describe("fromContract", () => {
it("should produce a VerifyingKey from a contract object", () => {
const file = path.join(__dirname, "./artifacts/test_vk.json");
const j = fs.readFileSync(file).toString();
const vk = VerifyingKey.fromJSON(j);
const obj = vk.asContractParam();
const vk2 = VerifyingKey.fromContract(obj);
expect(vk.equals(vk2)).to.eq(true);
});
});
});

View File

@@ -0,0 +1,131 @@
import { genRandomSalt, hash5, hashLeftRight, IncrementalQuinTree } from "../crypto";
import assert from "assert";
import type { IJsonBallot } from "./types";
/**
* A Ballot represents a User's votes in a Poll, as well as their next valid
* nonce.
*/
export class Ballot {
votes: bigint[] = [];
nonce = BigInt(0);
voteOptionTreeDepth: number;
/**
* Create a new Ballot instance
* @param _numVoteOptions How many vote options are available in the poll
* @param _voteOptionTreeDepth The depth of the merkle tree holding the vote options
*/
constructor(_numVoteOptions: number, _voteOptionTreeDepth: number) {
this.voteOptionTreeDepth = _voteOptionTreeDepth;
assert(5 ** _voteOptionTreeDepth >= _numVoteOptions);
assert(_numVoteOptions >= 0);
for (let i = 0; i < _numVoteOptions; i += 1) {
this.votes.push(BigInt(0));
}
}
/**
* Generate an hash of this ballot
* @returns The hash of the ballot
*/
hash = (): bigint => {
const vals = this.asArray();
return hashLeftRight(vals[0], vals[1]);
};
/**
* Convert in a format suitable for the circuit
* @returns the ballot as a BigInt array
*/
asCircuitInputs = (): bigint[] => this.asArray();
/**
* Convert in a an array of bigints
* @notice this is the nonce and the root of the vote option tree
* @returns the ballot as a bigint array
*/
asArray = (): bigint[] => {
const lastIndex = this.votes.length - 1;
const foundIndex = this.votes.findIndex((_, index) => this.votes[lastIndex - index] !== BigInt(0));
const lastIndexToInsert = foundIndex < 0 ? -1 : lastIndex - foundIndex;
const voTree = new IncrementalQuinTree(this.voteOptionTreeDepth, BigInt(0), 5, hash5);
for (let i = 0; i <= lastIndexToInsert; i += 1) {
voTree.insert(this.votes[i]);
}
return [this.nonce, voTree.root];
};
/**
* Create a deep clone of this Ballot
* @returns a copy of the ballot
*/
copy = (): Ballot => {
const b = new Ballot(this.votes.length, this.voteOptionTreeDepth);
b.votes = this.votes.map(x => BigInt(x.toString()));
b.nonce = BigInt(this.nonce.toString());
return b;
};
/**
* Check if two ballots are equal (same votes and same nonce)
* @param b - The ballot to compare with
* @returns whether the two ballots are equal
*/
equals(b: Ballot): boolean {
const isEqualVotes = this.votes.every((vote, index) => vote === b.votes[index]);
return isEqualVotes ? b.nonce === this.nonce && this.votes.length === b.votes.length : false;
}
/**
* Generate a random ballot
* @param numVoteOptions How many vote options are available
* @param voteOptionTreeDepth How deep is the merkle tree holding the vote options
* @returns a random Ballot
*/
static genRandomBallot(numVoteOptions: number, voteOptionTreeDepth: number): Ballot {
const ballot = new Ballot(numVoteOptions, voteOptionTreeDepth);
ballot.nonce = genRandomSalt();
return ballot;
}
/**
* Generate a blank ballot
* @param numVoteOptions How many vote options are available
* @param voteOptionTreeDepth How deep is the merkle tree holding the vote options
* @returns a Blank Ballot object
*/
static genBlankBallot(numVoteOptions: number, voteOptionTreeDepth: number): Ballot {
const ballot = new Ballot(numVoteOptions, voteOptionTreeDepth);
return ballot;
}
/**
* Serialize to a JSON object
*/
toJSON(): IJsonBallot {
return {
votes: this.votes.map(x => x.toString()),
nonce: this.nonce.toString(),
voteOptionTreeDepth: this.voteOptionTreeDepth.toString(),
};
}
/**
* Deserialize into a Ballot instance
* @param json - the json representation
* @returns the deserialized object as a Ballot instance
*/
static fromJSON(json: IJsonBallot): Ballot {
const ballot = new Ballot(json.votes.length, Number.parseInt(json.voteOptionTreeDepth.toString(), 10));
ballot.votes = json.votes.map(x => BigInt(x));
ballot.nonce = BigInt(json.nonce);
return ballot;
}
}

View File

@@ -0,0 +1,257 @@
import {
poseidonDecrypt,
poseidonEncrypt,
genRandomSalt,
hash4,
sign,
verifySignature,
type Signature,
type Ciphertext,
type EcdhSharedKey,
type Point,
poseidonDecryptWithoutCheck,
} from "../../crypto";
import assert from "assert";
import type { ICommand, IJsonPCommand } from "./types";
import type { PrivKey } from "../privateKey";
import { Message } from "../message";
import { PubKey } from "../publicKey";
export interface IDecryptMessage {
command: PCommand;
signature: Signature;
}
/**
* @notice Unencrypted data whose fields include the user's public key, vote etc.
* This represents a Vote command.
*/
export class PCommand implements ICommand {
cmdType: bigint;
stateIndex: bigint;
newPubKey: PubKey;
voteOptionIndex: bigint;
newVoteWeight: bigint;
nonce: bigint;
pollId: bigint;
salt: bigint;
/**
* Create a new PCommand
* @param stateIndex the state index of the user
* @param newPubKey the new public key of the user
* @param voteOptionIndex the index of the vote option
* @param newVoteWeight the new vote weight of the user
* @param nonce the nonce of the message
* @param pollId the poll ID
* @param salt the salt of the message
*/
constructor(
stateIndex: bigint,
newPubKey: PubKey,
voteOptionIndex: bigint,
newVoteWeight: bigint,
nonce: bigint,
pollId: bigint,
salt: bigint = genRandomSalt(),
) {
this.cmdType = BigInt(1);
const limit50Bits = BigInt(2 ** 50);
assert(limit50Bits >= stateIndex);
assert(limit50Bits >= voteOptionIndex);
assert(limit50Bits >= newVoteWeight);
assert(limit50Bits >= nonce);
assert(limit50Bits >= pollId);
this.stateIndex = stateIndex;
this.newPubKey = newPubKey;
this.voteOptionIndex = voteOptionIndex;
this.newVoteWeight = newVoteWeight;
this.nonce = nonce;
this.pollId = pollId;
this.salt = salt;
}
/**
* Create a deep clone of this PCommand
* @returns a copy of the PCommand
*/
copy = <T extends PCommand>(): T =>
new PCommand(
BigInt(this.stateIndex.toString()),
this.newPubKey.copy(),
BigInt(this.voteOptionIndex.toString()),
BigInt(this.newVoteWeight.toString()),
BigInt(this.nonce.toString()),
BigInt(this.pollId.toString()),
BigInt(this.salt.toString()),
) as unknown as T;
/**
* @notice Returns this Command as an array. Note that 5 of the Command's fields
* are packed into a single 250-bit value. This allows Messages to be
* smaller and thereby save gas when the user publishes a message.
* @returns bigint[] - the command as an array
*/
asArray = (): bigint[] => {
/* eslint-disable no-bitwise */
const params =
BigInt(this.stateIndex) +
(BigInt(this.voteOptionIndex) << BigInt(50)) +
(BigInt(this.newVoteWeight) << BigInt(100)) +
(BigInt(this.nonce) << BigInt(150)) +
(BigInt(this.pollId) << BigInt(200));
/* eslint-enable no-bitwise */
const command = [params, ...this.newPubKey.asArray(), this.salt];
assert(command.length === 4);
return command;
};
asCircuitInputs = (): bigint[] => this.asArray();
/*
* Check whether this command has deep equivalence to another command
*/
equals = (command: PCommand): boolean =>
this.stateIndex === command.stateIndex &&
this.newPubKey.equals(command.newPubKey) &&
this.voteOptionIndex === command.voteOptionIndex &&
this.newVoteWeight === command.newVoteWeight &&
this.nonce === command.nonce &&
this.pollId === command.pollId &&
this.salt === command.salt;
hash = (): bigint => hash4(this.asArray());
/**
* @notice Signs this command and returns a Signature.
*/
sign = (privKey: PrivKey): Signature => sign(privKey.rawPrivKey, this.hash());
/**
* @notice Returns true if the given signature is a correct signature of this
* command and signed by the private key associated with the given public
* key.
*/
verifySignature = (signature: Signature, pubKey: PubKey): boolean =>
verifySignature(this.hash(), signature, pubKey.rawPubKey);
/**
* @notice Encrypts this command along with a signature to produce a Message.
* To save gas, we can constrain the following values to 50 bits and pack
* them into a 250-bit value:
* 0. state index
* 3. vote option index
* 4. new vote weight
* 5. nonce
* 6. poll ID
*/
encrypt = (signature: Signature, sharedKey: EcdhSharedKey): Message => {
const plaintext = [...this.asArray(), BigInt(signature.R8[0]), BigInt(signature.R8[1]), BigInt(signature.S)];
assert(plaintext.length === 7);
const ciphertext: Ciphertext = poseidonEncrypt(plaintext, sharedKey, BigInt(0));
const message = new Message(BigInt(1), ciphertext as bigint[]);
return message;
};
/**
* Decrypts a Message to produce a Command.
* @dev You can force decrypt the message by setting `force` to true.
* This is useful in case you don't want an invalid message to throw an error.
* @param {Message} message - the message to decrypt
* @param {EcdhSharedKey} sharedKey - the shared key to use for decryption
* @param {boolean} force - whether to force decryption or not
*/
static decrypt = (message: Message, sharedKey: EcdhSharedKey, force = false): IDecryptMessage => {
const decrypted = force
? poseidonDecryptWithoutCheck(message.data, sharedKey, BigInt(0), 7)
: poseidonDecrypt(message.data, sharedKey, BigInt(0), 7);
const p = BigInt(decrypted[0].toString());
// Returns the value of the 50 bits at position `pos` in `val`
// create 50 '1' bits
// shift left by pos
// AND with val
// shift right by pos
const extract = (val: bigint, pos: number): bigint =>
// eslint-disable-next-line no-bitwise
BigInt((((BigInt(1) << BigInt(50)) - BigInt(1)) << BigInt(pos)) & val) >> BigInt(pos);
// p is a packed value
// bits 0 - 50: stateIndex
// bits 51 - 100: voteOptionIndex
// bits 101 - 150: newVoteWeight
// bits 151 - 200: nonce
// bits 201 - 250: pollId
const stateIndex = extract(p, 0);
const voteOptionIndex = extract(p, 50);
const newVoteWeight = extract(p, 100);
const nonce = extract(p, 150);
const pollId = extract(p, 200);
const newPubKey = new PubKey([decrypted[1], decrypted[2]]);
const salt = decrypted[3];
const command = new PCommand(stateIndex, newPubKey, voteOptionIndex, newVoteWeight, nonce, pollId, salt);
const signature = {
R8: [decrypted[4], decrypted[5]] as Point,
S: decrypted[6],
};
return { command, signature };
};
/**
* Serialize into a JSON object
*/
toJSON(): IJsonPCommand {
return {
stateIndex: this.stateIndex.toString(),
newPubKey: this.newPubKey.serialize(),
voteOptionIndex: this.voteOptionIndex.toString(),
newVoteWeight: this.newVoteWeight.toString(),
nonce: this.nonce.toString(),
pollId: this.pollId.toString(),
salt: this.salt.toString(),
cmdType: this.cmdType.toString(),
};
}
/**
* Deserialize into a PCommand instance
* @param json
* @returns a PComamnd instance
*/
static fromJSON(json: IJsonPCommand): PCommand {
const command = new PCommand(
BigInt(json.stateIndex),
PubKey.deserialize(json.newPubKey),
BigInt(json.voteOptionIndex),
BigInt(json.newVoteWeight),
BigInt(json.nonce),
BigInt(json.pollId),
BigInt(json.salt),
);
return command;
}
}

View File

@@ -0,0 +1,65 @@
import type { ICommand, IJsonTCommand } from "./types";
/**
* @notice Command for submitting a topup request
*/
export class TCommand implements ICommand {
cmdType: bigint;
stateIndex: bigint;
amount: bigint;
pollId: bigint;
/**
* Create a new TCommand
* @param stateIndex the state index of the user
* @param amount the amount of voice credits
* @param pollId the poll ID
*/
constructor(stateIndex: bigint, amount: bigint, pollId: bigint) {
this.cmdType = BigInt(2);
this.stateIndex = stateIndex;
this.amount = amount;
this.pollId = pollId;
}
/**
* Create a deep clone of this TCommand
* @returns a copy of the TCommand
*/
copy = <T extends TCommand>(): T => new TCommand(this.stateIndex, this.amount, this.pollId) as T;
/**
* Check whether this command has deep equivalence to another command
* @param command the command to compare with
* @returns whether they are equal or not
*/
equals = (command: TCommand): boolean =>
this.stateIndex === command.stateIndex &&
this.amount === command.amount &&
this.pollId === command.pollId &&
this.cmdType === command.cmdType;
/**
* Serialize into a JSON object
*/
toJSON(): IJsonTCommand {
return {
stateIndex: this.stateIndex.toString(),
amount: this.amount.toString(),
cmdType: this.cmdType.toString(),
pollId: this.pollId.toString(),
};
}
/**
* Deserialize into a TCommand object
* @param json - the json representation
* @returns the TCommand instance
*/
static fromJSON(json: IJsonTCommand): TCommand {
return new TCommand(BigInt(json.stateIndex), BigInt(json.amount), BigInt(json.pollId));
}
}

View File

@@ -0,0 +1,3 @@
export { TCommand } from "./TCommand";
export { PCommand } from "./PCommand";
export type { ICommand, IJsonCommand, IJsonTCommand, IJsonPCommand } from "./types";

View File

@@ -0,0 +1,38 @@
/**
* @notice A parent interface for all the commands
*/
export interface ICommand {
cmdType: bigint;
copy: <T extends this>() => T;
equals: <T extends this>(command: T) => boolean;
toJSON: () => unknown;
}
/**
* @notice An interface representing a generic json command
*/
export interface IJsonCommand {
cmdType: string;
}
/**
* @notice An interface representing a json T command
*/
export interface IJsonTCommand extends IJsonCommand {
stateIndex: string;
amount: string;
pollId: string;
}
/**
* @notice An interface representing a json P command
*/
export interface IJsonPCommand extends IJsonCommand {
stateIndex: string;
newPubKey: string;
voteOptionIndex: string;
newVoteWeight: string;
nonce: string;
pollId: string;
salt: string;
}

View File

@@ -0,0 +1,4 @@
import { StateLeaf } from "./stateLeaf";
export const blankStateLeaf = StateLeaf.genBlankLeaf();
export const blankStateLeafHash = blankStateLeaf.hash();

View File

@@ -0,0 +1,41 @@
export { Ballot } from "./ballot";
export { Message } from "./message";
export { PrivKey, SERIALIZED_PRIV_KEY_PREFIX } from "./privateKey";
export { PubKey, SERIALIZED_PUB_KEY_PREFIX } from "./publicKey";
export { Keypair } from "./keyPair";
export { StateLeaf } from "./stateLeaf";
export { blankStateLeaf, blankStateLeafHash } from "./constants";
export type {
Proof,
IStateLeaf,
VoteOptionTreeLeaf,
IJsonKeyPair,
IJsonPrivateKey,
IJsonPublicKey,
IJsonStateLeaf,
IG1ContractParams,
IG2ContractParams,
IVkContractParams,
IVkObjectParams,
IStateLeafContractParams,
IMessageContractParams,
IJsonBallot,
} from "./types";
export {
type ICommand,
type IJsonCommand,
type IJsonTCommand,
type IJsonPCommand,
TCommand,
PCommand,
} from "./commands";
export { VerifyingKey } from "./verifyingKey";

View File

@@ -0,0 +1,90 @@
import { EcdhSharedKey, genEcdhSharedKey, genKeypair, genPubKey } from "../crypto";
import assert from "assert";
import type { IJsonKeyPair } from "./types";
import { PrivKey } from "./privateKey";
import { PubKey } from "./publicKey";
/**
* @notice A KeyPair is a pair of public and private keys
* This is a MACI keypair, which is not to be
* confused with an Ethereum public and private keypair.
* A MACI keypair is comprised of a MACI public key and a MACI private key
*/
export class Keypair {
privKey: PrivKey;
pubKey: PubKey;
/**
* Create a new instance of a Keypair
* @param privKey the private key (optional)
* @notice if no privKey is passed, it will automatically generate a new private key
*/
constructor(privKey?: PrivKey) {
if (privKey) {
this.privKey = privKey;
this.pubKey = new PubKey(genPubKey(privKey.rawPrivKey));
} else {
const rawKeyPair = genKeypair();
this.privKey = new PrivKey(rawKeyPair.privKey);
this.pubKey = new PubKey(rawKeyPair.pubKey);
}
}
/**
* Create a deep clone of this Keypair
* @returns a copy of the Keypair
*/
copy = (): Keypair => new Keypair(this.privKey.copy());
/**
* Generate a shared key
* @param privKey
* @param pubKey
* @returns
*/
static genEcdhSharedKey(privKey: PrivKey, pubKey: PubKey): EcdhSharedKey {
return genEcdhSharedKey(privKey.rawPrivKey, pubKey.rawPubKey);
}
/**
* Check whether two Keypairs are equal
* @param keypair the keypair to compare with
* @returns whether they are equal or not
*/
equals(keypair: Keypair): boolean {
const equalPrivKey = this.privKey.rawPrivKey === keypair.privKey.rawPrivKey;
const equalPubKey =
this.pubKey.rawPubKey[0] === keypair.pubKey.rawPubKey[0] &&
this.pubKey.rawPubKey[1] === keypair.pubKey.rawPubKey[1];
// If this assertion fails, something is very wrong and this function
// should not return anything
// eslint-disable-next-line no-bitwise
assert(!(+equalPrivKey ^ +equalPubKey));
return equalPrivKey;
}
/**
* Serialize into a JSON object
*/
toJSON(): IJsonKeyPair {
return {
privKey: this.privKey.serialize(),
pubKey: this.pubKey.serialize(),
};
}
/**
* Deserialize into a Keypair instance
* @param json
* @returns a keypair instance
*/
static fromJSON(json: IJsonKeyPair): Keypair {
return new Keypair(PrivKey.deserialize(json.privKey));
}
}

View File

@@ -0,0 +1,101 @@
import { hash13 } from "../crypto";
import assert from "assert";
import type { PubKey } from "./publicKey";
import type { IMessageContractParams } from "./types";
/**
* @notice An encrypted command and signature.
*/
export class Message {
msgType: bigint;
data: bigint[];
static DATA_LENGTH = 10;
/**
* Create a new instance of a Message
* @param msgType the type of the message
* @param data the data of the message
*/
constructor(msgType: bigint, data: bigint[]) {
assert(data.length === Message.DATA_LENGTH);
this.msgType = msgType;
this.data = data;
}
/**
* Return the message as an array of bigints
* @returns the message as an array of bigints
*/
private asArray = (): bigint[] => [this.msgType].concat(this.data);
/**
* Return the message as a contract param
* @returns the message as a contract param
*/
asContractParam = (): IMessageContractParams => ({
msgType: this.msgType.toString(),
data: this.data.map((x: bigint) => x.toString()),
});
/**
* Return the message as a circuit input
* @returns the message as a circuit input
*/
asCircuitInputs = (): bigint[] => this.asArray();
/**
* Hash the message data and a public key
* @param encPubKey the public key that is used to encrypt this message
* @returns the hash of the message data and the public key
*/
hash = (encPubKey: PubKey): bigint => hash13([...[this.msgType], ...this.data, ...encPubKey.rawPubKey]);
/**
* Create a copy of the message
* @returns a copy of the message
*/
copy = (): Message =>
new Message(
BigInt(this.msgType.toString()),
this.data.map((x: bigint) => BigInt(x.toString())),
);
/**
* Check if two messages are equal
* @param m the message to compare with
* @returns the result of the comparison
*/
equals = (m: Message): boolean => {
if (this.data.length !== m.data.length) {
return false;
}
if (this.msgType !== m.msgType) {
return false;
}
return this.data.every((data, index) => data === m.data[index]);
};
/**
* Serialize to a JSON object
*/
toJSON(): IMessageContractParams {
return this.asContractParam();
}
/**
* Deserialize into a Message instance
* @param json - the json representation
* @returns the deserialized object as a Message instance
*/
static fromJSON(json: IMessageContractParams): Message {
return new Message(
BigInt(json.msgType),
json.data.map(x => BigInt(x)),
);
}
}

View File

@@ -0,0 +1,89 @@
import { formatPrivKeyForBabyJub, type PrivKey as RawPrivKey } from "../crypto";
import type { IJsonPrivateKey } from "./types";
export const SERIALIZED_PRIV_KEY_PREFIX = "macisk.";
/**
* @notice PrivKey is a TS Class representing a MACI PrivateKey (on the jubjub curve)
* This is a MACI private key, which is not to be
* confused with an Ethereum private key.
* A serialized MACI private key is prefixed by 'macisk.'
* A raw MACI private key can be thought as a point on the baby jubjub curve
*/
export class PrivKey {
rawPrivKey: RawPrivKey;
/**
* Generate a new Private key object
* @param rawPrivKey the raw private key (a bigint)
*/
constructor(rawPrivKey: RawPrivKey) {
this.rawPrivKey = rawPrivKey;
}
/**
* Create a copy of this Private key
* @returns a copy of the Private key
*/
copy = (): PrivKey => new PrivKey(BigInt(this.rawPrivKey.toString()));
/**
* Return this Private key as a circuit input
* @returns the Private key as a circuit input
*/
asCircuitInputs = (): string => formatPrivKeyForBabyJub(this.rawPrivKey).toString();
/**
* Serialize the private key
* @returns the serialized private key
*/
serialize = (): string => {
let x = this.rawPrivKey.toString(16);
if (x.length % 2 !== 0) {
x = `0${x}`;
}
return `${SERIALIZED_PRIV_KEY_PREFIX}${x.padStart(64, "0")}`;
};
/**
* Deserialize the private key
* @param s the serialized private key
* @returns the deserialized private key
*/
static deserialize = (s: string): PrivKey => {
const x = s.slice(SERIALIZED_PRIV_KEY_PREFIX.length);
return new PrivKey(BigInt(`0x${x}`));
};
/**
* Check if the serialized private key is valid
* @param s the serialized private key
* @returns whether it is a valid serialized private key
*/
static isValidSerializedPrivKey = (s: string): boolean => {
const correctPrefix = s.startsWith(SERIALIZED_PRIV_KEY_PREFIX);
const x = s.slice(SERIALIZED_PRIV_KEY_PREFIX.length);
return correctPrefix && x.length === 64;
};
/**
* Serialize this object
*/
toJSON(): IJsonPrivateKey {
return {
privKey: this.serialize(),
};
}
/**
* Deserialize this object from a JSON object
* @param json - the json object
* @returns the deserialized object as a PrivKey instance
*/
static fromJSON(json: IJsonPrivateKey): PrivKey {
return PrivKey.deserialize(json.privKey);
}
}

View File

@@ -0,0 +1,142 @@
import { SNARK_FIELD_SIZE, hashLeftRight, packPubKey, unpackPubKey, type PubKey as RawPubKey } from "../crypto";
import assert from "assert";
import type { IJsonPublicKey, IG1ContractParams } from "./types";
export const SERIALIZED_PUB_KEY_PREFIX = "macipk.";
/**
* @notice A class representing a public key
* This is a MACI public key, which is not to be
* confused with an Ethereum public key.
* A serialized MACI public key is prefixed by 'macipk.'
* A raw MACI public key can be thought as a pair of
* BigIntegers (x, y) representing a point on the baby jubjub curve
*/
export class PubKey {
rawPubKey: RawPubKey;
/**
* Create a new instance of a public key
* @param rawPubKey the raw public key
*/
constructor(rawPubKey: RawPubKey) {
assert(rawPubKey[0] < SNARK_FIELD_SIZE);
assert(rawPubKey[1] < SNARK_FIELD_SIZE);
this.rawPubKey = rawPubKey;
}
/**
* Create a copy of the public key
* @returns a copy of the public key
*/
copy = (): PubKey => new PubKey([BigInt(this.rawPubKey[0].toString()), BigInt(this.rawPubKey[1].toString())]);
/**
* Return this public key as smart contract parameters
* @returns the public key as smart contract parameters
*/
asContractParam = (): IG1ContractParams => {
const [x, y] = this.rawPubKey;
return {
x: x.toString(),
y: y.toString(),
};
};
/**
* Return this public key as circuit inputs
* @returns an array of strings
*/
asCircuitInputs = (): string[] => this.rawPubKey.map(x => x.toString());
/**
* Return this public key as an array of bigints
* @returns the public key as an array of bigints
*/
asArray = (): bigint[] => [this.rawPubKey[0], this.rawPubKey[1]];
/**
* Generate a serialized public key from this public key object
* @returns the string representation of a serialized public key
*/
serialize = (): string => {
const { x, y } = this.asContractParam();
// Blank leaves have pubkey [0, 0], which packPubKey does not support
if (BigInt(x) === BigInt(0) && BigInt(y) === BigInt(0)) {
return `${SERIALIZED_PUB_KEY_PREFIX}z`;
}
const packed = packPubKey(this.rawPubKey).toString(16);
if (packed.length % 2 !== 0) {
return `${SERIALIZED_PUB_KEY_PREFIX}0${packed}`;
}
return `${SERIALIZED_PUB_KEY_PREFIX}${packed}`;
};
/**
* Hash the two baby jubjub coordinates
* @returns the hash of this public key
*/
hash = (): bigint => hashLeftRight(this.rawPubKey[0], this.rawPubKey[1]);
/**
* Check whether this public key equals to another public key
* @param p the public key to compare with
* @returns whether they match
*/
equals = (p: PubKey): boolean => this.rawPubKey[0] === p.rawPubKey[0] && this.rawPubKey[1] === p.rawPubKey[1];
/**
* Deserialize a serialized public key
* @param s the serialized public key
* @returns the deserialized public key
*/
static deserialize = (s: string): PubKey => {
// Blank leaves have pubkey [0, 0], which packPubKey does not support
if (s === `${SERIALIZED_PUB_KEY_PREFIX}z`) {
return new PubKey([BigInt(0), BigInt(0)]);
}
const len = SERIALIZED_PUB_KEY_PREFIX.length;
return new PubKey(unpackPubKey(BigInt(`0x${s.slice(len).toString()}`)));
};
/**
* Check whether a serialized public key is serialized correctly
* @param s the serialized public key
* @returns whether the serialized public key is valid
*/
static isValidSerializedPubKey = (s: string): boolean => {
const correctPrefix = s.startsWith(SERIALIZED_PUB_KEY_PREFIX);
try {
PubKey.deserialize(s);
return correctPrefix;
} catch {
return false;
}
};
/**
* Serialize this object
*/
toJSON(): IJsonPublicKey {
return {
pubKey: this.serialize(),
};
}
/**
* Deserialize a JSON object into a PubKey instance
* @param json - the json object
* @returns PubKey
*/
static fromJSON(json: IJsonPublicKey): PubKey {
return PubKey.deserialize(json.pubKey);
}
}

View File

@@ -0,0 +1,159 @@
import { genRandomSalt, hash4 } from "../crypto";
import type { IJsonStateLeaf, IStateLeaf, IStateLeafContractParams } from "./types";
import { Keypair } from "./keyPair";
import { PubKey } from "./publicKey";
/**
* @notice A leaf in the state tree, which maps
* public keys to voice credit balances
*/
export class StateLeaf implements IStateLeaf {
pubKey: PubKey;
voiceCreditBalance: bigint;
timestamp: bigint;
/**
* Create a new instance of a state leaf
* @param pubKey the public key of the user signin up
* @param voiceCreditBalance the voice credit balance of the user
* @param timestamp the timestamp of when the user signed-up
*/
constructor(pubKey: PubKey, voiceCreditBalance: bigint, timestamp: bigint) {
this.pubKey = pubKey;
this.voiceCreditBalance = voiceCreditBalance;
this.timestamp = timestamp;
}
/**
* Crate a deep copy of the object
* @returns a copy of the state leaf
*/
copy(): StateLeaf {
return new StateLeaf(
this.pubKey.copy(),
BigInt(this.voiceCreditBalance.toString()),
BigInt(this.timestamp.toString()),
);
}
/**
* Generate a blank state leaf
* @returns a blank state leaf
*/
static genBlankLeaf(): StateLeaf {
// The public key for a blank state leaf is the first Pedersen base
// point from iden3's circomlib implementation of the Pedersen hash.
// Since it is generated using a hash-to-curve function, we are
// confident that no-one knows the private key associated with this
// public key. See:
// https://github.com/iden3/circomlib/blob/d5ed1c3ce4ca137a6b3ca48bec4ac12c1b38957a/src/pedersen_printbases.js
// Its hash should equal
// 6769006970205099520508948723718471724660867171122235270773600567925038008762.
return new StateLeaf(
new PubKey([
BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"),
BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"),
]),
BigInt(0),
BigInt(0),
);
}
/**
* Generate a random leaf (random salt and random key pair)
* @returns a random state leaf
*/
static genRandomLeaf(): StateLeaf {
const keypair = new Keypair();
return new StateLeaf(keypair.pubKey, genRandomSalt(), BigInt(0));
}
/**
* Return this state leaf as an array of bigints
* @returns the state leaf as an array of bigints
*/
private asArray = (): bigint[] => [...this.pubKey.asArray(), this.voiceCreditBalance, this.timestamp];
/**
* Return this state leaf as an array of bigints
* @returns the state leaf as an array of bigints
*/
asCircuitInputs = (): bigint[] => this.asArray();
/**
* Hash this state leaf (first convert as array)
* @returns the has of the state leaf elements
*/
hash = (): bigint => hash4(this.asArray());
/**
* Return this state leaf as a contract param
* @returns the state leaf as a contract param (object)
*/
asContractParam(): IStateLeafContractParams {
return {
pubKey: this.pubKey.asContractParam(),
voiceCreditBalance: this.voiceCreditBalance.toString(),
timestamp: this.timestamp.toString(),
};
}
/**
* Check if two state leaves are equal
* @param s the state leaf to compare with
* @returns whether they are equal or not
*/
equals(s: StateLeaf): boolean {
return (
this.pubKey.equals(s.pubKey) && this.voiceCreditBalance === s.voiceCreditBalance && this.timestamp === s.timestamp
);
}
/**
* Serialize the state leaf
* @notice serialize the public key
* @notice convert the voice credit balance and timestamp to a hex string
* @returns
*/
serialize = (): string => {
const j = [this.pubKey.serialize(), this.voiceCreditBalance.toString(16), this.timestamp.toString(16)];
return Buffer.from(JSON.stringify(j, null, 0), "utf8").toString("base64url");
};
/**
* Deserialize the state leaf
* @param serialized the serialized state leaf
* @returns a deserialized state leaf
*/
static deserialize = (serialized: string): StateLeaf => {
const base64 = serialized.replace(/-/g, "+").replace(/_/g, "/");
const json = JSON.parse(Buffer.from(base64, "base64").toString("utf8")) as [string, string, string];
return new StateLeaf(PubKey.deserialize(json[0]), BigInt(`0x${json[1]}`), BigInt(`0x${json[2]}`));
};
/**
* Serialize to a JSON object
*/
toJSON(): IJsonStateLeaf {
return {
pubKey: this.pubKey.serialize(),
voiceCreditBalance: this.voiceCreditBalance.toString(),
timestamp: this.timestamp.toString(),
};
}
/**
* Deserialize into a StateLeaf instance
* @param json - the json representation
* @returns the deserialized object as a StateLeaf instance
*/
static fromJSON(json: IJsonStateLeaf): StateLeaf {
return new StateLeaf(PubKey.deserialize(json.pubKey), BigInt(json.voiceCreditBalance), BigInt(json.timestamp));
}
}

View File

@@ -0,0 +1,90 @@
import type { PubKey } from "./publicKey";
import type { G1Point, G2Point } from "../crypto";
/**
* @notice An interface representing a zk-SNARK proof
*/
export interface Proof {
a: G1Point;
b: G2Point;
c: G1Point;
}
/**
* @notice An interface representing a MACI state leaf
*/
export interface IStateLeaf {
pubKey: PubKey;
voiceCreditBalance: bigint;
}
/**
* @notice An interface representing a MACI vote option leaf
*/
export interface VoteOptionTreeLeaf {
votes: bigint;
}
export interface IJsonKeyPair {
privKey: string;
pubKey: string;
}
export type IJsonPrivateKey = Pick<IJsonKeyPair, "privKey">;
export type IJsonPublicKey = Pick<IJsonKeyPair, "pubKey">;
export interface IJsonStateLeaf {
pubKey: string;
voiceCreditBalance: string;
timestamp: string;
}
export type BigNumberish = number | string | bigint;
export interface IG1ContractParams {
x: BigNumberish;
y: BigNumberish;
}
export interface IG2ContractParams {
x: BigNumberish[];
y: BigNumberish[];
}
export interface IVkContractParams {
alpha1: IG1ContractParams;
beta2: IG2ContractParams;
gamma2: IG2ContractParams;
delta2: IG2ContractParams;
ic: IG1ContractParams[];
}
export interface IVkObjectParams {
protocol: BigNumberish;
curve: BigNumberish;
nPublic: BigNumberish;
vk_alpha_1: BigNumberish[];
vk_beta_2: BigNumberish[][];
vk_gamma_2: BigNumberish[][];
vk_delta_2: BigNumberish[][];
vk_alphabeta_12: BigNumberish[][][];
IC: BigNumberish[][];
}
export interface IStateLeafContractParams {
pubKey: IG1ContractParams;
voiceCreditBalance: BigNumberish;
timestamp: BigNumberish;
}
export interface IMessageContractParams {
msgType: string;
data: BigNumberish[];
}
export interface IJsonBallot {
votes: BigNumberish[];
nonce: BigNumberish;
voteOptionTreeDepth: BigNumberish;
}

View File

@@ -0,0 +1,143 @@
import { G1Point, G2Point } from "../crypto";
import type { IVkContractParams, IVkObjectParams } from "./types";
/**
* @notice A TS Class representing a zk-SNARK VerifyingKey
*/
export class VerifyingKey {
alpha1: G1Point;
beta2: G2Point;
gamma2: G2Point;
delta2: G2Point;
ic: G1Point[];
/**
* Generate a new VerifyingKey
* @param alpha1 the alpha1 point
* @param beta2 the beta2 point
* @param gamma2 the gamma2 point
* @param delta2 the delta2 point
* @param ic the ic points
*/
constructor(alpha1: G1Point, beta2: G2Point, gamma2: G2Point, delta2: G2Point, ic: G1Point[]) {
this.alpha1 = alpha1;
this.beta2 = beta2;
this.gamma2 = gamma2;
this.delta2 = delta2;
this.ic = ic;
}
/**
* Return this as an object which can be passed
* to the smart contract
* @returns the object representation of this
*/
asContractParam(): IVkContractParams {
return {
alpha1: this.alpha1.asContractParam(),
beta2: this.beta2.asContractParam(),
gamma2: this.gamma2.asContractParam(),
delta2: this.delta2.asContractParam(),
ic: this.ic.map(x => x.asContractParam()),
};
}
/**
* Create a new verifying key from a contract representation of the VK
* @param data the object representation
* @returns a new VerifyingKey
*/
static fromContract(data: IVkContractParams): VerifyingKey {
const convertG2 = (point: IVkContractParams["beta2"]): G2Point =>
new G2Point([BigInt(point.x[0]), BigInt(point.x[1])], [BigInt(point.y[0]), BigInt(point.y[1])]);
return new VerifyingKey(
new G1Point(BigInt(data.alpha1.x), BigInt(data.alpha1.y)),
convertG2(data.beta2),
convertG2(data.gamma2),
convertG2(data.delta2),
data.ic.map(c => new G1Point(BigInt(c.x), BigInt(c.y))),
);
}
/**
* Check whether this is equal to another verifying key
* @param vk the other verifying key
* @returns whether this is equal to the other verifying key
*/
equals(vk: VerifyingKey): boolean {
// Immediately return false if the length doesn't match
if (this.ic.length !== vk.ic.length) {
return false;
}
const icEqual = this.ic.every((ic, index) => ic.equals(vk.ic[index]));
return (
this.alpha1.equals(vk.alpha1) &&
this.beta2.equals(vk.beta2) &&
this.gamma2.equals(vk.gamma2) &&
this.delta2.equals(vk.delta2) &&
icEqual
);
}
/**
* Produce a copy of this verifying key
* @returns the copy
*/
copy(): VerifyingKey {
const copyG2 = (point: G2Point): G2Point =>
new G2Point(
[BigInt(point.x[0].toString()), BigInt(point.x[1].toString())],
[BigInt(point.y[0].toString()), BigInt(point.y[1].toString())],
);
return new VerifyingKey(
new G1Point(BigInt(this.alpha1.x.toString()), BigInt(this.alpha1.y.toString())),
copyG2(this.beta2),
copyG2(this.gamma2),
copyG2(this.delta2),
this.ic.map((c: G1Point) => new G1Point(BigInt(c.x.toString()), BigInt(c.y.toString()))),
);
}
/**
* Deserialize into a VerifyingKey instance
* @param json the JSON representation
* @returns the VerifyingKey
*/
static fromJSON = (json: string): VerifyingKey => {
const data = JSON.parse(json) as IVkObjectParams;
return VerifyingKey.fromObj(data);
};
/**
* Convert an object representation to a VerifyingKey
* @param data the object representation
* @returns the VerifyingKey
*/
static fromObj = (data: IVkObjectParams): VerifyingKey => {
const alpha1 = new G1Point(BigInt(data.vk_alpha_1[0]), BigInt(data.vk_alpha_1[1]));
const beta2 = new G2Point(
[BigInt(data.vk_beta_2[0][1]), BigInt(data.vk_beta_2[0][0])],
[BigInt(data.vk_beta_2[1][1]), BigInt(data.vk_beta_2[1][0])],
);
const gamma2 = new G2Point(
[BigInt(data.vk_gamma_2[0][1]), BigInt(data.vk_gamma_2[0][0])],
[BigInt(data.vk_gamma_2[1][1]), BigInt(data.vk_gamma_2[1][0])],
);
const delta2 = new G2Point(
[BigInt(data.vk_delta_2[0][1]), BigInt(data.vk_delta_2[0][0])],
[BigInt(data.vk_delta_2[1][1]), BigInt(data.vk_delta_2[1][0])],
);
const ic = data.IC.map(([x, y]) => new G1Point(BigInt(x), BigInt(y)));
return new VerifyingKey(alpha1, beta2, gamma2, delta2, ic);
};
}

View File

@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
abstract contract EmptyBallotRoots {
// emptyBallotRoots contains the roots of Ballot trees of five leaf
// configurations.
// Each tree has a depth of 10, which is the hardcoded state tree depth.
// Each leaf is an empty ballot. A configuration refers to the depth of the
// voice option tree for that ballot.
// The leaf for the root at index 0 contains hash(0, root of a VO tree with
// depth 1 and zero-value 0)
// The leaf for the root at index 1 contains hash(0, root of a VO tree with
// depth 2 and zero-value 0)
// ... and so on.
// The first parameter to the hash function is the nonce, which is 0.
uint256[5] internal emptyBallotRoots;
constructor() {
<% ROOTS %>
}
}

View File

@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
abstract contract MerkleZeros {
uint256[<% NUM_ZEROS %>] internal zeros;
// <% COMMENT %>
constructor() {
<% ZEROS %>
}
}

View File

@@ -0,0 +1,52 @@
import fs from "fs";
import path from "path";
import type { Fragment, JsonFragment } from "ethers";
import { abiDir } from "./constants";
// a type representing the ABI of a contract
export type TAbi = string | readonly (string | Fragment | JsonFragment)[];
/**
* Parse a contract artifact and return its ABI and bytecode.
* @param filename - the name of the contract
* @returns the ABI and bytecode of the contract
*/
export const parseArtifact = (filename: string): [TAbi, string] => {
let filePath = "contracts/maci-contracts/";
if (filename.includes("Gatekeeper")) {
filePath += "gatekeepers/";
filePath += `${filename}.sol`;
}
if (filename.includes("VoiceCredit")) {
filePath += "initialVoiceCreditProxy/";
filePath += `${filename}.sol`;
}
if (filename.includes("Verifier")) {
filePath += "crypto/Verifier.sol/";
}
if (filename.includes("AccQueue")) {
filePath += `trees/${filename}.sol/`;
}
if (filename.includes("Poll") || filename.includes("MessageAq")) {
filePath += "Poll.sol";
}
if (!filePath.includes(".sol")) {
filePath += `${filename}.sol`;
}
const contractArtifact = JSON.parse(
fs.readFileSync(path.resolve(abiDir, filePath, `${filename}.json`)).toString(),
) as {
abi: TAbi;
bytecode: string;
};
return [contractArtifact.abi, contractArtifact.bytecode];
};

View File

@@ -0,0 +1,20 @@
import { poseidonContract } from "circomlibjs";
import hre from "hardhat";
type ExtendedHre = typeof hre & { overwriteArtifact: (name: string, code: unknown) => Promise<void> };
const buildPoseidon = async (numInputs: number) => {
await (hre as ExtendedHre).overwriteArtifact(`PoseidonT${numInputs + 1}`, poseidonContract.createCode(numInputs));
};
export const buildPoseidonT3 = (): Promise<void> => buildPoseidon(2);
export const buildPoseidonT4 = (): Promise<void> => buildPoseidon(3);
export const buildPoseidonT5 = (): Promise<void> => buildPoseidon(4);
export const buildPoseidonT6 = (): Promise<void> => buildPoseidon(5);
if (require.main === module) {
buildPoseidonT3();
buildPoseidonT4();
buildPoseidonT5();
buildPoseidonT6();
}

View File

@@ -0,0 +1,6 @@
import path from "path";
// The directory where the contract artifacts are stored.
export const abiDir = path.resolve(__dirname, "..", "..", "artifacts");
// The directory where the contract source files are stored.
export const solDir = path.resolve(__dirname, "..", "contracts");

Some files were not shown because too many files have changed in this diff Show More