clementine_core/builder/transaction/
operator_reimburse.rs

1//! # Operator Reimburse Transactions
2//!
3//! This module contains the logic for creating operator reimbursement and payout-related transactions in the protocol.
4//! These transactions handle the flow of funds for operator compensation, challenge handling, and user withdrawals.
5//!
6//! The main responsibilities include:
7//! - Constructing the kickoff transaction, which sets up all outputs needed for subsequent protocol steps (challenge, reimbursement, asserts, etc.).
8//! - Creating transactions for operator reimbursement in case of honest behavior.
9//! - Handling payout transactions for user withdrawals, including both standard (with BitVM) and optimistic payout flows.
10//!
11
12use super::create_move_to_vault_txhandler;
13use super::input::SpendableTxIn;
14use super::input::UtxoVout;
15use super::op_return_txout;
16use super::txhandler::DEFAULT_SEQUENCE;
17use super::HiddenNode;
18use super::Signed;
19use super::TransactionType;
20use super::TxError;
21use crate::builder::script::{CheckSig, SpendableScript, TimelockScript};
22use crate::builder::script::{PreimageRevealScript, SpendPath};
23use crate::builder::transaction::anchor_output;
24use crate::builder::transaction::output::UnspentTxOut;
25use crate::builder::transaction::txhandler::{TxHandler, TxHandlerBuilder};
26use crate::config::protocol::ProtocolParamset;
27use crate::constants::NON_EPHEMERAL_ANCHOR_AMOUNT;
28use crate::constants::NON_STANDARD_V3;
29use crate::deposit::{DepositData, KickoffData};
30use crate::errors::BridgeError;
31use crate::rpc::clementine::NormalSignatureKind;
32use crate::{builder, UTXO};
33use bitcoin::hashes::Hash;
34use bitcoin::script::PushBytesBuf;
35use bitcoin::ScriptBuf;
36use bitcoin::XOnlyPublicKey;
37use bitcoin::{taproot, TxOut, Txid};
38use std::sync::Arc;
39
40#[derive(Debug, Clone)]
41pub enum AssertScripts<'a> {
42    AssertScriptTapNodeHash(&'a [[u8; 32]]),
43    AssertSpendableScript(Vec<Arc<dyn SpendableScript>>),
44}
45
46#[derive(Debug, Clone)]
47pub enum DisprovePath<'a> {
48    Scripts(Vec<ScriptBuf>),
49    HiddenNode(HiddenNode<'a>),
50}
51
52/// Creates a [`TxHandler`] for the `kickoff_tx`.
53///
54/// This transaction is sent by the operator to initialize protocol state for a round, when operator fronted a peg-out and wants reimbursement. It sets up all outputs needed for subsequent protocol steps (challenge, reimbursement, asserts, etc.).
55///
56/// # Inputs
57/// 1. RoundTx: Kickoff utxo (for the given kickoff index)
58///
59/// # Outputs
60/// 1. Operator challenge output (for challenge or no-challenge path)
61/// 2. Kickoff finalizer connector
62/// 3. Reimburse connector (to be used in reimburse transaction)
63/// 4. Disprove output (Taproot, for BitVM disprove path)
64/// 5. Latest blockhash output (for latest blockhash assertion using winternitz signatures)
65/// 6. Multiple assert outputs (for BitVM assertions, currently 36)
66/// 7. For each watchtower 2 outputs:
67///     - Watchtower challenge output
68///     - Operator challenge ack/nack output
69/// 8. OP_RETURN output (with move-to-vault txid and operator xonly pubkey)
70/// 9. Anchor output for CPFP
71///
72/// # Arguments
73/// * `kickoff_data` - Data to identify the kickoff.
74/// * `round_txhandler` - The round transaction handler providing the input.
75/// * `move_txhandler` - The move-to-vault transaction handler.
76/// * `deposit_data` - Mutable reference to deposit data.
77/// * `operator_xonly_pk` - The operator's x-only public key.
78/// * `assert_scripts` - Actual assertion scripts or tapnode hashes (for faster creation of assert utxos) for BitVM assertion.
79/// * `disprove_root_hash` - Root hash for BitVM disprove scripts.
80/// * `additional_disprove_script` - Additional disprove script bytes (for additional disprove script specific to Clementine).
81/// * `latest_blockhash_script` - Actual script or tapnode hash for latest blockhash assertion.
82/// * `operator_unlock_hashes` - Unlock hashes for operator preimage reveals for OperatorChallengeAck transactions.
83/// * `paramset` - Protocol parameter set.
84///
85/// # Returns
86/// A [`TxHandler`] for the kickoff transaction, or a [`BridgeError`] if construction fails.
87#[allow(clippy::too_many_arguments)]
88pub fn create_kickoff_txhandler(
89    kickoff_data: KickoffData,
90    round_txhandler: &TxHandler,
91    move_txhandler: &TxHandler,
92    deposit_data: &mut DepositData,
93    operator_xonly_pk: XOnlyPublicKey,
94    assert_scripts: AssertScripts,
95    disprove_path: DisprovePath,
96    additional_disprove_script: Vec<u8>,
97    latest_blockhash_script: AssertScripts,
98    operator_unlock_hashes: &[[u8; 20]],
99    paramset: &'static ProtocolParamset,
100) -> Result<TxHandler, BridgeError> {
101    let kickoff_idx = kickoff_data.kickoff_idx as usize;
102    let move_txid: Txid = *move_txhandler.get_txid();
103    let mut builder = TxHandlerBuilder::new(TransactionType::Kickoff).with_version(NON_STANDARD_V3);
104    builder = builder.add_input(
105        NormalSignatureKind::OperatorSighashDefault,
106        round_txhandler.get_spendable_output(UtxoVout::Kickoff(kickoff_idx))?,
107        builder::script::SpendPath::ScriptSpend(0),
108        DEFAULT_SEQUENCE,
109    );
110
111    let nofn_script = Arc::new(CheckSig::new(deposit_data.get_nofn_xonly_pk()?));
112    let operator_script = Arc::new(CheckSig::new(operator_xonly_pk));
113
114    let operator_1week = Arc::new(TimelockScript::new(
115        Some(operator_xonly_pk),
116        paramset.operator_challenge_timeout_timelock,
117    ));
118
119    builder = builder
120        // goes to challenge tx or no challenge tx
121        .add_output(UnspentTxOut::from_scripts(
122            paramset.default_utxo_amount(),
123            vec![operator_script, operator_1week],
124            None,
125            paramset.network,
126        ))
127        // kickoff finalizer connector
128        .add_output(UnspentTxOut::from_scripts(
129            paramset.default_utxo_amount(),
130            vec![nofn_script.clone()],
131            None,
132            paramset.network,
133        ))
134        // UTXO to reimburse tx
135        .add_output(UnspentTxOut::from_scripts(
136            paramset.default_utxo_amount(),
137            vec![nofn_script.clone()],
138            None,
139            paramset.network,
140        ));
141
142    // Add disprove utxo
143    // Add Operator in 5 week script to taproot, that connects to disprove timeout
144    let operator_5week = Arc::new(TimelockScript::new(
145        Some(operator_xonly_pk),
146        paramset.disprove_timeout_timelock,
147    ));
148
149    let additional_disprove_script = ScriptBuf::from_bytes(additional_disprove_script);
150
151    // disprove utxo
152    builder = builder.add_output(super::create_disprove_taproot_output(
153        operator_5week,
154        additional_disprove_script.clone(),
155        disprove_path,
156        paramset.default_utxo_amount(),
157        paramset.network,
158    ));
159
160    let nofn_latest_blockhash = Arc::new(TimelockScript::new(
161        Some(deposit_data.get_nofn_xonly_pk()?),
162        paramset.latest_blockhash_timeout_timelock,
163    ));
164
165    match latest_blockhash_script {
166        AssertScripts::AssertScriptTapNodeHash(latest_blockhash_root_hash) => {
167            if latest_blockhash_root_hash.len() != 1 {
168                return Err(TxError::LatestBlockhashScriptNumber.into());
169            }
170            let latest_blockhash_root_hash = latest_blockhash_root_hash[0];
171            // latest blockhash utxo
172            builder = builder.add_output(super::create_taproot_output_with_hidden_node(
173                nofn_latest_blockhash,
174                &latest_blockhash_root_hash,
175                paramset.default_utxo_amount(),
176                paramset.network,
177            ));
178        }
179        AssertScripts::AssertSpendableScript(latest_blockhash_script) => {
180            if latest_blockhash_script.len() != 1 {
181                return Err(TxError::LatestBlockhashScriptNumber.into());
182            }
183            let latest_blockhash_script = latest_blockhash_script[0].clone();
184            builder = builder.add_output(UnspentTxOut::from_scripts(
185                paramset.default_utxo_amount(),
186                vec![nofn_latest_blockhash, latest_blockhash_script],
187                None,
188                paramset.network,
189            ));
190        }
191    }
192
193    // add nofn_4 week to all assert scripts
194    let nofn_4week = Arc::new(TimelockScript::new(
195        Some(deposit_data.get_nofn_xonly_pk()?),
196        paramset.assert_timeout_timelock,
197    ));
198
199    match assert_scripts {
200        AssertScripts::AssertScriptTapNodeHash(assert_script_hashes) => {
201            for script_hash in assert_script_hashes.iter() {
202                // Add N-of-N in 4 week script to taproot, that connects to assert timeout
203                builder = builder.add_output(super::create_taproot_output_with_hidden_node(
204                    nofn_4week.clone(),
205                    script_hash,
206                    paramset.default_utxo_amount(),
207                    paramset.network,
208                ));
209            }
210        }
211        AssertScripts::AssertSpendableScript(assert_scripts) => {
212            for script in assert_scripts {
213                builder = builder.add_output(UnspentTxOut::from_scripts(
214                    paramset.default_utxo_amount(),
215                    vec![nofn_4week.clone(), script],
216                    None,
217                    paramset.network,
218                ));
219            }
220        }
221    }
222
223    let watchtower_xonly_pks = deposit_data.get_watchtowers();
224
225    for (watchtower_idx, watchtower_xonly_pk) in watchtower_xonly_pks.iter().enumerate() {
226        let nofn_2week = Arc::new(TimelockScript::new(
227            Some(deposit_data.get_nofn_xonly_pk()?),
228            paramset.watchtower_challenge_timeout_timelock,
229        ));
230        // UTXO for watchtower challenge or watchtower challenge timeouts
231        builder = builder.add_output(UnspentTxOut::from_scripts(
232            paramset.default_utxo_amount() * 2 + paramset.anchor_amount(), // watchtower challenge has 2 taproot outputs, 1 op_return and 1 anchor
233            vec![nofn_2week.clone()],
234            Some(*watchtower_xonly_pk), // key path as watchtowers xonly pk
235            paramset.network,
236        ));
237
238        // UTXO for operator challenge ack, nack, and watchtower challenge timeouts
239        let nofn_3week = Arc::new(TimelockScript::new(
240            Some(deposit_data.get_nofn_xonly_pk()?),
241            paramset.operator_challenge_nack_timelock,
242        ));
243        let operator_with_preimage = Arc::new(PreimageRevealScript::new(
244            operator_xonly_pk,
245            operator_unlock_hashes[watchtower_idx],
246        ));
247        builder = builder.add_output(UnspentTxOut::from_scripts(
248            paramset.default_utxo_amount(),
249            vec![
250                nofn_3week.clone(),
251                nofn_2week.clone(),
252                operator_with_preimage,
253            ],
254            None,
255            paramset.network,
256        ));
257    }
258
259    let mut op_return_script = move_txid.to_byte_array().to_vec();
260    op_return_script.extend(kickoff_data.operator_xonly_pk.serialize());
261
262    let push_bytes = PushBytesBuf::try_from(op_return_script)
263        .expect("Can't fail since the script is shorter than 4294967296 bytes");
264
265    let op_return_txout = builder::transaction::op_return_txout(push_bytes);
266
267    Ok(builder
268        .add_output(UnspentTxOut::from_partial(op_return_txout))
269        .add_output(UnspentTxOut::from_partial(
270            builder::transaction::anchor_output(paramset.anchor_amount()),
271        ))
272        .finalize())
273}
274
275/// Creates a [`TxHandler`] for the `kickoff_not_finalized_tx`.
276///
277/// This transaction if an operator sends ReadyToReimburse transaction while not all kickoffs of the round are finalized, burning their collateral.
278///
279/// # Inputs
280/// 1. KickoffTx: KickoffFinalizer utxo
281/// 2. ReadyToReimburseTx: BurnConnector utxo
282///
283/// # Outputs
284/// 1. Anchor output for CPFP
285///
286/// # Arguments
287/// * `kickoff_txhandler` - The kickoff transaction handler providing the input.
288/// * `ready_to_reimburse_txhandler` - The ready-to-reimburse transaction handler providing the input.
289///
290/// # Returns
291/// A [`TxHandler`] for the kickoff not finalized transaction, or a [`BridgeError`] if construction fails.
292pub fn create_kickoff_not_finalized_txhandler(
293    kickoff_txhandler: &TxHandler,
294    ready_to_reimburse_txhandler: &TxHandler,
295    paramset: &'static ProtocolParamset,
296) -> Result<TxHandler, BridgeError> {
297    Ok(TxHandlerBuilder::new(TransactionType::KickoffNotFinalized)
298        .with_version(NON_STANDARD_V3)
299        .add_input(
300            NormalSignatureKind::KickoffNotFinalized1,
301            kickoff_txhandler.get_spendable_output(UtxoVout::KickoffFinalizer)?,
302            builder::script::SpendPath::ScriptSpend(0),
303            DEFAULT_SEQUENCE,
304        )
305        .add_input(
306            NormalSignatureKind::KickoffNotFinalized2,
307            ready_to_reimburse_txhandler
308                .get_spendable_output(UtxoVout::CollateralInReadyToReimburse)?,
309            builder::script::SpendPath::KeySpend,
310            DEFAULT_SEQUENCE,
311        )
312        .add_output(UnspentTxOut::from_partial(
313            builder::transaction::anchor_output(paramset.anchor_amount()),
314        ))
315        .finalize())
316}
317
318/// Creates a [`TxHandler`] for the `reimburse_tx`.
319///
320/// This transaction is sent by the operator if no challenge was sent, or a challenge was sent but no disprove was sent, to reimburse the operator for their payout.
321///
322/// # Inputs
323/// 1. MoveToVaultTx: Utxo containing the deposit
324/// 2. KickoffTx: Reimburse connector utxo in the kickoff
325/// 3. RoundTx: Reimburse connector utxo in the round (for the given kickoff index)
326///
327/// # Outputs
328/// 1. Reimbursement output to the operator
329/// 2. Anchor output for CPFP
330///
331/// # Arguments
332/// * `move_txhandler` - The move-to-vault transaction handler for the deposit.
333/// * `round_txhandler` - The round transaction handler for the round.
334/// * `kickoff_txhandler` - The kickoff transaction handler for the kickoff.
335/// * `kickoff_idx` - The kickoff index of the operator's kickoff.
336/// * `paramset` - Protocol parameter set.
337/// * `operator_reimbursement_address` - The address to reimburse the operator.
338///
339/// # Returns
340/// A [`TxHandler`] for the reimburse transaction, or a [`BridgeError`] if construction fails.
341pub fn create_reimburse_txhandler(
342    move_txhandler: &TxHandler,
343    round_txhandler: &TxHandler,
344    kickoff_txhandler: &TxHandler,
345    kickoff_idx: usize,
346    paramset: &'static ProtocolParamset,
347    operator_reimbursement_address: &bitcoin::Address,
348) -> Result<TxHandler, BridgeError> {
349    let builder = TxHandlerBuilder::new(TransactionType::Reimburse)
350        .with_version(NON_STANDARD_V3)
351        .add_input(
352            NormalSignatureKind::Reimburse1,
353            move_txhandler.get_spendable_output(UtxoVout::DepositInMove)?,
354            builder::script::SpendPath::ScriptSpend(0),
355            DEFAULT_SEQUENCE,
356        )
357        .add_input(
358            NormalSignatureKind::Reimburse2,
359            kickoff_txhandler.get_spendable_output(UtxoVout::ReimburseInKickoff)?,
360            builder::script::SpendPath::ScriptSpend(0),
361            DEFAULT_SEQUENCE,
362        )
363        .add_input(
364            NormalSignatureKind::OperatorSighashDefault,
365            round_txhandler
366                .get_spendable_output(UtxoVout::ReimburseInRound(kickoff_idx, paramset))?,
367            builder::script::SpendPath::KeySpend,
368            DEFAULT_SEQUENCE,
369        );
370
371    Ok(builder
372        .add_output(UnspentTxOut::from_partial(TxOut {
373            value: move_txhandler
374                .get_spendable_output(UtxoVout::DepositInMove)?
375                .get_prevout()
376                .value,
377            script_pubkey: operator_reimbursement_address.script_pubkey(),
378        }))
379        .add_output(UnspentTxOut::from_partial(
380            builder::transaction::anchor_output(paramset.anchor_amount()),
381        ))
382        .finalize())
383}
384
385/// Creates a [`TxHandler`] for the `payout_tx`.
386///
387/// This transaction is sent by the operator to front a peg-out, after which operator will send a kickoff transaction to get reimbursed.
388///
389/// # Inputs
390/// 1. UTXO: User's withdrawal input (committed in Citrea side, with the signature given to operators off-chain)
391///
392/// # Outputs
393/// 1. User payout output
394/// 2. OP_RETURN output (with operators x-only pubkey that fronts the peg-out)
395///
396/// # Arguments
397/// * `input_utxo` - The input UTXO for the payout, committed in Citrea side, with the signature given to operators off-chain.
398/// * `output_txout` - The output TxOut for the user payout.
399/// * `operator_xonly_pk` - The operator's x-only public key that fronts the peg-out.
400/// * `user_sig` - The user's signature for the payout, given to operators off-chain.
401/// * `network` - The Bitcoin network.
402///
403/// # Returns
404/// A [`TxHandler`] for the payout transaction, or a [`BridgeError`] if construction fails.
405pub fn create_payout_txhandler(
406    input_utxo: UTXO,
407    output_txout: TxOut,
408    operator_xonly_pk: XOnlyPublicKey,
409    user_sig: taproot::Signature,
410    _network: bitcoin::Network,
411) -> Result<TxHandler<Signed>, BridgeError> {
412    let txin = SpendableTxIn::new_partial(input_utxo.outpoint, input_utxo.txout);
413
414    let output_txout = UnspentTxOut::from_partial(output_txout.clone());
415
416    let op_return_txout = op_return_txout(PushBytesBuf::from(operator_xonly_pk.serialize()));
417
418    let mut txhandler = TxHandlerBuilder::new(TransactionType::Payout)
419        .with_version(NON_STANDARD_V3)
420        .add_input(
421            NormalSignatureKind::NotStored,
422            txin,
423            SpendPath::KeySpend,
424            DEFAULT_SEQUENCE,
425        )
426        .add_output(output_txout)
427        .add_output(UnspentTxOut::from_partial(anchor_output(
428            NON_EPHEMERAL_ANCHOR_AMOUNT,
429        )))
430        .add_output(UnspentTxOut::from_partial(op_return_txout))
431        .finalize();
432    txhandler.set_p2tr_key_spend_witness(&user_sig, 0)?;
433    txhandler.promote()
434}
435
436/// Creates a [`TxHandler`] for the `optimistic_payout_tx`.
437///
438/// This transaction is signed by all verifiers that participated in the corresponding deposit give the deposited funds directly to the user withdrawing from Citrea. This way no kickoff/BitVM process is needed.
439///
440/// # Inputs
441/// 1. UTXO: User's withdrawal input (committed in Citrea side, with the signature given to operators off-chain)
442/// 2. MoveToVaultTx: Utxo containing the deposit
443///
444/// # Outputs
445/// 1. User payout output (to the user withdrawing from Citrea)
446/// 2. Anchor output for CPFP
447///
448/// # Arguments
449/// * `deposit_data` - Mutable reference to deposit data.
450/// * `input_utxo` - The input UTXO for the payout, committed in Citrea side, with the signature given to operators off-chain.
451/// * `output_txout` - The output TxOut for the user payout.
452/// * `user_sig` - The user's signature for the payout, given to operators off-chain.
453/// * `paramset` - Protocol parameter set.
454///
455/// # Returns
456/// A [`TxHandler`] for the optimistic payout transaction, or a [`BridgeError`] if construction fails.
457pub fn create_optimistic_payout_txhandler(
458    deposit_data: &mut DepositData,
459    input_utxo: UTXO,
460    output_txout: TxOut,
461    user_sig: taproot::Signature,
462    paramset: &'static ProtocolParamset,
463) -> Result<TxHandler, BridgeError> {
464    let move_txhandler: TxHandler = create_move_to_vault_txhandler(deposit_data, paramset)?;
465    let txin = SpendableTxIn::new_partial(input_utxo.outpoint, input_utxo.txout);
466
467    let output_txout = UnspentTxOut::from_partial(output_txout.clone());
468
469    let mut txhandler = TxHandlerBuilder::new(TransactionType::Payout)
470        .with_version(NON_STANDARD_V3)
471        .add_input(
472            NormalSignatureKind::NotStored,
473            txin,
474            SpendPath::KeySpend,
475            DEFAULT_SEQUENCE,
476        )
477        .add_input(
478            NormalSignatureKind::NotStored,
479            move_txhandler.get_spendable_output(UtxoVout::DepositInMove)?,
480            SpendPath::ScriptSpend(0),
481            DEFAULT_SEQUENCE,
482        )
483        .add_output(output_txout)
484        .add_output(UnspentTxOut::from_partial(
485            builder::transaction::non_ephemeral_anchor_output(),
486        ))
487        .finalize();
488    txhandler.set_p2tr_key_spend_witness(&user_sig, 0)?;
489    Ok(txhandler)
490}