BIP: 445
Title: FROST Signing Protocol for BIP340 Signatures
Authors: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
Status: Draft
Type: Specification
Assigned: 2026-01-30
License: CC0-1.0
Discussion: 2024-07-31: https://groups.google.com/g/bitcoindev/c/PeMp2HQl-H4/m/AcJtK0aKAwAJ
Requires: 340
This document proposes a standard for the Flexible Round-Optimized Schnorr Threshold (FROST) signing protocol. The standard is compatible with BIP340 public keys and signatures. It supports tweaking, which allows deriving BIP32 child keys from the threshold public key and creating BIP341 Taproot outputs with key and script paths.
This document is made available under CC0 1.0 Universal. The accompanying source code is licensed under the MIT license.
The FROST signature scheme enables threshold Schnorr signatures. In a t-of-n threshold configuration, any t1 participants can cooperatively produce a Schnorr signature that is indistinguishable from a signature produced by a single signer. FROST signatures are unforgeable as long as fewer than t participants are corrupted. The signing protocol remains functional provided that at least t honest participants retain access to their secret key shares.
The IRTF has published RFC 9591, which specifies the FROST signing protocol for several elliptic curve and hash function combinations, including secp256k1 with SHA-256, the cryptographic primitives used in Bitcoin. However, the signatures produced by RFC 9591 are incompatible with BIP340 Schnorr signatures due to the X-only public keys introduced in BIP340. Additionally, RFC 9591 does not specify key tweaking mechanisms, which are essential for Bitcoin applications such as BIP32 key derivation and BIP341 Taproot. This document addresses these limitations by specifying a BIP340-compatible variant of FROST signing protocol that supports key tweaking.
Following the initial publication of the FROST protocol[KG20], several optimized variants have been proposed to improve computational efficiency and bandwidth optimization: FROST2[CKM21], FROST2-BTZ[BTZ21], and FROST3[RRJSS, CGRS23]. Among these variants, FROST3 is the most efficient variant to date.
This document specifies the FROST3 variant2. The FROST3 signing protocol shares substantial similarities with the MuSig2 signing protocol specified in BIP327. Accordingly, this specification adopts several design principles from BIP327, including support for key tweaking, partial signature verification, and identifiable abort mechanisms. We note that significant portions of this document have been directly adapted from BIP327 due to the similarities in the signing protocols. Key generation for FROST signing is out of scope for this document.
The on-chain footprint of a FROST Taproot output is essentially a single BIP340 public key, and a transaction spending the output only requires a single signature cooperatively produced by at least t signers. This is more compact and has lower verification cost than each signer providing an individual public key and signature, as would be required by an t-of-n policy implemented using OP_CHECKSIGADD as introduced in BIP342.
As a side effect, the number n of signers is not limited by any consensus rules when using FROST.
Moreover, FROST offers a higher level of privacy than OP_CHECKSIGADD: FROST Taproot outputs are indistinguishable for a blockchain observer from regular, single-signer Taproot outputs even though they are actually controlled by multiple signers. By tweaking the threshold public key, the shared Taproot output can have script spending paths that are hidden unless used.
Implementers must make sure to understand this section thoroughly to avoid subtle mistakes that may lead to catastrophic failure.
The goal of this proposal is to support a wide range of possible application scenarios. Given a specific application scenario, some features may be unnecessary or not desirable, and implementers can choose not to support them. Such optional features include:
- Applying plain tweaks after x-only tweaks.
- Applying tweaks at all.
- Dealing with messages that are not exactly 32 bytes.
- Identifying a disruptive signer after aborting (aborting itself remains mandatory). If applicable, the corresponding algorithms should simply fail when encountering inputs unsupported by a particular implementation. (For example, the signing algorithm may fail when given a message which is not 32 bytes.) Similarly, the test vectors that exercise the unimplemented features should be re-interpreted to expect an error, or be skipped if appropriate.
A FROST key generation protocol configures a group of n participants with a threshold public key (representing a t-of-n threshold policy). The corresponding threshold secret key is Shamir secret-shared among all n participants, where each participant holds a distinct long-term secret share. This ensures that any subset of at least t participants can jointly run the FROST signing protocol to produce a signature under the threshold secret key.
Key generation for FROST signing is out of scope for this document. Implementations can use either a trusted dealer setup, as specified in Appendix C of RFC 9591, or a distributed key generation (DKG) protocol such as ChillDKG. The appropriate choice depends on the implementation's trust model and operational requirements.
This protocol distinguishes between two public key formats: plain public keys are 33-byte compressed public keys traditionally used in Bitcoin, while X-only public keys are 32-byte keys defined in BIP340. Key generation protocols produce public shares and threshold public keys in the plain format. During signing, we conditionally negate secret shares to ensure the resulting threshold-signature verifies under the corresponding X-only threshold public key.
Warning
Key generation protocols must commit the threshold public key to an unspendable script path as recommended in BIP341. This prevents a malicious party from embedding a hidden script path during key generation that would allow them to bypass the t-of-n threshold policy.
There are u (where t <= u <= n < 2^32) participants and one coordinator initiating the FROST signing protocol. Each participant has a point-to-point communication link to the coordinator (but participants do not have direct communication links to each other).
If there is no dedicated coordinator, one of the participants can act as the coordinator.
Each signing session requires two inputs: a participant's long-term secret share (individual to each participant, not shared with the coordinator) and a Signers Context3 data structure (common to all participants and the coordinator).
This signing protocol is compatible with any key generation protocol that produces valid FROST keys. Valid keys satisfy: (1) each secret share is a Shamir share of the threshold secret key, and (2) each public share equals the scalar multiplication secshare * G. Implementations may optionally validate key compatibility for a signing session using the ValidateSignersCtx function. For comprehensive validation of the entire key material, ValidateSignersCtx can be run on all possible u signing participants.
Important
Passing ValidateSignersCtx ensures functional compatibility with the signing protocol but does not guarantee the security of the key generation protocol itself.
The output of the FROST signing protocol is a BIP340 Schnorr signature that verifies under the threshold public key as if it were produced by a single signer using the threshold secret key.
The coordinator and signing participants must be determined before initiating the signing protocol. The signing participants information is stored in a Signers Context data structure. The threshold public key may optionally be tweaked by initializing a Tweak Context at this stage.
Whenever the signing participants want to sign a message, the basic order of operations to create a threshold-signature is as follows:
First broadcast round: Signers begin the signing session by running NonceGen to compute their secnonce and pubnonce.4 Each signer sends their pubnonce to the coordinator, who aggregates them using NonceAgg to produce an aggregate nonce and sends it back to all signers.
Second broadcast round: At this point, every signer has the required data to sign, which, in the algorithms specified below, is stored in a data structure called Session Context. Every signer computes a partial signature by running Sign with their long-term secret share, secnonce and the session context. Then, the signers broadcast their partial signatures to the coordinator, who runs PartialSigAgg to produce the final signature. If all parties behaved honestly, the result passes BIP340 verification.
A malicious coordinator can cause the signing session to fail but cannot compromise the unforgeability of the scheme. Even when colluding with up to t-1 signers, a malicious coordinator cannot forge a signature.
Tip
The Sign algorithm must not be executed twice with the same secnonce. Otherwise, it is possible to extract the secret signing key from the two partial signatures output by the two executions of Sign. To avoid accidental reuse of secnonce, an implementation may securely erase the secnonce argument by overwriting it with 64 zero bytes after it has been read by Sign. A secnonce consisting of only zero bytes is invalid for Sign and will cause it to fail.
To simplify the specification of the algorithms, some intermediary values are unnecessarily recomputed from scratch, e.g., when executing GetSessionValues multiple times. Actual implementations can cache these values. As a result, the Session Context may look very different in implementations or may not exist at all. However, computation of GetSessionValues and storage of the result must be protected against modification from an untrusted third party. This party would have complete control over the aggregate public key and message to be signed.
NonceGen must have access to a high-quality random generator to draw an unbiased, uniformly random value rand'. In contrast to BIP340 signing, the values k1 and k2 must not be derived deterministically from the session parameters because deriving nonces deterministically allows for a complete key-recovery attack in multi-party discrete logarithm-based signatures.
The optional arguments to NonceGen enable a defense-in-depth mechanism that may prevent secret share exposure if rand' is accidentally not drawn uniformly at random. If the value rand' was identical in two NonceGen invocations, but any other argument was different, the secnonce would still be guaranteed to be different as well (with overwhelming probability), and thus accidentally using the same secnonce for Sign in both sessions would be avoided. Therefore, it is recommended to provide the optional arguments secshare, pubshare, thresh_pk, and m if these session parameters are already determined during nonce generation. The auxiliary input extra_in can contain additional contextual data that has a chance of changing between NonceGen runs, e.g., a supposedly unique session id (taken from the application), a session counter wide enough not to repeat in practice, any nonces by other signers (if already known), or the serialization of a data structure containing multiple of the above. However, the protection provided by the optional arguments should only be viewed as a last resort. In most conceivable scenarios, the assumption that the arguments are different between two executions of NonceGen is relatively strong, particularly when facing an active adversary.
In some applications, the coordinator may enable preprocessing of nonce generation to reduce signing latency. Participants run NonceGen to generate a batch of pubnonce values before the message or Signers Context5 is known, which are stored with the coordinator (e.g., on a centralized server). During this preprocessing phase, only the available arguments are provided to NonceGen. When a signing session begins, the coordinator selects and aggregates pubnonces of the signing participants, enabling them to run Sign immediately once the message is determined. This way, the final signature is created quicker and with fewer round trips. However, applications that use this method presumably store the nonces for a longer time and must therefore be even more careful not to reuse them. Moreover, this method is not compatible with the defense-in-depth mechanism described in the previous paragraph.
FROST signers are typically stateful: they generate secnonce, store it, and later use it to produce a partial signature after receiving the aggregated nonce. However, stateless signing is possible when one signer receives the aggregate nonce of all OTHER signers before generating their own nonce. In coordinator-based setups, the coordinator facilitates this by collecting pubnonces from the other signers, computing their aggregate (aggothernonce), and providing it to the stateless signer. The stateless signer then runs NonceGen, NonceAgg, and Sign in sequence, sending its pubnonce and partial signature simultaneously to the coordinator, who computes the final aggregate nonce for all participants. In coordinator-less setups, any one signer can achieve stateless operation by generating their nonce after seeing all other signers' pubnonces. Stateless signers may want to consider signing deterministically (see Modifications to Nonce Generation) to remove the reliance on the random number generator in the NonceGen algorithm.
The signing protocol makes it possible to identify malicious signers who send invalid contributions to a signing session in order to make the signing session abort and prevent the honest signers from obtaining a valid signature. This property is called "identifiable aborts" and ensures that honest parties can assign blame to malicious signers who cause an abort in the signing protocol.
Aborts are identifiable for an honest party if the following conditions hold in a signing session:
- The contributions received from all signers have not been tampered with (e.g., because they were sent over authenticated connections).
- Nonce aggregation is performed honestly (e.g., because the honest signer performs nonce aggregation on its own or because the coordinator is trusted).
- The partial signatures received from all signers are verified using the algorithm PartialSigVerify.
If these conditions hold and an honest party (signer or coordinator) runs an algorithm that fails due to invalid protocol contributions from malicious signers, then the algorithm run by the honest party will output the index (within the input list) of exactly one malicious signer. Additionally, if the honest parties agree on the contributions sent by all signers in the signing session, all the honest parties who run the aborting algorithm will identify the same malicious signer.
Some of the algorithms specified below may also assign blame to a malicious coordinator. While this is possible for some particular misbehavior of the coordinator, it is not guaranteed that a malicious coordinator can be identified. More specifically, a malicious coordinator (whose existence violates the second condition above) can always make signing abort and wrongly hold honest signers accountable for the abort (e.g., by claiming to have received an invalid contribution from a particular honest signer).
The only purpose of the algorithm PartialSigVerify is to ensure identifiable aborts, and it is not necessary to use it when identifiable aborts are not desired. In particular, partial signatures are not signatures. An adversary can forge a partial signature, i.e., create a partial signature without knowing the secret share for that particular participant public share.6 However, if PartialSigVerify succeeds for all partial signatures then PartialSigAgg will return a valid Schnorr signature.
The threshold public key can be tweaked, which modifies the key as defined in the Tweaking Definition subsection. In order to apply a tweak, the Tweak Context output by TweakCtxInit is provided to the ApplyTweak algorithm with the is_xonly_t argument set to false for plain tweaking and true for X-only tweaking. The resulting Tweak Context can be used to apply another tweak with ApplyTweak or obtain the threshold public key with GetXonlyPubkey or GetPlainPubkey.
The purpose of supporting tweaking is to ensure compatibility with existing uses of tweaking, i.e., that the result of signing is a valid signature for the tweaked public key. The FROST signing algorithms take arbitrary tweaks as input but accepting arbitrary tweaks may negatively affect the security of the scheme.7 Instead, signers should obtain the tweaks according to other specifications. This typically involves deriving the tweaks from a hash of the threshold public key and some other information. Depending on the specific scheme that is used for tweaking, either the plain or the X-only threshold public key is required. For example, to do BIP32 derivation, you call GetPlainPubkey to be able to compute the tweak, whereas BIP341 TapTweaks require X-only public keys that are obtained with GetXonlyPubkey.
The tweak mode provided to ApplyTweak depends on the application: Plain tweaking can be used to derive child public keys from a threshold public key using BIP32. On the other hand, X-only tweaking is required for Taproot tweaking per BIP341. A Taproot-tweaked public key commits to a script path, allowing users to create transaction outputs that are spendable either with a FROST threshold-signature or by providing inputs that satisfy the script path. Script path spends require a control block that contains a parity bit for the tweaked X-only public key.
The bit can be obtained with GetPlainPubkey(tweak_ctx)[0] & 1.
The following specification of the algorithms has been written with a focus on clarity. As a result, the specified algorithms are not always optimal in terms of computation and space. In particular, some values are recomputed but can be cached in actual implementations (see General Signing Flow).
The algorithms are defined over the secp256k1 group and its associated scalar field. We note that adapting this proposal to other elliptic curves is not straightforward and can result in an insecure scheme.
We rely on the following types and conventions throughout this document:
- Types: Points on the curve are represented by the object GE, and scalars are represented by Scalar.
- Naming: Points are denoted using uppercase letters (e.g., P, Q), while scalars are denoted using lowercase letters (e.g., r, s).
- Mathematical Context: Points are group elements under elliptic curve addition. The group includes all points on the secp256k1 curve plus the point at infinity (the identity element).
- Arithmetic: The operators +, -, and · are overloaded depending on their operands:
- Scalar Arithmetic: When applied to two Scalar operands, +, -, and · denote integer addition, subtraction, and multiplication modulo the group order.
- Point Addition: When applied to two GE operands, + denotes the elliptic curve group addition operation.
- Scalar Multiplication: The notation r · P denotes scalar multiplication (the repeated addition of point P, r times).
The reference code vendors the secp256k1lab library to handle underlying arithmetic, serialization, deserialization, and auxiliary functions. To improve the readability of this specification, we utilize simplified notation aliases for the library's internal methods, as mapped below:
| Notation | secp256k1lab | Description |
|---|---|---|
| p | FE.SIZE | Field element size |
| ord | GE.ORDER, Scalar.SIZE | Group order |
| G | G | The secp256k1 generator point |
| inf_point | GE() | The infinity point |
| is_infinity(P) | P.infinity | Returns whether P is the point at infinity |
| x(P) | P.x | Returns the x-coordinate of a non-infinity point P, in the range [0, p−1] |
| y(P) | P.y | Returns the y-coordinate of a non-infinity point P, in the range [0, p-1] |
| has_even_y(P) | P.has_even_y() | Returns whether P has an even y-coordinate |
| with_even_y(P) | - | Returns the version of point P that has an even y-coordinate. If P already has an even y-coordinate (or is infinity), it is returned unchanged. Otherwise, its negation -P is returned |
| xbytes(P) | P.to_bytes_xonly() | Returns the 32-byte x-only serialization of a non-infinity point P |
| cbytes(P) | P.to_bytes_compressed() | Returns the 33-byte compressed serialization of a non-infinity point P |
| cbytes_ext(P) | P.to_bytes_compressed _with_infinity() |
Returns the 33-byte compressed serialization of a point P. If P is the point at infinity, it is encoded as a 33-byte array of zeros. |
| lift_x(x)8 | GE.lift_x(x) | Decodes a 32-byte x-only serialization x into a non-infinity point P. The resulting point always has an even y-coordinate. |
| cpoint(b) | GE.from_bytes_compressed(b) | Decodes a 33-byte compressed serialization b into a non-infinity point |
| cpoint_ext(b) | GE.from_bytes_compressed _with_infinity(b) |
Decodes a 33-byte compressed serialization b into a point. If b is a 33-byte array of zeros, it returns the point at infinity |
| scalar_to_bytes(s) | s.to_bytes() | Returns the 32-byte serialization of a scalar s |
| scalar_from_bytes_checked(b) | Scalar.from_bytes_checked(b) | Deserializes a 32-byte array b to a scalar, fails if the value is ≥ ord |
| scalar_from_bytes _nonzero_checked(b) |
Scalar.from_bytes _nonzero_checked(b) |
Deserializes a 32-byte array b to a scalar, fails if the value is zero or ≥ ord |
| scalar_from_bytes_wrapping(b) | Scalar.from_bytes_wrapping(b) | Deserializes a 32-byte array b to a scalar, reducing the value modulo ord |
| hashtag(x) | tagged_hash(x) | Computes a 32-byte domain-separated hash of the byte array x. The output is SHA256(SHA256(tag) || SHA256(tag) || x), where tag is UTF-8 encoded string unique to the context |
| random_bytes(n) | - | Returns n bytes, sampled uniformly at random using a cryptographically secure pseudorandom number generator (CSPRNG) |
| xor_bytes(a, b) | xor_bytes(a, b) | Returns byte-wise xor of a and b |
The following helper functions and notation are used for operations on standard integers and byte arrays, independent of curve arithmetic. Note that like Scalars, these variables are denoted by lowercase letters (e.g., x, n); the intended type is implied by context.
| Notation | Description |
|---|---|
| || | Refers to byte array concatenation |
| len(x) | Returns the length of the byte array x in bytes |
| x[i:j] | Returns the sub-array of the byte array x starting at index i (inclusive) and ending at j (exclusive). The result has length j - i |
| empty_bytestring | A constant representing an empty byte array where length is 0 |
| bytes(n, x) | Returns the big-endian n-byte encoding of the integer x |
| count(x, lst) | Returns the number of times the element x occurs in the list lst |
| has_duplicates(lst) | Returns True if any element in lst appears more than once, False otherwise |
| sorted(lst) | Returns a new list containing the elements of lst arranged in ascending order |
| (a, b, ...) | Refers to a tuple containing the listed elements |
Note
In the following algorithms, all scalar arithmetic is understood to be modulo the group order. For example, a · b implicitly means a · b mod order
The Signers Context is a data structure consisting of the following elements:
- The total number n of participants involved in key generation: an integer with 2 ≤ n < 232
- The threshold number t of participants required to issue a signature: an integer with 1 ≤ t ≤ n
- The number u of signing participants: an integer with t ≤ u ≤ n
- The list of participant identifiers id1..u: u distinct integers, each with 0 ≤ idi ≤ n - 1
- The list of participant public shares pubshare1..u: u 33-byte arrays, each a compressed serialized point
- The threshold public key thresh_pk: a 33-byte array, compressed serialized point
We write "Let (n, t, u, id1..u, pubshare1..u, thresh_pk) = signers_ctx" to assign names to the elements of Signers Context.
Algorithm ValidateSignersCtx(signers_ctx):
- Inputs:
- The signers_ctx: a Signers Context data structure
- (n, t, u, id1..u, pubshare1..u, thresh_pk) = signers_ctx
- Fail if not 1 ≤ t ≤ n
- Fail if not t ≤ u ≤ n
- For i = 1 .. u:
- Fail if not 0 ≤ idi ≤ n - 1
- Fail if cpoint(pubsharei) fails
- Fail if has_duplicates(id1..u)
- Fail if DeriveThreshPubkey(id1..u, pubshare1..u) ≠ thresh_pk
- No return
Internal Algorithm DeriveThreshPubkey(id1..u, pubshare1..u)9
- Q = inf_point
- For i = 1..u:
- P = cpoint(pubsharei); fail if that fails
- λ = DeriveInterpolatingValue(id1..u, idi)
- Q = Q + λ · P
- Return cbytes(Q)
Internal Algorithm DeriveInterpolatingValue(id1..u, my_id):
- Fail if my_id not in id1..u
- Fail if has_duplicates(id1..u)
- Let num = Scalar(1)
- Let deno = Scalar(1)
- For i = 1..u:
- If idi ≠ my_id:
- Let num = num · Scalar(idi + 1)10 (mod ord)
- Let deno = deno · Scalar(idi - my_id) (mod ord)
- If idi ≠ my_id:
- λ = num · deno-1 (mod ord)
- Return λ
The Tweak Context is a data structure consisting of the following elements:
- The point Q representing the potentially tweaked threshold public key: a GE
- The value gacc: Scalar(1) or Scalar(-1)
- The accumulated tweak tacc: a Scalar
We write "Let (Q, gacc, tacc) = tweak_ctx" to assign names to the elements of a Tweak Context.
Algorithm TweakCtxInit(thresh_pk):
- Input:
- The threshold public key thresh_pk: a 33-byte array, compressed serialized point
- Let Q = cpoint(thresh_pk); fail if that fails
- Fail if is_infinity(Q)
- Let gacc = Scalar(1)
- Let tacc = Scalar(0)
- Return tweak_ctx = (Q, gacc, tacc)
Algorithm GetXonlyPubkey(tweak_ctx):
- Inputs:
- The tweak_ctx: a Tweak Context data structure
- Let (Q, _, _) = tweak_ctx
- Return xbytes(Q)
Algorithm GetPlainPubkey(tweak_ctx):
- Inputs:
- The tweak_ctx: a Tweak Context data structure
- Let (Q, _, _) = tweak_ctx
- Return cbytes(Q)
Algorithm ApplyTweak(tweak_ctx, tweak, is_xonly_t):
- Inputs:
- The tweak_ctx: a Tweak Context data structure
- The tweak: a 32-byte array, serialized scalar
- The tweak mode is_xonly_t: a boolean
- Let (Q, gacc, tacc) = tweak_ctx
- If is_xonly_t and not has_even_y(Q):
- Let g = Scalar(-1)
- Else:
- Let g = Scalar(1)
- Let t = scalar_from_bytes_nonzero_checked(tweak); fail if that fails
- Let Q' = g · Q + t · G
- Fail if is_infinity(Q')
- Let gacc' = g · gacc (mod ord)
- Let tacc' = t + g · tacc (mod ord)
- Return tweak_ctx' = (Q', gacc', tacc')
Algorithm NonceGen(secshare, pubshare, thresh_pk, m, extra_in):
- Inputs:
- The participant secret signing share secshare: a 32-byte array, serialized scalar (optional argument)
- The participant public share pubshare: a 33-byte array, compressed serialized point (optional argument)
- The x-only threshold public key thresh_pk: a 32-byte array, X-only serialized point (optional argument)
- The message m: a byte array (optional argument)11
- The auxiliary input extra_in: a byte array with 0 ≤ len(extra_in) ≤ 232-1 (optional argument)
- Let rand' = random_bytes(32)
- If the optional argument secshare is present:
- Let rand = xor_bytes(secshare, hashFROST/aux(rand'))12
- Else:
- Let rand = rand'
- If the optional argument pubshare is not present:
- Let pubshare = empty_bytestring
- If the optional argument thresh_pk is not present:
- Let thresh_pk = empty_bytestring
- If the optional argument m is not present:
- Let m_prefixed = bytes(1, 0)
- Else:
- Let m_prefixed = bytes(1, 1) || bytes(8, len(m)) || m
- If the optional argument extra_in is not present:
- Let extra_in = empty_bytestring
- Let ki = scalar_from_bytes_wrapping(hashFROST/nonce(rand || bytes(1, len(pubshare)) || pubshare || bytes(1, len(thresh_pk)) || thresh_pk || m_prefixed || bytes(4, len(extra_in)) || extra_in || bytes(1, i - 1))) for i = 1,2
- Fail if k1 = Scalar(0) or k2 = Scalar(0)
- Let R*,1 = k1 · G, R*,2 = k2 · G
- Let pubnonce = cbytes(R*,1) || cbytes(R*,2)
- Let secnonce = scalar_to_bytes(k1) || scalar_to_bytes(k2)13
- Return (secnonce, pubnonce)
Algorithm NonceAgg(pubnonce1..u):
- Inputs:
- The number u of signing participants: an integer with t ≤ u ≤ n
- The list of participant public nonces pubnonce1..u: u 66-byte array, each an output of NonceGen
- For j = 1 .. 2:
- For i = 1 .. u:
- Let Ri,j = cpoint(pubnoncei[(j-1)*33:j*33]); fail if that fails and blame signer at index i for invalid pubnonce
- Let Rj = R1,j + R2,j + ... + Ru,j
- For i = 1 .. u:
- Return aggnonce = cbytes_ext(R1) || cbytes_ext(R2)
The Session Context is a data structure consisting of the following elements:
- The signers_ctx: a Signers Context data structure
- The aggregate public nonce aggnonce: a 66-byte array, output of NonceAgg
- The number v of tweaks with 0 ≤ v < 2^32
- The list of tweaks tweak1..v: v 32-byte arrays, each a serialized scalar
- The list of tweak modes is_xonly_t1..v : v booleans
- The message m: a byte array11
We write "Let (signers_ctx, aggnonce, v, tweak1..v, is_xonly_t1..v, m) = session_ctx" to assign names to the elements of a Session Context.
Algorithm GetSessionValues(session_ctx):
- Let (signers_ctx, aggnonce, v, tweak1..v, is_xonly_t1..v, m) = session_ctx
- ValidateSignersCtx(signers_ctx); fail if that fails
- Let (_, _, u, id1..u, pubshare1..u, thresh_pk) = signers_ctx
- Let tweak_ctx0 = TweakCtxInit(thresh_pk); fail if that fails
- For i = 1 .. v:
- Let tweak_ctxi = ApplyTweak(tweak_ctxi-1, tweaki, is_xonly_ti); fail if that fails
- Let (Q, gacc, tacc) = tweak_ctxv
- Let ser_ids = SerializeIds(id1..u)
- Let b = scalar_from_bytes_wrapping(hashFROST/noncecoef(ser_ids || aggnonce || xbytes(Q) || m))
- Fail if b = Scalar(0)
- Let R1 = cpoint_ext(aggnonce[0:33]), R2 = cpoint_ext(aggnonce[33:66]); fail if that fails and blame the coordinator for invalid aggnonce.
- Let R' = R1 + b · R2
- If is_infinity(R'):
- Let final nonce R = G (see Dealing with Infinity in Nonce Aggregation)
- Else:
- Let final nonce R = R'
- Let e = scalar_from_bytes_wrapping(hashBIP0340/challenge((xbytes(R) || xbytes(Q) || m)))
- Fail if e = Scalar(0)
- Return (Q, gacc, tacc, id1..u, pubshare1..u, b, R, e)
Internal Algorithm SerializeIds(id1..u):
- res = empty_bytestring
- For id in sorted(id1..u):
- res = res || bytes(4, id)
- Return res
Algorithm Sign(secnonce, secshare, my_id, session_ctx):
- Inputs:
- The secret nonce secnonce that has never been used as input to Sign before: a 64-byte array13
- The participant secret signing share secshare: a 32-byte array, serialized scalar
- The participant identifier my_id: an integer with 0 ≤ my_id ≤ n-1
- The session_ctx: a Session Context data structure
- Let (Q, gacc, _, id1..u, pubshare1..u, b, R, e) = GetSessionValues(session_ctx); fail if that fails
- Let k1' = scalar_from_bytes_nonzero_checked(secnonce[0:32]); fail if that fails
- Let k2' = scalar_from_bytes_nonzero_checked(secnonce[32:64]); fail if that fails
- Let k1 = k1', k2 = k2' if has_even_y(R), otherwise let k1 = -k1', k2 = -k2'
- Let d' = scalar_from_bytes_nonzero_checked(secshare); fail if that fails
- Let pubshare = cbytes(d' · G)
- Fail if pubshare not in pubshare1..u
- Fail if my_id not in id1..u
- Let λ = DeriveInterpolatingValue(id1..u, my_id); fail if that fails
- Let g = Scalar(1) if has_even_y(Q), otherwise let g = Scalar(-1)
- Let d = g · gacc · d' (mod ord) (See Negation of Secret Share When Signing)
- Let s = k1 + b · k2 + e · λ · d (mod ord)
- Let psig = scalar_to_bytes(s)
- Let pubnonce = cbytes(k1' · G) || cbytes(k2' · G)
- If PartialSigVerifyInternal(psig, my_id, pubnonce, pubshare, session_ctx) (see below) returns failure, fail14
- Return partial signature psig
Algorithm PartialSigVerify(psig, pubnonce1..u, signers_ctx, tweak1..v, is_xonly_t1..v, m, i):
- Inputs:
- The partial signature psig: a 32-byte array, serialized scalar
- The list of public nonces pubnonce1..u: u 66-byte arrays, each an output of NonceGen
- The signers_ctx: a Signers Context data structure
- The number v of tweaks with 0 ≤ v < 2^32
- The list of tweaks tweak1..v: v 32-byte arrays, each a serialized scalar
- The list of tweak modes is_xonly_t1..v : v booleans
- The message m: a byte array11
- The index i of the signer in the list of public nonces where 0 < i ≤ u
- ValidateSignersCtx(signers_ctx); fail if that fails
- Let (_, _, u, id1..u, pubshare1..u, _) = signers_ctx
- Let aggnonce = NonceAgg(pubnonce1..u); fail if that fails
- Let session_ctx = (signers_ctx, aggnonce, v, tweak1..v, is_xonly_t1..v, m)
- Run PartialSigVerifyInternal(psig, idi, pubnoncei, pubsharei, session_ctx)
- Return success iff no failure occurred before reaching this point.
Internal Algorithm PartialSigVerifyInternal(psig, my_id, pubnonce, pubshare, session_ctx):
- Let (Q, gacc, _, id1..u, pubshare1..u, b, R, e) = GetSessionValues(session_ctx); fail if that fails
- Let s = scalar_from_bytes_nonzero_checked(psig); fail if that fails
- Fail if pubshare not in pubshare1..u
- Fail if my_id not in id1..u
- Let R*,1 = cpoint(pubnonce[0:33]), R*,2 = cpoint(pubnonce[33:66])
- Let Re*' = R*,1 + b · R*,2
- Let effective nonce Re* = Re*' if has_even_y(R), otherwise let Re* = -Re*'
- Let P = cpoint(pubshare); fail if that fails
- Let λ = DeriveInterpolatingValue(id1..u, my_id)15
- Let g = Scalar(1) if has_even_y(Q), otherwise let g = Scalar(-1)
- Let g' = g · gacc (mod ord) (See Negation of Pubshare When Partially Verifying)
- Fail if s · G ≠ Re* + e · λ · g' · P
- Return success iff no failure occurred before reaching this point.
Algorithm PartialSigAgg(psig1..u, session_ctx):
- Inputs:
- The number u of signatures with t ≤ u ≤ n
- The list of partial signatures psig1..u: u 32-byte arrays, each an output of Sign
- The session_ctx: a Session Context data structure
- Let (Q, _, tacc, _, _, _, R, e) = GetSessionValues(session_ctx); fail if that fails
- For i = 1 .. u:
- Let si = scalar_from_bytes_nonzero_checked(psigi); fail if that fails and blame signer at index i for invalid partial signature.
- Let g = Scalar(1) if has_even_y(Q), otherwise let g = Scalar(-1)
- Let s = s1 + ... + su + e · g · tacc (mod ord)
- Return sig = xbytes(R) || scalar_to_bytes(s)
We provide a naive, highly inefficient, and non-constant time pure Python 3 reference implementation of the threshold public key tweaking, nonce generation, partial signing, and partial signature verification algorithms.
Standalone JSON test vectors are also available in the same directory, to facilitate porting the test vectors into other implementations.
Caution
The reference implementation is for demonstration purposes only and not to be used in production environments.
Implementers must avoid modifying the NonceGen algorithm without being fully aware of the implications. We provide two modifications to NonceGen that are secure when applied correctly and may be useful in special circumstances, summarized in the following table.
| needs secure randomness | needs secure counter | needs to keep state securely | needs aggregate nonce of all other signers (only possible for one signer) | |
|---|---|---|---|---|
| NonceGen | ✓ | ✓ | ||
| CounterNonceGen | ✓ | ✓ | ||
| DeterministicSign | ✓ |
First, on systems where obtaining uniformly random values is much harder than maintaining a global atomic counter, it can be beneficial to modify NonceGen. The resulting algorithm CounterNonceGen does not draw rand' uniformly at random but instead sets rand' to the value of an atomic counter that is incremented whenever it is read. With this modification, the secret share secshare of the signer generating the nonce is not an optional argument and must be provided to NonceGen. The security of the resulting scheme then depends on the requirement that reading the counter must never yield the same counter value in two NonceGen invocations with the same secshare.
Second, if there is a unique signer who generates their nonce last (i.e., after receiving the aggregate nonce from all other signers), it is possible to modify nonce generation for this single signer to not require high-quality randomness. Such a nonce generation algorithm DeterministicSign is specified below. Note that the only optional argument is rand, which can be omitted if randomness is entirely unavailable. DeterministicSign requires the argument aggothernonce which should be set to the output of NonceAgg run on the pubnonce value of all other signers (but can be provided by an untrusted party). Hence, using DeterministicSign is only possible for the last signer to generate a nonce and makes the signer stateless, similar to the stateless signer described in the Nonce Generation section.
Algorithm DeterministicSign(secshare, my_id, aggothernonce, signers_ctx, tweak1..v, is_xonly_t1..v, m, rand):
- Inputs:
- The participant secret signing share secshare: a 32-byte array, serialized scalar
- The participant identifier my_id: an integer with 0 ≤ my_id ≤ n-1
- The aggregate public nonce aggothernonce (see above): a 66-byte array, output of NonceAgg
- The signers_ctx: a Signers Context data structure
- The number v of tweaks with 0 ≤ v < 2^32
- The list of tweaks tweak1..v: v 32-byte arrays, each a serialized scalar
- The list of tweak methods is_xonly_t1..v: v booleans
- The message m: a byte array11
- The auxiliary randomness rand: a 32-byte array (optional argument)
- If the optional argument rand is present:
- Let secshare' = xor_bytes(secshare, hashFROST/aux(rand))
- Else:
- Let secshare' = secshare
- Let (_, _, u, id1..u, pubshare1..u, thresh_pk) = signers_ctx
- Let tweak_ctx0 = TweakCtxInit(thresh_pk); fail if that fails
- For i = 1 .. v:
- Let tweak_ctxi = ApplyTweak(tweak_ctxi-1, tweaki, is_xonly_ti); fail if that fails
- Let tweaked_tpk = GetXonlyPubkey(tweak_ctxv)
- Let ki = scalar_from_bytes_wrapping(hashFROST/deterministic/nonce(secshare' || aggothernonce || tweaked_tpk || bytes(8, len(m)) || m || bytes(1, i - 1))) for i = 1,2
- Fail if k1 = Scalar(0) or k2 = Scalar(0)
- Let R*,1 = k1 · G, R*,2 = k2 · G
- Let pubnonce = cbytes(R*,1) || cbytes(R*,2)
- Let d = scalar_from_bytes_nonzero_checked(secshare'); fail if that fails
- Let my_pubshare = cbytes(d · G)
- Fail if my_pubshare is not present in pubshare1..u
- Let secnonce = scalar_to_bytes(k1) || scalar_to_bytes(k2)
- Let aggnonce = NonceAgg((pubnonce, aggothernonce)); fail if that fails and blame coordinator for invalid aggothernonce.
- Let session_ctx = (signers_ctx, aggnonce, v, tweak1..v, is_xonly_t1..v, m)
- Return (pubnonce, Sign(secnonce, secshare, my_id, session_ctx))
Two modes of tweaking the threshold public key are supported. They correspond to the following algorithms:
Algorithm ApplyPlainTweak(P, t):
- Inputs:
- P: a point
- The tweak t: a scalar
- Return P + t · G
Algorithm ApplyXonlyTweak(P, t):
- Inputs:
- P: a point
- The tweak t: a scalar
- Return with_even_y(P) + t · G
Note
In the following equations, all scalar arithmetic is understood to be modulo the group order, as specified in the Notation section.
During the signing process, the Sign algorithm might have to negate the secret share in order to produce a partial signature for an X-only threshold public key, which may be tweaked v times (X-only or plain).
The following elliptic curve points arise as intermediate steps when creating a signature:
-
The values Pi (pubshare), di' (secret share), and Q0 (threshold public key) are produced by a FROST key generation protocol. We have
Pi = di'·G Q0 = λid1·P1 + λid2·P2 + ... + λidu·Pu
Here, λidi denotes the interpolating value for the i-th signing participant in the Signers Context.
-
Qi is the tweaked threshold public key after the i-th execution of ApplyTweak for 1 ≤ i ≤ v. It holds that
Qi = f(i-1) + ti·G for i = 1, ..., v where f(i-1) := with_even_y(Qi-1) if is_xonly_ti and f(i-1) := Qi-1 otherwise. -
with_even_y(Qv) is the final result of the threshold public key tweaking operations. It corresponds to the output of GetXonlyPubkey applied on the final Tweak Context.
The signer's goal is to produce a partial signature corresponding to the final result of threshold pubkey derivation and tweaking, i.e., the X-only public key with_even_y(Qv).
For 1 ≤ i ≤ v, we denote the value g computed in the i-th execution of ApplyTweak by gi-1. Therefore, gi-1 equals Scalar(-1) if and only if is_xonly_ti is true and Qi-1 has an odd Y coordinate. In other words, gi-1 indicates whether Qi-1 needed to be negated to apply an X-only tweak:
f(i-1) = gi-1·Qi-1 for 1 ≤ i ≤ v
Furthermore, the Sign and PartialSigVerify algorithms set value g depending on whether Qv needed to be negated to produce the (X-only) final output. For consistency, this value g is referred to as gv in this section.
with_even_y(Qv) = gv·Qv
So, the (X-only) final public key is
with_even_y(Qv)
= gv·Qv
= gv·(f(v-1) + tv·G)
= gv·(gv-1·(f(v-2) + tv-1·G) + tv·G)
= gv·gv-1·f(v-2) + gv·(tv + gv-1·tv-1)·G
= gv·gv-1·f(v-2) + (sumi=v-1..v ti · prodj=i..v gj)·G
= gv·gv-1· ... ·g1·f(0) + (sumi=1..v ti · prodj=i..v gj)·G
= gv· ... ·g0·Q0 + gv·taccv·G
where tacci is computed by TweakCtxInit and ApplyTweak as follows:
tacc0 = 0 (mod ord) tacci = ti + gi-1·tacci-1 (mod ord) for i=1..v
for which it holds that
gv·taccv = sumi=1..v ti · prodj=i..v gj (mod ord)
TweakCtxInit and ApplyTweak compute
gacc0 = 1 (mod ord) gacci = gi-1 · gacci-1 (mod ord) for i=1..v
So we can rewrite above equation for the final public key as
with_even_y(Qv) = gv · gaccv · Q0 + gv · taccv · G
Then we have
with_even_y(Qv) - gv·taccv·G
= gv·gaccv·Q0
= gv·gaccv·(λid1·P1 + ... + λidu·Pu)
= gv·gaccv·(λid1·d1'·G + ... + λidu·du'·G)
= sumj=1..u(gv·gaccv·λidj·dj')·G
Intuitively, gacci tracks accumulated sign flipping and tacci tracks the accumulated tweak value after applying the first i individual tweaks. Additionally, gv indicates whether Qv needed to be negated to produce the final X-only result. Thus, participant i multiplies their secret share di' with gv·gaccv in the Sign algorithm.
As explained in Negation Of The Secret Share When Signing the signer uses a possibly negated secret share
d = gv·gaccv·d' (mod ord)
when producing a partial signature to ensure that the aggregate signature will correspond to a threshold public key with even Y coordinate.
The PartialSigVerifyInternal algorithm is supposed to check
s·G = Re* + e·λ·d·G
The verifier doesn't have access to d · G but can construct it using the participant pubshare as follows:
d·G = gv · gaccv · d' · G = gv · gaccv · cpoint(pubshare)
Note that the threshold public key and list of tweaks are inputs to partial signature verification, so the verifier can also construct gv and gaccv.
If the coordinator provides aggnonce = bytes(33,0) || bytes(33,0), either the coordinator is dishonest or there is at least one dishonest signer (except with negligible probability). If signing aborted in this case, it would be impossible to determine who is dishonest. Therefore, signing continues so that the culprit is revealed when collecting and verifying partial signatures.
However, the final nonce R of a BIP340 Schnorr signature cannot be the point at infinity. If we would nonetheless allow the final nonce to be the point at infinity, then the scheme would lose the following property: if PartialSigVerify succeeds for all partial signatures, then PartialSigAgg will return a valid Schnorr signature. Since this is a valuable feature, we modify FROST3 signing to avoid producing an invalid Schnorr signature while still allowing detection of the dishonest signer: In GetSessionValues, if the final nonce R would be the point at infinity, set it to the generator instead (an arbitrary choice).
This modification to GetSessionValues does not affect the unforgeability of the scheme. Given a successful adversary against the unforgeability game (EUF-CMA) for the modified scheme, a reduction can win the unforgeability game for the original scheme by simulating the modification towards the adversary: When the adversary provides aggnonce' = bytes(33, 0) || bytes(33, 0), the reduction sets aggnonce = cbytes_ext(G) || bytes(33, 0). For any other aggnonce', the reduction sets aggnonce = aggnonce'. (The case that the adversary provides an aggnonce' ≠ bytes(33, 0) || bytes(33, 0) but nevertheless R' in GetSessionValues is the point at infinity happens only with negligible probability.)
This document proposes a standard for the FROST threshold signature scheme that is compatible with BIP340. FROST is not compatible with ECDSA signatures traditionally used in Bitcoin.
- 0.4.1 (2026-03-03): Assign blame to signer index (of the input list) instead of their identifier value
- 0.4.0 (2026-01-30): Number 445 was assigned to this BIP.
- 0.3.6 (2026-01-28): Add MIT license file for reference code and other auxiliary files.
- 0.3.5 (2026-01-25): Update secp256k1lab to latest version, remove stub file, and fix formatting in the BIP text.
- 0.3.4 (2026-01-01): Add an example file to the reference code.
- 0.3.3 (2025-12-29): Replace the lengthy Introduction section with a concise Motivation section.
- 0.3.2 (2025-12-20): Use 2-of-3 keys in test vectors.
- 0.3.1 (2025-12-17): Update the Algorithms section to use secp256k1lab methods and types.
- 0.3.0 (2025-12-15): Introduces the following changes:
- Introduce SignersContext and define key material compatibility with ValidateSignersCtx.
- Rewrite the signing protocol assuming a coordinator, add sequence diagram, and warn key generation protocols to output Taproot-safe threshold public key.
- Remove GetSessionInterpolatingValue, SessionHasSignerPubshare, ValidatePubshares, and ValidateThreshPubkey algorithms
- Revert back to initializing TweakCtxInit with threshold public key instead of pubshares
- 0.2.3 (2025-11-25): Sync terminologies with the ChillDKG BIP.
- 0.2.2 (2025-11-11): Remove key generation test vectors as key generation is out of scope for this specification.
- 0.2.1 (2025-11-10): Vendor secp256k1lab library to provide Scalar and GE primitives. Restructure reference implementation into a Python package layout.
- 0.2.0 (2025-04-11): Includes minor fixes and the following major changes:
- Initialize TweakCtxInit using individual pubshares instead of the threshold public key.
- Add Python script to automate generation of test vectors.
- Represent participant identifiers as 4-byte integers in the range 0..n - 1 (inclusive).
- 0.1.0 (2024-07-31): Publication of draft BIP on the bitcoin-dev mailing list
We thank Jonas Nick, Tim Ruffing, Jesse Posner, and Sebastian Falbesoner for their contributions to this document.
Footnotes
-
While t = n and t = 1 are in principle supported, simpler alternatives are available in these cases. In the case t = n, using a dedicated n-of-n multi-signature scheme such as MuSig2 (see BIP327) instead of FROST avoids the need for an interactive DKG. The case t = 1 can be realized by letting one signer generate an ordinary BIP340 key pair and transmitting the key pair to every other signer, who can check its consistency and then simply use the ordinary BIP340 signing algorithm. Signers still need to ensure that they agree on a key pair. ↩
-
The FROST3 signing scheme has been proven existentially unforgeable for both trusted dealer and distributed key generation setups. When using a trusted dealer for key generation, security reduces to the standard One-More Discrete Logarithm (OMDL) assumption. When instantiated with a distributed key generation protocol such as SimplPedPoP, security reduces to the Algebraic One-More Discrete Logarithm (AOMDL) assumption. ↩
-
The Signers Context represents the public data of signing participants: their identifiers (id1..u) and public shares (pubshare1..u). Implementations may represent this as simply as two separate lists passed to signing APIs. The threshold public key thresh_pk can be stored for efficiency or recomputed when needed using DeriveThreshPubkey. Similarly, the values n and t are used only for validation, and can be omitted if validation is not performed. ↩
-
We treat the secnonce and pubnonce as grammatically singular even though they include serializations of two scalars and two elliptic curve points, respectively. This treatment may be confusing for readers familiar with the FROST paper. However, serialization is a technical detail that is irrelevant for users of FROST interfaces. ↩
-
When preprocessing NonceGen round, the Signers Context can be extended to include the pubnonces of the signing participants, as these are generated and stored before the signing session begins. ↩
-
Assume a malicious participant intends to forge a partial signature for the participant with public share P. It participates in the signing session pretending to be two distinct signers: one with the public share P and the other with its own public share. The adversary then sets the nonce for the second signer in such a way that allows it to generate a partial signature for P. As a side effect, it cannot generate a valid partial signature for its own public share. An explanation of the steps required to create a partial signature forgery can be found in this document. ↩
-
It is an open question whether allowing arbitrary tweaks from an adversary affects the unforgeability of FROST. ↩
-
Given a candidate X coordinate x in the range 0..p-1, there exist either exactly two or exactly zero valid Y coordinates. If no valid Y coordinate exists, then x is not a valid X coordinate either, i.e., no point P exists for which x(P) = x. The valid Y coordinates for a given candidate x are the square roots of c = x3 + 7 mod p and they can be computed as y = ±c(p+1)/4 mod p (see Quadratic residue) if they exist, which can be checked by squaring and comparing with c. ↩
-
DeriveThreshPubkey does not check that its inputs are in range. This validation is performed by ValidateSignersCtx, which is its only caller. ↩
-
The standard Lagrange interpolation coefficient uses the formula idi / (idi - my_id) for each term in the product, where ids are in the range 1..n. However, since participant identifiers in this protocol are zero-indexed (range 0..n-1), we shift them by adding 1. This transforms each term to (idi+1) / (idi - my_id). ↩
-
In theory, the allowed message size is restricted because SHA256 accepts byte strings only up to size of 2^61-1 bytes (and because of the 8-byte length encoding). ↩ ↩2 ↩3 ↩4
-
The random data is hashed (with a unique tag) as a precaution against situations where the randomness may be correlated with the secret signing share itself. It is xored with the secret share (rather than combined with it in a hash) to reduce the number of operations exposed to the actual secret share. ↩
-
The algorithms as specified here assume that the secnonce is stored as a 64-byte array using the serialization secnonce = scalar_to_bytes(k1) || scalar_to_bytes(k2). The same format is used in the reference implementation and in the test vectors. However, since the secnonce is (obviously) not meant to be sent over the wire, compatibility between implementations is not a concern, and this method of storing the secnonce is merely a suggestion. The secnonce is effectively a local data structure of the signer which comprises the value pair (k1, k2), and implementations may choose any suitable method to carry it from NonceGen (first communication round) to Sign (second communication round). In particular, implementations may choose to hide the secnonce in internal state without exposing it in an API explicitly, e.g., in an effort to prevent callers from reusing a secnonce accidentally. ↩ ↩2
-
Verifying the signature before leaving the signer prevents random or adversarially provoked computation errors. This prevents publishing invalid signatures which may leak information about the secret share. It is recommended but can be omitted if the computation cost is prohibitive. ↩
-
DeriveInterpolatingValue(id1..u, my_id) cannot fail when called from PartialSigVerifyInternal as PartialSigVerify picks my_id from id1..u ↩
