1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
//! Helper functions used to implement solochain collator
18

            
19
use {
20
    crate::cli::{Cli, RelayChainCli},
21
    futures::FutureExt,
22
    jsonrpsee::server::BatchRequestConfig,
23
    log::{info, warn},
24
    sc_chain_spec::{ChainType, GenericChainSpec, NoExtension},
25
    sc_cli::{CliConfiguration, DefaultConfigurationValues, Signals, SubstrateCli},
26
    sc_network::config::{NetworkBackendType, NetworkConfiguration, TransportConfig},
27
    sc_network_common::role::Role,
28
    sc_service::{
29
        config::{ExecutorConfiguration, KeystoreConfig},
30
        BasePath, BlocksPruning, Configuration, DatabaseSource, TaskManager,
31
    },
32
    sc_tracing::logging::LoggerBuilder,
33
    std::{
34
        future::Future,
35
        num::NonZeroUsize,
36
        path::{Path, PathBuf},
37
        time::Duration,
38
    },
39
    tc_service_container_chain::cli::ContainerChainCli,
40
};
41

            
42
/// Alternative to [Configuration] struct used in solochain context.
43
pub struct SolochainConfig {
44
    pub tokio_handle: tokio::runtime::Handle,
45
    pub base_path: BasePath,
46
    pub network_node_name: String,
47
    pub role: Role,
48
    pub relay_chain: String,
49
}
50

            
51
/// Alternative to [Runner](sc_cli::Runner) struct used in solochain context.
52
pub struct SolochainRunner {
53
    config: SolochainConfig,
54
    tokio_runtime: tokio::runtime::Runtime,
55
    signals: Signals,
56
}
57

            
58
impl SolochainRunner {
59
    /// Log information about the node itself.
60
    ///
61
    /// # Example:
62
    ///
63
    /// ```text
64
    /// 2020-06-03 16:14:21 Substrate Node
65
    /// 2020-06-03 16:14:21 ✌️  version 2.0.0-rc3-f4940588c-x86_64-linux-gnu
66
    /// 2020-06-03 16:14:21 ❤️  by Parity Technologies <admin@parity.io>, 2017-2020
67
    /// 2020-06-03 16:14:21 📋 Chain specification: Flaming Fir
68
    /// 2020-06-03 16:14:21 🏷  Node name: jolly-rod-7462
69
    /// 2020-06-03 16:14:21 👤 Role: FULL
70
    /// 2020-06-03 16:14:21 💾 Database: RocksDb at /tmp/c/chains/flamingfir7/db
71
    /// 2020-06-03 16:14:21 ⛓  Native runtime: node-251 (substrate-node-1.tx1.au10)
72
    /// ```
73
    fn print_node_infos(&self) {
74
        use chrono::{offset::Local, Datelike};
75
        type C = ContainerChainCli;
76
        info!("{}", C::impl_name());
77
        info!("✌️  version {}", C::impl_version());
78
        info!(
79
            "❤️  by {}, {}-{}",
80
            C::author(),
81
            C::copyright_start_year(),
82
            Local::now().year()
83
        );
84
        // No chain spec
85
        //info!("📋 Chain specification: {}", config.chain_spec.name());
86
        info!("🏷  Node name: {}", self.config.network_node_name);
87
        info!("👤 Role: {}", self.config.role);
88
        info!(
89
            "💾 Database: {} at {}",
90
            // Container chains only support paritydb
91
            "ParityDb",
92
            // Print base path instead of db path because each container will have its own db in a
93
            // different subdirectory.
94
            self.config.base_path.path().display(),
95
        );
96
    }
97

            
98
    /// A helper function that runs a node with tokio and stops if the process receives the signal
99
    /// `SIGTERM` or `SIGINT`.
100
    pub fn run_node_until_exit<F, E>(
101
        self,
102
        initialize: impl FnOnce(SolochainConfig) -> F,
103
    ) -> std::result::Result<(), E>
104
    where
105
        F: Future<Output = std::result::Result<TaskManager, E>>,
106
        E: std::error::Error + Send + Sync + 'static + From<sc_service::Error>,
107
    {
108
        self.print_node_infos();
109

            
110
        let mut task_manager = self.tokio_runtime.block_on(initialize(self.config))?;
111

            
112
        let res = self
113
            .tokio_runtime
114
            .block_on(self.signals.run_until_signal(task_manager.future().fuse()));
115
        // We need to drop the task manager here to inform all tasks that they should shut down.
116
        //
117
        // This is important to be done before we instruct the tokio runtime to shutdown. Otherwise
118
        // the tokio runtime will wait the full 60 seconds for all tasks to stop.
119
        let task_registry = task_manager.into_task_registry();
120

            
121
        // Give all futures 60 seconds to shutdown, before tokio "leaks" them.
122
        let shutdown_timeout = Duration::from_secs(60);
123
        self.tokio_runtime.shutdown_timeout(shutdown_timeout);
124

            
125
        let running_tasks = task_registry.running_tasks();
126

            
127
        if !running_tasks.is_empty() {
128
            log::error!("Detected running(potentially stalled) tasks on shutdown:");
129
            running_tasks.iter().for_each(|(task, count)| {
130
                let instances_desc = if *count > 1 {
131
                    format!("with {} instances ", count)
132
                } else {
133
                    "".to_string()
134
                };
135

            
136
                if task.is_default_group() {
137
                    log::error!(
138
                        "Task \"{}\" was still running {}after waiting {} seconds to finish.",
139
                        task.name,
140
                        instances_desc,
141
                        shutdown_timeout.as_secs(),
142
                    );
143
                } else {
144
                    log::error!(
145
						"Task \"{}\" (Group: {}) was still running {}after waiting {} seconds to finish.",
146
						task.name,
147
						task.group,
148
						instances_desc,
149
						shutdown_timeout.as_secs(),
150
					);
151
                }
152
            });
153
        }
154

            
155
        res.map_err(Into::into)
156
    }
157
}
158

            
159
/// Equivalent to [Cli::create_runner]
160
pub fn create_runner<T: CliConfiguration<DVC>, DVC: DefaultConfigurationValues>(
161
    command: &T,
162
) -> sc_cli::Result<SolochainRunner> {
163
    let tokio_runtime = sc_cli::build_runtime()?;
164

            
165
    // `capture` needs to be called in a tokio context.
166
    // Also capture them as early as possible.
167
    let signals = tokio_runtime.block_on(async { Signals::capture() })?;
168

            
169
    init_cmd(command, &Cli::support_url(), &Cli::impl_version())?;
170

            
171
    let base_path = command.base_path()?.unwrap();
172
    let network_node_name = command.node_name()?;
173
    let is_dev = command.is_dev()?;
174
    let role = command.role(is_dev)?;
175
    // This relay chain id is only used when the relay chain args have no `--chain` value
176
    // TODO: check if this works with an external relay rpc / light client
177
    let relay_chain_id = "dancelight_local_testnet".to_string();
178

            
179
    let config = SolochainConfig {
180
        tokio_handle: tokio_runtime.handle().clone(),
181
        base_path,
182
        network_node_name,
183
        role,
184
        relay_chain: relay_chain_id,
185
    };
186

            
187
    Ok(SolochainRunner {
188
        config,
189
        tokio_runtime,
190
        signals,
191
    })
192
}
193

            
194
/// The recommended open file descriptor limit to be configured for the process.
195
const RECOMMENDED_OPEN_FILE_DESCRIPTOR_LIMIT: u64 = 10_000;
196

            
197
/// Equivalent to [CliConfiguration::init]
198
fn init_cmd<T: CliConfiguration<DVC>, DVC: DefaultConfigurationValues>(
199
    this: &T,
200
    support_url: &String,
201
    impl_version: &String,
202
) -> sc_cli::Result<()> {
203
    sp_panic_handler::set(support_url, impl_version);
204

            
205
    let mut logger = LoggerBuilder::new(this.log_filters()?);
206
    logger
207
        .with_log_reloading(this.enable_log_reloading()?)
208
        .with_detailed_output(this.detailed_log_output()?);
209

            
210
    if let Some(tracing_targets) = this.tracing_targets()? {
211
        let tracing_receiver = this.tracing_receiver()?;
212
        logger.with_profiling(tracing_receiver, tracing_targets);
213
    }
214

            
215
    if this.disable_log_color()? {
216
        logger.with_colors(false);
217
    }
218

            
219
    logger.init()?;
220

            
221
    match fdlimit::raise_fd_limit() {
222
        Ok(fdlimit::Outcome::LimitRaised { to, .. }) => {
223
            if to < RECOMMENDED_OPEN_FILE_DESCRIPTOR_LIMIT {
224
                warn!(
225
                    "Low open file descriptor limit configured for the process. \
226
                        Current value: {:?}, recommended value: {:?}.",
227
                    to, RECOMMENDED_OPEN_FILE_DESCRIPTOR_LIMIT,
228
                );
229
            }
230
        }
231
        Ok(fdlimit::Outcome::Unsupported) => {
232
            // Unsupported platform (non-Linux)
233
        }
234
        Err(error) => {
235
            warn!(
236
                "Failed to configure file descriptor limit for the process: \
237
                    {}, recommended value: {:?}.",
238
                error, RECOMMENDED_OPEN_FILE_DESCRIPTOR_LIMIT,
239
            );
240
        }
241
    }
242

            
243
    Ok(())
244
}
245

            
246
/// Equivalent to [RelayChainCli::new]
247
pub fn relay_chain_cli_new<'a>(
248
    config: &SolochainConfig,
