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:
- Draw independent fresh nonzero
eandsuniformly from1..P-1using rejection sampling from a cryptographic random source.sis the note-encryption secret. Never reuseeors; the circuit can check their nonzero range but cannot prove freshness. - Validate the recipient’s registered
ekas a finite Grumpkin point and computeepk = e · Gandshared = e · ek. - Compute
K = H([D_KDF, shared.x, shared.y, epk.x, epk.y, ek.x, ek.y, context]). - Set
encrypted_secret = s + H([D_SECRET, K]). - For body positions
i = 0..4, setencrypted_body[i] = body[i] + H([D_BODY, K, s, i]). - 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]]).
Why Grumpkin
Grumpkin’s base field is exactly the BN254 scalar field. Its equation isy² = 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 encryptedNoteAdded 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.
- Require exactly nine canonical fields and a finite on-curve
epk. - Derive
ek = ivk · G,shared = ivk · epk,Kand the full tag. Reject a mismatch before accepting any decrypted body. - Recover
sby subtracting its mask and reject zero. Subtract the five body masks. - Recompute
H3(H2(rkHash, rkTrapdoor), H3(value, coinId, valueTrapdoor), context)and require it to equal the exact on-chain commitment being scanned. - 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.