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