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"
version = "0.1.0"
dependencies = [
"async-trait",
"curve25519-dalek",
"hex",
"monero-address",
@@ -5013,7 +5012,6 @@ dependencies = [
name = "monero-simple-request-rpc"
version = "0.1.0"
dependencies = [
"async-trait",
"digest_auth",
"hex",
"monero-address",

View File

@@ -18,7 +18,6 @@ workspace = true
[dependencies]
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 }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }

View File

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

View File

@@ -2,10 +2,9 @@
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use core::future::Future;
use std::{sync::Arc, io::Read, time::Duration};
use async_trait::async_trait;
use tokio::sync::Mutex;
use digest_auth::{WwwAuthenticateHeader, AuthContext};
@@ -280,11 +279,16 @@ impl SimpleRequestRpc {
}
}
#[async_trait]
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))
.await
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
}
}
}

View File

@@ -4,11 +4,12 @@
#![cfg_attr(not(feature = "std"), no_std)]
use core::{
future::Future,
fmt::Debug,
ops::{Bound, RangeBounds},
};
use std_shims::{
alloc::{boxed::Box, format},
alloc::format,
vec,
vec::Vec,
io,
@@ -17,8 +18,6 @@ use std_shims::{
use zeroize::Zeroize;
use async_trait::async_trait;
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
use serde::{Serialize, Deserialize, de::DeserializeOwned};
@@ -74,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.
@@ -237,22 +249,26 @@ fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
/// While no implementors are directly provided, [monero-simple-request-rpc](
/// https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc/simple-request
/// ) is recommended.
#[async_trait]
pub trait Rpc: Sync + Clone + Debug {
/// Perform a POST request to the specified route with the specified body.
///
/// 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.
///
/// This is NOT a JSON-RPC call. They use a route of "json_rpc" and are available via
/// `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,
route: &str,
params: Option<Params>,
) -> Result<Response, RpcError> {
) -> impl Send + Future<Output = Result<Response, RpcError>> {
async move {
let res = self
.post(
route,
@@ -268,29 +284,37 @@ pub trait Rpc: Sync + Clone + Debug {
serde_json::from_str(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.
async fn json_rpc_call<Response: DeserializeOwned + Debug>(
fn json_rpc_call<Response: DeserializeOwned + Debug>(
&self,
method: &str,
params: Option<Value>,
) -> Result<Response, RpcError> {
) -> impl Send + Future<Output = Result<Response, RpcError>> {
async move {
let mut req = json!({ "method": method });
if let Some(params) = params {
req.as_object_mut().unwrap().insert("params".into(), params);
}
Ok(self.rpc_call::<_, JsonRpcResponse<Response>>("json_rpc", Some(req)).await?.result)
}
}
/// 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> {
self.post(route, params).await
fn bin_call(
&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.
///
/// 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)]
struct HeaderResponse {
major_version: u8,
@@ -309,12 +333,14 @@ pub trait Rpc: Sync + Clone + Debug {
.major_version,
)
}
}
/// Get the height of the Monero blockchain.
///
/// 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.
async fn get_height(&self) -> Result<usize, RpcError> {
fn get_height(&self) -> impl Send + Future<Output = Result<usize, RpcError>> {
async move {
#[derive(Debug, Deserialize)]
struct HeightResponse {
height: usize,
@@ -325,12 +351,17 @@ pub trait Rpc: Sync + Clone + Debug {
}
Ok(res)
}
}
/// Get the specified transactions.
///
/// The received transactions will be hashed in order to verify the correct transactions were
/// 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() {
return Ok(vec![]);
}
@@ -396,12 +427,14 @@ pub trait Rpc: Sync + Clone + Debug {
})
.collect()
}
}
/// Get the specified transactions in their pruned format.
async fn get_pruned_transactions(
fn get_pruned_transactions(
&self,
hashes: &[[u8; 32]],
) -> Result<Vec<Transaction<Pruned>>, RpcError> {
) -> impl Send + Future<Output = Result<Vec<Transaction<Pruned>>, RpcError>> {
async move {
if hashes.is_empty() {
return Ok(vec![]);
}
@@ -447,25 +480,36 @@ pub trait Rpc: Sync + Clone + Debug {
})
.collect()
}
}
/// Get the specified transaction.
///
/// The received transaction will be hashed in order to verify the correct transaction was
/// returned.
async fn get_transaction(&self, tx: [u8; 32]) -> Result<Transaction, RpcError> {
self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0))
fn get_transaction(
&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.
async fn get_pruned_transaction(&self, tx: [u8; 32]) -> Result<Transaction<Pruned>, RpcError> {
self.get_pruned_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0))
fn get_pruned_transaction(
&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.
///
/// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis 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)]
struct BlockHeaderResponse {
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?;
hash_hex(&header.block_header.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.
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)]
struct BlockResponse {
blob: String,
@@ -499,12 +545,17 @@ pub trait Rpc: Sync + Clone + Debug {
}
Ok(block)
}
}
/// 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,
/// `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)]
struct BlockResponse {
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.
///
/// This may be manipulated to unsafe levels and MUST be sanity checked.
///
/// 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)]
struct FeeResponse {
status: String,
@@ -576,8 +721,11 @@ pub trait Rpc: Sync + Clone + Debug {
// src/wallet/wallet2.cpp#L7569-L7584
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
// src/wallet/wallet2.cpp#L7660-L7661
let priority_idx =
usize::try_from(if priority.fee_priority() == 0 { 1 } else { priority.fee_priority() - 1 })
let priority_idx = usize::try_from(if priority.fee_priority() == 0 {
1
} else {
priority.fee_priority() - 1
})
.map_err(|_| RpcError::InvalidPriority)?;
let multipliers = [1, 5, 25, 1000];
if priority_idx >= multipliers.len() {
@@ -589,9 +737,14 @@ pub trait Rpc: Sync + Clone + Debug {
FeeRate::new(res.fee * fee_multiplier, res.quantization_mask)
}
}
}
/// 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)]
#[derive(Debug, Deserialize)]
struct SendRawResponse {
@@ -621,15 +774,17 @@ pub trait Rpc: Sync + Clone + Debug {
Ok(())
}
}
/// Generate blocks, with the specified address receiving the block reward.
///
/// 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,
address: &Address<ADDR_BYTES>,
block_count: usize,
) -> Result<(Vec<[u8; 32]>, usize), RpcError> {
) -> impl Send + Future<Output = Result<(Vec<[u8; 32]>, usize), RpcError>> {
async move {
#[derive(Debug, Deserialize)]
struct BlocksResponse {
blocks: Vec<String>,
@@ -652,11 +807,16 @@ pub trait Rpc: Sync + Clone + Debug {
}
Ok((blocks, res.height))
}
}
/// Get the output indexes of the specified transaction.
async fn get_o_indexes(&self, hash: [u8; 32]) -> Result<Vec<u64>, RpcError> {
// Given the immaturity of Rust epee libraries, this is a homegrown one which is only validated
// to work against this specific function
fn get_o_indexes(
&self,
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
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
// 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)
_ => 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) {
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:?}")))
}
}
}
/// 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`
/// 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.
#[async_trait]
pub trait DecoyRpc: Sync + Clone + Debug {
/// 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
/// 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.
///
/// `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.
async fn get_output_distribution(
fn get_output_distribution(
&self,
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.
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
/// 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`.
/// This offers a deterministic decoy selection, yet is fingerprintable as time-based timelocks
/// aren't evaluated (and considered locked, preventing their selection).
async fn get_unlocked_outputs(
fn get_unlocked_outputs(
&self,
indexes: &[u64],
height: usize,
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 {
async fn get_output_distribution_end_height(&self) -> Result<usize, RpcError> {
<Self as Rpc>::get_height(self).await
fn get_output_distribution_end_height(
&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,
range: impl Send + RangeBounds<usize>,
) -> Result<Vec<u64>, RpcError> {
) -> impl Send + Future<Output = Result<Vec<u64>, RpcError>> {
async move {
#[derive(Default, Debug, Deserialize)]
struct Distribution {
distribution: Vec<u64>,
@@ -904,9 +1073,9 @@ impl<R: Rpc> DecoyRpc for R {
let from = match range.start_bound() {
Bound::Included(from) => *from,
Bound::Excluded(from) => from
.checked_add(1)
.ok_or_else(|| RpcError::InternalError("range's from wasn't representable".to_string()))?,
Bound::Excluded(from) => from.checked_add(1).ok_or_else(|| {
RpcError::InternalError("range's from wasn't representable".to_string())
})?,
Bound::Unbounded => 0,
};
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]);
// 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
// Unfortunately, we can't validate without a binary search to find the RingCT activation block
// and an iterative search from there, so we solely sanity check it
// Unfortunately, we can't validate without a binary search to find the RingCT activation
// block and an iterative search from there, so we solely sanity check it
if start_height < from {
Err(RpcError::InvalidNode(format!(
"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
// 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 {
distribution.pop();
}
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)]
struct OutputResponse {
height: usize,
@@ -1040,13 +1215,15 @@ impl<R: Rpc> DecoyRpc for R {
Ok(res)
}
}
async fn get_unlocked_outputs(
fn get_unlocked_outputs(
&self,
indexes: &[u64],
height: usize,
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?;
// Only need to fetch txs to do deterministic check on timelock
@@ -1061,8 +1238,8 @@ impl<R: Rpc> DecoyRpc for R {
.iter()
.enumerate()
.map(|(i, out)| {
// Allow keys to be invalid, though if they are, return None to trigger selection of a new
// decoy
// Allow keys to be invalid, though if they are, return None to trigger selection of a
// new decoy
// Only valid keys can be used in CLSAG proofs, hence the need for re-selection, yet
// invalid keys may honestly exist on the blockchain
let Some(key) = out.key.decompress() else {
@@ -1071,11 +1248,13 @@ impl<R: Rpc> DecoyRpc for R {
Ok(Some([key, out.commitment]).filter(|_| {
if fingerprintable_deterministic {
// 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;
// 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) &&
(Timelock::Block(height - 1 + ACCEPTED_TIMELOCK_DELTA) >=
txs[i].prefix().additional_timelock)
@@ -1086,4 +1265,5 @@ impl<R: Rpc> DecoyRpc for R {
})
.collect()
}
}
}

View File

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

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