mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 04:39: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:
@@ -51,9 +51,12 @@ async fn test_ecrecover_hack() {
|
||||
|
||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
||||
|
||||
let algo = Algo::<Secp256k1, crypto::EthereumHram>::new();
|
||||
let sig = sign(
|
||||
&mut OsRng,
|
||||
algorithm_machines(&mut OsRng, Algo::<Secp256k1, crypto::EthereumHram>::new(), &keys),
|
||||
algo.clone(),
|
||||
keys.clone(),
|
||||
algorithm_machines(&mut OsRng, algo, &keys),
|
||||
full_message,
|
||||
);
|
||||
let mut processed_sig =
|
||||
|
||||
@@ -40,8 +40,11 @@ fn test_signing() {
|
||||
|
||||
const MESSAGE: &'static [u8] = b"Hello, World!";
|
||||
|
||||
let algo = Schnorr::<Secp256k1, EthereumHram>::new();
|
||||
let _sig = sign(
|
||||
&mut OsRng,
|
||||
algo.clone(),
|
||||
keys.clone(),
|
||||
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, EthereumHram>::new(), &keys),
|
||||
MESSAGE,
|
||||
);
|
||||
@@ -67,9 +70,12 @@ fn test_ecrecover_hack() {
|
||||
|
||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
||||
|
||||
let algo = Schnorr::<Secp256k1, EthereumHram>::new();
|
||||
let sig = sign(
|
||||
&mut OsRng,
|
||||
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, EthereumHram>::new(), &keys),
|
||||
algo.clone(),
|
||||
keys.clone(),
|
||||
algorithm_machines(&mut OsRng, algo, &keys),
|
||||
full_message,
|
||||
);
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ use crate::{
|
||||
mod multisig;
|
||||
#[cfg(feature = "multisig")]
|
||||
pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig};
|
||||
#[cfg(feature = "multisig")]
|
||||
pub(crate) use multisig::add_key_image_share;
|
||||
|
||||
lazy_static! {
|
||||
static ref INV_EIGHT: Scalar = Scalar::from(8u8).invert();
|
||||
|
||||
@@ -22,8 +22,9 @@ use transcript::{Transcript, RecommendedTranscript};
|
||||
use dalek_ff_group as dfg;
|
||||
use dleq::DLEqProof;
|
||||
use frost::{
|
||||
dkg::lagrange,
|
||||
curve::Ed25519,
|
||||
FrostError, ThresholdView,
|
||||
FrostError, ThresholdKeys, ThresholdView,
|
||||
algorithm::{WriteAddendum, Algorithm},
|
||||
};
|
||||
|
||||
@@ -103,7 +104,7 @@ struct Interim {
|
||||
pub struct ClsagMultisig {
|
||||
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
|
||||
// an extra round
|
||||
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 {
|
||||
type Transcript = RecommendedTranscript;
|
||||
type Addendum = ClsagAddendum;
|
||||
@@ -154,10 +169,10 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
view: &ThresholdView<Ed25519>,
|
||||
keys: &ThresholdKeys<Ed25519>,
|
||||
) -> 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(
|
||||
rng,
|
||||
// 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)
|
||||
&mut dleq_transcript(),
|
||||
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
|
||||
view.secret_share(),
|
||||
keys.secret_share(),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -205,12 +220,19 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
||||
.verify(
|
||||
&mut dleq_transcript(),
|
||||
&[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))?;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -101,28 +101,28 @@ fn clsag_multisig() {
|
||||
}
|
||||
|
||||
let mask_sum = random_scalar(&mut OsRng);
|
||||
let algorithm = ClsagMultisig::new(
|
||||
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
|
||||
keys[&1].group_key().0,
|
||||
Arc::new(RwLock::new(Some(ClsagDetails::new(
|
||||
ClsagInput::new(
|
||||
Commitment::new(randomness, AMOUNT),
|
||||
Decoys {
|
||||
i: RING_INDEX,
|
||||
offsets: (1 ..= RING_LEN).into_iter().collect(),
|
||||
ring: ring.clone(),
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
mask_sum,
|
||||
)))),
|
||||
);
|
||||
|
||||
sign(
|
||||
&mut OsRng,
|
||||
algorithm_machines(
|
||||
&mut OsRng,
|
||||
ClsagMultisig::new(
|
||||
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
|
||||
keys[&1].group_key().0,
|
||||
Arc::new(RwLock::new(Some(ClsagDetails::new(
|
||||
ClsagInput::new(
|
||||
Commitment::new(randomness, AMOUNT),
|
||||
Decoys {
|
||||
i: RING_INDEX,
|
||||
offsets: (1 ..= RING_LEN).into_iter().collect(),
|
||||
ring: ring.clone(),
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
mask_sum,
|
||||
)))),
|
||||
),
|
||||
&keys,
|
||||
),
|
||||
algorithm.clone(),
|
||||
keys.clone(),
|
||||
algorithm_machines(&mut OsRng, algorithm, &keys),
|
||||
&[1; 32],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,25 +4,28 @@ use std::{
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
use group::ff::Field;
|
||||
use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint};
|
||||
use dalek_ff_group as dfg;
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
use frost::{
|
||||
curve::Ed25519,
|
||||
FrostError, ThresholdKeys,
|
||||
sign::{
|
||||
Writable, Preprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine,
|
||||
AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
|
||||
Writable, Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine,
|
||||
SignatureMachine, AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
random_scalar,
|
||||
ringct::{
|
||||
clsag::{ClsagInput, ClsagDetails, ClsagAddendum, ClsagMultisig},
|
||||
clsag::{ClsagInput, ClsagDetails, ClsagAddendum, ClsagMultisig, add_key_image_share},
|
||||
RctPrunable,
|
||||
},
|
||||
transaction::{Input, Transaction},
|
||||
@@ -34,11 +37,12 @@ use crate::{
|
||||
pub struct TransactionMachine {
|
||||
signable: SignableTransaction,
|
||||
i: u16,
|
||||
included: Vec<u16>,
|
||||
transcript: RecommendedTranscript,
|
||||
|
||||
decoys: Vec<Decoys>,
|
||||
|
||||
// Hashed key and scalar offset
|
||||
key_images: Vec<(EdwardsPoint, Scalar)>,
|
||||
inputs: Vec<Arc<RwLock<Option<ClsagDetails>>>>,
|
||||
clsags: Vec<AlgorithmMachine<Ed25519, ClsagMultisig>>,
|
||||
}
|
||||
@@ -46,11 +50,11 @@ pub struct TransactionMachine {
|
||||
pub struct TransactionSignMachine {
|
||||
signable: SignableTransaction,
|
||||
i: u16,
|
||||
included: Vec<u16>,
|
||||
transcript: RecommendedTranscript,
|
||||
|
||||
decoys: Vec<Decoys>,
|
||||
|
||||
key_images: Vec<(EdwardsPoint, Scalar)>,
|
||||
inputs: Vec<Arc<RwLock<Option<ClsagDetails>>>>,
|
||||
clsags: Vec<AlgorithmSignMachine<Ed25519, ClsagMultisig>>,
|
||||
|
||||
@@ -71,7 +75,6 @@ impl SignableTransaction {
|
||||
keys: ThresholdKeys<Ed25519>,
|
||||
mut transcript: RecommendedTranscript,
|
||||
height: usize,
|
||||
mut included: Vec<u16>,
|
||||
) -> Result<TransactionMachine, TransactionError> {
|
||||
let mut inputs = vec![];
|
||||
for _ in 0 .. self.inputs.len() {
|
||||
@@ -110,24 +113,20 @@ impl SignableTransaction {
|
||||
transcript.append_message(b"payment_amount", payment.1.to_le_bytes());
|
||||
}
|
||||
|
||||
// Sort included before cloning it around
|
||||
included.sort_unstable();
|
||||
|
||||
let mut key_images = vec![];
|
||||
for (i, input) in self.inputs.iter().enumerate() {
|
||||
// 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() {
|
||||
Err(TransactionError::WrongPrivateKey)?;
|
||||
}
|
||||
|
||||
clsags.push(
|
||||
AlgorithmMachine::new(
|
||||
ClsagMultisig::new(transcript.clone(), input.key(), inputs[i].clone()),
|
||||
offset,
|
||||
&included,
|
||||
)
|
||||
.map_err(TransactionError::FrostError)?,
|
||||
);
|
||||
let clsag = ClsagMultisig::new(transcript.clone(), input.key(), inputs[i].clone());
|
||||
key_images.push((
|
||||
clsag.H,
|
||||
keys.current_offset().unwrap_or(dfg::Scalar::zero()).0 + self.inputs[i].key_offset(),
|
||||
));
|
||||
clsags.push(AlgorithmMachine::new(clsag, offset).map_err(TransactionError::FrostError)?);
|
||||
}
|
||||
|
||||
// Select decoys
|
||||
@@ -150,11 +149,11 @@ impl SignableTransaction {
|
||||
Ok(TransactionMachine {
|
||||
signable: self,
|
||||
i: keys.params().i(),
|
||||
included,
|
||||
transcript,
|
||||
|
||||
decoys,
|
||||
|
||||
key_images,
|
||||
inputs,
|
||||
clsags,
|
||||
})
|
||||
@@ -196,11 +195,11 @@ impl PreprocessMachine for TransactionMachine {
|
||||
TransactionSignMachine {
|
||||
signable: self.signable,
|
||||
i: self.i,
|
||||
included: self.included,
|
||||
transcript: self.transcript,
|
||||
|
||||
decoys: self.decoys,
|
||||
|
||||
key_images: self.key_images,
|
||||
inputs: self.inputs,
|
||||
clsags,
|
||||
|
||||
@@ -212,10 +211,30 @@ impl PreprocessMachine for TransactionMachine {
|
||||
}
|
||||
|
||||
impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
type Params = ();
|
||||
type Keys = ThresholdKeys<Ed25519>;
|
||||
type Preprocess = Vec<Preprocess<Ed25519, ClsagAddendum>>;
|
||||
type SignatureShare = Vec<SignatureShare<Ed25519>>;
|
||||
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> {
|
||||
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
|
||||
let mut images = vec![EdwardsPoint::identity(); self.clsags.len()];
|
||||
let mut commitments = (0 .. self.clsags.len())
|
||||
.map(|c| {
|
||||
self
|
||||
.included
|
||||
included
|
||||
.iter()
|
||||
.map(|l| {
|
||||
// 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
|
||||
// images in its message), along with where the outputs are determined (where our
|
||||
// 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))
|
||||
})
|
||||
|
||||
@@ -216,14 +216,13 @@ macro_rules! test {
|
||||
keys[&i].clone(),
|
||||
RecommendedTranscript::new(b"Monero Serai Test Transaction"),
|
||||
rpc.get_height().await.unwrap() - 10,
|
||||
(1 ..= THRESHOLD).collect::<Vec<_>>(),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
frost::tests::sign(&mut OsRng, machines, &vec![])
|
||||
frost::tests::sign_without_caching(&mut OsRng, machines, &vec![])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user