Skip to content

refactor(token): restore zswap public key auth for fungible balances#512

Open
AndrewWestberg wants to merge 1 commit into
OpenZeppelin:mainfrom
AndrewWestberg:amw/revert_to_zswap
Open

refactor(token): restore zswap public key auth for fungible balances#512
AndrewWestberg wants to merge 1 commit into
OpenZeppelin:mainfrom
AndrewWestberg:amw/revert_to_zswap

Conversation

@AndrewWestberg
Copy link
Copy Markdown

@AndrewWestberg AndrewWestberg commented May 14, 2026

Summary

  • Restore FungibleToken public user accounts to ZswapCoinPublicKey so fungible balances can again be addressed with standard Midnight shielded wallet identities.
  • Replace the untrusted witness-based caller identity path with explicit zswapCoinSecretKey inputs and in-circuit deriveZswapCoinPublicKey(...) checks for all caller-authenticated exported
    circuits.
  • Update the token mock, simulator, witness helpers, and test suite to cover the new ownership model, and align the local Compact toolchain/runtime versions required by regenerated artifacts.

Why

The upstream accountId = persistentHash(secretKey) refactor fixed a real security issue in the old ownPublicKey() flow, but it also changed the user-facing recipient identity away from
Midnight shielded addresses. This PR keeps the security fix while restoring ZswapCoinPublicKey-based addressing, so a caller must now prove ownership of the public key that holds funds by
supplying the corresponding secret key directly to the contract.

Key Changes

  • FungibleToken.compact
  • revert user account branches from Bytes<32> to ZswapCoinPublicKey
  • remove computeAccountId / wit_FungibleTokenSK auth flow
  • add deriveZswapCoinPublicKey(zswapCoinSecretKey)
  • require explicit zswapCoinSecretKey on:
    • approve
    • transfer
    • _unsafeTransfer
    • transferFrom
    • _unsafeTransferFrom
  • Test/mocking infrastructure
  • update MockFungibleToken.compact
  • update FungibleTokenSimulator
  • simplify FungibleTokenWitnesses to private-state-only support
  • rewrite fungible-token tests around ZswapCoinPublicKey ownership and auth regression coverage
  • Tooling alignment
  • bump local Compact wrapper/toolchain defaults to 0.31.0
  • bump local @midnight-ntwrk/compact-runtime to 0.16.0

Security Impact

  • Removes reliance on ownPublicKey() / witness-returned public identity for fungible-token caller auth.
  • Ensures caller-owned actions only succeed when the provided secret key derives to the public key that owns the balance or allowance.
  • Preserves explicit-account internal primitives for contract-controlled flows without weakening their semantics.

Testing

  • npx -y @yarnpkg/cli-dist@4.12.0 fmt-and-lint
  • npx -y @yarnpkg/cli-dist@4.12.0 fmt-and-lint:ci
  • npx -y @yarnpkg/cli-dist@4.12.0 test (repo root)
  • npx -y @yarnpkg/cli-dist@4.12.0 test (packages/compact)
  • npx -y @yarnpkg/cli-dist@4.12.0 test (packages/simulator)
  • npx -y @yarnpkg/cli-dist@4.12.0 types (contracts)

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)

PR Checklist

Further comments

Summary by CodeRabbit

  • Refactor

    • Token transfer, approval, and spending operations now require an additional secret key parameter as input.
    • Updated internal account identity representation across token operations.
  • Chores

    • Updated @midnight-ntwrk/compact-runtime dependency from version 0.14.0 to 0.16.0.
    • Updated Compact version from 0.29.0 to 0.31.0.

Review Change Stack

Copilot AI review requested due to automatic review settings May 14, 2026 15:12
@AndrewWestberg AndrewWestberg requested review from a team as code owners May 14, 2026 15:12
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 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: acf87100-1be3-4ec5-a45f-9eccefcfcf9c

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 PR refactors FungibleToken's account identity model from Bytes<32> to ZswapCoinPublicKey, introducing a new deriveZswapCoinPublicKey circuit for caller identity derivation. Transfer and approval operations now require a caller-supplied secret key, with the identity derived on-circuit. The prior witness-based secret key mechanism was removed, and all dependent code—tests, simulator, mock, and witnesses—was updated accordingly.

Changes

FungibleToken Account Identity Refactoring

