Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 80 additions & 7 deletions payjoin-cli/src/app/v2/ohttp.rs
Original file line number Diff line number Diff line change
@@ -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<url::Url>,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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}");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the logger instead of println?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
println!("using Cached keys for relay: {selected_relay}");
println!("using Cached keys for relay: {selected_relay}");

if !is_expired(&cached) && cached.relay_url == selected_relay {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would read_cached_ohttp_keys return an expired or keys for a different relay? Perphaps read_cached_ohttp_keys should return ValidateOhttpKeys?

return Ok(ValidatedOhttpKeys {
ohttp_keys: cached.keys,
relay_url: cached.relay_url,
});
}
}

let ohttp_keys = {
#[cfg(feature = "_manual-tls")]
{
Expand All @@ -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());
}
Expand All @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are cached keys not being persisted in the database?

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<CachedOhttpKeys> {
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()
}
11 changes: 11 additions & 0 deletions payjoin-cli/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -203,6 +212,8 @@ mod e2e {
use tempfile::TempDir;
use tokio::process::Child;

clear_payjoin_cache()?;

type Result<T> = std::result::Result<T, BoxError>;

init_tracing();
Expand Down
Loading