Skip to content

NFT refactor -> replace pk with sk#469

Open
andrew-fleming wants to merge 12 commits intoOpenZeppelin:mainfrom
andrew-fleming:fix-nft-sk
Open

NFT refactor -> replace pk with sk#469
andrew-fleming wants to merge 12 commits intoOpenZeppelin:mainfrom
andrew-fleming:fix-nft-sk

Conversation

@andrew-fleming
Copy link
Copy Markdown
Contributor

@andrew-fleming andrew-fleming commented May 2, 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 #456

Summary by CodeRabbit

  • New Features

    • Implemented a new account identification system for NFT ownership and authorization based on witness-derived 32-byte identifiers.
  • Bug Fixes

    • Enhanced canonicalization behavior for account lookups in balance, approval, and transfer operations.

@andrew-fleming andrew-fleming requested review from a team as code owners May 2, 2026 12:00
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 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: 9cbc323b-ec36-458f-81e9-1213ef77cb18

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

The NonFungibleToken contract's identity scheme was refactored from ZswapCoinPublicKey-based ownership to a witness-derived 32-byte account identifier. New canonicalization logic was introduced, ledger mappings were updated, and all authorization and transfer circuits now operate on Either<Bytes<32>, ContractAddress> throughout.

Changes

NonFungibleToken Identity Migration

Layer / File(s) Summary
New Type & Zero Export
contracts/src/token/NonFungibleToken.compact (lines 128–150)
Added witness wit_NonFungibleTokenSK(): Bytes<32> and export circuit ZERO(): Either<Bytes<32>, ContractAddress> as canonical zero for mint/burn/approval clearing.
Ledger & Storage Types
contracts/src/token/NonFungibleToken.compact (lines 84–117)
Updated _owners, _balances, _tokenApprovals, and _operatorApprovals ledger mappings to use Either<Bytes<32>, ContractAddress> instead of Either<ZswapCoinPublicKey, ContractAddress>.
Core Identity & Canonicalization
contracts/src/token/NonFungibleToken.compact (lines 887–935)
Added _computeAccountId() deriving from wit_NonFungibleTokenSK and _isTargetZero() for zero-semantics validation; inserted Utils_canonicalize logic into read/write paths.
Public Read & Approval APIs
contracts/src/token/NonFungibleToken.compact (lines 176–396)
Updated balanceOf, ownerOf, approve, getApproved, setApprovalForAll, and isApprovedForAll to accept/return Either<Bytes<32>, ContractAddress> and derive caller identity via _computeAccountId().
Authorization & Transfer Logic
contracts/src/token/NonFungibleToken.compact (lines 425–780, 798–876)
Refactored transferFrom, _unsafeTransferFrom, _transfer, _unsafeTransfer, _ownerOf, _getApproved, _isAuthorized, _approve, _setApprovalForAll, and _requireOwned to canonicalize Either values and use new zero-branch checks.
State Transition Core
contracts/src/token/NonFungibleToken.compact (lines 597–632, 645–707)
Updated _update, _mint, and _burn to canonicalize owner/receiver, clear approvals via ZERO(), and manage balance/ownership under the new identity model.
Witness Implementation
contracts/src/token/witnesses/NonFungibleTokenWitnesses.ts (lines 1–80)
Implemented INonFungibleTokenWitnesses<L, P> interface, NonFungibleTokenPrivateState with generate() and withSecretKey() utilities, and NonFungibleTokenWitnesses factory returning wit_NonFungibleTokenSK.
Mock Contract
contracts/src/token/test/mocks/MockNonFungibleToken.compact
Updated export list (removed ZswapCoinPublicKey), added ZERO() and computeAccountId() pure circuits, and changed all account-related signatures to Either<Bytes<32>, ContractAddress>.
Simulator & Test Infrastructure
contracts/src/token/test/simulators/NonFungibleTokenSimulator.ts
Updated all method signatures to use Either<Uint8Array, ContractAddress>, added ZERO() method, implemented computeAccountId(), and exposed privateState with injectSecretKey() and getCurrentSecretKey() for test injection.
Witness Tests
contracts/src/token/witnesses/test/NonFungibleTokenWitnesses.test.ts
Added coverage for NonFungibleTokenPrivateState.generate() uniqueness, withSecretKey() 32-byte validation, and NonFungibleTokenWitnesses witness behavior across state instances.
Test Suite Refactoring
contracts/src/token/test/nonFungibleToken.test.ts (+746 lines)
Replaced account/key generation utilities with local helpers (buildAccountIdHash, createTestSK, eitherAccountId), refactored all scenarios to use token.privateState.injectSecretKey() for caller identity, updated constants (TOKENID_* as bigint, NON_EXISTENT_TOKEN as 0xdeadn), and expanded coverage for canonicalization and ZERO() semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit hops through keys and bytes so dear,
No public key shall linger here!
Now witness-born, a secret stays,
Canonical and safe through all our ways—
Identity derived from cryptic art, 🐰✨
A safer NFT right from the start.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'NFT refactor -> replace pk with sk' accurately captures the main change: replacing public key-based ownership with secret key-based account identifiers in the NonFungibleToken contract.
Linked Issues check ✅ Passed The pull request implements the NonFungibleToken contract refactoring to replace public key ownership with witness-derived secret key-based account identifiers, addressing issue #456's requirements.
Out of Scope Changes check ✅ Passed All changes are directly related to the NFT refactoring objective: updating ownership scheme, canonicalization logic, ledger mappings, test infrastructure, and simulator to support the new secret key-based account identification model.

