NixProtocol Documentation
Add zero-knowledge proofs and encryption to your application. NixProtocol provides privacy infrastructure for Ethereum and L2s. Reference implementation for quick integration, or enterprise engagements for custom solutions.
Architecture Overview
NixProtocol provides privacy through a layered architecture combining encryption, zero-knowledge proofs, and smart contracts.
┌─────────────────────────────────────────────────────────────────┐
│ Your Application │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ @nixprotocol/sdk │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Key Gen │ │ ElGamal │ │ ZK Proof Generator │ │
│ │ (BJJ) │ │ Encrypt │ │ (Groth16/WASM) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Smart Contracts │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ NixERC20 │ │ NixPool │ │ Verifier Contracts │ │
│ │ (Balances) │ │ (Dark Pool)│ │ (Groth16 Verifier) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Ethereum │ Base │ Arbitrum │ Optimism │
└─────────────────────────────────────────────────────────────────┘
Data Flow
- › Generate BabyJubJub keypair
- › Encrypt values with ElGamal
- › Generate ZK proofs locally
- › Sign transactions
- › Verify ZK proofs
- › Store encrypted balances
- › Homomorphic updates
- › Emit privacy-preserving events
- › Selective disclosure keys
- › View-only access grants
- › Compliance reporting
- › Audit trail generation
Key Components
Twisted Edwards curve optimized for ZK circuits. Used for keypairs and ElGamal encryption. Parameters: a=-1, d=168696/168700, base field = BN254 scalar field.
Succinct non-interactive proofs with constant 3-element proof size (~200 bytes). Verification cost: ~200k gas. Trusted setup required per circuit.
ZK-optimized hash function with ~8x fewer constraints than SHA256/Keccak in circuits. Used for commitments, nullifiers, and Merkle trees.
Quickstart
Get up and running in 5 minutes. Generate keys, encrypt data, and create proofs.
Install the SDK
npm install @nixprotocol/sdk ethersGenerate Keypair
import { generateKeyPair } from '@nixprotocol/sdk';
// Deterministic from wallet signature (recommended)
const signature = await signer.signMessage("NixProtocol Key Derivation");
const keyPair = generateKeyPair(signature);
console.log('Public Key:', keyPair.publicKey); // [x, y] curve point
console.log('Private Key:', keyPair.privateKey); // scalarEncrypt & Decrypt
import * as elgamal from '@nixprotocol/sdk/elgamal';
// Encrypt a value
const ciphertext = elgamal.encrypt(1000n, recipientPublicKey);
// Decrypt (only recipient can do this)
const decrypted = elgamal.decrypt(ciphertext, recipientPrivateKey);
// Homomorphic addition
const sum = elgamal.add(ciphertext1, ciphertext2);Installation
NixProtocol is available as a set of NPM packages for different integration needs.
@nixprotocol/sdkMITCore SDK - encryption, decryption, ZK proof generation
@nixprotocol/stealth-walletBUSL-1.1Stealth address generation and Nix ID encoding
@nixprotocol/relayerBUSL-1.1Gasless meta-transactions via EIP-712
@nixprotocol/primitivesMITLow-level cryptographic primitives
Encryption
NixProtocol uses ElGamal encryption on the BabyJubJub curve for homomorphic operations on encrypted values.
Key Properties
- •Additively Homomorphic: Add encrypted values without decrypting
- •Probabilistic: Same plaintext encrypts differently each time
- •ZK-Friendly: Efficient to prove statements about encrypted values
Mathematical Foundation
Twisted Edwards curve defined over the BN254 scalar field:
ax² + y² = 1 + dx²y² where a = -1, d = 168696/168700For message m, public key H = sG (where s is private key, G is generator):
Decryption requires solving discrete log. We use baby-step giant-step with precomputed tables for values up to 2⁴⁰ (~1 trillion). Larger values use windowed approach.
Security Parameters
~126-bit security level (subgroup order ~2²⁵¹)
256-bit random r per encryption, CSPRNG required
4 field elements = 128 bytes per encrypted value
2⁴⁰ with precomputation, larger with performance tradeoff
Zero-Knowledge Proofs
NixProtocol uses Groth16 proofs for efficient on-chain verification. Proofs are ~200 bytes and cost ~200k gas to verify.
Registration Proof
Proves knowledge of private key corresponding to public key
Transfer Proof
Proves balance ≥ amount and correct re-encryption
Withdraw Proof
Proves encrypted balance covers withdrawal amount
Spend Proof
Proves Merkle membership without revealing which leaf
Stealth Addresses
Generate one-time addresses for receiving payments that cannot be linked to your main address.
import { generateStealthWallet, packNixId } from '@nixprotocol/stealth-wallet';
// Generate a complete stealth wallet
const wallet = generateStealthWallet();
console.log('Nix ID:', wallet.nixId);
// NIX_ABC123... (~75 chars, shareable payment address)
// Unpack to get components
const { evmAddress, bjjPublicKeyX, signBit } = unpackNixId(nixId);Compliance & Auditor Access
Privacy with accountability. Grant selective view access to auditors, regulators, or internal compliance teams.
How Auditor Access Works
Derive a separate view-only key from your master key. This key can decrypt balances but cannot sign transactions.
Share the view key with authorized auditors. Can be time-limited or revoked.
Auditors can decrypt historical balances and transaction amounts for compliance reporting.
Implementation
import { deriveViewKey, grantAuditorAccess } from '@nixprotocol/sdk';
// Generate view-only key (cannot sign, only decrypt)
const viewKey = deriveViewKey(masterKeyPair);
// Create time-limited access grant
const accessGrant = await grantAuditorAccess({
viewKey: viewKey,
auditorId: '[email protected]',
expiresAt: Date.now() + 90 * 24 * 60 * 60 * 1000, // 90 days
scope: ['balances', 'transfers'], // What they can see
});
// Auditor can now decrypt with their access grant
const auditorClient = new NixAuditorClient(accessGrant);
const report = await auditorClient.generateComplianceReport({
address: userAddress,
fromDate: '2024-01-01',
toDate: '2024-12-31',
});Access Levels
Decrypt current and historical encrypted balances
Decrypt transfer amounts between addresses
Complete visibility including linked addresses
Regulatory Compatible: Designed for institutions requiring audit trails, tax reporting, and regulatory compliance while maintaining user privacy from public view.
SDK Overview
The @nixprotocol/sdk package is the main entry point for most integrations.
Exports
generateKeyPair(seed) - Deterministic BabyJubJub keypairgenerateRandomKeyPair() - Random keypairelgamal.encrypt(value, pubKey) - Encrypt a bigintelgamal.decrypt(ciphertext, privKey) - Decrypt ciphertextelgamal.add(c1, c2) - Homomorphic additionposeidon.hash(inputs) - Poseidon hash functionKey Generation
Generate BabyJubJub keypairs for encryption and proof generation.
Important: Use deterministic key derivation from wallet signatures in production. Random keypairs require secure private key storage.
ElGamal Encryption
ElGamal encryption on BabyJubJub curve for additively homomorphic operations.
import * as elgamal from '@nixprotocol/sdk/elgamal';
// Ciphertext structure
interface Ciphertext {
C1: [bigint, bigint]; // g^r - randomness component
C2: [bigint, bigint]; // h^r * g^m - message component
}
// Encrypt
const ct = elgamal.encrypt(1000n, publicKey);
// Decrypt
const value = elgamal.decrypt(ct, privateKey); // 1000n
// Homomorphic addition
const sum = elgamal.add(ct1, ct2);
// decrypt(sum) === decrypt(ct1) + decrypt(ct2)Nix ID
Nix ID is a shareable payment identifier that encodes both EVM address and BabyJubJub public key.
Example Nix ID:
NIX_5J2K7M9P3Q6R8T1V4W7X0Z3B6C9D2F5G8H1J4K7L0M3N6P9Q2R5S8T1U4V7W0X3Y6Z9Smart Contracts Overview
NixProtocol smart contracts handle on-chain privacy operations including encrypted balances and ZK proof verification.
NixERC20
ERC-20 wrapper with encrypted balances and private transfers
NixPool
Dark pool for completely unlinkable withdrawals using Merkle trees
NixTools
Utility contracts for cryptographic operations
NixERC20
Main contract for encrypted token operations.
// Register with ZK proof (~320k gas)
function register(
uint256[2] calldata publicKey,
bytes calldata proof
) external;
// Deposit ERC20 (~570k gas)
function deposit(address token, uint256 amount) external;
// Private transfer (~950k gas)
function privateTransfer(
address token,
address to,
bytes calldata proof
) external;
// Withdraw (~800k gas)
function withdraw(address token, bytes calldata proof) external;NixPool
Dark pool pattern for completely unlinkable transactions.
// Deposit creates commitment in Merkle tree
function deposit(uint256 commitment) external payable;
// Withdraw with ZK proof - UNLINKABLE to deposit
function withdraw(
uint256 nullifierHash,
uint256 recipient,
uint256 amount,
uint256[2] calldata pA,
uint256[2][2] calldata pB,
uint256[2] calldata pC
) external;Deployed Addresses
Reference implementation live on mainnet.
Reference implementation. Security audit in progress. Do not deploy with significant value until audit complete.
Private ERC-20 Guide
Add encrypted balances to any ERC-20 token in 5 steps.
- 1Deploy NixERC20 contract pointing to your token
- 2Users register with BabyJubJub public key
- 3Deposit converts public tokens to encrypted balance
- 4Transfer moves encrypted amounts between users
- 5Withdraw converts back to public tokens
Complete Integration Example
import { ethers } from 'ethers';
import {
generateKeyPair,
generateRegistrationProof,
generateTransferProof,
elgamal
} from '@nixprotocol/sdk';
import { NixERC20__factory } from '@nixprotocol/contracts';
// Setup
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const nixERC20 = NixERC20__factory.connect(NIX_ERC20_ADDRESS, signer);
// Step 1: Generate keypair from wallet signature (deterministic)
const signature = await signer.signMessage("NixProtocol Key Derivation v1");
const keyPair = generateKeyPair(signature);
// Step 2: Register (one-time per address)
const isRegistered = await nixERC20.isRegistered(signer.address);
if (!isRegistered) {
const regProof = await generateRegistrationProof(keyPair);
const regTx = await nixERC20.register(keyPair.publicKey, regProof);
await regTx.wait();
}
// Step 3: Deposit - convert public tokens to encrypted balance
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const amount = ethers.parseUnits('100', 6); // 100 USDC
// First approve
const usdc = new ethers.Contract(USDC_ADDRESS, ['function approve(address,uint256)'], signer);
await (await usdc.approve(NIX_ERC20_ADDRESS, amount)).wait();
// Then deposit
const depositTx = await nixERC20.deposit(USDC_ADDRESS, amount);
await depositTx.wait();
// Step 4: Check encrypted balance
const encryptedBalance = await nixERC20.getEncryptedBalance(USDC_ADDRESS, signer.address);
const balance = elgamal.decrypt({
C1: [encryptedBalance[0], encryptedBalance[1]],
C2: [encryptedBalance[2], encryptedBalance[3]]
}, keyPair.privateKey);
console.log('Decrypted balance:', balance.toString()); // 100000000 (6 decimals)
// Step 5: Private transfer
const recipientPubKey = await nixERC20.getPublicKey(recipientAddress);
const transferAmount = ethers.parseUnits('25', 6);
const transferProof = await generateTransferProof({
senderKeyPair: keyPair,
recipientPublicKey: recipientPubKey,
amount: transferAmount,
currentBalance: balance,
currentCiphertext: encryptedBalance
});
const transferTx = await nixERC20.privateTransfer(
USDC_ADDRESS,
recipientAddress,
transferProof
);
await transferTx.wait();
console.log('Private transfer complete!');Gasless Transactions
Let users transact without holding native tokens. They sign, you pay gas via EIP-712 meta-transactions.
How It Works
Basic Usage
import { NixRelayer } from '@nixprotocol/relayer';
const relayer = new NixRelayer({
url: 'https://relayer.nixprotocol.com',
chainId: 1, // Ethereum mainnet
});
// User signs, relayer pays gas
const result = await relayer.sendPrivateTransfer({
signer: userSigner, // User's wallet (MetaMask, etc.)
token: USDC_ADDRESS,
recipient: recipientAddress,
proof: transferProof,
feeToken: USDC_ADDRESS, // Pay fee in USDC
maxFee: ethers.parseUnits('1', 6), // Max 1 USDC fee
});
console.log('Tx hash:', result.txHash);Self-Hosted Relayer
// server.ts - Your own relayer backend
import { NixRelayerServer } from '@nixprotocol/relayer/server';
const relayer = new NixRelayerServer({
privateKey: process.env.RELAYER_PRIVATE_KEY,
rpcUrl: process.env.RPC_URL,
supportedTokens: [USDC, USDT, DAI],
feePercentage: 0.1, // 0.1% fee
});
// Validate and execute meta-transactions
app.post('/relay', async (req, res) => {
const { signedRequest } = req.body;
// Verify signature and fee payment
const isValid = await relayer.verifyRequest(signedRequest);
if (!isValid) return res.status(400).json({ error: 'Invalid request' });
// Execute on-chain
const result = await relayer.execute(signedRequest);
res.json({ txHash: result.hash });
});Fee Structure
| Operation | Gas Cost | ~Fee (at 30 gwei) |
|---|---|---|
| Private Transfer | ~950k gas | ~$50-80 |
| Withdraw | ~800k gas | ~$40-65 |
| Register | ~320k gas | ~$15-25 |
* Fees are significantly lower on L2s (Base, Arbitrum, Optimism)
Integration Paths
Choose the right approach based on your privacy requirements.
Add Privacy to ERC-20
Wrap any ERC-20 with encrypted balances. Amounts hidden but transfers linkable.
Gasless UX
Users sign, relayer pays gas. Better UX with no native token requirement.
Full Privacy Payments
Complete anonymity with unlinkable withdrawals using NixPool dark pool pattern.
Error Handling
Common errors and how to handle them in your integration.
| Error Code | Cause | Solution |
|---|---|---|
INVALID_PROOF | ZK proof verification failed | Regenerate proof with correct inputs |
INSUFFICIENT_BALANCE | Encrypted balance less than transfer amount | Check decrypted balance before transfer |
NOT_REGISTERED | User hasn't registered public key | Call register() with valid proof first |
NULLIFIER_USED | Attempting to spend already-spent note | Use fresh commitment for new transaction |
INVALID_MERKLE_ROOT | Merkle root doesn't match on-chain state | Fetch latest root before proof generation |
DECRYPTION_FAILED | Wrong private key or corrupted ciphertext | Verify keypair matches registered public key |
Handling Errors
import { NixError, NixErrorCode } from '@nixprotocol/sdk';
try {
const tx = await nixERC20.privateTransfer(recipient, proof);
await tx.wait();
} catch (error) {
if (error instanceof NixError) {
switch (error.code) {
case NixErrorCode.INVALID_PROOF:
// Regenerate proof with fresh randomness
const newProof = await generateTransferProof(/*...*/);
break;
case NixErrorCode.INSUFFICIENT_BALANCE:
// Show user their actual balance
const balance = await getDecryptedBalance();
throw new Error(`Insufficient balance: ${balance}`);
default:
console.error('Nix error:', error.message);
}
}
throw error;
}Debugging Tips
Set NIX_DEBUG=true to log proof inputs and circuit constraints.
Most errors stem from keypair mismatch. Always verify public key matches on-chain registration.
ZK proof verification requires ~200k gas. Ensure transactions have sufficient gas limit.
Security Best Practices
Critical security considerations for production deployments.
Key Management
Use deterministic key derivation from wallet signatures. Keys should be derived on-demand, not persisted.
Request user signature on a known message, hash it, and derive BabyJubJub keypair deterministically.
Proof Generation
Every proof must use cryptographically random values. Reusing randomness leaks private information.
Generate proofs in browser/client. Never send private keys or amounts to backend servers.
Always verify proofs locally before submitting to chain to save gas on invalid proofs.
Add random delays between operations to prevent transaction graph analysis.
Smart Contract Security
- •Verify Circuit Hash: Ensure deployed verifier matches trusted setup ceremony output
- •Check Nullifier Storage: Nullifiers must be stored permanently to prevent double-spend
- •Merkle Tree Depth: Use sufficient depth (20+) to support expected user base
- •Reentrancy Guards: All state-changing functions must be protected
Reference implementation is undergoing security audit. Do not deploy with significant value until audit is complete. Contact us for enterprise deployment security review.
Full API Reference
Complete function signatures and type definitions.
@nixprotocol/sdk
// Key Generation
function generateKeyPair(seed: string | Uint8Array): KeyPair;
function generateRandomKeyPair(): KeyPair;
function deriveViewKey(keyPair: KeyPair): ViewKey;
interface KeyPair {
publicKey: [bigint, bigint]; // BJJ curve point
privateKey: bigint; // scalar
}
// ElGamal Encryption
namespace elgamal {
function encrypt(value: bigint, publicKey: [bigint, bigint]): Ciphertext;
function decrypt(ciphertext: Ciphertext, privateKey: bigint): bigint;
function add(c1: Ciphertext, c2: Ciphertext): Ciphertext;
function scalarMul(c: Ciphertext, scalar: bigint): Ciphertext;
function rerandomize(c: Ciphertext, publicKey: [bigint, bigint]): Ciphertext;
}
interface Ciphertext {
C1: [bigint, bigint];
C2: [bigint, bigint];
}
// Poseidon Hash
namespace poseidon {
function hash(inputs: bigint[]): bigint;
function hashTwo(a: bigint, b: bigint): bigint;
}
// Proof Generation
function generateRegistrationProof(keyPair: KeyPair): Promise<Proof>;
function generateTransferProof(params: TransferParams): Promise<Proof>;
function generateWithdrawProof(params: WithdrawParams): Promise<Proof>;
function verifyProof(proof: Proof, circuitType: CircuitType): boolean;
interface Proof {
pA: [bigint, bigint];
pB: [[bigint, bigint], [bigint, bigint]];
pC: [bigint, bigint];
publicSignals: bigint[];
}@nixprotocol/stealth-wallet
function generateStealthWallet(): StealthWallet;
function packNixId(components: NixIdComponents): string;
function unpackNixId(nixId: string): NixIdComponents;
function computeStealthAddress(spendingKey: KeyPair, viewingKey: KeyPair): string;
function scanForPayments(viewKey: ViewKey, fromBlock: number): Promise<Payment[]>;
interface StealthWallet {
nixId: string; // NIX_... shareable address
evmAddress: string; // 0x... stealth address
spendingKeyPair: KeyPair;
viewingKeyPair: KeyPair;
}
interface NixIdComponents {
evmAddress: string;
bjjPublicKeyX: bigint;
signBit: boolean;
}Contract ABIs
interface INixERC20 {
function register(uint256[2] calldata publicKey, bytes calldata proof) external;
function deposit(address token, uint256 amount) external;
function privateTransfer(address token, address to, bytes calldata proof) external;
function withdraw(address token, bytes calldata proof) external;
function getEncryptedBalance(address token, address user)
external view returns (uint256[4] memory ciphertext);
function isRegistered(address user) external view returns (bool);
function getPublicKey(address user) external view returns (uint256[2] memory);
}
interface INixPool {
function deposit(uint256 commitment) external payable;
function withdraw(
uint256 nullifierHash,
address recipient,
uint256 amount,
bytes calldata proof
) external;
function getMerkleRoot() external view returns (uint256);
function isSpent(uint256 nullifierHash) external view returns (bool);
}Need Help Integrating?
We offer custom development for complex privacy requirements.