Coverage Report

Created: 2024-05-16 12:16

/__w/smoldot/smoldot/repo/full-node/bin/main.rs
Line
Count
Source (jump to first uncovered line)
1
// Smoldot
2
// Copyright (C) 2019-2022  Parity Technologies (UK) Ltd.
3
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
4
5
// This program is free software: you can redistribute it and/or modify
6
// it under the terms of the GNU General Public License as published by
7
// the Free Software Foundation, either version 3 of the License, or
8
// (at your option) any later version.
9
10
// This program is distributed in the hope that it will be useful,
11
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
// GNU General Public License for more details.
14
15
// You should have received a copy of the GNU General Public License
16
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18
#![deny(rustdoc::broken_intra_doc_links)]
19
// TODO: #![deny(unused_crate_dependencies)] doesn't work because some deps are used only by the library, figure if this can be fixed?
20
21
use std::{
22
    fs, io,
23
    sync::Arc,
24
    thread,
25
    time::{Duration, SystemTime, UNIX_EPOCH},
26
};
27
28
mod cli;
29
30
0
fn main() {
31
0
    smol::block_on(async_main())
32
0
}
33
34
0
async fn async_main() {
35
0
    match <cli::CliOptions as clap::Parser>::parse().command {
36
0
        cli::CliOptionsCommand::Run(r) => run(*r).await,
37
0
        cli::CliOptionsCommand::Blake264BitsHash(opt) => {
38
0
            let hash = blake2_rfc::blake2b::blake2b(8, &[], opt.payload.as_bytes());
39
0
            println!("0x{}", hex::encode(hash));
40
0
        }
41
0
        cli::CliOptionsCommand::Blake2256BitsHash(opt) => {
42
0
            let content = fs::read(opt.file).expect("Failed to read file content");
43
0
            let hash = blake2_rfc::blake2b::blake2b(32, &[], &content);
44
0
            println!("0x{}", hex::encode(hash));
45
0
        }
46
    }
47
0
}
48
49
0
async fn run(cli_options: cli::CliOptionsRun) {
50
    // Determine the actual CLI output by replacing `Auto` with the actual value.
51
0
    let cli_output = if let cli::Output::Auto = cli_options.output {
52
0
        if io::IsTerminal::is_terminal(&io::stderr()) && cli_options.log_level.is_none() {
53
0
            cli::Output::Informant
54
        } else {
55
0
            cli::Output::Logs
56
        }
57
    } else {
58
0
        cli_options.output
59
    };
60
0
    debug_assert!(!matches!(cli_output, cli::Output::Auto));
61
62
    // Setup the logging system of the binary.
63
0
    let log_callback: Arc<dyn smoldot_full_node::LogCallback + Send + Sync> = match cli_output {
64
0
        cli::Output::None => Arc::new(|_level, _message| {}),
65
        cli::Output::Informant | cli::Output::Logs => {
66
0
            let color_choice = cli_options.color.clone();
67
0
            let log_level = cli_options.log_level.clone().unwrap_or(
68
0
                if matches!(cli_output, cli::Output::Informant) {
69
0
                    cli::LogLevel::Info
70
                } else {
71
0
                    cli::LogLevel::Debug
72
                },
73
            );
74
75
0
            Arc::new(move |level, message| {
76
0
                match (&level, &log_level) {
77
0
                    (_, cli::LogLevel::Off) => return,
78
                    (
79
                        smoldot_full_node::LogLevel::Warn
80
                        | smoldot_full_node::LogLevel::Info
81
                        | smoldot_full_node::LogLevel::Debug
82
                        | smoldot_full_node::LogLevel::Trace,
83
                        cli::LogLevel::Error,
84
0
                    ) => return,
85
                    (
86
                        smoldot_full_node::LogLevel::Info
87
                        | smoldot_full_node::LogLevel::Debug
88
                        | smoldot_full_node::LogLevel::Trace,
89
                        cli::LogLevel::Warn,
90
0
                    ) => return,
91
                    (
92
                        smoldot_full_node::LogLevel::Debug | smoldot_full_node::LogLevel::Trace,
93
                        cli::LogLevel::Info,
94
0
                    ) => return,
95
0
                    (smoldot_full_node::LogLevel::Trace, cli::LogLevel::Debug) => return,
96
0
                    _ => {}
97
0
                }
98
0
99
0
                let when = humantime::format_rfc3339_millis(SystemTime::now());
100
101
0
                let level_str = match (level, &color_choice) {
102
0
                    (smoldot_full_node::LogLevel::Trace, cli::ColorChoice::Never) => "trace",
103
                    (smoldot_full_node::LogLevel::Trace, cli::ColorChoice::Always) => {
104
0
                        "\x1b[36mtrace\x1b[0m"
105
                    }
106
0
                    (smoldot_full_node::LogLevel::Debug, cli::ColorChoice::Never) => "debug",
107
                    (smoldot_full_node::LogLevel::Debug, cli::ColorChoice::Always) => {
108
0
                        "\x1b[34mdebug\x1b[0m"
109
                    }
110
0
                    (smoldot_full_node::LogLevel::Info, cli::ColorChoice::Never) => "info",
111
                    (smoldot_full_node::LogLevel::Info, cli::ColorChoice::Always) => {
112
0
                        "\x1b[32minfo\x1b[0m"
113
                    }
114
0
                    (smoldot_full_node::LogLevel::Warn, cli::ColorChoice::Never) => "warn",
115
                    (smoldot_full_node::LogLevel::Warn, cli::ColorChoice::Always) => {
116
0
                        "\x1b[33;1mwarn\x1b[0m"
117
                    }
118
0
                    (smoldot_full_node::LogLevel::Error, cli::ColorChoice::Never) => "error",
119
                    (smoldot_full_node::LogLevel::Error, cli::ColorChoice::Always) => {
120
0
                        "\x1b[31;1merror\x1b[0m"
121
                    }
122
                };
123
124
0
                eprintln!("[{}] [{}] {}", when, level_str, message);
125
0
            }) as Arc<dyn smoldot_full_node::LogCallback + Send + Sync>
126
        }
127
        cli::Output::LogsJson => {
128
0
            let log_level = cli_options
129
0
                .log_level
130
0
                .clone()
131
0
                .unwrap_or(cli::LogLevel::Debug);
132
0
            Arc::new(move |level, message| {
133
0
                match (&level, &log_level) {
134
0
                    (_, cli::LogLevel::Off) => return,
135
                    (
136
                        smoldot_full_node::LogLevel::Warn
137
                        | smoldot_full_node::LogLevel::Info
138
                        | smoldot_full_node::LogLevel::Debug
139
                        | smoldot_full_node::LogLevel::Trace,
140
                        cli::LogLevel::Error,
141
0
                    ) => return,
142
                    (
143
                        smoldot_full_node::LogLevel::Info
144
                        | smoldot_full_node::LogLevel::Debug
145
                        | smoldot_full_node::LogLevel::Trace,
146
                        cli::LogLevel::Warn,
147
0
                    ) => return,
148
                    (
149
                        smoldot_full_node::LogLevel::Debug | smoldot_full_node::LogLevel::Trace,
150
                        cli::LogLevel::Info,
151
0
                    ) => return,
152
0
                    (smoldot_full_node::LogLevel::Trace, cli::LogLevel::Debug) => return,
153
0
                    _ => {}
154
0
                }
155
0
156
0
                #[derive(serde::Serialize)]
157
0
                struct Record {
158
0
                    timestamp: u128,
159
0
                    level: &'static str,
160
0
                    message: String,
161
0
                }
162
0
163
0
                let mut lock = std::io::stderr().lock();
164
0
                if serde_json::to_writer(
165
0
                    &mut lock,
166
0
                    &Record {
167
0
                        timestamp: SystemTime::now()
168
0
                            .duration_since(UNIX_EPOCH)
169
0
                            .map(|d| d.as_millis())
170
0
                            .unwrap_or(0),
171
0
                        level: match level {
172
0
                            smoldot_full_node::LogLevel::Trace => "trace",
173
0
                            smoldot_full_node::LogLevel::Debug => "debug",
174
0
                            smoldot_full_node::LogLevel::Info => "info",
175
0
                            smoldot_full_node::LogLevel::Warn => "warn",
176
0
                            smoldot_full_node::LogLevel::Error => "error",
177
                        },
178
0
                        message,
179
0
                    },
180
0
                )
181
0
                .is_ok()
182
0
                {
183
0
                    let _ = io::Write::write_all(&mut lock, b"\n");
184
0
                }
185
0
            })
186
        }
187
0
        cli::Output::Auto => unreachable!(), // Handled above.
188
    };
189
190
0
    let chain_spec =
191
0
        fs::read(&cli_options.path_to_chain_spec).expect("Failed to read chain specification");
192
0
    let parsed_chain_spec = {
193
0
        smoldot::chain_spec::ChainSpec::from_json_bytes(&chain_spec)
194
0
            .expect("Failed to decode chain specification")
195
    };
196
197
    // Directory where we will store everything on the disk, such as the database, secret keys,
198
    // etc.
199
0
    let base_storage_directory = if cli_options.tmp {
200
0
        None
201
0
    } else if let Some(base) = directories::ProjectDirs::from("io", "smoldot", "smoldot") {
202
0
        Some(base.data_dir().to_owned())
203
    } else {
204
0
        log_callback.log(
205
0
            smoldot_full_node::LogLevel::Warn,
206
0
            "Failed to fetch $HOME directory. Falling back to storing everything in memory, \
207
0
                meaning that everything will be lost when the node stops. If this is intended, \
208
0
                please make this explicit by passing the `--tmp` flag instead."
209
0
                .to_string(),
210
0
        );
211
0
        None
212
    };
213
214
    // Create the directory if necessary.
215
0
    if let Some(base_storage_directory) = base_storage_directory.as_ref() {
216
0
        fs::create_dir_all(base_storage_directory.join(parsed_chain_spec.id())).unwrap();
217
0
    }
218
    // Directory supposed to contain the database.
219
0
    let sqlite_database_path = base_storage_directory
220
0
        .as_ref()
221
0
        .map(|d| d.join(parsed_chain_spec.id()).join("database"));
222
0
    // Directory supposed to contain the keystore.
223
0
    let keystore_path = base_storage_directory
224
0
        .as_ref()
225
0
        .map(|path| path.join(parsed_chain_spec.id()).join("keys"));
226
227
    // Build the relay chain information if relevant.
228
0
    let (relay_chain, relay_chain_name) =
229
0
        if let Some((relay_chain_name, _parachain_id)) = parsed_chain_spec.relay_chain() {
230
0
            let spec_json = {
231
0
                let relay_chain_path = cli_options
232
0
                    .path_to_chain_spec
233
0
                    .parent()
234
0
                    .unwrap()
235
0
                    .join(format!("{relay_chain_name}.json"));
236
0
                fs::read(&relay_chain_path).expect("Failed to read relay chain specification")
237
0
            };
238
0
239
0
            let parsed_relay_spec = smoldot::chain_spec::ChainSpec::from_json_bytes(&spec_json)
240
0
                .expect("Failed to decode relay chain chain specs");
241
0
242
0
            // Make sure we're not accidentally opening the same chain twice, otherwise weird
243
0
            // interactions will happen.
244
0
            assert_ne!(parsed_relay_spec.id(), parsed_chain_spec.id());
245
246
            // Create the directory if necessary.
247
0
            if let Some(base_storage_directory) = base_storage_directory.as_ref() {
248
0
                fs::create_dir_all(base_storage_directory.join(parsed_relay_spec.id())).unwrap();
249
0
            }
250
251
0
            let cfg = smoldot_full_node::ChainConfig {
252
0
                chain_spec: spec_json.into(),
253
0
                additional_bootnodes: Vec::new(),
254
0
                keystore_memory: Vec::new(),
255
0
                sqlite_database_path: base_storage_directory.as_ref().map(|d| {
256
0
                    d.join(parsed_relay_spec.id())
257
0
                        .join("database")
258
0
                        .join("database.sqlite")
259
0
                }),
260
0
                sqlite_cache_size: cli_options.relay_chain_database_cache_size.0,
261
0
                keystore_path: base_storage_directory
262
0
                    .as_ref()
263
0
                    .map(|path| path.join(parsed_relay_spec.id()).join("keys")),
264
0
                json_rpc_listen: None,
265
0
            };
266
0
267
0
            (Some(cfg), Some(relay_chain_name.to_owned()))
268
        } else {
269
0
            (None, None)
270
        };
271
272
    // Determine which networking key to use.
273
    //
274
    // This is either passed as a CLI option, loaded from disk, or generated randomly.
275
    // TODO: move this code to `/lib/src/identity`?
276
0
    let libp2p_key = if let Some(node_key) = cli_options.libp2p_key {
277
0
        node_key
278
0
    } else if let Some(dir) = base_storage_directory.as_ref() {
279
0
        let path = dir.join("libp2p_ed25519_secret_key.secret");
280
0
        let libp2p_key = if path.exists() {
281
0
            let file_content = zeroize::Zeroizing::new(
282
0
                fs::read_to_string(&path).expect("failed to read libp2p secret key file content"),
283
0
            );
284
0
            let mut hex_decoded = Box::new([0u8; 32]);
285
0
            hex::decode_to_slice(file_content, &mut *hex_decoded)
286
0
                .expect("invalid libp2p secret key file content");
287
0
            hex_decoded
288
        } else {
289
0
            let mut actual_key = Box::new([0u8; 32]);
290
0
            rand::Fill::try_fill(&mut *actual_key, &mut rand::thread_rng()).unwrap();
291
0
            let mut hex_encoded = Box::new([0; 64]);
292
0
            hex::encode_to_slice(*actual_key, &mut *hex_encoded).unwrap();
293
0
            fs::write(&path, *hex_encoded).expect("failed to write libp2p secret key file");
294
0
            zeroize::Zeroize::zeroize(&mut *hex_encoded);
295
0
            actual_key
296
        };
297
        // On Unix platforms, set the permission as 0o400 (only reading and by owner is permitted).
298
        // TODO: do something equivalent on Windows
299
        #[cfg(unix)]
300
0
        let _ = fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(0o400));
301
0
        libp2p_key
302
    } else {
303
0
        let mut key = Box::new([0u8; 32]);
304
0
        rand::Fill::try_fill(&mut *key, &mut rand::thread_rng()).unwrap();
305
0
        key
306
    };
