Skip to main content

fedimint_load_test_tool/
main.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_precision_loss)]
4#![allow(clippy::missing_errors_doc)]
5#![allow(clippy::ref_option)]
6#![allow(clippy::too_many_lines)]
7#![allow(clippy::large_futures)]
8
9use std::collections::{BTreeMap, HashMap};
10use std::path::PathBuf;
11use std::str::FromStr;
12use std::time::Duration;
13use std::vec;
14
15use anyhow::{Context, anyhow, bail};
16use clap::{Args, Parser, Subcommand, ValueEnum};
17use common::{
18    gateway_pay_invoice, get_note_summary, ldk_create_invoice, ldk_pay_invoice,
19    ldk_wait_invoice_payment, parse_gateway_id, reissue_notes,
20};
21use devimint::cmd;
22use devimint::util::GatewayLndCli;
23use devimint::version_constants::VERSION_0_10_0_ALPHA;
24use fedimint_api_client::download_from_invite_code;
25use fedimint_client::ClientHandleArc;
26use fedimint_connectors::ConnectorRegistry;
27use fedimint_core::Amount;
28use fedimint_core::endpoint_constants::SESSION_COUNT_ENDPOINT;
29use fedimint_core::invite_code::InviteCode;
30use fedimint_core::module::ApiRequestErased;
31use fedimint_core::runtime::spawn;
32use fedimint_core::util::{BoxFuture, SafeUrl};
33use fedimint_ln_client::{LightningClientModule, LnReceiveState};
34use fedimint_ln_common::LightningGateway;
35use fedimint_mint_client::OOBNotes;
36use futures::StreamExt;
37use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
38use serde::{Deserialize, Serialize};
39use tokio::fs::OpenOptions;
40use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufWriter};
41use tokio::sync::mpsc;
42use tracing::{debug, info, warn};
43
44use crate::common::{
45    build_client, do_spend_notes, get_invite_code_cli, remint_denomination, try_get_notes_cli,
46};
47pub mod common;
48#[cfg(test)]
49mod tests;
50
51#[derive(Parser, Clone)]
52#[command(version)]
53struct Opts {
54    #[arg(
55        long,
56        default_value = "10",
57        help = "Number of users. Each user will work in parallel"
58    )]
59    users: u16,
60
61    #[arg(long, help = "Output with the metrics results in JSON format")]
62    metrics_json_output: Option<PathBuf>,
63
64    #[arg(
65        long,
66        help = "If given, will be used to store and retrieve past metrics for comparison purposes"
67    )]
68    archive_dir: Option<PathBuf>,
69
70    #[clap(subcommand)]
71    command: Command,
72}
73
74#[derive(Debug, Clone, Copy, ValueEnum)]
75enum LnInvoiceGeneration {
76    LdkLightningCli,
77}
78
79#[derive(Subcommand, Clone)]
80enum Command {
81    #[command(about = "Keep many websocket connections to a federation for a duration of time")]
82    TestConnect {
83        #[arg(long, help = "Federation invite code")]
84        invite_code: String,
85        #[arg(
86            long,
87            default_value = "60",
88            help = "How much time to keep the connections open, in seconds"
89        )]
90        duration_secs: u64,
91        #[arg(
92            long,
93            default_value = "120",
94            help = "Timeout for connection attempt and for each request, in secnods"
95        )]
96        timeout_secs: u64,
97        #[arg(
98            long,
99            help = "If given, will limit the number of endpoints (guardians) to connect to"
100        )]
101        limit_endpoints: Option<usize>,
102    },
103    #[command(about = "Try to download the client config many times.")]
104    TestDownload {
105        #[arg(long, help = "Federation invite code")]
106        invite_code: String,
107    },
108    #[command(
109        about = "Run a load test where many users in parallel will try to reissue notes and pay invoices through the gateway"
110    )]
111    LoadTest(LoadTestArgs),
112    /// Run a load test where many users in parallel will receive then send a
113    /// payment through lightning.
114    /// It's 'circular' because the funds always come back to the same user then
115    /// we can keep making the payments in a loop
116    #[command()]
117    LnCircularLoadTest(LnCircularLoadTestArgs),
118}
119
120#[derive(Args, Clone)]
121struct LoadTestArgs {
122    #[arg(
123        long,
124        help = "Federation invite code. If none given, we assume the client already has a config downloaded in DB"
125    )]
126    invite_code: Option<InviteCode>,
127
128    #[arg(
129        long,
130        help = "Notes for the test. If none and no funds on archive, will call fedimint-cli spend"
131    )]
132    initial_notes: Option<OOBNotes>,
133
134    #[arg(
135        long,
136        help = "Gateway Id. If none, retrieve one according to --generate-invoice-with"
137    )]
138    gateway_id: Option<String>,
139
140    #[arg(
141        long,
142        help = "The method used to generate invoices to be paid through the gateway. If none and no --invoices-file provided, no gateway/LN tests will be run. Note that you can't generate an invoice using the same lightning node used by the gateway (i.e self payment is forbidden)"
143    )]
144    generate_invoice_with: Option<LnInvoiceGeneration>,
145
146    #[arg(
147        long,
148        default_value = "1",
149        help = "How many invoices will be created for each user. Only applicable if --generate-invoice-with is provided"
150    )]
151    invoices_per_user: u16,
152
153    #[arg(
154        long,
155        default_value = "0",
156        help = "How many seconds to sleep between LN payments"
157    )]
158    ln_payment_sleep_secs: u64,
159
160    #[arg(
161        long,
162        help = "A text file with one invoice per line. If --generate-invoice-with is provided, these will be additional invoices to be paid"
163    )]
164    invoices_file: Option<PathBuf>,
165
166    #[arg(
167        long,
168        help = "How many notes to distribute to each user",
169        default_value = "2"
170    )]
171    notes_per_user: u16,
172
173    #[arg(
174        long,
175        help = "Note denomination to use for the test",
176        default_value = "1024"
177    )]
178    note_denomination: Amount,
179
180    #[arg(
181        long,
182        help = "Invoice amount when generating one",
183        default_value = "1000"
184    )]
185    invoice_amount: Amount,
186}
187
188#[derive(Args, Clone)]
189struct LnCircularLoadTestArgs {
190    #[arg(
191        long,
192        help = "Federation invite code. If none given, we assume the client already has a config downloaded in DB"
193    )]
194    invite_code: Option<InviteCode>,
195
196    #[arg(
197        long,
198        help = "Notes for the test. If none and no funds on archive, will call fedimint-cli spend"
199    )]
200    initial_notes: Option<OOBNotes>,
201
202    #[arg(
203        long,
204        default_value = "60",
205        help = "For how many seconds to run the test"
206    )]
207    test_duration_secs: u64,
208
209    #[arg(
210        long,
211        default_value = "0",
212        help = "How many seconds to sleep between LN payments"
213    )]
214    ln_payment_sleep_secs: u64,
215
216    #[arg(
217        long,
218        help = "How many notes to distribute to each user",
219        default_value = "1"
220    )]
221    notes_per_user: u16,
222
223    #[arg(
224        long,
225        help = "Note denomination to use for the test",
226        default_value = "1024"
227    )]
228    note_denomination: Amount,
229
230    #[arg(
231        long,
232        help = "Invoice amount when generating one",
233        default_value = "1000"
234    )]
235    invoice_amount: Amount,
236
237    #[arg(long)]
238    strategy: LnCircularStrategy,
239}
240
241#[derive(Debug, Clone, Copy, ValueEnum)]
242enum LnCircularStrategy {
243    /// The user will pay its own invoice
244    SelfPayment,
245    /// One gateway will pay/receive to/from the other, then they will swap
246    /// places
247    TwoGateways,
248    /// Two clients will pay to each other using the same gateway
249    PartnerPingPong,
250}
251
252#[derive(Debug, Clone)]
253pub struct MetricEvent {
254    name: String,
255    duration: Duration,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
259struct EventMetricSummary {
260    name: String,
261    users: u64,
262    n: u64,
263    avg_ms: u128,
264    median_ms: u128,
265    max_ms: u128,
266    min_ms: u128,
267    timestamp_seconds: u64,
268}
269
270#[derive(Debug, Serialize, Deserialize)]
271struct EventMetricComparison {
272    avg_ms_gain: f64,
273    median_ms_gain: f64,
274    max_ms_gain: f64,
275    min_ms_gain: f64,
276    current: EventMetricSummary,
277    previous: EventMetricSummary,
278}
279
280impl std::fmt::Display for EventMetricComparison {
281    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282        fn to_percent(gain: f64) -> String {
283            if gain >= 1.0 {
284                format!("+{:.2}%", (gain - 1.0) * 100.0)
285            } else {
286                format!("-{:.2}%", (1.0 - gain) * 100.0)
287            }
288        }
289        f.write_str(&format!(
290            "avg: {}, median: {}, max: {}, min: {}",
291            to_percent(self.avg_ms_gain),
292            to_percent(self.median_ms_gain),
293            to_percent(self.max_ms_gain),
294            to_percent(self.min_ms_gain),
295        ))
296    }
297}
298
299#[tokio::main]
300async fn main() -> anyhow::Result<()> {
301    fedimint_logging::TracingSetup::default().init()?;
302    let opts = Opts::parse();
303    validate_total_notes(&opts)?;
304    let (event_sender, event_receiver) = tokio::sync::mpsc::unbounded_channel();
305    let summary_handle = spawn("handle metrics summary", {
306        let opts = opts.clone();
307        async { handle_metrics_summary(opts, event_receiver).await }
308    });
309    let futures = match opts.command.clone() {
310        Command::TestConnect {
311            invite_code,
312            duration_secs,
313            timeout_secs,
314            limit_endpoints,
315        } => {
316            let connectors = ConnectorRegistry::build_from_client_env().bind().await;
317            let invite_code = InviteCode::from_str(&invite_code).context("invalid invite code")?;
318            test_connect_raw_client(
319                &connectors,
320                invite_code,
321                opts.users,
322                Duration::from_secs(duration_secs),
323                Duration::from_secs(timeout_secs),
324                limit_endpoints,
325                event_sender.clone(),
326            )
327            .await?
328        }
329        Command::TestDownload { invite_code } => {
330            let connectors = ConnectorRegistry::build_from_client_env().bind().await;
331            let invite_code = InviteCode::from_str(&invite_code).context("invalid invite code")?;
332            test_download_config(&connectors, &invite_code, opts.users, &event_sender.clone())
333        }
334        Command::LoadTest(args) => {
335            let invite_code = invite_code_or_fallback(args.invite_code).await;
336
337            let gateway_id = if let Some(gateway_id) = args.gateway_id {
338                Some(gateway_id)
339            } else if let Some(generate_invoice_with) = args.generate_invoice_with {
340                Some(get_gateway_id(generate_invoice_with).await?)
341            } else {
342                None
343            };
344            let invoices = if let Some(invoices_file) = args.invoices_file {
345                let display_path = invoices_file.display();
346                let invoices_file = tokio::fs::File::open(&invoices_file)
347                    .await
348                    .with_context(|| format!("Failed to open {display_path}"))?;
349                let mut lines = tokio::io::BufReader::new(invoices_file).lines();
350                let mut invoices = vec![];
351                while let Some(line) = lines.next_line().await? {
352                    let invoice = Bolt11Invoice::from_str(&line)?;
353                    invoices.push(invoice);
354                }
355                invoices
356            } else {
357                vec![]
358            };
359            if args.generate_invoice_with.is_none() && invoices.is_empty() {
360                info!(
361                    "No --generate-invoice-with given no invoices on --invoices-file, not LN/gateway tests will be run"
362                );
363            }
364            run_load_test(
365                opts.archive_dir,
366                opts.users,
367                invite_code,
368                args.initial_notes,
369                args.generate_invoice_with,
370                args.invoices_per_user,
371                Duration::from_secs(args.ln_payment_sleep_secs),
372                invoices,
373                gateway_id,
374                args.notes_per_user,
375                args.note_denomination,
376                args.invoice_amount,
377                event_sender.clone(),
378            )
379            .await?
380        }
381        Command::LnCircularLoadTest(args) => {
382            let invite_code = invite_code_or_fallback(args.invite_code).await;
383            run_ln_circular_load_test(
384                opts.archive_dir,
385                opts.users,
386                invite_code,
387                args.initial_notes,
388                Duration::from_secs(args.test_duration_secs),
389                Duration::from_secs(args.ln_payment_sleep_secs),
390                args.notes_per_user,
391                args.note_denomination,
392                args.invoice_amount,
393                args.strategy,
394                event_sender.clone(),
395            )
396            .await?
397        }
398    };
399
400    let result = futures::future::join_all(futures).await;
401    drop(event_sender);
402    summary_handle.await??;
403    let len_failures = result.iter().filter(|r| r.is_err()).count();
404    eprintln!("{} results, {len_failures} failures", result.len());
405    for r in result {
406        if let Err(e) = r {
407            warn!("Task failed: {:?}", e);
408        }
409    }
410    if len_failures > 0 {
411        bail!("Finished with failures");
412    }
413    info!("Finished successfully");
414    Ok(())
415}
416
417fn validate_total_notes(opts: &Opts) -> anyhow::Result<()> {
418    match &opts.command {
419        Command::LoadTest(args) => {
420            checked_total(opts.users, args.notes_per_user)?;
421        }
422        Command::LnCircularLoadTest(args) => {
423            checked_total(opts.users, args.notes_per_user)?;
424        }
425        Command::TestConnect { .. } | Command::TestDownload { .. } => {}
426    }
427    Ok(())
428}
429
430fn checked_total(users: u16, notes_per_user: u16) -> anyhow::Result<u16> {
431    users.checked_mul(notes_per_user).ok_or_else(|| {
432        anyhow!(
433            "--users {users} multiplied by --notes-per-user {notes_per_user} exceeds the maximum supported combined note count of {}",
434            u16::MAX
435        )
436    })
437}
438
439async fn invite_code_or_fallback(invite_code: Option<InviteCode>) -> Option<InviteCode> {
440    if let Some(invite_code) = invite_code {
441        Some(invite_code)
442    } else {
443        // Try to get an invite code through cli in a best effort basis
444        match get_invite_code_cli(0.into()).await {
445            Ok(invite_code) => Some(invite_code),
446            Err(e) => {
447                info!(
448                    "No invite code provided and failed to get one with '{e}' error, will try to proceed without one..."
449                );
450                None
451            }
452        }
453    }
454}
455
456#[allow(clippy::too_many_arguments)]
457async fn run_load_test(
458    archive_dir: Option<PathBuf>,
459    users: u16,
460    invite_code: Option<InviteCode>,
461    initial_notes: Option<OOBNotes>,
462    generate_invoice_with: Option<LnInvoiceGeneration>,
463    generated_invoices_per_user: u16,
464    ln_payment_sleep: Duration,
465    invoices_from_file: Vec<Bolt11Invoice>,
466    gateway_id: Option<String>,
467    notes_per_user: u16,
468    note_denomination: Amount,
469    invoice_amount: Amount,
470    event_sender: mpsc::UnboundedSender<MetricEvent>,
471) -> anyhow::Result<Vec<BoxFuture<'static, anyhow::Result<()>>>> {
472    let minimum_notes = checked_total(users, notes_per_user)?;
473    let db_path = get_db_path(&archive_dir);
474    let (coordinator, invite_code) = get_coordinator_client(&db_path, &invite_code).await?;
475    let minimum_amount_required = note_denomination * u64::from(minimum_notes);
476
477    reissue_initial_notes(initial_notes, &coordinator, &event_sender).await?;
478    get_required_notes(&coordinator, minimum_amount_required, &event_sender).await?;
479    print_coordinator_notes(&coordinator).await?;
480    info!(
481        "Reminting {minimum_notes} notes of denomination {note_denomination} for {users} users, {notes_per_user} notes per user (this may take a while if the number of users/notes is high)"
482    );
483    remint_denomination(&coordinator, note_denomination, minimum_notes).await?;
484    print_coordinator_notes(&coordinator).await?;
485
486    let users_clients = get_users_clients(users, db_path, invite_code).await?;
487
488    let mut users_notes =
489        get_notes_for_users(users, notes_per_user, coordinator, note_denomination).await?;
490    let mut users_invoices = HashMap::new();
491    let mut user = 0;
492    // Distribute invoices to users in a round robin fashion
493    for invoice in invoices_from_file {
494        users_invoices
495            .entry(user)
496            .or_insert_with(Vec::new)
497            .push(invoice);
498        user = (user + 1) % users;
499    }
500
501    info!("Starting user tasks");
502    let futures = users_clients
503        .into_iter()
504        .enumerate()
505        .map(|(u, client)| {
506            let u = u as u16;
507            let oob_notes = users_notes.remove(&u).unwrap();
508            let invoices = users_invoices.remove(&u).unwrap_or_default();
509            let event_sender = event_sender.clone();
510            let f: BoxFuture<_> = Box::pin(do_load_test_user_task(
511                format!("User {u}:"),
512                client,
513                oob_notes,
514                generated_invoices_per_user,
515                ln_payment_sleep,
516                invoice_amount,
517                invoices,
518                generate_invoice_with,
519                event_sender,
520                gateway_id.clone(),
521            ));
522            f
523        })
524        .collect::<Vec<_>>();
525
526    Ok(futures)
527}
528
529async fn get_notes_for_users(
530    users: u16,
531    notes_per_user: u16,
532    coordinator: ClientHandleArc,
533    note_denomination: Amount,
534) -> anyhow::Result<HashMap<u16, Vec<OOBNotes>>> {
535    let mut users_notes = HashMap::new();
536    for u in 0..users {
537        users_notes.insert(u, Vec::with_capacity(notes_per_user.into()));
538        for _ in 0..notes_per_user {
539            let (_, oob_notes) = do_spend_notes(&coordinator, note_denomination).await?;
540            let user_amount = oob_notes.total_amount();
541            info!("Giving {user_amount} to user {u}");
542            users_notes.get_mut(&u).unwrap().push(oob_notes);
543        }
544    }
545    Ok(users_notes)
546}
547
548async fn get_users_clients(
549    n: u16,
550    db_path: Option<PathBuf>,
551    invite_code: Option<InviteCode>,
552) -> anyhow::Result<Vec<ClientHandleArc>> {
553    let mut users_clients = Vec::with_capacity(n.into());
554    for u in 0..n {
555        let (client, _) = get_user_client(u, &db_path, &invite_code).await?;
556        users_clients.push(client);
557    }
558    Ok(users_clients)
559}
560
561async fn get_user_client(
562    user_index: u16,
563    db_path: &Option<PathBuf>,
564    invite_code: &Option<InviteCode>,
565) -> anyhow::Result<(ClientHandleArc, Option<InviteCode>)> {
566    let user_db = db_path
567        .as_ref()
568        .map(|db_path| db_path.join(format!("user_{user_index}.db")));
569    let user_invite_code = if user_db.as_ref().is_some_and(|db| db.exists()) {
570        None
571    } else {
572        invite_code.clone()
573    };
574    let (client, invite_code) = build_client(user_invite_code, user_db.as_ref()).await?;
575    // if lightning module is present, update the gateway cache
576    if let Ok(ln_client) = client.get_first_module::<LightningClientModule>() {
577        let _ = ln_client.update_gateway_cache().await;
578    }
579    Ok((client, invite_code))
580}
581
582async fn print_coordinator_notes(coordinator: &ClientHandleArc) -> anyhow::Result<()> {
583    info!("Note summary:");
584    let summary = get_note_summary(coordinator).await?;
585    for (k, v) in summary.iter() {
586        info!("{k}: {v}");
587    }
588    Ok(())
589}
590
591async fn get_required_notes(
592    coordinator: &ClientHandleArc,
593    minimum_amount_required: Amount,
594    event_sender: &mpsc::UnboundedSender<MetricEvent>,
595) -> anyhow::Result<()> {
596    let current_balance = coordinator.get_balance_for_btc().await?;
597
598    if current_balance < minimum_amount_required {
599        let diff = minimum_amount_required.saturating_sub(current_balance);
600        info!(
601            "Current balance {current_balance} on coordinator not enough, trying to get {diff} more through fedimint-cli"
602        );
603        match try_get_notes_cli(&diff, 5).await {
604            Ok(notes) => {
605                info!("Got {} more notes, reissuing them", notes.total_amount());
606                reissue_notes(coordinator, notes, event_sender).await?;
607            }
608            Err(e) => {
609                info!("Unable to get more notes: '{e}', will try to proceed without them");
610            }
611        }
612    } else {
613        info!(
614            "Current balance of {current_balance} already covers the minimum required of {minimum_amount_required}"
615        );
616    }
617    Ok(())
618}
619
620async fn reissue_initial_notes(
621    initial_notes: Option<OOBNotes>,
622    coordinator: &ClientHandleArc,
623    event_sender: &mpsc::UnboundedSender<MetricEvent>,
624) -> anyhow::Result<()> {
625    if let Some(notes) = initial_notes {
626        let amount = notes.total_amount();
627        info!("Reissuing initial notes, got {amount}");
628        reissue_notes(coordinator, notes, event_sender).await?;
629    }
630    Ok(())
631}
632
633async fn get_coordinator_client(
634    db_path: &Option<PathBuf>,
635    invite_code: &Option<InviteCode>,
636) -> anyhow::Result<(ClientHandleArc, Option<InviteCode>)> {
637    let (client, invite_code) = if let Some(db_path) = db_path {
638        let coordinator_db = db_path.join("coordinator.db");
639        if coordinator_db.exists() {
640            build_client(invite_code.clone(), Some(&coordinator_db)).await?
641        } else {
642            tokio::fs::create_dir_all(db_path).await?;
643            build_client(
644                Some(invite_code.clone().context(
645                    "Running on this archive dir for the first time, an invite code is required",
646                )?),
647                Some(&coordinator_db),
648            )
649            .await?
650        }
651    } else {
652        build_client(
653            Some(
654                invite_code
655                    .clone()
656                    .context("No archive dir given, an invite code is strictly required")?,
657            ),
658            None,
659        )
660        .await?
661    };
662    Ok((client, invite_code))
663}
664
665fn get_db_path(archive_dir: &Option<PathBuf>) -> Option<PathBuf> {
666    archive_dir.as_ref().map(|p| p.join("db"))
667}
668
669async fn get_lightning_gateway(
670    client: &ClientHandleArc,
671    gateway_id: Option<String>,
672) -> Option<LightningGateway> {
673    let gateway_id = parse_gateway_id(gateway_id.or(None)?.as_str()).expect("Invalid gateway id");
674    let ln_module = client
675        .get_first_module::<LightningClientModule>()
676        .expect("Must have ln client module");
677    ln_module.select_gateway(&gateway_id).await
678}
679
680#[allow(clippy::too_many_arguments)]
681async fn do_load_test_user_task(
682    prefix: String,
683    client: ClientHandleArc,
684    oob_notes: Vec<OOBNotes>,
685    generated_invoices_per_user: u16,
686    ln_payment_sleep: Duration,
687    invoice_amount: Amount,
688    additional_invoices: Vec<Bolt11Invoice>,
689    generate_invoice_with: Option<LnInvoiceGeneration>,
690    event_sender: mpsc::UnboundedSender<MetricEvent>,
691    gateway_id: Option<String>,
692) -> anyhow::Result<()> {
693    let ln_gateway = get_lightning_gateway(&client, gateway_id).await;
694    for oob_note in oob_notes {
695        let amount = oob_note.total_amount();
696        reissue_notes(&client, oob_note, &event_sender)
697            .await
698            .map_err(|e| anyhow!("while reissuing initial {amount}: {e}"))?;
699    }
700    let mut generated_invoices_per_user_iterator = (0..generated_invoices_per_user).peekable();
701    while let Some(_) = generated_invoices_per_user_iterator.next() {
702        let total_amount = get_note_summary(&client).await?.total_amount();
703        if invoice_amount > total_amount {
704            warn!("Can't pay invoice, not enough funds: {invoice_amount} > {total_amount}");
705        } else {
706            match generate_invoice_with {
707                Some(LnInvoiceGeneration::LdkLightningCli) => {
708                    let invoice = ldk_create_invoice(invoice_amount).await?;
709                    gateway_pay_invoice(
710                        &prefix,
711                        "LND",
712                        &client,
713                        invoice.clone(),
714                        &event_sender,
715                        ln_gateway.clone(),
716                    )
717                    .await?;
718                    ldk_wait_invoice_payment(&invoice).await?;
719                }
720                None if additional_invoices.is_empty() => {
721                    debug!(
722                        "No method given to generate an invoice and no invoices on file, will not test the gateway"
723                    );
724                    break;
725                }
726                None => {
727                    break;
728                }
729            }
730            if generated_invoices_per_user_iterator.peek().is_some() {
731                // Only sleep while there are more invoices to pay
732                fedimint_core::task::sleep(ln_payment_sleep).await;
733            }
734        }
735    }
736    let mut additional_invoices = additional_invoices.into_iter().peekable();
737    while let Some(invoice) = additional_invoices.next() {
738        let total_amount = get_note_summary(&client).await?.total_amount();
739        let invoice_amount =
740            Amount::from_msats(invoice.amount_milli_satoshis().unwrap_or_default());
741        if invoice_amount > total_amount {
742            warn!("Can't pay invoice, not enough funds: {invoice_amount} > {total_amount}");
743        } else if invoice_amount == Amount::ZERO {
744            warn!("Can't pay invoice {invoice}, amount is zero");
745        } else {
746            gateway_pay_invoice(
747                &prefix,
748                "unknown",
749                &client,
750                invoice,
751                &event_sender,
752                ln_gateway.clone(),
753            )
754            .await?;
755            if additional_invoices.peek().is_some() {
756                // Only sleep while there are more invoices to pay
757                fedimint_core::task::sleep(ln_payment_sleep).await;
758            }
759        }
760    }
761    Ok(())
762}
763
764#[allow(clippy::too_many_arguments)]
765async fn run_ln_circular_load_test(
766    archive_dir: Option<PathBuf>,
767    users: u16,
768    invite_code: Option<InviteCode>,
769    initial_notes: Option<OOBNotes>,
770    test_duration: Duration,
771    ln_payment_sleep: Duration,
772    notes_per_user: u16,
773    note_denomination: Amount,
774    invoice_amount: Amount,
775    strategy: LnCircularStrategy,
776    event_sender: mpsc::UnboundedSender<MetricEvent>,
777) -> anyhow::Result<Vec<BoxFuture<'static, anyhow::Result<()>>>> {
778    let minimum_notes = checked_total(users, notes_per_user)?;
779    let db_path = get_db_path(&archive_dir);
780    let (coordinator, invite_code) = get_coordinator_client(&db_path, &invite_code).await?;
781    let minimum_amount_required = note_denomination * u64::from(minimum_notes);
782
783    reissue_initial_notes(initial_notes, &coordinator, &event_sender).await?;
784    get_required_notes(&coordinator, minimum_amount_required, &event_sender).await?;
785
786    info!(
787        "Reminting {minimum_notes} notes of denomination {note_denomination} for {users} users, {notes_per_user} notes per user (this may take a while if the number of users/notes is high)"
788    );
789    remint_denomination(&coordinator, note_denomination, minimum_notes).await?;
790
791    print_coordinator_notes(&coordinator).await?;
792
793    let users_clients = get_users_clients(users, db_path, invite_code.clone()).await?;
794
795    let mut users_notes =
796        get_notes_for_users(users, notes_per_user, coordinator, note_denomination).await?;
797
798    info!("Starting user tasks");
799    let futures = users_clients
800        .into_iter()
801        .enumerate()
802        .map(|(u, client)| {
803            let u = u as u16;
804            let oob_notes = users_notes.remove(&u).unwrap();
805            let event_sender = event_sender.clone();
806            let f: BoxFuture<_> = Box::pin(do_ln_circular_test_user_task(
807                format!("User {u}:"),
808                client,
809                invite_code.clone(),
810                oob_notes,
811                test_duration,
812                ln_payment_sleep,
813                invoice_amount,
814                strategy,
815                event_sender,
816            ));
817            f
818        })
819        .collect::<Vec<_>>();
820
821    Ok(futures)
822}
823
824#[allow(clippy::too_many_arguments)]
825async fn do_ln_circular_test_user_task(
826    prefix: String,
827    client: ClientHandleArc,
828    invite_code: Option<InviteCode>,
829    oob_notes: Vec<OOBNotes>,
830    test_duration: Duration,
831    ln_payment_sleep: Duration,
832    invoice_amount: Amount,
833    strategy: LnCircularStrategy,
834    event_sender: mpsc::UnboundedSender<MetricEvent>,
835) -> anyhow::Result<()> {
836    for oob_note in oob_notes {
837        let amount = oob_note.total_amount();
838        reissue_notes(&client, oob_note, &event_sender)
839            .await
840            .map_err(|e| anyhow!("while reissuing initial {amount}: {e}"))?;
841    }
842    let initial_time = fedimint_core::time::now();
843    let still_ontime = || async {
844        fedimint_core::time::now()
845            .duration_since(initial_time)
846            .expect("time to work")
847            <= test_duration
848    };
849    let sleep_a_bit = || async {
850        if still_ontime().await {
851            fedimint_core::task::sleep(ln_payment_sleep).await;
852        }
853    };
854    match strategy {
855        LnCircularStrategy::TwoGateways => {
856            let invoice_generation = LnInvoiceGeneration::LdkLightningCli;
857            while still_ontime().await {
858                let gateway_id = get_gateway_id(invoice_generation).await?;
859                let ln_gateway = get_lightning_gateway(&client, Some(gateway_id)).await;
860                run_two_gateways_strategy(
861                    &prefix,
862                    &invoice_generation,
863                    &invoice_amount,
864                    &event_sender,
865                    &client,
866                    ln_gateway,
867                )
868                .await?;
869                sleep_a_bit().await;
870            }
871        }
872        LnCircularStrategy::SelfPayment => {
873            while still_ontime().await {
874                do_self_payment(&prefix, &client, invoice_amount, &event_sender).await?;
875                sleep_a_bit().await;
876            }
877        }
878        LnCircularStrategy::PartnerPingPong => {
879            let (partner, _) = build_client(invite_code, None).await?;
880            while still_ontime().await {
881                do_partner_ping_pong(&prefix, &client, &partner, invoice_amount, &event_sender)
882                    .await?;
883                sleep_a_bit().await;
884            }
885        }
886    }
887    Ok(())
888}
889
890const GATEWAY_CREATE_INVOICE: &str = "gateway_create_invoice";
891
892async fn run_two_gateways_strategy(
893    prefix: &str,
894    invoice_generation: &LnInvoiceGeneration,
895    invoice_amount: &Amount,
896    event_sender: &mpsc::UnboundedSender<MetricEvent>,
897    client: &ClientHandleArc,
898    ln_gateway: Option<LightningGateway>,
899) -> Result<(), anyhow::Error> {
900    let create_invoice_time = fedimint_core::time::now();
901    match *invoice_generation {
902        LnInvoiceGeneration::LdkLightningCli => {
903            let invoice = ldk_create_invoice(*invoice_amount).await?;
904            let elapsed = create_invoice_time.elapsed()?;
905            info!("Created invoice using LDK in {elapsed:?}");
906            event_sender.send(MetricEvent {
907                name: GATEWAY_CREATE_INVOICE.into(),
908                duration: elapsed,
909            })?;
910            gateway_pay_invoice(
911                prefix,
912                "LND",
913                client,
914                invoice.clone(),
915                event_sender,
916                ln_gateway.clone(),
917            )
918            .await?;
919            ldk_wait_invoice_payment(&invoice).await?;
920            let (operation_id, invoice) =
921                client_create_invoice(client, *invoice_amount, event_sender, ln_gateway).await?;
922            let pay_invoice_time = fedimint_core::time::now();
923            ldk_pay_invoice(invoice).await?;
924            wait_invoice_payment(
925                prefix,
926                "LND",
927                client,
928                operation_id,
929                event_sender,
930                pay_invoice_time,
931            )
932            .await?;
933        }
934    }
935    Ok(())
936}
937
938async fn do_self_payment(
939    prefix: &str,
940    client: &ClientHandleArc,
941    invoice_amount: Amount,
942    event_sender: &mpsc::UnboundedSender<MetricEvent>,
943) -> anyhow::Result<()> {
944    let (operation_id, invoice) =
945        client_create_invoice(client, invoice_amount, event_sender, None).await?;
946    let pay_invoice_time = fedimint_core::time::now();
947    let lightning_module = client.get_first_module::<LightningClientModule>()?;
948    //let gateway = lightning_module.select_active_gateway_opt().await;
949    lightning_module
950        .pay_bolt11_invoice(None, invoice, ())
951        .await?;
952    wait_invoice_payment(
953        prefix,
954        "gateway",
955        client,
956        operation_id,
957        event_sender,
958        pay_invoice_time,
959    )
960    .await?;
961    Ok(())
962}
963
964async fn do_partner_ping_pong(
965    prefix: &str,
966    client: &ClientHandleArc,
967    partner: &ClientHandleArc,
968    invoice_amount: Amount,
969    event_sender: &mpsc::UnboundedSender<MetricEvent>,
970) -> anyhow::Result<()> {
971    // Ping (partner creates invoice, client pays)
972    let (operation_id, invoice) =
973        client_create_invoice(partner, invoice_amount, event_sender, None).await?;
974    let pay_invoice_time = fedimint_core::time::now();
975    let lightning_module = client.get_first_module::<LightningClientModule>()?;
976    // TODO: Select random gateway?
977    //let gateway = lightning_module.select_active_gateway_opt().await;
978    lightning_module
979        .pay_bolt11_invoice(None, invoice, ())
980        .await?;
981    wait_invoice_payment(
982        prefix,
983        "gateway",
984        partner,
985        operation_id,
986        event_sender,
987        pay_invoice_time,
988    )
989    .await?;
990    // Pong (client creates invoice, partner pays)
991    let (operation_id, invoice) =
992        client_create_invoice(client, invoice_amount, event_sender, None).await?;
993    let pay_invoice_time = fedimint_core::time::now();
994    let partner_lightning_module = partner.get_first_module::<LightningClientModule>()?;
995    //let gateway = partner_lightning_module.select_active_gateway_opt().await;
996    // TODO: Select random gateway?
997    partner_lightning_module
998        .pay_bolt11_invoice(None, invoice, ())
999        .await?;
1000    wait_invoice_payment(
1001        prefix,
1002        "gateway",
1003        client,
1004        operation_id,
1005        event_sender,
1006        pay_invoice_time,
1007    )
1008    .await?;
1009    Ok(())
1010}
1011
1012async fn wait_invoice_payment(
1013    prefix: &str,
1014    gateway_name: &str,
1015    client: &ClientHandleArc,
1016    operation_id: fedimint_core::core::OperationId,
1017    event_sender: &mpsc::UnboundedSender<MetricEvent>,
1018    pay_invoice_time: std::time::SystemTime,
1019) -> anyhow::Result<()> {
1020    let elapsed = pay_invoice_time.elapsed()?;
1021    info!("{prefix} Invoice payment receive started using {gateway_name} in {elapsed:?}");
1022    event_sender.send(MetricEvent {
1023        name: format!("gateway_{gateway_name}_payment_received_started"),
1024        duration: elapsed,
1025    })?;
1026    let lightning_module = client.get_first_module::<LightningClientModule>()?;
1027    let mut updates = lightning_module
1028        .subscribe_ln_receive(operation_id)
1029        .await?
1030        .into_stream();
1031    wait_invoice_payment_updates(
1032        prefix,
1033        gateway_name,
1034        operation_id,
1035        event_sender,
1036        pay_invoice_time,
1037        &mut updates,
1038    )
1039    .await
1040}
1041
1042async fn wait_invoice_payment_updates(
1043    prefix: &str,
1044    gateway_name: &str,
1045    operation_id: fedimint_core::core::OperationId,
1046    event_sender: &mpsc::UnboundedSender<MetricEvent>,
1047    pay_invoice_time: std::time::SystemTime,
1048    updates: &mut (impl futures::Stream<Item = LnReceiveState> + Unpin),
1049) -> anyhow::Result<()> {
1050    while let Some(update) = updates.next().await {
1051        debug!(%prefix, ?update, "Invoice payment update");
1052        match update {
1053            LnReceiveState::Claimed => {
1054                let elapsed: Duration = pay_invoice_time.elapsed()?;
1055                info!("{prefix} Invoice payment received on {gateway_name} in {elapsed:?}");
1056                event_sender.send(MetricEvent {
1057                    name: "gateway_payment_received_success".into(),
1058                    duration: elapsed,
1059                })?;
1060                event_sender.send(MetricEvent {
1061                    name: format!("gateway_{gateway_name}_payment_received_success"),
1062                    duration: elapsed,
1063                })?;
1064                return Ok(());
1065            }
1066            LnReceiveState::Canceled { reason } => {
1067                let elapsed: Duration = pay_invoice_time.elapsed()?;
1068                info!(
1069                    "{prefix} Invoice payment receive was canceled on {gateway_name}: {reason} in {elapsed:?}"
1070                );
1071                event_sender.send(MetricEvent {
1072                    name: "gateway_payment_received_canceled".into(),
1073                    duration: elapsed,
1074                })?;
1075                bail!(
1076                    "Invoice payment receive for operation {operation_id:?} was canceled on \
1077                     {gateway_name}: {reason}"
1078                );
1079            }
1080            _ => {}
1081        }
1082    }
1083    bail!(
1084        "Invoice payment receive stream ended before operation {operation_id:?} was claimed on \
1085         {gateway_name}"
1086    )
1087}
1088
1089async fn client_create_invoice(
1090    client: &ClientHandleArc,
1091    invoice_amount: Amount,
1092    event_sender: &mpsc::UnboundedSender<MetricEvent>,
1093    ln_gateway: Option<LightningGateway>,
1094) -> anyhow::Result<(fedimint_core::core::OperationId, Bolt11Invoice)> {
1095    let create_invoice_time = fedimint_core::time::now();
1096    let lightning_module = client.get_first_module::<LightningClientModule>()?;
1097    let desc = Description::new("test".to_string())?;
1098    let (operation_id, invoice, _) = lightning_module
1099        .create_bolt11_invoice(
1100            invoice_amount,
1101            Bolt11InvoiceDescription::Direct(desc),
1102            None,
1103            (),
1104            ln_gateway,
1105        )
1106        .await?;
1107    let elapsed = create_invoice_time.elapsed()?;
1108    info!("Created invoice using gateway in {elapsed:?}");
1109    event_sender.send(MetricEvent {
1110        name: GATEWAY_CREATE_INVOICE.into(),
1111        duration: elapsed,
1112    })?;
1113    Ok((operation_id, invoice))
1114}
1115
1116fn test_download_config(
1117    endpoints: &ConnectorRegistry,
1118    invite_code: &InviteCode,
1119    users: u16,
1120    event_sender: &mpsc::UnboundedSender<MetricEvent>,
1121) -> Vec<BoxFuture<'static, anyhow::Result<()>>> {
1122    (0..users)
1123        .map(move |_| {
1124            let invite_code = invite_code.clone();
1125            let event_sender = event_sender.clone();
1126            let endpoints = endpoints.clone();
1127            let f: BoxFuture<_> = Box::pin(async move {
1128                let m = fedimint_core::time::now();
1129                let _ = download_from_invite_code(&endpoints, &invite_code).await?;
1130                event_sender.send(MetricEvent {
1131                    name: "download_client_config".into(),
1132                    duration: m.elapsed()?,
1133                })?;
1134                Ok(())
1135            });
1136            f
1137        })
1138        .collect()
1139}
1140
1141async fn test_connect_raw_client(
1142    endpoints: &ConnectorRegistry,
1143    invite_code: InviteCode,
1144    users: u16,
1145    duration: Duration,
1146    timeout: Duration,
1147    limit_endpoints: Option<usize>,
1148    event_sender: mpsc::UnboundedSender<MetricEvent>,
1149) -> anyhow::Result<Vec<BoxFuture<'static, anyhow::Result<()>>>> {
1150    use jsonrpsee_core::client::ClientT;
1151    use jsonrpsee_ws_client::WsClientBuilder;
1152
1153    let (mut cfg, _) = download_from_invite_code(endpoints, &invite_code).await?;
1154
1155    if let Some(limit_endpoints) = limit_endpoints {
1156        cfg.global.api_endpoints = cfg
1157            .global
1158            .api_endpoints
1159            .into_iter()
1160            .take(limit_endpoints)
1161            .collect();
1162        info!("Limiting endpoints to {:?}", cfg.global.api_endpoints);
1163    }
1164
1165    info!("Connecting to {users} clients");
1166    let clients = (0..users)
1167        .flat_map(|_| {
1168            cfg.global.api_endpoints.values().map(|url| async {
1169                let ws_client = WsClientBuilder::default()
1170                    .request_timeout(timeout)
1171                    .connection_timeout(timeout)
1172                    .build(url_to_string_with_default_port(&url.url))
1173                    .await?;
1174                Ok::<_, anyhow::Error>(ws_client)
1175            })
1176        })
1177        .collect::<Vec<_>>();
1178    let clients = futures::future::try_join_all(clients).await?;
1179    info!("Keeping {users} clients connected for {duration:?}");
1180    Ok(clients
1181        .into_iter()
1182        .map(|client| {
1183            let event_sender = event_sender.clone();
1184            let f: BoxFuture<_> = Box::pin(async move {
1185                let initial_time = fedimint_core::time::now();
1186                while initial_time.elapsed()? < duration {
1187                    let m = fedimint_core::time::now();
1188                    let _epoch: u64 = client
1189                        .request::<_, _>(SESSION_COUNT_ENDPOINT, vec![ApiRequestErased::default()])
1190                        .await?;
1191                    event_sender.send(MetricEvent {
1192                        name: SESSION_COUNT_ENDPOINT.into(),
1193                        duration: m.elapsed()?,
1194                    })?;
1195                    fedimint_core::task::sleep(Duration::from_secs(1)).await;
1196                }
1197                Ok(())
1198            });
1199            f
1200        })
1201        .collect())
1202}
1203
1204fn url_to_string_with_default_port(url: &SafeUrl) -> String {
1205    format!(
1206        "{}://{}:{}{}",
1207        url.scheme(),
1208        url.host().expect("Asserted on construction"),
1209        url.port_or_known_default()
1210            .expect("Asserted on construction"),
1211        url.path()
1212    )
1213}
1214
1215async fn handle_metrics_summary(
1216    opts: Opts,
1217    mut event_receiver: mpsc::UnboundedReceiver<MetricEvent>,
1218) -> anyhow::Result<()> {
1219    let timestamp_seconds = fedimint_core::time::duration_since_epoch().as_secs();
1220    let mut metrics_json_output_files = vec![];
1221    let mut previous_metrics = vec![];
1222    let mut comparison_output = None;
1223    if let Some(archive_dir) = opts.archive_dir {
1224        let mut archive_metrics = archive_dir.join("metrics");
1225        archive_metrics.push(opts.users.to_string());
1226        tokio::fs::create_dir_all(&archive_metrics).await?;
1227        let mut archive_comparisons = archive_dir.join("comparisons");
1228        archive_comparisons.push(opts.users.to_string());
1229        tokio::fs::create_dir_all(&archive_comparisons).await?;
1230
1231        let latest_metrics_file = std::fs::read_dir(&archive_metrics)?
1232            .map(|entry| {
1233                let entry = entry.unwrap();
1234                let metadata = entry.metadata().unwrap();
1235                let created = metadata
1236                    .created()
1237                    .unwrap_or_else(|_| metadata.modified().unwrap());
1238                (entry, created)
1239            })
1240            .max_by_key(|(_entry, created)| created.to_owned())
1241            .map(|(entry, _)| entry.path());
1242        if let Some(latest_metrics_file) = latest_metrics_file {
1243            let display_path = latest_metrics_file.display();
1244            let latest_metrics_file = tokio::fs::File::open(&latest_metrics_file)
1245                .await
1246                .with_context(|| format!("Failed to open {display_path}"))?;
1247            let mut lines = tokio::io::BufReader::new(latest_metrics_file).lines();
1248            while let Some(line) = lines.next_line().await? {
1249                match serde_json::from_str::<EventMetricSummary>(&line) {
1250                    Ok(metric) => {
1251                        previous_metrics.push(metric);
1252                    }
1253                    Err(e) => {
1254                        warn!("Failed to parse previous metric: {e:?}");
1255                    }
1256                }
1257            }
1258        }
1259        let new_metric_output = archive_metrics.join(format!("{timestamp_seconds}.json",));
1260        let new_metric_output = BufWriter::new(
1261            OpenOptions::new()
1262                .write(true)
1263                .create(true)
1264                .truncate(true)
1265                .open(new_metric_output)
1266                .await?,
1267        );
1268        metrics_json_output_files.push(new_metric_output);
1269        if !previous_metrics.is_empty() {
1270            let new_comparison_output =
1271                archive_comparisons.join(format!("{timestamp_seconds}.json",));
1272            comparison_output = Some(BufWriter::new(
1273                OpenOptions::new()
1274                    .write(true)
1275                    .create(true)
1276                    .truncate(true)
1277                    .open(new_comparison_output)
1278                    .await?,
1279            ));
1280        }
1281    }
1282    if let Some(metrics_json_output) = opts.metrics_json_output {
1283        metrics_json_output_files.push(BufWriter::new(
1284            tokio::fs::OpenOptions::new()
1285                .write(true)
1286                .create(true)
1287                .truncate(true)
1288                .open(metrics_json_output)
1289                .await?,
1290        ));
1291    }
1292    let mut results = BTreeMap::new();
1293    while let Some(event) = event_receiver.recv().await {
1294        let entry = results.entry(event.name).or_insert_with(Vec::new);
1295        entry.push(event.duration);
1296    }
1297    let mut previous_metrics = previous_metrics
1298        .into_iter()
1299        .map(|metric| (metric.name.clone(), metric))
1300        .collect::<HashMap<_, _>>();
1301    for (k, mut v) in results {
1302        v.sort();
1303        let n = v.len();
1304        let max = v.iter().last().unwrap();
1305        let min = v.first().unwrap();
1306        let median = v[n / 2];
1307        let sum: Duration = v.iter().sum();
1308        let avg = sum / n as u32;
1309        let metric_summary = EventMetricSummary {
1310            name: k.clone(),
1311            users: u64::from(opts.users),
1312            n: n as u64,
1313            avg_ms: avg.as_millis(),
1314            median_ms: median.as_millis(),
1315            max_ms: max.as_millis(),
1316            min_ms: min.as_millis(),
1317            timestamp_seconds,
1318        };
1319        let comparison = if let Some(previous_metric) = previous_metrics.remove(&k) {
1320            if previous_metric.n == metric_summary.n {
1321                fn calculate_gain(current: u128, previous: u128) -> f64 {
1322                    current as f64 / previous as f64
1323                }
1324                let comparison = EventMetricComparison {
1325                    avg_ms_gain: calculate_gain(metric_summary.avg_ms, previous_metric.avg_ms),
1326                    median_ms_gain: calculate_gain(
1327                        metric_summary.median_ms,
1328                        previous_metric.median_ms,
1329                    ),
1330                    max_ms_gain: calculate_gain(metric_summary.max_ms, previous_metric.max_ms),
1331                    min_ms_gain: calculate_gain(metric_summary.min_ms, previous_metric.min_ms),
1332                    current: metric_summary.clone(),
1333                    previous: previous_metric,
1334                };
1335                if let Some(comparison_output) = &mut comparison_output {
1336                    let comparison_json =
1337                        serde_json::to_string(&comparison).expect("to be serializable");
1338                    comparison_output
1339                        .write_all(format!("{comparison_json}\n").as_bytes())
1340                        .await
1341                        .expect("to write on file");
1342                }
1343                Some(comparison)
1344            } else {
1345                info!(
1346                    "Skipping comparison for {k} because previous metric has different n ({} vs {})",
1347                    previous_metric.n, metric_summary.n
1348                );
1349                None
1350            }
1351        } else {
1352            None
1353        };
1354        if let Some(comparison) = comparison {
1355            println!(
1356                "{n} {k}: avg {avg:?}, median {median:?}, max {max:?}, min {min:?} (compared to previous: {comparison})"
1357            );
1358        } else {
1359            println!("{n} {k}: avg {avg:?}, median {median:?}, max {max:?}, min {min:?}");
1360        }
1361        let metric_summary_json =
1362            serde_json::to_string(&metric_summary).expect("to be serializable");
1363        for metrics_json_output_file in &mut metrics_json_output_files {
1364            metrics_json_output_file
1365                .write_all(format!("{metric_summary_json}\n").as_bytes())
1366                .await
1367                .expect("to write on file");
1368        }
1369    }
1370    for mut output in metrics_json_output_files {
1371        output.flush().await?;
1372    }
1373    if let Some(mut output) = comparison_output {
1374        output.flush().await?;
1375    }
1376    Ok(())
1377}
1378
1379async fn get_gateway_id(generate_invoice_with: LnInvoiceGeneration) -> anyhow::Result<String> {
1380    let gateway_json = match generate_invoice_with {
1381        LnInvoiceGeneration::LdkLightningCli => {
1382            // If we are paying a lnd invoice, we use the LDK node
1383            cmd!(GatewayLndCli, "info").out_json().await
1384        }
1385    }?;
1386
1387    let gatewayd_version = devimint::util::Gatewayd::version_or_default().await;
1388    let gateway_id = if gatewayd_version < *VERSION_0_10_0_ALPHA {
1389        gateway_json["gateway_id"]
1390            .as_str()
1391            .context("gateway_id must be a string")?
1392            .to_owned()
1393    } else {
1394        gateway_json["registrations"]["http"][1]
1395            .as_str()
1396            .context("gateway id must be a string")?
1397            .to_owned()
1398    };
1399
1400    Ok(gateway_id)
1401}