modular_frost/tests/
mod.rs

1use std::collections::HashMap;
2
3use rand_core::{RngCore, CryptoRng};
4
5use ciphersuite::Ciphersuite;
6pub use dkg_recovery::recover_key;
7
8use crate::{
9  Curve, Participant, ThresholdKeys, FrostError,
10  algorithm::{Algorithm, Hram, IetfSchnorr},
11  sign::{Writable, PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine},
12};
13
14/// Tests for the nonce handling code.
15pub mod nonces;
16use nonces::test_multi_nonce;
17
18/// Vectorized test suite to ensure consistency.
19pub mod vectors;
20
21// Literal test definitions to run during `cargo test`
22#[cfg(test)]
23mod literal;
24
25/// Constant amount of participants to use when testing.
26pub const PARTICIPANTS: u16 = 5;
27/// Constant threshold of participants to use when signing.
28pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1;
29
30/// Create a key, for testing purposes.
31pub fn key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
32  rng: &mut R,
33) -> HashMap<Participant, ThresholdKeys<C>> {
34  let res = dkg_dealer::key_gen::<R, C>(rng, THRESHOLD, PARTICIPANTS).unwrap();
35  assert_eq!(
36    C::generator() * *recover_key(&res.values().cloned().collect::<Vec<_>>()).unwrap(),
37    res.values().next().unwrap().group_key()
38  );
39  res
40}
41
42/// Clone a map without a specific value.
43pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
44  map: &HashMap<K, V>,
45  without: &K,
46) -> HashMap<K, V> {
47  let mut res = map.clone();
48  res.remove(without).unwrap();
49  res
50}
51
52/// Spawn algorithm machines for a random selection of signers, each executing the given algorithm.
53pub fn algorithm_machines_without_clone<R: RngCore, C: Curve, A: Algorithm<C>>(
54  rng: &mut R,
55  keys: &HashMap<Participant, ThresholdKeys<C>>,
56  machines: HashMap<Participant, AlgorithmMachine<C, A>>,
57) -> HashMap<Participant, AlgorithmMachine<C, A>> {
58  let mut included = vec![];
59  while included.len() < usize::from(keys[&Participant::new(1).unwrap()].params().t()) {
60    let n = Participant::new(
61      u16::try_from((rng.next_u64() % u64::try_from(keys.len()).unwrap()) + 1).unwrap(),
62    )
63    .unwrap();
64    if included.contains(&n) {
65      continue;
66    }
67    included.push(n);
68  }
69
70  machines
71    .into_iter()
72    .filter_map(|(i, machine)| if included.contains(&i) { Some((i, machine)) } else { None })
73    .collect()
74}
75
76/// Spawn algorithm machines for a random selection of signers, each executing the given algorithm.
77pub fn algorithm_machines<R: RngCore, C: Curve, A: Clone + Algorithm<C>>(
78  rng: &mut R,
79  algorithm: &A,
80  keys: &HashMap<Participant, ThresholdKeys<C>>,
81) -> HashMap<Participant, AlgorithmMachine<C, A>> {
82  algorithm_machines_without_clone(
83    rng,
84    keys,
85    keys
86      .values()
87      .map(|keys| (keys.params().i(), AlgorithmMachine::new(algorithm.clone(), keys.clone())))
88      .collect(),
89  )
90}
91
92// Run the preprocess step
93pub(crate) fn preprocess<
94  R: RngCore + CryptoRng,
95  M: PreprocessMachine,
96  F: FnMut(&mut R, &mut HashMap<Participant, M::SignMachine>),
97>(
98  rng: &mut R,
99  mut machines: HashMap<Participant, M>,
100  mut cache: F,
101) -> (HashMap<Participant, M::SignMachine>, HashMap<Participant, M::Preprocess>) {
102  let mut commitments = HashMap::new();
103  let mut machines = machines
104    .drain()
105    .map(|(i, machine)| {
106      let (machine, preprocess) = machine.preprocess(rng);
107      commitments.insert(i, {
108        let mut buf = vec![];
109        preprocess.write(&mut buf).unwrap();
110        machine.read_preprocess::<&[u8]>(&mut buf.as_ref()).unwrap()
111      });
112      (i, machine)
113    })
114    .collect::<HashMap<_, _>>();
115
116  cache(rng, &mut machines);
117
118  (machines, commitments)
119}
120
121// Run the preprocess and generate signature shares
122#[allow(clippy::type_complexity)]
123pub(crate) fn preprocess_and_shares<
124  R: RngCore + CryptoRng,
125  M: PreprocessMachine,
126  F: FnMut(&mut R, &mut HashMap<Participant, M::SignMachine>),
127>(
128  rng: &mut R,
129  machines: HashMap<Participant, M>,
130  cache: F,
131  msg: &[u8],
132) -> (
133  HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::SignatureMachine>,
134  HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::SignatureShare>,
135) {
136  let (mut machines, commitments) = preprocess(rng, machines, cache);
137
138  let mut shares = HashMap::new();
139  let machines = machines
140    .drain()
141    .map(|(i, machine)| {
142      let (machine, share) = machine.sign(clone_without(&commitments, &i), msg).unwrap();
143      shares.insert(i, {
144        let mut buf = vec![];
145        share.write(&mut buf).unwrap();
146        machine.read_share::<&[u8]>(&mut buf.as_ref()).unwrap()
147      });
148      (i, machine)
149    })
150    .collect::<HashMap<_, _>>();
151
152  (machines, shares)
153}
154
155fn sign_internal<
156  R: RngCore + CryptoRng,
157  M: PreprocessMachine,
158  F: FnMut(&mut R, &mut HashMap<Participant, M::SignMachine>),
159>(
160  rng: &mut R,
161  machines: HashMap<Participant, M>,
162  cache: F,
163  msg: &[u8],
164) -> M::Signature {
165  let (mut machines, shares) = preprocess_and_shares(rng, machines, cache, msg);
166
167  let mut signature = None;
168  for (i, machine) in machines.drain() {
169    let sig = machine.complete(clone_without(&shares, &i)).unwrap();
170    if signature.is_none() {
171      signature = Some(sig.clone());
172    }
173    assert_eq!(&sig, signature.as_ref().unwrap());
174  }
175  signature.unwrap()
176}
177
178/// Execute the signing protocol, without caching any machines. This isn't as comprehensive at
179/// testing as sign, and accordingly isn't preferred, yet is usable for machines not supporting
180/// caching.
181pub fn sign_without_caching<R: RngCore + CryptoRng, M: PreprocessMachine>(
182  rng: &mut R,
183  machines: HashMap<Participant, M>,
184  msg: &[u8],
185) -> M::Signature {
186  sign_internal(rng, machines, |_, _| {}, msg)
187}
188
189/// Execute the signing protocol, randomly caching various machines to ensure they can cache
190/// successfully.
191pub fn sign_without_clone<R: RngCore + CryptoRng, M: PreprocessMachine>(
192  rng: &mut R,
193  mut keys: HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::Keys>,
194  mut params: HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::Params>,
195  machines: HashMap<Participant, M>,
196  msg: &[u8],
197) -> M::Signature {
198  sign_internal(
199    rng,
200    machines,
201    |rng, machines| {
202      // Cache and rebuild half of the machines
203      let included = machines.keys().copied().collect::<Vec<_>>();
204      for i in included {
205        if (rng.next_u64() % 2) == 0 {
206          let cache = machines.remove(&i).unwrap().cache();
207          machines.insert(
208            i,
209            M::SignMachine::from_cache(params.remove(&i).unwrap(), keys.remove(&i).unwrap(), cache)
210              .0,
211          );
212        }
213      }
214    },
215    msg,
216  )
217}
218
219/// Execute the signing protocol, randomly caching various machines to ensure they can cache
220/// successfully.
221pub fn sign<
222  R: RngCore + CryptoRng,
223  M: PreprocessMachine<SignMachine: SignMachine<M::Signature, Params: Clone>>,
224>(
225  rng: &mut R,
226  params: &<M::SignMachine as SignMachine<M::Signature>>::Params,
227  keys: HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::Keys>,
228  machines: HashMap<Participant, M>,
229  msg: &[u8],
230) -> M::Signature {
231  let params = keys.keys().map(|i| (*i, params.clone())).collect();
232  sign_without_clone(rng, keys, params, machines, msg)
233}
234
235/// Test a basic Schnorr signature with the provided keys.
236pub fn test_schnorr_with_keys<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
237  rng: &mut R,
238  keys: &HashMap<Participant, ThresholdKeys<C>>,
239) {
240  const MSG: &[u8] = b"Hello, World!";
241
242  let machines = algorithm_machines(&mut *rng, &IetfSchnorr::<C, H>::ietf(), keys);
243  let sig = sign(&mut *rng, &IetfSchnorr::<C, H>::ietf(), keys.clone(), machines, MSG);
244  let group_key = keys[&Participant::new(1).unwrap()].group_key();
245  assert!(sig.verify(group_key, H::hram(&sig.R, &group_key, MSG)));
246}
247
248/// Test a basic Schnorr signature.
249pub fn test_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
250  let keys = key_gen(&mut *rng);
251  test_schnorr_with_keys::<_, _, H>(&mut *rng, &keys)
252}
253
254/// Test an offset Schnorr signature.
255pub fn test_offset_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
256  const MSG: &[u8] = b"Hello, World!";
257
258  let mut keys = key_gen(&mut *rng);
259  let group_key = keys[&Participant::new(1).unwrap()].group_key();
260
261  let scalar = C::F::from(3);
262  let offset = C::F::from(5);
263  let offset_key = (group_key * scalar) + (C::generator() * offset);
264  for keys in keys.values_mut() {
265    *keys = keys.clone().scale(scalar).unwrap().offset(offset);
266    assert_eq!(keys.group_key(), offset_key);
267  }
268
269  let machines = algorithm_machines(&mut *rng, &IetfSchnorr::<C, H>::ietf(), &keys);
270  let sig = sign(&mut *rng, &IetfSchnorr::<C, H>::ietf(), keys.clone(), machines, MSG);
271  let group_key = keys[&Participant::new(1).unwrap()].group_key();
272  assert!(sig.verify(offset_key, H::hram(&sig.R, &group_key, MSG)));
273}
274
275/// Test blame for an invalid Schnorr signature share.
276pub fn test_schnorr_blame<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
277  const MSG: &[u8] = b"Hello, World!";
278
279  let keys = key_gen(&mut *rng);
280  let machines = algorithm_machines(&mut *rng, &IetfSchnorr::<C, H>::ietf(), &keys);
281
282  let (mut machines, shares) = preprocess_and_shares(&mut *rng, machines, |_, _| {}, MSG);
283
284  for (i, machine) in machines.drain() {
285    let mut shares = clone_without(&shares, &i);
286
287    // Select a random participant to give an invalid share
288    let participants = shares.keys().collect::<Vec<_>>();
289    let faulty = *participants
290      [usize::try_from(rng.next_u64() % u64::try_from(participants.len()).unwrap()).unwrap()];
291    shares.get_mut(&faulty).unwrap().invalidate();
292
293    assert_eq!(machine.complete(shares).err(), Some(FrostError::InvalidShare(faulty)));
294  }
295}
296
297/// Run a variety of tests against a ciphersuite.
298pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
299  test_schnorr::<R, C, H>(rng);
300  test_offset_schnorr::<R, C, H>(rng);
301  test_schnorr_blame::<R, C, H>(rng);
302
303  test_multi_nonce::<R, C>(rng);
304}