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 = Some(service.run().await?);
49                let inner_storage_config = InnerStorageConfig::Service {
50                    endpoint: service_endpoint,
51                };
52                let namespace = "table_default".to_string();
53                let config = StorageConfig {
54                    inner_storage_config,
55                    namespace,
56                };
57                Ok(StorageConfigProvider {
58                    config,
59                    _service_guard: service_guard,
60                })
61            }
62            #[cfg(not(feature = "storage-service"))]
63            None => {
64                panic!("When storage is not selected, the storage-service needs to be enabled");
65            }
66            #[cfg(feature = "storage-service")]
67            Some(storage) => {
68                let config = StorageConfig::from_str(storage)?;
69                Ok(StorageConfigProvider {
70                    config,
71                    _service_guard: None,
72                })
73            }
74            #[cfg(not(feature = "storage-service"))]
75            Some(storage) => {
76                let config = StorageConfig::from_str(storage)?;
77                Ok(StorageConfigProvider { config })
78            }
79        }
80    }
81
82    pub fn inner_storage_config(&self) -> &InnerStorageConfig {
83        &self.config.inner_storage_config
84    }
85
86    pub fn namespace(&self) -> &str {
87        &self.config.namespace
88    }
89
90    pub fn database(&self) -> anyhow::Result<Database> {
91        match self.config.inner_storage_config {
92            InnerStorageConfig::Memory { .. } => anyhow::bail!("Not possible to work with memory"),
93            #[cfg(feature = "rocksdb")]
94            InnerStorageConfig::RocksDb { .. } => {
95                anyhow::bail!("Not possible to work with RocksDB")
96            }
97            #[cfg(feature = "storage-service")]
98            InnerStorageConfig::Service { .. } => Ok(Database::Service),
99            #[cfg(feature = "dynamodb")]
100            InnerStorageConfig::DynamoDb { .. } => Ok(Database::DynamoDb),
101            #[cfg(feature = "scylladb")]
102            InnerStorageConfig::ScyllaDb { .. } => Ok(Database::ScyllaDb),
103            #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
104            InnerStorageConfig::DualRocksDbScyllaDb { .. } => Ok(Database::DualRocksDbScyllaDb),
105        }
106    }
107}
108
109#[expect(clippy::too_many_arguments)]
110#[cfg(feature = "kubernetes")]
111pub async fn handle_net_up_kubernetes(
112    num_other_initial_chains: u32,
113    initial_amount: u128,
114    num_initial_validators: usize,
115    num_proxies: 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_port: NonZeroU16,
125    faucet_amount: Amount,
126    with_block_exporter: bool,
127    num_block_exporters: usize,
128    indexer_image_name: String,
129    explorer_image_name: String,
130    dual_store: bool,
131    path: &Option<String>,
132) -> anyhow::Result<()> {
133    assert!(
134        num_initial_validators >= 1,
135        "The local test network must have at least one validator."
136    );
137    assert!(
138        num_proxies >= 1,
139        "The local test network must have at least one proxy."
140    );
141    assert!(
142        num_shards >= 1,
143        "The local test network must have at least one shard per validator."
144    );
145
146    let shutdown_notifier = CancellationToken::new();
147    tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
148
149    let num_block_exporters = if with_block_exporter {
150        assert!(
151            num_block_exporters > 0,
152            "If --with-block-exporter is provided, --num-block-exporters must be greater than 0"
153        );
154        num_block_exporters
155    } else {
156        0
157    };
158
159    let initial_amount = Amount::from_tokens(initial_amount);
160    let config = LocalKubernetesNetConfig {
161        network: Network::Grpc,
162        testing_prng_seed,
163        num_other_initial_chains,
164        initial_amount,
165        num_initial_validators,
166        num_proxies,
167        num_shards,
168        binaries: binaries.clone().into(),
169        no_build,
170        docker_image_name,
171        build_mode,
172        policy_config,
173        num_block_exporters,
174        indexer_image_name,
175        explorer_image_name,
176        dual_store,
177        path_provider: PathProvider::from_path_option(path)?,
178    };
179    let (mut net, client) = config.instantiate().await?;
180    let faucet_service = print_messages_and_create_faucet(
181        client,
182        &mut net,
183        with_faucet,
184        faucet_port,
185        faucet_amount,
186        initial_amount,
187    )
188    .await?;
189    wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
190}
191
192#[expect(clippy::too_many_arguments)]
193pub async fn handle_net_up_service(
194    num_other_initial_chains: u32,
195    initial_amount: u128,
196    num_initial_validators: usize,
197    num_shards: usize,
198    testing_prng_seed: Option<u64>,
199    policy_config: ResourceControlPolicyConfig,
200    cross_chain_config: CrossChainConfig,
201    with_block_exporter: bool,
202    block_exporter_address: String,
203    block_exporter_port: NonZeroU16,
204    path: &Option<String>,
205    storage: &Option<String>,
206    external_protocol: String,
207    with_faucet: bool,
208    faucet_port: NonZeroU16,
209    faucet_amount: Amount,
210) -> anyhow::Result<()> {
211    assert!(
212        num_initial_validators >= 1,
213        "The local test network must have at least one validator."
214    );
215    assert!(
216        num_shards >= 1,
217        "The local test network must have at least one shard per validator."
218    );
219
220    let shutdown_notifier = CancellationToken::new();
221    tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
222
223    let storage = StorageConfigProvider::new(storage).await?;
224    let storage_config = storage.inner_storage_config().clone();
225    let namespace = storage.namespace().to_string();
226    let database = storage.database()?;
227    let storage_config_builder = InnerStorageConfigBuilder::ExistingConfig { storage_config };
228    let external = match external_protocol.as_str() {
229        "grpc" => Network::Grpc,
230        "grpcs" => Network::Grpcs,
231        _ => panic!("Only allowed options are grpc and grpcs"),
232    };
233    let internal = Network::Grpc;
234    let network = NetworkConfig { external, internal };
235    let path_provider = PathProvider::from_path_option(path)?;
236    let num_proxies = 1; // Local networks currently support exactly 1 proxy.
237    let block_exporters = ExportersSetup::new(
238        with_block_exporter,
239        block_exporter_address,
240        block_exporter_port,
241    );
242    let initial_amount = Amount::from_tokens(initial_amount);
243    let config = LocalNetConfig {
244        network,
245        database,
246        testing_prng_seed,
247        namespace,
248        num_other_initial_chains,
249        initial_amount,
250        num_initial_validators,
251        num_shards,
252        num_proxies,
253        policy_config,
254        http_request_allow_list: None,
255        cross_chain_config,
256        storage_config_builder,
257        path_provider,
258        block_exporters,
259        binary_dir: None,
260    };
261    let (mut net, client) = config.instantiate().await?;
262    let faucet_service = print_messages_and_create_faucet(
263        client,
264        &mut net,
265        with_faucet,
266        faucet_port,
267        faucet_amount,
268        initial_amount,
269    )
270    .await?;
271
272    wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
273}
274
275async fn wait_for_shutdown(
276    shutdown_notifier: CancellationToken,
277    net: &mut impl LineraNet,
278    faucet_service: Option<FaucetService>,
279) -> anyhow::Result<()> {
280    shutdown_notifier.cancelled().await;
281    eprintln!();
282    if let Some(service) = faucet_service {
283        eprintln!("Terminating the faucet service");
284        service.terminate().await?;
285    }
286    eprintln!("Terminating the local test network");
287    net.terminate().await?;
288    eprintln!("Done.");
289
290    Ok(())
291}
292
293async fn print_messages_and_create_faucet(
294    client: ClientWrapper,
295    net: &mut impl LineraNet,
296    with_faucet: bool,
297    faucet_port: NonZeroU16,
298    faucet_amount: Amount,
299    initial_amount: Amount,
300) -> Result<Option<FaucetService>, anyhow::Error> {
301    // Make time to (hopefully) display the message after the tracing logs.
302    linera_base::time::timer::sleep(Duration::from_secs(1)).await;
303
304    info!("Local test network successfully started.");
305
306    eprintln!(
307        "To use the admin wallet of this test network, you may set \
308         the environment variables LINERA_WALLET, LINERA_KEYSTORE, \
309         and LINERA_STORAGE as follows.\n"
310    );
311    println!(
312        "{}",
313        format!(
314            "export LINERA_WALLET=\"{}\"",
315            client.wallet_path().display(),
316        )
317        .bold(),
318    );
319    println!(
320        "{}",
321        format!(
322            "export LINERA_KEYSTORE=\"{}\"",
323            client.keystore_path().display(),
324        )
325        .bold()
326    );
327    println!(
328        "{}",
329        format!("export LINERA_STORAGE=\"{}\"", client.storage_path()).bold(),
330    );
331
332    // Run the faucet using a separate wallet so it doesn't lock the admin wallet.
333    // Keep half the balance on the admin chain for fee payments (e.g. committee changes).
334    let faucet_service = if with_faucet {
335        let faucet_client = net.make_client().await;
336        faucet_client.wallet_init(None).await?;
337        let faucet_balance = Amount::from_attos(initial_amount.to_attos() / 2);
338        let faucet_chain = client
339            .open_and_assign(&faucet_client, faucet_balance)
340            .await?;
341
342        eprintln!("To connect to this network, you can use the following faucet URL:");
343        println!(
344            "{}",
345            format!("export LINERA_FAUCET_URL=\"http://localhost:{faucet_port}\"").bold(),
346        );
347
348        let service = faucet_client
349            .run_faucet(Some(faucet_port.into()), Some(faucet_chain), faucet_amount)
350            .await?;
351        Some(service)
352    } else {
353        None
354    };
355
356    println!();
357
358    eprintln!(
359        "\nREADY!\nPress ^C to terminate the local test network and clean the temporary directory."
360    );
361
362    Ok(faucet_service)
363}