Skip to content

Commit 4625f94

Browse files
committed
feat: Log how long we'll wait until we retry locking Monero / redeeming Bitcoin, add some TODOs
1 parent 8899b07 commit 4625f94

File tree

1 file changed

+104
-90
lines changed

1 file changed

+104
-90
lines changed

swap/src/protocol/alice/swap.rs

+104-90
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::bitcoin::ExpiredTimelocks;
77
use crate::env::Config;
88
use crate::protocol::alice::{AliceState, Swap};
99
use crate::{bitcoin, monero};
10+
use ::bitcoin::consensus::encode::serialize_hex;
1011
use anyhow::{bail, Context, Result};
1112
use tokio::select;
1213
use tokio::time::timeout;
@@ -120,7 +121,7 @@ where
120121
.with_max_interval(Duration::from_secs(60))
121122
.build();
122123

123-
let transfer_proof = backoff::future::retry(backoff, || async {
124+
let transfer_proof = backoff::future::retry_notify(backoff, || async {
124125
// We check the status of the Bitcoin lock transaction
125126
// If the swap is cancelled, there is no need to lock the Monero funds anymore
126127
// because there is no way for the swap to succeed.
@@ -143,21 +144,23 @@ where
143144
"Failed to get Monero wallet block height while trying to lock XMR. We will retry."
144145
)
145146
})
147+
.context("Failed to get Monero wallet block height")
146148
.map_err(backoff::Error::transient)?;
147149

148150
// Lock the Monero
149151
monero_wallet
150152
.transfer(state3.lock_xmr_transfer_request())
151153
.await
152154
.map(|proof| Some((monero_wallet_restore_blockheight, proof)))
153-
.inspect_err(|e| {
154-
tracing::warn!(
155-
swap_id = %swap_id,
156-
error = ?e,
157-
"Failed to lock Monero. Make sure your monero-wallet-rpc is connected to a synced daemon and enough funds are available. We will retry."
158-
)
159-
})
155+
.context("Failed to transfer Monero. Make sure your monero-wallet-rpc is connected to a synced daemon and enough funds are available.")
160156
.map_err(backoff::Error::transient)
157+
}, |e, wait_time: Duration| {
158+
tracing::warn!(
159+
swap_id = %swap_id,
160+
error = ?e,
161+
"Failed to lock Monero. We will retry in {} seconds",
162+
wait_time.as_secs_f64()
163+
)
161164
})
162165
.await?;
163166

