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