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 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
49const MAX_NUMBER_SHARDS: usize = 1000;
51
52pub enum ProcessInbox {
54 Skip,
56 Automatic,
58}
59
60#[cfg(with_testing)]
61static PORT_PROVIDER: LazyLock<RwLock<u16>> = LazyLock::new(|| RwLock::new(7080));
62
63fn 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#[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
124pub enum InnerStorageConfigBuilder {
126 #[cfg(with_testing)]
128 TestConfig,
129 ExistingConfig {
131 storage_config: InnerStorageConfig,
133 },
134}
135
136impl InnerStorageConfigBuilder {
137 #[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#[derive(Clone)]
151pub enum PathProvider {
152 ExternalPath {
154 path_buf: PathBuf,
156 },
157 TemporaryDirectory {
159 tmp_dir: Arc<TempDir>,
161 },
162}
163
164impl PathProvider {
165 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 pub fn create_temporary_directory() -> Result<Self> {
175 let tmp_dir = Arc::new(tempdir()?);
176 Ok(PathProvider::TemporaryDirectory { tmp_dir })
177 }
178
179 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
195pub struct LocalNetConfig {
197 pub database: Database,
199 pub network: NetworkConfig,
201 pub testing_prng_seed: Option<u64>,
203 pub namespace: String,
205 pub num_other_initial_chains: u32,
207 pub initial_amount: Amount,
209 pub num_initial_validators: usize,
211 pub num_shards: usize,
213 pub num_proxies: usize,
215 pub policy_config: ResourceControlPolicyConfig,
217 pub http_request_allow_list: Option<Vec<String>>,
219 pub cross_chain_config: CrossChainConfig,
221 pub storage_config_builder: InnerStorageConfigBuilder,
223 pub path_provider: PathProvider,
225 pub block_exporters: ExportersSetup,
227 pub binary_dir: Option<PathBuf>,
230}
231
232#[derive(Clone, PartialEq)]
234pub enum ExportersSetup {
235 Local(Vec<BlockExporterConfig>),
237 Remote(Vec<ExporterServiceConfig>),
239}
240
241impl ExportersSetup {
242 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
258pub 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
277const SERVER_ENV: &str = "LINERA_SERVER_PARAMS";
280
281#[derive(Copy, Clone, Eq, PartialEq)]
283pub enum Database {
284 Service,
286 ScyllaDb,
288 DualRocksDbScyllaDb,
290}
291
292struct 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn validator_keys(&self, validator: usize) -> Option<&(String, String)> {
1070 self.validator_keys.get(&validator)
1071 }
1072
1073 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 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 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 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}