307
308
    // Create an executor where tasks are going to be spawned onto.
309
0
    let executor = Arc::new(smol::Executor::new());
310
0
    for n in 0..thread::available_parallelism()
311
0
        .map(|n| n.get() - 1)
312
0
        .unwrap_or(3)
313
    {
314
0
        let executor = executor.clone();
315
0
316
0
        let spawn_result = thread::Builder::new()
317
0
            .name(format!("tasks-pool-{}", n))
318
0
            .spawn(move || smol::block_on(executor.run(smol::future::pending::<()>())));
319
320
        // Ignore a failure to spawn a thread, as we're going to run tasks on the current thread
321
        // later down this function.
322
0
        if let Err(err) = spawn_result {
323
0
            log_callback.log(
324
0
                smoldot_full_node::LogLevel::Warn,
325
0
                format!("tasks-pool-thread-spawn-failure; err={err}"),
326
0
            );
327
0
        }
328
    }
329
330
    // Print some general information.
331
0
    log_callback.log(
332
0
        smoldot_full_node::LogLevel::Info,
333
0
        "smoldot full node".to_string(),
334
0
    );
335
0
    log_callback.log(
336
0
        smoldot_full_node::LogLevel::Info,
337
0
        "Copyright (C) 2019-2022  Parity Technologies (UK) Ltd.".to_string(),
338
0
    );
