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(id = "tanssi_experimental_max_pov_percentage")]
85
    pub experimental_max_pov_percentage: Option<u32>,
86
}
87

            
88
impl ContainerChainRunCmd {
89
    /// Create a [`NormalizedRunCmd`] which merges the `collator` cli argument into `validator` to
90
    /// have only one.
91
    pub fn normalize(&self) -> ContainerChainCli {
92
        let mut new_base = self.clone();
93

            
94
        new_base.base.validator = self.base.validator || self.collator;
95

            
96
        // Append `containers/` to base_path for this object. This is to ensure that when spawning
97
        // a new container chain, its database is always inside the `containers` folder.
98
        // So if the user passes `--base-path /tmp/node`, we want the ephemeral container data in
99
        // `/tmp/node/containers`, and the persistent storage in `/tmp/node/config`.
100
        let base_path = base_path_or_default(
101
            self.base.base_path().expect("failed to get base_path"),
102
            &ContainerChainCli::executable_name(),
103
        );
104

            
105
        let base_path = base_path.path().join("containers");
106
        new_base.base.shared_params.base_path = Some(base_path);
107

            
108
        ContainerChainCli {
109
            base: new_base,
110
            preloaded_chain_spec: None,
111
        }
112
    }
113

            
114
    /// Create [`CollatorOptions`] representing options only relevant to parachain collator nodes
115
    // Copied from polkadot-sdk/cumulus/client/cli/src/lib.rs
116
    pub fn collator_options(&self) -> CollatorOptions {
117
        let relay_chain_mode = match (
118
            self.relay_chain_light_client,
119
            !self.relay_chain_rpc_urls.is_empty(),
120
        ) {
121
            (true, _) => RelayChainMode::LightClient,
122
            (_, true) => RelayChainMode::ExternalRpc(self.relay_chain_rpc_urls.clone()),
123
            _ => RelayChainMode::Embedded,
124
        };
125

            
126
        CollatorOptions { relay_chain_mode }
127
    }
128
}
129

            
130
#[derive(Debug)]
131
pub struct ContainerChainCli {
132
    /// The actual container chain cli object.
133
    pub base: ContainerChainRunCmd,
134

            
135
    /// The ChainSpecs that this struct can initialize. This starts empty and gets filled
136
    /// by calling preload_chain_spec_file.
137
    pub preloaded_chain_spec: Option<Box<dyn sc_chain_spec::ChainSpec>>,
138
}
139

            
140
impl Clone for ContainerChainCli {
141
    fn clone(&self) -> Self {
142
        Self {
143
            base: self.base.clone(),
144
            preloaded_chain_spec: self.preloaded_chain_spec.as_ref().map(|x| x.cloned_box()),
145
        }
146
    }
147
}
148

            
149
impl ContainerChainCli {
150
    /// Parse the container chain CLI parameters using the para chain `Configuration`.
151
    pub fn new<'a>(
152
        para_config: &sc_service::Configuration,
153
        container_chain_args: impl Iterator<Item = &'a String>,
154
    ) -> Self {
155
        let mut base: ContainerChainRunCmd = clap::Parser::parse_from(container_chain_args);
156

            
157
        // Copy some parachain args into container chain args
158

            
159
        // If the container chain args have no --wasmtime-precompiled flag, use the same as the orchestrator
160
        if base.base.import_params.wasmtime_precompiled.is_none() {
161
            base.base
162
                .import_params
163
                .wasmtime_precompiled
164
                .clone_from(&para_config.executor.wasmtime_precompiled);
165
        }
166

            
167
        // Set container base path to the same value as orchestrator base_path.
168
        // "containers" is appended in `base.normalize()`
169
        if base.base.shared_params.base_path.is_some() {
170
            log::warn!("Container chain --base-path is being ignored");
171
        }
172
        let base_path = para_config.base_path.path().to_owned();
173
        base.base.shared_params.base_path = Some(base_path);
174

            
175
        base.normalize()
176
    }