Layer / File(s) Summary
Core type definitions and ledger declarations
contracts/src/token/FungibleToken.compact
Introduces ZswapCoinPublicKey and ZswapCoinSecretKey types. Updates ledger _balances and _allowances to use Either<ZswapCoinPublicKey, ContractAddress> keys and exports ZERO() circuit with the new key type.
Account identity derivation mechanism
contracts/src/token/FungibleToken.compact
Adds deriveZswapCoinPublicKey pure circuit to derive public keys from 32-byte secret keys. Updates _isTargetZero helper to operate over the new key type. Removes prior computeAccountId circuit.
Public read-only operations
contracts/src/token/FungibleToken.compact
Updates balanceOf and allowance circuits to accept Either<ZswapCoinPublicKey, ContractAddress> parameters and use updated canonicalization generics for map access.
Transfer operations with secret key derivation
contracts/src/token/FungibleToken.compact
transfer and _unsafeTransfer now accept zswapCoinSecretKey: Bytes<32> parameter and derive caller identity via deriveZswapCoinPublicKey. Internal _transfer and _unsafeUncheckedTransfer updated to use new key types.
TransferFrom operations with secret key derivation
contracts/src/token/FungibleToken.compact
transferFrom and _unsafeTransferFrom now accept zswapCoinSecretKey: Bytes<32> and derive spender identity via deriveZswapCoinPublicKey.
Approval and allowance operations
contracts/src/token/FungibleToken.compact
approve now accepts zswapCoinSecretKey and derives owner identity. _approve, _spendAllowance, and _update updated to use new key types and canonicalization. Nested allowance map types changed to Either<ZswapCoinPublicKey, ContractAddress>.
Mint and burn operations
contracts/src/token/FungibleToken.compact
_mint, _unsafeMint, and _burn circuit signatures updated to accept Either<ZswapCoinPublicKey, ContractAddress> for account parameter.
Mock contract test API alignment
contracts/src/token/test/mocks/MockFungibleToken.compact
All token operations updated: replace Bytes<32> with ZswapCoinPublicKey, add zswapCoinSecretKey parameter to mutating operations, replace computeAccountId with deriveZswapCoinPublicKey.
Simulator implementation with secret key injection
contracts/src/token/test/simulators/FungibleTokenSimulator.ts
Add ZswapCoinPublicKey type alias. Update all method signatures to use new key type. Inject getCurrentZswapCoinSecretKey() into circuit calls for transfer/approve operations. Add deriveZswapCoinPublicKey method. Refactor privateState to track zswapCoinSecretKey instead of generic secret key.
Witness interface and private state refactoring
contracts/src/token/witnesses/FungibleTokenWitnesses.ts, contracts/src/token/witnesses/test/FungibleTokenWitnesses.test.ts
Rename FungibleTokenPrivateState.secretKey to zswapCoinSecretKey. Simplify FungibleTokenWitnesses factory to return empty object; remove wit_FungibleTokenSK witness. Update generate() and withSecretKey() methods. Tests verify private state field and empty witness object.
Comprehensive test coverage for new identity model
contracts/src/token/test/FungibleToken.test.ts
Refactor test helpers to derive publicKey from secret key and construct Either identities with publicKey in left branch. Update all test cases (transfer, approve, transferFrom, mint, burn, allowance) to use injectZswapCoinSecretKey and publicKey-based identities. Add deriveZswapCoinPublicKey verification assertions.
Dependency version updates
package.json, packages/compact/src/versions.ts, packages/simulator/package.json
Bump @midnight-ntwrk/compact-runtime from 0.14.0 to 0.16.0. Update COMPACT_VERSION from 0.29.0 to 0.31.0.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • OpenZeppelin/compact-contracts#468: Prior refactoring of FungibleToken account-identity derivation and circuit surfaces, also involving changes to simulator, mock, test, and witness infrastructure.

Suggested reviewers

  • 0xisk

Poem

🐰 A rabbit hops through tokens bright,
Where secret keys unlock the night,
Now public derives from private seed,
Each transfer blessed with crypto deed,
The ledgers dance in either's way,
Hopping forward to a brighter day! ✨

🚥 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 title directly and clearly describes the main change: restoring zswap public key authentication for fungible token balances, which is the core objective of this refactor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 1

🧹 Nitpick comments (2)
contracts/src/token/FungibleToken.compact (1)

675-685: 💤 Low value

Doc pseudocode doesn't match the actual implementation.

Line 679 documents the derivation as persistentHash("midnight:zswap-pk[v1]", zswapCoinSecretKey), but the implementation hashes a ZswapCoinPublicKeyPreimage struct containing a nested ZswapCoinSecretKey wrapper. The struct layout is what consumers must replicate to verify correctness — the current pseudocode is misleading.

