Encrypted withdrawal change notes use Veilify note encryption v1, a precisely specified Grumpkin ECIES construction with Poseidon2 field masks and a full authentication tag. The Noir helper and TypeScript encryptor/scanner implement the same nine-field format and are checked against each other, including every ciphertext-field mutation. This is a new Veilify protocol specification. It does not claim compatibility with an unspecified upstream note-encryption format, and vector checks do not replace a security audit of the construction and complete spending circuit. Deposit notes have zero headers and clear bodies in the existing contract. They are processed separately and are not confidential v1 ciphertexts. The contract’s current swap output format combines a six-field header with amounts added after the swap, so that format also needs a separate specification and circuit; it cannot use the full v1 authentication tag unchanged.

Ciphertext layout

Every field is a canonical BN254 scalar (bytes32, less than P = 21888242871839275222246405745257275088548364400416034343698204186575808495617). Zero commitment plus nine zero fields denotes an absent change output, not an encrypted note. The format version is fixed by the circuit/verifier deployment and the domain constants below, rather than an additional field in the nine-field payload. Changing the format requires a new version and matching verifier/client configuration.

Encryption

H is the pool-compatible Poseidon2 hashMany sponge: state width 4, rate 3, length times 2^64 in the capacity, one permutation per absorbed block and the final squeeze, without an extra empty block when the input length is a multiple of three. All additions and subtractions below are modulo P. The body is exactly [rkHash, rkTrapdoor, value, coinId, valueTrapdoor]. context is the nullifiersHash used by the note commitment. Domains are the big-endian integers encoding these ASCII strings:
  1. Draw independent fresh nonzero e and s uniformly from 1..P-1 using rejection sampling from a cryptographic random source. s is the note-encryption secret. Never reuse e or s; the circuit can check their nonzero range but cannot prove freshness.
  2. Validate the recipient’s registered ek as a finite Grumpkin point and compute epk = e · G and shared = e · ek.
  3. Compute K = H([D_KDF, shared.x, shared.y, epk.x, epk.y, ek.x, ek.y, context]).
  4. Set encrypted_secret = s + H([D_SECRET, K]).
  5. For body positions i = 0..4, set encrypted_body[i] = body[i] + H([D_BODY, K, s, i]).
  6. Compute the full tag: H([D_TAG, K, epk.x, epk.y, ek.x, ek.y, context, encrypted_secret, encrypted_body[0], encrypted_body[1], encrypted_body[2], encrypted_body[3], encrypted_body[4]]).
The tag binds the recipient, ephemeral key, commitment context and every encrypted field. It is an authentication check, rather than a short scanning hint. The spending circuit must also bind the recipient key to the note owner and assert all nine output fields and the commitment produced from the same plaintext body and context.

Why Grumpkin

Grumpkin’s base field is exactly the BN254 scalar field. Its equation is y² = x³ − 17, its prime group order is 21888242871839275222246405745257275088696311157297823662689037894645226208583, and its cofactor is 1. The standard generator is G = (1, 17631683881184975370165255887551781615748388533673675138860). These match the pinned Noir standard library. The cycle and curve parameters are also documented by the Grumpkin implementation in arkworks and Barretenberg’s Grumpkin source. The contract and circuit check that ek is on curve and finite. This rejects invalid keys that would make encrypted funds unrecoverable. The curve equation does not prove that a registrant knows the secret or that a proxy supplied the intended key. The client must derive and verify its own registered key; the spending circuit must bind change to that registered key. The v1 scalar API uses a canonical nonzero BN254 Field, which is smaller than Grumpkin’s group order. Noir’s EmbeddedCurveScalar::from_field constrains the two 128-bit limbs, reconstruction and canonical decomposition. TypeScript rejects noncanonical values rather than reducing them. TypeScript uses @noble/curves for complete curve operations; JavaScript BigInt and its runtime do not provide an end-to-end constant-time guarantee.

Trial decryption

The recipient’s scanner processes the complete transaction receipt, associates encrypted NoteAdded commitments with the corresponding ShieldedWithdrawal change commitments, and obtains context from that withdrawal’s nullifiersHash. NoteAdded alone does not carry enough information to open a v1 note. Deposit events follow their separate clear format.
  1. Require exactly nine canonical fields and a finite on-curve epk.
  2. Derive ek = ivk · G, shared = ivk · epk, K and the full tag. Reject a mismatch before accepting any decrypted body.
  3. Recover s by subtracting its mask and reject zero. Subtract the five body masks.
  4. Recompute H3(H2(rkHash, rkTrapdoor), H3(value, coinId, valueTrapdoor), context) and require it to equal the exact on-chain commitment being scanned.
  5. Once the scanner’s key ownership and chain checks pass, derive the note’s nullifier and check whether it has been spent before adding it to the spendable balance.
decryptNote requires the expected commitment and context. tryDecryptNote skips wrong recipients and malformed notes. This module supplies the cryptographic trial-decryption primitive; chain indexing, account key derivation and spent-state tracking are separate integration work. It does not implement an outgoing viewing-key recovery format.