1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use fedimint_api_client::api::{DynGlobalApi, FederationApiExt};
6use fedimint_client::module_init::ClientModuleInitRegistry;
7use fedimint_client::{Client, ClientHandleArc, RootSecret};
8use fedimint_client_module::AdminCreds;
9use fedimint_client_module::secret::{PlainRootSecretStrategy, RootSecretStrategy};
10use fedimint_connectors::ConnectorRegistry;
11use fedimint_core::PeerId;
12use fedimint_core::config::{ClientConfig, FederationId};
13use fedimint_core::core::ModuleKind;
14use fedimint_core::db::Database;
15use fedimint_core::db::mem_impl::MemDatabase;
16use fedimint_core::endpoint_constants::SESSION_COUNT_ENDPOINT;
17use fedimint_core::invite_code::InviteCode;
18use fedimint_core::module::{ApiAuth, ApiRequestErased};
19use fedimint_core::net::IP2PConnections;
20use fedimint_core::rustls::install_crypto_provider;
21use fedimint_core::task::{TaskGroup, block_in_place, sleep_in_test};
22use fedimint_gateway_common::ConnectFedPayload;
23use fedimint_gateway_server::{Gateway, IAdminGateway};
24use fedimint_logging::LOG_TEST;
25use fedimint_rocksdb::RocksDb;
26use fedimint_server::config::ServerConfig;
27use fedimint_server::core::ServerModuleInitRegistry;
28use fedimint_server::net::api::ApiSecrets;
29use fedimint_server::net::p2p::{ReconnectP2PConnections, p2p_status_channels};
30use fedimint_server::net::p2p_connector::{IP2PConnector, TlsTcpConnector};
31use fedimint_server::{ConnectionLimits, consensus};
32use fedimint_server_core::bitcoin_rpc::DynServerBitcoinRpc;
33use fedimint_testing_core::config::local_config_gen_params;
34use tracing::info;
35
36#[derive(Clone)]
38pub struct FederationTest {
39 configs: BTreeMap<PeerId, ServerConfig>,
40 server_init: ServerModuleInitRegistry,
41 client_init: ClientModuleInitRegistry,
42 _task: TaskGroup,
43 num_peers: u16,
44 num_offline: u16,
45 connectors: ConnectorRegistry,
46}
47
48impl FederationTest {
49 pub async fn two_clients(&self) -> (ClientHandleArc, ClientHandleArc) {
51 (self.new_client().await, self.new_client().await)
52 }
53
54 pub async fn new_client(&self) -> ClientHandleArc {
56 let client_config = self.configs[&PeerId::from(0)]
57 .consensus
58 .to_client_config(&self.server_init)
59 .unwrap();
60
61 self.new_client_with(client_config, MemDatabase::new().into(), None)
62 .await
63 }
64
65 pub async fn new_client_rocksdb(&self) -> ClientHandleArc {
67 let client_config = self.configs[&PeerId::from(0)]
68 .consensus
69 .to_client_config(&self.server_init)
70 .unwrap();
71
72 let db_dir = tempfile::tempdir()
74 .expect("Couldn't create temp dir")
75 .keep();
76
77 self.new_client_with(
78 client_config,
79 RocksDb::build(db_dir)
80 .open()
81 .await
82 .map_err(anyhow::Error::from)
83 .expect("Couldn't open DB")
84 .into(),
85 None,
86 )
87 .await
88 }
89
90 pub async fn new_admin_api(&self, peer_id: PeerId) -> anyhow::Result<DynGlobalApi> {
92 let config = self.configs.get(&peer_id).expect("peer to have config");
93
94 Ok(DynGlobalApi::new_admin(
95 ConnectorRegistry::build_from_testing_env().bind().await,
96 peer_id,
97 config.consensus.api_endpoints()[&peer_id].url.clone(),
98 None,
99 ))
100 }
101
102 pub async fn new_admin_client(&self, peer_id: PeerId, auth: ApiAuth) -> ClientHandleArc {
104 let client_config = self.configs[&PeerId::from(0)]
105 .consensus
106 .to_client_config(&self.server_init)
107 .unwrap();
108
109 let admin_creds = AdminCreds { peer_id, auth };
110
111 self.new_client_with(client_config, MemDatabase::new().into(), Some(admin_creds))
112 .await
113 }
114
115 pub async fn new_client_with(
116 &self,
117 client_config: ClientConfig,
118 db: Database,
119 admin_creds: Option<AdminCreds>,
120 ) -> ClientHandleArc {
121 info!(target: LOG_TEST, "Setting new client with config");
122 let mut client_builder = Client::builder().await;
123 client_builder.with_module_inits(self.client_init.clone());
124 if let Some(admin_creds) = admin_creds {
125 client_builder.set_admin_creds(admin_creds);
126 }
127 let client_secret = Client::load_or_generate_client_secret(&db).await;
128 client_builder
129 .preview_with_existing_config(self.connectors.clone(), client_config, None)
130 .await
131 .join(
132 db,
133 RootSecret::StandardDoubleDerive(PlainRootSecretStrategy::to_root_secret(
134 &client_secret,
135 )),
136 )
137 .await
138 .map(Arc::new)
139 .expect("Failed to build client")
140 }
141
142 pub async fn join_client_with_db(
144 &self,
145 db: Database,
146 root_secret: RootSecret,
147 ) -> ClientHandleArc {
148 let client_config = self.configs[&PeerId::from(0)]
149 .consensus
150 .to_client_config(&self.server_init)
151 .unwrap();
152
153 info!(target: LOG_TEST, "Joining client with existing db");
154 let mut client_builder = Client::builder().await;
155 client_builder.with_module_inits(self.client_init.clone());
156 client_builder
157 .preview_with_existing_config(self.connectors.clone(), client_config, None)
158 .await
159 .join(db, root_secret)
160 .await
161 .map(Arc::new)
162 .expect("Failed to join client")
163 }
164
165 pub async fn recover_client_with_db(
169 &self,
170 db: Database,
171 root_secret: RootSecret,
172 ) -> ClientHandleArc {
173 let client_config = self.configs[&PeerId::from(0)]
174 .consensus
175 .to_client_config(&self.server_init)
176 .unwrap();
177
178 info!(target: LOG_TEST, "Recovering client with existing db");
179 let mut client_builder = Client::builder().await;
180 client_builder.with_module_inits(self.client_init.clone());
181 client_builder
182 .preview_with_existing_config(self.connectors.clone(), client_config, None)
183 .await
184 .recover(db, root_secret, None)
185 .await
186 .map(Arc::new)
187 .expect("Failed to recover client")
188 }
189
190 pub async fn open_client_with_db(
192 &self,
193 db: Database,
194 root_secret: RootSecret,
195 ) -> ClientHandleArc {
196 info!(target: LOG_TEST, "Opening client with existing db");
197 let mut client_builder = Client::builder().await;
198 client_builder.with_module_inits(self.client_init.clone());
199 client_builder
200 .open(self.connectors.clone(), db, root_secret)
201 .await
202 .map(Arc::new)
203 .expect("Failed to open client")
204 }
205
206 pub fn invite_code(&self) -> InviteCode {
208 let peer_id = PeerId::from(0);
209 let cfg = &self.configs[&peer_id];
210 InviteCode::new(
211 cfg.consensus.api_endpoints()[&peer_id].url.clone(),
212 peer_id,
213 cfg.calculate_federation_id(),
214 None,
215 )
216 }
217
218 pub fn id(&self) -> FederationId {
220 self.configs[&PeerId::from(0)]
221 .consensus
222 .to_client_config(&self.server_init)
223 .unwrap()
224 .global
225 .calculate_federation_id()
226 }
227
228 pub async fn connect_gateway(&self, gw: &Gateway) {
230 gw.handle_connect_federation(ConnectFedPayload {
231 invite_code: self.invite_code().to_string(),
232 use_tor: Some(false),
233 recover: Some(false),
234 })
235 .await
236 .expect("Failed to connect federation");
237 }
238
239 pub fn online_peer_ids(&self) -> impl Iterator<Item = PeerId> + use<> {
241 (0..(self.num_peers - self.num_offline)).map(PeerId::from)
243 }
244
245 pub fn is_degraded(&self) -> bool {
247 self.num_offline > 0
248 }
249}
250
251#[derive(Clone, Debug)]
253pub struct FederationTestBuilder {
254 num_peers: u16,
255 num_offline: u16,
256 base_port: u16,
257 primary_module_kind: ModuleKind,
258 version_hash: String,
259 server_init: ServerModuleInitRegistry,
260 client_init: ClientModuleInitRegistry,
261 bitcoin_rpc_connection: DynServerBitcoinRpc,
262 enable_mint_fees: bool,
263}
264
265impl FederationTestBuilder {
266 pub fn new(
267 server_init: ServerModuleInitRegistry,
268 client_init: ClientModuleInitRegistry,
269 primary_module_kind: ModuleKind,
270 num_offline: u16,
271 bitcoin_rpc_connection: DynServerBitcoinRpc,
272 ) -> FederationTestBuilder {
273 let num_peers = 4;
274 Self {
275 num_peers,
276 num_offline,
277 base_port: block_in_place(|| fedimint_portalloc::port_alloc(num_peers * 3))
278 .expect("Failed to allocate a port range"),
279 primary_module_kind,
280 version_hash: "fedimint-testing-dummy-version-hash".to_owned(),
281 server_init,
282 client_init,
283 bitcoin_rpc_connection,
284 enable_mint_fees: true,
285 }
286 }
287
288 pub fn num_peers(mut self, num_peers: u16) -> FederationTestBuilder {
289 self.num_peers = num_peers;
290 self
291 }
292
293 pub fn num_offline(mut self, num_offline: u16) -> FederationTestBuilder {
294 self.num_offline = num_offline;
295 self
296 }
297
298 pub fn base_port(mut self, base_port: u16) -> FederationTestBuilder {
299 self.base_port = base_port;
300 self
301 }
302
303 pub fn primary_module_kind(mut self, primary_module_kind: ModuleKind) -> FederationTestBuilder {
304 self.primary_module_kind = primary_module_kind;
305 self
306 }
307
308 pub fn version_hash(mut self, version_hash: String) -> FederationTestBuilder {
309 self.version_hash = version_hash;
310 self
311 }
312
313 pub fn disable_mint_fees(mut self) -> FederationTestBuilder {
314 self.enable_mint_fees = false;
315 self
316 }
317
318 #[allow(clippy::too_many_lines)]
319 pub async fn build(self) -> FederationTest {
320 install_crypto_provider().await;
321 let num_offline = self.num_offline;
322 assert!(
323 self.num_peers > 3 * self.num_offline,
324 "too many peers offline ({num_offline}) to reach consensus"
325 );
326 let peers = (0..self.num_peers).map(PeerId::from).collect::<Vec<_>>();
327 let params = local_config_gen_params(
328 &peers,
329 self.base_port,
330 self.enable_mint_fees,
331 &self.server_init,
332 )
333 .expect("Generates local config");
334
335 let configs =
336 ServerConfig::trusted_dealer_gen(¶ms, &self.server_init, &self.version_hash);
337
338 let task_group = TaskGroup::new();
339 for (peer_id, cfg) in configs.clone() {
340 let peer_port = self.base_port + u16::from(peer_id) * 3;
341
342 let p2p_bind = format!("127.0.0.1:{peer_port}").parse().unwrap();
343 let api_bind = format!("127.0.0.1:{}", peer_port + 1).parse().unwrap();
344 let ui_bind = format!("127.0.0.1:{}", peer_port + 2).parse().unwrap();
345
346 if u16::from(peer_id) >= self.num_peers - self.num_offline {
347 continue;
348 }
349
350 let instances = cfg.consensus.iter_module_instances();
351 let decoders = self.server_init.available_decoders(instances);
352 let db = Database::new(MemDatabase::new(), decoders);
353 let module_init_registry = self.server_init.clone();
354 let subgroup = task_group.make_subgroup();
355 let checkpoint_dir = tempfile::Builder::new().tempdir().unwrap().keep();
356 let code_version_str = env!("CARGO_PKG_VERSION");
357
358 let connector = TlsTcpConnector::new(
359 cfg.tls_config(),
360 p2p_bind,
361 cfg.local.p2p_endpoints.clone(),
362 cfg.local.identity,
363 )
364 .await
365 .into_dyn();
366
367 let (p2p_status_senders, p2p_status_receivers) = p2p_status_channels(connector.peers());
368
369 let connections = ReconnectP2PConnections::new(
370 cfg.local.identity,
371 connector,
372 &task_group,
373 p2p_status_senders,
374 None,
375 )
376 .into_dyn();
377
378 let bitcoin_rpc_connection = self.bitcoin_rpc_connection.clone();
379
380 task_group.spawn("fedimintd", move |_| async move {
381 Box::pin(consensus::run(
382 ConnectorRegistry::build_from_testing_env().bind().await,
383 Some(ApiAuth::new("pass".to_string())),
384 Some(ApiAuth::new("pass".to_string())),
385 connections,
386 p2p_status_receivers,
387 api_bind,
388 None,
389 vec![],
390 cfg.clone(),
391 db.clone(),
392 module_init_registry,
393 &subgroup,
394 ApiSecrets::default(),
395 checkpoint_dir,
396 code_version_str.to_string(),
397 String::new(),
398 bitcoin_rpc_connection,
399 ui_bind,
400 Box::new(|_| axum::Router::new()),
401 1,
402 Duration::from_secs(3600),
403 ConnectionLimits {
404 max_connections: 1000,
405 max_requests_per_connection: 100,
406 },
407 None,
408 ))
409 .await
410 .expect("Could not initialise consensus");
411 });
412 }
413
414 for (peer_id, config) in configs.clone() {
415 if u16::from(peer_id) >= self.num_peers - self.num_offline {
416 continue;
417 }
418
419 let connectors = ConnectorRegistry::build_from_testing_env().bind().await;
420 let api = DynGlobalApi::new_admin(
421 connectors,
422 peer_id,
423 config.consensus.api_endpoints()[&peer_id].url.clone(),
424 None,
425 );
426
427 while let Err(e) = api
428 .request_admin_no_auth::<u64>(SESSION_COUNT_ENDPOINT, ApiRequestErased::default())
429 .await
430 {
431 sleep_in_test(
432 format!("Waiting for api of peer {peer_id} to come online: {e}"),
433 Duration::from_millis(500),
434 )
435 .await;
436 }
437 }
438
439 FederationTest {
440 configs,
441 server_init: self.server_init,
442 client_init: self.client_init,
443 _task: task_group,
444 num_peers: self.num_peers,
445 num_offline: self.num_offline,
446 connectors: ConnectorRegistry::build_from_testing_env().bind().await,
447 }
448 }
449}