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
    sc_chain_spec::ChainSpec,
22
    sc_cli::{CliConfiguration, SubstrateCli},
23
    sc_network::config::MultiaddrWithPeerId,
24
    sc_service::BasePath,
25
    sp_runtime::Storage,
26
    std::collections::BTreeMap,
27
    url::Url,
28
};
29

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

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

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

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

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

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

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

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

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

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

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

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

            
112
        CollatorOptions { relay_chain_mode }
113
    }
114
}
115

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

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

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

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

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

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

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

            
161
        base.normalize()
162
    }
163

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

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

            
198
        let chain_spec = if let Some(fork_id) = genesis_data.fork_id {
199
            let fork_id_string =
200
                String::from_utf8(fork_id).map_err(|_e| "Invalid fork_id".to_string())?;
201
            chain_spec.with_fork_id(&fork_id_string)
202
        } else {
203
            chain_spec
204
        };
205

            
206
        let mut chain_spec = chain_spec.build();
207

            
208
        chain_spec.set_storage(Storage {
209
            top: storage_raw,
210
            children_default: Default::default(),
211
        });
212

            
213
        Ok(chain_spec)
214
    }
215

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

            
233
        Ok(())
234
    }
235
}
236

            
237
impl sc_cli::SubstrateCli for ContainerChainCli {
238
    fn impl_name() -> String {
239
        "Container chain".into()
240
    }
241

            
242
9
    fn impl_version() -> String {
243
9
        env!("SUBSTRATE_CLI_IMPL_VERSION").into()
244
9
    }
245

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

            
257
    fn author() -> String {
258
        env!("CARGO_PKG_AUTHORS").into()
259
    }
260

            
261
    fn support_url() -> String {
262
        "https://github.com/paritytech/cumulus/issues/new".into()
263
    }
264

            
265
    fn copyright_start_year() -> i32 {
266
        2020
267
    }
268

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

            
274
        match &self.preloaded_chain_spec {
275
            Some(spec) => {
276
                let spec_para_id = crate::chain_spec::Extensions::try_get(&**spec)
277
                    .map(|extension| extension.para_id);
278

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

            
293
impl sc_cli::DefaultConfigurationValues for ContainerChainCli {
294
    fn p2p_listen_port() -> u16 {
295
        30335
296
    }
297

            
298
    fn rpc_listen_port() -> u16 {
299
        9946
300
    }
301

            
302
    fn prometheus_listen_port() -> u16 {
303
        9617
304
    }
305
}
306

            
307
impl sc_cli::CliConfiguration<Self> for ContainerChainCli {
308
    fn shared_params(&self) -> &sc_cli::SharedParams {
309
        self.base.base.shared_params()
310
    }
311

            
312
    fn import_params(&self) -> Option<&sc_cli::ImportParams> {
313
        self.base.base.import_params()
314
    }
315

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

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

            
324
    fn base_path(&self) -> sc_cli::Result<Option<sc_service::BasePath>> {
325
        self.shared_params().base_path()
326
    }
327

            
328
    fn rpc_addr(
329
        &self,
330
        default_listen_port: u16,
331
    ) -> sc_cli::Result<Option<Vec<sc_cli::RpcEndpoint>>> {
332
        self.base.base.rpc_addr(default_listen_port)
333
    }
334

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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