mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Smash dkg into dkg, dkg-[recovery, promote, musig, pedpop]
promote and pedpop require dleq, which don't support no-std. All three should be moved outside the Serai repository, per #597, as none are planned for use and worth covering under our BBP.
This commit is contained in:
49
crypto/dkg/musig/Cargo.toml
Normal file
49
crypto/dkg/musig/Cargo.toml
Normal file
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "dkg-musig"
|
||||
version = "0.6.0"
|
||||
description = "The MuSig key aggregation protocol"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/musig"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { version = "2", default-features = false }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
|
||||
std-shims = { version = "0.1", path = "../../../common/std-shims", default-features = false }
|
||||
|
||||
multiexp = { path = "../../multiexp", version = "0.4", default-features = false }
|
||||
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false }
|
||||
dkg = { path = "../", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
||||
ciphersuite = { path = "../../ciphersuite", default-features = false, features = ["ristretto"] }
|
||||
dkg-recovery = { path = "../recovery", default-features = false, features = ["std"] }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"thiserror/std",
|
||||
|
||||
"rand_core/std",
|
||||
|
||||
"std-shims/std",
|
||||
|
||||
"multiexp/std",
|
||||
"ciphersuite/std",
|
||||
"dkg/std",
|
||||
]
|
||||
default = ["std"]
|
||||
21
crypto/dkg/musig/LICENSE
Normal file
21
crypto/dkg/musig/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-2025 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
12
crypto/dkg/musig/README.md
Normal file
12
crypto/dkg/musig/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Distributed Key Generation - MuSig
|
||||
|
||||
This implements the MuSig key aggregation protocol for the [`dkg`](https://docs.rs/dkg) crate's
|
||||
types.
|
||||
|
||||
This crate was originally part of the `dkg` crate, which was
|
||||
[audited by Cypher Stack in March 2023](
|
||||
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||
), culminating in commit
|
||||
[669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||
). Any subsequent changes have not undergone auditing.
|
||||
162
crypto/dkg/musig/src/lib.rs
Normal file
162
crypto/dkg/musig/src/lib.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use core::ops::Deref;
|
||||
use std_shims::{
|
||||
vec,
|
||||
vec::Vec,
|
||||
collections::{HashSet, HashMap},
|
||||
};
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
||||
|
||||
pub use dkg::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Errors encountered when working with threshold keys.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||
pub enum MusigError<C: Ciphersuite> {
|
||||
/// No keys were provided.
|
||||
#[error("no keys provided")]
|
||||
NoKeysProvided,
|
||||
/// Too many keys were provided.
|
||||
#[error("too many keys (allowed {max}, provided {provided})")]
|
||||
TooManyKeysProvided {
|
||||
/// The maximum amount of keys allowed.
|
||||
max: u16,
|
||||
/// The amount of keys provided.
|
||||
provided: usize,
|
||||
},
|
||||
/// A participant was duplicated.
|
||||
#[error("a participant was duplicated")]
|
||||
DuplicatedParticipant(C::G),
|
||||
/// Participating, yet our public key wasn't found in the list of keys.
|
||||
#[error("private key's public key wasn't present in the list of public keys")]
|
||||
NotPresent,
|
||||
/// An error propagated from the underlying `dkg` crate.
|
||||
#[error("error from dkg ({0})")]
|
||||
DkgError(DkgError),
|
||||
}
|
||||
|
||||
fn check_keys<C: Ciphersuite>(keys: &[C::G]) -> Result<u16, MusigError<C>> {
|
||||
if keys.is_empty() {
|
||||
Err(MusigError::NoKeysProvided)?;
|
||||
}
|
||||
|
||||
let keys_len = u16::try_from(keys.len())
|
||||
.map_err(|_| MusigError::TooManyKeysProvided { max: u16::MAX, provided: keys.len() })?;
|
||||
|
||||
let mut set = HashSet::with_capacity(keys.len());
|
||||
for key in keys {
|
||||
let bytes = key.to_bytes().as_ref().to_vec();
|
||||
if !set.insert(bytes) {
|
||||
Err(MusigError::DuplicatedParticipant(*key))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(keys_len)
|
||||
}
|
||||
|
||||
fn binding_factor_transcript<C: Ciphersuite>(
|
||||
context: [u8; 32],
|
||||
keys_len: u16,
|
||||
keys: &[C::G],
|
||||
) -> Vec<u8> {
|
||||
debug_assert_eq!(usize::from(keys_len), keys.len());
|
||||
|
||||
let mut transcript = vec![];
|
||||
transcript.extend(&context);
|
||||
transcript.extend(keys_len.to_le_bytes());
|
||||
for key in keys {
|
||||
transcript.extend(key.to_bytes().as_ref());
|
||||
}
|
||||
transcript
|
||||
}
|
||||
|
||||
fn binding_factor<C: Ciphersuite>(mut transcript: Vec<u8>, i: u16) -> C::F {
|
||||
transcript.extend(i.to_le_bytes());
|
||||
C::hash_to_F(b"dkg-musig", &transcript)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn musig_key_multiexp<C: Ciphersuite>(
|
||||
context: [u8; 32],
|
||||
keys: &[C::G],
|
||||
) -> Result<Vec<(C::F, C::G)>, MusigError<C>> {
|
||||
let keys_len = check_keys::<C>(keys)?;
|
||||
let transcript = binding_factor_transcript::<C>(context, keys_len, keys);
|
||||
let mut multiexp = Vec::with_capacity(keys.len());
|
||||
for i in 1 ..= keys_len {
|
||||
multiexp.push((binding_factor::<C>(transcript.clone(), i), keys[usize::from(i - 1)]));
|
||||
}
|
||||
Ok(multiexp)
|
||||
}
|
||||
|
||||
/// The group key resulting from using this library's MuSig key aggregation.
|
||||
///
|
||||
/// This function executes in variable time and MUST NOT be used with secret data.
|
||||
pub fn musig_key_vartime<C: Ciphersuite>(
|
||||
context: [u8; 32],
|
||||
keys: &[C::G],
|
||||
) -> Result<C::G, MusigError<C>> {
|
||||
Ok(multiexp::multiexp_vartime(&musig_key_multiexp(context, keys)?))
|
||||
}
|
||||
|
||||
/// The group key resulting from using this library's MuSig key aggregation.
|
||||
pub fn musig_key<C: Ciphersuite>(context: [u8; 32], keys: &[C::G]) -> Result<C::G, MusigError<C>> {
|
||||
Ok(multiexp::multiexp(&musig_key_multiexp(context, keys)?))
|
||||
}
|
||||
|
||||
/// A n-of-n non-interactive DKG which does not guarantee the usability of the resulting key.
|
||||
pub fn musig<C: Ciphersuite>(
|
||||
context: [u8; 32],
|
||||
private_key: Zeroizing<C::F>,
|
||||
keys: &[C::G],
|
||||
) -> Result<ThresholdKeys<C>, MusigError<C>> {
|
||||
let our_pub_key = C::generator() * private_key.deref();
|
||||
let Some(our_i) = keys.iter().position(|key| *key == our_pub_key) else {
|
||||
Err(MusigError::DkgError(DkgError::NotParticipating))?
|
||||
};
|
||||
|
||||
let keys_len: u16 = check_keys::<C>(keys)?;
|
||||
|
||||
let params = ThresholdParams::new(
|
||||
keys_len,
|
||||
keys_len,
|
||||
// The `+ 1` won't fail as `keys.len() <= u16::MAX`, so any index is `< u16::MAX`
|
||||
Participant::new(
|
||||
u16::try_from(our_i).expect("keys.len() <= u16::MAX yet index of keys > u16::MAX?") + 1,
|
||||
)
|
||||
.expect("i + 1 != 0"),
|
||||
)
|
||||
.map_err(MusigError::DkgError)?;
|
||||
|
||||
let transcript = binding_factor_transcript::<C>(context, keys_len, keys);
|
||||
let mut binding_factors = Vec::with_capacity(keys.len());
|
||||
let mut multiexp = Vec::with_capacity(keys.len());
|
||||
let mut verification_shares = HashMap::with_capacity(keys.len());
|
||||
for (i, key) in (1 ..= keys_len).zip(keys.iter().copied()) {
|
||||
let binding_factor = binding_factor::<C>(transcript.clone(), i);
|
||||
binding_factors.push(binding_factor);
|
||||
multiexp.push((binding_factor, key));
|
||||
|
||||
let i = Participant::new(i).expect("non-zero u16 wasn't a valid Participant index?");
|
||||
verification_shares.insert(i, key);
|
||||
}
|
||||
let group_key = multiexp::multiexp(&multiexp);
|
||||
debug_assert_eq!(our_pub_key, verification_shares[¶ms.i()]);
|
||||
debug_assert_eq!(musig_key_vartime::<C>(context, keys).unwrap(), group_key);
|
||||
|
||||
ThresholdKeys::new(
|
||||
params,
|
||||
Interpolation::Constant(binding_factors),
|
||||
private_key,
|
||||
verification_shares,
|
||||
)
|
||||
.map_err(MusigError::DkgError)
|
||||
}
|
||||
70
crypto/dkg/musig/src/tests.rs
Normal file
70
crypto/dkg/musig/src/tests.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
use rand_core::OsRng;
|
||||
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite, Ristretto};
|
||||
|
||||
use dkg_recovery::recover_key;
|
||||
use crate::*;
|
||||
|
||||
/// Tests MuSig key generation.
|
||||
#[test]
|
||||
pub fn test_musig() {
|
||||
const PARTICIPANTS: u16 = 5;
|
||||
|
||||
let mut keys = vec![];
|
||||
let mut pub_keys = vec![];
|
||||
for _ in 0 .. PARTICIPANTS {
|
||||
let key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
|
||||
pub_keys.push(<Ristretto as Ciphersuite>::generator() * *key);
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
const CONTEXT: [u8; 32] = *b"MuSig Test ";
|
||||
|
||||
// Empty signing set
|
||||
musig::<Ristretto>(CONTEXT, Zeroizing::new(<Ristretto as Ciphersuite>::F::ZERO), &[])
|
||||
.unwrap_err();
|
||||
// Signing set we're not part of
|
||||
musig::<Ristretto>(
|
||||
CONTEXT,
|
||||
Zeroizing::new(<Ristretto as Ciphersuite>::F::ZERO),
|
||||
&[<Ristretto as Ciphersuite>::generator()],
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
// Test with n keys
|
||||
{
|
||||
let mut created_keys = HashMap::new();
|
||||
let mut verification_shares = HashMap::new();
|
||||
let group_key = musig_key::<Ristretto>(CONTEXT, &pub_keys).unwrap();
|
||||
for (i, key) in keys.iter().enumerate() {
|
||||
let these_keys = musig::<Ristretto>(CONTEXT, key.clone(), &pub_keys).unwrap();
|
||||
assert_eq!(these_keys.params().t(), PARTICIPANTS);
|
||||
assert_eq!(these_keys.params().n(), PARTICIPANTS);
|
||||
assert_eq!(usize::from(u16::from(these_keys.params().i())), i + 1);
|
||||
|
||||
verification_shares.insert(
|
||||
these_keys.params().i(),
|
||||
<Ristretto as Ciphersuite>::generator() * **these_keys.secret_share(),
|
||||
);
|
||||
|
||||
assert_eq!(these_keys.group_key(), group_key);
|
||||
|
||||
created_keys.insert(these_keys.params().i(), these_keys);
|
||||
}
|
||||
|
||||
for keys in created_keys.values() {
|
||||
for (l, verification_share) in &verification_shares {
|
||||
assert_eq!(keys.original_verification_share(*l), *verification_share);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
<Ristretto as Ciphersuite>::generator() *
|
||||
*recover_key(&created_keys.values().cloned().collect::<Vec<_>>()).unwrap(),
|
||||
group_key
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user