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