Adds `cancel`, `refund`, `punish`, `redeem` and `safely-abort` commands to the ASB that can be used to trigger the specific scenario for the swap by ID.pull/452/head
parent
efcd39eeef
commit
4deb96a3c5
@ -0,0 +1,77 @@
|
||||
use crate::bitcoin::{ExpiredTimelocks, Txid, Wallet};
|
||||
use crate::database::{Database, Swap};
|
||||
use crate::protocol::alice::AliceState;
|
||||
use anyhow::{bail, Result};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, thiserror::Error, Clone, Copy)]
|
||||
pub enum Error {
|
||||
#[error("The cancel transaction cannot be published because the cancel timelock has not expired yet. Please try again later")]
|
||||
CancelTimelockNotExpiredYet,
|
||||
}
|
||||
|
||||
pub async fn cancel(
|
||||
swap_id: Uuid,
|
||||
bitcoin_wallet: Arc<Wallet>,
|
||||
db: Arc<Database>,
|
||||
force: bool,
|
||||
) -> Result<Result<(Txid, AliceState), Error>> {
|
||||
let state = db.get_state(swap_id)?.try_into_alice()?.into();
|
||||
|
||||
let (monero_wallet_restore_blockheight, transfer_proof, state3) = match state {
|
||||
|
||||
// In case no XMR has been locked, move to Safely Aborted
|
||||
AliceState::Started { .. }
|
||||
| AliceState::BtcLocked { .. } => bail!("Cannot cancel swap {} because it is in state {} where no XMR was locked.", swap_id, state),
|
||||
|
||||
AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, transfer_proof, state3, }
|
||||
| AliceState::XmrLocked { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||
| AliceState::XmrLockTransferProofSent { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||
// in cancel mode we do not care about the fact that we could redeem, but always wait for cancellation (leading either refund or punish)
|
||||
| AliceState::EncSigLearned { monero_wallet_restore_blockheight, transfer_proof, state3, .. }
|
||||
| AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, transfer_proof, state3} => {
|
||||
(monero_wallet_restore_blockheight, transfer_proof, state3)
|
||||
}
|
||||
|
||||
// The cancel tx was already published, but Alice not yet in final state
|
||||
AliceState::BtcCancelled { .. }
|
||||
| AliceState::BtcRefunded { .. }
|
||||
| AliceState::BtcPunishable { .. }
|
||||
|
||||
// Alice already in final state
|
||||
| AliceState::BtcRedeemed
|
||||
| AliceState::XmrRefunded
|
||||
| AliceState::BtcPunished
|
||||
| AliceState::SafelyAborted => bail!("Cannot cancel swap {} because it is in state {} which is not cancelable", swap_id, state),
|
||||
};
|
||||
|
||||
tracing::info!(%swap_id, "Trying to manually cancel swap");
|
||||
|
||||
if !force {
|
||||
tracing::debug!(%swap_id, "Checking if cancel timelock is expired");
|
||||
|
||||
if let ExpiredTimelocks::None = state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
|
||||
return Ok(Err(Error::CancelTimelockNotExpiredYet));
|
||||
}
|
||||
}
|
||||
|
||||
let txid = if let Ok(tx) = state3.check_for_tx_cancel(bitcoin_wallet.as_ref()).await {
|
||||
let txid = tx.txid();
|
||||
tracing::debug!(%swap_id, "Cancel transaction has already been published: {}", txid);
|
||||
txid
|
||||
} else {
|
||||
state3.submit_tx_cancel(bitcoin_wallet.as_ref()).await?
|
||||
};
|
||||
|
||||
let state = AliceState::BtcCancelled {
|
||||
monero_wallet_restore_blockheight,
|
||||
transfer_proof,
|
||||
state3,
|
||||
};
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, Swap::Alice(db_state))
|
||||
.await?;
|
||||
|
||||
Ok(Ok((txid, state)))
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
use crate::bitcoin::{self};
|
||||
use crate::database::{Database, Swap};
|
||||
use crate::monero;
|
||||
use crate::protocol::alice::AliceState;
|
||||
use anyhow::{bail, Result};
|
||||
use libp2p::PeerId;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
// Errors indicating the the swap can *currently* not be refunded but might be later
|
||||
#[error("Swap is not in a cancelled state. Make sure to cancel the swap before trying to refund or use --force.")]
|
||||
SwapNotCancelled,
|
||||
#[error(
|
||||
"Counterparty {0} did not refund the BTC yet. You can try again later or try to punish."
|
||||
)]
|
||||
RefundTransactionNotPublishedYet(PeerId),
|
||||
|
||||
// Errors indicating that the swap cannot be refunded because because it is in a abort/final
|
||||
// state
|
||||
#[error("Swa is in state {0} where no XMR was locked. Try aborting instead.")]
|
||||
NoXmrLocked(AliceState),
|
||||
#[error("Swap is in state {0} which is not refundable")]
|
||||
SwapNotRefundable(AliceState),
|
||||
}
|
||||
|
||||
pub async fn refund(
|
||||
swap_id: Uuid,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallet>,
|
||||
db: Arc<Database>,
|
||||
force: bool,
|
||||
) -> Result<Result<AliceState, Error>> {
|
||||
let state = db.get_state(swap_id)?.try_into_alice()?.into();
|
||||
|
||||
let (monero_wallet_restore_blockheight, transfer_proof, state3) = if force {
|
||||
match state {
|
||||
|
||||
// In case no XMR has been locked, move to Safely Aborted
|
||||
AliceState::Started { .. }
|
||||
| AliceState::BtcLocked { .. } => bail!(Error::NoXmrLocked(state)),
|
||||
|
||||
// Refund potentially possible (no knowledge of cancel transaction)
|
||||
AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, transfer_proof, state3, }
|
||||
| AliceState::XmrLocked { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||
| AliceState::XmrLockTransferProofSent { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||
| AliceState::EncSigLearned { monero_wallet_restore_blockheight, transfer_proof, state3, .. }
|
||||
| AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||
|
||||
// Refund possible due to cancel transaction already being published
|
||||
| AliceState::BtcCancelled { monero_wallet_restore_blockheight, transfer_proof, state3 }
|
||||
| AliceState::BtcRefunded { monero_wallet_restore_blockheight, transfer_proof, state3, .. }
|
||||
| AliceState::BtcPunishable { monero_wallet_restore_blockheight, transfer_proof, state3, .. } => {
|
||||
(monero_wallet_restore_blockheight, transfer_proof, state3)
|
||||
}
|
||||
|
||||
// Alice already in final state
|
||||
AliceState::BtcRedeemed
|
||||
| AliceState::XmrRefunded
|
||||
| AliceState::BtcPunished
|
||||
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),
|
||||
}
|
||||
} else {
|
||||
match state {
|
||||
AliceState::Started { .. } | AliceState::BtcLocked { .. } => {
|
||||
bail!(Error::NoXmrLocked(state))
|
||||
}
|
||||
|
||||
AliceState::BtcCancelled {
|
||||
monero_wallet_restore_blockheight,
|
||||
transfer_proof,
|
||||
state3,
|
||||
}
|
||||
| AliceState::BtcRefunded {
|
||||
monero_wallet_restore_blockheight,
|
||||
transfer_proof,
|
||||
state3,
|
||||
..
|
||||
}
|
||||
| AliceState::BtcPunishable {
|
||||
monero_wallet_restore_blockheight,
|
||||
transfer_proof,
|
||||
state3,
|
||||
..
|
||||
} => (monero_wallet_restore_blockheight, transfer_proof, state3),
|
||||
|
||||
AliceState::BtcRedeemed
|
||||
| AliceState::XmrRefunded
|
||||
| AliceState::BtcPunished
|
||||
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),
|
||||
|
||||
_ => return Ok(Err(Error::SwapNotCancelled)),
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!(%swap_id, "Trying to manually refund swap");
|
||||
|
||||
let spend_key = if let Ok(published_refund_tx) =
|
||||
state3.fetch_tx_refund(bitcoin_wallet.as_ref()).await
|
||||
{
|
||||
tracing::debug!(%swap_id, "Bitcoin refund transaction found, extracting key to refund Monero");
|
||||
state3.extract_monero_private_key(published_refund_tx)?
|
||||
} else {
|
||||
let bob_peer_id = db.get_peer_id(swap_id)?;
|
||||
return Ok(Err(Error::RefundTransactionNotPublishedYet(bob_peer_id)));
|
||||
};
|
||||
|
||||
state3
|
||||
.refund_xmr(
|
||||
&monero_wallet,
|
||||
monero_wallet_restore_blockheight,
|
||||
swap_id.to_string(),
|
||||
spend_key,
|
||||
transfer_proof,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let state = AliceState::XmrRefunded;
|
||||
let db_state = (&state).into();
|
||||
db.insert_latest_state(swap_id, Swap::Alice(db_state))
|
||||
.await?;
|
||||
|
||||
Ok(Ok(state))
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
pub mod harness;
|
||||
|
||||
use harness::alice_run_until::is_xmr_lock_transaction_sent;
|
||||
use harness::bob_run_until::is_btc_locked;
|
||||
use harness::SlowCancelConfig;
|
||||
use swap::protocol::alice::AliceState;
|
||||
use swap::protocol::bob::BobState;
|
||||
use swap::protocol::{alice, bob};
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_alice_and_bob_manually_cancel_when_timelock_not_expired_errors() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_xmr_lock_transaction_sent));
|
||||
|
||||
let bob_state = bob_swap.await??;
|
||||
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
||||
|
||||
let (bob_swap, bob_join_handle) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
let alice_state = alice_swap.await??;
|
||||
assert!(matches!(
|
||||
alice_state,
|
||||
AliceState::XmrLockTransactionSent { .. }
|
||||
));
|
||||
|
||||
// Bob tries but fails to manually cancel
|
||||
let result = bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false)
|
||||
.await?
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
result,
|
||||
bob::cancel::Error::CancelTimelockNotExpiredYet
|
||||
));
|
||||
|
||||
ctx.restart_alice().await;
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
assert!(matches!(
|
||||
alice_swap.state,
|
||||
AliceState::XmrLockTransactionSent { .. }
|
||||
));
|
||||
|
||||
// Alice tries but fails manual cancel
|
||||
let result = alice::cancel(
|
||||
alice_swap.swap_id,
|
||||
alice_swap.bitcoin_wallet,
|
||||
alice_swap.db,
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
result,
|
||||
alice::cancel::Error::CancelTimelockNotExpiredYet
|
||||
));
|
||||
|
||||
let (bob_swap, bob_join_handle) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
// Bob tries but fails to manually refund
|
||||
let result = bob::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false)
|
||||
.await?
|
||||
.unwrap_err();
|
||||
assert!(matches!(result, bob::refund::SwapNotCancelledYet(_)));
|
||||
|
||||
let (bob_swap, _) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
ctx.restart_alice().await;
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
assert!(matches!(
|
||||
alice_swap.state,
|
||||
AliceState::XmrLockTransactionSent { .. }
|
||||
));
|
||||
|
||||
// Alice tries but fails manual cancel
|
||||
let result = alice::refund(
|
||||
alice_swap.swap_id,
|
||||
alice_swap.bitcoin_wallet,
|
||||
alice_swap.monero_wallet,
|
||||
alice_swap.db,
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap_err();
|
||||
assert!(matches!(result, alice::refund::Error::SwapNotCancelled));
|
||||
|
||||
ctx.restart_alice().await;
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
assert!(matches!(
|
||||
alice_swap.state,
|
||||
AliceState::XmrLockTransactionSent { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
pub mod harness;
|
||||
|
||||
use harness::alice_run_until::is_xmr_lock_transaction_sent;
|
||||
use harness::bob_run_until::is_btc_locked;
|
||||
use harness::SlowCancelConfig;
|
||||
use swap::protocol::alice::AliceState;
|
||||
use swap::protocol::bob::BobState;
|
||||
use swap::protocol::{alice, bob};
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_alice_and_bob_manually_force_cancel_when_timelock_not_expired_errors() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_xmr_lock_transaction_sent));
|
||||
|
||||
let bob_state = bob_swap.await??;
|
||||
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
||||
|
||||
let (bob_swap, bob_join_handle) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
let alice_state = alice_swap.await??;
|
||||
assert!(matches!(
|
||||
alice_state,
|
||||
AliceState::XmrLockTransactionSent { .. }
|
||||
));
|
||||
|
||||
// Bob tries but fails to manually cancel
|
||||
let result = bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, true).await;
|
||||
assert!(matches!(result, Err(_)));
|
||||
|
||||
ctx.restart_alice().await;
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
assert!(matches!(
|
||||
alice_swap.state,
|
||||
AliceState::XmrLockTransactionSent { .. }
|
||||
));
|
||||
|
||||
// Alice tries but fails manual cancel
|
||||
let is_outer_err = alice::cancel(
|
||||
alice_swap.swap_id,
|
||||
alice_swap.bitcoin_wallet,
|
||||
alice_swap.db,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.is_err();
|
||||
assert!(is_outer_err);
|
||||
|
||||
let (bob_swap, bob_join_handle) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
// Bob tries but fails to manually refund
|
||||
let is_outer_err = bob::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, true)
|
||||
.await
|
||||
.is_err();
|
||||
assert!(is_outer_err);
|
||||
|
||||
let (bob_swap, _) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
ctx.restart_alice().await;
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
assert!(matches!(
|
||||
alice_swap.state,
|
||||
AliceState::XmrLockTransactionSent { .. }
|
||||
));
|
||||
|
||||
// Alice tries but fails manual cancel
|
||||
let refund_tx_not_published_yet = alice::refund(
|
||||
alice_swap.swap_id,
|
||||
alice_swap.bitcoin_wallet,
|
||||
alice_swap.monero_wallet,
|
||||
alice_swap.db,
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
refund_tx_not_published_yet,
|
||||
alice::refund::Error::RefundTransactionNotPublishedYet(..)
|
||||
));
|
||||
|
||||
ctx.restart_alice().await;
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
assert!(matches!(
|
||||
alice_swap.state,
|
||||
AliceState::XmrLockTransactionSent { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
pub mod harness;
|
||||
|
||||
use bob::cancel::Error;
|
||||
use harness::bob_run_until::is_btc_locked;
|
||||
use harness::SlowCancelConfig;
|
||||
use swap::protocol::bob::BobState;
|
||||
use swap::protocol::{alice, bob};
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_bob_manually_cancels_when_timelock_not_expired_errors() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
let _ = tokio::spawn(alice::run(alice_swap));
|
||||
|
||||
let bob_state = bob_swap.await??;
|
||||
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
||||
|
||||
let (bob_swap, bob_join_handle) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
// Bob tries but fails to manually cancel
|
||||
let result = bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false)
|
||||
.await?
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(result, Error::CancelTimelockNotExpiredYet));
|
||||
|
||||
let (bob_swap, bob_join_handle) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
// Bob tries but fails to manually refund
|
||||
bob::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false)
|
||||
.await?
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
let (bob_swap, _) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
pub mod harness;
|
||||
|
||||
use harness::bob_run_until::is_btc_locked;
|
||||
use harness::SlowCancelConfig;
|
||||
use swap::protocol::bob::BobState;
|
||||
use swap::protocol::{alice, bob};
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
let _ = tokio::spawn(alice::run(alice_swap));
|
||||
|
||||
let bob_state = bob_swap.await??;
|
||||
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
||||
|
||||
let (bob_swap, bob_join_handle) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
// Bob forces a cancel that will fail
|
||||
let is_error = bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, true)
|
||||
.await
|
||||
.is_err();
|
||||
|
||||
assert!(is_error);
|
||||
|
||||
let (bob_swap, bob_join_handle) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
// Bob forces a refund that will fail
|
||||
let is_error = bob::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, true)
|
||||
.await
|
||||
.is_err();
|
||||
|
||||
assert!(is_error);
|
||||
let (bob_swap, _) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
||||
.await;
|
||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
}
|
Loading…
Reference in new issue