monero_wallet/send/
multisig.rs

1use std_shims::{
2  vec::Vec,
3  io::{self, Read},
4  collections::HashMap,
5};
6
7use rand_core::{RngCore, CryptoRng};
8
9use curve25519_dalek::{traits::Identity, Scalar, EdwardsPoint};
10use dalek_ff_group as dfg;
11
12use transcript::{Transcript, RecommendedTranscript};
13use frost::{
14  curve::Ed25519,
15  Participant, FrostError, ThresholdKeys,
16  sign::{
17    Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine,
18    AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
19  },
20};
21
22use monero_oxide::{
23  ringct::{
24    clsag::{ClsagContext, ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig},
25    RctPrunable, RctProofs,
26  },
27  transaction::Transaction,
28};
29use crate::send::{SendError, SignableTransaction, key_image_sort};
30
31/// Initial FROST machine to produce a signed transaction.
32pub struct TransactionMachine {
33  signable: SignableTransaction,
34
35  keys: ThresholdKeys<Ed25519>,
36
37  // The key image generator, and the (scalar, offset) linear combination from the spend key
38  key_image_generators_and_lincombs: Vec<(EdwardsPoint, (Scalar, Scalar))>,
39  clsags: Vec<(ClsagMultisigMaskSender, AlgorithmMachine<Ed25519, ClsagMultisig>)>,
40}
41
42/// Second FROST machine to produce a signed transaction.
43///
44/// Panics if a non-empty message is provided, or if `cache`, `from_cache` are called.
45pub struct TransactionSignMachine {
46  signable: SignableTransaction,
47
48  keys: ThresholdKeys<Ed25519>,
49
50  key_image_generators_and_lincombs: Vec<(EdwardsPoint, (Scalar, Scalar))>,
51  clsags: Vec<(ClsagMultisigMaskSender, AlgorithmSignMachine<Ed25519, ClsagMultisig>)>,
52
53  our_preprocess: Vec<Preprocess<Ed25519, ClsagAddendum>>,
54}
55
56/// Final FROST machine to produce a signed transaction.
57pub struct TransactionSignatureMachine {
58  tx: Transaction,
59  clsags: Vec<AlgorithmSignatureMachine<Ed25519, ClsagMultisig>>,
60}
61
62impl SignableTransaction {
63  /// Create a FROST signing machine out of this signable transaction.
64  ///
65  /// The created machine is expected to be called with an empty message, as it will generate its
66  /// own, and may panic if a message is provided. The created machine DOES NOT support caching and
67  /// may panic if `cache`, `from_cache` are called.
68  pub fn multisig(self, keys: ThresholdKeys<Ed25519>) -> Result<TransactionMachine, SendError> {
69    let mut clsags = vec![];
70
71    let mut key_image_generators_and_lincombs = vec![];
72    for input in &self.inputs {
73      // Check this is the right set of keys
74      let key_scalar = Scalar::ONE;
75      let key_offset = input.key_offset();
76
77      let offset = keys
78        .clone()
79        .scale(dfg::Scalar(key_scalar))
80        .expect("non-zero scalar (1) was zero")
81        .offset(dfg::Scalar(key_offset));
82      if offset.group_key().0 != input.key() {
83        Err(SendError::WrongPrivateKey)?;
84      }
85
86      let context = ClsagContext::new(input.decoys().clone(), input.commitment().clone())
87        .map_err(SendError::ClsagError)?;
88      let (clsag, clsag_mask_send) = ClsagMultisig::new(
89        RecommendedTranscript::new(b"Monero Multisignature Transaction"),
90        context,
91      );
92      key_image_generators_and_lincombs.push((
93        clsag.key_image_generator(),
94        (offset.current_scalar().0, offset.current_offset().0),
95      ));
96      clsags.push((clsag_mask_send, AlgorithmMachine::new(clsag, offset)));
97    }
98
99    Ok(TransactionMachine { signable: self, keys, key_image_generators_and_lincombs, clsags })
100  }
101}
102
103impl PreprocessMachine for TransactionMachine {
104  type Preprocess = Vec<Preprocess<Ed25519, ClsagAddendum>>;
105  type Signature = Transaction;
106  type SignMachine = TransactionSignMachine;
107
108  fn preprocess<R: RngCore + CryptoRng>(
109    mut self,
110    rng: &mut R,
111  ) -> (TransactionSignMachine, Self::Preprocess) {
112    // Iterate over each CLSAG calling preprocess
113    let mut preprocesses = Vec::with_capacity(self.clsags.len());
114    let clsags = self
115      .clsags
116      .drain(..)
117      .map(|(clsag_mask_send, clsag)| {
118        let (clsag, preprocess) = clsag.preprocess(rng);
119        preprocesses.push(preprocess);
120        (clsag_mask_send, clsag)
121      })
122      .collect();
123    let our_preprocess = preprocesses.clone();
124
125    (
126      TransactionSignMachine {
127        signable: self.signable,
128
129        keys: self.keys,
130
131        key_image_generators_and_lincombs: self.key_image_generators_and_lincombs,
132        clsags,
133
134        our_preprocess,
135      },
136      preprocesses,
137    )
138  }
139}
140
141impl SignMachine<Transaction> for TransactionSignMachine {
142  type Params = ();
143  type Keys = ThresholdKeys<Ed25519>;
144  type Preprocess = Vec<Preprocess<Ed25519, ClsagAddendum>>;
145  type SignatureShare = Vec<SignatureShare<Ed25519>>;
146  type SignatureMachine = TransactionSignatureMachine;
147
148  fn cache(self) -> CachedPreprocess {
149    unimplemented!(
150      "Monero transactions don't support caching their preprocesses due to {}",
151      "being already bound to a specific transaction"
152    );
153  }
154
155  fn from_cache(
156    (): (),
157    _: ThresholdKeys<Ed25519>,
158    _: CachedPreprocess,
159  ) -> (Self, Self::Preprocess) {
160    unimplemented!(
161      "Monero transactions don't support caching their preprocesses due to {}",
162      "being already bound to a specific transaction"
163    );
164  }
165
166  fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
167    self.clsags.iter().map(|clsag| clsag.1.read_preprocess(reader)).collect()
168  }
169
170  fn sign(
171    self,
172    mut commitments: HashMap<Participant, Self::Preprocess>,
173    msg: &[u8],
174  ) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
175    if !msg.is_empty() {
176      panic!("message was passed to the TransactionMachine when it generates its own");
177    }
178
179    // We do not need to be included here, yet this set of signers has yet to be validated
180    // We explicitly remove ourselves to ensure we aren't included twice, if we were redundantly
181    // included
182    commitments.remove(&self.keys.params().i());
183
184    // Find out who's included
185    let mut included = commitments.keys().copied().collect::<Vec<_>>();
186    // This push won't duplicate due to the above removal
187    included.push(self.keys.params().i());
188    // unstable sort may reorder elements of equal order
189    // Given our lack of duplicates, we should have no elements of equal order
190    included.sort_unstable();
191
192    // Start calculating the key images, as needed on the TX level
193    let mut key_images = vec![EdwardsPoint::identity(); self.clsags.len()];
194
195    // Convert the serialized nonces commitments to a parallelized Vec
196    let view = self.keys.view(included.clone()).map_err(|_| {
197      FrostError::InvalidSigningSet("couldn't form an interpolated view of the key")
198    })?;
199    let mut commitments = (0 .. self.clsags.len())
200      .map(|c| {
201        included
202          .iter()
203          .map(|l| {
204            let preprocess = if *l == self.keys.params().i() {
205              self.our_preprocess[c].clone()
206            } else {
207              commitments.get_mut(l).ok_or(FrostError::MissingParticipant(*l))?[c].clone()
208            };
209
210            // While here, calculate the key image as needed to call sign
211            // The CLSAG algorithm will independently calculate the key image/verify these shares
212            key_images[c] += preprocess.addendum.key_image_share().0 *
213              view
214                .interpolation_factor(*l)
215                .ok_or(FrostError::InternalError(
216                  "view successfully formed with participant without an interpolation factor",
217                ))?
218                .0;
219
220            Ok((*l, preprocess))
221          })
222          .collect::<Result<HashMap<_, _>, _>>()
223      })
224      .collect::<Result<Vec<_>, _>>()?;
225
226    for (key_image, (generator, (scalar, offset))) in
227      key_images.iter_mut().zip(&self.key_image_generators_and_lincombs)
228    {
229      *key_image *= scalar;
230      *key_image += generator * offset;
231    }
232
233    // The above inserted our own preprocess into these maps (which is unnecessary)
234    // Remove it now
235    for map in &mut commitments {
236      map.remove(&self.keys.params().i());
237    }
238
239    // The actual TX will have sorted its inputs by key image
240    // We apply the same sort now to our CLSAG machines
241    let mut clsags = Vec::with_capacity(self.clsags.len());
242    for ((key_image, clsag), commitments) in key_images.iter().zip(self.clsags).zip(commitments) {
243      clsags.push((key_image, clsag, commitments));
244    }
245    clsags.sort_by(|x, y| key_image_sort(x.0, y.0));
246    let clsags =
247      clsags.into_iter().map(|(_, clsag, commitments)| (clsag, commitments)).collect::<Vec<_>>();
248
249    // Specify the TX's key images
250    let tx = self.signable.with_key_images(key_images);
251
252    // We now need to decide the masks for each CLSAG
253    let clsag_len = clsags.len();
254    let output_masks = tx.intent.sum_output_masks(&tx.key_images);
255    let mut rng = tx.intent.seeded_rng(b"multisig_pseudo_out_masks");
256    let mut sum_pseudo_outs = Scalar::ZERO;
257    let mut to_sign = Vec::with_capacity(clsag_len);
258    for (i, ((clsag_mask_send, clsag), commitments)) in clsags.into_iter().enumerate() {
259      let mut mask = Scalar::random(&mut rng);
260      if i == (clsag_len - 1) {
261        mask = output_masks - sum_pseudo_outs;
262      } else {
263        sum_pseudo_outs += mask;
264      }
265      clsag_mask_send.send(mask);
266      to_sign.push((clsag, commitments));
267    }
268
269    let tx = tx.transaction_without_signatures();
270    let msg = tx.signature_hash().expect("signing a transaction which isn't signed?");
271
272    // Iterate over each CLSAG calling sign
273    let mut shares = Vec::with_capacity(to_sign.len());
274    let clsags = to_sign
275      .drain(..)
276      .map(|(clsag, commitments)| {
277        let (clsag, share) = clsag.sign(commitments, &msg)?;
278        shares.push(share);
279        Ok(clsag)
280      })
281      .collect::<Result<_, _>>()?;
282
283    Ok((TransactionSignatureMachine { tx, clsags }, shares))
284  }
285}
286
287impl SignatureMachine<Transaction> for TransactionSignatureMachine {
288  type SignatureShare = Vec<SignatureShare<Ed25519>>;
289
290  fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
291    self.clsags.iter().map(|clsag| clsag.read_share(reader)).collect()
292  }
293
294  fn complete(
295    mut self,
296    shares: HashMap<Participant, Self::SignatureShare>,
297  ) -> Result<Transaction, FrostError> {
298    let mut tx = self.tx;
299    match tx {
300      Transaction::V2 {
301        proofs:
302          Some(RctProofs {
303            prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. },
304            ..
305          }),
306        ..
307      } => {
308        for (c, clsag) in self.clsags.drain(..).enumerate() {
309          let (clsag, pseudo_out) = clsag.complete(
310            shares.iter().map(|(l, shares)| (*l, shares[c].clone())).collect::<HashMap<_, _>>(),
311          )?;
312          clsags.push(clsag);
313          pseudo_outs.push(pseudo_out);
314        }
315      }
316      _ => unreachable!("attempted to sign a multisig TX which wasn't CLSAG"),
317    }
318    Ok(tx)
319  }
320}