Skip to main content

ark_core/
batch.rs

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/// 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 information needed to spend the UTXO.
53    boarding_output: BoardingOutput,
54    /// The amount of coins locked in the UTXO.
55    amount: Amount,
56    /// The location of this UTXO in the blockchain.
57    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/// A nonce key pair per batch-tree transaction output that we are a part of in the batch.
83///
84/// The [`musig::SecretNonce`] element of the tuple is an [`Option`] because it cannot be cloned or
85/// copied. When we are ready to sign a batch-tree transaction, we call the method `take_sk` to move
86/// out of the [`Option`].
87#[allow(clippy::type_complexity)]
88pub struct NonceKps(HashMap<Txid, (Option<musig::SecretNonce>, musig::PublicNonce)>);
89
90impl NonceKps {
91    /// Take ownership of the [`musig::SecretNonce`] for the transaction identified by `txid`.
92    ///
93    /// The caller must take ownership because the [`musig::SecretNonce`] ensures that it can only
94    /// be used once, to avoid nonce reuse.
95    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    /// Convert into [`NoncePks`].
100    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
111/// Generate a nonce key pair for each batch-tree transaction output that we are a part of in the
112/// batch.
113pub 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    // The tree PSBT to be signed.
160    psbt: &Psbt,
161    // The entire tree TX set for this batch, to look for the previous output.
162    tx_map: &HashMap<Txid, &Psbt>,
163    // The commitment transaction, in case it contains the previous output.
164    commitment_tx: &Psbt,
165) -> Result<[u8; 32], Error> {
166    let tx = &psbt.unsigned_tx;
167
168    // We expect a single input to a VTXO.
169    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    // Here we are generating a key spend sighash, because batch-tree outputs are signed by parties
200    // with VTXOs in this new batch. We use a musig key spend to efficiently coordinate with all the
201    // parties.
202    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
209/// Compute the aggregated nonce public key for a transaction in the batch-tree.
210///
211/// The [`TreeTxNoncePks`] holds the public nonces of all the cosigners of this batch-tree
212/// transaction.
213pub 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
219/// Use `own_cosigner_kp` to sign each batch-tree transaction output that we are a part of, using
220/// `our_nonce_kps` to provide our share of each aggregate nonce.
221pub 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    // This holds all the nonce KPs we generated earlier. We need to mutate it to be able to _move_
230    // the secret nonce out of it before signing.
231    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
296/// Build and sign a forfeit transaction per [`VtxoInput`] to be used in an upcoming commitment
297/// transaction.
298pub 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    // As defined by the server.
304    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            // Sub-dust VTXOs don't need to be forfeited.
325            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
442/// Sign every input of the `commitment_psbt` which is in the provided `onchain_inputs` list.
443pub 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    // Sign commitment transaction inputs that belong to us. For every output we are settling, we
460    // look through the commitment transaction inputs to find a matching input.
461    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                // In the case of a boarding output, we are actually using a
474                // script spend path.
475
476                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
518/// Build a map between VTXOs and their corresponding connector outputs.
519fn derive_vtxo_connector_map(
520    vtxo_inputs: &[intent::Input],
521    connectors_leaves: &[&Psbt],
522    dust: Amount,
523) -> Result<HashMap<OutPoint, OutPoint>, Error> {
524    // Collect all connector outpoints (non-anchor outputs).
525    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            // Skip anchor outputs.
529            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    // Sort connector outpoints for deterministic ordering
540    connector_outpoints.sort_by(|a, b| a.txid.cmp(&b.txid).then(a.vout.cmp(&b.vout)));
541
542    // Get virtual TX outpoints that need forfeiting (excluding sub-dust and swept).
543    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    // Sort virtual TX outpoints for deterministic ordering.
552    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    // Create mapping by position.
563    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/// A delegate contains all the information necessary for another party to settle VTXOs on behalf of
591/// the owner.
592///
593/// The owner pre-signs the intent and forfeit transactions, allowing another party to complete the
594/// settlement at a later time.
595#[derive(Debug, Clone)]
596pub struct Delegate {
597    pub intent: Intent,
598    /// Partial forfeit transactions signed with SIGHASH_ALL | ANYONECANPAY.
599    pub forfeit_psbts: Vec<Psbt>,
600    /// The cosigner public key of the party who will execute the settlement as the delegate.
601    pub delegate_cosigner_pk: PublicKey,
602}
603
604/// Prepare unsigned intent and forfeit PSBTs for delegate.
605///
606/// This is step 1 of the delegate flow. Bob can prepare these PSBTs and send them to Alice for
607/// signing.
608///
609/// # Arguments
610///
611/// * `intent_inputs` - VTXO inputs to be settled
612/// * `outputs` - Desired outputs (typically back to the owner's address)
613/// * `delegate_cosigner_pk` - Public keys of cosigner who will participate in the settlement
614/// * `server_forfeit_address` - Address where forfeits are sent
615/// * `dust` - Dust amount for connectors
616///
617/// # Returns
618///
619/// A [`Delegate`] struct containing unsigned PSBTs ready for signing.
620pub 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
637/// Like [`prepare_delegate_psbts`], but with an explicit `valid_at` timestamp.
638///
639/// When delegating to a third-party service, `valid_at` is set to the time at which the delegator
640/// should execute the renewal (e.g. 90% through the VTXO's lifetime). In this case `expire_at` is
641/// set to `0` (no expiry), since the delegator holds the intent until `valid_at` arrives.
642///
643/// If `valid_at` is `None`, the current time is used and the intent expires in 2 minutes (same as
644/// [`prepare_delegate_psbts`]).
645pub 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    // Create intent message
654    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    // When valid_at is explicit (delegator flow), the intent is held for future use — no expiry.
662    // When valid_at is None (P2P flow), expire after 2 minutes.
663    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    // Build the intent PSBT (unsigned)
685    let (mut intent_psbt, _fake_input) =
686        intent::build_proof_psbt(&intent_message, &intent_inputs, &outputs)?;
687
688    // Sign the intent PSBT
689    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    // Build unsigned forfeit PSBTs
717    let mut forfeit_psbts = Vec::new();
718    const FORFEIT_TX_VTXO_INDEX: usize = 0;
719
720    for intent_input in intent_inputs.iter() {
721        // Skip swept or sub-dust VTXOs - they cannot be forfeited.
722        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        // Create partial forfeit transaction with only the VTXO input
731        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        // Set sighash type to SIGHASH_ALL | ANYONECANPAY
753        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
780/// Complete the delegated forfeit transactions by adding connector inputs and finalizing them.
781pub 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        // Add the connector input to the partial forfeit transaction
902        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        // Copy the VTXO input data from the partial PSBT
915        completed_psbt.inputs[FORFEIT_TX_VTXO_INDEX] = forfeit_psbt.inputs[0].clone();
916
917        // Add connector input data
918        completed_psbt.inputs[FORFEIT_TX_CONNECTOR_INDEX].witness_utxo =
919            Some(connector_output.clone());
920
921        // Copy outputs from partial PSBT
922        completed_psbt.outputs = forfeit_psbt.outputs.clone();
923
924        completed_forfeit_psbts.push(completed_psbt);
925    }
926
927    Ok(completed_forfeit_psbts)
928}
929
930/// Build a map between virtual TX outpoints and their corresponding connector outputs.
931fn derive_vtxo_connector_map_delegate(
932    mut virtual_tx_outpoints: Vec<OutPoint>,
933    connectors_leaves: &[&Psbt],
934) -> Result<HashMap<OutPoint, OutPoint>, Error> {
935    // Collect all connector outpoints (non-anchor outputs).
936    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            // Skip anchor outputs.
940            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    // Sort connector outpoints for deterministic ordering
951    connector_outpoints.sort_by(|a, b| a.txid.cmp(&b.txid).then(a.vout.cmp(&b.vout)));
952
953    // Sort virtual TX outpoints for deterministic ordering.
954    virtual_tx_outpoints.sort_by(|a, b| a.txid.cmp(&b.txid).then(a.vout.cmp(&b.vout)));
955
956    // Ensure we have matching counts.
957    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    // Create mapping by position.
966    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
976/// Sign delegate PSBTs.
977///
978/// # Errors
979///
980/// Returns an error if signing fails.
981pub 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    // Sign the forfeit PSBTs
1026    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}