BIP445: FROST Signing Protocol for BIP340 Signatures - #2070
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.
This is just a first glance, but I noticed a few issues:
murchandamus
left a comment
There was a problem hiding this comment.
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.
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.
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.
Yes, it's a |
Click there. ;)
|
Thank you! TIL :-) |
DarkWindman
left a comment
There was a problem hiding this comment.
A few additional minor issues and questions.
|
@DarkWindman thanks a lot for the review! I've addressed most of your review comments in a88f033. |
There was a problem hiding this comment.
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.
|
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. |
dc8729a to
289286c
Compare
|
I've added multiple (t, n) configs to the test vectors. They now run across 4 configs, (2,3), (1,3), (3,3), and (3,5), instead of the single (2,3) setup we had before. That takes the total from 68 to 250 cases. Here's the per-API breakdown:
I also re-read the BIP text and the reference spec and fixed a few inconsistencies. As far as I can tell, this addresses all the open review comments. I think the BIP is good to merge once the other reviewers are happy with it. |
|
@siv2r: Okay cool, I resubscribed to the PR and have added it to my review queue. |
mllwchrry
left a comment
There was a problem hiding this comment.
Reviewed the BIP once again. Everything looks great! Found only two inconsistencies.
|
The changelog is at 0.6.0, using the same semver-style scheme as BIP327. BIP327 bumped to 1.0.0 when its number was assigned. I didn't follow that for 445 because it was still under active review with suggestions coming in, so tagging it 1.0.0 felt premature back then. It's now had a good stretch of review without any drastic changes suggested. @murchandamus should this be bumped to 1.0.0 just before merge? And what's the usual convention for bips changelog versioning? |
|
@siv2r: Version 1.0.0 is usually reserved for the stage of the BIP that is advanced to Complete. You can find more guidance on Changelog Section and Version Header in BIP3. |
murchandamus
left a comment
There was a problem hiding this comment.
I gave this another fast read. From an editorial perspective, this looks properly formatted and complete (when considering the footnotes and "Remarks on Security and Correctness" the stand-in for the Rationale section). I did not spend the time to fully understand the scheme and only skimmed the algorithm sections. I saw that there was a reference implementation included and that it had some tests. Given the many review comments, and extensive Changelog, it seems to have been read much more carefully by multiple other more qualified reviewers.
From an editorial perspective, I’d be happy to publish this whenever the author and reviewers indicate that the proposal is clear, comprehensive, and mature enough to do so.
|
@siv2r: It sounds like you think it’s ready for publication, some reviewers have indicated the same. Let’s merge this in a week (on or after July 6th) unless more things crop up. Or if you want it published before then, please let me know. |
Sounds good! I'll do one final review this week, and will update here if anything comes up. Else we can merge on July 6th. |
DarkWindman
left a comment
There was a problem hiding this comment.
Minor nit; everything else looks good to me!
|
Sorry for being late with a comment of this kind, but I think this should somehow take into account the "plausible attack on adaptive security of threshold Schnorr signatures" by Elizabeth Crites and Alistair Stewart (paper, talk). tl;dr of the paper:
The big question, of course, is what to do. I think there are two reasonable options:
I tend to think that option 1 is better. Of course, that's a restriction in practice, but we can't just move on and pretend that everything will be fine. If someone wants to ignore the restriction, they can still do this, but then they're on their own. |
|
Given these new review comments, I expect that we will not publish today. I’ll wait for your input, @siv2r. |
|
After consulting with @elizabeth-crites, I have a clearer understanding now, and I should have mentioned also a second paper of her with Jonathan Katz, Chelsea Komlo, Stefano Tessaro, and Chenzhi Zhu (talk; immediately after the other talk in the same video). In fact, the situation is better than I mentioned. It's not just that we don't know whether an attack exist. The new paper defines a computational problem LDVR and proves that FROST is adaptively secure when LDVR is hard in addition to the discrete logarithm problem DL (or strictly speaking, its variant AOMDL). Then the paper proves that LDVR is – at least for parameters we're interested in (i.e., small enough n) – so hard that it's better for the attacker to attack DL instead. Here's improved script that estimates the bit security (function from math import log2, perm
BITS_GROUP_ORDER = 256
def myperm(n: int, k: int):
if k >= 1:
return perm(n, k)
else:
return 1
def table1(n: int, t: int, t_c: int | None = None):
if t_c is None:
t_c = t # worst case
return log2(myperm(n, 2 * t_c - t)/myperm(t_c, 2 * t_c - t))
# Check some numbers from Table 1 in https://eprint.iacr.org/2025/1061
assert table1(128, 96, round(0.9 * 96)) - 79.24 <= 0.01
assert table1(256, 64, 64 ) - 203.57 <= 0.01
assert table1(1024, 512, round(0.6 * 512)) - 197.18 <= 0.01
def effort(n: int, t: int | None = None, t_c: int | None = None):
if t is None:
t = n // 2 # worst case
return -(table1(n, t, t_c) - BITS_GROUP_ORDER)DL has an (assumed) bit security of ~128, and to be on the safe side, we want that breaking LDVR is at least as hard, i.e., There's a cruder but much simpler restriction: As long as n <= 131, we'll always have effort >= 128. So a simple restriction (or recommendation) for the BIP could be simply to require n <= 131 (or maybe n <= 128 to be on the safe side and have a nicer looking number). I'm pretty sure that 128 parties will be enough for all envisioned currently envisioned use cases of FROST. |
|
@siv2r: Please let me know whether you want to go forward with the publication or are still making further updates. The recent review comments could also be addressed in a follow-up authored by another contributor. |
|
I'm working on fixing the recent review comments. Let's not publish until they're fixed. |
| This signing protocol is compatible with any key generation protocol that produces valid FROST keys. | ||
| Valid keys satisfy: (1) each *secret share* is a Shamir share of the *threshold secret key*, and (2) each *public share* equals the scalar multiplication *secshare \* G*. | ||
| Before signing, the signers context must pass *ValidateSignersCtx*, which rejects duplicate identifiers and confirms the key material reproduces the threshold public key. The signing algorithms (*Sign*, *PartialSigVerify*, and *PartialSigAgg*) include this check for clarity, so an implementation can instead validate a context once and skip the repeated checks in later calls. | ||
| For comprehensive validation of the entire key material, *ValidateSignersCtx* can be run on all possible *u* signing participants. |
There was a problem hiding this comment.
nit: Do you mean "on the full set of n participants (u = n)" here? Could be clarified.
edit: On a further look, this is much more sublte. Moved the discussion to the dev repo at siv2r/bip-frost-signing#43 (comment).
| - Let *tweak_ctx<sub>i</sub> = ApplyTweak(tweak_ctx<sub>i-1</sub>, tweak<sub>i</sub>, is_xonly_t<sub>i</sub>)*; fail if that fails | ||
| - Let *(Q, gacc, tacc) = tweak_ctx<sub>v</sub>* | ||
| - Let *ser_ids* = *SerializeIds(id<sub>1..u</sub>)*[^canonical-ids-det-sign] | ||
| - Let *b* = *scalar_from_bytes_wrapping(hash<sub>BIP0445/noncecoef</sub>(ser_ids || aggnonce || xbytes(Q) || m))* |
There was a problem hiding this comment.
ser_ids and m are of arbitrary length and not length prefixed when input to the noncecoef hash function. Hence, it seems easy to find tuples (ids, m) != (ids', m') that result in the same hash b.
There was a problem hiding this comment.
Oh, "nice" find. A simple fix is to let SerializeIds prefix its output with a serialization of u.
edit: Or, for consistency, make it similar to how the inputs to hashBIP0445/deterministic/nonce are serialized.
There was a problem hiding this comment.
Found by ChatGPT 5.6 when trying to prove security of the scheme specified in the BIP.
|
I completed a full pass on the text. Nothing substantial and sorry again that my comments arrive that late in the process. edit: Forgot to mention a "global" nit: The text uses "signer" and "participant" interchangeable. Sticking to one term would make things a tiny bit clearer (but this may be an annoying change to make at this stage). |
|
Tysm for the detailed review.
This was fixed in the development repo recently. "Participants" refers to anyone involved in key generation, and "signers" to those participating in the current signing session. Will pull in the latest changes once the other review comments are resolved. |
| - Inputs: | ||
| - The participant secret signing share *secshare*: a 32-byte array, serialized scalar (optional argument) | ||
| - The participant public share *pubshare*: a 33-byte array, compressed serialized point (optional argument) | ||
| - The x-only threshold public key *thresh_pk*: a 32-byte array, X-only serialized point (optional argument). When tweaks have been applied, this is ideally the tweaked threshold public key from *GetXonlyPubkey(tweak_ctx)*. |
There was a problem hiding this comment.
This is in the list of inputs to NonceGen.
It was a bit confusing to me that here the algorithms expect an xonly threshold pubkey as input whereas the signers_ctx requires a plain pubkey. (In particular because they use the same identifier thresh_pk.)
As far as I understand, the goal is to take account the tweaks for defense-in-depth reason (which is a good idea and an improvement over BIP327!).
I see two possible improvements:
- Make the wording and notation a bit clearer (see below for a possible wording).
- Or, instead of taking the xonly key, take all tweaks (and is_xonly_t values) and hash them.
| - The x-only threshold public key *thresh_pk*: a 32-byte array, X-only serialized point (optional argument). When tweaks have been applied, this is ideally the tweaked threshold public key from *GetXonlyPubkey(tweak_ctx)*. | |
| - The x-only threshold public key *tweaked_xonly_thresh_pk*: a 32-byte array, X-only serialized point (optional argument). If tweaks are supposed to be applied during signing, this should ideally be the resulting xonly public key after applying all tweaks to the threshold public key, i.e., the output of *GetXonlyPubkey(tweak_ctx)*. If no tweaks are supposed to be applied, or if it is not yet determined at nonce generation stage whether or what tweaks will be applied, this may also be the last 32 bytes of the threshold public key. |
There was a problem hiding this comment.
I thought about your second option, hashing all tweaks and is_xonly_t values. I believe it doesn't add any protection over hashing a single tweaked key, and the current x-only input is exactly as strong as both. So the choice is purely an API question. Here's my thinking:
All signing algorithms consume the tweak list only through the triple
Let's consider two different tweak sequences applied to the same threshold key and analyze when they collide on the NonceGen hash. Assume
Option 1: NonceGen hashes the plain tweaked key (PlainPk). A collision means the same
- Case 1 (same
$gacc$ ): the invariant forces the same$tacc$ , so both contexts produce byte-identical partial signatures. Deriving the same nonce for them is harmless. - Case 2 (opposite
$gacc$ ): the invariant forces$tacc' = tacc + 2 \cdot gacc \cdot x_0$ , where$x_0$ is the threshold secret key. Constructing such a pair requires knowing$x_0$ , and honest BIP32/BIP341 hash-derived tweaks hit it with probability$\sim 2^{-256}$ .
Option 2: NonceGen hashes the tweak and tweak mode lists (with count prefixing). Distinct sequences always get distinct nonces. The only pairs this separates beyond Option 1 are the benign ones from Case 1, so it adds no extra protection.
Option 3: NonceGen hashes the x-only tweaked key (XonlyPk, current spec). A collision means
- Case 1 (same
$gacc$ , same$Q_v$ ): same as Option 1, Case 1. - Case 2 (same
$gacc$ , opposite$Q_v$ ): the invariant forces$tacc' = -tacc - 2 \cdot gacc \cdot x_0$ , so it requires knowing$x_0$ , same reasoning as Option 1, Case 2. - Case 3 (opposite
$gacc$ , same$Q_v$ ): the invariant forces$tacc' = tacc + 2 \cdot gacc \cdot x_0$ , which is exactly Option 1, Case 2. - Case 4 (opposite
$gacc$ , opposite$Q_v$ ): the invariant forces$tacc' = -tacc$ . Here$g$ flips together with the parity of$Q_v$ , so$g \cdot gacc$ and$g \cdot tacc$ are unchanged, and both contexts produce byte-identical signatures for the same x-only key. Benign, like Case 1.
So all three options give equally good defense-in-depth. Given that, I'd prefer switching the argument from x-only to the plain, potentially tweaked key, i.e. the output of GetPlainPubkey(tweak_ctx). It matches the thresh_pk field in the Signers Context, matches what key generation outputs (ChillDKG returns a plain threshold pubkey), and lets the untweaked case pass thresh_pk through unchanged, which removes the confusion you pointed out. Plus the change is quite minimal compared to adding two new list params to NonceGen.

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