Files
nq/PROTOCOL.md
2026-06-12 14:46:59 -07:00

20 KiB
Raw Permalink Blame History

NIP-XX

FROST Multisig Quorum Protocol

draft optional

Abstract

This NIP defines a protocol for creating and operating FROST threshold signature quora 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", "<id>"] 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 (kind 1059) with at least 16 bits of proof-of-work per NIP-13. The content is encrypted to the recipient's pubkey and the wrap is sent to the recipient's 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:

{
  "kind": 7050,
  "content": "<optional human-readable message>",
  "tags": [
    ["threshold", "<signing threshold>"],
    ["member", "<pk_1>"],
    ["member", "<pk_n>"]
  ]
}

The event's inner ID identifies the DKG session; all subsequent events in this flow reference it via ["e", "<id>"]. 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 t1 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ᵢ || R || Cᵢ[0])
    • s = k + aᵢ₀ · c (mod q)
  4. Sends to all other members (n1 gift wraps, identical payload):
{
  "kind": 7051,
  "content": "",
  "tags": [
    ["e", "<kind 7050 inner id>"],
    ["commit", "<aᵢ₀·G hex>"],
    ["commit", "<aᵢ₁·G hex>"],
    ["proof", "<R hex>", "<s hex>"]
  ]
}

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):
{
  "kind": 7052,
  "content": "",
  "tags": [
    ["e", "<kind 7050 inner id>"],
    ["share", "<sᵢⱼ hex scalar>"]
  ]
}

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. Sends a DKG confirmation event (kind 7053) to all other n1 members.

Confirmation (kind 7053)

{
  "kind": 7053,
  "content": "",
  "tags": [
    ["e", "<kind 7050 inner id>"],
    ["quorum", "<Y x-only hex>"],
    ["transcript", "<H(initiating event id || sorted Round-1 commitments)>"]
  ]
}

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 all n members have sent confirmations with matching transcript and quorum values, or until a timeout elapses and all respondents match. Any single conflicting confirmation must trigger an immediate abort, regardless of how many matching confirmations have already been received.

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:

{
  "kind": 7054,
  "content": "<optional human-readable message>",
  "tags": [
    ["quorum", "<Y x-only hex>"],
    ["threshold", "<t'>"],
    ["contributor", "<pk_1>"],
    ["contributor", "<pk_n>"],
    ["member", "<pk_1'>"],
    ["member", "<pk_n'>"],
    ["dkg_commit", "<pk_1>", "<C₁₀ hex>", "<C₁₁ hex>", "…"],
    ["dkg_commit", "<pk_n>", "<Cₙ₀ hex>", "<Cₙ₁ hex>", "…"]
  ]
}

The event's inner ID identifies the resharing session; all subsequent events in this flow reference it via ["e", "<id>"].

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.

dkg_commit tags carry the Feldman commitments from the most recent DKG or resharing session for every current quorum member (not just contributors). One tag per member: the first value is the member's pubkey, followed by their ordered commitments [C₀, C₁, …, Cₜ₋₁]. New-only members use these to verify each contributor's first commitment in Round 1.

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 other participants — every new member and every fellow contributor (one gift wrap each, identical payload). New members consume Round 1 to verify and finalize; contributors must receive every contributor's Round 1 before sending Round 2, so a contributor who is rotated out of the new set (and therefore receives nothing addressed to "new members") still observes the complete contributing set and delivers its shard. It also lets contributors detect Round-1 equivocation among themselves.

{
  "kind": 7055,
  "content": "",
  "tags": [
    ["e", "<kind 7054 inner id>"],
    ["quorum", "<Y x-only hex>"],
    ["commit", "<λᵢ·xᵢ·G hex>"],
    ["commit", "<bᵢ₁·G hex>"],
    ["proof", "<R hex>", "<s hex>"]
  ]
}

