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