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}