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