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::{
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_proxies: usize,
117    num_shards: usize,
118    testing_prng_seed: Option<u64>,
119    binaries: &Option<Option<PathBuf>>,
120    no_build: bool,
121    docker_image_name: String,
122    build_mode: BuildMode,
123    policy_config: ResourceControlPolicyConfig,
124    with_faucet: bool,
125    faucet_chain: Option<u32>,
126    faucet_port: NonZeroU16,
127    faucet_amount: Amount,
128    with_block_exporter: bool,
129    num_block_exporters: usize,
130    indexer_image_name: String,
131    explorer_image_name: String,
132    dual_store: bool,
133    path: &Option<String>,
134) -> anyhow::Result<()> {
135    assert!(
136        num_initial_validators >= 1,
137        "The local test network must have at least one validator."
138    );
139    assert!(
140        num_proxies >= 1,
141        "The local test network must have at least one proxy."
142    );
143    assert!(
144        num_shards >= 1,
145        "The local test network must have at least one shard per validator."
146    );
147    if faucet_chain.is_some() {
148        assert!(
149            with_faucet,
150            "--faucet-chain must be provided only with --with-faucet"
151        );
152    }
153
154    let shutdown_notifier = CancellationToken::new();
155    tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
156
157    let num_block_exporters = if with_block_exporter {
158        assert!(
159            num_block_exporters > 0,
160            "If --with-block-exporter is provided, --num-block-exporters must be greater than 0"
161        );
162        num_block_exporters
163    } else {
164        0
165    };
166
167    let config = LocalKubernetesNetConfig {
168        network: Network::Grpc,
169        testing_prng_seed,
170        num_other_initial_chains,
171        initial_amount: Amount::from_tokens(initial_amount),
172        num_initial_validators,
173        num_proxies,
174        num_shards,
175        binaries: binaries.clone().into(),
176        no_build,
177        docker_image_name,
178        build_mode,
179        policy_config,
180        num_block_exporters,
181        indexer_image_name,
182        explorer_image_name,
183        dual_store,
184        path_provider: PathProvider::from_path_option(path)?,
185    };
186    let (mut net, client) = config.instantiate().await?;
187    let faucet_service = print_messages_and_create_faucet(
188        client,
189        with_faucet,
190        faucet_chain,
191        faucet_port,
192        faucet_amount,
193        num_other_initial_chains,
194    )
195    .await?;
196    wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
197}
198
199#[expect(clippy::too_many_arguments)]
200pub async fn handle_net_up_service(
201    num_other_initial_chains: u32,
202    initial_amount: u128,
203    num_initial_validators: usize,
204    num_shards: usize,
205    testing_prng_seed: Option<u64>,
206    policy_config: ResourceControlPolicyConfig,
207    cross_chain_config: CrossChainConfig,
208    with_block_exporter: bool,
209    block_exporter_address: String,
210    block_exporter_port: NonZeroU16,
211    path: &Option<String>,
212    storage: &Option<String>,
213    external_protocol: String,
214    with_faucet: bool,
215    faucet_chain: Option<u32>,
216    faucet_port: NonZeroU16,
217    faucet_amount: Amount,
218) -> anyhow::Result<()> {
219    assert!(
220        num_initial_validators >= 1,
221        "The local test network must have at least one validator."
222    );
223    assert!(
224        num_shards >= 1,
225        "The local test network must have at least one shard per validator."
226    );
227
228    let shutdown_notifier = CancellationToken::new();
229    tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
230
231    let storage = StorageConfigProvider::new(storage).await?;
232    let storage_config = storage.inner_storage_config().clone();
233    let namespace = storage.namespace().to_string();
234    let database = storage.database()?;
235    let storage_config_builder = InnerStorageConfigBuilder::ExistingConfig { storage_config };
236    let external = match external_protocol.as_str() {
237        "grpc" => Network::Grpc,
238        "grpcs" => Network::Grpcs,
239        _ => panic!("Only allowed options are grpc and grpcs"),
240    };
241    let internal = Network::Grpc;
242    let network = NetworkConfig { external, internal };
243    let path_provider = PathProvider::from_path_option(path)?;
244    let num_proxies = 1; // Local networks currently support exactly 1 proxy.
245    let block_exporters = ExportersSetup::new(
246        with_block_exporter,
247        block_exporter_address,
248        block_exporter_port,
249    );
250    let config = LocalNetConfig {
251        network,
252        database,
253        testing_prng_seed,
254        namespace,
255        num_other_initial_chains,
256        initial_amount: Amount::from_tokens(initial_amount),
257        num_initial_validators,
258        num_shards,
259        num_proxies,
260        policy_config,
261        cross_chain_config,
262        storage_config_builder,
263        path_provider,
264        block_exporters,
265    };
266    let (mut net, client) = config.instantiate().await?;
267    let faucet_service = print_messages_and_create_faucet(
268        client,
269        with_faucet,
270        faucet_chain,
271        faucet_port,
272        faucet_amount,
273        num_other_initial_chains,
274    )
275    .await?;
276
277    wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
278}
279
280async fn wait_for_shutdown(
281    shutdown_notifier: CancellationToken,
282    net: &mut impl LineraNet,
283    faucet_service: Option<FaucetService>,
284) -> anyhow::Result<()> {
285    shutdown_notifier.cancelled().await;
286    eprintln!();
287    if let Some(service) = faucet_service {
288        eprintln!("Terminating the faucet service");
289        service.terminate().await?;
290    }
291    eprintln!("Terminating the local test network");
292    net.terminate().await?;
293    eprintln!("Done.");
294
295    Ok(())
296}
297
298async fn print_messages_and_create_faucet(
299    client: ClientWrapper,
300    with_faucet: bool,
301    faucet_chain: Option<u32>,
302    faucet_port: NonZeroU16,
303    faucet_amount: Amount,
304    num_other_initial_chains: u32,
305) -> Result<Option<FaucetService>, anyhow::Error> {
306    // Make time to (hopefully) display the message after the tracing logs.
307    linera_base::time::timer::sleep(Duration::from_secs(1)).await;
308
309    // Create the wallet for the initial "root" chains.
310    info!("Local test network successfully started.");
311
312    eprintln!(
313        "To use the admin wallet of this test network, you may set \
314         the environment variables LINERA_WALLET, LINERA_KEYSTORE, \
315         and LINERA_STORAGE as follows.\n"
316    );
317    println!(
318        "{}",
319        format!(
320            "export LINERA_WALLET=\"{}\"",
321            client.wallet_path().display(),
322        )
323        .bold(),
324    );
325    println!(
326        "{}",
327        format!(
328            "export LINERA_KEYSTORE=\"{}\"",
329            client.keystore_path().display(),
330        )
331        .bold()
332    );
333    println!(
334        "{}",
335        format!("export LINERA_STORAGE=\"{}\"", client.storage_path()).bold(),
336    );
337
338    let wallet = client.load_wallet()?;
339    let chains = wallet.chain_ids();
340
341    // Run the faucet,
342    let faucet_service = if with_faucet {
343        let faucet_chain = if let Some(faucet_chain_idx) = faucet_chain {
344            assert!(
345                num_other_initial_chains > faucet_chain_idx,
346                "num_other_initial_chains must be strictly greater than the faucet chain index if \
347                 with_faucet is true"
348            );
349
350            eprintln!("To connect to this network, you can use the following faucet URL:");
351            println!(
352                "{}",
353                format!("export LINERA_FAUCET_URL=\"http://localhost:{faucet_port}\"").bold(),
354            );
355
356            // This picks a lexicographically faucet_chain_idx-th non-admin chain.
357            Some(
358                chains
359                    .into_iter()
360                    .filter(|chain_id| *chain_id != wallet.genesis_admin_chain())
361                    .nth(faucet_chain_idx as usize)
362                    .expect("there should be at least one non-admin chain"),
363            )
364        } else {
365            None
366        };
367        let service = client
368            .run_faucet(Some(faucet_port.into()), faucet_chain, faucet_amount)
369            .await?;
370        Some(service)
371    } else {
372        None
373    };
374
375    println!();
376
377    eprintln!(
378        "\nREADY!\nPress ^C to terminate the local test network and clean the temporary directory."
379    );
380
381    Ok(faucet_service)
382}