API Reference

Multisig API

This page provides the full API for the Multisig modules.

For an overview of the modules, read the Multisig guide.

The page covers the following modules:

  • Signer: the registry of who may approve and how many approvals are required, generic over the signer identity type.
  • Secp256k1EcdsaMultisig: a t-of-n signer set of secp256k1 key commitments that verifies t ECDSA approvals over a digest.
  • ProposalManager: the lifecycle of transfer proposals, independent of the token kind.
  • ShieldedTreasury: holds one shielded coin per token color on the ledger and sends from it.
  • ShieldedTreasuryStateless: receives and sends shielded coins without keeping any ledger state.
  • UnshieldedTreasury: holds unshielded tokens with a tracked per-color balance.
  • ForwarderPrivate: forwards shielded deposits to a parent stored as a commitment.
  • ForwarderShielded: forwards shielded deposits to a public parent in the same transaction.
  • ForwarderUnshielded: forwards unshielded deposits to a public parent in the same transaction.
  • ShieldedMultiSigV2: preset; a stateless shielded wallet whose spends are authorized by t EIP-712 ECDSA signatures.
  • NativeShieldedTokenIssuer: preset; a native shielded token minted and burned only with t EIP-712 ECDSA signatures.
import "./node_modules/@openzeppelin/compact-contracts/multisig/Signer"<Bytes<32>>;

Signer<T> is generic over the signer identity type T, chosen at import time (for example Bytes<32>, JubjubPoint, or Either<ZswapCoinPublicKey, ContractAddress>). The module never resolves the caller; the consuming contract passes an identity in. It has no initialize: the consuming contract registers signers with _addSigner and sets the threshold with _changeThreshold or _setThreshold.


Ledger

_signers: Set<T>

ledger

#

The set of registered signer identities.

_signerCount: Uint<8>

ledger

#

The number of registered signers (at most 255).

_threshold: Uint<8>

ledger

#

The minimum number of approvals required. Zero until it is set.

Witnesses

None.

Circuits

assertSigner(caller: T) → []

circuit

#

Asserts that the given caller is an active signer.

Requirements:

  • caller must be a member of the signers registry.

assertThresholdMet(approvalCount: Uint<8>) → []

circuit

#

Asserts that the given approval count meets the threshold.

approvalCount is supplied by the consuming contract and is compared against the threshold only. This circuit never consults the signer registry. Ensuring the count reflects distinct, currently-registered signers is the consuming contract's responsibility, and a contract whose approval accounting is not itself gated by assertSigner gets no protection here.

The count is deliberately left unbounded by _signerCount: approvals collected before a signer was removed must still count toward a threshold the remaining signers satisfy, and bounding the count would reject those. See the stale-signer-set guidance in the module header.

Requirements:

  • Ledger threshold must be set (not be zero). An unconfigured module has a zero threshold, so this rejects before any approval count is compared.
  • approvalCount must be >= threshold.
k=9, rows=52

getSignerCount() → Uint<8>

circuit

#

Returns the current signer count. Returns zero when no signers have been registered.

k=6, rows=24

getThreshold() → Uint<8>

circuit

#

Returns the approval threshold. Returns zero when the threshold has not been set.

k=6, rows=24

isSigner(account: T) → Boolean

circuit

#

Returns whether the given account is an active signer.

_addSigner(signer: T) → []

circuit

#

Adds a new signer to the registry.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • signer must not already be an active signer.

_removeSigner(signer: T) → []

circuit

#

Removes a signer from the registry.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • signer must be an active signer.
  • Removal must not drop signer count below threshold.

_changeThreshold(newThreshold: Uint<8>) → []

circuit

#

Updates the approval threshold.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • newThreshold must not be zero.
  • newThreshold must not exceed the current signer count.
k=9, rows=51

_setThreshold(newThreshold: Uint<8>) → []

circuit

#

Sets the approval threshold without checking against the current signer count.

This is intended for use during contract construction or custom setup flows where signers may not yet be registered.

Leaving newThreshold above the signer count is permitted and expected mid-setup, and is not self-enforcing: assertThresholdMet compares approvalCount against the threshold alone, so a caller supplying a matching count satisfies it even against an empty registry. Contracts must not gate operations on assertThresholdMet while setup is incomplete. Lower the threshold with _setThreshold; _changeThreshold cannot lower one while the signer count is zero, because that would require a zero threshold.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy. Use _changeThreshold for operational threshold changes with signer count validation.

Requirements:

  • newThreshold must not be zero.
k=6, rows=38

import "./node_modules/@openzeppelin/compact-contracts/multisig/Secp256k1EcdsaMultisig"<2>;

Secp256k1EcdsaMultisig<#t> takes the threshold t as its import generic; initialize fixes the signer count n. It wraps a Signer<Bytes<32>> registry, requires ZKIR v3, and must be imported at one width per contract and never alongside Signer.


Ledger

_signers: Set<Bytes<32>>

ledger

#

The registered signer commitments. Re-exported from Signer.

_signerCount: Uint<8>

ledger

#

The number of registered signers, n. Re-exported from Signer.

_threshold: Uint<8>

ledger

#

The approval threshold, t once initialized. Re-exported from Signer.

_instanceSalt: Bytes<32>

sealed ledger

#

Deployment-supplied salt for signer commitments. Set at initialization and never changed. The registered commitments must have been derived under this same salt.

Witnesses

