smoldot_light/lib.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//! Smoldot light client library.
19//!
20//! This library provides an easy way to create a light client.
21//!
22//! This light client is opinionated towards certain aspects: what it downloads, how much memory
23//! and CPU it is willing to consume, etc.
24//!
25//! # Usage
26//!
27//! ## Initialization
28//!
29//! In order to use the light client, call [`Client::new`], passing an implementation of the
30//! [`platform::PlatformRef`] trait. See the documentation of the [`platform::PlatformRef`] trait
31//! for more information.
32//!
33//! The [`Client`] contains two generic parameters:
34//!
35//! - An implementation of the [`platform::PlatformRef`] trait.
36//! - An opaque user data. If you do not use this, you can simply use `()`.
37//!
38//! When the `std` feature of this library is enabled, the [`platform::DefaultPlatform`] struct
39//! can be used as an implementation of [`platform::PlatformRef`].
40//!
41//! For example:
42//!
43//! ```rust
44//! use smoldot_light::{Client, platform::DefaultPlatform};
45//! let client = Client::new(DefaultPlatform::new(env!("CARGO_PKG_NAME").into(), env!("CARGO_PKG_VERSION").into()));
46//! # let _: Client<_, ()> = client; // Used in this example to infer the generic parameters of the Client
47//! ```
48//!
49//! If the `std` feature of this library is disabled, then you need to implement the
50//! [`platform::PlatformRef`] trait manually.
51//!
52//! ## Adding a chain
53//!
54//! After the client has been initialized, use [`Client::add_chain`] to ask the client to connect
55//! to said chain. See the documentation of [`AddChainConfig`] for information about what to
56//! provide.
57//!
58//! [`Client::add_chain`] returns a [`ChainId`], which identifies the chain within the [`Client`].
59//! A [`Client`] can be thought of as a collection of chain connections, each identified by their
60//! [`ChainId`], akin to a `HashMap<ChainId, ...>`.
61//!
62//! A chain can be removed at any time using [`Client::remove_chain`]. This will cause the client
63//! to stop all connections and clean up its internal services. The [`ChainId`] is instantly
64//! considered as invalid as soon as the method is called.
65//!
66//! ## JSON-RPC requests and responses
67//!
68//! Once a chain has been added, one can send JSON-RPC requests using [`Client::json_rpc_request`].
69//!
70//! The request parameter of this function must be a JSON-RPC request in its text form. For
71//! example: `{"id":53,"jsonrpc":"2.0","method":"system_name","params":[]}`.
72//!
73//! Calling [`Client::json_rpc_request`] queues the request in the internals of the client. Later,
74//! the client will process it.
75//!
76//! Responses can be pulled by calling the [`AddChainSuccess::json_rpc_responses`] that is returned
77//! after a chain has been added.
78//!
79
80#![cfg_attr(not(any(test, feature = "std")), no_std)]
81#![forbid(unsafe_code)]
82#![deny(rustdoc::broken_intra_doc_links)]
83// TODO: the `unused_crate_dependencies` lint is disabled because of dev-dependencies, see <https://github.com/rust-lang/rust/issues/95513>
84// #![deny(unused_crate_dependencies)]
85
86extern crate alloc;
87
88use alloc::{borrow::ToOwned as _, boxed::Box, format, string::String, sync::Arc, vec, vec::Vec};
89use core::{num::NonZero, ops, time::Duration};
90use hashbrown::{HashMap, hash_map::Entry};
91use itertools::Itertools as _;
92use platform::PlatformRef;
93use smoldot::{
94 chain, chain_spec, header,
95 informant::HashDisplay,
96 libp2p::{multiaddr, peer_id},
97};
98
99mod bitswap_service;
100mod database;
101mod json_rpc_service;
102mod runtime_service;
103mod sync_service;
104mod transactions_service;
105mod util;
106
107pub mod network_service;
108pub mod platform;
109
110pub use json_rpc_service::HandleRpcError;
111
112/// See [`Client::add_chain`].
113#[derive(Debug, Clone)]
114pub struct AddChainConfig<'a, TChain, TRelays> {
115 /// Opaque user data that the [`Client`] will hold for this chain. Can later be accessed using
116 /// the `Index` and `IndexMut` trait implementations on the [`Client`].
117 pub user_data: TChain,
118
119 /// JSON text containing the specification of the chain (the so-called "chain spec").
120 pub specification: &'a str,
121
122 /// Opaque data containing the database content that was retrieved by calling
123 /// the `chainHead_unstable_finalizedDatabase` JSON-RPC function in the past.
124 ///
125 /// Pass an empty string if no database content exists or is known.
126 ///
127 /// No error is generated if this data is invalid and/or can't be decoded. The implementation
128 /// reserves the right to break the format of this data at any point.
129 pub database_content: &'a str,
130
131 /// If [`AddChainConfig`] defines a parachain, contains the list of relay chains to choose
132 /// from. Ignored if not a parachain.
133 ///
134 /// This field is necessary because multiple different chain can have the same identity. If
135 /// the client tried to find the corresponding relay chain in all the previously-spawned
136 /// chains, it means that a call to [`Client::add_chain`] could influence the outcome of a
137 /// subsequent call to [`Client::add_chain`].
138 ///
139 /// For example: if user A adds a chain named "Kusama", then user B adds a different chain
140 /// also named "Kusama", then user B adds a parachain whose relay chain is "Kusama", it would
141 /// be wrong to connect to the "Kusama" created by user A.
142 pub potential_relay_chains: TRelays,
143
144 /// Configuration for the JSON-RPC endpoint.
145 pub json_rpc: AddChainConfigJsonRpc,
146
147 /// If `Some`, enables the statement store networking protocol.
148 pub statement_protocol_config: Option<network_service::StatementProtocolConfig>,
149}
150
151/// See [`AddChainConfig::json_rpc`].
152#[derive(Debug, Clone)]
153pub enum AddChainConfigJsonRpc {
154 /// No JSON-RPC endpoint is available for this chain. This saves up a lot of resources, but
155 /// will cause all JSON-RPC requests targeting this chain to fail.
156 Disabled,
157
158 /// The JSON-RPC endpoint is enabled. Normal operations.
159 Enabled {
160 /// Maximum number of JSON-RPC requests that can be added to a queue if it is not ready to
161 /// be processed immediately. Any additional request will be immediately rejected.
162 ///
163 /// This parameter is necessary in order to prevent JSON-RPC clients from using up too
164 /// much memory within the client.
165 /// If the JSON-RPC client is entirely trusted, then passing `u32::MAX` is
166 /// completely reasonable.
167 ///
168 /// A typical value is 128.
169 max_pending_requests: NonZero<u32>,
170
171 /// Maximum number of active subscriptions that can be started through JSON-RPC functions.
172 /// Any request that causes the JSON-RPC server to generate notifications counts as a
173 /// subscription.
174 /// Any additional subscription over this limit will be immediately rejected.
175 ///
176 /// This parameter is necessary in order to prevent JSON-RPC clients from using up too
177 /// much memory within the client.
178 /// If the JSON-RPC client is entirely trusted, then passing `u32::MAX` is
179 /// completely reasonable.
180 ///
181 /// While a typical reasonable value would be for example 64, existing UIs tend to start
182 /// a lot of subscriptions, and a value such as 1024 is recommended.
183 max_subscriptions: u32,
184 },
185}
186
187/// Chain registered in a [`Client`].
188///
189/// This type is a simple wrapper around a `usize`. Use the `From<usize> for ChainId` and
190/// `From<ChainId> for usize` trait implementations to convert back and forth if necessary.
191//
192// Implementation detail: corresponds to indices within [`Client::public_api_chains`].
193#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
194pub struct ChainId(usize);
195
196impl From<usize> for ChainId {
197 fn from(id: usize) -> ChainId {
198 ChainId(id)
199 }
200}
201
202impl From<ChainId> for usize {
203 fn from(chain_id: ChainId) -> usize {
204 chain_id.0
205 }
206}
207
208/// Holds a list of chains, connections, and JSON-RPC services.
209pub struct Client<TPlat: platform::PlatformRef, TChain = ()> {
210 /// Access to the platform capabilities.
211 platform: TPlat,
212
213 /// List of chains currently running according to the public API. Indices in this container
214 /// are reported through the public API. The values are either an error if the chain has failed
215 /// to initialize, or key found in [`Client::chains_by_key`].
216 public_api_chains: slab::Slab<PublicApiChain<TPlat, TChain>>,
217
218 /// De-duplicated list of chains that are *actually* running.
219 ///
220 /// For each key, contains the services running for this chain plus the number of public API
221 /// chains that correspond to it.
222 ///
223 /// Because we use a `SipHasher`, this hashmap isn't created in the `new` function (as this
224 /// function is `const`) but lazily the first time it is needed.
225 chains_by_key: Option<HashMap<ChainKey, RunningChain<TPlat>, util::SipHasherBuild>>,
226
227 /// All chains share a single networking service created lazily the first time that it
228 /// is used.
229 network_service: Option<Arc<network_service::NetworkService<TPlat>>>,
230}
231
232struct PublicApiChain<TPlat: PlatformRef, TChain> {
233 /// Opaque user data passed to [`Client::add_chain`].
234 user_data: TChain,
235
236 /// Index of the underlying chain found in [`Client::chains_by_key`].
237 key: ChainKey,
238
239 /// Identifier of the chain found in its chain spec. Equal to the return value of
240 /// [`chain_spec::ChainSpec::id`]. Used in order to match parachains with relay chains.
241 chain_spec_chain_id: String,
242
243 /// Handle that sends requests to the JSON-RPC service that runs in the background.
244 /// Destroying this handle also shuts down the service. `None` iff
245 /// [`AddChainConfig::json_rpc`] was [`AddChainConfigJsonRpc::Disabled`] when adding the chain.
246 json_rpc_frontend: Option<json_rpc_service::Frontend<TPlat>>,
247
248 /// Notified when the [`PublicApiChain`] is destroyed, in order for the [`JsonRpcResponses`]
249 /// to detect when the chain has been removed.
250 public_api_chain_destroyed_event: event_listener::Event,
251}
252
253/// Identifies a chain, so that multiple identical chains are de-duplicated.
254///
255/// This struct serves as the key in a `HashMap<ChainKey, ChainServices>`. It must contain all the
256/// values that are important to the logic of the fields that are contained in [`ChainServices`].
257/// Failing to include a field in this struct could lead to two different chains using the same
258/// [`ChainServices`], which has security consequences.
259#[derive(Debug, Clone, PartialEq, Eq, Hash)]
260struct ChainKey {
261 /// Hash of the genesis block of the chain.
262 genesis_block_hash: [u8; 32],
263
264 // TODO: what about light checkpoints?
265 // TODO: must also contain forkBlocks, and badBlocks fields
266 /// If the chain is a parachain, contains the relay chain and the "para ID" on this relay
267 /// chain.
268 relay_chain: Option<(Box<ChainKey>, u32)>,
269
270 /// Networking fork id, found in the chain specification.
271 fork_id: Option<String>,
272}
273
274struct RunningChain<TPlat: platform::PlatformRef> {
275 /// Services that are dedicated to this chain. Wrapped within a `MaybeDone` because the
276 /// initialization is performed asynchronously.
277 services: ChainServices<TPlat>,
278
279 /// Name of this chain in the logs. This is not necessarily the same as the identifier of the
280 /// chain in its chain specification.
281 log_name: String,
282
283 /// Number of elements in [`Client::public_api_chains`] that reference this chain. If this
284 /// number reaches `0`, the [`RunningChain`] should be destroyed.
285 num_references: NonZero<u32>,
286}
287
288struct ChainServices<TPlat: platform::PlatformRef> {
289 network_service: Arc<network_service::NetworkServiceChain<TPlat>>,
290 sync_service: Arc<sync_service::SyncService<TPlat>>,
291 runtime_service: Arc<runtime_service::RuntimeService<TPlat>>,
292 transactions_service: Arc<transactions_service::TransactionsService<TPlat>>,
293 bitswap_service: Arc<bitswap_service::BitswapService>,
294}
295
296impl<TPlat: platform::PlatformRef> Clone for ChainServices<TPlat> {
297 fn clone(&self) -> Self {
298 ChainServices {
299 network_service: self.network_service.clone(),
300 sync_service: self.sync_service.clone(),
301 runtime_service: self.runtime_service.clone(),
302 transactions_service: self.transactions_service.clone(),
303 bitswap_service: self.bitswap_service.clone(),
304 }
305 }
306}
307
308/// Returns by [`Client::add_chain`] on success.
309pub struct AddChainSuccess<TPlat: PlatformRef> {
310 /// Newly-allocated identifier for the chain.
311 pub chain_id: ChainId,
312
313 /// Stream of JSON-RPC responses or notifications.
314 ///
315 /// Is always `Some` if [`AddChainConfig::json_rpc`] was [`AddChainConfigJsonRpc::Enabled`],
316 /// and `None` if it was [`AddChainConfigJsonRpc::Disabled`]. In other words, you can unwrap
317 /// this `Option` if you passed `Enabled`.
318 pub json_rpc_responses: Option<JsonRpcResponses<TPlat>>,
319}
320
321/// Stream of JSON-RPC responses or notifications.
322///
323/// See [`AddChainSuccess::json_rpc_responses`].
324pub struct JsonRpcResponses<TPlat: PlatformRef> {
325 /// Receiving side for responses.
326 ///
327 /// As long as this object is alive, the JSON-RPC service will continue running. In order
328 /// to prevent that from happening, we destroy it as soon as the
329 /// [`JsonRpcResponses::public_api_chain_destroyed`] is notified of the destruction of
330 /// the sender.
331 inner: Option<json_rpc_service::Frontend<TPlat>>,
332
333 /// Notified when the [`PublicApiChain`] is destroyed.
334 public_api_chain_destroyed: event_listener::EventListener,
335}
336
337impl<TPlat: PlatformRef> JsonRpcResponses<TPlat> {
338 /// Returns the next response or notification, or `None` if the chain has been removed.
339 pub async fn next(&mut self) -> Option<String> {
340 if let Some(frontend) = self.inner.as_mut() {
341 if let Some(response) = futures_lite::future::or(
342 async { Some(frontend.next_json_rpc_response().await) },
343 async {
344 (&mut self.public_api_chain_destroyed).await;
345 None
346 },
347 )
348 .await
349 {
350 return Some(response);
351 }
352 }
353
354 self.inner = None;
355 None
356 }
357}
358
359impl<TPlat: platform::PlatformRef, TChain> Client<TPlat, TChain> {
360 /// Initializes the smoldot client.
361 pub const fn new(platform: TPlat) -> Self {
362 Client {
363 platform,
364 public_api_chains: slab::Slab::new(),
365 chains_by_key: None,
366 network_service: None,
367 }
368 }
369
370 /// Adds a new chain to the list of chains smoldot tries to synchronize.
371 ///
372 /// Returns an error in case something is wrong with the configuration.
373 pub fn add_chain(
374 &mut self,
375 config: AddChainConfig<'_, TChain, impl Iterator<Item = ChainId>>,
376 ) -> Result<AddChainSuccess<TPlat>, AddChainError> {
377 // `chains_by_key` is created lazily whenever needed.
378 let chains_by_key = self.chains_by_key.get_or_insert_with(|| {
379 HashMap::with_hasher(util::SipHasherBuild::new({
380 let mut seed = [0; 16];
381 self.platform.fill_random_bytes(&mut seed);
382 seed
383 }))
384 });
385
386 // Decode the chain specification.
387 let chain_spec = match chain_spec::ChainSpec::from_json_bytes(config.specification) {
388 Ok(cs) => cs,
389 Err(err) => {
390 return Err(AddChainError::ChainSpecParseError(err));
391 }
392 };
393
394 // Build the genesis block, its hash, and information about the chain.
395 let (
396 genesis_chain_information,
397 genesis_block_header,
398 print_warning_genesis_root_chainspec,
399 genesis_block_state_root,
400 ) = {
401 // TODO: don't build the chain information if only the genesis hash is needed: https://github.com/smol-dot/smoldot/issues/1017
402 let genesis_chain_information = chain_spec.to_chain_information().map(|(ci, _)| ci); // TODO: don't just throw away the runtime;
403
404 match genesis_chain_information {
405 Ok(genesis_chain_information) => {
406 let header = genesis_chain_information.as_ref().finalized_block_header;
407 let state_root = *header.state_root;
408 let scale_encoded =
409 header.scale_encoding_vec(usize::from(chain_spec.block_number_bytes()));
410 (
411 Some(genesis_chain_information),
412 scale_encoded,
413 chain_spec.light_sync_state().is_some()
414 || chain_spec.relay_chain().is_some(),
415 state_root,
416 )
417 }
418 Err(chain_spec::FromGenesisStorageError::UnknownStorageItems) => {
419 let state_root = *chain_spec.genesis_storage().into_trie_root_hash().unwrap();
420 let header = header::Header {
421 parent_hash: [0; 32],
422 number: 0,
423 state_root,
424 extrinsics_root: smoldot::trie::EMPTY_BLAKE2_TRIE_MERKLE_VALUE,
425 digest: header::DigestRef::empty().into(),
426 }
427 .scale_encoding_vec(usize::from(chain_spec.block_number_bytes()));
428 (None, header, false, state_root)
429 }
430 Err(err) => return Err(AddChainError::InvalidGenesisStorage(err)),
431 }
432 };
433 let genesis_block_hash = header::hash_from_scale_encoded_header(&genesis_block_header);
434
435 // Decode the database and make sure that it matches the chain by comparing the finalized
436 // block header in it with the actual one.
437 let (database, database_was_wrong_chain) = {
438 let mut maybe_database = database::decode_database(
439 config.database_content,
440 chain_spec.block_number_bytes().into(),
441 )
442 .ok();
443 let mut database_was_wrong = false;
444 if maybe_database
445 .as_ref()
446 .map_or(false, |db| db.genesis_block_hash != genesis_block_hash)
447 {
448 maybe_database = None;
449 database_was_wrong = true;
450 }
451 (maybe_database, database_was_wrong)
452 };
453
454 // Load the information about the chain. If a light sync state (also known as a checkpoint)
455 // is present in the chain spec, it is possible to start syncing at the finalized block
456 // it describes.
457 // At the same time, we deconstruct the database into `known_nodes`
458 // and `runtime_code_hint`.
459 let (chain_information, used_database_chain_information, known_nodes, runtime_code_hint) = {
460 let checkpoint = chain_spec
461 .light_sync_state()
462 .map(|s| s.to_chain_information());
463
464 match (genesis_chain_information, checkpoint, database) {
465 // Use the database if it contains a more recent block than the
466 // chain spec checkpoint.
467 (
468 _,
469 Some(Ok(checkpoint)),
470 Some(database::DatabaseContent {
471 chain_information: Some(db_ci),
472 known_nodes,
473 runtime_code_hint,
474 ..
475 }),
476 ) if db_ci.as_ref().finalized_block_header.number
477 >= checkpoint.as_ref().finalized_block_header.number =>
478 {
479 (Some(db_ci), true, known_nodes, runtime_code_hint)
480 }
481
482 // Otherwise, use the chain spec checkpoint.
483 (
484 _,
485 Some(Ok(checkpoint)),
486 Some(database::DatabaseContent {
487 known_nodes,
488 runtime_code_hint,
489 ..
490 }),
491 ) => (Some(checkpoint), false, known_nodes, runtime_code_hint),
492 (_, Some(Ok(checkpoint)), None) => (Some(checkpoint), false, Vec::new(), None),
493
494 // If neither the genesis chain information nor the checkpoint chain information
495 // is available, we could in principle use the database, but for API reasons we
496 // don't want users to be able to rely on just a database (as we reserve the right
497 // to break the database at any point) and thus return an error.
498 (
499 None,
500 None,
501 Some(database::DatabaseContent {
502 known_nodes,
503 runtime_code_hint,
504 ..
505 }),
506 ) => (None, false, known_nodes, runtime_code_hint),
507 (None, None, None) => (None, false, Vec::new(), None),
508
509 // Use the genesis block if no checkpoint is available.
510 (
511 Some(genesis_ci),
512 None
513 | Some(Err(
514 chain_spec::CheckpointToChainInformationError::GenesisBlockCheckpoint,
515 )),
516 Some(database::DatabaseContent {
517 known_nodes,
518 runtime_code_hint,
519 ..
520 }),
521 ) => (Some(genesis_ci), false, known_nodes, runtime_code_hint),
522 (
523 Some(genesis_ci),
524 None
525 | Some(Err(
526 chain_spec::CheckpointToChainInformationError::GenesisBlockCheckpoint,
527 )),
528 None,
529 ) => (Some(genesis_ci), false, Vec::new(), None),
530
531 // If the checkpoint format is invalid, we return an error no matter whether the
532 // genesis chain information could be used.
533 (_, Some(Err(err)), _) => {
534 return Err(AddChainError::InvalidCheckpoint(err));
535 }
536 }
537 };
538
539 // If the chain specification specifies a parachain, find the corresponding relay chain
540 // in the list of potential relay chains passed by the user.
541 // If no relay chain can be found, the chain creation fails. Exactly one matching relay
542 // chain must be found. If there are multiple ones, the creation fails as well.
543 let relay_chain_id = if let Some((relay_chain_id, para_id)) = chain_spec.relay_chain() {
544 let chain = config
545 .potential_relay_chains
546 .filter(|c| {
547 self.public_api_chains
548 .get(c.0)
549 .map_or(false, |chain| chain.chain_spec_chain_id == relay_chain_id)
550 })
551 .exactly_one();
552
553 match chain {
554 Ok(c) => Some((c, para_id)),
555 Err(mut iter) => {
556 // `iter` here is identical to the iterator above before `exactly_one` is
557 // called. This lets us know what failed.
558 return Err(if iter.next().is_none() {
559 AddChainError::NoRelayChainFound
560 } else {
561 debug_assert!(iter.next().is_some());
562 AddChainError::MultipleRelayChains
563 });
564 }
565 }
566 } else {
567 None
568 };
569
570 // Build the list of bootstrap nodes ahead of time.
571 // Because the specification of the format of a multiaddress is a bit flexible, it is
572 // not possible to firmly affirm that a multiaddress is invalid. For this reason, we
573 // simply ignore unparsable bootnode addresses rather than returning an error.
574 // A list of invalid bootstrap node addresses is kept in order to print a warning later
575 // in case it is non-empty. This list is sanitized in order to be safely printable as part
576 // of the logs.
577 let (bootstrap_nodes, invalid_bootstrap_nodes_sanitized) = {
578 let mut valid_list = Vec::with_capacity(chain_spec.boot_nodes().len());
579 let mut invalid_list = Vec::with_capacity(0);
580 for node in chain_spec.boot_nodes() {
581 match node {
582 chain_spec::Bootnode::Parsed { multiaddr, peer_id } => {
583 if let Ok(multiaddr) = multiaddr.parse::<multiaddr::Multiaddr>() {
584 let peer_id = peer_id::PeerId::from_bytes(peer_id).unwrap();
585 valid_list.push((peer_id, vec![multiaddr]));
586 } else {
587 invalid_list.push(multiaddr)
588 }
589 }
590 chain_spec::Bootnode::UnrecognizedFormat(unparsed) => invalid_list.push(
591 unparsed
592 .chars()
593 .filter(|c| c.is_ascii())
594 .collect::<String>(),
595 ),
596 }
597 }
598 (valid_list, invalid_list)
599 };
600
601 // All the checks are performed above. Adding the chain can't fail anymore at this point.
602
603 // Grab this field from the chain specification for later, as the chain specification is
604 // consumed below.
605 let chain_spec_chain_id = chain_spec.id().to_owned();
606
607 // The key generated here uniquely identifies this chain within smoldot. Multiple chains
608 // having the same key will use the same services.
609 //
610 // This struct is extremely important from a security perspective. We want multiple
611 // identical chains to be de-duplicated, but security issues would arise if two chains
612 // were considered identical while they're in reality not identical.
613 let new_chain_key = ChainKey {
614 genesis_block_hash,
615 relay_chain: relay_chain_id.map(|(ck, _)| {
616 (
617 Box::new(self.public_api_chains.get(ck.0).unwrap().key.clone()),
618 chain_spec.relay_chain().unwrap().1,
619 )
620 }),
621 fork_id: chain_spec.fork_id().map(|f| f.to_owned()),
622 };
623
624 // If the chain we are adding is a parachain, grab the services of the relay chain.
625 //
626 // This could in principle be done later on, but doing so raises borrow checker errors.
627 let relay_chain: Option<(ChainServices<_>, u32, String)> =
628 relay_chain_id.map(|(relay_chain, para_id)| {
629 let relay_chain = &chains_by_key
630 .get(&self.public_api_chains.get(relay_chain.0).unwrap().key)
631 .unwrap();
632 (
633 relay_chain.services.clone(),
634 para_id,
635 relay_chain.log_name.clone(),
636 )
637 });
638
639 // Determinate the name under which the chain will be identified in the logs.
640 // Because the chain spec is untrusted input, we must transform the `id` to remove all
641 // weird characters.
642 //
643 // By default, this log name will be equal to chain's `id`. Since it is possible for
644 // multiple different chains to have the same `id`, we need to look into the list of
645 // existing chains and make sure that there's no conflict, in which case the log name
646 // will have the suffix `-1`, or `-2`, or `-3`, and so on.
647 //
648 // This value is ignored if we enter the `Entry::Occupied` block below. Because the
649 // calculation requires accessing the list of existing chains, this block can't be put in
650 // the `Entry::Vacant` block below, even though it would make more sense for it to be
651 // there.
652 let log_name = {
653 let base = chain_spec
654 .id()
655 .chars()
656 .filter(|c| c.is_ascii_graphic())
657 .collect::<String>();
658 let mut suffix = None;
659
660 loop {
661 let attempt = if let Some(suffix) = suffix {
662 format!("{base}-{suffix}")
663 } else {
664 base.clone()
665 };
666
667 if !chains_by_key.values().any(|c| *c.log_name == attempt) {
668 break attempt;
669 }
670
671 match &mut suffix {
672 Some(v) => *v += 1,
673 v @ None => *v = Some(1),
674 }
675 }
676 };
677
678 let statement_protocol_config = config.statement_protocol_config;
679
680 let max_seen_statements = statement_protocol_config
681 .as_ref()
682 .map(|c| c.max_seen_statements());
683
684 // Start the services of the chain to add, or grab the services if they already exist.
685 let (services, log_name) = match chains_by_key.entry(new_chain_key.clone()) {
686 Entry::Occupied(mut entry) => {
687 // The chain to add always has a corresponding chain running. Simply grab the
688 // existing services and existing log name.
689 // The `log_name` created above is discarded in favour of the existing log name.
690 entry.get_mut().num_references = entry.get().num_references.checked_add(1).unwrap();
691 let entry = entry.into_mut();
692 (&mut entry.services, &entry.log_name)
693 }
694 Entry::Vacant(entry) => {
695 if let (None, None) = (&relay_chain, &chain_information) {
696 return Err(AddChainError::ChainSpecNeitherGenesisStorageNorCheckpoint);
697 }
698
699 // Start the services of the new chain.
700 let services = {
701 // Version of the client when requested through the networking.
702 let network_identify_agent_version = format!(
703 "{} {}",
704 self.platform.client_name(),
705 self.platform.client_version()
706 );
707
708 let config = match (&relay_chain, &chain_information) {
709 (Some((relay_chain, para_id, _)), _) => StartServicesChainTy::Parachain {
710 relay_chain,
711 para_id: *para_id,
712 },
713 (None, Some(chain_information)) => {
714 StartServicesChainTy::SubstrateCompatible { chain_information }
715 }
716 (None, None) => {
717 // Checked above.
718 unreachable!()
719 }
720 };
721
722 start_services(
723 log_name.clone(),
724 &self.platform,
725 &mut self.network_service,
726 runtime_code_hint,
727 genesis_block_header,
728 usize::from(chain_spec.block_number_bytes()),
729 chain_spec.fork_id().map(|f| f.to_owned()),
730 config,
731 network_identify_agent_version,
732 statement_protocol_config.clone(),
733 )
734 };
735
736 // Note that the chain name is printed through the `Debug` trait (rather
737 // than `Display`) because it is an untrusted user input.
738 if let Some((_, para_id, relay_chain_log_name)) = relay_chain.as_ref() {
739 log!(
740 &self.platform,
741 Info,
742 "smoldot",
743 format!(
744 "Parachain initialization complete for {}. Name: {:?}. Genesis \
745 hash: {}. Relay chain: {} (id: {})",
746 log_name,
747 chain_spec.name(),
748 HashDisplay(&genesis_block_hash),
749 relay_chain_log_name,
750 para_id
751 )
752 );
753 } else {
754 log!(
755 &self.platform,
756 Info,
757 "smoldot",
758 format!(
759 "Chain initialization complete for {}. Name: {:?}. Genesis \
760 hash: {}. {} starting at: {} (#{})",
761 log_name,
762 chain_spec.name(),
763 HashDisplay(&genesis_block_hash),
764 if used_database_chain_information {
765 "Database"
766 } else {
767 "Chain specification"
768 },
769 HashDisplay(
770 &chain_information
771 .as_ref()
772 .map(|ci| ci
773 .as_ref()
774 .finalized_block_header
775 .hash(usize::from(chain_spec.block_number_bytes())))
776 .unwrap_or(genesis_block_hash)
777 ),
778 chain_information
779 .as_ref()
780 .map(|ci| ci.as_ref().finalized_block_header.number)
781 .unwrap_or(0)
782 )
783 );
784 }
785
786 if print_warning_genesis_root_chainspec {
787 log!(
788 &self.platform,
789 Info,
790 "smoldot",
791 format!(
792 "Chain specification of {} contains a `genesis.raw` item. It is \
793 possible to significantly improve the initialization time by \
794 replacing the `\"raw\": ...` field with \
795 `\"stateRootHash\": \"0x{}\"`",
796 log_name,
797 hex::encode(genesis_block_state_root)
798 )
799 );
800 }
801
802 if chain_spec.protocol_id().is_some() {
803 log!(
804 &self.platform,
805 Warn,
806 "smoldot",
807 format!(
808 "Chain specification of {} contains a `protocolId` field. This \
809 field is deprecated and its value is no longer used. It can be \
810 safely removed from the JSON document.",
811 log_name
812 )
813 );
814 }
815
816 if chain_spec.telemetry_endpoints().count() != 0 {
817 log!(
818 &self.platform,
819 Warn,
820 "smoldot",
821 format!(
822 "Chain specification of {} contains a non-empty \
823 `telemetryEndpoints` field. Smoldot doesn't support telemetry \
824 endpoints and as such this field is unused.",
825 log_name
826 )
827 );
828 }
829
830 // TODO: remove after https://github.com/paritytech/smoldot/issues/2584
831 if chain_spec.bad_blocks_hashes().count() != 0 {
832 log!(
833 &self.platform,
834 Warn,
835 "smoldot",
836 format!(
837 "Chain specification of {} contains a list of bad blocks. Bad \
838 blocks are not implemented in the light client. An appropriate \
839 way to silence this warning is to remove the bad blocks from the \
840 chain specification, which can safely be done:\n\
841 - For relay chains: if the chain specification contains a \
842 checkpoint and that the bad blocks have a block number inferior \
843 to this checkpoint.\n\
844 - For parachains: if the bad blocks have a block number inferior \
845 to the current parachain finalized block.",
846 log_name
847 )
848 );
849 }
850
851 if database_was_wrong_chain {
852 log!(
853 &self.platform,
854 Warn,
855 "smoldot",
856 format!(
857 "Ignore database of {} because its genesis hash didn't match the \
858 genesis hash of the chain.",
859 log_name
860 )
861 )
862 }
863
864 let entry = entry.insert(RunningChain {
865 services,
866 log_name,
867 num_references: NonZero::<u32>::new(1).unwrap(),
868 });
869
870 (&mut entry.services, &entry.log_name)
871 }
872 };
873
874 if !invalid_bootstrap_nodes_sanitized.is_empty() {
875 log!(
876 &self.platform,
877 Warn,
878 "smoldot",
879 format!(
880 "Failed to parse some of the bootnodes of {}. \
881 These bootnodes have been ignored. List: {}",
882 log_name,
883 invalid_bootstrap_nodes_sanitized.join(", ")
884 )
885 );
886 }
887
888 // Print a warning if the list of bootnodes is empty, as this is a common mistake.
889 if bootstrap_nodes.is_empty() {
890 // Note the usage of the word "likely", because another chain with the same key might
891 // have been added earlier and contains bootnodes, or we might receive an incoming
892 // substream on a connection normally used for a different chain.
893 log!(
894 &self.platform,
895 Warn,
896 "smoldot",
897 format!(
898 "Newly-added chain {} has an empty list of bootnodes. Smoldot will \
899 likely fail to connect to its peer-to-peer network.",
900 log_name
901 )
902 );
903 }
904
905 // Apart from its services, each chain also has an entry in `public_api_chains`.
906 let public_api_chains_entry = self.public_api_chains.vacant_entry();
907 let new_chain_id = ChainId(public_api_chains_entry.key());
908
909 // Multiple chains can share the same network service, but each specify different
910 // bootstrap nodes and database nodes. In order to resolve this, each chain adds their own
911 // bootnodes and database nodes to the network service after it has been initialized. This
912 // is done by adding a short-lived task that waits for the chain initialization to finish
913 // then adds the nodes.
914 self.platform
915 .spawn_task("network-service-add-initial-topology".into(), {
916 let network_service = services.network_service.clone();
917 async move {
918 network_service.discover(known_nodes, false).await;
919 network_service.discover(bootstrap_nodes, true).await;
920 }
921 });
922
923 // JSON-RPC service initialization. This is done every time `add_chain` is called, even
924 // if a similar chain already existed.
925 let json_rpc_frontend = if let AddChainConfigJsonRpc::Enabled {
926 max_pending_requests,
927 max_subscriptions,
928 } = config.json_rpc
929 {
930 let frontend = json_rpc_service::service(json_rpc_service::Config {
931 platform: self.platform.clone(),
932 log_name: log_name.clone(), // TODO: add a way to differentiate multiple different json-rpc services under the same chain
933 max_pending_requests,
934 max_subscriptions,
935 sync_service: services.sync_service.clone(),
936 network_service: services.network_service.clone(),
937 transactions_service: services.transactions_service.clone(),
938 runtime_service: services.runtime_service.clone(),
939 bitswap_service: services.bitswap_service.clone(),
940 chain_name: chain_spec.name().to_owned(),
941 chain_ty: chain_spec.chain_type().to_owned(),
942 chain_is_live: chain_spec.has_live_network(),
943 chain_properties_json: chain_spec.properties().to_owned(),
944 system_name: self.platform.client_name().into_owned(),
945 system_version: self.platform.client_version().into_owned(),
946 genesis_block_hash,
947 statement_protocol_config,
948 max_seen_statements,
949 });
950
951 Some(frontend)
952 } else {
953 None
954 };
955
956 // Success!
957 let public_api_chain_destroyed_event = event_listener::Event::new();
958 let public_api_chain_destroyed = public_api_chain_destroyed_event.listen();
959 public_api_chains_entry.insert(PublicApiChain {
960 user_data: config.user_data,
961 key: new_chain_key,
962 chain_spec_chain_id,
963 json_rpc_frontend: json_rpc_frontend.clone(),
964 public_api_chain_destroyed_event,
965 });
966 Ok(AddChainSuccess {
967 chain_id: new_chain_id,
968 json_rpc_responses: json_rpc_frontend.map(|f| JsonRpcResponses {
969 inner: Some(f),
970 public_api_chain_destroyed,
971 }),
972 })
973 }
974
975 /// Removes the chain from smoldot. This instantaneously and silently cancels all on-going
976 /// JSON-RPC requests and subscriptions.
977 ///
978 /// The provided [`ChainId`] is now considered dead. Be aware that this same [`ChainId`] might
979 /// later be reused if [`Client::add_chain`] is called again.
980 ///
981 /// While from the API perspective it will look like the chain no longer exists, calling this
982 /// function will not actually immediately disconnect from the given chain if it is still used
983 /// as the relay chain of a parachain.
984 ///
985 /// If the [`JsonRpcResponses`] object that was returned when adding the chain is still alive,
986 /// [`JsonRpcResponses::next`] will now return `None`.
987 #[must_use]
988 pub fn remove_chain(&mut self, id: ChainId) -> TChain {
989 let removed_chain = self.public_api_chains.remove(id.0);
990
991 removed_chain
992 .public_api_chain_destroyed_event
993 .notify(usize::MAX);
994
995 // `chains_by_key` is created lazily when `add_chain` is called.
996 // Since we're removing a chain that has been added with `add_chain`, it is guaranteed
997 // that `chains_by_key` is set.
998 let chains_by_key = self
999 .chains_by_key
1000 .as_mut()
1001 .unwrap_or_else(|| unreachable!());
1002
1003 let running_chain = chains_by_key.get_mut(&removed_chain.key).unwrap();
1004 if running_chain.num_references.get() == 1 {
1005 log!(
1006 &self.platform,
1007 Info,
1008 "smoldot",
1009 format!("Shutting down chain {}", running_chain.log_name)
1010 );
1011 chains_by_key.remove(&removed_chain.key);
1012 } else {
1013 running_chain.num_references =
1014 NonZero::<u32>::new(running_chain.num_references.get() - 1).unwrap();
1015 }
1016
1017 self.public_api_chains.shrink_to_fit();
1018
1019 removed_chain.user_data
1020 }
1021
1022 /// Enqueues a JSON-RPC request towards the given chain.
1023 ///
1024 /// Since most JSON-RPC requests can only be answered asynchronously, the request is only
1025 /// queued and will be decoded and processed later.
1026 ///
1027 /// Returns an error if the number of requests that have been sent but whose answer hasn't been
1028 /// pulled with [`JsonRpcResponses::next`] is superior or equal to the value that was passed
1029 /// through [`AddChainConfigJsonRpc::Enabled::max_pending_requests`]. In that situation, the
1030 /// API user is encouraged to stop sending requests and start pulling answers with
1031 /// [`JsonRpcResponses::next`].
1032 ///
1033 /// Passing `u32::MAX` to [`AddChainConfigJsonRpc::Enabled::max_pending_requests`] is
1034 /// a good way to avoid errors here, but this should only be done if the JSON-RPC client is
1035 /// trusted.
1036 ///
1037 /// If the JSON-RPC request is not a valid JSON-RPC request, a JSON-RPC error response with
1038 /// an `id` equal to `null` is later generated, in accordance with the JSON-RPC specification.
1039 ///
1040 /// # Panic
1041 ///
1042 /// Panics if the [`ChainId`] is invalid, or if [`AddChainConfig::json_rpc`] was
1043 /// [`AddChainConfigJsonRpc::Disabled`] when adding the chain.
1044 ///
1045 pub fn json_rpc_request(
1046 &mut self,
1047 json_rpc_request: impl Into<String>,
1048 chain_id: ChainId,
1049 ) -> Result<(), HandleRpcError> {
1050 self.json_rpc_request_inner(json_rpc_request.into(), chain_id)
1051 }
1052
1053 fn json_rpc_request_inner(
1054 &mut self,
1055 json_rpc_request: String,
1056 chain_id: ChainId,
1057 ) -> Result<(), HandleRpcError> {
1058 let json_rpc_sender = match self
1059 .public_api_chains
1060 .get_mut(chain_id.0)
1061 .unwrap()
1062 .json_rpc_frontend
1063 {
1064 Some(ref mut json_rpc_sender) => json_rpc_sender,
1065 _ => panic!(),
1066 };
1067
1068 json_rpc_sender.queue_rpc_request(json_rpc_request)
1069 }
1070}
1071
1072impl<TPlat: platform::PlatformRef, TChain> ops::Index<ChainId> for Client<TPlat, TChain> {
1073 type Output = TChain;
1074
1075 fn index(&self, index: ChainId) -> &Self::Output {
1076 &self.public_api_chains.get(index.0).unwrap().user_data
1077 }
1078}
1079
1080impl<TPlat: platform::PlatformRef, TChain> ops::IndexMut<ChainId> for Client<TPlat, TChain> {
1081 fn index_mut(&mut self, index: ChainId) -> &mut Self::Output {
1082 &mut self.public_api_chains.get_mut(index.0).unwrap().user_data
1083 }
1084}
1085
1086/// Error potentially returned by [`Client::add_chain`].
1087#[derive(Debug, derive_more::Display, derive_more::Error)]
1088pub enum AddChainError {
1089 /// Failed to decode the specification of the chain.
1090 #[display("Failed to decode chain specification: {_0}")]
1091 ChainSpecParseError(chain_spec::ParseError),
1092 /// The chain specification must contain either the storage of the genesis block, or a
1093 /// checkpoint. Neither was provided.
1094 #[display("Either a checkpoint or the genesis storage must be provided")]
1095 ChainSpecNeitherGenesisStorageNorCheckpoint,
1096 /// Checkpoint provided in the chain specification is invalid.
1097 #[display("Invalid checkpoint in chain specification: {_0}")]
1098 InvalidCheckpoint(chain_spec::CheckpointToChainInformationError),
1099 /// Failed to build the information about the chain from the genesis storage. This indicates
1100 /// invalid data in the genesis storage.
1101 #[display("Failed to build genesis chain information: {_0}")]
1102 InvalidGenesisStorage(chain_spec::FromGenesisStorageError),
1103 /// The list of potential relay chains doesn't contain any relay chain with the name indicated
1104 /// in the chain specification of the parachain.
1105 #[display("Couldn't find relevant relay chain")]
1106 NoRelayChainFound,
1107 /// The list of potential relay chains contains more than one relay chain with the name
1108 /// indicated in the chain specification of the parachain.
1109 #[display("Multiple relevant relay chains found")]
1110 MultipleRelayChains,
1111}
1112
1113enum StartServicesChainTy<'a, TPlat: platform::PlatformRef> {
1114 SubstrateCompatible {
1115 chain_information: &'a chain::chain_information::ValidChainInformation,
1116 },
1117 Parachain {
1118 relay_chain: &'a ChainServices<TPlat>,
1119 para_id: u32,
1120 },
1121}
1122
1123/// Starts all the services of the client.
1124///
1125/// Returns some of the services that have been started. If these service get shut down, all the
1126/// other services will later shut down as well.
1127fn start_services<TPlat: platform::PlatformRef>(
1128 log_name: String,
1129 platform: &TPlat,
1130 network_service: &mut Option<Arc<network_service::NetworkService<TPlat>>>,
1131 runtime_code_hint: Option<database::DatabaseContentRuntimeCodeHint>,
1132 genesis_block_scale_encoded_header: Vec<u8>,
1133 block_number_bytes: usize,
1134 fork_id: Option<String>,
1135 config: StartServicesChainTy<'_, TPlat>,
1136 network_identify_agent_version: String,
1137 statement_protocol_config: Option<network_service::StatementProtocolConfig>,
1138) -> ChainServices<TPlat> {
1139 let network_service = network_service.get_or_insert_with(|| {
1140 network_service::NetworkService::new(network_service::Config {
1141 platform: platform.clone(),
1142 identify_agent_version: network_identify_agent_version,
1143 connections_open_pool_size: 8,
1144 connections_open_pool_restore_delay: Duration::from_millis(100),
1145 chains_capacity: 1,
1146 })
1147 });
1148
1149 let network_service_chain = network_service.add_chain(network_service::ConfigChain {
1150 log_name: log_name.clone(),
1151 num_out_slots: 4,
1152 grandpa_protocol_finalized_block_height: match &config {
1153 StartServicesChainTy::SubstrateCompatible { chain_information }
1154 if matches!(
1155 chain_information.as_ref().finality,
1156 chain::chain_information::ChainInformationFinalityRef::Grandpa { .. }
1157 ) =>
1158 {
1159 Some(chain_information.as_ref().finalized_block_header.number)
1160 }
1161 _ => None,
1162 },
1163 genesis_block_hash: header::hash_from_scale_encoded_header(
1164 &genesis_block_scale_encoded_header,
1165 ),
1166 best_block: match &config {
1167 StartServicesChainTy::SubstrateCompatible { chain_information } => (
1168 chain_information.as_ref().finalized_block_header.number,
1169 chain_information
1170 .as_ref()
1171 .finalized_block_header
1172 .hash(block_number_bytes),
1173 ),
1174 _ => (
1175 0,
1176 header::hash_from_scale_encoded_header(&genesis_block_scale_encoded_header),
1177 ),
1178 },
1179 fork_id,
1180 block_number_bytes,
1181 statement_protocol_config,
1182 });
1183
1184 let (sync_service, runtime_service) = match config {
1185 StartServicesChainTy::Parachain {
1186 relay_chain,
1187 para_id,
1188 } => {
1189 // Chain is a parachain.
1190
1191 // The sync service is leveraging the network service, downloads block headers,
1192 // and verifies them, to determine what are the best and finalized blocks of the
1193 // chain.
1194 let sync_service = Arc::new(sync_service::SyncService::new(sync_service::Config {
1195 platform: platform.clone(),
1196 log_name: log_name.clone(),
1197 block_number_bytes,
1198 network_service: network_service_chain.clone(),
1199 chain_type: sync_service::ConfigChainType::Parachain(
1200 sync_service::ConfigParachain {
1201 relay_chain: sync_service::ConfigRelayChain {
1202 para_id,
1203 relay_chain_sync: relay_chain.runtime_service.clone(),
1204 },
1205 },
1206 ),
1207 }));
1208
1209 // The runtime service follows the runtime of the best block of the chain,
1210 // and allows performing runtime calls.
1211 let runtime_service = Arc::new(runtime_service::RuntimeService::new(
1212 runtime_service::Config {
1213 log_name: log_name.clone(),
1214 platform: platform.clone(),
1215 sync_service: sync_service.clone(),
1216 network_service: network_service_chain.clone(),
1217 genesis_block_scale_encoded_header,
1218 },
1219 ));
1220
1221 (sync_service, runtime_service)
1222 }
1223 StartServicesChainTy::SubstrateCompatible { chain_information } => {
1224 // Chain is a Substrate-compatible non-parachain chain.
1225
1226 // The sync service is leveraging the network service, downloads block headers,
1227 // and verifies them, to determine what are the best and finalized blocks of the
1228 // chain.
1229 let sync_service = Arc::new(sync_service::SyncService::new(sync_service::Config {
1230 log_name: log_name.clone(),
1231 block_number_bytes,
1232 platform: platform.clone(),
1233 network_service: network_service_chain.clone(),
1234 chain_type: sync_service::ConfigChainType::SubstrateCompatible(
1235 sync_service::ConfigSubstrateCompatible {
1236 chain_information: chain_information.clone(),
1237 runtime_code_hint: runtime_code_hint.map(|hint| {
1238 sync_service::ConfigSubstrateCompatibleRuntimeCodeHint {
1239 storage_value: hint.code,
1240 merkle_value: hint.code_merkle_value,
1241 closest_ancestor_excluding: hint.closest_ancestor_excluding,
1242 }
1243 }),
1244 },
1245 ),
1246 }));
1247
1248 // The runtime service follows the runtime of the best block of the chain,
1249 // and allows performing runtime calls.
1250 let runtime_service = Arc::new(runtime_service::RuntimeService::new(
1251 runtime_service::Config {
1252 log_name: log_name.clone(),
1253 platform: platform.clone(),
1254 sync_service: sync_service.clone(),
1255 network_service: network_service_chain.clone(),
1256 genesis_block_scale_encoded_header,
1257 },
1258 ));
1259
1260 (sync_service, runtime_service)
1261 }
1262 };
1263
1264 // The transactions service lets one send transactions to the peer-to-peer network and watch
1265 // them being included in the chain.
1266 // While this service is in principle not needed if it is known ahead of time that no
1267 // transaction will be submitted, the service itself is pretty low cost.
1268 let transactions_service = Arc::new(transactions_service::TransactionsService::new(
1269 transactions_service::Config {
1270 log_name: log_name.clone(),
1271 platform: platform.clone(),
1272 sync_service: sync_service.clone(),
1273 runtime_service: runtime_service.clone(),
1274 network_service: network_service_chain.clone(),
1275 max_pending_transactions: NonZero::<u32>::new(64).unwrap(),
1276 max_concurrent_downloads: NonZero::<u32>::new(3).unwrap(),
1277 max_concurrent_validations: NonZero::<u32>::new(2).unwrap(),
1278 },
1279 ));
1280
1281 // The Bitswap service fulfils `bitswap_v1_get(cid)` JSON-RPC requests by querying remote
1282 // nodes for IPFS blocks.
1283 let bitswap_service = Arc::new(bitswap_service::BitswapService::new(
1284 bitswap_service::Config {
1285 log_name,
1286 platform: platform.clone(),
1287 network_service: network_service_chain.clone(),
1288 },
1289 ));
1290
1291 ChainServices {
1292 network_service: network_service_chain,
1293 runtime_service,
1294 sync_service,
1295 transactions_service,
1296 bitswap_service,
1297 }
1298}