DRAFT Multisig ZIP v1 for Comment
| Field | Description |
|---|---|
| zip | ZIP:-XXXX (to be assigned by sponsoring Pillar) |
| title | Protocol-Native Mutable Multisig Accounts |
| author | 0x3639 (specification) |
| implementation | digitalSloth, zenon-network/go-zenon#72 |
| status | Draft |
| type | Hard Fork — activated at MultisigForkHeight, a hard-coded momentum height (§9) |
| acceptance | Community review; code merges to dev independently of activation |
| activation | MultisigForkHeight (TBD), scheduled per the Governance ZIP’s §11 readiness process; independent of GovernanceForkHeight — either order, or the same height (§9). New networks MAY enable from genesis. |
| created | 2026-07-26 |
| updated | 2026-07-28 |
| requires | Nothing. Merge and activation are both independent of the Governance ZIP; the two fork heights MAY be scheduled in either order or coincide (§9). |
| license | GNU General Public License v3.0 |
| link | zenon-network/go-zenon#72 |
| verified against | branch feature/multisig-addresses, HEAD 8f2eddb2b4790e022fe8057aac739c4c99788e8d — the PR squashed to a single commit on the dev base; prior pins (10a67fa, b1aa143) are no longer in its history |
| companion | Governance ZIP (SetNetworkAdministrator / networkAdmin) |
Abstract
This ZIP adds native multisig accounts to the protocol: a third address type (leading byte 2, beside user 0 and embedded-contract 1) whose account blocks are valid only when carrying exactly N-of-M ed25519 threshold signatures, verified at momentum inclusion against a mutable signing policy stored in a new registry contract, MultisigContract (z1qxemdeddedxmultysygxxxxxxxxxxxxx42zwd4).
A multisig address is derived from its immutable creation event — 0x02 || SHA3-256(creatorPubKey || nonce)[:19] — and is therefore independent of the policy: signer rotation, threshold changes, and locking never change the address. Anyone told the (creatorPubKey, nonce) pair can recompute the address offline. This stability is what makes a multisig address a genuine drop-in wherever the protocol grants authority to an address — most importantly as the networkAdmin of the companion Governance ZIP, under which the multisig operates the Bridge, Liquidity, and Plasma admin surfaces while Pillars retain the power to repoint the role.
The registry exposes exactly two methods — CreateMultisig and ChangePolicy — and the multisig contract has no governance-caller path, no admin role, and no authority over any other contract. Its power is whatever other contracts choose to grant its address.
There is no key generation ceremony. This is plain signature counting over signers’ pre-existing ordinary keypairs, unrelated to the Bridge’s TSS.
Motivation
Every operational authority on the network today is a single ed25519 keypair: the Bridge/Liquidity administrator, the plasma GovernanceAddress placeholder, the legacy spork signer. A single key cannot be partially lost, partially compromised, or partially unavailable — it fails absolutely, and two of those keys have already failed in practice (the spork signer’s whereabouts are unknown; the plasma placeholder gates live consensus parameters on one holder).
Moving operational authority to a committee requires an account the protocol itself understands:
- M-of-N, not 1-of-1. Compromise or loss of fewer than the threshold’s complement of keys is survivable; no single signer can act alone.
- Mutable membership, immutable identity. Signers must be rotatable (people leave, keys age) without invalidating every delegation that points at the account. Deriving the address from the creation event rather than the policy achieves this.
- Nothing contract-specific. An account-layer primitive works for every present and future authority grant — the same multisig address can be Bridge admin, Plasma parameter-setter, and treasury holder without any of those contracts knowing what a multisig is.
- On-chain verifiability. A delegation target whose threshold, signer set, lock state, and pending changes are all readable from chain state — a property the companion Governance ZIP’s Type-2 voting flow depends on (§8).
Specification
1. Address Type and Derivation
MultisigAddrByte = byte(2) // common/types/address.go:22
types.IsMultisigAddress(addr) // addr[0] == 2
// address.go:131-146
address = 0x02 || SHA3-256(creatorPubKey || bigEndian64(nonce))[:19]
The address commits to the immutable creation event, not the mutable policy. Consequences, both normative:
- Signer rotation never changes the address. Delegations to the address survive every policy change.
- The address is offline-verifiable. Given
(creatorPubKey, nonce), anyone recomputes it and confirms it binds to the published creator and creation event. Note the limit of this proof: it establishes derivation, not that the creator didn’t grind nonces for a preferred address. That is immaterial to security — an address has no meaning beyond identity — and the substantive verification is always the signer list and threshold behind it, viagetPolicy.
2. Registry Contract
MultisigContract = z1qxemdeddedxmultysygxxxxxxxxxxxxx42zwd4, added to types.EmbeddedContracts. It stores one record per multisig address:
MultisigRecord {
Active MultisigPolicy // the policy in force
Pending *MultisigPolicy // at most one staged change; nil if none
PendingHeight uint64 // momentum height the change was staged at
}
MultisigPolicy {
Threshold uint8 // exact number of signatures required
Signers []ed25519.PublicKey // canonical: ascending byte-lexicographic, deduplicated
Locked bool // monotonic; see §4
}
Promote(rec, H) is the single source of truth for “which policy is active at height H”: if Pending != nil && PendingHeight + MultisigPolicyMaturityDelay <= H, the pending policy is the effective active. It is a pure function called identically by the verifier (read path), ChangePolicy (write path), and the RPC (§8) — every node and every consumer agrees on the effective policy by construction.
3. CreateMultisig(nonce uint64, threshold uint8, signers bytes[])
Permissionless — any user account may call it, with two structural requirements:
- The creator’s public key MUST be in the submitted signer set. Every multisig on chain was therefore demonstrably authorized by at least one of its own members; no one can create an account attributing membership to others without any member’s consent. The creator MUST be a single-sig user account — a multisig sender is rejected explicitly with
ErrMultisigCreatorMustBeSingleSig(address derivation and the membership check both key off the sender’s public key, which a multisig block never has). - The policy MUST be valid:
MinSigners <= len(signers) <= MaxSigners,2 <= threshold <= len(signers), each signer exactly 32 bytes, no duplicates. Wire input need not be sorted: the contract canonicalizes (sorts ascending byte-lexicographic in place) and stores the canonical form; independent implementations MUST accept unsorted calldata and canonicalize identically, not reject it. 1-of-N is rejected — that is a single-signer account.
The address is derived per §1; creation is rejected if a record already exists (ErrMultisigAlreadyExists). Creation takes effect immediately — there is no maturity delay, because there is no prior authority to protect.
The send MUST carry exactly MultisigCreationBurnAmount (1 ZNN) in ZnnTokenStandard; the contract burns it via a contract-send to the Token contract. This is an anti-squat cost at parity with comparable registrations, not a fee that accrues to anyone. Plasma: 2 × EmbeddedSimple (covers the burn send). ChangePolicy remains zero-amount at EmbeddedSimple.
Because the address depends on creatorPubKey, who submits the creation determines the address: the same signer set created by two different members yields two different (equally valid) addresses. Groups SHOULD agree in advance who creates, so the address verified offline matches the one proposed on-chain.
4. ChangePolicy(threshold uint8, signers bytes[], lock bool)
The only mutator, authorized by the multisig’s own current policy, twice (defence in depth):
- Send side: the call is itself a multisig block from the multisig’s own address, so momentum content verification (§6.3) has already demanded threshold signatures under the policy active at the block’s inclusion height.
- Receive side: the contract re-derives the effective policy at the receive frontier via the same
Promote(), materializes any matured pending change before authorizing (never silently discarding it), then re-verifies the block’s threshold signatures against the now-current active policy — if a rotation matured in the gap between the send’s inclusion and this receive, the superseded signer set is rejected withErrMultisigStaleAuthorityrather than allowed to mutate or lock the account. Finally it rejects withErrMultisigLockedif the effective active policy is locked.
The new policy does not take effect immediately: it is staged as Pending and matures after MultisigPolicyMaturityDelay (60 momentums, ~10 minutes). A second change while one is pending replaces it and resets the clock. ChangePolicy operates only on sendBlock.Address — there is no target parameter, and no path by which any other account or contract (including GovernanceContract) can rotate a multisig’s policy.
Locked is monotonic. Once a policy with lock == true matures, every subsequent ChangePolicy is rejected, permanently. There is no unlock. A locked multisig retains full account capability under its frozen policy; it can never again change who controls it.
5. Account-Block Format
AccountBlock gains one optional field:
MultisigAuth *MultisigAuth `json:"multisigAuth,omitempty" rlp:"optional"` // excluded from hash
type MultisigAuth struct { Signatures [][]byte }
Excluded from ComputeHash, mirroring the singular Signature field: signatures attach to an already-frozen, already-PoW’d block. A non-nil MultisigAuth on any non-multisig block — user or embedded — is rejected by the verifier (ErrABMultisigAuthMustBeZero); the field is exclusively for type-2 senders. Signing flow (helpers in wallet/multisig.go): build the template → compute PoW and hash → each signer signs the same hash with an ordinary keypair, off-chain, in any order → attach the collected signatures → submit once.
6. Verifier Rules
For any block whose sender is a multisig address:
- The multisig fork MUST be enforced at the block’s
MomentumAcknowledgedheight (MomentumAcknowledged.Height >= MultisigForkHeight), elseErrMultisigNotActivated. PublicKeyandSignatureMUST be empty;MultisigAuthMUST be present.- Authoritative authorization is live, at inclusion time (consensus rule). Momentum content verification verifies each multisig block’s threshold signatures against the policy active at the including momentum’s height, read from the committed state as of the previous momentum — deterministic and identical on every node. The record-read →
Promote()→VerifyThresholdSignaturessequence — packaged asVerifyMultisigAuth(storage, addr, H, hash, signatures)— is the single shared primitive; implementations MUST NOT fork these semantics between call sites (checklist item 6). A block signed by a superseded signer set can therefore never be included once the superseding rotation has matured, regardless of when or at what acknowledged height it was signed. - Admission prefilter (node-local, best-effort). The account-block verifier applies the same rules against the policy at the node’s current frontier before pooling a block: the signature count MUST equal the threshold exactly — not “at least” (
ErrMultisigThresholdNotMet; over-signing is malformed); each signature MUST ed25519-verify against a distinct active signer (trial-matched; no signer indices on the wire); no record →ErrMultisigNoPolicy. This rejects invalid blocks early and cheaply but is not the authoritative gate — rule 3 is. Scope (normative): this prefilter runs only when a block is admitted into the local pool (network gossip or RPC submission); it MUST NOT gate blocks arriving as the content of a momentum, which are authorized solely by rule 3 (checklist item 5). - Staleness bound (hygiene, not security). A multisig block whose
MomentumAcknowledgedlags the including height by more thanMultisigMaxMaLag(one week of momentums) is dropped as stale backlog. This is expressly not a security control — rule 3 already makes an old signer set’s signatures worthless — it only bounds how long dead blocks linger in pools. Scope (normative): the bound is enforced at pool admission and by the producer-side filter (rule 6) only; it MUST NOT be applied to momentum-carried blocks — a momentum’s validity never depends on it (checklist item 5). Sole constraint:MultisigMaxMaLag >= MultisigPolicyMaturityDelay(60480 ≥ 60), so the window is never narrower than one maturity cycle. - Producer-side filter. An honest producer excludes from the momentum it builds any pooled multisig block that is stale (rule 5) or that no longer satisfies live authorization (rule 3, evaluated against the same registry snapshot) — otherwise a rotation-invalidated “poison block” would fail content verification and stall that producer’s momentum production indefinitely (
pillar/worker_momentum.go). - Exclusivity: any non-multisig block carrying a non-nil
MultisigAuthis rejected (§5).
The producer/address-binding check is a no-op for multisig senders: authority is the in-state policy checked above; the address-to-creation binding was enforced once, at creation.
Capability limits (verified, normative). Multisig accounts are first-class senders and receivers — they hold and transfer every token standard, fuse/receive QSR plasma or use PoW, stake, issue tokens, use HTLC/Accelerator/Bridge user methods, and can be a Pillar owner (and therefore propose governance actions) or reward address — with three exceptions:
- Momentum production. A momentum’s producer is always
PubKeyToAddressof a single public key; no multisig momentum format exists, so a type-2 address can never be a producer identity. The Pillar contract nevertheless accepts a multisigProducerAddressat registration, yielding a Pillar that can never produce. Wallets and registration tooling MUST warn on a type-2 producer address; adding propose-time rejection in the Pillar contract is a possible future change, out of scope here. RegisterLegacy(Pillar) and 3.SwapRetrieveAssets(Swap) bind a singular public key to the sender via legacy swap proofs and are unusable by multisig accounts.
7. Constants and Errors
| Constant | Value | Rationale |
|---|---|---|
MinSigners |
2 | 1-of-N is a single-sig account; rejected |
MaxSigners |
16 | Bounds the verify loop and block/state size |
| Threshold bounds | 2 <= t <= len(signers) |
1-of-N rejected at the policy level too |
MultisigPolicyMaturityDelay |
60 momentums (~10 min) | Staged changes are publicly visible before taking effect |
MultisigMaxMaLag |
7 × MomentumsPerEpoch (~1 week) |
Backlog-hygiene staleness bound, not a security control (§6.5); MUST be ≥ the maturity delay |
MultisigCreationBurnAmount |
1 ZNN | Burned by CreateMultisig; anti-squat cost (§3) |
MultisigForkHeight |
TBD | Hard-fork activation height; gates verifier and contract methods (§9) |
| Plasma | CreateMultisig: 2 × EmbeddedSimple (incl. burn send); ChangePolicy: EmbeddedSimple |
— |
Errors (typed returns; no panics on malformed input): ErrMultisigLocked, ErrMultisigAlreadyExists, ErrMultisigNoPolicy, ErrMultisigInvalidPolicy, ErrMultisigNotActivated, ErrMultisigThresholdNotMet, ErrMultisigStaleAuthority, ErrMultisigCreatorMustBeSingleSig; ErrMultisigCorruptSigners on a corrupted stored signer blob; verifier-level ErrABMultisigAuthMustBeZero / ErrABMultisigAuthMissing.
8. RPC and Embedded-Contract Variable Visibility
New namespace embedded.multisig:
GetPolicy(address, height?) -> MultisigRecordInfo | null— the maturity-aware view at the given height (frontier if omitted), computed via the samePromote()the consensus paths use:
{
"active": { "threshold": 3, "signers": ["…"], "locked": false },
"pending": { "threshold": 2, "signers": ["…"], "locked": false },
"pendingHeight": 123456
}
GetPolicy is address-keyed; there is deliberately no enumeration RPC (the registry is unbounded and permissionless). Discovery of relevant multisigs happens through the delegations that reference them — which is why the cross-contract inlining requirements below are normative.
Joint visibility requirement (this ZIP + the Governance ZIP). The end state MUST leave every authority-bearing variable in the embedded contracts readable over RPC, with no step of the delegation chain requiring out-of-band knowledge:
| Variable | Stored in | Visible via |
|---|---|---|
Multisig policy (active, pending, pendingHeight, locked) |
Multisig registry | embedded.multisig.getPolicy(address, height?) |
networkAdministrator (the networkAdmin role) |
Governance storage | embedded.governance.getNetworkAdministrator() — MUST inline the address’s MultisigRecordInfo (or null if not a multisig) |
| Proposed admin (Type-2 action in flight) | Governance action state | getActionById / getAllActions — MUST inline the proposed address’s MultisigRecordInfo (Governance ZIP §8; defends the §10 bait-and-switch and locked-target traps) |
| Bridge admin (as seen by wallets/orchestrator) | Derived | Bridge RPC administrator field, populated from GetNetworkAdministrator() post-migration — response shape unchanged |
| Liquidity admin | Derived | Same as Bridge |
Plasma parameters (MaxBasePlasmaInMomentum, targets, price-change bounds) |
Plasma storage | embedded.plasma.getVariables() (existing, unchanged) |
Bridge SecurityInfo delays, TSS pubkey, halt state |
Bridge storage | Existing bridge RPC (unchanged) |
Reading down this table answers, from chain state alone: who is admin, what is that admin’s live signing policy, is a change to either staged, and what operational values does the admin currently control. A wallet or explorer needs no constant, no genesis file, and no forum post to reconstruct the authority graph.
9. Activation — Hard Fork at MultisigForkHeight
Constant: MultisigForkHeight (TBD — scheduled per the Governance ZIP §11
readiness process; MAY equal GovernanceForkHeight)
Enforcement: IsMultisigForkEnforced() == true for height >= MultisigForkHeight
Runtime prerequisite: None — at the height, the fork proceeds regardless
of Pillar adoption
Scheduling prerequisite: §11.3 readiness only; no dependency on the
Governance ZIP in either direction
Before MultisigForkHeight, every multisig-address block and every registry-contract call is rejected (ErrMultisigNotActivated); from the height, multisig accounts validate per §6. The gate is evaluated at the block’s MomentumAcknowledged height, so enforcement is deterministic on every node.
Spork-based activation is deliberately not used, for the same reason as the Governance ZIP: no spork can be activated today (the legacy signer is unreachable), and once governance is the spork authority, activating an implemented spork takes two Type-1 actions plus an intervening placeholder-replacement release (Governance ZIP §6.1) — three coordination events to gate a feature whose code already requires a release to ship. A hard-coded height in that same release is one coordination event and removes the ordering dependency on governance entirely.
The required activation change. The PR #72 head gates the multisig on a dormant MultisigSpork placeholder (IsSporkActive(types.MultisigSpork) in the verifier; IsMultisigSporkEnforced() in GetEmbeddedMethod). This ZIP replaces that gate with the IsMultisigForkEnforced() height check in both places and deletes the placeholder spork entry from ImplementedSporksMap. The devnet genesis (docker/devnet/genesis.json), which currently activates the placeholder spork at height 15, is updated in the same change to a genesis-height MultisigForkHeight per the new-networks clause below. Behavior is otherwise identical — ErrMultisigNotActivated semantics, the MomentumAcknowledged anchor, and everything in §1–§8 are unchanged.
Ordering. Merge order and activation order relative to the Governance ZIP are both unconstrained. The two heights MAY coincide — a single coordinated fork enabling both contracts at once is the preferred deployment when one release carries both implementations (Governance ZIP §2). The Governance ZIP’s AdminMigrationForkHeight requires both heights passed and the community multisig created and verified (Governance ZIP §11.5):
merge #72 ─┐ (any order) ┌─ merge governance
└────┬────────┘
one release carrying both implementations (preferred)
│
GovernanceForkHeight ─┬─ MultisigForkHeight (either order, or equal)
│
CreateMultisig → community multisig created, published, verified (§11.5 gate)
│
AdminMigrationForkHeight (hard fork; networkAdmin becomes live authority)
On new networks (devnet, testnet, hypothetical fresh mainnet), MultisigForkHeight MAY be set to genesis so multisig accounts are available from the start.
10. Interoperability with the Governance ZIP
- No governance-caller path — by design.
ChangePolicyis self-targeted and threshold-signed; a contract-send from governance carries noMultisigAuthand dies at the verifier. The Governance ZIP accordingly repoints admin (Type-2SetNetworkAdministrator) rather than rotating signer sets, yielding two independent recovery paths: signers rotate themselves without a vote; a captured, locked, or abandoned multisig is escaped by one Pillar vote. Neither path requires the other. - The multisig contract grants nothing. Its admin authority exists entirely in other contracts’ checks against
networkAdmin. The governance contract, in turn, setsnetworkAdminand nothing else — it cannot reach plasma variables or any operational method (Governance ZIP §6.3, “The boundary”). - Voting on a proposed admin is on-chain-verifiable. Pillars evaluating a Type-2 target: read
getPolicy(live threshold, signers,locked), recompute the address from the published(creatorPubKey, nonce), and match signer keys to nominated identities. The §8 inlining requirements put the first of these directly in the action view. - Emergency replacement (compromised/locked/unreachable admin): one of the intended new signers submits
CreateMultisig(creator-must-be-a-signer, §3; immediate effect), publishes(creatorPubKey, nonce), and a Type-2 proposal repoints — round-0 approval executes instantly. See Governance ZIP Security Considerations for the exposure-window analysis.
Backward Compatibility
Before MultisigForkHeight, every multisig-address block is rejected (ErrMultisigNotActivated) and behaviour is unchanged; the registry contract’s methods are gated the same way. From the height, nodes on older binaries cannot validate multisig blocks, stop following the canonical chain, and must upgrade and re-sync — standard hard-fork semantics, identical to the Governance ZIP’s (§10 there). There is no adoption threshold at the height; scheduling follows the shared readiness process (Governance ZIP §11).
MultisigAuth is a new optional, hash-excluded, rlp:"optional" account-block field: required absent on every non-multisig block (§5), so existing blocks, wallets, and tooling are unaffected. User and contract address semantics are unchanged. No existing contract’s ABI or storage changes.
Reference Implementation
zenon-network/go-zenon#72 by digitalSloth, verified at feature/multisig-addresses HEAD 8f2eddb2b4790e022fe8057aac739c4c99788e8d. The PR carries verifier, contract, conformance, and wallet test suites (the full go test ./... suite passes at this head). This ZIP requires two implementation changes: the activation-gate swap of §9 and the prefilter-scope fix of checklist item 5; §1–§8 are otherwise specified exactly as implemented. The head is a moving target — it has been rebuilt before and gained material consensus changes (creation burn, receive-side re-authorization, momentum-anchored recency) between pins; any ZIP revision MUST re-verify against the then-current head rather than assuming this pin. Pre-merge review checklist:
- Implement and test the activation-gate swap (§9):
IsMultisigForkEnforced()replacesIsSporkActive(types.MultisigSpork)in the verifier andIsMultisigSporkEnforced()inGetEmbeddedMethod; the placeholder spork entry is deleted; the devnet genesis (docker/devnet/genesis.json) drops its placeholder-spork activation in favour of a genesis-height fork constant; pre-height behaviour is byte-identical and post-height behaviour matches the previous spork-active behaviour exactly. - Conformance tests for §6 exactly as specified — including exact-count threshold, distinct-signer trial match, recency floor, and the
PublicKey/Signature-must-be-zero rules. - Confirm the invariant
MultisigMaxMaLag >= MultisigPolicyMaturityDelayholds (60480 ≥ 60) with a test assertion — the constants are now independent values, no longer equal by construction. - Independent review confirming the PR touches no Bridge, Liquidity, Plasma, or Governance authorization logic (the dynamic-plasma code on the branch is
devbase, not multisig work). - Enforce the §6.4/§6.5 scope on the momentum-insert path. At the verified head, inserting a momentum (
protocol/chain_bridge.go) routes every carried block through the admission verifier, so the two frontier-relative prefilters — the frontier-policy signature check and theErrABMATooOldstaleness floor — leak into consensus: (a) at insert of momentum H the frontier is H−1, adding a de facto policy check at H−1 beside rule 3’s check at H, divergent exactly at a policy’s maturity boundary; and (b) blocks already in the local pool skip the verifier entirely (theGetPatchshort-circuit), so whether the prefilters gate a momentum depends on node-local pool state. Fix: skip both multisig prefilters for blocks applied as momentum content — rule 3 alone gates them — with regression tests covering a maturity-boundary momentum and the pooled-vs-fresh-sync divergence. - Route the two rule-3 call sites (momentum content verification and the producer-side filter) through
VerifyMultisigAuth, or delete it and fix its doc comment — at the verified head it is defined and documented as the shared primitive but called only from tests.
Security Considerations
- Not TSS; no keygen. N-of-M counting of ordinary ed25519 signatures. There is no ceremony to compromise and no aggregate key to extract; each signer’s key is an ordinary wallet key with ordinary custody properties. Unrelated to the Bridge’s TSS, which this ZIP does not touch.
- Stale-policy replay / withholding — eliminated structurally. Authorization is evaluated live at each block’s inclusion height (§6.3), so a block threshold-signed by a superseded signer set can never be included after the superseding rotation matures — no matter how long it was withheld or what it acknowledges. There is no recency window to attack;
MultisigMaxMaLagis pure pool hygiene. ForChangePolicyspecifically, defence is double: a send included under the then-active policy is re-verified at receive against the by-then-current active policy and rejected withErrMultisigStaleAuthorityif a rotation matured in the inclusion-to-receive gap — a superseded signer set can neither mutate nor lock the account. The producer-side filter (§6.6) additionally prevents a rotation-invalidated pooled block from stalling momentum production. - Pending-policy visibility. A staged change is on-chain and RPC-visible for the full maturity window (~10 min) before taking effect — and, for governance’s purposes, inlined into Type-2 action views for the full voting period. The bait-and-switch (clean
Active, hostilePendingmaturing post-vote) is defended by visibility, not by restriction. - Lock is forever.
Lockedadmits no recovery within the contract: lost signers of a locked multisig mean a permanently frozen account. Delegations to a locked multisig remain escapable only at the delegation layer (governance repointingnetworkAdmin). Voters seelockedin the action view before delegating to one. - Creator consent, not creator authority. The creator-must-be-a-signer rule prevents non-consensual attribution; it grants the creator no ongoing power — post-creation, the creator is just one signer, removable by rotation like any other. The creation-derived address means a group must trust its chosen creator only to submit the agreed policy, which the whole group verifies on-chain before funding or delegating to the address.
- Exact-count threshold. Requiring signatures to equal the threshold exactly (not “at least”) makes the verify cost deterministic and removes a malleability dimension (padding a valid block with extra signatures).
- Canonical signer form. The contract canonicalizes on write (accepting unsorted wire input, §3) so the stored form is byte-deterministic, and deduplication prevents duplicate-key threshold inflation (a 2-of-2 whose signers are the same key twice is rejected). Independent implementations MUST replicate canonicalize-on-write, not reject-unsorted — the two behaviors diverge on the same calldata.
- Bounded verify cost.
MaxSigners = 16with trial-matching bounds signature verification at 16×16 ed25519 operations per block worst-case — bounded and paid for by the block’s plasma. - Registry as consensus state. The policy read is deterministic (momentum store pinned per block;
Promote()pure), so verifier, contract, and RPC cannot diverge on which policy is active at a height.
Changelog
Maintained as a separate post — see Multisig ZIP — Changelog (v1.4 is current; it records the 2026-07-28 external code review: the normative prefilter scope (§6.4–§6.5, checklist item 5), the gate swap extended to the devnet genesis (§9, checklist item 1), the shared-primitive requirement (§6.3, checklist item 6), and re-verification of the pin at 8f2eddb2 with the full test suite passing).
Copyright
This ZIP is licensed under the GNU General Public License v3.0.