Add protocol impl and app scaffold

This commit is contained in:
Jon Staab
2026-06-08 10:56:55 -07:00
parent 42c5d2c23d
commit 19c741684c
19 changed files with 1309 additions and 51 deletions
+42 -12
View File
@@ -22,7 +22,7 @@ Each protocol flow is initiated by a single event (the invite, resharing proposa
## 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.
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. Each wrapper carries a `["t", "b7ed"]` tag identifying this sub-protocol, allowing recipients to filter their inbox efficiently. 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
@@ -56,7 +56,7 @@ Each accepting participant Pᵢ:
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])`
- `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):
@@ -112,7 +112,7 @@ Each Pⱼ, after receiving shares from all other participants:
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).
7. Sends a DKG confirmation event (kind 7053) to all other n1 members.
### Confirmation (kind 7053)
@@ -130,7 +130,7 @@ Each Pⱼ, after receiving shares from all other participants:
`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.
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
@@ -154,7 +154,9 @@ The initiator (a current member) sends to all current and prospective new member
["contributor", "<pk_1>"],
["contributor", "<pk_n>"],
["member", "<pk_1'>"],
["member", "<pk_n'>"]
["member", "<pk_n'>"],
["dkg_commit", "<pk_1>", "<C₁₀ hex>", "<C₁₁ hex>", "…"],
["dkg_commit", "<pk_n>", "<Cₙ₀ hex>", "<Cₙ₁ hex>", "…"]
]
}
```
@@ -163,6 +165,8 @@ The event's inner ID identifies the resharing session; all subsequent events in
`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ᵢ`):
@@ -195,7 +199,19 @@ Each contributing member Pᵢ (index `i` from the original DKG, shard `xᵢ`):
}
```
New members verify before proceeding: `Σᵢ∈S Dᵢ[0] == Y`. Abort if this check fails.
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)
@@ -231,7 +247,7 @@ Each new member Qⱼ, after receiving shares from all members of `S`:
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).
6. Sends resharing confirmation (kind 7057) to all other n'1 new members.
### Resharing Confirmation (kind 7057)
@@ -249,7 +265,7 @@ Each new member Qⱼ, after receiving shares from all members of `S`:
`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.
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
@@ -271,6 +287,8 @@ The initiator sends to all members. Any member who will not sign should respond
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ᵢ:
@@ -294,9 +312,11 @@ Each willing signer Pᵢ:
### Round 2 — Signature Shares (kind 7060)
After collecting Round 1 from at least `t` signers, each Pᵢ:
**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.
1. Finalizes the signing set `S` (the participants whose commitments were collected)
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)`
@@ -311,14 +331,24 @@ After collecting Round 1 from at least `t` signers, each Pᵢ:
"tags": [
["e", "<kind 7058 inner id>"],
["quorum", "<Y x-only hex>"],
["z", "<zᵢ hex scalar>"]
["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 (any member) aggregates:
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)`