From cbee4e33f9ee9f7445a45a025437b746b38330b6 Mon Sep 17 00:00:00 2001 From: Yash Goyal Date: Sat, 23 Mar 2024 07:23:34 +0530 Subject: [PATCH] made the deployment scripts for the maci contracts --- packages/hardhat/.gitignore | 2 + packages/hardhat/constants.ts | 17 + .../hardhat/deploy/00_deploy_your_contract.ts | 44 - .../deploy/00_initial_voice_credit_proxy.ts | 23 + packages/hardhat/deploy/01_gatekeepers.ts | 21 + packages/hardhat/deploy/02_verifier.ts | 21 + packages/hardhat/deploy/03_topup_credit.ts | 21 + packages/hardhat/deploy/04_poseidon.ts | 37 + packages/hardhat/deploy/05_poll_factory.ts | 32 + .../deploy/06_message_processor_factory.ts | 32 + packages/hardhat/deploy/07_tally_factory.ts | 33 + packages/hardhat/deploy/08_subsidy_factory.ts | 32 + packages/hardhat/deploy/09_maci.ts | 59 + packages/hardhat/deploy/10_vk_registry.ts | 60 + packages/hardhat/hardhat.config.ts | 4 +- packages/hardhat/maci-scripts/compileSol.sh | 22 + .../writeMerkleZeroesContracts.sh | 41 + .../circuits/__tests__/CalculateTotal.test.ts | 30 + .../circuits/__tests__/CeremonyParams.test.ts | 354 ++++ .../maci-ts/circuits/__tests__/Ecdh.test.ts | 67 + .../maci-ts/circuits/__tests__/Hasher.test.ts | 304 +++ .../__tests__/IncrementalQuinTree.test.ts | 152 ++ .../__tests__/MessageToCommand.test.ts | 137 ++ .../__tests__/MessageValidator.test.ts | 156 ++ .../circuits/__tests__/PrivToPubKey.test.ts | 51 + .../__tests__/ProcessMessages.test.ts | 1063 +++++++++++ .../circuits/__tests__/QuinCheckRoot.test.ts | 56 + .../StateLeafAndBallotTransformer.test.ts | 140 ++ .../circuits/__tests__/TallyVotes.test.ts | 304 +++ .../circuits/__tests__/UnpackElement.test.ts | 75 + .../__tests__/VerifySignature.test.ts | 101 + .../circuits/__tests__/utils/constants.ts | 15 + .../maci-ts/circuits/__tests__/utils/types.ts | 22 + .../maci-ts/circuits/__tests__/utils/utils.ts | 45 + packages/hardhat/maci-ts/circuits/compile.ts | 91 + packages/hardhat/maci-ts/circuits/genZkeys.ts | 72 + packages/hardhat/maci-ts/circuits/index.ts | 3 + packages/hardhat/maci-ts/circuits/proofs.ts | 136 ++ packages/hardhat/maci-ts/circuits/types.ts | 78 + .../maci-ts/circuits/types/snarkjs.d.ts | 50 + packages/hardhat/maci-ts/circuits/utils.ts | 35 + packages/hardhat/maci-ts/core/MaciState.ts | 175 ++ packages/hardhat/maci-ts/core/Poll.ts | 1666 +++++++++++++++++ .../maci-ts/core/__benchmarks__/index.ts | 112 ++ .../core/__benchmarks__/utils/constants.ts | 19 + .../maci-ts/core/__tests__/MaciState.test.ts | 190 ++ .../maci-ts/core/__tests__/Poll.test.ts | 667 +++++++ .../maci-ts/core/__tests__/e2e.test.ts | 894 +++++++++ .../maci-ts/core/__tests__/utils.test.ts | 62 + .../maci-ts/core/__tests__/utils/constants.ts | 18 + .../maci-ts/core/__tests__/utils/utils.ts | 154 ++ packages/hardhat/maci-ts/core/index.ts | 27 + .../hardhat/maci-ts/core/utils/constants.ts | 5 + packages/hardhat/maci-ts/core/utils/errors.ts | 29 + packages/hardhat/maci-ts/core/utils/types.ts | 224 +++ packages/hardhat/maci-ts/core/utils/utils.ts | 155 ++ packages/hardhat/maci-ts/crypto/AccQueue.ts | 627 +++++++ .../maci-ts/crypto/__benchmarks__/index.ts | 3 + .../crypto/__benchmarks__/suites/trees.ts | 40 + .../maci-ts/crypto/__tests__/AccQueue.test.ts | 256 +++ .../maci-ts/crypto/__tests__/Crypto.test.ts | 578 ++++++ .../maci-ts/crypto/__tests__/IMT.test.ts | 167 ++ .../maci-ts/crypto/__tests__/Utils.test.ts | 241 +++ .../hardhat/maci-ts/crypto/__tests__/utils.ts | 131 ++ packages/hardhat/maci-ts/crypto/babyjub.ts | 132 ++ .../hardhat/maci-ts/crypto/bigIntUtils.ts | 135 ++ packages/hardhat/maci-ts/crypto/constants.ts | 12 + packages/hardhat/maci-ts/crypto/hashing.ts | 160 ++ packages/hardhat/maci-ts/crypto/index.ts | 43 + packages/hardhat/maci-ts/crypto/keys.ts | 77 + packages/hardhat/maci-ts/crypto/quinTree.ts | 377 ++++ packages/hardhat/maci-ts/crypto/types.ts | 95 + packages/hardhat/maci-ts/crypto/utils.ts | 55 + .../domainobjs/__tests__/ballot.test.ts | 119 ++ .../domainobjs/__tests__/commands.test.ts | 164 ++ .../domainobjs/__tests__/keypair.test.ts | 119 ++ .../domainobjs/__tests__/message.test.ts | 120 ++ .../domainobjs/__tests__/privateKey.test.ts | 128 ++ .../domainobjs/__tests__/publicKey.test.ts | 190 ++ .../domainobjs/__tests__/stateLeaf.test.ts | 140 ++ .../domainobjs/__tests__/verifyingKey.test.ts | 135 ++ packages/hardhat/maci-ts/domainobjs/ballot.ts | 131 ++ .../maci-ts/domainobjs/commands/PCommand.ts | 257 +++ .../maci-ts/domainobjs/commands/TCommand.ts | 65 + .../maci-ts/domainobjs/commands/index.ts | 3 + .../maci-ts/domainobjs/commands/types.ts | 38 + .../hardhat/maci-ts/domainobjs/constants.ts | 4 + packages/hardhat/maci-ts/domainobjs/index.ts | 41 + .../hardhat/maci-ts/domainobjs/keyPair.ts | 90 + .../hardhat/maci-ts/domainobjs/message.ts | 101 + .../hardhat/maci-ts/domainobjs/privateKey.ts | 89 + .../hardhat/maci-ts/domainobjs/publicKey.ts | 142 ++ .../hardhat/maci-ts/domainobjs/stateLeaf.ts | 159 ++ packages/hardhat/maci-ts/domainobjs/types.ts | 90 + .../maci-ts/domainobjs/verifyingKey.ts | 143 ++ .../templates/EmptyBallotRoots.sol.template | 26 + .../templates/MerkleZeros.sol.template | 11 + packages/hardhat/maci-ts/ts/abi.ts | 52 + packages/hardhat/maci-ts/ts/buildPoseidon.ts | 20 + packages/hardhat/maci-ts/ts/constants.ts | 6 + packages/hardhat/maci-ts/ts/deploy.ts | 378 ++++ packages/hardhat/maci-ts/ts/deployer.ts | 46 + .../maci-ts/ts/genEmptyBallotRootsContract.ts | 37 + packages/hardhat/maci-ts/ts/genMaciState.ts | 367 ++++ .../hardhat/maci-ts/ts/genZerosContract.ts | 68 + packages/hardhat/maci-ts/ts/index.ts | 23 + packages/hardhat/maci-ts/ts/types.ts | 185 ++ packages/hardhat/maci-ts/ts/utils.ts | 145 ++ packages/hardhat/package.json | 94 +- packages/hardhat/test/YourContract.ts | 28 - yarn.lock | 1559 +++++++++++++-- 111 files changed, 16784 insertions(+), 293 deletions(-) create mode 100644 packages/hardhat/constants.ts delete mode 100644 packages/hardhat/deploy/00_deploy_your_contract.ts create mode 100644 packages/hardhat/deploy/00_initial_voice_credit_proxy.ts create mode 100644 packages/hardhat/deploy/01_gatekeepers.ts create mode 100644 packages/hardhat/deploy/02_verifier.ts create mode 100644 packages/hardhat/deploy/03_topup_credit.ts create mode 100644 packages/hardhat/deploy/04_poseidon.ts create mode 100644 packages/hardhat/deploy/05_poll_factory.ts create mode 100644 packages/hardhat/deploy/06_message_processor_factory.ts create mode 100644 packages/hardhat/deploy/07_tally_factory.ts create mode 100644 packages/hardhat/deploy/08_subsidy_factory.ts create mode 100644 packages/hardhat/deploy/09_maci.ts create mode 100644 packages/hardhat/deploy/10_vk_registry.ts create mode 100755 packages/hardhat/maci-scripts/compileSol.sh create mode 100755 packages/hardhat/maci-scripts/writeMerkleZeroesContracts.sh create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/CalculateTotal.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/CeremonyParams.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/Ecdh.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/Hasher.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/IncrementalQuinTree.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/MessageToCommand.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/MessageValidator.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/PrivToPubKey.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/ProcessMessages.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/QuinCheckRoot.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/StateLeafAndBallotTransformer.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/TallyVotes.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/UnpackElement.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/VerifySignature.test.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/utils/constants.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/utils/types.ts create mode 100644 packages/hardhat/maci-ts/circuits/__tests__/utils/utils.ts create mode 100644 packages/hardhat/maci-ts/circuits/compile.ts create mode 100644 packages/hardhat/maci-ts/circuits/genZkeys.ts create mode 100644 packages/hardhat/maci-ts/circuits/index.ts create mode 100644 packages/hardhat/maci-ts/circuits/proofs.ts create mode 100644 packages/hardhat/maci-ts/circuits/types.ts create mode 100644 packages/hardhat/maci-ts/circuits/types/snarkjs.d.ts create mode 100644 packages/hardhat/maci-ts/circuits/utils.ts create mode 100644 packages/hardhat/maci-ts/core/MaciState.ts create mode 100644 packages/hardhat/maci-ts/core/Poll.ts create mode 100644 packages/hardhat/maci-ts/core/__benchmarks__/index.ts create mode 100644 packages/hardhat/maci-ts/core/__benchmarks__/utils/constants.ts create mode 100644 packages/hardhat/maci-ts/core/__tests__/MaciState.test.ts create mode 100644 packages/hardhat/maci-ts/core/__tests__/Poll.test.ts create mode 100644 packages/hardhat/maci-ts/core/__tests__/e2e.test.ts create mode 100644 packages/hardhat/maci-ts/core/__tests__/utils.test.ts create mode 100644 packages/hardhat/maci-ts/core/__tests__/utils/constants.ts create mode 100644 packages/hardhat/maci-ts/core/__tests__/utils/utils.ts create mode 100644 packages/hardhat/maci-ts/core/index.ts create mode 100644 packages/hardhat/maci-ts/core/utils/constants.ts create mode 100644 packages/hardhat/maci-ts/core/utils/errors.ts create mode 100644 packages/hardhat/maci-ts/core/utils/types.ts create mode 100644 packages/hardhat/maci-ts/core/utils/utils.ts create mode 100644 packages/hardhat/maci-ts/crypto/AccQueue.ts create mode 100644 packages/hardhat/maci-ts/crypto/__benchmarks__/index.ts create mode 100644 packages/hardhat/maci-ts/crypto/__benchmarks__/suites/trees.ts create mode 100644 packages/hardhat/maci-ts/crypto/__tests__/AccQueue.test.ts create mode 100644 packages/hardhat/maci-ts/crypto/__tests__/Crypto.test.ts create mode 100644 packages/hardhat/maci-ts/crypto/__tests__/IMT.test.ts create mode 100644 packages/hardhat/maci-ts/crypto/__tests__/Utils.test.ts create mode 100644 packages/hardhat/maci-ts/crypto/__tests__/utils.ts create mode 100644 packages/hardhat/maci-ts/crypto/babyjub.ts create mode 100644 packages/hardhat/maci-ts/crypto/bigIntUtils.ts create mode 100644 packages/hardhat/maci-ts/crypto/constants.ts create mode 100644 packages/hardhat/maci-ts/crypto/hashing.ts create mode 100644 packages/hardhat/maci-ts/crypto/index.ts create mode 100644 packages/hardhat/maci-ts/crypto/keys.ts create mode 100644 packages/hardhat/maci-ts/crypto/quinTree.ts create mode 100644 packages/hardhat/maci-ts/crypto/types.ts create mode 100644 packages/hardhat/maci-ts/crypto/utils.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/__tests__/ballot.test.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/__tests__/commands.test.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/__tests__/keypair.test.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/__tests__/message.test.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/__tests__/privateKey.test.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/__tests__/publicKey.test.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/__tests__/stateLeaf.test.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/__tests__/verifyingKey.test.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/ballot.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/commands/PCommand.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/commands/TCommand.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/commands/index.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/commands/types.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/constants.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/index.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/keyPair.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/message.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/privateKey.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/publicKey.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/stateLeaf.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/types.ts create mode 100644 packages/hardhat/maci-ts/domainobjs/verifyingKey.ts create mode 100644 packages/hardhat/maci-ts/templates/EmptyBallotRoots.sol.template create mode 100644 packages/hardhat/maci-ts/templates/MerkleZeros.sol.template create mode 100644 packages/hardhat/maci-ts/ts/abi.ts create mode 100644 packages/hardhat/maci-ts/ts/buildPoseidon.ts create mode 100644 packages/hardhat/maci-ts/ts/constants.ts create mode 100644 packages/hardhat/maci-ts/ts/deploy.ts create mode 100644 packages/hardhat/maci-ts/ts/deployer.ts create mode 100644 packages/hardhat/maci-ts/ts/genEmptyBallotRootsContract.ts create mode 100644 packages/hardhat/maci-ts/ts/genMaciState.ts create mode 100644 packages/hardhat/maci-ts/ts/genZerosContract.ts create mode 100644 packages/hardhat/maci-ts/ts/index.ts create mode 100644 packages/hardhat/maci-ts/ts/types.ts create mode 100644 packages/hardhat/maci-ts/ts/utils.ts delete mode 100644 packages/hardhat/test/YourContract.ts diff --git a/packages/hardhat/.gitignore b/packages/hardhat/.gitignore index be5e276..24fd0af 100644 --- a/packages/hardhat/.gitignore +++ b/packages/hardhat/.gitignore @@ -15,3 +15,5 @@ artifacts-zk cache-zk deployments/localhost + +zkeys diff --git a/packages/hardhat/constants.ts b/packages/hardhat/constants.ts new file mode 100644 index 0000000..ee518c7 --- /dev/null +++ b/packages/hardhat/constants.ts @@ -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; diff --git a/packages/hardhat/deploy/00_deploy_your_contract.ts b/packages/hardhat/deploy/00_deploy_your_contract.ts deleted file mode 100644 index 716fec7..0000000 --- a/packages/hardhat/deploy/00_deploy_your_contract.ts +++ /dev/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("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"]; diff --git a/packages/hardhat/deploy/00_initial_voice_credit_proxy.ts b/packages/hardhat/deploy/00_initial_voice_credit_proxy.ts new file mode 100644 index 0000000..a2ad0d8 --- /dev/null +++ b/packages/hardhat/deploy/00_initial_voice_credit_proxy.ts @@ -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"]; diff --git a/packages/hardhat/deploy/01_gatekeepers.ts b/packages/hardhat/deploy/01_gatekeepers.ts new file mode 100644 index 0000000..9f65d43 --- /dev/null +++ b/packages/hardhat/deploy/01_gatekeepers.ts @@ -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"]; diff --git a/packages/hardhat/deploy/02_verifier.ts b/packages/hardhat/deploy/02_verifier.ts new file mode 100644 index 0000000..9ebb4b2 --- /dev/null +++ b/packages/hardhat/deploy/02_verifier.ts @@ -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"]; diff --git a/packages/hardhat/deploy/03_topup_credit.ts b/packages/hardhat/deploy/03_topup_credit.ts new file mode 100644 index 0000000..e368a3f --- /dev/null +++ b/packages/hardhat/deploy/03_topup_credit.ts @@ -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"]; diff --git a/packages/hardhat/deploy/04_poseidon.ts b/packages/hardhat/deploy/04_poseidon.ts new file mode 100644 index 0000000..58528ac --- /dev/null +++ b/packages/hardhat/deploy/04_poseidon.ts @@ -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"]; diff --git a/packages/hardhat/deploy/05_poll_factory.ts b/packages/hardhat/deploy/05_poll_factory.ts new file mode 100644 index 0000000..28b9e2c --- /dev/null +++ b/packages/hardhat/deploy/05_poll_factory.ts @@ -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"]; diff --git a/packages/hardhat/deploy/06_message_processor_factory.ts b/packages/hardhat/deploy/06_message_processor_factory.ts new file mode 100644 index 0000000..d3ceffb --- /dev/null +++ b/packages/hardhat/deploy/06_message_processor_factory.ts @@ -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"]; diff --git a/packages/hardhat/deploy/07_tally_factory.ts b/packages/hardhat/deploy/07_tally_factory.ts new file mode 100644 index 0000000..4a9ba00 --- /dev/null +++ b/packages/hardhat/deploy/07_tally_factory.ts @@ -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"]; diff --git a/packages/hardhat/deploy/08_subsidy_factory.ts b/packages/hardhat/deploy/08_subsidy_factory.ts new file mode 100644 index 0000000..48bcd70 --- /dev/null +++ b/packages/hardhat/deploy/08_subsidy_factory.ts @@ -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"]; diff --git a/packages/hardhat/deploy/09_maci.ts b/packages/hardhat/deploy/09_maci.ts new file mode 100644 index 0000000..88015e5 --- /dev/null +++ b/packages/hardhat/deploy/09_maci.ts @@ -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(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"]; diff --git a/packages/hardhat/deploy/10_vk_registry.ts b/packages/hardhat/deploy/10_vk_registry.ts new file mode 100644 index 0000000..85dd4a3 --- /dev/null +++ b/packages/hardhat/deploy/10_vk_registry.ts @@ -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", 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"]; diff --git a/packages/hardhat/hardhat.config.ts b/packages/hardhat/hardhat.config.ts index d165b65..c68641f 100644 --- a/packages/hardhat/hardhat.config.ts +++ b/packages/hardhat/hardhat.config.ts @@ -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, diff --git a/packages/hardhat/maci-scripts/compileSol.sh b/packages/hardhat/maci-scripts/compileSol.sh new file mode 100755 index 0000000..7302069 --- /dev/null +++ b/packages/hardhat/maci-scripts/compileSol.sh @@ -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 diff --git a/packages/hardhat/maci-scripts/writeMerkleZeroesContracts.sh b/packages/hardhat/maci-scripts/writeMerkleZeroesContracts.sh new file mode 100755 index 0000000..8e7dc7c --- /dev/null +++ b/packages/hardhat/maci-scripts/writeMerkleZeroesContracts.sh @@ -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 diff --git a/packages/hardhat/maci-ts/circuits/__tests__/CalculateTotal.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/CalculateTotal.test.ts new file mode 100644 index 0000000..7ed1bea --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/CalculateTotal.test.ts @@ -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 }); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/CeremonyParams.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/CeremonyParams.test.ts new file mode 100644 index 0000000..52239ef --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/CeremonyParams.test.ts @@ -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); + }); + }); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/Ecdh.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/Ecdh.test.ts new file mode 100644 index 0000000..3985401 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/Ecdh.test.ts @@ -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", + ); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/Hasher.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/Hasher.test.ts new file mode 100644 index 0000000..d3b6784 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/Hasher.test.ts @@ -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()); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/IncrementalQuinTree.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/IncrementalQuinTree.test.ts new file mode 100644 index 0000000..05457d4 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/IncrementalQuinTree.test.ts @@ -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."); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/MessageToCommand.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/MessageToCommand.test.ts new file mode 100644 index 0000000..952cee1 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/MessageToCommand.test.ts @@ -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); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/MessageValidator.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/MessageValidator.test.ts new file mode 100644 index 0000000..3819345 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/MessageValidator.test.ts @@ -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"); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/PrivToPubKey.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/PrivToPubKey.test.ts new file mode 100644 index 0000000..14250f3 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/PrivToPubKey.test.ts @@ -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); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/ProcessMessages.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/ProcessMessages.test.ts new file mode 100644 index 0000000..9be4833 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/ProcessMessages.test.ts @@ -0,0 +1,1063 @@ +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, PubKey } from "../../domainobjs"; + +import { IProcessMessagesInputs } from "../types"; + +import { + STATE_TREE_DEPTH, + duration, + maxValues, + messageBatchSize, + treeDepths, + voiceCreditBalance, +} from "./utils/constants"; +import { getSignal, circomkitInstance } from "./utils/utils"; + +describe("ProcessMessage circuit", function test() { + this.timeout(900000); + + const coordinatorKeypair = new Keypair(); + + type ProcessMessageCircuitInputs = [ + "inputHash", + "packedVals", + "pollEndTimestamp", + "msgRoot", + "msgs", + "msgSubrootPathElements", + "coordPrivKey", + "coordPubKey", + "encPubKeys", + "currentStateRoot", + "currentStateLeaves", + "currentStateLeavesPathElements", + "currentSbCommitment", + "currentSbSalt", + "newSbCommitment", + "newSbSalt", + "currentBallotRoot", + "currentBallots", + "currentBallotsPathElements", + "currentVoteWeights", + "currentVoteWeightsPathElements", + ]; + + let circuit: WitnessTester; + + let circuitNonQv: WitnessTester; + + 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: [10, 2, 1, 2], + }); + + circuitNonQv = await circomkitInstance.WitnessTester("processMessagesNonQv", { + file: "processMessagesNonQv", + template: "ProcessMessagesNonQv", + params: [10, 2, 1, 2], + }); + + hasherCircuit = await circomkitInstance.WitnessTester("processMessageInputHasher", { + file: "processMessages", + template: "ProcessMessagesInputHasher", + }); + }); + + describe("1 user, 2 messages", () => { + const maciState = new MaciState(STATE_TREE_DEPTH); + const voteWeight = BigInt(9); + const voteOptionIndex = BigInt(1); + 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)!; + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + const nothing = new Message(1n, [ + 8370432830353022751713833565135785980866757267633941821328460903436894336785n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + ]); + + const encP = new PubKey([ + 10457101036533406547632367118273992217979173478358440826365724437999023779287n, + 19824078218392094440610104313265183977899662750282163392862422243483260492317n, + ]); + + poll.publishMessage(nothing, encP); + + // 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( + treeDepths.messageTreeSubDepth, + STATE_TREE_ARITY, + NOTHING_UP_MY_SLEEVE, + ); + accumulatorQueue.enqueue(nothing.hash(encP)); + accumulatorQueue.enqueue(message.hash(ecdhKeypair.pubKey)); + accumulatorQueue.enqueue(message2.hash(ecdhKeypair2.pubKey)); + accumulatorQueue.mergeSubRoots(0); + accumulatorQueue.merge(treeDepths.messageTreeDepth); + + expect(poll.messageTree.root.toString()).to.be.eq( + accumulatorQueue.getMainRoots()[treeDepths.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(STATE_TREE_DEPTH, 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, + 3, + ); + + // 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("1 user, 2 messages (non-quadratic voting)", () => { + const maciState = new MaciState(STATE_TREE_DEPTH); + 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)!; + 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); + }); + + 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(STATE_TREE_DEPTH, 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, false) as unknown as IProcessMessagesInputs; + + // Calculate the witness + const witness = await circuitNonQv.calculateWitness(inputs); + await circuitNonQv.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("2 users, 1 message", () => { + const maciState = new MaciState(STATE_TREE_DEPTH); + let pollId: bigint; + let poll: Poll; + const messages: Message[] = []; + const commands: PCommand[] = []; + + before(() => { + // Sign up and publish + const userKeypair = new Keypair(new PrivKey(BigInt(123))); + const userKeypair2 = new Keypair(new PrivKey(BigInt(456))); + + maciState.signUp( + userKeypair.pubKey, + voiceCreditBalance, + BigInt(1), // BigInt(Math.floor(Date.now() / 1000)), + ); + maciState.signUp( + userKeypair2.pubKey, + voiceCreditBalance, + BigInt(1), // BigInt(Math.floor(Date.now() / 1000)), + ); + + pollId = maciState.deployPoll( + BigInt(2 + duration), // BigInt(Math.floor(Date.now() / 1000) + duration), + maxValues, + treeDepths, + messageBatchSize, + coordinatorKeypair, + ); + + poll = maciState.polls.get(pollId)!; + + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + const command = new PCommand( + BigInt(1), + userKeypair.pubKey, + BigInt(0), // voteOptionIndex, + BigInt(1), // 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.getRoot(treeDepths.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(STATE_TREE_DEPTH, 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()); + }); + }); + + describe("1 user, key-change", () => { + const maciState = new MaciState(STATE_TREE_DEPTH); + const voteWeight = BigInt(9); + let stateIndex: number; + let pollId: bigint; + let poll: Poll; + const messages: Message[] = []; + const commands: PCommand[] = []; + + const NUM_BATCHES = 2; + + before(() => { + // Sign up and publish + const userKeypair = new Keypair(new PrivKey(BigInt(123))); + const userKeypair2 = new Keypair(new PrivKey(BigInt(456))); + + stateIndex = maciState.signUp( + userKeypair.pubKey, + voiceCreditBalance, + BigInt(1), // BigInt(Math.floor(Date.now() / 1000)), + ); + + pollId = maciState.deployPoll( + BigInt(2 + duration), // BigInt(Math.floor(Date.now() / 1000) + duration), + maxValues, + treeDepths, + messageBatchSize, + coordinatorKeypair, + ); + + poll = maciState.polls.get(pollId)!; + + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // Vote for option 0 + const command = new PCommand( + BigInt(stateIndex), // BigInt(1), + userKeypair.pubKey, + BigInt(0), // 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); + + // Vote for option 1 + const command2 = new PCommand( + BigInt(stateIndex), + userKeypair2.pubKey, + BigInt(1), // voteOptionIndex, + voteWeight, // vote weight + BigInt(2), // nonce + BigInt(pollId), + ); + const signature2 = command2.sign(userKeypair2.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); + + // Change key + const command3 = new PCommand( + BigInt(stateIndex), // BigInt(1), + userKeypair2.pubKey, + BigInt(1), // voteOptionIndex, + BigInt(0), // vote weight + BigInt(1), // nonce + BigInt(pollId), + ); + + const signature3 = command3.sign(userKeypair.privKey); + + const ecdhKeypair3 = new Keypair(); + const sharedKey3 = Keypair.genEcdhSharedKey(ecdhKeypair3.privKey, coordinatorKeypair.pubKey); + const message3 = command3.encrypt(signature3, sharedKey3); + messages.push(message3); + commands.push(command3); + poll.publishMessage(message3, ecdhKeypair3.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.enqueue(message2.hash(ecdhKeypair2.pubKey)); + accumulatorQueue.enqueue(message3.hash(ecdhKeypair3.pubKey)); + accumulatorQueue.mergeSubRoots(0); + accumulatorQueue.merge(treeDepths.messageTreeDepth); + + expect(poll.messageTree.root.toString()).to.be.eq( + accumulatorQueue.getRoot(treeDepths.messageTreeDepth)?.toString(), + ); + }); + + describe(`1 user, ${messageBatchSize * NUM_BATCHES} messages`, () => { + it("should produce the correct state root and ballot root", async () => { + const state = new MaciState(STATE_TREE_DEPTH); + const userKeypair = new Keypair(); + const index = state.signUp(userKeypair.pubKey, voiceCreditBalance, BigInt(Math.floor(Date.now() / 1000))); + + // Sign up and publish + const id = state.deployPoll( + BigInt(Math.floor(Date.now() / 1000) + duration), + maxValues, + treeDepths, + messageBatchSize, + coordinatorKeypair, + ); + + const selectedPoll = state.polls.get(id); + + selectedPoll?.updatePoll(BigInt(state.stateLeaves.length)); + + // Second batch is not a full batch + const numMessages = messageBatchSize * NUM_BATCHES - 1; + for (let i = 0; i < numMessages; i += 1) { + const command = new PCommand( + BigInt(index), + userKeypair.pubKey, + BigInt(i), // vote option index + BigInt(1), // vote weight + BigInt(numMessages - i), // nonce + BigInt(id), + ); + + const signature = command.sign(userKeypair.privKey); + + const ecdhKeypair = new Keypair(); + const sharedKey = Keypair.genEcdhSharedKey(ecdhKeypair.privKey, coordinatorKeypair.pubKey); + const message = command.encrypt(signature, sharedKey); + selectedPoll?.publishMessage(message, ecdhKeypair.pubKey); + } + + for (let i = 0; i < 2; i += 1) { + const inputs = selectedPoll?.processMessages(id) as unknown as IProcessMessagesInputs; + // eslint-disable-next-line no-await-in-loop + const witness = await circuit.calculateWitness(inputs); + // eslint-disable-next-line no-await-in-loop + await circuit.expectConstraintPass(witness); + } + }); + }); + }); + + describe("1 user, 1 topup, 2 messages", () => { + const maciState = new MaciState(STATE_TREE_DEPTH); + const voteOptionIndex = BigInt(0); + let stateIndex: bigint; + let pollId: bigint; + let poll: Poll; + const userKeypair = new Keypair(); + + before(() => { + // Sign up and publish + 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(BigInt(maciState.stateLeaves.length)); + }); + + it("should work when publishing 2 vote messages and a topup (the second vote uses more than initial voice credit balance)", async () => { + // First command (valid) + const command1 = new PCommand( + stateIndex, // BigInt(1), + userKeypair.pubKey, + voteOptionIndex + 1n, // voteOptionIndex, + 5n, // vote weight + BigInt(2), // nonce + BigInt(pollId), + ); + + const signature1 = command1.sign(userKeypair.privKey); + + const ecdhKeypair1 = new Keypair(); + const sharedKey1 = Keypair.genEcdhSharedKey(ecdhKeypair1.privKey, coordinatorKeypair.pubKey); + const message1 = command1.encrypt(signature1, sharedKey1); + + poll.publishMessage(message1, ecdhKeypair1.pubKey); + + poll.topupMessage(new Message(2n, [BigInt(stateIndex), 50n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n])); + + // First command (valid) + const command = new PCommand( + stateIndex, // BigInt(1), + userKeypair.pubKey, + voteOptionIndex, // voteOptionIndex, + 10n, // 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); + + poll.publishMessage(message, ecdhKeypair.pubKey); + + const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs; + const witness = await circuit.calculateWitness(inputs); + await circuit.expectConstraintPass(witness); + }); + }); + + describe("1 user, 2 messages", () => { + const maciState = new MaciState(STATE_TREE_DEPTH); + const voteOptionIndex = 1n; + 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)!; + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + const nothing = new Message(1n, [ + 8370432830353022751713833565135785980866757267633941821328460903436894336785n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + ]); + + const encP = new PubKey([ + 10457101036533406547632367118273992217979173478358440826365724437999023779287n, + 19824078218392094440610104313265183977899662750282163392862422243483260492317n, + ]); + + poll.publishMessage(nothing, encP); + + // First command (valid) + const command = new PCommand( + stateIndex, // BigInt(1), + userKeypair.pubKey, + 1n, // voteOptionIndex, + 2n, // vote weight + 2n, // nonce + 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, + 9n, // vote weight 9 ** 2 = 81 + 1n, // nonce + 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); + }); + + 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(STATE_TREE_DEPTH, 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()); + }); + }); + + describe("1 user, 2 messages in different batches", () => { + const maciState = new MaciState(STATE_TREE_DEPTH); + const voteOptionIndex = 1n; + 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)!; + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + const nothing = new Message(1n, [ + 8370432830353022751713833565135785980866757267633941821328460903436894336785n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + ]); + + const encP = new PubKey([ + 10457101036533406547632367118273992217979173478358440826365724437999023779287n, + 19824078218392094440610104313265183977899662750282163392862422243483260492317n, + ]); + + poll.publishMessage(nothing, encP); + + // First command (valid) + const command = new PCommand( + stateIndex, // BigInt(1), + userKeypair.pubKey, + 1n, // voteOptionIndex, + 2n, // vote weight + 2n, // nonce + 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); + + // fill the batch with nothing messages + for (let i = 0; i < messageBatchSize - 1; i += 1) { + poll.publishMessage(nothing, encP); + } + + // Second command (valid) in second batch (which is first due to reverse processing) + const command2 = new PCommand( + stateIndex, + userKeypair.pubKey, + voteOptionIndex, // voteOptionIndex, + 9n, // vote weight 9 ** 2 = 81 + 1n, // nonce + 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); + }); + + 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(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash5); + + ballotTree.insert(emptyBallot.hash()); + + poll.stateLeaves.forEach(() => { + ballotTree.insert(emptyBallotHash); + }); + + while (poll.hasUnprocessedMessages()) { + const currentStateRoot = poll.stateTree?.root; + const currentBallotRoot = ballotTree.root; + const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs; + + // Calculate the witness + // eslint-disable-next-line no-await-in-loop + const witness = await circuit.calculateWitness(inputs); + // eslint-disable-next-line no-await-in-loop + 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()); + } + }); + }); + + describe("1 user, 3 messages in different batches", () => { + const maciState = new MaciState(STATE_TREE_DEPTH); + const voteOptionIndex = 1n; + 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)!; + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + const nothing = new Message(1n, [ + 8370432830353022751713833565135785980866757267633941821328460903436894336785n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + 0n, + ]); + + const encP = new PubKey([ + 10457101036533406547632367118273992217979173478358440826365724437999023779287n, + 19824078218392094440610104313265183977899662750282163392862422243483260492317n, + ]); + + poll.publishMessage(nothing, encP); + + const commandFinal = new PCommand( + stateIndex, // BigInt(1), + userKeypair.pubKey, + 1n, // voteOptionIndex, + 1n, // vote weight + 3n, // nonce + pollId, + ); + + const signatureFinal = commandFinal.sign(userKeypair.privKey); + + const ecdhKeypairFinal = new Keypair(); + const sharedKeyFinal = Keypair.genEcdhSharedKey(ecdhKeypairFinal.privKey, coordinatorKeypair.pubKey); + const messageFinal = commandFinal.encrypt(signatureFinal, sharedKeyFinal); + messages.push(messageFinal); + commands.push(commandFinal); + + poll.publishMessage(messageFinal, ecdhKeypairFinal.pubKey); + + // First command (valid) + const command = new PCommand( + stateIndex, // BigInt(1), + userKeypair.pubKey, + 1n, // voteOptionIndex, + 2n, // vote weight + 2n, // nonce + 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); + + // fill the batch with nothing messages + for (let i = 0; i < messageBatchSize - 1; i += 1) { + poll.publishMessage(nothing, encP); + } + + // Second command (valid) in second batch (which is first due to reverse processing) + const command2 = new PCommand( + stateIndex, + userKeypair.pubKey, + voteOptionIndex, // voteOptionIndex, + 9n, // vote weight 9 ** 2 = 81 + 1n, // nonce + 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); + }); + + 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(STATE_TREE_DEPTH, emptyBallot.hash(), STATE_TREE_ARITY, hash5); + + ballotTree.insert(emptyBallot.hash()); + + poll.stateLeaves.forEach(() => { + ballotTree.insert(emptyBallotHash); + }); + + while (poll.hasUnprocessedMessages()) { + const currentStateRoot = poll.stateTree?.root; + const currentBallotRoot = ballotTree.root; + const inputs = poll.processMessages(pollId) as unknown as IProcessMessagesInputs; + + // Calculate the witness + // eslint-disable-next-line no-await-in-loop + const witness = await circuit.calculateWitness(inputs); + // eslint-disable-next-line no-await-in-loop + 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()); + } + }); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/QuinCheckRoot.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/QuinCheckRoot.test.ts new file mode 100644 index 0000000..b4e3d00 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/QuinCheckRoot.test.ts @@ -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(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(leavesPerNode ** treeDepth - 1).fill(5n); + + const circuitInputs = { + leaves, + }; + + await expect(circuit.calculateWitness(circuitInputs)).to.be.rejectedWith( + "Not enough values for input signal leaves", + ); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/StateLeafAndBallotTransformer.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/StateLeafAndBallotTransformer.test.ts new file mode 100644 index 0000000..a02801e --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/StateLeafAndBallotTransformer.test.ts @@ -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"); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/TallyVotes.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/TallyVotes.test.ts new file mode 100644 index 0000000..8a01de7 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/TallyVotes.test.ts @@ -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; + + let circuitNonQv: WitnessTester; + + 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); + } + }); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/UnpackElement.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/UnpackElement.test.ts new file mode 100644 index 0000000..ae06b98 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/UnpackElement.test.ts @@ -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()); + } + }); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/VerifySignature.test.ts b/packages/hardhat/maci-ts/circuits/__tests__/VerifySignature.test.ts new file mode 100644 index 0000000..88473d3 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/VerifySignature.test.ts @@ -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"); + }); +}); diff --git a/packages/hardhat/maci-ts/circuits/__tests__/utils/constants.ts b/packages/hardhat/maci-ts/circuits/__tests__/utils/constants.ts new file mode 100644 index 0000000..460c85e --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/utils/constants.ts @@ -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; diff --git a/packages/hardhat/maci-ts/circuits/__tests__/utils/types.ts b/packages/hardhat/maci-ts/circuits/__tests__/utils/types.ts new file mode 100644 index 0000000..bacbeb6 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/utils/types.ts @@ -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; +} diff --git a/packages/hardhat/maci-ts/circuits/__tests__/utils/utils.ts b/packages/hardhat/maci-ts/circuits/__tests__/utils/utils.ts new file mode 100644 index 0000000..d37dbfb --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/__tests__/utils/utils.ts @@ -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 => { + 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]); +}; diff --git a/packages/hardhat/maci-ts/circuits/compile.ts b/packages/hardhat/maci-ts/circuits/compile.ts new file mode 100644 index 0000000..f88a2ac --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/compile.ts @@ -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 => { + // 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); + } + })(); +} diff --git a/packages/hardhat/maci-ts/circuits/genZkeys.ts b/packages/hardhat/maci-ts/circuits/genZkeys.ts new file mode 100644 index 0000000..d56a31f --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/genZkeys.ts @@ -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 => { + // 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); + } + })(); +} diff --git a/packages/hardhat/maci-ts/circuits/index.ts b/packages/hardhat/maci-ts/circuits/index.ts new file mode 100644 index 0000000..d631e38 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/index.ts @@ -0,0 +1,3 @@ +export { genProof, verifyProof, extractVk } from "./proofs"; +export { cleanThreads } from "./utils"; +export type { ISnarkJSVerificationKey } from "./types"; diff --git a/packages/hardhat/maci-ts/circuits/proofs.ts b/packages/hardhat/maci-ts/circuits/proofs.ts new file mode 100644 index 0000000..d3b88ea --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/proofs.ts @@ -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 => { + // 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 => { + 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 => { + const vk = await zKey.exportVerificationKey(zkeyPath); + await cleanThreads(); + return vk; +}; diff --git a/packages/hardhat/maci-ts/circuits/types.ts b/packages/hardhat/maci-ts/circuits/types.ts new file mode 100644 index 0000000..8929a14 --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/types.ts @@ -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 }; diff --git a/packages/hardhat/maci-ts/circuits/types/snarkjs.d.ts b/packages/hardhat/maci-ts/circuits/types/snarkjs.d.ts new file mode 100644 index 0000000..19cb66a --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/types/snarkjs.d.ts @@ -0,0 +1,50 @@ +declare module "snarkjs" { + export type NumericString = string; + export type PublicSignals = Record; + 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; + } + + export namespace groth16 { + function verify( + vk_verifier: ISnarkJSVerificationKey, + publicSignals: PublicSignals, + proof: Groth16Proof, + logger?: unknown, + ): Promise; + + function fullProve( + input: PublicSignals, + wasmFile: string, + zkeyFileName: string, + logger?: unknown, + ): Promise; + } +} diff --git a/packages/hardhat/maci-ts/circuits/utils.ts b/packages/hardhat/maci-ts/circuits/utils.ts new file mode 100644 index 0000000..72a895d --- /dev/null +++ b/packages/hardhat/maci-ts/circuits/utils.ts @@ -0,0 +1,35 @@ +import os from "os"; + +declare global { + interface ITerminatable { + terminate: () => Promise; + } + + // 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 => { + // 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), + ); +}; diff --git a/packages/hardhat/maci-ts/core/MaciState.ts b/packages/hardhat/maci-ts/core/MaciState.ts new file mode 100644 index 0000000..bb38c48 --- /dev/null +++ b/packages/hardhat/maci-ts/core/MaciState.ts @@ -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 = new Map(); + + // 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; + } +} diff --git a/packages/hardhat/maci-ts/core/Poll.ts b/packages/hardhat/maci-ts/core/Poll.ts new file mode 100644 index 0000000..228bb8a --- /dev/null +++ b/packages/hardhat/maci-ts/core/Poll.ts @@ -0,0 +1,1666 @@ +import { + IncrementalQuinTree, + genRandomSalt, + SNARK_FIELD_SIZE, + NOTHING_UP_MY_SLEEVE, + hashLeftRight, + hash3, + hash5, + sha256Hash, + stringifyBigInts, + genTreeCommitment, +} from "../crypto"; +import { + PCommand, + TCommand, + Keypair, + Ballot, + PubKey, + PrivKey, + Message, + blankStateLeaf, + type ICommand, + type StateLeaf, + type IMessageContractParams, + type IJsonCommand, + type IJsonPCommand, + type IJsonTCommand, + blankStateLeafHash, +} from "../domainobjs"; + +import assert from "assert"; + +import type { MaciState } from "./MaciState"; +import type { + CircuitInputs, + TreeDepths, + MaxValues, + BatchSizes, + IPoll, + IJsonPoll, + IProcessMessagesOutput, + ITallyCircuitInputs, + ISubsidyCircuitInputs, + IProcessMessagesCircuitInputs, +} from "./utils/types"; +import type { PathElements } from "../crypto"; + +import { STATE_TREE_ARITY, MESSAGE_TREE_ARITY } from "./utils/constants"; +import { ProcessMessageErrors, ProcessMessageError } from "./utils/errors"; +import { packTallyVotesSmallVals, packSubsidySmallVals } from "./utils/utils"; + +/** + * A representation of the Poll contract. + */ +export class Poll implements IPoll { + // Note that we only store the PubKey on-chain while this class stores the + // Keypair for the sake of convenience + coordinatorKeypair: Keypair; + + treeDepths: TreeDepths; + + batchSizes: BatchSizes; + + maxValues: MaxValues; + + // the depth of the state tree + stateTreeDepth: number; + + pollEndTimestamp: bigint; + + ballots: Ballot[] = []; + + ballotTree?: IncrementalQuinTree; + + messages: Message[] = []; + + messageTree: IncrementalQuinTree; + + commands: ICommand[] = []; + + encPubKeys: PubKey[] = []; + + stateCopied = false; + + stateLeaves: StateLeaf[] = [blankStateLeaf]; + + stateTree?: IncrementalQuinTree; + + // For message processing + numBatchesProcessed = 0; + + currentMessageBatchIndex?: number; + + maciStateRef: MaciState; + + pollId: bigint; + + sbSalts: Record = {}; + + resultRootSalts: Record = {}; + + preVOSpentVoiceCreditsRootSalts: Record = {}; + + spentVoiceCreditSubtotalSalts: Record = {}; + + // For vote tallying + tallyResult: bigint[] = []; + + perVOSpentVoiceCredits: bigint[] = []; + + numBatchesTallied = 0; + + totalSpentVoiceCredits = 0n; + + // For coefficient and subsidy calculation + subsidy: bigint[] = []; // size: M, M is number of vote options + + subsidySalts: Record = {}; + + rbi = 0; // row batch index + + cbi = 0; // column batch index + + MM = 50; // adjustable parameter + + WW = 4; // number of digits for float representation + + // an empty ballot and its hash to be used as zero value in the + // ballot tree + emptyBallot: Ballot; + + emptyBallotHash?: bigint; + + // how many users signed up + private numSignups = 0n; + + /** + * Constructs a new Poll object. + * @param pollEndTimestamp - The Unix timestamp at which the poll ends. + * @param coordinatorKeypair - The keypair of the coordinator. + * @param treeDepths - The depths of the trees used in the poll. + * @param batchSizes - The sizes of the batches used in the poll. + * @param maxValues - The maximum values the MACI circuits can accept. + * @param maciStateRef - The reference to the MACI state. + */ + constructor( + pollEndTimestamp: bigint, + coordinatorKeypair: Keypair, + treeDepths: TreeDepths, + batchSizes: BatchSizes, + maxValues: MaxValues, + maciStateRef: MaciState, + ) { + this.pollEndTimestamp = pollEndTimestamp; + this.coordinatorKeypair = coordinatorKeypair; + this.treeDepths = treeDepths; + this.batchSizes = batchSizes; + this.maxValues = maxValues; + this.maciStateRef = maciStateRef; + this.pollId = BigInt(maciStateRef.polls.size); + this.stateTreeDepth = maciStateRef.stateTreeDepth; + + this.messageTree = new IncrementalQuinTree( + this.treeDepths.messageTreeDepth, + NOTHING_UP_MY_SLEEVE, + MESSAGE_TREE_ARITY, + hash5, + ); + + this.tallyResult = new Array(this.maxValues.maxVoteOptions).fill(0n) as bigint[]; + this.perVOSpentVoiceCredits = new Array(this.maxValues.maxVoteOptions).fill(0n) as bigint[]; + this.subsidy = new Array(this.maxValues.maxVoteOptions).fill(0n) as bigint[]; + + // we put a blank state leaf to prevent a DoS attack + this.emptyBallot = Ballot.genBlankBallot(this.maxValues.maxVoteOptions, treeDepths.voteOptionTreeDepth); + this.ballots.push(this.emptyBallot); + } + + /** + * Update a Poll with data from MaciState. + * This is the step where we copy the state from the MaciState instance, + * and set the number of signups we have so far. + */ + updatePoll = (numSignups: bigint): void => { + // there might be occasions where we fetch logs after new signups have been made + // logs are fetched (and MaciState/Poll created locally) after stateAq have been + // merged in. If someone signs up after that and we fetch that record + // then we won't be able to verify the processing on chain as the data will + // not match. For this, we must only copy up to the number of signups + + // Copy the state tree, ballot tree, state leaves, and ballot leaves + + // start by setting the number of signups + this.setNumSignups(numSignups); + // copy up to numSignups state leaves + this.stateLeaves = this.maciStateRef.stateLeaves.slice(0, Number(this.numSignups)).map(x => x.copy()); + + // create a new state tree + this.stateTree = new IncrementalQuinTree(this.stateTreeDepth, blankStateLeafHash, STATE_TREE_ARITY, hash5); + // add all leaves + this.stateLeaves.forEach(stateLeaf => { + this.stateTree?.insert(stateLeaf.hash()); + }); + + // Create as many ballots as state leaves + this.emptyBallotHash = this.emptyBallot.hash(); + this.ballotTree = new IncrementalQuinTree(this.stateTreeDepth, this.emptyBallotHash, STATE_TREE_ARITY, hash5); + this.ballotTree.insert(this.emptyBallot.hash()); + + // we fill the ballotTree with empty ballots hashes to match the number of signups in the tree + while (this.ballots.length < this.stateLeaves.length) { + this.ballotTree.insert(this.emptyBallotHash); + this.ballots.push(this.emptyBallot); + } + + this.stateCopied = true; + }; + + /** + * Process one message. + * @param message - The message to process. + * @param encPubKey - The public key associated with the encryption private key. + * @returns A number of variables which will be used in the zk-SNARK circuit. + */ + processMessage = (message: Message, encPubKey: PubKey, qv = true): IProcessMessagesOutput => { + try { + // Decrypt the message + const sharedKey = Keypair.genEcdhSharedKey(this.coordinatorKeypair.privKey, encPubKey); + + const { command, signature } = PCommand.decrypt(message, sharedKey); + + const stateLeafIndex = command.stateIndex; + + // If the state tree index in the command is invalid, do nothing + if ( + stateLeafIndex >= BigInt(this.ballots.length) || + stateLeafIndex < 1n || + stateLeafIndex >= BigInt(this.stateTree?.nextIndex || -1) + ) { + throw new ProcessMessageError(ProcessMessageErrors.InvalidStateLeafIndex); + } + + // The user to update (or not) + const stateLeaf = this.stateLeaves[Number(stateLeafIndex)]; + + // The ballot to update (or not) + const ballot = this.ballots[Number(stateLeafIndex)]; + + // If the signature is invalid, do nothing + if (!command.verifySignature(signature, stateLeaf.pubKey)) { + throw new ProcessMessageError(ProcessMessageErrors.InvalidSignature); + } + + // If the nonce is invalid, do nothing + if (command.nonce !== ballot.nonce + 1n) { + throw new ProcessMessageError(ProcessMessageErrors.InvalidNonce); + } + + // If the vote option index is invalid, do nothing + if (command.voteOptionIndex < 0n || command.voteOptionIndex >= BigInt(this.maxValues.maxVoteOptions)) { + throw new ProcessMessageError(ProcessMessageErrors.InvalidVoteOptionIndex); + } + + const voteOptionIndex = Number(command.voteOptionIndex); + const originalVoteWeight = ballot.votes[voteOptionIndex]; + + // the voice credits left are: + // voiceCreditsBalance (how many the user has) + + // voiceCreditsPreviouslySpent (the original vote weight for this option) ** 2 - + // command.newVoteWeight ** 2 (the new vote weight squared) + // basically we are replacing the previous vote weight for this + // particular vote option with the new one + // but we need to ensure that we are not going >= balance + // @note that above comment is valid for quadratic voting + // for non quadratic voting, we simply remove the exponentiation + const voiceCreditsLeft = qv + ? stateLeaf.voiceCreditBalance + + originalVoteWeight * originalVoteWeight - + command.newVoteWeight * command.newVoteWeight + : stateLeaf.voiceCreditBalance + originalVoteWeight - command.newVoteWeight; + + // If the remaining voice credits is insufficient, do nothing + if (voiceCreditsLeft < 0n) { + throw new ProcessMessageError(ProcessMessageErrors.InsufficientVoiceCredits); + } + + // Deep-copy the state leaf and update its attributes + const newStateLeaf = stateLeaf.copy(); + newStateLeaf.voiceCreditBalance = voiceCreditsLeft; + // if the key changes, this is effectively a key-change message too + newStateLeaf.pubKey = command.newPubKey.copy(); + + // Deep-copy the ballot and update its attributes + const newBallot = ballot.copy(); + // increase the nonce + newBallot.nonce += 1n; + // we change the vote for this exact vote option + newBallot.votes[voteOptionIndex] = command.newVoteWeight; + + // calculate the path elements for the state tree given the original state tree (before any changes) + // changes could effectively be made by this new vote - either a key change or vote change + // would result in a different state leaf + const originalStateLeafPathElements = this.stateTree?.genProof(Number(stateLeafIndex)).pathElements; + // calculate the path elements for the ballot tree given the original ballot tree (before any changes) + // changes could effectively be made by this new ballot + const originalBallotPathElements = this.ballotTree?.genProof(Number(stateLeafIndex)).pathElements; + + // create a new quinary tree where we insert the votes of the origin (up until this message is processed) ballot + const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, STATE_TREE_ARITY, hash5); + for (let i = 0; i < this.ballots[0].votes.length; i += 1) { + vt.insert(ballot.votes[i]); + } + // calculate the path elements for the vote option tree given the original vote option tree (before any changes) + const originalVoteWeightsPathElements = vt.genProof(voteOptionIndex).pathElements; + // we return the data which is then to be used in the processMessage circuit + // to generate a proof of processing + return { + stateLeafIndex: Number(stateLeafIndex), + newStateLeaf, + originalStateLeaf: stateLeaf.copy(), + originalStateLeafPathElements, + originalVoteWeight, + originalVoteWeightsPathElements, + newBallot, + originalBallot: ballot.copy(), + originalBallotPathElements, + command, + }; + } catch (e) { + if (e instanceof ProcessMessageError) { + throw e; + } else { + throw new ProcessMessageError(ProcessMessageErrors.FailedDecryption); + } + } + }; + + /** + * Top up the voice credit balance of a user. + * @param message - The message to top up the voice credit balance + */ + topupMessage = (message: Message): void => { + assert(message.msgType === 2n, "A Topup message must have msgType 2"); + + message.data.forEach(d => { + assert(d < SNARK_FIELD_SIZE, "The message data is not in the correct range"); + }); + + const padKey = new PubKey([ + BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"), + BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), + ]); + + // save the message + this.messages.push(message); + // save the pad key + this.encPubKeys.push(padKey); + // insert the message into the message tree + this.messageTree.insert(message.hash(padKey)); + + // we create a topup command and save it + const command = new TCommand(message.data[0], message.data[1], BigInt(this.pollId)); + this.commands.push(command as ICommand); + }; + + /** + * Inserts a Message and the corresponding public key used to generate the + * ECDH shared key which was used to encrypt said message. + * @param message - The message to insert + * @param encPubKey - The public key used to encrypt the message + */ + publishMessage = (message: Message, encPubKey: PubKey): void => { + assert(message.msgType === 1n, "A vote or key change message must have msgType 1"); + assert( + encPubKey.rawPubKey[0] < SNARK_FIELD_SIZE && encPubKey.rawPubKey[1] < SNARK_FIELD_SIZE, + "The public key is not in the correct range", + ); + + message.data.forEach(d => { + assert(d < SNARK_FIELD_SIZE, "The message data is not in the correct range"); + }); + + // store the encryption pub key + this.encPubKeys.push(encPubKey); + // store the message locally + this.messages.push(message); + // add the message hash to the message tree + this.messageTree.insert(message.hash(encPubKey)); + + // Decrypt the message and store the Command + // step 1. we generate the shared key + const sharedKey = Keypair.genEcdhSharedKey(this.coordinatorKeypair.privKey, encPubKey); + try { + // step 2. we decrypt it + const { command } = PCommand.decrypt(message, sharedKey); + // step 3. we store it in the commands array + this.commands.push(command as ICommand); + } catch (e) { + // if there is an error we store an empty command + const keyPair = new Keypair(); + const command = new PCommand(0n, keyPair.pubKey, 0n, 0n, 0n, 0n, 0n); + this.commands.push(command as ICommand); + } + }; + + /** + * This method checks if there are any unprocessed messages in the Poll instance. + * @returns Returns true if the number of processed batches is + * less than the total number of batches, false otherwise. + */ + hasUnprocessedMessages = (): boolean => { + const batchSize = this.batchSizes.messageBatchSize; + + let totalBatches = this.messages.length <= batchSize ? 1 : Math.floor(this.messages.length / batchSize); + + if (this.messages.length > batchSize && this.messages.length % batchSize > 0) { + totalBatches += 1; + } + + return this.numBatchesProcessed < totalBatches; + }; + + /** + * Process _batchSize messages starting from the saved index. This + * function will process messages even if the number of messages is not an + * exact multiple of _batchSize. e.g. if there are 10 messages, index is + * 8, and _batchSize is 4, this function will only process the last two + * messages in this.messages, and finally update the zeroth state leaf. + * Note that this function will only process as many state leaves as there + * are ballots to prevent accidental inclusion of a new user after this + * poll has concluded. + * @param pollId The ID of the poll associated with the messages to + * process + * @param quiet - Whether to log errors or not + * @returns stringified circuit inputs + */ + processMessages = (pollId: bigint, qv = true, quiet = true): IProcessMessagesCircuitInputs => { + assert(this.hasUnprocessedMessages(), "No more messages to process"); + + const batchSize = this.batchSizes.messageBatchSize; + + if (this.numBatchesProcessed === 0) { + // The starting index of the batch of messages to process. + // Note that we process messages in reverse order. + // e.g if there are 8 messages and the batch size is 5, then + // the starting index should be 5. + assert( + this.currentMessageBatchIndex === undefined, + "The current message batch index should not be defined if this is the first batch", + ); + // Prevent other polls from being processed until this poll has + // been fully processed + this.maciStateRef.pollBeingProcessed = true; + this.maciStateRef.currentPollBeingProcessed = pollId; + } + + // Only allow one poll to be processed at a time + if (this.maciStateRef.pollBeingProcessed) { + assert(this.maciStateRef.currentPollBeingProcessed === pollId, "Another poll is currently being processed"); + } + + if (this.numBatchesProcessed === 0) { + const r = this.messages.length % batchSize; + + this.currentMessageBatchIndex = this.messages.length; + + // if there are messages + if (this.currentMessageBatchIndex > 0) { + if (r === 0) { + this.currentMessageBatchIndex -= batchSize; + } else { + this.currentMessageBatchIndex -= r; + } + } + + this.sbSalts[this.currentMessageBatchIndex] = 0n; + } + + // The starting index must be valid + assert(this.currentMessageBatchIndex! >= 0, "The starting index must be >= 0"); + assert(this.currentMessageBatchIndex! % batchSize === 0, "The starting index must be a multiple of the batch size"); + + // ensure we copy the state from MACI when we start processing the + // first batch + if (!this.stateCopied) { + throw new Error("You must update the poll with the correct data first"); + } + + // Generate circuit inputs + const circuitInputs = stringifyBigInts( + this.genProcessMessagesCircuitInputsPartial(this.currentMessageBatchIndex!), + ) as CircuitInputs; + + // we want to store the state leaves at this point in time + // and the path elements of the state tree + const currentStateLeaves: StateLeaf[] = []; + const currentStateLeavesPathElements: PathElements[] = []; + + // we want to store the ballots at this point in time + // and the path elements of the ballot tree + const currentBallots: Ballot[] = []; + const currentBallotsPathElements: PathElements[] = []; + + // we want to store the vote weights at this point in time + // and the path elements of the vote weight tree + const currentVoteWeights: bigint[] = []; + const currentVoteWeightsPathElements: PathElements[] = []; + + // loop through the batch of messages + for (let i = 0; i < batchSize; i += 1) { + // we process the messages in reverse order + const idx = this.currentMessageBatchIndex! + batchSize - i - 1; + assert(idx >= 0, "The message index must be >= 0"); + let message: Message; + let encPubKey: PubKey; + if (idx < this.messages.length) { + message = this.messages[idx]; + encPubKey = this.encPubKeys[idx]; + + // based on the message type we have to process it differently + switch (message.msgType) { + case 1n: + try { + // check if the command is valid + const r = this.processMessage(message, encPubKey, qv); + const index = r.stateLeafIndex!; + + // we add at position 0 the original data + currentStateLeaves.unshift(r.originalStateLeaf!); + currentBallots.unshift(r.originalBallot!); + currentVoteWeights.unshift(r.originalVoteWeight!); + currentVoteWeightsPathElements.unshift(r.originalVoteWeightsPathElements!); + currentStateLeavesPathElements.unshift(r.originalStateLeafPathElements!); + currentBallotsPathElements.unshift(r.originalBallotPathElements!); + + // update the state leaves with the new state leaf (result of processing the message) + this.stateLeaves[index] = r.newStateLeaf!.copy(); + + // we also update the state tree with the hash of the new state leaf + this.stateTree?.update(index, r.newStateLeaf!.hash()); + + // store the new ballot + this.ballots[index] = r.newBallot!; + // update the ballot tree + this.ballotTree?.update(index, r.newBallot!.hash()); + } catch (e) { + // if the error is not a ProcessMessageError we throw it and exit here + // otherwise we continue processing but add the default blank data instead of + // this invalid message + if (e instanceof ProcessMessageError) { + // if logging is enabled, print the error + if (!quiet) { + // eslint-disable-next-line no-console + console.log(`Error at message index ${idx} - ${e.message}`); + } + + // @note we want to send the correct state leaf to the circuit + // even if a message is invalid + // this way if a message is invalid we can still generate a proof of processing + // we also want to prevent a DoS attack by a voter + // which sends a message that when force decrypted on the circuit + // results in a valid state index thus forcing the circuit to look + // for a valid state leaf, and failing to generate a proof + + // gen shared key + const sharedKey = Keypair.genEcdhSharedKey(this.coordinatorKeypair.privKey, encPubKey); + + // force decrypt it + const { command } = PCommand.decrypt(message, sharedKey, true); + + // cache state leaf index + const stateLeafIndex = command.stateIndex; + + // if the state leaf index is valid then use it + if (stateLeafIndex < this.stateLeaves.length) { + currentStateLeaves.unshift(this.stateLeaves[Number(stateLeafIndex)].copy()); + currentStateLeavesPathElements.unshift(this.stateTree!.genProof(Number(stateLeafIndex)).pathElements); + + // copy the ballot + const ballot = this.ballots[Number(stateLeafIndex)].copy(); + currentBallots.unshift(ballot); + currentBallotsPathElements.unshift(this.ballotTree!.genProof(Number(stateLeafIndex)).pathElements); + + // @note we check that command.voteOptionIndex is valid so < maxVoteOptions + // this might be unnecessary but we do it to prevent a possible DoS attack + // from voters who could potentially encrypt a message in such as way that + // when decrypted it results in a valid state leaf index but an invalid vote option index + if (command.voteOptionIndex < this.maxValues.maxVoteOptions) { + currentVoteWeights.unshift(ballot.votes[Number(command.voteOptionIndex)]); + + // create a new quinary tree and add all votes we have so far + const vt = new IncrementalQuinTree( + this.treeDepths.voteOptionTreeDepth, + 0n, + STATE_TREE_ARITY, + hash5, + ); + + // fill the vote option tree with the votes we have so far + for (let j = 0; j < this.ballots[0].votes.length; j += 1) { + vt.insert(ballot.votes[j]); + } + + // get the path elements for the first vote leaf + currentVoteWeightsPathElements.unshift(vt.genProof(Number(command.voteOptionIndex)).pathElements); + } else { + currentVoteWeights.unshift(ballot.votes[0]); + + // create a new quinary tree and add all votes we have so far + const vt = new IncrementalQuinTree( + this.treeDepths.voteOptionTreeDepth, + 0n, + STATE_TREE_ARITY, + hash5, + ); + + // fill the vote option tree with the votes we have so far + for (let j = 0; j < this.ballots[0].votes.length; j += 1) { + vt.insert(ballot.votes[j]); + } + + // get the path elements for the first vote leaf + currentVoteWeightsPathElements.unshift(vt.genProof(0).pathElements); + } + } else { + // just use state leaf index 0 + currentStateLeaves.unshift(this.stateLeaves[0].copy()); + currentStateLeavesPathElements.unshift(this.stateTree!.genProof(0).pathElements); + currentBallots.unshift(this.ballots[0].copy()); + currentBallotsPathElements.unshift(this.ballotTree!.genProof(0).pathElements); + + // Since the command is invalid, we use a zero vote weight + currentVoteWeights.unshift(this.ballots[0].votes[0]); + + // create a new quinary tree and add an empty vote + const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, STATE_TREE_ARITY, hash5); + vt.insert(this.ballots[0].votes[0]); + // get the path elements for this empty vote weight leaf + currentVoteWeightsPathElements.unshift(vt.genProof(0).pathElements); + } + } else { + throw e; + } + } + break; + case 2n: + try { + // -------------------------------------- + // generate topup circuit inputs + const stateIndex = Number(message.data[0] >= BigInt(this.ballots.length) ? 0n : message.data[0]); + const amount = message.data[0] >= BigInt(this.ballots.length) ? 0n : message.data[1]; + + currentStateLeaves.unshift(this.stateLeaves[stateIndex].copy()); + currentStateLeavesPathElements.unshift(this.stateTree!.genProof(stateIndex).pathElements); + + // create a copy of the state leaf + const newStateLeaf = this.stateLeaves[stateIndex].copy(); + // update the voice credit balance + newStateLeaf.voiceCreditBalance += amount; + + // we should not be in this state as it means we are dealing with very large numbers which will cause problems in the circuits + if (newStateLeaf.voiceCreditBalance > SNARK_FIELD_SIZE) { + throw new Error( + "State leaf voice credit balance exceeds SNARK_FIELD_SIZE. This should not be a state MACI should find itself in, as it will cause complications in the circuits. Rounds should not accept topups with large values.", + ); + } + + // save it + this.stateLeaves[stateIndex] = newStateLeaf; + // update the state tree + this.stateTree?.update(stateIndex, newStateLeaf.hash()); + + // we still need them as placeholder for vote command + const currentBallot = this.ballots[stateIndex].copy(); + currentBallots.unshift(currentBallot); + currentBallotsPathElements.unshift(this.ballotTree!.genProof(Number(stateIndex)).pathElements); + currentVoteWeights.unshift(currentBallot.votes[0]); + + // create a quinary tree to fill with the votes of the current ballot + const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, STATE_TREE_ARITY, hash5); + + for (let j = 0; j < this.ballots[0].votes.length; j += 1) { + vt.insert(currentBallot.votes[j]); + } + + // add to the first position the path elements of the vote weight tree + currentVoteWeightsPathElements.unshift(vt.genProof(0).pathElements); + } catch (e) { + if (!quiet) { + // eslint-disable-next-line no-console + console.log("Error processing topup message: ", (e as Error).message); + } + throw e; + } + break; + default: + break; + } + } else { + // Since we don't have a command at that position, use a blank state leaf + currentStateLeaves.unshift(this.stateLeaves[0].copy()); + currentStateLeavesPathElements.unshift(this.stateTree!.genProof(0).pathElements); + // since the command is invliad we use the blank ballot + currentBallots.unshift(this.ballots[0].copy()); + currentBallotsPathElements.unshift(this.ballotTree!.genProof(0).pathElements); + + // Since the command is invalid, we use a zero vote weight + currentVoteWeights.unshift(this.ballots[0].votes[0]); + + // create a new quinary tree and add an empty vote + const vt = new IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, STATE_TREE_ARITY, hash5); + vt.insert(this.ballots[0].votes[0]); + + // get the path elements for this empty vote weight leaf + currentVoteWeightsPathElements.unshift(vt.genProof(0).pathElements); + } + } + + // store the data in the circuit inputs object + circuitInputs.currentStateLeaves = currentStateLeaves.map(x => x.asCircuitInputs()); + circuitInputs.currentStateLeavesPathElements = currentStateLeavesPathElements; + circuitInputs.currentBallots = currentBallots.map(x => x.asCircuitInputs()); + circuitInputs.currentBallotsPathElements = currentBallotsPathElements; + circuitInputs.currentVoteWeights = currentVoteWeights; + circuitInputs.currentVoteWeightsPathElements = currentVoteWeightsPathElements; + + // record that we processed one batch + this.numBatchesProcessed += 1; + + if (this.currentMessageBatchIndex! > 0) { + this.currentMessageBatchIndex! -= batchSize; + } + + // ensure newSbSalt differs from currentSbSalt + let newSbSalt = genRandomSalt(); + while (this.sbSalts[this.currentMessageBatchIndex!] === newSbSalt) { + newSbSalt = genRandomSalt(); + } + this.sbSalts[this.currentMessageBatchIndex!] = newSbSalt; + + // store the salt in the circuit inputs + circuitInputs.newSbSalt = newSbSalt; + const newStateRoot = this.stateTree!.root; + const newBallotRoot = this.ballotTree!.root; + // create a commitment to the state and ballot tree roots + // this will be the hash of the roots with a salt + circuitInputs.newSbCommitment = hash3([newStateRoot, newBallotRoot, newSbSalt]); + + // here is important that a user validates it matches the one in the + // smart contract + const coordPubKeyHash = this.coordinatorKeypair.pubKey.hash(); + // create the input hash which is the only public input to the + // process messages circuit + circuitInputs.inputHash = sha256Hash([ + circuitInputs.packedVals as bigint, + coordPubKeyHash, + circuitInputs.msgRoot as bigint, + circuitInputs.currentSbCommitment as bigint, + circuitInputs.newSbCommitment, + this.pollEndTimestamp, + ]); + + // If this is the last batch, release the lock + if (this.numBatchesProcessed * batchSize >= this.messages.length) { + this.maciStateRef.pollBeingProcessed = false; + } + + return stringifyBigInts(circuitInputs) as unknown as IProcessMessagesCircuitInputs; + }; + + /** + * Generates partial circuit inputs for processing a batch of messages + * @param index - The index of the partial batch. + * @returns stringified partial circuit inputs + */ + private genProcessMessagesCircuitInputsPartial = (index: number): CircuitInputs => { + const { messageBatchSize } = this.batchSizes; + + assert(index <= this.messages.length, "The index must be <= the number of messages"); + assert(index % messageBatchSize === 0, "The index must be a multiple of the message batch size"); + + // fill the msgs array with a copy of the messages we have + // plus empty messages to fill the batch + + // @note create a message with state index 0 to add as padding + // this way the message will look for state leaf 0 + // and no effect will take place + + // create a random key + const key = new Keypair(); + // gen ecdh key + const ecdh = Keypair.genEcdhSharedKey(key.privKey, this.coordinatorKeypair.pubKey); + // create an empty command with state index 0n + const emptyCommand = new PCommand(0n, key.pubKey, 0n, 0n, 0n, 0n, 0n); + + // encrypt it + const msg = emptyCommand.encrypt(emptyCommand.sign(key.privKey), ecdh); + + // copy the messages to a new array + let msgs = this.messages.map(x => x.asCircuitInputs()); + + // pad with our state index 0 message + while (msgs.length % messageBatchSize > 0) { + msgs.push(msg.asCircuitInputs()); + } + + // we only take the messages we need for this batch + msgs = msgs.slice(index, index + messageBatchSize); + + // insert zero value in the message tree as padding + while (this.messageTree.nextIndex < index + messageBatchSize) { + this.messageTree.insert(this.messageTree.zeroValue); + } + + // generate the path to the subroot of the message tree for this batch + const messageSubrootPath = this.messageTree.genSubrootProof(index, index + messageBatchSize); + + // verify it + assert(this.messageTree.verifyProof(messageSubrootPath), "The message subroot path is invalid"); + + // validate that the batch index is correct, if not fix it + // this means that the end will be the last message + let batchEndIndex = index + messageBatchSize; + if (batchEndIndex > this.messages.length) { + batchEndIndex = this.messages.length; + } + + // copy the public keys, pad the array with the last keys if needed + let encPubKeys = this.encPubKeys.map(x => x.copy()); + while (encPubKeys.length % messageBatchSize > 0) { + // pad with the public key used to encrypt the message with state index 0 (padding) + encPubKeys.push(key.pubKey.copy()); + } + // then take the ones part of this batch + encPubKeys = encPubKeys.slice(index, index + messageBatchSize); + + // cache tree roots + const msgRoot = this.messageTree.root; + const currentStateRoot = this.stateTree!.root; + const currentBallotRoot = this.ballotTree!.root; + // calculate the current state and ballot root + // commitment which is the hash of the state tree + // root, the ballot tree root and a salt + const currentSbCommitment = hash3([ + currentStateRoot, + currentBallotRoot, + this.sbSalts[this.currentMessageBatchIndex!], + ]); + + // Generate a SHA256 hash of inputs which the contract provides + /* eslint-disable no-bitwise */ + const packedVals = + BigInt(this.maxValues.maxVoteOptions) + + (BigInt(this.numSignups) << 50n) + + (BigInt(index) << 100n) + + (BigInt(batchEndIndex) << 150n); + /* eslint-enable no-bitwise */ + + return stringifyBigInts({ + pollEndTimestamp: this.pollEndTimestamp, + packedVals, + msgRoot, + msgs, + msgSubrootPathElements: messageSubrootPath.pathElements, + coordPrivKey: this.coordinatorKeypair.privKey.asCircuitInputs(), + coordPubKey: this.coordinatorKeypair.pubKey.asCircuitInputs(), + encPubKeys: encPubKeys.map(x => x.asCircuitInputs()), + currentStateRoot, + currentBallotRoot, + currentSbCommitment, + currentSbSalt: this.sbSalts[this.currentMessageBatchIndex!], + }) as CircuitInputs; + }; + + /** + * Process all messages. This function does not update the ballots or state + * leaves; rather, it copies and then updates them. This makes it possible + * to test the result of multiple processMessage() invocations. + * @returns The state leaves and ballots of the poll + */ + processAllMessages = (): { stateLeaves: StateLeaf[]; ballots: Ballot[] } => { + const stateLeaves = this.stateLeaves.map(x => x.copy()); + const ballots = this.ballots.map(x => x.copy()); + + // process all messages in one go (batch by batch but without manual intervention) + while (this.hasUnprocessedMessages()) { + this.processMessages(this.pollId); + } + + return { stateLeaves, ballots }; + }; + + /** + * Checks whether there are any untallied ballots. + * @returns Whether there are any untallied ballots + */ + hasUntalliedBallots = (): boolean => this.numBatchesTallied * this.batchSizes.tallyBatchSize < this.ballots.length; + + /** + * This method checks if there are any unfinished subsidy calculations. + * @returns Returns true if the product of the row batch index (rbi) and batch size or + * the product of column batch index (cbi) and batch size is less than the length + * of the ballots array, indicating that there are still ballots left to be processed. + * Otherwise, it returns false. + */ + hasUnfinishedSubsidyCalculation = (): boolean => { + const batchSize = this.batchSizes.subsidyBatchSize; + return this.rbi * batchSize < this.ballots.length && this.cbi * batchSize < this.ballots.length; + }; + + /** + * This method calculates the subsidy per batch. + * @returns Returns an array of big integers which represent the circuit inputs for the subsidy calculation. + */ + subsidyPerBatch = (): ISubsidyCircuitInputs => { + const batchSize = this.batchSizes.subsidyBatchSize; + + assert(this.hasUnfinishedSubsidyCalculation(), "No more subsidy batches to calculate"); + + const stateRoot = this.stateTree!.root; + const ballotRoot = this.ballotTree!.root; + const sbSalt = this.sbSalts[this.currentMessageBatchIndex!]; + const sbCommitment = hash3([stateRoot, ballotRoot, sbSalt]); + + const currentSubsidy = this.subsidy.map(x => BigInt(x.toString())); + let currentSubsidyCommitment = 0n; + let currentSubsidySalt = 0n; + let saltIndex = this.previousSubsidyIndexToString(); + + if (this.rbi !== 0 || this.cbi !== 0) { + currentSubsidySalt = BigInt(this.subsidySalts[saltIndex]); + currentSubsidyCommitment = BigInt( + genTreeCommitment(this.subsidy, currentSubsidySalt, this.treeDepths.voteOptionTreeDepth).valueOf(), + ); + } + + const rowStartIndex = this.rbi * batchSize; + const colStartIndex = this.cbi * batchSize; + const [ballots1, ballots2] = this.subsidyCalculation(rowStartIndex, colStartIndex); + + const ballotSubrootProof1 = this.ballotTree?.genSubrootProof(rowStartIndex, rowStartIndex + batchSize); + const ballotSubrootProof2 = this.ballotTree?.genSubrootProof(colStartIndex, colStartIndex + batchSize); + + const newSubsidySalt = genRandomSalt(); + saltIndex = `${this.rbi.toString()}-${this.cbi.toString()}`; + this.subsidySalts[saltIndex] = newSubsidySalt; + const newSubsidyCommitment = genTreeCommitment(this.subsidy, newSubsidySalt, this.treeDepths.voteOptionTreeDepth); + + const packedVals = packSubsidySmallVals(this.rbi, this.cbi, Number(this.numSignups)); + + const inputHash = sha256Hash([packedVals, sbCommitment, currentSubsidyCommitment, newSubsidyCommitment]); + + const circuitInputs = stringifyBigInts({ + stateRoot, + ballotRoot, + sbSalt, + currentSubsidySalt, + newSubsidySalt, + sbCommitment, + currentSubsidyCommitment, + newSubsidyCommitment, + currentSubsidy, + packedVals, + inputHash, + ballots1: ballots1.map(x => x.asCircuitInputs()), + ballots2: ballots2.map(x => x.asCircuitInputs()), + votes1: ballots1.map(x => x.votes), + votes2: ballots2.map(x => x.votes), + ballotPathElements1: ballotSubrootProof1!.pathElements, + ballotPathElements2: ballotSubrootProof2!.pathElements, + }) as unknown as ISubsidyCircuitInputs; + + this.increaseSubsidyIndex(); + return circuitInputs; + }; + + /** + * It increases the index for the subsidy calculation. + */ + private increaseSubsidyIndex = (): void => { + const batchSize = this.batchSizes.subsidyBatchSize; + + if (this.cbi * batchSize + batchSize < this.ballots.length) { + this.cbi += 1; + } else { + this.rbi += 1; + this.cbi = this.rbi; + } + }; + + /** + * This method converts the previous subsidy index to a string. + * @returns Returns a string representation of the previous subsidy index. + * The string is in the format "rbi-cbi", where rbi and cbi are + * the previous row batch index and column batch index respectively. + */ + private previousSubsidyIndexToString = (): string => { + const batchSize = this.batchSizes.subsidyBatchSize; + const numBatches = Math.ceil(this.ballots.length / batchSize); + + let { cbi } = this; + let { rbi } = this; + + if (this.cbi === 0 && this.rbi === 0) { + return "0-0"; + } + + if (this.cbi > this.rbi) { + cbi -= 1; + } else { + rbi -= 1; + cbi = numBatches - 1; + } + + return `${rbi.toString()}-${cbi.toString()}`; + }; + + /** + * This method calculates the coefficient for a pair of ballots. + * @param rowBallot - The ballot in the row. + * @param colBallot - The ballot in the column. + * + * @returns Returns the calculated coefficient. + */ + private coefficientCalculation = (rowBallot: Ballot, colBallot: Ballot): bigint => { + let sum = 0n; + for (let p = 0; p < this.maxValues.maxVoteOptions; p += 1) { + sum += BigInt(rowBallot.votes[p].valueOf()) * colBallot.votes[p]; + } + const res = BigInt(this.MM * 10 ** this.WW) / (BigInt(this.MM) + BigInt(sum)); + return res; + }; + + /** + * This method calculates the subsidy for a batch of ballots. + * @param rowStartIndex - The starting index for the row ballots. + * @param colStartIndex - The starting index for the column ballots. + * @returns Returns a 2D array of ballots. The first array contains the row ballots and the second array contains the column ballots. + */ + private subsidyCalculation = (rowStartIndex: number, colStartIndex: number): Ballot[][] => { + const batchSize = this.batchSizes.subsidyBatchSize; + const ballots1: Ballot[] = []; + const ballots2: Ballot[] = []; + const emptyBallot = new Ballot(this.maxValues.maxVoteOptions, this.treeDepths.voteOptionTreeDepth); + + for (let i = 0; i < batchSize; i += 1) { + const row = rowStartIndex + i; + const col = colStartIndex + i; + const rowBallot = row < this.ballots.length ? this.ballots[row] : emptyBallot; + const colBallot = col < this.ballots.length ? this.ballots[col] : emptyBallot; + ballots1.push(rowBallot); + ballots2.push(colBallot); + } + for (let i = 0; i < batchSize; i += 1) { + for (let j = 0; j < batchSize; j += 1) { + const row = rowStartIndex + i; + const col = colStartIndex + j; + const rowBallot = row < this.ballots.length ? this.ballots[row] : emptyBallot; + const colBallot = col < this.ballots.length ? this.ballots[col] : emptyBallot; + + const kij = this.coefficientCalculation(rowBallot, colBallot); + for (let p = 0; p < this.maxValues.maxVoteOptions; p += 1) { + const vip = BigInt(rowBallot.votes[p].valueOf()); + const vjp = BigInt(colBallot.votes[p].valueOf()); + if (rowStartIndex !== colStartIndex || (rowStartIndex === colStartIndex && i < j)) { + this.subsidy[p] += 2n * kij * vip * vjp; + } + } + } + } + + return [ballots1, ballots2]; + }; + + /** + * This method tallies a ballots and updates the tally results. + * @returns the circuit inputs for the TallyVotes circuit. + */ + tallyVotes = (): ITallyCircuitInputs => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.sbSalts[this.currentMessageBatchIndex!] === undefined) { + throw new Error("You must process the messages first"); + } + + const batchSize = this.batchSizes.tallyBatchSize; + + assert(this.hasUntalliedBallots(), "No more ballots to tally"); + + // calculate where we start tallying next + const batchStartIndex = this.numBatchesTallied * batchSize; + + // get the salts needed for the commitments + const currentResultsRootSalt = batchStartIndex === 0 ? 0n : this.resultRootSalts[batchStartIndex - batchSize]; + + const currentPerVOSpentVoiceCreditsRootSalt = + batchStartIndex === 0 ? 0n : this.preVOSpentVoiceCreditsRootSalts[batchStartIndex - batchSize]; + + const currentSpentVoiceCreditSubtotalSalt = + batchStartIndex === 0 ? 0n : this.spentVoiceCreditSubtotalSalts[batchStartIndex - batchSize]; + + // generate a commitment to the current results + const currentResultsCommitment = genTreeCommitment( + this.tallyResult, + currentResultsRootSalt, + this.treeDepths.voteOptionTreeDepth, + ); + + // generate a commitment to the current per VO spent voice credits + const currentPerVOSpentVoiceCreditsCommitment = this.genPerVOSpentVoiceCreditsCommitment( + currentPerVOSpentVoiceCreditsRootSalt, + batchStartIndex, + true, + ); + + // generate a commitment to the current spent voice credits + const currentSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment( + currentSpentVoiceCreditSubtotalSalt, + batchStartIndex, + true, + ); + + // the current commitment for the first batch will be 0 + // otherwise calculate as + // hash([ + // currentResultsCommitment, + // currentSpentVoiceCreditsCommitment, + // currentPerVOSpentVoiceCreditsCommitment + // ]) + const currentTallyCommitment = + batchStartIndex === 0 + ? 0n + : hash3([ + currentResultsCommitment, + currentSpentVoiceCreditsCommitment, + currentPerVOSpentVoiceCreditsCommitment, + ]); + + const ballots: Ballot[] = []; + const currentResults = this.tallyResult.map(x => BigInt(x.toString())); + const currentPerVOSpentVoiceCredits = this.perVOSpentVoiceCredits.map(x => BigInt(x.toString())); + const currentSpentVoiceCreditSubtotal = BigInt(this.totalSpentVoiceCredits.toString()); + + // loop in normal order to tally the ballots one by one + for (let i = this.numBatchesTallied * batchSize; i < this.numBatchesTallied * batchSize + batchSize; i += 1) { + // we stop if we have no more ballots to tally + if (i >= this.ballots.length) { + break; + } + + // save to the local ballot array + ballots.push(this.ballots[i]); + + // for each possible vote option we loop and calculate + for (let j = 0; j < this.maxValues.maxVoteOptions; j += 1) { + const v = this.ballots[i].votes[j]; + + // the vote itself will be a quadratic vote (sqrt(voiceCredits)) + this.tallyResult[j] += v; + + // the per vote option spent voice credits will be the sum of the squares of the votes + this.perVOSpentVoiceCredits[j] += v * v; + + // the total spent voice credits will be the sum of the squares of the votes + this.totalSpentVoiceCredits += v * v; + } + } + + const emptyBallot = new Ballot(this.maxValues.maxVoteOptions, this.treeDepths.voteOptionTreeDepth); + + // pad the ballots array + while (ballots.length < batchSize) { + ballots.push(emptyBallot); + } + + // generate the new salts + const newResultsRootSalt = genRandomSalt(); + const newPerVOSpentVoiceCreditsRootSalt = genRandomSalt(); + const newSpentVoiceCreditSubtotalSalt = genRandomSalt(); + + // and save them to be used in the next batch + this.resultRootSalts[batchStartIndex] = newResultsRootSalt; + this.preVOSpentVoiceCreditsRootSalts[batchStartIndex] = newPerVOSpentVoiceCreditsRootSalt; + this.spentVoiceCreditSubtotalSalts[batchStartIndex] = newSpentVoiceCreditSubtotalSalt; + + // generate the new results commitment with the new salts and data + const newResultsCommitment = genTreeCommitment( + this.tallyResult, + newResultsRootSalt, + this.treeDepths.voteOptionTreeDepth, + ); + + // generate the new spent voice credits commitment with the new salts and data + const newSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment( + newSpentVoiceCreditSubtotalSalt, + batchStartIndex + batchSize, + true, + ); + + // generate the new per VO spent voice credits commitment with the new salts and data + const newPerVOSpentVoiceCreditsCommitment = this.genPerVOSpentVoiceCreditsCommitment( + newPerVOSpentVoiceCreditsRootSalt, + batchStartIndex + batchSize, + true, + ); + + // generate the new tally commitment + const newTallyCommitment = hash3([ + newResultsCommitment, + newSpentVoiceCreditsCommitment, + newPerVOSpentVoiceCreditsCommitment, + ]); + + // cache vars + const stateRoot = this.stateTree!.root; + const ballotRoot = this.ballotTree!.root; + const sbSalt = this.sbSalts[this.currentMessageBatchIndex!]; + const sbCommitment = hash3([stateRoot, ballotRoot, sbSalt]); + + const packedVals = packTallyVotesSmallVals(batchStartIndex, batchSize, Number(this.numSignups)); + const inputHash = sha256Hash([packedVals, sbCommitment, currentTallyCommitment, newTallyCommitment]); + + const ballotSubrootProof = this.ballotTree?.genSubrootProof(batchStartIndex, batchStartIndex + batchSize); + + const votes = ballots.map(x => x.votes); + + const circuitInputs = stringifyBigInts({ + stateRoot, + ballotRoot, + sbSalt, + sbCommitment, + currentTallyCommitment, + newTallyCommitment, + packedVals, // contains numSignUps and batchStartIndex + inputHash, + ballots: ballots.map(x => x.asCircuitInputs()), + ballotPathElements: ballotSubrootProof!.pathElements, + votes, + currentResults, + currentResultsRootSalt, + currentSpentVoiceCreditSubtotal, + currentSpentVoiceCreditSubtotalSalt, + currentPerVOSpentVoiceCredits, + currentPerVOSpentVoiceCreditsRootSalt, + newResultsRootSalt, + newPerVOSpentVoiceCreditsRootSalt, + newSpentVoiceCreditSubtotalSalt, + }) as unknown as ITallyCircuitInputs; + + this.numBatchesTallied += 1; + + return circuitInputs; + }; + + tallyVotesNonQv = (): ITallyCircuitInputs => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.sbSalts[this.currentMessageBatchIndex!] === undefined) { + throw new Error("You must process the messages first"); + } + + const batchSize = this.batchSizes.tallyBatchSize; + + assert(this.hasUntalliedBallots(), "No more ballots to tally"); + + // calculate where we start tallying next + const batchStartIndex = this.numBatchesTallied * batchSize; + + // get the salts needed for the commitments + const currentResultsRootSalt = batchStartIndex === 0 ? 0n : this.resultRootSalts[batchStartIndex - batchSize]; + + const currentSpentVoiceCreditSubtotalSalt = + batchStartIndex === 0 ? 0n : this.spentVoiceCreditSubtotalSalts[batchStartIndex - batchSize]; + + // generate a commitment to the current results + const currentResultsCommitment = genTreeCommitment( + this.tallyResult, + currentResultsRootSalt, + this.treeDepths.voteOptionTreeDepth, + ); + + // generate a commitment to the current spent voice credits + const currentSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment( + currentSpentVoiceCreditSubtotalSalt, + batchStartIndex, + false, + ); + + // the current commitment for the first batch will be 0 + // otherwise calculate as + // hash([ + // currentResultsCommitment, + // currentSpentVoiceCreditsCommitment, + // ]) + const currentTallyCommitment = + batchStartIndex === 0 ? 0n : hashLeftRight(currentResultsCommitment, currentSpentVoiceCreditsCommitment); + + const ballots: Ballot[] = []; + const currentResults = this.tallyResult.map(x => BigInt(x.toString())); + const currentSpentVoiceCreditSubtotal = BigInt(this.totalSpentVoiceCredits.toString()); + + // loop in normal order to tally the ballots one by one + for (let i = this.numBatchesTallied * batchSize; i < this.numBatchesTallied * batchSize + batchSize; i += 1) { + // we stop if we have no more ballots to tally + if (i >= this.ballots.length) { + break; + } + + // save to the local ballot array + ballots.push(this.ballots[i]); + + // for each possible vote option we loop and calculate + for (let j = 0; j < this.maxValues.maxVoteOptions; j += 1) { + const v = this.ballots[i].votes[j]; + + this.tallyResult[j] += v; + + // the total spent voice credits will be the sum of the votes + this.totalSpentVoiceCredits += v; + } + } + + const emptyBallot = new Ballot(this.maxValues.maxVoteOptions, this.treeDepths.voteOptionTreeDepth); + + // pad the ballots array + while (ballots.length < batchSize) { + ballots.push(emptyBallot); + } + + // generate the new salts + const newResultsRootSalt = genRandomSalt(); + const newSpentVoiceCreditSubtotalSalt = genRandomSalt(); + + // and save them to be used in the next batch + this.resultRootSalts[batchStartIndex] = newResultsRootSalt; + this.spentVoiceCreditSubtotalSalts[batchStartIndex] = newSpentVoiceCreditSubtotalSalt; + + // generate the new results commitment with the new salts and data + const newResultsCommitment = genTreeCommitment( + this.tallyResult, + newResultsRootSalt, + this.treeDepths.voteOptionTreeDepth, + ); + + // generate the new spent voice credits commitment with the new salts and data + const newSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment( + newSpentVoiceCreditSubtotalSalt, + batchStartIndex + batchSize, + false, + ); + + // generate the new tally commitment + const newTallyCommitment = hashLeftRight(newResultsCommitment, newSpentVoiceCreditsCommitment); + + // cache vars + const stateRoot = this.stateTree!.root; + const ballotRoot = this.ballotTree!.root; + const sbSalt = this.sbSalts[this.currentMessageBatchIndex!]; + const sbCommitment = hash3([stateRoot, ballotRoot, sbSalt]); + + const packedVals = packTallyVotesSmallVals(batchStartIndex, batchSize, Number(this.numSignups)); + const inputHash = sha256Hash([packedVals, sbCommitment, currentTallyCommitment, newTallyCommitment]); + + const ballotSubrootProof = this.ballotTree?.genSubrootProof(batchStartIndex, batchStartIndex + batchSize); + + const votes = ballots.map(x => x.votes); + + const circuitInputs = stringifyBigInts({ + stateRoot, + ballotRoot, + sbSalt, + sbCommitment, + currentTallyCommitment, + newTallyCommitment, + packedVals, // contains numSignUps and batchStartIndex + inputHash, + ballots: ballots.map(x => x.asCircuitInputs()), + ballotPathElements: ballotSubrootProof!.pathElements, + votes, + currentResults, + currentResultsRootSalt, + currentSpentVoiceCreditSubtotal, + currentSpentVoiceCreditSubtotalSalt, + newResultsRootSalt, + newSpentVoiceCreditSubtotalSalt, + }) as unknown as ITallyCircuitInputs; + + this.numBatchesTallied += 1; + + return circuitInputs; + }; + + /** + * This method generates a commitment to the total spent voice credits. + * + * This is the hash of the total spent voice credits and a salt, computed as Poseidon([totalCredits, _salt]). + * @param salt - The salt used in the hash function. + * @param numBallotsToCount - The number of ballots to count for the calculation. + * @param useQuadraticVoting - Whether to use quadratic voting or not. Default is true. + * @returns Returns the hash of the total spent voice credits and a salt, computed as Poseidon([totalCredits, _salt]). + */ + private genSpentVoiceCreditSubtotalCommitment = ( + salt: bigint, + numBallotsToCount: number, + useQuadraticVoting = true, + ): bigint => { + let subtotal = 0n; + for (let i = 0; i < numBallotsToCount; i += 1) { + if (this.ballots.length <= i) { + break; + } + + for (let j = 0; j < this.tallyResult.length; j += 1) { + const v = BigInt(`${this.ballots[i].votes[j]}`); + subtotal += useQuadraticVoting ? v * v : v; + } + } + return hashLeftRight(subtotal, salt); + }; + + /** + * This method generates a commitment to the spent voice credits per vote option. + * + * This is the hash of the Merkle root of the spent voice credits per vote option and a salt, computed as Poseidon([root, _salt]). + * @param salt - The salt used in the hash function. + * @param numBallotsToCount - The number of ballots to count for the calculation. + * @param useQuadraticVoting - Whether to use quadratic voting or not. Default is true. + * @returns Returns the hash of the Merkle root of the spent voice credits per vote option and a salt, computed as Poseidon([root, _salt]). + */ + private genPerVOSpentVoiceCreditsCommitment = ( + salt: bigint, + numBallotsToCount: number, + useQuadraticVoting = true, + ): bigint => { + const leaves: bigint[] = Array(this.tallyResult.length).fill(0n); + + for (let i = 0; i < numBallotsToCount; i += 1) { + // check that is a valid index + if (i >= this.ballots.length) { + break; + } + + for (let j = 0; j < this.tallyResult.length; j += 1) { + const v = this.ballots[i].votes[j]; + leaves[j] += useQuadraticVoting ? v * v : v; + } + } + + return genTreeCommitment(leaves, salt, this.treeDepths.voteOptionTreeDepth); + }; + + /** + * Create a deep copy of the Poll object. + * @returns A new instance of the Poll object with the same properties. + */ + copy = (): Poll => { + const copied = new Poll( + BigInt(this.pollEndTimestamp.toString()), + this.coordinatorKeypair.copy(), + { + intStateTreeDepth: Number(this.treeDepths.intStateTreeDepth), + messageTreeDepth: Number(this.treeDepths.messageTreeDepth), + messageTreeSubDepth: Number(this.treeDepths.messageTreeSubDepth), + voteOptionTreeDepth: Number(this.treeDepths.voteOptionTreeDepth), + }, + { + tallyBatchSize: Number(this.batchSizes.tallyBatchSize.toString()), + subsidyBatchSize: Number(this.batchSizes.subsidyBatchSize.toString()), + messageBatchSize: Number(this.batchSizes.messageBatchSize.toString()), + }, + { + maxMessages: Number(this.maxValues.maxMessages.toString()), + maxVoteOptions: Number(this.maxValues.maxVoteOptions.toString()), + }, + this.maciStateRef, + ); + + copied.stateLeaves = this.stateLeaves.map(x => x.copy()); + copied.messages = this.messages.map(x => x.copy()); + copied.commands = this.commands.map(x => x.copy()); + copied.ballots = this.ballots.map(x => x.copy()); + copied.encPubKeys = this.encPubKeys.map(x => x.copy()); + + if (this.ballotTree) { + copied.ballotTree = this.ballotTree.copy(); + } + + copied.currentMessageBatchIndex = this.currentMessageBatchIndex; + copied.maciStateRef = this.maciStateRef; + copied.messageTree = this.messageTree.copy(); + copied.tallyResult = this.tallyResult.map((x: bigint) => BigInt(x.toString())); + copied.perVOSpentVoiceCredits = this.perVOSpentVoiceCredits.map((x: bigint) => BigInt(x.toString())); + + copied.numBatchesProcessed = Number(this.numBatchesProcessed.toString()); + copied.numBatchesTallied = Number(this.numBatchesTallied.toString()); + copied.pollId = this.pollId; + copied.totalSpentVoiceCredits = BigInt(this.totalSpentVoiceCredits.toString()); + + copied.sbSalts = {}; + copied.resultRootSalts = {}; + copied.preVOSpentVoiceCreditsRootSalts = {}; + copied.spentVoiceCreditSubtotalSalts = {}; + + Object.keys(this.sbSalts).forEach(k => { + copied.sbSalts[k] = BigInt(this.sbSalts[k].toString()); + }); + + Object.keys(this.resultRootSalts).forEach(k => { + copied.resultRootSalts[k] = BigInt(this.resultRootSalts[k].toString()); + }); + + Object.keys(this.preVOSpentVoiceCreditsRootSalts).forEach(k => { + copied.preVOSpentVoiceCreditsRootSalts[k] = BigInt(this.preVOSpentVoiceCreditsRootSalts[k].toString()); + }); + + Object.keys(this.spentVoiceCreditSubtotalSalts).forEach(k => { + copied.spentVoiceCreditSubtotalSalts[k] = BigInt(this.spentVoiceCreditSubtotalSalts[k].toString()); + }); + + // subsidy related copy + copied.subsidy = this.subsidy.map((x: bigint) => BigInt(x.toString())); + copied.rbi = Number(this.rbi.toString()); + copied.cbi = Number(this.cbi.toString()); + copied.MM = Number(this.MM.toString()); + copied.WW = Number(this.WW.toString()); + + Object.keys(this.subsidySalts).forEach(k => { + copied.subsidySalts[k] = BigInt(this.subsidySalts[k].toString()); + }); + + // update the number of signups + copied.setNumSignups(this.numSignups); + + return copied; + }; + + /** + * Check if the Poll object is equal to another Poll object. + * @param p - The Poll object to compare. + * @returns True if the two Poll objects are equal, false otherwise. + */ + equals = (p: Poll): boolean => { + const result = + this.coordinatorKeypair.equals(p.coordinatorKeypair) && + this.treeDepths.intStateTreeDepth === p.treeDepths.intStateTreeDepth && + this.treeDepths.messageTreeDepth === p.treeDepths.messageTreeDepth && + this.treeDepths.messageTreeSubDepth === p.treeDepths.messageTreeSubDepth && + this.treeDepths.voteOptionTreeDepth === p.treeDepths.voteOptionTreeDepth && + this.batchSizes.tallyBatchSize === p.batchSizes.tallyBatchSize && + this.batchSizes.messageBatchSize === p.batchSizes.messageBatchSize && + this.maxValues.maxMessages === p.maxValues.maxMessages && + this.maxValues.maxVoteOptions === p.maxValues.maxVoteOptions && + this.messages.length === p.messages.length && + this.encPubKeys.length === p.encPubKeys.length && + this.numSignups === p.numSignups; + + if (!result) { + return false; + } + + for (let i = 0; i < this.messages.length; i += 1) { + if (!this.messages[i].equals(p.messages[i])) { + return false; + } + } + for (let i = 0; i < this.encPubKeys.length; i += 1) { + if (!this.encPubKeys[i].equals(p.encPubKeys[i])) { + return false; + } + } + return true; + }; + + /** + * Serialize the Poll object to a JSON object + * @returns a JSON object + */ + toJSON(): IJsonPoll { + return { + pollEndTimestamp: this.pollEndTimestamp.toString(), + treeDepths: this.treeDepths, + batchSizes: this.batchSizes, + maxValues: this.maxValues, + messages: this.messages.map(message => message.toJSON()), + commands: this.commands.map(command => command.toJSON() as IJsonCommand), + ballots: this.ballots.map(ballot => ballot.toJSON()), + encPubKeys: this.encPubKeys.map(encPubKey => encPubKey.serialize()), + currentMessageBatchIndex: this.currentMessageBatchIndex!, + stateLeaves: this.stateLeaves.map(leaf => leaf.toJSON()), + results: this.tallyResult.map(result => result.toString()), + numBatchesProcessed: this.numBatchesProcessed, + numSignups: this.numSignups.toString(), + }; + } + + /** + * Deserialize a json object into a Poll instance + * @param json the json object to deserialize + * @param maciState the reference to the MaciState Class + * @returns a new Poll instance + */ + static fromJSON(json: IJsonPoll, maciState: MaciState): Poll { + const poll = new Poll( + BigInt(json.pollEndTimestamp), + new Keypair(), + json.treeDepths, + json.batchSizes, + json.maxValues, + maciState, + ); + + // set all properties + poll.ballots = json.ballots.map(ballot => Ballot.fromJSON(ballot)); + poll.encPubKeys = json.encPubKeys.map((key: string) => PubKey.deserialize(key)); + poll.messages = json.messages.map(message => Message.fromJSON(message as IMessageContractParams)); + poll.commands = json.commands.map((command: IJsonCommand) => { + switch (command.cmdType) { + case "1": { + return PCommand.fromJSON(command as IJsonPCommand) as ICommand; + } + + case "2": { + return TCommand.fromJSON(command as IJsonTCommand) as ICommand; + } + + default: { + return { cmdType: command.cmdType } as unknown as ICommand; + } + } + }); + poll.tallyResult = json.results.map((result: string) => BigInt(result)); + poll.currentMessageBatchIndex = json.currentMessageBatchIndex; + poll.numBatchesProcessed = json.numBatchesProcessed; + + // fill the trees + for (let i = 0; i < poll.messages.length; i += 1) { + const messageLeaf = poll.messages[i].hash(poll.encPubKeys[i]); + poll.messageTree.insert(messageLeaf); + } + + // copy maci state + poll.updatePoll(BigInt(json.numSignups)); + + return poll; + } + + /** + * Set the coordinator's keypair + * @param serializedPrivateKey - the serialized private key + */ + setCoordinatorKeypair = (serializedPrivateKey: string): void => { + this.coordinatorKeypair = new Keypair(PrivKey.deserialize(serializedPrivateKey)); + }; + + /** + * Set the number of signups to match the ones from the contract + * @param numSignups - the number of signups + */ + setNumSignups = (numSignups: bigint): void => { + this.numSignups = numSignups; + }; + + /** + * Get the number of signups + * @returns The number of signups + */ + getNumSignups = (): bigint => this.numSignups; +} diff --git a/packages/hardhat/maci-ts/core/__benchmarks__/index.ts b/packages/hardhat/maci-ts/core/__benchmarks__/index.ts new file mode 100644 index 0000000..e6e0d27 --- /dev/null +++ b/packages/hardhat/maci-ts/core/__benchmarks__/index.ts @@ -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(); diff --git a/packages/hardhat/maci-ts/core/__benchmarks__/utils/constants.ts b/packages/hardhat/maci-ts/core/__benchmarks__/utils/constants.ts new file mode 100644 index 0000000..4d7cc77 --- /dev/null +++ b/packages/hardhat/maci-ts/core/__benchmarks__/utils/constants.ts @@ -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, +}; diff --git a/packages/hardhat/maci-ts/core/__tests__/MaciState.test.ts b/packages/hardhat/maci-ts/core/__tests__/MaciState.test.ts new file mode 100644 index 0000000..4331fdc --- /dev/null +++ b/packages/hardhat/maci-ts/core/__tests__/MaciState.test.ts @@ -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"); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/core/__tests__/Poll.test.ts b/packages/hardhat/maci-ts/core/__tests__/Poll.test.ts new file mode 100644 index 0000000..1e761f1 --- /dev/null +++ b/packages/hardhat/maci-ts/core/__tests__/Poll.test.ts @@ -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); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/core/__tests__/e2e.test.ts b/packages/hardhat/maci-ts/core/__tests__/e2e.test.ts new file mode 100644 index 0000000..9bfa2a4 --- /dev/null +++ b/packages/hardhat/maci-ts/core/__tests__/e2e.test.ts @@ -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); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/core/__tests__/utils.test.ts b/packages/hardhat/maci-ts/core/__tests__/utils.test.ts new file mode 100644 index 0000000..4e49e29 --- /dev/null +++ b/packages/hardhat/maci-ts/core/__tests__/utils.test.ts @@ -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); + }); +}); diff --git a/packages/hardhat/maci-ts/core/__tests__/utils/constants.ts b/packages/hardhat/maci-ts/core/__tests__/utils/constants.ts new file mode 100644 index 0000000..b9cef9e --- /dev/null +++ b/packages/hardhat/maci-ts/core/__tests__/utils/constants.ts @@ -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, +}; diff --git a/packages/hardhat/maci-ts/core/__tests__/utils/utils.ts b/packages/hardhat/maci-ts/core/__tests__/utils/utils.ts new file mode 100644 index 0000000..ca331a5 --- /dev/null +++ b/packages/hardhat/maci-ts/core/__tests__/utils/utils.ts @@ -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(); + + /** + * 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; +} diff --git a/packages/hardhat/maci-ts/core/index.ts b/packages/hardhat/maci-ts/core/index.ts new file mode 100644 index 0000000..9f756d4 --- /dev/null +++ b/packages/hardhat/maci-ts/core/index.ts @@ -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"; diff --git a/packages/hardhat/maci-ts/core/utils/constants.ts b/packages/hardhat/maci-ts/core/utils/constants.ts new file mode 100644 index 0000000..6e06744 --- /dev/null +++ b/packages/hardhat/maci-ts/core/utils/constants.ts @@ -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; diff --git a/packages/hardhat/maci-ts/core/utils/errors.ts b/packages/hardhat/maci-ts/core/utils/errors.ts new file mode 100644 index 0000000..3eb7e3f --- /dev/null +++ b/packages/hardhat/maci-ts/core/utils/errors.ts @@ -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; + } +} diff --git a/packages/hardhat/maci-ts/core/utils/types.ts b/packages/hardhat/maci-ts/core/utils/types.ts new file mode 100644 index 0000000..afb65e0 --- /dev/null +++ b/packages/hardhat/maci-ts/core/utils/types.ts @@ -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; + +/** + * 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[]; +} diff --git a/packages/hardhat/maci-ts/core/utils/utils.ts b/packages/hardhat/maci-ts/core/utils/utils.ts new file mode 100644 index 0000000..38374ef --- /dev/null +++ b/packages/hardhat/maci-ts/core/utils/utils.ts @@ -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; +}; diff --git a/packages/hardhat/maci-ts/crypto/AccQueue.ts b/packages/hardhat/maci-ts/crypto/AccQueue.ts new file mode 100644 index 0000000..4687bd6 --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/AccQueue.ts @@ -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(); + 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(); + 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>): 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> { + 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); + } +} diff --git a/packages/hardhat/maci-ts/crypto/__benchmarks__/index.ts b/packages/hardhat/maci-ts/crypto/__benchmarks__/index.ts new file mode 100644 index 0000000..a93b565 --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/__benchmarks__/index.ts @@ -0,0 +1,3 @@ +import runTrees from "./suites/trees"; + +runTrees(); diff --git a/packages/hardhat/maci-ts/crypto/__benchmarks__/suites/trees.ts b/packages/hardhat/maci-ts/crypto/__benchmarks__/suites/trees.ts new file mode 100644 index 0000000..3108c3e --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/__benchmarks__/suites/trees.ts @@ -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 }), + ); +} diff --git a/packages/hardhat/maci-ts/crypto/__tests__/AccQueue.test.ts b/packages/hardhat/maci-ts/crypto/__tests__/AccQueue.test.ts new file mode 100644 index 0000000..9ac7e61 --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/__tests__/AccQueue.test.ts @@ -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); + }); + }); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/crypto/__tests__/Crypto.test.ts b/packages/hardhat/maci-ts/crypto/__tests__/Crypto.test.ts new file mode 100644 index 0000000..81327db --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/__tests__/Crypto.test.ts @@ -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]); + }); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/crypto/__tests__/IMT.test.ts b/packages/hardhat/maci-ts/crypto/__tests__/IMT.test.ts new file mode 100644 index 0000000..00b1f3e --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/__tests__/IMT.test.ts @@ -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); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/crypto/__tests__/Utils.test.ts b/packages/hardhat/maci-ts/crypto/__tests__/Utils.test.ts new file mode 100644 index 0000000..f476939 --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/__tests__/Utils.test.ts @@ -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); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/crypto/__tests__/utils.ts b/packages/hardhat/maci-ts/crypto/__tests__/utils.ts new file mode 100644 index 0000000..08a3ce2 --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/__tests__/utils.ts @@ -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()); + } +}; diff --git a/packages/hardhat/maci-ts/crypto/babyjub.ts b/packages/hardhat/maci-ts/crypto/babyjub.ts new file mode 100644 index 0000000..7a45dcf --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/babyjub.ts @@ -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; +}; diff --git a/packages/hardhat/maci-ts/crypto/bigIntUtils.ts b/packages/hardhat/maci-ts/crypto/bigIntUtils.ts new file mode 100644 index 0000000..3a6097b --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/bigIntUtils.ts @@ -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>((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(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>((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"); +}; diff --git a/packages/hardhat/maci-ts/crypto/constants.ts b/packages/hardhat/maci-ts/crypto/constants.ts new file mode 100644 index 0000000..ce10528 --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/constants.ts @@ -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")); diff --git a/packages/hardhat/maci-ts/crypto/hashing.ts b/packages/hardhat/maci-ts/crypto/hashing.ts new file mode 100644 index 0000000..f86e112 --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/hashing.ts @@ -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)]); diff --git a/packages/hardhat/maci-ts/crypto/index.ts b/packages/hardhat/maci-ts/crypto/index.ts new file mode 100644 index 0000000..45b1f5f --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/index.ts @@ -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"; diff --git a/packages/hardhat/maci-ts/crypto/keys.ts b/packages/hardhat/maci-ts/crypto/keys.ts new file mode 100644 index 0000000..0c83cae --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/keys.ts @@ -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, formatPrivKeyForBabyJub(privKey)); diff --git a/packages/hardhat/maci-ts/crypto/quinTree.ts b/packages/hardhat/maci-ts/crypto/quinTree.ts new file mode 100644 index 0000000..345c4c5 --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/quinTree.ts @@ -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(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(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(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(arity).fill(currentLevelHash); + + currentLevelHash = hashFunc(z); + } + + return { zeros, root: currentLevelHash }; + }; +} diff --git a/packages/hardhat/maci-ts/crypto/types.ts b/packages/hardhat/maci-ts/crypto/types.ts new file mode 100644 index 0000000..495d451 --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/types.ts @@ -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, N]; + +// a shared key is a pair of BigNumbers +export type EcdhSharedKey = [N, N]; + +// a point is a pair of BigNumbers +export type Point = [N, N]; + +// a plaintext is an array of BigNumbers +export type Plaintext = N[]; + +// a ciphertext is an array of BigNumbers +export type Ciphertext = N[]; + +// a merkle tree path elements +export type PathElements = bigint[][]; + +/** + * A acc queue + */ +export interface Queue { + levels: Map>; + 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 { + R8: Point; + 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; + +// 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; diff --git a/packages/hardhat/maci-ts/crypto/utils.ts b/packages/hardhat/maci-ts/crypto/utils.ts new file mode 100644 index 0000000..f2e4f88 --- /dev/null +++ b/packages/hardhat/maci-ts/crypto/utils.ts @@ -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; +}; diff --git a/packages/hardhat/maci-ts/domainobjs/__tests__/ballot.test.ts b/packages/hardhat/maci-ts/domainobjs/__tests__/ballot.test.ts new file mode 100644 index 0000000..351382e --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/__tests__/ballot.test.ts @@ -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); + }); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/domainobjs/__tests__/commands.test.ts b/packages/hardhat/maci-ts/domainobjs/__tests__/commands.test.ts new file mode 100644 index 0000000..b4fdd9d --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/__tests__/commands.test.ts @@ -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); + }); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/domainobjs/__tests__/keypair.test.ts b/packages/hardhat/maci-ts/domainobjs/__tests__/keypair.test.ts new file mode 100644 index 0000000..86a5217 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/__tests__/keypair.test.ts @@ -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); + }); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/domainobjs/__tests__/message.test.ts b/packages/hardhat/maci-ts/domainobjs/__tests__/message.test.ts new file mode 100644 index 0000000..5491e98 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/__tests__/message.test.ts @@ -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(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(9).fill(BigInt(0)))).to.throw(); + }); + }); + + describe("asCircuitInputs", () => { + it("should produce an array", () => { + const msg = new Message(BigInt(0), Array(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(10).fill(BigInt(0))]); + }); + }); + + describe("asContractParam", () => { + it("should produce an object", () => { + const msg = new Message(BigInt(0), Array(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(10).fill(BigInt(0))); + const obj = msg.asContractParam(); + expect(obj.msgType).to.eq("0"); + expect(obj.data).to.deep.eq(Array(10).fill("0")); + }); + }); + + describe("hash", () => { + const keypair = new Keypair(); + it("should produce a hash", () => { + const msg = new Message(BigInt(0), Array(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(10).fill(BigInt(0))); + const msg2 = new Message(BigInt(0), Array(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(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(10).fill(BigInt(0))); + const msg2 = new Message(BigInt(0), Array(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(10).fill(BigInt(0))); + const msg2 = new Message(BigInt(0), Array(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(10).fill(BigInt(0))); + const msg2 = new Message(BigInt(1), Array(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(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(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(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(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(10).fill(BigInt(0))); + const json = msg.toJSON(); + const msg2 = Message.fromJSON(json); + expect(msg.equals(msg2)).to.eq(true); + }); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/domainobjs/__tests__/privateKey.test.ts b/packages/hardhat/maci-ts/domainobjs/__tests__/privateKey.test.ts new file mode 100644 index 0000000..fc3119b --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/__tests__/privateKey.test.ts @@ -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); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/domainobjs/__tests__/publicKey.test.ts b/packages/hardhat/maci-ts/domainobjs/__tests__/publicKey.test.ts new file mode 100644 index 0000000..6747d88 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/__tests__/publicKey.test.ts @@ -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); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/domainobjs/__tests__/stateLeaf.test.ts b/packages/hardhat/maci-ts/domainobjs/__tests__/stateLeaf.test.ts new file mode 100644 index 0000000..6c8e2ad --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/__tests__/stateLeaf.test.ts @@ -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); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/domainobjs/__tests__/verifyingKey.test.ts b/packages/hardhat/maci-ts/domainobjs/__tests__/verifyingKey.test.ts new file mode 100644 index 0000000..9254bd7 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/__tests__/verifyingKey.test.ts @@ -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); + }); + }); +}); diff --git a/packages/hardhat/maci-ts/domainobjs/ballot.ts b/packages/hardhat/maci-ts/domainobjs/ballot.ts new file mode 100644 index 0000000..080f0f1 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/ballot.ts @@ -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; + } +} diff --git a/packages/hardhat/maci-ts/domainobjs/commands/PCommand.ts b/packages/hardhat/maci-ts/domainobjs/commands/PCommand.ts new file mode 100644 index 0000000..4c6adac --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/commands/PCommand.ts @@ -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 => + 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; + } +} diff --git a/packages/hardhat/maci-ts/domainobjs/commands/TCommand.ts b/packages/hardhat/maci-ts/domainobjs/commands/TCommand.ts new file mode 100644 index 0000000..86f3405 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/commands/TCommand.ts @@ -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 => 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)); + } +} diff --git a/packages/hardhat/maci-ts/domainobjs/commands/index.ts b/packages/hardhat/maci-ts/domainobjs/commands/index.ts new file mode 100644 index 0000000..24323c4 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/commands/index.ts @@ -0,0 +1,3 @@ +export { TCommand } from "./TCommand"; +export { PCommand } from "./PCommand"; +export type { ICommand, IJsonCommand, IJsonTCommand, IJsonPCommand } from "./types"; diff --git a/packages/hardhat/maci-ts/domainobjs/commands/types.ts b/packages/hardhat/maci-ts/domainobjs/commands/types.ts new file mode 100644 index 0000000..03a96de --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/commands/types.ts @@ -0,0 +1,38 @@ +/** + * @notice A parent interface for all the commands + */ +export interface ICommand { + cmdType: bigint; + copy: () => T; + equals: (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; +} diff --git a/packages/hardhat/maci-ts/domainobjs/constants.ts b/packages/hardhat/maci-ts/domainobjs/constants.ts new file mode 100644 index 0000000..aae4709 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/constants.ts @@ -0,0 +1,4 @@ +import { StateLeaf } from "./stateLeaf"; + +export const blankStateLeaf = StateLeaf.genBlankLeaf(); +export const blankStateLeafHash = blankStateLeaf.hash(); diff --git a/packages/hardhat/maci-ts/domainobjs/index.ts b/packages/hardhat/maci-ts/domainobjs/index.ts new file mode 100644 index 0000000..8d4a5ff --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/index.ts @@ -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"; diff --git a/packages/hardhat/maci-ts/domainobjs/keyPair.ts b/packages/hardhat/maci-ts/domainobjs/keyPair.ts new file mode 100644 index 0000000..137e07f --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/keyPair.ts @@ -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)); + } +} diff --git a/packages/hardhat/maci-ts/domainobjs/message.ts b/packages/hardhat/maci-ts/domainobjs/message.ts new file mode 100644 index 0000000..b91097f --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/message.ts @@ -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)), + ); + } +} diff --git a/packages/hardhat/maci-ts/domainobjs/privateKey.ts b/packages/hardhat/maci-ts/domainobjs/privateKey.ts new file mode 100644 index 0000000..17be2e8 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/privateKey.ts @@ -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); + } +} diff --git a/packages/hardhat/maci-ts/domainobjs/publicKey.ts b/packages/hardhat/maci-ts/domainobjs/publicKey.ts new file mode 100644 index 0000000..edc740f --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/publicKey.ts @@ -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); + } +} diff --git a/packages/hardhat/maci-ts/domainobjs/stateLeaf.ts b/packages/hardhat/maci-ts/domainobjs/stateLeaf.ts new file mode 100644 index 0000000..6d5f618 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/stateLeaf.ts @@ -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)); + } +} diff --git a/packages/hardhat/maci-ts/domainobjs/types.ts b/packages/hardhat/maci-ts/domainobjs/types.ts new file mode 100644 index 0000000..7c288b5 --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/types.ts @@ -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; + +export type IJsonPublicKey = Pick; + +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; +} diff --git a/packages/hardhat/maci-ts/domainobjs/verifyingKey.ts b/packages/hardhat/maci-ts/domainobjs/verifyingKey.ts new file mode 100644 index 0000000..7b6869d --- /dev/null +++ b/packages/hardhat/maci-ts/domainobjs/verifyingKey.ts @@ -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); + }; +} diff --git a/packages/hardhat/maci-ts/templates/EmptyBallotRoots.sol.template b/packages/hardhat/maci-ts/templates/EmptyBallotRoots.sol.template new file mode 100644 index 0000000..f0ce1bf --- /dev/null +++ b/packages/hardhat/maci-ts/templates/EmptyBallotRoots.sol.template @@ -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 %> + } +} diff --git a/packages/hardhat/maci-ts/templates/MerkleZeros.sol.template b/packages/hardhat/maci-ts/templates/MerkleZeros.sol.template new file mode 100644 index 0000000..6e7d011 --- /dev/null +++ b/packages/hardhat/maci-ts/templates/MerkleZeros.sol.template @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +abstract contract MerkleZeros { + uint256[<% NUM_ZEROS %>] internal zeros; + + // <% COMMENT %> + constructor() { +<% ZEROS %> + } +} diff --git a/packages/hardhat/maci-ts/ts/abi.ts b/packages/hardhat/maci-ts/ts/abi.ts new file mode 100644 index 0000000..f3ec2e7 --- /dev/null +++ b/packages/hardhat/maci-ts/ts/abi.ts @@ -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]; +}; diff --git a/packages/hardhat/maci-ts/ts/buildPoseidon.ts b/packages/hardhat/maci-ts/ts/buildPoseidon.ts new file mode 100644 index 0000000..7b71d12 --- /dev/null +++ b/packages/hardhat/maci-ts/ts/buildPoseidon.ts @@ -0,0 +1,20 @@ +import { poseidonContract } from "circomlibjs"; +import hre from "hardhat"; + +type ExtendedHre = typeof hre & { overwriteArtifact: (name: string, code: unknown) => Promise }; + +const buildPoseidon = async (numInputs: number) => { + await (hre as ExtendedHre).overwriteArtifact(`PoseidonT${numInputs + 1}`, poseidonContract.createCode(numInputs)); +}; + +export const buildPoseidonT3 = (): Promise => buildPoseidon(2); +export const buildPoseidonT4 = (): Promise => buildPoseidon(3); +export const buildPoseidonT5 = (): Promise => buildPoseidon(4); +export const buildPoseidonT6 = (): Promise => buildPoseidon(5); + +if (require.main === module) { + buildPoseidonT3(); + buildPoseidonT4(); + buildPoseidonT5(); + buildPoseidonT6(); +} diff --git a/packages/hardhat/maci-ts/ts/constants.ts b/packages/hardhat/maci-ts/ts/constants.ts new file mode 100644 index 0000000..6d00761 --- /dev/null +++ b/packages/hardhat/maci-ts/ts/constants.ts @@ -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"); diff --git a/packages/hardhat/maci-ts/ts/deploy.ts b/packages/hardhat/maci-ts/ts/deploy.ts new file mode 100644 index 0000000..eaea7ae --- /dev/null +++ b/packages/hardhat/maci-ts/ts/deploy.ts @@ -0,0 +1,378 @@ +import { type ContractFactory, type Signer, BaseContract } from "ethers"; + +import type { IDeployMaciArgs, IDeployedMaci, IDeployedPoseidonContracts } from "./types"; + +import { + AccQueueQuinaryMaci, + ConstantInitialVoiceCreditProxy, + FreeForAllGatekeeper, + PoseidonT3__factory as PoseidonT3Factory, + PoseidonT4__factory as PoseidonT4Factory, + PoseidonT5__factory as PoseidonT5Factory, + PoseidonT6__factory as PoseidonT6Factory, + MACI, + MockVerifier, + PollFactory, + MessageProcessorFactory, + SubsidyFactory, + TallyFactory, + PoseidonT3, + PoseidonT4, + PoseidonT5, + PoseidonT6, + SignUpToken, + SignUpTokenGatekeeper, + TopupCredit, + Verifier, + VkRegistry, + TallyNonQvFactory, +} from "../../typechain-types"; + +import { parseArtifact } from "./abi"; +import { getDefaultSigner, getFeeData, log } from "./utils"; + +/** + * Link Poseidon libraries to a Smart Contract + * @param solFileToLink - the name of the contract to link the libraries to + * @param poseidonT3Address - the address of the PoseidonT3 contract + * @param poseidonT4Address - the address of the PoseidonT4 contract + * @param poseidonT5Address - the address of the PoseidonT5 contract + * @param poseidonT6Address - the address of the PoseidonT6 contract + * @param signer - the signer to use to deploy the contract + * @param quiet - whether to suppress console output + * @returns a contract factory with the libraries linked + */ +export const linkPoseidonLibraries = async ( + solFileToLink: string, + poseidonT3Address: string, + poseidonT4Address: string, + poseidonT5Address: string, + poseidonT6Address: string, + signer?: Signer, + quiet = false, +): Promise => { + log(`Linking Poseidon libraries to ${solFileToLink}`, quiet); + const { ethers } = await import("hardhat"); + + const contractFactory = await ethers.getContractFactory(solFileToLink, { + signer: signer || (await getDefaultSigner()), + libraries: { + PoseidonT3: poseidonT3Address, + PoseidonT4: poseidonT4Address, + PoseidonT5: poseidonT5Address, + PoseidonT6: poseidonT6Address, + }, + }); + + return contractFactory; +}; + +/** + * Deploy a Smart Contract given a name and some arguments + * @param contractName - the name of the contract + * @param signer - the signer to use to deploy the contract + * @param quiet - whether to suppress console output + * @param args - the constructor arguments of the contract + */ +export const deployContract = async ( + contractName: string, + signer?: Signer, + quiet = false, + ...args: unknown[] +): Promise => { + log(`Deploying ${contractName}`, quiet); + const { ethers } = await import("hardhat"); + + const contractFactory = await ethers.getContractFactory(contractName, signer || (await getDefaultSigner())); + const feeData = await getFeeData(); + const contract = await contractFactory.deploy(...args, { + maxFeePerGas: feeData?.maxFeePerGas, + maxPriorityFeePerGas: feeData?.maxPriorityFeePerGas, + }); + await contract.deploymentTransaction()!.wait(); + + return contract as unknown as T; +}; + +/** + * Deploy a TopupCredit contract + * @param signer - the signer to use to deploy the contract + * @param quiet - whether to suppress console output + * @returns the deployed TopupCredit contract + */ +export const deployTopupCredit = async (signer?: Signer, quiet = false): Promise => + deployContract("TopupCredit", signer, quiet); + +/** + * Deploy a VkRegistry contract + * @param signer - the signer to use to deploy the contract + * @param quiet - whether to suppress console output + * @returns the deployed VkRegistry contract + */ +export const deployVkRegistry = async (signer?: Signer, quiet = false): Promise => + deployContract("VkRegistry", signer, quiet); + +/** + * Deploy a MockVerifier contract (testing only) + * @param signer - the signer to use to deploy the contract + * @param quiet - whether to suppress console output + * @returns the deployed MockVerifier contract + */ +export const deployMockVerifier = async (signer?: Signer, quiet = false): Promise => + deployContract("MockVerifier", signer, quiet); + +/** + * Deploy a Verifier contract + * @param signer - the signer to use to deploy the contract + * @param quiet - whether to suppress console output + * @returns the deployed Verifier contract + */ +export const deployVerifier = async (signer?: Signer, quiet = false): Promise => + deployContract("Verifier", signer, quiet); + +/** + * Deploy a constant initial voice credit proxy contract + * @param signer - the signer to use to deploy the contract + * @param amount - the amount of initial voice credit to give to each user + * @param quiet - whether to suppress console output + * @returns the deployed ConstantInitialVoiceCreditProxy contract + */ +export const deployConstantInitialVoiceCreditProxy = async ( + amount: number, + signer?: Signer, + quiet = false, +): Promise => + deployContract("ConstantInitialVoiceCreditProxy", signer, quiet, amount.toString()); + +/** + * Deploy a SignUpToken contract + * @param signer - the signer to use to deploy the contract + * @param quiet - whether to suppress console output + * @returns the deployed SignUpToken contract + */ +export const deploySignupToken = async (signer?: Signer, quiet = false): Promise => + deployContract("SignUpToken", signer, quiet); + +/** + * Deploy a SignUpTokenGatekeeper contract + * @param signUpTokenAddress - the address of the SignUpToken contract + * @param signer - the signer to use to deploy the contract + * @param quiet - whether to suppress console output + * @returns a SignUpTokenGatekeeper contract + */ +export const deploySignupTokenGatekeeper = async ( + signUpTokenAddress: string, + signer?: Signer, + quiet = false, +): Promise => + deployContract("SignUpTokenGatekeeper", signer, quiet, signUpTokenAddress); + +/** + * Deploy a FreeForAllGatekeeper contract + * @param signer - the signer to use to deploy the contract + * @param quiet - whether to suppress console output + * @returns the deployed FreeForAllGatekeeper contract + */ +export const deployFreeForAllSignUpGatekeeper = async (signer?: Signer, quiet = false): Promise => + deployContract("FreeForAllGatekeeper", signer, quiet); + +/** + * Deploy Poseidon contracts + * @param signer - the signer to use to deploy the contracts + * @param quiet - whether to suppress console output + * @returns the deployed Poseidon contracts + */ +export const deployPoseidonContracts = async ( + signer?: Signer, + { poseidonT3, poseidonT4, poseidonT5, poseidonT6 }: IDeployMaciArgs["poseidonAddresses"] = {}, + quiet = false, +): Promise => { + const [PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract] = await Promise.all([ + !poseidonT3 ? await deployContract("PoseidonT3", signer, quiet) : PoseidonT3Factory.connect(poseidonT3), + !poseidonT4 ? await deployContract("PoseidonT4", signer, quiet) : PoseidonT4Factory.connect(poseidonT4), + !poseidonT5 ? await deployContract("PoseidonT5", signer, quiet) : PoseidonT5Factory.connect(poseidonT5), + !poseidonT6 ? await deployContract("PoseidonT6", signer, quiet) : PoseidonT6Factory.connect(poseidonT6), + ]); + + return { + PoseidonT3Contract, + PoseidonT4Contract, + PoseidonT5Contract, + PoseidonT6Contract, + }; +}; + +/** + * Deploy a contract with linked libraries + * @param contractFactory - the contract factory to use + * @param name - the name of the contract + * @param quiet - whether to suppress console output + * @param args - the constructor arguments of the contract + * @returns the deployed contract instance + */ +export const deployContractWithLinkedLibraries = async ( + contractFactory: ContractFactory, + name: string, + quiet = false, + ...args: unknown[] +): Promise => { + log(`Deploying ${name}`, quiet); + const feeData = await getFeeData(); + const contract = await contractFactory.deploy(...args, { + maxFeePerGas: feeData?.maxFeePerGas, + maxPriorityFeePerGas: feeData?.maxPriorityFeePerGas, + }); + await contract.deploymentTransaction()!.wait(); + + return contract as T; +}; + +/** + * Deploy a Poll Factory contract + * @param signer - the signer object to use to deploy the contract + * @param quiet - whether to suppress console output + * @returns the deployed Poll Factory contract + */ +export const deployPollFactory = async (signer: Signer, quiet = false): Promise => { + const poseidonContracts = await deployPoseidonContracts(signer, {}, quiet); + const [poseidonT3Contract, poseidonT4Contract, poseidonT5Contract, poseidonT6Contract] = await Promise.all([ + poseidonContracts.PoseidonT3Contract.getAddress(), + poseidonContracts.PoseidonT4Contract.getAddress(), + poseidonContracts.PoseidonT5Contract.getAddress(), + poseidonContracts.PoseidonT6Contract.getAddress(), + ]); + const contractFactory = await linkPoseidonLibraries( + "PollFactory", + poseidonT3Contract, + poseidonT4Contract, + poseidonT5Contract, + poseidonT6Contract, + signer, + quiet, + ); + return deployContractWithLinkedLibraries(contractFactory, "PollFactory", quiet); +}; + +/** + * Deploy a MACI contract + * @param {IDeployMaciArgs} args - deploy arguments + * @returns {IDeployedMaci} the deployed MACI contract + */ +export const deployMaci = async ({ + signUpTokenGatekeeperContractAddress, + initialVoiceCreditBalanceAddress, + topupCreditContractAddress, + signer, + poseidonAddresses, + stateTreeDepth = 10, + useQv = true, + quiet = true, +}: IDeployMaciArgs): Promise => { + const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } = + await deployPoseidonContracts(signer, poseidonAddresses, quiet); + + const poseidonAddrs = await Promise.all([ + PoseidonT3Contract.getAddress(), + PoseidonT4Contract.getAddress(), + PoseidonT5Contract.getAddress(), + PoseidonT6Contract.getAddress(), + ]).then(([poseidonT3, poseidonT4, poseidonT5, poseidonT6]) => ({ + poseidonT3, + poseidonT4, + poseidonT5, + poseidonT6, + })); + + const contractsToLink = [ + "MACI", + "PollFactory", + "MessageProcessorFactory", + "TallyFactory", + "TallyNonQvFactory", + "SubsidyFactory", + ]; + + // Link Poseidon contracts to MACI + const linkedContractFactories = await Promise.all( + contractsToLink.map(async (contractName: string) => + linkPoseidonLibraries( + contractName, + poseidonAddrs.poseidonT3, + poseidonAddrs.poseidonT4, + poseidonAddrs.poseidonT5, + poseidonAddrs.poseidonT6, + signer, + quiet, + ), + ), + ); + + const [ + maciContractFactory, + pollFactoryContractFactory, + messageProcessorFactory, + tallyFactory, + tallyFactoryNonQv, + subsidyFactory, + ] = await Promise.all(linkedContractFactories); + + const pollFactoryContract = await deployContractWithLinkedLibraries( + pollFactoryContractFactory, + "PollFactory", + quiet, + ); + + const messageProcessorFactoryContract = await deployContractWithLinkedLibraries( + messageProcessorFactory, + "MessageProcessorFactory", + quiet, + ); + + // deploy either the qv or non qv tally factory - they both implement the same interface + // so as long as maci is concerned, they are interchangeable + const tallyFactoryContract = useQv + ? await deployContractWithLinkedLibraries(tallyFactory, "TallyFactory", quiet) + : await deployContractWithLinkedLibraries(tallyFactoryNonQv, "TallyNonQvFactory", quiet); + + const subsidyFactoryContract = await deployContractWithLinkedLibraries( + subsidyFactory, + "SubsidyFactory", + quiet, + ); + + const [pollAddr, mpAddr, tallyAddr, subsidyAddr] = await Promise.all([ + pollFactoryContract.getAddress(), + messageProcessorFactoryContract.getAddress(), + tallyFactoryContract.getAddress(), + subsidyFactoryContract.getAddress(), + ]); + + const maciContract = await deployContractWithLinkedLibraries( + maciContractFactory, + "MACI", + quiet, + pollAddr, + mpAddr, + tallyAddr, + subsidyAddr, + signUpTokenGatekeeperContractAddress, + initialVoiceCreditBalanceAddress, + topupCreditContractAddress, + stateTreeDepth, + ); + + const [AccQueueQuinaryMaciAbi] = parseArtifact("AccQueue"); + const stateAqContractAddress = await maciContract.stateAq(); + const stateAqContract = new BaseContract( + stateAqContractAddress, + AccQueueQuinaryMaciAbi, + await getDefaultSigner(), + ) as AccQueueQuinaryMaci; + + return { + maciContract, + stateAqContract, + pollFactoryContract, + poseidonAddrs, + }; +}; diff --git a/packages/hardhat/maci-ts/ts/deployer.ts b/packages/hardhat/maci-ts/ts/deployer.ts new file mode 100644 index 0000000..f1fa930 --- /dev/null +++ b/packages/hardhat/maci-ts/ts/deployer.ts @@ -0,0 +1,46 @@ +import { type Contract, type Signer, ContractFactory, Interface, JsonRpcProvider, Wallet } from "ethers"; + +import type { TAbi } from "./abi"; + +/** + * A class that can deploy smart contracts using a JSON-RPC provider. + */ +export class JSONRPCDeployer { + provider: JsonRpcProvider; + + signer: Signer; + + /** + * Generate a new JSONRPCDeployer instance. + * @param privateKey - the private key of the deployer + * @param providerUrl - the URL of the JSON-RPC provider + */ + constructor(privateKey: string, providerUrl: string) { + this.provider = new JsonRpcProvider(providerUrl); + this.signer = new Wallet(privateKey, this.provider); + } + + /** + * Deploy a new smart contract using the deployer's signer. + * @param abi - the ABI of the contract + * @param bytecode - the bytecode of the contract + * @param args - the constructor arguments of the contract + * @returns a Contract object + */ + async deploy(abi: TAbi, bytecode: string, ...args: unknown[]): Promise { + const contractInterface = new Interface(abi); + const factory = new ContractFactory(contractInterface, bytecode, this.signer); + const contract = await factory.deploy(...args); + + return contract as Contract; + } +} + +/** + * Generate a new JSONRPCDeployer instance. + * @param privateKey - the private key of the deployer + * @param url - the URL of the JSON-RPC provider + * @returns the deployer instance + */ +export const genJsonRpcDeployer = (privateKey: string, url: string): JSONRPCDeployer => + new JSONRPCDeployer(privateKey, url); diff --git a/packages/hardhat/maci-ts/ts/genEmptyBallotRootsContract.ts b/packages/hardhat/maci-ts/ts/genEmptyBallotRootsContract.ts new file mode 100644 index 0000000..7b8fe08 --- /dev/null +++ b/packages/hardhat/maci-ts/ts/genEmptyBallotRootsContract.ts @@ -0,0 +1,37 @@ +import { IncrementalQuinTree, hash5 } from "../crypto"; +import { Ballot } from "../domainobjs"; + +import fs from "fs"; +import path from "path"; + +const genEmptyBallotRootsContract = (): string => { + const template = fs + .readFileSync(path.resolve(__dirname, "..", "templates", "EmptyBallotRoots.sol.template")) + .toString(); + + // This hard-coded value should be consistent with the value of `stateTreeDepth` of MACI.sol + const stateTreeDepth = process.env.STATE_TREE_DEPTH ? Number.parseInt(process.env.STATE_TREE_DEPTH, 10) : 10; + + let r = ""; + for (let i = 1; i < 6; i += 1) { + const ballot = new Ballot(0, i); + const z = ballot.hash(); + // The empty Ballot tree root + const ballotTree = new IncrementalQuinTree(stateTreeDepth, BigInt(z.toString()), 5, hash5); + + r += ` emptyBallotRoots[${i - 1}] = uint256(${ballotTree.root});${i !== 5 ? "\n" : ""}`; + } + + const generated = template.replace("<% ROOTS %>", r); + return generated.trim(); +}; + +if (require.main === module) { + const generated = genEmptyBallotRootsContract(); + fs.writeFileSync( + path.resolve(__dirname, "..", "..", "contracts", "maci-contracts", "trees", "EmptyBallotRoots.sol"), + `${generated}\n`, + ); +} + +export { genEmptyBallotRootsContract }; diff --git a/packages/hardhat/maci-ts/ts/genMaciState.ts b/packages/hardhat/maci-ts/ts/genMaciState.ts new file mode 100644 index 0000000..5cc2fb8 --- /dev/null +++ b/packages/hardhat/maci-ts/ts/genMaciState.ts @@ -0,0 +1,367 @@ +/* eslint-disable no-underscore-dangle */ +import { type Provider, type Log, Interface, BaseContract } from "ethers"; +import { MaciState, STATE_TREE_ARITY } from "../core"; +import { type Keypair, PubKey, Message } from "../domainobjs"; + +import assert from "assert"; + +import type { Action } from "./types"; +import type { MACI, Poll } from "../../typechain-types"; + +import { parseArtifact } from "./abi"; +import { sleep, sortActions } from "./utils"; + +/** + * Generate a MaciState object from the events of a MACI and Poll smart contracts + * @param provider - the ethereum provider + * @param address - the address of the MACI contract + * @param coordinatorKeypair - the keypair of the coordinator + * @param pollId - the id of the poll for which we are fetching events + * @param fromBlock - the block number from which to start fetching events + * @param blocksPerRequest - the number of blocks to fetch in each request + * @param endBlock - the block number at which to stop fetching events + * @param sleepAmount - the amount of time to sleep between each request + * @returns an instance of MaciState + */ +export const genMaciStateFromContract = async ( + provider: Provider, + address: string, + coordinatorKeypair: Keypair, + pollId: bigint, + fromBlock = 0, + blocksPerRequest = 50, + endBlock: number | undefined = undefined, + sleepAmount: number | undefined = undefined, +): Promise => { + // ensure the pollId is valid + assert(pollId >= 0); + + const [pollContractAbi] = parseArtifact("Poll"); + const [maciContractAbi] = parseArtifact("MACI"); + + const maciContract = new BaseContract(address, maciContractAbi, provider) as MACI; + + const maciIface = new Interface(maciContractAbi); + const pollIface = new Interface(pollContractAbi); + + // Check stateTreeDepth + const stateTreeDepth = await maciContract.stateTreeDepth(); + + // we need to pass the stateTreeDepth + const maciState = new MaciState(Number(stateTreeDepth)); + // ensure it is set correctly + assert(stateTreeDepth === BigInt(maciState.stateTreeDepth)); + + let signUpLogs: Log[] = []; + let deployPollLogs: Log[] = []; + + // if no last block is set then we fetch until the current block number + const lastBlock = endBlock || (await provider.getBlockNumber()); + + // Fetch event logs in batches (lastBlock inclusive) + for (let i = fromBlock; i <= lastBlock; i += blocksPerRequest + 1) { + // the last block batch will be either current iteration block + blockPerRequest + // or the end block if it is set + const toBlock = i + blocksPerRequest >= lastBlock ? lastBlock : i + blocksPerRequest; + + const [tmpSignUpLogs, tmpDeployPollLogs] = + // eslint-disable-next-line no-await-in-loop + await Promise.all([ + maciContract.queryFilter(maciContract.filters.SignUp(), i, toBlock), + maciContract.queryFilter(maciContract.filters.DeployPoll(), i, toBlock), + ]); + + signUpLogs = signUpLogs.concat(tmpSignUpLogs); + deployPollLogs = deployPollLogs.concat(tmpDeployPollLogs); + + if (sleepAmount) { + // eslint-disable-next-line no-await-in-loop + await sleep(sleepAmount); + } + } + + let actions: Action[] = []; + + signUpLogs.forEach(log => { + assert(!!log); + const mutableLog = { ...log, topics: [...log.topics] }; + const event = maciIface.parseLog(mutableLog) as unknown as { + args: { + _stateIndex: number; + _userPubKeyX: string; + _userPubKeyY: string; + _voiceCreditBalance: number; + _timestamp: number; + }; + }; + + actions.push({ + type: "SignUp", + blockNumber: log.blockNumber, + transactionIndex: log.transactionIndex, + data: { + stateIndex: Number(event.args._stateIndex), + pubKey: new PubKey([BigInt(event.args._userPubKeyX), BigInt(event.args._userPubKeyY)]), + voiceCreditBalance: Number(event.args._voiceCreditBalance), + timestamp: Number(event.args._timestamp), + }, + }); + }); + + let index = 0n; + const foundPollIds: number[] = []; + const pollContractAddresses = new Map(); + + deployPollLogs.forEach(log => { + assert(!!log); + const mutableLogs = { ...log, topics: [...log.topics] }; + const event = maciIface.parseLog(mutableLogs) as unknown as { + args: { + _coordinatorPubKeyX: string; + _coordinatorPubKeyY: string; + _pollId: bigint; + pollAddr: { + poll: string; + messageProcessor: string; + tally: string; + }; + }; + }; + + const pubKey = new PubKey([BigInt(event.args._coordinatorPubKeyX), BigInt(event.args._coordinatorPubKeyY)]); + + const p = event.args._pollId; + assert(p === index); + + const pollAddr = event.args.pollAddr.poll; + actions.push({ + type: "DeployPoll", + blockNumber: log.blockNumber, + transactionIndex: log.transactionIndex, + data: { pollId: p, pollAddr, pubKey }, + }); + + foundPollIds.push(Number(p)); + pollContractAddresses.set(BigInt(p), pollAddr); + index += 1n; + }); + + // Check whether each pollId exists + assert(foundPollIds.includes(Number(pollId)), "Error: the specified pollId does not exist on-chain"); + + const pollContractAddress = pollContractAddresses.get(pollId)!; + const pollContract = new BaseContract(pollContractAddress, pollContractAbi, provider) as Poll; + + const coordinatorPubKeyOnChain = await pollContract.coordinatorPubKey(); + assert(coordinatorPubKeyOnChain[0].toString() === coordinatorKeypair.pubKey.rawPubKey[0].toString()); + assert(coordinatorPubKeyOnChain[1].toString() === coordinatorKeypair.pubKey.rawPubKey[1].toString()); + + const dd = await pollContract.getDeployTimeAndDuration(); + const deployTime = Number(dd[0]); + const duration = Number(dd[1]); + const onChainMaxValues = await pollContract.maxValues(); + const onChainTreeDepths = await pollContract.treeDepths(); + + const maxValues = { + maxMessages: Number(onChainMaxValues.maxMessages), + maxVoteOptions: Number(onChainMaxValues.maxVoteOptions), + }; + const treeDepths = { + intStateTreeDepth: Number(onChainTreeDepths.intStateTreeDepth), + messageTreeDepth: Number(onChainTreeDepths.messageTreeDepth), + messageTreeSubDepth: Number(onChainTreeDepths.messageTreeSubDepth), + voteOptionTreeDepth: Number(onChainTreeDepths.voteOptionTreeDepth), + }; + const batchSizes = { + tallyBatchSize: STATE_TREE_ARITY ** Number(onChainTreeDepths.intStateTreeDepth), + subsidyBatchSize: STATE_TREE_ARITY ** Number(onChainTreeDepths.intStateTreeDepth), + messageBatchSize: STATE_TREE_ARITY ** Number(onChainTreeDepths.messageTreeSubDepth), + }; + + // fetch poll contract logs + let publishMessageLogs: Log[] = []; + let topupLogs: Log[] = []; + let mergeMaciStateAqSubRootsLogs: Log[] = []; + let mergeMaciStateAqLogs: Log[] = []; + let mergeMessageAqSubRootsLogs: Log[] = []; + let mergeMessageAqLogs: Log[] = []; + + for (let i = fromBlock; i <= lastBlock; i += blocksPerRequest + 1) { + const toBlock = i + blocksPerRequest >= lastBlock ? lastBlock : i + blocksPerRequest; + + const [ + tmpPublishMessageLogs, + tmpTopupLogs, + tmpMergeMaciStateAqSubRootsLogs, + tmpMergeMaciStateAqLogs, + tmpMergeMessageAqSubRootsLogs, + tmpMergeMessageAqLogs, + // eslint-disable-next-line no-await-in-loop + ] = await Promise.all([ + pollContract.queryFilter(pollContract.filters.PublishMessage(), i, toBlock), + pollContract.queryFilter(pollContract.filters.TopupMessage(), i, toBlock), + pollContract.queryFilter(pollContract.filters.MergeMaciStateAqSubRoots(), i, toBlock), + pollContract.queryFilter(pollContract.filters.MergeMaciStateAq(), i, toBlock), + pollContract.queryFilter(pollContract.filters.MergeMessageAqSubRoots(), i, toBlock), + pollContract.queryFilter(pollContract.filters.MergeMessageAq(), i, toBlock), + ]); + + publishMessageLogs = publishMessageLogs.concat(tmpPublishMessageLogs); + topupLogs = topupLogs.concat(tmpTopupLogs); + mergeMaciStateAqSubRootsLogs = mergeMaciStateAqSubRootsLogs.concat(tmpMergeMaciStateAqSubRootsLogs); + mergeMaciStateAqLogs = mergeMaciStateAqLogs.concat(tmpMergeMaciStateAqLogs); + mergeMessageAqSubRootsLogs = mergeMessageAqSubRootsLogs.concat(tmpMergeMessageAqSubRootsLogs); + mergeMessageAqLogs = mergeMessageAqLogs.concat(tmpMergeMessageAqLogs); + + if (sleepAmount) { + // eslint-disable-next-line no-await-in-loop + await sleep(sleepAmount); + } + } + + publishMessageLogs.forEach(log => { + assert(!!log); + const mutableLogs = { ...log, topics: [...log.topics] }; + const event = pollIface.parseLog(mutableLogs) as unknown as { + args: { _message: [string, string[]]; _encPubKey: string[] }; + }; + + const message = new Message( + BigInt(event.args._message[0]), + + event.args._message[1].map(x => BigInt(x)), + ); + + const encPubKey = new PubKey(event.args._encPubKey.map(x => BigInt(x.toString())) as [bigint, bigint]); + + actions.push({ + type: "PublishMessage", + blockNumber: log.blockNumber, + transactionIndex: log.transactionIndex, + data: { + message, + encPubKey, + }, + }); + }); + + topupLogs.forEach(log => { + assert(!!log); + const mutableLog = { ...log, topics: [...log.topics] }; + const event = pollIface.parseLog(mutableLog) as unknown as { + args: { _message: [string, string[]] }; + }; + const message = new Message( + BigInt(event.args._message[0]), + event.args._message[1].map(x => BigInt(x)), + ); + + actions.push({ + type: "TopupMessage", + blockNumber: log.blockNumber, + transactionIndex: log.transactionIndex, + data: { + message, + }, + }); + }); + + mergeMessageAqSubRootsLogs.forEach(log => { + assert(!!log); + const mutableLogs = { ...log, topics: [...log.topics] }; + const event = pollIface.parseLog(mutableLogs) as unknown as { args: { _numSrQueueOps: string } }; + + const numSrQueueOps = Number(event.args._numSrQueueOps); + actions.push({ + type: "MergeMessageAqSubRoots", + blockNumber: log.blockNumber, + transactionIndex: log.transactionIndex, + data: { + numSrQueueOps, + }, + }); + }); + + mergeMessageAqLogs.forEach(log => { + assert(!!log); + const mutableLogs = { ...log, topics: [...log.topics] }; + const event = pollIface.parseLog(mutableLogs); + + const messageRoot = BigInt((event?.args as unknown as { _messageRoot: string })._messageRoot); + actions.push({ + type: "MergeMessageAq", + blockNumber: log.blockNumber, + transactionIndex: log.transactionIndex, + data: { messageRoot }, + }); + }); + + // Sort actions + actions = sortActions(actions); + + // Reconstruct MaciState in order + actions.forEach(action => { + switch (true) { + case action.type === "SignUp": { + const { pubKey, voiceCreditBalance, timestamp } = action.data; + + maciState.signUp(pubKey!, BigInt(voiceCreditBalance!), BigInt(timestamp!)); + break; + } + + case action.type === "DeployPoll" && action.data.pollId?.toString() === pollId.toString(): { + maciState.deployPoll( + BigInt(deployTime + duration), + maxValues, + treeDepths, + batchSizes.messageBatchSize, + coordinatorKeypair, + ); + break; + } + + case action.type === "DeployPoll" && action.data.pollId?.toString() !== pollId.toString(): { + maciState.deployNullPoll(); + break; + } + + case action.type === "PublishMessage": { + const { encPubKey, message } = action.data; + maciState.polls.get(pollId)?.publishMessage(message!, encPubKey!); + break; + } + + case action.type === "TopupMessage": { + const { message } = action.data; + maciState.polls.get(pollId)?.topupMessage(message!); + break; + } + + // ensure that the message root is correct (i.e. all messages have been published offchain) + case action.type === "MergeMessageAq": { + assert(maciState.polls.get(pollId)?.messageTree.root.toString() === action.data.messageRoot?.toString()); + break; + } + + default: + break; + } + }); + + // Set numSignUps + const numSignUpsAndMessages = await pollContract.numSignUpsAndMessages(); + + const poll = maciState.polls.get(pollId); + + // ensure all messages were recorded + assert(Number(numSignUpsAndMessages[1]) === poll?.messages.length); + // set the number of signups + poll.updatePoll(numSignUpsAndMessages[0]); + + // we need to ensure that the stateRoot is correct + assert(poll.stateTree?.root.toString() === (await pollContract.mergedStateRoot()).toString()); + + maciState.polls.set(pollId, poll); + + return maciState; +}; diff --git a/packages/hardhat/maci-ts/ts/genZerosContract.ts b/packages/hardhat/maci-ts/ts/genZerosContract.ts new file mode 100644 index 0000000..6ae7ba7 --- /dev/null +++ b/packages/hardhat/maci-ts/ts/genZerosContract.ts @@ -0,0 +1,68 @@ +import { sha256Hash, hashLeftRight, hash5 } from "../crypto"; + +import assert from "assert"; +import fs from "fs"; +import path from "path"; + +const genZerosContract = ( + contractName: string, + zeroVal: bigint, + hashLength: number, + numZeros: number, + comment: string, + useSha256: boolean, + subDepth: number, +): string => { + assert(hashLength === 2 || hashLength === 5); + + const template = fs.readFileSync(path.resolve(__dirname, "..", "templates", "MerkleZeros.sol.template")).toString(); + + const zeros: bigint[] = [zeroVal]; + for (let i = 1; i < numZeros; i += 1) { + const z = zeros[i - 1]; + let hashed: bigint; + + if (useSha256 && i <= subDepth) { + if (hashLength === 2) { + hashed = sha256Hash([z, z]); + } else { + hashed = sha256Hash([z, z, z, z, z]); + } + } else if (hashLength === 2) { + hashed = hashLeftRight(z, z); + } else { + hashed = hash5([z, z, z, z, z]); + } + + zeros.push(hashed); + } + + let z = ""; + for (let i = 0; i < zeros.length; i += 1) { + z += ` zeros[${i}] = uint256(${zeros[i]});\n`; + } + + const generated = template + .replace("<% CONTRACT_NAME %>", contractName) + .replace("<% NUM_ZEROS %>", numZeros.toString()) + .replace("<% ZEROS %>", ` ${z.trim()}`) + .replace("<% COMMENT %>", comment.trim()); + + return generated.trim(); +}; + +if (require.main === module) { + const contractName = process.argv[2]; + const zero = BigInt(process.argv[3]); + const hashLength = Number(process.argv[4]); + const numZeros = Number(process.argv[5]); + const comment = process.argv[6]; + const useSha256 = process.argv[7] === "1"; + const subDepth = Number(process.argv[8]); + + const generated = genZerosContract(contractName, zero, hashLength, numZeros, comment, useSha256, subDepth); + // eslint-disable-next-line no-console + console.log(generated); +} + +export { genZerosContract }; diff --git a/packages/hardhat/maci-ts/ts/index.ts b/packages/hardhat/maci-ts/ts/index.ts new file mode 100644 index 0000000..e4013a8 --- /dev/null +++ b/packages/hardhat/maci-ts/ts/index.ts @@ -0,0 +1,23 @@ +export { + deployMockVerifier, + deployTopupCredit, + deployVkRegistry, + deployMaci, + deployContract, + deployContractWithLinkedLibraries, + deploySignupToken, + deploySignupTokenGatekeeper, + deployConstantInitialVoiceCreditProxy, + deployFreeForAllSignUpGatekeeper, + deployPollFactory, + linkPoseidonLibraries, + deployPoseidonContracts, + deployVerifier, +} from "./deploy"; +export { genJsonRpcDeployer } from "./deployer"; +export { genMaciStateFromContract } from "./genMaciState"; +export { formatProofForVerifierContract, getDefaultSigner, getDefaultNetwork, getSigners } from "./utils"; +export { abiDir, solDir } from "./constants"; + +export type { IVerifyingKeyStruct, SnarkProof, Groth16Proof } from "./types"; +export * from "../../typechain-types"; diff --git a/packages/hardhat/maci-ts/ts/types.ts b/packages/hardhat/maci-ts/ts/types.ts new file mode 100644 index 0000000..a09dee6 --- /dev/null +++ b/packages/hardhat/maci-ts/ts/types.ts @@ -0,0 +1,185 @@ +import type { + AccQueueQuinaryMaci, + ConstantInitialVoiceCreditProxy, + FreeForAllGatekeeper, + MACI, + MockVerifier, + PollFactory, + PoseidonT3, + PoseidonT4, + PoseidonT5, + PoseidonT6, + TopupCredit, + VkRegistry, +} from "../../typechain-types"; +import type { BigNumberish, Signer } from "ethers"; +import type { CircuitInputs } from "../core"; +import type { Message, PubKey } from "../domainobjs"; +import type { PublicSignals } from "snarkjs"; + +/** + * The data structure of the verifying key of the SNARK circuit. + */ +export interface IVerifyingKeyStruct { + alpha1: { + x: BigNumberish; + y: BigNumberish; + }; + beta2: { + x: [BigNumberish, BigNumberish]; + y: [BigNumberish, BigNumberish]; + }; + gamma2: { + x: [BigNumberish, BigNumberish]; + y: [BigNumberish, BigNumberish]; + }; + delta2: { + x: [BigNumberish, BigNumberish]; + y: [BigNumberish, BigNumberish]; + }; + ic: { + x: BigNumberish; + y: BigNumberish; + }[]; +} + +/** + * The data structure representing a SNARK proof. + */ +export interface SnarkProof { + pi_a: bigint[]; + pi_b: bigint[][]; + pi_c: bigint[]; +} + +/** + * The data structure representing a Groth16 proof. + */ +export interface Groth16Proof { + pi_a: string[]; + pi_b: string[][]; + pi_c: string[]; + protocol: string; + curve: string; +} + +/** + * The data structure representing a proof output + */ +export interface Proof { + proof: SnarkProof | Groth16Proof; + circuitInputs: CircuitInputs; + publicInputs: PublicSignals; +} + +/** + * An interface holding all of the smart contracts part of MACI. + */ +export interface IDeployedTestContracts { + mockVerifierContract: MockVerifier; + gatekeeperContract: FreeForAllGatekeeper; + constantIntialVoiceCreditProxyContract: ConstantInitialVoiceCreditProxy; + maciContract: MACI; + stateAqContract: AccQueueQuinaryMaci; + vkRegistryContract: VkRegistry; + topupCreditContract: TopupCredit; +} + +/** + * An interface that represents an action that should + * be applied to a MaciState and its Polls within the + * genMaciState function. + */ +export interface Action { + type: string; + data: Partial<{ + pubKey: PubKey; + encPubKey: PubKey; + message: Message; + voiceCreditBalance: number; + timestamp: number; + stateIndex: number; + numSrQueueOps: number; + pollId: bigint; + pollAddr: string; + stateRoot: bigint; + messageRoot: bigint; + }>; + blockNumber: number; + transactionIndex: number; +} + +/** + * An interface that represents the deployed Poseidon contracts. + */ +export interface IDeployedPoseidonContracts { + PoseidonT3Contract: PoseidonT3; + PoseidonT4Contract: PoseidonT4; + PoseidonT5Contract: PoseidonT5; + PoseidonT6Contract: PoseidonT6; +} + +/** + * An interface that represents the arguments for MACI contracts deployment. + */ +export interface IDeployMaciArgs { + /** + * The address of the SignUpTokenGatekeeper contract + */ + signUpTokenGatekeeperContractAddress: string; + + /** + * The address of the ConstantInitialVoiceCreditProxy contract + */ + initialVoiceCreditBalanceAddress: string; + + /** + * The address of the TopupCredit contract + */ + topupCreditContractAddress: string; + + /** + * The signer to use to deploy the contract + */ + signer?: Signer; + + /** + * Poseidon contract addresses (if not provided, they will be deployed automatically) + */ + poseidonAddresses?: Partial<{ + poseidonT3: string; + poseidonT4: string; + poseidonT5: string; + poseidonT6: string; + }>; + + /** + * The depth of the state tree + */ + stateTreeDepth?: number; + + /** + * Whether to suppress console output + */ + quiet?: boolean; + + /** + * Whether to support QV or not + */ + useQv?: boolean; +} + +/** + * An interface that represents the deployed MACI contracts. + */ +export interface IDeployedMaci { + maciContract: MACI; + stateAqContract: AccQueueQuinaryMaci; + pollFactoryContract: PollFactory; + poseidonAddrs: { + poseidonT3: string; + poseidonT4: string; + poseidonT5: string; + poseidonT6: string; + }; +} diff --git a/packages/hardhat/maci-ts/ts/utils.ts b/packages/hardhat/maci-ts/ts/utils.ts new file mode 100644 index 0000000..31281d0 --- /dev/null +++ b/packages/hardhat/maci-ts/ts/utils.ts @@ -0,0 +1,145 @@ +import type { Action, SnarkProof, Groth16Proof } from "./types"; +import type { Ownable } from "../../typechain-types"; +import type { BigNumberish, FeeData, Network, Signer } from "ethers"; + +/** + * Format a SnarkProof type to an array of strings + * which can be passed to the Groth16 verifier contract. + * @param proof the SnarkProof to format + * @returns an array of strings + */ +export const formatProofForVerifierContract = (proof: SnarkProof | Groth16Proof): string[] => + [ + proof.pi_a[0], + proof.pi_a[1], + + proof.pi_b[0][1], + proof.pi_b[0][0], + proof.pi_b[1][1], + proof.pi_b[1][0], + + proof.pi_c[0], + proof.pi_c[1], + ].map(x => x.toString()); + +/** + * Pause the thread for n milliseconds + * @param ms - the amount of time to sleep in milliseconds + */ +export const sleep = async (ms: number): Promise => { + await new Promise(resolve => { + setTimeout(resolve, ms); + }); +}; + +/** + * The comparison function for Actions based on block number and transaction + * index. + * @param actions - the array of actions to sort + * @returns the sorted array of actions + */ +export function sortActions(actions: Action[]): Action[] { + return actions.slice().sort((a, b) => { + if (a.blockNumber > b.blockNumber) { + return 1; + } + + if (a.blockNumber < b.blockNumber) { + return -1; + } + + if (a.transactionIndex > b.transactionIndex) { + return 1; + } + + if (a.transactionIndex < b.transactionIndex) { + return -1; + } + + return 0; + }); +} + +/** + * Print to the console + * @param msg - the message to print + * @param quiet - whether to suppress console output + */ +export const log = (msg: string, quiet: boolean): void => { + if (!quiet) { + // eslint-disable-next-line no-console + console.log(msg); + } +}; + +/** + * Get the default signer from the hardhat node + * @returns the default signer + */ +export const getDefaultSigner = async (): Promise => { + const { ethers } = await import("hardhat"); + + const [signer] = await ethers.getSigners(); + + return signer; +}; + +/** + * Get the default signer network from the hardhat node + * @returns the default network + */ +export const getDefaultNetwork = async (): Promise => { + const signer = await getDefaultSigner(); + + return signer.provider?.getNetwork(); +}; + +/** + * Get all of the available signers from the hardhat node + * @dev to be used while testing + * @returns the signers + */ +export const getSigners = async (): Promise => { + const { ethers } = await import("hardhat"); + + return ethers.getSigners(); +}; + +/** + * Get the current fee data from the blockchain node. + * This is needed to ensure transaction go through in busy times + * @returns - the fee data + */ +export const getFeeData = async (): Promise => { + const signer = await getDefaultSigner(); + return signer.provider?.getFeeData(); +}; + +/** + * Transfer ownership of a contract (using Ownable from OpenZeppelin) + * @param contract - the contract to transfer ownership of + * @param newOwner - the address of the new owner + * @param quiet - whether to suppress console output + */ +export const transferOwnership = async ( + contract: T, + newOwner: string, + quiet = false, +): Promise => { + log(`Transferring ownership of ${await contract.getAddress()} to ${newOwner}`, quiet); + const tx = await contract.transferOwnership(newOwner, { + maxFeePerGas: await getFeeData().then(res => res?.maxFeePerGas), + }); + + await tx.wait(); +}; + +/** + * Convert bignumberish to hex + * + * @param value - bignumberish string + * @returns hex representation of it + */ +export function asHex(value: BigNumberish): string { + return `0x${BigInt(value).toString(16)}`; +} diff --git a/packages/hardhat/package.json b/packages/hardhat/package.json index 0b619c5..b1c040f 100644 --- a/packages/hardhat/package.json +++ b/packages/hardhat/package.json @@ -4,53 +4,65 @@ "scripts": { "account": "hardhat run scripts/listAccount.ts", "chain": "hardhat node --network hardhat --no-deploy", - "compile": "hardhat compile", - "deploy": "hardhat deploy", + "compile": "./maci-scripts/compileSol.sh", + "deploy": "yarn compile && hardhat deploy", + "flatten": "yarn compile && hardhat flatten", "fork": "MAINNET_FORKING_ENABLED=true hardhat node --network hardhat --no-deploy", + "format": "prettier --write ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts", "generate": "hardhat run scripts/generateAccount.ts", - "flatten": "hardhat flatten", + "hardhat-verify": "hardhat verify", "lint": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts", "lint-staged": "eslint --config ./.eslintrc.json --ignore-path ./.eslintignore", - "format": "prettier --write ./*.ts ./deploy/**/*.ts ./scripts/**/*.ts ./test/**/*.ts", "test": "REPORT_GAS=true hardhat test --network hardhat", - "verify": "hardhat etherscan-verify", - "hardhat-verify": "hardhat verify" - }, - "devDependencies": { - "@ethersproject/abi": "^5.7.0", - "@ethersproject/providers": "^5.7.1", - "@nomicfoundation/hardhat-chai-matchers": "^2.0.3", - "@nomicfoundation/hardhat-ethers": "^3.0.5", - "@nomicfoundation/hardhat-network-helpers": "^1.0.6", - "@nomicfoundation/hardhat-verify": "^2.0.3", - "@typechain/ethers-v5": "^10.1.0", - "@typechain/hardhat": "^9.1.0", - "@types/eslint": "^8", - "@types/mocha": "^9.1.1", - "@types/prettier": "^2", - "@types/qrcode": "^1", - "@typescript-eslint/eslint-plugin": "latest", - "@typescript-eslint/parser": "latest", - "chai": "^4.3.6", - "eslint": "^8.26.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.2.1", - "ethers": "^6.10.0", - "hardhat": "^2.19.4", - "hardhat-deploy": "^0.11.45", - "hardhat-deploy-ethers": "^0.4.1", - "hardhat-gas-reporter": "^1.0.9", - "prettier": "^2.8.4", - "solidity-coverage": "^0.8.5", - "ts-node": "^10.9.1", - "typechain": "^8.1.0", - "typescript": "^5.1.6" + "verify": "hardhat etherscan-verify" }, "dependencies": { - "@openzeppelin/contracts": "^4.8.1", - "@typechain/ethers-v6": "^0.5.1", - "dotenv": "^16.0.3", - "envfile": "^6.18.0", - "qrcode": "^1.5.1" + "@commander-js/extra-typings": "^12.0.1", + "@openzeppelin/contracts": "~4.8.1", + "@typechain/ethers-v6": "~0.5.1", + "@zk-kit/eddsa-poseidon": "^0.10.0", + "@zk-kit/poseidon-cipher": "^0.3.0", + "benny": "^3.7.1", + "circomkit": "^0.0.24", + "circomlibjs": "^0.1.7", + "dotenv": "~16.0.3", + "envfile": "~6.18.0", + "hardhat-artifactor": "^0.2.0", + "prompt": "^1.3.0", + "qrcode": "~1.5.1", + "snarkjs": "^0.7.3" + }, + "devDependencies": { + "@ethersproject/abi": "~5.7.0", + "@ethersproject/providers": "~5.7.1", + "@nomicfoundation/hardhat-chai-matchers": "~2.0.3", + "@nomicfoundation/hardhat-ethers": "~3.0.5", + "@nomicfoundation/hardhat-network-helpers": "~1.0.6", + "@nomicfoundation/hardhat-verify": "~2.0.3", + "@typechain/ethers-v5": "~10.1.0", + "@typechain/hardhat": "~9.1.0", + "@types/circomlibjs": "^0", + "@types/eslint": "~8", + "@types/mocha": "~9.1.1", + "@types/prettier": "~2", + "@types/prompt": "^1", + "@types/qrcode": "~1", + "@types/snarkjs": "^0", + "@typescript-eslint/eslint-plugin": "~6.7.3", + "@typescript-eslint/parser": "~6.7.3", + "chai": "~4.3.6", + "eslint": "~8.26.0", + "eslint-config-prettier": "~8.5.0", + "eslint-plugin-prettier": "~4.2.1", + "ethers": "~6.10.0", + "hardhat": "~2.19.4", + "hardhat-deploy": "^0.12.2", + "hardhat-deploy-ethers": "~0.4.1", + "hardhat-gas-reporter": "~1.0.9", + "prettier": "~2.8.4", + "solidity-coverage": "~0.8.5", + "ts-node": "~10.9.1", + "typechain": "~8.1.0", + "typescript": "~5.1.6" } } diff --git a/packages/hardhat/test/YourContract.ts b/packages/hardhat/test/YourContract.ts deleted file mode 100644 index a44cf04..0000000 --- a/packages/hardhat/test/YourContract.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; -import { YourContract } from "../typechain-types"; - -describe("YourContract", function () { - // We define a fixture to reuse the same setup in every test. - - let yourContract: YourContract; - before(async () => { - const [owner] = await ethers.getSigners(); - const yourContractFactory = await ethers.getContractFactory("YourContract"); - yourContract = (await yourContractFactory.deploy(owner.address)) as YourContract; - await yourContract.waitForDeployment(); - }); - - describe("Deployment", function () { - it("Should have the right message on deploy", async function () { - expect(await yourContract.greeting()).to.equal("Building Unstoppable Apps!!!"); - }); - - it("Should allow setting a new message", async function () { - const newGreeting = "Learn Scaffold-ETH 2! :)"; - - await yourContract.setGreeting(newGreeting); - expect(await yourContract.greeting()).to.equal(newGreeting); - }); - }); -}); diff --git a/yarn.lock b/yarn.lock index cdd9ea4..39dcd5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,6 +33,50 @@ __metadata: languageName: node linkType: hard +"@arrows/array@npm:^1.4.1": + version: 1.4.1 + resolution: "@arrows/array@npm:1.4.1" + dependencies: + "@arrows/composition": ^1.2.2 + checksum: 39de47a49709376d91360955665f5cc33ad6fce85125a5b1fde777bf963bd2d053cc77a587253a55e6f4241a75ad7db991aacc26eb36edb7a746d824eb8ebd8a + languageName: node + linkType: hard + +"@arrows/composition@npm:^1.0.0, @arrows/composition@npm:^1.2.2": + version: 1.2.2 + resolution: "@arrows/composition@npm:1.2.2" + checksum: 3219e9a4e220c9778d8919fef329608b9966667b61f26e403d368646ebc65d96b68abcb7a73621992baad678e444ceb36914f1f2db2d6502ddfe738e9230e737 + languageName: node + linkType: hard + +"@arrows/dispatch@npm:^1.0.2": + version: 1.0.3 + resolution: "@arrows/dispatch@npm:1.0.3" + dependencies: + "@arrows/composition": ^1.2.2 + checksum: 2bd0b1ad5345b056cd300b63eedf3a1b9f17e8f891a5b5d1e70e9a3d8c426ec05828c38cd437f742e75387fbc98b3082fef23f62fe97688b63d060376d50dcd9 + languageName: node + linkType: hard + +"@arrows/error@npm:^1.0.2": + version: 1.0.2 + resolution: "@arrows/error@npm:1.0.2" + checksum: 35ad67e8d2781879a22711f5c7ba3907d6772ff42b24abc8b94b5165414e802f6c207f2024f50508c8f40637465a91da268ebf321c0eef5aaf44fc3d4acc7a58 + languageName: node + linkType: hard + +"@arrows/multimethod@npm:^1.1.6": + version: 1.4.1 + resolution: "@arrows/multimethod@npm:1.4.1" + dependencies: + "@arrows/array": ^1.4.1 + "@arrows/composition": ^1.2.2 + "@arrows/error": ^1.0.2 + fast-deep-equal: ^3.1.3 + checksum: 2a3a6b62debb163448ce1e90c9a0508866e605895967a67ef3c65f5248e5e7318ae95a92d4a62aff0518eea63755cc0467deb3265c3c9b41e00a892802ae729a + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.22.13": version: 7.22.13 resolution: "@babel/code-frame@npm:7.22.13" @@ -265,6 +309,22 @@ __metadata: languageName: node linkType: hard +"@colors/colors@npm:1.5.0": + version: 1.5.0 + resolution: "@colors/colors@npm:1.5.0" + checksum: d64d5260bed1d5012ae3fc617d38d1afc0329fec05342f4e6b838f46998855ba56e0a73833f4a80fa8378c84810da254f76a8a19c39d038260dc06dc4e007425 + languageName: node + linkType: hard + +"@commander-js/extra-typings@npm:^12.0.1": + version: 12.0.1 + resolution: "@commander-js/extra-typings@npm:12.0.1" + peerDependencies: + commander: ~12.0.0 + checksum: 2490d44c5046d94e73c5c65f8055522954855f7475e6d170d14a84a9e6235f28ac87d9b46d8e6ae8cc4268371937fba94953df5336547765bb87e94d59964eef + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -338,6 +398,23 @@ __metadata: languageName: node linkType: hard +"@eslint/eslintrc@npm:^1.3.3": + version: 1.4.1 + resolution: "@eslint/eslintrc@npm:1.4.1" + dependencies: + ajv: ^6.12.4 + debug: ^4.3.2 + espree: ^9.4.0 + globals: ^13.19.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + minimatch: ^3.1.2 + strip-json-comments: ^3.1.1 + checksum: cd3e5a8683db604739938b1c1c8b77927dc04fce3e28e0c88e7f2cd4900b89466baf83dfbad76b2b9e4d2746abdd00dd3f9da544d3e311633d8693f327d04cd7 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^2.1.2": version: 2.1.2 resolution: "@eslint/eslintrc@npm:2.1.2" @@ -382,7 +459,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.0.0-beta.146, @ethersproject/abi@npm:^5.0.9, @ethersproject/abi@npm:^5.1.2, @ethersproject/abi@npm:^5.7.0": +"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.0.0-beta.146, @ethersproject/abi@npm:^5.0.9, @ethersproject/abi@npm:^5.1.2, @ethersproject/abi@npm:^5.7.0, @ethersproject/abi@npm:~5.7.0": version: 5.7.0 resolution: "@ethersproject/abi@npm:5.7.0" dependencies: @@ -609,7 +686,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/providers@npm:5.7.2, @ethersproject/providers@npm:^5.7.1, @ethersproject/providers@npm:^5.7.2": +"@ethersproject/providers@npm:5.7.2, @ethersproject/providers@npm:^5.7.1, @ethersproject/providers@npm:^5.7.2, @ethersproject/providers@npm:~5.7.1": version: 5.7.2 resolution: "@ethersproject/providers@npm:5.7.2" dependencies: @@ -804,6 +881,17 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/config-array@npm:^0.11.6": + version: 0.11.14 + resolution: "@humanwhocodes/config-array@npm:0.11.14" + dependencies: + "@humanwhocodes/object-schema": ^2.0.2 + debug: ^4.3.1 + minimatch: ^3.0.5 + checksum: 861ccce9eaea5de19546653bccf75bf09fe878bc39c3aab00aeee2d2a0e654516adad38dd1098aab5e3af0145bbcbf3f309bdf4d964f8dab9dcd5834ae4c02f2 + languageName: node + linkType: hard + "@humanwhocodes/module-importer@npm:^1.0.1": version: 1.0.1 resolution: "@humanwhocodes/module-importer@npm:1.0.1" @@ -818,6 +906,30 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/object-schema@npm:^2.0.2": + version: 2.0.2 + resolution: "@humanwhocodes/object-schema@npm:2.0.2" + checksum: 2fc11503361b5fb4f14714c700c02a3f4c7c93e9acd6b87a29f62c522d90470f364d6161b03d1cc618b979f2ae02aed1106fd29d302695d8927e2fc8165ba8ee + languageName: node + linkType: hard + +"@iden3/bigarray@npm:0.0.2": + version: 0.0.2 + resolution: "@iden3/bigarray@npm:0.0.2" + checksum: a58a4aa75284f7c74ef8a43dd0720764e94389ae4314213f18de9d712fbdb170dd0e9b48672ad297b5f15bd1658d3bff33fe1998e189b48cbf501f1fe78f0adc + languageName: node + linkType: hard + +"@iden3/binfileutils@npm:0.0.11": + version: 0.0.11 + resolution: "@iden3/binfileutils@npm:0.0.11" + dependencies: + fastfile: 0.0.20 + ffjavascript: ^0.2.48 + checksum: ca61db1325c7e038c6bd723c856eff5f2c82c76394db09d3350ef4f5b7525e3c9ab1f7429900ff5d3e9d26c5970bf5900e6126ccb5c5caa597c16a47336a6be8 + languageName: node + linkType: hard + "@ioredis/commands@npm:^1.1.1": version: 1.2.0 resolution: "@ioredis/commands@npm:1.2.0" @@ -1353,9 +1465,9 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/hardhat-chai-matchers@npm:^2.0.3": - version: 2.0.3 - resolution: "@nomicfoundation/hardhat-chai-matchers@npm:2.0.3" +"@nomicfoundation/hardhat-chai-matchers@npm:~2.0.3": + version: 2.0.6 + resolution: "@nomicfoundation/hardhat-chai-matchers@npm:2.0.6" dependencies: "@types/chai-as-promised": ^7.1.3 chai-as-promised: ^7.1.1 @@ -1366,11 +1478,11 @@ __metadata: chai: ^4.2.0 ethers: ^6.1.0 hardhat: ^2.9.4 - checksum: bdab50e569ef85498a4d3bc374cb635c3072aa734ad8e9eff71f87e1467cf0fa941fa286aedce1651fa190bfe19119d65cd2ac614314bf6db87eb7738cd49c33 + checksum: 050bf0cf2f33b480bc93912330929649b0e08a0f9405bbadda66239bfeedaee7f2cfc7e34ed03540cb381b41925fc9dd4ec9a36088ccfa8d7461259d8c78003d languageName: node linkType: hard -"@nomicfoundation/hardhat-ethers@npm:^3.0.5": +"@nomicfoundation/hardhat-ethers@npm:~3.0.5": version: 3.0.5 resolution: "@nomicfoundation/hardhat-ethers@npm:3.0.5" dependencies: @@ -1383,20 +1495,20 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/hardhat-network-helpers@npm:^1.0.6": - version: 1.0.9 - resolution: "@nomicfoundation/hardhat-network-helpers@npm:1.0.9" +"@nomicfoundation/hardhat-network-helpers@npm:~1.0.6": + version: 1.0.10 + resolution: "@nomicfoundation/hardhat-network-helpers@npm:1.0.10" dependencies: ethereumjs-util: ^7.1.4 peerDependencies: hardhat: ^2.9.5 - checksum: ff378795075af853aeaacb7bc0783928d947d7f9fb043c046fcaffdf1e1219c4af47b18ea7fa2c10fe0b25daef48f13ae8b103bc11ea494ecdfbe34a3dcdf936 + checksum: 675da8d3229946a2bac0df9d1b5cc278bba9cd1a8214b5ff6099dcba874d913df07b9772a2ead0cb7ea2ced6b3fa430a73f94a3e257ae105493931c38fc7bf61 languageName: node linkType: hard -"@nomicfoundation/hardhat-verify@npm:^2.0.3": - version: 2.0.3 - resolution: "@nomicfoundation/hardhat-verify@npm:2.0.3" +"@nomicfoundation/hardhat-verify@npm:~2.0.3": + version: 2.0.5 + resolution: "@nomicfoundation/hardhat-verify@npm:2.0.5" dependencies: "@ethersproject/abi": ^5.1.2 "@ethersproject/address": ^5.0.2 @@ -1409,7 +1521,7 @@ __metadata: undici: ^5.14.0 peerDependencies: hardhat: ^2.0.4 - checksum: 5cc3513d3385eb45c19081e79b1b2a67b13376f0d2c5dc2da07ee41886f82110b0af5b46e7fae6204db8a9d4dcda7e568a630a254bd5567779068bc29c22c86f + checksum: f64bf0aedd9487177799043d22cc2628868f06b927cc7ad8966169922489b0014af27b38e3a5fcc43fdd8fe9c4dd49d3588e9a724fc317b3ead5fd263e845561 languageName: node linkType: hard @@ -1531,10 +1643,10 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts@npm:^4.8.1": - version: 4.9.3 - resolution: "@openzeppelin/contracts@npm:4.9.3" - checksum: 4932063e733b35fa7669b9fe2053f69b062366c5c208b0c6cfa1ac451712100c78acff98120c3a4b88d94154c802be05d160d71f37e7d74cadbe150964458838 +"@openzeppelin/contracts@npm:~4.8.1": + version: 4.8.3 + resolution: "@openzeppelin/contracts@npm:4.8.3" + checksum: aea130d38d46840c5cbe3adbaa9a7ac645e4bd66ad3f3baf2fa78588c408d1a686170b3408c9e2e5e05530fba22ecdc00d7efb6b27852a8b29f91accbc0af255 languageName: node linkType: hard @@ -1818,39 +1930,51 @@ __metadata: version: 0.0.0-use.local resolution: "@se-2/hardhat@workspace:packages/hardhat" dependencies: - "@ethersproject/abi": ^5.7.0 - "@ethersproject/providers": ^5.7.1 - "@nomicfoundation/hardhat-chai-matchers": ^2.0.3 - "@nomicfoundation/hardhat-ethers": ^3.0.5 - "@nomicfoundation/hardhat-network-helpers": ^1.0.6 - "@nomicfoundation/hardhat-verify": ^2.0.3 - "@openzeppelin/contracts": ^4.8.1 - "@typechain/ethers-v5": ^10.1.0 - "@typechain/ethers-v6": ^0.5.1 - "@typechain/hardhat": ^9.1.0 - "@types/eslint": ^8 - "@types/mocha": ^9.1.1 - "@types/prettier": ^2 - "@types/qrcode": ^1 - "@typescript-eslint/eslint-plugin": latest - "@typescript-eslint/parser": latest - chai: ^4.3.6 - dotenv: ^16.0.3 - envfile: ^6.18.0 - eslint: ^8.26.0 - eslint-config-prettier: ^8.5.0 - eslint-plugin-prettier: ^4.2.1 - ethers: ^6.10.0 - hardhat: ^2.19.4 - hardhat-deploy: ^0.11.45 - hardhat-deploy-ethers: ^0.4.1 - hardhat-gas-reporter: ^1.0.9 - prettier: ^2.8.4 - qrcode: ^1.5.1 - solidity-coverage: ^0.8.5 - ts-node: ^10.9.1 - typechain: ^8.1.0 - typescript: ^5.1.6 + "@commander-js/extra-typings": ^12.0.1 + "@ethersproject/abi": ~5.7.0 + "@ethersproject/providers": ~5.7.1 + "@nomicfoundation/hardhat-chai-matchers": ~2.0.3 + "@nomicfoundation/hardhat-ethers": ~3.0.5 + "@nomicfoundation/hardhat-network-helpers": ~1.0.6 + "@nomicfoundation/hardhat-verify": ~2.0.3 + "@openzeppelin/contracts": ~4.8.1 + "@typechain/ethers-v5": ~10.1.0 + "@typechain/ethers-v6": ~0.5.1 + "@typechain/hardhat": ~9.1.0 + "@types/circomlibjs": ^0 + "@types/eslint": ~8 + "@types/mocha": ~9.1.1 + "@types/prettier": ~2 + "@types/prompt": ^1 + "@types/qrcode": ~1 + "@types/snarkjs": ^0 + "@typescript-eslint/eslint-plugin": ~6.7.3 + "@typescript-eslint/parser": ~6.7.3 + "@zk-kit/eddsa-poseidon": ^0.10.0 + "@zk-kit/poseidon-cipher": ^0.3.0 + benny: ^3.7.1 + chai: ~4.3.6 + circomkit: ^0.0.24 + circomlibjs: ^0.1.7 + dotenv: ~16.0.3 + envfile: ~6.18.0 + eslint: ~8.26.0 + eslint-config-prettier: ~8.5.0 + eslint-plugin-prettier: ~4.2.1 + ethers: ~6.10.0 + hardhat: ~2.19.4 + hardhat-artifactor: ^0.2.0 + hardhat-deploy: ^0.12.2 + hardhat-deploy-ethers: ~0.4.1 + hardhat-gas-reporter: ~1.0.9 + prettier: ~2.8.4 + prompt: ^1.3.0 + qrcode: ~1.5.1 + snarkjs: ^0.7.3 + solidity-coverage: ~0.8.5 + ts-node: ~10.9.1 + typechain: ~8.1.0 + typescript: ~5.1.6 languageName: unknown linkType: soft @@ -2028,12 +2152,10 @@ __metadata: languageName: node linkType: hard -"@solidity-parser/parser@npm:^0.16.0": - version: 0.16.1 - resolution: "@solidity-parser/parser@npm:0.16.1" - dependencies: - antlr4ts: ^0.5.0-alpha.4 - checksum: d9e2f7042434fb850a97a2c3679f5fbf4997c7845278d0a436b3de30169e6758fe3818191694ece36dc39a40f55ae0384c4ae0ae912790b5b0806728a50466c2 +"@solidity-parser/parser@npm:^0.18.0": + version: 0.18.0 + resolution: "@solidity-parser/parser@npm:0.18.0" + checksum: 970d991529d632862fa88e107531339d84df35bf0374e31e8215ce301b19a01ede33fccf4d374402649814263f8bc278a8e6d62a0129bb877539fbdd16a604cc languageName: node linkType: hard @@ -2338,23 +2460,24 @@ __metadata: languageName: node linkType: hard -"@typechain/ethers-v5@npm:^10.1.0": - version: 10.2.1 - resolution: "@typechain/ethers-v5@npm:10.2.1" +"@typechain/ethers-v5@npm:~10.1.0": + version: 10.1.1 + resolution: "@typechain/ethers-v5@npm:10.1.1" dependencies: lodash: ^4.17.15 ts-essentials: ^7.0.1 peerDependencies: "@ethersproject/abi": ^5.0.0 + "@ethersproject/bytes": ^5.0.0 "@ethersproject/providers": ^5.0.0 ethers: ^5.1.3 typechain: ^8.1.1 typescript: ">=4.3.0" - checksum: 852da4b1ff368ef87251111a5d50077de3d0fc12c519529269a74223740f8bda89297e67a5eb6c1f5b04ee23119566d6cbccf58264d32a83132be0f328a58d22 + checksum: 6264294a03d0af117f99bee1639461be38161418d7be340a5bf24c4197aeaf7956aabec922119c20f90dba38ea7c12e5c85e0baddd358d18d68f4865037671cb languageName: node linkType: hard -"@typechain/ethers-v6@npm:^0.5.1": +"@typechain/ethers-v6@npm:~0.5.1": version: 0.5.1 resolution: "@typechain/ethers-v6@npm:0.5.1" dependencies: @@ -2368,7 +2491,7 @@ __metadata: languageName: node linkType: hard -"@typechain/hardhat@npm:^9.1.0": +"@typechain/hardhat@npm:~9.1.0": version: 9.1.0 resolution: "@typechain/hardhat@npm:9.1.0" dependencies: @@ -2416,6 +2539,13 @@ __metadata: languageName: node linkType: hard +"@types/circomlibjs@npm:^0": + version: 0.1.6 + resolution: "@types/circomlibjs@npm:0.1.6" + checksum: c1626380bd46f6da6ab45c59338a91f0166933ae987a03eda2ae09c490457aa010e412c46192502198b7b41054ee0129bece3d3edcbabdb05ea226582f772cc4 + languageName: node + linkType: hard + "@types/concat-stream@npm:^1.6.0": version: 1.6.1 resolution: "@types/concat-stream@npm:1.6.1" @@ -2443,13 +2573,13 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:^8": - version: 8.44.3 - resolution: "@types/eslint@npm:8.44.3" +"@types/eslint@npm:~8": + version: 8.56.6 + resolution: "@types/eslint@npm:8.56.6" dependencies: "@types/estree": "*" "@types/json-schema": "*" - checksum: 3a0d152785400cb83a887a646d9c8877468e686b6fb439635c64856b70dbe91019e588d2b32bc923cd60642bf5dca7f70b2cf61eb431cf25fbdf2932f6e13dd3 + checksum: 960996940c8702c6e9bf221f2927f088d8f6463ad21ae1eb8260c62642ce48097a79a4277d99cb7cafde6939beadbd79610015fdd08b18679e565bcad5fcd36f languageName: node linkType: hard @@ -2507,7 +2637,7 @@ __metadata: languageName: node linkType: hard -"@types/mocha@npm:^9.1.1": +"@types/mocha@npm:~9.1.1": version: 9.1.1 resolution: "@types/mocha@npm:9.1.1" checksum: 516077c0acd9806dc78317f88aaac0df5aaf0bdc2f63dfdadeabdf0b0137953b6ca65472e6ff7c30bc93ce4e0ae76eae70e8d46764b9a8eae4877a928b6ef49a @@ -2586,13 +2716,23 @@ __metadata: languageName: node linkType: hard -"@types/prettier@npm:^2, @types/prettier@npm:^2.1.1": +"@types/prettier@npm:^2.1.1, @types/prettier@npm:~2": version: 2.7.3 resolution: "@types/prettier@npm:2.7.3" checksum: 705384209cea6d1433ff6c187c80dcc0b95d99d5c5ce21a46a9a58060c527973506822e428789d842761e0280d25e3359300f017fbe77b9755bc772ab3dc2f83 languageName: node linkType: hard +"@types/prompt@npm:^1": + version: 1.1.8 + resolution: "@types/prompt@npm:1.1.8" + dependencies: + "@types/node": "*" + "@types/revalidator": "*" + checksum: 65637e5e94f85f307c522085823ded1c90b173cbf6e13ea544cd4bff380b6dcf65cdd6cce724e1d4af705ba35be1c0ffa406a948a81fcb4ecd6205eaafff2e2a + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.7 resolution: "@types/prop-types@npm:15.7.7" @@ -2600,12 +2740,12 @@ __metadata: languageName: node linkType: hard -"@types/qrcode@npm:^1": - version: 1.5.2 - resolution: "@types/qrcode@npm:1.5.2" +"@types/qrcode@npm:~1": + version: 1.5.5 + resolution: "@types/qrcode@npm:1.5.5" dependencies: "@types/node": "*" - checksum: 2f696916e66002ecf0616451a44308b33948ce68f7ee1eae0ff8a4acc133024a3d9a7314e9dcb6455ab7f7e126963c204a687a665e3688ba39ec4ae80eb30670 + checksum: d92c1d3e77406bf13a03ec521b2ffb1ac99b2e6ea3a17cad670f2610f62e1293554c57e4074bb2fd4e9369f475f863b69e0ae8c543cb049c4a3c1b0c2d92522a languageName: node linkType: hard @@ -2646,6 +2786,13 @@ __metadata: languageName: node linkType: hard +"@types/revalidator@npm:*": + version: 0.3.12 + resolution: "@types/revalidator@npm:0.3.12" + checksum: 8cd31dfe08d62bf34358ecdfecde93711b858fb280123e5cc0961024eca11d34b99c8955038a1614c2a4b1add6a3b2dc4f226df2fe1cdb916ef24a7084062f34 + languageName: node + linkType: hard + "@types/scheduler@npm:*": version: 0.16.4 resolution: "@types/scheduler@npm:0.16.4" @@ -2669,6 +2816,13 @@ __metadata: languageName: node linkType: hard +"@types/snarkjs@npm:^0": + version: 0.7.8 + resolution: "@types/snarkjs@npm:0.7.8" + checksum: 9ac4b5c48af2680321ec204fd31c4a5ce829807ff808a429182d2be519e98d577fd4d5a86f80b8c8503628a44198f96dce6d10fbe99b745335ad7abc1d34d4f4 + languageName: node + linkType: hard + "@types/trusted-types@npm:^2.0.2": version: 2.0.4 resolution: "@types/trusted-types@npm:2.0.4" @@ -2718,15 +2872,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:latest": - version: 6.7.3 - resolution: "@typescript-eslint/eslint-plugin@npm:6.7.3" +"@typescript-eslint/eslint-plugin@npm:~6.7.3": + version: 6.7.5 + resolution: "@typescript-eslint/eslint-plugin@npm:6.7.5" dependencies: "@eslint-community/regexpp": ^4.5.1 - "@typescript-eslint/scope-manager": 6.7.3 - "@typescript-eslint/type-utils": 6.7.3 - "@typescript-eslint/utils": 6.7.3 - "@typescript-eslint/visitor-keys": 6.7.3 + "@typescript-eslint/scope-manager": 6.7.5 + "@typescript-eslint/type-utils": 6.7.5 + "@typescript-eslint/utils": 6.7.5 + "@typescript-eslint/visitor-keys": 6.7.5 debug: ^4.3.4 graphemer: ^1.4.0 ignore: ^5.2.4 @@ -2739,11 +2893,11 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: ac2790882199047abc59c0407a862f3339645623d03ea0aae5a73fd4bac6abfb753afcf9f23fd51cd1d5aa73f132ef94e2850774c4b2a3d99ebb83030b09429c + checksum: c37edf5a703db4ff9227d67c2d2cf817e65c9afc94cc0e650fa3d2b05ac55201ef887ce9dadb9ca13779f4025bf4367e132b013e3559e777006a2332079bb180 languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.4.2 || ^6.0.0, @typescript-eslint/parser@npm:latest": +"@typescript-eslint/parser@npm:^5.4.2 || ^6.0.0": version: 6.7.3 resolution: "@typescript-eslint/parser@npm:6.7.3" dependencies: @@ -2761,6 +2915,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:~6.7.3": + version: 6.7.5 + resolution: "@typescript-eslint/parser@npm:6.7.5" + dependencies: + "@typescript-eslint/scope-manager": 6.7.5 + "@typescript-eslint/types": 6.7.5 + "@typescript-eslint/typescript-estree": 6.7.5 + "@typescript-eslint/visitor-keys": 6.7.5 + debug: ^4.3.4 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 63f988c1c87697bd20487933be952b97f7a5f2a9977f505af671c7d49367fc01ca508817576646caa937c15cc0a0ef1e86adff9111eb19df8b489e7436d10620 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/scope-manager@npm:5.62.0" @@ -2781,6 +2953,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:6.7.5": + version: 6.7.5 + resolution: "@typescript-eslint/scope-manager@npm:6.7.5" + dependencies: + "@typescript-eslint/types": 6.7.5 + "@typescript-eslint/visitor-keys": 6.7.5 + checksum: f21858ed78f81ab2d9879139f69657fda2a7b901078f79df64d1262d80f84ef66c56525ed0bb5e393fa5ca5474ad97f2225b7f713977c2d0f79cda31b2744af9 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/type-utils@npm:5.62.0" @@ -2798,12 +2980,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:6.7.3": - version: 6.7.3 - resolution: "@typescript-eslint/type-utils@npm:6.7.3" +"@typescript-eslint/type-utils@npm:6.7.5": + version: 6.7.5 + resolution: "@typescript-eslint/type-utils@npm:6.7.5" dependencies: - "@typescript-eslint/typescript-estree": 6.7.3 - "@typescript-eslint/utils": 6.7.3 + "@typescript-eslint/typescript-estree": 6.7.5 + "@typescript-eslint/utils": 6.7.5 debug: ^4.3.4 ts-api-utils: ^1.0.1 peerDependencies: @@ -2811,7 +2993,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: f30a5ab4f88f76457810d72e3ada79fefd94dbbb456069ac004bd7601c9b7f15689b906b66cd849c230f30ae65f6f7039fb169609177ab545b34bacab64f015e + checksum: 8023d8ddcfbf4a0411b192016711068e9e6787c5811aee3a25ac40025ade0d063a1a3d7b38469e1a534bb31fa9dbeec08ab53b7a6d7b3128358294ac5b219d9a languageName: node linkType: hard @@ -2829,6 +3011,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:6.7.5": + version: 6.7.5 + resolution: "@typescript-eslint/types@npm:6.7.5" + checksum: f21e5726b60f13feb3a920c92515fbc1205ba0e9bba9959b2e42c02c282a0ab4fb0e5ae84f3807b9b1cf95036027e9033d92a911fa88e6c243a87621d8dd7a01 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" @@ -2865,6 +3054,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:6.7.5": + version: 6.7.5 + resolution: "@typescript-eslint/typescript-estree@npm:6.7.5" + dependencies: + "@typescript-eslint/types": 6.7.5 + "@typescript-eslint/visitor-keys": 6.7.5 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + semver: ^7.5.4 + ts-api-utils: ^1.0.1 + peerDependenciesMeta: + typescript: + optional: true + checksum: 17685e8321edce1d1ec4278d84e63c0f41ccb19e9308f21c37450943ad0c33328755ac52b966e7855af17e01d22bc83d1fcda79c279fabe7d3460c8f315a7265 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" @@ -2883,20 +3090,20 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:6.7.3": - version: 6.7.3 - resolution: "@typescript-eslint/utils@npm:6.7.3" +"@typescript-eslint/utils@npm:6.7.5": + version: 6.7.5 + resolution: "@typescript-eslint/utils@npm:6.7.5" dependencies: "@eslint-community/eslint-utils": ^4.4.0 "@types/json-schema": ^7.0.12 "@types/semver": ^7.5.0 - "@typescript-eslint/scope-manager": 6.7.3 - "@typescript-eslint/types": 6.7.3 - "@typescript-eslint/typescript-estree": 6.7.3 + "@typescript-eslint/scope-manager": 6.7.5 + "@typescript-eslint/types": 6.7.5 + "@typescript-eslint/typescript-estree": 6.7.5 semver: ^7.5.4 peerDependencies: eslint: ^7.0.0 || ^8.0.0 - checksum: 685b7c9fa95ad085f30e26431dc41b3059a42a16925defe2a94b32fb46974bfc168000de7d4d9ad4a1d0568a983f9d3c01ea6bc6cfa9a798e482719af9e9165b + checksum: f365c654241f927e7784640079627d60a296aa3d575552b07594a69cfc419832eb5fa4adc87acb1988bea9741ae9cc4a5277dab168990310caef5de125255752 languageName: node linkType: hard @@ -2920,6 +3127,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:6.7.5": + version: 6.7.5 + resolution: "@typescript-eslint/visitor-keys@npm:6.7.5" + dependencies: + "@typescript-eslint/types": 6.7.5 + eslint-visitor-keys: ^3.4.1 + checksum: 2df996742f63d89fa339b0e8ff3a3a289d36b3f584f7538a7626bed3869e9ae27f8f56ab31748519d25a63de2ae22a43dd8413610b00436ff342b0a17eb85289 + languageName: node + linkType: hard + "@uniswap/sdk-core@npm:^4.0.1, @uniswap/sdk-core@npm:^4.0.7": version: 4.0.7 resolution: "@uniswap/sdk-core@npm:4.0.7" @@ -3720,6 +3937,70 @@ __metadata: languageName: node linkType: hard +"@zk-kit/baby-jubjub@npm:0.2.0": + version: 0.2.0 + resolution: "@zk-kit/baby-jubjub@npm:0.2.0" + dependencies: + "@zk-kit/utils": 0.3.0 + checksum: c801d0c7ff8f3fca26b79945de2f74f5bfaa31bfff5abf1af73a97af6fb20ab421e4958d275df74338c3330200ca265123f7f69359d26a7d66b9d59616fc076f + languageName: node + linkType: hard + +"@zk-kit/baby-jubjub@npm:0.3.0": + version: 0.3.0 + resolution: "@zk-kit/baby-jubjub@npm:0.3.0" + dependencies: + "@zk-kit/utils": 0.6.0 + checksum: 355e796116d5ed60b803b2077484b6980b735008c68790253f2c71df9dca5f3373171592a4c251b869d3fd464729f3331742067ea05e264b7bc2988fc5c943f2 + languageName: node + linkType: hard + +"@zk-kit/eddsa-poseidon@npm:^0.10.0": + version: 0.10.0 + resolution: "@zk-kit/eddsa-poseidon@npm:0.10.0" + dependencies: + "@zk-kit/baby-jubjub": 0.3.0 + "@zk-kit/utils": 0.8.1 + buffer: 6.0.3 + checksum: a74a927608cba67d7421581ba6088bc5b6437a3a6bfcd27c607f2e33a02dd1f6c3777ff4639e656d3b5c23995f49a7c27b20efbf7a0771985f70f92fc0026704 + languageName: node + linkType: hard + +"@zk-kit/poseidon-cipher@npm:^0.3.0": + version: 0.3.0 + resolution: "@zk-kit/poseidon-cipher@npm:0.3.0" + dependencies: + "@zk-kit/baby-jubjub": 0.2.0 + "@zk-kit/utils": 0.3.0 + checksum: 3dadbf7888ca203cabd463497cf1a26442f6db5f5e6deb1bf25c100c4ba0ad8db1b6eb91e84977e5d4e5a74bd4f81c98005d3b2216b75a97e6031c47e6f3a85a + languageName: node + linkType: hard + +"@zk-kit/utils@npm:0.3.0": + version: 0.3.0 + resolution: "@zk-kit/utils@npm:0.3.0" + checksum: 8de4ad50e32972d9477afd6223ee6ca99be559d55056c4e13bd7f131e311168a4ab2ec8498d08844270bde98ee124da155004e7ac3ae486a58bc4cc37a5d08e3 + languageName: node + linkType: hard + +"@zk-kit/utils@npm:0.6.0": + version: 0.6.0 + resolution: "@zk-kit/utils@npm:0.6.0" + dependencies: + buffer: ^6.0.3 + checksum: dd713f7fc561ab9633ee4555227e41cb20ce61b74079628377df3306bda3b3809c1536b231399bce60ad6c0c0255e061e1d65973ca5813dc75876d20ea58f15d + languageName: node + linkType: hard + +"@zk-kit/utils@npm:0.8.1": + version: 0.8.1 + resolution: "@zk-kit/utils@npm:0.8.1" + dependencies: + buffer: ^6.0.3 + checksum: 6944efde87d2ccfaca51a21da4b1fa0e387e0bc41d2588ac5928bb092f110b193531c8cb2a7c24014b6c1d0a394234b05cfe7b1282e30cf9eff8e903dccd5666 + languageName: node + linkType: hard + "JSONStream@npm:^1.3.5": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" @@ -3823,13 +4104,6 @@ __metadata: languageName: node linkType: hard -"address@npm:^1.0.1": - version: 1.2.2 - resolution: "address@npm:1.2.2" - checksum: ace439960c1e3564d8f523aff23a841904bf33a2a7c2e064f7f60a064194075758b9690e65bd9785692a4ef698a998c57eb74d145881a1cecab8ba658ddb1607 - languageName: node - linkType: hard - "adm-zip@npm:^0.4.16": version: 0.4.16 resolution: "adm-zip@npm:0.4.16" @@ -3898,7 +4172,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.0.0, ajv@npm:^6.12.3, ajv@npm:^6.12.4": +"ajv@npm:^6.0.0, ajv@npm:^6.10.0, ajv@npm:^6.12.3, ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -3929,6 +4203,15 @@ __metadata: languageName: node linkType: hard +"ansi-align@npm:^3.0.0": + version: 3.0.1 + resolution: "ansi-align@npm:3.0.1" + dependencies: + string-width: ^4.1.0 + checksum: 6abfa08f2141d231c257162b15292467081fa49a208593e055c866aa0455b57f3a86b5a678c190c618faa79b4c59e254493099cb700dd9cf2293c6be2c8f5d8d + languageName: node + linkType: hard + "ansi-colors@npm:3.2.3": version: 3.2.3 resolution: "ansi-colors@npm:3.2.3" @@ -4344,6 +4627,29 @@ __metadata: languageName: node linkType: hard +"async@npm:3.2.3": + version: 3.2.3 + resolution: "async@npm:3.2.3" + checksum: c4bee57ab2249af3dc83ca3ef9acfa8e822c0d5e5aa41bae3eaf7f673648343cd64ecd7d26091ffd357f3f044428b17b5f00098494b6cf8b6b3e9681f0636ca1 + languageName: node + linkType: hard + +"async@npm:^2.6.4": + version: 2.6.4 + resolution: "async@npm:2.6.4" + dependencies: + lodash: ^4.17.14 + checksum: a52083fb32e1ebe1d63e5c5624038bb30be68ff07a6c8d7dfe35e47c93fc144bd8652cbec869e0ac07d57dde387aa5f1386be3559cdee799cb1f789678d88e19 + languageName: node + linkType: hard + +"async@npm:^3.2.3": + version: 3.2.5 + resolution: "async@npm:3.2.5" + checksum: 5ec77f1312301dee02d62140a6b1f7ee0edd2a0f983b6fd2b0849b969f245225b990b47b8243e7b9ad16451a53e7f68e753700385b706198ced888beedba3af4 + languageName: node + linkType: hard + "asynciterator.prototype@npm:^1.0.0": version: 1.0.0 resolution: "asynciterator.prototype@npm:1.0.0" @@ -4438,6 +4744,13 @@ __metadata: languageName: node linkType: hard +"b4a@npm:^1.0.1": + version: 1.6.6 + resolution: "b4a@npm:1.6.6" + checksum: c46a27e3ac9c84426ae728f0fc46a6ae7703a7bc03e771fa0bef4827fd7cf3bb976d1a3d5afff54606248372ab8fdf595bd0114406690edf37f14d120630cf7f + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -4477,6 +4790,46 @@ __metadata: languageName: node linkType: hard +"benchmark@npm:^2.1.4": + version: 2.1.4 + resolution: "benchmark@npm:2.1.4" + dependencies: + lodash: ^4.17.4 + platform: ^1.3.3 + checksum: aa466561d4f2b0a2419a3069b8f90fd35ffacf26849697eea9de525ecfbd10b44da11070cc51c88d772076db8cb2415641b493de7d6c024fdf8551019c6fcf1c + languageName: node + linkType: hard + +"benny@npm:^3.7.1": + version: 3.7.1 + resolution: "benny@npm:3.7.1" + dependencies: + "@arrows/composition": ^1.0.0 + "@arrows/dispatch": ^1.0.2 + "@arrows/multimethod": ^1.1.6 + benchmark: ^2.1.4 + common-tags: ^1.8.0 + fs-extra: ^10.0.0 + json2csv: ^5.0.6 + kleur: ^4.1.4 + log-update: ^4.0.0 + checksum: 8dcca91afb6e97b986a16fc73a2a12b2d51c306dc1e9fca6ace988b3ca26405dffcb85309083a449d27cfab440d8164b5cff3a0deba034879da401305412af34 + languageName: node + linkType: hard + +"bfj@npm:^7.0.2": + version: 7.1.0 + resolution: "bfj@npm:7.1.0" + dependencies: + bluebird: ^3.7.2 + check-types: ^11.2.3 + hoopy: ^0.1.4 + jsonpath: ^1.1.1 + tryer: ^1.0.1 + checksum: 36da9ed36c60f377a3f43bb0433092af7dc40442914b8155a1330ae86b1905640baf57e9c195ab83b36d6518b27cf8ed880adff663aa444c193be149e027d722 + languageName: node + linkType: hard + "big.js@npm:^5.2.2": version: 5.2.2 resolution: "big.js@npm:5.2.2" @@ -4524,6 +4877,38 @@ __metadata: languageName: node linkType: hard +"blake-hash@npm:^2.0.0": + version: 2.0.0 + resolution: "blake-hash@npm:2.0.0" + dependencies: + node-addon-api: ^3.0.0 + node-gyp: latest + node-gyp-build: ^4.2.2 + readable-stream: ^3.6.0 + checksum: a0d9a8f3953b986d3b30a741a6c000dedcc9a03b1318f52cc01ae62d18829ba6cb1a4d8cbe74785abfdc952a21db410984523bd457764aca716162cfd3ca8ea4 + languageName: node + linkType: hard + +"blake2b-wasm@npm:^2.4.0": + version: 2.4.0 + resolution: "blake2b-wasm@npm:2.4.0" + dependencies: + b4a: ^1.0.1 + nanoassert: ^2.0.0 + checksum: 1839dd0693681228d81f781dd8680a6803a629ca374aed5ac02fa649ce40a67a85eb49eeb2ec6aeb7ef3ef5aa169b3579b50eabf7afa360d190338bc3e8666bd + languageName: node + linkType: hard + +"blake2b@npm:^2.1.3": + version: 2.1.4 + resolution: "blake2b@npm:2.1.4" + dependencies: + blake2b-wasm: ^2.4.0 + nanoassert: ^2.0.0 + checksum: cd598c96d459017714b7f9cb8892ef2b0a94f362e26e6baa46b7b8b189af0006ce25ab1713e3ec1484eb18ce7ebe1eb24e07d01e42ab1147067de2419c7f6158 + languageName: node + linkType: hard + "blakejs@npm:^1.1.0": version: 1.2.1 resolution: "blakejs@npm:1.2.1" @@ -4538,6 +4923,13 @@ __metadata: languageName: node linkType: hard +"bluebird@npm:^3.7.2": + version: 3.7.2 + resolution: "bluebird@npm:3.7.2" + checksum: 869417503c722e7dc54ca46715f70e15f4d9c602a423a02c825570862d12935be59ed9c7ba34a9b31f186c017c23cac6b54e35446f8353059c101da73eac22ef + languageName: node + linkType: hard + "bn.js@npm:4.11.6": version: 4.11.6 resolution: "bn.js@npm:4.11.6" @@ -4570,6 +4962,22 @@ __metadata: languageName: node linkType: hard +"boxen@npm:^5.1.2": + version: 5.1.2 + resolution: "boxen@npm:5.1.2" + dependencies: + ansi-align: ^3.0.0 + camelcase: ^6.2.0 + chalk: ^4.1.0 + cli-boxes: ^2.2.1 + string-width: ^4.2.2 + type-fest: ^0.20.2 + widest-line: ^3.1.0 + wrap-ansi: ^7.0.0 + checksum: 82d03e42a72576ff235123f17b7c505372fe05c83f75f61e7d4fa4bcb393897ec95ce766fecb8f26b915f0f7a7227d66e5ec7cef43f5b2bd9d3aeed47ec55877 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -4787,7 +5195,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.0.0": +"camelcase@npm:^6.0.0, camelcase@npm:^6.2.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -4857,6 +5265,36 @@ __metadata: languageName: node linkType: hard +"chai@npm:^4.3.7": + version: 4.4.1 + resolution: "chai@npm:4.4.1" + dependencies: + assertion-error: ^1.1.0 + check-error: ^1.0.3 + deep-eql: ^4.1.3 + get-func-name: ^2.0.2 + loupe: ^2.3.6 + pathval: ^1.1.1 + type-detect: ^4.0.8 + checksum: 9ab84f36eb8e0b280c56c6c21ca4da5933132cd8a0c89c384f1497f77953640db0bc151edd47f81748240a9fab57b78f7d925edfeedc8e8fc98016d71f40c36e + languageName: node + linkType: hard + +"chai@npm:~4.3.6": + version: 4.3.10 + resolution: "chai@npm:4.3.10" + dependencies: + assertion-error: ^1.1.0 + check-error: ^1.0.3 + deep-eql: ^4.1.3 + get-func-name: ^2.0.2 + loupe: ^2.3.6 + pathval: ^1.1.1 + type-detect: ^4.0.8 + checksum: 536668c60a0d985a0fbd94418028e388d243a925d7c5e858c7443e334753511614a3b6a124bac9ca077dfc4c37acc367d62f8c294960f440749536dc181dfc6d + languageName: node + linkType: hard + "chalk@npm:5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" @@ -4875,7 +5313,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": +"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -4901,6 +5339,20 @@ __metadata: languageName: node linkType: hard +"check-types@npm:^11.2.3": + version: 11.2.3 + resolution: "check-types@npm:11.2.3" + checksum: f99ff09ae65e63cfcfa40a1275c0a70d8c43ffbf9ac35095f3bf030cc70361c92e075a9975a1144329e50b4fe4620be6bedb4568c18abc96071a3e23aed3ed8e + languageName: node + linkType: hard + +"child_process@npm:^1.0.2": + version: 1.0.2 + resolution: "child_process@npm:1.0.2" + checksum: bd814d82bc8c6e85ed6fb157878978121cd03b5296c09f6135fa3d081fd9a6a617a6d509c50397711df713af403331241a9c0397a7fad30672051485e156c2a1 + languageName: node + linkType: hard + "chokidar@npm:3.3.0": version: 3.3.0 resolution: "chokidar@npm:3.3.0" @@ -4989,6 +5441,70 @@ __metadata: languageName: node linkType: hard +"circom_runtime@npm:0.1.21": + version: 0.1.21 + resolution: "circom_runtime@npm:0.1.21" + dependencies: + ffjavascript: 0.2.56 + bin: + calcwit: calcwit.js + checksum: 3071f1e0fba9a5fb41c940454edb911ce09edfd5d0bd12156ec79045a0bf3ff2cc5b35f46e84e42902ef8bb0a4166f428b75d0ceb363c0d485f1a111b27daba1 + languageName: node + linkType: hard + +"circom_runtime@npm:0.1.24": + version: 0.1.24 + resolution: "circom_runtime@npm:0.1.24" + dependencies: + ffjavascript: 0.2.60 + bin: + calcwit: calcwit.js + checksum: 0654649ed64ace5562cec025648422970df885e79048b7942e283ef50f43142673acfd752ac2851917a81c43d4d42d37f5fdfc8df71d59ee81b873c19ea81a31 + languageName: node + linkType: hard + +"circom_tester@npm:^0.0.19": + version: 0.0.19 + resolution: "circom_tester@npm:0.0.19" + dependencies: + chai: ^4.3.6 + child_process: ^1.0.2 + ffjavascript: ^0.2.56 + fnv-plus: ^1.3.1 + r1csfile: ^0.0.41 + snarkjs: 0.5.0 + tmp-promise: ^3.0.3 + util: ^0.12.4 + checksum: 703d7317493ddafb33462b5b0caf9b8a95bde938429e030d024fa7ac41ccb45a9ddbe8ebb93f91138dbee94bb2e0d504570c0aca070802d79ee532ba1ae5db0e + languageName: node + linkType: hard + +"circomkit@npm:^0.0.24": + version: 0.0.24 + resolution: "circomkit@npm:0.0.24" + dependencies: + chai: ^4.3.7 + circom_tester: ^0.0.19 + loglevel: ^1.8.1 + snarkjs: ^0.7.0 + bin: + circomkit: dist/bin/index.js + checksum: efc5cdf6fc0a76451dbbcee08df76acd39a9055cf75b67d2b54aec635af4c8177a1626257bc73457a06db3befa9b324880aaa88df5785e8a0a78d62dd0a4f404 + languageName: node + linkType: hard + +"circomlibjs@npm:^0.1.7": + version: 0.1.7 + resolution: "circomlibjs@npm:0.1.7" + dependencies: + blake-hash: ^2.0.0 + blake2b: ^2.1.3 + ethers: ^5.5.1 + ffjavascript: ^0.2.45 + checksum: 843aa422e7a203d06e1ef9ae6bf334d726a8091eda5afcc3a6531727a6ea3acb5c4a2498bf444fda35824064d45c25cb89fe99b02410fc0f7ff2dc6d7101d779 + languageName: node + linkType: hard + "citty@npm:^0.1.4, citty@npm:^0.1.5": version: 0.1.5 resolution: "citty@npm:0.1.5" @@ -5019,6 +5535,22 @@ __metadata: languageName: node linkType: hard +"cli-boxes@npm:^2.2.1": + version: 2.2.1 + resolution: "cli-boxes@npm:2.2.1" + checksum: be79f8ec23a558b49e01311b39a1ea01243ecee30539c880cf14bf518a12e223ef40c57ead0cb44f509bffdffc5c129c746cd50d863ab879385370112af4f585 + languageName: node + linkType: hard + +"cli-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "cli-cursor@npm:3.1.0" + dependencies: + restore-cursor: ^3.1.0 + checksum: 2692784c6cd2fd85cfdbd11f53aea73a463a6d64a77c3e098b2b4697a20443f430c220629e1ca3b195ea5ac4a97a74c2ee411f3807abf6df2b66211fec0c0a29 + languageName: node + linkType: hard + "cli-cursor@npm:^4.0.0": version: 4.0.0 resolution: "cli-cursor@npm:4.0.0" @@ -5179,6 +5711,13 @@ __metadata: languageName: node linkType: hard +"colors@npm:1.0.x": + version: 1.0.3 + resolution: "colors@npm:1.0.3" + checksum: 234e8d3ab7e4003851cdd6a1f02eaa16dabc502ee5f4dc576ad7959c64b7477b15bd21177bab4055a4c0a66aa3d919753958030445f87c39a253d73b7a3637f5 + languageName: node + linkType: hard + "colors@npm:1.4.0, colors@npm:^1.1.2": version: 1.4.0 resolution: "colors@npm:1.4.0" @@ -5254,6 +5793,20 @@ __metadata: languageName: node linkType: hard +"commander@npm:^6.1.0": + version: 6.2.1 + resolution: "commander@npm:6.2.1" + checksum: d7090410c0de6bc5c67d3ca41c41760d6d268f3c799e530aafb73b7437d1826bbf0d2a3edac33f8b57cc9887b4a986dce307fa5557e109be40eadb7c43b21742 + languageName: node + linkType: hard + +"common-tags@npm:^1.8.0": + version: 1.8.2 + resolution: "common-tags@npm:1.8.2" + checksum: 767a6255a84bbc47df49a60ab583053bb29a7d9687066a18500a516188a062c4e4cd52de341f22de0b07062e699b1b8fe3cfa1cb55b241cb9301aeb4f45b4dff + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -5448,6 +6001,13 @@ __metadata: languageName: node linkType: hard +"cycle@npm:1.0.x": + version: 1.0.3 + resolution: "cycle@npm:1.0.3" + checksum: b9f131094fb832a8c4ba18c6d2dc9c87fc80d3242847a45f0a5f70911b2acab68abc1c25eb23e5155fcf2135a27d8fcc3635556745b03b488c4f360cfbc352df + languageName: node + linkType: hard + "daisyui@npm:4.5.0": version: 4.5.0 resolution: "daisyui@npm:4.5.0" @@ -5550,7 +6110,7 @@ __metadata: languageName: node linkType: hard -"deep-eql@npm:^4.0.1, deep-eql@npm:^4.1.2": +"deep-eql@npm:^4.0.1, deep-eql@npm:^4.1.2, deep-eql@npm:^4.1.3": version: 4.1.3 resolution: "deep-eql@npm:4.1.3" dependencies: @@ -5702,19 +6262,6 @@ __metadata: languageName: node linkType: hard -"detect-port@npm:^1.3.0": - version: 1.5.1 - resolution: "detect-port@npm:1.5.1" - dependencies: - address: ^1.0.1 - debug: 4 - bin: - detect: bin/detect-port.js - detect-port: bin/detect-port.js - checksum: b48da9340481742547263d5d985e65d078592557863402ecf538511735e83575867e94f91fe74405ea19b61351feb99efccae7e55de9a151d5654e3417cea05b - languageName: node - linkType: hard - "didyoumean@npm:^1.2.2": version: 1.2.2 resolution: "didyoumean@npm:1.2.2" @@ -5793,10 +6340,10 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.0.3": - version: 16.3.1 - resolution: "dotenv@npm:16.3.1" - checksum: 15d75e7279018f4bafd0ee9706593dd14455ddb71b3bcba9c52574460b7ccaf67d5cf8b2c08a5af1a9da6db36c956a04a1192b101ee102a3e0cf8817bbcf3dfd +"dotenv@npm:~16.0.3": + version: 16.0.3 + resolution: "dotenv@npm:16.0.3" + checksum: afcf03f373d7a6d62c7e9afea6328e62851d627a4e73f2e12d0a8deae1cd375892004f3021883f8aec85932cd2834b091f568ced92b4774625b321db83b827f8 languageName: node linkType: hard @@ -5848,6 +6395,17 @@ __metadata: languageName: node linkType: hard +"ejs@npm:^3.1.6": + version: 3.1.9 + resolution: "ejs@npm:3.1.9" + dependencies: + jake: ^10.8.5 + bin: + ejs: bin/cli.js + checksum: af6f10eb815885ff8a8cfacc42c6b6cf87daf97a4884f87a30e0c3271fedd85d76a3a297d9c33a70e735b97ee632887f85e32854b9cdd3a2d97edf931519a35f + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.4.530": version: 1.4.531 resolution: "electron-to-chromium@npm:1.4.531" @@ -5952,7 +6510,7 @@ __metadata: languageName: node linkType: hard -"envfile@npm:^6.18.0": +"envfile@npm:~6.18.0": version: 6.18.0 resolution: "envfile@npm:6.18.0" bin: @@ -6342,6 +6900,25 @@ __metadata: languageName: node linkType: hard +"escodegen@npm:^1.8.1": + version: 1.14.3 + resolution: "escodegen@npm:1.14.3" + dependencies: + esprima: ^4.0.1 + estraverse: ^4.2.0 + esutils: ^2.0.2 + optionator: ^0.8.1 + source-map: ~0.6.1 + dependenciesMeta: + source-map: + optional: true + bin: + escodegen: bin/escodegen.js + esgenerate: bin/esgenerate.js + checksum: 381cdc4767ecdb221206bbbab021b467bbc2a6f5c9a99c9e6353040080bdd3dfe73d7604ad89a47aca6ea7d58bc635f6bd3fbc8da9a1998e9ddfa8372362ccd0 + languageName: node + linkType: hard + "eslint-config-next@npm:^14.0.4": version: 14.0.4 resolution: "eslint-config-next@npm:14.0.4" @@ -6376,6 +6953,17 @@ __metadata: languageName: node linkType: hard +"eslint-config-prettier@npm:~8.5.0": + version: 8.5.0 + resolution: "eslint-config-prettier@npm:8.5.0" + peerDependencies: + eslint: ">=7.0.0" + bin: + eslint-config-prettier: bin/cli.js + checksum: 0d0f5c32e7a0ad91249467ce71ca92394ccd343178277d318baf32063b79ea90216f4c81d1065d60f96366fdc60f151d4d68ae7811a58bd37228b84c2083f893 + languageName: node + linkType: hard + "eslint-import-resolver-node@npm:^0.3.6, eslint-import-resolver-node@npm:^0.3.7": version: 0.3.9 resolution: "eslint-import-resolver-node@npm:0.3.9" @@ -6470,7 +7058,7 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-prettier@npm:^4.2.1": +"eslint-plugin-prettier@npm:^4.2.1, eslint-plugin-prettier@npm:~4.2.1": version: 4.2.1 resolution: "eslint-plugin-prettier@npm:4.2.1" dependencies: @@ -6530,7 +7118,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.2.2": +"eslint-scope@npm:^7.1.1, eslint-scope@npm:^7.2.2": version: 7.2.2 resolution: "eslint-scope@npm:7.2.2" dependencies: @@ -6540,6 +7128,24 @@ __metadata: languageName: node linkType: hard +"eslint-utils@npm:^3.0.0": + version: 3.0.0 + resolution: "eslint-utils@npm:3.0.0" + dependencies: + eslint-visitor-keys: ^2.0.0 + peerDependencies: + eslint: ">=5" + checksum: 0668fe02f5adab2e5a367eee5089f4c39033af20499df88fe4e6aba2015c20720404d8c3d6349b6f716b08fdf91b9da4e5d5481f265049278099c4c836ccb619 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^2.0.0": + version: 2.1.0 + resolution: "eslint-visitor-keys@npm:2.1.0" + checksum: e3081d7dd2611a35f0388bbdc2f5da60b3a3c5b8b6e928daffff7391146b434d691577aa95064c8b7faad0b8a680266bcda0a42439c18c717b80e6718d7e267d + languageName: node + linkType: hard + "eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" @@ -6547,7 +7153,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.15.0, eslint@npm:^8.26.0": +"eslint@npm:^8.15.0": version: 8.50.0 resolution: "eslint@npm:8.50.0" dependencies: @@ -6594,7 +7200,56 @@ __metadata: languageName: node linkType: hard -"espree@npm:^9.6.0, espree@npm:^9.6.1": +"eslint@npm:~8.26.0": + version: 8.26.0 + resolution: "eslint@npm:8.26.0" + dependencies: + "@eslint/eslintrc": ^1.3.3 + "@humanwhocodes/config-array": ^0.11.6 + "@humanwhocodes/module-importer": ^1.0.1 + "@nodelib/fs.walk": ^1.2.8 + ajv: ^6.10.0 + chalk: ^4.0.0 + cross-spawn: ^7.0.2 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.1.1 + eslint-utils: ^3.0.0 + eslint-visitor-keys: ^3.3.0 + espree: ^9.4.0 + esquery: ^1.4.0 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + globals: ^13.15.0 + grapheme-splitter: ^1.0.4 + ignore: ^5.2.0 + import-fresh: ^3.0.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + is-path-inside: ^3.0.3 + js-sdsl: ^4.1.4 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.1 + regexpp: ^3.2.0 + strip-ansi: ^6.0.1 + strip-json-comments: ^3.1.0 + text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: a2aced939ea060f77d10dcfced5cfeb940f63f383fd7ab1decadea64170ab552582e1c5909db1db641d4283178c9bc569f19b0f8900e00314a5f783e4b3f759d + languageName: node + linkType: hard + +"espree@npm:^9.4.0, espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" dependencies: @@ -6605,6 +7260,16 @@ __metadata: languageName: node linkType: hard +"esprima@npm:1.2.2": + version: 1.2.2 + resolution: "esprima@npm:1.2.2" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: 4f10006f0e315f2f7d8cf6630e465f183512f1ab2e862b11785a133ce37ed1696573deefb5256e510eaa4368342b13b393334477f6ccdcdb8f10e782b0f5e6dc + languageName: node + linkType: hard + "esprima@npm:2.7.x, esprima@npm:^2.7.1": version: 2.7.3 resolution: "esprima@npm:2.7.3" @@ -6615,7 +7280,7 @@ __metadata: languageName: node linkType: hard -"esprima@npm:^4.0.0": +"esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" bin: @@ -6625,7 +7290,7 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.2": +"esquery@npm:^1.4.0, esquery@npm:^1.4.2": version: 1.5.0 resolution: "esquery@npm:1.5.0" dependencies: @@ -6650,7 +7315,7 @@ __metadata: languageName: node linkType: hard -"estraverse@npm:^4.1.1": +"estraverse@npm:^4.1.1, estraverse@npm:^4.2.0": version: 4.3.0 resolution: "estraverse@npm:4.3.0" checksum: a6299491f9940bb246124a8d44b7b7a413a8336f5436f9837aaa9330209bd9ee8af7e91a654a3545aee9c54b3308e78ee360cef1d777d37cfef77d2fa33b5827 @@ -6877,7 +7542,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^5.7.0, ethers@npm:^5.7.1": +"ethers@npm:^5.5.1, ethers@npm:^5.7.0, ethers@npm:^5.7.1, ethers@npm:~5.7.0": version: 5.7.2 resolution: "ethers@npm:5.7.2" dependencies: @@ -6915,7 +7580,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^6.10.0": +"ethers@npm:~6.10.0": version: 6.10.0 resolution: "ethers@npm:6.10.0" dependencies: @@ -7076,7 +7741,7 @@ __metadata: languageName: node linkType: hard -"eyes@npm:^0.1.8": +"eyes@npm:0.1.x, eyes@npm:^0.1.8": version: 0.1.8 resolution: "eyes@npm:0.1.8" checksum: c31703a92bf36ba75ee8d379ee7985c24ee6149f3a6175f44cec7a05b178c38bce9836d3ca48c9acb0329a960ac2c4b2ead4e60cdd4fe6e8c92cad7cd6913687 @@ -7145,6 +7810,13 @@ __metadata: languageName: node linkType: hard +"fastfile@npm:0.0.20": + version: 0.0.20 + resolution: "fastfile@npm:0.0.20" + checksum: e5d6e5f57a9b58c9534202e477cbffbca2182c407171950695ddb5c3e6b89554bc8561fbb6e370c99e371a8f23486a23fbaca527827886cec4897d481cbd03b6 + languageName: node + linkType: hard + "fastparse@npm:^1.1.2": version: 1.1.2 resolution: "fastparse@npm:1.1.2" @@ -7170,6 +7842,39 @@ __metadata: languageName: node linkType: hard +"ffjavascript@npm:0.2.56": + version: 0.2.56 + resolution: "ffjavascript@npm:0.2.56" + dependencies: + wasmbuilder: 0.0.16 + wasmcurves: 0.2.0 + web-worker: ^1.2.0 + checksum: d4e02263db4a94d111cdc7c1211ae96769370f5c8c3c338331e0ef99faed7b55e640bedf23fa8a83fc9a77f0e81140ea8f32e392812a00e15ca504221b879a4f + languageName: node + linkType: hard + +"ffjavascript@npm:0.2.60": + version: 0.2.60 + resolution: "ffjavascript@npm:0.2.60" + dependencies: + wasmbuilder: 0.0.16 + wasmcurves: 0.2.2 + web-worker: ^1.2.0 + checksum: 5dbd597ee108373af1ba1877757a08b47db4bcd7b3b043b260d4475a0b1125454de00fb214224f85d9e6e6a6c0a1cfabd1ba9a2b68279b99c581404266e78138 + languageName: node + linkType: hard + +"ffjavascript@npm:0.2.63, ffjavascript@npm:^0.2.45, ffjavascript@npm:^0.2.48, ffjavascript@npm:^0.2.56": + version: 0.2.63 + resolution: "ffjavascript@npm:0.2.63" + dependencies: + wasmbuilder: 0.0.16 + wasmcurves: 0.2.2 + web-worker: 1.2.0 + checksum: d4c549228b31692bb8a44f7eb46720b0504a9ff5bc00e8a9c44ad104622cb83613542b4c321f83cb3027c9dcd1b0834da2b1ea679c3277642f754e9e5ea1c06f + languageName: node + linkType: hard + "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -7186,6 +7891,15 @@ __metadata: languageName: node linkType: hard +"filelist@npm:^1.0.4": + version: 1.0.4 + resolution: "filelist@npm:1.0.4" + dependencies: + minimatch: ^5.0.1 + checksum: a303573b0821e17f2d5e9783688ab6fbfce5d52aaac842790ae85e704a6f5e4e3538660a63183d6453834dedf1e0f19a9dadcebfa3e926c72397694ea11f5160 + languageName: node + linkType: hard + "fill-range@npm:^7.0.1": version: 7.0.1 resolution: "fill-range@npm:7.0.1" @@ -7296,6 +8010,13 @@ __metadata: languageName: node linkType: hard +"fnv-plus@npm:^1.3.1": + version: 1.3.1 + resolution: "fnv-plus@npm:1.3.1" + checksum: 4d3de8026d538ffab13dfa38ac0662b045b2ad0f920efa54f1ca65f59ad1a49b4d62482c5fcdc9cce0a18d9852df1db97c618937089d85678ce03f2e76b07e8b + languageName: node + linkType: hard + "follow-redirects@npm:^1.12.1, follow-redirects@npm:^1.14.0": version: 1.15.3 resolution: "follow-redirects@npm:1.15.3" @@ -7614,7 +8335,7 @@ __metadata: languageName: node linkType: hard -"get-func-name@npm:^2.0.0, get-func-name@npm:^2.0.2": +"get-func-name@npm:^2.0.0, get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" checksum: 3f62f4c23647de9d46e6f76d2b3eafe58933a9b3830c60669e4180d6c601ce1b4aa310ba8366143f55e52b139f992087a9f0647274e8745621fa2af7e0acf13b @@ -7791,6 +8512,19 @@ __metadata: languageName: node linkType: hard +"glob@npm:8.1.0": + version: 8.1.0 + resolution: "glob@npm:8.1.0" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^5.0.1 + once: ^1.3.0 + checksum: 92fbea3221a7d12075f26f0227abac435de868dd0736a17170663783296d0dd8d3d532a5672b4488a439bf5d7fb85cdd07c11185d6cd39184f0385cbdfb86a47 + languageName: node + linkType: hard + "glob@npm:^10.2.2": version: 10.3.10 resolution: "glob@npm:10.3.10" @@ -7860,6 +8594,15 @@ __metadata: languageName: node linkType: hard +"globals@npm:^13.15.0": + version: 13.24.0 + resolution: "globals@npm:13.24.0" + dependencies: + type-fest: ^0.20.2 + checksum: 56066ef058f6867c04ff203b8a44c15b038346a62efbc3060052a1016be9f56f4cf0b2cd45b74b22b81e521a889fc7786c73691b0549c2f3a6e825b3d394f43c + languageName: node + linkType: hard + "globals@npm:^13.19.0": version: 13.22.0 resolution: "globals@npm:13.22.0" @@ -7933,6 +8676,13 @@ __metadata: languageName: node linkType: hard +"grapheme-splitter@npm:^1.0.4": + version: 1.0.4 + resolution: "grapheme-splitter@npm:1.0.4" + checksum: 0c22ec54dee1b05cd480f78cf14f732cb5b108edc073572c4ec205df4cd63f30f8db8025afc5debc8835a8ddeacf648a1c7992fe3dcd6ad38f9a476d84906620 + languageName: node + linkType: hard + "graphemer@npm:^1.4.0": version: 1.4.0 resolution: "graphemer@npm:1.4.0" @@ -7998,7 +8748,16 @@ __metadata: languageName: node linkType: hard -"hardhat-deploy-ethers@npm:^0.4.1": +"hardhat-artifactor@npm:^0.2.0": + version: 0.2.0 + resolution: "hardhat-artifactor@npm:0.2.0" + peerDependencies: + hardhat: ^2.0.0 + checksum: b5c2d02d150ef5b184080c522a0e64649a8b69578d2b74d61115ba5988bb0ef4798922cd3662c3f254e6fb9760398c2d83bbb645cc0f11b36bf5d9d690c0ffe6 + languageName: node + linkType: hard + +"hardhat-deploy-ethers@npm:~0.4.1": version: 0.4.1 resolution: "hardhat-deploy-ethers@npm:0.4.1" peerDependencies: @@ -8009,9 +8768,9 @@ __metadata: languageName: node linkType: hard -"hardhat-deploy@npm:^0.11.45": - version: 0.11.45 - resolution: "hardhat-deploy@npm:0.11.45" +"hardhat-deploy@npm:^0.12.2": + version: 0.12.2 + resolution: "hardhat-deploy@npm:0.12.2" dependencies: "@ethersproject/abi": ^5.7.0 "@ethersproject/abstract-signer": ^5.7.0 @@ -8036,27 +8795,27 @@ __metadata: match-all: ^1.2.6 murmur-128: ^0.2.1 qs: ^6.9.4 - zksync-web3: ^0.14.3 - checksum: 7ecce33c3305857bdd1873a25d391e27ae9f581df75757035cb028ace7bb5fbb83f053435e843bc3d925e7fd8412c3dc582797fe5b4bbe1fef7f3dd989a7c878 + zksync-ethers: ^5.0.0 + checksum: 0ace08209c7ec4fa6eafd24a76d3cf1b38784b3c130b6b6882fcba8ee12dda003402c61a16796c526654aa04cf9a8b4895b19aa3557bde81212863615a13f85c languageName: node linkType: hard -"hardhat-gas-reporter@npm:^1.0.9": - version: 1.0.9 - resolution: "hardhat-gas-reporter@npm:1.0.9" +"hardhat-gas-reporter@npm:~1.0.9": + version: 1.0.10 + resolution: "hardhat-gas-reporter@npm:1.0.10" dependencies: array-uniq: 1.0.3 eth-gas-reporter: ^0.2.25 sha1: ^1.1.1 peerDependencies: hardhat: ^2.0.2 - checksum: 77f8f8d085ff3d9d7787f0227e5355e1800f7d6707bc70171e0567bf69706703ae7f6f53dce1be1d409e7e71e3629a434c94b546bdbbc1e4c1af47cd5d0c6776 + checksum: caaec13ab3fcda47b8768257e4416b5fd0e8ef3aca5369aa8195419d3d4a948cc182075333651df44215cfc629d088f5ed9f762c8c14ae5a4b4a4f2613e583d0 languageName: node linkType: hard -"hardhat@npm:^2.19.4": - version: 2.19.4 - resolution: "hardhat@npm:2.19.4" +"hardhat@npm:~2.19.4": + version: 2.19.5 + resolution: "hardhat@npm:2.19.5" dependencies: "@ethersproject/abi": ^5.1.2 "@metamask/eth-sig-util": ^4.0.0 @@ -8077,6 +8836,7 @@ __metadata: adm-zip: ^0.4.16 aggregate-error: ^3.0.0 ansi-escapes: ^4.3.0 + boxen: ^5.1.2 chalk: ^2.4.2 chokidar: ^3.4.0 ci-info: ^2.0.0 @@ -8116,7 +8876,7 @@ __metadata: optional: true bin: hardhat: internal/cli/bootstrap.js - checksum: 05dcaeab5bb641e74426ad47acfda903dcd3fd229b0d30f45b9de1d3c54fe6364161f3c88517984233d18f5b9294a050500ca7336b6ca069fa259fede6f5acb1 + checksum: 316b03a1d090360e6ed471fe125360ec0c66c5bb62e29492898932b1a9a5227c12d7a18343877c59725f321647a01fde0841649bf7d8a4a746148a0d38b0ee27 languageName: node linkType: hard @@ -8261,6 +9021,13 @@ __metadata: languageName: node linkType: hard +"hoopy@npm:^0.1.4": + version: 0.1.4 + resolution: "hoopy@npm:0.1.4" + checksum: cfa60c7684c5e1ee4efe26e167bc54b73f839ffb59d1d44a5c4bf891e26b4f5bcc666555219a98fec95508fea4eda3a79540c53c05cc79afc1f66f9a238f4d9e + languageName: node + linkType: hard + "http-basic@npm:^8.1.1": version: 8.1.3 resolution: "http-basic@npm:8.1.3" @@ -8449,7 +9216,7 @@ __metadata: languageName: node linkType: hard -"import-fresh@npm:^3.2.1": +"import-fresh@npm:^3.0.0, import-fresh@npm:^3.2.1": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" dependencies: @@ -8961,7 +9728,7 @@ __metadata: languageName: node linkType: hard -"isstream@npm:~0.1.2": +"isstream@npm:0.1.x, isstream@npm:~0.1.2": version: 0.1.2 resolution: "isstream@npm:0.1.2" checksum: 1eb2fe63a729f7bdd8a559ab552c69055f4f48eb5c2f03724430587c6f450783c8f1cd936c1c952d0a927925180fcc892ebd5b174236cf1065d4bd5bdb37e963 @@ -8994,6 +9761,20 @@ __metadata: languageName: node linkType: hard +"jake@npm:^10.8.5": + version: 10.8.7 + resolution: "jake@npm:10.8.7" + dependencies: + async: ^3.2.3 + chalk: ^4.0.2 + filelist: ^1.0.4 + minimatch: ^3.1.2 + bin: + jake: bin/cli.js + checksum: a23fd2273fb13f0d0d845502d02c791fd55ef5c6a2d207df72f72d8e1eac6d2b8ffa6caf660bc8006b3242e0daaa88a3ecc600194d72b5c6016ad56e9cd43553 + languageName: node + linkType: hard + "javascript-natural-sort@npm:0.7.1": version: 0.7.1 resolution: "javascript-natural-sort@npm:0.7.1" @@ -9196,6 +9977,19 @@ __metadata: languageName: node linkType: hard +"json2csv@npm:^5.0.6": + version: 5.0.7 + resolution: "json2csv@npm:5.0.7" + dependencies: + commander: ^6.1.0 + jsonparse: ^1.3.1 + lodash.get: ^4.4.2 + bin: + json2csv: bin/json2csv.js + checksum: 81b511e4f5abba1dcda90593c193d15e5f05f1def91377b6289536e31fdb629889da6a2b4612b9ff699116a29b1758d20c0d71f7921fcfb09863da5b2d883139 + languageName: node + linkType: hard + "json5@npm:^1.0.2": version: 1.0.2 resolution: "json5@npm:1.0.2" @@ -9251,13 +10045,24 @@ __metadata: languageName: node linkType: hard -"jsonparse@npm:^1.2.0": +"jsonparse@npm:^1.2.0, jsonparse@npm:^1.3.1": version: 1.3.1 resolution: "jsonparse@npm:1.3.1" checksum: 6514a7be4674ebf407afca0eda3ba284b69b07f9958a8d3113ef1005f7ec610860c312be067e450c569aab8b89635e332cee3696789c750692bb60daba627f4d languageName: node linkType: hard +"jsonpath@npm:^1.1.1": + version: 1.1.1 + resolution: "jsonpath@npm:1.1.1" + dependencies: + esprima: 1.2.2 + static-eval: 2.0.2 + underscore: 1.12.1 + checksum: 5480d8e9e424fe2ed4ade6860b6e2cefddb21adb3a99abe0254cd9428e8ef9b0c9fb5729d6a5a514e90df50d645ccea9f3be48d627570e6222dd5dadc28eba7b + languageName: node + linkType: hard + "jsonschema@npm:^1.2.4": version: 1.4.1 resolution: "jsonschema@npm:1.4.1" @@ -9336,6 +10141,13 @@ __metadata: languageName: node linkType: hard +"kleur@npm:^4.1.4": + version: 4.1.5 + resolution: "kleur@npm:4.1.5" + checksum: 1dc476e32741acf0b1b5b0627ffd0d722e342c1b0da14de3e8ae97821327ca08f9fb944542fb3c126d90ac5f27f9d804edbe7c585bf7d12ef495d115e0f22c12 + languageName: node + linkType: hard + "language-subtag-registry@npm:~0.3.2": version: 0.3.22 resolution: "language-subtag-registry@npm:0.3.22" @@ -9577,6 +10389,13 @@ __metadata: languageName: node linkType: hard +"lodash.get@npm:^4.4.2": + version: 4.4.2 + resolution: "lodash.get@npm:4.4.2" + checksum: e403047ddb03181c9d0e92df9556570e2b67e0f0a930fcbbbd779370972368f5568e914f913e93f3b08f6d492abc71e14d4e9b7a18916c31fa04bd2306efe545 + languageName: node + linkType: hard + "lodash.isarguments@npm:^3.1.0": version: 3.1.0 resolution: "lodash.isarguments@npm:3.1.0" @@ -9605,7 +10424,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.21": +"lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.21, lodash@npm:^4.17.4": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -9631,6 +10450,18 @@ __metadata: languageName: node linkType: hard +"log-update@npm:^4.0.0": + version: 4.0.0 + resolution: "log-update@npm:4.0.0" + dependencies: + ansi-escapes: ^4.3.0 + cli-cursor: ^3.1.0 + slice-ansi: ^4.0.0 + wrap-ansi: ^6.2.0 + checksum: ae2f85bbabc1906034154fb7d4c4477c79b3e703d22d78adee8b3862fa913942772e7fa11713e3d96fb46de4e3cabefbf5d0a544344f03b58d3c4bff52aa9eb2 + languageName: node + linkType: hard + "log-update@npm:^5.0.1": version: 5.0.1 resolution: "log-update@npm:5.0.1" @@ -9644,6 +10475,20 @@ __metadata: languageName: node linkType: hard +"loglevel@npm:^1.8.1": + version: 1.9.1 + resolution: "loglevel@npm:1.9.1" + checksum: e1c8586108c4d566122e91f8a79c8df728920e3a714875affa5120566761a24077ec8ec9e5fc388b022e39fc411ec6e090cde1b5775871241b045139771eeb06 + languageName: node + linkType: hard + +"logplease@npm:^1.2.15": + version: 1.2.15 + resolution: "logplease@npm:1.2.15" + checksum: c2ac90a75dfa28234f67c79a112baf3c7d7a3011a6d16b052e299ad2f37430fd17959fa116629e14ef3b56796a2dd7015802d0a835ded107ceacb0ca5a44393f + languageName: node + linkType: hard + "loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -9664,6 +10509,15 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^2.3.6": + version: 2.3.7 + resolution: "loupe@npm:2.3.7" + dependencies: + get-func-name: ^2.0.1 + checksum: 96c058ec7167598e238bb7fb9def2f9339215e97d6685d9c1e3e4bdb33d14600e11fe7a812cf0c003dfb73ca2df374f146280b2287cae9e8d989e9d7a69a203b + languageName: node + linkType: hard + "lru-cache@npm:^10.0.2": version: 10.1.0 resolution: "lru-cache@npm:10.1.0" @@ -9932,6 +10786,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^5.0.1": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" + dependencies: + brace-expansion: ^2.0.1 + checksum: 7564208ef81d7065a370f788d337cd80a689e981042cb9a1d0e6580b6c6a8c9279eba80010516e258835a988363f99f54a6f711a315089b8b42694f5da9d0d77 + languageName: node + linkType: hard + "minimatch@npm:^9.0.1": version: 9.0.3 resolution: "minimatch@npm:9.0.3" @@ -10103,7 +10966,7 @@ __metadata: languageName: node linkType: hard -"mocha@npm:10.2.0, mocha@npm:^10.0.0": +"mocha@npm:^10.0.0": version: 10.2.0 resolution: "mocha@npm:10.2.0" dependencies: @@ -10135,6 +10998,37 @@ __metadata: languageName: node linkType: hard +"mocha@npm:^10.2.0": + version: 10.3.0 + resolution: "mocha@npm:10.3.0" + dependencies: + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.4 + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.0.1 + ms: 2.1.3 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.2.1 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + bin: + _mocha: bin/_mocha + mocha: bin/mocha.js + checksum: b5e95b9c270b2c33589e2f19d7ee37ac7577c0d471152d4e2692ebf4bc606a36040da4fbadc1e482b4cf5a0784daac7556bb962ad7b23143086b34a58e43e211 + languageName: node + linkType: hard + "mocha@npm:^7.1.1": version: 7.2.0 resolution: "mocha@npm:7.2.0" @@ -10244,6 +11138,13 @@ __metadata: languageName: node linkType: hard +"mute-stream@npm:~0.0.4": + version: 0.0.8 + resolution: "mute-stream@npm:0.0.8" + checksum: ff48d251fc3f827e5b1206cda0ffdaec885e56057ee86a3155e1951bc940fd5f33531774b1cc8414d7668c10a8907f863f6561875ee6e8768931a62121a531a1 + languageName: node + linkType: hard + "mz@npm:^2.7.0": version: 2.7.0 resolution: "mz@npm:2.7.0" @@ -10255,6 +11156,13 @@ __metadata: languageName: node linkType: hard +"nanoassert@npm:^2.0.0": + version: 2.0.0 + resolution: "nanoassert@npm:2.0.0" + checksum: b1d366f9524405f3337192d32dda6ac0b02374e4d0550c9aad33480caf2eb3c84c06f18f41f4c5404c14f6fc1ae6b84692b4375166dcb9f2d04a2ea9b9bccba0 + languageName: node + linkType: hard + "nanoid@npm:3.3.3": version: 3.3.3 resolution: "nanoid@npm:3.3.3" @@ -10391,6 +11299,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^3.0.0": + version: 3.2.1 + resolution: "node-addon-api@npm:3.2.1" + dependencies: + node-gyp: latest + checksum: 2369986bb0881ccd9ef6bacdf39550e07e089a9c8ede1cbc5fc7712d8e2faa4d50da0e487e333d4125f8c7a616c730131d1091676c9d499af1d74560756b4a18 + languageName: node + linkType: hard + "node-addon-api@npm:^7.0.0": version: 7.0.0 resolution: "node-addon-api@npm:7.0.0" @@ -10829,7 +11746,7 @@ __metadata: languageName: node linkType: hard -"optionator@npm:^0.9.3": +"optionator@npm:^0.9.1, optionator@npm:^0.9.3": version: 0.9.3 resolution: "optionator@npm:0.9.3" dependencies: @@ -11234,6 +12151,13 @@ __metadata: languageName: node linkType: hard +"platform@npm:^1.3.3": + version: 1.3.6 + resolution: "platform@npm:1.3.6" + checksum: 6f472a09c61d418c7e26c1c16d0bdc029549d512dbec6526216a1e59ec68100d07007d0097dcba69dddad883d6f2a83361b4bdfe0094a3d9a2af24158643d85e + languageName: node + linkType: hard + "pngjs@npm:^5.0.0": version: 5.0.0 resolution: "pngjs@npm:5.0.0" @@ -11363,7 +12287,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.3.1, prettier@npm:^2.8.4": +"prettier@npm:^2.3.1, prettier@npm:^2.8.4, prettier@npm:~2.8.4": version: 2.8.8 resolution: "prettier@npm:2.8.8" bin: @@ -11428,6 +12352,19 @@ __metadata: languageName: node linkType: hard +"prompt@npm:^1.3.0": + version: 1.3.0 + resolution: "prompt@npm:1.3.0" + dependencies: + "@colors/colors": 1.5.0 + async: 3.2.3 + read: 1.0.x + revalidator: 0.1.x + winston: 2.x + checksum: d2bebb05bfc39a86215011ee9f32660f23d54d373bd450d1193883b66bac817eca6d2267d42374735e3a57209fc52ab152bbe7c6d6d312f0edc6959cbcd0205a + languageName: node + linkType: hard + "prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" @@ -11479,7 +12416,7 @@ __metadata: languageName: node linkType: hard -"qrcode@npm:1.5.3, qrcode@npm:^1.5.1": +"qrcode@npm:1.5.3, qrcode@npm:^1.5.1, qrcode@npm:~1.5.1": version: 1.5.3 resolution: "qrcode@npm:1.5.3" dependencies: @@ -11547,6 +12484,30 @@ __metadata: languageName: node linkType: hard +"r1csfile@npm:0.0.41, r1csfile@npm:^0.0.41": + version: 0.0.41 + resolution: "r1csfile@npm:0.0.41" + dependencies: + "@iden3/bigarray": 0.0.2 + "@iden3/binfileutils": 0.0.11 + fastfile: 0.0.20 + ffjavascript: 0.2.56 + checksum: eec689416f66f09db2d6ca66fac1ef6841b088ab29abcde487145ebd2110916c92583e11ac86f0cdcc4e8a3a7c7df9ff5352ad959e8ae385d37c3b51cec5cf4d + languageName: node + linkType: hard + +"r1csfile@npm:0.0.47": + version: 0.0.47 + resolution: "r1csfile@npm:0.0.47" + dependencies: + "@iden3/bigarray": 0.0.2 + "@iden3/binfileutils": 0.0.11 + fastfile: 0.0.20 + ffjavascript: 0.2.60 + checksum: edeb325b83851a71cbca2e5de56eb622ee5347ecae921b526a5fc484c4825b6b30c73b6fde40e9bc5112b9d21e046af885bf212ed9cee2efbc6de93b8454ec06 + languageName: node + linkType: hard + "radix3@npm:^1.1.0": version: 1.1.0 resolution: "radix3@npm:1.1.0" @@ -11700,6 +12661,15 @@ __metadata: languageName: node linkType: hard +"read@npm:1.0.x": + version: 1.0.7 + resolution: "read@npm:1.0.7" + dependencies: + mute-stream: ~0.0.4 + checksum: 2777c254e5732cac96f5d0a1c0f6b836c89ae23d8febd405b206f6f24d5de1873420f1a0795e0e3721066650d19adf802c7882c4027143ee0acf942a4f34f97b + languageName: node + linkType: hard + "readable-stream@npm:^2.2.2": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" @@ -11833,6 +12803,13 @@ __metadata: languageName: node linkType: hard +"regexpp@npm:^3.2.0": + version: 3.2.0 + resolution: "regexpp@npm:3.2.0" + checksum: a78dc5c7158ad9ddcfe01aa9144f46e192ddbfa7b263895a70a5c6c73edd9ce85faf7c0430e59ac38839e1734e275b9c3de5c57ee3ab6edc0e0b1bdebefccef8 + languageName: node + linkType: hard + "req-cwd@npm:^2.0.0": version: 2.0.0 resolution: "req-cwd@npm:2.0.0" @@ -12036,6 +13013,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "restore-cursor@npm:3.1.0" + dependencies: + onetime: ^5.1.0 + signal-exit: ^3.0.2 + checksum: f877dd8741796b909f2a82454ec111afb84eb45890eb49ac947d87991379406b3b83ff9673a46012fca0d7844bb989f45cc5b788254cf1a39b6b5a9659de0630 + languageName: node + linkType: hard + "restore-cursor@npm:^4.0.0": version: 4.0.0 resolution: "restore-cursor@npm:4.0.0" @@ -12060,6 +13047,13 @@ __metadata: languageName: node linkType: hard +"revalidator@npm:0.1.x": + version: 0.1.8 + resolution: "revalidator@npm:0.1.8" + checksum: 9ac69162ce8fc86f5fa77f37f3ad634d3797ea70eff4faff13619167ebbf3f3ccf2ec115ccd9c3c860658f8859426022d61d2a1e49183db095ba4f0a016905fe + languageName: node + linkType: hard + "rfdc@npm:^1.3.0": version: 1.3.0 resolution: "rfdc@npm:1.3.0" @@ -12502,6 +13496,46 @@ __metadata: languageName: node linkType: hard +"snarkjs@npm:0.5.0": + version: 0.5.0 + resolution: "snarkjs@npm:0.5.0" + dependencies: + "@iden3/binfileutils": 0.0.11 + bfj: ^7.0.2 + blake2b-wasm: ^2.4.0 + circom_runtime: 0.1.21 + ejs: ^3.1.6 + fastfile: 0.0.20 + ffjavascript: 0.2.56 + js-sha3: ^0.8.0 + logplease: ^1.2.15 + r1csfile: 0.0.41 + bin: + snarkjs: build/cli.cjs + checksum: f0233103548bcd0f75b2ff8998ba02437e5131486d126c4a6a31355cd3558c7ce4311e21d5f24ea9ca198d0556e91e63e2ec6aef8da44014c16b29b6a7086ca2 + languageName: node + linkType: hard + +"snarkjs@npm:^0.7.0, snarkjs@npm:^0.7.3": + version: 0.7.3 + resolution: "snarkjs@npm:0.7.3" + dependencies: + "@iden3/binfileutils": 0.0.11 + bfj: ^7.0.2 + blake2b-wasm: ^2.4.0 + circom_runtime: 0.1.24 + ejs: ^3.1.6 + fastfile: 0.0.20 + ffjavascript: 0.2.63 + js-sha3: ^0.8.0 + logplease: ^1.2.15 + r1csfile: 0.0.47 + bin: + snarkjs: build/cli.cjs + checksum: 6a8818c0f23b1c213b3296b4a253ceb2f26accfd0259274230550dfce010e4c6d1f5ecaaea1d84ff9b9da54c42dc94869f820838af4e33ffd4e09d95e986d9ee + languageName: node + linkType: hard + "socks-proxy-agent@npm:^7.0.0": version: 7.0.0 resolution: "socks-proxy-agent@npm:7.0.0" @@ -12542,15 +13576,14 @@ __metadata: languageName: node linkType: hard -"solidity-coverage@npm:^0.8.5": - version: 0.8.5 - resolution: "solidity-coverage@npm:0.8.5" +"solidity-coverage@npm:~0.8.5": + version: 0.8.11 + resolution: "solidity-coverage@npm:0.8.11" dependencies: "@ethersproject/abi": ^5.0.9 - "@solidity-parser/parser": ^0.16.0 + "@solidity-parser/parser": ^0.18.0 chalk: ^2.4.2 death: ^1.1.0 - detect-port: ^1.3.0 difflib: ^0.2.4 fs-extra: ^8.1.0 ghost-testrpc: ^0.0.2 @@ -12558,7 +13591,7 @@ __metadata: globby: ^10.0.1 jsonschema: ^1.2.4 lodash: ^4.17.15 - mocha: 10.2.0 + mocha: ^10.2.0 node-emoji: ^1.10.0 pify: ^4.0.1 recursive-readdir: ^2.2.2 @@ -12570,7 +13603,7 @@ __metadata: hardhat: ^2.11.0 bin: solidity-coverage: plugins/bin.js - checksum: c9ca4deda9383c1db425117e72677f8908dcb2263ad41cfc1821c96afcfd5e8070146b87cd2c4b0812612fb707896928c07b776347143db838e486b4c938b394 + checksum: 137a3ede8dc5654aaf96d584ad3272fa878ecc46d6cb2bc0a780636b4e46cedccdf583f98fab19d5afef0b6c5e2ac943fb3a932f450b29f6e363591279039687 languageName: node linkType: hard @@ -12607,7 +13640,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:^0.6.1": +"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 59ce8640cf3f3124f64ac289012c2b8bd377c238e316fb323ea22fbfe83da07d81e000071d7242cad7a23cd91c7de98e4df8830ec3f133cb6133a5f6e9f67bc2 @@ -12674,6 +13707,13 @@ __metadata: languageName: node linkType: hard +"stack-trace@npm:0.0.x": + version: 0.0.10 + resolution: "stack-trace@npm:0.0.10" + checksum: 473036ad32f8c00e889613153d6454f9be0536d430eb2358ca51cad6b95cea08a3cc33cc0e34de66b0dad221582b08ed2e61ef8e13f4087ab690f388362d6610 + languageName: node + linkType: hard + "stacktrace-parser@npm:^0.1.10": version: 0.1.10 resolution: "stacktrace-parser@npm:0.1.10" @@ -12697,6 +13737,15 @@ __metadata: languageName: node linkType: hard +"static-eval@npm:2.0.2": + version: 2.0.2 + resolution: "static-eval@npm:2.0.2" + dependencies: + escodegen: ^1.8.1 + checksum: 335a923c5ccb29add404ac23d0a55c0da6cee3071f6f67a7053aeac0dedc6dbfc53ac9269e9c25f403f5b7603a291ef47d7114f99bde241184f7aa3f9286dc32 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -12790,7 +13839,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -12974,7 +14023,7 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.1.1": +"strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 @@ -13285,6 +14334,15 @@ __metadata: languageName: node linkType: hard +"tmp-promise@npm:^3.0.3": + version: 3.0.3 + resolution: "tmp-promise@npm:3.0.3" + dependencies: + tmp: ^0.2.0 + checksum: f854f5307dcee6455927ec3da9398f139897faf715c5c6dcee6d9471ae85136983ea06662eba2edf2533bdcb0fca66d16648e79e14381e30c7fb20be9c1aa62c + languageName: node + linkType: hard + "tmp@npm:0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" @@ -13294,6 +14352,13 @@ __metadata: languageName: node linkType: hard +"tmp@npm:^0.2.0": + version: 0.2.3 + resolution: "tmp@npm:0.2.3" + checksum: 73b5c96b6e52da7e104d9d44afb5d106bb1e16d9fa7d00dbeb9e6522e61b571fbdb165c756c62164be9a3bbe192b9b268c236d370a2a0955c7689cd2ae377b95 + languageName: node + linkType: hard + "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -13364,6 +14429,13 @@ __metadata: languageName: node linkType: hard +"tryer@npm:^1.0.1": + version: 1.0.1 + resolution: "tryer@npm:1.0.1" + checksum: 1cf14d7f67c79613f054b569bfc9a89c7020d331573a812dfcf7437244e8f8e6eb6893b210cbd9cc217f67c1d72617f89793df231e4fe7d53634ed91cf3a89d1 + languageName: node + linkType: hard + "ts-api-utils@npm:^1.0.1": version: 1.0.3 resolution: "ts-api-utils@npm:1.0.3" @@ -13413,7 +14485,7 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:10.9.1, ts-node@npm:^10.9.1": +"ts-node@npm:10.9.1": version: 10.9.1 resolution: "ts-node@npm:10.9.1" dependencies: @@ -13451,6 +14523,44 @@ __metadata: languageName: node linkType: hard +"ts-node@npm:~10.9.1": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2" + dependencies: + "@cspotcode/source-map-support": ^0.8.0 + "@tsconfig/node10": ^1.0.7 + "@tsconfig/node12": ^1.0.7 + "@tsconfig/node14": ^1.0.0 + "@tsconfig/node16": ^1.0.2 + acorn: ^8.4.1 + acorn-walk: ^8.1.1 + arg: ^4.1.0 + create-require: ^1.1.0 + diff: ^4.0.1 + make-error: ^1.1.1 + v8-compile-cache-lib: ^3.0.1 + yn: 3.1.1 + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: fde256c9073969e234526e2cfead42591b9a2aec5222bac154b0de2fa9e4ceb30efcd717ee8bc785a56f3a119bdd5aa27b333d9dbec94ed254bd26f8944c67ac + languageName: node + linkType: hard + "ts-toolbelt@npm:^6.15.5": version: 6.15.5 resolution: "ts-toolbelt@npm:6.15.5" @@ -13557,7 +14667,7 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:^4.0.0, type-detect@npm:^4.0.5": +"type-detect@npm:^4.0.0, type-detect@npm:^4.0.5, type-detect@npm:^4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15 @@ -13599,9 +14709,9 @@ __metadata: languageName: node linkType: hard -"typechain@npm:^8.1.0": - version: 8.3.1 - resolution: "typechain@npm:8.3.1" +"typechain@npm:~8.1.0": + version: 8.1.1 + resolution: "typechain@npm:8.1.1" dependencies: "@types/prettier": ^2.1.1 debug: ^4.3.1 @@ -13617,7 +14727,7 @@ __metadata: typescript: ">=4.3.0" bin: typechain: dist/cli/cli.js - checksum: c1e11ab1452d0c83be0c34a8b900b156b0c6654b95f7e7bb18dd98c0decd6009ffa1316e393f4e8def187af1bea3e931a13503815cc37155c0c945b7ae5b5215 + checksum: 77984239d9728befe5a484c4e1b55c8f194696fc8a78c44754f8e25ca8fd6d0208ddfcd9e71c90c1c35ac0689f5c3053107b54fdc2aab691c980614f6daf209b languageName: node linkType: hard @@ -13704,6 +14814,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:~5.1.6": + version: 5.1.6 + resolution: "typescript@npm:5.1.6" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: b2f2c35096035fe1f5facd1e38922ccb8558996331405eb00a5111cc948b2e733163cc22fab5db46992aba7dd520fff637f2c1df4996ff0e134e77d3249a7350 + languageName: node + linkType: hard + "typescript@patch:typescript@4.9.5#~builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=a1c5e5" @@ -13724,6 +14844,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@~5.1.6#~builtin": + version: 5.1.6 + resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=a1c5e5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 21e88b0a0c0226f9cb9fd25b9626fb05b4c0f3fddac521844a13e1f30beb8f14e90bd409a9ac43c812c5946d714d6e0dee12d5d02dfc1c562c5aacfa1f49b606 + languageName: node + linkType: hard + "typical@npm:^4.0.0": version: 4.0.0 resolution: "typical@npm:4.0.0" @@ -13796,6 +14926,13 @@ __metadata: languageName: node linkType: hard +"underscore@npm:1.12.1": + version: 1.12.1 + resolution: "underscore@npm:1.12.1" + checksum: ec327603aa112b99fe9d74cd9bf3b3b7451465a9d2610ceab269a532e3f191650ab017903be34dc86fe406a11d04d8905a3b04dd4c129493e51bee09a3f3074c + languageName: node + linkType: hard + "undici@npm:5.23.0": version: 5.23.0 resolution: "undici@npm:5.23.0" @@ -14220,6 +15357,31 @@ __metadata: languageName: node linkType: hard +"wasmbuilder@npm:0.0.16": + version: 0.0.16 + resolution: "wasmbuilder@npm:0.0.16" + checksum: a9c8ef4a0a770f71865dc7a75bc22e5204227ff6d70bed90e57f7df5939a013d4177947ea4c2c3fdfe6761b92d975908ea846c463ee635d1c95e1a3c434b7ee1 + languageName: node + linkType: hard + +"wasmcurves@npm:0.2.0": + version: 0.2.0 + resolution: "wasmcurves@npm:0.2.0" + dependencies: + wasmbuilder: 0.0.16 + checksum: 0769ca2d0c2f99619436f86dcab16294d94cc0c8a114b0c5e6ed71df49625a6ad35000011fee57471b7647838eeb75f631e74b200996ac2513e8ca0c47e5b8d0 + languageName: node + linkType: hard + +"wasmcurves@npm:0.2.2": + version: 0.2.2 + resolution: "wasmcurves@npm:0.2.2" + dependencies: + wasmbuilder: 0.0.16 + checksum: 95ca8afe3816862f16d4f8f4cbed4a4fb522be03a41af3cf7d65429bcac9ff4ccbc6e02762dd14933f0e5903cfb93d15cf31fc4acc953f3fcdb31190eba969e8 + languageName: node + linkType: hard + "watchpack@npm:2.4.0": version: 2.4.0 resolution: "watchpack@npm:2.4.0" @@ -14237,6 +15399,20 @@ __metadata: languageName: node linkType: hard +"web-worker@npm:1.2.0": + version: 1.2.0 + resolution: "web-worker@npm:1.2.0" + checksum: 1bb28348ddcf9b2e7c62c5fd02e49a84098795856cd905456de957271bba288e9618941cf69d8960f0a7ae81f5dfb74b427c0634be47ec69e3e955c4ec5213be + languageName: node + linkType: hard + +"web-worker@npm:^1.2.0": + version: 1.3.0 + resolution: "web-worker@npm:1.3.0" + checksum: ed1f869aefd1d81a43d0fbfe7b315a65beb6d7d2486b378c436a7047eed4216be34b2e6afca738b6fa95d016326b765f5f816355db33267dbf43b2b8a1837c0c + languageName: node + linkType: hard + "web3-utils@npm:^1.3.6": version: 1.10.2 resolution: "web3-utils@npm:1.10.2" @@ -14375,6 +15551,29 @@ __metadata: languageName: node linkType: hard +"widest-line@npm:^3.1.0": + version: 3.1.0 + resolution: "widest-line@npm:3.1.0" + dependencies: + string-width: ^4.0.0 + checksum: 03db6c9d0af9329c37d74378ff1d91972b12553c7d72a6f4e8525fe61563fa7adb0b9d6e8d546b7e059688712ea874edd5ded475999abdeedf708de9849310e0 + languageName: node + linkType: hard + +"winston@npm:2.x": + version: 2.4.7 + resolution: "winston@npm:2.4.7" + dependencies: + async: ^2.6.4 + colors: 1.0.x + cycle: 1.0.x + eyes: 0.1.x + isstream: 0.1.x + stack-trace: 0.0.x + checksum: 0843f39e7d5298b0bffbdea51bc0662715b3c49414fd2b245ebf9b9a4aca452683f35f03ae60e93542b7b16e1eeee34eb3c62bb7ec644201587a4067e8d64dda + languageName: node + linkType: hard + "word-wrap@npm:~1.2.3": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" @@ -14758,12 +15957,14 @@ __metadata: languageName: node linkType: hard -"zksync-web3@npm:^0.14.3": - version: 0.14.4 - resolution: "zksync-web3@npm:0.14.4" +"zksync-ethers@npm:^5.0.0": + version: 5.6.0 + resolution: "zksync-ethers@npm:5.6.0" + dependencies: + ethers: ~5.7.0 peerDependencies: - ethers: ^5.7.0 - checksum: f702a3437f48a8d42c4bb35b8dd13671a168aadfc4e23ce723d62959220ccb6bf9c529c60331fe5b91afaa622147c6a37490551474fe3e35c06ac476524b5160 + ethers: ~5.7.0 + checksum: 347ec845f18e9463350827cee5725da4d6ce8031f6c4c0b5e54c1bbac2a8c1d10a5bae3d9e61ec3ad987c8bad9921eab24cacd0415a9dabbee3ad8a71237823a languageName: node linkType: hard