339
0
    log_callback.log(
340
0
        smoldot_full_node::LogLevel::Info,
341
0
        "Copyright (C) 2023  Pierre Krieger.".to_string(),
342
0
    );
343
0
    log_callback.log(
344
0
        smoldot_full_node::LogLevel::Info,
345
0
        "This program comes with ABSOLUTELY NO WARRANTY.".to_string(),
346
0
    );
347
0
    log_callback.log(
348
0
        smoldot_full_node::LogLevel::Info,
349
0
        "This is free software, and you are welcome to redistribute it under certain conditions."
350
0
            .to_string(),
351
0
    );
352
0
353
0
    // This warning message should be removed if/when the full node becomes mature.
354
0
    log_callback.log(
355
0
        smoldot_full_node::LogLevel::Warn,
356
0
        "Please note that this full node is experimental. It is not feature complete and is \
357
0
        known to panic often. Please report any panic you might encounter to \
358
0
        <https://github.com/smol-dot/smoldot/issues>."
359
0
            .to_string(),
360
0
    );
361
362
0
    let client_init_result = smoldot_full_node::start(smoldot_full_node::Config {
363
        chain: smoldot_full_node::ChainConfig {
364
0
            chain_spec: chain_spec.into(),
365
0
            additional_bootnodes: cli_options
366
0
                .additional_bootnode
367
0
                .iter()
368
0
                .map(|cli::Bootnode { address, peer_id }| (peer_id.clone(), address.clone()))
369
0
                .collect(),
370
0
            keystore_memory: cli_options.keystore_memory,
371
0
            sqlite_database_path,
372
0
            sqlite_cache_size: cli_options.database_cache_size.0,
373
0
            keystore_path,
374
0
            json_rpc_listen: if let Some(address) = cli_options.json_rpc_address.0 {
375
0
                Some(smoldot_full_node::JsonRpcListenConfig {
376
0
                    address,
377
0
                    max_json_rpc_clients: cli_options.json_rpc_max_clients,
378
0
                })
379
            } else {
380
0
                None
381
            },
382
        },
383
0
        relay_chain,
384
0
        libp2p_key,
385
0
        listen_addresses: cli_options.listen_addr,
386
0
        tasks_executor: {
387
0
            let executor = executor.clone();
388
0
            Arc::new(move |task| executor.spawn(task).detach())
389
0
        },
390
0
        log_callback: log_callback.clone(),
391
0
        jaeger_agent: cli_options.jaeger,
392
    })
