Quick Start
Your first private transaction in 5 minutes
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.
pip install veil-solanaConvert 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:
- Your public SOL is transferred to the privacy pool
- A Pedersen commitment
C = amount * G + blinding * His created - The commitment is added to the on-chain Merkle tree
- On-chain: Only the commitment (32 bytes) is visible
- 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 generationSend 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:
- Client-side (5-10 seconds):
- Fetches Merkle proof for your note
- Generates zkSNARK proof (~7,000 constraints)
- Encrypts note data for recipient (ECDH)
- 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 spentWithdraw 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_transferinstead - Minimize graph analysis → wait for larger anonymity set
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 blockchainStatus: Relayer protocol implemented. Public relayer network launching in v0.2.0.
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:
- Fetch recent transactions to the privacy program
- Extract encrypted notes from transaction logs
- Trial decryption: Try to decrypt each note with your secret key
- If decryption succeeds → note is yours!
- 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:
- Incremental scanning: Only scan new blocks
- Bloom filters: Skip unlikely notes (99% reduction)
- Batch processing: Scan multiple blocks at once
- 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.
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:
-
"Nullifier already spent"
- You're trying to spend a note twice
- Solution: Track which notes you've spent
-
"Invalid Merkle root"
- Your proof is for an old Merkle root (>30 updates ago)
- Solution: Regenerate proof with current root
-
"Proof verification failed"
- Invalid proof or mismatched public inputs
- Solution: Check that secret, amount, and leaf index are correct
-
"Insufficient balance"
- Pool vault doesn't have enough tokens
- Solution: Ensure shield was successful before unshielding
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 thisError 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 debuggingPerformance
# ✅ 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)- API Reference - Complete VeilClient documentation
- Architecture - Understand how it works
- Privacy Model - Learn about privacy guarantees and limitations
- Relayers - Deep dive into IP privacy
On This Page
- Quick Start
- Prerequisites
- Example 1: Shield Assets (Make Private)
- Example 2: Private Transfer
- Example 3: Unshield (Withdraw to Public)
- Example 4: Using Relayers (IP Privacy)
- Example 5: Note Discovery (Recipient Scanning)
- Complete Workflow Example
- Best Practices
- Secret Management
- Error Handling
- Performance
- Privacy
- Next Steps