monero_interface/
provides_scannable_blocks.rs

1use core::{ops::RangeInclusive, future::Future};
2use alloc::{format, vec::Vec, string::ToString};
3
4use monero_oxide::{
5  transaction::{Pruned, Transaction},
6  block::Block,
7};
8
9use crate::{
10  InterfaceError, TransactionsError, PrunedTransactionWithPrunableHash, ProvidesTransactions,
11  ProvidesOutputs,
12};
13
14/// A block which is able to be scanned.
15///
16/// As this `struct`'s fields are public, no internal consistency is enforced.
17// TODO: Should these fields be private so we can check their integrity within a constructor?
18#[derive(Clone, PartialEq, Eq, Debug)]
19pub struct ScannableBlock {
20  /// The block which is scannable.
21  pub block: Block,
22  /// The non-miner transactions within this block.
23  pub transactions: Vec<Transaction<Pruned>>,
24  /// The output index for the first RingCT output within this block.
25  ///
26  /// This should be `None` if there are no RingCT outputs within this block, `Some` otherwise.
27  ///
28  /// This is not bound to be correct by any of the functions within this crate's API as it's
29  /// infeasible to verify the accuracy of. To do so would require a trusted view over the RingCT
30  /// outputs, synchronized to the block before this. To ensure correctness and privacy, the user
31  /// SHOULD locally maintain a database of the RingCT outputs and the user SHOULD use it to
32  /// override whatever is claimed to be the output index for the first RingCT output within this
33  /// block. If the values are different, the user SHOULD detect the interface is invalid and
34  /// disconnect entirely.
35  pub output_index_for_first_ringct_output: Option<u64>,
36}
37
38/// Extension trait for `ProvidesTransactions and `ProvidesOutputs`.
39pub trait ExpandToScannableBlock: ProvidesTransactions + ProvidesOutputs {
40  /// Expand a `Block` to a `ScannableBlock`.
41  ///
42  /// The resulting block will be validated to have the transactions corresponding to the block's
43  /// list of transactions.
44  fn expand_to_scannable_block(
45    &self,
46    block: Block,
47  ) -> impl Send + Future<Output = Result<ScannableBlock, TransactionsError>> {
48    async move {
49      let transactions = self.pruned_transactions(&block.transactions).await?;
50
51      /*
52        Requesting the output index for each output we sucessfully scan would cause a loss of
53        privacy. We could instead request the output indexes for all outputs we scan, yet this
54        would notably increase the amount of RPC calls we make.
55
56        We solve this by requesting the output index for the first RingCT output in the block,
57        which should be within the miner transaction. Then, as we scan transactions, we update the
58        output index ourselves.
59
60        Please note we only will scan RingCT outputs so we only need to track the RingCT output
61        index. This decision was made due to spending CN outputs potentially having burdensome
62        requirements (the need to make a v1 TX due to insufficient decoys).
63
64        We bound ourselves to only scanning RingCT outputs by only scanning v2 transactions. This
65        is safe and correct since:
66
67        1) v1 transactions cannot create RingCT outputs.
68
69           https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
70             /src/cryptonote_basic/cryptonote_format_utils.cpp#L866-L869
71
72        2) v2 miner transactions implicitly create RingCT outputs.
73
74           https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
75             /src/blockchain_db/blockchain_db.cpp#L232-L241
76
77        3) v2 transactions must create RingCT outputs.
78
79           https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45
80             /src/cryptonote_core/blockchain.cpp#L3055-L3065
81
82           That does bound on the hard fork version being >= 3, yet all v2 TXs have a hard fork
83           version > 3.
84
85           https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
86             /src/cryptonote_core/blockchain.cpp#L3417
87      */
88
89      // Get the index for the first output
90      let mut output_index_for_first_ringct_output = None;
91      let miner_tx_hash = block.miner_transaction().hash();
92      let miner_tx = Transaction::<Pruned>::from(block.miner_transaction().clone());
93      for (hash, tx) in core::iter::once((&miner_tx_hash, &miner_tx))
94        .chain(block.transactions.iter().zip(&transactions))
95      {
96        // If this isn't a RingCT output, or there are no outputs, move to the next TX
97        if (!matches!(tx, Transaction::V2 { .. })) || tx.prefix().outputs.is_empty() {
98          continue;
99        }
100
101        let index =
102          *ProvidesOutputs::output_indexes(self, *hash).await?.first().ok_or_else(|| {
103            InterfaceError::InvalidInterface(
104              "requested output indexes for a TX with outputs and got none".to_string(),
105            )
106          })?;
107        output_index_for_first_ringct_output = Some(index);
108        break;
109      }
110
111      Ok(ScannableBlock { block, transactions, output_index_for_first_ringct_output })
112    }
113  }
114}
115
116impl<P: ProvidesTransactions + ProvidesOutputs> ExpandToScannableBlock for P {}
117
118/// An unvalidated block which may be scannable.
119#[derive(Clone, PartialEq, Eq, Debug)]
120pub struct UnvalidatedScannableBlock {
121  /// The block which is to be scanned.
122  pub block: Block,
123  /// The non-miner transactions allegedly within this block.
124  pub transactions: Vec<PrunedTransactionWithPrunableHash>,
125  /// The alleged output index for the first RingCT output within this block.
126  ///
127  /// This should be `None` if there are no RingCT outputs within this block, `Some` otherwise.
128  pub output_index_for_first_ringct_output: Option<u64>,
129}
130
131/// Provides scannable blocks from an untrusted interface.
132///
133/// This provides some of its methods yet
134/// (`contiguous_scannable_blocks` || `scannable_block_by_number`) MUST be overriden, and the batch
135/// method SHOULD be overriden.
136pub trait ProvidesUnvalidatedScannableBlocks: Sync {
137  /// Get a contiguous range of `ScannableBlock`s.
138  ///
139  /// No validation is applied to the received blocks other than that they deserialize and have the
140  /// expected amount of blocks returned.
141  fn contiguous_scannable_blocks(
142    &self,
143    range: RangeInclusive<usize>,
144  ) -> impl Send + Future<Output = Result<Vec<UnvalidatedScannableBlock>, InterfaceError>> {
145    async move {
146      // If a caller requests an exorbitant amount of blocks, this may trigger an OOM kill
147      // In order to maintain correctness, we have to attempt to service this request though
148      let mut blocks =
149        Vec::with_capacity(range.end().saturating_sub(*range.start()).saturating_add(1));
150      for number in range {
151        blocks.push(self.scannable_block_by_number(number).await?);
152      }
153      Ok(blocks)
154    }
155  }
156
157  /// Get a `ScannableBlock` by its hash.
158  ///
159  /// No validation is applied to the received block other than that it deserializes.
160  fn scannable_block(
161    &self,
162    hash: [u8; 32],
163  ) -> impl Send + Future<Output = Result<UnvalidatedScannableBlock, InterfaceError>>;
164
165  /// Get a `ScannableBlock` by its number.
166  ///
167  /// No validation is applied to the received block other than that it deserializes.
168  fn scannable_block_by_number(
169    &self,
170    number: usize,
171  ) -> impl Send + Future<Output = Result<UnvalidatedScannableBlock, InterfaceError>> {
172    async move {
173      let mut blocks = self.contiguous_scannable_blocks(number ..= number).await?;
174      if blocks.len() != 1 {
175        Err(InterfaceError::InternalError(format!(
176          "`{}` returned {} blocks, expected {}",
177          "ProvidesUnvalidatedScannableBlocks::contiguous_scannable_blocks",
178          blocks.len(),
179          1,
180        )))?;
181      }
182      Ok(blocks.pop().expect("verified we had a scannable block"))
183    }
184  }
185}
186
187/// Provides scannable blocks which have been sanity-checked.
188pub trait ProvidesScannableBlocks: Sync {
189  /// Get a contiguous range of `ScannableBlock`s.
190  ///
191  /// The blocks will be validated to build upon each other, as expected, have the expected
192  /// numbers, and have the expected transactions according to the block's list of transactions.
193  fn contiguous_scannable_blocks(
194    &self,
195    range: RangeInclusive<usize>,
196  ) -> impl Send + Future<Output = Result<Vec<ScannableBlock>, InterfaceError>>;
197
198  /// Get a `ScannableBlock` by its hash.
199  ///
200  /// The block will be validated to be the requested block with a well-formed number and have the
201  /// expected transactions according to the block's list of transactions.
202  fn scannable_block(
203    &self,
204    hash: [u8; 32],
205  ) -> impl Send + Future<Output = Result<ScannableBlock, InterfaceError>>;
206
207  /// Get a `ScannableBlock` by its number.
208  ///
209  /// The number of a block is its index on the blockchain, so the genesis block would have
210  /// `number = 0`.
211  ///
212  /// The block will be validated to be a block with the requested number and have the expected
213  /// transactions according to the block's list of transactions.
214  fn scannable_block_by_number(
215    &self,
216    number: usize,
217  ) -> impl Send + Future<Output = Result<ScannableBlock, InterfaceError>>;
218}
219
220async fn validate_scannable_block<P: ProvidesTransactions + ProvidesUnvalidatedScannableBlocks>(
221  interface: &P,
222  block: UnvalidatedScannableBlock,
223) -> Result<ScannableBlock, InterfaceError> {
224  let UnvalidatedScannableBlock { block, transactions, output_index_for_first_ringct_output } =
225    block;
226  let transactions = match crate::provides_transactions::validate_pruned_transactions(
227    interface,
228    transactions,
229    &block.transactions,
230  )
231  .await
232  {
233    Ok(transactions) => transactions,
234    Err(e) => Err(match e {
235      TransactionsError::InterfaceError(e) => e,
236      TransactionsError::TransactionNotFound => InterfaceError::InvalidInterface(
237        "interface sent us a scannable block it doesn't have the transactions for".to_string(),
238      ),
239      TransactionsError::PrunedTransaction => InterfaceError::InvalidInterface(
240        // This happens if we're sent a pruned V1 transaction after requesting it in full
241        "interface sent us pruned transaction when validating a scannable block".to_string(),
242      ),
243    })?,
244  };
245  Ok(ScannableBlock { block, transactions, output_index_for_first_ringct_output })
246}
247
248impl<P: ProvidesTransactions + ProvidesUnvalidatedScannableBlocks> ProvidesScannableBlocks for P {
249  fn contiguous_scannable_blocks(
250    &self,
251    range: RangeInclusive<usize>,
252  ) -> impl Send + Future<Output = Result<Vec<ScannableBlock>, InterfaceError>> {
253    async move {
254      let blocks =
255        <P as ProvidesUnvalidatedScannableBlocks>::contiguous_scannable_blocks(self, range.clone())
256          .await?;
257      let expected_blocks =
258        range.end().saturating_sub(*range.start()).checked_add(1).ok_or_else(|| {
259          InterfaceError::InternalError(
260            "amount of blocks requested wasn't representable in a `usize`".to_string(),
261          )
262        })?;
263      if blocks.len() != expected_blocks {
264        Err(InterfaceError::InternalError(format!(
265          "`{}` returned {} blocks, expected {}",
266          "ProvidesUnvalidatedScannableBlocks::contiguous_scannable_blocks",
267          blocks.len(),
268          expected_blocks,
269        )))?;
270      }
271      crate::provides_blockchain::sanity_check_contiguous_blocks(
272        range,
273        blocks.iter().map(|scannable_block| &scannable_block.block),
274      )?;
275
276      let mut res = Vec::with_capacity(blocks.len());
277      for block in blocks {
278        res.push(validate_scannable_block(self, block).await?);
279      }
280      Ok(res)
281    }
282  }
283
284  fn scannable_block(
285    &self,
286    hash: [u8; 32],
287  ) -> impl Send + Future<Output = Result<ScannableBlock, InterfaceError>> {
288    async move {
289      let block = <P as ProvidesUnvalidatedScannableBlocks>::scannable_block(self, hash).await?;
290      crate::provides_blockchain::sanity_check_block_by_hash(&hash, &block.block)?;
291      validate_scannable_block(self, block).await
292    }
293  }
294
295  fn scannable_block_by_number(
296    &self,
297    number: usize,
298  ) -> impl Send + Future<Output = Result<ScannableBlock, InterfaceError>> {
299    async move {
300      let block =
301        <P as ProvidesUnvalidatedScannableBlocks>::scannable_block_by_number(self, number).await?;
302      crate::provides_blockchain::sanity_check_block_by_number(number, &block.block)?;
303      validate_scannable_block(self, block).await
304    }
305  }
306}