1use 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];
31pub type AssertTxHash = Vec<[u8; 32]>;
33
34pub type BitvmSetup = (AssertTxHash, RootHash, RootHash);
35
36impl Database {
37 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 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 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; 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 let addr = bitcoin::Address::from_str(&addr)
115 .wrap_err("Invalid Address")?
116 .assume_checked();
117 let outpoint = outpoint_db.0; Ok(Some(OperatorData {
119 xonly_pk: operator_xonly_pk,
120 reimburse_addr: addr,
121 collateral_funding_outpoint: outpoint,
122 }))
123 }
124 }
125 }
126
127 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 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 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 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 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 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 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 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 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 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), }
355 }
356
357 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 database
1017 .insert_operator_if_not_exists(None, ops[0].0, &ops[0].1, ops[0].2)
1018 .await
1019 .unwrap();
1020
1021 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}