cuprated: integrate RpcServer (#423)

* add rpc server

* add init fn

* add layers

* docs

* comments

* move

* warn

* split config

* split

* fix toml

* impl p2p port

* fix tests

* docs

* doc

* remove (de)compression

* `advertise`

* Update binaries/cuprated/src/config/rpc.rs

Co-authored-by: Boog900 <boog900@tutanota.com>

* update tracing

* `tracing::field::display`

* fix

* typo

* docs

* clippy

* remove comment_out

* add test for `cuprate_helper::net::ip_is_local`

* add `FIXME`

---------

Co-authored-by: Boog900 <boog900@tutanota.com>
This commit is contained in:
hinto-janai
2025-05-30 14:38:11 -04:00
committed by GitHub
parent 2a3de0b9ac
commit 004983d09e
13 changed files with 371 additions and 7 deletions

19
Cargo.lock generated
View File

@@ -1115,6 +1115,7 @@ version = "0.0.3"
dependencies = [
"anyhow",
"async-trait",
"axum",
"bitflags 2.9.0",
"borsh",
"bytemuck",
@@ -1164,6 +1165,7 @@ dependencies = [
"nu-ansi-term",
"paste",
"pin-project",
"pretty_assertions",
"rand",
"rand_distr",
"randomx-rs",
@@ -1181,6 +1183,7 @@ dependencies = [
"toml",
"toml_edit",
"tower 0.5.1",
"tower-http",
"tracing",
"tracing-appender",
"tracing-subscriber",
@@ -3382,6 +3385,22 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
dependencies = [
"bitflags 2.9.0",
"bytes",
"http",
"http-body",
"http-body-util",
"pin-project-lite",
"tower-layer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tower-service 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tower-layer"
version = "0.3.3"

View File

@@ -163,6 +163,7 @@ cuprate-rpc-interface = { path = "rpc/interface", default-featur
cuprate-zmq-types = { path = "zmq/types", default-features = false }
# External dependencies
axum = { version = "0.7", default-features = false }
anyhow = { version = "1", default-features = false }
arc-swap = { version = "1", default-features = false }
arrayvec = { version = "0.7", default-features = false }
@@ -204,6 +205,7 @@ tokio-util = { version = "0.7", default-features = false }
tokio-stream = { version = "0.1", default-features = false }
tokio = { version = "1", default-features = false }
tower = { git = "https://github.com/Cuprate/tower.git", rev = "6c7faf0", default-features = false } # <https://github.com/tower-rs/tower/pull/796>
tower-http = { version = "0.6", default-features = false }
toml = { version = "0.8", default-features = false }
toml_edit = { version = "0.22", default-features = false }
tracing-appender = { version = "0.2", default-features = false }

View File

@@ -23,14 +23,14 @@ cuprate-database = { workspace = true, features = ["serde"] }
cuprate-epee-encoding = { workspace = true }
cuprate-fast-sync = { workspace = true }
cuprate-fixed-bytes = { workspace = true }
cuprate-helper = { workspace = true, features = ["std", "serde", "time"] }
cuprate-helper = { workspace = true, features = ["std", "serde", "time", "net"] }
cuprate-hex = { workspace = true }
cuprate-json-rpc = { workspace = true }
cuprate-levin = { workspace = true }
cuprate-p2p-core = { workspace = true }
cuprate-p2p = { workspace = true }
cuprate-pruning = { workspace = true }
cuprate-rpc-interface = { workspace = true }
cuprate-rpc-interface = { workspace = true, features = ["dummy"] }
cuprate-rpc-types = { workspace = true, features = ["from"] }
cuprate-test-utils = { workspace = true }
cuprate-txpool = { workspace = true }
@@ -39,6 +39,7 @@ cuprate-wire = { workspace = true }
# TODO: after v1.0.0, remove unneeded dependencies.
axum = { workspace = true, features = ["tokio", "http1", "http2"] }
anyhow = { workspace = true }
async-trait = { workspace = true }
bitflags = { workspace = true }
@@ -78,13 +79,15 @@ tokio-stream = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true, features = ["parse", "display"]}
toml_edit = { workspace = true }
tower = { workspace = true }
tower = { workspace = true, features = ["limit"] }
tower-http = { workspace = true, features = ["limit"] }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true, features = ["std", "fmt", "default"] }
tracing = { workspace = true, features = ["default"] }
[dev-dependencies]
tempfile = { workspace = true }
tempfile = { workspace = true }
pretty_assertions = { workspace = true }
[build-dependencies]
cuprate-hex = { workspace = true }

View File

@@ -28,6 +28,7 @@ mod args;
mod fs;
mod p2p;
mod rayon;
mod rpc;
mod storage;
mod tokio;
mod tracing_config;
@@ -38,6 +39,7 @@ mod macros;
use fs::FileSystemConfig;
use p2p::P2PConfig;
use rayon::RayonConfig;
pub use rpc::{RpcConfig, SharedRpcConfig};
use storage::StorageConfig;
use tokio::TokioConfig;
use tracing_config::TracingConfig;
@@ -142,6 +144,10 @@ config_struct! {
/// Configuration for cuprated's P2P system.
pub p2p: P2PConfig,
#[child = true]
/// Configuration for cuprated's RPC system.
pub rpc: RpcConfig,
#[child = true]
/// Configuration for persistent data storage.
pub storage: StorageConfig,
@@ -161,6 +167,7 @@ impl Default for Config {
tokio: Default::default(),
rayon: Default::default(),
p2p: Default::default(),
rpc: Default::default(),
storage: Default::default(),
fs: Default::default(),
}
@@ -211,8 +218,7 @@ impl Config {
max_inbound_connections: self.p2p.clear_net.general.max_inbound_connections,
gray_peers_percent: self.p2p.clear_net.general.gray_peers_percent,
p2p_port: self.p2p.clear_net.general.p2p_port,
// TODO: set this if a public RPC server is set.
rpc_port: 0,
rpc_port: self.rpc.restricted.port_for_p2p(),
address_book_config: self
.p2p
.clear_net
@@ -271,6 +277,7 @@ impl fmt::Display for Config {
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use toml::from_str;
use super::*;

View File

@@ -9,6 +9,12 @@ use toml_edit::TableLike;
/// - `#[inline = true]`: inlines the struct into `{}` instead of having a separate `[]` header.
/// - `#[comment_out = true]`: comments out the field.
///
/// # Invariants
/// Required for this macro to work:
///
/// - struct must implement [`Default`] and `serde`
/// - None of the fields can be [`Option`]
///
/// # Documentation
/// Consider using the following style when adding documentation:
///

View File

@@ -0,0 +1,143 @@
use std::{
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
time::Duration,
};
use serde::{Deserialize, Serialize};
use crate::config::macros::config_struct;
config_struct! {
/// RPC config.
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields, default)]
pub struct RpcConfig {
#[child = true]
/// Configuration for the unrestricted RPC server.
pub unrestricted: UnrestrictedRpcConfig,
#[child = true]
/// Configuration for the restricted RPC server.
pub restricted: RestrictedRpcConfig,
}
}
config_struct! {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields, default)]
pub struct UnrestrictedRpcConfig {
/// Allow the unrestricted RPC server to be public.
///
/// ⚠️ WARNING ⚠️
/// -------------
/// Unrestricted RPC should almost never be made available
/// to the wider internet. If the unrestricted address
/// is a non-local address, `cuprated` will crash,
/// unless this setting is set to `true`.
///
/// Type | boolean
/// Valid values | true, false
pub i_know_what_im_doing_allow_public_unrestricted_rpc: bool,
#[flatten = true]
/// Shared config.
##[serde(flatten)]
pub shared: SharedRpcConfig,
}
}
impl Default for UnrestrictedRpcConfig {
fn default() -> Self {
Self {
i_know_what_im_doing_allow_public_unrestricted_rpc: false,
shared: SharedRpcConfig {
address: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 18081)),
enable: true,
request_byte_limit: 0,
},
}
}
}
config_struct! {
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields, default)]
pub struct RestrictedRpcConfig {
#[flatten = true]
/// Shared config.
##[serde(flatten)]
pub shared: SharedRpcConfig,
/// Advertise the restricted RPC port.
///
/// Setting this to `true` will make `cuprated`
/// share the restricted RPC server's port
/// publicly to the P2P network.
///
/// Type | boolean
/// Valid values | true, false
pub advertise: bool,
}
}
impl RestrictedRpcConfig {
/// Return the restricted RPC port for P2P if available and public.
pub const fn port_for_p2p(&self) -> u16 {
if self.advertise && self.shared.enable {
self.shared.address.port()
} else {
0
}
}
}
config_struct! {
/// Shared RPC configuration options.
///
/// Both RPC servers uses this struct.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields, default)]
pub struct SharedRpcConfig {
/// The address and port the RPC server will listen on.
///
/// Type | IPv4/IPv6 address + port
/// Examples | "", "127.0.0.1:18081", "192.168.1.50:18085"
pub address: SocketAddr,
/// Toggle the RPC server.
///
/// If `true` the RPC server will be enabled.
/// If `false` the RPC server will be disabled.
///
/// Type | boolean
/// Examples | true, false
pub enable: bool,
// FIXME: <https://github.com/Cuprate/cuprate/issues/492>
// Below should be `#[comment_out = true]` but is prevented by above issue.
/// If a request is above this byte limit, it will be rejected.
///
/// Setting this to `0` will disable the limit.
///
/// Type | Number
/// Valid values | >= 0
/// Examples | 0 (no limit), 5242880 (5MB), 10485760 (10MB)
pub request_byte_limit: usize,
// TODO: <https://github.com/Cuprate/cuprate/issues/445>
}
}
impl Default for SharedRpcConfig {
/// This returns the default for [`RestrictedRpcConfig`].
fn default() -> Self {
Self {
address: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 18089)),
enable: false,
// 1 megabyte.
// <https://github.com/monero-project/monero/blob/3b01c490953fe92f3c6628fa31d280a4f0490d28/src/cryptonote_config.h#L134>
request_byte_limit: 1024 * 1024,
}
}
}

