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