BIP445: FROST Signing Protocol for BIP340 Signatures #2070

pull siv2r wants to merge 7 commits into bitcoin:master from siv2r:bip-frost-signing changing 40 files +6582 −0
  1. siv2r commented at 6:00 pm on January 3, 2026: contributor

    This PR adds a BIP for the FROST (Flexible Round-Optimized Schnorr Threshold) signing protocol. The development repository is at https://github.com/siv2r/bip-frost-signing.

    There already exists RFC 9591, which standardizes the two-round FROST signing protocol, but it is incompatible with Bitcoin’s BIP340 X-only public keys. This BIP bridges that gap by providing a BIP340-compatible variant of FROST.

    This BIP standardizes the FROST3 variant (Section 2.3 of the ROAST paper). This variant shares significant similarities with the MuSig2 signing protocol (BIP327). Accordingly, this BIP follows the core design principles of BIP327, and many sections have been directly adapted from it.

    FROST key generation is out of scope for this BIP. There are sister BIPs such as ChillDKG and Trusted Dealer Generation that specify key generation mechanisms. This BIP must be used in conjunction with either of those for the full workflow from key generation to signature creation. Careful consideration has been taken to ensure the terminology in this BIP matches that of ChillDKG.

    There are multiple (experimental) implementations of this specification:

    • The reference Python implementation included in this PR
    • secp256k1-zkp FROST module (yet to implement the test vectors)
    • FROST-BIP340 TODO: verify if this impl is compatible with our test vectors
    • secp256kfun (implements ChillDKG with FROST signing) TODO: verify if this impl is compatible with our test vectors

    Disclosure: AI has been used to rephrase paragraphs for clarity, refactor certain sections of the reference code, and review pull requests made to the development repository.

    Feedback is appreciated! Please comment on this pull request or open an issue at https://github.com/siv2r/bip-frost-signing for any feedback. Thank you!

    cc @jonasnick @real-or-random @jesseposner

  2. Add BIP FROST Signing for Schnorr threshold signatures b7e42f93b5
  3. siv2r commented at 6:12 pm on January 3, 2026: contributor
    I’ll fix the typos check soon
  4. siv2r commented at 6:49 pm on January 3, 2026: contributor

    I can see that GitHub’s file changes view shows only one file at a time due to the large number of changes. This is because the reference implementation includes dependencies and auxiliary materials:

    • The reference code uses secp256k1lab python library (vendored as a git subtree, ~20 files) for scalar and group arithmetic. I can remove this from the PR when the library is integrated into this repository (#1855).
    • Auxiliary files include docs/partialsig_forgery.md (which I can move to a gist if preferred) and a test vector generation script (~1400 lines). I can exclude these if necessary.
  5. murchandamus added the label New BIP on Jan 6, 2026
  6. murchandamus renamed this:
    Add BIP: FROST Signing for BIP340-compatible Threshold Signatures
    BIP Draft: FROST Signing Protocol for BIP340 Schnorr Signatures
    on Jan 6, 2026
  7. in bip-frost-signing.md:1 in b7e42f93b5
    0@@ -0,0 +1,862 @@
    1+```yaml
    


    murchandamus commented at 0:58 am on January 6, 2026:
    The “yaml” here is superfluous and will likely break CI checks.

    siv2r commented at 7:13 am on January 8, 2026:
    I added it because the syntax highlighting looked nice. Removed it.
  8. in bip-frost-signing.md:52 in b7e42f93b5
    47+  - [Negation of the Secret Share when Signing](#negation-of-the-secret-share-when-signing)
    48+    - [Negation of the Pubshare when Partially Verifying](#negation-of-the-pubshare-when-partially-verifying)
    49+  - [Dealing with Infinity in Nonce Aggregation](#dealing-with-infinity-in-nonce-aggregation)
    50+- [Backwards Compatibility](#backwards-compatibility)
    51+- [Changelog](#changelog)
    52+- [Acknowledgments](#acknowledgments)
    


    murchandamus commented at 1:01 am on January 6, 2026:
    Thanks, but it is not necessary to manually include a table of contents. Mediawiki files are automatically rendered with one on GitHub, and you can find one for markdown files here:

    siv2r commented at 7:13 am on January 8, 2026:
    Ah, I was not aware of this. Thanks!
  9. in bip-frost-signing.md:8 in b7e42f93b5
    0@@ -0,0 +1,862 @@
    1+```yaml
    2+BIP:
    3+Title: FROST Signing Protocol for BIP340 Schnorr Signatures
    4+Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
    5+Status: Draft
    6+License: CC0-1.0
    7+License-Code: MIT
    8+Type: Informational
    


    murchandamus commented at 1:02 am on January 6, 2026:
    This should be labeled as a Standards Track BIP

    siv2r commented at 7:14 am on January 8, 2026:
    Fixed
  10. in bip-frost-signing.md:3 in b7e42f93b5
    0@@ -0,0 +1,862 @@
    1+```yaml
    2+BIP:
    3+Title: FROST Signing Protocol for BIP340 Schnorr Signatures
    


    murchandamus commented at 1:07 am on January 6, 2026:
    Title exceeds 44 characters

    real-or-random commented at 7:37 am on January 6, 2026:
    suggestion: “FROST Signing Protocol for BIP340 Signatures” (exactly 44 characters)

    siv2r commented at 7:14 am on January 8, 2026:
    I like the suggestion. Updated the title.

    murchandamus commented at 8:12 pm on January 8, 2026:
    I also adopted it for the title of this PR. :)
  11. in bip-frost-signing.md:11 in b7e42f93b5
     6+License: CC0-1.0
     7+License-Code: MIT
     8+Type: Informational
     9+Created:
    10+Post-History: https://groups.google.com/g/bitcoindev/c/PeMp2HQl-H4/m/AcJtK0aKAwAJ
    11+Comments-URI:
    


    murchandamus commented at 1:10 am on January 6, 2026:

    The headers here are a bit out of order. They must be in a specific order (see https://bips.xyz/2#bip-header-preamble), and CI checks will fail if they aren’t.

    The order should be:

    0Comments-URI:
    1Status: Draft
    2Type: Informational
    3Created:
    4License: CC0-1.0
    5License-Code: MIT
    6Post-History: https://groups.google.com/g/bitcoindev/c/PeMp2HQl-H4/m/AcJtK0aKAwAJ
    

    siv2r commented at 7:14 am on January 8, 2026:
    Rearragned
  12. murchandamus commented at 1:14 am on January 6, 2026: contributor
    This is just a first glance, but I noticed a few issues:
  13. bip-frost-signing: fix typos, preamble, and title ec46a20323
  14. murchandamus renamed this:
    BIP Draft: FROST Signing Protocol for BIP340 Schnorr Signatures
    BIP Draft: FROST Signing Protocol for BIP340 Signatures
    on Jan 8, 2026
  15. in bip-frost-signing.md:8 in ec46a20323 outdated
    0@@ -0,0 +1,822 @@
    1+```
    2+BIP: ?
    3+Title: FROST Signing Protocol for BIP340 Signatures
    4+Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
    5+Comments-URI:
    6+Status: Draft
    7+Type: Standards Track
    8+Assigned: ?
    


    murchandamus commented at 8:14 pm on January 8, 2026:

    As currently BIP 2 still regulates the BIP Process, this should still be Created. (Looking forward to BIP 3 activating any year now. ;))

    0Created: ?
    

    siv2r commented at 1:27 pm on January 9, 2026:
    Okay, I’ll revert it back.

    murchandamus commented at 5:07 pm on January 21, 2026:
    Sorry, it happened more quickly than I anticipated! :)
  16. murchandamus commented at 8:17 pm on January 8, 2026: contributor
    Thanks for the quick turn-around. It’s on my todo list to give this a more thorough look, but it might take a bit. If you can motivate some other reviewers meanwhile, that would also be welcome.
  17. siv2r commented at 1:30 pm on January 9, 2026: contributor

    If you can motivate some other reviewers meanwhile, that would also be welcome.

    I’ve shared it with most of the Bitcoin cryptographers I know and will post it on Twitter and the Bitcoin dev groups I’m part of. Hopefully that will bring in more reviewers!

  18. in bip-0445.md:23 in ec46a20323 outdated
    18+## Copyright
    19+
    20+This document is made available under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/).
    21+The accompanying source code is licensed under the [MIT license](https://opensource.org/license/mit).
    22+
    23+## Motivation
    


    DarkWindman commented at 10:36 am on January 14, 2026:
    The first sections of the BIP use code-style formatting for variables, which is convenient in Markdown and improves readability. However, the following sections, starting with General Signing Flow, do not follow this convention, resulting in an inconsistent style. We suggest adopting a single formatting style throughout the document.
  19. in bip-frost-signing.md:99 in ec46a20323
    94+
    95+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*.
    96+
    97+### General Signing Flow
    98+
    99+We assume that the coordinator and the signing participants (in the algorithms specified below, is stored in a data structure called [Signers Context](#signers-context)) are selected externally to the signing protocol before it is initiated. They could also optionally tweak the *threshold public key* now, by initializing [Tweak Context](#tweak-context) with it.
    


    DarkWindman commented at 10:42 am on January 14, 2026:
    The first two paragraphs of the General Signing Flow section are repetitive. We suggest keeping the second paragraph, as it is easier to follow.

    siv2r commented at 5:20 pm on January 22, 2026:
    Ah yes, thank you! I left this one in by mistake.
  20. in bip-frost-signing.md:261 in ec46a20323
    256+| *cbytes(P)* | *P.to_bytes_compressed()* | Returns the 33-byte compressed serialization of a non-infinity point *P* |
    257+| *cbytes_ext(P)* | *P.to_bytes_compressed<br>_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. |
    258+| *lift_x(x)*[^liftx-soln] | *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. |
    259+| *cpoint(b)* | *GE.from_bytes_compressed(b)* | Decodes a 33-byte compressed serialization *b* into a non-infinity point |
    260+| *cpoint_ext(b)* | *GE.from_bytes_compressed<br>_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 |
    261+| *scalar_from_bytes_checked(b)* | *Scalar.from_bytes_checked(b)* | Deserializes a 32-byte array *b* to a scalar, fails if the value is ≥ *order* |
    


    DarkWindman commented at 10:44 am on January 14, 2026:
    It is better to reorder the table to place the scalar to_bytes functions before the from_bytes functions, in order to be consistent with the order of the point transformation functions.
  21. in bip-0445.md:289 in ec46a20323 outdated
    284+| *has_duplicates(lst)* | Returns *True* if any element in *lst* appears more than once, *False* otherwise |
    285+| *sorted(lst)* | Returns a new list containing the elements of *lst* arranged in ascending order |
    286+| *(a, b, ...)* | Refers to a tuple containing the listed elements |
    287+
    288+> [!NOTE]
    289+> In the following algorithms, all scalar arithmetic is understood to be modulo the group order. For example, *a &middot; b* implicitly means *a &middot; b mod order*
    


    DarkWindman commented at 10:50 am on January 14, 2026:
    This note duplicates the footnote 8 and used twice in the text. We think, it would be simpler to just add modulo n = order wherever scalar arithmetic is used.

    siv2r commented at 5:30 pm on January 22, 2026:

    I don’t understand what you’re suggesting. Should I use mod n explicitly in the equations instead of adding the note? Or are you saying to remove the repeated footnote?

    I’ve removed the duplicate footnote for now.


    DarkWindman commented at 9:46 pm on January 22, 2026:
    I apologize for the confusion - I overlooked that n is already used as one of the threshold scheme parameters. This was our alternative proposal, use mod order wherever scalar arithmetic is used, as we think this is an area where mistakes can easily occur and could be critical. For example, MuSig2 explicitly uses mod order, which helps eliminate potential misunderstandings in the future. If using the term order is undesirable, an alternative would be to denote it by q.

    siv2r commented at 5:50 pm on January 24, 2026:

    Great point! I agree that explicitly mentioning it helps avoid mistakes. I initially removed it because, like you said, n was used in t-of-n, and mod order seemed a bit weird. I also considered q, but we use Q almost everywhere to represent the threshold public key, so I decided to remove the mod <x> altogether.

    I think it might be okay to use q since it’s lowercase and shouldn’t be too confusing with the uppercase pubkey point Q. Maybe mod ord is better? I’ll update this.


    siv2r commented at 9:29 am on January 25, 2026:
    I’ve used mod ord for now
  22. in bip-frost-signing.md:449 in ec46a20323
    444+  - The number *u* of signing participants: an integer with *t ≤ u ≤ n*
    445+  - The list of participant public nonces *pubnonce<sub>1..u</sub>*: *u* 66-byte array, each an output of *NonceGen*
    446+  - The list of participant identifiers *id<sub>1..u</sub>*: *u* integers, each with 0 ≤ *id<sub>i</sub>* < *n*
    447+- For *j = 1 .. 2*:
    448+  - For *i = 1 .. u*:
    449+    - Let *R<sub>i,j</sub> = cpoint(pubnonce<sub>i</sub>[(j-1)*33:j*33])*; fail if that fails and blame signer *id<sub>i</sub>* for invalid *pubnonce*
    


    DarkWindman commented at 11:02 am on January 14, 2026:
    The multiplication operator * is not displayed in the compiled version when calculating pubnonce array indices.
  23. in bip-frost-signing.md:509 in ec46a20323
    504+- Let *(Q, gacc, _, id<sub>1..u</sub>, pubshare<sub>1..u</sub>, b, R, e) = GetSessionValues(session_ctx)*; fail if that fails
    505+- Let *k<sub>1</sub>' = scalar_from_bytes_nonzero_checked(secnonce[0:32])*; fail if that fails
    506+- Let *k<sub>2</sub>' = scalar_from_bytes_nonzero_checked(secnonce[32:64])*; fail if that fails
    507+- Let *k<sub>1</sub> = k<sub>1</sub>', k<sub>2</sub> = k<sub>2</sub>'* if *has_even_y(R)*, otherwise let *k<sub>1</sub> = -k<sub>1</sub>', k<sub>2</sub> = -k<sub>2</sub>'*
    508+- Let *d' = scalar_from_bytes_nonzero_checked(secshare)*; fail if that fails
    509+- Let *P = d' &middot; G*
    


    DarkWindman commented at 11:14 am on January 14, 2026:
    The computed value P is only used in the subsequent line. It may be clearer to just write pubshare = cbytes(d′·G), similar to the DeterministicSign algorithm.
  24. in bip-0445.md:526 in ec46a20323 outdated
    521+
    522+[^why-verify-partialsig]: 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.
    523+
    524+### Partial Signature Verification
    525+
    526+Algorithm *PartialSigVerify(psig, pubnonce<sub>1..u</sub>, signers_ctx, tweak<sub>1..v</sub>, is_xonly_t<sub>1..v</sub>, m, i)*:
    


    DarkWindman commented at 11:16 am on January 14, 2026:
    Missing the v argument, which is listed as an input parameter below.

    siv2r commented at 5:32 pm on January 24, 2026:
    v isn’t explicitly taken as input. It’s mentioned in the input argument description to provide better context for the length of the tweaks and is_xonly_t lists.
  25. in bip-frost-signing.md:537 in ec46a20323
    532+  - The number *v* of tweaks with *0 ≤ v < 2^32*
    533+  - The list of tweaks *tweak<sub>1..v</sub>*: *v* 32-byte arrays, each a serialized scalar
    534+  - The list of tweak modes *is_xonly_t<sub>1..v</sub>* : *v* booleans
    535+  - The message *m*: a byte array[^max-msg-len]
    536+  - The index *i* of the signer in the list of public nonces where *0 < i ≤ u*
    537+- Let *(_, _, u, id<sub>1..u</sub>, pubshare<sub>1..u</sub>, thresh_pk) = signers_ctx*
    


    DarkWindman commented at 11:19 am on January 14, 2026:
    thresh_pk is never used and should be replaced with a wildcard.
  26. in bip-0445.md:577 in ec46a20323 outdated
    572+  - Let *s<sub>i</sub> = scalar_from_bytes_nonzero_checked(psig<sub>i</sub>)*; fail if that fails and blame signer *id<sub>i</sub>* for invalid partial signature.
    573+- Let *g = Scalar(1)* if *has_even_y(Q)*, otherwise let *g = Scalar(-1)*
    574+- Let *s = s<sub>1</sub> + ... + s<sub>u</sub> + e &middot; g &middot; tacc*
    575+- Return *sig = xbytes(R) || scalar_to_bytes(s)*
    576+
    577+### Test Vectors & Reference Code
    


    DarkWindman commented at 11:24 am on January 14, 2026:
    Links to the code and test vectors do not work.
  27. in bip-0445.md:774 in ec46a20323 outdated
    769+Therefore, signing continues so that the culprit is revealed when collecting and verifying partial signatures.
    770+
    771+However, the final nonce *R* of a BIP340 Schnorr signature cannot be the point at infinity.
    772+If we would nonetheless allow the final nonce to be the point at infinity, then the scheme would lose the following property:
    773+if *PartialSigVerify* succeeds for all partial signatures, then *PartialSigAgg* will return a valid Schnorr signature.
    774+Since this is a valuable feature, we modify [FROST3 signing][roast] 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).
    


    DarkWindman commented at 11:29 am on January 14, 2026:
    It might be better to also mention ROAST in the Motivation section when discussing FROST3.
  28. in bip-frost-signing.md:80 in ec46a20323
    75+
    76+If there is no dedicated coordinator, one of the participants can act as the coordinator.
    77+
    78+#### Signing Inputs and Outputs
    79+
    80+Each signing session requires two inputs: a participant's long-term *secret share* `secshare_i` (individual to each participant, not shared with the coordinator) and a [*Signers Context*](#signers-context)[^signers-ctx-struct] data structure (common to all participants and the coordinator).
    


    DarkWindman commented at 11:55 am on January 14, 2026:
    The Signers Context link is formatted in italics, deviating from the style in the rest of the specification. To maintain a consistent style, italics formatting should be removed from links that do not represent specific functions. Similar issues appear in other sections: Key Material and Setup: Signers Context (line:80) General Signing Flow: Signers Context, Tweak Context (line:102) Signing: See Negation of Secret Share When Signing (line:515) Partial Signature Verification: See Negation of Pubshare When Partially Verifying (line:555)
  29. in bip-frost-signing.md:66 in ec46a20323
    61+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*.
    62+
    63+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](https://www.rfc-editor.org/rfc/rfc9591.html#name-trusted-dealer-key-generati), or a distributed key generation (DKG) protocol such as [ChillDKG](https://github.com/BlockstreamResearch/bip-frost-dkg). The appropriate choice depends on the implementations's trust model and operational requirements.
    64+
    65+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][bip340].
    66+Key generation protocols produce *public shares* and *threshold public keys* in the plain format. During signing, we conditionally negates *secret shares* to ensure the resulting threshold-signature verifies under the corresponding *X-only threshold public key*.
    


    DarkWindman commented at 11:59 am on January 14, 2026:
    Minor typo: negates → negate
  30. in bip-frost-signing.md:63 in ec46a20323
    58+<!-- REVIEW: should we use "identifiers `i`", secret share `secshare_i` style here? -->
    59+A FROST key generation protocol configures a group of `n` participants with a *threshold public key* (representing a `t`-of-`n` threshold policy).
    60+The corresponding *threshold secret key* is Shamir secret-shared among all `n` participants, where each participant holds a distinct long-term *secret share*.
    61+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*.
    62+
    63+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](https://www.rfc-editor.org/rfc/rfc9591.html#name-trusted-dealer-key-generati), or a distributed key generation (DKG) protocol such as [ChillDKG](https://github.com/BlockstreamResearch/bip-frost-dkg). The appropriate choice depends on the implementations's trust model and operational requirements.
    


    DarkWindman commented at 12:00 pm on January 14, 2026:
    Minor typo: implementations’s → implementation’s
  31. DarkWindman commented at 1:05 pm on January 14, 2026: none
    Hi! Quite a remarkable job! We found a few minor issues, and correcting them would improve the overall specification of the BIP.
  32. in bip-0445.md:426 in ec46a20323 outdated
    421+  - Let *m_prefixed = bytes(1, 0)*
    422+- Else:
    423+  - Let *m_prefixed = bytes(1, 1) || bytes(8, len(m)) || m*
    424+- If the optional argument *extra_in* is not present:
    425+  - Let *extra_in = empty_bytestring*
    426+- Let *k<sub>i</sub> = scalar_from_bytes_wrapping(hash<sub>FROST/nonce</sub>(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*
    


    Christewart commented at 8:18 pm on January 18, 2026:
    Can you explain why i=1,2? This is unclear to me. Why not i=1,2,3,...,n? I don’t understand why we stop at 2.

    Christewart commented at 8:37 pm on January 18, 2026:
    Nevermind this is because you need a public and secret nonce feel free to resolve 😬

    siv2r commented at 4:37 am on January 21, 2026:
    According to the protocol, each signer generates two pairs of nonces, as specified by $\text{Preround(pk)}$ in Figure 4 of the paper. These secret nonces are later combined during signing. We could technically generate more than two nonces (section 2.2 of Musig2 paper), but that might be an overkill.
  33. Christewart commented at 8:19 pm on January 18, 2026: contributor
    As I mentioned on X i’m working on this, so you will likely see more comments in the future. Another nice-to-have would be a table of contents (example) as most other BIPs have this. Perhaps this is a limitation of the .md document vs .mediawiki. Not sure.
  34. in bip-0445.md:420 in ec46a20323 outdated
    415+  - Let *rand = rand'*
    416+- If the optional argument *pubshare* is not present:
    417+  - Let *pubshare* = *empty_bytestring*
    418+- If the optional argument *thresh_pk* is not present:
    419+  - Let *thresh_pk* = *empty_bytestring*
    420+- If the optional argument *m* is not present:
    


    Christewart commented at 9:07 pm on January 19, 2026:

    How would you suggest handling this case in C?

    We have a message m that equal to the empty bytestring? This is tested in the test vectors accompying this file here.

    The python implmentation allows us to represent 2 different states that are (perhaps?) semantically mean the same thing is my understanding. Here are the 2 cases

    1. m is not present (encoded as 00) with the prefix
    2. m is present, but its the empty bytestring (encoded as 0100) with the prefix.

    This can be represented in the type system of higher level languages like C++, Python, Rust, Scala etc.

    From looking at the API on zkp, it seems like this wouldn’t be possible to represent?


    vitrixLab commented at 11:34 pm on January 19, 2026:

    In C this must be modeled explicitly as an optional bytes type, otherwise we cannot distinguish “m not present” (00) from “m present but empty(0100).

    I recommend representing this as { uint8_t *ptr; size_t len; bool is_present; } and encoding based on is_present.

    Collapsing these cases would break the test vectors and change the hash domain.

    #sc


    siv2r commented at 5:35 am on January 21, 2026:

    We distinguish between a zeroed byte array and an empty byte string. Thus, m being absent (empty_bytestring) is different from m being equal to zero bytes (of any length), and we want to generate distinct nonces for these cases.

    Yes, the current the zkp API doesn’t add the message prefix correctly which needs to be fixed.

    I agree with @vitrixLab, we can model this in C with an is_present variable, Musig2 does exactly this.

    0unsigned char msg_present;
    1msg_present = msg32 != NULL;
    2secp256k1_sha256_write(&sha, &msg_present, 1);
    3if (msg_present) {
    4    secp256k1_nonce_function_musig_helper(&sha, 8, msg32, 32);
    5}
    
  35. siv2r commented at 4:13 am on January 21, 2026: contributor

    Another nice-to-have would be a table of contents (example) as most other BIPs have this. Perhaps this is a limitation of the .md document vs .mediawiki. Not sure.

    Yes, it’s a .md issue, this bip initially had a manually written table of contents but was removed after #2070 (review)

  36. murchandamus commented at 5:08 pm on January 21, 2026: contributor

    As I mentioned on X i’m working on this, so you will likely see more comments in the future. Another nice-to-have would be a table of contents (example) as most other BIPs have this. Perhaps this is a limitation of the .md document vs .mediawiki. Not sure.

    Click there. ;)

  37. Christewart commented at 7:02 pm on January 21, 2026: contributor

    As I mentioned on X i’m working on this, so you will likely see more comments in the future. Another nice-to-have would be a table of contents (example) as most other BIPs have this. Perhaps this is a limitation of the .md document vs .mediawiki. Not sure.

    Click there. ;)

    Thank you! TIL :-)

  38. in bip-0445.md:316 in ec46a20323 outdated
    311+- Fail if *t > n*
    312+- Fail if not *t ≤ u ≤ n*
    313+- For *i = 1 .. u*:
    314+  - Fail if not *0 ≤ id<sub>i</sub> ≤ n - 1*
    315+  - Fail if *cpoint(pubshare<sub>i</sub>)* fails
    316+- Fail if *has_duplicates(id<sub>1..u</sub>)*
    


    DarkWindman commented at 10:20 am on January 22, 2026:

    The same function is also invoked in the DeriveInterpolatingValue() algorithm, which is called through the following chain: ValidateSignersCtx()->DeriveThreshPubkey()->DeriveInterpolatingValue(). Is there a compelling reason to perform this check at the top level, or could the ID duplication check be retained solely at a lower level within DeriveInterpolatingValue()?

    In addition, the ValidateSignersCtx() algorithm checks the condition t > n. However, as stated in the description, the valid range is 1 <= t <= n. Therefore, it would be clearer to replace the condition “Fail if t > n” with “Fail if not 1 <= t <= n”.


    siv2r commented at 5:22 pm on January 24, 2026:

    ValidateSignersCtx() checks the sanity of u signing participants (represented by ids and pubshares). So it only made sense to fail for all faults, like ids being out of range or containing duplicates. So, it’s better to have it in the higher level?

    I added has_duplicates again inside DeriveInterpolatingValue() because it has many other callers, just to be safe. Now that I look at it closely, all these calls get the ids & pubshares list after running ValidateSignersCtx() (through GetSessionValues), so we could technically remove the has_duplicates check from DeriveInterpolatingValue().

    Therefore, it would be clearer to replace the condition “Fail if t > n” with “Fail if not 1 <= t <= n”.

    I agree.


    DarkWindman commented at 10:44 am on January 26, 2026:

    all these calls get the ids & pubshares list after running ValidateSignersCtx() (through GetSessionValues), so we could technically remove the has_duplicates check from DeriveInterpolatingValue().

    Yes, I agree with you, since we call GetSessionValues() at the beginning of both functions that independently invoke DeriveInterpolatingValue(). Thus, we can remove the check from the DeriveInterpolatingValue() and keep it only in ValidateSignersCtx().


    siv2r commented at 3:39 pm on January 27, 2026:
    Alright. I’ll update this.

    siv2r commented at 3:42 pm on January 28, 2026:

    I’d like to keep this redundant check in DeriveInterpolatingValue() for now. Just looked at RFC 9591 which defines their derive_interpolating_value function as:

    def derive_interpolating_value(L, x_i): if x_i not in L: raise “invalid parameters” for x_j in L: if count(x_j, L) > 1: raise “invalid parameters” … …

    We can remove this has_duplicates check later, if we really need to.

  39. in bip-frost-signing.md:435 in ec46a20323
    430+- Let *secnonce = bytes(32, k<sub>1</sub>) || bytes(32, k<sub>2</sub>)*[^secnonce-ser]
    431+- Return *(secnonce, pubnonce)*
    432+
    433+[^sk-xor-rand]: 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.
    434+
    435+[^secnonce-ser]: The algorithms as specified here assume that the *secnonce* is stored as a 64-byte array using the serialization *secnonce = bytes(32, k<sub>1</sub>) || bytes(32, k<sub>2</sub>)*. 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 triple *(k<sub>1</sub>, k<sub>2</sub>)*, 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.
    


    DarkWindman commented at 10:23 am on January 22, 2026:
    …which comprises the value triple… Minor typo: triple → pair
  40. in bip-0445.md:427 in ec46a20323 outdated
    422+- Else:
    423+  - Let *m_prefixed = bytes(1, 1) || bytes(8, len(m)) || m*
    424+- If the optional argument *extra_in* is not present:
    425+  - Let *extra_in = empty_bytestring*
    426+- Let *k<sub>i</sub> = scalar_from_bytes_wrapping(hash<sub>FROST/nonce</sub>(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*
    427+- Fail if *k<sub>1</sub> = Scalar(0)* or *k<sub>2</sub> = Scalar(0)*
    


    DarkWindman commented at 10:38 am on January 22, 2026:
    While reading the implementation, I noticed that it includes a check ensuring that k_1 != k_2. At first glance, omitting this check does not appear to introduce any vulnerabilities, and we have verified this. However, I would appreciate hearing your opinion on this point.

    siv2r commented at 5:04 pm on January 24, 2026:

    Which function are you referring to? I don’t see a k_1 != k_2 check in the nonce_gen_internal or deterministic_sign functions.

    This is an interesting question though. I never considered adding this check, primarily because BIP327’s reference implementation doesn’t have it either.


    DarkWindman commented at 9:45 am on January 26, 2026:

    Which function are you referring to? I don’t see a k_1 != k_2 check in the nonce_gen_internal or deterministic_sign functions.

    I apologize for not mentioning which implementation I was referring to. I meant the secp256k1-zkp FROST module, where the secp256k1_frost_nonce_gen() function performs this check. However, I think this verification is redundant, as I have not found any paper or specification that requires it. At first glance, it may seem that manipulation with Wagner attacks could apply, but I do not see any concrete attack vectors.


    siv2r commented at 3:38 pm on January 27, 2026:
    Oh, I was not aware of this. Thank you! I checked the Olaf paper as well, and it didn’t have this requirement. I haven’t thought about this from security proof perspective. Will keep this open till then :)
  41. in bip-0445.md:331 in ec46a20323 outdated
    326+  - *Q = Q + &lambda; &middot; P*
    327+- Return *cbytes(Q)*
    328+
    329+[^derive-thresh-no-validate-inputs]: *DeriveThreshPubkey* does not check that its inputs are in range. This validation is performed by *ValidateSignersCtx*, which is its only caller.
    330+
    331+Internal Algorithm *DeriveInterpolatingValue(id<sub>1..u</sub>, my_id):*
    


    DarkWindman commented at 11:13 am on January 22, 2026:
    In fact, the specification very subtly avoids an explicit global shift of all identifiers, simplifying the description to the use of id+1 only in the numerator. However, will not this create possible misunderstandings in the future? Perhaps it would make sense to mention this earlier in the specification? I would be glad to hear your thoughts!

    siv2r commented at 4:52 pm on January 24, 2026:

    I agree the DeriveInterpolatingValue formula isn’t conventional, and someone looking at it directly might think it’s incorrect without the context that ids are in the range 0..n-1.

    I initially considered having the caller increment the ids, but that would require modifying many callers: DeriveThresPubkey, Sign, and PartialSigVerifyInternal. I think adding a footnote on the id+1 line is the better option. What do you think?


    siv2r commented at 9:31 am on January 25, 2026:
    I’ve added a footnote. Do let me know your thoughts on it.

    DarkWindman commented at 9:49 am on January 26, 2026:

    I’ve added a footnote. Do let me know your thoughts on it.

    I think this is the best solution.

  42. DarkWindman commented at 11:20 am on January 22, 2026: none
    A few additional minor issues and questions.
  43. python: update secp256k1lab & remove the stub file f21244a9e5
  44. bip-frost-signing: fix formatting and explain more a88f033df7
  45. in bip-0445.md:299 in ec46a20323 outdated
    294+
    295+The Signers Context is a data structure consisting of the following elements:
    296+
    297+- The total number *n* of participants involved in key generation: an integer with *2 ≤ n < 2<sup>32</sup>*
    298+- The threshold number *t* of participants required to issue a signature: an integer with *1 ≤ t ≤ n*
    299+- The number *u* of signing participants: an integer with *t ≤ u ≤ n*
    


    Christewart commented at 10:50 pm on January 22, 2026:

    How is the signing context supposed to be derived for sign_verify_vectors.json. The test file has an n and a t encoded, however no u.

    Is u implied by the number of indices provided in id_indices, pubshare_indices etc?

    I assume an invariant that could be added to this is that pubshares and participant_ids length is equal to u ?

    Also why is u not available in the SignersContext in the python implementation? L304 in this BIP seems to infer that it can be pulled out of the SignersContext

    Let (n, t, u, id1..u, pubshare1..u, thresh_pk) = signers_ctx"


    siv2r commented at 3:44 pm on January 24, 2026:
    Yes, you’re correct. The u is implied by the length of ids and pubshares in SignersContext, u = len(ids) = len(pubshares). Also, t <= u <= n. The validate_signers_ctx function asserts this relationship. In python, we can infer u to be the length of ids or pubshares. I’ve explicitly added u in the specification for clarity.
  46. siv2r commented at 9:33 am on January 25, 2026: contributor
    @DarkWindman thanks a lot for the review! I’ve addressed most of your review comments in a88f033.
  47. in bip-frost-signing.md:11 in a88f033df7 outdated
     6+Status: Draft
     7+Type: Standards Track
     8+Assigned: ?
     9+License: CC0-1.0 or MIT
    10+Post-History: https://groups.google.com/g/bitcoindev/c/PeMp2HQl-H4/m/AcJtK0aKAwAJ
    11+Requires: 32, 340, 341
    


    murchandamus commented at 9:59 pm on January 27, 2026:

    Since BIP 3 activated now:

    0BIP: ?
    1Title: FROST Signing Protocol for BIP340 Signatures
    2Authors: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
    3Status: Draft
    4Type: Specification
    5Assigned: ?
    6License: CC0-1.0 or MIT
    7Discussion: 2024-07-31: https://groups.google.com/g/bitcoindev/c/PeMp2HQl-H4/m/AcJtK0aKAwAJ
    8Requires: 32, 340, 341
    

    siv2r commented at 3:59 pm on January 28, 2026:
    Fixed in 6ea53f2

    murchandamus commented at 10:03 pm on January 30, 2026:

    I had proposed a few more changes. Please:

    • Rename the Author header to Authors
    • Drop the Comments-URI header
    • Update the Type from Standards Track to Specification
  48. in bip-frost-signing.md:9 in a88f033df7
    0@@ -0,0 +1,821 @@
    1+```
    2+BIP: ?
    3+Title: FROST Signing Protocol for BIP340 Signatures
    4+Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
    5+Comments-URI:
    6+Status: Draft
    7+Type: Standards Track
    8+Assigned: ?
    9+License: CC0-1.0 or MIT
    


    murchandamus commented at 10:07 pm on January 27, 2026:

    The header value you used indicates that the BIP (including all auxiliary files) is made available under the terms of both CC0 1.0 Universal as well as the MIT License, and anyone may modify and redistribute it provided they comply with the terms of either license. However, this contradicts the text in the Copyright section which states that the proposal is licensed under CC0-1.0 and the code is licensed under MIT. To express what the Copyright section indicates, you would please only put CC0-1.0 here, and include either a license header in each file, or a COPYING/LICENSE file in the folder with the auxiliary files to express that different licensing agrees to the code files. You can find more information here: https://github.com/bitcoin/bips/blob/master/bip-0003.md#bip-licensing

    0License: CC0-1.0
    

    siv2r commented at 4:06 pm on January 28, 2026:
    Thank you for the detailed explanation. I’m usually confused when it comes to license files, so I took some time to read about them now. I’ve added a MIT COPYING file in the bips/bip-frost-signing/ directory. That should fix this issue.

    murchandamus commented at 6:46 pm on January 28, 2026:
    You’re welcome, thanks for the quick turnaround.
  49. murchandamus commented at 10:20 pm on January 27, 2026: contributor
    From an editorial standpoint, it looks pretty good and like all the required sections are present. I have read the proposal only partially, and do not have the expertise to fully understand all aspects, so I cannot comment on the technical soundness and whether the Specification is complete and sufficient.
  50. murchandamus added the label PR Author action required on Jan 27, 2026
  51. python: add license file and re-write to error msg 6ea53f248b
  52. in bip-frost-signing.md:25 in 6ea53f248b
    20+This document is made available under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/).
    21+The accompanying source code is licensed under the [MIT license](https://opensource.org/license/mit).
    22+
    23+## Motivation
    24+
    25+<!-- REVIEW: Should we add a paragraph about `OP_CHECKSIGADD` like BIP327 does? -->
    


    theStack commented at 5:24 pm on January 28, 2026:

    “Should we add a paragraph about OP_CHECKSIGADD like BIP327 does?”

    Sounds reasonable to add that. If I’m not missing anything, the two paragraphs in BIP327 would fully apply to FROST as well (after minor adaptions, s/MuSig2/FROST/ and s/n-of-n/t-of-n/).


    siv2r commented at 12:23 pm on January 29, 2026:
    Yes, prior versions of this BIP had those two paragraphs. I removed them in https://github.com/siv2r/bip-frost-signing/commit/5e7e913782ec9c11951be19bb1308a26e0a60294 wanting to make the motivation section more concise. These paragraphs seem important to ignore though. I’ll add it back.

    siv2r commented at 1:41 pm on January 29, 2026:
    Fixed in 5d87f56.
  53. in bip-frost-signing.md:111 in 6ea53f248b
    106+Signers begin the signing session by running *NonceGen* to compute their *secnonce* and *pubnonce*.[^nonce-serialization-detail]
    107+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.
    108+
    109+[^nonce-serialization-detail]: We treat the *secnonce* and *pubnonce* as grammatically singular even though they include serializations of two scalars and two elliptic curve points, respectively.
    110+This treatment may be confusing for readers familiar with the MuSig2 paper.
    111+However, serialization is a technical detail that is irrelevant for users of MuSig2 interfaces.
    


    theStack commented at 5:25 pm on January 28, 2026:
    nit: should “MuSig2” replaced with “FROST” here?

    siv2r commented at 1:41 pm on January 29, 2026:
    Fixed in 5d87f56.
  54. in bip-frost-signing.md:244 in 6ea53f248b
    239+
    240+<!-- markdownlint-disable MD033 -->
    241+| Notation | secp256k1lab | Description |
    242+| --- | --- | --- |
    243+| *p* | *FE.SIZE* | Field element size |
    244+| *ord* | *GE.ORDER* | Group order |
    


    theStack commented at 5:26 pm on January 28, 2026:
    nit: the constant is also available as Scalar.SIZE in secp256k1lab

    siv2r commented at 1:41 pm on January 29, 2026:
    Fixed in 5d87f56.
  55. in bip-frost-signing.md:247 in 6ea53f248b
    242+| --- | --- | --- |
    243+| *p* | *FE.SIZE* | Field element size |
    244+| *ord* | *GE.ORDER* | Group order |
    245+| *G* | *G* | The secp256k1 generator point |
    246+| *inf_point* | *GE()* | The infinity point |
    247+| *is_infinity(P)* | *P.infinity()* | Returns whether *P* is the point at infinity |
    


    theStack commented at 5:27 pm on January 28, 2026:
    0| *is_infinity(P)* | *P.infinity* | Returns whether *P* is the point at infinity |
    

    (since infinity is a property rather than a method within GE)


    siv2r commented at 1:41 pm on January 29, 2026:
    Fixed in 5d87f56.
  56. in bip-frost-signing.md:351 in 6ea53f248b
    346+
    347+The Tweak Context is a data structure consisting of the following elements:
    348+
    349+- The point *Q* representing the potentially tweaked threshold public key: a *GE*
    350+- The accumulated tweak *tacc*: a *Scalar*
    351+- The value *gacc*: *Scalar(1)* or *Scalar(-1)*
    


    theStack commented at 5:30 pm on January 28, 2026:
    pedantic nit: could swap these two lines, so the order matches the one of unpacking tweak_ctx (and the order of TweakCtxInit return values) below

    siv2r commented at 1:41 pm on January 29, 2026:
    Fixed in 5d87f56.
  57. in bip-frost-signing.md:429 in 6ea53f248b
    424+  - Let *extra_in = empty_bytestring*
    425+- Let *k<sub>i</sub> = scalar_from_bytes_wrapping(hash<sub>FROST/nonce</sub>(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*
    426+- Fail if *k<sub>1</sub> = Scalar(0)* or *k<sub>2</sub> = Scalar(0)*
    427+- Let *R<sub>\*,1</sub> = k<sub>1</sub> &middot; G*, *R<sub>\*,2</sub> = k<sub>2</sub> &middot; G*
    428+- Let *pubnonce = cbytes(R<sub>\*,1</sub>) || cbytes(R<sub>\*,2</sub>)*
    429+- Let *secnonce = bytes(32, k<sub>1</sub>) || bytes(32, k<sub>2</sub>)*[^secnonce-ser]
    


    theStack commented at 5:31 pm on January 28, 2026:
    nit: could use scalar_to_bytes here instead

    siv2r commented at 1:41 pm on January 29, 2026:
    Fixed in 5d87f56.
  58. in bip-frost-signing.md:445 in 6ea53f248b
    440+Algorithm *NonceAgg(pubnonce<sub>1..u</sub>, id<sub>1..u</sub>)*:
    441+
    442+- Inputs:
    443+  - The number *u* of signing participants: an integer with *t ≤ u ≤ n*
    444+  - The list of participant public nonces *pubnonce<sub>1..u</sub>*: *u* 66-byte array, each an output of *NonceGen*
    445+  - The list of participant identifiers *id<sub>1..u</sub>*: *u* integers, each with 0 ≤ *id<sub>i</sub>* < *n*
    


    theStack commented at 5:33 pm on January 28, 2026:

    pedantic nit, for consistency with other functions:

    0  - The list of participant identifiers *id<sub>1..u</sub>*: *u* integers, each with 0 ≤ *id<sub>i</sub>* <= *n-1*
    

    siv2r commented at 1:41 pm on January 29, 2026:
    Fixed in 5d87f56.
  59. in bip-frost-signing.md:528 in 6ea53f248b
    523+
    524+Algorithm *PartialSigVerify(psig, pubnonce<sub>1..u</sub>, signers_ctx, tweak<sub>1..v</sub>, is_xonly_t<sub>1..v</sub>, m, i)*:
    525+
    526+- Inputs:
    527+  - The partial signature *psig*: a 32-byte array, serialized scalar
    528+  - The list public nonces *pubnonce<sub>1..u</sub>*: *u* 66-byte arrays, each an output of *NonceGen*
    


    theStack commented at 5:33 pm on January 28, 2026:
    0  - The list of public nonces *pubnonce<sub>1..u</sub>*: *u* 66-byte arrays, each an output of *NonceGen*
    

    siv2r commented at 1:42 pm on January 29, 2026:
    Fixed in 5d87f56.
  60. bip-frost-signing: add info about OP_CHECKSIGADD and minor edits 5d87f5626e
  61. in bip-0445/python/vectors/sign_verify_vectors.json:417 in 6ea53f248b outdated
    412+                0,
    413+                1
    414+            ],
    415+            "msg_index": 0,
    416+            "signer_index": 0,
    417+            "comment": "The signer's pubshare is not in the list of pubshares"
    


    Christewart commented at 7:38 pm on January 28, 2026:

    I believe this test should be moved into sign_error_test_cases since this would cause an error when deriving the threshold public key from the pubshares?

    https://github.com/bitcoin/bips/blob/ec46a20323840b1a6aba83bc2d18b34dd0811245/bip-frost-signing.md#signers-context

    Fail if DeriveThreshPubkey(id1..u, pubshare1..u) ≠ thresh_pk


    siv2r commented at 1:00 pm on January 29, 2026:

    That’s a great observation! The DeriveThreshPubkey function would fail if we gave it a pubshare that’s not part of the t-of-n FROST key (i.e., any pubshare other than the n generated during keygen). Here, we’re providing a pubshare that was generated during keygen, so DeriveThreshPubkey passes. However, partial_sig_verify fails because we’re not providing the correct pubshare whose secshare was used to generate psig.

    TL;DR: We’re generating psig with secshare, but using a different pubshare (one that exists in the t-of-n FROST key) for verification. That’s why the pubshare indices list in this test vector looks like:

    0            "pubshare_indices": [
    1                2, # not signer's pubshare
    2                1
    3            ],
    

    instead of

    0            "pubshare_indices": [
    1                0, # signer's pubshare
    2                1
    3            ],
    

    siv2r commented at 1:02 pm on January 29, 2026:
    The test vectors covers a lot of cases (some of which may not be useful) if you find something unncessary or if I missed an essential edge case, please let me know I’ll add/remove them accordingly.

    Christewart commented at 8:31 pm on January 29, 2026:

    Doesn’t this compute the wrong pubkey because the lagrange coefficient ends up being incorrect - id should be 2 and ends it ends up being 0 because of the id_indices provided in this test case

    0"id_indices": [
    1    0,
    2    1
    3],
    

    Bigger picture, I don’t understand why we don’t call ValidateSignersCtx in either PartialSignatureVerify or PartialSignatureVerifyInternal as the BIP is written.

    It appears in the python implementation in this PR, we do call validate_signers_ctx. However in the test cases, you call partial_sig_verify_internal directly which circumvents the check. This is probably a signal that there should be some encapsulation of partial_sig_verify_internal - does this actually need to be in a separate method in the BIP? If so - and we allow it to be called directly - we need to redo any context validation in that function.

    Is this an oversight or an intentional choice ? If the former, i think we should add validating the signer context inside of partial_sig_verify_internal, if the latter I would be interested in hearing the reasoning.


    siv2r commented at 6:30 am on January 30, 2026:

    Ah yes, you’re correct. I missed the incorrect id_indices. The DeriveThresPubkey should fail here.

    Hmm, PartialSignatureVerify must call ValidateSignersCtx. The Python code does this, but it’s not reflected in the BIP text. I’ll update it. Thanks!

    PartialSignatureVerifyInternal does call ValidateSignersCtx in both the BIP text and Python code. It’s not apparent because it calls GetSessionValues, which internally calls ValidateSignersCtx. I think it’s useful to explicitly call it in PartialSignatureVerifyInternal (even though it’s redundant) to prevent implementers from making mistakes.

    I was wondering why the test vector passes in the code even though both partial_sig_verify_internal and partial_sig_verify call validate_signers_ctx. Apparently, there’s a bug in the test code. The verify_fail_test_cases reuses an old valid session_ctx (and fails to create a correct new one), so DeriveThresPubkey passes. It would have failed with the new session_ctx. I’ll fix this.

  62. murchandamus commented at 9:50 pm on January 30, 2026: contributor
    Let’s call this BIP 445. Please add an entry for your proposal in the README table, in the preamble update the BIP header to 445 and Assigned header to 2026-01-30, and update your documents file name as well as the auxiliary file directory.
  63. murchandamus renamed this:
    BIP Draft: FROST Signing Protocol for BIP340 Signatures
    BIP445: FROST Signing Protocol for BIP340 Signatures
    on Jan 30, 2026
  64. in bip-frost-signing.md:4 in 5d87f5626e
    0@@ -0,0 +1,825 @@
    1+```
    2+BIP: ?
    3+Title: FROST Signing Protocol for BIP340 Signatures
    4+Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
    


    murchandamus commented at 10:05 pm on January 30, 2026:
    0Authors: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
    

    siv2r commented at 10:39 am on February 1, 2026:
    Fixed in 289286c9
  65. in bip-frost-signing.md:5 in 5d87f5626e
    0@@ -0,0 +1,825 @@
    1+```
    2+BIP: ?
    3+Title: FROST Signing Protocol for BIP340 Signatures
    4+Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
    5+Comments-URI:
    


    murchandamus commented at 10:05 pm on January 30, 2026:

    siv2r commented at 10:38 am on February 1, 2026:
    Fixed in 289286c9
  66. in bip-frost-signing.md:7 in 5d87f5626e
    0@@ -0,0 +1,825 @@
    1+```
    2+BIP: ?
    3+Title: FROST Signing Protocol for BIP340 Signatures
    4+Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
    5+Comments-URI:
    6+Status: Draft
    7+Type: Standards Track
    


    murchandamus commented at 10:05 pm on January 30, 2026:
    0Type: Specification
    

    siv2r commented at 10:38 am on February 1, 2026:
    Fixed in 289286c9
  67. in bip-frost-signing.md:8 in 5d87f5626e
    0@@ -0,0 +1,825 @@
    1+```
    2+BIP: ?
    3+Title: FROST Signing Protocol for BIP340 Signatures
    4+Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
    5+Comments-URI:
    6+Status: Draft
    7+Type: Standards Track
    8+Assigned: ?
    


    murchandamus commented at 10:06 pm on January 30, 2026:
    0Assigned: 2026-01-30
    

    siv2r commented at 10:38 am on February 1, 2026:
    Fixed in 289286c9
  68. in bip-frost-signing.md:2 in 5d87f5626e
    0@@ -0,0 +1,825 @@
    1+```
    2+BIP: ?
    


    murchandamus commented at 10:06 pm on January 30, 2026:
    0BIP: 445
    

    siv2r commented at 10:38 am on February 1, 2026:
    Fixed in 289286c9.
  69. rename bip-frost-signing to bip-0445
    Also, add minor fixes in the preamble of the BIP
    289286c95b
  70. in bip-0445/python/vectors/det_sign_vectors.json:108 in 5d87f5626e outdated
    103+                "4F464D1B1F3EF7CABFBC446BBB1D226F82AC56B7DC1DAD4EF7A641E0A8F378FD"
    104+            ],
    105+            "comment": "Signing without auxiliary randomness"
    106+        },
    107+        {
    108+            "rand": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
    


    Christewart commented at 11:37 pm on January 31, 2026:

    I believe this isn’t a valid scalar as per the BIP:

    https://github.com/bitcoin/bips/blob/ec46a20323840b1a6aba83bc2d18b34dd0811245/bip-frost-signing.md#deterministic-and-stateless-signing-for-a-single-signer

    The auxiliary randomness rand: a 32-byte array, serialized scalar (optional argument)


    siv2r commented at 8:21 am on February 1, 2026:

    Yes, the value is not a valid scalar, but it’s the BIP text that is incorrect here. rand doesn’t need to be a scalar, it can be any 32-byte array. I’ll update the text. Good catch, thanks!

    In DeterministicSign, rand is XORed with sk and supplied to a hash function for generating the nonce. We never convert it to a scalar, so it should be okay to allow any 32-byte array.


    siv2r commented at 9:55 am on February 1, 2026:
    Fixed in 289286c.
  71. siv2r force-pushed on Feb 1, 2026
  72. in bip-0445.md:28 in 289286c95b
    23+
    24+The FROST signature scheme enables threshold Schnorr signatures. In a *t-of-n* threshold configuration, any *t*[^t-edge-cases] 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.
    25+
    26+[^t-edge-cases]: 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][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][bip340] key pair and transmitting the key pair to every other signer, who can check its consistency and then simply use the ordinary [BIP340][bip340] signing algorithm. Signers still need to ensure that they agree on a key pair.
    27+
    28+The IRTF has published [RFC 9591][rfc9591], 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][bip32] key derivation and [BIP341][bip341] Taproot. This document addresses these limitations by specifying a BIP340-compatible variant of FROST signing protocol that supports key tweaking.
    


    Christewart commented at 8:15 pm on February 2, 2026:

    I’m not sure where to best recommend putting this (here, or the tweaking section?) but I think it could be a good idea to mention that this BIP (AFAICT) follows the tweak handling of BIP327.

    This would give people a heads up that you may be able to reuse code from your existing MuSig implementation if you have one.

    We call it the “Tweak Context” in this BIP. In BIP327, they call it the “KeyAgg Context”. At minimum, it seems preferable to use the same nomenclature.

    I would guess that this data structure is gonna become more popular as taproot native protocols will have to handle x-only keys - perhaps its worth having a BIP of its own 🤷‍♂️ .


    siv2r commented at 8:36 am on February 3, 2026:

    The BIP has the following paragraph in the motivation section:

    The FROST3 signing protocol shares substantial similarities with the MuSig2 signing protocol specified in [BIP327][bip327]. Accordingly, this specification adopts several design principles from BIP327, including support for key tweaking, partial signature verification, and identifiable abort mechanisms.

    It specifies the key tweaking is adopted from BIP327. I’ll now add a note in the algorithms section, about the potential to reuse code from exisiting MuSig2 implementation. This should suffice, right?

    We call it the “Tweak Context” in this BIP. In BIP327, they call it the “KeyAgg Context”. At minimum, it seems preferable to use the same nomenclature.

    Yes, TweakContext is basically the renamed version of KeyAgg Context. It would be incorrect to use the same name here because in BIP327, this struct was the output of the KeyAgg function (a key generation protocol). However, key generation for this BIP is out of scope, and TweakContext is only needed when the user wants to tweak their threshold public key. It’s not needed otherwise. Hence, I prefer the TweakContext name.

    I would guess that this data structure is gonna become more popular as taproot native protocols will have to handle x-only keys - perhaps its worth having a BIP of its own 🤷‍♂️ .

    I agree this structure will become popular. It’s already used in BIP 89. As for standardizing it in a BIP, I think it could be done. Whenever this struct appears, it’s usually accompanied by Tweak Context, TweakCtxInit, and ApplyTweak. However, I’m not sure every BIP would follow this exact structure, they may modify it depending on their needs. For instance, this BIP renamed KeyAggContext. There’s also the possibility of “agnostic tweaking” in FROST, which if adopted would completely eliminate this data structure and would follow a different tweaking approach.

  73. in bip-0445.md:264 in 289286c95b
    259+| *cpoint_ext(b)* | *GE.from_bytes_compressed<br>_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 |
    260+| *scalar_to_bytes(s)* | *s.to_bytes()* | Returns the 32-byte serialization of a scalar *s* |
    261+| *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* |
    262+| *scalar_from_bytes<br>_nonzero_checked(b)* | *Scalar.from_bytes<br>_nonzero_checked(b)* | Deserializes a 32-byte array *b* to a scalar, fails if the value is zero or ≥ *ord* |
    263+| *scalar_from_bytes_wrapping(b)* | *Scalar.from_bytes_wrapping(b)* | Deserializes a 32-byte array *b* to a scalar, reducing the value modulo *ord* |
    264+| *hash<sub>tag</sub>(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 |
    


    theStack commented at 6:00 pm on February 3, 2026:

    secp256k1lab table nit:

    0| *hash<sub>tag</sub>(x)* | *tagged_hash(tag, 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 |
    
  74. Christewart commented at 7:58 pm on February 3, 2026: contributor
    tACK test vectors on 289286c95bd719cdd0fb3c2d3ad19a64193d614c
  75. murchandamus commented at 8:03 pm on February 3, 2026: contributor
    As I’m not involved in the technical review here, I’m gonna tune out here for the time being. Please feel free to mention me by nickname when you reach a point where you’d like an editor to take a look again, or when you think you are reaching a point where your document should be published.

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bips. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-02-10 16:10 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me