diff --git a/.github/workflows/forest.yml b/.github/workflows/forest.yml index 4e476f475585..7b2c4e73b5ee 100644 --- a/.github/workflows/forest.yml +++ b/.github/workflows/forest.yml @@ -2,7 +2,7 @@ name: Integration tests concurrency: group: '${{ github.workflow }}-${{ github.ref }}' cancel-in-progress: '${{ github.ref != ''refs/heads/main'' }}' -'on': +on: workflow_dispatch: merge_group: pull_request: @@ -185,6 +185,32 @@ jobs: chmod +x ~/.cargo/bin/forest* - run: ./scripts/tests/calibnet_stateless_mode_check.sh timeout-minutes: '${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }}' + calibnet-stateless-rpc-check: + needs: + - build-ubuntu + name: Calibnet stateless RPC check + runs-on: ubuntu-24.04 + steps: + - run: lscpu + - uses: actions/cache@v4 + with: + path: '${{ env.FIL_PROOFS_PARAMETER_CACHE }}' + key: proof-params-keys + - name: Checkout Sources + uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: 'forest-${{ runner.os }}' + path: ~/.cargo/bin + - uses: actions/download-artifact@v4 + with: + name: 'forest-${{ runner.os }}' + path: ~/.cargo/bin + - name: Set permissions + run: | + chmod +x ~/.cargo/bin/forest* + - run: ./scripts/tests/calibnet_stateless_rpc_check.sh + timeout-minutes: '${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }}' state-migrations-check: needs: - build-ubuntu @@ -558,6 +584,7 @@ jobs: - forest-cli-check - calibnet-check - calibnet-stateless-mode-check + - calibnet-stateless-rpc-check - state-migrations-check - calibnet-wallet-check - calibnet-export-check diff --git a/CHANGELOG.md b/CHANGELOG.md index a2146bd013b9..339aa940dc88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ - [#4769](https://github.com/ChainSafe/forest/issues/4769) Add delegated address support to `forest-wallet new` command. +- [#5147](https://github.com/ChainSafe/forest/issues/5147) Add support for the `--rpc-filter-list` flag to the `forest` daemon. This flag allows users to specify a list of RPC methods to whitelist or blacklist. + ### Changed - [#5237](https://github.com/ChainSafe/forest/pull/5237) Stylistic changes to FIL pretty printing. diff --git a/docs/dictionary.txt b/docs/dictionary.txt index 8455d69bc428..cb075d46a84e 100644 --- a/docs/dictionary.txt +++ b/docs/dictionary.txt @@ -65,6 +65,7 @@ M2 macOS Mainnet mainnet +namespace NV22 NV23 NV24 diff --git a/docs/docs/users/knowledge_base/methods_filtering.md b/docs/docs/users/knowledge_base/methods_filtering.md new file mode 100644 index 000000000000..ddd352d515c1 --- /dev/null +++ b/docs/docs/users/knowledge_base/methods_filtering.md @@ -0,0 +1,89 @@ +--- +title: RPC methods filtering +--- + +# RPC methods filtering + +## Why filter RPC methods? + +When running a Filecoin node, you might want to restrict the RPC methods that are available to the clients. This can be useful for security reasons, to limit the exposure of the node to the internet, or to reduce the load on the node by disabling unnecessary methods. + +:::note +[JWT authentication](./jwt_handling.md) is a different way to restrict access to the node. It allows you to authorize certain operations on the node using JWTs. However, JWT restrictions are hard-coded in the node and cannot be changed dynamically. If you want to make sure that a certain read-only method is not available to the clients, you can use the method filtering feature. + +The methods are first filtered by the method filtering feature, and then the JWT authentication is applied. If a method is disallowed by the method filtering, the JWT token will not be checked for this method. +::: + +## How to filter RPC methods + +You need to run `forest` with the `--rpc-filter-list ` argument. If the filter list is not provided, all methods are allowed by default. + +### Example + +In this example, will disallow the `Filecoin.ChainExport` method which is used to export the chain to a file. This method should not be available to the clients due to its impact (compute, disk space, etc.) on the node. + +1. Create a filter list file, for example, `filter-list.txt`: + +```plaintext +# Disabling the snapshot exporting +!Filecoin.ChainExport +``` + +2. Run `forest` with the `--rpc-filter-list` argument: + +```shell +forest --chain calibnet --encrypt-keystore false --rpc-filter-list filter-list.txt +``` + +3. Try to export the snapshot using the `forest-cli`: + +```shell +forest-cli snapshot-export +``` + +You should see the following error: + +```console +Getting ready to export... +Error: ErrorObject { code: ServerError(403), message: "Forbidden", data: None } + +Caused by: + ErrorObject { code: ServerError(403), message: "Forbidden", data: None } +``` + +## Filter list format + +The filter list is a text file where each line represents a method that should be allowed or disallowed. The format is as follows: + +- `!` at the beginning of the line means that the method is disallowed. +- `#` at the beginning of the line is a comment and is ignored. +- no prefix means that the method is allowed. + +If there is a single allowed method (no prefix), all other methods are disallowed by default. + +:::warning +Some methods have aliases, so you need to filter all of them. This is most prominent in the `Filecoin.Eth.*` namespace. They are implemented for compatibility with Lotus, see [here](https://github.com/filecoin-project/lotus/blob/a9718c841e1fced8afc6e9fee2db2a2b565acc42/api/eth_aliases.go). +::: + +## Example filter lists + +Allow only the `Filecoin.StateCall` method. All other methods are disallowed: + +```plaintext +Filecoin.StateCall +``` + +Disallow the `Filecoin.ChainExport` method. All other methods are allowed: + +```plaintext +!Filecoin.ChainExport +``` + +Disallow the `Filecoin.EthGasPrice`, `Filecoin.EthEstimateGas`, and their aliases. All other methods are allowed: + +```plaintext +!Filecoin.EthGasPrice +!eth_gasPrice +!Filecoin.EthEstimateGas +!eth_estimateGas +``` diff --git a/scripts/tests/calibnet_stateless_rpc_check.sh b/scripts/tests/calibnet_stateless_rpc_check.sh new file mode 100755 index 000000000000..088690d3799b --- /dev/null +++ b/scripts/tests/calibnet_stateless_rpc_check.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -euxo pipefail + +# This script tests RPC on a stateless node. This is done to avoid downloading the snapshot for this test and speed up the CI. + +source "$(dirname "$0")/harness.sh" + +# Run a stateless node with a filter list as an argument. +function forest_run_node_stateless_detached_with_filter_list { + pkill -9 forest || true + local filter_list=$1 + + $FOREST_PATH --detach --chain calibnet --encrypt-keystore false --log-dir "$LOG_DIRECTORY" --save-token ./admin_token --skip-load-actors --stateless --rpc-filter-list "$filter_list" + + ADMIN_TOKEN=$(cat admin_token) + FULLNODE_API_INFO="$ADMIN_TOKEN:/ip4/127.0.0.1/tcp/2345/http" + + export ADMIN_TOKEN + export FULLNODE_API_INFO +} + +# Tests the RPC method `Filecoin.ChainHead` and checks if the status code matches the expected code. +function test_rpc { + local expected_code=$1 + + # Test the RPC, get status code + status_code=$(curl --silent -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","id":2,"method":"Filecoin.ChainHead","params": [ ] }' \ + "http://127.0.0.1:2345/rpc/v1" | jq '.error.code') + + # check if the expected code is returned + if [ "$status_code" != "$expected_code" ]; then + echo "Expected status code $expected_code, got $status_code" + exit 1 + fi +} + +# No filter list - all RPCs are allowed. This is the default behavior. + +cat <<- EOF > "$TMP_DIR"/filter-list +# Cthulhu fhtagn +EOF + +forest_run_node_stateless_detached_with_filter_list "$TMP_DIR/filter-list" +test_rpc null # null means there is no error + +# Filter list with the `ChainHead` RPC disallowed. Should return 403. + +cat <<- EOF > "$TMP_DIR"/filter-list +!Filecoin.ChainHead +EOF + +forest_run_node_stateless_detached_with_filter_list "$TMP_DIR/filter-list" +test_rpc 403 + +# Filter list with a single other RPC allowed. `ChainHead` should be disallowed and return 403. +# Note - this method is required for the test harness. +cat <<- EOF > "$TMP_DIR"/filter-list +Filecoin.Shutdown +EOF + +forest_run_node_stateless_detached_with_filter_list "$TMP_DIR/filter-list" +test_rpc 403 + +# Filter list with a single other RPC allowed, along with `ChainHead`. Should succeed. +cat <<- EOF > "$TMP_DIR"/filter-list +Filecoin.Shutdown +Filecoin.ChainHead +EOF + +forest_run_node_stateless_detached_with_filter_list "$TMP_DIR/filter-list" +test_rpc null # null means there is no error diff --git a/src/cli_shared/cli/client.rs b/src/cli_shared/cli/client.rs index 40891be82f5e..3533aec12235 100644 --- a/src/cli_shared/cli/client.rs +++ b/src/cli_shared/cli/client.rs @@ -61,6 +61,9 @@ pub struct Client { pub metrics_address: SocketAddr, /// RPC bind, e.g. 127.0.0.1:1234 pub rpc_address: SocketAddr, + /// Path to a list of RPC methods to allow/disallow. + pub rpc_filter_list: Option, + /// Healthcheck bind, e.g. 127.0.0.1:2346 pub healthcheck_address: SocketAddr, /// Load actors from the bundle file (possibly generating it if it doesn't exist) pub load_actors: bool, @@ -87,6 +90,7 @@ impl Default for Client { encrypt_keystore: true, metrics_address: FromStr::from_str("0.0.0.0:6116").unwrap(), rpc_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), crate::rpc::DEFAULT_PORT), + rpc_filter_list: None, healthcheck_address: SocketAddr::new( IpAddr::V4(Ipv4Addr::LOCALHOST), crate::health::DEFAULT_HEALTHCHECK_PORT, diff --git a/src/cli_shared/cli/mod.rs b/src/cli_shared/cli/mod.rs index 7ce9a906dfac..0c78c733a657 100644 --- a/src/cli_shared/cli/mod.rs +++ b/src/cli_shared/cli/mod.rs @@ -57,6 +57,9 @@ pub struct CliOpts { /// Address used for RPC. By defaults binds on localhost on port 2345. #[arg(long)] pub rpc_address: Option, + /// Path to a list of RPC methods to allow/disallow. + #[arg(long)] + pub rpc_filter_list: Option, /// Disable healthcheck endpoints #[arg(long)] pub no_healthcheck: bool, @@ -165,6 +168,7 @@ impl CliOpts { } if self.rpc.unwrap_or(cfg.client.enable_rpc) { cfg.client.enable_rpc = true; + cfg.client.rpc_filter_list = self.rpc_filter_list.clone(); if let Some(rpc_address) = self.rpc_address { cfg.client.rpc_address = rpc_address; } diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 3d91f2a390e7..8f37b7ada483 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -397,6 +397,12 @@ pub(super) async fn start( let keystore_rpc = Arc::clone(&keystore); let rpc_state_manager = Arc::clone(&state_manager); let rpc_address = config.client.rpc_address; + let filter_list = config + .client + .rpc_filter_list + .as_ref() + .map(|path| crate::rpc::FilterList::new_from_file(path)) + .transpose()?; info!("JSON-RPC endpoint will listen at {rpc_address}"); @@ -418,6 +424,7 @@ pub(super) async fn start( tipset_send: tipset_sender, }, rpc_address, + filter_list, ) .await }); diff --git a/src/rpc/filter_layer.rs b/src/rpc/filter_layer.rs new file mode 100644 index 000000000000..c1ccc0c204ff --- /dev/null +++ b/src/rpc/filter_layer.rs @@ -0,0 +1,71 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::sync::Arc; + +use futures::future::BoxFuture; +use futures::FutureExt; +use jsonrpsee::server::middleware::rpc::RpcServiceT; +use jsonrpsee::types::ErrorObject; +use jsonrpsee::MethodResponse; +use tower::Layer; + +use super::FilterList; + +/// JSON-RPC middleware layer for filtering RPC methods based on their name. +#[derive(Clone, Default)] +pub(super) struct FilterLayer { + filter_list: Arc, +} + +impl FilterLayer { + pub fn new(filter_list: FilterList) -> Self { + Self { + filter_list: Arc::new(filter_list), + } + } +} + +impl Layer for FilterLayer { + type Service = Filtering; + + fn layer(&self, service: S) -> Self::Service { + Filtering { + service, + filter_list: self.filter_list.clone(), + } + } +} + +#[derive(Clone)] +pub(super) struct Filtering { + service: S, + filter_list: Arc, +} + +impl<'a, S> RpcServiceT<'a> for Filtering +where + S: RpcServiceT<'a> + Send + Sync + Clone + 'static, +{ + type Future = BoxFuture<'a, MethodResponse>; + + fn call(&self, req: jsonrpsee::types::Request<'a>) -> Self::Future { + let service = self.service.clone(); + let authorized = self.filter_list.authorize(req.method_name()); + async move { + if authorized { + service.call(req).await + } else { + MethodResponse::error( + req.id(), + ErrorObject::borrowed( + http::StatusCode::FORBIDDEN.as_u16() as _, + "Forbidden", + None, + ), + ) + } + } + .boxed() + } +} diff --git a/src/rpc/filter_list.rs b/src/rpc/filter_list.rs new file mode 100644 index 000000000000..8f6f1bdcbcf2 --- /dev/null +++ b/src/rpc/filter_list.rs @@ -0,0 +1,126 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::path::Path; + +/// A filter list that allows or rejects RPC methods based on their name. +#[derive(Default, Clone)] +pub struct FilterList { + allow: Vec, + reject: Vec, +} + +impl FilterList { + pub fn new_from_file(file: &Path) -> anyhow::Result { + let (allow, reject) = Self::create_allow_reject_list(file)?; + Ok(Self { allow, reject }) + } + + /// Authorize (or not) an RPC method based on its name. + /// If the allow list is empty, all methods are authorized, unless they are rejected. + pub fn authorize(&self, entry: impl AsRef) -> bool { + let entry = entry.as_ref(); + (self.allow.is_empty() || self.allow.iter().any(|a| entry.contains(a))) + && !self.reject.iter().any(|r| entry.contains(r)) + } + + pub fn allow(mut self, entry: String) -> Self { + self.allow.push(entry); + self + } + + #[allow(dead_code)] + pub fn reject(mut self, entry: String) -> Self { + self.reject.push(entry); + self + } + + /// Create a list of allowed and rejected RPC methods from a file. + fn create_allow_reject_list(file: &Path) -> anyhow::Result<(Vec, Vec)> { + let filter_file = std::fs::read_to_string(file)?; + let (reject, allow): (Vec<_>, Vec<_>) = filter_file + .lines() + .map(|line| line.trim().to_owned()) + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .partition(|line| line.starts_with('!')); + + let reject = reject + .into_iter() + .map(|entry| entry.trim_start_matches('!').to_owned()) + .collect::>(); + + Ok((allow, reject)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn test_filter_list_creation() { + // Create a temporary file and write some test data to it + let mut filter_file = tempfile::Builder::new().tempfile().unwrap(); + let list = FilterList::new_from_file(filter_file.path()).unwrap(); + assert!(list.allow.is_empty()); + assert!(list.reject.is_empty()); + + write!( + filter_file, + r#"# This is a comment + !cthulhu + azathoth + !nyarlathotep + "# + ) + .unwrap(); + + let list = FilterList::new_from_file(filter_file.path()).unwrap(); + assert_eq!(list.allow, vec!["azathoth".to_string()]); + assert_eq!( + list.reject, + vec!["cthulhu".to_string(), "nyarlathotep".to_string()] + ); + + let list = list + .allow("shub-niggurath".to_string()) + .reject("yog-sothoth".to_string()); + assert_eq!( + list.allow, + vec!["azathoth".to_string(), "shub-niggurath".to_string()] + ); + } + + #[test] + fn test_filter_list_authorize() { + let list = FilterList::default(); + // if allow is empty, all entries are authorized + assert!(list.authorize("Filecoin.ChainGetBlock")); + assert!(list.authorize("Filecoin.StateNetworkName")); + + // all entries are authorized, except the rejected ones + let list = list.reject("Network".to_string()); + assert!(list.authorize("Filecoin.ChainGetBlock")); + + // case-sensitive + assert!(list.authorize("Filecoin.StatenetworkName")); + assert!(!list.authorize("Filecoin.StateNetworkName")); + + // if allow is not empty, only the allowed entries are authorized + let list = FilterList::default().allow("Chain".to_string()); + assert!(list.authorize("Filecoin.ChainGetBlock")); + assert!(!list.authorize("Filecoin.StateNetworkName")); + + // unless they are rejected + let list = list.reject("GetBlock".to_string()); + assert!(!list.authorize("Filecoin.ChainGetBlock")); + assert!(list.authorize("Filecoin.ChainGetMessage")); + + // reject takes precedence over allow + let list = FilterList::default() + .allow("Chain".to_string()) + .reject("Chain".to_string()); + assert!(!list.authorize("Filecoin.ChainGetBlock")); + } +} diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 151be99e1613..153ef3666add 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -4,6 +4,8 @@ mod auth_layer; mod channel; mod client; +mod filter_layer; +mod filter_list; mod log_layer; mod metrics_layer; mod request; @@ -11,6 +13,8 @@ mod request; pub use client::Client; pub use error::ServerError; use eth::filter::EthEventHandler; +use filter_layer::FilterLayer; +pub use filter_list::FilterList; use futures::FutureExt as _; use log_layer::LogLayer; use reflect::Ctx; @@ -442,10 +446,15 @@ struct PerConnection { keystore: Arc>, } -pub async fn start_rpc(state: RPCState, rpc_endpoint: SocketAddr) -> anyhow::Result<()> +pub async fn start_rpc( + state: RPCState, + rpc_endpoint: SocketAddr, + filter_list: Option, +) -> anyhow::Result<()> where DB: Blockstore + Send + Sync + 'static, { + let filter_list = filter_list.unwrap_or_default(); // `Arc` is needed because we will share the state between two modules let state = Arc::new(state); let keystore = state.keystore.clone(); @@ -490,6 +499,7 @@ where let svc = tower::service_fn({ let per_conn = per_conn.clone(); + let filter_list = filter_list.clone(); move |req| { let is_websocket = jsonrpsee::server::ws::is_upgrade_request(&req); let PerConnection { @@ -508,6 +518,7 @@ where // with data from the connection such as the headers in this example let headers = req.headers().clone(); let rpc_middleware = RpcServiceBuilder::new() + .layer(FilterLayer::new(filter_list.clone())) .layer(AuthLayer { headers, keystore: keystore.clone(), diff --git a/src/tool/offline_server/server.rs b/src/tool/offline_server/server.rs index 505276154dea..2d0d11b07f46 100644 --- a/src/tool/offline_server/server.rs +++ b/src/tool/offline_server/server.rs @@ -165,7 +165,7 @@ where let mut terminate = signal(SignalKind::terminate())?; let result = tokio::select! { - ret = start_rpc(state, rpc_address) => ret, + ret = start_rpc(state, rpc_address, None) => ret, _ = ctrl_c() => { info!("Keyboard interrupt."); Ok(()) diff --git a/src/tool/subcommands/api_cmd.rs b/src/tool/subcommands/api_cmd.rs index 2892cb2405ca..23f30388bdc4 100644 --- a/src/tool/subcommands/api_cmd.rs +++ b/src/tool/subcommands/api_cmd.rs @@ -20,6 +20,7 @@ use crate::rpc::gas::GasEstimateGasLimit; use crate::rpc::miner::BlockTemplate; use crate::rpc::state::StateGetAllClaims; use crate::rpc::types::{ApiTipsetKey, MessageFilter, MessageLookup}; +use crate::rpc::FilterList; use crate::rpc::{prelude::*, Permission}; use crate::shim::actors::market; use crate::shim::actors::MarketActorStateLoad as _; @@ -2139,125 +2140,3 @@ fn validate_message_lookup(req: rpc::Request) -> RpcTest { forest == lotus }) } - -/// A filter list that allows or rejects RPC methods based on their name. -#[derive(Default)] -struct FilterList { - allow: Vec, - reject: Vec, -} - -impl FilterList { - fn new_from_file(file: &Path) -> anyhow::Result { - let (allow, reject) = Self::create_allow_reject_list(file)?; - Ok(Self { allow, reject }) - } - - /// Authorize (or not) an RPC method based on its name. - /// If the allow list is empty, all methods are authorized, unless they are rejected. - fn authorize(&self, entry: impl AsRef) -> bool { - let entry = entry.as_ref(); - (self.allow.is_empty() || self.allow.iter().any(|a| entry.contains(a))) - && !self.reject.iter().any(|r| entry.contains(r)) - } - - fn allow(mut self, entry: String) -> Self { - self.allow.push(entry); - self - } - - #[allow(dead_code)] - fn reject(mut self, entry: String) -> Self { - self.reject.push(entry); - self - } - - /// Create a list of allowed and rejected RPC methods from a file. - fn create_allow_reject_list(file: &Path) -> anyhow::Result<(Vec, Vec)> { - let filter_file = std::fs::read_to_string(file)?; - let (reject, allow): (Vec<_>, Vec<_>) = filter_file - .lines() - .map(|line| line.trim().to_owned()) - .filter(|line| !line.is_empty() && !line.starts_with('#')) - .partition(|line| line.starts_with('!')); - - let reject = reject - .into_iter() - .map(|entry| entry.trim_start_matches('!').to_owned()) - .collect::>(); - - Ok((allow, reject)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - - #[test] - fn test_filter_list_creation() { - // Create a temporary file and write some test data to it - let mut filter_file = tempfile::Builder::new().tempfile().unwrap(); - let list = FilterList::new_from_file(filter_file.path()).unwrap(); - assert!(list.allow.is_empty()); - assert!(list.reject.is_empty()); - - write!( - filter_file, - r#"# This is a comment - !cthulhu - azathoth - !nyarlathotep - "# - ) - .unwrap(); - - let list = FilterList::new_from_file(filter_file.path()).unwrap(); - assert_eq!(list.allow, vec!["azathoth".to_string()]); - assert_eq!( - list.reject, - vec!["cthulhu".to_string(), "nyarlathotep".to_string()] - ); - - let list = list - .allow("shub-niggurath".to_string()) - .reject("yog-sothoth".to_string()); - assert_eq!( - list.allow, - vec!["azathoth".to_string(), "shub-niggurath".to_string()] - ); - } - - #[test] - fn test_filter_list_authorize() { - let list = FilterList::default(); - // if allow is empty, all entries are authorized - assert!(list.authorize("Filecoin.ChainGetBlock")); - assert!(list.authorize("Filecoin.StateNetworkName")); - - // all entries are authorized, except the rejected ones - let list = list.reject("Network".to_string()); - assert!(list.authorize("Filecoin.ChainGetBlock")); - - // case-sensitive - assert!(list.authorize("Filecoin.StatenetworkName")); - assert!(!list.authorize("Filecoin.StateNetworkName")); - - // if allow is not empty, only the allowed entries are authorized - let list = FilterList::default().allow("Chain".to_string()); - assert!(list.authorize("Filecoin.ChainGetBlock")); - assert!(!list.authorize("Filecoin.StateNetworkName")); - - // unless they are rejected - let list = list.reject("GetBlock".to_string()); - assert!(!list.authorize("Filecoin.ChainGetBlock")); - assert!(list.authorize("Filecoin.ChainGetMessage")); - - // reject takes precedence over allow - let list = FilterList::default() - .allow("Chain".to_string()) - .reject("Chain".to_string()); - assert!(!list.authorize("Filecoin.ChainGetBlock")); - } -}