├── .gitattributes ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── README.MD ├── circuits ├── hasherPoseidon.circom ├── identityCommitment.circom ├── incrementalMerkleTree.circom ├── modulo.circom ├── poseidon │ ├── common.circom │ ├── poseidonHashT3.circom │ └── poseidonHashT6.circom ├── processAttestations.circom ├── proveReputation.circom ├── proveUserSignUp.circom ├── sparseMerkleTree.circom ├── startTransition.circom ├── test │ ├── epochKeyExists_test.circom │ ├── hasher5_test.circom │ ├── hashleftright_test.circom │ ├── identityCommitment_test.circom │ ├── merkleTreeInclusionProof_test.circom │ ├── merkleTreeLeafExists_test.circom │ ├── processAttestations_test.circom │ ├── proveReputation_test.circom │ ├── proveUserSignUp_test.circom │ ├── smtInclusionProof_test.circom │ ├── smtLeafExists_test.circom │ ├── startTransition_test.circom │ ├── userStateTransition_test.circom │ ├── verifyEpochKey_test.circom │ └── verifyHashChain_test.circom ├── userExists.circom ├── userStateTransition.circom ├── utils.ts ├── verifyEpochKey.circom └── verifyHashChain.circom ├── cli ├── attest.ts ├── attesterSignUp.ts ├── defaults.ts ├── deploy.ts ├── epochTransition.ts ├── genEpochKeyAndProof.ts ├── genReputationProof.ts ├── genUnirepIdentity.ts ├── genUserSignUpProof.ts ├── giveAirdrop.ts ├── index.ts ├── prefix.ts ├── setAirdropAmount.ts ├── spendReputation.ts ├── submitEpochKeyProof.ts ├── test │ ├── testAllCommands.ts │ └── utils.ts ├── userSignUp.ts ├── userStateTransition.ts ├── utils.ts ├── verifyEpochKeyProof.ts ├── verifyReputationProof.ts └── verifyUserSignUpProof.ts ├── config ├── circuitPath.ts ├── nullifierDomainSeparator.ts └── testLocal.ts ├── contracts ├── Address.sol ├── Hasher.sol ├── SnarkConstants.sol ├── Unirep.sol └── UnirepObjs.sol ├── core ├── UnirepContract.ts ├── UnirepState.ts ├── UserState.ts ├── index.ts └── utils.ts ├── crypto ├── SMT │ ├── SparseMerkleTree.ts │ ├── index.ts │ └── utils.ts ├── crypto.ts ├── index.ts └── semaphore │ └── identity.ts ├── hardhat.config.ts ├── package.json ├── scripts ├── buildCircuits.ts ├── buildProveReputationSnark.sh ├── buildProveUserSignUpSnark.sh ├── buildSnarks.ts ├── buildUserStateTransitionSnark.sh ├── buildVerifiers.sh ├── buildVerifiers.ts ├── buildVerifyEpochKeySnark.sh ├── downloadPtau.sh ├── genVerifier.ts ├── testCLI.sh └── verifier_groth16.sol ├── test ├── UnirepState │ ├── UnirepState.ts │ ├── epochKeyProofEvent.ts │ ├── reputationProofEvent.ts │ ├── userSignUp.ts │ ├── userSignUpProofEvent.ts │ └── userStateTransitionEvent.ts ├── UserState │ ├── UserState.ts │ ├── epochKeyProofEvent.ts │ ├── genUserState.ts │ ├── reputationProofEvent.ts │ ├── userSignUp.ts │ ├── userSignUpProofEvent.ts │ └── userStateTransitionEvent.ts ├── circuits │ ├── IncrementalMerkleTree.ts │ ├── hasher.ts │ ├── identityCommitment.ts │ ├── processAttestations.ts │ ├── proveReputation.ts │ ├── proveUserSignUp.ts │ ├── sparseMerkleTree.ts │ ├── startTransition.ts │ ├── userStateTransition.ts │ ├── verifyEpochKey.ts │ └── verifyHashChain.ts ├── contracts │ ├── Airdrop.ts │ ├── Attesting.ts │ ├── EpochKeyVerifier.ts │ ├── EpochTransition.ts │ ├── EventFilters.ts │ ├── EventSequencing.ts │ ├── ProcessAttestationsVerifier.ts │ ├── ReputationVerifier.ts │ ├── SignUp.ts │ ├── StartTransitionVerifier.ts │ ├── UserSignUpVerifier.ts │ └── UserStateTransitionVerifier.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: yarn install 24 | - run: yarn build 25 | - run: yarn test 26 | - run: yarn test-cli 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | 4 | #Buidler files 5 | cache 6 | artifacts 7 | build 8 | 9 | #Verifier solidity contracts 10 | EpochKeyValidityVerifier.sol 11 | ReputationVerifier.sol 12 | UserSignUpVerifier.sol 13 | StartTransitionVerifier.sol 14 | ProcessAttestationsVerifier.sol 15 | UserStateTransitionVerifier.sol 16 | -------------------------------------------------------------------------------- /circuits/hasherPoseidon.circom: -------------------------------------------------------------------------------- 1 | include "./poseidon/poseidonHashT3.circom" 2 | include "./poseidon/poseidonHashT6.circom" 3 | 4 | template Hasher5() { 5 | var length = 5; 6 | signal input in[length]; 7 | signal output hash; 8 | 9 | component hasher = PoseidonHashT6(); 10 | 11 | for (var i = 0; i < length; i++) { 12 | hasher.inputs[i] <== in[i]; 13 | } 14 | 15 | hash <== hasher.out; 16 | } 17 | 18 | template HashLeftRight() { 19 | signal input left; 20 | signal input right; 21 | 22 | signal output hash; 23 | 24 | component hasher = PoseidonHashT3(); 25 | left ==> hasher.inputs[0]; 26 | right ==> hasher.inputs[1]; 27 | 28 | hash <== hasher.out; 29 | } -------------------------------------------------------------------------------- /circuits/identityCommitment.circom: -------------------------------------------------------------------------------- 1 | include "../node_modules/circomlib/circuits/babyjub.circom"; 2 | include "../node_modules/circomlib/circuits/bitify.circom"; 3 | include "../node_modules/circomlib/circuits/poseidon.circom"; 4 | 5 | template CalculateIdentityCommitment() { 6 | 7 | signal input identity_pk; 8 | signal input identity_nullifier; 9 | signal input identity_trapdoor; 10 | 11 | signal output out; 12 | 13 | // identity commitment is a pedersen hash of (identity_pk, identity_nullifier, identity_trapdoor), each element padded up to 256 bits 14 | component identity_commitment = Poseidon(3); 15 | identity_commitment.inputs[0] <== identity_pk; 16 | identity_commitment.inputs[1] <== identity_nullifier; 17 | identity_commitment.inputs[2] <== identity_trapdoor; 18 | 19 | out <== identity_commitment.out; 20 | } 21 | 22 | template IdentityCommitment() { 23 | 24 | signal private input identity_pk[2]; 25 | signal private input identity_nullifier; 26 | signal private input identity_trapdoor; 27 | 28 | signal output out; 29 | 30 | // get a prime subgroup element derived from identity_pk 31 | component dbl1 = BabyDbl(); 32 | dbl1.x <== identity_pk[0]; 33 | dbl1.y <== identity_pk[1]; 34 | component dbl2 = BabyDbl(); 35 | dbl2.x <== dbl1.xout; 36 | dbl2.y <== dbl1.yout; 37 | component dbl3 = BabyDbl(); 38 | dbl3.x <== dbl2.xout; 39 | dbl3.y <== dbl2.yout; 40 | 41 | // BEGIN identity commitment 42 | component identity_commitment = CalculateIdentityCommitment(); 43 | identity_commitment.identity_pk <== dbl3.xout; 44 | identity_commitment.identity_nullifier <== identity_nullifier; 45 | identity_commitment.identity_trapdoor <== identity_trapdoor; 46 | out <== identity_commitment.out; 47 | // END identity commitment 48 | } -------------------------------------------------------------------------------- /circuits/incrementalMerkleTree.circom: -------------------------------------------------------------------------------- 1 | // Refer to: 2 | // https://github.com/peppersec/tornado-mixer/blob/master/circuits/merkleTree.circom 3 | // https://github.com/appliedzkp/semaphore/blob/master/circuits/circom/semaphore-base.circom 4 | 5 | include "../node_modules/circomlib/circuits/mux1.circom"; 6 | include "./hasherPoseidon.circom"; 7 | 8 | template MerkleTreeInclusionProof(n_levels) { 9 | signal input leaf; 10 | signal input path_index[n_levels]; 11 | signal input path_elements[n_levels][1]; 12 | signal output root; 13 | 14 | component hashers[n_levels]; 15 | component mux[n_levels]; 16 | 17 | signal levelHashes[n_levels + 1]; 18 | levelHashes[0] <== leaf; 19 | 20 | for (var i = 0; i < n_levels; i++) { 21 | // Should be 0 or 1 22 | path_index[i] * (1 - path_index[i]) === 0; 23 | 24 | hashers[i] = HashLeftRight(); 25 | mux[i] = MultiMux1(2); 26 | 27 | mux[i].c[0][0] <== levelHashes[i]; 28 | mux[i].c[0][1] <== path_elements[i][0]; 29 | 30 | mux[i].c[1][0] <== path_elements[i][0]; 31 | mux[i].c[1][1] <== levelHashes[i]; 32 | 33 | mux[i].s <== path_index[i]; 34 | hashers[i].left <== mux[i].out[0]; 35 | hashers[i].right <== mux[i].out[1]; 36 | 37 | levelHashes[i + 1] <== hashers[i].hash; 38 | } 39 | 40 | root <== levelHashes[n_levels]; 41 | } 42 | 43 | template LeafExists(levels){ 44 | // Ensures that a leaf exists within a merkletree with given `root` 45 | 46 | // levels is depth of tree 47 | signal input leaf; 48 | 49 | signal private input path_elements[levels][1]; 50 | signal private input path_index[levels]; 51 | 52 | signal input root; 53 | 54 | component merkletree = MerkleTreeInclusionProof(levels); 55 | merkletree.leaf <== leaf; 56 | for (var i = 0; i < levels; i++) { 57 | merkletree.path_index[i] <== path_index[i]; 58 | merkletree.path_elements[i][0] <== path_elements[i][0]; 59 | } 60 | 61 | root === merkletree.root; 62 | } 63 | -------------------------------------------------------------------------------- /circuits/modulo.circom: -------------------------------------------------------------------------------- 1 | include "../node_modules/circomlib/circuits/bitify.circom"; 2 | include "../node_modules/circomlib/circuits/comparators.circom"; 3 | include "../node_modules/circomlib/circuits/sign.circom"; 4 | 5 | template ModuloTreeDepth(TREE_DEPTH) { 6 | signal input dividend; 7 | signal output remainder; 8 | 9 | signal divisor <-- 2 ** TREE_DEPTH; 10 | signal quotient; 11 | 12 | // circom's best practices state that we should avoid using <-- unless 13 | // we know what we are doing. But this is the only way to perform the 14 | // modulo operation. 15 | quotient <-- dividend \ divisor; 16 | remainder <-- dividend % divisor; 17 | 18 | component remainder_lt; 19 | // Range check on remainder 20 | remainder_lt = LessEqThan(TREE_DEPTH); 21 | remainder_lt.in[0] <== remainder; 22 | remainder_lt.in[1] <== 2 ** TREE_DEPTH - 1; 23 | remainder_lt.out === 1; 24 | 25 | // Range check on quotient 26 | component quotient_lt; 27 | quotient_lt = LessEqThan(254 - TREE_DEPTH); 28 | quotient_lt.in[0] <== quotient; 29 | quotient_lt.in[1] <== 2 ** (254 - TREE_DEPTH) - 1; 30 | quotient_lt.out === 1; 31 | 32 | // Check equality 33 | dividend === divisor * quotient + remainder; 34 | 35 | } -------------------------------------------------------------------------------- /circuits/poseidon/common.circom: -------------------------------------------------------------------------------- 1 | template Sigma() { 2 | signal input in; 3 | signal output out; 4 | 5 | signal in2; 6 | signal in4; 7 | 8 | in2 <== in*in; 9 | in4 <== in2*in2; 10 | 11 | out <== in4*in; 12 | } 13 | 14 | template Ark(t, C) { 15 | signal input in[t]; 16 | signal output out[t]; 17 | for (var i=0; i 0, check if rep_nonce is valid 96 | component if_prove_rep_nullifiers = GreaterThan(MAX_REPUTATION_BUDGET); 97 | if_prove_rep_nullifiers.in[0] <== rep_nullifiers_amount; 98 | if_prove_rep_nullifiers.in[1] <== 0; 99 | 100 | component if_check_nullifiers[MAX_REPUTATION_BUDGET]; 101 | component if_output_nullifiers[MAX_REPUTATION_BUDGET]; 102 | component rep_nullifier_hasher[MAX_REPUTATION_BUDGET]; 103 | component nonce_gt[MAX_REPUTATION_BUDGET]; 104 | for(var i = 0; i< MAX_REPUTATION_BUDGET; i++) { 105 | // 3.2 verify is nonce is valid 106 | // If user wants to generate rep nullifiers, check if pos_rep - neg_rep > rep_nonce 107 | // Eg. if we have 10 rep score, we have 0-9 valid nonce 108 | nonce_gt[i] = GreaterThan(MAX_REPUTATION_SCORE_BITS); 109 | nonce_gt[i].in[0] <== pos_rep - neg_rep; 110 | nonce_gt[i].in[1] <== rep_nonce[i]; 111 | if_check_nullifiers[i] = Mux1(); 112 | if_check_nullifiers[i].c[0] <== 1; 113 | if_check_nullifiers[i].c[1] <== nonce_gt[i].out; 114 | if_check_nullifiers[i].s <== if_prove_rep_nullifiers.out; 115 | if_check_nullifiers[i].out === 1; 116 | 117 | // 3.3 Use rep_nonce to compute all reputation nullifiers 118 | if_output_nullifiers[i] = Mux1(); 119 | rep_nullifier_hasher[i] = Hasher5(); 120 | rep_nullifier_hasher[i].in[0] <== 2; // 2 is the domain separator for reputation nullifier 121 | rep_nullifier_hasher[i].in[1] <== identity_nullifier; 122 | rep_nullifier_hasher[i].in[2] <== epoch; 123 | rep_nullifier_hasher[i].in[3] <== rep_nonce[i]; 124 | rep_nullifier_hasher[i].in[4] <== attester_id; // The reputation nullifier is spent at the attester's app 125 | if_output_nullifiers[i].c[0] <== default_nullifier_zero; 126 | if_output_nullifiers[i].c[1] <== rep_nullifier_hasher[i].hash; 127 | if_output_nullifiers[i].s <== selectors[i] * if_prove_rep_nullifiers.out; 128 | rep_nullifiers[i] <== if_output_nullifiers[i].out; 129 | } 130 | /* End of check 3 */ 131 | 132 | /* 4. Check if user has reputation greater than min_rep */ 133 | // 4.1 if proving min_rep > 0, check if pos_rep - neg_rep >= min_rep 134 | component if_prove_min_rep = GreaterThan(MAX_REPUTATION_BUDGET); 135 | if_prove_min_rep.in[0] <== min_rep; 136 | if_prove_min_rep.in[1] <== 0; 137 | 138 | // 4.2 check if pos_rep - neg_rep >= 0 && pos_rep - neg_rep >= min_rep 139 | component if_check_min_rep = Mux1(); 140 | component rep_get = GreaterEqThan(MAX_REPUTATION_SCORE_BITS); 141 | rep_get.in[0] <== pos_rep - neg_rep; 142 | rep_get.in[1] <== min_rep; 143 | if_check_min_rep.c[0] <== 1; 144 | if_check_min_rep.c[1] <== rep_get.out; 145 | if_check_min_rep.s <== if_prove_min_rep.out; 146 | if_check_min_rep.out === 1; 147 | /* End of check 4 */ 148 | 149 | /* 5. Check pre-image of graffiti */ 150 | component if_check_graffiti = Mux1(); 151 | component graffiti_hasher = HashLeftRight(); 152 | graffiti_hasher.left <== graffiti_pre_image; 153 | graffiti_hasher.right <== 0; 154 | component graffiti_eq = IsEqual(); 155 | graffiti_eq.in[0] <== graffiti_hasher.hash; 156 | graffiti_eq.in[1] <== graffiti; 157 | if_check_graffiti.c[0] <== 1; 158 | if_check_graffiti.c[1] <== graffiti_eq.out; 159 | if_check_graffiti.s <== prove_graffiti; 160 | if_check_graffiti.out === 1; 161 | /* End of check 5 */ 162 | } -------------------------------------------------------------------------------- /circuits/proveUserSignUp.circom: -------------------------------------------------------------------------------- 1 | /* 2 | Prove: 3 | 1. if user has a leaf in an existed global state tree 4 | 2. user state tree has a `sign_up` flag given by the attester 5 | 3. output an epoch key with nonce = 0 (ensure one epoch key per user per epoch) 6 | */ 7 | 8 | include "../node_modules/circomlib/circuits/mux1.circom"; 9 | include "./hasherPoseidon.circom"; 10 | include "./sparseMerkleTree.circom"; 11 | include "./verifyEpochKey.circom"; 12 | 13 | template ProveUserSignUp(GST_tree_depth, user_state_tree_depth, epoch_tree_depth, EPOCH_KEY_NONCE_PER_EPOCH) { 14 | signal input epoch; 15 | signal input epoch_key; 16 | 17 | // Global state tree leaf: Identity & user state root 18 | signal private input identity_pk[2]; 19 | signal private input identity_nullifier; 20 | signal private input identity_trapdoor; 21 | signal private input user_tree_root; 22 | // Global state tree 23 | signal private input GST_path_index[GST_tree_depth]; 24 | signal private input GST_path_elements[GST_tree_depth][1]; 25 | signal input GST_root; 26 | // Attester to prove reputation from 27 | signal input attester_id; 28 | // Attestation by the attester 29 | signal private input pos_rep; 30 | signal private input neg_rep; 31 | signal private input graffiti; 32 | signal input sign_up; // indicate if the user has signed up in the attester's app or not 33 | signal private input UST_path_elements[user_state_tree_depth][1]; 34 | 35 | /* 1. Check if user exists in the Global State Tree and verify epoch key */ 36 | // set epoch_key_nonce = 0 to force user only use one epoch key to receive airdrop 37 | var epoch_key_nonce = 0; 38 | component verify_epoch_key = VerifyEpochKey(GST_tree_depth, epoch_tree_depth, EPOCH_KEY_NONCE_PER_EPOCH); 39 | for (var i = 0; i< GST_tree_depth; i++) { 40 | verify_epoch_key.GST_path_index[i] <== GST_path_index[i]; 41 | verify_epoch_key.GST_path_elements[i][0] <== GST_path_elements[i][0]; 42 | } 43 | verify_epoch_key.GST_root <== GST_root; 44 | verify_epoch_key.identity_pk[0] <== identity_pk[0]; 45 | verify_epoch_key.identity_pk[1] <== identity_pk[1]; 46 | verify_epoch_key.identity_nullifier <== identity_nullifier; 47 | verify_epoch_key.identity_trapdoor <== identity_trapdoor; 48 | verify_epoch_key.user_tree_root <== user_tree_root; 49 | verify_epoch_key.nonce <== epoch_key_nonce; 50 | verify_epoch_key.epoch <== epoch; 51 | verify_epoch_key.epoch_key <== epoch_key; 52 | /* End of check 1 */ 53 | 54 | /* 2. Check if the reputation given by the attester is in the user state tree */ 55 | component reputation_hasher = Hasher5(); 56 | reputation_hasher.in[0] <== pos_rep; 57 | reputation_hasher.in[1] <== neg_rep; 58 | reputation_hasher.in[2] <== graffiti; 59 | reputation_hasher.in[3] <== sign_up; 60 | reputation_hasher.in[4] <== 0; 61 | 62 | component reputation_membership_check = SMTLeafExists(user_state_tree_depth); 63 | reputation_membership_check.leaf_index <== attester_id; 64 | reputation_membership_check.leaf <== reputation_hasher.hash; 65 | for (var i = 0; i < user_state_tree_depth; i++) { 66 | reputation_membership_check.path_elements[i][0] <== UST_path_elements[i][0]; 67 | } 68 | reputation_membership_check.root <== user_tree_root; 69 | /* End of check 2 */ 70 | } -------------------------------------------------------------------------------- /circuits/sparseMerkleTree.circom: -------------------------------------------------------------------------------- 1 | include "../node_modules/circomlib/circuits/bitify.circom"; 2 | include "../node_modules/circomlib/circuits/mux1.circom"; 3 | include "./hasherPoseidon.circom"; 4 | 5 | template SMTInclusionProof(n_levels) { 6 | signal input leaf; 7 | signal input leaf_index; 8 | signal input path_elements[n_levels][1]; 9 | signal output root; 10 | 11 | component hashers[n_levels]; 12 | component mux[n_levels]; 13 | 14 | signal levelHashes[n_levels + 1]; 15 | levelHashes[0] <== leaf; 16 | 17 | // Build path indices from leaf index 18 | signal path_index[n_levels]; 19 | component n2b = Num2Bits(n_levels); 20 | n2b.in <== leaf_index; 21 | for (var i = 0; i < n_levels; i++) { 22 | path_index[i] <== n2b.out[i]; 23 | } 24 | 25 | 26 | for (var i = 0; i < n_levels; i++) { 27 | // Should be 0 or 1 28 | path_index[i] * (1 - path_index[i]) === 0; 29 | 30 | hashers[i] = HashLeftRight(); 31 | mux[i] = MultiMux1(2); 32 | 33 | mux[i].c[0][0] <== levelHashes[i]; 34 | mux[i].c[0][1] <== path_elements[i][0]; 35 | 36 | mux[i].c[1][0] <== path_elements[i][0]; 37 | mux[i].c[1][1] <== levelHashes[i]; 38 | 39 | mux[i].s <== path_index[i]; 40 | hashers[i].left <== mux[i].out[0]; 41 | hashers[i].right <== mux[i].out[1]; 42 | 43 | levelHashes[i + 1] <== hashers[i].hash; 44 | } 45 | 46 | root <== levelHashes[n_levels]; 47 | } 48 | 49 | 50 | template SMTLeafExists(levels){ 51 | // Ensures that a leaf exists within a merkletree with given `root` 52 | 53 | // levels is depth of tree 54 | signal input leaf; 55 | 56 | signal private input path_elements[levels][1]; 57 | signal private input leaf_index; 58 | 59 | signal input root; 60 | 61 | component merkletree = SMTInclusionProof(levels); 62 | merkletree.leaf <== leaf; 63 | merkletree.leaf_index <== leaf_index; 64 | for (var i = 0; i < levels; i++) { 65 | merkletree.path_elements[i][0] <== path_elements[i][0]; 66 | } 67 | 68 | root === merkletree.root; 69 | } -------------------------------------------------------------------------------- /circuits/startTransition.circom: -------------------------------------------------------------------------------- 1 | /* 2 | Prove: if user starts user state transition from an existed global state tree 3 | compute the blinded_user_state = hash5(identity, UST_root, epoch, epoch_key_nonce) 4 | compute teh blinded_hash_chain = hash5(identity, hash_chain_result, epoch, epoch_key_nonce) 5 | Process attestations proof should start with the blinded_user_state and blinded_hash_chain 6 | */ 7 | 8 | include "../node_modules/circomlib/circuits/comparators.circom"; 9 | include "../node_modules/circomlib/circuits/mux1.circom"; 10 | include "./hasherPoseidon.circom"; 11 | include "./incrementalMerkleTree.circom"; 12 | include "./modulo.circom"; 13 | include "./sparseMerkleTree.circom"; 14 | include "./processAttestations.circom"; 15 | include "./userExists.circom"; 16 | 17 | template StartTransition(GST_tree_depth) { 18 | // Start from which epoch key nonce 19 | signal private input epoch; 20 | signal private input nonce; 21 | 22 | // User state tree 23 | signal private input user_tree_root; 24 | 25 | // Global state tree leaf: Identity & user state root 26 | signal private input identity_pk[2]; 27 | signal private input identity_nullifier; 28 | signal private input identity_trapdoor; 29 | 30 | // Global state tree 31 | signal private input GST_path_elements[GST_tree_depth][1]; 32 | signal private input GST_path_index[GST_tree_depth]; 33 | signal input GST_root; 34 | 35 | signal output blinded_user_state; 36 | signal output blinded_hash_chain_result; 37 | 38 | /* 1. Check if user exists in the Global State Tree */ 39 | component user_exist = UserExists(GST_tree_depth); 40 | for (var i = 0; i< GST_tree_depth; i++) { 41 | user_exist.GST_path_index[i] <== GST_path_index[i]; 42 | user_exist.GST_path_elements[i][0] <== GST_path_elements[i][0]; 43 | } 44 | user_exist.GST_root <== GST_root; 45 | user_exist.identity_pk[0] <== identity_pk[0]; 46 | user_exist.identity_pk[1] <== identity_pk[1]; 47 | user_exist.identity_nullifier <== identity_nullifier; 48 | user_exist.identity_trapdoor <== identity_trapdoor; 49 | user_exist.user_tree_root <== user_tree_root; 50 | /* End of check 1 */ 51 | 52 | /* 2. Compute blinded public output */ 53 | // 2.1 blinded_user_state = hash5(identity, UST_root, epoch, epoch_key_nonce) 54 | component blinded_user_state_hasher = Hasher5(); 55 | blinded_user_state_hasher.in[0] <== identity_nullifier; 56 | blinded_user_state_hasher.in[1] <== user_tree_root; 57 | blinded_user_state_hasher.in[2] <== epoch; 58 | blinded_user_state_hasher.in[3] <== nonce; 59 | blinded_user_state_hasher.in[4] <== 0; 60 | blinded_user_state <== blinded_user_state_hasher.hash; 61 | 62 | // 2.2 blinded_hash_chain_result = hash5(identity, hash_chain_result, epoch, epoch_key_nonce) 63 | component blinded_hash_chain_result_hasher = Hasher5(); 64 | blinded_hash_chain_result_hasher.in[0] <== identity_nullifier; 65 | blinded_hash_chain_result_hasher.in[1] <== 0; // hashchain start from 0 66 | blinded_hash_chain_result_hasher.in[2] <== epoch; 67 | blinded_hash_chain_result_hasher.in[3] <== nonce; 68 | blinded_hash_chain_result_hasher.in[4] <== 0; 69 | blinded_hash_chain_result <== blinded_hash_chain_result_hasher.hash; 70 | /* End of 2. Compute blinded public output */ 71 | } -------------------------------------------------------------------------------- /circuits/test/epochKeyExists_test.circom: -------------------------------------------------------------------------------- 1 | include "../userStateTransition.circom" 2 | 3 | component main = EpochKeyExist(32); -------------------------------------------------------------------------------- /circuits/test/hasher5_test.circom: -------------------------------------------------------------------------------- 1 | include "../hasherPoseidon.circom" 2 | 3 | component main = Hasher5(); -------------------------------------------------------------------------------- /circuits/test/hashleftright_test.circom: -------------------------------------------------------------------------------- 1 | include "../hasherPoseidon.circom" 2 | 3 | component main = HashLeftRight(); -------------------------------------------------------------------------------- /circuits/test/identityCommitment_test.circom: -------------------------------------------------------------------------------- 1 | include "../identityCommitment.circom" 2 | 3 | component main = IdentityCommitment(); -------------------------------------------------------------------------------- /circuits/test/merkleTreeInclusionProof_test.circom: -------------------------------------------------------------------------------- 1 | include "../incrementalMerkleTree.circom" 2 | 3 | component main = MerkleTreeInclusionProof(4); -------------------------------------------------------------------------------- /circuits/test/merkleTreeLeafExists_test.circom: -------------------------------------------------------------------------------- 1 | include "../incrementalMerkleTree.circom" 2 | 3 | component main = LeafExists(4); -------------------------------------------------------------------------------- /circuits/test/processAttestations_test.circom: -------------------------------------------------------------------------------- 1 | include "../processAttestations.circom" 2 | 3 | component main = ProcessAttestations(4, 5, 3); -------------------------------------------------------------------------------- /circuits/test/proveReputation_test.circom: -------------------------------------------------------------------------------- 1 | include "../proveReputation.circom" 2 | 3 | component main = ProveReputation(4, 4, 32, 3, 10, 252); -------------------------------------------------------------------------------- /circuits/test/proveUserSignUp_test.circom: -------------------------------------------------------------------------------- 1 | include "../proveUserSignUp.circom" 2 | 3 | component main = ProveUserSignUp(4, 4, 32, 3); -------------------------------------------------------------------------------- /circuits/test/smtInclusionProof_test.circom: -------------------------------------------------------------------------------- 1 | include "../sparseMerkleTree.circom" 2 | 3 | component main = SMTInclusionProof(8); -------------------------------------------------------------------------------- /circuits/test/smtLeafExists_test.circom: -------------------------------------------------------------------------------- 1 | include "../sparseMerkleTree.circom" 2 | 3 | component main = SMTLeafExists(8); -------------------------------------------------------------------------------- /circuits/test/startTransition_test.circom: -------------------------------------------------------------------------------- 1 | include "../startTransition.circom" 2 | 3 | component main = StartTransition(4); -------------------------------------------------------------------------------- /circuits/test/userStateTransition_test.circom: -------------------------------------------------------------------------------- 1 | include "../userStateTransition.circom" 2 | 3 | component main = UserStateTransition( 4 | 4, // GST_tree_depth 5 | 32, // epoch_tree_depth 6 | 4, // user_state_tree_depth 7 | 3 // EPOCH_KEY_NONCE_PER_EPOCH 8 | ); -------------------------------------------------------------------------------- /circuits/test/verifyEpochKey_test.circom: -------------------------------------------------------------------------------- 1 | include "../verifiyEpochKey.circom" 2 | 3 | component main = VerifyEpochKey(4, 32, 3); 4 | -------------------------------------------------------------------------------- /circuits/test/verifyHashChain_test.circom: -------------------------------------------------------------------------------- 1 | include "../verifyHashChain.circom" 2 | 3 | component main = VerifyHashChain(10); -------------------------------------------------------------------------------- /circuits/userExists.circom: -------------------------------------------------------------------------------- 1 | /* 2 | Prove: if user's identity and the user state tree root is one of the global state tree leaf 3 | global state tree leaf = hashLeftRight(id_commitment, user_state_tree_root) 4 | */ 5 | 6 | 7 | include "./hasherPoseidon.circom"; 8 | include "./identityCommitment.circom"; 9 | include "./incrementalMerkleTree.circom"; 10 | 11 | template UserExists(GST_tree_depth){ 12 | // Global state tree 13 | signal private input GST_path_index[GST_tree_depth]; 14 | signal private input GST_path_elements[GST_tree_depth][1]; 15 | signal input GST_root; 16 | // Global state tree leaf: Identity & user state root 17 | signal private input identity_pk[2]; 18 | signal private input identity_nullifier; 19 | signal private input identity_trapdoor; 20 | signal private input user_tree_root; 21 | signal output out; 22 | 23 | component identity_commitment = IdentityCommitment(); 24 | identity_commitment.identity_pk[0] <== identity_pk[0]; 25 | identity_commitment.identity_pk[1] <== identity_pk[1]; 26 | identity_commitment.identity_nullifier <== identity_nullifier; 27 | identity_commitment.identity_trapdoor <== identity_trapdoor; 28 | out <== identity_commitment.out; 29 | 30 | // Compute user state tree root 31 | component leaf_hasher = HashLeftRight(); 32 | leaf_hasher.left <== identity_commitment.out; 33 | leaf_hasher.right <== user_tree_root; 34 | 35 | // Check if user state hash is in GST 36 | component GST_leaf_exists = LeafExists(GST_tree_depth); 37 | GST_leaf_exists.leaf <== leaf_hasher.hash; 38 | for (var i = 0; i < GST_tree_depth; i++) { 39 | GST_leaf_exists.path_index[i] <== GST_path_index[i]; 40 | GST_leaf_exists.path_elements[i][0] <== GST_path_elements[i][0]; 41 | } 42 | GST_leaf_exists.root <== GST_root; 43 | } -------------------------------------------------------------------------------- /circuits/utils.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | const snarkjs = require('snarkjs') 3 | import { SnarkProof, SnarkPublicSignals } from '../crypto' 4 | import verifyEpochKeyVkey from '../build/verifyEpochKey.vkey.json' 5 | import proveReputationVkey from '../build/proveReputation.vkey.json' 6 | import proveUserSignUpVkey from '../build/proveUserSignUp.vkey.json' 7 | import startTransitionVkey from '../build/startTransition.vkey.json' 8 | import processAttestationsVkey from '../build/processAttestations.vkey.json' 9 | import userStateTransitionVkey from '../build/userStateTransition.vkey.json' 10 | 11 | const buildPath = "../build" 12 | enum Circuit { 13 | verifyEpochKey = 'verifyEpochKey', 14 | proveReputation = 'proveReputation', 15 | proveUserSignUp = 'proveUserSignUp', 16 | startTransition = 'startTransition', 17 | processAttestations = 'processAttestations', 18 | userStateTransition = 'userStateTransition', 19 | } 20 | 21 | const executeCircuit = async ( 22 | circuit: any, 23 | inputs: any, 24 | ) => { 25 | 26 | const witness = await circuit.calculateWitness(inputs, true) 27 | await circuit.checkConstraints(witness) 28 | await circuit.loadSymbols() 29 | 30 | return witness 31 | } 32 | 33 | const getVKey = async ( 34 | circuitName: Circuit 35 | ) => { 36 | if (circuitName == Circuit.verifyEpochKey){ 37 | return verifyEpochKeyVkey 38 | } else if (circuitName == Circuit.proveReputation){ 39 | return proveReputationVkey 40 | } else if (circuitName == Circuit.proveUserSignUp){ 41 | return proveUserSignUpVkey 42 | } else if (circuitName == Circuit.startTransition){ 43 | return startTransitionVkey 44 | } else if (circuitName == Circuit.processAttestations){ 45 | return processAttestationsVkey 46 | } else if (circuitName == Circuit.userStateTransition){ 47 | return userStateTransitionVkey 48 | } else { 49 | console.log(`"${circuitName}" not found. Valid circuit name: verifyEpochKey, proveReputation, proveUserSignUp, startTransition, processAttestations, userStateTransition`) 50 | return 51 | } 52 | } 53 | 54 | const getSignalByName = ( 55 | circuit: any, 56 | witness: any, 57 | signal: string, 58 | ) => { 59 | 60 | return witness[circuit.symbols[signal].varIdx] 61 | } 62 | 63 | const genProofAndPublicSignals = async ( 64 | circuitName: Circuit, 65 | inputs: any, 66 | ) => { 67 | const circuitWasmPath = path.join(__dirname, buildPath, `${circuitName}.wasm`) 68 | const zkeyPath = path.join(__dirname, buildPath,`${circuitName}.zkey`) 69 | const { proof, publicSignals } = await snarkjs.groth16.fullProve(inputs, circuitWasmPath, zkeyPath); 70 | 71 | return { proof, publicSignals } 72 | } 73 | 74 | const verifyProof = async ( 75 | circuitName: Circuit, 76 | proof: SnarkProof, 77 | publicSignals: SnarkPublicSignals, 78 | ): Promise => { 79 | const vkey = await getVKey(circuitName) 80 | const res = await snarkjs.groth16.verify(vkey, publicSignals, proof); 81 | return res 82 | } 83 | 84 | const formatProofForVerifierContract = ( 85 | _proof: SnarkProof, 86 | ): string[] => { 87 | 88 | return ([ 89 | _proof.pi_a[0], 90 | _proof.pi_a[1], 91 | _proof.pi_b[0][1], 92 | _proof.pi_b[0][0], 93 | _proof.pi_b[1][1], 94 | _proof.pi_b[1][0], 95 | _proof.pi_c[0], 96 | _proof.pi_c[1], 97 | ]).map((x) => x.toString()) 98 | } 99 | 100 | const formatProofForSnarkjsVerification = ( 101 | _proof: string[] 102 | ): SnarkProof => { 103 | return { 104 | pi_a: [ 105 | BigInt(_proof[0]), 106 | BigInt(_proof[1]), 107 | BigInt('1') 108 | ], 109 | pi_b: [ 110 | [ 111 | BigInt(_proof[3]), 112 | BigInt(_proof[2]) 113 | ], 114 | [ 115 | BigInt(_proof[5]), 116 | BigInt(_proof[4]) 117 | ], 118 | [ BigInt('1'), 119 | BigInt('0') ] 120 | ], 121 | pi_c: [ 122 | BigInt(_proof[6]), 123 | BigInt(_proof[7]), 124 | BigInt('1') 125 | ], 126 | } 127 | } 128 | 129 | export { 130 | Circuit, 131 | executeCircuit, 132 | formatProofForVerifierContract, 133 | formatProofForSnarkjsVerification, 134 | getVKey, 135 | getSignalByName, 136 | genProofAndPublicSignals, 137 | verifyProof, 138 | } -------------------------------------------------------------------------------- /circuits/verifyEpochKey.circom: -------------------------------------------------------------------------------- 1 | /* 2 | Verify if epoch key is computed correctly 3 | epoch_key = hash5(id_nullifier, epoch, nonce); 4 | */ 5 | 6 | include "../node_modules/circomlib/circuits/comparators.circom"; 7 | include "./hasherPoseidon.circom"; 8 | include "./incrementalMerkleTree.circom"; 9 | include "./userExists.circom"; 10 | 11 | template VerifyEpochKey(GST_tree_depth, epoch_tree_depth, EPOCH_KEY_NONCE_PER_EPOCH) { 12 | // Global state tree 13 | signal private input GST_path_index[GST_tree_depth]; 14 | signal private input GST_path_elements[GST_tree_depth][1]; 15 | signal input GST_root; 16 | // Global state tree leaf: Identity & user state root 17 | signal private input identity_pk[2]; 18 | signal private input identity_nullifier; 19 | signal private input identity_trapdoor; 20 | signal private input user_tree_root; 21 | 22 | signal private input nonce; 23 | signal input epoch; 24 | signal input epoch_key; 25 | 26 | /* 1. Check if user exists in the Global State Tree */ 27 | component user_exist = UserExists(GST_tree_depth); 28 | for (var i = 0; i< GST_tree_depth; i++) { 29 | user_exist.GST_path_index[i] <== GST_path_index[i]; 30 | user_exist.GST_path_elements[i][0] <== GST_path_elements[i][0]; 31 | } 32 | user_exist.GST_root <== GST_root; 33 | user_exist.identity_pk[0] <== identity_pk[0]; 34 | user_exist.identity_pk[1] <== identity_pk[1]; 35 | user_exist.identity_nullifier <== identity_nullifier; 36 | user_exist.identity_trapdoor <== identity_trapdoor; 37 | user_exist.user_tree_root <== user_tree_root; 38 | /* End of check 1 */ 39 | 40 | /* 2. Check nonce validity */ 41 | var bitsPerNonce = 8; 42 | 43 | component nonce_lt = LessThan(bitsPerNonce); 44 | nonce_lt.in[0] <== nonce; 45 | nonce_lt.in[1] <== EPOCH_KEY_NONCE_PER_EPOCH; 46 | nonce_lt.out === 1; 47 | /* End of check 2*/ 48 | 49 | 50 | /* 3. Check epoch key is computed correctly */ 51 | // 3.1.1 Compute epoch key 52 | component epochKeyHasher = Hasher5(); 53 | epochKeyHasher.in[0] <== identity_nullifier; 54 | epochKeyHasher.in[1] <== epoch; 55 | epochKeyHasher.in[2] <== nonce; 56 | epochKeyHasher.in[3] <== 0; 57 | epochKeyHasher.in[4] <== 0; 58 | 59 | signal quotient; 60 | // 3.1.2 Mod epoch key 61 | // circom's best practices state that we should avoid using <-- unless 62 | // we know what we are doing. But this is the only way to perform the 63 | // modulo operation. 64 | quotient <-- epochKeyHasher.hash \ (2 ** epoch_tree_depth); 65 | 66 | // 3.1.3 Range check on epoch key 67 | component epk_lt = LessEqThan(epoch_tree_depth); 68 | epk_lt.in[0] <== epoch_key; 69 | epk_lt.in[1] <== 2 ** epoch_tree_depth - 1; 70 | epk_lt.out === 1; 71 | 72 | // 3.1.4 Range check on quotient 73 | component quot_lt = LessEqThan(254 - epoch_tree_depth); 74 | quot_lt.in[0] <== quotient; 75 | quot_lt.in[1] <== 2 ** (254 - epoch_tree_depth) - 1; 76 | quot_lt.out === 1; 77 | 78 | // 3.1.5 Check equality 79 | epochKeyHasher.hash === quotient * (2 ** epoch_tree_depth) + epoch_key; 80 | /* End of check 3*/ 81 | } -------------------------------------------------------------------------------- /circuits/verifyHashChain.circom: -------------------------------------------------------------------------------- 1 | include "../node_modules/circomlib/circuits/mux1.circom"; 2 | include "./hasherPoseidon.circom"; 3 | 4 | /* 5 | Verify sealed hash chain result is computed correctly 6 | hash_attestation_i = hash5(attester_id_i, pos_rep_i, neg_rep_i, graffiti_i, sign_up_i) 7 | hash_chain_1 = hashLeftRight(hash_attestation_1, 0) 8 | hash_chain_n = hashLeftRight(hash_attestation_n, hash_chain_{n-1}) 9 | sealed_hash_chain = hashLeftRight(1, hash_chain_n) 10 | */ 11 | 12 | template VerifyHashChain(NUM_ELEMENT) { 13 | signal input hashes[NUM_ELEMENT]; 14 | // Selector is used to determined if the hash should be included in the hash chain 15 | signal input selectors[NUM_ELEMENT]; 16 | signal input result; 17 | 18 | component hashers[NUM_ELEMENT]; 19 | component toHashOrNot[NUM_ELEMENT]; 20 | 21 | signal cur_hash[NUM_ELEMENT + 1]; 22 | // Hash chain starts with hashLeftRight(x, 0) 23 | cur_hash[0] <== 0; 24 | for (var i = 0; i < NUM_ELEMENT; i++) { 25 | hashers[i] = HashLeftRight(); 26 | hashers[i].left <== hashes[i]; 27 | hashers[i].right <== cur_hash[i]; 28 | 29 | toHashOrNot[i] = Mux1(); 30 | toHashOrNot[i].c[0] <== cur_hash[i]; 31 | toHashOrNot[i].c[1] <== hashers[i].hash; 32 | toHashOrNot[i].s <== selectors[i]; 33 | cur_hash[i + 1] <== toHashOrNot[i].out; 34 | } 35 | 36 | // Hash chain is sealed with hashLeftRight(1, y) 37 | component finalHasher = HashLeftRight(); 38 | finalHasher.left <== 1; 39 | finalHasher.right <== cur_hash[NUM_ELEMENT]; 40 | result === finalHasher.hash; 41 | } 42 | 43 | /* 44 | Verify sealed hash chain result is computed correctly without sealing 45 | hash_attestation_i = hash5(attester_id_i, pos_rep_i, neg_rep_i, graffiti_i, sign_up_i) 46 | hash_chain_1 = hashLeftRight(hash_attestation_1, 0) 47 | hash_chain_n = hashLeftRight(hash_attestation_n, hash_chain_{n-1}) 48 | */ 49 | 50 | template HashChainHaser(NUM_ELEMENT) { 51 | signal input hash_starter; 52 | signal input hashes[NUM_ELEMENT]; 53 | // Selector is used to determined if the hash should be included in the hash chain 54 | signal input selectors[NUM_ELEMENT]; 55 | signal output result; 56 | 57 | component hashers[NUM_ELEMENT]; 58 | component toHashOrNot[NUM_ELEMENT]; 59 | 60 | signal cur_hash[NUM_ELEMENT + 1]; 61 | // Hash chain starts with hashLeftRight(x, 0) 62 | cur_hash[0] <== hash_starter; 63 | for (var i = 0; i < NUM_ELEMENT; i++) { 64 | hashers[i] = HashLeftRight(); 65 | hashers[i].left <== hashes[i]; 66 | hashers[i].right <== cur_hash[i]; 67 | 68 | toHashOrNot[i] = Mux1(); 69 | toHashOrNot[i].c[0] <== cur_hash[i]; 70 | toHashOrNot[i].c[1] <== hashers[i].hash; 71 | toHashOrNot[i].s <== selectors[i]; 72 | cur_hash[i + 1] <== toHashOrNot[i].out; 73 | } 74 | 75 | result <== cur_hash[NUM_ELEMENT]; 76 | } -------------------------------------------------------------------------------- /cli/attest.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { add0x } from '../crypto' 3 | 4 | import { DEFAULT_ETH_PROVIDER } from './defaults' 5 | import { Attestation, UnirepContract } from '../core' 6 | 7 | const configureSubparser = (subparsers: any) => { 8 | const parser = subparsers.add_parser( 9 | 'attest', 10 | { add_help: true }, 11 | ) 12 | 13 | parser.add_argument( 14 | '-e', '--eth-provider', 15 | { 16 | action: 'store', 17 | type: 'str', 18 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 19 | } 20 | ) 21 | 22 | parser.add_argument( 23 | '-i', '--proof-index', 24 | { 25 | required: true, 26 | type: 'int', 27 | help: 'The proof index of the user\'s epoch key ', 28 | } 29 | ) 30 | 31 | parser.add_argument( 32 | '-epk', '--epoch-key', 33 | { 34 | required: true, 35 | type: 'str', 36 | help: 'The user\'s epoch key to attest to (in hex representation)', 37 | } 38 | ) 39 | 40 | parser.add_argument( 41 | '-pr', '--pos-rep', 42 | { 43 | type: 'int', 44 | help: 'Score of positive reputation to give to the user', 45 | } 46 | ) 47 | 48 | parser.add_argument( 49 | '-nr', '--neg-rep', 50 | { 51 | type: 'int', 52 | help: 'Score of negative reputation to give to the user', 53 | } 54 | ) 55 | 56 | parser.add_argument( 57 | '-gf', '--graffiti', 58 | { 59 | action: 'store', 60 | type: 'str', 61 | help: 'Graffiti for the reputation given to the user (in hex representation)', 62 | } 63 | ) 64 | 65 | parser.add_argument( 66 | '-s', '--sign-up', 67 | { 68 | action: 'store', 69 | type: 'int', 70 | help: 'Whether to set sign up flag to the user', 71 | } 72 | ) 73 | 74 | parser.add_argument( 75 | '-x', '--contract', 76 | { 77 | required: true, 78 | type: 'str', 79 | help: 'The Unirep contract address', 80 | } 81 | ) 82 | 83 | parser.add_argument( 84 | '-d', '--eth-privkey', 85 | { 86 | action: 'store', 87 | type: 'str', 88 | help: 'The attester\'s Ethereum private key', 89 | } 90 | ) 91 | } 92 | 93 | const attest = async (args: any) => { 94 | 95 | // Ethereum provider 96 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 97 | 98 | // Unirep contract 99 | const unirepContract = new UnirepContract(args.contract, ethProvider) 100 | 101 | // Connect a signer 102 | await unirepContract.unlock(args.eth_privkey) 103 | 104 | // Parse input 105 | const index = args.proof_index 106 | const epochKey = args.epoch_key 107 | const posRep = args.pos_rep != undefined ? args.pos_rep : 0 108 | const negRep = args.neg_rep != undefined ? args.neg_rep : 0 109 | const graffiti = args.graffiti != undefined ? BigInt(add0x(args.graffiti)) : BigInt(0) 110 | const signUp = args.sign_up != undefined ? args.sign_up : 0 111 | const ethAddr = ethers.utils.computeAddress(args.eth_privkey) 112 | const attesterId = await unirepContract.attesters(ethAddr) 113 | const attestation = new Attestation( 114 | BigInt(attesterId.toString()), 115 | BigInt(posRep), 116 | BigInt(negRep), 117 | graffiti, 118 | BigInt(signUp) 119 | ) 120 | console.log(`Attesting to epoch key ${epochKey} with pos rep ${posRep}, neg rep ${negRep}, graffiti ${graffiti.toString(16)} and sign up flag ${signUp}`) 121 | 122 | // Submit attestation 123 | const tx = await unirepContract.submitAttestation(attestation, epochKey, index) 124 | console.log('Transaction hash:', tx?.hash) 125 | } 126 | 127 | export { 128 | attest, 129 | configureSubparser, 130 | } -------------------------------------------------------------------------------- /cli/attesterSignUp.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | 3 | import { DEFAULT_ETH_PROVIDER } from './defaults' 4 | import { UnirepContract } from '../core' 5 | 6 | const configureSubparser = (subparsers: any) => { 7 | const parser = subparsers.add_parser( 8 | 'attesterSignUp', 9 | { add_help: true }, 10 | ) 11 | 12 | parser.add_argument( 13 | '-e', '--eth-provider', 14 | { 15 | action: 'store', 16 | type: 'str', 17 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 18 | } 19 | ) 20 | 21 | parser.add_argument( 22 | '-x', '--contract', 23 | { 24 | required: true, 25 | type: 'str', 26 | help: 'The Unirep contract address', 27 | } 28 | ) 29 | 30 | parser.add_argument( 31 | '-d', '--eth-privkey', 32 | { 33 | action: 'store', 34 | type: 'str', 35 | help: 'The attester\'s Ethereum private key', 36 | } 37 | ) 38 | } 39 | 40 | const attesterSignUp = async (args: any) => { 41 | // Ethereum provider 42 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 43 | 44 | // Unirep contract 45 | const unirepContract = new UnirepContract(args.contract, ethProvider) 46 | 47 | // Connect a signer 48 | const ehtSk = await unirepContract.unlock(args.eth_privkey) 49 | 50 | // Submit the user sign up transaction 51 | const tx = await unirepContract.attesterSignUp() 52 | await tx.wait() 53 | 54 | const ethAddr = ethers.utils.computeAddress(ehtSk) 55 | const attesterId = await unirepContract.attesters(ethAddr) 56 | if (attesterId.toNumber() == 0) { 57 | console.error('Error: sign up succeeded but has no attester id!') 58 | } 59 | console.log('Transaction hash:', tx?.hash) 60 | console.log('Attester sign up with attester id:', attesterId.toNumber()) 61 | } 62 | 63 | export { 64 | attesterSignUp, 65 | configureSubparser, 66 | } -------------------------------------------------------------------------------- /cli/defaults.ts: -------------------------------------------------------------------------------- 1 | import { attestingFee, epochLength, numEpochKeyNoncePerEpoch } from '../config/testLocal' 2 | // import { ALCHEMY_API_KEY } from '../config/privateKey' 3 | 4 | // apply the api key from https://www.alchemy.com/ 5 | // const DEFAULT_ETH_PROVIDER = `https://eth-goerli.alchemyapi.io/v2/${ALCHEMY_API_KEY}` 6 | // const DEFAULT_ETH_PROVIDER = `https://goerli.infura.io/v3/${INFURA_API_KEY}` 7 | const DEFAULT_ETH_PROVIDER = 'http://localhost:8545' 8 | const DEFAULT_START_BLOCK = 0 9 | const DEFAULT_MAX_EPOCH_KEY_NONCE = numEpochKeyNoncePerEpoch 10 | const DEFAULT_EPOCH_LENGTH = epochLength 11 | const DEFAULT_ATTESTING_FEE = attestingFee 12 | const DEFAULT_TREE_DEPTHS_CONFIG = 'circuit' 13 | 14 | export { 15 | DEFAULT_ETH_PROVIDER, 16 | DEFAULT_START_BLOCK, 17 | DEFAULT_MAX_EPOCH_KEY_NONCE, 18 | DEFAULT_EPOCH_LENGTH, 19 | DEFAULT_ATTESTING_FEE, 20 | DEFAULT_TREE_DEPTHS_CONFIG, 21 | } -------------------------------------------------------------------------------- /cli/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { deployUnirep } from '../core' 3 | 4 | import { circuitEpochTreeDepth, circuitGlobalStateTreeDepth, circuitUserStateTreeDepth, maxAttesters, maxReputationBudget, maxUsers } from '../config/testLocal' 5 | import { DEFAULT_ATTESTING_FEE, DEFAULT_EPOCH_LENGTH, DEFAULT_ETH_PROVIDER, DEFAULT_MAX_EPOCH_KEY_NONCE } from './defaults' 6 | import { checkDeployerProviderConnection, genJsonRpcDeployer, validateEthSk, } from './utils' 7 | 8 | const configureSubparser = (subparsers: any) => { 9 | const deployParser = subparsers.add_parser( 10 | 'deploy', 11 | { add_help: true }, 12 | ) 13 | 14 | deployParser.add_argument( 15 | '-d', '--deployer-privkey', 16 | { 17 | action: 'store', 18 | type: 'str', 19 | help: 'The deployer\'s Ethereum private key', 20 | } 21 | ) 22 | 23 | deployParser.add_argument( 24 | '-e', '--eth-provider', 25 | { 26 | action: 'store', 27 | type: 'str', 28 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 29 | } 30 | ) 31 | 32 | deployParser.add_argument( 33 | '-l', '--epoch-length', 34 | { 35 | action: 'store', 36 | type: 'int', 37 | help: 'The length of an epoch in seconds. Default: 30', 38 | } 39 | ) 40 | 41 | deployParser.add_argument( 42 | '-f', '--attesting-fee', 43 | { 44 | action: 'store', 45 | type: 'str', 46 | help: 'The fee to make an attestation. Default: 0.01 eth (i.e., 10 * 16)', 47 | } 48 | ) 49 | } 50 | 51 | const deploy = async (args: any) => { 52 | 53 | // The deployer's Ethereum private key 54 | // They may either enter it as a command-line option or via the 55 | // standard input 56 | const deployerPrivkey = args.deployer_privkey 57 | 58 | if (!validateEthSk(deployerPrivkey)) { 59 | console.error('Error: invalid Ethereum private key') 60 | return 61 | } 62 | 63 | // Max epoch key nonce 64 | const _numEpochKeyNoncePerEpoch = DEFAULT_MAX_EPOCH_KEY_NONCE 65 | 66 | // Max reputation budget 67 | const _maxReputationBudget = maxReputationBudget 68 | 69 | // Epoch length 70 | const _epochLength = (args.epoch_length != undefined) ? args.epoch_length : DEFAULT_EPOCH_LENGTH 71 | 72 | // Attesting fee 73 | const _attestingFee = (args.attesting_fee != undefined) ? ethers.BigNumber.from(args.attesting_fee) : DEFAULT_ATTESTING_FEE 74 | 75 | const settings = { 76 | maxUsers: maxUsers, 77 | maxAttesters: maxAttesters, 78 | numEpochKeyNoncePerEpoch: _numEpochKeyNoncePerEpoch, 79 | maxReputationBudget: _maxReputationBudget, 80 | epochLength: _epochLength, 81 | attestingFee: _attestingFee 82 | } 83 | 84 | const treeDepths = { 85 | "userStateTreeDepth": circuitUserStateTreeDepth, 86 | "globalStateTreeDepth": circuitGlobalStateTreeDepth, 87 | "epochTreeDepth": circuitEpochTreeDepth, 88 | } 89 | 90 | // Ethereum provider 91 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 92 | 93 | if (! (await checkDeployerProviderConnection(deployerPrivkey, ethProvider))) { 94 | console.error('Error: unable to connect to the Ethereum provider at', ethProvider) 95 | return 96 | } 97 | const deployer = genJsonRpcDeployer(deployerPrivkey, ethProvider) 98 | debugger 99 | 100 | const contract = await deployUnirep( 101 | deployer.signer, 102 | treeDepths, 103 | settings, 104 | ) 105 | 106 | console.log('Unirep:', contract.address) 107 | } 108 | 109 | export { 110 | deploy, 111 | configureSubparser, 112 | } -------------------------------------------------------------------------------- /cli/epochTransition.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_ETH_PROVIDER } from './defaults' 2 | import { UnirepContract } from '../core' 3 | 4 | const configureSubparser = (subparsers: any) => { 5 | const parser = subparsers.add_parser( 6 | 'epochTransition', 7 | { add_help: true }, 8 | ) 9 | 10 | parser.add_argument( 11 | '-e', '--eth-provider', 12 | { 13 | action: 'store', 14 | type: 'str', 15 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 16 | } 17 | ) 18 | 19 | parser.add_argument( 20 | '-t', '--is-test', 21 | { 22 | action: 'store_true', 23 | help: 'Indicate if the provider is a testing environment', 24 | } 25 | ) 26 | 27 | parser.add_argument( 28 | '-x', '--contract', 29 | { 30 | required: true, 31 | type: 'str', 32 | help: 'The Unirep contract address', 33 | } 34 | ) 35 | 36 | parser.add_argument( 37 | '-d', '--eth-privkey', 38 | { 39 | action: 'store', 40 | type: 'str', 41 | help: 'The deployer\'s Ethereum private key', 42 | } 43 | ) 44 | } 45 | 46 | const epochTransition = async (args: any) => { 47 | 48 | // Ethereum provider 49 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 50 | 51 | // Unirep contract 52 | const unirepContract = new UnirepContract(args.contract, ethProvider) 53 | 54 | // Connect a signer 55 | await unirepContract.unlock(args.eth_privkey) 56 | 57 | // Fast-forward to end of epoch if in test environment 58 | if (args.is_test) { 59 | await unirepContract.fastForward() 60 | } 61 | 62 | const currentEpoch = await unirepContract.currentEpoch() 63 | const tx = await unirepContract.epochTransition() 64 | 65 | if(tx != undefined) { 66 | console.log('Transaction hash:', tx.hash) 67 | console.log('End of epoch:', currentEpoch.toString()) 68 | } 69 | } 70 | 71 | export { 72 | epochTransition, 73 | configureSubparser, 74 | } -------------------------------------------------------------------------------- /cli/genEpochKeyAndProof.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { ethers } from 'ethers' 3 | import { unSerialiseIdentity } from '../crypto' 4 | import { Circuit, formatProofForVerifierContract, verifyProof } from '../circuits/utils' 5 | 6 | import { DEFAULT_ETH_PROVIDER, DEFAULT_MAX_EPOCH_KEY_NONCE } from './defaults' 7 | import { genUserStateFromContract, genEpochKey, circuitEpochTreeDepth} from '../core' 8 | import { epkProofPrefix, epkPublicSignalsPrefix, identityPrefix } from './prefix' 9 | 10 | const configureSubparser = (subparsers: any) => { 11 | const parser = subparsers.add_parser( 12 | 'genEpochKeyAndProof', 13 | { add_help: true }, 14 | ) 15 | 16 | parser.add_argument( 17 | '-e', '--eth-provider', 18 | { 19 | action: 'store', 20 | type: 'str', 21 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 22 | } 23 | ) 24 | 25 | parser.add_argument( 26 | '-id', '--identity', 27 | { 28 | required: true, 29 | type: 'str', 30 | help: 'The (serialized) user\'s identity', 31 | } 32 | ) 33 | 34 | parser.add_argument( 35 | '-n', '--epoch-key-nonce', 36 | { 37 | required: true, 38 | type: 'int', 39 | help: 'The epoch key nonce', 40 | } 41 | ) 42 | 43 | parser.add_argument( 44 | '-x', '--contract', 45 | { 46 | required: true, 47 | type: 'str', 48 | help: 'The Unirep contract address', 49 | } 50 | ) 51 | } 52 | 53 | const genEpochKeyAndProof = async (args: any) => { 54 | // Ethereum provider 55 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 56 | const provider = new ethers.providers.JsonRpcProvider(ethProvider) 57 | 58 | // Validate epoch key nonce 59 | const epkNonce = args.epoch_key_nonce 60 | const numEpochKeyNoncePerEpoch = DEFAULT_MAX_EPOCH_KEY_NONCE 61 | if (epkNonce >= numEpochKeyNoncePerEpoch) { 62 | console.error('Error: epoch key nonce must be less than max epoch key nonce') 63 | return 64 | } 65 | 66 | // Gen epoch key 67 | const encodedIdentity = args.identity.slice(identityPrefix.length) 68 | const decodedIdentity = base64url.decode(encodedIdentity) 69 | const id = unSerialiseIdentity(decodedIdentity) 70 | const epochTreeDepth = circuitEpochTreeDepth 71 | 72 | // Gen User State 73 | const userState = await genUserStateFromContract( 74 | provider, 75 | args.contract, 76 | id, 77 | ) 78 | const results = await userState.genVerifyEpochKeyProof(epkNonce) 79 | const currentEpoch = userState.getUnirepStateCurrentEpoch() 80 | const epk = genEpochKey(id.identityNullifier, currentEpoch, epkNonce, epochTreeDepth).toString() 81 | 82 | // TODO: Not sure if this validation is necessary 83 | const isValid = await verifyProof(Circuit.verifyEpochKey, results.proof, results.publicSignals) 84 | if(!isValid) { 85 | console.error('Error: epoch key proof generated is not valid!') 86 | return 87 | } 88 | 89 | const formattedProof = formatProofForVerifierContract(results.proof) 90 | const encodedProof = base64url.encode(JSON.stringify(formattedProof)) 91 | const encodedPublicSignals = base64url.encode(JSON.stringify(results.publicSignals)) 92 | console.log(`Epoch key of epoch ${currentEpoch} and nonce ${epkNonce}: ${epk}`) 93 | console.log(epkProofPrefix + encodedProof) 94 | console.log(epkPublicSignalsPrefix + encodedPublicSignals) 95 | } 96 | 97 | export { 98 | genEpochKeyAndProof, 99 | configureSubparser, 100 | } -------------------------------------------------------------------------------- /cli/genReputationProof.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { ethers } from 'ethers' 3 | import { unSerialiseIdentity, add0x } from '../crypto' 4 | import { Circuit, formatProofForVerifierContract, verifyProof } from '../circuits/utils' 5 | 6 | import { DEFAULT_ETH_PROVIDER } from './defaults' 7 | import { genReputationNullifier, genUserStateFromContract, maxReputationBudget } from '../core' 8 | import { identityPrefix, reputationPublicSignalsPrefix, reputationProofPrefix } from './prefix' 9 | 10 | const configureSubparser = (subparsers: any) => { 11 | const parser = subparsers.add_parser( 12 | 'genReputationProof', 13 | { add_help: true }, 14 | ) 15 | 16 | parser.add_argument( 17 | '-e', '--eth-provider', 18 | { 19 | action: 'store', 20 | type: 'str', 21 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 22 | } 23 | ) 24 | 25 | parser.add_argument( 26 | '-id', '--identity', 27 | { 28 | required: true, 29 | type: 'str', 30 | help: 'The (serialized) user\'s identity', 31 | } 32 | ) 33 | 34 | parser.add_argument( 35 | '-n', '--epoch-key-nonce', 36 | { 37 | required: true, 38 | type: 'int', 39 | help: 'The epoch key nonce', 40 | } 41 | ) 42 | 43 | parser.add_argument( 44 | '-a', '--attester-id', 45 | { 46 | required: true, 47 | type: 'str', 48 | help: 'The attester id (in hex representation)', 49 | } 50 | ) 51 | 52 | parser.add_argument( 53 | '-r', '--reputation-nullifier', 54 | { 55 | type: 'int', 56 | help: 'The number of reputation nullifiers to prove', 57 | } 58 | ) 59 | 60 | parser.add_argument( 61 | '-mr', '--min-rep', 62 | { 63 | type: 'int', 64 | help: 'The minimum positive score minus negative score the attester given to the user', 65 | } 66 | ) 67 | 68 | 69 | parser.add_argument( 70 | '-gp', '--graffiti-preimage', 71 | { 72 | type: 'str', 73 | help: 'The pre-image of the graffiti for the reputation the attester given to the user (in hex representation)', 74 | } 75 | ) 76 | 77 | parser.add_argument( 78 | '-x', '--contract', 79 | { 80 | required: true, 81 | type: 'str', 82 | help: 'The Unirep contract address', 83 | } 84 | ) 85 | } 86 | 87 | const genReputationProof = async (args: any) => { 88 | 89 | // Ethereum provider 90 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 91 | const provider = new ethers.providers.JsonRpcProvider(ethProvider) 92 | 93 | // User Identity 94 | const encodedIdentity = args.identity.slice(identityPrefix.length) 95 | const decodedIdentity = base64url.decode(encodedIdentity) 96 | const id = unSerialiseIdentity(decodedIdentity) 97 | 98 | // Gen User State 99 | const userState = await genUserStateFromContract( 100 | provider, 101 | args.contract, 102 | id, 103 | ) 104 | 105 | // Proving content 106 | const epoch = userState.getUnirepStateCurrentEpoch() 107 | const attesterId = BigInt(add0x(args.attester_id)) 108 | const epkNonce = args.epoch_key_nonce 109 | const proveGraffiti = args.graffiti_preimage != null ? BigInt(1) : BigInt(0) 110 | const minRep = args.min_rep != null ? args.min_rep : 0 111 | const repNullifiersAmount = args.reputation_nullifier != null ? args.reputation_nullifier : 0 112 | const nonceList: BigInt[] = [] 113 | const rep = userState.getRepByAttester(attesterId) 114 | let nonceStarter: number = -1 115 | if(repNullifiersAmount > 0) { 116 | // find valid nonce starter 117 | for (let n = 0; n < Number(rep.posRep) - Number(rep.negRep); n++) { 118 | const reputationNullifier = genReputationNullifier(id.identityNullifier, epoch, n, attesterId) 119 | if(!userState.nullifierExist(reputationNullifier)) { 120 | nonceStarter = n 121 | break 122 | } 123 | } 124 | if(nonceStarter == -1) { 125 | console.error('Error: All nullifiers are spent') 126 | } 127 | if((nonceStarter + repNullifiersAmount) > Number(rep.posRep) - Number(rep.negRep)){ 128 | console.error('Error: Not enough reputation to spend') 129 | } 130 | for (let i = 0; i < repNullifiersAmount; i++) { 131 | nonceList.push( BigInt(nonceStarter + i) ) 132 | } 133 | } 134 | 135 | for (let i = repNullifiersAmount ; i < maxReputationBudget ; i++) { 136 | nonceList.push(BigInt(-1)) 137 | } 138 | const graffitiPreImage = args.graffiti_preimage != null ? BigInt(add0x(args.graffiti_preimage)) : BigInt(0) 139 | const results = await userState.genProveReputationProof(attesterId, epkNonce, minRep, proveGraffiti, graffitiPreImage, nonceList) 140 | 141 | console.log('repnullifier amount', repNullifiersAmount) 142 | 143 | // TODO: Not sure if this validation is necessary 144 | const isValid = await verifyProof(Circuit.proveReputation,results.proof, results.publicSignals) 145 | if(!isValid) { 146 | console.error('Error: reputation proof generated is not valid!') 147 | } 148 | 149 | const formattedProof = formatProofForVerifierContract(results.proof) 150 | const encodedProof = base64url.encode(JSON.stringify(formattedProof)) 151 | const encodedPublicSignals = base64url.encode(JSON.stringify(results.publicSignals)) 152 | console.log(`Proof of reputation from attester ${results.attesterId}:`) 153 | console.log(`Epoch key of the user: ${BigInt(results.epochKey).toString()}`) 154 | console.log(reputationProofPrefix + encodedProof) 155 | console.log(reputationPublicSignalsPrefix + encodedPublicSignals) 156 | } 157 | 158 | export { 159 | genReputationProof, 160 | configureSubparser, 161 | } -------------------------------------------------------------------------------- /cli/genUnirepIdentity.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { genIdentity, genIdentityCommitment, serialiseIdentity } from '../crypto' 3 | 4 | import { identityPrefix, identityCommitmentPrefix } from "./prefix" 5 | 6 | const configureSubparser = (subparsers: any) => { 7 | subparsers.add_parser( 8 | 'genUnirepIdentity', 9 | { add_help: true }, 10 | ) 11 | } 12 | 13 | const genUnirepIdentity = async (args: any) => { // eslint-disable-line @typescript-eslint/no-unused-vars 14 | const id = genIdentity() 15 | const commitment = genIdentityCommitment(id) 16 | 17 | const serializedIdentity = serialiseIdentity(id) 18 | const encodedIdentity = base64url.encode(serializedIdentity) 19 | console.log(identityPrefix + encodedIdentity) 20 | 21 | const serializedIdentityCommitment = commitment.toString(16) 22 | const encodedIdentityCommitment = base64url.encode(serializedIdentityCommitment) 23 | console.log(identityCommitmentPrefix + encodedIdentityCommitment) 24 | } 25 | 26 | export { 27 | genUnirepIdentity, 28 | configureSubparser, 29 | } -------------------------------------------------------------------------------- /cli/genUserSignUpProof.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { ethers } from 'ethers' 3 | import { unSerialiseIdentity, add0x } from '../crypto' 4 | import { Circuit, formatProofForVerifierContract, verifyProof } from '../circuits/utils' 5 | 6 | import { DEFAULT_ETH_PROVIDER } from './defaults' 7 | import { genUserStateFromContract } from '../core' 8 | import { identityPrefix, signUpProofPrefix, signUpPublicSignalsPrefix } from './prefix' 9 | 10 | const configureSubparser = (subparsers: any) => { 11 | const parser = subparsers.add_parser( 12 | 'genUserSignUpProof', 13 | { add_help: true }, 14 | ) 15 | 16 | parser.add_argument( 17 | '-e', '--eth-provider', 18 | { 19 | action: 'store', 20 | type: 'str', 21 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 22 | } 23 | ) 24 | 25 | parser.add_argument( 26 | '-id', '--identity', 27 | { 28 | required: true, 29 | type: 'str', 30 | help: 'The (serialized) user\'s identity', 31 | } 32 | ) 33 | 34 | parser.add_argument( 35 | '-a', '--attester-id', 36 | { 37 | required: true, 38 | type: 'str', 39 | help: 'The attester id (in hex representation)', 40 | } 41 | ) 42 | 43 | parser.add_argument( 44 | '-x', '--contract', 45 | { 46 | required: true, 47 | type: 'str', 48 | help: 'The Unirep contract address', 49 | } 50 | ) 51 | } 52 | 53 | const genUserSignUpProof = async (args: any) => { 54 | 55 | // Ethereum provider 56 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 57 | const provider = new ethers.providers.JsonRpcProvider(ethProvider) 58 | 59 | const encodedIdentity = args.identity.slice(identityPrefix.length) 60 | const decodedIdentity = base64url.decode(encodedIdentity) 61 | const id = unSerialiseIdentity(decodedIdentity) 62 | 63 | // Gen user sign up proof 64 | const userState = await genUserStateFromContract( 65 | provider, 66 | args.contract, 67 | id, 68 | ) 69 | const attesterId = BigInt(add0x(args.attester_id)) 70 | const results = await userState.genUserSignUpProof(attesterId) 71 | 72 | // TODO: Not sure if this validation is necessary 73 | const isValid = await verifyProof(Circuit.proveUserSignUp,results.proof, results.publicSignals) 74 | if(!isValid) { 75 | console.error('Error: user sign up proof generated is not valid!') 76 | return 77 | } 78 | 79 | const formattedProof = formatProofForVerifierContract(results.proof) 80 | const encodedProof = base64url.encode(JSON.stringify(formattedProof)) 81 | const encodedPublicSignals = base64url.encode(JSON.stringify(results.publicSignals)) 82 | console.log(`Proof of user sign up from attester ${results.attesterId}:`) 83 | console.log(`Epoch key of the user: ${BigInt(results.epochKey).toString()}`) 84 | console.log(signUpProofPrefix + encodedProof) 85 | console.log(signUpPublicSignalsPrefix + encodedPublicSignals) 86 | } 87 | 88 | export { 89 | genUserSignUpProof, 90 | configureSubparser, 91 | } -------------------------------------------------------------------------------- /cli/giveAirdrop.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { SignUpProof } from '../core' 3 | import { formatProofForSnarkjsVerification } from '../circuits/utils' 4 | import { DEFAULT_ETH_PROVIDER } from './defaults' 5 | import { UnirepContract } from '../core' 6 | import { verifyUserSignUpProof } from './verifyUserSignUpProof' 7 | import { signUpProofPrefix, signUpPublicSignalsPrefix } from './prefix' 8 | 9 | const configureSubparser = (subparsers: any) => { 10 | const parser = subparsers.add_parser( 11 | 'giveAirdrop', 12 | { add_help: true }, 13 | ) 14 | 15 | parser.add_argument( 16 | '-e', '--eth-provider', 17 | { 18 | action: 'store', 19 | type: 'str', 20 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 21 | } 22 | ) 23 | 24 | parser.add_argument( 25 | '-p', '--public-signals', 26 | { 27 | required: true, 28 | type: 'str', 29 | help: 'The snark public signals of the user\'s epoch key ', 30 | } 31 | ) 32 | 33 | parser.add_argument( 34 | '-pf', '--proof', 35 | { 36 | required: true, 37 | type: 'str', 38 | help: 'The snark proof of the user\'s epoch key ', 39 | } 40 | ) 41 | 42 | parser.add_argument( 43 | '-x', '--contract', 44 | { 45 | required: true, 46 | type: 'str', 47 | help: 'The Unirep contract address', 48 | } 49 | ) 50 | 51 | parser.add_argument( 52 | '-d', '--eth-privkey', 53 | { 54 | action: 'store', 55 | type: 'str', 56 | help: 'The attester\'s Ethereum private key', 57 | } 58 | ) 59 | } 60 | 61 | const giveAirdrop = async (args: any) => { 62 | 63 | // Ethereum provider 64 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 65 | 66 | // Unirep contract 67 | const unirepContract = new UnirepContract(args.contract, ethProvider) 68 | 69 | // Connect a signer 70 | await unirepContract.unlock(args.eth_privkey) 71 | 72 | await verifyUserSignUpProof(args) 73 | 74 | // Parse input 75 | const decodedProof = base64url.decode(args.proof.slice(signUpProofPrefix.length)) 76 | const proof = JSON.parse(decodedProof) 77 | const decodedPublicSignals = base64url.decode(args.public_signals.slice(signUpPublicSignalsPrefix.length)) 78 | const publicSignals = JSON.parse(decodedPublicSignals) 79 | const userSignUpProof = new SignUpProof( 80 | publicSignals, 81 | formatProofForSnarkjsVerification(proof) 82 | ) 83 | 84 | console.log(`Airdrop to epoch key ${userSignUpProof.epochKey} in attester ID ${userSignUpProof.attesterId}`) 85 | 86 | // Submit attestation 87 | const tx = await unirepContract.airdropEpochKey(userSignUpProof) 88 | await tx.wait() 89 | const proofIndex = await unirepContract.getSignUpProofIndex(userSignUpProof) 90 | if(tx != undefined){ 91 | console.log('Transaction hash:', tx?.hash) 92 | console.log('Proof index:', proofIndex.toNumber()) 93 | } 94 | } 95 | 96 | export { 97 | giveAirdrop, 98 | configureSubparser, 99 | } -------------------------------------------------------------------------------- /cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import argparse from 'argparse' 4 | 5 | import { 6 | genUnirepIdentity, 7 | configureSubparser as configureSubparserForGenUnirepIdentity, 8 | } from './genUnirepIdentity' 9 | 10 | import { 11 | deploy, 12 | configureSubparser as configureSubparserForDeploy, 13 | } from './deploy' 14 | 15 | import { 16 | userSignUp, 17 | configureSubparser as configureSubparserForUserSignup, 18 | } from './userSignUp' 19 | 20 | import { 21 | attesterSignUp, 22 | configureSubparser as configureSubparserForattesterSignUp, 23 | } from './attesterSignUp' 24 | 25 | import { 26 | genEpochKeyAndProof, 27 | configureSubparser as configureSubparserForGenEpochKeyAndProof, 28 | } from './genEpochKeyAndProof' 29 | 30 | import { 31 | verifyEpochKeyProof, 32 | configureSubparser as configureSubparserForVerifyEpochKeyProof, 33 | } from './verifyEpochKeyProof' 34 | 35 | import { 36 | setAirdropAmount, 37 | configureSubparser as configureSubparserForSetAirdropAmount, 38 | } from './setAirdropAmount' 39 | 40 | import { 41 | attest, 42 | configureSubparser as configureSubparserForAttest, 43 | } from './attest' 44 | 45 | import { 46 | giveAirdrop, 47 | configureSubparser as configureSubparserForGiveAirdrop, 48 | } from './giveAirdrop' 49 | 50 | import { 51 | spendReputation, 52 | configureSubparser as configureSubparserForSpendReputation, 53 | } from './spendReputation' 54 | 55 | import { 56 | submitEpochKeyProof, 57 | configureSubparser as configureSubparserForSubmitEpochKeyProof 58 | } from './submitEpochKeyProof' 59 | 60 | import { 61 | epochTransition, 62 | configureSubparser as configureSubparserForEpochTransition, 63 | } from './epochTransition' 64 | 65 | import { 66 | userStateTransition, 67 | configureSubparser as configureSubparserForGenUserStateTransitionProof, 68 | } from './userStateTransition' 69 | 70 | import { 71 | genReputationProof, 72 | configureSubparser as configureSubparserForGenReputationProof, 73 | } from './genReputationProof' 74 | 75 | import { 76 | verifyReputationProof, 77 | configureSubparser as configureSubparserForVerifyReputationProof, 78 | } from './verifyReputationProof' 79 | 80 | import { 81 | genUserSignUpProof, 82 | configureSubparser as configureSubparserForGenUserSignUpProof, 83 | } from './genUserSignUpProof' 84 | 85 | import { 86 | verifyUserSignUpProof, 87 | configureSubparser as configureSubparserForVerifyUserSignUpProof, 88 | } from './verifyUserSignUpProof' 89 | 90 | 91 | 92 | const main = async () => { 93 | const parser = new argparse.ArgumentParser({ 94 | description: 'Unirep', 95 | }) 96 | 97 | const subparsers = parser.add_subparsers({ 98 | title: 'Subcommands', 99 | dest: 'subcommand', 100 | }) 101 | 102 | // Subcommand: genUnirepIdentity 103 | configureSubparserForGenUnirepIdentity(subparsers) 104 | 105 | // Subcommand: deploy 106 | configureSubparserForDeploy(subparsers) 107 | 108 | // Subcommand: userSignup 109 | configureSubparserForUserSignup(subparsers) 110 | 111 | // Subcommand: attesterSignUp 112 | configureSubparserForattesterSignUp(subparsers) 113 | 114 | // Subcommand: genEpochKeyAndProof 115 | configureSubparserForGenEpochKeyAndProof(subparsers) 116 | 117 | // Subcommand: verifyEpochKeyProof 118 | configureSubparserForVerifyEpochKeyProof(subparsers) 119 | 120 | // Subcommand: setAirdropAmount 121 | configureSubparserForSetAirdropAmount(subparsers) 122 | 123 | // Subcommand: attest 124 | configureSubparserForAttest(subparsers) 125 | 126 | // Subcommand: giveAirdrop 127 | configureSubparserForGiveAirdrop(subparsers) 128 | 129 | // Subcommand: spendReputation 130 | configureSubparserForSpendReputation(subparsers) 131 | 132 | // Subcommand: submitEpochKeyProof 133 | configureSubparserForSubmitEpochKeyProof(subparsers) 134 | 135 | // Subcommand: epochTransition 136 | configureSubparserForEpochTransition(subparsers) 137 | 138 | // Subcommand: userStateTransition 139 | configureSubparserForGenUserStateTransitionProof(subparsers) 140 | 141 | // Subcommand: genReputationProof 142 | configureSubparserForGenReputationProof(subparsers) 143 | 144 | // Subcommand: verifyReputationProof 145 | configureSubparserForVerifyReputationProof(subparsers) 146 | 147 | // Subcommand: genUserSignUpProof 148 | configureSubparserForGenUserSignUpProof(subparsers) 149 | 150 | // Subcommand: verifyUserSignUpProof 151 | configureSubparserForVerifyUserSignUpProof(subparsers) 152 | 153 | const args = parser.parse_args() 154 | 155 | // Execute the subcommand method 156 | if (args.subcommand === 'genUnirepIdentity') { 157 | await genUnirepIdentity(args) 158 | } else if (args.subcommand === 'deploy') { 159 | await deploy(args) 160 | } else if (args.subcommand === 'userSignUp') { 161 | await userSignUp(args) 162 | } else if (args.subcommand === 'attesterSignUp') { 163 | await attesterSignUp(args) 164 | } else if (args.subcommand === 'genEpochKeyAndProof') { 165 | await genEpochKeyAndProof(args) 166 | } else if (args.subcommand === 'verifyEpochKeyProof') { 167 | await verifyEpochKeyProof(args) 168 | } else if (args.subcommand === 'setAirdropAmount') { 169 | await setAirdropAmount(args) 170 | } else if (args.subcommand === 'attest') { 171 | await attest(args) 172 | } else if (args.subcommand === 'giveAirdrop') { 173 | await giveAirdrop(args) 174 | } else if (args.subcommand === 'spendReputation') { 175 | await spendReputation(args) 176 | } else if (args.subcommand === 'submitEpochKeyProof') { 177 | await submitEpochKeyProof(args) 178 | } else if (args.subcommand === 'epochTransition') { 179 | await epochTransition(args) 180 | } else if (args.subcommand === 'userStateTransition') { 181 | await userStateTransition(args) 182 | } else if (args.subcommand === 'genReputationProof') { 183 | await genReputationProof(args) 184 | } else if (args.subcommand === 'verifyReputationProof') { 185 | await verifyReputationProof(args) 186 | } else if (args.subcommand === 'genUserSignUpProof') { 187 | await genUserSignUpProof(args) 188 | } else if (args.subcommand === 'verifyUserSignUpProof') { 189 | await verifyUserSignUpProof(args) 190 | } 191 | process.exit(0) 192 | } 193 | 194 | if (require.main === module) { 195 | main() 196 | } -------------------------------------------------------------------------------- /cli/prefix.ts: -------------------------------------------------------------------------------- 1 | export const identityPrefix = 'Unirep.identity.' 2 | 3 | export const identityCommitmentPrefix = 'Unirep.identityCommitment.' 4 | 5 | export const epkProofPrefix = 'Unirep.epk.proof.' 6 | 7 | export const epkPublicSignalsPrefix = 'Unirep.epk.publicSignals.' 8 | 9 | export const reputationProofPrefix = 'Unirep.reputation.proof.' 10 | 11 | export const reputationPublicSignalsPrefix = 'Unirep.reputation.publicSignals.' 12 | 13 | export const signUpProofPrefix = 'Unirep.signUp.proof.' 14 | 15 | export const signUpPublicSignalsPrefix = 'Unirep.signUp.publicSignals.' -------------------------------------------------------------------------------- /cli/setAirdropAmount.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { add0x } from '../crypto' 3 | 4 | import { DEFAULT_ETH_PROVIDER } from './defaults' 5 | import { Attestation, UnirepContract } from '../core' 6 | import { verifyEpochKeyProof } from './verifyEpochKeyProof' 7 | import { epkProofPrefix, epkPublicSignalsPrefix } from './prefix' 8 | import base64url from 'base64url' 9 | 10 | const configureSubparser = (subparsers: any) => { 11 | const parser = subparsers.add_parser( 12 | 'setAirdropAmount', 13 | { add_help: true }, 14 | ) 15 | 16 | parser.add_argument( 17 | '-e', '--eth-provider', 18 | { 19 | action: 'store', 20 | type: 'str', 21 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 22 | } 23 | ) 24 | 25 | parser.add_argument( 26 | '-x', '--contract', 27 | { 28 | required: true, 29 | type: 'str', 30 | help: 'The Unirep contract address', 31 | } 32 | ) 33 | 34 | parser.add_argument( 35 | '-a', '--airdrop', 36 | { 37 | required: true, 38 | type: 'int', 39 | help: 'The amount of airdrop positive reputation given by the attester' 40 | } 41 | ) 42 | 43 | parser.add_argument( 44 | '-d', '--eth-privkey', 45 | { 46 | action: 'store', 47 | type: 'str', 48 | help: 'The attester\'s Ethereum private key', 49 | } 50 | ) 51 | } 52 | 53 | const setAirdropAmount = async (args: any) => { 54 | 55 | // Ethereum provider 56 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 57 | 58 | // Unirep contract 59 | const unirepContract = new UnirepContract(args.contract, ethProvider) 60 | 61 | // Connect a signer 62 | await unirepContract.unlock(args.eth_privkey) 63 | 64 | // Parse input 65 | const airdropPosRep = args.airdrop 66 | const attesterId = await unirepContract.getAttesterId() 67 | console.log(`Attester ${attesterId} sets its airdrop amount to ${airdropPosRep}`) 68 | 69 | // Submit attestation 70 | const tx = await unirepContract.setAirdropAmount(airdropPosRep) 71 | if (tx != undefined) console.log('Transaction hash:', tx?.hash) 72 | } 73 | 74 | export { 75 | setAirdropAmount, 76 | configureSubparser, 77 | } -------------------------------------------------------------------------------- /cli/spendReputation.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { ReputationProof } from '../core' 3 | import { formatProofForSnarkjsVerification } from '../circuits/utils' 4 | 5 | import { DEFAULT_ETH_PROVIDER } from './defaults' 6 | import { verifyReputationProof } from './verifyReputationProof' 7 | import { UnirepContract } from '../core' 8 | import { reputationProofPrefix, reputationPublicSignalsPrefix } from './prefix' 9 | 10 | const configureSubparser = (subparsers: any) => { 11 | const parser = subparsers.add_parser( 12 | 'spendReputation', 13 | { add_help: true }, 14 | ) 15 | 16 | parser.add_argument( 17 | '-e', '--eth-provider', 18 | { 19 | action: 'store', 20 | type: 'str', 21 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 22 | } 23 | ) 24 | 25 | parser.add_argument( 26 | '-ep', '--epoch', 27 | { 28 | action: 'store', 29 | type: 'int', 30 | help: 'The latest epoch user transitioned to. Default: current epoch', 31 | } 32 | ) 33 | 34 | parser.add_argument( 35 | '-p', '--public-signals', 36 | { 37 | required: true, 38 | type: 'str', 39 | help: 'The snark public signals of the user\'s epoch key ', 40 | } 41 | ) 42 | 43 | parser.add_argument( 44 | '-pf', '--proof', 45 | { 46 | required: true, 47 | type: 'str', 48 | help: 'The snark proof of the user\'s epoch key ', 49 | } 50 | ) 51 | 52 | parser.add_argument( 53 | '-x', '--contract', 54 | { 55 | required: true, 56 | type: 'str', 57 | help: 'The Unirep contract address', 58 | } 59 | ) 60 | 61 | parser.add_argument( 62 | '-d', '--eth-privkey', 63 | { 64 | action: 'store', 65 | type: 'str', 66 | help: 'The attester\'s Ethereum private key', 67 | } 68 | ) 69 | } 70 | 71 | const spendReputation = async (args: any) => { 72 | 73 | // Ethereum provider 74 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 75 | 76 | // Unirep contract 77 | const unirepContract = new UnirepContract(args.contract, ethProvider) 78 | 79 | // Connect a signer 80 | await unirepContract.unlock(args.eth_privkey) 81 | 82 | await verifyReputationProof(args) 83 | 84 | // Parse Inputs 85 | const decodedProof = base64url.decode(args.proof.slice(reputationProofPrefix.length)) 86 | const decodedPublicSignals = base64url.decode(args.public_signals.slice(reputationPublicSignalsPrefix.length)) 87 | const proof = JSON.parse(decodedProof) 88 | const publicSignals = JSON.parse(decodedPublicSignals) 89 | const reputationProof = new ReputationProof( 90 | publicSignals, 91 | formatProofForSnarkjsVerification(proof) 92 | ) 93 | 94 | 95 | console.log(`User spends ${reputationProof.proveReputationAmount} reputation points from attester ${reputationProof.attesterId}`) 96 | 97 | // Submit reputation 98 | const tx = await unirepContract.spendReputation(reputationProof) 99 | await tx.wait() 100 | 101 | const proofIndex = await unirepContract.getReputationProofIndex(reputationProof) 102 | if(tx != undefined){ 103 | console.log('Transaction hash:', tx?.hash) 104 | console.log('Proof index:', proofIndex.toNumber()) 105 | } 106 | } 107 | 108 | export { 109 | spendReputation, 110 | configureSubparser, 111 | } -------------------------------------------------------------------------------- /cli/submitEpochKeyProof.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { ethers } from 'ethers' 3 | import { formatProofForSnarkjsVerification } from '../circuits/utils' 4 | import { EpochKeyProof } from '../core' 5 | 6 | import { DEFAULT_ETH_PROVIDER } from './defaults' 7 | import { genUnirepStateFromContract, UnirepContract } from '../core' 8 | import { epkProofPrefix, epkPublicSignalsPrefix } from './prefix' 9 | 10 | const configureSubparser = (subparsers: any) => { 11 | const parser = subparsers.add_parser( 12 | 'submitEpochKeyProof', 13 | { add_help: true }, 14 | ) 15 | 16 | parser.add_argument( 17 | '-e', '--eth-provider', 18 | { 19 | action: 'store', 20 | type: 'str', 21 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 22 | } 23 | ) 24 | 25 | parser.add_argument( 26 | '-p', '--public-signals', 27 | { 28 | required: true, 29 | type: 'str', 30 | help: 'The snark public signals of the user\'s epoch key ', 31 | } 32 | ) 33 | 34 | parser.add_argument( 35 | '-pf', '--proof', 36 | { 37 | required: true, 38 | type: 'str', 39 | help: 'The snark proof of the user\'s epoch key ', 40 | } 41 | ) 42 | 43 | parser.add_argument( 44 | '-x', '--contract', 45 | { 46 | required: true, 47 | type: 'str', 48 | help: 'The Unirep contract address', 49 | } 50 | ) 51 | 52 | parser.add_argument( 53 | '-d', '--eth-privkey', 54 | { 55 | action: 'store', 56 | type: 'str', 57 | help: 'The attester\'s Ethereum private key', 58 | } 59 | ) 60 | } 61 | 62 | const submitEpochKeyProof = async (args: any) => { 63 | 64 | // Ethereum provider 65 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 66 | 67 | // Unirep contract 68 | const unirepContract = new UnirepContract(args.contract, ethProvider) 69 | const currentEpoch = Number(await unirepContract.currentEpoch()) 70 | 71 | const decodedProof = base64url.decode(args.proof.slice(epkProofPrefix.length)) 72 | const decodedPublicSignals = base64url.decode(args.public_signals.slice(epkPublicSignalsPrefix.length)) 73 | const proof = JSON.parse(decodedProof) 74 | const publicSignals = JSON.parse(decodedPublicSignals) 75 | const epochKeyProof = new EpochKeyProof( 76 | publicSignals, 77 | formatProofForSnarkjsVerification(proof) 78 | ) 79 | const inputEpoch = epochKeyProof.epoch 80 | console.log(`Submit epoch key ${epochKeyProof.epochKey} with GSTRoot ${epochKeyProof.globalStateTree} in epoch ${inputEpoch}`) 81 | if(inputEpoch != currentEpoch) { 82 | console.log(`Warning: the epoch key is expired. Epoch key is in epoch ${inputEpoch}, but the current epoch is ${currentEpoch}`) 83 | } 84 | 85 | // Connect a signer 86 | await unirepContract.unlock(args.eth_privkey) 87 | 88 | // Submit epoch key proof 89 | const tx = await unirepContract.submitEpochKeyProof(epochKeyProof) 90 | const proofIndex = await unirepContract.getEpochKeyProofIndex(epochKeyProof) 91 | if(tx != undefined){ 92 | await tx.wait() 93 | console.log('Transaction hash:', tx?.hash) 94 | console.log('Proof index: ', proofIndex.toNumber()) 95 | } 96 | } 97 | 98 | export { 99 | submitEpochKeyProof, 100 | configureSubparser, 101 | } -------------------------------------------------------------------------------- /cli/test/utils.ts: -------------------------------------------------------------------------------- 1 | import * as shell from 'shelljs' 2 | 3 | const exec = (command: string) => { 4 | return shell.exec(command, { silent: true }) 5 | } 6 | 7 | export { exec } -------------------------------------------------------------------------------- /cli/userSignUp.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { add0x } from '../crypto' 3 | 4 | import { DEFAULT_ETH_PROVIDER } from './defaults' 5 | import { identityCommitmentPrefix } from './prefix' 6 | import { UnirepContract } from '../core' 7 | 8 | const configureSubparser = (subparsers: any) => { 9 | const parser = subparsers.add_parser( 10 | 'userSignUp', 11 | { add_help: true }, 12 | ) 13 | 14 | parser.add_argument( 15 | '-e', '--eth-provider', 16 | { 17 | action: 'store', 18 | type: 'str', 19 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 20 | } 21 | ) 22 | 23 | parser.add_argument( 24 | '-c', '--identity-commitment', 25 | { 26 | required: true, 27 | type: 'str', 28 | help: 'The user\'s identity commitment (in hex representation)', 29 | } 30 | ) 31 | 32 | parser.add_argument( 33 | '-x', '--contract', 34 | { 35 | required: true, 36 | type: 'str', 37 | help: 'The Unirep contract address', 38 | } 39 | ) 40 | 41 | parser.add_argument( 42 | '-d', '--eth-privkey', 43 | { 44 | action: 'store', 45 | type: 'str', 46 | help: 'The user\'s Ethereum private key', 47 | } 48 | ) 49 | } 50 | 51 | const userSignUp = async (args: any) => { 52 | 53 | // Ethereum provider 54 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 55 | 56 | // Unirep contract 57 | const unirepContract = new UnirepContract(args.contract, ethProvider) 58 | 59 | // Connect a signer 60 | await unirepContract.unlock(args.eth_privkey) 61 | 62 | // Parse identity commitment 63 | const encodedCommitment = args.identity_commitment.slice(identityCommitmentPrefix.length) 64 | const decodedCommitment = base64url.decode(encodedCommitment) 65 | const commitment = add0x(decodedCommitment) 66 | 67 | // Submit the user sign up transaction 68 | const tx = await unirepContract.userSignUp(commitment) 69 | const epoch = await unirepContract.currentEpoch() 70 | 71 | console.log('Transaction hash:', tx?.hash) 72 | console.log('Sign up epoch:', epoch.toString()) 73 | } 74 | 75 | export { 76 | userSignUp, 77 | configureSubparser, 78 | } -------------------------------------------------------------------------------- /cli/userStateTransition.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { ethers } from 'ethers' 3 | import { unSerialiseIdentity } from '../crypto' 4 | import { Circuit, verifyProof } from '../circuits/utils' 5 | 6 | import { DEFAULT_ETH_PROVIDER } from './defaults' 7 | import { genUserStateFromContract, UnirepContract } from '../core' 8 | import { identityPrefix } from './prefix' 9 | import { UserTransitionProof } from '../core' 10 | 11 | const configureSubparser = (subparsers: any) => { 12 | const parser = subparsers.add_parser( 13 | 'userStateTransition', 14 | { add_help: true }, 15 | ) 16 | 17 | parser.add_argument( 18 | '-e', '--eth-provider', 19 | { 20 | action: 'store', 21 | type: 'str', 22 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 23 | } 24 | ) 25 | 26 | parser.add_argument( 27 | '-id', '--identity', 28 | { 29 | required: true, 30 | type: 'str', 31 | help: 'The (serialized) user\'s identity', 32 | } 33 | ) 34 | 35 | parser.add_argument( 36 | '-x', '--contract', 37 | { 38 | required: true, 39 | type: 'str', 40 | help: 'The Unirep contract address', 41 | } 42 | ) 43 | 44 | parser.add_argument( 45 | '-d', '--eth-privkey', 46 | { 47 | action: 'store', 48 | type: 'str', 49 | help: 'The user\'s Ethereum private key', 50 | } 51 | ) 52 | } 53 | 54 | const userStateTransition = async (args: any) => { 55 | 56 | // Ethereum provider 57 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 58 | const provider = new ethers.providers.JsonRpcProvider(ethProvider) 59 | 60 | // Unirep contract 61 | const unirepContract = new UnirepContract(args.contract, ethProvider) 62 | 63 | // Connect a signer 64 | await unirepContract.unlock(args.eth_privkey) 65 | 66 | // Parse inputs 67 | const encodedIdentity = args.identity.slice(identityPrefix.length) 68 | const decodedIdentity = base64url.decode(encodedIdentity) 69 | const id = unSerialiseIdentity(decodedIdentity) 70 | 71 | // Generate user state transition proofs 72 | const userState = await genUserStateFromContract( 73 | provider, 74 | args.contract, 75 | id, 76 | ) 77 | const { 78 | startTransitionProof, 79 | processAttestationProofs, 80 | finalTransitionProof 81 | } = await userState.genUserStateTransitionProofs() 82 | 83 | // Start user state transition proof 84 | let isValid = await verifyProof(Circuit.startTransition, startTransitionProof.proof, startTransitionProof.publicSignals) 85 | if (!isValid) { 86 | console.error('Error: start state transition proof generated is not valid!') 87 | } 88 | let tx = await unirepContract.startUserStateTransition( 89 | startTransitionProof.blindedUserState, 90 | startTransitionProof.blindedHashChain, 91 | startTransitionProof.globalStateTreeRoot, 92 | startTransitionProof.proof, 93 | ) 94 | console.log('Transaction hash:', tx?.hash) 95 | await tx.wait() 96 | 97 | // process attestations proof 98 | for (let i = 0; i < processAttestationProofs.length; i++) { 99 | const isValid = await verifyProof(Circuit.processAttestations, processAttestationProofs[i].proof, processAttestationProofs[i].publicSignals) 100 | if (!isValid) { 101 | console.error('Error: process attestations proof generated is not valid!') 102 | } 103 | 104 | tx = await unirepContract.processAttestations( 105 | processAttestationProofs[i].outputBlindedUserState, 106 | processAttestationProofs[i].outputBlindedHashChain, 107 | processAttestationProofs[i].inputBlindedUserState, 108 | processAttestationProofs[i].proof, 109 | ) 110 | console.log('Transaction hash:', tx?.hash) 111 | await tx.wait() 112 | } 113 | 114 | // Record all proof indexes 115 | const proofIndexes: BigInt[] = [] 116 | const proofIndex = await unirepContract.getStartTransitionProofIndex( 117 | startTransitionProof.blindedUserState, 118 | startTransitionProof.blindedHashChain, 119 | startTransitionProof.globalStateTreeRoot, 120 | startTransitionProof.proof, 121 | ) 122 | proofIndexes.push(BigInt(proofIndex)) 123 | for (let i = 0; i < processAttestationProofs.length; i++) { 124 | const proofIndex = await unirepContract.getProcessAttestationsProofIndex( 125 | processAttestationProofs[i].outputBlindedUserState, 126 | processAttestationProofs[i].outputBlindedHashChain, 127 | processAttestationProofs[i].inputBlindedUserState, 128 | processAttestationProofs[i].proof, 129 | ) 130 | proofIndexes.push(BigInt(proofIndex)) 131 | } 132 | 133 | // update user state proof 134 | isValid = await verifyProof(Circuit.userStateTransition, finalTransitionProof.proof, finalTransitionProof.publicSignals) 135 | if (!isValid) { 136 | console.error('Error: user state transition proof generated is not valid!') 137 | } 138 | 139 | const fromEpoch = finalTransitionProof.transitionedFromEpoch 140 | const epkNullifiers = userState.getEpochKeyNullifiers(fromEpoch) 141 | 142 | // Verify nullifiers outputted by circuit are the same as the ones computed off-chain 143 | for (let i = 0; i < epkNullifiers.length; i++) { 144 | const outputNullifier = finalTransitionProof.epochKeyNullifiers[i] 145 | if (outputNullifier != epkNullifiers[i]) { 146 | console.error(`Error: nullifier outputted by circuit(${outputNullifier}) does not match the ${i}-th computed attestation nullifier(${epkNullifiers[i]})`) 147 | } 148 | } 149 | 150 | // Check if Global state tree root and epoch tree root exist 151 | const GSTRoot = finalTransitionProof.fromGSTRoot 152 | const inputEpoch = finalTransitionProof.transitionedFromEpoch 153 | const epochTreeRoot= finalTransitionProof.fromEpochTree 154 | const isGSTRootExisted = userState.GSTRootExists(GSTRoot, inputEpoch) 155 | const isEpochTreeExisted = await userState.epochTreeRootExists(epochTreeRoot, inputEpoch) 156 | if(!isGSTRootExisted) { 157 | console.error('Error: invalid global state tree root') 158 | return 159 | } 160 | if(!isEpochTreeExisted){ 161 | console.error('Error: invalid epoch tree root') 162 | return 163 | } 164 | // Check if nullifiers submitted before 165 | for (const nullifier of epkNullifiers) { 166 | if(userState.nullifierExist(nullifier)){ 167 | console.error('Error: nullifier submitted before') 168 | return 169 | } 170 | } 171 | 172 | // Submit the user state transition transaction 173 | const USTProof = new UserTransitionProof( 174 | finalTransitionProof.publicSignals, 175 | finalTransitionProof.proof 176 | ) 177 | tx = await unirepContract.updateUserStateRoot(USTProof,proofIndexes) 178 | if(tx != undefined) { 179 | await tx.wait() 180 | console.log('Transaction hash:', tx?.hash) 181 | const currentEpoch = await unirepContract.currentEpoch() 182 | console.log(`User transitioned from epoch ${fromEpoch} to epoch ${currentEpoch}`) 183 | } 184 | } 185 | 186 | export { 187 | userStateTransition, 188 | configureSubparser, 189 | } -------------------------------------------------------------------------------- /cli/utils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | 3 | class JSONRPCDeployer { 4 | 5 | provider: ethers.providers.Provider 6 | signer: ethers.Signer 7 | options: any 8 | 9 | constructor(privateKey: string, providerUrl: string, options?: any) { 10 | this.provider = new ethers.providers.JsonRpcProvider(providerUrl) 11 | this.signer = new ethers.Wallet(privateKey, this.provider) 12 | this.options = options 13 | } 14 | 15 | async deploy(abi: any, bytecode: any, ...args): Promise { 16 | const factory = new ethers.ContractFactory(abi, bytecode, this.signer) 17 | return await factory.deploy(...args) 18 | } 19 | } 20 | 21 | const genJsonRpcDeployer = ( 22 | privateKey: string, 23 | url: string, 24 | ) => { 25 | 26 | return new JSONRPCDeployer( 27 | privateKey, 28 | url, 29 | ) 30 | } 31 | 32 | const checkDeployerProviderConnection = async ( 33 | sk: string, 34 | ethProvider: string, 35 | ) => { 36 | 37 | const deployer = genJsonRpcDeployer(sk, ethProvider) 38 | try { 39 | await deployer.provider.getBlockNumber() 40 | } catch { 41 | return false 42 | } 43 | 44 | return true 45 | } 46 | 47 | const validateEthSk = (sk: string): boolean => { 48 | try { 49 | new ethers.Wallet(sk) 50 | } catch { 51 | return false 52 | } 53 | return true 54 | } 55 | 56 | const validateEthAddress = (address: string) => { 57 | return address.match(/^0x[a-fA-F0-9]{40}$/) != null 58 | } 59 | 60 | const contractExists = async ( 61 | provider: ethers.providers.Provider, 62 | address: string, 63 | ) => { 64 | const code = await provider.getCode(address) 65 | return code.length > 2 66 | } 67 | 68 | export { 69 | checkDeployerProviderConnection, 70 | contractExists, 71 | genJsonRpcDeployer, 72 | validateEthAddress, 73 | validateEthSk, 74 | } -------------------------------------------------------------------------------- /cli/verifyEpochKeyProof.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { ethers } from 'ethers' 3 | 4 | import { DEFAULT_ETH_PROVIDER } from './defaults' 5 | import { genUnirepStateFromContract, UnirepContract } from '../core' 6 | import { epkProofPrefix, epkPublicSignalsPrefix } from './prefix' 7 | import { EpochKeyProof } from '../core' 8 | import { formatProofForSnarkjsVerification } from '../circuits/utils' 9 | 10 | 11 | const configureSubparser = (subparsers: any) => { 12 | const parser = subparsers.add_parser( 13 | 'verifyEpochKeyProof', 14 | { add_help: true }, 15 | ) 16 | 17 | parser.add_argument( 18 | '-e', '--eth-provider', 19 | { 20 | action: 'store', 21 | type: 'str', 22 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 23 | } 24 | ) 25 | 26 | parser.add_argument( 27 | '-p', '--public-signals', 28 | { 29 | required: true, 30 | type: 'str', 31 | help: 'The snark public signals of the user\'s epoch key ', 32 | } 33 | ) 34 | 35 | parser.add_argument( 36 | '-pf', '--proof', 37 | { 38 | required: true, 39 | type: 'str', 40 | help: 'The snark proof of the user\'s epoch key ', 41 | } 42 | ) 43 | 44 | parser.add_argument( 45 | '-x', '--contract', 46 | { 47 | required: true, 48 | type: 'str', 49 | help: 'The Unirep contract address', 50 | } 51 | ) 52 | } 53 | 54 | const verifyEpochKeyProof = async (args: any) => { 55 | 56 | // Ethereum provider 57 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 58 | const provider = new ethers.providers.JsonRpcProvider(ethProvider) 59 | 60 | // Unirep contract 61 | const unirepContract = new UnirepContract(args.contract, ethProvider) 62 | 63 | const unirepState = await genUnirepStateFromContract( 64 | provider, 65 | args.contract, 66 | ) 67 | 68 | const decodedProof = base64url.decode(args.proof.slice(epkProofPrefix.length)) 69 | const decodedPublicSignals = base64url.decode(args.public_signals.slice(epkPublicSignalsPrefix.length)) 70 | const proof = JSON.parse(decodedProof) 71 | const publicSignals = JSON.parse(decodedPublicSignals) 72 | const currentEpoch = unirepState.currentEpoch 73 | const epk = publicSignals[2] 74 | const inputEpoch = publicSignals[1] 75 | const GSTRoot = publicSignals[0] 76 | console.log(`Verifying epoch key ${epk} with GSTRoot ${GSTRoot} in epoch ${inputEpoch}`) 77 | if(inputEpoch != currentEpoch) { 78 | console.log(`Warning: the epoch key is expired. Epoch key is in epoch ${inputEpoch}, but the current epoch is ${currentEpoch}`) 79 | } 80 | 81 | // Check if Global state tree root exists 82 | const isGSTRootExisted = unirepState.GSTRootExists(GSTRoot, inputEpoch) 83 | if(!isGSTRootExisted) { 84 | console.error('Error: invalid global state tree root') 85 | return 86 | } 87 | 88 | // Verify the proof on-chain 89 | const epkProof: EpochKeyProof = new EpochKeyProof( 90 | publicSignals, 91 | formatProofForSnarkjsVerification(proof), 92 | ) 93 | const isProofValid = await unirepContract.verifyEpochKeyValidity(epkProof) 94 | if (!isProofValid) { 95 | console.error('Error: invalid epoch key proof') 96 | return 97 | } 98 | console.log(`Verify epoch key proof with epoch key ${epk} succeed`) 99 | } 100 | 101 | export { 102 | verifyEpochKeyProof, 103 | configureSubparser, 104 | } -------------------------------------------------------------------------------- /cli/verifyReputationProof.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { ethers } from 'ethers' 3 | 4 | import { DEFAULT_ETH_PROVIDER } from './defaults' 5 | import { genUnirepStateFromContract, UnirepContract } from '../core' 6 | import { reputationProofPrefix, reputationPublicSignalsPrefix } from './prefix' 7 | import { maxReputationBudget } from '../config/testLocal' 8 | import { ReputationProof } from '../core' 9 | import { formatProofForSnarkjsVerification } from '../circuits/utils' 10 | 11 | const configureSubparser = (subparsers: any) => { 12 | const parser = subparsers.add_parser( 13 | 'verifyReputationProof', 14 | { add_help: true }, 15 | ) 16 | 17 | parser.add_argument( 18 | '-e', '--eth-provider', 19 | { 20 | action: 'store', 21 | type: 'str', 22 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 23 | } 24 | ) 25 | 26 | parser.add_argument( 27 | '-ep', '--epoch', 28 | { 29 | action: 'store', 30 | type: 'int', 31 | help: 'The latest epoch user transitioned to. Default: current epoch', 32 | } 33 | ) 34 | 35 | parser.add_argument( 36 | '-p', '--public-signals', 37 | { 38 | required: true, 39 | type: 'str', 40 | help: 'The snark public signals of the user\'s epoch key ', 41 | } 42 | ) 43 | 44 | parser.add_argument( 45 | '-pf', '--proof', 46 | { 47 | required: true, 48 | type: 'str', 49 | help: 'The snark proof of the user\'s epoch key ', 50 | } 51 | ) 52 | 53 | parser.add_argument( 54 | '-x', '--contract', 55 | { 56 | required: true, 57 | type: 'str', 58 | help: 'The Unirep contract address', 59 | } 60 | ) 61 | } 62 | 63 | const verifyReputationProof = async (args: any) => { 64 | 65 | // Ethereum provider 66 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 67 | const provider = new ethers.providers.JsonRpcProvider(ethProvider) 68 | 69 | // Unirep contract 70 | const unirepContract = new UnirepContract(args.contract, ethProvider) 71 | 72 | const unirepState = await genUnirepStateFromContract( 73 | provider, 74 | args.contract, 75 | ) 76 | 77 | // Parse Inputs 78 | const decodedProof = base64url.decode(args.proof.slice(reputationProofPrefix.length)) 79 | const decodedPublicSignals = base64url.decode(args.public_signals.slice(reputationPublicSignalsPrefix.length)) 80 | const publicSignals = JSON.parse(decodedPublicSignals) 81 | const outputNullifiers = publicSignals.slice(0, maxReputationBudget) 82 | const epoch = publicSignals[maxReputationBudget] 83 | const epk = publicSignals[maxReputationBudget + 1] 84 | const GSTRoot = publicSignals[maxReputationBudget + 2] 85 | const attesterId = publicSignals[maxReputationBudget + 3] 86 | const repNullifiersAmount = publicSignals[maxReputationBudget + 4] 87 | const minRep = publicSignals[maxReputationBudget + 5] 88 | const proveGraffiti = publicSignals[maxReputationBudget + 6] 89 | const graffitiPreImage = publicSignals[maxReputationBudget + 7] 90 | const proof = JSON.parse(decodedProof) 91 | 92 | // Check if Global state tree root exists 93 | const isGSTRootExisted = unirepState.GSTRootExists(GSTRoot, epoch) 94 | if(!isGSTRootExisted) { 95 | console.error('Error: invalid global state tree root') 96 | return 97 | } 98 | 99 | // Verify the proof on-chain 100 | const reputationProof = new ReputationProof( 101 | publicSignals, 102 | formatProofForSnarkjsVerification(proof) 103 | ) 104 | const isProofValid = await unirepContract.verifyReputation(reputationProof) 105 | if (!isProofValid) { 106 | console.error('Error: invalid reputation proof') 107 | return 108 | } 109 | 110 | console.log(`Epoch key of the user: ${epk}`) 111 | console.log(`Verify reputation proof from attester ${attesterId} with min rep ${minRep}, reputation nullifiers amount ${repNullifiersAmount} and graffiti pre-image ${args.graffiti_preimage}, succeed`) 112 | } 113 | 114 | export { 115 | verifyReputationProof, 116 | configureSubparser, 117 | } -------------------------------------------------------------------------------- /cli/verifyUserSignUpProof.ts: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | import { ethers } from 'ethers' 3 | 4 | import { DEFAULT_ETH_PROVIDER } from './defaults' 5 | import { genUnirepStateFromContract, UnirepContract } from '../core' 6 | import { signUpProofPrefix, signUpPublicSignalsPrefix } from './prefix' 7 | import { SignUpProof } from '../core' 8 | import { formatProofForSnarkjsVerification } from '../circuits/utils' 9 | 10 | const configureSubparser = (subparsers: any) => { 11 | const parser = subparsers.add_parser( 12 | 'verifyUserSignUpProof', 13 | { add_help: true }, 14 | ) 15 | 16 | parser.add_argument( 17 | '-e', '--eth-provider', 18 | { 19 | action: 'store', 20 | type: 'str', 21 | help: `A connection string to an Ethereum provider. Default: ${DEFAULT_ETH_PROVIDER}`, 22 | } 23 | ) 24 | 25 | parser.add_argument( 26 | '-ep', '--epoch', 27 | { 28 | action: 'store', 29 | type: 'int', 30 | help: 'The latest epoch user transitioned to. Default: current epoch', 31 | } 32 | ) 33 | 34 | parser.add_argument( 35 | '-p', '--public-signals', 36 | { 37 | required: true, 38 | type: 'str', 39 | help: 'The snark public signals of the user\'s epoch key ', 40 | } 41 | ) 42 | 43 | parser.add_argument( 44 | '-pf', '--proof', 45 | { 46 | required: true, 47 | type: 'str', 48 | help: 'The snark proof of the user\'s epoch key ', 49 | } 50 | ) 51 | 52 | parser.add_argument( 53 | '-x', '--contract', 54 | { 55 | required: true, 56 | type: 'str', 57 | help: 'The Unirep contract address', 58 | } 59 | ) 60 | } 61 | 62 | const verifyUserSignUpProof = async (args: any) => { 63 | 64 | // Ethereum provider 65 | const ethProvider = args.eth_provider ? args.eth_provider : DEFAULT_ETH_PROVIDER 66 | const provider = new ethers.providers.JsonRpcProvider(ethProvider) 67 | 68 | // Unirep contract 69 | const unirepContract = new UnirepContract(args.contract, ethProvider) 70 | 71 | const unirepState = await genUnirepStateFromContract( 72 | provider, 73 | args.contract, 74 | ) 75 | 76 | // Parse Inputs 77 | const decodedProof = base64url.decode(args.proof.slice(signUpProofPrefix.length)) 78 | const decodedPublicSignals = base64url.decode(args.public_signals.slice(signUpPublicSignalsPrefix.length)) 79 | const publicSignals = JSON.parse(decodedPublicSignals) 80 | const epoch = publicSignals[0] 81 | const epk = publicSignals[1] 82 | const GSTRoot = publicSignals[2] 83 | const attesterId = publicSignals[3] 84 | const userHasSignedUp = publicSignals[4] 85 | const proof = JSON.parse(decodedProof) 86 | 87 | // Check if Global state tree root exists 88 | const isGSTRootExisted = unirepState.GSTRootExists(GSTRoot, epoch) 89 | if(!isGSTRootExisted) { 90 | console.error('Error: invalid global state tree root') 91 | return 92 | } 93 | 94 | // Verify the proof on-chain 95 | const signUpProof = new SignUpProof( 96 | publicSignals, 97 | formatProofForSnarkjsVerification(proof) 98 | ) 99 | const isProofValid = await unirepContract.verifyUserSignUp(signUpProof) 100 | if (!isProofValid) { 101 | console.error('Error: invalid user sign up proof') 102 | return 103 | } 104 | 105 | console.log(`Epoch key of the user: ${epk}`) 106 | console.log(`Verify user sign up proof from attester ${attesterId} succeed`) 107 | } 108 | 109 | export { 110 | verifyUserSignUpProof, 111 | configureSubparser, 112 | } -------------------------------------------------------------------------------- /config/circuitPath.ts: -------------------------------------------------------------------------------- 1 | const verifyEpochKeyCircuitPath = '../build/verifyEpochKey_main.circom' 2 | 3 | const proveReputationCircuitPath = '../build/proveReputation_main.circom' 4 | 5 | const proveUserSignUpCircuitPath = '../build/proveUserSignUp_main.circom' 6 | 7 | const startTransitionCircuitPath = '../build/startTransition_main.circom' 8 | 9 | const processAttestationsCircuitPath = '../build/processAttestations_main.circom' 10 | 11 | const userStateTransitionCircuitPath = '../build/userStateTransition_main.circom' 12 | 13 | export { 14 | verifyEpochKeyCircuitPath, 15 | proveReputationCircuitPath, 16 | proveUserSignUpCircuitPath, 17 | startTransitionCircuitPath, 18 | processAttestationsCircuitPath, 19 | userStateTransitionCircuitPath, 20 | } -------------------------------------------------------------------------------- /config/nullifierDomainSeparator.ts: -------------------------------------------------------------------------------- 1 | const EPOCH_KEY_NULLIFIER_DOMAIN = BigInt(1) 2 | const REPUTATION_NULLIFIER_DOMAIN = BigInt(2) 3 | 4 | export { 5 | EPOCH_KEY_NULLIFIER_DOMAIN, 6 | REPUTATION_NULLIFIER_DOMAIN, 7 | } -------------------------------------------------------------------------------- /config/testLocal.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | 3 | const globalStateTreeDepth = 16; 4 | 5 | const userStateTreeDepth = 16; 6 | 7 | const epochTreeDepth = 64; 8 | 9 | const attestingFee = ethers.utils.parseEther("0.01") 10 | 11 | const numEpochKeyNoncePerEpoch = 3; 12 | 13 | const numAttestationsPerProof = 5; 14 | 15 | const epochLength = 30; // 30 seconds 16 | 17 | 18 | const circuitGlobalStateTreeDepth = 4; 19 | 20 | const circuitUserStateTreeDepth = 4; 21 | 22 | const circuitEpochTreeDepth = 32; 23 | 24 | const maxReputationBudget = 10; 25 | 26 | const maxUsers = 2 ** circuitGlobalStateTreeDepth - 1; 27 | 28 | const maxAttesters = 2 ** circuitUserStateTreeDepth - 1; 29 | 30 | export { 31 | attestingFee, 32 | circuitGlobalStateTreeDepth, 33 | circuitUserStateTreeDepth, 34 | circuitEpochTreeDepth, 35 | epochLength, 36 | epochTreeDepth, 37 | globalStateTreeDepth, 38 | numEpochKeyNoncePerEpoch, 39 | numAttestationsPerProof, 40 | maxUsers, 41 | maxAttesters, 42 | userStateTreeDepth, 43 | maxReputationBudget, 44 | } -------------------------------------------------------------------------------- /contracts/Address.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.0; 4 | 5 | /** 6 | * @dev Collection of functions related to the address type 7 | */ 8 | library Address { 9 | /** 10 | * @dev Replacement for Solidity's `transfer`: sends `amount` wei to 11 | * `recipient`, forwarding all available gas and reverting on errors. 12 | * 13 | * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost 14 | * of certain opcodes, possibly making contracts go over the 2300 gas limit 15 | * imposed by `transfer`, making them unable to receive funds via 16 | * `transfer`. {sendValue} removes this limitation. 17 | * 18 | * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. 19 | * 20 | * IMPORTANT: because control is transferred to `recipient`, care must be 21 | * taken to not create reentrancy vulnerabilities. Consider using 22 | * {ReentrancyGuard} or the 23 | * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. 24 | */ 25 | function sendValue(address payable recipient, uint256 amount) internal { 26 | require(address(this).balance >= amount, "Address: insufficient balance"); 27 | 28 | // solhint-disable-next-line avoid-low-level-calls, avoid-call-value 29 | (bool success, ) = recipient.call{ value: amount }(""); 30 | require(success, "Address: unable to send value, recipient may have reverted"); 31 | } 32 | } -------------------------------------------------------------------------------- /contracts/Hasher.sol: -------------------------------------------------------------------------------- 1 | /* 2 | * Hasher object to abstract out hashing logic 3 | * to be shared between multiple files 4 | * 5 | * This file is part of maci 6 | */ 7 | 8 | pragma solidity 0.8.0; 9 | 10 | import { UnirepObjs } from "./UnirepObjs.sol"; 11 | 12 | contract Hasher is UnirepObjs { 13 | 14 | function hashEpochKeyProof(EpochKeyProof memory _input) public pure returns (bytes32) { 15 | return keccak256(abi.encodePacked( 16 | _input.globalStateTree, 17 | _input.epoch, 18 | _input.epochKey, 19 | _input.proof 20 | )); 21 | } 22 | 23 | function hashReputationProof(ReputationProof memory _input) public pure returns (bytes32) { 24 | return keccak256(abi.encodePacked( 25 | _input.repNullifiers, 26 | _input.epoch, 27 | _input.epochKey, 28 | _input.globalStateTree, 29 | _input.attesterId, 30 | _input.proveReputationAmount, 31 | _input.minRep, 32 | _input.proveGraffiti, 33 | _input.graffitiPreImage, 34 | _input.proof 35 | )); 36 | } 37 | 38 | function hashSignUpProof(SignUpProof memory _input) public pure returns (bytes32) { 39 | return keccak256(abi.encodePacked( 40 | _input.epoch, 41 | _input.epochKey, 42 | _input.globalStateTree, 43 | _input.attesterId, 44 | _input.userHasSignedUp, 45 | _input.proof 46 | )); 47 | } 48 | 49 | function hashStartTransitionProof( 50 | uint256 _blindedUserState, 51 | uint256 _blindedHashChain, 52 | uint256 _globalStateTree, 53 | uint256[8] memory _proof 54 | ) public pure returns (bytes32) { 55 | return keccak256(abi.encodePacked( 56 | _blindedUserState, 57 | _blindedHashChain, 58 | _globalStateTree, 59 | _proof 60 | )); 61 | } 62 | 63 | function hashProcessAttestationsProof( 64 | uint256 _outputBlindedUserState, 65 | uint256 _outputBlindedHashChain, 66 | uint256 _inputBlindedUserState, 67 | uint256[8] calldata _proof 68 | ) public pure returns (bytes32) { 69 | return keccak256(abi.encodePacked( 70 | _outputBlindedUserState, 71 | _outputBlindedHashChain, 72 | _inputBlindedUserState, 73 | _proof 74 | )); 75 | } 76 | 77 | function hashUserStateTransitionProof(UserTransitionProof memory _input) public pure returns (bytes32) { 78 | return keccak256(abi.encodePacked( 79 | _input.newGlobalStateTreeLeaf, 80 | _input.epkNullifiers, 81 | _input.transitionFromEpoch, 82 | _input.blindedUserStates, 83 | _input.fromGlobalStateTree, 84 | _input.blindedHashChains, 85 | _input.fromEpochTree, 86 | _input.proof 87 | )); 88 | } 89 | } -------------------------------------------------------------------------------- /contracts/SnarkConstants.sol: -------------------------------------------------------------------------------- 1 | /* 2 | * Semaphore - Zero-knowledge signaling on Ethereum 3 | * Copyright (C) 2020 Barry WhiteHat , Kobi 4 | * Gurkan and Koh Wei Jie (contact@kohweijie.com) 5 | * 6 | * This file is part of Semaphore. 7 | * 8 | * Semaphore is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * Semaphore is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with Semaphore. If not, see . 20 | */ 21 | 22 | pragma solidity 0.8.0; 23 | 24 | contract SnarkConstants { 25 | // The scalar field 26 | uint256 internal constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; 27 | } -------------------------------------------------------------------------------- /contracts/UnirepObjs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma abicoder v2; 3 | pragma solidity 0.8.0; 4 | 5 | contract UnirepObjs{ 6 | struct Attestation { 7 | // The attester’s ID 8 | uint256 attesterId; 9 | // Positive reputation 10 | uint256 posRep; 11 | // Negative reputation 12 | uint256 negRep; 13 | // A hash of an arbitary string 14 | uint256 graffiti; 15 | // A flag to indicate if user has signed up in the attester's app 16 | uint256 signUp; 17 | } 18 | 19 | struct TreeDepths { 20 | uint8 globalStateTreeDepth; 21 | uint8 userStateTreeDepth; 22 | uint8 epochTreeDepth; 23 | } 24 | 25 | struct MaxValues { 26 | uint256 maxUsers; 27 | uint256 maxAttesters; 28 | } 29 | 30 | struct ProofsRelated { 31 | uint256[2] a; 32 | uint256[2][2] b; 33 | uint256[2] c; 34 | bool isValid; 35 | } 36 | 37 | struct EpochKeyProof{ 38 | uint256 globalStateTree; 39 | uint256 epoch; 40 | uint256 epochKey; 41 | uint256[8] proof; 42 | } 43 | 44 | struct SignUpProof{ 45 | uint256 epoch; 46 | uint256 epochKey; 47 | uint256 globalStateTree; 48 | uint256 attesterId; 49 | uint256 userHasSignedUp; 50 | uint256[8] proof; 51 | } 52 | 53 | struct UserTransitionProof{ 54 | uint256 newGlobalStateTreeLeaf; 55 | uint256[] epkNullifiers; 56 | uint256 transitionFromEpoch; 57 | uint256[] blindedUserStates; 58 | uint256 fromGlobalStateTree; 59 | uint256[] blindedHashChains; 60 | uint256 fromEpochTree; 61 | uint256[8] proof; 62 | } 63 | 64 | struct ReputationProof{ 65 | uint256[] repNullifiers; 66 | uint256 epoch; 67 | uint256 epochKey; 68 | uint256 globalStateTree; 69 | uint256 attesterId; 70 | uint256 proveReputationAmount; 71 | uint256 minRep; 72 | uint256 proveGraffiti; 73 | uint256 graffitiPreImage; 74 | uint256[8] proof; 75 | } 76 | } -------------------------------------------------------------------------------- /core/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UnirepContract, 3 | } from './UnirepContract' 4 | 5 | import { 6 | Attestation, 7 | IAttestation, 8 | IEpochTreeLeaf, 9 | ISettings, 10 | IUnirepState, 11 | UnirepState, 12 | } from './UnirepState' 13 | 14 | import { 15 | IReputation, 16 | IUserStateLeaf, 17 | IUserState, 18 | Reputation, 19 | UserState, 20 | } from './UserState' 21 | 22 | import { 23 | defaultUserStateLeaf, 24 | SMT_ONE_LEAF, 25 | SMT_ZERO_LEAF, 26 | Event, 27 | AttestationEvent, 28 | IEpochKeyProof, 29 | IReputationProof, 30 | ISignUpProof, 31 | IUserTransitionProof, 32 | EpochKeyProof, 33 | ReputationProof, 34 | SignUpProof, 35 | UserTransitionProof, 36 | computeStartTransitionProofHash, 37 | computeProcessAttestationsProofHash, 38 | deployUnirep, 39 | getUnirepContract, 40 | Unirep, 41 | computeEmptyUserStateRoot, 42 | computeInitUserStateRoot, 43 | formatProofForSnarkjsVerification, 44 | verifyEpochKeyProofEvent, 45 | verifyReputationProofEvent, 46 | verifySignUpProofEvent, 47 | verifyStartTransitionProofEvent, 48 | verifyProcessAttestationEvent, 49 | verifyProcessAttestationEvents, 50 | verifyUserStateTransitionEvent, 51 | verifyUSTEvents, 52 | genEpochKey, 53 | genEpochKeyNullifier, 54 | genReputationNullifier, 55 | genNewSMT, 56 | genUnirepStateFromContract, 57 | genUnirepStateFromParams, 58 | genUserStateFromContract, 59 | genUserStateFromParams, 60 | } from './utils' 61 | 62 | import { 63 | EPOCH_KEY_NULLIFIER_DOMAIN, 64 | REPUTATION_NULLIFIER_DOMAIN, 65 | } from '../config/nullifierDomainSeparator' 66 | 67 | import { 68 | attestingFee, 69 | circuitGlobalStateTreeDepth, 70 | circuitUserStateTreeDepth, 71 | circuitEpochTreeDepth, 72 | epochLength, 73 | epochTreeDepth, 74 | globalStateTreeDepth, 75 | numEpochKeyNoncePerEpoch, 76 | numAttestationsPerProof, 77 | maxUsers, 78 | maxAttesters, 79 | userStateTreeDepth, 80 | maxReputationBudget, 81 | } from '../config/testLocal' 82 | 83 | export { 84 | UnirepContract, 85 | Attestation, 86 | IAttestation, 87 | IEpochTreeLeaf, 88 | ISettings, 89 | IUnirepState, 90 | UnirepState, 91 | IReputation, 92 | IUserStateLeaf, 93 | IUserState, 94 | Reputation, 95 | UserState, 96 | defaultUserStateLeaf, 97 | SMT_ONE_LEAF, 98 | SMT_ZERO_LEAF, 99 | Event, 100 | AttestationEvent, 101 | IEpochKeyProof, 102 | IReputationProof, 103 | ISignUpProof, 104 | IUserTransitionProof, 105 | EpochKeyProof, 106 | ReputationProof, 107 | SignUpProof, 108 | UserTransitionProof, 109 | computeStartTransitionProofHash, 110 | computeProcessAttestationsProofHash, 111 | deployUnirep, 112 | getUnirepContract, 113 | Unirep, 114 | computeEmptyUserStateRoot, 115 | computeInitUserStateRoot, 116 | formatProofForSnarkjsVerification, 117 | verifyEpochKeyProofEvent, 118 | verifyReputationProofEvent, 119 | verifySignUpProofEvent, 120 | verifyStartTransitionProofEvent, 121 | verifyProcessAttestationEvent, 122 | verifyProcessAttestationEvents, 123 | verifyUserStateTransitionEvent, 124 | verifyUSTEvents, 125 | genEpochKey, 126 | genEpochKeyNullifier, 127 | genReputationNullifier, 128 | genNewSMT, 129 | genUnirepStateFromContract, 130 | genUnirepStateFromParams, 131 | genUserStateFromContract, 132 | genUserStateFromParams, 133 | EPOCH_KEY_NULLIFIER_DOMAIN, 134 | REPUTATION_NULLIFIER_DOMAIN, 135 | attestingFee, 136 | circuitGlobalStateTreeDepth, 137 | circuitUserStateTreeDepth, 138 | circuitEpochTreeDepth, 139 | epochLength, 140 | epochTreeDepth, 141 | globalStateTreeDepth, 142 | numEpochKeyNoncePerEpoch, 143 | numAttestationsPerProof, 144 | maxUsers, 145 | maxAttesters, 146 | userStateTreeDepth, 147 | maxReputationBudget, 148 | } -------------------------------------------------------------------------------- /crypto/SMT/SparseMerkleTree.ts: -------------------------------------------------------------------------------- 1 | /* External Imports */ 2 | import assert from 'assert' 3 | 4 | /* Internal Imports */ 5 | import { newWrappedPoseidonT3Hash } from '../crypto' 6 | 7 | 8 | export class SparseMerkleTreeImpl { 9 | protected root?: BigInt 10 | private zeroHashes!: BigInt[] 11 | 12 | public readonly numLeaves: BigInt 13 | 14 | public static async create( 15 | db, 16 | height: number, 17 | zeroHash: BigInt, 18 | ): Promise { 19 | const tree = new SparseMerkleTreeImpl(db, height) 20 | await tree.init(zeroHash) 21 | return tree 22 | } 23 | 24 | constructor( 25 | protected db, 26 | private height: number, 27 | ) { 28 | assert(height > 0, 'SMT height needs to be > 0') 29 | 30 | this.numLeaves = BigInt(2 ** height) 31 | } 32 | 33 | private async init(zeroHash: BigInt): Promise { 34 | await this.populateZeroHashesAndRoot(zeroHash) 35 | } 36 | 37 | public getHeight(): number { 38 | return this.height 39 | } 40 | 41 | public getRootHash(): BigInt { 42 | return this.root! 43 | } 44 | 45 | public getZeroHash(index: number): BigInt { 46 | return this.zeroHashes[index] 47 | } 48 | 49 | public async update( 50 | leafKey: BigInt, 51 | leafHash: BigInt, 52 | ): Promise { 53 | assert(leafKey < this.numLeaves, `leaf key ${leafKey} exceeds total number of leaves ${this.numLeaves}`) 54 | 55 | let nodeIndex = leafKey.valueOf() + this.numLeaves.valueOf() 56 | let nodeHash 57 | const nodeHashString = await this.db.get(nodeIndex.toString()) 58 | if (nodeHashString === leafHash.toString()) return 59 | else nodeHash = leafHash 60 | 61 | await this.db.set(nodeIndex.toString(), nodeHash.toString()) 62 | 63 | let isLeftNode = nodeIndex % BigInt(2) === BigInt(0) ? true : false 64 | let sibNodeIndex = isLeftNode ? nodeIndex + BigInt(1) : nodeIndex - BigInt(1) 65 | let sibNodeHash 66 | let parentNodeIndex, parentHash 67 | for (let i = 0; i < this.height; i++) { 68 | const sibNodeHashString = await this.db.get(sibNodeIndex.toString()) 69 | if (!sibNodeHashString) sibNodeHash = this.zeroHashes[i] 70 | else sibNodeHash = BigInt(sibNodeHashString) 71 | 72 | parentNodeIndex = nodeIndex / BigInt(2) 73 | parentHash = isLeftNode ? newWrappedPoseidonT3Hash(nodeHash, sibNodeHash) : newWrappedPoseidonT3Hash(sibNodeHash, nodeHash) 74 | await this.db.set(parentNodeIndex.toString(), parentHash.toString()) 75 | 76 | nodeIndex = parentNodeIndex 77 | nodeHash = parentHash 78 | isLeftNode = nodeIndex % BigInt(2) === BigInt(0) ? true : false 79 | sibNodeIndex = isLeftNode ? nodeIndex + BigInt(1) : nodeIndex - BigInt(1) 80 | } 81 | assert(nodeIndex === BigInt(1), "Root node index must be 1") 82 | this.root = parentHash 83 | } 84 | 85 | public async getMerkleProof( 86 | leafKey: BigInt, 87 | ): Promise { 88 | assert(leafKey < this.numLeaves, `leaf key ${leafKey} exceeds total number of leaves ${this.numLeaves}`) 89 | 90 | const siblingNodeHashes: BigInt[] = [] 91 | let nodeIndex = leafKey.valueOf() + this.numLeaves.valueOf() 92 | let isLeftNode = nodeIndex % BigInt(2) === BigInt(0) ? true : false 93 | let sibNodeIndex = isLeftNode ? nodeIndex + BigInt(1) : nodeIndex - BigInt(1) 94 | let sibNodeHash 95 | for (let i = 0; i < this.height; i++) { 96 | const sibNodeHashString = await this.db.get(sibNodeIndex.toString()) 97 | if (!sibNodeHashString) sibNodeHash = this.zeroHashes[i] 98 | else sibNodeHash = BigInt(sibNodeHashString) 99 | siblingNodeHashes.push(sibNodeHash) 100 | 101 | nodeIndex = nodeIndex / BigInt(2) 102 | isLeftNode = nodeIndex % BigInt(2) === BigInt(0) ? true : false 103 | sibNodeIndex = isLeftNode ? nodeIndex + BigInt(1) : nodeIndex - BigInt(1) 104 | } 105 | assert(siblingNodeHashes.length == this.height, "Incorrect number of proof entries") 106 | return siblingNodeHashes 107 | } 108 | 109 | public async verifyMerkleProof( 110 | leafKey: BigInt, 111 | proof: BigInt[] 112 | ): Promise { 113 | assert(leafKey < this.numLeaves, `leaf key ${leafKey} exceeds total number of leaves ${this.numLeaves}`) 114 | assert(proof.length == this.height, "Incorrect number of proof entries") 115 | 116 | let nodeIndex = leafKey.valueOf() + this.numLeaves.valueOf() 117 | let nodeHash 118 | const nodeHashString = await this.db.get(nodeIndex.toString()) 119 | if (!nodeHashString) nodeHash = this.zeroHashes[0] 120 | else nodeHash = BigInt(nodeHashString) 121 | let isLeftNode = nodeIndex % BigInt(2) === BigInt(0) ? true : false 122 | for (let sibNodeHash of proof) { 123 | nodeHash = isLeftNode ? newWrappedPoseidonT3Hash(nodeHash, sibNodeHash) : newWrappedPoseidonT3Hash(sibNodeHash, nodeHash) 124 | 125 | nodeIndex = nodeIndex / BigInt(2) 126 | isLeftNode = nodeIndex % BigInt(2) === BigInt(0) ? true : false 127 | } 128 | if (nodeHash === this.root) return true 129 | else return false 130 | } 131 | 132 | private async populateZeroHashesAndRoot(zeroHash: BigInt): Promise { 133 | const hashes: BigInt[] = [ 134 | zeroHash, 135 | ] 136 | 137 | for (let i = 1; i < this.height; i++) { 138 | hashes[i] = newWrappedPoseidonT3Hash(hashes[i - 1], hashes[i - 1]) 139 | } 140 | 141 | this.zeroHashes = hashes 142 | 143 | this.root = newWrappedPoseidonT3Hash(hashes[this.height - 1], hashes[this.height - 1]) 144 | } 145 | } -------------------------------------------------------------------------------- /crypto/SMT/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SparseMerkleTree' 2 | export * from './utils' -------------------------------------------------------------------------------- /crypto/SMT/utils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | 3 | export const bufToHexString = (buf: Buffer): string => { 4 | return '0x' + buf.toString('hex') 5 | } 6 | 7 | /** 8 | * Removes "0x" from start of a string if it exists. 9 | * @param str String to modify. 10 | * @returns the string without "0x". 11 | */ 12 | export const remove0x = (str: string): string => { 13 | return str.startsWith('0x') ? str.slice(2) : str 14 | } 15 | 16 | /** 17 | * Adds "0x" to the start of a string if necessary. 18 | * @param str String to modify. 19 | * @returns the string with "0x". 20 | */ 21 | export const add0x = (str: string): string => { 22 | return str.startsWith('0x') ? str : '0x' + str 23 | } 24 | 25 | /** 26 | * Computes the keccak256 hash of a value. 27 | * @param value Value to hash 28 | * @returns the hash of the value. 29 | */ 30 | export const keccak256 = (value: string): string => { 31 | const preimage = add0x(value) 32 | return remove0x(ethers.utils.keccak256(preimage)) 33 | } -------------------------------------------------------------------------------- /crypto/crypto.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | 3 | import { SNARK_FIELD_SIZE, SnarkBigInt, genRandomSalt, hash5, hashOne, hashLeftRight, stringifyBigInts, unstringifyBigInts, IncrementalQuinTree } from 'maci-crypto' 4 | 5 | // A nothing-up-my-sleeve zero value 6 | // Should be equal to 16916383162496104613127564537688207714240750091683495371401923915264313510848 7 | const NOTHING_UP_MY_SLEEVE = 8 | BigInt(ethers.utils.solidityKeccak256(['bytes'], [ethers.utils.toUtf8Bytes('Unirep')])) % SNARK_FIELD_SIZE 9 | 10 | const newWrappedPoseidonT3Hash = (...elements: SnarkBigInt[]): SnarkBigInt => { 11 | let result: SnarkBigInt 12 | if ( elements.length == 1) { 13 | result = hashOne(elements[0]) 14 | } else if ( elements.length == 2) { 15 | result = hashLeftRight(elements[0], elements[1]) 16 | } else { 17 | throw new Error(`elements length should not greater than 2, got ${elements.length}`) 18 | } 19 | 20 | return result 21 | } 22 | 23 | const wrappedPoseidonT3Hash = (...elements: SnarkBigInt[]): string => { 24 | let result: SnarkBigInt 25 | if ( elements.length == 1) { 26 | result = hashOne(elements[0]) 27 | } else if ( elements.length == 2) { 28 | result = hashLeftRight(elements[0], elements[1]) 29 | } else { 30 | throw new Error(`elements length should not greater than 2, got ${elements.length}`) 31 | } 32 | 33 | return ethers.utils.hexZeroPad('0x' + result.toString(16), 32) 34 | } 35 | 36 | export { 37 | NOTHING_UP_MY_SLEEVE, 38 | SNARK_FIELD_SIZE, 39 | SnarkBigInt, 40 | genRandomSalt, 41 | hash5, 42 | hashOne, 43 | hashLeftRight, 44 | stringifyBigInts, 45 | unstringifyBigInts, 46 | IncrementalQuinTree, 47 | wrappedPoseidonT3Hash, 48 | newWrappedPoseidonT3Hash, 49 | } -------------------------------------------------------------------------------- /crypto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './semaphore/identity' 2 | export * from './SMT' 3 | export * from './crypto' -------------------------------------------------------------------------------- /crypto/semaphore/identity.ts: -------------------------------------------------------------------------------- 1 | import { SnarkBigInt } from 'maci-crypto' 2 | import * as circomlib from 'circomlib' 3 | import * as bigintConversion from 'bigint-conversion'; 4 | import * as crypto from 'crypto' 5 | 6 | type EddsaPrivateKey = Buffer 7 | type EddsaPublicKey = SnarkBigInt[] 8 | type SnarkWitness = Array 9 | type SnarkPublicSignals = SnarkBigInt[] 10 | 11 | interface EddsaKeyPair { 12 | pubKey: EddsaPublicKey, 13 | privKey: EddsaPrivateKey, 14 | } 15 | 16 | interface Identity { 17 | keypair: EddsaKeyPair, 18 | identityNullifier: SnarkBigInt, 19 | identityTrapdoor: SnarkBigInt, 20 | } 21 | 22 | interface SnarkProof { 23 | pi_a: SnarkBigInt[] 24 | pi_b: SnarkBigInt[][] 25 | pi_c: SnarkBigInt[] 26 | } 27 | 28 | 29 | const genRandomBuffer = (numBytes: number = 32): Buffer => { 30 | return crypto.randomBytes(numBytes) 31 | } 32 | 33 | const genPubKey = (privKey: EddsaPrivateKey): EddsaPublicKey => { 34 | const pubKey = circomlib.eddsa.prv2pub(privKey) 35 | 36 | return pubKey 37 | } 38 | 39 | const genEddsaKeyPair = ( 40 | privKey: Buffer = genRandomBuffer(), 41 | ): EddsaKeyPair => { 42 | 43 | const pubKey = genPubKey(privKey) 44 | return { pubKey, privKey } 45 | } 46 | 47 | const genIdentity = ( 48 | privKey: Buffer = genRandomBuffer(32), 49 | ): Identity => { 50 | 51 | // The identity nullifier and identity trapdoor are separate random 31-byte 52 | // values 53 | return { 54 | keypair: genEddsaKeyPair(privKey), 55 | identityNullifier: bigintConversion.bufToBigint(genRandomBuffer(31)), 56 | identityTrapdoor: bigintConversion.bufToBigint(genRandomBuffer(31)), 57 | } 58 | } 59 | 60 | const serializeIdentity = ( 61 | identity: Identity, 62 | ): string => { 63 | const data = [ 64 | identity.keypair.privKey.toString('hex'), 65 | identity.identityNullifier.toString(16), 66 | identity.identityTrapdoor.toString(16), 67 | ] 68 | return JSON.stringify(data) 69 | } 70 | 71 | const unSerializeIdentity = ( 72 | serialisedIdentity: string, 73 | ): Identity => { 74 | const data = JSON.parse(serialisedIdentity) 75 | return { 76 | keypair: genEddsaKeyPair(Buffer.from(data[0], 'hex')), 77 | identityNullifier: bigintConversion.hexToBigint(data[1]), 78 | identityTrapdoor: bigintConversion.hexToBigint(data[2]), 79 | } 80 | } 81 | 82 | const serialiseIdentity = serializeIdentity 83 | const unSerialiseIdentity = unSerializeIdentity 84 | 85 | const genIdentityCommitment = ( 86 | identity: Identity, 87 | ): SnarkBigInt => { 88 | 89 | return circomlib.poseidon([ 90 | circomlib.babyJub.mulPointEscalar(identity.keypair.pubKey, 8)[0], 91 | identity.identityNullifier, 92 | identity.identityTrapdoor, 93 | ]) 94 | } 95 | 96 | export { 97 | Identity, 98 | EddsaKeyPair, 99 | EddsaPrivateKey, 100 | EddsaPublicKey, 101 | SnarkWitness, 102 | SnarkPublicSignals, 103 | SnarkProof, 104 | SnarkBigInt, 105 | genPubKey, 106 | genIdentity, 107 | genIdentityCommitment, 108 | serialiseIdentity, 109 | unSerialiseIdentity, 110 | } 111 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config" 2 | import "@nomiclabs/hardhat-ethers" 3 | import "@nomiclabs/hardhat-waffle" 4 | 5 | const config: HardhatUserConfig = { 6 | defaultNetwork: "hardhat", 7 | networks: { 8 | hardhat: { 9 | blockGasLimit: 12000000 10 | }, 11 | local: { 12 | url: "http://localhost:8545" 13 | }, 14 | }, 15 | solidity: { 16 | version: "0.8.0", 17 | settings: { 18 | optimizer: { enabled: true, runs: 200 } 19 | } 20 | }, 21 | } 22 | 23 | export default config; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unirep", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "yarn downloadPtau && yarn buildCircuits && yarn buildVerifyEpochKeySnark && yarn buildUserStateTransitionSnark && yarn buildProveReputationSnark && yarn buildProveUserSignUpSnark && ./scripts/buildVerifiers.sh && yarn compile", 8 | "compile": "npx hardhat compile", 9 | "downloadPtau": "./scripts/downloadPtau.sh", 10 | "buildCircuits": "npx ts-node scripts/buildCircuits.ts", 11 | "buildVerifyEpochKeySnark": "./scripts/buildVerifyEpochKeySnark.sh", 12 | "buildUserStateTransitionSnark": "./scripts/buildUserStateTransitionSnark.sh", 13 | "buildProveReputationSnark": "./scripts/buildProveReputationSnark.sh", 14 | "buildProveUserSignUpSnark": "./scripts/buildProveUserSignUpSnark.sh", 15 | "test-cli": "./scripts/testCLI.sh", 16 | "test": "yarn contractUnitTests && yarn circuitUnitTests && yarn UnirepStateTests && yarn UserStateTests", 17 | "contractUnitTests": "NODE_OPTIONS=--max-old-space-size=4096 npx hardhat test --no-compile $(find test/contracts -name '*.ts')", 18 | "circuitUnitTests": "NODE_OPTIONS=--max-old-space-size=4096 npx hardhat test --no-compile $(find test/circuits -name '*.ts')", 19 | "UnirepStateTests": "NODE_OPTIONS=--max-old-space-size=4096 npx hardhat test --no-compile $(find test/UnirepState -name '*.ts')", 20 | "UserStateTests": "NODE_OPTIONS=--max-old-space-size=4096 npx hardhat test --no-compile $(find test/UserState -name '*.ts')" 21 | }, 22 | "repository": "git+https://github.com/appliedzkp/UniRep.git", 23 | "author": "", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/appliedzkp/UniRep/issues" 27 | }, 28 | "homepage": "https://github.com/appliedzkp/UniRep#readme", 29 | "devDependencies": { 30 | "@nomiclabs/hardhat-ethers": "^2.0.2", 31 | "@nomiclabs/hardhat-waffle": "^2.0.1", 32 | "@types/chai": "^4.2.14", 33 | "@types/mocha": "^9.0.0", 34 | "@types/node": "^16.9.1", 35 | "chai": "^4.3.4", 36 | "ethereum-waffle": "^3.4.0", 37 | "hardhat": "^2.8.3", 38 | "keyv": "^4.0.3", 39 | "mocha": "^9.2.0", 40 | "ts-node": "^10.2.1" 41 | }, 42 | "dependencies": { 43 | "@openzeppelin/contracts": "^4.4.2", 44 | "argparse": "^2.0.1", 45 | "base64url": "^3.0.1", 46 | "bigint-conversion": "^2.1.12", 47 | "circom": "^0.5.45", 48 | "circomlib": "^0.5.3", 49 | "ethers": "^5.5.3", 50 | "maci-config": "^0.9.1", 51 | "maci-crypto": "^0.9.1", 52 | "n-readlines": "^1.0.1", 53 | "prompt-async": "^0.9.9", 54 | "shelljs": "^0.8.3", 55 | "snarkjs": "^0.4.7", 56 | "typescript": "^4.4.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/buildCircuits.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | import { circuitGlobalStateTreeDepth, circuitUserStateTreeDepth, circuitEpochTreeDepth, numEpochKeyNoncePerEpoch, numAttestationsPerProof, maxReputationBudget } from '../config/testLocal' 4 | import { verifyEpochKeyCircuitPath, proveReputationCircuitPath, proveUserSignUpCircuitPath, startTransitionCircuitPath, processAttestationsCircuitPath, userStateTransitionCircuitPath, } from '../config/circuitPath' 5 | 6 | const main = async () => { 7 | let testCircuitContent 8 | let dirPath 9 | let circomPath 10 | 11 | // verifyEpochKey circuit 12 | dirPath = path.join(__dirname, '../build') 13 | circomPath = path.join(__dirname, verifyEpochKeyCircuitPath) 14 | 15 | // create .circom file 16 | testCircuitContent = `include "../circuits/verifyEpochKey.circom" \n\ncomponent main = VerifyEpochKey(${circuitGlobalStateTreeDepth}, ${circuitEpochTreeDepth}, ${numEpochKeyNoncePerEpoch})` 17 | 18 | try{ 19 | fs.mkdirSync(dirPath, { recursive: true }) 20 | } catch(e){ 21 | console.log('Cannot create folder ', e); 22 | } 23 | fs.writeFileSync(circomPath, testCircuitContent) 24 | 25 | 26 | // proveRepuation circuit 27 | dirPath = path.join(__dirname, '../build') 28 | circomPath = path.join(__dirname, proveReputationCircuitPath) 29 | 30 | // create .circom file 31 | testCircuitContent = `include "../circuits/proveReputation.circom" \n\ncomponent main = ProveReputation(${circuitGlobalStateTreeDepth}, ${circuitUserStateTreeDepth}, ${circuitEpochTreeDepth}, ${numEpochKeyNoncePerEpoch}, ${maxReputationBudget}, 252)` 32 | 33 | try{ 34 | fs.mkdirSync(dirPath, { recursive: true }) 35 | } catch(e){ 36 | console.log('Cannot create folder ', e); 37 | } 38 | fs.writeFileSync(circomPath, testCircuitContent) 39 | 40 | // proveUserSignUp circuit 41 | dirPath = path.join(__dirname, '../build') 42 | circomPath = path.join(__dirname, proveUserSignUpCircuitPath) 43 | 44 | // create .circom file 45 | testCircuitContent = `include "../circuits/proveUserSignUp.circom" \n\ncomponent main = ProveUserSignUp(${circuitGlobalStateTreeDepth}, ${circuitUserStateTreeDepth}, ${circuitEpochTreeDepth}, ${numEpochKeyNoncePerEpoch})` 46 | 47 | try{ 48 | fs.mkdirSync(dirPath, { recursive: true }) 49 | } catch(e){ 50 | console.log('Cannot create folder ', e); 51 | } 52 | fs.writeFileSync(circomPath, testCircuitContent) 53 | 54 | 55 | // startTransition circuit 56 | dirPath = path.join(__dirname, '../build') 57 | circomPath = path.join(__dirname, startTransitionCircuitPath) 58 | 59 | // create .circom file 60 | testCircuitContent = `include "../circuits/startTransition.circom" \n\ncomponent main = StartTransition(${circuitGlobalStateTreeDepth})` 61 | 62 | try{ 63 | fs.mkdirSync(dirPath, { recursive: true }) 64 | } catch(e){ 65 | console.log('Cannot create folder ', e); 66 | } 67 | fs.writeFileSync(circomPath, testCircuitContent) 68 | 69 | 70 | // processAttestations circuit 71 | dirPath = path.join(__dirname, '../build') 72 | circomPath = path.join(__dirname, processAttestationsCircuitPath) 73 | 74 | // create .circom file 75 | testCircuitContent = `include "../circuits/processAttestations.circom" \n\ncomponent main = ProcessAttestations(${circuitUserStateTreeDepth}, ${numAttestationsPerProof}, ${numEpochKeyNoncePerEpoch})` 76 | 77 | try{ 78 | fs.mkdirSync(dirPath, { recursive: true }) 79 | } catch(e){ 80 | console.log('Cannot create folder ', e); 81 | } 82 | fs.writeFileSync(circomPath, testCircuitContent) 83 | 84 | 85 | // userStateTransition circuit 86 | dirPath = path.join(__dirname, '../build') 87 | circomPath = path.join(__dirname, userStateTransitionCircuitPath) 88 | 89 | // create .circom file 90 | testCircuitContent = `include "../circuits/userStateTransition.circom" \n\ncomponent main = UserStateTransition(${circuitGlobalStateTreeDepth}, ${circuitEpochTreeDepth}, ${circuitUserStateTreeDepth}, ${numEpochKeyNoncePerEpoch})` 91 | 92 | try{ 93 | fs.mkdirSync(dirPath, { recursive: true }) 94 | } catch(e){ 95 | console.log('Cannot create folder ', e); 96 | } 97 | fs.writeFileSync(circomPath, testCircuitContent) 98 | 99 | return 0 100 | } 101 | 102 | (async () => { 103 | let exitCode; 104 | try { 105 | exitCode = await main(); 106 | } catch (err) { 107 | console.error(err) 108 | exitCode = 1 109 | } 110 | process.exit(exitCode) 111 | })(); -------------------------------------------------------------------------------- /scripts/buildProveReputationSnark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")" 6 | cd .. 7 | mkdir -p build 8 | 9 | NODE_OPTIONS=--max-old-space-size=8192 npx ts-node scripts/buildSnarks.ts -i build/proveReputation_main.circom -j build/proveReputationCircuit.r1cs -w build/proveReputation.wasm -y build/proveReputation.sym -pt build/powersOfTau28_hez_final_17.ptau -zk build/proveReputation.zkey -vk build/proveReputation.vkey.json -------------------------------------------------------------------------------- /scripts/buildProveUserSignUpSnark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")" 6 | cd .. 7 | mkdir -p build 8 | 9 | NODE_OPTIONS=--max-old-space-size=8192 npx ts-node scripts/buildSnarks.ts -i build/proveUserSignUp_main.circom -j build/proveUserSignUpCircuit.r1cs -w build/proveUserSignUp.wasm -y build/proveUserSignUp.sym -pt build/powersOfTau28_hez_final_17.ptau -zk build/proveUserSignUp.zkey -vk build/proveUserSignUp.vkey.json -------------------------------------------------------------------------------- /scripts/buildSnarks.ts: -------------------------------------------------------------------------------- 1 | import * as argparse from 'argparse' 2 | import * as fs from 'fs' 3 | import * as path from 'path' 4 | import { stringifyBigInts } from '../crypto'; 5 | const compiler = require('circom').compiler 6 | const snarkjs = require('snarkjs') 7 | const fastFile = require("fastfile") 8 | 9 | const fileExists = (filepath: string): boolean => { 10 | const currentPath = path.join(__dirname, '..') 11 | const inputFilePath = path.join(currentPath, filepath) 12 | const inputFileExists = fs.existsSync(inputFilePath) 13 | 14 | return inputFileExists 15 | } 16 | 17 | const main = async () => { 18 | const parser = new argparse.ArgumentParser({ 19 | description: 'Compile a circom circuit and generate its proving key, verification key, and Solidity verifier' 20 | }) 21 | 22 | parser.add_argument( 23 | '-i', '--input', 24 | { 25 | help: 'The filepath of the circom file', 26 | required: true 27 | } 28 | ) 29 | 30 | parser.add_argument( 31 | '-j', '--r1cs-out', 32 | { 33 | help: 'The filepath to save the compiled circom file', 34 | required: true 35 | } 36 | ) 37 | 38 | parser.add_argument( 39 | '-w', '--wasm-out', 40 | { 41 | help: 'The filepath to save the WASM file', 42 | required: true 43 | } 44 | ) 45 | 46 | parser.add_argument( 47 | '-y', '--sym-out', 48 | { 49 | help: 'The filepath to save the SYM file', 50 | required: true 51 | } 52 | ) 53 | 54 | parser.add_argument( 55 | '-pt', '--ptau', 56 | { 57 | help: 'The filepath of existed ptau', 58 | required: true 59 | } 60 | ) 61 | 62 | parser.add_argument( 63 | '-zk', '--zkey-out', 64 | { 65 | help: 'The filepath to save the zkey', 66 | required: true 67 | } 68 | ) 69 | 70 | parser.add_argument( 71 | '-vk', '--vkey-out', 72 | { 73 | help: 'The filepath to save the vkey', 74 | required: true 75 | } 76 | ) 77 | 78 | parser.add_argument( 79 | '-r', '--override', 80 | { 81 | help: 'Override an existing compiled circuit, proving key, and verifying key if set to true; otherwise (and by default), skip generation if a file already exists', 82 | action: 'store_true', 83 | required: false, 84 | default: false, 85 | } 86 | ) 87 | 88 | const args = parser.parse_args() 89 | const inputFile = args.input 90 | const override = args.override 91 | const circuitOut = args.r1cs_out 92 | const symOut = args.sym_out 93 | const wasmOut = args.wasm_out 94 | const ptau = args.ptau 95 | const zkey = args.zkey_out 96 | const vkOut = args.vkey_out 97 | 98 | // Check if the input circom file exists 99 | const inputFileExists = fileExists(inputFile) 100 | 101 | // Exit if it does not 102 | if (!inputFileExists) { 103 | console.error('File does not exist:', inputFile) 104 | return 1 105 | } 106 | 107 | // Check if the circuitOut file exists and if we should not override files 108 | const circuitOutFileExists = fileExists(circuitOut) 109 | 110 | if (!override && circuitOutFileExists) { 111 | console.log(circuitOut, 'exists. Skipping compilation.') 112 | } else { 113 | console.log(`Compiling ${inputFile}...`) 114 | // Compile the .circom file 115 | const options = { 116 | wasmFile: await fastFile.createOverride(wasmOut), 117 | r1csFileName: circuitOut, 118 | symWriteStream: fs.createWriteStream(symOut), 119 | }; 120 | await compiler(inputFile, options) 121 | console.log('Generated', circuitOut, 'and', wasmOut) 122 | } 123 | 124 | const zkeyOutFileExists = fileExists(zkey) 125 | if (!override && zkeyOutFileExists) { 126 | console.log(zkey, 'exists. Skipping compilation.') 127 | } else { 128 | console.log('Exporting verification key...') 129 | await snarkjs.zKey.newZKey(circuitOut, ptau, zkey) 130 | const vkeyJson = await snarkjs.zKey.exportVerificationKey(zkey) 131 | const S = JSON.stringify(stringifyBigInts(vkeyJson), null, 1); 132 | await fs.promises.writeFile(vkOut, S); 133 | console.log(`Generated ${zkey} and ${vkOut}`) 134 | } 135 | 136 | return 0 137 | } 138 | 139 | (async () => { 140 | let exitCode; 141 | try { 142 | exitCode = await main(); 143 | } catch (err) { 144 | console.error(err) 145 | exitCode = 1 146 | } 147 | process.exit(exitCode) 148 | })(); -------------------------------------------------------------------------------- /scripts/buildUserStateTransitionSnark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")" 6 | cd .. 7 | mkdir -p build 8 | 9 | NODE_OPTIONS=--max-old-space-size=8192 npx ts-node scripts/buildSnarks.ts -i build/startTransition_main.circom -j build/startTransitionCircuit.r1cs -w build/startTransition.wasm -y build/startTransition.sym -pt build/powersOfTau28_hez_final_17.ptau -zk build/startTransition.zkey -vk build/startTransition.vkey.json 10 | 11 | NODE_OPTIONS=--max-old-space-size=8192 npx ts-node scripts/buildSnarks.ts -i build/processAttestations_main.circom -j build/processAttestationsCircuit.r1cs -w build/processAttestations.wasm -y build/processAttestations.sym -pt build/powersOfTau28_hez_final_17.ptau -zk build/processAttestations.zkey -vk build/processAttestations.vkey.json 12 | 13 | NODE_OPTIONS=--max-old-space-size=8192 npx ts-node scripts/buildSnarks.ts -i build/userStateTransition_main.circom -j build/userStateTransitionCircuit.r1cs -w build/userStateTransition.wasm -y build/userStateTransition.sym -pt build/powersOfTau28_hez_final_17.ptau -zk build/userStateTransition.zkey -vk build/userStateTransition.vkey.json -------------------------------------------------------------------------------- /scripts/buildVerifiers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")" 6 | cd .. 7 | mkdir -p build 8 | 9 | npx ts-node scripts/buildVerifiers.ts -s build/EpochKeyValidityVerifier.sol -vs EpochKeyValidityVerifier -cn verifyEpochKey 10 | 11 | echo 'Copying EpochKeyValidityVerifier.sol to contracts/' 12 | cp ./build/EpochKeyValidityVerifier.sol ./contracts/ 13 | 14 | npx ts-node scripts/buildVerifiers.ts -s build/ReputationVerifier.sol -vs ReputationVerifier -cn proveReputation 15 | 16 | echo 'Copying ReputationVerifier.sol to contracts/' 17 | cp ./build/ReputationVerifier.sol ./contracts/ 18 | 19 | npx ts-node scripts/buildVerifiers.ts -s build/UserSignUpVerifier.sol -vs UserSignUpVerifier -cn proveUserSignUp 20 | 21 | echo 'Copying UserSignUpVerifier.sol to contracts/' 22 | cp ./build/UserSignUpVerifier.sol ./contracts/ 23 | 24 | npx ts-node scripts/buildVerifiers.ts -s build/StartTransitionVerifier.sol -vs StartTransitionVerifier -cn startTransition 25 | 26 | echo 'Copying StartTransitionVerifier.sol to contracts/' 27 | cp ./build/StartTransitionVerifier.sol ./contracts/ 28 | 29 | npx ts-node scripts/buildVerifiers.ts -s build/ProcessAttestationsVerifier.sol -vs ProcessAttestationsVerifier -cn processAttestations 30 | 31 | echo 'Copying ProcessAttestationsVerifier.sol to contracts/' 32 | cp ./build/ProcessAttestationsVerifier.sol ./contracts/ 33 | 34 | npx ts-node scripts/buildVerifiers.ts -s build/UserStateTransitionVerifier.sol -vs UserStateTransitionVerifier -cn userStateTransition 35 | 36 | echo 'Copying UserStateTransitionVerifier.sol to contracts/' 37 | cp ./build/UserStateTransitionVerifier.sol ./contracts/ -------------------------------------------------------------------------------- /scripts/buildVerifiers.ts: -------------------------------------------------------------------------------- 1 | import * as argparse from 'argparse' 2 | import * as fs from 'fs' 3 | import { getVKey } from "../circuits/utils" 4 | import { genSnarkVerifierSol } from './genVerifier' 5 | 6 | const main = async () => { 7 | const parser = new argparse.ArgumentParser({ 8 | description: 'Compile a circom circuit and generate its proving key, verification key, and Solidity verifier' 9 | }) 10 | 11 | parser.add_argument( 12 | '-s', '--sol-out', 13 | { 14 | help: 'The filepath to save the Solidity verifier contract', 15 | required: true 16 | } 17 | ) 18 | 19 | parser.add_argument( 20 | '-cn', '--circuit-name', 21 | { 22 | help: 'The name of the vkey', 23 | required: true 24 | } 25 | ) 26 | 27 | parser.add_argument( 28 | '-vs', '--verifier-name', 29 | { 30 | help: 'The desired name of the verifier contract', 31 | required: true 32 | } 33 | ) 34 | 35 | const args = parser.parse_args() 36 | const solOut = args.sol_out 37 | const verifierName = args.verifier_name 38 | const circuitName = args.circuit_name 39 | const vKey = await getVKey(circuitName) 40 | 41 | console.log('Exporting verification contract...') 42 | const verifier = genSnarkVerifierSol( 43 | verifierName, 44 | vKey, 45 | ) 46 | 47 | fs.writeFileSync(solOut, verifier) 48 | return 0 49 | } 50 | 51 | (async () => { 52 | let exitCode; 53 | try { 54 | exitCode = await main(); 55 | } catch (err) { 56 | console.error(err) 57 | exitCode = 1 58 | } 59 | process.exit(exitCode) 60 | })(); -------------------------------------------------------------------------------- /scripts/buildVerifyEpochKeySnark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")" 6 | cd .. 7 | mkdir -p build 8 | 9 | NODE_OPTIONS=--max-old-space-size=8192 npx ts-node scripts/buildSnarks.ts -i build/verifyEpochKey_main.circom -j build/verifyEpochKeyCircuit.r1cs -w build/verifyEpochKey.wasm -y build/verifyEpochKey.sym -pt build/powersOfTau28_hez_final_17.ptau -zk build/verifyEpochKey.zkey -vk build/verifyEpochKey.vkey.json -------------------------------------------------------------------------------- /scripts/downloadPtau.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "$0")" 4 | cd .. 5 | mkdir -p build 6 | 7 | if [[ -f build/powersOfTau28_hez_final_17.ptau ]] 8 | then 9 | exit 10 | fi 11 | 12 | curl -o build/powersOfTau28_hez_final_17.ptau https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_17.ptau -------------------------------------------------------------------------------- /scripts/genVerifier.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | 4 | const genSnarkVerifierSol = ( 5 | contractName: string, 6 | vk: any, 7 | ) => { 8 | const templatePath = path.join( 9 | __dirname, 10 | './verifier_groth16.sol', 11 | ) 12 | 13 | let template = fs.readFileSync(templatePath).toString() 14 | 15 | template = template.replace('<%contract_name%>', contractName) 16 | 17 | const vkalpha1 = `uint256(${vk.vk_alpha_1[0].toString()}),`+ 18 | `uint256(${vk.vk_alpha_1[1].toString()})` 19 | template = template.replace('<%vk_alpha1%>', vkalpha1) 20 | 21 | const vkbeta2 = `[uint256(${vk.vk_beta_2[0][1].toString()}),`+ 22 | `uint256(${vk.vk_beta_2[0][0].toString()})], `+ 23 | `[uint256(${vk.vk_beta_2[1][1].toString()}),` + 24 | `uint256(${vk.vk_beta_2[1][0].toString()})]` 25 | template = template.replace('<%vk_beta2%>', vkbeta2) 26 | 27 | const vkgamma2 = `[uint256(${vk.vk_gamma_2[0][1].toString()}),`+ 28 | `uint256(${vk.vk_gamma_2[0][0].toString()})], `+ 29 | `[uint256(${vk.vk_gamma_2[1][1].toString()}),` + 30 | `uint256(${vk.vk_gamma_2[1][0].toString()})]` 31 | template = template.replace('<%vk_gamma2%>', vkgamma2) 32 | 33 | const vkdelta2 = `[uint256(${vk.vk_delta_2[0][1].toString()}),`+ 34 | `uint256(${vk.vk_delta_2[0][0].toString()})], `+ 35 | `[uint256(${vk.vk_delta_2[1][1].toString()}),` + 36 | `uint256(${vk.vk_delta_2[1][0].toString()})]` 37 | template = template.replace('<%vk_delta2%>', vkdelta2) 38 | 39 | template = template.replace('<%vk_input_length%>', (vk.IC.length-1).toString()) 40 | template = template.replace('<%vk_ic_length%>', vk.IC.length.toString()) 41 | let vi = '' 42 | for (let i=0; i < vk.IC.length; i++) { 43 | if (vi.length !== 0) { 44 | vi = vi + ' ' 45 | } 46 | vi = vi + `vk.IC[${i}] = Pairing.G1Point(uint256(${vk.IC[i][0].toString()}),`+ 47 | `uint256(${vk.IC[i][1].toString()}));\n` 48 | } 49 | template = template.replace('<%vk_ic_pts%>', vi) 50 | 51 | return template 52 | } 53 | 54 | export { genSnarkVerifierSol } -------------------------------------------------------------------------------- /scripts/testCLI.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | cd "$(dirname "$0")" 4 | cd .. 5 | 6 | npx hardhat node & 7 | sleep 6 8 | NODE_OPTIONS=--max-old-space-size=4096 npx hardhat --network local test --no-compile cli/test/testAllCommands.ts -------------------------------------------------------------------------------- /test/circuits/IncrementalMerkleTree.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { expect } from "chai" 3 | import { genRandomSalt, IncrementalQuinTree, hashOne, SnarkBigInt, } from "../../crypto" 4 | import { executeCircuit, getSignalByName, } from "../../circuits/utils" 5 | import { compileAndLoadCircuit } from '../utils' 6 | 7 | const LEVELS = 4 8 | const ZERO_VALUE = 0 9 | const LeafExistsCircuitPath = path.join(__dirname, '../../circuits/test/merkleTreeLeafExists_test.circom') 10 | const InclusionProofCircuitPath = path.join(__dirname, '../../circuits/test/merkleTreeInclusionProof_test.circom') 11 | 12 | describe('Merkle Tree circuits', function () { 13 | this.timeout(30000) 14 | describe('LeafExists', () => { 15 | let circuit 16 | 17 | before(async () => { 18 | circuit = await compileAndLoadCircuit(LeafExistsCircuitPath) 19 | }) 20 | 21 | it('Valid LeafExists inputs should work', async () => { 22 | const tree = new IncrementalQuinTree(LEVELS, ZERO_VALUE, 2) 23 | const leaves: SnarkBigInt[] = [] 24 | 25 | for (let i = 0; i < 2 ** LEVELS; i++) { 26 | const randomVal = genRandomSalt() 27 | tree.insert(hashOne(randomVal)) 28 | leaves.push(hashOne(randomVal)) 29 | } 30 | 31 | const root = tree.root 32 | 33 | for (let i = 0; i < 2 ** LEVELS; i++) { 34 | const proof = tree.genMerklePath(i) 35 | const circuitInputs = { 36 | leaf: leaves[i], 37 | path_elements: proof.pathElements, 38 | path_index: proof.indices, 39 | root, 40 | } 41 | const witness = await executeCircuit(circuit, circuitInputs) 42 | const circuitRoot = getSignalByName(circuit, witness, 'main.root').toString() 43 | expect(circuitRoot).to.be.equal(root.toString()) 44 | } 45 | }) 46 | 47 | it('Invalid LeafExists inputs should not work', async () => { 48 | const tree = new IncrementalQuinTree(LEVELS, ZERO_VALUE, 2) 49 | const leaves: SnarkBigInt[] = [] 50 | 51 | for (let i = 0; i < 2 ** LEVELS; i++) { 52 | const randomVal = genRandomSalt() 53 | tree.insert(randomVal) 54 | leaves.push(hashOne(randomVal)) 55 | } 56 | 57 | const root = tree.root 58 | 59 | for (let i = 0; i < 2 ** LEVELS; i++) { 60 | const proof = tree.genMerklePath(i) 61 | const circuitInputs = { 62 | leaf: leaves[i], 63 | // The following are swapped to delibrately create an error 64 | path_elements: proof.pathElements, 65 | path_index: proof.indices, 66 | root, 67 | } 68 | try { 69 | await executeCircuit(circuit, circuitInputs) 70 | } catch { 71 | expect(true).to.be.true 72 | } 73 | } 74 | }) 75 | }) 76 | 77 | describe('MerkleTreeInclusionProof', () => { 78 | let circuit 79 | 80 | before(async () => { 81 | circuit = await compileAndLoadCircuit(InclusionProofCircuitPath) 82 | }) 83 | 84 | it('Valid update proofs should work', async () => { 85 | const tree = new IncrementalQuinTree(LEVELS, ZERO_VALUE, 2) 86 | 87 | // Populate the tree 88 | for (let i = 0; i < 2 ** LEVELS; i++) { 89 | const randomVal = genRandomSalt() 90 | const leaf = hashOne(randomVal) 91 | tree.insert(leaf) 92 | } 93 | 94 | for (let i = 0; i < 2 ** LEVELS; i++) { 95 | const randomVal = genRandomSalt() 96 | const leaf = hashOne(randomVal) 97 | 98 | tree.update(i, leaf) 99 | 100 | const proof = tree.genMerklePath(i) 101 | 102 | const root = tree.root 103 | 104 | const circuitInputs = { 105 | leaf: leaf.toString(), 106 | path_elements: proof.pathElements, 107 | path_index: proof.indices 108 | } 109 | 110 | const witness = await executeCircuit(circuit, circuitInputs) 111 | const circuitRoot = getSignalByName(circuit, witness, 'main.root').toString() 112 | expect(circuitRoot).to.equal(root.toString()) 113 | } 114 | }) 115 | 116 | it('Invalid update proofs should not work', async () => { 117 | const tree = new IncrementalQuinTree(LEVELS, ZERO_VALUE, 2) 118 | 119 | // Populate the tree 120 | for (let i = 0; i < 2 ** LEVELS; i++) { 121 | const randomVal = genRandomSalt() 122 | const leaf = hashOne(randomVal) 123 | tree.insert(leaf) 124 | } 125 | 126 | for (let i = 0; i < 2 ** LEVELS; i++) { 127 | const randomVal = genRandomSalt() 128 | const leaf = hashOne(randomVal) 129 | 130 | tree.update(i, leaf) 131 | 132 | const proof = tree.genMerklePath(i) 133 | 134 | // Delibrately create an invalid proof 135 | proof.pathElements[0][0] = BigInt(1) 136 | 137 | const isValid = IncrementalQuinTree.verifyMerklePath( 138 | proof, 139 | tree.hashFunc, 140 | ) 141 | expect(isValid).to.be.false 142 | 143 | const circuitInputs = { 144 | leaf: leaf.toString(), 145 | path_elements: proof.pathElements, 146 | path_index: proof.indices, 147 | } 148 | 149 | const witness = await executeCircuit(circuit, circuitInputs) 150 | const circuitRoot = getSignalByName(circuit, witness, 'main.root').toString() 151 | expect(circuitRoot).not.to.equal(tree.root.toString()) 152 | } 153 | }) 154 | }) 155 | }) -------------------------------------------------------------------------------- /test/circuits/hasher.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { expect } from "chai" 3 | import { stringifyBigInts, genRandomSalt, hashLeftRight, hash5, } from "../../crypto" 4 | import { executeCircuit, getSignalByName, } from "../../circuits/utils" 5 | import { compileAndLoadCircuit } from '../utils' 6 | 7 | const hasher5CircuitPath = path.join(__dirname, '../../circuits/test/hasher5_test.circom') 8 | const hashleftrightCircuitPath = path.join(__dirname, '../../circuits/test/hashleftright_test.circom') 9 | 10 | describe('Poseidon hash circuits', function (){ 11 | this.timeout(100000) 12 | let circuit 13 | 14 | describe('Hasher5', () => { 15 | it('correctly hashes 5 random values', async () => { 16 | 17 | circuit = await compileAndLoadCircuit(hasher5CircuitPath) 18 | const preImages: any = [] 19 | for (let i = 0; i < 5; i++) { 20 | preImages.push(genRandomSalt()) 21 | } 22 | 23 | const circuitInputs = stringifyBigInts({ 24 | in: preImages, 25 | }) 26 | 27 | const witness = await executeCircuit(circuit, circuitInputs) 28 | const output = getSignalByName(circuit, witness, 'main.hash') 29 | 30 | const outputJS = hash5(preImages) 31 | 32 | expect(output.toString()).equal(outputJS.toString()) 33 | }) 34 | }) 35 | 36 | describe('HashLeftRight', () => { 37 | 38 | it('correctly hashes two random values', async () => { 39 | const circuit = await compileAndLoadCircuit(hashleftrightCircuitPath) 40 | 41 | const left = genRandomSalt() 42 | const right = genRandomSalt() 43 | 44 | const circuitInputs = stringifyBigInts({ left, right }) 45 | 46 | const witness = await executeCircuit(circuit, circuitInputs) 47 | const output = getSignalByName(circuit, witness, 'main.hash') 48 | 49 | const outputJS = hashLeftRight(left, right) 50 | 51 | expect(output.toString()).equal(outputJS.toString()) 52 | }) 53 | }) 54 | }) -------------------------------------------------------------------------------- /test/circuits/identityCommitment.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { expect } from "chai" 3 | import { genIdentity, genIdentityCommitment } from "../../crypto" 4 | import { executeCircuit, getSignalByName, } from "../../circuits/utils" 5 | import { compileAndLoadCircuit } from '../utils' 6 | 7 | const circuitPath = path.join(__dirname, '../../circuits/test/identityCommitment_test.circom') 8 | 9 | describe('(Semaphore) identity commitment', function () { 10 | this.timeout(200000) 11 | 12 | it('identity computed should match', async () => { 13 | const startCompileTime = Math.floor(new Date().getTime() / 1000) 14 | const circuit = await compileAndLoadCircuit(circuitPath) 15 | const endCompileTime = Math.floor(new Date().getTime() / 1000) 16 | console.log(`Compile time: ${endCompileTime - startCompileTime} seconds`) 17 | 18 | const id = genIdentity() 19 | const pk = id['keypair']['pubKey'] 20 | const nullifier = id['identityNullifier'] 21 | const trapdoor = id['identityTrapdoor'] 22 | const commitment = genIdentityCommitment(id) 23 | 24 | const circuitInputs = { 25 | identity_pk: pk, 26 | identity_nullifier: nullifier, 27 | identity_trapdoor: trapdoor 28 | } 29 | 30 | const witness = await executeCircuit(circuit, circuitInputs) 31 | const output = getSignalByName(circuit, witness, 'main.out') 32 | 33 | 34 | expect(output.toString()).equal(commitment.toString()) 35 | }) 36 | }) -------------------------------------------------------------------------------- /test/circuits/proveUserSignUp.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { expect } from "chai" 3 | import { genRandomSalt, genIdentity, hashOne, } from "../../crypto" 4 | import { Circuit, executeCircuit, } from "../../circuits/utils" 5 | import { compileAndLoadCircuit, genProveSignUpCircuitInput, throwError, genProofAndVerify } from '../utils' 6 | import { circuitEpochTreeDepth } from "../../config/testLocal" 7 | import { proveUserSignUpCircuitPath } from "../../config/circuitPath" 8 | import { genEpochKey, Reputation } from '../../core' 9 | 10 | const circuitPath = path.join(__dirname, '../', proveUserSignUpCircuitPath) 11 | 12 | describe('Prove user has signed up circuit', function () { 13 | this.timeout(300000) 14 | 15 | let circuit 16 | 17 | const epoch = 1 18 | const user = genIdentity() 19 | 20 | let reputationRecords = {} 21 | const MIN_POS_REP = 20 22 | const MAX_NEG_REP = 10 23 | const signUp = 1 24 | const notSignUp = 0 25 | const signedUpAttesterId = 1 26 | const nonSignedUpAttesterId = 2 27 | 28 | before(async () => { 29 | const startCompileTime = Math.floor(new Date().getTime() / 1000) 30 | circuit = await compileAndLoadCircuit(circuitPath) 31 | const endCompileTime = Math.floor(new Date().getTime() / 1000) 32 | console.log(`Compile time: ${endCompileTime - startCompileTime} seconds`) 33 | 34 | // Bootstrap reputation 35 | const graffitiPreImage = genRandomSalt() 36 | reputationRecords[signedUpAttesterId] = new Reputation( 37 | BigInt(Math.floor(Math.random() * 100) + MIN_POS_REP), 38 | BigInt(Math.floor(Math.random() * MAX_NEG_REP)), 39 | hashOne(graffitiPreImage), 40 | BigInt(signUp) 41 | ) 42 | reputationRecords[signedUpAttesterId].addGraffitiPreImage(graffitiPreImage) 43 | 44 | reputationRecords[nonSignedUpAttesterId] = new Reputation( 45 | BigInt(Math.floor(Math.random() * 100) + MIN_POS_REP), 46 | BigInt(Math.floor(Math.random() * MAX_NEG_REP)), 47 | hashOne(graffitiPreImage), 48 | BigInt(notSignUp) 49 | ) 50 | reputationRecords[nonSignedUpAttesterId].addGraffitiPreImage(graffitiPreImage) 51 | }) 52 | 53 | it('successfully prove a user has signed up', async () => { 54 | const attesterId = signedUpAttesterId 55 | const circuitInputs = await genProveSignUpCircuitInput(user, epoch, reputationRecords, attesterId) 56 | 57 | await executeCircuit(circuit, circuitInputs) 58 | 59 | const isValid = await genProofAndVerify(Circuit.proveUserSignUp, circuitInputs) 60 | expect(isValid).to.be.true 61 | }) 62 | 63 | it('user does not sign up should success', async () => { 64 | const attesterId = nonSignedUpAttesterId 65 | const circuitInputs = await genProveSignUpCircuitInput(user, epoch, reputationRecords, attesterId) 66 | 67 | await executeCircuit(circuit, circuitInputs) 68 | 69 | const isValid = await genProofAndVerify(Circuit.proveUserSignUp, circuitInputs) 70 | expect(isValid).to.be.true 71 | }) 72 | 73 | it('prove with wrong attester id should fail', async () => { 74 | const attesterId = nonSignedUpAttesterId 75 | const wrongAttesterId = signedUpAttesterId 76 | const circuitInputs = await genProveSignUpCircuitInput(user, epoch, reputationRecords, attesterId) 77 | circuitInputs.attester_id = wrongAttesterId 78 | 79 | await throwError(circuit, circuitInputs, "Wrong attester id should throw error") 80 | 81 | const isValid = await genProofAndVerify(Circuit.proveUserSignUp, circuitInputs) 82 | expect(isValid).to.be.false 83 | }) 84 | 85 | it('prove with differnt epoch key should fail', async () => { 86 | const attesterId = signedUpAttesterId 87 | const wrongNonce = 1 88 | const wrongEpochKey = genEpochKey(user['identityNullifier'], epoch, wrongNonce, circuitEpochTreeDepth) 89 | const circuitInputs = await genProveSignUpCircuitInput(user, epoch, reputationRecords, attesterId) 90 | circuitInputs.epoch_key = wrongEpochKey 91 | 92 | await throwError(circuit, circuitInputs, "Invalid nonce should throw error") 93 | 94 | const isValid = await genProofAndVerify(Circuit.proveUserSignUp, circuitInputs) 95 | expect(isValid).to.be.false 96 | }) 97 | 98 | it('forge signed up flag should fail', async () => { 99 | const attesterId = nonSignedUpAttesterId 100 | const wrongSignUpInfo = 1 101 | const circuitInputs = await genProveSignUpCircuitInput(user, epoch, reputationRecords, attesterId) 102 | circuitInputs.sign_up = wrongSignUpInfo 103 | 104 | await throwError(circuit, circuitInputs, "Forge sign up flag should throw error") 105 | 106 | const isValid = await genProofAndVerify(Circuit.proveUserSignUp, circuitInputs) 107 | expect(isValid).to.be.false 108 | }) 109 | }) -------------------------------------------------------------------------------- /test/circuits/sparseMerkleTree.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { expect } from "chai" 3 | import { genRandomSalt, hashOne, SparseMerkleTreeImpl, } from "../../crypto" 4 | import { executeCircuit, getSignalByName, } from "../../circuits/utils" 5 | import { genNewSMT, compileAndLoadCircuit } from "../utils" 6 | // circuitEpochTreeDepth too large will greatly slow down the test... 7 | const circuitEpochTreeDepth = 8 8 | const circuitPath = path.join(__dirname, '../../circuits/test/smtLeafExists_test.circom') 9 | const InclusionProofCircuitPath = path.join(__dirname, '../../circuits/test/smtInclusionProof_test.circom') 10 | 11 | describe('Sparse Merkle Tree circuits', function () { 12 | this.timeout(500000) 13 | 14 | describe('LeafExists', () => { 15 | let circuit 16 | 17 | let tree: SparseMerkleTreeImpl, leaves, root, ZERO_VALUE 18 | let leafIndicesToInsert: number[], emptyLeafIndices: number[] 19 | 20 | before(async () => { 21 | circuit = await compileAndLoadCircuit(circuitPath) 22 | 23 | const defaultLeafHash = hashOne(BigInt(0)) 24 | tree = await genNewSMT(circuitEpochTreeDepth, defaultLeafHash) 25 | leaves = {} 26 | ZERO_VALUE = tree.getZeroHash(0) 27 | }) 28 | 29 | it('Valid LeafExists inputs should work', async () => { 30 | const half = 2 ** (circuitEpochTreeDepth - 1) 31 | 32 | // Insert half of the leaves 33 | leafIndicesToInsert = [] 34 | for (let i = 0; i < half; i++) { 35 | let ind = Math.floor(Math.random() * (2 ** circuitEpochTreeDepth)) 36 | while (leafIndicesToInsert.indexOf(ind) >= 0) { 37 | ind = Math.floor(Math.random() * (2 ** circuitEpochTreeDepth)) 38 | } 39 | leafIndicesToInsert.push(ind) 40 | } 41 | for (let ind of leafIndicesToInsert) { 42 | const leaf = genRandomSalt() 43 | await tree.update(BigInt(ind), leaf) 44 | leaves[ind] = leaf 45 | } 46 | 47 | root = tree.getRootHash() 48 | 49 | // Prove first half of existent leaves 50 | for (let ind of leafIndicesToInsert) { 51 | const leaf = leaves[ind] 52 | const pathElements = await tree.getMerkleProof(BigInt(ind)) 53 | const circuitInputs = { 54 | leaf: leaf, 55 | leaf_index: ind, 56 | path_elements: pathElements, 57 | root, 58 | } 59 | const witness = await executeCircuit(circuit, circuitInputs) 60 | } 61 | 62 | // Prove second half of empty leaves 63 | emptyLeafIndices = [] 64 | for (let i = 0; i < 2 ** circuitEpochTreeDepth; i++) { 65 | if (leafIndicesToInsert.indexOf(i) >= 0) continue 66 | else emptyLeafIndices.push(i) 67 | } 68 | for (let ind of emptyLeafIndices) { 69 | const pathElements = await tree.getMerkleProof(BigInt(ind)) 70 | const circuitInputs = { 71 | leaf: ZERO_VALUE, 72 | leaf_index: ind, 73 | path_elements: pathElements, 74 | root, 75 | } 76 | const witness = await executeCircuit(circuit, circuitInputs) 77 | } 78 | }) 79 | 80 | it('Invalid LeafExists inputs should not work', async () => { 81 | for (let ind of leafIndicesToInsert) { 82 | const leaf = leaves[ind] 83 | const pathElements = await tree.getMerkleProof(BigInt(ind)) 84 | 85 | // Check against wrong leaf 86 | const randomVal = genRandomSalt() 87 | const wrongLeaf = genRandomSalt() 88 | let circuitInputs = { 89 | leaf: wrongLeaf, 90 | leaf_index: ind, 91 | path_elements: pathElements, 92 | root, 93 | } 94 | 95 | let error 96 | try { 97 | await executeCircuit(circuit, circuitInputs) 98 | } catch (e) { 99 | error = e 100 | expect(true).to.be.true 101 | } finally { 102 | if (!error) throw Error("Root mismatch results from wrong leaf should throw error") 103 | } 104 | 105 | // Check against wrong leaf index 106 | circuitInputs = { 107 | leaf: leaf, 108 | leaf_index: ind < 15 ? (ind + 1) : (ind - 1), 109 | path_elements: pathElements, 110 | root, 111 | } 112 | 113 | error = undefined 114 | try { 115 | await executeCircuit(circuit, circuitInputs) 116 | } catch (e) { 117 | error = e 118 | expect(true).to.be.true 119 | } finally { 120 | if (!error) throw Error("Root mismatch results from wrong leaf should throw error") 121 | } 122 | 123 | // Check against wrong path elements 124 | const otherIndex = emptyLeafIndices[0] 125 | const wrongPathElements = await tree.getMerkleProof(BigInt(otherIndex)) 126 | circuitInputs = { 127 | leaf: leaf, 128 | leaf_index: ind, 129 | path_elements: wrongPathElements, 130 | root, 131 | } 132 | 133 | error = undefined 134 | try { 135 | await executeCircuit(circuit, circuitInputs) 136 | } catch (e) { 137 | error = e 138 | expect(true).to.be.true 139 | } finally { 140 | if (!error) throw Error("Root mismatch results from wrong path elements should throw error") 141 | } 142 | } 143 | }) 144 | }) 145 | 146 | describe('MerkleTreeInclusionProof', () => { 147 | let circuit 148 | 149 | before(async () => { 150 | circuit = await compileAndLoadCircuit(InclusionProofCircuitPath) 151 | }) 152 | 153 | it('Valid update proofs should work', async () => { 154 | const defaultLeafHash = hashOne(BigInt(0)) 155 | const tree = await genNewSMT(circuitEpochTreeDepth, defaultLeafHash) 156 | const leaves = {} 157 | 158 | // Populate the tree 159 | for (let ind = 0; ind < 2 ** circuitEpochTreeDepth; ind++) { 160 | const leaf = genRandomSalt() 161 | await tree.update(BigInt(ind), leaf) 162 | leaves[ind] = leaf 163 | } 164 | 165 | // Update the tree and verify inclusion proof 166 | for (let ind = 0; ind < 2 ** circuitEpochTreeDepth; ind++) { 167 | const leaf = genRandomSalt() 168 | await tree.update(BigInt(ind), leaf) 169 | leaves[ind] = leaf 170 | 171 | const pathElements = await tree.getMerkleProof(BigInt(ind)) 172 | 173 | const root = tree.getRootHash() 174 | 175 | const circuitInputs = { 176 | leaf: leaf, 177 | leaf_index: ind, 178 | path_elements: pathElements, 179 | } 180 | 181 | const witness = await executeCircuit(circuit, circuitInputs) 182 | 183 | const circuitRoot = getSignalByName(circuit, witness, 'main.root').toString() 184 | expect(circuitRoot).equal(root.toString()) 185 | } 186 | }) 187 | }) 188 | }) -------------------------------------------------------------------------------- /test/circuits/startTransition.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { expect } from "chai" 3 | import { hash5, hashLeftRight, genIdentity, genIdentityCommitment, SparseMerkleTreeImpl, IncrementalQuinTree } from "../../crypto" 4 | import { executeCircuit, getSignalByName, Circuit } from "../../circuits/utils" 5 | import { compileAndLoadCircuit, genStartTransitionCircuitInput, bootstrapRandomUSTree, genProofAndVerify } from '../utils' 6 | import { circuitGlobalStateTreeDepth } from "../../config/testLocal" 7 | import { startTransitionCircuitPath } from '../../config/circuitPath' 8 | 9 | const circuitPath = path.join(__dirname, '../', startTransitionCircuitPath) 10 | 11 | describe('User State Transition circuits', function () { 12 | this.timeout(60000) 13 | 14 | const user = genIdentity() 15 | 16 | describe('Start User State Transition', () => { 17 | 18 | let circuit 19 | const epoch = 1 20 | 21 | let GSTZERO_VALUE = 0, GSTree: IncrementalQuinTree 22 | let userStateTree: SparseMerkleTreeImpl 23 | 24 | let hashedLeaf 25 | const zeroHashChain = BigInt(0) 26 | const nonce = 0 27 | const leafIndex = 0 28 | 29 | before(async () => { 30 | const startCompileTime = Math.floor(new Date().getTime() / 1000) 31 | circuit = await compileAndLoadCircuit(circuitPath) 32 | const endCompileTime = Math.floor(new Date().getTime() / 1000) 33 | console.log(`Compile time: ${endCompileTime - startCompileTime} seconds`) 34 | 35 | // User state tree 36 | const results = await bootstrapRandomUSTree() 37 | userStateTree = results.userStateTree 38 | 39 | // Global state tree 40 | GSTree = new IncrementalQuinTree(circuitGlobalStateTreeDepth, GSTZERO_VALUE, 2) 41 | const commitment = genIdentityCommitment(user) 42 | hashedLeaf = hashLeftRight(commitment, userStateTree.getRootHash()) 43 | GSTree.insert(hashedLeaf) 44 | }) 45 | 46 | describe('Start process user state tree', () => { 47 | it('Valid user state update inputs should work', async () => { 48 | const circuitInputs = genStartTransitionCircuitInput(user, GSTree, leafIndex, userStateTree.getRootHash(), epoch, nonce) 49 | 50 | const witness = await executeCircuit(circuit, circuitInputs) 51 | const outputUserState = getSignalByName(circuit, witness, 'main.blinded_user_state') 52 | const expectedUserState = hash5([user['identityNullifier'], userStateTree.getRootHash(), BigInt(epoch), BigInt(nonce)]) 53 | expect(outputUserState).to.equal(expectedUserState) 54 | 55 | const outputHashChainResult = getSignalByName(circuit, witness, 'main.blinded_hash_chain_result') 56 | const expectedHashChainResult = hash5([user['identityNullifier'], zeroHashChain, BigInt(epoch), BigInt(nonce)]) 57 | expect(outputHashChainResult).to.equal(expectedHashChainResult) 58 | 59 | const isValid = await genProofAndVerify(Circuit.startTransition, circuitInputs) 60 | expect(isValid).to.be.true 61 | 62 | }) 63 | 64 | it('User can start with different epoch key nonce', async () => { 65 | const newNonce = 1 66 | const circuitInputs = genStartTransitionCircuitInput(user, GSTree, leafIndex, userStateTree.getRootHash(), epoch, newNonce) 67 | 68 | const witness = await executeCircuit(circuit, circuitInputs) 69 | const outputUserState = getSignalByName(circuit, witness, 'main.blinded_user_state') 70 | const expectedUserState = hash5([user['identityNullifier'], userStateTree.getRootHash(), BigInt(epoch), BigInt(newNonce)]) 71 | expect(outputUserState).to.equal(expectedUserState) 72 | 73 | const outputHashChainResult = getSignalByName(circuit, witness, 'main.blinded_hash_chain_result') 74 | const expectedHashChainResult = hash5([user['identityNullifier'], zeroHashChain, BigInt(epoch), BigInt(newNonce)]) 75 | expect(outputHashChainResult).to.equal(expectedHashChainResult) 76 | }) 77 | }) 78 | }) 79 | }) -------------------------------------------------------------------------------- /test/circuits/userStateTransition.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { expect } from "chai" 3 | import { genRandomSalt, hashLeftRight, genIdentity, genIdentityCommitment, SparseMerkleTreeImpl, SnarkBigInt } from "../../crypto" 4 | import { executeCircuit, getSignalByName, Circuit } from "../../circuits/utils" 5 | import { genNewEpochTree, compileAndLoadCircuit, genUserStateTransitionCircuitInput, genProofAndVerify } from '../utils' 6 | import { numEpochKeyNoncePerEpoch } from "../../config/testLocal" 7 | import { userStateTransitionCircuitPath } from "../../config/circuitPath" 8 | import { genEpochKey } from '../../core' 9 | 10 | const epkExistsCircuitPath = path.join(__dirname, '../../circuits/test/epochKeyExists_test.circom') 11 | const USTCircuitPath = path.join(__dirname, '../', userStateTransitionCircuitPath) 12 | 13 | describe('User State Transition circuits', function () { 14 | this.timeout(600000) 15 | 16 | const epoch = 1 17 | const user = genIdentity() 18 | 19 | describe('Epoch key exists', () => { 20 | 21 | let circuit 22 | 23 | const nonce = numEpochKeyNoncePerEpoch - 1 24 | const testEpochTreeDepth = 32 25 | const epochKey: SnarkBigInt = genEpochKey(user['identityNullifier'], epoch, nonce, testEpochTreeDepth) 26 | 27 | let epochTree: SparseMerkleTreeImpl, epochTreeRoot, epochTreePathElements 28 | 29 | let hashChainResult: SnarkBigInt 30 | 31 | before(async () => { 32 | const startCompileTime = Math.floor(new Date().getTime() / 1000) 33 | circuit = await compileAndLoadCircuit(epkExistsCircuitPath) 34 | const endCompileTime = Math.floor(new Date().getTime() / 1000) 35 | console.log(`Compile time: ${endCompileTime - startCompileTime} seconds`) 36 | 37 | // Epoch tree 38 | epochTree = await genNewEpochTree(testEpochTreeDepth) 39 | 40 | hashChainResult = genRandomSalt() 41 | 42 | await epochTree.update(epochKey, hashChainResult) 43 | 44 | epochTreePathElements = await epochTree.getMerkleProof(epochKey) 45 | epochTreeRoot = epochTree.getRootHash() 46 | }) 47 | 48 | it('Existed epoch key should pass check', async () => { 49 | const circuitInputs = { 50 | identity_nullifier: user['identityNullifier'], 51 | epoch: epoch, 52 | nonce: nonce, 53 | hash_chain_result: hashChainResult, 54 | epoch_tree_root: epochTreeRoot, 55 | path_elements: epochTreePathElements 56 | } 57 | 58 | 59 | await executeCircuit(circuit, circuitInputs) 60 | }) 61 | }) 62 | 63 | describe('User State Transition', () => { 64 | 65 | let circuit 66 | let circuitInputs 67 | 68 | before(async () => { 69 | const startCompileTime = Math.floor(new Date().getTime() / 1000) 70 | circuit = await compileAndLoadCircuit(USTCircuitPath) 71 | const endCompileTime = Math.floor(new Date().getTime() / 1000) 72 | console.log(`Compile time: ${endCompileTime - startCompileTime} seconds`) 73 | 74 | circuitInputs = await genUserStateTransitionCircuitInput(user, epoch) 75 | }) 76 | 77 | describe('Process user state transition proof', () => { 78 | it('Valid user state update inputs should work', async () => { 79 | const witness = await executeCircuit(circuit, circuitInputs) 80 | 81 | const commitment = genIdentityCommitment(user) 82 | const newGSTLeaf = hashLeftRight(commitment, circuitInputs.intermediate_user_state_tree_roots[1]) 83 | const _newGSTLeaf = getSignalByName(circuit, witness, 'main.new_GST_leaf') 84 | expect(_newGSTLeaf, 'new GST leaf mismatch').to.equal(newGSTLeaf) 85 | 86 | const isValid = await genProofAndVerify(Circuit.userStateTransition, circuitInputs) 87 | expect(isValid).to.be.true 88 | }) 89 | }) 90 | }) 91 | }) -------------------------------------------------------------------------------- /test/circuits/verifyEpochKey.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { expect } from "chai" 3 | import { genRandomSalt, hashLeftRight, genIdentity, genIdentityCommitment, IncrementalQuinTree, } from "../../crypto" 4 | import { Circuit, executeCircuit, formatProofForSnarkjsVerification, formatProofForVerifierContract, genProofAndPublicSignals, verifyProof } from "../../circuits/utils" 5 | import { numEpochKeyNoncePerEpoch, circuitEpochTreeDepth, circuitGlobalStateTreeDepth } from "../../config/testLocal" 6 | import { verifyEpochKeyCircuitPath } from "../../config/circuitPath" 7 | import { compileAndLoadCircuit, genEpochKeyCircuitInput, throwError } from '../utils' 8 | 9 | const circuitPath = path.join(__dirname, '../', verifyEpochKeyCircuitPath) 10 | 11 | describe('Verify Epoch Key circuits', function () { 12 | this.timeout(300000) 13 | 14 | let circuit 15 | let ZERO_VALUE = 0 16 | 17 | const maxEPK = BigInt(2 ** circuitEpochTreeDepth) 18 | 19 | let id, commitment, stateRoot 20 | let tree, leafIndex 21 | let nonce, currentEpoch 22 | let circuitInputs 23 | 24 | before(async () => { 25 | const startCompileTime = Math.floor(new Date().getTime() / 1000) 26 | circuit = await compileAndLoadCircuit(circuitPath) 27 | const endCompileTime = Math.floor(new Date().getTime() / 1000) 28 | console.log(`Compile time: ${endCompileTime - startCompileTime} seconds`) 29 | 30 | tree = new IncrementalQuinTree(circuitGlobalStateTreeDepth, ZERO_VALUE, 2) 31 | id = genIdentity() 32 | commitment = genIdentityCommitment(id) 33 | stateRoot = genRandomSalt() 34 | 35 | const hashedStateLeaf = hashLeftRight(commitment.toString(), stateRoot.toString()) 36 | tree.insert(BigInt(hashedStateLeaf.toString())) 37 | 38 | leafIndex = 0 39 | nonce = 0 40 | currentEpoch = 1 41 | }) 42 | 43 | it('Valid epoch key should pass check', async () => { 44 | // Check if every valid nonce works 45 | for (let i = 0; i < numEpochKeyNoncePerEpoch; i++) { 46 | const n = i 47 | circuitInputs = genEpochKeyCircuitInput(id, tree, leafIndex, stateRoot, currentEpoch, n) 48 | 49 | await executeCircuit(circuit, circuitInputs) 50 | const startTime = new Date().getTime() 51 | const { proof, publicSignals } = await genProofAndPublicSignals(Circuit.verifyEpochKey, circuitInputs) 52 | const endTime = new Date().getTime() 53 | console.log(`Gen Proof time: ${endTime - startTime} ms (${Math.floor((endTime - startTime) / 1000)} s)`) 54 | let isValid = await verifyProof(Circuit.verifyEpochKey, proof, publicSignals) 55 | expect(isValid).to.be.true 56 | 57 | const formatProof = formatProofForVerifierContract(proof) 58 | const snarkjsProof = formatProofForSnarkjsVerification(formatProof) 59 | isValid = await verifyProof(Circuit.verifyEpochKey, snarkjsProof, publicSignals) 60 | expect(isValid).to.be.true 61 | } 62 | }) 63 | 64 | it('Invalid epoch key should not pass check', async () => { 65 | // Validate against invalid epoch key 66 | const invalidEpochKey1 = maxEPK 67 | const invalidCircuitInputs = genEpochKeyCircuitInput(id, tree, leafIndex, stateRoot, currentEpoch, nonce) 68 | invalidCircuitInputs.epoch_key = invalidEpochKey1 69 | 70 | await throwError(circuit, invalidCircuitInputs, "Epoch key too large should throw error") 71 | }) 72 | 73 | it('Wrong Id should not pass check', async () => { 74 | const fakeId = genIdentity() 75 | const invalidCircuitInputs = circuitInputs = genEpochKeyCircuitInput(fakeId, tree, leafIndex, stateRoot, currentEpoch, nonce) 76 | 77 | await throwError(circuit, invalidCircuitInputs, "Wrong Id should throw error") 78 | }) 79 | 80 | it('Mismatched GST tree root should not pass check', async () => { 81 | const otherTreeRoot = genRandomSalt() 82 | const invalidCircuitInputs = circuitInputs = genEpochKeyCircuitInput(id, tree, leafIndex, otherTreeRoot, currentEpoch, nonce) 83 | 84 | await throwError(circuit, invalidCircuitInputs, "Wrong GST Root should throw error") 85 | }) 86 | 87 | it('Invalid nonce should not pass check', async () => { 88 | const invalidNonce = numEpochKeyNoncePerEpoch 89 | const invalidCircuitInputs = circuitInputs = genEpochKeyCircuitInput(id, tree, leafIndex, stateRoot, currentEpoch, invalidNonce) 90 | 91 | await throwError(circuit, invalidCircuitInputs, "Invalid nonce should throw error") 92 | }) 93 | 94 | it('Invalid epoch should not pass check', async () => { 95 | let invalidEpoch 96 | invalidEpoch = currentEpoch + 1 97 | const invalidCircuitInputs = genEpochKeyCircuitInput(id, tree, leafIndex, stateRoot, currentEpoch, nonce) 98 | invalidCircuitInputs.epoch = invalidEpoch 99 | 100 | await throwError(circuit, invalidCircuitInputs, "Wrong epoch should throw error") 101 | }) 102 | }) -------------------------------------------------------------------------------- /test/circuits/verifyHashChain.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { genRandomSalt, hashLeftRight, SnarkBigInt, } from "../../crypto" 3 | import { executeCircuit, } from "../../circuits/utils" 4 | import { compileAndLoadCircuit, throwError } from '../utils' 5 | 6 | const sealedHashChainCircuitPath = path.join(__dirname, '../', '../circuits/test/verifyHashChain_test.circom') 7 | 8 | describe('Hash chain circuit', function () { 9 | this.timeout(30000) 10 | let circuit 11 | 12 | const NUM_ELEMENT = 10 13 | let elements: SnarkBigInt[] = [] 14 | let cur: BigInt = BigInt(0), result, selectors: number[] = [] 15 | 16 | before(async () => { 17 | circuit = await compileAndLoadCircuit(sealedHashChainCircuitPath) 18 | 19 | for (let i = 0; i < NUM_ELEMENT; i++) { 20 | const element = genRandomSalt() 21 | const sel = Math.floor(Math.random() * 2) 22 | selectors.push(sel) 23 | elements.push(element) 24 | if ( sel == 1) { 25 | cur = hashLeftRight(element, cur) 26 | } 27 | } 28 | result = hashLeftRight(BigInt(1), cur) 29 | }) 30 | 31 | it('correctly verify hash chain', async () => { 32 | const circuitInputs = { 33 | hashes: elements, 34 | selectors: selectors, 35 | result: result 36 | } 37 | 38 | await executeCircuit(circuit, circuitInputs) 39 | }) 40 | 41 | it('verify incorrect elements should fail', async () => { 42 | elements.reverse() 43 | const circuitInputs = { 44 | hashes: elements, 45 | selectors: selectors, 46 | result: result 47 | } 48 | 49 | await throwError(circuit, circuitInputs, "Wrong hashes should throw error") 50 | elements.reverse() 51 | }) 52 | 53 | it('verify with incorrect selectors should fail', async () => { 54 | const wrongSelectors = selectors.slice() 55 | // Flip one of the selector 56 | const indexWrongSelector = Math.floor(Math.random() * NUM_ELEMENT) 57 | wrongSelectors[indexWrongSelector] = wrongSelectors[indexWrongSelector] ? 0 : 1 58 | const circuitInputs = { 59 | hashes: elements, 60 | selectors: wrongSelectors, 61 | result: result 62 | } 63 | 64 | await throwError(circuit, circuitInputs, "Wrong selectors should throw error") 65 | }) 66 | 67 | it('verify incorrect number of elements should fail', async () => { 68 | const circuitInputs = { 69 | hashes: elements.slice(1), 70 | selectors: selectors, 71 | result: result 72 | } 73 | 74 | await throwError(circuit, circuitInputs, "Wrong number of hashes should throw error") 75 | }) 76 | 77 | it('verify incorrect result should fail', async () => { 78 | const incorrectResult = genRandomSalt() 79 | const circuitInputs = { 80 | hashes: elements, 81 | selectors: selectors, 82 | result: incorrectResult 83 | } 84 | 85 | await throwError(circuit, circuitInputs, "Wrong hash chain result should throw error") 86 | }) 87 | }) -------------------------------------------------------------------------------- /test/contracts/EpochKeyVerifier.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { ethers as hardhatEthers } from 'hardhat' 3 | import { ethers } from 'ethers' 4 | import { expect } from "chai" 5 | import { Circuit } from "../../circuits/utils" 6 | import { genRandomSalt, hashLeftRight, genIdentity, genIdentityCommitment, IncrementalQuinTree, } from "../../crypto" 7 | import { numEpochKeyNoncePerEpoch, circuitEpochTreeDepth, circuitGlobalStateTreeDepth } from "../../config/testLocal" 8 | import { genEpochKeyCircuitInput, genInputForContract, getTreeDepthsForTesting } from '../utils' 9 | import { EpochKeyProof, deployUnirep } from '../../core' 10 | 11 | 12 | describe('Verify Epoch Key verifier', function () { 13 | this.timeout(30000) 14 | 15 | let ZERO_VALUE = 0 16 | 17 | const maxEPK = BigInt(2 ** circuitEpochTreeDepth) 18 | 19 | let unirepContract 20 | let accounts: ethers.Signer[] 21 | let id, commitment, stateRoot 22 | let tree 23 | let nonce, currentEpoch 24 | let leafIndex = 0 25 | let input: EpochKeyProof 26 | 27 | before(async () => { 28 | accounts = await hardhatEthers.getSigners() 29 | 30 | const _treeDepths = getTreeDepthsForTesting() 31 | unirepContract = await deployUnirep(accounts[0], _treeDepths) 32 | tree = new IncrementalQuinTree(circuitGlobalStateTreeDepth, ZERO_VALUE, 2) 33 | id = genIdentity() 34 | commitment = genIdentityCommitment(id) 35 | stateRoot = genRandomSalt() 36 | 37 | const hashedStateLeaf = hashLeftRight(commitment.toString(), stateRoot.toString()) 38 | tree.insert(BigInt(hashedStateLeaf.toString())) 39 | nonce = 0 40 | currentEpoch = 1 41 | }) 42 | 43 | it('Valid epoch key should pass check', async () => { 44 | // Check if every valid nonce works 45 | for (let i = 0; i < numEpochKeyNoncePerEpoch; i++) { 46 | const n = i 47 | const circuitInputs = genEpochKeyCircuitInput(id, tree, leafIndex, stateRoot, currentEpoch, n) 48 | 49 | input = await genInputForContract(Circuit.verifyEpochKey, circuitInputs) 50 | const isValid = await input.verify() 51 | expect(isValid, 'Verify epoch key proof off-chain failed').to.be.true 52 | let tx = await unirepContract.submitEpochKeyProof(input) 53 | const receipt = await tx.wait() 54 | expect(receipt.status).equal(1) 55 | const isProofValid = await unirepContract.verifyEpochKeyValidity(input) 56 | expect(isProofValid, 'Verify epk proof on-chain failed').to.be.true 57 | 58 | const pfIdx = await unirepContract.getProofIndex(input.hash()) 59 | expect(Number(pfIdx)).not.eq(0) 60 | } 61 | }) 62 | 63 | it('Invalid epoch key should not pass check', async () => { 64 | // Validate against invalid epoch key 65 | const invalidEpochKey1 = maxEPK 66 | const invalidCircuitInputs = genEpochKeyCircuitInput(id, tree, leafIndex, stateRoot, currentEpoch, nonce) 67 | invalidCircuitInputs.epoch_key = invalidEpochKey1 68 | 69 | input = await genInputForContract(Circuit.verifyEpochKey, invalidCircuitInputs) 70 | const isProofValid = await unirepContract.verifyEpochKeyValidity(input) 71 | expect(isProofValid, 'Verify epk proof on-chain should fail').to.be.false 72 | }) 73 | 74 | it('Wrong Id should not pass check', async () => { 75 | const fakeId = genIdentity() 76 | const invalidCircuitInputs = genEpochKeyCircuitInput(fakeId, tree, leafIndex, stateRoot, currentEpoch, nonce) 77 | 78 | input = await genInputForContract(Circuit.verifyEpochKey, invalidCircuitInputs) 79 | const isProofValid = await unirepContract.verifyEpochKeyValidity(input) 80 | expect(isProofValid, 'Verify epk proof on-chain should fail').to.be.false 81 | }) 82 | 83 | it('Mismatched GST tree root should not pass check', async () => { 84 | const otherTreeRoot = genRandomSalt() 85 | const invalidCircuitInputs = genEpochKeyCircuitInput(id, tree, leafIndex, otherTreeRoot, currentEpoch, nonce) 86 | 87 | input = await genInputForContract(Circuit.verifyEpochKey, invalidCircuitInputs) 88 | const isProofValid = await unirepContract.verifyEpochKeyValidity(input) 89 | expect(isProofValid, 'Verify epk proof on-chain should fail').to.be.false 90 | }) 91 | 92 | it('Invalid epoch should not pass check', async () => { 93 | const invalidNonce = numEpochKeyNoncePerEpoch 94 | const invalidCircuitInputs = genEpochKeyCircuitInput(id, tree, leafIndex, stateRoot, currentEpoch, invalidNonce) 95 | 96 | input = await genInputForContract(Circuit.verifyEpochKey, invalidCircuitInputs) 97 | const isProofValid = await unirepContract.verifyEpochKeyValidity(input) 98 | expect(isProofValid, 'Verify epk proof on-chain should fail').to.be.false 99 | }) 100 | }) -------------------------------------------------------------------------------- /test/contracts/ProcessAttestationsVerifier.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { ethers as hardhatEthers } from 'hardhat' 3 | import { ethers } from 'ethers' 4 | import { expect } from "chai" 5 | import { genIdentity } from "../../crypto" 6 | import { Circuit } from "../../circuits/utils" 7 | import { genInputForContract,genProcessAttestationsCircuitInput, getTreeDepthsForTesting } from '../utils' 8 | import { numAttestationsPerProof } from "../../config/testLocal" 9 | import { computeProcessAttestationsProofHash, deployUnirep } from '../../core' 10 | 11 | describe('Process attestation circuit', function () { 12 | this.timeout(300000) 13 | 14 | let accounts 15 | let unirepContract 16 | 17 | const epoch = BigInt(1) 18 | const nonce = BigInt(0) 19 | const user = genIdentity() 20 | 21 | before(async () => { 22 | accounts = await hardhatEthers.getSigners() 23 | 24 | const _treeDepths = getTreeDepthsForTesting() 25 | unirepContract = await deployUnirep(accounts[0], _treeDepths) 26 | }) 27 | 28 | it('successfully process attestations', async () => { 29 | const { circuitInputs } = await genProcessAttestationsCircuitInput(user, epoch, nonce, nonce) 30 | 31 | const { outputBlindedUserState, outputBlindedHashChain, inputBlindedUserState, proof } = await genInputForContract(Circuit.processAttestations, circuitInputs) 32 | const isProofValid = await unirepContract.verifyProcessAttestationProof( outputBlindedUserState, outputBlindedHashChain, inputBlindedUserState, proof) 33 | expect(isProofValid).to.be.true 34 | 35 | const tx = await unirepContract.processAttestations( 36 | outputBlindedUserState, 37 | outputBlindedHashChain, 38 | inputBlindedUserState, 39 | proof 40 | ) 41 | const receipt = await tx.wait() 42 | expect(receipt.status).equal(1) 43 | 44 | const pfIdx = await unirepContract.getProofIndex( 45 | computeProcessAttestationsProofHash( 46 | outputBlindedUserState, 47 | outputBlindedHashChain, 48 | inputBlindedUserState, 49 | proof 50 | ) 51 | ) 52 | expect(Number(pfIdx)).not.eq(0) 53 | }) 54 | 55 | it('successfully process zero attestations', async () => { 56 | let zeroSelectors: number[] = [] 57 | for (let i = 0; i < numAttestationsPerProof; i++) { 58 | zeroSelectors.push(0) 59 | } 60 | const { circuitInputs } = await genProcessAttestationsCircuitInput(user, epoch, nonce, nonce, zeroSelectors) 61 | const { outputBlindedUserState, outputBlindedHashChain, inputBlindedUserState, proof } = await genInputForContract(Circuit.processAttestations, circuitInputs) 62 | const isProofValid = await unirepContract.verifyProcessAttestationProof( outputBlindedUserState, outputBlindedHashChain, inputBlindedUserState, proof) 63 | expect(isProofValid).to.be.true 64 | 65 | const tx = await unirepContract.processAttestations( 66 | outputBlindedUserState, 67 | outputBlindedHashChain, 68 | inputBlindedUserState, 69 | proof 70 | ) 71 | const receipt = await tx.wait() 72 | expect(receipt.status).equal(1) 73 | 74 | const pfIdx = await unirepContract.getProofIndex( 75 | computeProcessAttestationsProofHash( 76 | outputBlindedUserState, 77 | outputBlindedHashChain, 78 | inputBlindedUserState, 79 | proof 80 | ) 81 | ) 82 | expect(Number(pfIdx)).not.eq(0) 83 | }) 84 | }) -------------------------------------------------------------------------------- /test/contracts/StartTransitionVerifier.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { ethers as hardhatEthers } from 'hardhat' 3 | import { ethers } from 'ethers' 4 | import { expect } from "chai" 5 | import { hashLeftRight, genIdentity, genIdentityCommitment, SparseMerkleTreeImpl, IncrementalQuinTree } from "../../crypto" 6 | import { Circuit } from "../../circuits/utils" 7 | import { genStartTransitionCircuitInput, getTreeDepthsForTesting, bootstrapRandomUSTree, genInputForContract } from '../utils' 8 | import { circuitGlobalStateTreeDepth } from "../../config/testLocal" 9 | import { computeStartTransitionProofHash, deployUnirep } from '../../core' 10 | 11 | 12 | describe('User State Transition circuits', function () { 13 | this.timeout(60000) 14 | 15 | const user = genIdentity() 16 | 17 | describe('Start User State Transition', () => { 18 | let accounts 19 | let unirepContract 20 | const epoch = 1 21 | 22 | let GSTZERO_VALUE = 0, GSTree: IncrementalQuinTree 23 | let userStateTree: SparseMerkleTreeImpl 24 | 25 | let hashedLeaf 26 | const nonce = 0 27 | const leafIndex = 0 28 | 29 | before(async () => { 30 | accounts = await hardhatEthers.getSigners() 31 | 32 | const _treeDepths = getTreeDepthsForTesting() 33 | unirepContract = await deployUnirep(accounts[0], _treeDepths) 34 | 35 | // User state tree 36 | const results = await bootstrapRandomUSTree() 37 | userStateTree = results.userStateTree 38 | 39 | // Global state tree 40 | GSTree = new IncrementalQuinTree(circuitGlobalStateTreeDepth, GSTZERO_VALUE, 2) 41 | const commitment = genIdentityCommitment(user) 42 | hashedLeaf = hashLeftRight(commitment, userStateTree.getRootHash()) 43 | GSTree.insert(hashedLeaf) 44 | }) 45 | 46 | describe('Start process user state tree', () => { 47 | it('Valid user state update inputs should work', async () => { 48 | const circuitInputs = genStartTransitionCircuitInput(user, GSTree, leafIndex, userStateTree.getRootHash(), epoch, nonce) 49 | 50 | const { blindedUserState, blindedHashChain, GSTRoot, proof } = await genInputForContract(Circuit.startTransition, circuitInputs) 51 | const isProofValid = await unirepContract.verifyStartTransitionProof( blindedUserState, blindedHashChain, GSTRoot, proof) 52 | expect(isProofValid).to.be.true 53 | 54 | const tx = await unirepContract.startUserStateTransition(blindedUserState, blindedHashChain, GSTRoot, proof) 55 | const receipt = await tx.wait() 56 | expect(receipt.status).equal(1) 57 | 58 | const pfIdx = await unirepContract.getProofIndex( 59 | computeStartTransitionProofHash( 60 | blindedUserState, 61 | blindedHashChain, 62 | GSTRoot, 63 | proof 64 | ) 65 | ) 66 | expect(Number(pfIdx)).not.eq(0) 67 | }) 68 | 69 | it('User can start with different epoch key nonce', async () => { 70 | const newNonce = 1 71 | const circuitInputs = genStartTransitionCircuitInput(user, GSTree, leafIndex, userStateTree.getRootHash(), epoch, newNonce) 72 | 73 | const { blindedUserState, blindedHashChain, GSTRoot, proof } = await genInputForContract(Circuit.startTransition, circuitInputs) 74 | const isProofValid = await unirepContract.verifyStartTransitionProof( blindedUserState, blindedHashChain, GSTRoot, proof) 75 | expect(isProofValid).to.be.true 76 | 77 | const tx = await unirepContract.startUserStateTransition(blindedUserState, blindedHashChain, GSTRoot, proof) 78 | const receipt = await tx.wait() 79 | expect(receipt.status).equal(1) 80 | 81 | const pfIdx = await unirepContract.getProofIndex( 82 | computeStartTransitionProofHash( 83 | blindedUserState, 84 | blindedHashChain, 85 | GSTRoot, 86 | proof 87 | ) 88 | ) 89 | expect(Number(pfIdx)).not.eq(0) 90 | }) 91 | }) 92 | }) 93 | }) -------------------------------------------------------------------------------- /test/contracts/UserSignUpVerifier.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { ethers as hardhatEthers } from 'hardhat' 3 | import { ethers } from 'ethers' 4 | import { expect } from "chai" 5 | import { Circuit } from "../../circuits/utils" 6 | import { genRandomSalt, genIdentity, hashOne, } from "../../crypto" 7 | import { circuitEpochTreeDepth, } from "../../config/testLocal" 8 | import { genInputForContract, genProveSignUpCircuitInput, getTreeDepthsForTesting } from '../utils' 9 | import { deployUnirep, SignUpProof, genEpochKey, Reputation } from '../../core' 10 | 11 | 12 | describe('Verify user sign up verifier', function () { 13 | this.timeout(30000) 14 | let unirepContract 15 | let accounts: ethers.Signer[] 16 | const epoch = 1 17 | const nonce = 0 18 | const user = genIdentity() 19 | 20 | let reputationRecords = {} 21 | const MIN_POS_REP = 20 22 | const MAX_NEG_REP = 10 23 | const signUp = 1 24 | const notSignUp = 0 25 | const signedUpAttesterId = 1 26 | const nonSignedUpAttesterId = 2 27 | 28 | before(async () => { 29 | accounts = await hardhatEthers.getSigners() 30 | 31 | const _treeDepths = getTreeDepthsForTesting() 32 | unirepContract = await deployUnirep(accounts[0], _treeDepths) 33 | // Bootstrap reputation 34 | const graffitiPreImage = genRandomSalt() 35 | reputationRecords[signedUpAttesterId] = new Reputation( 36 | BigInt(Math.floor(Math.random() * 100) + MIN_POS_REP), 37 | BigInt(Math.floor(Math.random() * MAX_NEG_REP)), 38 | hashOne(graffitiPreImage), 39 | BigInt(signUp) 40 | ) 41 | reputationRecords[signedUpAttesterId].addGraffitiPreImage(graffitiPreImage) 42 | 43 | reputationRecords[nonSignedUpAttesterId] = new Reputation( 44 | BigInt(Math.floor(Math.random() * 100) + MIN_POS_REP), 45 | BigInt(Math.floor(Math.random() * MAX_NEG_REP)), 46 | hashOne(graffitiPreImage), 47 | BigInt(notSignUp) 48 | ) 49 | reputationRecords[nonSignedUpAttesterId].addGraffitiPreImage(graffitiPreImage) 50 | }) 51 | 52 | it('successfully prove a user has signed up', async () => { 53 | const attesterId = signedUpAttesterId 54 | const circuitInputs = await genProveSignUpCircuitInput(user, epoch, reputationRecords, attesterId) 55 | const input: SignUpProof = await genInputForContract(Circuit.proveUserSignUp, circuitInputs) 56 | 57 | const isValid = await input.verify() 58 | expect(isValid, 'Verify user sign up proof off-chain failed').to.be.true 59 | const isProofValid = await unirepContract.verifyUserSignUp(input) 60 | expect(isProofValid, 'Verify reputation proof on-chain failed').to.be.true 61 | }) 62 | 63 | it('wrong attesterId should fail', async () => { 64 | const attesterId = signedUpAttesterId 65 | const wrongAttesterId = nonSignedUpAttesterId 66 | const circuitInputs = await genProveSignUpCircuitInput(user, epoch, reputationRecords, attesterId) 67 | const input: SignUpProof = await genInputForContract(Circuit.proveUserSignUp, circuitInputs) 68 | input.attesterId = wrongAttesterId 69 | 70 | const isProofValid = await unirepContract.verifyUserSignUp(input) 71 | expect(isProofValid, 'Verify user sign up proof on-chain should fail').to.be.false 72 | }) 73 | 74 | it('wrong epoch should fail', async () => { 75 | const attesterId = signedUpAttesterId 76 | const wrongEpoch = epoch + 1 77 | const circuitInputs = await genProveSignUpCircuitInput(user, epoch, reputationRecords, attesterId) 78 | const input: SignUpProof = await genInputForContract(Circuit.proveUserSignUp, circuitInputs) 79 | input.epoch = wrongEpoch 80 | 81 | const isProofValid = await unirepContract.verifyUserSignUp(input) 82 | expect(isProofValid, 'Verify user sign up proof on-chain should fail').to.be.false 83 | }) 84 | 85 | it('wrong epoch key should fail', async () => { 86 | const attesterId = signedUpAttesterId 87 | const wrongEpochKey = genEpochKey(user['identityNullifier'], epoch, nonce + 1, circuitEpochTreeDepth) 88 | const circuitInputs = await genProveSignUpCircuitInput(user, epoch, reputationRecords, attesterId) 89 | const input: SignUpProof = await genInputForContract(Circuit.proveUserSignUp, circuitInputs) 90 | input.epochKey = wrongEpochKey 91 | 92 | const isProofValid = await unirepContract.verifyUserSignUp(input) 93 | expect(isProofValid, 'Verify user sign up proof on-chain should fail').to.be.false 94 | }) 95 | }) -------------------------------------------------------------------------------- /test/contracts/UserStateTransitionVerifier.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { ethers as hardhatEthers } from 'hardhat' 3 | import { ethers } from 'ethers' 4 | import { expect } from "chai" 5 | import { genIdentity, genRandomSalt} from "../../crypto" 6 | import { Circuit } from "../../circuits/utils" 7 | import { genUserStateTransitionCircuitInput, getTreeDepthsForTesting, genInputForContract } from '../utils' 8 | import { deployUnirep, UserTransitionProof } from '../../core' 9 | import { epochLength } from '../../config/testLocal' 10 | 11 | describe('User State Transition', function () { 12 | this.timeout(600000) 13 | let accounts 14 | let unirepContract 15 | 16 | const epoch = 1 17 | const user = genIdentity() 18 | const proofIndexes = [] 19 | 20 | before(async () => { 21 | accounts = await hardhatEthers.getSigners() 22 | 23 | const _treeDepths = getTreeDepthsForTesting() 24 | unirepContract = await deployUnirep(accounts[0], _treeDepths) 25 | }) 26 | 27 | it('Valid user state update inputs should work', async () => { 28 | const circuitInputs = await genUserStateTransitionCircuitInput(user, epoch) 29 | const input: UserTransitionProof = await genInputForContract(Circuit.userStateTransition, circuitInputs) 30 | const isValid = await input.verify() 31 | expect(isValid, 'Verify user state transition proof off-chain failed').to.be.true 32 | const isProofValid = await unirepContract.verifyUserStateTransition(input) 33 | expect(isProofValid).to.be.true 34 | 35 | // UST should be performed after epoch transition 36 | // Fast-forward epochLength of seconds 37 | await hardhatEthers.provider.send("evm_increaseTime", [epochLength]) 38 | let tx = await unirepContract.beginEpochTransition() 39 | let receipt = await tx.wait() 40 | expect(receipt.status).equal(1) 41 | 42 | tx = await unirepContract.updateUserStateRoot( 43 | input, proofIndexes 44 | ) 45 | receipt = await tx.wait() 46 | expect(receipt.status).equal(1) 47 | 48 | const pfIdx = await unirepContract.getProofIndex(input.hash()) 49 | expect(Number(pfIdx)).not.eq(0) 50 | }) 51 | 52 | it('Proof with wrong epoch should fail', async () => { 53 | const wrongEpoch = epoch + 1 54 | const circuitInputs = await genUserStateTransitionCircuitInput(user, epoch) 55 | const input: UserTransitionProof = await genInputForContract(Circuit.userStateTransition, circuitInputs) 56 | input.transitionFromEpoch = wrongEpoch 57 | const isProofValid = await unirepContract.verifyUserStateTransition(input) 58 | expect(isProofValid).to.be.false 59 | }) 60 | 61 | it('Proof with wrong global state tree root should fail', async () => { 62 | const wrongGlobalStateTreeRoot = genRandomSalt() 63 | const circuitInputs = await genUserStateTransitionCircuitInput(user, epoch) 64 | const input: UserTransitionProof = await genInputForContract(Circuit.userStateTransition, circuitInputs) 65 | input.fromGlobalStateTree = wrongGlobalStateTreeRoot 66 | const isProofValid = await unirepContract.verifyUserStateTransition(input) 67 | expect(isProofValid).to.be.false 68 | }) 69 | 70 | it('Proof with wrong epoch tree root should fail', async () => { 71 | const wrongEpochTreeRoot = genRandomSalt() 72 | const circuitInputs = await genUserStateTransitionCircuitInput(user, epoch) 73 | const input: UserTransitionProof = await genInputForContract(Circuit.userStateTransition, circuitInputs) 74 | input.fromEpochTree = wrongEpochTreeRoot 75 | const isProofValid = await unirepContract.verifyUserStateTransition(input) 76 | expect(isProofValid).to.be.false 77 | }) 78 | 79 | it('Proof with wrong blinded user states should fail', async () => { 80 | const circuitInputs = await genUserStateTransitionCircuitInput(user, epoch) 81 | const input: UserTransitionProof = await genInputForContract(Circuit.userStateTransition, circuitInputs) 82 | input.blindedUserStates[0] = genRandomSalt() 83 | const isProofValid = await unirepContract.verifyUserStateTransition(input) 84 | expect(isProofValid).to.be.false 85 | }) 86 | 87 | it('Proof with wrong blinded hash chain should fail', async () => { 88 | const circuitInputs = await genUserStateTransitionCircuitInput(user, epoch) 89 | const input: UserTransitionProof = await genInputForContract(Circuit.userStateTransition, circuitInputs) 90 | input.blindedHashChains[0] = genRandomSalt() 91 | const isProofValid = await unirepContract.verifyUserStateTransition(input) 92 | expect(isProofValid).to.be.false 93 | }) 94 | 95 | it('Proof with wrong global state tree leaf should fail', async () => { 96 | const circuitInputs = await genUserStateTransitionCircuitInput(user, epoch) 97 | const input: UserTransitionProof = await genInputForContract(Circuit.userStateTransition, circuitInputs) 98 | input.newGlobalStateTreeLeaf = genRandomSalt() 99 | const isProofValid = await unirepContract.verifyUserStateTransition(input) 100 | expect(isProofValid).to.be.false 101 | }) 102 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "noImplicitAny": false, 9 | "outDir": "./build", 10 | }, 11 | "include": ["./crypto", "./core", "./cli", "./test"], 12 | "files": ["./hardhat.config.ts"] 13 | } --------------------------------------------------------------------------------