1use crate::anchor_output;
2use crate::server;
3use crate::BoardingOutput;
4use crate::Error;
5use crate::ErrorContext;
6use crate::VTXO_INPUT_INDEX;
7use bitcoin::absolute::LockTime;
8use bitcoin::hashes::Hash;
9use bitcoin::hex::DisplayHex;
10use bitcoin::key::Secp256k1;
11use bitcoin::opcodes::all::*;
12use bitcoin::psbt;
13use bitcoin::secp256k1;
14use bitcoin::secp256k1::schnorr;
15use bitcoin::sighash::Prevouts;
16use bitcoin::sighash::SighashCache;
17use bitcoin::taproot;
18use bitcoin::transaction;
19use bitcoin::Address;
20use bitcoin::Amount;
21use bitcoin::OutPoint;
22use bitcoin::Psbt;
23use bitcoin::ScriptBuf;
24use bitcoin::Sequence;
25use bitcoin::TapLeafHash;
26use bitcoin::TapSighashType;
27use bitcoin::Transaction;
28use bitcoin::TxIn;
29use bitcoin::TxOut;
30use bitcoin::Txid;
31use bitcoin::Weight;
32use bitcoin::Witness;
33use bitcoin::XOnlyPublicKey;
34use std::collections::HashMap;
35use std::collections::HashSet;
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct OnChainInput {
41 boarding_output: BoardingOutput,
43 amount: Amount,
45 outpoint: OutPoint,
47}
48
49impl OnChainInput {
50 pub fn new(boarding_output: BoardingOutput, amount: Amount, outpoint: OutPoint) -> Self {
51 Self {
52 boarding_output,
53 amount,
54 outpoint,
55 }
56 }
57
58 pub fn previous_output(&self) -> TxOut {
59 TxOut {
60 value: self.amount,
61 script_pubkey: self.boarding_output.script_pubkey(),
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67pub struct VtxoInput {
68 outpoint: OutPoint,
69 sequence: Sequence,
70 witness_utxo: TxOut,
71 spend_info: (ScriptBuf, taproot::ControlBlock),
73}
74
75impl VtxoInput {
76 pub fn new(
77 outpoint: OutPoint,
78 sequence: Sequence,
79 witness_utxo: TxOut,
80 spend_info: (ScriptBuf, taproot::ControlBlock),
81 ) -> Self {
82 Self {
83 outpoint,
84 sequence,
85 witness_utxo,
86 spend_info,
87 }
88 }
89
90 pub fn previous_output(&self) -> TxOut {
91 self.witness_utxo.clone()
92 }
93}
94
95pub fn create_unilateral_exit_transaction<S>(
105 to_address: Address,
106 to_amount: Amount,
107 change_address: Address,
108 onchain_inputs: &[OnChainInput],
109 vtxo_inputs: &[VtxoInput],
110 sign_fn: S,
111) -> Result<Transaction, Error>
112where
113 S: Fn(
114 &mut psbt::Input,
115 secp256k1::Message,
116 ) -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, Error>,
117{
118 if onchain_inputs.is_empty() && vtxo_inputs.is_empty() {
119 return Err(Error::transaction(
120 "cannot create transaction without inputs",
121 ));
122 }
123
124 let secp = Secp256k1::new();
125
126 let mut output = vec![TxOut {
127 value: to_amount,
128 script_pubkey: to_address.script_pubkey(),
129 }];
130
131 let total_amount: Amount = onchain_inputs
132 .iter()
133 .map(|o| o.amount)
134 .chain(vtxo_inputs.iter().map(|v| v.witness_utxo.value))
135 .sum();
136
137 let change_amount = total_amount.checked_sub(to_amount).ok_or_else(|| {
138 Error::transaction(format!(
139 "cannot cover to_amount ({to_amount}) with total input amount ({total_amount})"
140 ))
141 })?;
142
143 if change_amount > Amount::ZERO {
144 output.push(TxOut {
145 value: change_amount,
146 script_pubkey: change_address.script_pubkey(),
147 });
148 }
149
150 let input = {
151 let onchain_inputs = onchain_inputs.iter().map(|o| TxIn {
152 previous_output: o.outpoint,
153 sequence: o.boarding_output.exit_delay(),
154 ..Default::default()
155 });
156
157 let vtxo_inputs = vtxo_inputs.iter().map(|v| TxIn {
158 previous_output: v.outpoint,
159 sequence: v.sequence,
160 ..Default::default()
161 });
162
163 onchain_inputs.chain(vtxo_inputs).collect::<Vec<_>>()
164 };
165
166 let mut psbt = Psbt::from_unsigned_tx(Transaction {
167 version: transaction::Version::TWO,
168 lock_time: LockTime::ZERO,
169 input,
170 output,
171 })
172 .map_err(Error::transaction)?;
173
174 for (i, input) in psbt.inputs.iter_mut().enumerate() {
176 let outpoint = psbt.unsigned_tx.input[i].previous_output;
177
178 for onchain_input in onchain_inputs {
179 if onchain_input.outpoint == outpoint {
180 input.witness_utxo = Some(TxOut {
181 value: onchain_input.amount,
182 script_pubkey: onchain_input.boarding_output.address().script_pubkey(),
183 });
184
185 let (script, cb) = onchain_input.boarding_output.exit_spend_info();
186 let leaf_version = cb.leaf_version;
187 input.tap_scripts.insert(cb, (script, leaf_version));
188 }
189 }
190
191 for vtxo_input in vtxo_inputs.iter() {
192 if vtxo_input.outpoint == outpoint {
193 input.witness_utxo = Some(TxOut {
194 value: vtxo_input.witness_utxo.value,
195 script_pubkey: vtxo_input.witness_utxo.script_pubkey.clone(),
196 });
197
198 let (script, cb) = vtxo_input.spend_info.clone();
199 let leaf_version = cb.leaf_version;
200 input.tap_scripts.insert(cb, (script, leaf_version));
201 }
202 }
203 }
204
205 let prevouts = psbt
207 .inputs
208 .iter()
209 .filter_map(|i| i.witness_utxo.clone())
210 .collect::<Vec<_>>();
211
212 for (i, input) in psbt.inputs.iter_mut().enumerate() {
214 let (exit_control_block, (exit_script, leaf_version)) = input
215 .tap_scripts
216 .pop_first()
217 .ok_or_else(|| Error::ad_hoc(format!("no exit script found for input {i}")))?;
218
219 input.witness_script = Some(exit_script.clone());
220
221 let leaf_hash = TapLeafHash::from_script(&exit_script, leaf_version);
222
223 let tap_sighash = SighashCache::new(&psbt.unsigned_tx)
224 .taproot_script_spend_signature_hash(
225 i,
226 &Prevouts::All(&prevouts),
227 leaf_hash,
228 TapSighashType::Default,
229 )
230 .map_err(Error::crypto)?;
231
232 let msg = secp256k1::Message::from_digest(tap_sighash.to_raw_hash().to_byte_array());
233
234 let sigs = sign_fn(input, msg)?;
235
236 let mut witness = Vec::new();
237 for (sig, pk) in sigs.iter() {
238 secp.verify_schnorr(sig, &msg, pk)
239 .map_err(Error::crypto)
240 .with_context(|| format!("failed to verify own signature for input {i}"))?;
241
242 witness.push(&sig[..]);
243 }
244
245 witness.push(exit_script.as_bytes());
246
247 let control_block = exit_control_block.serialize();
248 witness.push(control_block.as_slice());
249
250 let witness = Witness::from_slice(&witness);
251
252 input.final_script_witness = Some(witness);
253 }
254
255 let tx = psbt.clone().extract_tx().map_err(Error::transaction)?;
256
257 tracing::debug!(
258 ?onchain_inputs,
259 ?vtxo_inputs,
260 raw_tx = %bitcoin::consensus::serialize(&tx).as_hex(),
261 "Built transaction sending inputs to on-chain address"
262 );
263
264 Ok(tx)
265}
266
267pub fn build_unilateral_exit_tree_txids(
269 vtxo_chains: &server::VtxoChains,
270 ark_txid: Txid,
272) -> Result<Vec<Vec<Txid>>, Error> {
273 let mut chain_map: HashMap<Txid, &server::VtxoChain> = HashMap::new();
275 for vtxo_chain in &vtxo_chains.inner {
276 chain_map.insert(vtxo_chain.txid, vtxo_chain);
277 }
278
279 fn find_paths_to_commitment(
282 current_txid: Txid,
283 chain_map: &HashMap<Txid, &server::VtxoChain>,
284 current_path: &mut Vec<Txid>,
285 all_paths: &mut Vec<Vec<Txid>>,
286 visited: &mut HashSet<Txid>,
287 ) -> Result<(), Error> {
288 if current_path.len() > 1_000 {
290 return Err(Error::ad_hoc(
291 "chain traversal exceeded maximum depth of 1000",
292 ));
293 }
294
295 if visited.contains(¤t_txid) {
297 return Err(Error::ad_hoc("chain traversal led to cycle"));
298 }
299 visited.insert(current_txid);
300
301 current_path.push(current_txid);
303
304 let chain = chain_map.get(¤t_txid).ok_or_else(|| {
306 Error::ad_hoc(format!("could not find VtxoChain for TXID: {current_txid}",))
307 })?;
308 let mut reached_commitment = false;
310
311 for &parent_txid in &chain.spends {
312 let parent_chain = chain_map.get(&parent_txid).ok_or_else(|| {
314 Error::ad_hoc(format!(
315 "could not find VtxoChain for parent TXID: {parent_txid}",
316 ))
317 })?;
318
319 match parent_chain.tx_type {
320 server::ChainedTxType::Commitment => {
321 all_paths.push(current_path.clone());
323
324 reached_commitment = true;
325 }
326 server::ChainedTxType::Ark
327 | server::ChainedTxType::Checkpoint
328 | server::ChainedTxType::Tree => {
329 find_paths_to_commitment(
331 parent_txid,
332 chain_map,
333 current_path,
334 all_paths,
335 visited,
336 )?;
337 }
338 server::ChainedTxType::Unspecified => {
339 tracing::warn!(
340 txid = %parent_txid,
341 "Found unspecified TX type when walking up virtual TX tree. \
342 Treating it like a virtual TX"
343 );
344
345 find_paths_to_commitment(
347 parent_txid,
348 chain_map,
349 current_path,
350 all_paths,
351 visited,
352 )?;
353 }
354 }
355 }
356
357 if !reached_commitment && chain.spends.is_empty() {
358 return Err(Error::ad_hoc(format!(
359 "dead end reached at TXID {current_txid} with no commitment transaction"
360 )));
361 }
362
363 visited.remove(¤t_txid);
364 current_path.pop();
365 Ok(())
366 }
367
368 let mut all_paths = Vec::new();
369 let mut current_path = Vec::new();
370 let mut visited = HashSet::new();
371
372 find_paths_to_commitment(
373 ark_txid,
374 &chain_map,
375 &mut current_path,
376 &mut all_paths,
377 &mut visited,
378 )?;
379
380 if all_paths.is_empty() {
381 return Err(Error::ad_hoc(format!(
382 "no paths found from Ark TX {ark_txid} to commitment transaction",
383 )));
384 }
385
386 let all_paths: Vec<Vec<Txid>> = all_paths
388 .into_iter()
389 .map(|mut path| {
390 path.reverse();
391 path
392 })
393 .collect();
394
395 Ok(all_paths)
396}
397
398pub struct UnilateralExitTree {
404 commitment_txids: Vec<Txid>,
408 inner: Vec<Vec<Psbt>>,
413}
414
415impl UnilateralExitTree {
416 pub fn new(commitment_txids: Vec<Txid>, virtual_tx_tree: Vec<Vec<Psbt>>) -> Self {
417 Self {
418 commitment_txids,
419 inner: virtual_tx_tree,
420 }
421 }
422
423 pub fn inner(&self) -> &Vec<Vec<Psbt>> {
424 &self.inner
425 }
426
427 pub fn commitment_txids(&self) -> &[Txid] {
428 &self.commitment_txids
429 }
430}
431
432pub fn sign_unilateral_exit_tree(
434 unilateral_exit_tree: &UnilateralExitTree,
435 commitment_txs: &[Transaction],
436) -> Result<Vec<Vec<Transaction>>, Error> {
437 let mut signed_virtual_tx_branches = Vec::new();
438 for unilateral_exit_branch in unilateral_exit_tree.inner.iter() {
439 let mut signed_unilateral_exit_branch = Vec::new();
440 for virtual_tx in unilateral_exit_branch.iter() {
441 let txid = virtual_tx.unsigned_tx.compute_txid();
442 let mut psbt = virtual_tx.clone();
443
444 let vtxo_previous_output = psbt.unsigned_tx.input[VTXO_INPUT_INDEX].previous_output;
445
446 let witness_utxo = {
447 unilateral_exit_branch
448 .iter()
449 .map(|p| &p.unsigned_tx)
450 .chain(commitment_txs.iter())
451 .find_map(|other_psbt| {
452 (other_psbt.compute_txid() == vtxo_previous_output.txid).then_some(
453 other_psbt.output[vtxo_previous_output.vout as usize].clone(),
454 )
455 })
456 }
457 .expect("witness UTXO in path");
458
459 psbt.inputs[VTXO_INPUT_INDEX].witness_utxo = Some(witness_utxo);
460
461 if let Some(tap_key_sig) = psbt.inputs[VTXO_INPUT_INDEX].tap_key_sig {
462 tracing::debug!(%txid, "Signing key spend for confirmed VTXO");
463
464 psbt.inputs[VTXO_INPUT_INDEX].final_script_witness =
465 Some(Witness::p2tr_key_spend(&tap_key_sig));
466 } else if !psbt.inputs[VTXO_INPUT_INDEX].tap_script_sigs.is_empty() {
467 tracing::debug!(%txid, "Signing script spend for pre-confirmed VTXO");
468
469 let tap_script = psbt.inputs[VTXO_INPUT_INDEX].tap_scripts.iter().next();
471 let tap_script_sigs = &psbt.inputs[VTXO_INPUT_INDEX].tap_script_sigs;
472
473 let (control_block, (script, _)) = tap_script.ok_or_else(|| {
474 Error::transaction(format!("missing tapscripts in virtual TX {txid}"))
475 })?;
476
477 let (pk_0, pk_1) = extract_pubkeys_from_2of2_script(script)?;
479
480 let leaf_hash = TapLeafHash::from_script(script, control_block.leaf_version);
482
483 let sig_0 = tap_script_sigs.get(&(pk_0, leaf_hash)).ok_or_else(|| {
485 Error::transaction(format!(
486 "missing signature for first pubkey {} in virtual TX {txid}",
487 pk_0
488 ))
489 })?;
490 let sig_1 = tap_script_sigs.get(&(pk_1, leaf_hash)).ok_or_else(|| {
491 Error::transaction(format!(
492 "missing signature for second pubkey {} in virtual TX {txid}",
493 pk_1
494 ))
495 })?;
496
497 let mut witness = Witness::new();
499 witness.push(sig_1.to_vec());
500 witness.push(sig_0.to_vec());
501 witness.push(script.as_bytes());
502 witness.push(control_block.serialize());
503
504 psbt.inputs[VTXO_INPUT_INDEX].final_script_witness = Some(witness);
505 } else {
506 return Err(Error::transaction(format!(
507 "missing taproot key spend or script spend data in virtual TX {txid}"
508 )));
509 };
510
511 let tx = psbt.clone().extract_tx().map_err(Error::transaction)?;
512
513 signed_unilateral_exit_branch.push(tx);
514 }
515 signed_virtual_tx_branches.push(signed_unilateral_exit_branch);
516 }
517
518 Ok(signed_virtual_tx_branches)
519}
520
521#[derive(Debug, Clone, PartialEq, Eq)]
522pub struct SelectedUtxo {
523 pub outpoint: OutPoint,
524 pub amount: Amount,
525 pub address: Address,
526}
527
528#[derive(Debug, Clone)]
529pub struct UtxoCoinSelection {
530 pub selected_utxos: Vec<SelectedUtxo>,
531 pub total_selected: Amount,
532 pub change_amount: Amount,
533}
534
535pub fn build_anchor_tx<F>(
538 bumpable_tx: &Transaction,
539 change_address: Address,
540 fee_rate: f64,
541 select_coins_fn: F,
542) -> Result<Psbt, Error>
543where
544 F: FnOnce(Amount) -> Result<UtxoCoinSelection, Error>,
545{
546 let anchor = find_anchor_outpoint(bumpable_tx)?;
547
548 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(
555 NESTED_P2WSH_INPUT_WEIGHT + P2TR_KEYSPEND_INPUT_WEIGHT + P2TR_OUTPUT_WEIGHT,
556 );
557
558 let child_vsize = estimated_weight.to_vbytes_ceil();
559 let package_size = child_vsize + bumpable_tx.weight().to_vbytes_ceil();
560
561 let fee = Amount::from_sat((package_size as f64 * fee_rate).ceil() as u64);
562
563 let UtxoCoinSelection {
565 selected_utxos,
566 total_selected,
567 change_amount,
568 } = select_coins_fn(fee)?;
569
570 if total_selected < fee {
571 return Err(Error::coin_select(format!(
572 "insufficient coins selected to cover {fee} fee"
573 )));
574 }
575
576 let mut inputs = vec![anchor];
578 let mut sequences = vec![Sequence::MAX];
579
580 for utxo in selected_utxos.iter() {
581 inputs.push(utxo.outpoint);
582 sequences.push(Sequence::MAX);
583 }
584
585 let outputs = vec![TxOut {
586 value: change_amount,
587 script_pubkey: change_address.script_pubkey(),
588 }];
589
590 let mut psbt = Psbt::from_unsigned_tx(Transaction {
592 version: transaction::Version::non_standard(3),
593 lock_time: LockTime::ZERO,
594 input: inputs
595 .iter()
596 .zip(sequences.iter())
597 .map(|(outpoint, sequence)| TxIn {
598 previous_output: *outpoint,
599 script_sig: ScriptBuf::new(),
600 sequence: *sequence,
601 witness: Witness::new(),
602 })
603 .collect(),
604 output: outputs,
605 })
606 .map_err(|e| Error::transaction(format!("Failed to create PSBT: {e}")))?;
607
608 psbt.inputs[0].witness_utxo = Some(anchor_output());
611 psbt.inputs[0].final_script_witness = Some(Witness::new());
612
613 for i in 1..psbt.inputs.len() {
615 if let Some(utxo) = selected_utxos.get(i - 1) {
616 psbt.inputs[i].witness_utxo = Some(TxOut {
617 value: utxo.amount,
618 script_pubkey: utxo.address.script_pubkey(),
619 });
620 }
621 }
622
623 Ok(psbt)
624}
625
626fn find_anchor_outpoint(tx: &Transaction) -> Result<OutPoint, Error> {
627 let anchor_output_template = anchor_output();
628
629 for (index, output) in tx.output.iter().enumerate() {
630 if output == &anchor_output_template {
631 return Ok(OutPoint {
632 txid: tx.compute_txid(),
633 vout: index as u32,
634 });
635 }
636 }
637
638 Err(Error::transaction("anchor output not found in transaction"))
639}
640
641fn extract_pubkeys_from_2of2_script(
645 script: &ScriptBuf,
646) -> Result<(XOnlyPublicKey, XOnlyPublicKey), Error> {
647 let bytes = script.as_bytes();
648
649 if bytes.len() < 68 {
652 return Err(Error::transaction(format!(
653 "script too short to be 2-of-2 multisig: {} bytes",
654 bytes.len()
655 )));
656 }
657
658 if bytes[0] != 0x20 {
660 return Err(Error::transaction(format!(
661 "expected OP_PUSHBYTES_32 (0x20) at position 0, got 0x{:02x}",
662 bytes[0]
663 )));
664 }
665
666 let pk_0_bytes: [u8; 32] = bytes[1..33]
668 .try_into()
669 .map_err(|_| Error::transaction("failed to extract first pubkey bytes"))?;
670 let pk_0 = XOnlyPublicKey::from_slice(&pk_0_bytes)
671 .map_err(|e| Error::transaction(format!("invalid first pubkey: {e}")))?;
672
673 if bytes[33] != OP_CHECKSIGVERIFY.to_u8() {
675 return Err(Error::transaction(format!(
676 "expected OP_CHECKSIGVERIFY (0xad) at position 33, got 0x{:02x}",
677 bytes[33]
678 )));
679 }
680
681 if bytes[34] != 0x20 {
683 return Err(Error::transaction(format!(
684 "expected OP_PUSHBYTES_32 (0x20) at position 34, got 0x{:02x}",
685 bytes[34]
686 )));
687 }
688
689 let pk_1_bytes: [u8; 32] = bytes[35..67]
691 .try_into()
692 .map_err(|_| Error::transaction("failed to extract second pubkey bytes"))?;
693 let pk_1 = XOnlyPublicKey::from_slice(&pk_1_bytes)
694 .map_err(|e| Error::transaction(format!("invalid second pubkey: {e}")))?;
695
696 if bytes[67] != OP_CHECKSIG.to_u8() {
698 return Err(Error::transaction(format!(
699 "expected OP_CHECKSIG (0xac) at position 67, got 0x{:02x}",
700 bytes[67]
701 )));
702 }
703
704 Ok((pk_0, pk_1))
705}