Docs
/getting started
/Quick Start

Quick Start

Your first private transaction in 5 minutes

Quick Start

This guide walks you through the core operations of Veil: shielding assets, transferring privately, and unshielding. You'll learn both async (blockchain submission) and sync (offline) modes.

Prerequisites

pip install veil-solana

Example 1: Shield Assets (Make Private)

Convert public SOL into a private commitment.

import asyncio
from veil import VeilClient, generate_secret
from solders.keypair import Keypair
 
async def shield_example():
    # Initialize client
    client = VeilClient(
        rpc_url="https://api.devnet.solana.com"
    )
 
    # Generate or load your keypair
    payer = Keypair()  # In production: Keypair.from_bytes(your_secret)
 
    # Generate a secret for your commitment (SAVE THIS!)
    secret = generate_secret()
    print(f"Generated secret: {secret}")
    print("⚠️  Store this secret securely - you need it to spend!")
 
    # Shield 1 SOL - deposit into privacy pool
    tx = await client.shield_async(
        amount=1_000_000_000,  # 1 SOL in lamports
        token="SOL",
        keypair=payer,
        secret=secret  # Optional: auto-generated if None
    )
 
    print(f"\n✅ Shield Complete!")
    print(f"Transaction: {tx.signature}")
    print(f"Commitment: {tx.commitment[:16]}...")
    print(f"Leaf Index: {tx.leaf_index}")  # Save this for spending
 
    # Save note data locally
    note_data = {
        "commitment": tx.commitment,
        "amount": 1_000_000_000,
        "secret": secret,
        "leaf_index": tx.leaf_index,
        "asset_id": 0  # SOL
    }
    # In production: save_to_database(note_data)
 
    await client.close()
    return note_data
 
# Run the example
note = asyncio.run(shield_example())

What happens:

  1. Your public SOL is transferred to the privacy pool
  2. A Pedersen commitment C = amount * G + blinding * H is created
  3. The commitment is added to the on-chain Merkle tree
  4. On-chain: Only the commitment (32 bytes) is visible
  5. Private: Amount and secret remain local

Offline Mode (no blockchain submission):

# Generate commitment data without submitting
tx = client.shield(
    amount=1_000_000_000,
    token="SOL",
    secret=secret
)
# Returns: PrivateTransaction with status=PENDING
# Useful for: air-gapped signing, testing, proof generation

Example 2: Private Transfer

Send privately to another user. Note: Proof generation takes 5-10 seconds.

async def private_transfer_example(sender_note):
    client = VeilClient(rpc_url="https://api.devnet.solana.com")
    payer = Keypair()  # Transaction payer
 
    # Recipient's public key (they share this like an address)
    recipient_pubkey = "RecipientPublicKeyHere..."
 
    print("⏳ Generating zkSNARK proof (5-10 seconds)...")
    start_time = time.time()
 
    # Private transfer - proves you own the note without revealing which one
    tx = await client.private_transfer_async(
        input_note_secret=sender_note["secret"],
        input_note_index=sender_note["leaf_index"],
        recipient_pubkey=recipient_pubkey,
        amount=500_000_000,  # 0.5 SOL
        keypair=payer
    )
 
    elapsed = time.time() - start_time
    print(f"✅ Proof generated in {elapsed:.1f} seconds")
 
    print(f"\n✅ Private Transfer Complete!")
    print(f"Transaction: {tx.signature}")
    print(f"Nullifier: {tx.nullifier[:16]}... (prevents double-spend)")
    print(f"New Commitment: {tx.commitment[:16]}... (for recipient)")
    print(f"Recipient Secret: {tx.recipient_secret[:16]}...")
 
    # Give recipient_secret to the recipient (encrypted channel)
    # They need it to spend the funds
 
    await client.close()
    return tx
 
# Run the example
# transfer_tx = asyncio.run(private_transfer_example(note))

What happens:

  1. Client-side (5-10 seconds):
    • Fetches Merkle proof for your note
    • Generates zkSNARK proof (~7,000 constraints)
    • Encrypts note data for recipient (ECDH)
  2. On-chain (~3 seconds):
    • Verifies zkSNARK proof (~200k CU)
    • Checks nullifier hasn't been spent
    • Creates nullifier PDA (marks note as spent)
    • Adds new commitment to Merkle tree

On-chain visibility:

  • ✅ Nullifier (32 bytes, random-looking)
  • ✅ New commitment (32 bytes, random-looking)
  • ✅ zkSNARK proof (256 bytes)
  • ✅ Encrypted note (96 bytes)

Hidden:

  • ❌ Sender identity
  • ❌ Recipient identity
  • ❌ Amount transferred

Common Pitfall:

# ⚠️ DON'T reuse the same note twice!
tx1 = await client.private_transfer_async(...)  # OK
tx2 = await client.private_transfer_async(...)  # ERROR: Nullifier already spent

Example 3: Unshield (Withdraw to Public)

Withdraw from the privacy pool to a public Solana address.

