mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 13:44:55 +01:00
Check change unique (#1112)
* fix(cdk): prevent duplicate blinded message processing with database constraints Add unique constraints on blinded_message column in both PostgreSQL and SQLite databases, and implement application-level checks to prevent duplicate blinded messages from being processed. Also ensure proper cleanup of melt requests after successful processing. * feat: db tests for unique * refactor(cdk-sql): consolidate blinded messages into blind signature table Migrate from separate blinded_messages table to unified blind_signature table. Add signed_time column and make c column nullable to track both pending blind messages (c=NULL) and completed signatures. Update insert/update logic to handle upsert scenarios for blind signature completion. * refactor(cdk-sql): remove unique constraint migration and filter queries for signed messages Remove database-level unique constraint on blinded_message and instead filter queries to only consider messages with signatures (c IS NOT NULL * refactor(database): improve blinded message duplicate detection using database constraints Replace manual duplicate checking with database constraint handling for better reliability and simplified code flow in melt request processing. * refactor(cdk-sql): optimize blind signature processing with batch queries Replace individual queries per blinded message with single batch query and HashMap lookup to eliminate N+1 query performance issue. * fix: signed time to swap sigs * refactor(cdk): split blinded message handling and improve duplicate detection - Split add_melt_request_and_blinded_messages into separate methods - Add blinded messages to database before signing in swap operations - Improve duplicate output detection with proper error handling - Make add_blinded_messages method accept optional quote_id for flexibility * refactor(cdk): add BlindedMessageWriter for improved transaction rollback - Add BlindedMessageWriter component for managing blinded message state - Implement proper rollback mechanisms in swap operations - Add delete_blinded_messages database interface for cleanup - Improve error handling with better state management
This commit is contained in:
@@ -6,5 +6,6 @@ extend-ignore-re = [
|
|||||||
"casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9",
|
"casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9",
|
||||||
"autheticator",
|
"autheticator",
|
||||||
"Gam",
|
"Gam",
|
||||||
"flate2"
|
"flate2",
|
||||||
|
"lnbc[A-Za-z0-9-_]+"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -132,15 +132,27 @@ pub trait QuotesTransaction<'a> {
|
|||||||
/// Mint Quotes Database Error
|
/// Mint Quotes Database Error
|
||||||
type Err: Into<Error> + From<Error>;
|
type Err: Into<Error> + From<Error>;
|
||||||
|
|
||||||
/// Add melt_request with quote_id, inputs_amount, and blinded_messages
|
/// Add melt_request with quote_id, inputs_amount, and inputs_fee
|
||||||
async fn add_melt_request_and_blinded_messages(
|
async fn add_melt_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
quote_id: &QuoteId,
|
quote_id: &QuoteId,
|
||||||
inputs_amount: Amount,
|
inputs_amount: Amount,
|
||||||
inputs_fee: Amount,
|
inputs_fee: Amount,
|
||||||
|
) -> Result<(), Self::Err>;
|
||||||
|
|
||||||
|
/// Add blinded_messages for a quote_id
|
||||||
|
async fn add_blinded_messages(
|
||||||
|
&mut self,
|
||||||
|
quote_id: Option<&QuoteId>,
|
||||||
blinded_messages: &[BlindedMessage],
|
blinded_messages: &[BlindedMessage],
|
||||||
) -> Result<(), Self::Err>;
|
) -> Result<(), Self::Err>;
|
||||||
|
|
||||||
|
/// Delete blinded_messages by their blinded secrets
|
||||||
|
async fn delete_blinded_messages(
|
||||||
|
&mut self,
|
||||||
|
blinded_secrets: &[PublicKey],
|
||||||
|
) -> Result<(), Self::Err>;
|
||||||
|
|
||||||
/// Get melt_request and associated blinded_messages by quote_id
|
/// Get melt_request and associated blinded_messages by quote_id
|
||||||
async fn get_melt_request_and_blinded_messages(
|
async fn get_melt_request_and_blinded_messages(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
//! Payments
|
//! Payments
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use cashu::quote_id::QuoteId;
|
||||||
|
use cashu::{Amount, Id, SecretKey};
|
||||||
|
|
||||||
use crate::database::mint::test::unique_string;
|
use crate::database::mint::test::unique_string;
|
||||||
use crate::database::mint::{Database, Error, KeysDatabase};
|
use crate::database::mint::{Database, Error, KeysDatabase};
|
||||||
use crate::mint::MintQuote;
|
use crate::database::MintSignaturesDatabase;
|
||||||
|
use crate::mint::{MeltPaymentRequest, MeltQuote, MintQuote};
|
||||||
use crate::payment::PaymentIdentifier;
|
use crate::payment::PaymentIdentifier;
|
||||||
|
|
||||||
/// Add a mint quote
|
/// Add a mint quote
|
||||||
@@ -404,3 +410,193 @@ where
|
|||||||
.await
|
.await
|
||||||
.is_err());
|
.is_err());
|
||||||
}
|
}
|
||||||
|
/// Successful melt with unique blinded messages
|
||||||
|
pub async fn add_melt_request_unique_blinded_messages<DB>(db: DB)
|
||||||
|
where
|
||||||
|
DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
|
||||||
|
{
|
||||||
|
let inputs_amount = Amount::from(100u64);
|
||||||
|
let inputs_fee = Amount::from(1u64);
|
||||||
|
let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
|
||||||
|
|
||||||
|
// Create a dummy blinded message
|
||||||
|
let blinded_secret = SecretKey::generate().public_key();
|
||||||
|
let blinded_message = cashu::BlindedMessage {
|
||||||
|
blinded_secret,
|
||||||
|
keyset_id,
|
||||||
|
amount: Amount::from(100u64),
|
||||||
|
witness: None,
|
||||||
|
};
|
||||||
|
let blinded_messages = vec![blinded_message];
|
||||||
|
|
||||||
|
let mut tx = Database::begin_transaction(&db).await.unwrap();
|
||||||
|
let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
|
||||||
|
tx.add_melt_quote(quote.clone()).await.unwrap();
|
||||||
|
tx.add_melt_request("e.id, inputs_amount, inputs_fee)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.add_blinded_messages(Some("e.id), &blinded_messages)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.commit().await.unwrap();
|
||||||
|
|
||||||
|
// Verify retrieval
|
||||||
|
let mut tx = Database::begin_transaction(&db).await.unwrap();
|
||||||
|
let retrieved = tx
|
||||||
|
.get_melt_request_and_blinded_messages("e.id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(retrieved.inputs_amount, inputs_amount);
|
||||||
|
assert_eq!(retrieved.inputs_fee, inputs_fee);
|
||||||
|
assert_eq!(retrieved.change_outputs.len(), 1);
|
||||||
|
assert_eq!(retrieved.change_outputs[0].amount, Amount::from(100u64));
|
||||||
|
tx.commit().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reject melt with duplicate blinded message (already signed)
|
||||||
|
pub async fn reject_melt_duplicate_blinded_signature<DB>(db: DB)
|
||||||
|
where
|
||||||
|
DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
|
||||||
|
{
|
||||||
|
let quote_id1 = QuoteId::new_uuid();
|
||||||
|
let inputs_amount = Amount::from(100u64);
|
||||||
|
let inputs_fee = Amount::from(1u64);
|
||||||
|
let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
|
||||||
|
|
||||||
|
// Create a dummy blinded message
|
||||||
|
let blinded_secret = SecretKey::generate().public_key();
|
||||||
|
let blinded_message = cashu::BlindedMessage {
|
||||||
|
blinded_secret,
|
||||||
|
keyset_id,
|
||||||
|
amount: Amount::from(100u64),
|
||||||
|
witness: None,
|
||||||
|
};
|
||||||
|
let blinded_messages = vec![blinded_message.clone()];
|
||||||
|
|
||||||
|
// First, "sign" it by adding to blind_signature (simulate successful mint)
|
||||||
|
let mut tx = Database::begin_transaction(&db).await.unwrap();
|
||||||
|
let c = SecretKey::generate().public_key();
|
||||||
|
let blind_sig = cashu::BlindSignature {
|
||||||
|
amount: Amount::from(100u64),
|
||||||
|
keyset_id,
|
||||||
|
c,
|
||||||
|
dleq: None,
|
||||||
|
};
|
||||||
|
let blinded_secrets = vec![blinded_message.blinded_secret];
|
||||||
|
tx.add_blind_signatures(&blinded_secrets, &[blind_sig], Some(quote_id1))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.commit().await.unwrap();
|
||||||
|
|
||||||
|
// Now try to add melt request with the same blinded message - should fail due to constraint
|
||||||
|
let mut tx = Database::begin_transaction(&db).await.unwrap();
|
||||||
|
let quote2 = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
|
||||||
|
tx.add_melt_quote(quote2.clone()).await.unwrap();
|
||||||
|
tx.add_melt_request("e2.id, inputs_amount, inputs_fee)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let result = tx
|
||||||
|
.add_blinded_messages(Some("e2.id), &blinded_messages)
|
||||||
|
.await;
|
||||||
|
assert!(result.is_err() && matches!(result.unwrap_err(), Error::Duplicate));
|
||||||
|
tx.rollback().await.unwrap(); // Rollback to avoid partial state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reject duplicate blinded message insert via DB constraint (different quotes)
|
||||||
|
pub async fn reject_duplicate_blinded_message_db_constraint<DB>(db: DB)
|
||||||
|
where
|
||||||
|
DB: Database<Error> + KeysDatabase<Err = Error>,
|
||||||
|
{
|
||||||
|
let inputs_amount = Amount::from(100u64);
|
||||||
|
let inputs_fee = Amount::from(1u64);
|
||||||
|
let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
|
||||||
|
|
||||||
|
// Create a dummy blinded message
|
||||||
|
let blinded_secret = SecretKey::generate().public_key();
|
||||||
|
let blinded_message = cashu::BlindedMessage {
|
||||||
|
blinded_secret,
|
||||||
|
keyset_id,
|
||||||
|
amount: Amount::from(100u64),
|
||||||
|
witness: None,
|
||||||
|
};
|
||||||
|
let blinded_messages = vec![blinded_message];
|
||||||
|
|
||||||
|
// First insert succeeds
|
||||||
|
let mut tx = Database::begin_transaction(&db).await.unwrap();
|
||||||
|
let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
|
||||||
|
tx.add_melt_quote(quote.clone()).await.unwrap();
|
||||||
|
tx.add_melt_request("e.id, inputs_amount, inputs_fee)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(tx
|
||||||
|
.add_blinded_messages(Some("e.id), &blinded_messages)
|
||||||
|
.await
|
||||||
|
.is_ok());
|
||||||
|
tx.commit().await.unwrap();
|
||||||
|
|
||||||
|
// Second insert with same blinded_message but different quote_id should fail due to unique constraint on blinded_message
|
||||||
|
let mut tx = Database::begin_transaction(&db).await.unwrap();
|
||||||
|
let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
|
||||||
|
tx.add_melt_quote(quote.clone()).await.unwrap();
|
||||||
|
tx.add_melt_request("e.id, inputs_amount, inputs_fee)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let result = tx
|
||||||
|
.add_blinded_messages(Some("e.id), &blinded_messages)
|
||||||
|
.await;
|
||||||
|
// Expect a database error due to unique violation
|
||||||
|
assert!(result.is_err()); // Specific error might be DB-specific, e.g., SqliteError or PostgresError
|
||||||
|
tx.rollback().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cleanup of melt request after processing
|
||||||
|
pub async fn cleanup_melt_request_after_processing<DB>(db: DB)
|
||||||
|
where
|
||||||
|
DB: Database<Error> + KeysDatabase<Err = Error>,
|
||||||
|
{
|
||||||
|
let inputs_amount = Amount::from(100u64);
|
||||||
|
let inputs_fee = Amount::from(1u64);
|
||||||
|
let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
|
||||||
|
|
||||||
|
// Create dummy blinded message
|
||||||
|
let blinded_secret = SecretKey::generate().public_key();
|
||||||
|
let blinded_message = cashu::BlindedMessage {
|
||||||
|
blinded_secret,
|
||||||
|
keyset_id,
|
||||||
|
amount: Amount::from(100u64),
|
||||||
|
witness: None,
|
||||||
|
};
|
||||||
|
let blinded_messages = vec![blinded_message];
|
||||||
|
|
||||||
|
// Insert melt request
|
||||||
|
let mut tx1 = Database::begin_transaction(&db).await.unwrap();
|
||||||
|
let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Bolt11);
|
||||||
|
tx1.add_melt_quote(quote.clone()).await.unwrap();
|
||||||
|
tx1.add_melt_request("e.id, inputs_amount, inputs_fee)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx1.add_blinded_messages(Some("e.id), &blinded_messages)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx1.commit().await.unwrap();
|
||||||
|
|
||||||
|
// Simulate processing: get and delete
|
||||||
|
let mut tx2 = Database::begin_transaction(&db).await.unwrap();
|
||||||
|
let _retrieved = tx2
|
||||||
|
.get_melt_request_and_blinded_messages("e.id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
tx2.delete_melt_request("e.id).await.unwrap();
|
||||||
|
tx2.commit().await.unwrap();
|
||||||
|
|
||||||
|
// Verify melt_request is deleted
|
||||||
|
let mut tx3 = Database::begin_transaction(&db).await.unwrap();
|
||||||
|
let retrieved = tx3
|
||||||
|
.get_melt_request_and_blinded_messages("e.id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(retrieved.is_none());
|
||||||
|
tx3.commit().await.unwrap();
|
||||||
|
}
|
||||||
|
|||||||
@@ -235,7 +235,11 @@ macro_rules! mint_db_test {
|
|||||||
reject_over_issue_same_tx,
|
reject_over_issue_same_tx,
|
||||||
reject_over_issue_different_tx,
|
reject_over_issue_different_tx,
|
||||||
reject_over_issue_with_payment,
|
reject_over_issue_with_payment,
|
||||||
reject_over_issue_with_payment_different_tx
|
reject_over_issue_with_payment_different_tx,
|
||||||
|
add_melt_request_unique_blinded_messages,
|
||||||
|
reject_melt_duplicate_blinded_signature,
|
||||||
|
reject_duplicate_blinded_message_db_constraint,
|
||||||
|
cleanup_melt_request_after_processing
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
($make_db_fn:ident, $($name:ident),+ $(,)?) => {
|
($make_db_fn:ident, $($name:ident),+ $(,)?) => {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- Remove NOT NULL constraint from c column in blind_signature table
|
||||||
|
ALTER TABLE blind_signature ALTER COLUMN c DROP NOT NULL;
|
||||||
|
|
||||||
|
-- Add signed_time column to blind_signature table
|
||||||
|
ALTER TABLE blind_signature ADD COLUMN signed_time INTEGER NULL;
|
||||||
|
|
||||||
|
-- Update existing records to set signed_time equal to created_time for existing signatures
|
||||||
|
UPDATE blind_signature SET signed_time = created_time WHERE c IS NOT NULL;
|
||||||
|
|
||||||
|
-- Insert data from blinded_messages table into blind_signature table with NULL c column
|
||||||
|
INSERT INTO blind_signature (blinded_message, amount, keyset_id, c, quote_id, created_time, signed_time)
|
||||||
|
SELECT blinded_message, amount, keyset_id, NULL as c, quote_id, 0 as created_time, NULL as signed_time
|
||||||
|
FROM blinded_messages
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM blind_signature
|
||||||
|
WHERE blind_signature.blinded_message = blinded_messages.blinded_message
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create index on quote_id if it does not exist
|
||||||
|
CREATE INDEX IF NOT EXISTS blind_signature_quote_id_index ON blind_signature(quote_id);
|
||||||
|
|
||||||
|
-- Drop the blinded_messages table as data has been migrated
|
||||||
|
DROP TABLE IF EXISTS blinded_messages;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- Remove NOT NULL constraint from c column in blind_signature table
|
||||||
|
-- SQLite does not support ALTER COLUMN directly, so we need to recreate the table
|
||||||
|
|
||||||
|
-- Step 1 - Create new table with nullable c column and signed_time column
|
||||||
|
CREATE TABLE blind_signature_new (
|
||||||
|
blinded_message BLOB PRIMARY KEY,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
keyset_id TEXT NOT NULL,
|
||||||
|
c BLOB NULL,
|
||||||
|
dleq_e TEXT,
|
||||||
|
dleq_s TEXT,
|
||||||
|
quote_id TEXT,
|
||||||
|
created_time INTEGER NOT NULL DEFAULT 0,
|
||||||
|
signed_time INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Step 2 - Copy existing data from old blind_signature table
|
||||||
|
INSERT INTO blind_signature_new (blinded_message, amount, keyset_id, c, dleq_e, dleq_s, quote_id, created_time)
|
||||||
|
SELECT blinded_message, amount, keyset_id, c, dleq_e, dleq_s, quote_id, created_time
|
||||||
|
FROM blind_signature;
|
||||||
|
|
||||||
|
-- Step 3 - Insert data from blinded_messages table with NULL c column
|
||||||
|
INSERT INTO blind_signature_new (blinded_message, amount, keyset_id, c, quote_id, created_time)
|
||||||
|
SELECT blinded_message, amount, keyset_id, NULL as c, quote_id, 0 as created_time
|
||||||
|
FROM blinded_messages
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM blind_signature_new
|
||||||
|
WHERE blind_signature_new.blinded_message = blinded_messages.blinded_message
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Step 4 - Drop old table and rename new table
|
||||||
|
DROP TABLE blind_signature;
|
||||||
|
ALTER TABLE blind_signature_new RENAME TO blind_signature;
|
||||||
|
|
||||||
|
-- Step 5 - Recreate indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS keyset_id_index ON blind_signature(keyset_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS blind_signature_quote_id_index ON blind_signature(quote_id);
|
||||||
|
|
||||||
|
-- Step 6 - Drop the blinded_messages table as data has been migrated
|
||||||
|
DROP TABLE IF EXISTS blinded_messages;
|
||||||
@@ -546,13 +546,13 @@ where
|
|||||||
{
|
{
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
async fn add_melt_request_and_blinded_messages(
|
async fn add_melt_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
quote_id: &QuoteId,
|
quote_id: &QuoteId,
|
||||||
inputs_amount: Amount,
|
inputs_amount: Amount,
|
||||||
inputs_fee: Amount,
|
inputs_fee: Amount,
|
||||||
blinded_messages: &[BlindedMessage],
|
|
||||||
) -> Result<(), Self::Err> {
|
) -> Result<(), Self::Err> {
|
||||||
|
// Insert melt_request
|
||||||
query(
|
query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO melt_request
|
INSERT INTO melt_request
|
||||||
@@ -567,25 +567,78 @@ where
|
|||||||
.execute(&self.inner)
|
.execute(&self.inner)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_blinded_messages(
|
||||||
|
&mut self,
|
||||||
|
quote_id: Option<&QuoteId>,
|
||||||
|
blinded_messages: &[BlindedMessage],
|
||||||
|
) -> Result<(), Self::Err> {
|
||||||
|
let current_time = unix_time();
|
||||||
|
|
||||||
|
// Insert blinded_messages directly into blind_signature with c = NULL
|
||||||
|
// Let the database constraint handle duplicate detection
|
||||||
for message in blinded_messages {
|
for message in blinded_messages {
|
||||||
query(
|
match query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO blinded_messages
|
INSERT INTO blind_signature
|
||||||
(quote_id, blinded_message, keyset_id, amount)
|
(blinded_message, amount, keyset_id, c, quote_id, created_time)
|
||||||
VALUES
|
VALUES
|
||||||
(:quote_id, :blinded_message, :keyset_id, :amount)
|
(:blinded_message, :amount, :keyset_id, NULL, :quote_id, :created_time)
|
||||||
"#,
|
"#,
|
||||||
)?
|
)?
|
||||||
.bind("quote_id", quote_id.to_string())
|
|
||||||
.bind(
|
.bind(
|
||||||
"blinded_message",
|
"blinded_message",
|
||||||
message.blinded_secret.to_bytes().to_vec(),
|
message.blinded_secret.to_bytes().to_vec(),
|
||||||
)
|
)
|
||||||
.bind("keyset_id", message.keyset_id.to_string())
|
|
||||||
.bind("amount", message.amount.to_i64())
|
.bind("amount", message.amount.to_i64())
|
||||||
|
.bind("keyset_id", message.keyset_id.to_string())
|
||||||
|
.bind("quote_id", quote_id.map(|q| q.to_string()))
|
||||||
|
.bind("created_time", current_time as i64)
|
||||||
|
.execute(&self.inner)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => continue,
|
||||||
|
Err(database::Error::Duplicate) => {
|
||||||
|
// Primary key constraint violation - blinded message already exists
|
||||||
|
// This could be either:
|
||||||
|
// 1. Already signed (c IS NOT NULL) - definitely an error
|
||||||
|
// 2. Already pending (c IS NULL) - also an error
|
||||||
|
return Err(database::Error::Duplicate);
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_blinded_messages(
|
||||||
|
&mut self,
|
||||||
|
blinded_secrets: &[PublicKey],
|
||||||
|
) -> Result<(), Self::Err> {
|
||||||
|
if blinded_secrets.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete blinded messages from blind_signature table where c IS NULL
|
||||||
|
// (only delete unsigned blinded messages)
|
||||||
|
query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM blind_signature
|
||||||
|
WHERE blinded_message IN (:blinded_secrets) AND c IS NULL
|
||||||
|
"#,
|
||||||
|
)?
|
||||||
|
.bind_vec(
|
||||||
|
"blinded_secrets",
|
||||||
|
blinded_secrets
|
||||||
|
.iter()
|
||||||
|
.map(|secret| secret.to_bytes().to_vec())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
.execute(&self.inner)
|
.execute(&self.inner)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -610,11 +663,12 @@ where
|
|||||||
let inputs_amount: u64 = column_as_number!(row[0].clone());
|
let inputs_amount: u64 = column_as_number!(row[0].clone());
|
||||||
let inputs_fee: u64 = column_as_number!(row[1].clone());
|
let inputs_fee: u64 = column_as_number!(row[1].clone());
|
||||||
|
|
||||||
|
// Get blinded messages from blind_signature table where c IS NULL
|
||||||
let blinded_messages_rows = query(
|
let blinded_messages_rows = query(
|
||||||
r#"
|
r#"
|
||||||
SELECT blinded_message, keyset_id, amount
|
SELECT blinded_message, keyset_id, amount
|
||||||
FROM blinded_messages
|
FROM blind_signature
|
||||||
WHERE quote_id = :quote_id
|
WHERE quote_id = :quote_id AND c IS NULL
|
||||||
"#,
|
"#,
|
||||||
)?
|
)?
|
||||||
.bind("quote_id", quote_id.to_string())
|
.bind("quote_id", quote_id.to_string())
|
||||||
@@ -650,6 +704,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_melt_request(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> {
|
async fn delete_melt_request(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> {
|
||||||
|
// Delete from melt_request table
|
||||||
query(
|
query(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM melt_request
|
DELETE FROM melt_request
|
||||||
@@ -660,6 +715,17 @@ where
|
|||||||
.execute(&self.inner)
|
.execute(&self.inner)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Also delete blinded messages (where c IS NULL) from blind_signature table
|
||||||
|
query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM blind_signature
|
||||||
|
WHERE quote_id = :quote_id AND c IS NULL
|
||||||
|
"#,
|
||||||
|
)?
|
||||||
|
.bind("quote_id", quote_id.to_string())
|
||||||
|
.execute(&self.inner)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1560,23 +1626,57 @@ where
|
|||||||
) -> Result<(), Self::Err> {
|
) -> Result<(), Self::Err> {
|
||||||
let current_time = unix_time();
|
let current_time = unix_time();
|
||||||
|
|
||||||
|
if blinded_messages.len() != blind_signatures.len() {
|
||||||
|
return Err(database::Error::Internal(
|
||||||
|
"Mismatched array lengths for blinded messages and blind signatures".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select all existing rows for the given blinded messages at once
|
||||||
|
let mut existing_rows = query(
|
||||||
|
r#"
|
||||||
|
SELECT blinded_message, c, dleq_e, dleq_s
|
||||||
|
FROM blind_signature
|
||||||
|
WHERE blinded_message IN (:blinded_messages)
|
||||||
|
FOR UPDATE
|
||||||
|
"#,
|
||||||
|
)?
|
||||||
|
.bind_vec(
|
||||||
|
"blinded_messages",
|
||||||
|
blinded_messages
|
||||||
|
.iter()
|
||||||
|
.map(|message| message.to_bytes().to_vec())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
.fetch_all(&self.inner)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut row| {
|
||||||
|
Ok((
|
||||||
|
column_as_string!(&row.remove(0), PublicKey::from_hex, PublicKey::from_slice),
|
||||||
|
(row[0].clone(), row[1].clone(), row[2].clone()),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Result<HashMap<_, _>, Error>>()?;
|
||||||
|
|
||||||
|
// Iterate over the provided blinded messages and signatures
|
||||||
for (message, signature) in blinded_messages.iter().zip(blind_signatures) {
|
for (message, signature) in blinded_messages.iter().zip(blind_signatures) {
|
||||||
|
match existing_rows.remove(message) {
|
||||||
|
None => {
|
||||||
|
// Unknown blind message: Insert new row with all columns
|
||||||
query(
|
query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO blind_signature
|
INSERT INTO blind_signature
|
||||||
(blinded_message, amount, keyset_id, c, quote_id, dleq_e, dleq_s, created_time)
|
(blinded_message, amount, keyset_id, c, quote_id, dleq_e, dleq_s, created_time, signed_time)
|
||||||
VALUES
|
VALUES
|
||||||
(:blinded_message, :amount, :keyset_id, :c, :quote_id, :dleq_e, :dleq_s, :created_time)
|
(:blinded_message, :amount, :keyset_id, :c, :quote_id, :dleq_e, :dleq_s, :created_time, :signed_time)
|
||||||
"#,
|
"#,
|
||||||
)?
|
)?
|
||||||
.bind("blinded_message", message.to_bytes().to_vec())
|
.bind("blinded_message", message.to_bytes().to_vec())
|
||||||
.bind("amount", u64::from(signature.amount) as i64)
|
.bind("amount", u64::from(signature.amount) as i64)
|
||||||
.bind("keyset_id", signature.keyset_id.to_string())
|
.bind("keyset_id", signature.keyset_id.to_string())
|
||||||
.bind("c", signature.c.to_bytes().to_vec())
|
.bind("c", signature.c.to_bytes().to_vec())
|
||||||
.bind("quote_id", quote_id.as_ref().map(|q| match q {
|
.bind("quote_id", quote_id.as_ref().map(|q| q.to_string()))
|
||||||
QuoteId::BASE64(s) => s.to_string(),
|
|
||||||
QuoteId::UUID(u) => u.hyphenated().to_string(),
|
|
||||||
}))
|
|
||||||
.bind(
|
.bind(
|
||||||
"dleq_e",
|
"dleq_e",
|
||||||
signature.dleq.as_ref().map(|dleq| dleq.e.to_secret_hex()),
|
signature.dleq.as_ref().map(|dleq| dleq.e.to_secret_hex()),
|
||||||
@@ -1586,9 +1686,63 @@ where
|
|||||||
signature.dleq.as_ref().map(|dleq| dleq.s.to_secret_hex()),
|
signature.dleq.as_ref().map(|dleq| dleq.s.to_secret_hex()),
|
||||||
)
|
)
|
||||||
.bind("created_time", current_time as i64)
|
.bind("created_time", current_time as i64)
|
||||||
|
.bind("signed_time", current_time as i64)
|
||||||
.execute(&self.inner)
|
.execute(&self.inner)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
Some((c, _dleq_e, _dleq_s)) => {
|
||||||
|
// Blind message exists: check if c is NULL
|
||||||
|
match c {
|
||||||
|
Column::Null => {
|
||||||
|
// Blind message with no c: Update with missing columns c, dleq_e, dleq_s
|
||||||
|
query(
|
||||||
|
r#"
|
||||||
|
UPDATE blind_signature
|
||||||
|
SET c = :c, dleq_e = :dleq_e, dleq_s = :dleq_s, signed_time = :signed_time, amount = :amount
|
||||||
|
WHERE blinded_message = :blinded_message
|
||||||
|
"#,
|
||||||
|
)?
|
||||||
|
.bind("c", signature.c.to_bytes().to_vec())
|
||||||
|
.bind(
|
||||||
|
"dleq_e",
|
||||||
|
signature.dleq.as_ref().map(|dleq| dleq.e.to_secret_hex()),
|
||||||
|
)
|
||||||
|
.bind(
|
||||||
|
"dleq_s",
|
||||||
|
signature.dleq.as_ref().map(|dleq| dleq.s.to_secret_hex()),
|
||||||
|
)
|
||||||
|
.bind("blinded_message", message.to_bytes().to_vec())
|
||||||
|
.bind("signed_time", current_time as i64)
|
||||||
|
.bind("amount", u64::from(signature.amount) as i64)
|
||||||
|
.execute(&self.inner)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Blind message already has c: Error
|
||||||
|
tracing::error!(
|
||||||
|
"Attempting to add signature to message already signed {}",
|
||||||
|
message
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(database::Error::Duplicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug_assert!(
|
||||||
|
existing_rows.is_empty(),
|
||||||
|
"Unexpected existing rows remain: {:?}",
|
||||||
|
existing_rows.keys().collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
if !existing_rows.is_empty() {
|
||||||
|
tracing::error!("Did not check all existing rows");
|
||||||
|
return Err(Error::Internal(
|
||||||
|
"Did not check all existing rows".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1607,14 +1761,14 @@ where
|
|||||||
blinded_message
|
blinded_message
|
||||||
FROM
|
FROM
|
||||||
blind_signature
|
blind_signature
|
||||||
WHERE blinded_message IN (:y)
|
WHERE blinded_message IN (:b) AND c IS NOT NULL
|
||||||
"#,
|
"#,
|
||||||
)?
|
)?
|
||||||
.bind_vec(
|
.bind_vec(
|
||||||
"y",
|
"b",
|
||||||
blinded_messages
|
blinded_messages
|
||||||
.iter()
|
.iter()
|
||||||
.map(|y| y.to_bytes().to_vec())
|
.map(|b| b.to_bytes().to_vec())
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
.fetch_all(&self.inner)
|
.fetch_all(&self.inner)
|
||||||
@@ -1660,11 +1814,11 @@ where
|
|||||||
blinded_message
|
blinded_message
|
||||||
FROM
|
FROM
|
||||||
blind_signature
|
blind_signature
|
||||||
WHERE blinded_message IN (:blinded_message)
|
WHERE blinded_message IN (:b) AND c IS NOT NULL
|
||||||
"#,
|
"#,
|
||||||
)?
|
)?
|
||||||
.bind_vec(
|
.bind_vec(
|
||||||
"blinded_message",
|
"b",
|
||||||
blinded_messages
|
blinded_messages
|
||||||
.iter()
|
.iter()
|
||||||
.map(|b_| b_.to_bytes().to_vec())
|
.map(|b_| b_.to_bytes().to_vec())
|
||||||
@@ -1706,7 +1860,7 @@ where
|
|||||||
FROM
|
FROM
|
||||||
blind_signature
|
blind_signature
|
||||||
WHERE
|
WHERE
|
||||||
keyset_id=:keyset_id
|
keyset_id=:keyset_id AND c IS NOT NULL
|
||||||
"#,
|
"#,
|
||||||
)?
|
)?
|
||||||
.bind("keyset_id", keyset_id.to_string())
|
.bind("keyset_id", keyset_id.to_string())
|
||||||
@@ -1734,7 +1888,7 @@ where
|
|||||||
FROM
|
FROM
|
||||||
blind_signature
|
blind_signature
|
||||||
WHERE
|
WHERE
|
||||||
quote_id=:quote_id
|
quote_id=:quote_id AND c IS NOT NULL
|
||||||
"#,
|
"#,
|
||||||
)?
|
)?
|
||||||
.bind("quote_id", quote_id.to_string())
|
.bind("quote_id", quote_id.to_string())
|
||||||
|
|||||||
152
crates/cdk/src/mint/blinded_message_writer.rs
Normal file
152
crates/cdk/src/mint/blinded_message_writer.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
//! Blinded message writer
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use cdk_common::database::{self, DynMintDatabase, MintTransaction};
|
||||||
|
use cdk_common::nuts::BlindedMessage;
|
||||||
|
use cdk_common::{Error, PublicKey, QuoteId};
|
||||||
|
|
||||||
|
type Tx<'a, 'b> = Box<dyn MintTransaction<'a, database::Error> + Send + Sync + 'b>;
|
||||||
|
|
||||||
|
/// Blinded message writer
|
||||||
|
///
|
||||||
|
/// This is a blinded message writer that emulates a database transaction but without holding the
|
||||||
|
/// transaction alive while waiting for external events to be fully committed to the database;
|
||||||
|
/// instead, it maintains a `pending` state.
|
||||||
|
///
|
||||||
|
/// This struct allows for premature exit on error, enabling it to remove blinded messages that
|
||||||
|
/// were added during the operation.
|
||||||
|
///
|
||||||
|
/// This struct is not fully ACID. If the process exits due to a panic, and the `Drop` function
|
||||||
|
/// cannot be run, the cleanup process should reset the state.
|
||||||
|
pub struct BlindedMessageWriter {
|
||||||
|
db: Option<DynMintDatabase>,
|
||||||
|
added_blinded_secrets: Option<HashSet<PublicKey>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlindedMessageWriter {
|
||||||
|
/// Creates a new BlindedMessageWriter on top of the database
|
||||||
|
pub fn new(db: DynMintDatabase) -> Self {
|
||||||
|
Self {
|
||||||
|
db: Some(db),
|
||||||
|
added_blinded_secrets: Some(Default::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The changes are permanent, consume the struct removing the database, so the Drop does
|
||||||
|
/// nothing
|
||||||
|
pub fn commit(mut self) {
|
||||||
|
self.db.take();
|
||||||
|
self.added_blinded_secrets.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add blinded messages
|
||||||
|
pub async fn add_blinded_messages(
|
||||||
|
&mut self,
|
||||||
|
tx: &mut Tx<'_, '_>,
|
||||||
|
quote_id: Option<QuoteId>,
|
||||||
|
blinded_messages: &[BlindedMessage],
|
||||||
|
) -> Result<Vec<PublicKey>, Error> {
|
||||||
|
let added_secrets = if let Some(secrets) = self.added_blinded_secrets.as_mut() {
|
||||||
|
secrets
|
||||||
|
} else {
|
||||||
|
return Err(Error::Internal);
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(err) = tx
|
||||||
|
.add_blinded_messages(quote_id.as_ref(), blinded_messages)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
{
|
||||||
|
return match err {
|
||||||
|
cdk_common::database::Error::Duplicate => Err(Error::DuplicateOutputs),
|
||||||
|
err => Err(Error::Database(err)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let blinded_secrets: Vec<PublicKey> = blinded_messages
|
||||||
|
.iter()
|
||||||
|
.map(|bm| bm.blinded_secret)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for blinded_secret in &blinded_secrets {
|
||||||
|
added_secrets.insert(*blinded_secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(blinded_secrets)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rollback all changes in this BlindedMessageWriter consuming it.
|
||||||
|
pub async fn rollback(mut self) -> Result<(), Error> {
|
||||||
|
let db = if let Some(db) = self.db.take() {
|
||||||
|
db
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let mut tx = db.begin_transaction().await?;
|
||||||
|
let blinded_secrets: Vec<PublicKey> =
|
||||||
|
if let Some(secrets) = self.added_blinded_secrets.take() {
|
||||||
|
secrets.into_iter().collect()
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if !blinded_secrets.is_empty() {
|
||||||
|
tracing::info!("Rollback {} blinded messages", blinded_secrets.len(),);
|
||||||
|
|
||||||
|
remove_blinded_messages(&mut tx, &blinded_secrets).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes blinded messages from the database
|
||||||
|
#[inline(always)]
|
||||||
|
async fn remove_blinded_messages(
|
||||||
|
tx: &mut Tx<'_, '_>,
|
||||||
|
blinded_secrets: &[PublicKey],
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
tx.delete_blinded_messages(blinded_secrets)
|
||||||
|
.await
|
||||||
|
.map_err(Error::Database)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
async fn rollback_blinded_messages(
|
||||||
|
db: DynMintDatabase,
|
||||||
|
blinded_secrets: Vec<PublicKey>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut tx = db.begin_transaction().await?;
|
||||||
|
remove_blinded_messages(&mut tx, &blinded_secrets).await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BlindedMessageWriter {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let db = if let Some(db) = self.db.take() {
|
||||||
|
db
|
||||||
|
} else {
|
||||||
|
tracing::debug!("Blinded message writer dropped after commit, no need to rollback.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let blinded_secrets: Vec<PublicKey> =
|
||||||
|
if let Some(secrets) = self.added_blinded_secrets.take() {
|
||||||
|
secrets.into_iter().collect()
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !blinded_secrets.is_empty() {
|
||||||
|
tracing::debug!("Blinded message writer dropper with messages attempting to remove.");
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = rollback_blinded_messages(db, blinded_secrets).await {
|
||||||
|
tracing::error!("Failed to rollback blinded messages in Drop: {}", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -636,10 +636,15 @@ impl Mint {
|
|||||||
|
|
||||||
let inputs_fee = self.get_proofs_fee(melt_request.inputs()).await?;
|
let inputs_fee = self.get_proofs_fee(melt_request.inputs()).await?;
|
||||||
|
|
||||||
tx.add_melt_request_and_blinded_messages(
|
tx.add_melt_request(
|
||||||
melt_request.quote_id(),
|
melt_request.quote_id(),
|
||||||
melt_request.inputs_amount()?,
|
melt_request.inputs_amount()?,
|
||||||
inputs_fee,
|
inputs_fee,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.add_blinded_messages(
|
||||||
|
Some(melt_request.quote_id()),
|
||||||
melt_request.outputs().as_ref().unwrap_or(&Vec::new()),
|
melt_request.outputs().as_ref().unwrap_or(&Vec::new()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -997,6 +1002,9 @@ impl Mint {
|
|||||||
change = Some(change_sigs);
|
change = Some(change_sigs);
|
||||||
|
|
||||||
proof_writer.commit();
|
proof_writer.commit();
|
||||||
|
|
||||||
|
tx.delete_melt_request("e.id).await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
} else {
|
} else {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -1006,11 +1014,13 @@ impl Mint {
|
|||||||
total_spent
|
total_spent
|
||||||
);
|
);
|
||||||
proof_writer.commit();
|
proof_writer.commit();
|
||||||
|
tx.delete_melt_request("e.id).await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("No change required for melt {}", quote.id);
|
tracing::debug!("No change required for melt {}", quote.id);
|
||||||
proof_writer.commit();
|
proof_writer.commit();
|
||||||
|
tx.delete_melt_request("e.id).await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ use crate::{cdk_database, Amount};
|
|||||||
|
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
pub(crate) mod auth;
|
pub(crate) mod auth;
|
||||||
|
mod blinded_message_writer;
|
||||||
mod builder;
|
mod builder;
|
||||||
mod check_spendable;
|
mod check_spendable;
|
||||||
mod issue;
|
mod issue;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
use cdk_prometheus::METRICS;
|
use cdk_prometheus::METRICS;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use super::blinded_message_writer::BlindedMessageWriter;
|
||||||
use super::nut11::{enforce_sig_flag, EnforceSigFlag};
|
use super::nut11::{enforce_sig_flag, EnforceSigFlag};
|
||||||
use super::proof_writer::ProofWriter;
|
use super::proof_writer::ProofWriter;
|
||||||
use super::{Mint, PublicKey, SigFlag, State, SwapRequest, SwapResponse};
|
use super::{Mint, PublicKey, SigFlag, State, SwapRequest, SwapResponse};
|
||||||
@@ -21,11 +22,41 @@ impl Mint {
|
|||||||
swap_request.input_amount()?;
|
swap_request.input_amount()?;
|
||||||
swap_request.output_amount()?;
|
swap_request.output_amount()?;
|
||||||
|
|
||||||
|
// We add blinded messages to db before attempting to sign
|
||||||
|
// this ensures that they are unique and have not been used before
|
||||||
|
let mut blinded_message_writer = BlindedMessageWriter::new(self.localstore.clone());
|
||||||
|
let mut tx = self.localstore.begin_transaction().await?;
|
||||||
|
|
||||||
|
match blinded_message_writer
|
||||||
|
.add_blinded_messages(&mut tx, None, swap_request.outputs())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
tx.commit().await?;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
#[cfg(feature = "prometheus")]
|
||||||
|
{
|
||||||
|
METRICS.dec_in_flight_requests("process_swap_request");
|
||||||
|
METRICS.record_mint_operation("process_swap_request", false);
|
||||||
|
METRICS.record_error();
|
||||||
|
}
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let promises = self.blind_sign(swap_request.outputs().to_owned()).await?;
|
let promises = self.blind_sign(swap_request.outputs().to_owned()).await?;
|
||||||
let input_verification =
|
let input_verification =
|
||||||
self.verify_inputs(swap_request.inputs())
|
self.verify_inputs(swap_request.inputs())
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
|
#[cfg(feature = "prometheus")]
|
||||||
|
{
|
||||||
|
METRICS.dec_in_flight_requests("process_swap_request");
|
||||||
|
METRICS.record_mint_operation("process_swap_request", false);
|
||||||
|
METRICS.record_error();
|
||||||
|
}
|
||||||
|
|
||||||
tracing::debug!("Input verification failed: {:?}", err);
|
tracing::debug!("Input verification failed: {:?}", err);
|
||||||
err
|
err
|
||||||
})?;
|
})?;
|
||||||
@@ -49,14 +80,21 @@ impl Mint {
|
|||||||
METRICS.record_error();
|
METRICS.record_error();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tx.rollback().await?;
|
||||||
|
blinded_message_writer.rollback().await?;
|
||||||
|
|
||||||
return Err(err);
|
return Err(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
let validate_sig_result = self.validate_sig_flag(&swap_request).await;
|
let validate_sig_result = self.validate_sig_flag(&swap_request).await;
|
||||||
if validate_sig_result.is_err() {
|
|
||||||
|
if let Err(err) = validate_sig_result {
|
||||||
|
tx.rollback().await?;
|
||||||
|
blinded_message_writer.rollback().await?;
|
||||||
|
|
||||||
#[cfg(feature = "prometheus")]
|
#[cfg(feature = "prometheus")]
|
||||||
self.record_swap_failure("process_swap_request");
|
self.record_swap_failure("process_swap_request");
|
||||||
return Err(validate_sig_result.err().unwrap());
|
return Err(err);
|
||||||
}
|
}
|
||||||
let mut proof_writer =
|
let mut proof_writer =
|
||||||
ProofWriter::new(self.localstore.clone(), self.pubsub_manager.clone());
|
ProofWriter::new(self.localstore.clone(), self.pubsub_manager.clone());
|
||||||
@@ -72,6 +110,8 @@ impl Mint {
|
|||||||
METRICS.record_mint_operation("process_swap_request", false);
|
METRICS.record_mint_operation("process_swap_request", false);
|
||||||
METRICS.record_error();
|
METRICS.record_error();
|
||||||
}
|
}
|
||||||
|
tx.rollback().await?;
|
||||||
|
blinded_message_writer.rollback().await?;
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -80,10 +120,13 @@ impl Mint {
|
|||||||
.update_proofs_states(&mut tx, &input_ys, State::Spent)
|
.update_proofs_states(&mut tx, &input_ys, State::Spent)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if update_proof_states_result.is_err() {
|
if let Err(err) = update_proof_states_result {
|
||||||
#[cfg(feature = "prometheus")]
|
#[cfg(feature = "prometheus")]
|
||||||
self.record_swap_failure("process_swap_request");
|
self.record_swap_failure("process_swap_request");
|
||||||
return Err(update_proof_states_result.err().unwrap());
|
|
||||||
|
tx.rollback().await?;
|
||||||
|
blinded_message_writer.rollback().await?;
|
||||||
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.add_blind_signatures(
|
tx.add_blind_signatures(
|
||||||
@@ -98,6 +141,7 @@ impl Mint {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
proof_writer.commit();
|
proof_writer.commit();
|
||||||
|
blinded_message_writer.commit();
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
let response = SwapResponse::new(promises);
|
let response = SwapResponse::new(promises);
|
||||||
|
|||||||
Reference in New Issue
Block a user