@@ -275,97 +278,104 @@ where
275278
transfer_proof,
276279
encrypted_signature,
277280
state3,
278-
} => match state3.expired_timelocks(bitcoin_wallet).await? {
279-
ExpiredTimelocks::None { .. } => {
280-
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
281-
match state3.signed_redeem_transaction(*encrypted_signature) {
282-
Ok(tx) => {
283-
// We will retry indefinitely to publish the redeem transaction, until the cancel timelock expires
284-
// We might not be able to publish the redeem transaction on the first try due to any number of reasons
285-
let backoff = backoff::ExponentialBackoffBuilder::new()
286-
.with_max_elapsed_time(None)
287-
.with_max_interval(Duration::from_secs(60))
288-
.build();
289-
290-
match backoff::future::retry(backoff, || async {
291-
// If the cancel timelock is expired, we do not need to publish anymore
292-
// We cannot use a tokio::select! here because this is not cancellation safe
293-
if !matches!(
294-
state3.expired_timelocks(bitcoin_wallet).await?,
295-
ExpiredTimelocks::None { .. }
296-
) {
297-
return Ok(None);
298-
}
299-
300-
bitcoin_wallet
301-
.broadcast(tx.clone(), "redeem")
302-
.await
303-
.inspect_err(|e| {
304-
tracing::warn!(
305-
swap_id = %swap_id,
306-
error = ?e,
307-
"Failed to broadcast Bitcoin redeem transaction. We will retry."
308-
)
309-
})
310-
.map(Some)
311-
.map_err(backoff::Error::transient)
312-
})
313-
.await
314-
{
315-
// We successfully published the redeem transaction
316-
// We wait until we see the transaction in the mempool before transitioning to the next state
317-
Ok(Some((_, subscription))) => match subscription.wait_until_seen().await {
318-
Ok(_) => AliceState::BtcRedeemTransactionPublished { state3 },
319-
Err(e) => {
320-
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)
281+
} => {
282+
match state3.expired_timelocks(bitcoin_wallet).await? {
283+
ExpiredTimelocks::None { .. } => {
284+
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
285+
match state3.signed_redeem_transaction(*encrypted_signature) {
286+
Ok(tx) => {
287+
// We will retry indefinitely to publish the redeem transaction, until the cancel timelock expires
288+
// We might not be able to publish the redeem transaction on the first try due to any number of reasons
289+
let backoff = backoff::ExponentialBackoffBuilder::new()
290+
.with_max_elapsed_time(None)
291+
.with_max_interval(Duration::from_secs(60))
292+
.build();
293+
294+
match backoff::future::retry_notify(backoff.clone(), || async {
295+
// If the cancel timelock is expired, we do not need to publish anymore
296+
// We cannot use a tokio::select! here because this is not cancellation safe
297+
if !matches!(
298+
state3.expired_timelocks(bitcoin_wallet).await?,
299+
ExpiredTimelocks::None { .. }
300+
) {
301+
return Ok(None);
321302
}
322-
},
323-
324-
// Cancel timelock expired before we could publish the redeem transaction
325-
Ok(None) => {
326-
tracing::error!("We were unable to publish the redeem transaction before the timelock expired.");
327303

328-
AliceState::CancelTimelockExpired {
329-
monero_wallet_restore_blockheight,
330-
transfer_proof,
331-
state3,
304+
bitcoin_wallet
305+
.broadcast(tx.clone(), "redeem")
306+
.await
307+
.map(Some)
308+
.map_err(backoff::Error::transient)
309+
}, |e, wait_time: Duration| {
310+
tracing::warn!(
311+
swap_id = %swap_id,
312+
error = ?e,
313+
"Failed to broadcast Bitcoin redeem transaction. We will retry in {} seconds",
314+
wait_time.as_secs_f64()
315+
)
316+
})
317+
.await
318+
{
319+
// We successfully published the redeem transaction
320+
// We wait until we see the transaction in the mempool before transitioning to the next state
321+
Ok(Some((_, subscription))) => match subscription.wait_until_seen().await {
322+
Ok(_) => AliceState::BtcRedeemTransactionPublished { state3 },
323+
Err(e) => {
324+
// We extract the txid and the hex representation of the transaction
325+
// this'll allow the user to manually re-publish the transaction
326+
let txid = tx.txid();
327+
let tx_hex = serialize_hex(&tx);
328+
329+
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 might be screwed. You can try to manually re-publish the transaction (TxID: {}, Tx Hex: {})", e, txid, tx_hex)
330+
}
331+
},
332+
333+
// Cancel timelock expired before we could publish the redeem transaction
334+
Ok(None) => {
335+
tracing::error!("We were unable to publish the redeem transaction before the timelock expired.");
336+
337+
AliceState::CancelTimelockExpired {
338+
monero_wallet_restore_blockheight,
339+
transfer_proof,
340+
state3,
341+
}
332342
}
333-
}
334343

335-
// We should never reach this because we retry indefinitely
336-
Err(error) => {
337-
unreachable!(
338-
"We construct the backoff without a max_elapsed_time. We should never error while retrying to publish the redeem transaction: {:#}",
339-
error
340-
)
344+
// We should never reach this because we retry indefinitely
345+
Err(error) => {
346+
unreachable!(
347+
"We construct the backoff without a max_elapsed_time. We should never error while retrying to publish the redeem transaction: {:#}",
348+
error
349+
)
350+
}
341351
}
342352
}
343-
}
344-
Err(error) => {
345-
tracing::error!("Failed to construct redeem transaction: {:#}", error);
346-
tracing::info!(
347-
timelock = %state3.cancel_timelock,
348-
"Waiting for cancellation timelock to expire",
349-
);
350-
351-
tx_lock_status
352-
.wait_until_confirmed_with(state3.cancel_timelock)
353-
.await?;
354-
355-
AliceState::CancelTimelockExpired {
356-
monero_wallet_restore_blockheight,
357-
transfer_proof,
358-
state3,
353+
Err(error) => {
354+
tracing::error!("Failed to construct redeem transaction: {:#}", error);
355+
tracing::info!(
356+
timelock = %state3.cancel_timelock,
357+
"Waiting for cancellation timelock to expire",
358+
);
359+
360+
tx_lock_status
361+
.wait_until_confirmed_with(state3.cancel_timelock)
362+
.await?;
363+
364+
AliceState::CancelTimelockExpired {
365+
monero_wallet_restore_blockheight,
366+
transfer_proof,
367+
state3,
368+
}
359369
}
360370
}
361371
}
372+
_ => AliceState::CancelTimelockExpired {
373+
monero_wallet_restore_blockheight,
374+
transfer_proof,
375+
state3,
376+
},
362377
}
363-
_ => AliceState::CancelTimelockExpired {
364-
monero_wallet_restore_blockheight,
365-
transfer_proof,
366-
state3,
367-
},
368-
},
378+
}
369379
AliceState::BtcRedeemTransactionPublished { state3 } => {
370380
let subscription = bitcoin_wallet.subscribe_to(state3.tx_redeem()).await;
371381

@@ -456,6 +466,9 @@ where
456466
transfer_proof,
457467
state3,
458468
} => {
469+
// TODO: We should retry indefinitely here until we find the refund transaction
470+
// TODO: If we crash while we are waiting for the punish_tx to be confirmed (punish_btc waits until confirmation), we will remain in this state forever because we will attempt to re-publish the punish transaction
471+
// as soon as we restart which will fail because it has already been included
459472
let punish = state3.punish_btc(bitcoin_wallet).await;
460473

461474
match punish {
@@ -474,7 +487,8 @@ where
474487

475488
let published_refund_tx = bitcoin_wallet
476489
.get_raw_transaction(state3.tx_refund().txid())
477-
.await?;
490+
.await
491+
.context("Failed to fetch refund transaction after assuming it was included because the punish transaction failed")?;
478492

479493
let spend_key = state3.extract_monero_private_key(published_refund_tx)?;
480494

0 commit comments

Comments
 (0)