Skip to content
Closed
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