Skip to content

Commit 34d2a02

Browse files
authored
Merge pull request #37 from Poly-pay/gitbook-sync
Update documentation and update API URL for ZK verification service
2 parents 29964c2 + 232d4c6 commit 34d2a02

File tree

7 files changed

+36
-42
lines changed

7 files changed

+36
-42
lines changed
-10.1 KB
Loading
34.1 KB
Loading
17.5 KB
Loading

docs/developer-documentation/circuit-code-walkthrough.md

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ This page provides a detailed explanation of the [Noir](https://noir-lang.org) c
44

55
## Overview
66

7-
The circuit file is located at `packages/nextjs/public/circuit/src/main.nr`. It proves four things in a single proof: transaction hash commitment is correct, ECDSA signature is valid, prover is a member of authorized signers, and nullifier prevents double-signing.
7+
The circuit file is located at `packages/nextjs/public/circuit/src/main.nr`. It proves four things in a single proof: transaction hash commitment is correct, ECDSA signature is valid, prover knows the secret for their commitment, and nullifier prevents double-signing.
88

99
## Circuit Structure
1010

11-
### Constants and Imports
11+
### Imports
1212

13-
The circuit uses `keccak256` for Ethereum-compatible message hashing, `poseidon` as a ZK-friendly hash function for commitments and nullifiers, and sets `DEPTH = 4` meaning the Merkle tree supports 2^4 = 16 signers maximum.
13+
The circuit uses `keccak256` for Ethereum-compatible message hashing and `poseidon` as a ZK-friendly hash function for commitments and nullifiers.
1414

1515
## Main Function Inputs
1616

@@ -24,8 +24,6 @@ These inputs are hidden from everyone - only the prover knows them:
2424
| pub_key_x | [u8; 32] | Public key X coordinate |
2525
| pub_key_y | [u8; 32] | Public key Y coordinate |
2626
| secret | Field | Signer's secret |
27-
| leaf_index | Field | Position in Merkle tree |
28-
| merkle_path | [Field; DEPTH] | Sibling hashes for proof |
2927
| tx_hash_bytes | [u8; 32] | Transaction hash to sign |
3028

3129
### Public Inputs
@@ -35,7 +33,7 @@ These inputs are visible on-chain and used for verification:
3533
| Input | Type | Description |
3634
|-------|------|-------------|
3735
| tx_hash_commitment | Field | Poseidon hash of tx_hash |
38-
| merkle_root | Field | Root of authorized signers tree |
36+
| commitment | Field | hash(secret, secret) - checked against signers list |
3937
| nullifier | Field | Unique identifier to prevent double-signing |
4038

4139
## Step-by-Step Explanation
@@ -52,11 +50,11 @@ The circuit reconstructs Ethereum's `personal_sign` prefix `"\x19Ethereum Signed
5250

5351
**Why prefix?** Ethereum wallets always add this prefix when signing. We must match the exact message that was signed.
5452

55-
### Step 3: Verify Merkle Membership
53+
### Step 3: Verify Commitment Ownership
5654

57-
The circuit computes commitment from secret using `commitment = hash(secret, secret)`, uses `leaf_index` and `merkle_path` to compute [Merkle](https://en.wikipedia.org/wiki/Merkle_tree) root, then compares with public `merkle_root`.
55+
The circuit computes commitment from secret using `commitment = hash(secret, secret)`, then compares with public `commitment`.
5856

59-
**Privacy:** The circuit proves "I know a secret whose commitment is in the tree" without revealing which leaf.
57+
**How authorization works:** The circuit proves "I know the secret for this commitment". Then the smart contract checks "Is this commitment in the signers list?" This two-step verification ensures only authorized signers can sign transactions.
6058

6159
### Step 4: Verify Nullifier
6260

@@ -66,10 +64,6 @@ The circuit computes nullifier using `nullifier = hash(secret, tx_hash)` and com
6664

6765
## Helper Functions
6866

69-
### compute_merkle_root
70-
71-
This function converts `leaf_index` to bits (little-endian), then for each level, the bit determines left/right position: bit = 0 means current is left child, bit = 1 means current is right child. It hashes with sibling from `merkle_path` and repeats until reaching root.
72-
7367
### bytes_to_field
7468

7569
Converts 32-byte array to single Field element by treating bytes as big-endian number.
@@ -83,7 +77,7 @@ Wrapper for [Poseidon](https://www.poseidon-hash.info) hash with 2 inputs.
8377
| Attack | Prevention |
8478
|--------|------------|
8579
| Fake signature | ECDSA verification in circuit |
86-
| Non-member signing | Merkle membership proof |
80+
| Non-member signing | Commitment checked against signers list on-chain |
8781
| Double signing | Nullifier stored on-chain |
8882
| Transaction tampering | tx_hash_commitment verification |
8983
| Replay attack | Nonce included in tx_hash |
@@ -92,7 +86,6 @@ Wrapper for [Poseidon](https://www.poseidon-hash.info) hash with 2 inputs.
9286

9387
Navigate to noir package with `cd packages/nextjs/public/circuit/src`, compile circuit with `nargo compile`, run tests with `nargo test`.
9488

95-
9689
## Learn More
9790

9891
- [Noir Language Documentation](https://noir-lang.org/docs)

docs/privacy-architecture.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,32 @@
55
| Aspect | Traditional Multisig | PolyPay |
66
| --------------- | -------------------------------- | ------------------------- |
77
| Signer Identity | Public addresses stored on-chain | Hidden behind commitments |
8-
| Who Signed | Visible to everyone | Only the signer knows |
8+
| Who Signed | Visible to everyone | Commitment visible, EOA hidden |
99

1010
### Commitment-Based Identity
1111

1212
Instead of storing addresses, PolyPay stores **commitments** (hash(secret, secret)):
1313

1414
* The **secret** is derived from signing a message with your wallet
15-
* The **commitment** is stored on-chain (in a Merkle tree)
15+
* The **commitment** is stored on-chain in a signers list
1616
* Only you know the secret that matches your commitment
1717

1818
### How It Works
1919

2020
1. **Setup**: Each signer generates a secret and computes their commitment
21-
2. **Registration**: Commitments are added to the smart contract's Merkle tree
22-
3. **Signing**: To approve a transaction, signers prove they know a secret that matches one of the commitments - without revealing which one
21+
2. **Registration**: Commitments are added to the smart contract's signers list
22+
3. **Signing**: To approve a transaction, signers prove they know the secret for their commitment using ZK proofs
23+
4. **Verification**: The smart contract checks if the commitment exists in the signers list
2324

24-
### Anonymity Set
25+
### Privacy Model
2526

26-
All signers share the same anonymity set. When you sign a transaction:
27+
When you sign a transaction:
2728

28-
* The contract knows "one of the N signers approved"
29-
* Nobody knows "which specific signer approved"
29+
* The ZK proof verifies you know the secret for your commitment
30+
* The contract checks your commitment is in the authorized signers list
31+
* Your Ethereum address (EOA) is never revealed on-chain
3032

31-
This is achieved through ZK proofs and Merkle tree membership proofs.
33+
This means observers can see which commitment signed, but cannot link it back to your wallet address.
3234

3335
### Relayer Privacy
3436

@@ -43,4 +45,4 @@ PolyPay's backend uses a dedicated relayer wallet to deploy wallets and execute
4345
| Deploy wallet | Creator address exposed | Only relayer visible |
4446
| Execute transaction | Executor address exposed | Only relayer visible |
4547

46-
This creates **complete anonymity**: no signer address ever appears on-chain.
48+
This creates **complete EOA anonymity**: no signer's Ethereum address ever appears on-chain.

docs/zero-knowledge-implementation.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ In a traditional multisig wallet:
1515
- Signer addresses are public on blockchain
1616

1717
In PolyPay:
18-
- You prove "I am an authorized signer" without revealing WHICH signer you are
19-
- Only the proof is public, your identity stays private
18+
- You prove "I know the secret for an authorized commitment" without revealing your EOA address
19+
- Your Ethereum address stays private, only the commitment is visible
2020

2121
## The Four Proofs
2222

@@ -44,18 +44,22 @@ When you sign a transaction in PolyPay, the ZK circuit proves four things simult
4444

4545
### Proof 3: "I am authorized"
4646

47-
**Problem:** How to prove you're in the signers list without revealing which one?
47+
**Problem:** How to prove you're in the signers list?
4848

4949
**Solution:**
5050
- Each signer has a secret "commitment" stored as: `commitment = hash(secret, secret)`
51-
- All commitments form a [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree)
52-
- You prove your commitment exists in the tree WITHOUT revealing which leaf
51+
- The circuit proves you know the secret for a given commitment
52+
- The smart contract checks if that commitment exists in the signers list
5353

54-
**Analogy:** Imagine a club membership list. You prove "my name is on the list" without pointing to which line.
54+
**Analogy:** Imagine a club membership list. You prove "I know the password for one of these memberships" and the club verifies that membership is on the list.
5555

56-
**How Merkle Proof works:**
56+
**How it works:**
5757

58-
You have a tree structure where your commitment is one of the leaves (A, B, C, or D). To prove membership, you provide sibling hashes along the path from your leaf to the root. The circuit computes the root from your path and checks it matches the public root.
58+
The circuit verifies: `hash(secret, secret) == commitment`
59+
60+
Then the smart contract checks: `commitment in signers list?`
61+
62+
This two-step verification ensures only authorized signers can sign transactions while keeping their Ethereum addresses private.
5963

6064
### Proof 4: "I haven't signed before"
6165

@@ -72,11 +76,11 @@ You have a tree structure where your commitment is one of the leaves (A, B, C, o
7276

7377
1. **User Signs:** User signs tx_hash with their Ethereum wallet → Produces signature, pub_key_x, pub_key_y
7478

75-
2. **Frontend Generates Proof:** [Noir](https://noir-lang.org) circuit receives private inputs (signature, pub_key, secret, merkle_path, tx_hash) and public inputs (tx_hash_commitment, merkle_root, nullifier) → Outputs ZK Proof
79+
2. **Frontend Generates Proof:** [Noir](https://noir-lang.org) circuit receives private inputs (signature, pub_key, secret, tx_hash) and public inputs (tx_hash_commitment, commitment, nullifier) → Outputs ZK Proof
7680

7781
3. **Backend Verifies via zkVerify:** Proof submitted to [zkVerify](https://docs.zkverify.io) for verification → Returns aggregation_id, attestation
7882

79-
4. **Smart Contract Executes:** When threshold signatures reached, contract verifies all proofs on-chain, checks nullifiers not used, checks merkle_root matches current signers, then executes transaction
83+
4. **Smart Contract Executes:** When threshold signatures reached, contract verifies all proofs on-chain, checks nullifiers not used, checks each commitment is in current signers list, then executes transaction
8084

8185
## Circuit Inputs Reference
8286

@@ -88,16 +92,14 @@ You have a tree structure where your commitment is one of the leaves (A, B, C, o
8892
| pub_key_x | [u8; 32] | Public key X coordinate |
8993
| pub_key_y | [u8; 32] | Public key Y coordinate |
9094
| secret | Field | Signer's secret (from signing "polypay-identity") |
91-
| leaf_index | Field | Position in Merkle tree |
92-
| merkle_path | [Field; 4] | Sibling hashes for proof |
9395
| tx_hash_bytes | [u8; 32] | Transaction hash to sign |
9496

9597
### Public Inputs (Visible on-chain)
9698

9799
| Input | Type | Description |
98100
|-------|------|-------------|
99101
| tx_hash_commitment | Field | Poseidon hash of tx_hash |
100-
| merkle_root | Field | Root of authorized signers tree |
102+
| commitment | Field | hash(secret, secret) - checked against signers list |
101103
| nullifier | Field | Prevents double-signing |
102104

103105
## More Detail

packages/backend/src/zkverify/zkverify.service.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,15 @@ import axios from 'axios';
99
import * as fs from 'fs';
1010
import * as path from 'path';
1111
import {
12-
SubmitProofDto,
1312
ZkVerifySubmitResponse,
1413
ZkVerifyJobStatusResponse,
15-
ProposeTxAndSubmitProofDto,
16-
SignTxDto,
1714
} from './dto';
1815
import { PrismaService } from '@/database/prisma.service';
1916

2017
@Injectable()
2118
export class ZkVerifyService {
2219
private readonly logger = new Logger(ZkVerifyService.name);
23-
private readonly apiUrl = 'https://relayer-api-testnet.horizenlabs.io/api/v1';
20+
private readonly apiUrl = 'https://api-testnet.kurier.xyz/api/v1';
2421
private readonly apiKey: string;
2522
private readonly vkeyPath: string;
2623

0 commit comments

Comments
 (0)