Skip to main content

ark_core/
unilateral_exit.rs

1use crate::anchor_output;
2use crate::contract::SpendSelection;
3use crate::script::extract_checksig_pubkeys;
4use crate::server;
5use crate::Error;
6use crate::ErrorContext;
7use crate::VTXO_CONDITION_KEY;
8use crate::VTXO_INPUT_INDEX;
9use bitcoin::absolute::LockTime;
10use bitcoin::consensus::Decodable;
11use bitcoin::hashes::Hash;
12use bitcoin::hex::DisplayHex;
13use bitcoin::key::Secp256k1;
14use bitcoin::psbt;
15use bitcoin::secp256k1;
16use bitcoin::secp256k1::schnorr;
17use bitcoin::sighash::Prevouts;
18use bitcoin::sighash::SighashCache;
19use bitcoin::taproot;
20use bitcoin::transaction;
21use bitcoin::Address;
22use bitcoin::Amount;
23use bitcoin::OutPoint;
24use bitcoin::Psbt;
25use bitcoin::ScriptBuf;
26use bitcoin::Sequence;
27use bitcoin::TapLeafHash;
28use bitcoin::TapSighashType;
29use bitcoin::Transaction;
30use bitcoin::TxIn;
31use bitcoin::TxOut;
32use bitcoin::Txid;
33use bitcoin::VarInt;
34use bitcoin::Weight;
35use bitcoin::Witness;
36use bitcoin::XOnlyPublicKey;
37use std::collections::HashMap;
38use std::collections::HashSet;
39
40/// A UTXO that could have become a VTXO with the help of the Ark server, but is now unilaterally
41/// spendable by the original owner.
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub struct OnChainInput {
44    /// The sequence needed by the selected spend path.
45    sequence: Sequence,
46    /// The script pubkey of the UTXO.
47    script_pubkey: ScriptBuf,
48    /// The selected spend path.
49    spend_info: (ScriptBuf, taproot::ControlBlock),
50    /// The amount of coins locked in the UTXO.
51    amount: Amount,
52    /// The location of this UTXO in the blockchain.
53    outpoint: OutPoint,
54}
55
56impl OnChainInput {
57    pub fn new(
58        sequence: Sequence,
59        script_pubkey: ScriptBuf,
60        spend_info: (ScriptBuf, taproot::ControlBlock),
61        amount: Amount,
62        outpoint: OutPoint,
63    ) -> Self {
64        Self {
65            sequence,
66            script_pubkey,
67            spend_info,
68            amount,
69            outpoint,
70        }
71    }
72
73    pub fn new_with_spend_selection(
74        default_sequence: Sequence,
75        script_pubkey: ScriptBuf,
76        spend_selection: SpendSelection,
77        amount: Amount,
78        outpoint: OutPoint,
79    ) -> Self {
80        Self::new(
81            spend_selection.sequence.unwrap_or(default_sequence),
82            script_pubkey,
83            spend_selection.spend_info(),
84            amount,
85            outpoint,
86        )
87    }
88
89    pub fn previous_output(&self) -> TxOut {
90        TxOut {
91            value: self.amount,
92            script_pubkey: self.script_pubkey.clone(),
93        }
94    }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Hash)]
98pub struct VtxoInput {
99    outpoint: OutPoint,
100    sequence: Sequence,
101    witness_utxo: TxOut,
102    /// Where the VTXO would end up on the blockchain if it were to become a UTXO.
103    spend_info: (ScriptBuf, taproot::ControlBlock),
104}
105
106impl VtxoInput {
107    pub fn new(
108        outpoint: OutPoint,
109        sequence: Sequence,
110        witness_utxo: TxOut,
111        spend_info: (ScriptBuf, taproot::ControlBlock),
112    ) -> Self {
113        Self {
114            outpoint,
115            sequence,
116            witness_utxo,
117            spend_info,
118        }
119    }
120
121    pub fn new_with_spend_selection(
122        outpoint: OutPoint,
123        default_sequence: Sequence,
124        witness_utxo: TxOut,
125        spend_selection: SpendSelection,
126    ) -> Self {
127        Self::new(
128            outpoint,
129            spend_selection.sequence.unwrap_or(default_sequence),
130            witness_utxo,
131            spend_selection.spend_info(),
132        )
133    }
134
135    pub fn previous_output(&self) -> TxOut {
136        self.witness_utxo.clone()
137    }
138}
139
140/// Build a transaction that spends boarding outputs and VTXOs to an _on-chain_ `to_address`. Any
141/// coins left over after covering the `to_amount` are sent to an on-chain change address.
142///
143/// All these outputs are spent unilaterally i.e. without the collaboration of the Ark server.
144///
145/// To be able to spend a boarding output, we must wait for the exit delay to pass.
146///
147/// To be able to spend a VTXO, the VTXO itself must be published on-chain, and then we must wait
148/// for the exit delay to pass.
149pub fn create_unilateral_exit_transaction<S>(
150    to_address: Address,
151    to_amount: Amount,
152    change_address: Address,
153    onchain_inputs: &[OnChainInput],
154    vtxo_inputs: &[VtxoInput],
155    sign_fn: S,
156) -> Result<Transaction, Error>
157where
158    S: Fn(
159        &mut psbt::Input,
160        secp256k1::Message,
161    ) -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, Error>,
162{
163    if onchain_inputs.is_empty() && vtxo_inputs.is_empty() {
164        return Err(Error::transaction(
165            "cannot create transaction without inputs",
166        ));
167    }
168
169    let secp = Secp256k1::new();
170
171    let mut output = vec![TxOut {
172        value: to_amount,
173        script_pubkey: to_address.script_pubkey(),
174    }];
175
176    let total_amount: Amount = onchain_inputs
177        .iter()
178        .map(|o| o.amount)
179        .chain(vtxo_inputs.iter().map(|v| v.witness_utxo.value))
180        .sum();
181
182    let change_amount = total_amount.checked_sub(to_amount).ok_or_else(|| {
183        Error::transaction(format!(
184            "cannot cover to_amount ({to_amount}) with total input amount ({total_amount})"
185        ))
186    })?;
187
188    if change_amount > Amount::ZERO {
189        output.push(TxOut {
190            value: change_amount,
191            script_pubkey: change_address.script_pubkey(),
192        });
193    }
194
195    let input = {
196        let onchain_inputs = onchain_inputs.iter().map(|o| TxIn {
197            previous_output: o.outpoint,
198            sequence: o.sequence,
199            ..Default::default()
200        });
201
202        let vtxo_inputs = vtxo_inputs.iter().map(|v| TxIn {
203            previous_output: v.outpoint,
204            sequence: v.sequence,
205            ..Default::default()
206        });
207
208        onchain_inputs.chain(vtxo_inputs).collect::<Vec<_>>()
209    };
210
211    let mut psbt = Psbt::from_unsigned_tx(Transaction {
212        version: transaction::Version::TWO,
213        lock_time: LockTime::ZERO,
214        input,
215        output,
216    })
217    .map_err(Error::transaction)?;
218
219    // Add a `witness_utxo` for every transaction input.
220    for (i, input) in psbt.inputs.iter_mut().enumerate() {
221        let outpoint = psbt.unsigned_tx.input[i].previous_output;
222
223        for onchain_input in onchain_inputs {
224            if onchain_input.outpoint == outpoint {
225                input.witness_utxo = Some(TxOut {
226                    value: onchain_input.amount,
227                    script_pubkey: onchain_input.script_pubkey.clone(),
228                });
229
230                let (script, cb) = onchain_input.spend_info.clone();
231                let leaf_version = cb.leaf_version;
232                input.tap_scripts.insert(cb, (script, leaf_version));
233            }
234        }
235
236        for vtxo_input in vtxo_inputs.iter() {
237            if vtxo_input.outpoint == outpoint {
238                input.witness_utxo = Some(TxOut {
239                    value: vtxo_input.witness_utxo.value,
240                    script_pubkey: vtxo_input.witness_utxo.script_pubkey.clone(),
241                });
242
243                let (script, cb) = vtxo_input.spend_info.clone();
244                let leaf_version = cb.leaf_version;
245                input.tap_scripts.insert(cb, (script, leaf_version));
246            }
247        }
248    }
249
250    // Collect all `witness_utxo` entries.
251    let prevouts = psbt
252        .inputs
253        .iter()
254        .filter_map(|i| i.witness_utxo.clone())
255        .collect::<Vec<_>>();
256
257    // Sign each input.
258    for (i, input) in psbt.inputs.iter_mut().enumerate() {
259        let (exit_control_block, (exit_script, leaf_version)) = input
260            .tap_scripts
261            .pop_first()
262            .ok_or_else(|| Error::ad_hoc(format!("no exit script found for input {i}")))?;
263
264        input.witness_script = Some(exit_script.clone());
265
266        let leaf_hash = TapLeafHash::from_script(&exit_script, leaf_version);
267
268        let tap_sighash = SighashCache::new(&psbt.unsigned_tx)
269            .taproot_script_spend_signature_hash(
270                i,
271                &Prevouts::All(&prevouts),
272                leaf_hash,
273                TapSighashType::Default,
274            )
275            .map_err(Error::crypto)?;
276
277        let msg = secp256k1::Message::from_digest(tap_sighash.to_raw_hash().to_byte_array());
278
279        let sigs = sign_fn(input, msg)?;
280
281        let mut witness = Vec::new();
282        for (sig, pk) in sigs.iter() {
283            secp.verify_schnorr(sig, &msg, pk)
284                .map_err(Error::crypto)
285                .with_context(|| format!("failed to verify own signature for input {i}"))?;
286
287            witness.push(&sig[..]);
288        }
289
290        witness.push(exit_script.as_bytes());
291
292        let control_block = exit_control_block.serialize();
293        witness.push(control_block.as_slice());
294
295        let witness = Witness::from_slice(&witness);
296
297        input.final_script_witness = Some(witness);
298    }
299
300    let tx = psbt.clone().extract_tx().map_err(Error::transaction)?;
301
302    tracing::debug!(
303        ?onchain_inputs,
304        ?vtxo_inputs,
305        raw_tx = %bitcoin::consensus::serialize(&tx).as_hex(),
306        "Built transaction sending inputs to on-chain address"
307    );
308
309    Ok(tx)
310}
311
312/// Build a topologically sorted unilateral exit branch of TXIDs for a VTXO from a
313/// [`server::VtxoChains`].
314///
315/// The returned branch contains every virtual transaction in the ancestor sub-DAG exactly once,
316/// ordered so every transaction appears after its virtual parents. This avoids enumerating every
317/// distinct root-to-leaf path, which can be exponential when a VTXO has many merged ancestors.
318pub fn build_unilateral_exit_tree_txids(
319    vtxo_chains: &server::VtxoChains,
320    // The TXID of the VTXO we want to commit on-chain.
321    ark_txid: Txid,
322) -> Result<Vec<Vec<Txid>>, Error> {
323    let chain_map = vtxo_chains
324        .inner
325        .iter()
326        .map(|vtxo_chain| (vtxo_chain.txid, vtxo_chain))
327        .collect::<HashMap<_, _>>();
328
329    fn visit_virtual_ancestors(
330        current_txid: Txid,
331        chain_map: &HashMap<Txid, &server::VtxoChain>,
332        visiting: &mut HashSet<Txid>,
333        visited: &mut HashSet<Txid>,
334        sorted: &mut Vec<Txid>,
335    ) -> Result<bool, Error> {
336        if visited.contains(&current_txid) {
337            return Ok(true);
338        }
339
340        if !visiting.insert(current_txid) {
341            return Err(Error::ad_hoc("chain traversal led to cycle"));
342        }
343
344        let chain = chain_map.get(&current_txid).ok_or_else(|| {
345            Error::ad_hoc(format!("could not find VtxoChain for TXID: {current_txid}"))
346        })?;
347
348        if chain.spends.is_empty() {
349            return Err(Error::ad_hoc(format!(
350                "dead end reached at TXID {current_txid} with no commitment transaction"
351            )));
352        }
353
354        let mut reached_commitment = false;
355        for &parent_txid in &chain.spends {
356            let parent_chain = chain_map.get(&parent_txid).ok_or_else(|| {
357                Error::ad_hoc(format!(
358                    "could not find VtxoChain for parent TXID: {parent_txid}",
359                ))
360            })?;
361
362            match parent_chain.tx_type {
363                server::ChainedTxType::Commitment => {
364                    reached_commitment = true;
365                }
366                server::ChainedTxType::Ark
367                | server::ChainedTxType::Checkpoint
368                | server::ChainedTxType::Tree => {
369                    reached_commitment |=
370                        visit_virtual_ancestors(parent_txid, chain_map, visiting, visited, sorted)?;
371                }
372                server::ChainedTxType::Unspecified => {
373                    tracing::warn!(
374                        txid = %parent_txid,
375                        "Found unspecified TX type when walking up virtual TX tree. \
376                         Treating it like a virtual TX"
377                    );
378
379                    reached_commitment |=
380                        visit_virtual_ancestors(parent_txid, chain_map, visiting, visited, sorted)?;
381                }
382            }
383        }
384
385        visiting.remove(&current_txid);
386        visited.insert(current_txid);
387        sorted.push(current_txid);
388
389        Ok(reached_commitment)
390    }
391
392    let mut visiting = HashSet::new();
393    let mut visited = HashSet::new();
394    let mut sorted = Vec::new();
395
396    if !visit_virtual_ancestors(
397        ark_txid,
398        &chain_map,
399        &mut visiting,
400        &mut visited,
401        &mut sorted,
402    )? {
403        return Err(Error::ad_hoc(format!(
404            "no path found from Ark TX {ark_txid} to commitment transaction",
405        )));
406    }
407
408    Ok(vec![sorted])
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    fn txid(n: u8) -> Txid {
416        Txid::from_byte_array([n; 32])
417    }
418
419    fn chain(
420        txid: Txid,
421        tx_type: server::ChainedTxType,
422        spends: impl Into<Vec<Txid>>,
423    ) -> server::VtxoChain {
424        server::VtxoChain {
425            txid,
426            tx_type,
427            spends: spends.into(),
428            expires_at: 0,
429        }
430    }
431
432    fn exit_branch(chains: Vec<server::VtxoChain>, ark_txid: Txid) -> Vec<Txid> {
433        build_unilateral_exit_tree_txids(&server::VtxoChains { inner: chains }, ark_txid)
434            .expect("valid unilateral exit branch")
435            .pop()
436            .expect("one topological branch")
437    }
438
439    #[test]
440    fn condition_witness_elements_decode_encoded_witness() {
441        let elements = vec![
442            b"preimage".to_vec(),
443            Vec::new(),
444            vec![0; 253],
445            vec![1, 2, 3, 4],
446        ];
447        let mut input = psbt::Input::default();
448
449        input.unknown.insert(
450            psbt::raw::Key {
451                type_value: 222,
452                key: VTXO_CONDITION_KEY.to_vec(),
453            },
454            crate::intent::encode_witness(&elements),
455        );
456
457        assert_eq!(condition_witness_elements(&input).unwrap(), elements);
458    }
459
460    #[test]
461    fn unilateral_exit_txids_for_linear_chain_are_parent_first() {
462        let commitment = txid(1);
463        let tree = txid(2);
464        let ark = txid(3);
465
466        let branch = exit_branch(
467            vec![
468                chain(commitment, server::ChainedTxType::Commitment, []),
469                chain(tree, server::ChainedTxType::Tree, [commitment]),
470                chain(ark, server::ChainedTxType::Ark, [tree]),
471            ],
472            ark,
473        );
474
475        assert_eq!(branch, vec![tree, ark]);
476    }
477
478    #[test]
479    fn unilateral_exit_txids_deduplicate_merged_ancestor_dag() {
480        let commitment = txid(1);
481        let left = txid(2);
482        let right = txid(3);
483        let merge = txid(4);
484        let ark = txid(5);
485
486        let branch = exit_branch(
487            vec![
488                chain(commitment, server::ChainedTxType::Commitment, []),
489                chain(left, server::ChainedTxType::Tree, [commitment]),
490                chain(right, server::ChainedTxType::Tree, [commitment]),
491                chain(merge, server::ChainedTxType::Checkpoint, [left, right]),
492                chain(ark, server::ChainedTxType::Ark, [merge]),
493            ],
494            ark,
495        );
496
497        assert_eq!(branch, vec![left, right, merge, ark]);
498    }
499
500    #[test]
501    fn unilateral_exit_txids_avoid_exponential_path_enumeration() {
502        let commitment = txid(1);
503        let a1 = txid(2);
504        let b1 = txid(3);
505        let m1 = txid(4);
506        let a2 = txid(5);
507        let b2 = txid(6);
508        let m2 = txid(7);
509        let ark = txid(8);
510
511        let branch = exit_branch(
512            vec![
513                chain(commitment, server::ChainedTxType::Commitment, []),
514                chain(a1, server::ChainedTxType::Tree, [commitment]),
515                chain(b1, server::ChainedTxType::Tree, [commitment]),
516                chain(m1, server::ChainedTxType::Checkpoint, [a1, b1]),
517                chain(a2, server::ChainedTxType::Tree, [m1]),
518                chain(b2, server::ChainedTxType::Tree, [m1]),
519                chain(m2, server::ChainedTxType::Checkpoint, [a2, b2]),
520                chain(ark, server::ChainedTxType::Ark, [m2]),
521            ],
522            ark,
523        );
524
525        assert_eq!(branch, vec![a1, b1, m1, a2, b2, m2, ark]);
526    }
527
528    #[test]
529    fn unilateral_exit_txids_reject_cycles() {
530        let a = txid(1);
531        let b = txid(2);
532
533        let err = build_unilateral_exit_tree_txids(
534            &server::VtxoChains {
535                inner: vec![
536                    chain(a, server::ChainedTxType::Ark, [b]),
537                    chain(b, server::ChainedTxType::Checkpoint, [a]),
538                ],
539            },
540            a,
541        )
542        .expect_err("cycle should be rejected");
543
544        assert!(err.to_string().contains("cycle"));
545    }
546}
547
548/// The full path from a commitment transaction to a VTXO. The entire path must be published
549/// on-chain to execute a unilateral exit with this VTXO.
550///
551/// A branch may contain both batch-tree internal node transactions, which spend their parent via
552/// key path, and VTXO spend transactions, which spend a confirmed or pre-confirmed VTXO via script
553/// path. We use the word "tree" because a VTXO may come from more than one path, e.g. if its
554/// corresponding Ark transaction has more than one input.
555pub struct UnilateralExitTree {
556    /// The commitment transactions from which this VTXO comes from.
557    ///
558    /// A pre-confirmed VTXO can have ancestors from more than one batch, hence the list.
559    commitment_txids: Vec<Txid>,
560    /// The chains of virtual transactions that lead to a VTXO.
561    ///
562    /// Virtual TXs in a branch are ordered by distance to the root commitment transaction, with
563    /// virtual TXs closest to it appearing first.
564    inner: Vec<Vec<Psbt>>,
565}
566
567impl UnilateralExitTree {
568    pub fn new(commitment_txids: Vec<Txid>, virtual_tx_tree: Vec<Vec<Psbt>>) -> Self {
569        Self {
570            commitment_txids,
571            inner: virtual_tx_tree,
572        }
573    }
574
575    pub fn inner(&self) -> &Vec<Vec<Psbt>> {
576        &self.inner
577    }
578
579    pub fn commitment_txids(&self) -> &[Txid] {
580        &self.commitment_txids
581    }
582}
583
584/// Finalize a virtual transaction input using only the authorization data already present in the
585/// PSBT input.
586///
587/// This is intended for historical virtual transactions in a unilateral-exit branch. The caller
588/// provides the `witness_utxo` for the input being finalized, and this function materializes either
589/// the taproot key-spend witness used by batch-tree internal nodes or a satisfiable taproot
590/// script-spend witness used when spending VTXOs.
591pub fn finalize_virtual_tx_input(
592    mut psbt: Psbt,
593    input_index: usize,
594    witness_utxo: TxOut,
595) -> Result<Transaction, Error> {
596    let input = psbt
597        .inputs
598        .get_mut(input_index)
599        .ok_or_else(|| Error::transaction(format!("missing PSBT input {input_index}")))?;
600
601    input.witness_utxo = Some(witness_utxo);
602
603    let txid = psbt.unsigned_tx.compute_txid();
604
605    if let Some(tap_key_sig) = input.tap_key_sig {
606        tracing::debug!(%txid, "Finalizing batch-tree internal node key spend");
607
608        input.final_script_witness = Some(Witness::p2tr_key_spend(&tap_key_sig));
609    } else {
610        tracing::debug!(%txid, "Finalizing VTXO script spend");
611
612        input.final_script_witness = Some(finalize_taproot_script_spend_witness(input)?);
613    }
614
615    psbt.extract_tx().map_err(Error::transaction)
616}
617
618/// Build the final witness for a taproot script-spend input from its PSBT data.
619///
620/// The selected tapleaf is the first tap script for which signatures are available for every
621/// `CHECKSIG`/`CHECKSIGVERIFY` pubkey in the script. Signatures are pushed in reverse script order.
622/// Extra condition witness elements, such as VHTLC preimages, are read from the
623/// `VTXO_CONDITION_KEY` unknown input field and pushed after signatures.
624///
625/// Condition witness elements are therefore placed at the top of the initial script stack. This
626/// requires condition-checking opcodes, such as `OP_HASH160` or `OP_SIZE`, to appear before any
627/// `CHECKSIG`/`CHECKSIGVERIFY` opcode in the tapleaf script.
628pub fn finalize_taproot_script_spend_witness(input: &psbt::Input) -> Result<Witness, Error> {
629    for (control_block, (script, leaf_version)) in input.tap_scripts.iter() {
630        let leaf_hash = TapLeafHash::from_script(script, *leaf_version);
631        let pubkeys = extract_checksig_pubkeys(script);
632
633        if pubkeys.is_empty() {
634            continue;
635        }
636
637        let signatures = pubkeys
638            .iter()
639            .map(|pk| {
640                input
641                    .tap_script_sigs
642                    .get(&(*pk, leaf_hash))
643                    .map(|sig| sig.to_vec())
644            })
645            .collect::<Option<Vec<_>>>();
646
647        let Some(signatures) = signatures else {
648            continue;
649        };
650
651        let mut witness = Witness::new();
652
653        for signature in signatures.into_iter().rev() {
654            witness.push(signature);
655        }
656
657        for element in condition_witness_elements(input)? {
658            witness.push(element);
659        }
660
661        witness.push(script.as_bytes());
662        witness.push(control_block.serialize());
663
664        return Ok(witness);
665    }
666
667    Err(Error::transaction(
668        "no satisfiable taproot script-spend leaf found in PSBT input",
669    ))
670}
671
672fn condition_witness_elements(input: &psbt::Input) -> Result<Vec<Vec<u8>>, Error> {
673    let condition_key = psbt::raw::Key {
674        type_value: 222,
675        key: VTXO_CONDITION_KEY.to_vec(),
676    };
677
678    let Some(condition_data) = input.unknown.get(&condition_key) else {
679        return Ok(Vec::new());
680    };
681
682    let mut cursor = std::io::Cursor::new(condition_data);
683    let element_count = VarInt::consensus_decode(&mut cursor)
684        .map_err(|e| Error::transaction(format!("failed to decode condition count: {e}")))?
685        .0;
686
687    let count_end = usize::try_from(cursor.position())
688        .map_err(|_| Error::transaction("condition cursor position overflow"))?;
689    let remaining_after_count = condition_data.len().saturating_sub(count_end);
690    let element_count = usize::try_from(element_count)
691        .map_err(|_| Error::transaction("condition witness element count overflow"))?;
692
693    // Each element needs at least a compact-size length byte, even when the element itself is
694    // empty.
695    if element_count > remaining_after_count {
696        return Err(Error::transaction(format!(
697            "condition witness element count {element_count} exceeds remaining buffer size {remaining_after_count}"
698        )));
699    }
700
701    let mut elements = Vec::with_capacity(element_count);
702    for _ in 0..element_count {
703        let element_len = VarInt::consensus_decode(&mut cursor)
704            .map_err(|e| Error::transaction(format!("failed to decode condition length: {e}")))?
705            .0;
706        let element_len = usize::try_from(element_len)
707            .map_err(|_| Error::transaction("condition witness element length overflow"))?;
708        let start = usize::try_from(cursor.position())
709            .map_err(|_| Error::transaction("condition cursor position overflow"))?;
710        let end = start
711            .checked_add(element_len)
712            .ok_or_else(|| Error::transaction("condition witness element end overflow"))?;
713
714        if condition_data.len() < end {
715            return Err(Error::transaction(format!(
716                "condition witness element too short: expected {element_len} bytes, got {}",
717                condition_data.len().saturating_sub(start)
718            )));
719        }
720
721        elements.push(condition_data[start..end].to_vec());
722        cursor.set_position(end as u64);
723    }
724
725    Ok(elements)
726}
727
728/// Finalize all virtual transactions needed to commit a VTXO on-chain.
729pub fn finalize_unilateral_exit_tree(
730    unilateral_exit_tree: &UnilateralExitTree,
731    commitment_txs: &[Transaction],
732) -> Result<Vec<Vec<Transaction>>, Error> {
733    let mut finalized_virtual_tx_branches = Vec::new();
734    for unilateral_exit_branch in unilateral_exit_tree.inner.iter() {
735        let mut finalized_unilateral_exit_branch = Vec::new();
736        for virtual_tx in unilateral_exit_branch.iter() {
737            let psbt = virtual_tx.clone();
738
739            let virtual_tx_previous_output =
740                psbt.unsigned_tx.input[VTXO_INPUT_INDEX].previous_output;
741
742            let witness_utxo = {
743                unilateral_exit_branch
744                    .iter()
745                    .map(|p| &p.unsigned_tx)
746                    .chain(commitment_txs.iter())
747                    .find_map(|other_psbt| {
748                        (other_psbt.compute_txid() == virtual_tx_previous_output.txid).then_some(
749                            other_psbt.output[virtual_tx_previous_output.vout as usize].clone(),
750                        )
751                    })
752            }
753            .ok_or_else(|| {
754                Error::ad_hoc(format!(
755                    "no witness UTXO found for virtual TX outpoint {virtual_tx_previous_output}"
756                ))
757            })?;
758
759            let tx = finalize_virtual_tx_input(psbt, VTXO_INPUT_INDEX, witness_utxo)?;
760
761            finalized_unilateral_exit_branch.push(tx);
762        }
763        finalized_virtual_tx_branches.push(finalized_unilateral_exit_branch);
764    }
765
766    Ok(finalized_virtual_tx_branches)
767}
768
769#[deprecated(note = "use finalize_unilateral_exit_tree")]
770pub fn sign_unilateral_exit_tree(
771    unilateral_exit_tree: &UnilateralExitTree,
772    commitment_txs: &[Transaction],
773) -> Result<Vec<Vec<Transaction>>, Error> {
774    finalize_unilateral_exit_tree(unilateral_exit_tree, commitment_txs)
775}
776
777#[derive(Debug, Clone, PartialEq, Eq)]
778pub struct SelectedUtxo {
779    pub outpoint: OutPoint,
780    pub amount: Amount,
781    pub address: Address,
782}
783
784#[derive(Debug, Clone)]
785pub struct UtxoCoinSelection {
786    pub selected_utxos: Vec<SelectedUtxo>,
787    pub total_selected: Amount,
788    pub change_amount: Amount,
789}
790
791/// Build an anchor transaction by spending a 0-value P2A output and adding another output to cover
792/// the transaction fees.
793pub fn build_anchor_tx<F>(
794    bumpable_tx: &Transaction,
795    change_address: Address,
796    fee_rate: f64,
797    select_coins_fn: F,
798) -> Result<Psbt, Error>
799where
800    F: FnOnce(Amount) -> Result<UtxoCoinSelection, Error>,
801{
802    let anchor = find_anchor_outpoint(bumpable_tx)?;
803
804    // Estimate for the size of the bump transaction.
805    const P2TR_KEYSPEND_INPUT_WEIGHT: u64 = 57 * 4 + 64; // 292 weight units
806    const NESTED_P2WSH_INPUT_WEIGHT: u64 = 91 * 4 + 3 * 4; // 376 weight units
807    const P2TR_OUTPUT_WEIGHT: u64 = 43 * 4; // 172 weight units
808
809    // We assume only one UTXO will be selected to have a correct estimate.
810    let estimated_weight = Weight::from_wu(
811        NESTED_P2WSH_INPUT_WEIGHT + P2TR_KEYSPEND_INPUT_WEIGHT + P2TR_OUTPUT_WEIGHT,
812    );
813
814    let child_vsize = estimated_weight.to_vbytes_ceil();
815    let package_size = child_vsize + bumpable_tx.weight().to_vbytes_ceil();
816
817    let fee = Amount::from_sat((package_size as f64 * fee_rate).ceil() as u64);
818
819    // Use dependency to select coins to cover the fee.
820    let UtxoCoinSelection {
821        selected_utxos,
822        total_selected,
823        change_amount,
824    } = select_coins_fn(fee)?;
825
826    if total_selected < fee {
827        return Err(Error::coin_select(format!(
828            "insufficient coins selected to cover {fee} fee"
829        )));
830    }
831
832    // Build inputs and outputs.
833    let mut inputs = vec![anchor];
834    let mut sequences = vec![Sequence::MAX];
835
836    for utxo in selected_utxos.iter() {
837        inputs.push(utxo.outpoint);
838        sequences.push(Sequence::MAX);
839    }
840
841    let outputs = vec![TxOut {
842        value: change_amount,
843        script_pubkey: change_address.script_pubkey(),
844    }];
845
846    // Create PSBT.
847    let mut psbt = Psbt::from_unsigned_tx(Transaction {
848        version: transaction::Version::non_standard(3),
849        lock_time: LockTime::ZERO,
850        input: inputs
851            .iter()
852            .zip(sequences.iter())
853            .map(|(outpoint, sequence)| TxIn {
854                previous_output: *outpoint,
855                script_sig: ScriptBuf::new(),
856                sequence: *sequence,
857                witness: Witness::new(),
858            })
859            .collect(),
860        output: outputs,
861    })
862    .map_err(|e| Error::transaction(format!("Failed to create PSBT: {e}")))?;
863
864    // Set witness UTXO for anchor input (first input). The anchor input does not need signing,
865    // hence the empty witness.
866    psbt.inputs[0].witness_utxo = Some(anchor_output());
867    psbt.inputs[0].final_script_witness = Some(Witness::new());
868
869    // Set witness UTXO for the additional inputs (probably just one).
870    for i in 1..psbt.inputs.len() {
871        if let Some(utxo) = selected_utxos.get(i - 1) {
872            psbt.inputs[i].witness_utxo = Some(TxOut {
873                value: utxo.amount,
874                script_pubkey: utxo.address.script_pubkey(),
875            });
876        }
877    }
878
879    Ok(psbt)
880}
881
882fn find_anchor_outpoint(tx: &Transaction) -> Result<OutPoint, Error> {
883    let anchor_output_template = anchor_output();
884
885    for (index, output) in tx.output.iter().enumerate() {
886        if output == &anchor_output_template {
887            return Ok(OutPoint {
888                txid: tx.compute_txid(),
889                vout: index as u32,
890            });
891        }
892    }
893
894    Err(Error::transaction("anchor output not found in transaction"))
895}