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