📝 Suggested doc clarification
-   * ## Key Derivation
-   * `zswapCoinPublicKey = persistentHash("midnight:zswap-pk[v1]", zswapCoinSecretKey)`
+   * ## Key Derivation
+   * `zswapCoinPublicKey.bytes = persistentHash(ZswapCoinPublicKeyPreimage {
+   *   sep: "midnight:zswap-pk[v1]",
+   *   secretKey: ZswapCoinSecretKey { bytes: zswapCoinSecretKey }
+   * })`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/token/FungibleToken.compact` around lines 675 - 685, The
documentation for Zswap coin public key derivation is misleading: it states
zswapCoinPublicKey = persistentHash("midnight:zswap-pk[v1]", zswapCoinSecretKey)
but the code actually hashes a ZswapCoinPublicKeyPreimage struct that wraps the
secret in a ZswapCoinSecretKey before calling persistentHash; update the
docstring for the function that returns ZswapCoinPublicKey to describe the
actual preimage layout (mention ZswapCoinPublicKeyPreimage and
ZswapCoinSecretKey) and show that persistentHash("midnight:zswap-pk[v1]",
ZswapCoinPublicKeyPreimage) is what is computed so consumers can reproduce the
exact struct encoding used by persistentHash.
contracts/src/token/test/simulators/FungibleTokenSimulator.ts (1)

16-16: ⚡ Quick win

Import ZswapCoinPublicKey from the generated artifact instead of defining it locally.

FungibleTokenSimulator is inconsistent with other contract simulators in the codebase (UtilsSimulator, ZOwnablePKSimulator, ShieldedTokenSimulator, etc.), which all import ZswapCoinPublicKey from their respective generated artifacts. Defining it locally here risks diverging from the contract boundary if the artifact's type shape changes during regeneration.

♻️ Proposed fix
 import {
   type ContractAddress,
   type Either,
+  type ZswapCoinPublicKey,
   ledger,
   Contract as MockFungibleToken,
 } from '../../../../artifacts/MockFungibleToken/contract/index.js';
-
-type ZswapCoinPublicKey = { bytes: Uint8Array };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/src/token/test/simulators/FungibleTokenSimulator.ts` at line 16,
FungibleTokenSimulator defines a local type alias "ZswapCoinPublicKey" which
should be imported from the generated contract artifact to stay consistent with
other simulators; remove the local "type ZswapCoinPublicKey = { bytes:
Uint8Array }" and add an import of ZswapCoinPublicKey from the contract's
generated artifact in the FungibleTokenSimulator file so all references use the
artifact-provided type and remain in sync with future regenerations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/src/token/FungibleToken.compact`:
- Around line 80-95: Add an integration test that validates
deriveZswapCoinPublicKey() output against canonical Midnight zswap outputs: pick
several known secret keys (test vectors) produced/verified by Midnight (or from
ownPublicKey() in Midnight), compute expected public key bytes and assert
deriveZswapCoinPublicKey(secret) matches those pinned bytes; ensure the test
references the exact domain separator string "midnight:zswap-pk[v1]" and the
persistentHash-based derivation so any divergence in
ZswapCoinPublicKey/ZswapCoinSecretKey layout or hashing will fail CI.

---

Nitpick comments:
In `@contracts/src/token/FungibleToken.compact`:
- Around line 675-685: The documentation for Zswap coin public key derivation is
misleading: it states zswapCoinPublicKey =
persistentHash("midnight:zswap-pk[v1]", zswapCoinSecretKey) but the code
actually hashes a ZswapCoinPublicKeyPreimage struct that wraps the secret in a
ZswapCoinSecretKey before calling persistentHash; update the docstring for the
function that returns ZswapCoinPublicKey to describe the actual preimage layout
(mention ZswapCoinPublicKeyPreimage and ZswapCoinSecretKey) and show that
persistentHash("midnight:zswap-pk[v1]", ZswapCoinPublicKeyPreimage) is what is
computed so consumers can reproduce the exact struct encoding used by
persistentHash.

In `@contracts/src/token/test/simulators/FungibleTokenSimulator.ts`:
- Line 16: FungibleTokenSimulator defines a local type alias
"ZswapCoinPublicKey" which should be imported from the generated contract
artifact to stay consistent with other simulators; remove the local "type
ZswapCoinPublicKey = { bytes: Uint8Array }" and add an import of
ZswapCoinPublicKey from the contract's generated artifact in the
FungibleTokenSimulator file so all references use the artifact-provided type and
remain in sync with future regenerations.
🪄 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: 31f15169-aeff-4efc-ad8e-518c37d6e117

📥 Commits

Reviewing files that changed from the base of the PR and between 9b6b21a and a6b2408.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (9)
  • contracts/src/token/FungibleToken.compact
  • contracts/src/token/test/FungibleToken.test.ts
  • contracts/src/token/test/mocks/MockFungibleToken.compact
  • contracts/src/token/test/simulators/FungibleTokenSimulator.ts
  • contracts/src/token/witnesses/FungibleTokenWitnesses.ts
  • contracts/src/token/witnesses/test/FungibleTokenWitnesses.test.ts
  • package.json
  • packages/compact/src/versions.ts
  • packages/simulator/package.json

Comment thread contracts/src/token/FungibleToken.compact
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors FungibleToken identity handling to restore ZswapCoinPublicKey-based balances while moving caller authentication to explicit Zswap secret-key-derived public key checks. It also updates simulators, mocks, witnesses, tests, and Compact/runtime versions to support the new ownership model.

Changes:

  • Replaces fungible account identifiers with ZswapCoinPublicKey in token storage/API paths.
  • Adds explicit zswapCoinSecretKey inputs for caller-authenticated token circuits and derives public keys in-circuit.
  • Updates witness/private-state helpers, simulator/test infrastructure, and toolchain/runtime dependency versions.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
contracts/src/token/FungibleToken.compact Core token identity/auth refactor from account IDs to Zswap public keys.
contracts/src/token/test/mocks/MockFungibleToken.compact Updates mock circuit signatures and forwarding to new token API.
contracts/src/token/test/FungibleToken.test.ts Reworks fungible token tests around Zswap public keys and explicit secret-key auth.
contracts/src/token/test/simulators/FungibleTokenSimulator.ts Updates simulator types and injects private-state Zswap secret keys into authenticated calls.
contracts/src/token/witnesses/FungibleTokenWitnesses.ts Simplifies witness support to private-state key storage without caller-auth witnesses.
contracts/src/token/witnesses/test/FungibleTokenWitnesses.test.ts Updates witness/private-state tests for the renamed Zswap secret key field and empty witnesses.
packages/compact/src/versions.ts Bumps default Compact toolchain version.
package.json Bumps root Compact runtime dependency.
packages/simulator/package.json Bumps simulator Compact runtime dependency.
yarn.lock Updates locked Midnight runtime packages and transitive runtime dependency versions.
Comments suppressed due to low confidence (1)

contracts/src/token/FungibleToken.compact:460

  • Correct the spelling of receipient to recipient in this updated parameter documentation.
   * @param {Either<ZswapCoinPublicKey, ContractAddress>} to - The receipient of the transferred tokens.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

export pure circuit ZERO(): Either<Bytes<32>, ContractAddress> {
export pure circuit ZERO(): Either<ZswapCoinPublicKey, ContractAddress> {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. I exported ZswapCoinPublicKey explicitly from MockFungibleToken.compact and rebuilt the artifacts, so contracts/artifacts/MockFungibleToken/contract/index.d.ts now exposes export type ZswapCoinPublicKey = { bytes: Uint8Array }.

That lets the simulator and tests import the generated alias directly rather than duplicating the structural type.

* @description Derives a Zswap coin public key from a Zswap coin secret key.
* @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
* @returns {Bytes<32>} accountId - The computed account identifier.
* @returns {Bytes<32>} publicKey - The derived Zswap coin public key.
* @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner of the tokens to transfer.
* @param {Either<Bytes<32>, ContractAddress>} to - The receipient of the transferred tokens.
* @param {Either<ZswapCoinPublicKey, ContractAddress>} fromAddress - The owner of the tokens to transfer.
* @param {Either<ZswapCoinPublicKey, ContractAddress>} to - The receipient of the transferred tokens.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. I corrected the parameter documentation spelling from receipient to recipient in both _transfer and _unsafeUncheckedTransfer.

@@ -1,2 +1,2 @@
export const COMPACT_VERSION = '0.29.0';
export const COMPACT_VERSION = '0.31.0';
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. I synced the repo’s primary setup/docs surfaces to the new default Compact toolchain version:

  • README.md
  • packages/compact/README.md
  • .github/actions/setup/action.yml
  • .devcontainer/devcontainer.json
  • .github/ISSUE_TEMPLATE/01_bug_report.yml

I kept the update scoped to the files that directly advertise or install the default toolchain so the repo’s documented setup path now matches the wrapper default of 0.31.0.

Comment on lines +683 to +686
* @param {Bytes<32>} zswapCoinSecretKey - The caller's Zswap coin secret key.
* @returns {ZswapCoinPublicKey} zswapCoinPublicKey - The derived Zswap coin public key.
*/
export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
export pure circuit deriveZswapCoinPublicKey(zswapCoinSecretKey: Bytes<32>): ZswapCoinPublicKey {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. I restored the OpSec guidance on deriveZswapCoinPublicKey(...) and expanded the derivation docs to describe the actual ZswapCoinPublicKeyPreimage layout rather than the earlier shorthand.

The doc block now explicitly warns against logging/persisting the secret key, recommends secure randomness/storage, and calls out the risk of running the derivation in untrusted environments.

@AndrewWestberg AndrewWestberg force-pushed the amw/revert_to_zswap branch 2 times, most recently from 2e75c7b to a92dfe8 Compare May 14, 2026 15:43
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.

2 participants