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,
220 },
221 #[error(
223 "not enough funds (inputs {inputs}, outputs {outputs}, necessary_fee {necessary_fee:?})"
224 )]
225 NotEnoughFunds {
226 inputs: u64,
228 outputs: u64,
230 necessary_fee: Option<u64>,
235 },
236 #[error("wrong spend private key")]
238 WrongPrivateKey,
239 #[error("this SignableTransaction was created by deserializing a malicious serialization")]
241 MaliciousSerialization,
242 #[error("clsag error ({0})")]
244 ClsagError(ClsagError),
245 #[cfg(feature = "multisig")]
247 #[error("frost error {0}")]
248 FrostError(FrostError),
249}
250
251#[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 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 {
315 let key = input.key().into();
316 if !key.is_torsion_free() {
318 Err(SendError::InvalidInputs)?;
319 }
320 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 if !self.payments.iter().any(|payment| matches!(payment, InternalPayment::Payment(_, _))) {
343 Err(SendError::NoOutputs)?;
344 }
345 if self.payments.len() < 2 {
347 Err(SendError::NoChange)?;
348 }
349 {
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 {
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 for part in &self.data {
377 if part.len() > MAX_ARBITRARY_DATA_SIZE {
378 Err(SendError::TooMuchArbitraryData)?;
379 }
380 }
381
382 if self.extra().len() > MAX_EXTRA_SIZE_BY_RELAY_RULE {
384 Err(SendError::TooMuchArbitraryData)?;
385 }
386
387 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 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 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 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 {
480 let mut rng = res.seeded_rng(b"shuffle_payments");
481 res.payments.shuffle(&mut rng);
482 }
483
484 Ok(res)
485 }
486
487 pub fn fee_rate(&self) -> FeeRate {
489 self.fee_rate
490 }
491
492 pub fn necessary_fee(&self) -> u64 {
497 u64::try_from(self.weight_and_necessary_fee().1).unwrap()
499 }
500
501 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 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 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 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 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 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 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 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 let tx = self.with_key_images(key_images);
682
683 let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len());
685 for input in &tx.intent.inputs {
686 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 let mask_sum = tx.intent.sum_output_masks(&tx.key_images);
698
699 let mut tx = tx.transaction_without_signatures();
701
702 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 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 Ok(tx)
733 }
734}