177

            
178
    pub fn chain_spec_from_genesis_data(
179
        para_id: u32,
180
        genesis_data: ContainerChainGenesisData,
181
        chain_type: sc_chain_spec::ChainType,
182
        relay_chain: String,
183
        boot_nodes: Vec<MultiaddrWithPeerId>,
184
    ) -> Result<crate::chain_spec::RawChainSpec, String> {
185
        let name = String::from_utf8(genesis_data.name.to_vec())
186
            .map_err(|_e| "Invalid name".to_string())?;
187
        let id: String =
188
            String::from_utf8(genesis_data.id.to_vec()).map_err(|_e| "Invalid id".to_string())?;
189
        let storage_raw: BTreeMap<_, _> =
190
            genesis_data.storage.into_iter().map(|x| x.into()).collect();
191
        let protocol_id = format!("container-chain-{}", para_id);
192
        let properties = properties_to_map(&genesis_data.properties)
193
            .map_err(|e| format!("Invalid properties: {}", e))?;
194
        let extensions = Extensions {
195
            relay_chain,
196
            para_id,
197
        };
198

            
199
        let chain_spec = crate::chain_spec::RawChainSpec::builder(
200
            // This code is not used, we override it in `set_storage` below
201
            &[],
202
            // TODO: what to do with extensions? We are hardcoding the relay_chain and the para_id, any
203
            // other extensions are being ignored
204
            extensions,
205
        )
206
        .with_name(&name)
207
        .with_id(&id)
208
        .with_chain_type(chain_type)
209
        .with_properties(properties)
210
        .with_boot_nodes(boot_nodes)
211
        .with_protocol_id(&protocol_id);
212

            
213
        let chain_spec = if let Some(fork_id) = genesis_data.fork_id {
214
            let fork_id_string =
215
                String::from_utf8(fork_id.to_vec()).map_err(|_e| "Invalid fork_id".to_string())?;
216
            chain_spec.with_fork_id(&fork_id_string)
217
        } else {
218
            chain_spec
219
        };
220

            
221
        let mut chain_spec = chain_spec.build();
222

            
223
        chain_spec.set_storage(Storage {
224
            top: storage_raw,
225
            children_default: Default::default(),
226
        });
227

            
228
        Ok(chain_spec)
229
    }
230

            
231
    pub fn preload_chain_spec_from_genesis_data(
232
        &mut self,
233
        para_id: u32,
234
        genesis_data: ContainerChainGenesisData,
235
        chain_type: sc_chain_spec::ChainType,
236
        relay_chain: String,
237
        boot_nodes: Vec<MultiaddrWithPeerId>,
238
    ) -> Result<(), String> {
239
        let chain_spec = Self::chain_spec_from_genesis_data(
240
            para_id,
241
            genesis_data,
242
            chain_type,
243
            relay_chain,
244
            boot_nodes,
245
        )?;
246
        self.preloaded_chain_spec = Some(Box::new(chain_spec));
247

            
248
        Ok(())
249
    }
250
}
251

            
252
impl sc_cli::SubstrateCli for ContainerChainCli {
253
    fn impl_name() -> String {
254
        "Container chain".into()
255
    }
256

            
257
9
    fn impl_version() -> String {
258
9
        env!("SUBSTRATE_CLI_IMPL_VERSION").into()
259
9
    }
260

            
261
    fn description() -> String {
262
        format!(
263
            "Container chain\n\nThe command-line arguments provided first will be \
264
		passed to the orchestrator chain node, while the arguments provided after -- will be passed \
265
		to the container chain node, and the arguments provided after another -- will be passed \
266
		to the relay chain node\n\n\
267
		{} [orchestrator-args] -- [container-chain-args] -- [relay-chain-args] -- ",
268
            Self::executable_name()
269
        )
270
    }
271

            
272
    fn author() -> String {
273
        env!("CARGO_PKG_AUTHORS").into()
274
    }
275

            
276
    fn support_url() -> String {
277
        "https://github.com/paritytech/cumulus/issues/new".into()
278
    }
279

            
280
    fn copyright_start_year() -> i32 {
281
        2020
282
    }
283

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

            
289
        match &self.preloaded_chain_spec {
290
            Some(spec) => {
291
                let spec_para_id = Extensions::try_get(&**spec).map(|extension| extension.para_id);
292

            
293
                if spec_para_id == Some(para_id) {
294
                    Ok(spec.cloned_box())
295
                } else {
296
                    Err(format!(
297
                        "Expected ChainSpec for id {}, found ChainSpec for id {:?} instead",
298
                        para_id, spec_para_id
299
                    ))
300
                }
301
            }
302
            None => Err(format!("ChainSpec for {} not found", id)),
303
        }
304
    }
