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 linera_base::{data_types::Amount, listen_for_shutdown_signals, time::Duration};
7use linera_client::client_options::ResourceControlPolicyConfig;
8use linera_rpc::config::CrossChainConfig;
9#[cfg(feature = "storage-service")]
10use linera_storage_service::{
11    child::{StorageService, StorageServiceGuard},
12    common::get_service_storage_binary,
13};
14use tokio_util::sync::CancellationToken;
15use tracing::info;
16#[cfg(feature = "kubernetes")]
17use {
18    crate::cli_wrappers::local_kubernetes_net::{BuildMode, LocalKubernetesNetConfig},
19    std::path::PathBuf,
20};
21
22use crate::{
23    cli_wrappers::{
24        local_net::{
25            Database, ExportersSetup, InnerStorageConfigBuilder, LocalNetConfig, PathProvider,
26        },
27        ClientWrapper, FaucetService, LineraNet, LineraNetConfig, Network, NetworkConfig,
28    },
29    storage::{InnerStorageConfig, StorageConfig},
30};
31
32struct StorageConfigProvider {
33    /// The storage config.
34    config: StorageConfig,
35    #[cfg(feature = "storage-service")]
36    _service_guard: Option<StorageServiceGuard>,
37}
38
39impl StorageConfigProvider {
40    pub async fn new(storage: &Option<String>) -> anyhow::Result<StorageConfigProvider> {
41        match storage {
42            #[cfg(feature = "storage-service")]
43            None => {
44                let service_endpoint = linera_base::port::get_free_endpoint().await?;
45                let binary = get_service_storage_binary().await?.display().to_string();
46                let service = StorageService::new(&service_endpoint, binary);
47                let service_guard = Some(service.run().await?);
48                let inner_storage_config = InnerStorageConfig::Service {
49                    endpoint: service_endpoint,
50                };
51                let namespace = "table_default".to_string();
52                let config = StorageConfig {
53                    inner_storage_config,
54                    namespace,
55                };
56                Ok(StorageConfigProvider {
57                    config,
58                    _service_guard: 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 config = StorageConfig::from_str(storage)?;
68                Ok(StorageConfigProvider {
69                    config,
70                    _service_guard: None,
71                })
72            }
73            #[cfg(not(feature = "storage-service"))]
74            Some(storage) => {
75                let config = StorageConfig::from_str(storage)?;
76                Ok(StorageConfigProvider { config })
77            }
78        }
79    }
80
81    pub fn inner_storage_config(&self) -> &InnerStorageConfig {
82        &self.config.inner_storage_config
83    }
84
85    pub fn namespace(&self) -> &str {
86        &self.config.namespace
87    }
88
89    pub fn database(&self) -> anyhow::Result<Database> {
90        match self.config.inner_storage_config {
91            InnerStorageConfig::Memory { .. } => anyhow::bail!("Not possible to work with memory"),
92            #[cfg(feature = "rocksdb")]
93            InnerStorageConfig::RocksDb { .. } => {
94                anyhow::bail!("Not possible to work with RocksDB")
95            }
96            #[cfg(feature = "storage-service")]
97            InnerStorageConfig::Service { .. } => Ok(Database::Service),
98            #[cfg(feature = "dynamodb")]
99            InnerStorageConfig::DynamoDb { .. } => Ok(Database::DynamoDb),
100            #[cfg(feature = "scylladb")]
101            InnerStorageConfig::ScyllaDb { .. } => Ok(Database::ScyllaDb),
102            #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
103            InnerStorageConfig::DualRocksDbScyllaDb { .. } => Ok(Database::DualRocksDbScyllaDb),
104        }
105    }
106}
107
108#[expect(clippy::too_many_arguments)]
109#[cfg(feature = "kubernetes")]
110pub async fn handle_net_up_kubernetes(
111    num_other_initial_chains: u32,
112    initial_amount: u128,
113    num_initial_validators: usize,
114    num_proxies: usize,
115    num_shards: usize,
116    testing_prng_seed: Option<u64>,
117    binaries: &Option<Option<PathBuf>>,
118    no_build: bool,
119    docker_image_name: String,
120    build_mode: BuildMode,
121    policy_config: ResourceControlPolicyConfig,
122    with_faucet: bool,
123    faucet_port: NonZeroU16,
124    faucet_amount: Amount,
125    with_block_exporter: bool,
126    num_block_exporters: usize,
127    indexer_image_name: String,
128    explorer_image_name: String,
129    dual_store: bool,
130    path: &Option<String>,
131) -> anyhow::Result<()> {
132    assert!(
133        num_initial_validators >= 1,
134        "The local test network must have at least one validator."
135    );
136    assert!(
137        num_proxies >= 1,
138        "The local test network must have at least one proxy."
139    );
140    assert!(
141        num_shards >= 1,
142        "The local test network must have at least one shard per validator."
143    );
144
145    let shutdown_notifier = CancellationToken::new();
146    tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
147
148    let num_block_exporters = if with_block_exporter {
149        assert!(
150            num_block_exporters > 0,
151            "If --with-block-exporter is provided, --num-block-exporters must be greater than 0"
152        );
153        num_block_exporters
154    } else {
155        0
156    };
157
158    let initial_amount = Amount::from_tokens(initial_amount);
159    let config = LocalKubernetesNetConfig {
160        network: Network::Grpc,
161        testing_prng_seed,
162        num_other_initial_chains,
163        initial_amount,
164        num_initial_validators,
165        num_proxies,
166        num_shards,
167        binaries: binaries.clone().into(),
168        no_build,
169        docker_image_name,
170        build_mode,
171        policy_config,
172        num_block_exporters,
173        indexer_image_name,
174        explorer_image_name,
175        dual_store,
176        path_provider: PathProvider::from_path_option(path)?,
177    };
178    let (mut net, client) = config.instantiate().await?;
179    let faucet_service = print_messages_and_create_faucet(
180        client,
181        &mut net,
182        with_faucet,
183        faucet_port,
184        faucet_amount,
185        initial_amount,
186    )
187    .await?;
188    wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
189}
190
191#[expect(clippy::too_many_arguments)]
192pub async fn handle_net_up_service(
193    num_other_initial_chains: u32,
194    initial_amount: u128,
195    num_initial_validators: usize,
196    num_shards: usize,
197    testing_prng_seed: Option<u64>,
198    policy_config: ResourceControlPolicyConfig,
199    cross_chain_config: CrossChainConfig,
200    with_block_exporter: bool,
201    block_exporter_address: String,
202    block_exporter_port: NonZeroU16,
203    path: &Option<String>,
204    storage: &Option<String>,
205    external_protocol: String,
206    with_faucet: bool,
207    faucet_port: NonZeroU16,
208    faucet_amount: Amount,
209    http_request_allow_list: Option<Vec<String>>,
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,
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        "export LINERA_WALLET=\"{}\"",
313        client.wallet_path().display(),
314    );
315    println!(
316        "export LINERA_KEYSTORE=\"{}\"",
317        client.keystore_path().display(),
318    );
319    println!("export LINERA_STORAGE=\"{}\"", client.storage_path(),);
320
321    // Run the faucet using a separate wallet so it doesn't lock the admin wallet.
322    // Keep half the balance on the admin chain for fee payments (e.g. committee changes).
323    let faucet_service = if with_faucet {
324        let faucet_client = net.make_client().await;
325        faucet_client.wallet_init(None).await?;
326        let faucet_balance = Amount::from_attos(initial_amount.to_attos() / 2);
327        let faucet_chain = client
328            .open_and_assign(&faucet_client, faucet_balance)
329            .await?;
330
331        eprintln!("To connect to this network, you can use the following faucet URL:");
332        println!("export LINERA_FAUCET_URL=\"http://localhost:{faucet_port}\"");
333
334        let service = faucet_client
335            .run_faucet(Some(faucet_port.into()), Some(faucet_chain), faucet_amount)
336            .await?;
337        Some(service)
338    } else {
339        None
340    };
341
342    println!();
343
344    eprintln!(
345        "\nREADY!\nPress ^C to terminate the local test network and clean the temporary directory."
346    );
347
348    Ok(faucet_service)
349}