diff --git a/Cargo.lock b/Cargo.lock index 35e3ade81c..79d7a6e679 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3084,11 +3084,14 @@ dependencies = [ "kaspa-p2p-lib", "kaspa-perf-monitor", "kaspa-rpc-core", + "kaspa-rpc-macros", "kaspa-txscript", "kaspa-utils", "kaspa-utils-tower", "kaspa-utxoindex", "log", + "serde", + "thiserror", "tokio", "triggered", "workflow-rpc", diff --git a/kaspad/src/args.rs b/kaspad/src/args.rs index 56dd7c1de7..4c21335be0 100644 --- a/kaspad/src/args.rs +++ b/kaspad/src/args.rs @@ -5,6 +5,7 @@ use kaspa_consensus_core::{ }; use kaspa_core::kaspad_env::version; use kaspa_notify::address::tracker::Tracker; +use kaspa_rpc_core::api::namespaces::Namespaces; use kaspa_utils::networking::ContextualNetAddress; use kaspa_wrpc_server::address::WrpcNetAddress; use serde::Deserialize; @@ -90,6 +91,8 @@ pub struct Args { #[serde(rename = "nogrpc")] pub disable_grpc: bool, pub ram_scale: f64, + #[serde(rename = "rpc-api")] + pub rpc_namespaces: Option<Namespaces>, } impl Default for Args { @@ -140,6 +143,7 @@ impl Default for Args { disable_dns_seeding: false, disable_grpc: false, ram_scale: 1.0, + rpc_namespaces: None, } } } @@ -369,7 +373,15 @@ Setting to 0 prevents the preallocation and sets the maximum to {}, leading to 0 .help("Apply a scale factor to memory allocation bounds. Nodes with limited RAM (~4-8GB) should set this to ~0.3-0.5 respectively. Nodes with a large RAM (~64GB) can set this value to ~3.0-4.0 and gain superior performance especially for syncing peers faster"), ) - ; + .arg( + Arg::new("rpc-api") + .long("rpc-api") + .value_name("namespaces") + .require_equals(true) + .default_missing_value(None) + .value_parser(clap::value_parser!(Namespaces)) + .help("Specify allowed RPC namespaces exposed over RPC servers."), + ); #[cfg(feature = "devnet-prealloc")] let cmd = cmd @@ -448,6 +460,7 @@ impl Args { disable_dns_seeding: arg_match_unwrap_or::<bool>(&m, "nodnsseed", defaults.disable_dns_seeding), disable_grpc: arg_match_unwrap_or::<bool>(&m, "nogrpc", defaults.disable_grpc), ram_scale: arg_match_unwrap_or::<f64>(&m, "ram-scale", defaults.ram_scale), + rpc_namespaces: m.get_one::<Namespaces>("rpc-api").cloned().or(defaults.rpc_namespaces), #[cfg(feature = "devnet-prealloc")] num_prealloc_utxos: m.get_one::<u64>("num-prealloc-utxos").cloned(), @@ -560,4 +573,6 @@ fn arg_match_many_unwrap_or<T: Clone + Send + Sync + 'static>(m: &clap::ArgMatch --override-dag-params-file= Overrides DAG params (allowed only on devnet) -s, --service= Service command {install, remove, start, stop} --nogrpc Don't initialize the gRPC server + --rpc-api= Set available namespaces over RPC server(s). + (By default all namespaces are enabled) */ diff --git a/kaspad/src/daemon.rs b/kaspad/src/daemon.rs index 0a1e7943ef..a220e95ce7 100644 --- a/kaspad/src/daemon.rs +++ b/kaspad/src/daemon.rs @@ -579,6 +579,7 @@ do you confirm? (answer y/n or pass --yes to the Kaspad command line to confirm p2p_tower_counters.clone(), grpc_tower_counters.clone(), system_info, + args.rpc_namespaces.clone(), )); let grpc_service_broadcasters: usize = 3; // TODO: add a command line argument or derive from other arg/config/host-related fields let grpc_service = if !args.disable_grpc { diff --git a/rpc/core/src/api/Extending RpcApi.md b/rpc/core/src/api/Extending RpcApi.md index 0940d78227..892eee77b9 100644 --- a/rpc/core/src/api/Extending RpcApi.md +++ b/rpc/core/src/api/Extending RpcApi.md @@ -23,7 +23,7 @@ As an illustration, let's pretend that we add a new `submit_block` method. Implement the first as a call to the second. (ie. `async fn submit_block(&self, block: RpcBlock, allow_non_daa_blocks: bool) -> RpcResult<SubmitBlockResponse>` and `async fn submit_block_call(&self, request: SubmitBlockRequest) -> RpcResult<SubmitBlockResponse>;`) -6. Implement the function having a `_call` suffix into `kaspa_rpc_core::server::service::RpcCoreService`. +6. Implement the function having a `_call` suffix into `kaspa_rpc_core::server::service::RpcCoreService` and wrap it with namespace macro with its corresponding category. ## rpc-grpc diff --git a/rpc/core/src/api/mod.rs b/rpc/core/src/api/mod.rs index a75056a841..68a2f1145b 100644 --- a/rpc/core/src/api/mod.rs +++ b/rpc/core/src/api/mod.rs @@ -4,6 +4,7 @@ pub mod connection; pub mod ctl; +pub mod namespaces; pub mod notifications; pub mod ops; pub mod rpc; diff --git a/rpc/core/src/api/namespaces.rs b/rpc/core/src/api/namespaces.rs new file mode 100644 index 0000000000..33e240f6b3 --- /dev/null +++ b/rpc/core/src/api/namespaces.rs @@ -0,0 +1,115 @@ +use kaspa_notify::scope::Scope; +use serde::Deserialize; +use std::collections::HashSet; +use std::str::FromStr; +use thiserror::Error; + +/// Enum representing available namespace groups +#[derive(Debug, Hash, Eq, PartialEq, Clone, Deserialize)] +pub enum Namespace { + General, + Networking, + DAG, + Mining, + Wallet, + Metrics, + Mempool, +} + +#[derive(Debug, Error)] +pub enum NamespaceError { + #[error("Invalid namespace value: {0}")] + InvalidValue(String), +} + +impl FromStr for Namespace { + type Err = NamespaceError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "General" => Ok(Namespace::General), + "Networking" => Ok(Namespace::Networking), + "DAG" => Ok(Namespace::DAG), + "Mining" => Ok(Namespace::Mining), + "Wallet" => Ok(Namespace::Wallet), + "Metrics" => Ok(Namespace::Metrics), + "Mempool" => Ok(Namespace::Mempool), + _ => Err(NamespaceError::InvalidValue(s.to_string())), + } + } +} + +impl std::fmt::Display for Namespace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Namespace::General => write!(f, "General"), + Namespace::Networking => write!(f, "Networking"), + Namespace::DAG => write!(f, "DAG"), + Namespace::Mining => write!(f, "Mining"), + Namespace::Wallet => write!(f, "Wallet"), + Namespace::Metrics => write!(f, "Metrics"), + Namespace::Mempool => write!(f, "Mempool"), + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Namespaces { + enabled: HashSet<Namespace>, +} + +impl Namespaces { + /// Check if a namespace is enabled + pub fn is_enabled(&self, namespace: &Namespace) -> bool { + self.enabled.contains(namespace) + } + + // Determine the namespace associated with a given subscription scope + pub fn get_scope_namespace(&self, scope: &Scope) -> Namespace { + match scope { + Scope::BlockAdded(_) => Namespace::DAG, + Scope::VirtualChainChanged(_) => Namespace::DAG, + Scope::FinalityConflict(_) => Namespace::DAG, + Scope::FinalityConflictResolved(_) => Namespace::DAG, + Scope::UtxosChanged(_) => Namespace::Wallet, + Scope::SinkBlueScoreChanged(_) => Namespace::DAG, + Scope::VirtualDaaScoreChanged(_) => Namespace::DAG, + Scope::PruningPointUtxoSetOverride(_) => Namespace::DAG, + Scope::NewBlockTemplate(_) => Namespace::Mining, + } + } + + /// Return enabled namespaces as string for get_info + pub fn enabled_namespaces(&self) -> Vec<String> { + self.enabled.iter().map(|namespace| namespace.to_string()).collect::<Vec<_>>() + } +} + +impl FromStr for Namespaces { + type Err = NamespaceError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let enabled = s + .split(',') + .map(str::trim) // To support case like "DAG, Metrics" + .map(|name| name.parse::<Namespace>()) + .collect::<Result<HashSet<_>, _>>()?; + Ok(Namespaces { enabled }) + } +} + +impl Default for Namespaces { + fn default() -> Self { + Self { + enabled: HashSet::from([ + Namespace::General, + Namespace::Networking, + Namespace::DAG, + Namespace::Mining, + Namespace::Wallet, + Namespace::Metrics, + Namespace::Mempool, + ]), + } + } +} diff --git a/rpc/core/src/error.rs b/rpc/core/src/error.rs index 54763c71ba..fec4cfb6e2 100644 --- a/rpc/core/src/error.rs +++ b/rpc/core/src/error.rs @@ -60,6 +60,9 @@ pub enum RpcError { #[error("Transaction {0} not found")] TransactionNotFound(TransactionId), + #[error("Method unavailable. {0} namespace is not available.")] + UnauthorizedMethod(String), + #[error("Method unavailable. Run the node with the --utxoindex argument.")] NoUtxoIndex, diff --git a/rpc/core/src/model/message.rs b/rpc/core/src/model/message.rs index cb663c394a..b03e1c2167 100644 --- a/rpc/core/src/model/message.rs +++ b/rpc/core/src/model/message.rs @@ -269,6 +269,7 @@ pub struct GetInfoResponse { pub server_version: String, pub is_utxo_indexed: bool, pub is_synced: bool, + pub namespaces: Vec<String>, pub has_notify_command: bool, pub has_message_id: bool, } @@ -283,6 +284,7 @@ impl Serializer for GetInfoResponse { store!(bool, &self.is_synced, writer)?; store!(bool, &self.has_notify_command, writer)?; store!(bool, &self.has_message_id, writer)?; + store!(Vec<String>, &self.namespaces, writer)?; Ok(()) } @@ -298,8 +300,9 @@ impl Deserializer for GetInfoResponse { let is_synced = load!(bool, reader)?; let has_notify_command = load!(bool, reader)?; let has_message_id = load!(bool, reader)?; + let namespaces = load!(Vec<String>, reader)?; - Ok(Self { p2p_id, mempool_size, server_version, is_utxo_indexed, is_synced, has_notify_command, has_message_id }) + Ok(Self { p2p_id, mempool_size, server_version, is_utxo_indexed, is_synced, namespaces, has_notify_command, has_message_id }) } } diff --git a/rpc/core/src/model/tests.rs b/rpc/core/src/model/tests.rs index d931f5ac23..17d40c8318 100644 --- a/rpc/core/src/model/tests.rs +++ b/rpc/core/src/model/tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod mockery { - use crate::{model::*, RpcScriptClass}; + use crate::{api::namespaces::Namespaces, model::*, RpcScriptClass}; use kaspa_addresses::{Prefix, Version}; use kaspa_consensus_core::api::BlockCount; use kaspa_consensus_core::network::NetworkType; @@ -486,6 +486,7 @@ mod mockery { server_version: "0.4.2".to_string(), is_utxo_indexed: true, is_synced: false, + namespaces: Namespaces::default().enabled_namespaces(), has_notify_command: true, has_message_id: false, } diff --git a/rpc/core/src/wasm/message.rs b/rpc/core/src/wasm/message.rs index 85c0857023..01bf83dcf4 100644 --- a/rpc/core/src/wasm/message.rs +++ b/rpc/core/src/wasm/message.rs @@ -243,6 +243,7 @@ declare! { serverVersion : string; isUtxoIndexed : boolean; isSynced : boolean; + namespaces : string[]; /** GRPC ONLY */ hasNotifyCommand : boolean; /** GRPC ONLY */ diff --git a/rpc/grpc/core/proto/rpc.proto b/rpc/grpc/core/proto/rpc.proto index 4c36150ad3..0a523a1c94 100644 --- a/rpc/grpc/core/proto/rpc.proto +++ b/rpc/grpc/core/proto/rpc.proto @@ -702,6 +702,7 @@ message GetInfoResponseMessage{ string serverVersion = 3; bool isUtxoIndexed = 4; bool isSynced = 5; + repeated string namespaces = 6; bool hasNotifyCommand = 11; bool hasMessageId = 12; RPCError error = 1000; diff --git a/rpc/grpc/core/src/convert/message.rs b/rpc/grpc/core/src/convert/message.rs index c92e824ed0..68ff6a1013 100644 --- a/rpc/grpc/core/src/convert/message.rs +++ b/rpc/grpc/core/src/convert/message.rs @@ -183,6 +183,7 @@ from!(item: RpcResult<&kaspa_rpc_core::GetInfoResponse>, protowire::GetInfoRespo server_version: item.server_version.clone(), is_utxo_indexed: item.is_utxo_indexed, is_synced: item.is_synced, + namespaces: item.namespaces.clone(), has_notify_command: item.has_notify_command, has_message_id: item.has_message_id, error: None, @@ -648,6 +649,7 @@ try_from!(item: &protowire::GetInfoResponseMessage, RpcResult<kaspa_rpc_core::Ge server_version: item.server_version.clone(), is_utxo_indexed: item.is_utxo_indexed, is_synced: item.is_synced, + namespaces: item.namespaces.clone(), has_notify_command: item.has_notify_command, has_message_id: item.has_message_id, } diff --git a/rpc/grpc/server/src/tests/rpc_core_mock.rs b/rpc/grpc/server/src/tests/rpc_core_mock.rs index ada801850e..bdae6295af 100644 --- a/rpc/grpc/server/src/tests/rpc_core_mock.rs +++ b/rpc/grpc/server/src/tests/rpc_core_mock.rs @@ -1,3 +1,4 @@ +use api::namespaces::Namespaces; use async_channel::{unbounded, Receiver}; use async_trait::async_trait; use kaspa_notify::events::EVENT_TYPE_ARRAY; @@ -73,6 +74,7 @@ impl RpcApi for RpcCoreMock { server_version: "mock".to_string(), is_utxo_indexed: false, is_synced: false, + namespaces: Namespaces::default().enabled_namespaces(), has_notify_command: true, has_message_id: true, }) diff --git a/rpc/macros/src/core/mod.rs b/rpc/macros/src/core/mod.rs new file mode 100644 index 0000000000..1f278a4d51 --- /dev/null +++ b/rpc/macros/src/core/mod.rs @@ -0,0 +1 @@ +pub mod service; diff --git a/rpc/macros/src/core/service.rs b/rpc/macros/src/core/service.rs new file mode 100644 index 0000000000..ed34670e67 --- /dev/null +++ b/rpc/macros/src/core/service.rs @@ -0,0 +1,19 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, ItemFn, Path}; + +pub fn namespace(attr: TokenStream, item: TokenStream) -> TokenStream { + let api_namespace = parse_macro_input!(attr as Path); + let mut func = parse_macro_input!(item as ItemFn); + + let check = syn::parse2(quote! { + if !self.namespaces.is_enabled(&#api_namespace) { + // As macro processing happens after async_trait processing its wrapped with async_trait return type + return std::boxed::Box::pin(std::future::ready(Err(RpcError::UnauthorizedMethod(#api_namespace.to_string())))); + } + }) + .unwrap(); + + func.block.stmts.insert(0, check); + quote!(#func).into() +} diff --git a/rpc/macros/src/lib.rs b/rpc/macros/src/lib.rs index 9ca49bf54a..c5c3ac95ff 100644 --- a/rpc/macros/src/lib.rs +++ b/rpc/macros/src/lib.rs @@ -1,5 +1,6 @@ use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; +mod core; mod grpc; mod handler; mod wrpc; @@ -45,3 +46,8 @@ pub fn build_grpc_server_interface(input: TokenStream) -> TokenStream { pub fn test_wrpc_serializer(input: TokenStream) -> TokenStream { wrpc::test::build_test(input) } + +#[proc_macro_attribute] +pub fn namespace(attr: TokenStream, item: TokenStream) -> TokenStream { + core::service::namespace(attr, item) +} diff --git a/rpc/service/Cargo.toml b/rpc/service/Cargo.toml index 54e9764088..d57e7cae8c 100644 --- a/rpc/service/Cargo.toml +++ b/rpc/service/Cargo.toml @@ -24,6 +24,7 @@ kaspa-p2p-flows.workspace = true kaspa-p2p-lib.workspace = true kaspa-perf-monitor.workspace = true kaspa-rpc-core.workspace = true +kaspa-rpc-macros.workspace = true kaspa-txscript.workspace = true kaspa-utils.workspace = true kaspa-utils-tower.workspace = true @@ -33,4 +34,6 @@ async-trait.workspace = true log.workspace = true tokio.workspace = true triggered.workspace = true -workflow-rpc.workspace = true \ No newline at end of file +workflow-rpc.workspace = true +thiserror.workspace = true +serde.workspace = true \ No newline at end of file diff --git a/rpc/service/src/service.rs b/rpc/service/src/service.rs index c5bae27a68..2012e2ec81 100644 --- a/rpc/service/src/service.rs +++ b/rpc/service/src/service.rs @@ -57,6 +57,7 @@ use kaspa_perf_monitor::{counters::CountersSnapshot, Monitor as PerfMonitor}; use kaspa_rpc_core::{ api::{ connection::DynRpcConnection, + namespaces::{Namespace, Namespaces}, ops::{RPC_API_REVISION, RPC_API_VERSION}, rpc::{RpcApi, MAX_SAFE_WINDOW_SIZE}, }, @@ -64,6 +65,7 @@ use kaspa_rpc_core::{ notify::connection::ChannelConnection, Notification, RpcError, RpcResult, }; +use kaspa_rpc_macros::namespace; use kaspa_txscript::{extract_script_pub_key_address, pay_to_address_script}; use kaspa_utils::expiring_cache::ExpiringCache; use kaspa_utils::sysinfo::SystemInfo; @@ -119,6 +121,7 @@ pub struct RpcCoreService { system_info: SystemInfo, fee_estimate_cache: ExpiringCache<RpcFeeEstimate>, fee_estimate_verbose_cache: ExpiringCache<kaspa_mining::errors::MiningManagerResult<GetFeeEstimateExperimentalResponse>>, + namespaces: Namespaces, } const RPC_CORE: &str = "rpc-core"; @@ -144,6 +147,7 @@ impl RpcCoreService { p2p_tower_counters: Arc<TowerConnectionCounters>, grpc_tower_counters: Arc<TowerConnectionCounters>, system_info: SystemInfo, + namespaces: Option<Namespaces>, ) -> Self { // This notifier UTXOs subscription granularity to index-processor or consensus notifier let policies = match index_notifier { @@ -196,7 +200,7 @@ impl RpcCoreService { // Protocol converter let protocol_converter = Arc::new(ProtocolConverter::new(flow_context.clone())); - // Create the rcp-core notifier + // Create the rpc-core notifier let notifier = Arc::new(Notifier::new(RPC_CORE, EVENT_TYPE_ARRAY[..].into(), collectors, subscribers, subscription_context, 1, policies)); @@ -222,6 +226,7 @@ impl RpcCoreService { system_info, fee_estimate_cache: ExpiringCache::new(Duration::from_millis(500), Duration::from_millis(1000)), fee_estimate_verbose_cache: ExpiringCache::new(Duration::from_millis(500), Duration::from_millis(1000)), + namespaces: namespaces.unwrap_or_default(), } } @@ -289,6 +294,7 @@ impl RpcCoreService { #[async_trait] impl RpcApi for RpcCoreService { + #[namespace(Namespace::Mining)] async fn submit_block_call( &self, _connection: Option<&DynRpcConnection>, @@ -357,6 +363,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and } } + #[namespace(Namespace::Mining)] async fn get_block_template_call( &self, _connection: Option<&DynRpcConnection>, @@ -393,6 +400,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } + #[namespace(Namespace::Mining)] async fn get_current_block_color_call( &self, _connection: Option<&DynRpcConnection>, @@ -406,6 +414,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and } } + #[namespace(Namespace::DAG)] async fn get_block_call(&self, _connection: Option<&DynRpcConnection>, request: GetBlockRequest) -> RpcResult<GetBlockResponse> { // TODO: test let session = self.consensus_manager.consensus().session().await; @@ -418,6 +427,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } + #[namespace(Namespace::DAG)] async fn get_blocks_call( &self, _connection: Option<&DynRpcConnection>, @@ -471,6 +481,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetBlocksResponse { block_hashes, blocks }) } + #[namespace(Namespace::General)] async fn get_info_call(&self, _connection: Option<&DynRpcConnection>, _request: GetInfoRequest) -> RpcResult<GetInfoResponse> { let is_nearly_synced = self.consensus_manager.consensus().unguarded_session().async_is_nearly_synced().await; Ok(GetInfoResponse { @@ -479,11 +490,13 @@ NOTE: This error usually indicates an RPC conversion error between the node and server_version: version().to_string(), is_utxo_indexed: self.config.utxoindex, is_synced: self.has_sufficient_peer_connectivity() && is_nearly_synced, + namespaces: self.namespaces.enabled_namespaces(), has_notify_command: true, has_message_id: true, }) } + #[namespace(Namespace::Mempool)] async fn get_mempool_entry_call( &self, _connection: Option<&DynRpcConnection>, @@ -497,6 +510,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetMempoolEntryResponse::new(self.consensus_converter.get_mempool_entry(&session, &transaction))) } + #[namespace(Namespace::Mempool)] async fn get_mempool_entries_call( &self, _connection: Option<&DynRpcConnection>, @@ -513,6 +527,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetMempoolEntriesResponse::new(mempool_entries)) } + #[namespace(Namespace::Mempool)] async fn get_mempool_entries_by_addresses_call( &self, _connection: Option<&DynRpcConnection>, @@ -539,6 +554,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetMempoolEntriesByAddressesResponse::new(mempool_entries)) } + #[namespace(Namespace::Wallet)] async fn submit_transaction_call( &self, _connection: Option<&DynRpcConnection>, @@ -564,6 +580,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(SubmitTransactionResponse::new(transaction_id)) } + #[namespace(Namespace::Wallet)] async fn submit_transaction_replacement_call( &self, _connection: Option<&DynRpcConnection>, @@ -581,6 +598,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(SubmitTransactionReplacementResponse::new(transaction_id, (&*replaced_transaction).into())) } + #[namespace(Namespace::General)] async fn get_current_network_call( &self, _connection: Option<&DynRpcConnection>, @@ -589,6 +607,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetCurrentNetworkResponse::new(*self.config.net)) } + #[namespace(Namespace::Networking)] async fn get_subnetwork_call( &self, _connection: Option<&DynRpcConnection>, @@ -597,10 +616,12 @@ NOTE: This error usually indicates an RPC conversion error between the node and Err(RpcError::NotImplemented) } + #[namespace(Namespace::DAG)] async fn get_sink_call(&self, _connection: Option<&DynRpcConnection>, _: GetSinkRequest) -> RpcResult<GetSinkResponse> { Ok(GetSinkResponse::new(self.consensus_manager.consensus().unguarded_session().async_get_sink().await)) } + #[namespace(Namespace::DAG)] async fn get_sink_blue_score_call( &self, _connection: Option<&DynRpcConnection>, @@ -610,6 +631,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetSinkBlueScoreResponse::new(session.async_get_ghostdag_data(session.async_get_sink().await).await?.blue_score)) } + #[namespace(Namespace::DAG)] async fn get_virtual_chain_from_block_call( &self, _connection: Option<&DynRpcConnection>, @@ -638,6 +660,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetVirtualChainFromBlockResponse::new(virtual_chain_batch.removed, virtual_chain_batch.added, accepted_transaction_ids)) } + #[namespace(Namespace::DAG)] async fn get_block_count_call( &self, _connection: Option<&DynRpcConnection>, @@ -646,6 +669,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(self.consensus_manager.consensus().unguarded_session().async_estimate_block_count().await) } + #[namespace(Namespace::Wallet)] async fn get_utxos_by_addresses_call( &self, _connection: Option<&DynRpcConnection>, @@ -660,6 +684,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetUtxosByAddressesResponse::new(self.index_converter.get_utxos_by_addresses_entries(&entry_map))) } + #[namespace(Namespace::Wallet)] async fn get_balance_by_address_call( &self, _connection: Option<&DynRpcConnection>, @@ -673,6 +698,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetBalanceByAddressResponse::new(balance)) } + #[namespace(Namespace::Wallet)] async fn get_balances_by_addresses_call( &self, _connection: Option<&DynRpcConnection>, @@ -694,6 +720,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetBalancesByAddressesResponse::new(entries)) } + #[namespace(Namespace::DAG)] async fn get_coin_supply_call( &self, _connection: Option<&DynRpcConnection>, @@ -707,6 +734,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetCoinSupplyResponse::new(MAX_SOMPI, circulating_sompi)) } + #[namespace(Namespace::Wallet)] // TODO: think again async fn get_daa_score_timestamp_estimate_call( &self, _connection: Option<&DynRpcConnection>, @@ -774,6 +802,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetDaaScoreTimestampEstimateResponse::new(timestamps)) } + #[namespace(Namespace::Wallet)] async fn get_fee_estimate_call( &self, _connection: Option<&DynRpcConnection>, @@ -793,6 +822,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetFeeEstimateResponse { estimate }) } + #[namespace(Namespace::Wallet)] async fn get_fee_estimate_experimental_call( &self, connection: Option<&DynRpcConnection>, @@ -817,6 +847,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and } } + #[namespace(Namespace::Wallet)] async fn get_utxo_return_address_call( &self, _connection: Option<&DynRpcConnection>, @@ -844,10 +875,12 @@ NOTE: This error usually indicates an RPC conversion error between the node and } } + #[namespace(Namespace::General)] async fn ping_call(&self, _connection: Option<&DynRpcConnection>, _: PingRequest) -> RpcResult<PingResponse> { Ok(PingResponse {}) } + #[namespace(Namespace::DAG)] async fn get_headers_call( &self, _connection: Option<&DynRpcConnection>, @@ -856,6 +889,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Err(RpcError::NotImplemented) } + #[namespace(Namespace::DAG)] async fn get_block_dag_info_call( &self, _connection: Option<&DynRpcConnection>, @@ -878,6 +912,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and )) } + #[namespace(Namespace::DAG)] async fn estimate_network_hashes_per_second_call( &self, _connection: Option<&DynRpcConnection>, @@ -910,6 +945,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and )) } + #[namespace(Namespace::Networking)] async fn add_peer_call(&self, _connection: Option<&DynRpcConnection>, request: AddPeerRequest) -> RpcResult<AddPeerResponse> { if !self.config.unsafe_rpc { warn!("AddPeer RPC command called while node in safe RPC mode -- ignoring."); @@ -924,6 +960,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(AddPeerResponse {}) } + #[namespace(Namespace::Networking)] async fn get_peer_addresses_call( &self, _connection: Option<&DynRpcConnection>, @@ -933,6 +970,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetPeerAddressesResponse::new(address_manager.get_all_addresses(), address_manager.get_all_banned_addresses())) } + #[namespace(Namespace::Networking)] async fn ban_call(&self, _connection: Option<&DynRpcConnection>, request: BanRequest) -> RpcResult<BanResponse> { if !self.config.unsafe_rpc { warn!("Ban RPC command called while node in safe RPC mode -- ignoring."); @@ -950,6 +988,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(BanResponse {}) } + #[namespace(Namespace::Networking)] async fn unban_call(&self, _connection: Option<&DynRpcConnection>, request: UnbanRequest) -> RpcResult<UnbanResponse> { if !self.config.unsafe_rpc { warn!("Unban RPC command called while node in safe RPC mode -- ignoring."); @@ -964,6 +1003,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(UnbanResponse {}) } + #[namespace(Namespace::Networking)] async fn get_connected_peer_info_call( &self, _connection: Option<&DynRpcConnection>, @@ -974,6 +1014,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetConnectedPeerInfoResponse::new(peer_info)) } + #[namespace(Namespace::General)] async fn shutdown_call(&self, _connection: Option<&DynRpcConnection>, _: ShutdownRequest) -> RpcResult<ShutdownResponse> { if !self.config.unsafe_rpc { warn!("Shutdown RPC command called while node in safe RPC mode -- ignoring."); @@ -995,6 +1036,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(ShutdownResponse {}) } + #[namespace(Namespace::DAG)] async fn resolve_finality_conflict_call( &self, _connection: Option<&DynRpcConnection>, @@ -1007,6 +1049,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Err(RpcError::NotImplemented) } + #[namespace(Namespace::Networking)] async fn get_connections_call( &self, _connection: Option<&DynRpcConnection>, @@ -1025,6 +1068,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetConnectionsResponse { clients, peers, profile_data }) } + #[namespace(Namespace::Metrics)] async fn get_metrics_call(&self, _connection: Option<&DynRpcConnection>, req: GetMetricsRequest) -> RpcResult<GetMetricsResponse> { let CountersSnapshot { resident_set_size, @@ -1118,6 +1162,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(response) } + #[namespace(Namespace::Metrics)] async fn get_system_info_call( &self, _connection: Option<&DynRpcConnection>, @@ -1136,6 +1181,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(response) } + #[namespace(Namespace::General)] async fn get_server_info_call( &self, _connection: Option<&DynRpcConnection>, @@ -1156,6 +1202,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } + #[namespace(Namespace::DAG)] async fn get_sync_status_call( &self, _connection: Option<&DynRpcConnection>, @@ -1184,6 +1231,11 @@ NOTE: This error usually indicates an RPC conversion error between the node and /// Start sending notifications of some type to a listener. async fn start_notify(&self, id: ListenerId, scope: Scope) -> RpcResult<()> { + let namespace = self.namespaces.get_scope_namespace(&scope); + if !self.namespaces.is_enabled(&namespace) { + return Err(RpcError::UnauthorizedMethod(namespace.to_string())); + } + match scope { Scope::UtxosChanged(ref utxos_changed_scope) if !self.config.unsafe_rpc && utxos_changed_scope.addresses.is_empty() => { // The subscription to blanket UtxosChanged notifications is restricted to unsafe mode only diff --git a/testing/integration/src/rpc_tests.rs b/testing/integration/src/rpc_tests.rs index 3ca619423a..08bd6fd694 100644 --- a/testing/integration/src/rpc_tests.rs +++ b/testing/integration/src/rpc_tests.rs @@ -15,7 +15,14 @@ use kaspa_notify::{ SinkBlueScoreChangedScope, UtxosChangedScope, VirtualChainChangedScope, VirtualDaaScoreChangedScope, }, }; -use kaspa_rpc_core::{api::rpc::RpcApi, model::*, Notification}; +use kaspa_rpc_core::{ + api::{ + namespaces::{Namespace, Namespaces}, + rpc::RpcApi, + }, + model::*, + Notification, +}; use kaspa_utils::{fd_budget, networking::ContextualNetAddress}; use kaspad_lib::args::Args; use tokio::task::JoinHandle; @@ -231,6 +238,7 @@ async fn sanity_test() { assert!(response.is_utxo_indexed); assert!(response.has_message_id); assert!(response.has_notify_command); + assert!(Namespaces::from_str(&response.namespaces.join(",")).unwrap().is_enabled(&Namespace::General)) }) } diff --git a/wallet/core/src/tests/rpc_core_mock.rs b/wallet/core/src/tests/rpc_core_mock.rs index 529fffffe8..d558970a5f 100644 --- a/wallet/core/src/tests/rpc_core_mock.rs +++ b/wallet/core/src/tests/rpc_core_mock.rs @@ -9,7 +9,7 @@ use kaspa_notify::scope::Scope; use kaspa_notify::subscription::context::SubscriptionContext; use kaspa_notify::subscription::{MutationPolicies, UtxosChangedMutationPolicy}; use kaspa_rpc_core::api::ctl::RpcCtl; -use kaspa_rpc_core::{api::connection::DynRpcConnection, api::rpc::RpcApi, *}; +use kaspa_rpc_core::{api::connection::DynRpcConnection, api::namespaces::Namespaces, api::rpc::RpcApi, *}; use kaspa_rpc_core::{notify::connection::ChannelConnection, RpcResult}; use std::sync::Arc; @@ -92,6 +92,7 @@ impl RpcApi for RpcCoreMock { is_synced: false, has_notify_command: false, has_message_id: false, + namespaces: Namespaces::default().enabled_namespaces(), }) }