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

_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.
  • instanceSalt is 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.
k=15, rows=16997

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.
k=15, rows=16997

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.
k=15, rows=29071

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.
k=15, rows=27007

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 accountId for 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.
k=14, rows=14442

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.
k=9, rows=374

_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.
k=14, rows=12326

_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.
k=14, rows=10262

_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
k=10, rows=581

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

circuit

#

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.
k=13, rows=6404

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>
) → 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 _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 secretKey in 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.