/__w/smoldot/smoldot/repo/light-base/src/json_rpc_service.rs
Line | Count | Source (jump to first uncovered line) |
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 | | |
40 | | mod background; |
41 | | |
42 | | use crate::{ |
43 | | log, network_service, platform::PlatformRef, runtime_service, sync_service, |
44 | | transactions_service, |
45 | | }; |
46 | | |
47 | | use alloc::{ |
48 | | borrow::Cow, |
49 | | boxed::Box, |
50 | | format, |
51 | | string::{String, ToString as _}, |
52 | | sync::Arc, |
53 | | }; |
54 | | use core::{num::NonZeroU32, pin::Pin}; |
55 | | use futures_lite::StreamExt as _; |
56 | | |
57 | | /// Configuration for [`service()`]. |
58 | | pub struct Config<TPlat: PlatformRef> { |
59 | | /// Access to the platform's capabilities. |
60 | | pub platform: TPlat, |
61 | | |
62 | | /// Name of the chain, for logging purposes. |
63 | | /// |
64 | | /// > **Note**: This name will be directly printed out. Any special character should already |
65 | | /// > have been filtered out from this name. |
66 | | pub log_name: String, |
67 | | |
68 | | /// Maximum number of JSON-RPC requests that can be added to a queue if it is not ready to be |
69 | | /// processed immediately. Any additional request will be immediately rejected. |
70 | | /// |
71 | | /// This parameter is necessary in order to prevent users from using up too much memory within |
72 | | /// the client. |
73 | | // TODO: unused at the moment |
74 | | pub max_pending_requests: NonZeroU32, |
75 | | |
76 | | /// Maximum number of active subscriptions. Any additional subscription will be immediately |
77 | | /// rejected. |
78 | | /// |
79 | | /// This parameter is necessary in order to prevent users from using up too much memory within |
80 | | /// the client. |
81 | | // TODO: unused at the moment |
82 | | pub max_subscriptions: u32, |
83 | | |
84 | | /// Access to the network, and identifier of the chain from the point of view of the network |
85 | | /// service. |
86 | | pub network_service: Arc<network_service::NetworkServiceChain<TPlat>>, |
87 | | |
88 | | /// Service responsible for synchronizing the chain. |
89 | | pub sync_service: Arc<sync_service::SyncService<TPlat>>, |
90 | | |
91 | | /// Service responsible for emitting transactions and tracking their state. |
92 | | pub transactions_service: Arc<transactions_service::TransactionsService<TPlat>>, |
93 | | |
94 | | /// Service that provides a ready-to-be-called runtime for the current best block. |
95 | | pub runtime_service: Arc<runtime_service::RuntimeService<TPlat>>, |
96 | | |
97 | | /// Name of the chain, as found in the chain specification. |
98 | | pub chain_name: String, |
99 | | /// Type of chain, as found in the chain specification. |
100 | | pub chain_ty: String, |
101 | | /// JSON-encoded properties of the chain, as found in the chain specification. |
102 | | pub chain_properties_json: String, |
103 | | /// Whether the chain is a live network. Found in the chain specification. |
104 | | pub chain_is_live: bool, |
105 | | |
106 | | /// Value to return when the `system_name` RPC is called. Should be set to the name of the |
107 | | /// final executable. |
108 | | pub system_name: String, |
109 | | |
110 | | /// Value to return when the `system_version` RPC is called. Should be set to the version of |
111 | | /// the final executable. |
112 | | pub system_version: String, |
113 | | |
114 | | /// Hash of the genesis block of the chain. |
115 | | pub genesis_block_hash: [u8; 32], |
116 | | } |
117 | | |
118 | | /// Creates a new JSON-RPC service with the given configuration. |
119 | | /// |
120 | | /// Returns a handler that allows sending requests and receiving responses. |
121 | | /// |
122 | | /// Destroying the [`Frontend`] automatically shuts down the service. |
123 | 0 | pub fn service<TPlat: PlatformRef>(config: Config<TPlat>) -> Frontend<TPlat> { |
124 | 0 | let log_target = format!("json-rpc-{}", config.log_name); |
125 | 0 |
|
126 | 0 | let (requests_tx, requests_rx) = async_channel::unbounded(); // TODO: capacity? |
127 | 0 | let (responses_tx, responses_rx) = async_channel::bounded(16); // TODO: capacity? |
128 | 0 |
|
129 | 0 | let frontend = Frontend { |
130 | 0 | platform: config.platform.clone(), |
131 | 0 | log_target: log_target.clone(), |
132 | 0 | responses_rx: Arc::new(async_lock::Mutex::new(Box::pin(responses_rx))), |
133 | 0 | requests_tx, |
134 | 0 | }; |
135 | 0 |
|
136 | 0 | let platform = config.platform.clone(); |
137 | 0 | platform.spawn_task( |
138 | 0 | Cow::Owned(log_target.clone()), |
139 | 0 | background::run( |
140 | 0 | log_target, |
141 | 0 | background::Config { |
142 | 0 | platform: config.platform, |
143 | 0 | network_service: config.network_service, |
144 | 0 | sync_service: config.sync_service, |
145 | 0 | transactions_service: config.transactions_service, |
146 | 0 | runtime_service: config.runtime_service, |
147 | 0 | chain_name: config.chain_name, |
148 | 0 | chain_ty: config.chain_ty, |
149 | 0 | chain_properties_json: config.chain_properties_json, |
150 | 0 | chain_is_live: config.chain_is_live, |
151 | 0 | system_name: config.system_name, |
152 | 0 | system_version: config.system_version, |
153 | 0 | genesis_block_hash: config.genesis_block_hash, |
154 | 0 | }, |
155 | 0 | requests_rx, |
156 | 0 | responses_tx, |
157 | 0 | ), |
158 | 0 | ); |
159 | 0 |
|
160 | 0 | frontend |
161 | 0 | } Unexecuted instantiation: _RINvNtCsiGub1lfKphe_13smoldot_light16json_rpc_service7servicepEB4_ Unexecuted instantiation: _RINvNtCsih6EgvAwZF2_13smoldot_light16json_rpc_service7serviceNtNtCsDDUKWWCHAU_18smoldot_light_wasm8platform11PlatformRefEB11_ Unexecuted instantiation: _RINvNtCsih6EgvAwZF2_13smoldot_light16json_rpc_service7servicepEB4_ |
162 | | |
163 | | /// Handle that allows sending JSON-RPC requests on the service. |
164 | | /// |
165 | | /// The [`Frontend`] can be cloned, in which case the clone will refer to the same JSON-RPC |
166 | | /// service. |
167 | | /// |
168 | | /// Destroying all the [`Frontend`]s automatically shuts down the associated service. |
169 | | #[derive(Clone)] |
170 | | pub struct Frontend<TPlat> { |
171 | | /// See [`Config::platform`]. |
172 | | platform: TPlat, |
173 | | |
174 | | /// How to send requests to the background task. |
175 | | requests_tx: async_channel::Sender<String>, |
176 | | |
177 | | /// How to receive responses coming from the background task. |
178 | | // TODO: we use an Arc so that it's clonable, but that's questionnable |
179 | | responses_rx: Arc<async_lock::Mutex<Pin<Box<async_channel::Receiver<String>>>>>, |
180 | | |
181 | | /// Target to use when emitting logs. |
182 | | log_target: String, |
183 | | } |
184 | | |
185 | | impl<TPlat: PlatformRef> Frontend<TPlat> { |
186 | | /// Queues the given JSON-RPC request to be processed in the background. |
187 | | /// |
188 | | /// An error is returned if [`Config::max_pending_requests`] is exceeded, which can happen |
189 | | /// if the requests take a long time to process or if [`Frontend::next_json_rpc_response`] |
190 | | /// isn't called often enough. |
191 | 0 | pub fn queue_rpc_request(&self, json_rpc_request: String) -> Result<(), HandleRpcError> { |
192 | 0 | let log_friendly_request = |
193 | 0 | crate::util::truncated_str(json_rpc_request.chars().filter(|c| !c.is_control()), 250) Unexecuted instantiation: _RNCNvMNtCsiGub1lfKphe_13smoldot_light16json_rpc_serviceINtB4_8FrontendpE17queue_rpc_request0B6_ Unexecuted instantiation: _RNCNvMNtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceINtB4_8FrontendNtNtCsDDUKWWCHAU_18smoldot_light_wasm8platform11PlatformRefE17queue_rpc_request0B1a_ Unexecuted instantiation: _RNCNvMNtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceINtB4_8FrontendpE17queue_rpc_request0B6_ |
194 | 0 | .to_string(); |
195 | 0 |
|
196 | 0 | match self.requests_tx.try_send(json_rpc_request) { |
197 | | Ok(()) => { |
198 | 0 | log!( |
199 | 0 | &self.platform, |
200 | 0 | Debug, |
201 | 0 | &self.log_target, |
202 | 0 | "json-rpc-request-queued", |
203 | 0 | request = log_friendly_request |
204 | 0 | ); |
205 | 0 | Ok(()) |
206 | | } |
207 | 0 | Err(err) => Err(HandleRpcError::TooManyPendingRequests { |
208 | 0 | json_rpc_request: err.into_inner(), |
209 | 0 | }), |
210 | | } |
211 | 0 | } Unexecuted instantiation: _RNvMNtCsiGub1lfKphe_13smoldot_light16json_rpc_serviceINtB2_8FrontendpE17queue_rpc_requestB4_ Unexecuted instantiation: _RNvMNtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceINtB2_8FrontendNtNtCsDDUKWWCHAU_18smoldot_light_wasm8platform11PlatformRefE17queue_rpc_requestB18_ Unexecuted instantiation: _RNvMNtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceINtB2_8FrontendpE17queue_rpc_requestB4_ |
212 | | |
213 | | /// Waits until a JSON-RPC response has been generated, then returns it. |
214 | | /// |
215 | | /// If this function is called multiple times in parallel, the order in which the calls are |
216 | | /// responded to is unspecified. |
217 | 0 | pub async fn next_json_rpc_response(&self) -> String { Unexecuted instantiation: _RNvMNtCsiGub1lfKphe_13smoldot_light16json_rpc_serviceINtB2_8FrontendpE22next_json_rpc_responseB4_ Unexecuted instantiation: _RNvMNtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceINtB2_8FrontendNtNtCsDDUKWWCHAU_18smoldot_light_wasm8platform11PlatformRefE22next_json_rpc_responseB18_ Unexecuted instantiation: _RNvMNtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceINtB2_8FrontendpE22next_json_rpc_responseB4_ |
218 | 0 | let message = match self.responses_rx.lock().await.next().await { |
219 | 0 | Some(m) => m, |
220 | 0 | None => unreachable!(), |
221 | | }; |
222 | | |
223 | 0 | log!( |
224 | 0 | &self.platform, |
225 | 0 | Debug, |
226 | 0 | &self.log_target, |
227 | 0 | "json-rpc-response-yielded", |
228 | 0 | response = |
229 | 0 | crate::util::truncated_str(message.chars().filter(|c| !c.is_control()), 250,) Unexecuted instantiation: _RNCNCNvMNtCsiGub1lfKphe_13smoldot_light16json_rpc_serviceINtB6_8FrontendpE22next_json_rpc_response00B8_ Unexecuted instantiation: _RNCNCNvMNtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceINtB6_8FrontendNtNtCsDDUKWWCHAU_18smoldot_light_wasm8platform11PlatformRefE22next_json_rpc_response00B1c_ Unexecuted instantiation: _RNCNCNvMNtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceINtB6_8FrontendpE22next_json_rpc_response00B8_ |
230 | 0 | ); |
231 | 0 |
|
232 | 0 | message |
233 | 0 | } Unexecuted instantiation: _RNCNvMNtCsiGub1lfKphe_13smoldot_light16json_rpc_serviceINtB4_8FrontendpE22next_json_rpc_response0B6_ Unexecuted instantiation: _RNCNvMNtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceINtB4_8FrontendNtNtCsDDUKWWCHAU_18smoldot_light_wasm8platform11PlatformRefE22next_json_rpc_response0B1a_ Unexecuted instantiation: _RNCNvMNtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceINtB4_8FrontendpE22next_json_rpc_response0B6_ |
234 | | } |
235 | | |
236 | | /// Error potentially returned when queuing a JSON-RPC request. |
237 | 0 | #[derive(Debug, derive_more::Display)] Unexecuted instantiation: _RNvXs1_NtCsiGub1lfKphe_13smoldot_light16json_rpc_serviceNtB5_14HandleRpcErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt Unexecuted instantiation: _RNvXs1_NtCsih6EgvAwZF2_13smoldot_light16json_rpc_serviceNtB5_14HandleRpcErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt |
238 | | pub enum HandleRpcError { |
239 | | /// The JSON-RPC service cannot process this request, as too many requests are already being |
240 | | /// processed. |
241 | | #[display( |
242 | | fmt = "The JSON-RPC service cannot process this request, as too many requests are already being processed." |
243 | | )] |
244 | | TooManyPendingRequests { |
245 | | /// Request that was being queued. |
246 | | json_rpc_request: String, |
247 | | }, |
248 | | } |