monero_wallet/send/
tx_keys.rs

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