monero_interface/
provides_transactions.rs

1use core::future::Future;
2use alloc::{format, vec, vec::Vec, string::String};
3
4use monero_oxide::transaction::{Pruned, Transaction};
5
6use crate::InterfaceError;
7
8/// A pruned transaction with the hash of its pruned data, if `version != 1`.
9#[derive(Clone, PartialEq, Eq, Debug)]
10pub struct PrunedTransactionWithPrunableHash {
11  transaction: Transaction<Pruned>,
12  prunable_hash: Option<[u8; 32]>,
13}
14
15impl PrunedTransactionWithPrunableHash {
16  /// Create a new `PrunedTransactionWithPrunableHash`.
17  ///
18  /// This expects `(version != 1) == (prunable_hash = Some(_))` and returns `None` otherwise.
19  pub fn new(
20    transaction: Transaction<Pruned>,
21    mut prunable_hash: Option<[u8; 32]>,
22  ) -> Option<Self> {
23    match &transaction {
24      Transaction::V1 { .. } => {
25        if prunable_hash.is_some() {
26          None?
27        }
28      }
29      Transaction::V2 { proofs, .. } => {
30        if prunable_hash.is_none() {
31          None?;
32        }
33        if proofs.is_none() {
34          prunable_hash = Some([0; 32]);
35        }
36      }
37    }
38    Some(Self { transaction, prunable_hash })
39  }
40
41  /// Verify the transaction has the expected hash, if possible.
42  ///
43  /// This only works for transaction where `version != 1`. Transactions where `version = 1` will
44  /// be returned without any verification.
45  ///
46  /// If verification fails, the actual hash of the transaction is returned as the error.
47  pub fn verify_as_possible(self, hash: [u8; 32]) -> Result<Transaction<Pruned>, [u8; 32]> {
48    if let Some(prunable_hash) = self.prunable_hash {
49      let actual_hash = self
50        .transaction
51        .hash_with_prunable_hash(prunable_hash)
52        .expect("couldn't hash with prunable hash despite prior ensuring presence was as expected");
53      if actual_hash != hash {
54        Err(actual_hash)?;
55      }
56    }
57    Ok(self.transaction)
58  }
59}
60
61impl AsRef<Transaction<Pruned>> for PrunedTransactionWithPrunableHash {
62  fn as_ref(&self) -> &Transaction<Pruned> {
63    &self.transaction
64  }
65}
66
67/// An error when fetching transactions.
68#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
69pub enum TransactionsError {
70  /// Error with the interface.
71  #[error("interface error ({0})")]
72  InterfaceError(InterfaceError),
73  /// A transaction wasn't found.
74  #[error("transaction wasn't found")]
75  TransactionNotFound,
76  /// A transaction expected to not be pruned was pruned.
77  #[error("transaction was unexpectedly pruned")]
78  PrunedTransaction,
79}
80
81impl From<InterfaceError> for TransactionsError {
82  fn from(err: InterfaceError) -> Self {
83    Self::InterfaceError(err)
84  }
85}
86
87/// Provides unvalidated transactions from an untrusted interface.
88///
89/// This provides all its methods yet (`transactions` || `transaction`) &&
90/// (`pruned_transactions` || `pruned_transaction`) MUST be overriden, ideally the batch
91/// methods.
92#[rustfmt::skip]
93pub trait ProvidesUnvalidatedTransactions: Sync {
94  /// Get transactions.
95  ///
96  /// This returns the correct amount of transactions, deserialized, without further validation.
97  fn transactions(
98    &self,
99    hashes: &[[u8; 32]],
100  ) -> impl Send + Future<Output = Result<Vec<Transaction>, TransactionsError>> {
101    async move {
102      let mut txs = Vec::with_capacity(hashes.len());
103      for hash in hashes {
104        txs.push(self.transaction(*hash).await?);
105      }
106      Ok(txs)
107    }
108  }
109
110  /// Get pruned transactions.
111  ///
112  /// This returns the correct amount of transactions, deserialized, without further validation.
113  fn pruned_transactions(
114    &self,
115    hashes: &[[u8; 32]],
116  ) -> impl Send + Future<Output = Result<Vec<PrunedTransactionWithPrunableHash>, TransactionsError>>
117  {
118    async move {
119      let mut txs = Vec::with_capacity(hashes.len());
120      for hash in hashes {
121        txs.push(self.pruned_transaction(*hash).await?);
122      }
123      Ok(txs)
124    }
125  }
126
127  /// Get a transaction.
128  fn transaction(
129    &self,
130    hash: [u8; 32],
131  ) -> impl Send + Future<Output = Result<Transaction, TransactionsError>> {
132    async move {
133      let mut txs = self.transactions(&[hash]).await?;
134      if txs.len() != 1 {
135        Err(InterfaceError::InternalError(format!(
136          "`{}` returned {} transactions, expected {}",
137          "ProvidesUnvalidatedTransactions::transactions",
138          txs.len(),
139          1,
140        )))?;
141      }
142      Ok(txs.pop().expect("verified we had a transaction"))
143    }
144  }
145
146  /// Get a pruned transaction.
147  fn pruned_transaction(
148    &self,
149    hash: [u8; 32],
150  ) -> impl Send + Future<Output = Result<PrunedTransactionWithPrunableHash, TransactionsError>> {
151    async move {
152      let mut txs = self.pruned_transactions(&[hash]).await?;
153      if txs.len() != 1 {
154        Err(InterfaceError::InternalError(format!(
155          "`{}` returned {} transactions, expected {}",
156          "ProvidesUnvalidatedTransactions::pruned_transactions",
157          txs.len(),
158          1,
159        )))?;
160      }
161      Ok(txs.pop().expect("verified we had a pruned transaction"))
162    }
163  }
164}
165
166/// Provides transactions which have been sanity-checked.
167pub trait ProvidesTransactions: Sync {
168  /// Get transactions.
169  ///
170  /// This returns all of the requested deserialized transactions, ensuring they're the requested
171  /// transactions.
172  fn transactions(
173    &self,
174    hashes: &[[u8; 32]],
175  ) -> impl Send + Future<Output = Result<Vec<Transaction>, TransactionsError>>;
176
177  /// Get pruned transactions.
178  ///
179  /// This returns all of the requested deserialized transactions, ensuring they're the requested
180  /// transactions. For transactions where `version == 1`, this may additionally request the
181  /// non-pruned transactions.
182  fn pruned_transactions(
183    &self,
184    hashes: &[[u8; 32]],
185  ) -> impl Send + Future<Output = Result<Vec<Transaction<Pruned>>, TransactionsError>>;
186
187  /// Get a transaction.
188  ///
189  /// This returns the requested transaction, ensuring it is the requested transaction.
190  fn transaction(
191    &self,
192    hash: [u8; 32],
193  ) -> impl Send + Future<Output = Result<Transaction, TransactionsError>>;
194
195  /// Get a pruned transaction.
196  ///
197  /// This returns the requested transaction, ensuring it is the requested transaction. For
198  /// transactions where `version == 1`, this may additionally request the non-pruned transactions.
199  fn pruned_transaction(
200    &self,
201    hash: [u8; 32],
202  ) -> impl Send + Future<Output = Result<Transaction<Pruned>, TransactionsError>>;
203}
204
205pub(crate) async fn validate_pruned_transactions<P: ProvidesTransactions>(
206  interface: &P,
207  unvalidated: Vec<PrunedTransactionWithPrunableHash>,
208  hashes: &[[u8; 32]],
209) -> Result<Vec<Transaction<Pruned>>, TransactionsError> {
210  if unvalidated.len() != hashes.len() {
211    Err(InterfaceError::InternalError(format!(
212      "`{}` returned {} transactions, expected {}",
213      "ProvidesUnvalidatedTransactions::pruned_transactions",
214      unvalidated.len(),
215      hashes.len(),
216    )))?;
217  }
218
219  let mut txs = Vec::with_capacity(unvalidated.len());
220  let mut v1_indexes = vec![];
221  let mut v1_hashes = vec![];
222  for (tx, expected_hash) in unvalidated.into_iter().zip(hashes) {
223    match tx.verify_as_possible(*expected_hash) {
224      Ok(tx) => {
225        if matches!(tx, Transaction::V1 { .. }) {
226          v1_indexes.push(txs.len());
227          v1_hashes.push(*expected_hash);
228        }
229        txs.push(tx)
230      }
231      Err(hash) => Err(InterfaceError::InvalidInterface(format!(
232        "interface returned TX {} when {} was requested",
233        hex::encode(hash),
234        hex::encode(expected_hash)
235      )))?,
236    }
237  }
238
239  if !v1_indexes.is_empty() {
240    let full_txs = <P as ProvidesTransactions>::transactions(interface, &v1_hashes).await?;
241    for ((pruned_tx, hash), tx) in
242      v1_indexes.into_iter().map(|i| &txs[i]).zip(v1_hashes).zip(full_txs)
243    {
244      if &Transaction::<Pruned>::from(tx) != pruned_tx {
245        Err(InterfaceError::InvalidInterface(format!(
246          "interface returned pruned V1 TX which didn't match TX {}",
247          hex::encode(hash)
248        )))?;
249      }
250    }
251  }
252
253  Ok(txs)
254}
255
256impl<P: ProvidesUnvalidatedTransactions> ProvidesTransactions for P {
257  fn transactions(
258    &self,
259    hashes: &[[u8; 32]],
260  ) -> impl Send + Future<Output = Result<Vec<Transaction>, TransactionsError>> {
261    async move {
262      let txs = <P as ProvidesUnvalidatedTransactions>::transactions(self, hashes).await?;
263      if txs.len() != hashes.len() {
264        Err(InterfaceError::InternalError(format!(
265          "`{}` returned {} transactions, expected {}",
266          "ProvidesUnvalidatedTransactions::transactions",
267          txs.len(),
268          hashes.len(),
269        )))?;
270      }
271
272      for (tx, expected_hash) in txs.iter().zip(hashes) {
273        let hash = tx.hash();
274        if &hash != expected_hash {
275          Err(InterfaceError::InvalidInterface(format!(
276            "interface returned TX {} when {} was requested",
277            hex::encode(hash),
278            hex::encode(expected_hash)
279          )))?;
280        }
281      }
282      Ok(txs)
283    }
284  }
285
286  fn pruned_transactions(
287    &self,
288    hashes: &[[u8; 32]],
289  ) -> impl Send + Future<Output = Result<Vec<Transaction<Pruned>>, TransactionsError>> {
290    async move {
291      let unvalidated =
292        <P as ProvidesUnvalidatedTransactions>::pruned_transactions(self, hashes).await?;
293      validate_pruned_transactions(self, unvalidated, hashes).await
294    }
295  }
296
297  fn transaction(
298    &self,
299    hash: [u8; 32],
300  ) -> impl Send + Future<Output = Result<Transaction, TransactionsError>> {
301    async move {
302      let tx = <P as ProvidesUnvalidatedTransactions>::transaction(self, hash).await?;
303      let actual_hash = tx.hash();
304      if actual_hash != hash {
305        Err(InterfaceError::InvalidInterface(format!(
306          "interface returned TX {} when {} was requested",
307          hex::encode(actual_hash),
308          hex::encode(hash)
309        )))?;
310      }
311      Ok(tx)
312    }
313  }
314
315  fn pruned_transaction(
316    &self,
317    hash: [u8; 32],
318  ) -> impl Send + Future<Output = Result<Transaction<Pruned>, TransactionsError>> {
319    async move {
320      let unvalidated =
321        <P as ProvidesUnvalidatedTransactions>::pruned_transaction(self, hash).await?;
322      Ok(validate_pruned_transactions(self, vec![unvalidated], &[hash]).await?.swap_remove(0))
323    }
324  }
325}
326
327/// An error from the interface.
328#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
329pub enum PublishTransactionError {
330  /// Error with the interface.
331  #[error("interface error ({0})")]
332  InterfaceError(InterfaceError),
333  /// The transaction was rejected.
334  #[error("transaction was rejected ({0})")]
335  TransactionRejected(String),
336}
337
338impl From<InterfaceError> for PublishTransactionError {
339  fn from(err: InterfaceError) -> Self {
340    Self::InterfaceError(err)
341  }
342}
343
344/// An interface eligible to publish transactions over.
345pub trait PublishTransaction: Sync {
346  /// Publish a transaction.
347  fn publish_transaction(
348    &self,
349    transaction: &Transaction,
350  ) -> impl Send + Future<Output = Result<(), PublishTransactionError>>;
351}