ShieldedAccessControl API
This page provides the full ShieldedAccessControl module API.
This module provides a shielded role-based access control (RBAC) mechanism,
where roles can be used to represent a set of permissions.
Roles are stored as Merkle tree commitments to avoid disclosing information about role holders.
Role commitments are created with the following hashing scheme,
where || denotes concatenation and all values are Bytes<32>:
roleCommitment := SHA256( role || accountId || instanceSalt || commitmentDomain )
accountId := SHA256( secretKey || instanceSalt || accountIdDomain )
roleNullifier := SHA256( roleCommitment || nullifierDomain )
commitmentDomain := pad(32, "ShieldedAccessControl:commitment")
accountIdDomain := pad(32, "ShieldedAccessControl:accountId")
nullifierDomain := pad(32, "ShieldedAccessControl:nullifier")In this RBAC model, role commitments behave like private bearer tokens.
Possession of a valid, non-revoked role commitment grants authorization.
Revocation permanently burns the role instance, requiring explicit new issuance under a new account identifier.
Users must rotate their secret key to generate a new accountId to be re-authorized.
A single secretKey may be used for all roles within a contract instance.
This is safe because the role identifier is mixed into the commitment (not the accountId),
so different roles produce different commitments and different nullifiers even when the underlying accountId is the same.
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/access/ShieldedAccessControl"
prefix ShieldedAccessControl_;
export sealed ledger MY_ROLE: Bytes<32>;
constructor(instanceSalt: Bytes<32>, defaultAdmin: ShieldedAccessControl_AccountIdentifier) {
MY_ROLE = persistentHash<Bytes<32>>(pad(32, "MY_ROLE"));
ShieldedAccessControl_initialize(instanceSalt);
ShieldedAccessControl__grantRole(ShieldedAccessControl_DEFAULT_ADMIN_ROLE(), defaultAdmin);
}To restrict access to a circuit, use assertOnlyRole:
circuit foo(): [] {
ShieldedAccessControl_assertOnlyRole(MY_ROLE as ShieldedAccessControl_RoleIdentifier);
}Roles can be granted and revoked dynamically via the grantRole and revokeRole circuits. 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.
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 ShieldedAccessControl guide.
import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl";Types
enum UpdateType { Grant, Revoke }
enum
#Enum indicating whether a role update is a grant or a revocation.
new type RoleCommitment = Bytes<32>
type
#A Merkle tree leaf committing a (role, accountId) pairing. Computed as SHA256(role || accountId || instanceSalt || commitmentDomain).
new type RoleIdentifier = Bytes<32>
type
#A unique identifier for a role.
new type AccountIdentifier = Bytes<32>
type
#A privacy-preserving identity commitment. Computed as SHA256(secretKey || instanceSalt || accountIdDomain).
new type RoleNullifier = Bytes<32>
type
#A one-time burn token that permanently invalidates a role commitment on revocation. Computed as SHA256(roleCommitment || nullifierDomain).
Ledger
_isInitialized: Boolean
ledger
#Initialization flag, tracked per-module to avoid the compiler's shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the Initializable module for rationale.
_operatorRoles: MerkleTree<20, RoleCommitment>
ledger
#A Merkle tree of role commitments stored as SHA256(role | accountId | instanceSalt | commitmentDomain)
Role commitments are derived from a public role identifier (e.g., persistentHash<Bytes<32>>(pad(32, "MY_ROLE")),
an account identifier (e.g., SHA256(secretKey, instanceSalt, accountIdDomain)), the instanceSalt, and a domain separator.
_adminRoles: Map<RoleIdentifier, RoleIdentifier>
ledger
#Mapping from a role identifier to an admin role identifier. The key is the role being administered and the value is the role permitted to grant and revoke it.
_roleCommitmentNullifiers: Set<RoleNullifier>
ledger
#A set of nullifiers used to prove a role has been revoked
_grantedRoleNullifiers: Set<RoleNullifier>
ledger
#Nullifiers of every pairing ever granted. Makes a repeated grant a no-op so
one pairing can never consume more than one _operatorRoles slot, and gates revocation
so a nullifier can only exist for a granted pairing.
_instanceSalt: Bytes<32>
sealed ledger
#A per-instance value provided at initialization used to namespace commitments for this contract instance.
This salt prevents commitment collisions across contracts that might otherwise use the same identifiers or domain parameters. It should be a cryptographically strong random value. If two deployments share the same instanceSalt and a user reuses their secretKey across both, the resulting accountId and all role commitments will be identical in both contracts. An observer who sees the same nullifier appear in both contracts' nullifier sets can conclude that it's the same user. It is immutable after initialization.
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_getRoleCommitmentPath(roleCommitment: RoleCommitment) → MerkleTreePath<20, RoleCommitment>
witness
#Returns a path to a role commitment in the _operatorRoles Merkle tree if one exists. Otherwise, returns an invalid path.
wit_secretKey() → Bytes<32>
witness
#Returns the user's secret key used in deriving the shielded account identifier.
The same key can be used across multiple roles within a contract instance. If a role is
revoked and re-granted, a new secret key must be generated to produce a new accountId.
Circuits
initialize(instanceSalt: Bytes<32>) → []
circuit
#Initializes the contract by storing the instanceSalt that acts as a privacy additive
for preventing duplicate commitments among other contracts implementing ShieldedAccessControl.
The instanceSalt must be calculated prior to contract deployment using a cryptographically
secure random number generator e.g. crypto.getRandomValues() to maintain strong privacy guarantees
Requirements:
- Contract is not initialized.
instanceSaltis not zero.
DEFAULT_ADMIN_ROLE() → RoleIdentifier
pure
#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.
The Compact language does not support constant declarations, so DEFAULT_ADMIN_ROLE is implemented as a circuit that returns a constant value by necessity.
assertOnlyRole(role: RoleIdentifier) → []
circuit
#Reverts if caller cannot provide a valid proof of ownership for role.
Requirements:
- caller must prove ownership of
role. - caller must not provide valid Merkle tree path for a different
(role, accountId)pairing. - Contract is initialized.
Disclosures:
- A Merkle tree path to a role commitment.
- A role commitment corresponding to a
(role, accountId)pairing. - A nullifier for the respective role commitment.
canProveRole(role: RoleIdentifier) → Boolean
circuit
#Returns true if a caller proves ownership of role and is not revoked. MAY return false for a legitimately credentialed
caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an
unauthorized caller.
Requirements:
- caller must not provide valid Merkle tree path for a different
(role, accountId)pairing. - Contract is initialized.
Disclosures:
- A Merkle tree path to a role commitment.
- A role commitment corresponding to a
(role, accountId)pairing. - A nullifier for the respective role commitment.
grantRole(
role: RoleIdentifier,
accountId: AccountIdentifier
) → []circuit
#Grants role to accountId by inserting a role commitment unique to the
(role, accountId) pairing into the _operatorRoles Merkle tree. Granting an already active
pairing is a no-op. Once revoked, a role cannot be re-granted to the same accountId. A new
accountId must be generated to be re-authorized for a revoked role.
Requirements:
- caller must prove they're an admin for
role. - caller must not provide valid Merkle tree path for a different
(role, accountId)pairing. - The
(role, accountId)pairing must not be already revoked. - Contract is initialized.
Disclosures:
- A Merkle tree path to a role commitment.
- A role commitment corresponding to a
(role, accountId)pairing. - A nullifier for the respective role commitment.
- A role identifier.
revokeRole(
role: RoleIdentifier,
accountId: AccountIdentifier
) → []circuit
#Permanently revokes role from accountId by inserting a role nullifier into the
_roleCommitmentNullifiers set. Once revoked, a new accountId must be generated to be re-authorized for
role.
Requirements:
- caller must prove they're an admin for
role. - caller must not provide valid Merkle tree path for a different
(role, accountId)pairing. - The
(role, accountId)pairing must have been granted. - The
(role, accountId)pairing must not be already revoked. - Contract is initialized.
Disclosures:
- A Merkle tree path to a role commitment.
- A role commitment corresponding to a
(role, accountId)pairing. - A nullifier for the respective role commitment.
- A role identifier.
renounceRole(
role: RoleIdentifier,
accountIdConfirmation: AccountIdentifier
) → []circuit
#Revokes role from the calling account by inserting a role nullifier into the
_roleCommitmentNullifiers set. Once revoked, a new accountId must be generated to be
re-authorized for role.
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). Consumers with no need for self-revocation should not export this circuit.
Outside observers may be able to use timing and pattern analysis to weaken pseudonymity guarantees if renounceRole is used in tandem with other on-chain actions.
Requirements:
- The caller must provide a valid
accountIdfor the role. - The
(role, accountId)pairing must have been granted. - The
(role, accountId)pairing must not be already revoked. - Contract is initialized.
Disclosures:
- A nullifier for the respective role commitment.
getRoleAdmin(role: RoleIdentifier) → RoleIdentifier
circuit
#Returns the admin role that controls role. Returns DEFAULT_ADMIN_ROLE for
roles with no explicitly set admin. Since DEFAULT_ADMIN_ROLE is the zero byte array,
there is no distinction between a nonexistent role and one whose admin is DEFAULT_ADMIN_ROLE.
See grantRole and revokeRole.
To change a role's admin use _setRoleAdmin.
Disclosures:
- A role identifier.
_grantRole(
role: RoleIdentifier,
accountId: AccountIdentifier
) → []circuit
#Grants role to accountId by inserting a role commitment unique to the
(role, accountId) pairing into the _operatorRoles Merkle tree. Granting an already active
pairing is a no-op. Once revoked, a role cannot be re-granted to the same accountId. A new
accountId must be generated to be re-authorized for a revoked role.
Internal circuit without access restriction.
Exposing this circuit directly in an implementing contract would allow anyone to grant roles without authorization. It must be wrapped with appropriate access control.
The _operatorRoles Merkle tree has a fixed capacity of 2^20 leaf slots, one per
distinct pairing. Deployers should monitor slot consumption off-chain.
Requirements:
- The
(role, accountId)pairing must not be already revoked. - Contract is initialized.
Disclosures:
- A role commitment corresponding to a
(role, accountId)pairing. - A nullifier for the respective role commitment.
_revokeRole(
role: RoleIdentifier,
accountId: AccountIdentifier
) → []circuit
#Permanently revokes role from accountId by inserting a role nullifier into the
_roleCommitmentNullifiers set. Once revoked, a new accountId must be generated to be re-authorized for
role.
Internal circuit without access restriction.
Exposing this circuit directly in an implementing contract would allow anyone to revoke roles without authorization. It must be wrapped with appropriate access control.
Requirements:
- The
(role, accountId)pairing must have been granted. - The
(role, accountId)pairing must not be already revoked. - Contract is initialized.
Disclosures:
- A nullifier for the respective role commitment.
_setRoleAdmin(role: RoleIdentifier, adminId: RoleIdentifier) → []
circuit
#Sets adminId as role's admin identifier. Users with valid admin identifiers
may grant and revoke access to the specified role. Internal circuit without access restriction.
Exposing this circuit directly in an implementing contract would allow anyone to assign arbitrary admin roles without authorization. It must be wrapped with appropriate access control.
Requirements:
- Contract is initialized.
Disclosures:
- The role identifier
- The admin identifier
computeRoleCommitment(
role: RoleIdentifier,
accountId: AccountIdentifier
) → RoleCommitmentcircuit
#Computes the role commitment from the given accountId and role.
This circuit does not perform an initialization check. It's only meant to be used as an internal helper in the Shielded Access Control module. Using this circuit outside of the module may cause undefined behavior and break security guarantees.
Account ID (accountId)
The accountId is expected to be computed as:
accountId = SHA256(secretKey, instanceSalt, accountIdDomain)
secretKey: A 32-byte cryptographically secure random value.
Role Commitment Derivation
roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)
accountId: See above.role: A unique role identifier.instanceSalt: A unique per-deployment salt, stored during initialization. This prevents commitment collisions across deployments.commitmentDomain: Domain separator"ShieldedAccessControl:commitment"(padded to 32 bytes) to prevent hash collisions when extending the module or using similar commitment schemes.
computeNullifier(roleCommitment: RoleCommitment) → RoleNullifier
pure
#Computes the role nullifier for a given roleCommitment.
Role Nullifier Derivation
roleNullifier = SHA256(roleCommitment, nullifierDomain)
roleCommitment: See computeRoleCommitment.nullifierDomain: Domain separator"ShieldedAccessControl:nullifier"(padded to 32 bytes) to prevent hash collisions when extending the module or using similar commitment schemes.
computeAccountId(
secretKey: Bytes<32>,
instanceSalt: Bytes<32>
) → AccountIdentifierpure
#Computes an accountId without on-chain state, allowing a user to derive
their shielded identity commitment before submitting it in a grant or revoke operation.
This is the off-chain counterpart to _computeAccountId and produces an identical result
given the same inputs.
OpSec: The secretKey parameter is a sensitive secret. Mishandling it can
permanently compromise the privacy guarantees of this system:
- Never log or persist the
secretKeyin plaintext — avoid browser devtools, application logs, analytics pipelines, or any observable side-channel. - Store offline or in secure enclaves — hardware security modules (HSMs), air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
- Use cryptographically secure randomness — generate keys with
crypto.getRandomValues()or equivalent; weak or predictable keys can be brute-forced to reveal your identity. - Treat key loss as identity loss — a lost key cannot be recovered. Back up keys securely before using them in role commitments.
- Avoid calling this circuit in untrusted environments — executing this in an unverified browser extension, compromised runtime, or shared machine may expose the key to a malicious observer.
ID Derivation
accountId = SHA256(secretKey, instanceSalt, accountIdDomain)
See _computeAccountId for further details.