Skip to main content

sui_cluster_test/
cluster.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use super::config::{ClusterTestOpt, Env};
5use async_trait::async_trait;
6use fastcrypto::traits::ToFromBytes;
7use std::path::Path;
8use sui_config::Config;
9use sui_config::{PersistedConfig, SUI_KEYSTORE_FILENAME, SUI_NETWORK_CONFIG};
10use sui_keys::keystore::{AccountKeystore, FileBasedKeystore, Keystore};
11use sui_sdk::sui_client_config::{SuiClientConfig, SuiEnv};
12use sui_sdk::wallet_context::WalletContext;
13use sui_swarm::memory::Swarm;
14use sui_swarm_config::genesis_config::GenesisConfig;
15use sui_swarm_config::network_config::NetworkConfig;
16use sui_types::base_types::SuiAddress;
17use sui_types::crypto::KeypairTraits;
18use sui_types::crypto::SuiKeyPair;
19use sui_types::crypto::{AccountKeyPair, get_key_pair};
20use tempfile::tempdir;
21use test_cluster::{TestCluster, TestClusterBuilder};
22use tracing::info;
23
24const DEVNET_FAUCET_ADDR: &str = "https://faucet.devnet.sui.io:443";
25const STAGING_FAUCET_ADDR: &str = "https://faucet.staging.sui.io:443";
26const CONTINUOUS_FAUCET_ADDR: &str = "https://faucet.ci.sui.io:443";
27const CONTINUOUS_NOMAD_FAUCET_ADDR: &str = "https://faucet.nomad.ci.sui.io:443";
28const TESTNET_FAUCET_ADDR: &str = "https://faucet.testnet.sui.io:443";
29const DEVNET_FULLNODE_ADDR: &str = "https://rpc.devnet.sui.io:443";
30const STAGING_FULLNODE_ADDR: &str = "https://fullnode.staging.sui.io:443";
31const CONTINUOUS_FULLNODE_ADDR: &str = "https://fullnode.ci.sui.io:443";
32const CONTINUOUS_NOMAD_FULLNODE_ADDR: &str = "https://fullnode.nomad.ci.sui.io:443";
33const TESTNET_FULLNODE_ADDR: &str = "https://fullnode.testnet.sui.io:443";
34
35pub const CLUSTER_TEST_ACCOUNT_ADDRESS: &str =
36    "0x7bd7e177baf86fb745b5270cf6c391cbd1998a759904d5f27cdd2b6e1b32f99e";
37const CLUSTER_TEST_ACCOUNT_KEY_BYTES: [u8; 32] = [0x42; 32];
38
39fn cluster_test_account_key() -> AccountKeyPair {
40    // This deliberately public key is only for ephemeral test networks whose
41    // genesis config explicitly funds the matching account.
42    AccountKeyPair::from_bytes(&CLUSTER_TEST_ACCOUNT_KEY_BYTES)
43        .expect("the fixed cluster-test account key must be valid")
44}
45
46pub struct ClusterFactory;
47
48impl ClusterFactory {
49    pub async fn start(
50        options: &ClusterTestOpt,
51    ) -> Result<Box<dyn Cluster + Sync + Send>, anyhow::Error> {
52        Ok(match &options.env {
53            Env::NewLocal => Box::new(LocalNewCluster::start(options).await?),
54            _ => Box::new(RemoteRunningCluster::start(options).await?),
55        })
56    }
57}
58
59/// Cluster Abstraction
60#[async_trait]
61pub trait Cluster {
62    async fn start(options: &ClusterTestOpt) -> Result<Self, anyhow::Error>
63    where
64        Self: Sized;
65
66    fn fullnode_url(&self) -> &str;
67    fn user_key(&self) -> AccountKeyPair;
68    fn uses_prefunded_account(&self) -> bool;
69
70    /// Returns faucet url in a remote cluster.
71    fn remote_faucet_url(&self) -> Option<&str>;
72
73    /// Returns faucet key in a local cluster.
74    fn local_faucet_key(&self) -> Option<&AccountKeyPair>;
75
76    /// Place to put config for the wallet, and any locally running services.
77    fn config_directory(&self) -> &Path;
78}
79
80/// Represents an up and running cluster deployed remotely.
81pub struct RemoteRunningCluster {
82    fullnode_url: String,
83    faucet_url: String,
84    user_key: AccountKeyPair,
85    uses_prefunded_account: bool,
86    config_directory: tempfile::TempDir,
87}
88
89#[async_trait]
90impl Cluster for RemoteRunningCluster {
91    async fn start(options: &ClusterTestOpt) -> Result<Self, anyhow::Error> {
92        let (fullnode_url, faucet_url) = match options.env {
93            Env::Devnet => (
94                String::from(DEVNET_FULLNODE_ADDR),
95                String::from(DEVNET_FAUCET_ADDR),
96            ),
97            Env::Staging => (
98                String::from(STAGING_FULLNODE_ADDR),
99                String::from(STAGING_FAUCET_ADDR),
100            ),
101            Env::Ci => (
102                String::from(CONTINUOUS_FULLNODE_ADDR),
103                String::from(CONTINUOUS_FAUCET_ADDR),
104            ),
105            Env::CiNomad => (
106                String::from(CONTINUOUS_NOMAD_FULLNODE_ADDR),
107                String::from(CONTINUOUS_NOMAD_FAUCET_ADDR),
108            ),
109            Env::Testnet => (
110                String::from(TESTNET_FULLNODE_ADDR),
111                String::from(TESTNET_FAUCET_ADDR),
112            ),
113            Env::CustomRemote => (
114                options
115                    .fullnode_address
116                    .clone()
117                    .expect("Expect 'fullnode_address' for Env::Custom"),
118                options
119                    .faucet_address
120                    .clone()
121                    .expect("Expect 'faucet_address' for Env::Custom"),
122            ),
123            Env::NewLocal => unreachable!("NewLocal shouldn't use RemoteRunningCluster"),
124        };
125
126        // TODO: test connectivity before proceeding?
127
128        let uses_prefunded_account = options.use_prefunded_account();
129        let user_key = if uses_prefunded_account {
130            cluster_test_account_key()
131        } else {
132            get_key_pair().1
133        };
134
135        Ok(Self {
136            fullnode_url,
137            faucet_url,
138            user_key,
139            uses_prefunded_account,
140            config_directory: tempfile::tempdir()?,
141        })
142    }
143
144    fn fullnode_url(&self) -> &str {
145        &self.fullnode_url
146    }
147
148    fn user_key(&self) -> AccountKeyPair {
149        self.user_key.copy()
150    }
151
152    fn uses_prefunded_account(&self) -> bool {
153        self.uses_prefunded_account
154    }
155
156    fn remote_faucet_url(&self) -> Option<&str> {
157        Some(&self.faucet_url)
158    }
159
160    fn local_faucet_key(&self) -> Option<&AccountKeyPair> {
161        None
162    }
163
164    fn config_directory(&self) -> &Path {
165        self.config_directory.path()
166    }
167}
168
169/// Represents a local Cluster which starts per cluster test run.
170pub struct LocalNewCluster {
171    test_cluster: TestCluster,
172    fullnode_url: String,
173    faucet_key: AccountKeyPair,
174    config_directory: tempfile::TempDir,
175    #[allow(unused)]
176    data_ingestion_path: tempfile::TempDir,
177}
178
179impl LocalNewCluster {
180    #[allow(unused)]
181    pub fn swarm(&self) -> &Swarm {
182        &self.test_cluster.swarm
183    }
184}
185
186#[async_trait]
187impl Cluster for LocalNewCluster {
188    async fn start(options: &ClusterTestOpt) -> Result<Self, anyhow::Error> {
189        let data_ingestion_path = tempdir()?;
190
191        let mut cluster_builder = TestClusterBuilder::new()
192            .enable_fullnode_events()
193            .with_data_ingestion_dir(data_ingestion_path.path().to_path_buf());
194
195        // Check if we already have a config directory that is passed
196        if let Some(config_dir) = options.config_dir.clone() {
197            assert!(options.epoch_duration_ms.is_none());
198            // Load the config of the Sui authority.
199            let network_config_path = config_dir.join(SUI_NETWORK_CONFIG);
200            let network_config: NetworkConfig = PersistedConfig::read(&network_config_path)
201                .map_err(|err| {
202                    err.context(format!(
203                        "Cannot open Sui network config file at {:?}",
204                        network_config_path
205                    ))
206                })?;
207
208            cluster_builder = cluster_builder.set_network_config(network_config);
209            cluster_builder = cluster_builder.with_config_dir(config_dir);
210        } else {
211            // Let the faucet account hold 1000 gas objects on genesis
212            let genesis_config = GenesisConfig::custom_genesis(1, 100);
213            // Custom genesis should be build here where we add the extra accounts
214            cluster_builder = cluster_builder.set_genesis_config(genesis_config);
215
216            if let Some(epoch_duration_ms) = options.epoch_duration_ms {
217                cluster_builder = cluster_builder.with_epoch_duration_ms(epoch_duration_ms);
218            }
219        }
220
221        let mut test_cluster = cluster_builder.build().await;
222
223        // Use the wealthy account for faucet
224        let faucet_key = test_cluster.swarm.config_mut().account_keys.swap_remove(0);
225        let faucet_address = SuiAddress::from(faucet_key.public());
226        info!(?faucet_address, "faucet_address");
227
228        // This cluster has fullnode handle, safe to unwrap
229        let fullnode_url = test_cluster.fullnode_handle.rpc_url.clone();
230
231        // Let nodes connect to one another
232        tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
233
234        Ok(Self {
235            test_cluster,
236            fullnode_url,
237            faucet_key,
238            config_directory: tempfile::tempdir()?,
239            data_ingestion_path,
240        })
241    }
242
243    fn fullnode_url(&self) -> &str {
244        &self.fullnode_url
245    }
246
247    fn user_key(&self) -> AccountKeyPair {
248        get_key_pair().1
249    }
250
251    fn uses_prefunded_account(&self) -> bool {
252        false
253    }
254
255    fn remote_faucet_url(&self) -> Option<&str> {
256        None
257    }
258
259    fn local_faucet_key(&self) -> Option<&AccountKeyPair> {
260        Some(&self.faucet_key)
261    }
262
263    fn config_directory(&self) -> &Path {
264        self.config_directory.path()
265    }
266}
267
268// Make linter happy
269#[async_trait]
270impl Cluster for Box<dyn Cluster + Send + Sync> {
271    async fn start(_options: &ClusterTestOpt) -> Result<Self, anyhow::Error> {
272        unreachable!(
273            "If we already have a boxed Cluster trait object we wouldn't have to call this function"
274        );
275    }
276    fn fullnode_url(&self) -> &str {
277        (**self).fullnode_url()
278    }
279
280    fn user_key(&self) -> AccountKeyPair {
281        (**self).user_key()
282    }
283
284    fn uses_prefunded_account(&self) -> bool {
285        (**self).uses_prefunded_account()
286    }
287
288    fn remote_faucet_url(&self) -> Option<&str> {
289        (**self).remote_faucet_url()
290    }
291
292    fn local_faucet_key(&self) -> Option<&AccountKeyPair> {
293        (**self).local_faucet_key()
294    }
295
296    fn config_directory(&self) -> &Path {
297        (**self).config_directory()
298    }
299}
300
301pub async fn new_wallet_context_from_cluster(
302    cluster: &(dyn Cluster + Sync + Send),
303    key_pair: AccountKeyPair,
304) -> WalletContext {
305    let config_dir = cluster.config_directory();
306    let wallet_config_path = config_dir.join("client.yaml");
307    let fullnode_url = cluster.fullnode_url();
308    info!("Use RPC: {}", &fullnode_url);
309    let keystore_path = config_dir.join(SUI_KEYSTORE_FILENAME);
310    let mut keystore = Keystore::from(FileBasedKeystore::load_or_create(&keystore_path).unwrap());
311    let address: SuiAddress = key_pair.public().into();
312    keystore
313        .import(None, SuiKeyPair::Ed25519(key_pair))
314        .await
315        .unwrap();
316    SuiClientConfig {
317        keystore,
318        external_keys: None,
319        envs: vec![SuiEnv {
320            alias: "localnet".to_string(),
321            rpc: fullnode_url.into(),
322            ws: None,
323            basic_auth: None,
324            chain_id: None,
325        }],
326        active_address: Some(address),
327        active_env: Some("localnet".to_string()),
328    }
329    .persisted(&wallet_config_path)
330    .save()
331    .unwrap();
332
333    info!(
334        "Initialize wallet from config path: {:?}",
335        wallet_config_path
336    );
337
338    WalletContext::new(&wallet_config_path).unwrap_or_else(|e| {
339        panic!(
340            "Failed to init wallet context from path {:?}, error: {e}",
341            wallet_config_path
342        )
343    })
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn fixed_cluster_test_key_matches_funded_address() {
352        let address = SuiAddress::from(cluster_test_account_key().public());
353        assert_eq!(address.to_string(), CLUSTER_TEST_ACCOUNT_ADDRESS);
354    }
355}