Skip to content

Add Musig2 module #716

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

Conversation

jlest01
Copy link
Contributor

@jlest01 jlest01 commented Jul 29, 2024

This PR adds a musig module based on bitcoin-core/secp256k1#1479.
The structure is based on @sanket1729's BlockstreamResearch/rust-secp256k1-zkp#48, but I removed the code related to adaptor signatures.

There is an example file in examples/musig.rs and can be run with cargo run --example musig --features "rand std".
The ffi functions were added to secp256k1-sys/src/lib.rs and the API level functions to the new src/musig.rs file.

Copy link
Collaborator

@Kixunil Kixunil left a comment

Choose a reason for hiding this comment

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

Awesome!!! I would wait until the upstream PR merges (and releases) before merging this but I'm looking forward to it. I gave it a quick look anyway.

src/musig.rs Outdated
// - Key agg cache is valid
// - extra input is 32 bytes
// This can only happen when the session id is all zeros
Err(MusigNonceGenError::ZeroSession)
Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO this should be just panic. It can only happen if someone passes wrong value to dangerous ID creation function.

@jlest01 jlest01 force-pushed the musig2-module branch 6 times, most recently from 447a94c to e730b8b Compare July 31, 2024 18:20
@jlest01 jlest01 force-pushed the musig2-module branch 3 times, most recently from a91d293 to 8bbd0d2 Compare August 29, 2024 11:39
@tcharding
Copy link
Member

This is a 10 thousand line diff, is something commited that shouldn't be?

@apoelstra
Copy link
Member

It updates the vendored library to bring in the upstream MuSig PR.

@jlest01
Copy link
Contributor Author

jlest01 commented Aug 29, 2024

It updates the vendored library to bring in the upstream MuSig PR.

Yes. For now, only the last three commits matter for review purposes.
The others will be discarded when the upstream MuSig PR is merged.

@tcharding
Copy link
Member

Cool, thanks. To clarify this is going to wait till upstream merges before being considered for merge, right? What sort of review are you chasing?

@Kixunil
Copy link
Collaborator

Kixunil commented Aug 30, 2024

@tcharding I will definitely not ack this until it's upstream is released. However I appreciate the experiment/demo.

@jlest01 jlest01 force-pushed the musig2-module branch 3 times, most recently from 0a2361b to 86e2b28 Compare August 30, 2024 22:13
@jlest01
Copy link
Contributor Author

jlest01 commented Aug 31, 2024

To clarify this is going to wait till upstream merges before being considered for merge, right? What sort of review are you chasing?

Yes, the idea is to wait for the upstream PR to be merged.
Regarding the review, I mean that the last three commits are the ones that are intended to be merged.

