mirror of
https://github.com/yashgo0018/maci-wrapper.git
synced 2026-05-04 03:00:44 -04:00
made the deployment scripts for the maci contracts
This commit is contained in:
2
packages/hardhat/.gitignore
vendored
2
packages/hardhat/.gitignore
vendored
@@ -15,3 +15,5 @@ artifacts-zk
|
||||
cache-zk
|
||||
|
||||
deployments/localhost
|
||||
|
||||
zkeys
|
||||
|
||||
17
packages/hardhat/constants.ts
Normal file
17
packages/hardhat/constants.ts
Normal 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;
|
||||
@@ -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"];
|
||||
23
packages/hardhat/deploy/00_initial_voice_credit_proxy.ts
Normal file
23
packages/hardhat/deploy/00_initial_voice_credit_proxy.ts
Normal 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"];
|
||||
21
packages/hardhat/deploy/01_gatekeepers.ts
Normal file
21
packages/hardhat/deploy/01_gatekeepers.ts
Normal 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"];
|
||||
21
packages/hardhat/deploy/02_verifier.ts
Normal file
21
packages/hardhat/deploy/02_verifier.ts
Normal 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"];
|
||||
21
packages/hardhat/deploy/03_topup_credit.ts
Normal file
21
packages/hardhat/deploy/03_topup_credit.ts
Normal 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"];
|
||||
37
packages/hardhat/deploy/04_poseidon.ts
Normal file
37
packages/hardhat/deploy/04_poseidon.ts
Normal 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"];
|
||||
32
packages/hardhat/deploy/05_poll_factory.ts
Normal file
32
packages/hardhat/deploy/05_poll_factory.ts
Normal 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"];
|
||||
32
packages/hardhat/deploy/06_message_processor_factory.ts
Normal file
32
packages/hardhat/deploy/06_message_processor_factory.ts
Normal 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"];
|
||||
33
packages/hardhat/deploy/07_tally_factory.ts
Normal file
33
packages/hardhat/deploy/07_tally_factory.ts
Normal 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"];
|
||||
32
packages/hardhat/deploy/08_subsidy_factory.ts
Normal file
32
packages/hardhat/deploy/08_subsidy_factory.ts
Normal 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"];
|
||||
59
packages/hardhat/deploy/09_maci.ts
Normal file
59
packages/hardhat/deploy/09_maci.ts
Normal 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"];
|
||||
60
packages/hardhat/deploy/10_vk_registry.ts
Normal file
60
packages/hardhat/deploy/10_vk_registry.ts
Normal 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"];
|
||||
@@ -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,
|
||||
|
||||
22
packages/hardhat/maci-scripts/compileSol.sh
Executable file
22
packages/hardhat/maci-scripts/compileSol.sh
Executable 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
|
||||
41
packages/hardhat/maci-scripts/writeMerkleZeroesContracts.sh
Executable file
41
packages/hardhat/maci-scripts/writeMerkleZeroesContracts.sh
Executable 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
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
67
packages/hardhat/maci-ts/circuits/__tests__/Ecdh.test.ts
Normal file
67
packages/hardhat/maci-ts/circuits/__tests__/Ecdh.test.ts
Normal 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",
|
||||
);
|
||||
});
|
||||
});
|
||||
304
packages/hardhat/maci-ts/circuits/__tests__/Hasher.test.ts
Normal file
304
packages/hardhat/maci-ts/circuits/__tests__/Hasher.test.ts
Normal 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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
1063
packages/hardhat/maci-ts/circuits/__tests__/ProcessMessages.test.ts
Normal file
1063
packages/hardhat/maci-ts/circuits/__tests__/ProcessMessages.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
304
packages/hardhat/maci-ts/circuits/__tests__/TallyVotes.test.ts
Normal file
304
packages/hardhat/maci-ts/circuits/__tests__/TallyVotes.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
22
packages/hardhat/maci-ts/circuits/__tests__/utils/types.ts
Normal file
22
packages/hardhat/maci-ts/circuits/__tests__/utils/types.ts
Normal 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;
|
||||
}
|
||||
45
packages/hardhat/maci-ts/circuits/__tests__/utils/utils.ts
Normal file
45
packages/hardhat/maci-ts/circuits/__tests__/utils/utils.ts
Normal 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]);
|
||||
};
|
||||
91
packages/hardhat/maci-ts/circuits/compile.ts
Normal file
91
packages/hardhat/maci-ts/circuits/compile.ts
Normal 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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
72
packages/hardhat/maci-ts/circuits/genZkeys.ts
Normal file
72
packages/hardhat/maci-ts/circuits/genZkeys.ts
Normal 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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
3
packages/hardhat/maci-ts/circuits/index.ts
Normal file
3
packages/hardhat/maci-ts/circuits/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { genProof, verifyProof, extractVk } from "./proofs";
|
||||
export { cleanThreads } from "./utils";
|
||||
export type { ISnarkJSVerificationKey } from "./types";
|
||||
136
packages/hardhat/maci-ts/circuits/proofs.ts
Normal file
136
packages/hardhat/maci-ts/circuits/proofs.ts
Normal 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;
|
||||
};
|
||||
78
packages/hardhat/maci-ts/circuits/types.ts
Normal file
78
packages/hardhat/maci-ts/circuits/types.ts
Normal 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 };
|
||||
50
packages/hardhat/maci-ts/circuits/types/snarkjs.d.ts
vendored
Normal file
50
packages/hardhat/maci-ts/circuits/types/snarkjs.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
}
|
||||
35
packages/hardhat/maci-ts/circuits/utils.ts
Normal file
35
packages/hardhat/maci-ts/circuits/utils.ts
Normal 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),
|
||||
);
|
||||
};
|
||||
175
packages/hardhat/maci-ts/core/MaciState.ts
Normal file
175
packages/hardhat/maci-ts/core/MaciState.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
1666
packages/hardhat/maci-ts/core/Poll.ts
Normal file
1666
packages/hardhat/maci-ts/core/Poll.ts
Normal file
File diff suppressed because it is too large
Load Diff
112
packages/hardhat/maci-ts/core/__benchmarks__/index.ts
Normal file
112
packages/hardhat/maci-ts/core/__benchmarks__/index.ts
Normal 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();
|
||||
@@ -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,
|
||||
};
|
||||
190
packages/hardhat/maci-ts/core/__tests__/MaciState.test.ts
Normal file
190
packages/hardhat/maci-ts/core/__tests__/MaciState.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
667
packages/hardhat/maci-ts/core/__tests__/Poll.test.ts
Normal file
667
packages/hardhat/maci-ts/core/__tests__/Poll.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
894
packages/hardhat/maci-ts/core/__tests__/e2e.test.ts
Normal file
894
packages/hardhat/maci-ts/core/__tests__/e2e.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
62
packages/hardhat/maci-ts/core/__tests__/utils.test.ts
Normal file
62
packages/hardhat/maci-ts/core/__tests__/utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
18
packages/hardhat/maci-ts/core/__tests__/utils/constants.ts
Normal file
18
packages/hardhat/maci-ts/core/__tests__/utils/constants.ts
Normal 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,
|
||||
};
|
||||
154
packages/hardhat/maci-ts/core/__tests__/utils/utils.ts
Normal file
154
packages/hardhat/maci-ts/core/__tests__/utils/utils.ts
Normal 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;
|
||||
}
|
||||
27
packages/hardhat/maci-ts/core/index.ts
Normal file
27
packages/hardhat/maci-ts/core/index.ts
Normal 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";
|
||||
5
packages/hardhat/maci-ts/core/utils/constants.ts
Normal file
5
packages/hardhat/maci-ts/core/utils/constants.ts
Normal 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;
|
||||
29
packages/hardhat/maci-ts/core/utils/errors.ts
Normal file
29
packages/hardhat/maci-ts/core/utils/errors.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
224
packages/hardhat/maci-ts/core/utils/types.ts
Normal file
224
packages/hardhat/maci-ts/core/utils/types.ts
Normal 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[];
|
||||
}
|
||||
155
packages/hardhat/maci-ts/core/utils/utils.ts
Normal file
155
packages/hardhat/maci-ts/core/utils/utils.ts
Normal 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;
|
||||
};
|
||||
627
packages/hardhat/maci-ts/crypto/AccQueue.ts
Normal file
627
packages/hardhat/maci-ts/crypto/AccQueue.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
3
packages/hardhat/maci-ts/crypto/__benchmarks__/index.ts
Normal file
3
packages/hardhat/maci-ts/crypto/__benchmarks__/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import runTrees from "./suites/trees";
|
||||
|
||||
runTrees();
|
||||
@@ -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 }),
|
||||
);
|
||||
}
|
||||
256
packages/hardhat/maci-ts/crypto/__tests__/AccQueue.test.ts
Normal file
256
packages/hardhat/maci-ts/crypto/__tests__/AccQueue.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
578
packages/hardhat/maci-ts/crypto/__tests__/Crypto.test.ts
Normal file
578
packages/hardhat/maci-ts/crypto/__tests__/Crypto.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
167
packages/hardhat/maci-ts/crypto/__tests__/IMT.test.ts
Normal file
167
packages/hardhat/maci-ts/crypto/__tests__/IMT.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
241
packages/hardhat/maci-ts/crypto/__tests__/Utils.test.ts
Normal file
241
packages/hardhat/maci-ts/crypto/__tests__/Utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
131
packages/hardhat/maci-ts/crypto/__tests__/utils.ts
Normal file
131
packages/hardhat/maci-ts/crypto/__tests__/utils.ts
Normal 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());
|
||||
}
|
||||
};
|
||||
132
packages/hardhat/maci-ts/crypto/babyjub.ts
Normal file
132
packages/hardhat/maci-ts/crypto/babyjub.ts
Normal 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;
|
||||
};
|
||||
135
packages/hardhat/maci-ts/crypto/bigIntUtils.ts
Normal file
135
packages/hardhat/maci-ts/crypto/bigIntUtils.ts
Normal 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");
|
||||
};
|
||||
12
packages/hardhat/maci-ts/crypto/constants.ts
Normal file
12
packages/hardhat/maci-ts/crypto/constants.ts
Normal 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"));
|
||||
160
packages/hardhat/maci-ts/crypto/hashing.ts
Normal file
160
packages/hardhat/maci-ts/crypto/hashing.ts
Normal 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)]);
|
||||
43
packages/hardhat/maci-ts/crypto/index.ts
Normal file
43
packages/hardhat/maci-ts/crypto/index.ts
Normal 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";
|
||||
77
packages/hardhat/maci-ts/crypto/keys.ts
Normal file
77
packages/hardhat/maci-ts/crypto/keys.ts
Normal 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 Diffie–Hellman (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));
|
||||
377
packages/hardhat/maci-ts/crypto/quinTree.ts
Normal file
377
packages/hardhat/maci-ts/crypto/quinTree.ts
Normal 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 };
|
||||
};
|
||||
}
|
||||
95
packages/hardhat/maci-ts/crypto/types.ts
Normal file
95
packages/hardhat/maci-ts/crypto/types.ts
Normal 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;
|
||||
55
packages/hardhat/maci-ts/crypto/utils.ts
Normal file
55
packages/hardhat/maci-ts/crypto/utils.ts
Normal 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;
|
||||
};
|
||||
119
packages/hardhat/maci-ts/domainobjs/__tests__/ballot.test.ts
Normal file
119
packages/hardhat/maci-ts/domainobjs/__tests__/ballot.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
164
packages/hardhat/maci-ts/domainobjs/__tests__/commands.test.ts
Normal file
164
packages/hardhat/maci-ts/domainobjs/__tests__/commands.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
119
packages/hardhat/maci-ts/domainobjs/__tests__/keypair.test.ts
Normal file
119
packages/hardhat/maci-ts/domainobjs/__tests__/keypair.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
120
packages/hardhat/maci-ts/domainobjs/__tests__/message.test.ts
Normal file
120
packages/hardhat/maci-ts/domainobjs/__tests__/message.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
128
packages/hardhat/maci-ts/domainobjs/__tests__/privateKey.test.ts
Normal file
128
packages/hardhat/maci-ts/domainobjs/__tests__/privateKey.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
190
packages/hardhat/maci-ts/domainobjs/__tests__/publicKey.test.ts
Normal file
190
packages/hardhat/maci-ts/domainobjs/__tests__/publicKey.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
140
packages/hardhat/maci-ts/domainobjs/__tests__/stateLeaf.test.ts
Normal file
140
packages/hardhat/maci-ts/domainobjs/__tests__/stateLeaf.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
131
packages/hardhat/maci-ts/domainobjs/ballot.ts
Normal file
131
packages/hardhat/maci-ts/domainobjs/ballot.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
257
packages/hardhat/maci-ts/domainobjs/commands/PCommand.ts
Normal file
257
packages/hardhat/maci-ts/domainobjs/commands/PCommand.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
65
packages/hardhat/maci-ts/domainobjs/commands/TCommand.ts
Normal file
65
packages/hardhat/maci-ts/domainobjs/commands/TCommand.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
3
packages/hardhat/maci-ts/domainobjs/commands/index.ts
Normal file
3
packages/hardhat/maci-ts/domainobjs/commands/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { TCommand } from "./TCommand";
|
||||
export { PCommand } from "./PCommand";
|
||||
export type { ICommand, IJsonCommand, IJsonTCommand, IJsonPCommand } from "./types";
|
||||
38
packages/hardhat/maci-ts/domainobjs/commands/types.ts
Normal file
38
packages/hardhat/maci-ts/domainobjs/commands/types.ts
Normal 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;
|
||||
}
|
||||
4
packages/hardhat/maci-ts/domainobjs/constants.ts
Normal file
4
packages/hardhat/maci-ts/domainobjs/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { StateLeaf } from "./stateLeaf";
|
||||
|
||||
export const blankStateLeaf = StateLeaf.genBlankLeaf();
|
||||
export const blankStateLeafHash = blankStateLeaf.hash();
|
||||
41
packages/hardhat/maci-ts/domainobjs/index.ts
Normal file
41
packages/hardhat/maci-ts/domainobjs/index.ts
Normal 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";
|
||||
90
packages/hardhat/maci-ts/domainobjs/keyPair.ts
Normal file
90
packages/hardhat/maci-ts/domainobjs/keyPair.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
101
packages/hardhat/maci-ts/domainobjs/message.ts
Normal file
101
packages/hardhat/maci-ts/domainobjs/message.ts
Normal 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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
89
packages/hardhat/maci-ts/domainobjs/privateKey.ts
Normal file
89
packages/hardhat/maci-ts/domainobjs/privateKey.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
142
packages/hardhat/maci-ts/domainobjs/publicKey.ts
Normal file
142
packages/hardhat/maci-ts/domainobjs/publicKey.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
159
packages/hardhat/maci-ts/domainobjs/stateLeaf.ts
Normal file
159
packages/hardhat/maci-ts/domainobjs/stateLeaf.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
90
packages/hardhat/maci-ts/domainobjs/types.ts
Normal file
90
packages/hardhat/maci-ts/domainobjs/types.ts
Normal 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;
|
||||
}
|
||||
143
packages/hardhat/maci-ts/domainobjs/verifyingKey.ts
Normal file
143
packages/hardhat/maci-ts/domainobjs/verifyingKey.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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 %>
|
||||
}
|
||||
}
|
||||
11
packages/hardhat/maci-ts/templates/MerkleZeros.sol.template
Normal file
11
packages/hardhat/maci-ts/templates/MerkleZeros.sol.template
Normal 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 %>
|
||||
}
|
||||
}
|
||||
52
packages/hardhat/maci-ts/ts/abi.ts
Normal file
52
packages/hardhat/maci-ts/ts/abi.ts
Normal 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];
|
||||
};
|
||||
20
packages/hardhat/maci-ts/ts/buildPoseidon.ts
Normal file
20
packages/hardhat/maci-ts/ts/buildPoseidon.ts
Normal 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();
|
||||
}
|
||||
6
packages/hardhat/maci-ts/ts/constants.ts
Normal file
6
packages/hardhat/maci-ts/ts/constants.ts
Normal 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
Reference in New Issue
Block a user