Support taking arbitrary linear combinations of signing keys, not just additive offsets

This commit is contained in:
Luke Parker
2025-08-15 21:15:59 -04:00
parent f2563d39cb
commit 38dd8cb191
3 changed files with 72 additions and 29 deletions

View File

@@ -412,14 +412,17 @@ mod lib {
#[zeroize(skip)] #[zeroize(skip)]
pub(crate) core: Arc<ThresholdCore<C>>, pub(crate) core: Arc<ThresholdCore<C>>,
// Scalar applied to these keys.
pub(crate) scalar: C::F,
// Offset applied to these keys. // Offset applied to these keys.
pub(crate) offset: Option<C::F>, pub(crate) offset: C::F,
} }
/// View of keys, interpolated and offset for usage. /// View of keys, interpolated and with the expected linear combination taken for usage.
#[derive(Clone)] #[derive(Clone)]
pub struct ThresholdView<C: Ciphersuite> { pub struct ThresholdView<C: Ciphersuite> {
interpolation: Interpolation<C::F>, interpolation: Interpolation<C::F>,
scalar: C::F,
offset: C::F, offset: C::F,
group_key: C::G, group_key: C::G,
included: Vec<Participant>, included: Vec<Participant>,
@@ -433,6 +436,7 @@ mod lib {
fmt fmt
.debug_struct("ThresholdView") .debug_struct("ThresholdView")
.field("interpolation", &self.interpolation) .field("interpolation", &self.interpolation)
.field("scalar", &self.scalar)
.field("offset", &self.offset) .field("offset", &self.offset)
.field("group_key", &self.group_key) .field("group_key", &self.group_key)
.field("included", &self.included) .field("included", &self.included)
@@ -444,6 +448,7 @@ mod lib {
impl<C: Ciphersuite> Zeroize for ThresholdView<C> { impl<C: Ciphersuite> Zeroize for ThresholdView<C> {
fn zeroize(&mut self) { fn zeroize(&mut self) {
self.scalar.zeroize();
self.offset.zeroize(); self.offset.zeroize();
self.group_key.zeroize(); self.group_key.zeroize();
self.included.zeroize(); self.included.zeroize();
@@ -460,25 +465,42 @@ mod lib {
impl<C: Ciphersuite> ThresholdKeys<C> { impl<C: Ciphersuite> ThresholdKeys<C> {
/// Create a new set of ThresholdKeys from a ThresholdCore. /// Create a new set of ThresholdKeys from a ThresholdCore.
pub fn new(core: ThresholdCore<C>) -> ThresholdKeys<C> { pub fn new(core: ThresholdCore<C>) -> ThresholdKeys<C> {
ThresholdKeys { core: Arc::new(core), offset: None } ThresholdKeys { core: Arc::new(core), scalar: C::F::ONE, offset: C::F::ZERO }
}
/// Scale the keys by a given scalar to allow for various account and privacy schemes.
///
/// This scalar is ephemeral and will not be included when these keys are serialized. The
/// scalar is applied on top of any already-existing scalar/offset.
///
/// Returns `None` if the scalar is equal to `0`.
#[must_use]
pub fn scale(mut self, scalar: C::F) -> Option<ThresholdKeys<C>> {
if bool::from(scalar.is_zero()) {
None?;
}
self.scalar *= scalar;
self.offset *= scalar;
Some(self)
} }
/// Offset the keys by a given scalar to allow for various account and privacy schemes. /// Offset the keys by a given scalar to allow for various account and privacy schemes.
/// ///
/// This offset is ephemeral and will not be included when these keys are serialized. It also /// This offset is ephemeral and will not be included when these keys are serialized. The
/// accumulates, so calling offset multiple times will produce a offset of the offsets' sum. /// offset is applied on top of any already-existing scalar/offset.
#[must_use] #[must_use]
pub fn offset(&self, offset: C::F) -> ThresholdKeys<C> { pub fn offset(mut self, offset: C::F) -> ThresholdKeys<C> {
let mut res = self.clone(); self.offset += offset;
// Carry any existing offset self
// Enables schemes like Monero's subaddresses which have a per-subaddress offset and then a }
// one-time-key offset
res.offset = Some(offset + res.offset.unwrap_or(C::F::ZERO)); /// Return the current scalar in-use for these keys.
res pub fn current_scalar(&self) -> C::F {
self.scalar
} }
/// Return the current offset in-use for these keys. /// Return the current offset in-use for these keys.
pub fn current_offset(&self) -> Option<C::F> { pub fn current_offset(&self) -> C::F {
self.offset self.offset
} }
@@ -492,9 +514,9 @@ mod lib {
&self.core.secret_share &self.core.secret_share
} }
/// Return the group key, with any offset applied. /// Return the group key, with the expected linear combination taken.
pub fn group_key(&self) -> C::G { pub fn group_key(&self) -> C::G {
self.core.group_key + (C::generator() * self.offset.unwrap_or(C::F::ZERO)) (self.core.group_key * self.scalar) + (C::generator() * self.offset)
} }
/// Return all participants' verification shares without any offsetting. /// Return all participants' verification shares without any offsetting.
@@ -507,8 +529,8 @@ mod lib {
self.core.serialize() self.core.serialize()
} }
/// Obtain a view of these keys, with any offset applied, interpolated for the specified signing /// Obtain a view of these keys, interpolated for the specified signing set, with the specified
/// set. /// linear combination taken.
pub fn view(&self, mut included: Vec<Participant>) -> Result<ThresholdView<C>, DkgError<()>> { pub fn view(&self, mut included: Vec<Participant>) -> Result<ThresholdView<C>, DkgError<()>> {
if (included.len() < self.params().t.into()) || if (included.len() < self.params().t.into()) ||
(usize::from(self.params().n()) < included.len()) (usize::from(self.params().n()) < included.len())
@@ -517,26 +539,36 @@ mod lib {
} }
included.sort(); included.sort();
// The interpolation occurs multiplicatively, letting us scale by the scalar now
let secret_share_scaled = Zeroizing::new(self.scalar * self.secret_share().deref());
let mut secret_share = Zeroizing::new( let mut secret_share = Zeroizing::new(
self.core.interpolation.interpolation_factor(self.params().i(), &included) * self.core.interpolation.interpolation_factor(self.params().i(), &included) *
self.secret_share().deref(), secret_share_scaled.deref(),
); );
let mut verification_shares = self.verification_shares(); let mut verification_shares = self.verification_shares();
for (i, share) in &mut verification_shares { for (i, share) in &mut verification_shares {
*share *= self.core.interpolation.interpolation_factor(*i, &included); *share *= self.scalar * self.core.interpolation.interpolation_factor(*i, &included);
} }
// The offset is included by adding it to the participant with the lowest ID /*
let offset = self.offset.unwrap_or(C::F::ZERO); The offset is included by adding it to the participant with the lowest ID.
This is done after interpolating to ensure, regardless of the method of interpolation, that
the method of interpolation does not scale the offset. For Lagrange interpolation, we could
add the offset to every key share before interpolating, yet for Constant interpolation, we
_have_ to add it as we do here (which also works even when we intend to perform Lagrange
interpolation).
*/
if included[0] == self.params().i() { if included[0] == self.params().i() {
*secret_share += offset; *secret_share += self.offset;
} }
*verification_shares.get_mut(&included[0]).unwrap() += C::generator() * offset; *verification_shares.get_mut(&included[0]).unwrap() += C::generator() * self.offset;
Ok(ThresholdView { Ok(ThresholdView {
interpolation: self.core.interpolation.clone(), interpolation: self.core.interpolation.clone(),
offset, scalar: self.scalar,
offset: self.offset,
group_key: self.group_key(), group_key: self.group_key(),
secret_share, secret_share,
original_verification_shares: self.verification_shares(), original_verification_shares: self.verification_shares(),
@@ -553,7 +585,12 @@ mod lib {
} }
impl<C: Ciphersuite> ThresholdView<C> { impl<C: Ciphersuite> ThresholdView<C> {
/// Return the offset for this view. /// Return the scalar applied to this view.
pub fn scalar(&self) -> C::F {
self.scalar
}
/// Return the offset applied to this view.
pub fn offset(&self) -> C::F { pub fn offset(&self) -> C::F {
self.offset self.offset
} }
@@ -576,7 +613,7 @@ mod lib {
Some(self.interpolation.interpolation_factor(participant, &self.included)) Some(self.interpolation.interpolation_factor(participant, &self.included))
} }
/// Return the interpolated, offset secret share. /// Return the interpolated secret share, with the expected linear combination taken.
pub fn secret_share(&self) -> &Zeroizing<C::F> { pub fn secret_share(&self) -> &Zeroizing<C::F> {
&self.secret_share &self.secret_share
} }
@@ -586,7 +623,8 @@ mod lib {
self.original_verification_shares[&l] self.original_verification_shares[&l]
} }
/// Return the interpolated, offset verification share for the specified participant. /// Return the interpolated verification share, with the expected linear combination taken,
/// for the specified participant.
pub fn verification_share(&self, l: Participant) -> C::G { pub fn verification_share(&self, l: Participant) -> C::G {
self.verification_shares[&l] self.verification_shares[&l]
} }

View File

@@ -111,6 +111,7 @@ pub fn musig<C: Ciphersuite>(
let mut group_key = C::G::identity(); let mut group_key = C::G::identity();
for l in 1 ..= keys_len { for l in 1 ..= keys_len {
let key = keys[usize::from(l) - 1]; let key = keys[usize::from(l) - 1];
// TODO: Use a multiexp for this
group_key += key * binding[usize::from(l - 1)]; group_key += key * binding[usize::from(l - 1)];
// These errors also shouldn't be possible, for the same reasons as documented above // These errors also shouldn't be possible, for the same reasons as documented above

View File

@@ -7,7 +7,10 @@ use std::{
use rand_core::{RngCore, CryptoRng}; use rand_core::{RngCore, CryptoRng};
use ciphersuite::{group::GroupEncoding, Ciphersuite}; use ciphersuite::{
group::{ff::Field, GroupEncoding},
Ciphersuite,
};
use transcript::{Transcript, RecommendedTranscript}; use transcript::{Transcript, RecommendedTranscript};
use dleq::DLEqProof; use dleq::DLEqProof;
@@ -117,7 +120,8 @@ impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<
self.base.secret_share().clone(), self.base.secret_share().clone(),
verification_shares, verification_shares,
)), )),
offset: None, scalar: C2::F::ONE,
offset: C2::F::ZERO,
}) })
} }
} }