Skip to main content

cdk_sql_common/mint/
signatures.rs

1//! Signatures database implementation
2
3use std::collections::HashMap;
4use std::str::FromStr;
5
6use async_trait::async_trait;
7use cdk_common::database::{self, Error, MintSignatureTransaction, MintSignaturesDatabase};
8use cdk_common::quote_id::QuoteId;
9use cdk_common::util::unix_time;
10use cdk_common::{Amount, BlindSignature, BlindSignatureDleq, Id, PublicKey, SecretKey};
11
12use super::proofs::sql_row_to_hashmap_amount;
13use super::{SQLMintDatabase, SQLTransaction};
14use crate::pool::DatabasePool;
15use crate::stmt::{query, Column};
16use crate::{column_as_nullable_string, column_as_number, column_as_string, unpack_into};
17
18pub(crate) fn sql_row_to_blind_signature(row: Vec<Column>) -> Result<BlindSignature, Error> {
19    unpack_into!(
20        let (
21            keyset_id, amount, c, dleq_e, dleq_s
22        ) = row
23    );
24
25    let dleq = match (
26        column_as_nullable_string!(dleq_e),
27        column_as_nullable_string!(dleq_s),
28    ) {
29        (Some(e), Some(s)) => Some(BlindSignatureDleq {
30            e: SecretKey::from_hex(e)?,
31            s: SecretKey::from_hex(s)?,
32        }),
33        _ => None,
34    };
35
36    let amount: u64 = column_as_number!(amount);
37
38    Ok(BlindSignature {
39        amount: Amount::from(amount),
40        keyset_id: column_as_string!(keyset_id, Id::from_str, Id::from_bytes),
41        c: column_as_string!(c, PublicKey::from_hex, PublicKey::from_slice),
42        dleq,
43    })
44}
45
46#[async_trait]
47impl<RM> MintSignatureTransaction for SQLTransaction<RM>
48where
49    RM: DatabasePool + 'static,
50{
51    type Err = Error;
52
53    async fn add_blind_signatures(
54        &mut self,
55        blinded_messages: &[PublicKey],
56        blind_signatures: &[BlindSignature],
57        quote_id: Option<QuoteId>,
58    ) -> Result<(), Self::Err> {
59        let current_time = unix_time();
60
61        if blinded_messages.len() != blind_signatures.len() {
62            return Err(database::Error::Internal(
63                "Mismatched array lengths for blinded messages and blind signatures".to_string(),
64            ));
65        }
66
67        // Select all existing rows for the given blinded messages at once
68        let mut existing_rows = query(
69            r#"
70            SELECT blinded_message, c, dleq_e, dleq_s
71            FROM blind_signature
72            WHERE blinded_message IN (:blinded_messages)
73            ORDER BY blinded_message
74            FOR UPDATE
75            "#,
76        )?
77        .bind_vec(
78            "blinded_messages",
79            blinded_messages
80                .iter()
81                .map(|message| message.to_bytes().to_vec())
82                .collect(),
83        )?
84        .fetch_all(&self.inner)
85        .await?
86        .into_iter()
87        .map(|mut row| {
88            Ok((
89                column_as_string!(&row.remove(0), PublicKey::from_hex, PublicKey::from_slice),
90                (row[0].clone(), row[1].clone(), row[2].clone()),
91            ))
92        })
93        .collect::<Result<HashMap<_, _>, Error>>()?;
94
95        let mut ordered_signatures = blinded_messages
96            .iter()
97            .zip(blind_signatures)
98            .enumerate()
99            .collect::<Vec<_>>();
100        ordered_signatures.sort_unstable_by(|(_, (left, _)), (_, (right, _))| {
101            left.to_bytes().cmp(&right.to_bytes())
102        });
103
104        // Mutate rows in blinded-message order while retaining the request order index.
105        for (i, (message, signature)) in ordered_signatures {
106            match existing_rows.remove(message) {
107                None => {
108                    // Unknown blind message: Insert new row with all columns
109                    query(
110                        r#"
111                        INSERT INTO blind_signature
112                        (blinded_message, amount, keyset_id, c, quote_id, dleq_e, dleq_s, created_time, signed_time, order_index)
113                        VALUES
114                        (:blinded_message, :amount, :keyset_id, :c, :quote_id, :dleq_e, :dleq_s, :created_time, :signed_time, :order_index)
115                        "#,
116                    )?
117                    .bind("blinded_message", message.to_bytes().to_vec())
118                    .bind("amount", u64::from(signature.amount) as i64)
119                    .bind("keyset_id", signature.keyset_id.to_string())
120                    .bind("c", signature.c.to_bytes().to_vec())
121                    .bind("quote_id", quote_id.as_ref().map(|q| q.to_string()))
122                    .bind(
123                        "dleq_e",
124                        signature.dleq.as_ref().map(|dleq| dleq.e.to_secret_hex()),
125                    )
126                    .bind(
127                        "dleq_s",
128                        signature.dleq.as_ref().map(|dleq| dleq.s.to_secret_hex()),
129                    )
130                    .bind("created_time", current_time as i64)
131                    .bind("signed_time", current_time as i64)
132                    .bind("order_index", i as i64)
133                    .execute(&self.inner)
134                    .await?;
135
136                    query(
137                        r#"
138                        INSERT INTO keyset_amounts (keyset_id, total_issued, total_redeemed)
139                        VALUES (:keyset_id, :amount, 0)
140                        ON CONFLICT (keyset_id)
141                        DO UPDATE SET total_issued = keyset_amounts.total_issued + EXCLUDED.total_issued
142                        "#,
143                    )?
144                    .bind("amount", u64::from(signature.amount) as i64)
145                    .bind("keyset_id", signature.keyset_id.to_string())
146                    .execute(&self.inner)
147                    .await?;
148                }
149                Some((c, _dleq_e, _dleq_s)) => {
150                    // Blind message exists: check if c is NULL
151                    match c {
152                        Column::Null => {
153                            // Blind message with no c: Update with missing columns c, dleq_e, dleq_s
154                            query(
155                                r#"
156                                UPDATE blind_signature
157                                SET c = :c, dleq_e = :dleq_e, dleq_s = :dleq_s, signed_time = :signed_time, amount = :amount
158                                WHERE blinded_message = :blinded_message
159                                "#,
160                            )?
161                            .bind("c", signature.c.to_bytes().to_vec())
162                            .bind(
163                                "dleq_e",
164                                signature.dleq.as_ref().map(|dleq| dleq.e.to_secret_hex()),
165                            )
166                            .bind(
167                                "dleq_s",
168                                signature.dleq.as_ref().map(|dleq| dleq.s.to_secret_hex()),
169                            )
170                            .bind("blinded_message", message.to_bytes().to_vec())
171                            .bind("signed_time", current_time as i64)
172                            .bind("amount", u64::from(signature.amount) as i64)
173                            .execute(&self.inner)
174                            .await?;
175
176                            query(
177                                r#"
178                                INSERT INTO keyset_amounts (keyset_id, total_issued, total_redeemed)
179                                VALUES (:keyset_id, :amount, 0)
180                                ON CONFLICT (keyset_id)
181                                DO UPDATE SET total_issued = keyset_amounts.total_issued + EXCLUDED.total_issued
182                                "#,
183                            )?
184                            .bind("amount", u64::from(signature.amount) as i64)
185                            .bind("keyset_id", signature.keyset_id.to_string())
186                            .execute(&self.inner)
187                            .await?;
188                        }
189                        _ => {
190                            // Blind message already has c: Error
191                            tracing::error!(
192                                "Attempting to add signature to message already signed {}",
193                                message
194                            );
195
196                            return Err(database::Error::Duplicate);
197                        }
198                    }
199                }
200            }
201        }
202
203        debug_assert!(
204            existing_rows.is_empty(),
205            "Unexpected existing rows remain: {:?}",
206            existing_rows.keys().collect::<Vec<_>>()
207        );
208
209        if !existing_rows.is_empty() {
210            tracing::error!("Did not check all existing rows");
211            return Err(Error::Internal(
212                "Did not check all existing rows".to_string(),
213            ));
214        }
215
216        Ok(())
217    }
218
219    async fn get_blind_signatures(
220        &mut self,
221        blinded_messages: &[PublicKey],
222    ) -> Result<Vec<Option<BlindSignature>>, Self::Err> {
223        let mut blinded_signatures = query(
224            r#"SELECT
225                keyset_id,
226                amount,
227                c,
228                dleq_e,
229                dleq_s,
230                blinded_message
231            FROM
232                blind_signature
233            WHERE blinded_message IN (:b) AND c IS NOT NULL
234            "#,
235        )?
236        .bind_vec(
237            "b",
238            blinded_messages
239                .iter()
240                .map(|b| b.to_bytes().to_vec())
241                .collect(),
242        )?
243        .fetch_all(&self.inner)
244        .await?
245        .into_iter()
246        .map(|mut row| {
247            Ok((
248                column_as_string!(
249                    &row.pop().ok_or(Error::InvalidDbResponse)?,
250                    PublicKey::from_hex,
251                    PublicKey::from_slice
252                ),
253                sql_row_to_blind_signature(row)?,
254            ))
255        })
256        .collect::<Result<HashMap<_, _>, Error>>()?;
257        Ok(blinded_messages
258            .iter()
259            .map(|y| blinded_signatures.remove(y))
260            .collect())
261    }
262}
263
264#[async_trait]
265impl<RM> MintSignaturesDatabase for SQLMintDatabase<RM>
266where
267    RM: DatabasePool + 'static,
268{
269    type Err = Error;
270
271    async fn get_blind_signatures(
272        &self,
273        blinded_messages: &[PublicKey],
274    ) -> Result<Vec<Option<BlindSignature>>, Self::Err> {
275        let conn = self
276            .pool
277            .get()
278            .await
279            .map_err(|e| Error::Database(Box::new(e)))?;
280        let mut blinded_signatures = query(
281            r#"SELECT
282                keyset_id,
283                amount,
284                c,
285                dleq_e,
286                dleq_s,
287                blinded_message
288            FROM
289                blind_signature
290            WHERE blinded_message IN (:b) AND c IS NOT NULL
291            "#,
292        )?
293        .bind_vec(
294            "b",
295            blinded_messages
296                .iter()
297                .map(|b_| b_.to_bytes().to_vec())
298                .collect(),
299        )?
300        .fetch_all(&*conn)
301        .await?
302        .into_iter()
303        .map(|mut row| {
304            Ok((
305                column_as_string!(
306                    &row.pop().ok_or(Error::InvalidDbResponse)?,
307                    PublicKey::from_hex,
308                    PublicKey::from_slice
309                ),
310                sql_row_to_blind_signature(row)?,
311            ))
312        })
313        .collect::<Result<HashMap<_, _>, Error>>()?;
314        Ok(blinded_messages
315            .iter()
316            .map(|y| blinded_signatures.remove(y))
317            .collect())
318    }
319
320    async fn get_blind_signatures_for_keyset(
321        &self,
322        keyset_id: &Id,
323    ) -> Result<Vec<BlindSignature>, Self::Err> {
324        let conn = self
325            .pool
326            .get()
327            .await
328            .map_err(|e| Error::Database(Box::new(e)))?;
329        Ok(query(
330            r#"
331            SELECT
332                keyset_id,
333                amount,
334                c,
335                dleq_e,
336                dleq_s
337            FROM
338                blind_signature
339            WHERE
340                keyset_id=:keyset_id AND c IS NOT NULL
341            "#,
342        )?
343        .bind("keyset_id", keyset_id.to_string())
344        .fetch_all(&*conn)
345        .await?
346        .into_iter()
347        .map(sql_row_to_blind_signature)
348        .collect::<Result<Vec<BlindSignature>, _>>()?)
349    }
350
351    /// Get [`BlindSignature`]s for quote
352    async fn get_blind_signatures_for_quote(
353        &self,
354        quote_id: &QuoteId,
355    ) -> Result<Vec<BlindSignature>, Self::Err> {
356        let conn = self
357            .pool
358            .get()
359            .await
360            .map_err(|e| Error::Database(Box::new(e)))?;
361        Ok(query(
362            r#"
363            SELECT
364                keyset_id,
365                amount,
366                c,
367                dleq_e,
368                dleq_s
369            FROM
370                blind_signature
371            WHERE
372                quote_id=:quote_id AND c IS NOT NULL
373            ORDER BY order_index ASC
374            "#,
375        )?
376        .bind("quote_id", quote_id.to_string())
377        .fetch_all(&*conn)
378        .await?
379        .into_iter()
380        .map(sql_row_to_blind_signature)
381        .collect::<Result<Vec<BlindSignature>, _>>()?)
382    }
383
384    /// Get total proofs redeemed by keyset id
385    async fn get_total_issued(&self) -> Result<HashMap<Id, Amount>, Self::Err> {
386        let conn = self
387            .pool
388            .get()
389            .await
390            .map_err(|e| Error::Database(Box::new(e)))?;
391        query(
392            r#"
393            SELECT
394                keyset_id,
395                total_issued as amount
396            FROM
397                keyset_amounts
398        "#,
399        )?
400        .fetch_all(&*conn)
401        .await?
402        .into_iter()
403        .map(sql_row_to_hashmap_amount)
404        .collect()
405    }
406
407    async fn get_blinded_secrets_by_operation_id(
408        &self,
409        operation_id: &uuid::Uuid,
410    ) -> Result<Vec<PublicKey>, Self::Err> {
411        let conn = self
412            .pool
413            .get()
414            .await
415            .map_err(|e| Error::Database(Box::new(e)))?;
416        query(
417            r#"
418            SELECT
419                blinded_message
420            FROM
421                blind_signature
422            WHERE
423                operation_id = :operation_id
424            "#,
425        )?
426        .bind("operation_id", operation_id.to_string())
427        .fetch_all(&*conn)
428        .await?
429        .into_iter()
430        .map(|row| -> Result<PublicKey, Error> {
431            Ok(column_as_string!(
432                &row[0],
433                PublicKey::from_hex,
434                PublicKey::from_slice
435            ))
436        })
437        .collect::<Result<Vec<_>, _>>()
438    }
439}