From 2198e3f505c6d577d2b1ea243182298956e665c9 Mon Sep 17 00:00:00 2001 From: zealsham Date: Wed, 3 Sep 2025 00:58:39 +0100 Subject: [PATCH] Payjoin-cli should cache ohttp-keys for re-use This pr adds the functionality of ohttp-keys catching to payjoin-cli , ohttp-keys should not be fetched each time and a cached key should be use. Keys expire in 6 months . --- payjoin-cli/src/app/v2/ohttp.rs | 87 ++++++++++++++++++++++++++++++--- payjoin-cli/tests/e2e.rs | 11 +++++ 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/payjoin-cli/src/app/v2/ohttp.rs b/payjoin-cli/src/app/v2/ohttp.rs index aa9b55d84..45b7ff77b 100644 --- a/payjoin-cli/src/app/v2/ohttp.rs +++ b/payjoin-cli/src/app/v2/ohttp.rs @@ -1,9 +1,16 @@ +use std::fs; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime}; use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; use super::Config; +// 6 months +const CACHE_DURATION: Duration = Duration::from_secs(6 * 30 * 24 * 60 * 60); + #[derive(Debug, Clone)] pub struct RelayManager { selected_relay: Option, @@ -38,12 +45,12 @@ pub(crate) async fn unwrap_ohttp_keys_or_else_fetch( ohttp_keys, relay_url: config.v2()?.ohttp_relays[0].clone(), }); - } else { - println!("Bootstrapping private network transport over Oblivious HTTP"); - let fetched_keys = fetch_ohttp_keys(config, directory, relay_manager).await?; - - Ok(fetched_keys) } + + println!("Bootstrapping private network transport over Oblivious HTTP"); + let fetched_keys = fetch_ohttp_keys(config, directory, relay_manager).await?; + + Ok(fetched_keys) } async fn fetch_ohttp_keys( @@ -77,6 +84,17 @@ async fn fetch_ohttp_keys( .expect("Lock should not be poisoned") .set_selected_relay(selected_relay.clone()); + // try cache for this selected relay first + if let Some(cached) = read_cached_ohttp_keys(&selected_relay) { + println!("using Cached keys for relay: {selected_relay}"); + if !is_expired(&cached) && cached.relay_url == selected_relay { + return Ok(ValidatedOhttpKeys { + ohttp_keys: cached.keys, + relay_url: cached.relay_url, + }); + } + } + let ohttp_keys = { #[cfg(feature = "_manual-tls")] { @@ -101,8 +119,17 @@ async fn fetch_ohttp_keys( }; match ohttp_keys { - Ok(keys) => - return Ok(ValidatedOhttpKeys { ohttp_keys: keys, relay_url: selected_relay }), + Ok(keys) => { + // Cache the keys if they are not already cached for this relay + if read_cached_ohttp_keys(&selected_relay).is_none() { + if let Err(e) = cache_ohttp_keys(&keys, &selected_relay) { + tracing::debug!( + "Failed to cache OHTTP keys for relay {selected_relay}: {e:?}" + ); + } + } + return Ok(ValidatedOhttpKeys { ohttp_keys: keys, relay_url: selected_relay }); + } Err(payjoin::io::Error::UnexpectedStatusCode(e)) => { return Err(payjoin::io::Error::UnexpectedStatusCode(e).into()); } @@ -116,3 +143,49 @@ async fn fetch_ohttp_keys( } } } + +#[derive(Serialize, Deserialize, Debug)] +struct CachedOhttpKeys { + keys: payjoin::OhttpKeys, + relay_url: url::Url, + fetched_at: u64, +} + +fn get_cache_file(relay_url: &url::Url) -> PathBuf { + dirs::cache_dir() + .unwrap() + .join("payjoin-cli") + .join(relay_url.host_str().unwrap()) + .join("ohttp-keys.json") +} + +fn read_cached_ohttp_keys(relay_url: &url::Url) -> Option { + let cache_file = get_cache_file(relay_url); + if !cache_file.exists() { + return None; + } + let data = fs::read_to_string(cache_file).ok().unwrap(); + serde_json::from_str(&data).ok() +} + +fn cache_ohttp_keys(ohttp_keys: &payjoin::OhttpKeys, relay_url: &url::Url) -> Result<()> { + let cached = CachedOhttpKeys { + keys: ohttp_keys.clone(), + relay_url: relay_url.clone(), + fetched_at: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(), + }; + + let serialized = serde_json::to_string(&cached)?; + let path = get_cache_file(relay_url); + fs::create_dir_all(path.parent().unwrap())?; + fs::write(path, serialized)?; + Ok(()) +} + +fn is_expired(cached_keys: &CachedOhttpKeys) -> bool { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs(); + now.saturating_sub(cached_keys.fetched_at) > CACHE_DURATION.as_secs() +} diff --git a/payjoin-cli/tests/e2e.rs b/payjoin-cli/tests/e2e.rs index 41691d491..1f5f67b66 100644 --- a/payjoin-cli/tests/e2e.rs +++ b/payjoin-cli/tests/e2e.rs @@ -64,6 +64,15 @@ mod e2e { res } + fn clear_payjoin_cache() -> std::io::Result<()> { + let cache_dir = dirs::cache_dir().unwrap().join("payjoin-cli"); + + if cache_dir.exists() { + std::fs::remove_dir_all(cache_dir)?; + } + Ok(()) + } + #[cfg(feature = "v1")] #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn send_receive_payjoin_v1() -> Result<(), BoxError> { @@ -203,6 +212,8 @@ mod e2e { use tempfile::TempDir; use tokio::process::Child; + clear_payjoin_cache()?; + type Result = std::result::Result; init_tracing();