git.delta.rocks / unique-network / refs/commits / 6c79996794c3

difftreelog

source

kb/how-not-to-break-rpc.md7.7 KiBsourcehistory
1# How not to break RPC23Let's discuss how to avoid the breaking of RPC with an example of how the `colection_by_id` RPC method was broken.45The `collection_by_id` was broken due to the change of the result type `RpcCollection`.6The new version of the `RpcCollection` was incompatible with the old one due to addition of the `flags` field:78```rust9// The new version of the `RpcCollection`10pub struct RpcCollection<AccountId> {11	/// Collection owner account.12	pub owner: AccountId,13	/// Collection mode.14	pub mode: CollectionMode,15	/// Collection name.16	pub name: Vec<u16>,17	/// Collection description.18	pub description: Vec<u16>,19	/// Token prefix.20	pub token_prefix: Vec<u8>,21	/// The state of sponsorship of the collection.22	pub sponsorship: SponsorshipState<AccountId>,23	/// Collection limits.24	pub limits: CollectionLimits,25	/// Collection permissions.26	pub permissions: CollectionPermissions,27	/// Token property permissions.28	pub token_property_permissions: Vec<PropertyKeyPermission>,29	/// Collection properties.30	pub properties: Vec<Property>,31	/// Is collection read only.32	pub read_only: bool,3334	/// Extra collection flags35	pub flags: RpcCollectionFlags, // <-- THIS IS A NEW FIELD!36}37```3839### Where exactly was RPC broken?4041To answer this question, we need to describe the process of handling an RPC call.42431. A user calls an RPC method.442. The node sees what method with what arguments should be executed.453. Since the code of each RPC method is located inside the runtime, the node does the following:46    - The node encodes the RPC arguments into the [SCALE format](https://docs.substrate.io/reference/scale-codec/), and then it will call the corresponding method of the runtime API with the encoded arguments.47    - The runtime executes the RPC logic and then returns the SCALE-encoded result.48    - The node receives the result from the runtime and then decodes it. **It is the place where RPC could break!**4950Point #3 describes a process implemented inside the [`pass_method`](https://github.com/UniqueNetwork/unique-chain/blob/1c7179877b5fb1eacf86c5ecf607317d11999675/client/rpc/src/lib.rs#L435-L472) macro.5152Using the `pass_method` macro the node maps each RPC method onto the corresponding runtime API method.5354See how [the node's RPC is implemented](https://github.com/UniqueNetwork/unique-chain/blob/1c7179877b5fb1eacf86c5ecf607317d11999675/client/rpc/src/lib.rs#L493-L569) and how [the runtime API is declared](https://github.com/UniqueNetwork/unique-chain/blob/1c7179877b5fb1eacf86c5ecf607317d11999675/primitives/rpc/src/lib.rs#L32-L129).5556### How can the node use the old runtime API? 5758As you can see from the previous section -- RPC breaks if the runtime API data format is incompatible with the node's RPC data format.5960When the node is working with an old runtime and exposes the new version of RPC that contains some methods with a changed signature, the node should call only the old versions of these methods to avoid RPC failure.6162The node should do the following to get an old runtime API method to run correctly:63* The node should convert all the RPC arguments into the old runtime API format.64* It should convert the result from the runtime to the new data format (it is the only action needed in the case of `collection_by_id`). 6566The `pass_method` macro can call the old runtime API methods.67For instance, the correct implementation of the `collection_by_id` RPC method looks like this: 68```rust69pass_method!(70	/* line 1 */ collection_by_id(collection: CollectionId) -> Option<RpcCollection<AccountId>>, unique_api;71	/* line 2 */ changed_in 3, collection_by_id_before_version_3(collection) => |value| value.map(|coll| coll.into())72);73```7475The first line describes the newest RPC signature.7677The second line tells us what should be called in the case if we're dealing with an old runtime API.78* `collection_by_id_before_version_3` -- the name of the corresponding runtime API method with an old signature.79* `(collection)` -- what arguments should the node pass to the old method. In the case of `collection_by_id`, we pass the arguments as is since there were no changes to the arguments' types.80* `=> |value| value.map(|coll| coll.into())` -- describes how to transform the return value from the old runtime API data format into the new RPC data format.8182### Runtime API backward compatibility support8384Methods like `collection_by_id_before_version_3` doesn't appear automatically.8586When changing the runtime API methods' signatures, we need to:87* Specify the number of the new version of the runtime API.88* Specify the old versions of the changed methods.8990See the documentation of the `decl_runtime_apis` macro: [runtime api trait versioning](https://docs.rs/sp-api/latest/sp_api/macro.decl_runtime_apis.html#runtime-api-trait-versioning).9192### How to easily implement the converting from the old structure into the new ones9394To describe structures that can have some fields changing over different versions, we use the `#[struct_versioning::versioned]` attribute.9596Let's take a look at the `RpcCollection` declaration.9798```rust99/// Collection parameters, used in RPC calls (see [`Collection`] for the storage version).100#[struct_versioning::versioned(version = 2, upper)]101#[derive(Encode, Decode, Clone, PartialEq, TypeInfo)]102#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]103pub struct RpcCollection<AccountId> {104	/// Collection owner account.105	pub owner: AccountId,106107	/// Collection mode.108	pub mode: CollectionMode,109110	/// Collection name.111	pub name: Vec<u16>,112113	/// Collection description.114	pub description: Vec<u16>,115116	/// Token prefix.117	pub token_prefix: Vec<u8>,118119	/// The state of sponsorship of the collection.120	pub sponsorship: SponsorshipState<AccountId>,121122	/// Collection limits.123	pub limits: CollectionLimits,124125	/// Collection permissions.126	pub permissions: CollectionPermissions,127128	/// Token property permissions.129	pub token_property_permissions: Vec<PropertyKeyPermission>,130131	/// Collection properties.132	pub properties: Vec<Property>,133134	/// Is collection read only.135	pub read_only: bool,136137	/// Extra collection flags138	#[version(2.., upper(RpcCollectionFlags {foreign: false, erc721metadata: false}))]139	pub flags: RpcCollectionFlags,140}141```142143The `#[struct_versioning::versioned]` will create 2 types for us: the `RpcCollectionVersion1` (the old version) and the `RpcCollection` (the new version).144145This attribute automatically implements the `impl From<RpcCollectionVersion1> for RpcCollection`.146147The attribute understands how to map the old fields to new ones with the help of the `#[version(...)]` field attribute, which should be placed right before the field in question.148149There were no field `flags` in the `RpcCollectionVersion1` structure. The `#[version(2.., upper(<expr>))]` tells the attribute to assign the `flags` field to `<expr>` in the new version of the `RpcCollection` structure.150151Given that we have the `From` trait implemented for the new version of the `RpcCollection`, we can use `.into()` to convert the old version to the new one as we did in the `pass_method` macro above.152153Here is the description of the `struct_versioning` attribute:154```155Generate versioned variants of a struct156157 `#[versioned(version = 1[, first_version = 1][, upper][, versions])]`158 - *version* - current version of a struct159 - *first_version* - allows to skip generation of structs, which predates first supported version160 - *upper* - generate From impls, which converts old version of structs to new161 - *versions* - generate enum, which contains all possible versions of struct162163 Each field may have version attribute164 `#[version([1]..[2][, upper(old)])]`165 - *1* - version, on which this field is appeared166 - *2* - version, in which this field was removed167 (i.e if set to 2, this field was exist on version 1, and no longer exist on version 2)168 - *upper* - code, which should be executed to transform old value to new/create new value169```