# DRAFT Multisig ZIP (working draft) for Comment

**URL:** https://forum.hypercore.one/t/draft-multisig-zip-working-draft-for-comment/940
**Category:** Development
**Created:** [July 28, 2026, 10:25am UTC](https://forum.hypercore.one/t/draft-multisig-zip-working-draft-for-comment/940 "2026-07-28T10:25:31Z")
**Posts on this page:** 1
**Page:** 1

<div class="post-metadata">

### Author: ![0x3639](https://forum.hypercore.one/user_avatar/forum.hypercore.one/0x3639/32/33_2.png) [@0x3639](https://forum.hypercore.one/u/0x3639)
#### Post date: [July 28, 2026, 10:25am UTC](https://forum.hypercore.one/t/draft-multisig-zip-working-draft-for-comment/940/1 "2026-07-28T10:25:32Z")

</div>

# 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](https://github.com/zenon-network/go-zenon/pull/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](https://github.com/zenon-network/go-zenon/pull/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

```go
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:

1. **Signer rotation never changes the address.** Delegations to the address survive every policy change.
2. **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, via `getPolicy`.

### 2. Registry Contract

`MultisigContract = z1qxemdeddedxmultysygxxxxxxxxxxxxx42zwd4`, added to `types.EmbeddedContracts`. It stores one record per multisig address:

```go
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 with `ErrMultisigStaleAuthority` rather than allowed to mutate or lock the account. Finally it rejects with `ErrMultisigLocked` if 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:

```go
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:

1. The multisig fork MUST be enforced at the block’s `MomentumAcknowledged` height (`MomentumAcknowledged.Height >= MultisigForkHeight`), else `ErrMultisigNotActivated`.
2. `PublicKey` and `Signature` MUST be empty; `MultisigAuth` MUST be present.
3. **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()` → `VerifyThresholdSignatures` sequence — packaged as `VerifyMultisigAuth(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.
4. **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).
5. **Staleness bound (hygiene, not security).** A multisig block whose `MomentumAcknowledged` lags the including height by more than `MultisigMaxMaLag` (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.
6. **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`).
7. **Exclusivity:** any non-multisig block carrying a non-nil `MultisigAuth` is 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:

1. **Momentum production.** A momentum’s producer is always `PubKeyToAddress` of 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 multisig `ProducerAddress` at 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.
2. `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 same `Promote()` the consensus paths use:

```json
{
  "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`

```yaml
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):

```auto
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.** `ChangePolicy` is self-targeted and threshold-signed; a contract-send from governance carries no `MultisigAuth` and dies at the verifier. The Governance ZIP accordingly repoints admin (Type-2 `SetNetworkAdministrator`) 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, sets `networkAdmin` and 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](https://github.com/zenon-network/go-zenon/pull/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:

1. Implement and test the activation-gate swap (§9): `IsMultisigForkEnforced()` replaces `IsSporkActive(types.MultisigSpork)` in the verifier and `IsMultisigSporkEnforced()` in `GetEmbeddedMethod`; 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.
2. 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.
3. Confirm the invariant `MultisigMaxMaLag >= MultisigPolicyMaturityDelay` holds (60480 ≥ 60) with a test assertion — the constants are now independent values, no longer equal by construction.
4. Independent review confirming the PR touches no Bridge, Liquidity, Plasma, or Governance authorization logic (the dynamic-plasma code on the branch is `dev` base, not multisig work).
5. 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 the `ErrABMATooOld` staleness 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 (the `GetPatch` short-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.
6. 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; `MultisigMaxMaLag` is pure pool hygiene. For `ChangePolicy` specifically, 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 with `ErrMultisigStaleAuthority` if 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`, hostile `Pending` maturing post-vote) is defended by visibility, not by restriction.
- **Lock is forever.** `Locked` admits 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 repointing `networkAdmin`). Voters see `locked` in 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 = 16` with 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.
