Docs
/api
/Crypto

Crypto

Low-level cryptographic functions via Rust core

Cryptographic Functions

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.

Module Access

# 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
)

Commitment Generation

generate_commitment()

Generate a Pedersen commitment on BN254 G1 curve.

def generate_commitment(amount: int, secret: bytes) -> bytes

Parameters:

  • 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, H are generator points on BN254 G1 curve
  • blinding = Poseidon(secret || "NYX_BLINDING")
  • H uses nothing-up-my-sleeve construction with domain separator NYX_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 bytes

Properties:

  • 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.


Nullifier Generation

generate_nullifier()

Generate a circuit-safe nullifier using two-step Poseidon hashing.

def generate_nullifier(secret: bytes, leaf_index: int) -> bytes

Parameters:

  • secret - 32-byte secret key
  • leaf_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_key is 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 init constraint prevents double-spending
  • Once created, nullifier PDAs are permanent (cannot be deleted)

Use cases:

  • Custom note spending logic
  • Advanced privacy protocols
  • Testing nullifier derivation

zkSNARK Proof Generation

generate_proof()

Generate a Groth16 zkSNARK proof from witness data.

def generate_proof(witness_json: str) -> bytes

Parameters:

  • witness_json - JSON string containing:
    • secret - Secret key (32 bytes hex)
    • amount - Transaction amount
    • merkle_path - List of 20 sibling hashes (Merkle proof)
    • leaf_index - Leaf index in Merkle tree
    • recipient_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 bytes

Example:

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 bytes
  • amount - 8 bytes
  • blinding - 32 bytes
  • merkle_path - 20 × 32 bytes
  • leaf_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.


zkSNARK Proof Verification

verify_proof()

Verify a Groth16 zkSNARK proof against public inputs.

def verify_proof(proof: bytes, public_inputs_json: str) -> bool

Parameters:

  • proof - 256-byte Groth16 proof
  • public_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:

  1. Parse proof into (A, B, C) G1/G2 points
  2. Parse public inputs from JSON
  3. Compute pairing check: e(A, B) = e(α, β) * e(L, γ) * e(C, δ)
  4. Return True if 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-solana crate
  • 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 Function

poseidon_hash()

Compute Poseidon hash optimized for zkSNARK circuits.

def poseidon_hash(inputs: list[bytes]) -> bytes

Parameters:

  • 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, then nullifier)
  • Commitment blinding: blinding = Poseidon(secret || domain_separator)

Comparison to other hashes:

Hash FunctionConstraintsSecurityUse Case
Poseidon~200128-bitzkSNARK circuits
SHA-256~20,000128-bitGeneral-purpose
Keccak-256~25,000128-bitEthereum

Use cases:

  • Custom Merkle tree implementations
  • Hash-based commitments
  • Advanced cryptographic protocols
  • Testing Poseidon properties

Utility Functions

generate_secret()

Generate a cryptographically secure random secret.

def generate_secret(length: int = 32) -> str

Parameters:

  • 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 characters

Entropy 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) -> bytes

Example:

from veil import hex_to_bytes
 
# With 0x prefix
bytes1 = hex_to_bytes("0xabcd1234")
 
# Without prefix
bytes2 = hex_to_bytes("abcd1234")
 
assert bytes1 == bytes2

commitment_to_hex()

Convert 32-byte commitment to hex string.

def commitment_to_hex(commitment: bytes) -> str

Example:

from veil import commitment_to_hex
 
commitment = b'\xab\xcd...'  # 32 bytes
hex_str = commitment_to_hex(commitment)
print(hex_str)  # "abcd..."

Advanced Example: Custom Privacy Protocol

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")

High-Level vs Low-Level APIs

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 automatically

When 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

Performance Benchmarks

OperationTime (avg)MemoryNotes
generate_commitment()<1msMinimalBN254 G1 scalar multiplication
generate_nullifier()~5msMinimal2× Poseidon hash
poseidon_hash()~2msMinimalt=3, RF=8, RP=57
generate_proof()5-10s2-4 GB~7,000 R1CS constraints
verify_proof()<10ms<100 MBBN254 pairing check

Hardware: Intel i7 / Apple M1 equivalent


Error Handling

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

Security Considerations

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

Technical Specifications

Cryptographic Primitives

PrimitiveSpecification
CurveBN254 (alt_bn128)
FieldFr (scalar field, ~254 bits)
GroupG1 (32-byte compressed points)
HashPoseidon (t=3, RF=8, RP=57)
CommitmentPedersen (C = aG + bH)
Proof SystemGroth16
Security~128 bits

Proof Details

ComponentSizeDescription
proof_a64 bytesG1 point (compressed)
proof_b128 bytesG2 point (compressed)
proof_c64 bytesG1 point (compressed)
Total256 bytesComplete proof

Circuit Complexity

ComponentConstraintsPercentage
Spending key~4006%
Input commitment~5007%
Merkle proof (20 levels)~4,00057%
Nullifier~4006%
Output commitment~5007%
Validation~1,20017%
Total~7,000100%

See Also