None.

Circuits

initialize<#n>(
  instanceSalt: Bytes<32>,
  signerCommitments: Vector<n, Bytes<32>>
) → []

circuit

#

Stores the instance salt and registers the signer set at threshold t. It writes the sealed salt, so it MUST only be called from the consuming contract's constructor. The set is final: signers must be known at deploy, and no circuit adds, removes, or replaces one afterwards.

Requirements:

  • Can only be called once.
  • instanceSalt should be cryptographically random.
  • signerCommitments must not contain duplicates.
  • n must be at least t.

assertApprovals(
  msgHash: Bytes<32>,
  pubkeys: Vector<t, Secp256k1Point>,
  signatures: Vector<t, Secp256k1EcdsaSignature>
) → []

circuit

#

Verifies t approvals over msgHash: distinct registered signers, each with a valid low-s signature. pubkeys and signatures are parallel: entry i of each belongs to the same signer.

The threshold check runs first, so an uninitialized registry fails before any signature work. Then all t(t-1)/2 pairs of commitments are compared, and each entry's commitment and signature are checked. Verification goes through secp256k1EcdsaVerifyLowS, so a high-s encoding is an invalid signature. The commitments of the t approvers are disclosed, so which signers approved is public; keys and signatures are not.

Cost (ZKIR v3): t = 1 is k=16, rows=40538; t = 2 is k=17, rows=64908; t = 3 is k=17, rows=94995.

This circuit verifies signatures over whatever msgHash it is handed. Replay, domain, and instance binding belong in the consumer's digest.

Requirements:

  • The module must be initialized.
  • No two entries may be the same signer.
  • Every public key must hash to a registered signer commitment.
  • Every signature must be valid over msgHash and use the low-s encoding.

calculateSignerId(pk: Secp256k1Point, salt: Bytes<32>) → Bytes<32>

pure

#

Computes a signer commitment from a secp256k1 public key, as persistentHash of the affine coordinates pkX and pkY, the salt, and the domain pad(32, "Secp256k1EcdsaMultisig:signer"). Pure, so the deployer can derive constructor commitments off-chain the same way the circuits derive them in-circuit.

The hash is over the Compact encoding of an internal struct, not a byte concatenation. Do not reimplement it: call this circuit. A wrong derivation registers commitments no key can satisfy.

Requirements:

  • pk must not be the secp256k1 identity point, which has no coordinates.

getSignerCount() → Uint<8>

circuit

#

Returns the number of registered signers.

k=6, rows=34

getThreshold() → Uint<8>

circuit

#

Returns the approval threshold.

k=6, rows=34

isSigner(commitment: Bytes<32>) → Boolean

circuit

#

Returns whether the given commitment is a registered signer.

k=8, rows=129

import "./node_modules/@openzeppelin/compact-contracts/multisig/ProposalManager";

Types

enum ProposalStatus { Inactive, Active, Executed, Cancelled, Expired }

enum

#

Proposal lifecycle states. Proposal.status stores Active, Executed, or Cancelled; getProposalStatus derives the other two.

  • Inactive: no proposal with that id exists. Never stored.
  • Active: created and not retired. Executable only while the block time is below Proposal.expiry.
  • Executed: _markExecuted ran. Terminal.
  • Cancelled: _cancelProposal ran. Terminal.
  • Expired: stored Active with an expiry at or before the current block time. Never stored.

enum RecipientKind { ShieldedUser, UnshieldedUser, Contract }

enum

#

Says how a recipient's 32 address bytes are to be read: as a Zswap coin public key, a user address, or a contract address. Contract is accepted by both toShieldedRecipient and toUnshieldedRecipient, so the tag alone does not determine the rail.

struct Recipient { kind: RecipientKind, address: Bytes<32> }

struct

#

A typed recipient. Construct it with the recipient helper circuits rather than directly.

struct Proposal { to: Recipient, color: Bytes<32>, amount: Uint<128>, expiry: Uint<64>, status: ProposalStatus }

struct

#

A transfer proposal: recipient, token color, amount, deadline, and stored lifecycle stage. expiry is in seconds since the Unix epoch. Reaching a terminal stage changes status only, so an executed or cancelled proposal keeps the deadline it was created with.

expiry marks when a proposal STOPS being executable, not when it starts: an Active proposal is executable while blockTime < expiry and inert once blockTime >= expiry. The module implements no execution delay; a consumer that needs one must compose it.

Ledger

_nextProposalId: Counter

ledger

#

Counter of proposals created. Its value is the id of the most recent proposal.

_proposals: Map<Uint<64>, Proposal>

ledger

#

Mapping from proposal id to proposal. Proposals are public: every recipient address, color, amount, expiry, and status is readable off-chain. An off-chain reader takes status as stored and, when it is Active, compares expiry with the current block time. Entries are never deleted.

Witnesses

None.

Circuits

shieldedUserRecipient(key: ZswapCoinPublicKey) → Recipient

circuit

#

Constructs a shielded user recipient.

unshieldedUserRecipient(addr: UserAddress) → Recipient

circuit

#

Constructs an unshielded user recipient.

contractRecipient(addr: ContractAddress) → Recipient

circuit

#

Constructs a contract recipient.

toShieldedRecipient(r: Recipient) → Either<ZswapCoinPublicKey, ContractAddress>

circuit

#

Converts a Recipient to a shielded send recipient. Handles both ShieldedUser and Contract kinds.

