Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(asb): Retry publishing Bitcoin redeem transaction #221

Merged
merged 8 commits into from
Dec 4, 2024
81 changes: 61 additions & 20 deletions swap/src/protocol/alice/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ where
// TODO: We should already listen for the encrypted signature here.
//
// If we send Bob the transfer proof, but for whatever reason we do not receive an acknoledgement from him
// we would be stuck in this state forever (deadlock). By listening for the encrypted signature here we
// we would be stuck in this state forever until the timelock expires. By listening for the encrypted signature here we
// can still proceed to the next state even if Bob does not respond with an acknoledgement.
result = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock) => {
result?;
Expand Down Expand Up @@ -279,27 +279,68 @@ where
ExpiredTimelocks::None { .. } => {
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
match state3.signed_redeem_transaction(*encrypted_signature) {
// TODO: We should retry publishing the redeem transaction if it fails
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
Ok((_, subscription)) => match subscription.wait_until_seen().await {
Ok(_) => AliceState::BtcRedeemTransactionPublished { state3 },
Err(e) => {
bail!("Waiting for Bitcoin redeem transaction to be in mempool failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
Ok(tx) => {
// We will retry indefinitely to publish the redeem transaction, until the cancel timelock expires
// We might not be able to publish the redeem transaction on the first try due to any number of reasons
let backoff = backoff::ExponentialBackoffBuilder::new()
.with_max_elapsed_time(None)
.with_max_interval(Duration::from_secs(60))
.build();

match backoff::future::retry(backoff, || async {
// If the cancel timelock is expired, we do not need to publish anymore
// We cannot use a tokio::select! here because this is not cancellation safe
if !matches!(
state3.expired_timelocks(bitcoin_wallet).await?,
ExpiredTimelocks::None { .. }
) {
binarybaron marked this conversation as resolved.
Show resolved Hide resolved
return Ok(None);
}
},
Err(error) => {
tracing::error!("Failed to publish redeem transaction: {:#}", error);
tx_lock_status
.wait_until_confirmed_with(state3.cancel_timelock)
.await?;

AliceState::CancelTimelockExpired {
monero_wallet_restore_blockheight,
transfer_proof,
state3,

bitcoin_wallet
.broadcast(tx.clone(), "redeem")
.await
.inspect_err(|e| {
tracing::warn!(
swap_id = %swap_id,
error = ?e,
"Failed to broadcast Bitcoin redeem transaction. We will retry."
binarybaron marked this conversation as resolved.
Show resolved Hide resolved
)
})
.map(Some)
.map_err(backoff::Error::transient)
})
.await
{
// We successfully published the redeem transaction
// We wait until we see the transaction in the mempool before transitioning to the next state
Ok(Some((_, subscription))) => match subscription.wait_until_seen().await {
Ok(_) => AliceState::BtcRedeemTransactionPublished { state3 },
Err(e) => {
bail!("Waiting for Bitcoin redeem transaction to be in mempool failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
binarybaron marked this conversation as resolved.
Show resolved Hide resolved
}
},

// Cancel timelock expired before we could publish the redeem transaction
Ok(None) => {
tracing::error!("We were unable to publish the redeem transaction before the timelock expired.");

AliceState::CancelTimelockExpired {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
}
}

// We should never reach this because we retry indefinitely
Err(error) => {
unreachable!(
"We construct the backoff without a max_elapsed_time. We should never error while retrying to publish the redeem transaction: {:#}",
error
)
}
}
},
}
Err(error) => {
tracing::error!("Failed to construct redeem transaction: {:#}", error);
tracing::info!(
Expand Down Expand Up @@ -347,7 +388,7 @@ where
// gets published once the cancel timelock expires.
if let Err(e) = state3.submit_tx_cancel(bitcoin_wallet).await {
tracing::debug!(
"Assuming cancel transaction is already broadcasted because: {:#}",
"Assuming cancel transaction is already broadcasted because we failed to publish: {:#}",
e
)
}
Expand Down
Loading