test-utils: add rpc module (#110)

* test-utils: impl `rpc` module

* client: use `spawn_blocking`

* client: add tests

* ignore test for now

* add example and `get_transaction_verification_data()`

* client: calculate proper `generated_coins`

* data: fix `generated_coins/reward` references

* data: fix height
This commit is contained in:
hinto-janai 2024-04-21 12:11:23 -04:00 committed by GitHub
parent 9ad7ea3fa0
commit ee22e81c7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 354 additions and 16 deletions

2
Cargo.lock generated
View file

@ -641,6 +641,8 @@ dependencies = [
"monero-serai",
"monero-wire",
"reqwest",
"serde",
"serde_json",
"tar",
"tempfile",
"tokio",

View file

@ -3,22 +3,25 @@ name = "cuprate-test-utils"
version = "0.1.0"
edition = "2021"
license = "MIT"
authors = ["Boog900"]
authors = ["Boog900", "hinto-janai"]
[dependencies]
monero-wire = { path = "../net/monero-wire"}
monero-p2p = { path = "../p2p/monero-p2p", features = ["borsh"] }
cuprate-types = { path = "../types" }
monero-wire = { path = "../net/monero-wire" }
monero-p2p = { path = "../p2p/monero-p2p", features = ["borsh"] }
monero-serai = { workspace = true }
hex = { workspace = true }
hex-literal = { workspace = true }
monero-serai = { workspace = true, features = ["std", "http-rpc"] }
futures = { workspace = true, features = ["std"] }
async-trait = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
bytes = { workspace = true, features = ["std"] }
tempfile = { workspace = true }
hex-literal = { workspace = true }
borsh = { workspace = true, features = ["derive"]}

View file

@ -6,4 +6,4 @@ Cuprate crate, only in tests.
It currently contains:
- Code to spawn monerod instances and a testing network zone
- Real raw and typed Monero data, e.g. `Block, Transaction`
- An RPC client to generate types from `cuprate_types`
- An RPC client to generate types from `cuprate_types`

View file

@ -22,7 +22,6 @@ macro_rules! const_block_blob {
minor_version: $minor_version:literal, // Block's minor version
timestamp: $timestamp:literal, // Block's timestamp
nonce: $nonce:literal, // Block's nonce
miner_tx_generated: $miner_tx_generated:literal, // Generated Monero in block's miner transaction
tx_len: $tx_len:literal, // How many transactions there are in the block
) => {
#[doc = concat!("Block with hash `", $hash, "`.")]
@ -39,7 +38,7 @@ macro_rules! const_block_blob {
#[doc = concat!("assert_eq!(block.header.minor_version, ", $minor_version, ");")]
#[doc = concat!("assert_eq!(block.header.timestamp, ", $timestamp, ");")]
#[doc = concat!("assert_eq!(block.header.nonce, ", $nonce, ");")]
#[doc = concat!("assert!(matches!(block.miner_tx.prefix.inputs[0], Input::Gen(", $miner_tx_generated, ")));")]
#[doc = concat!("assert!(matches!(block.miner_tx.prefix.inputs[0], Input::Gen(", $height, ")));")]
#[doc = concat!("assert_eq!(block.txs.len(), ", $tx_len, ");")]
#[doc = concat!("assert_eq!(hex::encode(block.hash()), \"", $hash, "\")")]
/// ```
@ -56,7 +55,6 @@ const_block_blob! {
minor_version: 0,
timestamp: 1409804570,
nonce: 1073744198,
miner_tx_generated: 202612,
tx_len: 513,
}
@ -69,20 +67,18 @@ const_block_blob! {
minor_version: 0,
timestamp: 1409804315,
nonce: 48426,
miner_tx_generated: 202609,
tx_len: 2,
}
const_block_blob! {
name: BLOCK_F91043,
height: 2_751_506,
height: 1_731_606,
hash: "f910435a5477ca27be1986c080d5476aeab52d0c07cf3d9c72513213350d25d4",
data_path: "block/f910435a5477ca27be1986c080d5476aeab52d0c07cf3d9c72513213350d25d4.bin",
major_version: 9,
minor_version: 9,
timestamp: 1545423190,
nonce: 4123173351,
miner_tx_generated: 1731606,
tx_len: 3,
}
@ -95,7 +91,6 @@ const_block_blob! {
minor_version: 16,
timestamp: 1667941829,
nonce: 4110909056,
miner_tx_generated: 2751506,
tx_len: 0,
}

View file

@ -124,7 +124,7 @@ macro_rules! verified_block_information_fn {
tx_blobs: [$($tx_blob:ident),*], // Array of contained transaction blobs
pow_hash: $pow_hash:literal, // PoW hash as a string literal
height: $height:literal, // Block height
generated_coins: $generated_coins:literal, // Generated coins in block (`reward`)
generated_coins: $generated_coins:literal, // Generated coins in block (minus fees)
weight: $weight:literal, // Block weight
long_term_weight: $long_term_weight:literal, // Block long term weight
cumulative_difficulty: $cumulative_difficulty:literal, // Block cumulative difficulty
@ -179,7 +179,7 @@ verified_block_information_fn! {
tx_blobs: [TX_2180A8, TX_D7FEBD],
pow_hash: "84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000",
height: 202_612,
generated_coins: 13_138_270_468_431,
generated_coins: 13_138_270_467_918,
weight: 55_503,
long_term_weight: 55_503,
cumulative_difficulty: 126_654_460_829_362,
@ -192,7 +192,7 @@ verified_block_information_fn! {
tx_blobs: [TX_E2D393, TX_E57440, TX_B6B439],
pow_hash: "7c78b5b67a112a66ea69ea51477492057dba9cfeaa2942ee7372c61800000000",
height: 1_731_606,
generated_coins: 3_403_921_682_163,
generated_coins: 3_403_774_022_163,
weight: 6_597,
long_term_weight: 6_597,
cumulative_difficulty: 23_558_910_234_058_343,

View file

@ -2,4 +2,5 @@
pub mod data;
pub mod monerod;
pub mod rpc;
pub mod test_netzone;

View file

@ -0,0 +1,305 @@
//! HTTP RPC client.
//---------------------------------------------------------------------------------------------------- Use
use std::sync::Arc;
use serde::Deserialize;
use serde_json::json;
use tokio::task::spawn_blocking;
use monero_serai::{
block::Block,
rpc::{HttpRpc, Rpc},
};
use cuprate_types::{TransactionVerificationData, VerifiedBlockInformation};
use crate::rpc::constants::LOCALHOST_RPC_URL;
//---------------------------------------------------------------------------------------------------- HttpRpcClient
/// An HTTP RPC client for Monero.
pub struct HttpRpcClient {
address: String,
rpc: Rpc<HttpRpc>,
}
impl HttpRpcClient {
/// Create an [`HttpRpcClient`].
///
/// `address` should be an HTTP URL pointing to a `monerod`.
///
/// If `None` is provided the default is used: [`LOCALHOST_RPC_URL`].
///
/// Note that for [`Self::get_verified_block_information`] to work, the `monerod`
/// must be in unrestricted mode such that some fields (e.g. `pow_hash`) appear
/// in the JSON response.
///
/// # Panics
/// This panics if the `address` is invalid or a connection could not be made.
pub async fn new(address: Option<String>) -> Self {
let address = address.unwrap_or_else(|| LOCALHOST_RPC_URL.to_string());
Self {
rpc: HttpRpc::new(address.clone()).await.unwrap(),
address,
}
}
/// The address used for this [`HttpRpcClient`].
#[allow(dead_code)]
const fn address(&self) -> &String {
&self.address
}
/// Access to the inner RPC client for other usage.
#[allow(dead_code)]
const fn rpc(&self) -> &Rpc<HttpRpc> {
&self.rpc
}
/// Request data and map the response to a [`VerifiedBlockInformation`].
///
/// # Panics
/// This function will panic at any error point, e.g.,
/// if the node cannot be connected to, if deserialization fails, etc.
pub async fn get_verified_block_information(&self, height: u64) -> VerifiedBlockInformation {
#[derive(Debug, Deserialize)]
struct Result {
blob: String,
block_header: BlockHeader,
}
#[derive(Debug, Deserialize)]
struct BlockHeader {
block_weight: usize,
long_term_weight: usize,
cumulative_difficulty: u128,
hash: String,
height: u64,
pow_hash: String,
reward: u64, // generated_coins + total_tx_fees
}
let result = self
.rpc
.json_rpc_call::<Result>(
"get_block",
Some(json!(
{
"height": height,
"fill_pow_hash": true
}
)),
)
.await
.unwrap();
// Make sure this is a trusted, `pow_hash` only works there.
assert!(
!result.block_header.pow_hash.is_empty(),
"untrusted node detected, `pow_hash` will not show on these nodes - use a trusted node!"
);
let reward = result.block_header.reward;
let (block_hash, block) = spawn_blocking(|| {
let block = Block::read(&mut hex::decode(result.blob).unwrap().as_slice()).unwrap();
(block.hash(), block)
})
.await
.unwrap();
let txs: Vec<Arc<TransactionVerificationData>> = self
.get_transaction_verification_data(&block.txs)
.await
.map(Arc::new)
.collect();
let block_header = result.block_header;
let block_hash_2 = <[u8; 32]>::try_from(hex::decode(&block_header.hash).unwrap()).unwrap();
let pow_hash = <[u8; 32]>::try_from(hex::decode(&block_header.pow_hash).unwrap()).unwrap();
// Assert the block hash matches.
assert_eq!(block_hash, block_hash_2);
let total_tx_fees = txs.iter().map(|tx| tx.fee).sum::<u64>();
let generated_coins = block
.miner_tx
.prefix
.outputs
.iter()
.map(|output| output.amount.expect("miner_tx amount was None"))
.sum::<u64>()
- total_tx_fees;
assert_eq!(
reward,
generated_coins + total_tx_fees,
"generated_coins ({generated_coins}) + total_tx_fees ({total_tx_fees}) != reward ({reward})"
);
VerifiedBlockInformation {
block,
txs,
block_hash,
pow_hash,
generated_coins,
height: block_header.height,
weight: block_header.block_weight,
long_term_weight: block_header.long_term_weight,
cumulative_difficulty: block_header.cumulative_difficulty,
}
}
/// Request data and map the response to a [`TransactionVerificationData`].
///
/// # Panics
/// This function will panic at any error point, e.g.,
/// if the node cannot be connected to, if deserialization fails, etc.
pub async fn get_transaction_verification_data<'a>(
&self,
tx_hashes: &'a [[u8; 32]],
) -> impl Iterator<Item = TransactionVerificationData> + 'a {
self.rpc
.get_transactions(tx_hashes)
.await
.unwrap()
.into_iter()
.enumerate()
.map(|(i, tx)| {
let tx_hash = tx.hash();
assert_eq!(tx_hash, tx_hashes[i]);
TransactionVerificationData {
tx_blob: tx.serialize(),
tx_weight: tx.weight(),
tx_hash,
fee: tx.rct_signatures.base.fee,
tx,
}
})
}
}
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod tests {
use super::*;
use hex_literal::hex;
/// Assert the default address is localhost.
#[tokio::test]
async fn localhost() {
assert_eq!(HttpRpcClient::new(None).await.address(), LOCALHOST_RPC_URL);
}
/// Assert blocks are correctly received/calculated.
#[ignore] // FIXME: doesn't work in CI, we need a real unrestricted node
#[tokio::test]
async fn get() {
#[allow(clippy::too_many_arguments)]
async fn assert_eq(
rpc: &HttpRpcClient,
height: u64,
block_hash: [u8; 32],
pow_hash: [u8; 32],
generated_coins: u64,
weight: usize,
long_term_weight: usize,
cumulative_difficulty: u128,
tx_count: usize,
) {
let block = rpc.get_verified_block_information(height).await;
println!("block height: {height}");
assert_eq!(block.txs.len(), tx_count);
println!("{block:#?}");
assert_eq!(block.block_hash, block_hash);
assert_eq!(block.pow_hash, pow_hash);
assert_eq!(block.height, height);
assert_eq!(block.generated_coins, generated_coins);
assert_eq!(block.weight, weight);
assert_eq!(block.long_term_weight, long_term_weight);
assert_eq!(block.cumulative_difficulty, cumulative_difficulty);
}
let rpc = HttpRpcClient::new(None).await;
assert_eq(
&rpc,
0, // height
hex!("418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3"), // block_hash
hex!("8a7b1a780e99eec31a9425b7d89c283421b2042a337d5700dfd4a7d6eb7bd774"), // pow_hash
17592186044415, // generated_coins
80, // weight
80, // long_term_weight
1, // cumulative_difficulty
0, // tx_count (miner_tx excluded)
)
.await;
assert_eq(
&rpc,
1,
hex!("771fbcd656ec1464d3a02ead5e18644030007a0fc664c0a964d30922821a8148"),
hex!("5aeebb3de73859d92f3f82fdb97286d81264ecb72a42e4b9f1e6d62eb682d7c0"),
17592169267200,
383,
383,
2,
0,
)
.await;
assert_eq(
&rpc,
202612,
hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698"),
hex!("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000"),
13138270467918,
55503,
55503,
126654460829362,
513,
)
.await;
assert_eq(
&rpc,
1731606,
hex!("f910435a5477ca27be1986c080d5476aeab52d0c07cf3d9c72513213350d25d4"),
hex!("7c78b5b67a112a66ea69ea51477492057dba9cfeaa2942ee7372c61800000000"),
3403774022163,
6597,
6597,
23558910234058343,
3,
)
.await;
assert_eq(
&rpc,
2751506,
hex!("43bd1f2b6556dcafa413d8372974af59e4e8f37dbf74dc6b2a9b7212d0577428"),
hex!("10b473b5d097d6bfa0656616951840724dfe38c6fb9c4adf8158800300000000"),
600000000000,
106,
176470,
236046001376524168,
0,
)
.await;
assert_eq(
&rpc,
3132285,
hex!("a999c6ba4d2993541ba9d81561bb8293baa83b122f8aa9ab65b3c463224397d8"),
hex!("4eaa3b3d4dc888644bc14dc4895ca0b008586e30b186fbaa009d330100000000"),
600000000000,
133498,
176470,
348189741564698577,
57,
)
.await;
}
}

View file

@ -0,0 +1,7 @@
//! RPC-related Constants.
//---------------------------------------------------------------------------------------------------- Use
//---------------------------------------------------------------------------------------------------- Constants
/// The default URL used for Monero RPC connections.
pub const LOCALHOST_RPC_URL: &str = "http://127.0.0.1:18081";

25
test-utils/src/rpc/mod.rs Normal file
View file

@ -0,0 +1,25 @@
//! Monero RPC client.
//!
//! This module is a client for Monero RPC that maps the types
//! into the native types used by Cuprate found in `cuprate_types`.
//!
//! # Usage
//! ```rust,ignore
//! #[tokio::main]
//! async fn main() {
//! // Create RPC client.
//! let rpc = HttpRpcClient::new(None).await;
//!
//! // Collect 20 blocks.
//! let mut vec: Vec<VerifiedBlockInformation> = vec![];
//! for height in (3130269 - 20)..3130269 {
//! vec.push(rpc.get_verified_block_information(height).await);
//! }
//! }
//! ```
mod client;
pub use client::HttpRpcClient;
mod constants;
pub use constants::LOCALHOST_RPC_URL;