monero_wallet/send/
tx_keys.rs1use 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 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 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 input_keys_and_commitments
45 .sort_by(|(key_a, _commitment_a), (key_b, _commitment_b)| key_image_sort(key_a, key_b));
46
47 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
59pub struct TransactionKeys(ChaCha20Rng);
64impl Drop for TransactionKeys {
65 fn drop(&mut self) {
66 let mut seed = [0; 64];
68 let _result = self.0.try_fill_bytes(&mut seed);
69 self.0.set_stream(0);
71 self.0.set_word_pos(0);
72 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 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 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 has_payments_to_subaddresses && !((self.payments.len() == 2) && has_change_view)
144 }
145
146 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 InternalPayment::Payment(_, _) |
178 InternalPayment::Change(ChangeEnum::AddressOnly { .. }) => {
179 Zeroizing::new(key_to_use.deref() * addr.view().into())
180 }
181 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 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 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 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 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 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 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}