452: Asb manual recovery commands r=da-kami a=da-kami

Fixes #377 

Allows us to manually finish swaps if we run into error scenarios during execution.

I opted for putting all these commands under subcommand `manual-recovery` to run them e.g. (cancel):

```
./asb manual-recovery cancel [--force] --swap-id (...)
```

I combined the `cancel` and `refund` command in the same commit, because I only adapted the e2e tests once cancel and refund were implemented (cancel alone is a bit useless anyway...).

Co-authored-by: Daniel Karzel <daniel@comit.network>
This commit is contained in:
bors[bot] 2021-05-10 00:09:59 +00:00 committed by GitHub
commit 86bcef8eba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1069 additions and 209 deletions

View File

@ -100,15 +100,17 @@ 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_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:

View File

@ -9,6 +9,20 @@ 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.
- 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.

View File

@ -10,13 +10,15 @@ 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_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)"
]

View File

@ -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(
@ -25,8 +26,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 +43,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,7 +55,74 @@ 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,
#[structopt(about = "Contains sub-commands for recovering a swap manually.")]
ManualRecovery(ManualRecovery),
}
#[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."
)]
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,
},
#[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)]
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> {

View File

@ -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,8 +29,9 @@ 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::protocol::alice::{redeem, run, EventLoop};
use swap::seed::Seed;
use swap::tor::AuthenticatedClient;
use swap::{asb, bitcoin, env, kraken, monero, tor};
@ -205,6 +206,65 @@ 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");
}
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(())

View File

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

View File

@ -8,12 +8,19 @@ use uuid::Uuid;
pub use self::behaviour::{Behaviour, OutEvent};
pub use self::event_loop::{EventLoop, EventLoopHandle};
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 event_loop;
mod execution_setup;
mod recovery;
mod spot_price;
pub mod state;
pub mod swap;

View File

@ -0,0 +1,5 @@
pub mod cancel;
pub mod punish;
pub mod redeem;
pub mod refund;
pub mod safely_abort;

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,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 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 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
// 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<bitcoin::Wallet>,
db: Arc<Database>,
force: bool,
) -> Result<Result<(Txid, AliceState), Error>> {
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)))
}

View File

@ -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<Wallet>,
db: Arc<Database>,
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
),
}
}

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 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("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),
}
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

@ -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<Database>) -> Result<AliceState> {
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
),
}
}

View File

@ -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,64 @@ 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 async fn punish_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
let signed_tx_punish = self.signed_punish_transaction()?;
let (txid, subscription) = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
subscription.wait_until_final().await?;
Ok(txid)
}
pub fn signed_redeem_transaction(
&self,
sig: bitcoin::EncryptedSignature,

View File

@ -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?;
@ -315,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,

View File

@ -13,11 +13,12 @@ pub enum Error {
pub async fn cancel(
swap_id: Uuid,
state: BobState,
bitcoin_wallet: Arc<Wallet>,
db: Database,
force: bool,
) -> Result<Result<(Txid, BobState), Error>> {
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(),

View File

@ -7,15 +7,16 @@ 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,
state: BobState,
bitcoin_wallet: Arc<Wallet>,
db: Database,
force: bool,
) -> Result<Result<BobState, SwapNotCancelledYet>> {
let state = db.get_state(swap_id)?.try_into_bob()?.into();
let state6 = if force {
match state {
BobState::BtcLocked(state3) => state3.cancel(),

View File

@ -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;
@ -36,14 +44,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,18 +55,41 @@ 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,
let bob_state =
bob::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db, false).await??;
ctx.assert_bob_refunded(bob_state).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??;
ctx.assert_bob_refunded(bob_state).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??;
let alice_state = alice_swap.await??;
ctx.assert_alice_refunded(alice_state).await;
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

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

View File

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

View File

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

View File

@ -1,66 +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.state,
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.state,
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,64 +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.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, 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.state,
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;
}