1use 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
67pub struct ValidatorQueryResults {
69 pub version_info: Result<VersionInfo, Error>,
71 pub genesis_config_hash: Result<CryptoHash, Error>,
73 pub chain_info: Result<ChainInfo, Error>,
75}
76
77impl ValidatorQueryResults {
78 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 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
211pub struct ClientContext<Env: Environment> {
214 pub client: Arc<Client<Env>>,
216 pub genesis_config: crate::config::GenesisConfig,
219 pub send_timeout: Duration,
221 pub recv_timeout: Duration,
223 pub retry_delay: Duration,
225 pub max_retries: u32,
227 pub max_backoff: Duration,
229 pub chain_listeners: JoinSet,
231 pub default_chain: Option<ChainId>,
234 #[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 #[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 pub fn wallet(&self) -> &Env::Wallet {
381 self.client.wallet()
382 }
383
384 pub fn admin_chain_id(&self) -> ChainId {
386 self.client.admin_chain_id()
387 }
388
389 pub fn default_account(&self) -> Account {
392 Account::chain(self.default_chain())
393 }
394
395 pub fn default_chain(&self) -> ChainId {
397 self.default_chain
398 .expect("default chain requested but none set")
399 }
400
401 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 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 #[cfg(not(web))]
435 pub fn client_metrics(&self) -> Option<&ClientMetrics> {
436 self.client_metrics.as_ref()
437 }
438
439 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 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 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 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 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 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 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 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 let modified = self
572 .wallet()
573 .modify(chain_id, |chain| chain.owner = Some(owner))
574 .await
575 .map_err(error::Inner::wallet)?;
576 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 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 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 let (listener, _listen_handle, mut notification_stream) = client.listen().await?;
623 self.chain_listeners.spawn_task(listener);
624
625 loop {
626 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 util::wait_for_next_round(&mut notification_stream, timeout).await;
638 }
639 }
640
641 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 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 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 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 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 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 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; 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 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 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 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 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 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}