From 9a3318391d3d8118ea02d76257a1fab8c7bc36aa Mon Sep 17 00:00:00 2001 From: yuetloo Date: Mon, 31 Jul 2023 13:45:02 -0400 Subject: [PATCH 1/7] refactor tally task to allow re-run --- contracts/package.json | 2 +- contracts/tasks/index.ts | 1 + contracts/tasks/tally.ts | 228 +++++++++++++++++++++++++++++++++++++++ contracts/utils/ipfs.ts | 4 +- 4 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 contracts/tasks/tally.ts diff --git a/contracts/package.json b/contracts/package.json index 44c9083a3..9afd783a1 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -11,7 +11,7 @@ "deployTestRound:local": "hardhat run --network localhost scripts/deployTestRound.ts", "contribute:local": "hardhat run --network localhost scripts/contribute.ts", "vote:local": "hardhat run --network localhost scripts/vote.ts", - "tally:local": "hardhat run --network localhost scripts/tally.ts", + "tally:local": "hardhat --network localhost tally", "finalize:local": "hardhat run --network localhost scripts/finalize.ts", "claim:local": "hardhat run --network localhost scripts/claim.ts", "test": "hardhat test", diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index 6f4bfc5e2..c37ce2f42 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -13,3 +13,4 @@ import './mergeAllocations' import './setDurations' import './deploySponsor' import './loadUsers' +import './tally' diff --git a/contracts/tasks/tally.ts b/contracts/tasks/tally.ts new file mode 100644 index 000000000..2c3244b8b --- /dev/null +++ b/contracts/tasks/tally.ts @@ -0,0 +1,228 @@ +import { task, types } from 'hardhat/config' +import fs from 'fs' +import { Contract, Wallet } from 'ethers' +import { genProofs, proveOnChain, fetchLogs } from 'maci-cli' + +import { getIpfsHash, Ipfs } from '../utils/ipfs' +import { addTallyResultsBatch } from '../utils/maci' + +type TallyArgs = { + fundingRound: Contract + coordinatorMaciPrivKey: string + coordinator: Wallet + startBlock: number + numBlocksPerRequest: number + batchSize: number + logsFile: string + maciStateFile: string + providerUrl: string + voteOptionTreeDepth: number +} + +async function main(args: TallyArgs) { + const { + fundingRound, + coordinatorMaciPrivKey, + coordinator, + batchSize, + logsFile, + maciStateFile, + providerUrl, + voteOptionTreeDepth, + } = args + + console.log('funding round address', fundingRound.address) + const maciAddress = await fundingRound.maci() + console.log('maci address', maciAddress) + + const publishedTallyHash = await fundingRound.tallyHash() + + let tally + + if (!publishedTallyHash) { + // Process messages and tally votes + const results = await genProofs({ + contract: maciAddress, + eth_provider: providerUrl, + privkey: coordinatorMaciPrivKey, + tally_file: 'tally.json', + output: 'proofs.json', + logs_file: logsFile, + macistate: maciStateFile, + }) + if (!results) { + throw new Error('generation of proofs failed') + } + const { proofs } = results + tally = results.tally + + // Submit proofs to MACI contract + await proveOnChain({ + contract: maciAddress, + eth_privkey: coordinator.privateKey, + eth_provider: providerUrl, + privkey: coordinatorMaciPrivKey, + proof_file: proofs, + }) + + // Publish tally hash + const tallyHash = await getIpfsHash(tally) + await fundingRound.publishTallyHash(tallyHash) + console.log(`Tally hash is ${tallyHash}`) + } else { + // read the tally file from ipfs + try { + tally = await Ipfs.fetchJson(publishedTallyHash) + } catch (err) { + console.log('Failed to get tally file', publishedTallyHash, err) + throw err + } + } + + // Submit results to the funding round contract + const startIndex = await fundingRound.totalTallyResults() + const total = tally.results.tally.length + console.log('Uploading tally results in batches of', batchSize) + const addTallyGas = await addTallyResultsBatch( + fundingRound, + voteOptionTreeDepth, + tally, + batchSize, + startIndex.toNumber(), + (processed: number) => { + console.log(`Processed ${processed} / ${total}`) + } + ) + console.log('Tally results uploaded. Gas used:', addTallyGas.toString()) +} + +task('tally', 'Tally votes for the current round') + .addParam( + 'roundAddress', + 'The funding round contract address', + '', + types.string + ) + .addParam( + 'batchSize', + 'Number of tally result to submit on chain per batch', + 20, + types.int + ) + .addParam( + 'numBlocksPerRequest', + 'The number of blocks to fetch for each get log request', + 200000, + types.int + ) + .addParam( + 'startBlock', + 'The first block containing the MACI events', + 0, + types.int + ) + .addOptionalParam('maciLogs', 'The file path containing the MACI logs') + .addOptionalParam( + 'maciStateFile', + 'The MACI state file, genProof will continue from it last run' + ) + .addOptionalParam( + 'ipfsGateway', + 'The IPFS gateway url', + 'https://ipfs.io', + types.string + ) + .setAction( + async ( + { + roundAddress, + maciLogs, + maciStateFile, + batchSize, + startBlock, + numBlocksPerRequest, + }, + { ethers, network } + ) => { + let fundingRoundAddress = roundAddress + let coordinatorMaciPrivKey = process.env.COORDINATOR_PK || '' + let coordinatorEthPrivKey = process.env.COORDINATOR_ETH_PK || '' + const providerUrl = (network.config as any).url + + if (network.name === 'localhost') { + const stateStr = fs.readFileSync('state.json').toString() + const state = JSON.parse(stateStr) + fundingRoundAddress = state.fundingRound + coordinatorMaciPrivKey = state.coordinatorPrivKey + // default to the first account + coordinatorEthPrivKey = coordinatorEthPrivKey + ? coordinatorEthPrivKey + : '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + } else { + if (!coordinatorEthPrivKey) { + throw Error( + 'Please set the environment variable COORDINATOR_ETH_PK, the coordinator private key' + ) + } + + if (!coordinatorMaciPrivKey) { + throw Error( + 'Please set the environment variable COORDINATOR_PK, the MACI private key' + ) + } + } + + if (!fundingRoundAddress) { + throw Error('round-address is required') + } + + console.log('Funding round address: ', fundingRoundAddress) + const coordinator = new Wallet(coordinatorEthPrivKey, ethers.provider) + console.log('Coordinator address: ', coordinator.address) + + const fundingRound = await ethers.getContractAt( + 'FundingRound', + fundingRoundAddress, + coordinator + ) + + const maciAddress = await fundingRound.maci() + const maci = await ethers.getContractAt('MACI', maciAddress, coordinator) + const [, , voteOptionTreeDepth] = await maci.treeDepths() + console.log('Vote option tree depth', voteOptionTreeDepth) + + const timeMs = new Date().getTime() + const logsFile = maciLogs ? maciLogs : `maci_logs_${timeMs}.json` + if (!maciLogs) { + const maciAddress = await fundingRound.maci() + console.log('maci address', maciAddress) + + // Fetch Maci logs + console.log('Fetching MACI logs from block', startBlock) + await fetchLogs({ + contract: maciAddress, + eth_provider: (network.config as any).url, + privkey: coordinatorMaciPrivKey, + start_block: startBlock, + num_blocks_per_request: numBlocksPerRequest, + output: logsFile, + }) + console.log('MACI logs generated at', logsFile) + } + + await main({ + fundingRound, + coordinatorMaciPrivKey, + coordinator, + startBlock, + numBlocksPerRequest, + batchSize, + voteOptionTreeDepth: voteOptionTreeDepth.toNumber(), + logsFile, + providerUrl, + maciStateFile: maciStateFile + ? maciStateFile + : `maci_state_${timeMs}.json`, + }) + } + ) diff --git a/contracts/utils/ipfs.ts b/contracts/utils/ipfs.ts index 33c8ce6c0..09e74c918 100644 --- a/contracts/utils/ipfs.ts +++ b/contracts/utils/ipfs.ts @@ -10,8 +10,8 @@ export async function getIpfsHash(object: any): Promise { } export class Ipfs { - static async fetchJson(hash: string): Promise { - const url = `${IPFS_BASE_URL}/ipfs/${hash}` + static async fetchJson(hash: string, gatewayUrl?: string): Promise { + const url = `${gatewayUrl || IPFS_BASE_URL}/ipfs/${hash}` const result = utils.fetchJson(url) return result } From 861a896e2c6f8e52964c9958b496f9701196726f Mon Sep 17 00:00:00 2001 From: yuetloo Date: Mon, 31 Jul 2023 16:43:47 -0400 Subject: [PATCH 2/7] fix number conversion error --- contracts/tasks/tally.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/tasks/tally.ts b/contracts/tasks/tally.ts index 2c3244b8b..fe6ab6729 100644 --- a/contracts/tasks/tally.ts +++ b/contracts/tasks/tally.ts @@ -217,7 +217,7 @@ task('tally', 'Tally votes for the current round') startBlock, numBlocksPerRequest, batchSize, - voteOptionTreeDepth: voteOptionTreeDepth.toNumber(), + voteOptionTreeDepth: Number(voteOptionTreeDepth), logsFile, providerUrl, maciStateFile: maciStateFile From 9f69695f8df8c78626dd673b0451689e334f43c4 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Tue, 1 Aug 2023 12:09:56 -0400 Subject: [PATCH 3/7] update finalize action to use tally task --- .github/workflows/finalize-round.yml | 2 +- contracts/tasks/tally.ts | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/finalize-round.yml b/.github/workflows/finalize-round.yml index 4183c5a45..d1e8e5219 100644 --- a/.github/workflows/finalize-round.yml +++ b/.github/workflows/finalize-round.yml @@ -66,7 +66,7 @@ jobs: echo "MACI_START_BLOCK:" $MACI_START_BLOCK # tally and finalize cd contracts - yarn hardhat run --network "${NETWORK}" scripts/tally.ts + yarn hardhat tally --round-address "${ROUND_ADDRESS}" --network "${NETWORK}" curl --location --request POST 'https://api.pinata.cloud/pinning/pinFileToIPFS' \ --header "Authorization: Bearer ${{ secrets.PINATA_JWT }}" \ --form 'file=@"tally.json"' diff --git a/contracts/tasks/tally.ts b/contracts/tasks/tally.ts index fe6ab6729..69b1ef285 100644 --- a/contracts/tasks/tally.ts +++ b/contracts/tasks/tally.ts @@ -146,7 +146,8 @@ task('tally', 'Tally votes for the current round') ) => { let fundingRoundAddress = roundAddress let coordinatorMaciPrivKey = process.env.COORDINATOR_PK || '' - let coordinatorEthPrivKey = process.env.COORDINATOR_ETH_PK || '' + let coordinatorEthPrivKey = + process.env.COORDINATOR_ETH_PK || process.env.WALLET_PRIVATE_KEY || '' const providerUrl = (network.config as any).url if (network.name === 'localhost') { @@ -161,19 +162,19 @@ task('tally', 'Tally votes for the current round') } else { if (!coordinatorEthPrivKey) { throw Error( - 'Please set the environment variable COORDINATOR_ETH_PK, the coordinator private key' + `Please set the environment variable COORDINATOR_ETH_PK, the coordinator's wallet private key` ) } if (!coordinatorMaciPrivKey) { throw Error( - 'Please set the environment variable COORDINATOR_PK, the MACI private key' + `Please set the environment variable COORDINATOR_PK, the coordinator's MACI private key` ) } } if (!fundingRoundAddress) { - throw Error('round-address is required') + throw Error(`The '--round-address' parameter is required`) } console.log('Funding round address: ', fundingRoundAddress) From f43e4ef30522068a4207ab9ed4c29b9f5158e4b9 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Tue, 1 Aug 2023 22:05:29 -0400 Subject: [PATCH 4/7] read tally.json locally --- contracts/tasks/tally.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/contracts/tasks/tally.ts b/contracts/tasks/tally.ts index 69b1ef285..c2106cb1b 100644 --- a/contracts/tasks/tally.ts +++ b/contracts/tasks/tally.ts @@ -3,7 +3,7 @@ import fs from 'fs' import { Contract, Wallet } from 'ethers' import { genProofs, proveOnChain, fetchLogs } from 'maci-cli' -import { getIpfsHash, Ipfs } from '../utils/ipfs' +import { getIpfsHash } from '../utils/ipfs' import { addTallyResultsBatch } from '../utils/maci' type TallyArgs = { @@ -70,9 +70,12 @@ async function main(args: TallyArgs) { await fundingRound.publishTallyHash(tallyHash) console.log(`Tally hash is ${tallyHash}`) } else { - // read the tally file from ipfs + // read the tally.json file + console.log(`Tally hash is ${publishedTallyHash}`) try { - tally = await Ipfs.fetchJson(publishedTallyHash) + console.log(`Reading tally.json file...`) + const tallyStr = fs.readFileSync('tally.json').toString() + tally = JSON.parse(tallyStr) } catch (err) { console.log('Failed to get tally file', publishedTallyHash, err) throw err @@ -126,12 +129,6 @@ task('tally', 'Tally votes for the current round') 'maciStateFile', 'The MACI state file, genProof will continue from it last run' ) - .addOptionalParam( - 'ipfsGateway', - 'The IPFS gateway url', - 'https://ipfs.io', - types.string - ) .setAction( async ( { From 29c943393bfd99ad8a9fe52f7bb7b41d5f0916b3 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Tue, 1 Aug 2023 22:06:00 -0400 Subject: [PATCH 5/7] remove obsolete file --- contracts/scripts/tally.ts | 123 ------------------------------------- 1 file changed, 123 deletions(-) delete mode 100644 contracts/scripts/tally.ts diff --git a/contracts/scripts/tally.ts b/contracts/scripts/tally.ts deleted file mode 100644 index 329577d76..000000000 --- a/contracts/scripts/tally.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -import fs from 'fs' -import { network, ethers } from 'hardhat' -import { Wallet } from 'ethers' -import { genProofs, proveOnChain, fetchLogs } from 'maci-cli' - -import { getIpfsHash } from '../utils/ipfs' -import { addTallyResultsBatch } from '../utils/maci' - -async function main() { - let fundingRoundAddress: string - let coordinatorPrivKey: string - let coordinatorEthPrivKey: string - let startBlock = 0 - let numBlocksPerRequest = 20000 - const batchSize = Number(process.env.TALLY_BATCH_SIZE) || 20 - if (network.name === 'localhost') { - const stateStr = fs.readFileSync('state.json').toString() - const state = JSON.parse(stateStr) - fundingRoundAddress = state.fundingRound - coordinatorPrivKey = state.coordinatorPrivKey - // default to the first account - coordinatorEthPrivKey = - process.env.COORDINATOR_ETH_PK || - '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' - } else { - fundingRoundAddress = process.env.ROUND_ADDRESS || '' - coordinatorPrivKey = process.env.COORDINATOR_PK || '' - coordinatorEthPrivKey = process.env.COORDINATOR_ETH_PK || '' - numBlocksPerRequest = - Number(process.env.NUM_BLOCKS_PER_REQUEST) || numBlocksPerRequest - - if (process.env.MACI_START_BLOCK) { - startBlock = Number(process.env.MACI_START_BLOCK) - } else { - throw new Error( - 'Please set MACI_START_BLOCK environment variable for fetchLogs' - ) - } - } - - const timeMs = new Date().getTime() - const maciStateFile = `maci_state_${timeMs}.json` - const logsFile = `maci_logs_${timeMs}.json` - const coordinator = new Wallet(coordinatorEthPrivKey, ethers.provider) - const fundingRound = await ethers.getContractAt( - 'FundingRound', - fundingRoundAddress, - coordinator - ) - console.log('funding round address', fundingRound.address) - const maciAddress = await fundingRound.maci() - console.log('maci address', maciAddress) - const providerUrl = (network.config as any).url - - // Fetch Maci logs - console.log('Fetching MACI logs from block', startBlock) - await fetchLogs({ - contract: maciAddress, - eth_provider: providerUrl, - privkey: coordinatorPrivKey, - start_block: startBlock, - num_blocks_per_request: numBlocksPerRequest, - output: logsFile, - }) - console.log('MACI logs generated at', logsFile) - - // Process messages and tally votes - const results = await genProofs({ - contract: maciAddress, - eth_provider: providerUrl, - privkey: coordinatorPrivKey, - tally_file: 'tally.json', - output: 'proofs.json', - logs_file: logsFile, - macistate: maciStateFile, - }) - if (!results) { - throw new Error('generation of proofs failed') - } - const { proofs, tally } = results - - // Submit proofs to MACI contract - await proveOnChain({ - contract: maciAddress, - eth_privkey: coordinatorEthPrivKey, - eth_provider: providerUrl, - privkey: coordinatorPrivKey, - proof_file: proofs, - }) - - // Publish tally hash - const tallyHash = await getIpfsHash(tally) - await fundingRound.publishTallyHash(tallyHash) - console.log(`Tally hash is ${tallyHash}`) - - // Submit results to the funding round contract - const maci = await ethers.getContractAt('MACI', maciAddress, coordinator) - const [, , voteOptionTreeDepth] = await maci.treeDepths() - console.log('Vote option tree depth', voteOptionTreeDepth) - - const startIndex = await fundingRound.totalTallyResults() - const total = tally.results.tally.length - console.log('Uploading tally results in batches of', batchSize) - const addTallyGas = await addTallyResultsBatch( - fundingRound, - voteOptionTreeDepth, - tally, - batchSize, - startIndex.toNumber(), - (processed: number) => { - console.log(`Processed ${processed} / ${total}`) - } - ) - console.log('Tally results uploaded. Gas used:', addTallyGas.toString()) -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exit(1) - }) From 25b8de60b1bc5a652e8d6650e36cf5e330ff1d05 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Tue, 1 Aug 2023 22:27:03 -0400 Subject: [PATCH 6/7] update readme on how to run the tally task --- docs/tally-verify.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/tally-verify.md b/docs/tally-verify.md index e62ecce4d..07ef68724 100644 --- a/docs/tally-verify.md +++ b/docs/tally-verify.md @@ -24,6 +24,9 @@ cd circuits/params chmod u+x qvt32 batchUst32 ``` +Or, run the script monorepo/.github/scripts/download-batch64-params.sh to download the parameter files. + + The contract deployment scripts, `deploy*.ts` in the [clrfund repository](https://github.com/clrfund/monorepo/tree/develop/contracts/scripts) currently use the `batch 64` circuits, if you want to use a smaller size circuits, you can find them [here](../contracts/contracts/snarkVerifiers/README.md). You will need to update the deploy script to call `deployMaciFactory()` with your circuit and redeploy the contracts. ``` @@ -166,26 +169,34 @@ cd snark-params chmod u+x qvt32 batchUst32 ``` +Or, run the script monorepo/.github/scripts/download-batch64-params.sh to download the parameter files. + + + Set the path to downloaded parameter files and also the path to `zkutil` binary (if needed): ``` -export NODE_CONFIG='{"snarkParamsPath": "../../../contracts/snark-params/", "zkutil_bin": "/usr/bin/zkutil"}' +export NODE_CONFIG='{"snarkParamsPath": "path-to/snark-params/", "zkutil_bin": "/usr/bin/zkutil"}' ``` Set the following env vars in `.env`: ``` -ROUND_ADDRESS= +# private key for decrypting messages COORDINATOR_PK= + +# private key for interacting with contracts COORDINATOR_ETH_PK= ``` Decrypt messages and tally the votes: ``` -yarn hardhat run --network {network} scripts/tally.ts +yarn hardhat tally --network {network} --round-address {funding-round-address} --start-block {maci-contract-start-block} ``` +If there's error and the tally task was stopped prematurely, it can be resumed by passing 2 additional parameters, '--maci-logs' and/or '--maci-state-file', if the files were generated. + Result will be saved to `tally.json` file, which must then be published via IPFS. **Using [command line](https://docs.ipfs.io/reference/cli/)** From 31c46ab611840d52c90fe47d3b8bad64ba611d32 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Wed, 2 Aug 2023 15:43:03 -0400 Subject: [PATCH 7/7] add usage comment in the task --- contracts/tasks/tally.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contracts/tasks/tally.ts b/contracts/tasks/tally.ts index c2106cb1b..d1bcf92da 100644 --- a/contracts/tasks/tally.ts +++ b/contracts/tasks/tally.ts @@ -6,6 +6,25 @@ import { genProofs, proveOnChain, fetchLogs } from 'maci-cli' import { getIpfsHash } from '../utils/ipfs' import { addTallyResultsBatch } from '../utils/maci' +/** + * Tally votes for the specified funding round. This task can be rerun by + * passing in additional parameters: --maci-logs, --maci-state-file + * + * Make sure to set the following environment variables in the .env file + * if not running test using the localhost network + * 1) COORDINATOR_ETH_PK - coordinator's wallet private key to interact with contracts + * 2) COORDINATOR_PK - coordinator's MACI private key to decrypt messages + * + * Sample usage: + * + * yarn hardhat tally --round-address
--start-block --network + * + * To rerun: + * + * yarn hardhat tally --round-address
--network \ + * --maci-logs --maci-state-file + */ + type TallyArgs = { fundingRound: Contract coordinatorMaciPrivKey: string