Explorar o código

Cache `SwapResponse`, `MeltBolt11Response` and `MintBolt11Response` (#361)

* added cache to mint state and post request wrapper macro.
---------

Co-authored-by: thesimplekid <tsk@thesimplekid.com>
lollerfirst hai 5 meses
pai
achega
c25bf79e8c

+ 0 - 2
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

+ 3 - 0
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"

+ 21 - 11
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<Mint>,
+    cache: Cache<String, String>,
+}
+
 /// Create mint [`Router`] with required endpoints for cashu mint
-pub async fn create_mint_router(mint: Arc<Mint>) -> Result<Router> {
-    let state = MintState { mint };
+pub async fn create_mint_router(mint: Arc<Mint>, cache_ttl: u64, cache_tti: u64) -> Result<Router> {
+    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<Mint>) -> Result<Router> {
 
     Ok(mint_router)
 }
-
-/// CDK Mint State
-#[derive(Clone)]
-pub struct MintState {
-    mint: Arc<Mint>,
-}

+ 36 - 0
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 [<cache_ $handler>](
+                state: State<MintState>,
+                payload: Json<$request_type>
+            ) -> Result<Json<$response_type>, 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<MintState>) -> Result<Json<KeysResponse>, Response> {
     let pubkeys = state.mint.pubkeys().await.map_err(|err| {
         tracing::error!("Could not get keys: {}", err);

+ 1 - 0
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 = [

+ 3 - 2
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();
 

+ 9 - 4
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)

+ 8 - 1
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)

+ 53 - 3
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(&quote.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()?,
+            &quote.id,
+            premint_secrets.clone(),
+        )
+        .await?;
+    let response1 = http_client
+        .post_mint(get_mint_url().as_str().parse()?, &quote.id, premint_secrets)
+        .await?;
+
+    assert!(response == response1);
+    Ok(())
+}

+ 2 - 0
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<u64>,
+    pub seconds_to_cache_requests_for: Option<u64>,
+    pub seconds_to_extend_cache_by: Option<u64>,
     pub input_fee_ppk: Option<u64>,
 }
 

+ 11 - 1
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)

+ 2 - 0
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;