mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-12 22:19:26 +00:00
Have the DKG explicitly declare how to interpolate its shares
Removes the hack for MuSig where we multiply keys by the inverse of their lagrange interpolation factor.
This commit is contained in:
@@ -88,7 +88,7 @@ use multiexp::multiexp_vartime;
|
||||
use generalized_bulletproofs::arithmetic_circuit_proof::*;
|
||||
use ec_divisors::DivisorCurve;
|
||||
|
||||
use crate::{Participant, ThresholdParams, ThresholdCore, ThresholdKeys};
|
||||
use crate::{Participant, ThresholdParams, Interpolation, ThresholdCore, ThresholdKeys};
|
||||
|
||||
pub(crate) mod proof;
|
||||
use proof::*;
|
||||
@@ -571,6 +571,7 @@ impl<C: EvrfCurve> EvrfDkg<C> {
|
||||
|
||||
res.push(ThresholdKeys::from(ThresholdCore {
|
||||
params: ThresholdParams::new(self.t, self.n, i).unwrap(),
|
||||
interpolation: Interpolation::Lagrange,
|
||||
secret_share,
|
||||
group_key: self.group_key,
|
||||
verification_shares: self.verification_shares.clone(),
|
||||
|
||||
@@ -209,25 +209,42 @@ mod lib {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the lagrange coefficient for a signing set.
|
||||
pub fn lagrange<F: PrimeField>(i: Participant, included: &[Participant]) -> F {
|
||||
let i_f = F::from(u64::from(u16::from(i)));
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize))]
|
||||
pub(crate) enum Interpolation {
|
||||
None,
|
||||
Lagrange,
|
||||
}
|
||||
|
||||
let mut num = F::ONE;
|
||||
let mut denom = F::ONE;
|
||||
for l in included {
|
||||
if i == *l {
|
||||
continue;
|
||||
impl Interpolation {
|
||||
pub(crate) fn interpolation_factor<F: PrimeField>(
|
||||
self,
|
||||
i: Participant,
|
||||
included: &[Participant],
|
||||
) -> F {
|
||||
match self {
|
||||
Interpolation::None => F::ONE,
|
||||
Interpolation::Lagrange => {
|
||||
let i_f = F::from(u64::from(u16::from(i)));
|
||||
|
||||
let mut num = F::ONE;
|
||||
let mut denom = F::ONE;
|
||||
for l in included {
|
||||
if i == *l {
|
||||
continue;
|
||||
}
|
||||
|
||||
let share = F::from(u64::from(u16::from(*l)));
|
||||
num *= share;
|
||||
denom *= share - i_f;
|
||||
}
|
||||
|
||||
// Safe as this will only be 0 if we're part of the above loop
|
||||
// (which we have an if case to avoid)
|
||||
num * denom.invert().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
let share = F::from(u64::from(u16::from(*l)));
|
||||
num *= share;
|
||||
denom *= share - i_f;
|
||||
}
|
||||
|
||||
// Safe as this will only be 0 if we're part of the above loop
|
||||
// (which we have an if case to avoid)
|
||||
num * denom.invert().unwrap()
|
||||
}
|
||||
|
||||
/// Keys and verification shares generated by a DKG.
|
||||
@@ -236,6 +253,8 @@ mod lib {
|
||||
pub struct ThresholdCore<C: Ciphersuite> {
|
||||
/// Threshold Parameters.
|
||||
pub(crate) params: ThresholdParams,
|
||||
/// The interpolation method used.
|
||||
pub(crate) interpolation: Interpolation,
|
||||
|
||||
/// Secret share key.
|
||||
pub(crate) secret_share: Zeroizing<C::F>,
|
||||
@@ -250,6 +269,7 @@ mod lib {
|
||||
fmt
|
||||
.debug_struct("ThresholdCore")
|
||||
.field("params", &self.params)
|
||||
.field("interpolation", &self.interpolation)
|
||||
.field("group_key", &self.group_key)
|
||||
.field("verification_shares", &self.verification_shares)
|
||||
.finish_non_exhaustive()
|
||||
@@ -259,6 +279,7 @@ mod lib {
|
||||
impl<C: Ciphersuite> Zeroize for ThresholdCore<C> {
|
||||
fn zeroize(&mut self) {
|
||||
self.params.zeroize();
|
||||
self.interpolation.zeroize();
|
||||
self.secret_share.zeroize();
|
||||
self.group_key.zeroize();
|
||||
for share in self.verification_shares.values_mut() {
|
||||
@@ -270,14 +291,19 @@ mod lib {
|
||||
impl<C: Ciphersuite> ThresholdCore<C> {
|
||||
pub(crate) fn new(
|
||||
params: ThresholdParams,
|
||||
interpolation: Interpolation,
|
||||
secret_share: Zeroizing<C::F>,
|
||||
verification_shares: HashMap<Participant, C::G>,
|
||||
) -> ThresholdCore<C> {
|
||||
let t = (1 ..= params.t()).map(Participant).collect::<Vec<_>>();
|
||||
ThresholdCore {
|
||||
params,
|
||||
interpolation,
|
||||
secret_share,
|
||||
group_key: t.iter().map(|i| verification_shares[i] * lagrange::<C::F>(*i, &t)).sum(),
|
||||
group_key: t
|
||||
.iter()
|
||||
.map(|i| verification_shares[i] * interpolation.interpolation_factor::<C::F>(*i, &t))
|
||||
.sum(),
|
||||
verification_shares,
|
||||
}
|
||||
}
|
||||
@@ -308,6 +334,10 @@ mod lib {
|
||||
writer.write_all(&self.params.t.to_le_bytes())?;
|
||||
writer.write_all(&self.params.n.to_le_bytes())?;
|
||||
writer.write_all(&self.params.i.to_bytes())?;
|
||||
writer.write_all(match self.interpolation {
|
||||
Interpolation::None => &[0],
|
||||
Interpolation::Lagrange => &[1],
|
||||
})?;
|
||||
let mut share_bytes = self.secret_share.to_repr();
|
||||
writer.write_all(share_bytes.as_ref())?;
|
||||
share_bytes.as_mut().zeroize();
|
||||
@@ -356,6 +386,14 @@ mod lib {
|
||||
)
|
||||
};
|
||||
|
||||
let mut interpolation = [0];
|
||||
reader.read_exact(&mut interpolation)?;
|
||||
let interpolation = match interpolation[0] {
|
||||
0 => Interpolation::None,
|
||||
1 => Interpolation::Lagrange,
|
||||
_ => Err(io::Error::other("invalid interpolation method"))?,
|
||||
};
|
||||
|
||||
let secret_share = Zeroizing::new(C::read_F(reader)?);
|
||||
|
||||
let mut verification_shares = HashMap::new();
|
||||
@@ -365,6 +403,7 @@ mod lib {
|
||||
|
||||
Ok(ThresholdCore::new(
|
||||
ThresholdParams::new(t, n, i).map_err(|_| io::Error::other("invalid parameters"))?,
|
||||
interpolation,
|
||||
secret_share,
|
||||
verification_shares,
|
||||
))
|
||||
@@ -387,6 +426,7 @@ mod lib {
|
||||
/// View of keys, interpolated and offset for usage.
|
||||
#[derive(Clone)]
|
||||
pub struct ThresholdView<C: Ciphersuite> {
|
||||
interpolation: Interpolation,
|
||||
offset: C::F,
|
||||
group_key: C::G,
|
||||
included: Vec<Participant>,
|
||||
@@ -399,6 +439,7 @@ mod lib {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt
|
||||
.debug_struct("ThresholdView")
|
||||
.field("interpolation", &self.interpolation)
|
||||
.field("offset", &self.offset)
|
||||
.field("group_key", &self.group_key)
|
||||
.field("included", &self.included)
|
||||
@@ -484,12 +525,13 @@ mod lib {
|
||||
included.sort();
|
||||
|
||||
let mut secret_share = Zeroizing::new(
|
||||
lagrange::<C::F>(self.params().i(), &included) * self.secret_share().deref(),
|
||||
self.core.interpolation.interpolation_factor::<C::F>(self.params().i(), &included) *
|
||||
self.secret_share().deref(),
|
||||
);
|
||||
|
||||
let mut verification_shares = self.verification_shares();
|
||||
for (i, share) in &mut verification_shares {
|
||||
*share *= lagrange::<C::F>(*i, &included);
|
||||
*share *= self.core.interpolation.interpolation_factor::<C::F>(*i, &included);
|
||||
}
|
||||
|
||||
// The offset is included by adding it to the participant with the lowest ID
|
||||
@@ -500,6 +542,7 @@ mod lib {
|
||||
*verification_shares.get_mut(&included[0]).unwrap() += C::generator() * offset;
|
||||
|
||||
Ok(ThresholdView {
|
||||
interpolation: self.core.interpolation,
|
||||
offset,
|
||||
group_key: self.group_key(),
|
||||
secret_share,
|
||||
@@ -532,6 +575,14 @@ mod lib {
|
||||
&self.included
|
||||
}
|
||||
|
||||
/// Return the interpolation factor for a signer.
|
||||
pub fn interpolation_factor(&self, participant: Participant) -> Option<C::F> {
|
||||
if !self.included.contains(&participant) {
|
||||
None?
|
||||
}
|
||||
Some(self.interpolation.interpolation_factor(participant, &self.included))
|
||||
}
|
||||
|
||||
/// Return the interpolated, offset secret share.
|
||||
pub fn secret_share(&self) -> &Zeroizing<C::F> {
|
||||
&self.secret_share
|
||||
|
||||
@@ -7,8 +7,6 @@ use std_shims::collections::HashMap;
|
||||
#[cfg(feature = "std")]
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use ciphersuite::group::ff::Field;
|
||||
use ciphersuite::{
|
||||
group::{Group, GroupEncoding},
|
||||
Ciphersuite,
|
||||
@@ -16,7 +14,7 @@ use ciphersuite::{
|
||||
|
||||
use crate::DkgError;
|
||||
#[cfg(feature = "std")]
|
||||
use crate::{Participant, ThresholdParams, ThresholdCore, lagrange};
|
||||
use crate::{Participant, ThresholdParams, Interpolation, ThresholdCore};
|
||||
|
||||
fn check_keys<C: Ciphersuite>(keys: &[C::G]) -> Result<u16, DkgError<()>> {
|
||||
if keys.is_empty() {
|
||||
@@ -110,32 +108,20 @@ pub fn musig<C: Ciphersuite>(
|
||||
|
||||
// Calculate verification shares
|
||||
let mut verification_shares = HashMap::new();
|
||||
// When this library offers a ThresholdView for a specific signing set, it applies the lagrange
|
||||
// factor
|
||||
// Since this is a n-of-n scheme, there's only one possible signing set, and one possible
|
||||
// lagrange factor
|
||||
// In the name of simplicity, we define the group key as the sum of all bound keys
|
||||
// Accordingly, the secret share must be multiplied by the inverse of the lagrange factor, along
|
||||
// with all verification shares
|
||||
// This is less performant than simply defining the group key as the sum of all post-lagrange
|
||||
// bound keys, yet the simplicity is preferred
|
||||
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 bound = keys[l] * binding[l];
|
||||
for (l, (key, binding)) in keys.iter().zip(binding).enumerate() {
|
||||
let bound = *key * binding;
|
||||
group_key += bound;
|
||||
|
||||
let lagrange_inv = lagrange::<C::F>(*p, &included).invert().unwrap();
|
||||
if params.i() == *p {
|
||||
*secret_share *= lagrange_inv;
|
||||
}
|
||||
verification_shares.insert(*p, bound * lagrange_inv);
|
||||
// These errors also shouldn't be possible, for the same reasons as documented above
|
||||
verification_shares.insert(
|
||||
Participant::new(1 + u16::try_from(l).map_err(|_| DkgError::InvalidSigningSet)?)
|
||||
.ok_or(DkgError::InvalidSigningSet)?,
|
||||
bound,
|
||||
);
|
||||
}
|
||||
debug_assert_eq!(C::generator() * secret_share.deref(), verification_shares[¶ms.i()]);
|
||||
debug_assert_eq!(musig_key::<C>(context, keys).unwrap(), group_key);
|
||||
|
||||
Ok(ThresholdCore { params, secret_share, group_key, verification_shares })
|
||||
Ok(ThresholdCore::new(params, Interpolation::None, secret_share, verification_shares))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use multiexp::{multiexp_vartime, BatchVerifier};
|
||||
use schnorr::SchnorrSignature;
|
||||
|
||||
use crate::{
|
||||
Participant, DkgError, ThresholdParams, ThresholdCore, validate_map,
|
||||
Participant, DkgError, ThresholdParams, Interpolation, ThresholdCore, validate_map,
|
||||
encryption::{
|
||||
ReadWrite, EncryptionKeyMessage, EncryptedMessage, Encryption, Decryption, EncryptionKeyProof,
|
||||
DecryptionError,
|
||||
@@ -477,6 +477,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
||||
encryption: encryption.into_decryption(),
|
||||
result: Some(ThresholdCore {
|
||||
params,
|
||||
interpolation: Interpolation::Lagrange,
|
||||
secret_share: secret,
|
||||
group_key: stripes[0],
|
||||
verification_shares,
|
||||
|
||||
@@ -113,6 +113,7 @@ impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<
|
||||
Ok(ThresholdKeys {
|
||||
core: Arc::new(ThresholdCore::new(
|
||||
params,
|
||||
self.base.core.interpolation,
|
||||
self.base.secret_share().clone(),
|
||||
verification_shares,
|
||||
)),
|
||||
|
||||
@@ -6,7 +6,7 @@ use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
use crate::{Participant, ThresholdCore, ThresholdKeys, lagrange, musig::musig as musig_fn};
|
||||
use crate::{Participant, ThresholdCore, ThresholdKeys, musig::musig as musig_fn};
|
||||
|
||||
mod musig;
|
||||
pub use musig::test_musig;
|
||||
@@ -46,7 +46,9 @@ pub fn recover_key<C: Ciphersuite>(keys: &HashMap<Participant, ThresholdKeys<C>>
|
||||
let included = keys.keys().copied().collect::<Vec<_>>();
|
||||
|
||||
let group_private = keys.iter().fold(C::F::ZERO, |accum, (i, keys)| {
|
||||
accum + (lagrange::<C::F>(*i, &included) * keys.secret_share().deref())
|
||||
accum +
|
||||
(first.core.interpolation.interpolation_factor::<C::F>(*i, &included) *
|
||||
keys.secret_share().deref())
|
||||
});
|
||||
assert_eq!(C::generator() * group_private, first.group_key(), "failed to recover keys");
|
||||
group_private
|
||||
|
||||
@@ -20,7 +20,6 @@ use group::{
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
use dalek_ff_group as dfg;
|
||||
use frost::{
|
||||
dkg::lagrange,
|
||||
curve::Ed25519,
|
||||
Participant, FrostError, ThresholdKeys, ThresholdView,
|
||||
algorithm::{WriteAddendum, Algorithm},
|
||||
@@ -233,8 +232,10 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
||||
.append_message(b"key_image_share", addendum.key_image_share.compress().to_bytes());
|
||||
|
||||
// Accumulate the interpolated share
|
||||
let interpolated_key_image_share =
|
||||
addendum.key_image_share * lagrange::<dfg::Scalar>(l, view.included());
|
||||
let interpolated_key_image_share = addendum.key_image_share *
|
||||
view
|
||||
.interpolation_factor(l)
|
||||
.ok_or(FrostError::InternalError("processing addendum of non-participant"))?;
|
||||
*self.image.as_mut().unwrap() += interpolated_key_image_share;
|
||||
|
||||
self
|
||||
|
||||
@@ -14,7 +14,6 @@ use transcript::{Transcript, RecommendedTranscript};
|
||||
use frost::{
|
||||
curve::Ed25519,
|
||||
Participant, FrostError, ThresholdKeys,
|
||||
dkg::lagrange,
|
||||
sign::{
|
||||
Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine,
|
||||
AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
|
||||
@@ -34,7 +33,7 @@ use crate::send::{SendError, SignableTransaction, key_image_sort};
|
||||
pub struct TransactionMachine {
|
||||
signable: SignableTransaction,
|
||||
|
||||
i: Participant,
|
||||
keys: ThresholdKeys<Ed25519>,
|
||||
|
||||
// The key image generator, and the scalar offset from the spend key
|
||||
key_image_generators_and_offsets: Vec<(EdwardsPoint, Scalar)>,
|
||||
@@ -45,7 +44,7 @@ pub struct TransactionMachine {
|
||||
pub struct TransactionSignMachine {
|
||||
signable: SignableTransaction,
|
||||
|
||||
i: Participant,
|
||||
keys: ThresholdKeys<Ed25519>,
|
||||
|
||||
key_image_generators_and_offsets: Vec<(EdwardsPoint, Scalar)>,
|
||||
clsags: Vec<(ClsagMultisigMaskSender, AlgorithmSignMachine<Ed25519, ClsagMultisig>)>,
|
||||
@@ -61,7 +60,7 @@ pub struct TransactionSignatureMachine {
|
||||
|
||||
impl SignableTransaction {
|
||||
/// Create a FROST signing machine out of this signable transaction.
|
||||
pub fn multisig(self, keys: &ThresholdKeys<Ed25519>) -> Result<TransactionMachine, SendError> {
|
||||
pub fn multisig(self, keys: ThresholdKeys<Ed25519>) -> Result<TransactionMachine, SendError> {
|
||||
let mut clsags = vec![];
|
||||
|
||||
let mut key_image_generators_and_offsets = vec![];
|
||||
@@ -85,12 +84,7 @@ impl SignableTransaction {
|
||||
clsags.push((clsag_mask_send, AlgorithmMachine::new(clsag, offset)));
|
||||
}
|
||||
|
||||
Ok(TransactionMachine {
|
||||
signable: self,
|
||||
i: keys.params().i(),
|
||||
key_image_generators_and_offsets,
|
||||
clsags,
|
||||
})
|
||||
Ok(TransactionMachine { signable: self, keys, key_image_generators_and_offsets, clsags })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +114,7 @@ impl PreprocessMachine for TransactionMachine {
|
||||
TransactionSignMachine {
|
||||
signable: self.signable,
|
||||
|
||||
i: self.i,
|
||||
keys: self.keys,
|
||||
|
||||
key_image_generators_and_offsets: self.key_image_generators_and_offsets,
|
||||
clsags,
|
||||
@@ -173,12 +167,12 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
// We do not need to be included here, yet this set of signers has yet to be validated
|
||||
// We explicitly remove ourselves to ensure we aren't included twice, if we were redundantly
|
||||
// included
|
||||
commitments.remove(&self.i);
|
||||
commitments.remove(&self.keys.params().i());
|
||||
|
||||
// Find out who's included
|
||||
let mut included = commitments.keys().copied().collect::<Vec<_>>();
|
||||
// This push won't duplicate due to the above removal
|
||||
included.push(self.i);
|
||||
included.push(self.keys.params().i());
|
||||
// unstable sort may reorder elements of equal order
|
||||
// Given our lack of duplicates, we should have no elements of equal order
|
||||
included.sort_unstable();
|
||||
@@ -192,12 +186,15 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
}
|
||||
|
||||
// Convert the serialized nonces commitments to a parallelized Vec
|
||||
let view = self.keys.view(included.clone()).map_err(|_| {
|
||||
FrostError::InvalidSigningSet("couldn't form an interpolated view of the key")
|
||||
})?;
|
||||
let mut commitments = (0 .. self.clsags.len())
|
||||
.map(|c| {
|
||||
included
|
||||
.iter()
|
||||
.map(|l| {
|
||||
let preprocess = if *l == self.i {
|
||||
let preprocess = if *l == self.keys.params().i() {
|
||||
self.our_preprocess[c].clone()
|
||||
} else {
|
||||
commitments.get_mut(l).ok_or(FrostError::MissingParticipant(*l))?[c].clone()
|
||||
@@ -206,7 +203,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
// While here, calculate the key image as needed to call sign
|
||||
// The CLSAG algorithm will independently calculate the key image/verify these shares
|
||||
key_images[c] +=
|
||||
preprocess.addendum.key_image_share().0 * lagrange::<dfg::Scalar>(*l, &included).0;
|
||||
preprocess.addendum.key_image_share().0 * view.interpolation_factor(*l).unwrap().0;
|
||||
|
||||
Ok((*l, preprocess))
|
||||
})
|
||||
@@ -217,7 +214,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
// The above inserted our own preprocess into these maps (which is unnecessary)
|
||||
// Remove it now
|
||||
for map in &mut commitments {
|
||||
map.remove(&self.i);
|
||||
map.remove(&self.keys.params().i());
|
||||
}
|
||||
|
||||
// The actual TX will have sorted its inputs by key image
|
||||
|
||||
@@ -281,7 +281,7 @@ macro_rules! test {
|
||||
{
|
||||
let mut machines = HashMap::new();
|
||||
for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) {
|
||||
machines.insert(i, tx.clone().multisig(&keys[&i]).unwrap());
|
||||
machines.insert(i, tx.clone().multisig(keys[&i].clone()).unwrap());
|
||||
}
|
||||
|
||||
frost::tests::sign_without_caching(&mut OsRng, machines, &[])
|
||||
|
||||
@@ -657,7 +657,7 @@ impl Network for Monero {
|
||||
keys: ThresholdKeys<Self::Curve>,
|
||||
transaction: SignableTransaction,
|
||||
) -> Result<Self::TransactionMachine, NetworkError> {
|
||||
match transaction.0.clone().multisig(&keys) {
|
||||
match transaction.0.clone().multisig(keys) {
|
||||
Ok(machine) => Ok(machine),
|
||||
Err(e) => panic!("failed to create a multisig machine for TX: {e}"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user