Skip to content

Ownable refactor -> replace pk with sk#466

Open
andrew-fleming wants to merge 11 commits intoOpenZeppelin:mainfrom
andrew-fleming:fix-ownable-sk
Open

Ownable refactor -> replace pk with sk#466
andrew-fleming wants to merge 11 commits intoOpenZeppelin:mainfrom
andrew-fleming:fix-ownable-sk

Conversation

@andrew-fleming
Copy link
Copy Markdown
Contributor

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

Types of changes

What types of changes does your code introduce to OpenZeppelin Midnight Contracts?
Put an x in the boxes that apply

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Update (if none of the other choices apply)

Fixes #453

Summary by CodeRabbit

  • New Features

    • Added account identifier computation capability for ownership verification.
    • Introduced witness-based authentication mechanism for enhanced access control.
  • Updates

    • Changed ownership identity representation.
    • Updated all ownership transfer and management operations.
  • Tests

    • Expanded test coverage for ownership scenarios with new validation tests.
    • Enhanced mock implementations and simulator for better testing support.

@andrew-fleming andrew-fleming requested review from a team as code owners April 27, 2026 21:28
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 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: e498cff1-bbc5-4e9e-8c94-fbfc24cdf9f6

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

This PR migrates the Ownable access control module from pubkey-based ownership (ZswapCoinPublicKey) to witness-derived account identifiers (Bytes<32>). It introduces a new witness wit_OwnableSK for secret key provisioning, adds a computeAccountId function using persistent hashing, and refactors all related simulators, tests, and type signatures accordingly.

Changes

Cohort / File(s) Summary
Core Ownable Contract
contracts/src/access/Ownable.compact
Changed _owner type from Either<ZswapCoinPublicKey, ContractAddress> to Either<Bytes<32>, ContractAddress>. Added witness wit_OwnableSK() and pure circuit computeAccountId(secretKey) using persistentHash. Updated all ownership initialization, transfer, and authorization logic to work with computed account identifiers instead of public keys. Updated assertOnlyOwner to compare _owner.left against persistentHash(wit_OwnableSK()).
Ownable Witnesses
contracts/src/access/witnesses/OwnableWitnesses.ts, contracts/src/access/witnesses/test/OwnableWitnesses.test.ts
Evolved witness implementation from empty placeholder to concrete structure. OwnablePrivateState now contains a 32-byte secretKey with generate() and withSecretKey() utility methods. OwnableWitnesses factory returns IOwnableWitnesses<OwnablePrivateState> implementing wit_OwnableSK. Added comprehensive test coverage validating state generation, validation, defensive copying, and witness tuple returns.
Ownable Simulator & Mocks
contracts/src/access/test/simulators/OwnableSimulator.ts, contracts/src/access/test/mocks/MockOwnable.compact
Updated all ownership-related method signatures to use Either<Uint8Array, ContractAddress> instead of Either<ZswapCoinPublicKey, ContractAddress>. OwnableSimulator now supports privateState accessor for test-driven secret key injection via updatePrivateState(). Added computeAccountId pure helper. MockOwnable delegates to new Ownable signatures.
Ownable Test Suite
contracts/src/access/test/Ownable.test.ts
Refactored to use deterministic accountId-based ownership. Added helper functions for deriving accountId via persistentHash and constructing canonical Either values. Updated all simulator initialization to inject privateState.secretKey and call methods directly. Rewritten transfer/renounce tests to validate permission behavior across ownership changes (accountId vs contract owners) and to assert zeroing of inactive Either sides.

Sequence Diagram(s)

sequenceDiagram
    actor Test as Test Suite
    participant Contract as Ownable Contract
    participant Witness as wit_OwnableSK Witness
    participant Hash as persistentHash

    Test->>Contract: initialize(Either::Left(accountId))
    Test->>Contract: assertOnlyOwner() / transferOwnership()
    Note over Contract: Authorization check needed
    Contract->>Witness: wit_OwnableSK()
    Witness-->>Contract: [privateState, secretKey]
    Contract->>Hash: persistentHash(secretKey)
    Hash-->>Contract: computed accountId (Bytes<32>)
    Contract->>Contract: compare computed ≟ _owner.left
    alt Owner matches
        Contract-->>Test: ✓ Operation allowed
    else Owner mismatch
        Contract-->>Test: ✗ Revert
    end
Loading
sequenceDiagram
    actor Caller as Caller
    participant SimCtx as OwnableSimulator
    participant PrivState as privateState
    participant ContractCall as Contract Execution

    Caller->>SimCtx: Create simulator instance
    Caller->>PrivState: injectSecretKey(newSK)
    PrivState-->>Caller: Updated state
    Caller->>SimCtx: transferOwnership(newOwner)
    SimCtx->>SimCtx: computeAccountId(newSK)
    SimCtx->>ContractCall: Call contract with witness context
    ContractCall-->>SimCtx: Execute with injected secret key
    SimCtx-->>Caller: Result + validated state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

audit

Suggested reviewers

  • 0xisk
  • emnul
  • pepebndc

Poem

