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