async def unshield_example(sender_note):
    client = VeilClient(rpc_url="https://api.devnet.solana.com")
    payer = Keypair()
 
    # Destination public address
    destination = str(payer.pubkey())
 
    print("⏳ Generating proof and withdrawing...")
 
    # Unshield - withdraw to public account
    tx = await client.unshield_async(
        note_secret=sender_note["secret"],
        note_index=sender_note["leaf_index"],
        amount=500_000_000,  # 0.5 SOL
        recipient_pubkey=destination,
        keypair=payer
    )
 
    print(f"\n✅ Unshield Complete!")
    print(f"Transaction: {tx.signature}")
    print(f"Amount withdrawn: 0.5 SOL to {destination[:16]}...")
    print(f"Nullifier: {tx.nullifier[:16]}... (marks note as spent)")
 
    await client.close()
    return tx
 
# Run the example
# unshield_tx = asyncio.run(unshield_example(note))

Privacy trade-offs:

  • Hidden: Sender identity (who owned the note)
  • Hidden: Original deposit (can't link shield → unshield)
  • Visible: Withdrawal amount (0.5 SOL)
  • Visible: Recipient address

When to use:

  • Converting private funds back to public
  • Paying to merchants/services requiring public addresses
  • Cashing out to exchanges

When NOT to use:

  • Keep funds private → use private_transfer instead
  • Minimize graph analysis → wait for larger anonymity set

Example 4: Using Relayers (IP Privacy)

Submit transactions through relayers to hide your IP address.

async def relayer_example(sender_note):
    from veil import VeilClient, RelayerClient
 
    # Initialize both clients
    veil_client = VeilClient(rpc_url="https://api.devnet.solana.com")
    relayer_client = RelayerClient()
 
    # Add relayers (public network in v0.2.0)
    relayer_client.add_relayer("https://relayer1.veil.network", priority=1)
    relayer_client.add_relayer("https://relayer2.veil.network", priority=2)
 
    # Or use default relayers
    relayer_client.add_default_relayers()
 
    # Generate proof locally (5-10 seconds)
    print("⏳ Generating proof locally...")
    proof = veil_client.generate_transfer_proof(
        input_note_secret=sender_note["secret"],
        input_note_index=sender_note["leaf_index"],
        recipient_pubkey="RecipientHere...",
        amount=500_000_000
    )
 
    # Estimate relayer fee
    fee_estimate = relayer_client.estimate_fee(proof)
    print(f"Relayer fee: {fee_estimate.fee / 1e9:.4f} SOL ({fee_estimate.rate * 100}%)")
 
    # Submit via relayer (IP hidden)
    result = relayer_client.submit_private_transfer(
        proof=proof,
        max_fee=fee_estimate.fee,
        strategy="balanced"  # or "lowest_fee", "fastest", "random"
    )
 
    print(f"\n✅ Submitted via relayer!")
    print(f"Transaction: {result.transaction_signature}")
    print(f"Relayer used: {result.relayer_url}")
    print(f"Fee paid: {result.fee_paid / 1e9:.4f} SOL")
 
    await veil_client.close()
    return result
 
# Run the example
# relayer_tx = asyncio.run(relayer_example(note))

Benefits:

  • ✅ Your IP address is NOT exposed to Solana RPC
  • ✅ Relayer submits from their infrastructure
  • ✅ Multiple relayers available (censorship resistant)
  • ✅ Fee market ensures competitive pricing

Cost:

  • Default: 0.3% (30 basis points)
  • Maximum: 5.0% (500 basis points)
  • Example: 1 SOL transfer = 0.003 SOL fee

Security:

  • ✅ Relayers CANNOT steal funds (you sign the transaction)
  • ✅ Relayers CANNOT modify transaction (would invalidate proof)
  • ❌ Relayers CAN see transaction metadata (but not amounts/identities)

For maximum privacy:

# Connect via Tor/VPN when contacting relayer
# This hides IP from relayer AND blockchain

Status: Relayer protocol implemented. Public relayer network launching in v0.2.0.


Example 5: Note Discovery (Recipient Scanning)

How recipients find their incoming transfers.

async def scan_for_notes_example():
    client = VeilClient(rpc_url="https://api.devnet.solana.com")
 
    # Recipient's secret key for decryption
    my_secret_key = "your_secret_key_here"
 
    # Track last scanned block to avoid re-scanning
    last_scanned_slot = load_last_scanned_slot()  # From database
    current_slot = await client.get_current_slot()
 
    print(f"Scanning blocks {last_scanned_slot} to {current_slot}...")
 
    # Scan for notes sent to you
    discovered_notes = await client.scan_notes(
        secret_key=my_secret_key,
        start_slot=last_scanned_slot,
        end_slot=current_slot
    )
 
    print(f"\n✅ Found {len(discovered_notes)} new notes!")
 
    for note in discovered_notes:
        print(f"\n📬 Received Note:")
        print(f"  Amount: {note.amount / 1e9} SOL")
        print(f"  Commitment: {note.commitment[:16]}...")
        print(f"  Leaf Index: {note.leaf_index}")
        print(f"  Asset ID: {note.asset_id}")
 
        # Save to local database
        save_note_to_database(note)
 
    # Update last scanned slot
    save_last_scanned_slot(current_slot)
 
    await client.close()
    return discovered_notes
 
# Run the example
# my_notes = asyncio.run(scan_for_notes_example())

How it works:

  1. Fetch recent transactions to the privacy program
  2. Extract encrypted notes from transaction logs
  3. Trial decryption: Try to decrypt each note with your secret key
  4. If decryption succeeds → note is yours!
  5. If decryption fails → note belongs to someone else

Performance:

  • 1,000 notes: ~5 seconds
  • 10,000 notes: ~50 seconds
  • Can be parallelized across CPU cores

Optimization strategies:

  1. Incremental scanning: Only scan new blocks
  2. Bloom filters: Skip unlikely notes (99% reduction)
  3. Batch processing: Scan multiple blocks at once
  4. Light clients: Delegate scanning to a server (privacy trade-off)

Current Implementation:

# Manual tracking required (for now)
# Save discovered notes to local database
# Track which notes you've spent vs unspent
 
notes_db = {
    "unspent": [note1, note2],
    "spent": [note3],
    "last_scanned": 12345678
}

Future: Automated note tracking with encrypted local database.


Complete Workflow Example

Putting it all together: shield → transfer → unshield with error handling.

async def complete_workflow():
    client = VeilClient(rpc_url="https://api.devnet.solana.com")
    payer = Keypair()
 
    try:
        # 1. Shield 2 SOL
        print("Step 1: Shielding 2 SOL...")
        secret = generate_secret()
 
        shield_tx = await client.shield_async(
            amount=2_000_000_000,
            token="SOL",
            keypair=payer,
            secret=secret
        )
        print(f"✅ Shielded: {shield_tx.signature}")
 
        # 2. Private transfer 1 SOL to recipient
        print("\nStep 2: Private transfer 1 SOL...")
        recipient = "RecipientPublicKeyHere..."
 
        transfer_tx = await client.private_transfer_async(
            input_note_secret=secret,
            input_note_index=shield_tx.leaf_index,
            recipient_pubkey=recipient,
            amount=1_000_000_000,
            keypair=payer
        )
        print(f"✅ Transferred: {transfer_tx.signature}")
 
        # 3. Unshield remaining 1 SOL
        print("\nStep 3: Unshielding 1 SOL...")
        # Note: Use the RECIPIENT's secret from transfer_tx
        # not the original secret (that note is spent)
 
        unshield_tx = await client.unshield_async(
            note_secret=transfer_tx.recipient_secret,  # New note's secret
            note_index=transfer_tx.leaf_index,          # New note's index
            amount=1_000_000_000,
            recipient_pubkey=str(payer.pubkey()),
            keypair=payer
        )
        print(f"✅ Unshielded: {unshield_tx.signature}")
 
        print("\n🎉 Complete workflow successful!")
 
    except Exception as e:
        print(f"❌ Error: {e}")
        # Handle errors appropriately
 
    finally:
        await client.close()
 
# Run the complete workflow
asyncio.run(complete_workflow())

Common Errors:

  1. "Nullifier already spent"

    • You're trying to spend a note twice
    • Solution: Track which notes you've spent
  2. "Invalid Merkle root"

    • Your proof is for an old Merkle root (>30 updates ago)
    • Solution: Regenerate proof with current root
  3. "Proof verification failed"

    • Invalid proof or mismatched public inputs
    • Solution: Check that secret, amount, and leaf index are correct
  4. "Insufficient balance"

    • Pool vault doesn't have enough tokens
    • Solution: Ensure shield was successful before unshielding

Best Practices

Secret Management

# ✅ DO: Store secrets securely
import os
from cryptography.fernet import Fernet
 
# Encrypt secrets before storing
encryption_key = os.environ.get("ENCRYPTION_KEY")
encrypted_secret = encrypt_secret(secret, encryption_key)
save_to_database(encrypted_secret)
 
# ❌ DON'T: Store secrets in plain text
secret = "my_secret_123"  # NEVER do this
save_to_file(secret)      # NEVER do this

Error Handling

# ✅ DO: Handle errors gracefully
try:
    tx = await client.private_transfer_async(...)
except ValueError as e:
    print(f"Invalid input: {e}")
except RuntimeError as e:
    print(f"Crypto operation failed: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")
    # Log for debugging

Performance

# ✅ DO: Use connection pooling
async with VeilClient(rpc_url=url) as client:
    # Multiple operations reuse connection
    await client.shield_async(...)
    await client.private_transfer_async(...)
 
# ❌ DON'T: Create new client for each operation
client1 = VeilClient(...)
await client1.shield_async(...)
await client1.close()
 
client2 = VeilClient(...)  # Inefficient
await client2.private_transfer_async(...)

Privacy

# ✅ DO: Wait for larger anonymity sets
# ✅ DO: Use standard denominations (1, 10, 100 SOL)
# ✅ DO: Use relayers for IP privacy
# ✅ DO: Perform multiple internal transfers before unshielding
 
# ❌ DON'T: Use unique amounts (123.456789 SOL)
# ❌ DON'T: Unshield immediately after shielding
# ❌ DON'T: Submit directly without relayer (IP exposed)

Next Steps