clementine_core/database/
operator.rs

1//! # Operator Related Database Operations
2//!
3//! This module includes database functions which are mainly used by an operator.
4
5use super::{
6    wrapper::{
7        AddressDB, DepositParamsDB, OutPointDB, ReceiptDB, SignaturesDB, TxidDB, XOnlyPublicKeyDB,
8    },
9    Database, DatabaseTransaction,
10};
11use crate::{
12    builder::transaction::create_move_to_vault_txhandler,
13    config::protocol::ProtocolParamset,
14    deposit::{DepositData, KickoffData, OperatorData},
15};
16use crate::{
17    execute_query_with_tx,
18    rpc::clementine::{DepositSignatures, TaggedSignature},
19};
20use bitcoin::{OutPoint, Txid, XOnlyPublicKey};
21use bitvm::signatures::winternitz;
22use bitvm::signatures::winternitz::PublicKey as WinternitzPublicKey;
23use clementine_errors::BridgeError;
24use clementine_primitives::{PublicHash, RoundIndex};
25use eyre::{eyre, Context};
26use risc0_zkvm::Receipt;
27use std::str::FromStr;
28
29pub type RootHash = [u8; 32];
30//pub type PublicInputWots = Vec<[u8; 20]>;
31pub type AssertTxHash = Vec<[u8; 32]>;
32
33pub type BitvmSetup = (AssertTxHash, RootHash, RootHash);
34
35impl Database {
36    /// Sets the operator details to the db.
37    /// This function additionally checks if the operator data already exists in the db.
38    /// As we don't want to overwrite operator data on the db, as it can prevent us slash malicious operators that signed
39    /// previous deposits. This function should give an error if an operator changed its data.
40    pub async fn insert_operator_if_not_exists(
41        &self,
42        mut tx: Option<DatabaseTransaction<'_>>,
43        xonly_pubkey: XOnlyPublicKey,
44        wallet_address: &bitcoin::Address,
45        collateral_funding_outpoint: OutPoint,
46    ) -> Result<(), BridgeError> {
47        let query = sqlx::query(
48            "INSERT INTO operators (xonly_pk, wallet_reimburse_address, collateral_funding_outpoint)
49             VALUES ($1, $2, $3)
50             ON CONFLICT (xonly_pk) DO NOTHING",
51        )
52        .bind(XOnlyPublicKeyDB(xonly_pubkey))
53        .bind(AddressDB(wallet_address.as_unchecked().clone()))
54        .bind(OutPointDB(collateral_funding_outpoint));
55
56        let result = execute_query_with_tx!(self.connection, tx.as_deref_mut(), query, execute)?;
57
58        // If no rows were affected, data already exists - check if it matches
59        if result.rows_affected() == 0 {
60            let existing = self.get_operator(tx, xonly_pubkey).await?;
61            if let Some(op) = existing {
62                if op.reimburse_addr != *wallet_address
63                    || op.collateral_funding_outpoint != collateral_funding_outpoint
64                {
65                    return Err(BridgeError::OperatorDataMismatch(xonly_pubkey));
66                }
67            }
68        }
69
70        Ok(())
71    }
72
73    pub async fn get_operators(
74        &self,
75        tx: Option<DatabaseTransaction<'_>>,
76    ) -> Result<Vec<(XOnlyPublicKey, bitcoin::Address, OutPoint)>, BridgeError> {
77        let query = sqlx::query_as(
78            "SELECT xonly_pk, wallet_reimburse_address, collateral_funding_outpoint FROM operators;"
79        );
80
81        let operators: Vec<(XOnlyPublicKeyDB, AddressDB, OutPointDB)> =
82            execute_query_with_tx!(self.connection, tx, query, fetch_all)?;
83
84        // Convert the result to the desired format
85        let data = operators
86            .into_iter()
87            .map(|(pk, addr, outpoint_db)| {
88                let xonly_pk = pk.0;
89                let addr = addr.0.assume_checked();
90                let outpoint = outpoint_db.0; // Extract the Txid from TxidDB
91                Ok((xonly_pk, addr, outpoint))
92            })
93            .collect::<Result<Vec<_>, BridgeError>>()?;
94        Ok(data)
95    }
96
97    pub async fn get_operator(
98        &self,
99        tx: Option<DatabaseTransaction<'_>>,
100        operator_xonly_pk: XOnlyPublicKey,
101    ) -> Result<Option<OperatorData>, BridgeError> {
102        let query = sqlx::query_as(
103            "SELECT xonly_pk, wallet_reimburse_address, collateral_funding_outpoint FROM operators WHERE xonly_pk = $1;"
104        ).bind(XOnlyPublicKeyDB(operator_xonly_pk));
105
106        let result: Option<(String, String, OutPointDB)> =
107            execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
108
109        match result {
110            None => Ok(None),
111            Some((_, addr, outpoint_db)) => {
112                // Convert the result to the desired format
113                let addr = bitcoin::Address::from_str(&addr)
114                    .wrap_err("Invalid Address")?
115                    .assume_checked();
116                let outpoint = outpoint_db.0; // Extract the Txid from TxidDB
117                Ok(Some(OperatorData {
118                    xonly_pk: operator_xonly_pk,
119                    reimburse_addr: addr,
120                    collateral_funding_outpoint: outpoint,
121                }))
122            }
123        }
124    }
125
126    /// Sets the unspent kickoff sigs received from operators during initial setup.
127    /// Sigs of each round are stored together in the same row.
128    /// On conflict, do not update the existing sigs. Although technically, as long as kickoff winternitz keys
129    /// and operator data(collateral funding outpoint and reimburse address) are not changed, the sigs are still valid
130    /// even if they are changed.
131    pub async fn insert_unspent_kickoff_sigs_if_not_exist(
132        &self,
133        tx: Option<DatabaseTransaction<'_>>,
134        operator_xonly_pk: XOnlyPublicKey,
135        round_idx: RoundIndex,
136        signatures: Vec<TaggedSignature>,
137    ) -> Result<(), BridgeError> {
138        let query = sqlx::query(
139            "INSERT INTO unspent_kickoff_signatures (xonly_pk, round_idx, signatures) VALUES ($1, $2, $3)
140             ON CONFLICT (xonly_pk, round_idx) DO NOTHING;",
141        ).bind(XOnlyPublicKeyDB(operator_xonly_pk)).bind(round_idx.to_index() as i32).bind(SignaturesDB(DepositSignatures{signatures}));
142
143        execute_query_with_tx!(self.connection, tx, query, execute)?;
144        Ok(())
145    }
146
147    /// Get unspent kickoff sigs for a specific operator and round.
148    pub async fn get_unspent_kickoff_sigs(
149        &self,
150        tx: Option<DatabaseTransaction<'_>>,
151        operator_xonly_pk: XOnlyPublicKey,
152        round_idx: RoundIndex,
153    ) -> Result<Option<Vec<TaggedSignature>>, BridgeError> {
154        let query = sqlx::query_as::<_, (SignaturesDB,)>("SELECT signatures FROM unspent_kickoff_signatures WHERE xonly_pk = $1 AND round_idx = $2")
155            .bind(XOnlyPublicKeyDB(operator_xonly_pk))
156            .bind(round_idx.to_index() as i32);
157
158        let result: Result<(SignaturesDB,), sqlx::Error> =
159            execute_query_with_tx!(self.connection, tx, query, fetch_one);
160
161        match result {
162            Ok((SignaturesDB(signatures),)) => Ok(Some(signatures.signatures)),
163            Err(sqlx::Error::RowNotFound) => Ok(None),
164            Err(e) => Err(BridgeError::DatabaseError(e)),
165        }
166    }
167
168    /// Sets Winternitz public keys for bitvm related inputs of an operator.
169    pub async fn insert_operator_bitvm_keys_if_not_exist(
170        &self,
171        mut tx: Option<DatabaseTransaction<'_>>,
172        operator_xonly_pk: XOnlyPublicKey,
173        deposit_outpoint: OutPoint,
174        winternitz_public_key: Vec<WinternitzPublicKey>,
175    ) -> Result<(), BridgeError> {
176        let wpk = borsh::to_vec(&winternitz_public_key).wrap_err(BridgeError::BorshError)?;
177        let deposit_id = self
178            .get_deposit_id(tx.as_deref_mut(), deposit_outpoint)
179            .await?;
180        let query = sqlx::query(
181                "INSERT INTO operator_bitvm_winternitz_public_keys (xonly_pk, deposit_id, bitvm_winternitz_public_keys) VALUES ($1, $2, $3)
182                ON CONFLICT DO NOTHING;",
183            )
184            .bind(XOnlyPublicKeyDB(operator_xonly_pk))
185            .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?)
186            .bind(wpk);
187
188        execute_query_with_tx!(self.connection, tx, query, execute)?;
189
190        Ok(())
191    }
192
193    /// Gets Winternitz public keys for bitvm related inputs of an operator.
194    pub async fn get_operator_bitvm_keys(
195        &self,
196        mut tx: Option<DatabaseTransaction<'_>>,
197        operator_xonly_pk: XOnlyPublicKey,
198        deposit_outpoint: OutPoint,
199    ) -> Result<Vec<winternitz::PublicKey>, BridgeError> {
200        let deposit_id = self
201            .get_deposit_id(tx.as_deref_mut(), deposit_outpoint)
202            .await?;
203        let query = sqlx::query_as(
204                "SELECT bitvm_winternitz_public_keys FROM operator_bitvm_winternitz_public_keys WHERE xonly_pk = $1 AND deposit_id = $2;"
205            )
206            .bind(XOnlyPublicKeyDB(operator_xonly_pk))
207            .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?);
208
209        let winternitz_pks: (Vec<u8>,) =
210            execute_query_with_tx!(self.connection, tx, query, fetch_one)?;
211
212        {
213            let operator_winternitz_pks: Vec<winternitz::PublicKey> =
214                borsh::from_slice(&winternitz_pks.0).wrap_err(BridgeError::BorshError)?;
215            Ok(operator_winternitz_pks)
216        }
217    }
218
219    /// Sets Winternitz public keys (only for kickoff blockhash commit) for an operator.
220    /// On conflict, do not update the existing keys. This is very important, as otherwise the txids of
221    /// operators round tx's will change.
222    pub async fn insert_operator_kickoff_winternitz_public_keys_if_not_exist(
223        &self,
224        mut tx: Option<DatabaseTransaction<'_>>,
225        operator_xonly_pk: XOnlyPublicKey,
226        winternitz_public_key: Vec<WinternitzPublicKey>,
227    ) -> Result<(), BridgeError> {
228        let wpk = borsh::to_vec(&winternitz_public_key).wrap_err(BridgeError::BorshError)?;
229
230        let query = sqlx::query(
231            "INSERT INTO operator_winternitz_public_keys (xonly_pk, winternitz_public_keys)
232             VALUES ($1, $2)
233             ON CONFLICT (xonly_pk) DO NOTHING",
234        )
235        .bind(XOnlyPublicKeyDB(operator_xonly_pk))
236        .bind(wpk);
237
238        let result = execute_query_with_tx!(self.connection, tx.as_deref_mut(), query, execute)?;
239
240        // If no rows were affected, data already exists - check if it matches
241        if result.rows_affected() == 0 {
242            let existing = self
243                .get_operator_kickoff_winternitz_public_keys(tx, operator_xonly_pk)
244                .await?;
245            if existing != winternitz_public_key {
246                return Err(BridgeError::OperatorWinternitzPublicKeysMismatch(
247                    operator_xonly_pk,
248                ));
249            }
250        }
251
252        Ok(())
253    }
254
255    /// Gets Winternitz public keys for every sequential collateral tx of an
256    /// operator and a watchtower.
257    pub async fn get_operator_kickoff_winternitz_public_keys(
258        &self,
259        tx: Option<DatabaseTransaction<'_>>,
260        op_xonly_pk: XOnlyPublicKey,
261    ) -> Result<Vec<winternitz::PublicKey>, BridgeError> {
262        let query = sqlx::query_as(
263                "SELECT winternitz_public_keys FROM operator_winternitz_public_keys WHERE xonly_pk = $1;",
264            )
265            .bind(XOnlyPublicKeyDB(op_xonly_pk));
266
267        let wpks: (Vec<u8>,) = execute_query_with_tx!(self.connection, tx, query, fetch_one)?;
268
269        let operator_winternitz_pks: Vec<winternitz::PublicKey> =
270            borsh::from_slice(&wpks.0).wrap_err(BridgeError::BorshError)?;
271
272        Ok(operator_winternitz_pks)
273    }
274
275    /// Sets public hashes for a specific operator, sequential collateral tx and
276    /// kickoff index combination. If there is hashes for given indexes, they
277    /// will be overwritten by the new hashes.
278    pub async fn insert_operator_challenge_ack_hashes_if_not_exist(
279        &self,
280        mut tx: Option<DatabaseTransaction<'_>>,
281        operator_xonly_pk: XOnlyPublicKey,
282        deposit_outpoint: OutPoint,
283        public_hashes: &Vec<[u8; 20]>,
284    ) -> Result<(), BridgeError> {
285        let deposit_id = self
286            .get_deposit_id(tx.as_deref_mut(), deposit_outpoint)
287            .await?;
288        let query = sqlx::query(
289            "INSERT INTO operators_challenge_ack_hashes (xonly_pk, deposit_id, public_hashes)
290             VALUES ($1, $2, $3)
291             ON CONFLICT (xonly_pk, deposit_id) DO NOTHING;",
292        )
293        .bind(XOnlyPublicKeyDB(operator_xonly_pk))
294        .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?)
295        .bind(public_hashes);
296
297        let result = execute_query_with_tx!(self.connection, tx.as_deref_mut(), query, execute)?;
298
299        // If no rows were affected, data already exists - check if it matches
300        if result.rows_affected() == 0 {
301            let existing = self
302                .get_operators_challenge_ack_hashes(tx, operator_xonly_pk, deposit_outpoint)
303                .await?;
304            if let Some(existing_hashes) = existing {
305                if existing_hashes != *public_hashes {
306                    return Err(BridgeError::OperatorChallengeAckHashesMismatch(
307                        operator_xonly_pk,
308                        deposit_outpoint,
309                    ));
310                }
311            }
312        }
313
314        Ok(())
315    }
316
317    /// Retrieves public hashes for a specific operator, sequential collateral
318    /// tx and kickoff index combination.
319    pub async fn get_operators_challenge_ack_hashes(
320        &self,
321        mut tx: Option<DatabaseTransaction<'_>>,
322        operator_xonly_pk: XOnlyPublicKey,
323        deposit_outpoint: OutPoint,
324    ) -> Result<Option<Vec<PublicHash>>, BridgeError> {
325        let deposit_id = self
326            .get_deposit_id(tx.as_deref_mut(), deposit_outpoint)
327            .await?;
328        let query = sqlx::query_as::<_, (Vec<Vec<u8>>,)>(
329            "SELECT public_hashes
330            FROM operators_challenge_ack_hashes
331            WHERE xonly_pk = $1 AND deposit_id = $2;",
332        )
333        .bind(XOnlyPublicKeyDB(operator_xonly_pk))
334        .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?);
335
336        let result = execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
337
338        match result {
339            Some((public_hashes,)) => {
340                let mut converted_hashes = Vec::new();
341                for hash in public_hashes {
342                    match hash.try_into() {
343                        Ok(public_hash) => converted_hashes.push(public_hash),
344                        Err(err) => {
345                            tracing::error!("Failed to convert hash: {:?}", err);
346                            return Err(eyre::eyre!("Failed to convert public hash").into());
347                        }
348                    }
349                }
350                Ok(Some(converted_hashes))
351            }
352            None => Ok(None), // If no result is found, return Ok(None)
353        }
354    }
355
356    /// Saves deposit infos, and returns the deposit_id
357    /// This function additionally checks if the deposit data already exists in the db.
358    /// As we don't want to overwrite deposit data on the db, this function should give an error if deposit data is changed.
359    pub async fn insert_deposit_data_if_not_exists(
360        &self,
361        mut tx: Option<DatabaseTransaction<'_>>,
362        deposit_data: &mut DepositData,
363        paramset: &'static ProtocolParamset,
364    ) -> Result<u32, BridgeError> {
365        // compute move to vault txid
366        let move_to_vault_txid = create_move_to_vault_txhandler(deposit_data, paramset)?
367            .get_cached_tx()
368            .compute_txid();
369
370        let query = sqlx::query_as::<_, (i32,)>(
371            "INSERT INTO deposits (deposit_outpoint, deposit_params, move_to_vault_txid)
372                VALUES ($1, $2, $3)
373                ON CONFLICT (deposit_outpoint) DO NOTHING
374                RETURNING deposit_id",
375        )
376        .bind(OutPointDB(deposit_data.get_deposit_outpoint()))
377        .bind(DepositParamsDB(deposit_data.clone().into()))
378        .bind(TxidDB(move_to_vault_txid));
379
380        let result =
381            execute_query_with_tx!(self.connection, tx.as_deref_mut(), query, fetch_optional)?;
382
383        // If we got a deposit_id back, that means we successfully inserted new data
384        if let Some((deposit_id,)) = result {
385            return Ok(u32::try_from(deposit_id).wrap_err("Failed to convert deposit id to u32")?);
386        }
387
388        // If no rows were returned, data already exists - check if it matches
389        let existing_query = sqlx::query_as::<_, (i32, DepositParamsDB, TxidDB)>(
390            "SELECT deposit_id, deposit_params, move_to_vault_txid FROM deposits WHERE deposit_outpoint = $1"
391        )
392        .bind(OutPointDB(deposit_data.get_deposit_outpoint()));
393
394        let (existing_deposit_id, existing_deposit_params, existing_move_txid): (
395            i32,
396            DepositParamsDB,
397            TxidDB,
398        ) = execute_query_with_tx!(self.connection, tx, existing_query, fetch_one)?;
399
400        let existing_deposit_data: DepositData = existing_deposit_params
401            .0
402            .try_into()
403            .map_err(|e| eyre::eyre!("Invalid deposit params {e}"))?;
404
405        if existing_deposit_data != *deposit_data {
406            tracing::error!(
407                "Deposit data mismatch: Existing {:?}, New {:?}",
408                existing_deposit_data,
409                deposit_data
410            );
411            return Err(BridgeError::DepositDataMismatch(
412                deposit_data.get_deposit_outpoint(),
413            ));
414        }
415
416        if existing_move_txid.0 != move_to_vault_txid {
417            // This should never happen, only a sanity check
418            tracing::error!(
419                "Move to vault txid mismatch in set_deposit_data: Existing {:?}, New {:?}",
420                existing_move_txid.0,
421                move_to_vault_txid
422            );
423            return Err(BridgeError::DepositDataMismatch(
424                deposit_data.get_deposit_outpoint(),
425            ));
426        }
427
428        // If data matches, return the existing deposit_id
429        Ok(u32::try_from(existing_deposit_id).wrap_err("Failed to convert deposit id to u32")?)
430    }
431
432    pub async fn get_deposit_data_with_move_tx(
433        &self,
434        tx: Option<DatabaseTransaction<'_>>,
435        move_to_vault_txid: Txid,
436    ) -> Result<Option<DepositData>, BridgeError> {
437        let query = sqlx::query_as::<_, (DepositParamsDB,)>(
438            "SELECT deposit_params FROM deposits WHERE move_to_vault_txid = $1;",
439        )
440        .bind(TxidDB(move_to_vault_txid));
441
442        let result: Option<(DepositParamsDB,)> =
443            execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
444
445        match result {
446            Some((deposit_params,)) => Ok(Some(
447                deposit_params
448                    .0
449                    .try_into()
450                    .map_err(|e| eyre::eyre!("Invalid deposit params {e}"))?,
451            )),
452            None => Ok(None),
453        }
454    }
455
456    pub async fn get_deposit_data(
457        &self,
458        tx: Option<DatabaseTransaction<'_>>,
459        deposit_outpoint: OutPoint,
460    ) -> Result<Option<(u32, DepositData)>, BridgeError> {
461        let query = sqlx::query_as(
462            "SELECT deposit_id, deposit_params FROM deposits WHERE deposit_outpoint = $1;",
463        )
464        .bind(OutPointDB(deposit_outpoint));
465
466        let result: Option<(i32, DepositParamsDB)> =
467            execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
468
469        match result {
470            Some((deposit_id, deposit_params)) => Ok(Some((
471                u32::try_from(deposit_id).wrap_err("Failed to convert deposit id to u32")?,
472                deposit_params
473                    .0
474                    .try_into()
475                    .map_err(|e| eyre::eyre!("Invalid deposit params {e}"))?,
476            ))),
477            None => Ok(None),
478        }
479    }
480
481    /// Saves the deposit signatures to the database for a single operator.
482    /// The signatures array is identified by the deposit_outpoint and operator_idx.
483    /// For the order of signatures, please check [`crate::builder::sighash::create_nofn_sighash_stream`]
484    /// which determines the order of the sighashes that are signed.
485    #[allow(clippy::too_many_arguments)]
486    pub async fn insert_deposit_signatures_if_not_exist(
487        &self,
488        mut tx: Option<DatabaseTransaction<'_>>,
489        deposit_outpoint: OutPoint,
490        operator_xonly_pk: XOnlyPublicKey,
491        round_idx: RoundIndex,
492        kickoff_idx: usize,
493        kickoff_txid: Txid,
494        signatures: Vec<TaggedSignature>,
495    ) -> Result<(), BridgeError> {
496        let deposit_id = self
497            .get_deposit_id(tx.as_deref_mut(), deposit_outpoint)
498            .await?;
499
500        // First check if the entry already exists.
501        let query = sqlx::query_as(
502            "SELECT kickoff_txid FROM deposit_signatures
503        WHERE deposit_id = $1 AND operator_xonly_pk = $2 AND round_idx = $3 AND kickoff_idx = $4;",
504        )
505        .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?)
506        .bind(XOnlyPublicKeyDB(operator_xonly_pk))
507        .bind(round_idx.to_index() as i32)
508        .bind(kickoff_idx as i32);
509        let txid_and_signatures: Option<(TxidDB,)> =
510            execute_query_with_tx!(self.connection, tx.as_deref_mut(), query, fetch_optional)?;
511
512        if let Some((existing_kickoff_txid,)) = txid_and_signatures {
513            if existing_kickoff_txid.0 == kickoff_txid {
514                return Ok(());
515            } else {
516                return Err(eyre!("Kickoff txid or signatures already set!").into());
517            }
518        }
519        // On conflict, the previous signatures are already valid. Signatures only depend on deposit_outpoint (which depends on nofn pk) and
520        // operator_xonly_pk (also depends on nofn_pk, as each operator is also a verifier and nofn_pk depends on verifiers pk)
521        // Additionally operator collateral outpoint and reimbursement addr should be unchanged which we ensure in relevant db fns.
522        // We add on conflict clause so it doesn't fail if the signatures are already set.
523        // Why do we need to do this? If deposit fails somehow just at the end because movetx
524        // signature fails to be collected, we might need to do a deposit again. Technically we can only collect movetx signature, not
525        // do the full deposit.
526
527        let query = sqlx::query(
528            "INSERT INTO deposit_signatures (deposit_id, operator_xonly_pk, round_idx, kickoff_idx, kickoff_txid, signatures)
529            VALUES ($1, $2, $3, $4, $5, $6)
530            ON CONFLICT DO NOTHING;"
531        )
532        .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?)
533        .bind(XOnlyPublicKeyDB(operator_xonly_pk))
534        .bind(round_idx.to_index() as i32)
535        .bind(kickoff_idx as i32)
536        .bind(TxidDB(kickoff_txid))
537        .bind(SignaturesDB(DepositSignatures{signatures: signatures.clone()}));
538
539        execute_query_with_tx!(self.connection, tx, query, execute)?;
540
541        Ok(())
542    }
543
544    /// Gets a unique int for a deposit outpoint
545    pub async fn get_deposit_id(
546        &self,
547        tx: Option<DatabaseTransaction<'_>>,
548        deposit_outpoint: OutPoint,
549    ) -> Result<u32, BridgeError> {
550        let query = sqlx::query_as(
551            r#"
552            WITH ins AS (
553                INSERT INTO deposits (deposit_outpoint)
554                VALUES ($1)
555                ON CONFLICT (deposit_outpoint) DO NOTHING
556                RETURNING deposit_id
557            )
558            SELECT deposit_id FROM ins
559            UNION ALL
560            SELECT d.deposit_id
561            FROM deposits d
562            WHERE d.deposit_outpoint = $1
563            LIMIT 1;
564            "#,
565        )
566        .bind(OutPointDB(deposit_outpoint));
567
568        let deposit_id: Result<(i32,), sqlx::Error> =
569            execute_query_with_tx!(self.connection, tx, query, fetch_one);
570        Ok(u32::try_from(deposit_id?.0).wrap_err("Failed to convert deposit id to u32")?)
571    }
572
573    /// For a given kickoff txid, get the deposit outpoint that corresponds to it
574    pub async fn get_deposit_outpoint_for_kickoff_txid(
575        &self,
576        tx: Option<DatabaseTransaction<'_>>,
577        kickoff_txid: Txid,
578    ) -> Result<OutPoint, BridgeError> {
579        let query = sqlx::query_as::<_, (OutPointDB,)>(
580            "SELECT d.deposit_outpoint FROM deposit_signatures ds
581             INNER JOIN deposits d ON d.deposit_id = ds.deposit_id
582             WHERE ds.kickoff_txid = $1;",
583        )
584        .bind(TxidDB(kickoff_txid));
585        let result: (OutPointDB,) = execute_query_with_tx!(self.connection, tx, query, fetch_one)?;
586        Ok(result.0 .0)
587    }
588
589    /// Retrieves the deposit signatures for a single operator for a single reimburse
590    /// process (single kickoff utxo).
591    /// The signatures are tagged so that each signature can be matched with the correct
592    /// txin it belongs to easily.
593    pub async fn get_deposit_signatures(
594        &self,
595        tx: Option<DatabaseTransaction<'_>>,
596        deposit_outpoint: OutPoint,
597        operator_xonly_pk: XOnlyPublicKey,
598        round_idx: RoundIndex,
599        kickoff_idx: usize,
600    ) -> Result<Option<Vec<TaggedSignature>>, BridgeError> {
601        let query = sqlx::query_as::<_, (SignaturesDB,)>(
602            "SELECT ds.signatures FROM deposit_signatures ds
603                    INNER JOIN deposits d ON d.deposit_id = ds.deposit_id
604                 WHERE d.deposit_outpoint = $1
605                 AND ds.operator_xonly_pk = $2
606                 AND ds.round_idx = $3
607                 AND ds.kickoff_idx = $4;",
608        )
609        .bind(OutPointDB(deposit_outpoint))
610        .bind(XOnlyPublicKeyDB(operator_xonly_pk))
611        .bind(round_idx.to_index() as i32)
612        .bind(kickoff_idx as i32);
613
614        let result: Result<(SignaturesDB,), sqlx::Error> =
615            execute_query_with_tx!(self.connection, tx, query, fetch_one);
616
617        match result {
618            Ok((SignaturesDB(signatures),)) => Ok(Some(signatures.signatures)),
619            Err(sqlx::Error::RowNotFound) => Ok(None),
620            Err(e) => Err(BridgeError::DatabaseError(e)),
621        }
622    }
623
624    /// Gets the kickoff_txid from deposit_signatures for a given deposit and kickoff.
625    pub async fn get_kickoff_txid_from_deposit_and_kickoff_data(
626        &self,
627        tx: Option<DatabaseTransaction<'_>>,
628        deposit_outpoint: OutPoint,
629        kickoff_data: &KickoffData,
630    ) -> Result<Option<Txid>, BridgeError> {
631        let query = sqlx::query_as::<_, (TxidDB,)>(
632            "SELECT ds.kickoff_txid FROM deposit_signatures ds
633             INNER JOIN deposits d ON d.deposit_id = ds.deposit_id
634             WHERE d.deposit_outpoint = $1
635             AND ds.operator_xonly_pk = $2
636             AND ds.round_idx = $3
637             AND ds.kickoff_idx = $4",
638        )
639        .bind(OutPointDB(deposit_outpoint))
640        .bind(XOnlyPublicKeyDB(kickoff_data.operator_xonly_pk))
641        .bind(kickoff_data.round_idx.to_index() as i32)
642        .bind(kickoff_data.kickoff_idx as i32);
643
644        let result: Result<(TxidDB,), sqlx::Error> =
645            execute_query_with_tx!(self.connection, tx, query, fetch_one);
646
647        match result {
648            Ok((txid,)) => Ok(Some(txid.0)),
649            Err(sqlx::Error::RowNotFound) => Ok(None),
650            Err(e) => Err(BridgeError::DatabaseError(e)),
651        }
652    }
653
654    /// Retrieves the light client proof for a deposit to be used while sending an assert.
655    pub async fn get_lcp_for_assert(
656        &self,
657        tx: Option<DatabaseTransaction<'_>>,
658        deposit_id: u32,
659    ) -> Result<Option<Receipt>, BridgeError> {
660        let query = sqlx::query_as::<_, (ReceiptDB,)>(
661            "SELECT lcp_receipt FROM lcp_for_asserts WHERE deposit_id = $1;",
662        )
663        .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?);
664
665        let result = execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
666
667        Ok(result.map(|(lcp,)| lcp.0))
668    }
669
670    /// Saves the light client proof for a deposit to be used while sending an assert.
671    /// We save first before sending kickoff to be sure we have the LCP available if we need to assert.
672    pub async fn insert_lcp_for_assert(
673        &self,
674        tx: Option<DatabaseTransaction<'_>>,
675        deposit_id: u32,
676        lcp: Receipt,
677    ) -> Result<(), BridgeError> {
678        let query = sqlx::query(
679            "INSERT INTO lcp_for_asserts (deposit_id, lcp_receipt)
680             VALUES ($1, $2)
681             ON CONFLICT (deposit_id) DO NOTHING;",
682        )
683        .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?)
684        .bind(ReceiptDB(lcp));
685
686        execute_query_with_tx!(self.connection, tx, query, execute)?;
687
688        Ok(())
689    }
690
691    pub async fn get_deposit_data_with_kickoff_txid(
692        &self,
693        tx: Option<DatabaseTransaction<'_>>,
694        kickoff_txid: Txid,
695    ) -> Result<Option<(DepositData, KickoffData)>, BridgeError> {
696        let query = sqlx::query_as::<_, (DepositParamsDB, XOnlyPublicKeyDB, i32, i32)>(
697            "SELECT d.deposit_params, ds.operator_xonly_pk, ds.round_idx, ds.kickoff_idx
698             FROM deposit_signatures ds
699             INNER JOIN deposits d ON d.deposit_id = ds.deposit_id
700             WHERE ds.kickoff_txid = $1;",
701        )
702        .bind(TxidDB(kickoff_txid));
703
704        let result = execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
705
706        match result {
707            Some((deposit_params, operator_xonly_pk, round_idx, kickoff_idx)) => Ok(Some((
708                deposit_params
709                    .0
710                    .try_into()
711                    .wrap_err("Can't convert deposit params")?,
712                KickoffData {
713                    operator_xonly_pk: operator_xonly_pk.0,
714                    round_idx: RoundIndex::from_index(
715                        usize::try_from(round_idx)
716                            .wrap_err("Failed to convert round idx to usize")?,
717                    ),
718                    kickoff_idx: u32::try_from(kickoff_idx)
719                        .wrap_err("Failed to convert kickoff idx to u32")?,
720                },
721            ))),
722            None => Ok(None),
723        }
724    }
725
726    /// Sets BitVM setup data for a specific operator and deposit combination.
727    /// This function additionally checks if the BitVM setup data already exists in the db.
728    /// As we don't want to overwrite BitVM setup data on the db, as maliciously overwriting
729    /// can prevent us to regenerate previously signed kickoff tx's.
730    pub async fn insert_bitvm_setup_if_not_exists(
731        &self,
732        mut tx: Option<DatabaseTransaction<'_>>,
733        operator_xonly_pk: XOnlyPublicKey,
734        deposit_outpoint: OutPoint,
735        assert_tx_addrs: impl AsRef<[[u8; 32]]>,
736        root_hash: &[u8; 32],
737        latest_blockhash_root_hash: &[u8; 32],
738    ) -> Result<(), BridgeError> {
739        let deposit_id = self
740            .get_deposit_id(tx.as_deref_mut(), deposit_outpoint)
741            .await?;
742
743        let query = sqlx::query(
744            "INSERT INTO bitvm_setups (xonly_pk, deposit_id, assert_tx_addrs, root_hash, latest_blockhash_root_hash)
745             VALUES ($1, $2, $3, $4, $5)
746             ON CONFLICT (xonly_pk, deposit_id) DO NOTHING;",
747        )
748        .bind(XOnlyPublicKeyDB(operator_xonly_pk))
749        .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?)
750        .bind(
751            assert_tx_addrs
752                .as_ref()
753                .iter()
754                .map(|addr| addr.as_ref())
755                .collect::<Vec<&[u8]>>(),
756        )
757        .bind(root_hash.to_vec())
758        .bind(latest_blockhash_root_hash.to_vec());
759
760        let result = execute_query_with_tx!(self.connection, tx.as_deref_mut(), query, execute)?;
761
762        // If no rows were affected, data already exists - check if it matches
763        if result.rows_affected() == 0 {
764            let existing = self
765                .get_bitvm_setup(tx, operator_xonly_pk, deposit_outpoint)
766                .await?;
767            if let Some((existing_addrs, existing_root, existing_blockhash)) = existing {
768                let new_addrs = assert_tx_addrs.as_ref();
769                if existing_addrs != new_addrs
770                    || existing_root != *root_hash
771                    || existing_blockhash != *latest_blockhash_root_hash
772                {
773                    return Err(BridgeError::BitvmSetupDataMismatch(
774                        operator_xonly_pk,
775                        deposit_outpoint,
776                    ));
777                }
778            }
779        }
780
781        Ok(())
782    }
783
784    /// Retrieves BitVM setup data for a specific operator, sequential collateral tx and kickoff index combination
785    pub async fn get_bitvm_setup(
786        &self,
787        mut tx: Option<DatabaseTransaction<'_>>,
788        operator_xonly_pk: XOnlyPublicKey,
789        deposit_outpoint: OutPoint,
790    ) -> Result<Option<BitvmSetup>, BridgeError> {
791        let deposit_id = self
792            .get_deposit_id(tx.as_deref_mut(), deposit_outpoint)
793            .await?;
794        let query = sqlx::query_as::<_, (Vec<Vec<u8>>, Vec<u8>, Vec<u8>)>(
795            "SELECT assert_tx_addrs, root_hash, latest_blockhash_root_hash
796             FROM bitvm_setups
797             WHERE xonly_pk = $1 AND deposit_id = $2;",
798        )
799        .bind(XOnlyPublicKeyDB(operator_xonly_pk))
800        .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?);
801
802        let result = execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
803
804        match result {
805            Some((assert_tx_addrs, root_hash, latest_blockhash_root_hash)) => {
806                // Convert root_hash Vec<u8> back to [u8; 32]
807                let root_hash_array: [u8; 32] = root_hash
808                    .try_into()
809                    .map_err(|_| eyre::eyre!("root_hash must be 32 bytes"))?;
810                let latest_blockhash_root_hash_array: [u8; 32] = latest_blockhash_root_hash
811                    .try_into()
812                    .map_err(|_| eyre::eyre!("latest_blockhash_root_hash must be 32 bytes"))?;
813
814                let assert_tx_addrs: Vec<[u8; 32]> = assert_tx_addrs
815                    .into_iter()
816                    .map(|addr| {
817                        let mut addr_array = [0u8; 32];
818                        addr_array.copy_from_slice(&addr);
819                        addr_array
820                    })
821                    .collect();
822
823                Ok(Some((
824                    assert_tx_addrs,
825                    root_hash_array,
826                    latest_blockhash_root_hash_array,
827                )))
828            }
829            None => Ok(None),
830        }
831    }
832
833    pub async fn mark_kickoff_connector_as_used(
834        &self,
835        tx: Option<DatabaseTransaction<'_>>,
836        round_idx: RoundIndex,
837        kickoff_connector_idx: u32,
838        kickoff_txid: Option<Txid>,
839    ) -> Result<(), BridgeError> {
840        let query = sqlx::query(
841            "INSERT INTO used_kickoff_connectors (round_idx, kickoff_connector_idx, kickoff_txid)
842             VALUES ($1, $2, $3)
843             ON CONFLICT (round_idx, kickoff_connector_idx) DO NOTHING;",
844        )
845        .bind(round_idx.to_index() as i32)
846        .bind(
847            i32::try_from(kickoff_connector_idx)
848                .wrap_err("Failed to convert kickoff connector idx to i32")?,
849        )
850        .bind(kickoff_txid.map(TxidDB));
851
852        execute_query_with_tx!(self.connection, tx, query, execute)?;
853
854        Ok(())
855    }
856
857    pub async fn get_kickoff_connector_for_kickoff_txid(
858        &self,
859        tx: Option<DatabaseTransaction<'_>>,
860        kickoff_txid: Txid,
861    ) -> Result<(RoundIndex, u32), BridgeError> {
862        let query = sqlx::query_as::<_, (i32, i32)>(
863            "SELECT round_idx, kickoff_connector_idx FROM used_kickoff_connectors WHERE kickoff_txid = $1;",
864        )
865        .bind(TxidDB(kickoff_txid));
866
867        let result: (i32, i32) = execute_query_with_tx!(self.connection, tx, query, fetch_one)?;
868        Ok((
869            RoundIndex::from_index(
870                result
871                    .0
872                    .try_into()
873                    .wrap_err(BridgeError::IntConversionError)?,
874            ),
875            result
876                .1
877                .try_into()
878                .wrap_err(BridgeError::IntConversionError)?,
879        ))
880    }
881
882    pub async fn get_kickoff_txid_for_used_kickoff_connector(
883        &self,
884        tx: Option<DatabaseTransaction<'_>>,
885        round_idx: RoundIndex,
886        kickoff_connector_idx: u32,
887    ) -> Result<Option<Txid>, BridgeError> {
888        let query = sqlx::query_as::<_, (Option<TxidDB>,)>(
889            "SELECT kickoff_txid FROM used_kickoff_connectors WHERE round_idx = $1 AND kickoff_connector_idx = $2;",
890        )
891        .bind(round_idx.to_index() as i32)
892        .bind(i32::try_from(kickoff_connector_idx).wrap_err("Failed to convert kickoff connector idx to i32")?);
893
894        let result = execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
895
896        match result {
897            Some((txid,)) => Ok(txid.map(|txid| txid.0)),
898            None => Ok(None),
899        }
900    }
901
902    pub async fn get_unused_and_signed_kickoff_connector(
903        &self,
904        tx: Option<DatabaseTransaction<'_>>,
905        deposit_id: u32,
906        operator_xonly_pk: XOnlyPublicKey,
907    ) -> Result<Option<(RoundIndex, u32)>, BridgeError> {
908        let query = sqlx::query_as::<_, (i32, i32)>(
909            "WITH current_round AS (
910                    SELECT round_idx
911                    FROM current_round_index
912                    WHERE id = 1
913                )
914                SELECT
915                    ds.round_idx as round_idx,
916                    ds.kickoff_idx as kickoff_connector_idx
917                FROM deposit_signatures ds
918                CROSS JOIN current_round cr
919                WHERE ds.deposit_id = $1  -- Parameter for deposit_id
920                    AND ds.operator_xonly_pk = $2
921                    AND ds.round_idx >= cr.round_idx
922                    AND NOT EXISTS (
923                        SELECT 1
924                        FROM used_kickoff_connectors ukc
925                        WHERE ukc.round_idx = ds.round_idx
926                        AND ukc.kickoff_connector_idx = ds.kickoff_idx
927                    )
928                ORDER BY ds.round_idx ASC
929                LIMIT 1;",
930        )
931        .bind(i32::try_from(deposit_id).wrap_err("Failed to convert deposit id to i32")?)
932        .bind(XOnlyPublicKeyDB(operator_xonly_pk));
933
934        let result = execute_query_with_tx!(self.connection, tx, query, fetch_optional)?;
935
936        match result {
937            Some((round_idx, kickoff_connector_idx)) => Ok(Some((
938                RoundIndex::from_index(
939                    usize::try_from(round_idx).wrap_err("Failed to convert round idx to u32")?,
940                ),
941                u32::try_from(kickoff_connector_idx)
942                    .wrap_err("Failed to convert kickoff connector idx to u32")?,
943            ))),
944            None => Ok(None),
945        }
946    }
947
948    pub async fn get_current_round_index(
949        &self,
950        tx: Option<DatabaseTransaction<'_>>,
951    ) -> Result<RoundIndex, BridgeError> {
952        let query =
953            sqlx::query_as::<_, (i32,)>("SELECT round_idx FROM current_round_index WHERE id = 1");
954        let result = execute_query_with_tx!(self.connection, tx, query, fetch_one)?;
955        Ok(RoundIndex::from_index(
956            usize::try_from(result.0).wrap_err(BridgeError::IntConversionError)?,
957        ))
958    }
959
960    pub async fn update_current_round_index(
961        &self,
962        tx: Option<DatabaseTransaction<'_>>,
963        round_idx: RoundIndex,
964    ) -> Result<(), BridgeError> {
965        let query = sqlx::query("UPDATE current_round_index SET round_idx = $1 WHERE id = 1")
966            .bind(round_idx.to_index() as i32);
967
968        execute_query_with_tx!(self.connection, tx, query, execute)?;
969
970        Ok(())
971    }
972}
973
974#[cfg(test)]
975mod tests {
976    use crate::bitvm_client::{SECP, UNSPENDABLE_XONLY_PUBKEY};
977    use crate::operator::Operator;
978    use crate::rpc::clementine::{
979        DepositSignatures, NormalSignatureKind, NumberedSignatureKind, TaggedSignature,
980    };
981    use crate::test::common::citrea::MockCitreaClient;
982    use crate::{database::Database, test::common::*};
983    use bitcoin::hashes::Hash;
984    use bitcoin::key::constants::SCHNORR_SIGNATURE_SIZE;
985    use bitcoin::key::Keypair;
986    use bitcoin::{Address, OutPoint, Txid, XOnlyPublicKey};
987    use clementine_primitives::RoundIndex;
988    use std::str::FromStr;
989
990    #[tokio::test]
991    async fn test_set_get_operator() {
992        let config = create_test_config_with_thread_name().await;
993        let database = Database::new(&config).await.unwrap();
994        let mut ops = Vec::new();
995        let operator_xonly_pks = [generate_random_xonly_pk(), generate_random_xonly_pk()];
996        let reimburse_addrs = [
997            Address::from_str("bc1q6d6cztycxjpm7p882emln0r04fjqt0kqylvku2")
998                .unwrap()
999                .assume_checked(),
1000            Address::from_str("bc1qj2mw4uh24qf67kn4nyqfsnta0mmxcutvhkyfp9")
1001                .unwrap()
1002                .assume_checked(),
1003        ];
1004        for i in 0..2 {
1005            let txid_str =
1006                format!("16b3a5951cb816afeb9dab8a30d0ece7acd3a7b34437436734edd1b72b6bf0{i:02x}");
1007            let txid = Txid::from_str(&txid_str).unwrap();
1008            ops.push((
1009                operator_xonly_pks[i],
1010                reimburse_addrs[i].clone(),
1011                OutPoint {
1012                    txid,
1013                    vout: i as u32,
1014                },
1015            ));
1016        }
1017
1018        // Test inserting multiple operators
1019        for x in ops.iter() {
1020            database
1021                .insert_operator_if_not_exists(None, x.0, &x.1, x.2)
1022                .await
1023                .unwrap();
1024        }
1025
1026        // Test getting all operators
1027        let res = database.get_operators(None).await.unwrap();
1028        assert_eq!(res.len(), ops.len());
1029        for i in 0..2 {
1030            assert_eq!(res[i].0, ops[i].0);
1031            assert_eq!(res[i].1, ops[i].1);
1032            assert_eq!(res[i].2, ops[i].2);
1033        }
1034
1035        // Test getting single operator
1036        let res_single = database
1037            .get_operator(None, operator_xonly_pks[1])
1038            .await
1039            .unwrap()
1040            .unwrap();
1041        assert_eq!(res_single.xonly_pk, ops[1].0);
1042        assert_eq!(res_single.reimburse_addr, ops[1].1);
1043        assert_eq!(res_single.collateral_funding_outpoint, ops[1].2);
1044
1045        // Test that we can insert the same data without errors
1046        database
1047            .insert_operator_if_not_exists(None, ops[0].0, &ops[0].1, ops[0].2)
1048            .await
1049            .unwrap();
1050
1051        // Test updating operator data
1052        let new_reimburse_addr = Address::from_str("bc1qj2mw4uh24qf67kn4nyqfsnta0mmxcutvhkyfp9")
1053            .unwrap()
1054            .assume_checked();
1055        let new_collateral_funding_outpoint = OutPoint {
1056            txid: Txid::from_byte_array([2u8; 32]),
1057            vout: 1,
1058        };
1059
1060        // test that we can't update the reimburse address
1061        assert!(database
1062            .insert_operator_if_not_exists(
1063                None,
1064                operator_xonly_pks[0],
1065                &reimburse_addrs[0],
1066                new_collateral_funding_outpoint
1067            )
1068            .await
1069            .is_err());
1070
1071        // test that we can't update the collateral funding outpoint
1072        assert!(database
1073            .insert_operator_if_not_exists(
1074                None,
1075                operator_xonly_pks[0],
1076                &new_reimburse_addr,
1077                ops[0].2
1078            )
1079            .await
1080            .is_err());
1081
1082        // test that we can't update both
1083        assert!(database
1084            .insert_operator_if_not_exists(
1085                None,
1086                operator_xonly_pks[0],
1087                &new_reimburse_addr,
1088                new_collateral_funding_outpoint
1089            )
1090            .await
1091            .is_err());
1092
1093        // Verify data remains unchanged after failed updates
1094        let res_unchanged = database
1095            .get_operator(None, operator_xonly_pks[0])
1096            .await
1097            .unwrap()
1098            .unwrap();
1099        assert_eq!(res_unchanged.xonly_pk, ops[0].0);
1100        assert_eq!(res_unchanged.reimburse_addr, ops[0].1);
1101        assert_eq!(res_unchanged.collateral_funding_outpoint, ops[0].2);
1102    }
1103
1104    #[tokio::test]
1105    async fn test_set_get_operator_challenge_ack_hashes() {
1106        let config = create_test_config_with_thread_name().await;
1107        let database = Database::new(&config).await.unwrap();
1108
1109        let public_hashes = vec![[1u8; 20], [2u8; 20]];
1110        let new_public_hashes = vec![[3u8; 20], [4u8; 20]];
1111
1112        let deposit_outpoint = OutPoint {
1113            txid: Txid::from_byte_array([1u8; 32]),
1114            vout: 0,
1115        };
1116
1117        let operator_xonly_pk = generate_random_xonly_pk();
1118        let non_existent_xonly_pk = generate_random_xonly_pk();
1119
1120        // Test inserting new data
1121        database
1122            .insert_operator_challenge_ack_hashes_if_not_exist(
1123                None,
1124                operator_xonly_pk,
1125                deposit_outpoint,
1126                &public_hashes,
1127            )
1128            .await
1129            .unwrap();
1130
1131        // Retrieve and verify
1132        let result = database
1133            .get_operators_challenge_ack_hashes(None, operator_xonly_pk, deposit_outpoint)
1134            .await
1135            .unwrap();
1136        assert_eq!(result, Some(public_hashes.clone()));
1137
1138        // Test that we can insert the same data without errors
1139        database
1140            .insert_operator_challenge_ack_hashes_if_not_exist(
1141                None,
1142                operator_xonly_pk,
1143                deposit_outpoint,
1144                &public_hashes,
1145            )
1146            .await
1147            .unwrap();
1148
1149        // Test non-existent entry
1150        let non_existent = database
1151            .get_operators_challenge_ack_hashes(None, non_existent_xonly_pk, deposit_outpoint)
1152            .await
1153            .unwrap();
1154        assert!(non_existent.is_none());
1155
1156        // Test that we can't update with different data
1157        assert!(database
1158            .insert_operator_challenge_ack_hashes_if_not_exist(
1159                None,
1160                operator_xonly_pk,
1161                deposit_outpoint,
1162                &new_public_hashes,
1163            )
1164            .await
1165            .is_err());
1166
1167        // Verify data remains unchanged after failed update
1168        let result = database
1169            .get_operators_challenge_ack_hashes(None, operator_xonly_pk, deposit_outpoint)
1170            .await
1171            .unwrap();
1172        assert_eq!(result, Some(public_hashes));
1173    }
1174
1175    #[tokio::test]
1176    async fn test_save_get_unspent_kickoff_sigs() {
1177        let config = create_test_config_with_thread_name().await;
1178        let database = Database::new(&config).await.unwrap();
1179
1180        let round_idx = 1;
1181        let signatures = DepositSignatures {
1182            signatures: vec![
1183                TaggedSignature {
1184                    signature_id: Some((NumberedSignatureKind::UnspentKickoff1, 1).into()),
1185                    signature: vec![0x1F; SCHNORR_SIGNATURE_SIZE],
1186                },
1187                TaggedSignature {
1188                    signature_id: Some((NumberedSignatureKind::UnspentKickoff2, 1).into()),
1189                    signature: (vec![0x2F; SCHNORR_SIGNATURE_SIZE]),
1190                },
1191                TaggedSignature {
1192                    signature_id: Some((NumberedSignatureKind::UnspentKickoff1, 2).into()),
1193                    signature: vec![0x1F; SCHNORR_SIGNATURE_SIZE],
1194                },
1195                TaggedSignature {
1196                    signature_id: Some((NumberedSignatureKind::UnspentKickoff2, 2).into()),
1197                    signature: (vec![0x2F; SCHNORR_SIGNATURE_SIZE]),
1198                },
1199            ],
1200        };
1201
1202        let operator_xonly_pk = generate_random_xonly_pk();
1203        let non_existent_xonly_pk = generate_random_xonly_pk();
1204
1205        database
1206            .insert_unspent_kickoff_sigs_if_not_exist(
1207                None,
1208                operator_xonly_pk,
1209                RoundIndex::Round(round_idx),
1210                signatures.signatures.clone(),
1211            )
1212            .await
1213            .unwrap();
1214
1215        let result = database
1216            .get_unspent_kickoff_sigs(None, operator_xonly_pk, RoundIndex::Round(round_idx))
1217            .await
1218            .unwrap()
1219            .unwrap();
1220        assert_eq!(result, signatures.signatures);
1221
1222        let non_existent = database
1223            .get_unspent_kickoff_sigs(None, non_existent_xonly_pk, RoundIndex::Round(round_idx))
1224            .await
1225            .unwrap();
1226        assert!(non_existent.is_none());
1227
1228        let non_existent = database
1229            .get_unspent_kickoff_sigs(
1230                None,
1231                non_existent_xonly_pk,
1232                RoundIndex::Round(round_idx + 1),
1233            )
1234            .await
1235            .unwrap();
1236        assert!(non_existent.is_none());
1237    }
1238
1239    #[tokio::test]
1240    async fn test_bitvm_setup() {
1241        let config = create_test_config_with_thread_name().await;
1242        let database = Database::new(&config).await.unwrap();
1243
1244        let assert_tx_hashes: Vec<[u8; 32]> = vec![[1u8; 32], [4u8; 32]];
1245        let root_hash = [42u8; 32];
1246        let latest_blockhash_root_hash = [43u8; 32];
1247
1248        let deposit_outpoint = OutPoint {
1249            txid: Txid::from_byte_array([1u8; 32]),
1250            vout: 0,
1251        };
1252        let operator_xonly_pk = generate_random_xonly_pk();
1253        let non_existent_xonly_pk = generate_random_xonly_pk();
1254
1255        // Test inserting new BitVM setup
1256        database
1257            .insert_bitvm_setup_if_not_exists(
1258                None,
1259                operator_xonly_pk,
1260                deposit_outpoint,
1261                &assert_tx_hashes,
1262                &root_hash,
1263                &latest_blockhash_root_hash,
1264            )
1265            .await
1266            .unwrap();
1267
1268        // Retrieve and verify
1269        let result = database
1270            .get_bitvm_setup(None, operator_xonly_pk, deposit_outpoint)
1271            .await
1272            .unwrap()
1273            .unwrap();
1274        assert_eq!(result.0, assert_tx_hashes);
1275        assert_eq!(result.1, root_hash);
1276        assert_eq!(result.2, latest_blockhash_root_hash);
1277
1278        // Test that we can insert the same data without errors
1279        database
1280            .insert_bitvm_setup_if_not_exists(
1281                None,
1282                operator_xonly_pk,
1283                deposit_outpoint,
1284                &assert_tx_hashes,
1285                &root_hash,
1286                &latest_blockhash_root_hash,
1287            )
1288            .await
1289            .unwrap();
1290
1291        // Test non-existent entry
1292        let non_existent = database
1293            .get_bitvm_setup(None, non_existent_xonly_pk, deposit_outpoint)
1294            .await
1295            .unwrap();
1296        assert!(non_existent.is_none());
1297
1298        // Test updating BitVM setup data
1299        let new_assert_tx_hashes: Vec<[u8; 32]> = vec![[2u8; 32], [5u8; 32]];
1300        let new_root_hash = [44u8; 32];
1301        let new_latest_blockhash_root_hash = [45u8; 32];
1302
1303        // test that we can't update the assert_tx_hashes
1304        assert!(database
1305            .insert_bitvm_setup_if_not_exists(
1306                None,
1307                operator_xonly_pk,
1308                deposit_outpoint,
1309                &new_assert_tx_hashes,
1310                &root_hash,
1311                &latest_blockhash_root_hash,
1312            )
1313            .await
1314            .is_err());
1315
1316        // test that we can't update the root_hash
1317        assert!(database
1318            .insert_bitvm_setup_if_not_exists(
1319                None,
1320                operator_xonly_pk,
1321                deposit_outpoint,
1322                &assert_tx_hashes,
1323                &new_root_hash,
1324                &latest_blockhash_root_hash,
1325            )
1326            .await
1327            .is_err());
1328
1329        // test that we can't update the latest_blockhash_root_hash
1330        assert!(database
1331            .insert_bitvm_setup_if_not_exists(
1332                None,
1333                operator_xonly_pk,
1334                deposit_outpoint,
1335                &assert_tx_hashes,
1336                &root_hash,
1337                &new_latest_blockhash_root_hash,
1338            )
1339            .await
1340            .is_err());
1341
1342        // test that we can't update all of them
1343        assert!(database
1344            .insert_bitvm_setup_if_not_exists(
1345                None,
1346                operator_xonly_pk,
1347                deposit_outpoint,
1348                &new_assert_tx_hashes,
1349                &new_root_hash,
1350                &new_latest_blockhash_root_hash,
1351            )
1352            .await
1353            .is_err());
1354
1355        // Verify data remains unchanged after failed updates
1356        let result = database
1357            .get_bitvm_setup(None, operator_xonly_pk, deposit_outpoint)
1358            .await
1359            .unwrap()
1360            .unwrap();
1361        assert_eq!(result.0, assert_tx_hashes);
1362        assert_eq!(result.1, root_hash);
1363        assert_eq!(result.2, latest_blockhash_root_hash);
1364    }
1365
1366    #[tokio::test]
1367    async fn upsert_get_operator_winternitz_public_keys() {
1368        let mut config = create_test_config_with_thread_name().await;
1369        let database = Database::new(&config).await.unwrap();
1370        let _regtest = create_regtest_rpc(&mut config).await;
1371
1372        let operator = Operator::<MockCitreaClient>::new(config.clone())
1373            .await
1374            .unwrap();
1375        let op_xonly_pk =
1376            XOnlyPublicKey::from_keypair(&Keypair::from_secret_key(&SECP, &config.secret_key)).0;
1377        let deposit_outpoint = OutPoint {
1378            txid: Txid::from_slice(&[0x45; 32]).unwrap(),
1379            vout: 0x1F,
1380        };
1381        let wpks = operator
1382            .generate_assert_winternitz_pubkeys(deposit_outpoint)
1383            .unwrap();
1384
1385        // Test inserting new data
1386        database
1387            .insert_operator_kickoff_winternitz_public_keys_if_not_exist(
1388                None,
1389                op_xonly_pk,
1390                wpks.clone(),
1391            )
1392            .await
1393            .unwrap();
1394
1395        let result = database
1396            .get_operator_kickoff_winternitz_public_keys(None, op_xonly_pk)
1397            .await
1398            .unwrap();
1399        assert_eq!(result, wpks);
1400
1401        // Test that we can insert the same data without errors
1402        database
1403            .insert_operator_kickoff_winternitz_public_keys_if_not_exist(
1404                None,
1405                op_xonly_pk,
1406                wpks.clone(),
1407            )
1408            .await
1409            .unwrap();
1410
1411        // Test that we can't update with different data
1412        let different_wpks = operator
1413            .generate_assert_winternitz_pubkeys(OutPoint {
1414                txid: Txid::from_slice(&[0x46; 32]).unwrap(),
1415                vout: 0x1F,
1416            })
1417            .unwrap();
1418        assert!(database
1419            .insert_operator_kickoff_winternitz_public_keys_if_not_exist(
1420                None,
1421                op_xonly_pk,
1422                different_wpks
1423            )
1424            .await
1425            .is_err());
1426
1427        let non_existent = database
1428            .get_operator_kickoff_winternitz_public_keys(None, *UNSPENDABLE_XONLY_PUBKEY)
1429            .await;
1430        assert!(non_existent.is_err());
1431    }
1432
1433    #[tokio::test]
1434    async fn upsert_get_operator_bitvm_wpks() {
1435        let mut config = create_test_config_with_thread_name().await;
1436        let database = Database::new(&config).await.unwrap();
1437        let _regtest = create_regtest_rpc(&mut config).await;
1438
1439        let operator = Operator::<MockCitreaClient>::new(config.clone())
1440            .await
1441            .unwrap();
1442        let op_xonly_pk =
1443            XOnlyPublicKey::from_keypair(&Keypair::from_secret_key(&SECP, &config.secret_key)).0;
1444        let deposit_outpoint = OutPoint {
1445            txid: Txid::from_slice(&[0x45; 32]).unwrap(),
1446            vout: 0x1F,
1447        };
1448        let wpks = operator
1449            .generate_assert_winternitz_pubkeys(deposit_outpoint)
1450            .unwrap();
1451
1452        database
1453            .insert_operator_bitvm_keys_if_not_exist(
1454                None,
1455                op_xonly_pk,
1456                deposit_outpoint,
1457                wpks.clone(),
1458            )
1459            .await
1460            .unwrap();
1461
1462        let result = database
1463            .get_operator_bitvm_keys(None, op_xonly_pk, deposit_outpoint)
1464            .await
1465            .unwrap();
1466        assert_eq!(result, wpks);
1467
1468        let non_existent = database
1469            .get_operator_kickoff_winternitz_public_keys(None, *UNSPENDABLE_XONLY_PUBKEY)
1470            .await;
1471        assert!(non_existent.is_err());
1472    }
1473
1474    #[tokio::test]
1475    async fn upsert_get_deposit_signatures() {
1476        let config = create_test_config_with_thread_name().await;
1477        let database = Database::new(&config).await.unwrap();
1478
1479        let operator_xonly_pk = generate_random_xonly_pk();
1480        let unset_operator_xonly_pk = generate_random_xonly_pk();
1481        let deposit_outpoint = OutPoint {
1482            txid: Txid::from_slice(&[0x45; 32]).unwrap(),
1483            vout: 0x1F,
1484        };
1485        let round_idx = 1;
1486        let kickoff_idx = 1;
1487        let signatures = DepositSignatures {
1488            signatures: vec![
1489                TaggedSignature {
1490                    signature_id: Some(NormalSignatureKind::Reimburse1.into()),
1491                    signature: vec![0x1F; SCHNORR_SIGNATURE_SIZE],
1492                },
1493                TaggedSignature {
1494                    signature_id: Some((NumberedSignatureKind::OperatorChallengeNack1, 1).into()),
1495                    signature: (vec![0x2F; SCHNORR_SIGNATURE_SIZE]),
1496                },
1497            ],
1498        };
1499
1500        database
1501            .insert_deposit_signatures_if_not_exist(
1502                None,
1503                deposit_outpoint,
1504                operator_xonly_pk,
1505                RoundIndex::Round(round_idx),
1506                kickoff_idx,
1507                Txid::all_zeros(),
1508                signatures.signatures.clone(),
1509            )
1510            .await
1511            .unwrap();
1512        // Setting this twice should not cause any issues
1513        database
1514            .insert_deposit_signatures_if_not_exist(
1515                None,
1516                deposit_outpoint,
1517                operator_xonly_pk,
1518                RoundIndex::Round(round_idx),
1519                kickoff_idx,
1520                Txid::all_zeros(),
1521                signatures.signatures.clone(),
1522            )
1523            .await
1524            .unwrap();
1525        // But with different kickoff txid and signatures should.
1526        assert!(database
1527            .insert_deposit_signatures_if_not_exist(
1528                None,
1529                deposit_outpoint,
1530                operator_xonly_pk,
1531                RoundIndex::Round(round_idx),
1532                kickoff_idx,
1533                Txid::from_slice(&[0x1F; 32]).unwrap(),
1534                signatures.signatures.clone(),
1535            )
1536            .await
1537            .is_err());
1538
1539        let result = database
1540            .get_deposit_signatures(
1541                None,
1542                deposit_outpoint,
1543                operator_xonly_pk,
1544                RoundIndex::Round(round_idx),
1545                kickoff_idx,
1546            )
1547            .await
1548            .unwrap()
1549            .unwrap();
1550        assert_eq!(result, signatures.signatures);
1551
1552        let non_existent = database
1553            .get_deposit_signatures(
1554                None,
1555                deposit_outpoint,
1556                operator_xonly_pk,
1557                RoundIndex::Round(round_idx + 1),
1558                kickoff_idx + 1,
1559            )
1560            .await
1561            .unwrap();
1562        assert!(non_existent.is_none());
1563
1564        let non_existent = database
1565            .get_deposit_signatures(
1566                None,
1567                OutPoint::null(),
1568                unset_operator_xonly_pk,
1569                RoundIndex::Round(round_idx),
1570                kickoff_idx,
1571            )
1572            .await
1573            .unwrap();
1574        assert!(non_existent.is_none());
1575    }
1576
1577    #[tokio::test]
1578    async fn concurrent_get_deposit_id_same_outpoint() {
1579        // this test was added to ensure get_deposit_id will not block if two different transactions only read the deposit_id
1580        use tokio::time::{timeout, Duration};
1581
1582        let config = create_test_config_with_thread_name().await;
1583        let database = Database::new(&config).await.unwrap();
1584
1585        let deposit_outpoint = OutPoint {
1586            txid: Txid::from_byte_array([7u8; 32]),
1587            vout: 0,
1588        };
1589        let mut first_insert = database.begin_transaction().await.unwrap();
1590        // insert the deposit outpoint into the database
1591        let original_id = database
1592            .get_deposit_id(Some(&mut first_insert), deposit_outpoint)
1593            .await
1594            .unwrap();
1595        first_insert.commit().await.unwrap();
1596
1597        let mut tx1 = database.begin_transaction().await.unwrap();
1598        let mut tx2 = database.begin_transaction().await.unwrap();
1599
1600        let id = database
1601            .get_deposit_id(Some(&mut tx1), deposit_outpoint)
1602            .await
1603            .unwrap();
1604
1605        let id2 = timeout(
1606            Duration::from_secs(30),
1607            database.get_deposit_id(Some(&mut tx2), deposit_outpoint),
1608        )
1609        .await
1610        .unwrap()
1611        .unwrap();
1612
1613        tx1.commit().await.unwrap();
1614        tx2.commit().await.unwrap();
1615
1616        assert_eq!(id, id2, "both transactions should see the same deposit id");
1617        assert_eq!(
1618            id, original_id,
1619            "new transaction should see the same deposit id as the original"
1620        );
1621    }
1622}