Skip to main content

monero_wallet/send/
tx.rs

1use std_shims::{vec, vec::Vec};
2
3#[cfg(feature = "compile-time-generators")]
4use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
5#[cfg(not(feature = "compile-time-generators"))]
6use curve25519_dalek::constants::ED25519_BASEPOINT_POINT as ED25519_BASEPOINT_TABLE;
7
8use crate::{
9  io::VarInt,
10  ed25519::*,
11  ringct::{
12    clsag::Clsag, bulletproofs::Bulletproof, EncryptedAmount, RctType, RctBase, RctPrunable,
13    RctProofs,
14  },
15  transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
16  extra::{ARBITRARY_DATA_MARKER, PaymentId, Extra},
17  send::{InternalPayment, SignableTransaction, SignableTransactionWithKeyImages},
18};
19
20impl SignableTransaction {
21  // Output the inputs for this transaction.
22  pub(crate) fn inputs(&self, key_images: &[CompressedPoint]) -> Vec<Input> {
23    debug_assert_eq!(self.inputs.len(), key_images.len());
24
25    let mut res = Vec::with_capacity(self.inputs.len());
26    for (input, key_image) in self.inputs.iter().zip(key_images) {
27      res.push(Input::ToKey {
28        amount: None,
29        key_offsets: input.decoys().offsets().to_vec(),
30        key_image: *key_image,
31      });
32    }
33    res
34  }
35
36  // Output the outputs for this transaction.
37  pub(crate) fn outputs(&self, key_images: &[CompressedPoint]) -> Vec<Output> {
38    let shared_key_derivations = self.shared_key_derivations(key_images);
39    debug_assert_eq!(self.payments.len(), shared_key_derivations.len());
40
41    let mut res = Vec::with_capacity(self.payments.len());
42    for (payment, shared_key_derivations) in self.payments.iter().zip(&shared_key_derivations) {
43      let key = (&shared_key_derivations.shared_key.into() * ED25519_BASEPOINT_TABLE) +
44        payment.address().spend().into();
45      res.push(Output {
46        key: Point::from(key).compress(),
47        amount: None,
48        view_tag: (match self.rct_type {
49          RctType::ClsagBulletproof => false,
50          RctType::ClsagBulletproofPlus => true,
51          RctType::AggregateMlsagBorromean |
52          RctType::MlsagBorromean |
53          RctType::MlsagBulletproofs |
54          RctType::MlsagBulletproofsCompactAmount => panic!("unsupported RctType"),
55        })
56        .then_some(shared_key_derivations.view_tag),
57      });
58    }
59    res
60  }
61
62  // Calculate the TX extra for this transaction.
63  pub(crate) fn extra(&self) -> Vec<u8> {
64    let (tx_key, additional_keys) = self.transaction_keys_pub();
65    debug_assert!(additional_keys.is_empty() || (additional_keys.len() == self.payments.len()));
66    let payment_id_xors = self.payment_id_xors();
67    debug_assert_eq!(self.payments.len(), payment_id_xors.len());
68
69    let amount_of_keys = 1 + additional_keys.len();
70    let mut extra = Extra::new(tx_key.compress(), additional_keys);
71
72    if let Some((id, id_xor)) =
73      self.payments.iter().zip(&payment_id_xors).find_map(|(payment, payment_id_xor)| {
74        payment.address().payment_id().map(|id| (id, payment_id_xor))
75      })
76    {
77      let id = (u64::from_le_bytes(id) ^ u64::from_le_bytes(*id_xor)).to_le_bytes();
78      let mut id_vec = Vec::with_capacity(1 + 8);
79      PaymentId::Encrypted(id)
80        .write(&mut id_vec)
81        .expect("write failed but <Vec as io::Write> doesn't fail");
82      extra.push_nonce(id_vec);
83    } else {
84      /*
85        If there's no payment ID, we push a dummy (as wallet2 does) to the first payment.
86
87        This does cause a random payment ID for the other recipient (a documented fingerprint).
88        Functionally, random payment IDs should be fine as wallet2 will trigger this same behavior
89        (a random payment ID being seen by the recipient) with a batch send if one of the recipient
90        addresses has a payment ID.
91
92        The alternative would be to not include any payment ID, fingerprinting to the entire
93        blockchain this is non-standard wallet software (instead of just a single recipient).
94      */
95      if self.payments.len() == 2 {
96        let (_, payment_id_xor) = self
97          .payments
98          .iter()
99          .zip(&payment_id_xors)
100          .find(|(payment, _)| matches!(payment, InternalPayment::Payment(_, _)))
101          .expect("multiple change outputs?");
102        let mut id_vec = Vec::with_capacity(1 + 8);
103        // The dummy payment ID is [0; 8], which when xor'd with the mask, is just the mask
104        PaymentId::Encrypted(*payment_id_xor)
105          .write(&mut id_vec)
106          .expect("write failed but <Vec as io::Write> doesn't fail");
107        extra.push_nonce(id_vec);
108      }
109    }
110
111    // Include data if present
112    for part in &self.data {
113      let mut arb = vec![ARBITRARY_DATA_MARKER];
114      arb.extend(part);
115      extra.push_nonce(arb);
116    }
117
118    let mut serialized = Vec::with_capacity(32 * amount_of_keys);
119    extra.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
120    serialized
121  }
122
123  pub(crate) fn weight_and_necessary_fee(&self) -> (usize, u128) {
124    /*
125      This transaction is variable length to:
126        - The decoy offsets (fixed)
127        - The TX extra (variable to key images, requiring an interactive protocol)
128
129      Thankfully, the TX extra *length* is fixed. Accordingly, we can calculate the inevitable TX's
130      weight at this time with a shimmed transaction.
131    */
132    let base_weight = {
133      let mut key_images = Vec::with_capacity(self.inputs.len());
134      let mut clsags = Vec::with_capacity(self.inputs.len());
135      let mut pseudo_outs = Vec::with_capacity(self.inputs.len());
136      for _ in &self.inputs {
137        key_images.push(CompressedPoint::G);
138        clsags.push(Clsag {
139          D: CompressedPoint::G,
140          s: vec![
141            Scalar::ZERO;
142            match self.rct_type {
143              RctType::ClsagBulletproof => 11,
144              RctType::ClsagBulletproofPlus => 16,
145              RctType::AggregateMlsagBorromean |
146              RctType::MlsagBorromean |
147              RctType::MlsagBulletproofs |
148              RctType::MlsagBulletproofsCompactAmount => unreachable!("unsupported RCT type"),
149            }
150          ],
151          c1: Scalar::ZERO,
152        });
153        pseudo_outs.push(CompressedPoint::G);
154      }
155      let mut encrypted_amounts = Vec::with_capacity(self.payments.len());
156      let mut bp_commitments = Vec::with_capacity(self.payments.len());
157      let mut commitments = Vec::with_capacity(self.payments.len());
158      for _ in &self.payments {
159        encrypted_amounts.push(EncryptedAmount::Compact { amount: [0; 8] });
160        bp_commitments.push(Commitment::zero());
161        commitments.push(CompressedPoint::G);
162      }
163
164      let padded_log2 = {
165        let mut log2_find = 0;
166        while (1 << log2_find) < self.payments.len() {
167          log2_find += 1;
168        }
169        log2_find
170      };
171      // This is log2 the padded amount of IPA rows
172      // We have 64 rows per commitment, so we need 64 * c IPA rows
173      // We rewrite this as 2**6 * c
174      // By finding the padded log2 of c, we get 2**6 * 2**p
175      // This declares the log2 to be 6 + p
176      let lr_len = 6 + padded_log2;
177
178      let bulletproof = match self.rct_type {
179        RctType::ClsagBulletproof => {
180          let mut bp = Vec::with_capacity(((9 + (2 * lr_len)) * 32) + 2);
181          let push_point = |bp: &mut Vec<u8>| {
182            bp.push(1);
183            bp.extend([0; 31]);
184          };
185          let push_scalar = |bp: &mut Vec<u8>| bp.extend([0; 32]);
186          for _ in 0 .. 4 {
187            push_point(&mut bp);
188          }
189          for _ in 0 .. 2 {
190            push_scalar(&mut bp);
191          }
192          for _ in 0 .. 2 {
193            VarInt::write(&lr_len, &mut bp)
194              .expect("write failed but <Vec as io::Write> doesn't fail");
195            for _ in 0 .. lr_len {
196              push_point(&mut bp);
197            }
198          }
199          for _ in 0 .. 3 {
200            push_scalar(&mut bp);
201          }
202          Bulletproof::read(&mut bp.as_slice()).expect("made an invalid dummy BP")
203        }
204        RctType::ClsagBulletproofPlus => {
205          let mut bp = Vec::with_capacity(((6 + (2 * lr_len)) * 32) + 2);
206          let push_point = |bp: &mut Vec<u8>| {
207            bp.push(1);
208            bp.extend([0; 31]);
209          };
210          let push_scalar = |bp: &mut Vec<u8>| bp.extend([0; 32]);
211          for _ in 0 .. 3 {
212            push_point(&mut bp);
213          }
214          for _ in 0 .. 3 {
215            push_scalar(&mut bp);
216          }
217          for _ in 0 .. 2 {
218            VarInt::write(&lr_len, &mut bp)
219              .expect("write failed but <Vec as io::Write> doesn't fail");
220            for _ in 0 .. lr_len {
221              push_point(&mut bp);
222            }
223          }
224          Bulletproof::read_plus(&mut bp.as_slice()).expect("made an invalid dummy BP+")
225        }
226        RctType::AggregateMlsagBorromean |
227        RctType::MlsagBorromean |
228        RctType::MlsagBulletproofs |
229        RctType::MlsagBulletproofsCompactAmount => panic!("unsupported RctType"),
230      };
231
232      // `- 1` to remove the one byte for the 0 fee
233      Transaction::V2 {
234        prefix: TransactionPrefix {
235          additional_timelock: Timelock::None,
236          inputs: self.inputs(&key_images),
237          outputs: self.outputs(&key_images),
238          extra: self.extra(),
239        },
240        proofs: Some(RctProofs {
241          base: RctBase { fee: 0, encrypted_amounts, pseudo_outs: vec![], commitments },
242          prunable: RctPrunable::Clsag { bulletproof, clsags, pseudo_outs },
243        }),
244      }
245      .weight() -
246        1
247    };
248
249    // We now have the base weight, without the fee encoded
250    // The fee itself will impact the weight as its encoding takes up a variable amount of bytes
251    let mut possible_weights = Vec::with_capacity(<u64 as VarInt>::UPPER_BOUND);
252    // Assert LOWER_BOUND == 1, which this code assumes
253    const _LOWER_BOUND_IS_LTE_ONE: [(); 1 - <u64 as VarInt>::LOWER_BOUND] = [(); _];
254    const _LOWER_BOUND_IS_GTE_ONE: [(); <u64 as VarInt>::LOWER_BOUND - 1] = [(); _];
255    for i in <u64 as VarInt>::LOWER_BOUND ..= <u64 as VarInt>::UPPER_BOUND {
256      possible_weights.push(base_weight + i);
257    }
258
259    // We now calculate the fee which would be used for each weight
260    let mut possible_fees = Vec::with_capacity(<u64 as VarInt>::UPPER_BOUND);
261    for weight in possible_weights {
262      possible_fees.push(self.fee_rate.calculate_fee_from_weight(
263        u64::try_from(weight).expect("candidate weight exceeded `u64::MAX`"),
264      ));
265    }
266
267    // We now look for the fee whose length matches the length used to derive it
268    let mut weight_and_fee = None;
269    for (fee_len, possible_fee) in possible_fees.into_iter().enumerate() {
270      // Increment by one as the enumeration is zero-indexed
271      let fee_len = 1 + fee_len;
272
273      /*
274        We use the first fee whose encoded length is not larger than the weight assigned by this
275        iteration. This should be because the length is equal to the weight, yet means if somehow
276        none are equal, this will still terminate successfully if any fee's length is less than
277        the weight of this iteration (which does iterate up to and include the upper bound).
278
279        The saturating into a `u64` means for `u128` fees, this will return a weight lesser than
280        reality, but such fees are invalid regardless (Garbage In, Garbage Out).
281      */
282      if u64::try_from(possible_fee).unwrap_or(u64::MAX).varint_len() <= fee_len {
283        weight_and_fee = Some((base_weight + fee_len, possible_fee));
284        break;
285      }
286    }
287    weight_and_fee
288      .expect("length of highest possible fee was greater than highest possible fee length")
289  }
290}
291
292impl SignableTransactionWithKeyImages {
293  pub(crate) fn transaction_without_signatures(&self) -> Transaction {
294    let commitments_and_encrypted_amounts =
295      self.intent.commitments_and_encrypted_amounts(&self.key_images);
296    let mut commitments = Vec::with_capacity(self.intent.payments.len());
297    let mut bp_commitments = Vec::with_capacity(self.intent.payments.len());
298    let mut encrypted_amounts = Vec::with_capacity(self.intent.payments.len());
299    for (commitment, encrypted_amount) in commitments_and_encrypted_amounts {
300      commitments.push(commitment.commit().compress());
301      bp_commitments.push(commitment);
302      encrypted_amounts.push(encrypted_amount);
303    }
304    let bulletproof = {
305      let mut bp_rng = self.intent.seeded_rng(b"bulletproof");
306      (match self.intent.rct_type {
307        RctType::ClsagBulletproof => Bulletproof::prove(&mut bp_rng, bp_commitments),
308        RctType::ClsagBulletproofPlus => Bulletproof::prove_plus(&mut bp_rng, bp_commitments),
309        RctType::AggregateMlsagBorromean |
310        RctType::MlsagBorromean |
311        RctType::MlsagBulletproofs |
312        RctType::MlsagBulletproofsCompactAmount => panic!("unsupported RctType"),
313      })
314      .expect("couldn't prove BP(+)s for this many payments despite checking in constructor?")
315    };
316
317    Transaction::V2 {
318      prefix: TransactionPrefix {
319        additional_timelock: Timelock::None,
320        inputs: self.intent.inputs(&self.key_images),
321        outputs: self.intent.outputs(&self.key_images),
322        extra: self.intent.extra(),
323      },
324      proofs: Some(RctProofs {
325        base: RctBase {
326          fee: if self
327            .intent
328            .payments
329            .iter()
330            .any(|payment| matches!(payment, InternalPayment::Change(_)))
331          {
332            // The necessary fee is the fee
333            u64::try_from(self.intent.weight_and_necessary_fee().1).unwrap()
334          } else {
335            // If we don't have a change output, the difference is the fee
336            let inputs =
337              self.intent.inputs.iter().map(|input| input.commitment().amount).sum::<u64>();
338            let payments = self
339              .intent
340              .payments
341              .iter()
342              .filter_map(|payment| match payment {
343                InternalPayment::Payment(_, amount) => Some(amount),
344                InternalPayment::Change(_) => None,
345              })
346              .sum::<u64>();
347            // Safe since the constructor checks inputs >= (payments + fee)
348            inputs - payments
349          },
350          encrypted_amounts,
351          pseudo_outs: vec![],
352          commitments,
353        },
354        prunable: RctPrunable::Clsag { bulletproof, clsags: vec![], pseudo_outs: vec![] },
355      }),
356    }
357  }
358}