Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ runs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: "16.18.x"
node-version: "20.x"
cache: npm
- name: Install packages
run: npm install
shell: bash
- name: Generate verifiers
run: npx hardhat zkit verifiers
shell: bash
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ generated-types

# Hardhat migrate
.storage.json

contracts/verifiers
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
# Basic Implementation of ZK Multisig Smart Contracts
# Private ZK Multisig Smart Contracts

This project consists of a basic implementation of ZK (Zero-Knowledge) multisig smart contracts.
This project consists of a basic implementation of Private ZK (Zero-Knowledge) multisig smart contracts.

It allows multisig participants to approve or reject proposals
without revealing who voted and how until everyone has voted.

The contracts are divided into two parts:

- **ZK Multisig Factory** - Manages and deploys multisig contracts.
- **ZK Multisig** - The implementation of the multisig contract itself.

For more details, refer to the documentation: [Link to Documentation]
## Key Features
- Anonymous membership via Cartesian Merkle proofs.
- ECC ElGamal encrypted votes with ciphertext aggregation.
- Non-interactive DKG-based keys.
- On-chain ZK verification of core operations.

For more details, refer to the [original paper](https://ethresear.ch/t/private-multisig-v0-1/23244).

## Limitations
- All participants are required to vote.
- Only one proposal can be in the voting state at a time.
- Votes revelation and results computation scale linearly with the number of participants.

## Steps to Build the Project

1. Compile the contracts:
```bash
npm run compile
npm run test
```
1. Generate circuit verifiers
```bash
npx hardhat zkit verifiers
```
2. Compile the contracts and run tests:
```bash
npm run compile
npm run test
```
43 changes: 43 additions & 0 deletions circuits/ElGamal.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
pragma circom 2.1.6;

include "./babyjubjub/babyjub.circom";
include "./babyjubjub/escalarmulany.circom";

template ElGamal() {
signal input pk[2];
signal input M[2];
signal input nonce;

signal output C[2];
signal output D[2];

component pbk = BabyPbk();
pbk.in <== nonce;

C[0] <== pbk.Ax;
C[1] <== pbk.Ay;

signal rP[2];

component nonceBits = Num2Bits(253);
nonceBits.in <== nonce;

component mul = EscalarMulAny(253);
mul.p <== pk;

var i;
for (i = 0; i < 253; i++) {
mul.e[i] <== nonceBits.out[i];
}

rP <== mul.out;

component add = BabyAdd();
add.x1 <== M[0];
add.y1 <== M[1];
add.x2 <== rP[0];
add.y2 <== rP[1];

D[0] <== add.xout;
D[1] <== add.yout;
}
34 changes: 34 additions & 0 deletions circuits/IsGOrInf.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
pragma circom 2.1.6;

include "@solarity/circom-lib/bitify/comparators.circom";

template IsGOrInf() {
var Gx = 5299619240641551281634865583518297030282874472190772894086521144482721001553;
var Gy = 16950150798460657717958625567821834550301663161624707787222815936182638968203;

signal input x;
signal input y;

component diffGx = IsZero();
diffGx.in <== x - Gx;

component diffGy = IsZero();
diffGy.in <== y - Gy;

signal isG;
isG <== diffGx.out * diffGy.out;

component diffInfX = IsZero();
diffInfX.in <== x - 0;

component diffInfY = IsZero();
diffInfY.in <== y - 1;

signal isInf;
isInf <== diffInfX.out * diffInfY.out;

signal isValid;
isValid <== isG + isInf;

isValid === 1;
}
47 changes: 47 additions & 0 deletions circuits/ProposalCreation.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
pragma circom 2.1.6;

include "./babyjubjub/babyjub.circom";
include "@solarity/circom-lib/hasher/poseidon/poseidon.circom";
include "@solarity/circom-lib/data-structures/CartesianMerkleTree.circom";

template ProposalCreation(proofSize) {
signal input cmtRoot;
signal input challenge;

signal input sk;

signal input siblings[proofSize];
signal input siblingsLength[proofSize/2];
signal input directionBits[proofSize/2];

component pbk = BabyPbk();
pbk.in <== sk;

signal publicKey[2];
publicKey[0] <== pbk.Ax;
publicKey[1] <== pbk.Ay;

component keyHash = Poseidon(3);
keyHash.in[0] <== publicKey[0];
keyHash.in[1] <== publicKey[1];
keyHash.in[2] <== 1;
keyHash.dummy <== 0;

signal key;
key <== keyHash.out;

component cmt = CartesianMerkleTree(proofSize);
cmt.root <== cmtRoot;
cmt.siblings <== siblings;
cmt.siblingsLength <== siblingsLength;
cmt.directionBits <== directionBits;
cmt.key <== key;
cmt.nonExistenceKey <== 0;
cmt.isExclusion <== 0;
cmt.dummy <== 0;

signal challengeConstraint;
challengeConstraint <== challenge * key;
}

component main {public [cmtRoot, challenge]} = ProposalCreation(40);
188 changes: 188 additions & 0 deletions circuits/Voting.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
pragma circom 2.1.6;

include "./IsGOrInf.circom";
include "./ElGamal.circom";
include "./babyjubjub/babyjub.circom";
include "./babyjubjub/escalarmulany.circom";
include "@solarity/circom-lib/hasher/poseidon/poseidon.circom";
include "@solarity/circom-lib/data-structures/CartesianMerkleTree.circom";

template Voting(proofSize) {
signal input decryptionKeyShare;
signal input encryptionKey[2];
signal input challenge;
signal input proposalId;
signal input cmtRoot;

signal input sk1;
signal input sk2;
signal input newSk2;

signal input vote[2];
signal input k;

// CMT inclusion proof
signal input siblings[2][proofSize];
signal input siblingsLength[2][proofSize/2];
signal input directionBits[2][proofSize/2];

signal output blinder;
signal output nullifier;
signal output C1[2];
signal output C2[2];

signal output rotationKey[2];

// pk = skG
component pbk1 = BabyPbk();
pbk1.in <== sk1;

signal publicKey1[2];
publicKey1[0] <== pbk1.Ax;
publicKey1[1] <== pbk1.Ay;

component pbk2 = BabyPbk();
pbk2.in <== sk2;

signal publicKey2[2];
publicKey2[0] <== pbk2.Ax;
publicKey2[1] <== pbk2.Ay;

component rotationPbk = BabyPbk();
rotationPbk.in <== newSk2;

rotationKey[0] <== rotationPbk.Ax;
rotationKey[1] <== rotationPbk.Ay;

component keyHash1 = Poseidon(3);
keyHash1.in[0] <== publicKey1[0];
keyHash1.in[1] <== publicKey1[1];
keyHash1.in[2] <== 1;
keyHash1.dummy <== 0;

signal key1;
key1 <== keyHash1.out;

component cmt1 = CartesianMerkleTree(proofSize);
cmt1.root <== cmtRoot;
cmt1.siblings <== siblings[0];
cmt1.siblingsLength <== siblingsLength[0];
cmt1.directionBits <== directionBits[0];
cmt1.key <== key1;
cmt1.nonExistenceKey <== 0;
cmt1.isExclusion <== 0;
cmt1.dummy <== 0;

component keyHash2 = Poseidon(3);
keyHash2.in[0] <== publicKey2[0];
keyHash2.in[1] <== publicKey2[1];
keyHash2.in[2] <== 2;
keyHash2.dummy <== 0;

signal key2;
key2 <== keyHash2.out;

component cmt2 = CartesianMerkleTree(proofSize);
cmt2.root <== cmtRoot;
cmt2.siblings <== siblings[1];
cmt2.siblingsLength <== siblingsLength[1];
cmt2.directionBits <== directionBits[1];
cmt2.key <== key2;
cmt2.nonExistenceKey <== 0;
cmt2.isExclusion <== 0;
cmt2.dummy <== 0;

// blinder check
component blinderHash = Poseidon(2);
blinderHash.in[0] <== sk1;
blinderHash.in[1] <== proposalId;
blinderHash.dummy <== 0;

blinder <== blinderHash.out;

// sk2 nullifier check
component nullifierHash = Poseidon(1);
nullifierHash.in[0] <== sk2;
nullifierHash.dummy <== 0;

nullifier <== nullifierHash.out;

// decryption key share calculation
component h1Hash = Poseidon(1);
h1Hash.in[0] <== challenge;
h1Hash.dummy <== 0;

signal h1;
h1 <== h1Hash.out;

component h2Hash = Poseidon(1);
h2Hash.in[0] <== h1;
h2Hash.dummy <== 0;

signal h2;
h2 <== h2Hash.out;

signal hpk1[2];
signal hpk2[2];

component h1Bits = Num2Bits(254);
h1Bits.in <== h1;

component pk1Mul = EscalarMulAny(254);
pk1Mul.p <== publicKey1;

var i;
for (i = 0; i < 254; i++) {
pk1Mul.e[i] <== h1Bits.out[i];
}

hpk1 <== pk1Mul.out;

component h2Bits = Num2Bits(254);
h2Bits.in <== h2;

component pk2Mul = EscalarMulAny(254);
pk2Mul.p <== publicKey2;

for (i = 0; i < 254; i++) {
pk2Mul.e[i] <== h2Bits.out[i];
}

hpk2 <== pk2Mul.out;

signal expectedEncryptionKeyShare[2];
component add = BabyAdd();
add.x1 <== hpk1[0];
add.y1 <== hpk1[1];
add.x2 <== hpk2[0];
add.y2 <== hpk2[1];

expectedEncryptionKeyShare[0] <== add.xout;
expectedEncryptionKeyShare[1] <== add.yout;

signal encryptionKeyShare[2];
component encKey = BabyPbk();
encKey.in <== decryptionKeyShare;

encryptionKeyShare[0] <== encKey.Ax;
encryptionKeyShare[1] <== encKey.Ay;

encryptionKeyShare[0] === expectedEncryptionKeyShare[0];
encryptionKeyShare[1] === expectedEncryptionKeyShare[1];

// check for the vote to be either G or inf
component voteChecker = IsGOrInf();
voteChecker.x <== vote[0];
voteChecker.y <== vote[1];

// vote ElGamal encryption
component elgamal = ElGamal();
elgamal.pk <== encryptionKey;
elgamal.M <== vote;
elgamal.nonce <== k;

C1 <== elgamal.C;
C2 <== elgamal.D;
}

component main {public [decryptionKeyShare, encryptionKey, challenge, proposalId, cmtRoot, decryptionKeyShare]} = Voting(40);
Loading