Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/tests #357

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
303 changes: 303 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,13 @@ members = [
"pruning",
"test-utils",
"types",

# Tests
"tests/compat",
]

[profile.release]
panic = "abort"
panic = "abort"
lto = true # Build with LTO
strip = "none" # Keep panic stack traces
codegen-units = 1 # Optimize for binary speed over compile times
Expand Down Expand Up @@ -159,6 +162,7 @@ pretty_assertions = { version = "1" }
proptest = { version = "1" }
proptest-derive = { version = "0.5" }
tokio-test = { version = "0.4" }
reqwest = { version = "0.12" }

## TODO:
## Potential dependencies.
Expand Down
6 changes: 6 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
# <https://rust-lang.github.io/rust-clippy/master/index.html#upper_case_acronyms>
upper-case-acronyms-aggressive = true

# <https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown>
doc-valid-idents = [
"RandomX", ".."
]
31 changes: 31 additions & 0 deletions tests/compat/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "cuprate-tests-compat"
version = "0.0.0"
edition = "2021"
description = "Compatibility tests between `cuprated` and `monerod`"
license = "MIT"
authors = ["hinto-janai"]
repository = "https://github.com/Cuprate/cuprate/tree/main/tests/compat"
keywords = ["cuprate", "tests", "compat"]


[dependencies]
cuprate-constants = { workspace = true, features = ["build",] }
cuprate-consensus-rules = { workspace = true }
cuprate-cryptonight = { workspace = true }

clap = { workspace = true, features = ["cargo", "derive", "default", "string"] }
crossbeam = { workspace = true, features = ["std"] }
futures = { workspace = true, features = ["std"] }
monero-serai = { workspace = true }
rayon = { workspace = true }
hex = { workspace = true, features = ["serde", "std"] }
hex-literal = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["std"] }
tokio = { workspace = true, features = ["full"] }
reqwest = { workspace = true, features = ["json"] }
randomx-rs = { workspace = true }

[lints]
workspace = true
50 changes: 50 additions & 0 deletions tests/compat/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use std::num::{NonZeroU64, NonZeroUsize};

use clap::Parser;

/// `cuprate` <-> `monerod` compatibility tester.
#[derive(Parser, Debug)]
#[command(
about,
long_about = None,
long_version = format!(
"{} {}",
clap::crate_version!(),
cuprate_constants::build::COMMIT
),
)]
pub struct Args {
/// Base URL to use for `monerod` RPC.
///
/// This must be a non-restricted RPC.
#[arg(long, default_value_t = String::from("http://127.0.0.1:18081"))]
pub rpc_url: String,

/// Amount of async RPC tasks to spawn.
#[arg(long, default_value_t = NonZeroUsize::new(4).unwrap())]
pub rpc_tasks: NonZeroUsize,

/// The maximum capacity of the block buffer in-between the RPC and verifier.
///
/// `0` will cause the buffer to be unbounded.
#[arg(long, default_value_t = 1000)]
pub buffer_limit: usize,

/// Amount of verifying threads to spawn.
#[arg(long, default_value_t = std::thread::available_parallelism().unwrap())]
pub threads: NonZeroUsize,

/// Print an update every `update` amount of blocks.
#[arg(long, default_value_t = NonZeroU64::new(500).unwrap())]
pub update: NonZeroU64,
}

impl Args {
pub fn get() -> Self {
let this = Self::parse();

println!("{this:#?}");

this
}
}
10 changes: 10 additions & 0 deletions tests/compat/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use std::sync::atomic::{AtomicU64, AtomicUsize};

/// Height at which RandomX activated.
pub const RANDOMX_START_HEIGHT: u64 = 1978433;

/// Total amount of blocks tested, used as a global counter.
pub static TESTED_BLOCK_COUNT: AtomicU64 = AtomicU64::new(0);

/// Total amount of transactions tested, used as a global counter.
pub static TESTED_TX_COUNT: AtomicUsize = AtomicUsize::new(0);
62 changes: 62 additions & 0 deletions tests/compat/src/cryptonight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::fmt::Display;

use hex_literal::hex;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CryptoNightHash {
V0,
V1,
V2,
R,
}

impl CryptoNightHash {
/// The last height this hash function is used for proof-of-work.
pub const fn from_height(height: u64) -> Self {
if height < 1546000 {
Self::V0
} else if height < 1685555 {
Self::V1
} else if height < 1788000 {
Self::V2
} else if height < 1978433 {
Self::R
} else {
panic!("height is large than 1978433");
}
}

pub fn hash(data: &[u8], height: u64) -> (&'static str, [u8; 32]) {
let this = Self::from_height(height);

let hash = match Self::from_height(height) {
Self::V0 => {
if height == 202612 {
hex!("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000")
} else {
cuprate_cryptonight::cryptonight_hash_v0(data)
}
}
Self::V1 => cuprate_cryptonight::cryptonight_hash_v1(data).unwrap(),
Self::V2 => cuprate_cryptonight::cryptonight_hash_v2(data),
Self::R => cuprate_cryptonight::cryptonight_hash_r(data, height),
};

(this.as_str(), hash)
}

pub const fn as_str(self) -> &'static str {
match self {
Self::V0 => "cryptonight_v0",
Self::V1 => "cryptonight_v1",
Self::V2 => "cryptonight_v2",
Self::R => "cryptonight_r",
}
}
}

