-
Notifications
You must be signed in to change notification settings - Fork 281
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
Add Musig2 module #716
base: master
Are you sure you want to change the base?
Add Musig2 module #716
Conversation
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.
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) |
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.
IMO this should be just panic
. It can only happen if someone passes wrong value to dangerous ID creation function.
447a94c
to
e730b8b
Compare
a91d293
to
8bbd0d2
Compare
This is a 10 thousand line diff, is something commited that shouldn't be? |
It updates the vendored library to bring in the upstream MuSig PR. |
Yes. For now, only the last three commits matter for review purposes. |
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? |
@tcharding I will definitely not ack this until it's upstream is released. However I appreciate the experiment/demo. |
0a2361b
to
86e2b28
Compare
Yes, the idea is to wait for the upstream PR to be merged. |
secp256k1-sys/src/lib.rs
Outdated
impl MusigSecNonce { | ||
pub fn new() -> Self { | ||
MusigSecNonce([0; MUSIG_SECNONCE_LEN]) | ||
} |
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.
Isn't this highly misleading? If it's all-zeros it's not a nonce and thus broken. Where would one need it?
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.
Same as here: #716 (comment)
secp256k1-sys/src/lib.rs
Outdated
MusigSecNonce([0; MUSIG_SECNONCE_LEN]) | ||
} | ||
|
||
/// Don't use this. Refer to the documentation of wrapper APIs in the crate. |
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 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.
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.
Done. Thanks.
secp256k1-sys/src/lib.rs
Outdated
impl_raw_debug!(MusigPubNonce); | ||
|
||
impl MusigPubNonce { | ||
pub fn new() -> Self { |
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.
Looks also broken.
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.
Same as here: #716 (comment)
secp256k1-sys/src/lib.rs
Outdated
fn default() -> Self { | ||
Self::new() | ||
} | ||
} |
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.
It looks to me that none of these Default
s should exist. People should just use arrays or MaybeUninit<T>
to represent the uninitialized state.
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.
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.
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.
No, provide a function that constructs initialized types only.
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, 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);
}
}
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 clarification.
Done in 2ea5674
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'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]); |
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.
FTR these struct declarations looked wrong but are indeed correct based on the current API.
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.
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 { |
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 usually name these InvalidLength
.
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.
Done. Thanks.
86e2b28
to
071ac15
Compare
Upstream was released yesterday |
Can you rebase and format each commit with the nightly formatter? That should fix CI. |
071ac15
to
7f76102
Compare
Yes, done. Thanks. |
Patch 1 can be removed now, right? Then your shellcheck CI fail should disappear. |
690b72f
to
2d8b4a3
Compare
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.
Still not deep review, just some problems I noticed when I was digging into the API for other reasons.
secp256k1-sys/src/lib.rs
Outdated
#[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: *const *const PublicKey, |
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 signature is invalid. It's supposed to be a mut
pointer pointing to a const
pointer. IIUC this is unsound.
@apoelstra this is exactly why I advocate for using bindgen
. Bugs like this can happen and without it we have to spend time to audit every single function to make sure the signature is correct.
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.
Yeah, ok, we should use bindgen. But (at least initially) I would like to commit the generated code and verify it in CI rather than putting it as part of the build process, to minimize the number of dependencies that downstream users need.
We would also need to figure out what to do about secp256k1-sys/src/types.rs
. Will bindgen require some sort of tweaking to use these types? Or will we need a post-processing step? Or maybe we should just bump our MSRV to 1.64 so we don't need this file anymore?
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.
Good catch. Fixed in ff77611
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.
However, it is interesting that Rust does not enforce the const
modifier in this case.
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.
But (at least initially) I would like to commit the generated code and verify it in CI rather than putting it as part of the build process, to minimize the number of dependencies that downstream users need.
Yes, but maybe use xtask pattern for that? Rather than committing, we would have a subcrate that generates it and the developers would run it when contributing or publishing the crate (so the code would still be on crates.io). The upside is no CI job needed, the downside is that every contributor has to run the xtask even for unrelated contributions.
Rust does not enforce the
const
modifier in this case.
The compiler has no way of understanding the C code.
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.
Moved bindgen discussion to #775
src/key.rs
Outdated
/// # pubkey_sort(&secp, pubkeys_ref); | ||
/// # } | ||
/// ``` | ||
pub fn pubkey_sort<C: Verification>(secp: &Secp256k1<C>, pubkeys: &[&PublicKey]) { |
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 definitely unsound. The function is mutating the argument but it's not using mut
.
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.
Fixed in 2755c15
src/musig.rs
Outdated
|
||
/// Musig tweaking related errors. | ||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)] | ||
pub enum MusigTweakErr { |
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.
Given this has only one variant, either it should be a struct or it should have #[non_exhaustive]
if we anticipate new errors in the future (personally I don't believe we have a reason to think there will be new ones).
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.
Done in 404a93b
src/musig.rs
Outdated
/// # Errors: | ||
/// | ||
/// - MalformedArg: If the signature [`PartialSignature`] is out of curve order | ||
pub fn from_slice(data: &[u8; ffi::MUSIG_PART_SIG_LEN]) -> Result<Self, ParseError> { |
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.
Rename
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.
To deserialize(...)
?
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.
Now I saw it in the comment above.
Updated to from_byte_array()
in 3e9d296
src/musig.rs
Outdated
#[allow(missing_copy_implementations)] | ||
#[derive(Debug)] | ||
pub struct SecretNonce { | ||
data: ffi::MusigSecNonce |
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.
Damn, I was unclear. I only meant to have named fields in structs that have more than one field. This could've been tuple struct. You don't have to change it back if you don't want to but you can if you wish.
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.
No problem. Reverted to the tuple struct those with only one field in 5cf37bd.
src/musig.rs
Outdated
/// A Musig partial signature. | ||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] | ||
#[repr(transparent)] | ||
pub struct PartialSignature{ |
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.
Missing space.
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.
Updated in 5cf37bd
} | ||
|
||
impl AggregatedSignature { | ||
/// Returns the aggregated signature [`schnorr::Signature`] assuming it is valid. |
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 doing this! I'd like the doc toe be more in-depth.
/// Returns the aggregated signature [`schnorr::Signature`] assuming it is valid.
///
/// The `partial_sig_agg` function cannot guarantee that the produced signature is valid because participants
/// may send invalid signatures. In some applications this doesn't matter because the invalid message is simply
/// dropped with no consequences. These can simply call this function to obtain the resulting signature. However
/// in applications that require having valid signatures before continuing (e.g. presigned transactions in Bitcoin Lightning Network) this would be exploitable. Such applications MUST verify the resulting signature using the
/// [`verify`](Self::verify) method.
///
/// Note that while an alternative approach of verifying partial signatures is valid, verifying the aggregated
/// signature is more performant. Thus it should be generally better to verify the signature using this function first
/// and fall back to detection of violators if it fails.
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 this. Updated in 982e877.
2d8b4a3
to
1fec645
Compare
1fec645
to
d8b07df
Compare
5198d9a
to
97fb6d1
Compare
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 |
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 references to libsecp256k1-zkp and secp256k1-zkp in the comments below seem to be left over from Sanket's original PR?
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.
Correct. Good catch.
Updated in 4012ed5
97fb6d1
to
ae8597c
Compare
What is the status of this PR? |
@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. |
#[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, | ||
} |
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.
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]
).
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.
IMO we should make it #[non_exhaustive]
.
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.
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).
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 an exercise, I am adding test vectors from BIP327, so far they revealed this. Will continue tomorrow.
pub fn serialize(&self) -> [u8; 32] { | ||
let mut data = MaybeUninit::<[u8; 32]>::uninit(); |
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.
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)
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.
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 { |
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 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; |
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.
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.
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 on the constants explaining this would be great too.
/// # 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> { |
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.
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> { |
At some point, it would be useful to somehow have a way of creating The upstream does not provide Test vectors contain secnonce as 97-byte, see footnote 11:
Internal memory representation of secnonce has 132 bytes:
And this is how libsecp256k1 converts the 97-byte values from tests into 132-byte secnonce: loads public key in |
We should just generate them from session. We don't have to use every single test vector since upstream already uses them. |
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 withcargo run --example musig --features "rand std"
.The
ffi
functions were added tosecp256k1-sys/src/lib.rs
and the API level functions to the newsrc/musig.rs
file.