1use crate::anchor_output;
2use crate::asset::packet;
3use crate::contract::SpendSelection;
4use crate::conversions::from_musig_xonly;
5use crate::conversions::to_musig_pk;
6use crate::intent;
7use crate::intent::Intent;
8use crate::server::NoncePks;
9use crate::server::PartialSigTree;
10use crate::server::TreeTxNoncePks;
11use crate::tree_tx_output_script::TreeTxOutputScript;
12use crate::Error;
13use crate::ErrorContext;
14use crate::TxGraph;
15use crate::VTXO_COSIGNER_PSBT_KEY;
16use crate::VTXO_INPUT_INDEX;
17use bitcoin::absolute::LockTime;
18use bitcoin::hashes::Hash;
19use bitcoin::key::Keypair;
20use bitcoin::key::Secp256k1;
21use bitcoin::psbt;
22use bitcoin::secp256k1;
23use bitcoin::secp256k1::schnorr;
24use bitcoin::secp256k1::PublicKey;
25use bitcoin::sighash::Prevouts;
26use bitcoin::sighash::SighashCache;
27use bitcoin::taproot;
28use bitcoin::transaction;
29use bitcoin::Address;
30use bitcoin::Amount;
31use bitcoin::OutPoint;
32use bitcoin::Psbt;
33use bitcoin::TapLeafHash;
34use bitcoin::TapSighashType;
35use bitcoin::Transaction;
36use bitcoin::TxIn;
37use bitcoin::TxOut;
38use bitcoin::Txid;
39use bitcoin::XOnlyPublicKey;
40use musig::musig;
41use rand::CryptoRng;
42use rand::Rng;
43use std::collections::BTreeMap;
44use std::collections::HashMap;
45
46#[derive(Debug, Clone)]
51pub struct OnChainInput {
52 sequence: bitcoin::Sequence,
54 script_pubkey: bitcoin::ScriptBuf,
56 tapscripts: Vec<bitcoin::ScriptBuf>,
58 spend_info: (bitcoin::ScriptBuf, taproot::ControlBlock),
60 owner_pk: XOnlyPublicKey,
62 amount: Amount,
64 outpoint: OutPoint,
66}
67
68impl OnChainInput {
69 pub fn new(
70 sequence: bitcoin::Sequence,
71 script_pubkey: bitcoin::ScriptBuf,
72 tapscripts: Vec<bitcoin::ScriptBuf>,
73 spend_info: (bitcoin::ScriptBuf, taproot::ControlBlock),
74 owner_pk: XOnlyPublicKey,
75 amount: Amount,
76 outpoint: OutPoint,
77 ) -> Self {
78 Self {
79 sequence,
80 script_pubkey,
81 tapscripts,
82 spend_info,
83 owner_pk,
84 amount,
85 outpoint,
86 }
87 }
88
89 pub fn new_with_spend_selection(
90 default_sequence: bitcoin::Sequence,
91 script_pubkey: bitcoin::ScriptBuf,
92 tapscripts: Vec<bitcoin::ScriptBuf>,
93 spend_selection: SpendSelection,
94 owner_pk: XOnlyPublicKey,
95 amount: Amount,
96 outpoint: OutPoint,
97 ) -> Self {
98 let (sequence, spend_info) = spend_selection.resolved_spend_info(default_sequence);
99 Self::new(
100 sequence,
101 script_pubkey,
102 tapscripts,
103 spend_info,
104 owner_pk,
105 amount,
106 outpoint,
107 )
108 }
109
110 pub fn sequence(&self) -> bitcoin::Sequence {
111 self.sequence
112 }
113
114 pub fn script_pubkey(&self) -> &bitcoin::ScriptBuf {
115 &self.script_pubkey
116 }
117
118 pub fn tapscripts(&self) -> &[bitcoin::ScriptBuf] {
119 &self.tapscripts
120 }
121
122 pub fn spend_info(&self) -> &(bitcoin::ScriptBuf, taproot::ControlBlock) {
123 &self.spend_info
124 }
125
126 pub fn owner_pk(&self) -> XOnlyPublicKey {
127 self.owner_pk
128 }
129
130 pub fn amount(&self) -> Amount {
131 self.amount
132 }
133
134 pub fn outpoint(&self) -> OutPoint {
135 self.outpoint
136 }
137}
138
139#[allow(clippy::type_complexity)]
145pub struct NonceKps(HashMap<Txid, (Option<musig::SecretNonce>, musig::PublicNonce)>);
146
147impl NonceKps {
148 pub fn take_sk(&mut self, txid: &Txid) -> Option<musig::SecretNonce> {
153 self.0.get_mut(txid).and_then(|(sec, _)| sec.take())
154 }
155
156 pub fn to_nonce_pks(&self) -> NoncePks {
158 let nonce_pks = self
159 .0
160 .iter()
161 .map(|(txid, (_, pub_nonce))| (*txid, *pub_nonce))
162 .collect::<HashMap<_, _>>();
163
164 NoncePks::new(nonce_pks)
165 }
166}
167
168pub fn generate_nonce_tree<R>(
171 rng: &mut R,
172 batch_tree_tx_graph: &TxGraph,
173 own_cosigner_pk: PublicKey,
174 commitment_tx: &Psbt,
175) -> Result<NonceKps, Error>
176where
177 R: Rng + CryptoRng,
178{
179 let batch_tree_tx_map = batch_tree_tx_graph.as_map();
180
181 let nonce_tree = batch_tree_tx_map
182 .iter()
183 .map(|(txid, tx)| {
184 let cosigner_pks = extract_cosigner_pks_from_vtxo_psbt(tx)?;
185
186 if !cosigner_pks.contains(&own_cosigner_pk) {
187 return Err(Error::crypto(format!(
188 "cosigner PKs does not contain {own_cosigner_pk} for tree TX {txid}"
189 )));
190 }
191
192 let session_id = musig::SessionSecretRand::assume_unique_per_nonce_gen(rng.r#gen());
193 let extra_rand = rng.r#gen();
194
195 let msg = tree_tx_sighash(tx, &batch_tree_tx_map, commitment_tx)?;
196
197 let key_agg_cache = {
198 let cosigner_pks = cosigner_pks
199 .iter()
200 .map(|pk| to_musig_pk(*pk))
201 .collect::<Vec<_>>();
202 musig::KeyAggCache::new(&cosigner_pks.iter().collect::<Vec<_>>())
203 };
204
205 let (nonce, pub_nonce) =
206 key_agg_cache.nonce_gen(session_id, to_musig_pk(own_cosigner_pk), &msg, extra_rand);
207
208 Ok((*txid, (Some(nonce), pub_nonce)))
209 })
210 .collect::<Result<HashMap<_, _>, _>>()?;
211
212 Ok(NonceKps(nonce_tree))
213}
214
215fn tree_tx_sighash(
216 psbt: &Psbt,
218 tx_map: &HashMap<Txid, &Psbt>,
220 commitment_tx: &Psbt,
222) -> Result<[u8; 32], Error> {
223 let tx = &psbt.unsigned_tx;
224
225 let previous_output = tx.input[VTXO_INPUT_INDEX].previous_output;
227
228 let parent_tx = tx_map
229 .get(&previous_output.txid)
230 .or_else(|| {
231 (previous_output.txid == commitment_tx.unsigned_tx.compute_txid())
232 .then_some(&commitment_tx)
233 })
234 .ok_or_else(|| {
235 Error::crypto(format!(
236 "parent transaction {} not found for tree TX {}",
237 previous_output.txid,
238 tx.compute_txid()
239 ))
240 })?;
241 let previous_output = parent_tx
242 .unsigned_tx
243 .output
244 .get(previous_output.vout as usize)
245 .ok_or_else(|| {
246 Error::crypto(format!(
247 "previous output {} not found for tree TX {}",
248 previous_output,
249 tx.compute_txid()
250 ))
251 })?;
252
253 let prevouts = [previous_output];
254 let prevouts = Prevouts::All(&prevouts);
255
256 let tap_sighash = SighashCache::new(tx)
260 .taproot_key_spend_signature_hash(VTXO_INPUT_INDEX, &prevouts, TapSighashType::Default)
261 .map_err(Error::crypto)?;
262
263 Ok(tap_sighash.to_raw_hash().to_byte_array())
264}
265
266pub fn aggregate_nonces(tree_tx_nonce_pks: TreeTxNoncePks) -> musig::AggregatedNonce {
271 let pks = tree_tx_nonce_pks.to_pks();
272 let ref_pks = pks.iter().collect::<Vec<_>>();
273 musig::AggregatedNonce::new(&ref_pks)
274}
275
276pub fn sign_batch_tree_tx(
279 tree_txid: Txid,
280 vtxo_tree_expiry: bitcoin::Sequence,
281 server_pk: XOnlyPublicKey,
282 own_cosigner_kp: &Keypair,
283 agg_nonce_pk: musig::AggregatedNonce,
284 batch_tree_tx_graph: &TxGraph,
285 commitment_psbt: &Psbt,
286 our_nonce_kps: &mut NonceKps,
289) -> Result<PartialSigTree, Error> {
290 let own_cosigner_pk = own_cosigner_kp.public_key();
291
292 let internal_node_script = TreeTxOutputScript::new(vtxo_tree_expiry, server_pk);
293
294 let secp = Secp256k1::new();
295
296 let own_cosigner_kp = ::musig::Keypair::from_seckey_byte_array(own_cosigner_kp.secret_bytes())
297 .map_err(|e| Error::ad_hoc(format!("invalid keypair: {e}")))?;
298
299 let batch_tree_tx_map = batch_tree_tx_graph.as_map();
300
301 let psbt = batch_tree_tx_map
302 .get(&tree_txid)
303 .ok_or_else(|| Error::ad_hoc(format!("TXID {tree_txid} not found in batch-tree map")))?;
304
305 let mut cosigner_pks = extract_cosigner_pks_from_vtxo_psbt(psbt)?;
306 cosigner_pks.sort_by_key(|k| k.serialize());
307
308 if !cosigner_pks.contains(&own_cosigner_pk) {
309 return Err(Error::ad_hoc(
310 "own cosigner PK not found among batch-tree transaction cosigner PKs",
311 ));
312 }
313
314 tracing::debug!(%tree_txid, "Generating partial signature");
315
316 let mut key_agg_cache = {
317 let cosigner_pks = cosigner_pks
318 .iter()
319 .map(|pk| to_musig_pk(*pk))
320 .collect::<Vec<_>>();
321 musig::KeyAggCache::new(&cosigner_pks.iter().collect::<Vec<_>>())
322 };
323
324 let sweep_tap_tree =
325 internal_node_script.sweep_spend_leaf(&secp, from_musig_xonly(key_agg_cache.agg_pk()));
326
327 let tweak = ::musig::Scalar::from(
328 ::musig::SecretKey::from_secret_bytes(*sweep_tap_tree.tap_tweak().as_byte_array())
329 .map_err(|e| Error::ad_hoc(format!("invalid tweak: {e}")))?,
330 );
331
332 key_agg_cache
333 .pubkey_xonly_tweak_add(&tweak)
334 .map_err(Error::crypto)?;
335
336 let msg = tree_tx_sighash(psbt, &batch_tree_tx_map, commitment_psbt)?;
337
338 let nonce_sk = our_nonce_kps
339 .take_sk(&tree_txid)
340 .ok_or_else(|| Error::crypto(format!("missing nonce for tree TX {tree_txid}")))?;
341
342 let sig = musig::Session::new(&key_agg_cache, agg_nonce_pk, &msg).partial_sign(
343 nonce_sk,
344 &own_cosigner_kp,
345 &key_agg_cache,
346 );
347
348 let partial_sig_tree = HashMap::from_iter([(tree_txid, sig)]);
349
350 Ok(PartialSigTree(partial_sig_tree))
351}
352
353pub fn create_and_sign_forfeit_txs<S>(
356 mut sign_fn: S,
357 vtxo_inputs: &[intent::Input],
358 connectors_leaves: &[&Psbt],
359 server_forfeit_address: &Address,
360 dust: Amount,
362) -> Result<Vec<Psbt>, Error>
363where
364 S: FnMut(
365 &mut psbt::Input,
366 secp256k1::Message,
367 ) -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, Error>,
368{
369 const FORFEIT_TX_CONNECTOR_INDEX: usize = 0;
370 const FORFEIT_TX_VTXO_INDEX: usize = 1;
371
372 let secp = Secp256k1::new();
373
374 let connector_amount = dust;
375
376 let connector_index = derive_vtxo_connector_map(vtxo_inputs, connectors_leaves, dust)?;
377
378 let mut signed_forfeit_psbts = Vec::new();
379 for vtxo_input in vtxo_inputs.iter() {
380 if vtxo_input.amount() < dust || vtxo_input.is_swept() {
381 continue;
383 }
384
385 let outpoint = vtxo_input.outpoint();
386
387 let connector_outpoint = connector_index.get(&outpoint).ok_or_else(|| {
388 Error::ad_hoc(format!(
389 "connector outpoint missing for virtual TX outpoint {outpoint}"
390 ))
391 })?;
392
393 let connector_psbt = connectors_leaves
394 .iter()
395 .find(|l| l.unsigned_tx.compute_txid() == connector_outpoint.txid)
396 .ok_or_else(|| {
397 Error::ad_hoc(format!(
398 "connector PSBT missing for virtual TX outpoint {outpoint}"
399 ))
400 })?;
401
402 let connector_output = connector_psbt
403 .unsigned_tx
404 .output
405 .get(connector_outpoint.vout as usize)
406 .ok_or_else(|| {
407 Error::ad_hoc(format!(
408 "connector output missing for virtual TX outpoint {outpoint}"
409 ))
410 })?;
411
412 let forfeit_output = TxOut {
413 value: vtxo_input.amount() + connector_amount,
414 script_pubkey: server_forfeit_address.script_pubkey(),
415 };
416
417 let mut forfeit_psbt = Psbt::from_unsigned_tx(Transaction {
418 version: transaction::Version::non_standard(3),
419 lock_time: LockTime::ZERO,
420 input: vec![
421 TxIn {
422 previous_output: *connector_outpoint,
423 ..Default::default()
424 },
425 TxIn {
426 previous_output: outpoint,
427 ..Default::default()
428 },
429 ],
430 output: vec![forfeit_output.clone(), anchor_output()],
431 })
432 .map_err(Error::transaction)?;
433
434 forfeit_psbt.inputs[FORFEIT_TX_CONNECTOR_INDEX].witness_utxo =
435 Some(connector_output.clone());
436
437 forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX].witness_utxo = Some(TxOut {
438 value: vtxo_input.amount(),
439 script_pubkey: vtxo_input.script_pubkey().clone(),
440 });
441
442 forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX].sighash_type =
443 Some(TapSighashType::Default.into());
444
445 let (forfeit_script, forfeit_control_block) = vtxo_input.spend_info();
446
447 let leaf_version = forfeit_control_block.leaf_version;
448 forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX]
449 .tap_scripts
450 .insert(
451 forfeit_control_block.clone(),
452 (forfeit_script.clone(), leaf_version),
453 );
454 forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX].witness_script = Some(forfeit_script.clone());
455
456 let prevouts = forfeit_psbt
457 .inputs
458 .iter()
459 .filter_map(|i| i.witness_utxo.clone())
460 .collect::<Vec<_>>();
461 let prevouts = Prevouts::All(&prevouts);
462
463 let leaf_hash = TapLeafHash::from_script(forfeit_script, leaf_version);
464
465 let tap_sighash = SighashCache::new(&forfeit_psbt.unsigned_tx)
466 .taproot_script_spend_signature_hash(
467 FORFEIT_TX_VTXO_INDEX,
468 &prevouts,
469 leaf_hash,
470 TapSighashType::Default,
471 )
472 .map_err(Error::crypto)?;
473
474 let msg = secp256k1::Message::from_digest(tap_sighash.to_raw_hash().to_byte_array());
475
476 let sigs = sign_fn(&mut forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX], msg)?;
477
478 for (sig, pk) in sigs {
479 secp.verify_schnorr(&sig, &msg, &pk)
480 .map_err(Error::crypto)
481 .context("failed to verify own forfeit signature")?;
482
483 let sig = taproot::Signature {
484 signature: sig,
485 sighash_type: TapSighashType::Default,
486 };
487
488 forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX]
489 .tap_script_sigs
490 .insert((pk, leaf_hash), sig);
491 }
492
493 signed_forfeit_psbts.push(forfeit_psbt.clone());
494 }
495
496 Ok(signed_forfeit_psbts)
497}
498
499pub fn sign_commitment_psbt<F>(
501 sign_for_pk_fn: F,
502 commitment_psbt: &mut Psbt,
503 onchain_inputs: &[OnChainInput],
504) -> Result<(), Error>
505where
506 F: Fn(&XOnlyPublicKey, &secp256k1::Message) -> Result<schnorr::Signature, Error>,
507{
508 let secp = Secp256k1::new();
509
510 let prevouts = commitment_psbt
511 .inputs
512 .iter()
513 .filter_map(|i| i.witness_utxo.clone())
514 .collect::<Vec<_>>();
515
516 for onchain_input in onchain_inputs.iter() {
519 let (forfeit_script, forfeit_control_block) = onchain_input.spend_info.clone();
520
521 for (i, input) in commitment_psbt.inputs.iter_mut().enumerate() {
522 let previous_outpoint = commitment_psbt.unsigned_tx.input[i].previous_output;
523
524 if previous_outpoint == onchain_input.outpoint {
525 let leaf_version = forfeit_control_block.leaf_version;
529 input.tap_scripts = BTreeMap::from_iter([(
530 forfeit_control_block.clone(),
531 (forfeit_script.clone(), leaf_version),
532 )]);
533
534 let prevouts = Prevouts::All(&prevouts);
535
536 let leaf_hash = TapLeafHash::from_script(&forfeit_script, leaf_version);
537
538 let tap_sighash = SighashCache::new(&commitment_psbt.unsigned_tx)
539 .taproot_script_spend_signature_hash(
540 i,
541 &prevouts,
542 leaf_hash,
543 TapSighashType::Default,
544 )
545 .map_err(Error::crypto)?;
546
547 let msg =
548 secp256k1::Message::from_digest(tap_sighash.to_raw_hash().to_byte_array());
549 let pk = onchain_input.owner_pk;
550
551 let sig = sign_for_pk_fn(&pk, &msg)?;
552
553 secp.verify_schnorr(&sig, &msg, &pk)
554 .map_err(Error::crypto)
555 .context("failed to verify own commitment TX signature")?;
556
557 let sig = taproot::Signature {
558 signature: sig,
559 sighash_type: TapSighashType::Default,
560 };
561
562 input.tap_script_sigs.insert((pk, leaf_hash), sig);
563 }
564 }
565 }
566
567 Ok(())
568}
569
570fn derive_vtxo_connector_map(
572 vtxo_inputs: &[intent::Input],
573 connectors_leaves: &[&Psbt],
574 dust: Amount,
575) -> Result<HashMap<OutPoint, OutPoint>, Error> {
576 let mut connector_outpoints = Vec::new();
578 for psbt in connectors_leaves.iter() {
579 for (vout, output) in psbt.unsigned_tx.output.iter().enumerate() {
580 if output.value == Amount::ZERO {
582 continue;
583 }
584 connector_outpoints.push(OutPoint {
585 txid: psbt.unsigned_tx.compute_txid(),
586 vout: vout as u32,
587 });
588 }
589 }
590
591 connector_outpoints.sort_by(|a, b| a.txid.cmp(&b.txid).then(a.vout.cmp(&b.vout)));
593
594 let mut virtual_tx_outpoints = vtxo_inputs
596 .iter()
597 .filter_map(|vtxo_input| {
598 ((vtxo_input.amount() >= dust) && !vtxo_input.is_swept())
599 .then_some(vtxo_input.outpoint())
600 })
601 .collect::<Vec<_>>();
602
603 virtual_tx_outpoints.sort_by(|a, b| a.txid.cmp(&b.txid).then(a.vout.cmp(&b.vout)));
605
606 if connector_outpoints.len() < virtual_tx_outpoints.len() {
607 return Err(Error::ad_hoc(format!(
608 "mismatch between VTXO count ({}) and connector count ({})",
609 virtual_tx_outpoints.len(),
610 connector_outpoints.len()
611 )));
612 }
613
614 let mut map = HashMap::new();
616 for (virtual_tx_outpoint, connector_outpoint) in
617 virtual_tx_outpoints.iter().zip(connector_outpoints.iter())
618 {
619 map.insert(*virtual_tx_outpoint, *connector_outpoint);
620 }
621
622 Ok(map)
623}
624
625fn extract_cosigner_pks_from_vtxo_psbt(psbt: &Psbt) -> Result<Vec<PublicKey>, Error> {
626 let vtxo_input = &psbt.inputs[VTXO_INPUT_INDEX];
627
628 let mut cosigner_pks = Vec::new();
629 for (key, pk) in vtxo_input.unknown.iter() {
630 if key.key.starts_with(&VTXO_COSIGNER_PSBT_KEY) {
631 cosigner_pks.push(
632 bitcoin::PublicKey::from_slice(pk)
633 .map_err(Error::crypto)
634 .context("invalid PK")?
635 .inner,
636 );
637 }
638 }
639 Ok(cosigner_pks)
640}
641
642#[derive(Debug, Clone)]
648pub struct Delegate {
649 pub intent: Intent,
650 pub forfeit_psbts: Vec<Psbt>,
652 pub delegate_cosigner_pk: PublicKey,
654}
655
656pub fn prepare_delegate_psbts(
673 intent_inputs: Vec<intent::Input>,
674 outputs: Vec<intent::Output>,
675 delegate_cosigner_pk: PublicKey,
676 server_forfeit_address: &Address,
677 dust: Amount,
678) -> Result<Delegate, Error> {
679 prepare_delegate_psbts_at(
680 intent_inputs,
681 outputs,
682 delegate_cosigner_pk,
683 server_forfeit_address,
684 dust,
685 None,
686 )
687}
688
689pub fn prepare_delegate_psbts_at(
698 intent_inputs: Vec<intent::Input>,
699 outputs: Vec<intent::Output>,
700 delegate_cosigner_pk: PublicKey,
701 server_forfeit_address: &Address,
702 dust: Amount,
703 valid_at: Option<u64>,
704) -> Result<Delegate, Error> {
705 let now = std::time::SystemTime::now();
707 let now = now
708 .duration_since(std::time::UNIX_EPOCH)
709 .map_err(Error::ad_hoc)
710 .context("failed to compute now timestamp")?;
711 let now = now.as_secs();
712
713 let (valid_at, expire_at) = match valid_at {
716 Some(vat) => (vat, 0),
717 None => (now, now + (2 * 60)),
718 };
719
720 let onchain_output_indexes = outputs
721 .iter()
722 .enumerate()
723 .filter_map(|(idx, output)| match output {
724 intent::Output::Onchain(_) => Some(idx),
725 intent::Output::Offchain(_) | intent::Output::AssetPacket(_) => None,
726 })
727 .collect();
728
729 let intent_message = intent::IntentMessage::Register {
730 onchain_output_indexes,
731 valid_at,
732 expire_at,
733 own_cosigner_pks: vec![delegate_cosigner_pk],
734 };
735
736 let (mut intent_psbt, _fake_input) =
738 intent::build_proof_psbt(&intent_message, &intent_inputs, &outputs)?;
739
740 for (i, proof_input) in intent_psbt.inputs.iter_mut().enumerate() {
742 if i == 0 {
743 let (script, control_block) = intent_inputs[0].spend_info().clone();
744
745 proof_input.tap_scripts =
746 BTreeMap::from_iter([(control_block, (script, taproot::LeafVersion::TapScript))]);
747 } else {
748 let (script, control_block) = intent_inputs[i - 1].spend_info().clone();
749
750 let tap_tree = intent::taptree::TapTree(intent_inputs[i - 1].tapscripts().to_vec());
751 let bytes = tap_tree
752 .encode()
753 .map_err(Error::ad_hoc)
754 .with_context(|| format!("failed to encode taptree for input {i}"))?;
755
756 proof_input.unknown.insert(
757 psbt::raw::Key {
758 type_value: 222,
759 key: crate::VTXO_TAPROOT_KEY.to_vec(),
760 },
761 bytes,
762 );
763 proof_input.tap_scripts =
764 BTreeMap::from_iter([(control_block, (script, taproot::LeafVersion::TapScript))]);
765 };
766 }
767
768 let mut forfeit_psbts = Vec::new();
770 const FORFEIT_TX_VTXO_INDEX: usize = 0;
771
772 for intent_input in intent_inputs.iter() {
773 if intent_input.is_swept() || intent_input.amount() < dust {
775 continue;
776 }
777
778 let vtxo_amount = intent_input.amount();
779 let virtual_tx_outpoint = intent_input.outpoint();
780 let connector_amount = dust;
781
782 let forfeit_output = TxOut {
784 value: vtxo_amount + connector_amount,
785 script_pubkey: server_forfeit_address.script_pubkey(),
786 };
787
788 let mut forfeit_psbt = Psbt::from_unsigned_tx(Transaction {
789 version: transaction::Version::non_standard(3),
790 lock_time: LockTime::ZERO,
791 input: vec![TxIn {
792 previous_output: virtual_tx_outpoint,
793 ..Default::default()
794 }],
795 output: vec![forfeit_output, anchor_output()],
796 })
797 .map_err(|e| Error::ad_hoc(format!("failed to create forfeit PSBT: {e}")))?;
798
799 forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX].witness_utxo = Some(TxOut {
800 value: vtxo_amount,
801 script_pubkey: intent_input.script_pubkey().clone(),
802 });
803
804 forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX].sighash_type = Some(
806 psbt::PsbtSighashType::from(TapSighashType::AllPlusAnyoneCanPay),
807 );
808
809 let (forfeit_script, forfeit_control_block) = intent_input.spend_info();
810 let leaf_version = forfeit_control_block.leaf_version;
811 forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX]
812 .tap_scripts
813 .insert(
814 forfeit_control_block.clone(),
815 (forfeit_script.clone(), leaf_version),
816 );
817
818 forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX].witness_script = Some(forfeit_script.clone());
819
820 forfeit_psbts.push(forfeit_psbt);
821 }
822
823 let intent = Intent::new(intent_psbt, intent_message);
824
825 Ok(Delegate {
826 intent,
827 forfeit_psbts,
828 delegate_cosigner_pk,
829 })
830}
831
832pub fn create_asset_preservation_packet(
834 inputs: &[intent::Input],
835 outputs: &[intent::Output],
836) -> Result<Option<packet::Packet>, Error> {
837 const INTENT_PROOF_FAKE_INPUT_INDEX_OFFSET: u16 = 1;
838
839 let mut groups: Vec<packet::AssetGroup> = Vec::new();
840
841 let preserved_output_index =
842 outputs
843 .iter()
844 .enumerate()
845 .find_map(|(index, output)| match output {
846 intent::Output::Offchain(_) => Some(index as u16),
847 intent::Output::Onchain(_) | intent::Output::AssetPacket(_) => None,
848 });
849
850 for (input_index, input) in inputs.iter().enumerate() {
851 for asset in input.assets() {
852 if let Some(group) = groups
853 .iter_mut()
854 .find(|group| group.asset_id == Some(asset.asset_id))
855 {
856 group.inputs.push(packet::AssetInput {
857 input_index: input_index as u16 + INTENT_PROOF_FAKE_INPUT_INDEX_OFFSET,
858 amount: asset.amount,
859 });
860
861 if let Some(output) = group.outputs.first_mut() {
862 output.amount = output.amount.checked_add(asset.amount).ok_or_else(|| {
863 Error::ad_hoc("asset amount overflow while preserving assets in settlement")
864 })?;
865 }
866 } else {
867 let mut asset_outputs = Vec::new();
868 match preserved_output_index {
869 Some(output_index) => asset_outputs.push(packet::AssetOutput {
870 output_index,
871 amount: asset.amount,
872 }),
873 None => {
874 return Err(Error::ad_hoc(
875 "cannot preserve assets in settlement without an offchain output",
876 ))
877 }
878 }
879
880 groups.push(packet::AssetGroup {
881 asset_id: Some(asset.asset_id),
882 control_asset: None,
883 metadata: None,
884 inputs: vec![packet::AssetInput {
885 input_index: input_index as u16 + INTENT_PROOF_FAKE_INPUT_INDEX_OFFSET,
886 amount: asset.amount,
887 }],
888 outputs: asset_outputs,
889 });
890 }
891 }
892 }
893
894 if groups.is_empty() {
895 return Ok(None);
896 }
897
898 groups.sort_by_key(|group| {
899 let asset_id = group
900 .asset_id
901 .expect("asset-preservation groups always have asset ids");
902 (*asset_id.txid.as_byte_array(), asset_id.group_index)
903 });
904
905 Ok(Some(packet::Packet { groups }))
906}
907
908pub fn complete_delegate_forfeit_txs(
909 forfeit_psbts: &[Psbt],
910 connectors_leaves: &[&Psbt],
911) -> Result<Vec<Psbt>, Error> {
912 const FORFEIT_TX_CONNECTOR_INDEX: usize = 0;
913 const FORFEIT_TX_VTXO_INDEX: usize = 1;
914
915 let connector_index = derive_vtxo_connector_map_delegate(
916 forfeit_psbts
917 .iter()
918 .map(|psbt| psbt.unsigned_tx.input[0].previous_output)
919 .collect(),
920 connectors_leaves,
921 )?;
922
923 let mut completed_forfeit_psbts = Vec::new();
924
925 for forfeit_psbt in forfeit_psbts.iter() {
926 let virtual_tx_outpoint = forfeit_psbt.unsigned_tx.input[0].previous_output;
927
928 let connector_outpoint = connector_index.get(&virtual_tx_outpoint).ok_or_else(|| {
929 Error::ad_hoc(format!(
930 "connector outpoint missing for virtual TX outpoint {virtual_tx_outpoint}",
931 ))
932 })?;
933
934 let connector_psbt = connectors_leaves
935 .iter()
936 .find(|l| l.unsigned_tx.compute_txid() == connector_outpoint.txid)
937 .ok_or_else(|| {
938 Error::ad_hoc(format!(
939 "connector PSBT missing for virtual TX outpoint {virtual_tx_outpoint}",
940 ))
941 })?;
942
943 let connector_output = connector_psbt
944 .unsigned_tx
945 .output
946 .get(connector_outpoint.vout as usize)
947 .ok_or_else(|| {
948 Error::ad_hoc(format!(
949 "connector output missing for virtual TX outpoint {virtual_tx_outpoint}",
950 ))
951 })?;
952
953 let mut completed_tx = forfeit_psbt.unsigned_tx.clone();
955 completed_tx.input.insert(
956 FORFEIT_TX_CONNECTOR_INDEX,
957 TxIn {
958 previous_output: *connector_outpoint,
959 ..Default::default()
960 },
961 );
962
963 let mut completed_psbt = Psbt::from_unsigned_tx(completed_tx)
964 .map_err(|e| Error::ad_hoc(format!("failed to create PSBT from unsigned tx: {e}")))?;
965
966 completed_psbt.inputs[FORFEIT_TX_VTXO_INDEX] = forfeit_psbt.inputs[0].clone();
968
969 completed_psbt.inputs[FORFEIT_TX_CONNECTOR_INDEX].witness_utxo =
971 Some(connector_output.clone());
972
973 completed_psbt.outputs = forfeit_psbt.outputs.clone();
975
976 completed_forfeit_psbts.push(completed_psbt);
977 }
978
979 Ok(completed_forfeit_psbts)
980}
981
982fn derive_vtxo_connector_map_delegate(
984 mut virtual_tx_outpoints: Vec<OutPoint>,
985 connectors_leaves: &[&Psbt],
986) -> Result<HashMap<OutPoint, OutPoint>, Error> {
987 let mut connector_outpoints = Vec::new();
989 for psbt in connectors_leaves.iter() {
990 for (vout, output) in psbt.unsigned_tx.output.iter().enumerate() {
991 if output.value == Amount::ZERO {
993 continue;
994 }
995 connector_outpoints.push(OutPoint {
996 txid: psbt.unsigned_tx.compute_txid(),
997 vout: vout as u32,
998 });
999 }
1000 }
1001
1002 connector_outpoints.sort_by(|a, b| a.txid.cmp(&b.txid).then(a.vout.cmp(&b.vout)));
1004
1005 virtual_tx_outpoints.sort_by(|a, b| a.txid.cmp(&b.txid).then(a.vout.cmp(&b.vout)));
1007
1008 if connector_outpoints.len() < virtual_tx_outpoints.len() {
1010 return Err(Error::ad_hoc(format!(
1011 "mismatch between VTXO count ({}) and connector count ({})",
1012 virtual_tx_outpoints.len(),
1013 connector_outpoints.len()
1014 )));
1015 }
1016
1017 let mut map = HashMap::new();
1019 for (virtual_tx_outpoint, connector_outpoint) in
1020 virtual_tx_outpoints.iter().zip(connector_outpoints.iter())
1021 {
1022 map.insert(*virtual_tx_outpoint, *connector_outpoint);
1023 }
1024
1025 Ok(map)
1026}
1027
1028pub fn sign_delegate_psbts<S>(
1034 mut sign_fn: S,
1035 intent_psbt: &mut Psbt,
1036 forfeit_psbts: &mut [Psbt],
1037) -> Result<(), Error>
1038where
1039 S: FnMut(
1040 &mut psbt::Input,
1041 secp256k1::Message,
1042 ) -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, Error>,
1043{
1044 let prevouts = intent_psbt
1045 .inputs
1046 .iter()
1047 .filter_map(|i| i.witness_utxo.clone())
1048 .collect::<Vec<_>>();
1049
1050 for (i, psbt_input) in intent_psbt.inputs.iter_mut().enumerate() {
1051 let prevouts = Prevouts::All(&prevouts);
1052
1053 let (_, (script, leaf_version)) =
1054 psbt_input.tap_scripts.first_key_value().expect("a value");
1055
1056 let leaf_hash = TapLeafHash::from_script(script, *leaf_version);
1057
1058 let tap_sighash = SighashCache::new(&intent_psbt.unsigned_tx)
1059 .taproot_script_spend_signature_hash(i, &prevouts, leaf_hash, TapSighashType::Default)
1060 .map_err(Error::crypto)
1061 .with_context(|| format!("failed to compute sighash for intent input {i}"))?;
1062
1063 let msg = secp256k1::Message::from_digest(tap_sighash.to_raw_hash().to_byte_array());
1064
1065 let sigs =
1066 sign_fn(psbt_input, msg).with_context(|| format!("failed to sign intent input {i}"))?;
1067 for (sig, pk) in sigs {
1068 let sig = taproot::Signature {
1069 signature: sig,
1070 sighash_type: TapSighashType::Default,
1071 };
1072
1073 psbt_input.tap_script_sigs.insert((pk, leaf_hash), sig);
1074 }
1075 }
1076
1077 const FORFEIT_TX_VTXO_INDEX: usize = 0;
1079
1080 for forfeit_psbt in forfeit_psbts {
1081 let prevouts = forfeit_psbt
1082 .inputs
1083 .iter()
1084 .filter_map(|i| i.witness_utxo.clone())
1085 .collect::<Vec<_>>();
1086 let prevouts = Prevouts::All(&prevouts);
1087
1088 let psbt_input = forfeit_psbt
1089 .inputs
1090 .get_mut(FORFEIT_TX_VTXO_INDEX)
1091 .expect("input at index");
1092
1093 let (_, (forfeit_script, leaf_version)) =
1094 psbt_input.tap_scripts.first_key_value().expect("one entry");
1095
1096 let leaf_hash = TapLeafHash::from_script(forfeit_script, *leaf_version);
1097
1098 let tap_sighash = SighashCache::new(&forfeit_psbt.unsigned_tx)
1099 .taproot_script_spend_signature_hash(
1100 FORFEIT_TX_VTXO_INDEX,
1101 &prevouts,
1102 leaf_hash,
1103 TapSighashType::AllPlusAnyoneCanPay,
1104 )
1105 .map_err(|e| Error::ad_hoc(format!("failed to compute forfeit sighash: {e}")))?;
1106
1107 let msg = secp256k1::Message::from_digest(tap_sighash.to_raw_hash().to_byte_array());
1108
1109 let sigs =
1110 sign_fn(&mut forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX], msg).with_context(|| {
1111 format!(
1112 "failed to sign forfeit PSBT {}",
1113 forfeit_psbt.unsigned_tx.compute_txid()
1114 )
1115 })?;
1116
1117 for (sig, pk) in sigs {
1118 let sig = taproot::Signature {
1119 signature: sig,
1120 sighash_type: TapSighashType::AllPlusAnyoneCanPay,
1121 };
1122
1123 forfeit_psbt.inputs[FORFEIT_TX_VTXO_INDEX]
1124 .tap_script_sigs
1125 .insert((pk, leaf_hash), sig);
1126 }
1127 }
1128
1129 Ok(())
1130}