Differentiate Rpc from DecoyRpc

Enables using a locally backed decoy DB.
This commit is contained in:
Luke Parker 2024-07-08 18:14:56 -04:00
parent ed662568e2
commit 774424b70b
No known key found for this signature in database
4 changed files with 202 additions and 148 deletions

View file

@ -3,7 +3,6 @@ use monero_address::{Network, MoneroAddress};
// monero-rpc doesn't include a transport
// We can't include the simple-request crate there as then we'd have a cyclical dependency
// Accordingly, we test monero-rpc here (implicitly testing the simple-request transport)
use monero_rpc::*;
use monero_simple_request_rpc::*;
const ADDRESS: &str =
@ -11,6 +10,8 @@ const ADDRESS: &str =
#[tokio::test]
async fn test_rpc() {
use monero_rpc::Rpc;
let rpc =
SimpleRequestRpc::new("http://serai:seraidex@127.0.0.1:18081".to_string()).await.unwrap();
@ -52,17 +53,35 @@ async fn test_rpc() {
}
assert_eq!(blocks, actual_blocks);
}
}
#[tokio::test]
async fn test_decoy_rpc() {
use monero_rpc::{Rpc, DecoyRpc};
let rpc =
SimpleRequestRpc::new("http://serai:seraidex@127.0.0.1:18081".to_string()).await.unwrap();
// Test get_output_distribution
// It's documented to take two inclusive block numbers
{
let height = rpc.get_height().await.unwrap();
let distribution_len = rpc.get_output_distribution_len().await.unwrap();
assert_eq!(distribution_len, rpc.get_height().await.unwrap());
rpc.get_output_distribution(0 ..= height).await.unwrap_err();
assert_eq!(rpc.get_output_distribution(0 .. height).await.unwrap().len(), height);
rpc.get_output_distribution(0 ..= distribution_len).await.unwrap_err();
assert_eq!(
rpc.get_output_distribution(0 .. distribution_len).await.unwrap().len(),
distribution_len
);
assert_eq!(rpc.get_output_distribution(0 .. (height - 1)).await.unwrap().len(), height - 1);
assert_eq!(rpc.get_output_distribution(1 .. height).await.unwrap().len(), height - 1);
assert_eq!(
rpc.get_output_distribution(0 .. (distribution_len - 1)).await.unwrap().len(),
distribution_len - 1
);
assert_eq!(
rpc.get_output_distribution(1 .. distribution_len).await.unwrap().len(),
distribution_len - 1
);
assert_eq!(rpc.get_output_distribution(0 ..= 0).await.unwrap().len(), 1);
assert_eq!(rpc.get_output_distribution(0 ..= 1).await.unwrap().len(), 2);

View file

@ -492,6 +492,129 @@ pub trait Rpc: Sync + Clone + Debug {
Ok(res)
}
/// 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.
// TODO: Take a sanity check argument
async fn get_fee_rate(&self, priority: FeePriority) -> Result<FeeRate, RpcError> {
#[derive(Deserialize, Debug)]
struct FeeResponse {
status: String,
fees: Option<Vec<u64>>,
fee: u64,
quantization_mask: u64,
}
let res: FeeResponse = self
.json_rpc_call(
"get_fee_estimate",
Some(json!({ "grace_blocks": GRACE_BLOCKS_FOR_FEE_ESTIMATE })),
)
.await?;
if res.status != "OK" {
Err(RpcError::InvalidFee)?;
}
if let Some(fees) = res.fees {
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
// src/wallet/wallet2.cpp#L7615-L7620
let priority_idx = usize::try_from(if priority.fee_priority() >= 4 {
3
} else {
priority.fee_priority().saturating_sub(1)
})
.map_err(|_| RpcError::InvalidPriority)?;
if priority_idx >= fees.len() {
Err(RpcError::InvalidPriority)
} else {
FeeRate::new(fees[priority_idx], res.quantization_mask)
}
} else {
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
// 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 })
.map_err(|_| RpcError::InvalidPriority)?;
let multipliers = [1, 5, 25, 1000];
if priority_idx >= multipliers.len() {
// though not an RPC error, it seems sensible to treat as such
Err(RpcError::InvalidPriority)?;
}
let fee_multiplier = multipliers[priority_idx];
FeeRate::new(res.fee * fee_multiplier, res.quantization_mask)
}
}
/// Publish a transaction.
async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> {
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct SendRawResponse {
status: String,
double_spend: bool,
fee_too_low: bool,
invalid_input: bool,
invalid_output: bool,
low_mixin: bool,
not_relayed: bool,
overspend: bool,
too_big: bool,
too_few_outputs: bool,
reason: String,
}
let res: SendRawResponse = self
.rpc_call(
"send_raw_transaction",
Some(json!({ "tx_as_hex": hex::encode(tx.serialize()), "do_sanity_checks": false })),
)
.await?;
if res.status != "OK" {
Err(RpcError::InvalidTransaction(tx.hash()))?;
}
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>(
&self,
address: &Address<ADDR_BYTES>,
block_count: usize,
) -> Result<(Vec<[u8; 32]>, usize), RpcError> {
#[derive(Debug, Deserialize)]
struct BlocksResponse {
blocks: Vec<String>,
height: usize,
}
let res = self
.json_rpc_call::<BlocksResponse>(
"generateblocks",
Some(json!({
"wallet_address": address.to_string(),
"amount_of_blocks": block_count
})),
)
.await?;
let mut blocks = Vec::with_capacity(res.blocks.len());
for block in res.blocks {
blocks.push(hash_hex(&block)?);
}
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
@ -673,10 +796,57 @@ 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 decoys.
///
/// An implementation is provided for any satisfier of `Rpc`. The benefit of this trait is the
/// ability to select decoys off of a locally stored output distribution, preventing potential
/// attacks a remote node can perform.
#[async_trait]
pub trait DecoyRpc: Sync + Clone + Debug {
/// Get the length of the output distribution.
///
/// 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_len(&self) -> Result<usize, RpcError>;
/// Get the output distribution.
///
/// `range` is in terms of block numbers.
async fn get_output_distribution(
&self,
range: impl Send + RangeBounds<usize>,
) -> Result<Vec<u64>, RpcError>;
/// Get the specified outputs from the RingCT (zero-amount) pool.
async fn get_outs(&self, indexes: &[u64]) -> Result<Vec<OutputResponse>, RpcError>;
/// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their
/// timelock has been satisfied.
///
/// The timelock being satisfied is distinct from being free of the 10-block lock applied to all
/// Monero transactions.
///
/// The node is trusted for if the output is unlocked unless `fingerprintable_canonical` is set
/// to true. If `fingerprintable_canonical` is set to true, the node's local view isn't used, yet
/// the transaction's timelock is checked to be unlocked at the specified `height`. This offers a
/// canonical decoy selection, yet is fingerprintable as time-based timelocks aren't evaluated
/// (and considered locked, preventing their selection).
async fn get_unlocked_outputs(
&self,
indexes: &[u64],
height: usize,
fingerprintable_canonical: bool,
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError>;
}
#[async_trait]
impl<R: Rpc> DecoyRpc for R {
async fn get_output_distribution_len(&self) -> Result<usize, RpcError> {
<Self as Rpc>::get_height(self).await
}
async fn get_output_distribution(
&self,
range: impl Send + RangeBounds<usize>,
@ -743,7 +913,6 @@ pub trait Rpc: Sync + Clone + Debug {
Ok(distribution)
}
/// Get the specified outputs from the RingCT (zero-amount) pool.
async fn get_outs(&self, indexes: &[u64]) -> Result<Vec<OutputResponse>, RpcError> {
#[derive(Deserialize, Debug)]
struct OutsResponse {
@ -771,17 +940,6 @@ pub trait Rpc: Sync + Clone + Debug {
Ok(res.outs)
}
/// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their
/// timelock has been satisfied.
///
/// The timelock being satisfied is distinct from being free of the 10-block lock applied to all
/// Monero transactions.
///
/// The node is trusted for if the output is unlocked unless `fingerprintable_canonical` is set
/// to true. If `fingerprintable_canonical` is set to true, the node's local view isn't used, yet
/// the transaction's timelock is checked to be unlocked at the specified `height`. This offers a
/// canonical decoy selection, yet is fingerprintable as time-based timelocks aren't evaluated
/// (and considered locked, preventing their selection).
async fn get_unlocked_outputs(
&self,
indexes: &[u64],
@ -830,127 +988,4 @@ pub trait Rpc: Sync + Clone + Debug {
})
.collect()
}
/// 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.
// TODO: Take a sanity check argument
async fn get_fee_rate(&self, priority: FeePriority) -> Result<FeeRate, RpcError> {
#[derive(Deserialize, Debug)]
struct FeeResponse {
status: String,
fees: Option<Vec<u64>>,
fee: u64,
quantization_mask: u64,
}
let res: FeeResponse = self
.json_rpc_call(
"get_fee_estimate",
Some(json!({ "grace_blocks": GRACE_BLOCKS_FOR_FEE_ESTIMATE })),
)
.await?;
if res.status != "OK" {
Err(RpcError::InvalidFee)?;
}
if let Some(fees) = res.fees {
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
// src/wallet/wallet2.cpp#L7615-L7620
let priority_idx = usize::try_from(if priority.fee_priority() >= 4 {
3
} else {
priority.fee_priority().saturating_sub(1)
})
.map_err(|_| RpcError::InvalidPriority)?;
if priority_idx >= fees.len() {
Err(RpcError::InvalidPriority)
} else {
FeeRate::new(fees[priority_idx], res.quantization_mask)
}
} else {
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
// 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 })
.map_err(|_| RpcError::InvalidPriority)?;
let multipliers = [1, 5, 25, 1000];
if priority_idx >= multipliers.len() {
// though not an RPC error, it seems sensible to treat as such
Err(RpcError::InvalidPriority)?;
}
let fee_multiplier = multipliers[priority_idx];
FeeRate::new(res.fee * fee_multiplier, res.quantization_mask)
}
}
/// Publish a transaction.
async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> {
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct SendRawResponse {
status: String,
double_spend: bool,
fee_too_low: bool,
invalid_input: bool,
invalid_output: bool,
low_mixin: bool,
not_relayed: bool,
overspend: bool,
too_big: bool,
too_few_outputs: bool,
reason: String,
}
let res: SendRawResponse = self
.rpc_call(
"send_raw_transaction",
Some(json!({ "tx_as_hex": hex::encode(tx.serialize()), "do_sanity_checks": false })),
)
.await?;
if res.status != "OK" {
Err(RpcError::InvalidTransaction(tx.hash()))?;
}
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>(
&self,
address: &Address<ADDR_BYTES>,
block_count: usize,
) -> Result<(Vec<[u8; 32]>, usize), RpcError> {
#[derive(Debug, Deserialize)]
struct BlocksResponse {
blocks: Vec<String>,
height: usize,
}
let res = self
.json_rpc_call::<BlocksResponse>(
"generateblocks",
Some(json!({
"wallet_address": address.to_string(),
"amount_of_blocks": block_count
})),
)
.await?;
let mut blocks = Vec::with_capacity(res.blocks.len());
for block in res.blocks {
blocks.push(hash_hex(&block)?);
}
Ok((blocks, res.height))
}
}

View file

@ -12,7 +12,7 @@ use curve25519_dalek::{Scalar, EdwardsPoint};
use crate::{
DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME,
primitives::{Commitment, Decoys},
rpc::{RpcError, Rpc},
rpc::{RpcError, DecoyRpc},
output::OutputData,
WalletOutput,
};
@ -24,7 +24,7 @@ const TIP_APPLICATION: f64 = (DEFAULT_LOCK_WINDOW * BLOCK_TIME) as f64;
async fn select_n(
rng: &mut (impl RngCore + CryptoRng),
rpc: &impl Rpc,
rpc: &impl DecoyRpc,
height: usize,
real_output: u64,
ring_len: usize,
@ -33,7 +33,7 @@ async fn select_n(
if height < DEFAULT_LOCK_WINDOW {
Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?;
}
if height > rpc.get_height().await? {
if height > rpc.get_output_distribution_len().await? {
Err(RpcError::InternalError(
"decoys being requested from blocks this node doesn't have".to_string(),
))?;
@ -164,7 +164,7 @@ async fn select_n(
async fn select_decoys<R: RngCore + CryptoRng>(
rng: &mut R,
rpc: &impl Rpc,
rpc: &impl DecoyRpc,
ring_len: usize,
height: usize,
input: &WalletOutput,
@ -230,7 +230,7 @@ impl OutputWithDecoys {
/// Select decoys for this output.
pub async fn new(
rng: &mut (impl Send + Sync + RngCore + CryptoRng),
rpc: &impl Rpc,
rpc: &impl DecoyRpc,
ring_len: usize,
height: usize,
output: WalletOutput,
@ -249,7 +249,7 @@ impl OutputWithDecoys {
/// methodology.
pub async fn fingerprintable_deterministic_new(
rng: &mut (impl Send + Sync + RngCore + CryptoRng),
rpc: &impl Rpc,
rpc: &impl DecoyRpc,
ring_len: usize,
height: usize,
output: WalletOutput,

View file

@ -2,7 +2,7 @@ use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{
DEFAULT_LOCK_WINDOW,
transaction::Transaction,
rpc::{OutputResponse, Rpc},
rpc::{OutputResponse, Rpc, DecoyRpc},
WalletOutput,
};