Threat model for the private M-of-N multisig on the Logos Execution Zone (LEZ v0.2.0).
What is trusted. LEZ uses RISC0's STARK-to-SNARK pipeline (Groth16 over BN254 for the outer succinct proof). The BN254 Groth16 parameters were generated by the RISC0 team in a multi-party ceremony; transcript and instructions at https://github.com/risc0/risc0. If the ceremony is compromised (a trapdoor is known), an adversary can forge proofs.
Scope. The multisig logic (threshold check, nullifier set, rider binding, state serialization) runs inside the LEZ zkVM. A ceremony compromise would allow forging any LEZ proof — this is not specific to this multisig, which adds no additional trusted setup beyond what LEZ requires.
What observers see. The multisig account is program-owned. Votes arrive as privacy-preserving transactions. Each vote transaction exposes:
- The multisig account ID (public; it is the governance target).
- A nullifier (one per member per proposal, derived as
SHA256("multisig/v1/vote" || nsk || proposal_id || multisig_id)). - One private-account commitment rotation (the member's voting account riding the LEZ privacy circuit) — indistinguishable from any private transfer's commitment/nullifier pair on-chain.
- The updated multisig state: vote count increments, nullifier added. The registered commitment set is visible.
What is hidden. Which member voted. The nsk never leaves the client; the vote instruction (including the nsk and member index) is private initial-call data. The nullifier cannot be linked to a registered commitment without knowing the nsk, and the rider's rotated commitment cannot be linked to a specific voting account without the account keys.
Chain analysis risk. The voting account is dedicated (identifier 0 under the member's key tree) and funded by a shielded transfer, so it has no public transaction graph. Residual correlation vectors: timing (a vote lands shortly after a member comes online) and RPC-level metadata. Mitigations: (a) add jitter between proposal publication and voting; (b) route submissions through Tor or a proxy; (c) fund the voting account well before any vote.
Member set size. For small N (e.g., 2-of-3), the anonymity set is at most N. An adversary who knows which members did not vote can narrow it down. This is inherent to small-N multisig; full anonymity requires larger N.
Threat. A malicious or compromised sequencer could:
- Censor vote transactions (drop them): the proposal never reaches threshold.
- Reorder votes (no security impact: votes are idempotent once their nullifier is spent).
- Attempt to replay a nullifier across a fork: rejected, because the nullifier set is part of the on-chain state committed in each block.
Mitigations. LEZ is designed for multiple sequencers and forced-inclusion paths; until deployed, a censored member can retry through an alternative RPC endpoint. There is no sequencer-level secret that can forge a vote — proof generation is entirely client-side.
SHA256("multisig/v1/vote" || nsk || proposal_id || multisig_id) has:
- Binding: one nullifier per (member, proposal, multisig) triple.
Re-voting with the same nsk reproduces the same nullifier and is rejected
in-circuit (
ERR_6004). - Hiding: without the nsk, the nullifier is a uniformly random 32-byte
value; linking it to a commitment
SHA256("member" || nsk || multisig_id)requires a SHA256 preimage. - Cross-proposal independence: nullifiers for different proposals are independent — a vote on proposal A reveals nothing about proposal B.
- Cross-multisig independence:
multisig_idis mixed in, so the same nsk in two deployments produces unrelated nullifiers.
Known limitation: SHA256 anonymity arguments are random-oracle-model; a production system with tighter requirements could use an audited ZK-native hash (e.g. Poseidon). On RISC0, SHA256 is hardware-accelerated in-circuit, which keeps vote proofs small and fast.
A vote must ride the member's LIVE shielded voting account. In-circuit, the program asserts:
rider.account_id == AccountId::for_regular_private_account(npk(nsk), 0)— the rider is derived from the SAME secret as the membership commitment (ERR_6012otherwise);rider.account != Account::default()— the account exists on chain (ERR_6013otherwise).
The LEZ privacy circuit additionally proves the rider's pre-state commitment is a member of the live commitment tree and derives its account-update nullifier from the same nsk, so a vote cannot be fabricated for an account the voter does not control, and cannot be cast at all without controlling a live shielded account enrolled in the member set. A bug in the LEZ privacy circuit's commitment-membership proof would weaken this binding; that circuit is LEZ core protocol and outside this program's audit scope.
| Limitation | Impact | Status |
|---|---|---|
| SHA256 (not Poseidon) for commitments/nullifiers | Random-oracle assumption; no security break | Accepted for v1 (RISC0-accelerated) |
| Small anonymity set for small N | Members can be narrowed down by exclusion | Inherent to M-of-N; documented |
| Member set fixed at initialization | Adding/removing members requires a new multisig instance | Planned: membership-rotation instruction |
| No forced-inclusion path (sequencer dependency) | Votes can be censored | LEZ protocol work; out of scope |
| Trusted setup (RISC0 ceremony) | Ceremony compromise allows proof forgery | External dependency |
execute finalizes action bytes (parameter change), no chained call into a target program |
Downstream programs must read the executed action from multisig state | v2 roadmap: chained-call execution |