Requirements:

  • Recipient kind must be ShieldedUser or Contract.

toUnshieldedRecipient(r: Recipient) → Either<ContractAddress, UserAddress>

circuit

#

Converts a Recipient to an unshielded send recipient. Handles both UnshieldedUser and Contract kinds.

Requirements:

  • Recipient kind must be UnshieldedUser or Contract.

assertProposalExists(id: Uint<64>) → []

circuit

#

Asserts that a proposal exists.

Requirements:

  • Proposal with id must have been created.

assertProposalActive(id: Uint<64>) → []

circuit

#

Asserts that a proposal exists and is still executable. A status other than Active and a deadline that has passed fail with different errors.

Expiry is terminal. A transaction rejected for a stale read succeeds once rebuilt against fresh state, but block time only moves forward, so an expired proposal stays expired. Check the proposal's status before re-proving.

Requirements:

  • Proposal must exist.
  • Proposal status must be Active.
  • Proposal must not have expired.
k=7, rows=103

_createProposal(
  to: Recipient,
  color: Bytes<32>,
  amount: Uint<128>,
  expiry: Uint<64>
) → Uint<64>

circuit

#

Creates a new proposal that is executable until expiry, in seconds since the Unix epoch.

The recipient is NOT validated beyond being well-typed. Two consequences the consumer must handle:

  • A kind the execution rail cannot accept (an UnshieldedUser recipient on a shielded treasury, say) creates a proposal that passes here and then reverts in toShieldedRecipient at execution. It stays Active and unexecutable on that rail until _cancelProposal retires it or its expiry lapses.
  • A zero or otherwise unspendable address is accepted, and funds sent to it on execution are unrecoverable.

_nextProposalId is incremented and then read back for the map key, making this a pinned read-modify-write.

expiry is only checked against the present: a proposal cannot be born expired. There is deliberately no maximum horizon, so a caller may pass an effectively unreachable expiry. Consuming contracts that need a bounded lifetime must impose their own cap.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • amount must be greater than 0.
  • expiry must be in the future.
k=10, rows=883

_cancelProposal(id: Uint<64>) → []

circuit

#

Cancels a proposal, retiring it before its expiry.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • Proposal must be active and not expired.

_markExecuted(id: Uint<64>) → []

circuit

#

Marks a proposal as executed.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • Proposal must be active and not expired.

getProposal(id: Uint<64>) → Proposal

circuit

#

Returns the full proposal data. The returned status is the stored stage, so an Active entry may already be past its expiry. Prefer getProposalStatus when the effective status is what is wanted.

getProposalRecipient(id: Uint<64>) → Recipient

circuit

#

Returns the recipient of a proposal.

getProposalAmount(id: Uint<64>) → Uint<128>

circuit

#

Returns the amount of a proposal.

getProposalColor(id: Uint<64>) → Bytes<32>

circuit

#

Returns the token color of a proposal.

getProposalStatus(id: Uint<64>) → ProposalStatus

circuit

#

Returns the effective status of a proposal: the stored status unless it is Active, in which case expiry and the current block time decide between Active and Expired. Returns Inactive for an id that was never created rather than failing.

k=9, rows=252

import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasury";

Ledger

_coins: Map<Bytes<32>, QualifiedShieldedCoinInfo>

ledger

#

One stored coin per token color. A deposit of an existing color merges into the stored coin rather than adding a second entry. Every coin held (nonce, color, value, and Merkle index) is readable off-chain in plaintext.

_received: Map<Bytes<32>, Uint<128>>

ledger

#

Cumulative value received per token color. Only ever increases, so it is a running total rather than a balance; getTokenBalance reads _coins and is the canonical balance.

_sent: Map<Bytes<32>, Uint<128>>

ledger

#

Cumulative value sent per token color. The counterpart to _received; also only ever increases.

Witnesses

None.

Circuits

_deposit(coin: ShieldedCoinInfo) → []

circuit

#

Receives a shielded coin into the treasury.

The coin is first claimed at the protocol level via receiveShielded, which allocates the Merkle tree index required by insertCoin. The coin is then merged with any existing coin of the same color, or inserted as a new entry if no coin of that color exists.

Zero-value deposits are permitted and are not always a no-op. On a color the treasury does not yet hold, a zero-value coin is inserted and occupies that color's single slot until a later deposit merges into it; if a coin of that color already exists, the zero merges into it via mergeCoinImmediate and the stored value is unchanged. They may serve as signaling mechanisms when events are supported. They are not useful as privacy decoys, this treasury's holdings are public (see the module header).

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • Deposit must not cause received total overflow.

_send(
  recipient: Either<ZswapCoinPublicKey, ContractAddress>,
  color: Bytes<32>,
  amount: Uint<128>
) → ShieldedSendResult

circuit

#

Sends shielded tokens from the treasury.

Looks up the stored coin by color, verifies sufficient value, and executes the shielded send. sendShielded already routes any change back to this contract as a self-owned output. Unlike the stateless variant, this treasury RETAINS the change: it is recorded in the UTXO map for the next spend. The returned result.change therefore belongs to the treasury and must not be re-spent by the caller (doing so would double-spend the recorded coin).

The returned ShieldedSendResult carries the sent and change coins in plaintext, so sinking it (returning it from an entry point, writing it to ledger, passing it cross-contract) discloses them. _coins publishes only the retained change coin, and nothing at all when the send leaves no change, so the sent coin's plaintext is disclosed beyond anything in public state.