✏️ 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: 2

🧹 Nitpick comments (1)
contracts/src/token/witnesses/test/NonFungibleTokenWitnesses.test.ts (1)

81-89: ⚡ Quick win

Assert that wit_NonFungibleTokenSK returns a defensive copy.

These cases only verify equality and shape. They would still pass if the witness returned context.privateState.secretKey directly, which would let callers mutate the stored key through the witness result.

Proposed test addition
   describe('wit_NonFungibleTokenSK', () => {
     it('should return a tuple of [privateState, secretKey]', () => {
       const state = NonFungibleTokenPrivateState.withSecretKey(SECRET_KEY);
       const ctx = makeContext(state);

       const [returnedState, returnedSK] = witnesses.wit_NonFungibleTokenSK(ctx);

       expect(returnedState).toBe(state);
       expect(returnedSK).toEqual(SECRET_KEY);
     });
+
+    it('should return a defensive copy of the secret key', () => {
+      const state = NonFungibleTokenPrivateState.withSecretKey(SECRET_KEY);
+      const ctx = makeContext(state);
+
+      const [, returnedSK] = witnesses.wit_NonFungibleTokenSK(ctx);
+      returnedSK.fill(0xff);
+
+      expect(state.secretKey).toEqual(SECRET_KEY);
+    });

Also applies to: 99-116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/src/token/witnesses/test/NonFungibleTokenWitnesses.test.ts` around
lines 81 - 89, Update the test for wit_NonFungibleTokenSK to assert the function
returns defensive copies instead of references: after calling
wit_NonFungibleTokenSK(ctx) (using NonFungibleTokenPrivateState.withSecretKey
and makeContext), verify that returnedSK is equal in value to SECRET_KEY but not
the same reference (e.g., mutating returnedSK does not change state.secretKey),
and similarly assert returnedState is not the identical object as the context
private state (mutating returnedState does not alter ctx.privateState); focus on
wit_NonFungibleTokenSK, NonFungibleTokenPrivateState.withSecretKey, and
makeContext to locate the code under test.
🤖 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/token/NonFungibleToken.compact`:
- Around line 798-816: The approve flow stores a non-canonical zero when
Utils_canonicalize preserves a right-branch zero, causing getApproved to return
a different value than ZERO(); in _approve, after computing canonTo (from
Utils_canonicalize<Bytes<32>, ContractAddress>(to)) normalize any zero approval
to the canonical ZERO() representation before inserting into _tokenApprovals:
detect the zero case (compare disclose(canonTo) to the zero contract/bytes) and
set the value to ZERO() so _tokenApprovals.insert(disclose(tokenId), ...) always
stores canonical ZERO(), keeping getApproved, _isAuthorized and external API
consistent.
- Around line 145-149: The exported circuit ZERO() is declared without purity,
causing a pure caller (MockNonFungibleToken's export pure circuit ZERO() which
calls NonFungibleToken_ZERO()) to violate Compact's purity rules; change the
declaration of ZERO in NonFungibleToken.compact to be pure (i.e., export pure
circuit ZERO()) so that the symbol ZERO / NonFungibleToken_ZERO() is marked pure
and can be called from MockNonFungibleToken's pure ZERO() and accessed via
this.circuits.pure.ZERO() without compilation errors.

---

Nitpick comments:
In `@contracts/src/token/witnesses/test/NonFungibleTokenWitnesses.test.ts`:
- Around line 81-89: Update the test for wit_NonFungibleTokenSK to assert the
function returns defensive copies instead of references: after calling
wit_NonFungibleTokenSK(ctx) (using NonFungibleTokenPrivateState.withSecretKey
and makeContext), verify that returnedSK is equal in value to SECRET_KEY but not
the same reference (e.g., mutating returnedSK does not change state.secretKey),
and similarly assert returnedState is not the identical object as the context
private state (mutating returnedState does not alter ctx.privateState); focus on
wit_NonFungibleTokenSK, NonFungibleTokenPrivateState.withSecretKey, and
makeContext to locate the code under test.
🪄 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: 7f7ee2b0-423e-410b-ac30-cce92af5c2cb

📥 Commits

Reviewing files that changed from the base of the PR and between a8c5225 and 2acabca.

📒 Files selected for processing (6)
  • contracts/src/token/NonFungibleToken.compact
  • contracts/src/token/test/mocks/MockNonFungibleToken.compact
  • contracts/src/token/test/nonFungibleToken.test.ts
  • contracts/src/token/test/simulators/NonFungibleTokenSimulator.ts
  • contracts/src/token/witnesses/NonFungibleTokenWitnesses.ts
  • contracts/src/token/witnesses/test/NonFungibleTokenWitnesses.test.ts

Comment thread contracts/src/token/NonFungibleToken.compact Outdated
Comment thread contracts/src/token/NonFungibleToken.compact Outdated
@andrew-fleming andrew-fleming requested a review from a team as a code owner May 6, 2026 20:34
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: NonFungibleToken Contract

1 participant