Skip to main content

linera_client/
client_context.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::sync::Arc;
5
6#[cfg(not(web))]
7use futures::StreamExt as _;
8use futures::{Future, TryStreamExt as _};
9use linera_base::{
10    crypto::{CryptoHash, ValidatorPublicKey},
11    data_types::{ChainDescription, Epoch, Timestamp},
12    identifiers::{Account, AccountOwner, ChainId},
13    ownership::ChainOwnership,
14    time::{Duration, Instant},
15    util::future::FutureSyncExt as _,
16};
17use linera_chain::{manager::LockingBlock, types::ConfirmedBlockCertificate};
18use linera_core::{
19    client::{chain_client, ChainClient, Client, ListeningMode},
20    data_types::{ChainInfo, ChainInfoQuery, ClientOutcome},
21    join_set_ext::JoinSet,
22    node::ValidatorNode,
23    wallet, Environment, JoinSetExt as _, Wallet as _,
24};
25use linera_rpc::node_provider::{NodeOptions, NodeProvider};
26use linera_storage::Storage as _;
27use linera_version::VersionInfo;
28use thiserror_context::Context;
29use tracing::{debug, info, warn};
30#[cfg(not(web))]
31use {
32    crate::{
33        benchmark::{fungible_transfer, Benchmark, BenchmarkError},
34        client_metrics::ClientMetrics,
35    },
36    futures::stream,
37    linera_base::{
38        crypto::AccountPublicKey,
39        data_types::{Amount, BlockHeight},
40        identifiers::{ApplicationId, BlobType},
41    },
42    linera_execution::{
43        system::{OpenChainConfig, SystemOperation},
44        Operation,
45    },
46    std::{collections::HashSet, path::Path},
47    tokio::{sync::mpsc, task},
48};
49#[cfg(feature = "fs")]
50use {
51    linera_base::{
52        data_types::{BlobContent, Bytecode},
53        identifiers::ModuleId,
54        vm::VmRuntime,
55    },
56    linera_core::client::create_bytecode_blobs,
57    std::{fs, path::PathBuf},
58};
59
60use crate::{
61    chain_listener::{self, ClientContext as _, ClientContextExt as _},
62    client_options::{ChainOwnershipConfig, Options},
63    config::GenesisConfig,
64    error, util, Error,
65};
66
67/// Results from querying a validator about version, network description, and chain info.
68pub struct ValidatorQueryResults {
69    /// The validator's version information.
70    pub version_info: Result<VersionInfo, Error>,
71    /// The validator's genesis config hash.
72    pub genesis_config_hash: Result<CryptoHash, Error>,
73    /// The validator's chain info (if valid and signature check passed).
74    pub chain_info: Result<ChainInfo, Error>,
75}
76
77impl ValidatorQueryResults {
78    /// Returns a vector of references to all errors in the query results.
79    pub fn errors(&self) -> Vec<&Error> {
80        let mut errors = Vec::new();
81        if let Err(e) = &self.version_info {
82            errors.push(e);
83        }
84        if let Err(e) = &self.genesis_config_hash {
85            errors.push(e);
86        }
87        if let Err(e) = &self.chain_info {
88            errors.push(e);
89        }
90        errors
91    }
92
93    /// Prints validator information to stdout.
94    ///
95    /// Prints public key, address, and optionally weight, version info, and chain info.
96    /// If `reference` is provided, only prints fields that differ from the reference.
97    pub fn print(
98        &self,
99        public_key: Option<&ValidatorPublicKey>,
100        address: Option<&str>,
101        weight: Option<u64>,
102        reference: Option<&ValidatorQueryResults>,
103    ) {
104        if let Some(key) = public_key {
105            println!("Public key: {key}");
106        }
107        if let Some(address) = address {
108            println!("Address: {address}");
109        }
110        if let Some(w) = weight {
111            println!("Weight: {w}");
112        }
113
114        let ref_version = reference.and_then(|ref_results| ref_results.version_info.as_ref().ok());
115        match &self.version_info {
116            Ok(version_info) => {
117                if ref_version.is_none_or(|ref_v| ref_v.crate_version != version_info.crate_version)
118                {
119                    println!("Linera protocol: v{}", version_info.crate_version);
120                }
121                if ref_version.is_none_or(|ref_v| ref_v.rpc_hash != version_info.rpc_hash) {
122                    println!("RPC API hash: {}", version_info.rpc_hash);
123                }
124                if ref_version.is_none_or(|ref_v| ref_v.graphql_hash != version_info.graphql_hash) {
125                    println!("GraphQL API hash: {}", version_info.graphql_hash);
126                }
127                if ref_version.is_none_or(|ref_v| ref_v.wit_hash != version_info.wit_hash) {
128                    println!("WIT API hash: {}", version_info.wit_hash);
129                }
130                if ref_version.is_none_or(|ref_v| {
131                    (&ref_v.git_commit, ref_v.git_dirty)
132                        != (&version_info.git_commit, version_info.git_dirty)
133                }) {
134                    println!(
135                        "Source code: {}/tree/{}{}",
136                        env!("CARGO_PKG_REPOSITORY"),
137                        version_info.git_commit,
138                        if version_info.git_dirty {
139                            " (dirty)"
140                        } else {
141                            ""
142                        }
143                    );
144                }
145            }
146            Err(err) => println!("Error getting version info: {err}"),
147        }
148
149        let ref_genesis_hash =
150            reference.and_then(|ref_results| ref_results.genesis_config_hash.as_ref().ok());
151        match &self.genesis_config_hash {
152            Ok(hash) if ref_genesis_hash.is_some_and(|ref_hash| ref_hash == hash) => {}
153            Ok(hash) => println!("Genesis config hash: {hash}"),
154            Err(err) => println!("Error getting genesis config: {err}"),
155        }
156
157        let ref_info = reference.and_then(|ref_results| ref_results.chain_info.as_ref().ok());
158        match &self.chain_info {
159            Ok(info) => {
160                if ref_info.is_none_or(|ref_info| info.block_hash != ref_info.block_hash) {
161                    if let Some(hash) = info.block_hash {
162                        println!("Block hash: {hash}");
163                    } else {
164                        println!("Block hash: None");
165                    }
166                }
167                if ref_info
168                    .is_none_or(|ref_info| info.next_block_height != ref_info.next_block_height)
169                {
170                    println!("Next height: {}", info.next_block_height);
171                }
172                if ref_info.is_none_or(|ref_info| info.timestamp != ref_info.timestamp) {
173                    println!("Timestamp: {}", info.timestamp);
174                }
175                if ref_info.is_none_or(|ref_info| info.epoch != ref_info.epoch) {
176                    println!("Epoch: {}", info.epoch);
177                }
178                if ref_info.is_none_or(|ref_info| {
179                    info.manager.current_round != ref_info.manager.current_round
180                }) {
181                    println!("Round: {}", info.manager.current_round);
182                }
183                if let Some(leader) = info.manager.leader {
184                    println!("Leader: {leader}");
185                }
186                if let Some(locking) = &info.manager.requested_locking {
187                    match &**locking {
188                        LockingBlock::Fast(proposal) => {
189                            println!(
190                                "Locking fast block from {}",
191                                proposal.content.block.timestamp
192                            );
193                        }
194                        LockingBlock::Regular(validated) => {
195                            println!(
196                                "Locking block {} in {} from {}",
197                                validated.hash(),
198                                validated.round,
199                                validated.block().header.timestamp
200                            );
201                        }
202                    }
203                }
204            }
205            Err(err) => println!("Error getting chain info: {err}"),
206        }
207        println!();
208    }
209}
210
211/// The state shared by the client commands: the core client, wallet configuration, and
212/// network timeouts.
213pub struct ClientContext<Env: Environment> {
214    /// The core client used to interact with chains and validators.
215    pub client: Arc<Client<Env>>,
216    /// The genesis configuration of the network.
217    // TODO(#5083): this doesn't really need to be stored
218    pub genesis_config: crate::config::GenesisConfig,
219    /// The timeout for sending requests to validators.
220    pub send_timeout: Duration,
221    /// The timeout for receiving responses from validators.
222    pub recv_timeout: Duration,
223    /// The delay before retrying a failed request to a validator.
224    pub retry_delay: Duration,
225    /// The maximum number of times to retry a failed request to a validator.
226    pub max_retries: u32,
227    /// The maximum backoff between retries of a failed request to a validator.
228    pub max_backoff: Duration,
229    /// The set of background tasks listening for chain notifications.
230    pub chain_listeners: JoinSet,
231    /// The default chain used when no chain is explicitly specified.
232    // TODO(#5082): move this into the upstream UI layers (maybe just the CLI)
233    pub default_chain: Option<ChainId>,
234    /// The metrics collector, if metrics collection is enabled.
235    #[cfg(not(web))]
236    pub client_metrics: Option<ClientMetrics>,
237}
238
239impl<Env: Environment> chain_listener::ClientContext for ClientContext<Env> {
240    type Environment = Env;
241
242    fn wallet(&self) -> &Env::Wallet {
243        self.client.wallet()
244    }
245
246    fn storage(&self) -> &Env::Storage {
247        self.client.storage_client()
248    }
249
250    fn client(&self) -> &Arc<Client<Env>> {
251        &self.client
252    }
253
254    #[cfg(not(web))]
255    fn timing_sender(
256        &self,
257    ) -> Option<mpsc::UnboundedSender<(u64, linera_core::client::TimingType)>> {
258        self.client_metrics
259            .as_ref()
260            .map(|metrics| metrics.timing_sender.clone())
261    }
262
263    async fn update_wallet_for_new_chain(
264        &mut self,
265        chain_id: ChainId,
266        owner: Option<AccountOwner>,
267        timestamp: Timestamp,
268        epoch: Epoch,
269    ) -> Result<(), Error> {
270        self.update_wallet_for_new_chain(chain_id, owner, timestamp, epoch)
271            .make_sync()
272            .await
273    }
274
275    async fn update_wallet(&mut self, client: &ChainClient<Env>) -> Result<(), Error> {
276        self.update_wallet_from_client(client).make_sync().await
277    }
278}
279
280impl<S, Si, W> ClientContext<linera_core::environment::Impl<S, NodeProvider, Si, W>>
281where
282    S: linera_core::environment::Storage,
283    Si: linera_core::environment::Signer,
284    W: linera_core::environment::Wallet,
285{
286    // not worth refactoring this because
287    // https://github.com/linera-io/linera-protocol/issues/5082
288    // https://github.com/linera-io/linera-protocol/issues/5083
289    /// Creates a new client context from the given storage, wallet, signer, and options.
290    #[expect(clippy::too_many_arguments)]
291    pub async fn new(
292        storage: S,
293        wallet: W,
294        signer: Si,
295        options: &Options,
296        default_chain: Option<ChainId>,
297        genesis_config: GenesisConfig,
298        block_cache_size: usize,
299        execution_state_cache_size: usize,
300    ) -> Result<Self, Error> {
301        #[cfg(not(web))]
302        let timing_config = options.to_timing_config();
303        let node_provider = NodeProvider::new(NodeOptions {
304            send_timeout: options.send_timeout,
305            recv_timeout: options.recv_timeout,
306            retry_delay: options.retry_delay,
307            max_retries: options.max_retries,
308            max_backoff: options.max_backoff,
309        });
310        let chain_modes: Vec<_> = wallet
311            .items()
312            .map_ok(|(id, chain)| {
313                let mode = if chain.is_follow_only() {
314                    ListeningMode::FollowChain
315                } else {
316                    ListeningMode::FullChain
317                };
318                (id, mode)
319            })
320            .try_collect()
321            .await
322            .map_err(error::Inner::wallet)?;
323        let name = match chain_modes.len() {
324            0 => "Client node".to_string(),
325            1 => format!("Client node for {:.8}", chain_modes[0].0),
326            n => format!(
327                "Client node for {:.8} and {} others",
328                chain_modes[0].0,
329                n - 1
330            ),
331        };
332
333        let client = Client::new(
334            linera_core::environment::Impl {
335                network: node_provider,
336                storage,
337                signer,
338                wallet,
339            },
340            genesis_config.admin_chain_id(),
341            options.long_lived_services,
342            chain_modes,
343            name,
344            util::non_zero_duration(options.chain_worker_ttl),
345            util::non_zero_duration(options.sender_chain_worker_ttl),
346            options.cross_chain_batch_size_limit,
347            options.to_chain_client_options(),
348            block_cache_size,
349            execution_state_cache_size,
350            &options.to_requests_scheduler_config(),
351        );
352
353        #[cfg(not(web))]
354        let client_metrics = if timing_config.enabled {
355            Some(ClientMetrics::new(timing_config))
356        } else {
357            None
358        };
359
360        Ok(ClientContext {
361            client: Arc::new(client),
362            default_chain,
363            genesis_config,
364            send_timeout: options.send_timeout,
365            recv_timeout: options.recv_timeout,
366            retry_delay: options.retry_delay,
367            max_retries: options.max_retries,
368            max_backoff: options.max_backoff,
369            chain_listeners: JoinSet::default(),
370            #[cfg(not(web))]
371            client_metrics,
372        })
373    }
374}
375
376impl<Env: Environment> ClientContext<Env> {
377    // TODO(#5084) this (and other injected dependencies) should not be re-exposed by the
378    // client interface
379    /// Returns a reference to the wallet.
380    pub fn wallet(&self) -> &Env::Wallet {
381        self.client.wallet()
382    }
383
384    /// Returns the ID of the admin chain.
385    pub fn admin_chain_id(&self) -> ChainId {
386        self.client.admin_chain_id()
387    }
388
389    /// Retrieve the default account. Current this is the common account of the default
390    /// chain.
391    pub fn default_account(&self) -> Account {
392        Account::chain(self.default_chain())
393    }
394
395    /// Retrieve the default chain.
396    pub fn default_chain(&self) -> ChainId {
397        self.default_chain
398            .expect("default chain requested but none set")
399    }
400
401    /// Returns the lowest non-admin chain ID in the wallet.
402    pub async fn first_non_admin_chain(&self) -> Result<ChainId, Error> {
403        let admin_chain_id = self.admin_chain_id();
404        let chain_ids = self
405            .wallet()
406            .chain_ids()
407            .try_filter(|chain_id| futures::future::ready(*chain_id != admin_chain_id))
408            .try_collect::<Vec<ChainId>>()
409            .await
410            .map_err(Error::wallet)?;
411        Ok(chain_ids
412            .into_iter()
413            .min()
414            .expect("No non-admin chain specified in wallet with no non-admin chain"))
415    }
416
417    /// Creates a node provider configured with this context's network options.
418    // TODO(#5084) this should match the `NodeProvider` from the `Environment`
419    pub fn make_node_provider(&self) -> NodeProvider {
420        NodeProvider::new(self.make_node_options())
421    }
422
423    fn make_node_options(&self) -> NodeOptions {
424        NodeOptions {
425            send_timeout: self.send_timeout,
426            recv_timeout: self.recv_timeout,
427            retry_delay: self.retry_delay,
428            max_retries: self.max_retries,
429            max_backoff: self.max_backoff,
430        }
431    }
432
433    /// Returns the client metrics, if metrics collection is enabled.
434    #[cfg(not(web))]
435    pub fn client_metrics(&self) -> Option<&ClientMetrics> {
436        self.client_metrics.as_ref()
437    }
438
439    /// Updates the wallet entry for the client's chain from its current chain info.
440    pub async fn update_wallet_from_client<Env_: Environment>(
441        &self,
442        client: &ChainClient<Env_>,
443    ) -> Result<(), Error> {
444        let info = client.chain_info().await?;
445        let chain_id = info.chain_id;
446        let existing_owner = self
447            .wallet()
448            .get(chain_id)
449            .await
450            .map_err(error::Inner::wallet)?
451            .and_then(|chain| chain.owner);
452
453        // Only persist proposals that were made in the fast round: they need to be
454        // remembered across sessions to make sure there are no conflicting fast proposals.
455        let pending_fast_proposal = client
456            .pending_proposal()
457            .await
458            .filter(|p| p.round.is_some_and(|r| r.is_fast()));
459        let new_chain = wallet::Chain {
460            pending_fast_proposal,
461            owner: existing_owner,
462            ..info.as_ref().into()
463        };
464
465        self.wallet()
466            .insert(chain_id, new_chain)
467            .await
468            .map_err(error::Inner::wallet)?;
469
470        Ok(())
471    }
472
473    /// Remembers the new chain and its owner (if any) in the wallet.
474    pub async fn update_wallet_for_new_chain(
475        &mut self,
476        chain_id: ChainId,
477        owner: Option<AccountOwner>,
478        timestamp: Timestamp,
479        epoch: Epoch,
480    ) -> Result<(), Error> {
481        self.wallet()
482            .try_insert(
483                chain_id,
484                linera_core::wallet::Chain::new(owner, epoch, timestamp),
485            )
486            .await
487            .map_err(error::Inner::wallet)?;
488        Ok(())
489    }
490
491    /// Registers a chain from its description: initializes local storage, adds to
492    /// wallet, and starts tracking it for cross-chain message delivery.
493    pub async fn extend_with_chain(
494        &mut self,
495        description: ChainDescription,
496        owner: Option<AccountOwner>,
497    ) -> Result<(), Error> {
498        let chain_id = description.id();
499        self.client
500            .storage_client()
501            .create_chain(description.clone())
502            .await?;
503        self.wallet()
504            .try_insert(
505                chain_id,
506                linera_core::wallet::Chain::new(
507                    owner,
508                    description.config().epoch,
509                    description.timestamp(),
510                ),
511            )
512            .await
513            .map_err(error::Inner::wallet)?;
514        self.client
515            .extend_chain_mode(chain_id, ListeningMode::FullChain);
516        Ok(())
517    }
518
519    /// Processes the chain's inbox, waiting for round timeouts, and updates the wallet.
520    pub async fn process_inbox(
521        &mut self,
522        chain_client: &ChainClient<Env>,
523    ) -> Result<Vec<ConfirmedBlockCertificate>, Error> {
524        let mut certificates = Vec::new();
525        // Try processing the inbox optimistically without waiting for validator notifications.
526        let (new_certificates, maybe_timeout) = {
527            chain_client.synchronize_from_validators().await?;
528            let result = chain_client.process_inbox_without_prepare().await;
529            self.update_wallet_from_client(chain_client).await?;
530            result?
531        };
532        certificates.extend(new_certificates);
533        if maybe_timeout.is_none() {
534            return Ok(certificates);
535        }
536
537        // Start listening for notifications, so we learn about new rounds and blocks.
538        let (listener, _listen_handle, mut notification_stream) = chain_client.listen().await?;
539        self.chain_listeners.spawn_task(listener);
540
541        loop {
542            let (new_certificates, maybe_timeout) = {
543                let result = chain_client.process_inbox().await;
544                self.update_wallet_from_client(chain_client).await?;
545                result?
546            };
547            certificates.extend(new_certificates);
548            if let Some(timestamp) = maybe_timeout {
549                util::wait_for_next_round(&mut notification_stream, timestamp).await
550            } else {
551                return Ok(certificates);
552            }
553        }
554    }
555
556    /// Assigns the given chain to the owner, tracking it and recording it in the wallet.
557    pub async fn assign_new_chain_to_key(
558        &mut self,
559        chain_id: ChainId,
560        owner: AccountOwner,
561    ) -> Result<(), Error> {
562        self.client
563            .extend_chain_mode(chain_id, ListeningMode::FullChain);
564        let client = self.make_chain_client(chain_id).await?;
565        let info = client.prepare_for_owner(owner).await.map_err(|error| {
566            tracing::error!(%chain_id, %owner, %error, "Chain is not owned");
567            error::Inner::ChainOwnership
568        })?;
569
570        // Try to modify existing chain entry, setting the owner.
571        let modified = self
572            .wallet()
573            .modify(chain_id, |chain| chain.owner = Some(owner))
574            .await
575            .map_err(error::Inner::wallet)?;
576        // If the chain didn't exist, insert a new entry.
577        if modified.is_none() {
578            self.wallet()
579                .insert(
580                    chain_id,
581                    wallet::Chain {
582                        owner: Some(owner),
583                        timestamp: info.timestamp,
584                        epoch: Some(info.epoch),
585                        ..Default::default()
586                    },
587                )
588                .await
589                .map_err(error::Inner::wallet)
590                .context("assigning new chain")?;
591        }
592        Ok(())
593    }
594
595    /// Applies the given function to the chain client.
596    ///
597    /// Updates the wallet regardless of the outcome. As long as the function returns a round
598    /// timeout, it will wait and retry.
599    pub async fn apply_client_command<E, F, Fut, T>(
600        &mut self,
601        client: &ChainClient<Env>,
602        mut f: F,
603    ) -> Result<T, Error>
604    where
605        F: FnMut(&ChainClient<Env>) -> Fut,
606        Fut: Future<Output = Result<ClientOutcome<T>, E>>,
607        Error: From<E>,
608    {
609        client.prepare_chain().await?;
610        // Try applying f optimistically without validator notifications. Return if committed.
611        let result = f(client).await;
612        self.update_wallet_from_client(client).await?;
613        match result? {
614            ClientOutcome::Committed(t) => return Ok(t),
615            ClientOutcome::Conflict(certificate) => {
616                return Err(chain_client::Error::Conflict(certificate.hash()).into());
617            }
618            ClientOutcome::WaitForTimeout(_) => {}
619        }
620
621        // Start listening for notifications, so we learn about new rounds and blocks.
622        let (listener, _listen_handle, mut notification_stream) = client.listen().await?;
623        self.chain_listeners.spawn_task(listener);
624
625        loop {
626            // Try applying f. Return if committed.
627            let result = f(client).await;
628            self.update_wallet_from_client(client).await?;
629            let timeout = match result? {
630                ClientOutcome::Committed(t) => return Ok(t),
631                ClientOutcome::Conflict(certificate) => {
632                    return Err(chain_client::Error::Conflict(certificate.hash()).into());
633                }
634                ClientOutcome::WaitForTimeout(timeout) => timeout,
635            };
636            // Otherwise wait and try again in the next round.
637            util::wait_for_next_round(&mut notification_stream, timeout).await;
638        }
639    }
640
641    /// Returns the ownership configuration of the given chain.
642    pub async fn ownership(&mut self, chain_id: Option<ChainId>) -> Result<ChainOwnership, Error> {
643        let chain_id = chain_id.unwrap_or_else(|| self.default_chain());
644        let client = self.make_chain_client(chain_id).await?;
645        let info = client.chain_info().await?;
646        Ok(info.manager.ownership)
647    }
648
649    /// Changes the ownership configuration of the given chain.
650    pub async fn change_ownership(
651        &mut self,
652        chain_id: Option<ChainId>,
653        ownership_config: ChainOwnershipConfig,
654    ) -> Result<(), Error> {
655        let chain_id = chain_id.unwrap_or_else(|| self.default_chain());
656        let mut chain_client = self.make_chain_client(chain_id).await?;
657        info!(
658            ?ownership_config, %chain_id, preferred_owner=?chain_client.preferred_owner(),
659            "Changing ownership of a chain"
660        );
661        let time_start = Instant::now();
662        let mut ownership = chain_client.query_chain_ownership().await?;
663        ownership_config.update(&mut ownership)?;
664
665        if ownership.super_owners.is_empty() && ownership.owners.is_empty() {
666            tracing::error!("At least one owner or super owner of the chain has to be set.");
667            return Err(error::Inner::ChainOwnership.into());
668        }
669
670        let certificate = self
671            .apply_client_command(&chain_client, |chain_client| {
672                let ownership = ownership.clone();
673                let chain_client = chain_client.clone();
674                async move {
675                    chain_client
676                        .change_ownership(ownership)
677                        .await
678                        .map_err(Error::from)
679                        .context("Failed to change ownership")
680                }
681            })
682            .await?;
683        let time_total = time_start.elapsed();
684        info!("Operation confirmed after {} ms", time_total.as_millis());
685        debug!("{:?}", certificate);
686        self.maybe_auto_assign_preferred_owner(&mut chain_client, &ownership)
687            .await?;
688        Ok(())
689    }
690
691    /// Sets the preferred owner used to propose blocks on the given chain.
692    pub async fn set_preferred_owner(
693        &mut self,
694        chain_id: Option<ChainId>,
695        preferred_owner: AccountOwner,
696    ) -> Result<(), Error> {
697        let chain_id = chain_id.unwrap_or_else(|| self.default_chain());
698        let mut chain_client = self.make_chain_client(chain_id).await?;
699        let old_owner = chain_client.preferred_owner();
700        info!(%chain_id, ?old_owner, %preferred_owner, "Changing preferred owner for chain");
701        chain_client.set_preferred_owner(preferred_owner);
702        self.update_wallet_from_client(&chain_client).await?;
703        info!("New preferred owner set");
704        Ok(())
705    }
706
707    /// Checks that the validator's version info is compatible with the local version.
708    pub async fn check_compatible_version_info(
709        &self,
710        address: &str,
711        node: &impl ValidatorNode,
712    ) -> Result<VersionInfo, Error> {
713        match node.get_version_info().await {
714            Ok(version_info) if version_info.is_compatible_with(&linera_version::VERSION_INFO) => {
715                debug!(
716                    "Version information for validator {address}: {}",
717                    version_info
718                );
719                Ok(version_info)
720            }
721            Ok(version_info) => Err(error::Inner::UnexpectedVersionInfo {
722                remote: Box::new(version_info),
723                local: Box::new(linera_version::VERSION_INFO.clone()),
724            }
725            .into()),
726            Err(error) => Err(error::Inner::UnavailableVersionInfo {
727                address: address.to_string(),
728                error: Box::new(error),
729            }
730            .into()),
731        }
732    }
733
734    /// Checks that the validator's network description matches the local genesis config.
735    pub async fn check_matching_network_description(
736        &self,
737        address: &str,
738        node: &impl ValidatorNode,
739    ) -> Result<CryptoHash, Error> {
740        let network_description = self.genesis_config.network_description();
741        match node.get_network_description().await {
742            Ok(description) => {
743                if description == network_description {
744                    Ok(description.genesis_config_hash)
745                } else {
746                    Err(error::Inner::UnexpectedNetworkDescription {
747                        remote: Box::new(description),
748                        local: Box::new(network_description),
749                    }
750                    .into())
751                }
752            }
753            Err(error) => Err(error::Inner::UnavailableNetworkDescription {
754                address: address.to_string(),
755                error: Box::new(error),
756            }
757            .into()),
758        }
759    }
760
761    /// Queries a validator for the given chain's info and verifies its signature.
762    pub async fn check_validator_chain_info_response(
763        &self,
764        public_key: Option<&ValidatorPublicKey>,
765        address: &str,
766        node: &impl ValidatorNode,
767        chain_id: ChainId,
768    ) -> Result<ChainInfo, Error> {
769        let query = ChainInfoQuery::new(chain_id).with_manager_values();
770        match node.handle_chain_info_query(query).await {
771            Ok(response) => {
772                debug!(
773                    "Validator {address} sees chain {chain_id} at block height {} and epoch {:?}",
774                    response.info.next_block_height, response.info.epoch,
775                );
776                if let Some(public_key) = public_key {
777                    if response.check(*public_key).is_ok() {
778                        debug!("Signature for public key {public_key} is OK.");
779                    } else {
780                        return Err(error::Inner::InvalidSignature {
781                            public_key: *public_key,
782                        }
783                        .into());
784                    }
785                } else {
786                    warn!("Not checking signature as public key was not given");
787                }
788                Ok(*response.info)
789            }
790            Err(error) => Err(error::Inner::UnavailableChainInfo {
791                address: address.to_string(),
792                chain_id,
793                error: Box::new(error),
794            }
795            .into()),
796        }
797    }
798
799    /// Query a validator for version info, network description, and chain info.
800    ///
801    /// Returns a `ValidatorQueryResults` struct with the results of all three queries.
802    pub async fn query_validator(
803        &self,
804        address: &str,
805        node: &impl ValidatorNode,
806        chain_id: ChainId,
807        public_key: Option<&ValidatorPublicKey>,
808    ) -> ValidatorQueryResults {
809        let version_info = self.check_compatible_version_info(address, node).await;
810        let genesis_config_hash = self.check_matching_network_description(address, node).await;
811        let chain_info = self
812            .check_validator_chain_info_response(public_key, address, node, chain_id)
813            .await;
814
815        ValidatorQueryResults {
816            version_info,
817            genesis_config_hash,
818            chain_info,
819        }
820    }
821
822    /// Query the local node for version info, network description, and chain info.
823    ///
824    /// Returns a `ValidatorQueryResults` struct with the local node's information.
825    pub async fn query_local_node(
826        &self,
827        chain_id: ChainId,
828    ) -> Result<ValidatorQueryResults, Error> {
829        let version_info = Ok(linera_version::VERSION_INFO.clone());
830        let genesis_config_hash = Ok(self
831            .genesis_config
832            .network_description()
833            .genesis_config_hash);
834        let chain_info = self
835            .make_chain_client(chain_id)
836            .await?
837            .chain_info_with_manager_values()
838            .await
839            .map(|info| *info)
840            .map_err(|e| e.into());
841
842        Ok(ValidatorQueryResults {
843            version_info,
844            genesis_config_hash,
845            chain_info,
846        })
847    }
848}
849
850#[cfg(feature = "fs")]
851impl<Env: Environment> ClientContext<Env> {
852    /// Publishes a module from its contract and service bytecode files.
853    pub async fn publish_module(
854        &mut self,
855        chain_client: &ChainClient<Env>,
856        contract: PathBuf,
857        service: PathBuf,
858        vm_runtime: VmRuntime,
859        formats: Option<PathBuf>,
860    ) -> Result<ModuleId, Error> {
861        info!("Loading bytecode files");
862        let contract_bytecode = Bytecode::load_from_file(&contract).await.map_err(|e| {
863            std::io::Error::new(
864                e.kind(),
865                format!("failed to load contract bytecode from {contract:?}: {e}"),
866            )
867        })?;
868        let service_bytecode = Bytecode::load_from_file(&service).await.map_err(|e| {
869            std::io::Error::new(
870                e.kind(),
871                format!("failed to load service bytecode from {service:?}: {e}"),
872            )
873        })?;
874
875        let formats_bytes = match formats {
876            Some(path) => Some(bcs::to_bytes(&load_formats_from_snap(&path)?)?),
877            None => None,
878        };
879
880        info!("Publishing module");
881        let (blobs, module_id) = create_bytecode_blobs(
882            contract_bytecode,
883            service_bytecode,
884            vm_runtime,
885            formats_bytes,
886        )
887        .await;
888        let (module_id, _) = self
889            .apply_client_command(chain_client, |chain_client| {
890                let blobs = blobs.clone();
891                let chain_client = chain_client.clone();
892                async move {
893                    chain_client
894                        .publish_module_blobs(blobs, module_id)
895                        .await
896                        .context("Failed to publish module")
897                }
898            })
899            .await?;
900
901        info!("{}", "Module published successfully!");
902
903        info!("Synchronizing client and processing inbox");
904        self.process_inbox(chain_client).await?;
905        Ok(module_id)
906    }
907
908    /// Publishes a data blob loaded from the given file.
909    pub async fn publish_data_blob(
910        &mut self,
911        chain_client: &ChainClient<Env>,
912        blob_path: PathBuf,
913    ) -> Result<CryptoHash, Error> {
914        info!("Loading data blob file");
915        let blob_bytes = fs::read(&blob_path).map_err(|e| {
916            std::io::Error::new(
917                e.kind(),
918                format!("failed to load data blob bytes from {blob_path:?}: {e}"),
919            )
920        })?;
921
922        info!("Publishing data blob");
923        self.apply_client_command(chain_client, |chain_client| {
924            let blob_bytes = blob_bytes.clone();
925            let chain_client = chain_client.clone();
926            async move {
927                chain_client
928                    .publish_data_blob(blob_bytes)
929                    .await
930                    .context("Failed to publish data blob")
931            }
932        })
933        .await?;
934
935        info!("{}", "Data blob published successfully!");
936        Ok(CryptoHash::new(&BlobContent::new_data(blob_bytes)))
937    }
938
939    // TODO(#2490): Consider removing or renaming this.
940    /// Verifies that a data blob with the given hash is available.
941    pub async fn read_data_blob(
942        &mut self,
943        chain_client: &ChainClient<Env>,
944        hash: CryptoHash,
945    ) -> Result<(), Error> {
946        info!("Verifying data blob");
947        self.apply_client_command(chain_client, |chain_client| {
948            let chain_client = chain_client.clone();
949            async move {
950                chain_client
951                    .read_data_blob(hash)
952                    .await
953                    .context("Failed to verify data blob")
954            }
955        })
956        .await?;
957
958        info!("{}", "Data blob verified successfully!");
959        Ok(())
960    }
961}
962
963/// Reads an insta SNAP file containing a YAML-encoded `Formats` value and parses
964/// it. The caller BCS-serializes the result to obtain the application formats
965/// blob payload: BCS matches the documented intent (the blob is "the BCS
966/// serialization of an application's `Formats`") and the encoding the explorer
967/// decodes with.
968#[cfg(feature = "fs")]
969fn load_formats_from_snap(path: &std::path::Path) -> Result<linera_sdk::formats::Formats, Error> {
970    let content = fs::read_to_string(path).map_err(|e| {
971        std::io::Error::new(e.kind(), format!("failed to read SNAP file {path:?}: {e}"))
972    })?;
973    let body = strip_snap_frontmatter(&content).ok_or_else(|| {
974        std::io::Error::new(
975            std::io::ErrorKind::InvalidData,
976            format!("SNAP file {path:?} is missing the `---` frontmatter delimiters"),
977        )
978    })?;
979    let formats = serde_yaml_08::from_str(body).map_err(|e| {
980        std::io::Error::new(
981            std::io::ErrorKind::InvalidData,
982            format!("failed to parse SNAP body in {path:?} as Formats: {e}"),
983        )
984    })?;
985    Ok(formats)
986}
987
988#[cfg(feature = "fs")]
989fn strip_snap_frontmatter(content: &str) -> Option<&str> {
990    let rest = content.strip_prefix("---\n")?;
991    let end = rest.find("\n---\n")?;
992    Some(&rest[end + "\n---\n".len()..])
993}
994
995#[cfg(not(web))]
996impl<Env: Environment> ClientContext<Env> {
997    /// Prepares the chains and fungible tokens needed to run a benchmark.
998    pub async fn prepare_for_benchmark(
999        &mut self,
1000        num_chains: usize,
1001        tokens_per_chain: Amount,
1002        fungible_application_id: Option<ApplicationId>,
1003        pub_keys: Vec<AccountPublicKey>,
1004        chains_config_path: Option<&Path>,
1005        close_chains: bool,
1006    ) -> Result<Vec<ChainClient<Env>>, Error> {
1007        let start = Instant::now();
1008        // Below all block proposals are supposed to succeed without retries, we
1009        // must make sure that all incoming payments have been accepted on-chain
1010        // and that no validator is missing user certificates.
1011        self.process_inboxes_and_force_validator_updates().await;
1012        info!(
1013            "Processed inboxes and forced validator updates in {} ms",
1014            start.elapsed().as_millis()
1015        );
1016
1017        let start = Instant::now();
1018        let (benchmark_chains, chain_clients) = self
1019            .make_benchmark_chains(
1020                num_chains,
1021                tokens_per_chain,
1022                pub_keys,
1023                chains_config_path.is_some(),
1024                close_chains,
1025            )
1026            .await?;
1027        info!(
1028            "Got {} chains in {} ms",
1029            num_chains,
1030            start.elapsed().as_millis()
1031        );
1032
1033        if let Some(id) = fungible_application_id {
1034            let start = Instant::now();
1035            self.supply_fungible_tokens(&benchmark_chains, id).await?;
1036            info!(
1037                "Supplied fungible tokens in {} ms",
1038                start.elapsed().as_millis()
1039            );
1040            // Need to process inboxes to make sure the chains receive the supplied tokens.
1041            let start = Instant::now();
1042            for chain_client in &chain_clients {
1043                chain_client.process_inbox().await?;
1044            }
1045            info!(
1046                "Processed inboxes after supplying fungible tokens in {} ms",
1047                start.elapsed().as_millis()
1048            );
1049        }
1050
1051        let all_chains = Benchmark::<Env>::get_all_chains(chains_config_path, &benchmark_chains)?;
1052        let known_chain_ids: HashSet<_> = benchmark_chains.iter().map(|(id, _)| *id).collect();
1053        let unknown_chain_ids: Vec<_> = all_chains
1054            .iter()
1055            .filter(|id| !known_chain_ids.contains(id))
1056            .copied()
1057            .collect();
1058        if !unknown_chain_ids.is_empty() {
1059            // The current client won't have the blobs for the chains in the other wallets. Even
1060            // though it will eventually get those blobs, we're getting a head start here and
1061            // fetching those blobs in advance.
1062            for chain_id in &unknown_chain_ids {
1063                self.client.get_chain_description(*chain_id).await?;
1064            }
1065        }
1066
1067        Ok(chain_clients)
1068    }
1069
1070    /// Closes the benchmark chains, or processes their inboxes and updates the wallet.
1071    pub async fn wrap_up_benchmark(
1072        &mut self,
1073        chain_clients: Vec<ChainClient<Env>>,
1074        close_chains: bool,
1075        wrap_up_max_in_flight: usize,
1076    ) -> Result<(), Error> {
1077        if close_chains {
1078            info!("Closing chains...");
1079            let stream = stream::iter(chain_clients)
1080                .map(|chain_client| async move {
1081                    Benchmark::<Env>::close_benchmark_chain(&chain_client).await?;
1082                    info!("Closed chain {:?}", chain_client.chain_id());
1083                    Ok::<(), BenchmarkError>(())
1084                })
1085                .buffer_unordered(wrap_up_max_in_flight);
1086            stream.try_collect::<Vec<_>>().await?;
1087        } else {
1088            info!("Processing inbox for all chains...");
1089            let stream = stream::iter(chain_clients.clone())
1090                .map(|chain_client| async move {
1091                    chain_client.process_inbox().await?;
1092                    info!("Processed inbox for chain {:?}", chain_client.chain_id());
1093                    Ok::<(), chain_client::Error>(())
1094                })
1095                .buffer_unordered(wrap_up_max_in_flight);
1096            stream.try_collect::<Vec<_>>().await?;
1097
1098            info!("Updating wallet from chain clients...");
1099            for chain_client in chain_clients {
1100                let info = chain_client.chain_info().await?;
1101                let client_owner = chain_client.preferred_owner();
1102                let pending_fast_proposal = chain_client
1103                    .pending_proposal()
1104                    .await
1105                    .filter(|p| p.round.is_some_and(|r| r.is_fast()));
1106                self.wallet()
1107                    .insert(
1108                        info.chain_id,
1109                        wallet::Chain {
1110                            pending_fast_proposal,
1111                            owner: client_owner,
1112                            ..info.as_ref().into()
1113                        },
1114                    )
1115                    .await
1116                    .map_err(error::Inner::wallet)?;
1117            }
1118        }
1119
1120        Ok(())
1121    }
1122
1123    async fn process_inboxes_and_force_validator_updates(&mut self) {
1124        let mut join_set = task::JoinSet::new();
1125
1126        let chain_clients: Vec<_> = self
1127            .wallet()
1128            .owned_chain_ids()
1129            .map_err(|e| error::Inner::wallet(e).into())
1130            .and_then(|id| self.make_chain_client(id))
1131            .try_collect()
1132            .await
1133            .unwrap();
1134
1135        for chain_client in chain_clients {
1136            join_set.spawn(async move {
1137                Self::process_inbox_without_updating_wallet(&chain_client)
1138                    .await
1139                    .expect("Processing inbox should not fail!");
1140                chain_client
1141            });
1142        }
1143
1144        for chain_client in join_set.join_all().await {
1145            self.update_wallet_from_client(&chain_client).await.unwrap();
1146        }
1147    }
1148
1149    async fn process_inbox_without_updating_wallet(
1150        chain_client: &ChainClient<Env>,
1151    ) -> Result<Vec<ConfirmedBlockCertificate>, Error> {
1152        // Try processing the inbox optimistically without waiting for validator notifications.
1153        chain_client.synchronize_from_validators().await?;
1154        let (certificates, maybe_timeout) = chain_client.process_inbox_without_prepare().await?;
1155        assert!(
1156            maybe_timeout.is_none(),
1157            "Should not timeout within benchmark!"
1158        );
1159
1160        Ok(certificates)
1161    }
1162
1163    /// Creates chains if necessary, and returns a map of exactly `num_chains` chain IDs
1164    /// with key pairs, as well as a map of the chain clients.
1165    ///
1166    /// If `close_chains` is true, chains are not looked up from or stored in the wallet,
1167    /// since they will be closed after the benchmark and shouldn't be reused.
1168    async fn make_benchmark_chains(
1169        &mut self,
1170        num_chains: usize,
1171        balance: Amount,
1172        pub_keys: Vec<AccountPublicKey>,
1173        wallet_only: bool,
1174        close_chains: bool,
1175    ) -> Result<(Vec<(ChainId, AccountOwner)>, Vec<ChainClient<Env>>), Error> {
1176        let mut chains_found_in_wallet = 0;
1177        let mut benchmark_chains = Vec::with_capacity(num_chains);
1178        let mut chain_clients = Vec::with_capacity(num_chains);
1179        let start = Instant::now();
1180
1181        // When close_chains is true and we're creating our own chains (not wallet_only),
1182        // skip wallet lookup to avoid picking up existing chains that would then be closed.
1183        // When wallet_only is true, chains were pre-created by the parent process and must
1184        // be read from the wallet.
1185        if !close_chains || wallet_only {
1186            let mut owned_chain_ids = std::pin::pin!(self.wallet().owned_chain_ids());
1187            while let Some(chain_id) = owned_chain_ids.next().await {
1188                let chain_id = chain_id.map_err(error::Inner::wallet)?;
1189                if chains_found_in_wallet == num_chains {
1190                    break;
1191                }
1192                let chain_client = self.make_chain_client(chain_id).await?;
1193                let ownership = chain_client.chain_info().await?.manager.ownership;
1194                if !ownership.owners.is_empty() || ownership.super_owners.len() != 1 {
1195                    continue;
1196                }
1197                let owner = *ownership.super_owners.first().unwrap();
1198                chain_client.process_inbox().await?;
1199                benchmark_chains.push((chain_id, owner));
1200                chain_clients.push(chain_client);
1201                chains_found_in_wallet += 1;
1202            }
1203            info!(
1204                "Got {} chains from the wallet in {} ms",
1205                benchmark_chains.len(),
1206                start.elapsed().as_millis()
1207            );
1208        }
1209
1210        let num_chains_to_create = num_chains - chains_found_in_wallet;
1211
1212        let default_chain_client = self.make_chain_client(self.default_chain()).await?;
1213
1214        if num_chains_to_create > 0 {
1215            if wallet_only {
1216                return Err(
1217                    error::Inner::Benchmark(BenchmarkError::NotEnoughChainsInWallet(
1218                        num_chains,
1219                        chains_found_in_wallet,
1220                    ))
1221                    .into(),
1222                );
1223            }
1224            let mut pub_keys_iter = pub_keys.into_iter().take(num_chains_to_create);
1225            let operations_per_block = 900; // Over this we seem to hit the block size limits.
1226            for i in (0..num_chains_to_create).step_by(operations_per_block) {
1227                let num_new_chains = operations_per_block.min(num_chains_to_create - i);
1228                // Each chain gets its own unique owner (previously all chains in a batch
1229                // shared one owner, which could cause conflicts during benchmarking).
1230                let owners: Vec<AccountOwner> = (&mut pub_keys_iter)
1231                    .take(num_new_chains)
1232                    .map(|pk| pk.into())
1233                    .collect();
1234
1235                let certificate = Self::execute_open_chains_operations(
1236                    &default_chain_client,
1237                    balance,
1238                    owners.clone(),
1239                )
1240                .await?;
1241                info!("Block executed successfully");
1242
1243                let block = certificate.block();
1244                for (i, owner) in owners.into_iter().enumerate() {
1245                    let chain_id = block.body.blobs[i]
1246                        .iter()
1247                        .find(|blob| blob.id().blob_type == BlobType::ChainDescription)
1248                        .map(|blob| ChainId(blob.id().hash))
1249                        .expect("failed to create a new chain");
1250                    self.client
1251                        .extend_chain_mode(chain_id, ListeningMode::FullChain);
1252
1253                    let mut chain_client = self.client.create_chain_client(
1254                        chain_id,
1255                        None,
1256                        BlockHeight::ZERO,
1257                        &None,
1258                        Some(owner),
1259                        self.timing_sender(),
1260                        false,
1261                    );
1262                    chain_client.set_preferred_owner(owner);
1263                    chain_client.process_inbox().await?;
1264                    benchmark_chains.push((chain_id, owner));
1265                    chain_clients.push(chain_client);
1266                }
1267            }
1268
1269            info!(
1270                "Created {} chains in {} ms",
1271                num_chains_to_create,
1272                start.elapsed().as_millis()
1273            );
1274        }
1275
1276        // Only update wallet if chains will be reused (not closed after benchmark)
1277        if !close_chains {
1278            info!("Updating wallet from client");
1279            self.update_wallet_from_client(&default_chain_client)
1280                .await?;
1281        }
1282        info!("Retrying pending outgoing messages");
1283        default_chain_client
1284            .retry_pending_outgoing_messages()
1285            .await
1286            .context("outgoing messages to create the new chains should be delivered")?;
1287        info!("Processing default chain inbox");
1288        default_chain_client.process_inbox().await?;
1289
1290        assert_eq!(
1291            benchmark_chains.len(),
1292            chain_clients.len(),
1293            "benchmark_chains and chain_clients must have the same size"
1294        );
1295
1296        Ok((benchmark_chains, chain_clients))
1297    }
1298
1299    async fn execute_open_chains_operations(
1300        chain_client: &ChainClient<Env>,
1301        balance: Amount,
1302        owners: Vec<AccountOwner>,
1303    ) -> Result<ConfirmedBlockCertificate, Error> {
1304        let operations: Vec<_> = owners
1305            .iter()
1306            .map(|owner| {
1307                let config = OpenChainConfig {
1308                    ownership: ChainOwnership::single_super(*owner),
1309                    balance,
1310                    application_permissions: Default::default(),
1311                };
1312                Operation::system(SystemOperation::OpenChain(config))
1313            })
1314            .collect();
1315        info!("Executing {} OpenChain operations", operations.len());
1316        Ok(chain_client
1317            .execute_operations(operations, vec![])
1318            .await?
1319            .expect("should execute block with OpenChain operations"))
1320    }
1321
1322    /// Supplies fungible tokens to the chains.
1323    async fn supply_fungible_tokens(
1324        &mut self,
1325        key_pairs: &[(ChainId, AccountOwner)],
1326        application_id: ApplicationId,
1327    ) -> Result<(), Error> {
1328        let default_chain_id = self.default_chain();
1329        let default_key = self
1330            .wallet()
1331            .get(default_chain_id)
1332            .await
1333            .unwrap()
1334            .unwrap()
1335            .owner
1336            .unwrap();
1337        // This should be enough to run the benchmark at 1M TPS for an hour.
1338        let amount = Amount::from_nanos(4);
1339        let operations: Vec<Operation> = key_pairs
1340            .iter()
1341            .map(|(chain_id, owner)| {
1342                fungible_transfer(application_id, *chain_id, default_key, *owner, amount)
1343            })
1344            .collect();
1345        let chain_client = self.make_chain_client(default_chain_id).await?;
1346        // Put at most 1000 fungible token operations in each block.
1347        for operation_chunk in operations.chunks(1000) {
1348            chain_client
1349                .execute_operations(operation_chunk.to_vec(), vec![])
1350                .await?
1351                .expect("should execute block with Transfer operations");
1352        }
1353        self.update_wallet_from_client(&chain_client).await?;
1354
1355        Ok(())
1356    }
1357}