mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 11:39:35 +00:00
Differentiate Rpc from DecoyRpc
Enables using a locally backed decoy DB.
This commit is contained in:
parent
ed662568e2
commit
774424b70b
4 changed files with 202 additions and 148 deletions
|
@ -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);
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue