From c25bf79e8c07f03f28f39109669b726fb1e065fb Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Sat, 5 Oct 2024 11:18:23 +0200 Subject: [PATCH] Cache `SwapResponse`, `MeltBolt11Response` and `MintBolt11Response` (#361) * added cache to mint state and post request wrapper macro. --------- Co-authored-by: thesimplekid --- Cargo.toml | 2 - crates/cdk-axum/Cargo.toml | 3 + crates/cdk-axum/src/lib.rs | 32 +++++++---- crates/cdk-axum/src/router_handlers.rs | 36 ++++++++++++ crates/cdk-integration-tests/Cargo.toml | 1 + .../src/init_fake_wallet.rs | 5 +- .../cdk-integration-tests/src/init_regtest.rs | 13 +++-- crates/cdk-integration-tests/src/lib.rs | 9 ++- crates/cdk-integration-tests/tests/regtest.rs | 56 ++++++++++++++++++- crates/cdk-mintd/src/config.rs | 2 + crates/cdk-mintd/src/main.rs | 12 +++- flake.nix | 2 + 12 files changed, 149 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 92c7e98d7..9f6a48905 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,6 @@ repository = "https://github.com/cashubtc/cdk" license-file = "LICENSE" keywords = ["bitcoin", "e-cash", "cashu"] -[profile] - [profile.ci] inherits = "dev" incremental = false diff --git a/crates/cdk-axum/Cargo.toml b/crates/cdk-axum/Cargo.toml index 616cc257d..45a71d4b5 100644 --- a/crates/cdk-axum/Cargo.toml +++ b/crates/cdk-axum/Cargo.toml @@ -16,3 +16,6 @@ cdk = { path = "../cdk", version = "0.4.0", default-features = false, features = tokio = { version = "1", default-features = false } tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } futures = { version = "0.3.28", default-features = false } +moka = { version = "0.11.1", features = ["future"] } +serde_json = "1" +paste = "1.0.15" diff --git a/crates/cdk-axum/src/lib.rs b/crates/cdk-axum/src/lib.rs index de841aa04..35078446d 100644 --- a/crates/cdk-axum/src/lib.rs +++ b/crates/cdk-axum/src/lib.rs @@ -9,31 +9,47 @@ use anyhow::Result; use axum::routing::{get, post}; use axum::Router; use cdk::mint::Mint; +use moka::future::Cache; use router_handlers::*; +use std::time::Duration; mod router_handlers; +/// CDK Mint State +#[derive(Clone)] +pub struct MintState { + mint: Arc, + cache: Cache, +} + /// Create mint [`Router`] with required endpoints for cashu mint -pub async fn create_mint_router(mint: Arc) -> Result { - let state = MintState { mint }; +pub async fn create_mint_router(mint: Arc, cache_ttl: u64, cache_tti: u64) -> Result { + let state = MintState { + mint, + cache: Cache::builder() + .max_capacity(10_000) + .time_to_live(Duration::from_secs(cache_ttl)) + .time_to_idle(Duration::from_secs(cache_tti)) + .build(), + }; let v1_router = Router::new() .route("/keys", get(get_keys)) .route("/keysets", get(get_keysets)) .route("/keys/:keyset_id", get(get_keyset_pubkeys)) - .route("/swap", post(post_swap)) + .route("/swap", post(cache_post_swap)) .route("/mint/quote/bolt11", post(get_mint_bolt11_quote)) .route( "/mint/quote/bolt11/:quote_id", get(get_check_mint_bolt11_quote), ) - .route("/mint/bolt11", post(post_mint_bolt11)) + .route("/mint/bolt11", post(cache_post_mint_bolt11)) .route("/melt/quote/bolt11", post(get_melt_bolt11_quote)) .route( "/melt/quote/bolt11/:quote_id", get(get_check_melt_bolt11_quote), ) - .route("/melt/bolt11", post(post_melt_bolt11)) + .route("/melt/bolt11", post(cache_post_melt_bolt11)) .route("/checkstate", post(post_check)) .route("/info", get(get_mint_info)) .route("/restore", post(post_restore)); @@ -42,9 +58,3 @@ pub async fn create_mint_router(mint: Arc) -> Result { Ok(mint_router) } - -/// CDK Mint State -#[derive(Clone)] -pub struct MintState { - mint: Arc, -} diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index aec451bea..10039f0bb 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -10,9 +10,45 @@ use cdk::nuts::{ SwapRequest, SwapResponse, }; use cdk::util::unix_time; +use cdk::Error; +use paste::paste; use crate::MintState; +macro_rules! post_cache_wrapper { + ($handler:ident, $request_type:ty, $response_type:ty) => { + paste! { + /// Cache wrapper function for $handler: + /// Wrap $handler into a function that caches responses using the request as key + pub async fn []( + state: State, + payload: Json<$request_type> + ) -> Result, Response> { + let Json(json_extracted_payload) = payload.clone(); + let State(mint_state) = state.clone(); + let cache_key = serde_json::to_string(&json_extracted_payload).map_err(|err| { + into_response(Error::from(err)) + })?; + + if let Some(cached_response) = mint_state.cache.get(&cache_key) { + return Ok(Json(serde_json::from_str(&cached_response) + .expect("Shouldn't panic: response is json-deserializable."))); + } + + let Json(response) = $handler(state, payload).await?; + mint_state.cache.insert(cache_key, serde_json::to_string(&response) + .expect("Shouldn't panic: response is json-serializable.") + ).await; + Ok(Json(response)) + } + } + }; +} + +post_cache_wrapper!(post_swap, SwapRequest, SwapResponse); +post_cache_wrapper!(post_mint_bolt11, MintBolt11Request, MintBolt11Response); +post_cache_wrapper!(post_melt_bolt11, MeltBolt11Request, MeltBolt11Response); + pub async fn get_keys(State(state): State) -> Result, Response> { let pubkeys = state.mint.pubkeys().await.map_err(|err| { tracing::error!("Could not get keys: {}", err); diff --git a/crates/cdk-integration-tests/Cargo.toml b/crates/cdk-integration-tests/Cargo.toml index 7e9e78bd4..7291eba9a 100644 --- a/crates/cdk-integration-tests/Cargo.toml +++ b/crates/cdk-integration-tests/Cargo.toml @@ -35,6 +35,7 @@ ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = " lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tower-service = "0.3.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1", features = [ diff --git a/crates/cdk-integration-tests/src/init_fake_wallet.rs b/crates/cdk-integration-tests/src/init_fake_wallet.rs index 5ad1a1cb0..3eeaa37f9 100644 --- a/crates/cdk-integration-tests/src/init_fake_wallet.rs +++ b/crates/cdk-integration-tests/src/init_fake_wallet.rs @@ -61,10 +61,11 @@ where ); let mint = create_mint(database, ln_backends.clone()).await?; - + let cache_ttl = 3600; + let cache_tti = 3600; let mint_arc = Arc::new(mint); - let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc)) + let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc), cache_ttl, cache_tti) .await .unwrap(); diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index cc46d4b11..769a33500 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -214,12 +214,17 @@ where ); let mint = create_mint(database, ln_backends.clone()).await?; - + let cache_time_to_live = 3600; + let cache_time_to_idle = 3600; let mint_arc = Arc::new(mint); - let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc)) - .await - .unwrap(); + let v1_service = cdk_axum::create_mint_router( + Arc::clone(&mint_arc), + cache_time_to_live, + cache_time_to_idle, + ) + .await + .unwrap(); let mint_service = Router::new() .merge(v1_service) diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index ff2d4dfc2..eacc3b3a5 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -84,10 +84,17 @@ pub async fn start_mint( supported_units, ) .await?; + let cache_time_to_live = 3600; + let cache_time_to_idle = 3600; let mint_arc = Arc::new(mint); - let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc)).await?; + let v1_service = cdk_axum::create_mint_router( + Arc::clone(&mint_arc), + cache_time_to_live, + cache_time_to_idle, + ) + .await?; let mint_service = Router::new() .merge(v1_service) diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index f430401dd..5658fb579 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -1,16 +1,17 @@ -use std::{str::FromStr, sync::Arc}; +use std::{str::FromStr, sync::Arc, time::Duration}; use anyhow::{bail, Result}; use bip39::Mnemonic; use cdk::{ amount::{Amount, SplitTarget}, cdk_database::WalletMemoryDatabase, - nuts::{CurrencyUnit, MeltQuoteState, State}, - wallet::Wallet, + nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, PreMintSecrets, State}, + wallet::{client::HttpClient, Wallet}, }; use cdk_integration_tests::init_regtest::{get_mint_url, init_cln_client, init_lnd_client}; use lightning_invoice::Bolt11Invoice; use ln_regtest_rs::InvoiceStatus; +use tokio::time::sleep; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_regtest_mint_melt_round_trip() -> Result<()> { @@ -253,3 +254,52 @@ async fn test_internal_payment() -> Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_cached_mint() -> Result<()> { + let lnd_client = init_lnd_client().await.unwrap(); + + let wallet = Wallet::new( + &get_mint_url(), + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let mint_amount = Amount::from(100); + + let quote = wallet.mint_quote(mint_amount, None).await?; + lnd_client.pay_invoice(quote.request).await?; + + loop { + let status = wallet.mint_quote_state("e.id).await.unwrap(); + + println!("Quote status: {}", status.state); + + if status.state == MintQuoteState::Paid { + break; + } + + sleep(Duration::from_secs(5)).await; + } + + let active_keyset_id = wallet.get_active_mint_keyset().await?.id; + let http_client = HttpClient::new(); + let premint_secrets = + PreMintSecrets::random(active_keyset_id, 31.into(), &SplitTarget::default()).unwrap(); + + let response = http_client + .post_mint( + get_mint_url().as_str().parse()?, + "e.id, + premint_secrets.clone(), + ) + .await?; + let response1 = http_client + .post_mint(get_mint_url().as_str().parse()?, "e.id, premint_secrets) + .await?; + + assert!(response == response1); + Ok(()) +} diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index 16a2a7f77..756c2d148 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -12,6 +12,8 @@ pub struct Info { pub listen_port: u16, pub mnemonic: String, pub seconds_quote_is_valid_for: Option, + pub seconds_to_cache_requests_for: Option, + pub seconds_to_extend_cache_by: Option, pub input_fee_ppk: Option, } diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index b86a41e3b..6462236b4 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -42,6 +42,8 @@ mod config; const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); const DEFAULT_QUOTE_TTL_SECS: u64 = 1800; +const DEFAULT_CACHE_TTL_SECS: u64 = 1800; +const DEFAULT_CACHE_TTI_SECS: u64 = 1800; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -458,8 +460,16 @@ async fn main() -> anyhow::Result<()> { .info .seconds_quote_is_valid_for .unwrap_or(DEFAULT_QUOTE_TTL_SECS); + let cache_ttl = settings + .info + .seconds_to_cache_requests_for + .unwrap_or(DEFAULT_CACHE_TTL_SECS); + let cache_tti = settings + .info + .seconds_to_extend_cache_by + .unwrap_or(DEFAULT_CACHE_TTI_SECS); - let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint)).await?; + let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint), cache_ttl, cache_tti).await?; let mut mint_service = Router::new() .merge(v1_service) diff --git a/flake.nix b/flake.nix index 75103e439..c1344873d 100644 --- a/flake.nix +++ b/flake.nix @@ -137,6 +137,8 @@ cargo update -p backtrace --precise 0.3.58 # For wasm32-unknown-unknown target cargo update -p bumpalo --precise 3.12.0 + cargo update -p moka --precise 0.11.1 + cargo update -p triomphe --precise 0.1.11 "; buildInputs = buildInputs ++ WASMInputs ++ [ msrv_toolchain ]; inherit nativeBuildInputs;