Note Encryption
Nullmask uses a Diffie-Hellman key exchange with custom Poseidon2-based symmetric encryption to encrypt notes for recipients.
Encryption Algorithm
Algorithm 5: Encrypt Note
Input:
A Note note
Recipient's Encryption Key ek
Sender's Outgoing Viewing Key ovk
Output: Encrypted Note Cnote
epk←randomScalar()
dhek←epk⋅G
shared_secret←Poseidon2T4(epk⋅ek)
(m1,m2,m3)←Poseidon2T4(shared_secret,1)
(m4,m5,tag)←Poseidon2T4(shared_secret,2)
ciphertext←note+(m1,m2,m3,m4,m5)
ek_out←shared_secret+Poseidon2T4(epk.x,ovk)
Return (dhek,ek_out,tag,ciphertext)
Ciphertext Format
The encrypted note is serialized as 9 field elements:
0
epk.x
Ephemeral public key x-coordinate
1
epk.y
Ephemeral public key y-coordinate
2
encrypted_secret
DH shared secret encrypted with OVK
3
tag
Verification tag for trial decryption
4
ciphertext[0]
Encrypted owner_hash
5
ciphertext[1]
Encrypted rk_trapdoor
6
ciphertext[2]
Encrypted value
7
ciphertext[3]
Encrypted coin_id
8
ciphertext[4]
Encrypted value_trapdoor
The nfs_hash field is not encrypted — it is a public value derived from the nullifiers.
Encryption Mask
For circuit constraint optimization, the Poseidon2T4 hash function generates the encryption mask. Two Poseidon2T4 invocations produce 6 masking elements:
Invocation 1: Poseidon2T4(shared_secret,1)→(m1,m2,m3)
Invocation 2: Poseidon2T4(shared_secret,2)→(m4,m5,tag)
Only the first 5 elements mask the note fields. The 6th element is retained as the tag for trial decryption verification.
Trial Decryption
The recipient's proxy continuously scans blockchain events for new NoteAdded events. For each event, it attempts trial decryption:
Incoming Notes (using decryption key)
Compute shared_secret=Poseidon2T4(ivk⋅epk)
Regenerate the encryption mask (m1,…,m5,tag′)
If tag′=tag, the note is for this recipient
Recover plaintext: note=ciphertext−(m1,…,m5)
Outgoing Notes (using outgoing viewing key)
Compute secret_mask=Poseidon2T4(epk.x,ovk)
Recover shared_secret=ek_out−secret_mask
Regenerate the encryption mask (m1,…,m5,tag′)
If tag′=tag, the note was sent by this account
Recover plaintext: note=ciphertext−(m1,…,m5)
Deposit Notes
Deposit notes are not encrypted — their ciphertext fields 0-3 are zero. The proxy identifies them by checking if the tag (field 3) is zero and verifying the owner_hash matches the user's receiving key hash.
In-Circuit Verification
The ZK circuit verifies note encryption correctness, which prevents the proxy from:
Creating notes the recipient cannot decrypt
Encrypting incorrect values (e.g., sending 0 while claiming to send 100)
Using a wrong recipient key
This verification is critical for security: without it, a malicious proxy could produce unspendable notes, effectively burning the user's funds.
Swap Notes (Half-Encrypted)
For shielded swaps, the output note value is determined by the on-chain swap execution and cannot be known at proof generation time. The circuit encrypts only the ownership fields (owner_hash, rk_trapdoor) as a 6-field half-ciphertext. The contract appends a plaintext of the actual swap output value, coin_id, and a zero value_trapdoor after executing the swap.
Last updated