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 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#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
115pub struct Change(Option<ChangeEnum>);
116
117impl Change {
118 pub fn new(view_pair: ViewPair, subaddress: Option<SubaddressIndex>) -> Change {
123 Change(Some(ChangeEnum::Standard { view_pair, subaddress }))
124 }
125
126 pub fn guaranteed(view_pair: GuaranteedViewPair, subaddress: Option<SubaddressIndex>) -> Change {
131 Change(Some(ChangeEnum::Guaranteed { view_pair, subaddress }))
132 }
133
134 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#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
174pub enum SendError {
175 #[error("this library doesn't yet support that RctType")]
177 UnsupportedRctType,
178 #[error("no inputs")]
180 NoInputs,
181 #[error("invalid inputs")]
183 InvalidInputs,
184 #[error("invalid number of decoys")]
186 InvalidDecoyQuantity,
187 #[error("no outputs")]
189 NoOutputs,
190 #[error("too many outputs")]
192 TooManyOutputs,
193 #[error("only one output and no change address")]
199 NoChange,
200 #[error("multiple addresses with payment IDs")]
204 MultiplePaymentIds,
205 #[error("too much data")]
207 TooMuchArbitraryData,
208 #[error("too large of a transaction")]
210 TooLargeTransaction,
211 #[error("transaction amounts exceed u64::MAX (in {in_amount}, out {out_amount})")]
213 AmountsUnrepresentable {
214 in_amount: u128,
216 out_amount: u128,
218 },
219 #[error(
221 "not enough funds (inputs {inputs}, outputs {outputs}, necessary_fee {necessary_fee:?})"
222 )]
223 NotEnoughFunds {
224 inputs: u64,
226 outputs: u64,
228 necessary_fee: Option<u64>,
233 },
234 #[error("wrong spend private key")]
236 WrongPrivateKey,
237 #[error("this SignableTransaction was created by deserializing a malicious serialization")]
239 MaliciousSerialization,
240 #[error("clsag error ({0})")]
242 ClsagError(ClsagError),
243 #[cfg(feature = "multisig")]
245 #[error("frost error {0}")]
246 FrostError(FrostError),
247}
248
249#[derive(Clone, Zeroize, ZeroizeOnDrop)]
251pub struct SignableTransaction {
252 rct_type: RctType,
253 outgoing_view_key: Zeroizing<[u8; 32]>,
254 inputs: Vec<OutputWithDecoys>,
255 payments: Vec<InternalPayment>,
256 data: Vec<Vec<u8>>,
257 fee_rate: FeeRate,
258}
259
260impl PartialEq for SignableTransaction {
261 fn eq(&self, other: &Self) -> bool {
262 (self.rct_type == other.rct_type) &&
263 bool::from(self.outgoing_view_key.deref().ct_eq(other.outgoing_view_key.deref())) &&
264 (self.inputs == other.inputs) &&
265 (self.payments == other.payments) &&
266 (self.data == other.data) &&
267 (self.fee_rate == other.fee_rate)
268 }
269}
270impl Eq for SignableTransaction {}
271
272impl fmt::Debug for SignableTransaction {
273 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
276 f.debug_struct("SignableTransaction")
277 .field("rct_type", &self.rct_type)
278 .field("inputs", &self.inputs)
279 .field("payments", &self.payments)
280 .field("data", &self.data)
281 .field("fee_rate", &self.fee_rate)
282 .finish_non_exhaustive()
283 }
284}
285
286#[derive(Zeroize, ZeroizeOnDrop)]
287struct SignableTransactionWithKeyImages {
288 intent: SignableTransaction,
289 key_images: Vec<CompressedPoint>,
290}
291
292impl SignableTransaction {
293 fn validate(&self) -> Result<(), SendError> {
294 match self.rct_type {
295 RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => {}
296 RctType::AggregateMlsagBorromean |
297 RctType::MlsagBorromean |
298 RctType::MlsagBulletproofs |
299 RctType::MlsagBulletproofsCompactAmount => Err(SendError::UnsupportedRctType)?,
300 }
301
302 if self.inputs.is_empty() {
303 Err(SendError::NoInputs)?;
304 }
305 if self.inputs.iter().map(|input| input.key().compress()).collect::<HashSet<_>>().len() !=
306 self.inputs.len()
307 {
308 Err(SendError::InvalidInputs)?;
309 }
310 for input in &self.inputs {
311 {
313 let key = input.key().into();
314 if !key.is_torsion_free() {
316 Err(SendError::InvalidInputs)?;
317 }
318 use curve25519_dalek::traits::IsIdentity as _;
320 if key.is_identity() {
321 Err(SendError::InvalidInputs)?;
322 }
323 }
324
325 if input.decoys().len() !=
326 match self.rct_type {
327 RctType::ClsagBulletproof => 11,
328 RctType::ClsagBulletproofPlus => 16,
329 RctType::AggregateMlsagBorromean |
330 RctType::MlsagBorromean |
331 RctType::MlsagBulletproofs |
332 RctType::MlsagBulletproofsCompactAmount => panic!("unsupported RctType"),
333 }
334 {
335 Err(SendError::InvalidDecoyQuantity)?;
336 }
337 }
338
339 if !self.payments.iter().any(|payment| matches!(payment, InternalPayment::Payment(_, _))) {
341 Err(SendError::NoOutputs)?;
342 }
343 if self.payments.len() < 2 {
345 Err(SendError::NoChange)?;
346 }
347 {
349 let mut change_count = 0;
350 for payment in &self.payments {
351 change_count += usize::from(u8::from(matches!(payment, InternalPayment::Change(_))));
352 }
353 if change_count > 1 {
354 Err(SendError::MaliciousSerialization)?;
355 }
356 }
357
358 {
360 let mut payment_ids = 0;
361 for payment in &self.payments {
362 payment_ids += usize::from(u8::from(payment.address().payment_id().is_some()));
363 }
364 if payment_ids > 1 {
365 Err(SendError::MultiplePaymentIds)?;
366 }
367 }
368
369 if self.payments.len() > MAX_BULLETPROOF_COMMITMENTS {
370 Err(SendError::TooManyOutputs)?;
371 }
372
373 for part in &self.data {
375 if part.len() > MAX_ARBITRARY_DATA_SIZE {
376 Err(SendError::TooMuchArbitraryData)?;
377 }
378 }
379
380 if self.extra().len() > MAX_EXTRA_SIZE_BY_RELAY_RULE {
382 Err(SendError::TooMuchArbitraryData)?;
383 }
384
385 let weight;
387 {
388 let in_amount: u128 =
389 self.inputs.iter().map(|input| u128::from(input.commitment().amount)).sum();
390 let payments_amount: u128 = self
391 .payments
392 .iter()
393 .filter_map(|payment| match payment {
394 InternalPayment::Payment(_, amount) => Some(u128::from(*amount)),
395 InternalPayment::Change(_) => None,
396 })
397 .sum();
398 let necessary_fee;
399 (weight, necessary_fee) = self.weight_and_necessary_fee();
400 let out_amount = payments_amount + u128::from(necessary_fee);
401 let in_out_amount = u64::try_from(in_amount)
402 .and_then(|in_amount| u64::try_from(out_amount).map(|out_amount| (in_amount, out_amount)));
403 let Ok((in_amount, out_amount)) = in_out_amount else {
404 Err(SendError::AmountsUnrepresentable { in_amount, out_amount })?
405 };
406 if in_amount < out_amount {
407 Err(SendError::NotEnoughFunds {
408 inputs: in_amount,
409 outputs: u64::try_from(payments_amount)
410 .expect("total out fit within u64 but not part of total out"),
411 necessary_fee: Some(necessary_fee),
412 })?;
413 }
414 }
415
416 const MAX_TX_SIZE: usize = (300_000 / 2) - 600;
424 if weight >= MAX_TX_SIZE {
425 Err(SendError::TooLargeTransaction)?;
426 }
427
428 Ok(())
429 }
430
431 pub fn new(
451 rct_type: RctType,
452 outgoing_view_key: Zeroizing<[u8; 32]>,
453 inputs: Vec<OutputWithDecoys>,
454 payments: Vec<(MoneroAddress, u64)>,
455 change: Change,
456 data: Vec<Vec<u8>>,
457 fee_rate: FeeRate,
458 ) -> Result<SignableTransaction, SendError> {
459 let mut payments = payments
461 .into_iter()
462 .map(|(addr, amount)| InternalPayment::Payment(addr, amount))
463 .collect::<Vec<_>>();
464
465 if let Some(change) = change.0 {
466 payments.push(InternalPayment::Change(change));
467 }
468
469 let mut res =
470 SignableTransaction { rct_type, outgoing_view_key, inputs, payments, data, fee_rate };
471 res.validate()?;
472
473 {
475 let mut rng = res.seeded_rng(b"shuffle_payments");
476 res.payments.shuffle(&mut rng);
477 }
478
479 Ok(res)
480 }
481
482 pub fn fee_rate(&self) -> FeeRate {
484 self.fee_rate
485 }
486
487 pub fn necessary_fee(&self) -> u64 {
492 self.weight_and_necessary_fee().1
493 }
494
495 pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
500 fn write_payment<W: io::Write>(payment: &InternalPayment, w: &mut W) -> io::Result<()> {
501 match payment {
502 InternalPayment::Payment(addr, amount) => {
503 w.write_all(&[0])?;
504 write_vec(write_byte, addr.to_string().as_bytes(), w)?;
505 w.write_all(&amount.to_le_bytes())
506 }
507 InternalPayment::Change(change) => match change {
508 ChangeEnum::AddressOnly(addr) => {
509 w.write_all(&[1])?;
510 write_vec(write_byte, addr.to_string().as_bytes(), w)
511 }
512 ChangeEnum::Standard { view_pair, subaddress } => {
513 w.write_all(&[2])?;
514 view_pair.spend().compress().write(w)?;
515 view_pair.view.write(w)?;
516 if let Some(subaddress) = subaddress {
517 w.write_all(&subaddress.account().to_le_bytes())?;
518 w.write_all(&subaddress.address().to_le_bytes())
519 } else {
520 w.write_all(&0u32.to_le_bytes())?;
521 w.write_all(&0u32.to_le_bytes())
522 }
523 }
524 ChangeEnum::Guaranteed { view_pair, subaddress } => {
525 w.write_all(&[3])?;
526 view_pair.spend().compress().write(w)?;
527 view_pair.0.view.write(w)?;
528 if let Some(subaddress) = subaddress {
529 w.write_all(&subaddress.account().to_le_bytes())?;
530 w.write_all(&subaddress.address().to_le_bytes())
531 } else {
532 w.write_all(&0u32.to_le_bytes())?;
533 w.write_all(&0u32.to_le_bytes())
534 }
535 }
536 },
537 }
538 }
539
540 write_byte(&u8::from(self.rct_type), w)?;
541 w.write_all(self.outgoing_view_key.as_slice())?;
542 write_vec(OutputWithDecoys::write, &self.inputs, w)?;
543 write_vec(write_payment, &self.payments, w)?;
544 write_vec(|data, w| write_vec(write_byte, data, w), &self.data, w)?;
545 self.fee_rate.write(w)
546 }
547
548 pub fn serialize(&self) -> Vec<u8> {
553 let mut buf = Vec::with_capacity(256);
554 self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
555 buf
556 }
557
558 pub fn read<R: io::Read>(r: &mut R) -> io::Result<SignableTransaction> {
563 fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> {
564 String::from_utf8(read_vec(read_byte, Some(MoneroAddress::SIZE_UPPER_BOUND.0), r)?)
565 .ok()
566 .and_then(|str| MoneroAddress::from_str_with_unchecked_network(&str).ok())
567 .ok_or_else(|| io::Error::other("invalid address"))
568 }
569
570 fn read_payment<R: io::Read>(r: &mut R) -> io::Result<InternalPayment> {
571 Ok(match read_byte(r)? {
572 0 => InternalPayment::Payment(read_address(r)?, read_u64(r)?),
573 1 => InternalPayment::Change(ChangeEnum::AddressOnly(read_address(r)?)),
574 2 => InternalPayment::Change(ChangeEnum::Standard {
575 view_pair: ViewPair::new(
576 CompressedPoint::read(r)?
577 .decompress()
578 .ok_or_else(|| io::Error::other("`Change` payment had invalid public spend key"))?,
579 Zeroizing::new(Scalar::read(r)?),
580 )
581 .map_err(io::Error::other)?,
582 subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?),
583 }),
584 3 => InternalPayment::Change(ChangeEnum::Guaranteed {
585 view_pair: GuaranteedViewPair::new(
586 CompressedPoint::read(r)?.decompress().ok_or_else(|| {
587 io::Error::other("guaranteed `Change` payment had invalid public spend key")
588 })?,
589 Zeroizing::new(Scalar::read(r)?),
590 )
591 .map_err(io::Error::other)?,
592 subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?),
593 }),
594 _ => Err(io::Error::other("invalid payment"))?,
595 })
596 }
597
598 let res = SignableTransaction {
599 rct_type: RctType::try_from(read_byte(r)?)
600 .map_err(|()| io::Error::other("unsupported/invalid RctType"))?,
601 outgoing_view_key: Zeroizing::new(read_bytes(r)?),
602 inputs: read_vec(OutputWithDecoys::read, Some(TransactionPrefix::INPUTS_UPPER_BOUND.0), r)?,
603 payments: read_vec(read_payment, Some(MAX_BULLETPROOF_COMMITMENTS), r)?,
604 data: read_vec(
609 |r| read_vec(read_byte, Some(MAX_ARBITRARY_DATA_SIZE), r),
610 Some(MAX_EXTRA_SIZE_BY_RELAY_RULE),
611 r,
612 )?,
613 fee_rate: FeeRate::read(r)?,
614 };
615 match res.validate() {
616 Ok(()) => {}
617 Err(e) => Err(io::Error::other(e))?,
618 }
619 Ok(res)
620 }
621
622 fn with_key_images(
623 mut self,
624 mut key_images: Vec<CompressedPoint>,
625 ) -> SignableTransactionWithKeyImages {
626 debug_assert_eq!(self.inputs.len(), key_images.len());
627
628 let mut sorted_inputs = self.inputs.drain(..).zip(key_images.drain(..)).collect::<Vec<_>>();
630 sorted_inputs
631 .sort_by(|(_, key_image_a), (_, key_image_b)| key_image_sort(key_image_a, key_image_b));
632
633 for (input, key_image) in sorted_inputs {
634 self.inputs.push(input);
635 key_images.push(key_image);
636 }
637
638 SignableTransactionWithKeyImages { intent: self, key_images }
639 }
640
641 pub fn unsigned_transaction(self, key_images: Vec<CompressedPoint>) -> Option<Transaction> {
645 if self.inputs.len() != key_images.len() {
646 None?;
647 }
648 Some(self.with_key_images(key_images).transaction_without_signatures())
649 }
650
651 pub fn sign(
655 self,
656 rng: &mut (impl RngCore + CryptoRng),
657 sender_spend_key: &Zeroizing<Scalar>,
658 ) -> Result<Transaction, SendError> {
659 let sender_spend_key = Zeroizing::new((**sender_spend_key).into());
660
661 let mut key_images = vec![];
663 for input in &self.inputs {
664 let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset().into());
665 if bool::from(!(input_key.deref() * ED25519_BASEPOINT_TABLE).ct_eq(&input.key().into())) {
666 Err(SendError::WrongPrivateKey)?;
667 }
668 let key_image = Point::from(
669 input_key.deref() * Point::biased_hash(input.key().compress().to_bytes()).into(),
670 );
671 key_images.push(key_image.compress());
672 }
673
674 let tx = self.with_key_images(key_images);
676
677 let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len());
679 for input in &tx.intent.inputs {
680 let input_key =
682 Zeroizing::new(Scalar::from(sender_spend_key.deref() + input.key_offset().into()));
683 clsag_signs.push((
684 input_key,
685 ClsagContext::new(input.decoys().clone(), input.commitment().clone())
686 .map_err(SendError::ClsagError)?,
687 ));
688 }
689
690 let mask_sum = tx.intent.sum_output_masks(&tx.key_images);
692
693 let mut tx = tx.transaction_without_signatures();
695
696 let clsags_and_pseudo_outs = Clsag::sign(
698 rng,
699 clsag_signs,
700 mask_sum,
701 tx.signature_hash().expect("signing a transaction which isn't signed?"),
702 )
703 .map_err(SendError::ClsagError)?;
704
705 let inputs_len = tx.prefix().inputs.len();
707 let Transaction::V2 {
708 proofs:
709 Some(RctProofs {
710 prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. },
711 ..
712 }),
713 ..
714 } = tx
715 else {
716 panic!("not signing clsag?")
717 };
718 *clsags = Vec::with_capacity(inputs_len);
719 *pseudo_outs = Vec::with_capacity(inputs_len);
720 for (clsag, pseudo_out) in clsags_and_pseudo_outs {
721 clsags.push(clsag);
722 pseudo_outs.push(pseudo_out.compress());
723 }
724
725 Ok(tx)
727 }
728}