Skip to main content

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
17use crate::{
18    cli_wrappers::{
19        local_net::{
20            Database, ExportersSetup, InnerStorageConfigBuilder, LocalNetConfig, PathProvider,
21        },
22        ClientWrapper, FaucetService, LineraNet, LineraNetConfig, Network, NetworkConfig,
23    },
24    storage::{InnerStorageConfig, StorageConfig},
25};
26
27struct StorageConfigProvider {
28    /// The storage config.
29    config: StorageConfig,
30    #[cfg(feature = "storage-service")]
31    _service_guard: Option<StorageServiceGuard>,
32}
33
34impl StorageConfigProvider {
35    pub async fn new(storage: &Option<String>) -> anyhow::Result<StorageConfigProvider> {
36        match storage {
37            #[cfg(feature = "storage-service")]
38            None => {
39                let service_endpoint = linera_base::port::get_free_endpoint().await?;
40                let binary = get_service_storage_binary().await?.display().to_string();
41                let service = StorageService::new(&service_endpoint, binary);
42                let service_guard = Some(service.run().await?);
43                let inner_storage_config = InnerStorageConfig::Service {
44                    endpoint: service_endpoint,
45                };
46                let namespace = "table_default".to_string();
47                let config = StorageConfig {
48                    inner_storage_config,
49                    namespace,
50                };
51                Ok(StorageConfigProvider {
52                    config,
53                    _service_guard: service_guard,
54                })
55            }
56            #[cfg(not(feature = "storage-service"))]
57            None => {
58                panic!("When storage is not selected, the storage-service needs to be enabled");
59            }
60            #[cfg(feature = "storage-service")]
61            Some(storage) => {
62                let config = StorageConfig::from_str(storage)?;
63                Ok(StorageConfigProvider {
64                    config,
65                    _service_guard: None,
66                })
67            }
68            #[cfg(not(feature = "storage-service"))]
69            Some(storage) => {
70                let config = StorageConfig::from_str(storage)?;
71                Ok(StorageConfigProvider { config })
72            }
73        }
74    }
75
76    pub fn inner_storage_config(&self) -> &InnerStorageConfig {
77        &self.config.inner_storage_config
78    }
79
80    pub fn namespace(&self) -> &str {
81        &self.config.namespace
82    }
83
84    pub fn database(&self) -> anyhow::Result<Database> {
85        match self.config.inner_storage_config {
86            InnerStorageConfig::Memory { .. } => anyhow::bail!("Not possible to work with memory"),
87            #[cfg(feature = "rocksdb")]
88            InnerStorageConfig::RocksDb { .. } => {
89                anyhow::bail!("Not possible to work with RocksDB")
90            }
91            #[cfg(feature = "storage-service")]
92            InnerStorageConfig::Service { .. } => Ok(Database::Service),
93            #[cfg(feature = "scylladb")]
94            InnerStorageConfig::ScyllaDb { .. } => Ok(Database::ScyllaDb),
95            #[cfg(all(feature = "rocksdb", feature = "scylladb"))]
96            InnerStorageConfig::DualRocksDbScyllaDb { .. } => Ok(Database::DualRocksDbScyllaDb),
97        }
98    }
99}
100
101/// Starts a local test network and, optionally, a faucet and block exporter.
102#[expect(clippy::too_many_arguments)]
103pub async fn handle_net_up_service(
104    num_other_initial_chains: u32,
105    initial_amount: u128,
106    num_initial_validators: usize,
107    num_shards: usize,
108    testing_prng_seed: Option<u64>,
109    policy_config: ResourceControlPolicyConfig,
110    cross_chain_config: CrossChainConfig,
111    with_block_exporter: bool,
112    block_exporter_address: String,
113    block_exporter_port: NonZeroU16,
114    path: &Option<String>,
115    storage: &Option<String>,
116    external_protocol: String,
117    with_faucet: bool,
118    faucet_port: NonZeroU16,
119    faucet_amount: Amount,
120    http_request_allow_list: Option<Vec<String>>,
121) -> anyhow::Result<()> {
122    assert!(
123        num_initial_validators >= 1,
124        "The local test network must have at least one validator."
125    );
126    assert!(
127        num_shards >= 1,
128        "The local test network must have at least one shard per validator."
129    );
130
131    let shutdown_notifier = CancellationToken::new();
132    tokio::spawn(listen_for_shutdown_signals(shutdown_notifier.clone()));
133
134    let storage = StorageConfigProvider::new(storage).await?;
135    let storage_config = storage.inner_storage_config().clone();
136    let namespace = storage.namespace().to_string();
137    let database = storage.database()?;
138    let storage_config_builder = InnerStorageConfigBuilder::ExistingConfig { storage_config };
139    let external = match external_protocol.as_str() {
140        "grpc" => Network::Grpc,
141        "grpcs" => Network::Grpcs,
142        _ => panic!("Only allowed options are grpc and grpcs"),
143    };
144    let internal = Network::Grpc;
145    let network = NetworkConfig { external, internal };
146    let path_provider = PathProvider::from_path_option(path)?;
147    let num_proxies = 1; // Local networks currently support exactly 1 proxy.
148    let block_exporters = ExportersSetup::new(
149        with_block_exporter,
150        block_exporter_address,
151        block_exporter_port,
152    );
153    let initial_amount = Amount::from_tokens(initial_amount);
154    let config = LocalNetConfig {
155        block_export_transport: crate::config::BlockExportTransport::Relay,
156        export_blocks_to_committee: false,
157        network,
158        database,
159        testing_prng_seed,
160        namespace,
161        num_other_initial_chains,
162        initial_amount,
163        num_initial_validators,
164        num_shards,
165        num_proxies,
166        policy_config,
167        http_request_allow_list,
168        cross_chain_config,
169        storage_config_builder,
170        path_provider,
171        block_exporters,
172        binary_dir: None,
173    };
174    let (mut net, client) = config.instantiate().await?;
175    let faucet_service = print_messages_and_create_faucet(
176        client,
177        &mut net,
178        with_faucet,
179        faucet_port,
180        faucet_amount,
181        initial_amount,
182    )
183    .await?;
184
185    wait_for_shutdown(shutdown_notifier, &mut net, faucet_service).await
186}
187
188async fn wait_for_shutdown(
189    shutdown_notifier: CancellationToken,
190    net: &mut impl LineraNet,
191    faucet_service: Option<FaucetService>,
192) -> anyhow::Result<()> {
193    shutdown_notifier.cancelled().await;
194    eprintln!();
195    if let Some(service) = faucet_service {
196        eprintln!("Terminating the faucet service");
197        service.terminate().await?;
198    }
199    eprintln!("Terminating the local test network");
200    net.terminate().await?;
201    eprintln!("Done.");
202
203    Ok(())
204}
205
206async fn print_messages_and_create_faucet(
207    client: ClientWrapper,
208    net: &mut impl LineraNet,
209    with_faucet: bool,
210    faucet_port: NonZeroU16,
211    faucet_amount: Amount,
212    initial_amount: Amount,
213) -> Result<Option<FaucetService>, anyhow::Error> {
214    // Make time to (hopefully) display the message after the tracing logs.
215    linera_base::time::timer::sleep(Duration::from_secs(1)).await;
216
217    info!("Local test network successfully started.");
218
219    eprintln!(
220        "To use the admin wallet of this test network, you may set \
221         the environment variables LINERA_WALLET, LINERA_KEYSTORE, \
222         and LINERA_STORAGE as follows.\n"
223    );
224    println!(
225        "export LINERA_WALLET=\"{}\"",
226        client.wallet_path().display(),
227    );
228    println!(
229        "export LINERA_KEYSTORE=\"{}\"",
230        client.keystore_path().display(),
231    );
232    println!("export LINERA_STORAGE=\"{}\"", client.storage_path(),);
233
234    // Run the faucet using a separate wallet so it doesn't lock the admin wallet.
235    // Keep half the balance on the admin chain for fee payments (e.g. committee changes).
236    let faucet_service = if with_faucet {
237        let faucet_client = net.make_client().await;
238        faucet_client.wallet_init(None).await?;
239        let faucet_balance = Amount::from_attos(initial_amount.to_attos() / 2);
240        let faucet_chain = client
241            .open_and_assign(&faucet_client, faucet_balance)
242            .await?;
243
244        eprintln!("To connect to this network, you can use the following faucet URL:");
245        println!("export LINERA_FAUCET_URL=\"http://localhost:{faucet_port}\"");
246
247        let service = faucet_client
248            .run_faucet(Some(faucet_port.into()), Some(faucet_chain), faucet_amount)
249            .await?;
250        Some(service)
251    } else {
252        None
253    };
254
255    println!();
256
257    eprintln!(
258        "\nREADY!\nPress ^C to terminate the local test network and clean the temporary directory."
259    );
260
261    Ok(faucet_service)
262}