diff --git a/Cargo.lock b/Cargo.lock index 9d4123c..3e96b6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3331,6 +3331,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "tests-pow" +version = "0.1.0" +dependencies = [ + "cuprate-consensus-rules", + "cuprate-cryptonight", + "function_name", + "hex", + "monero-serai", + "randomx-rs", + "rayon", + "reqwest", + "serde", + "serde_json", + "thread_local", + "tokio", +] + [[package]] name = "thiserror" version = "1.0.66" diff --git a/Cargo.toml b/Cargo.toml index 3f0257d..b942437 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ members = [ "types", # Tests + "tests/pow", "tests/monero-serai", ] diff --git a/tests/monero-serai/src/main.rs b/tests/monero-serai/src/main.rs index 370be33..e5fbc30 100644 --- a/tests/monero-serai/src/main.rs +++ b/tests/monero-serai/src/main.rs @@ -56,7 +56,7 @@ async fn main() { count = c; println!( - "blocks processed ... {c} ({:.2}%)", + "blocks processed ... {c}/{top_height} ({:.2}%)", (c as f64 / top_height as f64) * 100.0 ); diff --git a/tests/pow/Cargo.toml b/tests/pow/Cargo.toml new file mode 100644 index 0000000..4b965dd --- /dev/null +++ b/tests/pow/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tests-pow" +version = "0.1.0" +edition = "2021" + +[dependencies] +cuprate-cryptonight = { workspace = true } +cuprate-consensus-rules = { workspace = true } + +function_name = { workspace = true } +thread_local = { workspace = true } +monero-serai = { workspace = true } +hex = { workspace = true, features = ["serde", "std"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, features = ["std"] } +tokio = { workspace = true, features = ["full"] } +reqwest = { version = "0.12", features = ["json"] } +rayon = { workspace = true } +randomx-rs = { workspace = true } + +[lints] +workspace = true diff --git a/tests/pow/src/main.rs b/tests/pow/src/main.rs new file mode 100644 index 0000000..dbb9b9c --- /dev/null +++ b/tests/pow/src/main.rs @@ -0,0 +1,32 @@ +use std::{ + sync::atomic::{AtomicUsize, Ordering}, + time::{Duration, Instant}, +}; + +mod rpc; + +pub static TESTED_BLOCK_COUNT: AtomicUsize = AtomicUsize::new(0); + +#[tokio::main] +async fn main() { + let now = Instant::now(); + + let rpc_node_url = if let Ok(url) = std::env::var("RPC_NODE_URL") { + url + } else { + "http://127.0.0.1:18081/json_rpc".to_string() + }; + println!("rpc_node_url: {rpc_node_url}"); + + let rpc_client = rpc::RpcClient::new(rpc_node_url).await; + + tokio::join!( + rpc_client.cryptonight_v0(), + rpc_client.cryptonight_v1(), + rpc_client.cryptonight_v2(), + rpc_client.cryptonight_r(), + rpc_client.randomx(), + ); + + println!("finished all PoW, took: {}s", now.elapsed().as_secs()); +} diff --git a/tests/pow/src/rpc.rs b/tests/pow/src/rpc.rs new file mode 100644 index 0000000..e4a2bef --- /dev/null +++ b/tests/pow/src/rpc.rs @@ -0,0 +1,254 @@ +use std::{ + collections::BTreeMap, + ops::Range, + sync::{atomic::Ordering, Mutex}, +}; + +use function_name::named; +use hex::serde::deserialize; +use monero_serai::block::Block; +use randomx_rs::{RandomXCache, RandomXFlag, RandomXVM}; +use reqwest::{ + header::{HeaderMap, HeaderValue}, + Client, ClientBuilder, +}; +use serde::Deserialize; +use serde_json::{json, Value}; +use thread_local::ThreadLocal; + +use crate::TESTED_BLOCK_COUNT; + +#[derive(Debug, Clone, Deserialize)] +struct JsonRpcResponse { + result: GetBlockResponse, +} + +#[derive(Debug, Clone, Deserialize)] +struct GetBlockResponse { + #[serde(deserialize_with = "deserialize")] + pub blob: Vec, + pub block_header: BlockHeader, +} + +#[derive(Debug, Clone, Deserialize)] +struct BlockHeader { + #[serde(deserialize_with = "deserialize")] + pub pow_hash: Vec, + #[serde(deserialize_with = "deserialize")] + pub hash: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) struct RpcClient { + client: Client, + rpc_node_url: String, + top_height: usize, +} + +impl RpcClient { + pub(crate) async fn new(rpc_node_url: String) -> Self { + let headers = { + let mut h = HeaderMap::new(); + h.insert("Content-Type", HeaderValue::from_static("application/json")); + h + }; + + let client = ClientBuilder::new() + .default_headers(headers) + .build() + .unwrap(); + + let request = json!({ + "jsonrpc": "2.0", + "id": 0, + "method": "get_last_block_header", + "params": {} + }); + + let top_height = client + .get(&rpc_node_url) + .json(&request) + .send() + .await + .unwrap() + .json::() + .await + .unwrap() + .get("result") + .unwrap() + .get("block_header") + .unwrap() + .get("height") + .unwrap() + .as_u64() + .unwrap() + .try_into() + .unwrap(); + + println!("top_height: {top_height}"); + assert!(top_height > 3301441, "node is behind"); + + Self { + client, + rpc_node_url, + top_height, + } + } + + async fn get_block(&self, height: usize) -> GetBlockResponse { + let request = json!({ + "jsonrpc": "2.0", + "id": 0, + "method": "get_block", + "params": {"height": height, "fill_pow_hash": true} + }); + + tokio::task::spawn(self.client.get(&self.rpc_node_url).json(&request).send()) + .await + .unwrap() + .unwrap() + .json::() + .await + .unwrap() + .result + } + + async fn test( + &self, + range: Range, + hash: impl Fn(Vec, u64, u64, [u8; 32]) -> [u8; 32] + Send + Sync + 'static + Copy, + name: &'static str, + ) { + let tasks = range.map(|height| { + let task = self.get_block(height); + (height, task) + }); + + for (height, task) in tasks { + let result = task.await; + + let (seed_height, seed_hash) = if RANDOMX { + let seed_height = cuprate_consensus_rules::blocks::randomx_seed_height(height); + + let seed_hash: [u8; 32] = self + .get_block(seed_height) + .await + .block_header + .hash + .try_into() + .unwrap(); + + (seed_height, seed_hash) + } else { + (0, [0; 32]) + }; + + let top_height = self.top_height; + + #[expect(clippy::cast_precision_loss)] + rayon::spawn(move || { + let GetBlockResponse { blob, block_header } = result; + let header = block_header; + + let block = match Block::read(&mut blob.as_slice()) { + Ok(b) => b, + Err(e) => panic!("{e:?}\nblob: {blob:?}, header: {header:?}"), + }; + + let pow_hash = hash( + block.serialize_pow_hash(), + height.try_into().unwrap(), + seed_height.try_into().unwrap(), + seed_hash, + ); + + assert_eq!( + header.pow_hash, pow_hash, + "\nheight: {height}\nheader: {header:#?}\nblock: {block:#?}" + ); + + let count = TESTED_BLOCK_COUNT.fetch_add(1, Ordering::Release); + + let hex_header = hex::encode(header.pow_hash); + let hex_hash = hex::encode(pow_hash); + let percent = (count as f64 / top_height as f64) * 100.0; + + println!( + "progress | {count}/{top_height} ({percent:.2}%) +height | {height} +algo | {name} +header | {hex_header} +hash | {hex_hash}\n" + ); + }); + } + } + + #[named] + pub(crate) async fn cryptonight_v0(&self) { + self.test::( + 0..1546000, + |b, _, _, _| cuprate_cryptonight::cryptonight_hash_v0(&b), + function_name!(), + ) + .await; + } + + #[named] + pub(crate) async fn cryptonight_v1(&self) { + self.test::( + 1546000..1685555, + |b, _, _, _| cuprate_cryptonight::cryptonight_hash_v1(&b).unwrap(), + function_name!(), + ) + .await; + } + + #[named] + pub(crate) async fn cryptonight_v2(&self) { + self.test::( + 1685555..1788000, + |b, _, _, _| cuprate_cryptonight::cryptonight_hash_v2(&b), + function_name!(), + ) + .await; + } + + #[named] + pub(crate) async fn cryptonight_r(&self) { + self.test::( + 1788000..1978433, + |b, h, _, _| cuprate_cryptonight::cryptonight_hash_r(&b, h), + function_name!(), + ) + .await; + } + + #[named] + pub(crate) async fn randomx(&self) { + #[expect(clippy::significant_drop_tightening)] + let function = move |bytes: Vec, _, seed_height, seed_hash: [u8; 32]| { + static RANDOMX_VM: ThreadLocal>> = ThreadLocal::new(); + + let mut thread_local = RANDOMX_VM + .get_or(|| Mutex::new(BTreeMap::new())) + .lock() + .unwrap(); + + let randomx_vm = thread_local.entry(seed_height).or_insert_with(|| { + let flag = RandomXFlag::get_recommended_flags(); + let cache = RandomXCache::new(flag, &seed_hash).unwrap(); + RandomXVM::new(flag, Some(cache), None).unwrap() + }); + + randomx_vm + .calculate_hash(&bytes) + .unwrap() + .try_into() + .unwrap() + }; + + self.test::(1978433..self.top_height, function, function_name!()) + .await; + } +}