Skip to content
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

Musig2 support #48

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

Conversation

sanket1729
Copy link
Member

@sanket1729 sanket1729 commented Mar 21, 2022

This was taken by #29 , but significantly changed. In particular, I was super strict about adding unreachable! to make the APIs not return a result when possible. Each API has a doc test with guidelines of using it.

Things to pay special attention to:

  • Cross-check the use of unreachable! in the FFI calls to make sure they cannot panic for any other reasons.
  • Review the documentation about the usage of session_id and nonce re-use.

Edit: I wanted to add a extra-rand with time of day example, but doing so in the current MSRV is non-trivial. Will add an example after MSRV update

@sanket1729 sanket1729 changed the title Comment out Wasm build on CI Musig2 support Mar 21, 2022
@sanket1729 sanket1729 marked this pull request as ready for review April 15, 2022 20:02
@sanket1729
Copy link
Member Author

Marked as ready for review.

@sanket1729
Copy link
Member Author

sanket1729 commented Apr 15, 2022

Resolved

Copy link
Collaborator

@jonasnick jonasnick left a comment

Choose a reason for hiding this comment

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

I have only read through half the code yet but looks nice so far.

contrib/test.sh Outdated Show resolved Hide resolved
secp256k1-zkp-sys/src/zkp.rs Outdated Show resolved Hide resolved
src/zkp/musig.rs Outdated
/// let keypair2 = KeyPair::new(&secp, &mut thread_rng());
/// let pub_key2 = XOnlyPublicKey::from_keypair(&keypair2);
///
/// let (_key_agg_cache, _agg_pk) = MusigKeyAggCache::new(&secp, &[pub_key1, pub_key2]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are situations when you don't need the _key_agg_cache. I suppose this is the idiomatic way to do this in rust, but it looks a bit weird to call MusigKeyAggCache::new just to get the _agg_pk. An alternative API could just return the single _key_agg_cache that has a getter for the aggregate key (see secp256k1_musig_pubkey_get in the C code).

Copy link
Member Author

Choose a reason for hiding this comment

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

It should not be necessary to call the FFI in this case right?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't understand my suggestion anymore. I think I was mainly complaining about having to call something ominously named MusigKeyAggCache::new if you just want an aggregate key. I don't see how my suggestion improves the situation.

src/zkp/musig.rs Outdated Show resolved Hide resolved
src/zkp/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 `session_id`, or `extra_rand`
/// UNIFORMLY at random, refer to libsecp256k1-zkp documentation for additional considerations.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Fwiw extra_rand generally does not need to be uniformly random which makes this sound a bit confusing. Also, what is missing is that in the mode that you're recommending here (session_id uniformly random), the session_id must also be kept secret.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we also rename extra_rand? the libsecp implement calls it extra_input, the spec draft extra_in.

src/zkp/musig.rs Outdated
/// let sec_key = SecretKey::from_keypair(&keypair1);
/// let msg = Message::from_slice(&[3; 32]).unwrap();
/// let mut extra_rand = [0u8; 32];
/// thread_rng().fill_bytes(&mut extra_rand);
Copy link
Collaborator

Choose a reason for hiding this comment

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

extra_rand is there to help if session_id is a repeated value. In this example, if thread_rng is broken then extra_rand will not add anything. You may want to add system time, etc here but that seems like security theater. One legit thing to include as extra_rand is the other signer's nonces (if you know them already).

Copy link
Member Author

Choose a reason for hiding this comment

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

Will add an example here that uses time

src/zkp/musig.rs Outdated Show resolved Hide resolved
Copy link
Collaborator

@jonasnick jonasnick left a comment

Choose a reason for hiding this comment

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

First review pass done. I wonder if it is worth having so much redundancy in the doc examples versus just having a single example that shows an entire signing session.

