Crypto
Low-level cryptographic functions via Rust core
Low-level cryptographic primitives exposed via PyO3 bindings to the Rust core. These functions power the high-level VeilClient API but can also be used directly for advanced use cases.
⚠️ Warning: These are low-level functions. Most users should use the high-level VeilClient API instead. Use these functions only if you understand the cryptographic implications.
# Low-level Rust core (advanced users only)
from veil._rust_core import (
generate_commitment,
generate_nullifier,
generate_proof,
verify_proof,
poseidon_hash
)
# High-level utilities (recommended for most users)
from veil import (
generate_secret,
hex_to_bytes,
commitment_to_hex
)generate_commitment()
Generate a Pedersen commitment on BN254 G1 curve.
def generate_commitment(amount: int, secret: bytes) -> bytesParameters:
amount- Amount to commit (in lamports, 8 bytes)secret- 32-byte secret key (used to derive blinding factor)
Returns: 32-byte Pedersen commitment
Formula:
C = amount * G + blinding * H
Where:
G,Hare generator points on BN254 G1 curveblinding = Poseidon(secret || "NYX_BLINDING")Huses nothing-up-my-sleeve construction with domain separatorNYX_PROTOCOL_PEDERSEN_H_V1
Example:
from veil._rust_core import generate_commitment
from veil import generate_secret, hex_to_bytes
# Generate secret
secret_hex = generate_secret() # 64 hex chars (32 bytes)
secret_bytes = hex_to_bytes(secret_hex)
# Generate commitment
commitment = generate_commitment(
amount=1_000_000_000, # 1 SOL in lamports
secret=secret_bytes
)
print(f"Commitment: {commitment.hex()}")
print(f"Size: {len(commitment)} bytes") # 32 bytesProperties:
- Perfectly hiding: Cannot determine amount from commitment alone (information-theoretic)
- Computationally binding: Cannot find two different amounts with same commitment (under DLP assumption)
- Homomorphic:
C(a1 + a2) ≠ C(a1) + C(a2)due to different blinding factors - Security level: ~128 bits (BN254 discrete log hardness)
Use cases:
- Custom commitment schemes
- Advanced cryptographic protocols
- Testing and debugging
⚠️ Security Warning: The blinding factor is derived from the secret, not randomly generated. This is intentional for deterministic commitment generation but means the same secret+amount always produces the same commitment.
generate_nullifier()
Generate a circuit-safe nullifier using two-step Poseidon hashing.
def generate_nullifier(secret: bytes, leaf_index: int) -> bytesParameters:
secret- 32-byte secret keyleaf_index- Merkle tree leaf index (0 to ~1M)
Returns: 32-byte nullifier
Derivation (two-step circuit-safe):
Step 1: spending_key = Poseidon(secret || "NYX_SPENDING_KEY")
Step 2: nullifier = Poseidon(spending_key || Hash(leaf_index || "NYX_NULLIFIER"))
Why two steps?
- Prevents secret leakage in zkSNARK circuits
spending_keyis computed once and reused- Circuit only needs to verify
nullifier = Poseidon(spending_key, index_hash) - Reduces constraint count by ~500 constraints
Example:
from veil._rust_core import generate_nullifier
from veil import hex_to_bytes
secret = hex_to_bytes("a1b2c3...") # 32 bytes
leaf_index = 42
nullifier = generate_nullifier(secret, leaf_index)
print(f"Nullifier: {nullifier.hex()}")
print(f"Size: {len(nullifier)} bytes") # 32 bytes
# Nullifiers are unlinkable
nullifier2 = generate_nullifier(secret, 43)
assert nullifier != nullifier2
print("Different leaf indices produce different nullifiers")Properties:
- Unlinkable: Cannot link nullifier to commitment without secret
- Deterministic: Same secret + leaf index → same nullifier
- Collision-resistant: Infeasible to find two inputs with same nullifier (Poseidon security)
- Circuit-friendly: ~400 constraints in zkSNARK (two-step optimization)
On-chain usage:
- Nullifiers are stored as PDAs with seeds:
[b"nullifier", pool_id, nullifier_bytes] - Anchor's
initconstraint prevents double-spending - Once created, nullifier PDAs are permanent (cannot be deleted)
Use cases:
- Custom note spending logic
- Advanced privacy protocols
- Testing nullifier derivation
generate_proof()
Generate a Groth16 zkSNARK proof from witness data.
def generate_proof(witness_json: str) -> bytesParameters:
witness_json- JSON string containing:secret- Secret key (32 bytes hex)amount- Transaction amountmerkle_path- List of 20 sibling hashes (Merkle proof)leaf_index- Leaf index in Merkle treerecipient_pubkey- Recipient's public key (optional)- Other circuit-specific inputs
Returns: 256-byte Groth16 proof (proof_a: 64, proof_b: 128, proof_c: 64)
Proof Structure:
proof_bytes = proof_a || proof_b || proof_c
proof_a: 64 bytes (G1 point, compressed)
proof_b: 128 bytes (G2 point, compressed)
proof_c: 64 bytes (G1 point, compressed)
Total: 256 bytesExample:
import json
from veil._rust_core import generate_proof
# Prepare witness data
witness = {
"secret": "a1b2c3...", # 64 hex chars
"amount": 1_000_000_000,
"merkle_path": [
"hash1...",
"hash2...",
# ... 20 sibling hashes total
],
"leaf_index": 42,
"recipient_pubkey": "RecipientPublicKey..."
}
witness_json = json.dumps(witness)
# Generate proof (5-10 seconds on modern CPU)
import time
start = time.time()
proof = generate_proof(witness_json)
elapsed = time.time() - start
print(f"Proof generated in {elapsed:.1f} seconds")
print(f"Proof size: {len(proof)} bytes") # 256 bytes
print(f"Proof: {proof.hex()[:32]}...")Circuit Details:
- Total constraints: ~7,000 R1CS constraints
- Constraint breakdown:
- Spending key derivation: ~400 constraints
- Input commitment: ~500 constraints
- Merkle proof verification: ~4,000 constraints (20 levels × ~200/hash)
- Nullifier derivation: ~400 constraints
- Output commitment: ~500 constraints
- Validation logic: ~1,200 constraints
Public Inputs (96 bytes total):
merkle_root- 32 bytes (current tree root)nullifier- 32 bytes (proves note is spent)new_commitment- 32 bytes (new note for recipient)
Private Inputs (not revealed):
secret- 32 bytesamount- 8 bytesblinding- 32 bytesmerkle_path- 20 × 32 bytesleaf_index- 8 bytes
Performance:
- Generation time: 5-10 seconds (depends on CPU, ~7k constraints)
- Memory usage: 2-4 GB RAM
- Hardware requirements: Modern CPU (x86_64 or ARM64)
Use cases:
- Custom proof generation workflows
- Batch proof generation
- Air-gapped signing
- Advanced privacy protocols
⚠️ Security Warning: Proof generation requires the secret key. Never generate proofs on untrusted hardware or send the witness to third parties.
verify_proof()
Verify a Groth16 zkSNARK proof against public inputs.
def verify_proof(proof: bytes, public_inputs_json: str) -> boolParameters:
proof- 256-byte Groth16 proofpublic_inputs_json- JSON string containing:merkle_root- 32-byte Merkle root (hex)nullifier- 32-byte nullifier (hex)new_commitment- 32-byte new commitment (hex)
Returns: True if proof is valid, False otherwise
Example:
import json
from veil._rust_core import verify_proof
# Public inputs
public_inputs = {
"merkle_root": "abc123...", # 64 hex chars (32 bytes)
"nullifier": "def456...", # 64 hex chars
"new_commitment": "789ghi..." # 64 hex chars
}
public_inputs_json = json.dumps(public_inputs)
# Verify proof (<10ms on modern CPU)
is_valid = verify_proof(proof, public_inputs_json)
if is_valid:
print("✅ Proof is valid")
else:
print("❌ Proof is invalid")Verification Process:
- Parse proof into (A, B, C) G1/G2 points
- Parse public inputs from JSON
- Compute pairing check:
e(A, B) = e(α, β) * e(L, γ) * e(C, δ) - Return
Trueif pairing equality holds
Performance:
- Verification time: <10ms (local), ~200k CU (on-chain)
- Memory usage: Minimal (<100 MB)
- Hardware requirements: Any modern CPU
On-chain verification:
- Uses
groth16-solanacrate - BN254 pairing precompile (if available)
- Endianness conversion: LE (Solana) ↔ BE (arkworks)
- Cost: ~200,000 compute units
Use cases:
- Local proof validation before submission
- Testing proof generation
- Auditing transactions
- Custom verification logic
poseidon_hash()
Compute Poseidon hash optimized for zkSNARK circuits.
def poseidon_hash(inputs: list[bytes]) -> bytesParameters:
inputs- List of byte arrays to hash (each 32 bytes)
Returns: 32-byte Poseidon hash
Poseidon Parameters:
- Configuration: t=3 (3-to-1 hash), width=3
- Rounds: RF=8 (full rounds), RP=57 (partial rounds)
- S-box: x^5 (power of 5)
- Field: BN254 scalar field (Fr)
- Constraints: ~200 per hash (circuit-friendly)
Example:
from veil._rust_core import poseidon_hash
# Hash single input
input1 = b'\x01' * 32 # 32-byte input
hash1 = poseidon_hash([input1])
print(f"Hash: {hash1.hex()}")
# Hash multiple inputs (3-to-1)
input2 = b'\x02' * 32
input3 = b'\x03' * 32
hash2 = poseidon_hash([input1, input2, input3])
print(f"Combined hash: {hash2.hex()}")
# Variable-length hashing (sponge construction)
many_inputs = [b'\x00' * 32 for _ in range(10)]
hash3 = poseidon_hash(many_inputs)
print(f"10-input hash: {hash3.hex()}")Properties:
- Collision-resistant: Infeasible to find two inputs with same hash
- One-way: Cannot reverse hash to find input
- Circuit-friendly: ~200 constraints per hash (vs ~20,000 for SHA-256)
- Deterministic: Same inputs → same hash
Usage in Veil:
- Merkle tree: Poseidon hash for tree nodes (
hash(left || right)) - Nullifier derivation: Two-step Poseidon (
spending_key, thennullifier) - Commitment blinding:
blinding = Poseidon(secret || domain_separator)
Comparison to other hashes:
| Hash Function | Constraints | Security | Use Case |
|---|---|---|---|
| Poseidon | ~200 | 128-bit | zkSNARK circuits |
| SHA-256 | ~20,000 | 128-bit | General-purpose |
| Keccak-256 | ~25,000 | 128-bit | Ethereum |
Use cases:
- Custom Merkle tree implementations
- Hash-based commitments
- Advanced cryptographic protocols
- Testing Poseidon properties
generate_secret()
Generate a cryptographically secure random secret.
def generate_secret(length: int = 32) -> strParameters:
length- Length in bytes (default: 32)
Returns: Hex-encoded secret string (2 × length characters)
Example:
from veil import generate_secret
# Generate 32-byte secret (64 hex characters)
secret = generate_secret()
print(f"Secret: {secret}")
print(f"Length: {len(secret)} chars") # 64
# Generate custom length
secret_16 = generate_secret(length=16)
print(f"16-byte secret: {secret_16}") # 32 hex charactersEntropy source: OS random number generator (secrets.SystemRandom() in Python, OsRng in Rust)
Security: Cryptographically secure (suitable for keys, nonces, secrets)
hex_to_bytes()
Convert hex string to bytes (handles "0x" prefix).
def hex_to_bytes(hex_str: str) -> bytesExample:
from veil import hex_to_bytes
# With 0x prefix
bytes1 = hex_to_bytes("0xabcd1234")
# Without prefix
bytes2 = hex_to_bytes("abcd1234")
assert bytes1 == bytes2commitment_to_hex()
Convert 32-byte commitment to hex string.
def commitment_to_hex(commitment: bytes) -> strExample:
from veil import commitment_to_hex
commitment = b'\xab\xcd...' # 32 bytes
hex_str = commitment_to_hex(commitment)
print(hex_str) # "abcd..."Full example using low-level functions to build a custom privacy workflow.
import json
import time
from veil._rust_core import (
generate_commitment,
generate_nullifier,
generate_proof,
verify_proof,
poseidon_hash
)
from veil import generate_secret, hex_to_bytes
# Step 1: Generate commitment
print("Step 1: Generating commitment...")
secret = hex_to_bytes(generate_secret())
amount = 1_000_000_000 # 1 SOL
commitment = generate_commitment(amount, secret)
print(f"Commitment: {commitment.hex()}")
# Step 2: Simulate Merkle tree
print("\nStep 2: Building Merkle path...")
leaf_index = 42
# Generate fake merkle path (in production, fetch from chain)
merkle_path = []
for i in range(20):
sibling = poseidon_hash([commitment, i.to_bytes(32, 'little')])
merkle_path.append(sibling.hex())
# Compute merkle root
current = commitment
for i, sibling_hex in enumerate(merkle_path):
sibling = hex_to_bytes(sibling_hex)
if (leaf_index >> i) & 1:
current = poseidon_hash([sibling, current])
else:
current = poseidon_hash([current, sibling])
merkle_root = current
print(f"Merkle root: {merkle_root.hex()}")
# Step 3: Generate nullifier
print("\nStep 3: Generating nullifier...")
nullifier = generate_nullifier(secret, leaf_index)
print(f"Nullifier: {nullifier.hex()}")
# Step 4: Generate new commitment for recipient
print("\nStep 4: Generating recipient commitment...")
recipient_secret = hex_to_bytes(generate_secret())
recipient_amount = 500_000_000 # 0.5 SOL
new_commitment = generate_commitment(recipient_amount, recipient_secret)
print(f"New commitment: {new_commitment.hex()}")
# Step 5: Generate zkSNARK proof
print("\nStep 5: Generating zkSNARK proof...")
witness = {
"secret": secret.hex(),
"amount": amount,
"merkle_path": merkle_path,
"leaf_index": leaf_index,
"recipient_pubkey": "11111111111111111111111111111111"
}
start = time.time()
proof = generate_proof(json.dumps(witness))
elapsed = time.time() - start
print(f"Proof generated in {elapsed:.1f} seconds")
print(f"Proof size: {len(proof)} bytes")
# Step 6: Verify proof
print("\nStep 6: Verifying proof...")
public_inputs = {
"merkle_root": merkle_root.hex(),
"nullifier": nullifier.hex(),
"new_commitment": new_commitment.hex()
}
is_valid = verify_proof(proof, json.dumps(public_inputs))
print(f"Proof valid: {is_valid}")
if is_valid:
print("\n✅ Complete custom privacy protocol successful!")
else:
print("\n❌ Proof verification failed")When to use VeilClient (high-level):
✅ Recommended for most users
- Automatic Merkle proof fetching
- Built-in RPC communication
- Async/await blockchain submission
- Error handling and retries
- Note encryption/decryption
- Relayer integration
from veil import VeilClient
async def use_high_level():
client = VeilClient()
tx = await client.shield_async(
amount=1_000_000_000,
token="SOL",
keypair=payer,
secret=secret
)
# Everything handled automaticallyWhen to use low-level functions:
⚠️ Advanced users only
- Custom proof generation workflows
- Air-gapped signing (no network)
- Batch processing
- Custom Merkle tree implementations
- Research and testing
- Building higher-level abstractions
from veil._rust_core import generate_commitment, generate_proof
def use_low_level():
# Full manual control
commitment = generate_commitment(amount, secret)
# ... build witness manually
proof = generate_proof(witness_json)
# ... submit manually| Operation | Time (avg) | Memory | Notes |
|---|---|---|---|
generate_commitment() | <1ms | Minimal | BN254 G1 scalar multiplication |
generate_nullifier() | ~5ms | Minimal | 2× Poseidon hash |
poseidon_hash() | ~2ms | Minimal | t=3, RF=8, RP=57 |
generate_proof() | 5-10s | 2-4 GB | ~7,000 R1CS constraints |
verify_proof() | <10ms | <100 MB | BN254 pairing check |
Hardware: Intel i7 / Apple M1 equivalent
All low-level functions may raise:
ValueError
Raised for invalid input parameters.
from veil._rust_core import generate_commitment
try:
# Invalid: secret must be 32 bytes
commitment = generate_commitment(1000, b"short_secret")
except ValueError as e:
print(f"Invalid input: {e}")
# Output: "Secret must be exactly 32 bytes"Common causes:
- Secret wrong length (must be 32 bytes)
- Invalid JSON format (proof generation)
- Out-of-range values
RuntimeError
Raised for cryptographic operation failures.
from veil._rust_core import verify_proof
try:
is_valid = verify_proof(corrupted_proof, public_inputs_json)
except RuntimeError as e:
print(f"Crypto operation failed: {e}")
# Examples:
# - "Failed to deserialize proof"
# - "Pairing check failed"
# - "Invalid curve point"Common causes:
- Corrupted proof data
- Invalid curve points
- Malformed JSON
- Arithmetic errors in field operations
Secret Management
✅ DO:
- Generate secrets using
generate_secret()(cryptographically secure) - Store secrets encrypted at rest
- Use hardware wallets when possible
- Never log or print secrets in production
❌ DON'T:
- Reuse secrets across different notes
- Store secrets in plain text
- Send secrets over unencrypted channels
- Generate secrets with weak RNGs (e.g.,
random.random())
Proof Generation
✅ DO:
- Generate proofs on trusted, air-gapped hardware when possible
- Verify proofs locally before submitting
- Use fresh Merkle roots (within 30-root history)
- Validate all inputs before proof generation
❌ DON'T:
- Generate proofs on untrusted hardware
- Share witness data with third parties
- Reuse nullifiers (double-spend attempt)
- Submit proofs with stale Merkle roots
Side-Channel Attacks
Potential risks:
- Timing attacks: Proof generation time may leak information
- Memory dumps: Secrets in RAM could be recovered
- Power analysis: Hardware side-channels during crypto operations
Mitigations:
- Use constant-time implementations (Rust core)
- Clear sensitive memory after use
- Use hardware security modules (HSMs) for production
- Avoid proof generation on shared/cloud infrastructure
Cryptographic Primitives
| Primitive | Specification |
|---|---|
| Curve | BN254 (alt_bn128) |
| Field | Fr (scalar field, ~254 bits) |
| Group | G1 (32-byte compressed points) |
| Hash | Poseidon (t=3, RF=8, RP=57) |
| Commitment | Pedersen (C = aG + bH) |
| Proof System | Groth16 |
| Security | ~128 bits |
Proof Details
| Component | Size | Description |
|---|---|---|
proof_a | 64 bytes | G1 point (compressed) |
proof_b | 128 bytes | G2 point (compressed) |
proof_c | 64 bytes | G1 point (compressed) |
| Total | 256 bytes | Complete proof |
Circuit Complexity
| Component | Constraints | Percentage |
|---|---|---|
| Spending key | ~400 | 6% |
| Input commitment | ~500 | 7% |
| Merkle proof (20 levels) | ~4,000 | 57% |
| Nullifier | ~400 | 6% |
| Output commitment | ~500 | 7% |
| Validation | ~1,200 | 17% |
| Total | ~7,000 | 100% |
- VeilClient - High-level SDK API (recommended for most users)
- Types - Data structures used in crypto functions
- Concepts: zkSNARKs - Deep dive into Groth16 proofs
- Concepts: Commitments - Pedersen commitments explained
- Concepts: Nullifiers - Circuit-safe nullifier derivation
On This Page
- Cryptographic Functions
- Module Access
- Commitment Generation
- generate_commitment()
- Nullifier Generation
- generate_nullifier()
- zkSNARK Proof Generation
- generate_proof()
- zkSNARK Proof Verification
- verify_proof()
- Poseidon Hash Function
- poseidon_hash()
- Utility Functions
- generate_secret()
- hex_to_bytes()
- commitment_to_hex()
- Advanced Example: Custom Privacy Protocol
- High-Level vs Low-Level APIs
- When to use VeilClient (high-level):
- When to use low-level functions:
- Performance Benchmarks
- Error Handling
- ValueError
- RuntimeError
- Security Considerations
- Secret Management
- Proof Generation
- Side-Channel Attacks
- Technical Specifications
- Cryptographic Primitives
- Proof Details
- Circuit Complexity
- See Also