clementine_core/builder/transaction/
operator_collateral.rs

1//! # Collaterals
2//!
3//! This module contains the logic for creating the `round_tx`, `ready_to_reimburse_tx`,
4//! and `unspent_kickoff_tx` transactions. These transactions are used to control the sequence of transactions
5//! in the withdrawal process and limits the number of withdrawals the operator can make in a given time period.
6//!
7//! The flow is as follows:
8//! `round_tx -> ready_to_reimburse_tx -> round_tx -> ...`
9//!
10//! The `round_tx` is used to create a collateral for the withdrawal, kickoff utxos for the current
11//! round and the reimburse connectors for the previous round.
12
13use super::input::UtxoVout;
14use super::txhandler::DEFAULT_SEQUENCE;
15use crate::builder;
16use crate::builder::address::create_taproot_address;
17use crate::builder::script::{TimelockScript, WinternitzCommit};
18use crate::builder::transaction::creator::KickoffWinternitzKeys;
19use crate::builder::transaction::input::SpendableTxIn;
20use crate::builder::transaction::output::UnspentTxOut;
21use crate::builder::transaction::txhandler::TxHandler;
22use crate::builder::transaction::*;
23use crate::config::protocol::ProtocolParamset;
24use crate::constants::MIN_TAPROOT_AMOUNT;
25use crate::errors::BridgeError;
26use crate::rpc::clementine::NumberedSignatureKind;
27use bitcoin::Sequence;
28use bitcoin::{Amount, OutPoint, TxOut, XOnlyPublicKey};
29use std::sync::Arc;
30
31pub enum RoundTxInput {
32    Prevout(Box<SpendableTxIn>),
33    Collateral(OutPoint, Amount),
34}
35
36/// Creates a [`TxHandler`] for `round_tx`.
37///
38/// This transaction is used to create a collateral for the withdrawal, kickoff UTXOs for the current round, and the reimburse connectors for the previous round.
39/// It always uses the first output of the previous `ready_to_reimburse_tx` as the input, chaining rounds together.
40///
41/// `round tx` inputs:
42/// 1. Either the first collateral utxo of operator, or operators collateral in the previous rounds ready to reimburse tx.
43///
44/// `round tx` outputs:
45/// 1. Operator's Burn Connector
46/// 2. Kickoff utxo(s): the utxos will be used as the input for the kickoff transactions
47/// 3. Reimburse utxo(s): the utxo(s) will be used as an input to Reimburse TX
48/// 4. P2Anchor: Anchor output for CPFP
49///
50/// # Arguments
51/// * `operator_xonly_pk` - The operator's x-only public key.
52/// * `txin` - The input to the round transaction (either a previous output or the first collateral).
53/// * `pubkeys` - Winternitz public keys for the round's kickoff UTXOs.
54/// * `paramset` - Protocol parameter set.
55///
56/// # Returns
57/// A [`TxHandler`] for the round transaction, or a [`BridgeError`] if construction fails.
58pub fn create_round_txhandler(
59    operator_xonly_pk: XOnlyPublicKey,
60    txin: RoundTxInput,
61    pubkeys: &[bitvm::signatures::winternitz::PublicKey],
62    paramset: &'static ProtocolParamset,
63) -> Result<TxHandler, BridgeError> {
64    let mut builder = TxHandlerBuilder::new(TransactionType::Round).with_version(NON_STANDARD_V3);
65    let input_amount;
66    match txin {
67        RoundTxInput::Prevout(prevout) => {
68            input_amount = prevout.get_prevout().value;
69            builder = builder.add_input(
70                NormalSignatureKind::OperatorSighashDefault,
71                *prevout,
72                SpendPath::KeySpend,
73                Sequence::from_height(paramset.operator_reimburse_timelock),
74            );
75        }
76        RoundTxInput::Collateral(outpoint, amount) => {
77            let (op_address, op_spend) =
78                create_taproot_address(&[], Some(operator_xonly_pk), paramset.network);
79            input_amount = amount;
80            builder = builder.add_input(
81                NormalSignatureKind::OperatorSighashDefault,
82                SpendableTxIn::new(
83                    outpoint,
84                    TxOut {
85                        value: input_amount,
86                        script_pubkey: op_address.script_pubkey(),
87                    },
88                    vec![],
89                    Some(op_spend.clone()),
90                ),
91                SpendPath::KeySpend,
92                DEFAULT_SEQUENCE,
93            );
94        }
95    }
96
97    // This 1 block is to enforce that operator has to put a sequence number in the input
98    // so this spending path can't be used to send kickoff tx
99    let timeout_block_count_locked_script =
100        Arc::new(TimelockScript::new(Some(operator_xonly_pk), 1));
101
102    let total_required = (paramset.kickoff_amount + paramset.default_utxo_amount())
103        .checked_mul(paramset.num_kickoffs_per_round as u64)
104        .and_then(|kickoff_total| kickoff_total.checked_add(paramset.anchor_amount()))
105        .ok_or_else(|| {
106            BridgeError::ArithmeticOverflow("Total required amount calculation overflow")
107        })?;
108
109    let remaining_amount = input_amount.checked_sub(total_required).ok_or_else(|| {
110        BridgeError::InsufficientFunds("Input amount insufficient for required outputs")
111    })?;
112
113    builder = builder.add_output(UnspentTxOut::from_scripts(
114        remaining_amount,
115        vec![],
116        Some(operator_xonly_pk),
117        paramset.network,
118    ));
119
120    // add kickoff utxos
121    for pubkey in pubkeys.iter().take(paramset.num_kickoffs_per_round) {
122        let blockhash_commit = Arc::new(WinternitzCommit::new(
123            vec![(pubkey.clone(), paramset.kickoff_blockhash_commit_length)],
124            operator_xonly_pk,
125            paramset.winternitz_log_d,
126        ));
127        builder = builder.add_output(UnspentTxOut::from_scripts(
128            paramset.kickoff_amount,
129            vec![blockhash_commit, timeout_block_count_locked_script.clone()],
130            None,
131            paramset.network,
132        ));
133    }
134    // Create reimburse utxos
135    for _ in 0..paramset.num_kickoffs_per_round {
136        builder = builder.add_output(UnspentTxOut::from_scripts(
137            paramset.default_utxo_amount(),
138            vec![],
139            Some(operator_xonly_pk),
140            paramset.network,
141        ));
142    }
143    Ok(builder
144        .add_output(UnspentTxOut::from_partial(
145            builder::transaction::anchor_output(paramset.anchor_amount()),
146        ))
147        .finalize())
148}
149
150/// Creates a vector of [`TxHandler`] for `assert_timeout_tx` transactions.
151///
152/// These transactions can be sent by anyone if the operator did not send their asserts in time, burning their burn connector and kickoff finalizer.
153///
154/// # Inputs
155/// 1. KickoffTx: Assert utxo (corresponding to the assert)
156/// 2. KickoffTx: KickoffFinalizer utxo
157/// 3. RoundTx: BurnConnector utxo
158///
159/// # Outputs
160/// 1. Anchor output for CPFP
161///
162/// # Arguments
163/// * `kickoff_txhandler` - The kickoff transaction handler providing the input.
164/// * `round_txhandler` - The round transaction handler providing an additional input.
165/// * `num_asserts` - Number of assert timeout transactions to create.
166/// * `paramset` - Protocol parameter set.
167///
168/// # Returns
169/// A vector of [`TxHandler`] for all assert timeout transactions, or a [`BridgeError`] if construction fails.
170pub fn create_assert_timeout_txhandlers(
171    kickoff_txhandler: &TxHandler,
172    round_txhandler: &TxHandler,
173    num_asserts: usize,
174    paramset: &'static ProtocolParamset,
175) -> Result<Vec<TxHandler>, BridgeError> {
176    let mut txhandlers = Vec::new();
177    for idx in 0..num_asserts {
178        txhandlers.push(
179            TxHandlerBuilder::new(TransactionType::AssertTimeout(idx))
180                .with_version(NON_STANDARD_V3)
181                .add_input(
182                    (NumberedSignatureKind::AssertTimeout1, idx as i32),
183                    kickoff_txhandler.get_spendable_output(UtxoVout::Assert(idx))?,
184                    SpendPath::ScriptSpend(0),
185                    Sequence::from_height(paramset.assert_timeout_timelock),
186                )
187                .add_input(
188                    (NumberedSignatureKind::AssertTimeout2, idx as i32),
189                    kickoff_txhandler.get_spendable_output(UtxoVout::KickoffFinalizer)?,
190                    SpendPath::ScriptSpend(0),
191                    DEFAULT_SEQUENCE,
192                )
193                .add_input(
194                    (NumberedSignatureKind::AssertTimeout3, idx as i32),
195                    round_txhandler.get_spendable_output(UtxoVout::CollateralInRound)?,
196                    SpendPath::KeySpend,
197                    DEFAULT_SEQUENCE,
198                )
199                .add_output(UnspentTxOut::from_partial(
200                    builder::transaction::anchor_output(paramset.anchor_amount()),
201                ))
202                .finalize(),
203        );
204    }
205    Ok(txhandlers)
206}
207
208/// Creates the nth (1-indexed) `round_txhandler` and `reimburse_generator_txhandler` pair for a specific operator.
209///
210/// # Arguments
211/// * `operator_xonly_pk` - The operator's x-only public key.
212/// * `input_outpoint` - The outpoint to use as input for the first round.
213/// * `input_amount` - The amount for the input outpoint.
214/// * `index` - The index of the round to create.
215/// * `pubkeys` - Winternitz keys for all rounds.
216/// * `paramset` - Protocol parameter set.
217///
218/// # Returns
219/// A tuple of (`TxHandler` for the round, `TxHandler` for ready-to-reimburse), or a [`BridgeError`] if construction fails.
220pub fn create_round_nth_txhandler(
221    operator_xonly_pk: XOnlyPublicKey,
222    input_outpoint: OutPoint,
223    input_amount: Amount,
224    index: RoundIndex,
225    pubkeys: &KickoffWinternitzKeys,
226    paramset: &'static ProtocolParamset,
227) -> Result<(TxHandler, TxHandler), BridgeError> {
228    // 0th round is the collateral, there are no keys for the 0th round
229    // Additionally there are no keys after num_rounds + 1, +1 is because we need additional round to generate
230    // reimbursement connectors of previous round
231
232    if index == RoundIndex::Collateral
233        || index.to_index() > RoundIndex::Round(paramset.num_round_txs).to_index()
234    {
235        return Err(TxError::InvalidRoundIndex(index).into());
236    }
237
238    // create the first round txhandler
239    let mut round_txhandler = create_round_txhandler(
240        operator_xonly_pk,
241        RoundTxInput::Collateral(input_outpoint, input_amount),
242        pubkeys.get_keys_for_round(RoundIndex::Round(0))?,
243        paramset,
244    )?;
245    let mut ready_to_reimburse_txhandler =
246        create_ready_to_reimburse_txhandler(&round_txhandler, operator_xonly_pk, paramset)?;
247
248    // get which round index we are creating txhandlers for
249    let round_idx = match index {
250        RoundIndex::Collateral => 0, // impossible, checked before
251        RoundIndex::Round(idx) => idx,
252    };
253    // iterate starting from second round to the requested round
254    for round_idx in RoundIndex::iter_rounds_range(1, round_idx + 1) {
255        round_txhandler = create_round_txhandler(
256            operator_xonly_pk,
257            RoundTxInput::Prevout(Box::new(
258                ready_to_reimburse_txhandler
259                    .get_spendable_output(UtxoVout::CollateralInReadyToReimburse)?,
260            )),
261            pubkeys.get_keys_for_round(round_idx)?,
262            paramset,
263        )?;
264        ready_to_reimburse_txhandler =
265            create_ready_to_reimburse_txhandler(&round_txhandler, operator_xonly_pk, paramset)?;
266    }
267    Ok((round_txhandler, ready_to_reimburse_txhandler))
268}
269
270/// Creates a [`TxHandler`] for the `ready_to_reimburse_tx`.
271///
272/// # Inputs
273/// 1. RoundTx: BurnConnector utxo
274///
275/// # Outputs
276/// 1. Operator's collateral
277/// 2. Anchor output for CPFP
278///
279/// # Arguments
280/// * `round_txhandler` - The round transaction handler providing the input.
281/// * `operator_xonly_pk` - The operator's x-only public key.
282/// * `paramset` - Protocol parameter set.
283///
284/// # Returns
285/// A [`TxHandler`] for the ready-to-reimburse transaction, or a [`BridgeError`] if construction fails.
286pub fn create_ready_to_reimburse_txhandler(
287    round_txhandler: &TxHandler,
288    operator_xonly_pk: XOnlyPublicKey,
289    paramset: &'static ProtocolParamset,
290) -> Result<TxHandler, BridgeError> {
291    let prevout = round_txhandler.get_spendable_output(UtxoVout::CollateralInRound)?;
292    let prev_value = prevout.get_prevout().value;
293
294    Ok(TxHandlerBuilder::new(TransactionType::ReadyToReimburse)
295        .with_version(NON_STANDARD_V3)
296        .add_input(
297            NormalSignatureKind::OperatorSighashDefault,
298            prevout,
299            SpendPath::KeySpend,
300            DEFAULT_SEQUENCE,
301        )
302        .add_output(UnspentTxOut::from_scripts(
303            prev_value.checked_sub(paramset.anchor_amount()).ok_or(
304                BridgeError::ArithmeticOverflow(
305                    "Insufficient funds while creating ready to reimburse tx",
306                ),
307            )?,
308            vec![],
309            Some(operator_xonly_pk),
310            paramset.network,
311        ))
312        .add_output(UnspentTxOut::from_partial(
313            builder::transaction::anchor_output(paramset.anchor_amount()),
314        ))
315        .finalize())
316}
317
318/// Creates a vector of [`TxHandler`] for `unspent_kickoff_tx` transactions.
319/// These transactions can be sent if an operator sends ReadyToReimburse transaction without spending all the kickoff utxos of the round.
320///
321/// # Inputs
322/// 1. ReadyToReimburseTx: BurnConnector utxo
323/// 2. RoundTx: Any kickoff utxo of the same round
324///
325/// # Outputs
326/// 1. Anchor output for CPFP
327///
328/// # Arguments
329/// * `round_txhandler` - The round transaction handler providing the kickoff utxos.
330/// * `ready_to_reimburse_txhandler` - The ready-to-reimburse transaction handler providing the collateral.
331/// * `paramset` - Protocol parameter set.
332///
333/// # Returns
334/// A vector of [`TxHandler`] for unspent kickoff transactions, or a [`BridgeError`] if construction fails.
335pub fn create_unspent_kickoff_txhandlers(
336    round_txhandler: &TxHandler,
337    ready_to_reimburse_txhandler: &TxHandler,
338    paramset: &'static ProtocolParamset,
339) -> Result<Vec<TxHandler>, BridgeError> {
340    let mut txhandlers = Vec::new();
341    for idx in 0..paramset.num_kickoffs_per_round {
342        txhandlers.push(
343            TxHandlerBuilder::new(TransactionType::UnspentKickoff(idx))
344                .with_version(NON_STANDARD_V3)
345                .add_input(
346                    (NumberedSignatureKind::UnspentKickoff1, idx as i32),
347                    ready_to_reimburse_txhandler
348                        .get_spendable_output(UtxoVout::CollateralInReadyToReimburse)?,
349                    SpendPath::KeySpend,
350                    DEFAULT_SEQUENCE,
351                )
352                .add_input(
353                    (NumberedSignatureKind::UnspentKickoff2, idx as i32),
354                    round_txhandler.get_spendable_output(UtxoVout::Kickoff(idx))?,
355                    SpendPath::ScriptSpend(1),
356                    Sequence::from_height(1),
357                )
358                .add_output(UnspentTxOut::from_partial(
359                    builder::transaction::anchor_output(paramset.anchor_amount()),
360                ))
361                .finalize(),
362        );
363    }
364    Ok(txhandlers)
365}
366
367/// Creates a [`TxHandler`] for burning unused kickoff connectors.
368///
369/// # Inputs
370/// 1. RoundTx: Kickoff utxo(s) (per unused connector)
371///
372/// # Outputs
373/// 1. Change output to the provided address
374/// 2. Anchor output for CPFP
375///
376/// # Arguments
377/// * `round_txhandler` - The round transaction handler providing the input.
378/// * `unused_kickoff_connectors_indices` - Indices of the unused kickoff connectors (0-indexed).
379/// * `change_address` - The address to send the change to.
380///
381/// # Returns
382/// A [`TxHandler`] for burning unused kickoff connectors, or a [`BridgeError`] if construction fails or no unused kickoff connectors are provided.
383pub fn create_burn_unused_kickoff_connectors_txhandler(
384    round_txhandler: &TxHandler,
385    unused_kickoff_connectors_indices: &[usize],
386    change_address: &Address,
387    paramset: &'static ProtocolParamset,
388) -> Result<TxHandler, BridgeError> {
389    if unused_kickoff_connectors_indices.is_empty() {
390        return Err(eyre::eyre!(
391            "create_burn_unused_kickoff_connectors_txhandler called with no unused kickoff connectors"
392        )
393        .into());
394    }
395    let mut tx_handler_builder =
396        TxHandlerBuilder::new(TransactionType::BurnUnusedKickoffConnectors)
397            .with_version(NON_STANDARD_V3);
398    let mut input_amount = Amount::ZERO;
399    for &idx in unused_kickoff_connectors_indices {
400        let txin = round_txhandler.get_spendable_output(UtxoVout::Kickoff(idx))?;
401        input_amount = input_amount.checked_add(txin.get_prevout().value).ok_or(
402            BridgeError::ArithmeticOverflow("Amount overflow in burn unused kickoff connectors tx"),
403        )?;
404        tx_handler_builder = tx_handler_builder.add_input(
405            NormalSignatureKind::OperatorSighashDefault,
406            txin,
407            SpendPath::ScriptSpend(1),
408            Sequence::from_height(1),
409        );
410    }
411    if !paramset.bridge_nonstandard && input_amount >= paramset.anchor_amount() + MIN_TAPROOT_AMOUNT
412    {
413        // if we use standard tx's, kickoff utxo's will hold some sats so we can return the change to the change address
414        // but if we use nonstandard tx's with 0 sat values then the change is 0 anyway, no need to add an output
415        tx_handler_builder = tx_handler_builder.add_output(UnspentTxOut::from_partial(TxOut {
416            // In a standard bridge, a kickoff UTXO will hold on the order of 10^4 sats, so this UTXO will not be considered dust
417            value: input_amount - paramset.anchor_amount(),
418            script_pubkey: change_address.script_pubkey(),
419        }));
420    }
421    tx_handler_builder = tx_handler_builder.add_output(UnspentTxOut::from_partial(
422        builder::transaction::anchor_output(paramset.anchor_amount()),
423    ));
424    Ok(tx_handler_builder.finalize())
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use crate::config::protocol::REGTEST_PARAMSET;
431    use std::str::FromStr;
432
433    #[tokio::test]
434    async fn test_create_round_nth_txhandler_and_round_txhandlers() {
435        // check if round_nth_txhandler and round_txhandlers are consistent with each other
436        let op_xonly_pk = XOnlyPublicKey::from_str(
437            "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
438        )
439        .expect("this key is valid");
440        let paramset = &REGTEST_PARAMSET;
441        let input_outpoint = OutPoint::new(bitcoin::Txid::all_zeros(), 0);
442        let input_amount = Amount::from_sat(10000000000);
443        let pubkeys = KickoffWinternitzKeys::new(
444            vec![
445                vec![[0u8; 20]; 43];
446                (paramset.num_round_txs + 1) * paramset.num_kickoffs_per_round
447            ],
448            paramset.num_kickoffs_per_round,
449            paramset.num_round_txs,
450        )
451        .unwrap();
452
453        let mut round_tx_input = RoundTxInput::Collateral(input_outpoint, input_amount);
454
455        for i in 0..paramset.num_round_txs {
456            let (round_nth_txhandler, ready_to_reimburse_nth_txhandler) =
457                create_round_nth_txhandler(
458                    op_xonly_pk,
459                    input_outpoint,
460                    input_amount,
461                    RoundIndex::Round(i),
462                    &pubkeys,
463                    paramset,
464                )
465                .unwrap();
466
467            let round_txhandler = create_round_txhandler(
468                op_xonly_pk,
469                round_tx_input,
470                pubkeys.get_keys_for_round(RoundIndex::Round(i)).unwrap(),
471                paramset,
472            )
473            .unwrap();
474
475            let ready_to_reimburse_txhandler =
476                create_ready_to_reimburse_txhandler(&round_txhandler, op_xonly_pk, paramset)
477                    .unwrap();
478
479            assert_eq!(round_nth_txhandler.get_txid(), round_txhandler.get_txid());
480            assert_eq!(
481                ready_to_reimburse_nth_txhandler.get_txid(),
482                ready_to_reimburse_txhandler.get_txid()
483            );
484
485            let prev_ready_to_reimburse_txhandler = ready_to_reimburse_txhandler;
486            round_tx_input = RoundTxInput::Prevout(Box::new(
487                prev_ready_to_reimburse_txhandler
488                    .get_spendable_output(UtxoVout::CollateralInReadyToReimburse)
489                    .unwrap(),
490            ));
491        }
492    }
493}