249
    relay_chain_args: impl Iterator<Item = &'a String>,
250
) -> RelayChainCli {
251
    let base_path = config.base_path.path().join("polkadot");
252

            
253
    RelayChainCli {
254
        base_path,
255
        chain_id: Some(config.relay_chain.clone()),
256
        base: clap::Parser::parse_from(relay_chain_args),
257
    }
258
}
259

            
260
/// Create a dummy [Configuration] that should only be used as input to polkadot-sdk functions that
261
/// take this struct as input but only use one field of it.
262
/// This is needed because [Configuration] does not implement [Default].
263
pub fn dummy_config(tokio_handle: tokio::runtime::Handle, base_path: BasePath) -> Configuration {
264
    Configuration {
265
        impl_name: "".to_string(),
266
        impl_version: "".to_string(),
267
        role: Role::Full,
268
        tokio_handle,
269
        transaction_pool: Default::default(),
270
        network: NetworkConfiguration {
271
            net_config_path: None,
272
            listen_addresses: vec![],
273
            public_addresses: vec![],
274
            boot_nodes: vec![],
275
            node_key: Default::default(),
276
            default_peers_set: Default::default(),
277
            default_peers_set_num_full: 0,
278
            client_version: "".to_string(),
279
            node_name: "".to_string(),
280
            transport: TransportConfig::MemoryOnly,
281
            max_parallel_downloads: 0,
282
            max_blocks_per_request: 0,
283
            sync_mode: Default::default(),
284
            enable_dht_random_walk: false,
285
            allow_non_globals_in_dht: false,
286
            kademlia_disjoint_query_paths: false,
287
            kademlia_replication_factor: NonZeroUsize::new(20).unwrap(),
288
            ipfs_server: false,
289
            yamux_window_size: None,
290
            network_backend: NetworkBackendType::Libp2p,
291
        },
292
        keystore: KeystoreConfig::InMemory,
293
        database: DatabaseSource::ParityDb {
294
            path: Default::default(),
295
        },
296
        trie_cache_maximum_size: None,
297
        state_pruning: None,
298
        blocks_pruning: BlocksPruning::KeepAll,
299
        chain_spec: Box::new(
300
            GenericChainSpec::<NoExtension, ()>::builder(Default::default(), NoExtension::None)
301
                .with_name("test")
302
                .with_id("test_id")
303
                .with_chain_type(ChainType::Development)
304
                .with_genesis_config_patch(Default::default())
305
                .build(),
306
        ),
307
        executor: ExecutorConfiguration {
308
            wasm_method: Default::default(),
309
            wasmtime_precompiled: None,
310
            default_heap_pages: None,
311
            max_runtime_instances: 0,
312
            runtime_cache_size: 0,
313
        },
314
        wasm_runtime_overrides: None,
315
        rpc: sc_service::config::RpcConfiguration {
316
            addr: None,
317
            max_connections: 0,
318
            cors: None,
319
            methods: Default::default(),
320
            max_request_size: 0,
321
            max_response_size: 0,
322
            id_provider: None,
323
            max_subs_per_conn: 0,
324
            port: 0,
325
            message_buffer_capacity: 0,
326
            batch_config: BatchRequestConfig::Disabled,
327
            rate_limit: None,
328
            rate_limit_whitelisted_ips: vec![],
329
            rate_limit_trust_proxy_headers: false,
330
        },
331
        prometheus_config: None,
332
        telemetry_endpoints: None,
333
        offchain_worker: Default::default(),
334
        force_authoring: false,
335
        disable_grandpa: false,
336
        dev_key_seed: None,
337
        tracing_targets: None,
338
        tracing_receiver: Default::default(),
339
        announce_block: false,
340
        data_path: Default::default(),
341
        base_path,
342
    }
343
}
344

            
345
/// Returns the default path for configuration directory based on the chain_spec
346
pub(crate) fn build_solochain_config_dir(base_path: &PathBuf) -> PathBuf {
347
    // base_path:  Collator1000-01/data/containers
348
    // config_dir: Collator1000-01/data/config
349
    let mut base_path = base_path.clone();
350
    base_path.pop();
351

            
352
    base_path.join("config")
353
}
354

            
355
pub fn keystore_config(
356
    keystore_params: Option<&sc_cli::KeystoreParams>,
357
    config_dir: &PathBuf,
358
) -> sc_cli::Result<KeystoreConfig> {
359
    keystore_params
360
        .map(|x| x.keystore_config(config_dir))
361
        .unwrap_or_else(|| Ok(KeystoreConfig::InMemory))
362
}
363

            
364
/// Get the zombienet keystore path from the solochain collator keystore.
365
fn zombienet_keystore_path(keystore: &KeystoreConfig) -> PathBuf {
366
    let keystore_path = keystore.path().unwrap();
367
    let mut zombienet_path = keystore_path.to_owned();
368
    // Collator1000-01/data/config/keystore/
369
    zombienet_path.pop();
370
    // Collator1000-01/data/config/
371
    zombienet_path.pop();
372
    // Collator1000-01/data/
373
    zombienet_path.push("chains/simple_container_2000/keystore/");
374
    // Collator1000-01/data/chains/simple_container_2000/keystore/
375

            
376
    zombienet_path
377
}
378

            
379
/// When running under zombienet, collator keys are injected in a different folder from what we
380
/// expect. This function will check if the zombienet folder exists, and if so, copy all the keys
381
/// from there into the expected folder.
382
pub fn copy_zombienet_keystore(keystore: &KeystoreConfig) -> std::io::Result<()> {
383
    let keystore_path = keystore.path().unwrap();
384
    let zombienet_path = zombienet_keystore_path(keystore);
385

            
386
    if zombienet_path.exists() {
387
        // Copy to keystore folder
388
        let mut files_copied = 0;
389
        copy_dir_all(zombienet_path, keystore_path, &mut files_copied)?;
390
        log::info!("Copied {} keys from zombienet keystore", files_copied);
391

            
392
        Ok(())
393
    } else {
394
        // Zombienet folder does not exist, assume we are not running under zombienet
395
        Ok(())
396
    }
397
}
398

            
399
/// Equivalent to `cp -r src/* dst`
400
// https://stackoverflow.com/a/65192210
401
fn copy_dir_all(
402
    src: impl AsRef<Path>,
403
    dst: impl AsRef<Path>,
404
    files_copied: &mut u32,
405
) -> std::io::Result<()> {
406
    use std::fs;
407
    fs::create_dir_all(&dst)?;
408
    for entry in fs::read_dir(src)? {
409
        let entry = entry?;
410
        let ty = entry.file_type()?;
411
        if ty.is_dir() {
412
            copy_dir_all(
413
                entry.path(),
414
                dst.as_ref().join(entry.file_name()),
415
                files_copied,
416
            )?;
417
        } else {
418
            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
419
            *files_copied += 1;
420
        }
421
    }
422
    Ok(())
423
}