diff --git a/Cargo.lock b/Cargo.lock index ff1a136ef5..5b10a69d60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12625,10 +12625,17 @@ name = "subspace-gateway" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "clap", "fdlimit", "futures", + "hex", + "jsonrpsee", "mimalloc", + "parking_lot 0.12.3", + "subspace-core-primitives", + "subspace-networking", + "subspace-rpc-primitives", "supports-color", "thiserror", "tokio", diff --git a/crates/subspace-gateway/Cargo.toml b/crates/subspace-gateway/Cargo.toml index 977e7952c4..675a9968f2 100644 --- a/crates/subspace-gateway/Cargo.toml +++ b/crates/subspace-gateway/Cargo.toml @@ -18,10 +18,17 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] anyhow = "1.0.89" +async-trait = "0.1.83" clap = { version = "4.5.18", features = ["derive"] } fdlimit = "0.3.0" futures = "0.3.31" +hex = "0.4.3" +jsonrpsee = { version = "0.24.5", features = ["server"] } mimalloc = "0.1.43" +parking_lot = "0.12.2" +subspace-core-primitives = { version = "0.1.0", path = "../subspace-core-primitives" } +subspace-networking = { version = "0.1.0", path = "../subspace-networking" } +subspace-rpc-primitives = { version = "0.1.0", path = "../subspace-rpc-primitives" } supports-color = "3.0.1" thiserror = "1.0.64" tokio = { version = "1.40.0", features = ["rt-multi-thread", "signal", "macros"] } diff --git a/crates/subspace-gateway/README.md b/crates/subspace-gateway/README.md index 4f6869d7a0..e939f26bf8 100644 --- a/crates/subspace-gateway/README.md +++ b/crates/subspace-gateway/README.md @@ -1,6 +1,6 @@ # Subspace Gateway -Data Gateway implementation for Subspace Network Blockchain using [Substrate](https://docs.substrate.io/) framework. +Data Gateway implementation for the Subspace Network Blockchain. ## Getting Started diff --git a/crates/subspace-gateway/src/commands/run.rs b/crates/subspace-gateway/src/commands/run.rs index 1733699a1e..9d39d053a5 100644 --- a/crates/subspace-gateway/src/commands/run.rs +++ b/crates/subspace-gateway/src/commands/run.rs @@ -1,6 +1,9 @@ //! Gateway run command. //! This is the primary command for the gateway. +mod dsn; + +use crate::commands::run::dsn::NetworkArgs; use crate::commands::shutdown_signal; use clap::Parser; use futures::{select, FutureExt}; @@ -19,8 +22,14 @@ pub struct RunOptions { #[derive(Debug, Parser)] pub(super) struct GatewayOptions { /// Enable development mode. - #[arg(long)] + /// + /// Implies following flags (unless customized): + /// * `--allow-private-ips` + #[arg(long, verbatim_doc_comment)] dev: bool, + + #[clap(flatten)] + dsn_options: NetworkArgs, } /// Default run command for gateway @@ -29,14 +38,28 @@ pub async fn run(run_options: RunOptions) -> anyhow::Result<()> { let signal = shutdown_signal(); let RunOptions { - gateway: GatewayOptions { dev: _ }, + gateway: GatewayOptions { + dev, + mut dsn_options, + }, } = run_options; + // Development mode handling is limited to this section + { + if dev { + dsn_options.allow_private_ips = true; + } + } + info!("Subspace Gateway"); info!("✌️ version {}", env!("CARGO_PKG_VERSION")); info!("❤️ by {}", env!("CARGO_PKG_AUTHORS")); - let dsn_fut = future::pending::<()>(); + // TODO: move this service code into its own function, in a new library part of this crate + #[expect(unused_variables, reason = "implementation is incomplete")] + let (dsn_node, mut dsn_node_runner, node_client) = dsn::configure_network(dsn_options).await?; + let dsn_fut = dsn_node_runner.run(); + let rpc_fut = future::pending::<()>(); // This defines order in which things are dropped diff --git a/crates/subspace-gateway/src/commands/run/dsn.rs b/crates/subspace-gateway/src/commands/run/dsn.rs new file mode 100644 index 0000000000..14deb3ca8c --- /dev/null +++ b/crates/subspace-gateway/src/commands/run/dsn.rs @@ -0,0 +1,99 @@ +//! DSN config and implementation for the gateway. + +use crate::node_client::{NodeClient, RpcNodeClient}; +use anyhow::anyhow; +use clap::{Parser, ValueHint}; +use subspace_networking::libp2p::kad::Mode; +use subspace_networking::libp2p::Multiaddr; +use subspace_networking::{construct, Config, KademliaMode, Node, NodeRunner}; +use tracing::{debug, info}; + +/// Configuration for network stack +#[derive(Debug, Parser)] +pub(crate) struct NetworkArgs { + /// WebSocket RPC URL of the Subspace node to connect to. + /// + /// This node provides the DSN protocol version, default bootstrap nodes, and piece validation + /// metadata. + #[arg(long, value_hint = ValueHint::Url, default_value = "ws://127.0.0.1:9944")] + node_rpc_url: String, + + /// Multiaddrs of DSN bootstrap nodes to connect to on startup, multiple are supported. + /// + /// The default bootstrap nodes are fetched from the node RPC connection. + #[arg(long)] + pub(crate) dsn_bootstrap_nodes: Vec, + + /// Multiaddrs of DSN reserved nodes to maintain a connection to, multiple are supported. + #[arg(long)] + dsn_reserved_peers: Vec, + + /// Enable non-global (private, shared, loopback..) addresses in the Kademlia DHT. + /// By default these addresses are excluded from the DHT. + #[arg(long, default_value_t = false)] + pub(crate) allow_private_ips: bool, + + /// Maximum established outgoing swarm connection limit. + #[arg(long, default_value_t = 100)] + pub(crate) out_connections: u32, + + /// Maximum pending outgoing swarm connection limit. + #[arg(long, default_value_t = 100)] + pub(crate) pending_out_connections: u32, +} + +/// Create a DSN network client with the supplied configuration. +// TODO: +// - move this DSN code into a new library part of this crate +// - change NetworkArgs to a struct that's independent of clap +pub async fn configure_network( + NetworkArgs { + node_rpc_url, + mut dsn_bootstrap_nodes, + dsn_reserved_peers, + allow_private_ips, + out_connections, + pending_out_connections, + }: NetworkArgs, +) -> anyhow::Result<(Node, NodeRunner<()>, RpcNodeClient)> { + // TODO: + // - store keypair on disk and allow CLI override + // - cache known peers on disk + // - prometheus telemetry + let default_config = Config::<()>::default(); + + info!(url = %node_rpc_url, "Connecting to node RPC"); + let node_client = RpcNodeClient::new(&node_rpc_url) + .await + .map_err(|error| anyhow!("Failed to connect to node RPC: {error}"))?; + + // The gateway only needs the first part of the farmer info. + let farmer_app_info = node_client + .farmer_app_info() + .await + .map_err(|error| anyhow!("Failed to get farmer app info: {error}"))?; + + // Fall back to the node's bootstrap nodes. + if dsn_bootstrap_nodes.is_empty() { + debug!(dsn_bootstrap_nodes = ?farmer_app_info.dsn_bootstrap_nodes, "Setting DSN bootstrap nodes..."); + dsn_bootstrap_nodes.clone_from(&farmer_app_info.dsn_bootstrap_nodes); + } + + let dsn_protocol_version = hex::encode(farmer_app_info.genesis_hash); + debug!(?dsn_protocol_version, "Setting DSN protocol version..."); + + let config = Config { + protocol_version: dsn_protocol_version, + bootstrap_addresses: dsn_bootstrap_nodes, + reserved_peers: dsn_reserved_peers, + allow_non_global_addresses_in_dht: allow_private_ips, + max_established_outgoing_connections: out_connections, + max_pending_outgoing_connections: pending_out_connections, + kademlia_mode: KademliaMode::Static(Mode::Client), + ..default_config + }; + + let (node, node_runner) = construct(config)?; + + Ok((node, node_runner, node_client)) +} diff --git a/crates/subspace-gateway/src/main.rs b/crates/subspace-gateway/src/main.rs index 0e30450697..072b27399e 100644 --- a/crates/subspace-gateway/src/main.rs +++ b/crates/subspace-gateway/src/main.rs @@ -7,6 +7,7 @@ )] mod commands; +mod node_client; use crate::commands::{init_logger, raise_fd_limit, Command}; use clap::Parser; diff --git a/crates/subspace-gateway/src/node_client.rs b/crates/subspace-gateway/src/node_client.rs new file mode 100644 index 0000000000..ab2c5ea096 --- /dev/null +++ b/crates/subspace-gateway/src/node_client.rs @@ -0,0 +1,61 @@ +//! Node client implementation that connects to node via RPC (WebSockets) + +use async_trait::async_trait; +use jsonrpsee::core::client::{ClientT, Error as JsonError}; +use jsonrpsee::rpc_params; +use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; +use std::fmt; +use std::sync::Arc; +use subspace_core_primitives::segments::{SegmentHeader, SegmentIndex}; +use subspace_rpc_primitives::FarmerAppInfo; + +/// Node client implementation that connects to node via RPC (WebSockets). +/// +/// This implementation is supposed to be used on local network and not via public Internet due to +/// sensitive contents. +#[derive(Debug, Clone)] +pub struct RpcNodeClient { + client: Arc, +} + +impl RpcNodeClient { + /// Create a new instance of [`NodeClient`]. + pub async fn new(url: &str) -> Result { + let client = Arc::new(WsClientBuilder::default().build(url).await?); + Ok(Self { client }) + } +} + +/// Abstraction of the Node Client +#[async_trait] +pub trait NodeClient: fmt::Debug + Send + Sync + 'static { + /// Get farmer app info + async fn farmer_app_info(&self) -> anyhow::Result; + + /// Get segment headers for the segments + #[expect(dead_code, reason = "implementation is incomplete")] + async fn segment_headers( + &self, + segment_indices: Vec, + ) -> anyhow::Result>>; +} + +#[async_trait] +impl NodeClient for RpcNodeClient { + async fn farmer_app_info(&self) -> anyhow::Result { + Ok(self + .client + .request("subspace_getFarmerAppInfo", rpc_params![]) + .await?) + } + + async fn segment_headers( + &self, + segment_indices: Vec, + ) -> anyhow::Result>> { + Ok(self + .client + .request("subspace_segmentHeaders", rpc_params![&segment_indices]) + .await?) + } +}