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 $n$ parties such that any $t$ of the $n$ parties can cooperate to generate a digital signature for the group public key.

Initializing a $t$ of $n$ 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 $n$ participants to securely generate a shared group public key and individual secret shares for a $t$ of $n$ 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 $G$ 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. $msg_{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 $c_{i,i}$ addressed to participant $i$ 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 $n$ participants, and ensures every entry of the recipient-indexed bundle $C^{rec}_i$ is non-empty.

The full message is the concatenation of these elements:

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 $\widetilde{c_{i,j}}$ is the length-prefixed ciphertext defined above. $C_i$, $PoP_i$, and $E_i$ are fixed-length per the ciphersuite and are concatenated without additional framing.

2. $msg2$ (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 $i$ is structured as:

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 $\widetilde{c_{j,i}}$ is the length-prefixed framed ciphertext defined under $msg_{1|i}$ above. Concrete encodings MUST preserve the participant ordering; $C_j$, $PoP_j$, and $E_j$ are fixed-length per the ciphersuite, and each $\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 $msg_{1|j}$ messages to every participant.

3. $sig_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:

aggregated_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 $i$ is assumed to have:

Round 1: Commitment and Encryption §

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

    The coefficients $a_{i,k}$ are chosen as random scalars via $RandomScalar()$. $a_{i,0}$ is the participant's primary secret share. If any sampled coefficient equals $0$ (probability $\approx 2^{-\lceil \log_2 q \rceil}$ per coefficient, negligible in practice), implementations MUST resample that coefficient before continuing, so that every $C_{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 $i$ computes a VSS commitment $C_i$ to their polynomial by creating a public commitment for each coefficient: $C_i = (C_{i,0}, C_{i,1}, \cdots, C_{i,t-1})$, where $C_{i,k} = a_{i,k} * B$.

  3. Generate Ephemeral Key: Participant $i$ generates a fresh ephemeral key pair $(e_i, E_i)$ for this session with $e_i \leftarrow RandomScalar()$ and $E_i = e_i \cdot B$. If $e_i = 0$ (negligible probability), implementations MUST resample, so that $E_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 $i$ computes a digital signature $PoP_i$ over a concatenation of the context string, their VSS commitment $C_i$, and their ephemeral public key $E_i$. The signature is created using the secret $a_{i,0}$ as the private key and $C_{i,0}$ as the public key. The message to be signed is context || C_i || E_i, where $C_i$ is encoded as the byte concatenation $C_{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 $msg_{1|i}$). The signature algorithm is the Schnorr scheme defined in Schnorr Signature Scheme. If Sign aborts at its step 3 due to $k = 0$ (probability $\approx 2^{-\lceil \log_2 q \rceil}$, negligible), participant $i$ MUST treat this DKG session as locally failed: it MUST NOT send $msg_{1|i}$, MUST NOT publish any partially-derived value, and MUST abandon this session. To retry, participant $i$ joins a fresh DKG session with a different context string (which changes the Sign message $m$ and therefore the deterministic nonce $k$); simply resampling $a_{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 $i$ failed to deliver $msg_{1|i}$ within the agreed timeout.

  5. Compute and Encrypt Shares: For each participant $j$ from $1$ to $n$ (including $j = i$, the self-share):

    1. Compute Share: Participant $i$ computes the secret share $s_{i,j} = f_i(j)$.
    2. Derive Key: Participant $i$ 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: $S^{\mathrm{e}}_{i,j} = e_i * P_j$ and $S^{\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(S^{\mathrm{e}}_{i,j} \parallel S^{\mathrm{d}}_{i,j}, E_i, P_i, P_j, context)$.
        • $k_{i,j} = tmp[0:32]$
        • $iv_{i,j} = tmp[32:56]$
      • Otherwise:
        • $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"}))$
        • $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 $i$ prepares the plaintext to encrypt. This consists of the secret share $s_{i,j}$ followed by an optional application-defined payload $payload_{i,j}$: $plaintext_{i,j} = s_{i,j} \parallel payload_{i,j}$. If no application payload is used, $payload_{i,j}$ is empty.
    4. Encrypt Share: Participant $i$ encrypts the plaintext for participant $j$: $c_{i,j} = Enc(plaintext_{i,j}, k_{i,j}, iv_{i,j})$.
  6. Broadcast: Participant $i$ sends their $msg_{1|i}$ to the coordinator.

Round 2: Share Decryption and Verification §

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

  1. Validate Commitments: For each participant $j$ from $1$ to $n$:
    • Participant $i$ verifies that the VSS commitment $C_j$ contains exactly $t$ points. If $|C_j| \neq t$, participant $i$ MUST abort, identifying participant $j$ as malicious. This check prevents threshold elevation attacks.
  2. Verify All PoPs: For each participant $j$ from $1$ to $n$:
    • Participant $i$ verifies the proof of possession $PoP_j$ using the Schnorr Verify algorithm. The signature is checked against the message context || C_j || E_j (with $C_j$ encoded as in Round 1), using participant $j$'s public commitment $C_{j,0}$ as the public key.
    • If any $PoP_j$ is invalid, participant $i$ MUST abort, identifying participant $j$ as malicious.
  3. Decrypt and Verify Shares: For each participant $j$ from $1$ to $n$ (including the self-share $j = i$):
    1. Derive Key: Participant $i$ 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: $S^{\mathrm{e}}_{j,i} = d_i * E_j$ and $S^{\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(S^{\mathrm{e}}_{j,i} \parallel S^{\mathrm{d}}_{j,i}, E_j, P_j, P_i, context)$.
        • $k_{j,i} = tmp[0:32]$
        • $iv_{j,i} = tmp[32:56]$
      • Otherwise:
        • $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"}))$
        • $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 $i$ decrypts the ciphertext sent to them from participant $j$: $plaintext_{j,i} = Dec(c_{j,i}, k_{j,i}, iv_{j,i})$. If decryption fails, participant $i$ MUST abort and report a decryption failure for the ciphertext attributed to participant $j$.
    3. Parse Plaintext: Participant $i$ parses the plaintext to extract the secret share $s_{j,i}$ (the first scalar-sized portion) and any optional application payload $payload_{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, q-1]$, participant $i$ MUST abort, identifying $j$ as malicious.
    4. Verify Share: Participant $i$ verifies the decrypted share $s_{j,i}$ against $j$'s VSS commitment: $s_{j,i} * B = \sum_{k=0}^{t-1} i^k * C_{j,k}$ If the check fails, participant $i$ MUST abort, identifying $j$ as malicious.
  4. Compute Final Keys: If all shares are successfully decrypted and verified:
    1. Secret Share: Participant $i$ computes their final long-lived secret share by summing all received shares: $x_i = \sum_{j=1}^{n} s_{j,i}$.
    2. Group Public Key: Participant $i$ computes the group public key: $Y = \sum_{j=1}^n C_{j,0}$.
    3. Verification Share: Participant $i$ computes their public verification share $Y_i$. This is done by first computing the aggregated commitment for each coefficient $k$: $C_{agg,k} = \sum_{j=1}^{n} C_{j,k}$. Then, $Y_i = \sum_{k=0}^{t-1} i^k * C_{agg,k}$.
    4. Final Check: Participant $i$ performs a final self-consistency check: $x_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 $i$ constructs a canonical byte representation of the final public transcript, $T$. The transcript MUST be constructed by concatenating the following elements in this exact order:

    1. $\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)$: The length of the context string as a little-endian 64-bit unsigned integer.
    4. $context$: The context string bytes.
    5. $n$: The number of participants as a little-endian 32-bit unsigned integer.
    6. $t$: The threshold as a little-endian 32-bit unsigned integer.
    7. For $j$ from $1$ to $n$: $P_j$ (the static public key of participant $j$, in its standard encoding).
    8. For $j$ from $1$ to $n$: $C_j$ (the full VSS commitment of participant $j$: $C_{j,0} \parallel \cdots \parallel C_{j,t-1}$).
    9. For $j$ from $1$ to $n$: $PoP_j$ (the Proof of Possession signature from participant $j$).
    10. For $j$ from $1$ to $n$: $E_j$ (the ephemeral public key from participant $j$).
    11. $len(ext)$: The length of the application-specific extension as a little-endian 64-bit unsigned integer.
    12. $ext$: 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 $i$ signs the transcript $T$ with their long-term static private key $d_i$ using the Schnorr Sign algorithm, producing a signature $sig_i$. If Sign aborts at its step 3 due to $k = 0$ (negligible probability), participant $i$ 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 $T$ is identical across all honest participants, restarting CertEq within the same DKG session would reproduce the same $(d_i, T)$ inputs to Sign and therefore the same $k = 0$ result; the only safe retry path is a fresh DKG session with a different context (which yields a different $T$).

  3. Broadcast Signature: Participant $i$ sends $sig_i$ to the coordinator.

  4. Verify Certificate: The coordinator broadcasts the set of all signatures ${sig_1, \cdots, sig_n}$ to everyone. Each participant $i$ verifies every signature $sig_j$ on the transcript $T$ against the public key $P_j$ using the Schnorr Verify algorithm.

  5. Success: If all signatures are valid, the DKG is successful. The participant stores their secret share $x_i$ and the group public key $Y$. The collection of $T$ and all $n$ 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 $j$ from $1$ to $n$, collect their payload contributions. To produce a consistent extension across all participants, the application MUST ensure every recipient observes the same $payload_j$ from participant $j$ (e.g., by having $j$ broadcast the same payload to every recipient).

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

  2. Compute the extension as: $ext = H(n \parallel len(payload_1) \parallel payload_1 \parallel \cdots \parallel len(payload_n) \parallel payload_n)$ Where $H$ is the ciphersuite's hash function (the same $H$ used by $H6$), $n$ is encoded as a little-endian 64-bit unsigned integer, and each $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 $msg_{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 ($PoP_j$).
      • A VSS share verification failure ($s_{j,i} \cdot B \ne \sum_{k=0}^{t-1} i^k \cdot C_{j,k}$).
      • A failed decryption of an encrypted share ($c_{j,i}$).
      • An invalid signature on the final transcript ($sig_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, P_s, P_r, extra)$, an encryption method $Enc(plain, key, iv)$, and a decryption method $Dec(cipher, key, iv)$. Ciphersuites MUST use an AEAD mode for $Enc()$ and $Dec()$. The protocol's confidentiality, ciphertext-integrity, blame-finding, and optional-payload authentication properties all rely on AEAD; a non-AEAD $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 $H6$ (see Round 1: Commitment and Encryption); implementations MUST NOT sample nonces randomly. The 24-byte (192-bit) nonce width was chosen so that the $H6$-derived nonces have a negligible probability of collision across distinct $(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 $H6$ output.

For ciphersuites based on SHA-256, where the output is smaller than 56 bytes, we use two domain-separated $H6()$ invocations. For the key, $H6$ is called with $\mathrm{derive\_extra}(context, \text{"key"})$ and the full 32-byte output is used. For the nonce, $H6$ is called with $\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 $H6$ function is used to derive a symmetric key and nonce from ECDH shared secrets. Unless otherwise specified, it is defined as:

$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 $H6$ is used to derive the key and nonce for the AEAD.

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

$$ \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 $label$ is the ASCII string "key" or "nonce" (without quotes).

ECDH Shared-Secret Encoding §

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

Ciphersuite ECDH encoding of $S = s \cdot P$ Size
COCKTAIL(Ed25519, SHA-512) Canonical Ristretto255 encoding of $S$ (RFC 9496 §4.3.2) 32 B
COCKTAIL(Ristretto255, SHA-512) Canonical Ristretto255 encoding of $S$ (RFC 9496 §4.3.2) 32 B
COCKTAIL(Ed448, SHAKE256) RFC 8032 compressed Ed448 encoding of $S$ 57 B
COCKTAIL(P-256, SHA-256) SEC1 compressed point encoding of $S$ 33 B
COCKTAIL(secp256k1, SHA-256) SEC1 compressed point encoding of $S$ 33 B
COCKTAIL(JubJub, BLAKE2b-512) ZIP-0312 SerializeElement of $S$ (32-byte little-endian, y-parity) 32 B
COCKTAIL(Pallas, BLAKE2b-512) Halo2/Orchard compressed encoding of $S$ (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 = 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 $H6$, $H7$, and nonce prefixes.

For Ed448 ciphersuites (COCKTAIL(Ed448, SHAKE256)), the ECDH shared secret is the 57-byte RFC 8032 compressed Ed448 encoding of $S = s \cdot P$. Because all decoded protocol points are required to be non-identity prime-order subgroup points, $S$ 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 $S$ into $H6$.

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

For Zcash-family ciphersuites (COCKTAIL(JubJub, BLAKE2b-512), COCKTAIL(Pallas, BLAKE2b-512)), the ECDH shared secret is the standard Zcash compressed encoding of $S$, 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 $S$ when $S$ is exchanged outside this protocol. Within COCKTAIL-DKG itself, each side computes its own $S$ 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 $T$, 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 $T$ (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 $R$ followed by the scalar encoding of $z$. 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 $sk$ (a scalar) and a message $m$ (a byte string):

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

Where:

Schnorr Verify Algorithm §

Given a signature $(R, z)$, a public key $pk$, and a message $m$:

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

Schnorr Prefix Strings §

The ciphersuite-specific values for $prefix_{H7}$ and $prefix_{nonce}$ are:

Ciphersuite $prefix_{H7}$ $prefix_{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 = 0$ rejection rule from Sign step 3 also applies (and aborts under the same conditions):

Where $taggedHash(tag, msg) = SHA256(SHA256(tag) \parallel SHA256(tag) \parallel msg)$ as defined in BIP-340, $tag$ is encoded as its UTF-8 byte representation, $encode(sk)$ is the 32-byte big-endian secp256k1 scalar encoding, and $OS2IP$ 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 $2^{-128}$ (specifically $\approx 1.27 \cdot 2^{-128}$) for secp256k1 because $q \approx 2^{256} - 2^{128}$.

Schnorr Hash-to-Scalar Reduction §

$HashToScalar$ takes a byte string $input$ and reduces it to a scalar in $[0, q-1]$. The construction depends on the ciphersuite's hash output length and on the ratio between $2^{8L}$ (where $L$ is the hash output byte-length) and $q$:

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] * q$ for some small value $h$ and large prime $q$. If an attacker can select a point $P$ of order $h$ they can potentially leak partial information about the secret scalars from the output of $[s] P$. (They specifically learn $s \bmod h$).

Curves in these families we consider include ed25519 with $h = 8$, edwards448 with $h = 4$, and JubJub with $h = 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] \cdot P \stackrel{?}{=} O$) tests only that $P$ 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 $P_i$, ephemeral public keys $E_i$, VSS commitment points $C_{i,k}$, and Schnorr commitment points $R$) MUST be verified to lie in the prime-order subgroup of order $q$ by an explicit subgroup-membership check: compute $[q] \cdot P$ and abort if the result is not the identity element. The "cofactor clearing then check for identity" alternative ($[h] \cdot P \stackrel{?}{=} O$) tests only that $P$ 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 $c_{i,j}$ has the format: $plaintext_{i,j} = s_{i,j} \parallel payload_{i,j}$

Where $s_{i,j}$ is the fixed-size secret share (determined by the scalar size of the ciphersuite) and $payload_{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) \parallel payload)$

Where $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:

$S_{i,j} = e_i * P_j$

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

$S^{\mathrm{e}}_{i,j} = e_i * P_j$ and $S^{\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 $P_s$ in addition to the recipient's public key $P_r$: $H6(x, E, P_s, P_r, extra)$ instead of $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 $Y$ 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 $k_{j,i}$ is derived from recipient $i$'s static private key $d_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 $j$ actually addressed to recipient $i$.

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 $msg_{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 $msg_{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 $msg_{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 $msg_{1|j}$ plus the transcript's public material is insufficient to bind the ciphertexts to sender $j$: 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 $msg_{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 $i$'s claimed Round 2 decryption outcome for $c_{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 $C_j$ and $PoP_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 $|C_j| \neq t$ or that $PoP_j$ does not verify against $C_{j,0}$ over $context \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 ($x_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 $\widetilde{c_{j,i}}$ that is the subject of the dispute; the sender's ephemeral public key $E_j$; the sender's static public key $P_j$; the recipient's static public key $P_i$; the recipient's participant index $i$; the sender's full VSS commitment $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 $j$, the recipient index $i$, the framed ciphertext $\widetilde{c_{j,i}}$, and the failure-mode tag are never recoverable from $T$ 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 $T$, the remaining inputs ($E_j$, $P_j$, $C_j$ from $T$ at the j-th positions in the ordered participant lists; $P_i$ from $T$ at the i-th position; and context from the ciphersuite_id-prefixed context field at the head of $T$) are recoverable from $(T, j, i)$ and the proof MAY take $(T, j, i, \widetilde{c_{j,i}}, \text{tag})$ as a more compact public input and require the verifier to extract those values from $T$ before evaluating the relation.

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

    • Relation proved: the witness $d_i$ satisfies $d_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 $S^{\mathrm{e}}_{j,i} = d_i \cdot E_j$ and $S^{\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(\cdot)$ with the ciphersuite's $prefix_{H6}$, $E_j$, $P_j$, $P_i$, and context, produces an AEAD outcome on the inner $c_{j,i}$ consistent with the claimed failure-mode tag:

      • AEAD-fail tag: AEAD authentication of $c_{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, q-1]$ per Primitive Types.
      • VSS-mismatch tag: AEAD authentication succeeds, the leading portion decodes to a canonical scalar $s$, and $s \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 $d_i$ beyond what is already implied by $P_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 $\widetilde{c_{j,i}}$ via the success certificate) or Option 2 (republished original $msg_{1|j}$ supplies $\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 $msg_{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 $C^{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 ($T$): The canonical byte representation of the protocol transcript, as constructed in Round 3.
  2. Success Certificate: The collection of all $n$ signatures on the transcript: $sig_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 $i$ additionally needs their participant-specific encrypted share bundle:

C^{rec}_i = \widetilde{c_{1,i}} \parallel \widetilde{c_{2,i}} \parallel \cdots \parallel \widetilde{c_{n,i}}

where each $\widetilde{c_{j,i}}$ is the length-prefixed framed Round 1 ciphertext from participant $j$ to participant $i$ (length encoded as a 64-bit big-endian unsigned integer, as defined under $msg_{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 $d_i$ plus $C^{rec}_i$ allows decryption of participant $i$'s historical shares.

Backup Requirements §

A complete backup for any participant $i$ consists of three components:

  1. Static Secret Key ($d_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 ($C^{rec}_i$): The ordered, length-framed ciphertexts $\widetilde{c_{1,i}}, \ldots, \widetilde{c_{n,i}}$ (each prefixed with its 64-bit big-endian length, as defined in Protocol Messages under $msg_{1|i}$) for each DKG session.

If an application does not store $C^{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 $d_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 $n$ and threshold $t$
    • All static public keys $P_1, \ldots, P_n$
    • All VSS commitments $C_1, \ldots, C_n$
    • All Proofs of Possession $PoP_1, \ldots, PoP_n$
    • All ephemeral public keys $E_1, \ldots, E_n$
    • The application-specific extension (if any)
  2. Validate Certificate: Using the ciphersuite parsed above, verify each signature $sig_j$ on the transcript $T$ against the public key $P_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 $i$ such that $P_i = d_i * B$. If zero or more than one matching index exists, abort.
  4. Reconstruct Encryption Keys: For each participant $j$ from $1$ to $n$:
    1. Compute the ECDH shared secrets:
      • $S^{\mathrm{e}}_{j,i} = d_i * E_j$ (using the sender's ephemeral key)
      • $S^{\mathrm{d}}_{j,i} = d_i * P_j$ (using the sender's static key)
    2. Derive the symmetric key $k_{j,i}$ and nonce $iv_{j,i}$ using H6, with the same sender/recipient ordering used in Round 2.
  5. Load Ciphertexts: Parse $C^{rec}_i$ as the concatenation of exactly $n$ length-framed ciphertexts: for each $j$ from 1 to $n$, read the next 8 bytes as a 64-bit big-endian unsigned integer $L_j$, then read the next $L_j$ bytes as the AEAD ciphertext $c_{j,i}$. Abort if any of the following hold: the bundle terminates before all $n$ ciphertexts are recovered; any $L_j$ exceeds the implementation's maximum-ciphertext-size policy; any $c_{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 $C^{rec}_i$ after all $n$ framed ciphertext has been read. The recovery bundle MUST parse exactly, with no trailing data.
  6. Decrypt and Verify Shares: For each participant $j$, decrypt $c_{j,i}$ using $k_{j,i}$ and $iv_{j,i}$ to obtain $plaintext_{j,i}$. If AEAD decryption fails, abort. If $plaintext_{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, q-1]$, abort. Set $s_{j,i}$ to the decoded leading scalar. Verify each share against the VSS commitment: $s_{j,i} * B = \sum_{k=0}^{t-1} i^k \cdot C_{j,k}$. If the equation does not hold for any $j$, 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: $x_i = \sum_{j=1}^{n} s_{j,i}$.
  8. Compute Public Outputs:
    • Group public key: $Y = \sum_{j=1}^{n} C_{j,0}$.
    • Public verification shares: For each participant $m$, $Y_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:

$$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 $Y$ 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 $\mathbb{F}_q$. The pseudocode does not always show explicit mod q for brevity; implementations using arbitrary-precision integer types MUST reduce modulo $q$ 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 $\widetilde{c_{j,i}} = \mathrm{uint64\_be}(L_{j,i}) \parallel c_{j,i}$. Unwrap with bounds checks: if $len(\widetilde{c_{j,i}}) < 8$, or $L_{j,i}$ exceeds the implementation's maximum-ciphertext-size policy, or $len(\widetilde{c_{j,i}}) \neq 8 + L_{j,i}$, or $L_{j,i} < \mathtt{scalar\_size} + \mathtt{AEAD\_TAG\_SIZE}$, abort and blame participant $j$. Let $c_{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 $HashToScalar$ (used in the Schnorr scheme), which performs a near-uniform reduction with bias bounded by $\approx 2^{-128}$ (see Schnorr Hash-to-Scalar Reduction). The test vectors use direct mod $q$ 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 $H$ 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 $H$, session_tag, ciphersuite_id, $n$, and the $P_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 $j$ from 1 to $n$, the raw AEAD ciphertext $c_{j,1}$ from participant $j$ 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 $C^{rec}_1$ from these table rows, implementations concatenate the framed forms $\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|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 $x_1$.
    • The expected recovered verification share $Y_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.