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
    crate::chain_spec::RawGenesisConfig,
19
    cumulus_client_cli::{CollatorOptions, RelayChainMode},
20
    dc_orchestrator_chain_interface::ContainerChainGenesisData,
21
    dp_container_chain_genesis_data::json::properties_to_map,
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, net::SocketAddr},
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
    /// Creates a less resource-hungry node that retrieves relay chain data from an RPC endpoint.
54
    ///
55
    /// The provided URLs should point to RPC endpoints of the relay chain.
56
    /// This node connects to the remote nodes following the order they were specified in. If the
57
    /// connection fails, it attempts to connect to the next endpoint in the list.
58
    ///
59
    /// Note: This option doesn't stop the node from connecting to the relay chain network but
60
    /// reduces bandwidth use.
61
    #[arg(
62
		long,
63
		value_parser = validate_relay_chain_url,
64
		num_args = 0..,
65
		alias = "relay-chain-rpc-url"
66
    )]
67
    pub relay_chain_rpc_urls: Vec<Url>,
68

            
69
    /// EXPERIMENTAL: Embed a light client for the relay chain. Only supported for full-nodes.
70
    /// Will use the specified relay chain chainspec.
71
    #[arg(long, conflicts_with_all = ["relay_chain_rpc_urls", "collator"])]
72
    pub relay_chain_light_client: bool,
73
}
74

            
75
impl ContainerChainRunCmd {
76
    /// Create a [`NormalizedRunCmd`] which merges the `collator` cli argument into `validator` to
77
    /// have only one.
78
    pub fn normalize(&self) -> ContainerChainCli {
79
        let mut new_base = self.clone();
80

            
81
        new_base.base.validator = self.base.validator || self.collator;
82

            
83
        // Append `containers/` to base_path for this object. This is to ensure that when spawning
84
        // a new container chain, its database is always inside the `containers` folder.
85
        // So if the user passes `--base-path /tmp/node`, we want the ephemeral container data in
86
        // `/tmp/node/containers`, and the persistent storage in `/tmp/node/config`.
87
        let base_path = base_path_or_default(
88
            self.base.base_path().expect("failed to get base_path"),
89
            &ContainerChainCli::executable_name(),
90
        );
91

            
92
        let base_path = base_path.path().join("containers");
93
        new_base.base.shared_params.base_path = Some(base_path);
94

            
95
        ContainerChainCli {
96
            base: new_base,
97
            preloaded_chain_spec: None,
98
        }
99
    }
100

            
101
    /// Create [`CollatorOptions`] representing options only relevant to parachain collator nodes
102
    // Copied from polkadot-sdk/cumulus/client/cli/src/lib.rs
103
    pub fn collator_options(&self) -> CollatorOptions {
104
        let relay_chain_mode = match (
105
            self.relay_chain_light_client,
106
            !self.relay_chain_rpc_urls.is_empty(),
107
        ) {
108
            (true, _) => RelayChainMode::LightClient,
109
            (_, true) => RelayChainMode::ExternalRpc(self.relay_chain_rpc_urls.clone()),
110
            _ => RelayChainMode::Embedded,
111
        };
112

            
113
        CollatorOptions { relay_chain_mode }
114
    }
115
}
116

            
117
#[derive(Debug)]
118
pub struct ContainerChainCli {
119
    /// The actual container chain cli object.
120
    pub base: ContainerChainRunCmd,
121

            
122
    /// The ChainSpecs that this struct can initialize. This starts empty and gets filled
123
    /// by calling preload_chain_spec_file.
124
    pub preloaded_chain_spec: Option<Box<dyn sc_chain_spec::ChainSpec>>,
125
}
126

            
127
impl Clone for ContainerChainCli {
128
    fn clone(&self) -> Self {
129
        Self {
130
            base: self.base.clone(),
131
            preloaded_chain_spec: self.preloaded_chain_spec.as_ref().map(|x| x.cloned_box()),
132
        }
133
    }
134
}
135

            
136
impl ContainerChainCli {
137
    /// Parse the container chain CLI parameters using the para chain `Configuration`.
138
    pub fn new<'a>(
139
        para_config: &sc_service::Configuration,
140
        container_chain_args: impl Iterator<Item = &'a String>,
141
    ) -> Self {
142
        let mut base: ContainerChainRunCmd = clap::Parser::parse_from(container_chain_args);
143

            
144
        // Copy some parachain args into container chain args
145

            
146
        // If the container chain args have no --wasmtime-precompiled flag, use the same as the orchestrator
147
        if base.base.import_params.wasmtime_precompiled.is_none() {
148
            base.base
149
                .import_params
150
                .wasmtime_precompiled
151
                .clone_from(&para_config.wasmtime_precompiled);
152
        }
153

            
154
        // Set container base path to the same value as orchestrator base_path.
155
        // "containers" is appended in `base.normalize()`
156
        if base.base.shared_params.base_path.is_some() {
157
            log::warn!("Container chain --base-path is being ignored");
158
        }
159
        let base_path = para_config.base_path.path().to_owned();
160
        base.base.shared_params.base_path = Some(base_path);
161

            
162
        base.normalize()
163
    }
