Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions content/contracts-compact/access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ title: AccessControl

[FungibleToken]: ./fungible-token.mdx
[Ownable]: ./ownable.mdx
[Initializable]: ./security#initializable
[Utils]: ./utils.mdx
[AccessControl]: api/access-control
[ShieldedAccessControl]: ./shielded-access-control.mdx

[assertOnlyRole]: api/access-control#AccessControl-assertOnlyRole
[hasRole]: api/access-control#AccessControl-hasRole
[grantRole]: api/access-control#AccessControl-grantRole
[_grantRole]: api/access-control#AccessControl-_grantRole
[_unsafeGrantRole]: api/access-control#AccessControl-_unsafeGrantRole
[revokeRole]: api/access-control#AccessControl-revokeRole
[_setRoleAdmin]: api/access-control#AccessControl-_setRoleAdmin
[computeAccountId]: api/access-control#AccessControl-computeAccountId
[computeAccountId]: api/utils#Utils-computeAccountId

This module provides a role-based access control mechanism,
where roles can be used to represent a set of permissions providing the flexibility to create different levels of account authorization.
Expand Down Expand Up @@ -50,13 +51,37 @@ some users are regular users, some may be supervisors or managers, and a few wil

AccessControl uses a witness-derived identity scheme. Each caller proves knowledge of a secret key
by injecting it via the `wit_AccessControlSK` witness. The module computes an account identifier
as `persistentHash(secretKey)`, which is a commitment that hides the key while providing a stable,
as `H(secretKey)` (a `persistentHash` of the key), which is a commitment that hides the key while providing a stable,
pseudonymous on-chain identity.

