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