164

            
165
    pub fn chain_spec_from_genesis_data(
166
        para_id: u32,
167
        genesis_data: ContainerChainGenesisData,
168
        chain_type: sc_chain_spec::ChainType,
169
        relay_chain: String,
170
        boot_nodes: Vec<MultiaddrWithPeerId>,
171
    ) -> Result<crate::chain_spec::RawChainSpec, String> {
172
        let name = String::from_utf8(genesis_data.name).map_err(|_e| "Invalid name".to_string())?;
173
        let id: String =
174
            String::from_utf8(genesis_data.id).map_err(|_e| "Invalid id".to_string())?;
175
        let storage_raw: BTreeMap<_, _> =
176
            genesis_data.storage.into_iter().map(|x| x.into()).collect();
177
        let protocol_id = format!("container-chain-{}", para_id);
178
        let properties = properties_to_map(&genesis_data.properties)
179
            .map_err(|e| format!("Invalid properties: {}", e))?;
180
        let extensions = crate::chain_spec::Extensions {
181
            relay_chain,
182
            para_id,
183
        };
184
        let raw_genesis_config = RawGenesisConfig {
185
            storage_raw: storage_raw.clone(),
186
        };
187

            
188
        let chain_spec = crate::chain_spec::RawChainSpec::builder(
189
            // This code is not used, we override it in `set_storage` below
190
            &[],
191
            // TODO: what to do with extensions? We are hardcoding the relay_chain and the para_id, any
192
            // other extensions are being ignored
193
            extensions,
194
        )
195
        .with_name(&name)
196
        .with_id(&id)
197
        .with_chain_type(chain_type)
198
        .with_properties(properties)
199
        .with_boot_nodes(boot_nodes)
200
        .with_protocol_id(&protocol_id);
201

            
202
        let chain_spec = if let Some(fork_id) = genesis_data.fork_id {
203
            let fork_id_string =
204
                String::from_utf8(fork_id).map_err(|_e| "Invalid fork_id".to_string())?;
205
            chain_spec.with_fork_id(&fork_id_string)
206
        } else {
207
            chain_spec
208
        };
209

            
210
        let mut chain_spec = chain_spec.build();
211

            
212
        chain_spec.set_storage(Storage {
213
            top: raw_genesis_config.storage_raw,
214
            children_default: Default::default(),
215
        });
216

            
217
        Ok(chain_spec)
218
    }
219

            
220
    pub fn preload_chain_spec_from_genesis_data(
221
        &mut self,
222
        para_id: u32,
223
        genesis_data: ContainerChainGenesisData,
224
        chain_type: sc_chain_spec::ChainType,
225
        relay_chain: String,
226
        boot_nodes: Vec<MultiaddrWithPeerId>,
227
    ) -> Result<(), String> {
228
        let chain_spec = Self::chain_spec_from_genesis_data(
229
            para_id,
230
            genesis_data,
231
            chain_type,
232
            relay_chain,
233
            boot_nodes,
234
        )?;
235
        self.preloaded_chain_spec = Some(Box::new(chain_spec));
236

            
237
        Ok(())
238
    }
