monero_wallet/send/
tx_keys.rs

1use core::ops::Deref;
2use std_shims::{vec, vec::Vec};
3
4use zeroize::{Zeroize, Zeroizing};
5
6use rand_core::SeedableRng;
7use rand_chacha::ChaCha20Rng;
8
9use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint};
10
11use crate::{
12  primitives::{keccak256, Commitment},
13  ringct::EncryptedAmount,
14  SharedKeyDerivations, OutputWithDecoys,
15  send::{ChangeEnum, InternalPayment, SignableTransaction, key_image_sort},
16};
17
18fn seeded_rng(
19  dst: &'static [u8],
20  outgoing_view_key: &[u8; 32],
21  mut input_keys: Vec<EdwardsPoint>,
22) -> ChaCha20Rng {
23  // Apply the DST
24  let mut transcript = Zeroizing::new(vec![
25    u8::try_from(dst.len()).expect("internal RNG with constant DST had a too-long DST specified")
26  ]);
27  transcript.extend(dst);
28
29  // Bind to the outgoing view key to prevent foreign entities from rebuilding the transcript
30  transcript.extend(outgoing_view_key);
31
32  // We sort the inputs here to ensure a consistent order
33  // We use the key image sort as it's applicable and well-defined, not because these are key
34  // images
35  input_keys.sort_by(key_image_sort);
36
37  // Ensure uniqueness across transactions by binding to a use-once object
38  // The keys for the inputs is binding to their key images, making them use-once
39  for key in input_keys {
40    transcript.extend(key.compress().to_bytes());
41  }
42
43  let res = ChaCha20Rng::from_seed(keccak256(&transcript));
44  transcript.zeroize();
45  res
46}
47
48/// An iterator yielding an endless amount of ephemeral keys to use within a transaction.
49///
50/// This is used when sending and can be used after sending to re-derive the keys used, as
51/// necessary for payment proofs.
52pub struct TransactionKeys(ChaCha20Rng);
53impl TransactionKeys {
54  /// Construct a new `TransactionKeys`.
55  ///
56  /// `input_keys` is the list of keys from the outputs spent within this transaction.
57  pub fn new(outgoing_view_key: &Zeroizing<[u8; 32]>, input_keys: Vec<EdwardsPoint>) -> Self {
58    Self(seeded_rng(b"transaction_keys", outgoing_view_key, input_keys))
59  }
60}
61impl Iterator for TransactionKeys {
62  type Item = Zeroizing<Scalar>;
63  fn next(&mut self) -> Option<Self::Item> {
64    Some(Zeroizing::new(Scalar::random(&mut self.0)))
65  }
66}
67
68impl SignableTransaction {
69  fn input_keys(&self) -> Vec<EdwardsPoint> {
70    self.inputs.iter().map(OutputWithDecoys::key).collect()
71  }
72
73  pub(crate) fn seeded_rng(&self, dst: &'static [u8]) -> ChaCha20Rng {
74    seeded_rng(dst, &self.outgoing_view_key, self.input_keys())
75  }
76
77  fn has_payments_to_subaddresses(&self) -> bool {
78    self.payments.iter().any(|payment| match payment {
79      InternalPayment::Payment(addr, _) => addr.is_subaddress(),
80      InternalPayment::Change(change) => match change {
81        ChangeEnum::AddressOnly(addr) => addr.is_subaddress(),
82        // These aren't considered payments to subaddresses as we don't need to send to them as
83        // subaddresses
84        // We can calculate the shared key using the view key, as if we were receiving, instead
85        ChangeEnum::Standard { .. } | ChangeEnum::Guaranteed { .. } => false,
86      },
87    })
88  }
89
90  fn should_use_additional_keys(&self) -> bool {
91    let has_payments_to_subaddresses = self.has_payments_to_subaddresses();
92    if !has_payments_to_subaddresses {
93      return false;
94    }
95
96    let has_change_view = self.payments.iter().any(|payment| match payment {
97      InternalPayment::Payment(_, _) => false,
98      InternalPayment::Change(change) => match change {
99        ChangeEnum::AddressOnly(_) => false,
100        ChangeEnum::Standard { .. } | ChangeEnum::Guaranteed { .. } => true,
101      },
102    });
103
104    /*
105      If sending to a subaddress, the shared key is not `rG` yet `rB`. Because of this, a
106      per-subaddress shared key is necessary, causing the usage of additional keys.
107
108      The one exception is if we're sending to a subaddress in a 2-output transaction. The second
109      output, the change output, will attempt scanning the singular key `rB` with `v rB`. While we
110      cannot calculate `r vB` with just `r` (as that'd require `vB` when we presumably only have
111      `vG` when sending), since we do in fact have `v` (due to it being our own view key for our
112      change output), we can still calculate the shared secret.
113    */
114    has_payments_to_subaddresses && !((self.payments.len() == 2) && has_change_view)
115  }
116
117  // Calculate the transaction keys used as randomness.
118  fn transaction_keys(&self) -> (Zeroizing<Scalar>, Vec<Zeroizing<Scalar>>) {
119    let mut tx_keys = TransactionKeys::new(&self.outgoing_view_key, self.input_keys());
120
121    let tx_key = tx_keys.next().expect("TransactionKeys (never-ending) was exhausted");
122
123    let mut additional_keys = vec![];
124    if self.should_use_additional_keys() {
125      for _ in 0 .. self.payments.len() {
126        additional_keys.push(tx_keys.next().expect("TransactionKeys (never-ending) was exhausted"));
127      }
128    }
129    (tx_key, additional_keys)
130  }
131
132  fn ecdhs(&self) -> Vec<Zeroizing<EdwardsPoint>> {
133    let (tx_key, additional_keys) = self.transaction_keys();
134    debug_assert!(additional_keys.is_empty() || (additional_keys.len() == self.payments.len()));
135    let (tx_key_pub, additional_keys_pub) = self.transaction_keys_pub();
136    debug_assert_eq!(additional_keys_pub.len(), additional_keys.len());
137
138    let mut res = Vec::with_capacity(self.payments.len());
139    for (i, payment) in self.payments.iter().enumerate() {
140      let addr = payment.address();
141      let key_to_use =
142        if addr.is_subaddress() { additional_keys.get(i).unwrap_or(&tx_key) } else { &tx_key };
143
144      let ecdh = match payment {
145        // If we don't have the view key, use the key dedicated for this address (r A)
146        InternalPayment::Payment(_, _) |
147        InternalPayment::Change(ChangeEnum::AddressOnly { .. }) => {
148          Zeroizing::new(key_to_use.deref() * addr.view())
149        }
150        // If we do have the view key, use the commitment to the key (a R)
151        InternalPayment::Change(ChangeEnum::Standard { view_pair, .. }) => {
152          Zeroizing::new(view_pair.view.deref() * tx_key_pub)
153        }
154        InternalPayment::Change(ChangeEnum::Guaranteed { view_pair, .. }) => {
155          Zeroizing::new(view_pair.0.view.deref() * tx_key_pub)
156        }
157      };
158
159      res.push(ecdh);
160    }
161    res
162  }
163
164  // Calculate the shared keys and the necessary derivations.
165  pub(crate) fn shared_key_derivations(
166    &self,
167    key_images: &[EdwardsPoint],
168  ) -> Vec<Zeroizing<SharedKeyDerivations>> {
169    let ecdhs = self.ecdhs();
170
171    let uniqueness = SharedKeyDerivations::uniqueness(&self.inputs(key_images));
172
173    let mut res = Vec::with_capacity(self.payments.len());
174    for (i, (payment, ecdh)) in self.payments.iter().zip(ecdhs).enumerate() {
175      let addr = payment.address();
176      res.push(SharedKeyDerivations::output_derivations(
177        addr.is_guaranteed().then_some(uniqueness),
178        ecdh,
179        i,
180      ));
181    }
182    res
183  }
184
185  // Calculate the payment ID XOR masks.
186  pub(crate) fn payment_id_xors(&self) -> Vec<[u8; 8]> {
187    let mut res = Vec::with_capacity(self.payments.len());
188    for ecdh in self.ecdhs() {
189      res.push(SharedKeyDerivations::payment_id_xor(ecdh));
190    }
191    res
192  }
193
194  // Calculate the transaction_keys' commitments.
195  //
196  // These depend on the payments. Commitments for payments to subaddresses use the spend key for
197  // the generator.
198  pub(crate) fn transaction_keys_pub(&self) -> (EdwardsPoint, Vec<EdwardsPoint>) {
199    let (tx_key, additional_keys) = self.transaction_keys();
200    debug_assert!(additional_keys.is_empty() || (additional_keys.len() == self.payments.len()));
201
202    // The single transaction key uses the subaddress's spend key as its generator
203    let has_payments_to_subaddresses = self.has_payments_to_subaddresses();
204    let should_use_additional_keys = self.should_use_additional_keys();
205    if has_payments_to_subaddresses && (!should_use_additional_keys) {
206      debug_assert_eq!(additional_keys.len(), 0);
207
208      let InternalPayment::Payment(addr, _) = self
209        .payments
210        .iter()
211        .find(|payment| matches!(payment, InternalPayment::Payment(_, _)))
212        .expect("payment to subaddress yet no payment")
213      else {
214        panic!("filtered payment wasn't a payment")
215      };
216
217      return (tx_key.deref() * addr.spend(), vec![]);
218    }
219
220    if should_use_additional_keys {
221      let mut additional_keys_pub = vec![];
222      for (additional_key, payment) in additional_keys.into_iter().zip(&self.payments) {
223        let addr = payment.address();
224        // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
225        //   /src/device/device_default.cpp#L308-L312
226        if addr.is_subaddress() {
227          additional_keys_pub.push(additional_key.deref() * addr.spend());
228        } else {
229          additional_keys_pub.push(additional_key.deref() * ED25519_BASEPOINT_TABLE)
230        }
231      }
232      return (tx_key.deref() * ED25519_BASEPOINT_TABLE, additional_keys_pub);
233    }
234
235    debug_assert!(!has_payments_to_subaddresses);
236    debug_assert!(!should_use_additional_keys);
237    (tx_key.deref() * ED25519_BASEPOINT_TABLE, vec![])
238  }
239
240  pub(crate) fn commitments_and_encrypted_amounts(
241    &self,
242    key_images: &[EdwardsPoint],
243  ) -> Vec<(Commitment, EncryptedAmount)> {
244    let shared_key_derivations = self.shared_key_derivations(key_images);
245
246    let mut res = Vec::with_capacity(self.payments.len());
247    for (payment, shared_key_derivations) in self.payments.iter().zip(shared_key_derivations) {
248      let amount = match payment {
249        InternalPayment::Payment(_, amount) => *amount,
250        InternalPayment::Change(_) => {
251          let inputs = self.inputs.iter().map(|input| input.commitment().amount).sum::<u64>();
252          let payments = self
253            .payments
254            .iter()
255            .filter_map(|payment| match payment {
256              InternalPayment::Payment(_, amount) => Some(amount),
257              InternalPayment::Change(_) => None,
258            })
259            .sum::<u64>();
260          let necessary_fee = self.weight_and_necessary_fee().1;
261          // Safe since the constructor checked this TX has enough funds for itself
262          inputs - (payments + necessary_fee)
263        }
264      };
265      let commitment = Commitment::new(shared_key_derivations.commitment_mask(), amount);
266      let encrypted_amount = EncryptedAmount::Compact {
267        amount: shared_key_derivations.compact_amount_encryption(amount),
268      };
269      res.push((commitment, encrypted_amount));
270    }
271    res
272  }
273
274  pub(crate) fn sum_output_masks(&self, key_images: &[EdwardsPoint]) -> Scalar {
275    self
276      .commitments_and_encrypted_amounts(key_images)
277      .into_iter()
278      .map(|(commitment, _)| commitment.mask)
279      .sum()
280  }
281}