-
Notifications
You must be signed in to change notification settings - Fork 5.9k
BIP445: FROST Signing Protocol for BIP340 Signatures #2070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
I'll fix the typos check soon |
|
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:
|
murchandamus
left a comment
There was a problem hiding this 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
left a comment
There was a problem hiding this 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.
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! |
DarkWindman
left a comment
There was a problem hiding this 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.
Christewart
left a comment
There was a problem hiding this 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: |
There was a problem hiding this comment.
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
mis not present (encoded as00) with the prefixmis present, but its the empty bytestring (encoded as0100) 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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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);
}
Yes, it's a |
Click there. ;)
|
Thank you! TIL :-) |
DarkWindman
left a comment
There was a problem hiding this 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)* |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_2check in thenonce_gen_internalordeterministic_signfunctions.
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.
There was a problem hiding this comment.
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 :)
|
@DarkWindman thanks a lot for the review! I've addressed most of your review comments in a88f033. |
There was a problem hiding this 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.
| ], | ||
| "msg_index": 0, | ||
| "signer_index": 0, | ||
| "comment": "The signer's pubshare is not in the list of pubshares" |
There was a problem hiding this comment.
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?
Fail if DeriveThreshPubkey(id1..u, pubshare1..u) ≠ thresh_pk
There was a problem hiding this comment.
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
],
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
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. |
| ``` | ||
| BIP: ? | ||
| Title: FROST Signing Protocol for BIP340 Signatures | ||
| Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Comments-URI: |
| Author: Sivaram Dhakshinamoorthy <siv2ram@gmail.com> | ||
| Comments-URI: | ||
| Status: Draft | ||
| Type: Standards Track |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Type: Standards Track | |
| Type: Specification |
| Comments-URI: | ||
| Status: Draft | ||
| Type: Standards Track | ||
| Assigned: ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Assigned: ? | |
| Assigned: 2026-01-30 |
| @@ -0,0 +1,825 @@ | |||
| ``` | |||
| BIP: ? | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| BIP: ? | |
| BIP: 445 |

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:
FROST-BIP340TODO: verify if this impl is compatible with our test vectorssecp256kfun (implements ChillDKG with FROST signing)TODO: verify if this impl is compatible with our test vectorsDisclosure: 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