boltz_client/swaps/
liquid.rs

1use bitcoin::{
2    hashes::{hash160, Hash},
3    hex::DisplayHex,
4    key::rand::{rngs::OsRng, RngCore},
5    secp256k1::Keypair,
6    Amount, Witness, XOnlyPublicKey,
7};
8use elements::{
9    confidential::{Asset, AssetBlindingFactor, ValueBlindingFactor},
10    hex::FromHex,
11    secp256k1_zkp::{Secp256k1, SecretKey},
12    sighash::{Prevouts, SighashCache},
13    taproot::{LeafVersion, TapLeafHash, TaprootBuilder, TaprootSpendInfo},
14    Address, AssetIssuance, BlockHash, LockTime, OutPoint, SchnorrSig, SchnorrSighashType, Script,
15    Sequence, Transaction, TxIn, TxInWitness, TxOut, TxOutSecrets, TxOutWitness,
16};
17use secp256k1_musig::{musig, Scalar};
18use std::str::FromStr;
19
20use elements::encode::serialize;
21use elements::secp256k1_zkp::Message;
22
23use crate::util::{
24    hex_to_bytes32,
25    secrets::{rng_32b, Preimage},
26};
27
28use crate::error::Error;
29
30use super::{
31    boltz::{
32        BoltzApiClientV2, ChainSwapDetails, Cooperative, CreateReverseResponse,
33        CreateSubmarineResponse, Side, SwapTxKind, SwapType, ToSign,
34    },
35    wrappers::SwapScriptCommon,
36};
37use crate::fees::{create_tx_with_fee, Fee};
38use crate::network::{LiquidChain, LiquidClient};
39use elements::bitcoin::PublicKey;
40use elements::secp256k1_zkp::Keypair as ZKKeyPair;
41use elements::{
42    address::Address as EAddress,
43    opcodes::all::*,
44    script::{Builder as EBuilder, Instruction},
45};
46
47pub(crate) fn find_utxo(tx: &Transaction, script_pubkey: &Script) -> Option<(OutPoint, TxOut)> {
48    for (vout, output) in tx.clone().output.into_iter().enumerate() {
49        if output.script_pubkey == *script_pubkey {
50            let outpoint = OutPoint::new(tx.txid(), vout as u32);
51            return Some((outpoint, output));
52        }
53    }
54    None
55}
56
57pub(crate) fn unblind_utxo(
58    network: LiquidChain,
59    utxo: TxOut,
60    blinding_key: SecretKey,
61) -> Result<TxOutSecrets, Error> {
62    let secp = Secp256k1::new();
63    let secrets = utxo.unblind(&secp, blinding_key)?;
64    if secrets.asset != network.bitcoin() {
65        return Err(Error::Protocol(format!(
66            "Asset is not bitcoin: {}",
67            secrets.asset
68        )));
69    }
70    Ok(secrets)
71}
72
73/// Liquid v2 swap script helper.
74#[derive(Debug, Clone, PartialEq)]
75pub struct LBtcSwapScript {
76    pub swap_type: SwapType,
77    pub side: Option<Side>,
78    pub funding_addrs: Option<Address>,
79    pub hashlock: hash160::Hash,
80    pub receiver_pubkey: PublicKey,
81    pub locktime: LockTime,
82    pub sender_pubkey: PublicKey,
83    pub blinding_key: ZKKeyPair,
84}
85
86impl LBtcSwapScript {
87    /// Create the struct for a submarine swap from boltz create response.
88    pub fn submarine_from_swap_resp(
89        create_swap_response: &CreateSubmarineResponse,
90        our_pubkey: PublicKey,
91    ) -> Result<Self, Error> {
92        let claim_script = Script::from_hex(&create_swap_response.swap_tree.claim_leaf.output)?;
93        let refund_script = Script::from_hex(&create_swap_response.swap_tree.refund_leaf.output)?;
94
95        let claim_instructions = claim_script.instructions();
96        let refund_instructions = refund_script.instructions();
97
98        let mut last_op = OP_0NOTEQUAL;
99        let mut hashlock = None;
100        let mut locktime = None;
101
102        for instruction in claim_instructions {
103            match instruction {
104                Ok(Instruction::PushBytes(bytes)) => {
105                    if bytes.len() == 20 {
106                        hashlock = Some(hash160::Hash::from_slice(bytes)?);
107                    } else {
108                        continue;
109                    }
110                }
111                _ => continue,
112            }
113        }
114
115        for instruction in refund_instructions {
116            match instruction {
117                Ok(Instruction::Op(opcode)) => last_op = opcode,
118                Ok(Instruction::PushBytes(bytes)) => {
119                    if last_op == OP_CHECKSIGVERIFY {
120                        locktime =
121                            Some(LockTime::from_consensus(bytes_to_u32_little_endian(bytes)));
122                    } else {
123                        continue;
124                    }
125                }
126                _ => continue,
127            }
128        }
129
130        let hashlock =
131            hashlock.ok_or_else(|| Error::Protocol("No hashlock provided".to_string()))?;
132
133        let locktime =
134            locktime.ok_or_else(|| Error::Protocol("No timelock provided".to_string()))?;
135
136        let funding_addrs = Address::from_str(&create_swap_response.address)?;
137
138        let blinding_str = create_swap_response
139            .blinding_key
140            .as_ref()
141            .ok_or(Error::Protocol(
142                "No blinding key provided in Create Swap Response".to_string(),
143            ))?;
144        let blinding_key = ZKKeyPair::from_seckey_str(&Secp256k1::new(), blinding_str)?;
145
146        Ok(Self {
147            swap_type: SwapType::Submarine,
148            side: None,
149            funding_addrs: Some(funding_addrs),
150            hashlock,
151            receiver_pubkey: create_swap_response.claim_public_key,
152            locktime,
153            sender_pubkey: our_pubkey,
154            blinding_key,
155        })
156    }
157
158    /// Create the struct for a reverse swap from boltz create response.
159    pub fn reverse_from_swap_resp(
160        reverse_response: &CreateReverseResponse,
161        our_pubkey: PublicKey,
162    ) -> Result<Self, Error> {
163        let claim_script = Script::from_hex(&reverse_response.swap_tree.claim_leaf.output)?;
164        let refund_script = Script::from_hex(&reverse_response.swap_tree.refund_leaf.output)?;
165
166        let claim_instructions = claim_script.instructions();
167        let refund_instructions = refund_script.instructions();
168
169        let mut last_op = OP_0NOTEQUAL;
170        let mut hashlock = None;
171        let mut locktime = None;
172
173        for instruction in claim_instructions {
174            match instruction {
175                Ok(Instruction::PushBytes(bytes)) => {
176                    if bytes.len() == 20 {
177                        hashlock = Some(hash160::Hash::from_slice(bytes)?);
178                    } else {
179                        continue;
180                    }
181                }
182                _ => continue,
183            }
184        }
185
186        for instruction in refund_instructions {
187            match instruction {
188                Ok(Instruction::Op(opcode)) => last_op = opcode,
189                Ok(Instruction::PushBytes(bytes)) => {
190                    if last_op == OP_CHECKSIGVERIFY {
191                        locktime =
192                            Some(LockTime::from_consensus(bytes_to_u32_little_endian(bytes)));
193                    } else {
194                        continue;
195                    }
196                }
197                _ => continue,
198            }
199        }
200
201        let hashlock =
202            hashlock.ok_or_else(|| Error::Protocol("No hashlock provided".to_string()))?;
203
204        let locktime =
205            locktime.ok_or_else(|| Error::Protocol("No timelock provided".to_string()))?;
206
207        let funding_addrs = Address::from_str(&reverse_response.lockup_address)?;
208
209        let blinding_str = reverse_response
210            .blinding_key
211            .as_ref()
212            .ok_or(Error::Protocol(
213                "No blinding key provided in Create Swap Response".to_string(),
214            ))?;
215        let blinding_key = ZKKeyPair::from_seckey_str(&Secp256k1::new(), blinding_str)?;
216
217        Ok(Self {
218            swap_type: SwapType::ReverseSubmarine,
219            side: None,
220            funding_addrs: Some(funding_addrs),
221            hashlock,
222            receiver_pubkey: our_pubkey,
223            locktime,
224            sender_pubkey: reverse_response.refund_public_key,
225            blinding_key,
226        })
227    }
228
229    /// Create the struct for a chain swap from boltz create response.
230    pub fn chain_from_swap_resp(
231        side: Side,
232        chain_swap_details: ChainSwapDetails,
233        our_pubkey: PublicKey,
234    ) -> Result<Self, Error> {
235        let claim_script = Script::from_hex(&chain_swap_details.swap_tree.claim_leaf.output)?;
236        let refund_script = Script::from_hex(&chain_swap_details.swap_tree.refund_leaf.output)?;
237
238        let claim_instructions = claim_script.instructions();
239        let refund_instructions = refund_script.instructions();
240
241        let mut last_op = OP_0NOTEQUAL;
242        let mut hashlock = None;
243        let mut locktime = None;
244
245        for instruction in claim_instructions {
246            match instruction {
247                Ok(Instruction::PushBytes(bytes)) => {
248                    if bytes.len() == 20 {
249                        hashlock = Some(hash160::Hash::from_slice(bytes)?);
250                    } else {
251                        continue;
252                    }
253                }
254                _ => continue,
255            }
256        }
257
258        for instruction in refund_instructions {
259            match instruction {
260                Ok(Instruction::Op(opcode)) => last_op = opcode,
261                Ok(Instruction::PushBytes(bytes)) => {
262                    if last_op == OP_CHECKSIGVERIFY {
263                        locktime =
264                            Some(LockTime::from_consensus(bytes_to_u32_little_endian(bytes)));
265                    } else {
266                        continue;
267                    }
268                }
269                _ => continue,
270            }
271        }
272
273        let hashlock =
274            hashlock.ok_or_else(|| Error::Protocol("No hashlock provided".to_string()))?;
275
276        let locktime =
277            locktime.ok_or_else(|| Error::Protocol("No timelock provided".to_string()))?;
278
279        let funding_addrs = Address::from_str(&chain_swap_details.lockup_address)?;
280
281        let (sender_pubkey, receiver_pubkey) = match side {
282            Side::Lockup => (our_pubkey, chain_swap_details.server_public_key),
283            Side::Claim => (chain_swap_details.server_public_key, our_pubkey),
284        };
285
286        let blinding_str = chain_swap_details
287            .blinding_key
288            .as_ref()
289            .ok_or(Error::Protocol(
290                "No blinding key provided in ChainSwapDetails".to_string(),
291            ))?;
292        let blinding_key = ZKKeyPair::from_seckey_str(&Secp256k1::new(), blinding_str)?;
293
294        Ok(Self {
295            swap_type: SwapType::Chain,
296            side: Some(side),
297            funding_addrs: Some(funding_addrs),
298            hashlock,
299            receiver_pubkey,
300            locktime,
301            sender_pubkey,
302            blinding_key,
303        })
304    }
305
306    fn claim_script(&self) -> Script {
307        match self.swap_type {
308            SwapType::Submarine => EBuilder::new()
309                .push_opcode(OP_HASH160)
310                .push_slice(self.hashlock.as_byte_array())
311                .push_opcode(OP_EQUALVERIFY)
312                .push_slice(&self.receiver_pubkey.inner.x_only_public_key().0.serialize())
313                .push_opcode(OP_CHECKSIG)
314                .into_script(),
315
316            SwapType::ReverseSubmarine | SwapType::Chain => EBuilder::new()
317                .push_opcode(OP_SIZE)
318                .push_int(32)
319                .push_opcode(OP_EQUALVERIFY)
320                .push_opcode(OP_HASH160)
321                .push_slice(self.hashlock.as_byte_array())
322                .push_opcode(OP_EQUALVERIFY)
323                .push_slice(&self.receiver_pubkey.inner.x_only_public_key().0.serialize())
324                .push_opcode(OP_CHECKSIG)
325                .into_script(),
326        }
327    }
328
329    fn refund_script(&self) -> Script {
330        // Refund scripts are same for all swap types
331        EBuilder::new()
332            .push_slice(&self.sender_pubkey.inner.x_only_public_key().0.serialize())
333            .push_opcode(OP_CHECKSIGVERIFY)
334            .push_int(self.locktime.to_consensus_u32().into())
335            .push_opcode(OP_CLTV)
336            .into_script()
337    }
338
339    pub fn musig_keyagg_cache(&self) -> musig::KeyAggCache {
340        match (self.swap_type, self.side.clone()) {
341            (SwapType::ReverseSubmarine, _) | (SwapType::Chain, Some(Side::Claim)) => {
342                let pubkeys = [self.sender_pubkey.inner, self.receiver_pubkey.inner];
343                let [a, b] = convert_pubkeys_for_musig(&pubkeys);
344                musig::KeyAggCache::new(&[&a, &b])
345            }
346
347            (SwapType::Submarine, _) | (SwapType::Chain, _) => {
348                let pubkeys = [self.receiver_pubkey.inner, self.sender_pubkey.inner];
349                let [a, b] = convert_pubkeys_for_musig(&pubkeys);
350                musig::KeyAggCache::new(&[&a, &b])
351            }
352        }
353    }
354
355    /// Internally used to convert struct into a bitcoin::Script type
356    fn taproot_spendinfo(&self) -> Result<TaprootSpendInfo, Error> {
357        let secp = Secp256k1::new();
358
359        // Setup Key Aggregation cache
360        let key_agg_cache = self.musig_keyagg_cache();
361
362        // Construct the Taproot
363        let internal_key = key_agg_cache.agg_pk();
364
365        let taproot_builder = TaprootBuilder::new();
366
367        let taproot_builder =
368            taproot_builder.add_leaf_with_ver(1, self.claim_script(), LeafVersion::default())?;
369        let taproot_builder =
370            taproot_builder.add_leaf_with_ver(1, self.refund_script(), LeafVersion::default())?;
371
372        let taproot_spend_info =
373            taproot_builder.finalize(&secp, convert_xonly_key(internal_key))?;
374
375        // Verify taproot construction
376        if let Some(funding_addrs) = &self.funding_addrs {
377            let claim_key = taproot_spend_info.output_key();
378
379            let lockup_spk = funding_addrs.script_pubkey();
380
381            let pubkey_instruction = lockup_spk
382                .instructions()
383                .last()
384                .ok_or(Error::Protocol(
385                    "Script should contain at least one instruction".to_string(),
386                ))?
387                .map_err(|_| Error::Protocol("Failed to parse script instruction".to_string()))?;
388
389            let lockup_xonly_pubkey_bytes = pubkey_instruction.push_bytes().ok_or(
390                Error::Protocol("Expected push bytes instruction for pubkey".to_string()),
391            )?;
392
393            let lockup_xonly_pubkey = XOnlyPublicKey::from_slice(lockup_xonly_pubkey_bytes)?;
394
395            if lockup_xonly_pubkey != claim_key.into_inner() {
396                return Err(Error::Protocol(format!(
397                    "Taproot construction Failed. Lockup Pubkey: {lockup_xonly_pubkey}, Claim Pubkey {claim_key:?}"
398                )));
399            }
400
401            log::info!("Taproot creation and verification success!");
402        }
403
404        Ok(taproot_spend_info)
405    }
406
407    /// Get taproot address for the swap script.
408    /// Always returns a confidential address
409    pub fn to_address(&self, network: LiquidChain) -> Result<EAddress, Error> {
410        let taproot_spend_info = self.taproot_spendinfo()?;
411
412        Ok(EAddress::p2tr(
413            &Secp256k1::new(),
414            taproot_spend_info.internal_key(),
415            taproot_spend_info.merkle_root(),
416            Some(self.blinding_key.public_key()),
417            network.into(),
418        ))
419    }
420
421    pub fn validate_address(&self, chain: LiquidChain, address: String) -> Result<(), Error> {
422        let to_address = self.to_address(chain)?;
423        if to_address.to_string() == address {
424            Ok(())
425        } else {
426            Err(Error::Protocol("Script/LockupAddress Mismatch".to_string()))
427        }
428    }
429
430    /// Fetch utxo for script from Electrum
431    pub async fn fetch_utxo<LC: LiquidClient + ?Sized>(
432        &self,
433        liquid_client: &LC,
434    ) -> Result<Option<(OutPoint, TxOut)>, Error> {
435        let address = self.to_address(liquid_client.network())?;
436        liquid_client.get_address_utxo(&address).await
437    }
438
439    pub(crate) async fn fetch_swap_utxo<LC: LiquidClient + ?Sized>(
440        &self,
441        lockup_tx: Option<&Transaction>,
442        liquid_client: &LC,
443        boltz_client: &BoltzApiClientV2,
444        swap_id: &str,
445        tx_kind: SwapTxKind,
446    ) -> Result<(OutPoint, TxOut), Error> {
447        let utxo = match lockup_tx {
448            Some(tx) => self.find_utxo(tx, liquid_client.network()).await,
449            None => match self.fetch_utxo(liquid_client).await {
450                Ok(Some(r)) => Ok(r),
451                Ok(None) | Err(_) => {
452                    self.fetch_lockup_utxo_boltz(
453                        liquid_client.network(),
454                        boltz_client,
455                        swap_id,
456                        tx_kind,
457                    )
458                    .await
459                }
460            },
461        }?;
462        Ok(utxo)
463    }
464
465    pub(crate) async fn find_utxo(
466        &self,
467        tx: &Transaction,
468        network: LiquidChain,
469    ) -> Result<(OutPoint, TxOut), Error> {
470        let address = self.to_address(network)?;
471        find_utxo(tx, &address.script_pubkey()).ok_or(Error::Protocol(
472            "No Liquid UTXO detected for this script".to_string(),
473        ))
474    }
475
476    /// Fetch utxo for script from BoltzApi
477    pub async fn fetch_lockup_utxo_boltz(
478        &self,
479        network: LiquidChain,
480        boltz_client: &BoltzApiClientV2,
481        swap_id: &str,
482        tx_kind: SwapTxKind,
483    ) -> Result<(OutPoint, TxOut), Error> {
484        let hex = match self.swap_type {
485            SwapType::Chain => match tx_kind {
486                SwapTxKind::Claim => {
487                    boltz_client
488                        .get_chain_txs(swap_id)
489                        .await?
490                        .server_lock
491                        .ok_or(Error::Protocol(
492                            "No server_lock transaction for Chain Swap available".to_string(),
493                        ))?
494                        .transaction
495                        .hex
496                }
497                SwapTxKind::Refund => {
498                    boltz_client
499                        .get_chain_txs(swap_id)
500                        .await?
501                        .user_lock
502                        .ok_or(Error::Protocol(
503                            "No user_lock transaction for Chain Swap available".to_string(),
504                        ))?
505                        .transaction
506                        .hex
507                }
508            },
509            SwapType::ReverseSubmarine => boltz_client.get_reverse_tx(swap_id).await?.hex,
510            SwapType::Submarine => boltz_client.get_submarine_tx(swap_id).await?.hex,
511        };
512        if hex.is_none() {
513            return Err(Error::Hex(
514                "No transaction hex found in boltz response".to_string(),
515            ));
516        }
517        let tx: Transaction = elements::encode::deserialize(&hex::decode(hex.unwrap())?)?;
518        self.find_utxo(&tx, network).await
519    }
520
521    // Get the chain genesis hash. Requires for sighash calculation
522    pub async fn genesis_hash<LC: LiquidClient>(
523        &self,
524        liquid_client: &LC,
525    ) -> Result<BlockHash, Error> {
526        liquid_client.get_genesis_hash().await
527    }
528}
529
530fn bytes_to_u32_little_endian(bytes: &[u8]) -> u32 {
531    let mut result = 0u32;
532    for (i, &byte) in bytes.iter().enumerate() {
533        result |= (byte as u32) << (8 * i);
534    }
535    result
536}
537
538/// Liquid swap transaction helper.
539#[derive(Debug, Clone)]
540pub struct LBtcSwapTx {
541    pub kind: SwapTxKind,
542    pub swap_script: LBtcSwapScript,
543    pub output_address: Address,
544    pub funding_outpoint: OutPoint,
545    pub funding_utxo: TxOut, // there should only ever be one outpoint in a swap
546    pub genesis_hash: BlockHash, // Required to calculate sighash
547}
548
549impl LBtcSwapTx {
550    pub(crate) async fn new_claim_with_utxo<LC: LiquidClient + ?Sized>(
551        swap_script: LBtcSwapScript,
552        output_address: String,
553        liquid_client: &LC,
554        utxo: (OutPoint, TxOut),
555    ) -> Result<LBtcSwapTx, Error> {
556        if swap_script.swap_type == SwapType::Submarine {
557            return Err(Error::Protocol(
558                "Claim transactions cannot be constructed for Submarine swaps.".to_string(),
559            ));
560        }
561
562        let genesis_hash = liquid_client.get_genesis_hash().await?;
563
564        Ok(LBtcSwapTx {
565            kind: SwapTxKind::Claim,
566            swap_script,
567            output_address: Address::from_str(&output_address)?,
568            funding_outpoint: utxo.0,
569            funding_utxo: utxo.1,
570            genesis_hash,
571        })
572    }
573
574    /// Craft a new ClaimTx. Only works for Reverse and Chain Swaps.
575    pub async fn new_claim<LC: LiquidClient + ?Sized>(
576        swap_script: LBtcSwapScript,
577        output_address: String,
578        liquid_client: &LC,
579        boltz_client: &BoltzApiClientV2,
580        swap_id: String,
581    ) -> Result<LBtcSwapTx, Error> {
582        let utxo = swap_script
583            .fetch_swap_utxo(
584                None,
585                liquid_client,
586                boltz_client,
587                &swap_id,
588                SwapTxKind::Claim,
589            )
590            .await?;
591
592        Self::new_claim_with_utxo(swap_script, output_address, liquid_client, utxo).await
593    }
594
595    /// Construct a RefundTX corresponding to the swap_script. Only works for Submarine and Chain Swaps.
596    pub async fn new_refund<LC: LiquidClient + ?Sized>(
597        swap_script: LBtcSwapScript,
598        output_address: &str,
599        liquid_client: &LC,
600        boltz_client: &BoltzApiClientV2,
601        swap_id: String,
602    ) -> Result<LBtcSwapTx, Error> {
603        if swap_script.swap_type == SwapType::ReverseSubmarine {
604            return Err(Error::Protocol(
605                "Refund Txs cannot be constructed for Reverse Submarine Swaps.".to_string(),
606            ));
607        }
608
609        let address = Address::from_str(output_address)?;
610        let (funding_outpoint, funding_utxo) = swap_script
611            .fetch_swap_utxo(
612                None,
613                liquid_client,
614                boltz_client,
615                &swap_id,
616                SwapTxKind::Refund,
617            )
618            .await?;
619
620        let genesis_hash = liquid_client.get_genesis_hash().await?;
621
622        Ok(LBtcSwapTx {
623            kind: SwapTxKind::Refund,
624            swap_script,
625            output_address: address,
626            funding_outpoint,
627            funding_utxo,
628            genesis_hash,
629        })
630    }
631
632    /// Compute the Musig partial signature.
633    /// This is used to cooperatively close a Submarine or Chain Swap.
634    pub fn partial_sign(
635        &self,
636        keys: &Keypair,
637        pub_nonce: &str,
638        transaction_hash: &str,
639    ) -> Result<(musig::PartialSignature, musig::PublicNonce), Error> {
640        self.swap_script
641            .partial_sign(keys, pub_nonce, transaction_hash)
642    }
643
644    /// Sign a claim transaction.
645    /// Panics if called on a Submarine Swap or Refund Tx.
646    /// If the claim is cooperative, provide the other party's partial sigs.
647    /// If this is None, transaction will be claimed via taproot script path.
648    pub async fn sign_claim(
649        &self,
650        keys: &Keypair,
651        preimage: &Preimage,
652        fee: Fee,
653        is_cooperative: Option<Cooperative<'_>>,
654        is_discount_ct: bool,
655    ) -> Result<Transaction, Error> {
656        if self.swap_script.swap_type == SwapType::Submarine {
657            return Err(Error::Protocol(
658                "Claim Tx signing is not applicable for Submarine Swaps".to_string(),
659            ));
660        }
661
662        if self.kind == SwapTxKind::Refund {
663            return Err(Error::Protocol(
664                "Cannot sign claim with refund-type LBtcSwapTx".to_string(),
665            ));
666        }
667
668        let mut claim_tx = create_tx_with_fee(
669            fee,
670            |fee| self.create_claim(keys, preimage, fee, is_cooperative.is_some()),
671            |tx| tx_size(&tx, is_discount_ct),
672        )?;
673
674        // If its a cooperative claim, compute the Musig2 Aggregate Signature and use Keypath spending
675        if let Some(Cooperative {
676            boltz_api,
677            swap_id,
678            signature,
679        }) = is_cooperative
680        {
681            let claim_tx_taproot_hash = SighashCache::new(&claim_tx)
682                .taproot_key_spend_signature_hash(
683                    0,
684                    &Prevouts::All(&[&self.funding_utxo]),
685                    SchnorrSighashType::Default,
686                    self.genesis_hash,
687                )?;
688
689            let msg = *claim_tx_taproot_hash.as_byte_array();
690
691            let mut key_agg_cache = self.swap_script.musig_keyagg_cache();
692
693            let tweak = Scalar::from_be_bytes(
694                *self
695                    .swap_script
696                    .taproot_spendinfo()?
697                    .tap_tweak()
698                    .as_byte_array(),
699            )?;
700
701            let _ = key_agg_cache.pubkey_xonly_tweak_add(&tweak)?;
702
703            let session_secret_rand =
704                musig::SessionSecretRand::assume_unique_per_nonce_gen(rng_32b());
705
706            let mut extra_rand = [0u8; 32];
707            OsRng.fill_bytes(&mut extra_rand);
708
709            let (claim_sec_nonce, claim_pub_nonce) = key_agg_cache.nonce_gen(
710                session_secret_rand,
711                convert_public_key(keys.public_key()),
712                &msg,
713                Some(extra_rand),
714            );
715
716            // Step 7: Get boltz's partial sig
717            let claim_tx_hex = serialize(&claim_tx).to_lower_hex_string();
718            let partial_sig_resp = match self.swap_script.swap_type {
719                SwapType::Chain => {
720                    boltz_api
721                        .post_chain_claim_tx_details(
722                            &swap_id,
723                            preimage,
724                            signature,
725                            ToSign {
726                                pub_nonce: claim_pub_nonce.serialize().to_lower_hex_string(),
727                                transaction: claim_tx_hex,
728                                index: 0,
729                            },
730                        )
731                        .await
732                }
733                SwapType::ReverseSubmarine => {
734                    boltz_api
735                        .get_reverse_partial_sig(
736                            &swap_id,
737                            preimage,
738                            &claim_pub_nonce,
739                            &claim_tx_hex,
740                        )
741                        .await
742                }
743                _ => Err(Error::Protocol(format!(
744                    "Cannot get partial sig for {:?} Swap",
745                    self.swap_script.swap_type
746                ))),
747            }?;
748
749            let boltz_public_nonce = musig::PublicNonce::from_str(&partial_sig_resp.pub_nonce)?;
750
751            let boltz_partial_sig =
752                musig::PartialSignature::from_str(&partial_sig_resp.partial_signature)?;
753
754            let agg_nonce = musig::AggregatedNonce::new(&[&boltz_public_nonce, &claim_pub_nonce]);
755
756            let musig_session = musig::Session::new(&key_agg_cache, agg_nonce, &msg);
757
758            // Verify the sigs.
759            let boltz_partial_sig_verify = musig_session.partial_verify(
760                &key_agg_cache,
761                &boltz_partial_sig,
762                &boltz_public_nonce,
763                convert_public_key(self.swap_script.sender_pubkey.inner), //boltz key
764            );
765
766            if !boltz_partial_sig_verify {
767                return Err(Error::Taproot(
768                    "Unable to verify Partial Signature".to_string(),
769                ));
770            }
771
772            let our_partial_sig =
773                musig_session.partial_sign(claim_sec_nonce, &convert_keypair(keys), &key_agg_cache);
774
775            let schnorr_sig = musig_session
776                .partial_sig_agg(&[&boltz_partial_sig, &our_partial_sig])
777                .assume_valid();
778
779            let final_schnorr_sig = SchnorrSig {
780                sig: convert_schnorr_signature(schnorr_sig),
781                hash_ty: SchnorrSighashType::Default,
782            };
783
784            let output_key = self.swap_script.taproot_spendinfo()?.output_key();
785
786            let secp = Secp256k1::new();
787            let msg = Message::from_digest_slice(&msg)?;
788            secp.verify_schnorr(&final_schnorr_sig.sig, &msg, &output_key.into_inner())?;
789
790            let mut script_witness = Witness::new();
791            script_witness.push(final_schnorr_sig.to_vec());
792
793            let witness = TxInWitness {
794                amount_rangeproof: None,
795                inflation_keys_rangeproof: None,
796                script_witness: script_witness.to_vec(),
797                pegin_witness: vec![],
798            };
799
800            claim_tx.input[0].witness = witness;
801        }
802
803        Ok(claim_tx)
804    }
805
806    fn create_claim(
807        &self,
808        keys: &Keypair,
809        preimage: &Preimage,
810        absolute_fees: u64,
811        is_cooperative: bool,
812    ) -> Result<Transaction, Error> {
813        if preimage.bytes.is_none() {
814            return Err(Error::Protocol("No preimage provided".to_string()));
815        }
816
817        let claim_txin = TxIn {
818            sequence: Sequence::MAX,
819            previous_output: self.funding_outpoint,
820            script_sig: Script::new(),
821            witness: TxInWitness::default(),
822            is_pegin: false,
823            asset_issuance: AssetIssuance::default(),
824        };
825
826        let secp = Secp256k1::new();
827        let mut rng = OsRng;
828
829        let unblined_utxo = self
830            .funding_utxo
831            .unblind(&secp, self.swap_script.blinding_key.secret_key())?;
832        let asset_id = unblined_utxo.asset;
833        let out_abf = AssetBlindingFactor::new(&mut rng);
834        let exp_asset = Asset::Explicit(asset_id);
835
836        let (blinded_asset, asset_surjection_proof) =
837            exp_asset.blind(&mut rng, &secp, out_abf, &[unblined_utxo])?;
838
839        let output_value = Amount::from_sat(unblined_utxo.value)
840            .checked_sub(Amount::from_sat(absolute_fees))
841            .ok_or(Error::Protocol(format!(
842                "Output value {} is less than fees {}",
843                unblined_utxo.value, absolute_fees
844            )))?;
845
846        let final_vbf = ValueBlindingFactor::last(
847            &secp,
848            output_value.to_sat(),
849            out_abf,
850            &[(
851                unblined_utxo.value,
852                unblined_utxo.asset_bf,
853                unblined_utxo.value_bf,
854            )],
855            &[(
856                absolute_fees,
857                AssetBlindingFactor::zero(),
858                ValueBlindingFactor::zero(),
859            )],
860        );
861        let explicit_value = elements::confidential::Value::Explicit(output_value.to_sat());
862        let msg = elements::RangeProofMessage {
863            asset: asset_id,
864            bf: out_abf,
865        };
866        let ephemeral_sk = SecretKey::new(&mut rng);
867
868        // assuming we always use a blinded address that has an extractable blinding pub
869        let blinding_key = self
870            .output_address
871            .blinding_pubkey
872            .ok_or(Error::Protocol("No blinding key in tx.".to_string()))?;
873        let (blinded_value, nonce, rangeproof) = explicit_value.blind(
874            &secp,
875            final_vbf,
876            blinding_key,
877            ephemeral_sk,
878            &self.output_address.script_pubkey(),
879            &msg,
880        )?;
881
882        let tx_out_witness = TxOutWitness {
883            surjection_proof: Some(Box::new(asset_surjection_proof)), // from asset blinding
884            rangeproof: Some(Box::new(rangeproof)),                   // from value blinding
885        };
886        let payment_output: TxOut = TxOut {
887            script_pubkey: self.output_address.script_pubkey(),
888            value: blinded_value,
889            asset: blinded_asset,
890            nonce,
891            witness: tx_out_witness,
892        };
893        let fee_output: TxOut = TxOut::new_fee(absolute_fees, asset_id);
894
895        let mut claim_tx = Transaction {
896            version: 2,
897            lock_time: LockTime::ZERO,
898            input: vec![claim_txin],
899            output: vec![payment_output, fee_output],
900        };
901
902        if is_cooperative {
903            claim_tx.input[0].witness = Self::stubbed_cooperative_witness();
904        } else {
905            // If Non-Cooperative claim use the Script Path spending
906            claim_tx.input[0].sequence = Sequence::ZERO;
907            let claim_script = self.swap_script.claim_script();
908            let leaf_hash = TapLeafHash::from_script(&claim_script, LeafVersion::default());
909
910            let sighash = SighashCache::new(&claim_tx).taproot_script_spend_signature_hash(
911                0,
912                &Prevouts::All(&[&self.funding_utxo]),
913                leaf_hash,
914                SchnorrSighashType::Default,
915                self.genesis_hash,
916            )?;
917
918            let msg = Message::from_digest_slice(sighash.as_byte_array())?;
919
920            let sig = secp.sign_schnorr(&msg, keys);
921
922            let final_sig = SchnorrSig {
923                sig,
924                hash_ty: SchnorrSighashType::Default,
925            };
926
927            let control_block = match self
928                .swap_script
929                .taproot_spendinfo()?
930                .control_block(&(claim_script.clone(), LeafVersion::default()))
931            {
932                Some(r) => r,
933                None => return Err(Error::Taproot("Could not create control block".to_string())),
934            };
935
936            let mut script_witness = Witness::new();
937            script_witness.push(final_sig.to_vec());
938            script_witness.push(preimage.bytes.ok_or(Error::Protocol(
939                "Preimage bytes not available - cannot claim without actual preimage".to_string(),
940            ))?);
941            script_witness.push(claim_script.as_bytes());
942            script_witness.push(control_block.serialize());
943
944            let witness = TxInWitness {
945                amount_rangeproof: None,
946                inflation_keys_rangeproof: None,
947                script_witness: script_witness.to_vec(),
948                pegin_witness: vec![],
949            };
950
951            claim_tx.input[0].witness = witness;
952        }
953
954        Ok(claim_tx)
955    }
956
957    /// Sign a refund transaction.
958    /// Panics if called on a Reverse Swap or Claim Tx.
959    pub async fn sign_refund(
960        &self,
961        keys: &Keypair,
962        fee: Fee,
963        is_cooperative: Option<Cooperative<'_>>,
964        is_discount_ct: bool,
965    ) -> Result<Transaction, Error> {
966        if self.swap_script.swap_type == SwapType::ReverseSubmarine {
967            return Err(Error::Protocol(
968                "Refund Tx signing is not applicable for Reverse Submarine Swaps".to_string(),
969            ));
970        }
971
972        if self.kind == SwapTxKind::Claim {
973            return Err(Error::Protocol(
974                "Cannot sign refund with a claim-type LBtcSwapTx".to_string(),
975            ));
976        }
977
978        let mut refund_tx = create_tx_with_fee(
979            fee,
980            |fee| self.create_refund(keys, fee, is_cooperative.is_some()),
981            |tx| tx_size(&tx, is_discount_ct),
982        )?;
983
984        if let Some(Cooperative {
985            boltz_api, swap_id, ..
986        }) = is_cooperative
987        {
988            let secp = Secp256k1::new();
989
990            refund_tx.lock_time = LockTime::ZERO;
991
992            let claim_tx_taproot_hash = SighashCache::new(&refund_tx)
993                .taproot_key_spend_signature_hash(
994                    0,
995                    &Prevouts::All(&[&self.funding_utxo]),
996                    SchnorrSighashType::Default,
997                    self.genesis_hash,
998                )?;
999
1000            let msg = *claim_tx_taproot_hash.as_byte_array();
1001
1002            let mut key_agg_cache = self.swap_script.musig_keyagg_cache();
1003
1004            let tweak = Scalar::from_be_bytes(
1005                *self
1006                    .swap_script
1007                    .taproot_spendinfo()?
1008                    .tap_tweak()
1009                    .as_byte_array(),
1010            )?;
1011
1012            let _ = key_agg_cache.pubkey_xonly_tweak_add(&tweak)?;
1013
1014            let session_secret_rand =
1015                musig::SessionSecretRand::assume_unique_per_nonce_gen(rng_32b());
1016
1017            let mut extra_rand = [0u8; 32];
1018            OsRng.fill_bytes(&mut extra_rand);
1019
1020            let (sec_nonce, pub_nonce) = key_agg_cache.nonce_gen(
1021                session_secret_rand,
1022                convert_public_key(keys.public_key()),
1023                &msg,
1024                Some(extra_rand),
1025            );
1026
1027            // Step 7: Get boltz's partial sig
1028            let refund_tx_hex = serialize(&refund_tx).to_lower_hex_string();
1029            let partial_sig_resp = match self.swap_script.swap_type {
1030                SwapType::Chain => {
1031                    boltz_api
1032                        .get_chain_partial_sig(&swap_id, 0, &pub_nonce, &refund_tx_hex)
1033                        .await
1034                }
1035                SwapType::Submarine => {
1036                    boltz_api
1037                        .get_submarine_partial_sig(&swap_id, 0, &pub_nonce, &refund_tx_hex)
1038                        .await
1039                }
1040                _ => Err(Error::Protocol(format!(
1041                    "Cannot get partial sig for {:?} Swap",
1042                    self.swap_script.swap_type
1043                ))),
1044            }?;
1045
1046            let boltz_public_nonce = musig::PublicNonce::from_str(&partial_sig_resp.pub_nonce)?;
1047
1048            let boltz_partial_sig =
1049                musig::PartialSignature::from_str(&partial_sig_resp.partial_signature)?;
1050
1051            let agg_nonce = musig::AggregatedNonce::new(&[&boltz_public_nonce, &pub_nonce]);
1052
1053            let musig_session = musig::Session::new(&key_agg_cache, agg_nonce, &msg);
1054
1055            // Verify the sigs.
1056            let boltz_partial_sig_verify = musig_session.partial_verify(
1057                &key_agg_cache,
1058                &boltz_partial_sig,
1059                &boltz_public_nonce,
1060                convert_public_key(self.swap_script.receiver_pubkey.inner), //boltz key
1061            );
1062
1063            if !boltz_partial_sig_verify {
1064                return Err(Error::Taproot(
1065                    "Unable to verify Partial Signature".to_string(),
1066                ));
1067            }
1068
1069            let our_partial_sig =
1070                musig_session.partial_sign(sec_nonce, &convert_keypair(keys), &key_agg_cache);
1071
1072            let schnorr_sig = musig_session
1073                .partial_sig_agg(&[&boltz_partial_sig, &our_partial_sig])
1074                .assume_valid();
1075
1076            let final_schnorr_sig = SchnorrSig {
1077                sig: convert_schnorr_signature(schnorr_sig),
1078                hash_ty: SchnorrSighashType::Default,
1079            };
1080
1081            let output_key = self.swap_script.taproot_spendinfo()?.output_key();
1082
1083            let msg = Message::from_digest_slice(&msg)?;
1084            secp.verify_schnorr(&final_schnorr_sig.sig, &msg, &output_key.into_inner())?;
1085
1086            let mut script_witness = Witness::new();
1087            script_witness.push(final_schnorr_sig.to_vec());
1088
1089            let witness = TxInWitness {
1090                amount_rangeproof: None,
1091                inflation_keys_rangeproof: None,
1092                script_witness: script_witness.to_vec(),
1093                pegin_witness: vec![],
1094            };
1095
1096            refund_tx.input[0].witness = witness;
1097        }
1098
1099        Ok(refund_tx)
1100    }
1101
1102    fn create_refund(
1103        &self,
1104        keys: &Keypair,
1105        absolute_fees: u64,
1106        is_cooperative: bool,
1107    ) -> Result<Transaction, Error> {
1108        // Create unsigned refund transaction
1109        let refund_txin = TxIn {
1110            sequence: Sequence::MAX,
1111            previous_output: self.funding_outpoint,
1112            script_sig: Script::new(),
1113            witness: TxInWitness::default(),
1114            is_pegin: false,
1115            asset_issuance: AssetIssuance::default(),
1116        };
1117
1118        let secp = Secp256k1::new();
1119        let mut rng = OsRng;
1120
1121        let unblined_utxo = self
1122            .funding_utxo
1123            .unblind(&secp, self.swap_script.blinding_key.secret_key())?;
1124        let asset_id = unblined_utxo.asset;
1125        let out_abf = AssetBlindingFactor::new(&mut rng);
1126        let exp_asset = Asset::Explicit(asset_id);
1127
1128        let (blinded_asset, asset_surjection_proof) =
1129            exp_asset.blind(&mut rng, &secp, out_abf, &[unblined_utxo])?;
1130
1131        let output_value = Amount::from_sat(unblined_utxo.value)
1132            .checked_sub(Amount::from_sat(absolute_fees))
1133            .ok_or(Error::Protocol(format!(
1134                "Output value {} is less than fees {}",
1135                unblined_utxo.value, absolute_fees
1136            )))?;
1137
1138        let final_vbf = ValueBlindingFactor::last(
1139            &secp,
1140            output_value.to_sat(),
1141            out_abf,
1142            &[(
1143                unblined_utxo.value,
1144                unblined_utxo.asset_bf,
1145                unblined_utxo.value_bf,
1146            )],
1147            &[(
1148                absolute_fees,
1149                AssetBlindingFactor::zero(),
1150                ValueBlindingFactor::zero(),
1151            )],
1152        );
1153        let explicit_value = elements::confidential::Value::Explicit(output_value.to_sat());
1154        let msg = elements::RangeProofMessage {
1155            asset: asset_id,
1156            bf: out_abf,
1157        };
1158        let ephemeral_sk = SecretKey::new(&mut rng);
1159
1160        // assuming we always use a blinded address that has an extractable blinding pub
1161        let blinding_key = self
1162            .output_address
1163            .blinding_pubkey
1164            .ok_or(Error::Protocol("No blinding key in tx.".to_string()))?;
1165        let (blinded_value, nonce, rangeproof) = explicit_value.blind(
1166            &secp,
1167            final_vbf,
1168            blinding_key,
1169            ephemeral_sk,
1170            &self.output_address.script_pubkey(),
1171            &msg,
1172        )?;
1173
1174        let tx_out_witness = TxOutWitness {
1175            surjection_proof: Some(Box::new(asset_surjection_proof)), // from asset blinding
1176            rangeproof: Some(Box::new(rangeproof)),                   // from value blinding
1177        };
1178        let payment_output: TxOut = TxOut {
1179            script_pubkey: self.output_address.script_pubkey(),
1180            value: blinded_value,
1181            asset: blinded_asset,
1182            nonce,
1183            witness: tx_out_witness,
1184        };
1185        let fee_output: TxOut = TxOut::new_fee(absolute_fees, asset_id);
1186
1187        let refund_script = self.swap_script.refund_script();
1188
1189        let lock_time = match refund_script
1190            .instructions()
1191            .filter_map(|i| {
1192                let ins = i.ok()?;
1193                if let Instruction::PushBytes(bytes) = ins {
1194                    if bytes.len() < 5_usize {
1195                        Some(LockTime::from_consensus(bytes_to_u32_little_endian(bytes)))
1196                    } else {
1197                        None
1198                    }
1199                } else {
1200                    None
1201                }
1202            })
1203            .next()
1204        {
1205            Some(r) => r,
1206            None => {
1207                return Err(Error::Protocol(
1208                    "Error getting timelock from refund script".to_string(),
1209                ))
1210            }
1211        };
1212
1213        let mut refund_tx = Transaction {
1214            version: 2,
1215            lock_time,
1216            input: vec![refund_txin],
1217            output: vec![fee_output, payment_output],
1218        };
1219
1220        if is_cooperative {
1221            refund_tx.input[0].witness = Self::stubbed_cooperative_witness();
1222        } else {
1223            refund_tx.input[0].sequence = Sequence::ZERO;
1224
1225            let leaf_hash = TapLeafHash::from_script(&refund_script, LeafVersion::default());
1226
1227            let sighash = SighashCache::new(&refund_tx).taproot_script_spend_signature_hash(
1228                0,
1229                &Prevouts::All(&[&self.funding_utxo]),
1230                leaf_hash,
1231                SchnorrSighashType::Default,
1232                self.genesis_hash,
1233            )?;
1234
1235            let msg = Message::from_digest_slice(sighash.as_byte_array())?;
1236
1237            let sig = secp.sign_schnorr(&msg, keys);
1238
1239            let final_sig = SchnorrSig {
1240                sig,
1241                hash_ty: SchnorrSighashType::Default,
1242            };
1243
1244            let control_block = match self
1245                .swap_script
1246                .taproot_spendinfo()?
1247                .control_block(&(refund_script.clone(), LeafVersion::default()))
1248            {
1249                Some(r) => r,
1250                None => return Err(Error::Taproot("Could not create control block".to_string())),
1251            };
1252
1253            let mut script_witness = Witness::new();
1254            script_witness.push(final_sig.to_vec());
1255            script_witness.push(refund_script.as_bytes());
1256            script_witness.push(control_block.serialize());
1257
1258            let witness = TxInWitness {
1259                amount_rangeproof: None,
1260                inflation_keys_rangeproof: None,
1261                script_witness: script_witness.to_vec(),
1262                pegin_witness: vec![],
1263            };
1264
1265            refund_tx.input[0].witness = witness;
1266        }
1267
1268        Ok(refund_tx)
1269    }
1270
1271    fn stubbed_cooperative_witness() -> TxInWitness {
1272        let mut witness = Witness::new();
1273        // Stub because we don't want to create cooperative signatures here
1274        // but still be able to have an accurate size estimation
1275        witness.push([0; 64]);
1276
1277        TxInWitness {
1278            amount_rangeproof: None,
1279            inflation_keys_rangeproof: None,
1280            script_witness: witness.to_vec(),
1281            pegin_witness: vec![],
1282        }
1283    }
1284
1285    /// Calculate the size of a transaction.
1286    /// Use this before calling drain to help calculate the absolute fees.
1287    /// Multiply the size by the fee_rate to get the absolute fees.
1288    pub fn size(
1289        &self,
1290        keys: &Keypair,
1291        is_cooperative: bool,
1292        is_discount_ct: bool,
1293    ) -> Result<usize, Error> {
1294        let dummy_abs_fee = 1;
1295        let tx = match self.kind {
1296            SwapTxKind::Claim => {
1297                let preimage = Preimage::from_vec([0; 32].to_vec())?;
1298                self.create_claim(keys, &preimage, dummy_abs_fee, is_cooperative)?
1299            }
1300            SwapTxKind::Refund => self.create_refund(keys, dummy_abs_fee, is_cooperative)?,
1301        };
1302        Ok(tx_size(&tx, is_discount_ct))
1303    }
1304
1305    /// Broadcast transaction to the network
1306    pub async fn broadcast<LC: LiquidClient + ?Sized>(
1307        &self,
1308        signed_tx: &Transaction,
1309        liquid_client: &LC,
1310    ) -> Result<String, Error> {
1311        liquid_client.broadcast_tx(signed_tx).await
1312    }
1313}
1314
1315fn convert_schnorr_signature(
1316    schnorr_sig: secp256k1_musig::schnorr::Signature,
1317) -> bitcoin::secp256k1::schnorr::Signature {
1318    bitcoin::secp256k1::schnorr::Signature::from_slice(schnorr_sig.as_byte_array())
1319        .expect("signature size matches")
1320}
1321
1322fn convert_pubkeys_for_musig(
1323    pubkeys: &[elements::secp256k1_zkp::PublicKey; 2],
1324) -> [secp256k1_musig::PublicKey; 2] {
1325    [
1326        convert_public_key(pubkeys[0]),
1327        convert_public_key(pubkeys[1]),
1328    ]
1329}
1330
1331fn convert_xonly_key(key: secp256k1_musig::XOnlyPublicKey) -> bitcoin::XOnlyPublicKey {
1332    bitcoin::XOnlyPublicKey::from_slice(&key.serialize()[..]).expect("xonly key size matches")
1333}
1334
1335fn convert_public_key(key: elements::secp256k1_zkp::PublicKey) -> secp256k1_musig::PublicKey {
1336    secp256k1_musig::PublicKey::from_slice(&key.serialize()[..]).expect("public key size matches")
1337}
1338
1339impl SwapScriptCommon for LBtcSwapScript {
1340    fn swap_type(&self) -> SwapType {
1341        self.swap_type
1342    }
1343
1344    /// Compute the Musig partial signature.
1345    /// This is used to cooperatively close a Submarine or Chain Swap.
1346    fn partial_sign(
1347        &self,
1348        keys: &Keypair,
1349        pub_nonce: &str,
1350        transaction_hash: &str,
1351    ) -> Result<(musig::PartialSignature, musig::PublicNonce), Error> {
1352        // Step 1: Start with a Musig KeyAgg Cache
1353        let pubkeys = [self.receiver_pubkey.inner, self.sender_pubkey.inner];
1354        let [a, b] = convert_pubkeys_for_musig(&pubkeys);
1355
1356        let mut key_agg_cache = musig::KeyAggCache::new(&[&a, &b]);
1357
1358        let tweak = Scalar::from_be_bytes(*self.taproot_spendinfo()?.tap_tweak().as_byte_array())?;
1359
1360        let _ = key_agg_cache.pubkey_xonly_tweak_add(&tweak)?;
1361
1362        let session_secret_rand = musig::SessionSecretRand::assume_unique_per_nonce_gen(rng_32b());
1363
1364        let msg = hex_to_bytes32(transaction_hash)?;
1365
1366        // Step 4: Start the Musig2 Signing session
1367        let mut extra_rand = [0u8; 32];
1368        OsRng.fill_bytes(&mut extra_rand);
1369
1370        let (gen_sec_nonce, gen_pub_nonce) = key_agg_cache.nonce_gen(
1371            session_secret_rand,
1372            convert_public_key(keys.public_key()),
1373            &msg,
1374            Some(extra_rand),
1375        );
1376
1377        let boltz_nonce = musig::PublicNonce::from_str(pub_nonce)?;
1378
1379        let agg_nonce = musig::AggregatedNonce::new(&[&boltz_nonce, &gen_pub_nonce]);
1380
1381        let musig_session = musig::Session::new(&key_agg_cache, agg_nonce, &msg);
1382
1383        let partial_sig =
1384            musig_session.partial_sign(gen_sec_nonce, &convert_keypair(keys), &key_agg_cache);
1385
1386        Ok((partial_sig, gen_pub_nonce))
1387    }
1388}
1389
1390fn convert_keypair(keys: &Keypair) -> secp256k1_musig::Keypair {
1391    secp256k1_musig::Keypair::from_seckey_byte_array(keys.secret_bytes())
1392        .expect("keypair size matches")
1393}
1394
1395fn tx_size(tx: &Transaction, is_discount_ct: bool) -> usize {
1396    match is_discount_ct {
1397        true => tx.discount_vsize(),
1398        false => tx.vsize(),
1399    }
1400}
1401
1402#[cfg(test)]
1403mod tests {
1404    use super::*;
1405
1406    #[macros::test_all]
1407    fn test_tx_size() {
1408        // From https://github.com/ElementsProject/ELIPs/blob/main/elip-0200.mediawiki#test-vectors
1409        let tx: Transaction = elements::encode::deserialize(&hex::decode("0200000001017b85545c658d507ff56f315c77f910dd19cc9ceb7d5e1e4d3a3f8be4a91fe7440000000000fdffffff020bb6478c61c8f5f024ded219c967314685257f0ded894eaf626a00843a6ab80412091ee78237e38fb36c8be564ecd76e65f743065522f38f838367680ed7287b459103aabd97d4c8f3eac9555edfd2a709370b802335da478b6578501f72a4d100482716001455f4f701eec6059f956a40335e317a96a5e87ab5016d521c38ec1ea15734ae22b7c46064412829c0d0579f0a713d1c04ede979026f01000000000000000e00000000000000000347304402205d62bc013832eb6a631fe0285c49b7e27846e03189a245bec8f86346382282a702206c6e839b4b1d79d74662e432b724671402a6cfa2287911677c7061a3a32abe34012042c6504afda18a302bbf935f1dc646f71872a9a2fb5ed9e0cffb64588fd0d0a865a9141243397ee5e188bdcd17c9529c1382c7f8bc0fe987632102a3cd0d865794542994737e776dc3827a046c02ea2693f1d1f64315b3557bbb8b670395f72bb17521034a2e0343a515cf7d4a583d05bec3ee9fc16758cae791c10064fa92d65672d1fe68ac004301000177ce2a14a4f9e556fc846219827e1bc584caf9ef35e761dbf1f961a89b8285bde8fbe242c6984dd28719a792cd2e63535287db9a3b1fc4e4c5ae28cc5e8973d0fd4e10603300000000000000014cf45a01f0036bec883cdd4d5d8de1d7b3f2ec125733ce2e123ef3ff0085c50fd1b8cd3101c24fd8fff0bab803cda813aad9645ca6714ce768da75da09b58851585551c425e729d6faf4186a6659ea107f4ef35cc458dae565f1337af46cde218563eb3a756dc5d532717cc775fc0d04fbf4492070eb3cd9943a12fd07939d69a71090871e1ddf8fe716e2bc3f3364783cdb1d6a704325ca6c4334171563ae7bfcc9766ab848a65f47973753b2758b4404f17e54527080cfb980d1227f70cc0e77212d06aea909c7f2ac38f4a75c387464f8b70e33061f017a6fbbccf0673d08aebae2a1ce6cf9dd8c98791b1f4d653788b2ed6dd65cf9795eac568744e386d68c89d973ca079298f8d292b6bee71fad94a0f83aaf070ccfeb6c6de20baf8c6f1083dcdd539fae6ed74832100ea7c07296c0af2201523c3abf8b784ca8a235556d5bae668f17d9a353fd49dbae623ca44830a8fc4963419e49a9dc99bf87ea0414be3b43a6eab8ce54695d66887b261c08252a501d0c78d30be1ae3fc10f557f4d228ef38da496b22c5fa79d92e2c190b9d31f286dc0e3c8489fcb8e0603f8b93a6eb1ec726a7e0015e70407da186d85b290b054747276a8928443e1108cb67738d156787d20553c39fa0449f95addbf42170fdab8107d1f93fcd841964b6e6c4c140d0c4ed1463835e603f5012a4aafd5b038ceb9b4a5b7e2688cfd8c4f2bfafaf0bb5bb1aa7a7f13bd47ff3da57c4c88b741fd9ff97abc23d4047f690d59c4c67494f47125fe0f626ad409a92d72907ad0b1762b5271f474fa552d9139fcb1103db24f7a29726a5e41a6dbc43590c14a62eb1b2aa0f160134c42c6c87c696e7c42546bb72f9f531729555d01c529570553aeec70709c3a4f9aacf810d5018f776af48b93eff8e120242105c06a32e64bfc825fde488c99d5845adba2cf349717f64e488852cca73cc5813b7872f7e89d24b4bfafdf75faa368375d5bfdd8b8a7ad641703cbff131616c77e79d8f78c5fe63810781db44fb1fa5cc9387cf0de6807d1a3d5e3d8f9ec7418bbb1d4e10b1fcdb300abd8625b4e24842f1f4c4e567fe9f8c6e9d314757d4568889bccc740fb36f0270804cc11c0044093ab9586ed034cd1eb70bacdedd573750794f0286dfb91c91308e507147ea8e8534c655b931f4e68543e93c57cf2f2159e021739943e40c0dbc8a68193218d40d71e0956b00b4a01fa9c06e67ea55e0213fab48a8dfcf3a047e8c438e7c94fc195026cec82ad532e2aa5970a9fe6c03d9088d0ab45e0b9c7bf9597bd2db93ef7d7f139c291f59e03cda1a5f9a793eb7ec6d50fa9482b712500b5e5a780319769836f7053e3c5a3276a7d65467578a7fbf9079fb5c6bb1b0558acbf3cd896644d42a7b0fd87b12b571b3d8122b1c254750bf9b097d0ec5ed31f9af7db9571f706f5909f0ef2fdcdb255a0795f5c28b70fd1d25b74eb2524ae8f47756875ff439a2b2769adc844312c4ac7bde16b561e62ee3069d25718bf6c2e11ffbb83c863a51c52ff4ead581dd6b1ff0913905163683b97ecbad003a1c71469050eed5ad79e9bb44179b90b8e6b0e6a61a0ed4e919cb96c2615b61cf93905adc3e6e2a127bd661f05e928a45bc1c0599c41450dea0182043b977fcfcf3620f765d3aab13cbe684028dc78a4bd02324427379735934ab4cb821623f49e3af05391c1b7acfe8be33c9201efeded50838ff216d6744d61e8d1d600260c8f7275a46764ac9392132f0b3661e5e92e9daa87b9329d9c89353f40a130bcf8611cce25335f9f1c1208ae1bdc47d96c3f83170a7d27367a043debdfd0e43776d330d1f7a806b32c4363d1dca14715dae4f4d1c99a92673954094e61387080353974097adfde15de4009caa28d42703fdb56fcdac47bd9c5e3bad2fbf90b4a3fab4d89a9933e445ba85f759cc149101f5045a6f3a6d741424318249d96277cea3dc0c4814763d727c72a1867618ac05e5ff103b985cc6f78829bae92794680a51c4b7f7f8b88e39ddd4471890914594f3f03ae668d501732ea77b3eb1fb38b5ad9efdac8775e0995c60a3949e84d2298ea3463aaa16d5ff633da654463e90004915ccc19663c87e006fcd05e904b85b71428d79913e3afdecb7ad51a66f7dcb738d028b62b307025d524320dbe064330da5cbd70467635cf492197c7be3513363b4000bf176827011b2894d33dc9d806b2526a6e91cc1cf0582c5330484b8d48be4855c1859a5b20cab6d08d95b42b57fc709dcb637ba9c6e70b72c473af88ebe8723fe94a0d5ee5d483f19c3b2aade19bafed774b786c0d24383fe0f71c085655f4bd78cb36da83b5429576576c0718b4549efe5b8f602c543c3a8e3d86f19b70d6be1fb39b7cbbac6fcf6d80d69c00ed44dbed1b8555593bd6dcf9ddd519f9325f6faa146d4b631cc6ee418ef9d07a0036fb26a792e7733ec0b58d9f0ebba9ea9493fa026bab62f70381e534c8c3b349be651e9fd5d472b3cbf8f7e912b7030a1992df35e17f4c5aa54f1632464a7c3b0dd133da8d436205bf45d8ded924e35b366803ee52a3d1c85d9f4f976785270dafb63d2cd5052328ed2e5381e9a6e9d8409675c2a9a43c74b07e8a3df8043b2b6d42832cabfcd495b8b30727346990fbc79e436d7ba4d7035603ab98532c5497ef493511e498b1b9c5ff413e919ab6f3cd6acc472f6a39ad0a8c9677ac9a5380a6bebbaaf13a114d097efbf140acad7edecc758bb070fa0b88bb0646d3bed911414a3f10b12bf8372d66f4525f9a8a66d7bf2b5d364119a687e5f416511c27659cf70969863ed7f80e80a4f2e55bf25721e1ab415305b66bfc25b9630a265b553d3e806807f23ec1e2a5f657dbd73a4a36e95e6616faa6aefc5143ca29b0e4bc9eb1042d99c74115d96a2eec5e7fb8c3f598d4df8fa8953e96689651a705dd3f385cd27e0173baca570ce53001cdb002e4476e6af47b9a891f84f7c1c472cce3cd4a70a40c298819f6d75e6adac193798c740c9f5f57fee4df5d140cce8ee4152c17784899003dc000cd2e7c7f23e74da085b254e0843d97d147e44ab3ba12e308925fc6ab0460c7ceb107b0900cef5ff939bc3fe5640f0bb11597c561be275fc8b5b85f5e38a3c12ea26b5b7b32e407685db70d16a3ce51043d4009a647fd3656a54adcbd4d1baa6d89881973fe32faf071123de1712e85db628bdd987566b362845d0c5f818547ec2d1f7c668cae44f0bec74c6663134dd0273c3363f31901903e4e976a447af96f6f521059fb6b892a0599cf7aae457df3aed72f1f55e145332c91430a2f8184bb917d317f8d9c4b6769b9a3a0ac5baea88b39b8f7662ecc16585e7166f61a948f48e6d30c2cfd82820cccdf5e722db2156bd848ea4d13c92544d1d9064414a305215a8271631ffebf08cdf0bcbbbd939f78eafec0d7238bdb90f211d6c44589187d1a501eef7d0b6118e028afcf76ffda95a43e2211206d9d50d34c3e33a6c991952ccd73e722802a14227692f037bba585e73cb9a6cd7556f9ec2158f197a51e3884afb8e59eaa8e7ac3568d88b27b2a5ab8cd72648193ff6068e4d481c58c117e2adda564d5a49f6b992ff6f938acb283e7baf704c71861d60b263f6c6684d7544878b7aca942af8b3a70ae0def309b68fac2aed2b11ba753d7b47f7369805e5b3b9b41d22196e2cc098ece59bdf5231b03fba8adae08fee227a582490b0db34c115620c72afb6fcb507397d1333ea19e7969b729bc2733e6546d2d9f3edb08f9c74201f9ed4e3fcb446cc3fd688b1345e97b32492c9173fa71df2772bd825506ddd6447e9f9e8ece0ffb860e1c755bcf2400deef094219795d4ee84acc34dedc9a3b3adf7fc81733bc511b8edcb54769400940b53471d8e82cb82d9967a97297bdd87f165968ea046291234da176efd20889aa4c07179df83cb500b40bdb96b0c27f2bfa57353268b776740432d29f1761fee77755c7b219def785a42b683e1f70240ec45cdf660e894d4fb541d0511547c9a2c503cf605d72ea7f2abaee4e8adc222a82f4b86c34ad8b25e2932df02f0090d2dbf8817c44659b1245d5579277ad406c538914f90dbaefdd110c5ca0d63a24706cd51096ec19f819c446c9fcb55b777ae633f0257dc4d1b293e6ef68ea7867d852058212a0a9ace9442422a638f73dfb14cc4354b6481ee6591037e7287e962037d963b38a7e4ec12b30e0f6e0ee4d8c30d288e99e22e43b4c795c51d66cc4225c5cab3685b1b3a6fd3a82dfc355634b347cc4f4e55413728fb67fb9f34d3f7e4ecce3254ea843ab361b0f652faa9e54470e3e414c1bb2593e36d88109c36dfab505a16c19152fe021de608c6b3d924c981231ea9cf1cf8c93e53f0df78033e81fdb578a45b7dc4f3f0f68feedc78ec7c347f91a0464bccd58aa2fc11016e88cbaddfb22112edad752792af12fa550be3e6f15d69a6a9d547ab5381b93c58c12753b8085d9e17ed1f2519cc5cb756e3777ea9f8e49a6141460f8f6ced8d12d13d950691479e1207ed35ab71554122beb215a0fb6b34b90784f4be6bd6fbf93daf9d3bc4640bc52a662e750ce361c12c1bfa2ca4e2c784cbf70c406587b2ebd69faa7a891aca63d600247ad7dde426c1ef4e3b22a072ff8eb69c1b1cb30c605112786546c48cf1c4821b5bc0d0bd44ba83b05656b6e19a3d1a76931d983dd39efcc64298e892858e847e99519c1fa25b1998839788c5852b94202d803639d69058604374f76769670a60269dbc0688cea2d9d8672212b93ca501fbf6f7dfefad058e4bd0e0da1cff41b2f408c980f29a49b03efa9e3edef091d7df7529b6b5e8f7d43d103681cd7c38d02a431b15d539e9a3cf44dc71621664e756ad6404ba185b5e20c82760c488fde4253fb52ab850484a082e7ca275f475012be9c8d16d6b4a2c9d863440d5e113d18bbf42f128462764a99ca90af4fde890aee138fe4cbb45658eacd9d38c8a1fb4499c043cc25af87e6a650f38149ab018cc49f50bbd085e2a0ba3eeecde5764f7997748a660593191977792d7176e4c2ff0113d67b9abe8fbc10f364c6fa68e52a455aa56ff15099c6efb6b5812972380d5b8e256b0feb1190835b7d076744c1b5b738c710a07a32676a15d96583e89e39eb4ff08cf02c6e2ad540c2b66299afe01bf2e50c81465a04d229a07c58ffd25a6cd9288110045526b376548d373273e6227d117d491020fd68e366ed697a0d30a5bdff25fa9a5800aa534a3669215dfa8f30960f142a8ae7ffcb654ca60aa7dc8a586670f9db37d05644ff5f934785c5433e605f3fbd0340e168511e209a0aedd8b18f3b948eb58051136d155f53b0e2e027361330e005f83f3a72dcc5d9161dd4b1e6abd16635dc0887dcc833a1fb59c10e0b8bea2536e7acd58d5e11179d13a24dc4292624c527266351b9a48893b956ffe545c8d2c1563805addef2a82134c9c686449d83471f22c1e14601895e854a5f854230e4fb4ed4f9a7ee22e83234be6c5bb19d200c16543468f186ae11cba84ae1aeda5136f7f5b380d02ddb9cbe2c5f5bb39138fa29b2ceb549d2e337eba10171fc237473351cf8e5989c193ef0100c75778ad0c05b64b614067c9a70680c818a566c4ba5e2991eedfe165199a55b0bef1333988f2add167e268db389c2d25bd85eedff9e6851e3df84c9e41128b5a76869c086fcf9275b1d51af02e4a92b66850785319dbf004a29594e32d12ca42da69fac69f886f963409ce1d4514d1ab9e915e071887e7f316b15014d083769afea374e0771f74f632db5ed7d7352546ed686e3ee161cd263dafc2acab74a67a5721f923f9b07c647c2a04f7d1c2f831d4319a60b16ed4c995e35ccbc291ff647a382976ba5a957547b0000").unwrap()).unwrap();
1410
1411        assert_eq!(tx_size(&tx, false), 1333);
1412        assert_eq!(tx_size(&tx, true), 216);
1413    }
1414}