-
Notifications
You must be signed in to change notification settings - Fork 0
added verifier contract and keygen, setup, prove utils and scripts #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: multi-schnorr
Are you sure you want to change the base?
Conversation
anxolin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fleupold
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great proof of concept!
A few thoughts
- I think the repo can be public
- Could you add a devcontainer + readme with sample commands (to avoid setup difficulties)
- I think we should differentiate more clearly between setup (doesn't have to happen every time) and proof generation (has to happen every time)
gnark/multi-schnorr/prove.sh
Outdated
| --private-key) PK="$2"; shift 2 ;; | ||
| --verifier) VERIFIER="$2"; shift 2 ;; | ||
| --msg) MSG="$2"; shift 2 ;; | ||
| --maxK) MAXK="$2"; shift 2 ;; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed here? Shouldn't this be part of the circuit and not variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is just left in the case, we want to change the depth in the circuit any day, but agree, can be hardcoded somewhere and then change when you change the depth.
gnark/multi-schnorr/keygen.sh
Outdated
| --num-validators) NUM_VALIDATORS="$2"; shift 2 ;; | ||
| --maxK) MAXK="$2"; shift 2 ;; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the depth of the tree is hardcoded, I think we should not have to specify these values here. MaxK should always be 2**depth and num-validators 2/3 of K.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for this was modularity. If we want to increase the maxK in the circuit any day, it can be done easily.
The num-validators necessarily don't need to be 2/3 of MaxK as it is the number of validator keys you want to generate, not the number of validators who sign. So, if a system only have 56 validators right now, for MaxK = 64, you can have 56 validator keys (rest are just padded keys, that are being used for building the merkle root) and some out of them sign the bids, which is then maybe 2/3 of the validators being present in the system (i.e threshold, which you can change in the contract anytime validators enter/exit)
| --rpc-url) RPC_URL="$2"; shift 2 ;; | ||
| --private-key) PK="$2"; shift 2 ;; | ||
| --verifier) VERIFIER="$2"; shift 2 ;; | ||
| --msg) MSG="$2"; shift 2 ;; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Message should probably an arbitrary long message (not a single point). I would like to see how the hashing/reduction to zk input works in the verifier contract as well.
At the end of the day we want to verify an ethereum transaction (not an integer).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added a MiMC library which performs the hashing and reduction from bytes to points and so now, you can pass the message as bytes (abi.encode can be used) to the verifier contract along with the proof and other public inputs and the message can be decoded later. There is also an event that emits this message once the proof is verified for the same. cc @fedgiac
| --verifier) VERIFIER="$2"; shift 2 ;; | ||
| --msg) MSG="$2"; shift 2 ;; | ||
| --maxK) MAXK="$2"; shift 2 ;; | ||
| --signers) SIGNERS_STR="$2"; shift 2 ;; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this supposed to be used? Does the script hardcode private/public keys? Can you add sample commands in the readme?
No, since the verification key needs to be set in the smart contract and uses constant. There were two way:
So, when you call the setup script, the |
fedgiac
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice proof of concept!
I didn't read the code in detail, I just tried to run the script and fill the gaps by checking what I needed from the code. I couldn't verify the final proof (see comment) and I'm not sure why, maybe you have a better idea on what's going wrong?
Something I think would be helpful is using the message in some way in Solidity. When executing a transaction I couldn't recover the actual message from the user input, so I wonder how a contract is going to use it. What I'd like to see is having the autogenerated contract used as a library: for example, there could be another contract Verifier.sol (representing CoW exchange) that imports the autogenerated one, has an emitSignedMessage function and emits the original message if the verification through the autogenerated code succeeds. I imagine the actual message to be either decoded or compared with the proof input for validation.
gnark/multi-schnorr/keygen.sh
Outdated
| --num-validators <N> \ | ||
| --maxK <K> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what these two are. Without knowing anything I thought that N was the total number of validators in the system and K was the threshold (size of the min validator set needed for a signature to be valid).
After reading the circuit, I believe my guess for N is correct but the threshold isn't enforced in the circuit, it's just verified that it matches the input. Then K is just an internal parameter to make the validator set fit a power of two and then it could be derived automatically instead of being user specified, right?
|
|
||
| pushd "$CONTRACT_DIR" >/dev/null | ||
| echo ">> forge build" | ||
| forge build |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this to work I had to first run forge install OpenZeppelin/[email protected].
Probably the submodule needs to be checked in as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed now. It installs OZ library if it isn't present.
gnark/multi-schnorr/setup/main.go
Outdated
| outPath := filepath.Join(outDir, "MultischnorrVerifier.sol") | ||
| if _, err := os.Stat(outPath); err == nil { | ||
| return fmt.Errorf("file already exists: %s (delete or move it first)", outPath) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The script threw an error because the file already exists, here maybe you could delete the existing file in advance.
writing MultischnorrVerifier contract...
2025/10/29 10:15:33 file already exists: /workspaces/zk-benchmark/gnark/multi-schnorr/contract/src/MultischnorrVerifier.sol (delete or move it first)
exit status 1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also fixed in the new setup flow
| for i := 0; i < multischnorr.MaxK; i++ { | ||
| c := wd.Candidates[i] | ||
| assignment.S[i].Ax = c.Ax |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing the scripts with maxK = 32 this errors out with:
panic: runtime error: index out of range [32] with length 32
This is because multischnorr.MaxK is 64 regardless of user input. Where does it come from? It looks like it's an import from the GitHub mirror of this repo, can it use the local version instead?
gnark/multi-schnorr/prove.sh
Outdated
| echo ">> Sending verifyProof tx…" | ||
| cast send \ | ||
| --rpc-url "$RPC_URL" \ | ||
| --private-key "$PK" \ | ||
| "$VERIFIER" \ | ||
| "verifyProof(uint256[8],uint256[3])" \ | ||
| "$PROOF" "$INPUT" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This step reverts for me with execution reverted, data: "0x7fcdd1f4": ProofInvalid (I changed the code to execute it regardless, here it is).
Maybe this is related to the use of different code through the GitHub import?
What I did
$ go version
go version go1.25.3 linux/amd64
$ forge --version
forge Version: 1.4.3-stable
Commit SHA: fa9f934bdac4bcf57e694e852a61997dda90668a
Build Timestamp: 2025-10-22T05:31:43.173937269Z (1761111103)
Build Profile: maxperf
$ cd gnark/multi-schnorr/contract
$ forge install OpenZeppelin/[email protected]
$ cd ..
$ bash ./keygen.sh --num-validators 40 --maxK 64
$ rm -rf ./contract/src/MultischnorrVerifier.sol
$ bash ./setup_and_deploy_sepolia.sh --private-key "$PK" --rpc-url "$SEPOLIA_RPC_URL" --threshold 10 --merkle-root "$(cat merkle_root.txt)" --etherscan-api-key "$ETHERSCAN_API_KEY"
$ deployed_contract="$(cat contract/broadcast/DeployMultiSchnorrVerifier.s.sol/11155111/run-latest.json | jq '.transactions[0].contractAddress' --raw-output)"
$ bash ./prove.sh --rpc-url "$SEPOLIA_RPC_URL" --private-key "$PK" --verifier "$deployed_contract" --msg 'greetings' --maxK 64 --signers "$(seq 1 15)"There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cloned the repo again and executed the exact steps described above and the transaction was executed successfully! Here it is: https://sepolia.etherscan.io/tx/0x8047f3a27592486647de6a068d338732d723c0eaebc99315570a81bbecac3988
I wasn't able to understand what went wrong before. My suspicion is that there's some state that doesn't get cleared or updated correctly and so my multiple attempts were using some corrupted state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The most probable reason for this is that the Verifier contract was deployed with the wrong (old) verifiyingKey and then the proof was generated with a new provingKey and hence the proof was failing.
|
Incorporated all the changes requested, along with a Devcontainer and Readme. Ready to be reviewed again. |


This PR adds a Groth16 verifier contract with input validation logic, comprehensive utility functions, and scripts for full proof generation and on-chain verification, including key generation, setup and deployment and proving proofs onchain scripts.
Relevant Information:
keygen.shnandmaxKfor the number of validator key pairs you want to generate and total number of validators set by the circuitmaxKcannot be more than 64 or the proof will fail. Because the circuit needs to be static and have fixed size at compile time, if in future, more validators are added, just update the ciruit.keys.jsonwith public/private key pairs andmerkle_root.txtwith merkle root.maxKis required for the circuit, ifn < maxK, all the restmaxK - nkeys are zeroes, but are used to build the merkle tree.setup_and_deploy_sepolia.shVKinside the smart contract template for reproducible deploymenttemplate_sol.gotemplate as it uses theinput[N](whereNis the number of public inputs) in functionspublicInputMSM,verifyCompressedProofandverifyProof. Since, the circuit implementation in this case does not need to change the public inputs often or ever, it is highly unlikely to require a change to template contract.circuit.r1cs: compiled form of the circuit andmultischnorr.g16.pkandmultischnorr.g16.vkprove.shcast sendsumValid, which is the number of valid signatures that the circuit doesn't ignore.verifyprooffunction on the verifier contract.proof.jsonwith a flattened version of proof and public inputs (public witness) required by the contract to verfiy the proof.Workflow:
Step 1 - Generate Keys & Merkle Root
Step 2 - Setup & Deploy Verifier
Step 3 - Generate & Verify Proof On-Chain
len(signers) > threshold.