Skip to content

feat(multisig): add MultisigNativeShieldedToken mint/burn preset#632

Draft
0xisk wants to merge 18 commits into
mainfrom
feat/native-shielded-mint-burn-preset
Draft

feat(multisig): add MultisigNativeShieldedToken mint/burn preset#632
0xisk wants to merge 18 commits into
mainfrom
feat/native-shielded-mint-burn-preset

Conversation

@0xisk

@0xisk 0xisk commented Jun 26, 2026

Copy link
Copy Markdown
Member

Types of changes

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)

No linked issue — design is tracked separately.

Draft / WIP. Opened to share the shape for review. Not yet compiled against
the toolchain, and ECDSA verification is stubbed (stubVerifySignature) pending
the upstream secp256k1 / Keccak primitive. Not production-ready.

Blocked by #621 (feat/native-shielded-token-supply). This preset composes
the native shielded token core and extensions added there, so #621 must merge
first. This PR targets main, so part of the diff (the src/token/ native-token
work) belongs to #621 and will drop out of this diff once #621 lands.

What this adds

  • MultisigNativeShieldedToken preset — a single omnibus contract that mints
    and burns one shielded (Zswap) native token under an M-of-N ECDSA signer set.
    Mint targets contract-owned coins only (kernel.self()); the token is
    mint/burn-only (non-transferable). Composes the native-token core + supply +
    derived-nonce extensions with EcdsaSignerManager, rather than reimplementing
    mint/burn. Supersedes the bespoke examples/SignatureMintBurn.

Supporting multisig refactor (prerequisite for the preset)

  • rename SignerSignerManager; remove the legacy module
  • split the signer manager by scheme; add EcdsaSignerManager
  • regroup modules into subdirs (forwarder/, proposal/, treasury/, presets/)
  • move example contracts (Signature*, ProposalTreasury) into integration _mocks

PR Checklist

  • I have read the Contributing Guide
  • I have added tests that prove my fix is effective or that my feature works. (pending compile-verify)
  • I have added documentation for new methods or changes to existing method behavior. (pending)
  • CI Workflows Are Passing (draft — not yet building)

Further comments

The preset is the institutional omnibus shape: one contract, one token color
(bound to the minter via tokenType(domain, self())), M-of-N ECDSA approval,
contract-held coins (no coin secret key, no single-sig hop), and per-customer
balances tracked off-chain. The multisig refactor is the prerequisite that turns
the bespoke example contracts into reusable, composable modules. ECDSA / Keccak is
an upstream dependency; the verifier stays stubbed until it lands.

0xisk added 18 commits June 19, 2026 11:06
Start a clean branch off main carrying only the Native Shielded
Token Standard MIP and its reference Compact modules, so the
standard can be compiled, deployed, and integration-tested against
local infra in isolation from the broader exploration branch.

Includes:
* mip-xxxx-native-shielded-token.md (concise rewrite)
* NativeShieldedToken / NativeShieldedTokenFamily core modules
* NativeShieldedTokenDerivedNonce optional extension
* Ownable + AccessControl presets for both profiles
* Mocks for the simulator/test harness

Custody, conversion, and unshielded siblings are intentionally
left out; each is tracked by a separate companion MIP.
The native shielded token module set did not compile and lagged the
library's current conventions. This lands it cleanly:

* extensions: shorten the derived-nonce domain tag to
  "NativeShieldedToken:nonce" so pad(32, ...) fits 32 bytes. The prior
  37-byte tag was a hard compile error, and the shorter tag matches the
  MIP.
* token: drop the Initializable import and track _isInitialized inline
  per module, following FungibleToken and the per-module init change in
  the library. The shared Initializable flag collapses across modules in
  one directory, which the inline flag avoids.
* build: raise the language_version pragma to >= 0.23.0 across the
  native files to match the 0.31.0 toolchain.
* remove the four presets; they are out of scope for now.
* mip: re-justify the Family-profile requirement via converter
  composition and drop the stale shared-Initializable-flag rationale.
Move the flat multisig modules into concern-based subdirectories so the
package reads as composable building blocks rather than a flat pile:

* signer/    — Signer, SignerManager
* proposal/  — ProposalManager
* treasury/  — ShieldedTreasury, ShieldedTreasuryStateless, UnshieldedTreasury
* forwarder/ — ForwarderPrivate, ForwarderShielded, ForwarderUnshielded

Presets and mocks keep their locations; only their import paths change.
Mocks stay flat, so compiled artifact names (flat by basename) and every
TS test import are untouched. The five moved modules that reach into
utils/ gain an extra ../ in their import path.

