From 78f3fe1e3ea7553b3df721c924432501c4cd6aaf Mon Sep 17 00:00:00 2001 From: Arik Sosman Date: Sat, 28 Jan 2023 12:50:22 -0800 Subject: [PATCH] Introduce experimental native MuSig2 crate. --- .editorconfig | 7 + .github/workflows/check.yml | 30 +++ Cargo.toml | 6 +- README.md | 1 + src/key_preparation.rs | 159 ++++++++++++++++ src/lib.rs | 368 ++++++++++++++++++++++++++++++++++++ src/nonce_preparation.rs | 89 +++++++++ src/types.rs | 81 ++++++++ 8 files changed, 740 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/check.yml create mode 100644 src/key_preparation.rs create mode 100644 src/lib.rs create mode 100644 src/nonce_preparation.rs create mode 100644 src/types.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9baffad --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..727f253 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,30 @@ +name: Test MuSig2 Library + +on: + push: + branches: + - main + pull_request: + +jobs: + test-library: + runs-on: ubuntu-latest + env: + TOOLCHAIN: stable + steps: + - name: Checkout source code + uses: actions/checkout@v3 + - name: Install Rust ${{ matrix.toolchain }} toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal + - name: Build + run: cargo build --verbose --color always + - name: Test + run: cargo test --verbose --color always + - name: Document + run: cargo doc --document-private-items --verbose --color always + - name: Check + run: cargo check --verbose --color always diff --git a/Cargo.toml b/Cargo.toml index c9de5fd..468969d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,12 @@ [package] -name = "rust-musig2" +name = "musig2" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bitcoin = { version = "0.29.2" } + +[dev-dependencies] +hex = { version = "0.4.3" } diff --git a/README.md b/README.md index e69de29..ddbb07e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1 @@ +# rust-musig2 diff --git a/src/key_preparation.rs b/src/key_preparation.rs new file mode 100644 index 0000000..7052385 --- /dev/null +++ b/src/key_preparation.rs @@ -0,0 +1,159 @@ +use std::borrow::Borrow; + +use bitcoin::hashes::{Hash, HashEngine}; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1}; + +use crate::{AggregateKey, tagged_hash_scalar, tagged_sha_engine}; +use crate::types::SignerPublicKeys; + +/// Aggregate individual 33-byte public keys into the final 32-byte MuSig2 public key +pub fn aggregate_keys(keys: &SignerPublicKeys, secp_context: &Secp256k1) -> AggregateKey { + let ordered_keys = keys.ordered_keys(); + aggregate_ordered_keys(&ordered_keys, secp_context) +} + +pub(crate) fn aggregate_ordered_keys, C: bitcoin::secp256k1::Verification>(ordered_keys: &[K], secp_context: &Secp256k1) -> AggregateKey { + let mut key_summands = Vec::with_capacity(ordered_keys.len()); + + for i in 0..ordered_keys.len() { + let current_key = ordered_keys[i].borrow(); + let current_coefficient = key_aggregation_coefficient(&ordered_keys, ¤t_key); + + let current_summand = current_key.mul_tweak(&secp_context, ¤t_coefficient).unwrap(); + + key_summands.push(current_summand); + } + + let summand_references: Vec<&PublicKey> = key_summands.iter().collect(); + let combined_pubkey = PublicKey::combine_keys(&summand_references).unwrap(); + let (key, parity) = combined_pubkey.x_only_public_key(); + AggregateKey{ key, parity } +} + +pub(super) fn has_even_y(public_key: &PublicKey) -> bool { + let first_byte = public_key.serialize()[0]; + assert!([2, 3].contains(&first_byte)); + first_byte == 2 +} + +impl SignerPublicKeys { + pub(crate) fn ordered_keys(&self) -> Vec<&PublicKey> { + match &self { + Self::Ordered(keys) => { + keys.iter().collect::>() + } + Self::ToSort(keys) => { + let mut sorted_keys = keys.iter().collect::>(); + sorted_keys.sort_by(|key_a, key_b| { + let slice_a = key_a.serialize(); + let slice_b = key_b.serialize(); + slice_a.cmp(&slice_b) + }); + sorted_keys + } + } + } +} + +fn hash_keys>(sorted_keys: &[K]) -> [u8; 32] { + let mut sha_engine = tagged_sha_engine("KeyAgg list"); + for current_key in sorted_keys { + sha_engine.input(¤t_key.borrow().serialize()); + } + Sha256::from_engine(sha_engine).into_inner() +} + +fn second_key_serialization>(ordered_keys: &[K]) -> [u8; 33] { + let first_key = ordered_keys[0].borrow(); + for j in 1..ordered_keys.len() { + let current_key = ordered_keys[j].borrow(); + if current_key != first_key { + return current_key.serialize(); + } + } + [0; 33] +} + +pub(super) fn key_aggregation_coefficient>(ordered_keys: &[K], public_key: &PublicKey) -> Scalar { + // could be memoized + let second_key = second_key_serialization(ordered_keys); + let referenced_key = public_key.serialize(); + if referenced_key == second_key { + return Scalar::ONE; + } + + // could be memoized + let key_list_hash = hash_keys(ordered_keys); + tagged_hash_scalar("KeyAgg coefficient", &[&key_list_hash, &referenced_key]) +} + +#[cfg(test)] +mod tests { + use bitcoin::secp256k1::{PublicKey, Secp256k1}; + + use crate::{key_aggregation_coefficient, SignerPublicKeys}; + use crate::key_preparation::aggregate_keys; + + #[test] + fn test_key_sorting() { + let unsorted_keys = vec![ + PublicKey::from_slice(&hex::decode("02dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8").unwrap()).unwrap(), + PublicKey::from_slice(&hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap()).unwrap(), + PublicKey::from_slice(&hex::decode("03dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659").unwrap()).unwrap(), + PublicKey::from_slice(&hex::decode("023590a94e768f8e1815c2f24b4d80a8e3149316c3518ce7b7ad338368d038ca66").unwrap()).unwrap(), + PublicKey::from_slice(&hex::decode("02dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8").unwrap()).unwrap(), + ]; + + let sortable_keys = SignerPublicKeys::ToSort(unsorted_keys); + let sorted_keys = sortable_keys.ordered_keys(); + + assert_eq!(hex::encode(sorted_keys[0].serialize()), "023590a94e768f8e1815c2f24b4d80a8e3149316c3518ce7b7ad338368d038ca66"); + assert_eq!(hex::encode(sorted_keys[1].serialize()), "02dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8"); + assert_eq!(hex::encode(sorted_keys[2].serialize()), "02dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8"); + assert_eq!(hex::encode(sorted_keys[3].serialize()), "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"); + assert_eq!(hex::encode(sorted_keys[4].serialize()), "03dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659"); + } + + #[test] + fn test_key_aggregation_coefficient_on_distinct_key() { + let keys = vec![ + PublicKey::from_slice(&hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap()).unwrap(), + PublicKey::from_slice(&hex::decode("03dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659").unwrap()).unwrap(), + PublicKey::from_slice(&hex::decode("023590a94e768f8e1815c2f24b4d80a8e3149316c3518ce7b7ad338368d038ca66").unwrap()).unwrap(), + ]; + + let coefficients = [ + key_aggregation_coefficient(&keys, &keys[0]), + key_aggregation_coefficient(&keys, &keys[1]), + key_aggregation_coefficient(&keys, &keys[2]), + ]; + + assert_eq!(hex::encode(coefficients[0].to_be_bytes()), "ad0537c883813849e3b95ce5db1d45eb25cc5fae197c4e8759719065932aa183"); + assert_eq!(hex::encode(coefficients[1].to_be_bytes()), "0000000000000000000000000000000000000000000000000000000000000001"); + assert_eq!(hex::encode(coefficients[2].to_be_bytes()), "f45bb025038e68f050ce4dbf8c63e613678892e2a3e930780afe6293376005c6"); + } + + #[test] + fn test_key_aggregation_coefficient_on_repeated_key() { + let keys = vec![ + PublicKey::from_slice(&hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap()).unwrap(), + PublicKey::from_slice(&hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap()).unwrap(), + PublicKey::from_slice(&hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap()).unwrap(), + ]; + + let coefficients = [ + key_aggregation_coefficient(&keys, &keys[0]), + key_aggregation_coefficient(&keys, &keys[1]), + key_aggregation_coefficient(&keys, &keys[2]), + ]; + + assert_eq!(hex::encode(coefficients[0].to_be_bytes()), "d18a55755a3b6aa3b171bd484eb508318abd01a3cd5e6c78aca613dbd72fa061"); + assert_eq!(hex::encode(coefficients[1].to_be_bytes()), "d18a55755a3b6aa3b171bd484eb508318abd01a3cd5e6c78aca613dbd72fa061"); + assert_eq!(hex::encode(coefficients[2].to_be_bytes()), "d18a55755a3b6aa3b171bd484eb508318abd01a3cd5e6c78aca613dbd72fa061"); + + let secp_context = Secp256k1::new(); + let aggregate_key = aggregate_keys(&SignerPublicKeys::Ordered(keys), &secp_context); + assert_eq!(hex::encode(aggregate_key.key.serialize()), "b436e3bad62b8cd409969a224731c193d051162d8c5ae8b109306127da3aa935"); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bce5e13 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,368 @@ +#![deny(missing_docs)] +#![deny(unsafe_code)] +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![deny(unused_variables)] +#![deny(unused_imports)] +#![deny(warnings)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(rustdoc::private_intra_doc_links)] + +//! Experimental crate implementing partial MuSig2 functions. Key tweaking is not supported. + +use std::borrow::Borrow; + +use bitcoin::hashes::{Hash, HashEngine}; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::sha256::HashEngine as ShaEngine; +use bitcoin::secp256k1::{Parity, PublicKey, Scalar, Secp256k1, SecretKey}; +use bitcoin::secp256k1::schnorr::Signature; + +use crate::key_preparation::{aggregate_ordered_keys, has_even_y, key_aggregation_coefficient}; +pub use crate::key_preparation::aggregate_keys; +pub use crate::nonce_preparation::aggregate_nonces; +use crate::nonce_preparation::calculate_final_nonce; +use crate::types::{AggregateKey, PartialSignature, PublicNonce, SecretNonce, SignerPublicKeys}; + +pub mod types; +mod key_preparation; +mod nonce_preparation; + +/// Create a partial MuSig2 signature. +/// +/// Note: This does not include the partial nonce. +pub fn sign(secret_key: &SecretKey, secret_nonce: &SecretNonce, aggregate_nonce: &PublicNonce, keys: &SignerPublicKeys, message: &[u8], secp_context: &Secp256k1) -> PartialSignature { + let ordered_keys = keys.ordered_keys(); + let aggregate_public_key = aggregate_ordered_keys(&ordered_keys, &secp_context); + let (effective_nonce, nonce_coefficient) = calculate_final_nonce(aggregate_nonce, &aggregate_public_key, message, &secp_context); + + let final_secret_nonce = if has_even_y(&effective_nonce) { + secret_nonce.clone() + } else { + secret_nonce.negate() + }; + + // determine whether the aggregate public key has an even y coordinate + let final_secret_key = if let Parity::Even = aggregate_public_key.parity { + secret_key.clone() + } else { + secret_key.negate() + }; + + let m = tagged_hash_scalar("BIP0340/challenge", &[ + &effective_nonce.x_only_public_key().0.serialize(), + &aggregate_public_key.key.serialize(), + message + ]); + + let a = key_aggregation_coefficient(&ordered_keys, &secret_key.public_key(&secp_context)); + // m*a*d + let summand_a = final_secret_key.mul_tweak(&m).unwrap().mul_tweak(&a).unwrap(); + // b*k2 + let summand_b = final_secret_nonce.1.mul_tweak(&nonce_coefficient).unwrap(); + // s = m*a*d + k1 + b*k2 + let s = summand_a.add_tweak(&Scalar::from(summand_b)).unwrap().add_tweak(&Scalar::from(final_secret_nonce.0)).unwrap(); + let is_valid = verify_partial_signature(&PartialSignature(s), &secret_nonce.to_public(&secp_context), aggregate_nonce, &secret_key.public_key(&secp_context), keys, message, secp_context); + assert!(is_valid); + PartialSignature(s) +} + +/// Aggregate partial MuSig2 signatures into the final Schnorr signature +pub fn aggregate_partial_signatures>(partial_signatures: &[K], final_nonce: &PublicKey) -> Signature { + let mut aggregate_signature = Scalar::ZERO; + for current_signature in partial_signatures { + aggregate_signature = Scalar::from(current_signature.borrow().0.add_tweak(&aggregate_signature).unwrap()); + } + let mut serialized_signature = Vec::with_capacity(64); + serialized_signature.extend_from_slice(&final_nonce.x_only_public_key().0.serialize()); + serialized_signature.extend_from_slice(&aggregate_signature.to_be_bytes()); + Signature::from_slice(&serialized_signature).unwrap() +} + +/// Verify partial MuSig2 signature +pub fn verify_partial_signature(partial_signature: &PartialSignature, partial_public_nonce: &PublicNonce, aggregate_public_nonce: &PublicNonce, partial_public_key: &PublicKey, keys: &SignerPublicKeys, message: &[u8], secp_context: &Secp256k1) -> bool { + let ordered_keys = keys.ordered_keys(); + verify_partial_signature_with_ordered_keys(partial_signature, partial_public_nonce, aggregate_public_nonce, partial_public_key, &ordered_keys, message, secp_context) +} + +fn verify_partial_signature_with_ordered_keys, C: bitcoin::secp256k1::Verification + bitcoin::secp256k1::Signing>(partial_signature: &PartialSignature, partial_public_nonce: &PublicNonce, aggregate_public_nonce: &PublicNonce, partial_public_key: &PublicKey, ordered_keys: &[K], message: &[u8], secp_context: &Secp256k1) -> bool { + let aggregate_public_key = aggregate_ordered_keys(ordered_keys, &secp_context); + + let (effective_nonce, nonce_coefficient) = calculate_final_nonce(aggregate_public_nonce, &aggregate_public_key, message, &secp_context); + let partial_nonce_summand = partial_public_nonce.1.mul_tweak(&secp_context, &nonce_coefficient).unwrap(); + let mut effective_partial_nonce = PublicKey::combine_keys(&[&partial_public_nonce.0, &partial_nonce_summand]).unwrap(); + if !has_even_y(&effective_nonce) { + effective_partial_nonce = effective_partial_nonce.negate(&secp_context); + } + + let m = tagged_hash_scalar("BIP0340/challenge", &[ + &effective_nonce.x_only_public_key().0.serialize(), + &aggregate_public_key.key.serialize(), + message + ]); + + // determine whether the aggregate public key has an even y coordinate + let final_public_key = if let Parity::Even = aggregate_public_key.parity { + partial_public_key.clone() + } else { + partial_public_key.negate(&secp_context) + }; + + let a = key_aggregation_coefficient(ordered_keys, partial_public_key); + let summand = final_public_key.mul_tweak(&secp_context, &a).unwrap().mul_tweak(&secp_context, &m).unwrap(); + let equality_check = PublicKey::combine_keys(&[&summand, &effective_partial_nonce]).unwrap(); + + let partial_signature_point = partial_signature.0.public_key(&secp_context); + + equality_check.serialize() == partial_signature_point.serialize() +} + + +fn tagged_sha_engine(tag: &str) -> ShaEngine { + let tag_hash = Sha256::hash(tag.as_bytes()).into_inner(); + let mut sha_engine = Sha256::engine(); + sha_engine.input(&tag_hash); + sha_engine.input(&tag_hash); + sha_engine +} + +fn tagged_hash_scalar(tag: &str, inputs: &[&[u8]]) -> Scalar { + let mut sha_engine = tagged_sha_engine(tag); + for current_input in inputs { + sha_engine.input(current_input); + } + let output_hash = Sha256::from_engine(sha_engine).into_inner(); + Scalar::from_be_bytes(output_hash) + .expect("SHA256 appears to be broken due to the heat death of the universe. If the user has a cat, please make sure to kill it.") +} + + +#[cfg(test)] +mod tests { + use bitcoin::XOnlyPublicKey; + + use crate::key_preparation::aggregate_keys; + use crate::nonce_preparation::aggregate_nonces; + + use super::*; + + fn verify_schnorr_signature(signature: &Signature, public_key: &PublicKey, message: &[u8]) -> bool { + assert_eq!(signature.len(), 64); + + let nonce_x = &signature[..32]; + let s = &signature[32..]; + + let nonce = XOnlyPublicKey::from_slice(nonce_x).unwrap(); + let nonce = PublicKey::from_x_only_public_key(nonce, Parity::Even); + + let secp_context = Secp256k1::new(); + let s = SecretKey::from_slice(s).unwrap(); + let signature_point = s.public_key(&secp_context); + assert!(has_even_y(&signature_point)); + + let m = tagged_hash_scalar("BIP0340/challenge", &[ + &nonce.x_only_public_key().0.serialize(), + &public_key.x_only_public_key().0.serialize(), + message + ]); + + let summand = public_key.mul_tweak(&secp_context, &m).unwrap(); + let equality_check = PublicKey::combine_keys(&[&summand, &nonce]).unwrap(); + + equality_check.serialize() == signature_point.serialize() + } + + #[test] + fn test_signature_aggregation() { + let pubkey_hexes = vec![ + "03935f972da013f80ae011890fa89b67a27b7be6ccb24d3274d18b2d4067f261a9", + "02d2dc6f5df7c56acf38c7fa0ae7a759ae30e19b37359dfde015872324c7ef6e05", + ]; + + let public_nonce_hexes = vec![ + "0300a32f8548f59c533f55db9754e3c0ba3c2544f085649fdce42b8bd3f244c2ca0384449bed61004e8863452a38534e91875516c3cc543122ce2be1f31845025588", + "03f66b072a869bc2a57d776d487151d707e82b4f1b885066a589858c1bf3871db603ed391c9658ab6031a96acbd5e2d9fec465efdc8c0d0b765c9b9f3579d520fb6f", + ]; + + let partial_signature_hexes = vec![ + "7918521f42e5727fe2e82d802876e0c8844336fda1b58c82696a55b0188c8b3d", + "599044037ae15c4a99fb94f022b48e7ab215bf703954ec0b83d0e06230476001", + ]; + + let mut public_keys = Vec::with_capacity(pubkey_hexes.len()); + for current_pubkey_hex in pubkey_hexes { + let current_key = PublicKey::from_slice(&hex::decode(current_pubkey_hex).unwrap()).unwrap(); + public_keys.push(current_key); + } + + let mut public_nonces = Vec::with_capacity(public_nonce_hexes.len()); + for current_nonce_hex in public_nonce_hexes { + let current_nonce = PublicNonce::from_slice(&hex::decode(current_nonce_hex).unwrap()).unwrap(); + public_nonces.push(current_nonce); + } + + let mut partial_signatures = Vec::with_capacity(partial_signature_hexes.len()); + for current_signature_hex in partial_signature_hexes { + let current_signature = SecretKey::from_slice(&hex::decode(current_signature_hex).unwrap()).unwrap(); + partial_signatures.push(PartialSignature(current_signature)); + } + + assert_eq!(public_keys.len(), 2); + assert_eq!(public_nonces.len(), 2); + assert_eq!(partial_signatures.len(), 2); + + let message = hex::decode("599c67ea410d005b9da90817cf03ed3b1c868e4da4edf00a5880b0082c237869").unwrap(); + + let aggregate_nonce = aggregate_nonces(&public_nonces); + let aggregate_nonce_hex = hex::encode(aggregate_nonce.serialize()); + assert_eq!(aggregate_nonce_hex, "02bc34cdf6fa1298d7b6a126812fad0739005bc44e45c21276eefe41aaf841c86f03f3562aed52243bb99f43d1677db59f0fefb961633997f7ac924b78fbd0b0334f"); + + let secp_context = Secp256k1::new(); + { + let ordered_public_keys = SignerPublicKeys::Ordered(public_keys.clone()); + let aggregate_public_key = aggregate_keys(&ordered_public_keys, &secp_context); + println!("aggregate pubkey (Q): {}", hex::encode(aggregate_public_key.key.serialize())); + + let (final_nonce, _) = calculate_final_nonce(&aggregate_nonce, &aggregate_public_key, &message, &secp_context); + println!("final nonce (R): {}", hex::encode(final_nonce.serialize())); + + let signature = aggregate_partial_signatures(partial_signatures.as_slice(), &final_nonce); + let signature_hex = hex::encode(signature.as_ref()); + assert_eq!(signature_hex, "ca3c28729659e50f829f55dc5db1de88a05d1702b4165b85f95b627fc57733f8d2a89622bdc6ceca7ce3c2704b2b6f433658f66ddb0a788ded3b361248d3eb3e") + } + { + // experiment with sorted pub keys + let reversed_public_keys = SignerPublicKeys::ToSort(public_keys.clone()); + let aggregate_public_key = aggregate_keys(&reversed_public_keys, &secp_context); + println!("aggregate pubkey (Q, reversed order): {}", hex::encode(aggregate_public_key.key.serialize())); + + let (final_nonce, _) = calculate_final_nonce(&aggregate_nonce, &aggregate_public_key, &message, &secp_context); + println!("final nonce (R): {}", hex::encode(final_nonce.serialize())); + + let signature = aggregate_partial_signatures(partial_signatures.as_slice(), &final_nonce); + let signature_hex = hex::encode(signature.as_ref()); + assert_eq!(signature_hex, "13f2dba79042c42b8b25544c3b8d6785f170571a3b23e6c7d2997e94dbc04284d2a89622bdc6ceca7ce3c2704b2b6f433658f66ddb0a788ded3b361248d3eb3e") + } + { + // experiment with explicitly reversed pub keys + let reversed_public_keys = SignerPublicKeys::Ordered(vec![public_keys[1], public_keys[0]]); + let aggregate_public_key = aggregate_keys(&reversed_public_keys, &secp_context); + println!("aggregate pubkey (Q, reversed order): {}", hex::encode(aggregate_public_key.key.serialize())); + + let (final_nonce, _) = calculate_final_nonce(&aggregate_nonce, &aggregate_public_key, &message, &secp_context); + println!("final nonce (R): {}", hex::encode(final_nonce.serialize())); + + let signature = aggregate_partial_signatures(partial_signatures.as_slice(), &final_nonce); + let signature_hex = hex::encode(signature.as_ref()); + assert_eq!(signature_hex, "13f2dba79042c42b8b25544c3b8d6785f170571a3b23e6c7d2997e94dbc04284d2a89622bdc6ceca7ce3c2704b2b6f433658f66ddb0a788ded3b361248d3eb3e") + } + } + + #[test] + fn test_signature_verification() { + let secp_context = Secp256k1::new(); + + let secret_keys = vec![ + SecretKey::from_slice(&hex::decode("d2dc6f5df7c56acf38c7fa0ae7a759ae30e19b37359dfde015872324c7ef6e05").unwrap()).unwrap(), + SecretKey::from_slice(&hex::decode("935f972da013f80ae011890fa89b67a27b7be6ccb24d3274d18b2d4067f261a9").unwrap()).unwrap(), + ]; + + let public_keys = vec![ + PublicKey::from_secret_key(&secp_context, &secret_keys[0]), + PublicKey::from_secret_key(&secp_context, &secret_keys[1]), + ]; + + let sortable_public_keys = SignerPublicKeys::ToSort(public_keys.clone()); + // let sorted_public_keys = public_keys.clone(); + + + let secret_nonce_hexes = vec![ + vec!["00a32f8548f59c533f55db9754e3c0ba3c2544f085649fdce42b8bd3f244c2ca", "0384449bed61004e8863452a38534e91875516c3cc543122ce2be1f318450255"], + vec!["f66b072a869bc2a57d776d487151d707e82b4f1b885066a589858c1bf3871db6", "03ed391c9658ab6031a96acbd5e2d9fec465efdc8c0d0b765c9b9f3579d520fb"], + ]; + let mut secret_nonces = Vec::with_capacity(secret_nonce_hexes.len()); + for current_nonce_hex in secret_nonce_hexes { + let nonce_a = SecretKey::from_slice(&hex::decode(current_nonce_hex[0]).unwrap()).unwrap(); + let nonce_b = SecretKey::from_slice(&hex::decode(current_nonce_hex[0]).unwrap()).unwrap(); + secret_nonces.push(SecretNonce(nonce_a, nonce_b)); + } + + let public_nonces = vec![ + secret_nonces[0].to_public(&secp_context), + secret_nonces[1].to_public(&secp_context), + ]; + + let aggregate_nonce = aggregate_nonces(public_nonces.as_slice()); + let aggregate_pubkey = aggregate_keys(&sortable_public_keys, &secp_context); + let message = hex::decode("599c67ea410d005b9da90817cf03ed3b1c868e4da4edf00a5880b0082c237869").unwrap(); + let (effective_nonce, _) = calculate_final_nonce(&aggregate_nonce, &aggregate_pubkey, &message, &secp_context); + + let partial_signature_a = sign(&secret_keys[0], &secret_nonces[0], &aggregate_nonce, &sortable_public_keys, &message, &secp_context); + let partial_signature_b = sign(&secret_keys[1], &secret_nonces[1], &aggregate_nonce, &sortable_public_keys, &message, &secp_context); + + let is_signature_a_valid = verify_partial_signature(&partial_signature_a, &public_nonces[0], &aggregate_nonce, &public_keys[0], &sortable_public_keys, &message, &secp_context); + let is_signature_b_valid = verify_partial_signature(&partial_signature_b, &public_nonces[1], &aggregate_nonce, &public_keys[1], &sortable_public_keys, &message, &secp_context); + + assert!(is_signature_a_valid); + assert!(is_signature_b_valid); + + let schnorr_signature = aggregate_partial_signatures(&[partial_signature_a, partial_signature_b], &effective_nonce); + let is_schnorr_signature_valid = verify_schnorr_signature(&schnorr_signature, &PublicKey::from_x_only_public_key(aggregate_pubkey.key, Parity::Even), &message); + assert!(is_schnorr_signature_valid) + } + + #[test] + fn test_signature_verification_against_vectors() { + let secp_context = Secp256k1::new(); + + let secret_key_hex = "7fb9e0e687ada1eebf7ecfe2f21e73ebdb51a7d450948dfe8d76d7f2d1007671"; + let pubkey_hexes = vec![ + "03935f972da013f80ae011890fa89b67a27b7be6ccb24d3274d18b2d4067f261a9", + "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "02dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba661", + ]; + + let secret_nonce_hex = "508b81a611f100a6b2b6b29656590898af488bcf2e1f55cf22e5cfb84421fe61fa27fd49b1d50085b481285e1ca205d55c82cc1b31ff5cd54a489829355901f7"; + let public_nonce_hexes = vec![ + "0337c87821afd50a8644d820a8f3e02e499c931865c2360fb43d0a0d20dafe07ea0287bf891d2a6deaebadc909352aa9405d1428c15f4b75f04dae642a95c2548480", + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "032de2662628c90b03f5e720284eb52ff7d71f4284f627b68a853d78c78e1ffe9303e4c5524e83ffe1493b9077cf1ca6beb2090c93d930321071ad40b2f44e599046", + ]; + let aggregate_nonce_hex = "028465fcf0bbdbcf443aabcce533d42b4b5a10966ac09a49655e8c42daab8fcd61037496a3cc86926d452cafcfd55d25972ca1675d549310de296bff42f72eeea8c9"; + let message_hex = "f95466d086770e689964664219266fe5ed215c92ae20bab5c9d79addddf3c0cf"; + + let secret_key = SecretKey::from_slice(&hex::decode(secret_key_hex).unwrap()).unwrap(); + + let public_keys = vec![ + PublicKey::from_slice(&hex::decode(&pubkey_hexes[0]).unwrap()).unwrap(), + PublicKey::from_slice(&hex::decode(&pubkey_hexes[1]).unwrap()).unwrap(), + PublicKey::from_slice(&hex::decode(&pubkey_hexes[2]).unwrap()).unwrap(), + ]; + + let secret_nonce_a = SecretKey::from_slice(&hex::decode(&secret_nonce_hex[..64]).unwrap()).unwrap(); + let secret_nonce_b = SecretKey::from_slice(&hex::decode(&secret_nonce_hex[64..]).unwrap()).unwrap(); + println!("nonce A pubkey: {}", hex::encode(secret_nonce_a.public_key(&secp_context).serialize())); + println!("nonce B pubkey: {}", hex::encode(secret_nonce_b.public_key(&secp_context).serialize())); + let secret_nonce = SecretNonce(secret_nonce_a, secret_nonce_b); + + let mut public_nonces = Vec::with_capacity(public_nonce_hexes.len()); + for current_nonce_hex in public_nonce_hexes { + let current_nonce = PublicNonce::from_slice(&hex::decode(current_nonce_hex).unwrap()).unwrap(); + public_nonces.push(current_nonce); + } + + let aggregate_nonce = PublicNonce::from_slice(&hex::decode(aggregate_nonce_hex).unwrap()).unwrap(); + + let message = hex::decode(message_hex).unwrap(); + + let ordered_public_keys = SignerPublicKeys::Ordered(public_keys); + let partial_signature = sign(&secret_key, &secret_nonce, &aggregate_nonce, &ordered_public_keys, &message, &secp_context); + assert_eq!(hex::encode(&partial_signature.0.secret_bytes()), "012abbcb52b3016ac03ad82395a1a415c48b93def78718e62a7a90052fe224fb"); + + // let is_signature_valid = verify_partial_signature_internal(&partial_signature, &public_nonces[0], &aggregate_nonce, &public_keys[0], public_keys.as_slice(), &message); + let is_signature_valid = verify_partial_signature(&partial_signature, &secret_nonce.to_public(&secp_context), &aggregate_nonce, &secret_key.public_key(&secp_context), &ordered_public_keys, &message, &secp_context); + assert!(is_signature_valid); + } +} diff --git a/src/nonce_preparation.rs b/src/nonce_preparation.rs new file mode 100644 index 0000000..54ffc8b --- /dev/null +++ b/src/nonce_preparation.rs @@ -0,0 +1,89 @@ +use std::borrow::Borrow; + +use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1}; +use bitcoin::XOnlyPublicKey; + +use crate::{AggregateKey, PublicNonce, tagged_hash_scalar}; + +/// Aggregate individual MuSig2 nonces into the final Schnorr signature nonce +pub fn aggregate_nonces>(nonces: &[N]) -> PublicNonce { + let mut first_nonce_summands = Vec::with_capacity(nonces.len()); + let mut second_nonce_summands = Vec::with_capacity(nonces.len()); + + for current_nonce in nonces { + let first_nonce = ¤t_nonce.borrow().0; + let second_nonce = ¤t_nonce.borrow().1; + + first_nonce_summands.push(first_nonce); + second_nonce_summands.push(second_nonce); + } + + let first_nonce = PublicKey::combine_keys(&first_nonce_summands).unwrap(); + let second_nonce = PublicKey::combine_keys(&second_nonce_summands).unwrap(); + PublicNonce(first_nonce, second_nonce) +} + +pub(crate) fn calculate_final_nonce(aggregate_nonce: &PublicNonce, aggregate_public_key: &AggregateKey, message: &[u8], secp_context: &Secp256k1) -> (PublicKey, Scalar) { + let nonce_coefficient = nonce_linear_combination_coefficient(&aggregate_public_key.key, &aggregate_nonce, message); + let nonce_summand = aggregate_nonce.1.mul_tweak(&secp_context, &nonce_coefficient).unwrap(); + let public_nonce = PublicKey::combine_keys(&[&aggregate_nonce.0, &nonce_summand]).unwrap(); + (public_nonce, nonce_coefficient) +} + +fn nonce_linear_combination_coefficient(aggregate_public_key: &XOnlyPublicKey, aggregate_nonce: &PublicNonce, message: &[u8]) -> Scalar { + tagged_hash_scalar("MuSig/noncecoef", &[&aggregate_nonce.serialize(), &aggregate_public_key.serialize(), message]) +} + +#[cfg(test)] +mod tests { + use crate::nonce_preparation::aggregate_nonces; + use crate::PublicNonce; + + #[test] + fn test_nonce_aggregation() { + let public_nonce_hexes = vec![ + "020151c80f435648df67a22b749cd798ce54e0321d034b92b709b567d60a42e66603ba47fbc1834437b3212e89a84d8425e7bf12e0245d98262268ebdcb385d50641", + "03ff406ffd8adb9cd29877e4985014f66a59f6cd01c0e88caa8e5f3166b1f676a60248c264cdd57d3c24d79990b0f865674eb62a0f9018277a95011b41bfc193b833", + ]; + + let mut public_nonces = Vec::with_capacity(public_nonce_hexes.len()); + for current_nonce_hex in public_nonce_hexes { + let current_nonce = PublicNonce::from_slice(&hex::decode(current_nonce_hex).unwrap()).unwrap(); + public_nonces.push(current_nonce); + } + + assert_eq!(public_nonces.len(), 2); + + let aggregate_nonce = aggregate_nonces(&public_nonces); + let mut aggregate_nonce_bytes = Vec::with_capacity(66); + aggregate_nonce_bytes.extend_from_slice(&aggregate_nonce.serialize()); + let aggregate_nonce_hex = hex::encode(aggregate_nonce_bytes); + + assert_eq!(aggregate_nonce_hex, "035fe1873b4f2967f52fea4a06ad5a8eccbe9d0fd73068012c894e2e87ccb5804b024725377345bde0e9c33af3c43c0a29a9249f2f2956fa8cfeb55c8573d0262dc8") + } + + #[test] + #[should_panic] + // TODO: make it such that this doesn't panic + fn test_nonce_aggregation_at_infinity_point() { + let public_nonce_hexes = vec![ + "020151c80f435648df67a22b749cd798ce54e0321d034b92b709b567d60a42e6660279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "03ff406ffd8adb9cd29877e4985014f66a59f6cd01c0e88caa8e5f3166b1f676a60379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ]; + + let mut public_nonces = Vec::with_capacity(public_nonce_hexes.len()); + for current_nonce_hex in public_nonce_hexes { + let current_nonce = PublicNonce::from_slice(&hex::decode(current_nonce_hex).unwrap()).unwrap(); + public_nonces.push(current_nonce); + } + + assert_eq!(public_nonces.len(), 2); + + let aggregate_nonce = aggregate_nonces(&public_nonces); + let mut aggregate_nonce_bytes = Vec::with_capacity(66); + aggregate_nonce_bytes.extend_from_slice(&aggregate_nonce.serialize()); + let aggregate_nonce_hex = hex::encode(aggregate_nonce_bytes); + + assert_eq!(aggregate_nonce_hex, "035fe1873b4f2967f52fea4a06ad5a8eccbe9d0fd73068012c894e2e87ccb5804b000000000000000000000000000000000000000000000000000000000000000000") + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..cc1db29 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,81 @@ +//! Types used exclusively for MuSig2 shenanigans + +use bitcoin::secp256k1::{Parity, PublicKey, Secp256k1, SecretKey}; +use bitcoin::XOnlyPublicKey; + +/// A public nonce used for Musig2, comprised of two ECPoints +#[derive(Clone, Debug, PartialEq)] +pub struct PublicNonce(pub(crate) PublicKey, pub(crate) PublicKey); + +/// A secret nonce used for Musig2, comprised of two SecretKeys +#[derive(Clone, Debug, PartialEq)] +pub struct SecretNonce(pub(crate) SecretKey, pub(crate) SecretKey); + +/// A type representing an aggregated public key for MuSig2 use +pub struct AggregateKey { + pub(crate) key: XOnlyPublicKey, + pub(crate) parity: Parity +} + +/// A partial MuSig2 signature +pub struct PartialSignature(pub(crate) SecretKey); + +/// A pubkey container that discriminates between preördered and sortable keys +pub enum SignerPublicKeys { + /// These keys are ordered a priori + Ordered(Vec), + /// These keys may or may not be ordered. Usage will automatically result in sorting. + ToSort(Vec) +} + +impl SecretNonce { + /// Convert to public + pub fn to_public(&self, secp_context: &Secp256k1) -> PublicNonce { + PublicNonce( + PublicKey::from_secret_key(&secp_context, &self.0), + PublicKey::from_secret_key(&secp_context, &self.1), + ) + } + + /// Serialize the two private keys contained within + pub fn serialize(&self) -> [u8; 64] { + let mut serialization = [0; 64]; + let (first, second) = serialization.split_at_mut(32); + first.copy_from_slice(&self.0.secret_bytes()); + second.copy_from_slice(&self.1.secret_bytes()); + serialization + } + + /// Deserialize from bytes + pub fn from_slice(data: &[u8]) -> Result { + assert_eq!(data.len(), 64, "SecretNonce can only be read from exactly 64 bytes"); + let first = SecretKey::from_slice(&data[0..32])?; + let second = SecretKey::from_slice(&data[32..64])?; + Ok(Self(first, second)) + } + + pub(crate) fn negate(&self) -> Self { + let first_nonce = self.0.negate(); + let second_nonce = self.1.negate(); + SecretNonce(first_nonce, second_nonce) + } +} + +impl PublicNonce { + /// Serialize the two public keys contained within + pub fn serialize(&self) -> [u8; 66] { + let mut serialization = [0; 66]; + let (first, second) = serialization.split_at_mut(33); + first.copy_from_slice(&self.0.serialize()); + second.copy_from_slice(&self.1.serialize()); + serialization + } + + /// Deserialize from bytes + pub fn from_slice(data: &[u8]) -> Result { + assert_eq!(data.len(), 66, "SecretNonce can only be read from exactly 66 bytes"); + let first = PublicKey::from_slice(&data[0..33])?; + let second = PublicKey::from_slice(&data[33..66])?; + Ok(Self(first, second)) + } +}