Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework light client #1475

Merged
merged 17 commits into from
Mar 15, 2024
Merged

Rework light client #1475

merged 17 commits into from
Mar 15, 2024

Conversation

jsdw
Copy link
Collaborator

@jsdw jsdw commented Mar 11, 2024

The main changes:

  • Move almost all light client code into subxt_lightclient; only a little glue in Subxt to integrate it.
  • LightClient struct to create light client instance given relay chain spec, and to create parachain connections on top of this.
  • LightClientRpc returned from each of the above which is the means to interact with the chain.
  • Subxt implements RpcClientT for this, so that it can be used to construct an OnlineClient.
  • Function provided for low level control but not exported yet (maybe best to hide Smoldot away unless somebody has a good reason to need to configure it directly.
  • Each light client RPC connection has an associated BackgroundTask. This isn't exposed yet, but can eventually be in order that users can drive it and exert backpressure if need be (I'm unsure what effect that would have on Smoldot though!).

Other changes along the way:

  • light client background task is now per-chain-connection so no need to combine and then separate messages from different chains, and in the future we can opt to drive each one manually if we want.
  • Handle error messages on subscriptions, too.
  • Generally split errors up so that we get quite specific erros back when constructing, calling or getting responses back from the light client stuff.

Things not done:

  • Exposing something like OnlineClient::from_chain_spec(); need a relay + parachain version etc, prob need a builder or something to make it nice. Instead I made it easier to instantiate an OnlineClient from an RPC client (impl Into) so it's actually pretty easy anyway now.
  • I didn't add a nice from_url to the ChainSpecConfig. I staretd doing this but it involved pulling in extra deps and feature flags etc, and the code was just easier to keep in subxt. So I exposed a utility function to fetch chain spec from url instead. I'm ok with this because it's actually only really a dev use case anyway; we prob shouldn't encourage it too much!

>;

/// Type of subscription IDs we can get back.
pub type SubscriptionId = String;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was previously an unsigned int, but even if Smoldot currently does that, I didn't think we should rely on it since it probably also expects us to treat the valeus as opaque.

to_backend: mpsc::UnboundedSender<Message>,
}

impl BackgroundTaskHandle {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this to give a nice "API" to interact with the background task, so that nothing outside of the backend task actually needs to care about messages and such

/// Run the background task, which:
/// - Forwards messages/subscription requests to Smoldot from the front end.
/// - Forwards responses back from Smoldot to the front end.
pub async fn run(self) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simplified version of what was called start_task before. No longer takes in any args, so the API for BackgroundTask is super simple; just .run().await it to start it running (and in the future we can return something users can poll which will just wrap this).

let from_back_fut = channels.from_back.next().fuse();
}

futures::select! {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved to this I think to avoid allocating somewhere or annoying lifetime things; can't remember exactly now. It just takes two channels as inputs though and so I think should have no cancellation concerns (this is the usual footgun with select!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, btw you asked me exactly this question "What is the usual usual footgun with select!" when interviewing me as a job applicant about a year ago.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to make a note somewhere because if I move to doing a code review style task then future::select! is def a candidate to put somewhere :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are good, streams are cancel-safe.

I like tokio::select better because it doesn't require pinning the futures

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I normally prefer the function call version to avoid some footguns but iirc I had an issue that made it hard to use it nicely; I'll have to try again at some point!

}

/// The state of the subscription.
struct ActiveSubscription {
/// Channel to send the subscription notifications back to frontend.
sender: mpsc::UnboundedSender<Box<RawValue>>,
notification_sender: mpsc::UnboundedSender<Result<Box<RawValue>, JsonRpcError>>,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tweaked subscriptions a bit to:

a) return any subscription notification errors that come back (previously these were being ignored I think) and
b) send back the ID and repsonse channel rather than one channel for ID and one for responses, just to simplify usage a little.

}

/// Configuration to connect to a chain.
pub struct ChainConfig<'a> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can grow to have more options like about max json RPC subs etc if we want to expose them. A user can then either build this config and configure it, or pass a chain spec &str directly, which will convert into this easily.

Comment on lines +32 to +39
NotificationError {
/// RPC method that generated the notification.
method: String,
/// Subscription ID.
subscription_id: String,
/// Result.
error: Box<RawValue>,
},
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is copied from the previous impl in background.rs, but I added this NotificationError to handle the case when we get errors back in subscriptions. Worth double checking that this is right :)


cfg_jsonrpsee! {
/// Fetch a chain spec from an RPC node at the given URL.
pub async fn fetch_chainspec_from_rpc_node(url: impl AsRef<str>) -> Result<Box<RawValue>, FetchChainspecError> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried putting a fn like this (from_url) on ChainConfig but it required pulling lots of deps in etc, and since it's a dev use case mostly I was sortof ok with just exposing this util function instead in Subxt, since it's easier to do it here. Not the cleanest though!

@jsdw jsdw marked this pull request as ready for review March 12, 2024 16:50
@jsdw jsdw requested a review from a team as a code owner March 12, 2024 16:50
// Connecting to a parachain is a multi step process.
// Instantiate a light client with the Polkadot relay chain,
// and connect it to Asset Hub, too.
let (lightclient, polkadot_rpc) = LightClient::relay_chain(POLKADOT_SPEC)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks amazing!

Copy link
Collaborator

@lexnv lexnv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I like the idea of having a background task per chain ID, this should isolate things quite nicely 👍

@jsdw
Copy link
Collaborator Author

jsdw commented Mar 14, 2024

I added back a compile error and docsrs thing that went missing, and updated the book, swapping the examples around so now connecting to parachains is the basic example, and connecting to a local node is more advanced :)

Copy link
Contributor

@tadeohepperle tadeohepperle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the interface is well designed, good job!

let from_back_fut = channels.from_back.next().fuse();
}

futures::select! {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, btw you asked me exactly this question "What is the usual usual footgun with select!" when interviewing me as a job applicant about a year ago.

lightclient/src/background.rs Show resolved Hide resolved
// Send the method response and a channel to receive notifications back.
if pending_subscription
.response_sender
.send(Ok((sub_id.clone(), sub_rx)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we avoid allocation here by using Arc<str> for subscription ids? Probably irrelevant though, feel free to ignore.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd guess it's not worth while because we (technically, but not really in reality) need to allocate a String anyway for the deserializing bit, and then the Arc is another alloc, and we onlyclone it once anyways :)

/// https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/fn.future_to_promise.html.
pub fn parachain<'a>(
&self,
chain_config: impl Into<ChainConfig<'a>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface with the relay_chain and parachain functions looks really clean.

@jsdw jsdw merged commit b069c44 into master Mar 15, 2024
13 checks passed
@jsdw jsdw deleted the jsdw-lightclient-2 branch March 15, 2024 15:21
@Polkadot-Forum
Copy link

This pull request has been mentioned on Polkadot Forum. There might be relevant details there:

https://forum.polkadot.network/t/parity-tech-update-for-march/7226/1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants