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

Introduce Backend trait to allow different RPC (or other) backends to be implemented #1126

Merged
merged 31 commits into from Aug 22, 2023

Conversation

jsdw
Copy link
Collaborator

@jsdw jsdw commented Aug 18, 2023

Previously, Subxt looked something like:

high level APIs <- RPC calls

This PR introduces a Backend trait inbetween those; a trait which provides the minimal set of things that the high level APIs need (while trying to cater for what the new RPCs can provide). It then implements that trait for the "legacy" API methods, and migrates the high level code to using the trait rather than directly calling methods (in fact; making this impossible). So now we have:

high level APIs <- backend implementation <- RPC calls

(Where the RPC calls are just an implementation detail of the backend implementation)

I'd suggest to look at the subxt/backend folder first to see what this trait looks like, then the subxt/backend/legacy to see the LegacyBackend implementation, which is based on the old RPC methods.

The idea is that we can add a v2 backend which is based on the new methods.

The examples are handy to look at next to see what's changed. Much of the rest of the code is fallout from adding this new backend thing!

There are a number of breaking changes here (mostly I'll cover below in the notes), but the high level APIs haven't changed significantly. Manually calling RPC methods is now more difficult, but still possible via creating an RpcClient manually, and using that to instantiate either/both of LegacyRpcMethods to make RPC calls and OnlineClient to do high level Subxt things.

Random Notes:

  • I've removed all of the chainHead RPC stuff at the moment. The plan is to put this stuff back as part of the new backend.
  • block.body().await.extrinsics() is now just block.extrinsics().await; the body of a block only contains extrinsics anyway (the new API inparticular only returns extrinsics, and not justifications, which we didn't expose anyway).
  • Filling in gaps in finalized blocks is now handled by the backend, whose job it is to return a full stream.
  • Things now take an RpcClient instead of an Arc<impl RpcClientT>. RpcClient::from_url replaces default_rpc_client(), or you can instantiate RpcClient from an underlying trait. I've done renames so that the raw trait stuff is better separated now (eg Raw* names) to make the difference clearer. One reason for this all is that calling RPC methods requires an RPC client, so it's easier if people can create a single RpcClient quite easily and then use that in the places it's needed).
  • We have a new BlockRef type that, in the new backend, can try to "hold onto" a block, ie keep it from being unpinned, if it's alive. Things that may need to get details at a particular block should now keep the corresponding BlockRef around until they've finished.
  • Storage changes include removing the explicit "page_count" from the user API; the backend will now determine the page size to use if applicable, renaming fetch_keys to fetch_keys_raw (it returns raw bytes like fetch_raw), and making iter() hand back a proper Stream which should make it more ergonomic (and changes results from being Result<Option<T>> to Option<Result<T>>). Currently it fetches keys in batches but values individually; we can look to improve that again though if needbe.
  • No more client.rpc(), because this is now an implementation detail of a specific Backend. Instead, you can OnlineClient::from_backend to pass a backend which has been instantiated with some RPC client that you can also use elsewhere.
  • tx.dry_run() is now tx.validate() and uses a runtime call for validating (in part because the new RPCs won't expose a dry run call). This returns a different result though, leading to breakage here, so I've also kept the RPC call around so that one can manually call that instead if they need the old behaviour.
  • tx.submit() used to directly call a submit RPC which handed the hash back. Now we delegate to submit_and_watch and return the hash once the first non-error status comes back, since the new APIs don't (currently) have a way to just submit a TX anyway.
  • The underlying TransactionProgress events are altered to better fit the new API results (omitting a couple of details that the old API can't replicate). The high level API around submitting and watching is otherwise unchanged.
  • Stream-like things always have a next() method on them instead of eg next_item() now, which is mostly just a convenience to avoid needing to import StreamExt for this common op.

@jsdw jsdw marked this pull request as ready for review August 18, 2023 18:16
@jsdw jsdw requested a review from a team as a code owner August 18, 2023 18:16
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 looked over the entire thing, looks good. From my understanding we will add a newer backend soon as a replacement of the LegacyBackend? If that is the case, what will be the mechanism by which it is determined what backend is used when an OnlineClient is constructed?

/// events will be emitted.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransactionStatus<Hash> {
/// Transaction is part of the future queue.
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the new TransactionStatus!

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's basically much more similar to the one that the new APIs will return, so making this change now means that hopefully we won't need to change anything else as we implement the new backend :)

/// Prevent the backend trait being implemented externally.
#[doc(hidden)]
pub(crate) mod sealed {
pub trait Sealed {}
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the reason to put Sealed into a separate mod? Would pub(crate) trait Sealed {} not suffice?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good question! This is a bit of a hack, because if you have pub (crate) trait Sealed on it's own then you run into:

crate-private trait `Sealed` in public interface
can't leak crate-private trait

But having the trait be fully pub (but in a module that people can't access, so the thing is pub but nobody can name it) circumvents this. It's weird, but it's a generally established pattern that crates use for this purpose :)

(I have only skimmed it but maybe eg https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/ is interesting; it also talks about another approach via methods that can't be implemented :))

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh that is very interesting!


/// Fetch system health
pub async fn system_health(&self) -> Result<SystemHealth, Error> {
self.client.request("system_health", rpc_params![]).await
Copy link
Contributor

Choose a reason for hiding this comment

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

I did not know these RPC methods were available, maybe we should add an example, that showcases all the little things we can fetch from a node?

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'm only really leaving them here because I felt a little bad stripping away all of the RPC methods that we don't use immediately, but in the long run we'll ultimately deprecate and remove these things once the new RPCs are stable and in widespread usage. For this reason, I'm happy to not really "publicise" these legacy RPC methods, because the more people rely on them, the more they will be disappointed when they go away (which, to be clear, may still be over a year away :))

@jsdw
Copy link
Collaborator Author

jsdw commented Aug 21, 2023

From my understanding we will add a newer backend soon as a replacement of the LegacyBackend? If that is the case, what will be the mechanism by which it is determined what backend is used when an OnlineClient is constructed?

At the moment, the "simple" constructors on OnlineClient will pick the backend to instantiate. When the new RPCs are in widespread use and stable, we can switch the backend internally to be the new one.

I've also added from_backend (and from_backend_with) which would allow users to pick the backend of choice.

The Backend trait is only sealed for now because it's still liable to changing more as we add the new stuff and update things, so I guess I want to discourage people from doing too much with it right now and then being disappointed (though they can still directly call the methods of it etc). I expect we'll "unseal" it when things settle a little so that people can build their own backends if they like, and then use OnlineClient::from_backend to instantiate a client with it.

@@ -24,8 +23,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!(" Extrinsics:");

// Log each of the extrinsic with it's associated events:
let body = block.body().await?;
for ext in body.extrinsics().iter() {
let extrinsics = block.extrinsics().await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice simplification for the end user!

If (ever) justification turns to be a thing in the future we could expose it on the same level as extrinsics (ie block.extrinsics())

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup definitely; if they are a thing then they won't be in the block body I guess anyway (since the body of a block afaik is just the extrinsics). I'm not actually sure where they live offhand; maybe theres a storage or runtime API call to fetch them (I haven't looked!)

// We copy some of the inner types and put the three states (valid, invalid, unknown) into one enum,
// because from our perspective, the call was successful regardless.
if bytes[0] == 0 {
// ok: valid (more detail is available here, but ).
Copy link
Contributor

@lexnv lexnv Aug 21, 2023

Choose a reason for hiding this comment

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

nit: ok: valid (more detail is available here, but we only care about the Result enum variant).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hehe I feel like I do this quite often; get distracted and forget to finish comments!

// https://github.com/paritytech/substrate/blob/0cdf7029017b70b7c83c21a4dc0aa1020e7914f6/primitives/runtime/src/transaction_validity.rs#L210
// We copy some of the inner types and put the three states (valid, invalid, unknown) into one enum,
// because from our perspective, the call was successful regardless.
if bytes[0] == 0 {
Copy link
Contributor

@lexnv lexnv Aug 21, 2023

Choose a reason for hiding this comment

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

nit: In case we somehow don't receive an Result<ValidTransaction, TransactionValidityError> and end-up with some empty bytes, would it be beneficial to do a bytes.get(0).expect("Invalid wire format for ValidationResult")? Or return the Error::Unkown?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point; I should handle these cases better :)

let api = ctx.client();

// Check subscription with runtime updates set on false.
let mut blocks = api.rpc().chainhead_unstable_follow(false).await.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is fine since we plan to add them back under a dedicated backend for the Spec V2.

In the meanwhile, I remember this is used by polakadot-interceptor here. However I think, the same could be achieved with the new Subxt API (for reference @AndreiEres ) which will allow seamless transition between chainHead and the legacy RPC.

IIRC this was used to create a metric about the duration time between two reported blocks (either best / finalized), we could work together here to find an appropriate solution with the new API (we might merge the 2 streams for best and finalized)

Choose a reason for hiding this comment

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

Thank you for mentioning me! I should've expected it by unstable mark in chainHead call :-)

So I see that I should switch our subscription in introspector to new API, right? Which one, btw?

Copy link
Contributor

Choose a reason for hiding this comment

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

After we release the next subxt version, merging the Backend::stream_best_block_headers and Backend::stream_finalized_block_headers should produce the same information as the chainHead

Copy link
Collaborator Author

@jsdw jsdw Aug 22, 2023

Choose a reason for hiding this comment

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

Would this call also work (if all you care about is the time between any new block)?

api.blocks().subscribe_all().await

or

api.blocks().subscribe_best().await

If you care about all new "best" blocks (likewise there is a version for finalized blocks only. Hopefully that way you can lean on the higher level stable APIs more :)

Choose a reason for hiding this comment

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

Thank you

Copy link
Contributor

@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.

Amazing work here! 👍

@jsdw jsdw merged commit d7124b5 into master Aug 22, 2023
9 checks passed
@jsdw jsdw deleted the jsdw-backend-trait branch August 22, 2023 11:32
@jsdw jsdw mentioned this pull request Sep 27, 2023
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.

None yet

4 participants