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
use {
18
    cumulus_client_cli::{CollatorOptions, RelayChainMode},
19
    dc_orchestrator_chain_interface::ContainerChainGenesisData,
20
    dp_container_chain_genesis_data::json::properties_to_map,
21
    node_common::chain_spec::Extensions,
22
    sc_chain_spec::ChainSpec,
23
    sc_cli::{CliConfiguration, SubstrateCli},
24
    sc_network::config::MultiaddrWithPeerId,
25
    sc_service::BasePath,
26
    sp_runtime::Storage,
27
    std::collections::BTreeMap,
28
    url::Url,
29
};
30

            
31
/// The `run` command used to run a container chain node.
32
#[derive(Debug, clap::Parser, Clone)]
33
#[group(skip)]
34
pub struct ContainerChainRunCmd {
35
    /// The cumulus RunCmd inherits from sc_cli's
36
    #[command(flatten)]
37
    pub base: sc_cli::RunCmd,
38

            
39
    /// Run node as collator.
40
    ///
41
    /// Note that this is the same as running with `--validator`.
42
    #[arg(long, conflicts_with = "validator")]
43
    pub collator: bool,
44

            
45
    /// Optional container chain para id that should be used to build chain spec.
46
    #[arg(long)]
47
    pub para_id: Option<u32>,
48

            
49
    /// Keep container-chain db after changing collator assignments
50
    #[arg(long)]
51
    pub keep_db: bool,
52

            
53
    /// Download the full block history for container chains after the warp sync is done.
54
    /// Default value: false for container collators, true for data preservers.
55
    #[arg(long)]
56
    pub download_block_history: Option<bool>,
57

            
58
    /// Creates a less resource-hungry node that retrieves relay chain data from an RPC endpoint.
59
    ///
60
    /// The provided URLs should point to RPC endpoints of the relay chain.
61
    /// This node connects to the remote nodes following the order they were specified in. If the
62
    /// connection fails, it attempts to connect to the next endpoint in the list.
63
    ///
64
    /// Note: This option doesn't stop the node from connecting to the relay chain network but
65
    /// reduces bandwidth use.
66
    #[arg(
67
		long,
68
		value_parser = validate_relay_chain_url,
69
		num_args = 0..,
70
		alias = "relay-chain-rpc-url"
71
    )]
72
    pub relay_chain_rpc_urls: Vec<Url>,
73

            
74
    /// EXPERIMENTAL: Embed a light client for the relay chain. Only supported for full-nodes.
75
    /// Will use the specified relay chain chainspec.
76
    #[arg(long, conflicts_with_all = ["relay_chain_rpc_urls", "collator"])]
77
    pub relay_chain_light_client: bool,
78

            
79
    /// EXPERIMENTAL: This is meant to be used only if collator is overshooting the PoV size, and
80
    /// building blocks that do not fit in the max_pov_size. It is a percentage of the max_pov_size
81
    /// configuration of the relay-chain.
82
    ///
83
    /// It will be removed once <https://github.com/paritytech/polkadot-sdk/issues/6020> is fixed.
84
    #[arg(long)]
85
    pub experimental_max_pov_percentage: Option<u32>,
86

            
87
    /// Disable RPC service for this node. Useful for bootnode-only nodes that only provide network services.
88
    #[arg(long)]
89
    pub disable_rpc: bool,
90

            
91
    /// Disable embedded DHT bootnode.
92
    ///
93
    /// Do not advertise the node as a parachain bootnode on the relay chain DHT.
94
    #[arg(long)]
95
    pub no_dht_bootnode: bool,
96

            
97
    /// Disable DHT bootnode discovery.
98
    ///
99
    /// Disable discovery of the parachain bootnodes via the relay chain DHT.
100
    #[arg(long)]
101
    pub no_dht_bootnode_discovery: bool,