View File

@@ -156,6 +156,9 @@ fn main() {
)
.await;
// Initialize the RPC server(s).
rpc::init_rpc_servers(config.rpc);
// Start the command listener.
if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
let (command_tx, command_rx) = mpsc::channel(1);

View File

@@ -5,6 +5,8 @@
mod constants;
mod handlers;
mod rpc_handler;
mod server;
mod service;
pub use rpc_handler::CupratedRpcHandler;
pub use server::init_rpc_servers;

View File

@@ -0,0 +1,99 @@
//! RPC server initialization and main loop.
use std::{
net::{IpAddr, SocketAddr},
time::Duration,
};
use anyhow::Error;
use tokio::net::TcpListener;
use tower::limit::rate::RateLimitLayer;
use tower_http::limit::RequestBodyLimitLayer;
use tracing::{field::display, info, warn};
use cuprate_rpc_interface::{RouterBuilder, RpcHandlerDummy};
use crate::{
config::{RpcConfig, SharedRpcConfig},
rpc::CupratedRpcHandler,
};
/// Initialize the RPC server(s).
///
/// # Panics
/// This function will panic if:
/// - the server(s) could not be started
/// - unrestricted RPC is started on non-local
/// address without override option
pub fn init_rpc_servers(config: RpcConfig) {
for (c, restricted) in [
(config.unrestricted.shared, false),
(config.restricted.shared, true),
] {
if !c.enable {
info!(restricted, "Skipping RPC server");
continue;
}
let addr = c.address;
if !restricted && !cuprate_helper::net::ip_is_local(addr.ip()) {
if config
.unrestricted
.i_know_what_im_doing_allow_public_unrestricted_rpc
{
warn!(
address = display(addr),
"Starting unrestricted RPC on non-local address, this is dangerous!"
);
} else {
panic!("Refusing to start unrestricted RPC on a non-local address ({addr})");
}
}
tokio::task::spawn(async move {
run_rpc_server(restricted, c).await.unwrap();
});
}
}
/// This initializes and runs an RPC server.
///
/// The function will only return when the server itself returns or an error occurs.
async fn run_rpc_server(restricted: bool, config: SharedRpcConfig) -> Result<(), Error> {
info!(
restricted,
address = display(&config.address),
"Starting RPC server"
);
// Create the router.
//
// TODO: impl more layers, rate-limiting, configuration, etc.
let state = RpcHandlerDummy { restricted };
// TODO:
// - add functions that are `all()` but for restricted RPC
// - enable aliases automatically `other_get_height` + `other_getheight`?
//
// FIXME:
// - `json_rpc` is 1 endpoint; `RouterBuilder` operates at the
// level endpoint; we can't selectively enable certain `json_rpc` methods
let router = RouterBuilder::new().fallback().build().with_state(state);
// Add restrictive layers if restricted RPC.
//
// TODO: <https://github.com/Cuprate/cuprate/issues/445>
let router = if config.request_byte_limit != 0 {
router.layer(RequestBodyLimitLayer::new(config.request_byte_limit))
} else {
router
};
// Start the server.
//
// TODO: impl custom server code, don't use axum.
let listener = TcpListener::bind(config.address).await?;
axum::serve(listener, router).await?;
Ok(())
}

