Skip to main content

test_cluster/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use fastcrypto_zkp::bn254::zk_login::JwkId;
5use futures::future::join_all;
6use jsonrpsee::http_client::{HttpClient, HttpClientBuilder};
7use mysten_common::ZipDebugEqIteratorExt;
8use mysten_common::fatal;
9use rand::{Rng, distributions::*, rngs::OsRng, seq::SliceRandom};
10use std::net::SocketAddr;
11use std::num::NonZeroUsize;
12use std::path::PathBuf;
13use std::sync::{Arc, Mutex};
14use std::time::Duration;
15use sui_config::genesis::Genesis;
16use sui_config::node::FundsWithdrawSchedulerType;
17use sui_config::node::{
18    AuthorityOverloadConfig, ConsensusTransactionPoolConfig, DBCheckpointConfig, RunWithRange,
19};
20use sui_config::{Config, ExecutionCacheConfig, SUI_CLIENT_CONFIG, SUI_NETWORK_CONFIG};
21use sui_config::{NodeConfig, PersistedConfig, SUI_KEYSTORE_FILENAME};
22use sui_core::authority_aggregator::AuthorityAggregator;
23use sui_core::authority_client::NetworkAuthorityClient;
24use sui_core::transaction_driver::SubmitTransactionOptions;
25use sui_keys::keystore::{AccountKeystore, FileBasedKeystore, Keystore};
26use sui_node::SuiNodeHandle;
27use sui_protocol_config::{Chain, ProtocolVersion};
28use sui_rpc_api::Client;
29use sui_rpc_api::client::ExecutedTransaction;
30use sui_sdk::sui_client_config::{SuiClientConfig, SuiEnv};
31use sui_sdk::wallet_context::WalletContext;
32use sui_sdk::{SuiClient, SuiClientBuilder};
33use sui_swarm::memory::{Swarm, SwarmBuilder};
34use sui_swarm_config::genesis_config::{
35    AccountConfig, DEFAULT_GAS_AMOUNT, GenesisConfig, ValidatorGenesisConfig,
36};
37use sui_swarm_config::network_config::NetworkConfig;
38use sui_swarm_config::network_config_builder::{
39    FundsWithdrawSchedulerTypeConfig, GlobalStateHashV2EnabledCallback,
40    GlobalStateHashV2EnabledConfig, ProtocolVersionsConfig, SupportedProtocolVersionsCallback,
41    ValidatorObserverConfigCallback,
42};
43use sui_swarm_config::node_config_builder::{FullnodeConfigBuilder, ValidatorConfigBuilder};
44use sui_test_transaction_builder::TestTransactionBuilder;
45use sui_types::authenticator_state::get_authenticator_state;
46use sui_types::base_types::ConciseableName;
47use sui_types::base_types::{AuthorityName, ObjectID, ObjectRef, SuiAddress};
48use sui_types::committee::CommitteeTrait;
49use sui_types::committee::{Committee, EpochId};
50use sui_types::crypto::KeypairTraits;
51use sui_types::crypto::SuiKeyPair;
52use sui_types::digests::{ChainIdentifier, TransactionDigest};
53use sui_types::effects::TransactionEffectsAPI;
54use sui_types::effects::{TransactionEffects, TransactionEvents};
55use sui_types::error::{SuiErrorKind, SuiResult};
56use sui_types::messages_grpc::{
57    RawSubmitTxRequest, SubmitTxRequest, SubmitTxResult, SubmitTxType, WaitForEffectsRequest,
58    WaitForEffectsResponse,
59};
60use sui_types::object::Object;
61use sui_types::sui_system_state::SuiSystemState;
62use sui_types::sui_system_state::SuiSystemStateTrait;
63use sui_types::sui_system_state::epoch_start_sui_system_state::EpochStartSystemStateTrait;
64use sui_types::supported_protocol_versions::SupportedProtocolVersions;
65use sui_types::traffic_control::{PolicyConfig, RemoteFirewallConfig};
66use sui_types::transaction::{Transaction, TransactionData};
67use tokio::sync::broadcast;
68use tokio::time::{Instant, timeout};
69use tokio::{task::JoinHandle, time::sleep};
70use tonic::IntoRequest;
71use tracing::{error, info};
72
73pub mod addr_balance_test_env;
74
75const NUM_VALIDATOR: usize = 4;
76// Keep direct test submissions bounded like the production TransactionOrchestrator.
77const TRANSACTION_FINALITY_TIMEOUT: Duration = Duration::from_secs(90);
78
79pub struct FullNodeHandle {
80    pub sui_node: SuiNodeHandle,
81    #[deprecated = "use grpc_client"]
82    pub sui_client: SuiClient,
83    #[deprecated = "use grpc_client"]
84    pub rpc_client: HttpClient,
85    pub grpc_client: Client,
86    pub rpc_url: String,
87}
88
89impl FullNodeHandle {
90    pub async fn new(sui_node: SuiNodeHandle, json_rpc_address: SocketAddr) -> Self {
91        let rpc_url = format!("http://{}", json_rpc_address);
92        let rpc_client = HttpClientBuilder::default().build(&rpc_url).unwrap();
93
94        let sui_client = SuiClientBuilder::default().build(&rpc_url).await.unwrap();
95        let grpc_client = Client::new(&rpc_url).unwrap();
96
97        Self {
98            sui_node,
99            #[allow(deprecated)]
100            sui_client,
101            #[allow(deprecated)]
102            rpc_client,
103            grpc_client,
104            rpc_url,
105        }
106    }
107}
108
109pub struct TestCluster {
110    pub swarm: Swarm,
111    pub wallet: WalletContext,
112    pub fullnode_handle: FullNodeHandle,
113}
114
115impl TestCluster {
116    #[deprecated = "use grpc_client()"]
117    pub fn rpc_client(&self) -> &HttpClient {
118        #[allow(deprecated)]
119        &self.fullnode_handle.rpc_client
120    }
121
122    #[deprecated = "use grpc_client()"]
123    pub fn sui_client(&self) -> &SuiClient {
124        #[allow(deprecated)]
125        &self.fullnode_handle.sui_client
126    }
127
128    pub fn grpc_client(&self) -> Client {
129        self.fullnode_handle.grpc_client.clone()
130    }
131
132    pub fn rpc_url(&self) -> &str {
133        &self.fullnode_handle.rpc_url
134    }
135
136    pub fn wallet(&mut self) -> &WalletContext {
137        &self.wallet
138    }
139
140    pub fn wallet_mut(&mut self) -> &mut WalletContext {
141        &mut self.wallet
142    }
143
144    pub fn get_addresses(&self) -> Vec<SuiAddress> {
145        self.wallet.get_addresses()
146    }
147
148    // Helper function to get the 0th address in WalletContext
149    pub fn get_address_0(&self) -> SuiAddress {
150        self.get_addresses()[0]
151    }
152
153    // Helper function to get the 1st address in WalletContext
154    pub fn get_address_1(&self) -> SuiAddress {
155        self.get_addresses()[1]
156    }
157
158    // Helper function to get the 2nd address in WalletContext
159    pub fn get_address_2(&self) -> SuiAddress {
160        self.get_addresses()[2]
161    }
162
163    pub fn fullnode_config_builder(&self) -> FullnodeConfigBuilder {
164        self.swarm.get_fullnode_config_builder()
165    }
166
167    pub fn committee(&self) -> Arc<Committee> {
168        self.fullnode_handle
169            .sui_node
170            .with(|node| node.state().epoch_store_for_testing().committee().clone())
171    }
172
173    pub fn get_sui_system_state(&self) -> SuiSystemState {
174        self.fullnode_handle.sui_node.with(|node| {
175            node.state()
176                .get_sui_system_state_object_for_testing()
177                .unwrap()
178        })
179    }
180
181    /// Convenience method to start a new fullnode in the test cluster.
182    pub async fn spawn_new_fullnode(&mut self) -> FullNodeHandle {
183        self.start_fullnode_from_config(
184            self.fullnode_config_builder()
185                .build(&mut OsRng, self.swarm.config()),
186        )
187        .await
188    }
189
190    /// The observer fullnode of the cluster, when built with `with_observer_fullnode()`.
191    /// The cluster deliberately does not hold a node handle for the observer: a handle
192    /// keeps the running instance (and its open stores) alive, which prevents the
193    /// simulator from restarting the node after a crash. Acquire a fresh handle via
194    /// `Node::get_node_handle()` at the point of use and drop it promptly.
195    pub fn observer_node(&self) -> Option<&sui_swarm::memory::Node> {
196        self.swarm.observer_nodes().next()
197    }
198
199    pub async fn start_fullnode_from_config(&mut self, config: NodeConfig) -> FullNodeHandle {
200        let json_rpc_address = config.json_rpc_address;
201        let node = self.swarm.spawn_new_node(config).await;
202        FullNodeHandle::new(node, json_rpc_address).await
203    }
204
205    pub fn all_node_handles(&self) -> Vec<SuiNodeHandle> {
206        self.swarm
207            .all_nodes()
208            .flat_map(|n| n.get_node_handle())
209            .collect()
210    }
211
212    pub fn all_validator_handles(&self) -> Vec<SuiNodeHandle> {
213        self.swarm
214            .validator_nodes()
215            .map(|n| n.get_node_handle().unwrap())
216            .collect()
217    }
218
219    pub fn get_validator_pubkeys(&self) -> Vec<AuthorityName> {
220        self.swarm.active_validators().map(|v| v.name()).collect()
221    }
222
223    pub fn get_genesis(&self) -> Genesis {
224        self.swarm.config().genesis.clone()
225    }
226
227    pub fn stop_node(&self, name: &AuthorityName) {
228        self.swarm.node(name).unwrap().stop();
229    }
230
231    pub async fn stop_all_validators(&self) {
232        info!("Stopping all validators in the cluster");
233        self.swarm.active_validators().for_each(|v| v.stop());
234        tokio::time::sleep(Duration::from_secs(3)).await;
235    }
236
237    pub async fn start_all_validators(&self) {
238        info!("Starting all validators in the cluster");
239        for v in self.swarm.validator_nodes() {
240            if v.is_running() {
241                continue;
242            }
243            v.start().await.unwrap();
244        }
245        tokio::time::sleep(Duration::from_secs(3)).await;
246    }
247
248    pub async fn start_node(&self, name: &AuthorityName) {
249        let node = self.swarm.node(name).unwrap();
250        if node.is_running() {
251            return;
252        }
253        node.start().await.unwrap();
254    }
255
256    pub async fn spawn_new_validator(
257        &mut self,
258        genesis_config: ValidatorGenesisConfig,
259    ) -> SuiNodeHandle {
260        let node_config = ValidatorConfigBuilder::new()
261            .build(genesis_config, self.swarm.config().genesis.clone());
262        self.swarm.spawn_new_node(node_config).await
263    }
264
265    pub fn random_node_restarter(self: &Arc<Self>) -> RandomNodeRestarter {
266        RandomNodeRestarter::new(self.clone())
267    }
268
269    pub async fn get_reference_gas_price(&self) -> u64 {
270        self.grpc_client()
271            .get_reference_gas_price()
272            .await
273            .expect("failed to get reference gas price")
274    }
275
276    pub fn get_chain_identifier(&self) -> ChainIdentifier {
277        ChainIdentifier::from(*self.swarm.config().genesis.checkpoint().digest())
278    }
279
280    pub async fn get_object_from_fullnode_store(&self, object_id: &ObjectID) -> Option<Object> {
281        self.fullnode_handle
282            .sui_node
283            .with_async(|node| async { node.state().get_object(object_id) })
284            .await
285    }
286
287    pub async fn get_latest_object_ref(&self, object_id: &ObjectID) -> ObjectRef {
288        self.get_object_from_fullnode_store(object_id)
289            .await
290            .unwrap()
291            .compute_object_reference()
292    }
293
294    pub async fn get_object_or_tombstone_from_fullnode_store(
295        &self,
296        object_id: ObjectID,
297    ) -> ObjectRef {
298        self.fullnode_handle
299            .sui_node
300            .state()
301            .get_object_cache_reader()
302            .get_latest_object_ref_or_tombstone(object_id)
303            .unwrap()
304    }
305
306    pub async fn wait_for_run_with_range_shutdown_signal(&self) -> Option<RunWithRange> {
307        self.wait_for_run_with_range_shutdown_signal_with_timeout(Duration::from_secs(60))
308            .await
309    }
310
311    pub async fn wait_for_run_with_range_shutdown_signal_with_timeout(
312        &self,
313        timeout_dur: Duration,
314    ) -> Option<RunWithRange> {
315        let mut shutdown_channel_rx = self
316            .fullnode_handle
317            .sui_node
318            .with(|node| node.subscribe_to_shutdown_channel());
319
320        timeout(timeout_dur, async move {
321            tokio::select! {
322                msg = shutdown_channel_rx.recv() =>
323                {
324                    match msg {
325                        Ok(Some(run_with_range)) => Some(run_with_range),
326                        Ok(None) => None,
327                        Err(e) => {
328                            error!("failed recv from sui-node shutdown channel: {}", e);
329                            None
330                        },
331                    }
332                },
333            }
334        })
335        .await
336        .expect("Timed out waiting for cluster to hit target epoch and recv shutdown signal from sui-node")
337    }
338
339    pub async fn wait_for_protocol_version(
340        &self,
341        target_protocol_version: ProtocolVersion,
342    ) -> SuiSystemState {
343        self.wait_for_protocol_version_with_timeout(
344            target_protocol_version,
345            Duration::from_secs(60),
346        )
347        .await
348    }
349
350    pub async fn wait_for_protocol_version_with_timeout(
351        &self,
352        target_protocol_version: ProtocolVersion,
353        timeout_dur: Duration,
354    ) -> SuiSystemState {
355        timeout(timeout_dur, async move {
356            loop {
357                let system_state = self.wait_for_epoch(None).await;
358                if system_state.protocol_version() >= target_protocol_version.as_u64() {
359                    return system_state;
360                }
361            }
362        })
363        .await
364        .expect("Timed out waiting for cluster to target protocol version")
365    }
366
367    /// Ask 2f+1 validators to close epoch actively, and wait for the entire network to reach the next
368    /// epoch. This requires waiting for both the fullnode and all validators to reach the next epoch.
369    pub async fn trigger_reconfiguration(&self) {
370        info!("Starting reconfiguration");
371        let start = Instant::now();
372
373        // Close epoch on 2f+1 validators.
374        let cur_committee = self
375            .fullnode_handle
376            .sui_node
377            .with(|node| node.state().clone_committee_for_testing());
378        let mut cur_stake = 0;
379        for node in self.swarm.active_validators() {
380            node.get_node_handle()
381                .unwrap()
382                .with_async(|node| async {
383                    node.close_epoch_for_testing().await.unwrap_or_else(|_| {
384                        fatal!(
385                            "Failed to close epoch for validator {:?}",
386                            node.state().name
387                        );
388                    });
389                    cur_stake += cur_committee.weight(&node.state().name);
390                })
391                .await;
392            if cur_stake >= cur_committee.quorum_threshold() {
393                break;
394            }
395        }
396        info!("close_epoch complete after {:?}", start.elapsed());
397
398        self.wait_for_epoch(Some(cur_committee.epoch + 1)).await;
399        self.wait_for_epoch_all_nodes(cur_committee.epoch + 1).await;
400
401        info!("reconfiguration complete after {:?}", start.elapsed());
402    }
403
404    /// To detect whether the network has reached such state, we use the fullnode as the
405    /// source of truth, since a fullnode only does epoch transition when the network has
406    /// done so.
407    /// If target_epoch is specified, wait until the cluster reaches that epoch.
408    /// If target_epoch is None, wait until the cluster reaches the next epoch.
409    /// Note that this function does not guarantee that every node is at the target epoch.
410    pub async fn wait_for_epoch(&self, target_epoch: Option<EpochId>) -> SuiSystemState {
411        self.wait_for_epoch_with_timeout(target_epoch, Duration::from_secs(60))
412            .await
413    }
414
415    pub async fn wait_for_epoch_on_node(
416        &self,
417        handle: &SuiNodeHandle,
418        target_epoch: Option<EpochId>,
419        timeout_dur: Duration,
420    ) -> SuiSystemState {
421        let mut epoch_rx = handle.with(|node| node.subscribe_to_epoch_change());
422
423        let mut state = None;
424        timeout(timeout_dur, async {
425            let epoch = handle.with(|node| node.state().epoch_store_for_testing().epoch());
426            if Some(epoch) == target_epoch {
427                return handle.with(|node| node.state().get_sui_system_state_object_for_testing().unwrap());
428            }
429            while let Ok(system_state) = epoch_rx.recv().await {
430                info!("received epoch {}", system_state.epoch());
431                state = Some(system_state.clone());
432                match target_epoch {
433                    Some(target_epoch) if system_state.epoch() >= target_epoch => {
434                        return system_state;
435                    }
436                    None => {
437                        return system_state;
438                    }
439                    _ => (),
440                }
441            }
442            unreachable!("Broken reconfig channel");
443        })
444        .await
445        .unwrap_or_else(|_| {
446            error!("Timed out waiting for cluster to reach epoch {target_epoch:?}");
447            if let Some(state) = state {
448                panic!("Timed out waiting for cluster to reach epoch {target_epoch:?}. Current epoch: {}", state.epoch());
449            }
450            panic!("Timed out waiting for cluster to target epoch {target_epoch:?}")
451        })
452    }
453
454    pub async fn wait_for_epoch_with_timeout(
455        &self,
456        target_epoch: Option<EpochId>,
457        timeout_dur: Duration,
458    ) -> SuiSystemState {
459        self.wait_for_epoch_on_node(&self.fullnode_handle.sui_node, target_epoch, timeout_dur)
460            .await
461    }
462
463    pub async fn wait_for_epoch_all_nodes(&self, target_epoch: EpochId) {
464        let handles: Vec<_> = self
465            .swarm
466            .all_nodes()
467            .map(|node| node.get_node_handle().unwrap())
468            .collect();
469        let tasks: Vec<_> = handles
470            .iter()
471            .map(|handle| {
472                handle.with_async(|node| async {
473                    let mut retries = 0;
474                    loop {
475                        let epoch = node.state().epoch_store_for_testing().epoch();
476                        if epoch == target_epoch {
477                            if let Some(agg) = node.clone_authority_aggregator() {
478                                // This is a fullnode, we need to wait for its auth aggregator to reconfigure as well.
479                                if agg.committee.epoch() == target_epoch {
480                                    break;
481                                }
482                            } else {
483                                // This is a validator, we don't need to check the auth aggregator.
484                                break;
485                            }
486                        }
487                        tokio::time::sleep(Duration::from_secs(1)).await;
488                        retries += 1;
489                        if retries % 5 == 0 {
490                            tracing::warn!(validator=?node.state().name.concise(), "Waiting for {:?} seconds to reach epoch {:?}. Currently at epoch {:?}", retries, target_epoch, epoch);
491                        }
492                    }
493                })
494            })
495            .collect();
496
497        timeout(Duration::from_secs(40), join_all(tasks))
498            .await
499            .expect("timed out waiting for reconfiguration to complete");
500    }
501
502    /// Waits until every node advances to a strictly higher epoch than the one it is at when this is
503    /// called. Unlike `wait_for_epoch_all_nodes`, which matches a target epoch exactly, this only
504    /// requires forward progress, so it is safe when the cluster catches up through several epochs at
505    /// once (e.g. recovering from a stall) and would blow past any fixed target.
506    pub async fn wait_for_next_epoch_all_nodes(&self) {
507        let handles: Vec<_> = self
508            .swarm
509            .all_nodes()
510            .map(|node| node.get_node_handle().unwrap())
511            .collect();
512        let tasks: Vec<_> = handles
513            .iter()
514            .map(|handle| {
515                handle.with_async(|node| async {
516                    let start_epoch = node.state().epoch_store_for_testing().epoch();
517                    let mut retries = 0;
518                    loop {
519                        let epoch = node.state().epoch_store_for_testing().epoch();
520                        if epoch > start_epoch {
521                            if let Some(agg) = node.clone_authority_aggregator() {
522                                // Fullnode: also wait for its auth aggregator to reconfigure.
523                                if agg.committee.epoch() > start_epoch {
524                                    break;
525                                }
526                            } else {
527                                break;
528                            }
529                        }
530                        tokio::time::sleep(Duration::from_secs(1)).await;
531                        retries += 1;
532                        if retries % 5 == 0 {
533                            tracing::warn!(validator=?node.state().name.concise(), "Waiting {:?}s for an epoch beyond {:?}; currently at {:?}", retries, start_epoch, epoch);
534                        }
535                    }
536                })
537            })
538            .collect();
539
540        timeout(Duration::from_secs(40), join_all(tasks))
541            .await
542            .expect("timed out waiting for all nodes to advance an epoch");
543    }
544
545    pub fn subscribe_to_epoch_change(&self) -> broadcast::Receiver<SuiSystemState> {
546        // fullnode_handle is not part of swarm and cannot be dropped / killed
547        self.fullnode_handle
548            .sui_node
549            .with(|node| node.subscribe_to_epoch_change())
550    }
551
552    /// Upgrade the network protocol version, by restarting every validator with a new
553    /// supported versions.
554    /// Note that we don't restart the fullnode here, and it is assumed that the fulnode supports
555    /// the entire version range.
556    pub async fn update_validator_supported_versions(
557        &self,
558        new_supported_versions: SupportedProtocolVersions,
559    ) {
560        for authority in self.get_validator_pubkeys() {
561            self.stop_node(&authority);
562            tokio::time::sleep(Duration::from_millis(1000)).await;
563            self.swarm
564                .node(&authority)
565                .unwrap()
566                .config()
567                .supported_protocol_versions = Some(new_supported_versions);
568            self.start_node(&authority).await;
569            info!("Restarted validator {}", authority);
570        }
571    }
572
573    /// Wait for all nodes in the network to upgrade to `protocol_version`.
574    pub async fn wait_for_all_nodes_upgrade_to(&self, protocol_version: u64) {
575        for h in self.all_node_handles() {
576            h.with_async(|node| async {
577                while node
578                    .state()
579                    .epoch_store_for_testing()
580                    .epoch_start_state()
581                    .protocol_version()
582                    .as_u64()
583                    != protocol_version
584                {
585                    tokio::time::sleep(Duration::from_secs(1)).await;
586                }
587            })
588            .await;
589        }
590    }
591
592    /// Wait until the on-chain authenticator state contains any active JWK.
593    pub async fn wait_for_authenticator_state_update(&self) {
594        self.wait_for_authenticator_state_update_for_providers(&[])
595            .await;
596    }
597
598    /// Wait until the on-chain authenticator state contains all the given JWK ids.
599    pub async fn wait_for_authenticator_state_update_for_providers(&self, jwk_ids: &[JwkId]) {
600        timeout(Duration::from_secs(60), async {
601            loop {
602                let active: Vec<JwkId> = self.fullnode_handle.sui_node.with(|node| {
603                    get_authenticator_state(node.state().get_object_store())
604                        .ok()
605                        .flatten()
606                        .map(|state| {
607                            state
608                                .active_jwks
609                                .iter()
610                                .map(|active| active.jwk_id.clone())
611                                .collect()
612                        })
613                        .unwrap_or_default()
614                });
615                let ready = if jwk_ids.is_empty() {
616                    !active.is_empty()
617                } else {
618                    jwk_ids.iter().all(|id| active.contains(id))
619                };
620                if ready {
621                    return;
622                }
623                tokio::time::sleep(Duration::from_millis(500)).await;
624            }
625        })
626        .await
627        .expect("Timed out waiting for authenticator state update");
628    }
629
630    /// Return the highest observed protocol version in the test cluster.
631    pub fn highest_protocol_version(&self) -> ProtocolVersion {
632        self.all_node_handles()
633            .into_iter()
634            .map(|h| {
635                h.with(|node| {
636                    node.state()
637                        .epoch_store_for_testing()
638                        .epoch_start_state()
639                        .protocol_version()
640                })
641            })
642            .max()
643            .expect("at least one node must be up to get highest protocol version")
644    }
645
646    pub async fn test_transaction_builder(&self) -> TestTransactionBuilder {
647        let (sender, gas) = self.wallet.get_one_gas_object().await.unwrap().unwrap();
648        self.test_transaction_builder_with_gas_object(sender, gas)
649            .await
650    }
651
652    pub async fn test_transaction_builder_with_sender(
653        &self,
654        sender: SuiAddress,
655    ) -> TestTransactionBuilder {
656        let gas = self
657            .wallet
658            .get_one_gas_object_owned_by_address(sender)
659            .await
660            .unwrap()
661            .unwrap();
662        self.test_transaction_builder_with_gas_object(sender, gas)
663            .await
664    }
665
666    pub async fn test_transaction_builder_with_gas_object(
667        &self,
668        sender: SuiAddress,
669        gas: ObjectRef,
670    ) -> TestTransactionBuilder {
671        let rgp = self.get_reference_gas_price().await;
672        TestTransactionBuilder::new(sender, gas, rgp)
673    }
674
675    pub async fn sign_transaction(&self, tx_data: &TransactionData) -> Transaction {
676        self.wallet.sign_transaction(tx_data).await
677    }
678
679    pub async fn sign_and_execute_transaction(
680        &self,
681        tx_data: &TransactionData,
682    ) -> ExecutedTransaction {
683        let tx = self.wallet.sign_transaction(tx_data).await;
684        self.execute_transaction(tx).await
685    }
686
687    /// Sign and execute the transaction via direct validator submission, bypassing the fullnode.
688    pub async fn sign_and_execute_transaction_directly(
689        &self,
690        tx_data: &TransactionData,
691    ) -> SuiResult<(TransactionDigest, TransactionEffects)> {
692        let mut res = self
693            .sign_and_execute_txns_in_soft_bundle(std::slice::from_ref(tx_data))
694            .await?;
695        assert_eq!(res.len(), 1);
696        Ok(res.pop().unwrap())
697    }
698
699    /// Execute an already-signed transaction via direct validator submission, bypassing the fullnode.
700    pub async fn execute_transaction_directly(
701        &self,
702        tx: &Transaction,
703    ) -> SuiResult<(TransactionDigest, TransactionEffects)> {
704        let mut res = self
705            .execute_signed_txns_in_soft_bundle(std::slice::from_ref(tx))
706            .await?;
707        assert_eq!(res.len(), 1);
708        Ok(res.pop().unwrap())
709    }
710
711    /// Sign and execute multiple transactions in a soft bundle.
712    /// Soft bundles allow submitting multiple transactions together with best-effort
713    /// ordering if they use the same gas price. Transactions in a soft bundle can be
714    /// individually rejected or deferred without affecting other transactions.
715    ///
716    /// NOTE: This is a simplified implementation that processes transactions individually.
717    /// For true soft bundle submission, the test file should use the raw gRPC client directly
718    /// with tonic, as shown in test_soft_bundle_different_gas_payers.
719    pub async fn sign_and_execute_txns_in_soft_bundle(
720        &self,
721        txns: &[TransactionData],
722    ) -> SuiResult<Vec<(TransactionDigest, TransactionEffects)>> {
723        // Sign all transactions
724        let signed_txs: Vec<Transaction> =
725            futures::future::join_all(txns.iter().map(|tx| self.wallet.sign_transaction(tx))).await;
726
727        self.execute_signed_txns_in_soft_bundle(&signed_txs).await
728    }
729
730    pub async fn execute_signed_txns_in_soft_bundle(
731        &self,
732        signed_txs: &[Transaction],
733    ) -> SuiResult<Vec<(TransactionDigest, TransactionEffects)>> {
734        let digests: Vec<_> = signed_txs.iter().map(|tx| *tx.digest()).collect();
735
736        let request = RawSubmitTxRequest {
737            transactions: signed_txs
738                .iter()
739                .map(|tx| bcs::to_bytes(tx).unwrap().into())
740                .collect(),
741            submit_type: SubmitTxType::SoftBundle.into(),
742        };
743
744        let agg = self.authority_aggregator();
745        let clients = &agg.authority_clients;
746        // Use seeded RNG for deterministic but varying validator selection in simtests
747        let index = rand::thread_rng().gen_range(0..clients.len());
748        let (_, safe_client) = clients.iter().nth(index).unwrap();
749        let mut validator_client = safe_client
750            .authority_client()
751            .get_client_for_testing()
752            .unwrap();
753
754        let result = validator_client
755            .submit_transaction(request.into_request())
756            .await
757            .map(tonic::Response::into_inner)?;
758        assert_eq!(result.results.len(), signed_txs.len());
759
760        let mut executed_results = vec![None; signed_txs.len()];
761        let mut submitted_positions = Vec::new();
762        for (index, raw_result) in result.results.into_iter().enumerate() {
763            let submit_result: SubmitTxResult = raw_result.try_into()?;
764            match submit_result {
765                SubmitTxResult::Executed { details, .. } => {
766                    let data = details.ok_or_else(|| SuiErrorKind::GenericAuthorityError {
767                        error: "Expected execution details".to_string(),
768                    })?;
769                    executed_results[index] = Some((digests[index], data.effects));
770                }
771                SubmitTxResult::Rejected { error } => {
772                    return Err(error);
773                }
774                SubmitTxResult::Submitted { consensus_position } => {
775                    submitted_positions.push((index, consensus_position));
776                }
777            }
778        }
779
780        let wait_futures: Vec<_> = submitted_positions
781            .iter()
782            .map(|(index, position)| {
783                let request = WaitForEffectsRequest {
784                    transaction_digest: Some(digests[*index]),
785                    consensus_position: Some(*position),
786                    include_details: true,
787                    ping_type: None,
788                };
789                safe_client.wait_for_effects(request, None)
790            })
791            .collect();
792
793        let wait_responses = join_all(wait_futures).await;
794        for ((index, _), response) in submitted_positions.into_iter().zip_debug_eq(wait_responses) {
795            match response? {
796                WaitForEffectsResponse::Executed { details, .. } => {
797                    let data = details.ok_or_else(|| SuiErrorKind::GenericAuthorityError {
798                        error: "Expected execution details".to_string(),
799                    })?;
800                    executed_results[index] = Some((digests[index], data.effects));
801                }
802                WaitForEffectsResponse::Rejected { error } => {
803                    return Err(error.unwrap_or_else(|| {
804                        SuiErrorKind::GenericAuthorityError {
805                            error: "Transaction was rejected".to_string(),
806                        }
807                        .into()
808                    }));
809                }
810                WaitForEffectsResponse::Expired { .. } => {
811                    return Err(SuiErrorKind::TransactionExpired.into());
812                }
813            }
814        }
815
816        // Effects were already obtained from the validator above. Wait for the
817        // rpc fullnode to settle the transactions in an executed checkpoint and
818        // for its embedded rpc-store index to catch up, so callers that query
819        // the fullnode (e.g. RPC for owned objects or balances) after this
820        // returns observe these transactions. The embedded indexer follows the
821        // tip asynchronously and is not a blocker for execution, so the index
822        // wait is required for read-after-write consistency.
823        self.wait_for_tx_settlement(&digests).await;
824
825        executed_results
826            .into_iter()
827            .map(|result| {
828                result.ok_or_else(|| {
829                    SuiErrorKind::GenericAuthorityError {
830                        error: "Missing execution result".to_string(),
831                    }
832                    .into()
833                })
834            })
835            .collect()
836    }
837
838    /// Execute signed transactions in a soft bundle and return results for each transaction.
839    /// Unlike `execute_signed_txns_in_soft_bundle`, this method handles conflicting transactions
840    /// where some may be executed and others rejected.
841    ///
842    /// Returns a vector of (digest, WaitForEffectsResponse) for each transaction.
843    pub async fn execute_soft_bundle_with_conflicts(
844        &self,
845        signed_txs: &[Transaction],
846    ) -> SuiResult<Vec<(TransactionDigest, WaitForEffectsResponse)>> {
847        let digests: Vec<_> = signed_txs.iter().map(|tx| *tx.digest()).collect();
848
849        let request = RawSubmitTxRequest {
850            transactions: signed_txs
851                .iter()
852                .map(|tx| bcs::to_bytes(tx).unwrap().into())
853                .collect(),
854            submit_type: SubmitTxType::SoftBundle.into(),
855        };
856
857        let authority_aggregator = self.authority_aggregator();
858        let (_, safe_client) = authority_aggregator
859            .authority_clients
860            .iter()
861            .next()
862            .unwrap();
863        let mut validator_client = safe_client
864            .authority_client()
865            .get_client_for_testing()
866            .unwrap();
867
868        let result = validator_client
869            .submit_transaction(request.into_request())
870            .await
871            .map(tonic::Response::into_inner)?;
872        assert_eq!(result.results.len(), signed_txs.len());
873
874        // Extract consensus positions from submission results
875        let mut consensus_positions = Vec::new();
876        for (i, raw_result) in result.results.iter().enumerate() {
877            let submit_result: SubmitTxResult = raw_result.clone().try_into()?;
878            match submit_result {
879                SubmitTxResult::Submitted { consensus_position } => {
880                    consensus_positions.push(consensus_position);
881                }
882                SubmitTxResult::Executed { .. } => {
883                    panic!(
884                        "Transaction {} was already executed during submission",
885                        i + 1
886                    );
887                }
888                SubmitTxResult::Rejected { error } => {
889                    return Err(error);
890                }
891            }
892        }
893
894        // Wait for effects using consensus positions
895        let wait_futures: Vec<_> = digests
896            .iter()
897            .zip_debug_eq(consensus_positions.iter())
898            .map(|(digest, position)| {
899                let request = WaitForEffectsRequest {
900                    transaction_digest: Some(*digest),
901                    consensus_position: Some(*position),
902                    include_details: false,
903                    ping_type: None,
904                };
905                safe_client.wait_for_effects(request, None)
906            })
907            .collect();
908
909        let responses = futures::future::join_all(wait_futures).await;
910
911        let results: SuiResult<Vec<_>> = digests
912            .into_iter()
913            .zip_debug_eq(responses)
914            .map(|(digest, response)| Ok((digest, response?)))
915            .collect();
916
917        results
918    }
919
920    pub async fn wait_for_tx_settlement(&self, digests: &[TransactionDigest]) {
921        Self::wait_for_tx_settlement_on_handles(
922            std::slice::from_ref(&self.fullnode_handle.sui_node),
923            digests,
924        )
925        .await;
926    }
927
928    /// Like `wait_for_tx_settlement`, but waits on every node (all validators and fullnodes)
929    /// instead of only the rpc fullnode. Nodes can execute the same checkpoint at different
930    /// times, so the rpc fullnode having settled a transaction does not imply every validator
931    /// has. Use this when a subsequent step may interact with an arbitrary validator (e.g. a
932    /// soft bundle is submitted to a randomly chosen validator) and that validator must have
933    /// already applied the settlement of `digests`.
934    pub async fn wait_for_tx_settlement_all_nodes(&self, digests: &[TransactionDigest]) {
935        let handles = self.all_node_handles();
936        Self::wait_for_tx_settlement_on_handles(&handles, digests).await;
937    }
938
939    /// Waits, concurrently across `handles`, until every transaction in `digests` has settled
940    /// on that node: the transaction's checkpoint is present in the node's store and that
941    /// checkpoint has been executed (which is when the transaction's settlement is visible).
942    async fn wait_for_tx_settlement_on_handles(
943        handles: &[SuiNodeHandle],
944        digests: &[TransactionDigest],
945    ) {
946        let waits = handles.iter().map(|handle| async move {
947            let max_checkpoint_seq = handle
948                .with_async(|node| async move {
949                    let state = node.state();
950                    // wait until the transactions are in checkpoints on this node
951                    let checkpoint_seqs = state
952                        .epoch_store_for_testing()
953                        .transactions_executed_in_checkpoint_notify(digests.to_vec())
954                        .await;
955
956                    // then wait until the highest of those checkpoints is executed on this node
957                    let max_checkpoint_seq = checkpoint_seqs.into_iter().max().unwrap();
958                    state
959                        .checkpoint_store
960                        .notify_read_executed_checkpoint(max_checkpoint_seq)
961                        .await;
962                    max_checkpoint_seq
963                })
964                .await;
965
966            // The embedded rpc-store indexes asynchronously, decoupled from
967            // checkpoint execution, so a settled transaction is not yet visible
968            // through the live index surface (owned objects, balances). Wait
969            // for the live cohort to catch up so subsequent index reads
970            // observe it.
971            Self::wait_for_rpc_index_on_handle(handle, max_checkpoint_seq, false).await;
972        });
973        join_all(waits).await;
974    }
975
976    /// Wait until the embedded rpc-store on `handle` has indexed through
977    /// `checkpoint`. No-op for a node without an embedded store (a validator,
978    /// or a fullnode with indexing disabled).
979    ///
980    /// Unlike the legacy synchronous `rpc-index`, the embedded indexer follows
981    /// the tip asynchronously and is not a blocker for checkpoint execution, so
982    /// reads of the index surface must wait for it explicitly.
983    async fn wait_for_rpc_index_on_handle(
984        handle: &SuiNodeHandle,
985        checkpoint: u64,
986        wait_for_history: bool,
987    ) {
988        // Skip nodes without an embedded index; there is nothing to wait for.
989        if handle.with(|node| node.embedded_rpc_store().is_none()) {
990            return;
991        }
992        let deadline = Instant::now() + Duration::from_secs(60);
993        loop {
994            let (live_committed, history_committed) = handle.with(|node| {
995                node.embedded_rpc_store()
996                    .map(|embedded| {
997                        (
998                            embedded.live_committed_checkpoint(),
999                            embedded.history_committed_checkpoint(),
1000                        )
1001                    })
1002                    .unwrap_or((None, None))
1003            });
1004            if live_committed.is_some_and(|c| c >= checkpoint)
1005                && (!wait_for_history || history_committed.is_some_and(|c| c >= checkpoint))
1006            {
1007                return;
1008            }
1009            assert!(
1010                Instant::now() < deadline,
1011                "timed out waiting for the embedded rpc-store to index checkpoint \
1012                 {checkpoint} (live committed = {live_committed:?}, \
1013                 history committed = {history_committed:?})",
1014            );
1015            tokio::time::sleep(Duration::from_millis(50)).await;
1016        }
1017    }
1018
1019    /// Wait until the rpc fullnode's embedded rpc-store has indexed through its
1020    /// current highest executed checkpoint. Call after building the cluster so
1021    /// genesis data is queryable through index surfaces before tests issue
1022    /// their first index reads. No-op when the fullnode has indexing disabled.
1023    pub async fn wait_for_rpc_index_ready(&self) {
1024        let handle = &self.fullnode_handle.sui_node;
1025        let highest_executed = handle.with(|node| {
1026            node.state()
1027                .get_checkpoint_store()
1028                .get_highest_executed_checkpoint_seq_number()
1029                .expect("db error")
1030                .unwrap_or(0)
1031        });
1032        Self::wait_for_rpc_index_on_handle(handle, highest_executed, true).await;
1033    }
1034
1035    /// Execute a transaction on the network and wait for it to be executed on the rpc fullnode.
1036    /// Also expects the effects status to be ExecutionStatus::Success.
1037    /// This function is recommended for transaction execution since it most resembles the
1038    /// production path.
1039    pub async fn execute_transaction(&self, tx: Transaction) -> ExecutedTransaction {
1040        self.wallet.execute_transaction_must_succeed(tx).await
1041    }
1042
1043    /// Different from `execute_transaction` which returns RPC effects types, this function
1044    /// returns raw effects and events from the transaction driver.
1045    /// It also does not check whether the transaction is executed successfully.
1046    /// Before returning, it waits for the transaction to settle on the fullnode so that
1047    /// subsequent queries there read consistent results.
1048    pub async fn execute_transaction_return_raw_effects(
1049        &self,
1050        tx: Transaction,
1051    ) -> anyhow::Result<(TransactionEffects, TransactionEvents)> {
1052        let digest = *tx.digest();
1053        let results = self.submit_and_execute(tx, None).await?;
1054        self.wait_for_tx_settlement(&[digest]).await;
1055        Ok(results)
1056    }
1057
1058    pub fn authority_aggregator(&self) -> Arc<AuthorityAggregator<NetworkAuthorityClient>> {
1059        self.fullnode_handle
1060            .sui_node
1061            .with(|node| node.clone_authority_aggregator().unwrap())
1062    }
1063
1064    /// Submit a transaction through the transaction driver and wait for finality.
1065    /// Returns the raw transaction effects and events without checking execution status.
1066    pub async fn submit_and_execute(
1067        &self,
1068        tx: Transaction,
1069        client_addr: Option<SocketAddr>,
1070    ) -> anyhow::Result<(TransactionEffects, TransactionEvents)> {
1071        let transaction_driver = self.fullnode_handle.sui_node.with(|node| {
1072            node.transaction_orchestrator()
1073                .expect("fullnode must have a transaction orchestrator")
1074                .transaction_driver()
1075                .clone()
1076        });
1077        let response = transaction_driver
1078            .drive_transaction(
1079                SubmitTxRequest::new_transaction(tx),
1080                SubmitTransactionOptions {
1081                    forwarded_client_addr: client_addr,
1082                    ..Default::default()
1083                },
1084                Some(TRANSACTION_FINALITY_TIMEOUT),
1085            )
1086            .await?;
1087
1088        Ok((
1089            response.effects.effects,
1090            response.events.unwrap_or_default(),
1091        ))
1092    }
1093
1094    /// This call sends some funds from the seeded address to the funding
1095    /// address for the given amount and returns the gas object ref. This
1096    /// is useful to construct transactions from the funding address.
1097    pub async fn fund_address_and_return_gas(
1098        &self,
1099        rgp: u64,
1100        amount: Option<u64>,
1101        funding_address: SuiAddress,
1102    ) -> ObjectRef {
1103        let context = &self.wallet;
1104        let (sender, gas) = context.get_one_gas_object().await.unwrap().unwrap();
1105        let tx = context
1106            .sign_transaction(
1107                &TestTransactionBuilder::new(sender, gas, rgp)
1108                    .transfer_sui(amount, funding_address)
1109                    .build(),
1110            )
1111            .await;
1112        context.execute_transaction_must_succeed(tx).await;
1113
1114        context
1115            .get_one_gas_object_owned_by_address(funding_address)
1116            .await
1117            .unwrap()
1118            .unwrap()
1119    }
1120
1121    pub async fn transfer_sui_must_exceed(
1122        &self,
1123        sender: SuiAddress,
1124        receiver: SuiAddress,
1125        amount: u64,
1126    ) -> ObjectID {
1127        let tx = self
1128            .test_transaction_builder_with_sender(sender)
1129            .await
1130            .transfer_sui(Some(amount), receiver)
1131            .build();
1132        let effects = self.sign_and_execute_transaction(&tx).await.effects;
1133        assert!(effects.status().is_ok());
1134        effects.created().first().unwrap().0.0
1135    }
1136
1137    #[cfg(msim)]
1138    pub fn set_safe_mode_expected(&self, value: bool) {
1139        for n in self.all_node_handles() {
1140            n.with(|node| node.set_safe_mode_expected(value));
1141        }
1142    }
1143}
1144
1145pub struct RandomNodeRestarter {
1146    test_cluster: Arc<TestCluster>,
1147
1148    // How frequently should we kill nodes
1149    kill_interval: Uniform<Duration>,
1150    // How long should we wait before restarting them.
1151    restart_delay: Uniform<Duration>,
1152
1153    task_handle: Mutex<Option<JoinHandle<()>>>,
1154}
1155
1156impl RandomNodeRestarter {
1157    fn new(test_cluster: Arc<TestCluster>) -> Self {
1158        Self {
1159            test_cluster,
1160            kill_interval: Uniform::new(Duration::from_secs(10), Duration::from_secs(11)),
1161            restart_delay: Uniform::new(Duration::from_secs(1), Duration::from_secs(2)),
1162            task_handle: Default::default(),
1163        }
1164    }
1165
1166    pub fn with_kill_interval_secs(mut self, a: u64, b: u64) -> Self {
1167        self.kill_interval = Uniform::new(Duration::from_secs(a), Duration::from_secs(b));
1168        self
1169    }
1170
1171    pub fn with_restart_delay_secs(mut self, a: u64, b: u64) -> Self {
1172        self.restart_delay = Uniform::new(Duration::from_secs(a), Duration::from_secs(b));
1173        self
1174    }
1175
1176    pub fn run(&self) {
1177        let test_cluster = self.test_cluster.clone();
1178        let kill_interval = self.kill_interval;
1179        let restart_delay = self.restart_delay;
1180        let validators = self.test_cluster.get_validator_pubkeys();
1181        let mut task_handle = self.task_handle.lock().unwrap();
1182        assert!(task_handle.is_none());
1183        task_handle.replace(tokio::task::spawn(async move {
1184            loop {
1185                let delay = kill_interval.sample(&mut OsRng);
1186                info!("Sleeping {delay:?} before killing a validator");
1187                sleep(delay).await;
1188
1189                let validator = validators.choose(&mut OsRng).unwrap();
1190                info!("Killing validator {:?}", validator.concise());
1191                test_cluster.stop_node(validator);
1192
1193                let delay = restart_delay.sample(&mut OsRng);
1194                info!("Sleeping {delay:?} before restarting");
1195                sleep(delay).await;
1196                info!("Starting validator {:?}", validator.concise());
1197                test_cluster.start_node(validator).await;
1198            }
1199        }));
1200    }
1201}
1202
1203impl Drop for RandomNodeRestarter {
1204    fn drop(&mut self) {
1205        if let Some(handle) = self.task_handle.lock().unwrap().take() {
1206            handle.abort();
1207        }
1208    }
1209}
1210
1211pub struct TestClusterBuilder {
1212    genesis_config: Option<GenesisConfig>,
1213    network_config: Option<NetworkConfig>,
1214    additional_objects: Vec<Object>,
1215    num_validators: Option<usize>,
1216    validators: Option<Vec<ValidatorGenesisConfig>>,
1217    fullnode_rpc_port: Option<u16>,
1218    enable_fullnode_events: bool,
1219    disable_fullnode_pruning: bool,
1220    validator_supported_protocol_versions_config: ProtocolVersionsConfig,
1221    // Default to validator_supported_protocol_versions_config, but can be overridden.
1222    fullnode_supported_protocol_versions_config: Option<ProtocolVersionsConfig>,
1223    db_checkpoint_config_validators: DBCheckpointConfig,
1224    db_checkpoint_config_fullnodes: DBCheckpointConfig,
1225    num_unpruned_validators: Option<usize>,
1226    jwk_fetch_interval: Option<Duration>,
1227    config_dir: Option<PathBuf>,
1228    default_jwks: bool,
1229    authority_overload_config: Option<AuthorityOverloadConfig>,
1230    consensus_transaction_pool_config: Option<ConsensusTransactionPoolConfig>,
1231    execution_cache_config: Option<ExecutionCacheConfig>,
1232    data_ingestion_dir: Option<PathBuf>,
1233    fullnode_run_with_range: Option<RunWithRange>,
1234    fullnode_policy_config: Option<PolicyConfig>,
1235    fullnode_fw_config: Option<RemoteFirewallConfig>,
1236
1237    validator_global_state_hash_v2_enabled_config: GlobalStateHashV2EnabledConfig,
1238    validator_funds_withdraw_scheduler_type_config: FundsWithdrawSchedulerTypeConfig,
1239
1240    rpc_config: Option<sui_config::RpcConfig>,
1241
1242    chain_override: Option<Chain>,
1243
1244    execution_time_observer_config: Option<sui_config::node::ExecutionTimeObserverConfig>,
1245
1246    validator_observer_config: Option<ValidatorObserverConfigCallback>,
1247
1248    observer_fullnode: bool,
1249
1250    state_sync_config: Option<sui_config::p2p::StateSyncConfig>,
1251
1252    peer_deny_sync_config_callback:
1253        Option<sui_swarm_config::network_config_builder::PeerDenySyncConfigCallback>,
1254
1255    #[cfg(msim)]
1256    inject_synthetic_execution_time: bool,
1257}
1258
1259impl TestClusterBuilder {
1260    pub fn new() -> Self {
1261        TestClusterBuilder {
1262            genesis_config: None,
1263            network_config: None,
1264            chain_override: None,
1265            additional_objects: vec![],
1266            fullnode_rpc_port: None,
1267            num_validators: None,
1268            validators: None,
1269            enable_fullnode_events: false,
1270            disable_fullnode_pruning: false,
1271            validator_supported_protocol_versions_config: ProtocolVersionsConfig::Default,
1272            fullnode_supported_protocol_versions_config: None,
1273            db_checkpoint_config_validators: DBCheckpointConfig::default(),
1274            db_checkpoint_config_fullnodes: DBCheckpointConfig::default(),
1275            num_unpruned_validators: None,
1276            jwk_fetch_interval: None,
1277            config_dir: None,
1278            default_jwks: false,
1279            authority_overload_config: None,
1280            consensus_transaction_pool_config: None,
1281            execution_cache_config: None,
1282            data_ingestion_dir: None,
1283            fullnode_run_with_range: None,
1284            fullnode_policy_config: None,
1285            fullnode_fw_config: None,
1286            validator_global_state_hash_v2_enabled_config: GlobalStateHashV2EnabledConfig::Global(
1287                true,
1288            ),
1289            validator_funds_withdraw_scheduler_type_config:
1290                FundsWithdrawSchedulerTypeConfig::PerValidator(Arc::new(|idx| {
1291                    if idx % 2 == 0 {
1292                        FundsWithdrawSchedulerType::Eager
1293                    } else {
1294                        FundsWithdrawSchedulerType::Naive
1295                    }
1296                })),
1297            rpc_config: None,
1298            execution_time_observer_config: None,
1299            validator_observer_config: None,
1300            observer_fullnode: false,
1301            state_sync_config: None,
1302            peer_deny_sync_config_callback: None,
1303            #[cfg(msim)]
1304            inject_synthetic_execution_time: false,
1305        }
1306    }
1307
1308    pub fn with_state_sync_config(mut self, config: sui_config::p2p::StateSyncConfig) -> Self {
1309        self.state_sync_config = Some(config);
1310        self
1311    }
1312
1313    /// Per-validator hook for `peer_deny_sync_config`. The closure receives this
1314    /// validator's authority name and the slice of all genesis-committee authority
1315    /// names, so callers can compute an allowlist that references peers (e.g.
1316    /// "trust everyone but myself").
1317    pub fn with_peer_deny_sync_config_per_validator(
1318        mut self,
1319        f: sui_swarm_config::network_config_builder::PeerDenySyncConfigCallback,
1320    ) -> Self {
1321        self.peer_deny_sync_config_callback = Some(f);
1322        self
1323    }
1324
1325    pub fn with_execution_time_observer_config(
1326        mut self,
1327        config: sui_config::node::ExecutionTimeObserverConfig,
1328    ) -> Self {
1329        self.execution_time_observer_config = Some(config);
1330        self
1331    }
1332
1333    pub fn with_validator_observer_config(mut self, c: ValidatorObserverConfigCallback) -> Self {
1334        self.validator_observer_config = Some(c);
1335        self
1336    }
1337
1338    /// Adds a fullnode to the cluster that syncs as a consensus observer, subscribed to the
1339    /// first validator. Unless a validator observer config is provided, the first validator
1340    /// gets its observer server enabled. The observer is available via
1341    /// `TestCluster::observer_node()`.
1342    ///
1343    /// The first validator must end up with its observer server enabled, which `build()`
1344    /// validates: a callback passed to `with_validator_observer_config` must return
1345    /// `Some(_)` for index 0, and a prebuilt network config passed to `set_network_config`
1346    /// must already enable the observer server on the first validator (the callback is not
1347    /// applied to prebuilt network configs).
1348    pub fn with_observer_fullnode(mut self) -> Self {
1349        self.observer_fullnode = true;
1350        self
1351    }
1352
1353    pub fn with_fullnode_run_with_range(mut self, run_with_range: Option<RunWithRange>) -> Self {
1354        if let Some(run_with_range) = run_with_range {
1355            self.fullnode_run_with_range = Some(run_with_range);
1356        }
1357        self
1358    }
1359
1360    pub fn with_fullnode_policy_config(mut self, config: Option<PolicyConfig>) -> Self {
1361        self.fullnode_policy_config = config;
1362        self
1363    }
1364
1365    pub fn with_fullnode_fw_config(mut self, config: Option<RemoteFirewallConfig>) -> Self {
1366        self.fullnode_fw_config = config;
1367        self
1368    }
1369
1370    pub fn with_fullnode_rpc_port(mut self, rpc_port: u16) -> Self {
1371        self.fullnode_rpc_port = Some(rpc_port);
1372        self
1373    }
1374
1375    pub fn set_genesis_config(mut self, genesis_config: GenesisConfig) -> Self {
1376        assert!(self.genesis_config.is_none() && self.network_config.is_none());
1377        self.genesis_config = Some(genesis_config);
1378        self
1379    }
1380
1381    pub fn set_network_config(mut self, network_config: NetworkConfig) -> Self {
1382        assert!(self.genesis_config.is_none() && self.network_config.is_none());
1383        self.network_config = Some(network_config);
1384        self
1385    }
1386
1387    pub fn with_objects<I: IntoIterator<Item = Object>>(mut self, objects: I) -> Self {
1388        self.additional_objects.extend(objects);
1389        self
1390    }
1391
1392    /// Set the number of default validators to spawn. Can be overridden by `with_validators`, if
1393    /// you need to provide more specific genesis configs for each validator.
1394    pub fn with_num_validators(mut self, num: usize) -> Self {
1395        self.num_validators = Some(num);
1396        self
1397    }
1398
1399    /// Provide validator genesis configs, overrides the `num_validators` setting.
1400    pub fn with_validators(mut self, validators: Vec<ValidatorGenesisConfig>) -> Self {
1401        self.validators = Some(validators);
1402        self
1403    }
1404
1405    pub fn enable_fullnode_events(mut self) -> Self {
1406        self.enable_fullnode_events = true;
1407        self
1408    }
1409
1410    pub fn disable_fullnode_pruning(mut self) -> Self {
1411        self.disable_fullnode_pruning = true;
1412        self
1413    }
1414
1415    pub fn with_enable_db_checkpoints_validators(mut self) -> Self {
1416        self.db_checkpoint_config_validators = DBCheckpointConfig {
1417            perform_db_checkpoints_at_epoch_end: true,
1418            checkpoint_path: None,
1419            object_store_config: None,
1420            perform_index_db_checkpoints_at_epoch_end: None,
1421            prune_and_compact_before_upload: None,
1422        };
1423        self
1424    }
1425
1426    pub fn with_enable_db_checkpoints_fullnodes(mut self) -> Self {
1427        self.db_checkpoint_config_fullnodes = DBCheckpointConfig {
1428            perform_db_checkpoints_at_epoch_end: true,
1429            checkpoint_path: None,
1430            object_store_config: None,
1431            perform_index_db_checkpoints_at_epoch_end: None,
1432            prune_and_compact_before_upload: Some(true),
1433        };
1434        self
1435    }
1436
1437    pub fn with_epoch_duration_ms(mut self, epoch_duration_ms: u64) -> Self {
1438        assert!(
1439            epoch_duration_ms >= 10000,
1440            "Epoch duration must be at least 10s (10000ms) to avoid flaky tests. Got {epoch_duration_ms}ms."
1441        );
1442        self.get_or_init_genesis_config()
1443            .parameters
1444            .epoch_duration_ms = epoch_duration_ms;
1445        self
1446    }
1447
1448    pub fn with_stake_subsidy_start_epoch(mut self, stake_subsidy_start_epoch: u64) -> Self {
1449        self.get_or_init_genesis_config()
1450            .parameters
1451            .stake_subsidy_start_epoch = stake_subsidy_start_epoch;
1452        self
1453    }
1454
1455    pub fn with_supported_protocol_versions(mut self, c: SupportedProtocolVersions) -> Self {
1456        self.validator_supported_protocol_versions_config = ProtocolVersionsConfig::Global(c);
1457        self
1458    }
1459
1460    pub fn with_jwk_fetch_interval(mut self, i: Duration) -> Self {
1461        self.jwk_fetch_interval = Some(i);
1462        self
1463    }
1464
1465    pub fn with_fullnode_supported_protocol_versions_config(
1466        mut self,
1467        c: SupportedProtocolVersions,
1468    ) -> Self {
1469        self.fullnode_supported_protocol_versions_config = Some(ProtocolVersionsConfig::Global(c));
1470        self
1471    }
1472
1473    pub fn with_protocol_version(mut self, v: ProtocolVersion) -> Self {
1474        self.get_or_init_genesis_config()
1475            .parameters
1476            .protocol_version = v;
1477        self
1478    }
1479
1480    pub fn with_supported_protocol_version_callback(
1481        mut self,
1482        func: SupportedProtocolVersionsCallback,
1483    ) -> Self {
1484        self.validator_supported_protocol_versions_config =
1485            ProtocolVersionsConfig::PerValidator(func);
1486        self
1487    }
1488
1489    pub fn with_global_state_hash_v2_enabled_callback(
1490        mut self,
1491        func: GlobalStateHashV2EnabledCallback,
1492    ) -> Self {
1493        self.validator_global_state_hash_v2_enabled_config =
1494            GlobalStateHashV2EnabledConfig::PerValidator(func);
1495        self
1496    }
1497
1498    pub fn with_validator_candidates(
1499        mut self,
1500        addresses: impl IntoIterator<Item = SuiAddress>,
1501    ) -> Self {
1502        self.get_or_init_genesis_config()
1503            .accounts
1504            .extend(addresses.into_iter().map(|address| AccountConfig {
1505                address: Some(address),
1506                gas_amounts: vec![DEFAULT_GAS_AMOUNT, DEFAULT_GAS_AMOUNT],
1507            }));
1508        self
1509    }
1510
1511    pub fn with_num_unpruned_validators(mut self, n: usize) -> Self {
1512        self.num_unpruned_validators = Some(n);
1513        self
1514    }
1515
1516    pub fn with_accounts(mut self, accounts: Vec<AccountConfig>) -> Self {
1517        self.get_or_init_genesis_config().accounts = accounts;
1518        self
1519    }
1520
1521    pub fn with_additional_accounts(mut self, accounts: Vec<AccountConfig>) -> Self {
1522        self.get_or_init_genesis_config().accounts.extend(accounts);
1523        self
1524    }
1525
1526    pub fn with_config_dir(mut self, config_dir: PathBuf) -> Self {
1527        self.config_dir = Some(config_dir);
1528        self
1529    }
1530
1531    pub fn with_default_jwks(mut self) -> Self {
1532        self.default_jwks = true;
1533        self
1534    }
1535
1536    pub fn with_authority_overload_config(mut self, config: AuthorityOverloadConfig) -> Self {
1537        assert!(self.network_config.is_none());
1538        self.authority_overload_config = Some(config);
1539        self
1540    }
1541
1542    pub fn with_consensus_transaction_pool_config(
1543        mut self,
1544        config: ConsensusTransactionPoolConfig,
1545    ) -> Self {
1546        assert!(self.network_config.is_none());
1547        self.consensus_transaction_pool_config = Some(config);
1548        self
1549    }
1550
1551    pub fn with_execution_cache_config(mut self, config: ExecutionCacheConfig) -> Self {
1552        assert!(self.network_config.is_none());
1553        self.execution_cache_config = Some(config);
1554        self
1555    }
1556
1557    pub fn with_data_ingestion_dir(mut self, path: PathBuf) -> Self {
1558        self.data_ingestion_dir = Some(path);
1559        self
1560    }
1561
1562    pub fn with_rpc_config(mut self, config: sui_config::RpcConfig) -> Self {
1563        self.rpc_config = Some(config);
1564        self
1565    }
1566
1567    pub fn with_chain_override(mut self, chain: Chain) -> Self {
1568        self.chain_override = Some(chain);
1569        self
1570    }
1571
1572    #[cfg(msim)]
1573    pub fn with_synthetic_execution_time_injection(mut self) -> Self {
1574        self.inject_synthetic_execution_time = true;
1575        self
1576    }
1577
1578    pub async fn build(mut self) -> TestCluster {
1579        // All test clusters receive a continuous stream of random JWKs.
1580        // If we later use zklogin authenticated transactions in tests we will need to supply
1581        // valid JWKs as well.
1582        #[cfg(msim)]
1583        if !self.default_jwks {
1584            sui_node::set_jwk_injector(Arc::new(|_authority, provider| {
1585                use fastcrypto_zkp::bn254::zk_login::{JWK, JwkId};
1586                use rand::Rng;
1587
1588                // generate random (and possibly conflicting) id/key pairings.
1589                let id_num = rand::thread_rng().gen_range(1..=4);
1590                let key_num = rand::thread_rng().gen_range(1..=4);
1591
1592                let id = JwkId {
1593                    iss: provider.get_config().iss,
1594                    kid: format!("kid{}", id_num),
1595                };
1596
1597                let jwk = JWK {
1598                    kty: "kty".to_string(),
1599                    e: "e".to_string(),
1600                    n: format!("n{}", key_num),
1601                    alg: "alg".to_string(),
1602                };
1603
1604                Ok(vec![(id, jwk)])
1605            }));
1606        }
1607
1608        if self.observer_fullnode {
1609            // The observer fullnode subscribes to the first validator, so its observer server
1610            // must be enabled. Validate this up front to fail with an actionable error instead
1611            // of a deep panic in `observer_peer_record` when the observer's config is built.
1612            if let Some(network_config) = &self.network_config {
1613                // A prebuilt network config bypasses the validator observer config callback,
1614                // so it must already have the observer server enabled.
1615                let observer_enabled = network_config
1616                    .validator_configs()
1617                    .first()
1618                    .and_then(|c| c.consensus_config())
1619                    .and_then(|c| c.parameters.as_ref())
1620                    .and_then(|p| p.observer.server_port)
1621                    .is_some();
1622                assert!(
1623                    observer_enabled,
1624                    "with_observer_fullnode() requires the first validator's observer server \
1625                     to be enabled, but the network config passed to set_network_config() does \
1626                     not enable it. Enable the observer server on the first validator when \
1627                     building the network config."
1628                );
1629            } else if let Some(cb) = &self.validator_observer_config {
1630                assert!(
1631                    cb(0).is_some(),
1632                    "with_observer_fullnode() requires the validator observer config callback \
1633                     to enable the observer server on the first validator (index 0)."
1634                );
1635            } else {
1636                self.validator_observer_config = Some(Arc::new(|idx| {
1637                    (idx == 0).then(consensus_config::ObserverParameters::default)
1638                }));
1639            }
1640        }
1641
1642        let mut swarm = self.start_swarm().await.unwrap();
1643        let working_dir = swarm.dir().to_path_buf();
1644
1645        let fullnode = swarm.fullnodes().next().unwrap();
1646        let json_rpc_address = fullnode.config().json_rpc_address;
1647        let fullnode_handle =
1648            FullNodeHandle::new(fullnode.get_node_handle().unwrap(), json_rpc_address).await;
1649
1650        if self.observer_fullnode {
1651            // Boxed so the frame (NodeConfig by value, held across the await) lives on
1652            // the heap instead of inflating build()'s state machine for every caller,
1653            // most of which never enable the observer. Unoptimized async frames are
1654            // large enough that this tips borderline test binaries over the 2 MiB
1655            // tokio worker stack.
1656            Box::pin(async {
1657                let mut config = swarm
1658                    .get_fullnode_config_builder()
1659                    .with_observer_subscribed_to_validator(0)
1660                    .build(&mut OsRng, swarm.config());
1661                // An observer that halts at a checkpoint range boundary defeats its purpose.
1662                config.run_with_range = None;
1663                // Deliberately drop the returned node handle: holding it would keep the
1664                // instance alive across a crash and prevent the simulator from restarting
1665                // the node. Access the observer via `TestCluster::observer_node()`.
1666                swarm.spawn_new_node(config).await;
1667            })
1668            .await;
1669        }
1670
1671        let mut wallet_conf: SuiClientConfig =
1672            PersistedConfig::read(&working_dir.join(SUI_CLIENT_CONFIG)).unwrap();
1673        wallet_conf.envs.push(SuiEnv {
1674            alias: "localnet".to_string(),
1675            rpc: fullnode_handle.rpc_url.clone(),
1676            ws: None,
1677            basic_auth: None,
1678            chain_id: None,
1679        });
1680        wallet_conf.active_env = Some("localnet".to_string());
1681
1682        wallet_conf
1683            .persisted(&working_dir.join(SUI_CLIENT_CONFIG))
1684            .save()
1685            .unwrap();
1686
1687        let wallet_conf = swarm.dir().join(SUI_CLIENT_CONFIG);
1688        let wallet = WalletContext::new(&wallet_conf).unwrap();
1689
1690        let cluster = TestCluster {
1691            swarm,
1692            wallet,
1693            fullnode_handle,
1694        };
1695
1696        // The embedded rpc-store indexes the tip asynchronously, so genesis
1697        // data is not queryable through every index surface the instant the node
1698        // is up. Wait for it before handing the cluster to tests.
1699        cluster.wait_for_rpc_index_ready().await;
1700
1701        cluster
1702    }
1703
1704    /// Start a Swarm and set up WalletConfig
1705    async fn start_swarm(&mut self) -> Result<Swarm, anyhow::Error> {
1706        let mut builder: SwarmBuilder = Swarm::builder()
1707            .with_objects(self.additional_objects.clone())
1708            .with_db_checkpoint_config(self.db_checkpoint_config_validators.clone())
1709            .with_supported_protocol_versions_config(
1710                self.validator_supported_protocol_versions_config.clone(),
1711            )
1712            .with_global_state_hash_v2_enabled_config(
1713                self.validator_global_state_hash_v2_enabled_config.clone(),
1714            )
1715            .with_funds_withdraw_scheduler_type_config(
1716                self.validator_funds_withdraw_scheduler_type_config.clone(),
1717            )
1718            .with_fullnode_count(1)
1719            .with_fullnode_supported_protocol_versions_config(
1720                self.fullnode_supported_protocol_versions_config
1721                    .clone()
1722                    .unwrap_or(self.validator_supported_protocol_versions_config.clone()),
1723            )
1724            .with_db_checkpoint_config(self.db_checkpoint_config_fullnodes.clone())
1725            .with_fullnode_run_with_range(self.fullnode_run_with_range)
1726            .with_fullnode_policy_config(self.fullnode_policy_config.clone())
1727            .with_fullnode_fw_config(self.fullnode_fw_config.clone());
1728
1729        if let Some(validators) = self.validators.take() {
1730            builder = builder.with_validators(validators);
1731        } else {
1732            builder = builder.committee_size(
1733                NonZeroUsize::new(self.num_validators.unwrap_or(NUM_VALIDATOR)).unwrap(),
1734            )
1735        };
1736
1737        if let Some(chain) = self.chain_override {
1738            builder = builder.with_chain_override(chain);
1739        }
1740
1741        if let Some(genesis_config) = self.genesis_config.take() {
1742            builder = builder.with_genesis_config(genesis_config);
1743        }
1744
1745        if let Some(network_config) = self.network_config.take() {
1746            builder = builder.with_network_config(network_config);
1747        }
1748
1749        if let Some(authority_overload_config) = self.authority_overload_config.take() {
1750            builder = builder.with_authority_overload_config(authority_overload_config);
1751        }
1752
1753        if let Some(config) = self.consensus_transaction_pool_config.take() {
1754            builder = builder.with_consensus_transaction_pool_config(config);
1755        }
1756
1757        if let Some(execution_cache_config) = self.execution_cache_config.take() {
1758            builder = builder.with_execution_cache_config(execution_cache_config);
1759        }
1760
1761        if let Some(fullnode_rpc_port) = self.fullnode_rpc_port {
1762            builder = builder.with_fullnode_rpc_port(fullnode_rpc_port);
1763        }
1764
1765        if let Some(rpc_config) = &self.rpc_config {
1766            builder = builder.with_fullnode_rpc_config(rpc_config.clone());
1767        }
1768        if let Some(num_unpruned_validators) = self.num_unpruned_validators {
1769            builder = builder.with_num_unpruned_validators(num_unpruned_validators);
1770        }
1771
1772        if let Some(jwk_fetch_interval) = self.jwk_fetch_interval {
1773            builder = builder.with_jwk_fetch_interval(jwk_fetch_interval);
1774        }
1775
1776        if let Some(config_dir) = self.config_dir.take() {
1777            builder = builder.dir(config_dir);
1778        }
1779
1780        if let Some(data_ingestion_dir) = self.data_ingestion_dir.take() {
1781            builder = builder.with_data_ingestion_dir(data_ingestion_dir);
1782        }
1783
1784        if let Some(state_sync_config) = self.state_sync_config.clone() {
1785            builder = builder.with_state_sync_config(state_sync_config);
1786        }
1787
1788        if let Some(cb) = self.peer_deny_sync_config_callback.clone() {
1789            builder = builder.with_peer_deny_sync_config_per_validator(cb);
1790        }
1791
1792        if self.disable_fullnode_pruning {
1793            builder = builder.with_disable_fullnode_pruning();
1794        }
1795
1796        if let Some(validator_observer_config) = self.validator_observer_config.take() {
1797            builder = builder.with_validator_observer_config(validator_observer_config);
1798        }
1799
1800        #[cfg(msim)]
1801        {
1802            if let Some(mut config) = self.execution_time_observer_config.clone() {
1803                if self.inject_synthetic_execution_time {
1804                    config.inject_synthetic_execution_time = Some(true);
1805                }
1806                builder = builder.with_execution_time_observer_config(config);
1807            } else if self.inject_synthetic_execution_time {
1808                use sui_config::node::ExecutionTimeObserverConfig;
1809
1810                let mut config = ExecutionTimeObserverConfig::default();
1811                config.inject_synthetic_execution_time = Some(true);
1812                builder = builder.with_execution_time_observer_config(config);
1813            }
1814        }
1815
1816        let mut swarm = builder.build();
1817        swarm.launch().await?;
1818
1819        let dir = swarm.dir();
1820
1821        let network_path = dir.join(SUI_NETWORK_CONFIG);
1822        let wallet_path = dir.join(SUI_CLIENT_CONFIG);
1823        let keystore_path = dir.join(SUI_KEYSTORE_FILENAME);
1824
1825        swarm.config().save(network_path)?;
1826        let mut keystore = Keystore::from(FileBasedKeystore::load_or_create(&keystore_path)?);
1827        for key in &swarm.config().account_keys {
1828            keystore
1829                .import(None, SuiKeyPair::Ed25519(key.copy()))
1830                .await?;
1831        }
1832
1833        let active_address = keystore.addresses().first().cloned();
1834
1835        // Create wallet config with stated authorities port
1836        SuiClientConfig {
1837            keystore: Keystore::from(FileBasedKeystore::load_or_create(&keystore_path)?),
1838            external_keys: None,
1839            envs: Default::default(),
1840            active_address,
1841            active_env: Default::default(),
1842        }
1843        .save(wallet_path)?;
1844
1845        // Return network handle
1846        Ok(swarm)
1847    }
1848
1849    fn get_or_init_genesis_config(&mut self) -> &mut GenesisConfig {
1850        if self.genesis_config.is_none() {
1851            self.genesis_config = Some(GenesisConfig::for_local_testing());
1852        }
1853        self.genesis_config.as_mut().unwrap()
1854    }
1855}
1856
1857impl Default for TestClusterBuilder {
1858    fn default() -> Self {
1859        Self::new()
1860    }
1861}