@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.
- package/README.md +278 -0
- package/access/AccessControl.compact +479 -0
- package/access/Ownable.compact +353 -0
- package/access/ShieldedAccessControl.compact +784 -0
- package/access/ZOwnablePK.compact +304 -0
- package/package.json +54 -0
- package/security/Initializable.compact +60 -0
- package/security/Pausable.compact +89 -0
- package/token/FungibleToken.compact +742 -0
- package/token/MultiToken.compact +728 -0
- package/token/NonFungibleToken.compact +954 -0
- package/utils/Utils.compact +98 -0
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.1.0 (access/ShieldedAccessControl.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.21.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module ShieldedAccessControl
|
|
8
|
+
* @description A Shielded AccessControl library.
|
|
9
|
+
*
|
|
10
|
+
* This module provides a shielded role-based access control (RBAC) mechanism, where roles can be used to
|
|
11
|
+
* represent a set of permissions. Roles are stored as Merkle tree commitments to avoid
|
|
12
|
+
* disclosing information about role holders. Role commitments are created with the following
|
|
13
|
+
* hashing scheme, where `‖` denotes concatenation and all values are `Bytes<32>`:
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* roleCommitment := SHA256( role ‖ accountId ‖ instanceSalt ‖ commitmentDomain ) where
|
|
17
|
+
*
|
|
18
|
+
* accountId := SHA256( secretKey ‖ instanceSalt ‖ accountIdDomain )
|
|
19
|
+
*
|
|
20
|
+
* roleNullifier := SHA256( roleCommitment ‖ nullifierDomain )
|
|
21
|
+
*
|
|
22
|
+
* commitmentDomain := pad(32, "ShieldedAccessControl:commitment")
|
|
23
|
+
* accountIdDomain := pad(32, "ShieldedAccessControl:accountId")
|
|
24
|
+
* nullifierDomain := pad(32, "ShieldedAccessControl:nullifier")
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* - `roleCommitment` is a Merkle tree leaf committing a `(roleId, accountId)` pairing, inserted
|
|
28
|
+
* into `_operatorRoles` on grant. The `instanceSalt` prevents commitment collisions across
|
|
29
|
+
* deployments that share the same role identifiers.
|
|
30
|
+
* - `accountId` is a privacy-preserving identity commitment. `secretKey` is a 32-byte
|
|
31
|
+
* cryptographically secure random value held in local private state (supplied by `wit_secretKey`);
|
|
32
|
+
* `instanceSalt` ensures the same key cannot be correlated across contracts.
|
|
33
|
+
* A single `secretKey` can be used across multiple roles within the same contract instance.
|
|
34
|
+
* - `roleNullifier` is a one-time burn token inserted into `_roleCommitmentNullifiers` on
|
|
35
|
+
* revocation. Its presence permanently invalidates the corresponding role commitment,
|
|
36
|
+
* making re-grant under the same `accountId` impossible without generating a new identity.
|
|
37
|
+
* - `instanceSalt` should be an immutable, cryptographically strong random value provided on deployment
|
|
38
|
+
* - `commitmentDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:commitment"
|
|
39
|
+
* - `accountIdDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:accountId"
|
|
40
|
+
* - `nullifierDomain` is the following zero padded 32 byte string: "ShieldedAccessControl:nullifier"
|
|
41
|
+
*
|
|
42
|
+
* In this RBAC model, role commitments behave like private bearer tokens. Possession of a valid, non-revoked role
|
|
43
|
+
* commitment grants authorization. Revocation permanently burns the role instance, requiring explicit new issuance
|
|
44
|
+
* under a new account identifier. Users must rotate their secret key to generate a new `accountId` to be
|
|
45
|
+
* re-authorized. This creates stronger security invariants over traditional RBAC systems and enables
|
|
46
|
+
* privacy-preserving identity rotation.
|
|
47
|
+
*
|
|
48
|
+
* A single `secretKey` may be used for all roles within a contract instance. This is safe because the
|
|
49
|
+
* `role` identifier is mixed into the commitment (not the `accountId`), so different roles produce
|
|
50
|
+
* different commitments and different nullifiers even when the underlying `accountId` is the same.
|
|
51
|
+
* Revoking one role does not affect other roles held by the same `accountId`.
|
|
52
|
+
*
|
|
53
|
+
* If a role is revoked and the user needs to be re-authorized, they must generate a new `secretKey`
|
|
54
|
+
* to produce a new `accountId`. Other roles held under the old `secretKey` remain valid. The user
|
|
55
|
+
* will need to retain both keys until all roles under the old key are no longer needed.
|
|
56
|
+
*
|
|
57
|
+
* Roles are referred to by their `Bytes<32>` identifier. These should be exposed
|
|
58
|
+
* in the top-level contract and be unique. One way to achieve this is by
|
|
59
|
+
* using `export sealed ledger` hash digests that are initialized in the top-level contract:
|
|
60
|
+
*
|
|
61
|
+
* ```compact
|
|
62
|
+
* import CompactStandardLibrary;
|
|
63
|
+
* import "./node_modules/@openzeppelin/compact-contracts/src/access/ShieldedAccessControl" prefix ShieldedAccessControl_;
|
|
64
|
+
*
|
|
65
|
+
* export sealed ledger MY_ROLE: Bytes<32>;
|
|
66
|
+
*
|
|
67
|
+
* constructor(instanceSalt: Bytes<32>, defaultAdmin: ShieldedAccessControl_AccountIdentifier) {
|
|
68
|
+
* MY_ROLE = persistentHash<Bytes<32>>(pad(32, "MY_ROLE"));
|
|
69
|
+
* ShieldedAccessControl_initialize(instanceSalt);
|
|
70
|
+
* ShieldedAccessControl__grantRole(ShieldedAccessControl_DEFAULT_ADMIN_ROLE(), defaultAdmin);
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* To restrict access to a circuit, use {assertOnlyRole}:
|
|
75
|
+
*
|
|
76
|
+
* ```compact
|
|
77
|
+
* circuit foo(): [] {
|
|
78
|
+
* ShieldedAccessControl_assertOnlyRole(MY_ROLE as ShieldedAccessControl_RoleIdentifier);
|
|
79
|
+
* // ... rest of circuit logic
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* Roles can be granted and revoked dynamically via the {grantRole} and
|
|
84
|
+
* {revokeRole} circuits. Each role has an associated admin role, and only
|
|
85
|
+
* accounts that have a role's admin role can call {grantRole} and {revokeRole}.
|
|
86
|
+
*
|
|
87
|
+
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
|
|
88
|
+
* that only accounts with this role will be able to grant or revoke other
|
|
89
|
+
* roles. More complex role relationships can be created by using
|
|
90
|
+
* {_setRoleAdmin}.
|
|
91
|
+
*
|
|
92
|
+
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
|
|
93
|
+
* grant and revoke this role. Extra precautions should be taken to secure
|
|
94
|
+
* accounts that have been granted it.
|
|
95
|
+
*
|
|
96
|
+
* @dev Privacy Assumptions
|
|
97
|
+
* - Outside observers will know when an admin is added and how many admins exist.
|
|
98
|
+
* - Outside observers will know which role identifiers are admin identifiers.
|
|
99
|
+
* - Outside observers will have knowledge of all role identifiers.
|
|
100
|
+
* - Role commitments inserted into the `_operatorRoles` MerkleTree are NOT visible in the
|
|
101
|
+
* public proof transcript. The MerkleTree insert hashes the value internally, and only
|
|
102
|
+
* the internal leaf hash is declared as a public input. Observers cannot recover the
|
|
103
|
+
* commitment from the leaf hash. This is in contrast to Set operations, where values
|
|
104
|
+
* are directly visible in the proof transcript.
|
|
105
|
+
* - Because commitments are hidden, outside observers will have knowledge of all role
|
|
106
|
+
* nullifiers but cannot link them to their corresponding commitments unless the
|
|
107
|
+
* commitment value space is small enough to enumerate. The strength of this unlinkability
|
|
108
|
+
* depends on the number of plausible `(role, accountId)` pairings. Since role identifiers
|
|
109
|
+
* are public, the anonymity set is effectively the number of plausible account IDs per role.
|
|
110
|
+
* - Outside observers will know when roles are granted and revoked.
|
|
111
|
+
* - Outside observers can infer the total number of role grants made across all roles—not per-role counts,
|
|
112
|
+
* but the cumulative total.
|
|
113
|
+
* - Outside observers can link calls made by the same role instance across time.
|
|
114
|
+
* - Users can be retroactively deanonymized if their secret key is exposed.
|
|
115
|
+
* - Outside observers will NOT be able to identify the holder of any role
|
|
116
|
+
* so long as secret key values are kept private and generated using cryptographically
|
|
117
|
+
* secure random values.
|
|
118
|
+
* - Outside observers can correlate grant and revocation transactions for the same
|
|
119
|
+
* `(role, accountId)` pairing by matching nullifier values. During a grant, the nullifier
|
|
120
|
+
* is disclosed via the `_roleCommitmentNullifiers.member` check. During a revocation, the
|
|
121
|
+
* same nullifier is disclosed via `_roleCommitmentNullifiers.insert`. This allows an observer
|
|
122
|
+
* to determine that a specific grant and revocation are related, but does not reveal the
|
|
123
|
+
* identity of the role holder or the commitment value.
|
|
124
|
+
* - Role holders have a stable pseudonymous on-chain identity for the lifetime of their role
|
|
125
|
+
* commitment. Every call to a protected circuit (`assertOnlyRole`, `canProveRole`) discloses
|
|
126
|
+
* the same nullifier via the `_roleCommitmentNullifiers.member` check, enabling observers to
|
|
127
|
+
* link all actions performed under the same role instance. This provides pseudonymity, not anonymity.
|
|
128
|
+
*
|
|
129
|
+
* @dev Security Considerations:
|
|
130
|
+
* - The `secretKey` must be kept private. Loss of the key prevents role holders
|
|
131
|
+
* and admins from proving access or transferring it. Key exposure may weaken privacy
|
|
132
|
+
* guarantees and allow retroactive deanonymization.
|
|
133
|
+
* - It's strongly recommended to use cryptographically secure random values for the `_instanceSalt`.
|
|
134
|
+
* Failure to do so may weaken privacy guarantees.
|
|
135
|
+
* - The `_instanceSalt` is immutable and used to differentiate deployments.
|
|
136
|
+
* - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots.
|
|
137
|
+
* Deployers should monitor slot consumption off-chain. A careless admin can exhaust
|
|
138
|
+
* capacity through repeated grants of the same active (role, accountId) pairing.
|
|
139
|
+
* Duplicate grants are benign from a security perspective. They produce identical
|
|
140
|
+
* commitments and a single nullifier invalidates all copies; however, they waste tree capacity.
|
|
141
|
+
* Implementing contracts are responsible for mitigating tree exhaustion risk.
|
|
142
|
+
* - A single `secretKey` compromise exposes all roles held by that user within the contract
|
|
143
|
+
* instance, since all roles share the same `accountId`. Users requiring compartmentalization
|
|
144
|
+
* between roles should use separate keys per role and accept the additional key management overhead.
|
|
145
|
+
* In the event of key compromise, the user should coordinate with admins to revoke all roles
|
|
146
|
+
* under the compromised `accountId` and re-grant under a new identity.
|
|
147
|
+
* - Admins can revoke `(role, accountId)` pairings that were never granted, since on-chain
|
|
148
|
+
* non-membership proofs are not available for the Merkle tree. This permanently burns the
|
|
149
|
+
* nullifier for that pairing, blocking any future grant. Admin trust is a fundamental
|
|
150
|
+
* assumption of this system. Off-chain validation should confirm that a pairing was actually
|
|
151
|
+
* granted before submitting a revocation transaction.
|
|
152
|
+
* - `renounceRole` allows burning nullifiers for roles the caller does not currently hold,
|
|
153
|
+
* provided they possess the correct `secretKey`. This requires knowledge of the secret key,
|
|
154
|
+
* at which point the `accountId` should be considered fully compromised regardless.
|
|
155
|
+
* - The Merkle tree capacity of 2^20 leaf slots is a permanent, irrecoverable resource.
|
|
156
|
+
* There is no mechanism to reclaim slots after revocation, no upgrade path, and no emergency
|
|
157
|
+
* recovery. Once exhausted, the contract is permanently unable to grant new roles. Deployers
|
|
158
|
+
* must monitor slot consumption off-chain and plan capacity accordingly.
|
|
159
|
+
*
|
|
160
|
+
* @notice Using the SHA256 hashing function comes at a significant performance cost. In the future, we
|
|
161
|
+
* plan on migrating to a ZK-friendly hashing function when an implementation is available.
|
|
162
|
+
*
|
|
163
|
+
* @notice Missing Features and Improvements:
|
|
164
|
+
* - Role events
|
|
165
|
+
* - An ERC165-like interface
|
|
166
|
+
* - Migrate from SHA256 to a ZK-friendly hashing function when an implementation is available.
|
|
167
|
+
*/
|
|
168
|
+
module ShieldedAccessControl {
|
|
169
|
+
import CompactStandardLibrary;
|
|
170
|
+
import "../utils/Utils" prefix Utils_;
|
|
171
|
+
import "../security/Initializable" prefix Initializable_;
|
|
172
|
+
|
|
173
|
+
export enum UpdateType {
|
|
174
|
+
Grant,
|
|
175
|
+
Revoke
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// TODO: Standardize types across contracts https://github.com/OpenZeppelin/compact-contracts/issues/368
|
|
179
|
+
export new type RoleCommitment = Bytes<32>;
|
|
180
|
+
export new type RoleIdentifier = Bytes<32>;
|
|
181
|
+
export new type AccountIdentifier = Bytes<32>;
|
|
182
|
+
export new type RoleNullifier = Bytes<32>;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @ledger _operatorRoles
|
|
186
|
+
* @description A Merkle tree of role commitments stored as SHA256(role | accountId | instanceSalt | commitmentDomain)
|
|
187
|
+
* Role commitments are derived from a public role identifier (e.g., `persistentHash<Bytes<32>>(pad(32, "MY_ROLE")`),
|
|
188
|
+
* an account identifier (e.g., `SHA256(secretKey, instanceSalt, accountIdDomain)`), the `instanceSalt`, and a domain separator.
|
|
189
|
+
* @type {RoleCommitment} roleCommitment - A role commitment created by the following hash: SHA256( role | accountId | instanceSalt | commitmentDomain).
|
|
190
|
+
*/
|
|
191
|
+
export ledger _operatorRoles: MerkleTree<20, RoleCommitment>;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @ledger _adminRoles
|
|
195
|
+
* @description Mapping from a role identifier to an admin role identifier.
|
|
196
|
+
*/
|
|
197
|
+
export ledger _adminRoles: Map<RoleIdentifier, RoleIdentifier>;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @description A set of nullifiers used to prove a role has been revoked
|
|
201
|
+
* @type {RoleNullifier} roleNullifier - A role nullifier created by the following hash: SHA256(roleCommitment | nullifierDomain).
|
|
202
|
+
* @type {Set<RoleNullifier>} _roleCommitmentNullifiers
|
|
203
|
+
*/
|
|
204
|
+
export ledger _roleCommitmentNullifiers: Set<RoleNullifier>;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @sealed @ledger _instanceSalt
|
|
208
|
+
* @description A per-instance value provided at initialization used to namespace
|
|
209
|
+
* commitments for this contract instance.
|
|
210
|
+
*
|
|
211
|
+
* This salt prevents commitment collisions across contracts that might otherwise use
|
|
212
|
+
* the same identifiers or domain parameters. It should be a cryptographically strong random value.
|
|
213
|
+
* If two deployments share the same instanceSalt and a user reuses their secretKey across both,
|
|
214
|
+
* the resulting accountId and all role commitments will be identical in both contracts.
|
|
215
|
+
* An observer who sees the same nullifier appear in both contracts' nullifier sets can conclude
|
|
216
|
+
* that it's the same user.
|
|
217
|
+
* It is immutable after initialization.
|
|
218
|
+
*/
|
|
219
|
+
export sealed ledger _instanceSalt: Bytes<32>;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @witness wit_getRoleCommitmentPath
|
|
223
|
+
* @description Returns a path to a role commitment in the `_operatorRoles` Merkle tree if one exists. Otherwise, returns an invalid path.
|
|
224
|
+
*
|
|
225
|
+
* @param {RoleCommitment} roleCommitment - A commitment created by the following hash: SHA256( role | accountId | instanceSalt | commitmentDomain).
|
|
226
|
+
*
|
|
227
|
+
* @return {MerkleTreePath<20, RoleCommitment>} - The Merkle tree path to `roleCommitment` in the `_operatorRoles` Merkle tree
|
|
228
|
+
*/
|
|
229
|
+
witness wit_getRoleCommitmentPath(
|
|
230
|
+
roleCommitment: RoleCommitment
|
|
231
|
+
): MerkleTreePath<20, RoleCommitment>;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @witness wit_secretKey
|
|
235
|
+
* @description Returns the user's secret key used in deriving the shielded account identifier.
|
|
236
|
+
*
|
|
237
|
+
* The same key can be used across multiple roles within a contract instance. If a role is
|
|
238
|
+
* revoked and re-granted, a new secret key must be generated to produce a new `accountId`.
|
|
239
|
+
*
|
|
240
|
+
* @returns {Bytes<32>} secretKey - A secret key used in deriving the shielded account identifier.
|
|
241
|
+
*/
|
|
242
|
+
witness wit_secretKey(): Bytes<32>;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @description Initializes the contract by storing the `instanceSalt` that acts as a privacy additive
|
|
246
|
+
* for preventing duplicate commitments among other contracts implementing ShieldedAccessControl.
|
|
247
|
+
*
|
|
248
|
+
* @warning The `instanceSalt` must be calculated prior to contract deployment using a cryptographically
|
|
249
|
+
* secure random number generator e.g. crypto.getRandomValues() to maintain strong privacy guarantees
|
|
250
|
+
*
|
|
251
|
+
* Requirements:
|
|
252
|
+
*
|
|
253
|
+
* - Contract is not initialized.
|
|
254
|
+
*
|
|
255
|
+
* @param {Bytes<32>} instanceSalt - Contract salt to prevent duplicate commitments if
|
|
256
|
+
* users reuse their secret key across different contracts (not recommended). Must not be zero.
|
|
257
|
+
*
|
|
258
|
+
* @returns {[]} Empty tuple.
|
|
259
|
+
*/
|
|
260
|
+
export circuit initialize(instanceSalt: Bytes<32>): [] {
|
|
261
|
+
assert(instanceSalt != default<Bytes<32>>, "ShieldedAccessControl: Instance salt must not be 0");
|
|
262
|
+
Initializable_initialize();
|
|
263
|
+
|
|
264
|
+
_instanceSalt = disclose(instanceSalt);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @description The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles
|
|
269
|
+
* unless custom admin roles are created.
|
|
270
|
+
*
|
|
271
|
+
* @remarks The Compact language does not support constant declarations,
|
|
272
|
+
* so DEFAULT_ADMIN_ROLE is implemented as a circuit that returns a constant value by necessity.
|
|
273
|
+
*/
|
|
274
|
+
export pure circuit DEFAULT_ADMIN_ROLE(): RoleIdentifier {
|
|
275
|
+
return default<Bytes<32>> as RoleIdentifier;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @description Reverts if caller cannot provide a valid proof of ownership for `role`.
|
|
280
|
+
*
|
|
281
|
+
* Requirements:
|
|
282
|
+
*
|
|
283
|
+
* - caller must prove ownership of `role`.
|
|
284
|
+
* - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing.
|
|
285
|
+
* - Contract is initialized.
|
|
286
|
+
*
|
|
287
|
+
* Disclosures:
|
|
288
|
+
*
|
|
289
|
+
* - A Merkle tree path to a role commitment.
|
|
290
|
+
* - A role commitment corresponding to a `(role, accountId)` pairing.
|
|
291
|
+
* - A nullifier for the respective role commitment.
|
|
292
|
+
*
|
|
293
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
294
|
+
*
|
|
295
|
+
* @return {[]} - Empty tuple.
|
|
296
|
+
*/
|
|
297
|
+
export circuit assertOnlyRole(role: RoleIdentifier): [] {
|
|
298
|
+
Initializable_assertInitialized();
|
|
299
|
+
assert(_uncheckedCanProveRole(role), "ShieldedAccessControl: unauthorized account");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @description Returns `true` if a caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed
|
|
304
|
+
* caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an
|
|
305
|
+
* unauthorized caller.
|
|
306
|
+
*
|
|
307
|
+
* Requirements:
|
|
308
|
+
*
|
|
309
|
+
* - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing.
|
|
310
|
+
* - Contract is initialized.
|
|
311
|
+
*
|
|
312
|
+
* Disclosures:
|
|
313
|
+
*
|
|
314
|
+
* - A Merkle tree path to a role commitment.
|
|
315
|
+
* - A role commitment corresponding to a `(role, accountId)` pairing.
|
|
316
|
+
* - A nullifier for the respective role commitment.
|
|
317
|
+
*
|
|
318
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
319
|
+
*
|
|
320
|
+
* @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role`
|
|
321
|
+
*/
|
|
322
|
+
export circuit canProveRole(role: RoleIdentifier): Boolean {
|
|
323
|
+
Initializable_assertInitialized();
|
|
324
|
+
return _uncheckedCanProveRole(role);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @description Returns `true` if a caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed
|
|
329
|
+
* caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an
|
|
330
|
+
* unauthorized caller.
|
|
331
|
+
*
|
|
332
|
+
* @warning This circuit does not perform an initialization check. It's only meant to be used as
|
|
333
|
+
* an internal helper in the Shielded Access Control module. Using this circuit outside of the
|
|
334
|
+
* module may cause undefined behavior and break security guarantees.
|
|
335
|
+
*
|
|
336
|
+
* Requirements:
|
|
337
|
+
*
|
|
338
|
+
* - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing.
|
|
339
|
+
*
|
|
340
|
+
* Disclosures:
|
|
341
|
+
*
|
|
342
|
+
* - A Merkle tree path to a role commitment.
|
|
343
|
+
* - A role commitment corresponding to a `(role, accountId)` pairing.
|
|
344
|
+
* - A nullifier for the respective role commitment.
|
|
345
|
+
*
|
|
346
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
347
|
+
*
|
|
348
|
+
* @return {Boolean} - A boolean determining if a caller successfully proved ownership of `role`
|
|
349
|
+
*/
|
|
350
|
+
circuit _uncheckedCanProveRole(role: RoleIdentifier): Boolean {
|
|
351
|
+
const accountId = _computeAccountId();
|
|
352
|
+
return _validateRole(role, accountId);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @description Grants `role` to `accountId` by inserting a role commitment unique to the
|
|
357
|
+
* `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued
|
|
358
|
+
* so long as they remain unrevoked. This does not yield any additional authority and simply wastes
|
|
359
|
+
* limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new
|
|
360
|
+
* `accountId` must be generated to be re-authorized for a revoked `role`.
|
|
361
|
+
*
|
|
362
|
+
* Requirements:
|
|
363
|
+
*
|
|
364
|
+
* - caller must prove they're an admin for `role`.
|
|
365
|
+
* - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing.
|
|
366
|
+
* - The `(role, accountId)` pairing must not be already revoked.
|
|
367
|
+
* - Contract is initialized.
|
|
368
|
+
*
|
|
369
|
+
* Disclosures:
|
|
370
|
+
*
|
|
371
|
+
* - A Merkle tree path to a role commitment.
|
|
372
|
+
* - A role commitment corresponding to a `(role, accountId)` pairing.
|
|
373
|
+
* - A nullifier for the respective role commitment.
|
|
374
|
+
* - A role identifier.
|
|
375
|
+
*
|
|
376
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
377
|
+
* @param {AccountIdentifier} accountId - The unique identifier of the account.
|
|
378
|
+
*
|
|
379
|
+
* @return {[]} - Empty tuple.
|
|
380
|
+
*/
|
|
381
|
+
export circuit grantRole(role: RoleIdentifier, accountId: AccountIdentifier): [] {
|
|
382
|
+
// Initialization check performed in assertOnlyRole
|
|
383
|
+
assertOnlyRole(getRoleAdmin(role));
|
|
384
|
+
_updateRole(role, accountId, UpdateType.Grant);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @description Grants `role` to `accountId` by inserting a role commitment unique to the
|
|
389
|
+
* `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued
|
|
390
|
+
* so long as they remain unrevoked. This does not yield any additional authority and simply wastes
|
|
391
|
+
* limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new
|
|
392
|
+
* `accountId` must be generated to be re-authorized for a revoked `role`.
|
|
393
|
+
*
|
|
394
|
+
* Internal circuit without access restriction.
|
|
395
|
+
*
|
|
396
|
+
* @warning Exposing this circuit directly in an implementing contract would allow anyone to grant
|
|
397
|
+
* roles without authorization. It must be wrapped with appropriate access control.
|
|
398
|
+
*
|
|
399
|
+
* @warning The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots.
|
|
400
|
+
* Deployers should monitor slot consumption off-chain. Duplicate grants waste tree capacity
|
|
401
|
+
* but are otherwise benign. A single nullifier invalidates all duplicate commitments.
|
|
402
|
+
* Implementing contracts are responsible for mitigating tree exhaustion risk.
|
|
403
|
+
*
|
|
404
|
+
* Requirements:
|
|
405
|
+
*
|
|
406
|
+
* - The `(role, accountId)` pairing must not be already revoked.
|
|
407
|
+
* - Contract is initialized.
|
|
408
|
+
*
|
|
409
|
+
* Disclosures:
|
|
410
|
+
*
|
|
411
|
+
* - A role commitment corresponding to a `(role, accountId)` pairing.
|
|
412
|
+
* - A nullifier for the respective role commitment.
|
|
413
|
+
*
|
|
414
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
415
|
+
* @param {AccountIdentifier} accountId - The unique identifier of the account.
|
|
416
|
+
*
|
|
417
|
+
* @return {[]} - Empty tuple.
|
|
418
|
+
*/
|
|
419
|
+
export circuit _grantRole(role: RoleIdentifier, accountId: AccountIdentifier): [] {
|
|
420
|
+
Initializable_assertInitialized();
|
|
421
|
+
_updateRole(role, accountId, UpdateType.Grant);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* @description Revokes `role` from the calling account by inserting a role nullifier into the
|
|
426
|
+
* `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be
|
|
427
|
+
* re-authorized for `role`.
|
|
428
|
+
*
|
|
429
|
+
* @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's
|
|
430
|
+
* purpose is to provide a mechanism for accounts to lose their privileges
|
|
431
|
+
* if they are compromised (such as when a trusted device is misplaced).
|
|
432
|
+
*
|
|
433
|
+
* @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity
|
|
434
|
+
* guarantees if renounceRole is used in tandem with other on-chain actions.
|
|
435
|
+
*
|
|
436
|
+
* Requirements:
|
|
437
|
+
*
|
|
438
|
+
* - The caller must provide a valid `accountId` for the role.
|
|
439
|
+
* - The `(role, accountId)` pairing must not be already revoked.
|
|
440
|
+
* - Contract is initialized.
|
|
441
|
+
*
|
|
442
|
+
* Disclosures:
|
|
443
|
+
*
|
|
444
|
+
* - A nullifier for the respective role commitment.
|
|
445
|
+
*
|
|
446
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
447
|
+
* @param {AccountIdentifier} accountIdConfirmation - The caller's account identifier, must match the internally computed value.
|
|
448
|
+
*
|
|
449
|
+
* @return {[]} - Empty tuple.
|
|
450
|
+
*/
|
|
451
|
+
export circuit renounceRole(role: RoleIdentifier, accountIdConfirmation: AccountIdentifier): [] {
|
|
452
|
+
Initializable_assertInitialized();
|
|
453
|
+
|
|
454
|
+
assert(accountIdConfirmation == _computeAccountId(),
|
|
455
|
+
"ShieldedAccessControl: bad confirmation"
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
_updateRole(role, accountIdConfirmation, UpdateType.Revoke);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the
|
|
463
|
+
* `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for
|
|
464
|
+
* `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible
|
|
465
|
+
* so a `(role, accountId)` pairing that does not exist can still be revoked.
|
|
466
|
+
*
|
|
467
|
+
* Requirements:
|
|
468
|
+
*
|
|
469
|
+
* - caller must prove they're an admin for `role`.
|
|
470
|
+
* - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing.
|
|
471
|
+
* - The `(role, accountId)` pairing must not be already revoked.
|
|
472
|
+
* - Contract is initialized.
|
|
473
|
+
*
|
|
474
|
+
* Disclosures:
|
|
475
|
+
*
|
|
476
|
+
* - A Merkle tree path to a role commitment.
|
|
477
|
+
* - A role commitment corresponding to a `(role, accountId)` pairing.
|
|
478
|
+
* - A nullifier for the respective role commitment.
|
|
479
|
+
* - A role identifier.
|
|
480
|
+
*
|
|
481
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
482
|
+
* @param {AccountIdentifier} accountId - The unique identifier of the account.
|
|
483
|
+
*
|
|
484
|
+
* @return {[]} - Empty tuple.
|
|
485
|
+
*/
|
|
486
|
+
export circuit revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): [] {
|
|
487
|
+
// Initialization check performed in assertOnlyRole
|
|
488
|
+
assertOnlyRole(getRoleAdmin(role));
|
|
489
|
+
_updateRole(role, accountId, UpdateType.Revoke);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the
|
|
494
|
+
* `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for
|
|
495
|
+
* `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible
|
|
496
|
+
* so a `(role, accountId)` pairing that does not exist can still be revoked.
|
|
497
|
+
*
|
|
498
|
+
* Internal circuit without access restriction.
|
|
499
|
+
*
|
|
500
|
+
* @warning Exposing this circuit directly in an implementing contract would allow anyone to revoke
|
|
501
|
+
* roles without authorization. It must be wrapped with appropriate access control.
|
|
502
|
+
*
|
|
503
|
+
* Requirements:
|
|
504
|
+
*
|
|
505
|
+
* - The `(role, accountId)` pairing must not be already revoked.
|
|
506
|
+
* - Contract is initialized.
|
|
507
|
+
*
|
|
508
|
+
* Disclosures:
|
|
509
|
+
*
|
|
510
|
+
* - A nullifier for the respective role commitment.
|
|
511
|
+
*
|
|
512
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
513
|
+
* @param {AccountIdentifier} accountId - The unique identifier of the account.
|
|
514
|
+
*
|
|
515
|
+
* @return {[]} - Empty tuple.
|
|
516
|
+
*/
|
|
517
|
+
export circuit _revokeRole(role: RoleIdentifier, accountId: AccountIdentifier): [] {
|
|
518
|
+
Initializable_assertInitialized();
|
|
519
|
+
_updateRole(role, accountId, UpdateType.Revoke);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @description Core business logic for the grant/revoke role circuits. Asserts that the
|
|
524
|
+
* `(role, accountId)` pairing has not already been revoked. On success, dispatches on
|
|
525
|
+
* `updateType`: a `Grant` inserts the role commitment into `_operatorRoles`, and
|
|
526
|
+
* a `Revoke` inserts the nullifier into `_roleCommitmentNullifiers`.
|
|
527
|
+
*
|
|
528
|
+
* Disclosures:
|
|
529
|
+
*
|
|
530
|
+
* - A nullifier for the respective role commitment.
|
|
531
|
+
* - A role commitment (on Grant only).
|
|
532
|
+
*
|
|
533
|
+
* @notice The nullifier is disclosed via `_roleCommitmentNullifiers.member` on every call,
|
|
534
|
+
* regardless of the update type. Since `Set.member` publicly reveals its argument, the
|
|
535
|
+
* nullifier value is observable in the transaction transcript. This enables observers to
|
|
536
|
+
* correlate grant and revocation transactions for the same `(role, accountId)` pairing.
|
|
537
|
+
*
|
|
538
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
539
|
+
* @param {AccountIdentifier} accountId - The unique identifier of the account.
|
|
540
|
+
* @param {UpdateType} updateType - Whether to grant or revoke.
|
|
541
|
+
*
|
|
542
|
+
* @return {[]} - Empty tuple.
|
|
543
|
+
*/
|
|
544
|
+
circuit _updateRole(
|
|
545
|
+
role: RoleIdentifier,
|
|
546
|
+
accountId: AccountIdentifier,
|
|
547
|
+
updateType: UpdateType
|
|
548
|
+
): [] {
|
|
549
|
+
const roleCommitment = computeRoleCommitment(role, accountId);
|
|
550
|
+
const roleNullifier = computeNullifier(roleCommitment);
|
|
551
|
+
const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier));
|
|
552
|
+
assert(!isRevoked, "ShieldedAccessControl: role is already revoked");
|
|
553
|
+
|
|
554
|
+
if (updateType == UpdateType.Grant) {
|
|
555
|
+
_operatorRoles.insert(disclose(roleCommitment));
|
|
556
|
+
} else {
|
|
557
|
+
_roleCommitmentNullifiers.insert(disclose(roleNullifier));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @description Returns the admin role that controls `role`. Returns `DEFAULT_ADMIN_ROLE` for
|
|
563
|
+
* roles with no explicitly set admin. Since `DEFAULT_ADMIN_ROLE` is the zero byte array,
|
|
564
|
+
* there is no distinction between a nonexistent `role` and one whose admin is `DEFAULT_ADMIN_ROLE`.
|
|
565
|
+
* See {grantRole} and {revokeRole}.
|
|
566
|
+
*
|
|
567
|
+
* To change a role's admin use {_setRoleAdmin}.
|
|
568
|
+
*
|
|
569
|
+
* Disclosures:
|
|
570
|
+
*
|
|
571
|
+
* - A role identifier.
|
|
572
|
+
*
|
|
573
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
574
|
+
*
|
|
575
|
+
* @return {RoleIdentifier} roleAdmin - The admin role that controls `role`.
|
|
576
|
+
*/
|
|
577
|
+
export circuit getRoleAdmin(role: RoleIdentifier): RoleIdentifier {
|
|
578
|
+
if (_adminRoles.member(disclose(role))) {
|
|
579
|
+
return _adminRoles.lookup(disclose(role));
|
|
580
|
+
}
|
|
581
|
+
return DEFAULT_ADMIN_ROLE();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* @description Sets `adminId` as `role`'s admin identifier. Users with valid admin identifiers
|
|
586
|
+
* may grant and revoke access to the specified `role`. Internal circuit without access restriction.
|
|
587
|
+
*
|
|
588
|
+
* @warning Exposing this circuit directly in an implementing contract would allow anyone to assign
|
|
589
|
+
* arbitrary admin roles without authorization. It must be wrapped with appropriate access control.
|
|
590
|
+
*
|
|
591
|
+
* Disclosures:
|
|
592
|
+
*
|
|
593
|
+
* - The role identifier
|
|
594
|
+
* - The admin identifier
|
|
595
|
+
*
|
|
596
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
597
|
+
* @param {RoleIdentifier} adminId - The admin identifier for `role`.
|
|
598
|
+
*
|
|
599
|
+
* @return {[]} - Empty tuple.
|
|
600
|
+
*/
|
|
601
|
+
export circuit _setRoleAdmin(role: RoleIdentifier, adminId: RoleIdentifier): [] {
|
|
602
|
+
Initializable_assertInitialized();
|
|
603
|
+
_adminRoles.insert(disclose(role), disclose(adminId));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* @description Verifies whether `accountId` holds `role`. This circuit MAY return false for a
|
|
608
|
+
* legitimately credentialed account if the proving environment supplies an invalid Merkle path.
|
|
609
|
+
*
|
|
610
|
+
* @warning This circuit does not perform an initialization check. It's only meant to be used as
|
|
611
|
+
* an internal helper in the Shielded Access Control module. Using this circuit outside of the
|
|
612
|
+
* module may cause undefined behavior and break security guarantees.
|
|
613
|
+
*
|
|
614
|
+
* Requirements:
|
|
615
|
+
*
|
|
616
|
+
* - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing.
|
|
617
|
+
*
|
|
618
|
+
* Disclosures:
|
|
619
|
+
*
|
|
620
|
+
* - A Merkle tree path to a role commitment.
|
|
621
|
+
* - A nullifier for the respective role commitment.
|
|
622
|
+
*
|
|
623
|
+
* @notice The nullifier is disclosed via `_roleCommitmentNullifiers.member` on every call.
|
|
624
|
+
* Since this circuit is invoked by `assertOnlyRole` and `canProveRole`, every protected
|
|
625
|
+
* operation discloses the same nullifier, allowing observers to link all actions performed
|
|
626
|
+
* under the same role instance across time.
|
|
627
|
+
*
|
|
628
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
629
|
+
* @param {AccountIdentifier} accountId - The unique identifier of the account.
|
|
630
|
+
*
|
|
631
|
+
* @return {Boolean} isValidRole - A boolean indicating whether `accountId` has a valid role
|
|
632
|
+
*/
|
|
633
|
+
circuit _validateRole(role: RoleIdentifier, accountId: AccountIdentifier): Boolean {
|
|
634
|
+
const roleCommitment = computeRoleCommitment(role, accountId);
|
|
635
|
+
const roleCommitmentPath = wit_getRoleCommitmentPath(roleCommitment);
|
|
636
|
+
const isValidPath =
|
|
637
|
+
_operatorRoles.checkRoot(
|
|
638
|
+
merkleTreePathRoot<20, RoleCommitment>(disclose(roleCommitmentPath))
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
// If the path is valid we assert it's a path for the queried leaf (not some other leaf).
|
|
642
|
+
if (isValidPath) {
|
|
643
|
+
assert(roleCommitmentPath.leaf == roleCommitment,
|
|
644
|
+
"ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const roleNullifier = computeNullifier(roleCommitment);
|
|
649
|
+
const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier));
|
|
650
|
+
|
|
651
|
+
return isValidPath && !isRevoked;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* @description Computes the role commitment from the given `accountId` and `role`.
|
|
656
|
+
*
|
|
657
|
+
* @warning This circuit does not perform an initialization check. It's only meant to be used as
|
|
658
|
+
* an internal helper in the Shielded Access Control module. Using this circuit outside of the
|
|
659
|
+
* module may cause undefined behavior and break security guarantees.
|
|
660
|
+
*
|
|
661
|
+
* ## Account ID (`accountId`)
|
|
662
|
+
* The `accountId` is expected to be computed as:
|
|
663
|
+
* `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)`
|
|
664
|
+
*
|
|
665
|
+
* - `secretKey`: A 32-byte cryptographically secure random value.
|
|
666
|
+
*
|
|
667
|
+
* ## Role Commitment Derivation
|
|
668
|
+
* `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)`
|
|
669
|
+
*
|
|
670
|
+
* - `accountId`: See above.
|
|
671
|
+
* - `role`: A unique role identifier.
|
|
672
|
+
* - `instanceSalt`: A unique per-deployment salt, stored during initialization.
|
|
673
|
+
* This prevents commitment collisions across deployments.
|
|
674
|
+
* - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent
|
|
675
|
+
* hash collisions when extending the module or using similar commitment schemes.
|
|
676
|
+
*
|
|
677
|
+
* @param {RoleIdentifier} role - The role identifier.
|
|
678
|
+
* @param {AccountIdentifier} accountId - The unique identifier of the account.
|
|
679
|
+
*
|
|
680
|
+
* @returns {RoleCommitment} The commitment derived from `accountId` and `role`.
|
|
681
|
+
*/
|
|
682
|
+
export circuit computeRoleCommitment(
|
|
683
|
+
role: RoleIdentifier,
|
|
684
|
+
accountId: AccountIdentifier,
|
|
685
|
+
): RoleCommitment {
|
|
686
|
+
|
|
687
|
+
return persistentHash<Vector<4, Bytes<32>>>(
|
|
688
|
+
[role as Bytes<32>,
|
|
689
|
+
accountId as Bytes<32>,
|
|
690
|
+
_instanceSalt,
|
|
691
|
+
pad(32, "ShieldedAccessControl:commitment")]
|
|
692
|
+
)
|
|
693
|
+
as RoleCommitment;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* @description Computes the role nullifier for a given `roleCommitment`.
|
|
698
|
+
*
|
|
699
|
+
* ## Role Nullifier Derivation
|
|
700
|
+
* `roleNullifier = SHA256(roleCommitment, nullifierDomain)`
|
|
701
|
+
*
|
|
702
|
+
* - `roleCommitment`: See `computeRoleCommitment`.
|
|
703
|
+
* - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent
|
|
704
|
+
* hash collisions when extending the module or using similar commitment schemes.
|
|
705
|
+
*
|
|
706
|
+
* @param {RoleCommitment} roleCommitment - The role commitment for a particular `(role, accountId)` pairing.
|
|
707
|
+
*
|
|
708
|
+
* @returns {RoleNullifier} roleNullifier - The associated nullifier for `roleCommitment`.
|
|
709
|
+
*/
|
|
710
|
+
export pure circuit computeNullifier(roleCommitment: RoleCommitment): RoleNullifier {
|
|
711
|
+
return persistentHash<Vector<2, Bytes<32>>>(
|
|
712
|
+
[roleCommitment as Bytes<32>, pad(32, "ShieldedAccessControl:nullifier")]
|
|
713
|
+
)
|
|
714
|
+
as RoleNullifier;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* @description Computes the unique identifier (`accountId`) of a caller from their
|
|
719
|
+
* secret key and the instance salt.
|
|
720
|
+
*
|
|
721
|
+
* @warning This circuit does not perform an initialization check. It's only meant to be used as
|
|
722
|
+
* an internal helper in the Shielded Access Control module. Using this circuit outside of the
|
|
723
|
+
* module may cause undefined behavior and break security guarantees.
|
|
724
|
+
*
|
|
725
|
+
* ## ID Derivation
|
|
726
|
+
* `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)`
|
|
727
|
+
*
|
|
728
|
+
* - `secretKey`: A 32-byte cryptographically secure random value supplied by the `wit_secretKey` witness.
|
|
729
|
+
* - `instanceSalt`: A unique per-deployment salt, stored during initialization.
|
|
730
|
+
* This prevents commitment collisions across deployments.
|
|
731
|
+
* - `accountIdDomain`: Domain separator `"ShieldedAccessControl:accountId"` (padded to 32 bytes) to prevent
|
|
732
|
+
* hash collisions when extending the module or using similar commitment schemes.
|
|
733
|
+
*
|
|
734
|
+
* The result is a 32-byte commitment that uniquely identifies the account.
|
|
735
|
+
* This value is later used in role commitment hashing,
|
|
736
|
+
* and acts as a privacy-preserving identity.
|
|
737
|
+
*
|
|
738
|
+
* @returns {AccountIdentifier} accountId - The computed account ID.
|
|
739
|
+
*/
|
|
740
|
+
circuit _computeAccountId(): AccountIdentifier {
|
|
741
|
+
return computeAccountId(wit_secretKey(), _instanceSalt);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* @description Computes an `accountId` without on-chain state, allowing a user to derive
|
|
746
|
+
* their shielded identity commitment before submitting it in a grant or revoke operation.
|
|
747
|
+
* This is the off-chain counterpart to {_computeAccountId} and produces an identical result
|
|
748
|
+
* given the same inputs.
|
|
749
|
+
*
|
|
750
|
+
* @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
|
|
751
|
+
* permanently compromise the privacy guarantees of this system:
|
|
752
|
+
*
|
|
753
|
+
* - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
|
|
754
|
+
* application logs, analytics pipelines, or any observable side-channel.
|
|
755
|
+
* - **Store offline or in secure enclaves** — hardware security modules (HSMs),
|
|
756
|
+
* air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
|
|
757
|
+
* - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
|
|
758
|
+
* or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
|
|
759
|
+
* - **Treat key loss as identity loss** — a lost key cannot be recovered. Back up
|
|
760
|
+
* keys securely before using them in role commitments.
|
|
761
|
+
* - **Avoid calling this circuit in untrusted environments** — executing this in an
|
|
762
|
+
* unverified browser extension, compromised runtime, or shared machine may expose
|
|
763
|
+
* the key to a malicious observer.
|
|
764
|
+
*
|
|
765
|
+
* ## ID Derivation
|
|
766
|
+
* `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)`
|
|
767
|
+
*
|
|
768
|
+
* See {_computeAccountId} for further details.
|
|
769
|
+
*
|
|
770
|
+
* @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
|
|
771
|
+
* @param {Bytes<32>} instanceSalt - The unique per-deployment salt for the contract instance.
|
|
772
|
+
*
|
|
773
|
+
* @returns {AccountIdentifier} accountId - The computed account ID.
|
|
774
|
+
*/
|
|
775
|
+
export pure circuit computeAccountId(
|
|
776
|
+
secretKey: Bytes<32>,
|
|
777
|
+
instanceSalt: Bytes<32>
|
|
778
|
+
): AccountIdentifier {
|
|
779
|
+
return persistentHash<Vector<3, Bytes<32>>>(
|
|
780
|
+
[secretKey, instanceSalt, pad(32, "ShieldedAccessControl:accountId")]
|
|
781
|
+
)
|
|
782
|
+
as AccountIdentifier;
|
|
783
|
+
}
|
|
784
|
+
}
|