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, u64) {
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(weight));
263    }
264
265    // We now look for the fee whose length matches the length used to derive it
266    let mut weight_and_fee = None;
267    for (fee_len, possible_fee) in possible_fees.into_iter().enumerate() {
268      // Increment by one as the enumeration is zero-indexed
269      let fee_len = 1 + fee_len;
270
271      // We use the first fee whose encoded length is not larger than the length used within this
272      // weight
273      // This should be because the lengths are equal, yet means if somehow none are equal, this
274      // will still terminate successfully
275      if possible_fee.varint_len() <= fee_len {
276        weight_and_fee = Some((base_weight + fee_len, possible_fee));
277        break;
278      }
279    }
280    weight_and_fee
281      .expect("length of highest possible fee was greater than highest possible fee length")
282  }
283}
284
285impl SignableTransactionWithKeyImages {
286  pub(crate) fn transaction_without_signatures(&self) -> Transaction {
287    let commitments_and_encrypted_amounts =
288      self.intent.commitments_and_encrypted_amounts(&self.key_images);
289    let mut commitments = Vec::with_capacity(self.intent.payments.len());
290    let mut bp_commitments = Vec::with_capacity(self.intent.payments.len());
291    let mut encrypted_amounts = Vec::with_capacity(self.intent.payments.len());
292    for (commitment, encrypted_amount) in commitments_and_encrypted_amounts {
293      commitments.push(commitment.commit().compress());
294      bp_commitments.push(commitment);
295      encrypted_amounts.push(encrypted_amount);
296    }
297    let bulletproof = {
298      let mut bp_rng = self.intent.seeded_rng(b"bulletproof");
299      (match self.intent.rct_type {
300        RctType::ClsagBulletproof => Bulletproof::prove(&mut bp_rng, bp_commitments),
301        RctType::ClsagBulletproofPlus => Bulletproof::prove_plus(&mut bp_rng, bp_commitments),
302        RctType::AggregateMlsagBorromean |
303        RctType::MlsagBorromean |
304        RctType::MlsagBulletproofs |
305        RctType::MlsagBulletproofsCompactAmount => panic!("unsupported RctType"),
306      })
307      .expect("couldn't prove BP(+)s for this many payments despite checking in constructor?")
308    };
309
310    Transaction::V2 {
311      prefix: TransactionPrefix {
312        additional_timelock: Timelock::None,
313        inputs: self.intent.inputs(&self.key_images),
314        outputs: self.intent.outputs(&self.key_images),
315        extra: self.intent.extra(),
316      },
317      proofs: Some(RctProofs {
318        base: RctBase {
319          fee: if self
320            .intent
321            .payments
322            .iter()
323            .any(|payment| matches!(payment, InternalPayment::Change(_)))
324          {
325            // The necessary fee is the fee
326            self.intent.weight_and_necessary_fee().1
327          } else {
328            // If we don't have a change output, the difference is the fee
329            let inputs =
330              self.intent.inputs.iter().map(|input| input.commitment().amount).sum::<u64>();
331            let payments = self
332              .intent
333              .payments
334              .iter()
335              .filter_map(|payment| match payment {
336                InternalPayment::Payment(_, amount) => Some(amount),
337                InternalPayment::Change(_) => None,
338              })
339              .sum::<u64>();
340            // Safe since the constructor checks inputs >= (payments + fee)
341            inputs - payments
342          },
343          encrypted_amounts,
344          pseudo_outs: vec![],
345          commitments,
346        },
347        prunable: RctPrunable::Clsag { bulletproof, clsags: vec![], pseudo_outs: vec![] },
348      }),
349    }
350  }
351}