ASB manual recovery commands

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.
This commit is contained in:
Daniel Karzel 2021-04-27 14:51:53 +10:00
parent efcd39eeef
commit 4deb96a3c5
No known key found for this signature in database
GPG Key ID: 30C3FC2E438ADB6E
16 changed files with 601 additions and 143 deletions

View File

@ -100,9 +100,9 @@ jobs:
happy_path_restart_bob_after_xmr_locked, happy_path_restart_bob_after_xmr_locked,
happy_path_restart_bob_before_xmr_locked, happy_path_restart_bob_before_xmr_locked,
happy_path_restart_alice_after_xmr_locked, happy_path_restart_alice_after_xmr_locked,
bob_refunds_using_cancel_and_refund_command, alice_and_bob_refund_using_cancel_and_refund_command,
bob_refunds_using_cancel_and_refund_command_timelock_not_expired, alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired,
bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force, alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force,
punish, punish,
alice_punishes_after_restart_punish_timelock_expired, alice_punishes_after_restart_punish_timelock_expired,
alice_refunds_after_restart_bob_refunded, alice_refunds_after_restart_bob_refunded,

View File

@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Cancel command for the ASB that allows cancelling a specific swap by id.
Using the cancel command requires the cancel timelock to be expired, but `--force` can be used to circumvent this check.
- Refund command for the ASB that allows refunding a specific swap by id.
Using the refund command to refund the XMR locked by the ASB requires the CLI to first refund the BTC of the swap.
If the BTC was not refunded yet the command will print an error accordingly.
The command has a `--force` flag that allows executing the command without checking for cancel constraints.
- Resume-only mode for the ASB. - Resume-only mode for the ASB.
When started with `--resume-only` the ASB does not accept new, incoming swap requests but only finishes swaps that are resumed upon startup. When started with `--resume-only` the ASB does not accept new, incoming swap requests but only finishes swaps that are resumed upon startup.

View File

@ -10,9 +10,9 @@ status = [
"docker_tests (happy_path_restart_bob_after_xmr_locked)", "docker_tests (happy_path_restart_bob_after_xmr_locked)",
"docker_tests (happy_path_restart_alice_after_xmr_locked)", "docker_tests (happy_path_restart_alice_after_xmr_locked)",
"docker_tests (happy_path_restart_bob_before_xmr_locked)", "docker_tests (happy_path_restart_bob_before_xmr_locked)",
"docker_tests (bob_refunds_using_cancel_and_refund_command)", "docker_tests (alice_and_bob_refund_using_cancel_and_refund_command)",
"docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force)", "docker_tests (alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired)",
"docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired)", "docker_tests (alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force)",
"docker_tests (punish)", "docker_tests (punish)",
"docker_tests (alice_punishes_after_restart_punish_timelock_expired)", "docker_tests (alice_punishes_after_restart_punish_timelock_expired)",
"docker_tests (alice_refunds_after_restart_bob_refunded)", "docker_tests (alice_refunds_after_restart_bob_refunded)",

View File

@ -3,6 +3,7 @@ use bitcoin::util::amount::ParseAmountError;
use bitcoin::{Address, Denomination}; use bitcoin::{Address, Denomination};
use rust_decimal::Decimal; use rust_decimal::Decimal;
use std::path::PathBuf; use std::path::PathBuf;
use uuid::Uuid;
#[derive(structopt::StructOpt, Debug)] #[derive(structopt::StructOpt, Debug)]
#[structopt( #[structopt(
@ -58,6 +59,42 @@ pub enum Command {
about = "Prints the Bitcoin and Monero balance. Requires the monero-wallet-rpc to be running." about = "Prints the Bitcoin and Monero balance. Requires the monero-wallet-rpc to be running."
)] )]
Balance, Balance,
#[structopt(about = "Contains sub-commands for recovering a swap manually.")]
ManualRecovery(ManualRecovery),
}
#[derive(structopt::StructOpt, Debug)]
pub enum ManualRecovery {
#[structopt(
about = "Publishes the Bitcoin cancel transaction. By default, the cancel timelock will be enforced. A confirmed cancel transaction enables refund and punish."
)]
Cancel {
#[structopt(flatten)]
cancel_params: RecoverCommandParams,
},
#[structopt(
about = "Publishes the Monero refund transaction. By default, a swap-state where the cancel transaction was already published will be enforced. This command requires the counterparty Bitcoin refund transaction and will error if it was not published yet. "
)]
Refund {
#[structopt(flatten)]
refund_params: RecoverCommandParams,
},
}
#[derive(structopt::StructOpt, Debug)]
pub struct RecoverCommandParams {
#[structopt(
long = "swap-id",
help = "The swap id can be retrieved using the history subcommand"
)]
pub swap_id: Uuid,
#[structopt(
short,
long,
help = "Circumvents certain checks when recovering. It is recommended to run a recovery command without --force first to see what is returned."
)]
pub force: bool,
} }
fn parse_btc(s: &str) -> Result<Amount, ParseAmountError> { fn parse_btc(s: &str) -> Result<Amount, ParseAmountError> {

View File

@ -20,7 +20,7 @@ use prettytable::{row, Table};
use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc; use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
use swap::asb::command::{Arguments, Command}; use swap::asb::command::{Arguments, Command, ManualRecovery, RecoverCommandParams};
use swap::asb::config::{ use swap::asb::config::{
default_config_path, initial_setup, query_user_for_initial_testnet_config, read_config, Config, default_config_path, initial_setup, query_user_for_initial_testnet_config, read_config, Config,
ConfigNotInitialized, ConfigNotInitialized,
@ -29,6 +29,7 @@ use swap::database::Database;
use swap::env::GetConfig; use swap::env::GetConfig;
use swap::monero::Amount; use swap::monero::Amount;
use swap::network::swarm; use swap::network::swarm;
use swap::protocol::alice;
use swap::protocol::alice::event_loop::KrakenRate; use swap::protocol::alice::event_loop::KrakenRate;
use swap::protocol::alice::{run, EventLoop}; use swap::protocol::alice::{run, EventLoop};
use swap::seed::Seed; use swap::seed::Seed;
@ -205,6 +206,33 @@ async fn main() -> Result<()> {
tracing::info!("Current balance: {}, {}", bitcoin_balance, monero_balance); tracing::info!("Current balance: {}, {}", bitcoin_balance, monero_balance);
} }
Command::ManualRecovery(ManualRecovery::Cancel {
cancel_params: RecoverCommandParams { swap_id, force },
}) => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let (txid, _) =
alice::cancel(swap_id, Arc::new(bitcoin_wallet), Arc::new(db), force).await??;
tracing::info!("Cancel transaction successfully published with id {}", txid);
}
Command::ManualRecovery(ManualRecovery::Refund {
refund_params: RecoverCommandParams { swap_id, force },
}) => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let monero_wallet = init_monero_wallet(&config, env_config).await?;
alice::refund(
swap_id,
Arc::new(bitcoin_wallet),
Arc::new(monero_wallet),
Arc::new(db),
force,
)
.await??;
tracing::info!("Monero successfully refunded");
}
}; };
Ok(()) Ok(())

