use core::{ops::Deref, fmt}; use std_shims::{ vec::Vec, io, string::{String, ToString}, }; use rand_core::{RngCore, CryptoRng, SeedableRng}; use rand_chacha::ChaCha20Rng; use rand::seq::SliceRandom; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use group::Group; use curve25519_dalek::{ constants::{ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_TABLE}, scalar::Scalar, edwards::EdwardsPoint, }; use dalek_ff_group as dfg; #[cfg(feature = "multisig")] use frost::FrostError; use crate::{ Protocol, Commitment, hash, random_scalar, serialize::{ read_byte, read_bytes, read_u64, read_scalar, read_point, read_vec, write_byte, write_scalar, write_point, write_raw_vec, write_vec, }, ringct::{ generate_key_image, clsag::{ClsagError, ClsagInput, Clsag}, bulletproofs::{MAX_OUTPUTS, Bulletproofs}, RctBase, RctPrunable, RctSignatures, }, transaction::{Input, Output, Timelock, TransactionPrefix, Transaction}, rpc::RpcError, wallet::{ address::{Network, AddressSpec, MoneroAddress}, ViewPair, SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort, uniqueness, shared_key, commitment_mask, amount_encryption, extra::{ARBITRARY_DATA_MARKER, MAX_ARBITRARY_DATA_SIZE}, }, }; #[cfg(feature = "std")] mod builder; #[cfg(feature = "std")] pub use builder::SignableTransactionBuilder; #[cfg(feature = "multisig")] mod multisig; #[cfg(feature = "multisig")] pub use multisig::TransactionMachine; use crate::ringct::EncryptedAmount; #[allow(non_snake_case)] #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] struct SendOutput { R: EdwardsPoint, view_tag: u8, dest: EdwardsPoint, commitment: Commitment, amount: [u8; 8], } impl SendOutput { #[allow(non_snake_case)] fn internal( unique: [u8; 32], output: (usize, (MoneroAddress, u64)), ecdh: EdwardsPoint, R: EdwardsPoint, ) -> (SendOutput, Option<[u8; 8]>) { let o = output.0; let output = output.1; let (view_tag, shared_key, payment_id_xor) = shared_key(Some(unique).filter(|_| output.0.is_guaranteed()), ecdh, o); ( SendOutput { R, view_tag, dest: ((&shared_key * ED25519_BASEPOINT_TABLE) + output.0.spend), commitment: Commitment::new(commitment_mask(shared_key), output.1), amount: amount_encryption(output.1, shared_key), }, output .0 .payment_id() .map(|id| (u64::from_le_bytes(id) ^ u64::from_le_bytes(payment_id_xor)).to_le_bytes()), ) } fn new( r: &Zeroizing, unique: [u8; 32], output: (usize, (MoneroAddress, u64)), ) -> (SendOutput, Option<[u8; 8]>) { let address = output.1 .0; SendOutput::internal( unique, output, r.deref() * address.view, if !address.is_subaddress() { r.deref() * ED25519_BASEPOINT_TABLE } else { r.deref() * address.spend }, ) } fn change( ecdh: EdwardsPoint, unique: [u8; 32], output: (usize, (MoneroAddress, u64)), ) -> (SendOutput, Option<[u8; 8]>) { SendOutput::internal(unique, output, ecdh, ED25519_BASEPOINT_POINT) } } #[derive(Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum TransactionError { #[cfg_attr(feature = "std", error("multiple addresses with payment IDs"))] MultiplePaymentIds, #[cfg_attr(feature = "std", error("no inputs"))] NoInputs, #[cfg_attr(feature = "std", error("no outputs"))] NoOutputs, #[cfg_attr(feature = "std", error("invalid number of decoys"))] InvalidDecoyQuantity, #[cfg_attr(feature = "std", error("only one output and no change address"))] NoChange, #[cfg_attr(feature = "std", error("too many outputs"))] TooManyOutputs, #[cfg_attr(feature = "std", error("too much data"))] TooMuchData, #[cfg_attr(feature = "std", error("too many inputs/too much arbitrary data"))] TooLargeTransaction, #[cfg_attr( feature = "std", error("not enough funds (inputs {inputs}, outputs {outputs}, fee {fee})") )] NotEnoughFunds { inputs: u64, outputs: u64, fee: u64 }, #[cfg_attr(feature = "std", error("wrong spend private key"))] WrongPrivateKey, #[cfg_attr(feature = "std", error("rpc error ({0})"))] RpcError(RpcError), #[cfg_attr(feature = "std", error("clsag error ({0})"))] ClsagError(ClsagError), #[cfg_attr(feature = "std", error("invalid transaction ({0})"))] InvalidTransaction(RpcError), #[cfg(feature = "multisig")] #[cfg_attr(feature = "std", error("frost error {0}"))] FrostError(FrostError), } fn prepare_inputs( inputs: &[(SpendableOutput, Decoys)], spend: &Zeroizing, tx: &mut Transaction, ) -> Result, EdwardsPoint, ClsagInput)>, TransactionError> { let mut signable = Vec::with_capacity(inputs.len()); for (i, (input, decoys)) in inputs.iter().enumerate() { let input_spend = Zeroizing::new(input.key_offset() + spend.deref()); let image = generate_key_image(&input_spend); signable.push(( input_spend, image, ClsagInput::new(input.commitment().clone(), decoys.clone()) .map_err(TransactionError::ClsagError)?, )); tx.prefix.inputs.push(Input::ToKey { amount: None, key_offsets: decoys.offsets.clone(), key_image: signable[i].1, }); } signable.sort_by(|x, y| x.1.compress().to_bytes().cmp(&y.1.compress().to_bytes()).reverse()); tx.prefix.inputs.sort_by(|x, y| { if let (Input::ToKey { key_image: x, .. }, Input::ToKey { key_image: y, .. }) = (x, y) { x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse() } else { panic!("Input wasn't ToKey") } }); Ok(signable) } // Deterministically calculate what the TX weight and fee will be. fn calculate_weight_and_fee( protocol: Protocol, decoy_weights: &[usize], n_outputs: usize, extra: usize, fee_rate: Fee, ) -> (usize, u64) { // Starting the fee at 0 here is different than core Monero's wallet2.cpp, which starts its fee // calculation with an estimate. // // This difference is okay in practice because wallet2 still ends up using a fee calculated from // a TX's weight, as calculated later in this function. // // See this PR highlighting wallet2's behavior: // https://github.com/monero-project/monero/pull/8882 // // Even with that PR, if the estimated fee's VarInt byte length is larger than the calculated // fee's, the wallet can theoretically use a fee not based on the actual TX weight. This does not // occur in practice as it's nearly impossible for wallet2 to estimate a fee that is larger // than the calculated fee today, and on top of that, even more unlikely for that estimate's // VarInt to be larger in byte length than the calculated fee's. let mut weight = 0usize; let mut fee = 0u64; let mut done = false; let mut iters = 0; let max_iters = 5; while !done { weight = Transaction::fee_weight(protocol, decoy_weights, n_outputs, extra, fee); let fee_calculated_from_weight = fee_rate.calculate_fee_from_weight(weight); // Continue trying to use the fee calculated from the tx's weight done = fee_calculated_from_weight == fee; fee = fee_calculated_from_weight; #[cfg(test)] debug_assert!(iters < max_iters, "Reached max fee calculation attempts"); // Should never happen because the fee VarInt byte length shouldn't change *every* single iter. // `iters` reaching `max_iters` is unexpected. if iters >= max_iters { // Fail-safe break to ensure funds are still spendable break; } iters += 1; } (weight, fee) } /// Fee struct, defined as a per-unit cost and a mask for rounding purposes. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] pub struct Fee { pub per_weight: u64, pub mask: u64, } impl Fee { pub fn calculate_fee_from_weight(&self, weight: usize) -> u64 { let fee = (((self.per_weight * u64::try_from(weight).unwrap()) + self.mask - 1) / self.mask) * self.mask; debug_assert_eq!(weight, self.calculate_weight_from_fee(fee), "Miscalculated weight from fee"); fee } pub fn calculate_weight_from_fee(&self, fee: u64) -> usize { usize::try_from(fee / self.per_weight).unwrap() } } /// Fee priority, determining how quickly a transaction is included in a block. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[allow(non_camel_case_types)] pub enum FeePriority { Lowest, Low, Medium, High, Custom { priority: u32 }, } impl FeePriority { pub(crate) fn fee_priority(&self) -> u32 { match self { FeePriority::Lowest => 0, FeePriority::Low => 1, FeePriority::Medium => 2, FeePriority::High => 3, FeePriority::Custom { priority, .. } => *priority, } } } #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub(crate) enum InternalPayment { Payment((MoneroAddress, u64)), Change(Change, u64), } /// The eventual output of a SignableTransaction. /// /// If the SignableTransaction has a Change with a view key, this will also have the view key. /// Accordingly, it must be treated securely. #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct Eventuality { protocol: Protocol, r_seed: Zeroizing<[u8; 32]>, inputs: Vec, payments: Vec, extra: Vec, } /// A signable transaction, either in a single-signer or multisig context. #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] pub struct SignableTransaction { protocol: Protocol, r_seed: Option>, inputs: Vec<(SpendableOutput, Decoys)>, payments: Vec, data: Vec>, fee: u64, fee_rate: Fee, } /// Specification for a change output. #[derive(Clone, PartialEq, Eq, Zeroize)] pub struct Change { address: MoneroAddress, view: Option>, } impl fmt::Debug for Change { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Change").field("address", &self.address).finish_non_exhaustive() } } impl Change { /// Create a change output specification from a ViewPair, as needed to maintain privacy. pub fn new(view: &ViewPair, guaranteed: bool) -> Change { Change { address: view.address( Network::Mainnet, if !guaranteed { AddressSpec::Standard } else { AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: true } }, ), view: Some(view.view.clone()), } } /// Create a fingerprintable change output specification which will harm privacy. Only use this /// if you know what you're doing. pub fn fingerprintable(address: MoneroAddress) -> Change { Change { address, view: None } } } fn need_additional(payments: &[InternalPayment]) -> (bool, bool) { let mut has_change_view = false; let subaddresses = payments .iter() .filter(|payment| match *payment { InternalPayment::Payment(payment) => payment.0.is_subaddress(), InternalPayment::Change(change, _) => { if change.view.is_some() { has_change_view = true; // It should not be possible to construct a change specification to a subaddress with a // view key debug_assert!(!change.address.is_subaddress()); } change.address.is_subaddress() } }) .count() != 0; // We need additional keys if we have any subaddresses let mut additional = subaddresses; // Unless the above change view key path is taken if (payments.len() == 2) && has_change_view { additional = false; } (subaddresses, additional) } fn sanity_check_change_payment(payments: &[InternalPayment], has_change_address: bool) { debug_assert_eq!( payments .iter() .filter(|payment| match *payment { InternalPayment::Payment(_) => false, InternalPayment::Change(_, _) => true, }) .count(), if has_change_address { 1 } else { 0 }, "Unexpected number of change outputs" ); } impl SignableTransaction { /// Create a signable transaction. /// /// `r_seed` refers to a seed used to derive the transaction's ephemeral keys (colloquially /// called Rs). If None is provided, one will be automatically generated. /// /// Up to 16 outputs may be present, including the change output. If the change address is /// specified, leftover funds will be sent to it. /// /// Each chunk of data must not exceed MAX_ARBITRARY_DATA_SIZE and will be embedded in TX extra. pub fn new( protocol: Protocol, r_seed: Option>, inputs: Vec<(SpendableOutput, Decoys)>, payments: Vec<(MoneroAddress, u64)>, change_address: Option, data: Vec>, fee_rate: Fee, ) -> Result { // Make sure there's only one payment ID let mut has_payment_id = { let mut payment_ids = 0; let mut count = |addr: MoneroAddress| { if addr.payment_id().is_some() { payment_ids += 1 } }; for payment in &payments { count(payment.0); } if let Some(change) = change_address.as_ref() { count(change.address); } if payment_ids > 1 { Err(TransactionError::MultiplePaymentIds)?; } payment_ids == 1 }; if inputs.is_empty() { Err(TransactionError::NoInputs)?; } if payments.is_empty() { Err(TransactionError::NoOutputs)?; } for (_, decoys) in &inputs { if decoys.len() != protocol.ring_len() { Err(TransactionError::InvalidDecoyQuantity)?; } } for part in &data { if part.len() > MAX_ARBITRARY_DATA_SIZE { Err(TransactionError::TooMuchData)?; } } // If we don't have two outputs, as required by Monero, error if (payments.len() == 1) && change_address.is_none() { Err(TransactionError::NoChange)?; } // Get the outgoing amount ignoring fees let out_amount = payments.iter().map(|payment| payment.1).sum::(); let outputs = payments.len() + usize::from(change_address.is_some()); if outputs > MAX_OUTPUTS { Err(TransactionError::TooManyOutputs)?; } // Collect payments in a container that includes a change output if a change address is provided let mut payments = payments.into_iter().map(InternalPayment::Payment).collect::>(); if change_address.is_some() { // Push a 0 amount change output that we'll use to do fee calculations. // We'll modify the change amount after calculating the fee payments.push(InternalPayment::Change(change_address.clone().unwrap(), 0)); } // Determine if we'll need additional pub keys in tx extra let (_, additional) = need_additional(&payments); // Add a dummy payment ID if there's only 2 payments has_payment_id |= outputs == 2; // Calculate the extra length let extra = Extra::fee_weight(outputs, additional, has_payment_id, data.as_ref()); // https://github.com/monero-project/monero/pull/8733 const MAX_EXTRA_SIZE: usize = 1060; if extra > MAX_EXTRA_SIZE { Err(TransactionError::TooMuchData)?; } // Caclculate weight of decoys let decoy_weights = inputs.iter().map(|(_, decoy)| Decoys::fee_weight(&decoy.offsets)).collect::>(); // Deterministically calculate tx weight and fee let (weight, fee) = calculate_weight_and_fee(protocol, &decoy_weights, outputs, extra, fee_rate); // The actual limit is half the block size, and for the minimum block size of 300k, that'd be // 150k // wallet2 will only create transactions up to 100k bytes however const MAX_TX_SIZE: usize = 100_000; if weight >= MAX_TX_SIZE { Err(TransactionError::TooLargeTransaction)?; } // Make sure we have enough funds let in_amount = inputs.iter().map(|(input, _)| input.commitment().amount).sum::(); if in_amount < (out_amount + fee) { Err(TransactionError::NotEnoughFunds { inputs: in_amount, outputs: out_amount, fee })?; } // Sanity check we have the expected number of change outputs sanity_check_change_payment(&payments, change_address.is_some()); // Modify the amount of the change output if change_address.is_some() { let change_payment = payments.last_mut().unwrap(); debug_assert!(matches!(change_payment, InternalPayment::Change(_, _))); *change_payment = InternalPayment::Change(change_address.clone().unwrap(), in_amount - out_amount - fee); } // Sanity check the change again after modifying sanity_check_change_payment(&payments, change_address.is_some()); // Sanity check outgoing amount + fee == incoming amount debug_assert_eq!( payments .iter() .map(|payment| match *payment { InternalPayment::Payment(payment) => payment.1, InternalPayment::Change(_, amount) => amount, }) .sum::() + fee, in_amount, "Outgoing amount + fee != incoming amount" ); Ok(SignableTransaction { protocol, r_seed, inputs, payments, data, fee, fee_rate }) } pub fn fee(&self) -> u64 { self.fee } pub fn fee_rate(&self) -> Fee { self.fee_rate } #[allow(clippy::type_complexity)] fn prepare_payments( seed: &Zeroizing<[u8; 32]>, inputs: &[EdwardsPoint], payments: &mut Vec, uniqueness: [u8; 32], ) -> (EdwardsPoint, Vec>, Vec, Option<[u8; 8]>) { let mut rng = { // Hash the inputs into the seed so we don't re-use Rs // Doesn't re-use uniqueness as that's based on key images, which requires interactivity // to generate. The output keys do not // This remains private so long as the seed is private let mut r_uniqueness = vec![]; for input in inputs { r_uniqueness.extend(input.compress().to_bytes()); } ChaCha20Rng::from_seed(hash( &[b"monero-serai_outputs".as_ref(), seed.as_ref(), &r_uniqueness].concat(), )) }; // Shuffle the payments payments.shuffle(&mut rng); // Used for all non-subaddress outputs, or if there's only one subaddress output and a change let tx_key = Zeroizing::new(random_scalar(&mut rng)); let mut tx_public_key = tx_key.deref() * ED25519_BASEPOINT_TABLE; // If any of these outputs are to a subaddress, we need keys distinct to them // The only time this *does not* force having additional keys is when the only other output // is a change output we have the view key for, enabling rewriting rA to aR let (subaddresses, additional) = need_additional(payments); let modified_change_ecdh = subaddresses && (!additional); // If we're using the aR rewrite, update tx_public_key from rG to rB if modified_change_ecdh { for payment in &*payments { match payment { InternalPayment::Payment(payment) => { // This should be the only payment and it should be a subaddress debug_assert!(payment.0.is_subaddress()); tx_public_key = tx_key.deref() * payment.0.spend; } InternalPayment::Change(_, _) => {} } } debug_assert!(tx_public_key != (tx_key.deref() * ED25519_BASEPOINT_TABLE)); } // Actually create the outputs let mut additional_keys = vec![]; let mut outputs = Vec::with_capacity(payments.len()); let mut id = None; for (o, mut payment) in payments.drain(..).enumerate() { // Downcast the change output to a payment output if it doesn't require special handling // regarding it's view key payment = if !modified_change_ecdh { if let InternalPayment::Change(change, amount) = &payment { InternalPayment::Payment((change.address, *amount)) } else { payment } } else { payment }; let (output, payment_id) = match payment { InternalPayment::Payment(payment) => { // If this is a subaddress, generate a dedicated r. Else, reuse the TX key let dedicated = Zeroizing::new(random_scalar(&mut rng)); let use_dedicated = additional && payment.0.is_subaddress(); let r = if use_dedicated { &dedicated } else { &tx_key }; let (mut output, payment_id) = SendOutput::new(r, uniqueness, (o, payment)); if modified_change_ecdh { debug_assert_eq!(tx_public_key, output.R); } if use_dedicated { additional_keys.push(dedicated); } else { // If this used tx_key, randomize its R // This is so when extra is created, there's a distinct R for it to use output.R = dfg::EdwardsPoint::random(&mut rng).0; } (output, payment_id) } InternalPayment::Change(change, amount) => { // Instead of rA, use Ra, where R is r * subaddress_spend_key // change.view must be Some as if it's None, this payment would've been downcast let ecdh = tx_public_key * change.view.unwrap().deref(); SendOutput::change(ecdh, uniqueness, (o, (change.address, amount))) } }; outputs.push(output); id = id.or(payment_id); } // Include a random payment ID if we don't actually have one // It prevents transactions from leaking if they're sending to integrated addresses or not // Only do this if we only have two outputs though, as Monero won't add a dummy if there's // more than two outputs if outputs.len() <= 2 { let mut rand = [0; 8]; rng.fill_bytes(&mut rand); id = id.or(Some(rand)); } (tx_public_key, additional_keys, outputs, id) } #[allow(non_snake_case)] fn extra( tx_key: EdwardsPoint, additional: bool, Rs: Vec, id: Option<[u8; 8]>, data: &mut Vec>, ) -> Vec { #[allow(non_snake_case)] let Rs_len = Rs.len(); let mut extra = Extra::new(tx_key, if additional { Rs } else { vec![] }); if let Some(id) = id { let mut id_vec = Vec::with_capacity(1 + 8); PaymentId::Encrypted(id).write(&mut id_vec).unwrap(); extra.push(ExtraField::Nonce(id_vec)); } // Include data if present let extra_len = Extra::fee_weight(Rs_len, additional, id.is_some(), data.as_ref()); for part in data.drain(..) { let mut arb = vec![ARBITRARY_DATA_MARKER]; arb.extend(part); extra.push(ExtraField::Nonce(arb)); } let mut serialized = Vec::with_capacity(extra_len); extra.write(&mut serialized).unwrap(); debug_assert_eq!(extra_len, serialized.len()); serialized } /// Returns the eventuality of this transaction. /// /// The eventuality is defined as the TX extra/outputs this transaction will create, if signed /// with the specified seed. This eventuality can be compared to on-chain transactions to see /// if the transaction has already been signed and published. pub fn eventuality(&self) -> Option { let inputs = self.inputs.iter().map(|(input, _)| input.key()).collect::>(); let (tx_key, additional, outputs, id) = Self::prepare_payments( self.r_seed.as_ref()?, &inputs, &mut self.payments.clone(), // Lie about the uniqueness, used when determining output keys/commitments yet not the // ephemeral keys, which is want we want here // While we do still grab the outputs variable, it's so we can get its Rs [0; 32], ); #[allow(non_snake_case)] let Rs = outputs.iter().map(|output| output.R).collect(); drop(outputs); let additional = !additional.is_empty(); let extra = Self::extra(tx_key, additional, Rs, id, &mut self.data.clone()); Some(Eventuality { protocol: self.protocol, r_seed: self.r_seed.clone()?, inputs, payments: self.payments.clone(), extra, }) } fn prepare_transaction( &mut self, rng: &mut R, uniqueness: [u8; 32], ) -> (Transaction, Scalar) { // If no seed for the ephemeral keys was provided, make one let r_seed = self.r_seed.clone().unwrap_or_else(|| { let mut res = Zeroizing::new([0; 32]); rng.fill_bytes(res.as_mut()); res }); let (tx_key, additional, outputs, id) = Self::prepare_payments( &r_seed, &self.inputs.iter().map(|(input, _)| input.key()).collect::>(), &mut self.payments, uniqueness, ); // This function only cares if additional keys were necessary, not what they were let additional = !additional.is_empty(); let commitments = outputs.iter().map(|output| output.commitment.clone()).collect::>(); let sum = commitments.iter().map(|commitment| commitment.mask).sum(); // Safe due to the constructor checking MAX_OUTPUTS let bp = Bulletproofs::prove(rng, &commitments, self.protocol.bp_plus()).unwrap(); // Create the TX extra let extra = Self::extra( tx_key, additional, outputs.iter().map(|output| output.R).collect(), id, &mut self.data, ); let mut fee = self.inputs.iter().map(|(input, _)| input.commitment().amount).sum::(); let mut tx_outputs = Vec::with_capacity(outputs.len()); let mut encrypted_amounts = Vec::with_capacity(outputs.len()); for output in &outputs { fee -= output.commitment.amount; tx_outputs.push(Output { amount: None, key: output.dest.compress(), view_tag: Some(output.view_tag).filter(|_| self.protocol.view_tags()), }); encrypted_amounts.push(EncryptedAmount::Compact { amount: output.amount }); } debug_assert_eq!(self.fee, fee, "transaction will use an unexpected fee"); ( Transaction { prefix: TransactionPrefix { version: 2, timelock: Timelock::None, inputs: vec![], outputs: tx_outputs, extra, }, signatures: vec![], rct_signatures: RctSignatures { base: RctBase { fee, encrypted_amounts, pseudo_outs: vec![], commitments: commitments.iter().map(Commitment::calculate).collect(), }, prunable: RctPrunable::Clsag { bulletproofs: bp, clsags: vec![], pseudo_outs: vec![] }, }, }, sum, ) } /// Sign this transaction. pub fn sign( mut self, rng: &mut R, spend: &Zeroizing, ) -> Result { let mut images = Vec::with_capacity(self.inputs.len()); for (input, _) in &self.inputs { let mut offset = Zeroizing::new(spend.deref() + input.key_offset()); if (offset.deref() * ED25519_BASEPOINT_TABLE) != input.key() { Err(TransactionError::WrongPrivateKey)?; } images.push(generate_key_image(&offset)); offset.zeroize(); } images.sort_by(key_image_sort); let (mut tx, mask_sum) = self.prepare_transaction( rng, uniqueness( &images .iter() .map(|image| Input::ToKey { amount: None, key_offsets: vec![], key_image: *image }) .collect::>(), ), ); let signable = prepare_inputs(&self.inputs, spend, &mut tx)?; let clsag_pairs = Clsag::sign(rng, signable, mask_sum, tx.signature_hash()); match tx.rct_signatures.prunable { RctPrunable::Null => panic!("Signing for RctPrunable::Null"), RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => { clsags.append(&mut clsag_pairs.iter().map(|clsag| clsag.0.clone()).collect::>()); pseudo_outs.append(&mut clsag_pairs.iter().map(|clsag| clsag.1).collect::>()); } _ => unreachable!("attempted to sign a TX which wasn't CLSAG"), } debug_assert_eq!( self.fee_rate.calculate_fee_from_weight(tx.weight()), tx.rct_signatures.base.fee, "transaction used unexpected fee", ); Ok(tx) } } impl Eventuality { /// Enables building a HashMap of Extra -> Eventuality for efficiently checking if an on-chain /// transaction may match this eventuality. /// /// This extra is cryptographically bound to: /// 1) A specific set of inputs (via their output key) /// 2) A specific seed for the ephemeral keys /// /// This extra may be used in a transaction with a distinct set of inputs, yet no honest /// transaction which doesn't satisfy this Eventuality will contain it. pub fn extra(&self) -> &[u8] { &self.extra } #[must_use] pub fn matches(&self, tx: &Transaction) -> bool { if self.payments.len() != tx.prefix.outputs.len() { return false; } // Verify extra. // Even if all the outputs were correct, a malicious extra could still cause a recipient to // fail to receive their funds. // This is the cheapest check available to perform as it does not require TX-specific ECC ops. if self.extra != tx.prefix.extra { return false; } // Also ensure no timelock was set. if tx.prefix.timelock != Timelock::None { return false; } // Generate the outputs. This is TX-specific due to uniqueness. let (_, _, outputs, _) = SignableTransaction::prepare_payments( &self.r_seed, &self.inputs, &mut self.payments.clone(), uniqueness(&tx.prefix.inputs), ); let rct_type = tx.rct_signatures.rct_type(); if rct_type != self.protocol.optimal_rct_type() { return false; } // TODO: Remove this when the following for loop is updated assert!( rct_type.compact_encrypted_amounts(), "created an Eventuality for a very old RctType we don't support proving for" ); for (o, (expected, actual)) in outputs.iter().zip(tx.prefix.outputs.iter()).enumerate() { // Verify the output, commitment, and encrypted amount. if (&Output { amount: None, key: expected.dest.compress(), view_tag: Some(expected.view_tag).filter(|_| self.protocol.view_tags()), } != actual) || (Some(&expected.commitment.calculate()) != tx.rct_signatures.base.commitments.get(o)) || (Some(&EncryptedAmount::Compact { amount: expected.amount }) != tx.rct_signatures.base.encrypted_amounts.get(o)) { return false; } } true } pub fn write(&self, w: &mut W) -> io::Result<()> { self.protocol.write(w)?; write_raw_vec(write_byte, self.r_seed.as_ref(), w)?; write_vec(write_point, &self.inputs, w)?; fn write_payment(payment: &InternalPayment, w: &mut W) -> io::Result<()> { match payment { InternalPayment::Payment(payment) => { w.write_all(&[0])?; write_vec(write_byte, payment.0.to_string().as_bytes(), w)?; w.write_all(&payment.1.to_le_bytes()) } InternalPayment::Change(change, amount) => { w.write_all(&[1])?; write_vec(write_byte, change.address.to_string().as_bytes(), w)?; if let Some(view) = change.view.as_ref() { w.write_all(&[1])?; write_scalar(view, w)?; } else { w.write_all(&[0])?; } w.write_all(&amount.to_le_bytes()) } } } write_vec(write_payment, &self.payments, w)?; write_vec(write_byte, &self.extra, w) } pub fn serialize(&self) -> Vec { let mut buf = Vec::with_capacity(128); self.write(&mut buf).unwrap(); buf } pub fn read(r: &mut R) -> io::Result { fn read_address(r: &mut R) -> io::Result { String::from_utf8(read_vec(read_byte, r)?) .ok() .and_then(|str| MoneroAddress::from_str_raw(&str).ok()) .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid address")) } fn read_payment(r: &mut R) -> io::Result { Ok(match read_byte(r)? { 0 => InternalPayment::Payment((read_address(r)?, read_u64(r)?)), 1 => InternalPayment::Change( Change { address: read_address(r)?, view: match read_byte(r)? { 0 => None, 1 => Some(Zeroizing::new(read_scalar(r)?)), _ => Err(io::Error::new(io::ErrorKind::Other, "invalid change payment"))?, }, }, read_u64(r)?, ), _ => Err(io::Error::new(io::ErrorKind::Other, "invalid payment"))?, }) } Ok(Eventuality { protocol: Protocol::read(r)?, r_seed: Zeroizing::new(read_bytes::<_, 32>(r)?), inputs: read_vec(read_point, r)?, payments: read_vec(read_payment, r)?, extra: read_vec(read_byte, r)?, }) } }