@openzeppelin/compact-contracts 0.1.0

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.
@@ -0,0 +1,353 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.1.0 (access/Ownable.compact)
3
+
4
+ pragma language_version >= 0.21.0;
5
+
6
+ /**
7
+ * @module Ownable
8
+ * @description An unshielded Ownable library.
9
+ * This module provides a basic access control mechanism, where there is an owner
10
+ * that can be granted exclusive access to specific circuits.
11
+ * This approach is perfectly reasonable for contracts that have a single administrative user.
12
+ *
13
+ * Authorization is based on a witness-derived identity scheme. The owner proves knowledge
14
+ * of a secret key by injecting it via the `wit_OwnableSK` witness. The module computes
15
+ * an account identifier as `persistentHash(secretKey)` which is a commitment that hides the key
16
+ * while providing a stable, pseudonymous on-chain identity.
17
+ *
18
+ * Because the account identifier is `H(secretKey)` with no per-deployment salt or domain
19
+ * separator, the same secret key produces the same identity across all contracts. This is
20
+ * intentional. It provides a linkable pseudonymous identity analogous to Solidity's
21
+ * `msg.sender`. Users who desire cross-contract unlinkability can use different secret keys
22
+ * per contract at the wallet layer.
23
+ *
24
+ * The initial owner must be set by using the `initialize` circuit during construction.
25
+ * This can later be changed with `transferOwnership`.
26
+ *
27
+ * @notice Ownership can only be transferred to Bytes<32> account identifiers
28
+ * through the main transfer circuits (`transferOwnership` and `_transferOwnership`).
29
+ * In other words, ownership transfers to contract addresses are disallowed through these
30
+ * circuits.
31
+ * This is because Compact currently does not support contract-to-contract calls which means
32
+ * if a contract is granted ownership, the owner contract cannot directly call the protected
33
+ * circuit.
34
+ *
35
+ * @notice This module does offer experimental circuits that allow ownership to be granted
36
+ * to contract addresses (`_unsafeTransferOwnership` and `_unsafeUncheckedTransferOwnership`).
37
+ * Note that the circuit names are very explicit ("unsafe") with these experimental circuits.
38
+ * Until contract-to-contract calls are supported, there is no direct way for a contract
39
+ * to authenticate as an owner.
40
+ *
41
+ * @notice The unsafe circuits are planned to become deprecated once contract-to-contract calls
42
+ * are supported.
43
+ *
44
+ * @dev Migration plan (safe -> C2C-capable ownership transfers)
45
+ *
46
+ * The owner type is the final `Either<Bytes<32>, ContractAddress>` from day one, in both circuit
47
+ * signatures and the `_owner` ledger field. This is deliberate. Midnight's upgrade mechanism, the
48
+ * Contract Maintenance Authority (CMA), can rotate a circuit's verifier key but cannot migrate
49
+ * ledger state (there is no state-rewrite primitive; see the `updatability` tutorial). Fixing the
50
+ * type now makes the future migration a pure verifier-key rotation with zero ledger-state change,
51
+ * which is the only kind of upgrade the CMA supports. Widening the type later would instead force
52
+ * a ledger-layout rewrite, which the CMA cannot perform.
53
+ *
54
+ * When C2C lands, an already-deployed contract migrates without changing its address or ledger
55
+ * state:
56
+ *
57
+ * - Minor release: mark `_unsafeTransferOwnership` (and `_unsafeUncheckedTransferOwnership`)
58
+ * deprecated (and emit a warning if possible); keep their implementation intact so existing
59
+ * callers continue to work.
60
+ * - Major release: ship `transferOwnership` / `_transferOwnership` with the `isContract` guard
61
+ * removed and drop the `_unsafe` variants. Operators adopt the new logic by signing a CMA
62
+ * `MaintenanceUpdate` that rotates the affected verifier keys (`VerifierKeyRemove` +
63
+ * `VerifierKeyInsert` for each guarded circuit, and a `VerifierKeyRemove` for each `_unsafe`
64
+ * variant). The contract address and all ledger state are preserved. Because the CMA allows
65
+ * multiple active VK versions per operation, old and new keys can coexist during a transition
66
+ * window so in-flight proofs still verify. By this point, anyone using the `_unsafe` variants
67
+ * should have migrated to the now C2C-capable circuits.
68
+ *
69
+ * @dev Canonicalization
70
+ * All `Either<Bytes<32>, ContractAddress>` values are canonicalized before being written
71
+ * to the `_owner` ledger field. Canonicalization zeroes out the inactive branch of the Either,
72
+ * ensuring consistent storage regardless of what data the inactive branch carries.
73
+ * The single write point is `_unsafeUncheckedTransferOwnership`, through which all
74
+ * ownership changes flow.
75
+ *
76
+ * @dev Security Considerations:
77
+ * - The `secretKey` must be kept private. Loss of the key prevents the owner
78
+ * from proving ownership. Key exposure allows impersonation.
79
+ * - It is strongly recommended to use cryptographically secure random values for the secret key
80
+ * (e.g., `crypto.getRandomValues()`). Weak or predictable keys can be brute-forced.
81
+ * - The `secretKey` is provided via the `wit_OwnableSK` witness. As with all witnesses,
82
+ * the contract must not assume that the witness implementation matches the developer's code.
83
+ * Any DApp may provide any implementation. The ZK proof system constrains what values
84
+ * can produce valid proofs.
85
+ */
86
+ module Ownable {
87
+ import CompactStandardLibrary;
88
+ import "../security/Initializable" prefix Initializable_;
89
+ import "../utils/Utils" prefix Utils_;
90
+
91
+ export ledger _owner: Either<Bytes<32>, ContractAddress>;
92
+
93
+ /**
94
+ * @witness wit_OwnableSK
95
+ * @description Returns the caller's secret key used in deriving the account identifier.
96
+ *
97
+ * The same key produces the same account identifier across all contracts. Users who
98
+ * desire cross-contract unlinkability should use different keys per contract.
99
+ *
100
+ * @returns {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
101
+ */
102
+ witness wit_OwnableSK(): Bytes<32>;
103
+
104
+ /**
105
+ * @description Initializes the contract by setting the `initialOwner`.
106
+ * This must be called in the contract's constructor.
107
+ *
108
+ * @circuitInfo k=10, rows=626
109
+ *
110
+ * Requirements:
111
+ *
112
+ * - Contract is not already initialized.
113
+ * - `initialOwner` is not a ContractAddress.
114
+ * - `initialOwner` is not the zero address.
115
+ *
116
+ * @param {Either<Bytes<32>, ContractAddress>} initialOwner - The initial owner of the contract.
117
+ * @returns {[]} Empty tuple.
118
+ */
119
+ export circuit initialize(initialOwner: Either<Bytes<32>, ContractAddress>): [] {
120
+ Initializable_initialize();
121
+ assert(!_isTargetZero(initialOwner), "Ownable: invalid initial owner");
122
+ _transferOwnership(initialOwner);
123
+ }
124
+
125
+ /**
126
+ * @description Returns the current contract owner.
127
+ *
128
+ * @circuitInfo k=7, rows=76
129
+ *
130
+ * Requirements:
131
+ *
132
+ * - Contract is initialized.
133
+ *
134
+ * @returns {Either<Bytes<32>, ContractAddress>} - The contract owner.
135
+ */
136
+ export circuit owner(): Either<Bytes<32>, ContractAddress> {
137
+ Initializable_assertInitialized();
138
+ return _owner;
139
+ }
140
+
141
+ /**
142
+ * @description Transfers ownership of the contract to `newOwner`.
143
+ *
144
+ * @notice Ownership transfers to contract addresses are currently disallowed until contract-to-contract
145
+ * interactions are supported in Compact.
146
+ * This restriction prevents permanently disabling access to a circuit.
147
+ *
148
+ * @circuitInfo k=13, rows=2959
149
+ *
150
+ * Requirements:
151
+ *
152
+ * - Contract is initialized.
153
+ * - The caller is the current contract owner.
154
+ * - `newOwner` is not a ContractAddress.
155
+ * - `newOwner` is not the zero address.
156
+ *
157
+ * @param {Either<Bytes<32>, ContractAddress>} newOwner - The new owner.
158
+ * @returns {[]} Empty tuple.
159
+ */
160
+ export circuit transferOwnership(newOwner: Either<Bytes<32>, ContractAddress>): [] {
161
+ Initializable_assertInitialized();
162
+ const isContractAddr = !newOwner.is_left;
163
+ assert(!isContractAddr, "Ownable: unsafe ownership transfer");
164
+ _unsafeTransferOwnership(newOwner);
165
+ }
166
+
167
+ /**
168
+ * @description Unsafe variant of `transferOwnership`.
169
+ *
170
+ * @warning Ownership transfers to contract addresses are considered unsafe because contract-to-contract
171
+ * calls are not currently supported.
172
+ * Ownership privileges sent to a contract address may become uncallable.
173
+ * Once contract-to-contract calls are supported, this circuit may be deprecated.
174
+ *
175
+ * @circuitInfo k=13, rows=2956
176
+ *
177
+ * Requirements:
178
+ *
179
+ * - Contract is initialized.
180
+ * - The caller is the current contract owner.
181
+ * - `newOwner` is not the zero address.
182
+ *
183
+ * @param {Either<Bytes<32>, ContractAddress>} newOwner - The new owner.
184
+ * @returns {[]} Empty tuple.
185
+ */
186
+ export circuit _unsafeTransferOwnership(newOwner: Either<Bytes<32>, ContractAddress>): [] {
187
+ Initializable_assertInitialized();
188
+ assertOnlyOwner();
189
+ assert(!_isTargetZero(newOwner), "Ownable: invalid new owner");
190
+ _unsafeUncheckedTransferOwnership(newOwner);
191
+ }
192
+
193
+ /**
194
+ * @description Leaves the contract without an owner.
195
+ * It will not be possible to call `assertOnlyOwner` circuits anymore.
196
+ * Can only be called by the current owner.
197
+ *
198
+ * @circuitInfo k=13, rows=2364
199
+ *
200
+ * Requirements:
201
+ *
202
+ * - Contract is initialized.
203
+ * - The caller is the current contract owner.
204
+ *
205
+ * @returns {[]} Empty tuple.
206
+ */
207
+ export circuit renounceOwnership(): [] {
208
+ Initializable_assertInitialized();
209
+ assertOnlyOwner();
210
+
211
+ // We use `left` here because `assertOnlyOwner` would return a misleading
212
+ // error msg (c2c is not currently supported)
213
+ const zero = Either<Bytes<32>, ContractAddress> {
214
+ is_left: true, left: default<Bytes<32>>, right: default<ContractAddress>
215
+ };
216
+ _unsafeUncheckedTransferOwnership(zero);
217
+ }
218
+
219
+ /**
220
+ * @description Throws if called by any account other than the owner.
221
+ * Use this to restrict access of specific circuits to the owner.
222
+ *
223
+ * In the case of an external (non-contract) caller, the caller’s identity is
224
+ * derived from the `wit_OwnableSK` witness as `persistentHash(secretKey)`.
225
+ *
226
+ * @circuitInfo k=13, rows=2360
227
+ *
228
+ * Requirements:
229
+ *
230
+ * - Contract is initialized.
231
+ * - The caller is the current contract owner.
232
+ *
233
+ * @returns {[]} Empty tuple.
234
+ */
235
+ export circuit assertOnlyOwner(): [] {
236
+ Initializable_assertInitialized();
237
+ if (_owner.is_left) {
238
+ assert(_computeAccountId() == _owner.left, "Ownable: caller is not the owner");
239
+ } else {
240
+ assert(false, "Ownable: contract address owner authentication is not yet supported");
241
+ }
242
+ }
243
+
244
+ /**
245
+ * @description Transfers ownership of the contract to `newOwner` without
246
+ * enforcing permission checks on the caller.
247
+ *
248
+ * @notice Ownership transfers to contract addresses are currently disallowed until contract-to-contract
249
+ * interactions are supported in Compact.
250
+ * This restriction prevents circuits from being inadvertently locked in contracts.
251
+ *
252
+ * @circuitInfo k=10, rows=600
253
+ *
254
+ * Requirements:
255
+ *
256
+ * - Contract is initialized.
257
+ * - `newOwner` is not a ContractAddress.
258
+ *
259
+ * @param {Either<Bytes<32>, ContractAddress>} newOwner - The new owner.
260
+ * @returns {[]} Empty tuple.
261
+ */
262
+ export circuit _transferOwnership(newOwner: Either<Bytes<32>, ContractAddress>): [] {
263
+ Initializable_assertInitialized();
264
+ const isContractAddr = !newOwner.is_left;
265
+ assert(!isContractAddr, "Ownable: unsafe ownership transfer");
266
+ _unsafeUncheckedTransferOwnership(newOwner);
267
+ }
268
+
269
+ /**
270
+ * @description Unsafe variant of `_transferOwnership`.
271
+ *
272
+ * @warning Ownership transfers to contract addresses are considered unsafe because contract-to-contract
273
+ * calls are not currently supported.
274
+ * Ownership privileges sent to a contract address may become uncallable.
275
+ * Once contract-to-contract calls are supported, this circuit may be deprecated.
276
+ *
277
+ * @circuitInfo k=10, rows=597
278
+ *
279
+ * Requirements:
280
+ *
281
+ * - Contract is initialized.
282
+ *
283
+ * @param {Either<Bytes<32>, ContractAddress>} newOwner - The new owner.
284
+ * @returns {[]} Empty tuple.
285
+ */
286
+ export circuit _unsafeUncheckedTransferOwnership(
287
+ newOwner: Either<Bytes<32>, ContractAddress>
288
+ ): [] {
289
+ Initializable_assertInitialized();
290
+ const canonAcct = Utils_canonicalize<Bytes<32>, ContractAddress>(newOwner);
291
+ _owner = disclose(canonAcct);
292
+ }
293
+
294
+ /**
295
+ * @description Computes the caller's account identifier from the `wit_OwnableSK` witness.
296
+ *
297
+ * ## ID Derivation
298
+ * `accountId = persistentHash(secretKey)`
299
+ *
300
+ * The result is a 32-byte commitment that uniquely identifies the caller.
301
+ *
302
+ * @returns {Bytes<32>} accountId - The computed account identifier.
303
+ */
304
+ circuit _computeAccountId(): Bytes<32> {
305
+ return computeAccountId(wit_OwnableSK());
306
+ }
307
+
308
+ /**
309
+ * @description Computes an account identifier without on-chain state, allowing a user to derive
310
+ * their identity commitment before submitting it in an ownership transfer.
311
+ * This is the off-chain counterpart to {_computeAccountId} and produces an identical result
312
+ * given the same inputs.
313
+ *
314
+ * @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
315
+ * permanently compromise the security of this system:
316
+ *
317
+ * - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
318
+ * application logs, analytics pipelines, or any observable side-channel.
319
+ * - **Store offline or in secure enclaves** — hardware security modules (HSMs),
320
+ * air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
321
+ * - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
322
+ * or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
323
+ * - **Treat key loss as identity loss** — a lost key cannot be recovered.
324
+ * - **Avoid calling this circuit in untrusted environments** — executing this in an
325
+ * unverified browser extension, compromised runtime, or shared machine may expose
326
+ * the key to a malicious observer.
327
+ *
328
+ * ## ID Derivation
329
+ * `accountId = persistentHash(secretKey)`
330
+ *
331
+ * @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
332
+ *
333
+ * @returns {Bytes<32>} accountId - The computed account identifier.
334
+ */
335
+ export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
336
+ return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
337
+ }
338
+
339
+ /**
340
+ * @description Returns `true` if `target`'s active branch (as indicated by `is_left`)
341
+ * holds the zero value.
342
+ *
343
+ * @param {Either<Bytes<32>, ContractAddress>} target - The value to check.
344
+ * @returns {Boolean} - `true` if the active branch is zero, `false` otherwise.
345
+ */
346
+ circuit _isTargetZero(target: Either<Bytes<32>, ContractAddress>): Boolean {
347
+ if (target.is_left) {
348
+ return target.left == default<Bytes<32>>;
349
+ } else {
350
+ return target.right == default<ContractAddress>;
351
+ }
352
+ }
353
+ }