Also mark V3's inlined mint/burn with a TODO pointing at the future
ShieldedToken module.

Pure move plus import-path change, no logic change. compact:multisig
compiles 24/24; the multisig suite passes 273/273.

Refs: #619
SignerManager<T> is an older generation of the same module as Signer<T>:
identical signer-set/threshold state, but without the init-safety guards
(assertInitialized / re-init protection) and the custom-setup
_setThreshold path that Signer<T> adds.

Remove the legacy module together with its mock, simulator, witnesses,
and test. The next commit renames Signer<T> into its place; splitting the
removal out first lets git record that as a rename (preserving Signer's
history) rather than a rewrite of this file.

Refs: #619
Rename the hardened Signer<T> into the SignerManager name freed by the
previous commit, keeping the more descriptive name on the init-safe
implementation. Git records this as a rename, so the module's history,
blame, and log --follow carry over.

* signer/Signer.compact -> signer/SignerManager.compact (module decl +
  assert prefix Signer: -> SignerManager:)
* mock, simulator, witnesses, and test renamed to match
* repoint the ShieldedMultiSigV3 import and migrate the preset tests to
  the new module name and messages (the init-safe module reports
  "threshold must not be zero" rather than the legacy "threshold must
  be > 0")

compact:multisig compiles 22/22; multisig suite passes 249/249.

Refs: #619
Move supply accounting out of the native shielded token core modules
into standalone extensions, because tracking it is a privacy trade-off,
not just modularity.

A contract-mediated burn is amount-private on its own: the coin ops
(receiveShielded / sendImmediateShielded / sendShielded) emit only
commitments and nullifiers, and the disclose() wrappers they require are
compiler permission markers, not disclosure sinks. The sole thing that
puts a burned amount into the public transcript is the supply-counter
ledger write. So a bare _burn / _burnFromContract hides the amount;
composing supply trades that privacy for on-chain auditability. Mint
amounts stay public regardless, via the protocol shieldedMints effect.

* token: strip _totalMinted / _totalBurned, the totalMinted /
  totalBurned / totalSupply getters, and _addMinted / _addBurned from
  NativeShieldedToken and NativeShieldedTokenFamily; mint/burn keep their
  coin ops and disclose wrappers.
* extensions: add NativeShieldedTokenSupply (scalar) and
  NativeShieldedTokenFamilySupply (per-domain), standalone modules
  mirroring NativeShieldedTokenDerivedNonce. The consumer pairs
  _addMinted / _addBurned with the matching mint/burn op. Supply getters
  no longer gate on init (a counter reading 0 pre-init is correct).
* tests/mocks: wire the supply extension into the unit + integration
  deployables and add unit and integration coverage for the standard.
Reshape the multisig package per the team layout review: reusable
primitives sit at the package root, the composable behaviour modules
become example contracts, and presets are named for what they do
instead of a version number.

* Root primitives: move SignerManager to the package root and add the
  extracted SignatureVerifier (commitment signer registry + threshold
  ECDSA-commitment verification), the single owner of the registry the
  signature presets share.
* examples/: house the behaviour modules (SignatureTreasury,
  SignatureMintBurn, ProposalTreasury) and the three forwarder example
  contracts.
* Presets named for behaviour: ShieldedMultiSigV2 becomes
  NativeShieldedStatelessTreasury, ShieldedMultiSigV3 becomes
  NativeShieldedMintBurn, ShieldedMultiSig becomes NativeShieldedProposal;
  add NativeShieldedTokenVault (mint/burn plus treasury under one signer
  set, the no-C2C composition).
* Rename treasury and forwarder modules to NativeShielded / Private
  naming and repoint every import path, including the test mocks.

Compiles green under SKIP_ZK (28/28 multisig contracts). The .ts test
stacks (specs, simulators, witnesses) are renamed on disk but their
rewiring to the new names is deferred to a follow-up.
Make signature verification a swappable per-scheme module, since Compact
cannot make `verify` generic over a signature scheme.

* SignerManager<T> stays the general signer registry (signer set,
  threshold, membership, add/remove), generic over the identity type:
  <Bytes<32>> commitments for the signature path, <Either<...>> for
  caller-authorized governance.
* EcdsaSignerManager (formerly SignatureVerifier) wraps
  SignerManager<Bytes<32>> and adds threshold ECDSA-commitment
  verification (instance salt, commitment derivation, verify). The
  cryptographic check is stubbed until the Compact ECDSA primitive lands.
  It is the single entrance the signature examples import; a future
  SchnorrSignerManager is a sibling of the same shape.
* SignatureTreasury, SignatureMintBurn, and the NativeShieldedTokenVault
  preset import EcdsaSignerManager. ProposalTreasury (caller-auth V1)
  keeps using SignerManager<Either> directly.

The signer count stays a single source of truth: one SignerManager
registry under EcdsaSignerManager, shared across the Vault's
mint/burn/execute via the same import path.

Compiles green under SKIP_ZK (28/28 multisig contracts). The .ts test
stacks remain renamed-on-disk and deferred to a follow-up.
The commitment domain separator padded the full descriptive string
"PrivateNativeShieldedForwarder:commitment" (41 bytes) to 32, which
exceeds the pad width and fails to compile. Use the module name alone
(30 bytes) as the unique domain tag.

This unblocks MockForwarderPrivate and the private-forwarder suite, which
the compiler wrapper had been masking as a false success.
The deployable forwarder example contracts (examples/*Forwarder) are
thin top-level wrappers in the same category as the presets, and their
basenames collide with the forwarder modules in the shared flat artifact
namespace. Remove them (and their now-stale preset tests and simulators)
from this branch; they return with the presets in a follow-up branch.

The forwarder modules and their Mock-based tests stay and remain the
coverage for forwarder behavior.
…esses

Every multisig contract has empty private state and declares no
witnesses, so each per-contract *Witnesses.ts was byte-identical to the
shared EmptyWitnesses.ts (the forwarder simulators already used it).
Delete them all and repoint the remaining simulators (SignerManager,
ProposalManager, ShieldedTreasury) at EmptyWitnesses, matching where
main is heading and shrinking the rebase surface.
…nerManager

The example modules (SignatureTreasury, SignatureMintBurn,
ProposalTreasury) are composable behaviors with no constructor, so each
gets a Mock top-level wrapper plus a simulator and spec, matching how
every other module in the package is tested. The specs carry over from
the old ShieldedMultiSigV2/V3/(V1) suites (rename-preserved) and target
the modules directly via EmptyWitnesses.

Also rename the signature primitive's test stack to match the renamed
module: MockSignatureVerifier -> MockEcdsaSignerManager, with a new
EcdsaSignerManager spec/simulator and the corrected assert prefix.

The example modules' duplicate-signer assertion now reads
"EcdsaSignerManager: duplicate signer".
The forwarder modules were renamed (ForwarderShielded ->
NativeShieldedForwarder, etc.) and their assert messages updated, but
the kept module tests still expected the old prefixes. Update the
toThrow expectations to the current "Native*Forwarder:" /
"PrivateNativeShieldedForwarder:" messages.
The deployable preset contracts (NativeShieldedMintBurn,
NativeShieldedProposal, NativeShieldedStatelessTreasury,
NativeShieldedTokenVault) need rework and move to a separate branch.
Remove them here and drop the now-dangling `presets/...` references from
the SignatureTreasury and SignatureMintBurn module docs, describing the
thin-wrapper composition pattern in prose instead.
Wire the previously-orphan MockShieldedTreasuryStateless to a simulator
and spec covering deposit, full/partial send (with change accounting),
and the over-send rejection. Uses the shared EmptyWitnesses.
Add MultisigNativeShieldedToken: a deployable preset composing the
NativeShieldedToken standard (core + derived-nonce extension) with
EcdsaSignerManager for institutional 2-of-3 ECDSA-authorized mint/burn
of a single shielded token.

* Coins are contract-owned (mint targets kernel.self()), so there is
  no coin secret key and no single-sig hop; the mint is constant-shape.
* Authorization is by circuit parameters (public keys + signatures),
  not msg.sender; the preset declares no witnesses.
* A dedicated replay nonce is consumed into each op's message hash, and
  the burn hash binds the spent coin's nonce so a signed burn cannot be
  redirected to another coin.
* Burns are amount-private: the supply extension is intentionally not
  composed, so no burned-amount cell is published.

ECDSA verification remains stubbed and the message hash uses
persistentHash pending the Compact ECDSA primitive; not for production
deployment until that lands.
Move the multisig examples (SignatureMintBurn, SignatureTreasury,
ProposalTreasury) out of src/multisig/examples/ into
test/integration/_examples/, converting each from a module (+ test
mock) into a self-contained top-level contract. They stay as usage
examples and now sit where they can double as integration-test
fixtures (composing production modules into one deployable contract).

Remove the now-redundant unit tests, simulators, and mocks for the
three. Integration tests are not added here; they are tracked in #630.

Also repoint the preset's doc cross-reference to the moved example.

Refs: #628, #630
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

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: 1a703f6f-7565-4e1c-84c5-4027145f541c

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/native-shielded-mint-burn-preset

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

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.

1 participant