clementine_core/builder/
sighash.rs

1//! # Sighash Builder
2//!
3//! This module provides functions and types for constructing signature hashes (sighashes) for the transactions in the Clementine bridge protocol.
4//! Sighash is the message that is signed by the private key of the signer.
5//!
6//! The module supports generating sighash streams for both N-of-N (verifier) and operator signatures, as well as utilities for signature identification and protocol-specific signature requirements.
7//! As the number of transactions can reach around 100_000 depending on number of entities in the protocol, we generate the sighashes in a stream to avoid memory issues.
8//!
9//! ## Responsibilities
10//!
11//! - Calculate the number of required signatures for various protocol roles and transaction types.
12//! - Generate sighash streams for all protocol-required signatures for a deposit, for both verifiers and operators.
13//! - Provide types for tracking signature requirements and spend paths.
14//!
15//! ## Key Types for Signatures
16//!
17//! - [`PartialSignatureInfo`] - Identifies a signature by operator, round, and kickoff index.
18//! - [`SignatureInfo`] - Uniquely identifies a signature, including spend path of the signature.
19//! - [`TapTweakData`] - Describes the spend path (key or script) and any required tweak data.
20//!
21//! For more on sighash types, see: <https://developer.bitcoin.org/devguide/transactions.html?highlight=sighash#signature-hash-types>
22
23use crate::bitvm_client;
24use crate::builder::transaction::deposit_signature_owner::EntityType;
25use crate::builder::transaction::sign::get_kickoff_utxos_to_sign;
26use crate::builder::transaction::{
27    create_txhandlers, ContractContext, ReimburseDbCache, TxHandlerCache,
28};
29use crate::config::BridgeConfig;
30use crate::database::Database;
31use crate::deposit::{DepositData, KickoffData};
32use crate::rpc::clementine::tagged_signature::SignatureId;
33use crate::rpc::clementine::NormalSignatureKind;
34use async_stream::try_stream;
35use bitcoin::hashes::Hash;
36use bitcoin::{TapSighash, XOnlyPublicKey};
37use clementine_errors::BridgeError;
38use clementine_errors::TransactionType;
39use clementine_primitives::RoundIndex;
40use futures_core::stream::Stream;
41
42impl BridgeConfig {
43    /// Returns the number of required signatures for N-of-N signing session.
44    ///
45    /// # Arguments
46    /// * `deposit_data` - The deposit data for which to calculate required signatures.
47    ///
48    /// # Returns
49    /// The number of required N-of-N signatures for the deposit.
50    pub fn get_num_required_nofn_sigs(&self, deposit_data: &DepositData) -> usize {
51        deposit_data.get_num_operators()
52            * self.protocol_paramset().num_round_txs
53            * self.protocol_paramset().num_signed_kickoffs
54            * self.get_num_required_nofn_sigs_per_kickoff(deposit_data)
55    }
56
57    /// Returns the number of required operator signatures for a deposit.
58    ///
59    /// # Arguments
60    /// * `deposit_data` - The deposit data for which to calculate required signatures.
61    ///
62    /// # Returns
63    /// The number of required operator signatures for the deposit.
64    pub fn get_num_required_operator_sigs(&self, deposit_data: &DepositData) -> usize {
65        self.protocol_paramset().num_round_txs
66            * self.protocol_paramset().num_signed_kickoffs
67            * self.get_num_required_operator_sigs_per_kickoff(deposit_data)
68    }
69
70    /// Returns the number of required N-of-N signatures per kickoff for a deposit.
71    ///
72    /// # Arguments
73    /// * `deposit_data` - The deposit data for which to calculate required signatures per kickoff.
74    ///
75    /// # Returns
76    /// The number of required N-of-N signatures per kickoff.
77    pub fn get_num_required_nofn_sigs_per_kickoff(&self, deposit_data: &DepositData) -> usize {
78        7 + 4 * deposit_data.get_num_verifiers()
79            + bitvm_client::ClementineBitVMPublicKeys::number_of_assert_txs() * 2
80    }
81
82    /// Returns the number of required operator signatures per kickoff for a deposit.
83    ///
84    /// # Arguments
85    /// * `deposit_data` - The deposit data for which to calculate required signatures per kickoff.
86    ///
87    /// # Returns
88    /// The number of required operator signatures per kickoff.
89    pub fn get_num_required_operator_sigs_per_kickoff(&self, deposit_data: &DepositData) -> usize {
90        4 + bitvm_client::ClementineBitVMPublicKeys::number_of_assert_txs()
91            + deposit_data.get_num_verifiers()
92    }
93
94    /// Returns the total number of Winternitz public keys used in kickoff UTXOs for blockhash commits.
95    ///
96    /// # Returns
97    /// The number of Winternitz public keys required for all rounds and kickoffs.
98    pub fn get_num_kickoff_winternitz_pks(&self) -> usize {
99        self.protocol_paramset().num_kickoffs_per_round
100            * (self.protocol_paramset().num_round_txs + 1) // we need num_round_txs + 1 because we need one extra round tx to generate the reimburse connectors of the actual last round
101    }
102
103    /// Returns the total number of unspent kickoff signatures needed from each operator.
104    ///
105    /// # Returns
106    /// The number of unspent kickoff signatures required for all rounds from one operator.
107    pub fn get_num_unspent_kickoff_sigs(&self) -> usize {
108        self.protocol_paramset().num_round_txs * self.protocol_paramset().num_kickoffs_per_round * 2
109    }
110
111    /// Returns the number of challenge ack hashes needed for a single operator for each round.
112    ///
113    /// # Arguments
114    /// * `deposit_data` - The deposit data for which to calculate required challenge ack hashes.
115    ///
116    /// # Returns
117    /// The number of challenge ack hashes required for the deposit.
118    pub fn get_num_challenge_ack_hashes(&self, deposit_data: &DepositData) -> usize {
119        deposit_data.get_num_watchtowers()
120    }
121
122    // /// Returns the number of winternitz pks needed for a single operator for each round
123    // pub fn get_num_assert_winternitz_pks(&self) -> usize {
124    //     crate::utils::BITVM_CACHE.num_intermediate_variables
125    // }
126}
127
128/// Identifies a signature by operator, round, and kickoff index.
129#[derive(Copy, Clone, Debug)]
130pub struct PartialSignatureInfo {
131    pub operator_idx: usize,
132    pub round_idx: RoundIndex,
133    pub kickoff_utxo_idx: usize,
134}
135
136/// information about the spend path that is needed to sign the utxo.
137pub use clementine_utils::TapTweakData;
138
139/// Contains information to uniquely identify a single signature in the deposit.
140/// operator_idx, round_idx, and kickoff_utxo_idx uniquely identify a kickoff.
141/// signature_id uniquely identifies a signature in that specific kickoff.
142/// tweak_data contains information about the spend path that is needed to sign the utxo.
143/// kickoff_txid is the txid of the kickoff tx the signature belongs to. This is not actually needed for the signature, it is only used to
144/// pass the kickoff txid to the caller of the sighash streams in this module.
145#[derive(Copy, Clone, Debug)]
146pub struct SignatureInfo {
147    pub operator_idx: usize,
148    pub round_idx: RoundIndex,
149    pub kickoff_utxo_idx: usize,
150    pub signature_id: SignatureId,
151    pub tweak_data: TapTweakData,
152    pub kickoff_txid: Option<bitcoin::Txid>,
153}
154
155impl PartialSignatureInfo {
156    pub fn new(
157        operator_idx: usize,
158        round_idx: RoundIndex,
159        kickoff_utxo_idx: usize,
160    ) -> PartialSignatureInfo {
161        PartialSignatureInfo {
162            operator_idx,
163            round_idx,
164            kickoff_utxo_idx,
165        }
166    }
167    /// Completes the partial info with a signature id and spend path data.
168    pub fn complete(&self, signature_id: SignatureId, spend_data: TapTweakData) -> SignatureInfo {
169        SignatureInfo {
170            operator_idx: self.operator_idx,
171            round_idx: self.round_idx,
172            kickoff_utxo_idx: self.kickoff_utxo_idx,
173            signature_id,
174            tweak_data: spend_data,
175            kickoff_txid: None,
176        }
177    }
178    /// Completes the partial info with a kickoff txid (for yielding kickoff txid in sighash streams).
179    pub fn complete_with_kickoff_txid(&self, kickoff_txid: bitcoin::Txid) -> SignatureInfo {
180        SignatureInfo {
181            operator_idx: self.operator_idx,
182            round_idx: self.round_idx,
183            kickoff_utxo_idx: self.kickoff_utxo_idx,
184            signature_id: NormalSignatureKind::YieldKickoffTxid.into(),
185            tweak_data: TapTweakData::ScriptPath,
186            kickoff_txid: Some(kickoff_txid),
187        }
188    }
189}
190
191/// Generates the sighash stream for all N-of-N (verifier) signatures required for a deposit. See [clementine whitepaper](https://citrea.xyz/clementine_whitepaper.pdf) for details on the transactions.
192///
193/// For a given deposit, for each operator and round, generates the sighash stream for all protocol-required transactions.
194/// If `yield_kickoff_txid` is true, yields the kickoff txid as a special entry.
195///
196/// # Arguments
197/// * `db` - Database handle.
198/// * `config` - Bridge configuration.
199/// * `deposit_data` - Deposit data for which to generate sighashes.
200/// * `deposit_blockhash` - Block hash of the deposit.
201/// * `yield_kickoff_txid` - Whether to yield the kickoff txid as a special entry.
202///
203/// # Returns
204///
205/// An async stream of ([`TapSighash`], [`SignatureInfo`]) pairs, or [`BridgeError`] on failure.
206pub fn create_nofn_sighash_stream(
207    db: Database,
208    config: BridgeConfig,
209    deposit_data: DepositData,
210    deposit_blockhash: bitcoin::BlockHash,
211    yield_kickoff_txid: bool,
212) -> impl Stream<Item = Result<(TapSighash, SignatureInfo), BridgeError>> {
213    try_stream! {
214        let paramset = config.protocol_paramset();
215
216        let operators = deposit_data.get_operators();
217
218        for (operator_idx, op_xonly_pk) in
219            operators.iter().enumerate()
220        {
221
222            let utxo_idxs = get_kickoff_utxos_to_sign(
223                config.protocol_paramset(),
224                *op_xonly_pk,
225                deposit_blockhash,
226                deposit_data.get_deposit_outpoint(),
227            );
228            // need to create new TxHandlerDbData for each operator
229            let mut tx_db_data = ReimburseDbCache::new_for_deposit(db.clone(), *op_xonly_pk, deposit_data.get_deposit_outpoint(), config.protocol_paramset(), None);
230
231            let mut txhandler_cache = TxHandlerCache::new();
232
233            for round_idx in RoundIndex::iter_rounds(paramset.num_round_txs) {
234                // For each round, we have multiple kickoff_utxos to sign for the deposit.
235                for &kickoff_idx in &utxo_idxs {
236                    let partial = PartialSignatureInfo::new(operator_idx, round_idx, kickoff_idx);
237
238                    let context = ContractContext::new_context_for_kickoff(
239                        KickoffData {
240                            operator_xonly_pk: *op_xonly_pk,
241                            round_idx,
242                            kickoff_idx: kickoff_idx as u32,
243                        },
244                        deposit_data.clone(),
245                        config.protocol_paramset(),
246                    );
247
248                    let mut txhandlers = create_txhandlers(
249                        TransactionType::AllNeededForDeposit,
250                        context,
251                        &mut txhandler_cache,
252                        &mut tx_db_data,
253                    ).await?;
254
255                    let mut sum = 0;
256                    let mut kickoff_txid = None;
257                    for (tx_type, txhandler) in txhandlers.iter() {
258                        let sighashes = txhandler.calculate_shared_txins_sighash(EntityType::VerifierDeposit, partial)?;
259                        sum += sighashes.len();
260                        for sighash in sighashes {
261                            yield sighash;
262                        }
263                        if tx_type == &TransactionType::Kickoff {
264                            kickoff_txid = Some(txhandler.get_txid());
265                        }
266                    }
267
268                    match (yield_kickoff_txid, kickoff_txid) {
269                        (true, Some(kickoff_txid)) => {
270                            yield (TapSighash::all_zeros(), partial.complete_with_kickoff_txid(*kickoff_txid));
271                        }
272                        (true, None) => {
273                            Err(eyre::eyre!("Kickoff txid not found in sighash stream"))?;
274                        }
275                        _ => {}
276                    }
277
278
279                    if sum != config.get_num_required_nofn_sigs_per_kickoff(&deposit_data) {
280                        Err(eyre::eyre!("NofN sighash count does not match: expected {0}, got {1}", config.get_num_required_nofn_sigs_per_kickoff(&deposit_data), sum))?;
281                    }
282                    // recollect round_tx, ready_to_reimburse_tx, and move_to_vault_tx for the next kickoff_utxo
283                    txhandler_cache.store_for_next_kickoff(&mut txhandlers)?;
284                }
285                // collect the last ready_to_reimburse txhandler for the next round
286                txhandler_cache.store_for_next_round()?;
287            }
288        }
289    }
290}
291
292/// Generates the sighash stream for all operator signatures required for a deposit. These signatures required by the operators are
293/// the signatures needed to burn the collateral of the operators, only able to be burned if the operator is malicious.
294/// See [clementine whitepaper](https://citrea.xyz/clementine_whitepaper.pdf) for details on the transactions.
295///
296/// # Arguments
297/// * `db` - Database handle.
298/// * `operator_xonly_pk` - X-only public key of the operator.
299/// * `config` - Bridge configuration.
300/// * `deposit_data` - Deposit data for which to generate sighashes.
301/// * `deposit_blockhash` - Block hash of the deposit.
302///
303/// # Returns
304///
305/// An async stream of (sighash, [`SignatureInfo`]) pairs, or [`BridgeError`] on failure.
306// Possible future optimization: Each verifier already generates some of these TX's in create_nofn_sighash_stream()
307// It is possible to for verifiers somehow return the required sighashes for operator signatures there too. But operators only needs to use sighashes included in this function.
308pub fn create_operator_sighash_stream(
309    db: Database,
310    operator_xonly_pk: XOnlyPublicKey,
311    config: BridgeConfig,
312    deposit_data: DepositData,
313    deposit_blockhash: bitcoin::BlockHash,
314) -> impl Stream<Item = Result<(TapSighash, SignatureInfo), BridgeError>> {
315    try_stream! {
316        let mut tx_db_data = ReimburseDbCache::new_for_deposit(db.clone(), operator_xonly_pk, deposit_data.get_deposit_outpoint(), config.protocol_paramset(), None);
317
318        let operator = db.get_operator(None, operator_xonly_pk).await?;
319
320        let operator = match operator {
321            Some(operator) => operator,
322            None => Err(BridgeError::OperatorNotFound(operator_xonly_pk))?,
323        };
324
325        let utxo_idxs = get_kickoff_utxos_to_sign(
326            config.protocol_paramset(),
327            operator.xonly_pk,
328            deposit_blockhash,
329            deposit_data.get_deposit_outpoint(),
330        );
331
332        let paramset = config.protocol_paramset();
333        let mut txhandler_cache = TxHandlerCache::new();
334        let operator_idx = deposit_data.get_operator_index(operator_xonly_pk)?;
335
336        // For each round_tx, we have multiple kickoff_utxos as the connectors.
337        for round_idx in RoundIndex::iter_rounds(paramset.num_round_txs) {
338            for &kickoff_idx in &utxo_idxs {
339                let partial = PartialSignatureInfo::new(operator_idx, round_idx, kickoff_idx);
340
341                let context = ContractContext::new_context_for_kickoff(
342                    KickoffData {
343                        operator_xonly_pk,
344                        round_idx,
345                        kickoff_idx: kickoff_idx as u32,
346                    },
347                    deposit_data.clone(),
348                    config.protocol_paramset(),
349                );
350
351                let mut txhandlers = create_txhandlers(
352                    TransactionType::AllNeededForDeposit,
353                    context,
354                    &mut txhandler_cache,
355                    &mut tx_db_data,
356                ).await?;
357
358                let mut sum = 0;
359                for (_, txhandler) in txhandlers.iter() {
360                    let sighashes = txhandler.calculate_shared_txins_sighash(EntityType::OperatorDeposit, partial)?;
361                    sum += sighashes.len();
362                    for sighash in sighashes {
363                        yield sighash;
364                    }
365                }
366                if sum != config.get_num_required_operator_sigs_per_kickoff(&deposit_data) {
367                    Err(eyre::eyre!("Operator sighash count does not match: expected {0}, got {1}", config.get_num_required_operator_sigs_per_kickoff(&deposit_data), sum))?;
368                }
369                // recollect round_tx, ready_to_reimburse_tx, and move_to_vault_tx for the next kickoff_utxo
370                txhandler_cache.store_for_next_kickoff(&mut txhandlers)?;
371            }
372            // collect the last ready_to_reimburse txhandler for the next round
373            txhandler_cache.store_for_next_round()?;
374        }
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use crate::{
382        bitvm_client::SECP,
383        builder::transaction::sign::TransactionRequestData,
384        config::protocol::ProtocolParamset,
385        deposit::{Actors, DepositInfo, OperatorData},
386        extended_bitcoin_rpc::ExtendedBitcoinRpc,
387        rpc::clementine::{
388            clementine_operator_client::ClementineOperatorClient, TransactionRequest,
389        },
390        test::common::{
391            citrea::MockCitreaClient, create_actors, create_regtest_rpc,
392            create_test_config_with_thread_name, run_single_deposit,
393            tx_utils::get_tx_from_signed_txs_with_type,
394        },
395    };
396    use bincode;
397    use bitcoin::hashes::sha256;
398    use bitcoin::secp256k1::PublicKey;
399    use bitcoin::{Block, BlockHash, OutPoint, Txid};
400    use bitcoincore_rpc::RpcApi;
401    use futures_util::stream::TryStreamExt;
402    use std::fs::File;
403
404    #[cfg(debug_assertions)]
405    pub const DEPOSIT_STATE_FILE_PATH_DEBUG: &str = "src/test/data/deposit_state_debug.bincode";
406    #[cfg(not(debug_assertions))]
407    pub const DEPOSIT_STATE_FILE_PATH_RELEASE: &str = "src/test/data/deposit_state_release.bincode";
408
409    /// State of the chain and the deposit generated in generate_deposit_state() test.
410    /// Contains:
411    /// - Blocks: All blocks from height 1 until the chain tip.
412    /// - Deposit info: Deposit info of the deposit that were signed.
413    /// - Deposit blockhash: Block hash of the deposit outpoint.
414    /// - Move txid: Move to vault txid of the deposit.
415    /// - Operator data: Operator data of the single operator that were used in the deposit.
416    /// - Round tx txid hash: Hash of all round tx txids of the operator.
417    /// - Nofn sighash hash: Hash of all nofn sighashes of the deposit.
418    /// - Operator sighash hash: Hash of all operator sighashes of the deposit.
419    #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
420    struct DepositChainState {
421        blocks: Vec<Block>,
422        deposit_info: DepositInfo,
423        deposit_blockhash: BlockHash,
424        move_txid: Txid,
425        operator_data: OperatorData,
426        round_tx_txid_hash: sha256::Hash,
427        nofn_sighash_hash: sha256::Hash,
428        operator_sighash_hash: sha256::Hash,
429    }
430
431    /// To make the [`test_bridge_contract_change`] test work if breaking changes are expected, run this test again
432    /// (with both debug and release), the states will get updated with the current values.
433    /// Read [`test_bridge_contract_change`] test doc for more details.
434    #[cfg(feature = "automation")]
435    #[tokio::test]
436    #[ignore = "Run this to generate fresh deposit state data, in case any breaking change occurs to deposits"]
437    async fn generate_deposit_state() {
438        let mut config = create_test_config_with_thread_name().await;
439        // only run with one operator
440        config.test_params.all_operators_secret_keys.truncate(1);
441
442        let regtest = create_regtest_rpc(&mut config).await;
443        let rpc = regtest.rpc().clone();
444
445        let actors = create_actors::<MockCitreaClient>(&config).await;
446        let (deposit_info, move_txid, deposit_blockhash, verifiers_public_keys) =
447            run_single_deposit::<MockCitreaClient>(&mut config, rpc.clone(), None, &actors, None)
448                .await
449                .unwrap();
450
451        // get generated blocks
452        let height = rpc.get_current_chain_height().await.unwrap();
453        let mut blocks = Vec::new();
454        for i in 1..=height {
455            let (blockhash, _) = rpc.get_block_info_by_height(i as u64).await.unwrap();
456            let block = rpc.get_block(&blockhash).await.unwrap();
457            blocks.push(block);
458        }
459
460        let op0_config = BridgeConfig {
461            secret_key: config.test_params.all_verifiers_secret_keys[0],
462            db_name: config.db_name + "0",
463            ..config
464        };
465
466        let operators_xonly_pks = op0_config
467            .test_params
468            .all_operators_secret_keys
469            .iter()
470            .map(|sk| sk.x_only_public_key(&SECP).0)
471            .collect::<Vec<_>>();
472
473        let op0_xonly_pk = operators_xonly_pks[0];
474
475        let db = Database::new(&op0_config).await.unwrap();
476        let operator_data = db.get_operator(None, op0_xonly_pk).await.unwrap().unwrap();
477
478        let (nofn_sighash_hash, operator_sighash_hash) = calculate_hash_of_sighashes(
479            deposit_info.clone(),
480            verifiers_public_keys,
481            operators_xonly_pks.clone(),
482            op0_config.clone(),
483            deposit_blockhash,
484        )
485        .await;
486
487        let operator = actors.get_operator_client_by_index(0);
488
489        let round_tx_txid_hash = compute_hash_of_round_txs(
490            operator,
491            deposit_info.deposit_outpoint,
492            operators_xonly_pks[0],
493            deposit_blockhash,
494            op0_config.protocol_paramset(),
495        )
496        .await;
497
498        let deposit_state = DepositChainState {
499            blocks,
500            deposit_blockhash,
501            move_txid,
502            deposit_info,
503            operator_data,
504            round_tx_txid_hash,
505            nofn_sighash_hash,
506            operator_sighash_hash,
507        };
508
509        #[cfg(debug_assertions)]
510        let file_path = DEPOSIT_STATE_FILE_PATH_DEBUG;
511        #[cfg(not(debug_assertions))]
512        let file_path = DEPOSIT_STATE_FILE_PATH_RELEASE;
513
514        // save to file
515        let file = File::create(file_path).unwrap();
516        bincode::serialize_into(file, &deposit_state).unwrap();
517    }
518
519    async fn load_deposit_state(rpc: &ExtendedBitcoinRpc) -> DepositChainState {
520        tracing::debug!(
521            "Current chain height: {}",
522            rpc.get_current_chain_height().await.unwrap()
523        );
524        #[cfg(debug_assertions)]
525        let file_path = DEPOSIT_STATE_FILE_PATH_DEBUG;
526        #[cfg(not(debug_assertions))]
527        let file_path = DEPOSIT_STATE_FILE_PATH_RELEASE;
528
529        let file = File::open(file_path).unwrap();
530        let deposit_state: DepositChainState = bincode::deserialize_from(file).unwrap();
531
532        // submit blocks to current rpc
533        for block in &deposit_state.blocks {
534            rpc.submit_block(block).await.unwrap();
535        }
536        deposit_state
537    }
538
539    /// Returns the hash of all round txs txids for a given operator.
540    async fn compute_hash_of_round_txs(
541        mut operator: ClementineOperatorClient<tonic::transport::Channel>,
542        deposit_outpoint: OutPoint,
543        operator_xonly_pk: XOnlyPublicKey,
544        deposit_blockhash: bitcoin::BlockHash,
545        paramset: &'static ProtocolParamset,
546    ) -> sha256::Hash {
547        let kickoff_utxo = get_kickoff_utxos_to_sign(
548            paramset,
549            operator_xonly_pk,
550            deposit_blockhash,
551            deposit_outpoint,
552        )[0];
553
554        let mut all_round_txids = Vec::new();
555        for i in 0..paramset.num_round_txs {
556            let tx_req = TransactionRequestData {
557                deposit_outpoint,
558                kickoff_data: KickoffData {
559                    operator_xonly_pk,
560                    round_idx: RoundIndex::Round(i),
561                    kickoff_idx: kickoff_utxo as u32,
562                },
563            };
564            let signed_txs = operator
565                .internal_create_signed_txs(TransactionRequest::from(tx_req))
566                .await
567                .unwrap()
568                .into_inner();
569            let round_tx =
570                get_tx_from_signed_txs_with_type(&signed_txs, TransactionType::Round).unwrap();
571            all_round_txids.push(round_tx.compute_txid());
572        }
573
574        sha256::Hash::hash(&all_round_txids.concat())
575    }
576
577    /// Calculates the hash of all nofn and operator sighashes for a given deposit.
578    async fn calculate_hash_of_sighashes(
579        deposit_info: DepositInfo,
580        verifiers_public_keys: Vec<PublicKey>,
581        operators_xonly_pks: Vec<XOnlyPublicKey>,
582        op0_config: BridgeConfig,
583        deposit_blockhash: bitcoin::BlockHash,
584    ) -> (sha256::Hash, sha256::Hash) {
585        let deposit_data = DepositData {
586            nofn_xonly_pk: None,
587            deposit: deposit_info,
588            actors: Actors {
589                verifiers: verifiers_public_keys,
590                watchtowers: vec![],
591                operators: operators_xonly_pks.clone(),
592            },
593            security_council: op0_config.security_council.clone(),
594        };
595
596        let db = Database::new(&op0_config).await.unwrap();
597
598        let sighash_stream = create_nofn_sighash_stream(
599            db.clone(),
600            op0_config.clone(),
601            deposit_data.clone(),
602            deposit_blockhash,
603            true,
604        );
605
606        let nofn_sighashes: Vec<_> = sighash_stream.try_collect().await.unwrap();
607        let nofn_sighashes = nofn_sighashes
608            .into_iter()
609            .map(|(sighash, _info)| sighash.to_byte_array())
610            .collect::<Vec<_>>();
611
612        let operator_streams = create_operator_sighash_stream(
613            db.clone(),
614            operators_xonly_pks[0],
615            op0_config.clone(),
616            deposit_data.clone(),
617            deposit_blockhash,
618        );
619
620        let operator_sighashes: Vec<_> = operator_streams.try_collect().await.unwrap();
621        let operator_sighashes = operator_sighashes
622            .into_iter()
623            .map(|(sighash, _info)| sighash.to_byte_array())
624            .collect::<Vec<_>>();
625
626        // Hash the vectors
627        let nofn_hash = sha256::Hash::hash(&nofn_sighashes.concat());
628        let operator_hash = sha256::Hash::hash(&operator_sighashes.concat());
629
630        (nofn_hash, operator_hash)
631    }
632
633    /// Test for checking if the sighash stream is changed due to changes in code.
634    /// If this test fails, the code contains breaking changes that needs replacement deposits on deployment.
635    /// It is also possible that round tx's are changed, which is a bigger issue. In addition to replacement deposits,
636    /// the collaterals of operators that created at least round 1 are unusable.
637    ///
638    /// Its also possible for this test to fail if default config is changed(for example num_verifiers, operators, etc).
639    ///
640    /// This test only uses one operator, because it is hard (too much code duplication) with
641    /// current test setup fn's to generate operators with different configs (config has the
642    /// reimburse address and collateral funding outpoint, which should be loaded from the saved
643    /// deposit state)
644    ///
645    /// To make the test work if breaking changes are expected, run generate_deposit_state() test again
646    /// (with both debug and release), it will get updated with the current values. Run following commands:
647    /// debug: cargo test --all-features generate_deposit_state -- --ignored
648    /// release: cargo test --all-features --release generate_deposit_state -- --ignored
649    /// If test_bridge_contract_change failed on github CI, CI also uploads the deposit state file as an artifact, so it can be downloaded
650    /// and committed to the repo.
651    #[cfg(feature = "automation")]
652    #[tokio::test]
653    async fn test_bridge_contract_change() {
654        let mut config = create_test_config_with_thread_name().await;
655        // only run with one operator
656        config.test_params.all_operators_secret_keys.truncate(1);
657
658        // do not generate to address
659        config.test_params.generate_to_address = false;
660
661        let regtest = create_regtest_rpc(&mut config).await;
662        let rpc = regtest.rpc().clone();
663
664        let deposit_state = load_deposit_state(&rpc).await;
665
666        // set operator reimbursement address and collateral funding outpoint to the ones from the saved deposit state
667        config.operator_reimbursement_address = Some(
668            deposit_state
669                .operator_data
670                .reimburse_addr
671                .as_unchecked()
672                .to_owned(),
673        );
674        config.operator_collateral_funding_outpoint =
675            Some(deposit_state.operator_data.collateral_funding_outpoint);
676
677        // after loading generate some funds to rpc wallet
678        // needed so that the deposit doesn't crash (I don't know why) due to insufficient funds
679        let address = rpc
680            .get_new_address(None, None)
681            .await
682            .expect("Failed to get new address");
683
684        rpc.generate_to_address(105, address.assume_checked_ref())
685            .await
686            .expect("Failed to generate blocks");
687
688        let actors = create_actors::<MockCitreaClient>(&config).await;
689        let (deposit_info, move_txid, deposit_blockhash, verifiers_public_keys) =
690            run_single_deposit::<MockCitreaClient>(
691                &mut config,
692                rpc.clone(),
693                None,
694                &actors,
695                Some(deposit_state.deposit_info.deposit_outpoint),
696            )
697            .await
698            .unwrap();
699
700        // sanity checks, these should be equal if the deposit state saved is still valid
701        // if not a new deposit state needs to be generated
702        assert_eq!(move_txid, deposit_state.move_txid);
703        assert_eq!(deposit_blockhash, deposit_state.deposit_blockhash);
704        assert_eq!(deposit_info, deposit_state.deposit_info);
705
706        let op0_config = BridgeConfig {
707            secret_key: config.test_params.all_verifiers_secret_keys[0],
708            db_name: config.db_name.clone() + "0",
709            ..config.clone()
710        };
711
712        let operators_xonly_pks = op0_config
713            .test_params
714            .all_operators_secret_keys
715            .iter()
716            .map(|sk| sk.x_only_public_key(&SECP).0)
717            .collect::<Vec<_>>();
718
719        let operator = actors.get_operator_client_by_index(0);
720
721        let round_tx_hash = compute_hash_of_round_txs(
722            operator,
723            deposit_info.deposit_outpoint,
724            operators_xonly_pks[0],
725            deposit_blockhash,
726            op0_config.protocol_paramset(),
727        )
728        .await;
729
730        // If this fails, the round txs are changed.
731        assert_eq!(
732            round_tx_hash, deposit_state.round_tx_txid_hash,
733            "Round tx hash does not match the previous values, round txs are changed"
734        );
735
736        let (nofn_hash, operator_hash) = calculate_hash_of_sighashes(
737            deposit_info,
738            verifiers_public_keys,
739            operators_xonly_pks,
740            op0_config,
741            deposit_blockhash,
742        )
743        .await;
744
745        // If these fail, the bridge contract is changed.
746        assert_eq!(
747            nofn_hash, deposit_state.nofn_sighash_hash,
748            "NofN sighashes do not match the previous values"
749        );
750        assert_eq!(
751            operator_hash, deposit_state.operator_sighash_hash,
752            "Operator sighashes do not match the previous values"
753        );
754    }
755}