239
}
240

            
241
impl sc_cli::SubstrateCli for ContainerChainCli {
242
    fn impl_name() -> String {
243
        "Container chain".into()
244
    }
245

            
246
    fn impl_version() -> String {
247
        env!("SUBSTRATE_CLI_IMPL_VERSION").into()
248
    }
249

            
250
    fn description() -> String {
251
        format!(
252
            "Container chain\n\nThe command-line arguments provided first will be \
253
		passed to the orchestrator chain node, while the arguments provided after -- will be passed \
254
		to the container chain node, and the arguments provided after another -- will be passed \
255
		to the relay chain node\n\n\
256
		{} [orchestrator-args] -- [container-chain-args] -- [relay-chain-args] -- ",
257
            Self::executable_name()
258
        )
259
    }
260

            
261
    fn author() -> String {
262
        env!("CARGO_PKG_AUTHORS").into()
263
    }
264

            
265
    fn support_url() -> String {
266
        "https://github.com/paritytech/cumulus/issues/new".into()
267
    }
268

            
269
    fn copyright_start_year() -> i32 {
270
        2020
271
    }
272

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

            
278
        match &self.preloaded_chain_spec {
279
            Some(spec) => {
280
                let spec_para_id = crate::chain_spec::Extensions::try_get(&**spec)
281
                    .map(|extension| extension.para_id);
282

            
283
                if spec_para_id == Some(para_id) {
284
                    Ok(spec.cloned_box())
285
                } else {
286
                    Err(format!(
287
                        "Expected ChainSpec for id {}, found ChainSpec for id {:?} instead",
288
                        para_id, spec_para_id
289
                    ))
290
                }
291
            }
292
            None => Err(format!("ChainSpec for {} not found", id)),
293
        }
294
    }
