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