102
}
103

            
104
impl ContainerChainRunCmd {
105
    /// Create a [`NormalizedRunCmd`] which merges the `collator` cli argument into `validator` to
106
    /// have only one.
107
    pub fn normalize(&self) -> ContainerChainCli {
108
        let mut new_base = self.clone();
109

            
110
        new_base.base.validator = self.base.validator || self.collator;
111

            
112
        // Append `containers/` to base_path for this object. This is to ensure that when spawning
113
        // a new container chain, its database is always inside the `containers` folder.
114
        // So if the user passes `--base-path /tmp/node`, we want the ephemeral container data in
115
        // `/tmp/node/containers`, and the persistent storage in `/tmp/node/config`.
116
        let base_path = base_path_or_default(
117
            self.base.base_path().expect("failed to get base_path"),
118
            &ContainerChainCli::executable_name(),
119
        );
120

            
121
        let base_path = base_path.path().join("containers");
122
        new_base.base.shared_params.base_path = Some(base_path);
123

            
124
        ContainerChainCli {
125
            base: new_base,
126
            preloaded_chain_spec: None,
127
        }
128
    }
129

            
130
    /// Create [`CollatorOptions`] representing options only relevant to parachain collator nodes
131
    // Copied from polkadot-sdk/cumulus/client/cli/src/lib.rs
132
    pub fn collator_options(&self) -> CollatorOptions {
133
        let relay_chain_mode = match (
134
            self.relay_chain_light_client,
135
            !self.relay_chain_rpc_urls.is_empty(),
136
        ) {
137
            (true, _) => RelayChainMode::LightClient,
138
            (_, true) => RelayChainMode::ExternalRpc(self.relay_chain_rpc_urls.clone()),
139
            _ => RelayChainMode::Embedded,
140
        };
141

            
142
        CollatorOptions {
143
            relay_chain_mode,
144
            embedded_dht_bootnode: !self.no_dht_bootnode,
145
            dht_bootnode_discovery: !self.no_dht_bootnode_discovery,
146
        }
147
    }
148
}
149

            
150
#[derive(Debug)]
151
pub struct ContainerChainCli {
152
    /// The actual container chain cli object.
153
    pub base: ContainerChainRunCmd,
154

            
155
    /// The ChainSpecs that this struct can initialize. This starts empty and gets filled
156
    /// by calling preload_chain_spec_file.
157
    pub preloaded_chain_spec: Option<Box<dyn sc_chain_spec::ChainSpec>>,
158
}
159

            
160
impl Clone for ContainerChainCli {
161
    fn clone(&self) -> Self {
162
        Self {
163
            base: self.base.clone(),
164
            preloaded_chain_spec: self.preloaded_chain_spec.as_ref().map(|x| x.cloned_box()),
165
        }
166
    }
167
}
168

            
169
impl ContainerChainCli {
170
    /// Parse the container chain CLI parameters using the para chain `Configuration`.
171
    pub fn new<'a>(
172
        para_config: &sc_service::Configuration,
173
        container_chain_args: impl Iterator<Item = &'a String>,
174
    ) -> Self {
175
        let mut base: ContainerChainRunCmd = clap::Parser::parse_from(container_chain_args);
176

            
177
        // Copy some parachain args into container chain args
178

            
179
        // If the container chain args have no --wasmtime-precompiled flag, use the same as the orchestrator
180
        if base.base.import_params.wasmtime_precompiled.is_none() {
181
            base.base
182
                .import_params
183
                .wasmtime_precompiled
184
                .clone_from(&para_config.executor.wasmtime_precompiled);
185
        }
186

            
187
        // Set container base path to the same value as orchestrator base_path.
188
        // "containers" is appended in `base.normalize()`
189
        if base.base.shared_params.base_path.is_some() {
190
            log::warn!("Container chain --base-path is being ignored");
191
        }
192
        let base_path = para_config.base_path.path().to_owned();
193
        base.base.shared_params.base_path = Some(base_path);
194

            
195
        base.normalize()
196
    }
