1use bitcoin::consensus::deserialize;
2use bitcoin::hashes::Hash;
3use bitcoin::hex::DisplayHex;
4use bitcoin::key::rand::rngs::OsRng;
5use bitcoin::key::rand::RngCore;
6use bitcoin::secp256k1::{Keypair, Message, Secp256k1};
7use bitcoin::sighash::Prevouts;
8use bitcoin::taproot::{LeafVersion, Signature, TaprootBuilder, TaprootSpendInfo};
9use bitcoin::transaction::Version;
10use bitcoin::{
11 blockdata::script::{Builder, Instruction, ScriptBuf},
12 opcodes::{all::*, OP_0},
13 Address, OutPoint, PublicKey,
14};
15use bitcoin::{sighash::SighashCache, Network, Sequence, Transaction, TxIn, TxOut, Witness};
16use bitcoin::{Amount, Script, TapLeafHash, TapSighashType, Txid, XOnlyPublicKey};
17use elements::pset::serialize::Serialize;
18use secp256k1_musig::{
19 musig::{self},
20 Scalar,
21};
22use std::str::FromStr;
23
24use crate::util::hex_to_bytes32;
25use crate::util::secrets::rng_32b;
26use crate::{error::Error, util::secrets::Preimage};
27
28use bitcoin::{blockdata::locktime::absolute::LockTime, hashes::hash160};
29
30use super::boltz::{
31 BoltzApiClientV2, ChainSwapDetails, Cooperative, CreateReverseResponse,
32 CreateSubmarineResponse, Side, SwapTxKind, SwapType, ToSign,
33};
34use super::wrappers::SwapScriptCommon;
35
36use crate::network::{BitcoinChain, BitcoinClient};
37use crate::util::fees::{create_tx_with_fee, Fee};
38
39pub(crate) fn find_utxo(tx: &Transaction, script_pubkey: &Script) -> Option<(OutPoint, TxOut)> {
40 for (vout, output) in tx.clone().output.into_iter().enumerate() {
41 if output.script_pubkey == *script_pubkey {
42 let outpoint = OutPoint::new(tx.compute_txid(), vout as u32);
43 return Some((outpoint, output));
44 }
45 }
46 None
47}
48
49#[derive(Debug, PartialEq, Clone)]
52pub struct BtcSwapScript {
53 pub swap_type: SwapType,
54 pub side: Option<Side>,
56 pub funding_addrs: Option<Address>, pub hashlock: hash160::Hash,
59 pub receiver_pubkey: PublicKey,
60 pub locktime: LockTime,
61 pub sender_pubkey: PublicKey,
62}
63
64impl BtcSwapScript {
65 pub fn submarine_from_swap_resp(
67 create_swap_response: &CreateSubmarineResponse,
68 our_pubkey: PublicKey,
69 ) -> Result<Self, Error> {
70 let claim_script = ScriptBuf::from_hex(&create_swap_response.swap_tree.claim_leaf.output)?;
71 let refund_script =
72 ScriptBuf::from_hex(&create_swap_response.swap_tree.refund_leaf.output)?;
73
74 let claim_instructions = claim_script.instructions();
75 let refund_instructions = refund_script.instructions();
76
77 let mut last_op = OP_0;
78 let mut hashlock = None;
79 let mut timelock = None;
80
81 for instruction in claim_instructions {
82 match instruction {
83 Ok(Instruction::PushBytes(bytes)) => {
84 if bytes.len() == 20 {
85 hashlock = Some(hash160::Hash::from_slice(bytes.as_bytes())?);
86 } else {
87 continue;
88 }
89 }
90 _ => continue,
91 }
92 }
93
94 for instruction in refund_instructions {
95 match instruction {
96 Ok(Instruction::Op(opcode)) => last_op = opcode,
97 Ok(Instruction::PushBytes(bytes)) => {
98 if last_op == OP_CHECKSIGVERIFY {
99 timelock = Some(LockTime::from_consensus(bytes_to_u32_little_endian(
100 bytes.as_bytes(),
101 )));
102 } else {
103 continue;
104 }
105 }
106 _ => continue,
107 }
108 }
109
110 let hashlock =
111 hashlock.ok_or_else(|| Error::Protocol("No hashlock provided".to_string()))?;
112
113 let timelock =
114 timelock.ok_or_else(|| Error::Protocol("No timelock provided".to_string()))?;
115
116 let funding_addrs = Address::from_str(&create_swap_response.address)?.assume_checked();
117
118 Ok(BtcSwapScript {
119 swap_type: SwapType::Submarine,
120 side: None,
122 funding_addrs: Some(funding_addrs),
123 hashlock,
124 receiver_pubkey: create_swap_response.claim_public_key,
125 locktime: timelock,
126 sender_pubkey: our_pubkey,
127 })
128 }
129
130 pub fn musig_keyagg_cache(&self) -> musig::KeyAggCache {
131 match (self.swap_type, self.side.clone()) {
132 (SwapType::ReverseSubmarine, _) | (SwapType::Chain, Some(Side::Claim)) => {
133 let pubkeys = [self.sender_pubkey.inner, self.receiver_pubkey.inner];
134 let [a, b] = convert_pubkeys_for_musig(&pubkeys);
135 musig::KeyAggCache::new(&[&a, &b])
136 }
137
138 (SwapType::Submarine, _) | (SwapType::Chain, _) => {
139 let pubkeys = [self.receiver_pubkey.inner, self.sender_pubkey.inner];
140 let [a, b] = convert_pubkeys_for_musig(&pubkeys);
141 musig::KeyAggCache::new(&[&a, &b])
142 }
143 }
144 }
145
146 pub fn reverse_from_swap_resp(
148 reverse_response: &CreateReverseResponse,
149 our_pubkey: PublicKey,
150 ) -> Result<Self, Error> {
151 let claim_script = ScriptBuf::from_hex(&reverse_response.swap_tree.claim_leaf.output)?;
152 let refund_script = ScriptBuf::from_hex(&reverse_response.swap_tree.refund_leaf.output)?;
153
154 let claim_instructions = claim_script.instructions();
155 let refund_instructions = refund_script.instructions();
156
157 let mut last_op = OP_0;
158 let mut hashlock = None;
159 let mut timelock = None;
160
161 for instruction in claim_instructions {
162 match instruction {
163 Ok(Instruction::PushBytes(bytes)) => {
164 if bytes.len() == 20 {
165 hashlock = Some(hash160::Hash::from_slice(bytes.as_bytes())?);
166 } else {
167 continue;
168 }
169 }
170 _ => continue,
171 }
172 }
173
174 for instruction in refund_instructions {
175 match instruction {
176 Ok(Instruction::Op(opcode)) => last_op = opcode,
177 Ok(Instruction::PushBytes(bytes)) => {
178 if last_op == OP_CHECKSIGVERIFY {
179 timelock = Some(LockTime::from_consensus(bytes_to_u32_little_endian(
180 bytes.as_bytes(),
181 )));
182 } else {
183 continue;
184 }
185 }
186 _ => continue,
187 }
188 }
189
190 let hashlock =
191 hashlock.ok_or_else(|| Error::Protocol("No hashlock provided".to_string()))?;
192
193 let timelock =
194 timelock.ok_or_else(|| Error::Protocol("No timelock provided".to_string()))?;
195
196 let funding_addrs = Address::from_str(&reverse_response.lockup_address)?.assume_checked();
197
198 Ok(BtcSwapScript {
199 swap_type: SwapType::ReverseSubmarine,
200 side: None,
202 funding_addrs: Some(funding_addrs),
203 hashlock,
204 receiver_pubkey: our_pubkey,
205 locktime: timelock,
206 sender_pubkey: reverse_response.refund_public_key,
207 })
208 }
209
210 pub fn chain_from_swap_resp(
212 side: Side,
213 chain_swap_details: ChainSwapDetails,
214 our_pubkey: PublicKey,
215 ) -> Result<Self, Error> {
216 let claim_script = ScriptBuf::from_hex(&chain_swap_details.swap_tree.claim_leaf.output)?;
217 let refund_script = ScriptBuf::from_hex(&chain_swap_details.swap_tree.refund_leaf.output)?;
218
219 let claim_instructions = claim_script.instructions();
220 let refund_instructions = refund_script.instructions();
221
222 let mut last_op = OP_0;
223 let mut hashlock = None;
224 let mut timelock = None;
225
226 for instruction in claim_instructions {
227 match instruction {
228 Ok(Instruction::PushBytes(bytes)) => {
229 if bytes.len() == 20 {
230 hashlock = Some(hash160::Hash::from_slice(bytes.as_bytes())?);
231 } else {
232 continue;
233 }
234 }
235 _ => continue,
236 }
237 }
238
239 for instruction in refund_instructions {
240 match instruction {
241 Ok(Instruction::Op(opcode)) => last_op = opcode,
242 Ok(Instruction::PushBytes(bytes)) => {
243 if last_op == OP_CHECKSIGVERIFY {
244 timelock = Some(LockTime::from_consensus(bytes_to_u32_little_endian(
245 bytes.as_bytes(),
246 )));
247 } else {
248 continue;
249 }
250 }
251 _ => continue,
252 }
253 }
254
255 let hashlock =
256 hashlock.ok_or_else(|| Error::Protocol("No hashlock provided".to_string()))?;
257
258 let timelock =
259 timelock.ok_or_else(|| Error::Protocol("No timelock provided".to_string()))?;
260
261 let funding_addrs = Address::from_str(&chain_swap_details.lockup_address)?.assume_checked();
262
263 let (sender_pubkey, receiver_pubkey) = match side {
264 Side::Lockup => (our_pubkey, chain_swap_details.server_public_key),
265 Side::Claim => (chain_swap_details.server_public_key, our_pubkey),
266 };
267
268 Ok(BtcSwapScript {
269 swap_type: SwapType::Chain,
270 side: Some(side),
272 funding_addrs: Some(funding_addrs),
273 hashlock,
274 receiver_pubkey,
275 locktime: timelock,
276 sender_pubkey,
277 })
278 }
279
280 fn claim_script(&self) -> ScriptBuf {
281 match self.swap_type {
282 SwapType::Submarine => Builder::new()
283 .push_opcode(OP_HASH160)
284 .push_slice(self.hashlock.to_byte_array())
285 .push_opcode(OP_EQUALVERIFY)
286 .push_x_only_key(&self.receiver_pubkey.inner.x_only_public_key().0)
287 .push_opcode(OP_CHECKSIG)
288 .into_script(),
289
290 SwapType::ReverseSubmarine | SwapType::Chain => Builder::new()
291 .push_opcode(OP_SIZE)
292 .push_int(32)
293 .push_opcode(OP_EQUALVERIFY)
294 .push_opcode(OP_HASH160)
295 .push_slice(self.hashlock.to_byte_array())
296 .push_opcode(OP_EQUALVERIFY)
297 .push_x_only_key(&self.receiver_pubkey.inner.x_only_public_key().0)
298 .push_opcode(OP_CHECKSIG)
299 .into_script(),
300 }
301 }
302
303 fn refund_script(&self) -> ScriptBuf {
304 Builder::new()
306 .push_x_only_key(&self.sender_pubkey.inner.x_only_public_key().0)
307 .push_opcode(OP_CHECKSIGVERIFY)
308 .push_lock_time(self.locktime)
309 .push_opcode(OP_CLTV)
310 .into_script()
311 }
312
313 fn taproot_spendinfo(&self) -> Result<TaprootSpendInfo, Error> {
315 let secp = Secp256k1::new();
316
317 let key_agg_cache = self.musig_keyagg_cache();
321
322 let internal_key = key_agg_cache.agg_pk();
324
325 let taproot_builder = TaprootBuilder::new();
326
327 let taproot_builder =
328 taproot_builder.add_leaf_with_ver(1, self.claim_script(), LeafVersion::TapScript)?;
329 let taproot_builder =
330 taproot_builder.add_leaf_with_ver(1, self.refund_script(), LeafVersion::TapScript)?;
331
332 let taproot_spend_info =
333 match taproot_builder.finalize(&secp, convert_xonly_key(internal_key)) {
334 Ok(r) => r,
335 Err(e) => {
336 return Err(Error::Taproot(format!(
337 "Could not finalize taproot constructions: {e:?}"
338 )))
339 }
340 };
341
342 if let Some(funding_address) = &self.funding_addrs {
345 let claim_key = taproot_spend_info.output_key();
346
347 let lockup_spk = funding_address.script_pubkey();
348
349 let pubkey_instruction = lockup_spk
350 .instructions()
351 .last()
352 .ok_or(Error::Protocol(
353 "Script should contain at least one instruction".to_string(),
354 ))?
355 .map_err(|_| Error::Protocol("Failed to parse script instruction".to_string()))?;
356
357 let lockup_xonly_pubkey_bytes = pubkey_instruction.push_bytes().ok_or(
358 Error::Protocol("Expected push bytes instruction for pubkey".to_string()),
359 )?;
360
361 let lockup_xonly_pubkey =
362 XOnlyPublicKey::from_slice(lockup_xonly_pubkey_bytes.as_bytes())?;
363
364 if lockup_xonly_pubkey != claim_key.to_x_only_public_key() {
365 return Err(Error::Protocol(format!(
366 "Taproot construction Failed. Lockup Pubkey: {lockup_xonly_pubkey}, Claim Pubkey {claim_key}"
367 )));
368 }
369
370 log::info!("Taproot creation and verification success!");
371 }
372
373 Ok(taproot_spend_info)
374 }
375
376 pub fn to_address(&self, network: BitcoinChain) -> Result<Address, Error> {
378 let spend_info = self.taproot_spendinfo()?;
379 let output_key = spend_info.output_key();
380
381 let network = match network {
382 BitcoinChain::Bitcoin => Network::Bitcoin,
383 BitcoinChain::BitcoinRegtest => Network::Regtest,
384 BitcoinChain::BitcoinTestnet => Network::Testnet,
385 };
386
387 Ok(Address::p2tr_tweaked(output_key, network))
388 }
389
390 pub fn validate_address(&self, chain: BitcoinChain, address: String) -> Result<(), Error> {
391 let to_address = self.to_address(chain)?;
392 if to_address.to_string() == address {
393 Ok(())
394 } else {
395 Err(Error::Protocol("Script/LockupAddress Mismatch".to_string()))
396 }
397 }
398
399 pub async fn get_balance<BC: BitcoinClient + ?Sized>(
401 &self,
402 bitcoin_client: &BC,
403 ) -> Result<(u64, i64), Error> {
404 bitcoin_client
405 .get_address_balance(&self.to_address(bitcoin_client.network())?)
406 .await
407 }
408
409 pub async fn fetch_utxos<BC: BitcoinClient + ?Sized>(
411 &self,
412 bitcoin_client: &BC,
413 ) -> Result<Vec<(OutPoint, TxOut)>, Error> {
414 bitcoin_client
415 .get_address_utxos(&self.to_address(bitcoin_client.network())?)
416 .await
417 }
418
419 pub(crate) async fn fetch_swap_utxo<BC: BitcoinClient + ?Sized>(
420 &self,
421 lockup_tx: Option<&Transaction>,
422 bitcoin_client: &BC,
423 boltz_client: &BoltzApiClientV2,
424 swap_id: &str,
425 tx_kind: SwapTxKind,
426 ) -> Result<(OutPoint, TxOut), Error> {
427 let outpoint = match lockup_tx {
428 Some(tx) => self.find_utxo(tx, bitcoin_client.network()),
429 None => match self.fetch_utxos(bitcoin_client).await {
430 Ok(v) => Ok(v.first().cloned()),
431 Err(_) => {
432 self.fetch_lockup_utxo_boltz(
433 bitcoin_client.network(),
434 boltz_client,
435 swap_id,
436 tx_kind,
437 )
438 .await
439 }
440 },
441 }?;
442
443 outpoint.ok_or(Error::Protocol(
444 "No Bitcoin UTXO detected for this script".to_string(),
445 ))
446 }
447
448 pub(crate) fn find_utxo(
449 &self,
450 tx: &Transaction,
451 network: BitcoinChain,
452 ) -> Result<Option<(OutPoint, TxOut)>, Error> {
453 let address = self.to_address(network)?;
454 Ok(find_utxo(tx, &address.script_pubkey()))
455 }
456
457 pub async fn fetch_lockup_utxo_boltz(
459 &self,
460 network: BitcoinChain,
461 boltz_client: &BoltzApiClientV2,
462 swap_id: &str,
463 tx_kind: SwapTxKind,
464 ) -> Result<Option<(OutPoint, TxOut)>, Error> {
465 let hex = match self.swap_type {
466 SwapType::Chain => match tx_kind {
467 SwapTxKind::Claim => {
468 let chain_txs = boltz_client.get_chain_txs(swap_id).await?;
469 chain_txs
470 .server_lock
471 .ok_or(Error::Protocol(
472 "No server_lock transaction for Chain Swap available".to_string(),
473 ))?
474 .transaction
475 .hex
476 }
477 SwapTxKind::Refund => {
478 let chain_txs = boltz_client.get_chain_txs(swap_id).await?;
479 chain_txs
480 .user_lock
481 .ok_or(Error::Protocol(
482 "No user_lock transaction for Chain Swap available".to_string(),
483 ))?
484 .transaction
485 .hex
486 }
487 },
488 SwapType::ReverseSubmarine => boltz_client.get_reverse_tx(swap_id).await?.hex,
489 SwapType::Submarine => boltz_client.get_submarine_tx(swap_id).await?.hex,
490 };
491 if hex.is_none() {
492 return Err(Error::Hex(
493 "No transaction hex found in boltz response".to_string(),
494 ));
495 }
496 let tx: Transaction = deserialize(&hex::decode(hex.unwrap())?)?;
497 self.find_utxo(&tx, network)
498 }
499}
500
501pub fn bytes_to_u32_little_endian(bytes: &[u8]) -> u32 {
502 let mut result = 0u32;
503 for (i, &byte) in bytes.iter().enumerate() {
504 result |= (byte as u32) << (8 * i);
505 }
506 result
507}
508
509#[derive(Debug, Clone)]
512pub struct BtcSwapTx {
513 pub kind: SwapTxKind, pub swap_script: BtcSwapScript,
515 pub output_address: Address,
516 pub utxos: Vec<(OutPoint, TxOut)>,
520}
521
522impl BtcSwapTx {
523 pub async fn new_claim<BC: BitcoinClient + ?Sized>(
526 swap_script: BtcSwapScript,
527 claim_address: String,
528 bitcoin_client: &BC,
529 boltz_client: &BoltzApiClientV2,
530 swap_id: String,
531 ) -> Result<BtcSwapTx, Error> {
532 let utxo = swap_script
533 .fetch_swap_utxo(
534 None,
535 bitcoin_client,
536 boltz_client,
537 &swap_id,
538 SwapTxKind::Claim,
539 )
540 .await?;
541 Self::new_claim_with_utxo(swap_script, claim_address, bitcoin_client, utxo)
542 }
543
544 pub(crate) fn new_claim_with_utxo<BC: BitcoinClient + ?Sized>(
545 swap_script: BtcSwapScript,
546 claim_address: String,
547 bitcoin_client: &BC,
548 utxo: (OutPoint, TxOut),
549 ) -> Result<BtcSwapTx, Error> {
550 if swap_script.swap_type == SwapType::Submarine {
551 return Err(Error::Protocol(
552 "Claim transactions cannot be constructed for Submarine swaps.".to_string(),
553 ));
554 }
555
556 let address = Address::from_str(&claim_address)?;
557
558 address.is_valid_for_network(bitcoin_client.network().into());
559
560 Ok(BtcSwapTx {
561 kind: SwapTxKind::Claim,
562 swap_script,
563 output_address: address.assume_checked(),
564 utxos: vec![utxo], })
566 }
567
568 pub async fn new_refund<BC: BitcoinClient + ?Sized>(
571 swap_script: BtcSwapScript,
572 refund_address: &str,
573 bitcoin_client: &BC,
574 boltz_client: &BoltzApiClientV2,
575 swap_id: String,
576 ) -> Result<BtcSwapTx, Error> {
577 if swap_script.swap_type == SwapType::ReverseSubmarine {
578 return Err(Error::Protocol(
579 "Refund Txs cannot be constructed for Reverse Submarine Swaps.".to_string(),
580 ));
581 }
582
583 let address = Address::from_str(refund_address)?;
584 if !address.is_valid_for_network(bitcoin_client.network().into()) {
585 return Err(Error::Address("Address validation failed".to_string()));
586 };
587
588 let utxos = match swap_script.fetch_utxos(bitcoin_client).await {
589 Ok(r) => r,
590 Err(_) => {
591 let lockup_utxo_info = swap_script
592 .fetch_lockup_utxo_boltz(
593 bitcoin_client.network(),
594 boltz_client,
595 &swap_id,
596 SwapTxKind::Refund,
597 )
598 .await?;
599
600 match lockup_utxo_info {
601 Some(r) => vec![r],
602 None => vec![],
603 }
604 }
605 };
606
607 match utxos.is_empty() {
608 true => Err(Error::Protocol(
609 "No Bitcoin UTXO detected for this script".to_string(),
610 )),
611 false => Ok(BtcSwapTx {
612 kind: SwapTxKind::Refund,
613 swap_script,
614 output_address: address.assume_checked(),
615 utxos,
616 }),
617 }
618 }
619
620 pub fn partial_sign(
623 &self,
624 keys: &Keypair,
625 pub_nonce: &str,
626 transaction_hash: &str,
627 ) -> Result<(musig::PartialSignature, musig::PublicNonce), Error> {
628 self.swap_script
629 .partial_sign(keys, pub_nonce, transaction_hash)
630 }
631
632 pub async fn sign_claim(
637 &self,
638 keys: &Keypair,
639 preimage: &Preimage,
640 fee: Fee,
641 is_cooperative: Option<Cooperative<'_>>,
642 ) -> Result<Transaction, Error> {
643 if self.swap_script.swap_type == SwapType::Submarine {
644 return Err(Error::Protocol(
645 "Claim Tx signing is not applicable for Submarine Swaps".to_string(),
646 ));
647 }
648
649 if self.kind == SwapTxKind::Refund {
650 return Err(Error::Protocol(
651 "Cannot sign claim with refund-type BtcSwapTx".to_string(),
652 ));
653 }
654
655 if self.utxos.is_empty() {
656 return Err(Error::Protocol(
657 "No Bitcoin UTXO available for claim transaction".to_string(),
658 ));
659 }
660
661 let mut claim_tx = create_tx_with_fee(
662 fee,
663 |fee| self.create_claim(keys, preimage, fee, is_cooperative.is_some()),
664 |tx| tx.vsize(),
665 )?;
666
667 if let Some(Cooperative {
669 boltz_api,
670 swap_id,
671 signature,
672 }) = is_cooperative
673 {
674 let secp = Secp256k1::new();
675
676 let claim_tx_taproot_hash = SighashCache::new(claim_tx.clone())
679 .taproot_key_spend_signature_hash(
680 0,
681 &Prevouts::All(&[&self.utxos.first().unwrap().1]),
682 bitcoin::TapSighashType::Default,
683 )?;
684
685 let msg = *claim_tx_taproot_hash.as_byte_array();
686
687 let mut key_agg_cache = self.swap_script.musig_keyagg_cache();
689
690 let tweak = Scalar::from_be_bytes(
691 *self
692 .swap_script
693 .taproot_spendinfo()?
694 .tap_tweak()
695 .as_byte_array(),
696 )?;
697
698 let _ = key_agg_cache.pubkey_xonly_tweak_add(&tweak)?;
699
700 let session_secret_rand =
701 musig::SessionSecretRand::assume_unique_per_nonce_gen(rng_32b());
702
703 let mut extra_rand = [0u8; 32];
704 OsRng.fill_bytes(&mut extra_rand);
705
706 let (claim_sec_nonce, claim_pub_nonce) = key_agg_cache.nonce_gen(
707 session_secret_rand,
708 convert_public_key(keys.public_key()),
709 &msg,
710 Some(extra_rand),
711 );
712
713 let claim_tx_hex = claim_tx.serialize().to_lower_hex_string();
715 let partial_sig_resp = match self.swap_script.swap_type {
716 SwapType::Chain => {
717 boltz_api
718 .post_chain_claim_tx_details(
719 &swap_id,
720 preimage,
721 signature,
722 ToSign {
723 pub_nonce: claim_pub_nonce.serialize().to_lower_hex_string(),
724 transaction: claim_tx_hex,
725 index: 0,
726 },
727 )
728 .await
729 }
730 SwapType::ReverseSubmarine => {
731 boltz_api
732 .get_reverse_partial_sig(
733 &swap_id,
734 preimage,
735 &claim_pub_nonce,
736 &claim_tx_hex,
737 )
738 .await
739 }
740 _ => Err(Error::Protocol(format!(
741 "Cannot get partial sig for {:?} Swap",
742 self.swap_script.swap_type
743 ))),
744 }?;
745
746 let boltz_public_nonce = musig::PublicNonce::from_str(&partial_sig_resp.pub_nonce)?;
747
748 let boltz_partial_sig =
749 musig::PartialSignature::from_str(&partial_sig_resp.partial_signature)?;
750
751 let agg_nonce = musig::AggregatedNonce::new(&[&boltz_public_nonce, &claim_pub_nonce]);
753
754 let musig_session = musig::Session::new(&key_agg_cache, agg_nonce, &msg);
755
756 let boltz_partial_sig_verify = musig_session.partial_verify(
758 &key_agg_cache,
759 &boltz_partial_sig,
760 &boltz_public_nonce,
761 convert_public_key(self.swap_script.sender_pubkey.inner),
762 );
763
764 if !boltz_partial_sig_verify {
765 return Err(Error::Protocol(
766 "Invalid partial-sig received from Boltz".to_string(),
767 ));
768 }
769
770 let our_partial_sig =
771 musig_session.partial_sign(claim_sec_nonce, &convert_keypair(keys), &key_agg_cache);
772
773 let schnorr_sig = musig_session
774 .partial_sig_agg(&[&boltz_partial_sig, &our_partial_sig])
775 .assume_valid();
776
777 let final_schnorr_sig = Signature {
778 signature: convert_schnorr_signature(schnorr_sig),
779 sighash_type: TapSighashType::Default,
780 };
781
782 let output_key = self.swap_script.taproot_spendinfo()?.output_key();
783
784 secp.verify_schnorr(
785 &final_schnorr_sig.signature,
786 &bitcoin::secp256k1::Message::from_digest_slice(&msg)?,
787 &output_key.to_x_only_public_key(),
788 )?;
789
790 let mut witness = Witness::new();
791 witness.push(final_schnorr_sig.to_vec());
792
793 claim_tx.input[0].witness = witness;
794 }
795
796 Ok(claim_tx)
797 }
798
799 fn create_claim(
800 &self,
801 keys: &Keypair,
802 preimage: &Preimage,
803 absolute_fees: u64,
804 is_cooperative: bool,
805 ) -> Result<Transaction, Error> {
806 if preimage.bytes.is_none() {
807 return Err(Error::Protocol(
808 "No preimage provided while signing.".to_string(),
809 ));
810 };
811
812 let utxo = self.utxos.first().ok_or(Error::Protocol(
814 "No Bitcoin UTXO detected for this script".to_string(),
815 ))?;
816
817 let txin = TxIn {
818 previous_output: utxo.0,
819 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
820 script_sig: ScriptBuf::new(),
821 witness: Witness::new(),
822 };
823
824 let destination_spk = self.output_address.script_pubkey();
825
826 let output_value = utxo
827 .1
828 .value
829 .checked_sub(Amount::from_sat(absolute_fees))
830 .ok_or(Error::Protocol(format!(
831 "Claim output value {} is less than fees {}",
832 utxo.1.value, absolute_fees
833 )))?;
834
835 let txout = TxOut {
836 script_pubkey: destination_spk,
837 value: output_value,
838 };
839
840 let mut claim_tx = Transaction {
841 version: Version::TWO,
842 lock_time: LockTime::ZERO,
843 input: vec![txin],
844 output: vec![txout],
845 };
846
847 if is_cooperative {
848 claim_tx.input[0].witness = Self::stubbed_cooperative_witness();
849 } else {
850 let secp = Secp256k1::new();
851
852 claim_tx.input[0].sequence = Sequence::ZERO;
854
855 let leaf_hash =
856 TapLeafHash::from_script(&self.swap_script.claim_script(), LeafVersion::TapScript);
857
858 let sighash = SighashCache::new(claim_tx.clone()).taproot_script_spend_signature_hash(
859 0,
860 &Prevouts::All(&[&utxo.1]),
861 leaf_hash,
862 TapSighashType::Default,
863 )?;
864
865 let msg = Message::from_digest_slice(sighash.as_byte_array())?;
866
867 let signature = secp.sign_schnorr(&msg, keys);
868
869 let final_sig = Signature {
870 signature,
871 sighash_type: TapSighashType::Default,
872 };
873
874 let control_block = self
875 .swap_script
876 .taproot_spendinfo()?
877 .control_block(&(self.swap_script.claim_script(), LeafVersion::TapScript))
878 .ok_or(Error::Taproot(
879 "Control block calculation failed".to_string(),
880 ))?;
881
882 let mut witness = Witness::new();
883
884 witness.push(final_sig.to_vec());
885 witness.push(preimage.bytes.ok_or(Error::Protocol(
886 "Preimage bytes not available - cannot claim without actual preimage".to_string(),
887 ))?);
888 witness.push(self.swap_script.claim_script().as_bytes());
889 witness.push(control_block.serialize());
890
891 claim_tx.input[0].witness = witness;
892 }
893
894 Ok(claim_tx)
895 }
896
897 pub async fn sign_refund(
900 &self,
901 keys: &Keypair,
902 fee: Fee,
903 is_cooperative: Option<Cooperative<'_>>,
904 ) -> Result<Transaction, Error> {
905 if self.swap_script.swap_type == SwapType::ReverseSubmarine {
906 return Err(Error::Protocol(
907 "Refund Tx signing is not applicable for Reverse Submarine Swaps".to_string(),
908 ));
909 }
910
911 if self.kind == SwapTxKind::Claim {
912 return Err(Error::Protocol(
913 "Cannot sign refund with a claim-type BtcSwapTx".to_string(),
914 ));
915 }
916
917 let mut refund_tx = create_tx_with_fee(
918 fee,
919 |fee| self.create_refund(keys, fee, is_cooperative.is_some()),
920 |tx| tx.vsize(),
921 )?;
922
923 if let Some(Cooperative {
924 boltz_api, swap_id, ..
925 }) = is_cooperative
926 {
927 let secp = Secp256k1::new();
928
929 refund_tx.lock_time = LockTime::ZERO; for input_index in 0..refund_tx.input.len() {
933 let tx_outs: Vec<&TxOut> = self.utxos.iter().map(|(_, out)| out).collect();
935 let refund_tx_taproot_hash = SighashCache::new(refund_tx.clone())
936 .taproot_key_spend_signature_hash(
937 input_index,
938 &Prevouts::All(&tx_outs),
939 bitcoin::TapSighashType::Default,
940 )?;
941
942 let msg = *refund_tx_taproot_hash.as_byte_array();
943
944 let mut key_agg_cache = self.swap_script.musig_keyagg_cache();
946
947 let tweak = Scalar::from_be_bytes(
948 *self
949 .swap_script
950 .taproot_spendinfo()?
951 .tap_tweak()
952 .as_byte_array(),
953 )?;
954
955 let _ = key_agg_cache.pubkey_xonly_tweak_add(&tweak)?;
956
957 let session_secret_rand =
958 musig::SessionSecretRand::assume_unique_per_nonce_gen(rng_32b());
959
960 let mut extra_rand = [0u8; 32];
961 OsRng.fill_bytes(&mut extra_rand);
962
963 let (sec_nonce, pub_nonce) = key_agg_cache.nonce_gen(
964 session_secret_rand,
965 convert_public_key(keys.public_key()),
966 &msg,
967 Some(extra_rand),
968 );
969
970 let refund_tx_hex = refund_tx.serialize().to_lower_hex_string();
972 let partial_sig_resp = match self.swap_script.swap_type {
973 SwapType::Chain => {
974 boltz_api
975 .get_chain_partial_sig(
976 &swap_id,
977 input_index,
978 &pub_nonce,
979 &refund_tx_hex,
980 )
981 .await
982 }
983 SwapType::Submarine => {
984 boltz_api
985 .get_submarine_partial_sig(
986 &swap_id,
987 input_index,
988 &pub_nonce,
989 &refund_tx_hex,
990 )
991 .await
992 }
993 _ => Err(Error::Protocol(format!(
994 "Cannot get partial sig for {:?} Swap",
995 self.swap_script.swap_type
996 ))),
997 }?;
998
999 let boltz_public_nonce = musig::PublicNonce::from_str(&partial_sig_resp.pub_nonce)?;
1000
1001 let boltz_partial_sig =
1002 musig::PartialSignature::from_str(&partial_sig_resp.partial_signature)?;
1003
1004 let agg_nonce = musig::AggregatedNonce::new(&[&boltz_public_nonce, &pub_nonce]);
1006
1007 let musig_session = musig::Session::new(&key_agg_cache, agg_nonce, &msg);
1008
1009 let boltz_partial_sig_verify = musig_session.partial_verify(
1011 &key_agg_cache,
1012 &boltz_partial_sig,
1013 &boltz_public_nonce,
1014 convert_public_key(self.swap_script.receiver_pubkey.inner), );
1016
1017 if !boltz_partial_sig_verify {
1018 return Err(Error::Protocol(
1019 "Invalid partial-sig received from Boltz".to_string(),
1020 ));
1021 }
1022
1023 let our_partial_sig =
1024 musig_session.partial_sign(sec_nonce, &convert_keypair(keys), &key_agg_cache);
1025
1026 let schnorr_sig = musig_session
1027 .partial_sig_agg(&[&boltz_partial_sig, &our_partial_sig])
1028 .assume_valid();
1029
1030 let final_schnorr_sig = Signature {
1031 signature: convert_schnorr_signature(schnorr_sig),
1032 sighash_type: TapSighashType::Default,
1033 };
1034
1035 let output_key = self.swap_script.taproot_spendinfo()?.output_key();
1036
1037 secp.verify_schnorr(
1038 &final_schnorr_sig.signature,
1039 &bitcoin::secp256k1::Message::from_digest_slice(&msg)?,
1040 &output_key.to_x_only_public_key(),
1041 )?;
1042
1043 let mut witness = Witness::new();
1044 witness.push(final_schnorr_sig.to_vec());
1045 refund_tx.input[input_index].witness = witness;
1046 }
1047 }
1048
1049 Ok(refund_tx)
1050 }
1051
1052 fn create_refund(
1053 &self,
1054 keys: &Keypair,
1055 absolute_fees: u64,
1056 is_cooperative: bool,
1057 ) -> Result<Transaction, Error> {
1058 let utxos_amount = self
1059 .utxos
1060 .iter()
1061 .fold(Amount::ZERO, |acc, (_, txo)| acc + txo.value);
1062 let absolute_fees_amount = Amount::from_sat(absolute_fees);
1063 let output_amount =
1064 utxos_amount
1065 .checked_sub(absolute_fees_amount)
1066 .ok_or(Error::Protocol(format!(
1067 "Refund output value {utxos_amount} is less than fees {absolute_fees_amount}"
1068 )))?;
1069 let output: TxOut = TxOut {
1070 script_pubkey: self.output_address.script_pubkey(),
1071 value: output_amount,
1072 };
1073
1074 let unsigned_inputs = self
1075 .utxos
1076 .iter()
1077 .map(|(outpoint, _txo)| TxIn {
1078 previous_output: *outpoint,
1079 script_sig: ScriptBuf::new(),
1080 sequence: Sequence::MAX,
1081 witness: Witness::new(),
1082 })
1083 .collect();
1084
1085 let lock_time = match self
1086 .swap_script
1087 .refund_script()
1088 .instructions()
1089 .filter_map(|i| {
1090 let ins = i.ok()?;
1091 if let Instruction::PushBytes(bytes) = ins {
1092 if bytes.len() < 5_usize {
1093 Some(LockTime::from_consensus(bytes_to_u32_little_endian(
1094 bytes.as_bytes(),
1095 )))
1096 } else {
1097 None
1098 }
1099 } else {
1100 None
1101 }
1102 })
1103 .next()
1104 {
1105 Some(r) => r,
1106 None => {
1107 return Err(Error::Protocol(
1108 "Error getting timelock from refund script".to_string(),
1109 ))
1110 }
1111 };
1112
1113 let mut refund_tx = Transaction {
1114 version: Version::TWO,
1115 lock_time,
1116 input: unsigned_inputs,
1117 output: vec![output],
1118 };
1119
1120 let tx_outs: Vec<&TxOut> = self.utxos.iter().map(|(_, out)| out).collect();
1121
1122 if is_cooperative {
1123 for index in 0..refund_tx.input.len() {
1124 refund_tx.input[index].witness = Self::stubbed_cooperative_witness();
1125 }
1126 } else {
1127 let leaf_hash =
1128 TapLeafHash::from_script(&self.swap_script.refund_script(), LeafVersion::TapScript);
1129
1130 let control_block = self
1131 .swap_script
1132 .taproot_spendinfo()?
1133 .control_block(&(
1134 self.swap_script.refund_script().clone(),
1135 LeafVersion::TapScript,
1136 ))
1137 .ok_or(Error::Protocol(
1138 "Control block calculation failed".to_string(),
1139 ))?;
1140
1141 for input_index in 0..refund_tx.input.len() {
1143 refund_tx.input[input_index].sequence = Sequence::ZERO;
1144 }
1145
1146 for input_index in 0..refund_tx.input.len() {
1147 let sighash = SighashCache::new(refund_tx.clone())
1148 .taproot_script_spend_signature_hash(
1149 input_index,
1150 &Prevouts::All(&tx_outs),
1151 leaf_hash,
1152 TapSighashType::Default,
1153 )?;
1154
1155 let msg = Message::from_digest_slice(sighash.as_byte_array())?;
1156
1157 let signature = Secp256k1::new().sign_schnorr(&msg, keys);
1158
1159 let final_sig = Signature {
1160 signature,
1161 sighash_type: TapSighashType::Default,
1162 };
1163
1164 let mut witness = Witness::new();
1165 witness.push(final_sig.to_vec());
1166 witness.push(self.swap_script.refund_script().as_bytes());
1167 witness.push(control_block.serialize());
1168 refund_tx.input[input_index].witness = witness;
1169 }
1170 }
1171
1172 Ok(refund_tx)
1173 }
1174
1175 fn stubbed_cooperative_witness() -> Witness {
1176 let mut witness = Witness::new();
1177 witness.push([0; 64]);
1180 witness
1181 }
1182
1183 pub fn size(&self, keys: &Keypair, is_cooperative: bool) -> Result<usize, Error> {
1187 let dummy_abs_fee = 1;
1188 let tx = match self.kind {
1189 SwapTxKind::Claim => {
1190 let preimage = Preimage::from_vec([0; 32].to_vec())?;
1191 self.create_claim(keys, &preimage, dummy_abs_fee, is_cooperative)?
1192 }
1193 SwapTxKind::Refund => self.create_refund(keys, dummy_abs_fee, is_cooperative)?,
1194 };
1195 Ok(tx.vsize())
1196 }
1197
1198 pub async fn broadcast<BC: BitcoinClient + ?Sized>(
1200 &self,
1201 signed_tx: &Transaction,
1202 bitcoin_client: &BC,
1203 ) -> Result<Txid, Error> {
1204 bitcoin_client.broadcast_tx(signed_tx).await
1205 }
1206}
1207
1208impl SwapScriptCommon for BtcSwapScript {
1209 fn swap_type(&self) -> SwapType {
1210 self.swap_type
1211 }
1212
1213 fn partial_sign(
1216 &self,
1217 keys: &Keypair,
1218 pub_nonce: &str,
1219 transaction_hash: &str,
1220 ) -> Result<(musig::PartialSignature, musig::PublicNonce), Error> {
1221 let mut key_agg_cache = self.musig_keyagg_cache();
1224
1225 let tweak = Scalar::from_be_bytes(*self.taproot_spendinfo()?.tap_tweak().as_byte_array())?;
1226
1227 let _ = key_agg_cache.pubkey_xonly_tweak_add(&tweak)?;
1228
1229 let session_secret_rand = musig::SessionSecretRand::assume_unique_per_nonce_gen(rng_32b());
1230
1231 let msg = hex_to_bytes32(transaction_hash)?;
1232
1233 let mut extra_rand = [0u8; 32];
1235 OsRng.fill_bytes(&mut extra_rand);
1236
1237 let (gen_sec_nonce, gen_pub_nonce) = key_agg_cache.nonce_gen(
1238 session_secret_rand,
1239 convert_public_key(keys.public_key()),
1240 &msg,
1241 Some(extra_rand),
1242 );
1243
1244 let boltz_nonce = musig::PublicNonce::from_str(pub_nonce)?;
1245
1246 let agg_nonce = musig::AggregatedNonce::new(&[&boltz_nonce, &gen_pub_nonce]);
1247
1248 let musig_session = musig::Session::new(&key_agg_cache, agg_nonce, &msg);
1249
1250 let partial_sig =
1251 musig_session.partial_sign(gen_sec_nonce, &convert_keypair(keys), &key_agg_cache);
1252
1253 Ok((partial_sig, gen_pub_nonce))
1254 }
1255}
1256
1257fn convert_pubkeys_for_musig(
1258 pubkeys: &[bitcoin::secp256k1::PublicKey; 2],
1259) -> [secp256k1_musig::PublicKey; 2] {
1260 [
1261 convert_public_key(pubkeys[0]),
1262 convert_public_key(pubkeys[1]),
1263 ]
1264}
1265
1266fn convert_xonly_key(key: secp256k1_musig::XOnlyPublicKey) -> bitcoin::XOnlyPublicKey {
1267 bitcoin::XOnlyPublicKey::from_slice(&key.serialize()[..]).expect("xonly key size matches")
1268}
1269
1270fn convert_public_key(key: bitcoin::secp256k1::PublicKey) -> secp256k1_musig::PublicKey {
1271 secp256k1_musig::PublicKey::from_slice(&key.serialize()[..]).expect("public key size matches")
1272}
1273
1274fn convert_keypair(keys: &bitcoin::secp256k1::Keypair) -> secp256k1_musig::Keypair {
1275 secp256k1_musig::Keypair::from_seckey_byte_array(keys.secret_bytes())
1276 .expect("keypair size matches")
1277}
1278
1279fn convert_schnorr_signature(
1280 schnorr_sig: secp256k1_musig::schnorr::Signature,
1281) -> bitcoin::secp256k1::schnorr::Signature {
1282 bitcoin::secp256k1::schnorr::Signature::from_slice(schnorr_sig.as_byte_array())
1283 .expect("signature size matches")
1284}