mirror of
https://github.com/hinto-janai/cuprate.git
synced 2024-12-22 19:49:33 +00:00
Compare commits
5 commits
63378a0e96
...
e6c96a69fc
Author | SHA1 | Date | |
---|---|---|---|
|
e6c96a69fc | ||
|
6f85531966 | ||
|
a97367167b | ||
|
a6e506a8b6 | ||
|
1e6f760d72 |
18 changed files with 912 additions and 802 deletions
123
Cargo.lock
generated
123
Cargo.lock
generated
|
@ -59,12 +59,55 @@ version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
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]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
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]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.92"
|
version = "1.0.92"
|
||||||
|
@ -444,8 +487,10 @@ version = "4.5.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"clap_lex",
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
"terminal_size",
|
"terminal_size",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -467,6 +512,12 @@ version = "0.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const_format"
|
name = "const_format"
|
||||||
version = "0.2.33"
|
version = "0.2.33"
|
||||||
|
@ -1099,6 +1150,27 @@ dependencies = [
|
||||||
"tokio-util",
|
"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]]
|
[[package]]
|
||||||
name = "cuprate-txpool"
|
name = "cuprate-txpool"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
@ -1898,6 +1970,12 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
|
@ -3192,6 +3270,12 @@ dependencies = [
|
||||||
"spin",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.26.3"
|
version = "0.26.3"
|
||||||
|
@ -3317,39 +3401,6 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.66"
|
version = "1.0.66"
|
||||||
|
@ -3731,6 +3782,12 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -54,8 +54,7 @@ members = [
|
||||||
"types",
|
"types",
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
"tests/pow",
|
"tests/compat",
|
||||||
"tests/monero-serai",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
|
@ -1 +1,7 @@
|
||||||
|
# <https://rust-lang.github.io/rust-clippy/master/index.html#upper_case_acronyms>
|
||||||
upper-case-acronyms-aggressive = true
|
upper-case-acronyms-aggressive = true
|
||||||
|
|
||||||
|
# <https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown>
|
||||||
|
doc-valid-idents = [
|
||||||
|
"RandomX", ".."
|
||||||
|
]
|
||||||
|
|
31
tests/compat/Cargo.toml
Normal file
31
tests/compat/Cargo.toml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
[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
|
38
tests/compat/src/cli.rs
Normal file
38
tests/compat/src/cli.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
10
tests/compat/src/constants.rs
Normal file
10
tests/compat/src/constants.rs
Normal file
|
@ -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
tests/compat/src/cryptonight.rs
Normal file
62
tests/compat/src/cryptonight.rs
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
49
tests/compat/src/main.rs
Normal file
49
tests/compat/src/main.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
#![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));
|
||||||
|
}
|
||||||
|
}
|
28
tests/compat/src/randomx.rs
Normal file
28
tests/compat/src/randomx.rs
Normal file
|
@ -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
|
||||||
|
}
|
205
tests/compat/src/rpc.rs
Normal file
205
tests/compat/src/rpc.rs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
69
tests/compat/src/types.rs
Normal file
69
tests/compat/src/types.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
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,
|
||||||
|
}
|
322
tests/compat/src/verify.rs
Normal file
322
tests/compat/src/verify.rs
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
[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
|
|
|
@ -1,85 +0,0 @@
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,320 +0,0 @@
|
||||||
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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
[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
|
|
|
@ -1,59 +0,0 @@
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,262 +0,0 @@
|
||||||
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