Skip to content

Commit 29066e1

Browse files
committed
Implement musig2 key aggregation
1 parent 84134da commit 29066e1

File tree

4 files changed

+190
-1
lines changed

4 files changed

+190
-1
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ members = [
55
"schnorr_fun",
66
"ecdsa_fun",
77
"secp256kfun_parity_backend",
8-
"sigma_fun"
8+
"sigma_fun",
99
]
1010
resolver = "2"

schnorr_fun/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ features = ["all"]
1818
[dependencies]
1919
secp256kfun = { path = "../secp256kfun", version = "0.6.2-alpha.0", default-features = false }
2020
serde_crate = { package = "serde", version = "1.0", default-features = false, optional = true, features = ["derive", "alloc"] }
21+
bitcoin_hashes = "0.10.0"
2122

2223
[dev-dependencies]
2324
rand = { version = "0.8" }

schnorr_fun/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ extern crate std;
1414
#[cfg(feature = "serde")]
1515
extern crate serde_crate as serde;
1616

17+
extern crate bitcoin_hashes;
18+
1719
pub use secp256kfun as fun;
1820
pub use secp256kfun::nonce;
1921
mod signature;
@@ -25,6 +27,8 @@ mod schnorr;
2527
pub use schnorr::*;
2628
mod message;
2729
pub use message::*;
30+
mod musig2;
31+
pub use musig2::Musig;
2832

2933
#[macro_export]
3034
#[doc(hidden)]

