Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

### Added

- [#6008](https://github.com/ChainSafe/forest/issues/6008): Added the `FOREST_PATH` environment variable to override the Forest data directory (taking precedence over the configuration file and the default), mirroring Lotus' `LOTUS_PATH`. It is honored by all `forest*` binaries, so `forest-cli`/`forest-tool` read the JWT admin token from the same directory. The resolved data directory is now logged on daemon startup.

- [#7168](https://github.com/ChainSafe/forest/pull/7168): Added the `FOREST_RPC_METRICS_DISABLED` environment variable to disable JSON-RPC per-method metrics while leaving other metrics intact.

- [#7195](https://github.com/ChainSafe/forest/pull/7195): Added the `rpc_in_flight_requests` metric reporting the number of JSON-RPC requests currently being processed.
Expand Down
125 changes: 63 additions & 62 deletions docs/docs/users/reference/env_variables.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/tests/butterflynet_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function shutdown {

trap shutdown EXIT

$FOREST_PATH --chain butterflynet --encrypt-keystore false &
$FOREST_DAEMON_PATH --chain butterflynet --encrypt-keystore false &
FOREST_NODE_PID=$!

forest_wait_api
Expand Down
2 changes: 1 addition & 1 deletion scripts/tests/calibnet_kademlia_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ cat <<- EOF > $CONFIG_PATH
kademlia = true
EOF

$FOREST_PATH --chain calibnet --encrypt-keystore false --auto-download-snapshot --config "$CONFIG_PATH" --save-token ./admin_token --rpc-address 127.0.0.1:12345 --metrics-address 127.0.0.1:6117 --healthcheck-address 127.0.0.1:2347 &
$FOREST_DAEMON_PATH --chain calibnet --encrypt-keystore false --auto-download-snapshot --config "$CONFIG_PATH" --save-token ./admin_token --rpc-address 127.0.0.1:12345 --metrics-address 127.0.0.1:6117 --healthcheck-address 127.0.0.1:2347 &
FOREST_NODE_PID=$!
# Verify that more peers are connected via kademlia
until (( $(curl http://127.0.0.1:6117/metrics | grep full_peers | tail -n 1 | cut --delimiter=" " --fields=2) > 1 )); do
Expand Down
4 changes: 2 additions & 2 deletions scripts/tests/calibnet_migration_regression_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ set -e
# migration point and then we validate the last 200 tipsets. This triggers the
# migration logic without connecting to the real Filecoin network.

FOREST_PATH="forest"
MIGRATION_TEST="/usr/bin/time -v $FOREST_PATH --chain calibnet --encrypt-keystore false --halt-after-import --height=-200 --no-gc --import-snapshot"
FOREST_DAEMON_PATH="forest"
MIGRATION_TEST="/usr/bin/time -v $FOREST_DAEMON_PATH --chain calibnet --encrypt-keystore false --halt-after-import --height=-200 --no-gc --import-snapshot"

# NV17 - Shark, uncomment when we support the nv17 migration
echo NV17 - Shark
Expand Down
4 changes: 2 additions & 2 deletions scripts/tests/calibnet_no_discovery_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ function shutdown {

trap shutdown EXIT

$FOREST_PATH --chain calibnet --encrypt-keystore false --mdns false --kademlia false --auto-download-snapshot --exit-after-init
$FOREST_PATH --chain calibnet --encrypt-keystore false --mdns false --kademlia false --auto-download-snapshot --log-dir "$LOG_DIRECTORY" &
$FOREST_DAEMON_PATH --chain calibnet --encrypt-keystore false --mdns false --kademlia false --auto-download-snapshot --exit-after-init
$FOREST_DAEMON_PATH --chain calibnet --encrypt-keystore false --mdns false --kademlia false --auto-download-snapshot --log-dir "$LOG_DIRECTORY" &
FOREST_NODE_PID=$!

forest_wait_api
Expand Down
2 changes: 1 addition & 1 deletion scripts/tests/calibnet_stateless_mode_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ cat <<- EOF > $CONFIG_PATH
EOF

# Disable discovery to not connect to more nodes
$FOREST_PATH --chain calibnet --encrypt-keystore false --auto-download-snapshot --config "$CONFIG_PATH" --rpc false --metrics-address 127.0.0.1:6117 --healthcheck-address 127.0.0.1:2347 &
$FOREST_DAEMON_PATH --chain calibnet --encrypt-keystore false --auto-download-snapshot --config "$CONFIG_PATH" --rpc false --metrics-address 127.0.0.1:6117 --healthcheck-address 127.0.0.1:2347 &
FOREST_NODE_PID=$!
# Verify that the stateless node can respond to chain exchange requests
until curl http://127.0.0.1:6117/metrics | grep "chain_exchange_response_in"; do
Expand Down
2 changes: 1 addition & 1 deletion scripts/tests/calibnet_stateless_rpc_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function forest_run_node_stateless_detached_with_filter_list {
pkill -9 forest || true
local filter_list=$1

$FOREST_PATH --chain calibnet --encrypt-keystore false --log-dir "$LOG_DIRECTORY" --stateless --rpc-filter-list "$filter_list" &
$FOREST_DAEMON_PATH --chain calibnet --encrypt-keystore false --log-dir "$LOG_DIRECTORY" --stateless --rpc-filter-list "$filter_list" &
forest_wait_api
}

Expand Down
14 changes: 9 additions & 5 deletions scripts/tests/harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

export FOREST_CHAIN_INDEXER_ENABLED="1"

export FOREST_PATH="forest"
# Path to the `forest` daemon binary. Note this is intentionally NOT named
# `FOREST_PATH`: that environment variable is honored by Forest itself to
# override the data directory, so exporting it here would clobber the data dir
# of every daemon launched by these tests.
export FOREST_DAEMON_PATH="forest"
export FOREST_CLI_PATH="forest-cli"
export FOREST_WALLET_PATH="forest-wallet"
export FOREST_TOOL_PATH="forest-tool"
Expand All @@ -18,12 +22,12 @@ export LOG_DIRECTORY

function forest_import_non_calibnet_snapshot {
echo "Importing a non calibnet snapshot"
$FOREST_PATH --chain calibnet --encrypt-keystore false --halt-after-import --import-snapshot ./test-snapshots/chain4.car
$FOREST_DAEMON_PATH --chain calibnet --encrypt-keystore false --halt-after-import --import-snapshot ./test-snapshots/chain4.car
}

function forest_download_and_import_snapshot {
echo "Downloading and importing snapshot"
$FOREST_PATH --chain calibnet --encrypt-keystore false --halt-after-import --height=-200 --auto-download-snapshot
$FOREST_DAEMON_PATH --chain calibnet --encrypt-keystore false --halt-after-import --height=-200 --auto-download-snapshot
}

function get_epoch_from_car_db {
Expand Down Expand Up @@ -71,7 +75,7 @@ function forest_query_format {

function forest_run_node_detached {
echo "Running forest"
/usr/bin/time -v $FOREST_PATH --chain calibnet --encrypt-keystore false --log-dir "$LOG_DIRECTORY" &
/usr/bin/time -v $FOREST_DAEMON_PATH --chain calibnet --encrypt-keystore false --log-dir "$LOG_DIRECTORY" &
}

function forest_run_node_stateless_detached {
Expand All @@ -86,7 +90,7 @@ function forest_run_node_stateless_detached {
listening_multiaddrs = ["/ip4/127.0.0.1/tcp/0", "/ip4/127.0.0.1/udp/0/quic-v1"]
EOF

$FOREST_PATH --chain calibnet --encrypt-keystore false --config "$CONFIG_PATH" --log-dir "$LOG_DIRECTORY" --save-token ./stateless_admin_token --stateless &
$FOREST_DAEMON_PATH --chain calibnet --encrypt-keystore false --config "$CONFIG_PATH" --log-dir "$LOG_DIRECTORY" --save-token ./stateless_admin_token --stateless &
}

function forest_wait_api {
Expand Down
5 changes: 4 additions & 1 deletion src/cli_shared/cli/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,11 @@ impl Default for Client {
}

impl Client {
/// File name of the RPC admin token, stored within the data directory.
pub const RPC_TOKEN_FILENAME: &'static str = "token";

pub fn default_rpc_token_path(&self) -> PathBuf {
self.data_dir.join("token")
self.data_dir.join(Self::RPC_TOKEN_FILENAME)
}

pub fn rpc_v1_endpoint(&self) -> Result<url::Url, url::ParseError> {
Expand Down
75 changes: 75 additions & 0 deletions src/cli_shared/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ cfg_if::cfg_if! {
}
}

/// Environment variable that overrides the Forest data directory, taking
/// precedence over both the configuration file and the built-in default. Named
/// after Lotus' `LOTUS_PATH` to ease switching between implementations.
pub const FOREST_DATA_DIR_ENV: &str = "FOREST_PATH";

/// Gets chain data directory
pub fn chain_path(config: &Config) -> PathBuf {
PathBuf::from(&config.client.data_dir).join(config.chain().to_string())
Expand All @@ -37,9 +42,39 @@ pub fn read_config(
if let Some(chain) = chain_opt {
config.chain = chain;
}
// The `FOREST_PATH` environment variable takes precedence over the data
// directory set in the configuration file (or the default one).
if let Some(data_dir) = data_dir_from_env() {
config.client.data_dir = data_dir;
}
Ok((path, config))
}

/// Returns the data directory set via the [`FOREST_DATA_DIR_ENV`] environment
/// variable, if it is present and non-empty.
fn data_dir_from_env() -> Option<PathBuf> {
match std::env::var(FOREST_DATA_DIR_ENV) {
Ok(s) if !s.trim().is_empty() => Some(PathBuf::from(s)),
_ => None,
}
}

/// Returns the effective Forest data directory: the [`FOREST_DATA_DIR_ENV`]
/// environment variable if set, otherwise the built-in default. Unlike
/// [`read_config`], this does not consult a configuration file and is meant for
/// contexts (e.g. the RPC client) that need the data directory without loading
/// the full configuration.
pub fn default_data_dir() -> PathBuf {
data_dir_from_env().unwrap_or_else(|| crate::cli_shared::cli::Client::default().data_dir)
}

/// Returns the path to the RPC admin token within the effective data directory
/// (see [`default_data_dir`]). This is where a daemon started with the same
/// environment saves the token, so clients can read it back from here.
pub fn default_token_path() -> PathBuf {
default_data_dir().join(crate::cli_shared::cli::Client::RPC_TOKEN_FILENAME)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -68,7 +103,47 @@ mod tests {
assert_eq!(config.chain(), &NetworkChain::Butterflynet);
}

/// Runs `f` with [`FOREST_DATA_DIR_ENV`] set to `value`, restoring the
/// environment afterwards.
fn with_data_dir_env<T>(value: &str, f: impl FnOnce() -> T) -> T {
unsafe { std::env::set_var(FOREST_DATA_DIR_ENV, value) };
let result = f();
unsafe { std::env::remove_var(FOREST_DATA_DIR_ENV) };
result
}

#[test]
#[serial_test::serial]
fn read_config_data_dir_env_override() {
let data_dir = "/tmp/forest-path-env-override-test";
let (_, config) = with_data_dir_env(data_dir, || read_config(None, None).unwrap());

// The env variable takes precedence over the default data directory.
assert_eq!(config.client.data_dir, std::path::Path::new(data_dir));
}

#[test]
#[serial_test::serial]
fn default_data_dir_honors_env_override() {
let data_dir = "/tmp/forest-path-default-data-dir-test";
let resolved = with_data_dir_env(data_dir, default_data_dir);
assert_eq!(resolved, std::path::Path::new(data_dir));

// Without the env variable, it falls back to the default data directory.
assert_eq!(default_data_dir(), Config::default().client.data_dir);
}

#[test]
#[serial_test::serial]
fn read_config_data_dir_env_empty_is_ignored() {
let (_, config) = with_data_dir_env("", || read_config(None, None).unwrap());

// An empty env variable falls back to the default data directory.
assert_eq!(config.client.data_dir, Config::default().client.data_dir);
}

#[test]
#[serial_test::serial]
fn read_config_with_path() {
let default_config = Config::default();
let temp_dir = tempfile::tempdir().expect("couldn't create temp dir");
Expand Down
1 change: 1 addition & 0 deletions src/daemon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ fn startup_init(config: &Config) -> anyhow::Result<()> {
"Starting Forest daemon, version {}",
FOREST_VERSION_STRING.as_str()
);
info!("Using data directory: {}", config.client.data_dir.display());
Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion src/dev/subcommands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ impl Subcommand {
async fn fetch_test_snapshots(actor_bundle: Option<PathBuf>) -> anyhow::Result<()> {
// Prepare proof parameter files
crate::utils::proofs_api::maybe_set_proofs_parameter_cache_dir_env(
&crate::Config::default().client.data_dir,
&crate::cli_shared::default_data_dir(),
);
ensure_proof_params_downloaded().await?;

Expand Down
30 changes: 28 additions & 2 deletions src/rpc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ impl Client {
}
// Set default token if not provided
if token.is_none() && base_url.password().is_none() {
let client_config = crate::cli_shared::cli::Client::default();
let default_token_path = client_config.default_rpc_token_path();
// Honor the `FOREST_PATH` data directory override so the token saved
// by a daemon started with `FOREST_PATH` set is found here as well.
let default_token_path = crate::cli_shared::default_token_path();
if default_token_path.is_file() {
if let Ok(token) = std::fs::read_to_string(&default_token_path) {
if base_url.set_password(Some(token.trim())).is_ok() {
Expand Down Expand Up @@ -325,3 +326,28 @@ impl jsonrpsee::core::client::SubscriptionClientT for UrlClient {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::cli_shared::FOREST_DATA_DIR_ENV;

// The RPC client should pick up the admin token from the data directory
// pointed to by `FOREST_PATH`, mirroring where a daemon started with the
// same variable saves it.
#[test]
#[serial_test::serial]
fn default_token_is_loaded_from_forest_path_data_dir() {
let tmp_dir = tempfile::tempdir().unwrap();
std::fs::write(tmp_dir.path().join("token"), "secret-token").unwrap();

unsafe {
env::remove_var("FULLNODE_API_INFO");
env::set_var(FOREST_DATA_DIR_ENV, tmp_dir.path());
}
let client = Client::default_or_from_env(None).unwrap();
unsafe { env::remove_var(FOREST_DATA_DIR_ENV) };

assert_eq!(client.token.as_deref(), Some("secret-token"));
}
}
4 changes: 2 additions & 2 deletions src/tool/offline_server/server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use crate::JWT_IDENTIFIER;
use crate::auth::generate_priv_key;
use crate::chain::ChainStore;
use crate::chain_sync::SyncStatusReport;
Expand All @@ -21,7 +22,6 @@ use crate::shim::address::{CurrentNetwork, Network};
use crate::state_manager::StateManager;
use crate::utils::net::{DownloadFileOption, download_to};
use crate::utils::proofs_api::{self, ensure_proof_params_downloaded};
use crate::{Config, JWT_IDENTIFIER};
use jsonrpsee::server::stop_channel;
use parking_lot::RwLock;
use std::{
Expand Down Expand Up @@ -132,7 +132,7 @@ pub async fn start_offline_server(

// Set proof parameter data dir and make sure the proofs are available. Otherwise,
// validation might fail due to missing proof parameters.
proofs_api::maybe_set_proofs_parameter_cache_dir_env(&Config::default().client.data_dir);
proofs_api::maybe_set_proofs_parameter_cache_dir_env(&crate::cli_shared::default_data_dir());
ensure_proof_params_downloaded().await?;

let db = {
Expand Down
4 changes: 2 additions & 2 deletions src/tool/subcommands/api_cmd/api_compare_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::eth::EthChainId as EthChainIdType;
use crate::lotus_json::HasLotusJson;
use crate::message::{MessageRead as _, SignedMessage};
use crate::prelude::*;
use crate::rpc;
use crate::rpc::auth::AuthNewParams;
use crate::rpc::beacon::BeaconGetEntry;
use crate::rpc::eth::{
Expand Down Expand Up @@ -39,7 +40,6 @@ use crate::tool::subcommands::api_cmd::NetworkChain;
use crate::tool::subcommands::api_cmd::report::ReportBuilder;
use crate::tool::subcommands::api_cmd::state_decode_params_tests::create_all_state_decode_params_tests;
use crate::utils::proofs_api::{self, ensure_proof_params_downloaded};
use crate::{Config, rpc};
use ahash::HashMap;
use bls_signatures::Serialize as _;
use chrono::Utc;
Expand Down Expand Up @@ -2633,7 +2633,7 @@ async fn revalidate_chain(db: Arc<ManyCar>, n_ts_to_validate: usize) -> anyhow::

// Set proof parameter data dir and make sure the proofs are available. Otherwise,
// validation might fail due to missing proof parameters.
proofs_api::maybe_set_proofs_parameter_cache_dir_env(&Config::default().client.data_dir);
proofs_api::maybe_set_proofs_parameter_cache_dir_env(&crate::cli_shared::default_data_dir());
ensure_proof_params_downloaded().await?;
state_manager.validate_tipsets_blocking(
head_ts
Expand Down
3 changes: 1 addition & 2 deletions src/tool/subcommands/api_cmd/test_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ pub(super) async fn drain_mpool_services(mut services: JoinSet<anyhow::Result<()
#[cfg(test)]
mod tests {
use super::*;
use crate::Config;
use crate::utils::proofs_api::ensure_proof_params_downloaded;
use ahash::HashSet;
use std::sync::LazyLock;
Expand All @@ -228,7 +227,7 @@ mod tests {
LazyLock::force(&INIT_RNG_SEED);
tokio::time::timeout(RPC_REGRESSION_TEST_TIMEOUT, async {
crate::utils::proofs_api::maybe_set_proofs_parameter_cache_dir_env(
&Config::default().client.data_dir,
&crate::cli_shared::default_data_dir(),
);
ensure_proof_params_downloaded().await.unwrap();
let path = crate::dev::subcommands::fetch_rpc_test_snapshot(name.into())
Expand Down
2 changes: 1 addition & 1 deletion src/tool/subcommands/snapshot_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ where

// Set proof parameter data dir before downloading proofs.
crate::utils::proofs_api::maybe_set_proofs_parameter_cache_dir_env(
&Config::default().client.data_dir,
&crate::cli_shared::default_data_dir(),
);

// independent downloads - fetch in parallel
Expand Down
21 changes: 21 additions & 0 deletions tests/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,27 @@ fn test_config_parameter() {
assert!(tmp_dir.is_dir());
}

// Verify that the data directory can be set with the `FOREST_PATH` environment
// variable alone, without any configuration file. We assume 'data_dir' will be
// created iff the path is correctly honored.
#[test]
fn test_data_dir_env_var() {
let tmp_dir = TempDir::new().unwrap();
let data_dir = tmp_dir.path().join("forest-path-data-dir");
assert!(!data_dir.exists());

daemon()
.common_args()
.arg("--chain")
.arg("calibnet")
.arg("--encrypt-keystore")
.arg("false")
.env("FOREST_PATH", &data_dir)
.assert()
.success();
assert!(data_dir.is_dir());
}

// Verify that a configuration path can be set with FOREST_CONFIG_PATH. We
// assume 'data_dir' will be created iff the configuration is correctly parsed.
#[test]
Expand Down
Loading