impl MusigSecNonce {
pub fn new() -> Self {
MusigSecNonce([0; MUSIG_SECNONCE_LEN])
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't this highly misleading? If it's all-zeros it's not a nonce and thus broken. Where would one need it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as here: #716 (comment)

MusigSecNonce([0; MUSIG_SECNONCE_LEN])
}

/// Don't use this. Refer to the documentation of wrapper APIs in the crate.
Copy link
Collaborator

Choose a reason for hiding this comment

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

The documentation of these methods is intended for the higher-level API implementors not for for end consumers so it should rather properly describe what's going on here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Thanks.

impl_raw_debug!(MusigPubNonce);

impl MusigPubNonce {
pub fn new() -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks also broken.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as here: #716 (comment)

fn default() -> Self {
Self::new()
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks to me that none of these Defaults should exist. People should just use arrays or MaybeUninit<T> to represent the uninitialized state.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you suggesting something like this ?

let key_agg_cache = MaybeUninit::<ffi::MusigKeyAggCache>::uninit();
let mut key_agg_cache = key_agg_cache.assume_init();

This will cause UB (without MaybeUninit::write).
The reason for pub fn new() is that the internal array is private (ex: pub struct MusigKeyAggCache([c_uchar; MUSIG_KEYAGG_LEN]);), which is consistent with the other structs in the code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

No, provide a function that constructs initialized types only.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, now I see that I was confused because these are the FFI structs. However, I still maintain they are highly confusing.

The correct usage (inside secp256k1::musig::MusigKeyAggCache::new) is this:

let mut key_agg_cache = MaybeUninit::<ffi::MusigKeyAggCache>::uninit();
let mut agg_pk = MaybeUninit::<ffi::XOnlyPublicKey>::uninit();
unsafe {
    if ffi::secp256k1_musig_pubkey_agg(
        cx,
        agg_pk.as_mut_ptr(),
        key_agg_cache.as_mut_ptr(),
        pubkeys.as_ptr(),
        pubkey_ptrs.len(),
    ) == 0 {
        panic!(...);
    } else {
        // secp256k1_musig_pubkey_agg overwrites the cache and the key so this is sound.
        let key_agg_cache = key_agg_cache.assume_init();
        let agg_pk = agg_pk.assume_init();
        MusigKeyAggCache(key_agg_cache, 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.

Thanks for the clarification.
Done in 2ea5674

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've also applied the same approach to the other structs.


#[repr(C)]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MusigPartialSignature([c_uchar; MUSIG_PART_SIG_LEN]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

FTR these struct declarations looked wrong but are indeed correct based on the current API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you think they should be changed?

src/musig.rs Outdated
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub enum ParseError {
/// Length mismatch
ArgLenMismatch {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We usually name these InvalidLength.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Thanks.

@sanket1729
Copy link
Member

Upstream was released yesterday

@apoelstra
Copy link
Member

Can you rebase and format each commit with the nightly formatter? That should fix CI.

@jlest01
Copy link
Contributor Author

jlest01 commented Nov 7, 2024

Can you rebase and format each commit with the nightly formatter? That should fix CI.

Yes, done. Thanks.

@tcharding
Copy link
Member

tcharding commented Nov 11, 2024

Patch 1 can be removed now, right? Then your shellcheck CI fail should disappear.

src/musig.rs Outdated
///
/// MuSig differs from regular Schnorr signing in that implementers _must_ take
/// special care to not reuse a nonce. If you cannot provide a `sec_key`, `session_secrand`
/// UNIFORMLY RANDOM AND KEPT SECRET (even from other signers). Refer to libsecp256k1-zkp

Choose a reason for hiding this comment

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

The references to libsecp256k1-zkp and secp256k1-zkp in the comments below seem to be left over from Sanket's original PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. Good catch.
Updated in 4012ed5

@stevenroose
Copy link
Contributor

What is the status of this PR?

@Kixunil
Copy link
Collaborator

Kixunil commented Mar 5, 2025

@stevenroose it needs reviewers, I guess. :) This is quite difficult to review. I really wanted to but it requires a significant chunk of undisrupted time and I had more pressing things to do. I suspect I might be able to do it Friday. The good news is over the previous reviews and some more digging I got to understand Musig much better so it should be easier now.

Comment on lines +20 to +25
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub enum ParseError {
/// Parse Argument is malformed. This might occur if the point is on the secp order,
/// or if the secp scalar is outside of group order
MalformedArg,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't it make more sense to use Option in the from_byte_array functions? The underlying functions do not provide information about why they failed anyway, so this type is not too useful (unless it is expected to be expanded in the future, in which case it should be #[non_exhaustive]).

Copy link
Member

Choose a reason for hiding this comment

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

IMO we should make it #[non_exhaustive].

Copy link
Collaborator

Choose a reason for hiding this comment

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

Even if we didn't intend to expand it, an empty error type is often better than Option (which is the case here since passing malformed argument is clearly an error as opposed to stuff like passing non-existing key to get function on a map).

Copy link
Contributor

@jirijakes jirijakes left a comment

Choose a reason for hiding this comment

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

As an exercise, I am adding test vectors from BIP327, so far they revealed this. Will continue tomorrow.

Comment on lines +220 to +221
pub fn serialize(&self) -> [u8; 32] {
let mut data = MaybeUninit::<[u8; 32]>::uninit();
Copy link
Contributor

@jirijakes jirijakes Mar 18, 2025

Choose a reason for hiding this comment

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

Suggested change
pub fn serialize(&self) -> [u8; 32] {
let mut data = MaybeUninit::<[u8; 32]>::uninit();
pub fn serialize(&self) -> [u8; ffi::MUSIG_PART_SIG_SERIALIZED_LEN] {
let mut data = MaybeUninit::<[u8; ffi::MUSIG_PART_SIG_SERIALIZED_LEN]>::uninit();

(Edited, previously suggested wrong constant)

Copy link
Contributor

@jirijakes jirijakes left a comment

Choose a reason for hiding this comment

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

Correcting my previous review comments.

Partial signature occupies 36 bytes in memory but it is 32 in serialized form.

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AggregatedSignature([u8; 64]);

impl AggregatedSignature {
Copy link
Contributor

Choose a reason for hiding this comment

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

This one is missing from_byte_array.

pub const MUSIG_AGGNONCE_SERIALIZED_LEN: usize = 66;
pub const MUSIG_PUBNONCE_SERIALIZED_LEN: usize = 66;
pub const MUSIG_SESSION_LEN: usize = 133;
pub const MUSIG_PART_SIG_LEN: usize = 36;
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
pub const MUSIG_PART_SIG_LEN: usize = 36;
pub const MUSIG_PART_SIG_LEN: usize = 36;
pub const MUSIG_PART_SIG_SERIALIZED_LEN: usize = 32;

FTR, I previously suggested changing MUSIG_PART_SIG_LEN to 32 but that is wrong. It really holds 36 bytes in memory, however, it is 32 bytes in serialized form. Therefore need for another constant.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Comments on the constants explaining this would be great too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Fixed the serialized size and also added comments explaining the constants.

src/musig.rs Outdated
/// # Errors:
///
/// - MalformedArg: If the signature [`PartialSignature`] is out of curve order
pub fn from_byte_array(data: &[u8; ffi::MUSIG_PART_SIG_LEN]) -> Result<Self, ParseError> {
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
pub fn from_byte_array(data: &[u8; ffi::MUSIG_PART_SIG_LEN]) -> Result<Self, ParseError> {
pub fn from_byte_array(data: &[u8; ffi::MUSIG_PART_SIG_SERIALIZED_LEN]) -> Result<Self, ParseError> {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@jirijakes
Copy link
Contributor

jirijakes commented Mar 19, 2025

At some point, it would be useful to somehow have a way of creating SecretNonce from bytes, which is needed for testing, either of this module via test vectors or for downstream users.


The upstream does not provide parse function (there is no standard serialization) but it also needs to be able to create SecretNonces for its tests.

Test vectors contain secnonce as 97-byte, see footnote 11:

The algorithms as specified here assume that the secnonce is stored as a 97-byte array using the serialization secnonce = bytes(32, k1) || bytes(32, k2) || pk. 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.

Internal memory representation of secnonce has 132 bytes:

  • 4 bytes: magic 0x22, 0x0e, 0xdc, 0xf1
  • 32 bytes: k1
  • 32 bytes: k2
  • 64 bytes: group element of pk (is it curve coordinates?)

And this is how libsecp256k1 converts the 97-byte values from tests into 132-byte secnonce: loads public key in musig_test_set_secnonce and prepends magic in secp256k1_musig_secnonce_save.

@Kixunil
Copy link
Collaborator

Kixunil commented Mar 19, 2025

We should just generate them from session. We don't have to use every single test vector since upstream already uses them.

@stevenroose
Copy link
Contributor

How about doing a rc for the release containing this? I suspect there are quite some teams already using some flavor of this code in some ways (I am in two different ways on sanket's old branch and on this new branch), so maybe when they move over they can provide feedback while they might not have time to review.

@stevenroose
Copy link
Contributor

I can already comment about the SecretNonce that the previous iteration that had the serialization functions marked "dangerous" were useful. We have to store a signing session in our database so we have to serialize the nonce and deserialize it later so we can finish the session.

This can be secure because all the other parameters of the session are already known at that time. Why don't we finish the session and store the signtature? Because we have potentially hundreds of thousands of them at the same time and might not have to use any of the signatures, so storing everything at least an order of magnitude faster than signing :)

@jlest01
Copy link
Contributor Author

jlest01 commented Apr 9, 2025

@stevenroose I believe you are referring to the function below

impl SecretNonce {
    // ...
    pub fn dangerous_into_bytes(self) -> [c_uchar; MUSIG_SECNONCE_LEN] {
        self.0
    }
}

If I recall, it was removed due to a review.
It can be reintroduced if necessary.

@jirijakes Does this also suit your case?

@jirijakes
Copy link
Contributor

Does this also suit your case?

I did not have a particular case in mind. My idea was that it might be needed to create secret nonces from raw bytes (and apparently also the other way), however I do not have a specific need myself.

@apoelstra apoelstra mentioned this pull request Apr 16, 2025
@apoelstra
Copy link
Member

Yeah, we should retain the methods (though they still should be marked dangerous!). I asked on IRC if there's a better way to do what Steven is suggesting and the designers of MuSig basically said "no, but this is exactly what we tell people not to do and it's dangerous".

So there isn't really an alternative -- and even if we take away a byte-conversion method there are other ways to get the bytes (e.g. by transmuting). I think as long as both sides of the conversion are clearly marked dangerous the methods should exist in the API.

@michael1011
Copy link

I think as long as both sides of the conversion are clearly marked dangerous the methods should exist in the API.

Agreed. Can be very useful and not having those methods makes the life of the library user unnecessarily hard

@Kixunil
Copy link
Collaborator

Kixunil commented Apr 19, 2025

This can be secure because all the other parameters of the session are already known at that time.

As I understand it, this is not true. There are attacks involving reentrancy where the attacker sends multiple different nonces. If you want to make it secure, you need to store it in a separate database that is never backed up, is locked such that only a single piece of code/thread can operate on a secret and makes sure to wipe it from DB once it's consumed.

I think the method is not strictly required - one can already store the session and, if the same inputs are provided, same secret should get generated from it. But it is arguably more annoying and I'm not opposed to having the dangerous_ method as long as the danger is correctly explained in detail.

Side note: I have a different case where it'd be nice to have a bunch of nonces generated deterministically from a seed which gets handled the way I describe above. Having a constructor for that would be likely helpful but I don't remember all the details to be certain that I don't need the session anyway. Makes me wonder if such approach would be useful to you too @stevenroose

@apoelstra
Copy link
Member

I think what @stevenroose is doing is a fair bit less dangerous than what you're describing -- in his use case all the nonces have already been determined. Everything has been done except computation of the final signature; all the inputs have been determined and the signature is also determined. He's just deferring the heavy computation until he's sure that he's necessary.

What you want to do (precomputing nonces) is much harder, for all the reasons you list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.