mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-18 21:25:09 +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:
@@ -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;
|
||||
|
||||
async fn add_melt_request_and_blinded_messages(
|
||||
async fn add_melt_request(
|
||||
&mut self,
|
||||
quote_id: &QuoteId,
|
||||
inputs_amount: Amount,
|
||||
inputs_fee: Amount,
|
||||
blinded_messages: &[BlindedMessage],
|
||||
) -> Result<(), Self::Err> {
|
||||
// Insert melt_request
|
||||
query(
|
||||
r#"
|
||||
INSERT INTO melt_request
|
||||
@@ -567,29 +567,82 @@ where
|
||||
.execute(&self.inner)
|
||||
.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 {
|
||||
query(
|
||||
match query(
|
||||
r#"
|
||||
INSERT INTO blinded_messages
|
||||
(quote_id, blinded_message, keyset_id, amount)
|
||||
INSERT INTO blind_signature
|
||||
(blinded_message, amount, keyset_id, c, quote_id, created_time)
|
||||
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(
|
||||
"blinded_message",
|
||||
message.blinded_secret.to_bytes().to_vec(),
|
||||
)
|
||||
.bind("keyset_id", message.keyset_id.to_string())
|
||||
.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?;
|
||||
.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)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_melt_request_and_blinded_messages(
|
||||
&mut self,
|
||||
quote_id: &QuoteId,
|
||||
@@ -610,11 +663,12 @@ where
|
||||
let inputs_amount: u64 = column_as_number!(row[0].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(
|
||||
r#"
|
||||
SELECT blinded_message, keyset_id, amount
|
||||
FROM blinded_messages
|
||||
WHERE quote_id = :quote_id
|
||||
FROM blind_signature
|
||||
WHERE quote_id = :quote_id AND c IS NULL
|
||||
"#,
|
||||
)?
|
||||
.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> {
|
||||
// Delete from melt_request table
|
||||
query(
|
||||
r#"
|
||||
DELETE FROM melt_request
|
||||
@@ -660,6 +715,17 @@ where
|
||||
.execute(&self.inner)
|
||||
.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(())
|
||||
}
|
||||
|
||||
@@ -1560,34 +1626,122 @@ where
|
||||
) -> Result<(), Self::Err> {
|
||||
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) {
|
||||
query(
|
||||
r#"
|
||||
INSERT INTO blind_signature
|
||||
(blinded_message, amount, keyset_id, c, quote_id, dleq_e, dleq_s, created_time)
|
||||
VALUES
|
||||
(:blinded_message, :amount, :keyset_id, :c, :quote_id, :dleq_e, :dleq_s, :created_time)
|
||||
"#,
|
||||
)?
|
||||
.bind("blinded_message", message.to_bytes().to_vec())
|
||||
.bind("amount", u64::from(signature.amount) as i64)
|
||||
.bind("keyset_id", signature.keyset_id.to_string())
|
||||
.bind("c", signature.c.to_bytes().to_vec())
|
||||
.bind("quote_id", quote_id.as_ref().map(|q| match q {
|
||||
QuoteId::BASE64(s) => s.to_string(),
|
||||
QuoteId::UUID(u) => u.hyphenated().to_string(),
|
||||
}))
|
||||
.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("created_time", current_time as i64)
|
||||
.execute(&self.inner)
|
||||
.await?;
|
||||
match existing_rows.remove(message) {
|
||||
None => {
|
||||
// Unknown blind message: Insert new row with all columns
|
||||
query(
|
||||
r#"
|
||||
INSERT INTO blind_signature
|
||||
(blinded_message, amount, keyset_id, c, quote_id, dleq_e, dleq_s, created_time, signed_time)
|
||||
VALUES
|
||||
(: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("amount", u64::from(signature.amount) as i64)
|
||||
.bind("keyset_id", signature.keyset_id.to_string())
|
||||
.bind("c", signature.c.to_bytes().to_vec())
|
||||
.bind("quote_id", quote_id.as_ref().map(|q| q.to_string()))
|
||||
.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("created_time", current_time as i64)
|
||||
.bind("signed_time", current_time as i64)
|
||||
.execute(&self.inner)
|
||||
.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(())
|
||||
@@ -1607,14 +1761,14 @@ where
|
||||
blinded_message
|
||||
FROM
|
||||
blind_signature
|
||||
WHERE blinded_message IN (:y)
|
||||
WHERE blinded_message IN (:b) AND c IS NOT NULL
|
||||
"#,
|
||||
)?
|
||||
.bind_vec(
|
||||
"y",
|
||||
"b",
|
||||
blinded_messages
|
||||
.iter()
|
||||
.map(|y| y.to_bytes().to_vec())
|
||||
.map(|b| b.to_bytes().to_vec())
|
||||
.collect(),
|
||||
)
|
||||
.fetch_all(&self.inner)
|
||||
@@ -1660,11 +1814,11 @@ where
|
||||
blinded_message
|
||||
FROM
|
||||
blind_signature
|
||||
WHERE blinded_message IN (:blinded_message)
|
||||
WHERE blinded_message IN (:b) AND c IS NOT NULL
|
||||
"#,
|
||||
)?
|
||||
.bind_vec(
|
||||
"blinded_message",
|
||||
"b",
|
||||
blinded_messages
|
||||
.iter()
|
||||
.map(|b_| b_.to_bytes().to_vec())
|
||||
@@ -1706,7 +1860,7 @@ where
|
||||
FROM
|
||||
blind_signature
|
||||
WHERE
|
||||
keyset_id=:keyset_id
|
||||
keyset_id=:keyset_id AND c IS NOT NULL
|
||||
"#,
|
||||
)?
|
||||
.bind("keyset_id", keyset_id.to_string())
|
||||
@@ -1734,7 +1888,7 @@ where
|
||||
FROM
|
||||
blind_signature
|
||||
WHERE
|
||||
quote_id=:quote_id
|
||||
quote_id=:quote_id AND c IS NOT NULL
|
||||
"#,
|
||||
)?
|
||||
.bind("quote_id", quote_id.to_string())
|
||||
|
||||
Reference in New Issue
Block a user