lightclient: Add support for multi-chain usecase (#1238)

* lightclient: Make `smoldot::chainID` part of the RPC requests

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Make `BackgroundTask` generic over `PlatformRef` and chain

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Construct from raw smoldot and target different chains

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* testing: Update cargo lock for wasm tests

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Reuse `new_from_client` method and removed unused imports

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Reexport smoldot client and RPC objects used in pub
interface

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Adjust `new_from_client` interface

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Extend background to poll over multiple RPC objects

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Build light client from raw and target different chains

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* artifacts: Add demo chain specs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* artifacts: Move artifacts to dedicated folder

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Use SelectAll to drive all streams

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Fetch initial data from the target chain

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Reexport other smoldot objects

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Target chain with potentially different config

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt/rpc: Log chainID for debugging

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt/examples: Add smoldot client with parachain example

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Propagate chain ID together with rpc responses object

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Multiplex responses by request ID and chain ID

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Add raw light client builder

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Add cargo feature flag for parachains example

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Derive default for internal structure

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Guard reexports by std feature flag

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update subxt/src/client/light_client/mod.rs

Co-authored-by: James Wilson <james@jsdw.me>

* lightclient: Update the builder pattern and chain targetting

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Fix documentation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Provide more insightful docs wrt native/wasm panics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Adjust comment location

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Refactor UniqueChainId into the background task

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update lightclient/src/background.rs

Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>

* Update subxt/src/client/light_client/builder.rs

Co-authored-by: James Wilson <james@jsdw.me>

* lightclient: Update docs wrt panics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Update docs wrt to smoldot instance -> client

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Use IntoIter instead of Iterator

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Adjsut docs wrt [`Self::new_from_client`]

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Remove RawRpc from LightClient in favor of chainID

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* lightclient: Reexport everything under smoldot module

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* artifacts: Use stateRootHash instead of genesis.raw

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: James Wilson <james@jsdw.me>
Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>
This commit is contained in:
Alexandru Vasile
2023-11-16 18:29:00 +02:00
committed by GitHub
parent 7b210f5a8e
commit 34ff3da37d
11 changed files with 606 additions and 104 deletions
+89 -11
View File
@@ -4,9 +4,10 @@
use super::{rpc::LightClientRpc, LightClient, LightClientError};
use crate::backend::rpc::RpcClient;
use crate::client::RawLightClient;
use crate::{config::Config, error::Error, OnlineClient};
use std::num::NonZeroU32;
use subxt_lightclient::{AddChainConfig, AddChainConfigJsonRpc, ChainId};
use subxt_lightclient::{smoldot, AddedChain};
/// Builder for [`LightClient`].
#[derive(Clone, Debug)]
@@ -14,7 +15,7 @@ pub struct LightClientBuilder<T: Config> {
max_pending_requests: NonZeroU32,
max_subscriptions: u32,
bootnodes: Option<Vec<serde_json::Value>>,
potential_relay_chains: Option<Vec<ChainId>>,
potential_relay_chains: Option<Vec<smoldot::ChainId>>,
_marker: std::marker::PhantomData<T>,
}
@@ -77,7 +78,7 @@ impl<T: Config> LightClientBuilder<T> {
/// be wrong to connect to the "Kusama" created by user A.
pub fn potential_relay_chains(
mut self,
potential_relay_chains: impl IntoIterator<Item = ChainId>,
potential_relay_chains: impl IntoIterator<Item = smoldot::ChainId>,
) -> Self {
self.potential_relay_chains = Some(potential_relay_chains.into_iter().collect());
self
@@ -88,7 +89,16 @@ impl<T: Config> LightClientBuilder<T> {
///
/// ## Panics
///
/// Panics if being called outside of `tokio` runtime context.
/// The panic behaviour depends on the feature flag being used:
///
/// ### Native
///
/// Panics when called outside of a `tokio` runtime context.
///
/// ### Web
///
/// If smoldot panics, then the promise created will be leaked. For more details, see
/// https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/fn.future_to_promise.html.
#[cfg(feature = "jsonrpsee")]
pub async fn build_from_url<Url: AsRef<str>>(self, url: Url) -> Result<LightClient<T>, Error> {
let chain_spec = fetch_url(url.as_ref()).await?;
@@ -104,7 +114,7 @@ impl<T: Config> LightClientBuilder<T> {
///
/// The chain spec must be obtained from a trusted entity.
///
/// It can be fetched from a trused node with the following command:
/// It can be fetched from a trusted node with the following command:
/// ```bash
/// curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "sync_state_genSyncSpec", "params":[true]}' http://localhost:9944/ | jq .result > res.spec
/// ```
@@ -116,7 +126,16 @@ impl<T: Config> LightClientBuilder<T> {
///
/// ## Panics
///
/// Panics if being called outside of `tokio` runtime context.
/// The panic behaviour depends on the feature flag being used:
///
/// ### Native
///
/// Panics when called outside of a `tokio` runtime context.
///
/// ### Web
///
/// If smoldot panics, then the promise created will be leaked. For more details, see
/// https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/fn.future_to_promise.html.
pub async fn build(self, chain_spec: &str) -> Result<LightClient<T>, Error> {
let chain_spec = serde_json::from_str(chain_spec)
.map_err(|_| Error::LightClient(LightClientError::InvalidChainSpec))?;
@@ -136,9 +155,9 @@ impl<T: Config> LightClientBuilder<T> {
}
}
let config = AddChainConfig {
let config = smoldot::AddChainConfig {
specification: &chain_spec.to_string(),
json_rpc: AddChainConfigJsonRpc::Enabled {
json_rpc: smoldot::AddChainConfigJsonRpc::Enabled {
max_pending_requests: self.max_pending_requests,
max_subscriptions: self.max_subscriptions,
},
@@ -147,12 +166,71 @@ impl<T: Config> LightClientBuilder<T> {
user_data: (),
};
let rpc_client = RpcClient::new(LightClientRpc::new(config)?);
let online_client = OnlineClient::<T>::from_rpc_client(rpc_client).await?;
Ok(LightClient(online_client))
let raw_rpc = LightClientRpc::new(config)?;
build_client_from_rpc(raw_rpc).await
}
}
/// Raw builder for [`RawLightClient`].
pub struct RawLightClientBuilder {
chains: Vec<AddedChain>,
}
impl Default for RawLightClientBuilder {
fn default() -> Self {
Self { chains: Vec::new() }
}
}
impl RawLightClientBuilder {
/// Create a new [`RawLightClientBuilder`].
pub fn new() -> RawLightClientBuilder {
RawLightClientBuilder::default()
}
/// Adds a new chain to the list of chains synchronized by the light client.
pub fn add_chain(
mut self,
chain_id: smoldot::ChainId,
rpc_responses: smoldot::JsonRpcResponses,
) -> Self {
self.chains.push(AddedChain {
chain_id,
rpc_responses,
});
self
}
/// Construct a [`RawLightClient`] from a raw smoldot client.
///
/// The provided `chain_id` is the chain with which the current instance of light client will interact.
/// To target a different chain call the [`LightClient::target_chain`] method.
pub async fn build<TPlatform: smoldot::PlatformRef>(
self,
client: smoldot::Client<TPlatform>,
) -> Result<RawLightClient, Error> {
// The raw subxt light client that spawns the smoldot background task.
let raw_rpc: subxt_lightclient::RawLightClientRpc =
subxt_lightclient::LightClientRpc::new_from_client(client, self.chains.into_iter());
// The crate implementation of `RpcClientT` over the raw subxt light client.
let raw_rpc = crate::client::light_client::rpc::RawLightClientRpc::from_inner(raw_rpc);
Ok(RawLightClient { raw_rpc })
}
}
/// Build the light client from a raw rpc client.
async fn build_client_from_rpc<T: Config>(
raw_rpc: LightClientRpc,
) -> Result<LightClient<T>, Error> {
let chain_id = raw_rpc.chain_id();
let rpc_client = RpcClient::new(raw_rpc);
let client = OnlineClient::<T>::from_rpc_client(rpc_client).await?;
Ok(LightClient { client, chain_id })
}
/// Fetch the chain spec from the URL.
#[cfg(feature = "jsonrpsee")]
async fn fetch_url(url: impl AsRef<str>) -> Result<serde_json::Value, Error> {
+62 -6
View File
@@ -8,6 +8,7 @@ mod builder;
mod rpc;
use crate::{
backend::rpc::RpcClient,
blocks::BlocksClient,
client::{OfflineClientT, OnlineClientT},
config::Config,
@@ -19,10 +20,13 @@ use crate::{
tx::TxClient,
OnlineClient,
};
pub use builder::LightClientBuilder;
pub use builder::{LightClientBuilder, RawLightClientBuilder};
use derivative::Derivative;
use subxt_lightclient::LightClientRpcError;
// Re-export smoldot related objects.
pub use subxt_lightclient::smoldot;
/// Light client error.
#[derive(Debug, thiserror::Error)]
pub enum LightClientError {
@@ -51,10 +55,57 @@ pub enum LightClientError {
Handshake,
}
/// The raw light-client RPC implementation that is used to connect with the chain.
#[derive(Clone)]
pub struct RawLightClient {
raw_rpc: rpc::RawLightClientRpc,
}
impl RawLightClient {
/// Construct a [`RawLightClient`] using its builder interface.
///
/// The raw builder is utilized for constructing light-clients from a low
/// level smoldot client.
///
/// This is especially useful when you want to gain access to the smoldot client.
/// For example, you may want to connect to multiple chains and/or parachains while reusing the
/// same smoldot client under the hood. Or you may want to configure different values for
/// smoldot internal buffers, number of subscriptions and relay chains.
///
/// # Note
///
/// If you are unsure, please use [`LightClient::builder`] instead.
pub fn builder() -> RawLightClientBuilder {
RawLightClientBuilder::default()
}
/// Target a different chain identified by the provided chain ID for requests.
///
/// The provided chain ID is provided by the `smoldot_light::Client::add_chain` and it must
/// match one of the `smoldot_light::JsonRpcResponses` provided in [`RawLightClientBuilder::add_chain`].
///
/// # Note
///
/// This uses the same underlying instance spawned by the builder.
pub async fn for_chain<TChainConfig: Config>(
&self,
chain_id: smoldot::ChainId,
) -> Result<LightClient<TChainConfig>, crate::Error> {
let raw_rpc = self.raw_rpc.for_chain(chain_id);
let rpc_client = RpcClient::new(raw_rpc.clone());
let client = OnlineClient::<TChainConfig>::from_rpc_client(rpc_client).await?;
Ok(LightClient { client, chain_id })
}
}
/// The light-client RPC implementation that is used to connect with the chain.
#[derive(Derivative)]
#[derivative(Clone(bound = ""))]
pub struct LightClient<T: Config>(OnlineClient<T>);
pub struct LightClient<T: Config> {
client: OnlineClient<T>,
chain_id: smoldot::ChainId,
}
impl<T: Config> LightClient<T> {
/// Construct a [`LightClient`] using its builder interface.
@@ -68,17 +119,17 @@ impl<T: Config> LightClient<T> {
/// Return the [`crate::Metadata`] used in this client.
fn metadata(&self) -> crate::Metadata {
self.0.metadata()
self.client.metadata()
}
/// Return the genesis hash.
fn genesis_hash(&self) -> <T as Config>::Hash {
self.0.genesis_hash()
self.client.genesis_hash()
}
/// Return the runtime version.
fn runtime_version(&self) -> crate::backend::RuntimeVersion {
self.0.runtime_version()
self.client.runtime_version()
}
/// Work with transactions.
@@ -115,11 +166,16 @@ impl<T: Config> LightClient<T> {
pub fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
<Self as OfflineClientT<T>>::runtime_api(self)
}
/// Returns the chain ID of the current light-client.
pub fn chain_id(&self) -> smoldot::ChainId {
self.chain_id
}
}
impl<T: Config> OnlineClientT<T> for LightClient<T> {
fn backend(&self) -> &dyn crate::backend::Backend<T> {
self.0.backend()
self.client.backend()
}
}
+42 -10
View File
@@ -2,17 +2,32 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::LightClientError;
use super::{smoldot, LightClientError};
use crate::{
backend::rpc::{RawRpcFuture, RawRpcSubscription, RpcClientT},
error::{Error, RpcError},
};
use futures::StreamExt;
use serde_json::value::RawValue;
use subxt_lightclient::{AddChainConfig, ChainId, LightClientRpcError};
use tokio_stream::wrappers::UnboundedReceiverStream;
pub const LOG_TARGET: &str = "light-client";
pub const LOG_TARGET: &str = "subxt-rpc-light-client";
/// The raw light-client RPC implementation that is used to connect with the chain.
#[derive(Clone)]
pub struct RawLightClientRpc(subxt_lightclient::RawLightClientRpc);
impl RawLightClientRpc {
/// Constructs a new [`RawLightClientRpc`] from a low level [`subxt_lightclient::RawLightClientRpc`].
pub fn from_inner(client: subxt_lightclient::RawLightClientRpc) -> RawLightClientRpc {
RawLightClientRpc(client)
}
/// Constructs a new [`LightClientRpc`] that communicates with the provided chain.
pub fn for_chain(&self, chain_id: smoldot::ChainId) -> LightClientRpc {
LightClientRpc(self.0.for_chain(chain_id))
}
}
/// The light-client RPC implementation that is used to connect with the chain.
#[derive(Clone)]
@@ -28,15 +43,29 @@ impl LightClientRpc {
///
/// ## Panics
///
/// Panics if being called outside of `tokio` runtime context.
/// The panic behaviour depends on the feature flag being used:
///
/// ### Native
///
/// Panics when called outside of a `tokio` runtime context.
///
/// ### Web
///
/// If smoldot panics, then the promise created will be leaked. For more details, see
/// https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/fn.future_to_promise.html.
pub fn new(
config: AddChainConfig<'_, (), impl Iterator<Item = ChainId>>,
config: smoldot::AddChainConfig<'_, (), impl Iterator<Item = smoldot::ChainId>>,
) -> Result<LightClientRpc, Error> {
let rpc = subxt_lightclient::LightClientRpc::new(config)
.map_err(|err| LightClientError::Rpc(err))?;
Ok(LightClientRpc(rpc))
}
/// Returns the chain ID of the current light-client.
pub fn chain_id(&self) -> smoldot::ChainId {
self.0.chain_id()
}
}
impl RpcClientT for LightClientRpc {
@@ -46,6 +75,7 @@ impl RpcClientT for LightClientRpc {
params: Option<Box<RawValue>>,
) -> RawRpcFuture<'a, Box<RawValue>> {
let client = self.clone();
let chain_id = self.chain_id();
Box::pin(async move {
let params = match params {
@@ -66,7 +96,7 @@ impl RpcClientT for LightClientRpc {
.await
.map_err(|_| RpcError::ClientError(Box::new(LightClientError::BackgroundClosed)))?;
tracing::trace!(target: LOG_TARGET, "RPC response {:?}", response);
tracing::trace!(target: LOG_TARGET, "RPC response={:?} chain={:?}", response, chain_id);
response.map_err(|err| RpcError::ClientError(Box::new(err)))
})
@@ -79,13 +109,15 @@ impl RpcClientT for LightClientRpc {
_unsub: &'a str,
) -> RawRpcFuture<'a, RawRpcSubscription> {
let client = self.clone();
let chain_id = self.chain_id();
Box::pin(async move {
tracing::trace!(
target: LOG_TARGET,
"Subscribe to {:?} with params {:?}",
"Subscribe to {:?} with params {:?} chain={:?}",
sub,
params
params,
chain_id,
);
let params = match params {
@@ -107,7 +139,7 @@ impl RpcClientT for LightClientRpc {
.map_err(|_| RpcError::ClientError(Box::new(LightClientError::BackgroundClosed)))?
.map_err(|err| {
RpcError::ClientError(Box::new(LightClientError::Rpc(
LightClientRpcError::Request(err.to_string()),
subxt_lightclient::LightClientRpcError::Request(err.to_string()),
)))
})?;
@@ -116,7 +148,7 @@ impl RpcClientT for LightClientRpc {
.trim_start_matches('"')
.trim_end_matches('"')
.to_string();
tracing::trace!(target: LOG_TARGET, "Received subscription ID: {}", sub_id);
tracing::trace!(target: LOG_TARGET, "Received subscription={} chain={:?}", sub_id, chain_id);
let stream = UnboundedReceiverStream::new(notif);
+3 -1
View File
@@ -20,4 +20,6 @@ pub use online_client::{
};
#[cfg(feature = "unstable-light-client")]
pub use light_client::{LightClient, LightClientBuilder, LightClientError};
pub use light_client::{
LightClient, LightClientBuilder, LightClientError, RawLightClient, RawLightClientBuilder,
};