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