3 Commits

Author SHA1 Message Date
Luke Parker
bdcc061bb4 Add ScannableBlock abstraction in the RPC
Makes scanning synchronous and only error upon a malicious node/unplanned for
hard fork.
2024-09-13 04:38:49 -04:00
Luke Parker
2c7148d636 Add machete exception for monero-clsag to monero-wallet 2024-09-13 02:39:43 -04:00
Luke Parker
6b270bc6aa Remove async-trait from monero-rpc 2024-09-13 02:36:53 -04:00
17 changed files with 998 additions and 842 deletions

2
Cargo.lock generated
View File

@@ -4949,7 +4949,6 @@ dependencies = [
name = "monero-rpc" name = "monero-rpc"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait",
"curve25519-dalek", "curve25519-dalek",
"hex", "hex",
"monero-address", "monero-address",
@@ -5013,7 +5012,6 @@ dependencies = [
name = "monero-simple-request-rpc" name = "monero-simple-request-rpc"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait",
"digest_auth", "digest_auth",
"hex", "hex",
"monero-address", "monero-address",

View File

@@ -18,7 +18,6 @@ workspace = true
[dependencies] [dependencies]
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false } std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
async-trait = { version = "0.1", default-features = false }
thiserror = { version = "1", default-features = false, optional = true } thiserror = { version = "1", default-features = false, optional = true }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }

View File

@@ -16,8 +16,6 @@ rustdoc-args = ["--cfg", "docsrs"]
workspace = true workspace = true
[dependencies] [dependencies]
async-trait = { version = "0.1", default-features = false }
hex = { version = "0.4", default-features = false, features = ["alloc"] } hex = { version = "0.4", default-features = false, features = ["alloc"] }
digest_auth = { version = "0.3", default-features = false } digest_auth = { version = "0.3", default-features = false }
simple-request = { path = "../../../../common/request", version = "0.1", default-features = false, features = ["tls"] } simple-request = { path = "../../../../common/request", version = "0.1", default-features = false, features = ["tls"] }

View File

@@ -2,10 +2,9 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![deny(missing_docs)] #![deny(missing_docs)]
use core::future::Future;
use std::{sync::Arc, io::Read, time::Duration}; use std::{sync::Arc, io::Read, time::Duration};
use async_trait::async_trait;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use digest_auth::{WwwAuthenticateHeader, AuthContext}; use digest_auth::{WwwAuthenticateHeader, AuthContext};
@@ -280,11 +279,16 @@ impl SimpleRequestRpc {
} }
} }
#[async_trait]
impl Rpc for SimpleRequestRpc { impl Rpc for SimpleRequestRpc {
async fn post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError> { fn post(
&self,
route: &str,
body: Vec<u8>,
) -> impl Send + Future<Output = Result<Vec<u8>, RpcError>> {
async move {
tokio::time::timeout(self.request_timeout, self.inner_post(route, body)) tokio::time::timeout(self.request_timeout, self.inner_post(route, body))
.await .await
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))? .map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
} }
}
} }

View File

