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