Crypto API
This page provides the full API for the Crypto modules.
For an overview of the modules, read the Crypto guide.
The page covers the following modules:
- ElGamal: exponential ElGamal encryption over Jubjub with homomorphic addition and re-randomization.
- EcdhMask: an ECDH-derived mask for encrypting a value to a recipient's public key.
- Ecdsa: the low-s rule for secp256k1 ECDSA signatures.
- Eip712: the EIP-712 typed-data envelope, for verifying signatures produced by Ethereum signing infrastructure.
import "./node_modules/@openzeppelin/compact-contracts/crypto/ElGamal";Scalars are typed JubjubScalar, not Field. A Field as JubjubScalar cast reduces modulo the subgroup order,
so casting at a call site would silently accept an out-of-range scalar the typed parameter rejects.
Types
struct Ciphertext { c1: JubjubPoint; c2: JubjubPoint; }
struct
#Single-receiver ElGamal ciphertext over Jubjub. c1 = g^r c2 = pk^r * m (lifted: m = g^v)
Ledger
None.
Witnesses
None.
Circuits
secretToScalar(secret: Bytes<32>) → JubjubScalar
pure
#Maps a 32-byte secret to a valid Jubjub scalar. See the
module-level hash-to-scalar note for why degradeToTransient is required.
Domain separation here is load-bearing, not cosmetic. This output is
the private key protecting every ciphertext held under an account. Public
account identifiers derived from Utils.computeAccountId, for example,
are computed as persistentHash([secretKey]), and that identifier
is stored in the clear as a ledger map key. Untagged, this circuit
would compute that same hash, so a secret used in both roles would
have its encryption key recoverable from a published identifier by applying
degradeToTransient. The tag makes the two derivations unrelated, so a
wallet MAY safely derive its account secret and its encryption secret from
common key material.
The tag is FIRST, deliberately. expandRandomness hashes
[seed, tag] with both slots caller-supplied, so tagging this circuit as
[secret, tag] would make secretToScalar(x) identical to
expandRandomness(x, <this tag>) reachable by passing this domain string
to the parameter that exists to receive domain strings. Leading with the
tag means reproducing this output through expandRandomness would require
passing the domain constant as the seed. Do not normalise the orderings.
derivePk(ek: Bytes<32>) → JubjubPoint
pure
#Derives the ElGamal public key pk = g^secretToScalar(ek)
from a secret ek.
expandRandomness(seed: Bytes<32>, tag: Bytes<32>) → JubjubScalar
pure
#Deterministically expands a 32-byte seed into a Jubjub scalar tagged by a distinguishing string, so a single witness seed can yield many distinct randomness values within one circuit invocation. Distinct tags always produce distinct outputs (collision resistance of the hash); the caller is responsible for using distinct tags.
encryptZero() → Ciphertext
pure
#The identity ciphertext Enc(0) (both components are the
curve identity). Useful for initializing a fresh balance.
This is a fixed, deterministic value (O, O) — NOT a hiding
encryption. It is publicly recognizable, so a slot left at encryptZero is
distinguishable as never-written. Rerandomize (or credit with fresh r)
before treating it as private.
encryptPoint(
pk: JubjubPoint,
m: JubjubPoint,
r: JubjubScalar
) → Ciphertextpure
#Encrypts an arbitrary message point m under pk with
randomness r, producing the ciphertext (g^r, pk^r * m). This is the
general (non-lifted) ElGamal encryption; encrypt is the lifted special
case encryptPoint(pk, g^value, r).
m (like pk) is constrained into the prime-order subgroup by the
runtime (see the module-level subgroup note), so no in-circuit membership
check is performed. pk is asserted non-identity and r non-zero — the
two inputs that would zero the mask and expose m (see the weak-keys and
randomness notes). Decrypting m is only feasible when it is a known or
small-exponent point; the caller owns that concern (the lifted encrypt
bounds it via the discrete-log range).
Requirements:
pkis not the identity point.ris non-zero.
encrypt(
pk: JubjubPoint,
value: Uint<128>,
r: JubjubScalar
) → Ciphertextpure
#Produces a fresh ciphertext of value under pk with
randomness r, lifting value to the point g^value. Randomness is taken
as a scalar directly to avoid a redundant conversion at every call site;
expandRandomness produces one for the same reason.
Requirements:
pkis not the identity point.ris non-zero.
add(a: Ciphertext, b: Ciphertext) → Ciphertext
pure
#Homomorphically adds two ciphertexts: Enc(a) + Enc(b)
decrypts to a + b. This is the defining additive-homomorphism primitive
and combines two independently produced ciphertexts componentwise.
Both inputs MUST be encrypted under the same public key for the result to decrypt under that key's secret; combining ciphertexts under different keys yields a ciphertext no single key can open. The randomness of the result is the sum of the inputs' randomness, so the result is a valid fresh ciphertext under that key.
sub(a: Ciphertext, b: Ciphertext) → Ciphertext
pure
#Homomorphically subtracts two ciphertexts: Enc(a) - Enc(b)
decrypts to a - b. Implemented as add(a, negate(b)).
Same-key requirement as add. The caller MUST ensure the underlying plaintext subtraction does not underflow; this circuit cannot check it (see subEncrypted).
scalarMul(ct: Ciphertext, k: JubjubScalar) → Ciphertext
pure
#Homomorphically multiplies the plaintext of ct by a public
scalar k: if ct encrypts v then the result encrypts k * v under the
same key. Both components are scaled by k, so the randomness scales to
k * r and the result is a valid fresh ciphertext under that key.
This is the scalar-multiplication leg of the homomorphism, and the only way
to express linear combinations: a weighted sum Enc(3a + 5b) is
add(scalarMul(ca, 3), scalarMul(cb, 5)), which repeated add cannot do
for large weights.
k MUST be a valid Jubjub scalar (less than the subgroup order);
larger values fault ecMul (see the module-level hash-to-scalar note). The
caller also remains responsible for the discrete-log bound — k * v must
stay within the range a recovery search can clear.
addEncrypted(
old: Ciphertext,
pk: JubjubPoint,
value: Uint<128>,
r: JubjubScalar
) → Ciphertextpure
#Homomorphically adds value to the plaintext encrypted by
old, producing a fresh ciphertext under pk with new randomness r.
Convenience over add for folding a known plaintext into a ciphertext:
add(old, encrypt(pk, value, r)).
Requirements:
pkis not the identity point.ris non-zero.
subEncrypted(
old: Ciphertext,
pk: JubjubPoint,
value: Uint<128>,
r: JubjubScalar
) → Ciphertextpure
#Homomorphically subtracts value from the plaintext encrypted
by old. Convenience over sub: sub(old, encrypt(pk, value, r)).
The caller MUST ensure the underlying plaintext subtraction does not underflow; this circuit cannot check it. Callers verify sufficiency by reading the plaintext via their own witness and asserting it with assertDecryptsTo before calling.
Requirements:
pkis not the identity point.ris non-zero.
rerandomize(
ct: Ciphertext,
pk: JubjubPoint,
r: JubjubScalar
) → Ciphertextpure
#Rerandomizes ct: returns a fresh ciphertext of the same
plaintext under the same key pk, using new randomness r. The output is
unlinkable to the input (it is a uniformly fresh encryption of the same
value) yet decrypts identically. Equivalent to homomorphically adding an
encryption of zero.
r = 0 is rejected (it would be a no-op via the encryptPoint
zero-randomness assert). One silent failure mode remains that the circuit
cannot catch: a pk that is not the key ct was encrypted under corrupts
the plaintext (the result no longer decrypts to the original value). Pass
fresh randomness and the matching pk.
Requirements:
pkis not the identity point.ris non-zero.
assertKeyPair(pk: JubjubPoint, ek: Bytes<32>) → []
pure
#Asserts that ek is the secret for pk, i.e.
derivePk(ek) == pk. Lets a caller prove ownership of a public key
without touching any ciphertext or plaintext.
Requirements:
ekis the secret forpk(derivePk(ek) == pk).
assertDecryptsToPoint(
ct: Ciphertext,
pk: JubjubPoint,
ek: Bytes<32>,
m: JubjubPoint
) → []pure
#Asserts that ct decrypts under (pk, ek) to the message
point m, and that ek is the secret for pk. This is the general
(non-lifted) verification; assertDecryptsTo is the lifted special case
with m = g^claimedValue.
Requirements:
ekis the secret forpk.ctdecrypts under(pk, ek)tom.
assertDecryptsTo(
ct: Ciphertext,
pk: JubjubPoint,
ek: Bytes<32>,
claimedValue: Uint<248>
) → []pure
#Asserts that ct decrypts under (pk, ek) to claimedValue,
and that ek is the secret for pk. This is the in-circuit verification
path an importing module uses to bind its witness-supplied ek and cached
plaintext to publicly stored state: a wrong ek fails the key check, a
wrong plaintext fails the decryption check. Lifted special case of
assertDecryptsToPoint with the message point g^claimedValue.
claimedValue is the widest Uint Compact offers, so this parameter
never bounds a caller. A narrower value costs proportionally fewer rows: the
range constraint belongs to where the value is declared, not to this
signature.
The claim domain must stay below the Jubjub subgroup order l:
verification compares g^claimedValue, so two claims congruent mod l are
indistinguishable. Uint<248> clears l (~2^251.86) by under 4 bits, and
that width is set by Compact's cap rather than by this bound. 251 is the
widest sound Uint if the cap ever rises.
Requirements:
ekis the secret forpk.ctdecrypts under(pk, ek)toclaimedValue.
import "./node_modules/@openzeppelin/compact-contracts/crypto/EcdhMask";Types
struct Ciphertext { ephemeralPk: JubjubPoint; ct: Field; }
struct
#An ECDH one-time-pad ciphertext: the ephemeral public key and the field-masked value.
Ledger
None.
Witnesses
None.
Circuits
kdf(sShared: JubjubPoint, domain: Bytes<32>) → Field
pure
#Derives the field mask from the ECDH shared secret point,
domain-separated by a caller-supplied domain tag. Hashes the point to
Bytes<32>, then re-hashes it with the domain tag and truncates into the
field via degradeToTransient. The consumer chooses domain (so this
module is not tied to any one protocol) and MUST use the same value on
encrypt and decrypt.
Uses persistentHash deliberately: the recipient reproduces this mask
to decrypt, possibly across a platform upgrade, so it must be upgrade-stable.
encrypt(
recipientPk: JubjubPoint,
value: Uint<128>,
e: JubjubScalar,
domain: Bytes<32>
) → Ciphertextpure
#Encrypts value to recipientPk under ephemeral scalar e.
e MUST be fresh per call (see the module freshness precondition).
Requirements:
recipientPkis not the identity point.eis non-zero, a valid Jubjub scalar (< ℓ), secret, and fresh per call.
decrypt(
ciphertext: Ciphertext,
ekScalar: JubjubScalar,
domain: Bytes<32>
) → Fieldpure
#Recovers the value from a ciphertext using the recipient's secret scalar. Pure and off-chain (no proof needed to read a received value).
Requirements:
ciphertext.ephemeralPkis not the identity point.
import "./node_modules/@openzeppelin/compact-contracts/crypto/Ecdsa";secp256k1EcdsaVerify accepts both (r, s) and (r, n - s), where n is the secp256k1 group order.
This module rejects s > n / 2, so each signature has one encoding.
Requires ZKIR v3: the Secp256k1 types are unavailable on the v2 backend.
Ledger
None.
Witnesses
None.
Circuits
isLowS(s: Secp256k1Scalar) → Boolean
pure
#Returns whether s is canonical, i.e. s <= n / 2 where n
is the secp256k1 group order.
assertLowS(sig: Secp256k1EcdsaSignature) → []
pure
#Rejects a signature in the high half.
Requirements:
sig.smust satisfys <= n / 2.
secp256k1EcdsaVerifyLowS(
msgHash: Bytes<32>,
sig: Secp256k1EcdsaSignature,
pk: Secp256k1Point
) → Booleanpure
#Verifies sig over msgHash under pk, accepting only the
low-s encoding. Reports the result instead of asserting, so a caller can
fold it into a count or a threshold.
import "./node_modules/@openzeppelin/compact-contracts/crypto/Eip712";The domain is (name, version, salt); chainId and verifyingContract are omitted.
Requires ZKIR v3: keccak256 is unavailable on the v2 backend.
Ledger
None.
Witnesses
None.
Circuits
domainSeparator(
hashedName: Bytes<32>,
hashedVersion: Bytes<32>,
salt: Bytes<32>
) → Bytes<32>pure
#Computes the domain separator for (name, version, salt).
EIP-712 encodes a string field as the keccak hash of its UTF-8 bytes,
so the name and version arrive already hashed: hashedName is keccak256(bytes(name))
and hashedVersion is keccak256(bytes(version)). Compact has no dynamic types,
so a consumer hashes its own fixed-length literals. salt is a per-deployment,
per-network random value.
hashTypedData(separator: Bytes<32>, structHash: Bytes<32>) → Bytes<32>
pure
#Wraps a struct hash in the typed-data envelope keccak256(0x1901 || separator || structHash),
yielding the digest an EVM signer actually signs. structHash is
keccak256(typeHash || the operation's ABI words).
A zero separator is what an uninitialized ledger cell reads as, and a digest built from one still verifies signatures, so this circuit fails closed on it.
Requirements:
separatormust be non-zero, which an initialized domain always is.