A self-addressed coin is auto-received by sendShielded but never recorded in _coins, so it would be stranded. This circuit therefore rejects sends to self.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • recipient must not be this contract's own address.
  • A coin of the given color must exist in the treasury.
  • The coin's value must be >= amount.
  • Send must not cause sent total overflow.

getTokenBalance(color: Bytes<32>) → Uint<128>

circuit

#

Returns the current token balance for a color. Reads the actual coin value from the UTXO map.

getReceivedTotal(color: Bytes<32>) → Uint<128>

circuit

#

Returns the cumulative received total for a color.

getSentTotal(color: Bytes<32>) → Uint<128>

circuit

#

Returns the cumulative sent total for a color.

getReceivedMinusSent(color: Bytes<32>) → Uint<128>

circuit

#

Returns the difference between cumulative received and cumulative sent totals for a color. Should equal getTokenBalance if accounting is consistent.

The subtraction is unguarded. _send cannot drive the sent total past the received total. Change re-insertion never increments _received so the invariant holds for any consumer that goes through _deposit and _send. A consumer that writes the exported _coins directly can break it, and this circuit then fails as an unprovable range violation rather than a named assert.


import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasuryStateless";

Keeping coin data off the ledger hides it from the public, not from a depositor. Whoever knows a deposited coin can find the spend that consumes it, recover the amount sent, and follow every later spend descending from its change.


Ledger

None. The module keeps no coin map, balances, or totals; the operator tracks coins off-chain.

Witnesses

None.

Circuits

_deposit(coin: ShieldedCoinInfo) → []

circuit

#

Receives a shielded coin into the treasury.

_send(
  coin: QualifiedShieldedCoinInfo,
  recipient: Either<ZswapCoinPublicKey, ContractAddress>,
  amount: Uint<128>
) → ShieldedSendResult

circuit

#

Sends shielded tokens using a caller-provided coin.

The coin is supplied by the caller rather than looked up from on-chain state; the token color is derived from coin.color. Executes the shielded send and returns the result untouched. sendShielded already routes any change back to this contract as a self-owned output, so result.change is a live, spendable coin. The implementing contract chooses what to do with it: retain it off-chain and pass it back as the coin for a later spend, or spend it onward to a different recipient in the same transaction via sendImmediateShielded. (Re-spending change is only meaningful when it goes somewhere other than self — sendShielded already routes it to self.) No balance or received/sent accounting is tracked on the ledger.

The returned ShieldedSendResult holds the sent and change coins in plaintext (nonce, color, value). Consuming it in-circuit is safe (passing result.change to sendImmediateShielded, for instance). Returning it from your entry point, writing it to ledger, or passing it to another contract publishes those coin details, including the amount left over from the coin that was spent. Note the compiler will NOT stop you: the disclose calls below clear the witness taint, so such a return compiles with no disclose of your own and no marker at the disclosure site. If you do not need in-circuit change handling, ignore the result. The change is recoverable off-chain from the transaction's private proving state.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • coin.value must be >= amount.

import "./node_modules/@openzeppelin/compact-contracts/multisig/UnshieldedTreasury";

Ledger

_balances: Map<Bytes<32>, Uint<128>>

ledger

#

Tracked balance per token color. Deposits add to it and sends subtract. It is the authority for the module's sufficiency and overflow guards and stays in step with the protocol balance only while every credit and debit goes through _deposit and _send.

Witnesses

None.

Circuits

_deposit(color: Bytes<32>, amount: Uint<128>) → []

circuit

#

Receives unshielded tokens into the treasury.

The token receive is executed at the protocol level first via receiveUnshielded. The balance map is then updated.

Zero-value deposits are permitted. While currently a no-op economically, they may serve as signaling mechanisms when events are supported. On a color the treasury has not held, a zero-value operation creates a zero-valued _balances entry, which getTokenBalance reports the same as absence.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • Deposit must not cause balance overflow.

_send(
  recipient: Either<ContractAddress, UserAddress>,
  color: Bytes<32>,
  amount: Uint<128>
) → []

circuit

#

Sends unshielded tokens from the treasury.

Recipients other than this contract are not validated. An unspendable recipient (such as a zero address) is accepted and permanently removes the funds from the contract. This is not a sanctioned burn (no unshielded equivalent of shieldedBurnAddress is exposed to contracts), so treat recipient validation as the consuming contract's responsibility.

A self-addressed send is auto-received by sendUnshielded but still debited from _balances, so the funds would be stranded. This circuit therefore rejects sends to self.

Access control is NOT enforced here. The consuming contract must gate this behind its own authorization policy.

Requirements:

  • recipient must not be this contract's own address.
  • amount must be non-zero. The ledger rejects zero-value outputs.
  • The treasury must hold sufficient balance for the given token color.

getTokenBalance(color: Bytes<32>) → Uint<128>

circuit

#

Returns the balance for a given token color.


import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderPrivate";

The commitment hides the parent from the public, not from a depositor. Whoever knows a deposited coin can find the drain that spends it, recover the drained amount, and test candidate parent keys against the sent coin's commitment.


Ledger

_parentCommitment: Bytes<32>

ledger

#

