Skip to main content

ark_core/
unilateral_exit.rs

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