Files
nq/PROTOCOL.md
T
2026-06-08 09:30:44 -07:00

13 KiB
Raw Blame History

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.

Event Kinds

All events in this protocol are sent as the content payload of a NIP-59 gift wrap (kind 1059). 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:

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

session_id is chosen by the initiator and identifies all subsequent events for this DKG session. The quorum's keypair does not yet exist; no key material is present in the invite.

Participation in Round 1 signals acceptance. A member may send a decline event, but is not required to.

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ᵢ || Cᵢ[0])
    • s = k + aᵢ₀ · c (mod q)
  4. Sends to all other members (n1 gift wraps, identical payload):
{
  "kind": 7051,
  "content": "",
  "tags": [
    ["session_id", "<32-byte hex>"],
    ["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": [
    ["session_id", "<32-byte hex>"],
    ["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, session_id, Round-1 commitments) durably in IndexedDB. 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)

{
  "kind": 7053,
  "content": "",
  "tags": [
    ["session_id", "<32-byte hex>"],
    ["quorum", "<Y x-only hex>"],
    ["transcript", "<H(session_id || sorted Round-1 commitments)>"]
  ]
}

transcript_hash enables equivocation detection: a malicious participant may send different Round-1 commitments to different members. If any two confirmations carry the same session_id but different transcript_hash or quorum_pubkey, all participants must abort.

The quorum is considered live once t confirmations with matching transcript_hash and quorum_pubkey have been observed.


Protocol 2: Key Redistribution (Rotation)

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.

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": [
    ["session_id", "<32-byte hex>"],
    ["quorum", "<Y x-only hex>"],
    ["threshold", "<t'>"],
    ["old_member", "<pk_1>"],
    ["old_member", "<pk_n>"],
    ["member", "<pk_1'>"],
    ["member", "<pk_n'>"]
  ]
}

New member indices are derived by sorting new_members lexicographically (1-based), independently of the old index assignment.

Old members signal participation by contributing in Round 1. The contributing set S must be fixed before shares are combined, because each old member's Lagrange coefficient depends on the full set S. Implementations should establish S via a timeout or an explicit "I'm participating" acknowledgment step before proceeding to Round 2.

Round 1 — Old Member Commitments (kind 7055)

Each participating old member Pᵢ (with index i from the original DKG and 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):

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

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:

{
  "kind": 7056,
  "content": "",
  "tags": [
    ["session_id", "<32-byte hex>"],
    ["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, session_id, Round-1 commitments from this session).

  6. Publishes resharing confirmation (kind 7057).

Resharing Confirmation (kind 7057)

{
  "kind": 7057,
  "content": "",
  "tags": [
    ["session_id", "<32-byte hex>"],
    ["quorum", "<Y x-only hex>"],
    ["transcript", "<H(session_id || sorted Round-1 commitments)>"]
  ]
}

Once t' new members have published matching confirmations, the rotation is considered complete. The quorum then publishes a signed kind 0 metadata event under Y recording the new member list. This event is the on-chain rotation record and is used to gate NIP-17 chat display by membership at time of message.


Protocol 3: Collaborative Signing (FROST)

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:

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

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": [
    ["session_id", "<32-byte hex>"],
    ["quorum", "<Y x-only hex>"],
    ["D", "<dᵢ·G hex>"],
    ["E", "<eᵢ·G hex>"]
  ]
}

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:
{
  "kind": 7060,
  "content": "",
  "tags": [
    ["session_id", "<32-byte hex>"],
    ["quorum", "<Y x-only hex>"],
    ["z", "<zᵢ hex scalar>"]
  ]
}

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.


Storage

Quorum state must be stored durably in IndexedDB 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 All signed rotation kind 0 events in order

dkg_commitments are retained permanently because they are required to verify shard authenticity during resharing (step 4 of Protocol 2 Round 1).

rotation_records are used to determine which members were active at a given time, which gates NIP-17 chat message display.


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_hash in confirmation events provides detection. Implementations must abort if any two confirmations for the same session have different hashes.

Abort and restart: If any participant fails to complete their round within a timeout, the session must be fully aborted. Partial state (nonces, sub-shares) must be discarded. A new session with a new session_id must be started from Phase 0.

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 meaningful over the same set S. The set must be fixed and agreed upon before shares are combined. Any late-joining or aborting member after S is finalized requires a full session restart.

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.