305
}
306

            
307
impl sc_cli::DefaultConfigurationValues for ContainerChainCli {
308
    fn p2p_listen_port() -> u16 {
309
        30335
310
    }
311

            
312
    fn rpc_listen_port() -> u16 {
313
        9946
314
    }
315

            
316
    fn prometheus_listen_port() -> u16 {
317
        9617
318
    }
319
}
320

            
321
impl sc_cli::CliConfiguration<Self> for ContainerChainCli {
322
    fn shared_params(&self) -> &sc_cli::SharedParams {
323
        self.base.base.shared_params()
324
    }
325

            
326
    fn import_params(&self) -> Option<&sc_cli::ImportParams> {
327
        self.base.base.import_params()
328
    }
329

            
330
    fn network_params(&self) -> Option<&sc_cli::NetworkParams> {
331
        self.base.base.network_params()
332
    }
333

            
334
    fn keystore_params(&self) -> Option<&sc_cli::KeystoreParams> {
335
        self.base.base.keystore_params()
336
    }
337

            
338
    fn base_path(&self) -> sc_cli::Result<Option<sc_service::BasePath>> {
339
        self.shared_params().base_path()
340
    }
341

            
342
    fn rpc_addr(
343
        &self,
344
        default_listen_port: u16,
345
    ) -> sc_cli::Result<Option<Vec<sc_cli::RpcEndpoint>>> {
346
        self.base.base.rpc_addr(default_listen_port)
347
    }
348

            
349
    fn prometheus_config(
350
        &self,
351
        default_listen_port: u16,
352
        chain_spec: &Box<dyn sc_cli::ChainSpec>,
353
    ) -> sc_cli::Result<Option<sc_service::config::PrometheusConfig>> {
354
        self.base
355
            .base
356
            .prometheus_config(default_listen_port, chain_spec)
357
    }
358

            
359
    fn init<F>(
360
        &self,
361
        _support_url: &String,
362
        _impl_version: &String,
363
        _logger_hook: F,
364
    ) -> sc_cli::Result<()>
365
    where
366
        F: FnOnce(&mut sc_cli::LoggerBuilder),
367
    {
368
        unreachable!("PolkadotCli is never initialized; qed");
369
    }
370

            
371
    fn chain_id(&self, _is_dev: bool) -> sc_cli::Result<String> {
372
        self.base
373
            .para_id
374
            .map(|para_id| format!("container-chain-{}", para_id))
375
            .ok_or("no para-id in container chain args".into())
376
    }
377

            
378
    fn role(&self, is_dev: bool) -> sc_cli::Result<sc_service::Role> {
379
        self.base.base.role(is_dev)
380
    }
381

            
382
    fn transaction_pool(
383
        &self,
384
        is_dev: bool,
385
    ) -> sc_cli::Result<sc_service::config::TransactionPoolOptions> {
386
        self.base.base.transaction_pool(is_dev)
387
    }
388

            
389
    fn trie_cache_maximum_size(&self) -> sc_cli::Result<Option<usize>> {
390
        self.base.base.trie_cache_maximum_size()
391
    }
392

            
393
    fn rpc_methods(&self) -> sc_cli::Result<sc_service::config::RpcMethods> {
394
        self.base.base.rpc_methods()
395
    }
396

            
397
    fn rpc_max_connections(&self) -> sc_cli::Result<u32> {
398
        self.base.base.rpc_max_connections()
399
    }
400

            
401
    fn rpc_cors(&self, is_dev: bool) -> sc_cli::Result<Option<Vec<String>>> {
402
        self.base.base.rpc_cors(is_dev)
403
    }
404

            
405
    fn default_heap_pages(&self) -> sc_cli::Result<Option<u64>> {
406
        self.base.base.default_heap_pages()
407
    }
408

            
409
    fn force_authoring(&self) -> sc_cli::Result<bool> {
410
        self.base.base.force_authoring()
411
    }
412

            
413
    fn disable_grandpa(&self) -> sc_cli::Result<bool> {
414
        self.base.base.disable_grandpa()
415
    }
416

            
417
    fn max_runtime_instances(&self) -> sc_cli::Result<Option<usize>> {
418
        self.base.base.max_runtime_instances()
419
    }
420

            
421
    fn announce_block(&self) -> sc_cli::Result<bool> {
422
        self.base.base.announce_block()
423
    }
424

            
425
    fn telemetry_endpoints(
426
        &self,
427
        chain_spec: &Box<dyn sc_chain_spec::ChainSpec>,
428
    ) -> sc_cli::Result<Option<sc_telemetry::TelemetryEndpoints>> {
429
        self.base.base.telemetry_endpoints(chain_spec)
430
    }
431

            
432
    fn node_name(&self) -> sc_cli::Result<String> {
433
        self.base.base.node_name()
434
    }
435
}
436

            
437
/// Parse ParaId(2000) from a string like "container-chain-2000"
438
fn parse_container_chain_id_str(id: &str) -> std::result::Result<u32, String> {
439
    // The id has been created using format!("container-chain-{}", para_id), so here we need
440
    // to reverse that.
441
    id.strip_prefix("container-chain-")
442
        .and_then(|s| {
443
            let id: u32 = s.parse().ok()?;
444

            
445
            // `.parse()` ignores leading zeros, so convert the id back to string to check
446
            // if we get the same string, this way we ensure a 1:1 mapping
447
            if id.to_string() == s {
448
                Some(id)
449
            } else {
450
                None
451
            }
452
        })
453
        .ok_or_else(|| format!("load_spec called with invalid id: {:?}", id))
454
}
455

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

            
460
    let scheme = url.scheme();
461
    if scheme == "ws" || scheme == "wss" {
462
        Ok(url)
463
    } else {
464
        Err(format!(
465
            "'{}' URL scheme not supported. Only websocket RPC is currently supported",
466
            url.scheme()
467
        ))
468
    }
469
}
470

            
471
/// Returns the value of `base_path` or the default_path if it is None
472
pub(crate) fn base_path_or_default(
473
    base_path: Option<BasePath>,
474
    executable_name: &String,
475
) -> BasePath {
476
    base_path.unwrap_or_else(|| BasePath::from_project("", "", executable_name))
477
}