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 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
66pub struct ValidatorQueryResults {
68 pub version_info: Result<VersionInfo, Error>,
70 pub genesis_config_hash: Result<CryptoHash, Error>,
72 pub chain_info: Result<ChainInfo, Error>,
74}
75
76impl ValidatorQueryResults {
77 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 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
210pub struct ClientContext<Env: Environment> {
213 pub client: Arc<Client<Env>>,
215 pub genesis_config: crate::config::GenesisConfig,
218 pub send_timeout: Duration,
220 pub recv_timeout: Duration,
222 pub retry_delay: Duration,
224 pub max_retries: u32,
226 pub max_backoff: Duration,
228 pub chain_listeners: JoinSet,
230 pub default_chain: Option<ChainId>,
233 #[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 #[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 pub fn wallet(&self) -> &Env::Wallet {
380 self.client.wallet()
381 }
382
383 pub fn admin_chain_id(&self) -> ChainId {
385 self.client.admin_chain_id()
386 }
387
388 pub fn default_account(&self) -> Account {
391 Account::chain(self.default_chain())
392 }
393
394 pub fn default_chain(&self) -> ChainId {
396 self.default_chain
397 .expect("default chain requested but none set")
398 }
399
400 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 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 #[cfg(not(web))]
434 pub fn client_metrics(&self) -> Option<&ClientMetrics> {
435 self.client_metrics.as_ref()
436 }
437
438 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 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 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 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 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 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 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 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 let modified = self
571 .wallet()
572 .modify(chain_id, |chain| chain.owner = Some(owner))
573 .await
574 .map_err(error::Error::wallet)?;
575 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 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 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 let (listener, _listen_handle, mut notification_stream) = client.listen().await?;
621 self.chain_listeners.spawn_task(listener);
622
623 loop {
624 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 util::wait_for_next_round(&mut notification_stream, timeout).await;
636 }
637 }
638
639 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 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 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 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 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 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 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; 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 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 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 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 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 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}