From 0c616c74379d572a343dead39cb5dfd5b38d03b7 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Thu, 29 Apr 2021 11:02:26 +1000 Subject: [PATCH 1/5] Move loading the state into the function In the production code it is a weird indirection that we load the state and then pass in the state and the database. In the tests we have one additional load by doing it inside the command, but loading from the db is not expensive. --- swap/src/bin/swap.rs | 8 ++---- swap/src/protocol/bob/cancel.rs | 3 +- swap/src/protocol/bob/refund.rs | 3 +- ...refunds_using_cancel_and_refund_command.rs | 20 +++---------- ...and_refund_command_timelock_not_expired.rs | 28 ++++++------------- ...fund_command_timelock_not_expired_force.rs | 24 ++++------------ 6 files changed, 24 insertions(+), 62 deletions(-) diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index bb43c1a9..ef337512 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -242,9 +242,7 @@ async fn main() -> Result<()> { ) .await?; - let resume_state = db.get_state(swap_id)?.try_into_bob()?.into(); - let cancel = - bob::cancel(swap_id, resume_state, Arc::new(bitcoin_wallet), db, force).await?; + let cancel = bob::cancel(swap_id, Arc::new(bitcoin_wallet), db, force).await?; match cancel { Ok((txid, _)) => { @@ -279,9 +277,7 @@ async fn main() -> Result<()> { ) .await?; - let resume_state = db.get_state(swap_id)?.try_into_bob()?.into(); - - bob::refund(swap_id, resume_state, Arc::new(bitcoin_wallet), db, force).await??; + bob::refund(swap_id, Arc::new(bitcoin_wallet), db, force).await??; } }; Ok(()) diff --git a/swap/src/protocol/bob/cancel.rs b/swap/src/protocol/bob/cancel.rs index ee764e79..9e667a5f 100644 --- a/swap/src/protocol/bob/cancel.rs +++ b/swap/src/protocol/bob/cancel.rs @@ -13,11 +13,12 @@ pub enum Error { pub async fn cancel( swap_id: Uuid, - state: BobState, bitcoin_wallet: Arc, db: Database, force: bool, ) -> Result> { + let state = db.get_state(swap_id)?.try_into_bob()?.into(); + let state6 = match state { BobState::BtcLocked(state3) => state3.cancel(), BobState::XmrLockProofReceived { state, .. } => state.cancel(), diff --git a/swap/src/protocol/bob/refund.rs b/swap/src/protocol/bob/refund.rs index 82168eb0..492f8191 100644 --- a/swap/src/protocol/bob/refund.rs +++ b/swap/src/protocol/bob/refund.rs @@ -11,11 +11,12 @@ pub struct SwapNotCancelledYet(Uuid); pub async fn refund( swap_id: Uuid, - state: BobState, bitcoin_wallet: Arc, db: Database, force: bool, ) -> Result> { + let state = db.get_state(swap_id)?.try_into_bob()?.into(); + let state6 = if force { match state { BobState::BtcLocked(state3) => state3.cancel(), 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 d218341e..50e99fd9 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs @@ -36,14 +36,8 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { // Bob manually cancels bob_join_handle.abort(); - let (_, state) = bob::cancel( - bob_swap.id, - bob_swap.state, - bob_swap.bitcoin_wallet, - bob_swap.db, - false, - ) - .await??; + let (_, state) = + bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false).await??; assert!(matches!(state, BobState::BtcCancelled { .. })); let (bob_swap, bob_join_handle) = ctx @@ -53,14 +47,8 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { // Bob manually refunds bob_join_handle.abort(); - let bob_state = bob::refund( - bob_swap.id, - bob_swap.state, - bob_swap.bitcoin_wallet, - bob_swap.db, - false, - ) - .await??; + let bob_state = + bob::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false).await??; ctx.assert_bob_refunded(bob_state).await; diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs index 5c8e0f20..152c74c7 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs @@ -25,16 +25,10 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); // Bob tries but fails to manually cancel - let result = bob::cancel( - bob_swap.id, - bob_swap.state, - bob_swap.bitcoin_wallet, - bob_swap.db, - false, - ) - .await? - .err() - .unwrap(); + let result = bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false) + .await? + .err() + .unwrap(); assert!(matches!(result, Error::CancelTimelockNotExpiredYet)); @@ -44,16 +38,10 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); // Bob tries but fails to manually refund - bob::refund( - bob_swap.id, - bob_swap.state, - bob_swap.bitcoin_wallet, - bob_swap.db, - false, - ) - .await? - .err() - .unwrap(); + 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) diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs index df06416d..85f4de88 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs @@ -24,15 +24,9 @@ async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() { assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); // Bob forces a cancel that will fail - let is_error = bob::cancel( - bob_swap.id, - bob_swap.state, - bob_swap.bitcoin_wallet, - bob_swap.db, - true, - ) - .await - .is_err(); + let is_error = bob::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, true) + .await + .is_err(); assert!(is_error); @@ -42,15 +36,9 @@ async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() { assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); // Bob forces a refund that will fail - let is_error = bob::refund( - bob_swap.id, - bob_swap.state, - bob_swap.bitcoin_wallet, - bob_swap.db, - true, - ) - .await - .is_err(); + 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 From efcd39eeef92bcaf90f64dca092e9a9bbbe3b358 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Wed, 28 Apr 2021 17:29:25 +1000 Subject: [PATCH 2/5] Add info messages to each subcommand `asb --help` : (...) SUBCOMMANDS: balance Prints the Bitcoin and Monero balance. Requires the monero-wallet-rpc to be running. help Prints this message or the help of the given subcommand(s) history Prints swap-id and the state of each swap ever made. start Main command to run the ASB. withdraw-btc Allows withdrawing BTC from the internal Bitcoin wallet. --- swap/src/asb/command.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/swap/src/asb/command.rs b/swap/src/asb/command.rs index 85c82d1f..6254d6c0 100644 --- a/swap/src/asb/command.rs +++ b/swap/src/asb/command.rs @@ -25,8 +25,9 @@ pub struct Arguments { #[derive(structopt::StructOpt, Debug)] #[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")] pub enum Command { + #[structopt(about = "Main command to run the ASB.")] Start { - #[structopt(long = "max-buy-btc", help = "The maximum amount of BTC the ASB is willing to buy.", default_value="0.005", parse(try_from_str = parse_btc))] + #[structopt(long = "max-buy-btc", help = "The maximum amount of BTC the ASB is willing to buy.", default_value = "0.005", parse(try_from_str = parse_btc))] max_buy: Amount, #[structopt( long = "ask-spread", @@ -41,7 +42,9 @@ pub enum Command { )] resume_only: bool, }, + #[structopt(about = "Prints swap-id and the state of each swap ever made.")] History, + #[structopt(about = "Allows withdrawing BTC from the internal Bitcoin wallet.")] WithdrawBtc { #[structopt( long = "amount", @@ -51,6 +54,9 @@ pub enum Command { #[structopt(long = "address", help = "The address to receive the Bitcoin.")] address: Address, }, + #[structopt( + about = "Prints the Bitcoin and Monero balance. Requires the monero-wallet-rpc to be running." + )] Balance, } From 4deb96a3c5a7603be747a72cc8bdf7b17caaeb58 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Tue, 27 Apr 2021 14:51:53 +1000 Subject: [PATCH 3/5] 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. --- .github/workflows/ci.yml | 6 +- CHANGELOG.md | 6 + bors.toml | 6 +- swap/src/asb/command.rs | 37 ++++++ swap/src/bin/asb.rs | 30 ++++- swap/src/protocol/alice.rs | 4 + swap/src/protocol/alice/cancel.rs | 77 +++++++++++ swap/src/protocol/alice/refund.rs | 125 ++++++++++++++++++ swap/src/protocol/alice/state.rs | 52 +++++++- swap/src/protocol/alice/swap.rs | 37 ++---- swap/src/protocol/bob/refund.rs | 2 +- ...refund_using_cancel_and_refund_command.rs} | 43 +++++- ...and_refund_command_timelock_not_expired.rs | 109 +++++++++++++++ ...fund_command_timelock_not_expired_force.rs | 104 +++++++++++++++ ...and_refund_command_timelock_not_expired.rs | 54 -------- ...fund_command_timelock_not_expired_force.rs | 52 -------- 16 files changed, 601 insertions(+), 143 deletions(-) create mode 100644 swap/src/protocol/alice/cancel.rs create mode 100644 swap/src/protocol/alice/refund.rs rename swap/tests/{bob_refunds_using_cancel_and_refund_command.rs => alice_and_bob_refund_using_cancel_and_refund_command.rs} (59%) create mode 100644 swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired.rs create mode 100644 swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force.rs delete mode 100644 swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs delete mode 100644 swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0ae31f7..73c717ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,9 +100,9 @@ jobs: happy_path_restart_bob_after_xmr_locked, happy_path_restart_bob_before_xmr_locked, happy_path_restart_alice_after_xmr_locked, - bob_refunds_using_cancel_and_refund_command, - bob_refunds_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, + alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired, + alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force, punish, alice_punishes_after_restart_punish_timelock_expired, alice_refunds_after_restart_bob_refunded, diff --git a/CHANGELOG.md b/CHANGELOG.md index 37152d5a..aa66b285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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. When started with `--resume-only` the ASB does not accept new, incoming swap requests but only finishes swaps that are resumed upon startup. diff --git a/bors.toml b/bors.toml index 04973080..77501d38 100644 --- a/bors.toml +++ b/bors.toml @@ -10,9 +10,9 @@ status = [ "docker_tests (happy_path_restart_bob_after_xmr_locked)", "docker_tests (happy_path_restart_alice_after_xmr_locked)", "docker_tests (happy_path_restart_bob_before_xmr_locked)", - "docker_tests (bob_refunds_using_cancel_and_refund_command)", - "docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force)", - "docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired)", + "docker_tests (alice_and_bob_refund_using_cancel_and_refund_command)", + "docker_tests (alice_and_bob_refund_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 (alice_punishes_after_restart_punish_timelock_expired)", "docker_tests (alice_refunds_after_restart_bob_refunded)", diff --git a/swap/src/asb/command.rs b/swap/src/asb/command.rs index 6254d6c0..19f140a1 100644 --- a/swap/src/asb/command.rs +++ b/swap/src/asb/command.rs @@ -3,6 +3,7 @@ use bitcoin::util::amount::ParseAmountError; use bitcoin::{Address, Denomination}; use rust_decimal::Decimal; use std::path::PathBuf; +use uuid::Uuid; #[derive(structopt::StructOpt, Debug)] #[structopt( @@ -58,6 +59,42 @@ pub enum Command { about = "Prints the Bitcoin and Monero balance. Requires the monero-wallet-rpc to be running." )] 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 { diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 5d099055..9a5d920f 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -20,7 +20,7 @@ use prettytable::{row, Table}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use structopt::StructOpt; -use swap::asb::command::{Arguments, Command}; +use swap::asb::command::{Arguments, Command, ManualRecovery, RecoverCommandParams}; use swap::asb::config::{ default_config_path, initial_setup, query_user_for_initial_testnet_config, read_config, Config, ConfigNotInitialized, @@ -29,6 +29,7 @@ use swap::database::Database; use swap::env::GetConfig; use swap::monero::Amount; use swap::network::swarm; +use swap::protocol::alice; use swap::protocol::alice::event_loop::KrakenRate; use swap::protocol::alice::{run, EventLoop}; use swap::seed::Seed; @@ -205,6 +206,33 @@ async fn main() -> Result<()> { 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(()) diff --git a/swap/src/protocol/alice.rs b/swap/src/protocol/alice.rs index 2410e289..d21617e1 100644 --- a/swap/src/protocol/alice.rs +++ b/swap/src/protocol/alice.rs @@ -7,13 +7,17 @@ use std::sync::Arc; use uuid::Uuid; pub use self::behaviour::{Behaviour, OutEvent}; +pub use self::cancel::cancel; pub use self::event_loop::{EventLoop, EventLoopHandle}; +pub use self::refund::refund; pub use self::state::*; pub use self::swap::{run, run_until}; mod behaviour; +pub mod cancel; pub mod event_loop; mod execution_setup; +pub mod refund; mod spot_price; pub mod state; pub mod swap; diff --git a/swap/src/protocol/alice/cancel.rs b/swap/src/protocol/alice/cancel.rs new file mode 100644 index 00000000..cfa1a085 --- /dev/null +++ b/swap/src/protocol/alice/cancel.rs @@ -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, + db: Arc, + force: bool, +) -> Result> { + 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))) +} diff --git a/swap/src/protocol/alice/refund.rs b/swap/src/protocol/alice/refund.rs new file mode 100644 index 00000000..2f0fe86f --- /dev/null +++ b/swap/src/protocol/alice/refund.rs @@ -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, + monero_wallet: Arc, + db: Arc, + force: bool, +) -> Result> { + 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)) +} diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index b091ebb4..3840d28f 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -1,5 +1,6 @@ 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::monero::wallet::{TransferRequest, WatchRequest}; @@ -460,6 +461,55 @@ impl State3 { ) } + pub async fn check_for_tx_cancel( + &self, + bitcoin_wallet: &bitcoin::Wallet, + ) -> Result { + 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 { + 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 { + 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( &self, sig: bitcoin::EncryptedSignature, diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 6f97f2c0..1b90d9f7 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -229,23 +229,17 @@ async fn next_state( transfer_proof, state3, } => { - let transaction = state3.signed_cancel_transaction()?; - - // If Bob hasn't yet broadcasted the tx cancel, we do it - if bitcoin_wallet - .get_raw_transaction(transaction.txid()) - .await - .is_err() - { - if let Err(e) = bitcoin_wallet.broadcast(transaction, "cancel").await { + 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 + // to be able to eventually punish. Since the punish timelock is + // relative to the publication of the cancel transaction we have to ensure it + // gets published once the cancel timelock expires. + if let Err(e) = state3.submit_tx_cancel(bitcoin_wallet).await { tracing::debug!( - "Assuming transaction is already broadcasted because: {:#}", + "Assuming cancel transaction is already broadcasted because: {:#}", e ) } - - // TODO(Franck): Wait until transaction is mined and - // returned mined block height } AliceState::BtcCancelled { @@ -291,20 +285,13 @@ async fn next_state( spend_key, state3, } => { - let view_key = state3.v; - - // Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations - // on the lock transaction - monero_wallet - .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof, 10)) - .await?; - - monero_wallet - .create_from( + state3 + .refund_xmr( + monero_wallet, + monero_wallet_restore_blockheight, swap_id.to_string(), spend_key, - view_key, - monero_wallet_restore_blockheight, + transfer_proof, ) .await?; diff --git a/swap/src/protocol/bob/refund.rs b/swap/src/protocol/bob/refund.rs index 492f8191..2fe324ce 100644 --- a/swap/src/protocol/bob/refund.rs +++ b/swap/src/protocol/bob/refund.rs @@ -7,7 +7,7 @@ use uuid::Uuid; #[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.")] -pub struct SwapNotCancelledYet(Uuid); +pub struct SwapNotCancelledYet(pub Uuid); pub async fn refund( swap_id: Uuid, diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs similarity index 59% rename from swap/tests/bob_refunds_using_cancel_and_refund_command.rs rename to swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs index 50e99fd9..d342d8e7 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs +++ b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs @@ -1,23 +1,31 @@ pub mod harness; +use harness::alice_run_until::is_xmr_lock_transaction_sent; use harness::bob_run_until::is_btc_locked; use harness::FastCancelConfig; +use swap::protocol::alice::AliceState; use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; #[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 { 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 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??; 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 .stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id) .await; @@ -52,7 +60,36 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { 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; Ok(()) diff --git a/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired.rs b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired.rs new file mode 100644 index 00000000..bd0761e3 --- /dev/null +++ b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired.rs @@ -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; +} diff --git a/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force.rs b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force.rs new file mode 100644 index 00000000..59f5141e --- /dev/null +++ b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force.rs @@ -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; +} diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs deleted file mode 100644 index 152c74c7..00000000 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs +++ /dev/null @@ -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; -} diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs deleted file mode 100644 index 85f4de88..00000000 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs +++ /dev/null @@ -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; -} From daa572e5bf7fd1f42c26adaecff08e0b4989a32a Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Wed, 28 Apr 2021 16:13:04 +1000 Subject: [PATCH 4/5] Move recovery commands in dedicated module Less clutter in the folder structure. --- .github/workflows/ci.yml | 6 +- CHANGELOG.md | 8 ++ bors.toml | 6 +- swap/src/asb/command.rs | 28 ++++++ swap/src/bin/asb.rs | 34 ++++++- swap/src/protocol/alice.rs | 11 ++- swap/src/protocol/alice/recovery.rs | 5 + .../protocol/alice/{ => recovery}/cancel.rs | 0 swap/src/protocol/alice/recovery/punish.rs | 98 +++++++++++++++++++ swap/src/protocol/alice/recovery/redeem.rs | 84 ++++++++++++++++ .../protocol/alice/{ => recovery}/refund.rs | 0 .../protocol/alice/recovery/safely_abort.rs | 38 +++++++ swap/src/protocol/alice/state.rs | 12 +++ swap/src/protocol/alice/swap.rs | 11 +-- .../alice_manually_punishes_after_bob_dead.rs | 91 +++++++++++++++++ ..._manually_redeems_after_enc_sig_learned.rs | 42 ++++++++ ... alice_punishes_after_restart_bob_dead.rs} | 9 +- 17 files changed, 460 insertions(+), 23 deletions(-) create mode 100644 swap/src/protocol/alice/recovery.rs rename swap/src/protocol/alice/{ => recovery}/cancel.rs (100%) create mode 100644 swap/src/protocol/alice/recovery/punish.rs create mode 100644 swap/src/protocol/alice/recovery/redeem.rs rename swap/src/protocol/alice/{ => recovery}/refund.rs (100%) create mode 100644 swap/src/protocol/alice/recovery/safely_abort.rs create mode 100644 swap/tests/alice_manually_punishes_after_bob_dead.rs create mode 100644 swap/tests/alice_manually_redeems_after_enc_sig_learned.rs rename swap/tests/{alice_punishes_after_restart_punish_timelock_expired.rs => alice_punishes_after_restart_bob_dead.rs} (85%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73c717ad..98997780 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,11 +104,13 @@ jobs: alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired, alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force, punish, - alice_punishes_after_restart_punish_timelock_expired, + alice_punishes_after_restart_bob_dead, + alice_manually_punishes_after_bob_dead, alice_refunds_after_restart_bob_refunded, ensure_same_swap_id, concurrent_bobs_after_xmr_lock_proof_sent, - concurrent_bobs_before_xmr_lock_proof_sent + concurrent_bobs_before_xmr_lock_proof_sent, + alice_manually_redeems_after_enc_sig_learned ] runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index aa66b285..a6c0c6a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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. +- Punish command for the ASB that allows punishing a specific swap by id. + Includes a `--force` parameter that when set disables the punish timelock check and verifying that the swap is in a cancelled state already. +- Abort command for the ASB that allows safely aborting a specific swap. + Only swaps in a state prior to locking XMR can be safely aborted. +- Redeem command for the ASB that allows redeeming a specific swap. + Only swaps where we learned the encrypted signature are redeemable. + The command checks for expired timelocks to ensure redeeming is safe, but the timelock check can be disable using the `--force` flag. + By default we wait for finality of the redeem transaction; this can be disabled by setting `--do-not-await-finality`. - 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. diff --git a/bors.toml b/bors.toml index 77501d38..70016fc1 100644 --- a/bors.toml +++ b/bors.toml @@ -14,9 +14,11 @@ status = [ "docker_tests (alice_and_bob_refund_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 (alice_punishes_after_restart_punish_timelock_expired)", + "docker_tests (alice_punishes_after_restart_bob_dead)", + "docker_tests (alice_manually_punishes_after_bob_dead)", "docker_tests (alice_refunds_after_restart_bob_refunded)", "docker_tests (ensure_same_swap_id)", "docker_tests (concurrent_bobs_after_xmr_lock_proof_sent)", - "docker_tests (concurrent_bobs_before_xmr_lock_proof_sent)" + "docker_tests (concurrent_bobs_before_xmr_lock_proof_sent)", + "docker_tests (alice_manually_redeems_after_enc_sig_learned)" ] diff --git a/swap/src/asb/command.rs b/swap/src/asb/command.rs index 19f140a1..4ad8e118 100644 --- a/swap/src/asb/command.rs +++ b/swap/src/asb/command.rs @@ -65,6 +65,19 @@ pub enum Command { #[derive(structopt::StructOpt, Debug)] pub enum ManualRecovery { + #[structopt( + about = "Publishes the Bitcoin redeem transaction. This requires that we learned the encrypted signature from Bob and is only safe if no timelock has expired." + )] + Redeem { + #[structopt(flatten)] + redeem_params: RecoverCommandParams, + + #[structopt( + long = "do_not_await_finality", + help = "If this flag is present we exit directly after publishing the redeem transaction without waiting for the transaction to be included in a block" + )] + do_not_await_finality: bool, + }, #[structopt( about = "Publishes the Bitcoin cancel transaction. By default, the cancel timelock will be enforced. A confirmed cancel transaction enables refund and punish." )] @@ -79,6 +92,21 @@ pub enum ManualRecovery { #[structopt(flatten)] refund_params: RecoverCommandParams, }, + #[structopt( + about = "Publishes the Bitcoin punish transaction. By default, the punish timelock and a swap-state where the cancel transaction was already published will be enforced." + )] + Punish { + #[structopt(flatten)] + punish_params: RecoverCommandParams, + }, + #[structopt(about = "Safely Abort requires the swap to be in a state prior to locking XMR.")] + SafelyAbort { + #[structopt( + long = "swap-id", + help = "The swap id can be retrieved using the history subcommand" + )] + swap_id: Uuid, + }, } #[derive(structopt::StructOpt, Debug)] diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 9a5d920f..55158343 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -31,7 +31,7 @@ use swap::monero::Amount; use swap::network::swarm; use swap::protocol::alice; use swap::protocol::alice::event_loop::KrakenRate; -use swap::protocol::alice::{run, EventLoop}; +use swap::protocol::alice::{redeem, run, EventLoop}; use swap::seed::Seed; use swap::tor::AuthenticatedClient; use swap::{asb, bitcoin, env, kraken, monero, tor}; @@ -233,6 +233,38 @@ async fn main() -> Result<()> { tracing::info!("Monero successfully refunded"); } + Command::ManualRecovery(ManualRecovery::Punish { + punish_params: RecoverCommandParams { swap_id, force }, + }) => { + let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?; + + let (txid, _) = + alice::punish(swap_id, Arc::new(bitcoin_wallet), Arc::new(db), force).await??; + + tracing::info!("Punish transaction successfully published with id {}", txid); + } + Command::ManualRecovery(ManualRecovery::SafelyAbort { swap_id }) => { + alice::safely_abort(swap_id, Arc::new(db)).await?; + + tracing::info!("Swap safely aborted"); + } + Command::ManualRecovery(ManualRecovery::Redeem { + redeem_params: RecoverCommandParams { swap_id, force }, + do_not_await_finality, + }) => { + let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?; + + let (txid, _) = alice::redeem( + swap_id, + Arc::new(bitcoin_wallet), + Arc::new(db), + force, + redeem::Finality::from_bool(do_not_await_finality), + ) + .await?; + + tracing::info!("Redeem transaction successfully published with id {}", txid); + } }; Ok(()) diff --git a/swap/src/protocol/alice.rs b/swap/src/protocol/alice.rs index d21617e1..9d5394ba 100644 --- a/swap/src/protocol/alice.rs +++ b/swap/src/protocol/alice.rs @@ -7,17 +7,20 @@ use std::sync::Arc; use uuid::Uuid; pub use self::behaviour::{Behaviour, OutEvent}; -pub use self::cancel::cancel; pub use self::event_loop::{EventLoop, EventLoopHandle}; -pub use self::refund::refund; +pub use self::recovery::cancel::cancel; +pub use self::recovery::punish::punish; +pub use self::recovery::redeem::redeem; +pub use self::recovery::refund::refund; +pub use self::recovery::safely_abort::safely_abort; +pub use self::recovery::{cancel, punish, redeem, refund, safely_abort}; pub use self::state::*; pub use self::swap::{run, run_until}; mod behaviour; -pub mod cancel; pub mod event_loop; mod execution_setup; -pub mod refund; +mod recovery; mod spot_price; pub mod state; pub mod swap; diff --git a/swap/src/protocol/alice/recovery.rs b/swap/src/protocol/alice/recovery.rs new file mode 100644 index 00000000..dd4a7b86 --- /dev/null +++ b/swap/src/protocol/alice/recovery.rs @@ -0,0 +1,5 @@ +pub mod cancel; +pub mod punish; +pub mod redeem; +pub mod refund; +pub mod safely_abort; diff --git a/swap/src/protocol/alice/cancel.rs b/swap/src/protocol/alice/recovery/cancel.rs similarity index 100% rename from swap/src/protocol/alice/cancel.rs rename to swap/src/protocol/alice/recovery/cancel.rs diff --git a/swap/src/protocol/alice/recovery/punish.rs b/swap/src/protocol/alice/recovery/punish.rs new file mode 100644 index 00000000..488017be --- /dev/null +++ b/swap/src/protocol/alice/recovery/punish.rs @@ -0,0 +1,98 @@ +use crate::bitcoin::{self, ExpiredTimelocks, Txid}; +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)] +pub enum Error { + // Errors indicating the the swap can *currently* not be refunded but might be later + #[error("Cannot punish because swap is not in a cancelled state. Make sure to cancel the swap before trying to punish or use --force.")] + SwapNotCancelled, + #[error("The punish timelock has not expired yet because the timelock has not expired. Please try again later")] + PunishTimelockNotExpiredYet, + + // Errors indicating that the swap cannot be refunded because it is in a abort/final state + // state + #[error("Cannot punish swap because it is in state {0} where no BTC was locked. Try aborting instead.")] + NoBtcLocked(AliceState), + #[error("Cannot punish swap because it is in state {0} which is not punishable")] + SwapNotPunishable(AliceState), +} + +pub async fn punish( + swap_id: Uuid, + bitcoin_wallet: Arc, + db: Arc, + force: bool, +) -> Result> { + let state = db.get_state(swap_id)?.try_into_alice()?.into(); + + let state3 = if force { + match state { + + // In case no XMR has been locked, move to Safely Aborted + AliceState::Started { .. } => bail!(Error::NoBtcLocked(state)), + + // Punish potentially possible (no knowledge of cancel transaction) + AliceState::BtcLocked { state3, .. } + | AliceState::XmrLockTransactionSent {state3, ..} + | AliceState::XmrLocked {state3, ..} + | AliceState::XmrLockTransferProofSent {state3, ..} + | AliceState::EncSigLearned {state3, ..} + | AliceState::CancelTimelockExpired {state3, ..} + + // Punish possible due to cancel transaction already being published + | AliceState::BtcCancelled {state3, ..} + | AliceState::BtcPunishable {state3, ..} => { + state3 + } + + // If the swap was refunded it cannot be punished + AliceState::BtcRefunded {..} + // Alice already in final state + | AliceState::BtcRedeemed + | AliceState::XmrRefunded + | AliceState::BtcPunished + | AliceState::SafelyAborted => bail!(Error::SwapNotPunishable(state)), + } + } else { + match state { + AliceState::Started { .. } => { + bail!(Error::NoBtcLocked(state)) + } + + AliceState::BtcCancelled { state3, .. } | AliceState::BtcPunishable { state3, .. } => { + state3 + } + + AliceState::BtcRefunded { .. } + | AliceState::BtcRedeemed + | AliceState::XmrRefunded + | AliceState::BtcPunished + | AliceState::SafelyAborted => bail!(Error::SwapNotPunishable(state)), + + _ => return Ok(Err(Error::SwapNotCancelled)), + } + }; + + tracing::info!(%swap_id, "Trying to manually punish swap"); + + if !force { + tracing::debug!(%swap_id, "Checking if punish timelock is expired"); + + if let ExpiredTimelocks::Cancel = state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { + return Ok(Err(Error::PunishTimelockNotExpiredYet)); + } + } + + let txid = state3.punish_btc(&bitcoin_wallet).await?; + + let state = AliceState::BtcPunished; + let db_state = (&state).into(); + db.insert_latest_state(swap_id, Swap::Alice(db_state)) + .await?; + + Ok(Ok((txid, state))) +} diff --git a/swap/src/protocol/alice/recovery/redeem.rs b/swap/src/protocol/alice/recovery/redeem.rs new file mode 100644 index 00000000..12999db9 --- /dev/null +++ b/swap/src/protocol/alice/recovery/redeem.rs @@ -0,0 +1,84 @@ +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; + +pub enum Finality { + Await, + NotAwait, +} + +impl Finality { + pub fn from_bool(do_not_await_finality: bool) -> Self { + if do_not_await_finality { + Self::NotAwait + } else { + Self::Await + } + } +} + +pub async fn redeem( + swap_id: Uuid, + bitcoin_wallet: Arc, + db: Arc, + force: bool, + finality: Finality, +) -> Result<(Txid, AliceState)> { + let state = db.get_state(swap_id)?.try_into_alice()?.into(); + + match state { + AliceState::EncSigLearned { + state3, + encrypted_signature, + .. + } => { + tracing::info!(%swap_id, "Trying to redeem swap"); + + if !force { + tracing::debug!(%swap_id, "Checking if timelocks have expired"); + + let expired_timelocks = state3.expired_timelocks(bitcoin_wallet.as_ref()).await?; + match expired_timelocks { + ExpiredTimelocks::None => (), + _ => bail!("{:?} timelock already expired, consider using refund or punish. You can use --force to publish the redeem transaction, but be aware that it is not safe to do so anymore!", expired_timelocks) + } + } + + let redeem_tx = state3.signed_redeem_transaction(*encrypted_signature)?; + let (txid, subscription) = bitcoin_wallet.broadcast(redeem_tx, "redeem").await?; + + if let Finality::Await = finality { + subscription.wait_until_final().await?; + } + + let state = AliceState::BtcRedeemed; + let db_state = (&state).into(); + + db.insert_latest_state(swap_id, Swap::Alice(db_state)) + .await?; + + Ok((txid, state)) + } + + AliceState::Started { .. } + | AliceState::BtcLocked { .. } + | AliceState::XmrLockTransactionSent { .. } + | AliceState::XmrLocked { .. } + | AliceState::XmrLockTransferProofSent { .. } + | AliceState::CancelTimelockExpired { .. } + | AliceState::BtcCancelled { .. } + | AliceState::BtcRefunded { .. } + | AliceState::BtcPunishable { .. } + | AliceState::BtcRedeemed + | AliceState::XmrRefunded + | AliceState::BtcPunished + | AliceState::SafelyAborted => bail!( + "Cannot redeem swap {} because it is in state {} which cannot be manually redeemed", + swap_id, + state + ), + } +} diff --git a/swap/src/protocol/alice/refund.rs b/swap/src/protocol/alice/recovery/refund.rs similarity index 100% rename from swap/src/protocol/alice/refund.rs rename to swap/src/protocol/alice/recovery/refund.rs diff --git a/swap/src/protocol/alice/recovery/safely_abort.rs b/swap/src/protocol/alice/recovery/safely_abort.rs new file mode 100644 index 00000000..605c5029 --- /dev/null +++ b/swap/src/protocol/alice/recovery/safely_abort.rs @@ -0,0 +1,38 @@ +use crate::database::{Database, Swap}; +use crate::protocol::alice::AliceState; +use anyhow::{bail, Result}; +use std::sync::Arc; +use uuid::Uuid; + +pub async fn safely_abort(swap_id: Uuid, db: Arc) -> Result { + let state = db.get_state(swap_id)?.try_into_alice()?.into(); + + match state { + AliceState::Started { .. } | AliceState::BtcLocked { .. } => { + let state = AliceState::SafelyAborted; + + let db_state = (&state).into(); + db.insert_latest_state(swap_id, Swap::Alice(db_state)) + .await?; + + Ok(state) + } + + AliceState::XmrLockTransactionSent { .. } + | AliceState::XmrLocked { .. } + | AliceState::XmrLockTransferProofSent { .. } + | AliceState::EncSigLearned { .. } + | AliceState::CancelTimelockExpired { .. } + | AliceState::BtcCancelled { .. } + | AliceState::BtcRefunded { .. } + | AliceState::BtcPunishable { .. } + | AliceState::BtcRedeemed + | AliceState::XmrRefunded + | AliceState::BtcPunished + | AliceState::SafelyAborted => bail!( + "Cannot safely abort swap {} because it is in state {} which cannot be safely aborted", + swap_id, + state + ), + } +} diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index 3840d28f..d8b7d004 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -510,6 +510,18 @@ impl State3 { Ok(()) } + pub async fn punish_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { + let signed_tx_punish = self.signed_punish_transaction()?; + + async { + let (txid, subscription) = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; + subscription.wait_until_final().await?; + + Result::<_, anyhow::Error>::Ok(txid) + } + .await + } + pub fn signed_redeem_transaction( &self, sig: bitcoin::EncryptedSignature, diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 1b90d9f7..14002f60 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -302,16 +302,7 @@ async fn next_state( transfer_proof, state3, } => { - let signed_tx_punish = state3.signed_punish_transaction()?; - - let punish = async { - let (txid, subscription) = - bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; - subscription.wait_until_final().await?; - - Result::<_, anyhow::Error>::Ok(txid) - } - .await; + let punish = state3.punish_btc(bitcoin_wallet).await; match punish { Ok(_) => AliceState::BtcPunished, diff --git a/swap/tests/alice_manually_punishes_after_bob_dead.rs b/swap/tests/alice_manually_punishes_after_bob_dead.rs new file mode 100644 index 00000000..1bf71d62 --- /dev/null +++ b/swap/tests/alice_manually_punishes_after_bob_dead.rs @@ -0,0 +1,91 @@ +pub mod harness; + +use harness::alice_run_until::is_xmr_lock_transaction_sent; +use harness::bob_run_until::is_btc_locked; +use harness::FastPunishConfig; +use swap::protocol::alice::AliceState; +use swap::protocol::bob::BobState; +use swap::protocol::{alice, bob}; + +/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice +/// the encsig and fail to refund or redeem. Alice punishes using the cancel and +/// punish command. +#[tokio::test] +async fn alice_manually_punishes_after_bob_dead() { + harness::setup_test(FastPunishConfig, |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 alice_bitcoin_wallet = alice_swap.bitcoin_wallet.clone(); + + 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 alice_state = alice_swap.await??; + + // Ensure cancel timelock is expired + if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state { + alice_bitcoin_wallet + .subscribe_to(state3.tx_lock) + .await + .wait_until_confirmed_with(state3.cancel_timelock) + .await?; + } else { + panic!("Alice in unexpected state {}", alice_state); + } + + // manual cancel (required to be able to punish) + + ctx.restart_alice().await; + let alice_swap = ctx.alice_next_swap().await; + let (_, alice_state) = alice::cancel( + alice_swap.swap_id, + alice_swap.bitcoin_wallet, + alice_swap.db, + false, + ) + .await??; + + // Ensure punish timelock is expired + if let AliceState::BtcCancelled { state3, .. } = alice_state { + alice_bitcoin_wallet + .subscribe_to(state3.tx_cancel()) + .await + .wait_until_confirmed_with(state3.punish_timelock) + .await?; + } else { + panic!("Alice in unexpected state {}", alice_state); + } + + // manual punish + + ctx.restart_alice().await; + let alice_swap = ctx.alice_next_swap().await; + let (_, alice_state) = alice::punish( + alice_swap.swap_id, + alice_swap.bitcoin_wallet, + alice_swap.db, + false, + ) + .await??; + ctx.assert_alice_punished(alice_state).await; + + // Restart Bob after Alice punished to ensure Bob transitions to + // punished and does not run indefinitely + let (bob_swap, _) = ctx + .stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id) + .await; + assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); + + let bob_state = bob::run(bob_swap).await?; + + ctx.assert_bob_punished(bob_state).await; + + Ok(()) + }) + .await; +} diff --git a/swap/tests/alice_manually_redeems_after_enc_sig_learned.rs b/swap/tests/alice_manually_redeems_after_enc_sig_learned.rs new file mode 100644 index 00000000..38a0da9b --- /dev/null +++ b/swap/tests/alice_manually_redeems_after_enc_sig_learned.rs @@ -0,0 +1,42 @@ +pub mod harness; + +use harness::alice_run_until::is_encsig_learned; +use harness::SlowCancelConfig; +use swap::protocol::alice::redeem::Finality; +use swap::protocol::alice::AliceState; +use swap::protocol::{alice, bob}; + +/// Bob locks Btc and Alice locks Xmr. Alice redeems using manual redeem command +/// after learning encsig from Bob +#[tokio::test] +async fn alice_manually_redeems_after_enc_sig_learned() { + harness::setup_test(SlowCancelConfig, |mut ctx| async move { + let (bob_swap, _) = ctx.bob_swap().await; + let bob_swap = tokio::spawn(bob::run(bob_swap)); + + let alice_swap = ctx.alice_next_swap().await; + let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_encsig_learned)); + + let alice_state = alice_swap.await??; + assert!(matches!(alice_state, AliceState::EncSigLearned { .. })); + + // manual redeem + ctx.restart_alice().await; + let alice_swap = ctx.alice_next_swap().await; + let (_, alice_state) = alice::redeem( + alice_swap.swap_id, + alice_swap.bitcoin_wallet, + alice_swap.db, + false, + Finality::Await, + ) + .await?; + ctx.assert_alice_redeemed(alice_state).await; + + let bob_state = bob_swap.await??; + ctx.assert_bob_redeemed(bob_state).await; + + Ok(()) + }) + .await; +} diff --git a/swap/tests/alice_punishes_after_restart_punish_timelock_expired.rs b/swap/tests/alice_punishes_after_restart_bob_dead.rs similarity index 85% rename from swap/tests/alice_punishes_after_restart_punish_timelock_expired.rs rename to swap/tests/alice_punishes_after_restart_bob_dead.rs index 9157a19c..377b7b47 100644 --- a/swap/tests/alice_punishes_after_restart_punish_timelock_expired.rs +++ b/swap/tests/alice_punishes_after_restart_bob_dead.rs @@ -8,9 +8,9 @@ use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; /// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice -/// the encsig and fail to refund or redeem. Alice punishes. +/// the encsig and fail to refund or redeem. Alice cancels and punishes. #[tokio::test] -async fn alice_punishes_after_restart_if_punish_timelock_expired() { +async fn alice_punishes_after_restart_if_bob_dead() { harness::setup_test(FastPunishConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap_id = bob_swap.id; @@ -26,12 +26,13 @@ async fn alice_punishes_after_restart_if_punish_timelock_expired() { let alice_state = alice_swap.await??; - // Ensure punish timelock is expired + // Ensure cancel timelock is expired (we can only ensure that, because the + // cancel transaction is not published at this point) if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state { alice_bitcoin_wallet .subscribe_to(state3.tx_lock) .await - .wait_until_confirmed_with(state3.punish_timelock) + .wait_until_confirmed_with(state3.cancel_timelock) .await?; } else { panic!("Alice in unexpected state {}", alice_state); From 23d9637a4be64d2f70fb83319cf553d564fbb0a3 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Fri, 7 May 2021 14:28:06 +1000 Subject: [PATCH 5/5] Work in review comments --- swap/src/protocol/alice/recovery/punish.rs | 6 +++--- swap/src/protocol/alice/recovery/refund.rs | 4 ++-- swap/src/protocol/alice/state.rs | 9 +++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/swap/src/protocol/alice/recovery/punish.rs b/swap/src/protocol/alice/recovery/punish.rs index 488017be..5a06b5ec 100644 --- a/swap/src/protocol/alice/recovery/punish.rs +++ b/swap/src/protocol/alice/recovery/punish.rs @@ -7,10 +7,10 @@ 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("Cannot punish because swap is not in a cancelled state. Make sure to cancel the swap before trying to punish or use --force.")] + // Errors indicating the swap can *currently* not be punished but might be later + #[error("Swap is not in a cancelled state Make sure to cancel the swap before trying to punish or use --force.")] SwapNotCancelled, - #[error("The punish timelock has not expired yet because the timelock has not expired. Please try again later")] + #[error("The punish transaction cannot be published because the punish timelock has not expired yet. Please try again later")] PunishTimelockNotExpiredYet, // Errors indicating that the swap cannot be refunded because it is in a abort/final state diff --git a/swap/src/protocol/alice/recovery/refund.rs b/swap/src/protocol/alice/recovery/refund.rs index 2f0fe86f..8168bace 100644 --- a/swap/src/protocol/alice/recovery/refund.rs +++ b/swap/src/protocol/alice/recovery/refund.rs @@ -9,7 +9,7 @@ use uuid::Uuid; #[derive(Debug, thiserror::Error)] pub enum Error { - // Errors indicating the the swap can *currently* not be refunded but might be later + // Errors indicating 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( @@ -19,7 +19,7 @@ pub enum Error { // 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.")] + #[error("Swap 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), diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index d8b7d004..80f12a0a 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -513,13 +513,10 @@ impl State3 { pub async fn punish_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { let signed_tx_punish = self.signed_punish_transaction()?; - async { - let (txid, subscription) = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; - subscription.wait_until_final().await?; + let (txid, subscription) = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; + subscription.wait_until_final().await?; - Result::<_, anyhow::Error>::Ok(txid) - } - .await + Ok(txid) } pub fn signed_redeem_transaction(