monero_wallet/send/
mod.rs

1use core::{ops::Deref, fmt};
2use std_shims::{
3  io, vec,
4  vec::Vec,
5  string::{String, ToString},
6};
7
8use zeroize::{Zeroize, Zeroizing};
9
10use rand_core::{RngCore, CryptoRng};
11use rand::seq::SliceRandom;
12
13use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar};
14#[cfg(feature = "multisig")]
15use frost::FrostError;
16
17use crate::{
18  io::*,
19  generators::{MAX_BULLETPROOF_COMMITMENTS, biased_hash_to_point},
20  ringct::{
21    clsag::{ClsagError, ClsagContext, Clsag},
22    RctType, RctPrunable, RctProofs,
23  },
24  transaction::{INPUTS_UPPER_BOUND, Transaction},
25  address::{Network, SubaddressIndex, MoneroAddress},
26  extra::{MAX_ARBITRARY_DATA_SIZE, MAX_EXTRA_SIZE_BY_RELAY_RULE},
27  rpc::FeeRate,
28  ViewPair, GuaranteedViewPair, OutputWithDecoys,
29};
30
31mod tx_keys;
32pub use tx_keys::TransactionKeys;
33mod tx;
34mod eventuality;
35pub use eventuality::Eventuality;
36
37#[cfg(feature = "multisig")]
38mod multisig;
39#[cfg(feature = "multisig")]
40pub use multisig::{TransactionMachine, TransactionSignMachine, TransactionSignatureMachine};
41
42pub(crate) fn key_image_sort(x: &CompressedPoint, y: &CompressedPoint) -> core::cmp::Ordering {
43  x.cmp(y).reverse()
44}
45
46#[derive(Clone, PartialEq, Eq, Zeroize)]
47enum ChangeEnum {
48  AddressOnly(MoneroAddress),
49  Standard { view_pair: ViewPair, subaddress: Option<SubaddressIndex> },
50  Guaranteed { view_pair: GuaranteedViewPair, subaddress: Option<SubaddressIndex> },
51}
52
53impl fmt::Debug for ChangeEnum {
54  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
55    match self {
56      ChangeEnum::AddressOnly(addr) => {
57        f.debug_struct("ChangeEnum::AddressOnly").field("addr", &addr).finish()
58      }
59      ChangeEnum::Standard { subaddress, .. } => f
60        .debug_struct("ChangeEnum::Standard")
61        .field("subaddress", &subaddress)
62        .finish_non_exhaustive(),
63      ChangeEnum::Guaranteed { subaddress, .. } => f
64        .debug_struct("ChangeEnum::Guaranteed")
65        .field("subaddress", &subaddress)
66        .finish_non_exhaustive(),
67    }
68  }
69}
70
71/// Specification for a change output.
72#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
73pub struct Change(Option<ChangeEnum>);
74
75impl Change {
76  /// Create a change output specification.
77  ///
78  /// This take the view key as Monero assumes it has the view key for change outputs. It optimizes
79  /// its wallet protocol accordingly.
80  pub fn new(view_pair: ViewPair, subaddress: Option<SubaddressIndex>) -> Change {
81    Change(Some(ChangeEnum::Standard { view_pair, subaddress }))
82  }
83
84  /// Create a change output specification for a guaranteed view pair.
85  ///
86  /// This take the view key as Monero assumes it has the view key for change outputs. It optimizes
87  /// its wallet protocol accordingly.
88  pub fn guaranteed(view_pair: GuaranteedViewPair, subaddress: Option<SubaddressIndex>) -> Change {
89    Change(Some(ChangeEnum::Guaranteed { view_pair, subaddress }))
90  }
91
92  /// Create a fingerprintable change output specification.
93  ///
94  /// You MUST assume this will harm your privacy. Only use this if you know what you're doing.
95  ///
96  /// If the change address is Some, this will be unable to optimize the transaction as the
97  /// Monero wallet protocol expects it can (due to presumably having the view key for the change
98  /// output). If a transaction should be optimized, and isn'tm it will be fingerprintable.
99  ///
100  /// If the change address is None, there are two fingerprints:
101  ///
102  /// 1) The change in the TX is shunted to the fee (making it fingerprintable).
103  ///
104  /// 2) In two-output transactions, where the payment address doesn't have a payment ID, wallet2
105  ///    includes an encrypted dummy payment ID for the non-change output in order to not allow
106  ///    differentiating if transactions send to addresses with payment IDs or not. monero-wallet
107  ///    includes a dummy payment ID which at least one recipient will identify as not the expected
108  ///    dummy payment ID, revealing to the recipient(s) the sender is using non-wallet2 software.
109  pub fn fingerprintable(address: Option<MoneroAddress>) -> Change {
110    if let Some(address) = address {
111      Change(Some(ChangeEnum::AddressOnly(address)))
112    } else {
113      Change(None)
114    }
115  }
116}
117
118#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
119enum InternalPayment {
120  Payment(MoneroAddress, u64),
121  Change(ChangeEnum),
122}
123
124impl InternalPayment {
125  fn address(&self) -> MoneroAddress {
126    match self {
127      InternalPayment::Payment(addr, _) => *addr,
128      InternalPayment::Change(change) => match change {
129        ChangeEnum::AddressOnly(addr) => *addr,
130        // Network::Mainnet as the network won't effect the derivations
131        ChangeEnum::Standard { view_pair, subaddress } => match subaddress {
132          Some(subaddress) => view_pair.subaddress(Network::Mainnet, *subaddress),
133          None => view_pair.legacy_address(Network::Mainnet),
134        },
135        ChangeEnum::Guaranteed { view_pair, subaddress } => {
136          view_pair.address(Network::Mainnet, *subaddress, None)
137        }
138      },
139    }
140  }
141}
142
143/// An error while sending Monero.
144#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
145pub enum SendError {
146  /// The RingCT type to produce proofs for this transaction with weren't supported.
147  #[error("this library doesn't yet support that RctType")]
148  UnsupportedRctType,
149  /// The transaction had no inputs specified.
150  #[error("no inputs")]
151  NoInputs,
152  /// The decoy quantity was invalid for the specified RingCT type.
153  #[error("invalid number of decoys")]
154  InvalidDecoyQuantity,
155  /// The transaction had no outputs specified.
156  #[error("no outputs")]
157  NoOutputs,
158  /// The transaction had too many outputs specified.
159  #[error("too many outputs")]
160  TooManyOutputs,
161  /// The transaction did not have a change output, and did not have two outputs.
162  ///
163  /// Monero requires all transactions have at least two outputs, assuming one payment and one
164  /// change (or at least one dummy and one change). Accordingly, specifying no change and only
165  /// one payment prevents creating a valid transaction
166  #[error("only one output and no change address")]
167  NoChange,
168  /// Multiple addresses had payment IDs specified.
169  ///
170  /// Only one payment ID is allowed per transaction.
171  #[error("multiple addresses with payment IDs")]
172  MultiplePaymentIds,
173  /// Too much arbitrary data was specified.
174  #[error("too much data")]
175  TooMuchArbitraryData,
176  /// The created transaction was too large.
177  #[error("too large of a transaction")]
178  TooLargeTransaction,
179  /// The transactions' amounts could not be represented within a `u64`.
180  #[error("transaction amounts exceed u64::MAX (in {in_amount}, out {out_amount})")]
181  AmountsUnrepresentable {
182    /// The amount in (via inputs).
183    in_amount: u128,
184    /// The amount which would be out (between outputs and the fee).
185    out_amount: u128,
186  },
187  /// This transaction could not pay for itself.
188  #[error(
189    "not enough funds (inputs {inputs}, outputs {outputs}, necessary_fee {necessary_fee:?})"
190  )]
191  NotEnoughFunds {
192    /// The amount of funds the inputs contributed.
193    inputs: u64,
194    /// The amount of funds the outputs required.
195    outputs: u64,
196    /// The fee necessary to be paid on top.
197    ///
198    /// If this is None, it is because the fee was not calculated as the outputs alone caused this
199    /// error.
200    necessary_fee: Option<u64>,
201  },
202  /// This transaction is being signed with the wrong private key.
203  #[error("wrong spend private key")]
204  WrongPrivateKey,
205  /// This transaction was read from a bytestream which was malicious.
206  #[error("this SignableTransaction was created by deserializing a malicious serialization")]
207  MaliciousSerialization,
208  /// There was an error when working with the CLSAGs.
209  #[error("clsag error ({0})")]
210  ClsagError(ClsagError),
211  /// There was an error when working with FROST.
212  #[cfg(feature = "multisig")]
213  #[error("frost error {0}")]
214  FrostError(FrostError),
215}
216
217/// A signable transaction.
218#[derive(Clone, PartialEq, Eq, Zeroize)]
219pub struct SignableTransaction {
220  rct_type: RctType,
221  outgoing_view_key: Zeroizing<[u8; 32]>,
222  inputs: Vec<OutputWithDecoys>,
223  payments: Vec<InternalPayment>,
224  data: Vec<Vec<u8>>,
225  fee_rate: FeeRate,
226}
227
228impl fmt::Debug for SignableTransaction {
229  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
230    f.debug_struct("SignableTransaction")
231      .field("rct_type", &self.rct_type)
232      .field("inputs", &self.inputs)
233      .field("payments", &self.payments)
234      .field("data", &self.data)
235      .field("fee_rate", &self.fee_rate)
236      .finish_non_exhaustive()
237  }
238}
239
240struct SignableTransactionWithKeyImages {
241  intent: SignableTransaction,
242  key_images: Vec<CompressedPoint>,
243}
244
245impl SignableTransaction {
246  fn validate(&self) -> Result<(), SendError> {
247    match self.rct_type {
248      RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => {}
249      _ => Err(SendError::UnsupportedRctType)?,
250    }
251
252    if self.inputs.is_empty() {
253      Err(SendError::NoInputs)?;
254    }
255    for input in &self.inputs {
256      if input.decoys().len() !=
257        match self.rct_type {
258          RctType::ClsagBulletproof => 11,
259          RctType::ClsagBulletproofPlus => 16,
260          _ => panic!("unsupported RctType"),
261        }
262      {
263        Err(SendError::InvalidDecoyQuantity)?;
264      }
265    }
266
267    // Check we have at least one non-change output
268    if !self.payments.iter().any(|payment| matches!(payment, InternalPayment::Payment(_, _))) {
269      Err(SendError::NoOutputs)?;
270    }
271    // If we don't have at least two outputs, as required by Monero, error
272    if self.payments.len() < 2 {
273      Err(SendError::NoChange)?;
274    }
275    // Check we don't have multiple Change outputs due to decoding a malicious serialization
276    {
277      let mut change_count = 0;
278      for payment in &self.payments {
279        change_count += usize::from(u8::from(matches!(payment, InternalPayment::Change(_))));
280      }
281      if change_count > 1 {
282        Err(SendError::MaliciousSerialization)?;
283      }
284    }
285
286    // Make sure there's at most one payment ID
287    {
288      let mut payment_ids = 0;
289      for payment in &self.payments {
290        payment_ids += usize::from(u8::from(payment.address().payment_id().is_some()));
291      }
292      if payment_ids > 1 {
293        Err(SendError::MultiplePaymentIds)?;
294      }
295    }
296
297    if self.payments.len() > MAX_BULLETPROOF_COMMITMENTS {
298      Err(SendError::TooManyOutputs)?;
299    }
300
301    // Check the length of each arbitrary data
302    for part in &self.data {
303      if part.len() > MAX_ARBITRARY_DATA_SIZE {
304        Err(SendError::TooMuchArbitraryData)?;
305      }
306    }
307
308    // Check the length of TX extra
309    if self.extra().len() > MAX_EXTRA_SIZE_BY_RELAY_RULE {
310      Err(SendError::TooMuchArbitraryData)?;
311    }
312
313    // Make sure we have enough funds
314    let weight;
315    {
316      let in_amount: u128 =
317        self.inputs.iter().map(|input| u128::from(input.commitment().amount)).sum();
318      let payments_amount: u128 = self
319        .payments
320        .iter()
321        .filter_map(|payment| match payment {
322          InternalPayment::Payment(_, amount) => Some(u128::from(*amount)),
323          InternalPayment::Change(_) => None,
324        })
325        .sum();
326      let necessary_fee;
327      (weight, necessary_fee) = self.weight_and_necessary_fee();
328      let out_amount = payments_amount + u128::from(necessary_fee);
329      let in_out_amount = u64::try_from(in_amount)
330        .and_then(|in_amount| u64::try_from(out_amount).map(|out_amount| (in_amount, out_amount)));
331      let Ok((in_amount, out_amount)) = in_out_amount else {
332        Err(SendError::AmountsUnrepresentable { in_amount, out_amount })?
333      };
334      if in_amount < out_amount {
335        Err(SendError::NotEnoughFunds {
336          inputs: in_amount,
337          outputs: u64::try_from(payments_amount)
338            .expect("total out fit within u64 but not part of total out"),
339          necessary_fee: Some(necessary_fee),
340        })?;
341      }
342    }
343
344    // The limit is half the no-penalty block size
345    // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
346    //   /src/wallet/wallet2.cpp#L11076-L11085
347    // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
348    //   /src/cryptonote_config.h#L61
349    // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
350    //   /src/cryptonote_config.h#L64
351    const MAX_TX_SIZE: usize = (300_000 / 2) - 600;
352    if weight >= MAX_TX_SIZE {
353      Err(SendError::TooLargeTransaction)?;
354    }
355
356    Ok(())
357  }
358
359  /// Create a new SignableTransaction.
360  ///
361  /// `outgoing_view_key` is used to seed the RNGs for this transaction. Anyone with knowledge of
362  /// the outgoing view key will be able to identify a transaction produced with this methodology,
363  /// and the data within it. Accordingly, it must be treated as a private key.
364  ///
365  /// If one `outgoing_view_key` is reused across two transactions which share keys in their
366  /// inputs, such transactions being mutually incompatible with each other on that premise, some
367  /// ephemeral secrets MAY be reused causing adverse effects. Do NOT reuse an `outgoing_view_key`
368  /// across incompatible transactions accordingly.
369  ///
370  /// `data` represents arbitrary data which will be embedded into the transaction's `extra` field.
371  /// Please see `Extra::arbitrary_data` for the full impacts of this.
372  pub fn new(
373    rct_type: RctType,
374    outgoing_view_key: Zeroizing<[u8; 32]>,
375    inputs: Vec<OutputWithDecoys>,
376    payments: Vec<(MoneroAddress, u64)>,
377    change: Change,
378    data: Vec<Vec<u8>>,
379    fee_rate: FeeRate,
380  ) -> Result<SignableTransaction, SendError> {
381    // Re-format the payments and change into a consolidated payments list
382    let mut payments = payments
383      .into_iter()
384      .map(|(addr, amount)| InternalPayment::Payment(addr, amount))
385      .collect::<Vec<_>>();
386
387    if let Some(change) = change.0 {
388      payments.push(InternalPayment::Change(change));
389    }
390
391    let mut res =
392      SignableTransaction { rct_type, outgoing_view_key, inputs, payments, data, fee_rate };
393    res.validate()?;
394
395    // Shuffle the payments
396    {
397      let mut rng = res.seeded_rng(b"shuffle_payments");
398      res.payments.shuffle(&mut rng);
399    }
400
401    Ok(res)
402  }
403
404  /// The fee rate this transaction uses.
405  pub fn fee_rate(&self) -> FeeRate {
406    self.fee_rate
407  }
408
409  /// The fee this transaction requires.
410  ///
411  /// This is distinct from the fee this transaction will use. If no change output is specified,
412  /// all unspent coins will be shunted to the fee.
413  pub fn necessary_fee(&self) -> u64 {
414    self.weight_and_necessary_fee().1
415  }
416
417  /// Write a SignableTransaction.
418  ///
419  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
420  /// defined serialization.
421  pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
422    fn write_payment<W: io::Write>(payment: &InternalPayment, w: &mut W) -> io::Result<()> {
423      match payment {
424        InternalPayment::Payment(addr, amount) => {
425          w.write_all(&[0])?;
426          write_vec(write_byte, addr.to_string().as_bytes(), w)?;
427          w.write_all(&amount.to_le_bytes())
428        }
429        InternalPayment::Change(change) => match change {
430          ChangeEnum::AddressOnly(addr) => {
431            w.write_all(&[1])?;
432            write_vec(write_byte, addr.to_string().as_bytes(), w)
433          }
434          ChangeEnum::Standard { view_pair, subaddress } => {
435            w.write_all(&[2])?;
436            write_point(&view_pair.spend(), w)?;
437            write_scalar(&view_pair.view, w)?;
438            if let Some(subaddress) = subaddress {
439              w.write_all(&subaddress.account().to_le_bytes())?;
440              w.write_all(&subaddress.address().to_le_bytes())
441            } else {
442              w.write_all(&0u32.to_le_bytes())?;
443              w.write_all(&0u32.to_le_bytes())
444            }
445          }
446          ChangeEnum::Guaranteed { view_pair, subaddress } => {
447            w.write_all(&[3])?;
448            write_point(&view_pair.spend(), w)?;
449            write_scalar(&view_pair.0.view, w)?;
450            if let Some(subaddress) = subaddress {
451              w.write_all(&subaddress.account().to_le_bytes())?;
452              w.write_all(&subaddress.address().to_le_bytes())
453            } else {
454              w.write_all(&0u32.to_le_bytes())?;
455              w.write_all(&0u32.to_le_bytes())
456            }
457          }
458        },
459      }
460    }
461
462    write_byte(&u8::from(self.rct_type), w)?;
463    w.write_all(self.outgoing_view_key.as_slice())?;
464    write_vec(OutputWithDecoys::write, &self.inputs, w)?;
465    write_vec(write_payment, &self.payments, w)?;
466    write_vec(|data, w| write_vec(write_byte, data, w), &self.data, w)?;
467    self.fee_rate.write(w)
468  }
469
470  /// Serialize the SignableTransaction to a `Vec<u8>`.
471  ///
472  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
473  /// defined serialization.
474  pub fn serialize(&self) -> Vec<u8> {
475    let mut buf = Vec::with_capacity(256);
476    self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
477    buf
478  }
479
480  /// Read a `SignableTransaction`.
481  ///
482  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
483  /// defined serialization.
484  pub fn read<R: io::Read>(r: &mut R) -> io::Result<SignableTransaction> {
485    fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> {
486      /*
487        This uses featured addresses for a bound as they'll always be potentially larger than
488        traditional addresses.
489
490        https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789
491      */
492      const FEATURED_ADDRESS_DATA_SIZE_UPPER_BOUND: usize =
493        <u64 as VarInt>::UPPER_BOUND + 32 + 32 + <u64 as VarInt>::UPPER_BOUND + 8;
494      // Checksum
495      const FEATURED_ADDRESS_CHECKED_DATA_SIZE_UPPER_BOUND: usize =
496        FEATURED_ADDRESS_DATA_SIZE_UPPER_BOUND + 4;
497      // Base58-encoding, using 5 for `log2(58)`
498      const FEATURED_ADDRESS_ENCODED_SIZE_UPPER_BOUND: usize =
499        (FEATURED_ADDRESS_CHECKED_DATA_SIZE_UPPER_BOUND * 8).div_ceil(5);
500
501      /*
502        https://gist.github.com/tevador
503          /50160d160d24cfc6c52ae02eb3d17024?permalink_comment_id=4744307#gistcomment-4744307
504        notes one JAMTIS proposal which used 247 bytes, while most are 244 bytes. JAMTIS-RCT is
505        244 (https://gist.github.com/tevador/d3656a217c0177c160b9b6219d9ebb96).
506      */
507      const JAMTIS_ADDRESS_ENCODED_SIZE: usize = 247;
508
509      const fn const_max(a: usize, b: usize) -> usize {
510        if a > b {
511          a
512        } else {
513          b
514        }
515      }
516      const ADDRESS_ENCODED_SIZE_UPPER_BOUND: usize =
517        const_max(FEATURED_ADDRESS_ENCODED_SIZE_UPPER_BOUND, JAMTIS_ADDRESS_ENCODED_SIZE);
518
519      const ADDRESS_ENCODED_SIZE_SAFETY_FACTOR: usize = 2;
520      const ADDRESS_ENCODED_SIZE_BOUND: usize =
521        ADDRESS_ENCODED_SIZE_SAFETY_FACTOR * ADDRESS_ENCODED_SIZE_UPPER_BOUND;
522
523      String::from_utf8(read_vec(read_byte, Some(ADDRESS_ENCODED_SIZE_BOUND), r)?)
524        .ok()
525        .and_then(|str| MoneroAddress::from_str_with_unchecked_network(&str).ok())
526        .ok_or_else(|| io::Error::other("invalid address"))
527    }
528
529    fn read_payment<R: io::Read>(r: &mut R) -> io::Result<InternalPayment> {
530      Ok(match read_byte(r)? {
531        0 => InternalPayment::Payment(read_address(r)?, read_u64(r)?),
532        1 => InternalPayment::Change(ChangeEnum::AddressOnly(read_address(r)?)),
533        2 => InternalPayment::Change(ChangeEnum::Standard {
534          view_pair: ViewPair::new(read_point(r)?, Zeroizing::new(read_scalar(r)?))
535            .map_err(io::Error::other)?,
536          subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?),
537        }),
538        3 => InternalPayment::Change(ChangeEnum::Guaranteed {
539          view_pair: GuaranteedViewPair::new(read_point(r)?, Zeroizing::new(read_scalar(r)?))
540            .map_err(io::Error::other)?,
541          subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?),
542        }),
543        _ => Err(io::Error::other("invalid payment"))?,
544      })
545    }
546
547    let res = SignableTransaction {
548      rct_type: RctType::try_from(read_byte(r)?)
549        .map_err(|()| io::Error::other("unsupported/invalid RctType"))?,
550      outgoing_view_key: Zeroizing::new(read_bytes(r)?),
551      inputs: read_vec(OutputWithDecoys::read, Some(INPUTS_UPPER_BOUND), r)?,
552      payments: read_vec(read_payment, Some(MAX_BULLETPROOF_COMMITMENTS), r)?,
553      /*
554        This doesn't assert the _total_ length is `< MAX_EXTRA_SIZE_BY_RELAY_RULE`, yet the
555        following call to `validate` will.
556      */
557      data: read_vec(
558        |r| read_vec(read_byte, Some(MAX_ARBITRARY_DATA_SIZE), r),
559        Some(MAX_EXTRA_SIZE_BY_RELAY_RULE),
560        r,
561      )?,
562      fee_rate: FeeRate::read(r)?,
563    };
564    match res.validate() {
565      Ok(()) => {}
566      Err(e) => Err(io::Error::other(e))?,
567    }
568    Ok(res)
569  }
570
571  fn with_key_images(
572    mut self,
573    key_images: Vec<CompressedPoint>,
574  ) -> SignableTransactionWithKeyImages {
575    debug_assert_eq!(self.inputs.len(), key_images.len());
576
577    // Sort the inputs by their key images
578    let mut sorted_inputs = self.inputs.into_iter().zip(key_images).collect::<Vec<_>>();
579    sorted_inputs
580      .sort_by(|(_, key_image_a), (_, key_image_b)| key_image_sort(key_image_a, key_image_b));
581
582    self.inputs = Vec::with_capacity(sorted_inputs.len());
583    let mut key_images = Vec::with_capacity(sorted_inputs.len());
584    for (input, key_image) in sorted_inputs {
585      self.inputs.push(input);
586      key_images.push(key_image);
587    }
588
589    SignableTransactionWithKeyImages { intent: self, key_images }
590  }
591
592  /// Sign this transaction.
593  pub fn sign(
594    self,
595    rng: &mut (impl RngCore + CryptoRng),
596    sender_spend_key: &Zeroizing<Scalar>,
597  ) -> Result<Transaction, SendError> {
598    // Calculate the key images
599    let mut key_images = vec![];
600    for input in &self.inputs {
601      let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset());
602      if (input_key.deref() * ED25519_BASEPOINT_TABLE) != input.key() {
603        Err(SendError::WrongPrivateKey)?;
604      }
605      let key_image = input_key.deref() * biased_hash_to_point(input.key().compress().to_bytes());
606      key_images.push(CompressedPoint::from(key_image.compress()));
607    }
608
609    // Convert to a SignableTransactionWithKeyImages
610    let tx = self.with_key_images(key_images);
611
612    // Prepare the CLSAG signatures
613    let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len());
614    for input in &tx.intent.inputs {
615      // Re-derive the input key as this will be in a different order
616      let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset());
617      clsag_signs.push((
618        input_key,
619        ClsagContext::new(input.decoys().clone(), input.commitment().clone())
620          .map_err(SendError::ClsagError)?,
621      ));
622    }
623
624    // Get the output commitments' mask sum
625    let mask_sum = tx.intent.sum_output_masks(&tx.key_images);
626
627    // Get the actual TX, just needing the CLSAGs
628    let mut tx = tx.transaction_without_signatures();
629
630    // Sign the CLSAGs
631    let clsags_and_pseudo_outs = Clsag::sign(
632      rng,
633      clsag_signs,
634      mask_sum,
635      tx.signature_hash().expect("signing a transaction which isn't signed?"),
636    )
637    .map_err(SendError::ClsagError)?;
638
639    // Fill in the CLSAGs/pseudo-outs
640    let inputs_len = tx.prefix().inputs.len();
641    let Transaction::V2 {
642      proofs:
643        Some(RctProofs {
644          prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. },
645          ..
646        }),
647      ..
648    } = tx
649    else {
650      panic!("not signing clsag?")
651    };
652    *clsags = Vec::with_capacity(inputs_len);
653    *pseudo_outs = Vec::with_capacity(inputs_len);
654    for (clsag, pseudo_out) in clsags_and_pseudo_outs {
655      clsags.push(clsag);
656      pseudo_outs.push(CompressedPoint::from(pseudo_out.compress()));
657    }
658
659    // Return the signed TX
660    Ok(tx)
661  }
662}