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