From d20877af26e06e719589a662a967d11e49e3cfda Mon Sep 17 00:00:00 2001 From: yuetloo Date: Tue, 2 Apr 2024 23:30:29 -0400 Subject: [PATCH 01/17] break the tally script into multiple smaller scripts --- contracts/tasks/index.ts | 4 + contracts/tasks/runners/genProofs.ts | 185 ++++++++++++++++++ contracts/tasks/runners/proveOnChain.ts | 99 ++++++++++ .../tasks/runners/publishTallyResults.ts | 163 +++++++++++++++ contracts/tasks/runners/resetTally.ts | 51 +++++ contracts/utils/contracts.ts | 29 ++- 6 files changed, 530 insertions(+), 1 deletion(-) create mode 100644 contracts/tasks/runners/genProofs.ts create mode 100644 contracts/tasks/runners/proveOnChain.ts create mode 100644 contracts/tasks/runners/publishTallyResults.ts create mode 100644 contracts/tasks/runners/resetTally.ts diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index 5bcc7e4fc..934d30612 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -23,3 +23,7 @@ import './runners/addRecipients' import './runners/findStorageSlot' import './runners/verifyTallyFile' import './runners/verifyAll' +import './runners/genProofs' +import './runners/proveOnChain' +import './runners/publishTallyResults' +import './runners/resetTally' diff --git a/contracts/tasks/runners/genProofs.ts b/contracts/tasks/runners/genProofs.ts new file mode 100644 index 000000000..55c9d5521 --- /dev/null +++ b/contracts/tasks/runners/genProofs.ts @@ -0,0 +1,185 @@ +/** + * Script for generating MACI proofs + * + * Pass --maci-tx-hash if this is the first time running the script and you + * want to get MACI logs starting from the block as recorded in the MACI creation + * transaction hash + * + * Pass --maci-state-file if you have previously ran the script and have + * the maci-state file (maci-state.json) + * + * Make sure to set the following environment variables in the .env file + * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC + * - coordinator's wallet private key to interact with contracts + * 2) COORDINATOR_MACISK - coordinator's MACI private key to decrypt messages + * + * Sample usage: + * + * yarn hardhat tally --clrfund --proof-dir \ + * --maci-tx-hash --network + * + */ +import { getNumber, NonceManager } from 'ethers' +import { task, types } from 'hardhat/config' + +import { + DEFAULT_GET_LOG_BATCH_SIZE, + DEFAULT_SR_QUEUE_OPS, +} from '../../utils/constants' +import { + getGenProofArgs, + genProofs, + genLocalState, + mergeMaciSubtrees, +} from '../../utils/maci' +import { getMaciStateFilePath } from '../../utils/misc' +import { EContracts } from '../../utils/types' +import { Subtask } from '../helpers/Subtask' +import { getCurrentFundingRoundContract } from '../../utils/contracts' + +task('gen-proofs', 'Generate MACI proofs offchain') + .addParam('clrfund', 'FundingRound contract address') + .addParam('proofDir', 'The proof output directory') + .addOptionalParam('maciTxHash', 'MACI creation transaction hash') + .addOptionalParam( + 'maciStartBlock', + 'MACI creation block', + undefined, + types.int + ) + .addOptionalParam('maciStateFile', 'MACI state file') + .addFlag('manageNonce', 'Whether to manually manage transaction nonce') + .addOptionalParam('rapidsnark', 'The rapidsnark prover path') + .addOptionalParam( + 'blocksPerBatch', + 'The number of blocks per batch of logs to fetch on-chain', + DEFAULT_GET_LOG_BATCH_SIZE, + types.int + ) + .addOptionalParam( + 'numQueueOps', + 'The number of operations for MACI tree merging', + getNumber(DEFAULT_SR_QUEUE_OPS), + types.int + ) + .addOptionalParam('sleep', 'Number of seconds to sleep between log fetch') + .addOptionalParam( + 'quiet', + 'Whether to disable verbose logging', + false, + types.boolean + ) + .setAction( + async ( + { + clrfund, + maciStartBlock, + maciTxHash, + quiet, + maciStateFile, + proofDir, + blocksPerBatch, + rapidsnark, + numQueueOps, + sleep, + manageNonce, + }, + hre + ) => { + console.log('Verbose logging enabled:', !quiet) + + const { ethers, network } = hre + const subtask = Subtask.getInstance(hre) + subtask.setHre(hre) + + if (!maciStateFile && !maciTxHash && maciStartBlock == undefined) { + throw new Error('Please provide --maci-start-block or --maci-tx-hash') + } + + const [coordinatorSigner] = await ethers.getSigners() + if (!coordinatorSigner) { + throw new Error('Env. variable WALLET_PRIVATE_KEY not set') + } + const coordinator = manageNonce + ? new NonceManager(coordinatorSigner) + : coordinatorSigner + console.log('Coordinator address: ', await coordinator.getAddress()) + + const coordinatorMacisk = process.env.COORDINATOR_MACISK + if (!coordinatorMacisk) { + throw new Error('Env. variable COORDINATOR_MACISK not set') + } + + const circuit = subtask.getConfigField( + EContracts.VkRegistry, + 'circuit' + ) + const circuitDirectory = subtask.getConfigField( + EContracts.VkRegistry, + 'paramsDirectory' + ) + + await subtask.logStart() + + const fundingRoundContract = await getCurrentFundingRoundContract( + clrfund, + coordinator, + ethers + ) + console.log('Funding round contract', fundingRoundContract.target) + + const pollId = await fundingRoundContract.pollId() + console.log('PollId', pollId) + + const maciAddress = await fundingRoundContract.maci() + + const providerUrl = (network.config as any).url + + await mergeMaciSubtrees({ + maciAddress, + pollId, + numQueueOps, + signer: coordinator, + quiet, + }) + + const maciStateFilePath = maciStateFile + ? maciStateFile + : getMaciStateFilePath(proofDir) + + if (!maciStateFile) { + await genLocalState({ + quiet, + outputPath: maciStateFilePath, + pollId, + maciContractAddress: maciAddress, + coordinatorPrivateKey: coordinatorMacisk, + ethereumProvider: providerUrl, + transactionHash: maciTxHash, + startBlock: maciStartBlock, + blockPerBatch: blocksPerBatch, + signer: coordinator, + sleep, + }) + } + + const genProofArgs = getGenProofArgs({ + maciAddress, + pollId, + coordinatorMacisk, + rapidsnark, + circuitType: circuit, + circuitDirectory, + outputDir: proofDir, + blocksPerBatch: getNumber(blocksPerBatch), + maciTxHash, + maciStateFile: maciStateFilePath, + signer: coordinator, + quiet, + }) + await genProofs(genProofArgs) + + const success = true + await subtask.finish(success) + } + ) diff --git a/contracts/tasks/runners/proveOnChain.ts b/contracts/tasks/runners/proveOnChain.ts new file mode 100644 index 000000000..b1eb499de --- /dev/null +++ b/contracts/tasks/runners/proveOnChain.ts @@ -0,0 +1,99 @@ +/** + * Prove on chain the MACI proofs generated using genProofs + * + * Make sure to set the following environment variables in the .env file + * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC + * - coordinator's wallet private key to interact with contracts + * + * Sample usage: + * + * yarn hardhat prove-on-chain --clrfund --proof-dir --network + * + */ +import { BaseContract, NonceManager } from 'ethers' +import { task, types } from 'hardhat/config' + +import { proveOnChain } from '../../utils/maci' +import { Tally } from '../../typechain-types' +import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' +import { EContracts } from '../../utils/types' +import { Subtask } from '../helpers/Subtask' +import { getCurrentFundingRoundContract } from '../../utils/contracts' + +/** + * Get the message processor contract address from the tally contract + * @param tallyAddress Tally contract address + * @param ethers Hardhat ethers helper + * @returns Message processor contract address + */ +async function getMessageProcessorAddress( + tallyAddress: string, + ethers: HardhatEthersHelpers +): Promise { + const tallyContract = (await ethers.getContractAt( + EContracts.Tally, + tallyAddress + )) as BaseContract as Tally + + const messageProcessorAddress = await tallyContract.messageProcessor() + return messageProcessorAddress +} + +task('prove-on-chain', 'Prove on chain with the MACI proofs') + .addParam('clrfund', 'ClrFund contract address') + .addParam('proofDir', 'The proof output directory') + .addFlag('manageNonce', 'Whether to manually manage transaction nonce') + .addOptionalParam( + 'quiet', + 'Whether to disable verbose logging', + false, + types.boolean + ) + .setAction(async ({ clrfund, quiet, manageNonce, proofDir }, hre) => { + console.log('Verbose logging enabled:', !quiet) + + const { ethers } = hre + const subtask = Subtask.getInstance(hre) + subtask.setHre(hre) + + const [coordinatorSigner] = await ethers.getSigners() + if (!coordinatorSigner) { + throw new Error('Env. variable WALLET_PRIVATE_KEY not set') + } + const coordinator = manageNonce + ? new NonceManager(coordinatorSigner) + : coordinatorSigner + console.log('Coordinator address: ', await coordinator.getAddress()) + + await subtask.logStart() + + const fundingRoundContract = await getCurrentFundingRoundContract( + clrfund, + coordinator, + ethers + ) + console.log('Funding round contract', fundingRoundContract.target) + + const pollId = await fundingRoundContract.pollId() + const maciAddress = await fundingRoundContract.maci() + const tallyAddress = await fundingRoundContract.tally() + const messageProcessorAddress = await getMessageProcessorAddress( + tallyAddress, + ethers + ) + + // proveOnChain if not already processed + await proveOnChain({ + pollId, + proofDir, + subsidyEnabled: false, + maciAddress, + messageProcessorAddress, + tallyAddress, + signer: coordinator, + quiet, + }) + + const success = true + await subtask.finish(success) + }) diff --git a/contracts/tasks/runners/publishTallyResults.ts b/contracts/tasks/runners/publishTallyResults.ts new file mode 100644 index 000000000..46f535f8b --- /dev/null +++ b/contracts/tasks/runners/publishTallyResults.ts @@ -0,0 +1,163 @@ +/** + * Script for tallying votes which involves fetching MACI logs, generating proofs, + * and proving on chain + * + * Make sure to set the following environment variables in the .env file + * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC + * - coordinator's wallet private key to interact with contracts + * + * Sample usage: + * + * yarn hardhat publish-tally-results --clrfund + * --proof-dir --network + * + */ +import { BaseContract, getNumber, NonceManager } from 'ethers' +import { task, types } from 'hardhat/config' + +import { getIpfsHash } from '../../utils/ipfs' +import { JSONFile } from '../../utils/JSONFile' +import { addTallyResultsBatch, TallyData, verify } from '../../utils/maci' +import { FundingRound, Poll } from '../../typechain-types' +import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' +import { EContracts } from '../../utils/types' +import { Subtask } from '../helpers/Subtask' +import { getCurrentFundingRoundContract } from '../../utils/contracts' +import { getTalyFilePath } from '../../utils/misc' + +/** + * Publish the tally IPFS hash on chain if it's not already published + * @param fundingRoundContract Funding round contract + * @param tallyData Tally data + */ +async function publishTallyHash( + fundingRoundContract: FundingRound, + tallyData: TallyData +) { + const tallyHash = await getIpfsHash(tallyData) + console.log(`Tally hash is ${tallyHash}`) + + const tallyHashOnChain = await fundingRoundContract.tallyHash() + if (tallyHashOnChain !== tallyHash) { + const tx = await fundingRoundContract.publishTallyHash(tallyHash) + const receipt = await tx.wait() + if (receipt?.status !== 1) { + throw new Error('Failed to publish tally hash on chain') + } + + console.log('Published tally hash on chain') + } +} +/** + * Submit tally data to funding round contract + * @param fundingRoundContract Funding round contract + * @param batchSize Number of tally results per batch + * @param tallyData Tally file content + */ +async function submitTallyResults( + fundingRoundContract: FundingRound, + recipientTreeDepth: number, + tallyData: TallyData, + batchSize: number +) { + const startIndex = await fundingRoundContract.totalTallyResults() + const total = tallyData.results.tally.length + console.log('Uploading tally results in batches of', batchSize) + const addTallyGas = await addTallyResultsBatch( + fundingRoundContract, + recipientTreeDepth, + tallyData, + getNumber(batchSize), + getNumber(startIndex), + (processed: number) => { + console.log(`Processed ${processed} / ${total}`) + } + ) + console.log('Tally results uploaded. Gas used:', addTallyGas.toString()) +} + +/** + * Get the recipient tree depth (aka vote option tree depth) + * @param fundingRoundContract Funding round conract + * @param ethers Hardhat Ethers Helper + * @returns Recipient tree depth + */ +async function getRecipientTreeDepth( + fundingRoundContract: FundingRound, + ethers: HardhatEthersHelpers +): Promise { + const pollAddress = await fundingRoundContract.poll() + const pollContract = await ethers.getContractAt(EContracts.Poll, pollAddress) + const treeDepths = await (pollContract as BaseContract as Poll).treeDepths() + const voteOptionTreeDepth = treeDepths.voteOptionTreeDepth + return getNumber(voteOptionTreeDepth) +} + +task('publish-tally-results', 'Publish tally results') + .addParam('clrfund', 'ClrFund contract address') + .addParam('proofDir', 'The proof output directory') + .addOptionalParam( + 'batchSize', + 'The batch size to upload tally result on-chain', + 10, + types.int + ) + .addFlag('manageNonce', 'Whether to manually manage transaction nonce') + .addFlag('quiet', 'Whether to log on the console') + .setAction( + async ({ clrfund, proofDir, batchSize, manageNonce, quiet }, hre) => { + const { ethers } = hre + const subtask = Subtask.getInstance(hre) + subtask.setHre(hre) + + const [signer] = await ethers.getSigners() + if (!signer) { + throw new Error('Env. variable WALLET_PRIVATE_KEY not set') + } + const coordinator = manageNonce ? new NonceManager(signer) : signer + console.log('Coordinator address: ', await coordinator.getAddress()) + + await subtask.logStart() + + const fundingRoundContract = await getCurrentFundingRoundContract( + clrfund, + coordinator, + ethers + ) + console.log('Funding round contract', fundingRoundContract.target) + + const recipientTreeDepth = await getRecipientTreeDepth( + fundingRoundContract, + ethers + ) + + const tallyFile = getTalyFilePath(proofDir) + const tallyData = JSONFile.read(tallyFile) + const tallyAddress = await fundingRoundContract.tally() + + await verify({ + pollId: BigInt(tallyData.pollId), + subsidyEnabled: false, + tallyData, + maciAddress: tallyData.maci, + tallyAddress, + signer: coordinator, + quiet, + }) + + // Publish tally hash if it is not already published + await publishTallyHash(fundingRoundContract, tallyData) + + // Submit tally results to the funding round contract + // This function can be re-run from where it left off + await submitTallyResults( + fundingRoundContract, + recipientTreeDepth, + tallyData, + batchSize + ) + + const success = true + await subtask.finish(success) + } + ) diff --git a/contracts/tasks/runners/resetTally.ts b/contracts/tasks/runners/resetTally.ts new file mode 100644 index 000000000..e3283434d --- /dev/null +++ b/contracts/tasks/runners/resetTally.ts @@ -0,0 +1,51 @@ +/** + * WARNING: + * This script will create a new instance of the tally contract in the funding round contract + * + * Usage: + * hardhat resetTally --funding-round --network + * + * Note: + * 1) This script needs to be run by the coordinator + * 2) It can only be run if the funding round hasn't been finalized + */ +import { task } from 'hardhat/config' +import { getCurrentFundingRoundContract } from '../../utils/contracts' +import { Subtask } from '../helpers/Subtask' + +task('reset-tally', 'Reset the tally contract') + .addParam('clrfund', 'The clrfund contract address') + .setAction(async ({ clrfund }, hre) => { + const subtask = Subtask.getInstance(hre) + subtask.setHre(hre) + + let success = false + try { + const [coordinator] = await hre.ethers.getSigners() + console.log('Coordinator address: ', await coordinator.getAddress()) + + const fundingRoundContract = await getCurrentFundingRoundContract( + clrfund, + coordinator, + hre.ethers + ) + + const tx = await fundingRoundContract.resetTally() + const receipt = await tx.wait() + if (receipt?.status !== 1) { + throw new Error('Failed to reset the tally contract') + } + + subtask.logTransaction(tx) + success = true + } catch (err) { + console.error( + '\n=========================================================\nERROR:', + err, + '\n' + ) + success = false + } + + await subtask.finish(success) + }) diff --git a/contracts/utils/contracts.ts b/contracts/utils/contracts.ts index e60d11a76..45250e3fb 100644 --- a/contracts/utils/contracts.ts +++ b/contracts/utils/contracts.ts @@ -2,6 +2,7 @@ import { BaseContract, ContractTransactionResponse, TransactionResponse, + Signer, } from 'ethers' import { getEventArg } from '@clrfund/common' import { EContracts } from './types' @@ -9,7 +10,7 @@ import { DeployContractOptions, HardhatEthersHelpers, } from '@nomicfoundation/hardhat-ethers/types' -import { VkRegistry } from '../typechain-types' +import { VkRegistry, FundingRound } from '../typechain-types' import { MaciParameters } from './maciParameters' import { IVerifyingKeyStruct } from 'maci-contracts' @@ -89,4 +90,30 @@ export async function getTxFee( return receipt ? BigInt(receipt.gasUsed) * BigInt(receipt.gasPrice) : 0n } +/** + * Return the current funding round contract handle + * @param clrfund ClrFund contract address + * @param signer Signer who will interact with the funding round contract + * @param hre Hardhat runtime environment + */ +export async function getCurrentFundingRoundContract( + clrfund: string, + signer: Signer, + ethers: HardhatEthersHelpers +): Promise { + const clrfundContract = await ethers.getContractAt( + EContracts.ClrFund, + clrfund, + signer + ) + + const fundingRound = await clrfundContract.getCurrentRound() + const fundingRoundContract = await ethers.getContractAt( + EContracts.FundingRound, + fundingRound, + signer + ) + + return fundingRoundContract as BaseContract as FundingRound +} export { getEventArg } From 4b513e87de8590d26bf8a28cbd718cd37d4173cc Mon Sep 17 00:00:00 2001 From: yuetloo Date: Wed, 3 Apr 2024 00:34:31 -0400 Subject: [PATCH 02/17] adjust tally scripts for testing purposes --- contracts/sh/runScriptTests.sh | 10 ++++++---- contracts/tasks/runners/genProofs.ts | 10 +++++++--- contracts/tasks/runners/proveOnChain.ts | 10 +++++++--- contracts/tasks/runners/publishTallyResults.ts | 10 +++++++--- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/contracts/sh/runScriptTests.sh b/contracts/sh/runScriptTests.sh index f03a47bf9..b17de7b2f 100755 --- a/contracts/sh/runScriptTests.sh +++ b/contracts/sh/runScriptTests.sh @@ -33,13 +33,15 @@ yarn hardhat contribute --network ${HARDHAT_NETWORK} yarn hardhat time-travel --seconds ${ROUND_DURATION} --network ${HARDHAT_NETWORK} -# run the tally script +# tally the votes NODE_OPTIONS="--max-old-space-size=4096" -yarn hardhat tally \ +yarn hardhat gen-proofs \ --rapidsnark ${RAPID_SNARK} \ - --batch-size 8 \ - --output-dir ${OUTPUT_DIR} \ + --proof-dir ${OUTPUT_DIR} \ + --maci-start-block 0 \ --network "${HARDHAT_NETWORK}" +yarn hardhat prove-on-chain --proof-dir ${OUTPUT_DIR} --network "${HARDHAT_NETWORK}" +yarn hardhat publish-tally-results --proof-dir ${OUTPUT_DIR} --network "${HARDHAT_NETWORK}" # finalize the round yarn hardhat finalize --tally-file ${TALLY_FILE} --network ${HARDHAT_NETWORK} diff --git a/contracts/tasks/runners/genProofs.ts b/contracts/tasks/runners/genProofs.ts index 55c9d5521..c9da580f3 100644 --- a/contracts/tasks/runners/genProofs.ts +++ b/contracts/tasks/runners/genProofs.ts @@ -15,7 +15,7 @@ * * Sample usage: * - * yarn hardhat tally --clrfund --proof-dir \ + * yarn hardhat gen-proofs --clrfund --proof-dir \ * --maci-tx-hash --network * */ @@ -36,9 +36,10 @@ import { getMaciStateFilePath } from '../../utils/misc' import { EContracts } from '../../utils/types' import { Subtask } from '../helpers/Subtask' import { getCurrentFundingRoundContract } from '../../utils/contracts' +import { ContractStorage } from '../helpers/ContractStorage' task('gen-proofs', 'Generate MACI proofs offchain') - .addParam('clrfund', 'FundingRound contract address') + .addOptionalParam('clrfund', 'FundingRound contract address') .addParam('proofDir', 'The proof output directory') .addOptionalParam('maciTxHash', 'MACI creation transaction hash') .addOptionalParam( @@ -89,6 +90,7 @@ task('gen-proofs', 'Generate MACI proofs offchain') console.log('Verbose logging enabled:', !quiet) const { ethers, network } = hre + const storage = ContractStorage.getInstance() const subtask = Subtask.getInstance(hre) subtask.setHre(hre) @@ -121,8 +123,10 @@ task('gen-proofs', 'Generate MACI proofs offchain') await subtask.logStart() + const clrfundContractAddress = + clrfund ?? storage.mustGetAddress(EContracts.ClrFund, network.name) const fundingRoundContract = await getCurrentFundingRoundContract( - clrfund, + clrfundContractAddress, coordinator, ethers ) diff --git a/contracts/tasks/runners/proveOnChain.ts b/contracts/tasks/runners/proveOnChain.ts index b1eb499de..af777ed12 100644 --- a/contracts/tasks/runners/proveOnChain.ts +++ b/contracts/tasks/runners/proveOnChain.ts @@ -19,6 +19,7 @@ import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' import { EContracts } from '../../utils/types' import { Subtask } from '../helpers/Subtask' import { getCurrentFundingRoundContract } from '../../utils/contracts' +import { ContractStorage } from '../helpers/ContractStorage' /** * Get the message processor contract address from the tally contract @@ -40,7 +41,7 @@ async function getMessageProcessorAddress( } task('prove-on-chain', 'Prove on chain with the MACI proofs') - .addParam('clrfund', 'ClrFund contract address') + .addOptionalParam('clrfund', 'ClrFund contract address') .addParam('proofDir', 'The proof output directory') .addFlag('manageNonce', 'Whether to manually manage transaction nonce') .addOptionalParam( @@ -52,7 +53,8 @@ task('prove-on-chain', 'Prove on chain with the MACI proofs') .setAction(async ({ clrfund, quiet, manageNonce, proofDir }, hre) => { console.log('Verbose logging enabled:', !quiet) - const { ethers } = hre + const { ethers, network } = hre + const storage = ContractStorage.getInstance() const subtask = Subtask.getInstance(hre) subtask.setHre(hre) @@ -67,8 +69,10 @@ task('prove-on-chain', 'Prove on chain with the MACI proofs') await subtask.logStart() + const clrfundContractAddress = + clrfund ?? storage.mustGetAddress(EContracts.ClrFund, network.name) const fundingRoundContract = await getCurrentFundingRoundContract( - clrfund, + clrfundContractAddress, coordinator, ethers ) diff --git a/contracts/tasks/runners/publishTallyResults.ts b/contracts/tasks/runners/publishTallyResults.ts index 46f535f8b..d2fef14f1 100644 --- a/contracts/tasks/runners/publishTallyResults.ts +++ b/contracts/tasks/runners/publishTallyResults.ts @@ -24,6 +24,7 @@ import { EContracts } from '../../utils/types' import { Subtask } from '../helpers/Subtask' import { getCurrentFundingRoundContract } from '../../utils/contracts' import { getTalyFilePath } from '../../utils/misc' +import { ContractStorage } from '../helpers/ContractStorage' /** * Publish the tally IPFS hash on chain if it's not already published @@ -94,7 +95,7 @@ async function getRecipientTreeDepth( } task('publish-tally-results', 'Publish tally results') - .addParam('clrfund', 'ClrFund contract address') + .addOptionalParam('clrfund', 'ClrFund contract address') .addParam('proofDir', 'The proof output directory') .addOptionalParam( 'batchSize', @@ -106,7 +107,8 @@ task('publish-tally-results', 'Publish tally results') .addFlag('quiet', 'Whether to log on the console') .setAction( async ({ clrfund, proofDir, batchSize, manageNonce, quiet }, hre) => { - const { ethers } = hre + const { ethers, network } = hre + const storage = ContractStorage.getInstance() const subtask = Subtask.getInstance(hre) subtask.setHre(hre) @@ -119,8 +121,10 @@ task('publish-tally-results', 'Publish tally results') await subtask.logStart() + const clrfundContractAddress = + clrfund ?? storage.mustGetAddress(EContracts.ClrFund, network.name) const fundingRoundContract = await getCurrentFundingRoundContract( - clrfund, + clrfundContractAddress, coordinator, ethers ) From 2a8fa9097f2043c3063d6393c136ae4fbf163f9d Mon Sep 17 00:00:00 2001 From: yuetloo Date: Wed, 3 Apr 2024 00:52:18 -0400 Subject: [PATCH 03/17] add missing start --- contracts/tasks/runners/resetTally.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/tasks/runners/resetTally.ts b/contracts/tasks/runners/resetTally.ts index e3283434d..adc9ebb4e 100644 --- a/contracts/tasks/runners/resetTally.ts +++ b/contracts/tasks/runners/resetTally.ts @@ -21,6 +21,8 @@ task('reset-tally', 'Reset the tally contract') let success = false try { + await subtask.logStart() + const [coordinator] = await hre.ethers.getSigners() console.log('Coordinator address: ', await coordinator.getAddress()) From 83023b7c5a0a1429ed4c076fd92cc825c2219c49 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Wed, 3 Apr 2024 02:01:15 -0400 Subject: [PATCH 04/17] update documentation with new commands for votes tallying --- docs/tally-verify.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/tally-verify.md b/docs/tally-verify.md index b2edd2a50..a2d2d66b0 100644 --- a/docs/tally-verify.md +++ b/docs/tally-verify.md @@ -20,22 +20,34 @@ WALLET_MNEMONIC= WALLET_PRIVATE_KEY ``` -Decrypt messages and tally the votes: +Decrypt messages, tally the votes and generate proofs: ``` -yarn hardhat tally --rapidsnark {RAPID_SNARK} --output-dir {OUTPUT_DIR} --network {network} +yarn hardhat gen-proofs --clrfund {CLRFUND_CONTRACT_ADDRESS} --maci-tx-hash {MACI_CREATION_TRANSACTION_HASH} --proof-dir {OUTPUT_DIR} --rapidsnark {RAPID_SNARK} --network {network} ``` You only need to provide `--rapidsnark` if you are running the `tally` command on an intel chip. +If `gen-proofs` failed, you can rerun the command with the same parameters. If the maci-state.json file has been created, you can skip fetching MACI logs by providing the MACI state file as follow: -If there's error and the tally task was stopped prematurely, it can be resumed by passing 2 additional parameters, '--tally-file' and/or '--maci-state-file', if the files were generated. +``` +yarn hardhat gen-proofs --clrfund {CLRFUND_CONTRACT_ADDRESS} --maci-state-file {MACI_STATE_FILE_PATH} --proof-dir {OUTPUT_DIR} --rapidsnark {RAPID_SNARK} --network {network} +``` + + +** Make a backup of the {OUTPUT_DIR} before continuing to the next step ** + +Upload the proofs on chain: ``` -# for rerun -yarn hardhat tally --maci-state-file {maci-state.json} --tally-file {tally.json} --output-dir {OUTPUT_DIR} --network {network} +yarn hardhat prove-on-chain --clrfund {CLRFUND_CONTRACT_ADDRESS} --proof-dir {OUTPUT_DIR} --network {network} +yarn hardhat publish-tally-results --clrfund {CLRFUND_CONTRACT_ADDRESS} --proof-dir {OUTPUT_DIR} --network localhost ``` -Result will be saved to `tally.json` file, which must then be published via IPFS. +If there's error running `prove-on-chain` or `publish-tally-resuls`, simply rerun the commands with the same parameters. + + + +Result will be saved to `{OUTPUT_DIR}/tally.json` file, which must then be published via IPFS. **Using [command line](https://docs.ipfs.tech/reference/kubo/cli/#ipfs)** From 214e3958ebbebdc1e274d1f59c8ebae6fbf6e389 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Wed, 3 Apr 2024 02:35:08 -0400 Subject: [PATCH 05/17] upload smaller batches to avoid socket error on linux --- contracts/tasks/runners/publishTallyResults.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/tasks/runners/publishTallyResults.ts b/contracts/tasks/runners/publishTallyResults.ts index d2fef14f1..91699fa94 100644 --- a/contracts/tasks/runners/publishTallyResults.ts +++ b/contracts/tasks/runners/publishTallyResults.ts @@ -100,7 +100,7 @@ task('publish-tally-results', 'Publish tally results') .addOptionalParam( 'batchSize', 'The batch size to upload tally result on-chain', - 10, + 8, types.int ) .addFlag('manageNonce', 'Whether to manually manage transaction nonce') From 8e3a1a25d179f9d9dae220dd1dbab0010d2b9e5a Mon Sep 17 00:00:00 2001 From: yuetloo Date: Wed, 3 Apr 2024 09:56:24 -0400 Subject: [PATCH 06/17] add --round-address optino --- contracts/tasks/runners/claim.ts | 86 +++++++++++++++++--------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/contracts/tasks/runners/claim.ts b/contracts/tasks/runners/claim.ts index acce6d3a2..bf90576e2 100644 --- a/contracts/tasks/runners/claim.ts +++ b/contracts/tasks/runners/claim.ts @@ -18,6 +18,7 @@ import { EContracts } from '../../utils/types' import { ContractStorage } from '../helpers/ContractStorage' task('claim', 'Claim funnds for test recipients') + .addOptionalParam('roundAddress', 'Funding round contract address') .addParam( 'recipient', 'The recipient index in the tally file', @@ -25,52 +26,55 @@ task('claim', 'Claim funnds for test recipients') types.int ) .addParam('tallyFile', 'The tally file') - .setAction(async ({ tallyFile, recipient }, { ethers, network }) => { - if (!isPathExist(tallyFile)) { - throw new Error(`Path ${tallyFile} does not exist`) - } + .setAction( + async ({ tallyFile, recipient, roundAddress }, { ethers, network }) => { + if (!isPathExist(tallyFile)) { + throw new Error(`Path ${tallyFile} does not exist`) + } - if (recipient <= 0) { - throw new Error('Recipient must be greater than 0') - } + if (recipient <= 0) { + throw new Error('Recipient must be greater than 0') + } - const storage = ContractStorage.getInstance() - const fundingRound = storage.mustGetAddress( - EContracts.FundingRound, - network.name - ) + const storage = ContractStorage.getInstance() + const fundingRound = + roundAddress ?? + storage.mustGetAddress(EContracts.FundingRound, network.name) - const tally = JSONFile.read(tallyFile) + const tally = JSONFile.read(tallyFile) - const fundingRoundContract = await ethers.getContractAt( - EContracts.FundingRound, - fundingRound - ) + const fundingRoundContract = await ethers.getContractAt( + EContracts.FundingRound, + fundingRound + ) - const recipientStatus = await fundingRoundContract.recipients(recipient) - if (recipientStatus.fundsClaimed) { - throw new Error(`Recipient already claimed funds`) - } + const recipientStatus = await fundingRoundContract.recipients(recipient) + if (recipientStatus.fundsClaimed) { + throw new Error(`Recipient already claimed funds`) + } - const pollAddress = await fundingRoundContract.poll() - console.log('pollAddress', pollAddress) + const pollAddress = await fundingRoundContract.poll() + console.log('pollAddress', pollAddress) - const poll = await ethers.getContractAt(EContracts.Poll, pollAddress) - const treeDepths = await poll.treeDepths() - const recipientTreeDepth = getNumber(treeDepths.voteOptionTreeDepth) + const poll = await ethers.getContractAt(EContracts.Poll, pollAddress) + const treeDepths = await poll.treeDepths() + const recipientTreeDepth = getNumber(treeDepths.voteOptionTreeDepth) - // Claim funds - const recipientClaimData = getRecipientClaimData( - recipient, - recipientTreeDepth, - tally - ) - const claimTx = await fundingRoundContract.claimFunds(...recipientClaimData) - const claimedAmount = await getEventArg( - claimTx, - fundingRoundContract, - 'FundsClaimed', - '_amount' - ) - console.log(`Recipient ${recipient} claimed ${claimedAmount} tokens.`) - }) + // Claim funds + const recipientClaimData = getRecipientClaimData( + recipient, + recipientTreeDepth, + tally + ) + const claimTx = await fundingRoundContract.claimFunds( + ...recipientClaimData + ) + const claimedAmount = await getEventArg( + claimTx, + fundingRoundContract, + 'FundsClaimed', + '_amount' + ) + console.log(`Recipient ${recipient} claimed ${claimedAmount} tokens.`) + } + ) From 7b705835da7c8e25b13cd7b3aa80e7de192d8ca5 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Wed, 3 Apr 2024 20:18:57 -0400 Subject: [PATCH 07/17] use consistent network name to get etherscan api key --- contracts/hardhat.config.ts | 2 +- contracts/utils/providers/EtherscanProvider.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index e9fcb6931..0a30c06b2 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -59,7 +59,7 @@ export default { 'https://sepolia-rollup.arbitrum.io/rpc', accounts, }, - optimism: { + optimisticEthereum: { url: process.env.JSONRPC_HTTP_URL || 'https://mainnet.optimism.io', accounts, }, diff --git a/contracts/utils/providers/EtherscanProvider.ts b/contracts/utils/providers/EtherscanProvider.ts index 5a35ac557..efa0cadb0 100644 --- a/contracts/utils/providers/EtherscanProvider.ts +++ b/contracts/utils/providers/EtherscanProvider.ts @@ -6,7 +6,7 @@ const EtherscanApiUrl: Record = { arbitrum: 'https://api.arbiscan.io', 'arbitrum-goerli': 'https://api-goerli.arbiscan.io', 'arbitrum-sepolia': 'https://api-sepolia.arbiscan.io', - optimism: 'https://api-optimistic.etherscan.io', + optimisticEthereum: 'https://api-optimistic.etherscan.io', 'optimism-sepolia': 'https://api-sepolia-optimistic.etherscan.io', } From e7df7a8a0311f5853b567b13551f97945e7f246c Mon Sep 17 00:00:00 2001 From: yuetloo Date: Wed, 3 Apr 2024 20:56:38 -0400 Subject: [PATCH 08/17] map hardhat network name to etherscan name to retrieve the etherscan api key --- contracts/hardhat.config.ts | 2 +- contracts/tasks/runners/exportRound.ts | 12 +++++++++++- contracts/utils/providers/EtherscanProvider.ts | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 0a30c06b2..e9fcb6931 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -59,7 +59,7 @@ export default { 'https://sepolia-rollup.arbitrum.io/rpc', accounts, }, - optimisticEthereum: { + optimism: { url: process.env.JSONRPC_HTTP_URL || 'https://mainnet.optimism.io', accounts, }, diff --git a/contracts/tasks/runners/exportRound.ts b/contracts/tasks/runners/exportRound.ts index 4eadf9818..da14269c1 100644 --- a/contracts/tasks/runners/exportRound.ts +++ b/contracts/tasks/runners/exportRound.ts @@ -29,6 +29,11 @@ type RoundListEntry = { isFinalized: boolean } +// Map hardhat network name to the etherscan api network name in the hardhat.config +const ETHERSCAN_NETWORKS: Record = { + optimism: 'optimisticEthereum', +} + const toUndefined = () => undefined const toString = (val: bigint) => BigInt(val).toString() const toZero = () => BigInt(0) @@ -41,13 +46,18 @@ function roundListFileName(directory: string): string { return path.join(directory, 'rounds.json') } +function toEtherscanNetworkName(network: string): string { + return ETHERSCAN_NETWORKS[network] ?? network +} + function getEtherscanApiKey(config: any, network: string): string { let etherscanApiKey = '' if (config.etherscan?.apiKey) { if (typeof config.etherscan.apiKey === 'string') { etherscanApiKey = config.etherscan.apiKey } else { - etherscanApiKey = config.etherscan.apiKey[network] + const etherscanNetwork = toEtherscanNetworkName(network) + etherscanApiKey = config.etherscan.apiKey[etherscanNetwork] } } diff --git a/contracts/utils/providers/EtherscanProvider.ts b/contracts/utils/providers/EtherscanProvider.ts index efa0cadb0..5a35ac557 100644 --- a/contracts/utils/providers/EtherscanProvider.ts +++ b/contracts/utils/providers/EtherscanProvider.ts @@ -6,7 +6,7 @@ const EtherscanApiUrl: Record = { arbitrum: 'https://api.arbiscan.io', 'arbitrum-goerli': 'https://api-goerli.arbiscan.io', 'arbitrum-sepolia': 'https://api-sepolia.arbiscan.io', - optimisticEthereum: 'https://api-optimistic.etherscan.io', + optimism: 'https://api-optimistic.etherscan.io', 'optimism-sepolia': 'https://api-sepolia-optimistic.etherscan.io', } From 3df43e0763d01224e86ce9376a7cc948ce1203b8 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Thu, 4 Apr 2024 14:03:12 -0400 Subject: [PATCH 09/17] remove tally script --- .github/workflows/finalize-round.yml | 18 +- contracts/.env.example | 2 +- contracts/tasks/index.ts | 1 - contracts/tasks/runners/tally.ts | 353 --------------------------- docs/tally-verify.md | 2 +- 5 files changed, 15 insertions(+), 361 deletions(-) delete mode 100644 contracts/tasks/runners/tally.ts diff --git a/.github/workflows/finalize-round.yml b/.github/workflows/finalize-round.yml index adfcdfdfc..a1d903269 100644 --- a/.github/workflows/finalize-round.yml +++ b/.github/workflows/finalize-round.yml @@ -79,14 +79,22 @@ jobs: export BLOCKS_PER_BATCH=${{ github.event.inputs.blocks_per_batch }} export RAPID_SNARK="$GITHUB_WORKSPACE/rapidsnark/package/bin/prover" export CIRCUIT_DIRECTORY=$GITHUB_WORKSPACE/params + export PROOF_OUTPUT_DIR=./proof_output # tally and finalize cd monorepo/contracts - mkdir -p proof_output - yarn hardhat tally --clrfund "${CLRFUND_ADDRESS}" --network "${NETWORK}" \ - --rapidsnark ${RAPID_SNARK} \ - --circuit-directory ${CIRCUIT_DIRECTORY} \ + mkdir -p ${PROOF_OUTPUT_DIR} + yarn gen-proofs --clrfund "${CLRFUND_ADDRESS}" \ --blocks-per-batch ${BLOCKS_PER_BATCH} \ - --maci-tx-hash "${MACI_TX_HASH}" --output-dir "./proof_output" + --rapidsnark ${RAPID_SNARK} \ + --maci-tx-hash "${MACI_TX_HASH}" \ + --proof-dir ${PROOF_OUTPUT_DIR} \ + --network "${NETWORK}" + yarn hardhat prove-on-chain --clrfund "${CLRFUND_ADDRESS}" \ + --proof-dir ${PROOF_OUTPUT_DIR} \ + --network "${NETWORK}" + yarn hardhat publish-tally-results --clrfund "${CLRFUND_ADDRESS}" \ + --proof-dir ${PROOF_OUTPUT_DIR} \ + --network "${NETWORK}" curl --location --request POST 'https://api.pinata.cloud/pinning/pinFileToIPFS' \ --header "Authorization: Bearer ${{ secrets.PINATA_JWT }}" \ --form 'file=@"./proof_output/tally.json"' diff --git a/contracts/.env.example b/contracts/.env.example index 5ace336a7..bdede7da1 100644 --- a/contracts/.env.example +++ b/contracts/.env.example @@ -6,7 +6,7 @@ JSONRPC_HTTP_URL=https://eth-goerli.alchemyapi.io/v2/ADD_API_KEY WALLET_MNEMONIC= WALLET_PRIVATE_KEY= -# The coordinator MACI private key, required by the tally script +# The coordinator MACI private key, required by the gen-proofs script COORDINATOR_MACISK= # API key used to verify contracts on arbitrum chain (including testnet) diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index 934d30612..227256785 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -10,7 +10,6 @@ import './runners/setMaciParameters' import './runners/setToken' import './runners/setUserRegistry' import './runners/setStorageRoot' -import './runners/tally' import './runners/finalize' import './runners/claim' import './runners/cancel' diff --git a/contracts/tasks/runners/tally.ts b/contracts/tasks/runners/tally.ts deleted file mode 100644 index eb135b3e8..000000000 --- a/contracts/tasks/runners/tally.ts +++ /dev/null @@ -1,353 +0,0 @@ -/** - * Script for tallying votes which involves fetching MACI logs, generating proofs, - * and proving on chain - * - * This script can be rerun by passing in --maci-state-file and --tally-file - * If the --maci-state-file is passed, it will skip MACI log fetching - * If the --tally-file is passed, it will skip MACI log fetching and proof generation - * - * Make sure to set the following environment variables in the .env file - * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC - * - coordinator's wallet private key to interact with contracts - * 2) COORDINATOR_MACISK - coordinator's MACI private key to decrypt messages - * - * Sample usage: - * - * yarn hardhat tally --clrfund --maci-tx-hash --network - * - * To rerun: - * - * yarn hardhat tally --clrfund --maci-state-file \ - * --tally-file --network - */ -import { BaseContract, getNumber, Signer, NonceManager } from 'ethers' -import { task, types } from 'hardhat/config' - -import { - DEFAULT_SR_QUEUE_OPS, - DEFAULT_GET_LOG_BATCH_SIZE, -} from '../../utils/constants' -import { getIpfsHash } from '../../utils/ipfs' -import { JSONFile } from '../../utils/JSONFile' -import { - getGenProofArgs, - genProofs, - proveOnChain, - addTallyResultsBatch, - mergeMaciSubtrees, - genLocalState, - TallyData, -} from '../../utils/maci' -import { getMaciStateFilePath, getDirname } from '../../utils/misc' -import { FundingRound, Poll, Tally } from '../../typechain-types' -import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' -import { EContracts } from '../../utils/types' -import { ContractStorage } from '../helpers/ContractStorage' -import { Subtask } from '../helpers/Subtask' - -/** - * Publish the tally IPFS hash on chain if it's not already published - * @param fundingRoundContract Funding round contract - * @param tallyData Tally data - */ -async function publishTallyHash( - fundingRoundContract: FundingRound, - tallyData: TallyData -) { - const tallyHash = await getIpfsHash(tallyData) - console.log(`Tally hash is ${tallyHash}`) - - const tallyHashOnChain = await fundingRoundContract.tallyHash() - if (tallyHashOnChain !== tallyHash) { - const tx = await fundingRoundContract.publishTallyHash(tallyHash) - const receipt = await tx.wait() - if (receipt?.status !== 1) { - throw new Error('Failed to publish tally hash on chain') - } - - console.log('Published tally hash on chain') - } -} -/** - * Submit tally data to funding round contract - * @param fundingRoundContract Funding round contract - * @param batchSize Number of tally results per batch - * @param tallyData Tally file content - */ -async function submitTallyResults( - fundingRoundContract: FundingRound, - recipientTreeDepth: number, - tallyData: TallyData, - batchSize: number -) { - const startIndex = await fundingRoundContract.totalTallyResults() - const total = tallyData.results.tally.length - console.log('Uploading tally results in batches of', batchSize) - const addTallyGas = await addTallyResultsBatch( - fundingRoundContract, - recipientTreeDepth, - tallyData, - getNumber(batchSize), - getNumber(startIndex), - (processed: number) => { - console.log(`Processed ${processed} / ${total}`) - } - ) - console.log('Tally results uploaded. Gas used:', addTallyGas.toString()) -} - -/** - * Return the current funding round contract handle - * @param clrfund ClrFund contract address - * @param coordinator Signer who will interact with the funding round contract - * @param hre Hardhat runtime environment - */ -async function getFundingRound( - clrfund: string, - coordinator: Signer, - ethers: HardhatEthersHelpers -): Promise { - const clrfundContract = await ethers.getContractAt( - EContracts.ClrFund, - clrfund, - coordinator - ) - - const fundingRound = await clrfundContract.getCurrentRound() - const fundingRoundContract = await ethers.getContractAt( - EContracts.FundingRound, - fundingRound, - coordinator - ) - - return fundingRoundContract as BaseContract as FundingRound -} - -/** - * Get the recipient tree depth (aka vote option tree depth) - * @param fundingRoundContract Funding round conract - * @param ethers Hardhat Ethers Helper - * @returns Recipient tree depth - */ -async function getRecipientTreeDepth( - fundingRoundContract: FundingRound, - ethers: HardhatEthersHelpers -): Promise { - const pollAddress = await fundingRoundContract.poll() - const pollContract = await ethers.getContractAt(EContracts.Poll, pollAddress) - const treeDepths = await (pollContract as BaseContract as Poll).treeDepths() - const voteOptionTreeDepth = treeDepths.voteOptionTreeDepth - return getNumber(voteOptionTreeDepth) -} - -/** - * Get the message processor contract address from the tally contract - * @param tallyAddress Tally contract address - * @param ethers Hardhat ethers helper - * @returns Message processor contract address - */ -async function getMessageProcessorAddress( - tallyAddress: string, - ethers: HardhatEthersHelpers -): Promise { - const tallyContract = (await ethers.getContractAt( - EContracts.Tally, - tallyAddress - )) as BaseContract as Tally - - const messageProcessorAddress = await tallyContract.messageProcessor() - return messageProcessorAddress -} - -task('tally', 'Tally votes') - .addOptionalParam('clrfund', 'ClrFund contract address') - .addOptionalParam('maciTxHash', 'MACI creation transaction hash') - .addOptionalParam('maciStateFile', 'MACI state file') - .addFlag('manageNonce', 'Whether to manually manage transaction nonce') - .addOptionalParam('tallyFile', 'The tally file path') - .addOptionalParam( - 'batchSize', - 'The batch size to upload tally result on-chain', - 10, - types.int - ) - .addParam('outputDir', 'The proof output directory', './proof_output') - .addOptionalParam('rapidsnark', 'The rapidsnark prover path') - .addOptionalParam( - 'numQueueOps', - 'The number of operations for MACI tree merging', - getNumber(DEFAULT_SR_QUEUE_OPS), - types.int - ) - .addOptionalParam( - 'blocksPerBatch', - 'The number of blocks per batch of logs to fetch on-chain', - DEFAULT_GET_LOG_BATCH_SIZE, - types.int - ) - .addOptionalParam('sleep', 'Number of seconds to sleep between log fetch') - .addOptionalParam( - 'quiet', - 'Whether to disable verbose logging', - false, - types.boolean - ) - .setAction( - async ( - { - clrfund, - maciTxHash, - quiet, - maciStateFile, - outputDir, - numQueueOps, - tallyFile, - blocksPerBatch, - rapidsnark, - sleep, - batchSize, - manageNonce, - }, - hre - ) => { - console.log('Verbose logging enabled:', !quiet) - - const { ethers, network } = hre - const storage = ContractStorage.getInstance() - const subtask = Subtask.getInstance(hre) - subtask.setHre(hre) - - const [coordinatorSigner] = await ethers.getSigners() - if (!coordinatorSigner) { - throw new Error('Env. variable WALLET_PRIVATE_KEY not set') - } - const coordinator = manageNonce - ? new NonceManager(coordinatorSigner) - : coordinatorSigner - console.log('Coordinator address: ', await coordinator.getAddress()) - - const coordinatorMacisk = process.env.COORDINATOR_MACISK - if (!coordinatorMacisk) { - throw new Error('Env. variable COORDINATOR_MACISK not set') - } - - const circuit = subtask.getConfigField( - EContracts.VkRegistry, - 'circuit' - ) - const circuitDirectory = subtask.getConfigField( - EContracts.VkRegistry, - 'paramsDirectory' - ) - - await subtask.logStart() - - const clrfundContractAddress = - clrfund ?? storage.mustGetAddress(EContracts.ClrFund, network.name) - const fundingRoundContract = await getFundingRound( - clrfundContractAddress, - coordinator, - ethers - ) - console.log('Funding round contract', fundingRoundContract.target) - - const recipientTreeDepth = await getRecipientTreeDepth( - fundingRoundContract, - ethers - ) - - const pollId = await fundingRoundContract.pollId() - console.log('PollId', pollId) - - const maciAddress = await fundingRoundContract.maci() - const maciTransactionHash = - maciTxHash ?? storage.getTxHash(maciAddress, network.name) - console.log('MACI address', maciAddress) - - const tallyAddress = await fundingRoundContract.tally() - const messageProcessorAddress = await getMessageProcessorAddress( - tallyAddress, - ethers - ) - - const providerUrl = (network.config as any).url - - const outputPath = maciStateFile - ? maciStateFile - : getMaciStateFilePath(outputDir) - - await mergeMaciSubtrees({ - maciAddress, - pollId, - numQueueOps, - signer: coordinator, - quiet, - }) - - let tallyFilePath: string = tallyFile || '' - if (!tallyFile) { - if (!maciStateFile) { - await genLocalState({ - quiet, - outputPath, - pollId, - maciContractAddress: maciAddress, - coordinatorPrivateKey: coordinatorMacisk, - ethereumProvider: providerUrl, - transactionHash: maciTransactionHash, - blockPerBatch: blocksPerBatch, - signer: coordinator, - sleep, - }) - } - - const genProofArgs = getGenProofArgs({ - maciAddress, - pollId, - coordinatorMacisk, - rapidsnark, - circuitType: circuit, - circuitDirectory, - outputDir, - blocksPerBatch: getNumber(blocksPerBatch), - maciTxHash: maciTransactionHash, - maciStateFile: outputPath, - signer: coordinator, - quiet, - }) - await genProofs(genProofArgs) - tallyFilePath = genProofArgs.tallyFile - } - - const tally = JSONFile.read(tallyFilePath) as TallyData - const proofDir = getDirname(tallyFilePath) - console.log('Proof directory', proofDir) - - // proveOnChain if not already processed - await proveOnChain({ - pollId, - proofDir, - subsidyEnabled: false, - maciAddress, - messageProcessorAddress, - tallyAddress, - signer: coordinator, - quiet, - }) - - // Publish tally hash if it is not already published - await publishTallyHash(fundingRoundContract, tally) - - // Submit tally results to the funding round contract - // This function can be re-run from where it left off - await submitTallyResults( - fundingRoundContract, - recipientTreeDepth, - tally, - batchSize - ) - - const success = true - await subtask.finish(success) - } - ) diff --git a/docs/tally-verify.md b/docs/tally-verify.md index a2d2d66b0..61139360e 100644 --- a/docs/tally-verify.md +++ b/docs/tally-verify.md @@ -34,7 +34,7 @@ yarn hardhat gen-proofs --clrfund {CLRFUND_CONTRACT_ADDRESS} --maci-state-file { ``` -** Make a backup of the {OUTPUT_DIR} before continuing to the next step ** +**Make a backup of the {OUTPUT_DIR} before continuing to the next step** Upload the proofs on chain: From ac777d71bafb6eb60573ee49909d97cff16e0498 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Fri, 5 Apr 2024 13:45:55 -0400 Subject: [PATCH 10/17] add tally script back --- .github/workflows/finalize-round.yml | 18 ++-- contracts/tasks/index.ts | 1 + contracts/tasks/runners/genProofs.ts | 115 +++++++++++++------------ contracts/tasks/runners/tally.ts | 122 +++++++++++++++++++++++++++ contracts/utils/maci.ts | 5 +- contracts/utils/misc.ts | 9 +- 6 files changed, 195 insertions(+), 75 deletions(-) create mode 100644 contracts/tasks/runners/tally.ts diff --git a/.github/workflows/finalize-round.yml b/.github/workflows/finalize-round.yml index a1d903269..d0af1ab9f 100644 --- a/.github/workflows/finalize-round.yml +++ b/.github/workflows/finalize-round.yml @@ -79,22 +79,14 @@ jobs: export BLOCKS_PER_BATCH=${{ github.event.inputs.blocks_per_batch }} export RAPID_SNARK="$GITHUB_WORKSPACE/rapidsnark/package/bin/prover" export CIRCUIT_DIRECTORY=$GITHUB_WORKSPACE/params - export PROOF_OUTPUT_DIR=./proof_output # tally and finalize cd monorepo/contracts - mkdir -p ${PROOF_OUTPUT_DIR} - yarn gen-proofs --clrfund "${CLRFUND_ADDRESS}" \ - --blocks-per-batch ${BLOCKS_PER_BATCH} \ + mkdir -p proof_output + yarn hardhat tally --clrfund "${CLRFUND_ADDRESS}" --network "${NETWORK}" \ --rapidsnark ${RAPID_SNARK} \ - --maci-tx-hash "${MACI_TX_HASH}" \ - --proof-dir ${PROOF_OUTPUT_DIR} \ - --network "${NETWORK}" - yarn hardhat prove-on-chain --clrfund "${CLRFUND_ADDRESS}" \ - --proof-dir ${PROOF_OUTPUT_DIR} \ - --network "${NETWORK}" - yarn hardhat publish-tally-results --clrfund "${CLRFUND_ADDRESS}" \ - --proof-dir ${PROOF_OUTPUT_DIR} \ - --network "${NETWORK}" + --params-dir ${CIRCUIT_DIRECTORY} \ + --blocks-per-batch ${BLOCKS_PER_BATCH} \ + --maci-tx-hash "${MACI_TX_HASH}" --output-dir "./proof_output" curl --location --request POST 'https://api.pinata.cloud/pinning/pinFileToIPFS' \ --header "Authorization: Bearer ${{ secrets.PINATA_JWT }}" \ --form 'file=@"./proof_output/tally.json"' diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index 227256785..934d30612 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -10,6 +10,7 @@ import './runners/setMaciParameters' import './runners/setToken' import './runners/setUserRegistry' import './runners/setStorageRoot' +import './runners/tally' import './runners/finalize' import './runners/claim' import './runners/cancel' diff --git a/contracts/tasks/runners/genProofs.ts b/contracts/tasks/runners/genProofs.ts index c9da580f3..a0656e650 100644 --- a/contracts/tasks/runners/genProofs.ts +++ b/contracts/tasks/runners/genProofs.ts @@ -1,13 +1,6 @@ /** * Script for generating MACI proofs * - * Pass --maci-tx-hash if this is the first time running the script and you - * want to get MACI logs starting from the block as recorded in the MACI creation - * transaction hash - * - * Pass --maci-state-file if you have previously ran the script and have - * the maci-state file (maci-state.json) - * * Make sure to set the following environment variables in the .env file * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC * - coordinator's wallet private key to interact with contracts @@ -32,11 +25,17 @@ import { genLocalState, mergeMaciSubtrees, } from '../../utils/maci' -import { getMaciStateFilePath } from '../../utils/misc' +import { + getMaciStateFilePath, + getTalyFilePath, + isPathExist, + makeDirectory, +} from '../../utils/misc' import { EContracts } from '../../utils/types' import { Subtask } from '../helpers/Subtask' import { getCurrentFundingRoundContract } from '../../utils/contracts' import { ContractStorage } from '../helpers/ContractStorage' +import { DEFAULT_CIRCUIT } from '../../utils/circuits' task('gen-proofs', 'Generate MACI proofs offchain') .addOptionalParam('clrfund', 'FundingRound contract address') @@ -48,9 +47,9 @@ task('gen-proofs', 'Generate MACI proofs offchain') undefined, types.int ) - .addOptionalParam('maciStateFile', 'MACI state file') .addFlag('manageNonce', 'Whether to manually manage transaction nonce') .addOptionalParam('rapidsnark', 'The rapidsnark prover path') + .addParam('paramsDir', 'The circuit zkeys directory', './params') .addOptionalParam( 'blocksPerBatch', 'The number of blocks per batch of logs to fetch on-chain', @@ -77,8 +76,8 @@ task('gen-proofs', 'Generate MACI proofs offchain') maciStartBlock, maciTxHash, quiet, - maciStateFile, proofDir, + paramsDir, blocksPerBatch, rapidsnark, numQueueOps, @@ -94,10 +93,6 @@ task('gen-proofs', 'Generate MACI proofs offchain') const subtask = Subtask.getInstance(hre) subtask.setHre(hre) - if (!maciStateFile && !maciTxHash && maciStartBlock == undefined) { - throw new Error('Please provide --maci-start-block or --maci-tx-hash') - } - const [coordinatorSigner] = await ethers.getSigners() if (!coordinatorSigner) { throw new Error('Env. variable WALLET_PRIVATE_KEY not set') @@ -112,14 +107,15 @@ task('gen-proofs', 'Generate MACI proofs offchain') throw new Error('Env. variable COORDINATOR_MACISK not set') } - const circuit = subtask.getConfigField( - EContracts.VkRegistry, - 'circuit' - ) - const circuitDirectory = subtask.getConfigField( - EContracts.VkRegistry, - 'paramsDirectory' - ) + const circuit = + subtask.tryGetConfigField(EContracts.VkRegistry, 'circuit') || + DEFAULT_CIRCUIT + + const circuitDirectory = + subtask.tryGetConfigField( + EContracts.VkRegistry, + 'paramsDirectory' + ) || paramsDir await subtask.logStart() @@ -136,9 +132,6 @@ task('gen-proofs', 'Generate MACI proofs offchain') console.log('PollId', pollId) const maciAddress = await fundingRoundContract.maci() - - const providerUrl = (network.config as any).url - await mergeMaciSubtrees({ maciAddress, pollId, @@ -147,42 +140,54 @@ task('gen-proofs', 'Generate MACI proofs offchain') quiet, }) - const maciStateFilePath = maciStateFile - ? maciStateFile - : getMaciStateFilePath(proofDir) + if (!isPathExist(proofDir)) { + makeDirectory(proofDir) + } + + const tallyFile = getTalyFilePath(proofDir) + const maciStateFile = getMaciStateFilePath(proofDir) + const providerUrl = (network.config as any).url - if (!maciStateFile) { - await genLocalState({ - quiet, - outputPath: maciStateFilePath, + if (!isPathExist(tallyFile)) { + if (!isPathExist(maciStateFile)) { + if (!maciTxHash && maciStartBlock == null) { + throw new Error( + 'Please provide a value for --maci-tx-hash or --maci-start-block' + ) + } + + await genLocalState({ + quiet, + outputPath: maciStateFile, + pollId, + maciContractAddress: maciAddress, + coordinatorPrivateKey: coordinatorMacisk, + ethereumProvider: providerUrl, + transactionHash: maciTxHash, + startBlock: maciStartBlock, + blockPerBatch: blocksPerBatch, + signer: coordinator, + sleep, + }) + } + + const genProofArgs = getGenProofArgs({ + maciAddress, pollId, - maciContractAddress: maciAddress, - coordinatorPrivateKey: coordinatorMacisk, - ethereumProvider: providerUrl, - transactionHash: maciTxHash, - startBlock: maciStartBlock, - blockPerBatch: blocksPerBatch, + coordinatorMacisk, + rapidsnark, + circuitType: circuit, + circuitDirectory, + outputDir: proofDir, + blocksPerBatch: getNumber(blocksPerBatch), + maciStateFile, + tallyFile, signer: coordinator, - sleep, + quiet, }) + await genProofs(genProofArgs) } - const genProofArgs = getGenProofArgs({ - maciAddress, - pollId, - coordinatorMacisk, - rapidsnark, - circuitType: circuit, - circuitDirectory, - outputDir: proofDir, - blocksPerBatch: getNumber(blocksPerBatch), - maciTxHash, - maciStateFile: maciStateFilePath, - signer: coordinator, - quiet, - }) - await genProofs(genProofArgs) - const success = true await subtask.finish(success) } diff --git a/contracts/tasks/runners/tally.ts b/contracts/tasks/runners/tally.ts new file mode 100644 index 000000000..f524dda72 --- /dev/null +++ b/contracts/tasks/runners/tally.ts @@ -0,0 +1,122 @@ +/** + * Script for tallying votes which involves fetching MACI logs, generating proofs, + * proving on chain, and uploading tally results on chain + * + * Sample usage: + * yarn hardhat tally --clrfund --maci-tx-hash --network + * + * This script can be re-run with the same input parameters + */ +import { getNumber } from 'ethers' +import { task, types } from 'hardhat/config' + +import { + DEFAULT_SR_QUEUE_OPS, + DEFAULT_GET_LOG_BATCH_SIZE, +} from '../../utils/constants' +import { EContracts } from '../../utils/types' +import { ContractStorage } from '../helpers/ContractStorage' +import { Subtask } from '../helpers/Subtask' + +task('tally', 'Tally votes') + .addOptionalParam('clrfund', 'ClrFund contract address') + .addOptionalParam('maciTxHash', 'MACI creation transaction hash') + .addOptionalParam( + 'maciStartBlock', + 'MACI creation block', + undefined, + types.int + ) + .addFlag('manageNonce', 'Whether to manually manage transaction nonce') + .addOptionalParam( + 'batchSize', + 'The batch size to upload tally result on-chain', + 8, + types.int + ) + .addParam('proofDir', 'The proof output directory', './proof_output') + .addParam('paramsDir', 'The circuit zkeys directory', './params') + .addOptionalParam('rapidsnark', 'The rapidsnark prover path') + .addOptionalParam( + 'numQueueOps', + 'The number of operations for MACI tree merging', + getNumber(DEFAULT_SR_QUEUE_OPS), + types.int + ) + .addOptionalParam( + 'blocksPerBatch', + 'The number of blocks per batch of logs to fetch on-chain', + DEFAULT_GET_LOG_BATCH_SIZE, + types.int + ) + .addOptionalParam('sleep', 'Number of seconds to sleep between log fetch') + .addOptionalParam( + 'quiet', + 'Whether to disable verbose logging', + false, + types.boolean + ) + .setAction( + async ( + { + clrfund, + maciTxHash, + maciStartBlock, + quiet, + proofDir, + paramsDir, + numQueueOps, + blocksPerBatch, + rapidsnark, + sleep, + batchSize, + manageNonce, + }, + hre + ) => { + console.log('Verbose logging enabled:', !quiet) + + const storage = ContractStorage.getInstance() + const subtask = Subtask.getInstance(hre) + subtask.setHre(hre) + + await subtask.logStart() + + const clrfundContractAddress = + clrfund ?? storage.mustGetAddress(EContracts.ClrFund, hre.network.name) + + await hre.run('gen-proofs', { + clrfund: clrfundContractAddress, + maciStartBlock, + maciTxHash, + numQueueOps, + blocksPerBatch, + rapidsnark, + sleep, + proofDir, + paramsDir, + manageNonce, + quiet, + }) + + // proveOnChain if not already processed + await hre.run('prove-on-chain', { + clrfund: clrfundContractAddress, + proofDir, + manageNonce, + quiet, + }) + + // Publish tally hash if it is not already published + await hre.run('publish-tally-results', { + clrfund: clrfundContractAddress, + proofDir, + batchSize, + manageNonce, + quiet, + }) + + const success = true + await subtask.finish(success) + } + ) diff --git a/contracts/utils/maci.ts b/contracts/utils/maci.ts index 6065f383d..bb3e3bb0b 100644 --- a/contracts/utils/maci.ts +++ b/contracts/utils/maci.ts @@ -183,6 +183,8 @@ type getGenProofArgsInput = { endBlock?: number // MACI state file maciStateFile?: string + // Tally output file + tallyFile: string // transaction signer signer: Signer // flag to turn on verbose logging in MACI cli @@ -206,12 +208,11 @@ export function getGenProofArgs(args: getGenProofArgsInput): GenProofsArgs { startBlock, endBlock, maciStateFile, + tallyFile, signer, quiet, } = args - const tallyFile = getTalyFilePath(outputDir) - const { processZkFile, tallyZkFile, diff --git a/contracts/utils/misc.ts b/contracts/utils/misc.ts index 34bb53482..029a455de 100644 --- a/contracts/utils/misc.ts +++ b/contracts/utils/misc.ts @@ -29,10 +29,9 @@ export function isPathExist(path: string): boolean { } /** - * Returns the directory of the path - * @param file The file path - * @returns The directory of the file + * Create a directory + * @param directory The directory to create */ -export function getDirname(file: string): string { - return path.dirname(file) +export function makeDirectory(directory: string): void { + fs.mkdirSync(directory) } From d70d4147285420f06706168907ee34bc6f6df684 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Fri, 5 Apr 2024 22:06:02 -0400 Subject: [PATCH 11/17] refactor the tally script to re-run without extra inputs --- .github/workflows/finalize-round.yml | 9 +- contracts/.env.example | 4 + contracts/e2e/index.ts | 4 + contracts/package.json | 1 + contracts/sh/runScriptTests.sh | 10 +- contracts/tasks/runners/claim.ts | 30 +++--- contracts/tasks/runners/finalize.ts | 26 +++--- contracts/tasks/runners/genProofs.ts | 92 ++++++++++++------- .../tasks/runners/publishTallyResults.ts | 28 ++++-- contracts/tasks/runners/tally.ts | 33 ++++++- contracts/utils/ipfs.ts | 28 +++++- contracts/utils/misc.ts | 25 ++++- docs/tally-verify.md | 41 ++------- yarn.lock | 69 +++++++++++++- 14 files changed, 287 insertions(+), 113 deletions(-) diff --git a/.github/workflows/finalize-round.yml b/.github/workflows/finalize-round.yml index d0af1ab9f..fd972749f 100644 --- a/.github/workflows/finalize-round.yml +++ b/.github/workflows/finalize-round.yml @@ -86,8 +86,7 @@ jobs: --rapidsnark ${RAPID_SNARK} \ --params-dir ${CIRCUIT_DIRECTORY} \ --blocks-per-batch ${BLOCKS_PER_BATCH} \ - --maci-tx-hash "${MACI_TX_HASH}" --output-dir "./proof_output" - curl --location --request POST 'https://api.pinata.cloud/pinning/pinFileToIPFS' \ - --header "Authorization: Bearer ${{ secrets.PINATA_JWT }}" \ - --form 'file=@"./proof_output/tally.json"' - yarn hardhat --network "${NETWORK}" finalize --clrfund "${CLRFUND_ADDRESS}" + --maci-tx-hash "${MACI_TX_HASH}" \ + --proof-dir "./proof_output" + yarn hardhat --network "${NETWORK}" finalize --clrfund "${CLRFUND_ADDRESS}" \ + --proof-dir "./proof_output" diff --git a/contracts/.env.example b/contracts/.env.example index bdede7da1..2db7006e4 100644 --- a/contracts/.env.example +++ b/contracts/.env.example @@ -13,6 +13,10 @@ COORDINATOR_MACISK= # Update the etherscan section in hardhat.config to add API key for other chains ARBISCAN_API_KEY= +# PINATE credentials to upload tally.json file to IPFS; used by the tally script +PINATA_API_KEY= +PINATA_SECRET_API_KEY= + # these are used in the e2e testing CIRCUIT_TYPE= CIRCUIT_DIRECTORY= diff --git a/contracts/e2e/index.ts b/contracts/e2e/index.ts index 5a22205dd..76e21525c 100644 --- a/contracts/e2e/index.ts +++ b/contracts/e2e/index.ts @@ -36,6 +36,7 @@ import path from 'path' import { FundingRound } from '../typechain-types' import { JSONFile } from '../utils/JSONFile' import { EContracts } from '../utils/types' +import { getTalyFilePath } from '../utils/misc' type VoteData = { recipientIndex: number; voiceCredits: bigint } type ClaimData = { [index: number]: bigint } @@ -359,6 +360,8 @@ describe('End-to-end Tests', function () { mkdirSync(outputDir, { recursive: true }) } + const tallyFile = getTalyFilePath(outputDir) + // past an end block that's later than the MACI start block const genProofArgs = getGenProofArgs({ maciAddress, @@ -368,6 +371,7 @@ describe('End-to-end Tests', function () { circuitType: circuit, circuitDirectory, outputDir, + tallyFile, blocksPerBatch: DEFAULT_GET_LOG_BATCH_SIZE, maciTxHash: maciTransactionHash, signer: coordinator, diff --git a/contracts/package.json b/contracts/package.json index 7bbbb8133..a52b79d4f 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@openzeppelin/contracts": "4.9.0", + "@pinata/sdk": "^2.1.0", "dotenv": "^8.2.0", "maci-contracts": "^1.2.0", "solidity-rlp": "2.0.8" diff --git a/contracts/sh/runScriptTests.sh b/contracts/sh/runScriptTests.sh index b17de7b2f..11cde09a2 100755 --- a/contracts/sh/runScriptTests.sh +++ b/contracts/sh/runScriptTests.sh @@ -35,17 +35,15 @@ yarn hardhat time-travel --seconds ${ROUND_DURATION} --network ${HARDHAT_NETWORK # tally the votes NODE_OPTIONS="--max-old-space-size=4096" -yarn hardhat gen-proofs \ +yarn hardhat tally \ --rapidsnark ${RAPID_SNARK} \ --proof-dir ${OUTPUT_DIR} \ --maci-start-block 0 \ --network "${HARDHAT_NETWORK}" -yarn hardhat prove-on-chain --proof-dir ${OUTPUT_DIR} --network "${HARDHAT_NETWORK}" -yarn hardhat publish-tally-results --proof-dir ${OUTPUT_DIR} --network "${HARDHAT_NETWORK}" # finalize the round -yarn hardhat finalize --tally-file ${TALLY_FILE} --network ${HARDHAT_NETWORK} +yarn hardhat finalize --proof-dir ${OUTPUT_DIR} --network ${HARDHAT_NETWORK} # claim funds -yarn hardhat claim --recipient 1 --tally-file ${TALLY_FILE} --network ${HARDHAT_NETWORK} -yarn hardhat claim --recipient 2 --tally-file ${TALLY_FILE} --network ${HARDHAT_NETWORK} +yarn hardhat claim --recipient 1 --proof-dir ${OUTPUT_DIR} --network ${HARDHAT_NETWORK} +yarn hardhat claim --recipient 2 --proof-dir ${OUTPUT_DIR} --network ${HARDHAT_NETWORK} diff --git a/contracts/tasks/runners/claim.ts b/contracts/tasks/runners/claim.ts index bf90576e2..e5135aa24 100644 --- a/contracts/tasks/runners/claim.ts +++ b/contracts/tasks/runners/claim.ts @@ -2,16 +2,17 @@ * Claim funds. This script is mainly used by e2e testing * * Sample usage: - * yarn hardhat claim \ - * --tally-file \ - * --recipient \ - * --network + * yarn hardhat claim --recipient --network */ import { getEventArg } from '../../utils/contracts' import { getRecipientClaimData } from '@clrfund/common' import { JSONFile } from '../../utils/JSONFile' -import { isPathExist } from '../../utils/misc' +import { + getProofDirForRound, + getTalyFilePath, + isPathExist, +} from '../../utils/misc' import { getNumber } from 'ethers' import { task, types } from 'hardhat/config' import { EContracts } from '../../utils/types' @@ -25,13 +26,9 @@ task('claim', 'Claim funnds for test recipients') undefined, types.int ) - .addParam('tallyFile', 'The tally file') + .addParam('proofDir', 'The proof output directory', './proof_output') .setAction( - async ({ tallyFile, recipient, roundAddress }, { ethers, network }) => { - if (!isPathExist(tallyFile)) { - throw new Error(`Path ${tallyFile} does not exist`) - } - + async ({ proofDir, recipient, roundAddress }, { ethers, network }) => { if (recipient <= 0) { throw new Error('Recipient must be greater than 0') } @@ -41,6 +38,17 @@ task('claim', 'Claim funnds for test recipients') roundAddress ?? storage.mustGetAddress(EContracts.FundingRound, network.name) + const proofDirForRound = getProofDirForRound( + proofDir, + network.name, + fundingRound + ) + + const tallyFile = getTalyFilePath(proofDirForRound) + if (!isPathExist(tallyFile)) { + throw new Error(`Path ${tallyFile} does not exist`) + } + const tally = JSONFile.read(tallyFile) const fundingRoundContract = await ethers.getContractAt( diff --git a/contracts/tasks/runners/finalize.ts b/contracts/tasks/runners/finalize.ts index 0240ba16b..e48a6c2b6 100644 --- a/contracts/tasks/runners/finalize.ts +++ b/contracts/tasks/runners/finalize.ts @@ -6,7 +6,7 @@ * - clrfund owner's wallet private key to interact with the contract * * Sample usage: - * yarn hardhat finalize --clrfund --tally-file --network + * yarn hardhat finalize --clrfund --network */ import { JSONFile } from '../../utils/JSONFile' @@ -16,20 +16,13 @@ import { task } from 'hardhat/config' import { EContracts } from '../../utils/types' import { ContractStorage } from '../helpers/ContractStorage' import { Subtask } from '../helpers/Subtask' +import { getProofDirForRound, getTalyFilePath } from '../../utils/misc' task('finalize', 'Finalize a funding round') .addOptionalParam('clrfund', 'The ClrFund contract address') - .addOptionalParam( - 'tallyFile', - 'The tally file path', - './proof_output/tally.json' - ) - .setAction(async ({ clrfund, tallyFile }, hre) => { + .addParam('proofDir', 'The proof output directory', './proof_output') + .setAction(async ({ clrfund, proofDir }, hre) => { const { ethers, network } = hre - const tally = JSONFile.read(tallyFile) - if (!tally.maci) { - throw Error('Bad tally file ' + tallyFile) - } const storage = ContractStorage.getInstance() const subtask = Subtask.getInstance(hre) @@ -63,6 +56,17 @@ task('finalize', 'Finalize a funding round') const treeDepths = await pollContract.treeDepths() console.log('voteOptionTreeDepth', treeDepths.voteOptionTreeDepth) + const currentRoundProofDir = getProofDirForRound( + proofDir, + network.name, + currentRoundAddress + ) + const tallyFile = getTalyFilePath(currentRoundProofDir) + const tally = JSONFile.read(tallyFile) + if (!tally.maci) { + throw Error('Bad tally file ' + tallyFile) + } + const totalSpent = tally.totalSpentVoiceCredits.spent const totalSpentSalt = tally.totalSpentVoiceCredits.salt diff --git a/contracts/tasks/runners/genProofs.ts b/contracts/tasks/runners/genProofs.ts index a0656e650..ed3061edb 100644 --- a/contracts/tasks/runners/genProofs.ts +++ b/contracts/tasks/runners/genProofs.ts @@ -36,6 +36,29 @@ import { Subtask } from '../helpers/Subtask' import { getCurrentFundingRoundContract } from '../../utils/contracts' import { ContractStorage } from '../helpers/ContractStorage' import { DEFAULT_CIRCUIT } from '../../utils/circuits' +import { JSONFile } from '../../utils/JSONFile' + +/** + * Check if the tally file with the maci contract address exists + * @param tallyFile The tally file path + * @param maciAddress The MACI contract address + * @returns true if the file exists and it contains the MACI contract address + */ +function tallyFileExists(tallyFile: string, maciAddress: string): boolean { + if (!isPathExist(tallyFile)) { + return false + } + try { + const tallyData = JSONFile.read(tallyFile) + return ( + tallyData.maci && + tallyData.maci.toLowerCase() === maciAddress.toLowerCase() + ) + } catch { + // in case the file does not have the expected format/field + return false + } +} task('gen-proofs', 'Generate MACI proofs offchain') .addOptionalParam('clrfund', 'FundingRound contract address') @@ -148,46 +171,49 @@ task('gen-proofs', 'Generate MACI proofs offchain') const maciStateFile = getMaciStateFilePath(proofDir) const providerUrl = (network.config as any).url - if (!isPathExist(tallyFile)) { - if (!isPathExist(maciStateFile)) { - if (!maciTxHash && maciStartBlock == null) { - throw new Error( - 'Please provide a value for --maci-tx-hash or --maci-start-block' - ) - } - - await genLocalState({ - quiet, - outputPath: maciStateFile, - pollId, - maciContractAddress: maciAddress, - coordinatorPrivateKey: coordinatorMacisk, - ethereumProvider: providerUrl, - transactionHash: maciTxHash, - startBlock: maciStartBlock, - blockPerBatch: blocksPerBatch, - signer: coordinator, - sleep, - }) + if (tallyFileExists(tallyFile, maciAddress)) { + console.log('The tally file has already been generated.') + return + } + + if (!isPathExist(maciStateFile)) { + if (!maciTxHash && maciStartBlock == null) { + throw new Error( + 'Please provide a value for --maci-tx-hash or --maci-start-block' + ) } - const genProofArgs = getGenProofArgs({ - maciAddress, + await genLocalState({ + quiet, + outputPath: maciStateFile, pollId, - coordinatorMacisk, - rapidsnark, - circuitType: circuit, - circuitDirectory, - outputDir: proofDir, - blocksPerBatch: getNumber(blocksPerBatch), - maciStateFile, - tallyFile, + maciContractAddress: maciAddress, + coordinatorPrivateKey: coordinatorMacisk, + ethereumProvider: providerUrl, + transactionHash: maciTxHash, + startBlock: maciStartBlock, + blockPerBatch: blocksPerBatch, signer: coordinator, - quiet, + sleep, }) - await genProofs(genProofArgs) } + const genProofArgs = getGenProofArgs({ + maciAddress, + pollId, + coordinatorMacisk, + rapidsnark, + circuitType: circuit, + circuitDirectory, + outputDir: proofDir, + blocksPerBatch: getNumber(blocksPerBatch), + maciStateFile, + tallyFile, + signer: coordinator, + quiet, + }) + await genProofs(genProofArgs) + const success = true await subtask.finish(success) } diff --git a/contracts/tasks/runners/publishTallyResults.ts b/contracts/tasks/runners/publishTallyResults.ts index 91699fa94..5651ac353 100644 --- a/contracts/tasks/runners/publishTallyResults.ts +++ b/contracts/tasks/runners/publishTallyResults.ts @@ -5,6 +5,8 @@ * Make sure to set the following environment variables in the .env file * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC * - coordinator's wallet private key to interact with contracts + * 2) PINATA_API_KEY - The Pinata api key for pinning file to IPFS + * 3) PINATA_SECRET_API_KEY - The Pinata secret api key for pinning file to IPFS * * Sample usage: * @@ -15,7 +17,7 @@ import { BaseContract, getNumber, NonceManager } from 'ethers' import { task, types } from 'hardhat/config' -import { getIpfsHash } from '../../utils/ipfs' +import { Ipfs } from '../../utils/ipfs' import { JSONFile } from '../../utils/JSONFile' import { addTallyResultsBatch, TallyData, verify } from '../../utils/maci' import { FundingRound, Poll } from '../../typechain-types' @@ -25,17 +27,17 @@ import { Subtask } from '../helpers/Subtask' import { getCurrentFundingRoundContract } from '../../utils/contracts' import { getTalyFilePath } from '../../utils/misc' import { ContractStorage } from '../helpers/ContractStorage' +import { PINATA_PINNING_URL } from '../../utils/constants' /** * Publish the tally IPFS hash on chain if it's not already published * @param fundingRoundContract Funding round contract - * @param tallyData Tally data + * @param tallyHash Tally hash */ async function publishTallyHash( fundingRoundContract: FundingRound, - tallyData: TallyData + tallyHash: string ) { - const tallyHash = await getIpfsHash(tallyData) console.log(`Tally hash is ${tallyHash}`) const tallyHashOnChain = await fundingRoundContract.tallyHash() @@ -63,7 +65,9 @@ async function submitTallyResults( ) { const startIndex = await fundingRoundContract.totalTallyResults() const total = tallyData.results.tally.length - console.log('Uploading tally results in batches of', batchSize) + if (startIndex < total) { + console.log('Uploading tally results in batches of', batchSize) + } const addTallyGas = await addTallyResultsBatch( fundingRoundContract, recipientTreeDepth, @@ -119,6 +123,16 @@ task('publish-tally-results', 'Publish tally results') const coordinator = manageNonce ? new NonceManager(signer) : signer console.log('Coordinator address: ', await coordinator.getAddress()) + const apiKey = process.env.PINATA_API_KEY + if (!apiKey) { + throw new Error('Env. variable PINATA_API_KEY not set') + } + + const secretApiKey = process.env.PINATA_SECRET_API_KEY + if (!secretApiKey) { + throw new Error('Env. variable PINATA_SECRET_API_KEY not set') + } + await subtask.logStart() const clrfundContractAddress = @@ -149,8 +163,10 @@ task('publish-tally-results', 'Publish tally results') quiet, }) + const tallyHash = await Ipfs.pinFile(tallyFile, apiKey, secretApiKey) + // Publish tally hash if it is not already published - await publishTallyHash(fundingRoundContract, tallyData) + await publishTallyHash(fundingRoundContract, tallyHash) // Submit tally results to the funding round contract // This function can be re-run from where it left off diff --git a/contracts/tasks/runners/tally.ts b/contracts/tasks/runners/tally.ts index f524dda72..94482b679 100644 --- a/contracts/tasks/runners/tally.ts +++ b/contracts/tasks/runners/tally.ts @@ -9,11 +9,13 @@ */ import { getNumber } from 'ethers' import { task, types } from 'hardhat/config' +import { ClrFund } from '../../typechain-types' import { DEFAULT_SR_QUEUE_OPS, DEFAULT_GET_LOG_BATCH_SIZE, } from '../../utils/constants' +import { getProofDirForRound } from '../../utils/misc' import { EContracts } from '../../utils/types' import { ContractStorage } from '../helpers/ContractStorage' import { Subtask } from '../helpers/Subtask' @@ -76,6 +78,16 @@ task('tally', 'Tally votes') ) => { console.log('Verbose logging enabled:', !quiet) + const apiKey = process.env.PINATA_API_KEY + if (!apiKey) { + throw new Error('Env. variable PINATA_API_KEY not set') + } + + const secretApiKey = process.env.PINATA_SECRET_API_KEY + if (!secretApiKey) { + throw new Error('Env. variable PINATA_SECRET_API_KEY not set') + } + const storage = ContractStorage.getInstance() const subtask = Subtask.getInstance(hre) subtask.setHre(hre) @@ -85,6 +97,21 @@ task('tally', 'Tally votes') const clrfundContractAddress = clrfund ?? storage.mustGetAddress(EContracts.ClrFund, hre.network.name) + const clrfundContract = subtask.getContract({ + name: EContracts.ClrFund, + address: clrfundContractAddress, + }) + + const fundingRoundContractAddress = await ( + await clrfundContract + ).getCurrentRound() + + const outputDir = getProofDirForRound( + proofDir, + hre.network.name, + fundingRoundContractAddress + ) + await hre.run('gen-proofs', { clrfund: clrfundContractAddress, maciStartBlock, @@ -93,7 +120,7 @@ task('tally', 'Tally votes') blocksPerBatch, rapidsnark, sleep, - proofDir, + proofDir: outputDir, paramsDir, manageNonce, quiet, @@ -102,7 +129,7 @@ task('tally', 'Tally votes') // proveOnChain if not already processed await hre.run('prove-on-chain', { clrfund: clrfundContractAddress, - proofDir, + proofDir: outputDir, manageNonce, quiet, }) @@ -110,7 +137,7 @@ task('tally', 'Tally votes') // Publish tally hash if it is not already published await hre.run('publish-tally-results', { clrfund: clrfundContractAddress, - proofDir, + proofDir: outputDir, batchSize, manageNonce, quiet, diff --git a/contracts/utils/ipfs.ts b/contracts/utils/ipfs.ts index 8fc5de275..092d9a876 100644 --- a/contracts/utils/ipfs.ts +++ b/contracts/utils/ipfs.ts @@ -2,7 +2,9 @@ const Hash = require('ipfs-only-hash') import { FetchRequest } from 'ethers' import { DEFAULT_IPFS_GATEWAY } from './constants' - +import fs from 'fs' +import path from 'path' +import pinataSDK from '@pinata/sdk' /** * Get the ipfs hash for the input object * @param object a json object to get the ipfs hash for @@ -26,4 +28,28 @@ export class Ipfs { const resp = await req.send() return resp.bodyJson } + + /** + * Pin a file to IPFS + * @param file The file path to be uploaded to IPFS + * @param apiKey Pinata api key + * @param secretApiKey Pinata secret api key + * @returns IPFS hash + */ + static async pinFile( + file: string, + apiKey: string, + secretApiKey: string + ): Promise { + const pinata = new pinataSDK(apiKey, secretApiKey) + const data = fs.createReadStream(file) + const name = path.basename(file) + const options = { + pinataMetadata: { + name, + }, + } + const res = await pinata.pinFileToIPFS(data, options) + return res.IpfsHash + } } diff --git a/contracts/utils/misc.ts b/contracts/utils/misc.ts index 029a455de..9f7a34297 100644 --- a/contracts/utils/misc.ts +++ b/contracts/utils/misc.ts @@ -19,6 +19,29 @@ export function getMaciStateFilePath(directory: string) { return path.join(directory, 'maci-state.json') } +/** + * Return the proof output directory + * @param directory The root directory + * @param network The network + * @param roundAddress The funding round contract address + * @returns The proofs output directory + */ +export function getProofDirForRound( + directory: string, + network: string, + roundAddress: string +) { + try { + return path.join( + directory, + network.toLowerCase(), + roundAddress.toLowerCase() + ) + } catch { + return directory + } +} + /** * Check if the path exist * @param path The path to check for existence @@ -33,5 +56,5 @@ export function isPathExist(path: string): boolean { * @param directory The directory to create */ export function makeDirectory(directory: string): void { - fs.mkdirSync(directory) + fs.mkdirSync(directory, { recursive: true }) } diff --git a/docs/tally-verify.md b/docs/tally-verify.md index 61139360e..9ce6bc7dd 100644 --- a/docs/tally-verify.md +++ b/docs/tally-verify.md @@ -18,46 +18,23 @@ COORDINATOR_MACISK= # private key for interacting with contracts WALLET_MNEMONIC= WALLET_PRIVATE_KEY -``` - -Decrypt messages, tally the votes and generate proofs: -``` -yarn hardhat gen-proofs --clrfund {CLRFUND_CONTRACT_ADDRESS} --maci-tx-hash {MACI_CREATION_TRANSACTION_HASH} --proof-dir {OUTPUT_DIR} --rapidsnark {RAPID_SNARK} --network {network} +# credential to upload tally result to IPFS +PINATA_API_KEY= +PINATA_SECRET_API_KEY= ``` -You only need to provide `--rapidsnark` if you are running the `tally` command on an intel chip. -If `gen-proofs` failed, you can rerun the command with the same parameters. If the maci-state.json file has been created, you can skip fetching MACI logs by providing the MACI state file as follow: +Decrypt messages, tally the votes: ``` -yarn hardhat gen-proofs --clrfund {CLRFUND_CONTRACT_ADDRESS} --maci-state-file {MACI_STATE_FILE_PATH} --proof-dir {OUTPUT_DIR} --rapidsnark {RAPID_SNARK} --network {network} +yarn hardhat tally --clrfund {CLRFUND_CONTRACT_ADDRESS} --maci-tx-hash {MACI_CREATION_TRANSACTION_HASH} --proof-dir {OUTPUT_DIR} --rapidsnark {RAPID_SNARK} --network {network} ``` - -**Make a backup of the {OUTPUT_DIR} before continuing to the next step** - - -Upload the proofs on chain: -``` -yarn hardhat prove-on-chain --clrfund {CLRFUND_CONTRACT_ADDRESS} --proof-dir {OUTPUT_DIR} --network {network} -yarn hardhat publish-tally-results --clrfund {CLRFUND_CONTRACT_ADDRESS} --proof-dir {OUTPUT_DIR} --network localhost -``` - -If there's error running `prove-on-chain` or `publish-tally-resuls`, simply rerun the commands with the same parameters. - - - -Result will be saved to `{OUTPUT_DIR}/tally.json` file, which must then be published via IPFS. - -**Using [command line](https://docs.ipfs.tech/reference/kubo/cli/#ipfs)** - +You only need to provide `--rapidsnark` if you are running the `tally` command on an intel chip. +If the `tally` script failed, you can rerun the command with the same parameters. ``` -# start ipfs daemon in one terminal -ipfs daemon -# in a diff terminal, go to `/contracts` (or where you have the file) and publish the file -ipfs add tally.json -``` +Result will be saved to `{OUTPUT_DIR}/{network}-{fundingRoundAddress}/tally.json` file, which is also available on IPFS at `https://{ipfs-gateway-host}/ipfs/{tally-hash}`. ### Finalize round @@ -72,7 +49,7 @@ WALLET_PRIVATE_KEY= Once you have the `tally.json` from the tally script, run: ``` -yarn hardhat finalize --tally-file {tally.json} --network {network} +yarn hardhat finalize --clrfund {CLRFUND_CONTRACT_ADDRESS} --proof-dir {OUTPUT_DIR} --network {network} ``` # How to verify the tally results diff --git a/yarn.lock b/yarn.lock index 11e8b7f78..86e13a1f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3409,6 +3409,16 @@ tslib "^2.5.0" webcrypto-core "^1.7.7" +"@pinata/sdk@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@pinata/sdk/-/sdk-2.1.0.tgz#d61aa8f21ec1206e867f4b65996db52b70316945" + integrity sha512-hkS0tcKtsjf9xhsEBs2Nbey5s+Db7x5rlOH9TaWHBXkJ7IwwOs2xnEDigNaxAHKjYAwcw+m2hzpO5QgOfeF7Zw== + dependencies: + axios "^0.21.1" + form-data "^2.3.3" + is-ipfs "^0.6.0" + path "^0.12.7" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -6830,7 +6840,7 @@ browserslist@^4.21.10, browserslist@^4.22.2: node-releases "^2.0.14" update-browserslist-db "^1.0.13" -bs58@^4.0.0: +bs58@^4.0.0, bs58@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== @@ -10984,7 +10994,7 @@ form-data-encoder@^2.1.2: resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== -form-data@^2.2.0: +form-data@^2.2.0, form-data@^2.3.3: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -12507,6 +12517,11 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, i resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + ini@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" @@ -13200,6 +13215,18 @@ is-ip@^3.1.0: dependencies: ip-regex "^4.0.0" +is-ipfs@^0.6.0: + version "0.6.3" + resolved "https://registry.yarnpkg.com/is-ipfs/-/is-ipfs-0.6.3.tgz#82a5350e0a42d01441c40b369f8791e91404c497" + integrity sha512-HyRot1dvLcxImtDqPxAaY1miO6WsiP/z7Yxpg2qpaLWv5UdhAPtLvHJ4kMLM0w8GSl8AFsVF23PHe1LzuWrUlQ== + dependencies: + bs58 "^4.0.1" + cids "~0.7.0" + mafmt "^7.0.0" + multiaddr "^7.2.1" + multibase "~0.6.0" + multihashes "~0.4.13" + is-lower-case@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-2.0.2.tgz#1c0884d3012c841556243483aa5d522f47396d2a" @@ -14968,6 +14995,13 @@ macos-release@^3.1.0: resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-3.2.0.tgz#dcee82b6a4932971b1538dbf6f3aabc4a903b613" integrity sha512-fSErXALFNsnowREYZ49XCdOHF8wOPWuFOGQrAhP7x5J/BqQv+B02cNsTykGpDgRVx43EKg++6ANmTaGTtW+hUA== +mafmt@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/mafmt/-/mafmt-7.1.0.tgz#4126f6d0eded070ace7dbbb6fb04977412d380b5" + integrity sha512-vpeo9S+hepT3k2h5iFxzEHvvR0GPBx9uKaErmnRzYNcaKb03DgOArjEMlgG4a9LcuZZ89a3I8xbeto487n26eA== + dependencies: + multiaddr "^7.3.0" + magic-string@^0.26.7: version "0.26.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f" @@ -15646,6 +15680,18 @@ multiaddr@^10.0.0: uint8arrays "^3.0.0" varint "^6.0.0" +multiaddr@^7.2.1, multiaddr@^7.3.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/multiaddr/-/multiaddr-7.5.0.tgz#976c88e256e512263445ab03b3b68c003d5f485e" + integrity sha512-GvhHsIGDULh06jyb6ev+VfREH9evJCFIRnh3jUt9iEZ6XDbyoisZRFEI9bMvK/AiR6y66y6P+eoBw9mBYMhMvw== + dependencies: + buffer "^5.5.0" + cids "~0.8.0" + class-is "^1.1.0" + is-ip "^3.1.0" + multibase "^0.7.0" + varint "^5.0.0" + multibase@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/multibase/-/multibase-0.7.0.tgz#1adfc1c50abe05eefeb5091ac0c2728d6b84581b" @@ -15690,7 +15736,7 @@ multiformats@^9.4.13, multiformats@^9.4.2, multiformats@^9.4.5, multiformats@^9. resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== -multihashes@^0.4.15, multihashes@~0.4.15: +multihashes@^0.4.15, multihashes@~0.4.13, multihashes@~0.4.15: version "0.4.21" resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-0.4.21.tgz#dc02d525579f334a7909ade8a122dabb58ccfcb5" integrity sha512-uVSvmeCWf36pU2nB4/1kzYZjsXD9vofZKpgudqkceYY5g2aZZXJ5r9lxuzoRLl1OAp28XljXsEJ/X/85ZsKmKw== @@ -17063,6 +17109,14 @@ path-type@^5.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== +path@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" + integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q== + dependencies: + process "^0.11.1" + util "^0.10.3" + pathe@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/pathe/-/pathe-0.2.0.tgz#30fd7bbe0a0d91f0e60bae621f5d19e9e225c339" @@ -17431,7 +17485,7 @@ process-warning@^3.0.0: resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== -process@^0.11.10: +process@^0.11.1, process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== @@ -21003,6 +21057,13 @@ util.promisify@^1.0.0: object.getownpropertydescriptors "^2.1.6" safe-array-concat "^1.0.0" +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + util@^0.12.4, util@^0.12.5: version "0.12.5" resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" From 88f8898c1666a33a310a0600070e69cfa3d82ec8 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Fri, 5 Apr 2024 22:32:36 -0400 Subject: [PATCH 12/17] add PINATA env variables --- .github/workflows/finalize-round.yml | 2 ++ .github/workflows/test-scripts.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/finalize-round.yml b/.github/workflows/finalize-round.yml index fd972749f..0109651d5 100644 --- a/.github/workflows/finalize-round.yml +++ b/.github/workflows/finalize-round.yml @@ -29,6 +29,8 @@ env: CIRCUIT_TYPE: micro ZKEYS_DOWNLOAD_SCRIPT: "download-6-9-2-3.sh" JSONRPC_HTTP_URL: ${{ github.event.inputs.jsonrpc_url }} + PINATA_API_KEY: ${{ secrets.PINATA_API_KEY }} + PINATA_SECRET_API_KEY: ${{ secrets.PINATA_SECRET_API_KEY }} jobs: finalize: diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml index 5ebe7d346..05082aa5e 100644 --- a/.github/workflows/test-scripts.yml +++ b/.github/workflows/test-scripts.yml @@ -10,6 +10,8 @@ on: env: NODE_VERSION: 20.x ZKEYS_DOWNLOAD_SCRIPT: "download-6-9-2-3.sh" + PINATA_API_KEY: ${{ secrets.PINATA_API_KEY }} + PINATA_SECRET_API_KEY: ${{ secrets.PINATA_SECRET_API_KEY }} jobs: script-tests: From 00cb623dd17c29cb102080a3f5f34d5ea38e3a42 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Fri, 12 Apr 2024 17:33:45 -0400 Subject: [PATCH 13/17] verify ClrFund contract by address --- .../tasks/helpers/ConstructorArguments.ts | 339 ++++++++++++++++ contracts/tasks/helpers/ContractStorage.ts | 20 + contracts/tasks/helpers/ContractVerifier.ts | 6 +- contracts/tasks/index.ts | 1 + contracts/tasks/runners/newClrFund.ts | 12 +- contracts/tasks/runners/newDeployer.ts | 11 +- contracts/tasks/runners/setToken.ts | 8 +- contracts/tasks/runners/verifyAll.ts | 368 ++++++++++++++---- contracts/tasks/runners/verifyDeployer.ts | 34 ++ contracts/tests/maciFactory.ts | 2 +- contracts/utils/testutils.ts | 2 +- contracts/utils/types.ts | 2 + 12 files changed, 715 insertions(+), 90 deletions(-) create mode 100644 contracts/tasks/helpers/ConstructorArguments.ts create mode 100644 contracts/tasks/runners/verifyDeployer.ts diff --git a/contracts/tasks/helpers/ConstructorArguments.ts b/contracts/tasks/helpers/ConstructorArguments.ts new file mode 100644 index 000000000..6f9f3d5b4 --- /dev/null +++ b/contracts/tasks/helpers/ConstructorArguments.ts @@ -0,0 +1,339 @@ +import type { HardhatRuntimeEnvironment } from 'hardhat/types' +import { BaseContract, Interface } from 'ethers' +import { ContractStorage } from './ContractStorage' +import { EContracts } from './types' +import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' +import { + BrightIdUserRegistry, + ClrFundDeployer, + MACIFactory, + MessageProcessor, + OptimisticRecipientRegistry, + Poll, + Tally, +} from '../../typechain-types' + +/** A list of functions to get contract constructor arguments from the contract */ +const ConstructorArgumentsGetters: Record< + string, + (address: string, ethers: HardhatEthersHelpers) => Promise> +> = { + [EContracts.FundingRound]: getFundingRoundConstructorArguments, + [EContracts.MACI]: getMaciConstructorArguments, + [EContracts.Poll]: getPollConstructorArguments, + [EContracts.Tally]: getTallyConstructorArguments, + [EContracts.MessageProcessor]: getMessageProcessorConstructorArguments, + [EContracts.BrightIdUserRegistry]: + getBrightIdUserRegistryConstructorArguments, + [EContracts.OptimisticRecipientRegistry]: + getOptimisticRecipientRegistryConstructorArguments, + [EContracts.ClrFundDeployer]: getClrFundDeployerConstructorArguments, + [EContracts.MACIFactory]: getMACIFactoryConstructorArguments, +} + +/** + * Get the constructor arguments for FundingRound + * @param address The funding round contract address + * @param ethers The Hardhat Ethers helper + * @returns The funding round constructor arguments + */ +async function getFundingRoundConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const round = await ethers.getContractAt(EContracts.FundingRound, address) + + const args = await Promise.all([ + round.nativeToken(), + round.userRegistry(), + round.recipientRegistry(), + round.coordinator(), + ]) + + return args +} + +/** + * Get the constructor arguments for MACI + * @param address The MACI contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getMaciConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const maci = await ethers.getContractAt(EContracts.MACI, address) + + const args = await Promise.all([ + maci.pollFactory(), + maci.messageProcessorFactory(), + maci.tallyFactory(), + maci.subsidyFactory(), + maci.signUpGatekeeper(), + maci.initialVoiceCreditProxy(), + maci.topupCredit(), + maci.stateTreeDepth(), + ]) + + return args +} + +/** + * Get the constructor arguments for Poll + * @param address The Poll contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getPollConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const pollContract = (await ethers.getContractAt( + EContracts.Poll, + address + )) as BaseContract as Poll + + const [, duration] = await pollContract.getDeployTimeAndDuration() + const [maxValues, treeDepths, coordinatorPubKey, extContracts] = + await Promise.all([ + pollContract.maxValues(), + pollContract.treeDepths(), + pollContract.coordinatorPubKey(), + pollContract.extContracts(), + ]) + + const args = [ + duration, + { + maxMessages: maxValues.maxMessages, + maxVoteOptions: maxValues.maxVoteOptions, + }, + { + intStateTreeDepth: treeDepths.intStateTreeDepth, + messageTreeSubDepth: treeDepths.messageTreeSubDepth, + messageTreeDepth: treeDepths.messageTreeDepth, + voteOptionTreeDepth: treeDepths.voteOptionTreeDepth, + }, + { + x: coordinatorPubKey.x, + y: coordinatorPubKey.y, + }, + { + maci: extContracts.maci, + messageAq: extContracts.messageAq, + topupCredit: extContracts.topupCredit, + }, + ] + + return args +} + +/** + * Get the constructor arguments for Tally + * @param address The Tally contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getTallyConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const tallyContract = (await ethers.getContractAt( + EContracts.Tally, + address + )) as BaseContract as Tally + + const args = await Promise.all([ + tallyContract.verifier(), + tallyContract.vkRegistry(), + tallyContract.poll(), + tallyContract.messageProcessor(), + ]) + + return args +} + +/** + * Get the constructor arguments for MessageProcessor + * @param address The MessageProcessor contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getMessageProcessorConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const messageProcesor = (await ethers.getContractAt( + EContracts.MessageProcessor, + address + )) as BaseContract as MessageProcessor + + const args = await Promise.all([ + messageProcesor.verifier(), + messageProcesor.vkRegistry(), + messageProcesor.poll(), + ]) + + return args +} + +/** + * Get the constructor arguments for BrightIdUserRegistry + * @param address The BrightIdUserRegistry contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getBrightIdUserRegistryConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const registry = (await ethers.getContractAt( + EContracts.BrightIdUserRegistry, + address + )) as BaseContract as BrightIdUserRegistry + + const args = await Promise.all([ + registry.context(), + registry.verifier(), + registry.brightIdSponsor(), + ]) + + return args +} + +/** + * Get the constructor arguments for OptimisticRecipientRegistry + * @param address The OptimisticRecipientRegistry contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getOptimisticRecipientRegistryConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const registry = (await ethers.getContractAt( + EContracts.OptimisticRecipientRegistry, + address + )) as BaseContract as OptimisticRecipientRegistry + + const args = await Promise.all([ + registry.baseDeposit(), + registry.challengePeriodDuration(), + registry.controller(), + ]) + + return args +} + +/** + * Get the constructor arguments for ClrFundDeployer + * @param address The ClrFundDeployer contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getClrFundDeployerConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const registry = (await ethers.getContractAt( + EContracts.ClrFundDeployer, + address + )) as BaseContract as ClrFundDeployer + + const args = await Promise.all([ + registry.clrfundTemplate(), + registry.maciFactory(), + registry.roundFactory(), + ]) + + return args +} + +/** + * Get the constructor arguments for MACIFactory + * @param address The MACIFactory contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getMACIFactoryConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const registry = (await ethers.getContractAt( + EContracts.MACIFactory, + address + )) as BaseContract as MACIFactory + + const args = await Promise.all([ + registry.vkRegistry(), + registry.factories(), + registry.verifier(), + ]) + + return args +} + +/** + * @notice A helper to retrieve contract constructor arguments + */ +export class ConstructorArguments { + /** + * Hardhat runtime environment + */ + private hre: HardhatRuntimeEnvironment + + /** + * Local contract deployment information + */ + private storage: ContractStorage + + /** + * Initialize class properties + * + * @param hre - Hardhat runtime environment + */ + constructor(hre: HardhatRuntimeEnvironment) { + this.hre = hre + this.storage = ContractStorage.getInstance() + } + + /** + * Get the contract constructor arguments + * @param name - contract name + * @param address - contract address + * @param ethers = Hardhat Ethers helper + * @returns - stringified constructor arguments + */ + async get( + name: string, + address: string, + ethers: HardhatEthersHelpers + ): Promise> { + const contractArtifact = this.hre.artifacts.readArtifactSync(name) + const contractInterface = new Interface(contractArtifact.abi) + if (contractInterface.deploy.inputs.length === 0) { + // no argument + return [] + } + + // try to get arguments from deployed-contract.json file + const constructorArguments = this.storage.getConstructorArguments( + address, + this.hre.network.name + ) + if (constructorArguments) { + return constructorArguments + } + + // try to get custom constructor arguments from contract + let args: Array = [] + + const getConstructorArguments = ConstructorArgumentsGetters[name] + if (getConstructorArguments) { + args = await getConstructorArguments(address, ethers) + } + + return args + } +} diff --git a/contracts/tasks/helpers/ContractStorage.ts b/contracts/tasks/helpers/ContractStorage.ts index 9c092bd24..56db46e6e 100644 --- a/contracts/tasks/helpers/ContractStorage.ts +++ b/contracts/tasks/helpers/ContractStorage.ts @@ -204,6 +204,26 @@ export class ContractStorage { return instance?.txHash } + /** + * Get contract constructor argument by address from the json file + * + * @param address - contract address + * @param network - selected network + * @returns contract constructor arguments + */ + getConstructorArguments( + address: string, + network: string + ): Array | undefined { + if (!this.db[network]) { + return undefined + } + + const instance = this.db[network].instance?.[address] + const args = instance?.verify?.args + return args ? JSON.parse(args) : undefined + } + /** * Get contract address by name from the json file * diff --git a/contracts/tasks/helpers/ContractVerifier.ts b/contracts/tasks/helpers/ContractVerifier.ts index 6d71952bc..3a517373f 100644 --- a/contracts/tasks/helpers/ContractVerifier.ts +++ b/contracts/tasks/helpers/ContractVerifier.ts @@ -31,13 +31,13 @@ export class ContractVerifier { */ async verify( address: string, - constructorArguments: string, + constructorArguments: unknown[], libraries?: string, contract?: string ): Promise<[boolean, string]> { const params: IVerificationSubtaskArgs = { address, - constructorArguments: JSON.parse(constructorArguments) as unknown[], + constructorArguments, contract, } @@ -50,7 +50,7 @@ export class ContractVerifier { .run('verify:verify', params) .then(() => '') .catch((err: Error) => { - if (err.message === 'Contract source code already verified') { + if (err.message && err.message.match(/already verified/i)) { return '' } diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index 5bcc7e4fc..297320679 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -23,3 +23,4 @@ import './runners/addRecipients' import './runners/findStorageSlot' import './runners/verifyTallyFile' import './runners/verifyAll' +import './runners/verifyDeployer' diff --git a/contracts/tasks/runners/newClrFund.ts b/contracts/tasks/runners/newClrFund.ts index 31ff081a0..9986ca223 100644 --- a/contracts/tasks/runners/newClrFund.ts +++ b/contracts/tasks/runners/newClrFund.ts @@ -13,9 +13,9 @@ * where `nonce too low` errors occur occasionally */ import { task, types } from 'hardhat/config' - +import { ContractStorage } from '../helpers/ContractStorage' import { Subtask } from '../helpers/Subtask' -import { type ISubtaskParams } from '../helpers/types' +import { EContracts, type ISubtaskParams } from '../helpers/types' task('new-clrfund', 'Deploy a new instance of ClrFund') .addFlag('incremental', 'Incremental deployment') @@ -26,6 +26,7 @@ task('new-clrfund', 'Deploy a new instance of ClrFund') .setAction(async (params: ISubtaskParams, hre) => { const { verify, manageNonce } = params const subtask = Subtask.getInstance(hre) + const storage = ContractStorage.getInstance() subtask.setHre(hre) const deployer = await subtask.getDeployer() @@ -62,7 +63,10 @@ task('new-clrfund', 'Deploy a new instance of ClrFund') await subtask.finish(success) if (verify) { - console.log('Verify all contracts') - await hre.run('verify-all') + const clrfund = storage.getAddress(EContracts.ClrFund, hre.network.name) + if (clrfund) { + console.log('Verify all contracts') + await hre.run('verify-all', { clrfund }) + } } }) diff --git a/contracts/tasks/runners/newDeployer.ts b/contracts/tasks/runners/newDeployer.ts index 14157d4f8..e0023e9b2 100644 --- a/contracts/tasks/runners/newDeployer.ts +++ b/contracts/tasks/runners/newDeployer.ts @@ -15,7 +15,8 @@ import { task, types } from 'hardhat/config' import { Subtask } from '../helpers/Subtask' -import { type ISubtaskParams } from '../helpers/types' +import { ContractStorage } from '../helpers/ContractStorage' +import { EContracts, type ISubtaskParams } from '../helpers/types' task('new-deployer', 'Deploy a new instance of ClrFund') .addFlag('incremental', 'Incremental deployment') @@ -26,6 +27,7 @@ task('new-deployer', 'Deploy a new instance of ClrFund') .setAction(async (params: ISubtaskParams, hre) => { const { verify, manageNonce } = params const subtask = Subtask.getInstance(hre) + const storage = ContractStorage.getInstance() subtask.setHre(hre) const deployer = await subtask.getDeployer() @@ -63,7 +65,10 @@ task('new-deployer', 'Deploy a new instance of ClrFund') await subtask.finish(success) if (verify) { - console.log('Verify all contracts') - await hre.run('verify-all') + const address = storage.mustGetAddress( + EContracts.ClrFundDeployer, + hre.network.name + ) + await hre.run('verify-deployer', { address }) } }) diff --git a/contracts/tasks/runners/setToken.ts b/contracts/tasks/runners/setToken.ts index f9fc9272a..348aac137 100644 --- a/contracts/tasks/runners/setToken.ts +++ b/contracts/tasks/runners/setToken.ts @@ -18,12 +18,11 @@ import { type ISubtaskParams } from '../helpers/types' task('set-token', 'Set the token in ClrFund') .addFlag('incremental', 'Incremental deployment') .addFlag('strict', 'Fail on warnings') - .addFlag('verify', 'Verify contracts at Etherscan') .addFlag('manageNonce', 'Manually increment nonce for each transaction') .addOptionalParam('clrfund', 'The ClrFund contract address') .addOptionalParam('skip', 'Skip steps with less or equal index', 0, types.int) .setAction(async (params: ISubtaskParams, hre) => { - const { verify, manageNonce } = params + const { manageNonce } = params const subtask = Subtask.getInstance(hre) subtask.setHre(hre) @@ -53,9 +52,4 @@ task('set-token', 'Set the token in ClrFund') } await subtask.finish(success) - - if (verify) { - console.log('Verify all contracts') - await hre.run('verify-all') - } }) diff --git a/contracts/tasks/runners/verifyAll.ts b/contracts/tasks/runners/verifyAll.ts index ab4647563..6425bf5eb 100644 --- a/contracts/tasks/runners/verifyAll.ts +++ b/contracts/tasks/runners/verifyAll.ts @@ -1,108 +1,334 @@ /* eslint-disable no-console */ import { task } from 'hardhat/config' -import type { IStorageInstanceEntry, IVerifyAllArgs } from '../helpers/types' +import { EContracts } from '../helpers/types' import { ContractStorage } from '../helpers/ContractStorage' import { ContractVerifier } from '../helpers/ContractVerifier' +import { + BrightIdUserRegistry, + ClrFund, + MerkleUserRegistry, + SemaphoreUserRegistry, + SnapshotUserRegistry, +} from '../../typechain-types' +import { BaseContract } from 'ethers' +import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' +import { ZERO_ADDRESS } from '../../utils/constants' +import { ConstructorArguments } from '../helpers/ConstructorArguments' + +type ContractInfo = { + name: string + address: string +} + +type VerificationSummary = { + contract: string + ok: boolean + err?: string +} /** - * Main verification task which runs hardhat-etherscan task for all the deployed contract. + * Get the recipient registry contract name + * @param registryAddress The recipient registry contract address + * @param ethers The Hardhat Ethers helper + * @returns The recipient registry contract name */ -task('verify-all', 'Verify contracts listed in storage') - .addFlag('force', 'Ignore verified status') - .setAction(async ({ force = false }: IVerifyAllArgs, hre) => { - const storage = ContractStorage.getInstance() - const verifier = new ContractVerifier(hre) - const addressList: string[] = [] - const entryList: IStorageInstanceEntry[] = [] - let index = 0 +async function getRecipientRegistryName( + registryAddress: string, + ethers: HardhatEthersHelpers +): Promise { + try { + const contract = await ethers.getContractAt( + EContracts.KlerosGTCRAdapter, + registryAddress + ) + const tcr = await contract.tcr() + if (tcr === ZERO_ADDRESS) { + throw new Error( + 'Unexpected zero tcr from a Kleros recipient registry: ' + + registryAddress + ) + } + return EContracts.KlerosGTCRAdapter + } catch { + // not a kleros registry + } - const addEntry = (address: string, entry: IStorageInstanceEntry) => { - if (!entry.verify) { - return - } + // try optimistic + const contract = await ethers.getContractAt( + EContracts.OptimisticRecipientRegistry, + registryAddress + ) - addressList.push(address) - entryList.push(entry) - index += 1 - } + try { + await contract.challengePeriodDuration() + return EContracts.OptimisticRecipientRegistry + } catch { + // not optimistic, use simple registry + return EContracts.SimpleRecipientRegistry + } +} + +/** + * Get the user registry contract name + * @param registryAddress The user registry contract address + * @param ethers The Hardhat Ethers helper + * @returns The user registry contract name + */ +async function getUserRegistryName( + registryAddress: string, + ethers: HardhatEthersHelpers +): Promise { + try { + const contract = (await ethers.getContractAt( + EContracts.BrightIdUserRegistry, + registryAddress + )) as BaseContract as BrightIdUserRegistry + await contract.context() + return EContracts.BrightIdUserRegistry + } catch { + // not a BrightId user registry + } - const instances = storage.getInstances(hre.network.name) + // try semaphore user registry + try { + const contract = (await ethers.getContractAt( + EContracts.SemaphoreUserRegistry, + registryAddress + )) as BaseContract as SemaphoreUserRegistry + await contract.isVerifiedSemaphoreId(1) + return EContracts.SemaphoreUserRegistry + } catch { + // not a semaphore user registry + } - instances.forEach(([key, entry]) => { - if (entry.id.includes('Poseidon')) { - return - } + // try snapshot user regitry + try { + const contract = (await ethers.getContractAt( + EContracts.SnapshotUserRegistry, + registryAddress + )) as BaseContract as SnapshotUserRegistry + await contract.storageRoot() + } catch { + // not snapshot user registry + } + + // try merkle user regitry + try { + const contract = (await ethers.getContractAt( + EContracts.MerkleUserRegistry, + registryAddress + )) as BaseContract as MerkleUserRegistry + await contract.merkleRoot() + } catch { + // not merkle user registry + } - addEntry(key, entry) + return EContracts.SimpleUserRegistry +} + +/** + * Get the list of contracts to verify + * @param clrfund The ClrFund contract address + * @param ethers The Hardhat Ethers helper + * @param etherscanProvider The Etherscan provider + * @returns The list of contracts to verify + */ +async function getContractList( + clrfund: string, + ethers: HardhatEthersHelpers +): Promise { + const contractList: ContractInfo[] = [ + { + name: EContracts.ClrFund, + address: clrfund, + }, + ] + + const clrfundContract = (await ethers.getContractAt( + EContracts.ClrFund, + clrfund + )) as BaseContract as ClrFund + + const fundingRoundFactoryAddress = await clrfundContract.roundFactory() + if (fundingRoundFactoryAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.FundingRoundFactory, + address: fundingRoundFactoryAddress, }) + } - console.log( - '======================================================================' - ) - console.log( - '======================================================================' - ) - console.log( - `Verification batch with ${addressList.length} entries of ${index} total.` + const maciFactoryAddress = await clrfundContract.maciFactory() + if (maciFactoryAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.MACIFactory, + address: maciFactoryAddress, + }) + + const maciFactory = await ethers.getContractAt( + EContracts.MACIFactory, + maciFactoryAddress ) - console.log( - '======================================================================' + const vkRegistryAddress = await maciFactory.vkRegistry() + contractList.push({ + name: EContracts.VkRegistry, + address: vkRegistryAddress, + }) + + const factories = await maciFactory.factories() + contractList.push({ + name: EContracts.PollFactory, + address: factories.pollFactory, + }) + + contractList.push({ + name: EContracts.TallyFactory, + address: factories.tallyFactory, + }) + + contractList.push({ + name: EContracts.MessageProcessorFactory, + address: factories.messageProcessorFactory, + }) + } + + const fundingRoundAddress = await clrfundContract.getCurrentRound() + if (fundingRoundAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.FundingRound, + address: fundingRoundAddress, + }) + + const fundingRound = await ethers.getContractAt( + EContracts.FundingRound, + fundingRoundAddress ) - const summary: string[] = [] - for (let i = 0; i < addressList.length; i += 1) { - const address = addressList[i] - const entry = entryList[i] + const maciAddress = await fundingRound.maci() + if (maciAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.MACI, + address: maciAddress, + }) + } - const params = entry.verify + // Poll + const pollAddress = await fundingRound.poll() + if (pollAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.Poll, + address: pollAddress, + }) + } - console.log( - '\n======================================================================' + // Tally + const tallyAddress = await fundingRound.tally() + if (tallyAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.Tally, + address: tallyAddress, + }) + + // Verifier + const tallyContract = await ethers.getContractAt( + EContracts.Tally, + tallyAddress ) - console.log( - `[${i}/${addressList.length}] Verify contract: ${entry.id} ${address}` + const verifierAddress = await tallyContract.verifier() + if (verifierAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.Verifier, + address: verifierAddress, + }) + } + + // MessageProcessor + const messageProcessorAddress = await tallyContract.messageProcessor() + if (messageProcessorAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.MessageProcessor, + address: messageProcessorAddress, + }) + } + } + + // User Registry + const userRegistryAddress = await fundingRound.userRegistry() + if (userRegistryAddress !== ZERO_ADDRESS) { + const name = await getUserRegistryName(userRegistryAddress, ethers) + contractList.push({ + name, + address: userRegistryAddress, + }) + } + + // Recipient Registry + const recipientRegistryAddress = await fundingRound.recipientRegistry() + if (recipientRegistryAddress !== ZERO_ADDRESS) { + const name = await getRecipientRegistryName( + recipientRegistryAddress, + ethers ) - console.log('\tArgs:', params?.args) + contractList.push({ + name, + address: recipientRegistryAddress, + }) + } + } - const verifiedEntity = storage.getVerified(address, hre.network.name) + return contractList +} - if (!force && verifiedEntity) { - console.log('Already verified') - } else { +/** + * Main verification task which runs hardhat-etherscan task for all the deployed contract. + */ +task('verify-all', 'Verify contracts listed in storage') + .addOptionalParam('clrfund', 'The ClrFund contract address') + .addFlag('force', 'Ignore verified status') + .setAction(async ({ clrfund }, hre) => { + const { ethers, config, network } = hre + + const storage = ContractStorage.getInstance() + const clrfundContractAddress = + clrfund ?? storage.mustGetAddress(EContracts.ClrFund, network.name) + + const contractList = await getContractList(clrfundContractAddress, ethers) + const constructorArguments = new ConstructorArguments(hre) + const verifier = new ContractVerifier(hre) + const summary: VerificationSummary[] = [] + + for (let i = 0; i < contractList.length; i += 1) { + const { name, address } = contractList[i] + + try { + const args = await constructorArguments.get(name, address, ethers) let contract: string | undefined let libraries: string | undefined - if (entry.id === 'AnyOldERC20Token') { - contract = 'contracts/AnyOldERC20Token.sol:AnyOldERC20Token' - } - // eslint-disable-next-line no-await-in-loop const [ok, err] = await verifier.verify( address, - params?.args ?? '', + args, libraries, contract ) - if (ok) { - storage.setVerified(address, hre.network.name, true) - } else { - summary.push(`${address} ${entry.id}: ${err}`) - } + summary.push({ contract: `${address} ${name}`, ok, err }) + } catch (e) { + // error getting the constructors, skipping + summary.push({ + contract: `${address} ${name}`, + ok: false, + err: 'Failed to get constructor. ' + (e as Error).message, + }) } } - console.log( - '\n======================================================================' - ) - console.log( - `Verification batch has finished with ${summary.length} issue(s).` - ) - console.log( - '======================================================================' - ) - console.log(summary.join('\n')) - console.log( - '======================================================================' - ) + summary.forEach(({ contract, ok, err }, i) => { + const color = ok ? '32' : '31' + console.log( + `${i + 1} ${contract}: \x1b[%sm%s\x1b[0m`, + color, + ok ? 'ok' : err + ) + }) }) diff --git a/contracts/tasks/runners/verifyDeployer.ts b/contracts/tasks/runners/verifyDeployer.ts new file mode 100644 index 000000000..a2fad0cf6 --- /dev/null +++ b/contracts/tasks/runners/verifyDeployer.ts @@ -0,0 +1,34 @@ +import { task } from 'hardhat/config' +import { EContracts } from '../../utils/types' +import { EtherscanProvider } from '../../utils/providers/EtherscanProvider' +import { ContractVerifier } from '../helpers/ContractVerifier' +import { ConstructorArguments } from '../helpers/ConstructorArguments' + +/** + * Verifies the ClrFundDeployer contract + * - it constructs the constructor arguments by querying the ClrFundDeployer contract + * - it calls the etherscan hardhat plugin to verify the contract + */ +task('verify-deployer', 'Verify a ClrFundDeployer contract') + .addParam('address', 'ClrFundDeployer contract address') + .setAction(async ({ address }, hre) => { + const contractVerifier = new ContractVerifier(hre) + const getter = new ConstructorArguments(hre) + + const name = EContracts.ClrFundDeployer + const constructorArgument = await getter.get( + EContracts.ClrFundDeployer, + address, + hre.ethers + ) + const [ok, err] = await contractVerifier.verify( + address, + constructorArgument + ) + + console.log( + `${address} ${name}: \x1b[%sm%s\x1b[0m`, + ok ? 32 : 31, + ok ? 'ok' : err + ) + }) diff --git a/contracts/tests/maciFactory.ts b/contracts/tests/maciFactory.ts index 44296bbf9..3f23d58ad 100644 --- a/contracts/tests/maciFactory.ts +++ b/contracts/tests/maciFactory.ts @@ -1,4 +1,4 @@ -import { artifacts, ethers, config } from 'hardhat' +import { artifacts, ethers } from 'hardhat' import { Contract, TransactionResponse } from 'ethers' import { expect } from 'chai' import { deployMockContract, MockContract } from '@clrfund/waffle-mock-contract' diff --git a/contracts/utils/testutils.ts b/contracts/utils/testutils.ts index db63401ec..e5ace7da7 100644 --- a/contracts/utils/testutils.ts +++ b/contracts/utils/testutils.ts @@ -1,6 +1,6 @@ import { Signer, Contract } from 'ethers' import { MockContract, deployMockContract } from '@clrfund/waffle-mock-contract' -import { artifacts, ethers, config } from 'hardhat' +import { artifacts, ethers } from 'hardhat' import { MaciParameters } from './maciParameters' import { PubKey } from '@clrfund/common' import { deployContract, getEventArg, setVerifyingKeys } from './contracts' diff --git a/contracts/utils/types.ts b/contracts/utils/types.ts index ba8f753f4..a389bec65 100644 --- a/contracts/utils/types.ts +++ b/contracts/utils/types.ts @@ -27,6 +27,8 @@ export enum EContracts { IUserRegistry = 'IUserRegistry', SimpleUserRegistry = 'SimpleUserRegistry', SemaphoreUserRegistry = 'SemaphoreUserRegistry', + SnapshotUserRegistry = 'SnapshotUserRegistry', + MerkleUserRegistry = 'MerkleUserRegistry', BrightIdUserRegistry = 'BrightIdUserRegistry', AnyOldERC20Token = 'AnyOldERC20Token', BrightIdSponsor = 'BrightIdSponsor', From e7ded06adb82128bf59f694d6f76442baf20c324 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Tue, 16 Apr 2024 17:07:27 -0400 Subject: [PATCH 14/17] refactor etherscan provider to extract API key from custom config --- contracts/tasks/runners/exportRound.ts | 35 ++++-------- contracts/tasks/runners/verifyAll.ts | 1 - contracts/tasks/runners/verifyDeployer.ts | 1 - .../utils/RecipientRegistryLogProcessor.ts | 7 ++- contracts/utils/abi.ts | 11 ++++ .../utils/providers/EtherscanProvider.ts | 57 +++++++++++++++---- contracts/utils/providers/ProviderFactory.ts | 10 ++-- 7 files changed, 78 insertions(+), 44 deletions(-) diff --git a/contracts/tasks/runners/exportRound.ts b/contracts/tasks/runners/exportRound.ts index 4eadf9818..fa3e332b1 100644 --- a/contracts/tasks/runners/exportRound.ts +++ b/contracts/tasks/runners/exportRound.ts @@ -16,7 +16,7 @@ import { Contract, formatUnits, getNumber } from 'ethers' import { Ipfs } from '../../utils/ipfs' import { Project, Round, RoundFileContent } from '../../utils/types' import { RecipientRegistryLogProcessor } from '../../utils/RecipientRegistryLogProcessor' -import { getRecipientAddressAbi } from '../../utils/abi' +import { getRecipientAddressAbi, MaciV0Abi } from '../../utils/abi' import { JSONFile } from '../../utils/JSONFile' import path from 'path' import fs from 'fs' @@ -41,19 +41,6 @@ function roundListFileName(directory: string): string { return path.join(directory, 'rounds.json') } -function getEtherscanApiKey(config: any, network: string): string { - let etherscanApiKey = '' - if (config.etherscan?.apiKey) { - if (typeof config.etherscan.apiKey === 'string') { - etherscanApiKey = config.etherscan.apiKey - } else { - etherscanApiKey = config.etherscan.apiKey[network] - } - } - - return etherscanApiKey -} - function roundMapKey(round: RoundListEntry): string { return `${round.network}.${round.address}` } @@ -76,7 +63,10 @@ async function updateRoundList(filePath: string, round: RoundListEntry) { const rounds: RoundListEntry[] = Array.from(roundMap.values()) // sort in ascending start time order - rounds.sort((round1, round2) => round1.startTime - round2.startTime) + rounds.sort( + (round1, round2) => + getNumber(round1.startTime) - getNumber(round2.startTime) + ) JSONFile.write(filePath, rounds) console.log('Finished writing to', filePath) } @@ -137,7 +127,11 @@ async function mergeRecipientTally({ } const tallyResult = tally.results.tally[i] - const spentVoiceCredits = tally.perVOSpentVoiceCredits.tally[i] + + // In MACI V1, totalVoiceCreditsPerVoteOption is called perVOSpentVoiceCredits + const spentVoiceCredits = tally.perVOSpentVoiceCredits + ? tally.perVOSpentVoiceCredits.tally[i] + : tally.totalVoiceCreditsPerVoteOption.tally[i] const formattedDonationAmount = formatUnits( BigInt(spentVoiceCredits) * BigInt(voiceCreditFactor), nativeTokenDecimals @@ -237,7 +231,7 @@ async function getRoundInfo( maxMessages = maxValues.maxMessages maxRecipients = maxValues.maxVoteOptions } else { - const maci = await ethers.getContractAt('MACI', maciAddress) + const maci = await ethers.getContractAt(MaciV0Abi, maciAddress) startTime = await maci.signUpTimestamp().catch(toZero) signUpDuration = await maci.signUpDurationSeconds().catch(toZero) votingDuration = await maci.votingDurationSeconds().catch(toZero) @@ -352,11 +346,6 @@ task('export-round', 'Export round data for the leaderboard') console.log('Processing on ', network.name) console.log('Funding round address', roundAddress) - const etherscanApiKey = getEtherscanApiKey(config, network.name) - if (!etherscanApiKey) { - throw new Error('Etherscan API key not set') - } - const outputSubDir = path.join(outputDir, network.name) try { fs.statSync(outputSubDir) @@ -383,7 +372,7 @@ task('export-round', 'Export round data for the leaderboard') endBlock, blocksPerBatch, network: network.name, - etherscanApiKey, + config, }) console.log('Parsing logs...') diff --git a/contracts/tasks/runners/verifyAll.ts b/contracts/tasks/runners/verifyAll.ts index 6425bf5eb..c010df8ff 100644 --- a/contracts/tasks/runners/verifyAll.ts +++ b/contracts/tasks/runners/verifyAll.ts @@ -132,7 +132,6 @@ async function getUserRegistryName( * Get the list of contracts to verify * @param clrfund The ClrFund contract address * @param ethers The Hardhat Ethers helper - * @param etherscanProvider The Etherscan provider * @returns The list of contracts to verify */ async function getContractList( diff --git a/contracts/tasks/runners/verifyDeployer.ts b/contracts/tasks/runners/verifyDeployer.ts index a2fad0cf6..284689063 100644 --- a/contracts/tasks/runners/verifyDeployer.ts +++ b/contracts/tasks/runners/verifyDeployer.ts @@ -1,6 +1,5 @@ import { task } from 'hardhat/config' import { EContracts } from '../../utils/types' -import { EtherscanProvider } from '../../utils/providers/EtherscanProvider' import { ContractVerifier } from '../helpers/ContractVerifier' import { ConstructorArguments } from '../helpers/ConstructorArguments' diff --git a/contracts/utils/RecipientRegistryLogProcessor.ts b/contracts/utils/RecipientRegistryLogProcessor.ts index 615ab1cb0..6d74a4334 100644 --- a/contracts/utils/RecipientRegistryLogProcessor.ts +++ b/contracts/utils/RecipientRegistryLogProcessor.ts @@ -7,6 +7,7 @@ import { Log } from './providers/BaseProvider' import { toDate } from './date' import { EVENT_ABIS } from './abi' import { AbiInfo } from './types' +import { HardhatConfig } from 'hardhat/types' function getFilter(address: string, abiInfo: AbiInfo): EventFilter { const eventInterface = new Interface([abiInfo.abi]) @@ -41,14 +42,14 @@ export class RecipientRegistryLogProcessor { endBlock, startBlock, blocksPerBatch, - etherscanApiKey, + config, network, }: { recipientRegistry: Contract startBlock: number endBlock: number blocksPerBatch: number - etherscanApiKey: string + config: HardhatConfig network: string }): Promise { // fetch event logs containing project information @@ -64,7 +65,7 @@ export class RecipientRegistryLogProcessor { const logProvider = ProviderFactory.createProvider({ network, - etherscanApiKey, + config, }) let logs: Log[] = [] diff --git a/contracts/utils/abi.ts b/contracts/utils/abi.ts index 98101978a..dc3c0c004 100644 --- a/contracts/utils/abi.ts +++ b/contracts/utils/abi.ts @@ -6,6 +6,17 @@ type EventAbiEntry = { remove: AbiInfo } +/** + * MACI v0 ABI used in exportRound.ts + */ +export const MaciV0Abi = [ + 'function signUpTimestamp() view returns (uint256)', + 'function signUpDurationSeconds() view returns (uint256)', + 'function votingDurationSeconds() view returns (uint256)', + `function treeDepths() view returns ((uint8 stateTreeDepth, uint8 messageTreeDepth, uint8 voteOptionTreeDepth))`, + 'function numMessages() view returns (uint256)', +] + export const getRecipientAddressAbi = [ `function getRecipientAddress(uint256 _index, uint256 _startTime, uint256 _endTime)` + ` external view returns (address)`, diff --git a/contracts/utils/providers/EtherscanProvider.ts b/contracts/utils/providers/EtherscanProvider.ts index 5a35ac557..8534156ae 100644 --- a/contracts/utils/providers/EtherscanProvider.ts +++ b/contracts/utils/providers/EtherscanProvider.ts @@ -1,5 +1,6 @@ import { BaseProvider, FetchLogArgs, Log } from './BaseProvider' import { FetchRequest } from 'ethers' +import { HardhatConfig } from 'hardhat/types' const EtherscanApiUrl: Record = { xdai: 'https://api.gnosisscan.io', @@ -10,14 +11,57 @@ const EtherscanApiUrl: Record = { 'optimism-sepolia': 'https://api-sepolia-optimistic.etherscan.io', } +/** + * Mapping of the hardhat network name to the Etherscan network name in the hardhat.config + */ +const EtherscanNetworks: Record = { + arbitrum: 'arbitrumOne', + optimism: 'optimisticEthereum', +} + +/** + * The the Etherscan API key from the hardhat.config file + * @param config The Hardhat config object + * @param network The Hardhat network name + * @returns The Etherscan API key + */ +function getEtherscanApiKey(config: HardhatConfig, network: string): string { + let etherscanApiKey = '' + if (config.etherscan?.apiKey) { + if (typeof config.etherscan.apiKey === 'string') { + etherscanApiKey = config.etherscan.apiKey + } else { + const etherscanNetwork = EtherscanNetworks[network] ?? network + etherscanApiKey = config.etherscan.apiKey[etherscanNetwork] + } + } + + return etherscanApiKey +} + export class EtherscanProvider extends BaseProvider { apiKey: string network: string + baseUrl: string - constructor(apiKey: string, network: string) { + constructor(config: HardhatConfig, network: string) { super() - this.apiKey = apiKey + + const etherscanApiKey = getEtherscanApiKey(config, network) + if (!etherscanApiKey) { + throw new Error(`Etherscan API key is not found for ${network}`) + } + + const etherscanBaseUrl = EtherscanApiUrl[network] + if (!etherscanBaseUrl) { + throw new Error( + `Network ${network} is not supported in etherscan fetch log api` + ) + } + this.network = network + this.apiKey = etherscanApiKey + this.baseUrl = etherscanBaseUrl } async fetchLogs({ @@ -25,17 +69,10 @@ export class EtherscanProvider extends BaseProvider { startBlock, lastBlock, }: FetchLogArgs): Promise { - const baseUrl = EtherscanApiUrl[this.network] - if (!baseUrl) { - throw new Error( - `Network ${this.network} is not supported in etherscan fetch log api` - ) - } - const topic0 = filter.topics?.[0] || '' const toBlockQuery = lastBlock ? `&toBlock=${lastBlock}` : '' const url = - `${baseUrl}/api?module=logs&action=getLogs&address=${filter.address}` + + `${this.baseUrl}/api?module=logs&action=getLogs&address=${filter.address}` + `&topic0=${topic0}&fromBlock=${startBlock}${toBlockQuery}&apikey=${this.apiKey}` const req = new FetchRequest(url) diff --git a/contracts/utils/providers/ProviderFactory.ts b/contracts/utils/providers/ProviderFactory.ts index 6a79ad0ec..b7c5474d2 100644 --- a/contracts/utils/providers/ProviderFactory.ts +++ b/contracts/utils/providers/ProviderFactory.ts @@ -1,17 +1,15 @@ +import { HardhatConfig } from 'hardhat/types' import { BaseProvider } from './BaseProvider' import { EtherscanProvider } from './EtherscanProvider' export type CreateProviderArgs = { network: string - etherscanApiKey: string + config: HardhatConfig } export class ProviderFactory { - static createProvider({ - network, - etherscanApiKey, - }: CreateProviderArgs): BaseProvider { + static createProvider({ network, config }: CreateProviderArgs): BaseProvider { // use etherscan provider only as JsonRpcProvider is not reliable - return new EtherscanProvider(etherscanApiKey, network) + return new EtherscanProvider(config, network) } } From 6dfaeb476b51cca488f42945a86be0b4f0b2aa13 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Wed, 17 Apr 2024 11:55:00 -0400 Subject: [PATCH 15/17] Refactor modal and not allow close on click or esc. --- vue-app/src/App.vue | 1 + vue-app/src/components.d.ts | 1 + vue-app/src/components/BaseModal.vue | 9 +++++++++ vue-app/src/components/Cart.vue | 2 +- vue-app/src/components/ClaimModal.vue | 6 ++---- vue-app/src/components/ContributionModal.vue | 6 ++---- vue-app/src/components/ErrorModal.vue | 6 ++---- vue-app/src/components/MatchingFundsModal.vue | 5 ++--- vue-app/src/components/ReallocationModal.vue | 6 ++---- vue-app/src/components/SignatureModal.vue | 6 ++---- vue-app/src/components/TransactionModal.vue | 6 ++---- vue-app/src/components/WalletModal.vue | 5 ++--- vue-app/src/components/WithdrawalModal.vue | 6 ++---- 13 files changed, 30 insertions(+), 35 deletions(-) create mode 100644 vue-app/src/components/BaseModal.vue diff --git a/vue-app/src/App.vue b/vue-app/src/App.vue index 019d15523..923ff52d0 100644 --- a/vue-app/src/App.vue +++ b/vue-app/src/App.vue @@ -486,6 +486,7 @@ summary:focus { padding: $modal-space; text-align: center; box-shadow: var(--box-shadow); + border: 2px solid rgba(115, 117, 166, 0.3); width: 400px; .loader { margin: $modal-space auto; diff --git a/vue-app/src/components.d.ts b/vue-app/src/components.d.ts index 1cef45f0e..d4d8a573b 100644 --- a/vue-app/src/components.d.ts +++ b/vue-app/src/components.d.ts @@ -11,6 +11,7 @@ declare module '@vue/runtime-core' { AddToCartButton: typeof import('./components/AddToCartButton.vue')['default'] BackLink: typeof import('./components/BackLink.vue')['default'] BalanceItem: typeof import('./components/BalanceItem.vue')['default'] + BaseModal: typeof import('./components/BaseModal.vue')['default'] Breadcrumbs: typeof import('./components/Breadcrumbs.vue')['default'] BrightIdWidget: typeof import('./components/BrightIdWidget.vue')['default'] CallToActionCard: typeof import('./components/CallToActionCard.vue')['default'] diff --git a/vue-app/src/components/BaseModal.vue b/vue-app/src/components/BaseModal.vue new file mode 100644 index 000000000..fef021f8b --- /dev/null +++ b/vue-app/src/components/BaseModal.vue @@ -0,0 +1,9 @@ + + + diff --git a/vue-app/src/components/Cart.vue b/vue-app/src/components/Cart.vue index 0169f6fc3..cb10c454a 100644 --- a/vue-app/src/components/Cart.vue +++ b/vue-app/src/components/Cart.vue @@ -605,7 +605,7 @@ function submitCart(event: any) { } const canWithdrawContribution = computed( - () => currentRound.value?.status === RoundStatus.Cancelled && !contribution.value, + () => currentRound.value?.status === RoundStatus.Cancelled && contribution.value > 0n, ) const showCollapseCart = computed(() => route.name !== 'cart') diff --git a/vue-app/src/components/ClaimModal.vue b/vue-app/src/components/ClaimModal.vue index d6c2cd29c..9f0b3f2e3 100644 --- a/vue-app/src/components/ClaimModal.vue +++ b/vue-app/src/components/ClaimModal.vue @@ -1,5 +1,5 @@