fedimint_testing/
fixtures.rs

1use std::env;
2use std::net::SocketAddr;
3use std::str::FromStr;
4use std::sync::Arc;
5use std::time::Duration;
6
7use fedimint_bip39::Bip39RootSecretStrategy;
8use fedimint_bitcoind::{BitcoindTracked, DynBitcoindRpc, IBitcoindRpc, create_esplora_rpc};
9use fedimint_client::Client;
10use fedimint_client::module_init::{
11    ClientModuleInitRegistry, DynClientModuleInit, IClientModuleInit,
12};
13use fedimint_client::secret::RootSecretStrategy;
14use fedimint_core::core::{ModuleInstanceId, ModuleKind};
15use fedimint_core::db::Database;
16use fedimint_core::db::mem_impl::MemDatabase;
17use fedimint_core::envs::BitcoinRpcConfig;
18use fedimint_core::task::{MaybeSend, MaybeSync};
19use fedimint_core::util::SafeUrl;
20use fedimint_gateway_common::{ChainSource, LightningInfo, LightningMode};
21use fedimint_gateway_server::Gateway;
22use fedimint_gateway_server::client::GatewayClientBuilder;
23use fedimint_gateway_server::config::DatabaseBackend;
24use fedimint_lightning::{ILnRpcClient, LightningContext};
25use fedimint_logging::TracingSetup;
26use fedimint_server::core::{DynServerModuleInit, IServerModuleInit, ServerModuleInitRegistry};
27use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
28use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
29use fedimint_server_core::bitcoin_rpc::{DynServerBitcoinRpc, IServerBitcoinRpc};
30use fedimint_testing_core::test_dir;
31use rand::rngs::OsRng;
32
33use crate::btc::BitcoinTest;
34use crate::btc::mock::FakeBitcoinTest;
35use crate::btc::real::RealBitcoinTest;
36use crate::envs::{
37    FM_PORT_ESPLORA_ENV, FM_TEST_BACKEND_BITCOIN_RPC_KIND_ENV, FM_TEST_BACKEND_BITCOIN_RPC_URL_ENV,
38    FM_TEST_BITCOIND_RPC_ENV, FM_TEST_USE_REAL_DAEMONS_ENV,
39};
40use crate::federation::{FederationTest, FederationTestBuilder};
41use crate::ln::FakeLightningTest;
42
43/// A default timeout for things happening in tests
44pub const TIMEOUT: Duration = Duration::from_secs(10);
45
46pub const DEFAULT_GATEWAY_PASSWORD: &str = "thereisnosecondbest";
47
48/// A tool for easily writing fedimint integration tests
49pub struct Fixtures {
50    clients: ClientModuleInitRegistry,
51    servers: ServerModuleInitRegistry,
52    bitcoin_rpc: BitcoinRpcConfig,
53    bitcoin: Arc<dyn BitcoinTest>,
54    fake_bitcoin_rpc: Option<DynBitcoindRpc>,
55    server_bitcoin_rpc: DynServerBitcoinRpc,
56    primary_module_kind: ModuleKind,
57}
58
59impl Fixtures {
60    pub fn new_primary(
61        client: impl IClientModuleInit + 'static,
62        server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
63    ) -> Self {
64        // Ensure tracing has been set once
65        let _ = TracingSetup::default().init();
66        let real_testing = Fixtures::is_real_test();
67        let (bitcoin, config, bitcoin_rpc_connection, fake_bitcoin_rpc): (
68            Arc<dyn BitcoinTest>,
69            BitcoinRpcConfig,
70            DynServerBitcoinRpc,
71            Option<DynBitcoindRpc>,
72        ) = if real_testing {
73            // `backend-test.sh` overrides which Bitcoin RPC to use for esplora
74            // backend tests
75            let override_bitcoin_rpc_kind = env::var(FM_TEST_BACKEND_BITCOIN_RPC_KIND_ENV);
76            let override_bitcoin_rpc_url = env::var(FM_TEST_BACKEND_BITCOIN_RPC_URL_ENV);
77
78            let rpc_config = match (override_bitcoin_rpc_kind, override_bitcoin_rpc_url) {
79                (Ok(kind), Ok(url)) => BitcoinRpcConfig {
80                    kind: kind.parse().expect("must provide valid kind"),
81                    url: url.parse().expect("must provide valid url"),
82                },
83                _ => BitcoinRpcConfig::get_defaults_from_env_vars()
84                    .expect("must provide valid default env vars"),
85            };
86
87            let server_bitcoin_rpc = match rpc_config.kind.as_ref() {
88                "bitcoind" => {
89                    // Directly extract the authentication details from the url.
90                    // Since this is just testing we can be careful to not use characters that need
91                    // to be URL-encoded
92                    let bitcoind_username = rpc_config.url.username();
93                    let bitcoind_password = rpc_config
94                        .url
95                        .password()
96                        .expect("bitcoind password was not set");
97                    BitcoindClient::new(
98                        bitcoind_username.to_string(),
99                        bitcoind_password.to_string(),
100                        &rpc_config.url,
101                    )
102                    .unwrap()
103                    .into_dyn()
104                }
105                "esplora" => EsploraClient::new(&rpc_config.url).unwrap().into_dyn(),
106                kind => panic!("Unknown bitcoin rpc kind {kind}"),
107            };
108
109            let bitcoincore_url = env::var(FM_TEST_BITCOIND_RPC_ENV)
110                .expect("Must have bitcoind RPC defined for real tests")
111                .parse()
112                .expect("Invalid bitcoind RPC URL");
113            let bitcoin = RealBitcoinTest::new(&bitcoincore_url, server_bitcoin_rpc.clone());
114
115            (Arc::new(bitcoin), rpc_config, server_bitcoin_rpc, None)
116        } else {
117            let bitcoin = FakeBitcoinTest::new();
118
119            let config = BitcoinRpcConfig {
120                kind: format!("test_btc-{}", rand::random::<u64>()),
121                url: "http://ignored".parse().unwrap(),
122            };
123
124            let dyn_bitcoin_rpc = IBitcoindRpc::into_dyn(bitcoin.clone());
125
126            let server_bitcoin_rpc = IServerBitcoinRpc::into_dyn(bitcoin.clone());
127
128            let bitcoin = Arc::new(bitcoin);
129
130            (
131                bitcoin.clone(),
132                config,
133                server_bitcoin_rpc,
134                Some(dyn_bitcoin_rpc),
135            )
136        };
137
138        Self {
139            clients: ClientModuleInitRegistry::default(),
140            servers: ServerModuleInitRegistry::default(),
141            bitcoin_rpc: config,
142            fake_bitcoin_rpc,
143            bitcoin,
144            server_bitcoin_rpc: bitcoin_rpc_connection,
145            primary_module_kind: IClientModuleInit::module_kind(&client),
146        }
147        .with_module(client, server)
148    }
149
150    pub fn is_real_test() -> bool {
151        env::var(FM_TEST_USE_REAL_DAEMONS_ENV) == Ok("1".to_string())
152    }
153
154    /// Add a module to the fed
155    pub fn with_module(
156        mut self,
157        client: impl IClientModuleInit + 'static,
158        server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
159    ) -> Self {
160        self.clients.attach(DynClientModuleInit::from(client));
161        self.servers.attach(DynServerModuleInit::from(server));
162        self
163    }
164
165    pub fn with_server_only_module(
166        mut self,
167        server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
168    ) -> Self {
169        self.servers.attach(DynServerModuleInit::from(server));
170        self
171    }
172
173    /// Starts a new federation with 3/4 peers online
174    pub async fn new_fed_degraded(&self) -> FederationTest {
175        self.new_fed_builder(1).build().await
176    }
177
178    /// Starts a new federation with 4/4 peers online
179    pub async fn new_fed_not_degraded(&self) -> FederationTest {
180        self.new_fed_builder(0).build().await
181    }
182
183    /// Creates a new `FederationTestBuilder` that can be used to build up a
184    /// `FederationTest` for module tests.
185    pub fn new_fed_builder(&self, num_offline: u16) -> FederationTestBuilder {
186        FederationTestBuilder::new(
187            self.servers.clone(),
188            self.clients.clone(),
189            self.primary_module_kind.clone(),
190            num_offline,
191            self.server_bitcoin_rpc(),
192        )
193    }
194
195    /// Creates a new Gateway that can be used for module tests.
196    pub async fn new_gateway(&self) -> Gateway {
197        // Use server_gens.iter() to match the alphabetical order used by the server
198        // when assigning module instance IDs (BTreeMap iteration order)
199        let module_kinds: Vec<_> = self
200            .servers
201            .iter()
202            .enumerate()
203            .map(|(id, (kind, _))| (id as ModuleInstanceId, kind.clone()))
204            .collect();
205        let decoders = self
206            .servers
207            .available_decoders(module_kinds.iter().map(|(id, kind)| (*id, kind)))
208            .unwrap();
209        let gateway_db = Database::new(MemDatabase::new(), decoders.clone());
210
211        let mnemonic = Bip39RootSecretStrategy::<12>::random(&mut OsRng);
212        Client::store_encodable_client_secret(&gateway_db, mnemonic.to_entropy())
213            .await
214            .expect("Could not generate root secret for gateway");
215
216        let registry = self
217            .clients
218            .iter()
219            .filter(|(kind, _)| {
220                // Remove LN module because the gateway adds one
221                **kind != ModuleKind::from_static_str("ln")
222            })
223            .filter(|(kind, _)| {
224                // Remove LN NG module because the gateway adds one
225                **kind != ModuleKind::from_static_str("lnv2")
226            })
227            .map(|(_, client)| client.clone())
228            .collect();
229
230        let (path, _config_dir) = test_dir(&format!("gateway-{}", rand::random::<u64>()));
231
232        // Create federation client builder for the gateway
233        let client_builder: GatewayClientBuilder =
234            GatewayClientBuilder::new(path.clone(), registry, DatabaseBackend::RocksDb)
235                .await
236                .expect("Failed to initialize gateway");
237
238        let ln_client: Arc<dyn ILnRpcClient> = Arc::new(FakeLightningTest::new());
239
240        let LightningInfo::Connected {
241            public_key: lightning_public_key,
242            alias: lightning_alias,
243            network: lightning_network,
244            block_height: _,
245            synced_to_chain: _,
246        } = ln_client.parsed_node_info().await
247        else {
248            panic!("Could not connect to Lightning node")
249        };
250        let lightning_context = LightningContext {
251            lnrpc: ln_client.clone(),
252            lightning_public_key,
253            lightning_alias,
254            lightning_network,
255        };
256
257        // Module tests do not use the webserver, so any port is ok
258        let listen: SocketAddr = "127.0.0.1:9000".parse().unwrap();
259        let address: SafeUrl = format!("http://{listen}").parse().unwrap();
260
261        let esplora_server_url = SafeUrl::parse(&format!(
262            "http://127.0.0.1:{}",
263            env::var(FM_PORT_ESPLORA_ENV).unwrap_or(String::from("50002"))
264        ))
265        .expect("Failed to parse default esplora server");
266        let esplora_chain_source = ChainSource::Esplora {
267            server_url: esplora_server_url,
268        };
269
270        Gateway::new_with_custom_registry(
271            // Fixtures does not use real lightning connection, so just fake the connection
272            // parameters
273            LightningMode::Lnd {
274                lnd_rpc_addr: "FakeRpcAddr".to_string(),
275                lnd_tls_cert: "FakeTlsCert".to_string(),
276                lnd_macaroon: "FakeMacaroon".to_string(),
277            },
278            client_builder,
279            listen,
280            address.clone(),
281            bcrypt::HashParts::from_str(
282                &bcrypt::hash(DEFAULT_GATEWAY_PASSWORD, bcrypt::DEFAULT_COST).unwrap(),
283            )
284            .unwrap(),
285            bitcoin::Network::Regtest,
286            0,
287            gateway_db,
288            // Manually set the gateway's state to `Running`. In tests, we do don't run the
289            // webserver or intercept HTLCs, so this is necessary for instructing the
290            // gateway that it is connected to the mock Lightning node.
291            fedimint_gateway_server::GatewayState::Running { lightning_context },
292            esplora_chain_source,
293            None,
294        )
295        .await
296        .expect("Failed to create gateway")
297    }
298
299    /// Get a server bitcoin RPC config
300    pub fn bitcoin_server(&self) -> BitcoinRpcConfig {
301        self.bitcoin_rpc.clone()
302    }
303
304    pub fn client_esplora_rpc(&self) -> DynBitcoindRpc {
305        let rpc = if Fixtures::is_real_test() {
306            create_esplora_rpc(
307                &SafeUrl::parse(&format!(
308                    "http://127.0.0.1:{}/",
309                    env::var(FM_PORT_ESPLORA_ENV).unwrap_or(String::from("50002"))
310                ))
311                .expect("Failed to parse default esplora server"),
312            )
313            .unwrap()
314        } else {
315            self.fake_bitcoin_rpc.clone().unwrap()
316        };
317        BitcoindTracked::new(rpc, "test-fixture").into_dyn()
318    }
319
320    /// Get a test bitcoin fixture
321    pub fn bitcoin(&self) -> Arc<dyn BitcoinTest> {
322        self.bitcoin.clone()
323    }
324
325    pub fn server_bitcoin_rpc(&self) -> DynServerBitcoinRpc {
326        self.server_bitcoin_rpc.clone()
327    }
328}