1#[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
51const MAX_NUMBER_SHARDS: usize = 1000;
53
54pub enum ProcessInbox {
56 Skip,
58 Automatic,
60}
61
62#[cfg(with_testing)]
63static PORT_PROVIDER: LazyLock<RwLock<u16>> = LazyLock::new(|| RwLock::new(7080));
64
65fn 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#[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
126pub enum InnerStorageConfigBuilder {
128 #[cfg(with_testing)]
130 TestConfig,
131 ExistingConfig {
133 storage_config: InnerStorageConfig,
135 },
136}
137
138impl InnerStorageConfigBuilder {
139 #[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#[derive(Clone)]
153pub enum PathProvider {
154 ExternalPath {
156 path_buf: PathBuf,
158 },
159 TemporaryDirectory {
161 tmp_dir: Arc<TempDir>,
163 },
164}
165
166impl PathProvider {
167 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 pub fn create_temporary_directory() -> Result<Self> {
177 let tmp_dir = Arc::new(tempdir()?);
178 Ok(PathProvider::TemporaryDirectory { tmp_dir })
179 }
180
181 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
197pub struct LocalNetConfig {
199 pub database: Database,
201 pub network: NetworkConfig,
203 pub testing_prng_seed: Option<u64>,
205 pub namespace: String,
207 pub num_other_initial_chains: u32,
209 pub initial_amount: Amount,
211 pub num_initial_validators: usize,
213 pub num_shards: usize,
215 pub num_proxies: usize,
217 pub policy_config: ResourceControlPolicyConfig,
219 pub http_request_allow_list: Option<Vec<String>>,
221 pub cross_chain_config: CrossChainConfig,
223 pub storage_config_builder: InnerStorageConfigBuilder,
225 pub path_provider: PathProvider,
227 pub block_exporters: ExportersSetup,
229 pub binary_dir: Option<PathBuf>,
232 pub export_blocks_to_committee: bool,
235 pub block_export_transport: BlockExportTransport,
238}
239
240#[derive(Clone, PartialEq)]
242pub enum ExportersSetup {
243 Local(Vec<BlockExporterConfig>),
245 Remote(Vec<ExporterServiceConfig>),
247}
248
249impl ExportersSetup {
250 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
266pub 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
287const SERVER_ENV: &str = "LINERA_SERVER_PARAMS";
290
291#[derive(Copy, Clone, Eq, PartialEq)]
293pub enum Database {
294 Service,
296 ScyllaDb,
298 DualRocksDbScyllaDb,
300}
301
302struct 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn validator_keys(&self, validator: usize) -> Option<&(String, String)> {
1125 self.validator_keys.get(&validator)
1126 }
1127
1128 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 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 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 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}