Commitment to the (parent, opSecret) pair, H(pad(32, "ForwarderPrivate:commitment") || parent || opSecret), published in place of the parent itself. The parent key alone is not secret, so opSecret supplies the hiding: without it a candidate parent could be hashed and matched against this value. Immutable after initialization.

_isInitialized: Boolean

ledger

#

Initialization flag, tracked per-module to avoid the compiler's shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the Initializable module for rationale.

Witnesses

None.

Circuits

initialize(parentCommitment: Bytes<32>) → []

circuit

#

Initializes the forwarder with a parent commitment. Called once from the consuming contract's constructor. The commitment is immutable after init.

Requirements:

  • Contract must not be initialized.
  • parentCommitment must not be the zero bytes. A zero commitment is the sole drain gate and would leave every deposited coin permanently unrecoverable (no preimage exists for default<Bytes<32>> under the domain-tagged hash).

_deposit(coin: ShieldedCoinInfo) → []

circuit

#

Receives a shielded coin into the forwarder's custody. No ledger write — coins dwell at the contract address until drained.

Requirements:

  • Contract must be initialized.
k=13, rows=6522

_drain(
  coin: QualifiedShieldedCoinInfo,
  parent: ZswapCoinPublicKey,
  opSecret: Bytes<32>,
  value: Uint<128>
) → ShieldedSendResult

circuit

#

Spends a previously-deposited shielded coin to the parent, a coin public key. The caller proves knowledge of (parent, opSecret) matching the stored commitment; the coin is sent as a plain shielded note, so the parent stays hidden from anyone who holds no deposited coin. If coin.value exceeds value, sendShielded routes the change back to this contract as a self-owned output; _drain returns it untouched via result.change. The implementing contract chooses what to do with it: leave it dwelling at the contract for a future drain, or spend it onward to a different recipient in the same transaction via sendImmediateShielded.

The parent is a ZswapCoinPublicKey, never a ContractAddress: a shielded send to a contract publishes that contract's address in cleartext on the transaction (the protocol must route the coin to a named contract), which would defeat the private-parent guarantee. A coin public key is encrypted inside the Zswap output and never appears on the public transcript.

Requirements:

  • Contract must be initialized.
  • parent must not be the zero key.
  • _calculateParentCommitment(parent.bytes, opSecret) == _parentCommitment.
  • coin.value must be >= value (enforced by sendShielded).

Losing the operational secret is permanent fund loss. It is the sole drain authorization, with no rotation, revocation, or recovery path. If the operator misplaces it, every shielded coin accumulated at this contract is forever inaccessible, equivalent to losing a hot-wallet private key. Back it up offline before the first deposit and treat it with the same hygiene as a signing key.

k=15, rows=23837

_calculateParentCommitment(parentAddr: Bytes<32>, opSecret: Bytes<32>) → Bytes<32>

pure

#

Computes the parent commitment from (parentAddr, opSecret). Pure circuit — used off-chain by the deployer to compute the constructor argument, and inside _drain for the preimage check. Callable without initialization.

The first hash input is a fixed domain tag (pad(32, "ForwarderPrivate:commitment")). The tag prevents preimage collisions with other persistentHash users in the system that hash two Bytes<32> values — a colliding preimage crafted under a different domain cannot satisfy this commitment.


import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderShielded";

Ledger

_parent: Either<ZswapCoinPublicKey, ContractAddress>

ledger

#

The parent recipient, stored generically as Either<ZswapCoinPublicKey, ContractAddress> for forward compatibility. initialize only ever writes the left (coin public key) arm today. See the module header.

_isInitialized: Boolean

ledger

#

Initialization flag, tracked per-module to avoid the compiler's shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the Initializable module for rationale.

Witnesses

None.

Circuits

initialize(parent: ZswapCoinPublicKey) → []

circuit

#

Initializes the forwarder with a parent recipient. Called once from the consuming contract's constructor. The coin public key is stored in the left arm of the generic _parent ledger field.

Requirements:

  • Contract must not be initialized.
  • parent must not be the zero key. A zero parent would forward every deposit to an unspendable key with no recovery path.

Beyond the zero-key check, the caller must ensure parent can spend forwarded coins. An unspendable parent strands every deposit with no recovery path.

_deposit(coin: ShieldedCoinInfo) → ShieldedSendResult

circuit

#

Receives a shielded coin and atomically forwards it to _parent. The coin is claimed at the protocol level via receiveShielded, then immediately re-sent via sendImmediateShielded to the stored parent recipient (always the left / coin-public-key arm today). Returns the coin sent to the parent, with no change: the whole value is forwarded.

The depositor must encrypt the forwarded output to the parent's encryption public key, obtained off-chain. The ledger cannot check that key against _parent, so a wrong or missing key leaves the coin spendable by the parent but invisible to its wallet.

Requirements:

  • Contract must be initialized.
  • The forwarded output must be encrypted to the parent's encryption public key. If unsure, hand the returned ShieldedSendResult to the parent so it can recover the coin.
k=15, rows=23197

import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderUnshielded";

Ledger

_parent: Either<ContractAddress, UserAddress>

ledger

#

The parent recipient, stored generically as Either<ContractAddress, UserAddress> for forward compatibility. initialize only ever writes the right (user address) arm today. See the module header.

_isInitialized: Boolean

ledger

#

Initialization flag, tracked per-module to avoid the compiler's shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the Initializable module for rationale.

Witnesses

None.

Circuits

initialize(parent: UserAddress) → []

