Skip to main content

linera_service/cli_wrappers/
local_net.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4#[cfg(with_testing)]
5use std::sync::LazyLock;
6use std::{
7    collections::BTreeMap,
8    env,
9    num::NonZeroU16,
10    path::{Path, PathBuf},
11    sync::Arc,
12    time::Duration,
13};
14
15use anyhow::{anyhow, bail, ensure, Context, Result};
16#[cfg(with_testing)]
17use async_lock::RwLock;
18use async_trait::async_trait;
19use linera_base::{
20    command::{resolve_binary, CommandExt},
21    data_types::Amount,
22};
23use linera_client::client_options::ResourceControlPolicyConfig;
24use linera_core::node::ValidatorNodeProvider;
25use linera_exporter::config::{BlockExporterConfig, Destination, DestinationConfig};
26use linera_rpc::config::{CrossChainConfig, ExporterServiceConfig, TlsConfig};
27#[cfg(all(feature = "storage-service", with_testing))]
28use linera_storage_service::common::storage_service_test_endpoint;
29#[cfg(all(feature = "rocksdb", feature = "scylladb", with_testing))]
30use linera_views::rocks_db::{RocksDbDatabase, RocksDbSpawnMode};
31#[cfg(all(feature = "scylladb", with_testing))]
32use linera_views::{scylla_db::ScyllaDbDatabase, store::TestKeyValueDatabase as _};
33use tempfile::{tempdir, TempDir};
34use tokio::process::{Child, Command};
35use tonic::transport::{channel::ClientTlsConfig, Endpoint};
36use tonic_health::pb::{
37    health_check_response::ServingStatus, health_client::HealthClient, HealthCheckRequest,
38};
39use tracing::{error, info, warn};
40
41use crate::{
42    cli_wrappers::{
43        ClientWrapper, LineraNet, LineraNetConfig, Network, NetworkConfig, OnClientDrop,
44    },
45    storage::{InnerStorageConfig, StorageConfig},
46    util::ChildExt,
47};
48
49/// Maximum allowed number of shards over all validators.
50const MAX_NUMBER_SHARDS: usize = 1000;
51
52/// Whether to process the inbox automatically before an operation.
53pub enum ProcessInbox {
54    /// Leaves the inbox untouched before the operation.
55    Skip,
56    /// Processes the inbox automatically before the operation.
57    Automatic,
58}
59
60#[cfg(with_testing)]
61static PORT_PROVIDER: LazyLock<RwLock<u16>> = LazyLock::new(|| RwLock::new(7080));
62
63/// The offset of the port
64fn test_offset_port() -> usize {
65    std::env::var("TEST_OFFSET_PORT")
66        .ok()
67        .and_then(|port_str| port_str.parse::<usize>().ok())
68        .unwrap_or(9000)
69}
70
71/// Provides a port for the node service. Increment the port numbers.
72#[cfg(with_testing)]
73pub async fn get_node_port() -> u16 {
74    let mut port = PORT_PROVIDER.write().await;
75    let port_ret = *port;
76    *port += 1;
77    info!("get_node_port returning port_ret={}", port_ret);
78    assert!(port_selector::is_free(port_ret));
79    port_ret
80}
81
82#[cfg(with_testing)]
83async fn make_testing_config(database: Database) -> Result<InnerStorageConfig> {
84    match database {
85        Database::Service => {
86            #[cfg(feature = "storage-service")]
87            {
88                let endpoint = storage_service_test_endpoint()
89                    .expect("Reading LINERA_STORAGE_SERVICE environment variable");
90                Ok(InnerStorageConfig::Service { endpoint })
91            }
92            #[cfg(not(feature = "storage-service"))]
93            panic!("Database::Service is selected without the feature storage_service");
94        }
95        Database::ScyllaDb => {
96            #[cfg(feature = "scylladb")]
97            {
98                let config = ScyllaDbDatabase::new_test_config().await?;
99                Ok(InnerStorageConfig::ScyllaDb {
100                    uri: config.inner_config.uri,
101                })
102            }
103            #[cfg(not(feature = "scylladb"))]
104            panic!("Database::ScyllaDb is selected without the feature scylladb");
105        }
106        Database::DualRocksDbScyllaDb => {
107            #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
108            {
109                let rocksdb_config = RocksDbDatabase::new_test_config().await?;
110                let scylla_config = ScyllaDbDatabase::new_test_config().await?;
111                let spawn_mode = RocksDbSpawnMode::get_spawn_mode_from_runtime();
112                Ok(InnerStorageConfig::DualRocksDbScyllaDb {
113                    path_with_guard: rocksdb_config.inner_config.path_with_guard,
114                    spawn_mode,
115                    uri: scylla_config.inner_config.uri,
116                })
117            }
118            #[cfg(not(all(feature = "rocksdb", feature = "scylladb")))]
119            panic!("Database::DualRocksDbScyllaDb is selected without the features rocksdb and scylladb");
120        }
121    }
122}
123
124/// A way to obtain the storage configuration for a local network.
125pub enum InnerStorageConfigBuilder {
126    /// Builds a fresh test configuration for the selected database engine.
127    #[cfg(with_testing)]
128    TestConfig,
129    /// Uses a storage configuration that has already been built.
130    ExistingConfig {
131        /// The pre-built storage configuration to use.
132        storage_config: InnerStorageConfig,
133    },
134}
135
136impl InnerStorageConfigBuilder {
137    /// Builds the storage configuration for the given database engine.
138    #[cfg_attr(not(with_testing), expect(unused_variables))]
139    pub async fn build(self, database: Database) -> Result<InnerStorageConfig> {
140        match self {
141            #[cfg(with_testing)]
142            InnerStorageConfigBuilder::TestConfig => make_testing_config(database).await,
143            InnerStorageConfigBuilder::ExistingConfig { storage_config } => Ok(storage_config),
144        }
145    }
146}
147
148/// Path used for the run can come from a path whose lifetime is controlled
149/// by an external user or as a temporary directory
150#[derive(Clone)]
151pub enum PathProvider {
152    /// A path whose lifetime is managed by an external caller.
153    ExternalPath {
154        /// The externally managed path.
155        path_buf: PathBuf,
156    },
157    /// A temporary directory whose lifetime is managed by this provider.
158    TemporaryDirectory {
159        /// The temporary directory, removed when the last reference is dropped.
160        tmp_dir: Arc<TempDir>,
161    },
162}
163
164impl PathProvider {
165    /// Returns the path managed by this provider.
166    pub fn path(&self) -> &Path {
167        match self {
168            PathProvider::ExternalPath { path_buf } => path_buf.as_path(),
169            PathProvider::TemporaryDirectory { tmp_dir } => tmp_dir.path(),
170        }
171    }
172
173    /// Creates a provider backed by a freshly created temporary directory.
174    pub fn create_temporary_directory() -> Result<Self> {
175        let tmp_dir = Arc::new(tempdir()?);
176        Ok(PathProvider::TemporaryDirectory { tmp_dir })
177    }
178
179    /// Creates a provider from the given path, or a temporary directory if `None`.
180    pub fn from_path_option(path: &Option<String>) -> anyhow::Result<Self> {
181        Ok(match path {
182            None => {
183                let tmp_dir = Arc::new(tempfile::tempdir()?);
184                PathProvider::TemporaryDirectory { tmp_dir }
185            }
186            Some(path) => {
187                let path = Path::new(path);
188                let path_buf = path.to_path_buf();
189                PathProvider::ExternalPath { path_buf }
190            }
191        })
192    }
193}
194
195/// The information needed to start a [`LocalNet`].
196pub struct LocalNetConfig {
197    /// The storage backend used by the validators.
198    pub database: Database,
199    /// The network protocols used for the validators' internal and external endpoints.
200    pub network: NetworkConfig,
201    /// The seed used to make key generation deterministic in tests, if any.
202    pub testing_prng_seed: Option<u64>,
203    /// The namespace used for the validators' storage.
204    pub namespace: String,
205    /// The number of additional chains to create in the genesis configuration.
206    pub num_other_initial_chains: u32,
207    /// The initial balance assigned to each chain in the genesis configuration.
208    pub initial_amount: Amount,
209    /// The number of validators to start initially.
210    pub num_initial_validators: usize,
211    /// The number of shards to run per validator.
212    pub num_shards: usize,
213    /// The number of proxies to run per validator.
214    pub num_proxies: usize,
215    /// The resource control policy applied to the network.
216    pub policy_config: ResourceControlPolicyConfig,
217    /// The list of hosts that applications are allowed to make HTTP requests to, if restricted.
218    pub http_request_allow_list: Option<Vec<String>>,
219    /// The configuration for cross-chain message queuing between validators.
220    pub cross_chain_config: CrossChainConfig,
221    /// The builder that produces the storage configuration for the network.
222    pub storage_config_builder: InnerStorageConfigBuilder,
223    /// The provider for the working directory of the network.
224    pub path_provider: PathProvider,
225    /// The setup describing how block exporters are started or connected to.
226    pub block_exporters: ExportersSetup,
227    /// Optional directory where the `linera`, `linera-proxy`, and `linera-server` binaries
228    /// are located. If `None`, binaries are resolved from the current binary's directory.
229    pub binary_dir: Option<PathBuf>,
230}
231
232/// The setup for the block exporters.
233#[derive(Clone, PartialEq)]
234pub enum ExportersSetup {
235    /// Block exporters are meant to be started and managed by the testing framework.
236    Local(Vec<BlockExporterConfig>),
237    /// Block exporters are already started and we just need to connect to them.
238    Remote(Vec<ExporterServiceConfig>),
239}
240
241impl ExportersSetup {
242    /// Creates an exporter setup, connecting to a remote exporter if requested.
243    pub fn new(
244        with_block_exporter: bool,
245        block_exporter_address: String,
246        block_exporter_port: NonZeroU16,
247    ) -> ExportersSetup {
248        if with_block_exporter {
249            let exporter_config =
250                ExporterServiceConfig::new(block_exporter_address, block_exporter_port.into());
251            ExportersSetup::Remote(vec![exporter_config])
252        } else {
253            ExportersSetup::Local(vec![])
254        }
255    }
256}
257
258/// A set of Linera validators running locally as native processes.
259pub struct LocalNet {
260    network: NetworkConfig,
261    testing_prng_seed: Option<u64>,
262    next_client_id: usize,
263    num_initial_validators: usize,
264    num_proxies: usize,
265    num_shards: usize,
266    validator_keys: BTreeMap<usize, (String, String)>,
267    running_validators: BTreeMap<usize, Validator>,
268    initialized_validator_storages: BTreeMap<usize, StorageConfig>,
269    common_namespace: String,
270    common_storage_config: InnerStorageConfig,
271    cross_chain_config: CrossChainConfig,
272    path_provider: PathProvider,
273    block_exporters: ExportersSetup,
274    binary_dir: Option<PathBuf>,
275}
276
277/// The name of the environment variable that allows specifying additional arguments to be passed
278/// to the binary when starting a server.
279const SERVER_ENV: &str = "LINERA_SERVER_PARAMS";
280
281/// Description of the database engine to use inside a local Linera network.
282#[derive(Copy, Clone, Eq, PartialEq)]
283pub enum Database {
284    /// The storage service backend.
285    Service,
286    /// The ScyllaDB backend.
287    ScyllaDb,
288    /// The dual backend combining RocksDB and ScyllaDB.
289    DualRocksDbScyllaDb,
290}
291
292/// The processes of a running validator.
293struct Validator {
294    proxies: Vec<Child>,
295    servers: Vec<Child>,
296    exporters: Vec<Child>,
297}
298
299impl Validator {
300    fn new() -> Self {
301        Self {
302            proxies: vec![],
303            servers: vec![],
304            exporters: vec![],
305        }
306    }
307
308    async fn terminate(&mut self) -> Result<()> {
309        for proxy in &mut self.proxies {
310            proxy.kill().await.context("terminating validator proxy")?;
311        }
312        for server in &mut self.servers {
313            server
314                .kill()
315                .await
316                .context("terminating validator server")?;
317        }
318        Ok(())
319    }
320
321    fn add_proxy(&mut self, proxy: Child) {
322        self.proxies.push(proxy)
323    }
324
325    fn add_server(&mut self, server: Child) {
326        self.servers.push(server)
327    }
328
329    #[cfg(with_testing)]
330    async fn terminate_server(&mut self, index: usize) -> Result<()> {
331        let mut server = self.servers.remove(index);
332        server
333            .kill()
334            .await
335            .context("terminating validator server")?;
336        Ok(())
337    }
338
339    fn add_block_exporter(&mut self, exporter: Child) {
340        self.exporters.push(exporter);
341    }
342
343    fn ensure_is_running(&mut self) -> Result<()> {
344        for proxy in &mut self.proxies {
345            proxy.ensure_is_running()?;
346        }
347        for child in &mut self.servers {
348            child.ensure_is_running()?;
349        }
350        for exporter in &mut self.exporters {
351            exporter.ensure_is_running()?;
352        }
353        Ok(())
354    }
355}
356
357#[cfg(with_testing)]
358impl LocalNetConfig {
359    /// Creates a configuration for a local test network with default test parameters.
360    pub fn new_test(database: Database, network: Network) -> Self {
361        let num_shards = 4;
362        let num_proxies = 1;
363        let storage_config_builder = InnerStorageConfigBuilder::TestConfig;
364        let path_provider = PathProvider::create_temporary_directory().unwrap();
365        let internal = network.drop_tls();
366        let external = network;
367        let network = NetworkConfig { internal, external };
368        let cross_chain_config = CrossChainConfig::default();
369        Self {
370            database,
371            network,
372            num_other_initial_chains: 2,
373            initial_amount: Amount::from_tokens(1_000_000),
374            policy_config: ResourceControlPolicyConfig::Testnet,
375            cross_chain_config,
376            testing_prng_seed: Some(37),
377            namespace: linera_views::random::generate_test_namespace(),
378            num_initial_validators: 4,
379            num_shards,
380            num_proxies,
381            storage_config_builder,
382            path_provider,
383            block_exporters: ExportersSetup::Local(vec![]),
384            http_request_allow_list: Some(vec!["localhost".to_string()]),
385            binary_dir: None,
386        }
387    }
388}
389
390#[async_trait]
391impl LineraNetConfig for LocalNetConfig {
392    type Net = LocalNet;
393
394    async fn instantiate(self) -> Result<(Self::Net, ClientWrapper)> {
395        let storage_config = self.storage_config_builder.build(self.database).await?;
396        let mut net = LocalNet::new(
397            self.network,
398            self.testing_prng_seed,
399            self.namespace,
400            self.num_initial_validators,
401            self.num_proxies,
402            self.num_shards,
403            storage_config,
404            self.cross_chain_config,
405            self.path_provider,
406            self.block_exporters,
407            self.binary_dir,
408        );
409        let client = net.make_client().await;
410        ensure!(
411            self.num_initial_validators > 0,
412            "There should be at least one initial validator"
413        );
414        let total_number_shards = self.num_initial_validators * self.num_shards;
415        ensure!(
416            total_number_shards <= MAX_NUMBER_SHARDS,
417            "Total number of shards ({}) exceeds maximum allowed ({})",
418            self.num_shards,
419            MAX_NUMBER_SHARDS
420        );
421        net.generate_initial_validator_config().await?;
422        client
423            .create_genesis_config(
424                self.num_other_initial_chains,
425                self.initial_amount,
426                self.policy_config,
427                self.http_request_allow_list
428                    .clone()
429                    .or_else(|| Some(vec!["localhost".to_owned()])),
430            )
431            .await?;
432        net.run().await?;
433        Ok((net, client))
434    }
435}
436
437#[async_trait]
438impl LineraNet for LocalNet {
439    async fn ensure_is_running(&mut self) -> Result<()> {
440        for validator in self.running_validators.values_mut() {
441            validator.ensure_is_running().context("in local network")?;
442        }
443        Ok(())
444    }
445
446    async fn make_client(&mut self) -> ClientWrapper {
447        let client = ClientWrapper::new_with_extra_args(
448            self.path_provider.clone(),
449            self.network.external,
450            self.testing_prng_seed,
451            self.next_client_id,
452            OnClientDrop::LeakChains,
453            vec!["--wait-for-outgoing-messages".to_string()],
454            self.binary_dir.clone(),
455        );
456        if let Some(seed) = self.testing_prng_seed {
457            self.testing_prng_seed = Some(seed + 1);
458        }
459        self.next_client_id += 1;
460        client
461    }
462
463    async fn terminate(&mut self) -> Result<()> {
464        for validator in self.running_validators.values_mut() {
465            validator.terminate().await.context("in local network")?
466        }
467        Ok(())
468    }
469}
470
471impl LocalNet {
472    #[expect(clippy::too_many_arguments)]
473    fn new(
474        network: NetworkConfig,
475        testing_prng_seed: Option<u64>,
476        common_namespace: String,
477        num_initial_validators: usize,
478        num_proxies: usize,
479        num_shards: usize,
480        common_storage_config: InnerStorageConfig,
481        cross_chain_config: CrossChainConfig,
482        path_provider: PathProvider,
483        block_exporters: ExportersSetup,
484        binary_dir: Option<PathBuf>,
485    ) -> Self {
486        Self {
487            network,
488            testing_prng_seed,
489            next_client_id: 0,
490            num_initial_validators,
491            num_proxies,
492            num_shards,
493            validator_keys: BTreeMap::new(),
494            running_validators: BTreeMap::new(),
495            initialized_validator_storages: BTreeMap::new(),
496            common_namespace,
497            common_storage_config,
498            cross_chain_config,
499            path_provider,
500            block_exporters,
501            binary_dir,
502        }
503    }
504
505    async fn command_for_binary(&self, name: &'static str) -> Result<Command> {
506        let path = if let Some(dir) = &self.binary_dir {
507            dir.join(name)
508        } else {
509            resolve_binary(name, env!("CARGO_PKG_NAME")).await?
510        };
511        let mut command = Command::new(path);
512        command.current_dir(self.path_provider.path());
513        Ok(command)
514    }
515
516    #[cfg(with_testing)]
517    /// Reads the genesis configuration of the local network.
518    pub fn genesis_config(&self) -> Result<linera_client::config::GenesisConfig> {
519        let path = self.path_provider.path();
520        crate::util::read_json(path.join("genesis.json"))
521    }
522
523    fn shard_port(&self, validator: usize, shard: usize) -> usize {
524        test_offset_port() + validator * self.num_shards + shard + 1
525    }
526
527    fn proxy_internal_port(&self, validator: usize, proxy_id: usize) -> usize {
528        test_offset_port() + 1000 + validator * self.num_proxies + proxy_id + 1
529    }
530
531    fn shard_metrics_port(&self, validator: usize, shard: usize) -> usize {
532        test_offset_port() + 2000 + validator * self.num_shards + shard + 1
533    }
534
535    fn proxy_metrics_port(&self, validator: usize, proxy_id: usize) -> usize {
536        test_offset_port() + 3000 + validator * self.num_proxies + proxy_id + 1
537    }
538
539    fn block_exporter_port(&self, validator: usize, exporter_id: usize) -> usize {
540        test_offset_port() + 3000 + validator * self.num_shards + exporter_id + 1
541    }
542
543    /// Returns the public port of the given proxy of the given validator.
544    pub fn proxy_public_port(&self, validator: usize, proxy_id: usize) -> usize {
545        test_offset_port() + 4000 + validator * self.num_proxies + proxy_id + 1
546    }
547
548    /// Returns the public port of the first proxy of the first validator.
549    pub fn first_public_port() -> usize {
550        test_offset_port() + 4000 + 1
551    }
552
553    fn block_exporter_metrics_port(exporter_id: usize) -> usize {
554        test_offset_port() + 4000 + exporter_id + 1
555    }
556
557    fn configuration_string(&self, server_number: usize) -> Result<String> {
558        let n = server_number;
559        let path = self
560            .path_provider
561            .path()
562            .join(format!("validator_{n}.toml"));
563        let port = self.proxy_public_port(n, 0);
564        let external_protocol = self.network.external.toml();
565        let internal_protocol = self.network.internal.toml();
566        let external_host = self.network.external.localhost();
567        let internal_host = self.network.internal.localhost();
568        let mut content = format!(
569            r#"
570                server_config_path = "server_{n}.json"
571                host = "{external_host}"
572                port = {port}
573                external_protocol = {external_protocol}
574                internal_protocol = {internal_protocol}
575            "#
576        );
577
578        for k in 0..self.num_proxies {
579            let public_port = self.proxy_public_port(n, k);
580            let internal_port = self.proxy_internal_port(n, k);
581            let metrics_port = self.proxy_metrics_port(n, k);
582            // In the local network, the validator ingress is
583            // the proxy - so the `public_port` is the validator
584            // port.
585            content.push_str(&format!(
586                r#"
587                [[proxies]]
588                host = "{internal_host}"
589                public_port = {public_port}
590                private_port = {internal_port}
591                metrics_port = {metrics_port}
592                "#
593            ));
594        }
595
596        for k in 0..self.num_shards {
597            let shard_port = self.shard_port(n, k);
598            let shard_metrics_port = self.shard_metrics_port(n, k);
599            content.push_str(&format!(
600                r#"
601
602                [[shards]]
603                host = "{internal_host}"
604                port = {shard_port}
605                metrics_port = {shard_metrics_port}
606                "#
607            ));
608        }
609
610        match self.block_exporters {
611            ExportersSetup::Local(ref exporters) => {
612                for (j, exporter) in exporters.iter().enumerate() {
613                    let host = Network::Grpc.localhost();
614                    let port = self.block_exporter_port(n, j);
615                    let config_content = format!(
616                        r#"
617
618                        [[block_exporters]]
619                        host = "{host}"
620                        port = {port}
621                        "#
622                    );
623
624                    content.push_str(&config_content);
625                    #[expect(
626                        clippy::cast_possible_truncation,
627                        reason = "loop index over a local config list bounded well below u32::MAX"
628                    )]
629                    let exporter_index = j as u32;
630                    let exporter_config = self.generate_block_exporter_config(
631                        n,
632                        exporter_index,
633                        &exporter.destination_config,
634                    );
635                    let config_path = self
636                        .path_provider
637                        .path()
638                        .join(format!("exporter_config_{n}:{j}.toml"));
639
640                    fs_err::write(&config_path, &exporter_config)?;
641                }
642            }
643            ExportersSetup::Remote(ref exporters) => {
644                for exporter in exporters {
645                    let host = exporter.host.clone();
646                    let port = exporter.port;
647                    let config_content = format!(
648                        r#"
649
650                        [[block_exporters]]
651                        host = "{host}"
652                        port = {port}
653                        "#
654                    );
655
656                    content.push_str(&config_content);
657                }
658            }
659        }
660
661        fs_err::write(&path, content)?;
662        path.into_os_string().into_string().map_err(|error| {
663            anyhow!(
664                "could not parse OS string into string: {}",
665                error.to_string_lossy()
666            )
667        })
668    }
669
670    fn generate_block_exporter_config(
671        &self,
672        validator: usize,
673        exporter_id: u32,
674        destination_config: &DestinationConfig,
675    ) -> String {
676        let n = validator;
677        let host = Network::Grpc.localhost();
678        let port = self.block_exporter_port(n, exporter_id as usize);
679        let metrics_port = Self::block_exporter_metrics_port(exporter_id as usize);
680        let mut config = format!(
681            r#"
682            id = {exporter_id}
683
684            metrics_port = {metrics_port}
685
686            [service_config]
687            host = "{host}"
688            port = {port}
689
690            "#
691        );
692
693        let DestinationConfig {
694            destinations,
695            committee_destination,
696        } = destination_config;
697
698        if *committee_destination {
699            let destination_string_to_push = r#"
700
701            [destination_config]
702            committee_destination = true
703            "#
704            .to_string();
705
706            config.push_str(&destination_string_to_push);
707        }
708
709        for destination in destinations {
710            let destination_string_to_push = match destination {
711                Destination::Indexer {
712                    tls,
713                    endpoint,
714                    port,
715                } => {
716                    let tls = match tls {
717                        TlsConfig::ClearText => "ClearText",
718                        TlsConfig::Tls => "Tls",
719                    };
720                    format!(
721                        r#"
722                        [[destination_config.destinations]]
723                        tls = "{tls}"
724                        endpoint = "{endpoint}"
725                        port = {port}
726                        kind = "Indexer"
727                        "#
728                    )
729                }
730                Destination::Validator { endpoint, port } => {
731                    format!(
732                        r#"
733                        [[destination_config.destinations]]
734                        endpoint = "{endpoint}"
735                        port = {port}
736                        kind = "Validator"
737                        "#
738                    )
739                }
740                Destination::Logging { file_name } => {
741                    format!(
742                        r#"
743                        [[destination_config.destinations]]
744                        file_name = "{file_name}"
745                        kind = "Logging"
746                        "#
747                    )
748                }
749            };
750
751            config.push_str(&destination_string_to_push);
752        }
753
754        config
755    }
756
757    async fn generate_initial_validator_config(&mut self) -> Result<()> {
758        let mut command = self.command_for_binary("linera-server").await?;
759        command.arg("generate");
760        if let Some(seed) = self.testing_prng_seed {
761            command.arg("--testing-prng-seed").arg(seed.to_string());
762            self.testing_prng_seed = Some(seed + 1);
763        }
764        command.arg("--validators");
765        for i in 0..self.num_initial_validators {
766            command.arg(&self.configuration_string(i)?);
767        }
768        let output = command
769            .args(["--committee", "committee.json"])
770            .spawn_and_wait_for_stdout()
771            .await?;
772        self.validator_keys = output
773            .split_whitespace()
774            .map(str::to_string)
775            .map(|keys| keys.split(',').map(str::to_string).collect::<Vec<_>>())
776            .enumerate()
777            .map(|(i, keys)| {
778                let validator_key = keys[0].to_string();
779                let account_key = keys[1].to_string();
780                (i, (validator_key, account_key))
781            })
782            .collect();
783        Ok(())
784    }
785
786    async fn run_proxy(&self, validator: usize, proxy_id: usize) -> Result<Child> {
787        let storage = self
788            .initialized_validator_storages
789            .get(&validator)
790            .expect("initialized storage");
791        let child = self
792            .command_for_binary("linera-proxy")
793            .await?
794            .arg(format!("server_{validator}.json"))
795            .args(["--storage", &storage.to_string()])
796            .args(["--id", &proxy_id.to_string()])
797            .spawn_into()?;
798
799        let port = self.proxy_public_port(validator, proxy_id);
800        let nickname = format!("validator proxy {validator}");
801        match self.network.external {
802            Network::Grpc => {
803                Self::ensure_grpc_server_has_started(&nickname, port, "http").await?;
804                let nickname = format!("validator proxy {validator}");
805                Self::ensure_grpc_server_has_started(&nickname, port, "http").await?;
806            }
807            Network::Grpcs => {
808                let nickname = format!("validator proxy {validator}");
809                Self::ensure_grpc_server_has_started(&nickname, port, "https").await?;
810            }
811            Network::Tcp => {
812                Self::ensure_simple_server_has_started(&nickname, port, "tcp").await?;
813            }
814            Network::Udp => {
815                Self::ensure_simple_server_has_started(&nickname, port, "udp").await?;
816            }
817        }
818        Ok(child)
819    }
820
821    async fn run_exporter(&self, validator: usize, exporter_id: u32) -> Result<Child> {
822        let config_path = format!("exporter_config_{validator}:{exporter_id}.toml");
823        let storage = self
824            .initialized_validator_storages
825            .get(&validator)
826            .expect("initialized storage");
827
828        tracing::debug!(config=?config_path, storage=?storage.to_string(), "starting block exporter");
829
830        let child = self
831            .command_for_binary("linera-exporter")
832            .await?
833            .args(["run", "--config-path", &config_path])
834            .args(["--storage", &storage.to_string()])
835            .spawn_into()?;
836
837        match self.network.internal {
838            Network::Grpc => {
839                let port = self.block_exporter_port(validator, exporter_id as usize);
840                let nickname = format!("block exporter {validator}:{exporter_id}");
841                Self::ensure_grpc_server_has_started(&nickname, port, "http").await?;
842            }
843            Network::Grpcs => {
844                let port = self.block_exporter_port(validator, exporter_id as usize);
845                let nickname = format!("block exporter  {validator}:{exporter_id}");
846                Self::ensure_grpc_server_has_started(&nickname, port, "https").await?;
847            }
848            Network::Tcp | Network::Udp => {
849                unreachable!("Only allowed options are grpc and grpcs")
850            }
851        }
852
853        tracing::info!("block exporter started {validator}:{exporter_id}");
854
855        Ok(child)
856    }
857
858    /// Waits until the gRPC server at the given port responds as healthy.
859    pub async fn ensure_grpc_server_has_started(
860        nickname: &str,
861        port: usize,
862        scheme: &str,
863    ) -> Result<()> {
864        let endpoint = match scheme {
865            "http" => Endpoint::new(format!("http://localhost:{port}"))
866                .context("endpoint should always parse")?,
867            "https" => {
868                use linera_rpc::CERT_PEM;
869                let certificate = tonic::transport::Certificate::from_pem(CERT_PEM);
870                let tls_config = ClientTlsConfig::new().ca_certificate(certificate);
871                Endpoint::new(format!("https://localhost:{port}"))
872                    .context("endpoint should always parse")?
873                    .tls_config(tls_config)?
874            }
875            _ => bail!("Only supported scheme are http and https"),
876        };
877        let connection = endpoint.connect_lazy();
878        let mut client = HealthClient::new(connection);
879        linera_base::time::timer::sleep(Duration::from_millis(100)).await;
880        for i in 0..10 {
881            linera_base::time::timer::sleep(Duration::from_millis(i * 500)).await;
882            let result = client.check(HealthCheckRequest::default()).await;
883            if result.is_ok() && result.unwrap().get_ref().status() == ServingStatus::Serving {
884                info!(?port, "Successfully started {nickname}");
885                return Ok(());
886            } else {
887                warn!("Waiting for {nickname} to start");
888            }
889        }
890        bail!("Failed to start {nickname}");
891    }
892
893    async fn ensure_simple_server_has_started(
894        nickname: &str,
895        port: usize,
896        protocol: &str,
897    ) -> Result<()> {
898        use linera_core::node::ValidatorNode as _;
899
900        let options = linera_rpc::NodeOptions {
901            send_timeout: Duration::from_secs(5),
902            recv_timeout: Duration::from_secs(5),
903            retry_delay: Duration::from_secs(1),
904            max_retries: 1,
905            ..Default::default()
906        };
907        let provider = linera_rpc::simple::SimpleNodeProvider::new(options);
908        let address = format!("{protocol}:127.0.0.1:{port}");
909        // All "simple" services (i.e. proxy and "server") are based on `RpcMessage` and
910        // support `VersionInfoQuery`.
911        let node = provider.make_node(&address)?;
912        linera_base::time::timer::sleep(Duration::from_millis(100)).await;
913        for i in 0..10 {
914            linera_base::time::timer::sleep(Duration::from_millis(i * 500)).await;
915            let result = node.get_version_info().await;
916            if result.is_ok() {
917                info!("Successfully started {nickname}");
918                return Ok(());
919            } else {
920                warn!("Waiting for {nickname} to start");
921            }
922        }
923        bail!("Failed to start {nickname}");
924    }
925
926    async fn initialize_storage(&mut self, validator: usize) -> Result<()> {
927        let namespace = format!("{}_server_{}_db", self.common_namespace, validator);
928        let inner_storage_config = self.common_storage_config.clone();
929        let storage = StorageConfig {
930            inner_storage_config,
931            namespace,
932        };
933        let mut command = self.command_for_binary("linera").await?;
934        if let Ok(var) = env::var(SERVER_ENV) {
935            command.args(var.split_whitespace());
936        }
937        command.args(["storage", "initialize"]);
938        command
939            .args(["--storage", &storage.to_string()])
940            .args(["--genesis", "genesis.json"])
941            .spawn_and_wait_for_stdout()
942            .await?;
943
944        self.initialized_validator_storages
945            .insert(validator, storage);
946        Ok(())
947    }
948
949    async fn run_server(&self, validator: usize, shard: usize) -> Result<Child> {
950        let mut storage = self
951            .initialized_validator_storages
952            .get(&validator)
953            .expect("initialized storage")
954            .clone();
955
956        // For the storage backends with a local directory, make sure that we don't reuse
957        // the same directory for all the shards.
958        storage.maybe_append_shard_path(shard)?;
959
960        let mut command = self.command_for_binary("linera-server").await?;
961        if let Ok(var) = env::var(SERVER_ENV) {
962            command.args(var.split_whitespace());
963        }
964        command
965            .arg("run")
966            .args(["--storage", &storage.to_string()])
967            .args(["--server", &format!("server_{validator}.json")])
968            .args(["--shard", &shard.to_string()])
969            .args(self.cross_chain_config.to_args());
970        let child = command.spawn_into()?;
971
972        let port = self.shard_port(validator, shard);
973        let nickname = format!("validator server {validator}:{shard}");
974        match self.network.internal {
975            Network::Grpc => {
976                Self::ensure_grpc_server_has_started(&nickname, port, "http").await?;
977            }
978            Network::Grpcs => {
979                Self::ensure_grpc_server_has_started(&nickname, port, "https").await?;
980            }
981            Network::Tcp => {
982                Self::ensure_simple_server_has_started(&nickname, port, "tcp").await?;
983            }
984            Network::Udp => {
985                Self::ensure_simple_server_has_started(&nickname, port, "udp").await?;
986            }
987        }
988        Ok(child)
989    }
990
991    async fn run(&mut self) -> Result<()> {
992        for validator in 0..self.num_initial_validators {
993            self.start_validator(validator).await?;
994        }
995        Ok(())
996    }
997
998    /// Start a validator.
999    pub async fn start_validator(&mut self, index: usize) -> Result<()> {
1000        self.initialize_storage(index).await?;
1001        self.restart_validator(index).await
1002    }
1003
1004    /// Restart a validator. This is similar to `start_validator` except that the
1005    /// database was already initialized once.
1006    pub async fn restart_validator(&mut self, index: usize) -> Result<()> {
1007        let mut validator = Validator::new();
1008        for k in 0..self.num_proxies {
1009            let proxy = self.run_proxy(index, k).await?;
1010            validator.add_proxy(proxy);
1011        }
1012        for shard in 0..self.num_shards {
1013            let server = self.run_server(index, shard).await?;
1014            validator.add_server(server);
1015        }
1016        if let ExportersSetup::Local(ref exporters) = self.block_exporters {
1017            for block_exporter in 0..exporters.len() {
1018                #[expect(
1019                    clippy::cast_possible_truncation,
1020                    reason = "loop index over a local exporter list bounded well below u32::MAX"
1021                )]
1022                let exporter_id = block_exporter as u32;
1023                let exporter = self.run_exporter(index, exporter_id).await?;
1024                validator.add_block_exporter(exporter);
1025            }
1026        }
1027
1028        self.running_validators.insert(index, validator);
1029        Ok(())
1030    }
1031
1032    /// Terminates all the processes of a given validator.
1033    pub async fn stop_validator(&mut self, index: usize) -> Result<()> {
1034        if let Some(mut validator) = self.running_validators.remove(&index) {
1035            if let Err(error) = validator.terminate().await {
1036                error!("Failed to stop validator {index}: {error}");
1037                return Err(error);
1038            }
1039        }
1040        Ok(())
1041    }
1042
1043    /// Returns a [`linera_rpc::Client`] to interact directly with a `validator`.
1044    pub fn validator_client(&mut self, validator: usize) -> Result<linera_rpc::Client> {
1045        let node_provider = linera_rpc::NodeProvider::new(linera_rpc::NodeOptions {
1046            send_timeout: Duration::from_secs(1),
1047            recv_timeout: Duration::from_secs(1),
1048            retry_delay: Duration::ZERO,
1049            max_retries: 0,
1050            ..Default::default()
1051        });
1052
1053        Ok(node_provider.make_node(&self.validator_address(validator))?)
1054    }
1055
1056    /// Returns the address to connect to a validator's proxy.
1057    /// In local networks, the zeroth proxy _is_ the validator ingress.
1058    pub fn validator_address(&self, validator: usize) -> String {
1059        let port = self.proxy_public_port(validator, 0);
1060        let schema = self.network.external.schema();
1061
1062        format!("{schema}:localhost:{port}")
1063    }
1064}
1065
1066#[cfg(with_testing)]
1067impl LocalNet {
1068    /// Returns the validating key and an account key of the validator.
1069    pub fn validator_keys(&self, validator: usize) -> Option<&(String, String)> {
1070        self.validator_keys.get(&validator)
1071    }
1072
1073    /// Generates the configuration and keys for the given validator.
1074    pub async fn generate_validator_config(&mut self, validator: usize) -> Result<()> {
1075        let stdout = self
1076            .command_for_binary("linera-server")
1077            .await?
1078            .arg("generate")
1079            .arg("--validators")
1080            .arg(&self.configuration_string(validator)?)
1081            .spawn_and_wait_for_stdout()
1082            .await?;
1083        let keys = stdout
1084            .trim()
1085            .split(',')
1086            .map(str::to_string)
1087            .collect::<Vec<_>>();
1088        self.validator_keys
1089            .insert(validator, (keys[0].clone(), keys[1].clone()));
1090        Ok(())
1091    }
1092
1093    /// Terminates the server for the given shard of the given validator.
1094    pub async fn terminate_server(&mut self, validator: usize, shard: usize) -> Result<()> {
1095        self.running_validators
1096            .get_mut(&validator)
1097            .context("server not found")?
1098            .terminate_server(shard)
1099            .await?;
1100        Ok(())
1101    }
1102
1103    /// Removes the given validator from the set of running validators.
1104    pub fn remove_validator(&mut self, validator: usize) -> Result<()> {
1105        self.running_validators
1106            .remove(&validator)
1107            .context("validator not found")?;
1108        Ok(())
1109    }
1110
1111    /// Starts the server for the given shard of the given validator.
1112    pub async fn start_server(&mut self, validator: usize, shard: usize) -> Result<()> {
1113        let server = self.run_server(validator, shard).await?;
1114        self.running_validators
1115            .get_mut(&validator)
1116            .context("could not find validator")?
1117            .add_server(server);
1118        Ok(())
1119    }
1120}