linera_service/cli/
net_up_utils.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{num::NonZeroU16, str::FromStr};
5
6use colored::Colorize as _;
7use linera_base::{data_types::Amount, listen_for_shutdown_signals, time::Duration};
8use linera_client::client_options::ResourceControlPolicyConfig;
9use linera_rpc::config::{CrossChainConfig, ExporterServiceConfig};
10#[cfg(feature = "storage-service")]
11use linera_storage_service::{
12    child::{StorageService, StorageServiceGuard},
13    common::get_service_storage_binary,
14};
15use tokio_util::sync::CancellationToken;
16use tracing::info;
17#[cfg(feature = "kubernetes")]
18use {
19    crate::cli_wrappers::local_kubernetes_net::{BuildMode, LocalKubernetesNetConfig},
20    std::path::PathBuf,
21};
22
23use crate::{
24    cli_wrappers::{
25        local_net::{
26            Database, ExportersSetup, InnerStorageConfigBuilder, LocalNetConfig, PathProvider,
27        },
28        ClientWrapper, FaucetService, LineraNet, LineraNetConfig, Network, NetworkConfig,
29    },
30    storage::{InnerStorageConfig, StorageConfig},
31};
32
33struct StorageConfigProvider {
34    /// The storage config.
35    config: StorageConfig,
36    #[cfg(feature = "storage-service")]
37    _service_guard: Option<StorageServiceGuard>,
38}
39
40impl StorageConfigProvider {
41    pub async fn new(storage: &Option<String>) -> anyhow::Result<StorageConfigProvider> {
42        match storage {
43            #[cfg(feature = "storage-service")]
44            None => {
45                let service_endpoint = linera_base::port::get_free_endpoint().await?;
46                let binary = get_service_storage_binary().await?.display().to_string();
47                let service = StorageService::new(&service_endpoint, binary);
48                let _service_guard = service.run().await?;
49                let _service_guard = Some(_service_guard);
50                let inner_storage_config = InnerStorageConfig::Service {
51                    endpoint: service_endpoint,
52                };
53                let namespace = "table_default".to_string();
54                let config = StorageConfig {
55                    inner_storage_config,
56                    namespace,
57                };
58                Ok(StorageConfigProvider {
59                    config,
60                    _service_guard,
61                })
62            }
63            #[cfg(not(feature = "storage-service"))]
64            None => {
65                panic!("When storage is not selected, the storage-service needs to be enabled");
66            }
67            #[cfg(feature = "storage-service")]
68            Some(storage) => {
69                let config = StorageConfig::from_str(storage)?;
70                Ok(StorageConfigProvider {
71                    config,
72                    _service_guard: None,
73                })
74            }
75            #[cfg(not(feature = "storage-service"))]
76            Some(storage) => {
77                let config = StorageConfig::from_str(storage)?;
78                Ok(StorageConfigProvider { config })
79            }
80        }
81    }
82
83    pub fn inner_storage_config(&self) -> &InnerStorageConfig {
84        &self.config.inner_storage_config
85    }
86
87    pub fn namespace(&self) -> &str {
88        &self.config.namespace
89    }
90
91    pub fn database(&self) -> anyhow::Result<Database> {
92        match self.config.inner_storage_config {
93            InnerStorageConfig::Memory { .. } => anyhow::bail!("Not possible to work with memory"),
94            #[cfg(feature = "rocksdb")]
95            InnerStorageConfig::RocksDb { .. } => {
96                anyhow::bail!("Not possible to work with RocksDB")
97            }
98            #[cfg(feature = "storage-service")]
99            InnerStorageConfig::Service { .. } => Ok(Database::Service),
100            #[cfg(feature = "dynamodb")]
101            InnerStorageConfig::DynamoDb { .. } => Ok(Database::DynamoDb),
102            #[cfg(feature = "scylladb")]
103            InnerStorageConfig::ScyllaDb { .. } => Ok(Database::ScyllaDb),
104            #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
105            InnerStorageConfig::DualRocksDbScyllaDb { .. } => Ok(Database::DualRocksDbScyllaDb),
106        }
107    }
108}
109
110#[expect(clippy::too_many_arguments)]
111#[cfg(feature = "kubernetes")]
112pub async fn handle_net_up_kubernetes(
113    num_other_initial_chains: u32,
114    initial_amount: u128,
115    num_initial_validators: usize,
116    num_shards: usize,
117    testing_prng_seed: Option<u64>,
118    binaries: &Option<Option<PathBuf>>,
119    no_build: bool,
120    docker_image_name: String,
121    build_mode: BuildMode,
122    policy_config: ResourceControlPolicyConfig,
123    with_faucet: bool,
124    faucet_chain: Option<u32>,
125    faucet_port: NonZeroU16,
126    faucet_amount: Amount,
127    dual_store: bool,
128) -> anyhow::Result<()> {
129    if num_initial_validators < 1 {
130        panic!("The local test network must have at least one validator.");
131    }
132    if num_shards < 1 {
133        panic!("The local test network must have at least one shard per validator.");
134    }
135    if faucet_chain.is_some() {
136        assert!(
137            with_faucet,
138            "--faucet-chain must be provided only with --with-faucet"
139        );
140    }
141
142    let shutdown_notifier = CancellationToken::new();
143    tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
144
145    let config = LocalKubernetesNetConfig {
146        network: Network::Grpc,
147        testing_prng_seed,
148        num_other_initial_chains,
149        initial_amount: Amount::from_tokens(initial_amount),
150        num_initial_validators,
151        num_shards,
152        binaries: binaries.clone().into(),
153        no_build,
154        docker_image_name,
155        build_mode,
156        policy_config,
157        dual_store,
158    };
159    let (mut net, client) = config.instantiate().await?;
160    let faucet_service = print_messages_and_create_faucet(
161        client,
162        with_faucet,
163        faucet_chain,
164        faucet_port,
165        faucet_amount,
166        num_other_initial_chains,
167    )
168    .await?;
169    wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
170}
171
172#[expect(clippy::too_many_arguments)]
173pub async fn handle_net_up_service(
174    num_other_initial_chains: u32,
175    initial_amount: u128,
176    num_initial_validators: usize,
177    num_shards: usize,
178    testing_prng_seed: Option<u64>,
179    policy_config: ResourceControlPolicyConfig,
180    cross_chain_config: CrossChainConfig,
181    with_block_exporter: bool,
182    block_exporter_port: NonZeroU16,
183    path: &Option<String>,
184    storage: &Option<String>,
185    external_protocol: String,
186    with_faucet: bool,
187    faucet_chain: Option<u32>,
188    faucet_port: NonZeroU16,
189    faucet_amount: Amount,
190) -> anyhow::Result<()> {
191    if num_initial_validators < 1 {
192        panic!("The local test network must have at least one validator.");
193    }
194    if num_shards < 1 {
195        panic!("The local test network must have at least one shard per validator.");
196    }
197
198    let shutdown_notifier = CancellationToken::new();
199    tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
200
201    let storage = StorageConfigProvider::new(storage).await?;
202    let storage_config = storage.inner_storage_config().clone();
203    let namespace = storage.namespace().to_string();
204    let database = storage.database()?;
205    let storage_config_builder = InnerStorageConfigBuilder::ExistingConfig { storage_config };
206    let external = match external_protocol.as_str() {
207        "grpc" => Network::Grpc,
208        "grpcs" => Network::Grpcs,
209        _ => panic!("Only allowed options are grpc and grpcs"),
210    };
211    let internal = Network::Grpc;
212    let network = NetworkConfig { external, internal };
213    let path_provider = PathProvider::from_path_option(path)?;
214    let num_proxies = 1; // Local networks currently support exactly 1 proxy.
215    let block_exporters = if with_block_exporter {
216        let exporter_config = ExporterServiceConfig {
217            host: "localhost".to_owned(),
218            port: block_exporter_port.into(),
219        };
220        ExportersSetup::Remote(vec![exporter_config])
221    } else {
222        ExportersSetup::Local(vec![])
223    };
224    let config = LocalNetConfig {
225        network,
226        database,
227        testing_prng_seed,
228        namespace,
229        num_other_initial_chains,
230        initial_amount: Amount::from_tokens(initial_amount),
231        num_initial_validators,
232        num_shards,
233        num_proxies,
234        policy_config,
235        cross_chain_config,
236        storage_config_builder,
237        path_provider,
238        block_exporters,
239    };
240    let (mut net, client) = config.instantiate().await?;
241    let faucet_service = print_messages_and_create_faucet(
242        client,
243        with_faucet,
244        faucet_chain,
245        faucet_port,
246        faucet_amount,
247        num_other_initial_chains,
248    )
249    .await?;
250
251    wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
252}
253
254async fn wait_for_shutdown(
255    shutdown_notifier: CancellationToken,
256    net: &mut impl LineraNet,
257    faucet_service: Option<FaucetService>,
258) -> anyhow::Result<()> {
259    shutdown_notifier.cancelled().await;
260    eprintln!();
261    if let Some(service) = faucet_service {
262        eprintln!("Terminating the faucet service");
263        service.terminate().await?;
264    }
265    eprintln!("Terminating the local test network");
266    net.terminate().await?;
267    eprintln!("Done.");
268
269    Ok(())
270}
271
272async fn print_messages_and_create_faucet(
273    client: ClientWrapper,
274    with_faucet: bool,
275    faucet_chain: Option<u32>,
276    faucet_port: NonZeroU16,
277    faucet_amount: Amount,
278    num_other_initial_chains: u32,
279) -> Result<Option<FaucetService>, anyhow::Error> {
280    // Make time to (hopefully) display the message after the tracing logs.
281    linera_base::time::timer::sleep(Duration::from_secs(1)).await;
282
283    // Create the wallet for the initial "root" chains.
284    info!("Local test network successfully started.");
285
286    eprintln!(
287        "To use the admin wallet of this test network, you may set \
288         the environment variables LINERA_WALLET, LINERA_KEYSTORE, \
289         and LINERA_STORAGE as follows.\n"
290    );
291    println!(
292        "{}",
293        format!(
294            "export LINERA_WALLET=\"{}\"",
295            client.wallet_path().display()
296        )
297        .bold()
298    );
299    println!(
300        "{}",
301        format!(
302            "export LINERA_KEYSTORE=\"{}\"",
303            client.keystore_path().display()
304        )
305        .bold()
306    );
307    println!(
308        "{}",
309        format!("export LINERA_STORAGE=\"{}\"\n", client.storage_path()).bold()
310    );
311
312    let wallet = client.load_wallet()?;
313    let chains = wallet.chain_ids();
314
315    // Run the faucet,
316    let faucet_service = if with_faucet {
317        let faucet_chain_idx = faucet_chain.unwrap_or(0);
318        assert!(
319            num_other_initial_chains > faucet_chain_idx,
320            "num_other_initial_chains must be strictly greater than the faucet chain index if \
321            with_faucet is true"
322        );
323        // This picks a lexicographically faucet_chain_idx-th non-admin chain.
324        let faucet_chain = chains
325            .into_iter()
326            .filter(|chain_id| *chain_id != wallet.genesis_admin_chain())
327            .nth(faucet_chain_idx as usize)
328            .unwrap(); // we checked that there are enough chains above, so this should be safe
329        let service = client
330            .run_faucet(Some(faucet_port.into()), faucet_chain, faucet_amount)
331            .await?;
332        Some(service)
333    } else {
334        None
335    };
336
337    eprintln!(
338        "\nREADY!\nPress ^C to terminate the local test network and clean the temporary directory."
339    );
340
341    Ok(faucet_service)
342}