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 as _, Scalar, EdwardsPoint};
10
11use transcript::{Transcript as _, RecommendedTranscript};
12use frost::{
13 curve::Ed25519,
14 Participant, FrostError, ThresholdKeys,
15 sign::{
16 Writable, Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine,
17 SignatureMachine, AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
18 },
19};
20
21use monero_oxide::{
22 ed25519::CompressedPoint,
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
31pub struct TransactionMachine {
33 signable: SignableTransaction,
34
35 keys: ThresholdKeys<Ed25519>,
36
37 key_image_generators_and_lincombs: Vec<(EdwardsPoint, (Scalar, Scalar))>,
39 clsags: Vec<(ClsagMultisigMaskSender, AlgorithmMachine<Ed25519, ClsagMultisig>)>,
40}
41
42pub struct TransactionSignMachine {
50 signable: SignableTransaction,
51
52 keys: ThresholdKeys<Ed25519>,
53
54 key_image_generators_and_lincombs: Vec<(EdwardsPoint, (Scalar, Scalar))>,
55 clsags: Vec<(ClsagMultisigMaskSender, AlgorithmSignMachine<Ed25519, ClsagMultisig>)>,
56
57 our_preprocess: Vec<Preprocess<Ed25519, ClsagAddendum>>,
58}
59
60pub struct TransactionSignatureMachine {
66 tx: Transaction,
67 clsags: Vec<AlgorithmSignatureMachine<Ed25519, ClsagMultisig>>,
68}
69
70impl SignableTransaction {
71 pub fn multisig(self, keys: ThresholdKeys<Ed25519>) -> Result<TransactionMachine, SendError> {
79 let mut clsags = vec![];
80
81 let mut key_image_generators_and_lincombs = vec![];
82 for input in &self.inputs {
83 let key_scalar = Scalar::ONE;
85 let key_offset = input.key_offset();
86
87 let offset = keys
88 .clone()
89 .scale(key_scalar)
90 .expect("non-zero scalar (1) was zero")
91 .offset(key_offset.into());
92 if offset.group_key().0 != input.key().into() {
93 Err(SendError::WrongPrivateKey)?;
94 }
95
96 let context = ClsagContext::new(input.decoys().clone(), input.commitment().clone())
97 .map_err(SendError::ClsagError)?;
98 let (clsag, clsag_mask_send) = ClsagMultisig::new(
99 RecommendedTranscript::new(b"Monero Multisignature Transaction"),
100 context,
101 );
102 key_image_generators_and_lincombs
103 .push((clsag.key_image_generator(), (offset.current_scalar(), offset.current_offset())));
104 clsags.push((clsag_mask_send, AlgorithmMachine::new(clsag, offset)));
105 }
106
107 Ok(TransactionMachine { signable: self, keys, key_image_generators_and_lincombs, clsags })
108 }
109}
110
111#[derive(Clone, PartialEq)]
115pub struct TransactionPreprocess(Vec<Preprocess<Ed25519, ClsagAddendum>>);
116impl Writable for TransactionPreprocess {
117 fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
118 for preprocess in &self.0 {
119 preprocess.write(writer)?;
120 }
121 Ok(())
122 }
123}
124
125impl PreprocessMachine for TransactionMachine {
126 type Preprocess = TransactionPreprocess;
127 type Signature = Transaction;
128 type SignMachine = TransactionSignMachine;
129
130 fn preprocess<R: RngCore + CryptoRng>(
131 mut self,
132 rng: &mut R,
133 ) -> (TransactionSignMachine, Self::Preprocess) {
134 let mut preprocesses = Vec::with_capacity(self.clsags.len());
136 let clsags = self
137 .clsags
138 .drain(..)
139 .map(|(clsag_mask_send, clsag)| {
140 let (clsag, preprocess) = clsag.preprocess(rng);
141 preprocesses.push(preprocess);
142 (clsag_mask_send, clsag)
143 })
144 .collect();
145 let our_preprocess = preprocesses.clone();
146
147 (
148 TransactionSignMachine {
149 signable: self.signable,
150
151 keys: self.keys,
152
153 key_image_generators_and_lincombs: self.key_image_generators_and_lincombs,
154 clsags,
155
156 our_preprocess,
157 },
158 TransactionPreprocess(preprocesses),
159 )
160 }
161}
162
163#[derive(Clone, PartialEq)]
167pub struct TransactionSignatureShare(Vec<SignatureShare<Ed25519>>);
168impl Writable for TransactionSignatureShare {
169 fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
170 for share in &self.0 {
171 share.write(writer)?;
172 }
173 Ok(())
174 }
175}
176
177impl SignMachine<Transaction> for TransactionSignMachine {
178 type Params = ();
179 type Keys = ThresholdKeys<Ed25519>;
180 type Preprocess = TransactionPreprocess;
181 type SignatureShare = TransactionSignatureShare;
182 type SignatureMachine = TransactionSignatureMachine;
183
184 fn cache(self) -> CachedPreprocess {
185 unimplemented!(
186 "Monero transactions don't support caching their preprocesses due to {}",
187 "being already bound to a specific transaction"
188 );
189 }
190
191 fn from_cache(
192 (): (),
193 _: ThresholdKeys<Ed25519>,
194 _: CachedPreprocess,
195 ) -> (Self, Self::Preprocess) {
196 unimplemented!(
197 "Monero transactions don't support caching their preprocesses due to {}",
198 "being already bound to a specific transaction"
199 );
200 }
201
202 fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
203 Ok(TransactionPreprocess(
204 self.clsags.iter().map(|clsag| clsag.1.read_preprocess(reader)).collect::<Result<_, _>>()?,
205 ))
206 }
207
208 fn sign(
209 self,
210 mut commitments: HashMap<Participant, Self::Preprocess>,
211 msg: &[u8],
212 ) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
213 assert!(
214 msg.is_empty(),
215 "message was passed to the TransactionMachine when it generates its own"
216 );
217
218 #[expect(clippy::iter_over_hash_type)]
219 for preprocess in commitments.values() {
220 if preprocess.0.len() != self.clsags.len() {
221 Err(FrostError::InternalError(
222 "preprocesses from another instance of the signing protocol were passed in",
223 ))?;
224 }
225 }
226
227 commitments.remove(&self.keys.params().i());
231
232 let mut included = commitments.keys().copied().collect::<Vec<_>>();
234 included.push(self.keys.params().i());
236 included.sort_unstable();
239
240 let mut key_images = vec![EdwardsPoint::identity(); self.clsags.len()];
242
243 let view = self.keys.view(included.clone()).map_err(|_| {
245 FrostError::InvalidSigningSet("couldn't form an interpolated view of the key")
246 })?;
247 let mut commitments = (0 .. self.clsags.len())
248 .map(|c| {
249 included
250 .iter()
251 .map(|l| {
252 let preprocess = if *l == self.keys.params().i() {
253 self.our_preprocess[c].clone()
254 } else {
255 commitments.get_mut(l).ok_or(FrostError::MissingParticipant(*l))?.0[c].clone()
256 };
257
258 key_images[c] += preprocess.addendum.key_image_share().0 *
261 view.interpolation_factor(*l).ok_or(FrostError::InternalError(
262 "view successfully formed with participant without an interpolation factor",
263 ))?;
264
265 Ok((*l, preprocess))
266 })
267 .collect::<Result<HashMap<_, _>, _>>()
268 })
269 .collect::<Result<Vec<_>, _>>()?;
270
271 let key_images: Vec<_> = key_images
272 .into_iter()
273 .zip(&self.key_image_generators_and_lincombs)
274 .map(|(mut key_image, (generator, (scalar, offset)))| {
275 key_image *= scalar;
276 key_image += generator * offset;
277 CompressedPoint::from(key_image.compress().to_bytes())
278 })
279 .collect();
280
281 for map in &mut commitments {
284 map.remove(&self.keys.params().i());
285 }
286
287 let mut clsags = Vec::with_capacity(self.clsags.len());
290 for ((key_image, clsag), commitments) in key_images.iter().zip(self.clsags).zip(commitments) {
291 clsags.push((key_image, clsag, commitments));
292 }
293 clsags.sort_by(|x, y| key_image_sort(x.0, y.0));
294 let clsags =
295 clsags.into_iter().map(|(_, clsag, commitments)| (clsag, commitments)).collect::<Vec<_>>();
296
297 let tx = self.signable.with_key_images(key_images);
299
300 let clsag_len = clsags.len();
302 let output_masks = tx.intent.sum_output_masks(&tx.key_images);
303 let mut rng = tx.intent.seeded_rng(b"multisig_pseudo_out_masks");
304 let mut sum_pseudo_outs = Scalar::ZERO;
305 let mut to_sign = Vec::with_capacity(clsag_len);
306 for (i, ((clsag_mask_send, clsag), commitments)) in clsags.into_iter().enumerate() {
307 let mut mask = monero_oxide::ed25519::Scalar::random(&mut rng).into();
308 if i == (clsag_len - 1) {
309 mask = output_masks.into() - sum_pseudo_outs;
310 } else {
311 sum_pseudo_outs += mask;
312 }
313 clsag_mask_send.send(mask);
314 to_sign.push((clsag, commitments));
315 }
316
317 let tx = tx.transaction_without_signatures();
318 let msg = tx.signature_hash().expect("signing a transaction which isn't signed?");
319
320 let mut shares = Vec::with_capacity(to_sign.len());
322 let clsags = to_sign
323 .drain(..)
324 .map(|(clsag, commitments)| {
325 let (clsag, share) = clsag.sign(commitments, &msg)?;
326 shares.push(share);
327 Ok(clsag)
328 })
329 .collect::<Result<_, _>>()?;
330
331 Ok((TransactionSignatureMachine { tx, clsags }, TransactionSignatureShare(shares)))
332 }
333}
334
335impl SignatureMachine<Transaction> for TransactionSignatureMachine {
336 type SignatureShare = TransactionSignatureShare;
337
338 fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
339 Ok(TransactionSignatureShare(
340 self.clsags.iter().map(|clsag| clsag.read_share(reader)).collect::<Result<_, _>>()?,
341 ))
342 }
343
344 fn complete(
345 mut self,
346 shares: HashMap<Participant, Self::SignatureShare>,
347 ) -> Result<Transaction, FrostError> {
348 #[expect(clippy::iter_over_hash_type)]
349 for share in shares.values() {
350 if share.0.len() != self.clsags.len() {
351 Err(FrostError::InternalError(
352 "signature shares from another instance of the signing protocol were passed in",
353 ))?;
354 }
355 }
356
357 let mut tx = self.tx;
358 #[expect(clippy::wildcard_enum_match_arm)]
359 match tx {
360 Transaction::V2 {
361 proofs:
362 Some(RctProofs {
363 prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. },
364 ..
365 }),
366 ..
367 } => {
368 for (c, clsag) in self.clsags.drain(..).enumerate() {
369 let (clsag, pseudo_out) = clsag.complete(
370 shares.iter().map(|(l, shares)| (*l, shares.0[c].clone())).collect::<HashMap<_, _>>(),
371 )?;
372 clsags.push(clsag);
373 pseudo_outs.push(CompressedPoint::from(pseudo_out.compress().to_bytes()));
374 }
375 }
376 _ => unreachable!("attempted to sign a multisig TX which wasn't CLSAG"),
377 }
378 Ok(tx)
379 }
380}