diff --git a/src/lib.rs b/src/lib.rs index 7219fee..0fea087 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,3 +41,4 @@ pub use swaps::{ boltz, liquid::{LBtcSwapScript, LBtcSwapTx}, }; +pub use util::fees; diff --git a/src/swaps/bitcoin.rs b/src/swaps/bitcoin.rs index 2240676..df1c948 100644 --- a/src/swaps/bitcoin.rs +++ b/src/swaps/bitcoin.rs @@ -36,6 +36,7 @@ use super::boltz::{ SwapTxKind, SwapType, ToSign, }; +use crate::util::fees::{create_tx_with_fee, Fee}; use elements::secp256k1_zkp::{ musig, MusigAggNonce, MusigKeyAggCache, MusigPartialSignature, MusigPubNonce, MusigSession, MusigSessionId, @@ -656,7 +657,7 @@ impl BtcSwapTx { &self, keys: &Keypair, preimage: &Preimage, - absolute_fees: u64, + fee: Fee, is_cooperative: Option, ) -> Result { if self.swap_script.swap_type == SwapType::Submarine { @@ -671,42 +672,13 @@ impl BtcSwapTx { )); } - let preimage_bytes = if let Some(value) = preimage.bytes { - value - } else { - return Err(Error::Protocol( - "No preimage provided while signing.".to_string(), - )); - }; - - // For claim, we only consider 1 utxo - let utxo = self.utxos.first().ok_or(Error::Protocol( - "No Bitcoin UTXO detected for this script".to_string(), - ))?; - let txin = TxIn { - previous_output: utxo.0, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - script_sig: ScriptBuf::new(), - witness: Witness::new(), - }; - - let destination_spk = self.output_address.script_pubkey(); - - let txout = TxOut { - script_pubkey: destination_spk, - value: Amount::from_sat(utxo.1.value.to_sat() - absolute_fees), - }; - - let mut claim_tx = Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: vec![txin], - output: vec![txout], - }; - - let secp = Secp256k1::new(); + let mut claim_tx = create_tx_with_fee( + fee, + |fee| self.create_claim(keys, preimage, fee, is_cooperative.is_some()), + |tx| tx.vsize(), + )?; - // If its a cooperative claim, compute the Musig2 Aggregate Signature and use Keypath spending + // If it's a cooperative claim, compute the Musig2 Aggregate Signature and use Keypath spending if let Some(Cooperative { boltz_api, swap_id, @@ -714,12 +686,14 @@ impl BtcSwapTx { partial_sig, }) = is_cooperative { + let secp = Secp256k1::new(); + // Start the Musig session // Step 1: Get the sighash let claim_tx_taproot_hash = SighashCache::new(claim_tx.clone()) .taproot_key_spend_signature_hash( 0, - &Prevouts::All(&[&utxo.1]), + &Prevouts::All(&[&self.utxos.first().unwrap().1]), bitcoin::TapSighashType::Default, )?; @@ -826,7 +800,57 @@ impl BtcSwapTx { witness.push(final_schnorr_sig.to_vec()); claim_tx.input[0].witness = witness; + } + + Ok(claim_tx) + } + + fn create_claim( + &self, + keys: &Keypair, + preimage: &Preimage, + absolute_fees: u64, + is_cooperative: bool, + ) -> Result { + let preimage_bytes = if let Some(value) = preimage.bytes { + value + } else { + return Err(Error::Protocol( + "No preimage provided while signing.".to_string(), + )); + }; + + // For claim, we only consider 1 utxo + let utxo = self.utxos.first().ok_or(Error::Protocol( + "No Bitcoin UTXO detected for this script".to_string(), + ))?; + + let txin = TxIn { + previous_output: utxo.0, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + script_sig: ScriptBuf::new(), + witness: Witness::new(), + }; + + let destination_spk = self.output_address.script_pubkey(); + + let txout = TxOut { + script_pubkey: destination_spk, + value: Amount::from_sat(utxo.1.value.to_sat() - absolute_fees), + }; + + let mut claim_tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![txin], + output: vec![txout], + }; + + if is_cooperative { + claim_tx.input[0].witness = Self::stubbed_cooperative_witness(); } else { + let secp = Secp256k1::new(); + // If Non-Cooperative claim use the Script Path spending claim_tx.input[0].sequence = Sequence::ZERO; @@ -873,7 +897,7 @@ impl BtcSwapTx { pub fn sign_refund( &self, keys: &Keypair, - absolute_fees: u64, + fee: Fee, is_cooperative: Option, ) -> Result { if self.swap_script.swap_type == SwapType::ReverseSubmarine { @@ -888,70 +912,11 @@ impl BtcSwapTx { )); } - let utxos_amount = self - .utxos - .iter() - .fold(Amount::ZERO, |acc, (_, txo)| acc + txo.value); - let absolute_fees_amount = Amount::from_sat(absolute_fees); - if utxos_amount <= absolute_fees_amount { - return Err(Error::Generic( - format!("Cannot sign Refund Tx because utxos_amount ({utxos_amount}) <= absolute_fees ({absolute_fees_amount})") - )); - } - let output_amount: Amount = utxos_amount - absolute_fees_amount; - let output: TxOut = TxOut { - script_pubkey: self.output_address.script_pubkey(), - value: output_amount, - }; - - let unsigned_inputs = self - .utxos - .iter() - .map(|(outpoint, _txo)| TxIn { - previous_output: *outpoint, - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::new(), - }) - .collect(); - - let lock_time = match self - .swap_script - .refund_script() - .instructions() - .filter_map(|i| { - let ins = i.unwrap(); - if let Instruction::PushBytes(bytes) = ins { - if bytes.len() < 5_usize { - Some(LockTime::from_consensus(bytes_to_u32_little_endian( - bytes.as_bytes(), - ))) - } else { - None - } - } else { - None - } - }) - .next() - { - Some(r) => r, - None => { - return Err(Error::Protocol( - "Error getting timelock from refund script".to_string(), - )) - } - }; - - let mut refund_tx = Transaction { - version: Version::TWO, - lock_time, - input: unsigned_inputs, - output: vec![output], - }; - - let secp = Secp256k1::new(); - let tx_outs: Vec<&TxOut> = self.utxos.iter().map(|(_, out)| out).collect(); + let mut refund_tx = create_tx_with_fee( + fee, + |fee| self.create_refund(keys, fee, is_cooperative.is_some()), + |tx| tx.vsize(), + )?; if let Some(Cooperative { boltz_api, swap_id, .. @@ -962,6 +927,7 @@ impl BtcSwapTx { for input_index in 0..refund_tx.input.len() { // Step 1: Get the sighash + let tx_outs: Vec<&TxOut> = self.utxos.iter().map(|(_, out)| out).collect(); let refund_tx_taproot_hash = SighashCache::new(refund_tx.clone()) .taproot_key_spend_signature_hash( input_index, @@ -972,7 +938,6 @@ impl BtcSwapTx { let msg = Message::from_digest_slice(refund_tx_taproot_hash.as_byte_array())?; // Step 2: Get the Public and Secret nonces - let mut key_agg_cache = self.swap_script.musig_keyagg_cache(); let tweak = SecretKey::from_slice( @@ -982,6 +947,7 @@ impl BtcSwapTx { .as_byte_array(), )?; + let secp = Secp256k1::new(); let _ = key_agg_cache.pubkey_xonly_tweak_add(&secp, tweak)?; let session_id = MusigSessionId::new(&mut thread_rng()); @@ -1064,6 +1030,85 @@ impl BtcSwapTx { witness.push(final_schnorr_sig.to_vec()); refund_tx.input[input_index].witness = witness; } + } + + Ok(refund_tx) + } + + fn create_refund( + &self, + keys: &Keypair, + absolute_fees: u64, + is_cooperative: bool, + ) -> Result { + let utxos_amount = self + .utxos + .iter() + .fold(Amount::ZERO, |acc, (_, txo)| acc + txo.value); + let absolute_fees_amount = Amount::from_sat(absolute_fees); + if utxos_amount <= absolute_fees_amount { + return Err(Error::Generic( + format!("Cannot sign Refund Tx because utxos_amount ({utxos_amount}) <= absolute_fees ({absolute_fees_amount})") + )); + } + let output_amount: Amount = utxos_amount - absolute_fees_amount; + let output: TxOut = TxOut { + script_pubkey: self.output_address.script_pubkey(), + value: output_amount, + }; + + let unsigned_inputs = self + .utxos + .iter() + .map(|(outpoint, _txo)| TxIn { + previous_output: *outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }) + .collect(); + + let lock_time = match self + .swap_script + .refund_script() + .instructions() + .filter_map(|i| { + let ins = i.unwrap(); + if let Instruction::PushBytes(bytes) = ins { + if bytes.len() < 5_usize { + Some(LockTime::from_consensus(bytes_to_u32_little_endian( + bytes.as_bytes(), + ))) + } else { + None + } + } else { + None + } + }) + .next() + { + Some(r) => r, + None => { + return Err(Error::Protocol( + "Error getting timelock from refund script".to_string(), + )) + } + }; + + let mut refund_tx = Transaction { + version: Version::TWO, + lock_time, + input: unsigned_inputs, + output: vec![output], + }; + + let tx_outs: Vec<&TxOut> = self.utxos.iter().map(|(_, out)| out).collect(); + + if is_cooperative { + for index in 0..refund_tx.input.len() { + refund_tx.input[index].witness = Self::stubbed_cooperative_witness(); + } } else { let leaf_hash = TapLeafHash::from_script(&self.swap_script.refund_script(), LeafVersion::TapScript); @@ -1108,11 +1153,19 @@ impl BtcSwapTx { witness.push(control_block.serialize()); refund_tx.input[input_index].witness = witness; } - }; + } Ok(refund_tx) } + fn stubbed_cooperative_witness() -> Witness { + let mut witness = Witness::new(); + // Stub because we don't want to create cooperative signatures here + // but still be able to have an accurate size estimation + witness.push([0, 64]); + witness + } + /// Calculate the size of a transaction. /// Use this before calling drain to help calculate the absolute fees. /// Multiply the size by the fee_rate to get the absolute fees. @@ -1120,8 +1173,10 @@ impl BtcSwapTx { let dummy_abs_fee = 0; // Can only calculate non-coperative claims let tx = match self.kind { - SwapTxKind::Claim => self.sign_claim(keys, preimage, dummy_abs_fee, None)?, - SwapTxKind::Refund => self.sign_refund(keys, dummy_abs_fee, None)?, + SwapTxKind::Claim => { + self.sign_claim(keys, preimage, Fee::Absolute(dummy_abs_fee), None)? + } + SwapTxKind::Refund => self.sign_refund(keys, Fee::Absolute(dummy_abs_fee), None)?, }; Ok(tx.vsize()) } diff --git a/src/swaps/liquid.rs b/src/swaps/liquid.rs index 06c317b..608d3e5 100644 --- a/src/swaps/liquid.rs +++ b/src/swaps/liquid.rs @@ -32,6 +32,11 @@ use crate::{ use crate::error::Error; +use super::boltz::{ + BoltzApiClientV2, ChainClaimTxResponse, ChainSwapDetails, Cooperative, CreateReverseResponse, + CreateSubmarineResponse, Side, SubmarineClaimTxResponse, SwapTxKind, SwapType, ToSign, +}; +use crate::fees::{create_tx_with_fee, Fee}; use elements::bitcoin::PublicKey; use elements::secp256k1_zkp::Keypair as ZKKeyPair; use elements::{ @@ -41,11 +46,6 @@ use elements::{ AddressParams, }; -use super::boltz::{ - BoltzApiClientV2, ChainClaimTxResponse, ChainSwapDetails, Cooperative, CreateReverseResponse, - CreateSubmarineResponse, Side, SubmarineClaimTxResponse, SwapTxKind, SwapType, ToSign, -}; - /// Liquid v2 swap script helper. #[derive(Debug, Clone, PartialEq)] pub struct LBtcSwapScript { @@ -649,8 +649,9 @@ impl LBtcSwapTx { &self, keys: &Keypair, preimage: &Preimage, - absolute_fees: Amount, + fee: Fee, is_cooperative: Option, + is_discount_ct: bool, ) -> Result { if self.swap_script.swap_type == SwapType::Submarine { return Err(Error::Protocol( @@ -664,89 +665,12 @@ impl LBtcSwapTx { )); } - let preimage_bytes = preimage - .bytes - .ok_or(Error::Protocol("No preimage provided".to_string()))?; - - let claim_txin = TxIn { - sequence: Sequence::MAX, - previous_output: self.funding_outpoint, - script_sig: Script::new(), - witness: TxInWitness::default(), - is_pegin: false, - asset_issuance: AssetIssuance::default(), - }; - - let secp = Secp256k1::new(); - - let unblined_utxo = self - .funding_utxo - .unblind(&secp, self.swap_script.blinding_key.secret_key())?; - let asset_id = unblined_utxo.asset; - let out_abf = AssetBlindingFactor::new(&mut thread_rng()); - let exp_asset = Asset::Explicit(asset_id); - - let (blinded_asset, asset_surjection_proof) = - exp_asset.blind(&mut thread_rng(), &secp, out_abf, &[unblined_utxo])?; - - let output_value = Amount::from_sat(unblined_utxo.value) - absolute_fees; - - let final_vbf = ValueBlindingFactor::last( - &secp, - output_value.to_sat(), - out_abf, - &[( - unblined_utxo.value, - unblined_utxo.asset_bf, - unblined_utxo.value_bf, - )], - &[( - absolute_fees.to_sat(), - AssetBlindingFactor::zero(), - ValueBlindingFactor::zero(), - )], - ); - let explicit_value = elements::confidential::Value::Explicit(output_value.to_sat()); - let msg = elements::RangeProofMessage { - asset: asset_id, - bf: out_abf, - }; - let ephemeral_sk = SecretKey::new(&mut thread_rng()); - - // assuming we always use a blinded address that has an extractable blinding pub - let blinding_key = self - .output_address - .blinding_pubkey - .ok_or(Error::Protocol("No blinding key in tx.".to_string()))?; - let (blinded_value, nonce, rangeproof) = explicit_value.blind( - &secp, - final_vbf, - blinding_key, - ephemeral_sk, - &self.output_address.script_pubkey(), - &msg, + let mut claim_tx = create_tx_with_fee( + fee, + |fee| self.create_claim(keys, preimage, fee, is_cooperative.is_some()), + |tx| tx_size(&tx, is_discount_ct), )?; - let tx_out_witness = TxOutWitness { - surjection_proof: Some(Box::new(asset_surjection_proof)), // from asset blinding - rangeproof: Some(Box::new(rangeproof)), // from value blinding - }; - let payment_output: TxOut = TxOut { - script_pubkey: self.output_address.script_pubkey(), - value: blinded_value, - asset: blinded_asset, - nonce, - witness: tx_out_witness, - }; - let fee_output: TxOut = TxOut::new_fee(absolute_fees.to_sat(), asset_id); - - let mut claim_tx = Transaction { - version: 2, - lock_time: LockTime::ZERO, - input: vec![claim_txin], - output: vec![payment_output, fee_output], - }; - // If its a cooperative claim, compute the Musig2 Aggregate Signature and use Keypath spending if let Some(Cooperative { boltz_api, @@ -774,6 +698,7 @@ impl LBtcSwapTx { .as_byte_array(), )?; + let secp = Secp256k1::new(); let _ = key_agg_cache.pubkey_xonly_tweak_add(&secp, tweak)?; let session_id = MusigSessionId::new(&mut thread_rng()); @@ -870,80 +795,24 @@ impl LBtcSwapTx { pegin_witness: vec![], }; - claim_tx.input[0].witness = witness; - } else { - // If Non-Cooperative claim use the Script Path spending - claim_tx.input[0].sequence = Sequence::ZERO; - let claim_script = self.swap_script.claim_script(); - let leaf_hash = TapLeafHash::from_script(&claim_script, LeafVersion::default()); - - let sighash = SighashCache::new(&claim_tx).taproot_script_spend_signature_hash( - 0, - &Prevouts::All(&[&self.funding_utxo]), - leaf_hash, - SchnorrSighashType::Default, - self.genesis_hash, - )?; - - let msg = Message::from_digest_slice(sighash.as_byte_array())?; - - let sig = secp.sign_schnorr(&msg, keys); - - let final_sig = SchnorrSig { - sig, - hash_ty: SchnorrSighashType::Default, - }; - - let control_block = match self - .swap_script - .taproot_spendinfo()? - .control_block(&(claim_script.clone(), LeafVersion::default())) - { - Some(r) => r, - None => return Err(Error::Taproot("Could not create control block".to_string())), - }; - - let mut script_witness = Witness::new(); - script_witness.push(final_sig.to_vec()); - script_witness.push(preimage.bytes.unwrap()); // checked for none - script_witness.push(claim_script.as_bytes()); - script_witness.push(control_block.serialize()); - - let witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness: script_witness.to_vec(), - pegin_witness: vec![], - }; - claim_tx.input[0].witness = witness; } Ok(claim_tx) } - /// Sign a refund transaction. - /// Panics if called on a Reverse Swap or Claim Tx. - pub fn sign_refund( + fn create_claim( &self, keys: &Keypair, - absolute_fees: Amount, - is_cooperative: Option, + preimage: &Preimage, + absolute_fees: u64, + is_cooperative: bool, ) -> Result { - if self.swap_script.swap_type == SwapType::ReverseSubmarine { - return Err(Error::Protocol( - "Refund Tx signing is not applicable for Reverse Submarine Swaps".to_string(), - )); - } - - if self.kind == SwapTxKind::Claim { - return Err(Error::Protocol( - "Cannot sign refund with a claim-type LBtcSwapTx".to_string(), - )); - } + let preimage_bytes = preimage + .bytes + .ok_or(Error::Protocol("No preimage provided".to_string()))?; - // Create unsigned refund transaction - let refund_txin = TxIn { + let claim_txin = TxIn { sequence: Sequence::MAX, previous_output: self.funding_outpoint, script_sig: Script::new(), @@ -964,7 +833,7 @@ impl LBtcSwapTx { let (blinded_asset, asset_surjection_proof) = exp_asset.blind(&mut thread_rng(), &secp, out_abf, &[unblined_utxo])?; - let output_value = Amount::from_sat(unblined_utxo.value) - absolute_fees; + let output_value = Amount::from_sat(unblined_utxo.value) - Amount::from_sat(absolute_fees); let final_vbf = ValueBlindingFactor::last( &secp, @@ -976,7 +845,7 @@ impl LBtcSwapTx { unblined_utxo.value_bf, )], &[( - absolute_fees.to_sat(), + absolute_fees, AssetBlindingFactor::zero(), ValueBlindingFactor::zero(), )], @@ -1013,45 +882,101 @@ impl LBtcSwapTx { nonce, witness: tx_out_witness, }; - let fee_output: TxOut = TxOut::new_fee(absolute_fees.to_sat(), asset_id); - - let refund_script = self.swap_script.refund_script(); - - let lock_time = match refund_script - .instructions() - .filter_map(|i| { - let ins = i.unwrap(); - if let Instruction::PushBytes(bytes) = ins { - if bytes.len() < 5_usize { - Some(LockTime::from_consensus(bytes_to_u32_little_endian(bytes))) - } else { - None - } - } else { - None - } - }) - .next() - { - Some(r) => r, - None => { - return Err(Error::Protocol( - "Error getting timelock from refund script".to_string(), - )) - } - }; + let fee_output: TxOut = TxOut::new_fee(absolute_fees, asset_id); - let mut refund_tx = Transaction { + let mut claim_tx = Transaction { version: 2, - lock_time, - input: vec![refund_txin], - output: vec![fee_output, payment_output], + lock_time: LockTime::ZERO, + input: vec![claim_txin], + output: vec![payment_output, fee_output], }; + if is_cooperative { + claim_tx.input[0].witness = Self::stubbed_cooperative_witness(); + } else { + // If Non-Cooperative claim use the Script Path spending + claim_tx.input[0].sequence = Sequence::ZERO; + let claim_script = self.swap_script.claim_script(); + let leaf_hash = TapLeafHash::from_script(&claim_script, LeafVersion::default()); + + let sighash = SighashCache::new(&claim_tx).taproot_script_spend_signature_hash( + 0, + &Prevouts::All(&[&self.funding_utxo]), + leaf_hash, + SchnorrSighashType::Default, + self.genesis_hash, + )?; + + let msg = Message::from_digest_slice(sighash.as_byte_array())?; + + let sig = secp.sign_schnorr(&msg, keys); + + let final_sig = SchnorrSig { + sig, + hash_ty: SchnorrSighashType::Default, + }; + + let control_block = match self + .swap_script + .taproot_spendinfo()? + .control_block(&(claim_script.clone(), LeafVersion::default())) + { + Some(r) => r, + None => return Err(Error::Taproot("Could not create control block".to_string())), + }; + + let mut script_witness = Witness::new(); + script_witness.push(final_sig.to_vec()); + script_witness.push(preimage.bytes.unwrap()); // checked for none + script_witness.push(claim_script.as_bytes()); + script_witness.push(control_block.serialize()); + + let witness = TxInWitness { + amount_rangeproof: None, + inflation_keys_rangeproof: None, + script_witness: script_witness.to_vec(), + pegin_witness: vec![], + }; + + claim_tx.input[0].witness = witness; + } + + Ok(claim_tx) + } + + /// Sign a refund transaction. + /// Panics if called on a Reverse Swap or Claim Tx. + pub fn sign_refund( + &self, + keys: &Keypair, + fee: Fee, + is_cooperative: Option, + is_discount_ct: bool, + ) -> Result { + if self.swap_script.swap_type == SwapType::ReverseSubmarine { + return Err(Error::Protocol( + "Refund Tx signing is not applicable for Reverse Submarine Swaps".to_string(), + )); + } + + if self.kind == SwapTxKind::Claim { + return Err(Error::Protocol( + "Cannot sign refund with a claim-type LBtcSwapTx".to_string(), + )); + } + + let mut refund_tx = create_tx_with_fee( + fee, + |fee| self.create_refund(keys, fee, is_cooperative.is_some()), + |tx| tx_size(&tx, is_discount_ct), + )?; + if let Some(Cooperative { boltz_api, swap_id, .. }) = is_cooperative { + let secp = Secp256k1::new(); + refund_tx.lock_time = LockTime::ZERO; let claim_tx_taproot_hash = SighashCache::new(&refund_tx) @@ -1154,6 +1079,125 @@ impl LBtcSwapTx { }; refund_tx.input[0].witness = witness; + } + + Ok(refund_tx) + } + + fn create_refund( + &self, + keys: &Keypair, + absolute_fees: u64, + is_cooperative: bool, + ) -> Result { + // Create unsigned refund transaction + let refund_txin = TxIn { + sequence: Sequence::MAX, + previous_output: self.funding_outpoint, + script_sig: Script::new(), + witness: TxInWitness::default(), + is_pegin: false, + asset_issuance: AssetIssuance::default(), + }; + + let secp = Secp256k1::new(); + + let unblined_utxo = self + .funding_utxo + .unblind(&secp, self.swap_script.blinding_key.secret_key())?; + let asset_id = unblined_utxo.asset; + let out_abf = AssetBlindingFactor::new(&mut thread_rng()); + let exp_asset = Asset::Explicit(asset_id); + + let (blinded_asset, asset_surjection_proof) = + exp_asset.blind(&mut thread_rng(), &secp, out_abf, &[unblined_utxo])?; + + let output_value = Amount::from_sat(unblined_utxo.value) - Amount::from_sat(absolute_fees); + + let final_vbf = ValueBlindingFactor::last( + &secp, + output_value.to_sat(), + out_abf, + &[( + unblined_utxo.value, + unblined_utxo.asset_bf, + unblined_utxo.value_bf, + )], + &[( + absolute_fees, + AssetBlindingFactor::zero(), + ValueBlindingFactor::zero(), + )], + ); + let explicit_value = elements::confidential::Value::Explicit(output_value.to_sat()); + let msg = elements::RangeProofMessage { + asset: asset_id, + bf: out_abf, + }; + let ephemeral_sk = SecretKey::new(&mut thread_rng()); + + // assuming we always use a blinded address that has an extractable blinding pub + let blinding_key = self + .output_address + .blinding_pubkey + .ok_or(Error::Protocol("No blinding key in tx.".to_string()))?; + let (blinded_value, nonce, rangeproof) = explicit_value.blind( + &secp, + final_vbf, + blinding_key, + ephemeral_sk, + &self.output_address.script_pubkey(), + &msg, + )?; + + let tx_out_witness = TxOutWitness { + surjection_proof: Some(Box::new(asset_surjection_proof)), // from asset blinding + rangeproof: Some(Box::new(rangeproof)), // from value blinding + }; + let payment_output: TxOut = TxOut { + script_pubkey: self.output_address.script_pubkey(), + value: blinded_value, + asset: blinded_asset, + nonce, + witness: tx_out_witness, + }; + let fee_output: TxOut = TxOut::new_fee(absolute_fees, asset_id); + + let refund_script = self.swap_script.refund_script(); + + let lock_time = match refund_script + .instructions() + .filter_map(|i| { + let ins = i.unwrap(); + if let Instruction::PushBytes(bytes) = ins { + if bytes.len() < 5_usize { + Some(LockTime::from_consensus(bytes_to_u32_little_endian(bytes))) + } else { + None + } + } else { + None + } + }) + .next() + { + Some(r) => r, + None => { + return Err(Error::Protocol( + "Error getting timelock from refund script".to_string(), + )) + } + }; + + let mut refund_tx = Transaction { + version: 2, + lock_time, + input: vec![refund_txin], + output: vec![fee_output, payment_output], + }; + + if is_cooperative { + refund_tx.input[0].witness = Self::stubbed_cooperative_witness(); } else { refund_tx.input[0].sequence = Sequence::ZERO; @@ -1203,6 +1247,20 @@ impl LBtcSwapTx { Ok(refund_tx) } + fn stubbed_cooperative_witness() -> TxInWitness { + let mut witness = Witness::new(); + // Stub because we don't want to create cooperative signatures here + // but still be able to have an accurate size estimation + witness.push([0, 64]); + + TxInWitness { + amount_rangeproof: None, + inflation_keys_rangeproof: None, + script_witness: witness.to_vec(), + pegin_witness: vec![], + } + } + /// Calculate the size of a transaction. /// Use this before calling drain to help calculate the absolute fees. /// Multiply the size by the fee_rate to get the absolute fees. @@ -1212,15 +1270,20 @@ impl LBtcSwapTx { preimage: &Preimage, is_discount_ct: bool, ) -> Result { - let dummy_abs_fee = Amount::from_sat(0); + let dummy_abs_fee = 0; let tx = match self.kind { - SwapTxKind::Claim => self.sign_claim(keys, preimage, dummy_abs_fee, None)?, // TODO: Hardcode cooperative spend size - SwapTxKind::Refund => self.sign_refund(keys, dummy_abs_fee, None)?, + SwapTxKind::Claim => self.sign_claim( + keys, + preimage, + Fee::Absolute(dummy_abs_fee), + None, + is_discount_ct, + )?, + SwapTxKind::Refund => { + self.sign_refund(keys, Fee::Absolute(dummy_abs_fee), None, is_discount_ct)? + } }; - Ok(match is_discount_ct { - true => tx.discount_vsize(), - false => tx.vsize(), - }) + Ok(tx_size(&tx, is_discount_ct)) } /// Broadcast transaction to the network @@ -1261,6 +1324,13 @@ impl LBtcSwapTx { } } +fn tx_size(tx: &Transaction, is_discount_ct: bool) -> usize { + match is_discount_ct { + true => tx.discount_vsize(), + false => tx.vsize(), + } +} + fn hex_to_bytes(hex_str: &str) -> Result, Error> { if hex_str.len() % 2 != 0 { return Err(Error::Hex( @@ -1283,3 +1353,17 @@ fn hex_to_bytes(hex_str: &str) -> Result, Error> { Ok(bytes) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tx_size() { + // From https://github.com/ElementsProject/ELIPs/blob/main/elip-0200.mediawiki#test-vectors + let tx: Transaction = elements::encode::deserialize(&hex::decode("0200000001017b85545c658d507ff56f315c77f910dd19cc9ceb7d5e1e4d3a3f8be4a91fe7440000000000fdffffff020bb6478c61c8f5f024ded219c967314685257f0ded894eaf626a00843a6ab80412091ee78237e38fb36c8be564ecd76e65f743065522f38f838367680ed7287b459103aabd97d4c8f3eac9555edfd2a709370b802335da478b6578501f72a4d100482716001455f4f701eec6059f956a40335e317a96a5e87ab5016d521c38ec1ea15734ae22b7c46064412829c0d0579f0a713d1c04ede979026f01000000000000000e00000000000000000347304402205d62bc013832eb6a631fe0285c49b7e27846e03189a245bec8f86346382282a702206c6e839b4b1d79d74662e432b724671402a6cfa2287911677c7061a3a32abe34012042c6504afda18a302bbf935f1dc646f71872a9a2fb5ed9e0cffb64588fd0d0a865a9141243397ee5e188bdcd17c9529c1382c7f8bc0fe987632102a3cd0d865794542994737e776dc3827a046c02ea2693f1d1f64315b3557bbb8b670395f72bb17521034a2e0343a515cf7d4a583d05bec3ee9fc16758cae791c10064fa92d65672d1fe68ac004301000177ce2a14a4f9e556fc846219827e1bc584caf9ef35e761dbf1f961a89b8285bde8fbe242c6984dd28719a792cd2e63535287db9a3b1fc4e4c5ae28cc5e8973d0fd4e10603300000000000000014cf45a01f0036bec883cdd4d5d8de1d7b3f2ec125733ce2e123ef3ff0085c50fd1b8cd3101c24fd8fff0bab803cda813aad9645ca6714ce768da75da09b58851585551c425e729d6faf4186a6659ea107f4ef35cc458dae565f1337af46cde218563eb3a756dc5d532717cc775fc0d04fbf4492070eb3cd9943a12fd07939d69a71090871e1ddf8fe716e2bc3f3364783cdb1d6a704325ca6c4334171563ae7bfcc9766ab848a65f47973753b2758b4404f17e54527080cfb980d1227f70cc0e77212d06aea909c7f2ac38f4a75c387464f8b70e33061f017a6fbbccf0673d08aebae2a1ce6cf9dd8c98791b1f4d653788b2ed6dd65cf9795eac568744e386d68c89d973ca079298f8d292b6bee71fad94a0f83aaf070ccfeb6c6de20baf8c6f1083dcdd539fae6ed74832100ea7c07296c0af2201523c3abf8b784ca8a235556d5bae668f17d9a353fd49dbae623ca44830a8fc4963419e49a9dc99bf87ea0414be3b43a6eab8ce54695d66887b261c08252a501d0c78d30be1ae3fc10f557f4d228ef38da496b22c5fa79d92e2c190b9d31f286dc0e3c8489fcb8e0603f8b93a6eb1ec726a7e0015e70407da186d85b290b054747276a8928443e1108cb67738d156787d20553c39fa0449f95addbf42170fdab8107d1f93fcd841964b6e6c4c140d0c4ed1463835e603f5012a4aafd5b038ceb9b4a5b7e2688cfd8c4f2bfafaf0bb5bb1aa7a7f13bd47ff3da57c4c88b741fd9ff97abc23d4047f690d59c4c67494f47125fe0f626ad409a92d72907ad0b1762b5271f474fa552d9139fcb1103db24f7a29726a5e41a6dbc43590c14a62eb1b2aa0f160134c42c6c87c696e7c42546bb72f9f531729555d01c529570553aeec70709c3a4f9aacf810d5018f776af48b93eff8e120242105c06a32e64bfc825fde488c99d5845adba2cf349717f64e488852cca73cc5813b7872f7e89d24b4bfafdf75faa368375d5bfdd8b8a7ad641703cbff131616c77e79d8f78c5fe63810781db44fb1fa5cc9387cf0de6807d1a3d5e3d8f9ec7418bbb1d4e10b1fcdb300abd8625b4e24842f1f4c4e567fe9f8c6e9d314757d4568889bccc740fb36f0270804cc11c0044093ab9586ed034cd1eb70bacdedd573750794f0286dfb91c91308e507147ea8e8534c655b931f4e68543e93c57cf2f2159e021739943e40c0dbc8a68193218d40d71e0956b00b4a01fa9c06e67ea55e0213fab48a8dfcf3a047e8c438e7c94fc195026cec82ad532e2aa5970a9fe6c03d9088d0ab45e0b9c7bf9597bd2db93ef7d7f139c291f59e03cda1a5f9a793eb7ec6d50fa9482b712500b5e5a780319769836f7053e3c5a3276a7d65467578a7fbf9079fb5c6bb1b0558acbf3cd896644d42a7b0fd87b12b571b3d8122b1c254750bf9b097d0ec5ed31f9af7db9571f706f5909f0ef2fdcdb255a0795f5c28b70fd1d25b74eb2524ae8f47756875ff439a2b2769adc844312c4ac7bde16b561e62ee3069d25718bf6c2e11ffbb83c863a51c52ff4ead581dd6b1ff0913905163683b97ecbad003a1c71469050eed5ad79e9bb44179b90b8e6b0e6a61a0ed4e919cb96c2615b61cf93905adc3e6e2a127bd661f05e928a45bc1c0599c41450dea0182043b977fcfcf3620f765d3aab13cbe684028dc78a4bd02324427379735934ab4cb821623f49e3af05391c1b7acfe8be33c9201efeded50838ff216d6744d61e8d1d600260c8f7275a46764ac9392132f0b3661e5e92e9daa87b9329d9c89353f40a130bcf8611cce25335f9f1c1208ae1bdc47d96c3f83170a7d27367a043debdfd0e43776d330d1f7a806b32c4363d1dca14715dae4f4d1c99a92673954094e61387080353974097adfde15de4009caa28d42703fdb56fcdac47bd9c5e3bad2fbf90b4a3fab4d89a9933e445ba85f759cc149101f5045a6f3a6d741424318249d96277cea3dc0c4814763d727c72a1867618ac05e5ff103b985cc6f78829bae92794680a51c4b7f7f8b88e39ddd4471890914594f3f03ae668d501732ea77b3eb1fb38b5ad9efdac8775e0995c60a3949e84d2298ea3463aaa16d5ff633da654463e90004915ccc19663c87e006fcd05e904b85b71428d79913e3afdecb7ad51a66f7dcb738d028b62b307025d524320dbe064330da5cbd70467635cf492197c7be3513363b4000bf176827011b2894d33dc9d806b2526a6e91cc1cf0582c5330484b8d48be4855c1859a5b20cab6d08d95b42b57fc709dcb637ba9c6e70b72c473af88ebe8723fe94a0d5ee5d483f19c3b2aade19bafed774b786c0d24383fe0f71c085655f4bd78cb36da83b5429576576c0718b4549efe5b8f602c543c3a8e3d86f19b70d6be1fb39b7cbbac6fcf6d80d69c00ed44dbed1b8555593bd6dcf9ddd519f9325f6faa146d4b631cc6ee418ef9d07a0036fb26a792e7733ec0b58d9f0ebba9ea9493fa026bab62f70381e534c8c3b349be651e9fd5d472b3cbf8f7e912b7030a1992df35e17f4c5aa54f1632464a7c3b0dd133da8d436205bf45d8ded924e35b366803ee52a3d1c85d9f4f976785270dafb63d2cd5052328ed2e5381e9a6e9d8409675c2a9a43c74b07e8a3df8043b2b6d42832cabfcd495b8b30727346990fbc79e436d7ba4d7035603ab98532c5497ef493511e498b1b9c5ff413e919ab6f3cd6acc472f6a39ad0a8c9677ac9a5380a6bebbaaf13a114d097efbf140acad7edecc758bb070fa0b88bb0646d3bed911414a3f10b12bf8372d66f4525f9a8a66d7bf2b5d364119a687e5f416511c27659cf70969863ed7f80e80a4f2e55bf25721e1ab415305b66bfc25b9630a265b553d3e806807f23ec1e2a5f657dbd73a4a36e95e6616faa6aefc5143ca29b0e4bc9eb1042d99c74115d96a2eec5e7fb8c3f598d4df8fa8953e96689651a705dd3f385cd27e0173baca570ce53001cdb002e4476e6af47b9a891f84f7c1c472cce3cd4a70a40c298819f6d75e6adac193798c740c9f5f57fee4df5d140cce8ee4152c17784899003dc000cd2e7c7f23e74da085b254e0843d97d147e44ab3ba12e308925fc6ab0460c7ceb107b0900cef5ff939bc3fe5640f0bb11597c561be275fc8b5b85f5e38a3c12ea26b5b7b32e407685db70d16a3ce51043d4009a647fd3656a54adcbd4d1baa6d89881973fe32faf071123de1712e85db628bdd987566b362845d0c5f818547ec2d1f7c668cae44f0bec74c6663134dd0273c3363f31901903e4e976a447af96f6f521059fb6b892a0599cf7aae457df3aed72f1f55e145332c91430a2f8184bb917d317f8d9c4b6769b9a3a0ac5baea88b39b8f7662ecc16585e7166f61a948f48e6d30c2cfd82820cccdf5e722db2156bd848ea4d13c92544d1d9064414a305215a8271631ffebf08cdf0bcbbbd939f78eafec0d7238bdb90f211d6c44589187d1a501eef7d0b6118e028afcf76ffda95a43e2211206d9d50d34c3e33a6c991952ccd73e722802a14227692f037bba585e73cb9a6cd7556f9ec2158f197a51e3884afb8e59eaa8e7ac3568d88b27b2a5ab8cd72648193ff6068e4d481c58c117e2adda564d5a49f6b992ff6f938acb283e7baf704c71861d60b263f6c6684d7544878b7aca942af8b3a70ae0def309b68fac2aed2b11ba753d7b47f7369805e5b3b9b41d22196e2cc098ece59bdf5231b03fba8adae08fee227a582490b0db34c115620c72afb6fcb507397d1333ea19e7969b729bc2733e6546d2d9f3edb08f9c74201f9ed4e3fcb446cc3fd688b1345e97b32492c9173fa71df2772bd825506ddd6447e9f9e8ece0ffb860e1c755bcf2400deef094219795d4ee84acc34dedc9a3b3adf7fc81733bc511b8edcb54769400940b53471d8e82cb82d9967a97297bdd87f165968ea046291234da176efd20889aa4c07179df83cb500b40bdb96b0c27f2bfa57353268b776740432d29f1761fee77755c7b219def785a42b683e1f70240ec45cdf660e894d4fb541d0511547c9a2c503cf605d72ea7f2abaee4e8adc222a82f4b86c34ad8b25e2932df02f0090d2dbf8817c44659b1245d5579277ad406c538914f90dbaefdd110c5ca0d63a24706cd51096ec19f819c446c9fcb55b777ae633f0257dc4d1b293e6ef68ea7867d852058212a0a9ace9442422a638f73dfb14cc4354b6481ee6591037e7287e962037d963b38a7e4ec12b30e0f6e0ee4d8c30d288e99e22e43b4c795c51d66cc4225c5cab3685b1b3a6fd3a82dfc355634b347cc4f4e55413728fb67fb9f34d3f7e4ecce3254ea843ab361b0f652faa9e54470e3e414c1bb2593e36d88109c36dfab505a16c19152fe021de608c6b3d924c981231ea9cf1cf8c93e53f0df78033e81fdb578a45b7dc4f3f0f68feedc78ec7c347f91a0464bccd58aa2fc11016e88cbaddfb22112edad752792af12fa550be3e6f15d69a6a9d547ab5381b93c58c12753b8085d9e17ed1f2519cc5cb756e3777ea9f8e49a6141460f8f6ced8d12d13d950691479e1207ed35ab71554122beb215a0fb6b34b90784f4be6bd6fbf93daf9d3bc4640bc52a662e750ce361c12c1bfa2ca4e2c784cbf70c406587b2ebd69faa7a891aca63d600247ad7dde426c1ef4e3b22a072ff8eb69c1b1cb30c605112786546c48cf1c4821b5bc0d0bd44ba83b05656b6e19a3d1a76931d983dd39efcc64298e892858e847e99519c1fa25b1998839788c5852b94202d803639d69058604374f76769670a60269dbc0688cea2d9d8672212b93ca501fbf6f7dfefad058e4bd0e0da1cff41b2f408c980f29a49b03efa9e3edef091d7df7529b6b5e8f7d43d103681cd7c38d02a431b15d539e9a3cf44dc71621664e756ad6404ba185b5e20c82760c488fde4253fb52ab850484a082e7ca275f475012be9c8d16d6b4a2c9d863440d5e113d18bbf42f128462764a99ca90af4fde890aee138fe4cbb45658eacd9d38c8a1fb4499c043cc25af87e6a650f38149ab018cc49f50bbd085e2a0ba3eeecde5764f7997748a660593191977792d7176e4c2ff0113d67b9abe8fbc10f364c6fa68e52a455aa56ff15099c6efb6b5812972380d5b8e256b0feb1190835b7d076744c1b5b738c710a07a32676a15d96583e89e39eb4ff08cf02c6e2ad540c2b66299afe01bf2e50c81465a04d229a07c58ffd25a6cd9288110045526b376548d373273e6227d117d491020fd68e366ed697a0d30a5bdff25fa9a5800aa534a3669215dfa8f30960f142a8ae7ffcb654ca60aa7dc8a586670f9db37d05644ff5f934785c5433e605f3fbd0340e168511e209a0aedd8b18f3b948eb58051136d155f53b0e2e027361330e005f83f3a72dcc5d9161dd4b1e6abd16635dc0887dcc833a1fb59c10e0b8bea2536e7acd58d5e11179d13a24dc4292624c527266351b9a48893b956ffe545c8d2c1563805addef2a82134c9c686449d83471f22c1e14601895e854a5f854230e4fb4ed4f9a7ee22e83234be6c5bb19d200c16543468f186ae11cba84ae1aeda5136f7f5b380d02ddb9cbe2c5f5bb39138fa29b2ceb549d2e337eba10171fc237473351cf8e5989c193ef0100c75778ad0c05b64b614067c9a70680c818a566c4ba5e2991eedfe165199a55b0bef1333988f2add167e268db389c2d25bd85eedff9e6851e3df84c9e41128b5a76869c086fcf9275b1d51af02e4a92b66850785319dbf004a29594e32d12ca42da69fac69f886f963409ce1d4514d1ab9e915e071887e7f316b15014d083769afea374e0771f74f632db5ed7d7352546ed686e3ee161cd263dafc2acab74a67a5721f923f9b07c647c2a04f7d1c2f831d4319a60b16ed4c995e35ccbc291ff647a382976ba5a957547b0000").unwrap()).unwrap(); + + assert_eq!(tx_size(&tx, false), 1333); + assert_eq!(tx_size(&tx, true), 216); + } +} diff --git a/src/util/fees.rs b/src/util/fees.rs new file mode 100644 index 0000000..006ab6d --- /dev/null +++ b/src/util/fees.rs @@ -0,0 +1,52 @@ +use crate::error::Error; + +pub enum Fee { + // In sat/vByte + Relative(f64), + // In satoshis + Absolute(u64), +} + +pub(crate) fn create_tx_with_fee( + fee: Fee, + tx_constructor: F, + get_vsize: S, +) -> Result +where + F: Fn(u64) -> Result, + S: Fn(T) -> usize, +{ + match fee { + Fee::Relative(fee) => { + let vsize = get_vsize(tx_constructor(1)?); + // Round up to make sure we are not under the min relay fee + tx_constructor((vsize as f64 * fee).ceil() as u64) + } + Fee::Absolute(fee) => tx_constructor(fee), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct StubTx { + fee: u64, + } + + #[test] + fn test_create_tx_with_fee_relative() { + let fee = 0.1; + let vsize = 42; + let tx = + create_tx_with_fee(Fee::Relative(fee), |fee| Ok(StubTx { fee }), |_| vsize).unwrap(); + assert_eq!(tx.fee, 5); + } + + #[test] + fn test_create_tx_with_fee_absolute() { + let fee = 21; + let tx = create_tx_with_fee(Fee::Absolute(fee), |fee| Ok(StubTx { fee }), |_| 42).unwrap(); + assert_eq!(tx.fee, fee); + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index c03dddf..48fd9e3 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -8,6 +8,7 @@ use lightning_invoice::{Bolt11Invoice, RouteHintHop}; use crate::{error::Error, network::electrum::ElectrumConfig}; pub mod ec; +pub mod fees; #[cfg(feature = "lnurl")] pub mod lnurl; pub mod secrets; diff --git a/tests/bitcoin.rs b/tests/bitcoin.rs index d4f9928..70c0628 100644 --- a/tests/bitcoin.rs +++ b/tests/bitcoin.rs @@ -20,6 +20,7 @@ use bitcoin::{ secp256k1::Keypair, PublicKey, }; +use boltz_client::fees::Fee; pub mod test_utils; @@ -177,7 +178,7 @@ fn bitcoin_v2_submarine() { } // This means the funding transaction was rejected by Boltz for whatever reason, and we need to get - // fund back via refund. + // the funds back via refund. if update.status == "transaction.lockupFailed" || update.status == "invoice.failedToPay" { @@ -192,7 +193,7 @@ fn bitcoin_v2_submarine() { match swap_tx.sign_refund( &our_keys, - 1000, + Fee::Absolute(1000), Some(Cooperative { boltz_api: &boltz_api_v2, swap_id: swap_id.clone(), @@ -210,7 +211,9 @@ fn bitcoin_v2_submarine() { log::info!("Cooperative refund failed. {:?}", e); log::info!("Attempting Non-cooperative refund."); - let tx = swap_tx.sign_refund(&our_keys, 1000, None).unwrap(); + let tx = swap_tx + .sign_refund(&our_keys, Fee::Absolute(1000), None) + .unwrap(); let txid = swap_tx .broadcast(&tx, &ElectrumConfig::default_bitcoin()) .unwrap(); @@ -353,7 +356,7 @@ fn bitcoin_v2_reverse() { .sign_claim( &our_keys, &preimage, - 1000, + Fee::Absolute(1000), Some(Cooperative { boltz_api: &boltz_api_v2, swap_id: swap_id.clone(), @@ -502,7 +505,7 @@ fn bitcoin_v2_reverse_script_path() { .expect("Funding tx expected"); let tx = claim_tx - .sign_claim(&our_keys, &preimage, 1000, None) + .sign_claim(&our_keys, &preimage, Fee::Absolute(1000), None) .unwrap(); claim_tx diff --git a/tests/chain_swaps.rs b/tests/chain_swaps.rs index a4cb66d..a70b42c 100644 --- a/tests/chain_swaps.rs +++ b/tests/chain_swaps.rs @@ -1,10 +1,11 @@ use std::time::Duration; -use bitcoin::{key::rand::thread_rng, Amount, PublicKey}; +use bitcoin::{key::rand::thread_rng, PublicKey}; use boltz_client::boltz::{ BoltzApiClientV2, ChainSwapDetails, Cooperative, CreateChainRequest, Side, Subscription, SwapUpdate, BOLTZ_TESTNET_URL_V2, }; +use boltz_client::fees::Fee; use boltz_client::{ network::{electrum::ElectrumConfig, Chain}, util::{liquid_genesis_hash, secrets::Preimage, setup_logger}, @@ -180,13 +181,14 @@ fn bitcoin_liquid_v2_chain() { .sign_claim( &our_claim_keys, &preimage, - Amount::from_sat(1000), + Fee::Absolute(1000), Some(Cooperative { boltz_api: &boltz_api_v2, swap_id: swap_id.clone(), pub_nonce: Some(pub_nonce), partial_sig: Some(partial_sig), }), + false, ) .unwrap(); @@ -217,7 +219,7 @@ fn bitcoin_liquid_v2_chain() { let tx = refund_tx .sign_refund( &our_refund_keys, - 1000, + Fee::Absolute(1000), Some(Cooperative { boltz_api: &boltz_api_v2, swap_id: swap_id.clone(), @@ -419,7 +421,7 @@ fn liquid_bitcoin_v2_chain() { .sign_claim( &our_claim_keys, &preimage, - 1000, + Fee::Absolute(1000), Some(Cooperative { boltz_api: &boltz_api_v2, swap_id: swap_id.clone(), @@ -456,13 +458,14 @@ fn liquid_bitcoin_v2_chain() { let tx = refund_tx .sign_refund( &our_refund_keys, - Amount::from_sat(1000), + Fee::Absolute(1000), Some(Cooperative { boltz_api: &boltz_api_v2, swap_id: swap_id.clone(), pub_nonce: None, partial_sig: None, }), + false, ) .unwrap(); diff --git a/tests/liquid.rs b/tests/liquid.rs index b73c12f..cc326ce 100644 --- a/tests/liquid.rs +++ b/tests/liquid.rs @@ -18,8 +18,9 @@ use bitcoin::{ hex::{DisplayHex, FromHex}, key::rand::thread_rng, secp256k1::Keypair, - Amount, PublicKey, + PublicKey, }; +use boltz_client::fees::Fee; use elements::encode::serialize; pub mod test_utils; @@ -193,7 +194,7 @@ fn liquid_v2_submarine() { match swap_tx.sign_refund( &our_keys, - Amount::from_sat(1000), + Fee::Absolute(1000), None, // Some(Cooperative { // boltz_api: &boltz_api_v2, @@ -201,6 +202,7 @@ fn liquid_v2_submarine() { // pub_nonce: None, // partial_sig: None, // }), + false, ) { Ok(tx) => { println!("{}", tx.serialize().to_lower_hex_string()); @@ -214,7 +216,7 @@ fn liquid_v2_submarine() { log::info!("Attempting Non-cooperative refund."); let tx = swap_tx - .sign_refund(&our_keys, Amount::from_sat(1000), None) + .sign_refund(&our_keys, Fee::Absolute(1000), None, false) .unwrap(); let txid = swap_tx .broadcast(&tx, &ElectrumConfig::default_liquid(), None) @@ -372,7 +374,7 @@ fn liquid_v2_reverse() { .sign_claim( &our_keys, &preimage, - Amount::from_sat(1000), + Fee::Absolute(1000), None, // Some(Cooperative { // boltz_api: &boltz_api_v2, @@ -380,6 +382,7 @@ fn liquid_v2_reverse() { // pub_nonce: None, // partial_sig: None, // }), + false, ) .unwrap(); @@ -538,7 +541,7 @@ fn liquid_v2_reverse_script_path() { .unwrap(); let tx = claim_tx - .sign_claim(&our_keys, &preimage, Amount::from_sat(1000), None) + .sign_claim(&our_keys, &preimage, Fee::Absolute(1000), None, false) .unwrap(); claim_tx @@ -636,7 +639,7 @@ fn test_recover_liquidv2_refund() { partial_sig: None, }); let signed_tx = rev_swap_tx - .sign_refund(&keypair, Amount::from_sat(absolute_fees), coop) + .sign_refund(&keypair, Fee::Absolute(absolute_fees), coop, false) .unwrap(); let tx_hex = serialize(&signed_tx).to_lower_hex_string(); log::info!("TX_HEX: {}", tx_hex); diff --git a/tests/regtest.rs b/tests/regtest.rs index a425612..6ef5572 100644 --- a/tests/regtest.rs +++ b/tests/regtest.rs @@ -6,14 +6,25 @@ use bitcoin::{Amount, OutPoint, TxOut}; use bitcoind::bitcoincore_rpc::json::ScanTxOutRequest; use bitcoind::bitcoincore_rpc::RpcApi; use boltz_client::boltz::{SwapTxKind, SwapType}; +use boltz_client::fees::Fee; use boltz_client::network::Chain; use boltz_client::util::secrets::Preimage; use boltz_client::{BtcSwapScript, BtcSwapTx, LBtcSwapScript, LBtcSwapTx}; +use elements::Address; + mod test_framework; use test_framework::{BtcTestFramework, LbtcTestFramework}; -#[test] -fn btc_reverse_claim() { +const FUNDING_AMOUNT: u64 = 10_000; + +fn prepare_btc_claim() -> ( + BtcTestFramework, + ScanTxOutRequest, + BtcSwapTx, + Preimage, + Keypair, + Vec<(OutPoint, TxOut)>, +) { // Init test framework and get a test-wallet let test_framework = BtcTestFramework::init(); @@ -46,7 +57,7 @@ fn btc_reverse_claim() { let swap_addrs = swap_script.to_address(Chain::BitcoinRegtest).unwrap(); let spk = swap_addrs.script_pubkey(); println!("spk: {}", spk); - test_framework.send_coins(&swap_addrs, Amount::from_sat(10000)); + test_framework.send_coins(&swap_addrs, Amount::from_sat(FUNDING_AMOUNT)); test_framework.generate_blocks(1); let scan_request = ScanTxOutRequest::Single(format!("addr({})", swap_addrs)); @@ -57,7 +68,7 @@ fn btc_reverse_claim() { .unwrap(); assert_eq!(scan_result.unspents.len(), 1); - assert_eq!(scan_result.total_amount, Amount::from_sat(10000)); + assert_eq!(scan_result.total_amount, Amount::from_sat(FUNDING_AMOUNT)); // Create a refund spending transaction from the swap let utxos: Vec<(OutPoint, TxOut)> = scan_result @@ -83,13 +94,36 @@ fn btc_reverse_claim() { kind: SwapTxKind::Claim, swap_script, output_address: refund_addrs, - utxos, + utxos: utxos.clone(), }; + ( + test_framework, + scan_request, + swap_tx, + preimage, + recvr_keypair, + utxos, + ) +} + +#[test] +fn btc_reverse_claim() { + let (test_framework, scan_request, swap_tx, preimage, recvr_keypair, utxos) = + prepare_btc_claim(); + let test_wallet = test_framework.get_test_wallet(); + + let absolute_fee = 1_000; let claim_tx = swap_tx - .sign_claim(&recvr_keypair, &preimage, 1000, None) + .sign_claim(&recvr_keypair, &preimage, Fee::Absolute(absolute_fee), None) .unwrap(); + let claim_tx_fee = utxos + .iter() + .fold(0, |acc, (_, out)| acc + out.value.to_sat()) + - claim_tx.output[0].value.to_sat(); + assert_eq!(claim_tx_fee, absolute_fee); + test_framework .as_ref() .send_raw_transaction(&claim_tx) @@ -105,12 +139,55 @@ fn btc_reverse_claim() { assert_eq!(scan_result.total_amount, Amount::from_sat(0)); let test_balance = test_wallet.get_balance(None, None).unwrap(); - - assert_eq!(test_balance, Amount::from_sat(19000)); + assert_eq!(test_balance, Amount::from_sat(FUNDING_AMOUNT * 2 - 1_000)); } #[test] -fn btc_submarine_refund() { +fn btc_reverse_claim_relative_fee() { + let (test_framework, scan_request, swap_tx, preimage, recvr_keypair, utxos) = + prepare_btc_claim(); + let test_wallet = test_framework.get_test_wallet(); + + let relative_fee = 1.0; + let claim_tx = swap_tx + .sign_claim(&recvr_keypair, &preimage, Fee::Relative(relative_fee), None) + .unwrap(); + + let claim_tx_fee = utxos + .iter() + .fold(0, |acc, (_, out)| acc + out.value.to_sat()) + - claim_tx.output[0].value.to_sat(); + assert_eq!(relative_fee, claim_tx_fee as f64 / claim_tx.vsize() as f64); + assert_eq!(claim_tx_fee, 140); + + test_framework + .as_ref() + .send_raw_transaction(&claim_tx) + .unwrap(); + test_framework.generate_blocks(1); + + let scan_result = test_framework + .as_ref() + .scan_tx_out_set_blocking(&[scan_request]) + .unwrap(); + + assert_eq!(scan_result.unspents.len(), 0); + assert_eq!(scan_result.total_amount, Amount::from_sat(0)); + + let test_balance = test_wallet.get_balance(None, None).unwrap(); + assert_eq!( + test_balance, + Amount::from_sat(FUNDING_AMOUNT * 2 - claim_tx_fee) + ); +} + +fn prepare_btc_refund() -> ( + BtcTestFramework, + ScanTxOutRequest, + BtcSwapTx, + Keypair, + Vec<(OutPoint, TxOut)>, +) { // Init test framework and get a test-wallet let test_framework = BtcTestFramework::init(); @@ -176,10 +253,27 @@ fn btc_submarine_refund() { kind: SwapTxKind::Refund, swap_script, output_address: refund_addrs, - utxos, + utxos: utxos.clone(), }; - let refund_tx = swap_tx.sign_refund(&sender_keypair, 1000, None).unwrap(); + (test_framework, scan_request, swap_tx, sender_keypair, utxos) +} + +#[test] +fn btc_submarine_refund() { + let (test_framework, scan_request, swap_tx, sender_keypair, utxos) = prepare_btc_refund(); + let test_wallet = test_framework.get_test_wallet(); + + let absolute_fee = 1_000; + let refund_tx = swap_tx + .sign_refund(&sender_keypair, Fee::Absolute(absolute_fee), None) + .unwrap(); + + let refund_tx_fee = utxos + .iter() + .fold(0, |acc, (_, out)| acc + out.value.to_sat()) + - refund_tx.output[0].value.to_sat(); + assert_eq!(refund_tx_fee, absolute_fee); // Make the timelock matured and broadcast the spend test_framework.generate_blocks(100); @@ -199,11 +293,65 @@ fn btc_submarine_refund() { let test_balance = test_wallet.get_balance(None, None).unwrap(); - assert_eq!(test_balance, Amount::from_sat(19000)); + assert_eq!( + test_balance, + Amount::from_sat(FUNDING_AMOUNT * 2 - absolute_fee) + ); } #[test] -fn lbtc_reverse_claim() { +fn btc_submarine_refund_relative_fee() { + let (test_framework, scan_request, swap_tx, sender_keypair, utxos) = prepare_btc_refund(); + let test_wallet = test_framework.get_test_wallet(); + + let relative_fee = 1.0; + let refund_tx = swap_tx + .sign_refund(&sender_keypair, Fee::Relative(relative_fee), None) + .unwrap(); + + let refund_tx_fee = utxos + .iter() + .fold(0, |acc, (_, out)| acc + out.value.to_sat()) + - refund_tx.output[0].value.to_sat(); + assert_eq!( + relative_fee, + refund_tx_fee as f64 / refund_tx.vsize() as f64 + ); + assert_eq!(refund_tx_fee, 126); + + // Make the timelock matured and broadcast the spend + test_framework.generate_blocks(100); + test_framework + .as_ref() + .send_raw_transaction(&refund_tx) + .unwrap(); + test_framework.generate_blocks(1); + + let scan_result = test_framework + .as_ref() + .scan_tx_out_set_blocking(&[scan_request]) + .unwrap(); + + assert_eq!(scan_result.unspents.len(), 0); + assert_eq!(scan_result.total_amount, Amount::from_sat(0)); + + let test_balance = test_wallet.get_balance(None, None).unwrap(); + + assert_eq!( + test_balance, + Amount::from_sat(FUNDING_AMOUNT * 2 - refund_tx_fee) + ); +} + +fn prepare_lbtc_claim() -> ( + LbtcTestFramework, + LBtcSwapTx, + Preimage, + Keypair, + elements::secp256k1_zkp::Keypair, + Address, + (elements::OutPoint, elements::TxOut), +) { // Init test framework and get a test-wallet let test_framework = LbtcTestFramework::init(); @@ -251,23 +399,92 @@ fn lbtc_reverse_claim() { swap_script, output_address: refund_addrs, funding_outpoint: utxo.0, - funding_utxo: utxo.1, + funding_utxo: utxo.1.clone(), genesis_hash, }; + ( + test_framework, + swap_tx, + preimage, + recvr_keypair, + blinding_keypair, + swap_addrs, + utxo, + ) +} + +#[test] +fn lbtc_reverse_claim() { + let (test_framework, swap_tx, preimage, recvr_keypair, blinding_keypair, swap_addrs, utxo) = + prepare_lbtc_claim(); + + let absolute_fee = 1_000; let claim_tx = swap_tx - .sign_claim(&recvr_keypair, &preimage, Amount::from_sat(1000), None) + .sign_claim( + &recvr_keypair, + &preimage, + Fee::Absolute(absolute_fee), + None, + false, + ) .unwrap(); + let secp = Secp256k1::new(); + assert_eq!( + claim_tx.fee_in( + utxo.1 + .unblind(&secp, blinding_keypair.secret_key()) + .unwrap() + .asset + ), + absolute_fee + ); test_framework.send_tx(&claim_tx); - test_framework.generate_blocks(1); assert!(test_framework.fetch_utxo(&swap_addrs).is_none()); } #[test] -fn lbtc_submarine_refund() { +fn lbtc_reverse_claim_relative_fee() { + let (test_framework, swap_tx, preimage, recvr_keypair, blinding_keypair, swap_addrs, utxo) = + prepare_lbtc_claim(); + + let relative_fee = 0.1; + let claim_tx = swap_tx + .sign_claim( + &recvr_keypair, + &preimage, + Fee::Relative(relative_fee), + None, + false, + ) + .unwrap(); + assert_eq!( + claim_tx.fee_in( + utxo.1 + .unblind(&Secp256k1::new(), blinding_keypair.secret_key()) + .unwrap() + .asset + ), + (relative_fee * claim_tx.vsize() as f64).ceil() as u64 + ); + + test_framework.send_tx(&claim_tx); + test_framework.generate_blocks(1); + + assert!(test_framework.fetch_utxo(&swap_addrs).is_none()); +} + +fn prepare_lbtc_refund() -> ( + LbtcTestFramework, + LBtcSwapTx, + Keypair, + Keypair, + Address, + (elements::OutPoint, elements::TxOut), +) { // Init test framework and get a test-wallet let test_framework = LbtcTestFramework::init(); @@ -313,13 +530,65 @@ fn lbtc_submarine_refund() { swap_script, output_address: refund_addrs, funding_outpoint: utxo.0, - funding_utxo: utxo.1, + funding_utxo: utxo.1.clone(), genesis_hash, }; + ( + test_framework, + swap_tx, + sender_keypair, + blinding_keypair, + swap_addrs, + utxo, + ) +} + +#[test] +fn lbtc_submarine_refund() { + let (test_framework, swap_tx, sender_keypair, blinding_keypair, swap_addrs, utxo) = + prepare_lbtc_refund(); + + let absolute_fee = 1_000; + let refund_tx = swap_tx + .sign_refund(&sender_keypair, Fee::Absolute(absolute_fee), None, false) + .unwrap(); + assert_eq!( + refund_tx.fee_in( + utxo.1 + .unblind(&Secp256k1::new(), blinding_keypair.secret_key()) + .unwrap() + .asset + ), + absolute_fee + ); + + // Make the timelock matured and broadcast the spend + test_framework.generate_blocks(100); + test_framework.send_tx(&refund_tx); + test_framework.generate_blocks(1); + + assert!(test_framework.fetch_utxo(&swap_addrs).is_none()); +} + +#[test] +fn lbtc_submarine_refund_relative_fee() { + let (test_framework, swap_tx, sender_keypair, blinding_keypair, swap_addrs, utxo) = + prepare_lbtc_refund(); + + let relative_fee = 0.1; let refund_tx = swap_tx - .sign_refund(&sender_keypair, Amount::from_sat(1000), None) + .sign_refund(&sender_keypair, Fee::Relative(relative_fee), None, false) .unwrap(); + assert_eq!( + refund_tx.fee_in( + utxo.1 + .unblind(&Secp256k1::new(), blinding_keypair.secret_key()) + .unwrap() + .asset + ), + (relative_fee * refund_tx.vsize() as f64).ceil() as u64 + ); // Make the timelock matured and broadcast the spend test_framework.generate_blocks(100);