HomeBuildRemote procedure calls

Remote procedure calls

Remote procedure calls, or RPC methods, are a way for an external program—for example, a browser or front-end application—to communicate with a Substrate node. In general, these methods enable an RPC client to connect to an RPC server endpoint to request some type of service. For example, you might use an RPC method to read a stored value, submit a transaction, or request information about the chain a node is connected to.

The most convenient way to access the default JSON-RPC methods for a Substrate node is through the Polkadot-JS API.

Safe and unsafe RPC methods

It's important to be aware that RPC methods can provide access to core node operations, including consensus and storage, and can also be exposed as public interfaces to allow external users to submit transactions to or retrieve information from the blockchain. Therefore, for the security of the blockchain, it's important to consider what different RPC methods expose and whether they should be restricted to running on a local node or made publicly available.

Public RPC interfaces

Substrate nodes provide the following command-line options that allow you to expose the RPC interfaces publicly:

--ws-external
--rpc-external
--unsafe-ws-external
--unsafe-rpc-external

By default, the node will refuse to start if you try to expose an RPC interface and run a validator node at the same time. The --unsafe-* flags allow you to suppress this security measure. Exposing RPC interfaces can open up a huge surface of attacks and has to be carefully reviewed.

There are quite a few RPC methods that you can use to control the node's behavior, but you should avoid exposing. For example, you should not expose the following RPC methods:

You should also avoid exposing RPC methods that can take a long time to execute, potentially blocking the client from syncing. For example, you should avoid using the following RPC methods:

  • storage_keys_paged - get all the keys in the state with a particular prefix and pagination support.
  • storage_pairs - get all the keys in the state with a particular prefix together with their values.

These RPCs are declared by using the #[rpc(name = "rpc_method")] macro, where rpc_method is be the name of the function, for example, submit_extrinsic.

It's critical to filter out these kind of calls if the requests are coming from untrusted users. The way to do it is through a JSON-RPC proxy that is able to inspect calls and only pass an allowed set of API calls.

RPCs for remote_externalities

Substrate also provides some specialized RPC methods to call remote_externalities for a node. These specialized methods for remote externalities enable you to make one-off RPC calls to a Substrate node to get information about blocks and headers. The information returned by these calls can be useful for testing purposes with tools like try-runtime.

Endpoints

When you start a Substrate node locally, there is one endpoint available by default:

  • HTTP & WebSocket endpoint: ws://localhost:9944/

Most of the Substrate front-end libraries and tools use the endpoint to interact with the blockchain. For example, if you use the Polkadot-JS application to connect to a local node or a public chain, your are typically connecting to the HTTP & WebSocket endpoint. WebSocket connections allow for bidirectional communication between the front-end application and the backend node responding to requests. However, you can also call RPC methods individually without keeping an open communication channel by connecting to the endpoint using curl commands. For example, you can use curl commands to get system information or subscribe to a chain to receive notification when there are specific types of changes to the block state.

To call RPC methods using the endpoint:

  1. Open a terminal shell and change to the root directory for the Substrate node template.
  2. Start the node locally in development mode by running the following command:

    ./target/release/node-template --dev
  3. Connect to the local node and call the rpc_methods endpoint by running the following command:

    curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "rpc_methods"}' http://localhost:9944/

    This command returns a list of the JSON-RPC methods exposed for the local node.

  4. Call additional methods using the appropriate method name.

    For example, you can run the following command to get version information about the local node:

    curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "system_version"}' http://localhost:9944/

    In most cases, connecting to the RPC endpoint directly returns JSON-formatted results. For example:

    {"jsonrpc":"2.0","result":"4.0.0-dev-de262935ede","id":1}

For the return value to be human-readable, you can decode it using SCALE codec. For more information about encoding and decoding information, see Type encoding (SCALE).

Each storage item has a relative storage key associated to it which is used to query storage. This is how RPC endpoints know where to look.

Examples

state_getMetadata

To get metadata for a local node, you can run the following command:

curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "state_getMetadata"}' http://localhost:9944/

This command returns the metadata in hex-encoded bytes rather than a human-readable format. The JavaScript for this RPC request looks like this:

function get_metadata_request(endpoint) {
  let request = new Request(endpoint, {
    method: "POST",
    body: JSON.stringify({
      id: 1,
      jsonrpc: "2.0",
      method: "state_getMetadata",
    }),
    headers: { "Content-Type": "application/json" },
  });
  return request;
}

Naive text decoding:

function decode_metadata(metadata) {
  return new TextDecoder().decode(util.hexToU8a(metadata));
}

state_getStorage

RPC request:

Request:   {"id":1,"jsonrpc":"2.0","method":"state_getStorage",["{storage_key}"]}

Where storage_key is a parameter generated by the name of a pallet, function and key (optionally):

function get_runtime_storage_parameter_with_key(module_name, function_name, key) {
  // We use xxhash 128 for strings the runtime developer can control
  let module_hash = util_crypto.xxhashAsU8a(module_name, 128);
  let function_hash = util_crypto.xxhashAsU8a(function_name, 128);

  // We use blake2 256 for strings the end user can control
  let key_hash = util_crypto.blake2AsU8a(keyToBytes(key));

  // Special syntax to concatenate Uint8Array
  let final_key = new Uint8Array([...module_hash, ...function_hash, ...key_hash]);

  // Return a hex string
  return util.u8aToHex(final_key);
}

Where to go next