c2sp.org/cocktail-dkg
This is the development version of this specification, rendered from the tip of the main branch.

Introduction §

Threshold signature schemes (i.e., FROST (RFC 9591)) allow a secret key be shared among nn parties such that any tt of the nn parties can cooperate to generate a digital signature for the group public key.

Initializing a tt of nn threshold requires a key generation protocol. This can be a Distributed Key Generation (DKG) protocol or a Trusted Dealer approach. RFC 9591 specifies a Trusted Dealer approach and leaves DKG specification out of scope.

However, the original FROST paper did specify a DKG protocol based on Verifiable Secret Sharing, Pedersen commitments, and Proofs of Possession. This protocol is secure only if communications are performed over a secure channel, with the following requirements:

To satisfy these requirements in a standalone protocol, Nick and Ruffing proposed ChillDKG as a Bitcoin Improvement Proposal. Because of its tight coupling to the Bitcoin project, ChillDKG was only specified and implemented over the secp256k1 elliptic curve group.

COCKTAIL is a recursive acronym that stands for: "COCKTAIL Orchestrates Cryptographic Key Threshold Agreement for Interoperable Libraries."

COCKTAIL is an independent derivative of ChillDKG intended to be used with any FROST ciphersuite.

Abstract §

COCKTAIL-DKG is a standalone, three-round distributed key generation protocol for threshold signature schemes like FROST.

COCKTAIL-DKG allows a group of nn participants to securely generate a shared group public key and individual secret shares for a tt of nn threshold, without a trusted dealer. The protocol is built on Feldman's Verifiable Secret Sharing (VSS), uses pairwise ECDH to encrypt shares for transport over insecure channels, and includes a final certification round to ensure all participants agree on the outcome. It is designed to be ciphersuite-agile, with specific recommendations for curves like secp256k1, Ed25519 (via Ristretto255), and Ed448.

Design Overview §

COCKTAIL-DKG, like ChillDKG before it, starts off with a simplified variant of a Pedersen Commitment with Proof of Possession protocol, referred to as "SimplPedPop". If SimplPedPop were a standalone protocol, it would depend on an external equality check protocol to ensure all participants received the same messages.

An encryption layer, called "EncPedPop", wraps SimplPedPop with pairwise ECDH to encrypt secret shares.

Finally, an equality check protocol called "CertEq" is built atop EncPedPop to create a standalone protocol.

