Skip to content

Conversation

@siv2r
Copy link
Contributor

@siv2r siv2r commented Jan 3, 2026

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

@siv2r
Copy link
Contributor Author

siv2r commented Jan 3, 2026

I'll fix the typos check soon

@siv2r
Copy link
Contributor Author

siv2r commented Jan 3, 2026

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 (RFC: Integrate secp256k1lab v1.0.0 as subtree, use it for BIP-374 #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.

@murchandamus murchandamus changed the title Add BIP: FROST Signing for BIP340-compatible Threshold Signatures BIP Draft: FROST Signing Protocol for BIP340 Schnorr Signatures Jan 6, 2026
Copy link
Contributor

@murchandamus murchandamus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a first glance, but I noticed a few issues:

@murchandamus murchandamus changed the title BIP Draft: FROST Signing Protocol for BIP340 Schnorr Signatures BIP Draft: FROST Signing Protocol for BIP340 Signatures Jan 8, 2026
Copy link
Contributor

@murchandamus murchandamus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@siv2r
Copy link
Contributor Author

siv2r commented Jan 9, 2026

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!

Copy link

@DarkWindman DarkWindman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Quite a remarkable job! We found a few minor issues, and correcting them would improve the overall specification of the BIP.

Copy link
Contributor

@Christewart Christewart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

- 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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link

@vitrixLab vitrixLab Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

@siv2r siv2r Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

unsigned char msg_present;
msg_present = msg32 != NULL;
secp256k1_sha256_write(&sha, &msg_present, 1);
if (msg_present) {
    secp256k1_nonce_function_musig_helper(&sha, 8, msg32, 32);
}

@siv2r
Copy link
Contributor Author

siv2r commented Jan 21, 2026

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 (comment)

@murchandamus
Copy link
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. ;)

image

@Christewart
Copy link
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 :-)

Copy link

@DarkWindman DarkWindman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few additional minor issues and questions.

- If the optional argument *extra_in* is not present:
- Let *extra_in = empty_bytestring*
- 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*
- Fail if *k<sub>1</sub> = Scalar(0)* or *k<sub>2</sub> = Scalar(0)*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

@siv2r
Copy link
Contributor Author

siv2r commented Jan 25, 2026

@DarkWindman thanks a lot for the review! I've addressed most of your review comments in a88f033.

Copy link
Contributor

@murchandamus murchandamus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@murchandamus murchandamus added the PR Author action required Needs updates, has unaddressed review comments, or is otherwise waiting for PR author label Jan 27, 2026
],
"msg_index": 0,
"signer_index": 0,
"comment": "The signer's pubshare is not in the list of pubshares"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

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

instead of

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

@Christewart Christewart Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

"id_indices": [
    0,
    1
],

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@murchandamus
Copy link
Contributor

murchandamus commented Jan 30, 2026

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.

@murchandamus murchandamus changed the title BIP Draft: FROST Signing Protocol for BIP340 Signatures BIP445: FROST Signing Protocol for BIP340 Signatures Jan 30, 2026
```
BIP: ?
Title: FROST Signing Protocol for BIP340 Signatures
Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
Authors: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>

BIP: ?
Title: FROST Signing Protocol for BIP340 Signatures
Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
Comments-URI:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Comments-URI:

Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com>
Comments-URI:
Status: Draft
Type: Standards Track
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Type: Standards Track
Type: Specification

Comments-URI:
Status: Draft
Type: Standards Track
Assigned: ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Assigned: ?
Assigned: 2026-01-30

@@ -0,0 +1,825 @@
```
BIP: ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
BIP: ?
BIP: 445

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

New BIP PR Author action required Needs updates, has unaddressed review comments, or is otherwise waiting for PR author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants