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

refactor(rpc): re-worked rpc tower server and added proper websocket support #350

Merged
merged 15 commits into from
Oct 23, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Next release

- refactor(rpc): re-worked rpc tower server and added proper websocket support
- fix(block_hash): block hash mismatch on transaction with an empty signature
- feat: declare v0, l1 handler support added
- feat: strk gas price cli param added
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

94 changes: 94 additions & 0 deletions crates/client/rpc/src/RPC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# RPC

_This section consists of a brief overview of RPC handling architecture inside
of Madara, as its structure can be quite confusing at first._

## Properties

Madara RPC has the folliwing properties:

**Each RPC category is independent** and decoupled from the rest, so `trace`
methods exist in isolation from `read` methods for example.

**RPC methods are versioned**. It is therefore possible for a user to call
_different versions_ of the same RPC method. This is mostly present for ease of
development of new RPC versions, but also serves to assure a level of backwards
compatibility. To select a specific version of an rpc method, you will need to
append `/rcp/v{version}` to the rpc url you are connecting to.

**RPC versions are grouped under the `Starknet` struct**. This serves as a
common point of implementation for all RPC methods across all versions, and is
also the point of interaction between RPC methods and the node backend.

> [!NOTE]
> All of this is regrouped as an RPC _service_.

## Implementation details

There are **two** main parts to the implementation of RPC methods in Madara.

### Jsonrpsee implementation

> [!NOTE] > `jsonrpsee` is a library developed by Parity which is used to implement JSON
> RPC APIs through simple macro logic.

Each RPC version is defined under the `version` folder using the
`versioned_starknet_rpc` macro. This just serves to rename the trait it is
defined on and all jsonrpsee `#[method]` definitions to include the version
name. The latter is especially important as it avoids name clashes when merging
multiple `RpcModule`s from different versions together.

#### Renaming

```rust
#[versioned_starknet_rpc("V0_7_1)")]
trait yourTrait {
#[method(name = "foo")]
async fn foo();
}
```

Will become

```rust
#[jsonrpsee::proc_macros::rpc(server, namespace = "starknet")]
trait yourTraitV0_7_1 {
#[method(name = "V0_7_1_foo")]
async fn foo();
}
```

### Implementation as a service

> [!IMPORTANT]
> This is where the RPC server is set up and where RPC calls are actually
> parsed, validated, routed and handled.

`RpcService` is responsible for starting the rpc service, and hence the rpc
server. This is done with tower in the following steps:

- RPC apis are built and combined into a single `RpcModule` using
`versioned_rpc_api`, and all other configurations are loaded.

- Request filtering middleware is set up. This includes host origin filtering
and CORS filtering.

> [!NOTE]
> Rpc middleware will apply to both websocket and http rpc requests, which is
> why we do not apply versioning in the http middleware.

- Request constraints are set, such as the maximum number of connections and
request / response size constraints.

- Additional service layers are added on each rpc call inside `service_fn`.
These are composed into versioning, rate limiting (which is optional) and
metrics layers. Importantly, version filtering with `RpcMiddlewareServiceVersion`
will transforms rpc methods request with header `/rpc/v{version}` and a json rpc
body with a `{method}` field into the correct `starknet_{version}_{method}` rpc
method call, as this is how we version them internally with jsonrpsee.

> [!NOTE]
> The `starknet` prefix comes from the secondary macro expansion of
> `#[rpc(server, namespace = "starknet)]`

- Finally, the RPC service is added to tower as `RpcServiceBuilder`. Note that
22 changes: 16 additions & 6 deletions crates/client/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

mod constants;
mod errors;
mod macros;
pub mod providers;
#[cfg(test)]
pub mod test_utils;
Expand Down Expand Up @@ -99,14 +98,25 @@ pub fn versioned_rpc_api(
write: bool,
trace: bool,
internal: bool,
ws: bool,
) -> anyhow::Result<RpcModule<()>> {
let mut rpc_api = RpcModule::new(());

merge_rpc_versions!(
rpc_api, starknet, read, write, trace, internal,
v0_7_1, // We can add new versions by adding the version module below
// , v0_8_0 (for example)
);
if read {
rpc_api.merge(versions::v0_7_1::StarknetReadRpcApiV0_7_1Server::into_rpc(starknet.clone()))?;
}
if write {
rpc_api.merge(versions::v0_7_1::StarknetWriteRpcApiV0_7_1Server::into_rpc(starknet.clone()))?;
}
if trace {
rpc_api.merge(versions::v0_7_1::StarknetTraceRpcApiV0_7_1Server::into_rpc(starknet.clone()))?;
}
if internal {
rpc_api.merge(versions::v0_7_1::MadaraWriteRpcApiV0_7_1Server::into_rpc(starknet.clone()))?;
}
if ws {
// V0.8.0 ...
}

Ok(rpc_api)
}
21 changes: 0 additions & 21 deletions crates/client/rpc/src/macros.rs

This file was deleted.

1 change: 0 additions & 1 deletion crates/client/rpc/src/versions/v0_7_1/api.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use jsonrpsee::core::RpcResult;
use jsonrpsee::proc_macros::rpc;
use starknet_core::types::{
BlockHashAndNumber, BlockId, BroadcastedDeclareTransaction, BroadcastedDeployAccountTransaction,
BroadcastedInvokeTransaction, BroadcastedTransaction, ContractClass, DeclareTransactionResult,
Expand Down
16 changes: 0 additions & 16 deletions crates/node/src/cli/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use std::num::NonZeroU32;
use std::str::FromStr;

use clap::ValueEnum;
use ip_network::IpNetwork;
use jsonrpsee::server::BatchRequestConfig;

/// Available RPC methods.
Expand Down Expand Up @@ -99,21 +98,6 @@ pub struct RpcParams {
#[arg(env = "MADARA_RPC_RATE_LIMIT", long)]
pub rpc_rate_limit: Option<NonZeroU32>,

/// Disable RPC rate limiting for certain ip addresses or ranges.
///
/// Each IP address must be in the following notation: `1.2.3.4/24`.
#[arg(env = "MADARA_RPC_RATE_LIMIT_WHITELISTED_IPS", long, num_args = 1..)]
pub rpc_rate_limit_whitelisted_ips: Vec<IpNetwork>,

/// Trust proxy headers for disable rate limiting.
///
/// When using a reverse proxy setup, the real requester IP is usually added to the headers as `X-Real-IP` or `X-Forwarded-For`.
/// By default, the RPC server will not trust these headers.
///
/// This is currently only useful for rate-limiting reasons.
#[arg(env = "MADARA_RPC_RATE_LIMIT_TRUST_PROXY_HEADERS", long)]
pub rpc_rate_limit_trust_proxy_headers: bool,

/// Set the maximum RPC request payload size for both HTTP and WebSockets in megabytes.
#[arg(env = "MADARA_RPC_MAX_REQUEST_SIZE", long, default_value_t = RPC_DEFAULT_MAX_REQUEST_SIZE_MB)]
pub rpc_max_request_size: u32,
Expand Down
8 changes: 3 additions & 5 deletions crates/node/src/service/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ impl RpcService {
(true, false)
}
};
let (read, write, trace, internal) = (rpcs, rpcs, rpcs, node_operator);
let starknet = Starknet::new(Arc::clone(db.backend()), chain_config.clone(), add_txs_method_provider.clone());
let (read, write, trace, internal, ws) = (rpcs, rpcs, rpcs, node_operator, rpcs);
let starknet = Starknet::new(Arc::clone(db.backend()), chain_config.clone(), add_txs_method_provider);
let metrics = RpcMetrics::register(metrics_handle)?;

Ok(Self {
Expand All @@ -59,12 +59,10 @@ impl RpcService {
max_payload_out_mb: config.rpc_max_response_size,
max_subs_per_conn: config.rpc_max_subscriptions_per_connection,
message_buffer_capacity: config.rpc_message_buffer_capacity_per_connection,
rpc_api: versioned_rpc_api(&starknet, read, write, trace, internal)?,
rpc_api: versioned_rpc_api(&starknet, read, write, trace, internal, ws)?,
metrics,
cors: config.cors(),
rate_limit: config.rpc_rate_limit,
rate_limit_whitelisted_ips: config.rpc_rate_limit_whitelisted_ips.clone(),
rate_limit_trust_proxy_headers: config.rpc_rate_limit_trust_proxy_headers,
}),
server_handle: None,
})
Expand Down
Loading
Loading