clementine_core/builder/transaction/
sign.rs

1//! # Transaction Signing Utilities
2//!
3//! This module provides logic signing the transactions used in the Clementine bridge.
4
5use super::challenge::create_watchtower_challenge_txhandler;
6use super::{ContractContext, TxHandlerCache};
7use crate::actor::{Actor, TweakCache, WinternitzDerivationPath};
8use crate::bitvm_client::ClementineBitVMPublicKeys;
9use crate::builder;
10use crate::builder::transaction::creator::ReimburseDbCache;
11use crate::citrea::CitreaClientT;
12use crate::config::protocol::ProtocolParamset;
13use crate::config::BridgeConfig;
14use crate::database::{Database, DatabaseTransaction};
15use crate::deposit::KickoffData;
16use crate::operator::Operator;
17use crate::utils::{Last20Bytes, RbfSigningInfo};
18use crate::verifier::Verifier;
19use bitcoin::hashes::Hash;
20use bitcoin::{BlockHash, OutPoint, Transaction, XOnlyPublicKey};
21use clementine_errors::BridgeError;
22use clementine_errors::{TransactionType, TxError};
23use clementine_primitives::RoundIndex;
24use eyre::Context;
25use rand_chacha::rand_core::SeedableRng;
26use rand_chacha::ChaCha12Rng;
27use secp256k1::rand::seq::SliceRandom;
28
29/// Data to identify the deposit and kickoff.
30#[derive(Debug, Clone)]
31pub struct TransactionRequestData {
32    pub deposit_outpoint: OutPoint,
33    pub kickoff_data: KickoffData,
34}
35
36/// Deterministically (given same seed) generates a set of kickoff indices for an operator to sign, using the operator's public key, deposit block hash, and deposit outpoint as the seed.
37/// To make the output consistent across versions, a fixed rng algorithm (ChaCha12Rng) is used.
38///
39/// This function creates a deterministic seed from the operator's public key, deposit block hash,
40/// and deposit outpoint, then uses it to select a subset of kickoff indices.
41/// deposit_blockhash is also included in the seed to ensure the randomness of the selected kickoff indices, otherwise deposit_outpoint
42/// can be selected in a way to create hash collisions by the user depositing.
43///
44/// # Arguments
45/// * `paramset` - Protocol parameter set.
46/// * `op_xonly_pk` - Operator's x-only public key.
47/// * `deposit_blockhash` - Block hash of the block containing the deposit.
48/// * `deposit_outpoint` - Outpoint of the deposit.
49///
50/// # Returns
51/// A vector of indices that the operator should sign, with the count determined by the protocol parameter `num_signed_kickoffs`.
52pub fn get_kickoff_utxos_to_sign(
53    paramset: &'static ProtocolParamset,
54    op_xonly_pk: XOnlyPublicKey,
55    deposit_blockhash: BlockHash,
56    deposit_outpoint: bitcoin::OutPoint,
57) -> Vec<usize> {
58    let deposit_data = [
59        op_xonly_pk.serialize().to_vec(),
60        deposit_blockhash.to_byte_array().to_vec(),
61        deposit_outpoint.txid.to_byte_array().to_vec(),
62        deposit_outpoint.vout.to_le_bytes().to_vec(),
63    ]
64    .concat();
65
66    let seed = bitcoin::hashes::sha256d::Hash::hash(&deposit_data).to_byte_array();
67    let mut rng = ChaCha12Rng::from_seed(seed);
68
69    let mut numbers: Vec<usize> = (0..paramset.num_kickoffs_per_round).collect();
70    numbers.shuffle(&mut rng);
71
72    numbers
73        .into_iter()
74        .take(paramset.num_signed_kickoffs)
75        .collect()
76}
77
78/// Creates and signs all transaction types that can be signed by the entity.
79///
80/// This function handles the creation and signing of transactions based on the provided
81/// transaction data. It returns a vector of signed transactions with their corresponding types.
82///
83/// # Note
84/// This function should not be used for transaction types that require special handling:
85/// - MiniAsserts
86/// - WatchtowerChallenge
87/// - LatestBlockhash
88/// - Disprove
89///
90/// These transaction types have their own specialized signing flows.
91pub async fn create_and_sign_txs(
92    db: Database,
93    signer: &Actor,
94    config: BridgeConfig,
95    context: ContractContext,
96    block_hash: Option<[u8; 20]>, //to sign kickoff
97    dbtx: Option<DatabaseTransaction<'_>>,
98) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
99    let txhandlers = builder::transaction::create_txhandlers(
100        match context.is_context_for_kickoff() {
101            true => TransactionType::AllNeededForDeposit,
102            // if context is only for a round, we can only sign the round txs
103            false => TransactionType::Round,
104        },
105        context.clone(),
106        &mut TxHandlerCache::new(),
107        &mut match context.is_context_for_kickoff() {
108            true => ReimburseDbCache::new_for_deposit(
109                db.clone(),
110                context.operator_xonly_pk,
111                context
112                    .deposit_data
113                    .as_ref()
114                    .expect("Already checked existence of deposit data")
115                    .get_deposit_outpoint(),
116                config.protocol_paramset(),
117                dbtx,
118            ),
119            false => ReimburseDbCache::new_for_rounds(
120                db.clone(),
121                context.operator_xonly_pk,
122                config.protocol_paramset(),
123                dbtx,
124            ),
125        },
126    )
127    .await?;
128
129    let mut signatures = Vec::new();
130
131    if context.is_context_for_kickoff() {
132        // signatures saved during deposit
133        let deposit_sigs_query = db
134            .get_deposit_signatures(
135                None,
136                context
137                    .deposit_data
138                    .as_ref()
139                    .expect("Should have deposit data at this point")
140                    .get_deposit_outpoint(),
141                context.operator_xonly_pk,
142                context.round_idx,
143                context
144                    .kickoff_idx
145                    .expect("Already checked existence of kickoff idx") as usize,
146            )
147            .await?;
148        signatures.extend(deposit_sigs_query.unwrap_or_default());
149    }
150
151    // signatures saved during setup
152    let setup_sigs_query = db
153        .get_unspent_kickoff_sigs(None, context.operator_xonly_pk, context.round_idx)
154        .await?;
155
156    signatures.extend(setup_sigs_query.unwrap_or_default());
157
158    let mut signed_txs = Vec::with_capacity(txhandlers.len());
159    let mut tweak_cache = TweakCache::default();
160
161    for (tx_type, mut txhandler) in txhandlers.into_iter() {
162        let _ = signer
163            .tx_sign_and_fill_sigs(&mut txhandler, &signatures, Some(&mut tweak_cache))
164            .wrap_err(format!(
165                "Couldn't sign transaction {tx_type:?} in create_and_sign_txs for context {context:?}"
166            ));
167
168        if let TransactionType::OperatorChallengeAck(watchtower_idx) = tx_type {
169            let path = WinternitzDerivationPath::ChallengeAckHash(
170                watchtower_idx as u32,
171                context
172                    .deposit_data
173                    .as_ref()
174                    .expect("Should have deposit data at this point")
175                    .get_deposit_outpoint(),
176                config.protocol_paramset(),
177            );
178            let preimage = signer.generate_preimage_from_path(path)?;
179            let _ = signer.tx_sign_preimage(&mut txhandler, preimage);
180        }
181
182        if let TransactionType::Kickoff = tx_type {
183            if let Some(block_hash) = block_hash {
184                // need to commit blockhash to start kickoff
185                let path = WinternitzDerivationPath::Kickoff(
186                    context.round_idx,
187                    context
188                        .kickoff_idx
189                        .expect("Should have kickoff idx at this point"),
190                    config.protocol_paramset(),
191                );
192                signer.tx_sign_winternitz(&mut txhandler, &[(block_hash.to_vec(), path)])?;
193            }
194            // do not give err if blockhash was not given
195        }
196
197        let checked_txhandler = txhandler.promote();
198
199        match checked_txhandler {
200            Ok(checked_txhandler) => {
201                signed_txs.push((tx_type, checked_txhandler.get_cached_tx().clone()));
202            }
203            Err(e) => {
204                tracing::debug!(
205                    "Couldn't sign transaction {:?} in create_and_sign_all_txs: {:?}.
206                    This might be normal if the transaction is not needed to be/cannot be signed.",
207                    tx_type,
208                    e
209                );
210            }
211        }
212    }
213
214    Ok(signed_txs)
215}
216
217impl<C> Verifier<C>
218where
219    C: CitreaClientT,
220{
221    /// Creates and signs the watchtower challenge with the given commit data.
222    ///
223    /// # Arguments
224    /// * `transaction_data` - Data to identify the deposit and kickoff.
225    /// * `commit_data` - Commit data for the watchtower challenge.
226    ///
227    /// # Returns
228    /// A tuple of:
229    ///     1. TransactionType: WatchtowerChallenge
230    ///     2. Transaction: Signed watchtower challenge transaction
231    ///     3. RbfSigningInfo: Rbf signing info for the watchtower challenge (for re-signing the transaction after a rbf input is added to the tx)
232    pub async fn create_watchtower_challenge(
233        &self,
234        transaction_data: TransactionRequestData,
235        commit_data: &[u8],
236        dbtx: Option<DatabaseTransaction<'_>>,
237    ) -> Result<(TransactionType, Transaction, RbfSigningInfo), BridgeError> {
238        if commit_data.len() != self.config.protocol_paramset().watchtower_challenge_bytes {
239            return Err(TxError::IncorrectWatchtowerChallengeDataLength.into());
240        }
241
242        let deposit_data = self
243            .db
244            .get_deposit_data(None, transaction_data.deposit_outpoint)
245            .await?
246            .ok_or(BridgeError::DepositNotFound(
247                transaction_data.deposit_outpoint,
248            ))?
249            .1;
250
251        let context = ContractContext::new_context_with_signer(
252            transaction_data.kickoff_data,
253            deposit_data.clone(),
254            self.config.protocol_paramset(),
255            self.signer.clone(),
256        );
257
258        let mut txhandlers = builder::transaction::create_txhandlers(
259            TransactionType::AllNeededForDeposit,
260            context,
261            &mut TxHandlerCache::new(),
262            &mut ReimburseDbCache::new_for_deposit(
263                self.db.clone(),
264                transaction_data.kickoff_data.operator_xonly_pk,
265                transaction_data.deposit_outpoint,
266                self.config.protocol_paramset(),
267                dbtx,
268            ),
269        )
270        .await?;
271
272        let kickoff_txhandler = txhandlers
273            .remove(&TransactionType::Kickoff)
274            .ok_or(TxError::TxHandlerNotFound(TransactionType::Kickoff))?;
275
276        let watchtower_index = deposit_data.get_watchtower_index(&self.signer.xonly_public_key)?;
277
278        let watchtower_challenge_txhandler = create_watchtower_challenge_txhandler(
279            &kickoff_txhandler,
280            watchtower_index,
281            commit_data,
282            self.config.protocol_paramset(),
283            #[cfg(test)]
284            &self.config.test_params,
285        )?;
286
287        let merkle_root = watchtower_challenge_txhandler.get_merkle_root_of_txin(0)?;
288
289        // The annex and additional_taproot_output_count fields are only used for testing.
290        // Consider using cfg(test) if struct size optimization is needed.
291        #[cfg(test)]
292        let (annex, additional_taproot_output_count) = {
293            let mut annex: Option<Vec<u8>> = None;
294            let mut additional_taproot_output_count = None;
295
296            if self.config.test_params.use_small_annex {
297                annex = Some(vec![80u8; 10000]);
298            } else if self.config.test_params.use_large_annex {
299                annex = Some(vec![80u8; 3990000]);
300            } else if self.config.test_params.use_large_annex_and_output {
301                annex = Some(vec![80u8; 3000000]);
302                additional_taproot_output_count = Some(2300);
303            } else if self.config.test_params.use_large_output {
304                additional_taproot_output_count = Some(2300);
305            }
306            (annex, additional_taproot_output_count)
307        };
308
309        #[cfg(not(test))]
310        let (annex, additional_taproot_output_count) = (None, None);
311
312        Ok((
313            TransactionType::WatchtowerChallenge(watchtower_index),
314            watchtower_challenge_txhandler.get_cached_tx().clone(),
315            RbfSigningInfo {
316                vout: 0,
317                tweak_merkle_root: merkle_root,
318                annex,
319                additional_taproot_output_count,
320            },
321        ))
322    }
323
324    /// Creates and signs all the unspent kickoff connector (using the previously saved signatures from operator during setup)
325    ///  transactions for a single round of an operator.
326    ///
327    /// # Arguments
328    /// * `round_idx` - Index of the round.
329    /// * `operator_xonly_pk` - Operator's x-only public key.
330    ///
331    /// # Returns
332    /// A vector of tuples:
333    ///     1. TransactionType: UnspentKickoff(idx) for idx'th kickoff in the round
334    ///     2. Transaction: Signed unspent kickoff connector transaction
335    pub async fn create_and_sign_unspent_kickoff_connector_txs(
336        &self,
337        round_idx: RoundIndex,
338        operator_xonly_pk: XOnlyPublicKey,
339        mut dbtx: Option<DatabaseTransaction<'_>>,
340    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
341        let context = ContractContext::new_context_for_round(
342            operator_xonly_pk,
343            round_idx,
344            self.config.protocol_paramset(),
345        );
346
347        let txhandlers = builder::transaction::create_txhandlers(
348            TransactionType::UnspentKickoff(0),
349            context,
350            &mut TxHandlerCache::new(),
351            &mut ReimburseDbCache::new_for_rounds(
352                self.db.clone(),
353                operator_xonly_pk,
354                self.config.protocol_paramset(),
355                dbtx.as_deref_mut(),
356            ),
357        )
358        .await?;
359
360        // signatures saved during setup
361        let unspent_kickoff_sigs = self
362            .db
363            .get_unspent_kickoff_sigs(dbtx, operator_xonly_pk, round_idx)
364            .await?
365            .ok_or(eyre::eyre!(
366                "No unspent kickoff signatures found for operator {:?} and round {:?}",
367                operator_xonly_pk,
368                round_idx
369            ))?;
370
371        let mut signed_txs = Vec::with_capacity(txhandlers.len());
372        let mut tweak_cache = TweakCache::default();
373
374        for (tx_type, mut txhandler) in txhandlers.into_iter() {
375            if !matches!(tx_type, TransactionType::UnspentKickoff(_)) {
376                // do not try to sign unrelated txs
377                continue;
378            }
379            let res = self.signer
380                .tx_sign_and_fill_sigs(
381                    &mut txhandler,
382                    &unspent_kickoff_sigs,
383                    Some(&mut tweak_cache),
384                )
385                .wrap_err(format!(
386                    "Couldn't sign transaction {tx_type:?} in create_and_sign_unspent_kickoff_connector_txs for round {round_idx:?} and operator {operator_xonly_pk:?}",
387                ));
388
389            let checked_txhandler = txhandler.promote();
390
391            match checked_txhandler {
392                Ok(checked_txhandler) => {
393                    signed_txs.push((tx_type, checked_txhandler.get_cached_tx().clone()));
394                }
395                Err(e) => {
396                    tracing::trace!(
397                        "Couldn't sign transaction {:?} in create_and_sign_unspent_kickoff_connector_txs: {:?}: {:?}",
398                        tx_type,
399                        e,
400                        res.err()
401                    );
402                }
403            }
404        }
405
406        Ok(signed_txs)
407    }
408}
409
410impl<C> Operator<C>
411where
412    C: CitreaClientT,
413{
414    /// Creates and signs all the assert commitment transactions for a single kickoff of an operator.
415    ///
416    /// # Arguments
417    /// * `assert_data` - Data to identify the deposit and kickoff.
418    /// * `commit_data` - BitVM assertions for the kickoff, for each assert tx.
419    ///
420    /// # Returns
421    /// A vector of tuples:
422    ///     1. TransactionType: MiniAssert(idx) for idx'th assert commitment
423    ///     2. Transaction: Signed assert commitment transaction
424    pub async fn create_assert_commitment_txs(
425        &self,
426        assert_data: TransactionRequestData,
427        commit_data: Vec<Vec<Vec<u8>>>,
428        dbtx: Option<DatabaseTransaction<'_>>,
429    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
430        let deposit_data = self
431            .db
432            .get_deposit_data(None, assert_data.deposit_outpoint)
433            .await?
434            .ok_or(BridgeError::DepositNotFound(assert_data.deposit_outpoint))?
435            .1;
436
437        let context = ContractContext::new_context_with_signer(
438            assert_data.kickoff_data,
439            deposit_data.clone(),
440            self.config.protocol_paramset(),
441            self.signer.clone(),
442        );
443
444        let mut txhandlers = builder::transaction::create_txhandlers(
445            TransactionType::MiniAssert(0),
446            context,
447            &mut TxHandlerCache::new(),
448            &mut ReimburseDbCache::new_for_deposit(
449                self.db.clone(),
450                self.signer.xonly_public_key,
451                assert_data.deposit_outpoint,
452                self.config.protocol_paramset(),
453                dbtx,
454            ),
455        )
456        .await?;
457
458        let mut signed_txhandlers = Vec::new();
459
460        for idx in 0..ClementineBitVMPublicKeys::number_of_assert_txs() {
461            let mut mini_assert_txhandler = txhandlers
462                .remove(&TransactionType::MiniAssert(idx))
463                .ok_or(TxError::TxHandlerNotFound(TransactionType::MiniAssert(idx)))?;
464            let derivations = ClementineBitVMPublicKeys::get_assert_derivations(
465                idx,
466                assert_data.deposit_outpoint,
467                self.config.protocol_paramset(),
468            );
469            // Combine data to be committed with the corresponding bitvm derivation path (needed to regenerate the winternitz secret keys
470            // to sign the transaction)
471            let winternitz_data: Vec<(Vec<u8>, WinternitzDerivationPath)> = derivations
472                .iter()
473                .zip(commit_data[idx].iter())
474                .map(|(derivation, commit_data)| match derivation {
475                    WinternitzDerivationPath::BitvmAssert(_len, _, _, _, _) => {
476                        (commit_data.clone(), derivation.clone())
477                    }
478                    _ => unreachable!(),
479                })
480                .collect();
481            self.signer
482                .tx_sign_winternitz(&mut mini_assert_txhandler, &winternitz_data)?;
483            signed_txhandlers.push(mini_assert_txhandler.promote()?);
484        }
485
486        Ok(signed_txhandlers
487            .into_iter()
488            .map(|txhandler| {
489                (
490                    txhandler.get_transaction_type(),
491                    txhandler.get_cached_tx().clone(),
492                )
493            })
494            .collect())
495    }
496
497    /// Creates and signs the latest blockhash transaction for a single kickoff of an operator.
498    ///
499    /// # Arguments
500    /// * `assert_data` - Data to identify the deposit and kickoff.
501    /// * `block_hash` - Block hash to commit using winternitz signatures.
502    ///
503    /// # Returns
504    /// A tuple of:
505    ///     1. TransactionType: LatestBlockhash
506    ///     2. Transaction: Signed latest blockhash transaction
507    pub async fn create_latest_blockhash_tx(
508        &self,
509        assert_data: TransactionRequestData,
510        block_hash: BlockHash,
511        dbtx: Option<DatabaseTransaction<'_>>,
512    ) -> Result<(TransactionType, Transaction), BridgeError> {
513        let deposit_data = self
514            .db
515            .get_deposit_data(None, assert_data.deposit_outpoint)
516            .await?
517            .ok_or(BridgeError::DepositNotFound(assert_data.deposit_outpoint))?
518            .1;
519
520        let context = ContractContext::new_context_with_signer(
521            assert_data.kickoff_data,
522            deposit_data,
523            self.config.protocol_paramset(),
524            self.signer.clone(),
525        );
526
527        let mut txhandlers = builder::transaction::create_txhandlers(
528            TransactionType::LatestBlockhash,
529            context,
530            &mut TxHandlerCache::new(),
531            &mut ReimburseDbCache::new_for_deposit(
532                self.db.clone(),
533                assert_data.kickoff_data.operator_xonly_pk,
534                assert_data.deposit_outpoint,
535                self.config.protocol_paramset(),
536                dbtx,
537            ),
538        )
539        .await?;
540
541        let mut latest_blockhash_txhandler =
542            txhandlers
543                .remove(&TransactionType::LatestBlockhash)
544                .ok_or(TxError::TxHandlerNotFound(TransactionType::LatestBlockhash))?;
545
546        let block_hash: [u8; 32] = {
547            let raw = block_hash.to_byte_array();
548
549            #[cfg(test)]
550            {
551                self.config.test_params.maybe_disrupt_block_hash(raw)
552            }
553
554            #[cfg(not(test))]
555            {
556                raw
557            }
558        };
559
560        // get last 20 bytes of block_hash
561        let block_hash_last_20 = block_hash.last_20_bytes().to_vec();
562
563        tracing::info!(
564            "Creating latest blockhash tx with block hash's last 20 bytes: {:?}",
565            block_hash_last_20
566        );
567        self.signer.tx_sign_winternitz(
568            &mut latest_blockhash_txhandler,
569            &[(
570                block_hash_last_20,
571                ClementineBitVMPublicKeys::get_latest_blockhash_derivation(
572                    assert_data.deposit_outpoint,
573                    self.config.protocol_paramset(),
574                ),
575            )],
576        )?;
577
578        let latest_blockhash_txhandler = latest_blockhash_txhandler.promote()?;
579
580        // log the block hash witness
581        tracing::info!(
582            "Latest blockhash tx created with block hash witness: {:?}",
583            latest_blockhash_txhandler.get_cached_tx().input
584        );
585
586        Ok((
587            latest_blockhash_txhandler.get_transaction_type(),
588            latest_blockhash_txhandler.get_cached_tx().to_owned(),
589        ))
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use std::str::FromStr;
596
597    use crate::test::common::create_test_config_with_thread_name;
598
599    use super::*;
600
601    #[tokio::test]
602    /// Checks if get_kickoff_utxos_to_sign returns the same values as before.
603    /// This test should never fail, do not make changes to code that changes the result of
604    /// get_kickoff_utxos_to_sign, as doing so will invalidate all past deposits.
605    async fn test_get_kickoff_utxos_to_sign_consistency() {
606        let config = create_test_config_with_thread_name().await;
607        let mut paramset = config.protocol_paramset().clone();
608        paramset.num_kickoffs_per_round = 2000;
609        paramset.num_signed_kickoffs = 20;
610        let paramset_ref: &'static ProtocolParamset = Box::leak(Box::new(paramset));
611        let op_xonly_pk = XOnlyPublicKey::from_str(
612            "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
613        )
614        .unwrap();
615        let deposit_blockhash =
616            BlockHash::from_str("0000000000000000000000000000000000000000000000000000000000000000")
617                .unwrap();
618        let deposit_outpoint = OutPoint::from_str(
619            "0000000000000000000000000000000000000000000000000000000000000000:0",
620        )
621        .unwrap();
622        let utxos_to_sign = get_kickoff_utxos_to_sign(
623            paramset_ref,
624            op_xonly_pk,
625            deposit_blockhash,
626            deposit_outpoint,
627        );
628        assert_eq!(utxos_to_sign.len(), 20);
629        assert_eq!(
630            utxos_to_sign,
631            vec![
632                1124, 447, 224, 1664, 1673, 1920, 713, 125, 1936, 1150, 1079, 1922, 596, 984, 567,
633                1134, 530, 539, 700, 1864
634            ]
635        );
636
637        // one more test
638        let deposit_blockhash =
639            BlockHash::from_str("1100000000000000000000000000000000000000000000000000000000000000")
640                .unwrap();
641        let utxos_to_sign = get_kickoff_utxos_to_sign(
642            paramset_ref,
643            op_xonly_pk,
644            deposit_blockhash,
645            deposit_outpoint,
646        );
647
648        assert_eq!(utxos_to_sign.len(), 20);
649        assert_eq!(
650            utxos_to_sign,
651            vec![
652                1454, 26, 157, 1900, 451, 1796, 881, 544, 23, 1080, 1112, 1503, 1233, 1583, 1054,
653                603, 329, 1635, 213, 1331
654            ]
655        );
656    }
657}