@openzeppelin/compact-contracts 0.2.0 → 0.3.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/access/AccessControl.compact +3 -35
- package/access/Ownable.compact +5 -51
- package/access/ShieldedAccessControl.compact +2 -2
- package/access/ZOwnablePK.compact +2 -2
- package/crypto/ElGamal.compact +473 -0
- package/multisig/ForwarderPrivate.compact +229 -0
- package/multisig/ForwarderShielded.compact +149 -0
- package/multisig/ForwarderUnshielded.compact +152 -0
- package/multisig/ProposalManager.compact +328 -0
- package/multisig/ShieldedTreasury.compact +201 -0
- package/multisig/ShieldedTreasuryStateless.compact +76 -0
- package/multisig/Signer.compact +312 -0
- package/multisig/SignerManager.compact +205 -0
- package/multisig/UnshieldedTreasury.compact +114 -0
- package/multisig/presets/ShieldedMultiSig.compact +236 -0
- package/multisig/presets/ShieldedMultiSigV2.compact +280 -0
- package/multisig/presets/ShieldedMultiSigV3.compact +368 -0
- package/multisig/presets/forwarder/ForwarderPrivate.compact +125 -0
- package/multisig/presets/forwarder/ForwarderShielded.compact +62 -0
- package/multisig/presets/forwarder/ForwarderUnshielded.compact +64 -0
- package/package.json +9 -5
- package/security/Allowlist.compact +101 -0
- package/security/Blocklist.compact +99 -0
- package/security/Initializable.compact +2 -2
- package/security/Pausable.compact +2 -2
- package/token/FungibleToken.compact +24 -82
- package/token/MultiToken.compact +21 -80
- package/token/NativeShieldedToken.compact +285 -0
- package/token/NativeShieldedTokenCore.compact +343 -0
- package/token/NativeShieldedTokenFamily.compact +287 -0
- package/token/NonFungibleToken.compact +25 -83
- package/utils/Utils.compact +86 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[](https://docs.midnight.network/relnotes/compact/minokawa-0-18-26-0)
|
|
2
2
|
[](CODE_OF_CONDUCT.md)
|
|
3
3
|
|
|
4
4
|
This project is built on the Midnight Network.
|
|
@@ -37,8 +37,8 @@ Follow Midnight's [Compact Developer Tools installation guide](https://docs.midn
|
|
|
37
37
|
```bash
|
|
38
38
|
$ compact compile --version
|
|
39
39
|
|
|
40
|
-
Compactc version: 0.
|
|
41
|
-
0.
|
|
40
|
+
Compactc version: 0.31.0
|
|
41
|
+
0.31.0
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
### Installation
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (access/AccessControl.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 0.
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @module AccessControl
|
|
@@ -442,38 +442,6 @@ module AccessControl {
|
|
|
442
442
|
* @returns {Bytes<32>} accountId - The computed account identifier.
|
|
443
443
|
*/
|
|
444
444
|
circuit _computeAccountId(): Bytes<32> {
|
|
445
|
-
return
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* @description Computes an account identifier without on-chain state, allowing a user to derive
|
|
450
|
-
* their identity commitment before submitting it in a grant or revoke operation.
|
|
451
|
-
* This is the off-chain counterpart to {_computeAccountId} and produces an identical result
|
|
452
|
-
* given the same inputs.
|
|
453
|
-
*
|
|
454
|
-
* @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
|
|
455
|
-
* permanently compromise the privacy guarantees of this system:
|
|
456
|
-
*
|
|
457
|
-
* - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
|
|
458
|
-
* application logs, analytics pipelines, or any observable side-channel.
|
|
459
|
-
* - **Store offline or in secure enclaves** — hardware security modules (HSMs),
|
|
460
|
-
* air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
|
|
461
|
-
* - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
|
|
462
|
-
* or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
|
|
463
|
-
* - **Treat key loss as identity loss** — a lost key cannot be recovered. Back up
|
|
464
|
-
* keys securely before using them in role commitments.
|
|
465
|
-
* - **Avoid calling this circuit in untrusted environments** — executing this in an
|
|
466
|
-
* unverified browser extension, compromised runtime, or shared machine may expose
|
|
467
|
-
* the key to a malicious observer.
|
|
468
|
-
*
|
|
469
|
-
* ## ID Derivation
|
|
470
|
-
* `accountId = persistentHash(secretKey)`
|
|
471
|
-
*
|
|
472
|
-
* @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
|
|
473
|
-
*
|
|
474
|
-
* @returns {Bytes<32>} accountId - The computed account identifier.
|
|
475
|
-
*/
|
|
476
|
-
export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
|
|
477
|
-
return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
|
|
445
|
+
return Utils_computeAccountId(wit_AccessControlSK());
|
|
478
446
|
}
|
|
479
447
|
}
|
package/access/Ownable.compact
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (access/Ownable.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 0.
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @module Ownable
|
|
@@ -125,7 +125,7 @@ module Ownable {
|
|
|
125
125
|
export circuit initialize(initialOwner: Either<Bytes<32>, ContractAddress>): [] {
|
|
126
126
|
assertNotInitialized();
|
|
127
127
|
_isInitialized = true;
|
|
128
|
-
assert(!
|
|
128
|
+
assert(!Utils_isTargetZero(initialOwner), "Ownable: invalid initial owner");
|
|
129
129
|
_transferOwnership(initialOwner);
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -219,7 +219,7 @@ module Ownable {
|
|
|
219
219
|
export circuit _unsafeTransferOwnership(newOwner: Either<Bytes<32>, ContractAddress>): [] {
|
|
220
220
|
assertInitialized();
|
|
221
221
|
assertOnlyOwner();
|
|
222
|
-
assert(!
|
|
222
|
+
assert(!Utils_isTargetZero(newOwner), "Ownable: invalid new owner");
|
|
223
223
|
_unsafeUncheckedTransferOwnership(newOwner);
|
|
224
224
|
}
|
|
225
225
|
|
|
@@ -335,52 +335,6 @@ module Ownable {
|
|
|
335
335
|
* @returns {Bytes<32>} accountId - The computed account identifier.
|
|
336
336
|
*/
|
|
337
337
|
circuit _computeAccountId(): Bytes<32> {
|
|
338
|
-
return
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* @description Computes an account identifier without on-chain state, allowing a user to derive
|
|
343
|
-
* their identity commitment before submitting it in an ownership transfer.
|
|
344
|
-
* This is the off-chain counterpart to {_computeAccountId} and produces an identical result
|
|
345
|
-
* given the same inputs.
|
|
346
|
-
*
|
|
347
|
-
* @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
|
|
348
|
-
* permanently compromise the security of this system:
|
|
349
|
-
*
|
|
350
|
-
* - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
|
|
351
|
-
* application logs, analytics pipelines, or any observable side-channel.
|
|
352
|
-
* - **Store offline or in secure enclaves** — hardware security modules (HSMs),
|
|
353
|
-
* air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
|
|
354
|
-
* - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
|
|
355
|
-
* or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
|
|
356
|
-
* - **Treat key loss as identity loss** — a lost key cannot be recovered.
|
|
357
|
-
* - **Avoid calling this circuit in untrusted environments** — executing this in an
|
|
358
|
-
* unverified browser extension, compromised runtime, or shared machine may expose
|
|
359
|
-
* the key to a malicious observer.
|
|
360
|
-
*
|
|
361
|
-
* ## ID Derivation
|
|
362
|
-
* `accountId = persistentHash(secretKey)`
|
|
363
|
-
*
|
|
364
|
-
* @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
|
|
365
|
-
*
|
|
366
|
-
* @returns {Bytes<32>} accountId - The computed account identifier.
|
|
367
|
-
*/
|
|
368
|
-
export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
|
|
369
|
-
return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* @description Returns `true` if `target`'s active branch (as indicated by `is_left`)
|
|
374
|
-
* holds the zero value.
|
|
375
|
-
*
|
|
376
|
-
* @param {Either<Bytes<32>, ContractAddress>} target - The value to check.
|
|
377
|
-
* @returns {Boolean} - `true` if the active branch is zero, `false` otherwise.
|
|
378
|
-
*/
|
|
379
|
-
circuit _isTargetZero(target: Either<Bytes<32>, ContractAddress>): Boolean {
|
|
380
|
-
if (target.is_left) {
|
|
381
|
-
return target.left == default<Bytes<32>>;
|
|
382
|
-
} else {
|
|
383
|
-
return target.right == default<ContractAddress>;
|
|
384
|
-
}
|
|
338
|
+
return Utils_computeAccountId(wit_OwnableSK());
|
|
385
339
|
}
|
|
386
340
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (access/ShieldedAccessControl.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 0.
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @module ShieldedAccessControl
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (access/ZOwnablePK.compact)
|
|
3
3
|
|
|
4
|
-
pragma language_version >= 0.
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @module ZOwnablePK
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (crypto/ElGamal.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module ElGamal
|
|
8
|
+
* @description Stateless, single-receiver ElGamal primitives over the Jubjub
|
|
9
|
+
* curve. Provides general point encryption (`encryptPoint` / `assertDecryptsToPoint`)
|
|
10
|
+
* and the exponential (lifted) variant (`encrypt` / `assertDecryptsTo`) for
|
|
11
|
+
* bounded scalar values.
|
|
12
|
+
*
|
|
13
|
+
* In the lifted variant a value `v` is encrypted as the point `g^v`, so the
|
|
14
|
+
* scheme is additively homomorphic in the exponent: ciphertexts of `a` and `b`
|
|
15
|
+
* combine into a ciphertext of `a + b`. Recovering `v` from `g^v` requires a
|
|
16
|
+
* discrete-log search, so the lifted variant is only usable when `v` is bounded;
|
|
17
|
+
* the caller is responsible for enforcing that bound (e.g. capping each encrypted
|
|
18
|
+
* value to a range a discrete-log search can clear). The general point form
|
|
19
|
+
* carries an arbitrary message point and sidesteps the bound, at the cost of
|
|
20
|
+
* decryption recovering only a point (not a scalar).
|
|
21
|
+
*
|
|
22
|
+
* @notice This module holds NO ledger state and declares NO witnesses. Every
|
|
23
|
+
* circuit takes its keys, plaintexts, and randomness as explicit arguments so
|
|
24
|
+
* the same primitives can back many importing modules (single-receiver today;
|
|
25
|
+
* multi-receiver / auditor variants later) without each importer inheriting a
|
|
26
|
+
* witness interface it may not need. Secrets and cached plaintexts are supplied
|
|
27
|
+
* by the importing module's own witnesses and verified there.
|
|
28
|
+
*
|
|
29
|
+
* @dev Hash-to-scalar discipline. Both `secretToScalar` and `expandRandomness`
|
|
30
|
+
* map bytes to a Jubjub scalar via `degradeToTransient(persistentHash(...))`.
|
|
31
|
+
* `persistentHash` is used because the derived public key is stored on-chain
|
|
32
|
+
* and compared against on every later verification; `degradeToTransient`
|
|
33
|
+
* truncates the Field to 31 bytes so the result is ALWAYS a valid Jubjub
|
|
34
|
+
* scalar. Feeding a raw `persistentHash` output into `ecMulGenerator` would
|
|
35
|
+
* occasionally exceed the Jubjub scalar field order and fault at runtime.
|
|
36
|
+
*
|
|
37
|
+
* @dev TRUST ASSUMPTION — subgroup membership. This is the module's most
|
|
38
|
+
* load-bearing assumption. Every `JubjubPoint` reaching a curve operation is
|
|
39
|
+
* assumed to be in the Jubjub prime-order subgroup, including points supplied by
|
|
40
|
+
* a caller as a witness or argument (e.g. a recipient `pk`, or the message point
|
|
41
|
+
* in `encryptPoint`). The Midnight runtime enforces this at the ZK-constraint
|
|
42
|
+
* level: the embedded-curve gadget assigns points via cofactor clearing, so an
|
|
43
|
+
* off-curve / low-order / mixed-order point makes the circuit unsatisfiable.
|
|
44
|
+
* The module therefore performs NO in-circuit subgroup check: the runtime
|
|
45
|
+
* already constrains membership (so it is unnecessary), and the direct test
|
|
46
|
+
* `ℓ*P == O` is unavailable anyway because `ecMul` by `ℓ` faults (verified — `ℓ`
|
|
47
|
+
* exceeds the valid scalar range, while `ℓ-1` is accepted). The `ecNeg` identity
|
|
48
|
+
* (`(ℓ-1)*P = -P`) and the homomorphic algebra depend on this holding.
|
|
49
|
+
*
|
|
50
|
+
* Basis (and its limits): source-verified against `midnight-circuits` 6.0.0 —
|
|
51
|
+
* the version vendored by the Midnight onchain-runtime we compile against —
|
|
52
|
+
* specifically `ecc::native::edwards_chip`, where `assign` cofactor-clears the
|
|
53
|
+
* point and `q_mem`'s `create_membership_gate` enforces on-curve, reached from
|
|
54
|
+
* the ZKIR `ec_mul`/`ec_add` lowering via `ecc_from_parts`. This is NOT a
|
|
55
|
+
* team-confirmed or audited guarantee: compactc and midnight-ledger are
|
|
56
|
+
* unaudited, so treat it as a substantiated-but-unaudited assumption. It is
|
|
57
|
+
* pinned against regression by `crypto/test/CurveRuntimeInvariants.test.ts` (a
|
|
58
|
+
* runtime behavior canary) and MUST be re-verified in source on any Midnight
|
|
59
|
+
* runtime / circuits version bump.
|
|
60
|
+
*
|
|
61
|
+
* @dev Weak keys. Encrypting under the identity point yields `c2 = m` with no
|
|
62
|
+
* masking (the plaintext is exposed), and the identity is a valid subgroup
|
|
63
|
+
* point so the runtime subgroup guarantee does NOT catch it. `encryptPoint`
|
|
64
|
+
* therefore asserts `pk != identity`, and every encryption path routes through
|
|
65
|
+
* it, so confidentiality is protected regardless of the importer. An importing
|
|
66
|
+
* module MAY additionally reject an identity `pk` where keys enter the system
|
|
67
|
+
* (e.g. at registration) to fail fast with a clear error rather than at first
|
|
68
|
+
* encryption — a UX nicety, not a confidentiality requirement.
|
|
69
|
+
*
|
|
70
|
+
* @dev Randomness. `encryptPoint` asserts `r != 0` — like `pk = identity`,
|
|
71
|
+
* `r = 0` zeros the mask (`r*pk = O`), yielding the non-hiding `(O, m)`; it is
|
|
72
|
+
* the unique mask-zeroing randomness, so the two asserts together guarantee a
|
|
73
|
+
* non-trivial mask. The remaining obligations the circuit CANNOT enforce:
|
|
74
|
+
* every `r` MUST be a valid Jubjub scalar (`< ℓ`, the subgroup order; larger
|
|
75
|
+
* values fault `ecMul`), and MUST be fresh and secret. Reusing an `r` under the
|
|
76
|
+
* same `pk` leaks the difference of the two plaintexts (`c1` collides, so
|
|
77
|
+
* `c2 - c2' = m - m'`). Prefer `expandRandomness` with a distinct tag per
|
|
78
|
+
* encryption.
|
|
79
|
+
*/
|
|
80
|
+
module ElGamal {
|
|
81
|
+
import CompactStandardLibrary;
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Types
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @description Single-receiver ElGamal ciphertext over Jubjub.
|
|
89
|
+
* c1 = g^r
|
|
90
|
+
* c2 = pk^r * m (lifted: m = g^v)
|
|
91
|
+
*/
|
|
92
|
+
export struct Ciphertext {
|
|
93
|
+
c1: JubjubPoint;
|
|
94
|
+
c2: JubjubPoint;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Key / scalar derivation
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @description Maps a 32-byte secret to a valid Jubjub scalar. See the
|
|
103
|
+
* module-level hash-to-scalar note for why `degradeToTransient` is required.
|
|
104
|
+
*
|
|
105
|
+
* @param secret - The 32-byte secret to map.
|
|
106
|
+
* @return A Field guaranteed to be a valid Jubjub scalar.
|
|
107
|
+
*/
|
|
108
|
+
export pure circuit secretToScalar(secret: Bytes<32>): Field {
|
|
109
|
+
return degradeToTransient(persistentHash<Vector<1, Bytes<32>>>([secret]));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @description Derives the ElGamal public key `pk = g^secretToScalar(ek)`
|
|
114
|
+
* from a secret `ek`.
|
|
115
|
+
*
|
|
116
|
+
* @param ek - The 32-byte encryption secret.
|
|
117
|
+
* @return The corresponding public key point.
|
|
118
|
+
*/
|
|
119
|
+
export pure circuit derivePk(ek: Bytes<32>): JubjubPoint {
|
|
120
|
+
return ecMulGenerator(secretToScalar(ek));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @description Deterministically expands a 32-byte seed into a Jubjub scalar
|
|
125
|
+
* tagged by a distinguishing string, so a single witness seed can yield many
|
|
126
|
+
* distinct randomness values within one circuit invocation. Distinct tags
|
|
127
|
+
* always produce distinct outputs (collision resistance of the hash); the
|
|
128
|
+
* caller is responsible for using distinct tags.
|
|
129
|
+
*
|
|
130
|
+
* @param seed - The seed to expand.
|
|
131
|
+
* @param tag - A 32-byte tag distinguishing this expansion from its siblings.
|
|
132
|
+
* @return A Field suitable for use as encryption randomness.
|
|
133
|
+
*/
|
|
134
|
+
export pure circuit expandRandomness(seed: Bytes<32>, tag: Bytes<32>): Field {
|
|
135
|
+
return degradeToTransient(persistentHash<Vector<2, Bytes<32>>>([seed, tag]));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Curve helpers
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @description The order of the Jubjub prime-order subgroup (`EmbeddedFr`
|
|
144
|
+
* modulus `ℓ`), minus one. Used as the scalar for point negation: for any `P`
|
|
145
|
+
* in the subgroup, `(ℓ - 1) * P = ℓ*P - P = O - P = -P`.
|
|
146
|
+
*
|
|
147
|
+
* @notice The negation identity this enables (`P + (ℓ-1)*P == O`) is exercised
|
|
148
|
+
* by the `negate` round-trip tests. Compact has no module-level constants, so
|
|
149
|
+
* the value is exposed as a pure circuit returning the literal.
|
|
150
|
+
*/
|
|
151
|
+
pure circuit JUBJUB_SUBGROUP_ORDER_MINUS_ONE(): Field {
|
|
152
|
+
return 6554484396890773809930967563523245729705921265872317281365359162392183254198 as Field;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @description Negates a Jubjub point.
|
|
157
|
+
*
|
|
158
|
+
* @notice Implemented as scalar multiplication by `ℓ-1` (via
|
|
159
|
+
* `JUBJUB_SUBGROUP_ORDER_MINUS_ONE`) rather than the obvious `ecMul(p, -1)`:
|
|
160
|
+
* the scalar `-1` computed in the `Field` type is `BLS_modulus - 1`, which
|
|
161
|
+
* exceeds the Jubjub scalar field order and faults `ecMul` ("failed to decode
|
|
162
|
+
* for built-in type EmbeddedFr"). (Coordinate negation `(-x, y)` via
|
|
163
|
+
* `constructJubjubPoint`/`jubjubPointX`/`jubjubPointY` is an equivalent
|
|
164
|
+
* alternative that does not depend on the subgroup assumption.) The
|
|
165
|
+
* `(ℓ-1)*P = -P` identity holds only in the prime-order subgroup, but that
|
|
166
|
+
* always applies here: the runtime constrains every assigned `JubjubPoint`
|
|
167
|
+
* into the prime-order subgroup (the embedded-curve gadget assigns points via
|
|
168
|
+
* cofactor clearing), so no in-subgroup precondition needs to be checked or
|
|
169
|
+
* assumed. See the module-level subgroup note.
|
|
170
|
+
*/
|
|
171
|
+
pure circuit ecNeg(p: JubjubPoint): JubjubPoint {
|
|
172
|
+
return ecMul(p, JUBJUB_SUBGROUP_ORDER_MINUS_ONE());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Encryption & homomorphic operations
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @description The identity ciphertext `Enc(0)` (both components are the
|
|
181
|
+
* curve identity). Useful for initializing a fresh balance.
|
|
182
|
+
*
|
|
183
|
+
* @notice This is a fixed, deterministic value `(O, O)` — NOT a hiding
|
|
184
|
+
* encryption. It is publicly recognizable, so a slot left at `encryptZero` is
|
|
185
|
+
* distinguishable as never-written. Rerandomize (or credit with fresh `r`)
|
|
186
|
+
* before treating it as private.
|
|
187
|
+
*/
|
|
188
|
+
export pure circuit encryptZero(): Ciphertext {
|
|
189
|
+
const id = ecMulGenerator(0 as Field);
|
|
190
|
+
return Ciphertext { c1: id, c2: id };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @description Encrypts an arbitrary message point `m` under `pk` with
|
|
195
|
+
* randomness `r`, producing the ciphertext `(g^r, pk^r * m)`. This is the
|
|
196
|
+
* general (non-lifted) ElGamal encryption; `encrypt` is the lifted special
|
|
197
|
+
* case `encryptPoint(pk, g^value, r)`.
|
|
198
|
+
*
|
|
199
|
+
* @notice `m` (like `pk`) is constrained into the prime-order subgroup by the
|
|
200
|
+
* runtime (see the module-level subgroup note), so no in-circuit membership
|
|
201
|
+
* check is performed. `pk` is asserted non-identity and `r` non-zero — the
|
|
202
|
+
* two inputs that would zero the mask and expose `m` (see the weak-keys and
|
|
203
|
+
* randomness notes). Decrypting `m` is only feasible when it is a known or
|
|
204
|
+
* small-exponent point; the caller owns that concern (the lifted `encrypt`
|
|
205
|
+
* bounds it via the discrete-log range).
|
|
206
|
+
*
|
|
207
|
+
* Requirements:
|
|
208
|
+
*
|
|
209
|
+
* - `pk` is not the identity point.
|
|
210
|
+
* - `r` is non-zero.
|
|
211
|
+
*
|
|
212
|
+
* @param pk - The recipient public key.
|
|
213
|
+
* @param m - The message point to encrypt.
|
|
214
|
+
* @param r - The encryption randomness.
|
|
215
|
+
* @return The ciphertext `(g^r, pk^r * m)`.
|
|
216
|
+
*/
|
|
217
|
+
export pure circuit encryptPoint(pk: JubjubPoint, m: JubjubPoint, r: Field): Ciphertext {
|
|
218
|
+
assert(pk != ecMulGenerator(0 as Field), "ElGamal: identity pk");
|
|
219
|
+
assert(r != 0 as Field, "ElGamal: zero randomness");
|
|
220
|
+
const c1 = ecMulGenerator(r);
|
|
221
|
+
const mask = ecMul(pk, r);
|
|
222
|
+
const c2 = ecAdd(mask, m);
|
|
223
|
+
return Ciphertext { c1: c1, c2: c2 };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @description Produces a fresh ciphertext of `value` under `pk` with
|
|
228
|
+
* randomness `r`, lifting `value` to the point `g^value`. Randomness is taken
|
|
229
|
+
* as a `Field` directly to avoid a redundant `Bytes<32> -> Field` conversion
|
|
230
|
+
* at every call site; `expandRandomness` produces `Field` for the same reason.
|
|
231
|
+
*
|
|
232
|
+
* Requirements:
|
|
233
|
+
*
|
|
234
|
+
* - `pk` is not the identity point.
|
|
235
|
+
* - `r` is non-zero.
|
|
236
|
+
*
|
|
237
|
+
* @param pk - The recipient public key.
|
|
238
|
+
* @param value - The plaintext value to encrypt.
|
|
239
|
+
* @param r - The encryption randomness.
|
|
240
|
+
* @return The ciphertext `(g^r, pk^r * g^value)`.
|
|
241
|
+
*/
|
|
242
|
+
export pure circuit encrypt(pk: JubjubPoint, value: Uint<128>, r: Field): Ciphertext {
|
|
243
|
+
return encryptPoint(pk, ecMulGenerator(value as Field), r);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @description Negates a ciphertext componentwise: if `ct` encrypts `v` then
|
|
248
|
+
* the result encrypts `-v` under the same key (with negated randomness).
|
|
249
|
+
* This is the building block `sub` rests on.
|
|
250
|
+
*
|
|
251
|
+
* @param ct - The ciphertext to negate.
|
|
252
|
+
* @return A ciphertext of `-v`.
|
|
253
|
+
*/
|
|
254
|
+
export pure circuit negate(ct: Ciphertext): Ciphertext {
|
|
255
|
+
return Ciphertext { c1: ecNeg(ct.c1), c2: ecNeg(ct.c2) };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @description Homomorphically adds two ciphertexts: `Enc(a) + Enc(b)`
|
|
260
|
+
* decrypts to `a + b`. This is the defining additive-homomorphism primitive
|
|
261
|
+
* and combines two independently produced ciphertexts componentwise.
|
|
262
|
+
*
|
|
263
|
+
* @notice Both inputs MUST be encrypted under the same public key for the
|
|
264
|
+
* result to decrypt under that key's secret; combining ciphertexts under
|
|
265
|
+
* different keys yields a ciphertext no single key can open. The randomness
|
|
266
|
+
* of the result is the sum of the inputs' randomness, so the result is a
|
|
267
|
+
* valid fresh ciphertext under that key.
|
|
268
|
+
*
|
|
269
|
+
* @param a - The first ciphertext.
|
|
270
|
+
* @param b - The second ciphertext (same recipient key as `a`).
|
|
271
|
+
* @return A ciphertext of `a + b`.
|
|
272
|
+
*/
|
|
273
|
+
export pure circuit add(a: Ciphertext, b: Ciphertext): Ciphertext {
|
|
274
|
+
return Ciphertext { c1: ecAdd(a.c1, b.c1), c2: ecAdd(a.c2, b.c2) };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @description Homomorphically subtracts two ciphertexts: `Enc(a) - Enc(b)`
|
|
279
|
+
* decrypts to `a - b`. Implemented as `add(a, negate(b))`.
|
|
280
|
+
*
|
|
281
|
+
* @notice Same-key requirement as `add`. The caller MUST ensure the
|
|
282
|
+
* underlying plaintext subtraction does not underflow; this circuit cannot
|
|
283
|
+
* check it (see `subEncrypted`).
|
|
284
|
+
*
|
|
285
|
+
* @param a - The minuend ciphertext.
|
|
286
|
+
* @param b - The subtrahend ciphertext (same recipient key as `a`).
|
|
287
|
+
* @return A ciphertext of `a - b`.
|
|
288
|
+
*/
|
|
289
|
+
export pure circuit sub(a: Ciphertext, b: Ciphertext): Ciphertext {
|
|
290
|
+
return add(a, negate(b));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @description Homomorphically multiplies the plaintext of `ct` by a public
|
|
295
|
+
* scalar `k`: if `ct` encrypts `v` then the result encrypts `k * v` under the
|
|
296
|
+
* same key. Both components are scaled by `k`, so the randomness scales to
|
|
297
|
+
* `k * r` and the result is a valid fresh ciphertext under that key.
|
|
298
|
+
*
|
|
299
|
+
* This is the scalar-multiplication leg of the homomorphism, and the only way
|
|
300
|
+
* to express linear combinations: a weighted sum `Enc(3a + 5b)` is
|
|
301
|
+
* `add(scalarMul(ca, 3), scalarMul(cb, 5))`, which repeated `add` cannot do
|
|
302
|
+
* for large weights.
|
|
303
|
+
*
|
|
304
|
+
* @notice `k` MUST be a valid Jubjub scalar (less than the subgroup order);
|
|
305
|
+
* larger values fault `ecMul` (see the module-level hash-to-scalar note). The
|
|
306
|
+
* caller also remains responsible for the discrete-log bound — `k * v` must
|
|
307
|
+
* stay within the range a recovery search can clear.
|
|
308
|
+
*
|
|
309
|
+
* @param ct - The ciphertext whose plaintext is scaled.
|
|
310
|
+
* @param k - The public scalar multiplier (a valid Jubjub scalar).
|
|
311
|
+
* @return A ciphertext of `k * v`.
|
|
312
|
+
*/
|
|
313
|
+
export pure circuit scalarMul(ct: Ciphertext, k: Field): Ciphertext {
|
|
314
|
+
return Ciphertext { c1: ecMul(ct.c1, k), c2: ecMul(ct.c2, k) };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @description Homomorphically adds `value` to the plaintext encrypted by
|
|
319
|
+
* `old`, producing a fresh ciphertext under `pk` with new randomness `r`.
|
|
320
|
+
* Convenience over `add` for folding a known plaintext into a ciphertext:
|
|
321
|
+
* `add(old, encrypt(pk, value, r))`.
|
|
322
|
+
*
|
|
323
|
+
* Requirements:
|
|
324
|
+
*
|
|
325
|
+
* - `pk` is not the identity point.
|
|
326
|
+
* - `r` is non-zero.
|
|
327
|
+
*/
|
|
328
|
+
export pure circuit addEncrypted(
|
|
329
|
+
old: Ciphertext,
|
|
330
|
+
pk: JubjubPoint,
|
|
331
|
+
value: Uint<128>,
|
|
332
|
+
r: Field
|
|
333
|
+
): Ciphertext {
|
|
334
|
+
return add(old, encrypt(pk, value, r));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @description Homomorphically subtracts `value` from the plaintext encrypted
|
|
339
|
+
* by `old`. Convenience over `sub`: `sub(old, encrypt(pk, value, r))`.
|
|
340
|
+
*
|
|
341
|
+
* @notice The caller MUST ensure the underlying plaintext subtraction does
|
|
342
|
+
* not underflow; this circuit cannot check it. Callers verify sufficiency by
|
|
343
|
+
* reading the plaintext via their own witness and asserting it with
|
|
344
|
+
* `assertDecryptsTo` before calling.
|
|
345
|
+
*
|
|
346
|
+
* Requirements:
|
|
347
|
+
*
|
|
348
|
+
* - `pk` is not the identity point.
|
|
349
|
+
* - `r` is non-zero.
|
|
350
|
+
*/
|
|
351
|
+
export pure circuit subEncrypted(
|
|
352
|
+
old: Ciphertext,
|
|
353
|
+
pk: JubjubPoint,
|
|
354
|
+
value: Uint<128>,
|
|
355
|
+
r: Field
|
|
356
|
+
): Ciphertext {
|
|
357
|
+
return sub(old, encrypt(pk, value, r));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @description Rerandomizes `ct`: returns a fresh ciphertext of the same
|
|
362
|
+
* plaintext under the same key `pk`, using new randomness `r`. The output is
|
|
363
|
+
* unlinkable to the input (it is a uniformly fresh encryption of the same
|
|
364
|
+
* value) yet decrypts identically. Equivalent to homomorphically adding an
|
|
365
|
+
* encryption of zero.
|
|
366
|
+
*
|
|
367
|
+
* @notice `r = 0` is rejected (it would be a no-op via the `encryptPoint`
|
|
368
|
+
* zero-randomness assert). One silent failure mode remains that the circuit
|
|
369
|
+
* cannot catch: a `pk` that is not the key `ct` was encrypted under corrupts
|
|
370
|
+
* the plaintext (the result no longer decrypts to the original value). Pass
|
|
371
|
+
* fresh randomness and the matching `pk`.
|
|
372
|
+
*
|
|
373
|
+
* Requirements:
|
|
374
|
+
*
|
|
375
|
+
* - `pk` is not the identity point.
|
|
376
|
+
* - `r` is non-zero.
|
|
377
|
+
*
|
|
378
|
+
* @param ct - The ciphertext to rerandomize.
|
|
379
|
+
* @param pk - The recipient public key `ct` is encrypted under.
|
|
380
|
+
* @param r - Fresh, nonzero randomness for the rerandomization.
|
|
381
|
+
* @return A fresh ciphertext of the same plaintext.
|
|
382
|
+
*/
|
|
383
|
+
export pure circuit rerandomize(ct: Ciphertext, pk: JubjubPoint, r: Field): Ciphertext {
|
|
384
|
+
return add(ct, encrypt(pk, 0, r));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// Verification
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* @description Asserts that `ek` is the secret for `pk`, i.e.
|
|
393
|
+
* `derivePk(ek) == pk`. Lets a caller prove ownership of a public key
|
|
394
|
+
* without touching any ciphertext or plaintext.
|
|
395
|
+
*
|
|
396
|
+
* Requirements:
|
|
397
|
+
*
|
|
398
|
+
* - `ek` is the secret for `pk` (`derivePk(ek) == pk`).
|
|
399
|
+
*
|
|
400
|
+
* @param pk - The public key to check.
|
|
401
|
+
* @param ek - The secret claimed to correspond to `pk`.
|
|
402
|
+
*/
|
|
403
|
+
export pure circuit assertKeyPair(pk: JubjubPoint, ek: Bytes<32>): [] {
|
|
404
|
+
assert(derivePk(ek) == pk,
|
|
405
|
+
"ElGamal: ek/pk mismatch");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* @description Asserts that `ct` decrypts under `(pk, ek)` to the message
|
|
410
|
+
* point `m`, and that `ek` is the secret for `pk`. This is the general
|
|
411
|
+
* (non-lifted) verification; `assertDecryptsTo` is the lifted special case
|
|
412
|
+
* with `m = g^claimedValue`.
|
|
413
|
+
*
|
|
414
|
+
* Requirements:
|
|
415
|
+
*
|
|
416
|
+
* - `ek` is the secret for `pk`.
|
|
417
|
+
* - `ct` decrypts under `(pk, ek)` to `m`.
|
|
418
|
+
*
|
|
419
|
+
* @param ct - The ciphertext to verify.
|
|
420
|
+
* @param pk - The public key `ct` is expected to be encrypted under.
|
|
421
|
+
* @param ek - The secret claimed to correspond to `pk`.
|
|
422
|
+
* @param m - The message point `ct` is claimed to encrypt.
|
|
423
|
+
*/
|
|
424
|
+
export pure circuit assertDecryptsToPoint(
|
|
425
|
+
ct: Ciphertext,
|
|
426
|
+
pk: JubjubPoint,
|
|
427
|
+
ek: Bytes<32>,
|
|
428
|
+
m: JubjubPoint
|
|
429
|
+
): [] {
|
|
430
|
+
// Inlines the same key check as `assertKeyPair` (rather than calling it) so
|
|
431
|
+
// `ekField` is computed once and reused for the decryption below, avoiding
|
|
432
|
+
// a second `secretToScalar` hash in this hot verification path.
|
|
433
|
+
const ekField = secretToScalar(ek);
|
|
434
|
+
assert(ecMulGenerator(ekField) == pk,
|
|
435
|
+
"ElGamal: ek/pk mismatch");
|
|
436
|
+
|
|
437
|
+
// m = c2 - ek*c1. ek*c1 is negated via ecNeg (scalar mul by ℓ-1) rather
|
|
438
|
+
// than ecMul by a negative scalar, which would exceed the Jubjub scalar
|
|
439
|
+
// field. ekField < ℓ (the subgroup order), so ecMul(ct.c1, ekField) is a
|
|
440
|
+
// valid scalar mul.
|
|
441
|
+
const ekC1 = ecMul(ct.c1, ekField);
|
|
442
|
+
const mPoint = ecAdd(ct.c2, ecNeg(ekC1));
|
|
443
|
+
assert(mPoint == m,
|
|
444
|
+
"ElGamal: plaintext mismatch");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @description Asserts that `ct` decrypts under `(pk, ek)` to `claimedValue`,
|
|
449
|
+
* and that `ek` is the secret for `pk`. This is the in-circuit verification
|
|
450
|
+
* path an importing module uses to bind its witness-supplied `ek` and cached
|
|
451
|
+
* plaintext to publicly stored state: a wrong `ek` fails the key check, a
|
|
452
|
+
* wrong plaintext fails the decryption check. Lifted special case of
|
|
453
|
+
* `assertDecryptsToPoint` with the message point `g^claimedValue`.
|
|
454
|
+
*
|
|
455
|
+
* Requirements:
|
|
456
|
+
*
|
|
457
|
+
* - `ek` is the secret for `pk`.
|
|
458
|
+
* - `ct` decrypts under `(pk, ek)` to `claimedValue`.
|
|
459
|
+
*
|
|
460
|
+
* @param ct - The ciphertext to verify.
|
|
461
|
+
* @param pk - The public key `ct` is expected to be encrypted under.
|
|
462
|
+
* @param ek - The secret claimed to correspond to `pk`.
|
|
463
|
+
* @param claimedValue - The plaintext `ct` is claimed to encrypt.
|
|
464
|
+
*/
|
|
465
|
+
export pure circuit assertDecryptsTo(
|
|
466
|
+
ct: Ciphertext,
|
|
467
|
+
pk: JubjubPoint,
|
|
468
|
+
ek: Bytes<32>,
|
|
469
|
+
claimedValue: Uint<128>
|
|
470
|
+
): [] {
|
|
471
|
+
assertDecryptsToPoint(ct, pk, ek, ecMulGenerator(claimedValue as Field));
|
|
472
|
+
}
|
|
473
|
+
}
|