circuit

#

Initializes the forwarder with a parent recipient. Called once from the consuming contract's constructor. The user address is stored in the right arm of the generic _parent ledger field.

Requirements:

  • Contract must not be initialized.
  • parent must not be the zero address. A zero parent would forward every deposit to an unspendable address with no recovery path.

Beyond the zero-address check, the caller must ensure parent can spend forwarded amounts. An unspendable parent strands every deposit with no recovery path.

_deposit(color: Bytes<32>, amount: Uint<128>) → []

circuit

#

Receives an unshielded amount of color and atomically forwards it to _parent via sendUnshielded (always the right / user-address arm today).

Requirements:

  • Contract must be initialized.
k=10, rows=563

import "./node_modules/@openzeppelin/compact-contracts/multisig/presets/ShieldedMultiSigV2"<2>;

ShieldedMultiSigV2<#t> composes Secp256k1EcdsaMultisig and ShieldedTreasuryStateless; t is both the threshold and the approval width. It requires ZKIR v3, and implementing contracts MUST NOT import Signer or Secp256k1EcdsaMultisig directly. The execute complexity is measured at t = 2.


Types

struct Proposal_Recipient { kind: Proposal_RecipientKind, address: Bytes<32> }

struct

#

Re-export of the ProposalManager Recipient, needed for the execute signature. Importing ProposalManager again instead would give the implementing contract a second, unwritten copy of its state.

Ledger

_domainSeparator: Bytes<32>

sealed ledger

#

The EIP-712 domain separator every execute digest is built under, derived from (name, version, instanceSalt). Set once at construction.

Witnesses

None.

Circuits

initialize<#n>(
  instanceSalt: Bytes<32>,
  signerCommitments: Vector<n, Bytes<32>>
) → []

circuit

#

Registers the signer set at threshold t and fixes the EIP-712 domain separator. It writes the sealed _domainSeparator, so it MUST only be called from the implementing contract's constructor and MUST NOT be exposed as a post-deploy circuit.

The EIP-712 domain fixed here is ("ShieldedMultiSigV2", "1", instanceSalt). chainId and verifyingContract are absent: no network identifier is available in-circuit, and a 32-byte Midnight address does not fit verifyingContract's address type. Replay protection comes from execute binding kernel.self() in its struct instead.

instanceSalt also keys the signer commitments, so a fresh value per deployment keeps one public key from producing the same commitment in two deployments, which would link the signer set across them.

Requirements:

  • Can only be called once.
  • instanceSalt should be cryptographically random.
  • signerCommitments must not contain duplicates.
  • n must be at least t.

deposit(coin: ShieldedCoinInfo) → []

circuit

#

Receives a shielded coin into the multisig treasury. Anyone may deposit. The coin is claimed through receiveShielded. No coin data is stored on the public ledger, so no balance is readable from it; a depositor can still follow the coin they deposited (see ShieldedTreasuryStateless).

The operator discovers the coin's Merkle tree index by subscribing to ZswapOutput events via the indexer, filtering by contract address, and extracting mt_index. Combined with the known ShieldedCoinInfo, this produces the QualifiedShieldedCoinInfo needed for spending.

k=13, rows=5898

execute(
  to: Proposal_Recipient,
  amount: Uint<128>,
  coin: QualifiedShieldedCoinInfo,
  pubkeys: Vector<t, Secp256k1Point>,
  signatures: Vector<t, Secp256k1EcdsaSignature>
) → ShieldedSendResult

circuit

#

Executes a shielded send authorized by threshold signatures.

The circuit reads the current nonce, increments it, then rebuilds the EIP-712 digest that signers must have signed off-chain: the Execute struct hashed under _domainSeparator, with type string Execute(bytes32 contractAddress,uint256 nonce,uint8 recipientKind,bytes32 recipient,bytes32 coinColor,uint256 amount). contractAddress is kernel.self(), so a signature authorizes exactly one operation on one instance. Approvals are checked by assertApprovals. The spend goes through ShieldedTreasuryStateless _send; read its warning on the returned ShieldedSendResult before returning it from an entry point.

to must be a ShieldedUser. A contract-addressed coin would be unclaimable, making the transaction malformed.

Requirements:

  • No two entries may be the same signer.
  • Every public key must hash to a registered signer commitment.
  • Every signature must be valid over the message hash and use the low-s encoding.
  • to.kind must be ShieldedUser.
  • coin.value must be >= amount.
k=17, rows=98969

_calculateSignerId(pk: Secp256k1Point, salt: Bytes<32>) → Bytes<32>

pure

#

Computes a signer commitment from a secp256k1 public key with calculateSignerId. Pure, so the deployer can derive constructor commitments off-chain.

Requirements:

  • pk must not be the secp256k1 identity point, which has no coordinates.

getNonce() → Uint<64>

circuit

#

Returns the nonce the next execute digest will bind to.

k=6, rows=34

getSignerCount() → Uint<8>

circuit

#

Returns the number of registered signers.

k=6, rows=34

getThreshold() → Uint<8>

circuit

#

Returns the approval threshold.

k=6, rows=34

isSigner(commitment: Bytes<32>) → Boolean

circuit

#

Returns whether the given commitment is a registered signer.

k=8, rows=129

import "./node_modules/@openzeppelin/compact-contracts/multisig/presets/NativeShieldedTokenIssuer"<2>;

