smoldot_light/json_rpc_service.rs
1// Smoldot
2// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
3// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
4
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18//! Background JSON-RPC service.
19//!
20//! # Usage
21//!
22//! Create a new JSON-RPC service by calling [`service()`].
23//! Creating a JSON-RPC service spawns a background task (through [`PlatformRef::spawn_task`])
24//! dedicated to processing JSON-RPC requests.
25//!
26//! In order to process a JSON-RPC request, call [`Frontend::queue_rpc_request`]. Later, the
27//! JSON-RPC service can queue a response or, in the case of subscriptions, a notification. They
28//! can be retrieved by calling [`Frontend::next_json_rpc_response`].
29//!
30//! In the situation where an attacker finds a JSON-RPC request that takes a long time to be
31//! processed and continuously submits this same expensive request over and over again, the queue
32//! of pending requests will start growing and use more and more memory. For this reason, if this
33//! queue grows past [`Config::max_pending_requests`] items, [`Frontend::queue_rpc_request`]
34//! will instead return an error.
35//!
36
37// TODO: doc
38// TODO: re-review this once finished
39
40mod background;
41mod statement;
42
43use crate::{
44 bitswap_service, log, network_service, platform::PlatformRef, runtime_service, sync_service,
45 transactions_service,
46};
47
48use alloc::{
49 borrow::Cow,
50 boxed::Box,
51 format,
52 string::{String, ToString as _},
53 sync::Arc,
54};
55use core::{num::NonZero, pin::Pin};
56use futures_lite::StreamExt as _;
57
58pub use statement::StatementProtocolConfig;
59
60/// Configuration for [`service()`].
61pub struct Config<TPlat: PlatformRef> {
62 /// Access to the platform's capabilities.
63 pub platform: TPlat,
64
65 /// Name of the chain, for logging purposes.
66 ///
67 /// > **Note**: This name will be directly printed out. Any special character should already
68 /// > have been filtered out from this name.
69 pub log_name: String,
70
71 /// Maximum number of JSON-RPC requests that can be added to a queue if it is not ready to be
72 /// processed immediately. Any additional request will be immediately rejected.
73 ///
74 /// This parameter is necessary in order to prevent users from using up too much memory within
75 /// the client.
76 // TODO: unused at the moment
77 #[allow(unused)]
78 pub max_pending_requests: NonZero<u32>,
79
80 /// Maximum number of active subscriptions. Any additional subscription will be immediately
81 /// rejected.
82 ///
83 /// This parameter is necessary in order to prevent users from using up too much memory within
84 /// the client.
85 // TODO: unused at the moment
86 #[allow(unused)]
87 pub max_subscriptions: u32,
88
89 /// Access to the network, and identifier of the chain from the point of view of the network
90 /// service.
91 pub network_service: Arc<network_service::NetworkServiceChain<TPlat>>,
92
93 /// Service responsible for synchronizing the chain.
94 pub sync_service: Arc<sync_service::SyncService<TPlat>>,
95
96 /// Service responsible for emitting transactions and tracking their state.
97 pub transactions_service: Arc<transactions_service::TransactionsService<TPlat>>,
98
99 /// Service that provides a ready-to-be-called runtime for the current best block.
100 pub runtime_service: Arc<runtime_service::RuntimeService<TPlat>>,
101
102 /// Service that fulfills IPFS CID requests.
103 pub bitswap_service: Arc<bitswap_service::BitswapService>,
104
105 /// Metrics of the chain, returned by `sudo_unstable_metrics`.
106 pub chain_metrics: Arc<crate::metrics::ChainMetrics>,
107
108 /// Process-wide network metrics, returned by `sudo_unstable_metrics`.
109 pub network_metrics: Arc<crate::metrics::NetworkMetrics>,
110
111 /// Lifecycle state of the chain, served by `lifecycle_unstable_follow`.
112 pub lifecycle_service: Arc<crate::lifecycle_service::LifecycleService>,
113
114 /// Name of the chain, as found in the chain specification.
115 pub chain_name: String,
116 /// Type of chain, as found in the chain specification.
117 pub chain_ty: String,
118 /// JSON-encoded properties of the chain, as found in the chain specification.
119 pub chain_properties_json: String,
120 /// Whether the chain is a live network. Found in the chain specification.
121 pub chain_is_live: bool,
122
123 /// Value to return when the `system_name` RPC is called. Should be set to the name of the
124 /// final executable.
125 pub system_name: String,
126
127 /// Value to return when the `system_version` RPC is called. Should be set to the version of
128 /// the final executable.
129 pub system_version: String,
130
131 /// Hash of the genesis block of the chain.
132 pub genesis_block_hash: [u8; 32],
133
134 /// Statement protocol configuration. `None` if the statement protocol is disabled.
135 pub statement_protocol_config: Option<StatementProtocolConfig>,
136}
137
138/// Creates a new JSON-RPC service with the given configuration.
139///
140/// Returns a handler that allows sending requests and receiving responses.
141///
142/// Destroying the [`Frontend`] automatically shuts down the service.
143pub fn service<TPlat: PlatformRef>(config: Config<TPlat>) -> Frontend<TPlat> {
144 let log_target = format!("json-rpc-{}", config.log_name);
145
146 let (requests_tx, requests_rx) = async_channel::unbounded(); // TODO: capacity?
147 let (responses_tx, responses_rx) = async_channel::bounded(16); // TODO: capacity?
148
149 let frontend = Frontend {
150 platform: config.platform.clone(),
151 log_target: log_target.clone(),
152 responses_rx: Arc::new(async_lock::Mutex::new(Box::pin(responses_rx))),
153 requests_tx,
154 metrics: config.chain_metrics.clone(),
155 };
156
157 let platform = config.platform.clone();
158 platform.spawn_task(
159 Cow::Owned(log_target.clone()),
160 background::run(
161 log_target,
162 background::Config {
163 platform: config.platform,
164 network_service: config.network_service,
165 sync_service: config.sync_service,
166 transactions_service: config.transactions_service,
167 runtime_service: config.runtime_service,
168 bitswap_service: config.bitswap_service,
169 chain_metrics: config.chain_metrics,
170 network_metrics: config.network_metrics,
171 lifecycle_service: config.lifecycle_service,
172 chain_name: config.chain_name,
173 chain_ty: config.chain_ty,
174 chain_properties_json: config.chain_properties_json,
175 chain_is_live: config.chain_is_live,
176 system_name: config.system_name,
177 system_version: config.system_version,
178 genesis_block_hash: config.genesis_block_hash,
179 statement_protocol_config: config.statement_protocol_config,
180 },
181 requests_rx,
182 responses_tx,
183 ),
184 );
185
186 frontend
187}
188
189/// Handle that allows sending JSON-RPC requests on the service.
190///
191/// The [`Frontend`] can be cloned, in which case the clone will refer to the same JSON-RPC
192/// service.
193///
194/// Destroying all the [`Frontend`]s automatically shuts down the associated service.
195#[derive(Clone)]
196pub struct Frontend<TPlat> {
197 /// See [`Config::platform`].
198 platform: TPlat,
199
200 /// How to send requests to the background task.
201 requests_tx: async_channel::Sender<String>,
202
203 /// How to receive responses coming from the background task.
204 // TODO: we use an Arc so that it's clonable, but that's questionnable
205 responses_rx: Arc<async_lock::Mutex<Pin<Box<async_channel::Receiver<String>>>>>,
206
207 /// Target to use when emitting logs.
208 log_target: String,
209
210 /// Metrics of the chain.
211 metrics: Arc<crate::metrics::ChainMetrics>,
212}
213
214impl<TPlat: PlatformRef> Frontend<TPlat> {
215 /// Queues the given JSON-RPC request to be processed in the background.
216 ///
217 /// An error is returned if [`Config::max_pending_requests`] is exceeded, which can happen
218 /// if the requests take a long time to process or if [`Frontend::next_json_rpc_response`]
219 /// isn't called often enough.
220 pub fn queue_rpc_request(&self, json_rpc_request: String) -> Result<(), HandleRpcError> {
221 let log_friendly_request =
222 crate::util::truncated_str(json_rpc_request.chars().filter(|c| !c.is_control()), 250)
223 .to_string();
224
225 match self.requests_tx.try_send(json_rpc_request) {
226 Ok(()) => {
227 self.metrics.json_rpc_requests.inc();
228 log!(
229 &self.platform,
230 Debug,
231 &self.log_target,
232 "json-rpc-request-queued",
233 request = log_friendly_request
234 );
235 Ok(())
236 }
237 Err(err) => Err(HandleRpcError::TooManyPendingRequests {
238 json_rpc_request: err.into_inner(),
239 }),
240 }
241 }
242
243 /// Waits until a JSON-RPC response has been generated, then returns it.
244 ///
245 /// If this function is called multiple times in parallel, the order in which the calls are
246 /// responded to is unspecified.
247 pub async fn next_json_rpc_response(&self) -> String {
248 let message = match self.responses_rx.lock().await.next().await {
249 Some(m) => m,
250 None => unreachable!(),
251 };
252
253 log!(
254 &self.platform,
255 Debug,
256 &self.log_target,
257 "json-rpc-response-yielded",
258 response =
259 crate::util::truncated_str(message.chars().filter(|c| !c.is_control()), 250,)
260 );
261
262 message
263 }
264}
265
266/// Error potentially returned when queuing a JSON-RPC request.
267#[derive(Debug, derive_more::Display, derive_more::Error)]
268pub enum HandleRpcError {
269 /// The JSON-RPC service cannot process this request, as too many requests are already being
270 /// processed.
271 #[display(
272 "The JSON-RPC service cannot process this request, as too many requests are already being processed."
273 )]
274 TooManyPendingRequests {
275 /// Request that was being queued.
276 json_rpc_request: String,
277 },
278}