Skip to content

Add shielded access control#420

Open
andrew-fleming wants to merge 7 commits intoOpenZeppelin:mainfrom
andrew-fleming:squash-shielded-access
Open

Add shielded access control#420
andrew-fleming wants to merge 7 commits intoOpenZeppelin:mainfrom
andrew-fleming:squash-shielded-access

Conversation

@andrew-fleming
Copy link
Copy Markdown
Contributor

@andrew-fleming andrew-fleming commented Apr 7, 2026

Supersedes #190. Early commits were not signed and this would require cherry-picking commits, resigning everything, and fixing a ridiculous amount of conflicts. Credit to @emnul for most of the work and @0xisk for contributions

Resolves #88

Summary by CodeRabbit

  • New Features

    • Added ShieldedAccessControl module with privacy-preserving role-based access control system using Merkle-tree-based commitments and revocation tracking.
  • Documentation

    • Updated simulator usage examples to demonstrate generalized ledger type patterns for witness factories.

@andrew-fleming andrew-fleming requested review from a team as code owners April 7, 2026 04:40
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d09de50-0fb5-4849-b5e1-473cab86123c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Introduces ShieldedAccessControl, a privacy-preserving role-based access control system using Merkle-tree-based role commitments and revocation nullifiers. Adds comprehensive test coverage, witness infrastructure, and simulators. Updates witness typings across the codebase to use generic ledger type parameters.

Changes

Cohort / File(s) Summary
ShieldedAccessControl Core
contracts/src/access/ShieldedAccessControl.compact
New privacy-preserving RBAC module implementing role commitments via Merkle trees, revocation via nullifiers, account ID derivation from secret key, and role lifecycle operations (grant, revoke, renounce).
ShieldedAccessControl Tests & Mocks
contracts/src/access/test/ShieldedAccessControl.test.ts, contracts/src/access/test/mocks/MockShieldedAccessControl.compact
Comprehensive test suite covering initialization, role validation, account ID derivation, role lifecycle operations, multi-user/multi-role scenarios, and equivalence checks; mock contract exposes internal testing circuits.
ShieldedAccessControl Witnesses & Simulators
contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts, contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts
New witness and simulator infrastructure for role commitment path retrieval and secret key extraction; simulator exposes public wrappers around circuit operations and private state helpers for test setup.
Generic Ledger Type Updates
contracts/src/access/witnesses/ZOwnablePKWitnesses.ts, contracts/src/access/test/simulators/ZOwnablePKSimulator.ts, packages/simulator/test/fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses.ts, packages/simulator/test/fixtures/sample-contracts/witnesses/WitnessWitnesses.ts, packages/simulator/test/integration/SampleZOwnableSimulator.ts, packages/simulator/test/integration/WitnessSimulator.ts
Updated witness interfaces and factories to accept generic ledger type parameter L instead of hard-coded Ledger import; maintains runtime behavior while improving type flexibility.
Test-Only Warnings
contracts/src/access/test/mocks/MockAccessControl.compact, contracts/src/access/test/mocks/MockOwnable.compact, contracts/src/access/test/mocks/MockZOwnablePK.compact, contracts/src/archive/test/mocks/MockShieldedToken.compact, contracts/src/security/test/mocks/MockInitializable.compact, contracts/src/security/test/mocks/MockPausable.compact, contracts/src/token/test/mocks/MockFungibleToken.compact, contracts/src/token/test/mocks/MockMultiToken.compact, contracts/src/token/test/mocks/MockNonFungibleToken.compact, contracts/src/utils/test/mocks/MockUtils.compact
Added prominent testing-only warning comments (and SPDX license headers where applicable) to mock contracts clarifying they expose internal circuits and bypass production safety checks.
Documentation & Type Updates
CHANGELOG.md, packages/simulator/README.md, contracts/test-utils/address.ts
Updated changelog with ShieldedAccessControl entry; updated simulator README examples with explicit ledger type definitions; added explicit return type annotation to createEitherTestUser.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Caller
    participant Contract as ShieldedAccessControl
    participant Tree as Merkle Tree Ledger
    participant Witness as Witness Provider
    participant Nullifiers as Revocation Set

    User->>Contract: canProveRole(role)
    Note over Contract: Derive accountId from secret key<br/>+ instanceSalt
    Contract->>Contract: Compute expected<br/>roleCommitment
    Contract->>Witness: wit_getRoleCommitmentPath(roleCommitment)
    Witness->>Tree: Query Merkle path<br/>for commitment
    Tree-->>Witness: Return path + leaf
    Witness-->>Contract: Return path
    Contract->>Tree: Verify path leads to<br/>valid leaf in root
    Contract->>Nullifiers: Check if nullifier<br/>is revoked
    alt Not revoked & path valid
        Contract-->>User: Return true
    else Revoked or invalid
        Contract-->>User: Return false
    end
