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

New reference doc for Custom RPC V2 #4654

Merged
merged 11 commits into from
Jun 7, 2024
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions docs/sdk/src/reference_docs/custom_runtime_api_rpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! # Custom RPC do's and don'ts
//!
//! **TLDR:** don't create new custom RPCs. Instead, rely on custom Runtime APIs, combined with
//! `state_call`
//!
//! ## Background
//!
//! Polkadot-SDK offers the ability to query and subscribe storages directly. However what it does
//! not have is [view functions](https://github.com/paritytech/polkadot-sdk/issues/216). This is an
//! essential feature to avoid duplicated logic between runtime and the client SDK. Custom RPC was
//! used as a solution. It allow the RPC node to expose new RPCs that clients can be used to query
//! computed properties.
//!
//! ## Problems with Custom RPC
//!
//! Unfortunately, custom RPC comes with many problems. To list a few:
//!
//! - It is offchain logic executed by the RPC node and therefore the client has to trust the RPC
//! node.
//! - To upgrade or add a new RPC logic, the RPC node has to be upgraded. This can cause significant
//! trouble when the RPC infrastructure is decentralized as we will need to coordinate multiple
//! parties to upgrade the RPC nodes.
//! - A lot of boilerplate code are required to add custom RPC.
//! - It prevents the dApp to use a light client or alternative client.
//! - It makes ecosystem tooling integration much more complicated. For example, the dApp will not
//! be able to use [Chopsticks](https://github.com/AcalaNetwork/chopsticks) for testing as
//! Chopsticks will not have the custom RPC implementation.
//! - Poorly implemented custom RPC can be a DoS vector.
//!
//! Hence, we should avoid custom RPC.
//!
//! ## Alternatives
//!
//! Generally, [`sc_rpc::state::StateBackend::call`] aka. `state_call` should be used instead of
//! custom RPC.
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
//!
//! Usually, each custom RPC comes with a corresponding runtime API which implements the business
//! logic. So instead of invoke the custom RPC, we can use `state_call` to invoke the runtime API
//! directly. This is a trivial change on the dApp and no change on the runtime side. We may remove
//! the custom RPC from the node side if wanted.
//!
//! There are some other cases that a simple runtime API is not enough. For example, implementation
//! of Ethereum RPC requires an additional offchain database to index transactions. In this
//! particular case, we can have the RPC implemented on another client.
//!
//! For example, the Acala EVM+ RPC are implemented by
//! [eth-rpc-adapter](https://github.com/AcalaNetwork/bodhi.js/tree/master/packages/eth-rpc-adapter).
//! Alternatively, the [Frontier](https://github.com/polkadot-evm/frontier) project also provided
//! Ethereum RPC compatibility directly in the node-side software.
//!
//! ## Create a new Runtime API
//!
//! For example, let's take a look a the process through which the account nonce can be queried
//! through an RPC. First, a new runtime-api needs to be declared:
#![doc = docify::embed!("../../substrate/frame/system/rpc/runtime-api/src/lib.rs", AccountNonceApi)]
//!
//! This API is implemented at the runtime level, always inside [`sp_api::impl_runtime_apis!`].
//!
//! As noted, this is already enough to make this API usable via `state_call`.
//!
//! ## Create a new custom RPC (Legacy)
//!
//! Should you wish to implement the legacy approach of exposing this runtime-api as a custom
//! RPC-api, then a custom RPC server has to be defined.
#![doc = docify::embed!("../../substrate/utils/frame/rpc/system/src/lib.rs", SystemApi)]
//!
//! ## Add a new RPC to the node (Legacy)
//!
//! Finally, this custom RPC needs to be integrated into the node side. This is usually done in a
//! `rpc.rs` in a typical template, as follows:
#![doc = docify::embed!("../../templates/minimal/node/src/rpc.rs", create_full)]
//!
//! ## Future
//!
//! - [XCQ](https://forum.polkadot.network/t/cross-consensus-query-language-xcq/7583) will be a good
//! solution for most of the query needs.
//! - [New JSON-RPC Specification](https://github.com/paritytech/json-rpc-interface-spec)
3 changes: 3 additions & 0 deletions docs/sdk/src/reference_docs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,6 @@ pub mod frame_pallet_coupling;

/// Learn about the Polkadot Umbrella crate that re-exports all other crates.
pub mod umbrella_crate;

/// Learn about how to create custom RPC endpoints and runtime APIs.
pub mod custom_runtime_api_rpc;
1 change: 1 addition & 0 deletions substrate/frame/system/rpc/runtime-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "3.6.12", default-features = false }
sp-api = { path = "../../../../primitives/api", default-features = false }
docify = "0.2.0"

[features]
default = ["std"]
Expand Down
1 change: 1 addition & 0 deletions substrate/frame/system/rpc/runtime-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

#![cfg_attr(not(feature = "std"), no_std)]

#[docify::export(AccountNonceApi)]
sp_api::decl_runtime_apis! {
/// The API to query account nonce.
pub trait AccountNonceApi<AccountId, Nonce> where
Expand Down
9 changes: 7 additions & 2 deletions substrate/utils/frame/rpc/system/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ workspace = true
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
codec = { package = "parity-scale-codec", version = "3.6.12" }
jsonrpsee = { version = "0.22.5", features = ["client-core", "macros", "server-core"] }
futures = "0.3.30"
codec = { package = "parity-scale-codec", version = "3.6.12" }
docify = "0.2.0"
jsonrpsee = { version = "0.22.5", features = [
"client-core",
"macros",
"server-core",
] }
log = { workspace = true, default-features = true }
frame-system-rpc-runtime-api = { path = "../../../../frame/system/rpc/runtime-api" }
sc-rpc-api = { path = "../../../../client/rpc-api" }
Expand Down
1 change: 1 addition & 0 deletions substrate/utils/frame/rpc/system/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use sp_runtime::{legacy, traits};
pub use frame_system_rpc_runtime_api::AccountNonceApi;

/// System RPC methods.
#[docify::export]
#[rpc(client, server)]
pub trait SystemApi<BlockHash, AccountId, Nonce> {
/// Returns the next valid index (aka nonce) for given account.
Expand Down
1 change: 1 addition & 0 deletions templates/minimal/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ build = "build.rs"
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
docify = "0.2.0"
clap = { version = "4.5.3", features = ["derive"] }
futures = { version = "0.3.30", features = ["thread-pool"] }
futures-timer = "3.0.1"
Expand Down
3 changes: 2 additions & 1 deletion templates/minimal/node/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use runtime::interface::{AccountId, Nonce, OpaqueBlock};
use sc_transaction_pool_api::TransactionPool;
use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata};
use std::sync::Arc;
use substrate_frame_rpc_system::{System, SystemApiServer};

pub use sc_rpc_api::DenyUnsafe;

Expand All @@ -41,6 +40,7 @@ pub struct FullDeps<C, P> {
pub deny_unsafe: DenyUnsafe,
}

#[docify::export]
/// Instantiate all full RPC extensions.
pub fn create_full<C, P>(
deps: FullDeps<C, P>,
Expand All @@ -57,6 +57,7 @@ where
C::Api: substrate_frame_rpc_system::AccountNonceApi<OpaqueBlock, AccountId, Nonce>,
P: TransactionPool + 'static,
{
use substrate_frame_rpc_system::{System, SystemApiServer};
let mut module = RpcModule::new(());
let FullDeps { client, pool, deny_unsafe } = deps;

Expand Down
Loading