API Reference

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
) → Ciphertext

pure

#

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:

  • pk is not the identity point.
  • r is non-zero.

encrypt(
  pk: JubjubPoint,
  value: Uint<128>,
  r: JubjubScalar
) → Ciphertext

pure

#

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:

  • pk is not the identity point.
  • r is non-zero.

negate(ct: Ciphertext) → Ciphertext

pure

#

Negates a ciphertext componentwise: if ct encrypts v then the result encrypts -v under the same key (with negated randomness). This is the building block sub rests on.

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
) → Ciphertext

pure

#

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:

  • pk is not the identity point.
  • r is non-zero.

subEncrypted(
  old: Ciphertext,
  pk: JubjubPoint,
  value: Uint<128>,
  r: JubjubScalar
) → Ciphertext

pure

#

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:

  • pk is not the identity point.
  • r is non-zero.

rerandomize(
  ct: Ciphertext,
  pk: JubjubPoint,
  r: JubjubScalar
) → Ciphertext

pure

#

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:

  • pk is not the identity point.
  • r is 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:

  • ek is the secret for pk (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:

  • ek is the secret for pk.
  • ct decrypts under (pk, ek) to m.

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:

  • ek is the secret for pk.
  • ct decrypts under (pk, ek) to claimedValue.

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>
) → Ciphertext

pure

#

Encrypts value to recipientPk under ephemeral scalar e.

e MUST be fresh per call (see the module freshness precondition).

Requirements:

  • recipientPk is not the identity point.
  • e is non-zero, a valid Jubjub scalar (< ℓ), secret, and fresh per call.

decrypt(
  ciphertext: Ciphertext,
  ekScalar: JubjubScalar,
  domain: Bytes<32>
) → Field

pure

#

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.ephemeralPk is 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.

k=13, rows=5125

assertLowS(sig: Secp256k1EcdsaSignature) → []

pure

#

Rejects a signature in the high half.

Requirements:

  • sig.s must satisfy s <= n / 2.
k=13, rows=5156

secp256k1EcdsaVerifyLowS(
  msgHash: Bytes<32>,
  sig: Secp256k1EcdsaSignature,
  pk: Secp256k1Point
) → Boolean

pure

#

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.

k=16, rows=34056

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.

k=14, rows=4561

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:

  • separator must be non-zero, which an initialized domain always is.
k=14, rows=4463