Loading
sequenceDiagram
    participant Admin as Admin
    participant Contract as ShieldedAccessControl
    participant Tree as Merkle Tree
    participant Nullifiers as Nullifier Set

    Admin->>Contract: grantRole(role, accountId)
    Contract->>Contract: Compute roleCommitment<br/>= H(role, accountId,<br/>instanceSalt)
    Contract->>Contract: Compute nullifier<br/>= H(roleCommitment)
    Contract->>Nullifiers: Check nullifier<br/>not present (not revoked)
    Contract->>Tree: Insert commitment<br/>into Merkle tree
    Note over Tree: Root updates
    Contract-->>Admin: Confirmed

    Admin->>Contract: revokeRole(role, accountId)
    Contract->>Contract: Compute roleCommitment
    Contract->>Contract: Compute nullifier
    Contract->>Nullifiers: Insert nullifier<br/>into revocation set
    Note over Nullifiers: Marks commitment as revoked
    Contract-->>Admin: Confirmed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • OpenZeppelin/compact-contracts#279: Updates witness and simulator typings to use generic ledger type parameter, directly overlapping with this PR's generic ledger refactor for ZOwnablePKWitnesses and simulator wiring.
  • OpenZeppelin/compact-contracts#366: Implements similar generic ledger typing for witnesses and simulators (witnessesFactory parameterized with ReturnType<typeof ledger>), showing parallel refactoring of the witness infrastructure.

Suggested reviewers

  • 0xisk

🐰 Merkle trees and secrets dance in bloom,
Role commitments hiding in the gloom,
With nullifiers marking what's revoked,
Privacy-preserved access, gently stoked! ✨🌳

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add shielded access control' is clear and directly reflects the main change: introducing a new shielded access control module.
Linked Issues check ✅ Passed Implementation aligns with #88 requirements: introduces privacy-preserving RBAC with Merkle-tree commitments, role management circuits (grant/revoke/renounce), witness support, and account ID derivation from secret keys.
Out of Scope Changes check ✅ Passed Changes include generalization of witness typing (ZOwnablePKWitnesses, test fixtures) and documentation updates to examples. These changes support the new module and facilitate testing infrastructure without introducing unrelated functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts (1)

136-141: Clone and validate the injected secret key before storing it.

Line 139 keeps the caller-owned buffer by reference and accepts any length. A later mutation of that buffer silently changes the simulator identity, and non-32-byte keys violate the witness contract.

