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