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#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
73pub struct Change(Option<ChangeEnum>);
74
75impl Change {
76 pub fn new(view_pair: ViewPair, subaddress: Option<SubaddressIndex>) -> Change {
81 Change(Some(ChangeEnum::Standard { view_pair, subaddress }))
82 }
83
84 pub fn guaranteed(view_pair: GuaranteedViewPair, subaddress: Option<SubaddressIndex>) -> Change {
89 Change(Some(ChangeEnum::Guaranteed { view_pair, subaddress }))
90 }
91
92 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 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#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
145pub enum SendError {
146 #[error("this library doesn't yet support that RctType")]
148 UnsupportedRctType,
149 #[error("no inputs")]
151 NoInputs,
152 #[error("invalid number of decoys")]
154 InvalidDecoyQuantity,
155 #[error("no outputs")]
157 NoOutputs,
158 #[error("too many outputs")]
160 TooManyOutputs,
161 #[error("only one output and no change address")]
167 NoChange,
168 #[error("multiple addresses with payment IDs")]
172 MultiplePaymentIds,
173 #[error("too much data")]
175 TooMuchArbitraryData,
176 #[error("too large of a transaction")]
178 TooLargeTransaction,
179 #[error("transaction amounts exceed u64::MAX (in {in_amount}, out {out_amount})")]
181 AmountsUnrepresentable {
182 in_amount: u128,
184 out_amount: u128,
186 },
187 #[error(
189 "not enough funds (inputs {inputs}, outputs {outputs}, necessary_fee {necessary_fee:?})"
190 )]
191 NotEnoughFunds {
192 inputs: u64,
194 outputs: u64,
196 necessary_fee: Option<u64>,
201 },
202 #[error("wrong spend private key")]
204 WrongPrivateKey,
205 #[error("this SignableTransaction was created by deserializing a malicious serialization")]
207 MaliciousSerialization,
208 #[error("clsag error ({0})")]
210 ClsagError(ClsagError),
211 #[cfg(feature = "multisig")]
213 #[error("frost error {0}")]
214 FrostError(FrostError),
215}
216
217#[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 if !self.payments.iter().any(|payment| matches!(payment, InternalPayment::Payment(_, _))) {
269 Err(SendError::NoOutputs)?;
270 }
271 if self.payments.len() < 2 {
273 Err(SendError::NoChange)?;
274 }
275 {
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 {
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 for part in &self.data {
303 if part.len() > MAX_ARBITRARY_DATA_SIZE {
304 Err(SendError::TooMuchArbitraryData)?;
305 }
306 }
307
308 if self.extra().len() > MAX_EXTRA_SIZE_BY_RELAY_RULE {
310 Err(SendError::TooMuchArbitraryData)?;
311 }
312
313 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 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 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 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 {
397 let mut rng = res.seeded_rng(b"shuffle_payments");
398 res.payments.shuffle(&mut rng);
399 }
400
401 Ok(res)
402 }
403
404 pub fn fee_rate(&self) -> FeeRate {
406 self.fee_rate
407 }
408
409 pub fn necessary_fee(&self) -> u64 {
414 self.weight_and_necessary_fee().1
415 }
416
417 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 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 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 const FEATURED_ADDRESS_DATA_SIZE_UPPER_BOUND: usize =
493 <u64 as VarInt>::UPPER_BOUND + 32 + 32 + <u64 as VarInt>::UPPER_BOUND + 8;
494 const FEATURED_ADDRESS_CHECKED_DATA_SIZE_UPPER_BOUND: usize =
496 FEATURED_ADDRESS_DATA_SIZE_UPPER_BOUND + 4;
497 const FEATURED_ADDRESS_ENCODED_SIZE_UPPER_BOUND: usize =
499 (FEATURED_ADDRESS_CHECKED_DATA_SIZE_UPPER_BOUND * 8).div_ceil(5);
500
501 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 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 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 pub fn sign(
594 self,
595 rng: &mut (impl RngCore + CryptoRng),
596 sender_spend_key: &Zeroizing<Scalar>,
597 ) -> Result<Transaction, SendError> {
598 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 let tx = self.with_key_images(key_images);
611
612 let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len());
614 for input in &tx.intent.inputs {
615 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 let mask_sum = tx.intent.sum_output_masks(&tx.key_images);
626
627 let mut tx = tx.transaction_without_signatures();
629
630 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 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 Ok(tx)
661 }
662}