mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Implement MuSig key aggregation into DKG
Isn't spec compliant due to the lack of a spec to be compliant too. Slight deviation from the paper by using a unique list instead of a multiset. Closes #186, progresses #277.
This commit is contained in:
@@ -22,6 +22,9 @@ use ciphersuite::{
|
|||||||
/// Encryption types and utilities used to secure DKG messages.
|
/// Encryption types and utilities used to secure DKG messages.
|
||||||
pub mod encryption;
|
pub mod encryption;
|
||||||
|
|
||||||
|
mod musig;
|
||||||
|
pub use musig::musig;
|
||||||
|
|
||||||
/// The distributed key generation protocol described in the
|
/// The distributed key generation protocol described in the
|
||||||
/// [FROST paper](https://eprint.iacr.org/2020/852).
|
/// [FROST paper](https://eprint.iacr.org/2020/852).
|
||||||
pub mod frost;
|
pub mod frost;
|
||||||
|
|||||||
92
crypto/dkg/src/musig.rs
Normal file
92
crypto/dkg/src/musig.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use core::ops::Deref;
|
||||||
|
use std::collections::{HashSet, HashMap};
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
|
||||||
|
use ciphersuite::{
|
||||||
|
group::{Group, GroupEncoding},
|
||||||
|
Ciphersuite,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{Participant, DkgError, ThresholdParams, ThresholdCore, lagrange};
|
||||||
|
|
||||||
|
/// A n-of-n non-interactive DKG which does not guarantee the usability of the resulting key.
|
||||||
|
///
|
||||||
|
/// Creating a key with duplicated public keys returns an error.
|
||||||
|
pub fn musig<C: Ciphersuite>(
|
||||||
|
private_key: &Zeroizing<C::F>,
|
||||||
|
keys: &[C::G],
|
||||||
|
) -> Result<ThresholdCore<C>, DkgError<()>> {
|
||||||
|
if keys.is_empty() {
|
||||||
|
Err(DkgError::InvalidSigningSet)?;
|
||||||
|
}
|
||||||
|
// Too many signers
|
||||||
|
let keys_len = u16::try_from(keys.len()).map_err(|_| DkgError::InvalidSigningSet)?;
|
||||||
|
|
||||||
|
// Duplicated public keys
|
||||||
|
if keys.iter().map(|key| key.to_bytes().as_ref().to_vec()).collect::<HashSet<_>>().len() !=
|
||||||
|
keys.len()
|
||||||
|
{
|
||||||
|
Err(DkgError::InvalidSigningSet)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let our_pub_key = C::generator() * private_key.deref();
|
||||||
|
let Some(pos) = keys.iter().position(|key| *key == our_pub_key) else {
|
||||||
|
// Not present in signing set
|
||||||
|
Err(DkgError::InvalidSigningSet)?
|
||||||
|
};
|
||||||
|
let params = ThresholdParams::new(
|
||||||
|
keys_len,
|
||||||
|
keys_len,
|
||||||
|
// These errors shouldn't be possible, as pos is bounded to len - 1
|
||||||
|
// Since len is prior guaranteed to be within u16::MAX, pos + 1 must also be
|
||||||
|
Participant::new((pos + 1).try_into().map_err(|_| DkgError::InvalidSigningSet)?)
|
||||||
|
.ok_or(DkgError::InvalidSigningSet)?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Calculate the binding factor per-key
|
||||||
|
let mut transcript = RecommendedTranscript::new(b"DKG MuSig v0.5");
|
||||||
|
transcript.domain_separate(b"musig_binding_factors");
|
||||||
|
for key in keys {
|
||||||
|
transcript.append_message(b"key", key.to_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut binding = Vec::with_capacity(keys.len());
|
||||||
|
for i in 1 ..= keys_len {
|
||||||
|
let mut transcript = transcript.clone();
|
||||||
|
transcript.append_message(b"participant", i.to_le_bytes());
|
||||||
|
binding
|
||||||
|
.push(C::hash_to_F(b"DKG-MuSig-binding_factor", &transcript.challenge(b"binding_factor")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply our private key by our binding factor
|
||||||
|
let mut secret_share = private_key.clone();
|
||||||
|
*secret_share *= binding[pos];
|
||||||
|
|
||||||
|
// Calculate verification shares
|
||||||
|
let mut verification_shares = HashMap::new();
|
||||||
|
// When this library generates shares for a specific signing set, it applies the lagrange
|
||||||
|
// coefficient
|
||||||
|
// Since this is a n-of-n scheme, there's only one possible signing set, and one possible
|
||||||
|
// lagrange factor
|
||||||
|
// Define the group key as the sum of all verification shares, post-lagrange
|
||||||
|
// While we could invert our lagrange factor and multiply it by our secret share, so the group
|
||||||
|
// key wasn't post-lagrange, the inversion is ~300 multiplications and we'd have to apply similar
|
||||||
|
// inversions + multiplications to all verification shares
|
||||||
|
// Accordingly, it'd never be more performant, though it would simplify group key calculation
|
||||||
|
let included = (1 ..= keys_len)
|
||||||
|
// This error also shouldn't be possible, for the same reasons as documented above
|
||||||
|
.map(|l| Participant::new(l).ok_or(DkgError::InvalidSigningSet))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let mut group_key = C::G::identity();
|
||||||
|
for (l, p) in included.iter().enumerate() {
|
||||||
|
let verification_share = keys[l] * binding[l];
|
||||||
|
group_key += verification_share * lagrange::<C::F>(*p, &included);
|
||||||
|
verification_shares.insert(*p, verification_share);
|
||||||
|
}
|
||||||
|
debug_assert_eq!(C::generator() * secret_share.deref(), verification_shares[¶ms.i()]);
|
||||||
|
|
||||||
|
Ok(ThresholdCore { params, secret_share, group_key, verification_shares })
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||||
|
|
||||||
use crate::{Participant, ThresholdCore, ThresholdKeys, lagrange};
|
use crate::{Participant, ThresholdCore, ThresholdKeys, lagrange, musig as musig_fn};
|
||||||
|
|
||||||
|
mod musig;
|
||||||
|
pub use musig::test_musig;
|
||||||
|
|
||||||
/// FROST key generation testing utility.
|
/// FROST key generation testing utility.
|
||||||
pub mod frost;
|
pub mod frost;
|
||||||
@@ -63,6 +67,28 @@ pub fn key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate MuSig keys for tests.
|
||||||
|
pub fn musig_key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||||
|
rng: &mut R,
|
||||||
|
) -> HashMap<Participant, ThresholdKeys<C>> {
|
||||||
|
let mut keys = vec![];
|
||||||
|
let mut pub_keys = vec![];
|
||||||
|
for _ in 0 .. PARTICIPANTS {
|
||||||
|
let key = Zeroizing::new(C::F::random(&mut *rng));
|
||||||
|
pub_keys.push(C::generator() * *key);
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut res = HashMap::new();
|
||||||
|
for key in keys {
|
||||||
|
let these_keys = musig_fn::<C>(&key, &pub_keys).unwrap();
|
||||||
|
res.insert(these_keys.params().i(), ThresholdKeys::new(these_keys));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(C::generator() * recover_key(&res), res[&Participant(1)].group_key());
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the test suite on a ciphersuite.
|
/// Run the test suite on a ciphersuite.
|
||||||
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
||||||
key_gen::<_, C>(rng);
|
key_gen::<_, C>(rng);
|
||||||
|
|||||||
61
crypto/dkg/src/tests/musig.rs
Normal file
61
crypto/dkg/src/tests/musig.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ThresholdKeys, musig,
|
||||||
|
tests::{PARTICIPANTS, recover_key},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Tests MuSig key generation.
|
||||||
|
pub fn test_musig<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
||||||
|
let mut keys = vec![];
|
||||||
|
let mut pub_keys = vec![];
|
||||||
|
for _ in 0 .. PARTICIPANTS {
|
||||||
|
let key = Zeroizing::new(C::F::random(&mut *rng));
|
||||||
|
pub_keys.push(C::generator() * *key);
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty signing set
|
||||||
|
assert!(musig::<C>(&Zeroizing::new(C::F::ZERO), &[]).is_err());
|
||||||
|
// Signing set we're not part of
|
||||||
|
assert!(musig::<C>(&Zeroizing::new(C::F::ZERO), &[C::generator()]).is_err());
|
||||||
|
|
||||||
|
// Test with n keys
|
||||||
|
{
|
||||||
|
let mut created_keys = HashMap::new();
|
||||||
|
let mut verification_shares = HashMap::new();
|
||||||
|
let mut group_key = None;
|
||||||
|
for (i, key) in keys.iter().enumerate() {
|
||||||
|
let these_keys = musig::<C>(key, &pub_keys).unwrap();
|
||||||
|
assert_eq!(these_keys.params().t(), PARTICIPANTS);
|
||||||
|
assert_eq!(these_keys.params().n(), PARTICIPANTS);
|
||||||
|
assert_eq!(usize::from(these_keys.params().i().0), i + 1);
|
||||||
|
|
||||||
|
verification_shares
|
||||||
|
.insert(these_keys.params().i(), C::generator() * **these_keys.secret_share());
|
||||||
|
|
||||||
|
if group_key.is_none() {
|
||||||
|
group_key = Some(these_keys.group_key());
|
||||||
|
}
|
||||||
|
assert_eq!(these_keys.group_key(), group_key.unwrap());
|
||||||
|
|
||||||
|
created_keys.insert(these_keys.params().i(), ThresholdKeys::new(these_keys));
|
||||||
|
}
|
||||||
|
|
||||||
|
for keys in created_keys.values() {
|
||||||
|
assert_eq!(keys.verification_shares(), verification_shares);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(C::generator() * recover_key(&created_keys), group_key.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn musig_literal() {
|
||||||
|
test_musig::<_, ciphersuite::Ristretto>(&mut rand_core::OsRng)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
pub use dkg::tests::{key_gen, recover_key};
|
pub use dkg::tests::{key_gen, musig_key_gen, recover_key};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Curve, Participant, ThresholdKeys, FrostError,
|
Curve, Participant, ThresholdKeys, FrostError,
|
||||||
@@ -24,7 +24,7 @@ mod literal;
|
|||||||
/// Constant amount of participants to use when testing.
|
/// Constant amount of participants to use when testing.
|
||||||
pub const PARTICIPANTS: u16 = 5;
|
pub const PARTICIPANTS: u16 = 5;
|
||||||
/// Constant threshold of participants to use when signing.
|
/// Constant threshold of participants to use when signing.
|
||||||
pub const THRESHOLD: u16 = ((PARTICIPANTS / 3) * 2) + 1;
|
pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1;
|
||||||
|
|
||||||
/// Clone a map without a specific value.
|
/// Clone a map without a specific value.
|
||||||
pub fn clone_without<K: Clone + std::cmp::Eq + std::hash::Hash, V: Clone>(
|
pub fn clone_without<K: Clone + std::cmp::Eq + std::hash::Hash, V: Clone>(
|
||||||
@@ -192,17 +192,31 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test a basic Schnorr signature.
|
/// Test a basic Schnorr signature with the provided keys.
|
||||||
pub fn test_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
pub fn test_schnorr_with_keys<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
||||||
|
rng: &mut R,
|
||||||
|
keys: HashMap<Participant, ThresholdKeys<C>>,
|
||||||
|
) {
|
||||||
const MSG: &[u8] = b"Hello, World!";
|
const MSG: &[u8] = b"Hello, World!";
|
||||||
|
|
||||||
let keys = key_gen(&mut *rng);
|
|
||||||
let machines = algorithm_machines(&mut *rng, IetfSchnorr::<C, H>::ietf(), &keys);
|
let machines = algorithm_machines(&mut *rng, IetfSchnorr::<C, H>::ietf(), &keys);
|
||||||
let sig = sign(&mut *rng, IetfSchnorr::<C, H>::ietf(), keys.clone(), machines, MSG);
|
let sig = sign(&mut *rng, IetfSchnorr::<C, H>::ietf(), keys.clone(), machines, MSG);
|
||||||
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||||
assert!(sig.verify(group_key, H::hram(&sig.R, &group_key, MSG)));
|
assert!(sig.verify(group_key, H::hram(&sig.R, &group_key, MSG)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test a basic Schnorr signature.
|
||||||
|
pub fn test_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||||
|
let keys = key_gen(&mut *rng);
|
||||||
|
test_schnorr_with_keys::<_, _, H>(&mut *rng, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test a basic Schnorr signature, yet with MuSig.
|
||||||
|
pub fn test_musig_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||||
|
let keys = musig_key_gen(&mut *rng);
|
||||||
|
test_schnorr_with_keys::<_, _, H>(&mut *rng, keys)
|
||||||
|
}
|
||||||
|
|
||||||
/// Test an offset Schnorr signature.
|
/// Test an offset Schnorr signature.
|
||||||
pub fn test_offset_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
pub fn test_offset_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||||
const MSG: &[u8] = b"Hello, World!";
|
const MSG: &[u8] = b"Hello, World!";
|
||||||
@@ -248,6 +262,7 @@ pub fn test_schnorr_blame<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mu
|
|||||||
/// Run a variety of tests against a ciphersuite.
|
/// Run a variety of tests against a ciphersuite.
|
||||||
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||||
test_schnorr::<R, C, H>(rng);
|
test_schnorr::<R, C, H>(rng);
|
||||||
|
test_musig_schnorr::<R, C, H>(rng);
|
||||||
test_offset_schnorr::<R, C, H>(rng);
|
test_offset_schnorr::<R, C, H>(rng);
|
||||||
test_schnorr_blame::<R, C, H>(rng);
|
test_schnorr_blame::<R, C, H>(rng);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user