Runtime API versioning (#11779)

* Runtime API versioning

Related to issue #11577

Add support for multiple versions of a Runtime API. The purpose is to
have one main version of the API, which is considered stable and
multiple unstable (aka staging) ones.

How it works
===========
Some methods of the API trait can be tagged with `#[api_version(N)]`
attribute where N is version number bigger than the main one. Let's call
them **staging methods** for brevity.

The implementor of the API decides which version to implement.

Example (from https://github.com/paritytech/substrate/issues/11577#issuecomment-1145347025):

```
decl_runtime_apis! {
    #{api_version(10)]
    trait Test {
         fn something() -> Vec<u8>;
         #[api_version(11)]
         fn new_cool_function() -> u32;
    }
}
```

```
impl_runtime_apis! {
    #[api_version(11)]
    impl Test for Runtime {
         fn something() -> Vec<u8> { vec![1, 2, 3] }

         fn new_cool_function() -> u32 {
             10
         }
    }
}
```

Version safety checks (currently not implemented)
=================================================
By default in the API trait all staging methods has got default
implementation calling `unimplemented!()`. This is a problem because if
the developer wants to implement version 11 in the example above and
forgets to add `fn new_cool_function()` in `impl_runtime_apis!` the
runtime will crash when the function is executed.

Ideally a compilation error should be generated in such cases.

TODOs
=====

Things not working well at the moment:
[ ] Version safety check
[ ] Integration tests of `primitives/api` are messed up a bit. More
specifically `primitives/api/test/tests/decl_and_impl.rs`
[ ] Integration test covering the new functionality.
[ ] Some duplicated code

* Update primitives/api/proc-macro/src/impl_runtime_apis.rs

Code review feedback and formatting

Co-authored-by: asynchronous rob <rphmeier@gmail.com>

* Code review feedback

Applying suggestions from @bkchr

* fmt

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Code review feedback

* dummy trait -> versioned trait

* Implement only versioned traits (not compiling)

* Remove native API calls (still not compiling)

* fmt

* Fix compilation

* Comments

* Remove unused code

* Remove native runtime tests

* Remove unused code

* Fix UI tests

* Code review feedback

* Code review feedback

* attribute_names -> common

* Rework `append_api_version`

* Code review feedback

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Code review feedback

* Code review feedback

* Code review feedback

* Use type alias for the default trait - doesn't compile

* Fixes

* Better error for `method_api_ver < trait_api_version`

* fmt

* Rework how we call runtime functions

* Update UI tests

* Fix warnings

* Fix doctests

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Fix formatting and small compilation errors

* Update primitives/api/proc-macro/src/impl_runtime_apis.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

Co-authored-by: asynchronous rob <rphmeier@gmail.com>
Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
Co-authored-by: Bastian Köcher <info@kchr.de>
This commit is contained in:
Tsvetomir Dimitrov
2022-08-13 14:56:40 +03:00
committed by GitHub
parent 8e927daa77
commit 2bff2f84e3
32 changed files with 944 additions and 862 deletions
@@ -18,8 +18,7 @@
use crate::utils::{
extract_block_type_from_trait_path, extract_impl_trait,
extract_parameter_names_types_and_borrows, generate_crate_access, generate_hidden_includes,
generate_method_runtime_api_impl_name, return_type_extract_type, AllowSelfRefInParameters,
RequireQualifiedTraitPath,
return_type_extract_type, AllowSelfRefInParameters, RequireQualifiedTraitPath,
};
use proc_macro2::{Span, TokenStream};
@@ -31,7 +30,7 @@ use syn::{
parse::{Error, Parse, ParseStream, Result},
parse_macro_input, parse_quote,
spanned::Spanned,
Attribute, Ident, ItemImpl, Pat, Type, TypePath,
Attribute, ItemImpl, Pat, Type, TypePath,
};
/// Unique identifier used to make the hidden includes unique for this macro.
@@ -40,7 +39,7 @@ const HIDDEN_INCLUDES_ID: &str = "MOCK_IMPL_RUNTIME_APIS";
/// The `advanced` attribute.
///
/// If this attribute is given to a function, the function gets access to the `BlockId` as first
/// parameter and needs to return a `Result` with the appropiate error type.
/// parameter and needs to return a `Result` with the appropriate error type.
const ADVANCED_ATTRIBUTE: &str = "advanced";
/// The structure used for parsing the runtime api implementations.
@@ -126,34 +125,63 @@ fn implement_common_api_traits(block_type: TypePath, self_ty: Type) -> Result<To
}
impl #crate_::Core<#block_type> for #self_ty {
fn Core_version_runtime_api_impl(
fn __runtime_api_internal_call_api_at(
&self,
_: &#crate_::BlockId<#block_type>,
_: #crate_::ExecutionContext,
_: Option<()>,
_: Vec<u8>,
) -> std::result::Result<#crate_::NativeOrEncoded<#crate_::RuntimeVersion>, #crate_::ApiError> {
unimplemented!("Not required for testing!")
_: std::vec::Vec<u8>,
_: &dyn Fn(#crate_::RuntimeVersion) -> &'static str,
) -> std::result::Result<std::vec::Vec<u8>, #crate_::ApiError> {
unimplemented!("`__runtime_api_internal_call_api_at` not implemented for runtime api mocks")
}
fn Core_execute_block_runtime_api_impl(
fn version(
&self,
_: &#crate_::BlockId<#block_type>,
_: #crate_::ExecutionContext,
_: Option<#block_type>,
_: Vec<u8>,
) -> std::result::Result<#crate_::NativeOrEncoded<()>, #crate_::ApiError> {
unimplemented!("Not required for testing!")
) -> std::result::Result<#crate_::RuntimeVersion, #crate_::ApiError> {
unimplemented!("`Core::version` not implemented for runtime api mocks")
}
fn Core_initialize_block_runtime_api_impl(
fn version_with_context(
&self,
_: &#crate_::BlockId<#block_type>,
_: #crate_::ExecutionContext,
_: Option<&<#block_type as #crate_::BlockT>::Header>,
_: Vec<u8>,
) -> std::result::Result<#crate_::NativeOrEncoded<()>, #crate_::ApiError> {
unimplemented!("Not required for testing!")
) -> std::result::Result<#crate_::RuntimeVersion, #crate_::ApiError> {
unimplemented!("`Core::version` not implemented for runtime api mocks")
}
fn execute_block(
&self,
_: &#crate_::BlockId<#block_type>,
_: #block_type,
) -> std::result::Result<(), #crate_::ApiError> {
unimplemented!("`Core::execute_block` not implemented for runtime api mocks")
}
fn execute_block_with_context(
&self,
_: &#crate_::BlockId<#block_type>,
_: #crate_::ExecutionContext,
_: #block_type,
) -> std::result::Result<(), #crate_::ApiError> {
unimplemented!("`Core::execute_block` not implemented for runtime api mocks")
}
fn initialize_block(
&self,
_: &#crate_::BlockId<#block_type>,
_: &<#block_type as #crate_::BlockT>::Header,
) -> std::result::Result<(), #crate_::ApiError> {
unimplemented!("`Core::initialize_block` not implemented for runtime api mocks")
}
fn initialize_block_with_context(
&self,
_: &#crate_::BlockId<#block_type>,
_: #crate_::ExecutionContext,
_: &<#block_type as #crate_::BlockT>::Header,
) -> std::result::Result<(), #crate_::ApiError> {
unimplemented!("`Core::initialize_block` not implemented for runtime api mocks")
}
}
))
@@ -216,14 +244,53 @@ fn get_at_param_name(
}
}
/// Auxialiry structure to fold a runtime api trait implementation into the expected format.
/// Auxiliary structure to fold a runtime api trait implementation into the expected format.
///
/// This renames the methods, changes the method parameters and extracts the error type.
struct FoldRuntimeApiImpl<'a> {
/// The block type that is being used.
block_type: &'a TypePath,
/// The identifier of the trait being implemented.
impl_trait: &'a Ident,
}
impl<'a> FoldRuntimeApiImpl<'a> {
/// Process the given [`syn::ItemImpl`].
fn process(mut self, impl_item: syn::ItemImpl) -> syn::ItemImpl {
let mut impl_item = self.fold_item_impl(impl_item);
let crate_ = generate_crate_access(HIDDEN_INCLUDES_ID);
// We also need to overwrite all the `_with_context` methods. To do this,
// we clone all methods and add them again with the new name plus one more argument.
impl_item.items.extend(impl_item.items.clone().into_iter().filter_map(|i| {
if let syn::ImplItem::Method(mut m) = i {
m.sig.ident = quote::format_ident!("{}_with_context", m.sig.ident);
m.sig.inputs.insert(2, parse_quote!( _: #crate_::ExecutionContext ));
Some(m.into())
} else {
None
}
}));
let block_type = self.block_type;
impl_item.items.push(parse_quote! {
fn __runtime_api_internal_call_api_at(
&self,
_: &#crate_::BlockId<#block_type>,
_: #crate_::ExecutionContext,
_: std::vec::Vec<u8>,
_: &dyn Fn(#crate_::RuntimeVersion) -> &'static str,
) -> std::result::Result<std::vec::Vec<u8>, #crate_::ApiError> {
unimplemented!(
"`__runtime_api_internal_call_api_at` not implemented for runtime api mocks. \
Calling deprecated methods is not supported by mocked runtime api."
)
}
});
impl_item
}
}
impl<'a> Fold for FoldRuntimeApiImpl<'a> {
@@ -277,14 +344,9 @@ impl<'a> Fold for FoldRuntimeApiImpl<'a> {
input.sig.inputs = parse_quote! {
&self,
#at_param_name: #block_id_type,
_: #crate_::ExecutionContext,
___params___sp___api___: Option<( #( #param_types ),* )>,
_: Vec<u8>,
#( #param_names: #param_types ),*
};
input.sig.ident =
generate_method_runtime_api_impl_name(self.impl_trait, &input.sig.ident);
// When using advanced, the user needs to declare the correct return type on its own,
// otherwise do it for the user.
if !is_advanced {
@@ -292,7 +354,7 @@ impl<'a> Fold for FoldRuntimeApiImpl<'a> {
// Generate the correct return type.
input.sig.output = parse_quote!(
-> std::result::Result<#crate_::NativeOrEncoded<#ret_type>, #crate_::ApiError>
-> std::result::Result<#ret_type, #crate_::ApiError>
);
}
@@ -304,7 +366,7 @@ impl<'a> Fold for FoldRuntimeApiImpl<'a> {
quote! {
let __fn_implementation__ = move || #orig_block;
Ok(#crate_::NativeOrEncoded::Native(__fn_implementation__()))
Ok(__fn_implementation__())
}
};
@@ -314,9 +376,6 @@ impl<'a> Fold for FoldRuntimeApiImpl<'a> {
// Get the error to the user (if we have one).
#( #errors )*
let (#( #param_names ),*) = ___params___sp___api___
.expect("Mocked runtime apis don't support calling deprecated api versions");
#construct_return_value
}
)
@@ -351,11 +410,6 @@ fn generate_runtime_api_impls(impls: &[ItemImpl]) -> Result<GeneratedRuntimeApiI
for impl_ in impls {
let impl_trait_path = extract_impl_trait(impl_, RequireQualifiedTraitPath::No)?;
let impl_trait = &impl_trait_path
.segments
.last()
.ok_or_else(|| Error::new(impl_trait_path.span(), "Empty trait path not possible!"))?
.clone();
let block_type = extract_block_type_from_trait_path(impl_trait_path)?;
self_ty = match self_ty.take() {
@@ -395,9 +449,7 @@ fn generate_runtime_api_impls(impls: &[ItemImpl]) -> Result<GeneratedRuntimeApiI
None => Some(block_type.clone()),
};
let mut visitor = FoldRuntimeApiImpl { block_type, impl_trait: &impl_trait.ident };
result.push(visitor.fold_item_impl(impl_.clone()));
result.push(FoldRuntimeApiImpl { block_type }.process(impl_.clone()));
}
Ok(GeneratedRuntimeApiImpls {