src/zkp/musig.rs Outdated Show resolved Hide resolved
src/zkp/musig.rs Outdated Show resolved Hide resolved
src/zkp/musig.rs Outdated Show resolved Hide resolved
src/zkp/musig.rs Outdated Show resolved Hide resolved
src/zkp/musig.rs Outdated Show resolved Hide resolved
Comment on lines +1102 to +999
) == 0
{
// This can only crash if the individual nonces are invalid which is not possible is rust.
// Note that even if aggregate nonce is point at infinity, the musig spec sets it as `G`
unreachable!("Public key nonces are well-formed and valid in rust typesystem")
Copy link
Collaborator

Choose a reason for hiding this comment

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

nonce_agg will also fail if none_ptrs.len() == 0. The reason is that there's no aggnonce to return in that case.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch. I think it is okay to panic in this case.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Followup: The term "Public key nonces" is confusing, perhaps just "nonces"?

src/zkp/musig.rs Outdated Show resolved Hide resolved
justinmoon added a commit to justinmoon/fedimint that referenced this pull request May 7, 2022
Copy link
Member Author

@sanket1729 sanket1729 left a comment

Choose a reason for hiding this comment

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

Sorry for the delay @jonasnick. Addressed the reviews.

contrib/test.sh Outdated Show resolved Hide resolved
src/zkp/musig.rs Outdated
/// let keypair2 = KeyPair::new(&secp, &mut thread_rng());
/// let pub_key2 = XOnlyPublicKey::from_keypair(&keypair2);
///
/// let (_key_agg_cache, _agg_pk) = MusigKeyAggCache::new(&secp, &[pub_key1, pub_key2]);
Copy link
Member Author

Choose a reason for hiding this comment

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

It should not be necessary to call the FFI in this case right?

src/zkp/musig.rs Outdated
/// let sec_key = SecretKey::from_keypair(&keypair1);
/// let msg = Message::from_slice(&[3; 32]).unwrap();
/// let mut extra_rand = [0u8; 32];
/// thread_rng().fill_bytes(&mut extra_rand);
Copy link
Member Author

Choose a reason for hiding this comment

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

Will add an example here that uses time

src/zkp/musig.rs Outdated Show resolved Hide resolved
src/zkp/musig.rs Outdated Show resolved Hide resolved
src/zkp/musig.rs Outdated Show resolved Hide resolved
Comment on lines +1102 to +999
) == 0
{
// This can only crash if the individual nonces are invalid which is not possible is rust.
// Note that even if aggregate nonce is point at infinity, the musig spec sets it as `G`
unreachable!("Public key nonces are well-formed and valid in rust typesystem")
Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch. I think it is okay to panic in this case.

src/zkp/musig.rs Outdated Show resolved Hide resolved
@Tibo-lg
Copy link
Contributor

Tibo-lg commented Jun 23, 2022

Thanks for providing this! I played a bit with it and I'm curious about the reason for having separate Error enums. It feels a bit impractical to me as for example if I want to easily convert them to my library error type I need to implement From many times. Or maybe there is a specific reason behind this choice?

I also spotted quite some typos in the docs but not sure if it's the right time to be nitpicking (if it is let me know I can point them out).

Otherwise it's really neat :)!

@sanket1729
Copy link
Member Author

Or maybe there is a specific reason behind this choice?

@Tibo-lg Indeed. IMO, the return type of the function should only return the corresponding error variants that are expected from the function. If we collect all of them into a single enum, it is would impossible for the caller to know which variants are reachable for the corresponding function. This is handy for callers that want to act on an error instead of propagating forward.

match key_agg_cache.pubkey_ec_tweak_add(&secp, tweak) {
	Ok(_) => {},
	Err(MusigTweakError::InvalidTweak) => panic!("not possible in bip32 tweaking"),
}

If there is a single variant with everything, the caller has no choice but to propagate it forward. Unfortunately, there is a tradeoff that the implementors that want to forward the error instead of dealing with it would have more code to deal with. But overall, I think the tradeoff is clear as it clearly expresses the conditions the function errors with and allows sanely dealing with errors.

I also spotted quite some typos in the docs but not sure if it's the right time to be nitpicking (if it is let me know I can point them out).

Corrections welcome.

@sanket1729 sanket1729 force-pushed the pr29 branch 2 times, most recently from 0c87d00 to c817be6 Compare May 1, 2023 20:53
src/zkp/musig.rs Outdated
///
/// Musig2 nonces can be precomputed without knowing the aggregate public key, the message to sign.
/// However, for maximal mis-use resistance, this API requires user to have already
/// have [`SecretKey`], [`Message`] and [`MusigKeyAggCache`]. See the `new_nonce_pair` method
Copy link

Choose a reason for hiding this comment

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

have [SecretKey]

nonce_gen() appears to take a public key, not a secret key.

@stevenroose
Copy link
Contributor

@sanket1729 sanket1729 changed the title Comment out Wasm build on CI Musig2 support Mar 21, 2022

Well that escalated quickly.. I'm super excited about this though, guys!

@Ademan
Copy link

Ademan commented Jul 28, 2023

It would be nice to have serde serialization for pubnonces and partial signatures. (I may provide a patch for that)

@ssantos21
Copy link

ssantos21 commented Sep 5, 2023

Exposing the inner bytes of ffi::MusigSecNonce and ffi::MusigSession would allow to serialize and store them.

secp256k1-zkp-sys/src/zkp.rs

 #[repr(C)]
 #[derive(Copy, Clone)]
-pub struct MusigSecNonce([c_uchar; MUSIG_SECNONCE_LEN]);
+pub struct MusigSecNonce(pub [c_uchar; MUSIG_SECNONCE_LEN]);
 impl_array_newtype!(MusigSecNonce, c_uchar, MUSIG_SECNONCE_LEN);
 impl_raw_debug!(MusigSecNonce);
 
@@ -1008,7 +1008,7 @@ impl MusigAggNonce {
 
 #[repr(C)]
 #[derive(Copy, Clone)]
-pub struct MusigSession([c_uchar; MUSIG_SESSION_LEN]);
+pub struct MusigSession(pub [c_uchar; MUSIG_SESSION_LEN]);
 impl_array_newtype!(MusigSession, c_uchar, MUSIG_SESSION_LEN);
 impl_raw_debug!(MusigSession);

src/zkp/musig.rs

 use crate::{Signing, Verification};
+use ffi::{MUSIG_SECNONCE_LEN, MUSIG_SESSION_LEN};
 use secp256k1::Parity;

impl MusigSecNonce {
     pub fn as_mut_ptr(&mut self) -> *mut ffi::MusigSecNonce {
         &mut self.0
     }
+
+    /// Function to return a copy of the internal array
+    pub fn serialize(&self) -> [u8; MUSIG_SECNONCE_LEN] {
+        let ffi::MusigSecNonce(array) = &self.0;
+        *array
+    }
+
+    /// Function to create a new MusigKeyAggCoef from a 32 byte array
+    pub fn from_slice(array: [u8; MUSIG_SECNONCE_LEN]) -> Self {
+        MusigSecNonce(ffi::MusigSecNonce(array))
+    }
 }
 
 impl CPtr for MusigSession {
     type Target = ffi::MusigSession;
@@ -1671,6 +1695,17 @@ impl MusigSession {
     pub fn as_mut_ptr(&mut self) -> *mut ffi::MusigSession {
         &mut self.0
     }
+
+    /// Function to return a copy of the internal array
+    pub fn serialize(&self) -> [u8; MUSIG_SESSION_LEN] {
+        let ffi::MusigSession(array) = &self.0;
+        *array
+    }
+
+    /// Function to create a new MusigKeyAggCoef from a 32 byte array
+    pub fn from_slice(array: [u8; MUSIG_SESSION_LEN]) -> Self {
+        MusigSession(ffi::MusigSession(array))
+    }
 }

Copy link

@ssantos21 ssantos21 left a comment

Choose a reason for hiding this comment

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

The functions pubkey_xonly_tweak_add and secp256k1_musig_pubkey_xonly_tweak_add have the wrong parameter (XOnlyPublicKey instead of PublicKey).

This causes an error when verifying a signature of a tweaked key. Changing them to use the expected parameter fixes this.

)]
pub fn secp256k1_musig_pubkey_xonly_tweak_add(
cx: *const Context,
output_pubkey: *mut XOnlyPublicKey,

Choose a reason for hiding this comment

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

secp256k1_musig_pubkey_xonly_tweak_add expects a PublicKey, not XOnlyPublicKey.
This is causing signature verification to fail.

Suggested change
output_pubkey: *mut XOnlyPublicKey,
output_pubkey: *mut PublicKey,

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch.

src/zkp/musig.rs Outdated
Comment on lines 254 to 257
) -> Result<XOnlyPublicKey, MusigTweakErr> {
let cx = secp.ctx().as_ptr();
unsafe {
let mut out = XOnlyPublicKey::from(ffi::XOnlyPublicKey::new());

Choose a reason for hiding this comment

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

Suggested change
) -> Result<XOnlyPublicKey, MusigTweakErr> {
let cx = secp.ctx().as_ptr();
unsafe {
let mut out = XOnlyPublicKey::from(ffi::XOnlyPublicKey::new());
) -> Result<PublicKey, MusigTweakErr> {
let cx = secp.ctx().as_ptr();
unsafe {
let mut out = PublicKey::from(ffi::PublicKey::new());

src/zkp/musig.rs Outdated
///
/// # Returns
///
/// A pair ([`MusigKeyAggCache`], [`XOnlyPublicKey`]) where the first element is the `key_agg_cache`.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is no longer true.

@sanket1729
Copy link
Member Author

Hello everyone. Sorry for the delay in working on this.

@ssantos21, there was a good security reason to disallow serialization APIs for SecNonce. I have prefix dangerous to those in the name and doc-comment explaining the reason. I am yet sure the use-case of serializing sessions, but we can do that as a follow-up PR.

I suggest we review this PR for correctness, any incomplete serialization features can be added as a followup.

Ready for review. cc @stevenroose @jonasnick @Ademan @ssantos21 @apoelstra

Signed-off-by: Sanket Kanjalkar <sanketk@squareup.com>
/// Refer to libsecp256k1-zkp documentation for additional considerations.
///
/// Musig2 nonces can be precomputed without knowing the aggregate public key, the message to sign.
/// See the `new_nonce_pair` method that allows generating [`MusigSecNonce`] and [`MusigPubNonce`]
Copy link

Choose a reason for hiding this comment

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

The new_nonce_pair method doesn't seem to exist?

Additionally, such a method should probably include the the optional extra_rand field as in this case you might well want to use it for extra protection against accidental misuse?

Choose a reason for hiding this comment

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

ahh I see it's actually called new_musig_nonce_pair now so just a doc-comment issue then

@stevenroose
Copy link
Contributor

Not actively reviewing, but trying to use this.

The new_musig_nonce_pair function has this sentence in the documentation that is not correct english and confuses me:

If you cannot provide a sec_key, session_id UNIFORMLY RANDOM AND KEPT SECRET (even from other signers).

This makes me wonder, the name "SessionId" somehow made me thing it had to be shared between everyone in the same signing session. But from that sentence maybe it seems that it should be locally random and not shared?

secp: &Secp256k1<C>,
mut secnonce: MusigSecNonce,
keypair: &Keypair,
key_agg_cache: &MusigKeyAggCache,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this need to be provided twice? It's already provided in the constructor..

/// #
/// let key_agg_cache = MusigKeyAggCache::new(&secp, &[pub_key1, pub_key2]);
/// // The session id must be sampled at random. Read documentation for more details.
/// let session_id = MusigSessionId::new(&mut thread_rng());
Copy link
Contributor

Choose a reason for hiding this comment

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

This method doesn't exist anymore and seems to be renamed to assume_unique_per_nonce_gen.

pub fn nonce_gen<C: Signing>(
&self,
secp: &Secp256k1<C>,
session_id: MusigSessionId,
Copy link
Contributor

Choose a reason for hiding this comment

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

MusigSessionId is not Copy and it has to be kept around for partial signature generation, right? So this API should take a ref, I suppose.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I seem to be mistaken. The session ID is not needed to create a MusigSession at all. So probably ignore this, but maybe double check :)

@stevenroose
Copy link
Contributor

Are you planning serde serialization for the types?

@stevenroose
Copy link
Contributor

stevenroose commented Jan 18, 2024

Suggestion: change the MusigKeyAggCache pubkey argument to an iterator so that the user doesn't necessarily have to allocate.

    pub fn new<C: Verification>(secp: &Secp256k1<C>, pubkeys: &[PublicKey]) -> Self {

EDIT: Same goes for nonces. I've been using this for a bit and it seems that a common situation is that you have "all other pubkeys" or "all other nonces", plus your own one and then you either have to re-allocate just to add your own, where with iterators, you could just .iter().chain(Some(&mine)).

So the argument would be impl IntoIterator<Item = PublicKey> and MusigPubNonce respectively. They are both Copy, so no need to take a ref iter.

@sanket1729
Copy link
Member Author

sanket1729 commented Jan 29, 2024

@stevenroose, thanks for the testing this and providing valuable user feedback. Working on suggested changes.

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.

None yet

7 participants