295
}
296

            
297
impl sc_cli::DefaultConfigurationValues for ContainerChainCli {
298
    fn p2p_listen_port() -> u16 {
299
        30335
300
    }
301

            
302
    fn rpc_listen_port() -> u16 {
303
        9946
304
    }
305

            
306
    fn prometheus_listen_port() -> u16 {
307
        9617
308
    }
309
}
310

            
311
impl sc_cli::CliConfiguration<Self> for ContainerChainCli {
312
    fn shared_params(&self) -> &sc_cli::SharedParams {
313
        self.base.base.shared_params()
314
    }
315

            
316
    fn import_params(&self) -> Option<&sc_cli::ImportParams> {
317
        self.base.base.import_params()
318
    }
319

            
320
    fn network_params(&self) -> Option<&sc_cli::NetworkParams> {
321
        self.base.base.network_params()
322
    }
323

            
324
    fn keystore_params(&self) -> Option<&sc_cli::KeystoreParams> {
325
        self.base.base.keystore_params()
326
    }
327

            
328
    fn base_path(&self) -> sc_cli::Result<Option<sc_service::BasePath>> {
329
        self.shared_params().base_path()
330
    }
331

            
332
    fn rpc_addr(&self, default_listen_port: u16) -> sc_cli::Result<Option<SocketAddr>> {
333
        self.base.base.rpc_addr(default_listen_port)
334
    }
335

            
336
    fn prometheus_config(
337
        &self,
338
        default_listen_port: u16,
339
        chain_spec: &Box<dyn sc_cli::ChainSpec>,
340
    ) -> sc_cli::Result<Option<sc_service::config::PrometheusConfig>> {
341
        self.base
342
            .base
343
            .prometheus_config(default_listen_port, chain_spec)
344
    }
345

            
346
    fn init<F>(
347
        &self,
348
        _support_url: &String,
349
        _impl_version: &String,
350
        _logger_hook: F,
351
        _config: &sc_service::Configuration,
352
    ) -> sc_cli::Result<()>
353
    where
354
        F: FnOnce(&mut sc_cli::LoggerBuilder, &sc_service::Configuration),
355
    {
356
        unreachable!("PolkadotCli is never initialized; qed");
357
    }
358

            
359
    fn chain_id(&self, _is_dev: bool) -> sc_cli::Result<String> {
360
        self.base
361
            .para_id
362
            .map(|para_id| format!("container-chain-{}", para_id))
363
            .ok_or("no para-id in container chain args".into())
364
    }
365

            
366
    fn role(&self, is_dev: bool) -> sc_cli::Result<sc_service::Role> {
367
        self.base.base.role(is_dev)
368
    }
369

            
370
    fn transaction_pool(
371
        &self,
372
        is_dev: bool,
373
    ) -> sc_cli::Result<sc_service::config::TransactionPoolOptions> {
374
        self.base.base.transaction_pool(is_dev)
375
    }
376

            
377
    fn trie_cache_maximum_size(&self) -> sc_cli::Result<Option<usize>> {
378
        self.base.base.trie_cache_maximum_size()
379
    }
380

            
381
    fn rpc_methods(&self) -> sc_cli::Result<sc_service::config::RpcMethods> {
382
        self.base.base.rpc_methods()
383
    }
384

            
385
    fn rpc_max_connections(&self) -> sc_cli::Result<u32> {
386
        self.base.base.rpc_max_connections()
387
    }
388

            
389
    fn rpc_cors(&self, is_dev: bool) -> sc_cli::Result<Option<Vec<String>>> {
390
        self.base.base.rpc_cors(is_dev)
391
    }
392

            
393
    fn default_heap_pages(&self) -> sc_cli::Result<Option<u64>> {
394
        self.base.base.default_heap_pages()
395
    }
396

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

            
401
    fn disable_grandpa(&self) -> sc_cli::Result<bool> {
402
        self.base.base.disable_grandpa()
403
    }
404

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

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

            
413
    fn telemetry_endpoints(
414
        &self,
415
        chain_spec: &Box<dyn sc_chain_spec::ChainSpec>,
416
    ) -> sc_cli::Result<Option<sc_telemetry::TelemetryEndpoints>> {
417
        self.base.base.telemetry_endpoints(chain_spec)
418
    }
419

            
420
    fn node_name(&self) -> sc_cli::Result<String> {
421
        self.base.base.node_name()
422
    }
423
}
424

            
425
/// Parse ParaId(2000) from a string like "container-chain-2000"
426
fn parse_container_chain_id_str(id: &str) -> std::result::Result<u32, String> {
427
    // The id has been created using format!("container-chain-{}", para_id), so here we need
428
    // to reverse that.
429
    id.strip_prefix("container-chain-")
430
        .and_then(|s| {
431
            let id: u32 = s.parse().ok()?;
432

            
433
            // `.parse()` ignores leading zeros, so convert the id back to string to check
434
            // if we get the same string, this way we ensure a 1:1 mapping
435
            if id.to_string() == s {
436
                Some(id)
437
            } else {
438
                None
439
            }
440
        })
441
        .ok_or_else(|| format!("load_spec called with invalid id: {:?}", id))
442
}
443

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

            
448
    let scheme = url.scheme();
449
    if scheme == "ws" || scheme == "wss" {
450
        Ok(url)
451
    } else {
452
        Err(format!(
453
            "'{}' URL scheme not supported. Only websocket RPC is currently supported",
454
            url.scheme()
455
        ))
456
    }
457
}
458

            
459
/// Returns the value of `base_path` or the default_path if it is None
460
pub(crate) fn base_path_or_default(
461
    base_path: Option<BasePath>,
462
    executable_name: &String,
463
) -> BasePath {
464
    base_path.unwrap_or_else(|| BasePath::from_project("", "", executable_name))
465
}