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