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

Merged
merged 6 commits into from
May 15, 2025
Merged

Add Musig2 module #716

merged 6 commits into from
May 15, 2025

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)

Choose a reason for hiding this comment

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

This seems like a huge footgun.

Copy link
Member

Choose a reason for hiding this comment

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

It's also not present in the current version of the PR.

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.

@jlest01
Copy link
Contributor Author

jlest01 commented May 1, 2025

The new push addresses the latest comments and reintroduces the dangerous_bytes functions for secret nonces as requested.

@apoelstra
Copy link
Member

@Kixunil my plan is to review this this week and put my comments in a followup issue/PR (unless there's something really egregious) and just merge this. It's been nearly a year and 161 comments. I know this is a huge change but I'd like to move forward.

@jlest01 thanks so much for your continued patience!

cx: *const Context,
secnonce: *mut MusigSecNonce,
pubnonce: *mut MusigPubNonce,
session_secrand32: *const c_uchar,
Copy link
Member

Choose a reason for hiding this comment

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

In baa26350fcc107a9e9c143bbefead679504225cd:

(will fix in followup) this const needs to be mut, and your usage of it in the code needs to be changed to use a &mut rather than & pointer. (This is actual UB so fairly high priority for us to fix, but it's simple so I'll do it.)

Copy link
Member

Choose a reason for hiding this comment

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

In f384a2d I meant.

pubnonces: *const *const MusigPubNonce,
n_pubnonces: size_t,
) -> c_int;

Copy link
Member

Choose a reason for hiding this comment

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

In f384a2d:

I (or rather, Claude) noticed that you don't have bindings to secp256k1_musig_nonce_gen_counter. Should I add one?

Copy link
Member

Choose a reason for hiding this comment

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

I'm gonna say no. Easy to add later, and this method is kinda scary.