NativeShieldedTokenIssuer<#t> composes Secp256k1EcdsaMultisig, NativeShieldedToken, and NativeShieldedTokenDerivedNonce; every mint and burn carries t approvals. It requires ZKIR v3, and implementing contracts MUST NOT import Signer or Secp256k1EcdsaMultisig directly. The gated circuits' complexity is measured at t = 2.


Ledger

_counter: Counter

ledger

#

Signature replay counter. Every operation (mint, mintToSelf, burn, burnFromSelf) reads it into its digest and increments it. The read is pinned, so at most one operation lands per block; the losing signers re-sign at the next nonce.

_domainSeparator: Bytes<32>

sealed ledger

#

The EIP-712 domain separator every operation digest is built under, derived from (name, version, instanceSalt). Set once at construction.

_derivedNonceCounter: Counter

ledger

#

The counter that feeds coin nonces, separate from _counter. Re-exported from NativeShieldedTokenDerivedNonce as _counter, renamed because this module's replay counter owns that name.

_instanceSalt: Bytes<32>

sealed ledger

#

Salt for the signer commitments and the EIP-712 domain. Re-exported from Secp256k1EcdsaMultisig.

_signers: Set<Bytes<32>>

ledger

#

The registered signer commitments. Re-exported from Secp256k1EcdsaMultisig.

_signerCount: Uint<8>

ledger

#

The number of registered signers. Re-exported from Secp256k1EcdsaMultisig.

_threshold: Uint<8>

ledger

#

The approval threshold. Re-exported from Secp256k1EcdsaMultisig.

_domain: Bytes<32>

sealed ledger

#

Domain separator fixed at construction; with the contract address it determines this token's color. Re-exported from NativeShieldedToken.

_name: Opaque<"string">

sealed ledger

#

The immutable token name. Re-exported from NativeShieldedToken.

_symbol: Opaque<"string">

sealed ledger

#

The immutable token symbol. Re-exported from NativeShieldedToken.

_decimals: Uint<8>

sealed ledger

#

The immutable token decimals. Re-exported from NativeShieldedToken.

_isInitialized: Boolean

ledger

#

The token module's initialization flag. Re-exported from NativeShieldedToken.

Witnesses

None.

Circuits

initialize<#n>(
  instanceSalt: Bytes<32>,
  tokenDomain: Bytes<32>,
  name_: Opaque<"string">,
  symbol_: Opaque<"string">,
  decimals_: Uint<8>,
  signerCommitments: Vector<n, Bytes<32>>
) → []

circuit

#

Registers the signer set at threshold t, initializes the token metadata and domain, and fixes the EIP-712 domain separator ("NativeShieldedTokenIssuer", "1", instanceSalt). It writes the token module's sealed domain and _domainSeparator, so it MUST only be called from the implementing contract's constructor and MUST NOT be exposed as a post-deploy circuit.

instanceSalt is both the EIP-712 domain salt and the signer-commitment salt, so a fresh value per deployment keeps one public key from producing the same commitment in two deployments. The token color is tokenType(tokenDomain, kernel.self()). Only coins of this color can be burned through this contract.

Requirements:

  • Can only be called once.
  • instanceSalt should be cryptographically random.
  • signerCommitments must not contain duplicates.
  • n must be at least t.

_calculateSignerId(pk: Secp256k1Point, salt: Bytes<32>) → Bytes<32>

pure

#

Computes a signer commitment from a secp256k1 public key with calculateSignerId. Pure, so the deployer can derive constructor commitments off-chain.

Requirements:

  • pk must not be the secp256k1 identity point, which has no coordinates.

mint(
  amount: Uint<64>,
  recipient: ZswapCoinPublicKey,
  pubkeys: Vector<t, Secp256k1Point>,
  signatures: Vector<t, Secp256k1EcdsaSignature>
) → ShieldedCoinInfo

circuit

#

Mints a new shielded coin of this contract's token to recipient, authorized by threshold signatures over Mint(bytes32 contractAddress,bytes32 recipient,uint256 nonce,uint256 amount).

The amount is public. The coin nonce comes from NativeShieldedTokenDerivedNonce, so anyone can test a candidate recipient against the coin commitment.

The returned coin info is the only copy available to the recipient; deliver it out of band.

Requirements:

  • No two entries may be the same signer.
  • Every public key must hash to a registered signer commitment.
  • Every signature must be valid over the mint message hash and use the low-s encoding.
  • recipient is not zero.
k=17, rows=89807

mintToSelf(
  amount: Uint<64>,
  pubkeys: Vector<t, Secp256k1Point>,
  signatures: Vector<t, Secp256k1EcdsaSignature>
) → ShieldedCoinInfo

circuit

#

Mints a new shielded coin of this contract's token to the contract itself, claimed in the same call, authorized by threshold signatures over MintToSelf(bytes32 contractAddress,uint256 nonce,uint256 amount).

The returned coin info is the contract's only record of the holding; persist it for burnFromSelf.

Requirements:

  • No two entries may be the same signer.
  • Every public key must hash to a registered signer commitment.
  • Every signature must be valid over the mint-to-self message hash and use the low-s encoding.
k=17, rows=85586

burn(
  coin: ShieldedCoinInfo,
  amount: Uint<128>,
  refundTo: ZswapCoinPublicKey,
  pubkeys: Vector<t, Secp256k1Point>,
  signatures: Vector<t, Secp256k1EcdsaSignature>
) → Maybe<ShieldedCoinInfo>

