monero_clsag/
multisig.rs

1use core::{ops::Deref, fmt::Debug};
2use std_shims::{
3  sync::{Arc, Mutex},
4  io::{self, Read, Write},
5  collections::HashMap,
6};
7
8use rand_core::{RngCore, CryptoRng, SeedableRng};
9use rand_chacha::ChaCha20Rng;
10
11use zeroize::{Zeroize, Zeroizing};
12
13use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
14
15use group::{
16  ff::{Field, PrimeField},
17  Group, GroupEncoding,
18};
19
20use transcript::{Transcript, RecommendedTranscript};
21use dalek_ff_group as dfg;
22use frost::{
23  curve::Ed25519,
24  Participant, FrostError, ThresholdKeys, ThresholdView,
25  algorithm::{WriteAddendum, Algorithm},
26};
27
28use monero_generators::biased_hash_to_point;
29
30use crate::{ClsagContext, Clsag};
31
32impl ClsagContext {
33  fn transcript<T: Transcript>(&self, transcript: &mut T) {
34    // Doesn't domain separate as this is considered part of the larger CLSAG proof
35
36    // Ring index
37    transcript.append_message(b"signer_index", [self.decoys.signer_index()]);
38
39    // Ring
40    for (i, pair) in self.decoys.ring().iter().enumerate() {
41      // Doesn't include global output indexes as CLSAG doesn't care/won't be affected by it
42      // They're just a unreliable reference to this data which will be included in the message
43      // if somehow relevant
44      transcript.append_message(b"member", [u8::try_from(i).expect("ring size exceeded 255")]);
45      // This also transcripts the key image generator since it's derived from this key
46      transcript.append_message(b"key", pair[0].compress().to_bytes());
47      transcript.append_message(b"commitment", pair[1].compress().to_bytes())
48    }
49
50    // Doesn't include the commitment's parts as the above ring + index includes the commitment
51    // The only potential malleability would be if the G/H relationship is known, breaking the
52    // discrete log problem, which breaks everything already
53  }
54}
55
56/// A channel to send the mask to use for the pseudo-out (rerandomized commitment) with.
57///
58/// A mask must be sent along this channel before any preprocess addendums are handled.
59#[derive(Debug)]
60pub struct ClsagMultisigMaskSender {
61  buf: Arc<Mutex<Option<Scalar>>>,
62}
63#[derive(Debug)]
64struct ClsagMultisigMaskReceiver {
65  buf: Arc<Mutex<Option<Scalar>>>,
66}
67impl ClsagMultisigMaskSender {
68  fn new() -> (ClsagMultisigMaskSender, ClsagMultisigMaskReceiver) {
69    let buf = Arc::new(Mutex::new(None));
70    (ClsagMultisigMaskSender { buf: buf.clone() }, ClsagMultisigMaskReceiver { buf })
71  }
72
73  /// Send a mask to a CLSAG multisig instance.
74  pub fn send(self, mask: Scalar) {
75    // There is no risk this was prior set as this consumes `self`, which does not implement
76    // `Clone`
77    *self.buf.lock() = Some(mask);
78  }
79}
80impl ClsagMultisigMaskReceiver {
81  fn recv(self) -> Option<Scalar> {
82    *self.buf.lock()
83  }
84}
85
86/// Addendum produced during the signing process.
87#[derive(Clone, PartialEq, Eq, Zeroize, Debug)]
88pub struct ClsagAddendum {
89  key_image_share: dfg::EdwardsPoint,
90}
91
92impl ClsagAddendum {
93  /// The key image share within this addendum.
94  pub fn key_image_share(&self) -> dfg::EdwardsPoint {
95    self.key_image_share
96  }
97}
98
99impl WriteAddendum for ClsagAddendum {
100  fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
101    writer.write_all(self.key_image_share.compress().to_bytes().as_ref())
102  }
103}
104
105#[allow(non_snake_case)]
106#[derive(Clone, PartialEq, Eq, Debug)]
107struct Interim {
108  p: Scalar,
109  c: Scalar,
110
111  clsag: Clsag,
112  pseudo_out: EdwardsPoint,
113}
114
115/// FROST-inspired algorithm for producing a CLSAG signature.
116///
117/// Before this has its `process_addendum` called, a mask must be set. Before this has its
118/// `sign_share` called, all addendums (a non-zero amount) must be processed with
119/// `process_addendum`. Before `verify`, `verify_share` are called, `sign_share` must be called.
120/// Violation of this timeline is fundamentally incorrect and may cause panics.
121///
122/// The message signed is expected to be a 32-byte value. Per Monero, it's the keccak256 hash of
123/// the transaction data which is signed. This will panic if the message is not a 32-byte value.
124#[allow(non_snake_case)]
125#[derive(Debug)]
126pub struct ClsagMultisig {
127  transcript: RecommendedTranscript,
128
129  key_image_generator: EdwardsPoint,
130  key_image_shares: HashMap<[u8; 32], dfg::EdwardsPoint>,
131  image: dfg::EdwardsPoint,
132
133  context: ClsagContext,
134
135  mask_recv: Option<ClsagMultisigMaskReceiver>,
136  mask: Option<Scalar>,
137
138  msg_hash: Option<[u8; 32]>,
139  interim: Option<Interim>,
140}
141
142impl ClsagMultisig {
143  /// Construct a new instance of multisignature CLSAG signing.
144  pub fn new(
145    transcript: RecommendedTranscript,
146    context: ClsagContext,
147  ) -> (ClsagMultisig, ClsagMultisigMaskSender) {
148    let (mask_send, mask_recv) = ClsagMultisigMaskSender::new();
149    (
150      ClsagMultisig {
151        transcript,
152
153        key_image_generator: biased_hash_to_point(
154          context.decoys.signer_ring_members()[0].compress().0,
155        ),
156        key_image_shares: HashMap::new(),
157        image: dfg::EdwardsPoint::identity(),
158
159        context,
160
161        mask_recv: Some(mask_recv),
162        mask: None,
163
164        msg_hash: None,
165        interim: None,
166      },
167      mask_send,
168    )
169  }
170
171  /// The key image generator used by the signer.
172  pub fn key_image_generator(&self) -> EdwardsPoint {
173    self.key_image_generator
174  }
175}
176
177impl Algorithm<Ed25519> for ClsagMultisig {
178  type Transcript = RecommendedTranscript;
179  type Addendum = ClsagAddendum;
180  // We output the CLSAG and the key image, which requires an interactive protocol to obtain
181  type Signature = (Clsag, EdwardsPoint);
182
183  // We need the nonce represented against both G and the key image generator
184  fn nonces(&self) -> Vec<Vec<dfg::EdwardsPoint>> {
185    vec![vec![dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.key_image_generator)]]
186  }
187
188  // We also publish our share of the key image
189  fn preprocess_addendum<R: RngCore + CryptoRng>(
190    &mut self,
191    _rng: &mut R,
192    keys: &ThresholdKeys<Ed25519>,
193  ) -> ClsagAddendum {
194    ClsagAddendum {
195      key_image_share: dfg::EdwardsPoint(self.key_image_generator) *
196        keys.original_secret_share().deref(),
197    }
198  }
199
200  fn read_addendum<R: Read>(&self, reader: &mut R) -> io::Result<ClsagAddendum> {
201    let mut bytes = [0; 32];
202    reader.read_exact(&mut bytes)?;
203    // dfg ensures the point is torsion free
204    let xH = Option::<dfg::EdwardsPoint>::from(dfg::EdwardsPoint::from_bytes(&bytes))
205      .ok_or_else(|| io::Error::other("invalid key image"))?;
206    // Ensure this is a canonical point
207    if xH.to_bytes() != bytes {
208      Err(io::Error::other("non-canonical key image"))?;
209    }
210
211    Ok(ClsagAddendum { key_image_share: xH })
212  }
213
214  fn process_addendum(
215    &mut self,
216    view: &ThresholdView<Ed25519>,
217    l: Participant,
218    addendum: ClsagAddendum,
219  ) -> Result<(), FrostError> {
220    if let Some(mask_recv) = self.mask_recv.take() {
221      self.transcript.domain_separate(b"CLSAG");
222      // Transcript the ring
223      self.context.transcript(&mut self.transcript);
224      // Fetch the mask from the Mutex
225      // We set it to a variable to ensure our view of it is consistent
226      // It was this or a mpsc channel... std doesn't have oneshot :/
227      let mask = mask_recv
228        .recv()
229        .ok_or(FrostError::InternalError("CLSAG mask was not provided before process_addendum"))?;
230      self.mask = Some(mask);
231      // Transcript the mask
232      self.transcript.append_message(b"mask", mask.to_bytes());
233    }
234
235    // Transcript this participant's contribution
236    self.transcript.append_message(b"participant", l.to_bytes());
237    self
238      .transcript
239      .append_message(b"key_image_share", addendum.key_image_share.compress().to_bytes());
240
241    // Accumulate the interpolated share
242    let interpolated_key_image_share = addendum.key_image_share *
243      view
244        .interpolation_factor(l)
245        .ok_or(FrostError::InternalError("processing addendum for non-participant"))?;
246    self.image += interpolated_key_image_share;
247
248    self
249      .key_image_shares
250      .insert(view.verification_share(l).to_bytes(), interpolated_key_image_share);
251
252    Ok(())
253  }
254
255  fn transcript(&mut self) -> &mut Self::Transcript {
256    &mut self.transcript
257  }
258
259  fn sign_share(
260    &mut self,
261    view: &ThresholdView<Ed25519>,
262    nonce_sums: &[Vec<dfg::EdwardsPoint>],
263    nonces: Vec<Zeroizing<dfg::Scalar>>,
264    msg_hash: &[u8],
265  ) -> dfg::Scalar {
266    self.image =
267      (self.image * view.scalar()) + (dfg::EdwardsPoint(self.key_image_generator) * view.offset());
268
269    // Use the transcript to get a seeded random number generator
270    //
271    // The transcript contains private data, preventing passive adversaries from recreating this
272    // process even if they have access to the commitments/key image share broadcast so far
273    //
274    // Specifically, the transcript contains the signer's index within the ring, along with the
275    // opening of the commitment being re-randomized (and what it's re-randomized to)
276    let mut rng = ChaCha20Rng::from_seed(self.transcript.rng_seed(b"decoy_responses"));
277
278    let msg_hash = msg_hash.try_into().expect("CLSAG message hash should be 32-bytes");
279    self.msg_hash = Some(msg_hash);
280
281    let sign_core = Clsag::sign_core(
282      &mut rng,
283      &self.image,
284      &self.context,
285      self.mask.expect("mask wasn't set within process_addendum"),
286      &msg_hash,
287      nonce_sums[0][0].0,
288      nonce_sums[0][1].0,
289    );
290    self.interim = Some(Interim {
291      p: sign_core.key_challenge,
292      c: sign_core.challenged_mask,
293      clsag: sign_core.incomplete_clsag,
294      pseudo_out: sign_core.pseudo_out,
295    });
296
297    // r - p x, where p is the challenge for the keys
298    *nonces[0] - dfg::Scalar(sign_core.key_challenge) * view.secret_share().deref()
299  }
300
301  fn verify(
302    &self,
303    _: dfg::EdwardsPoint,
304    _: &[Vec<dfg::EdwardsPoint>],
305    sum: dfg::Scalar,
306  ) -> Option<Self::Signature> {
307    let interim = self.interim.as_ref().expect("verify called before sign_share");
308    let mut clsag = interim.clsag.clone();
309    // We produced shares as `r - p x`, yet the signature is actually `r - p x - c x`
310    // Substract `c x` (saved as `c`) now
311    clsag.s[usize::from(self.context.decoys.signer_index())] = sum.0 - interim.c;
312    if clsag
313      .verify(
314        self.context.decoys.ring(),
315        &self.image.0,
316        &interim.pseudo_out,
317        self.msg_hash.as_ref().expect("verify called before sign_share"),
318      )
319      .is_ok()
320    {
321      return Some((clsag, interim.pseudo_out));
322    }
323    None
324  }
325
326  fn verify_share(
327    &self,
328    verification_share: dfg::EdwardsPoint,
329    nonces: &[Vec<dfg::EdwardsPoint>],
330    share: dfg::Scalar,
331  ) -> Result<Vec<(dfg::Scalar, dfg::EdwardsPoint)>, ()> {
332    let interim = self.interim.as_ref().expect("verify_share called before sign_share");
333
334    // For a share `r - p x`, the following two equalities should hold:
335    // - `(r - p x)G == R.0 - pV`, where `V = xG`
336    // - `(r - p x)H == R.1 - pK`, where `K = xH` (the key image share)
337    //
338    // This is effectively a discrete log equality proof for:
339    // V, K over G, H
340    // with nonces
341    // R.0, R.1
342    // and solution
343    // s
344    //
345    // Which is a batch-verifiable rewrite of the traditional CP93 proof
346    // (and also writable as Generalized Schnorr Protocol)
347    //
348    // That means that given a proper challenge, this alone can be certainly argued to prove the
349    // key image share is well-formed and the provided signature so proves for that.
350
351    // This is a bit funky as it doesn't prove the nonces are well-formed however. They're part of
352    // the prover data/transcript for a CP93/GSP proof, not part of the statement. This practically
353    // is fine, for a variety of reasons (given a consistent `x`, a consistent `r` can be
354    // extracted, and the nonces as used in CLSAG are also part of its prover data/transcript).
355
356    let key_image_share = self.key_image_shares[&verification_share.to_bytes()];
357
358    // Hash every variable relevant here, using the hash output as the random weight
359    let mut weight_transcript =
360      RecommendedTranscript::new(b"monero-oxide v0.1 ClsagMultisig::verify_share");
361    weight_transcript.append_message(b"G", dfg::EdwardsPoint::generator().to_bytes());
362    weight_transcript.append_message(b"H", self.key_image_generator.to_bytes());
363    weight_transcript.append_message(b"xG", verification_share.to_bytes());
364    weight_transcript.append_message(b"xH", key_image_share.to_bytes());
365    weight_transcript.append_message(b"rG", nonces[0][0].to_bytes());
366    weight_transcript.append_message(b"rH", nonces[0][1].to_bytes());
367    weight_transcript.append_message(b"c", dfg::Scalar(interim.p).to_repr());
368    weight_transcript.append_message(b"s", share.to_repr());
369    let weight = weight_transcript.challenge(b"weight");
370    let weight = dfg::Scalar(Scalar::from_bytes_mod_order_wide(&weight.into()));
371
372    let part_one = vec![
373      (share, dfg::EdwardsPoint::generator()),
374      // -(R.0 - pV) == -R.0 + pV
375      (-dfg::Scalar::ONE, nonces[0][0]),
376      (dfg::Scalar(interim.p), verification_share),
377    ];
378
379    let mut part_two = vec![
380      (weight * share, dfg::EdwardsPoint(self.key_image_generator)),
381      // -(R.1 - pK) == -R.1 + pK
382      (-weight, nonces[0][1]),
383      (weight * dfg::Scalar(interim.p), key_image_share),
384    ];
385
386    let mut all = part_one;
387    all.append(&mut part_two);
388    Ok(all)
389  }
390}