From a5bbca70fe0bb93b2fcd6f06de3a5fcf9ab4ca24 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 9 Jan 2025 15:46:43 +0000 Subject: [PATCH] feat: mint sql auth db --- crates/cdk-mintd/src/main.rs | 17 +- .../auth/migrations/20250109143347_init.sql | 37 ++ crates/cdk-sqlite/src/mint/auth/mod.rs | 392 ++++++++++++++++++ crates/cdk-sqlite/src/mint/mod.rs | 3 + crates/cdk/src/cdk_database/mod.rs | 3 + 5 files changed, 445 insertions(+), 7 deletions(-) create mode 100644 crates/cdk-sqlite/src/mint/auth/migrations/20250109143347_init.sql create mode 100644 crates/cdk-sqlite/src/mint/auth/mod.rs diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 5eb3507c..3e8d3c2f 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -14,7 +14,7 @@ use axum::middleware::Next; use axum::response::Response; use axum::{middleware, Router}; use bip39::Mnemonic; -use cdk::cdk_database::{self, MintDatabase, MintMemoryAuthDatabase}; +use cdk::cdk_database::{self, MintDatabase}; use cdk::cdk_lightning; use cdk::cdk_lightning::MintLightning; use cdk::mint::{MintAuthDatabase, MintBuilder, MintMeltLimits}; @@ -31,6 +31,7 @@ use cdk_mintd::config::{self, DatabaseEngine, LnBackend}; use cdk_mintd::setup::LnBackendSetup; use cdk_redb::mint::MintRedbAuthDatabase; use cdk_redb::MintRedbDatabase; +use cdk_sqlite::mint::MintSqliteAuthDatabase; use cdk_sqlite::MintSqliteDatabase; use clap::Parser; use tokio::sync::Notify; @@ -373,12 +374,14 @@ async fn main() -> anyhow::Result<()> { let auth_localstore: Arc + Send + Sync> = match settings.database.engine { - DatabaseEngine::Sqlite => Arc::new(MintMemoryAuthDatabase::new( - None, - vec![], - vec![], - HashMap::new(), - )?), + DatabaseEngine::Sqlite => { + let sql_db_path = work_dir.join("cdk-mintd-auth.sqlite"); + let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?; + + sqlite_db.migrate().await; + + Arc::new(sqlite_db) + } DatabaseEngine::Redb => { let redb_path = work_dir.join("cdk-mintd-auth.redb"); Arc::new(MintRedbAuthDatabase::new(&redb_path)?) diff --git a/crates/cdk-sqlite/src/mint/auth/migrations/20250109143347_init.sql b/crates/cdk-sqlite/src/mint/auth/migrations/20250109143347_init.sql new file mode 100644 index 00000000..23664ad7 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/auth/migrations/20250109143347_init.sql @@ -0,0 +1,37 @@ +CREATE TABLE IF NOT EXISTS proof ( +y BLOB PRIMARY KEY, +keyset_id TEXT NOT NULL, +secret TEXT NOT NULL, +c BLOB NOT NULL, +state TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS state_index ON proof(state); +CREATE INDEX IF NOT EXISTS secret_index ON proof(secret); + + +-- Keysets Table + +CREATE TABLE IF NOT EXISTS keyset ( + id TEXT PRIMARY KEY, + unit TEXT NOT NULL, + active BOOL NOT NULL, + valid_from INTEGER NOT NULL, + valid_to INTEGER, + derivation_path TEXT NOT NULL, + max_order INTEGER NOT NULL, + derivation_path_index INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS unit_index ON keyset(unit); +CREATE INDEX IF NOT EXISTS active_index ON keyset(active); + + +CREATE TABLE IF NOT EXISTS blind_signature ( + y BLOB PRIMARY KEY, + amount INTEGER NOT NULL, + keyset_id TEXT NOT NULL, + c BLOB NOT NULL +); + +CREATE INDEX IF NOT EXISTS keyset_id_index ON blind_signature(keyset_id); diff --git a/crates/cdk-sqlite/src/mint/auth/mod.rs b/crates/cdk-sqlite/src/mint/auth/mod.rs new file mode 100644 index 00000000..854b25f1 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/auth/mod.rs @@ -0,0 +1,392 @@ +//! SQLite Mint Auth + +use std::collections::HashMap; +use std::path::Path; +use std::str::FromStr; +use std::time::Duration; + +use async_trait::async_trait; +use cdk::cdk_database::{self}; +use cdk::mint::{MintAuthDatabase, MintKeySetInfo}; +use cdk::nuts::{AuthProof, BlindSignature, Id, PublicKey, State}; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; +use sqlx::Row; + +use super::{sqlite_row_to_blind_signature, sqlite_row_to_keyset_info}; +use crate::mint::Error; + +/// Mint SQLite Database +#[derive(Debug, Clone)] +pub struct MintSqliteAuthDatabase { + pool: SqlitePool, +} + +impl MintSqliteAuthDatabase { + /// Create new [`MintSqliteDatabase`] + pub async fn new(path: &Path) -> Result { + let path = path.to_str().ok_or(Error::InvalidDbPath)?; + let db_options = SqliteConnectOptions::from_str(path)? + .busy_timeout(Duration::from_secs(5)) + .read_only(false) + .create_if_missing(true) + .auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Full); + + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect_with(db_options) + .await?; + + Ok(Self { pool }) + } + + /// Migrate [`MintSqliteDatabase`] + pub async fn migrate(&self) { + sqlx::migrate!("./src/mint/auth/migrations") + .run(&self.pool) + .await + .expect("Could not run migrations"); + } +} + +#[async_trait] +impl MintAuthDatabase for MintSqliteAuthDatabase { + type Err = cdk_database::Error; + + async fn set_active_keyset(&self, id: Id) -> Result<(), Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + let update_res = sqlx::query( + r#" + UPDATE keyset + SET active = CASE + WHEN id = ? THEN TRUE + ELSE FALSE + END; + "#, + ) + .bind(id.to_string()) + .execute(&mut transaction) + .await; + + match update_res { + Ok(_) => { + transaction.commit().await.map_err(Error::from)?; + Ok(()) + } + Err(err) => { + tracing::error!("SQLite Could not update keyset"); + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + Err(Error::from(err).into()) + } + } + } + + async fn get_active_keyset_id(&self) -> Result, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + + let rec = sqlx::query( + r#" +SELECT id +FROM keyset +WHERE active = 1; + "#, + ) + .fetch_one(&mut transaction) + .await; + + let rec = match rec { + Ok(rec) => { + transaction.commit().await.map_err(Error::from)?; + rec + } + Err(err) => match err { + sqlx::Error::RowNotFound => { + transaction.commit().await.map_err(Error::from)?; + return Ok(None); + } + _ => { + return { + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + Err(Error::SQLX(err).into()) + } + } + }, + }; + + Ok(Some( + Id::from_str(rec.try_get("id").map_err(Error::from)?).map_err(Error::from)?, + )) + } + + async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + let res = sqlx::query( + r#" +INSERT OR REPLACE INTO keyset +(id, unit, active, valid_from, valid_to, derivation_path, max_order, derivation_path_index) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); + "#, + ) + .bind(keyset.id.to_string()) + .bind(keyset.unit.to_string()) + .bind(keyset.active) + .bind(keyset.valid_from as i64) + .bind(keyset.valid_to.map(|v| v as i64)) + .bind(keyset.derivation_path.to_string()) + .bind(keyset.max_order) + .bind(keyset.derivation_path_index) + .execute(&mut transaction) + .await; + + match res { + Ok(_) => { + transaction.commit().await.map_err(Error::from)?; + Ok(()) + } + Err(err) => { + tracing::error!("SQLite could not add keyset info"); + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + + Err(Error::from(err).into()) + } + } + } + + async fn get_keyset_info(&self, id: &Id) -> Result, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + let rec = sqlx::query( + r#" +SELECT * +FROM keyset +WHERE id=?; + "#, + ) + .bind(id.to_string()) + .fetch_one(&mut transaction) + .await; + + match rec { + Ok(rec) => { + transaction.commit().await.map_err(Error::from)?; + Ok(Some(sqlite_row_to_keyset_info(rec)?)) + } + Err(err) => match err { + sqlx::Error::RowNotFound => { + transaction.commit().await.map_err(Error::from)?; + return Ok(None); + } + _ => { + tracing::error!("SQLite could not get keyset info"); + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + return Err(Error::SQLX(err).into()); + } + }, + } + } + + async fn get_keyset_infos(&self) -> Result, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + let recs = sqlx::query( + r#" +SELECT * +FROM keyset; + "#, + ) + .fetch_all(&mut transaction) + .await + .map_err(Error::from); + + match recs { + Ok(recs) => { + transaction.commit().await.map_err(Error::from)?; + Ok(recs + .into_iter() + .map(sqlite_row_to_keyset_info) + .collect::>()?) + } + Err(err) => { + tracing::error!("SQLite could not get keyset info"); + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + Err(err.into()) + } + } + } + + async fn add_proof(&self, proof: AuthProof) -> Result<(), Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + if let Err(err) = sqlx::query( + r#" +INSERT INTO proof +(y, keyset_id, secret, c, state) +VALUES (?, ?, ?, ?, ?, ?); + "#, + ) + .bind(proof.y()?.to_bytes().to_vec()) + .bind(proof.keyset_id.to_string()) + .bind(proof.secret.to_string()) + .bind(proof.c.to_bytes().to_vec()) + .bind("UNSPENT") + .execute(&mut transaction) + .await + .map_err(Error::from) + { + tracing::debug!("Attempting to add known proof. Skipping.... {:?}", err); + } + transaction.commit().await.map_err(Error::from)?; + + Ok(()) + } + + async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result>, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + + let sql = format!( + "SELECT y, state FROM proof WHERE y IN ({})", + "?,".repeat(ys.len()).trim_end_matches(',') + ); + + let mut current_states = ys + .iter() + .fold(sqlx::query(&sql), |query, y| { + query.bind(y.to_bytes().to_vec()) + }) + .fetch_all(&mut transaction) + .await + .map_err(|err| { + tracing::error!("SQLite could not get state of proof: {err:?}"); + Error::SQLX(err) + })? + .into_iter() + .map(|row| { + PublicKey::from_slice(row.get("y")) + .map_err(Error::from) + .and_then(|y| { + let state: String = row.get("state"); + State::from_str(&state) + .map_err(Error::from) + .map(|state| (y, state)) + }) + }) + .collect::, _>>()?; + + Ok(ys.iter().map(|y| current_states.remove(y)).collect()) + } + + async fn update_proof_state( + &self, + y: &PublicKey, + proofs_state: State, + ) -> Result, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + + // Get current state for single y + let current_state = sqlx::query("SELECT state FROM proof WHERE y = ?") + .bind(y.to_bytes().to_vec()) + .fetch_optional(&mut transaction) + .await + .map_err(|err| { + tracing::error!("SQLite could not get state of proof: {err:?}"); + Error::SQLX(err) + })? + .map(|row| { + let state: String = row.get("state"); + State::from_str(&state).map_err(Error::from) + }) + .transpose()?; + + // Update state for single y + sqlx::query("UPDATE proof SET state = ? WHERE state != ? AND y = ?") + .bind(proofs_state.to_string()) + .bind(State::Spent.to_string()) + .bind(y.to_bytes().to_vec()) + .execute(&mut transaction) + .await + .map_err(|err| { + tracing::error!("SQLite could not update proof state: {err:?}"); + Error::SQLX(err) + })?; + + transaction.commit().await.map_err(Error::from)?; + Ok(current_state) + } + + async fn add_blind_signatures( + &self, + blinded_messages: &[PublicKey], + blind_signatures: &[BlindSignature], + ) -> Result<(), Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + for (message, signature) in blinded_messages.iter().zip(blind_signatures) { + let res = sqlx::query( + r#" +INSERT INTO blind_signature +(y, amount, keyset_id, c) +VALUES (?, ?, ?, ?, ?, ?); + "#, + ) + .bind(message.to_bytes().to_vec()) + .bind(u64::from(signature.amount) as i64) + .bind(signature.keyset_id.to_string()) + .bind(signature.c.to_bytes().to_vec()) + .execute(&mut transaction) + .await; + + if let Err(err) = res { + tracing::error!("SQLite could not add blind signature"); + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + return Err(Error::SQLX(err).into()); + } + } + + transaction.commit().await.map_err(Error::from)?; + + Ok(()) + } + + async fn get_blind_signatures( + &self, + blinded_messages: &[PublicKey], + ) -> Result>, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + + let sql = format!( + "SELECT * FROM blind_signature WHERE y IN ({})", + "?,".repeat(blinded_messages.len()).trim_end_matches(',') + ); + + let mut blinded_signatures = blinded_messages + .iter() + .fold(sqlx::query(&sql), |query, y| { + query.bind(y.to_bytes().to_vec()) + }) + .fetch_all(&mut transaction) + .await + .map_err(|err| { + tracing::error!("SQLite could not get state of proof: {err:?}"); + Error::SQLX(err) + })? + .into_iter() + .map(|row| { + PublicKey::from_slice(row.get("y")) + .map_err(Error::from) + .and_then(|y| sqlite_row_to_blind_signature(row).map(|blinded| (y, blinded))) + }) + .collect::, _>>()?; + + Ok(blinded_messages + .iter() + .map(|y| blinded_signatures.remove(y)) + .collect()) + } +} diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index 2d8c5623..7711351c 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -26,8 +26,11 @@ use sqlx::Row; use uuid::fmt::Hyphenated; use uuid::Uuid; +mod auth; pub mod error; +pub use auth::MintSqliteAuthDatabase; + /// Mint SQLite Database #[derive(Debug, Clone)] pub struct MintSqliteDatabase { diff --git a/crates/cdk/src/cdk_database/mod.rs b/crates/cdk/src/cdk_database/mod.rs index 48d2299a..f91f2164 100644 --- a/crates/cdk/src/cdk_database/mod.rs +++ b/crates/cdk/src/cdk_database/mod.rs @@ -62,6 +62,9 @@ pub enum Error { /// NUT02 Error #[error(transparent)] NUT02(#[from] crate::nuts::nut02::Error), + /// NUTXX1 Error + #[error(transparent)] + NUTXX1(#[from] crate::nuts::nutxx1::Error), /// Serde Error #[error(transparent)] Serde(#[from] serde_json::Error),