refine protocol
This commit is contained in:
+59
-43
@@ -16,9 +16,13 @@ Each member is assigned a positive integer index. Indices are derived determinis
|
||||
|
||||
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). The `content` is encrypted to the recipient's pubkey and the wrap is sent to the recipients inbox relays (kind 10050) per NIP 17.
|
||||
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
|
||||
|
||||
@@ -33,17 +37,16 @@ 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>"],
|
||||
],
|
||||
["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.
|
||||
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 may send a decline event, but is not required to.
|
||||
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)
|
||||
|
||||
@@ -62,7 +65,7 @@ Each accepting participant Pᵢ:
|
||||
"kind": 7051,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["session_id", "<32-byte hex>"],
|
||||
["e", "<kind 7050 inner id>"],
|
||||
["commit", "<aᵢ₀·G hex>"],
|
||||
["commit", "<aᵢ₁·G hex>"],
|
||||
["proof", "<R hex>", "<s hex>"]
|
||||
@@ -83,7 +86,7 @@ After receiving Round 1 from all other participants, Pᵢ:
|
||||
"kind": 7052,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["session_id", "<32-byte hex>"],
|
||||
["e", "<kind 7050 inner id>"],
|
||||
["share", "<sᵢⱼ hex scalar>"]
|
||||
]
|
||||
}
|
||||
@@ -107,7 +110,7 @@ Each Pⱼ, after receiving shares from all other participants:
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
@@ -118,25 +121,25 @@ Each Pⱼ, after receiving shares from all other participants:
|
||||
"kind": 7053,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["session_id", "<32-byte hex>"],
|
||||
["e", "<kind 7050 inner id>"],
|
||||
["quorum", "<Y x-only hex>"],
|
||||
["transcript", "<H(session_id || sorted Round-1 commitments)>"]
|
||||
["transcript", "<H(initiating event 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.
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
## Protocol 2: Key Redistribution (Rotation)
|
||||
## 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:
|
||||
@@ -146,24 +149,23 @@ The initiator (a current member) sends to all current and prospective new member
|
||||
"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>"],
|
||||
["contributor", "<pk_1>"],
|
||||
["contributor", "<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.
|
||||
The event's inner ID identifies the resharing session; all subsequent events in this flow reference it via `["e", "<id>"]`.
|
||||
|
||||
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.
|
||||
`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 participating old member Pᵢ (with index `i` from the original DKG and shard `xᵢ`):
|
||||
Each contributing member Pᵢ (index `i` from the original DKG, shard `xᵢ`):
|
||||
|
||||
1. Computes Lagrange coefficient over contributing set `S` at point 0:
|
||||
|
||||
@@ -184,7 +186,7 @@ Each participating old member Pᵢ (with index `i` from the original DKG and sha
|
||||
"kind": 7055,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["session_id", "<32-byte hex>"],
|
||||
["e", "<kind 7054 inner id>"],
|
||||
["quorum", "<Y x-only hex>"],
|
||||
["commit", "<λᵢ·xᵢ·G hex>"],
|
||||
["commit", "<bᵢ₁·G hex>"],
|
||||
@@ -204,7 +206,7 @@ Each participating old member Pᵢ evaluates `hᵢ(j)` at each new member Qⱼ's
|
||||
"kind": 7056,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["session_id", "<32-byte hex>"],
|
||||
["e", "<kind 7054 inner id>"],
|
||||
["quorum", "<Y x-only hex>"],
|
||||
["share", "<hᵢ(j) hex scalar>"]
|
||||
]
|
||||
@@ -227,7 +229,7 @@ Each new member Qⱼ, after receiving shares from all members of `S`:
|
||||
|
||||
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)`.
|
||||
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).
|
||||
|
||||
@@ -238,36 +240,37 @@ Each new member Qⱼ, after receiving shares from all members of `S`:
|
||||
"kind": 7057,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["session_id", "<32-byte hex>"],
|
||||
["e", "<kind 7054 inner id>"],
|
||||
["quorum", "<Y x-only hex>"],
|
||||
["transcript", "<H(session_id || sorted Round-1 commitments)>"]
|
||||
["transcript", "<H(initiating event 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.
|
||||
`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.
|
||||
|
||||
## Protocol 3: Collaborative Signing (FROST)
|
||||
## 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:
|
||||
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": "<JSON-stringified unsigned nostr event>",
|
||||
"tags": [
|
||||
["session_id", "<32-byte hex>"],
|
||||
["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>"]`.
|
||||
|
||||
### Round 1 — Nonce Commitments (kind 7059)
|
||||
|
||||
Each willing signer Pᵢ:
|
||||
@@ -281,7 +284,7 @@ Each willing signer Pᵢ:
|
||||
"kind": 7059,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["session_id", "<32-byte hex>"],
|
||||
["e", "<kind 7058 inner id>"],
|
||||
["quorum", "<Y x-only hex>"],
|
||||
["D", "<dᵢ·G hex>"],
|
||||
["E", "<eᵢ·G hex>"]
|
||||
@@ -306,7 +309,7 @@ After collecting Round 1 from at least `t` signers, each Pᵢ:
|
||||
"kind": 7060,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["session_id", "<32-byte hex>"],
|
||||
["e", "<kind 7058 inner id>"],
|
||||
["quorum", "<Y x-only hex>"],
|
||||
["z", "<zᵢ hex scalar>"]
|
||||
]
|
||||
@@ -322,11 +325,26 @@ The coordinator (any member) aggregates:
|
||||
|
||||
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": "<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 in IndexedDB per-quorum:
|
||||
Quorum state must be stored durably per-quorum:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
@@ -336,22 +354,20 @@ Quorum state must be stored durably in IndexedDB per-quorum:
|
||||
| `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 |
|
||||
| `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 Protocol 2 Round 1).
|
||||
`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 time, which gates NIP-17 chat message display.
|
||||
|
||||
---
|
||||
`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_hash` in confirmation events provides detection. Implementations must abort if any two confirmations for the same session have different hashes.
|
||||
**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 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.
|
||||
**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 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.
|
||||
**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.
|
||||
|
||||
Reference in New Issue
Block a user