monero_wallet/send/
eventuality.rs

1use std_shims::{vec::Vec, io};
2
3use zeroize::Zeroize;
4
5use monero_oxide::io::CompressedPoint;
6
7use crate::{
8  ringct::PrunedRctProofs,
9  transaction::{Input, Timelock, Pruned, Transaction},
10  send::SignableTransaction,
11};
12
13/// The eventual output of a SignableTransaction.
14///
15/// If a SignableTransaction is signed and published on-chain, it will create a Transaction
16/// identifiable to whoever else has the same SignableTransaction (with the same outgoing view
17/// key). This structure enables checking if a Transaction is in fact such a result, as it can.
18///
19/// Since Monero is a privacy coin without outgoing view keys, this only performs a fuzzy match.
20/// The fuzzy match executes over the outputs and associated data necessary to work with the
21/// outputs (the transaction randomness, ciphertexts). This transaction does not check if the
22/// inputs intended to be spent where actually the inputs spent (as infeasible).
23///
24/// The transaction randomness does bind to the inputs intended to be spent, so an on-chain
25/// transaction will not match for multiple `Eventuality`s unless the `SignableTransaction`s they
26/// were built from were in conflict (and their intended transactions cannot simultaneously exist
27/// on-chain).
28#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
29pub struct Eventuality(SignableTransaction);
30
31impl From<SignableTransaction> for Eventuality {
32  fn from(tx: SignableTransaction) -> Eventuality {
33    Eventuality(tx)
34  }
35}
36
37impl Eventuality {
38  /// Return the `extra` field any transaction following this intent would use.
39  ///
40  /// This enables building a HashMap of Extra -> Eventuality for efficiently fetching the
41  /// `Eventuality` an on-chain transaction may complete.
42  ///
43  /// This extra is cryptographically bound to the inputs intended to be spent. If the
44  /// `SignableTransaction`s the `Eventuality`s are built from are not in conflict (their intended
45  /// transactions can simultaneously exist on-chain), then each extra will only have a single
46  /// Eventuality associated (barring a cryptographic problem considered hard failing).
47  pub fn extra(&self) -> Vec<u8> {
48    self.0.extra()
49  }
50
51  /// Return if this transaction matches the `SignableTransaction` this was created from.
52  ///
53  /// Matching the `SignableTransaction` means this transaction created the expected outputs,
54  /// they're scannable by the intended recipient, they're not locked, and this transaction claims
55  /// to use the intended inputs (though this is not guaranteed).
56  ///
57  /// Being scannable by the intended recipient does not equate to being spendable due to the
58  /// [burning bug](https://web.getmonero.org/2018/09/25/a-post-mortum-of-the-burning-bug.html).
59  /// Note that transactions with matching `SignableTransaction`s will not incur the burning bug
60  /// against each other without a hash collision.
61  ///
62  /// The 'claim' about using the intended inputs is evaluated by this transaction using
63  /// transaction keys derived from the intended inputs. This ensures two `SignableTransaction`s
64  /// with the same intended payments don't match for each other's `Eventuality`s (as they'll have
65  /// distinct inputs intended if they can legitimately co-exist).
66  #[must_use]
67  pub fn matches(&self, tx: &Transaction<Pruned>) -> bool {
68    // Verify extra
69    if self.0.extra() != tx.prefix().extra {
70      return false;
71    }
72
73    // Also ensure no timelock was set
74    if tx.prefix().additional_timelock != Timelock::None {
75      return false;
76    }
77
78    // Check the amount of inputs aligns
79    if tx.prefix().inputs.len() != self.0.inputs.len() {
80      return false;
81    }
82    // Collect the key images used by this transaction
83    let Ok(key_images) = tx
84      .prefix()
85      .inputs
86      .iter()
87      .map(|input| match input {
88        Input::Gen(_) => Err(()),
89        Input::ToKey { key_image, .. } => Ok(*key_image),
90      })
91      .collect::<Result<Vec<_>, _>>()
92    else {
93      return false;
94    };
95
96    // Check the outputs
97    if self.0.outputs(&key_images) != tx.prefix().outputs {
98      return false;
99    }
100
101    // Check the encrypted amounts and commitments
102    let commitments_and_encrypted_amounts = self.0.commitments_and_encrypted_amounts(&key_images);
103    let Transaction::V2 { proofs: Some(PrunedRctProofs { ref base, .. }), .. } = tx else {
104      return false;
105    };
106    if base.commitments !=
107      commitments_and_encrypted_amounts
108        .iter()
109        .map(|(commitment, _)| CompressedPoint::from(commitment.calculate().compress()))
110        .collect::<Vec<_>>()
111    {
112      return false;
113    }
114    if base.encrypted_amounts !=
115      commitments_and_encrypted_amounts.into_iter().map(|(_, amount)| amount).collect::<Vec<_>>()
116    {
117      return false;
118    }
119
120    true
121  }
122
123  /// Write the Eventuality.
124  ///
125  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
126  /// defined serialization.
127  pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
128    self.0.write(w)
129  }
130
131  /// Serialize the Eventuality to a `Vec<u8>`.
132  ///
133  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
134  /// defined serialization.
135  pub fn serialize(&self) -> Vec<u8> {
136    self.0.serialize()
137  }
138
139  /// Read a Eventuality.
140  ///
141  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
142  /// defined serialization.
143  pub fn read<R: io::Read>(r: &mut R) -> io::Result<Eventuality> {
144    Ok(Eventuality(SignableTransaction::read(r)?))
145  }
146}