mirror of
https://github.com/hinto-janai/cuprate.git
synced 2024-12-22 19:49:33 +00:00
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:
parent
9ad7ea3fa0
commit
ee22e81c7e
9 changed files with 354 additions and 16 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -641,6 +641,8 @@ dependencies = [
|
|||
"monero-serai",
|
||||
"monero-wire",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
|
|
|
@ -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"]}
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
|
||||
pub mod data;
|
||||
pub mod monerod;
|
||||
pub mod rpc;
|
||||
pub mod test_netzone;
|
||||
|
|
305
test-utils/src/rpc/client.rs
Normal file
305
test-utils/src/rpc/client.rs
Normal 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;
|
||||
}
|
||||
}
|
7
test-utils/src/rpc/constants.rs
Normal file
7
test-utils/src/rpc/constants.rs
Normal 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
25
test-utils/src/rpc/mod.rs
Normal 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;
|
Loading…
Reference in a new issue