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