393
0
    .await;
394
395
0
    let client = match client_init_result {
396
0
        Ok(c) => c,
397
0
        Err(err) => {
398
0
            log_callback.log(
399
0
                smoldot_full_node::LogLevel::Error,
400
0
                format!("Failed to initialize client: {}", err),
401
0
            );
402
0
            panic!("Failed to initialize client: {}", err);
403
        }
404
    };
405
406
0
    if let Some(addr) = client.json_rpc_server_addr() {
407
0
        log_callback.log(
408
0
            smoldot_full_node::LogLevel::Info,
409
0
            format!(
410
0
                "JSON-RPC server listening on {addr}. Visit \
411
0
                <https://cloudflare-ipfs.com/ipns/dotapps.io/?rpc=ws%3A%2F%2F{addr}> in order to \
412
0
                interact with the node."
413
0
            ),
414
0
        );
415
0
    }
416
417
    // Starting from here, a SIGINT (or equivalent) handler is set up. If the user does Ctrl+C,
418
    // an event will be triggered on `ctrlc_detected`.
419
    // This should be performed after all the expensive initialization is done, as otherwise these
420
    // expensive initializations aren't interrupted by Ctrl+C, which could be frustrating for the
421
    // user.
422
0
    let ctrlc_detected = {
423
0
        let event = event_listener::Event::new();
424
0
        let listen = event.listen();
425
0
        if let Err(err) = ctrlc::set_handler(move || {
426
0
            event.notify(usize::MAX);
427
0
        }) {
428
0
            // It is not critical to fail to setup the Ctrl-C handler.
429
0
            log_callback.log(
430
0
                smoldot_full_node::LogLevel::Warn,
431
0
                format!("ctrlc-handler-setup-fail; err={err}"),
432
0
            );
433
0
        }
434
0
        listen
435
    };
