Skip to main content

linera_core/unit_tests/
test_utils.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(clippy::cast_possible_truncation)]
5
6use std::{
7    collections::{BTreeMap, HashMap, HashSet},
8    sync::Arc,
9    time::Duration,
10    vec,
11};
12
13use async_trait::async_trait;
14use futures::{
15    future::Either,
16    lock::{Mutex, MutexGuard},
17    Future,
18};
19use linera_base::{
20    crypto::{
21        AccountPublicKey, CryptoHash, ValidatorKeypair, ValidatorPublicKey, ValidatorSecretKey,
22    },
23    data_types::*,
24    identifiers::{AccountOwner, BlobId, ChainId, EventId},
25    ownership::ChainOwnership,
26};
27use linera_chain::{
28    data_types::BlockProposal,
29    types::{
30        CertificateKind, Certified, ConfirmedBlock, ConfirmedBlockCertificate, GenericCertificate,
31        LiteCertificate, Timeout, ValidatedBlock, ValidatedBlockCertificate,
32    },
33};
34use linera_execution::{committee::Committee, ResourceControlPolicy, WasmRuntime};
35use linera_storage::{Arc as CacheArc, DbStorage, ResultReadCertificates, Storage, TestClock};
36#[cfg(all(not(target_arch = "wasm32"), feature = "storage-service"))]
37use linera_storage_service::client::StorageServiceDatabase;
38use linera_version::VersionInfo;
39#[cfg(feature = "scylladb")]
40use linera_views::scylla_db::ScyllaDbDatabase;
41use linera_views::{
42    memory::MemoryDatabase,
43    random::generate_test_namespace,
44    store::{KeyValueStore, TestKeyValueDatabase},
45};
46use tokio::sync::oneshot;
47use tokio_stream::wrappers::UnboundedReceiverStream;
48#[cfg(feature = "rocksdb")]
49use {
50    linera_views::rocks_db::RocksDbDatabase,
51    tokio::sync::{Semaphore, SemaphorePermit},
52};
53
54use crate::{
55    chain_worker::ChainWorkerConfig,
56    client::{chain_client, Client},
57    data_types::*,
58    environment::{TestSigner, TestWallet},
59    node::{
60        CrossChainMessageDelivery, NodeError, NotificationStream, ValidatorNode,
61        ValidatorNodeProvider,
62    },
63    notifier::ChannelNotifier,
64    worker::{
65        Notification, ProcessableCertificate, WorkerState, DEFAULT_BLOCK_CACHE_SIZE,
66        DEFAULT_EXECUTION_STATE_CACHE_SIZE,
67    },
68};
69
70/// The kind of misbehavior a test validator simulates.
71#[derive(Debug, PartialEq, Clone, Copy)]
72#[allow(missing_docs)]
73pub enum FaultType {
74    Honest,
75    Offline,
76    OfflineWithInfo,
77    NoChains,
78    DontSendConfirmVote,
79    DontProcessValidated,
80    DontSendValidateVote,
81}
82
83/// A validator used for testing. "Faulty" validators ignore block proposals (but not
84/// certificates or info queries) and have the wrong initial balance for all chains.
85///
86/// All methods are executed in spawned Tokio tasks, so that canceling a client task doesn't cause
87/// the validator's tasks to be canceled: In a real network, a validator also wouldn't cancel
88/// tasks if the client stopped waiting for the response.
89struct LocalValidator<S>
90where
91    S: Storage,
92{
93    state: WorkerState<S>,
94    notifier: Arc<ChannelNotifier<Notification>>,
95}
96
97/// A client used by tests to talk to an in-process `LocalValidator`.
98#[derive(Clone)]
99pub struct LocalValidatorClient<S>
100where
101    S: Storage,
102{
103    public_key: ValidatorPublicKey,
104    client: Arc<Mutex<LocalValidator<S>>>,
105    fault_type: FaultType,
106}
107
108impl<S> ValidatorNode for LocalValidatorClient<S>
109where
110    S: Storage + Clone + Send + Sync + 'static,
111{
112    type NotificationStream = NotificationStream;
113
114    fn address(&self) -> String {
115        format!("local:{}", self.public_key)
116    }
117
118    async fn handle_block_proposal(
119        &self,
120        proposal: BlockProposal,
121    ) -> Result<ChainInfoResponse, NodeError> {
122        self.spawn_and_receive(move |validator, sender| {
123            validator.do_handle_block_proposal(proposal, sender)
124        })
125        .await
126    }
127
128    async fn handle_lite_certificate(
129        &self,
130        certificate: LiteCertificate<'_>,
131        _delivery: CrossChainMessageDelivery,
132    ) -> Result<ChainInfoResponse, NodeError> {
133        let certificate = certificate.cloned();
134        self.spawn_and_receive(move |validator, sender| {
135            validator.do_handle_lite_certificate(certificate, sender)
136        })
137        .await
138    }
139
140    async fn handle_timeout_certificate(
141        &self,
142        certificate: GenericCertificate<Timeout>,
143    ) -> Result<ChainInfoResponse, NodeError> {
144        self.spawn_and_receive(move |validator, sender| {
145            validator.do_handle_certificate::<Timeout>(certificate, sender)
146        })
147        .await
148    }
149
150    async fn handle_validated_certificate(
151        &self,
152        certificate: ValidatedBlockCertificate,
153    ) -> Result<ChainInfoResponse, NodeError> {
154        self.spawn_and_receive(move |validator, sender| {
155            validator.do_handle_certificate::<ValidatedBlock>(certificate, sender)
156        })
157        .await
158    }
159
160    async fn handle_confirmed_certificate(
161        &self,
162        certificate: CacheArc<ConfirmedBlockCertificate>,
163        _delivery: CrossChainMessageDelivery,
164    ) -> Result<ChainInfoResponse, NodeError> {
165        self.spawn_and_receive(move |validator, sender| {
166            validator.do_handle_certificate::<ConfirmedBlock>(
167                CacheArc::unwrap_or_clone(certificate),
168                sender,
169            )
170        })
171        .await
172    }
173
174    async fn handle_chain_info_query(
175        &self,
176        query: ChainInfoQuery,
177    ) -> Result<ChainInfoResponse, NodeError> {
178        self.spawn_and_receive(move |validator, sender| {
179            validator.do_handle_chain_info_query(query, sender)
180        })
181        .await
182    }
183
184    async fn subscribe(&self, chains: Vec<ChainId>) -> Result<NotificationStream, NodeError> {
185        self.spawn_and_receive(move |validator, sender| validator.do_subscribe(chains, sender))
186            .await
187    }
188
189    async fn get_version_info(&self) -> Result<VersionInfo, NodeError> {
190        Ok(Default::default())
191    }
192
193    async fn get_network_description(&self) -> Result<NetworkDescription, NodeError> {
194        Ok(self
195            .client
196            .lock()
197            .await
198            .state
199            .storage_client()
200            .read_network_description()
201            .await
202            .transpose()
203            .ok_or_else(|| NodeError::ViewError {
204                error: "missing NetworkDescription".to_owned(),
205            })??)
206    }
207
208    async fn upload_blob(&self, content: BlobContent) -> Result<BlobId, NodeError> {
209        self.spawn_and_receive(move |validator, sender| validator.do_upload_blob(content, sender))
210            .await
211    }
212
213    async fn download_blob(&self, blob_id: BlobId) -> Result<BlobContent, NodeError> {
214        self.spawn_and_receive(move |validator, sender| validator.do_download_blob(blob_id, sender))
215            .await
216    }
217
218    async fn download_blobs(
219        &self,
220        blob_ids: Vec<BlobId>,
221    ) -> Result<crate::node::BlobStream, NodeError> {
222        let this = self.clone();
223        let stream = futures::stream::unfold(blob_ids.into_iter(), move |mut iter| {
224            let this = this.clone();
225            async move {
226                let blob_id = iter.next()?;
227                let result = this.download_blob(blob_id).await;
228                Some((result, iter))
229            }
230        });
231        Ok(Box::pin(stream))
232    }
233
234    async fn download_pending_blob(
235        &self,
236        chain_id: ChainId,
237        blob_id: BlobId,
238    ) -> Result<BlobContent, NodeError> {
239        self.spawn_and_receive(move |validator, sender| {
240            validator.do_download_pending_blob(chain_id, blob_id, sender)
241        })
242        .await
243    }
244
245    async fn handle_pending_blob(
246        &self,
247        chain_id: ChainId,
248        blob: BlobContent,
249    ) -> Result<ChainInfoResponse, NodeError> {
250        self.spawn_and_receive(move |validator, sender| {
251            validator.do_handle_pending_blob(chain_id, blob, sender)
252        })
253        .await
254    }
255
256    async fn download_certificate(
257        &self,
258        hash: CryptoHash,
259    ) -> Result<ConfirmedBlockCertificate, NodeError> {
260        self.spawn_and_receive(move |validator, sender| {
261            validator.do_download_certificate(hash, sender)
262        })
263        .await
264    }
265
266    async fn download_certificates(
267        &self,
268        hashes: Vec<CryptoHash>,
269    ) -> Result<Vec<ConfirmedBlockCertificate>, NodeError> {
270        self.spawn_and_receive(move |validator, sender| {
271            validator.do_download_certificates(hashes, sender)
272        })
273        .await
274    }
275
276    async fn download_certificates_by_heights(
277        &self,
278        chain_id: ChainId,
279        heights: Vec<BlockHeight>,
280    ) -> Result<Vec<ConfirmedBlockCertificate>, NodeError> {
281        self.spawn_and_receive(move |validator, sender| {
282            validator.do_download_certificates_by_heights(chain_id, heights, sender)
283        })
284        .await
285    }
286
287    async fn blob_last_used_by(&self, blob_id: BlobId) -> Result<CryptoHash, NodeError> {
288        self.spawn_and_receive(move |validator, sender| {
289            validator.do_blob_last_used_by(blob_id, sender)
290        })
291        .await
292    }
293
294    async fn blob_last_used_by_certificate(
295        &self,
296        blob_id: BlobId,
297    ) -> Result<ConfirmedBlockCertificate, NodeError> {
298        self.spawn_and_receive(move |validator, sender| {
299            validator.do_blob_last_used_by_certificate(blob_id, sender)
300        })
301        .await
302    }
303
304    async fn missing_blob_ids(&self, blob_ids: Vec<BlobId>) -> Result<Vec<BlobId>, NodeError> {
305        self.spawn_and_receive(move |validator, sender| {
306            validator.do_missing_blob_ids(blob_ids, sender)
307        })
308        .await
309    }
310
311    async fn event_block_heights(
312        &self,
313        event_ids: Vec<EventId>,
314    ) -> Result<Vec<Option<BlockHeight>>, NodeError> {
315        self.spawn_and_receive(move |validator, sender| {
316            validator.do_event_block_heights(event_ids, sender)
317        })
318        .await
319    }
320
321    async fn get_shard_info(
322        &self,
323        _chain_id: ChainId,
324    ) -> Result<crate::data_types::ShardInfo, NodeError> {
325        // For test purposes, return a dummy shard info
326        Ok(crate::data_types::ShardInfo {
327            shard_id: 0,
328            total_shards: 1,
329        })
330    }
331}
332
333impl<S> LocalValidatorClient<S>
334where
335    S: Storage + Clone + Send + Sync + 'static,
336{
337    fn new(public_key: ValidatorPublicKey, state: WorkerState<S>) -> Self {
338        let client = LocalValidator {
339            state,
340            notifier: Arc::new(ChannelNotifier::default()),
341        };
342        Self {
343            public_key,
344            client: Arc::new(Mutex::new(client)),
345            fault_type: FaultType::Honest,
346        }
347    }
348
349    /// Returns the validator's public key.
350    pub fn name(&self) -> ValidatorPublicKey {
351        self.public_key
352    }
353
354    /// Returns the validator's currently configured [`FaultType`].
355    pub fn fault_type(&self) -> FaultType {
356        self.fault_type
357    }
358
359    fn set_fault_type(&mut self, fault_type: FaultType) {
360        self.fault_type = fault_type;
361    }
362
363    /// Obtains the basic `ChainInfo` data for the local validator chain, with chain manager values.
364    pub async fn chain_info_with_manager_values(
365        &mut self,
366        chain_id: ChainId,
367    ) -> Result<Box<ChainInfo>, NodeError> {
368        let query = ChainInfoQuery::new(chain_id).with_manager_values();
369        let response = self.handle_chain_info_query(query).await?;
370        Ok(response.info)
371    }
372
373    /// Executes the future produced by `f` in a new thread in a new Tokio runtime.
374    /// Returns the value that the future puts into the sender.
375    async fn spawn_and_receive<F, R, T>(&self, f: F) -> T
376    where
377        T: Send + 'static,
378        R: Future<Output = Result<(), T>> + Send,
379        F: FnOnce(Self, oneshot::Sender<T>) -> R + Send + 'static,
380    {
381        let validator = self.clone();
382        let (sender, receiver) = oneshot::channel();
383        tokio::spawn(async move {
384            if f(validator, sender).await.is_err() {
385                tracing::debug!("result could not be sent");
386            }
387        });
388        receiver.await.unwrap()
389    }
390
391    async fn do_handle_block_proposal(
392        self,
393        proposal: BlockProposal,
394        sender: oneshot::Sender<Result<ChainInfoResponse, NodeError>>,
395    ) -> Result<(), Result<ChainInfoResponse, NodeError>> {
396        let result = match self.fault_type {
397            FaultType::Offline | FaultType::OfflineWithInfo => Err(NodeError::ClientIoError {
398                error: "offline".to_string(),
399            }),
400            FaultType::NoChains => Err(NodeError::InactiveChain(proposal.content.block.chain_id)),
401            FaultType::DontSendValidateVote
402            | FaultType::Honest
403            | FaultType::DontSendConfirmVote
404            | FaultType::DontProcessValidated => {
405                let (response_result, _actions) = self
406                    .client
407                    .lock()
408                    .await
409                    .state
410                    .handle_block_proposal(proposal)
411                    .await;
412                let result = response_result.map_err(NodeError::from);
413                if self.fault_type == FaultType::DontSendValidateVote {
414                    Err(NodeError::ClientIoError {
415                        error: "refusing to validate".to_string(),
416                    })
417                } else {
418                    result
419                }
420            }
421        };
422        // In a local node cross-chain messages can't get lost, so we can ignore the actions here.
423        sender.send(result)
424    }
425
426    async fn do_handle_lite_certificate(
427        self,
428        certificate: LiteCertificate<'_>,
429        sender: oneshot::Sender<Result<ChainInfoResponse, NodeError>>,
430    ) -> Result<(), Result<ChainInfoResponse, NodeError>> {
431        let client = self.client.clone();
432        let validator = client.lock().await;
433        let result = async move {
434            match validator.state.full_certificate(certificate).await? {
435                Either::Left(confirmed) => {
436                    self.do_handle_certificate_internal::<ConfirmedBlock>(confirmed, &validator)
437                        .await
438                }
439                Either::Right(validated) => {
440                    self.do_handle_certificate_internal::<ValidatedBlock>(validated, &validator)
441                        .await
442                }
443            }
444        }
445        .await;
446        sender.send(result)
447    }
448
449    async fn do_handle_certificate_internal<T: ProcessableCertificate>(
450        &self,
451        certificate: T::Certificate,
452        validator: &MutexGuard<'_, LocalValidator<S>>,
453    ) -> Result<ChainInfoResponse, NodeError> {
454        match self.fault_type {
455            FaultType::DontProcessValidated if T::KIND == CertificateKind::Validated => {
456                Err(NodeError::ClientIoError {
457                    error: "refusing to process validated block".to_string(),
458                })
459            }
460            FaultType::NoChains => Err(NodeError::InactiveChain(certificate.value().chain_id())),
461            FaultType::Honest
462            | FaultType::DontSendConfirmVote
463            | FaultType::DontProcessValidated
464            | FaultType::DontSendValidateVote => {
465                let result = validator
466                    .state
467                    .fully_handle_certificate_with_notifications(certificate, &validator.notifier)
468                    .await
469                    .map_err(Into::into);
470                if T::KIND == CertificateKind::Validated
471                    && self.fault_type == FaultType::DontSendConfirmVote
472                {
473                    Err(NodeError::ClientIoError {
474                        error: "refusing to confirm".to_string(),
475                    })
476                } else {
477                    result
478                }
479            }
480            FaultType::Offline | FaultType::OfflineWithInfo => Err(NodeError::ClientIoError {
481                error: "offline".to_string(),
482            }),
483        }
484    }
485
486    async fn do_handle_certificate<T: ProcessableCertificate>(
487        self,
488        certificate: T::Certificate,
489        sender: oneshot::Sender<Result<ChainInfoResponse, NodeError>>,
490    ) -> Result<(), Result<ChainInfoResponse, NodeError>> {
491        let validator = self.client.lock().await;
492        let result = self
493            .do_handle_certificate_internal::<T>(certificate, &validator)
494            .await;
495        sender.send(result)
496    }
497
498    async fn do_handle_chain_info_query(
499        self,
500        query: ChainInfoQuery,
501        sender: oneshot::Sender<Result<ChainInfoResponse, NodeError>>,
502    ) -> Result<(), Result<ChainInfoResponse, NodeError>> {
503        let validator = self.client.lock().await;
504        let result = match self.fault_type {
505            FaultType::Offline => Err(NodeError::ClientIoError {
506                error: "offline".to_string(),
507            }),
508            FaultType::NoChains => Err(NodeError::InactiveChain(query.chain_id)),
509            FaultType::Honest
510            | FaultType::DontSendConfirmVote
511            | FaultType::DontProcessValidated
512            | FaultType::DontSendValidateVote
513            | FaultType::OfflineWithInfo => validator
514                .state
515                .handle_chain_info_query(query)
516                .await
517                .map_err(Into::into),
518        };
519        sender.send(result)
520    }
521
522    async fn do_subscribe(
523        self,
524        chains: Vec<ChainId>,
525        sender: oneshot::Sender<Result<NotificationStream, NodeError>>,
526    ) -> Result<(), Result<NotificationStream, NodeError>> {
527        let validator = self.client.lock().await;
528        let rx = validator.notifier.subscribe(chains);
529        let stream: NotificationStream = Box::pin(UnboundedReceiverStream::new(rx));
530        sender.send(Ok(stream))
531    }
532
533    async fn do_upload_blob(
534        self,
535        content: BlobContent,
536        sender: oneshot::Sender<Result<BlobId, NodeError>>,
537    ) -> Result<(), Result<BlobId, NodeError>> {
538        let validator = self.client.lock().await;
539        let blob = Blob::new(content);
540        let id = blob.id();
541        let storage = validator.state.storage_client();
542        let result = match storage.maybe_write_blobs(&[blob]).await {
543            Ok(has_state) if has_state.first() == Some(&true) => Ok(id),
544            Ok(_) => Err(NodeError::BlobsNotFound(vec![id])),
545            Err(error) => Err(error.into()),
546        };
547        sender.send(result)
548    }
549
550    async fn do_download_blob(
551        self,
552        blob_id: BlobId,
553        sender: oneshot::Sender<Result<BlobContent, NodeError>>,
554    ) -> Result<(), Result<BlobContent, NodeError>> {
555        let validator = self.client.lock().await;
556        let blob = validator
557            .state
558            .storage_client()
559            .read_blob(blob_id)
560            .await
561            .map_err(Into::into);
562        let blob = match blob {
563            Ok(blob) => blob.ok_or_else(|| NodeError::BlobsNotFound(vec![blob_id])),
564            Err(error) => Err(error),
565        };
566        sender.send(blob.map(|blob| CacheArc::unwrap_or_clone(blob).into_content()))
567    }
568
569    async fn do_download_pending_blob(
570        self,
571        chain_id: ChainId,
572        blob_id: BlobId,
573        sender: oneshot::Sender<Result<BlobContent, NodeError>>,
574    ) -> Result<(), Result<BlobContent, NodeError>> {
575        let validator = self.client.lock().await;
576        let result = validator
577            .state
578            .download_pending_blob(chain_id, blob_id)
579            .await
580            .map_err(Into::into);
581        sender.send(result.map(|blob| blob.content().clone()))
582    }
583
584    async fn do_handle_pending_blob(
585        self,
586        chain_id: ChainId,
587        blob: BlobContent,
588        sender: oneshot::Sender<Result<ChainInfoResponse, NodeError>>,
589    ) -> Result<(), Result<ChainInfoResponse, NodeError>> {
590        let validator = self.client.lock().await;
591        let result = validator
592            .state
593            .handle_pending_blob(chain_id, Blob::new(blob))
594            .await
595            .map_err(Into::into);
596        sender.send(result)
597    }
598
599    async fn do_download_certificate(
600        self,
601        hash: CryptoHash,
602        sender: oneshot::Sender<Result<ConfirmedBlockCertificate, NodeError>>,
603    ) -> Result<(), Result<ConfirmedBlockCertificate, NodeError>> {
604        let validator = self.client.lock().await;
605        let certificate = validator
606            .state
607            .storage_client()
608            .read_certificate(hash)
609            .await
610            .map_err(Into::into);
611
612        let certificate = match certificate {
613            Err(error) => Err(error),
614            Ok(entry) => match entry {
615                Some(certificate) => Ok(CacheArc::unwrap_or_clone(certificate)),
616                None => {
617                    panic!("Missing certificate: {hash}");
618                }
619            },
620        };
621
622        sender.send(certificate)
623    }
624
625    async fn do_download_certificates(
626        self,
627        hashes: Vec<CryptoHash>,
628        sender: oneshot::Sender<Result<Vec<ConfirmedBlockCertificate>, NodeError>>,
629    ) -> Result<(), Result<Vec<ConfirmedBlockCertificate>, NodeError>> {
630        let validator = self.client.lock().await;
631        let certificates = validator
632            .state
633            .storage_client()
634            .read_certificates(&hashes)
635            .await
636            .map_err(Into::into);
637
638        let certificates = match certificates {
639            Err(error) => Err(error),
640            Ok(certificates) => match ResultReadCertificates::new(certificates, hashes) {
641                ResultReadCertificates::Certificates(certificates) => Ok(certificates),
642                ResultReadCertificates::InvalidHashes(hashes) => {
643                    panic!("Missing certificates: {hashes:?}")
644                }
645            },
646        };
647
648        sender.send(certificates)
649    }
650
651    async fn do_download_certificates_by_heights(
652        self,
653        chain_id: ChainId,
654        heights: Vec<BlockHeight>,
655        sender: oneshot::Sender<Result<Vec<ConfirmedBlockCertificate>, NodeError>>,
656    ) -> Result<(), Result<Vec<ConfirmedBlockCertificate>, NodeError>> {
657        // First, use do_handle_chain_info_query to get the certificate hashes
658        let (query_sender, query_receiver) = oneshot::channel();
659        let query = ChainInfoQuery::new(chain_id).with_sent_certificate_hashes_by_heights(heights);
660
661        let self_clone = self.clone();
662        self.do_handle_chain_info_query(query, query_sender)
663            .await
664            .expect("Failed to handle chain info query");
665
666        // Get the response from the chain info query
667        let chain_info_response = query_receiver.await.map_err(|_| {
668            Err(NodeError::ClientIoError {
669                error: "Failed to receive chain info response".to_string(),
670            })
671        })?;
672
673        let hashes = match chain_info_response {
674            Ok(response) => response.info.requested_sent_certificate_hashes,
675            Err(e) => {
676                return sender.send(Err(e));
677            }
678        };
679
680        // Now use do_download_certificates to get the actual certificates
681        let (cert_sender, cert_receiver) = oneshot::channel();
682        self_clone
683            .do_download_certificates(hashes, cert_sender)
684            .await?;
685
686        // Forward the result to the original sender
687        let result = cert_receiver.await.map_err(|_| {
688            Err(NodeError::ClientIoError {
689                error: "Failed to receive certificates".to_string(),
690            })
691        })?;
692
693        sender.send(result)
694    }
695
696    async fn do_blob_last_used_by(
697        self,
698        blob_id: BlobId,
699        sender: oneshot::Sender<Result<CryptoHash, NodeError>>,
700    ) -> Result<(), Result<CryptoHash, NodeError>> {
701        let validator = self.client.lock().await;
702        let blob_state = validator
703            .state
704            .storage_client()
705            .read_blob_state(blob_id)
706            .await
707            .map_err(Into::into);
708        let certificate_hash = match blob_state {
709            Err(err) => Err(err),
710            Ok(blob_state) => match blob_state {
711                None => Err(NodeError::BlobsNotFound(vec![blob_id])),
712                Some(blob_state) => blob_state
713                    .last_used_by
714                    .ok_or_else(|| NodeError::BlobsNotFound(vec![blob_id])),
715            },
716        };
717
718        sender.send(certificate_hash)
719    }
720
721    async fn do_blob_last_used_by_certificate(
722        self,
723        blob_id: BlobId,
724        sender: oneshot::Sender<Result<ConfirmedBlockCertificate, NodeError>>,
725    ) -> Result<(), Result<ConfirmedBlockCertificate, NodeError>> {
726        match self.blob_last_used_by(blob_id).await {
727            Ok(cert_hash) => {
728                let cert = self.download_certificate(cert_hash).await;
729                sender.send(cert)
730            }
731            Err(err) => sender.send(Err(err)),
732        }
733    }
734
735    async fn do_missing_blob_ids(
736        self,
737        blob_ids: Vec<BlobId>,
738        sender: oneshot::Sender<Result<Vec<BlobId>, NodeError>>,
739    ) -> Result<(), Result<Vec<BlobId>, NodeError>> {
740        let validator = self.client.lock().await;
741        let missing_blob_ids = validator
742            .state
743            .storage_client()
744            .missing_blobs(&blob_ids)
745            .await
746            .map_err(Into::into);
747        sender.send(missing_blob_ids)
748    }
749
750    async fn do_event_block_heights(
751        self,
752        event_ids: Vec<EventId>,
753        sender: oneshot::Sender<Result<Vec<Option<BlockHeight>>, NodeError>>,
754    ) -> Result<(), Result<Vec<Option<BlockHeight>>, NodeError>> {
755        let validator = self.client.lock().await;
756        let heights = validator
757            .state
758            .storage_client()
759            .read_event_block_heights(&event_ids)
760            .await
761            .map_err(Into::into);
762        sender.send(heights)
763    }
764}
765
766/// A [`ValidatorNodeProvider`] holding the in-process test validator clients.
767#[derive(Clone)]
768pub struct NodeProvider<S>(Arc<std::sync::Mutex<Vec<LocalValidatorClient<S>>>>)
769where
770    S: Storage;
771
772impl<S> NodeProvider<S>
773where
774    S: Storage + Clone,
775{
776    fn all_nodes(&self) -> Vec<LocalValidatorClient<S>> {
777        self.0.lock().unwrap().clone()
778    }
779}
780
781impl<S> ValidatorNodeProvider for NodeProvider<S>
782where
783    S: Storage + Clone + Send + Sync + 'static,
784{
785    type Node = LocalValidatorClient<S>;
786
787    fn make_node(&self, _name: &str) -> Result<Self::Node, NodeError> {
788        unimplemented!()
789    }
790
791    fn make_nodes_from_list<A>(
792        &self,
793        validators: impl IntoIterator<Item = (ValidatorPublicKey, A)>,
794    ) -> Result<impl Iterator<Item = (ValidatorPublicKey, Self::Node)>, NodeError>
795    where
796        A: AsRef<str>,
797    {
798        let list = self.0.lock().unwrap();
799        Ok(validators
800            .into_iter()
801            .map(|(public_key, address)| {
802                list.iter()
803                    .find(|client| client.public_key == public_key)
804                    .ok_or_else(|| NodeError::CannotResolveValidatorAddress {
805                        address: address.as_ref().to_string(),
806                    })
807                    .map(|client| (public_key, client.clone()))
808            })
809            .collect::<Result<Vec<_>, _>>()?
810            .into_iter())
811    }
812}
813
814impl<S> FromIterator<LocalValidatorClient<S>> for NodeProvider<S>
815where
816    S: Storage,
817{
818    fn from_iter<T>(iter: T) -> Self
819    where
820        T: IntoIterator<Item = LocalValidatorClient<S>>,
821    {
822        Self(Arc::new(std::sync::Mutex::new(iter.into_iter().collect())))
823    }
824}
825
826// NOTE:
827// * To communicate with a quorum of validators, chain clients iterate over a copy of
828// `validator_clients` to spawn I/O tasks.
829// * When using `LocalValidatorClient`, clients communicate with an exact quorum then stop.
830// * Most tests have 1 faulty validator out 4 so that there is exactly only 1 quorum to
831// communicate with.
832#[allow(missing_docs)]
833pub struct TestBuilder<B: StorageBuilder> {
834    storage_builder: B,
835    pub initial_committee: Committee,
836    admin_description: Option<ChainDescription>,
837    network_description: Option<NetworkDescription>,
838    genesis_storage_builder: GenesisStorageBuilder,
839    node_provider: NodeProvider<B::Storage>,
840    pub validator_storages: HashMap<ValidatorPublicKey, B::Storage>,
841    pub validator_key_pairs: HashMap<ValidatorPublicKey, ValidatorSecretKey>,
842    chain_client_storages: Vec<B::Storage>,
843    pub chain_owners: BTreeMap<ChainId, AccountOwner>,
844    pub signer: TestSigner,
845}
846
847/// Builds storage instances of a specific backend for use in tests.
848#[async_trait]
849pub trait StorageBuilder {
850    /// The storage type produced by this builder.
851    type Storage: Storage + Clone + Send + Sync + 'static;
852
853    /// Builds a new storage instance.
854    async fn build(&mut self) -> Result<Self::Storage, anyhow::Error>;
855
856    /// Returns the test clock shared by all storages built here.
857    fn clock(&self) -> &TestClock;
858}
859
860#[derive(Default)]
861struct GenesisStorageBuilder {
862    accounts: Vec<GenesisAccount>,
863}
864
865struct GenesisAccount {
866    description: ChainDescription,
867    public_key: AccountPublicKey,
868}
869
870impl GenesisStorageBuilder {
871    fn add(&mut self, description: ChainDescription, public_key: AccountPublicKey) {
872        self.accounts.push(GenesisAccount {
873            description,
874            public_key,
875        })
876    }
877
878    async fn build<S>(&self, storage: S) -> S
879    where
880        S: Storage + Clone + Send + Sync + 'static,
881    {
882        for account in &self.accounts {
883            storage
884                .create_chain(account.description.clone())
885                .await
886                .unwrap();
887        }
888        storage
889    }
890}
891
892/// A chain client wired up to the in-process test validator network.
893pub type ChainClient<S> = crate::client::ChainClient<crate::environment::Impl<S, NodeProvider<S>>>;
894
895impl<S: Storage + Clone + Send + Sync + 'static> ChainClient<S> {
896    /// Reads the hashed certificate values in descending order from the given hash.
897    pub async fn read_confirmed_blocks_downward(
898        &self,
899        from: CryptoHash,
900        limit: u32,
901    ) -> anyhow::Result<Vec<Arc<ConfirmedBlock>>> {
902        let mut hash = Some(from);
903        let mut values = Vec::new();
904        for _ in 0..limit {
905            let Some(next_hash) = hash else {
906                break;
907            };
908            let value = self.read_confirmed_block(next_hash).await?;
909            hash = value.block().header.previous_block_hash;
910            values.push(value);
911        }
912        Ok(values)
913    }
914}
915
916impl<B> TestBuilder<B>
917where
918    B: StorageBuilder,
919{
920    /// The simulated clock every storage here shares, so a test can drive the export queue's
921    /// tick in virtual time instead of sleeping through it.
922    pub fn clock(&self) -> &TestClock {
923        self.storage_builder.clock()
924    }
925
926    /// Creates a test setup with `count` validators, `with_faulty_validators` of which are faulty.
927    pub async fn new(
928        storage_builder: B,
929        count: usize,
930        with_faulty_validators: usize,
931        signer: TestSigner,
932    ) -> Result<Self, anyhow::Error> {
933        Self::build(
934            storage_builder,
935            count,
936            with_faulty_validators,
937            signer,
938            None,
939            None,
940        )
941        .await
942    }
943
944    /// Creates a test setup like [`TestBuilder::new`], in which every validator also pushes the
945    /// blocks it executes to the rest of the committee.
946    pub async fn new_with_block_export(
947        storage_builder: B,
948        count: usize,
949        with_faulty_validators: usize,
950        signer: TestSigner,
951    ) -> Result<Self, anyhow::Error> {
952        Self::build(
953            storage_builder,
954            count,
955            with_faulty_validators,
956            signer,
957            Some(Self::test_block_export_config()),
958            None,
959        )
960        .await
961    }
962
963    /// Creates a test setup like [`TestBuilder::new_with_block_export`] with an explicit chain
964    /// worker TTL, for asserting that workers expire even while export is enabled.
965    pub async fn new_with_block_export_and_ttl(
966        storage_builder: B,
967        count: usize,
968        with_faulty_validators: usize,
969        signer: TestSigner,
970        chain_worker_ttl: Duration,
971    ) -> Result<Self, anyhow::Error> {
972        Self::build(
973            storage_builder,
974            count,
975            with_faulty_validators,
976            signer,
977            Some(Self::test_block_export_config()),
978            Some(chain_worker_ttl),
979        )
980        .await
981    }
982
983    /// Creates a test setup like [`TestBuilder::new_with_block_export`], with the export tuned by
984    /// the caller. Used to shrink `max_catch_up_blocks` far below its default so that a backlog a
985    /// test can actually produce still takes several rounds to drain.
986    pub async fn new_with_block_export_config(
987        storage_builder: B,
988        count: usize,
989        with_faulty_validators: usize,
990        signer: TestSigner,
991        config: crate::BlockExportConfig,
992    ) -> Result<Self, anyhow::Error> {
993        Self::build(
994            storage_builder,
995            count,
996            with_faulty_validators,
997            signer,
998            Some(config),
999            None,
1000        )
1001        .await
1002    }
1003
1004    /// The export settings the block-export tests run with: production backoff is measured in
1005    /// seconds, which would make every test that exercises a failing destination wait it out.
1006    pub fn test_block_export_config() -> crate::BlockExportConfig {
1007        crate::BlockExportConfig {
1008            retry_delay: Duration::from_millis(20),
1009            max_retry_delay: Duration::from_millis(200),
1010            ..crate::BlockExportConfig::default()
1011        }
1012    }
1013
1014    async fn build(
1015        mut storage_builder: B,
1016        count: usize,
1017        with_faulty_validators: usize,
1018        mut signer: TestSigner,
1019        block_export: Option<crate::BlockExportConfig>,
1020        chain_worker_ttl: Option<Duration>,
1021    ) -> Result<Self, anyhow::Error> {
1022        let mut validators = Vec::new();
1023        for _ in 0..count {
1024            let validator_keypair = ValidatorKeypair::generate();
1025            let account_public_key = signer.generate_new();
1026            validators.push((validator_keypair, account_public_key));
1027        }
1028        let for_committee = validators
1029            .iter()
1030            .map(|(validating, account)| (validating.public_key, *account))
1031            .collect::<Vec<_>>();
1032        let initial_committee = Committee::make_simple(for_committee);
1033        // Created up front and filled in below, so that each validator's export tasks can resolve
1034        // the others through it even though those clients do not exist yet.
1035        let node_provider = NodeProvider(Arc::new(std::sync::Mutex::new(Vec::new())));
1036        let mut validator_storages = HashMap::new();
1037        let mut validator_key_pairs = HashMap::new();
1038        let mut faulty_validators = HashSet::new();
1039        for (i, (validator_keypair, _account_public_key)) in validators.into_iter().enumerate() {
1040            let validator_public_key = validator_keypair.public_key;
1041            let storage = storage_builder.build().await?;
1042            let secret_key_copy = validator_keypair.secret_key.copy();
1043            let config = ChainWorkerConfig {
1044                nickname: format!("Node {i}"),
1045                // Export folds progress into the chain state when the worker next saves, so
1046                // give workers a lifetime instead of dropping them after every request, and fold
1047                // unthrottled so assertions see progress as it happens.
1048                ttl: chain_worker_ttl
1049                    .or_else(|| block_export.is_some().then(|| Duration::from_secs(60))),
1050                exported_heights_fold_interval: Duration::ZERO,
1051                ..ChainWorkerConfig::default()
1052            }
1053            .with_key_pair(Some(validator_keypair.secret_key));
1054            let mut state = WorkerState::new(storage.clone(), config, None);
1055            if let Some(export_config) = block_export.clone() {
1056                let handle = crate::spawn_block_export_queue(
1057                    storage.clone(),
1058                    Arc::new(node_provider.clone()),
1059                    export_config,
1060                    Some(validator_public_key),
1061                );
1062                state = state.with_block_export(handle);
1063            }
1064            let mut validator = LocalValidatorClient::new(validator_public_key, state);
1065            if i < with_faulty_validators {
1066                faulty_validators.insert(validator_public_key);
1067                validator.set_fault_type(FaultType::NoChains);
1068            }
1069            node_provider.0.lock().unwrap().push(validator);
1070            validator_storages.insert(validator_public_key, storage);
1071            validator_key_pairs.insert(validator_public_key, secret_key_copy);
1072        }
1073        tracing::info!(
1074            "Test will use the following faulty validators: {:?}",
1075            faulty_validators
1076        );
1077        Ok(Self {
1078            storage_builder,
1079            initial_committee,
1080            admin_description: None,
1081            network_description: None,
1082            genesis_storage_builder: GenesisStorageBuilder::default(),
1083            node_provider,
1084            validator_storages,
1085            validator_key_pairs,
1086            chain_client_storages: Vec::new(),
1087            chain_owners: BTreeMap::new(),
1088            signer,
1089        })
1090    }
1091
1092    /// Replaces the initial committee's resource control policy.
1093    pub fn with_policy(mut self, policy: ResourceControlPolicy) -> Self {
1094        let validators = self.initial_committee.validators().clone();
1095        self.initial_committee =
1096            Committee::new(validators, policy).expect("committee votes should not overflow");
1097        self
1098    }
1099
1100    /// Sets the cross-chain message chunk limit on every validator in the test setup.
1101    pub fn with_cross_chain_message_chunk_limit(self, limit: usize) -> Self {
1102        let validator_clients = self.node_provider.0.lock().unwrap();
1103        for validator in validator_clients.iter() {
1104            let mut inner = validator.client.try_lock().expect("no contention at setup");
1105            inner.state.set_cross_chain_message_chunk_limit(limit);
1106        }
1107        drop(validator_clients);
1108        self
1109    }
1110
1111    /// Returns the [`FaultType`] currently configured for the given validator, or `None`
1112    /// if no validator with that key is in the test setup.
1113    pub fn fault_type(&self, public_key: &ValidatorPublicKey) -> Option<FaultType> {
1114        self.node_provider
1115            .0
1116            .lock()
1117            .unwrap()
1118            .iter()
1119            .find(|client| client.public_key == *public_key)
1120            .map(|client| client.fault_type())
1121    }
1122
1123    /// Sets the [`FaultType`] for the validators at the given indexes.
1124    pub fn set_fault_type(&mut self, indexes: impl AsRef<[usize]>, fault_type: FaultType) {
1125        let mut faulty_validators = vec![];
1126        let mut validator_clients = self.node_provider.0.lock().unwrap();
1127        for index in indexes.as_ref() {
1128            let validator = &mut validator_clients[*index];
1129            validator.set_fault_type(fault_type);
1130            faulty_validators.push(validator.public_key);
1131        }
1132        tracing::info!(
1133            "Making the following validators {:?}: {:?}",
1134            fault_type,
1135            faulty_validators
1136        );
1137    }
1138
1139    /// Creates the root chain with the given `index`, and returns a client for it.
1140    ///
1141    /// Root chain 0 is the admin chain and needs to be initialized first, otherwise its balance
1142    /// is automatically set to zero.
1143    pub async fn add_root_chain(
1144        &mut self,
1145        index: u32,
1146        balance: Amount,
1147    ) -> anyhow::Result<ChainClient<B::Storage>> {
1148        self.add_root_chain_with_ownership(index, balance, ChainOwnership::single)
1149            .await
1150    }
1151
1152    /// Creates the root chain with the given `index` and a genesis ownership built from its
1153    /// freshly generated owner key, and returns a client for it.
1154    ///
1155    /// Root chain 0 is the admin chain and needs to be initialized first, otherwise its balance
1156    /// is automatically set to zero.
1157    pub async fn add_root_chain_with_ownership(
1158        &mut self,
1159        index: u32,
1160        balance: Amount,
1161        make_ownership: impl FnOnce(AccountOwner) -> ChainOwnership,
1162    ) -> anyhow::Result<ChainClient<B::Storage>> {
1163        // Make sure the admin chain is initialized.
1164        if self.admin_description.is_none() && index != 0 {
1165            Box::pin(self.add_root_chain(0, Amount::ZERO)).await?;
1166        }
1167        let origin = ChainOrigin::Root(index);
1168        let public_key = self.signer.generate_new();
1169        let open_chain_config = InitialChainConfig {
1170            ownership: make_ownership(public_key.into()),
1171            epoch: Epoch(0),
1172            account: AccountOwner::CHAIN,
1173            balance,
1174            application_permissions: ApplicationPermissions::default(),
1175        };
1176        let description = ChainDescription::new(origin, open_chain_config, Timestamp::from(0));
1177        let committee_blob = Blob::new_committee(bcs::to_bytes(&self.initial_committee).unwrap());
1178        if index == 0 {
1179            self.admin_description = Some(description.clone());
1180            self.network_description = Some(NetworkDescription {
1181                admin_chain_id: description.id(),
1182                // dummy values to fill the description
1183                genesis_config_hash: CryptoHash::test_hash("genesis config"),
1184                genesis_timestamp: Timestamp::from(0),
1185                genesis_committee_blob_hash: committee_blob.id().hash,
1186                name: "test network".to_string(),
1187            });
1188        }
1189        // Remember what's in the genesis store for future clients to join.
1190        self.genesis_storage_builder
1191            .add(description.clone(), public_key);
1192
1193        let network_description = self.network_description.as_ref().unwrap();
1194
1195        for validator in self.node_provider.all_nodes() {
1196            let storage = self
1197                .validator_storages
1198                .get_mut(&validator.public_key)
1199                .unwrap();
1200            storage
1201                .write_network_description(network_description)
1202                .await
1203                .expect("writing the NetworkDescription should succeed");
1204            storage
1205                .write_blob(&committee_blob)
1206                .await
1207                .expect("writing a blob should succeed");
1208            storage.create_chain(description.clone()).await.unwrap();
1209        }
1210        for storage in &mut self.chain_client_storages {
1211            storage.create_chain(description.clone()).await.unwrap();
1212        }
1213        let chain_id = description.id();
1214        self.chain_owners.insert(chain_id, public_key.into());
1215        self.make_client(chain_id, None, BlockHeight::ZERO).await
1216    }
1217
1218    /// Returns the public key and balance of each genesis root chain.
1219    pub fn genesis_chains(&self) -> Vec<(AccountPublicKey, Amount)> {
1220        let mut result = Vec::new();
1221        for (i, genesis_account) in self.genesis_storage_builder.accounts.iter().enumerate() {
1222            assert_eq!(
1223                genesis_account.description.origin(),
1224                ChainOrigin::Root(i as u32)
1225            );
1226            result.push((
1227                genesis_account.public_key,
1228                genesis_account.description.config().balance,
1229            ));
1230        }
1231        result
1232    }
1233
1234    /// Returns the admin chain's ID, panicking if it has not been initialized.
1235    pub fn admin_chain_id(&self) -> ChainId {
1236        self.admin_description
1237            .as_ref()
1238            .expect("admin chain not initialized")
1239            .id()
1240    }
1241
1242    /// Returns the admin chain description, if the admin chain has been initialized.
1243    pub fn admin_description(&self) -> Option<&ChainDescription> {
1244        self.admin_description.as_ref()
1245    }
1246
1247    /// Returns a clone of the node provider backing this test setup.
1248    pub fn make_node_provider(&self) -> NodeProvider<B::Storage> {
1249        self.node_provider.clone()
1250    }
1251
1252    /// Returns the storage of the validator at `index`, which holds every block that validator
1253    /// has processed.
1254    pub fn validator_storage(&mut self, index: usize) -> B::Storage {
1255        let public_key = self.node(index).public_key;
1256        self.validator_storages.get(&public_key).unwrap().clone()
1257    }
1258
1259    /// Returns a clone of the validator client at the given index.
1260    pub fn node(&mut self, index: usize) -> LocalValidatorClient<B::Storage> {
1261        self.node_provider.0.lock().unwrap()[index].clone()
1262    }
1263
1264    /// Builds a fresh storage seeded with the network description and genesis chains.
1265    pub async fn make_storage(&mut self) -> anyhow::Result<B::Storage> {
1266        let storage = self.storage_builder.build().await?;
1267        let network_description = self.network_description.as_ref().unwrap();
1268        let committee_blob = Blob::new_committee(bcs::to_bytes(&self.initial_committee).unwrap());
1269        storage
1270            .write_network_description(network_description)
1271            .await
1272            .expect("writing the NetworkDescription should succeed");
1273        storage
1274            .write_blob(&committee_blob)
1275            .await
1276            .expect("writing a blob should succeed");
1277        Ok(self.genesis_storage_builder.build(storage).await)
1278    }
1279
1280    /// Creates a chain client for the given chain with the given client options.
1281    pub async fn make_client_with_options(
1282        &mut self,
1283        chain_id: ChainId,
1284        block_hash: Option<CryptoHash>,
1285        block_height: BlockHeight,
1286        options: chain_client::Options,
1287        follow_only: bool,
1288    ) -> anyhow::Result<ChainClient<B::Storage>> {
1289        // Note that new clients are only given the genesis store: they must figure out
1290        // the rest by asking validators.
1291        let storage = self.make_storage().await?;
1292        self.chain_client_storages.push(storage.clone());
1293        let mode = if follow_only {
1294            crate::client::ListeningMode::FollowChain
1295        } else {
1296            crate::client::ListeningMode::FullChain
1297        };
1298        let client = Arc::new(Client::new(
1299            crate::environment::Impl {
1300                network: self.make_node_provider(),
1301                storage,
1302                signer: self.signer.clone(),
1303                wallet: TestWallet::default(),
1304            },
1305            self.admin_chain_id(),
1306            false,
1307            [(chain_id, mode)],
1308            format!("Client node for {chain_id:.8}"),
1309            Some(Duration::from_secs(30)),
1310            Some(Duration::from_secs(1)),
1311            1000,
1312            options,
1313            DEFAULT_BLOCK_CACHE_SIZE,
1314            DEFAULT_EXECUTION_STATE_CACHE_SIZE,
1315            &crate::client::RequestsSchedulerConfig::default(),
1316        ));
1317        Ok(client.create_chain_client(
1318            chain_id,
1319            block_hash,
1320            block_height,
1321            &None,
1322            self.chain_owners.get(&chain_id).copied(),
1323            None,
1324            follow_only,
1325        ))
1326    }
1327
1328    /// Creates a chain client for the given chain with default test options.
1329    pub async fn make_client(
1330        &mut self,
1331        chain_id: ChainId,
1332        block_hash: Option<CryptoHash>,
1333        block_height: BlockHeight,
1334    ) -> anyhow::Result<ChainClient<B::Storage>> {
1335        self.make_client_with_options(
1336            chain_id,
1337            block_hash,
1338            block_height,
1339            chain_client::Options::test_default(),
1340            false,
1341        )
1342        .await
1343    }
1344
1345    /// Tries to find a (confirmation) certificate for the given chain_id and block height.
1346    pub async fn check_that_validators_have_certificate(
1347        &self,
1348        chain_id: ChainId,
1349        block_height: BlockHeight,
1350        target_count: usize,
1351    ) -> Option<ConfirmedBlockCertificate> {
1352        let query = ChainInfoQuery::new(chain_id)
1353            .with_sent_certificate_hashes_by_heights(vec![block_height]);
1354        let mut count = 0;
1355        let mut certificate = None;
1356        for validator in self.node_provider.all_nodes() {
1357            if let Ok(response) = validator.handle_chain_info_query(query.clone()).await {
1358                if response.check(validator.public_key).is_ok() {
1359                    let ChainInfo {
1360                        mut requested_sent_certificate_hashes,
1361                        ..
1362                    } = *response.info;
1363                    debug_assert!(requested_sent_certificate_hashes.len() <= 1);
1364                    if let Some(cert_hash) = requested_sent_certificate_hashes.pop() {
1365                        if let Ok(cert) = validator.download_certificate(cert_hash).await {
1366                            if cert.inner().block().header.chain_id == chain_id
1367                                && cert.inner().block().header.height == block_height
1368                            {
1369                                cert.check(&self.initial_committee).unwrap();
1370                                count += 1;
1371                                certificate = Some(cert);
1372                            }
1373                        }
1374                    }
1375                }
1376            }
1377        }
1378        assert!(count >= target_count);
1379        certificate
1380    }
1381
1382    /// Tries to find a (confirmation) certificate for the given chain_id and block height, and are
1383    /// in the expected round.
1384    pub async fn check_that_validators_are_in_round(
1385        &self,
1386        chain_id: ChainId,
1387        block_height: BlockHeight,
1388        round: Round,
1389        target_count: usize,
1390    ) {
1391        let query = ChainInfoQuery::new(chain_id);
1392        let mut count = 0;
1393        for validator in self.node_provider.all_nodes() {
1394            if let Ok(response) = validator.handle_chain_info_query(query.clone()).await {
1395                if response.info.manager.current_round == round
1396                    && response.info.next_block_height == block_height
1397                    && response.check(validator.public_key).is_ok()
1398                {
1399                    count += 1;
1400                }
1401            }
1402        }
1403        assert!(count >= target_count);
1404    }
1405
1406    /// Returns how far the validator at `index` believes it has exported the given chain to each
1407    /// of the other validators.
1408    pub async fn exported_heights(
1409        &self,
1410        index: usize,
1411        chain_id: ChainId,
1412    ) -> BTreeMap<ValidatorPublicKey, BlockHeight> {
1413        let validator = self.node_provider.all_nodes()[index].clone();
1414        let guard = validator.client.lock().await;
1415        let chain = guard.state.chain_state_view(chain_id).await.unwrap();
1416        chain.exported_heights.get().clone().into()
1417    }
1418
1419    /// Returns how many chain workers the validator at `index` currently has resident.
1420    pub async fn resident_chain_workers(&self, index: usize) -> usize {
1421        let validator = self.node_provider.all_nodes()[index].clone();
1422        let guard = validator.client.lock().await;
1423        guard.state.resident_chain_worker_count()
1424    }
1425
1426    /// Returns the next block height the validator at `index` has for the given chain.
1427    pub async fn next_block_height(&self, index: usize, chain_id: ChainId) -> BlockHeight {
1428        let validator = self.node_provider.all_nodes()[index].clone();
1429        let response = validator
1430            .handle_chain_info_query(ChainInfoQuery::new(chain_id))
1431            .await
1432            .unwrap();
1433        response.info.next_block_height
1434    }
1435
1436    /// Panics if any validator has a nonempty outbox for the given chain.
1437    pub async fn check_that_validators_have_empty_outboxes(&self, chain_id: ChainId) {
1438        for validator in self.node_provider.all_nodes() {
1439            let guard = validator.client.lock().await;
1440            let chain = guard.state.chain_state_view(chain_id).await.unwrap();
1441            assert_eq!(chain.outboxes.indices().await.unwrap(), []);
1442        }
1443    }
1444}
1445
1446#[cfg(feature = "rocksdb")]
1447/// Limit concurrency for RocksDB tests to avoid "too many open files" errors.
1448static ROCKS_DB_SEMAPHORE: Semaphore = Semaphore::const_new(5);
1449
1450/// State shared by every [`StorageBuilder`] in this module. The actual
1451/// database type varies, so `build_storage` is generic over it.
1452#[derive(Default)]
1453struct CommonStorageBuilder {
1454    namespace: String,
1455    instance_counter: usize,
1456    wasm_runtime: Option<WasmRuntime>,
1457    clock: TestClock,
1458}
1459
1460impl CommonStorageBuilder {
1461    fn with_wasm_runtime(wasm_runtime: impl Into<Option<WasmRuntime>>) -> Self {
1462        Self {
1463            wasm_runtime: wasm_runtime.into(),
1464            ..Self::default()
1465        }
1466    }
1467
1468    async fn build_storage<DB>(
1469        &mut self,
1470        config: DB::Config,
1471    ) -> anyhow::Result<DbStorage<DB, TestClock>>
1472    where
1473        DB: TestKeyValueDatabase + Clone + Send + Sync + 'static,
1474        DB::Store: KeyValueStore + Clone + Send + Sync + 'static,
1475        DB::Error: std::error::Error + Send + Sync + 'static,
1476    {
1477        self.instance_counter += 1;
1478        if self.namespace.is_empty() {
1479            self.namespace = generate_test_namespace();
1480        }
1481        let namespace = format!("{}_{}", self.namespace, self.instance_counter);
1482        Ok(
1483            DbStorage::new_for_testing(config, &namespace, self.wasm_runtime, self.clock.clone())
1484                .await?,
1485        )
1486    }
1487}
1488
1489/// A [`StorageBuilder`] backed by in-memory storage.
1490#[derive(Default)]
1491pub struct MemoryStorageBuilder {
1492    inner: CommonStorageBuilder,
1493}
1494
1495impl MemoryStorageBuilder {
1496    /// Creates a [`MemoryStorageBuilder`] that uses the specified [`WasmRuntime`] to run Wasm
1497    /// applications.
1498    pub fn with_wasm_runtime(wasm_runtime: impl Into<Option<WasmRuntime>>) -> Self {
1499        Self {
1500            inner: CommonStorageBuilder::with_wasm_runtime(wasm_runtime),
1501        }
1502    }
1503}
1504
1505#[async_trait]
1506impl StorageBuilder for MemoryStorageBuilder {
1507    type Storage = DbStorage<MemoryDatabase, TestClock>;
1508
1509    async fn build(&mut self) -> Result<Self::Storage, anyhow::Error> {
1510        let config = MemoryDatabase::new_test_config().await?;
1511        self.inner.build_storage::<MemoryDatabase>(config).await
1512    }
1513
1514    fn clock(&self) -> &TestClock {
1515        &self.inner.clock
1516    }
1517}
1518
1519#[cfg(feature = "rocksdb")]
1520/// A [`StorageBuilder`] backed by RocksDB storage.
1521pub struct RocksDbStorageBuilder {
1522    inner: CommonStorageBuilder,
1523    _permit: SemaphorePermit<'static>,
1524}
1525
1526#[cfg(feature = "rocksdb")]
1527impl RocksDbStorageBuilder {
1528    /// Creates a [`RocksDbStorageBuilder`], acquiring a concurrency permit.
1529    pub async fn new() -> Self {
1530        Self {
1531            inner: CommonStorageBuilder::default(),
1532            _permit: ROCKS_DB_SEMAPHORE.acquire().await.unwrap(),
1533        }
1534    }
1535
1536    /// Creates a [`RocksDbStorageBuilder`] that uses the specified [`WasmRuntime`] to run Wasm
1537    /// applications.
1538    #[cfg(any(feature = "wasmer", feature = "wasmtime"))]
1539    pub async fn with_wasm_runtime(wasm_runtime: impl Into<Option<WasmRuntime>>) -> Self {
1540        Self {
1541            inner: CommonStorageBuilder::with_wasm_runtime(wasm_runtime),
1542            _permit: ROCKS_DB_SEMAPHORE.acquire().await.unwrap(),
1543        }
1544    }
1545}
1546
1547#[cfg(feature = "rocksdb")]
1548#[async_trait]
1549impl StorageBuilder for RocksDbStorageBuilder {
1550    type Storage = DbStorage<RocksDbDatabase, TestClock>;
1551
1552    async fn build(&mut self) -> Result<Self::Storage, anyhow::Error> {
1553        let config = RocksDbDatabase::new_test_config().await?;
1554        self.inner.build_storage::<RocksDbDatabase>(config).await
1555    }
1556
1557    fn clock(&self) -> &TestClock {
1558        &self.inner.clock
1559    }
1560}
1561
1562#[cfg(all(not(target_arch = "wasm32"), feature = "storage-service"))]
1563/// A [`StorageBuilder`] backed by the storage service.
1564#[derive(Default)]
1565pub struct ServiceStorageBuilder {
1566    inner: CommonStorageBuilder,
1567}
1568
1569#[cfg(all(not(target_arch = "wasm32"), feature = "storage-service"))]
1570impl ServiceStorageBuilder {
1571    /// Creates a `ServiceStorage`.
1572    pub fn new() -> Self {
1573        Self::with_wasm_runtime(None)
1574    }
1575
1576    /// Creates a `ServiceStorage` with the given Wasm runtime.
1577    pub fn with_wasm_runtime(wasm_runtime: impl Into<Option<WasmRuntime>>) -> Self {
1578        Self {
1579            inner: CommonStorageBuilder::with_wasm_runtime(wasm_runtime),
1580        }
1581    }
1582}
1583
1584#[cfg(all(not(target_arch = "wasm32"), feature = "storage-service"))]
1585#[async_trait]
1586impl StorageBuilder for ServiceStorageBuilder {
1587    type Storage = DbStorage<StorageServiceDatabase, TestClock>;
1588
1589    async fn build(&mut self) -> anyhow::Result<Self::Storage> {
1590        let config = StorageServiceDatabase::new_test_config().await?;
1591        self.inner
1592            .build_storage::<StorageServiceDatabase>(config)
1593            .await
1594    }
1595
1596    fn clock(&self) -> &TestClock {
1597        &self.inner.clock
1598    }
1599}
1600
1601#[cfg(feature = "scylladb")]
1602/// A [`StorageBuilder`] backed by ScyllaDB storage.
1603#[derive(Default)]
1604pub struct ScyllaDbStorageBuilder {
1605    inner: CommonStorageBuilder,
1606}
1607
1608#[cfg(feature = "scylladb")]
1609impl ScyllaDbStorageBuilder {
1610    /// Creates a [`ScyllaDbStorageBuilder`] that uses the specified [`WasmRuntime`] to run Wasm
1611    /// applications.
1612    pub fn with_wasm_runtime(wasm_runtime: impl Into<Option<WasmRuntime>>) -> Self {
1613        Self {
1614            inner: CommonStorageBuilder::with_wasm_runtime(wasm_runtime),
1615        }
1616    }
1617}
1618
1619#[cfg(feature = "scylladb")]
1620#[async_trait]
1621impl StorageBuilder for ScyllaDbStorageBuilder {
1622    type Storage = DbStorage<ScyllaDbDatabase, TestClock>;
1623
1624    async fn build(&mut self) -> Result<Self::Storage, anyhow::Error> {
1625        let config = ScyllaDbDatabase::new_test_config().await?;
1626        self.inner.build_storage::<ScyllaDbDatabase>(config).await
1627    }
1628
1629    fn clock(&self) -> &TestClock {
1630        &self.inner.clock
1631    }
1632}
1633
1634/// Helpers for asserting on [`ClientOutcome`] results in tests.
1635pub trait ClientOutcomeResultExt<T, E> {
1636    /// Unwraps the result and panics if it's not `Committed`.
1637    /// Use this when you expect the operation to succeed without conflicts.
1638    fn unwrap_ok_committed(self) -> T;
1639
1640    /// Unwraps the result, accepting both `Committed` and `Conflict` outcomes.
1641    /// Returns the committed value or the conflicting certificate (boxed).
1642    fn unwrap_ok_or_conflict(self) -> Result<T, Box<ConfirmedBlockCertificate>>;
1643}
1644
1645impl<T, E: std::fmt::Debug> ClientOutcomeResultExt<T, E> for Result<ClientOutcome<T>, E> {
1646    fn unwrap_ok_committed(self) -> T {
1647        match self.unwrap() {
1648            ClientOutcome::Committed(t) => t,
1649            ClientOutcome::WaitForTimeout(timeout) => {
1650                panic!("unexpected timeout: {timeout}")
1651            }
1652            ClientOutcome::Conflict(certificate) => {
1653                panic!("unexpected conflict: {}", certificate.hash())
1654            }
1655        }
1656    }
1657
1658    fn unwrap_ok_or_conflict(self) -> Result<T, Box<ConfirmedBlockCertificate>> {
1659        match self.unwrap() {
1660            ClientOutcome::Committed(t) => Ok(t),
1661            ClientOutcome::Conflict(certificate) => Err(certificate),
1662            ClientOutcome::WaitForTimeout(timeout) => {
1663                panic!("unexpected timeout: {timeout}")
1664            }
1665        }
1666    }
1667}