Access Control
This directory provides ways to restrict who can access the circuits of a contract or when they can do it.
-
AccessControl
provides a per-contract role based access control mechanism. Multiple hierarchical roles can be created and assigned each to multiple accounts within the same instance. -
Ownable
is a simpler mechanism with a single owner "role" that can be assigned to a single account. This simpler mechanism can be useful for quick tests but projects with production concerns are likely to outgrow it. -
ZOwnablePK
provides a privacy-preserving single owner access control mechanism using cryptographic commitments. The owner’s public key is never revealed on-chain, instead storing only a commitment that proves ownership without exposing identity, suitable for applications requiring administrative control with strong privacy guarantees.
Core
AccessControl
import "./node_modules/@openzeppelin-compact/contracts/src/access/AccessControl";
Roles are referred to by their Bytes<32>
identifier. These should be exposed in the top-level contract and be unique. The best way to achieve this is by using export sealed ledger
hash digests that are initialized in the top-level contract:
import CompactStandardLibrary;
import "./node_modules/@openzeppelin-compact/contracts/src/access/AccessControl"
prefix AccessControl_;
export sealed ledger MY_ROLE: Bytes<32>;
constructor() {
MY_ROLE = persistentHash<Bytes<32>>(pad(32, "MY_ROLE"));
}
To restrict access to a circuit, use assertOnlyRole:
circuit foo(): [] {
assertOnlyRole(MY_ROLE);
...
}
Roles can be granted and revoked dynamically via the grantRole and revokeRole functions. Each role has an associated admin role, and only accounts that have a role’s admin role can call grantRole and revokeRole.
By default, the admin role for all roles is DEFAULT_ADMIN_ROLE
, which means that only accounts with this role will be able to grant or revoke other roles. More complex role relationships can be created by using _setRoleAdmin. To set a custom DEFAULT_ADMIN_ROLE
, implement the Initializable
module and set DEFAULT_ADMIN_ROLE
in the initialize()
function.
The DEFAULT_ADMIN_ROLE is also its own admin: it has permission to grant and revoke this role. Extra precautions should be taken to secure accounts that have been granted it.
|
For an overview of the module, read the AccessControl guide. |
hasRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>) → Boolean
circuit
Returns true
if account
has been granted roleId
.
Constraints:
-
k=10, rows=487
assertOnlyRole(roleId: Bytes<32>) → []
circuit
Reverts if caller is missing roleId
.
Requirements:
-
The caller must have
roleId
. -
The caller must not be a
ContractAddress
.
Constraints:
-
k=10, rows=345
_checkRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>) → []
circuit
Reverts if account
is missing roleId
.
Requirements:
-
account
must haveroleId
.
Constraints:
-
k=10, rows=467
getRoleAdmin(roleId: Bytes<32>) → Bytes<32>
circuit
Returns the admin role that controls roleId
or a byte array with all zero bytes if roleId
doesn’t exist. See grantRole and revokeRole.
To change a role’s admin use _setRoleAdmin.
Constraints:
-
k=10, rows=207
grantRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>) → []
circuit
Grants roleId
to account
.
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. |
Requirements:
-
account
must not be a ContractAddress. -
The caller must have
roleId
's admin role.
Constraints:
-
k=10, rows=994
revokeRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>) → []
circuit
Revokes roleId
from account
.
Requirements:
-
The caller must have
roleId
's admin role.
Constraints:
-
k=10, rows=827
renounceRole(roleId: Bytes<32>, callerConfirmation: Either<ZswapCoinPublicKey, ContractAddress>) → []
circuit
Revokes roleId
from the calling account.
Roles are often managed via grantRole and 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).
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. |
Requirements:
-
The caller must be
callerConfirmation
. -
The caller must not be a
ContractAddress
.
Constraints:
-
k=10, rows=640
_setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>) → []
circuit
Sets adminRole
as roleId
's admin role.
Constraints:
-
k=10, rows=209
_grantRole(roleId: Bytes<32>, adminRole: Bytes<32>) → Boolean
circuit
Attempts to grant roleId
to account
and returns a boolean indicating if roleId
was granted.
Internal circuit without access restriction.
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. |
Requirements:
-
account
must not be a ContractAddress.
Constraints:
-
k=10, rows=734
_unsafeGrantRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>) → Boolean
circuit
Unsafe variant of _grantRole.
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. |
Constraints:
-
k=10, rows=733
Ownable
import "./node_modules/@openzeppelin-compact/contracts/src/access/Ownable";
Ownable provides a basic access control mechanism where an account (an owner) can be granted exclusive access to specific circuits.
This module includes assertOnlyOwner to restrict a circuit to be used only by the owner.
For an overview of the module, read the Ownable guide. |
initialize(initialOwner: Either<ZswapCoinPublicKey, ContractAddress>) → []
circuit
Initializes the contract by setting the initialOwner
.
This must be called in the contract’s constructor.
Requirements:
-
Contract is not already initialized.
-
initialOwner
is not a ContractAddress. -
initialOwner
is not the zero address.
Constraints:
-
k=10, rows=258
owner() → Either<ZswapCoinPublicKey, ContractAddress>
circuit
Returns the current contract owner.
Requirements:
-
Contract is initialized.
Constraints:
-
k=10, rows=84
transferOwnership(newOwner: Either<ZswapCoinPublicKey, ContractAddress>) → []
circuit
Transfers ownership of the contract to newOwner
.
Ownership transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. This restriction prevents permanently disabling access to a circuit. |
Requirements:
-
Contract is initialized.
-
The caller is the current contract owner.
-
newOwner
is not a ContractAddress. -
newOwner
is not the zero address.
Constraints:
-
k=10, rows=338
_unsafeTransferOwnership(newOwner: Either<ZswapCoinPublicKey, ContractAddress>) → []
circuit
Unsafe variant of transferOwnership.
Ownership transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. Ownership privileges sent to a contract address may become uncallable. Once contract-to-contract calls are supported, this circuit may be deprecated. |
Requirements:
-
Contract is initialized.
-
The caller is the current contract owner.
-
newOwner
is not the zero address.
Constraints:
-
k=10, rows=335
renounceOwnership() → []
circuit
Leaves the contract without an owner. It will not be possible to call assertOnlyOwner circuits anymore. Can only be called by the current owner.
Requirements:
-
Contract is initialized.
-
The caller is the current contract owner.
Constraints:
-
k=10, rows=124
assertOnlyOwner() → []
circuit
Throws if called by any account other than the owner. Use this to restrict access of specific circuits to the owner.
Requirements:
-
Contract is initialized.
-
The caller is the current contract owner.
Constraints:
-
k=10, rows=115
_transferOwnership(newOwner: Either<ZswapCoinPublicKey, ContractAddress>) → []
circuit
Transfers ownership of the contract to a newOwner
without enforcing permission checks on the caller.
Ownership transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. This restriction prevents permanently disabling access to a circuit. |
Requirements:
-
Contract is initialized.
-
newOwner
is not a ContractAddress.
Constraints:
-
k=10, rows=219
_unsafeUncheckedTransferOwnership(newOwner: Either<ZswapCoinPublicKey, ContractAddress>) → []
circuit
Unsafe variant of _transferOwnership.
Ownership transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. Ownership privileges sent to a contract address may become uncallable. Once contract-to-contract calls are supported, this circuit may be deprecated. |
Requirements:
-
Contract is initialized.
Constraints:
-
k=10, rows=216
ZOwnablePK
import "./node_modules/@openzeppelin-compact/contracts/src/access/ZOwnablePK";
ZOwnablePK
provides a privacy-preserving access control mechanism for contracts with a single administrative user. Unlike traditional Ownable
implementations that store or expose the owner’s public key on-chain,
this module stores only a commitment to a hashed identifier derived from the owner’s public key and a secret nonce.
For the strongest security guarantees, use an Air-Gapped Public Key.
Ownable provides a basic access control mechanism where an account (an owner) can be granted exclusive access to specific circuits.
This module includes ZOwnablePK-assertOnlyOwner to restrict a circuit to be used only by the owner.
For an overview of the module, read the ZOwnablePK guide. |
initialize(initialOwner: Either<ZswapCoinPublicKey, ContractAddress>) → []
circuit
Initializes the contract by setting the initial owner via ownerId
and storing the instanceSalt
that acts as a privacy additive
for preventing duplicate commitments among other contracts implementing ZOwnablePK
.
The ownerId must be calculated prior to contract deployment.
See ZOwnablePK-_computeOwnerId]
|
Requirements:
-
Contract is not already initialized.
-
ownerId
is not an empty array.
Constraints:
-
k=14, rows=14933
owner() → Bytes<32>
circuit
Returns the current commitment representing the contract owner.
The full commitment is: SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)
.
Requirements:
-
Contract is initialized.
Constraints:
-
k=10, rows=57
transferOwnership(newOwnerId: Bytes<32>) → []
circuit
Transfers ownership of the contract to newOwnerId
.
newOwnerId
must be precalculated and given to the current owner off chain.
Requirements:
-
Contract is initialized.
-
Caller is the current contract owner.
-
newOwnerId
is not an empty array.
Constraints:
-
k=16, rows=39240
renounceOwnership() → []
circuit
Leaves the contract without an owner. It will not be possible to call ZOwnablePK-assertOnlyOwner circuits anymore. Can only be called by the current owner.
Requirements:
-
Contract is initialized.
-
Caller is the current owner.
Constraints:
-
k=15, rows=24442
assertOnlyOwner() → []
circuit
Throws if called by any account whose id hash SHA256(pk, nonce)
does not match the stored owner commitment.
Use this to only allow the owner to call specific circuits.
Requirements:
-
Contract is initialized.
-
Caller’s id (
SHA256(pk, nonce)
) when used in ZOwnablePK-_computeOwnerCommitment equals the stored_ownerCommitment
, thus verifying themselves as the owner.
Constraints:
-
k=15, rows=24437
_computeOwnerCommitment(id: Bytes<32>, counter: Uint<64>) → Bytes<32>
circuit
Computes the owner commitment from the given id
and counter
.
Owner ID (id
)
The id
is expected to be computed off-chain as: id = SHA256(pk, nonce)
-
pk
: The owner’s public key. -
nonce
: A secret nonce scoped to the instance, ideally rotated with each transfer.
Commitment Derivation
commitment = SHA256(id, instanceSalt, counter, domain)
-
id
: See above. -
instanceSalt
: A unique per-deployment salt, stored during initialization. This prevents commitment collisions across deployments. -
counter
: Incremented with each ownership transfer, ensuring uniqueness even with repeatedid
values. Cast toField
thenBytes<32>
for hashing. -
domain
: Domain separator"ZOwnablePK:shield:"
(padded to 32 bytes) to prevent hash collisions when extending the module or using similar commitment schemes.
Requirements:
-
Contract is initialized.
Constraints:
-
k=14, rows=14853
_computeOwnerId(pk: Either<ZswapCoinPublicKey, ContractAddress>, nonce: Bytes<32>) → Bytes<32>
circuit
Computes the unique identifier (id
) of the owner from their public key and a secret nonce.
ID Derivation
id = SHA256(pk, nonce)
-
pk
: The public key of the caller. This is passed explicitly to allow for off-chain derivation, testing, or scenarios where the caller is different from the subject of the computation. We recommend using an Air-Gapped Public Key. -
nonce
: A secret nonce tied to the identity. This value should be randomly generated and kept private. It may be rotated periodically for enhanced unlinkability.
The result is a 32-byte commitment that uniquely identifies the owner. This value is later used in owner commitment hashing, and acts as a privacy-preserving alternative to a raw public key.
This module allows ownership to be tied to an identity commitment derived from a public key and secret nonce.
While typically used with user public keys,
this mechanism may also support contract addresses as identifiers in future contract-to-contract interactions.
Both are treated as 32-byte values (Bytes<32> ).
|
Requirements:
-
Contract is initialized.
-
pk
is not a ContractAddress.