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/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/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)
- })
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..d1bcf92da
--- /dev/null
+++ b/contracts/tasks/tally.ts
@@ -0,0 +1,245 @@
+import { task, types } from 'hardhat/config'
+import fs from 'fs'
+import { Contract, Wallet } from 'ethers'
+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
+ 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.json file
+ console.log(`Tally hash is ${publishedTallyHash}`)
+ try {
+ 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
+ }
+ }
+
+ // 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'
+ )
+ .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 || process.env.WALLET_PRIVATE_KEY || ''
+ 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's wallet private key`
+ )
+ }
+
+ if (!coordinatorMaciPrivKey) {
+ throw Error(
+ `Please set the environment variable COORDINATOR_PK, the coordinator's MACI private key`
+ )
+ }
+ }
+
+ if (!fundingRoundAddress) {
+ throw Error(`The '--round-address' parameter 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: Number(voteOptionTreeDepth),
+ 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
}
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/)**