A coordinator is assumed to facilitate message passing between participants. The coordinator is an untrusted facilitator: it is responsible for receiving messages from all participants, aggregating them where necessary, and broadcasting them to all participants, but it is not trusted with the confidentiality, integrity, or consistency of any protocol message. A malicious coordinator can disrupt availability (a liveness concern: refusing to broadcast, dropping participants, or stalling the protocol; none of which can be prevented by cryptographic mechanisms), but it cannot break confidentiality (it never sees any participant's secret share or the final group secret key) or consistency (any split-view attack, in which different participants receive different messages, causes the CertEq phase to fail safely with all-or-nothing semantics). The coordinator does not need any private key material. The role of the coordinator can be fulfilled by a simple broadcast channel, a peer-to-peer network among the participants themselves, or a dedicated server application; the choice does not change the protocol's security properties.

Supporting Definitions §

This section provides detailed definitions for the notation, message formats, and cryptographic primitives used throughout the COCKTAIL-DKG protocol.

Notation §

Operations §

Message Formats §

All messages exchanged between participants are encoded as byte arrays. The specific encoding of protocol elements is defined below. Implementations MUST adhere to these formats to ensure interoperability.

Primitive Types §

Let GG be an elliptic curve group with a standardized name (e.g., "P-256", "Ed25519", "secp256k1").

Protocol Messages §

The following messages are exchanged during the COCKTAIL-DKG protocol. They are constructed by concatenating the byte representations of their constituent parts in the specified order.

1. msg1|imsg_{1|i} (Participant -> Coordinator, Round 1)

This message contains the participant's VSS commitment, their Proof-of-Possession, their ephemeral public key, and one encrypted share per participant; including a self-share ci,ic_{i,i} addressed to participant ii themselves. The self-share is encrypted under the same procedure as the others; including it keeps the message structure uniform, makes the recovery procedure symmetric across the nn participants, and ensures every entry of the recipient-indexed bundle CirecC^{rec}_i is non-empty.

The full message is the concatenation of these elements:

msg1|i=CiPoPiEici,1~ci,2~ci,n~msg_{1|i} = C_i \parallel PoP_i \parallel E_i \parallel \widetilde{c_{i,1}} \parallel \widetilde{c_{i,2}} \parallel \cdots \parallel \widetilde{c_{i,n}}

where each ci,j~\widetilde{c_{i,j}} is the length-prefixed ciphertext defined above. CiC_i, PoPiPoP_i, and EiE_i are fixed-length per the ciphersuite and are concatenated without additional framing.

2. msg2msg2 (Coordinator -> All Participants, Round 2)

This message aggregates the public information from all participants and the ciphertexts addressed to the recipient. It is equivalent to an ordered, recipient-specific projection of all Round 1 messages.

The message broadcast to participant ii is structured as:

msg2|i=(C1PoP1E1c1,i~)(CnPoPnEncn,i~)msg_{2|i} = (C_1 \parallel PoP_1 \parallel E_1 \parallel \widetilde{c_{1,i}}) \parallel \cdots \parallel (C_n \parallel PoP_n \parallel E_n \parallel \widetilde{c_{n,i}})

where cj,i~\widetilde{c_{j,i}} is the length-prefixed framed ciphertext defined under msg1|imsg_{1|i} above. Concrete encodings MUST preserve the participant ordering; CjC_j, PoPjPoP_j, and EjE_j are fixed-length per the ciphersuite, and each cj,i~\widetilde{c_{j,i}} carries its own 64-bit big-endian length prefix, so the full message parses unambiguously.

Implementations MAY instead broadcast the complete ordered list of all msg1|jmsg_{1|j} messages to every participant.

3. sigisig_i (Participant -> Coordinator, CertEq Phase)

This message contains the participant's signature over the protocol transcript.

4. Aggregated Signatures (Coordinator -> All Participants, CertEq Phase)

This is the final message, containing all signatures on the transcript.

The message is structured as:

aggregatedsigs=sig1sig2signaggregated_sigs = sig_1 \parallel sig_2 \parallel \cdots \parallel sig_n

Cryptographic Primitives §

As COCKTAIL-DKG is intended to be used in conjunction with RFC 9591, we begin our enumeration of named hash functions with "H6".

Protocol Definition §

This section describes the COCKTAIL-DKG protocol in detail.

Setup §

Each participant ii is assumed to have:

Round 1: Commitment and Encryption §

  1. Generate Polynomial: Participant ii generates a secret polynomial fi(x)f_i(x) of degree t1t-1: fi(x)=ai,0+ai,1x++ai,t1xt1f_i(x) = a_{i,0} + a_{i,1}x + \cdots + a_{i,t-1}x^{t-1}.

    The coefficients ai,ka_{i,k} are chosen as random scalars via RandomScalar()RandomScalar(). ai,0a_{i,0} is the participant's primary secret share. If any sampled coefficient equals 00 (probability 2log2q\approx 2^{-\lceil \log_2 q \rceil} per coefficient, negligible in practice), implementations MUST resample that coefficient before continuing, so that every Ci,k=ai,kBC_{i,k} = a_{i,k} \cdot B is a non-identity point and the parsing rule at Primitive Types rejecting identity VSS commitment points does not cause an honest abort.

  2. Compute VSS Commitment: Participant ii computes a VSS commitment CiC_i to their polynomial by creating a public commitment for each coefficient: Ci=(Ci,0,Ci,1,,Ci,t1)C_i = (C_{i,0}, C_{i,1}, \cdots, C_{i,t-1}), where Ci,k=ai,kBC_{i,k} = a_{i,k} * B.

  3. Generate Ephemeral Key: Participant ii generates a fresh ephemeral key pair (ei,Ei)(e_i, E_i) for this session with eiRandomScalar()e_i \leftarrow RandomScalar() and Ei=eiBE_i = e_i \cdot B. If ei=0e_i = 0 (negligible probability), implementations MUST resample, so that EiE_i is a non-identity point and the parsing rule at Primitive Types rejecting identity ephemeral public keys does not cause an honest abort.

  4. Compute Proof of Possession (PoP): Participant ii computes a digital signature PoPiPoP_i over a concatenation of the context string, their VSS commitment CiC_i, and their ephemeral public key EiE_i. The signature is created using the secret ai,0a_{i,0} as the private key and Ci,0C_{i,0} as the public key. The message to be signed is context || C_i || E_i, where CiC_i is encoded as the byte concatenation Ci,0Ci,1Ci,t1C_{i,0} \parallel C_{i,1} \parallel \cdots \parallel C_{i,t-1} (each point in the ciphersuite's compressed encoding, identical to its encoding in msg1|imsg_{1|i}). The signature algorithm is the Schnorr scheme defined in Schnorr Signature Scheme. If Sign aborts at its step 3 due to k=0k = 0 (probability 2log2q\approx 2^{-\lceil \log_2 q \rceil}, negligible), participant ii MUST treat this DKG session as locally failed: it MUST NOT send msg1|imsg_{1|i}, MUST NOT publish any partially-derived value, and MUST abandon this session. To retry, participant ii joins a fresh DKG session with a different context string (which changes the Sign message mm and therefore the deterministic nonce kk); simply resampling ai,0a_{i,0} within the same context would require restarting Round 1 from step 1 and is indistinguishable from the participant dropping out, so a fresh-context retry is the recommended path. This is not a blameable misbehavior. The other participants observe only that participant ii failed to deliver msg1|imsg_{1|i} within the agreed timeout.

  5. Compute and Encrypt Shares: For each participant jj from 11 to nn (including j=ij = i, the self-share):

    1. Compute Share: Participant ii computes the secret share si,j=fi(j)s_{i,j} = f_i(j).
    2. Derive Key: Participant ii computes two ECDH shared secrets: one with their ephemeral key and the recipient's static public key, and one with their static key and the recipient's static public key: Se_i,j=eiPjS^{\mathrm{e}}\_{i,j} = e_i * P_j and Sd_i,j=diPjS^{\mathrm{d}}\_{i,j} = d_i * P_j. It then derives a symmetric key and nonce for the AEAD.
      • If the hash function has an output length of at least 56 bytes (448 bits):
        • tmp=H6(Se_i,jSd_i,j,Ei,Pi,Pj,context)tmp = H6(S^{\mathrm{e}}\_{i,j} \parallel S^{\mathrm{d}}\_{i,j}, E_i, P_i, P_j, context).
        • ki,j=tmp[0:32]k_{i,j} = tmp[0:32]
        • ivi,j=tmp[32:56]iv_{i,j} = tmp[32:56]
      • Otherwise:
        • ki,j=H6(Se_i,jSd_i,j,Ei,Pi,Pj,deriveextra(context,"key"))k_{i,j} = H6(S^{\mathrm{e}}\_{i,j} \parallel S^{\mathrm{d}}\_{i,j}, E_i, P_i, P_j, \mathrm{derive\\_extra}(context, \text{"key"}))
        • ivi,j=H6(Se_i,jSd_i,j,Ei,Pi,Pj,deriveextra(context,"nonce"))[0:24]iv_{i,j} = H6(S^{\mathrm{e}}\_{i,j} \parallel S^{\mathrm{d}}\_{i,j}, E_i, P_i, P_j, \mathrm{derive\\_extra}(context, \text{"nonce"}))[0:24]
    3. Prepare Plaintext: Participant ii prepares the plaintext to encrypt. This consists of the secret share si,js_{i,j} followed by an optional application-defined payload payloadi,jpayload_{i,j}: plaintexti,j=si,jpayloadi,jplaintext_{i,j} = s_{i,j} \parallel payload_{i,j}. If no application payload is used, payloadi,jpayload_{i,j} is empty.
    4. Encrypt Share: Participant ii encrypts the plaintext for participant jj: ci,j=Enc(plaintexti,j,ki,j,ivi,j)c_{i,j} = Enc(plaintext_{i,j}, k_{i,j}, iv_{i,j}).
  6. Broadcast: Participant ii sends their msg1|imsg_{1|i} to the coordinator.

Round 2: Share Decryption and Verification §

The coordinator waits to receive msg1|imsg_{1|i} from all nn participants. It then broadcasts a list of all received messages to every participant. Upon receiving the list of all msg1|imsg_{1|i} messages, each participant ii performs the following steps:

  1. Validate Commitments: For each participant jj from 11 to nn:
    • Participant ii verifies that the VSS commitment CjC_j contains exactly tt points. If |Cj|t|C_j| \neq t, participant ii MUST abort, identifying participant jj as malicious. This check prevents threshold elevation attacks.
  2. Verify All PoPs: For each participant jj from 11 to nn:
    • Participant ii verifies the proof of possession PoPjPoP_j using the Schnorr Verify algorithm. The signature is checked against the message context || C_j || E_j (with CjC_j encoded as in Round 1), using participant jj's public commitment Cj,0C_{j,0} as the public key.
    • If any PoPjPoP_j is invalid, participant ii MUST abort, identifying participant jj as malicious.
  3. Decrypt and Verify Shares: For each participant jj from 11 to nn (including the self-share j=ij = i):
    1. Derive Key: Participant ii computes two ECDH shared secrets: one with the sender's ephemeral public key and their static key, and one with the sender's static public key and their static key: Se_j,i=diEjS^{\mathrm{e}}\_{j,i} = d_i * E_j and Sd_j,i=diPjS^{\mathrm{d}}\_{j,i} = d_i * P_j. They then derive the symmetric key and nonce:
      • If the hash function has an output length of at least 56 bytes (448 bits):
        • tmp=H6(Se_j,iSd_j,i,Ej,Pj,Pi,context)tmp = H6(S^{\mathrm{e}}\_{j,i} \parallel S^{\mathrm{d}}\_{j,i}, E_j, P_j, P_i, context).
        • kj,i=tmp[0:32]k_{j,i} = tmp[0:32]
        • ivj,i=tmp[32:56]iv_{j,i} = tmp[32:56]
      • Otherwise:
        • kj,i=H6(Se_j,iSd_j,i,Ej,Pj,Pi,deriveextra(context,"key"))k_{j,i} = H6(S^{\mathrm{e}}\_{j,i} \parallel S^{\mathrm{d}}\_{j,i}, E_j, P_j, P_i, \mathrm{derive\\_extra}(context, \text{"key"}))
        • ivj,i=H6(Se_j,iSd_j,i,Ej,Pj,Pi,deriveextra(context,"nonce"))[0:24]iv_{j,i} = H6(S^{\mathrm{e}}\_{j,i} \parallel S^{\mathrm{d}}\_{j,i}, E_j, P_j, P_i, \mathrm{derive\\_extra}(context, \text{"nonce"}))[0:24]
    2. Decrypt Plaintext: Participant ii decrypts the ciphertext sent to them from participant jj: plaintextj,i=Dec(cj,i,kj,i,ivj,i)plaintext_{j,i} = Dec(c_{j,i}, k_{j,i}, iv_{j,i}). If decryption fails, participant ii MUST abort and report a decryption failure for the ciphertext attributed to participant jj.
    3. Parse Plaintext: Participant ii parses the plaintext to extract the secret share sj,is_{j,i} (the first scalar-sized portion) and any optional application payload payloadj,ipayload_{j,i} (the remainder). If the plaintext is shorter than the ciphersuite's scalar encoding size, or if the leading scalar-sized portion does not decode to a valid scalar in [0,q1][0, q-1], participant ii MUST abort, identifying jj as malicious.
    4. Verify Share: Participant ii verifies the decrypted share sj,is_{j,i} against jj's VSS commitment: sj,iB=k=0t1ikCj,ks_{j,i} * B = \sum_{k=0}^{t-1} i^k * C_{j,k} If the check fails, participant ii MUST abort, identifying jj as malicious.
  4. Compute Final Keys: If all shares are successfully decrypted and verified:
    1. Secret Share: Participant ii computes their final long-lived secret share by summing all received shares: xi=j=1nsj,ix_i = \sum_{j=1}^{n} s_{j,i}.
    2. Group Public Key: Participant ii computes the group public key: Y=j=1nCj,0Y = \sum_{j=1}^n C_{j,0}.
    3. Verification Share: Participant ii computes their public verification share YiY_i. This is done by first computing the aggregated commitment for each coefficient kk: Cagg,k=j=1nCj,kC_{agg,k} = \sum_{j=1}^{n} C_{j,k}. Then, Yi=k=0t1ikCagg,kY_i = \sum_{k=0}^{t-1} i^k * C_{agg,k}.
    4. Final Check: Participant ii performs a final self-consistency check: xiB=Yix_i * B = Y_i. If this check fails, the participant MUST abort. Note: This check is mathematically guaranteed to pass if all VSS share verifications in step 3.4 succeeded. It serves as a defense-in-depth measure to catch implementation bugs in the share summation or verification share computation.

Round 3: Certification §

This round ensures that all honest participants have arrived at the same public state.

  1. Construct Transcript: Each participant ii constructs a canonical byte representation of the final public transcript, TT. The transcript MUST be constructed by concatenating the following elements in this exact order:

    1. len(𝚌𝚒𝚙𝚑𝚎𝚛𝚜𝚞𝚒𝚝𝚎𝚒𝚍)\mathrm{len}(\mathtt{ciphersuite\\_id}): The length of the ciphersuite identifier string (e.g., COCKTAIL(Ristretto255, SHA-512)) as a little-endian 64-bit unsigned integer.
    2. 𝚌𝚒𝚙𝚑𝚎𝚛𝚜𝚞𝚒𝚝𝚎𝚒𝚍\mathtt{ciphersuite\\_id}: The ciphersuite identifier as its UTF-8 byte representation. Including the ciphersuite identifier in the transcript makes the success certificate self-describing and prevents cross-ciphersuite confusion in audit and recovery tooling.
    3. len(context)len(context): The length of the context string as a little-endian 64-bit unsigned integer.
    4. contextcontext: The context string bytes.
    5. nn: The number of participants as a little-endian 32-bit unsigned integer.
    6. tt: The threshold as a little-endian 32-bit unsigned integer.
    7. For jj from 11 to nn: PjP_j (the static public key of participant jj, in its standard encoding).
    8. For jj from 11 to nn: CjC_j (the full VSS commitment of participant jj: Cj,0Cj,t1C_{j,0} \parallel \cdots \parallel C_{j,t-1}).
    9. For jj from 11 to nn: PoPjPoP_j (the Proof of Possession signature from participant jj).
    10. For jj from 11 to nn: EjE_j (the ephemeral public key from participant jj).
    11. len(ext)len(ext): The length of the application-specific extension as a little-endian 64-bit unsigned integer.
    12. extext: The application-specific extension bytes (may be empty).

    All participants MUST produce identical transcripts. Any difference indicates a split-view attack or implementation bug.

  2. Sign Transcript: Participant ii signs the transcript TT with their long-term static private key did_i using the Schnorr Sign algorithm, producing a signature sigisig_i. If Sign aborts at its step 3 due to k=0k = 0 (negligible probability), participant ii MUST NOT publish any partially-derived value and MUST treat this session as locally failed (analogous to the Round 1 PoP k = 0 handling). Since the transcript TT is identical across all honest participants, restarting CertEq within the same DKG session would reproduce the same (di,T)(d_i, T) inputs to Sign and therefore the same k=0k = 0 result; the only safe retry path is a fresh DKG session with a different context (which yields a different TT).

  3. Broadcast Signature: Participant ii sends sigisig_i to the coordinator.

  4. Verify Certificate: The coordinator broadcasts the set of all signatures sig1,,sign{sig_1, \cdots, sig_n} to everyone. Each participant ii verifies every signature sigjsig_j on the transcript TT against the public key PjP_j using the Schnorr Verify algorithm.

  5. Success: If all signatures are valid, the DKG is successful. The participant stores their secret share xix_i and the group public key YY. The collection of TT and all nn signatures on it is called a "success certificate" and can be stored for auditing.

Application-Specific Extensions §

COCKTAIL-DKG supports an optional application-specific extension that is appended to the transcript before signing. This allows protocols that build atop COCKTAIL-DKG to ensure all parties agree on some application-defined value.

Extension Format:

The extension is appended to the transcript as:

Recommended Use Cases:

Applications MAY use the extension field for different purposes. Two common patterns are:

  1. Consensus on External State: Ensure all parties commit to some agreed-upon value (e.g., a Merkle tree root, a configuration hash, or a session identifier). The protocol only succeeds if everyone provides the same extension value. This is useful when the DKG must be bound to external application state.

  2. Collective Randomness Derivation: All participants hash together independently-contributed random data via the extension. If the transcript hashes all match, the participants can derive a shared random value that no single party could have predicted or biased. This is useful for protocols that need distributed randomness as a byproduct of the DKG.

These use cases are not compatible with each other; an application must choose one approach. The extension semantics are entirely application-defined; COCKTAIL-DKG simply ensures that all participants agreed on the extension value before the protocol completes.

Deriving Extensions from Payloads:

When applications use the optional payloads in the encryption step (see Optional Application Payloads), they MAY derive the extension from those payloads to ensure all participants agree on the exchanged data. A recommended approach is to compute a hash of the participant-ordered payloads:

  1. For each participant jj from 11 to nn, collect their payload contributions. To produce a consistent extension across all participants, the application MUST ensure every recipient observes the same payloadjpayload_j from participant jj (e.g., by having jj broadcast the same payload to every recipient).

    In this construction, payloadjpayload_j denotes the single application payload sent by participant jj, and is only well-defined when payloadj,1==payloadj,npayload_{j,1} = \cdots = payload_{j,n}.

  2. Compute the extension as: ext=H(nlen(payload1)payload1len(payloadn)payloadn)ext = H(n \parallel len(payload_1) \parallel payload_1 \parallel \cdots \parallel len(payload_n) \parallel payload_n) Where HH is the ciphersuite's hash function (the same HH used by H6H6), nn is encoded as a little-endian 64-bit unsigned integer, and each len(payloadj)len(payload_j) is a little-endian 64-bit unsigned integer.

This ensures that any disagreement about the payloads will result in different transcripts and failed signature verification.

Security Note:

The extension is included in the transcript and thus covered by all participants' signatures. This provides the same consensus guarantee as the rest of the transcript: if any participant has a different extension value, the signatures will not verify and the protocol will abort safely.

Error Handling §

A robust implementation of COCKTAIL-DKG MUST handle a variety of error conditions. Errors can arise from malformed messages, invalid cryptographic values, or protocol violations. The ability to distinguish between these cases is crucial for security and usability.

Error Categories §

We recommend that implementations define distinct error types to represent different failure modes. This allows applications to react appropriately, whether by retrying an operation, aborting the protocol, or initiating a blame-finding procedure.

The following categories cover the most common errors:

  1. Parsing and Deserialization Errors:
    • Description: These errors occur when a received message does not conform to the byte-based format specified in the Message Formats section. This could be due to an incorrect length, an invalid point or scalar encoding, or other structural defects.
    • Action: An honest participant should never produce a malformed message. If a participant receives such a message, it should be treated as evidence of a bug in the sender's implementation or a deliberate protocol violation. The protocol MUST be aborted. If the sender can be identified (e.g., in msg1|imsg_{1|i}), they should be flagged as faulty.
  2. Cryptographic Verification Failures:
    • Description: These errors occur when a cryptographic check fails. This category includes:
      • An invalid Proof-of-Possession (PoPjPoP_j).
      • A VSS share verification failure (sj,iBk=0t1ikCj,ks_{j,i} \cdot B \ne \sum_{k=0}^{t-1} i^k \cdot C_{j,k}).
      • A failed decryption of an encrypted share (cj,ic_{j,i}).
      • An invalid signature on the final transcript (sigjsig_j).
    • Action: A cryptographic failure is a clear indication that a participant is behaving maliciously or has a serious bug. The protocol MUST be aborted immediately. For public failures such as an invalid PoP, invalid VSS share, or invalid transcript signature, the participant who sent the invalid data MUST be identified and blamed. A decryption failure is locally attributable to the sender's ciphertext, but it is not publicly verifiable in the same way as a VSS or PoP failure: the AEAD key derives from the recipient's static private key. See Differences from ChillDKG for the option set: Options 1 and 2 establish only that a specific ciphertext was sent (ciphertext binding), while only Option 3 provides public verification of the decryption outcome itself.
  3. Protocol Logic Errors:
    • Description: These errors relate to violations of the protocol's state machine or rules, such as:
      • A participant sending a message at the wrong time.
      • The coordinator broadcasting an inconsistent msg2 (e.g., omitting a participant's data).
    • Action: These errors indicate a faulty participant or coordinator. The protocol MUST be aborted. If the error can be traced to a specific participant, they should be blamed.

Blame-Finding and Reporting §

A key feature of a secure DKG protocol is the ability to identify malicious participants. When an error occurs, the protocol MUST terminate and, when evidence permits, output information about who caused the failure. This is crucial for accountability in decentralized systems.

Ciphersuites §

This section describes the ciphersuites that are specified for use with COCKTAIL. The current scope includes both RFC 9591 and ZIP-0312.

Each ciphersuite defines a key derivation function H6(x,E,Ps,Pr,extra)H6(x, E, P_s, P_r, extra), an encryption method Enc(plain,key,iv)Enc(plain, key, iv), and a decryption method Dec(cipher,key,iv)Dec(cipher, key, iv). Ciphersuites MUST use an AEAD mode for Enc()Enc() and Dec()Dec(). The protocol's confidentiality, ciphertext-integrity, blame-finding, and optional-payload authentication properties all rely on AEAD; a non-AEAD Enc()/Dec()Enc()/Dec() pair is not a valid COCKTAIL-DKG ciphersuite.

The choice of AEAD is guided by the principle of preventing nonce reuse. All COCKTAIL-DKG AEAD nonces are deterministically derived via H6H6 (see Round 1: Commitment and Encryption); implementations MUST NOT sample nonces randomly. The 24-byte (192-bit) nonce width was chosen so that the H6H6-derived nonces have a negligible probability of collision across distinct (sender,recipient,session)(sender, recipient, session) triples even though they are not random; i.e., the width is a safety margin for deterministic derivation, not a license to sample nonces independently. For ciphersuites where the underlying hash function provides a large enough output (at least 56 bytes / 448 bits; e.g., SHA-512), we can derive both the 256-bit key and the 24-byte nonce directly from a single H6H6 output.

For ciphersuites based on SHA-256, where the output is smaller than 56 bytes, we use two domain-separated H6()H6() invocations. For the key, H6H6 is called with deriveextra(context,"key")\mathrm{derive\\_extra}(context, \text{"key"}) and the full 32-byte output is used. For the nonce, H6H6 is called with deriveextra(context,"nonce")\mathrm{derive\\_extra}(context, \text{"nonce"}) and the first 24 bytes are used. The AEAD of choice for the SHA-256 based ciphersuites we specify here is XAES-256-GCM.

The H6H6 function is used to derive a symmetric key and nonce from ECDH shared secrets. Unless otherwise specified, it is defined as:

H6(x,E,Ps,Pr,extra)=H(prefixxEPsPrlen(extra)extra)H6(x, E, P_s, P_r, extra) = H(prefix \parallel x \parallel E \parallel P_s \parallel P_r \parallel len(extra) \parallel extra)

The output of H6H6 is used to derive the key and nonce for the AEAD.

For hash functions with output smaller than 56 bytes, deriveextra(context,label)\mathrm{derive\\_extra}(context, label) is defined as:

deriveextra(context,label)=len(context)contextlen(label)label\mathrm{derive\\_extra}(context, label) = len(context) \parallel context \parallel len(label) \parallel label

where both lengths are encoded as little-endian 64-bit unsigned integers, and labellabel is the ASCII string "key" or "nonce" (without quotes).

ECDH Shared-Secret Encoding §

Each ECDH shared secret S=sPS = s \cdot P (the scalar-mult result that is fed into H6H6 as part of x=SeSdx = S^{\mathrm{e}} \parallel S^{\mathrm{d}}) is encoded as a fixed-length byte string per the ciphersuite, so that the concatenation xx is unambiguously parsed and reproducible across implementations:

Ciphersuite ECDH encoding of S=sPS = s \cdot P Size
COCKTAIL(Ed25519, SHA-512) Canonical Ristretto255 encoding of SS (RFC 9496 §4.3.2) 32 B
COCKTAIL(Ristretto255, SHA-512) Canonical Ristretto255 encoding of SS (RFC 9496 §4.3.2) 32 B
COCKTAIL(Ed448, SHAKE256) RFC 8032 compressed Ed448 encoding of SS 57 B
COCKTAIL(P-256, SHA-256) SEC1 compressed point encoding of SS 33 B
COCKTAIL(secp256k1, SHA-256) SEC1 compressed point encoding of SS 33 B
COCKTAIL(JubJub, BLAKE2b-512) ZIP-0312 SerializeElement of SS (32-byte little-endian, y-parity) 32 B
COCKTAIL(Pallas, BLAKE2b-512) Halo2/Orchard compressed encoding of SS (32-byte little-endian, y-parity) 32 B

For Curve25519-family ciphersuites (COCKTAIL(Ed25519, SHA-512), COCKTAIL(Ristretto255, SHA-512)), the ECDH shared secret is the canonical 32-byte Ristretto255 encoding of S=sPS = s \cdot P as defined in RFC 9496 §4.3.2. Both ciphersuites operate internally over Ristretto255 (see Working with curves with small subgroups) and therefore produce byte-identical ECDH shared secrets for equal scalar/point inputs; the ciphersuites are domain-separated only via their distinct H6H6, H7H7, and nonce prefixes.

For Ed448 ciphersuites (COCKTAIL(Ed448, SHAKE256)), the ECDH shared secret is the 57-byte RFC 8032 compressed Ed448 encoding of S=sPS = s \cdot P. Because all decoded protocol points are required to be non-identity prime-order subgroup points, SS is computed inside the prime-order subgroup and has a unique compressed Ed448 encoding. Decaf448-internal implementations use the deterministic Decaf448-to-Ed448 output mapping defined in the COCKTAIL(Ed448, SHAKE256) ciphersuite definition before feeding SS into H6H6.

For Weierstrass-curve ciphersuites (COCKTAIL(P-256, SHA-256), COCKTAIL(secp256k1, SHA-256)), the ECDH shared secret is the SEC1 compressed encoding of S=sPS = s \cdot P: a one-byte tag {0x02,0x03}\{0x02, 0x03\} indicating the parity of SS's yy-coordinate, followed by the 32-byte big-endian encoding of SS's xx-coordinate.

For Zcash-family ciphersuites (COCKTAIL(JubJub, BLAKE2b-512), COCKTAIL(Pallas, BLAKE2b-512)), the ECDH shared secret is the standard Zcash compressed encoding of SS, with conventions that differ between JubJub and Pallas:

Both are 32-byte canonical encodings; implementations MUST use the curve-appropriate convention and MUST NOT confuse them.

In every case, the encoding MUST be canonical (a single byte representation per element); non-canonical encodings MUST be rejected by the recipient on decode of SS when SS is exchanged outside this protocol. Within COCKTAIL-DKG itself, each side computes its own SS locally and encodes it deterministically, so the encoded value is byte-identical on the sender and recipient sides by construction.

Ciphersuite Definitions §

Each ciphersuite is identified by a canonical ciphersuite_id string. The ciphersuite_id is consensus-critical: it is bound into the session context, the canonical transcript TT, the test-vector labeled-hash derivation, and per-implementation domain separation. Implementations MUST use the exact byte representations below (no whitespace normalization, no case folding, no alternate punctuation); the strings are UTF-8 encoded but happen to be pure ASCII.

Ciphersuite group ciphersuite_id byte string
Ed25519 / SHA-512 COCKTAIL(Ed25519, SHA-512)
Ristretto255 / SHA-512 COCKTAIL(Ristretto255, SHA-512)
Ed448 / SHAKE256 COCKTAIL(Ed448, SHAKE256)
P-256 / SHA-256 COCKTAIL(P-256, SHA-256)
secp256k1 / SHA-256 COCKTAIL(secp256k1, SHA-256)
JubJub / BLAKE2b-512 COCKTAIL(JubJub, BLAKE2b-512)
Pallas / BLAKE2b-512 COCKTAIL(Pallas, BLAKE2b-512)

Implementations MUST reject any transcript whose ciphersuite_id field, which appears as explicit length-prefixed bytes at the head of TT (see Round 3: Certification), does not exactly match one of the byte strings above. For the session context, which under the recommended construction is a hash digest over a preimage that includes ciphersuite_id rather than the raw ciphersuite_id bytes, the corresponding check is structural: implementations MUST ensure their own context preimage uses the exact ciphersuite_id byte string for the ciphersuite they believe they are running, and MUST abort if any participant's reconstructed context value disagrees with the one they themselves derived (cf. Setup, where context-agreement is already a MUST). For implementations using a non-hashing context construction in which ciphersuite_id appears as an explicit substring, the direct byte-string match above applies.

Schnorr Signature Scheme §

COCKTAIL-DKG uses a simple Schnorr signature scheme for both the Proof of Possession (PoP) and the transcript certification in Round 3. The signature scheme is defined as follows:

Schnorr Signature Format §

A signature consists of two components:

The signature is encoded as the concatenation of the compressed point encoding of RR followed by the scalar encoding of zz. The total signature size is the point size plus the scalar size for the ciphersuite's group:

Ciphersuite group Point size Scalar size Signature size
Ristretto255 / Ed25519 32 bytes 32 bytes 64 bytes
Ed448 57 bytes 57 bytes 114 bytes
P-256 / secp256k1 33 bytes 32 bytes 65 bytes
JubJub 32 bytes 32 bytes 64 bytes
Pallas 32 bytes 32 bytes 64 bytes

Schnorr Sign Algorithm §

Given a secret key sksk (a scalar) and a message mm (a byte string):

  1. Compute the public key: pk=skBpk = sk * B
  2. Compute a deterministic nonce: k=HashToScalar(prefixnonceencode(sk)m)k = HashToScalar(prefix_{nonce} \parallel encode(sk) \parallel m)
  3. Reject k=0k = 0: if k=0k = 0 (probability 2log2q\approx 2^{-\lceil \log_2 q \rceil}, negligible), implementations MUST abort signing immediately and MUST NOT reveal any further derived value. A k=0k = 0 would yield R=OR = O (the identity), which the Verify algorithm rejects at step 2, but the response z=k+csk=cskz = k + c \cdot sk = c \cdot sk would also be computable from the signer's outputs and would directly leak sksk to any observer who saw zz. Since the nonce is deterministic, a signer who hits this branch on a given (sk,m)(sk, m) pair MUST NOT simply retry with the same inputs; this is treated as an unrecoverable signing failure for this (sk,m)(sk, m) pair. Applications MAY introduce an additional disambiguator (e.g., a 32-byte counter mixed into prefixnonceprefix_{nonce}) as an out-of-band recovery mechanism, but the base scheme aborts.
  4. Compute the commitment: R=kBR = k * B
  5. Compute the challenge: c=HashToScalar(prefixH7Rpkm)c = HashToScalar(prefix_{H7} \parallel R \parallel pk \parallel m)
  6. Compute the response: z=k+cskmodqz = k + c \cdot sk \bmod q
  7. Return the signature (R,z)(R, z)

Where:

Schnorr Verify Algorithm §

Given a signature (R,z)(R, z), a public key pkpk, and a message mm:

  1. Reject if zz does not decode to a canonical scalar in [0,q1][0, q-1].
  2. Reject if RR does not decode to a valid prime-order subgroup point that is not the identity.
  3. Compute the challenge: c=HashToScalar(prefixH7Rpkm)c = HashToScalar(prefix_{H7} \parallel R \parallel pk \parallel m)
  4. Compute the left-hand side: lhs=zBlhs = z * B
  5. Compute the right-hand side: rhs=R+cpkrhs = R + c * pk
  6. Return true if lhs=rhslhs = rhs, otherwise return false

Schnorr Prefix Strings §

The ciphersuite-specific values for prefixH7prefix_{H7} and prefixnonceprefix_{nonce} are:

Ciphersuite prefixH7prefix_{H7} prefixnonceprefix_{nonce}
COCKTAIL(Ed25519, SHA-512) COCKTAIL-DKG-Ed25519-SHA512-H7 COCKTAIL-DKG-Ed25519-SHA512-NONCE
COCKTAIL(Ristretto255, SHA-512) COCKTAIL-DKG-Ristretto255-SHA512-H7 COCKTAIL-DKG-Ristretto255-SHA512-NONCE
COCKTAIL(Ed448, SHAKE256) COCKTAIL-DKG-Ed448-SHAKE256-H7 COCKTAIL-DKG-Ed448-SHAKE256-NONCE
COCKTAIL(P-256, SHA-256) COCKTAIL-DKG-P256-SHA256-H7 COCKTAIL-DKG-P256-SHA256-NONCE
COCKTAIL(secp256k1, SHA-256) (BIP-340 tagged hash; see below) (BIP-340 tagged hash; see below)
COCKTAIL(JubJub, BLAKE2b-512) COCKTAIL-DKG-JubJub-BLAKE2b-H7 COCKTAIL-DKG-JubJub-BLAKE2b-NONCE
COCKTAIL(Pallas, BLAKE2b-512) COCKTAIL-DKG-Pallas-BLAKE2b-H7 COCKTAIL-DKG-Pallas-BLAKE2b-NONCE

For the secp256k1 ciphersuite, the nonce and challenge derivations use BIP-340 tagged hashes directly and MUST override (not wrap) the generic Sign step 2 and step 5 formulas above. The k=0k = 0 rejection rule from Sign step 3 also applies (and aborts under the same conditions):

Where taggedHash(tag,msg)=SHA256(SHA256(tag)SHA256(tag)msg)taggedHash(tag, msg) = SHA256(SHA256(tag) \parallel SHA256(tag) \parallel msg) as defined in BIP-340, tagtag is encoded as its UTF-8 byte representation, encode(sk)encode(sk) is the 32-byte big-endian secp256k1 scalar encoding, and OS2IPOS2IP interprets the 32-byte tagged-hash output as a big-endian integer. The same Verify step 3 substitution applies. The resulting scalar bias is approximately 21282^{-128} (specifically 1.272128\approx 1.27 \cdot 2^{-128}) for secp256k1 because q22562128q \approx 2^{256} - 2^{128}.

Schnorr Hash-to-Scalar Reduction §

HashToScalarHashToScalar takes a byte string inputinput and reduces it to a scalar in [0,q1][0, q-1]. The construction depends on the ciphersuite's hash output length and on the ratio between 28L2^{8L} (where LL is the hash output byte-length) and qq:

Schnorr Security Notes §

Security Considerations §

Working with curves with small subgroups §

Certain elliptic curves used in cryptography are in the so-called Montgomery or Edwards model, picked for particularly efficient arithmetic. The downside to these curves is that they have an order which factors as n=[h]qn = [h] * q for some small value hh and large prime qq. If an attacker can select a point PP of order hh they can potentially leak partial information about the secret scalars from the output of [s]P[s] P. (They specifically learn smodhs \bmod h).

Curves in these families we consider include ed25519 with h=8h = 8, edwards448 with h=4h = 4, and JubJub with h=8h = 8. For Ed25519, COCKTAIL-DKG uses Ristretto255 internally and emits Ed25519 outputs via a canonical lift. For Ed448, COCKTAIL-DKG emits raw RFC 9591-compatible Ed448 outputs, but implementations may still use Decaf448 internally as an implementation strategy as long as they apply the specified Decaf448-to-Ed448 output mapping and enforce the same prime-order input validation. In all cases, decoded protocol points on cofactor-bearing curves MUST be rejected unless they are valid, non-identity, prime-order subgroup points.

The "cofactor clearing then check for identity" shortcut ([h]P=?O[h] \cdot P \stackrel{?}{=} O) tests only that PP is not a small-subgroup point and does NOT establish prime-order subgroup membership for mixed-order points. It MUST NOT be used as a substitute for explicit subgroup-membership checks. Separately, cofactor clearing on input does not define a canonical raw-curve output representative; any ciphersuite that uses an internal prime-order abstraction but emits raw curve encodings must specify its output lift explicitly, as COCKTAIL-DKG does for Ed25519 and Ed448.

JubJub does not have an analogous prime-order group abstraction in widespread use. For COCKTAIL-DKG implementations using the JubJub ciphersuite, validating that a decoded byte string represents a valid curve point is not sufficient. All decoded protocol points (static public keys PiP_i, ephemeral public keys EiE_i, VSS commitment points Ci,kC_{i,k}, and Schnorr commitment points RR) MUST be verified to lie in the prime-order subgroup of order qq by an explicit subgroup-membership check: compute [q]P[q] \cdot P and abort if the result is not the identity element. The "cofactor clearing then check for identity" alternative ([h]P=?O[h] \cdot P \stackrel{?}{=} O) tests only that PP is not a small-subgroup point and does NOT establish prime-order subgroup membership for mixed-order points; it MUST NOT be used as a substitute for the explicit subgroup-membership check. Raw cofactor-8 JubJub points that fail the subgroup check MUST NOT be accepted as protocol points.

COCKTAIL-DKG handles abstractions and underlying raw curves on a per-ciphersuite basis:

Implementations that need to interoperate with separate protocols using raw curves not covered by the lift rules above MUST treat any such cross-abstraction lift as out-of-band and specify the canonical-representative selection there; cofactor clearing alone is not sufficient when the raw curve is not directly addressed by this document.

Optional Application Payloads §

COCKTAIL-DKG supports an optional application-defined payload that can be included alongside each secret share in the encrypted ciphertext. This feature enables applications to derive additional shared secrets during the DKG without running a separate protocol.

Use Cases:

Payload Format:

The plaintext encrypted in each ciphertext ci,jc_{i,j} has the format: plaintexti,j=si,jpayloadi,jplaintext_{i,j} = s_{i,j} \parallel payload_{i,j}

Where si,js_{i,j} is the fixed-size secret share (determined by the scalar size of the ciphersuite) and payloadi,jpayload_{i,j} is the variable-length application payload (which may be empty).

Payload Commitments:

If applications use payloads and need to ensure all participants agree on the payloads exchanged, they SHOULD include commitments to the payloads in the transcript or in application-specific extension data. A recommended commitment format is: commitment=H(len(payload)payload)commitment = H(len(payload) \parallel payload)

Where len(payload)len(payload) is the length of the payload encoded as a little-endian 64-bit integer. This format ensures domain separation between payloads of different lengths.

Security Note:

The optional payload is encrypted and authenticated by the same AEAD that protects the secret share. However, the payload is NOT verified against the VSS commitments (only the share portion is). Applications using payloads MUST implement their own verification logic if payload integrity beyond AEAD authentication is required.

Differences from ChillDKG §

COCKTAIL-DKG is derived from ChillDKG but introduces several important changes that break backwards compatibility:

1. Key Derivation Uses Both Static and Ephemeral ECDH §

ChillDKG derives encryption keys using only an ephemeral-to-static ECDH:

Si,j=eiPjS_{i,j} = e_i * P_j

COCKTAIL-DKG uses both ephemeral-to-static AND static-to-static ECDH:

Se_i,j=eiPjS^{\mathrm{e}}\_{i,j} = e_i * P_j and Sd_i,j=diPjS^{\mathrm{d}}\_{i,j} = d_i * P_j

Why this matters:

Including the sender's static key in the derivation binds each ciphertext to the sender and recipient identities, in addition to the sender's ephemeral key. This prevents ciphertexts from being replayed across senders, recipients, or sessions with the same ephemeral public key and context.

This change does not make decryption-failure blame publicly verifiable by itself. A third party cannot compute or verify Diffie-Hellman shared secrets from public keys alone without an additional proof mechanism. The full set of options for an application that needs public resolution of decryption disputes is documented in Differences from ChillDKG and is divided there into two sub-properties (ciphertext binding vs public decryption verification); the static-static ECDH binding introduced in this section only prevents ciphertext replay across senders/recipients/sessions, which is a complementary property to either binding or decryption verification.

2. Extended H6 Function §

The H6 key derivation function now includes the sender's static public key PsP_s in addition to the recipient's public key PrP_r: H6(x,E,Ps,Pr,extra)H6(x, E, P_s, P_r, extra) instead of H6(x,E,Pr,extra)H6(x, E, P_r, extra).

3. Optional Application Payloads §

COCKTAIL-DKG allows optional application-defined data to be included in the encrypted share ciphertexts, which ChillDKG does not support.

4. Multi-Ciphersuite Scope (No Taproot Tweak) §

ChillDKG is specified only over secp256k1 and additionally specifies a Taproot-safe BIP-341 key tweak that ensures the resulting group public key YY is safe to use as a Bitcoin Taproot output (preventing a malicious participant from embedding a hidden Taproot script-path commitment).

COCKTAIL-DKG covers seven ciphersuites (most of which are not cryptocurrency-specific and therefore have no Taproot analogue) and therefore does not specify a built-in Taproot tweak.

Applications using COCKTAIL(secp256k1, SHA-256) as the input to a Bitcoin Taproot deployment MUST apply the externally-applied mitigation defined in the Bitcoin Taproot Warning in the COCKTAIL(secp256k1, SHA-256) ciphersuite definition; that warning is the single normative source for the mitigation requirement.

5. Ciphertexts Not Bound by the Transcript §

ChillDKG's CertEq transcript binds all participants' encrypted shares so that the success certificate cryptographically attests to the exact ciphertexts each participant received, which in turn makes decryption failure blame publicly verifiable from the certificate alone.

COCKTAIL-DKG's CertEq transcript binds only the ciphersuite identifier, context, parameters, static public keys, VSS commitments, PoPs, ephemeral public keys, and application-specific extension; but not the ciphertexts. This is a deliberate trade-off in favour of a smaller, simpler transcript and a smaller success certificate. The cost is that decryption failures are not publicly verifiable from the certificate alone, because the AEAD verification key kj,ik_{j,i} is derived from recipient ii's static private key did_i and is therefore not public.

Decryption-failure attribution has two distinct sub-properties:

The options below split along this axis. Applications that need ciphertext binding only (i.e., that are willing to trust the recipient's self-attested decryption outcome once the ciphertext is binding) MUST select option 1 or option 2. Applications that additionally need independent public verification of the decryption outcome itself MUST combine option 3 with option 1 or option 2. Option 3 is not, by itself, sufficient because it proves the decryption outcome for some claimed ciphertext but does not pin down which ciphertext sender jj actually addressed to recipient ii.

Note that "fully-non-interactive certificate-only" adjudication (i.e., adjudication using only the signed success certificate, with no additional out-of-band material) is only achievable via the combination option 1 + option 3; option 2 + option 3 also yields third-party-verifiable blame, but requires an out-of-band republished msg1|jmsg_{1|j} and its application-layer authentication material in addition to the certificate.

The three options also differ in when they apply. Option 1 binds ciphertexts via the success certificate, so it is only available after a successful DKG run (e.g., for recovery-time disputes when an archived ciphertext fails decryption). Options 2 and 3 are usable both during an active session (a Round 2 abort where no success certificate exists yet) and after a successful run; for active-session disputes, option 1 is not available and applications MUST rely on option 2, option 3, or some application-level transport authentication described separately.

  1. Commit to ciphertexts via the application-specific extension (ciphertext binding only; success-path disputes only). Place a binding commitment to all msg1|jmsg_{1|j} ciphertexts (e.g., a hash of the participant-ordered concatenation of the framed ciphertexts) in the application-specific extension bytes that feed the transcript. The success certificate then binds the ciphertexts via the signed transcript; an after-the-fact dispute can reference the certificate to fix the ciphertext under dispute. This requires the DKG to have completed successfully (a certificate to exist) and does not by itself let a third party verify the decryption outcome; for that, combine with option 3 or accept the recipient's self-attestation.

  2. Preserve the original Round 1 messages out-of-band (ciphertext binding only; requires an application-level transport-authentication layer). Each participant durably stores their own msg1|jmsg_{1|j} outside the protocol's required artifacts. A framed participant can later republish their original message. The COCKTAIL-DKG PoP signs only context || C_j || E_j and does NOT sign the ciphertexts, so the bare republished msg1|jmsg_{1|j} plus the transcript's public material is insufficient to bind the ciphertexts to sender jj: a third party cannot distinguish the original ciphertexts from substituted ones on PoP/transcript evidence alone.

    To make option 2 sound, the application MUST layer some independent authentication over the msg1|jmsg_{1|j} bytes.

    • For example, a sender-signed transport envelope that signs the full message (including ciphertexts), a per-participant signature over the framed ciphertext bundle, or a broadcast layer whose receipts are bound to specific bytes. With such a layer in place, this establishes which ciphertext was sent; without it, option 2 is not sufficient.
  3. Define a separate dispute-evidence channel (public decryption-outcome verification; ciphertext binding must come from Option 1 or Option 2). Define an application-level mechanism that produces a non-interactive proof that recipient ii's claimed Round 2 decryption outcome for cj,ic_{j,i} is correct.

    This option attests only to the decryption outcome; it does not by itself attest to which ciphertext bytes were originally sent. Option 3 therefore MUST be combined with Option 1 or Option 2 to produce a complete, third-party-verifiable decryption-failure blame chain (Option 1/2 fixes the disputed ciphertext bytes; Option 3 proves what happens when those bytes are AEAD-processed under the recipient's private key). Option 3 is scoped specifically to Round 2 decryption-related aborts (AEAD failure, non-canonical plaintext, VSS mismatch).

    The remaining Round 2 abort conditions fall into two buckets:

    • Transcript-verifiable aborts: invalid commitment length and invalid PoP. Both CjC_j and PoPjPoP_j are part of the transcript and are therefore bound by the CertEq signatures (when a transcript exists) or directly observable from the broadcast (during an active session); a third party can independently check |Cj|t|C_j| \neq t or that PoPjPoP_j does not verify against Cj,0C_{j,0} over contextCjEjcontext \parallel C_j \parallel E_j, using only public data. These aborts do not require Option 3.
    • Ciphertext-dependent aborts: framing-format failures (a malformed length prefix on a framed ciphertext) and the decryption-related aborts covered by Option 3. Because the ciphertexts themselves are not bound by the transcript, these aborts are not publicly verifiable from the transcript and Round 1 messages alone; public blame for either kind requires Option 1 (extension commitment) or Option 2 (out-of-band republication with application-level authentication) to first establish which ciphertext bytes are under dispute. With ciphertext bytes thereby fixed, a framing-format failure is locally checkable by any third party from the bytes themselves; a decryption-related failure additionally requires Option 3 for public verification of the decryption outcome.

    The Round 2 final self-consistency check (xiBYix_i \cdot B \neq Y_i) is mathematically guaranteed to pass when the VSS share verification has passed. So it is a defense-in-depth check against implementation bugs rather than a public-blame scenario, and is therefore not addressed by any of the three options above.

    The proof system is application-defined, but the public statement it MUST establish is precisely as follows:

    • Public inputs (visible to the verifier): the ciphersuite identifier; the framed ciphertext cj,i~\widetilde{c_{j,i}} that is the subject of the dispute; the sender's ephemeral public key EjE_j; the sender's static public key PjP_j; the recipient's static public key PiP_i; the recipient's participant index ii; the sender's full VSS commitment Cj=(Cj,0,,Cj,t1)C_j = (C_{j,0}, \ldots, C_{j,t-1}); the session context; and a tag identifying which of the three Round 2 decryption-related failure modes is being proved (AEAD-fail / non-canonical-plaintext / VSS-mismatch).

      The sender index jj, the recipient index ii, the framed ciphertext cj,i~\widetilde{c_{j,i}}, and the failure-mode tag are never recoverable from TT alone (they identify what is being disputed), so they MUST always appear as explicit public inputs. When the proof is additionally bound to a published success certificate signed under transcript TT, the remaining inputs (EjE_j, PjP_j, CjC_j from TT at the j-th positions in the ordered participant lists; PiP_i from TT at the i-th position; and context from the ciphersuite_id-prefixed context field at the head of TT) are recoverable from (T,j,i)(T, j, i) and the proof MAY take (T,j,i,cj,i~,tag)(T, j, i, \widetilde{c_{j,i}}, \text{tag}) as a more compact public input and require the verifier to extract those values from TT before evaluating the relation.

    • Private witness (held by the recipient): the recipient's static private key did_i.

    • Relation proved: the witness did_i satisfies diB=Pid_i \cdot B = P_i (witness-binding to the named recipient key; without this, an arbitrary scalar could produce a fake AEAD outcome for some other recipient's key), and deriving Se_j,i=diEjS^{\mathrm{e}}\_{j,i} = d_i \cdot E_j and Sd_j,i=diPjS^{\mathrm{d}}\_{j,i} = d_i \cdot P_j, encoding them per ECDH Shared-Secret Encoding, and computing the AEAD key and nonce via H6()H6(\cdot) with the ciphersuite's prefixH6prefix_{H6}, EjE_j, PjP_j, PiP_i, and context, produces an AEAD outcome on the inner cj,ic_{j,i} consistent with the claimed failure-mode tag:

      • AEAD-fail tag: AEAD authentication of cj,ic_{j,i} fails.
      • Non-canonical-plaintext tag: AEAD authentication succeeds, but the recovered plaintext is shorter than the ciphersuite's scalar encoding size, or its leading scalar-sized portion does not decode to a canonical scalar in [0,q1][0, q-1] per Primitive Types.
      • VSS-mismatch tag: AEAD authentication succeeds, the leading portion decodes to a canonical scalar ss, and sBk=0t1ikCj,ks \cdot B \neq \sum_{k=0}^{t-1} i^k \cdot C_{j,k}.

    These three tags correspond exactly to the three Round 2 abort conditions for decryption-related failures, so any honest Round 2 decryption-failure abort can be backed by a proof under this option. The verifier learns the failure-mode tag and the identity of the framed ciphertext, but learns no information about did_i beyond what is already implied by PiP_i. The specific zero-knowledge proof system (e.g., a Bulletproofs- or PLONK-style circuit over AEAD verification, or a sigma-protocol-based DH+AEAD proof) is out of scope for this document; any sound, zero-knowledge, non-interactive argument for the statement above is sufficient.

    When combined with Option 1 (extension commitment binds cj,i~\widetilde{c_{j,i}} via the success certificate) or Option 2 (republished original msg1|jmsg_{1|j} supplies cj,i~\widetilde{c_{j,i}} from an application-authenticated binding source), Option 3 produces complete, third-party-verifiable decryption-failure blame without requiring further participant cooperation. The combined evidence is certificate-only only when paired with Option 1, because Option 1 binds the disputed ciphertext via the signed transcript itself; the Option 2 + Option 3 combination produces blame that is third-party-verifiable but not certificate-only; it additionally requires the out-of-band msg1|jmsg_{1|j} and its application- layer authentication material to be made available to the verifier. Option 3 alone, without 1 or 2, proves the decryption outcome for some claimed ciphertext but does not bind the disputed ciphertext to a particular sender's Round 1 message and is therefore insufficient on its own for end-to-end public blame.

6. Recovery Requires Both Common Data and a Per-Participant Encrypted Share Bundle §

In ChillDKG, recovery is possible from the participant's static secret key plus the common recovery data (transcript and certificate) alone.

In COCKTAIL-DKG, recovery additionally requires a per-participant encrypted share bundle CirecC^{rec}_i (the ordered, length-framed Round 1 ciphertexts addressed to the recovering participant). See Backup Requirements for the full backup set.

Share Recovery §

COCKTAIL-DKG inherits a key feature from ChillDKG: the ability to recover DKG outputs from minimal backup material. This eliminates the need for participants to store session-specific secrets and simplifies backup procedures.

Recovery Data §

The common recovery data for a successful DKG session consists of:

  1. Transcript (TT): The canonical byte representation of the protocol transcript, as constructed in Round 3.
  2. Success Certificate: The collection of all nn signatures on the transcript: sig1,sig2,,signsig_1, sig_2, \ldots, sig_n.

This common recovery data is identical for all participants and contains no confidential information. It can be safely:

Recovering participant ii additionally needs their participant-specific encrypted share bundle:

Cirec=c1,i~c2,i~cn,i~C^{rec}_i = \widetilde{c_{1,i}} \parallel \widetilde{c_{2,i}} \parallel \cdots \parallel \widetilde{c_{n,i}}

where each cj,i~\widetilde{c_{j,i}} is the length-prefixed framed Round 1 ciphertext from participant jj to participant ii (length encoded as a 64-bit big-endian unsigned integer, as defined under msg1|imsg_{1|i} above), in participant order. This bundle is not identical for all participants. The bundle is encrypted, but implementations should treat it as sensitive backup material because compromise of did_i plus CirecC^{rec}_i allows decryption of participant ii's historical shares.

Backup Requirements §

A complete backup for any participant ii consists of three components:

  1. Static Secret Key (did_i): The participant's long-term static private key.
  2. Common Recovery Data: The transcript and success certificate from each DKG session the participant joined.
  3. Encrypted Share Bundle (CirecC^{rec}_i): The ordered, length-framed ciphertexts c1,i~,,cn,i~\widetilde{c_{1,i}}, \ldots, \widetilde{c_{n,i}} (each prefixed with its 64-bit big-endian length, as defined in Protocol Messages under msg1|imsg_{1|i}) for each DKG session.

If an application does not store CirecC^{rec}_i directly in the participant's backup, it MUST ensure the exact ciphertexts can be retrieved later from durable coordinator or application storage. Recovery is impossible if the participant loses both their local encrypted share bundle and every durable copy of those ciphertexts.

This is a significant simplification compared to traditional DKG backup schemes, which typically require storing session-specific secrets for each key generation ceremony.

Recovery Algorithm §

Given the static secret key did_i, the common recovery data, and the participant-specific encrypted share bundle, a participant can deterministically reconstruct all DKG outputs:

Input:

Output:

Steps:

  1. Extract Parameters: Parse the transcript to obtain:
    • The ciphersuite identifier string. Implementations MUST verify that the ciphersuite identifier matches the one expected by the recovery routine; mismatch indicates the wrong recovery codepath and MUST abort. This step occurs before signature verification because the signature scheme (Schnorr Verify, HashToScalar, point/scalar encodings, etc.) is ciphersuite-dependent.
    • The context string and its length
    • The number of participants nn and threshold tt
    • All static public keys P1,,PnP_1, \ldots, P_n
    • All VSS commitments C1,,CnC_1, \ldots, C_n
    • All Proofs of Possession PoP1,,PoPnPoP_1, \ldots, PoP_n
    • All ephemeral public keys E1,,EnE_1, \ldots, E_n
    • The application-specific extension (if any)
  2. Validate Certificate: Using the ciphersuite parsed above, verify each signature sigjsig_j on the transcript TT against the public key PjP_j extracted from the transcript using the Schnorr Verify algorithm. If any signature is invalid, abort with an error.
  3. Determine Participant Index: Find the unique index ii such that Pi=diBP_i = d_i * B. If zero or more than one matching index exists, abort.
  4. Reconstruct Encryption Keys: For each participant jj from 11 to nn:
    1. Compute the ECDH shared secrets:
      • Se_j,i=diEjS^{\mathrm{e}}\_{j,i} = d_i * E_j (using the sender's ephemeral key)
      • Sd_j,i=diPjS^{\mathrm{d}}\_{j,i} = d_i * P_j (using the sender's static key)
    2. Derive the symmetric key kj,ik_{j,i} and nonce ivj,iiv_{j,i} using H6, with the same sender/recipient ordering used in Round 2.
  5. Load Ciphertexts: Parse Crec_iC^{rec}\_i as the concatenation of exactly nn length-framed ciphertexts: for each jj from 1 to nn, read the next 8 bytes as a 64-bit big-endian unsigned integer LjL_j, then read the next LjL_j bytes as the AEAD ciphertext c_j,ic\_{j,i}. Abort if any of the following hold: the bundle terminates before all nn ciphertexts are recovered; any LjL_j exceeds the implementation's maximum-ciphertext-size policy; any c_j,ic\_{j,i} is shorter than the ciphersuite's minimum ciphertext size (scalar encoding size plus AEAD authentication tag size); or there are any bytes remaining in Crec_iC^{rec}\_i after all nn framed ciphertext has been read. The recovery bundle MUST parse exactly, with no trailing data.
  6. Decrypt and Verify Shares: For each participant jj, decrypt cj,ic_{j,i} using kj,ik_{j,i} and ivj,iiv_{j,i} to obtain plaintextj,iplaintext_{j,i}. If AEAD decryption fails, abort. If plaintextj,iplaintext_{j,i} is shorter than the ciphersuite's scalar encoding size, or if the leading scalar-sized portion does not decode to a canonical scalar in [0,q1][0, q-1], abort. Set sj,is_{j,i} to the decoded leading scalar. Verify each share against the VSS commitment: sj,iB=k=0t1ikCj,ks_{j,i} * B = \sum_{k=0}^{t-1} i^k \cdot C_{j,k}. If the equation does not hold for any jj, the recovery procedure MUST abort with an error. Successful recovery requires every share to verify against the transcript-bound VSS commitment, mirroring the Round 2 share-verification MUST.
  7. Compute Final Share: xi=j=1nsj,ix_i = \sum_{j=1}^{n} s_{j,i}.
  8. Compute Public Outputs:
    • Group public key: Y=j=1nCj,0Y = \sum_{j=1}^{n} C_{j,0}.
    • Public verification shares: For each participant mm, Ym=j=1nk=0t1mkCj,kY_m = \sum_{j=1}^{n} \sum_{k=0}^{t-1} m^k \cdot C_{j,k}.

Privacy Considerations §

Users should be aware that common recovery data reveals:

The participant-specific encrypted share bundle additionally reveals ciphertext sizes, and ciphertext sizes may reveal application payload sizes. If the participant's static secret key is later compromised, the encrypted share bundle can be decrypted to recover that participant's historical shares and any encrypted payloads.

This information may create correlations between participants and their threshold setup. For privacy-sensitive applications, the recovery material SHOULD be encrypted before storage with untrusted providers. A recommended approach is to derive an encryption key from the static secret key using the ciphersuite's hash function:

kbackup=H("COCKTAIL-BACKUP-KEY"di)k_{backup} = H(\text{"COCKTAIL-BACKUP-KEY"} \parallel d_i)

Security Considerations for Recovery §

  1. Timing of Critical Key Use: Participants MUST NOT rely on the group public key YY for critical operations (operations whose loss-of-access consequences are catastrophic; e.g., long-lived asset custody, irreversible signatures, anchored production deployments) until they have confirmed that all participants possess the common recovery data and their own participant-specific encrypted share bundles. A catastrophic scenario could otherwise occur: one participant deems the session successful and begins using the threshold key for critical operations, but they are the only one with complete recovery material. If that participant's storage fails, the key becomes unrecoverable and any data or operations depending on it are lost. Non-critical use (e.g., low-stakes test signatures, internal protocol-correctness exchanges, or short-lived ceremonies that can be replayed) is allowed before confirmation; see item 3.

  2. Explicit Confirmations: Before using a threshold public key for critical operations, applications SHOULD obtain explicit confirmations from all participants that they have successfully stored the common recovery data and their own encrypted share bundles.

  3. Recovery Without Confirmations: If a participant receives complete recovery material after a session but cannot verify other participants' completion, they MAY still recover their outputs for non-critical future signing participation. However, they MUST NOT rely on that threshold key for critical operations until all participants have confirmed storage.

Alternatives §

Appendix A: Pseudocode §

This appendix provides a series of algorithms that describe the COCKTAIL-DKG protocol in a high-level, implementation-agnostic manner. The notation is meant to be illustrative rather than strictly formal.

Implementation note: All scalar arithmetic in the pseudocode below (polynomial evaluation, share summation, index powers, etc.) is performed in 𝔽q\mathbb{F}_q. The pseudocode does not always show explicit mod q for brevity; implementations using arbitrary-precision integer types MUST reduce modulo qq before encoding any scalar to bytes, per the Scalar primitive type definition.

Algorithm 1: Polynomial Generation and VSS Commitment §

Input:

Output:

Steps:

  1. Initialize an empty polynomial f.
  2. Initialize an empty list of commitments C.
  3. For k from 0 to t-1:
    1. Generate a random scalar a_k in the range [0, q-1]. If a_k == 0 (negligible probability), resample.
    2. Add the term a_k * x^k to the polynomial f.
    3. Compute the commitment C_k = a_k * B.
    4. Append C_k to the list C.
  4. Return (f, C).

Pseudocode:

function GeneratePolynomial(t, G, q):
    f = new Polynomial()
    C = new List<Point>()

    for k from 0 to t-1:
        a_k = RandomScalar(q)
        while a_k == 0:                  # negligible probability; ensures C_k != identity
            a_k = RandomScalar(q)
        f.add_coefficient(a_k)

        C_k = a_k * B
        C.append(C_k)

    return (f, C)

Algorithm 2: Secret Share Evaluation §

Input:

Output:

Steps:

  1. Evaluate the polynomial f at the point x = j.
  2. Let the result be s_j = f(j).
  3. Return s_j.

Pseudocode:

function EvaluatePolynomial(f, j):
    result = 0
    # iterate from highest degree to lowest
    for a in reverse(f.coefficients):
        result = result * j + a
    return result

Algorithm 3: Secret Share Verification §

Input:

Output:

Steps:

  1. Compute the public verification point from the share: V = s_j * B.
  2. Compute the expected verification point from the commitments:
    1. Initialize an identity point R.
    2. For k from 0 to t-1:
      1. Compute term = (j^k) * C_k.
      2. Add term to R: R = R + term.
  3. Compare the points: valid = (V == R).
  4. Return valid.

Pseudocode:

function VerifyShare(s_ji, i, C_j, G, t):
    # V = s_{j,i} * B
    V = s_ji * B

    # R = sum_{k=0}^{t-1} (i^k * C_{j,k})
    R = IdentityPoint()
    for k from 0 to t-1:
        i_k = power(i, k)
        C_jk = C_j[k]
        term = i_k * C_jk
        R = R + term

    return V == R

Helper: Key and Nonce Derivation §

This helper abstracts the ciphersuite-dependent key derivation logic used in Rounds 1 and 2.

function DeriveKeyAndNonce(cs, ecdh_secret, E, P_sender, P_recipient, context):
    if cs.HashFunction.OutputSizeInBytes() >= 56:
        tmp = H6(ecdh_secret, E, P_sender, P_recipient, context)
        return (tmp[0:32], tmp[32:56])
    else:
        key = H6(ecdh_secret, E, P_sender, P_recipient, derive_extra(context, "key"))
        nonce = H6(ecdh_secret, E, P_sender, P_recipient, derive_extra(context, "nonce"))[0:24]
        return (key, nonce)

Algorithm 4: COCKTAIL-DKG Round 1 (Participant i§

Input:

Output:

Steps:

  1. Generate Polynomial and VSS Commitment:
    1. (f_i, C_i) = Algorithm1(t, cs.Group.G, cs.Group.q)
  2. Generate Ephemeral Key:
    1. e_i = cs.Group.RandomScalar(); while e_i == 0: e_i = cs.Group.RandomScalar() # negligible probability
    2. E_i = e_i * cs.Group.G
  3. Compute Proof of Possession (PoP):
    1. Let a_i_0 be the constant term of f_i.
    2. Let C_i_0 be the first commitment in C_i.
    3. PoP_i = Sign(private_key=a_i_0, message=context || C_i || E_i) (using the Schnorr scheme; the public key is C_i_0; C_i is encoded as the concatenation of the t compressed point encodings, identical to its encoding in msg1_i).
  4. Compute and Encrypt Shares:
    1. Initialize an empty list encrypted_shares.
    2. For j from 1 to n:
      1. s_i_j = Algorithm2(f_i, j).
      2. Derive ECDH key using both ephemeral and static secrets. Each scalar-mult result is encoded as a ciphersuite-specific fixed-length byte string per ECDH Shared-Secret Encoding:
        • ecdh_ephemeral = ecdh_encode(e_i * P_j)
        • ecdh_static = ecdh_encode(d_i * P_j)
        • ecdh_secret = ecdh_ephemeral || ecdh_static
        • If the hash function used has an output size fewer than 56 bytes, use two domain-separated H6 calls:
          • key = H6(ecdh_secret, E_i, P_i, P_j, derive_extra(context, "key"))
          • nonce = H6(ecdh_secret, E_i, P_i, P_j, derive_extra(context, "nonce"))[0:24]
        • If the hash function used has an output size greater than or equal to 56 bytes, just split it:
          • tmp = H6(ecdh_secret, E_i, P_i, P_j, context)
          • key = tmp[0:32] (32 bytes)
          • nonce = tmp[32:56] (24 bytes)
      3. Prepare plaintext with optional payload:
        • payload_i_j = payloads[j] if j in payloads else empty bytes
        • plaintext = s_i_j || payload_i_j
      4. Encrypt plaintext:
        • c_i_j = Encrypt(key, nonce, plaintext)
      5. Wire-frame the ciphertext as framed_c_i_j = uint64_be(len(c_i_j)) || c_i_j, then append framed_c_i_j to encrypted_shares (the list stores the on-wire framed bytes, not raw ciphertexts).
  5. Construct Message:
    1. msg1_i = (C_i, PoP_i, E_i, encrypted_shares)
  6. Store State:
    1. internal_state = (f_i, e_i)
  7. Return (msg1_i, internal_state).

Pseudocode:

function Round1(i, t, n, cs, context, d_i, P_i, AllPublicKeys, payloads={}):
    (f_i, C_i) = GeneratePolynomial(t, cs.Group.G, cs.Group.q)
    a_i0 = f_i.coefficient(0)
    e_i = cs.Group.RandomScalar()
    while e_i == 0:                       # negligible probability; ensures E_i != identity
        e_i = cs.Group.RandomScalar()
    E_i = e_i * cs.Group.G
    PoP_i = Sign(private_key=a_i0, message=context || C_i || E_i)

    encrypted_shares = new List<FramedCiphertext>()
    for j from 1 to n:
        s_ij = EvaluatePolynomial(f_i, j)
        P_j = AllPublicKeys[j]
        # ecdh_secret = canonical_encode(e_i * P_j) || canonical_encode(d_i * P_j),
        # per "ECDH Shared-Secret Encoding"; each side is a fixed-length byte string for the ciphersuite.
        ecdh_secret = ecdh_encode(cs, e_i * P_j) || ecdh_encode(cs, d_i * P_j)
        (key, nonce) = DeriveKeyAndNonce(cs, ecdh_secret, E_i, P_i, P_j, context)
        payload_ij = payloads.get(j, empty_bytes)
        c_ij = Encrypt(key, nonce, s_ij || payload_ij)
        # Wire-frame with a 64-bit big-endian length prefix; encrypted_shares stores framed bytes.
        framed_c_ij = uint64_be(len(c_ij)) || c_ij
        encrypted_shares.append(framed_c_ij)

    msg1_i = new Round1Message(C_i, PoP_i, E_i, encrypted_shares)
    internal_state = new State(f_i, e_i)
    return (msg1_i, internal_state)

Algorithm 5: COCKTAIL-DKG Round 2 (Participant i§

Input:

Output:

Steps:

  1. Initialize an empty list received_shares.
  2. Initialize an empty map received_payloads.
  3. Process messages from each participant j:
    1. For j from 1 to n:
      1. Parse (C_j, PoP_j, E_j, encrypted_shares_j) from msg1_j.
      2. Verify PoP:
        • valid_pop = Verify(public_key=C_j_0, signature=PoP_j, message=context || C_j || E_j)
        • If valid_pop is false, abort and blame participant j.
      3. Decrypt Plaintext:
        • Take encrypted_shares_j[i] as the framed ciphertext c_j,i~=uint64be(L_j,i)c_j,i\widetilde{c\_{j,i}} = \mathrm{uint64\\_be}(L\_{j,i}) \parallel c\_{j,i}. Unwrap with bounds checks: if len(c_j,i~)<8len(\widetilde{c\_{j,i}}) < 8, or L_j,iL\_{j,i} exceeds the implementation's maximum-ciphertext-size policy, or len(c_j,i~)8+L_j,ilen(\widetilde{c\_{j,i}}) \neq 8 + L\_{j,i}, or L_j,i<𝚜𝚌𝚊𝚕𝚊𝚛𝚜𝚒𝚣𝚎+𝙰𝙴𝙰𝙳𝚃𝙰𝙶𝚂𝙸𝚉𝙴L\_{j,i} < \mathtt{scalar\\_size} + \mathtt{AEAD\\_TAG\\_SIZE}, abort and blame participant jj. Let c_j,ic\_{j,i} be the unwrapped AEAD ciphertext.
        • Let P_j = AllPublicKeys[j].
        • Derive ECDH key using both ephemeral and static secrets. Each scalar-mult result is encoded as a ciphersuite-specific fixed-length byte string per ECDH Shared-Secret Encoding:
          • ecdh_ephemeral = ecdh_encode(d_i * E_j)
          • ecdh_static = ecdh_encode(d_i * P_j)
          • ecdh_secret = ecdh_ephemeral || ecdh_static
          • If the hash function used has an output size fewer than 56 bytes, use two domain-separated H6 calls:
            • key = H6(ecdh_secret, E_j, P_j, P_i, derive_extra(context, "key"))
            • nonce = H6(ecdh_secret, E_j, P_j, P_i, derive_extra(context, "nonce"))[0:24]
          • If the hash function used has an output size greater than or equal to 56 bytes, just split it:
            • tmp = H6(ecdh_secret, E_j, P_j, P_i, context)
            • key = tmp[0:32] (32 bytes)
            • nonce = tmp[32:56] (24 bytes)
        • Decrypt:
          • plaintext = Decrypt(key, nonce, ciphertext=c_j_i)
          • If decryption fails, abort and report a decryption failure for participant j's ciphertext.
      4. Parse Plaintext:
        • If len(plaintext) < scalar_size, abort and blame participant j.
        • s_j_i = decode_scalar(plaintext[0:scalar_size]); if decoding fails (i.e., the bytes do not represent a canonical scalar in [0, q-1]), abort and blame participant j.
        • payload_j_i = plaintext[scalar_size:] (any remaining bytes are the optional payload)
        • received_payloads[j] = payload_j_i
      5. Verify Share:
        • valid_share = Algorithm3(s_j_i, i, C_j, cs.Group.G, t)
        • If valid_share is false, abort and blame participant j.
      6. Add s_j_i to received_shares.
  4. Compute Final Keys:
    1. x_i = sum(received_shares) (scalar addition).
    2. Y = sum(C_j_0 for all j) (point addition).
    3. Compute Verification Share Y_i:
      1. Initialize Y_i to the identity point.
      2. For k from 0 to t-1:
        • C_agg_k = sum(C_j_k for all j) (point addition).
        • term = (i^k) * C_agg_k
        • Y_i = Y_i + term
    4. Final Check:
      • If x_i * cs.Group.G != Y_i, abort (protocol failure).
  5. Prepare for Round 3:
    1. transcript_data = (cs.id, all_msg1s, context, all_static_public_keys), where cs.id is the ciphersuite identifier string (e.g., "COCKTAIL(Ristretto255, SHA-512)").
  6. Return (x_i, Y, Y_i, received_payloads, transcript_data).

Pseudocode:

function Round2(i, all_msg1s, internal_state, d_i, P_i, AllPublicKeys, cs, context):
    received_shares = new List<Scalar>()
    received_payloads = new Map<int, bytes>()

    for j from 1 to n:
        msg1_j = all_msg1s[j]
        C_j, PoP_j, E_j = msg1_j.C, msg1_j.PoP, msg1_j.E
        P_j = AllPublicKeys[j]

        if len(C_j) != t:
            abort("Invalid commitment length", j)
        if not Verify(public_key=C_j[0], signature=PoP_j, message=context || C_j || E_j):
            abort("Invalid PoP", j)

        # encrypted_shares[i] is the framed bytes uint64_be(len(c_ji)) || c_ji; unwrap with full bounds checks
        # before any slicing.
        framed_c_ji = msg1_j.encrypted_shares[i]
        if len(framed_c_ji) < 8:
            abort("Framed ciphertext shorter than 8-byte length prefix", j)
        L_ji = uint64_be_decode(framed_c_ji[0:8])
        if L_ji > MAX_CIPHERTEXT_SIZE:
            abort("Framed ciphertext length exceeds policy maximum", j)
        if len(framed_c_ji) != 8 + L_ji:
            abort("Framed ciphertext length mismatch (trailing bytes or short read)", j)
        c_ji = framed_c_ji[8:8 + L_ji]
        if len(c_ji) < scalar_size + AEAD_TAG_SIZE:
            abort("Ciphertext below minimum size", j)
        ecdh_secret = ecdh_encode(cs, d_i * E_j) || ecdh_encode(cs, d_i * P_j)
        (key, nonce) = DeriveKeyAndNonce(cs, ecdh_secret, E_j, P_j, P_i, context)
        plaintext = Decrypt(key, nonce, ciphertext=c_ji)
        if plaintext is null:
            abort("Decryption failed for participant j's ciphertext", j)

        if len(plaintext) < scalar_size:
            abort("Plaintext shorter than scalar size", j)
        s_ji = decode_scalar(plaintext[0:scalar_size])
        if s_ji is null:
            abort("Invalid scalar encoding", j)
        received_payloads[j] = plaintext[scalar_size:]

        if not VerifyShare(s_ji, i, C_j, cs.Group.G, t):
            abort("Invalid share", j)
        received_shares.append(s_ji)

    x_i = sum(received_shares)
    Y = sum(all_msg1s[j].C[0] for j in 1..n)
    Y_i = IdentityPoint()
    for k from 0 to t-1:
        C_agg_k = sum(all_msg1s[j].C[k] for j in 1..n)
        Y_i = Y_i + (power(i, k) * C_agg_k)

    if (x_i * cs.Group.G) != Y_i:
        abort("Final check failed")

    transcript_data = new TranscriptData(cs.id, all_msg1s, context, AllPublicKeys)
    return (x_i, Y, Y_i, received_payloads, transcript_data)

Algorithm 6: COCKTAIL-DKG Round 3 (Participant i§

Input:

Output:

Steps:

  1. Construct Transcript:
    1. T = CanonicalEncode(transcript_data, extension)
  2. Sign Transcript:
    1. sig_i = Sign(private_key=d_i, message=T)
  3. Broadcast and Receive Signatures:
    1. Send sig_i to the coordinator.
    2. Receive all_signatures from the coordinator.
  4. Verify All Signatures:
    1. For j from 1 to n:
      1. Let sig_j be the signature from participant j.
      2. valid_sig = Verify(public_key=P_j, signature=sig_j, message=T)
      3. If valid_sig is false, abort and blame participant j.
  5. Success:
    1. success = true
  6. Return success.

Pseudocode:

function Round3(i, d_i, AllPublicKeys, transcript_data, extension=empty_bytes):
    T = CanonicalEncode(transcript_data, extension)
    sig_i = Sign(private_key=d_i, message=T)
    all_signatures = broadcast_and_receive(sig_i)

    for j from 1 to n:
        if not Verify(public_key=AllPublicKeys[j], signature=all_signatures[j], message=T):
            abort("Invalid transcript signature", j)

    return true


function CanonicalEncode(transcript_data, extension):
    T = empty_bytes
    T = T || uint64_le(len(transcript_data.ciphersuite_id))
    T = T || transcript_data.ciphersuite_id
    T = T || uint64_le(len(transcript_data.context))
    T = T || transcript_data.context
    T = T || uint32_le(transcript_data.n)
    T = T || uint32_le(transcript_data.t)
    for j from 1 to n:
        T = T || transcript_data.static_public_keys[j]
    for j from 1 to n:
        T = T || transcript_data.vss_commitments[j]
    for j from 1 to n:
        T = T || transcript_data.pop_signatures[j]
    for j from 1 to n:
        T = T || transcript_data.ephemeral_public_keys[j]
    T = T || uint64_le(len(extension))
    T = T || extension
    return T

Appendix B: Test Vectors §

This section provides test vectors for various threshold configurations (2-of-3, 3-of-5, and 7-of-14) across all supported ciphersuites. The vectors were generated deterministically using a seed derived from the authors' names:

seed = SHA256("Daniel Bourdrez,Soatok Dreamseeker,Tjaden Hess")
     = b171b6992cc6db1f40b18dd8b1361d642f013e4b1208a735259a516af60dcb68

Transparent Derivation Scheme §

All secret values are derived using a labeled hash with ciphersuite and threshold domain separation:

derived_bytes = H(seed || ciphersuite_id || uint32_le(t) || uint32_le(n) || label)

Note: This labeled-hash construction is a deterministic test-vector seed expansion only. It is distinct from the protocol's HashToScalarHashToScalar (used in the Schnorr scheme), which performs a near-uniform reduction with bias bounded by 2128\approx 2^{-128} (see Schnorr Hash-to-Scalar Reduction). The test vectors use direct mod qq reduction here purely for reproducibility; production deployments MUST NOT reuse this routine for protocol-level scalar derivation.

Where:

Labels §

Value Label Format Example
Static secret key for participant i static_secret_key_{i} static_secret_key_1
Round 1 RNG stream for participant i round1_participant_{i} round1_participant_1
Payload for participant i payload_{i} payload_1

Scalar Reduction §

This is the seed-expansion-only reduction used to derive test-vector secret scalars from the labeled-hash output above; it is distinct from the protocol's HashToScalar (see Schnorr Hash-to-Scalar Reduction) and exists only to make the test vectors reproducible:

This allows any developer to independently reproduce the test vectors by:

  1. Computing H(seed || ciphersuite_id || uint32_le(t) || uint32_le(n) || label) for each value
  2. Reducing the hash output to a scalar using the appropriate method

Context String Format §

To satisfy all three Setup context MUSTs (session uniqueness, ordered participant binding, and verbatim ciphersuite_id binding; see Setup), the test vectors use a context construction that includes the test-vector tag, the canonical ciphersuite_id byte string, and the seed-derived participant public keys:

session_tag      = "COCKTAIL-DKG-TEST-VECTOR-{t}-OF-{n}"
context          = H( "COCKTAIL-DKG-CONTEXT"
                   || uint64_be(len(session_tag))   || session_tag
                   || uint64_be(len(ciphersuite_id)) || ciphersuite_id
                   || uint32_le(n)
                   || P_1 || P_2 || ... || P_n )

where HH is the ciphersuite's hash function and ciphersuite_id matches the test-vector header (e.g., "COCKTAIL(Ristretto255, SHA-512)"). When the test vectors are regenerated, each entry MUST record the bare session_tag (as a session_tag field) alongside the derived context byte string (as a context field or equivalent), so that implementations can independently reconstruct the context from HH, session_tag, ciphersuite_id, nn, and the PjP_j list, and verify byte-for-byte that their result matches the published context. This is the same recommended construction described under Setup; test vectors do not use a separate context format from production deployments.

Test Vector Types §

Each ciphersuite includes the following test vector types:

  1. Basic vectors (2-of-3, 3-of-5, 7-of-14): Standard DKG execution with empty extension.

  2. Payload extension vector (2-of-3): Each participant includes a seed-derived payload with their encrypted shares. The extension is computed as a hash of the participant-ordered payloads:

    ext = H(uint64_le(n) || uint64_le(len(payload_1)) || payload_1 || ... || uint64_le(len(payload_n)) || payload_n)
    
  3. Recovery vectors: The 2-of-3 configuration of each ciphersuite includes recovery test data for participant 1 (the 3-of-5 and 7-of-14 configurations omit recovery data). The participant-specific encrypted share bundle contains:

    • For each participant jj from 1 to nn, the raw AEAD ciphertext c_j,1c\_{j,1} from participant jj to participant 1, listed in the table rows below as Ciphertext from P{j} and shown in bare (unframed) hex form. To assemble the on-wire bundle Crec_1C^{rec}\_1 from these table rows, implementations concatenate the framed forms c_j,1~=uint64be(𝑙𝑒𝑛(c_j,1))c_j,1\widetilde{c\_{j,1}} = \mathrm{uint64\\_be}(\mathit{len}(c\_{j,1})) \parallel c\_{j,1} in participant order, as defined in Protocol Messages under msg_1|imsg\_{1|i}. The table omits the length prefixes because (a) every ciphertext in these basic test vectors is the same fixed length (scalar encoding size plus AEAD authentication tag size, e.g. 48 bytes), so the prefixes carry no test-discriminating information, and (b) listing bare ciphertexts keeps the vectors aligned with the analogous JSON files in CCTV.
    • The expected recovered secret share x1x_1.
    • The expected recovered verification share Y1Y_1.

    These values enable implementers to verify their recovery implementation produces correct outputs when decrypting the framed bundle assembled from the listed ciphertexts using participant 1's static secret key and the transcript from the DKG session.

Vector Data §

The full, byte-for-byte authoritative test vector data is published as JSON files in CCTV, with one file per ciphersuite:

Ciphersuite CCTV file
COCKTAIL(Ed25519, SHA-512) cocktail-dkg-ed25519-sha512.json
COCKTAIL(Ristretto255, SHA-512) cocktail-dkg-ristretto255-sha512.json
COCKTAIL(Ed448, SHAKE256) cocktail-dkg-ed448-shake256.json
COCKTAIL(P-256, SHA-256) cocktail-dkg-p256-sha256.json
COCKTAIL(secp256k1, SHA-256) cocktail-dkg-secp256k1-sha256.json
COCKTAIL(JubJub, BLAKE2b-512) cocktail-dkg-jubjub-blake2b512.json
COCKTAIL(Pallas, BLAKE2b-512) cocktail-dkg-pallas-blake2b512.json

Each file contains the 2-of-3, 3-of-5, and 7-of-14 threshold configurations plus the 2-of-3 payload-extension variant; each entry records session_tag, derived context, static keys, Round 1 outputs (ephemeral public keys, VSS commitments, PoPs, encrypted shares), Round 2 outputs (secret share, verification share), Round 3 transcript hash, the final group public key, and (for the 2-of-3 configuration) recovery vectors with the ordered AEAD ciphertexts addressed to participant 1. Implementations MUST reproduce these JSON files byte-for-byte from the deterministic derivation procedure defined above.

The inline byte tables that previously appeared in this section are intentionally elided in favor of CCTV as the single source of truth; the JSON format is mechanically parseable, less prone to copy-paste drift, and versioned alongside other C2SP test vectors.