@cheny56/zk-confidential-offchain
v1.0.0
Published
Confidential smart contracts using ZK proofs - Off-chain proof generation with snarkjs/Circom. Works on ANY EVM chain.
Downloads
104
Maintainers
Readme
@cheny56/zk-confidential-offchain
Confidential Smart Contracts using Zero-Knowledge Proofs (Off-Chain Verification)
A privacy-preserving token system where:
- ✅ Inputs are private - Transfer amounts are hidden
- ✅ State is hidden - Balances stored as commitments
- ✅ Only proofs are public - ZK proofs verify correctness without revealing data
This package uses snarkjs/Circom for proof generation and works on ANY EVM chain.
Features
- 🌐 Any EVM Chain - Works on Ethereum, Polygon, BSC, Quorum, etc.
- 🌍 Browser Compatible - Proofs can be generated in web browsers
- 🔒 Full Privacy - Balances, transfers, identities all hidden
- 📦 Self-Contained - No external dependencies on special nodes
- 🛠️ Customizable - Modify Circom circuits as needed
Installation
npm install @cheny56/zk-confidential-offchainQuick Start
const {
NoteWallet,
Note,
MerkleTree,
poseidonHash
} = require('@cheny56/zk-confidential-offchain');
// Create a wallet
const wallet = new NoteWallet();
await wallet.init();
// Create a private note (hidden balance)
const note = await wallet.createNote(1000n);
console.log('Commitment:', note.getCommitmentHex());
// Check balance (only you know this)
console.log('Balance:', wallet.getBalance());Table of Contents
- How It Works
- Core Concepts
- API Reference
- Step-by-Step Guide
- Examples
- Circuit Compilation
- Contract Deployment
- Security Considerations
How It Works
┌─────────────────────────────────────────────────────────────────────────┐
│ CONFIDENTIAL TOKEN FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ DEPOSIT (Public ETH → Private Balance) │
│ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │
│ │ ETH │ --> │ ZK Proof │ --> │ Commitment stored │ │
│ │ (public) │ │ (snarkjs) │ │ in Merkle tree │ │
│ └────────────┘ └────────────┘ └────────────────────────┘ │
│ │
│ TRANSFER (Private → Private) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Sender creates proof showing: │ │
│ │ 1. They own the input note (know secret) │ │
│ │ 2. Input note exists in Merkle tree │ │
│ │ 3. Input value = Output values (conservation) │ │
│ │ 4. Nullifier prevents double-spending │ │
│ │ │ │
│ │ Public: nullifier, new commitments │ │
│ │ Hidden: sender, recipient, amounts │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ WITHDRAW (Private Balance → Public ETH) │
│ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │
│ │ Commitment │ --> │ ZK Proof │ --> │ ETH sent to │ │
│ │ (private) │ │ │ │ recipient (public) │ │
│ └────────────┘ └────────────┘ └────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘Core Concepts
Note
A Note is a private balance unit (like a UTXO):
Note = {
value: 1000, // Hidden amount
ownerSecret: 0x..., // Only owner knows this
nonce: 0x..., // Random value
commitment: Hash(value, ownerSecret, nonce) // Public
}Commitment
A Commitment hides the note's contents:
commitment = Poseidon(value, ownerSecret, nonce)- Anyone can see the commitment
- Nobody can determine value or owner from it
Nullifier
A Nullifier prevents double-spending:
nullifier = Poseidon(commitment, ownerSecret, leafIndex)- Revealed when spending a note
- Can't be linked back to the commitment
Merkle Tree
All commitments are stored in a Merkle Tree:
- Efficient membership proofs (O(log n))
- ZK-friendly with Poseidon hash
API Reference
NoteWallet
const { NoteWallet } = require('@cheny56/zk-confidential-offchain');
const wallet = new NoteWallet();
await wallet.init();
// Create a note
const note = await wallet.createNote(amount);
// Get balance
const balance = wallet.getBalance();
// Get spendable notes
const notes = wallet.getSpendableNotes();
// Select notes for transfer
const selected = wallet.selectNotesForAmount(amount);
// Export for backup
const backup = wallet.toJSON();
// Restore from backup
const restored = NoteWallet.fromJSON(backup);Note
const { Note } = require('@cheny56/zk-confidential-offchain');
// Create manually
const note = new Note(value, ownerSecret, nonce);
// Get commitment
const commitment = note.getCommitmentHex();
// Get nullifier (after tree insertion)
const nullifier = note.getNullifierHex();
// Check if spendable
const canSpend = note.canSpend();
// Get private inputs for ZK proof
const inputs = note.getPrivateInputs();MerkleTree
const { MerkleTree } = require('@cheny56/zk-confidential-offchain');
const tree = new MerkleTree(20); // depth 20
await tree.init();
// Insert commitment
const index = await tree.insert(commitment);
// Get root
const root = tree.getRoot();
// Generate proof
const { path, indices } = await tree.generateProof(index);
// Verify proof
const valid = MerkleTree.verifyProof(leaf, index, path, indices, root);Poseidon Hash
const { poseidonHash, toHex32 } = require('@cheny56/zk-confidential-offchain');
// Hash values
const hash = await poseidonHash(value1, value2, value3);
console.log(toHex32(hash));Step-by-Step Guide
Step 1: Install and Initialize
# Create project
mkdir my-confidential-app
cd my-confidential-app
npm init -y
# Install package
npm install @cheny56/zk-confidential-offchainStep 2: Create a Wallet
const { NoteWallet } = require('@cheny56/zk-confidential-offchain');
async function main() {
// Create wallet
const wallet = new NoteWallet();
await wallet.init();
console.log('Wallet created!');
console.log('Master secret (KEEP SAFE):', wallet.getMasterSecretHex());
}
main();Step 3: Create Notes (Private Balances)
// Create notes representing private balances
const note1 = await wallet.createNote(1000n);
const note2 = await wallet.createNote(500n);
const note3 = await wallet.createNote(250n);
console.log('Created notes:');
console.log(' Note 1:', note1.value, '→', note1.getCommitmentHex().slice(0, 20) + '...');
console.log(' Note 2:', note2.value, '→', note2.getCommitmentHex().slice(0, 20) + '...');
console.log(' Note 3:', note3.value, '→', note3.getCommitmentHex().slice(0, 20) + '...');
console.log('Total balance:', wallet.getBalance());Step 4: Insert into Merkle Tree
const { MerkleTree } = require('@cheny56/zk-confidential-offchain');
const tree = new MerkleTree(20);
await tree.init();
// Insert notes (simulates on-chain deposit)
for (const note of wallet.getSpendableNotes()) {
const index = await tree.insert(note.commitment);
const { path, indices } = await tree.generateProof(index);
note.setTreePosition(index, tree.getRoot(), path, indices);
console.log(`Inserted note at index ${index}`);
}
console.log('Merkle root:', tree.getRootHex());Step 5: Generate Transfer Inputs
// Select notes to spend
const amountToSend = 300n;
const notesToSpend = wallet.selectNotesForAmount(amountToSend);
// Get private inputs for ZK proof
for (const note of notesToSpend) {
const inputs = note.getPrivateInputs();
console.log('Private inputs:', inputs);
}Step 6: Verify Your Setup
# Run the verification example
node examples/verify-setup.jsExamples
Basic Balance Management
// examples/basic-balance.js
const { NoteWallet, MerkleTree, toHex32 } = require('@cheny56/zk-confidential-offchain');
async function main() {
console.log('=== Private Balance Management ===\n');
// 1. Create wallet
const wallet = new NoteWallet();
await wallet.init();
// 2. Create Merkle tree
const tree = new MerkleTree(20);
await tree.init();
// 3. Create notes
const amounts = [1000n, 500n, 250n];
for (const amount of amounts) {
const note = await wallet.createNote(amount);
const index = await tree.insert(note.commitment);
const { path, indices } = await tree.generateProof(index);
note.setTreePosition(index, tree.getRoot(), path, indices);
console.log(`Created note: ${amount} → ${note.getCommitmentHex().slice(0, 30)}...`);
}
// 4. Check balance
console.log(`\nTotal private balance: ${wallet.getBalance()}`);
console.log(`Spendable notes: ${wallet.getSpendableNotes().length}`);
// 5. Select for transfer
const selected = wallet.selectNotesForAmount(600n);
console.log(`\nTo send 600, would spend ${selected.length} notes totaling ${selected.reduce((s, n) => s + n.value, 0n)}`);
}
main().catch(console.error);Transfer Simulation
// examples/transfer-simulation.js
const { NoteWallet, Note, MerkleTree, toHex32, generateOwnerSecret } = require('@cheny56/zk-confidential-offchain');
async function main() {
console.log('=== Private Transfer Simulation ===\n');
// Alice's wallet
const aliceWallet = new NoteWallet();
await aliceWallet.init();
// Bob's receiving secret
const bobSecret = generateOwnerSecret();
// Shared Merkle tree (on-chain)
const tree = new MerkleTree(20);
await tree.init();
// Alice deposits 1000
console.log('1. Alice deposits 1000');
const aliceNote = await aliceWallet.createNote(1000n);
const aliceIndex = await tree.insert(aliceNote.commitment);
const aliceProof = await tree.generateProof(aliceIndex);
aliceNote.setTreePosition(aliceIndex, tree.getRoot(), aliceProof.path, aliceProof.indices);
console.log(` Commitment: ${aliceNote.getCommitmentHex().slice(0, 30)}...`);
// Alice transfers 300 to Bob
console.log('\n2. Alice transfers 300 to Bob');
// Create Bob's note
const bobNote = new Note(300n, bobSecret);
console.log(` Bob's commitment: ${bobNote.getCommitmentHex().slice(0, 30)}...`);
// Create Alice's change note
const aliceChangeNote = await aliceWallet.createNote(700n);
console.log(` Alice's change: ${aliceChangeNote.getCommitmentHex().slice(0, 30)}...`);
// Compute nullifier (prevents double-spend)
const nullifier = aliceNote.getNullifierHex();
console.log(` Nullifier: ${nullifier.slice(0, 30)}...`);
// Mark Alice's original note as spent
aliceNote.markSpent();
// Insert new notes into tree
const bobIndex = await tree.insert(bobNote.commitment);
const changeIndex = await tree.insert(aliceChangeNote.commitment);
// Update positions
const bobProof = await tree.generateProof(bobIndex);
bobNote.setTreePosition(bobIndex, tree.getRoot(), bobProof.path, bobProof.indices);
const changeProof = await tree.generateProof(changeIndex);
aliceChangeNote.setTreePosition(changeIndex, tree.getRoot(), changeProof.path, changeProof.indices);
// Final state
console.log('\n3. Final State');
console.log(` Alice's balance: ${aliceWallet.getBalance()}`);
console.log(` Bob's note value: ${bobNote.value}`);
console.log(` Merkle root: ${tree.getRootHex().slice(0, 30)}...`);
// Privacy summary
console.log('\n4. Privacy Summary');
console.log(' PUBLIC: nullifier, new commitments, merkle root');
console.log(' HIDDEN: Alice, Bob, 300 transferred, 700 change');
}
main().catch(console.error);Circuit Compilation
If you need to customize the circuits:
# Install circom globally
npm install -g circom snarkjs
# Compile mint circuit
circom circuits/mint.circom --r1cs --wasm --sym -o build/
# Compile transfer circuit
circom circuits/transfer.circom --r1cs --wasm --sym -o build/
# Download powers of tau (if needed)
wget https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_12.ptau -O pot12_final.ptau
# Generate proving keys
snarkjs groth16 setup build/mint.r1cs pot12_final.ptau build/mint.zkey
snarkjs groth16 setup build/transfer.r1cs pot12_final.ptau build/transfer.zkey
# Export Solidity verifiers
snarkjs zkey export solidityverifier build/mint.zkey contracts/MintVerifier.sol
snarkjs zkey export solidityverifier build/transfer.zkey contracts/TransferVerifier.solContract Deployment
const { ethers } = require('ethers');
async function deploy() {
const provider = new ethers.JsonRpcProvider('http://localhost:8545');
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
// Deploy MintVerifier
const MintVerifier = await ethers.ContractFactory.fromSolidity(
require('./artifacts/MintVerifier.json'),
wallet
);
const mintVerifier = await MintVerifier.deploy();
// Deploy TransferVerifier
const TransferVerifier = await ethers.ContractFactory.fromSolidity(
require('./artifacts/TransferVerifier.json'),
wallet
);
const transferVerifier = await TransferVerifier.deploy();
// Deploy ConfidentialToken
const ConfidentialToken = await ethers.ContractFactory.fromSolidity(
require('./artifacts/ConfidentialToken.json'),
wallet
);
const token = await ConfidentialToken.deploy(
'Private Token',
'PRIV',
await mintVerifier.getAddress(),
await transferVerifier.getAddress()
);
console.log('Contracts deployed:');
console.log(' MintVerifier:', await mintVerifier.getAddress());
console.log(' TransferVerifier:', await transferVerifier.getAddress());
console.log(' ConfidentialToken:', await token.getAddress());
}Security Considerations
- Backup Your Wallet: Losing your master secret means losing all funds
- Trusted Setup: The proving keys require a trusted setup ceremony
- Nullifier Tracking: On-chain nullifier set prevents double-spending
- Gas Costs: Solidity verification costs ~5M gas per proof
- Timing Attacks: Consider using a relayer to hide sender address
Package Contents
zk-confidential-offchain/
├── lib/
│ ├── index.js # Main exports
│ ├── poseidon.js # Poseidon hash
│ ├── merkle.js # Merkle tree
│ ├── note.js # Note management
│ └── commitment.js # Commitment utilities
├── client/
│ └── confidential-client.js # Contract interaction
├── contracts/
│ ├── ConfidentialToken.sol
│ ├── CommitmentTree.sol
│ └── interfaces/
├── circuits/
│ ├── mint.circom
│ └── transfer.circom
├── examples/
│ ├── basic-balance.js
│ ├── transfer-simulation.js
│ └── verify-setup.js
├── package.json
└── README.mdLicense
MIT
Related
- @cheny56/zk-confidential-onchain - Native precompile version
- @cheny56/zk-voting - ZK voting system
