Add ScannableBlock abstraction in the RPC
Some checks failed
Coordinator Tests / build (push) Has been cancelled
Full Stack Tests / build (push) Has been cancelled
Lint / clippy (macos-13) (push) Has been cancelled
Lint / clippy (macos-14) (push) Has been cancelled
Processor Tests / build (push) Has been cancelled
Reproducible Runtime / build (push) Has been cancelled
Lint / clippy (ubuntu-latest) (push) Has been cancelled
Lint / clippy (windows-latest) (push) Has been cancelled
Lint / deny (push) Has been cancelled
Lint / fmt (push) Has been cancelled
Lint / machete (push) Has been cancelled
Monero Tests / unit-tests (push) Has been cancelled
Monero Tests / integration-tests (v0.17.3.2) (push) Has been cancelled
Monero Tests / integration-tests (v0.18.3.4) (push) Has been cancelled
networks/ Tests / test-networks (push) Has been cancelled
no-std build / build (push) Has been cancelled
Tests / test-infra (push) Has been cancelled
Tests / test-substrate (push) Has been cancelled
Tests / test-serai-client (push) Has been cancelled

Makes scanning synchronous and only error upon a malicious node/unplanned for
hard fork.
This commit is contained in:
Luke Parker 2024-09-13 04:26:08 -04:00
parent 2c7148d636
commit bdcc061bb4
No known key found for this signature in database
12 changed files with 252 additions and 177 deletions

View file

@ -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<Transaction<Pruned>>,
/// 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<u64>,
}
/// 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<Output = Result<ScannableBlock, RpcError>> {
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::<Pruned>::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<Output = Result<ScannableBlock, RpcError>> {
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<Output = Result<ScannableBlock, RpcError>> {
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.

View file

@ -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;

View file

@ -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<Pruned>,
) -> Result<Timelocked, RpcError> {
) -> Result<Timelocked, ScanError> {
// 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<Timelocked, RpcError> {
fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
// 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::<Pruned>::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<Timelocked, RpcError> {
self.0.scan(rpc, block).await
pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
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<Timelocked, RpcError> {
self.0.scan(rpc, block).await
pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
self.0.scan(block)
}
}

View file

@ -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<u8>,)| 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<u8>,)| 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<Vec<u8>>| 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<Vec<u8>>| 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<u8>| 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<u8>| 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]);

View file

@ -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

View file

@ -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,

View file

@ -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)));

View file

@ -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<SubaddressIndex>)| async move {
|_rpc: SimpleRequestRpc,
block,
tx: Transaction,
_,
mut state: (Scanner, Vec<SubaddressIndex>)| 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);

View file

@ -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);

View file

@ -520,7 +520,13 @@ impl Network for Monero {
async fn get_outputs(&self, block: &Block, key: EdwardsPoint) -> Vec<Output> {
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;

View file

@ -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);

View file

@ -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();
}