API Reference

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

_operatorRoles: MerkleTree<20, RoleCommitment>

ledger

#

A Merkle tree of role commitments stored as SHA256(role | accountId | instanceSalt | commitmentDomain). Has a fixed capacity of 2^20 leaf slots.

_adminRoles: Map<RoleIdentifier, RoleIdentifier>

ledger

#

Mapping from a role identifier to an admin role identifier.

_roleCommitmentNullifiers: Set<RoleNullifier>

ledger

#

A set of nullifiers used to prove a role has been revoked.

_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. It is immutable after initialization.

Witnesses

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.
  • instanceSalt must not be zero.

DEFAULT_ADMIN_ROLE() → RoleIdentifier

pure

#

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.

assertOnlyRole(role: RoleIdentifier) → []

circuit

#

Reverts if the caller cannot provide a valid proof of ownership for role.

Disclosures: a Merkle tree path to a role commitment, a role commitment corresponding to a (role, accountId) pairing, and a nullifier for the respective role commitment.

Requirements:

  • Contract is initialized.
  • Caller must prove ownership of role.
  • Caller must not provide a valid Merkle tree path for a different (role, accountId) pairing.

canProveRole(role: RoleIdentifier) → Boolean

circuit

#

Returns true if the 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.

Disclosures: a Merkle tree path to a role commitment, a role commitment corresponding to a (role, accountId) pairing, and a nullifier for the respective role commitment.

Requirements:

  • Contract is initialized.
  • Caller must not provide a valid Merkle tree path for a different (role, accountId) pairing.

_uncheckedCanProveRole(role: RoleIdentifier) → Boolean

internal

#

Returns true if the 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.

Disclosures: a Merkle tree path to a role commitment, a role commitment corresponding to a (role, accountId) pairing, and a nullifier for the respective role commitment.

This circuit does not perform an initialization check. It is only meant to be used as an internal helper. Using this circuit outside of the module may cause undefined behavior and break security guarantees.

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. Duplicate role commitments can be issued so long as they remain unrevoked. 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.

Disclosures: a Merkle tree path to a role commitment, a role commitment, a nullifier, and a role identifier.

Requirements:

  • Contract is initialized.
  • Caller must prove they are an admin for role.
  • Caller must not provide a valid Merkle tree path for a different (role, accountId) pairing.
  • The (role, accountId) pairing must not be already revoked.

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.

At this time, proofs of non-membership on values in the Merkle tree are not available, so a (role, accountId) pairing that does not exist can still be revoked.

Disclosures: a Merkle tree path to a role commitment, a role commitment, a nullifier, and a role identifier.

Requirements:

  • Contract is initialized.
  • Caller must prove they are an admin for role.
  • Caller must not provide a valid Merkle tree path for a different (role, accountId) pairing.
  • The (role, accountId) pairing must not be already revoked.

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).

Disclosures: a nullifier for the respective role commitment.

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:

  • Contract is initialized.
  • The caller must provide a valid accountId for the role.
  • The (role, accountId) pairing must not be already revoked.

_updateRole(
  role: RoleIdentifier,
  accountId: AccountIdentifier,
  updateType: UpdateType
) → []

internal

#

Core business logic for the grant/revoke role circuits. Asserts that the (role, accountId) pairing has not already been revoked. On success, dispatches on updateType: a Grant inserts the role commitment into _operatorRoles, and a Revoke inserts the nullifier into _roleCommitmentNullifiers.

Disclosures: a nullifier for the respective role commitment, and a role commitment (on Grant only).

The nullifier is disclosed via _roleCommitmentNullifiers.member on every call, regardless of the update type. This enables observers to correlate grant and revocation transactions for the same (role, accountId) pairing.

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.

Internal circuit without access restriction.

Disclosures: a role commitment corresponding to a (role, accountId) pairing, and a nullifier for the respective role commitment.

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. Deployers should monitor slot consumption off-chain. Duplicate grants waste tree capacity but are otherwise benign. A single nullifier invalidates all duplicate commitments. Implementing contracts are responsible for mitigating tree exhaustion risk.

Requirements:

  • Contract is initialized.
  • The (role, accountId) pairing must not be already revoked.

_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.

Disclosures: a nullifier for the respective role commitment.

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:

  • Contract is initialized.
  • The (role, accountId) pairing must not be already revoked.

_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.

Disclosures: the role identifier and the admin identifier.

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.

_validateRole(
  role: RoleIdentifier,
  accountId: AccountIdentifier
) → Boolean

internal

#

Verifies whether accountId holds role. MAY return false for a legitimately credentialed account if the proving environment supplies an invalid Merkle path.

Disclosures: a Merkle tree path to a role commitment, and a nullifier for the respective role commitment.

This circuit does not perform an initialization check. It is only meant to be used as an internal helper. Using this circuit outside of the module may cause undefined behavior and break security guarantees.

The nullifier is disclosed via _roleCommitmentNullifiers.member on every call. Since this circuit is invoked by assertOnlyRole and canProveRole, every protected operation discloses the same nullifier, allowing observers to link all actions performed under the same role instance across time.

computeRoleCommitment(
  role: RoleIdentifier,
  accountId: AccountIdentifier
) → RoleCommitment

circuit

#

Computes the role commitment from the given role and accountId.

Role Commitment Derivation: roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)

This circuit does not perform an initialization check. It is only meant to be used as an internal helper. Using this circuit outside of the module may cause undefined behavior and break security guarantees.

computeNullifier(roleCommitment: RoleCommitment) → RoleNullifier

circuit

#

Computes the role nullifier for a given roleCommitment.

Role Nullifier Derivation: roleNullifier = SHA256(roleCommitment, nullifierDomain)

_computeAccountId() → AccountIdentifier

internal

#

Computes the caller's account identifier from the wit_secretKey witness and the _instanceSalt.

ID Derivation: accountId = SHA256(secretKey, instanceSalt, accountIdDomain)

The result is a 32-byte commitment that uniquely identifies the account.

This circuit does not perform an initialization check. It is only meant to be used as an internal helper. Using this circuit outside of the module may cause undefined behavior and break security guarantees.

computeAccountId(
  secretKey: Bytes<32>,
  instanceSalt: Bytes<32>
) → AccountIdentifier

pure

#

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 the internal _computeAccountId and produces an identical result given the same inputs.

ID Derivation: accountId = SHA256(secretKey, instanceSalt, accountIdDomain)

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.