New members verify before proceeding using the dkg_commit data from the proposal:

  1. Determine the BIP-340 sign factor: let σ_Y = 1 iff Σⱼ Cⱼ₀ (the raw group key from the dkg_commit entries) has odd y-coordinate; otherwise σ_Y = 1.

  2. For each contributor Pᵢ in S, compute Pᵢ's raw verification share from the original DKG:

    Ỹᵢ = Σⱼ Σₖ₌₀ᵗ⁻¹ ( i^k · Cⱼₖ ) (summing over all original members' dkg_commit entries)

    Apply the same normalization used during DKG finalization: Yᵢ = σ_Y · Ỹᵢ.

  3. Verify Dᵢ[0] == λᵢ · Yᵢ for each contributor. Abort if any check fails.

If all per-contributor checks pass, Σᵢ∈S Dᵢ[0] == Y holds automatically (Lagrange identity).

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:

{
  "kind": 7056,
  "content": "",
  "tags": [
    ["e", "<kind 7054 inner id>"],
    ["quorum", "<Y x-only hex>"],
    ["share", "<hᵢ(j) hex scalar>"]
  ]
}

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, this session's commitments). The stored commitment is the group commitment G = Σᵢ∈S Dᵢ (the coefficient-wise sum of the contributors' Round-1 commitments), not the per-contributor breakdown. The contributing set S rarely has the same size as the new member set, so a per-contributor list cannot be re-serialised as one dkg_commit tag per member without leaving empty tags (members added) or dropping commitments (members removed). Because every verifier only ever sums over the commitment values (Σⱼ Σₖ i^k·Cⱼₖ and Σⱼ Cⱼ₀), the single collapsed vector G reconstructs every member's verification share and the raw group key identically. On the next rotation it is carried in one non-empty dkg_commit tag (the remaining members' tags are empty and ignored).

  6. Sends resharing confirmation (kind 7057) to all other participants — every other new member and every contributor, including contributors rotated out of the new set. A rotated-out contributor holds no new shard and never finalizes locally, so receiving the new members' confirmations is its only signal that the rotation succeeded (and how it records the rotation for chat gating).

Resharing Confirmation (kind 7057)

{
  "kind": 7057,
  "content": "",
  "tags": [
    ["e", "<kind 7054 inner id>"],
    ["quorum", "<Y x-only hex>"],
    ["transcript", "<H(initiating event id || sorted Round-1 commitments)>"]
  ]
}

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 all n' new members have sent kind 7057 confirmations with matching transcript and quorum values, or until a timeout elapses and all respondents match. Any single conflicting confirmation must trigger an immediate abort. 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.

{
  "kind": 7058,
  "content": "<JSON-stringified unsigned nostr event>",
  "tags": [
    ["quorum", "<Y x-only hex>"]
  ]
}

The event's inner ID identifies the signing session; all subsequent events in this flow reference it via ["e", "<id>"].

msg in all subsequent computations is the 32-byte NIP-01 event id of the unsigned event: the SHA-256 hash of its canonical NIP-01 serialisation [0,pubkey,created_at,kind,tags,content].

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:
{
  "kind": 7059,
  "content": "",
  "tags": [
    ["e", "<kind 7058 inner id>"],
    ["quorum", "<Y x-only hex>"],
    ["D", "<dᵢ·G hex>"],
    ["E", "<eᵢ·G hex>"]
  ]
}

Round 2 — Signature Shares (kind 7060)

Signing set finalisation: After collecting at least t Round-1 nonces, the coordinator constructs the signing set S — the indices of exactly those participants whose nonces were received, ordered ascending — and distributes the complete finalized nonce list {(j, Dⱼ, Eⱼ)}ⱼ∈S to all Round-2 participants. Signers must not begin Round 2 before receiving this distribution.

After receiving the finalized S and nonce list from the coordinator, each Pᵢ:

  1. Uses the signing set S and nonce list as distributed by the coordinator
  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:
{
  "kind": 7060,
  "content": "",
  "tags": [
    ["e", "<kind 7058 inner id>"],
    ["quorum", "<Y x-only hex>"],
    ["z", "<zᵢ hex scalar>"],
    ["signer", "<index_1>"],
    ["signer", "<index_n>"]
  ]
}

The signer tags list every member of S by index. The coordinator rejects aggregation and aborts the session if any two received Round-2 events carry different signer sets.

Aggregation

The coordinator first verifies each received share individually. Let σ_Y = 1 iff the raw group key Σⱼ Cⱼ₀ from stored DKG commitments has odd y-coordinate; otherwise σ_Y = 1. Let σ_R = 1 iff the pre-normalization group nonce R̃ = Σᵢ∈S (Dᵢ + ρᵢ·Eᵢ) had odd y-coordinate; otherwise σ_R = 1. For each Pᵢ in S, compute Ỹᵢ = Σⱼ Σₖ ( i^k · Cⱼₖ ) from stored DKG commitments, set Yᵢ = σ_Y · Ỹᵢ, and check:

zᵢ·G == σ_R · (Dᵢ + ρᵢ·Eᵢ) + λᵢ · c · Yᵢ

If any share fails, abort the session — the failing signer is identified and may be excluded from future attempts.

The coordinator then 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.

{
  "kind": 7061,
  "content": "<optional reason>",
  "tags": [
    ["e", "<initiating event inner id>"],
    ["quorum", "<Y x-only hex>"]
  ]
}

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 The commitments that define the current sharing: after a DKG, all participants' Round-1 commitments (one per member); after a resharing, the single group commitment Σᵢ∈S Dᵢ (see Key Redistribution Finalization step 5)
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.