Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
479 changes: 465 additions & 14 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "3"
members = [
# Binaries
"binaries/cuprated",
"binaries/rpc-compat",

# Consensus
"consensus",
Expand Down
35 changes: 35 additions & 0 deletions binaries/rpc-compat/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "cuprate-rpc-compat"
version = "0.0.0"
edition = "2024"
description = "Cuprate RPC compatability testing harness"
license = "MIT"
authors = ["hinto-janai"]
repository = "https://github.com/Cuprate/cuprate/tree/main/rpc/cuprate-rpc-compat"
keywords = ["cuprate", "rpc", "compat"]

[features]
default = []

[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 = { version = "0.12", features = ["json"] }
randomx-rs = { workspace = true }

[dev-dependencies]

[lints]
workspace = true
108 changes: 108 additions & 0 deletions binaries/rpc-compat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# `cuprate-rpc-compat`
This crate provides tools for testing compatibility between `monerod` and `cuprated`'s daemon RPC API, specifically:
- Request/response type correctness (both have same schema)
- Value correctness (both output the same values)

There are cases where type/value correctness cannot be upheld by `cuprated`,
this crate provides tools for these cases as well.

# Harness
TODO

# Input permutations
TODO

# Leniency on correctness
TODO

# Configuring the RPC servers
TODO

# Example
TODO

```rust
// use std::sync::Arc;

// use tokio::{net::TcpListener, sync::Barrier};

// use cuprate_json_rpc::{Request, Response, Id};
// use cuprate_rpc_types::{
// json::{JsonRpcRequest, JsonRpcResponse, GetBlockCountResponse},
// other::{OtherRequest, OtherResponse},
// };
// use cuprate_rpc_interface::{RouterBuilder, RpcHandlerDummy};

// // Send a `/get_height` request. This endpoint has no inputs.
// async fn get_height(port: u16) -> OtherResponse {
// let url = format!("http://127.0.0.1:{port}/get_height");
// ureq::get(&url)
// .set("Content-Type", "application/json")
// .call()
// .unwrap()
// .into_json()
// .unwrap()
// }

// // Send a JSON-RPC request with the `get_block_count` method.
// //
// // The returned [`String`] is JSON.
// async fn get_block_count(port: u16) -> String {
// let url = format!("http://127.0.0.1:{port}/json_rpc");
// let method = JsonRpcRequest::GetBlockCount(Default::default());
// let request = Request::new(method);
// ureq::get(&url)
// .set("Content-Type", "application/json")
// .send_json(request)
// .unwrap()
// .into_string()
// .unwrap()
// }

// #[tokio::main]
// async fn main() {
// // Start a local RPC server.
// let port = {
// // Create the router.
// let state = RpcHandlerDummy { restricted: false };
// let router = RouterBuilder::new().all().build().with_state(state);

// // Start a server.
// let listener = TcpListener::bind("127.0.0.1:0")
// .await
// .unwrap();
// let port = listener.local_addr().unwrap().port();

// // Run the server with `axum`.
// tokio::task::spawn(async move {
// axum::serve(listener, router).await.unwrap();
// });

// port
// };

// // Assert the response is the default.
// let response = get_height(port).await;
// let expected = OtherResponse::GetHeight(Default::default());
// assert_eq!(response, expected);

// // Assert the response JSON is correct.
// let response = get_block_count(port).await;
// let expected = r#"{"jsonrpc":"2.0","id":null,"result":{"status":"OK","untrusted":false,"count":0}}"#;
// assert_eq!(response, expected);

// // Assert that (de)serialization works.
// let expected = Response::ok(Id::Null, Default::default());
// let response: Response<GetBlockCountResponse> = serde_json::from_str(&response).unwrap();
// assert_eq!(response, expected);
// }
```

# Feature flags
List of feature flags for `cuprate-rpc-compat`.

All are enabled by default.

| Feature flag | Does what |
|--------------|-----------|
| TODO | TODO
50 changes: 50 additions & 0 deletions binaries/rpc-compat/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use std::num::{NonZeroU64, NonZeroUsize};

use clap::Parser;

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

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

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

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

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

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

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

this
}
}
1 change: 1 addition & 0 deletions binaries/rpc-compat/src/harness.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

78 changes: 78 additions & 0 deletions binaries/rpc-compat/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(unreachable_pub, reason = "This is a binary, everything `pub` is ok")]

mod cli;
mod rpc;

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

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

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

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

todo!()
}

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

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

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

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

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

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

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

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

// // for each input permutation...
// for r in requests {
// // assert (a potential subset of) `monerod` and `cuprated`'s response fields match in value.
// let monerod_response = r.get(Node::Monerod);
// let cuprated_response = r.get(Node::Cuprated);
// assert_eq!(
// monerod_response.subset_of_response(),
// cuprated_response.subset_of_response(),
// );
// }
// }
Loading
Loading