mirror of
https://github.com/hinto-janai/cuprate.git
synced 2025-01-03 17:40:01 +00:00
Compare commits
No commits in common. "e6c96a69fc7ad35e1ee02dae62172d26ffa8ecec" and "63378a0e961924ab09e3c432d0c77cc69c4aa14f" have entirely different histories.
e6c96a69fc
...
63378a0e96
18 changed files with 802 additions and 912 deletions
123
Cargo.lock
generated
123
Cargo.lock
generated
|
@ -59,55 +59,12 @@ version = "0.1.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.92"
|
||||
|
@ -487,10 +444,8 @@ version = "4.5.20"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"terminal_size",
|
||||
]
|
||||
|
||||
|
@ -512,12 +467,6 @@ version = "0.7.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "const_format"
|
||||
version = "0.2.33"
|
||||
|
@ -1150,27 +1099,6 @@ dependencies = [
|
|||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cuprate-tests-compat"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"crossbeam",
|
||||
"cuprate-consensus-rules",
|
||||
"cuprate-constants",
|
||||
"cuprate-cryptonight",
|
||||
"futures",
|
||||
"hex",
|
||||
"hex-literal",
|
||||
"monero-serai",
|
||||
"randomx-rs",
|
||||
"rayon",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cuprate-txpool"
|
||||
version = "0.0.0"
|
||||
|
@ -1970,12 +1898,6 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
|
@ -3270,12 +3192,6 @@ dependencies = [
|
|||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
|
@ -3401,6 +3317,39 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tests-monero-serai"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"hex",
|
||||
"monero-serai",
|
||||
"rayon",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tests-pow"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"cuprate-consensus-rules",
|
||||
"cuprate-cryptonight",
|
||||
"function_name",
|
||||
"hex",
|
||||
"hex-literal",
|
||||
"monero-serai",
|
||||
"randomx-rs",
|
||||
"rayon",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thread_local",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.66"
|
||||
|
@ -3782,12 +3731,6 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
|
|
@ -54,7 +54,8 @@ members = [
|
|||
"types",
|
||||
|
||||
# Tests
|
||||
"tests/compat",
|
||||
"tests/pow",
|
||||
"tests/monero-serai",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
|
|
@ -1,7 +1 @@
|
|||
# <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", ".."
|
||||
]
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
[package]
|
||||
name = "cuprate-tests-compat"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
description = "Compatability 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
|
|
@ -1,38 +0,0 @@
|
|||
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 {
|
||||
/// Name of the person to greet
|
||||
#[arg(short, long, default_value_t = String::from("http://127.0.0.1:18081"))]
|
||||
pub rpc_url: String,
|
||||
|
||||
/// Amount of verifying threads to spawn.
|
||||
#[arg(short, long, default_value_t = std::thread::available_parallelism().unwrap())]
|
||||
pub threads: NonZeroUsize,
|
||||
|
||||
/// Print an update every `update` amount of blocks.
|
||||
#[arg(short, long, default_value_t = NonZeroU64::new(500).unwrap())]
|
||||
pub update: NonZeroU64,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn get() -> Self {
|
||||
let this = Self::parse();
|
||||
|
||||
println!("{this:#?}");
|
||||
|
||||
this
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
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);
|
|
@ -1,62 +0,0 @@
|
|||
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())
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
#![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,
|
||||
threads,
|
||||
} = cli::Args::get();
|
||||
|
||||
// Set-up RPC client.
|
||||
let client = rpc::RpcClient::new(rpc_url).await;
|
||||
let top_height = client.top_height;
|
||||
println!("top_height: {top_height}");
|
||||
println!();
|
||||
|
||||
// Test.
|
||||
let (tx, rx) = crossbeam::channel::unbounded();
|
||||
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));
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,205 +0,0 @@
|
|||
use crossbeam::channel::Sender;
|
||||
use monero_serai::{block::Block, transaction::Transaction};
|
||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderValue},
|
||||
Client, ClientBuilder,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::{
|
||||
constants::RANDOMX_START_HEIGHT,
|
||||
types::{GetBlockResponse, JsonRpcResponse, RpcBlockData, RpcTxData},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RpcClient {
|
||||
client: Client,
|
||||
json_rpc_url: String,
|
||||
get_transactions_url: String,
|
||||
pub top_height: u64,
|
||||
}
|
||||
|
||||
impl RpcClient {
|
||||
pub async fn new(rpc_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 json_rpc_url = format!("{rpc_url}/json_rpc");
|
||||
let get_transactions_url = format!("{rpc_url}/get_transactions");
|
||||
|
||||
let top_height = client
|
||||
.get(&json_rpc_url)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap()
|
||||
.get("result")
|
||||
.unwrap()
|
||||
.get("block_header")
|
||||
.unwrap()
|
||||
.get("height")
|
||||
.unwrap()
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
|
||||
assert!(top_height > 3301441, "node is behind");
|
||||
|
||||
Self {
|
||||
client,
|
||||
json_rpc_url,
|
||||
get_transactions_url,
|
||||
top_height,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_block(&self, height: u64) -> GetBlockResponse {
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"method": "get_block",
|
||||
"params": {"height": height, "fill_pow_hash": true}
|
||||
});
|
||||
|
||||
self.client
|
||||
.get(&self.json_rpc_url)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<JsonRpcResponse>()
|
||||
.await
|
||||
.unwrap()
|
||||
.result
|
||||
}
|
||||
|
||||
async fn get_transactions(&self, tx_hashes: Vec<[u8; 32]>) -> Vec<RpcTxData> {
|
||||
assert!(!tx_hashes.is_empty());
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct GetTransactionsResponse {
|
||||
txs: Vec<Tx>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct Tx {
|
||||
as_hex: String,
|
||||
pruned_as_hex: String,
|
||||
}
|
||||
|
||||
let txs_hashes = tx_hashes.iter().map(hex::encode).collect::<Vec<String>>();
|
||||
let request = json!({"txs_hashes":txs_hashes});
|
||||
|
||||
let txs = self
|
||||
.client
|
||||
.get(&self.get_transactions_url)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<GetTransactionsResponse>()
|
||||
.await
|
||||
.unwrap()
|
||||
.txs;
|
||||
|
||||
assert_eq!(txs.len(), tx_hashes.len());
|
||||
|
||||
txs.into_par_iter()
|
||||
.zip(tx_hashes)
|
||||
.map(|(r, tx_hash)| {
|
||||
let tx_blob = hex::decode(if r.as_hex.is_empty() {
|
||||
r.pruned_as_hex
|
||||
} else {
|
||||
r.as_hex
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let tx = Transaction::read(&mut tx_blob.as_slice()).unwrap();
|
||||
|
||||
RpcTxData {
|
||||
tx,
|
||||
tx_blob,
|
||||
tx_hash,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn test(self, top_height: u64, tx: Sender<RpcBlockData>) {
|
||||
use futures::StreamExt;
|
||||
|
||||
let iter = (0..top_height).map(|height| {
|
||||
let this = self.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
async move {
|
||||
let get_block_response = this.get_block(height).await;
|
||||
|
||||
let (this, get_block_response, block, txs) =
|
||||
tokio::task::spawn_blocking(move || async move {
|
||||
// Deserialize the block.
|
||||
let block = Block::read(&mut get_block_response.blob.as_slice()).unwrap();
|
||||
|
||||
// Fetch and deserialize all transactions.
|
||||
let mut tx_hashes = Vec::with_capacity(block.transactions.len() + 1);
|
||||
tx_hashes.push(block.miner_transaction.hash());
|
||||
tx_hashes.extend(block.transactions.iter());
|
||||
let txs = this.get_transactions(tx_hashes).await;
|
||||
|
||||
(this, get_block_response, block, txs)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.await;
|
||||
|
||||
let (seed_height, seed_hash) = if height < RANDOMX_START_HEIGHT {
|
||||
(0, [0; 32])
|
||||
} else {
|
||||
let seed_height = cuprate_consensus_rules::blocks::randomx_seed_height(
|
||||
height.try_into().unwrap(),
|
||||
)
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
let seed_hash = this.get_block(seed_height).await.block_header.hash;
|
||||
|
||||
(seed_height, seed_hash)
|
||||
};
|
||||
|
||||
let data = RpcBlockData {
|
||||
get_block_response,
|
||||
block,
|
||||
seed_height,
|
||||
seed_hash,
|
||||
txs,
|
||||
};
|
||||
|
||||
tx.send(data).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
futures::stream::iter(iter)
|
||||
.buffer_unordered(4) // This can't be too high or else we get bottlenecked by `monerod`
|
||||
.for_each(|()| async {})
|
||||
.await;
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
use hex::serde::deserialize;
|
||||
use monero_serai::{block::Block, transaction::Transaction};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Data of a single block from RPC.
|
||||
#[derive(Debug)]
|
||||
pub struct RpcBlockData {
|
||||
/// Subset of JSON-RPC `get_block` data.
|
||||
pub get_block_response: GetBlockResponse,
|
||||
|
||||
/// The block itself.
|
||||
pub block: Block,
|
||||
|
||||
/// The correct seed height needed for this block for `RandomX`.
|
||||
pub seed_height: u64,
|
||||
/// The correct seed hash needed for this block for `RandomX`.
|
||||
pub seed_hash: [u8; 32],
|
||||
|
||||
/// All transactions in the block.
|
||||
/// This vec is:
|
||||
/// - the original transaction blobs
|
||||
pub txs: Vec<RpcTxData>,
|
||||
}
|
||||
|
||||
/// Data of a transaction.
|
||||
#[derive(Debug)]
|
||||
pub struct RpcTxData {
|
||||
/// The transactions itself.
|
||||
pub tx: Transaction,
|
||||
/// The transactions blob.
|
||||
pub tx_blob: Vec<u8>,
|
||||
/// The transaction's hash.
|
||||
pub tx_hash: [u8; 32],
|
||||
}
|
||||
|
||||
/// Subset of JSON-RPC `get_block` response.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct JsonRpcResponse {
|
||||
pub result: GetBlockResponse,
|
||||
}
|
||||
|
||||
/// Subset of JSON-RPC `get_block` data.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct GetBlockResponse {
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub blob: Vec<u8>,
|
||||
pub block_header: BlockHeader,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||
pub(crate) struct BlockHeader {
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub hash: [u8; 32],
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub pow_hash: [u8; 32],
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub miner_tx_hash: [u8; 32],
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub prev_hash: [u8; 32],
|
||||
|
||||
pub block_weight: usize,
|
||||
pub height: u64,
|
||||
pub major_version: u8,
|
||||
pub minor_version: u8,
|
||||
pub nonce: u32,
|
||||
pub num_txes: usize,
|
||||
pub reward: u64,
|
||||
pub timestamp: u64,
|
||||
}
|
|
@ -1,322 +0,0 @@
|
|||
use std::{
|
||||
collections::HashSet,
|
||||
num::{NonZeroU64, NonZeroUsize},
|
||||
sync::{atomic::Ordering, LazyLock, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crossbeam::channel::Receiver;
|
||||
use monero_serai::block::Block;
|
||||
use randomx_rs::RandomXVM;
|
||||
|
||||
use crate::{
|
||||
constants::{RANDOMX_START_HEIGHT, TESTED_BLOCK_COUNT, TESTED_TX_COUNT},
|
||||
cryptonight::CryptoNightHash,
|
||||
types::{BlockHeader, GetBlockResponse, RpcBlockData, RpcTxData},
|
||||
};
|
||||
|
||||
struct Verifier {
|
||||
id: usize,
|
||||
now: Instant,
|
||||
thread_count: NonZeroUsize,
|
||||
update: NonZeroU64,
|
||||
top_height: u64,
|
||||
rx: Receiver<RpcBlockData>,
|
||||
seed_hash: [u8; 32],
|
||||
timestamp: u64,
|
||||
randomx_vm: Option<RandomXVM>,
|
||||
}
|
||||
|
||||
#[expect(clippy::needless_pass_by_value)]
|
||||
pub fn spawn_verify_pool(
|
||||
thread_count: NonZeroUsize,
|
||||
update: NonZeroU64,
|
||||
top_height: u64,
|
||||
rx: Receiver<RpcBlockData>,
|
||||
) {
|
||||
let now = Instant::now();
|
||||
|
||||
for id in 0..thread_count.get() {
|
||||
let rx = rx.clone();
|
||||
std::thread::spawn(move || {
|
||||
Verifier {
|
||||
id,
|
||||
now,
|
||||
thread_count,
|
||||
update,
|
||||
top_height,
|
||||
rx,
|
||||
seed_hash: [0; 32],
|
||||
timestamp: 0,
|
||||
randomx_vm: None,
|
||||
}
|
||||
.loop_listen_verify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Verifier {
|
||||
fn loop_listen_verify(mut self) {
|
||||
loop {
|
||||
let Ok(data) = self.rx.recv() else {
|
||||
println!("Exiting verify thread {}/{}", self.id, self.thread_count);
|
||||
return;
|
||||
};
|
||||
|
||||
self.verify(data);
|
||||
}
|
||||
}
|
||||
|
||||
fn verify(&mut self, data: RpcBlockData) {
|
||||
//----------------------------------------------- Create panic info.
|
||||
let p = format!("data: {data:#?}");
|
||||
|
||||
//----------------------------------------------- Extract data.
|
||||
let RpcBlockData {
|
||||
get_block_response,
|
||||
block,
|
||||
seed_height,
|
||||
seed_hash,
|
||||
txs,
|
||||
} = data;
|
||||
let GetBlockResponse { blob, block_header } = get_block_response;
|
||||
|
||||
//----------------------------------------------- Calculate some data.
|
||||
let calculated_block_reward = block
|
||||
.miner_transaction
|
||||
.prefix()
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|o| o.amount.unwrap())
|
||||
.sum::<u64>();
|
||||
let calculated_block_weight = txs
|
||||
.iter()
|
||||
.map(|RpcTxData { tx, .. }| tx.weight())
|
||||
.sum::<usize>();
|
||||
let calculated_pow_data = block.serialize_pow_hash();
|
||||
let miner_tx_weight = block.miner_transaction.weight();
|
||||
|
||||
//----------------------------------------------- Verify.
|
||||
Self::verify_block_properties(&blob, &block, calculated_block_reward, &p);
|
||||
Self::verify_all_transactions_are_unique(&txs, &p);
|
||||
Self::verify_transaction_properties(txs, &p);
|
||||
|
||||
self.verify_block_fields(
|
||||
calculated_block_weight,
|
||||
calculated_block_reward,
|
||||
&block,
|
||||
&p,
|
||||
block_header,
|
||||
);
|
||||
|
||||
let algo = self.verify_pow(
|
||||
block_header.height,
|
||||
seed_hash,
|
||||
block_header.pow_hash,
|
||||
&calculated_pow_data,
|
||||
&p,
|
||||
);
|
||||
|
||||
//----------------------------------------------- Print progress.
|
||||
self.print_progress(algo, seed_height, miner_tx_weight, block_header);
|
||||
}
|
||||
|
||||
fn verify_block_properties(
|
||||
block_blob: &[u8],
|
||||
block: &Block,
|
||||
calculated_block_reward: u64,
|
||||
p: &str,
|
||||
) {
|
||||
// Test block properties.
|
||||
assert_eq!(block_blob, block.serialize(), "{p}");
|
||||
|
||||
assert!(
|
||||
!block.miner_transaction.prefix().outputs.is_empty(),
|
||||
"miner_tx has no outputs\n{p}"
|
||||
);
|
||||
|
||||
assert_ne!(calculated_block_reward, 0, "block reward is 0\n{p}");
|
||||
}
|
||||
|
||||
#[expect(clippy::significant_drop_tightening)]
|
||||
fn verify_all_transactions_are_unique(txs: &[RpcTxData], p: &str) {
|
||||
static TX_SET: LazyLock<Mutex<HashSet<[u8; 32]>>> =
|
||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||
|
||||
let mut tx_set = TX_SET.lock().unwrap();
|
||||
|
||||
for tx_hash in txs.iter().map(|RpcTxData { tx_hash, .. }| tx_hash) {
|
||||
assert!(
|
||||
tx_set.insert(*tx_hash),
|
||||
"duplicated tx_hash: {}, {p}",
|
||||
hex::encode(tx_hash),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_transaction_properties(txs: Vec<RpcTxData>, p: &str) {
|
||||
// Test transaction properties.
|
||||
for RpcTxData {
|
||||
tx,
|
||||
tx_blob,
|
||||
tx_hash,
|
||||
} in txs
|
||||
{
|
||||
assert_eq!(tx_hash, tx.hash(), "{p}, tx: {tx:#?}");
|
||||
assert_ne!(tx.weight(), 0, "{p}, tx: {tx:#?}");
|
||||
assert!(!tx.prefix().inputs.is_empty(), "{p}, tx: {tx:#?}");
|
||||
assert_eq!(tx_blob, tx.serialize(), "{p}, tx: {tx:#?}");
|
||||
assert!(matches!(tx.version(), 1 | 2), "{p}, tx: {tx:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_block_fields(
|
||||
&mut self,
|
||||
calculated_block_weight: usize,
|
||||
calculated_block_reward: u64,
|
||||
block: &Block,
|
||||
p: &str,
|
||||
BlockHeader {
|
||||
block_weight,
|
||||
hash,
|
||||
pow_hash: _,
|
||||
height,
|
||||
major_version,
|
||||
minor_version,
|
||||
miner_tx_hash,
|
||||
nonce,
|
||||
num_txes,
|
||||
prev_hash,
|
||||
reward,
|
||||
timestamp,
|
||||
}: BlockHeader,
|
||||
) {
|
||||
// Test block fields are correct.
|
||||
assert_eq!(block_weight, calculated_block_weight, "{p}");
|
||||
assert_ne!(block.miner_transaction.weight(), 0, "{p}");
|
||||
assert_eq!(hash, block.hash(), "{p}");
|
||||
assert_eq!(
|
||||
height,
|
||||
u64::try_from(block.number().unwrap()).unwrap(),
|
||||
"{p}"
|
||||
);
|
||||
assert_eq!(major_version, block.header.hardfork_version, "{p}");
|
||||
assert_eq!(minor_version, block.header.hardfork_signal, "{p}");
|
||||
assert_eq!(miner_tx_hash, block.miner_transaction.hash(), "{p}");
|
||||
assert_eq!(nonce, block.header.nonce, "{p}");
|
||||
assert_eq!(num_txes, block.transactions.len(), "{p}");
|
||||
assert_eq!(prev_hash, block.header.previous, "{p}");
|
||||
assert_eq!(reward, calculated_block_reward, "{p}");
|
||||
assert_eq!(timestamp, block.header.timestamp, "{p}");
|
||||
|
||||
if timestamp != 0 {
|
||||
assert!(timestamp > self.timestamp, "{p}");
|
||||
self.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_pow(
|
||||
&mut self,
|
||||
height: u64,
|
||||
seed_hash: [u8; 32],
|
||||
pow_hash: [u8; 32],
|
||||
calculated_pow_data: &[u8],
|
||||
p: &str,
|
||||
) -> &'static str {
|
||||
let (algo, calculated_pow_hash) = if height < RANDOMX_START_HEIGHT {
|
||||
CryptoNightHash::hash(calculated_pow_data, height)
|
||||
} else {
|
||||
if self.seed_hash != seed_hash {
|
||||
self.randomx_vm = None;
|
||||
}
|
||||
|
||||
let randomx_vm = self.randomx_vm.get_or_insert_with(|| {
|
||||
self.seed_hash = seed_hash;
|
||||
// crate::randomx::randomx_vm_optimized(&seed_hash)
|
||||
crate::randomx::randomx_vm_default(&seed_hash)
|
||||
});
|
||||
|
||||
let pow_hash = randomx_vm
|
||||
.calculate_hash(calculated_pow_data)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
("randomx", pow_hash)
|
||||
};
|
||||
|
||||
assert_eq!(calculated_pow_hash, pow_hash, "{p}",);
|
||||
|
||||
algo
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn print_progress(
|
||||
&self,
|
||||
algo: &'static str,
|
||||
seed_height: u64,
|
||||
miner_tx_weight: usize,
|
||||
BlockHeader {
|
||||
block_weight,
|
||||
hash,
|
||||
pow_hash,
|
||||
height,
|
||||
major_version,
|
||||
minor_version,
|
||||
miner_tx_hash,
|
||||
nonce,
|
||||
num_txes,
|
||||
prev_hash,
|
||||
reward,
|
||||
timestamp,
|
||||
}: BlockHeader,
|
||||
) {
|
||||
let count = TESTED_BLOCK_COUNT.fetch_add(1, Ordering::Release) + 1;
|
||||
let total_tx_count = TESTED_TX_COUNT.fetch_add(num_txes, Ordering::Release) + 1;
|
||||
|
||||
if count % self.update.get() != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let top_height = self.top_height;
|
||||
|
||||
let percent = (count as f64 / top_height as f64) * 100.0;
|
||||
|
||||
let elapsed = self.now.elapsed().as_secs_f64();
|
||||
let secs_per_hash = elapsed / count as f64;
|
||||
let bps = count as f64 / elapsed;
|
||||
let remaining_secs = (top_height as f64 - count as f64) * secs_per_hash;
|
||||
let h = (remaining_secs / 60.0 / 60.0) as u64;
|
||||
let m = (remaining_secs / 60.0 % 60.0) as u64;
|
||||
let s = (remaining_secs % 60.0) as u64;
|
||||
|
||||
let pow_hash = hex::encode(pow_hash);
|
||||
let seed_hash = hex::encode(self.seed_hash);
|
||||
let block_hash = hex::encode(hash);
|
||||
let miner_tx_hash = hex::encode(miner_tx_hash);
|
||||
let prev_hash = hex::encode(prev_hash);
|
||||
|
||||
println!("progress | {count}/{top_height} ({percent:.2}%, {algo}, {bps:.2} blocks/sec, {h}h {m}m {s}s left)
|
||||
seed_hash | {seed_hash}
|
||||
pow_hash | {pow_hash}
|
||||
block_hash | {block_hash}
|
||||
miner_tx_hash | {miner_tx_hash}
|
||||
prev_hash | {prev_hash}
|
||||
reward | {reward}
|
||||
timestamp | {timestamp}
|
||||
nonce | {nonce}
|
||||
total_tx_count | {total_tx_count}
|
||||
height | {height}
|
||||
seed_height | {seed_height}
|
||||
block_weight | {block_weight}
|
||||
miner_tx_weight | {miner_tx_weight}
|
||||
major_version | {major_version}
|
||||
minor_version | {minor_version}
|
||||
num_txes | {num_txes}\n",
|
||||
);
|
||||
}
|
||||
}
|
17
tests/monero-serai/Cargo.toml
Normal file
17
tests/monero-serai/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "tests-monero-serai"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
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 = { workspace = true, features = ["json"] }
|
||||
rayon = { workspace = true }
|
||||
futures = { workspace = true, features = ["std"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
85
tests/monero-serai/src/main.rs
Normal file
85
tests/monero-serai/src/main.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use std::{
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
mod rpc;
|
||||
|
||||
pub static TESTED_BLOCK_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
pub static TESTED_TX_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let now = Instant::now();
|
||||
|
||||
let rpc_url = if let Ok(url) = std::env::var("RPC_URL") {
|
||||
println!("RPC_URL (found): {url}");
|
||||
url
|
||||
} else {
|
||||
let rpc_url = "http://127.0.0.1:18081".to_string();
|
||||
println!("RPC_URL (off, using default): {rpc_url}");
|
||||
rpc_url
|
||||
};
|
||||
if std::env::var("VERBOSE").is_ok() {
|
||||
println!("VERBOSE: true");
|
||||
} else {
|
||||
println!("VERBOSE: false");
|
||||
}
|
||||
|
||||
let mut client = rpc::RpcClient::new(rpc_url).await;
|
||||
|
||||
let top_height = if let Ok(Ok(h)) = std::env::var("TOP_HEIGHT").map(|s| s.parse()) {
|
||||
client.top_height = h;
|
||||
println!("TOP_HEIGHT (found): {h}");
|
||||
h
|
||||
} else {
|
||||
println!("TOP_HEIGHT (off, using latest): {}", client.top_height);
|
||||
client.top_height
|
||||
};
|
||||
|
||||
let ranges = (0..top_height)
|
||||
.collect::<Vec<usize>>()
|
||||
.chunks(100_000)
|
||||
.map(<[usize]>::to_vec)
|
||||
.collect::<Vec<Vec<usize>>>();
|
||||
|
||||
println!("ranges: [");
|
||||
for range in &ranges {
|
||||
println!(
|
||||
" ({}..{}),",
|
||||
range.first().unwrap(),
|
||||
range.last().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
println!("]\n");
|
||||
|
||||
let iter = ranges.into_iter().map(move |range| {
|
||||
let c = client.clone();
|
||||
async move {
|
||||
tokio::task::spawn_blocking(move || async move {
|
||||
c.get_block_test_batch(range.into_iter().collect()).await;
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
futures::future::join_all(iter).await;
|
||||
|
||||
loop {
|
||||
let block_count = TESTED_BLOCK_COUNT.load(Ordering::Acquire);
|
||||
let tx_count = TESTED_TX_COUNT.load(Ordering::Acquire);
|
||||
|
||||
if top_height == block_count {
|
||||
println!(
|
||||
"finished processing: blocks: {block_count}/{top_height}, txs: {tx_count}, took {}s",
|
||||
now.elapsed().as_secs()
|
||||
);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
320
tests/monero-serai/src/rpc.rs
Normal file
320
tests/monero-serai/src/rpc.rs
Normal file
|
@ -0,0 +1,320 @@
|
|||
use std::{
|
||||
collections::{BTreeSet, HashSet},
|
||||
sync::{atomic::Ordering, LazyLock},
|
||||
};
|
||||
|
||||
use hex::serde::deserialize;
|
||||
use monero_serai::{block::Block, transaction::Transaction};
|
||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderValue},
|
||||
Client, ClientBuilder,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{TESTED_BLOCK_COUNT, TESTED_TX_COUNT};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(crate) struct BlockHeader {
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub hash: Vec<u8>,
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub miner_tx_hash: Vec<u8>,
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub prev_hash: Vec<u8>,
|
||||
|
||||
pub block_weight: usize,
|
||||
pub height: usize,
|
||||
pub major_version: u8,
|
||||
pub minor_version: u8,
|
||||
pub nonce: u32,
|
||||
pub num_txes: usize,
|
||||
pub reward: u64,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct RpcClient {
|
||||
client: Client,
|
||||
rpc_url: String,
|
||||
pub top_height: usize,
|
||||
}
|
||||
|
||||
impl RpcClient {
|
||||
pub(crate) async fn new(rpc_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();
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct JsonRpcResponse {
|
||||
result: GetLastBlockHeaderResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(crate) struct GetLastBlockHeaderResponse {
|
||||
pub block_header: BlockHeader,
|
||||
}
|
||||
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"method": "get_last_block_header",
|
||||
"params": {}
|
||||
});
|
||||
|
||||
let top_height = client
|
||||
.get(format!("{rpc_url}/json_rpc"))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<JsonRpcResponse>()
|
||||
.await
|
||||
.unwrap()
|
||||
.result
|
||||
.block_header
|
||||
.height;
|
||||
|
||||
assert!(top_height > 3301441, "node is behind");
|
||||
|
||||
Self {
|
||||
client,
|
||||
rpc_url,
|
||||
top_height,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_transactions(&self, tx_hashes: Vec<[u8; 32]>) -> Vec<(Transaction, Vec<u8>)> {
|
||||
assert!(!tx_hashes.is_empty());
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(crate) struct GetTransactionsResponse {
|
||||
pub txs: Vec<Tx>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(crate) struct Tx {
|
||||
pub as_hex: String,
|
||||
pub pruned_as_hex: String,
|
||||
}
|
||||
|
||||
let url = format!("{}/get_transactions", self.rpc_url);
|
||||
|
||||
let txs_hashes = tx_hashes
|
||||
.into_iter()
|
||||
.map(hex::encode)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let request = json!({
|
||||
"txs_hashes": txs_hashes,
|
||||
});
|
||||
|
||||
let txs = self
|
||||
.client
|
||||
.get(&url)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<GetTransactionsResponse>()
|
||||
.await
|
||||
.unwrap()
|
||||
.txs;
|
||||
|
||||
txs.into_par_iter()
|
||||
.map(|r| {
|
||||
let blob = hex::decode(if r.as_hex.is_empty() {
|
||||
r.pruned_as_hex
|
||||
} else {
|
||||
r.as_hex
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
(Transaction::read(&mut blob.as_slice()).unwrap(), blob)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[expect(clippy::significant_drop_tightening)]
|
||||
pub(crate) async fn get_block_test_batch(&self, heights: BTreeSet<usize>) {
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct JsonRpcResponse {
|
||||
result: GetBlockResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(crate) struct GetBlockResponse {
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub blob: Vec<u8>,
|
||||
pub block_header: BlockHeader,
|
||||
}
|
||||
|
||||
let tasks = heights.into_iter().map(|height| {
|
||||
let json_rpc_url = format!("{}/json_rpc", self.rpc_url);
|
||||
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"method": "get_block",
|
||||
"params": {"height": height}
|
||||
});
|
||||
|
||||
let task = tokio::task::spawn(self.client.get(&json_rpc_url).json(&request).send());
|
||||
|
||||
(height, task)
|
||||
});
|
||||
|
||||
for (height, task) in tasks {
|
||||
let resp = task
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.json::<JsonRpcResponse>()
|
||||
.await
|
||||
.unwrap()
|
||||
.result;
|
||||
|
||||
let info = format!("\nheight: {height}\nresponse: {resp:#?}");
|
||||
|
||||
// Test block deserialization.
|
||||
let block = match Block::read(&mut resp.blob.as_slice()) {
|
||||
Ok(b) => b,
|
||||
Err(e) => panic!("{e:?}\n{info}"),
|
||||
};
|
||||
|
||||
// Fetch all transactions.
|
||||
let mut tx_hashes = vec![block.miner_transaction.hash()];
|
||||
tx_hashes.extend(block.transactions.iter());
|
||||
let txs = self.get_transactions(tx_hashes.clone()).await;
|
||||
assert_eq!(tx_hashes.len(), txs.len());
|
||||
|
||||
// Test all transactions are unique.
|
||||
{
|
||||
static TX_SET: LazyLock<Mutex<HashSet<[u8; 32]>>> =
|
||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||
|
||||
let tx_hashes = tx_hashes.clone();
|
||||
let mut tx_set = TX_SET.lock().await;
|
||||
|
||||
for hash in tx_hashes {
|
||||
assert!(
|
||||
tx_set.insert(hash),
|
||||
"duplicated tx hash: {}\n{info}",
|
||||
hex::encode(hash),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let top_height = self.top_height;
|
||||
|
||||
#[expect(clippy::cast_precision_loss)]
|
||||
rayon::spawn(move || {
|
||||
// Test block properties.
|
||||
assert_eq!(resp.blob, block.serialize(), "{info}");
|
||||
|
||||
assert!(
|
||||
!block.miner_transaction.prefix().outputs.is_empty(),
|
||||
"miner_tx has no outputs\n{info}"
|
||||
);
|
||||
|
||||
let block_reward = block
|
||||
.miner_transaction
|
||||
.prefix()
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|o| o.amount.unwrap())
|
||||
.sum::<u64>();
|
||||
assert_ne!(block_reward, 0, "block reward is 0\n{info}");
|
||||
|
||||
let BlockHeader {
|
||||
block_weight,
|
||||
hash,
|
||||
height,
|
||||
major_version,
|
||||
minor_version,
|
||||
miner_tx_hash,
|
||||
nonce,
|
||||
num_txes,
|
||||
prev_hash,
|
||||
reward,
|
||||
timestamp,
|
||||
} = resp.block_header;
|
||||
|
||||
let total_block_weight = txs.iter().map(|(tx, _)| tx.weight()).sum::<usize>();
|
||||
|
||||
// Test transaction properties.
|
||||
txs.into_par_iter()
|
||||
.zip(tx_hashes)
|
||||
.for_each(|((tx, blob), hash)| {
|
||||
assert_eq!(hash, tx.hash(), "{info}, tx: {tx:#?}");
|
||||
assert_ne!(tx.weight(), 0, "{info}, tx: {tx:#?}");
|
||||
assert!(!tx.prefix().inputs.is_empty(), "{info}, tx: {tx:#?}");
|
||||
assert_eq!(blob, tx.serialize(), "{info}, tx: {tx:#?}");
|
||||
assert!(matches!(tx.version(), 1 | 2), "{info}, tx: {tx:#?}");
|
||||
});
|
||||
|
||||
// Test block fields are correct.
|
||||
assert_eq!(block_weight, total_block_weight, "{info}");
|
||||
assert_ne!(block.miner_transaction.weight(), 0, "{info}");
|
||||
assert_eq!(hash, block.hash(), "{info}");
|
||||
assert_eq!(height, block.number().unwrap(), "{info}");
|
||||
assert_eq!(major_version, block.header.hardfork_version, "{info}");
|
||||
assert_eq!(minor_version, block.header.hardfork_signal, "{info}");
|
||||
assert_eq!(miner_tx_hash, block.miner_transaction.hash(), "{info}");
|
||||
assert_eq!(nonce, block.header.nonce, "{info}");
|
||||
assert_eq!(num_txes, block.transactions.len(), "{info}");
|
||||
assert_eq!(prev_hash, block.header.previous, "{info}");
|
||||
assert_eq!(reward, block_reward, "{info}");
|
||||
assert_eq!(timestamp, block.header.timestamp, "{info}");
|
||||
|
||||
let progress = TESTED_BLOCK_COUNT.fetch_add(1, Ordering::Release) + 1;
|
||||
let tx_count = TESTED_TX_COUNT.fetch_add(num_txes, Ordering::Release) + 1;
|
||||
|
||||
if std::env::var("VERBOSE").is_err() && progress % 1000 != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let percent = (progress as f64 / top_height as f64) * 100.0;
|
||||
|
||||
println!(
|
||||
"progress | {progress}/{top_height} ({percent:.2}%)
|
||||
tx_count | {tx_count}
|
||||
hash | {}
|
||||
miner_tx_hash | {}
|
||||
prev_hash | {}
|
||||
reward | {}
|
||||
timestamp | {}
|
||||
nonce | {}
|
||||
height | {}
|
||||
block_weight | {}
|
||||
miner_tx_weight | {}
|
||||
major_version | {}
|
||||
minor_version | {}
|
||||
num_txes | {}\n",
|
||||
hex::encode(hash),
|
||||
hex::encode(miner_tx_hash),
|
||||
hex::encode(prev_hash),
|
||||
reward,
|
||||
timestamp,
|
||||
nonce,
|
||||
height,
|
||||
block_weight,
|
||||
block.miner_transaction.weight(),
|
||||
major_version,
|
||||
minor_version,
|
||||
num_txes,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
23
tests/pow/Cargo.toml
Normal file
23
tests/pow/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "tests-pow"
|
||||
version = "0.0.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"] }
|
||||
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"] }
|
||||
rayon = { workspace = true }
|
||||
randomx-rs = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
59
tests/pow/src/main.rs
Normal file
59
tests/pow/src/main.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
mod rpc;
|
||||
|
||||
use std::{
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub static TESTED_BLOCK_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let now = Instant::now();
|
||||
|
||||
let rpc_url = if let Ok(url) = std::env::var("RPC_URL") {
|
||||
println!("RPC_URL (found): {url}");
|
||||
url
|
||||
} else {
|
||||
let rpc_url = "http://127.0.0.1:18081".to_string();
|
||||
println!("RPC_URL (off, using default): {rpc_url}");
|
||||
rpc_url
|
||||
};
|
||||
if std::env::var("VERBOSE").is_ok() {
|
||||
println!("VERBOSE: true");
|
||||
} else {
|
||||
println!("VERBOSE: false");
|
||||
}
|
||||
|
||||
let mut client = rpc::RpcClient::new(rpc_url).await;
|
||||
|
||||
let top_height = if let Ok(Ok(h)) = std::env::var("TOP_HEIGHT").map(|s| s.parse()) {
|
||||
client.top_height = h;
|
||||
println!("TOP_HEIGHT (found): {h}");
|
||||
h
|
||||
} else {
|
||||
println!("TOP_HEIGHT (off, using latest): {}", client.top_height);
|
||||
client.top_height
|
||||
};
|
||||
|
||||
println!();
|
||||
|
||||
tokio::join!(
|
||||
client.cryptonight_v0(),
|
||||
client.cryptonight_v1(),
|
||||
client.cryptonight_v2(),
|
||||
client.cryptonight_r(),
|
||||
client.randomx(),
|
||||
);
|
||||
|
||||
loop {
|
||||
let count = TESTED_BLOCK_COUNT.load(Ordering::Acquire);
|
||||
|
||||
if top_height == count {
|
||||
println!("finished all PoW, took {}s", now.elapsed().as_secs());
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
262
tests/pow/src/rpc.rs
Normal file
262
tests/pow/src/rpc.rs
Normal file
|
@ -0,0 +1,262 @@
|
|||
use std::{
|
||||
collections::BTreeMap,
|
||||
ops::Range,
|
||||
sync::{atomic::Ordering, Mutex},
|
||||
};
|
||||
|
||||
use function_name::named;
|
||||
use hex::serde::deserialize;
|
||||
use hex_literal::hex;
|
||||
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<u8>,
|
||||
pub block_header: BlockHeader,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct BlockHeader {
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub pow_hash: Vec<u8>,
|
||||
#[serde(deserialize_with = "deserialize")]
|
||||
pub hash: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct RpcClient {
|
||||
client: Client,
|
||||
rpc_url: String,
|
||||
pub top_height: usize,
|
||||
}
|
||||
|
||||
impl RpcClient {
|
||||
pub(crate) async fn new(rpc_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(format!("{rpc_url}/json_rpc"))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap()
|
||||
.get("result")
|
||||
.unwrap()
|
||||
.get("block_header")
|
||||
.unwrap()
|
||||
.get("height")
|
||||
.unwrap()
|
||||
.as_u64()
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
assert!(top_height > 3301441, "node is behind");
|
||||
|
||||
Self {
|
||||
client,
|
||||
rpc_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}
|
||||
});
|
||||
|
||||
let rpc_url = format!("{}/json_rpc", self.rpc_url);
|
||||
|
||||
tokio::task::spawn(self.client.get(rpc_url).json(&request).send())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.json::<JsonRpcResponse>()
|
||||
.await
|
||||
.unwrap()
|
||||
.result
|
||||
}
|
||||
|
||||
async fn test<const RANDOMX: bool, const CRYPTONIGHT_V0: bool>(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
hash: impl Fn(Vec<u8>, 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 = if CRYPTONIGHT_V0 && height == 202612 {
|
||||
hex!("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000")
|
||||
} else {
|
||||
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) + 1;
|
||||
|
||||
if std::env::var("VERBOSE").is_err() && count % 500 != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let 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}
|
||||
hash | {hash}\n"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[named]
|
||||
pub(crate) async fn cryptonight_v0(&self) {
|
||||
self.test::<false, true>(
|
||||
0..1546000,
|
||||
|b, _, _, _| cuprate_cryptonight::cryptonight_hash_v0(&b),
|
||||
function_name!(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[named]
|
||||
pub(crate) async fn cryptonight_v1(&self) {
|
||||
self.test::<false, false>(
|
||||
1546000..1685555,
|
||||
|b, _, _, _| cuprate_cryptonight::cryptonight_hash_v1(&b).unwrap(),
|
||||
function_name!(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[named]
|
||||
pub(crate) async fn cryptonight_v2(&self) {
|
||||
self.test::<false, false>(
|
||||
1685555..1788000,
|
||||
|b, _, _, _| cuprate_cryptonight::cryptonight_hash_v2(&b),
|
||||
function_name!(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[named]
|
||||
pub(crate) async fn cryptonight_r(&self) {
|
||||
self.test::<false, false>(
|
||||
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<u8>, _, seed_height, seed_hash: [u8; 32]| {
|
||||
static RANDOMX_VM: ThreadLocal<Mutex<BTreeMap<u64, RandomXVM>>> = 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::<true, false>(1978433..self.top_height, function, function_name!())
|
||||
.await;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue