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#[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 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 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 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 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 fn taproot_spendinfo(&self) -> Result<TaprootSpendInfo, Error> {
357 let secp = Secp256k1::new();
358
359 let key_agg_cache = self.musig_keyagg_cache();
361
362 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 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 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 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 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 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#[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, pub genesis_hash: BlockHash, }
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 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 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 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 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 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 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 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), );
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 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)), rangeproof: Some(Box::new(rangeproof)), };
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 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 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 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 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), );
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 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 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)), rangeproof: Some(Box::new(rangeproof)), };
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 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 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 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 fn partial_sign(
1347 &self,
1348 keys: &Keypair,
1349 pub_nonce: &str,
1350 transaction_hash: &str,
1351 ) -> Result<(musig::PartialSignature, musig::PublicNonce), Error> {
1352 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 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 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}