A Node.js proof-of-concept demonstrating zero-knowledge proof techniques for verifying financial totals without revealing individual entry values. Perfect for privacy-preserving audits where a client proves their total to an auditor without disclosing sensitive transaction details.
This system uses:
- Pedersen Commitments - Cryptographically hide individual entry values
- Homomorphic Addition - Sum commitments without decrypting
- Schnorr Proofs via Fiat-Shamir - Non-interactive zero-knowledge proof that the client knows the committed values
- Client (User): Enters multiple financial entries (e.g., transactions, P&L items). Each entry is committed using a Pedersen Commitment. The commitments are summed homomorphically.
- NIZK Generation: A Schnorr proof is generated using the Fiat-Shamir transformation to prove knowledge of the total value and blinding factors.
- Auditor (Verifier): Receives only the total commitment and proof. Verifies the proof without seeing individual entries.
- Prime modulus:
p = 89 - Generator for values:
g = 3 - Generator for blinding:
h = 7 - Group order:
88(p - 1)
C = g^v · h^r (mod p)
Where v is the value and r is a random blinding factor.
C_total = ∏ C_i (mod p)
The product of commitments equals the commitment of the sum.
Client (Prover):
- Pick random
k_v,k_r - Compute announcement:
a = g^k_v · h^k_r (mod p) - Compute challenge:
e = H(C_total, a, v_total) - Compute responses:
z_v = k_v + e·v_total (mod order),z_r = k_r + e·r_total (mod order)
Auditor (Verifier):
- Recompute challenge:
e = H(C_total, a, v_total) - Verify:
g^z_v · h^z_r ≡ a · C_total^e (mod p)
Here's the exact sequence of how the NIZK proof is generated:
┌─────────────────────────────────────────────────────────┐
│ CLIENT already has these from the entries: │
│ • C_total (product of all commitments) │
│ • v_total (sum of entry values - public) │
│ • r_total (sum of blinding factors - SECRET) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ STEP 1: Pick random k_v and k_r │
│ • k_v = random() (kept secret) │
│ • k_r = random() (kept secret) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ STEP 2: Compute announcement 'a' │
│ • a = g^k_v × h^k_r mod p │
│ This is a "commitment to randomness" │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ STEP 3: Compute challenge 'e' via Fiat-Shamir hash │
│ • e = Hash(C_total, a, v_total) mod order │
│ │
│ Note: Uses v_total (public value), NOT z_v! │
│ The client cannot predict 'e' before committing 'a' │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ STEP 4: Compute responses z_v and z_r │
│ • z_v = k_v + (e × v_total) mod order │
│ • z_r = k_r + (e × r_total) mod order │
│ │
│ These "blend" the random k values with secrets, │
│ making them verifiable but not reversible. │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ PROOF SENT TO AUDITOR: │
│ • C_total - Total commitment │
│ • a - Announcement │
│ • e - Challenge │
│ • z_v - Response for value │
│ • z_r - Response for blinding factor │
│ • v_total - Claimed total value │
│ • entryCount - Number of entries │
│ │
│ Individual entry values remain PRIVATE! │
└─────────────────────────────────────────────────────────┘
Key Insight: The order is a → e → z_v, z_r (no circular dependency). The challenge e is computed from the hash of (C_total, a, v_total) BEFORE computing z_v and z_r.
Why This Works:
- The client commits to random
afirst - Challenge
eis unpredictably derived fromavia hash - Responses
z_vandz_rare "blinded" by randomk_vandk_r - The auditor cannot extract secrets, but the verification equation only passes if the client knows the real values
# No external dependencies required - uses only Node.js built-ins
npm install # (optional, no dependencies)npm start
# or
node index.js| Command | Description |
|---|---|
[number] |
Enter an audit entry amount (positive or negative) |
status |
View all entries and commitments |
finish |
Generate the NIZK proof |
audit |
Enter Auditor Mode (verification view) |
reset |
Clear all entries |
help |
Show available commands |
exit |
Quit the application |
Client> 100
--- Entry Committed ---
Entry #1
Value: 100
Blinding Factor (r): 45
Commitment (C): 23
Client> 50
--- Entry Committed ---
Entry #2
Value: 50
...
Client> -30
--- Entry Committed ---
Entry #3
Value: -30
...
Client> finish
--- NIZK PROOF GENERATED ---
Total Commitment: 56
Announcement: 31
Challenge: 42
...
Client> audit
--- AUDITOR MODE (Verification View) ---
[AUDITOR] Verifying proof...
VERIFICATION RESULT: SUCCESS
[AUDITOR] Verified total: 120
[AUDITOR] Individual entry values remain PRIVATE.
npm test
# or
node test.js├── index.js # Main interactive application
├── test.js # Automated test script
├── package.json
└── README.md
This is a POC for educational purposes only:
- Uses small parameters (
p = 89) for demonstration - Production systems require:
- Large primes (2048+ bits)
- Cryptographically secure parameter generation
- Additional security hardening
- Individual entries are hidden: Each entry is wrapped in a commitment that cannot be opened without the blinding factor.
- Homomorphic aggregation: Commitments can be combined mathematically without revealing underlying values.
- Zero-knowledge proof: The auditor is convinced the client knows the total value without learning anything else about individual entries.
- Financial Audits: Prove total revenue/expenses without revealing individual transactions
- Tax Compliance: Verify aggregate amounts while keeping transaction details private
- Supply Chain: Prove total quantities without exposing supplier relationships
- Healthcare: Verify aggregate patient data without exposing individual records
ISC