197

            
198
    pub fn chain_spec_from_genesis_data(
199
        para_id: u32,
200
        genesis_data: ContainerChainGenesisData,
201
        chain_type: sc_chain_spec::ChainType,
202
        relay_chain: String,
203
        boot_nodes: Vec<MultiaddrWithPeerId>,
204
    ) -> Result<crate::chain_spec::RawChainSpec, String> {
205
        let name = String::from_utf8(genesis_data.name.to_vec())
206
            .map_err(|_e| "Invalid name".to_string())?;
207
        let id: String =
208
            String::from_utf8(genesis_data.id.to_vec()).map_err(|_e| "Invalid id".to_string())?;
209
        let storage_raw: BTreeMap<_, _> =
210
            genesis_data.storage.into_iter().map(|x| x.into()).collect();
211
        let protocol_id = format!("container-chain-{}", para_id);
212
        let properties = properties_to_map(&genesis_data.properties)
213
            .map_err(|e| format!("Invalid properties: {}", e))?;
214
        let extensions = Extensions {
215
            relay_chain,
216
            para_id,
217
        };
218

            
219
        let chain_spec = crate::chain_spec::RawChainSpec::builder(
220
            // This code is not used, we override it in `set_storage` below
221
            &[],
222
            // TODO: what to do with extensions? We are hardcoding the relay_chain and the para_id, any
223
            // other extensions are being ignored
224
            extensions,
225
        )
226
        .with_name(&name)
227
        .with_id(&id)
228
        .with_chain_type(chain_type)
229
        .with_properties(properties)
230
        .with_boot_nodes(boot_nodes)
231
        .with_protocol_id(&protocol_id);
232

            
233
        let chain_spec = if let Some(fork_id) = genesis_data.fork_id {
234
            let fork_id_string =
235
                String::from_utf8(fork_id.to_vec()).map_err(|_e| "Invalid fork_id".to_string())?;
236
            chain_spec.with_fork_id(&fork_id_string)
237
        } else {
238
            chain_spec
239
        };
240

            
241
        let mut chain_spec = chain_spec.build();
242

            
243
        chain_spec.set_storage(Storage {
244
            top: storage_raw,
245
            children_default: Default::default(),
246
        });
247

            
248
        Ok(chain_spec)
249
    }
250

            
251
    pub fn preload_chain_spec_from_genesis_data(
252
        &mut self,
253
        para_id: u32,
254
        genesis_data: ContainerChainGenesisData,
255
        chain_type: sc_chain_spec::ChainType,
256
        relay_chain: String,
257
        boot_nodes: Vec<MultiaddrWithPeerId>,
258
    ) -> Result<(), String> {
259
        let chain_spec = Self::chain_spec_from_genesis_data(
260
            para_id,
261
            genesis_data,
262
            chain_type,
263
            relay_chain,
264
            boot_nodes,
265
        )?;
266
        self.preloaded_chain_spec = Some(Box::new(chain_spec));
267

            
268
        Ok(())
269
    }