impl Display for CryptoNightHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str((*self).as_str())
}
}
100 changes: 100 additions & 0 deletions tests/compat/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#![allow(unreachable_pub, reason = "This is a binary, everything `pub` is ok")]

mod cli;
mod constants;
mod cryptonight;
mod randomx;
mod rpc;
mod types;
mod verify;

use std::{
sync::atomic::Ordering,
time::{Duration, Instant},
};

#[tokio::main]
async fn main() {
let now = Instant::now();

// Parse CLI args.
let cli::Args {
rpc_url,
update,
rpc_tasks,
buffer_limit,
threads,
} = cli::Args::get();

// Set-up RPC client.
let client = rpc::RpcClient::new(rpc_url, rpc_tasks).await;
let top_height = client.top_height;
println!("top_height: {top_height}");
println!();

// Test.
let (tx, rx) = if buffer_limit == 0 {
crossbeam::channel::unbounded()
} else {
crossbeam::channel::bounded(buffer_limit)
};
verify::spawn_verify_pool(threads, update, top_height, rx);
client.test(top_height, tx).await;

// Wait for other threads to finish.
loop {
let count = constants::TESTED_BLOCK_COUNT.load(Ordering::Acquire);

if top_height == count {
println!("Finished, took {}s", now.elapsed().as_secs());
std::process::exit(0);
}

std::thread::sleep(Duration::from_secs(1));
}
}

// some draft code for `monerod` <-> `cuprated` RPC compat testing

// /// represents a `monerod/cuprated` RPC request type.
// trait RpcRequest {
// /// the expected response type, potentially only being a subset of the fields.
// type SubsetOfResponse: PartialEq;

// /// create a 'base' request.
// fn base() -> Self;

// /// permutate the base request into all (or practically) possible requests.
// // e.g. `{"height":0}`, `{"height":1}`, etc
// fn all_possible_inputs_for_rpc_request(self) -> Vec<Self>;

// /// send the request, get the response.
// ///
// /// `monerod` and `cuprated` are both expected to be fully synced.
// fn get(self, node: Node) -> Self::SubsetOfResponse;
// }

// enum Node {
// Monerod,
// Cuprated,
// }

// // all RPC requests.
// let all_rpc_requests: Vec<dyn RpcRequest> = todo!();

// // for each request...
// for base in all_rpc_requests {
// // create all possible inputs...
// let requests = all_possible_inputs_for_rpc_request(base);

// // for each input permutation...
// for r in requests {
// // assert (a potential subset of) `monerod` and `cuprated`'s response fields match in value.
// let monerod_response = r.get(Node::Monerod);
// let cuprated_response = r.get(Node::Cuprated);
// assert_eq!(
// monerod_response.subset_of_response(),
// cuprated_response.subset_of_response(),
// );
// }
// }
28 changes: 28 additions & 0 deletions tests/compat/src/randomx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use randomx_rs::{RandomXCache, RandomXDataset, RandomXFlag, RandomXVM};

/// Returns a [`RandomXVM`] with no optimization flags (default, light-verification).
pub fn randomx_vm_default(seed_hash: &[u8; 32]) -> RandomXVM {
const FLAG: RandomXFlag = RandomXFlag::FLAG_DEFAULT;

let cache = RandomXCache::new(FLAG, seed_hash).unwrap();
RandomXVM::new(FLAG, Some(cache), None).unwrap()
}

/// Returns a [`RandomXVM`] with most optimization flags.
#[expect(dead_code)]
pub fn randomx_vm_optimized(seed_hash: &[u8; 32]) -> RandomXVM {
// TODO: conditional FLAG_LARGE_PAGES, FLAG_JIT

let vm_flag = RandomXFlag::get_recommended_flags() | RandomXFlag::FLAG_FULL_MEM;
let cache_flag = RandomXFlag::get_recommended_flags();

let hash = hex::encode(seed_hash);

println!("Generating RandomX VM: seed_hash: {hash}, flags: {vm_flag:#?}");
let cache = RandomXCache::new(cache_flag, seed_hash).unwrap();
let dataset = RandomXDataset::new(RandomXFlag::FLAG_DEFAULT, cache, 0).unwrap();
let vm = RandomXVM::new(vm_flag, None, Some(dataset)).unwrap();
println!("Generating RandomX VM: seed_hash: {hash}, flags: {vm_flag:#?} ... OK");

vm
}
Loading
Loading