diff --git a/networks/monero/rpc/src/lib.rs b/networks/monero/rpc/src/lib.rs index 976f1325..4c5055cc 100644 --- a/networks/monero/rpc/src/lib.rs +++ b/networks/monero/rpc/src/lib.rs @@ -73,6 +73,19 @@ pub enum RpcError { InvalidPriority, } +/// A block which is able to be scanned. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ScannableBlock { + /// The block which is being scanned. + pub block: Block, + /// The non-miner transactions within this block. + pub transactions: Vec>, + /// The output index for the first RingCT output within this block. + /// + /// None if there are no RingCT outputs within this block, Some otherwise. + pub output_index_for_first_ringct_output: Option, +} + /// A struct containing a fee rate. /// /// The fee rate is defined as a per-weight cost, along with a mask for rounding purposes. @@ -570,6 +583,95 @@ pub trait Rpc: Sync + Clone + Debug { } } + /// Get a block's scannable form. + fn get_scannable_block( + &self, + block: Block, + ) -> impl Send + Future> { + async move { + let transactions = self.get_pruned_transactions(&block.transactions).await?; + + /* + Requesting the output index for each output we sucessfully scan would cause a loss of + privacy. We could instead request the output indexes for all outputs we scan, yet this + would notably increase the amount of RPC calls we make. + + We solve this by requesting the output index for the first RingCT output in the block, which + should be within the miner transaction. Then, as we scan transactions, we update the output + index ourselves. + + Please note we only will scan RingCT outputs so we only need to track the RingCT output + index. This decision was made due to spending CN outputs potentially having burdensome + requirements (the need to make a v1 TX due to insufficient decoys). + + We bound ourselves to only scanning RingCT outputs by only scanning v2 transactions. This is + safe and correct since: + + 1) v1 transactions cannot create RingCT outputs. + + https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + /src/cryptonote_basic/cryptonote_format_utils.cpp#L866-L869 + + 2) v2 miner transactions implicitly create RingCT outputs. + + https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + /src/blockchain_db/blockchain_db.cpp#L232-L241 + + 3) v2 transactions must create RingCT outputs. + + https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45 + /src/cryptonote_core/blockchain.cpp#L3055-L3065 + + That does bound on the hard fork version being >= 3, yet all v2 TXs have a hard fork + version > 3. + + https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + /src/cryptonote_core/blockchain.cpp#L3417 + */ + + // Get the index for the first output + let mut output_index_for_first_ringct_output = None; + let miner_tx_hash = block.miner_transaction.hash(); + let miner_tx = Transaction::::from(block.miner_transaction.clone()); + for (hash, tx) in core::iter::once((&miner_tx_hash, &miner_tx)) + .chain(block.transactions.iter().zip(&transactions)) + { + // If this isn't a RingCT output, or there are no outputs, move to the next TX + if (!matches!(tx, Transaction::V2 { .. })) || tx.prefix().outputs.is_empty() { + continue; + } + + let index = *self.get_o_indexes(*hash).await?.first().ok_or_else(|| { + RpcError::InvalidNode( + "requested output indexes for a TX with outputs and got none".to_string(), + ) + })?; + output_index_for_first_ringct_output = Some(index); + break; + } + + Ok(ScannableBlock { block, transactions, output_index_for_first_ringct_output }) + } + } + + /// Get a block's scannable form by its hash. + // TODO: get_blocks.bin + fn get_scannable_block_by_hash( + &self, + hash: [u8; 32], + ) -> impl Send + Future> { + async move { self.get_scannable_block(self.get_block(hash).await?).await } + } + + /// Get a block's scannable form by its number. + // TODO: get_blocks_by_height.bin + fn get_scannable_block_by_number( + &self, + number: usize, + ) -> impl Send + Future> { + async move { self.get_scannable_block(self.get_block_by_number(number).await?).await } + } + /// Get the currently estimated fee rate from the node. /// /// This may be manipulated to unsafe levels and MUST be sanity checked. diff --git a/networks/monero/wallet/src/lib.rs b/networks/monero/wallet/src/lib.rs index a54d51c9..63f7488a 100644 --- a/networks/monero/wallet/src/lib.rs +++ b/networks/monero/wallet/src/lib.rs @@ -23,7 +23,7 @@ pub use monero_rpc as rpc; pub use monero_address as address; mod view_pair; -pub use view_pair::{ViewPair, GuaranteedViewPair}; +pub use view_pair::{ViewPairError, ViewPair, GuaranteedViewPair}; /// Structures and functionality for working with transactions' extra fields. pub mod extra; @@ -33,7 +33,7 @@ pub(crate) mod output; pub use output::WalletOutput; mod scan; -pub use scan::{Scanner, GuaranteedScanner}; +pub use scan::{ScanError, Scanner, GuaranteedScanner}; mod decoys; pub use decoys::OutputWithDecoys; diff --git a/networks/monero/wallet/src/scan.rs b/networks/monero/wallet/src/scan.rs index 07dc81c3..0de35f35 100644 --- a/networks/monero/wallet/src/scan.rs +++ b/networks/monero/wallet/src/scan.rs @@ -1,16 +1,15 @@ use core::ops::Deref; -use std_shims::{alloc::format, vec, vec::Vec, string::ToString, collections::HashMap}; +use std_shims::{vec, vec::Vec, collections::HashMap}; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY}; -use monero_rpc::{RpcError, Rpc}; +use monero_rpc::ScannableBlock; use monero_serai::{ io::*, primitives::Commitment, transaction::{Timelock, Pruned, Transaction}, - block::Block, }; use crate::{ address::SubaddressIndex, ViewPair, GuaranteedViewPair, output::*, PaymentId, Extra, @@ -67,6 +66,18 @@ impl Timelocked { } } +/// Errors when scanning a block. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum ScanError { + /// The block was for an unsupported protocol version. + #[cfg_attr(feature = "std", error("unsupported protocol version ({0})"))] + UnsupportedProtocol(u8), + /// The ScannableBlock was invalid. + #[cfg_attr(feature = "std", error("invalid scannable block ({0})"))] + InvalidScannableBlock(&'static str), +} + #[derive(Clone)] struct InternalScanner { pair: ViewPair, @@ -107,10 +118,10 @@ impl InternalScanner { fn scan_transaction( &self, - tx_start_index_on_blockchain: u64, + output_index_for_first_ringct_output: u64, tx_hash: [u8; 32], tx: &Transaction, - ) -> Result { + ) -> Result { // Only scan TXs creating RingCT outputs // For the full details on why this check is equivalent, please see the documentation in `scan` if tx.version() != 2 { @@ -197,14 +208,14 @@ impl InternalScanner { } else { let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else { // Invalid transaction, as of consensus rules at the time of writing this code - Err(RpcError::InvalidNode("non-miner v2 transaction without RCT proofs".to_string()))? + Err(ScanError::InvalidScannableBlock("non-miner v2 transaction without RCT proofs"))? }; commitment = match proofs.base.encrypted_amounts.get(o) { Some(amount) => output_derivations.decrypt(amount), // Invalid transaction, as of consensus rules at the time of writing this code - None => Err(RpcError::InvalidNode( - "RCT proofs without an encrypted amount per output".to_string(), + None => Err(ScanError::InvalidScannableBlock( + "RCT proofs without an encrypted amount per output", ))?, }; @@ -223,7 +234,7 @@ impl InternalScanner { index_in_transaction: o.try_into().unwrap(), }, relative_id: RelativeId { - index_on_blockchain: tx_start_index_on_blockchain + u64::try_from(o).unwrap(), + index_on_blockchain: output_index_for_first_ringct_output + u64::try_from(o).unwrap(), }, data: OutputData { key: output_key, key_offset, commitment }, metadata: Metadata { @@ -243,12 +254,22 @@ impl InternalScanner { Ok(Timelocked(res)) } - async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result { + fn scan(&mut self, block: ScannableBlock) -> Result { + // This is the output index for the first RingCT output within the block + // We mutate it to be the output index for the first RingCT for each transaction + let ScannableBlock { block, transactions, output_index_for_first_ringct_output } = block; + if block.transactions.len() != transactions.len() { + Err(ScanError::InvalidScannableBlock( + "scanning a ScannableBlock with more/less transactions than it should have", + ))?; + } + let Some(mut output_index_for_first_ringct_output) = output_index_for_first_ringct_output + else { + return Ok(Timelocked(vec![])); + }; + if block.header.hardfork_version > 16 { - Err(RpcError::InternalError(format!( - "scanning a hardfork {} block, when we only support up to 16", - block.header.hardfork_version - )))?; + Err(ScanError::UnsupportedProtocol(block.header.hardfork_version))?; } // We obtain all TXs in full @@ -256,80 +277,17 @@ impl InternalScanner { block.miner_transaction.hash(), Transaction::::from(block.miner_transaction.clone()), )]; - let txs = rpc.get_pruned_transactions(&block.transactions).await?; - for (hash, tx) in block.transactions.iter().zip(txs) { + for (hash, tx) in block.transactions.iter().zip(transactions) { txs_with_hashes.push((*hash, tx)); } - /* - Requesting the output index for each output we sucessfully scan would cause a loss of privacy - We could instead request the output indexes for all outputs we scan, yet this would notably - increase the amount of RPC calls we make. - - We solve this by requesting the output index for the first RingCT output in the block, which - should be within the miner transaction. Then, as we scan transactions, we update the output - index ourselves. - - Please note we only will scan RingCT outputs so we only need to track the RingCT output - index. This decision was made due to spending CN outputs potentially having burdensome - requirements (the need to make a v1 TX due to insufficient decoys). - - We bound ourselves to only scanning RingCT outputs by only scanning v2 transactions. This is - safe and correct since: - - 1) v1 transactions cannot create RingCT outputs. - - https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 - /src/cryptonote_basic/cryptonote_format_utils.cpp#L866-L869 - - 2) v2 miner transactions implicitly create RingCT outputs. - - https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 - /src/blockchain_db/blockchain_db.cpp#L232-L241 - - 3) v2 transactions must create RingCT outputs. - - https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45 - /src/cryptonote_core/blockchain.cpp#L3055-L3065 - - That does bound on the hard fork version being >= 3, yet all v2 TXs have a hard fork - version > 3. - - https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 - /src/cryptonote_core/blockchain.cpp#L3417 - */ - - // Get the starting index - let mut tx_start_index_on_blockchain = { - let mut tx_start_index_on_blockchain = None; - for (hash, tx) in &txs_with_hashes { - // If this isn't a RingCT output, or there are no outputs, move to the next TX - if (!matches!(tx, Transaction::V2 { .. })) || tx.prefix().outputs.is_empty() { - continue; - } - - let index = *rpc.get_o_indexes(*hash).await?.first().ok_or_else(|| { - RpcError::InvalidNode( - "requested output indexes for a TX with outputs and got none".to_string(), - ) - })?; - tx_start_index_on_blockchain = Some(index); - break; - } - let Some(tx_start_index_on_blockchain) = tx_start_index_on_blockchain else { - // Block had no RingCT outputs - return Ok(Timelocked(vec![])); - }; - tx_start_index_on_blockchain - }; - let mut res = Timelocked(vec![]); for (hash, tx) in txs_with_hashes { // Push all outputs into our result { let mut this_txs_outputs = vec![]; core::mem::swap( - &mut self.scan_transaction(tx_start_index_on_blockchain, hash, &tx)?.0, + &mut self.scan_transaction(output_index_for_first_ringct_output, hash, &tx)?.0, &mut this_txs_outputs, ); res.0.extend(this_txs_outputs); @@ -337,7 +295,7 @@ impl InternalScanner { // Update the RingCT starting index for the next TX if matches!(tx, Transaction::V2 { .. }) { - tx_start_index_on_blockchain += u64::try_from(tx.prefix().outputs.len()).unwrap() + output_index_for_first_ringct_output += u64::try_from(tx.prefix().outputs.len()).unwrap() } } @@ -384,8 +342,8 @@ impl Scanner { } /// Scan a block. - pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result { - self.0.scan(rpc, block).await + pub fn scan(&mut self, block: ScannableBlock) -> Result { + self.0.scan(block) } } @@ -413,7 +371,7 @@ impl GuaranteedScanner { } /// Scan a block. - pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result { - self.0.scan(rpc, block).await + pub fn scan(&mut self, block: ScannableBlock) -> Result { + self.0.scan(block) } } diff --git a/networks/monero/wallet/tests/add_data.rs b/networks/monero/wallet/tests/add_data.rs index 6aa57dbc..bd600d53 100644 --- a/networks/monero/wallet/tests/add_data.rs +++ b/networks/monero/wallet/tests/add_data.rs @@ -1,8 +1,12 @@ use monero_serai::transaction::Transaction; +use monero_simple_request_rpc::SimpleRequestRpc; use monero_wallet::{rpc::Rpc, extra::MAX_ARBITRARY_DATA_SIZE, send::SendError}; mod runner; +#[allow(clippy::upper_case_acronyms)] +type SRR = SimpleRequestRpc; + test!( add_single_data_less_than_max, ( @@ -15,9 +19,8 @@ test!( builder.add_payment(addr, 5); (builder.build().unwrap(), (arbitrary_data,)) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, data: (Vec,)| async move { - let output = - scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SRR, block, tx: Transaction, mut scanner: Scanner, data: (Vec,)| async move { + let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.arbitrary_data()[0], data.0); @@ -42,9 +45,8 @@ test!( builder.add_payment(addr, 5); (builder.build().unwrap(), data) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, data: Vec>| async move { - let output = - scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SRR, block, tx: Transaction, mut scanner: Scanner, data: Vec>| async move { + let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.arbitrary_data(), data); @@ -70,9 +72,8 @@ test!( builder.add_payment(addr, 5); (builder.build().unwrap(), data) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, data: Vec| async move { - let output = - scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SRR, block, tx: Transaction, mut scanner: Scanner, data: Vec| async move { + let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.arbitrary_data(), vec![data]); diff --git a/networks/monero/wallet/tests/decoys.rs b/networks/monero/wallet/tests/decoys.rs index 6aaaeb07..9200f7d6 100644 --- a/networks/monero/wallet/tests/decoys.rs +++ b/networks/monero/wallet/tests/decoys.rs @@ -16,9 +16,8 @@ test!( builder.add_payment(addr, 2000000000000); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let output = - scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 2000000000000); output @@ -94,9 +93,8 @@ test!( builder.add_payment(addr, 2000000000000); (builder.build().unwrap(), ()) }, - |rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let output = - scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 2000000000000); output diff --git a/networks/monero/wallet/tests/runner/mod.rs b/networks/monero/wallet/tests/runner/mod.rs index 4d042b53..b83f939a 100644 --- a/networks/monero/wallet/tests/runner/mod.rs +++ b/networks/monero/wallet/tests/runner/mod.rs @@ -105,7 +105,11 @@ pub async fn get_miner_tx_output(rpc: &SimpleRequestRpc, view: &ViewPair) -> Wal rpc.generate_blocks(&view.legacy_address(Network::Mainnet), 60).await.unwrap(); let block = rpc.get_block_by_number(start).await.unwrap(); - scanner.scan(rpc, &block).await.unwrap().ignore_additional_timelock().swap_remove(0) + scanner + .scan(rpc.get_scannable_block(block).await.unwrap()) + .unwrap() + .ignore_additional_timelock() + .swap_remove(0) } /// Make sure the weight and fee match the expected calculation. @@ -315,6 +319,7 @@ macro_rules! test { rpc.publish_transaction(&signed).await.unwrap(); let block = mine_until_unlocked(&rpc, &random_address().2, signed.hash()).await; + let block = rpc.get_scannable_block(block).await.unwrap(); let tx = rpc.get_transaction(signed.hash()).await.unwrap(); check_weight_and_fee(&tx, fee_rate); let scanner = Scanner::new(view.clone()); @@ -336,6 +341,7 @@ macro_rules! test { rpc.publish_transaction(&signed).await.unwrap(); let block = mine_until_unlocked(&rpc, &random_address().2, signed.hash()).await; + let block = rpc.get_scannable_block(block).await.unwrap(); let tx = rpc.get_transaction(signed.hash()).await.unwrap(); if stringify!($name) != "spend_one_input_to_two_outputs_no_change" { // Skip weight and fee check for the above test because when there is no change, diff --git a/networks/monero/wallet/tests/scan.rs b/networks/monero/wallet/tests/scan.rs index b2a51c60..1eb7583b 100644 --- a/networks/monero/wallet/tests/scan.rs +++ b/networks/monero/wallet/tests/scan.rs @@ -1,8 +1,14 @@ -use monero_serai::transaction::Transaction; -use monero_wallet::{rpc::Rpc, address::SubaddressIndex, extra::PaymentId, GuaranteedScanner}; +use monero_simple_request_rpc::SimpleRequestRpc; +use monero_wallet::{ + transaction::Transaction, rpc::Rpc, address::SubaddressIndex, extra::PaymentId, GuaranteedScanner, +}; mod runner; +#[allow(clippy::upper_case_acronyms)] +type SRR = SimpleRequestRpc; +type Tx = Transaction; + test!( scan_standard_address, ( @@ -12,8 +18,8 @@ test!( builder.add_payment(view.legacy_address(Network::Mainnet), 5); (builder.build().unwrap(), scanner) }, - |rpc, block, tx: Transaction, _, mut state: Scanner| async move { - let output = state.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SRR, block, tx: Transaction, _, mut state: Scanner| async move { + let output = state.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); let dummy_payment_id = PaymentId::Encrypted([0u8; 8]); @@ -35,9 +41,8 @@ test!( builder.add_payment(view.subaddress(Network::Mainnet, subaddress), 5); (builder.build().unwrap(), (scanner, subaddress)) }, - |rpc, block, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move { - let output = - state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SRR, block, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move { + let output = state.0.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.subaddress(), Some(state.1)); @@ -58,9 +63,8 @@ test!( builder.add_payment(view.legacy_integrated_address(Network::Mainnet, payment_id), 5); (builder.build().unwrap(), (scanner, payment_id)) }, - |rpc, block, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move { - let output = - state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SRR, block, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move { + let output = state.0.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1))); @@ -77,9 +81,8 @@ test!( builder.add_payment(view.address(Network::Mainnet, None, None), 5); (builder.build().unwrap(), scanner) }, - |rpc, block, tx: Transaction, _, mut scanner: GuaranteedScanner| async move { - let output = - scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SRR, block, tx: Transaction, _, mut scanner: GuaranteedScanner| async move { + let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.subaddress(), None); @@ -100,9 +103,8 @@ test!( builder.add_payment(view.address(Network::Mainnet, Some(subaddress), None), 5); (builder.build().unwrap(), (scanner, subaddress)) }, - |rpc, block, tx: Transaction, _, mut state: (GuaranteedScanner, SubaddressIndex)| async move { - let output = - state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SRR, block, tx: Tx, _, mut state: (GuaranteedScanner, SubaddressIndex)| async move { + let output = state.0.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.subaddress(), Some(state.1)); @@ -122,9 +124,8 @@ test!( builder.add_payment(view.address(Network::Mainnet, None, Some(payment_id)), 5); (builder.build().unwrap(), (scanner, payment_id)) }, - |rpc, block, tx: Transaction, _, mut state: (GuaranteedScanner, [u8; 8])| async move { - let output = - state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SRR, block, tx: Transaction, _, mut state: (GuaranteedScanner, [u8; 8])| async move { + let output = state.0.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1))); @@ -132,7 +133,6 @@ test!( ), ); -#[rustfmt::skip] test!( scan_guaranteed_integrated_subaddress, ( @@ -149,14 +149,8 @@ test!( builder.add_payment(view.address(Network::Mainnet, Some(subaddress), Some(payment_id)), 5); (builder.build().unwrap(), (scanner, payment_id, subaddress)) }, - | - rpc, - block, - tx: Transaction, - _, - mut state: (GuaranteedScanner, [u8; 8], SubaddressIndex), - | async move { - let output = state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc, block, tx: Tx, _, mut state: (GuaranteedScanner, [u8; 8], SubaddressIndex)| async move { + let output = state.0.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1))); diff --git a/networks/monero/wallet/tests/send.rs b/networks/monero/wallet/tests/send.rs index 86a801d7..de225fe1 100644 --- a/networks/monero/wallet/tests/send.rs +++ b/networks/monero/wallet/tests/send.rs @@ -4,13 +4,21 @@ use rand_core::OsRng; use monero_simple_request_rpc::SimpleRequestRpc; use monero_wallet::{ - ringct::RctType, transaction::Transaction, rpc::Rpc, address::SubaddressIndex, extra::Extra, + ringct::RctType, + transaction::Transaction, + rpc::{ScannableBlock, Rpc}, + address::SubaddressIndex, + extra::Extra, WalletOutput, OutputWithDecoys, }; mod runner; use runner::{SignableTransactionBuilder, ring_len}; +#[allow(clippy::upper_case_acronyms)] +type SRR = SimpleRequestRpc; +type SB = ScannableBlock; + // Set up inputs, select decoys, then add them to the TX builder async fn add_inputs( rct_type: RctType, @@ -40,9 +48,8 @@ test!( builder.add_payment(addr, 5); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let output = - scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); }, @@ -57,8 +64,8 @@ test!( builder.add_payment(addr, 2000000000000); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let mut outputs = scanner.scan(block).unwrap().not_additionally_locked(); assert_eq!(outputs.len(), 2); assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].transaction(), tx.hash()); @@ -74,9 +81,8 @@ test!( builder.add_payment(addr, 6); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let output = - scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 6); }, @@ -93,8 +99,8 @@ test!( builder.add_payment(addr, 1000000000000); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(block).unwrap().not_additionally_locked(); assert_eq!(outputs.len(), 1); assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].commitment().amount, 1000000000000); @@ -130,17 +136,15 @@ test!( .add_payment(sub_view.subaddress(Network::Mainnet, SubaddressIndex::new(0, 1).unwrap()), 1); (builder.build().unwrap(), (change_view, sub_view)) }, - |rpc, block, tx: Transaction, _, views: (ViewPair, ViewPair)| async move { + |_rpc: SRR, block: SB, tx: Transaction, _, views: (ViewPair, ViewPair)| async move { // Make sure the change can pick up its output let mut change_scanner = Scanner::new(views.0); - assert!( - change_scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().len() == 1 - ); + assert!(change_scanner.scan(block.clone()).unwrap().not_additionally_locked().len() == 1); // Make sure the subaddress can pick up its output let mut sub_scanner = Scanner::new(views.1); sub_scanner.register_subaddress(SubaddressIndex::new(0, 1).unwrap()); - let sub_outputs = sub_scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + let sub_outputs = sub_scanner.scan(block).unwrap().not_additionally_locked(); assert!(sub_outputs.len() == 1); assert_eq!(sub_outputs[0].transaction(), tx.hash()); assert_eq!(sub_outputs[0].commitment().amount, 1); @@ -165,8 +169,8 @@ test!( builder.add_payment(addr, 2000000000000); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(block).unwrap().not_additionally_locked(); assert_eq!(outputs.len(), 1); assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].commitment().amount, 2000000000000); @@ -179,9 +183,8 @@ test!( builder.add_payment(addr, 2); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let output = - scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 2); }, @@ -195,8 +198,8 @@ test!( builder.add_payment(addr, 1000000000000); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(block).unwrap().not_additionally_locked(); assert_eq!(outputs.len(), 1); assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].commitment().amount, 1000000000000); @@ -212,8 +215,8 @@ test!( } (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut scanned_tx = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let mut scanned_tx = scanner.scan(block).unwrap().not_additionally_locked(); let mut output_amounts = HashSet::new(); for i in 0 .. 15 { @@ -237,8 +240,8 @@ test!( builder.add_payment(addr, 1000000000000); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(block).unwrap().not_additionally_locked(); assert_eq!(outputs.len(), 1); assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].commitment().amount, 1000000000000); @@ -263,10 +266,14 @@ test!( (builder.build().unwrap(), (scanner, subaddresses)) }, - |rpc, block, tx: Transaction, _, mut state: (Scanner, Vec)| async move { + |_rpc: SimpleRequestRpc, + block, + tx: Transaction, + _, + mut state: (Scanner, Vec)| async move { use std::collections::HashMap; - let mut scanned_tx = state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + let mut scanned_tx = state.0.scan(block).unwrap().not_additionally_locked(); let mut output_amounts_by_subaddress = HashMap::new(); for i in 0 .. 15 { @@ -294,8 +301,8 @@ test!( builder.add_payment(addr, 1000000000000); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(block).unwrap().not_additionally_locked(); assert_eq!(outputs.len(), 1); assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].commitment().amount, 1000000000000); @@ -320,8 +327,8 @@ test!( (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let mut outputs = scanner.scan(block).unwrap().not_additionally_locked(); assert_eq!(outputs.len(), 2); assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[1].transaction(), tx.hash()); @@ -345,8 +352,8 @@ test!( builder.add_payment(addr, 1000000000000); (builder.build().unwrap(), ()) }, - |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { - let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(block).unwrap().not_additionally_locked(); assert_eq!(outputs.len(), 1); assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].commitment().amount, 1000000000000); @@ -381,11 +388,11 @@ test!( builder.add_payment(view.legacy_address(Network::Mainnet), 1); (builder.build().unwrap(), change_view) }, - |rpc, block, _, _, change_view: ViewPair| async move { + |_rpc: SimpleRequestRpc, block, _, _, change_view: ViewPair| async move { // Make sure the change can pick up its output let mut change_scanner = Scanner::new(change_view); change_scanner.register_subaddress(SubaddressIndex::new(0, 1).unwrap()); - let outputs = change_scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + let outputs = change_scanner.scan(block).unwrap().not_additionally_locked(); assert!(outputs.len() == 1); assert!(outputs[0].subaddress().unwrap().account() == 0); assert!(outputs[0].subaddress().unwrap().address() == 1); diff --git a/networks/monero/wallet/tests/wallet2_compatibility.rs b/networks/monero/wallet/tests/wallet2_compatibility.rs index 1a3fe4c4..f9e2a5cd 100644 --- a/networks/monero/wallet/tests/wallet2_compatibility.rs +++ b/networks/monero/wallet/tests/wallet2_compatibility.rs @@ -106,6 +106,7 @@ async fn from_wallet_rpc_to_self(spec: AddressSpec) { // unlock it let block = runner::mine_until_unlocked(&daemon_rpc, &wallet_rpc_addr, tx_hash).await; + let block = daemon_rpc.get_scannable_block(block).await.unwrap(); // Create the scanner let mut scanner = Scanner::new(view_pair); @@ -114,8 +115,7 @@ async fn from_wallet_rpc_to_self(spec: AddressSpec) { } // Retrieve it and scan it - let output = - scanner.scan(&daemon_rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0); assert_eq!(output.transaction(), tx_hash); runner::check_weight_and_fee(&daemon_rpc.get_transaction(tx_hash).await.unwrap(), fee_rate); diff --git a/processor/src/networks/monero.rs b/processor/src/networks/monero.rs index 54a3af24..154702fe 100644 --- a/processor/src/networks/monero.rs +++ b/processor/src/networks/monero.rs @@ -520,7 +520,13 @@ impl Network for Monero { async fn get_outputs(&self, block: &Block, key: EdwardsPoint) -> Vec { let outputs = loop { - match Self::scanner(key).scan(&self.rpc, block).await { + match self + .rpc + .get_scannable_block(block.clone()) + .await + .map_err(|e| format!("{e:?}")) + .and_then(|block| Self::scanner(key).scan(block).map_err(|e| format!("{e:?}"))) + { Ok(outputs) => break outputs, Err(e) => { log::error!("couldn't scan block {}: {e:?}", hex::encode(block.id())); @@ -738,8 +744,10 @@ impl Network for Monero { } let new_block = self.rpc.get_block_by_number(new_block).await.unwrap(); - let mut outputs = - Self::test_scanner().scan(&self.rpc, &new_block).await.unwrap().ignore_additional_timelock(); + let mut outputs = Self::test_scanner() + .scan(self.rpc.get_scannable_block(new_block.clone()).await.unwrap()) + .unwrap() + .ignore_additional_timelock(); let output = outputs.swap_remove(0); let amount = output.commitment().amount; diff --git a/tests/full-stack/src/tests/mint_and_burn.rs b/tests/full-stack/src/tests/mint_and_burn.rs index 3bb9e11f..ce19808f 100644 --- a/tests/full-stack/src/tests/mint_and_burn.rs +++ b/tests/full-stack/src/tests/mint_and_burn.rs @@ -357,8 +357,7 @@ async fn mint_and_burn_test() { let view_pair = ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE)).unwrap(); let mut scanner = Scanner::new(view_pair.clone()); let output = scanner - .scan(&rpc, &rpc.get_block_by_number(1).await.unwrap()) - .await + .scan(rpc.get_scannable_block_by_number(1).await.unwrap()) .unwrap() .additional_timelock_satisfied_by(rpc.get_height().await.unwrap(), 0) .swap_remove(0); @@ -587,7 +586,10 @@ async fn mint_and_burn_test() { while i < (5 * 6) { if let Ok(block) = rpc.get_block_by_number(start_monero_block).await { start_monero_block += 1; - let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + let outputs = scanner + .scan(rpc.get_scannable_block(block.clone()).await.unwrap()) + .unwrap() + .not_additionally_locked(); if !outputs.is_empty() { assert_eq!(outputs.len(), 1); diff --git a/tests/processor/src/networks.rs b/tests/processor/src/networks.rs index eae77c01..32563c9f 100644 --- a/tests/processor/src/networks.rs +++ b/tests/processor/src/networks.rs @@ -429,8 +429,7 @@ impl Wallet { block.transactions.contains(&last_tx.1) { outputs = Scanner::new(view_pair.clone()) - .scan(&rpc, &block) - .await + .scan(rpc.get_scannable_block(block).await.unwrap()) .unwrap() .ignore_additional_timelock(); }