Skip to main content

ark_core/
batch.rs

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/// A UTXO that is primed to become a VTXO. Alternatively, the owner of this UTXO may decide to
47/// spend it into a vanilla UTXO.
48///
49/// Only UTXOs with a particular script (involving an Ark server) can become VTXOs.
50#[derive(Debug, Clone)]
51pub struct OnChainInput {
52    /// The sequence needed by the selected spend path.
53    sequence: bitcoin::Sequence,
54    /// The script pubkey of the UTXO.
55    script_pubkey: bitcoin::ScriptBuf,
56    /// The full tapscript set for the contract.
57    tapscripts: Vec<bitcoin::ScriptBuf>,
58    /// The selected spend path.
59    spend_info: (bitcoin::ScriptBuf, taproot::ControlBlock),
60    /// The owner key that signs the selected spend path.
61    owner_pk: XOnlyPublicKey,
62    /// The amount of coins locked in the UTXO.
63    amount: Amount,
64    /// The location of this UTXO in the blockchain.
65    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/// A nonce key pair per batch-tree transaction output that we are a part of in the batch.
140///
141/// The [`musig::SecretNonce`] element of the tuple is an [`Option`] because it cannot be cloned or
142/// copied. When we are ready to sign a batch-tree transaction, we call the method `take_sk` to move
143/// out of the [`Option`].
144#[allow(clippy::type_complexity)]
145pub struct NonceKps(HashMap<Txid, (Option<musig::SecretNonce>, musig::PublicNonce)>);
146
147impl NonceKps {
148    /// Take ownership of the [`musig::SecretNonce`] for the transaction identified by `txid`.
149    ///
150    /// The caller must take ownership because the [`musig::SecretNonce`] ensures that it can only
151    /// be used once, to avoid nonce reuse.
152    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    /// Convert into [`NoncePks`].
157    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
168/// Generate a nonce key pair for each batch-tree transaction output that we are a part of in the
169/// batch.
170pub 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    // The tree PSBT to be signed.
217    psbt: &Psbt,
218    // The entire tree TX set for this batch, to look for the previous output.
219    tx_map: &HashMap<Txid, &Psbt>,
220    // The commitment transaction, in case it contains the previous output.
221    commitment_tx: &Psbt,
222) -> Result<[u8; 32], Error> {
223    let tx = &psbt.unsigned_tx;
224
225    // We expect a single input to a VTXO.
226    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    // Here we are generating a key spend sighash, because batch-tree outputs are signed by parties
257    // with VTXOs in this new batch. We use a musig key spend to efficiently coordinate with all the
258    // parties.
259    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
266/// Compute the aggregated nonce public key for a transaction in the batch-tree.
267///
268/// The [`TreeTxNoncePks`] holds the public nonces of all the cosigners of this batch-tree
269/// transaction.
270pub 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
276/// Use `own_cosigner_kp` to sign each batch-tree transaction output that we are a part of, using
277/// `our_nonce_kps` to provide our share of each aggregate nonce.
278pub 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    // This holds all the nonce KPs we generated earlier. We need to mutate it to be able to _move_
287    // the secret nonce out of it before signing.
288    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
353/// Build and sign a forfeit transaction per [`VtxoInput`] to be used in an upcoming commitment
354/// transaction.
355pub 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    // As defined by the server.
361    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            // Sub-dust VTXOs don't need to be forfeited.
382            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
499/// Sign every input of the `commitment_psbt` which is in the provided `onchain_inputs` list.
500pub 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    // Sign commitment transaction inputs that belong to us. For every output we are settling, we
517    // look through the commitment transaction inputs to find a matching input.
518    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                // In the case of a boarding output, we are actually using a
526                // script spend path.
527
528                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
570/// Build a map between VTXOs and their corresponding connector outputs.
571fn derive_vtxo_connector_map(
572    vtxo_inputs: &[intent::Input],
573    connectors_leaves: &[&Psbt],
574    dust: Amount,
575) -> Result<HashMap<OutPoint, OutPoint>, Error> {
576    // Collect all connector outpoints (non-anchor outputs).
577    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            // Skip anchor outputs.
581            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    // Sort connector outpoints for deterministic ordering
592    connector_outpoints.sort_by(|a, b| a.txid.cmp(&b.txid).then(a.vout.cmp(&b.vout)));
593
594    // Get virtual TX outpoints that need forfeiting (excluding sub-dust and swept).
595    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    // Sort virtual TX outpoints for deterministic ordering.
604    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    // Create mapping by position.
615    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/// A delegate contains all the information necessary for another party to settle VTXOs on behalf of
643/// the owner.
644///
645/// The owner pre-signs the intent and forfeit transactions, allowing another party to complete the
646/// settlement at a later time.
647#[derive(Debug, Clone)]
648pub struct Delegate {
649    pub intent: Intent,
650    /// Partial forfeit transactions signed with SIGHASH_ALL | ANYONECANPAY.
651    pub forfeit_psbts: Vec<Psbt>,
652    /// The cosigner public key of the party who will execute the settlement as the delegate.
653    pub delegate_cosigner_pk: PublicKey,
654}
655
656/// Prepare unsigned intent and forfeit PSBTs for delegate.
657///
658/// This is step 1 of the delegate flow. Bob can prepare these PSBTs and send them to Alice for
659/// signing.
660///
661/// # Arguments
662///
663/// * `intent_inputs` - VTXO inputs to be settled
664/// * `outputs` - Desired outputs (typically back to the owner's address)
665/// * `delegate_cosigner_pk` - Public keys of cosigner who will participate in the settlement
666/// * `server_forfeit_address` - Address where forfeits are sent
667/// * `dust` - Dust amount for connectors
668///
669/// # Returns
670///
671/// A [`Delegate`] struct containing unsigned PSBTs ready for signing.
672pub 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
689/// Like [`prepare_delegate_psbts`], but with an explicit `valid_at` timestamp.
690///
691/// When delegating to a third-party service, `valid_at` is set to the time at which the delegator
692/// should execute the renewal (e.g. 90% through the VTXO's lifetime). In this case `expire_at` is
693/// set to `0` (no expiry), since the delegator holds the intent until `valid_at` arrives.
694///
695/// If `valid_at` is `None`, the current time is used and the intent expires in 2 minutes (same as
696/// [`prepare_delegate_psbts`]).
697pub 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    // Create intent message
706    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    // When valid_at is explicit (delegator flow), the intent is held for future use — no expiry.
714    // When valid_at is None (P2P flow), expire after 2 minutes.
715    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    // Build the intent PSBT (unsigned)
737    let (mut intent_psbt, _fake_input) =
738        intent::build_proof_psbt(&intent_message, &intent_inputs, &outputs)?;
739
740    // Sign the intent PSBT
741    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    // Build unsigned forfeit PSBTs
769    let mut forfeit_psbts = Vec::new();
770    const FORFEIT_TX_VTXO_INDEX: usize = 0;
771
772    for intent_input in intent_inputs.iter() {
773        // Skip swept or sub-dust VTXOs - they cannot be forfeited.
774        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        // Create partial forfeit transaction with only the VTXO input
783        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        // Set sighash type to SIGHASH_ALL | ANYONECANPAY
805        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
832/// Complete the delegated forfeit transactions by adding connector inputs and finalizing them.
833pub 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        // Add the connector input to the partial forfeit transaction
954        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        // Copy the VTXO input data from the partial PSBT
967        completed_psbt.inputs[FORFEIT_TX_VTXO_INDEX] = forfeit_psbt.inputs[0].clone();
968
969        // Add connector input data
970        completed_psbt.inputs[FORFEIT_TX_CONNECTOR_INDEX].witness_utxo =
971            Some(connector_output.clone());
972
973        // Copy outputs from partial PSBT
974        completed_psbt.outputs = forfeit_psbt.outputs.clone();
975
976        completed_forfeit_psbts.push(completed_psbt);
977    }
978
979    Ok(completed_forfeit_psbts)
980}
981
982/// Build a map between virtual TX outpoints and their corresponding connector outputs.
983fn derive_vtxo_connector_map_delegate(
984    mut virtual_tx_outpoints: Vec<OutPoint>,
985    connectors_leaves: &[&Psbt],
986) -> Result<HashMap<OutPoint, OutPoint>, Error> {
987    // Collect all connector outpoints (non-anchor outputs).
988    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            // Skip anchor outputs.
992            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    // Sort connector outpoints for deterministic ordering
1003    connector_outpoints.sort_by(|a, b| a.txid.cmp(&b.txid).then(a.vout.cmp(&b.vout)));
1004
1005    // Sort virtual TX outpoints for deterministic ordering.
1006    virtual_tx_outpoints.sort_by(|a, b| a.txid.cmp(&b.txid).then(a.vout.cmp(&b.vout)));
1007
1008    // Ensure we have matching counts.
1009    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    // Create mapping by position.
1018    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
1028/// Sign delegate PSBTs.
1029///
1030/// # Errors
1031///
1032/// Returns an error if signing fails.
1033pub 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    // Sign the forfeit PSBTs
1078    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}