Coverage Report

Created: 2024-05-16 12:16

/__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
}