436
437
    // Spawn a task that prints the informant at a regular interval.
438
    // The interval is fast enough that the informant should be visible roughly at any time,
439
    // even if the terminal is filled with logs.
440
    // Note that this task also holds the smoldot `client` alive, and thus we spawn it even if
441
    // the informant is disabled.
442
0
    let main_task = executor.spawn({
443
0
        let show_informant = matches!(cli_output, cli::Output::Informant);
444
0
        let informant_colors = match cli_options.color {
445
0
            cli::ColorChoice::Always => true,
446
0
            cli::ColorChoice::Never => false,
447
        };
448
449
0
        async move {
450
0
            let mut informant_timer = if show_informant {
451
0
                smol::Timer::after(Duration::new(0, 0))
452
            } else {
453
0
                smol::Timer::never()
454
            };
455
456
            loop {
457
0
                informant_timer =
458
0
                    smol::Timer::at(informant_timer.await + Duration::from_millis(100));
459
460
                // We end the informant line with a `\r` so that it overwrites itself
461
                // every time. If any other line gets printed, it will overwrite the
462
                // informant, and the informant will then print itself below, which is
463
                // a fine behaviour.
464
0
                let sync_state = client.sync_state().await;
465
0
                eprint!(
466
0
                    "{}\r",
467
0
                    smoldot::informant::InformantLine {
468
0
                        enable_colors: informant_colors,
469
0
                        chain_name: parsed_chain_spec.name(),
470
0
                        relay_chain: client.relay_chain_sync_state().await.map(
471
0
                            |relay_sync_state| smoldot::informant::RelayChain {
472
0
                                chain_name: relay_chain_name.as_ref().unwrap(),
473
0
                                best_number: relay_sync_state.best_block_number,
474
0
                            }
475
0
                        ),
476
0
                        max_line_width: terminal_size::terminal_size()
477
0
                            .map_or(80, |(w, _)| w.0.into()),
478
0
                        num_peers: client.num_peers().await,
479
0
                        num_network_connections: client.num_network_connections().await,
480
0
                        best_number: sync_state.best_block_number,
481
0
                        finalized_number: sync_state.finalized_block_number,
482
0
                        best_hash: &sync_state.best_block_hash,
483
0
                        finalized_hash: &sync_state.finalized_block_hash,
484
0
                        network_known_best: client.network_known_best().await,
485
                    }
486
                );
487
            }
488
        }
489
    });
490
491
    // Now run all the tasks that have been spawned.
492
0
    executor.run(ctrlc_detected).await;
493
494
    // Add a new line after the informant so that the user's shell doesn't
495
    // overwrite it.
496
0
    if matches!(cli_output, cli::Output::Informant) {
497
0
        eprintln!();
498
0
    }
499
500
    // After `ctrlc_detected` has triggered, we destroy `main_task`, which cancels it and destroys
501
    // the smoldot client.
502
0
    drop::<smol::Task<_>>(main_task);
503
0
504
0
    // TODO: consider running the executor until all tasks shut down gracefully; unfortunately this currently hangs
505
0
}