Skip to main content

monero_wallet/send/
mod.rs

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