Skip to main content

forest/daemon/
context.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use crate::auth::{ADMIN, create_token, generate_priv_key};
5use crate::chain::ChainStore;
6use crate::cli_shared::chain_path;
7use crate::cli_shared::cli::CliOpts;
8use crate::daemon::asyncify;
9use crate::daemon::bundle::load_actor_bundles;
10use crate::daemon::db_util::load_all_forest_cars_with_cleanup;
11use crate::db::CAR_DB_DIR_NAME;
12use crate::db::car::ManyCar;
13use crate::db::db_engine::db_root;
14use crate::db::parity_db::{GarbageCollectableParityDb, ParityDb};
15use crate::genesis::read_genesis_header;
16use crate::libp2p::{Keypair, PeerId};
17use crate::networks::ChainConfig;
18use crate::prelude::*;
19use crate::rpc::sync::SnapshotProgressTracker;
20use crate::shim::address::CurrentNetwork;
21use crate::state_manager::StateManager;
22use crate::{
23    Config, ENCRYPTED_KEYSTORE_NAME, FOREST_KEYSTORE_PHRASE_ENV, JWT_IDENTIFIER, KeyStore,
24    KeyStoreConfig,
25};
26use anyhow::Context;
27use dialoguer::console::Term;
28use fvm_shared4::address::Network;
29use parking_lot::RwLock;
30use std::cell::RefCell;
31use std::path::PathBuf;
32use std::sync::Arc;
33use tracing::{info, warn};
34
35type DbType = Arc<ManyCar<Arc<GarbageCollectableParityDb>>>;
36
37pub struct AppContext {
38    pub net_keypair: Keypair,
39    pub p2p_peer_id: PeerId,
40    pub db: DbType,
41    pub db_meta_data: DbMetadata,
42    pub state_manager: StateManager,
43    pub keystore: Arc<RwLock<KeyStore>>,
44    pub admin_jwt: String,
45    pub snapshot_progress_tracker: SnapshotProgressTracker,
46    pub temp_dir: std::path::PathBuf,
47}
48
49impl AppContext {
50    pub async fn init(opts: &CliOpts, cfg: &Config) -> anyhow::Result<AppContext> {
51        let chain_cfg = get_chain_config_and_set_network(cfg);
52        let (net_keypair, p2p_peer_id) = get_or_create_p2p_keypair_and_peer_id(cfg)?;
53        let (db, db_meta_data) = setup_db(opts, cfg).await?;
54        db.shallow_clone().register_metrics();
55        let state_manager = create_state_manager(cfg, &db, &chain_cfg).await?;
56        let (keystore, admin_jwt) = load_or_create_keystore_and_configure_jwt(opts, cfg).await?;
57        let snapshot_progress_tracker = SnapshotProgressTracker::default();
58        let temp_dir = chain_path(cfg).join("tmp");
59        std::fs::create_dir_all(&temp_dir).context("Failed to create temporary directory")?;
60        Ok(Self {
61            net_keypair,
62            p2p_peer_id,
63            db,
64            db_meta_data,
65            state_manager,
66            keystore,
67            admin_jwt,
68            snapshot_progress_tracker,
69            temp_dir,
70        })
71    }
72
73    pub fn chain_config(&self) -> &Arc<ChainConfig> {
74        self.state_manager.chain_config()
75    }
76
77    pub fn chain_store(&self) -> &ChainStore {
78        self.state_manager.chain_store()
79    }
80}
81
82fn get_chain_config_and_set_network(config: &Config) -> Arc<ChainConfig> {
83    let chain_config = ChainConfig::from_chain(config.chain());
84    if chain_config.is_testnet() {
85        CurrentNetwork::set_global(Network::Testnet);
86    }
87    Arc::new(ChainConfig {
88        enable_indexer: config.chain_indexer.enable_indexer,
89        default_max_fee: config.fee.max_fee.clone(),
90        ..chain_config
91    })
92}
93
94fn get_or_create_p2p_keypair_and_peer_id(config: &Config) -> anyhow::Result<(Keypair, PeerId)> {
95    let path = config.client.data_dir.join("libp2p");
96    let keypair = crate::libp2p::keypair::get_or_create_keypair(&path)?;
97    let peer_id = keypair.public().to_peer_id();
98    Ok((keypair, peer_id))
99}
100
101/// This may:
102/// - create a [`KeyStore`]
103/// - load a [`KeyStore`]
104/// - ask a user for password input
105async fn load_or_create_keystore(config: &Config) -> anyhow::Result<KeyStore> {
106    use std::env::VarError;
107
108    let passphrase_from_env = std::env::var(FOREST_KEYSTORE_PHRASE_ENV);
109    let require_encryption = config.client.encrypt_keystore;
110    let keystore_already_exists = config
111        .client
112        .data_dir
113        .join(ENCRYPTED_KEYSTORE_NAME)
114        .is_dir();
115
116    match (require_encryption, passphrase_from_env) {
117        // don't need encryption, we can implicitly create a keystore
118        (false, maybe_passphrase) => {
119            warn!("Forest has encryption disabled");
120            if let Ok(_) | Err(VarError::NotUnicode(_)) = maybe_passphrase {
121                warn!(
122                    "Ignoring passphrase provided in {} - encryption is disabled",
123                    FOREST_KEYSTORE_PHRASE_ENV
124                )
125            }
126            KeyStore::new(KeyStoreConfig::Persistent(config.client.data_dir.clone()))
127                .map_err(anyhow::Error::new)
128        }
129
130        // need encryption, the user has provided the password through env
131        (true, Ok(passphrase)) => KeyStore::new(KeyStoreConfig::Encrypted(
132            config.client.data_dir.clone(),
133            passphrase,
134        ))
135        .map_err(anyhow::Error::new),
136
137        // need encryption, we've not been given a password
138        (true, Err(error)) => {
139            // prompt for passphrase and try and load the keystore
140
141            if let VarError::NotUnicode(_) = error {
142                // If we're ignoring the user's password, tell them why
143                warn!(
144                    "Ignoring passphrase provided in {} - it's not utf-8",
145                    FOREST_KEYSTORE_PHRASE_ENV
146                )
147            }
148
149            let data_dir = config.client.data_dir.clone();
150
151            match keystore_already_exists {
152                true => asyncify(move || input_password_to_load_encrypted_keystore(data_dir))
153                    .await
154                    .context("Couldn't load keystore"),
155                false => {
156                    let password =
157                        asyncify(|| create_password("Create a password for Forest's keystore"))
158                            .await?;
159                    KeyStore::new(KeyStoreConfig::Encrypted(data_dir, password))
160                        .context("Couldn't create keystore")
161                }
162            }
163        }
164    }
165}
166
167async fn load_or_create_keystore_and_configure_jwt(
168    opts: &CliOpts,
169    config: &Config,
170) -> anyhow::Result<(Arc<RwLock<KeyStore>>, String)> {
171    let mut keystore = load_or_create_keystore(config).await?;
172    if keystore.get(JWT_IDENTIFIER).is_err() {
173        keystore.put(JWT_IDENTIFIER, generate_priv_key())?;
174    }
175    let admin_jwt = handle_admin_token(opts, config, &keystore)?;
176    let keystore = Arc::new(RwLock::new(keystore));
177    Ok((keystore, admin_jwt))
178}
179
180fn maybe_migrate_db(config: &Config) {
181    // Try to migrate the database if needed. In case the migration fails, we fallback to creating a new database
182    // to avoid breaking the node.
183    let db_migration = crate::db::migration::DbMigration::new(config);
184    if let Err(e) = db_migration.migrate() {
185        warn!("Failed to migrate database: {e:#}");
186    }
187}
188
189pub(crate) struct DbMetadata {
190    db_root_dir: PathBuf,
191    forest_car_db_dir: PathBuf,
192}
193
194impl DbMetadata {
195    pub(crate) fn get_root_dir(&self) -> PathBuf {
196        self.db_root_dir.clone()
197    }
198
199    pub(crate) fn get_forest_car_db_dir(&self) -> PathBuf {
200        self.forest_car_db_dir.clone()
201    }
202}
203
204/// This function configures database with below steps
205/// - migrate database auto-magically on Forest version bump
206/// - load parity-db
207/// - load CAR database
208/// - load actor bundles
209async fn setup_db(opts: &CliOpts, config: &Config) -> anyhow::Result<(DbType, DbMetadata)> {
210    maybe_migrate_db(config);
211    let chain_data_path = chain_path(config);
212    let db_root_dir = db_root(&chain_data_path)?;
213    let db_writer = Arc::new(GarbageCollectableParityDb::new(ParityDb::to_options(
214        db_root_dir.clone(),
215        config.db_config(),
216    ))?);
217    let db = Arc::new(ManyCar::new(db_writer.clone()));
218    let forest_car_db_dir = db_root_dir.join(CAR_DB_DIR_NAME);
219    load_all_forest_cars_with_cleanup(&db, &forest_car_db_dir)?;
220    if config.client.load_actors && !opts.stateless {
221        load_actor_bundles(&db, config.chain()).await?;
222    }
223    Ok((
224        db,
225        DbMetadata {
226            db_root_dir,
227            forest_car_db_dir,
228        },
229    ))
230}
231
232async fn create_state_manager(
233    config: &Config,
234    db: &DbType,
235    chain_config: &Arc<ChainConfig>,
236) -> anyhow::Result<StateManager> {
237    // Read Genesis file
238    // * When snapshot command implemented, this genesis does not need to be
239    //   initialized
240    let genesis_header = read_genesis_header(
241        config.client.genesis_file.as_deref(),
242        chain_config.genesis_bytes(db).await?.as_deref(),
243        db,
244    )
245    .await?;
246
247    let chain_store = ChainStore::new(
248        db.shallow_clone(),
249        chain_config.shallow_clone(),
250        genesis_header,
251    )?;
252
253    // Initialize StateManager
254    let state_manager = StateManager::new(chain_store)?;
255    Ok(state_manager)
256}
257
258/// Prompts for password, looping until the [`KeyStore`] is successfully loaded.
259///
260/// This code makes blocking syscalls.
261fn input_password_to_load_encrypted_keystore(data_dir: PathBuf) -> dialoguer::Result<KeyStore> {
262    let keystore = RefCell::new(None);
263    let term = Term::stderr();
264
265    // Unlike `dialoguer::Confirm`, `dialoguer::Password` doesn't fail if the terminal is not a tty
266    // so do that check ourselves.
267    // This means users can't pipe their password from stdin.
268    if !term.is_term() {
269        return Err(std::io::Error::new(
270            std::io::ErrorKind::NotConnected,
271            "cannot read password from non-terminal",
272        )
273        .into());
274    }
275
276    dialoguer::Password::new()
277        .with_prompt("Enter the password for Forest's keystore")
278        .allow_empty_password(true) // let validator do validation
279        .validate_with(|input: &String| {
280            KeyStore::new(KeyStoreConfig::Encrypted(data_dir.clone(), input.clone()))
281                .map(|created| *keystore.borrow_mut() = Some(created))
282                .context(
283                    "Error: couldn't load keystore with this password. Try again or press Ctrl+C to abort.",
284                )
285        })
286        .interact_on(&term)?;
287
288    Ok(keystore
289        .into_inner()
290        .expect("validation succeeded, so keystore must be emplaced"))
291}
292
293/// Loops until the user provides two matching passwords.
294///
295/// This code makes blocking syscalls
296fn create_password(prompt: &str) -> dialoguer::Result<String> {
297    let term = Term::stderr();
298
299    // Unlike `dialoguer::Confirm`, `dialoguer::Password` doesn't fail if the terminal is not a tty
300    // so do that check ourselves.
301    // This means users can't pipe their password from stdin.
302    if !term.is_term() {
303        return Err(std::io::Error::new(
304            std::io::ErrorKind::NotConnected,
305            "cannot read password from non-terminal",
306        )
307        .into());
308    }
309    dialoguer::Password::new()
310        .with_prompt(prompt)
311        .allow_empty_password(false)
312        .with_confirmation(
313            "Confirm password",
314            "Error: the passwords do not match. Try again or press Ctrl+C to abort.",
315        )
316        .interact_on(&term)
317}
318
319/// Generates, prints and optionally writes to a file the administrator JWT
320/// token.
321fn handle_admin_token(
322    opts: &CliOpts,
323    config: &Config,
324    keystore: &KeyStore,
325) -> anyhow::Result<String> {
326    let ki = keystore.get(JWT_IDENTIFIER)?;
327    // Lotus admin tokens do not expire but Forest requires all JWT tokens to
328    // have an expiration date. So we set the expiration date to 100 years in
329    // the future to match user-visible behavior of Lotus.
330    let token_exp = chrono::Duration::days(365 * 100);
331    let token = create_token(
332        ADMIN.iter().map(ToString::to_string).collect(),
333        ki.private_key(),
334        token_exp,
335    )?;
336    let default_token_path = config.client.default_rpc_token_path();
337    if let Err(e) =
338        crate::utils::io::write_new_sensitive_file(token.as_bytes(), &default_token_path)
339    {
340        tracing::warn!("Failed to save the default admin token file: {e}");
341    } else {
342        info!("Admin token is saved to {}", default_token_path.display());
343    }
344    if let Some(path) = opts.save_token.as_ref() {
345        if let Some(dir) = path.parent()
346            && !dir.is_dir()
347        {
348            std::fs::create_dir_all(dir).with_context(|| {
349                format!(
350                    "Failed to create `--save-token` directory {}",
351                    dir.display()
352                )
353            })?;
354        }
355        std::fs::write(path, &token)
356            .with_context(|| format!("Failed to save admin token to {}", path.display()))?;
357        info!("Admin token is saved to {}", path.display());
358    }
359
360    Ok(token)
361}