🐰 A rabbit hops through ownership's new way,
Where witnesses whisper secrets in the fray,
Bytes of thirty-two, hashed swift and true,
No pubkeys now—just accountIds through and through!
Account-derived, systematized,
The Ownable's witness-blessed and optimized! 🔐

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive Linked issue #453 contains no detailed requirements; assessment cannot verify code changes against specific objectives. Review issue #453 to confirm the PR implementation matches stated requirements; or confirm this refactor is the sole requirement.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Ownable refactor -> replace pk with sk' is concise and reflects the core change: switching from public-key-based ownership to secret-key-based account identifiers.
Out of Scope Changes check ✅ Passed All changes align with replacing public-key-based ownership with secret-key-derived account identifiers; no unrelated modifications detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

@andrew-fleming andrew-fleming changed the title refactor ownable -> replace pk with sk Ownable refactor -> replace pk with sk Apr 27, 2026
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: 2

🤖 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/simulators/OwnableSimulator.ts`:
- Around line 125-149: injectSecretKey currently stores the provided Uint8Array
verbatim which breaks the 32-byte invariant and allows external mutation; change
injectSecretKey to call OwnablePrivateState.withSecretKey(newSK) (which enforces
length and makes a defensive copy) and pass that returned state into
circuitContextManager.updatePrivateState; also ensure getCurrentSecretKey
continues to return the stored key but consider returning a copy if
withSecretKey does not already guarantee immutability (refer to
OwnablePrivateState.withSecretKey and OwnableWitnesses.ts for the expected
behavior).

In `@contracts/src/access/witnesses/OwnableWitnesses.ts`:
- Around line 5-18: The file imports a concrete Ledger type from artifacts which
couples this helper to MockOwnable; remove the Ledger import and make the
WitnessContext ledger type generic/agnostic (e.g., use WitnessContext<unknown,
P> or WitnessContext<any, P>) in the IOwnableWitnesses.wit_OwnableSK signature
so the module only depends on the private state type P; keep the WitnessContext
type import but stop referencing MockOwnable Ledger here and instead bind the
concrete ledger type in simulator/tests where the interface is implemented.
🪄 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: 70e1d33e-6c3b-49a4-9e4b-fcc0f2aa3c80

📥 Commits

Reviewing files that changed from the base of the PR and between a8c5225 and 130c362.

📒 Files selected for processing (6)
  • contracts/src/access/Ownable.compact
  • contracts/src/access/test/Ownable.test.ts
  • contracts/src/access/test/mocks/MockOwnable.compact
  • contracts/src/access/test/simulators/OwnableSimulator.ts
  • contracts/src/access/witnesses/OwnableWitnesses.ts
  • contracts/src/access/witnesses/test/OwnableWitnesses.test.ts

Comment thread contracts/src/access/test/simulators/OwnableSimulator.ts Outdated
Comment thread contracts/src/access/witnesses/OwnableWitnesses.ts Outdated
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.

Looking good! Thank you @andrew-fleming!

Comment on lines +398 to +400
it('should allow transfers to zero', () => {
ownable._transferOwnership(utils.ZERO_KEY);
expect(ownable.owner()).toEqual(utils.ZERO_KEY);
ownable._transferOwnership(ZERO_ACCOUNT);
expect(ownable.owner()).toEqual(ZERO_ACCOUNT);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔵 followup: I think that's a repeated test ` it('should allow transfers to zero', () => {` on line 427

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Fixed 4b82fac

* @param {Either<Bytes<32>, ContractAddress>} target - The value to check.
* @returns {Boolean} - `true` if the active branch is zero, `false` otherwise.
*/
circuit _isTargetZero(target: Either<Bytes<32>, ContractAddress>): Boolean {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔵 followup: That should be generalized in Utils.compact

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔵 followup: OR we generalize the left side of the isKeyOrAddressZero.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think the former. The latter's name doesn't work and I don't (currently) see any case where the first type would not be Bytes<32>

*
* @returns {Bytes<32>} accountId - The computed account identifier.
*/
export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔵 followup: I see the pattern of using those circuits, was thinking if we have a module that only has AccountId state and it's circuit. Then it is reused by all the other modules.

Comment on lines +227 to +228
const isContractAddr = !newOwner.is_left;
assert(!isContractAddr, "Ownable: unsafe ownership transfer");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔵 followup: as discussed on the AC PR, a generalied Left for the isContractAddress would be better.

Comment thread contracts/src/access/Ownable.compact Outdated
Comment on lines +132 to +133
const isContractAddr = !newOwner.is_left;
assert(!isContractAddr, "Ownable: unsafe ownership transfer");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔵 followup: Related to a generalized isContractAdd

/**
* @description Transfers ownership of the contract to `newOwner`.
*
* @circuitInfo k=10, rows=338
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

❔︎ question: why remove?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed f4a18f3

Comment thread contracts/src/access/Ownable.compact Outdated
Comment on lines +42 to +45
* Useful for deterministic nonce generation or advanced use cases.
*
* @param sk - The 32-byte secret nonce to use.
* @returns A fresh OwnablePrivateState instance with the provided nonce.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitpick: I see the same here of using nonce although the contract says its key I think it is better to unify the terms.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed. Fixed 735cdd6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔴 blocking: let's get back the removed @circuitInfo` s.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed f4a18f3

andrew-fleming and others added 5 commits April 30, 2026 10:08
Co-authored-by: 0xisk <0xisk@proton.me>
Signed-off-by: Andrew Fleming <fleming.andrew@protonmail.com>
Co-authored-by: 0xisk <0xisk@proton.me>
Signed-off-by: Andrew Fleming <fleming.andrew@protonmail.com>
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.

C-01: Ownable Contract

2 participants