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:
34
crypto/dkg/promote/Cargo.toml
Normal file
34
crypto/dkg/promote/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "dkg-promote"
|
||||
version = "0.6.0"
|
||||
description = "Promotions for keys from the dkg crate"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/promote"
|
||||
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, features = ["std"] }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../../transcript", version = "^0.3.2", default-features = false, features = ["std", "recommended"] }
|
||||
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false, features = ["std"] }
|
||||
dleq = { path = "../../dleq", version = "^0.4.1", default-features = false, features = ["std", "serialize"] }
|
||||
|
||||
dkg = { path = "../", default-features = false, features = ["std"] }
|
||||
|
||||
[dev-dependencies]
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["std", "zeroize_derive"] }
|
||||
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"] }
|
||||
21
crypto/dkg/promote/LICENSE
Normal file
21
crypto/dkg/promote/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/promote/README.md
Normal file
12
crypto/dkg/promote/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Distributed Key Generation - Promote
|
||||
|
||||
This crate implements 'promotions' for keys from the [`dkg`](https://docs.rs/dkg) crate. A promotion
|
||||
takes a set of keys and maps it to a different `Ciphersuite`.
|
||||
|
||||
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.
|
||||
168
crypto/dkg/promote/src/lib.rs
Normal file
168
crypto/dkg/promote/src/lib.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
// This crate requires `dleq` which doesn't support no-std via std-shims
|
||||
// #![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use core::{marker::PhantomData, ops::Deref};
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
use dleq::DLEqProof;
|
||||
|
||||
pub use dkg::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Errors encountered when promoting keys.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||
pub enum PromotionError {
|
||||
/// Invalid participant identifier.
|
||||
#[error("invalid participant (1 <= participant <= {n}, yet participant is {participant})")]
|
||||
InvalidParticipant {
|
||||
/// The total amount of participants.
|
||||
n: u16,
|
||||
/// The specified participant.
|
||||
participant: Participant,
|
||||
},
|
||||
|
||||
/// An incorrect amount of participants was specified.
|
||||
#[error("incorrect amount of participants. {t} <= amount <= {n}, yet amount is {amount}")]
|
||||
IncorrectAmountOfParticipants {
|
||||
/// The threshold required.
|
||||
t: u16,
|
||||
/// The total amount of participants.
|
||||
n: u16,
|
||||
/// The amount of participants specified.
|
||||
amount: usize,
|
||||
},
|
||||
|
||||
/// Participant provided an invalid proof.
|
||||
#[error("invalid proof {0}")]
|
||||
InvalidProof(Participant),
|
||||
}
|
||||
|
||||
fn transcript<G: GroupEncoding>(key: &G, i: Participant) -> RecommendedTranscript {
|
||||
let mut transcript = RecommendedTranscript::new(b"DKG Generator Promotion v0.2");
|
||||
transcript.append_message(b"group_key", key.to_bytes());
|
||||
transcript.append_message(b"participant", i.to_bytes());
|
||||
transcript
|
||||
}
|
||||
|
||||
/// Proof of valid promotion to another generator.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GeneratorProof<C: Ciphersuite> {
|
||||
share: C::G,
|
||||
proof: DLEqProof<C::G>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> GeneratorProof<C> {
|
||||
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(self.share.to_bytes().as_ref())?;
|
||||
self.proof.write(writer)
|
||||
}
|
||||
|
||||
pub fn read<R: Read>(reader: &mut R) -> io::Result<GeneratorProof<C>> {
|
||||
Ok(GeneratorProof {
|
||||
share: <C as Ciphersuite>::read_G(reader)?,
|
||||
proof: DLEqProof::read(reader)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = vec![];
|
||||
self.write(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
/// Promote a set of keys from one generator to another, where the elliptic curve is the same.
|
||||
///
|
||||
/// Since the Ciphersuite trait additionally specifies a generator, this provides an O(n) way to
|
||||
/// update the generator used with keys. This outperforms the key generation protocol which is
|
||||
/// exponential.
|
||||
pub struct GeneratorPromotion<C1: Ciphersuite, C2: Ciphersuite> {
|
||||
base: ThresholdKeys<C1>,
|
||||
proof: GeneratorProof<C1>,
|
||||
_c2: PhantomData<C2>,
|
||||
}
|
||||
|
||||
impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<C1, C2> {
|
||||
/// Begin promoting keys from one generator to another.
|
||||
///
|
||||
/// Returns a proof this share was properly promoted.
|
||||
pub fn promote<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
base: ThresholdKeys<C1>,
|
||||
) -> (GeneratorPromotion<C1, C2>, GeneratorProof<C1>) {
|
||||
// Do a DLEqProof for the new generator
|
||||
let proof = GeneratorProof {
|
||||
share: C2::generator() * base.secret_share().deref(),
|
||||
proof: DLEqProof::prove(
|
||||
rng,
|
||||
&mut transcript(&base.original_group_key(), base.params().i()),
|
||||
&[C1::generator(), C2::generator()],
|
||||
base.secret_share(),
|
||||
),
|
||||
};
|
||||
|
||||
(GeneratorPromotion { base, proof, _c2: PhantomData::<C2> }, proof)
|
||||
}
|
||||
|
||||
/// Complete promotion by taking in the proofs from all other participants.
|
||||
pub fn complete(
|
||||
self,
|
||||
proofs: &HashMap<Participant, GeneratorProof<C1>>,
|
||||
) -> Result<ThresholdKeys<C2>, PromotionError> {
|
||||
let params = self.base.params();
|
||||
if proofs.len() != (usize::from(params.n()) - 1) {
|
||||
Err(PromotionError::IncorrectAmountOfParticipants {
|
||||
t: params.n(),
|
||||
n: params.n(),
|
||||
amount: proofs.len() + 1,
|
||||
})?;
|
||||
}
|
||||
for i in proofs.keys().copied() {
|
||||
if u16::from(i) > params.n() {
|
||||
Err(PromotionError::InvalidParticipant { n: params.n(), participant: i })?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut verification_shares = HashMap::new();
|
||||
verification_shares.insert(params.i(), self.proof.share);
|
||||
for i in 1 ..= params.n() {
|
||||
let i = Participant::new(i).unwrap();
|
||||
if i == params.i() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let proof = proofs.get(&i).unwrap();
|
||||
proof
|
||||
.proof
|
||||
.verify(
|
||||
&mut transcript(&self.base.original_group_key(), i),
|
||||
&[C1::generator(), C2::generator()],
|
||||
&[self.base.original_verification_share(i), proof.share],
|
||||
)
|
||||
.map_err(|_| PromotionError::InvalidProof(i))?;
|
||||
verification_shares.insert(i, proof.share);
|
||||
}
|
||||
|
||||
Ok(
|
||||
ThresholdKeys::new(
|
||||
params,
|
||||
self.base.interpolation().clone(),
|
||||
self.base.secret_share().clone(),
|
||||
verification_shares,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
113
crypto/dkg/promote/src/tests.rs
Normal file
113
crypto/dkg/promote/src/tests.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use core::marker::PhantomData;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
use rand_core::OsRng;
|
||||
|
||||
use ciphersuite::{
|
||||
group::{ff::Field, Group},
|
||||
Ciphersuite, Ristretto,
|
||||
};
|
||||
|
||||
use dkg::*;
|
||||
use dkg_recovery::recover_key;
|
||||
use crate::{GeneratorPromotion, GeneratorProof};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
struct AltGenerator<C: Ciphersuite> {
|
||||
_curve: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Ciphersuite for AltGenerator<C> {
|
||||
type F = C::F;
|
||||
type G = C::G;
|
||||
type H = C::H;
|
||||
|
||||
const ID: &'static [u8] = b"Alternate Ciphersuite";
|
||||
|
||||
fn generator() -> Self::G {
|
||||
C::G::generator() * <C as Ciphersuite>::hash_to_F(b"DKG Promotion Test", b"generator")
|
||||
}
|
||||
|
||||
fn reduce_512(scalar: [u8; 64]) -> Self::F {
|
||||
<C as Ciphersuite>::reduce_512(scalar)
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||
<C as Ciphersuite>::hash_to_F(dst, data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone a map without a specific value.
|
||||
pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||
map: &HashMap<K, V>,
|
||||
without: &K,
|
||||
) -> HashMap<K, V> {
|
||||
let mut res = map.clone();
|
||||
res.remove(without).unwrap();
|
||||
res
|
||||
}
|
||||
|
||||
// Test promotion of threshold keys to another generator
|
||||
#[test]
|
||||
fn test_generator_promotion() {
|
||||
// Generate a set of `ThresholdKeys`
|
||||
const PARTICIPANTS: u16 = 5;
|
||||
let keys: [ThresholdKeys<_>; PARTICIPANTS as usize] = {
|
||||
let shares: [<Ristretto as Ciphersuite>::F; PARTICIPANTS as usize] =
|
||||
core::array::from_fn(|_| <Ristretto as Ciphersuite>::F::random(&mut OsRng));
|
||||
let verification_shares = (0 .. PARTICIPANTS)
|
||||
.map(|i| {
|
||||
(
|
||||
Participant::new(i + 1).unwrap(),
|
||||
<Ristretto as Ciphersuite>::generator() * shares[usize::from(i)],
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
core::array::from_fn(|i| {
|
||||
ThresholdKeys::new(
|
||||
ThresholdParams::new(
|
||||
PARTICIPANTS,
|
||||
PARTICIPANTS,
|
||||
Participant::new(u16::try_from(i + 1).unwrap()).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
Interpolation::Constant(vec![<Ristretto as Ciphersuite>::F::ONE; PARTICIPANTS as usize]),
|
||||
Zeroizing::new(shares[i]),
|
||||
verification_shares.clone(),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
};
|
||||
|
||||
// Perform the promotion
|
||||
let mut promotions = HashMap::new();
|
||||
let mut proofs = HashMap::new();
|
||||
for keys in &keys {
|
||||
let i = keys.params().i();
|
||||
let (promotion, proof) =
|
||||
GeneratorPromotion::<_, AltGenerator<Ristretto>>::promote(&mut OsRng, keys.clone());
|
||||
promotions.insert(i, promotion);
|
||||
proofs.insert(
|
||||
i,
|
||||
GeneratorProof::<Ristretto>::read::<&[u8]>(&mut proof.serialize().as_ref()).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// Complete the promotion, and verify it worked
|
||||
let new_group_key = AltGenerator::<Ristretto>::generator() * *recover_key(&keys).unwrap();
|
||||
for (i, promoting) in promotions.drain() {
|
||||
let promoted = promoting.complete(&clone_without(&proofs, &i)).unwrap();
|
||||
assert_eq!(keys[usize::from(u16::from(i) - 1)].params(), promoted.params());
|
||||
assert_eq!(keys[usize::from(u16::from(i) - 1)].secret_share(), promoted.secret_share());
|
||||
assert_eq!(new_group_key, promoted.group_key());
|
||||
for l in 0 .. PARTICIPANTS {
|
||||
let verification_share =
|
||||
promoted.original_verification_share(Participant::new(l + 1).unwrap());
|
||||
assert_eq!(
|
||||
AltGenerator::<Ristretto>::generator() * **keys[usize::from(l)].secret_share(),
|
||||
verification_share
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user