View File

@@ -24,6 +24,7 @@ time = ["dep:chrono", "std"]
thread = ["std", "dep:target_os_lib"]
tx = ["dep:monero-serai"]
fmt = ["map", "std"]
net = []
[dependencies]
cuprate-constants = { workspace = true, optional = true, features = ["block"] }

View File

@@ -36,3 +36,6 @@ pub mod crypto;
#[cfg(feature = "fmt")]
pub mod fmt;
#[cfg(feature = "net")]
pub mod net;

76
helper/src/net.rs Normal file
View File

@@ -0,0 +1,76 @@
//! Networking utilities.
//!
//! `#[no_std]` compatible.
use core::net::IpAddr;
/// Returns [`true`] if the address is a local address
/// (non-reachable via the broader internet).
///
/// # FIXME
/// This is only mostly accurate.
///
/// It should be replaced when `std` stabilizes things:
/// <https://github.com/rust-lang/rust/issues/27709>
pub const fn ip_is_local(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(ip) => ip.is_loopback() || ip.is_private(),
IpAddr::V6(ip) => ip.is_loopback() || ip.is_unique_local() || ip.is_unicast_link_local(),
}
}
#[cfg(test)]
mod test {
use core::net::{Ipv4Addr, Ipv6Addr};
use super::*;
#[test]
fn ip_local() {
for ipv4 in [
Ipv4Addr::LOCALHOST,
Ipv4Addr::new(10, 0, 0, 0),
Ipv4Addr::new(10, 0, 255, 255),
Ipv4Addr::new(172, 16, 0, 0),
Ipv4Addr::new(172, 16, 255, 255),
Ipv4Addr::new(192, 168, 0, 0),
Ipv4Addr::new(192, 168, 255, 255),
] {
assert!(ip_is_local(ipv4.into()));
}
for ipv4 in [
Ipv4Addr::UNSPECIFIED,
Ipv4Addr::new(1, 1, 1, 1),
Ipv4Addr::new(176, 9, 0, 187),
Ipv4Addr::new(88, 198, 163, 90),
Ipv4Addr::new(66, 85, 74, 134),
Ipv4Addr::new(51, 79, 173, 165),
Ipv4Addr::new(192, 99, 8, 110),
Ipv4Addr::new(37, 187, 74, 171),
Ipv4Addr::new(77, 172, 183, 193),
] {
assert!(!ip_is_local(ipv4.into()));
}
for ipv6 in [
Ipv6Addr::LOCALHOST,
Ipv6Addr::new(0xfc02, 0, 0, 0, 0, 0, 0, 0),
Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0),
Ipv6Addr::new(0xfe80, 0, 0, 1, 0, 0, 0, 0),
Ipv6Addr::new(0xfe81, 0, 0, 0, 0, 0, 0, 0),
] {
assert!(ip_is_local(ipv6.into()));
}
for ipv6 in [
Ipv6Addr::UNSPECIFIED,
Ipv6Addr::new(1, 1, 1, 1, 1, 1, 1, 1),
Ipv6Addr::new(
0x1020, 0x3040, 0x5060, 0x7080, 0x90A0, 0xB0C0, 0xD0E0, 0xF00D,
),
] {
assert!(!ip_is_local(ipv6.into()));
}
}
}

View File

@@ -19,7 +19,7 @@ cuprate-rpc-types = { workspace = true, features = ["serde", "epee", "from"]
cuprate-helper = { workspace = true, features = ["asynch"], default-features = false, optional = true }
anyhow = { workspace = true }
axum = { version = "0.7.5", features = ["json"], default-features = false }
axum = { workspace = true, features = ["json"], default-features = false }
serde = { workspace = true, optional = true }
tower = { workspace = true, features = ["util"] }
paste = { workspace = true }