#[cfg_attr(not(rust_secp_no_symbol_renaming), link_name = "rustsecp256k1_v0_11_ec_pubkey_sort")]
pub fn secp256k1_ec_pubkey_sort(
ctx: *const Context,
pubkeys: *mut *const PublicKey,
Copy link
Member

Choose a reason for hiding this comment

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

In f384a2d:

This *mut should be *const. (This one is harmless, but we ought to fix it.)

/// # pubkey_sort(&secp, pubkeys_ref);
/// # }
/// ```
pub fn pubkey_sort<C: Verification>(secp: &Secp256k1<C>, pubkeys: &mut [&PublicKey]) {
Copy link
Member

Choose a reason for hiding this comment

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

In e90015e:

Would prefer this be called sort_pubkeys and be a method on Secp256k1. (Will do in followup.)

///
/// # Errors:
///
/// * `ZeroSession`: if the `session_secrand` is supplied is all zeros.
Copy link
Member

Choose a reason for hiding this comment

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

In dd26c30:

(Will fix in followup) there is no such error anymore. (Same below.)

///
/// In rand-std environment, [`SessionSecretRand::new`] can be used to generate a random
/// session id using thread rng.
pub fn assume_unique_per_nonce_gen(inner: [u8; 32]) -> Self { SessionSecretRand(inner) }
Copy link
Member

Choose a reason for hiding this comment

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

In dd26c30:

I think you should panic here if the user provides all 0s. Will probably be easier to diagnose than them getting a panic later when signing. (I wouldn't bother returning a Result or something ... this function is supposed to be passed the result of a cryptographic hash or RNG or something.) (Will do in followup.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably OK to do it later but this kind of thing tends to come up in cryptography more often so maybe we should have a newtype for it.

Copy link
Member

Choose a reason for hiding this comment

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

Hard to say. This function is pretty-explicitly "I got these bytes from somewhere and they're legit, promise" so it feels both paternalistic and insufficient to provide a newtype that does a single validity check on it.

Since the upstream library will do bad things if you provide all zeros, I think we should catch that, but I think that immediately panicking is probably the least bad thing to do.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I mean we could just rename the function to be less scary and make the constructor of the newtype scary with the purpose that various RNG libraries can construct it. But maybe it's not worth it here.

Copy link
Member

Choose a reason for hiding this comment

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

I think in this case we should just keep it as-is. We sorta tried this with the ThirtyTwoByteHash trait and it wound up just causing a lot of unnecessary and frustrating coupling between libraries, and also the "per-nonce" uniqueness constraint here is stronger than what a rng library can necessarily produce.

/// # Returns
///
/// 32-byte array
pub fn serialize(&self) -> [u8; 32] {
Copy link
Member

Choose a reason for hiding this comment

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

In dd26c30:

+1 to using the constant rather than just 32. Also I would like to rename this to to_byte_array for symmetry. We should do this across the codebase, but for now let's just do it with new code. (Will do in followup.)

Copy link
Member

Choose a reason for hiding this comment

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

Same rename for PublicNonce.

/// let aggnonce = AggregatedNonce::new(&secp, &[pub_nonce1, pub_nonce2]);
/// # }
/// ```
pub fn new<C: Signing>(secp: &Secp256k1<C>, nonces: &[&PublicNonce]) -> Self {
Copy link
Member

Choose a reason for hiding this comment

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

In dd26c30:

Needs a Panics section of the docs (will do in followup).

/// This is useful to reduce the communication between signers, because instead
/// of everyone sending nonces to everyone else, there can be one party
/// receiving all nonces, combining the nonces with this function and then
/// sending only the combined nonce back to the signers.
Copy link
Member

Choose a reason for hiding this comment

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

In dd26c30:

In the upstream docs there's an extra line clarifying that if the aggregator lies, the signature will just be invalid. We should copy that here. (Will do in followup.)

/// let _agg_pk = key_agg_cache.agg_pk();
/// # }
/// ```
pub fn new<C: Verification>(secp: &Secp256k1<C>, pubkeys: &[&PublicKey]) -> Self {
Copy link
Member

Choose a reason for hiding this comment

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

In dd26c30:

What if you pass an empty array of pubkeys? I'll play with this. Probably I'll just add an explict panic, though given that we have the same issue with partial sigs, I'm tempted to add an error variant for this, or maybe a nonemptyarray newtype. Will address in followup, at least to add a panic (I suspect the existing code will do weird potentially-UB things) but I might wind up just filing an issue.

Copy link
Member

Choose a reason for hiding this comment

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

For your curiosity, what happens is that we try to pass a bad pointer to the C code, but our alignment trickery in secp256k1-sys catches it and panics (though I doubt the panic is very reliable). So on my system it's not actually UB, but it's pretty close.

src/musig.rs Outdated
/// Generates a new session ID using thread RNG.
#[cfg(all(feature = "rand", feature = "std"))]
pub fn new() -> Self {
Self::from_rng(&mut rand::thread_rng())
Copy link
Member

Choose a reason for hiding this comment

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

In dd26c30:

Clippy is complaining about there being new but not Default. I think it's got a point, and that we should rename this function from_thread_rng to make it more obvious that it's not a pure function. Will do in a followup.

@apoelstra apoelstra dismissed Kixunil’s stale review May 14, 2025 23:44

I believe your comments have been addressed, and your review is blocking merge. I went through them all even though Github has literally hidden them and marked every one as "outdated". If I'm wrong I'd appreciate a heads up and for you to re-open your comments, maybe on my followup PR.

Copy link
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

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

ACK c6fc3d7; successfully ran local tests

@apoelstra apoelstra merged commit 4d36fef into rust-bitcoin:master May 15, 2025
27 of 30 checks passed
@apoelstra
Copy link
Member

Will open a followup PR tomorrow.

apoelstra pushed a commit to apoelstra/rust-secp256k1 that referenced this pull request May 15, 2025
Pretty-much all of the doctests included in rust-bitcoin#716 corresponded to an old
version of the API. I'm unsure why it is that my local CI accepted this.
The Github CI did not.
apoelstra added a commit to apoelstra/rust-secp256k1 that referenced this pull request May 16, 2025
Pretty-much all of the doctests included in rust-bitcoin#716 corresponded to an old
version of the API. I'm unsure why it is that my local CI accepted this.
The Github CI did not.
apoelstra added a commit to apoelstra/rust-secp256k1 that referenced this pull request May 16, 2025
Pretty-much all of the doctests included in rust-bitcoin#716 corresponded to an old
version of the API. I'm unsure why it is that my local CI accepted this.
The Github CI did not.
apoelstra added a commit to apoelstra/rust-secp256k1 that referenced this pull request May 16, 2025
Pretty-much all of the doctests included in rust-bitcoin#716 corresponded to an old
version of the API. I'm unsure why it is that my local CI accepted this.
The Github CI did not.
apoelstra added a commit to apoelstra/rust-secp256k1 that referenced this pull request May 16, 2025
Pretty-much all of the doctests included in rust-bitcoin#716 corresponded to an old
version of the API. I'm unsure why it is that my local CI accepted this.
The Github CI did not.
@apoelstra
Copy link
Member

#794

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.