schnorr_fun/src/musig2.rs

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use bitcoin_hashes::borrow_slice_impl;
2+
use bitcoin_hashes::hash_newtype;
3+
use bitcoin_hashes::hex_fmt_impl;
4+
use bitcoin_hashes::index_impl;
5+
use bitcoin_hashes::serde_impl;
6+
use bitcoin_hashes::sha256t_hash_newtype;
7+
use bitcoin_hashes::{sha256, Hash, HashEngine};
8+
use secp256kfun::g;
9+
use secp256kfun::marker::{Jacobian, Mark, NonZero};
10+
use secp256kfun::{Point, Scalar, XOnly};
11+
12+
/// The SHA-256 midstate value for the "KeyAgg coefficient" hash.
13+
const MIDSTATE_KEYAGGHASH: [u8; 32] = [
14+
110, 240, 44, 90, 6, 164, 128, 222, 31, 41, 134, 101, 29, 17, 52, 242, 86, 160, 176, 99, 82,
15+
218, 65, 71, 242, 128, 217, 212, 68, 132, 190, 21,
16+
];
17+
// 6ef02c5a06a480de1f2986651d1134f256a0b06352da4147f280d9d44484be15
18+
19+
sha256t_hash_newtype!(
20+
KeyAggHash,
21+
KeyAggTag,
22+
MIDSTATE_KEYAGGHASH,
23+
64,
24+
doc = "Tagged hash for key aggregation",
25+
true
26+
);
27+
28+
/// Struct to be create to aggregate keys with the Musig2 protocol as defined in
29+
/// https://github.com/ElementsProject/secp256k1-zkp/blob/5d2df0541960554be5c0ba58d86e5fa479935000/src/modules/musig/musig-spec.mediawiki
30+
#[derive(Debug)]
31+
pub struct Musig<'a> {
32+
keys: &'a [XOnly],
33+
keys_hash: [u8; 32],
34+
second: Option<&'a XOnly>,
35+
}
36+
37+
impl<'a> Musig<'a> {
38+
/// Create new Musig struct
39+
pub fn new(keys: &'a [XOnly]) -> Option<Musig> {
40+
if keys.len() < 2 {
41+
return None;
42+
}
43+
let mut engine = sha256::Hash::engine();
44+
let mut second = None;
45+
for k in keys.iter() {
46+
engine.input(k.as_bytes());
47+
if second.is_none() {
48+
if *k != keys[0] {
49+
second = Some(k)
50+
}
51+
}
52+
}
53+
let keys_hash = sha256::Hash::from_engine(engine).into_inner();
54+
Some(Musig {
55+
keys,
56+
keys_hash,
57+
second,
58+
})
59+
}
60+
61+
/// returns the scalar coefficient to be used for key `index`
62+
fn coeff(&self, index: usize) -> Option<Scalar> {
63+
if index >= self.keys.len() {
64+
return None;
65+
}
66+
if Some(&self.keys[index]) == self.second {
67+
return Some(Scalar::one());
68+
} else {
69+
let mut engine = KeyAggHash::engine();
70+
engine.input(&self.keys_hash);
71+
engine.input(self.keys[index].as_bytes());
72+
let hash = KeyAggHash::from_engine(engine);
73+
let s = Scalar::from_bytes(hash.into_inner());
74+
s.and_then(|s| s.mark::<NonZero>())
75+
}
76+
}
77+
78+
/// Combine keys into one key
79+
pub fn combine(&self) -> Option<XOnly> {
80+
let mut accumulator = Point::zero().mark::<Jacobian>();
81+
for (i, k) in self.keys.iter().enumerate() {
82+
let current_scalar = self.coeff(i)?;
83+
let current_point = k.to_point();
84+
accumulator = g!(accumulator + current_scalar * current_point);
85+
}
86+
let result = accumulator.mark::<NonZero>()?;
87+
Some(result.into_point_with_even_y().0.to_xonly())
88+
}
89+
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use super::{KeyAggHash, Musig, MIDSTATE_KEYAGGHASH};
94+
use bitcoin_hashes::hex::{FromHex, ToHex};
95+
use bitcoin_hashes::Hash;
96+
use secp256kfun::XOnly;
97+
use std::vec::Vec;
98+
99+
#[test]
100+
fn test_keyagghash() {
101+
let v =
102+
Vec::<u8>::from_hex("6ef02c5a06a480de1f2986651d1134f256a0b06352da4147f280d9d44484be15")
103+
.unwrap();
104+
assert_eq!(MIDSTATE_KEYAGGHASH.to_vec(), v);
105+
106+
let h = KeyAggHash::hash(b"");
107+
assert_eq!(
108+
h.to_hex(),
109+
"c73cff1ec19568213104330a946930c4ee2ea7c65a1c43973a038a372620a055"
110+
);
111+
}
112+
113+
#[test]
114+
fn test_combine() {
115+
// test taken from
116+
// https://github.com/ElementsProject/secp256k1-zkp/blob/5d2df0541960554be5c0ba58d86e5fa479935000/src/modules/musig/tests_impl.h
117+
let x1 = XOnly::from_bytes([
118+
0xF9, 0x30, 0x8A, 0x01, 0x92, 0x58, 0xC3, 0x10, 0x49, 0x34, 0x4F, 0x85, 0xF8, 0x9D,
119+
0x52, 0x29, 0xB5, 0x31, 0xC8, 0x45, 0x83, 0x6F, 0x99, 0xB0, 0x86, 0x01, 0xF1, 0x13,
120+
0xBC, 0xE0, 0x36, 0xF9,
121+
])
122+
.unwrap();
123+
let x2 = XOnly::from_bytes([
124+
0xDF, 0xF1, 0xD7, 0x7F, 0x2A, 0x67, 0x1C, 0x5F, 0x36, 0x18, 0x37, 0x26, 0xDB, 0x23,
125+
0x41, 0xBE, 0x58, 0xFE, 0xAE, 0x1D, 0xA2, 0xDE, 0xCE, 0xD8, 0x43, 0x24, 0x0F, 0x7B,
126+
0x50, 0x2B, 0xA6, 0x59,
127+
])
128+
.unwrap();
129+
let x3 = XOnly::from_bytes([
130+
0x35, 0x90, 0xA9, 0x4E, 0x76, 0x8F, 0x8E, 0x18, 0x15, 0xC2, 0xF2, 0x4B, 0x4D, 0x80,
131+
0xA8, 0xE3, 0x14, 0x93, 0x16, 0xC3, 0x51, 0x8C, 0xE7, 0xB7, 0xAD, 0x33, 0x83, 0x68,
132+
0xD0, 0x38, 0xCA, 0x66,
133+
])
134+
.unwrap();
135+
136+
let x1_x2_x3 = vec![x1, x2, x3];
137+
let expected_x1_x2_x3 = XOnly::from_bytes([
138+
0xEA, 0x06, 0x7B, 0x01, 0x67, 0x24, 0x5A, 0x6F, 0xED, 0xB1, 0xB1, 0x22, 0xBB, 0x03,
139+
0xAB, 0x7E, 0x5D, 0x48, 0x6C, 0x81, 0x83, 0x42, 0xE0, 0xE9, 0xB6, 0x41, 0x79, 0xAD,
140+
0x32, 0x8D, 0x9D, 0x19,
141+
])
142+
.unwrap();
143+
assert_eq!(
144+
Musig::new(&x1_x2_x3).unwrap().combine(),
145+
Some(expected_x1_x2_x3)
146+
);
147+
148+
let x3_x2_x1 = vec![x3, x2, x1];
149+
let expected_x3_x2_x1 = XOnly::from_bytes([
150+
0x14, 0xE1, 0xF8, 0x3E, 0x9E, 0x25, 0x60, 0xFB, 0x2A, 0x6C, 0x04, 0x24, 0x55, 0x6C,
151+
0x86, 0x8D, 0x9F, 0xB4, 0x63, 0x35, 0xD4, 0xF7, 0x8D, 0x22, 0x7D, 0x5D, 0x1D, 0x3C,
152+
0x89, 0x90, 0x6F, 0x1E,
153+
])
154+
.unwrap();
155+
assert_eq!(
156+
Musig::new(&x3_x2_x1).unwrap().combine(),
157+
Some(expected_x3_x2_x1)
158+
);
159+
160+
let x1_x1_x1 = vec![x1, x1, x1];
161+
let expected_x1_x1_x1 = XOnly::from_bytes([
162+
0x70, 0x28, 0x8D, 0xF2, 0xB7, 0x60, 0x3D, 0xBE, 0xA0, 0xC7, 0xB7, 0x41, 0xDD, 0xAA,
163+
0xB9, 0x46, 0x81, 0x14, 0x4E, 0x0B, 0x19, 0x08, 0x6C, 0x69, 0xB2, 0x34, 0x89, 0xE4,
164+
0xF5, 0xB7, 0x01, 0x9A,
165+
])
166+
.unwrap();
167+
assert_eq!(
168+
Musig::new(&x1_x1_x1).unwrap().combine(),
169+
Some(expected_x1_x1_x1)
170+
);
171+
172+
let x1_x1_x2_x2 = vec![x1, x1, x2, x2];
173+
let expected_x1_x1_x2_x2 = XOnly::from_bytes([
174+
0x93, 0xEE, 0xD8, 0x24, 0xF2, 0x3C, 0x5A, 0xE1, 0xC1, 0x05, 0xE7, 0x31, 0x09, 0x97,
175+
0x3F, 0xCD, 0x4A, 0xE3, 0x3A, 0x9F, 0xA0, 0x2F, 0x0A, 0xC8, 0x5A, 0x3E, 0x55, 0x89,
176+
0x07, 0x53, 0xB0, 0x67,
177+
])
178+
.unwrap();
179+
assert_eq!(
180+
Musig::new(&x1_x1_x2_x2).unwrap().combine(),
181+
Some(expected_x1_x1_x2_x2)
182+
);
183+
}
184+
}

0 commit comments

Comments
 (0)