The same secret key produces the same identity across all contracts (analogous to Solidity's `msg.sender`).
What is public and what is private:

* **On the ledger**: `_operatorRoles`, a public map from role identifier to account identifier to a Boolean,
and `_adminRoles`. Every grant, revoke, and role check discloses the role identifier and the account identifier it touches,
so an observer can see which identifiers hold which roles.
* **Held privately by each account**: the 32-byte `secretKey`, supplied through `wit_AccessControlSK` at proving time.
The key never reaches the ledger; only `H(secretKey)` does.

Witness implementations are authored by the consuming DApp.
The TypeScript witnesses in the repository are test-only and are not published with the package.

The same secret key produces the same identity across all contracts.
Users who desire cross-contract unlinkability can use different secret keys per contract at the wallet layer.

To derive an account identifier off-chain (e.g. before passing it to a grant operation), use [computeAccountId].
To derive an account identifier off-chain (e.g. before passing it to a grant operation),
use [computeAccountId] from the [Utils] module:

```typescript
import "./node_modules/@openzeppelin/compact-contracts/utils/Utils"
prefix Utils_;

export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
return Utils_computeAccountId(secretKey);
}
```

A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side.
The [Utils guide](./utils#deriving-an-identifier-off-chain) shows the equivalent computation with the Compact runtime.

### Using `AccessControl`

Expand Down Expand Up @@ -164,7 +189,7 @@ an internal circuit that is useful when programmatically assigning roles (such a
But what if we later want to grant the 'minter' role to additional accounts?

By default, **accounts with a role cannot grant it or revoke it from other accounts**:
all having a role does is making the `hasRole` check pass.
all having a role does is making the [hasRole] check pass.
To grant and revoke roles dynamically, you will need help from the _role's admin_.

Every role has an associated admin role,
Expand Down Expand Up @@ -212,7 +237,7 @@ constructor(
BURNER_ROLE = persistentHash<Bytes<32>>(pad(32, "BURNER_ROLE"));
// Grant the default admin role to the provided account identifier.
// This account will be able to grant and revoke any roles.
// The account identifier should be derived off-chain using computeAccountId(secretKey).
// The account identifier should be derived off-chain using Utils_computeAccountId(secretKey).
AccessControl__grantRole(AccessControl_DEFAULT_ADMIN_ROLE(), defaultAdmin);
}

Expand All @@ -232,20 +257,22 @@ Note that, unlike the previous examples, no accounts are granted the 'minter' or
However, because those roles' admin role is the default admin role, and _that_ role was granted to `defaultAdmin`,
that same account can call [grantRole] to give minting or burning permission, and [revokeRole] to remove it.

The `defaultAdmin` account identifier should be derived off-chain using `AccessControl_computeAccountId(secretKey)`,
The `defaultAdmin` account identifier should be derived off-chain using [computeAccountId] from the [Utils] module
(`Utils_computeAccountId(secretKey)`),
where `secretKey` is a 32-byte cryptographically secure random value held by the deployer.
Pass it as `left<Bytes<32>, ContractAddress>(accountId)`.

Dynamic role allocation is often a desirable property,
for example in systems where trust in a participant may vary over time.
It can also be used to support use cases such as KYC,
where the list of role-bearers may not be known up-front,
or may be prohibitively expensive to include in a single transaction.

### Experimental features
### Unsafe circuits

This module offers an experimental circuit that allows access control permissions to be granted to contract addresses [_unsafeGrantRole].
This module offers a circuit that allows access control permissions to be granted to contract addresses, [_unsafeGrantRole].

Note that the circuit name is very explicit ("unsafe") with this experimental circuit.
Note that the circuit name is very explicit ("unsafe").
Until contract-to-contract calls are supported,
there is no direct way for a contract to call permissioned circuits of other contracts or grant/revoke role permissions.

Expand Down
109 changes: 24 additions & 85 deletions content/contracts-compact/api/access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ More complex role relationships can be created by using [_setRoleAdmin](#AccessC
## AccessControl [toc] [#AccessControl]
<APIGithubLinkHeader
moduleName="AccessControl"
link={`https://github.com/OpenZeppelin/compact-contracts/blob/main/contracts/src/access/AccessControl.compact`}
link={`https://github.com/OpenZeppelin/compact-contracts/blob/v0.3.0/contracts/src/access/AccessControl.compact`}
/>

```ts
Expand Down Expand Up @@ -84,6 +84,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
### Witnesses [toc] [#AccessControl-Witnesses]
### Witnesses [!toc] [#AccessControl-Witnesses]

Witness implementations are authored by the consuming DApp.
The TypeScript witnesses in the repository are test-only and are not published with the package.

#### wit_AccessControlSK [toc] [#AccessControl-wit_AccessControlSK]
#### [!toc] [#AccessControl-wit_AccessControlSK]
<APIItemCompact
Expand All @@ -104,12 +107,11 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
#### [!toc] [#AccessControl-DEFAULT_ADMIN_ROLE]
<APIItemCompact
circuitSig="DEFAULT_ADMIN_ROLE() → Bytes<32>"
kind="circuit"
kind="pure"
modulePrefix="AccessControl"
>
The default admin role for all roles. Returns zero bytes (`default<Bytes<32>>`).
Only accounts with this role will be able to grant or revoke other roles
unless custom admin roles are created via [_setRoleAdmin](#AccessControl-_setRoleAdmin).
The default admin role for all roles. Only accounts with this role will be able
to grant or revoke other roles unless custom admin roles are created via [_setRoleAdmin](#AccessControl-_setRoleAdmin).
</APIItemCompact>

#### hasRole [toc] [#AccessControl-hasRole]
Expand All @@ -123,19 +125,6 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
Returns `true` if `account` has been granted `roleId`.
</APIItemCompact>

#### _hasRole [toc] [#AccessControl-_hasRole]
#### [!toc] [#AccessControl-_hasRole]
<APIItemCompact
circuitSig="_hasRole(roleId: Bytes<32>, account: Either<Bytes<32>, ContractAddress>) → Boolean"
kind="internal"
stackParams="true"
modulePrefix="AccessControl"
>
Returns `true` if `account` has been granted `roleId`.
Internal variant that assumes `account` has already been canonicalized by the caller.
External consumers should use `hasRole` which canonicalizes the input automatically.
</APIItemCompact>

#### assertOnlyRole [toc] [#AccessControl-assertOnlyRole]
#### [!toc] [#AccessControl-assertOnlyRole]
<APIItemCompact
Expand All @@ -145,7 +134,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
modulePrefix="AccessControl"
>
Reverts if the caller cannot prove ownership of `roleId`.
The caller's identity is derived from the `wit_AccessControlSK` witness as `persistentHash(secretKey)`.
In the case of an external (non-contract) caller, the caller’s identity is
derived from the [wit_AccessControlSK](#AccessControl-wit_AccessControlSK) witness as `persistentHash(secretKey)`.

Requirements:

Expand All @@ -157,7 +147,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
<APIItemCompact
circuitSig="_checkRole(roleId: Bytes<32>, account: Either<Bytes<32>, ContractAddress>) → []"
kind="circuit"
complexity="k=10, rows=999"
complexity="k=10, rows=987"
modulePrefix="AccessControl"
>
Reverts if `account` is missing `roleId`.
Expand All @@ -175,8 +165,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
complexity="k=9, rows=373"
modulePrefix="AccessControl"
>
Returns the admin role that controls `roleId` or a byte array with all zero bytes if `roleId` doesn't exist.
See [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole).
Returns the admin role that controls `roleId` or
a byte array with all zero bytes if `roleId` doesn't exist. See [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole).

To change a role's admin use [_setRoleAdmin](#AccessControl-_setRoleAdmin).
</APIItemCompact>
Expand All @@ -191,11 +181,6 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
>
Grants `roleId` to `account`.

<Callout type='info'>
Granting roles to contract addresses is currently disallowed until contract-to-contract interactions are supported in Compact.
This restriction prevents permanently disabling access to a circuit.
</Callout>

Requirements:

* `account` must not be a ContractAddress.
Expand Down Expand Up @@ -228,17 +213,14 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
>
Revokes `roleId` from the calling account.

Roles are often managed via [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole):
this circuit's purpose is to provide a mechanism for accounts to lose their privileges
if they are compromised (such as when a trusted device is misplaced).

The caller's identity is derived from the `wit_AccessControlSK` witness as
`persistentHash(secretKey)`. The `callerConfirmation` parameter must match
the caller's computed account identifier to prevent accidental renunciation.

<Callout type='info'>
We do not provide functionality for smart contracts to renounce roles because self-executing transactions are not supported on Midnight at this time.
We may revisit this in future if this feature is made available in Compact.
Roles are often managed via [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole): this circuit's
purpose is to provide a mechanism for accounts to lose their privileges
if they are compromised (such as when a trusted device is misplaced).

The caller's identity is derived from the [wit_AccessControlSK](#AccessControl-wit_AccessControlSK) witness as
`persistentHash(secretKey)`. The `callerConfirmation` parameter must match
the caller's computed account identifier to prevent accidental renunciation.
</Callout>

Requirements:
Expand Down Expand Up @@ -267,14 +249,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
modulePrefix="AccessControl"
>
Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted.

Internal circuit without access restriction.

<Callout type='info'>
Granting roles to contract addresses is currently disallowed in this circuit until contract-to-contract interactions are supported in Compact.
This restriction prevents permanently disabling access to a circuit.
</Callout>

Requirements:

* `account` must not be a ContractAddress.
Expand All @@ -288,12 +264,12 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
complexity="k=11, rows=1142"
modulePrefix="AccessControl"
>
Unsafe variant of [_grantRole](#AccessControl-_grantRole).
Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted.
Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress.

<Callout type='warn'>
Granting roles to contract addresses is considered unsafe because contract-to-contract calls are not currently supported.
Granting a role to a smart contract may render a circuit permanently inaccessible.
Once contract-to-contract calls are supported, this circuit may be deprecated.
<Callout type='info'>
External smart contracts cannot call the access control contract at this time, so granting a role to a ContractAddress may
render a circuit permanently inaccessible.
</Callout>
</APIItemCompact>

Expand All @@ -306,42 +282,5 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl";
modulePrefix="AccessControl"
>
Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked.

Internal circuit without access restriction.
</APIItemCompact>

#### _computeAccountId [toc] [#AccessControl-_computeAccountId]
#### [!toc] [#AccessControl-_computeAccountId]
<APIItemCompact
circuitSig="_computeAccountId() → Bytes<32>"
kind="internal"
modulePrefix="AccessControl"
>
Computes the caller's account identifier from the `wit_AccessControlSK` witness.

ID Derivation: `accountId = persistentHash(secretKey)`

The result is a 32-byte commitment that uniquely identifies the caller.
</APIItemCompact>

#### computeAccountId [toc] [#AccessControl-computeAccountId]
#### [!toc] [#AccessControl-computeAccountId]
<APIItemCompact
circuitSig="computeAccountId(secretKey: Bytes<32>) → Bytes<32>"
kind="pure"
modulePrefix="AccessControl"
>
Computes an account identifier without on-chain state, allowing a user to derive
their identity commitment before submitting it in a grant or revoke operation.
This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result
given the same inputs.

ID Derivation: `accountId = persistentHash(secretKey)`

<Callout type='warn'>
The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the privacy guarantees of this system.
Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys.
Treat key loss as identity loss.
A lost key cannot be recovered.
</Callout>
</APIItemCompact>
Loading
Loading