circuit

#

Burns amount from a holder's coin, authorized by threshold signatures over Burn(bytes32 contractAddress,bytes32 refundTo,uint256 nonce,uint256 amount). The holder's transaction pays coin in; the contract receives it and burns it in the same call. Any change goes to refundTo. Returns the refund coin, or none if the coin was burned in full.

The returned refund coin info is the only copy available to refundTo; deliver it out of band.

amount, coin.value, and refundTo stay hidden only while coin carries a nonce the observer does not know. A coin passed straight from mint has a derived nonce, so the burn links to its mint and all three are recoverable by trial. For an unlinkable burn the holder re-nonces the coin first with a wallet self-transfer; the contract cannot enforce this.

Requirements:

  • No two entries may be the same signer.
  • Every public key must hash to a registered signer commitment.
  • Every signature must be valid over the burn message hash and use the low-s encoding.
  • coin.color is this contract's token color.
  • amount is less than or equal to coin.value.
  • refundTo is not zero.
k=17, rows=119818

burnFromSelf(
  coin: QualifiedShieldedCoinInfo,
  amount: Uint<128>,
  pubkeys: Vector<t, Secp256k1Point>,
  signatures: Vector<t, Secp256k1EcdsaSignature>
) → Maybe<ShieldedCoinInfo>

circuit

#

Burns amount from a coin this contract holds, authorized by threshold signatures over BurnFromSelf(bytes32 contractAddress,uint256 nonce,uint256 amount,bytes32 coinNonce,uint256 coinValue).

The operator supplies the QualifiedShieldedCoinInfo from the off-chain UTXO pool. On a partial burn the change is auto-received by the contract and returned; it replaces coin as the contract's holding, so the operator must persist it. The digest binds coin.nonce and coin.value, so approvals authorize one specific holding rather than an amount. Pick the coin before collecting signatures.

Requirements:

  • No two entries may be the same signer.
  • Every public key must hash to a registered signer commitment.
  • Every signature must be valid over the burn-from-self message hash and use the low-s encoding.
  • coin.color is this contract's token color.
  • amount is less than or equal to coin.value.
  • coin.nonce and coin.value match the approved coin.
k=17, rows=104973

getNonce() → Uint<64>

circuit

#

Returns the counter the next operation's digest will bind to. Submitted as a transaction, it pins _counter and conflicts with any same-block operation.

k=6, rows=34

name() → Opaque<"string">

circuit

#

Returns the token name.

k=6, rows=45

symbol() → Opaque<"string">

circuit

#

Returns the token symbol.

k=6, rows=45

decimals() → Uint<8>

circuit

#

Returns the token decimals.

k=6, rows=45

tokenColor() → Bytes<32>

circuit

#

Returns this contract's token color, tokenType(tokenDomain, kernel.self()).

k=13, rows=3847

getSignerCount() → Uint<8>

circuit

#

Returns the number of registered signers.

k=6, rows=34

getThreshold() → Uint<8>

circuit

#

Returns the approval threshold.

k=6, rows=34

isSigner(commitment: Bytes<32>) → Boolean

circuit

#

Returns whether the given commitment is a registered signer.

k=8, rows=129

On this page

SignerLedger_signers_signerCount_thresholdWitnessesCircuitsassertSignerassertThresholdMetgetSignerCountgetThresholdisSigner_addSigner_removeSigner_changeThreshold_setThresholdSecp256k1EcdsaMultisigLedger_signers_signerCount_threshold_instanceSaltWitnessesCircuitsinitializeassertApprovalscalculateSignerIdgetSignerCountgetThresholdisSignerProposalManagerTypesProposalStatusRecipientKindRecipientProposalLedger_nextProposalId_proposalsWitnessesCircuitsshieldedUserRecipientunshieldedUserRecipientcontractRecipienttoShieldedRecipienttoUnshieldedRecipientassertProposalExistsassertProposalActive_createProposal_cancelProposal_markExecutedgetProposalgetProposalRecipientgetProposalAmountgetProposalColorgetProposalStatusShieldedTreasuryLedger_coins_received_sentWitnessesCircuits_deposit_sendgetTokenBalancegetReceivedTotalgetSentTotalgetReceivedMinusSentShieldedTreasuryStatelessLedgerWitnessesCircuits_deposit_sendUnshieldedTreasuryLedger_balancesWitnessesCircuits_deposit_sendgetTokenBalanceForwarderPrivateLedger_parentCommitment_isInitializedWitnessesCircuitsinitialize_deposit_drain_calculateParentCommitmentForwarderShieldedLedger_parent_isInitializedWitnessesCircuitsinitialize_depositForwarderUnshieldedLedger_parent_isInitializedWitnessesCircuitsinitialize_depositShieldedMultiSigV2TypesProposal_RecipientLedger_domainSeparatorWitnessesCircuitsinitializedepositexecute_calculateSignerIdgetNoncegetSignerCountgetThresholdisSignerNativeShieldedTokenIssuerLedger_counter_domainSeparator_derivedNonceCounter_instanceSalt_signers_signerCount_threshold_domain_name_symbol_decimals_isInitializedWitnessesCircuitsinitialize_calculateSignerIdmintmintToSelfburnburnFromSelfgetNoncenamesymboldecimalstokenColorgetSignerCountgetThresholdisSigner