From c930ad84a4c3e504beb1da256190cff3e3d67570 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Mon, 1 Feb 2021 22:32:54 +1100 Subject: [PATCH] Add --force flag for cancel and refund --- .github/workflows/ci.yml | 1 + swap/src/cli.rs | 6 +++ swap/src/main.rs | 13 ++++- swap/src/protocol/bob/cancel.rs | 27 +++++----- swap/src/protocol/bob/refund.rs | 27 ++++++++-- ...refunds_using_cancel_and_refund_command.rs | 2 + ..._and_refund_command_timelock_not_exired.rs | 10 ++-- ...efund_command_timelock_not_exired_force.rs | 54 +++++++++++++++++++ 8 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_exired_force.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3dbb7ab..a46ef1f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,6 +136,7 @@ jobs: refund_restart_alice, bob_refunds_using_cancel_and_refund_command, bob_refunds_using_cancel_and_refund_command_timelock_not_exired, + bob_refunds_using_cancel_and_refund_command_timelock_not_exired_force, ] runs-on: ubuntu-latest steps: diff --git a/swap/src/cli.rs b/swap/src/cli.rs index 6fa8d56b..16f80f2b 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -96,6 +96,9 @@ pub enum Cancel { #[structopt(flatten)] config: Config, + + #[structopt(short, long)] + force: bool, }, } @@ -114,6 +117,9 @@ pub enum Refund { #[structopt(flatten)] config: Config, + + #[structopt(short, long)] + force: bool, }, } diff --git a/swap/src/main.rs b/swap/src/main.rs index 0b7573d6..71798092 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -215,6 +215,7 @@ async fn main() -> Result<()> { alice_peer_id, alice_addr, config, + force, }) => { // TODO: Optimization: Only init the Bitcoin wallet, Monero wallet unnecessary let (bitcoin_wallet, monero_wallet) = @@ -234,7 +235,15 @@ async fn main() -> Result<()> { tokio::spawn(async move { event_loop.run().await }); - match bob::cancel(swap.swap_id, swap.state, swap.bitcoin_wallet, swap.db).await? { + match bob::cancel( + swap.swap_id, + swap.state, + swap.bitcoin_wallet, + swap.db, + force, + ) + .await? + { Ok((txid, _)) => { info!("Cancel transaction successfully published with id {}", txid) } @@ -251,6 +260,7 @@ async fn main() -> Result<()> { alice_peer_id, alice_addr, config, + force, }) => { let (bitcoin_wallet, monero_wallet) = init_wallets(config.path, bitcoin_network, monero_network).await?; @@ -275,6 +285,7 @@ async fn main() -> Result<()> { swap.execution_params, swap.bitcoin_wallet, swap.db, + force, ) .await??; } diff --git a/swap/src/protocol/bob/cancel.rs b/swap/src/protocol/bob/cancel.rs index 5cff9710..0c1cb0fe 100644 --- a/swap/src/protocol/bob/cancel.rs +++ b/swap/src/protocol/bob/cancel.rs @@ -20,6 +20,7 @@ pub async fn cancel( state: BobState, bitcoin_wallet: Arc, db: Database, + force: bool, ) -> Result> { let state4 = match state { BobState::BtcLocked(state3) => state3.state4(), @@ -34,20 +35,22 @@ pub async fn cancel( ), }; - if let ExpiredTimelocks::None = state4.expired_timelock(bitcoin_wallet.as_ref()).await? { - return Ok(Err(CancelError::CancelTimelockNotExpiredYet)); - } + if !force { + if let ExpiredTimelocks::None = state4.expired_timelock(bitcoin_wallet.as_ref()).await? { + return Ok(Err(CancelError::CancelTimelockNotExpiredYet)); + } - if state4 - .check_for_tx_cancel(bitcoin_wallet.as_ref()) - .await - .is_ok() - { - let state = BobState::BtcCancelled(state4); - let db_state = state.into(); - db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; + if state4 + .check_for_tx_cancel(bitcoin_wallet.as_ref()) + .await + .is_ok() + { + let state = BobState::BtcCancelled(state4); + let db_state = state.into(); + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; - return Ok(Err(CancelError::CancelTxAlreadyPublished)); + return Ok(Err(CancelError::CancelTxAlreadyPublished)); + } } let txid = state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; diff --git a/swap/src/protocol/bob/refund.rs b/swap/src/protocol/bob/refund.rs index 88331516..b341dad2 100644 --- a/swap/src/protocol/bob/refund.rs +++ b/swap/src/protocol/bob/refund.rs @@ -4,7 +4,7 @@ use crate::{ execution_params::ExecutionParams, protocol::bob::BobState, }; -use anyhow::Result; +use anyhow::{bail, Result}; use std::sync::Arc; use uuid::Uuid; @@ -18,11 +18,28 @@ pub async fn refund( execution_params: ExecutionParams, bitcoin_wallet: Arc, db: Database, + force: bool, ) -> Result> { - let state4 = match state { - BobState::BtcCancelled(state4) => state4, - _ => { - return Ok(Err(SwapNotCancelledYet(swap_id))); + let state4 = if force { + match state { + BobState::BtcLocked(state3) => state3.state4(), + BobState::XmrLockProofReceived { state, .. } => state.state4(), + BobState::XmrLocked(state4) => state4, + BobState::EncSigSent(state4) => state4, + BobState::CancelTimelockExpired(state4) => state4, + BobState::BtcCancelled(state4) => state4, + _ => bail!( + "Cannot refund swap {} because it is in state {} which is not refundable.", + swap_id, + state + ), + } + } else { + match state { + BobState::BtcCancelled(state4) => state4, + _ => { + return Ok(Err(SwapNotCancelledYet(swap_id))); + } } }; diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs index 854fbe33..9b64d826 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs @@ -34,6 +34,7 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { bob_swap.state, bob_swap.bitcoin_wallet, bob_swap.db, + false, ) .await .unwrap() @@ -50,6 +51,7 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { bob_swap.execution_params, bob_swap.bitcoin_wallet, bob_swap.db, + false, ) .await .unwrap() diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_exired.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_exired.rs index 3540a18c..a1c3da82 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_exired.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_exired.rs @@ -14,10 +14,10 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { tokio::spawn(alice_handle); let bob_state = bob::run_until(bob_swap, is_btc_locked).await.unwrap(); - assert!(matches!(bob_state, BobState::BtcLocked {..})); + assert!(matches!(bob_state, BobState::BtcLocked { .. })); let (bob_swap, bob_join_handle) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await; - assert!(matches!(bob_swap.state, BobState::BtcLocked {..})); + assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); // Bob manually cancels let result = bob::cancel( @@ -25,6 +25,7 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { bob_swap.state, bob_swap.bitcoin_wallet, bob_swap.db, + false, ) .await .unwrap() @@ -34,7 +35,7 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { assert!(matches!(result, CancelError::CancelTimelockNotExpiredYet)); let (bob_swap, bob_join_handle) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await; - assert!(matches!(bob_swap.state, BobState::BtcLocked {..})); + assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); // Bob manually refunds bob::refund( @@ -43,6 +44,7 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { bob_swap.execution_params, bob_swap.bitcoin_wallet, bob_swap.db, + false, ) .await .unwrap() @@ -50,7 +52,7 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { .unwrap(); let (bob_swap, _) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await; - assert!(matches!(bob_swap.state, BobState::BtcLocked {..})); + assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); }) .await; } diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_exired_force.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_exired_force.rs new file mode 100644 index 00000000..098f8752 --- /dev/null +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_exired_force.rs @@ -0,0 +1,54 @@ +pub mod testutils; + +use swap::protocol::{alice, bob, bob::BobState}; +use testutils::{bob_run_until::is_btc_locked, SlowCancelConfig}; + +#[tokio::test] +async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() { + testutils::setup_test(SlowCancelConfig, |mut ctx| async move { + let (alice_swap, _) = ctx.new_swap_as_alice().await; + let (bob_swap, bob_join_handle) = ctx.new_swap_as_bob().await; + + let alice_handle = alice::run(alice_swap); + tokio::spawn(alice_handle); + + let bob_state = bob::run_until(bob_swap, is_btc_locked).await.unwrap(); + assert!(matches!(bob_state, BobState::BtcLocked { .. })); + + let (bob_swap, bob_join_handle) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await; + assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); + + // Bob forces a cancel that will fail + let is_error = bob::cancel( + bob_swap.swap_id, + bob_swap.state, + 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).await; + assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); + + // Bob forces a refund that will fail + let is_error = bob::refund( + bob_swap.swap_id, + bob_swap.state, + bob_swap.execution_params, + 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).await; + assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); + }) + .await; +}