🔐 Suggested change
     injectSecretKey: (
       newSK: Buffer<ArrayBufferLike>,
     ): ShieldedAccessControlPrivateState => {
-      const updatedState = { secretKey: newSK };
+      if (newSK.length !== 32) {
+        throw new Error('ShieldedAccessControl secretKey must be 32 bytes');
+      }
+      const updatedState = { secretKey: Uint8Array.from(newSK) };
       this.circuitContextManager.updatePrivateState(updatedState);
       return updatedState;
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts`
around lines 136 - 141, The injectSecretKey implementation keeps the
caller-owned buffer reference and accepts any length; change injectSecretKey to
first validate that newSK is exactly 32 bytes and throw an error if not, then
clone the input (e.g. create a new Buffer copy via Buffer.from(newSK) or by
slicing into a new Uint8Array) and store that cloned value in the
ShieldedAccessControlPrivateState before calling
this.circuitContextManager.updatePrivateState(updatedState) so the simulator
owns an immutable 32-byte secretKey rather than a mutable external reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/src/access/test/ShieldedAccessControl.test.ts`:
- Around line 136-145: The test currently uses
expect(...).not.toThrow('Initializable: contract not initialized'), which only
asserts the thrown error message isn't that specific string; update the
assertion in the test iterating circuitsNotRequiringInit (the it.each block
invoking contract[circuitName as keyof ShieldedAccessControlSimulator]) to use
expect(...).not.toThrow() so it fails if any error is thrown at all, ensuring
the "%s should succeed" cases truly do not throw.
- Around line 223-238: The test creates newContract without using the same
secret key so it may pass for the wrong reason; instantiate the second
ShieldedAccessControlSimulator with the same ADMIN_SK private key (seed) used by
contract so the only variable is instanceSalt, then call
contract._computeAccountId() and newContract._computeAccountId() to assert they
differ — ensure you pass ADMIN_SK into the ShieldedAccessControlSimulator
constructor for newContract to isolate the salt change.
- Around line 35-39: The test helper buildAccountIdHash (and the similar helper
around lines 58-62) currently passes raw UTF-8 bytes for domain strings, but the
contract pads all three domains to 32 bytes; update these helpers to explicitly
pad the accountId/nullifier domain bytes to 32 bytes (e.g., left/right pad with
zero bytes to length 32) before calling persistentHash with rt_type
(CompactTypeVector(3, CompactTypeBytes(32))) so the inputs mirror the contract’s
pad(32, ...) behavior for ACCOUNT_DOMAIN and the nullifier domain.

---

Nitpick comments:
In `@contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts`:
- Around line 136-141: The injectSecretKey implementation keeps the caller-owned
buffer reference and accepts any length; change injectSecretKey to first
validate that newSK is exactly 32 bytes and throw an error if not, then clone
the input (e.g. create a new Buffer copy via Buffer.from(newSK) or by slicing
into a new Uint8Array) and store that cloned value in the
ShieldedAccessControlPrivateState before calling
this.circuitContextManager.updatePrivateState(updatedState) so the simulator
owns an immutable 32-byte secretKey rather than a mutable external reference.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5299f02d-1141-468c-a90e-018431d6be88

📥 Commits

Reviewing files that changed from the base of the PR and between c953f86 and 5e4720a.

📒 Files selected for processing (24)
  • CHANGELOG.md
  • contracts/src/access/ShieldedAccessControl.compact
  • contracts/src/access/test/ShieldedAccessControl.test.ts
  • contracts/src/access/test/mocks/MockAccessControl.compact
  • contracts/src/access/test/mocks/MockOwnable.compact
  • contracts/src/access/test/mocks/MockShieldedAccessControl.compact
  • contracts/src/access/test/mocks/MockZOwnablePK.compact
  • contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts
  • contracts/src/access/test/simulators/ZOwnablePKSimulator.ts
  • contracts/src/access/witnesses/ShieldedAccessControlWitnesses.ts
  • contracts/src/access/witnesses/ZOwnablePKWitnesses.ts
  • contracts/src/archive/test/mocks/MockShieldedToken.compact
  • contracts/src/security/test/mocks/MockInitializable.compact
  • contracts/src/security/test/mocks/MockPausable.compact
  • contracts/src/token/test/mocks/MockFungibleToken.compact
  • contracts/src/token/test/mocks/MockMultiToken.compact
  • contracts/src/token/test/mocks/MockNonFungibleToken.compact
  • contracts/src/utils/test/mocks/MockUtils.compact
  • contracts/test-utils/address.ts
  • packages/simulator/README.md
  • packages/simulator/test/fixtures/sample-contracts/witnesses/SampleZOwnableWitnesses.ts
  • packages/simulator/test/fixtures/sample-contracts/witnesses/WitnessWitnesses.ts
  • packages/simulator/test/integration/SampleZOwnableSimulator.ts
  • packages/simulator/test/integration/WitnessSimulator.ts

Copy link
Copy Markdown
Member

@0xisk 0xisk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! thank you @andrew-fleming! I opened this PR for proposing improvements on the re-implmented circuits for testing which I think they are redundant as they are fully covered by their public circuits.

@andrew-fleming andrew-fleming requested a review from 0xisk April 9, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add shielded access control

2 participants