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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub struct OnChainInput {
44 boarding_output: BoardingOutput,
46 amount: Amount,
48 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 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
98pub 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 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 let prevouts = psbt
210 .inputs
211 .iter()
212 .filter_map(|i| i.witness_utxo.clone())
213 .collect::<Vec<_>>();
214
215 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
270pub fn build_unilateral_exit_tree_txids(
277 vtxo_chains: &server::VtxoChains,
278 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(¤t_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(¤t_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(¤t_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
506pub struct UnilateralExitTree {
514 commitment_txids: Vec<Txid>,
518 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
542pub 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
576pub 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 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
686pub 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
749pub 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 const P2TR_KEYSPEND_INPUT_WEIGHT: u64 = 57 * 4 + 64; const NESTED_P2WSH_INPUT_WEIGHT: u64 = 91 * 4 + 3 * 4; const P2TR_OUTPUT_WEIGHT: u64 = 43 * 4; 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 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 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 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 psbt.inputs[0].witness_utxo = Some(anchor_output());
825 psbt.inputs[0].final_script_witness = Some(Witness::new());
826
827 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}