Skip to content

Commit

Permalink
Receive Chain Swap: support refund even when lockup address is re-used (
Browse files Browse the repository at this point in the history
#471)

* rescan_onchain_swaps: separate internal (scheduled) from external (manual) call

* Add TODOs for supporting more utxos in BtcSwapTx::new_refund

* Update boltz-client to build refund tx with all utxos

* list-refundables: show refundable amount, not swap amount

* Chain swap cooperative refund: fix "Liquid chain used for Bitcoin operations" error

* Revert "Chain swap cooperative refund: fix "Liquid chain used for Bitcoin operations" error"

This reverts commit 8a325e3.

* Bump boltz-rust to include sign_refund fix

* Bump boltz-rust to include sign_refund fix for non-coop refund

* Fix state handling when incoming chain swaps are refunded

* Move swap state change inside refund_incoming_swap

* Bump to latest boltz-client branch version
  • Loading branch information
ok300 authored Oct 29, 2024
1 parent 414c9f1 commit 59dfacc
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 33 deletions.
2 changes: 1 addition & 1 deletion cli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 40 additions & 23 deletions lib/core/src/chain_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ impl ChainSwapHandler {
loop {
tokio::select! {
_ = rescan_interval.tick() => {
if let Err(e) = cloned.rescan_incoming_chain_swaps().await {
if let Err(e) = cloned.rescan_incoming_chain_swaps(false).await {
error!("Error checking incoming chain swaps: {e:?}");
}
if let Err(e) = cloned.rescan_outgoing_chain_swaps().await {
Expand Down Expand Up @@ -110,7 +110,10 @@ impl ChainSwapHandler {
}
}

pub(crate) async fn rescan_incoming_chain_swaps(&self) -> Result<()> {
pub(crate) async fn rescan_incoming_chain_swaps(
&self,
ignore_monitoring_block_height: bool,
) -> Result<()> {
let current_height = self.bitcoin_chain_service.lock().await.tip()?.height as u32;
let chain_swaps: Vec<ChainSwap> = self
.persister
Expand All @@ -124,22 +127,34 @@ impl ChainSwapHandler {
current_height
);
for swap in chain_swaps {
if let Err(e) = self.rescan_incoming_chain_swap(&swap, current_height).await {
if let Err(e) = self
.rescan_incoming_chain_swap(&swap, current_height, ignore_monitoring_block_height)
.await
{
error!("Error rescanning incoming Chain Swap {}: {e:?}", swap.id);
}
}
Ok(())
}

/// ### Arguments
/// - `swap`: the swap being rescanned
/// - `current_height`: the tip
/// - `ignore_monitoring_block_height`: if true, it rescans an expired swap even after the
/// cutoff monitoring block height
async fn rescan_incoming_chain_swap(
&self,
swap: &ChainSwap,
current_height: u32,
ignore_monitoring_block_height: bool,
) -> Result<()> {
let monitoring_block_height =
swap.timeout_block_height + CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS;
let is_swap_expired = current_height > swap.timeout_block_height;
let is_monitoring_expired = current_height > monitoring_block_height;
let is_monitoring_expired = match ignore_monitoring_block_height {
true => false,
false => current_height > monitoring_block_height,
};

if (is_swap_expired && !is_monitoring_expired) || swap.state == RefundPending {
let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?;
Expand Down Expand Up @@ -730,7 +745,7 @@ impl ChainSwapHandler {
Ok(())
}

pub async fn prepare_refund(
pub(crate) async fn prepare_refund(
&self,
lockup_address: &str,
refund_address: &str,
Expand Down Expand Up @@ -771,30 +786,25 @@ impl ChainSwapHandler {
.persister
.fetch_chain_swap_by_lockup_address(lockup_address)?
.ok_or(PaymentError::Generic {
err: format!("Swap {} not found", lockup_address),
err: format!("Swap for lockup address {} not found", lockup_address),
})?;
let id = &swap.id;

ensure_sdk!(
swap.state == Refundable,
PaymentError::Generic {
err: format!("Chain Swap {} was not marked as `Refundable`", swap.id)
err: format!("Chain Swap {id} was not marked as `Refundable`")
}
);

ensure_sdk!(
swap.refund_tx_id.is_none(),
PaymentError::Generic {
err: format!(
"A refund tx for incoming Chain Swap {} was already broadcast",
swap.id
)
err: format!("A refund tx for incoming Chain Swap {id} was already broadcast",)
}
);

info!(
"Initiating refund for incoming Chain Swap {}, is_cooperative: {is_cooperative}",
swap.id
);
info!("Initiating refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}",);

let SwapScriptV2::Bitcoin(swap_script) = swap.get_lockup_swap_script()? else {
return Err(PaymentError::Generic {
Expand All @@ -818,18 +828,25 @@ impl ChainSwapHandler {
)?
else {
return Err(PaymentError::Generic {
err: format!(
"Unexpected refund tx type returned for incoming Chain swap {}",
swap.id
),
err: format!("Unexpected refund tx type returned for incoming Chain swap {id}",),
});
};
let refund_tx_id = bitcoin_chain_service.broadcast(&refund_tx)?.to_string();

info!(
"Successfully broadcast refund for incoming Chain Swap {}, is_cooperative: {is_cooperative}",
swap.id
);
info!("Successfully broadcast refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}");

// After refund tx is broadcasted, set the payment state to `RefundPending`. This ensures:
// - the swap is not shown in `list-refundables` anymore
// - the background thread will move it to Failed once the refund tx confirms
self.update_swap_info(
&swap.id,
RefundPending,
None,
None,
None,
Some(&refund_tx_id),
)
.await?;

Ok(refund_tx_id)
}
Expand Down
12 changes: 9 additions & 3 deletions lib/core/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1862,14 +1862,20 @@ impl LiquidSdk {
)
})
.await?;

Ok(RefundResponse { refund_tx_id })
}

/// Rescans all expired chain swaps created from calling [LiquidSdk::receive_onchain] within
/// the monitoring period to check if there are any confirmed funds available to refund.
/// Rescans all expired chain swaps created from calling [LiquidSdk::receive_onchain] to check
/// if there are any confirmed funds available to refund.
///
/// Since it bypasses the monitoring period, this should be called rarely or when the caller
/// expects there is a very old refundable chain swap. Otherwise, for relatively recent swaps
/// (within last [CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS] blocks = ~30 days), calling this
/// is not necessary as it happens automatically in the background.
pub async fn rescan_onchain_swaps(&self) -> SdkResult<()> {
self.chain_swap_handler
.rescan_incoming_chain_swaps()
.rescan_incoming_chain_swaps(true)
.await?;
Ok(())
}
Expand Down
10 changes: 5 additions & 5 deletions lib/core/src/swapper/boltz/bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,17 @@ impl BoltzSwapper {
SdkError::generic("Address network validation failed")
);

let utxo = utxos
.first()
.and_then(|utxo| utxo.as_bitcoin().cloned())
.ok_or(SdkError::generic("No UTXO found"))?;
let utxos = utxos
.iter()
.filter_map(|utxo| utxo.as_bitcoin().cloned())
.collect();

let swap_script = swap.get_lockup_swap_script()?.as_bitcoin_script()?;
let refund_tx = BtcSwapTx {
kind: SwapTxKind::Refund,
swap_script,
output_address: address.assume_checked(),
utxo,
utxos,
};

let refund_keypair = swap.get_refund_keypair()?;
Expand Down

0 comments on commit 59dfacc

Please sign in to comment.