mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Support caching preprocesses in FROST (#190)
* Remove the explicit included participants from FROST Now, whoever submits preprocesses becomes the signing set. Better separates preprocess from sign, at the cost of slightly more annoying integrations (Monero needs to now independently lagrange/offset its key images). * Support caching preprocesses Closes https://github.com/serai-dex/serai/issues/40. I *could* have added a serialization trait to Algorithm and written a ton of data to disk, while requiring Algorithm implementors also accept such work. Instead, I moved preprocess to a seeded RNG (Chacha20) which should be as secure as the regular RNG. Rebuilding from cache simply loads the previously used Chacha seed, making the Algorithm oblivious to the fact it's being rebuilt from a cache. This removes any requirements for it to be modified while guaranteeing equivalency. This builds on the last commit which delayed determining the signing set till post-preprocess acquisition. Unfortunately, that commit did force preprocess from ThresholdView to ThresholdKeys which had visible effects on Monero. Serai will actually need delayed set determination for #163, and overall, it remains better, hence it's inclusion. * Document FROST preprocess caching * Update ethereum to new FROST * Fix bug in Monero offset calculation and update processor
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -4631,6 +4631,7 @@ dependencies = [
|
|||||||
"hkdf",
|
"hkdf",
|
||||||
"minimal-ed448",
|
"minimal-ed448",
|
||||||
"multiexp",
|
"multiexp",
|
||||||
|
"rand_chacha 0.3.1",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"schnorr-signatures",
|
"schnorr-signatures",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -51,9 +51,12 @@ async fn test_ecrecover_hack() {
|
|||||||
|
|
||||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
||||||
|
|
||||||
|
let algo = Algo::<Secp256k1, crypto::EthereumHram>::new();
|
||||||
let sig = sign(
|
let sig = sign(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
algorithm_machines(&mut OsRng, Algo::<Secp256k1, crypto::EthereumHram>::new(), &keys),
|
algo.clone(),
|
||||||
|
keys.clone(),
|
||||||
|
algorithm_machines(&mut OsRng, algo, &keys),
|
||||||
full_message,
|
full_message,
|
||||||
);
|
);
|
||||||
let mut processed_sig =
|
let mut processed_sig =
|
||||||
|
|||||||
@@ -40,8 +40,11 @@ fn test_signing() {
|
|||||||
|
|
||||||
const MESSAGE: &'static [u8] = b"Hello, World!";
|
const MESSAGE: &'static [u8] = b"Hello, World!";
|
||||||
|
|
||||||
|
let algo = Schnorr::<Secp256k1, EthereumHram>::new();
|
||||||
let _sig = sign(
|
let _sig = sign(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
|
algo.clone(),
|
||||||
|
keys.clone(),
|
||||||
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, EthereumHram>::new(), &keys),
|
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, EthereumHram>::new(), &keys),
|
||||||
MESSAGE,
|
MESSAGE,
|
||||||
);
|
);
|
||||||
@@ -67,9 +70,12 @@ fn test_ecrecover_hack() {
|
|||||||
|
|
||||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
||||||
|
|
||||||
|
let algo = Schnorr::<Secp256k1, EthereumHram>::new();
|
||||||
let sig = sign(
|
let sig = sign(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, EthereumHram>::new(), &keys),
|
algo.clone(),
|
||||||
|
keys.clone(),
|
||||||
|
algorithm_machines(&mut OsRng, algo, &keys),
|
||||||
full_message,
|
full_message,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ use crate::{
|
|||||||
mod multisig;
|
mod multisig;
|
||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig};
|
pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig};
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
pub(crate) use multisig::add_key_image_share;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref INV_EIGHT: Scalar = Scalar::from(8u8).invert();
|
static ref INV_EIGHT: Scalar = Scalar::from(8u8).invert();
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ use transcript::{Transcript, RecommendedTranscript};
|
|||||||
use dalek_ff_group as dfg;
|
use dalek_ff_group as dfg;
|
||||||
use dleq::DLEqProof;
|
use dleq::DLEqProof;
|
||||||
use frost::{
|
use frost::{
|
||||||
|
dkg::lagrange,
|
||||||
curve::Ed25519,
|
curve::Ed25519,
|
||||||
FrostError, ThresholdView,
|
FrostError, ThresholdKeys, ThresholdView,
|
||||||
algorithm::{WriteAddendum, Algorithm},
|
algorithm::{WriteAddendum, Algorithm},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ struct Interim {
|
|||||||
pub struct ClsagMultisig {
|
pub struct ClsagMultisig {
|
||||||
transcript: RecommendedTranscript,
|
transcript: RecommendedTranscript,
|
||||||
|
|
||||||
H: EdwardsPoint,
|
pub(crate) H: EdwardsPoint,
|
||||||
// Merged here as CLSAG needs it, passing it would be a mess, yet having it beforehand requires
|
// Merged here as CLSAG needs it, passing it would be a mess, yet having it beforehand requires
|
||||||
// an extra round
|
// an extra round
|
||||||
image: EdwardsPoint,
|
image: EdwardsPoint,
|
||||||
@@ -142,6 +143,20 @@ impl ClsagMultisig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_key_image_share(
|
||||||
|
image: &mut EdwardsPoint,
|
||||||
|
generator: EdwardsPoint,
|
||||||
|
offset: Scalar,
|
||||||
|
included: &[u16],
|
||||||
|
participant: u16,
|
||||||
|
share: EdwardsPoint,
|
||||||
|
) {
|
||||||
|
if image.is_identity() {
|
||||||
|
*image = generator * offset;
|
||||||
|
}
|
||||||
|
*image += share * lagrange::<dfg::Scalar>(participant, included).0;
|
||||||
|
}
|
||||||
|
|
||||||
impl Algorithm<Ed25519> for ClsagMultisig {
|
impl Algorithm<Ed25519> for ClsagMultisig {
|
||||||
type Transcript = RecommendedTranscript;
|
type Transcript = RecommendedTranscript;
|
||||||
type Addendum = ClsagAddendum;
|
type Addendum = ClsagAddendum;
|
||||||
@@ -154,10 +169,10 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
|||||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
view: &ThresholdView<Ed25519>,
|
keys: &ThresholdKeys<Ed25519>,
|
||||||
) -> ClsagAddendum {
|
) -> ClsagAddendum {
|
||||||
ClsagAddendum {
|
ClsagAddendum {
|
||||||
key_image: dfg::EdwardsPoint(self.H) * view.secret_share().deref(),
|
key_image: dfg::EdwardsPoint(self.H) * keys.secret_share().deref(),
|
||||||
dleq: DLEqProof::prove(
|
dleq: DLEqProof::prove(
|
||||||
rng,
|
rng,
|
||||||
// Doesn't take in a larger transcript object due to the usage of this
|
// Doesn't take in a larger transcript object due to the usage of this
|
||||||
@@ -167,7 +182,7 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
|||||||
// try to merge later in some form, when it should instead just merge xH (as it does)
|
// try to merge later in some form, when it should instead just merge xH (as it does)
|
||||||
&mut dleq_transcript(),
|
&mut dleq_transcript(),
|
||||||
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
|
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
|
||||||
view.secret_share(),
|
keys.secret_share(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,12 +220,19 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
|||||||
.verify(
|
.verify(
|
||||||
&mut dleq_transcript(),
|
&mut dleq_transcript(),
|
||||||
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
|
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
|
||||||
&[view.verification_share(l), addendum.key_image],
|
&[view.original_verification_share(l), addendum.key_image],
|
||||||
)
|
)
|
||||||
.map_err(|_| FrostError::InvalidPreprocess(l))?;
|
.map_err(|_| FrostError::InvalidPreprocess(l))?;
|
||||||
|
|
||||||
self.transcript.append_message(b"key_image_share", addendum.key_image.compress().to_bytes());
|
self.transcript.append_message(b"key_image_share", addendum.key_image.compress().to_bytes());
|
||||||
self.image += addendum.key_image.0;
|
add_key_image_share(
|
||||||
|
&mut self.image,
|
||||||
|
self.H,
|
||||||
|
view.offset().0,
|
||||||
|
&view.included(),
|
||||||
|
l,
|
||||||
|
addendum.key_image.0,
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,11 +101,7 @@ fn clsag_multisig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mask_sum = random_scalar(&mut OsRng);
|
let mask_sum = random_scalar(&mut OsRng);
|
||||||
sign(
|
let algorithm = ClsagMultisig::new(
|
||||||
&mut OsRng,
|
|
||||||
algorithm_machines(
|
|
||||||
&mut OsRng,
|
|
||||||
ClsagMultisig::new(
|
|
||||||
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
|
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
|
||||||
keys[&1].group_key().0,
|
keys[&1].group_key().0,
|
||||||
Arc::new(RwLock::new(Some(ClsagDetails::new(
|
Arc::new(RwLock::new(Some(ClsagDetails::new(
|
||||||
@@ -120,9 +116,13 @@ fn clsag_multisig() {
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
mask_sum,
|
mask_sum,
|
||||||
)))),
|
)))),
|
||||||
),
|
);
|
||||||
&keys,
|
|
||||||
),
|
sign(
|
||||||
|
&mut OsRng,
|
||||||
|
algorithm.clone(),
|
||||||
|
keys.clone(),
|
||||||
|
algorithm_machines(&mut OsRng, algorithm, &keys),
|
||||||
&[1; 32],
|
&[1; 32],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,28 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||||
use rand_chacha::ChaCha20Rng;
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
|
use group::ff::Field;
|
||||||
use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint};
|
use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint};
|
||||||
|
use dalek_ff_group as dfg;
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
use frost::{
|
use frost::{
|
||||||
curve::Ed25519,
|
curve::Ed25519,
|
||||||
FrostError, ThresholdKeys,
|
FrostError, ThresholdKeys,
|
||||||
sign::{
|
sign::{
|
||||||
Writable, Preprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine,
|
Writable, Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine,
|
||||||
AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
|
SignatureMachine, AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
random_scalar,
|
random_scalar,
|
||||||
ringct::{
|
ringct::{
|
||||||
clsag::{ClsagInput, ClsagDetails, ClsagAddendum, ClsagMultisig},
|
clsag::{ClsagInput, ClsagDetails, ClsagAddendum, ClsagMultisig, add_key_image_share},
|
||||||
RctPrunable,
|
RctPrunable,
|
||||||
},
|
},
|
||||||
transaction::{Input, Transaction},
|
transaction::{Input, Transaction},
|
||||||
@@ -34,11 +37,12 @@ use crate::{
|
|||||||
pub struct TransactionMachine {
|
pub struct TransactionMachine {
|
||||||
signable: SignableTransaction,
|
signable: SignableTransaction,
|
||||||
i: u16,
|
i: u16,
|
||||||
included: Vec<u16>,
|
|
||||||
transcript: RecommendedTranscript,
|
transcript: RecommendedTranscript,
|
||||||
|
|
||||||
decoys: Vec<Decoys>,
|
decoys: Vec<Decoys>,
|
||||||
|
|
||||||
|
// Hashed key and scalar offset
|
||||||
|
key_images: Vec<(EdwardsPoint, Scalar)>,
|
||||||
inputs: Vec<Arc<RwLock<Option<ClsagDetails>>>>,
|
inputs: Vec<Arc<RwLock<Option<ClsagDetails>>>>,
|
||||||
clsags: Vec<AlgorithmMachine<Ed25519, ClsagMultisig>>,
|
clsags: Vec<AlgorithmMachine<Ed25519, ClsagMultisig>>,
|
||||||
}
|
}
|
||||||
@@ -46,11 +50,11 @@ pub struct TransactionMachine {
|
|||||||
pub struct TransactionSignMachine {
|
pub struct TransactionSignMachine {
|
||||||
signable: SignableTransaction,
|
signable: SignableTransaction,
|
||||||
i: u16,
|
i: u16,
|
||||||
included: Vec<u16>,
|
|
||||||
transcript: RecommendedTranscript,
|
transcript: RecommendedTranscript,
|
||||||
|
|
||||||
decoys: Vec<Decoys>,
|
decoys: Vec<Decoys>,
|
||||||
|
|
||||||
|
key_images: Vec<(EdwardsPoint, Scalar)>,
|
||||||
inputs: Vec<Arc<RwLock<Option<ClsagDetails>>>>,
|
inputs: Vec<Arc<RwLock<Option<ClsagDetails>>>>,
|
||||||
clsags: Vec<AlgorithmSignMachine<Ed25519, ClsagMultisig>>,
|
clsags: Vec<AlgorithmSignMachine<Ed25519, ClsagMultisig>>,
|
||||||
|
|
||||||
@@ -71,7 +75,6 @@ impl SignableTransaction {
|
|||||||
keys: ThresholdKeys<Ed25519>,
|
keys: ThresholdKeys<Ed25519>,
|
||||||
mut transcript: RecommendedTranscript,
|
mut transcript: RecommendedTranscript,
|
||||||
height: usize,
|
height: usize,
|
||||||
mut included: Vec<u16>,
|
|
||||||
) -> Result<TransactionMachine, TransactionError> {
|
) -> Result<TransactionMachine, TransactionError> {
|
||||||
let mut inputs = vec![];
|
let mut inputs = vec![];
|
||||||
for _ in 0 .. self.inputs.len() {
|
for _ in 0 .. self.inputs.len() {
|
||||||
@@ -110,24 +113,20 @@ impl SignableTransaction {
|
|||||||
transcript.append_message(b"payment_amount", payment.1.to_le_bytes());
|
transcript.append_message(b"payment_amount", payment.1.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort included before cloning it around
|
let mut key_images = vec![];
|
||||||
included.sort_unstable();
|
|
||||||
|
|
||||||
for (i, input) in self.inputs.iter().enumerate() {
|
for (i, input) in self.inputs.iter().enumerate() {
|
||||||
// Check this the right set of keys
|
// Check this the right set of keys
|
||||||
let offset = keys.offset(dalek_ff_group::Scalar(input.key_offset()));
|
let offset = keys.offset(dfg::Scalar(input.key_offset()));
|
||||||
if offset.group_key().0 != input.key() {
|
if offset.group_key().0 != input.key() {
|
||||||
Err(TransactionError::WrongPrivateKey)?;
|
Err(TransactionError::WrongPrivateKey)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
clsags.push(
|
let clsag = ClsagMultisig::new(transcript.clone(), input.key(), inputs[i].clone());
|
||||||
AlgorithmMachine::new(
|
key_images.push((
|
||||||
ClsagMultisig::new(transcript.clone(), input.key(), inputs[i].clone()),
|
clsag.H,
|
||||||
offset,
|
keys.current_offset().unwrap_or(dfg::Scalar::zero()).0 + self.inputs[i].key_offset(),
|
||||||
&included,
|
));
|
||||||
)
|
clsags.push(AlgorithmMachine::new(clsag, offset).map_err(TransactionError::FrostError)?);
|
||||||
.map_err(TransactionError::FrostError)?,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select decoys
|
// Select decoys
|
||||||
@@ -150,11 +149,11 @@ impl SignableTransaction {
|
|||||||
Ok(TransactionMachine {
|
Ok(TransactionMachine {
|
||||||
signable: self,
|
signable: self,
|
||||||
i: keys.params().i(),
|
i: keys.params().i(),
|
||||||
included,
|
|
||||||
transcript,
|
transcript,
|
||||||
|
|
||||||
decoys,
|
decoys,
|
||||||
|
|
||||||
|
key_images,
|
||||||
inputs,
|
inputs,
|
||||||
clsags,
|
clsags,
|
||||||
})
|
})
|
||||||
@@ -196,11 +195,11 @@ impl PreprocessMachine for TransactionMachine {
|
|||||||
TransactionSignMachine {
|
TransactionSignMachine {
|
||||||
signable: self.signable,
|
signable: self.signable,
|
||||||
i: self.i,
|
i: self.i,
|
||||||
included: self.included,
|
|
||||||
transcript: self.transcript,
|
transcript: self.transcript,
|
||||||
|
|
||||||
decoys: self.decoys,
|
decoys: self.decoys,
|
||||||
|
|
||||||
|
key_images: self.key_images,
|
||||||
inputs: self.inputs,
|
inputs: self.inputs,
|
||||||
clsags,
|
clsags,
|
||||||
|
|
||||||
@@ -212,10 +211,30 @@ impl PreprocessMachine for TransactionMachine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SignMachine<Transaction> for TransactionSignMachine {
|
impl SignMachine<Transaction> for TransactionSignMachine {
|
||||||
|
type Params = ();
|
||||||
|
type Keys = ThresholdKeys<Ed25519>;
|
||||||
type Preprocess = Vec<Preprocess<Ed25519, ClsagAddendum>>;
|
type Preprocess = Vec<Preprocess<Ed25519, ClsagAddendum>>;
|
||||||
type SignatureShare = Vec<SignatureShare<Ed25519>>;
|
type SignatureShare = Vec<SignatureShare<Ed25519>>;
|
||||||
type SignatureMachine = TransactionSignatureMachine;
|
type SignatureMachine = TransactionSignatureMachine;
|
||||||
|
|
||||||
|
fn cache(self) -> Zeroizing<CachedPreprocess> {
|
||||||
|
unimplemented!(
|
||||||
|
"Monero transactions don't support caching their preprocesses due to {}",
|
||||||
|
"being already bound to a specific transaction"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_cache(
|
||||||
|
_: (),
|
||||||
|
_: ThresholdKeys<Ed25519>,
|
||||||
|
_: Zeroizing<CachedPreprocess>,
|
||||||
|
) -> Result<Self, FrostError> {
|
||||||
|
unimplemented!(
|
||||||
|
"Monero transactions don't support caching their preprocesses due to {}",
|
||||||
|
"being already bound to a specific transaction"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
|
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
|
||||||
self.clsags.iter().map(|clsag| clsag.read_preprocess(reader)).collect()
|
self.clsags.iter().map(|clsag| clsag.read_preprocess(reader)).collect()
|
||||||
}
|
}
|
||||||
@@ -231,12 +250,18 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
|||||||
))?;
|
))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find out who's included
|
||||||
|
// This may not be a valid set of signers yet the algorithm machine will error if it's not
|
||||||
|
commitments.remove(&self.i); // Remove, if it was included for some reason
|
||||||
|
let mut included = commitments.keys().into_iter().cloned().collect::<Vec<_>>();
|
||||||
|
included.push(self.i);
|
||||||
|
included.sort_unstable();
|
||||||
|
|
||||||
// Convert the unified commitments to a Vec of the individual commitments
|
// Convert the unified commitments to a Vec of the individual commitments
|
||||||
let mut images = vec![EdwardsPoint::identity(); self.clsags.len()];
|
let mut images = vec![EdwardsPoint::identity(); self.clsags.len()];
|
||||||
let mut commitments = (0 .. self.clsags.len())
|
let mut commitments = (0 .. self.clsags.len())
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
self
|
included
|
||||||
.included
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|l| {
|
.map(|l| {
|
||||||
// Add all commitments to the transcript for their entropy
|
// Add all commitments to the transcript for their entropy
|
||||||
@@ -262,7 +287,14 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
|||||||
// provides the easiest API overall, as this is where the TX is (which needs the key
|
// provides the easiest API overall, as this is where the TX is (which needs the key
|
||||||
// images in its message), along with where the outputs are determined (where our
|
// images in its message), along with where the outputs are determined (where our
|
||||||
// outputs may need these in order to guarantee uniqueness)
|
// outputs may need these in order to guarantee uniqueness)
|
||||||
images[c] += preprocess.addendum.key_image.0;
|
add_key_image_share(
|
||||||
|
&mut images[c],
|
||||||
|
self.key_images[c].0,
|
||||||
|
self.key_images[c].1,
|
||||||
|
&included,
|
||||||
|
*l,
|
||||||
|
preprocess.addendum.key_image.0,
|
||||||
|
);
|
||||||
|
|
||||||
Ok((*l, preprocess))
|
Ok((*l, preprocess))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -216,14 +216,13 @@ macro_rules! test {
|
|||||||
keys[&i].clone(),
|
keys[&i].clone(),
|
||||||
RecommendedTranscript::new(b"Monero Serai Test Transaction"),
|
RecommendedTranscript::new(b"Monero Serai Test Transaction"),
|
||||||
rpc.get_height().await.unwrap() - 10,
|
rpc.get_height().await.unwrap() - 10,
|
||||||
(1 ..= THRESHOLD).collect::<Vec<_>>(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
frost::tests::sign(&mut OsRng, machines, &vec![])
|
frost::tests::sign_without_caching(&mut OsRng, machines, &vec![])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,11 +284,13 @@ pub struct ThresholdKeys<C: Ciphersuite> {
|
|||||||
/// View of keys passed to algorithm implementations.
|
/// View of keys passed to algorithm implementations.
|
||||||
#[derive(Clone, Zeroize)]
|
#[derive(Clone, Zeroize)]
|
||||||
pub struct ThresholdView<C: Ciphersuite> {
|
pub struct ThresholdView<C: Ciphersuite> {
|
||||||
|
offset: C::F,
|
||||||
group_key: C::G,
|
group_key: C::G,
|
||||||
#[zeroize(skip)]
|
|
||||||
included: Vec<u16>,
|
included: Vec<u16>,
|
||||||
secret_share: Zeroizing<C::F>,
|
secret_share: Zeroizing<C::F>,
|
||||||
#[zeroize(skip)]
|
#[zeroize(skip)]
|
||||||
|
original_verification_shares: HashMap<u16, C::G>,
|
||||||
|
#[zeroize(skip)]
|
||||||
verification_shares: HashMap<u16, C::G>,
|
verification_shares: HashMap<u16, C::G>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,10 +349,12 @@ impl<C: Ciphersuite> ThresholdKeys<C> {
|
|||||||
let offset_verification_share = C::generator() * offset_share;
|
let offset_verification_share = C::generator() * offset_share;
|
||||||
|
|
||||||
Ok(ThresholdView {
|
Ok(ThresholdView {
|
||||||
|
offset: self.offset.unwrap_or_else(C::F::zero),
|
||||||
group_key: self.group_key(),
|
group_key: self.group_key(),
|
||||||
secret_share: Zeroizing::new(
|
secret_share: Zeroizing::new(
|
||||||
(lagrange::<C::F>(self.params().i, included) * self.secret_share().deref()) + offset_share,
|
(lagrange::<C::F>(self.params().i, included) * self.secret_share().deref()) + offset_share,
|
||||||
),
|
),
|
||||||
|
original_verification_shares: self.verification_shares(),
|
||||||
verification_shares: self
|
verification_shares: self
|
||||||
.verification_shares()
|
.verification_shares()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -364,6 +368,10 @@ impl<C: Ciphersuite> ThresholdKeys<C> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> ThresholdView<C> {
|
impl<C: Ciphersuite> ThresholdView<C> {
|
||||||
|
pub fn offset(&self) -> C::F {
|
||||||
|
self.offset
|
||||||
|
}
|
||||||
|
|
||||||
pub fn group_key(&self) -> C::G {
|
pub fn group_key(&self) -> C::G {
|
||||||
self.group_key
|
self.group_key
|
||||||
}
|
}
|
||||||
@@ -376,6 +384,10 @@ impl<C: Ciphersuite> ThresholdView<C> {
|
|||||||
&self.secret_share
|
&self.secret_share
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn original_verification_share(&self, l: u16) -> C::G {
|
||||||
|
self.original_verification_shares[&l]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn verification_share(&self, l: u16) -> C::G {
|
pub fn verification_share(&self, l: u16) -> C::G {
|
||||||
self.verification_shares[&l]
|
self.verification_shares[&l]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"]
|
|||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
rand_core = "0.6"
|
rand_core = "0.6"
|
||||||
|
rand_chacha = "0.3"
|
||||||
|
|
||||||
zeroize = { version = "1.5", features = ["zeroize_derive"] }
|
zeroize = { version = "1.5", features = ["zeroize_derive"] }
|
||||||
subtle = "2"
|
subtle = "2"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use rand_core::{RngCore, CryptoRng};
|
|||||||
|
|
||||||
use transcript::Transcript;
|
use transcript::Transcript;
|
||||||
|
|
||||||
use crate::{Curve, FrostError, ThresholdView};
|
use crate::{Curve, FrostError, ThresholdKeys, ThresholdView};
|
||||||
pub use schnorr::SchnorrSignature;
|
pub use schnorr::SchnorrSignature;
|
||||||
|
|
||||||
/// Write an addendum to a writer.
|
/// Write an addendum to a writer.
|
||||||
@@ -45,7 +45,7 @@ pub trait Algorithm<C: Curve>: Clone {
|
|||||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
params: &ThresholdView<C>,
|
keys: &ThresholdKeys<C>,
|
||||||
) -> Self::Addendum;
|
) -> Self::Addendum;
|
||||||
|
|
||||||
/// Read an addendum from a reader.
|
/// Read an addendum from a reader.
|
||||||
@@ -148,7 +148,7 @@ impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
|
|||||||
vec![vec![C::generator()]]
|
vec![vec![C::generator()]]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preprocess_addendum<R: RngCore + CryptoRng>(&mut self, _: &mut R, _: &ThresholdView<C>) {}
|
fn preprocess_addendum<R: RngCore + CryptoRng>(&mut self, _: &mut R, _: &ThresholdKeys<C>) {}
|
||||||
|
|
||||||
fn read_addendum<R: Read>(&self, _: &mut R) -> io::Result<Self::Addendum> {
|
fn read_addendum<R: Read>(&self, _: &mut R) -> io::Result<Self::Addendum> {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub mod sign;
|
|||||||
pub mod tests;
|
pub mod tests;
|
||||||
|
|
||||||
// Validate a map of values to have the expected included participants
|
// Validate a map of values to have the expected included participants
|
||||||
pub(crate) fn validate_map<T>(
|
pub fn validate_map<T>(
|
||||||
map: &HashMap<u16, T>,
|
map: &HashMap<u16, T>,
|
||||||
included: &[u16],
|
included: &[u16],
|
||||||
ours: u16,
|
ours: u16,
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||||
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||||
|
|
||||||
use transcript::Transcript;
|
use transcript::Transcript;
|
||||||
|
|
||||||
@@ -47,54 +48,16 @@ pub struct Params<C: Curve, A: Algorithm<C>> {
|
|||||||
#[zeroize(skip)]
|
#[zeroize(skip)]
|
||||||
algorithm: A,
|
algorithm: A,
|
||||||
keys: ThresholdKeys<C>,
|
keys: ThresholdKeys<C>,
|
||||||
view: ThresholdView<C>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Curve, A: Algorithm<C>> Params<C, A> {
|
impl<C: Curve, A: Algorithm<C>> Params<C, A> {
|
||||||
pub fn new(
|
pub fn new(algorithm: A, keys: ThresholdKeys<C>) -> Result<Params<C, A>, FrostError> {
|
||||||
algorithm: A,
|
Ok(Params { algorithm, keys })
|
||||||
keys: ThresholdKeys<C>,
|
|
||||||
included: &[u16],
|
|
||||||
) -> Result<Params<C, A>, FrostError> {
|
|
||||||
let params = keys.params();
|
|
||||||
|
|
||||||
let mut included = included.to_vec();
|
|
||||||
included.sort_unstable();
|
|
||||||
|
|
||||||
// Included < threshold
|
|
||||||
if included.len() < usize::from(params.t()) {
|
|
||||||
Err(FrostError::InvalidSigningSet("not enough signers"))?;
|
|
||||||
}
|
|
||||||
// Invalid index
|
|
||||||
if included[0] == 0 {
|
|
||||||
Err(FrostError::InvalidParticipantIndex(included[0], params.n()))?;
|
|
||||||
}
|
|
||||||
// OOB index
|
|
||||||
if included[included.len() - 1] > params.n() {
|
|
||||||
Err(FrostError::InvalidParticipantIndex(included[included.len() - 1], params.n()))?;
|
|
||||||
}
|
|
||||||
// Same signer included multiple times
|
|
||||||
for i in 0 .. (included.len() - 1) {
|
|
||||||
if included[i] == included[i + 1] {
|
|
||||||
Err(FrostError::DuplicatedIndex(included[i]))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Not included
|
|
||||||
if !included.contains(¶ms.i()) {
|
|
||||||
Err(FrostError::InvalidSigningSet("signing despite not being included"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Out of order arguments to prevent additional cloning
|
|
||||||
Ok(Params { algorithm, view: keys.view(&included).unwrap(), keys })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn multisig_params(&self) -> ThresholdParams {
|
pub fn multisig_params(&self) -> ThresholdParams {
|
||||||
self.keys.params()
|
self.keys.params()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self) -> ThresholdView<C> {
|
|
||||||
self.view.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Preprocess for an instance of the FROST signing protocol.
|
/// Preprocess for an instance of the FROST signing protocol.
|
||||||
@@ -111,6 +74,12 @@ impl<C: Curve, A: Addendum> Writable for Preprocess<C, A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A cached preprocess. A preprocess MUST only be used once. Reuse will enable third-party
|
||||||
|
/// recovery of your private key share. Additionally, this MUST be handled with the same security
|
||||||
|
/// as your private key share, as knowledge of it also enables recovery.
|
||||||
|
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||||
|
pub struct CachedPreprocess(pub [u8; 32]);
|
||||||
|
|
||||||
/// Trait for the initial state machine of a two-round signing protocol.
|
/// Trait for the initial state machine of a two-round signing protocol.
|
||||||
pub trait PreprocessMachine {
|
pub trait PreprocessMachine {
|
||||||
/// Preprocess message for this machine.
|
/// Preprocess message for this machine.
|
||||||
@@ -134,12 +103,26 @@ pub struct AlgorithmMachine<C: Curve, A: Algorithm<C>> {
|
|||||||
|
|
||||||
impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
|
impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
|
||||||
/// Creates a new machine to generate a signature with the specified keys.
|
/// Creates a new machine to generate a signature with the specified keys.
|
||||||
pub fn new(
|
pub fn new(algorithm: A, keys: ThresholdKeys<C>) -> Result<AlgorithmMachine<C, A>, FrostError> {
|
||||||
algorithm: A,
|
Ok(AlgorithmMachine { params: Params::new(algorithm, keys)? })
|
||||||
keys: ThresholdKeys<C>,
|
}
|
||||||
included: &[u16],
|
|
||||||
) -> Result<AlgorithmMachine<C, A>, FrostError> {
|
fn seeded_preprocess(
|
||||||
Ok(AlgorithmMachine { params: Params::new(algorithm, keys, included)? })
|
self,
|
||||||
|
seed: Zeroizing<CachedPreprocess>,
|
||||||
|
) -> (AlgorithmSignMachine<C, A>, Preprocess<C, A::Addendum>) {
|
||||||
|
let mut params = self.params;
|
||||||
|
|
||||||
|
let mut rng = ChaCha20Rng::from_seed(seed.0);
|
||||||
|
let (nonces, commitments) = Commitments::new::<_, A::Transcript>(
|
||||||
|
&mut rng,
|
||||||
|
params.keys.secret_share(),
|
||||||
|
¶ms.algorithm.nonces(),
|
||||||
|
);
|
||||||
|
let addendum = params.algorithm.preprocess_addendum(&mut rng, ¶ms.keys);
|
||||||
|
|
||||||
|
let preprocess = Preprocess { commitments, addendum };
|
||||||
|
(AlgorithmSignMachine { params, seed, nonces, preprocess: preprocess.clone() }, preprocess)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "tests"))]
|
#[cfg(any(test, feature = "tests"))]
|
||||||
@@ -148,7 +131,12 @@ impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
|
|||||||
nonces: Vec<Nonce<C>>,
|
nonces: Vec<Nonce<C>>,
|
||||||
preprocess: Preprocess<C, A::Addendum>,
|
preprocess: Preprocess<C, A::Addendum>,
|
||||||
) -> AlgorithmSignMachine<C, A> {
|
) -> AlgorithmSignMachine<C, A> {
|
||||||
AlgorithmSignMachine { params: self.params, nonces, preprocess }
|
AlgorithmSignMachine {
|
||||||
|
params: self.params,
|
||||||
|
seed: Zeroizing::new(CachedPreprocess([0; 32])),
|
||||||
|
nonces,
|
||||||
|
preprocess,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,17 +149,9 @@ impl<C: Curve, A: Algorithm<C>> PreprocessMachine for AlgorithmMachine<C, A> {
|
|||||||
self,
|
self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
) -> (Self::SignMachine, Preprocess<C, A::Addendum>) {
|
) -> (Self::SignMachine, Preprocess<C, A::Addendum>) {
|
||||||
let mut params = self.params;
|
let mut seed = Zeroizing::new(CachedPreprocess([0; 32]));
|
||||||
|
rng.fill_bytes(seed.0.as_mut());
|
||||||
let (nonces, commitments) = Commitments::new::<_, A::Transcript>(
|
self.seeded_preprocess(seed)
|
||||||
&mut *rng,
|
|
||||||
params.view().secret_share(),
|
|
||||||
¶ms.algorithm.nonces(),
|
|
||||||
);
|
|
||||||
let addendum = params.algorithm.preprocess_addendum(rng, ¶ms.view);
|
|
||||||
|
|
||||||
let preprocess = Preprocess { commitments, addendum };
|
|
||||||
(AlgorithmSignMachine { params, nonces, preprocess: preprocess.clone() }, preprocess)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +165,11 @@ impl<C: Curve> Writable for SignatureShare<C> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for the second machine of a two-round signing protocol.
|
/// Trait for the second machine of a two-round signing protocol.
|
||||||
pub trait SignMachine<S> {
|
pub trait SignMachine<S>: Sized {
|
||||||
|
/// Params used to instantiate this machine which can be used to rebuild from a cache.
|
||||||
|
type Params: Clone;
|
||||||
|
/// Keys used for signing operations.
|
||||||
|
type Keys;
|
||||||
/// Preprocess message for this machine.
|
/// Preprocess message for this machine.
|
||||||
type Preprocess: Clone + PartialEq + Writable;
|
type Preprocess: Clone + PartialEq + Writable;
|
||||||
/// SignatureShare message for this machine.
|
/// SignatureShare message for this machine.
|
||||||
@@ -193,12 +177,28 @@ pub trait SignMachine<S> {
|
|||||||
/// SignatureMachine this SignMachine turns into.
|
/// SignatureMachine this SignMachine turns into.
|
||||||
type SignatureMachine: SignatureMachine<S, SignatureShare = Self::SignatureShare>;
|
type SignatureMachine: SignatureMachine<S, SignatureShare = Self::SignatureShare>;
|
||||||
|
|
||||||
/// Read a Preprocess message.
|
/// Cache this preprocess for usage later. This cached preprocess MUST only be used once. Reuse
|
||||||
|
/// of it enables recovery of your private key share. Third-party recovery of a cached preprocess
|
||||||
|
/// also enables recovery of your private key share, so this MUST be treated with the same
|
||||||
|
/// security as your private key share.
|
||||||
|
fn cache(self) -> Zeroizing<CachedPreprocess>;
|
||||||
|
|
||||||
|
/// Create a sign machine from a cached preprocess. After this, the preprocess should be fully
|
||||||
|
/// deleted, as it must never be reused. It is
|
||||||
|
fn from_cache(
|
||||||
|
params: Self::Params,
|
||||||
|
keys: Self::Keys,
|
||||||
|
cache: Zeroizing<CachedPreprocess>,
|
||||||
|
) -> Result<Self, FrostError>;
|
||||||
|
|
||||||
|
/// Read a Preprocess message. Despite taking self, this does not save the preprocess.
|
||||||
|
/// It must be externally cached and passed into sign.
|
||||||
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess>;
|
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess>;
|
||||||
|
|
||||||
/// Sign a message.
|
/// Sign a message.
|
||||||
/// Takes in the participants' preprocess messages. Returns the signature share to be broadcast
|
/// Takes in the participants' preprocess messages. Returns the signature share to be broadcast
|
||||||
/// to all participants, over an authenticated channel.
|
/// to all participants, over an authenticated channel. The parties who participate here will
|
||||||
|
/// become the signing set for this session.
|
||||||
fn sign(
|
fn sign(
|
||||||
self,
|
self,
|
||||||
commitments: HashMap<u16, Self::Preprocess>,
|
commitments: HashMap<u16, Self::Preprocess>,
|
||||||
@@ -210,16 +210,33 @@ pub trait SignMachine<S> {
|
|||||||
#[derive(Zeroize)]
|
#[derive(Zeroize)]
|
||||||
pub struct AlgorithmSignMachine<C: Curve, A: Algorithm<C>> {
|
pub struct AlgorithmSignMachine<C: Curve, A: Algorithm<C>> {
|
||||||
params: Params<C, A>,
|
params: Params<C, A>,
|
||||||
|
seed: Zeroizing<CachedPreprocess>,
|
||||||
|
|
||||||
pub(crate) nonces: Vec<Nonce<C>>,
|
pub(crate) nonces: Vec<Nonce<C>>,
|
||||||
#[zeroize(skip)]
|
#[zeroize(skip)]
|
||||||
pub(crate) preprocess: Preprocess<C, A::Addendum>,
|
pub(crate) preprocess: Preprocess<C, A::Addendum>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachine<C, A> {
|
impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachine<C, A> {
|
||||||
|
type Params = A;
|
||||||
|
type Keys = ThresholdKeys<C>;
|
||||||
type Preprocess = Preprocess<C, A::Addendum>;
|
type Preprocess = Preprocess<C, A::Addendum>;
|
||||||
type SignatureShare = SignatureShare<C>;
|
type SignatureShare = SignatureShare<C>;
|
||||||
type SignatureMachine = AlgorithmSignatureMachine<C, A>;
|
type SignatureMachine = AlgorithmSignatureMachine<C, A>;
|
||||||
|
|
||||||
|
fn cache(self) -> Zeroizing<CachedPreprocess> {
|
||||||
|
self.seed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_cache(
|
||||||
|
algorithm: A,
|
||||||
|
keys: ThresholdKeys<C>,
|
||||||
|
cache: Zeroizing<CachedPreprocess>,
|
||||||
|
) -> Result<Self, FrostError> {
|
||||||
|
let (machine, _) = AlgorithmMachine::new(algorithm, keys)?.seeded_preprocess(cache);
|
||||||
|
Ok(machine)
|
||||||
|
}
|
||||||
|
|
||||||
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
|
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
|
||||||
Ok(Preprocess {
|
Ok(Preprocess {
|
||||||
commitments: Commitments::read::<_, A::Transcript>(reader, &self.params.algorithm.nonces())?,
|
commitments: Commitments::read::<_, A::Transcript>(reader, &self.params.algorithm.nonces())?,
|
||||||
@@ -233,7 +250,35 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
|
|||||||
msg: &[u8],
|
msg: &[u8],
|
||||||
) -> Result<(Self::SignatureMachine, SignatureShare<C>), FrostError> {
|
) -> Result<(Self::SignatureMachine, SignatureShare<C>), FrostError> {
|
||||||
let multisig_params = self.params.multisig_params();
|
let multisig_params = self.params.multisig_params();
|
||||||
validate_map(&preprocesses, &self.params.view.included(), multisig_params.i())?;
|
|
||||||
|
let mut included = Vec::with_capacity(preprocesses.len() + 1);
|
||||||
|
included.push(multisig_params.i());
|
||||||
|
for l in preprocesses.keys() {
|
||||||
|
included.push(*l);
|
||||||
|
}
|
||||||
|
included.sort_unstable();
|
||||||
|
|
||||||
|
// Included < threshold
|
||||||
|
if included.len() < usize::from(multisig_params.t()) {
|
||||||
|
Err(FrostError::InvalidSigningSet("not enough signers"))?;
|
||||||
|
}
|
||||||
|
// Invalid index
|
||||||
|
if included[0] == 0 {
|
||||||
|
Err(FrostError::InvalidParticipantIndex(included[0], multisig_params.n()))?;
|
||||||
|
}
|
||||||
|
// OOB index
|
||||||
|
if included[included.len() - 1] > multisig_params.n() {
|
||||||
|
Err(FrostError::InvalidParticipantIndex(included[included.len() - 1], multisig_params.n()))?;
|
||||||
|
}
|
||||||
|
// Same signer included multiple times
|
||||||
|
for i in 0 .. (included.len() - 1) {
|
||||||
|
if included[i] == included[i + 1] {
|
||||||
|
Err(FrostError::DuplicatedIndex(included[i]))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let view = self.params.keys.view(&included).unwrap();
|
||||||
|
validate_map(&preprocesses, &included, multisig_params.i())?;
|
||||||
|
|
||||||
{
|
{
|
||||||
// Domain separate FROST
|
// Domain separate FROST
|
||||||
@@ -242,10 +287,10 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
|
|||||||
|
|
||||||
let nonces = self.params.algorithm.nonces();
|
let nonces = self.params.algorithm.nonces();
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
let mut B = BindingFactor(HashMap::<u16, _>::with_capacity(self.params.view.included().len()));
|
let mut B = BindingFactor(HashMap::<u16, _>::with_capacity(included.len()));
|
||||||
{
|
{
|
||||||
// Parse the preprocesses
|
// Parse the preprocesses
|
||||||
for l in &self.params.view.included() {
|
for l in &included {
|
||||||
{
|
{
|
||||||
self
|
self
|
||||||
.params
|
.params
|
||||||
@@ -266,7 +311,7 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
|
|||||||
}
|
}
|
||||||
|
|
||||||
B.insert(*l, commitments);
|
B.insert(*l, commitments);
|
||||||
self.params.algorithm.process_addendum(&self.params.view, *l, addendum)?;
|
self.params.algorithm.process_addendum(&view, *l, addendum)?;
|
||||||
} else {
|
} else {
|
||||||
let preprocess = preprocesses.remove(l).unwrap();
|
let preprocess = preprocesses.remove(l).unwrap();
|
||||||
preprocess.commitments.transcript(self.params.algorithm.transcript());
|
preprocess.commitments.transcript(self.params.algorithm.transcript());
|
||||||
@@ -277,7 +322,7 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
|
|||||||
}
|
}
|
||||||
|
|
||||||
B.insert(*l, preprocess.commitments);
|
B.insert(*l, preprocess.commitments);
|
||||||
self.params.algorithm.process_addendum(&self.params.view, *l, preprocess.addendum)?;
|
self.params.algorithm.process_addendum(&view, *l, preprocess.addendum)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,10 +378,10 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let share = self.params.algorithm.sign_share(&self.params.view, &Rs, nonces, msg);
|
let share = self.params.algorithm.sign_share(&view, &Rs, nonces, msg);
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
AlgorithmSignatureMachine { params: self.params.clone(), B, Rs, share },
|
AlgorithmSignatureMachine { params: self.params.clone(), view, B, Rs, share },
|
||||||
SignatureShare(share),
|
SignatureShare(share),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -359,6 +404,7 @@ pub trait SignatureMachine<S> {
|
|||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub struct AlgorithmSignatureMachine<C: Curve, A: Algorithm<C>> {
|
pub struct AlgorithmSignatureMachine<C: Curve, A: Algorithm<C>> {
|
||||||
params: Params<C, A>,
|
params: Params<C, A>,
|
||||||
|
view: ThresholdView<C>,
|
||||||
B: BindingFactor<C>,
|
B: BindingFactor<C>,
|
||||||
Rs: Vec<Vec<C::G>>,
|
Rs: Vec<Vec<C::G>>,
|
||||||
share: C::F,
|
share: C::F,
|
||||||
@@ -376,7 +422,7 @@ impl<C: Curve, A: Algorithm<C>> SignatureMachine<A::Signature> for AlgorithmSign
|
|||||||
mut shares: HashMap<u16, SignatureShare<C>>,
|
mut shares: HashMap<u16, SignatureShare<C>>,
|
||||||
) -> Result<A::Signature, FrostError> {
|
) -> Result<A::Signature, FrostError> {
|
||||||
let params = self.params.multisig_params();
|
let params = self.params.multisig_params();
|
||||||
validate_map(&shares, &self.params.view.included(), params.i())?;
|
validate_map(&shares, &self.view.included(), params.i())?;
|
||||||
|
|
||||||
let mut responses = HashMap::new();
|
let mut responses = HashMap::new();
|
||||||
responses.insert(params.i(), self.share);
|
responses.insert(params.i(), self.share);
|
||||||
@@ -389,16 +435,16 @@ impl<C: Curve, A: Algorithm<C>> SignatureMachine<A::Signature> for AlgorithmSign
|
|||||||
// Perform signature validation instead of individual share validation
|
// Perform signature validation instead of individual share validation
|
||||||
// For the success route, which should be much more frequent, this should be faster
|
// For the success route, which should be much more frequent, this should be faster
|
||||||
// It also acts as an integrity check of this library's signing function
|
// It also acts as an integrity check of this library's signing function
|
||||||
if let Some(sig) = self.params.algorithm.verify(self.params.view.group_key(), &self.Rs, sum) {
|
if let Some(sig) = self.params.algorithm.verify(self.view.group_key(), &self.Rs, sum) {
|
||||||
return Ok(sig);
|
return Ok(sig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find out who misbehaved. It may be beneficial to randomly sort this to have detection be
|
// Find out who misbehaved. It may be beneficial to randomly sort this to have detection be
|
||||||
// within n / 2 on average, and not gameable to n, though that should be minor
|
// within n / 2 on average, and not gameable to n, though that should be minor
|
||||||
// TODO
|
// TODO
|
||||||
for l in &self.params.view.included() {
|
for l in &self.view.included() {
|
||||||
if !self.params.algorithm.verify_share(
|
if !self.params.algorithm.verify_share(
|
||||||
self.params.view.verification_share(*l),
|
self.view.verification_share(*l),
|
||||||
&self.B.bound(*l),
|
&self.B.bound(*l),
|
||||||
responses[l],
|
responses[l],
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -53,10 +53,7 @@ pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(i, keys)| {
|
.filter_map(|(i, keys)| {
|
||||||
if included.contains(i) {
|
if included.contains(i) {
|
||||||
Some((
|
Some((*i, AlgorithmMachine::new(algorithm.clone(), keys.clone()).unwrap()))
|
||||||
*i,
|
|
||||||
AlgorithmMachine::new(algorithm.clone(), keys.clone(), &included.clone()).unwrap(),
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -64,10 +61,14 @@ pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the signing protocol.
|
fn sign_internal<
|
||||||
pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
R: RngCore + CryptoRng,
|
||||||
|
M: PreprocessMachine,
|
||||||
|
F: FnMut(&mut R, &mut HashMap<u16, M::SignMachine>),
|
||||||
|
>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
mut machines: HashMap<u16, M>,
|
mut machines: HashMap<u16, M>,
|
||||||
|
mut cache: F,
|
||||||
msg: &[u8],
|
msg: &[u8],
|
||||||
) -> M::Signature {
|
) -> M::Signature {
|
||||||
let mut commitments = HashMap::new();
|
let mut commitments = HashMap::new();
|
||||||
@@ -84,6 +85,8 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
|||||||
})
|
})
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
cache(rng, &mut machines);
|
||||||
|
|
||||||
let mut shares = HashMap::new();
|
let mut shares = HashMap::new();
|
||||||
let mut machines = machines
|
let mut machines = machines
|
||||||
.drain()
|
.drain()
|
||||||
@@ -108,3 +111,43 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
|||||||
}
|
}
|
||||||
signature.unwrap()
|
signature.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute the signing protocol, without caching any machines. This isn't as comprehensive at
|
||||||
|
/// testing as sign, and accordingly isn't preferred, yet is usable for machines not supporting
|
||||||
|
/// caching.
|
||||||
|
pub fn sign_without_caching<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
||||||
|
rng: &mut R,
|
||||||
|
machines: HashMap<u16, M>,
|
||||||
|
msg: &[u8],
|
||||||
|
) -> M::Signature {
|
||||||
|
sign_internal(rng, machines, |_, _| {}, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute the signing protocol, randomly caching various machines to ensure they can cache
|
||||||
|
/// successfully.
|
||||||
|
pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
||||||
|
rng: &mut R,
|
||||||
|
params: <M::SignMachine as SignMachine<M::Signature>>::Params,
|
||||||
|
mut keys: HashMap<u16, <M::SignMachine as SignMachine<M::Signature>>::Keys>,
|
||||||
|
machines: HashMap<u16, M>,
|
||||||
|
msg: &[u8],
|
||||||
|
) -> M::Signature {
|
||||||
|
sign_internal(
|
||||||
|
rng,
|
||||||
|
machines,
|
||||||
|
|rng, machines| {
|
||||||
|
// Cache and rebuild half of the machines
|
||||||
|
let mut included = machines.keys().into_iter().cloned().collect::<Vec<_>>();
|
||||||
|
for i in included.drain(..) {
|
||||||
|
if (rng.next_u64() % 2) == 0 {
|
||||||
|
let cache = machines.remove(&i).unwrap().cache();
|
||||||
|
machines.insert(
|
||||||
|
i,
|
||||||
|
M::SignMachine::from_cache(params.clone(), keys.remove(&i).unwrap(), cache).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use rand_core::{RngCore, CryptoRng};
|
|||||||
|
|
||||||
use group::{ff::PrimeField, GroupEncoding};
|
use group::{ff::PrimeField, GroupEncoding};
|
||||||
|
|
||||||
use dkg::tests::{test_ciphersuite as test_dkg};
|
use dkg::tests::{key_gen, test_ciphersuite as test_dkg};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
curve::Curve,
|
curve::Curve,
|
||||||
@@ -19,7 +19,7 @@ use crate::{
|
|||||||
Nonce, GeneratorCommitments, NonceCommitments, Commitments, Writable, Preprocess, SignMachine,
|
Nonce, GeneratorCommitments, NonceCommitments, Commitments, Writable, Preprocess, SignMachine,
|
||||||
SignatureMachine, AlgorithmMachine,
|
SignatureMachine, AlgorithmMachine,
|
||||||
},
|
},
|
||||||
tests::{clone_without, recover_key, curve::test_curve},
|
tests::{clone_without, recover_key, algorithm_machines, sign, curve::test_curve},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Vectors {
|
pub struct Vectors {
|
||||||
@@ -124,6 +124,15 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
|||||||
// Test the DKG
|
// Test the DKG
|
||||||
test_dkg::<_, C>(&mut *rng);
|
test_dkg::<_, C>(&mut *rng);
|
||||||
|
|
||||||
|
// Test a basic Schnorr signature
|
||||||
|
{
|
||||||
|
let keys = key_gen(&mut *rng);
|
||||||
|
let machines = algorithm_machines(&mut *rng, Schnorr::<C, H>::new(), &keys);
|
||||||
|
const MSG: &[u8] = b"Hello, World!";
|
||||||
|
let sig = sign(&mut *rng, Schnorr::<C, H>::new(), keys.clone(), machines, MSG);
|
||||||
|
assert!(sig.verify(keys[&1].group_key(), H::hram(&sig.R, &keys[&1].group_key(), MSG)));
|
||||||
|
}
|
||||||
|
|
||||||
// Test against the vectors
|
// Test against the vectors
|
||||||
let keys = vectors_to_multisig_keys::<C>(&vectors);
|
let keys = vectors_to_multisig_keys::<C>(&vectors);
|
||||||
let group_key =
|
let group_key =
|
||||||
@@ -135,15 +144,7 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
|||||||
|
|
||||||
let mut machines = vec![];
|
let mut machines = vec![];
|
||||||
for i in &vectors.included {
|
for i in &vectors.included {
|
||||||
machines.push((
|
machines.push((i, AlgorithmMachine::new(Schnorr::<C, H>::new(), keys[i].clone()).unwrap()));
|
||||||
i,
|
|
||||||
AlgorithmMachine::new(
|
|
||||||
Schnorr::<C, H>::new(),
|
|
||||||
keys[i].clone(),
|
|
||||||
&vectors.included.to_vec().clone(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut commitments = HashMap::new();
|
let mut commitments = HashMap::new();
|
||||||
|
|||||||
@@ -35,3 +35,22 @@ The public key signed for is also offset by this value. During the signing
|
|||||||
process, the offset is explicitly transcripted. Then, the offset is divided by
|
process, the offset is explicitly transcripted. Then, the offset is divided by
|
||||||
`p`, the amount of participating signers, and each signer adds it to their
|
`p`, the amount of participating signers, and each signer adds it to their
|
||||||
post-interpolation key share.
|
post-interpolation key share.
|
||||||
|
|
||||||
|
# Caching
|
||||||
|
|
||||||
|
modular-frost supports caching a preprocess. This is done by having all
|
||||||
|
preprocesses use a seeded RNG. Accordingly, the entire preprocess can be derived
|
||||||
|
from the RNG seed, making the cache just the seed.
|
||||||
|
|
||||||
|
Reusing preprocesses would enable a third-party to recover your private key
|
||||||
|
share. Accordingly, you MUST not reuse preprocesses. Third-party knowledge of
|
||||||
|
your preprocess would also enable their recovery of your private key share.
|
||||||
|
Accordingly, you MUST treat cached preprocesses with the same security as your
|
||||||
|
private key share.
|
||||||
|
|
||||||
|
Since a reused seed will lead to a reused preprocess, seeded RNGs are generally
|
||||||
|
frowned upon when doing multisignature operations. This isn't an issue as each
|
||||||
|
new preprocess obtains a fresh seed from the specified RNG. Assuming the
|
||||||
|
provided RNG isn't generating the same seed multiple times, the only way for
|
||||||
|
this seeded RNG to fail is if a preprocess is loaded multiple times, which was
|
||||||
|
already a failure point.
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ pub trait Coin {
|
|||||||
async fn attempt_send(
|
async fn attempt_send(
|
||||||
&self,
|
&self,
|
||||||
transaction: Self::SignableTransaction,
|
transaction: Self::SignableTransaction,
|
||||||
included: &[u16],
|
|
||||||
) -> Result<Self::TransactionMachine, CoinError>;
|
) -> Result<Self::TransactionMachine, CoinError>;
|
||||||
|
|
||||||
async fn publish_transaction(
|
async fn publish_transaction(
|
||||||
|
|||||||
@@ -188,7 +188,6 @@ impl Coin for Monero {
|
|||||||
async fn attempt_send(
|
async fn attempt_send(
|
||||||
&self,
|
&self,
|
||||||
transaction: SignableTransaction,
|
transaction: SignableTransaction,
|
||||||
included: &[u16],
|
|
||||||
) -> Result<Self::TransactionMachine, CoinError> {
|
) -> Result<Self::TransactionMachine, CoinError> {
|
||||||
transaction
|
transaction
|
||||||
.actual
|
.actual
|
||||||
@@ -198,7 +197,6 @@ impl Coin for Monero {
|
|||||||
transaction.keys.clone(),
|
transaction.keys.clone(),
|
||||||
transaction.transcript.clone(),
|
transaction.transcript.clone(),
|
||||||
transaction.height,
|
transaction.height,
|
||||||
included.to_vec(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| CoinError::ConnectionError)
|
.map_err(|_| CoinError::ConnectionError)
|
||||||
@@ -209,7 +207,6 @@ impl Coin for Monero {
|
|||||||
tx: &Self::Transaction,
|
tx: &Self::Transaction,
|
||||||
) -> Result<(Vec<u8>, Vec<<Self::Output as OutputTrait>::Id>), CoinError> {
|
) -> Result<(Vec<u8>, Vec<<Self::Output as OutputTrait>::Id>), CoinError> {
|
||||||
self.rpc.publish_transaction(tx).await.map_err(|_| CoinError::ConnectionError)?;
|
self.rpc.publish_transaction(tx).await.map_err(|_| CoinError::ConnectionError)?;
|
||||||
|
|
||||||
Ok((tx.hash().to_vec(), tx.prefix.outputs.iter().map(|output| output.key.to_bytes()).collect()))
|
Ok((tx.hash().to_vec(), tx.prefix.outputs.iter().map(|output| output.key.to_bytes()).collect()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,11 +98,7 @@ async fn test_send<C: Coin + Clone>(coin: C, fee: C::Fee) {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.1
|
.1
|
||||||
.swap_remove(0);
|
.swap_remove(0);
|
||||||
futures.push(wallet.attempt_send(
|
futures.push(wallet.attempt_send(network, signable));
|
||||||
network,
|
|
||||||
signable,
|
|
||||||
(1 ..= threshold).into_iter().collect::<Vec<_>>(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{:?}", hex::encode(futures::future::join_all(futures).await.swap_remove(0).unwrap().0));
|
println!("{:?}", hex::encode(futures::future::join_all(futures).await.swap_remove(0).unwrap().0));
|
||||||
|
|||||||
@@ -339,10 +339,8 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
network: &mut N,
|
network: &mut N,
|
||||||
prepared: C::SignableTransaction,
|
prepared: C::SignableTransaction,
|
||||||
included: Vec<u16>,
|
|
||||||
) -> Result<(Vec<u8>, Vec<<C::Output as Output>::Id>), SignError> {
|
) -> Result<(Vec<u8>, Vec<<C::Output as Output>::Id>), SignError> {
|
||||||
let attempt =
|
let attempt = self.coin.attempt_send(prepared).await.map_err(SignError::CoinError)?;
|
||||||
self.coin.attempt_send(prepared, &included).await.map_err(SignError::CoinError)?;
|
|
||||||
|
|
||||||
let (attempt, commitments) = attempt.preprocess(&mut OsRng);
|
let (attempt, commitments) = attempt.preprocess(&mut OsRng);
|
||||||
let commitments = network
|
let commitments = network
|
||||||
|
|||||||
Reference in New Issue
Block a user