270
}
271

            
272
impl sc_cli::SubstrateCli for ContainerChainCli {
273
    fn impl_name() -> String {
274
        "Container chain".into()
275
    }
276

            
277
15
    fn impl_version() -> String {
278
15
        env!("SUBSTRATE_CLI_IMPL_VERSION").into()
279
15
    }
280

            
281
    fn description() -> String {
282
        format!(
283
            "Container chain\n\nThe command-line arguments provided first will be \
284
		passed to the orchestrator chain node, while the arguments provided after -- will be passed \
285
		to the container chain node, and the arguments provided after another -- will be passed \
286
		to the relay chain node\n\n\
287
		{} [orchestrator-args] -- [container-chain-args] -- [relay-chain-args] -- ",
288
            Self::executable_name()
289
        )
290
    }
291

            
292
    fn author() -> String {
293
        env!("CARGO_PKG_AUTHORS").into()
294
    }
295

            
296
    fn support_url() -> String {
297
        "https://github.com/moondance-labs/tanssi/issues/new".into()
298
    }
299

            
300
    fn copyright_start_year() -> i32 {
301
        2020
302
    }
303

            
304
    fn load_spec(&self, id: &str) -> std::result::Result<Box<dyn sc_cli::ChainSpec>, String> {
305
        // ContainerChain ChainSpec must be preloaded beforehand because we need to call async
306
        // functions to generate it, and this function is not async.
307
        let para_id = parse_container_chain_id_str(id)?;
308

            
309
        match &self.preloaded_chain_spec {
310
            Some(spec) => {
311
                let spec_para_id = Extensions::try_get(&**spec).map(|extension| extension.para_id);
312

            
313
                if spec_para_id == Some(para_id) {
314
                    Ok(spec.cloned_box())
315
                } else {
316
                    Err(format!(
317
                        "Expected ChainSpec for id {}, found ChainSpec for id {:?} instead",
318
                        para_id, spec_para_id
319
                    ))
320
                }
321
            }
322
            None => Err(format!("ChainSpec for {} not found", id)),
323
        }
324
    }
325
}
326

            
327
impl sc_cli::DefaultConfigurationValues for ContainerChainCli {
328
    fn p2p_listen_port() -> u16 {
329
        30335
330
    }
331

            
332
    fn rpc_listen_port() -> u16 {
333
        9946
334
    }
335

            
336
    fn prometheus_listen_port() -> u16 {
337
        9617
338
    }
339
}
340

            
341
impl sc_cli::CliConfiguration<Self> for ContainerChainCli {
342
    fn shared_params(&self) -> &sc_cli::SharedParams {
343
        self.base.base.shared_params()
344
    }
345

            
346
    fn import_params(&self) -> Option<&sc_cli::ImportParams> {
347
        self.base.base.import_params()
348
    }
349

            
350
    fn network_params(&self) -> Option<&sc_cli::NetworkParams> {
351
        self.base.base.network_params()
352
    }
353

            
354
    fn keystore_params(&self) -> Option<&sc_cli::KeystoreParams> {
355
        self.base.base.keystore_params()
356
    }
357

            
358
    fn base_path(&self) -> sc_cli::Result<Option<sc_service::BasePath>> {
359
        self.shared_params().base_path()
360
    }
361

            
362
    fn rpc_addr(
363
        &self,
364
        default_listen_port: u16,
365
    ) -> sc_cli::Result<Option<Vec<sc_cli::RpcEndpoint>>> {
366
        self.base.base.rpc_addr(default_listen_port)
367
    }
368

            
369
    fn prometheus_config(
370
        &self,
371
        default_listen_port: u16,
372
        chain_spec: &Box<dyn sc_cli::ChainSpec>,
373
    ) -> sc_cli::Result<Option<sc_service::config::PrometheusConfig>> {
374
        self.base
375
            .base
376
            .prometheus_config(default_listen_port, chain_spec)
377
    }
378

            
379
    fn init<F>(
380
        &self,
381
        support_url: &String,
382
        impl_version: &String,
383
        logger_hook: F,
384
    ) -> sc_cli::Result<()>
385
    where
386
        F: FnOnce(&mut sc_cli::LoggerBuilder),
387
    {
388
        self.base.base.init(support_url, impl_version, logger_hook)
389
    }
390

            
391
    fn chain_id(&self, _is_dev: bool) -> sc_cli::Result<String> {
392
        // Make chain id from para_id if present, otherwise use a generic name.
393
        // We may not know the para_id in advance.
394
        Ok(self
395
            .base
396
            .para_id
397
            .map(|para_id| format!("container-chain-{}", para_id))
398
            .unwrap_or("container-chain-unknown".into()))
399
    }
400

            
401
    fn role(&self, is_dev: bool) -> sc_cli::Result<sc_service::Role> {
402
        self.base.base.role(is_dev)
403
    }
404

            
405
    fn transaction_pool(
406
        &self,
407
        is_dev: bool,
408
    ) -> sc_cli::Result<sc_service::config::TransactionPoolOptions> {
409
        self.base.base.transaction_pool(is_dev)
410
    }
411

            
412
    fn trie_cache_maximum_size(&self) -> sc_cli::Result<Option<usize>> {
413
        self.base.base.trie_cache_maximum_size()
414
    }
415

            
416
    fn rpc_methods(&self) -> sc_cli::Result<sc_service::config::RpcMethods> {
417
        self.base.base.rpc_methods()
418
    }
419

            
420
    fn rpc_max_connections(&self) -> sc_cli::Result<u32> {
421
        self.base.base.rpc_max_connections()
422
    }
423

            
424
    fn rpc_cors(&self, is_dev: bool) -> sc_cli::Result<Option<Vec<String>>> {
425
        self.base.base.rpc_cors(is_dev)
426
    }
427

            
428
    fn default_heap_pages(&self) -> sc_cli::Result<Option<u64>> {
429
        self.base.base.default_heap_pages()
430
    }
431

            
432
    fn force_authoring(&self) -> sc_cli::Result<bool> {
433
        self.base.base.force_authoring()
434
    }
435

            
436
    fn disable_grandpa(&self) -> sc_cli::Result<bool> {
437
        self.base.base.disable_grandpa()
438
    }
439

            
440
    fn max_runtime_instances(&self) -> sc_cli::Result<Option<usize>> {
441
        self.base.base.max_runtime_instances()
442
    }
443

            
444
    fn announce_block(&self) -> sc_cli::Result<bool> {
445
        self.base.base.announce_block()
446
    }
447

            
448
    fn telemetry_endpoints(
449
        &self,
450
        chain_spec: &Box<dyn sc_chain_spec::ChainSpec>,
451
    ) -> sc_cli::Result<Option<sc_telemetry::TelemetryEndpoints>> {
452
        self.base.base.telemetry_endpoints(chain_spec)
453
    }
454

            
455
    fn node_name(&self) -> sc_cli::Result<String> {
456
        self.base.base.node_name()
457
    }
458
}
459

            
460
/// Parse ParaId(2000) from a string like "container-chain-2000"
461
fn parse_container_chain_id_str(id: &str) -> std::result::Result<u32, String> {
462
    // The id has been created using format!("container-chain-{}", para_id), so here we need
463
    // to reverse that.
464
    id.strip_prefix("container-chain-")
465
        .and_then(|s| {
466
            let id: u32 = s.parse().ok()?;
467

            
468
            // `.parse()` ignores leading zeros, so convert the id back to string to check
469
            // if we get the same string, this way we ensure a 1:1 mapping
470
            if id.to_string() == s {
471
                Some(id)
472
            } else {
473
                None
474
            }
475
        })
476
        .ok_or_else(|| format!("load_spec called with invalid id: {:?}", id))
477
}
478

            
479
// Copied from polkadot-sdk/cumulus/client/cli/src/lib.rs
480
fn validate_relay_chain_url(arg: &str) -> Result<Url, String> {
481
    let url = Url::parse(arg).map_err(|e| e.to_string())?;
482

            
483
    let scheme = url.scheme();
484
    if scheme == "ws" || scheme == "wss" {
485
        Ok(url)
486
    } else {
487
        Err(format!(
488
            "'{}' URL scheme not supported. Only websocket RPC is currently supported",
489
            url.scheme()
490
        ))
491
    }
492
}
493

            
494
/// Returns the value of `base_path` or the default_path if it is None
495
pub(crate) fn base_path_or_default(base_path: Option<BasePath>, executable_name: &str) -> BasePath {
496
    base_path.unwrap_or_else(|| BasePath::from_project("", "", executable_name))
497
}