View File

@ -7,13 +7,17 @@ use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
pub use self::behaviour::{Behaviour, OutEvent}; pub use self::behaviour::{Behaviour, OutEvent};
pub use self::cancel::cancel;
pub use self::event_loop::{EventLoop, EventLoopHandle}; pub use self::event_loop::{EventLoop, EventLoopHandle};
pub use self::refund::refund;
pub use self::state::*; pub use self::state::*;
pub use self::swap::{run, run_until}; pub use self::swap::{run, run_until};
mod behaviour; mod behaviour;
pub mod cancel;
pub mod event_loop; pub mod event_loop;
mod execution_setup; mod execution_setup;
pub mod refund;
mod spot_price; mod spot_price;
pub mod state; pub mod state;
pub mod swap; pub mod swap;

View File

@ -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)))
}

View File

@ -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))
}

View File

@ -1,5 +1,6 @@
use crate::bitcoin::{ use crate::bitcoin::{
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
TxPunish, TxRefund, Txid,
}; };
use crate::env::Config; use crate::env::Config;
use crate::monero::wallet::{TransferRequest, WatchRequest}; use crate::monero::wallet::{TransferRequest, WatchRequest};
@ -460,6 +461,55 @@ impl State3 {
) )
} }
pub async fn check_for_tx_cancel(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<Transaction> {
let tx_cancel = self.tx_cancel();
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
Ok(tx)
}
pub async fn fetch_tx_refund(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Transaction> {
let tx_refund = self.tx_refund();
let tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
Ok(tx)
}
pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
let transaction = self.signed_cancel_transaction()?;
let (tx_id, _) = bitcoin_wallet.broadcast(transaction, "cancel").await?;
Ok(tx_id)
}
pub async fn refund_xmr(
&self,
monero_wallet: &monero::Wallet,
monero_wallet_restore_blockheight: BlockHeight,
file_name: String,
spend_key: monero::PrivateKey,
transfer_proof: TransferProof,
) -> Result<()> {
let view_key = self.v;
// Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations
// on the lock transaction
monero_wallet
.watch_for_transfer(self.lock_xmr_watch_request(transfer_proof, 10))
.await?;
monero_wallet
.create_from(
file_name,
spend_key,
view_key,
monero_wallet_restore_blockheight,
)
.await?;
Ok(())
}
pub fn signed_redeem_transaction( pub fn signed_redeem_transaction(
&self, &self,
sig: bitcoin::EncryptedSignature, sig: bitcoin::EncryptedSignature,

View File

@ -229,23 +229,17 @@ async fn next_state(
transfer_proof, transfer_proof,
state3, state3,
} => { } => {
let transaction = state3.signed_cancel_transaction()?; if state3.check_for_tx_cancel(bitcoin_wallet).await.is_err() {
// If Bob hasn't yet broadcasted the cancel transaction, Alice has to publish it
// If Bob hasn't yet broadcasted the tx cancel, we do it // to be able to eventually punish. Since the punish timelock is
if bitcoin_wallet // relative to the publication of the cancel transaction we have to ensure it
.get_raw_transaction(transaction.txid()) // gets published once the cancel timelock expires.
.await if let Err(e) = state3.submit_tx_cancel(bitcoin_wallet).await {
.is_err()
{
if let Err(e) = bitcoin_wallet.broadcast(transaction, "cancel").await {
tracing::debug!( tracing::debug!(
"Assuming transaction is already broadcasted because: {:#}", "Assuming cancel transaction is already broadcasted because: {:#}",
e e
) )
} }
// TODO(Franck): Wait until transaction is mined and
// returned mined block height
} }
AliceState::BtcCancelled { AliceState::BtcCancelled {
@ -291,20 +285,13 @@ async fn next_state(
spend_key, spend_key,
state3, state3,
} => { } => {
let view_key = state3.v; state3
.refund_xmr(
// Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations monero_wallet,
// on the lock transaction monero_wallet_restore_blockheight,
monero_wallet
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof, 10))
.await?;
monero_wallet
.create_from(
swap_id.to_string(), swap_id.to_string(),
spend_key, spend_key,
view_key, transfer_proof,
monero_wallet_restore_blockheight,
) )
.await?; .await?;

View File

@ -7,7 +7,7 @@ use uuid::Uuid;
#[derive(thiserror::Error, Debug, Clone, Copy)] #[derive(thiserror::Error, Debug, Clone, Copy)]
#[error("Cannot refund because swap {0} was not cancelled yet. Make sure to cancel the swap before trying to refund.")] #[error("Cannot refund because swap {0} was not cancelled yet. Make sure to cancel the swap before trying to refund.")]
pub struct SwapNotCancelledYet(Uuid); pub struct SwapNotCancelledYet(pub Uuid);
pub async fn refund( pub async fn refund(
swap_id: Uuid, swap_id: Uuid,

View File

@ -1,23 +1,31 @@
pub mod harness; pub mod harness;
use harness::alice_run_until::is_xmr_lock_transaction_sent;
use harness::bob_run_until::is_btc_locked; use harness::bob_run_until::is_btc_locked;
use harness::FastCancelConfig; use harness::FastCancelConfig;
use swap::protocol::alice::AliceState;
use swap::protocol::bob::BobState; use swap::protocol::bob::BobState;
use swap::protocol::{alice, bob}; use swap::protocol::{alice, bob};
#[tokio::test] #[tokio::test]
async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { async fn given_alice_and_bob_manually_refund_after_funds_locked_both_refund() {
harness::setup_test(FastCancelConfig, |mut ctx| async move { harness::setup_test(FastCancelConfig, |mut ctx| async move {
let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
let bob_swap_id = bob_swap.id; let bob_swap_id = bob_swap.id;
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
let alice_swap = ctx.alice_next_swap().await; let alice_swap = ctx.alice_next_swap().await;
let alice_swap = tokio::spawn(alice::run(alice_swap)); let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_xmr_lock_transaction_sent));
let bob_state = bob_swap.await??; let bob_state = bob_swap.await??;
assert!(matches!(bob_state, BobState::BtcLocked { .. })); assert!(matches!(bob_state, BobState::BtcLocked { .. }));
let alice_state = alice_swap.await??;
assert!(matches!(
alice_state,
AliceState::XmrLockTransactionSent { .. }
));
let (bob_swap, bob_join_handle) = ctx let (bob_swap, bob_join_handle) = ctx
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id) .stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
.await; .await;
@ -52,7 +60,36 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
ctx.assert_bob_refunded(bob_state).await; ctx.assert_bob_refunded(bob_state).await;
let alice_state = alice_swap.await??; // manually cancel ALice's swap (effectively just notice that Bob already
// cancelled and record that)
ctx.restart_alice().await;
let alice_swap = ctx.alice_next_swap().await;
assert!(matches!(
alice_swap.state,
AliceState::XmrLockTransactionSent { .. }
));
alice::cancel(
alice_swap.swap_id,
alice_swap.bitcoin_wallet,
alice_swap.db,
false,
)
.await??;
// manually refund ALice's swap
ctx.restart_alice().await;
let alice_swap = ctx.alice_next_swap().await;
assert!(matches!(alice_swap.state, AliceState::BtcCancelled { .. }));
let alice_state = alice::refund(
alice_swap.swap_id,
alice_swap.bitcoin_wallet,
alice_swap.monero_wallet,
alice_swap.db,
false,
)
.await??;
ctx.assert_alice_refunded(alice_state).await; ctx.assert_alice_refunded(alice_state).await;
Ok(()) Ok(())

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}