Note Encryption
ECDH-based recipient discovery system
When you send a private transfer in Veil, the recipient needs to discover that they've received funds. Since all commitments in the Merkle tree look random, Veil uses ECDH-based note encryption to enable recipients to identify and decrypt notes intended for them.
In a privacy-preserving system:
- All commitments appear as random 32-byte values on-chain
- No addresses or identifiers are attached to commitments
- Recipients need a way to determine which commitments belong to them
The Challenge: How can a recipient discover their notes without compromising privacy?
The Solution: Encrypt note metadata using the recipient's public key, allowing only them to decrypt and claim their funds.
Veil uses Elliptic Curve Diffie-Hellman (ECDH) on the BN254 G1 curve for key exchange:
Key Generation
Recipient's Long-Term Keypair:
recipient_secret_key = random 32 bytes
recipient_public_key = recipient_secret_key * G (point on BN254 G1)
The recipient shares their public key with potential senders (like sharing a payment address).
Encryption Process (Sender)
When sending a private transfer:
-
Generate Ephemeral Keypair:
ephemeral_secret = random 32 bytes ephemeral_public = ephemeral_secret * G -
Compute Shared Secret:
shared_secret = ephemeral_secret * recipient_public_key = ephemeral_secret * (recipient_secret_key * G) -
Derive Encryption Key:
encryption_key = KDF(shared_secret, "NYX_NOTE_ENCRYPTION_V1") -
Encrypt Note Data:
- Note plaintext:
amount (8 bytes) || blinding (32 bytes) || asset_id (8 bytes)= 48 bytes - Encrypted using ChaCha20-Poly1305 AEAD
- Includes 16-byte authentication tag
- Note plaintext:
-
Publish On-Chain:
EncryptedNote { ephemeral_public_key: 32 bytes, ciphertext: 64 bytes (48 data + 16 MAC), total: 96 bytes }
Decryption Process (Recipient)
The recipient scans the blockchain for potential notes:
-
Compute Shared Secret:
shared_secret = recipient_secret_key * ephemeral_public_key = recipient_secret_key * (ephemeral_secret * G)(Same shared secret as sender computed!)
-
Derive Decryption Key:
decryption_key = KDF(shared_secret, "NYX_NOTE_ENCRYPTION_V1") -
Attempt Decryption:
- Try to decrypt the ciphertext with ChaCha20-Poly1305
- If authentication succeeds → this note belongs to the recipient
- If authentication fails → note belongs to someone else
-
Extract Note Data:
amount = data[0..8] blinding = data[8..40] asset_id = data[40..48] -
Reconstruct Commitment:
commitment = amount * G + blinding * HVerify this matches the on-chain commitment.
Veil uses ChaCha20-Poly1305, an Authenticated Encryption with Associated Data (AEAD) cipher:
Why ChaCha20-Poly1305?
- Fast software implementation (no AES hardware needed)
- Authenticated encryption (prevents tampering)
- Well-studied and widely deployed (TLS 1.3, WireGuard)
- 256-bit security
Encryption:
nonce = random 12 bytes (or derived from ephemeral_public_key)
ciphertext || tag = ChaCha20Poly1305.encrypt(
key=encryption_key,
nonce=nonce,
plaintext=note_data,
associated_data=""
)
Decryption:
plaintext = ChaCha20Poly1305.decrypt(
key=decryption_key,
nonce=nonce,
ciphertext=ciphertext,
tag=tag,
associated_data=""
)
// Returns plaintext if tag is valid, error otherwise
Plaintext Note (48 bytes)
Offset | Size | Field | Description
--------|------|------------|----------------------------------
0 | 8 | amount | Amount in lamports (little-endian)
8 | 32 | blinding | Blinding factor for commitment
40 | 8 | asset_id | Token identifier (0 = SOL)
Encrypted Note (96 bytes)
Offset | Size | Field | Description
--------|------|--------------------|---------------------------
0 | 32 | ephemeral_public | Ephemeral ECDH public key
32 | 48 | ciphertext | Encrypted note data
80 | 16 | authentication_tag | Poly1305 MAC
Recipients must scan the blockchain to discover their notes:
Scanning Process
-
Fetch Recent Transactions:
- Query Solana RPC for transactions to the privacy program
- Filter for
private_transferinstructions
-
Extract Encrypted Notes:
- Parse transaction data for
EncryptedNotestructures
- Parse transaction data for
-
Trial Decryption:
- For each encrypted note:
- Compute shared secret using recipient's secret key
- Attempt decryption
- If successful → note belongs to recipient
- If failed → skip to next note
- For each encrypted note:
-
Store Discovered Notes:
- Save
amount,blinding,leaf_index,commitment - Track in local database for later spending
- Save
Optimization Strategies
Bloom Filters:
- Recipients can use Bloom filters to quickly skip unlikely notes
- Reduces trial decryption attempts by ~99%
Batch Scanning:
- Scan multiple blocks at once
- Process encrypted notes in parallel
Light Clients:
- Delegate scanning to a server (with privacy trade-offs)
- Server cannot decrypt notes but can assist in filtering
Index by Block Height:
- Only scan new blocks since last check
- Maintain "last scanned block" pointer
1. Confidentiality
Property: Only the recipient can read the note data.
Mechanism: ECDH ensures only parties knowing either ephemeral_secret or recipient_secret_key can derive the shared secret. ChaCha20 provides semantic security.
2. Authenticity
Property: Recipients can verify the note was created by someone knowing the correct shared secret.
Mechanism: Poly1305 MAC provides authentication. If the tag verifies, the note data hasn't been tampered with.
3. Forward Secrecy
Property: Compromising the recipient's long-term key doesn't reveal past notes.
Mechanism: Each note uses a unique ephemeral key. Even if recipient_secret_key is compromised later, past shared secrets cannot be recomputed without the ephemeral secrets (which should be deleted after encryption).
4. Deniability
Property: The sender can plausibly deny creating a specific encrypted note.
Mechanism: The ephemeral key is generated randomly for each note. There's no signature or proof binding the sender to the encrypted note.
What Encrypted Notes Reveal
Public On-Chain:
- Ephemeral public key (32 bytes, appears random)
- Ciphertext (64 bytes, appears random)
- Existence of a note (meta-data: "someone sent a note")
Hidden:
- Amount
- Blinding factor
- Sender identity
- Recipient identity (requires trial decryption to determine)
Scanning Privacy
Risk: Scanning the blockchain reveals information about your activity pattern.
Mitigation:
- Use Tor/VPN when querying RPC nodes
- Delegate to privacy-preserving light clients
- Scan from multiple IPs to avoid correlation
- Batch requests to minimize query patterns
Note Tagging Attacks
Attack: An adversary creates fake encrypted notes with predictable patterns to tag users.
Mitigation:
- Recipients verify commitment on-chain matches decrypted data
- Malformed notes are rejected during verification
- Proper authentication prevents tampering
Encrypting a Note (Sender)
from veil import VeilClient, encrypt_note_for_recipient
client = VeilClient()
# Recipient shares their public key (e.g., via social profile)
recipient_pubkey = "0x1234...5678"
# Create encrypted note for recipient
encrypted_note = encrypt_note_for_recipient(
amount=1_000_000_000, # 1 SOL
recipient_pubkey=recipient_pubkey,
asset_id=0 # SOL
)
# Submit in private_transfer transaction
tx_signature = client.private_transfer(
input_note=my_note,
encrypted_note=encrypted_note
)Scanning for Notes (Recipient)
from veil import VeilClient, decrypt_note
client = VeilClient()
my_secret_key = "your_secret_key_here"
# Scan recent transactions
recent_notes = client.scan_notes(
secret_key=my_secret_key,
start_slot=last_scanned_slot,
end_slot=current_slot
)
for note in recent_notes:
print(f"Received: {note.amount} lamports")
print(f"Commitment: {note.commitment}")
print(f"Leaf index: {note.leaf_index}")
# Store note for later spending
save_note_to_database(note)- Stealth Addresses: One-time addresses generated per transaction for enhanced privacy
- Payment Codes: BIP47-style reusable payment codes to avoid sharing long-term public keys
- View Keys: Separate keys for scanning vs spending (auditing without spending permission)
- Hierarchical Scanning: Derive scanning keys from master seed for multi-device support
See also:
- Privacy Model - Overall privacy guarantees
- Commitments - How note amounts are hidden
- Relayers - IP privacy for transaction submission
On This Page
- Note Encryption
- The Recipient Discovery Problem
- ECDH Key Exchange
- Key Generation
- Encryption Process (Sender)
- Decryption Process (Recipient)
- ChaCha20-Poly1305 AEAD
- Note Format Specification
- Plaintext Note (48 bytes)
- Encrypted Note (96 bytes)
- Recipient Note Scanning
- Scanning Process
- Optimization Strategies
- Security Properties
- 1. Confidentiality
- 2. Authenticity
- 3. Forward Secrecy
- 4. Deniability
- Privacy Considerations
- What Encrypted Notes Reveal
- Scanning Privacy
- Note Tagging Attacks
- Usage Example
- Encrypting a Note (Sender)
- Scanning for Notes (Recipient)
- Future Improvements