@@ -4,11 +4,12 @@
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
use core::{ use core::{
future::Future,
fmt::Debug, fmt::Debug,
ops::{Bound, RangeBounds}, ops::{Bound, RangeBounds},
}; };
use std_shims::{ use std_shims::{
alloc::{boxed::Box, format}, alloc::format,
vec, vec,
vec::Vec, vec::Vec,
io, io,
@@ -17,8 +18,6 @@ use std_shims::{
use zeroize::Zeroize; use zeroize::Zeroize;
use async_trait::async_trait;
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint}; use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
use serde::{Serialize, Deserialize, de::DeserializeOwned}; use serde::{Serialize, Deserialize, de::DeserializeOwned};
@@ -74,6 +73,19 @@ pub enum RpcError {
InvalidPriority, 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. /// A struct containing a fee rate.
/// ///
/// The fee rate is defined as a per-weight cost, along with a mask for rounding purposes. /// The fee rate is defined as a per-weight cost, along with a mask for rounding purposes.
@@ -237,22 +249,26 @@ fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
/// While no implementors are directly provided, [monero-simple-request-rpc]( /// While no implementors are directly provided, [monero-simple-request-rpc](
/// https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc/simple-request /// https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc/simple-request
/// ) is recommended. /// ) is recommended.
#[async_trait]
pub trait Rpc: Sync + Clone + Debug { pub trait Rpc: Sync + Clone + Debug {
/// Perform a POST request to the specified route with the specified body. /// Perform a POST request to the specified route with the specified body.
/// ///
/// The implementor is left to handle anything such as authentication. /// The implementor is left to handle anything such as authentication.
async fn post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError>; fn post(
&self,
route: &str,
body: Vec<u8>,
) -> impl Send + Future<Output = Result<Vec<u8>, RpcError>>;
/// Perform a RPC call to the specified route with the provided parameters. /// Perform a RPC call to the specified route with the provided parameters.
/// ///
/// This is NOT a JSON-RPC call. They use a route of "json_rpc" and are available via /// This is NOT a JSON-RPC call. They use a route of "json_rpc" and are available via
/// `json_rpc_call`. /// `json_rpc_call`.
async fn rpc_call<Params: Send + Serialize + Debug, Response: DeserializeOwned + Debug>( fn rpc_call<Params: Send + Serialize + Debug, Response: DeserializeOwned + Debug>(
&self, &self,
route: &str, route: &str,
params: Option<Params>, params: Option<Params>,
) -> Result<Response, RpcError> { ) -> impl Send + Future<Output = Result<Response, RpcError>> {
async move {
let res = self let res = self
.post( .post(
route, route,
@@ -268,29 +284,37 @@ pub trait Rpc: Sync + Clone + Debug {
serde_json::from_str(res_str) serde_json::from_str(res_str)
.map_err(|_| RpcError::InvalidNode(format!("response wasn't the expected json: {res_str}"))) .map_err(|_| RpcError::InvalidNode(format!("response wasn't the expected json: {res_str}")))
} }
}
/// Perform a JSON-RPC call with the specified method with the provided parameters. /// Perform a JSON-RPC call with the specified method with the provided parameters.
async fn json_rpc_call<Response: DeserializeOwned + Debug>( fn json_rpc_call<Response: DeserializeOwned + Debug>(
&self, &self,
method: &str, method: &str,
params: Option<Value>, params: Option<Value>,
) -> Result<Response, RpcError> { ) -> impl Send + Future<Output = Result<Response, RpcError>> {
async move {
let mut req = json!({ "method": method }); let mut req = json!({ "method": method });
if let Some(params) = params { if let Some(params) = params {
req.as_object_mut().unwrap().insert("params".into(), params); req.as_object_mut().unwrap().insert("params".into(), params);
} }
Ok(self.rpc_call::<_, JsonRpcResponse<Response>>("json_rpc", Some(req)).await?.result) Ok(self.rpc_call::<_, JsonRpcResponse<Response>>("json_rpc", Some(req)).await?.result)
} }
}
/// Perform a binary call to the specified route with the provided parameters. /// Perform a binary call to the specified route with the provided parameters.
async fn bin_call(&self, route: &str, params: Vec<u8>) -> Result<Vec<u8>, RpcError> { fn bin_call(
self.post(route, params).await &self,
route: &str,
params: Vec<u8>,
) -> impl Send + Future<Output = Result<Vec<u8>, RpcError>> {
async move { self.post(route, params).await }
} }
/// Get the active blockchain protocol version. /// Get the active blockchain protocol version.
/// ///
/// This is specifically the major version within the most recent block header. /// This is specifically the major version within the most recent block header.
async fn get_hardfork_version(&self) -> Result<u8, RpcError> { fn get_hardfork_version(&self) -> impl Send + Future<Output = Result<u8, RpcError>> {
async move {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct HeaderResponse { struct HeaderResponse {
major_version: u8, major_version: u8,
@@ -309,12 +333,14 @@ pub trait Rpc: Sync + Clone + Debug {
.major_version, .major_version,
) )
} }
}
/// Get the height of the Monero blockchain. /// Get the height of the Monero blockchain.
/// ///
/// The height is defined as the amount of blocks on the blockchain. For a blockchain with only /// The height is defined as the amount of blocks on the blockchain. For a blockchain with only
/// its genesis block, the height will be 1. /// its genesis block, the height will be 1.
async fn get_height(&self) -> Result<usize, RpcError> { fn get_height(&self) -> impl Send + Future<Output = Result<usize, RpcError>> {
async move {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct HeightResponse { struct HeightResponse {
height: usize, height: usize,
@@ -325,12 +351,17 @@ pub trait Rpc: Sync + Clone + Debug {
} }
Ok(res) Ok(res)
} }
}
/// Get the specified transactions. /// Get the specified transactions.
/// ///
/// The received transactions will be hashed in order to verify the correct transactions were /// The received transactions will be hashed in order to verify the correct transactions were
/// returned. /// returned.
async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result<Vec<Transaction>, RpcError> { fn get_transactions(
&self,
hashes: &[[u8; 32]],
) -> impl Send + Future<Output = Result<Vec<Transaction>, RpcError>> {
async move {
if hashes.is_empty() { if hashes.is_empty() {
return Ok(vec![]); return Ok(vec![]);
} }
@@ -396,12 +427,14 @@ pub trait Rpc: Sync + Clone + Debug {
}) })
.collect() .collect()
} }
}
/// Get the specified transactions in their pruned format. /// Get the specified transactions in their pruned format.
async fn get_pruned_transactions( fn get_pruned_transactions(
&self, &self,
hashes: &[[u8; 32]], hashes: &[[u8; 32]],
) -> Result<Vec<Transaction<Pruned>>, RpcError> { ) -> impl Send + Future<Output = Result<Vec<Transaction<Pruned>>, RpcError>> {
async move {
if hashes.is_empty() { if hashes.is_empty() {
return Ok(vec![]); return Ok(vec![]);
} }
@@ -447,25 +480,36 @@ pub trait Rpc: Sync + Clone + Debug {
}) })
.collect() .collect()
} }
}
/// Get the specified transaction. /// Get the specified transaction.
/// ///
/// The received transaction will be hashed in order to verify the correct transaction was /// The received transaction will be hashed in order to verify the correct transaction was
/// returned. /// returned.
async fn get_transaction(&self, tx: [u8; 32]) -> Result<Transaction, RpcError> { fn get_transaction(
self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) &self,
tx: [u8; 32],
) -> impl Send + Future<Output = Result<Transaction, RpcError>> {
async move { self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) }
} }
/// Get the specified transaction in its pruned format. /// Get the specified transaction in its pruned format.
async fn get_pruned_transaction(&self, tx: [u8; 32]) -> Result<Transaction<Pruned>, RpcError> { fn get_pruned_transaction(
self.get_pruned_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) &self,
tx: [u8; 32],
) -> impl Send + Future<Output = Result<Transaction<Pruned>, RpcError>> {
async move { self.get_pruned_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) }
} }
/// Get the hash of a block from the node. /// Get the hash of a block from the node.
/// ///
/// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block, /// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block,
/// `height - 1` for the latest block). /// `height - 1` for the latest block).
async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> { fn get_block_hash(
&self,
number: usize,
) -> impl Send + Future<Output = Result<[u8; 32], RpcError>> {
async move {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct BlockHeaderResponse { struct BlockHeaderResponse {
hash: String, hash: String,
@@ -479,11 +523,13 @@ pub trait Rpc: Sync + Clone + Debug {
self.json_rpc_call("get_block_header_by_height", Some(json!({ "height": number }))).await?; self.json_rpc_call("get_block_header_by_height", Some(json!({ "height": number }))).await?;
hash_hex(&header.block_header.hash) hash_hex(&header.block_header.hash)
} }
}
/// Get a block from the node by its hash. /// Get a block from the node by its hash.
/// ///
/// The received block will be hashed in order to verify the correct block was returned. /// The received block will be hashed in order to verify the correct block was returned.
async fn get_block(&self, hash: [u8; 32]) -> Result<Block, RpcError> { fn get_block(&self, hash: [u8; 32]) -> impl Send + Future<Output = Result<Block, RpcError>> {
async move {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct BlockResponse { struct BlockResponse {
blob: String, blob: String,
@@ -499,12 +545,17 @@ pub trait Rpc: Sync + Clone + Debug {
} }
Ok(block) Ok(block)
} }
}
/// Get a block from the node by its number. /// Get a block from the node by its number.
/// ///
/// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block, /// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block,
/// `height - 1` for the latest block). /// `height - 1` for the latest block).
async fn get_block_by_number(&self, number: usize) -> Result<Block, RpcError> { fn get_block_by_number(
&self,
number: usize,
) -> impl Send + Future<Output = Result<Block, RpcError>> {
async move {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct BlockResponse { struct BlockResponse {
blob: String, blob: String,
@@ -530,13 +581,107 @@ 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. /// Get the currently estimated fee rate from the node.
/// ///
/// This may be manipulated to unsafe levels and MUST be sanity checked. /// This may be manipulated to unsafe levels and MUST be sanity checked.
/// ///
/// This MUST NOT be expected to be deterministic in any way. /// This MUST NOT be expected to be deterministic in any way.
async fn get_fee_rate(&self, priority: FeePriority) -> Result<FeeRate, RpcError> { fn get_fee_rate(
&self,
priority: FeePriority,
) -> impl Send + Future<Output = Result<FeeRate, RpcError>> {
async move {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct FeeResponse { struct FeeResponse {
status: String, status: String,
@@ -576,8 +721,11 @@ pub trait Rpc: Sync + Clone + Debug {
// src/wallet/wallet2.cpp#L7569-L7584 // src/wallet/wallet2.cpp#L7569-L7584
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/ // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
// src/wallet/wallet2.cpp#L7660-L7661 // src/wallet/wallet2.cpp#L7660-L7661
let priority_idx = let priority_idx = usize::try_from(if priority.fee_priority() == 0 {
usize::try_from(if priority.fee_priority() == 0 { 1 } else { priority.fee_priority() - 1 }) 1
} else {
priority.fee_priority() - 1
})
.map_err(|_| RpcError::InvalidPriority)?; .map_err(|_| RpcError::InvalidPriority)?;
let multipliers = [1, 5, 25, 1000]; let multipliers = [1, 5, 25, 1000];
if priority_idx >= multipliers.len() { if priority_idx >= multipliers.len() {
@@ -589,9 +737,14 @@ pub trait Rpc: Sync + Clone + Debug {
FeeRate::new(res.fee * fee_multiplier, res.quantization_mask) FeeRate::new(res.fee * fee_multiplier, res.quantization_mask)
} }
} }
}
/// Publish a transaction. /// Publish a transaction.
async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> { fn publish_transaction(
&self,
tx: &Transaction,
) -> impl Send + Future<Output = Result<(), RpcError>> {
async move {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct SendRawResponse { struct SendRawResponse {
@@ -621,15 +774,17 @@ pub trait Rpc: Sync + Clone + Debug {
Ok(()) Ok(())
} }
}
/// Generate blocks, with the specified address receiving the block reward. /// Generate blocks, with the specified address receiving the block reward.
/// ///
/// Returns the hashes of the generated blocks and the last block's number. /// Returns the hashes of the generated blocks and the last block's number.
async fn generate_blocks<const ADDR_BYTES: u128>( fn generate_blocks<const ADDR_BYTES: u128>(
&self, &self,
address: &Address<ADDR_BYTES>, address: &Address<ADDR_BYTES>,
block_count: usize, block_count: usize,
) -> Result<(Vec<[u8; 32]>, usize), RpcError> { ) -> impl Send + Future<Output = Result<(Vec<[u8; 32]>, usize), RpcError>> {
async move {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct BlocksResponse { struct BlocksResponse {
blocks: Vec<String>, blocks: Vec<String>,
@@ -652,11 +807,16 @@ pub trait Rpc: Sync + Clone + Debug {
} }
Ok((blocks, res.height)) Ok((blocks, res.height))
} }
}
/// Get the output indexes of the specified transaction. /// Get the output indexes of the specified transaction.
async fn get_o_indexes(&self, hash: [u8; 32]) -> Result<Vec<u64>, RpcError> { fn get_o_indexes(
// Given the immaturity of Rust epee libraries, this is a homegrown one which is only validated &self,
// to work against this specific function hash: [u8; 32],
) -> impl Send + Future<Output = Result<Vec<u64>, RpcError>> {
async move {
// Given the immaturity of Rust epee libraries, this is a homegrown one which is only
// validated to work against this specific function
// Header for EPEE, an 8-byte magic and a version // Header for EPEE, an 8-byte magic and a version
const EPEE_HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01"; const EPEE_HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01";
@@ -735,7 +895,9 @@ pub trait Rpc: Sync + Clone + Debug {
// claim this to be a complete deserialization function // claim this to be a complete deserialization function
// To ensure it works for this specific use case, it's best to ensure it's limited // To ensure it works for this specific use case, it's best to ensure it's limited
// to this specific use case (ensuring we have less variables to deal with) // to this specific use case (ensuring we have less variables to deal with)
_ => Err(io::Error::other(format!("unrecognized field in get_o_indexes: {name:?}")))?, _ => {
Err(io::Error::other(format!("unrecognized field in get_o_indexes: {name:?}")))?
}
}; };
if (expected_type != kind) || (expected_array_flag != has_array_flag) { if (expected_type != kind) || (expected_array_flag != has_array_flag) {
let fmt_array_bool = |array_bool| if array_bool { "array" } else { "not array" }; let fmt_array_bool = |array_bool| if array_bool { "array" } else { "not array" };
@@ -833,6 +995,7 @@ pub trait Rpc: Sync + Clone + Debug {
})() })()
.map_err(|e| RpcError::InvalidNode(format!("invalid binary response: {e:?}"))) .map_err(|e| RpcError::InvalidNode(format!("invalid binary response: {e:?}")))
} }
}
} }
/// A trait for any object which can be used to select RingCT decoys. /// A trait for any object which can be used to select RingCT decoys.
@@ -840,25 +1003,29 @@ pub trait Rpc: Sync + Clone + Debug {
/// An implementation is provided for any satisfier of `Rpc`. It is not recommended to use an `Rpc` /// An implementation is provided for any satisfier of `Rpc`. It is not recommended to use an `Rpc`
/// object to satisfy this. This should be satisfied by a local store of the output distribution, /// object to satisfy this. This should be satisfied by a local store of the output distribution,
/// both for performance and to prevent potential attacks a remote node can perform. /// both for performance and to prevent potential attacks a remote node can perform.
#[async_trait]
pub trait DecoyRpc: Sync + Clone + Debug { pub trait DecoyRpc: Sync + Clone + Debug {
/// Get the height the output distribution ends at. /// Get the height the output distribution ends at.
/// ///
/// This is equivalent to the hight of the blockchain it's for. This is intended to be cheaper /// This is equivalent to the hight of the blockchain it's for. This is intended to be cheaper
/// than fetching the entire output distribution. /// than fetching the entire output distribution.
async fn get_output_distribution_end_height(&self) -> Result<usize, RpcError>; fn get_output_distribution_end_height(
&self,
) -> impl Send + Future<Output = Result<usize, RpcError>>;
/// Get the RingCT (zero-amount) output distribution. /// Get the RingCT (zero-amount) output distribution.
/// ///
/// `range` is in terms of block numbers. The result may be smaller than the requested range if /// `range` is in terms of block numbers. The result may be smaller than the requested range if
/// the range starts before RingCT outputs were created on-chain. /// the range starts before RingCT outputs were created on-chain.
async fn get_output_distribution( fn get_output_distribution(
&self, &self,
range: impl Send + RangeBounds<usize>, range: impl Send + RangeBounds<usize>,
) -> Result<Vec<u64>, RpcError>; ) -> impl Send + Future<Output = Result<Vec<u64>, RpcError>>;
/// Get the specified outputs from the RingCT (zero-amount) pool. /// Get the specified outputs from the RingCT (zero-amount) pool.
async fn get_outs(&self, indexes: &[u64]) -> Result<Vec<OutputInformation>, RpcError>; fn get_outs(
&self,
indexes: &[u64],
) -> impl Send + Future<Output = Result<Vec<OutputInformation>, RpcError>>;
/// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their /// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their
/// timelock has been satisfied. /// timelock has been satisfied.
@@ -871,24 +1038,26 @@ pub trait DecoyRpc: Sync + Clone + Debug {
/// used, yet the transaction's timelock is checked to be unlocked at the specified `height`. /// used, yet the transaction's timelock is checked to be unlocked at the specified `height`.
/// This offers a deterministic decoy selection, yet is fingerprintable as time-based timelocks /// This offers a deterministic decoy selection, yet is fingerprintable as time-based timelocks
/// aren't evaluated (and considered locked, preventing their selection). /// aren't evaluated (and considered locked, preventing their selection).
async fn get_unlocked_outputs( fn get_unlocked_outputs(
&self, &self,
indexes: &[u64], indexes: &[u64],
height: usize, height: usize,
fingerprintable_deterministic: bool, fingerprintable_deterministic: bool,
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError>; ) -> impl Send + Future<Output = Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError>>;
} }
#[async_trait]
impl<R: Rpc> DecoyRpc for R { impl<R: Rpc> DecoyRpc for R {
async fn get_output_distribution_end_height(&self) -> Result<usize, RpcError> { fn get_output_distribution_end_height(
<Self as Rpc>::get_height(self).await &self,
) -> impl Send + Future<Output = Result<usize, RpcError>> {
async move { <Self as Rpc>::get_height(self).await }
} }
async fn get_output_distribution( fn get_output_distribution(
&self, &self,
range: impl Send + RangeBounds<usize>, range: impl Send + RangeBounds<usize>,
) -> Result<Vec<u64>, RpcError> { ) -> impl Send + Future<Output = Result<Vec<u64>, RpcError>> {
async move {
#[derive(Default, Debug, Deserialize)] #[derive(Default, Debug, Deserialize)]
struct Distribution { struct Distribution {
distribution: Vec<u64>, distribution: Vec<u64>,
@@ -904,9 +1073,9 @@ impl<R: Rpc> DecoyRpc for R {
let from = match range.start_bound() { let from = match range.start_bound() {
Bound::Included(from) => *from, Bound::Included(from) => *from,
Bound::Excluded(from) => from Bound::Excluded(from) => from.checked_add(1).ok_or_else(|| {
.checked_add(1) RpcError::InternalError("range's from wasn't representable".to_string())
.ok_or_else(|| RpcError::InternalError("range's from wasn't representable".to_string()))?, })?,
Bound::Unbounded => 0, Bound::Unbounded => 0,
}; };
let to = match range.end_bound() { let to = match range.end_bound() {
@@ -947,8 +1116,8 @@ impl<R: Rpc> DecoyRpc for R {
let Distribution { start_height, mut distribution } = core::mem::take(&mut distributions[0]); let Distribution { start_height, mut distribution } = core::mem::take(&mut distributions[0]);
// start_height is also actually a block number, and it should be at least `from` // start_height is also actually a block number, and it should be at least `from`
// It may be after depending on when these outputs first appeared on the blockchain // It may be after depending on when these outputs first appeared on the blockchain
// Unfortunately, we can't validate without a binary search to find the RingCT activation block // Unfortunately, we can't validate without a binary search to find the RingCT activation
// and an iterative search from there, so we solely sanity check it // block and an iterative search from there, so we solely sanity check it
if start_height < from { if start_height < from {
Err(RpcError::InvalidNode(format!( Err(RpcError::InvalidNode(format!(
"requested distribution from {from} and got from {start_height}" "requested distribution from {from} and got from {start_height}"
@@ -971,14 +1140,20 @@ impl<R: Rpc> DecoyRpc for R {
)))?; )))?;
} }
// Requesting to = 0 returns the distribution for the entire chain // Requesting to = 0 returns the distribution for the entire chain
// We work-around this by requesting 0, 1 (yielding two blocks), then popping the second block // We work around this by requesting 0, 1 (yielding two blocks), then popping the second
// block
if zero_zero_case { if zero_zero_case {
distribution.pop(); distribution.pop();
} }
Ok(distribution) Ok(distribution)
} }
}
async fn get_outs(&self, indexes: &[u64]) -> Result<Vec<OutputInformation>, RpcError> { fn get_outs(
&self,
indexes: &[u64],
) -> impl Send + Future<Output = Result<Vec<OutputInformation>, RpcError>> {
async move {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct OutputResponse { struct OutputResponse {
height: usize, height: usize,
@@ -1040,13 +1215,15 @@ impl<R: Rpc> DecoyRpc for R {
Ok(res) Ok(res)
} }
}
async fn get_unlocked_outputs( fn get_unlocked_outputs(
&self, &self,
indexes: &[u64], indexes: &[u64],
height: usize, height: usize,
fingerprintable_deterministic: bool, fingerprintable_deterministic: bool,
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> { ) -> impl Send + Future<Output = Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError>> {
async move {
let outs = self.get_outs(indexes).await?; let outs = self.get_outs(indexes).await?;
// Only need to fetch txs to do deterministic check on timelock // Only need to fetch txs to do deterministic check on timelock
@@ -1061,8 +1238,8 @@ impl<R: Rpc> DecoyRpc for R {
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, out)| { .map(|(i, out)| {
// Allow keys to be invalid, though if they are, return None to trigger selection of a new // Allow keys to be invalid, though if they are, return None to trigger selection of a
// decoy // new decoy
// Only valid keys can be used in CLSAG proofs, hence the need for re-selection, yet // Only valid keys can be used in CLSAG proofs, hence the need for re-selection, yet
// invalid keys may honestly exist on the blockchain // invalid keys may honestly exist on the blockchain
let Some(key) = out.key.decompress() else { let Some(key) = out.key.decompress() else {
@@ -1071,11 +1248,13 @@ impl<R: Rpc> DecoyRpc for R {
Ok(Some([key, out.commitment]).filter(|_| { Ok(Some([key, out.commitment]).filter(|_| {
if fingerprintable_deterministic { if fingerprintable_deterministic {
// https://github.com/monero-project/monero/blob // https://github.com/monero-project/monero/blob
// /cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core/blockchain.cpp#L90 // /cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core
// /blockchain.cpp#L90
const ACCEPTED_TIMELOCK_DELTA: usize = 1; const ACCEPTED_TIMELOCK_DELTA: usize = 1;
// https://github.com/monero-project/monero/blob // https://github.com/monero-project/monero/blob
// /cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core/blockchain.cpp#L3836 // /cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core
// /blockchain.cpp#L3836
((out.height + DEFAULT_LOCK_WINDOW) <= height) && ((out.height + DEFAULT_LOCK_WINDOW) <= height) &&
(Timelock::Block(height - 1 + ACCEPTED_TIMELOCK_DELTA) >= (Timelock::Block(height - 1 + ACCEPTED_TIMELOCK_DELTA) >=
txs[i].prefix().additional_timelock) txs[i].prefix().additional_timelock)
@@ -1086,4 +1265,5 @@ impl<R: Rpc> DecoyRpc for R {
}) })
.collect() .collect()
} }
}
} }

View File

@@ -11,6 +11,10 @@ rust-version = "1.80"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
rust-version = "1.80"
[package.metadata.cargo-machete]
ignored = ["monero-clsag"]
[lints] [lints]
workspace = true workspace = true

View File

@@ -23,7 +23,7 @@ pub use monero_rpc as rpc;
pub use monero_address as address; pub use monero_address as address;
mod view_pair; 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. /// Structures and functionality for working with transactions' extra fields.
pub mod extra; pub mod extra;
@@ -33,7 +33,7 @@ pub(crate) mod output;
pub use output::WalletOutput; pub use output::WalletOutput;
mod scan; mod scan;
pub use scan::{Scanner, GuaranteedScanner}; pub use scan::{ScanError, Scanner, GuaranteedScanner};
mod decoys; mod decoys;
pub use decoys::OutputWithDecoys; pub use decoys::OutputWithDecoys;

View File

@@ -1,16 +1,15 @@
use core::ops::Deref; 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 zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY}; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY};
use monero_rpc::{RpcError, Rpc}; use monero_rpc::ScannableBlock;
use monero_serai::{ use monero_serai::{
io::*, io::*,
primitives::Commitment, primitives::Commitment,
transaction::{Timelock, Pruned, Transaction}, transaction::{Timelock, Pruned, Transaction},
block::Block,
}; };
use crate::{ use crate::{
address::SubaddressIndex, ViewPair, GuaranteedViewPair, output::*, PaymentId, Extra, 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)] #[derive(Clone)]
struct InternalScanner { struct InternalScanner {
pair: ViewPair, pair: ViewPair,
@@ -107,10 +118,10 @@ impl InternalScanner {
fn scan_transaction( fn scan_transaction(
&self, &self,
tx_start_index_on_blockchain: u64, output_index_for_first_ringct_output: u64,
tx_hash: [u8; 32], tx_hash: [u8; 32],
tx: &Transaction<Pruned>, tx: &Transaction<Pruned>,
) -> Result<Timelocked, RpcError> { ) -> Result<Timelocked, ScanError> {
// Only scan TXs creating RingCT outputs // Only scan TXs creating RingCT outputs
// For the full details on why this check is equivalent, please see the documentation in `scan` // For the full details on why this check is equivalent, please see the documentation in `scan`
if tx.version() != 2 { if tx.version() != 2 {
@@ -197,14 +208,14 @@ impl InternalScanner {
} else { } else {
let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else { let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else {
// Invalid transaction, as of consensus rules at the time of writing this code // 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) { commitment = match proofs.base.encrypted_amounts.get(o) {
Some(amount) => output_derivations.decrypt(amount), Some(amount) => output_derivations.decrypt(amount),
// Invalid transaction, as of consensus rules at the time of writing this code // Invalid transaction, as of consensus rules at the time of writing this code
None => Err(RpcError::InvalidNode( None => Err(ScanError::InvalidScannableBlock(
"RCT proofs without an encrypted amount per output".to_string(), "RCT proofs without an encrypted amount per output",
))?, ))?,
}; };
@@ -223,7 +234,7 @@ impl InternalScanner {
index_in_transaction: o.try_into().unwrap(), index_in_transaction: o.try_into().unwrap(),
}, },
relative_id: RelativeId { 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 }, data: OutputData { key: output_key, key_offset, commitment },
metadata: Metadata { metadata: Metadata {
@@ -243,12 +254,22 @@ impl InternalScanner {
Ok(Timelocked(res)) 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 { if block.header.hardfork_version > 16 {
Err(RpcError::InternalError(format!( Err(ScanError::UnsupportedProtocol(block.header.hardfork_version))?;
"scanning a hardfork {} block, when we only support up to 16",
block.header.hardfork_version
)))?;
} }
// We obtain all TXs in full // We obtain all TXs in full
@@ -256,80 +277,17 @@ impl InternalScanner {
block.miner_transaction.hash(), block.miner_transaction.hash(),
Transaction::<Pruned>::from(block.miner_transaction.clone()), Transaction::<Pruned>::from(block.miner_transaction.clone()),
)]; )];
let txs = rpc.get_pruned_transactions(&block.transactions).await?; for (hash, tx) in block.transactions.iter().zip(transactions) {
for (hash, tx) in block.transactions.iter().zip(txs) {
txs_with_hashes.push((*hash, tx)); 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![]); let mut res = Timelocked(vec![]);
for (hash, tx) in txs_with_hashes { for (hash, tx) in txs_with_hashes {
// Push all outputs into our result // Push all outputs into our result
{ {
let mut this_txs_outputs = vec![]; let mut this_txs_outputs = vec![];
core::mem::swap( 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, &mut this_txs_outputs,
); );
res.0.extend(this_txs_outputs); res.0.extend(this_txs_outputs);
@@ -337,7 +295,7 @@ impl InternalScanner {
// Update the RingCT starting index for the next TX // Update the RingCT starting index for the next TX
if matches!(tx, Transaction::V2 { .. }) { 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. /// Scan a block.
pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> { pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
self.0.scan(rpc, block).await self.0.scan(block)
} }
} }
@@ -413,7 +371,7 @@ impl GuaranteedScanner {
} }
/// Scan a block. /// Scan a block.
pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result<Timelocked, RpcError> { pub fn scan(&mut self, block: ScannableBlock) -> Result<Timelocked, ScanError> {
self.0.scan(rpc, block).await self.0.scan(block)
} }
} }

View File

@@ -1,8 +1,12 @@
use monero_serai::transaction::Transaction; use monero_serai::transaction::Transaction;
use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{rpc::Rpc, extra::MAX_ARBITRARY_DATA_SIZE, send::SendError}; use monero_wallet::{rpc::Rpc, extra::MAX_ARBITRARY_DATA_SIZE, send::SendError};
mod runner; mod runner;
#[allow(clippy::upper_case_acronyms)]
type SRR = SimpleRequestRpc;
test!( test!(
add_single_data_less_than_max, add_single_data_less_than_max,
( (
@@ -15,9 +19,8 @@ test!(
builder.add_payment(addr, 5); builder.add_payment(addr, 5);
(builder.build().unwrap(), (arbitrary_data,)) (builder.build().unwrap(), (arbitrary_data,))
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, data: (Vec<u8>,)| async move { |_rpc: SRR, block, tx: Transaction, mut scanner: Scanner, data: (Vec<u8>,)| async move {
let output = let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
assert_eq!(output.arbitrary_data()[0], data.0); assert_eq!(output.arbitrary_data()[0], data.0);
@@ -42,9 +45,8 @@ test!(
builder.add_payment(addr, 5); builder.add_payment(addr, 5);
(builder.build().unwrap(), data) (builder.build().unwrap(), data)
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, data: Vec<Vec<u8>>| async move { |_rpc: SRR, block, tx: Transaction, mut scanner: Scanner, data: Vec<Vec<u8>>| async move {
let output = let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
assert_eq!(output.arbitrary_data(), data); assert_eq!(output.arbitrary_data(), data);
@@ -70,9 +72,8 @@ test!(
builder.add_payment(addr, 5); builder.add_payment(addr, 5);
(builder.build().unwrap(), data) (builder.build().unwrap(), data)
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, data: Vec<u8>| async move { |_rpc: SRR, block, tx: Transaction, mut scanner: Scanner, data: Vec<u8>| async move {
let output = let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
assert_eq!(output.arbitrary_data(), vec![data]); assert_eq!(output.arbitrary_data(), vec![data]);

View File

@@ -16,9 +16,8 @@ test!(
builder.add_payment(addr, 2000000000000); builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let output = let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 2000000000000); assert_eq!(output.commitment().amount, 2000000000000);
output output
@@ -94,9 +93,8 @@ test!(
builder.add_payment(addr, 2000000000000); builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let output = let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 2000000000000); assert_eq!(output.commitment().amount, 2000000000000);
output 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(); rpc.generate_blocks(&view.legacy_address(Network::Mainnet), 60).await.unwrap();
let block = rpc.get_block_by_number(start).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. /// Make sure the weight and fee match the expected calculation.
@@ -315,6 +319,7 @@ macro_rules! test {
rpc.publish_transaction(&signed).await.unwrap(); rpc.publish_transaction(&signed).await.unwrap();
let block = let block =
mine_until_unlocked(&rpc, &random_address().2, signed.hash()).await; 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(); let tx = rpc.get_transaction(signed.hash()).await.unwrap();
check_weight_and_fee(&tx, fee_rate); check_weight_and_fee(&tx, fee_rate);
let scanner = Scanner::new(view.clone()); let scanner = Scanner::new(view.clone());
@@ -336,6 +341,7 @@ macro_rules! test {
rpc.publish_transaction(&signed).await.unwrap(); rpc.publish_transaction(&signed).await.unwrap();
let block = let block =
mine_until_unlocked(&rpc, &random_address().2, signed.hash()).await; 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(); let tx = rpc.get_transaction(signed.hash()).await.unwrap();
if stringify!($name) != "spend_one_input_to_two_outputs_no_change" { 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, // 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_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{rpc::Rpc, address::SubaddressIndex, extra::PaymentId, GuaranteedScanner}; use monero_wallet::{
transaction::Transaction, rpc::Rpc, address::SubaddressIndex, extra::PaymentId, GuaranteedScanner,
};
mod runner; mod runner;
#[allow(clippy::upper_case_acronyms)]
type SRR = SimpleRequestRpc;
type Tx = Transaction;
test!( test!(
scan_standard_address, scan_standard_address,
( (
@@ -12,8 +18,8 @@ test!(
builder.add_payment(view.legacy_address(Network::Mainnet), 5); builder.add_payment(view.legacy_address(Network::Mainnet), 5);
(builder.build().unwrap(), scanner) (builder.build().unwrap(), scanner)
}, },
|rpc, block, tx: Transaction, _, mut state: Scanner| async move { |_rpc: SRR, block, tx: Transaction, _, mut state: Scanner| async move {
let output = state.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); let output = state.scan(block).unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
let dummy_payment_id = PaymentId::Encrypted([0u8; 8]); let dummy_payment_id = PaymentId::Encrypted([0u8; 8]);
@@ -35,9 +41,8 @@ test!(
builder.add_payment(view.subaddress(Network::Mainnet, subaddress), 5); builder.add_payment(view.subaddress(Network::Mainnet, subaddress), 5);
(builder.build().unwrap(), (scanner, subaddress)) (builder.build().unwrap(), (scanner, subaddress))
}, },
|rpc, block, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move { |_rpc: SRR, block, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move {
let output = let output = state.0.scan(block).unwrap().not_additionally_locked().swap_remove(0);
state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
assert_eq!(output.subaddress(), Some(state.1)); 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.add_payment(view.legacy_integrated_address(Network::Mainnet, payment_id), 5);
(builder.build().unwrap(), (scanner, payment_id)) (builder.build().unwrap(), (scanner, payment_id))
}, },
|rpc, block, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move { |_rpc: SRR, block, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move {
let output = let output = state.0.scan(block).unwrap().not_additionally_locked().swap_remove(0);
state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1))); 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.add_payment(view.address(Network::Mainnet, None, None), 5);
(builder.build().unwrap(), scanner) (builder.build().unwrap(), scanner)
}, },
|rpc, block, tx: Transaction, _, mut scanner: GuaranteedScanner| async move { |_rpc: SRR, block, tx: Transaction, _, mut scanner: GuaranteedScanner| async move {
let output = let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
assert_eq!(output.subaddress(), None); assert_eq!(output.subaddress(), None);
@@ -100,9 +103,8 @@ test!(
builder.add_payment(view.address(Network::Mainnet, Some(subaddress), None), 5); builder.add_payment(view.address(Network::Mainnet, Some(subaddress), None), 5);
(builder.build().unwrap(), (scanner, subaddress)) (builder.build().unwrap(), (scanner, subaddress))
}, },
|rpc, block, tx: Transaction, _, mut state: (GuaranteedScanner, SubaddressIndex)| async move { |_rpc: SRR, block, tx: Tx, _, mut state: (GuaranteedScanner, SubaddressIndex)| async move {
let output = let output = state.0.scan(block).unwrap().not_additionally_locked().swap_remove(0);
state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
assert_eq!(output.subaddress(), Some(state.1)); 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.add_payment(view.address(Network::Mainnet, None, Some(payment_id)), 5);
(builder.build().unwrap(), (scanner, payment_id)) (builder.build().unwrap(), (scanner, payment_id))
}, },
|rpc, block, tx: Transaction, _, mut state: (GuaranteedScanner, [u8; 8])| async move { |_rpc: SRR, block, tx: Transaction, _, mut state: (GuaranteedScanner, [u8; 8])| async move {
let output = let output = state.0.scan(block).unwrap().not_additionally_locked().swap_remove(0);
state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1))); assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1)));
@@ -132,7 +133,6 @@ test!(
), ),
); );
#[rustfmt::skip]
test!( test!(
scan_guaranteed_integrated_subaddress, scan_guaranteed_integrated_subaddress,
( (
@@ -149,14 +149,8 @@ test!(
builder.add_payment(view.address(Network::Mainnet, Some(subaddress), Some(payment_id)), 5); builder.add_payment(view.address(Network::Mainnet, Some(subaddress), Some(payment_id)), 5);
(builder.build().unwrap(), (scanner, payment_id, subaddress)) (builder.build().unwrap(), (scanner, payment_id, subaddress))
}, },
| |_rpc, block, tx: Tx, _, mut state: (GuaranteedScanner, [u8; 8], SubaddressIndex)| async move {
rpc, let output = state.0.scan(block).unwrap().not_additionally_locked().swap_remove(0);
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);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1))); 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_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{ 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, WalletOutput, OutputWithDecoys,
}; };
mod runner; mod runner;
use runner::{SignableTransactionBuilder, ring_len}; 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 // Set up inputs, select decoys, then add them to the TX builder
async fn add_inputs( async fn add_inputs(
rct_type: RctType, rct_type: RctType,
@@ -40,9 +48,8 @@ test!(
builder.add_payment(addr, 5); builder.add_payment(addr, 5);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let output = let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 5); assert_eq!(output.commitment().amount, 5);
}, },
@@ -57,8 +64,8 @@ test!(
builder.add_payment(addr, 2000000000000); builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); let mut outputs = scanner.scan(block).unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 2); assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].transaction(), tx.hash());
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.add_payment(addr, 6);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let output = let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 6); assert_eq!(output.commitment().amount, 6);
}, },
@@ -93,8 +99,8 @@ test!(
builder.add_payment(addr, 1000000000000); builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); let outputs = scanner.scan(block).unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 1000000000000); 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); .add_payment(sub_view.subaddress(Network::Mainnet, SubaddressIndex::new(0, 1).unwrap()), 1);
(builder.build().unwrap(), (change_view, sub_view)) (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 // Make sure the change can pick up its output
let mut change_scanner = Scanner::new(views.0); let mut change_scanner = Scanner::new(views.0);
assert!( assert!(change_scanner.scan(block.clone()).unwrap().not_additionally_locked().len() == 1);
change_scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().len() == 1
);
// Make sure the subaddress can pick up its output // Make sure the subaddress can pick up its output
let mut sub_scanner = Scanner::new(views.1); let mut sub_scanner = Scanner::new(views.1);
sub_scanner.register_subaddress(SubaddressIndex::new(0, 1).unwrap()); 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!(sub_outputs.len() == 1);
assert_eq!(sub_outputs[0].transaction(), tx.hash()); assert_eq!(sub_outputs[0].transaction(), tx.hash());
assert_eq!(sub_outputs[0].commitment().amount, 1); assert_eq!(sub_outputs[0].commitment().amount, 1);
@@ -165,8 +169,8 @@ test!(
builder.add_payment(addr, 2000000000000); builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); let outputs = scanner.scan(block).unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 2000000000000); assert_eq!(outputs[0].commitment().amount, 2000000000000);
@@ -179,9 +183,8 @@ test!(
builder.add_payment(addr, 2); builder.add_payment(addr, 2);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let output = let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.transaction(), tx.hash());
assert_eq!(output.commitment().amount, 2); assert_eq!(output.commitment().amount, 2);
}, },
@@ -195,8 +198,8 @@ test!(
builder.add_payment(addr, 1000000000000); builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); let outputs = scanner.scan(block).unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 1000000000000); assert_eq!(outputs[0].commitment().amount, 1000000000000);
@@ -212,8 +215,8 @@ test!(
} }
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut scanned_tx = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); let mut scanned_tx = scanner.scan(block).unwrap().not_additionally_locked();
let mut output_amounts = HashSet::new(); let mut output_amounts = HashSet::new();
for i in 0 .. 15 { for i in 0 .. 15 {
@@ -237,8 +240,8 @@ test!(
builder.add_payment(addr, 1000000000000); builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); let outputs = scanner.scan(block).unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 1000000000000); assert_eq!(outputs[0].commitment().amount, 1000000000000);
@@ -263,10 +266,14 @@ test!(
(builder.build().unwrap(), (scanner, subaddresses)) (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; 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(); let mut output_amounts_by_subaddress = HashMap::new();
for i in 0 .. 15 { for i in 0 .. 15 {
@@ -294,8 +301,8 @@ test!(
builder.add_payment(addr, 1000000000000); builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); let outputs = scanner.scan(block).unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 1000000000000); assert_eq!(outputs[0].commitment().amount, 1000000000000);
@@ -320,8 +327,8 @@ test!(
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let mut outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); let mut outputs = scanner.scan(block).unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 2); assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[1].transaction(), tx.hash()); assert_eq!(outputs[1].transaction(), tx.hash());
@@ -345,8 +352,8 @@ test!(
builder.add_payment(addr, 1000000000000); builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ()) (builder.build().unwrap(), ())
}, },
|rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { |_rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move {
let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); let outputs = scanner.scan(block).unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].transaction(), tx.hash()); assert_eq!(outputs[0].transaction(), tx.hash());
assert_eq!(outputs[0].commitment().amount, 1000000000000); assert_eq!(outputs[0].commitment().amount, 1000000000000);
@@ -381,11 +388,11 @@ test!(
builder.add_payment(view.legacy_address(Network::Mainnet), 1); builder.add_payment(view.legacy_address(Network::Mainnet), 1);
(builder.build().unwrap(), change_view) (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 // Make sure the change can pick up its output
let mut change_scanner = Scanner::new(change_view); let mut change_scanner = Scanner::new(change_view);
change_scanner.register_subaddress(SubaddressIndex::new(0, 1).unwrap()); 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.len() == 1);
assert!(outputs[0].subaddress().unwrap().account() == 0); assert!(outputs[0].subaddress().unwrap().account() == 0);
assert!(outputs[0].subaddress().unwrap().address() == 1); assert!(outputs[0].subaddress().unwrap().address() == 1);

View File

@@ -106,6 +106,7 @@ async fn from_wallet_rpc_to_self(spec: AddressSpec) {
// unlock it // unlock it
let block = runner::mine_until_unlocked(&daemon_rpc, &wallet_rpc_addr, tx_hash).await; 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 // Create the scanner
let mut scanner = Scanner::new(view_pair); 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 // Retrieve it and scan it
let output = let output = scanner.scan(block).unwrap().not_additionally_locked().swap_remove(0);
scanner.scan(&daemon_rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0);
assert_eq!(output.transaction(), tx_hash); assert_eq!(output.transaction(), tx_hash);
runner::check_weight_and_fee(&daemon_rpc.get_transaction(tx_hash).await.unwrap(), fee_rate); 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> { async fn get_outputs(&self, block: &Block, key: EdwardsPoint) -> Vec<Output> {
let outputs = loop { 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, Ok(outputs) => break outputs,
Err(e) => { Err(e) => {
log::error!("couldn't scan block {}: {e:?}", hex::encode(block.id())); 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 new_block = self.rpc.get_block_by_number(new_block).await.unwrap();
let mut outputs = let mut outputs = Self::test_scanner()
Self::test_scanner().scan(&self.rpc, &new_block).await.unwrap().ignore_additional_timelock(); .scan(self.rpc.get_scannable_block(new_block.clone()).await.unwrap())
.unwrap()
.ignore_additional_timelock();
let output = outputs.swap_remove(0); let output = outputs.swap_remove(0);
let amount = output.commitment().amount; 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 view_pair = ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE)).unwrap();
let mut scanner = Scanner::new(view_pair.clone()); let mut scanner = Scanner::new(view_pair.clone());
let output = scanner let output = scanner
.scan(&rpc, &rpc.get_block_by_number(1).await.unwrap()) .scan(rpc.get_scannable_block_by_number(1).await.unwrap())
.await
.unwrap() .unwrap()
.additional_timelock_satisfied_by(rpc.get_height().await.unwrap(), 0) .additional_timelock_satisfied_by(rpc.get_height().await.unwrap(), 0)
.swap_remove(0); .swap_remove(0);
@@ -587,7 +586,10 @@ async fn mint_and_burn_test() {
while i < (5 * 6) { while i < (5 * 6) {
if let Ok(block) = rpc.get_block_by_number(start_monero_block).await { if let Ok(block) = rpc.get_block_by_number(start_monero_block).await {
start_monero_block += 1; 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() { if !outputs.is_empty() {
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);

View File

@@ -429,8 +429,7 @@ impl Wallet {
block.transactions.contains(&last_tx.1) block.transactions.contains(&last_tx.1)
{ {
outputs = Scanner::new(view_pair.clone()) outputs = Scanner::new(view_pair.clone())
.scan(&rpc, &block) .scan(rpc.get_scannable_block(block).await.unwrap())
.await
.unwrap() .unwrap()
.ignore_additional_timelock(); .ignore_additional_timelock();
} }