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