1use std::{
9 collections::HashMap,
10 io,
11 path::{Path, PathBuf},
12 sync::Arc,
13};
14
15use cargo_toml::Manifest;
16use futures::future;
17use linera_base::{
18 crypto::{AccountPublicKey, AccountSecretKey},
19 data_types::{
20 Amount, ApplicationDescription, Blob, BlockHeight, Bytecode, ChainDescription,
21 CompressedBytecode, Epoch,
22 },
23 identifiers::{AccountOwner, ApplicationId, ChainId, ModuleId, OwnerSpender},
24 vm::VmRuntime,
25};
26use linera_chain::{types::ConfirmedBlockCertificate, ChainExecutionContext};
27use linera_core::{data_types::ChainInfoQuery, worker::WorkerError};
28use linera_execution::{
29 system::{SystemOperation, SystemQuery, SystemResponse},
30 ExecutionError, Operation, Query, QueryOutcome, QueryResponse, ResourceTracker,
31};
32use linera_storage::Storage as _;
33use serde::Serialize;
34use tokio::{fs, sync::Mutex};
35
36use super::{BlockBuilder, TestValidator};
37use crate::{abis::fungible::FungibleTokenAbi, ContractAbi, ServiceAbi};
38
39pub struct ActiveChain {
41 key_pair: AccountSecretKey,
42 description: ChainDescription,
43 tip: Arc<Mutex<Option<ConfirmedBlockCertificate>>>,
44 validator: TestValidator,
45}
46
47impl Clone for ActiveChain {
48 fn clone(&self) -> Self {
49 ActiveChain {
50 key_pair: self.key_pair.copy(),
51 description: self.description.clone(),
52 tip: self.tip.clone(),
53 validator: self.validator.clone(),
54 }
55 }
56}
57
58impl ActiveChain {
59 pub fn new(
65 key_pair: AccountSecretKey,
66 description: ChainDescription,
67 validator: TestValidator,
68 ) -> Self {
69 ActiveChain {
70 key_pair,
71 description,
72 tip: Arc::default(),
73 validator,
74 }
75 }
76
77 pub fn id(&self) -> ChainId {
79 self.description.id()
80 }
81
82 pub fn public_key(&self) -> AccountPublicKey {
84 self.key_pair.public()
85 }
86
87 pub fn key_pair(&self) -> &AccountSecretKey {
89 &self.key_pair
90 }
91
92 pub fn set_key_pair(&mut self, key_pair: AccountSecretKey) {
94 self.key_pair = key_pair
95 }
96
97 pub async fn epoch(&self) -> Epoch {
99 *Box::pin(self.validator.worker().chain_state_view(self.id()))
100 .await
101 .expect("Failed to load chain")
102 .execution_state
103 .system
104 .epoch
105 .get()
106 }
107
108 pub async fn chain_balance(&self) -> Amount {
110 let query = Query::System(SystemQuery);
111
112 let (QueryOutcome { response, .. }, _) = self
113 .validator
114 .worker()
115 .query_application(self.id(), query, None)
116 .await
117 .expect("Failed to query chain's balance");
118
119 let QueryResponse::System(SystemResponse { balance, .. }) = response else {
120 panic!("Unexpected response from system application");
121 };
122
123 balance
124 }
125
126 pub async fn owner_balance(&self, owner: &AccountOwner) -> Option<Amount> {
128 let chain_state = Box::pin(self.validator.worker().chain_state_view(self.id()))
129 .await
130 .expect("Failed to read chain state");
131
132 chain_state
133 .execution_state
134 .system
135 .balances
136 .get(owner)
137 .await
138 .expect("Failed to read owner balance")
139 }
140
141 pub async fn owner_balances(
143 &self,
144 owners: impl IntoIterator<Item = AccountOwner>,
145 ) -> HashMap<AccountOwner, Option<Amount>> {
146 let chain_state = Box::pin(self.validator.worker().chain_state_view(self.id()))
147 .await
148 .expect("Failed to read chain state");
149
150 let mut balances = HashMap::new();
151
152 for owner in owners {
153 let balance = chain_state
154 .execution_state
155 .system
156 .balances
157 .get(&owner)
158 .await
159 .expect("Failed to read an owner's balance");
160
161 balances.insert(owner, balance);
162 }
163
164 balances
165 }
166
167 pub async fn accounts(&self) -> Vec<AccountOwner> {
169 let chain_state = Box::pin(self.validator.worker().chain_state_view(self.id()))
170 .await
171 .expect("Failed to read chain state");
172
173 chain_state
174 .execution_state
175 .system
176 .balances
177 .indices()
178 .await
179 .expect("Failed to list accounts on the chain")
180 }
181
182 pub async fn all_owner_balances(&self) -> HashMap<AccountOwner, Amount> {
184 self.owner_balances(self.accounts().await)
185 .await
186 .into_iter()
187 .map(|(owner, balance)| {
188 (
189 owner,
190 balance.expect("`accounts` should only return accounts with non-zero balance"),
191 )
192 })
193 .collect()
194 }
195
196 pub async fn add_block(
203 &self,
204 block_builder: impl FnOnce(&mut BlockBuilder),
205 ) -> (ConfirmedBlockCertificate, ResourceTracker) {
206 self.try_add_block(block_builder)
207 .await
208 .expect("Failed to execute block.")
209 }
210
211 pub async fn add_block_with_blobs(
218 &self,
219 block_builder: impl FnOnce(&mut BlockBuilder),
220 blobs: Vec<Blob>,
221 ) -> (ConfirmedBlockCertificate, ResourceTracker) {
222 self.try_add_block_with_blobs(block_builder, blobs)
223 .await
224 .expect("Failed to execute block.")
225 }
226
227 pub async fn try_add_block(
234 &self,
235 block_builder: impl FnOnce(&mut BlockBuilder),
236 ) -> Result<(ConfirmedBlockCertificate, ResourceTracker), WorkerError> {
237 self.try_add_block_with_blobs(block_builder, vec![]).await
238 }
239
240 async fn try_add_block_with_blobs(
250 &self,
251 block_builder: impl FnOnce(&mut BlockBuilder),
252 blobs: Vec<Blob>,
253 ) -> Result<(ConfirmedBlockCertificate, ResourceTracker), WorkerError> {
254 let mut tip = self.tip.lock().await;
255 let mut block = BlockBuilder::new(
256 self.description.id(),
257 self.key_pair.public().into(),
258 Box::pin(self.epoch()).await,
259 tip.as_ref(),
260 self.validator.clone(),
261 );
262
263 block_builder(&mut block);
264
265 let (certificate, resource_tracker) = Box::pin(block.try_sign(&blobs)).await?;
267
268 let result = self
269 .validator
270 .worker()
271 .fully_handle_certificate_with_notifications(certificate.clone(), &())
272 .await;
273 if let Err(WorkerError::BlobsNotFound(_)) = &result {
274 self.validator.storage().maybe_write_blobs(&blobs).await?;
275 self.validator
276 .worker()
277 .fully_handle_certificate_with_notifications(certificate.clone(), &())
278 .await
279 .expect("Rejected certificate");
280 } else {
281 result.expect("Rejected certificate");
282 }
283
284 *tip = Some(certificate.clone());
285
286 Ok((certificate, resource_tracker))
287 }
288
289 pub async fn handle_received_messages(
297 &self,
298 ) -> Option<(ConfirmedBlockCertificate, ResourceTracker)> {
299 let chain_id = self.id();
300 let information = self
301 .validator
302 .worker()
303 .handle_chain_info_query(ChainInfoQuery::new(chain_id).with_pending_message_bundles())
304 .await
305 .expect("Failed to query chain's pending messages");
306 let messages = information.info.requested_pending_message_bundles;
307 if messages.is_empty() {
310 return None;
311 }
312 let result = Box::pin(self.add_block(|block| {
313 block.with_incoming_bundles(messages);
314 }))
315 .await;
316 Some(result)
317 }
318
319 pub async fn handle_new_events(&self) -> (ConfirmedBlockCertificate, ResourceTracker) {
325 let chain_id = self.id();
326 let worker = self.validator.worker();
327 let subscription_map = Box::pin(worker.chain_state_view(chain_id))
328 .await
329 .expect("Failed to query chain state view")
330 .execution_state
331 .system
332 .event_subscriptions
333 .index_values()
334 .await
335 .expect("Failed to query chain's event subscriptions");
336 let futures = subscription_map
337 .into_iter()
338 .map(|((chain_id, stream_id), subscriptions)| {
339 let worker = worker.clone();
340 async move {
341 let counts = Box::pin(worker.chain_state_view(chain_id))
342 .await
343 .expect("Failed to query chain state view")
344 .next_expected_events
345 .get(&stream_id)
346 .await
347 .expect("Failed to query chain's event indices");
348 let Some(counts) =
349 counts.filter(|counts| counts.next_index > subscriptions.min_next_index)
350 else {
351 return Vec::new();
352 };
353 let first_index = counts.first_index;
354 let next_index = counts.next_index;
355 subscriptions
356 .applications
357 .into_iter()
358 .filter(|(_, app_index)| *app_index < next_index)
359 .map(|(application_id, _)| SystemOperation::UpdateStream {
360 application_id,
361 chain_id,
362 stream_id: stream_id.clone(),
363 first_index,
364 next_index,
365 })
366 .collect::<Vec<_>>()
367 }
368 });
369 let updates: Vec<SystemOperation> = future::join_all(futures)
370 .await
371 .into_iter()
372 .flatten()
373 .collect();
374 assert!(!updates.is_empty(), "No new events to process");
375
376 Box::pin(self.add_block(|block| {
377 for update in updates {
378 block.with_system_operation(update);
379 }
380 }))
381 .await
382 }
383
384 pub async fn publish_current_module<Abi, Parameters, InstantiationArgument>(
390 &self,
391 ) -> ModuleId<Abi, Parameters, InstantiationArgument> {
392 Box::pin(self.publish_bytecode_files_in(".")).await
393 }
394
395 pub async fn publish_bytecode_files_in<Abi, Parameters, InstantiationArgument>(
401 &self,
402 repository_path: impl AsRef<Path>,
403 ) -> ModuleId<Abi, Parameters, InstantiationArgument> {
404 let repository_path = fs::canonicalize(repository_path)
405 .await
406 .expect("Failed to obtain absolute application repository path");
407 Self::build_bytecode_files_in(&repository_path);
408 let (contract, service) = Self::find_compressed_bytecode_files_in(&repository_path).await;
409 let contract_blob = Blob::new_contract_bytecode(contract);
410 let service_blob = Blob::new_service_bytecode(service);
411 let contract_blob_hash = contract_blob.id().hash;
412 let service_blob_hash = service_blob.id().hash;
413 let vm_runtime = VmRuntime::Wasm;
414
415 let module_id = ModuleId::new(contract_blob_hash, service_blob_hash, vm_runtime);
416
417 let (certificate, _) = Box::pin(self.add_block_with_blobs(
418 |block| {
419 block.with_system_operation(SystemOperation::PublishModule { module_id });
420 },
421 vec![contract_blob, service_blob],
422 ))
423 .await;
424
425 let block = certificate.inner().block();
426 assert_eq!(block.messages().len(), 1);
427 assert_eq!(block.messages()[0].len(), 0);
428
429 module_id.with_abi()
430 }
431
432 pub fn build_bytecode_files_in(repository: &Path) {
434 let output = std::process::Command::new("cargo")
435 .args(["build", "--release", "--target", "wasm32-unknown-unknown"])
436 .current_dir(repository)
437 .output()
438 .expect("Failed to build Wasm binaries");
439
440 assert!(
441 output.status.success(),
442 "Failed to build bytecode binaries.\nstdout: {}\nstderr: {}",
443 String::from_utf8_lossy(&output.stdout),
444 String::from_utf8_lossy(&output.stderr)
445 );
446 }
447
448 pub async fn find_bytecode_files_in(repository: &Path) -> (Bytecode, Bytecode) {
454 let manifest_path = repository.join("Cargo.toml");
455 let cargo_manifest =
456 Manifest::from_path(manifest_path).expect("Failed to load Cargo.toml manifest");
457
458 let binaries = cargo_manifest
459 .bin
460 .into_iter()
461 .filter_map(|binary| binary.name)
462 .filter(|name| name.ends_with("service") || name.ends_with("contract"))
463 .collect::<Vec<_>>();
464
465 assert_eq!(
466 binaries.len(),
467 2,
468 "Could not figure out contract and service bytecode binaries.\
469 Please specify them manually using `publish_module`."
470 );
471
472 let (contract_binary, service_binary) = if binaries[0].ends_with("contract") {
473 (&binaries[0], &binaries[1])
474 } else {
475 (&binaries[1], &binaries[0])
476 };
477
478 let base_path = Self::find_output_directory_of(repository)
479 .await
480 .expect("Failed to look for output binaries");
481 let contract_path = base_path.join(format!("{contract_binary}.wasm"));
482 let service_path = base_path.join(format!("{service_binary}.wasm"));
483
484 let contract = Bytecode::load_from_file(contract_path)
485 .await
486 .expect("Failed to load contract bytecode from file");
487 let service = Bytecode::load_from_file(service_path)
488 .await
489 .expect("Failed to load service bytecode from file");
490 (contract, service)
491 }
492
493 pub async fn find_compressed_bytecode_files_in(
496 repository: &Path,
497 ) -> (CompressedBytecode, CompressedBytecode) {
498 let (contract, service) = Self::find_bytecode_files_in(repository).await;
499 tokio::task::spawn_blocking(move || (contract.compress(), service.compress()))
500 .await
501 .expect("Failed to compress bytecode files")
502 }
503
504 async fn find_output_directory_of(repository: &Path) -> Result<PathBuf, io::Error> {
511 let output_sub_directory = Path::new("target/wasm32-unknown-unknown/release");
512 let mut current_directory = repository;
513 let mut output_path = current_directory.join(output_sub_directory);
514
515 while !fs::try_exists(&output_path).await? {
516 current_directory = current_directory.parent().unwrap_or_else(|| {
517 panic!(
518 "Failed to find Wasm binary output directory in {}",
519 repository.display()
520 )
521 });
522
523 output_path = current_directory.join(output_sub_directory);
524 }
525
526 Ok(output_path)
527 }
528
529 pub async fn get_tip_height(&self) -> BlockHeight {
531 self.tip
532 .lock()
533 .await
534 .as_ref()
535 .expect("Block was not successfully added")
536 .inner()
537 .block()
538 .header
539 .height
540 }
541
542 pub async fn create_application<Abi, Parameters, InstantiationArgument>(
553 &mut self,
554 module_id: ModuleId<Abi, Parameters, InstantiationArgument>,
555 parameters: Parameters,
556 instantiation_argument: InstantiationArgument,
557 required_application_ids: Vec<ApplicationId>,
558 ) -> ApplicationId<Abi>
559 where
560 Abi: ContractAbi,
561 Parameters: Serialize,
562 InstantiationArgument: Serialize,
563 {
564 let parameters = serde_json::to_vec(¶meters).unwrap();
565 let instantiation_argument = serde_json::to_vec(&instantiation_argument).unwrap();
566
567 let (creation_certificate, _) = Box::pin(self.add_block(|block| {
568 block.with_system_operation(SystemOperation::CreateApplication {
569 module_id: module_id.forget_abi(),
570 parameters: parameters.clone(),
571 instantiation_argument,
572 required_application_ids: required_application_ids.clone(),
573 });
574 }))
575 .await;
576
577 let block = creation_certificate.inner().block();
578 assert_eq!(block.messages().len(), 1);
579
580 let description = ApplicationDescription {
581 module_id: module_id.forget_abi(),
582 creator_chain_id: block.header.chain_id,
583 block_height: block.header.height,
584 application_index: 0,
585 parameters,
586 required_application_ids,
587 };
588
589 ApplicationId::<()>::from(&description).with_abi()
590 }
591
592 pub async fn try_create_application<Abi, Parameters, InstantiationArgument>(
596 &mut self,
597 module_id: ModuleId<Abi, Parameters, InstantiationArgument>,
598 parameters: Parameters,
599 instantiation_argument: InstantiationArgument,
600 required_application_ids: Vec<ApplicationId>,
601 ) -> Result<ApplicationId<Abi>, WorkerError>
602 where
603 Abi: ContractAbi,
604 Parameters: Serialize,
605 InstantiationArgument: Serialize,
606 {
607 let parameters = serde_json::to_vec(¶meters).unwrap();
608 let instantiation_argument = serde_json::to_vec(&instantiation_argument).unwrap();
609
610 let (creation_certificate, _) = self
611 .try_add_block(|block| {
612 block.with_system_operation(SystemOperation::CreateApplication {
613 module_id: module_id.forget_abi(),
614 parameters: parameters.clone(),
615 instantiation_argument,
616 required_application_ids: required_application_ids.clone(),
617 });
618 })
619 .await?;
620
621 let block = creation_certificate.inner().block();
622
623 let description = ApplicationDescription {
624 module_id: module_id.forget_abi(),
625 creator_chain_id: block.header.chain_id,
626 block_height: block.header.height,
627 application_index: 0,
628 parameters,
629 required_application_ids,
630 };
631
632 Ok(ApplicationId::<()>::from(&description).with_abi())
633 }
634
635 pub async fn is_closed(&self) -> bool {
637 let chain = Box::pin(self.validator.worker().chain_state_view(self.id()))
638 .await
639 .expect("Failed to load chain");
640 *chain.execution_state.system.closed.get()
641 }
642
643 pub async fn query<Abi>(
647 &self,
648 application_id: ApplicationId<Abi>,
649 query: Abi::Query,
650 ) -> QueryOutcome<Abi::QueryResponse>
651 where
652 Abi: ServiceAbi,
653 {
654 self.try_query(application_id, query)
655 .await
656 .expect("Failed to execute application service query")
657 }
658
659 pub async fn try_query<Abi>(
663 &self,
664 application_id: ApplicationId<Abi>,
665 query: Abi::Query,
666 ) -> Result<QueryOutcome<Abi::QueryResponse>, TryQueryError>
667 where
668 Abi: ServiceAbi,
669 {
670 let query_bytes = serde_json::to_vec(&query)?;
671
672 let (
673 QueryOutcome {
674 response,
675 operations,
676 },
677 _,
678 ) = self
679 .validator
680 .worker()
681 .query_application(
682 self.id(),
683 Query::User {
684 application_id: application_id.forget_abi(),
685 bytes: query_bytes,
686 },
687 None,
688 )
689 .await?;
690
691 let deserialized_response = match response {
692 QueryResponse::User(bytes) => {
693 serde_json::from_slice(&bytes).expect("Failed to deserialize query response")
694 }
695 QueryResponse::System(_) => {
696 unreachable!("User query returned a system response")
697 }
698 };
699
700 Ok(QueryOutcome {
701 response: deserialized_response,
702 operations,
703 })
704 }
705
706 pub async fn graphql_query<Abi>(
710 &self,
711 application_id: ApplicationId<Abi>,
712 query: impl Into<async_graphql::Request>,
713 ) -> QueryOutcome<serde_json::Value>
714 where
715 Abi: ServiceAbi<Query = async_graphql::Request, QueryResponse = async_graphql::Response>,
716 {
717 let query = query.into();
718 let query_str = query.query.clone();
719
720 self.try_graphql_query(application_id, query)
721 .await
722 .unwrap_or_else(|error| panic!("Service query {query_str:?} failed: {error}"))
723 }
724
725 pub async fn try_graphql_query<Abi>(
729 &self,
730 application_id: ApplicationId<Abi>,
731 query: impl Into<async_graphql::Request>,
732 ) -> Result<QueryOutcome<serde_json::Value>, TryGraphQLQueryError>
733 where
734 Abi: ServiceAbi<Query = async_graphql::Request, QueryResponse = async_graphql::Response>,
735 {
736 let query = query.into();
737 let QueryOutcome {
738 response,
739 operations,
740 } = self.try_query(application_id, query).await?;
741
742 if !response.errors.is_empty() {
743 return Err(TryGraphQLQueryError::Service(response.errors));
744 }
745 let json_response = response.data.into_json()?;
746
747 Ok(QueryOutcome {
748 response: json_response,
749 operations,
750 })
751 }
752
753 pub async fn graphql_mutation<Abi>(
758 &self,
759 application_id: ApplicationId<Abi>,
760 query: impl Into<async_graphql::Request>,
761 ) -> ConfirmedBlockCertificate
762 where
763 Abi: ServiceAbi<Query = async_graphql::Request, QueryResponse = async_graphql::Response>,
764 {
765 self.try_graphql_mutation(application_id, query)
766 .await
767 .expect("Failed to execute service GraphQL mutation")
768 }
769
770 pub async fn try_graphql_mutation<Abi>(
775 &self,
776 application_id: ApplicationId<Abi>,
777 query: impl Into<async_graphql::Request>,
778 ) -> Result<ConfirmedBlockCertificate, TryGraphQLMutationError>
779 where
780 Abi: ServiceAbi<Query = async_graphql::Request, QueryResponse = async_graphql::Response>,
781 {
782 let QueryOutcome { operations, .. } = self.try_graphql_query(application_id, query).await?;
783
784 let (certificate, _) = Box::pin(self.try_add_block(|block| {
785 for operation in operations {
786 match operation {
787 Operation::User {
788 application_id,
789 bytes,
790 } => {
791 block.with_raw_operation(application_id, bytes);
792 }
793 Operation::System(system_operation) => {
794 block.with_system_operation(*system_operation);
795 }
796 }
797 }
798 }))
799 .await?;
800
801 Ok(certificate)
802 }
803
804 pub async fn query_account(
806 &self,
807 application_id: ApplicationId<FungibleTokenAbi>,
808 account_owner: AccountOwner,
809 ) -> Option<Amount> {
810 use async_graphql::InputType as _;
811
812 let query = format!(
813 "query {{ accounts {{ entry(key: {}) {{ value }} }} }}",
814 account_owner.to_value()
815 );
816 let QueryOutcome { response, .. } = self.graphql_query(application_id, query).await;
817 let balance = response.pointer("/accounts/entry/value")?.as_str()?;
818
819 Some(
820 balance
821 .parse()
822 .expect("Account balance cannot be parsed as a number"),
823 )
824 }
825
826 pub async fn query_allowance(
828 &self,
829 application_id: ApplicationId<FungibleTokenAbi>,
830 owner: AccountOwner,
831 spender: AccountOwner,
832 ) -> Option<Amount> {
833 use async_graphql::InputType as _;
834
835 let owner_spender = OwnerSpender::new(owner, spender);
836 let query = format!(
837 "query {{ allowances {{ entry(key: {}) {{ value }} }} }}",
838 owner_spender.to_value()
839 );
840 let QueryOutcome { response, .. } = self.graphql_query(application_id, query).await;
841 let allowance = response.pointer("/allowances/entry/value")?.as_str()?;
842
843 Some(
844 allowance
845 .parse()
846 .expect("Allowance cannot be parsed as a number"),
847 )
848 }
849}
850
851#[derive(Debug, thiserror::Error)]
853pub enum TryQueryError {
854 #[error("Failed to serialize query request")]
856 Serialization(#[from] serde_json::Error),
857
858 #[error("Failed to execute service query")]
860 Execution(#[from] WorkerError),
861}
862
863#[derive(Debug, thiserror::Error)]
865pub enum TryGraphQLQueryError {
866 #[error("Failed to serialize GraphQL query request")]
868 RequestSerialization(#[source] serde_json::Error),
869
870 #[error("Failed to execute service query")]
872 Execution(#[from] WorkerError),
873
874 #[error("Unexpected non-JSON service query response")]
876 ResponseDeserialization(#[from] serde_json::Error),
877
878 #[error("Service returned errors: {_0:#?}")]
880 Service(Vec<async_graphql::ServerError>),
881}
882
883impl From<TryQueryError> for TryGraphQLQueryError {
884 fn from(query_error: TryQueryError) -> Self {
885 match query_error {
886 TryQueryError::Serialization(error) => {
887 TryGraphQLQueryError::RequestSerialization(error)
888 }
889 TryQueryError::Execution(error) => TryGraphQLQueryError::Execution(error),
890 }
891 }
892}
893
894impl TryGraphQLQueryError {
895 pub fn expect_execution_error(self) -> ExecutionError {
901 let TryGraphQLQueryError::Execution(worker_error) = self else {
902 panic!("Expected an `ExecutionError`. Got: {self:#?}");
903 };
904
905 worker_error.expect_execution_error(ChainExecutionContext::Query)
906 }
907}
908
909#[derive(Debug, thiserror::Error)]
911pub enum TryGraphQLMutationError {
912 #[error(transparent)]
914 Query(#[from] TryGraphQLQueryError),
915
916 #[error("Failed to propose block with operations scheduled by the GraphQL mutation")]
918 Proposal(#[from] WorkerError),
919}
920
921impl TryGraphQLMutationError {
922 pub fn expect_proposal_execution_error(self, transaction_index: u32) -> ExecutionError {
928 let TryGraphQLMutationError::Proposal(proposal_error) = self else {
929 panic!("Expected an `ExecutionError` during the block proposal. Got: {self:#?}");
930 };
931
932 proposal_error.expect_execution_error(ChainExecutionContext::Operation(transaction_index))
933 }
934}