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;
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    pub async fn create_watchtower_challenge(
232        &self,
233        transaction_data: TransactionRequestData,
234        commit_data: &[u8],
235        dbtx: Option<DatabaseTransaction<'_>>,
236    ) -> Result<(TransactionType, Transaction), 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 mut 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        self.signer
287            .tx_sign_and_fill_sigs(&mut watchtower_challenge_txhandler, &[], None)?;
288
289        Ok((
290            TransactionType::WatchtowerChallenge(watchtower_index),
291            watchtower_challenge_txhandler.get_cached_tx().clone(),
292        ))
293    }
294
295    /// Creates and signs all the unspent kickoff connector (using the previously saved signatures from operator during setup)
296    ///  transactions for a single round of an operator.
297    ///
298    /// # Arguments
299    /// * `round_idx` - Index of the round.
300    /// * `operator_xonly_pk` - Operator's x-only public key.
301    ///
302    /// # Returns
303    /// A vector of tuples:
304    ///     1. TransactionType: UnspentKickoff(idx) for idx'th kickoff in the round
305    ///     2. Transaction: Signed unspent kickoff connector transaction
306    pub async fn create_and_sign_unspent_kickoff_connector_txs(
307        &self,
308        round_idx: RoundIndex,
309        operator_xonly_pk: XOnlyPublicKey,
310        mut dbtx: Option<DatabaseTransaction<'_>>,
311    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
312        let context = ContractContext::new_context_for_round(
313            operator_xonly_pk,
314            round_idx,
315            self.config.protocol_paramset(),
316        );
317
318        let txhandlers = builder::transaction::create_txhandlers(
319            TransactionType::UnspentKickoff(0),
320            context,
321            &mut TxHandlerCache::new(),
322            &mut ReimburseDbCache::new_for_rounds(
323                self.db.clone(),
324                operator_xonly_pk,
325                self.config.protocol_paramset(),
326                dbtx.as_deref_mut(),
327            ),
328        )
329        .await?;
330
331        // signatures saved during setup
332        let unspent_kickoff_sigs = self
333            .db
334            .get_unspent_kickoff_sigs(dbtx, operator_xonly_pk, round_idx)
335            .await?
336            .ok_or(eyre::eyre!(
337                "No unspent kickoff signatures found for operator {:?} and round {:?}",
338                operator_xonly_pk,
339                round_idx
340            ))?;
341
342        let mut signed_txs = Vec::with_capacity(txhandlers.len());
343        let mut tweak_cache = TweakCache::default();
344
345        for (tx_type, mut txhandler) in txhandlers.into_iter() {
346            if !matches!(tx_type, TransactionType::UnspentKickoff(_)) {
347                // do not try to sign unrelated txs
348                continue;
349            }
350            let res = self.signer
351                .tx_sign_and_fill_sigs(
352                    &mut txhandler,
353                    &unspent_kickoff_sigs,
354                    Some(&mut tweak_cache),
355                )
356                .wrap_err(format!(
357                    "Couldn't sign transaction {tx_type:?} in create_and_sign_unspent_kickoff_connector_txs for round {round_idx:?} and operator {operator_xonly_pk:?}",
358                ));
359
360            let checked_txhandler = txhandler.promote();
361
362            match checked_txhandler {
363                Ok(checked_txhandler) => {
364                    signed_txs.push((tx_type, checked_txhandler.get_cached_tx().clone()));
365                }
366                Err(e) => {
367                    tracing::trace!(
368                        "Couldn't sign transaction {:?} in create_and_sign_unspent_kickoff_connector_txs: {:?}: {:?}",
369                        tx_type,
370                        e,
371                        res.err()
372                    );
373                }
374            }
375        }
376
377        Ok(signed_txs)
378    }
379}
380
381impl<C> Operator<C>
382where
383    C: CitreaClientT,
384{
385    /// Creates and signs all the assert commitment transactions for a single kickoff of an operator.
386    ///
387    /// # Arguments
388    /// * `assert_data` - Data to identify the deposit and kickoff.
389    /// * `commit_data` - BitVM assertions for the kickoff, for each assert tx.
390    ///
391    /// # Returns
392    /// A vector of tuples:
393    ///     1. TransactionType: MiniAssert(idx) for idx'th assert commitment
394    ///     2. Transaction: Signed assert commitment transaction
395    pub async fn create_assert_commitment_txs(
396        &self,
397        assert_data: TransactionRequestData,
398        commit_data: Vec<Vec<Vec<u8>>>,
399        dbtx: Option<DatabaseTransaction<'_>>,
400    ) -> Result<Vec<(TransactionType, Transaction)>, BridgeError> {
401        let deposit_data = self
402            .db
403            .get_deposit_data(None, assert_data.deposit_outpoint)
404            .await?
405            .ok_or(BridgeError::DepositNotFound(assert_data.deposit_outpoint))?
406            .1;
407
408        let context = ContractContext::new_context_with_signer(
409            assert_data.kickoff_data,
410            deposit_data.clone(),
411            self.config.protocol_paramset(),
412            self.signer.clone(),
413        );
414
415        let mut txhandlers = builder::transaction::create_txhandlers(
416            TransactionType::MiniAssert(0),
417            context,
418            &mut TxHandlerCache::new(),
419            &mut ReimburseDbCache::new_for_deposit(
420                self.db.clone(),
421                self.signer.xonly_public_key,
422                assert_data.deposit_outpoint,
423                self.config.protocol_paramset(),
424                dbtx,
425            ),
426        )
427        .await?;
428
429        let mut signed_txhandlers = Vec::new();
430
431        for idx in 0..ClementineBitVMPublicKeys::number_of_assert_txs() {
432            let mut mini_assert_txhandler = txhandlers
433                .remove(&TransactionType::MiniAssert(idx))
434                .ok_or(TxError::TxHandlerNotFound(TransactionType::MiniAssert(idx)))?;
435            let derivations = ClementineBitVMPublicKeys::get_assert_derivations(
436                idx,
437                assert_data.deposit_outpoint,
438                self.config.protocol_paramset(),
439            );
440            // Combine data to be committed with the corresponding bitvm derivation path (needed to regenerate the winternitz secret keys
441            // to sign the transaction)
442            let winternitz_data: Vec<(Vec<u8>, WinternitzDerivationPath)> = derivations
443                .iter()
444                .zip(commit_data[idx].iter())
445                .map(|(derivation, commit_data)| match derivation {
446                    WinternitzDerivationPath::BitvmAssert(_len, _, _, _, _) => {
447                        (commit_data.clone(), derivation.clone())
448                    }
449                    _ => unreachable!(),
450                })
451                .collect();
452            self.signer
453                .tx_sign_winternitz(&mut mini_assert_txhandler, &winternitz_data)?;
454            signed_txhandlers.push(mini_assert_txhandler.promote()?);
455        }
456
457        Ok(signed_txhandlers
458            .into_iter()
459            .map(|txhandler| {
460                (
461                    txhandler.get_transaction_type(),
462                    txhandler.get_cached_tx().clone(),
463                )
464            })
465            .collect())
466    }
467
468    /// Creates and signs the latest blockhash transaction for a single kickoff of an operator.
469    ///
470    /// # Arguments
471    /// * `assert_data` - Data to identify the deposit and kickoff.
472    /// * `block_hash` - Block hash to commit using winternitz signatures.
473    ///
474    /// # Returns
475    /// A tuple of:
476    ///     1. TransactionType: LatestBlockhash
477    ///     2. Transaction: Signed latest blockhash transaction
478    pub async fn create_latest_blockhash_tx(
479        &self,
480        assert_data: TransactionRequestData,
481        block_hash: BlockHash,
482        dbtx: Option<DatabaseTransaction<'_>>,
483    ) -> Result<(TransactionType, Transaction), BridgeError> {
484        let deposit_data = self
485            .db
486            .get_deposit_data(None, assert_data.deposit_outpoint)
487            .await?
488            .ok_or(BridgeError::DepositNotFound(assert_data.deposit_outpoint))?
489            .1;
490
491        let context = ContractContext::new_context_with_signer(
492            assert_data.kickoff_data,
493            deposit_data,
494            self.config.protocol_paramset(),
495            self.signer.clone(),
496        );
497
498        let mut txhandlers = builder::transaction::create_txhandlers(
499            TransactionType::LatestBlockhash,
500            context,
501            &mut TxHandlerCache::new(),
502            &mut ReimburseDbCache::new_for_deposit(
503                self.db.clone(),
504                assert_data.kickoff_data.operator_xonly_pk,
505                assert_data.deposit_outpoint,
506                self.config.protocol_paramset(),
507                dbtx,
508            ),
509        )
510        .await?;
511
512        let mut latest_blockhash_txhandler =
513            txhandlers
514                .remove(&TransactionType::LatestBlockhash)
515                .ok_or(TxError::TxHandlerNotFound(TransactionType::LatestBlockhash))?;
516
517        let block_hash: [u8; 32] = {
518            let raw = block_hash.to_byte_array();
519
520            #[cfg(test)]
521            {
522                self.config.test_params.maybe_disrupt_block_hash(raw)
523            }
524
525            #[cfg(not(test))]
526            {
527                raw
528            }
529        };
530
531        // get last 20 bytes of block_hash
532        let block_hash_last_20 = block_hash.last_20_bytes().to_vec();
533
534        tracing::info!(
535            "Creating latest blockhash tx with block hash's last 20 bytes: {:?}",
536            block_hash_last_20
537        );
538        self.signer.tx_sign_winternitz(
539            &mut latest_blockhash_txhandler,
540            &[(
541                block_hash_last_20,
542                ClementineBitVMPublicKeys::get_latest_blockhash_derivation(
543                    assert_data.deposit_outpoint,
544                    self.config.protocol_paramset(),
545                ),
546            )],
547        )?;
548
549        let latest_blockhash_txhandler = latest_blockhash_txhandler.promote()?;
550
551        // log the block hash witness
552        tracing::info!(
553            "Latest blockhash tx created with block hash witness: {:?}",
554            latest_blockhash_txhandler.get_cached_tx().input
555        );
556
557        Ok((
558            latest_blockhash_txhandler.get_transaction_type(),
559            latest_blockhash_txhandler.get_cached_tx().to_owned(),
560        ))
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use std::str::FromStr;
567
568    use crate::test::common::create_test_config_with_thread_name;
569
570    use super::*;
571
572    #[tokio::test]
573    /// Checks if get_kickoff_utxos_to_sign returns the same values as before.
574    /// This test should never fail, do not make changes to code that changes the result of
575    /// get_kickoff_utxos_to_sign, as doing so will invalidate all past deposits.
576    async fn test_get_kickoff_utxos_to_sign_consistency() {
577        let config = create_test_config_with_thread_name().await;
578        let mut paramset = config.protocol_paramset().clone();
579        paramset.num_kickoffs_per_round = 2000;
580        paramset.num_signed_kickoffs = 20;
581        let paramset_ref: &'static ProtocolParamset = Box::leak(Box::new(paramset));
582        let op_xonly_pk = XOnlyPublicKey::from_str(
583            "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
584        )
585        .unwrap();
586        let deposit_blockhash =
587            BlockHash::from_str("0000000000000000000000000000000000000000000000000000000000000000")
588                .unwrap();
589        let deposit_outpoint = OutPoint::from_str(
590            "0000000000000000000000000000000000000000000000000000000000000000:0",
591        )
592        .unwrap();
593        let utxos_to_sign = get_kickoff_utxos_to_sign(
594            paramset_ref,
595            op_xonly_pk,
596            deposit_blockhash,
597            deposit_outpoint,
598        );
599        assert_eq!(utxos_to_sign.len(), 20);
600        assert_eq!(
601            utxos_to_sign,
602            vec![
603                1124, 447, 224, 1664, 1673, 1920, 713, 125, 1936, 1150, 1079, 1922, 596, 984, 567,
604                1134, 530, 539, 700, 1864
605            ]
606        );
607
608        // one more test
609        let deposit_blockhash =
610            BlockHash::from_str("1100000000000000000000000000000000000000000000000000000000000000")
611                .unwrap();
612        let utxos_to_sign = get_kickoff_utxos_to_sign(
613            paramset_ref,
614            op_xonly_pk,
615            deposit_blockhash,
616            deposit_outpoint,
617        );
618
619        assert_eq!(utxos_to_sign.len(), 20);
620        assert_eq!(
621            utxos_to_sign,
622            vec![
623                1454, 26, 157, 1900, 451, 1796, 881, 544, 23, 1080, 1112, 1503, 1233, 1583, 1054,
624                603, 329, 1635, 213, 1331
625            ]
626        );
627    }
628}