NIP-XX ====== FROST Multisig Quorum Protocol ------------------------------- `draft` `optional` ## Abstract This NIP defines a protocol for creating and operating FROST threshold signature quorums over Nostr keys. A quorum is a group of n participants who collectively control a shared Nostr keypair via a (t,n) threshold signing scheme, where any t members can produce a valid signature but no fewer. The quorum's private key is never known to any single party. ## Participant Indexing Each member is assigned a positive integer index. Indices are derived deterministically by sorting member pubkeys lexicographically as lowercase hex strings and assigning 1-based positions. This ordering must be consistent across all participants and all rounds of a session. When a new member set is established (rotation), indices are re-derived from the new member list. ## Session References Each protocol flow is initiated by a single event (the invite, resharing proposal, or sign request). All subsequent events in that flow reference the initiating event's inner ID via a standard `["e", ""]` tag. The inner ID is computed from the unsigned rumor before gift-wrapping and is identical across all per-recipient gift wraps of the same event. ## Event Kinds All events in this protocol are sent as the `content` payload of a NIP-59 gift wrap using a kind 7049 wrapper instead of a kind 1059 with at least 16 bits of work on the wrapper per NIP 13. The `content` is encrypted to the recipient's pubkey and the wrap is sent to the recipients inbox relays (kind 10050) per NIP 17. ## Quorum Creation Creates a new quorum and derives its shared Nostr keypair. The private key is never known to any party. ### Phase 0 — Invitation (kind 7050) The initiator sends each proposed member a gift-wrapped event: ```json { "kind": 7050, "content": "", "tags": [ ["threshold", ""], ["member", ""], ["member", ""] ] } ``` The event's inner ID identifies the DKG session; all subsequent events in this flow reference it via `["e", ""]`. The quorum's keypair does not yet exist; no key material is present in the invite. Participation in Round 1 signals acceptance. A member who will not participate should send a decline event (kind 7061) so the initiator knows not to wait. ### Round 1 — Commitments (kind 7051) Each accepting participant Pᵢ: 1. Samples a random polynomial `fᵢ(x)` of degree `t−1` over the secp256k1 scalar field, with random coefficients `aᵢ₀, aᵢ₁, …, aᵢ,ₜ₋₁` 2. Computes Feldman commitments: `Cᵢ = [aᵢ₀·G, aᵢ₁·G, …, aᵢ,ₜ₋₁·G]` (compressed 33-byte EC points as hex) 3. Computes a Schnorr proof of knowledge of `aᵢ₀`: - Sample ephemeral scalar `k`; compute `R = k·G` - `c = H("frost/dkg/round1" || session_id || pkᵢ || Cᵢ[0])` - `s = k + aᵢ₀ · c (mod q)` 4. Sends to all other members (n−1 gift wraps, identical payload): ```json { "kind": 7051, "content": "", "tags": [ ["e", ""], ["commit", ""], ["commit", ""], ["proof", "", ""] ] } ``` ### Round 2 — Share Distribution (kind 7052) After receiving Round 1 from all other participants, Pᵢ: 1. Verifies each Pⱼ's PoK: `s·G == R + c·Cⱼ[0]` where `c` is recomputed from the proof 2. For each Pⱼ (j ≠ i), evaluates `sᵢⱼ = fᵢ(j)` (polynomial evaluated at Pⱼ's 1-based index) 3. Sends encrypted to Pⱼ only (one gift wrap per recipient): ```json { "kind": 7052, "content": "", "tags": [ ["e", ""], ["share", ""] ] } ``` ### Finalization Each Pⱼ, after receiving shares from all other participants: 1. Verifies each received share against Pᵢ's commitments: `sᵢⱼ·G == Σₖ₌₀ᵗ⁻¹ ( j^k · Cᵢₖ )` Abort if any check fails. 2. Computes secret shard: `xⱼ = Σᵢ sᵢⱼ (mod q)` 3. Computes group public key: `Y = Σᵢ Cᵢ₀` (sum of all participants' first commitments) 4. Computes own verification share: `Yⱼ = Σᵢ Σₖ₌₀ᵗ⁻¹ ( j^k · Cᵢₖ )`; verify `xⱼ·G == Yⱼ` 5. **BIP-340 normalization**: if `Y` has odd y-coordinate, negate `xⱼ` and `Yⱼ` and use the even-y form of `Y` as the quorum pubkey. This negation must be applied consistently across all subsequent operations. 6. Stores `(xⱼ, Y, Yⱼ, members, threshold, Round-1 commitments)` durably. The Round-1 commitments are retained because they are needed to verify resharing participants' shards in Protocol 2. 7. Publishes a DKG confirmation event (kind 7053). ### Confirmation (kind 7053) ```json { "kind": 7053, "content": "", "tags": [ ["e", ""], ["quorum", ""], ["transcript", ""] ] } ``` `transcript` enables equivocation detection: a malicious participant may send different Round-1 commitments to different members. If any two confirmations reference the same initiating event but carry different `transcript` or `quorum` values, all participants must abort. The quorum is considered live once `t` confirmations with matching `transcript_hash` and `quorum_pubkey` have been observed. ## Key Redistribution Redistributes the existing quorum key to a new member set and/or threshold without reconstructing the private key. The quorum's Nostr pubkey `Y` is preserved, so the quorum's identity, profile, and event history are unaffected. **Prerequisite**: A contributing set `S` of at least `t` current members must participate. The `contributor` list in the proposal is the finalized set `S`. Every listed contributor must complete Round 1 or the flow stalls; any contributor or prospective new member who will not participate should send a decline event (kind 7061). The initiator may restart with a new proposal and a different contributor set. ### Phase 0 — Resharing Proposal (kind 7054) The initiator (a current member) sends to all current and prospective new members: ```json { "kind": 7054, "content": "", "tags": [ ["quorum", ""], ["threshold", ""], ["contributor", ""], ["contributor", ""], ["member", ""], ["member", ""] ] } ``` The event's inner ID identifies the resharing session; all subsequent events in this flow reference it via `["e", ""]`. `contributor` tags list the exact contributing set `S` — the old members who will reshare their shards. `member` tags list the new member set after rotation. A retained member appears in both. New member indices are derived by sorting `member` pubkeys lexicographically (1-based); `contributor` pubkeys retain their original indices from the prior DKG session, which are used for Lagrange coefficient computation. Because `S` is fixed in the proposal, Lagrange coefficients can be computed immediately without waiting for Round 1 responses. ### Round 1 — Old Member Commitments (kind 7055) Each contributing member Pᵢ (index `i` from the original DKG, shard `xᵢ`): 1. Computes Lagrange coefficient over contributing set `S` at point 0: `λᵢ = Πⱼ∈S, j≠i (-j) / (i - j) (mod q)` 2. Samples a new random polynomial `hᵢ(x)` of degree `t'−1` with constant term `hᵢ(0) = λᵢ · xᵢ` 3. Computes commitments: `Dᵢ = [λᵢ·xᵢ·G, …]` (same structure as Round-1 commitments in Protocol 1) 4. The first commitment `Dᵢ[0]` must equal `λᵢ · Yᵢ`, where `Yᵢ` is Pᵢ's verification share from the original DKG (publicly computable from stored Round-1 commitments). This proves Pᵢ is resharing their actual shard. 5. Computes Schnorr PoK of `λᵢ · xᵢ` (same construction as Protocol 1 Round 1, using `"frost/resharing/round1"` as domain tag) 6. Sends to all new members (m gift wraps, identical payload): ```json { "kind": 7055, "content": "", "tags": [ ["e", ""], ["quorum", ""], ["commit", "<λᵢ·xᵢ·G hex>"], ["commit", ""], ["proof", "", ""] ] } ``` New members verify before proceeding: `Σᵢ∈S Dᵢ[0] == Y`. Abort if this check fails. ### Round 2 — Share Distribution (kind 7056) Each participating old member Pᵢ evaluates `hᵢ(j)` at each new member Qⱼ's index `j` and sends encrypted to Qⱼ only: ```json { "kind": 7056, "content": "", "tags": [ ["e", ""], ["quorum", ""], ["share", ""] ] } ``` ### Finalization Each new member Qⱼ, after receiving shares from all members of `S`: 1. Verifies each received share against Dᵢ commitments: `hᵢ(j)·G == Σₖ₌₀ᵗ'⁻¹ ( j^k · Dᵢₖ )` Abort if any check fails. 2. Computes new shard: `x'ⱼ = Σᵢ∈S hᵢ(j) (mod q)` 3. Computes new verification share: `Y'ⱼ = Σᵢ∈S Σₖ ( j^k · Dᵢₖ )`; verify `x'ⱼ·G == Y'ⱼ` 4. BIP-340 normalization: `Y` is unchanged, so the same even-y convention applies. If `xᵢ` was negated during the original DKG finalization, `hᵢ(0) = λᵢ · xᵢ` already incorporates that negation. Qⱼ verifies against the same `Y` and does not re-negate. 5. Replaces stored quorum state with `(x'ⱼ, Y, Y'ⱼ, new_members, new_threshold, Round-1 commitments from this session)`. 6. Publishes resharing confirmation (kind 7057). ### Resharing Confirmation (kind 7057) ```json { "kind": 7057, "content": "", "tags": [ ["e", ""], ["quorum", ""], ["transcript", ""] ] } ``` `transcript` enables equivocation detection for the resharing round, the same way it does for DKG confirmations. If any two kind 7057 events reference the same initiating event but carry different `transcript` or `quorum` values, all participants must abort. The rotation is considered complete once `t'` new members have published kind 7057 confirmations with matching `transcript` and `quorum` values. Clients retain all kind 7057 confirmation sets in order to determine which members were active at a given time, which gates NIP-17 chat message display. ## Collaborative Signing Any quorum member may initiate a signing session. At least t members must participate to produce a valid signature. ### Sign Request (kind 7058) The initiator sends to all members. Any member who will not sign should respond with a decline event (kind 7061) so the initiator knows not to wait. If fewer than `t` members are willing to sign, the session cannot proceed. ```json { "kind": 7058, "content": "", "tags": [ ["quorum", ""] ] } ``` The event's inner ID identifies the signing session; all subsequent events in this flow reference it via `["e", ""]`. ### Round 1 — Nonce Commitments (kind 7059) Each willing signer Pᵢ: 1. Samples ephemeral nonce pair `(dᵢ, eᵢ)` uniformly at random (must never be reused) 2. Computes commitments `Dᵢ = dᵢ·G`, `Eᵢ = eᵢ·G` 3. Sends to all other participating signers: ```json { "kind": 7059, "content": "", "tags": [ ["e", ""], ["quorum", ""], ["D", ""], ["E", ""] ] } ``` ### Round 2 — Signature Shares (kind 7060) After collecting Round 1 from at least `t` signers, each Pᵢ: 1. Finalizes the signing set `S` (the participants whose commitments were collected) 2. Computes binding factors: `ρᵢ = H("frost/sign/rho" || i || msg || {(Dⱼ, Eⱼ)}ⱼ∈S)` for each `i ∈ S` 3. Computes group nonce: `R = Σᵢ∈S (Dᵢ + ρᵢ·Eᵢ)`; if `R` has odd y-coordinate, negate `dᵢ, eᵢ` and use the even-y `R` 4. Computes BIP-340 challenge: `c = H_BIP340("BIP0340/challenge" || R.x || Y || msg)` 5. Computes Lagrange coefficient `λᵢ` over `S` at point 0 (same formula as Protocol 2) 6. Computes signature share: `zᵢ = dᵢ + eᵢ·ρᵢ + λᵢ·xᵢ·c (mod q)` (using the BIP-340-normalized `xᵢ`) 7. Sends to coordinator: ```json { "kind": 7060, "content": "", "tags": [ ["e", ""], ["quorum", ""], ["z", ""] ] } ``` ### Aggregation The coordinator (any member) aggregates: - `z = Σᵢ∈S zᵢ (mod q)` - Final signature: `(R.x, z)` The coordinator verifies the signature against `Y` and `msg` using standard BIP-340 verification before publishing the event. The resulting signature is a valid BIP-340 Schnorr signature, indistinguishable from a single-key signature. ## Decline (kind 7061) Any participant may decline an invitation or request by sending a kind 7061 event to the initiator. The `e` tag references the inner ID of the initiating event. The `quorum` tag is included where the quorum pubkey is already known (resharing and signing); it is omitted for quorum creation invites where the key does not yet exist. ```json { "kind": 7061, "content": "", "tags": [ ["e", ""], ["quorum", ""] ] } ``` A decline is informational. It does not itself abort a session, but receiving one signals the initiator that the session cannot complete as proposed and should be restarted. ## Storage Quorum state must be stored durably per-quorum: | Field | Description | |-------|-------------| | `quorum_pubkey` | `Y` as x-only hex — the quorum's Nostr pubkey | | `shard` | `xⱼ` — this member's secret share (encrypted at rest) | | `verification_share` | `Yⱼ` — public verification share for this member | | `members` | Current member pubkeys and indices | | `threshold` | Current signing threshold `t` | | `dkg_commitments` | All participants' Round-1 commitments from the most recent DKG or resharing session | | `rotation_records` | Ordered list of completed rotation sessions, each stored as the quorum's kind 7057 confirmation set (all `t'` matching confirmations) | `dkg_commitments` are retained permanently because they are required to verify shard authenticity during resharing (step 4 of the Key Redistribution Round 1). `rotation_records` are used to determine which members were active at a given point in time, which gates NIP-17 chat message display. Each entry is identified by the kind 7054 inner ID and stores the full set of kind 7057 confirmations that completed that rotation. ## Security Notes **Equivocation**: A malicious participant can send different Round-1 commitments to different members, causing different members to derive different group keys. The `transcript` tag in confirmation events (kind 7053 and 7057) provides detection. Implementations must abort if any two confirmations referencing the same initiating event carry different `transcript` or `quorum` values. **Abort and restart**: If any participant fails to complete their round, the session must be fully aborted. Partial state (nonces, sub-shares) must be discarded. The initiator starts a new session from Phase 0, producing a new initiating event with a new inner ID. **Nonce reuse in signing**: Reusing `(dᵢ, eᵢ)` across two signing sessions leaks the shard `xᵢ`. Implementations must use fresh randomness for every session and must not persist signing nonces. **Contributing set integrity**: In Protocol 2, the Lagrange coefficients and the integrity check `Σ Dᵢ[0] == Y` are only valid over the exact set `S` fixed in the proposal. If any contributor fails to complete Round 1, the session stalls and must be restarted with a new proposal. **BIP-340 y-coordinate normalization**: Nostr uses x-only public keys. Both the group key `Y` (finalized in DKG) and the signing nonce `R` (per signing session) require even-y normalization, which affects the sign of `xⱼ` and `(dᵢ, eᵢ)` respectively. These negations are independent and must both be applied correctly.