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, EdwardsPoint};
14#[cfg(feature = "multisig")]
15use frost::FrostError;
16
17use crate::{
18  io::*,
19  generators::{MAX_COMMITMENTS, hash_to_point},
20  ringct::{
21    clsag::{ClsagError, ClsagContext, Clsag},
22    RctType, RctPrunable, RctProofs,
23  },
24  transaction::Transaction,
25  address::{Network, SubaddressIndex, MoneroAddress},
26  extra::MAX_ARBITRARY_DATA_SIZE,
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: &EdwardsPoint, y: &EdwardsPoint) -> core::cmp::Ordering {
43  x.compress().to_bytes().cmp(&y.compress().to_bytes()).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, Debug, 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
228struct SignableTransactionWithKeyImages {
229  intent: SignableTransaction,
230  key_images: Vec<EdwardsPoint>,
231}
232
233impl SignableTransaction {
234  fn validate(&self) -> Result<(), SendError> {
235    match self.rct_type {
236      RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => {}
237      _ => Err(SendError::UnsupportedRctType)?,
238    }
239
240    if self.inputs.is_empty() {
241      Err(SendError::NoInputs)?;
242    }
243    for input in &self.inputs {
244      if input.decoys().len() !=
245        match self.rct_type {
246          RctType::ClsagBulletproof => 11,
247          RctType::ClsagBulletproofPlus => 16,
248          _ => panic!("unsupported RctType"),
249        }
250      {
251        Err(SendError::InvalidDecoyQuantity)?;
252      }
253    }
254
255    // Check we have at least one non-change output
256    if !self.payments.iter().any(|payment| matches!(payment, InternalPayment::Payment(_, _))) {
257      Err(SendError::NoOutputs)?;
258    }
259    // If we don't have at least two outputs, as required by Monero, error
260    if self.payments.len() < 2 {
261      Err(SendError::NoChange)?;
262    }
263    // Check we don't have multiple Change outputs due to decoding a malicious serialization
264    {
265      let mut change_count = 0;
266      for payment in &self.payments {
267        change_count += usize::from(u8::from(matches!(payment, InternalPayment::Change(_))));
268      }
269      if change_count > 1 {
270        Err(SendError::MaliciousSerialization)?;
271      }
272    }
273
274    // Make sure there's at most one payment ID
275    {
276      let mut payment_ids = 0;
277      for payment in &self.payments {
278        payment_ids += usize::from(u8::from(payment.address().payment_id().is_some()));
279      }
280      if payment_ids > 1 {
281        Err(SendError::MultiplePaymentIds)?;
282      }
283    }
284
285    if self.payments.len() > MAX_COMMITMENTS {
286      Err(SendError::TooManyOutputs)?;
287    }
288
289    // Check the length of each arbitrary data
290    for part in &self.data {
291      if part.len() > MAX_ARBITRARY_DATA_SIZE {
292        Err(SendError::TooMuchArbitraryData)?;
293      }
294    }
295
296    // Check the length of TX extra
297    // https://github.com/monero-project/monero/pull/8733
298    const MAX_EXTRA_SIZE: usize = 1060;
299    if self.extra().len() > MAX_EXTRA_SIZE {
300      Err(SendError::TooMuchArbitraryData)?;
301    }
302
303    // Make sure we have enough funds
304    let weight;
305    {
306      let in_amount: u128 =
307        self.inputs.iter().map(|input| u128::from(input.commitment().amount)).sum();
308      let payments_amount: u128 = self
309        .payments
310        .iter()
311        .filter_map(|payment| match payment {
312          InternalPayment::Payment(_, amount) => Some(u128::from(*amount)),
313          InternalPayment::Change(_) => None,
314        })
315        .sum();
316      let necessary_fee;
317      (weight, necessary_fee) = self.weight_and_necessary_fee();
318      let out_amount = payments_amount + u128::from(necessary_fee);
319      let in_out_amount = u64::try_from(in_amount)
320        .and_then(|in_amount| u64::try_from(out_amount).map(|out_amount| (in_amount, out_amount)));
321      let Ok((in_amount, out_amount)) = in_out_amount else {
322        Err(SendError::AmountsUnrepresentable { in_amount, out_amount })?
323      };
324      if in_amount < out_amount {
325        Err(SendError::NotEnoughFunds {
326          inputs: in_amount,
327          outputs: u64::try_from(payments_amount)
328            .expect("total out fit within u64 but not part of total out"),
329          necessary_fee: Some(necessary_fee),
330        })?;
331      }
332    }
333
334    // The limit is half the no-penalty block size
335    // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
336    //   /src/wallet/wallet2.cpp#L11076-L11085
337    // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
338    //   /src/cryptonote_config.h#L61
339    // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
340    //   /src/cryptonote_config.h#L64
341    const MAX_TX_SIZE: usize = (300_000 / 2) - 600;
342    if weight >= MAX_TX_SIZE {
343      Err(SendError::TooLargeTransaction)?;
344    }
345
346    Ok(())
347  }
348
349  /// Create a new SignableTransaction.
350  ///
351  /// `outgoing_view_key` is used to seed the RNGs for this transaction. Anyone with knowledge of
352  /// the outgoing view key will be able to identify a transaction produced with this methodology,
353  /// and the data within it. Accordingly, it must be treated as a private key.
354  ///
355  /// `data` represents arbitrary data which will be embedded into the transaction's `extra` field.
356  /// The embedding occurs using an `ExtraField::Nonce` with a custom marker byte (as to not
357  /// conflict with a payment ID).
358  pub fn new(
359    rct_type: RctType,
360    outgoing_view_key: Zeroizing<[u8; 32]>,
361    inputs: Vec<OutputWithDecoys>,
362    payments: Vec<(MoneroAddress, u64)>,
363    change: Change,
364    data: Vec<Vec<u8>>,
365    fee_rate: FeeRate,
366  ) -> Result<SignableTransaction, SendError> {
367    // Re-format the payments and change into a consolidated payments list
368    let mut payments = payments
369      .into_iter()
370      .map(|(addr, amount)| InternalPayment::Payment(addr, amount))
371      .collect::<Vec<_>>();
372
373    if let Some(change) = change.0 {
374      payments.push(InternalPayment::Change(change));
375    }
376
377    let mut res =
378      SignableTransaction { rct_type, outgoing_view_key, inputs, payments, data, fee_rate };
379    res.validate()?;
380
381    // Shuffle the payments
382    {
383      let mut rng = res.seeded_rng(b"shuffle_payments");
384      res.payments.shuffle(&mut rng);
385    }
386
387    Ok(res)
388  }
389
390  /// The fee rate this transaction uses.
391  pub fn fee_rate(&self) -> FeeRate {
392    self.fee_rate
393  }
394
395  /// The fee this transaction requires.
396  ///
397  /// This is distinct from the fee this transaction will use. If no change output is specified,
398  /// all unspent coins will be shunted to the fee.
399  pub fn necessary_fee(&self) -> u64 {
400    self.weight_and_necessary_fee().1
401  }
402
403  /// Write a SignableTransaction.
404  ///
405  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
406  /// defined serialization.
407  pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
408    fn write_payment<W: io::Write>(payment: &InternalPayment, w: &mut W) -> io::Result<()> {
409      match payment {
410        InternalPayment::Payment(addr, amount) => {
411          w.write_all(&[0])?;
412          write_vec(write_byte, addr.to_string().as_bytes(), w)?;
413          w.write_all(&amount.to_le_bytes())
414        }
415        InternalPayment::Change(change) => match change {
416          ChangeEnum::AddressOnly(addr) => {
417            w.write_all(&[1])?;
418            write_vec(write_byte, addr.to_string().as_bytes(), w)
419          }
420          ChangeEnum::Standard { view_pair, subaddress } => {
421            w.write_all(&[2])?;
422            write_point(&view_pair.spend(), w)?;
423            write_scalar(&view_pair.view, w)?;
424            if let Some(subaddress) = subaddress {
425              w.write_all(&subaddress.account().to_le_bytes())?;
426              w.write_all(&subaddress.address().to_le_bytes())
427            } else {
428              w.write_all(&0u32.to_le_bytes())?;
429              w.write_all(&0u32.to_le_bytes())
430            }
431          }
432          ChangeEnum::Guaranteed { view_pair, subaddress } => {
433            w.write_all(&[3])?;
434            write_point(&view_pair.spend(), w)?;
435            write_scalar(&view_pair.0.view, w)?;
436            if let Some(subaddress) = subaddress {
437              w.write_all(&subaddress.account().to_le_bytes())?;
438              w.write_all(&subaddress.address().to_le_bytes())
439            } else {
440              w.write_all(&0u32.to_le_bytes())?;
441              w.write_all(&0u32.to_le_bytes())
442            }
443          }
444        },
445      }
446    }
447
448    write_byte(&u8::from(self.rct_type), w)?;
449    w.write_all(self.outgoing_view_key.as_slice())?;
450    write_vec(OutputWithDecoys::write, &self.inputs, w)?;
451    write_vec(write_payment, &self.payments, w)?;
452    write_vec(|data, w| write_vec(write_byte, data, w), &self.data, w)?;
453    self.fee_rate.write(w)
454  }
455
456  /// Serialize the SignableTransaction to a `Vec<u8>`.
457  ///
458  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
459  /// defined serialization.
460  pub fn serialize(&self) -> Vec<u8> {
461    let mut buf = Vec::with_capacity(256);
462    self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
463    buf
464  }
465
466  /// Read a `SignableTransaction`.
467  ///
468  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
469  /// defined serialization.
470  pub fn read<R: io::Read>(r: &mut R) -> io::Result<SignableTransaction> {
471    fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> {
472      String::from_utf8(read_vec(read_byte, None, r)?)
473        .ok()
474        .and_then(|str| MoneroAddress::from_str_with_unchecked_network(&str).ok())
475        .ok_or_else(|| io::Error::other("invalid address"))
476    }
477
478    fn read_payment<R: io::Read>(r: &mut R) -> io::Result<InternalPayment> {
479      Ok(match read_byte(r)? {
480        0 => InternalPayment::Payment(read_address(r)?, read_u64(r)?),
481        1 => InternalPayment::Change(ChangeEnum::AddressOnly(read_address(r)?)),
482        2 => InternalPayment::Change(ChangeEnum::Standard {
483          view_pair: ViewPair::new(read_point(r)?, Zeroizing::new(read_scalar(r)?))
484            .map_err(io::Error::other)?,
485          subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?),
486        }),
487        3 => InternalPayment::Change(ChangeEnum::Guaranteed {
488          view_pair: GuaranteedViewPair::new(read_point(r)?, Zeroizing::new(read_scalar(r)?))
489            .map_err(io::Error::other)?,
490          subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?),
491        }),
492        _ => Err(io::Error::other("invalid payment"))?,
493      })
494    }
495
496    let res = SignableTransaction {
497      rct_type: RctType::try_from(read_byte(r)?)
498        .map_err(|()| io::Error::other("unsupported/invalid RctType"))?,
499      outgoing_view_key: Zeroizing::new(read_bytes(r)?),
500      inputs: read_vec(OutputWithDecoys::read, None, r)?,
501      payments: read_vec(read_payment, None, r)?,
502      data: read_vec(|r| read_vec(read_byte, None, r), None, r)?,
503      fee_rate: FeeRate::read(r)?,
504    };
505    match res.validate() {
506      Ok(()) => {}
507      Err(e) => Err(io::Error::other(e))?,
508    }
509    Ok(res)
510  }
511
512  fn with_key_images(mut self, key_images: Vec<EdwardsPoint>) -> SignableTransactionWithKeyImages {
513    debug_assert_eq!(self.inputs.len(), key_images.len());
514
515    // Sort the inputs by their key images
516    let mut sorted_inputs = self.inputs.into_iter().zip(key_images).collect::<Vec<_>>();
517    sorted_inputs
518      .sort_by(|(_, key_image_a), (_, key_image_b)| key_image_sort(key_image_a, key_image_b));
519
520    self.inputs = Vec::with_capacity(sorted_inputs.len());
521    let mut key_images = Vec::with_capacity(sorted_inputs.len());
522    for (input, key_image) in sorted_inputs {
523      self.inputs.push(input);
524      key_images.push(key_image);
525    }
526
527    SignableTransactionWithKeyImages { intent: self, key_images }
528  }
529
530  /// Sign this transaction.
531  pub fn sign(
532    self,
533    rng: &mut (impl RngCore + CryptoRng),
534    sender_spend_key: &Zeroizing<Scalar>,
535  ) -> Result<Transaction, SendError> {
536    // Calculate the key images
537    let mut key_images = vec![];
538    for input in &self.inputs {
539      let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset());
540      if (input_key.deref() * ED25519_BASEPOINT_TABLE) != input.key() {
541        Err(SendError::WrongPrivateKey)?;
542      }
543      let key_image = input_key.deref() * hash_to_point(input.key().compress().to_bytes());
544      key_images.push(key_image);
545    }
546
547    // Convert to a SignableTransactionWithKeyImages
548    let tx = self.with_key_images(key_images);
549
550    // Prepare the CLSAG signatures
551    let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len());
552    for input in &tx.intent.inputs {
553      // Re-derive the input key as this will be in a different order
554      let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset());
555      clsag_signs.push((
556        input_key,
557        ClsagContext::new(input.decoys().clone(), input.commitment().clone())
558          .map_err(SendError::ClsagError)?,
559      ));
560    }
561
562    // Get the output commitments' mask sum
563    let mask_sum = tx.intent.sum_output_masks(&tx.key_images);
564
565    // Get the actual TX, just needing the CLSAGs
566    let mut tx = tx.transaction_without_signatures();
567
568    // Sign the CLSAGs
569    let clsags_and_pseudo_outs = Clsag::sign(
570      rng,
571      clsag_signs,
572      mask_sum,
573      tx.signature_hash().expect("signing a transaction which isn't signed?"),
574    )
575    .map_err(SendError::ClsagError)?;
576
577    // Fill in the CLSAGs/pseudo-outs
578    let inputs_len = tx.prefix().inputs.len();
579    let Transaction::V2 {
580      proofs:
581        Some(RctProofs {
582          prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. },
583          ..
584        }),
585      ..
586    } = tx
587    else {
588      panic!("not signing clsag?")
589    };
590    *clsags = Vec::with_capacity(inputs_len);
591    *pseudo_outs = Vec::with_capacity(inputs_len);
592    for (clsag, pseudo_out) in clsags_and_pseudo_outs {
593      clsags.push(clsag);
594      pseudo_outs.push(pseudo_out);
595    }
596
597    // Return the signed TX
598    Ok(tx)
599  }
600}