Move recovery commands in dedicated module

Less clutter in the folder structure.
This commit is contained in:
Daniel Karzel 2021-04-28 16:13:04 +10:00
parent 4deb96a3c5
commit daa572e5bf
No known key found for this signature in database
GPG Key ID: 30C3FC2E438ADB6E
17 changed files with 460 additions and 23 deletions

View File

@ -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,
alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force, alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force,
punish, punish,
alice_punishes_after_restart_punish_timelock_expired, alice_punishes_after_restart_bob_dead,
alice_manually_punishes_after_bob_dead,
alice_refunds_after_restart_bob_refunded, alice_refunds_after_restart_bob_refunded,
ensure_same_swap_id, ensure_same_swap_id,
concurrent_bobs_after_xmr_lock_proof_sent, 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 runs-on: ubuntu-latest
steps: steps:

View File

@ -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. 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. 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. 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. - Resume-only mode for the ASB.
When started with `--resume-only` the ASB does not accept new, incoming swap requests but only finishes swaps that are resumed upon startup. When started with `--resume-only` the ASB does not accept new, incoming swap requests but only finishes swaps that are resumed upon startup.

View File

@ -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)",
"docker_tests (alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force)", "docker_tests (alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired_force)",
"docker_tests (punish)", "docker_tests (punish)",
"docker_tests (alice_punishes_after_restart_punish_timelock_expired)", "docker_tests (alice_punishes_after_restart_bob_dead)",
"docker_tests (alice_manually_punishes_after_bob_dead)",
"docker_tests (alice_refunds_after_restart_bob_refunded)", "docker_tests (alice_refunds_after_restart_bob_refunded)",
"docker_tests (ensure_same_swap_id)", "docker_tests (ensure_same_swap_id)",
"docker_tests (concurrent_bobs_after_xmr_lock_proof_sent)", "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

@ -65,6 +65,19 @@ pub enum Command {
#[derive(structopt::StructOpt, Debug)] #[derive(structopt::StructOpt, Debug)]
pub enum ManualRecovery { 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( #[structopt(
about = "Publishes the Bitcoin cancel transaction. By default, the cancel timelock will be enforced. A confirmed cancel transaction enables refund and punish." 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)] #[structopt(flatten)]
refund_params: RecoverCommandParams, 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)] #[derive(structopt::StructOpt, Debug)]

View File

@ -31,7 +31,7 @@ use swap::monero::Amount;
use swap::network::swarm; use swap::network::swarm;
use swap::protocol::alice; use swap::protocol::alice;
use swap::protocol::alice::event_loop::KrakenRate; use swap::protocol::alice::event_loop::KrakenRate;
use swap::protocol::alice::{run, EventLoop}; use swap::protocol::alice::{redeem, run, EventLoop};
use swap::seed::Seed; use swap::seed::Seed;
use swap::tor::AuthenticatedClient; use swap::tor::AuthenticatedClient;
use swap::{asb, bitcoin, env, kraken, monero, tor}; use swap::{asb, bitcoin, env, kraken, monero, tor};
@ -233,6 +233,38 @@ async fn main() -> Result<()> {
tracing::info!("Monero successfully refunded"); 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(()) Ok(())

View File

@ -7,17 +7,20 @@ use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
pub use self::behaviour::{Behaviour, OutEvent}; pub use self::behaviour::{Behaviour, OutEvent};
pub use self::cancel::cancel;
pub use self::event_loop::{EventLoop, EventLoopHandle}; pub use self::event_loop::{EventLoop, EventLoopHandle};
pub use self::refund::refund; pub use self::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::state::*;
pub use self::swap::{run, run_until}; pub use self::swap::{run, run_until};
mod behaviour; mod behaviour;
pub mod cancel;
pub mod event_loop; pub mod event_loop;
mod execution_setup; mod execution_setup;
pub mod refund; mod recovery;
mod spot_price; mod spot_price;
pub mod state; pub mod state;
pub mod swap; 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,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<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,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

@ -510,6 +510,18 @@ impl State3 {
Ok(()) Ok(())
} }
pub async fn punish_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
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( pub fn signed_redeem_transaction(
&self, &self,
sig: bitcoin::EncryptedSignature, sig: bitcoin::EncryptedSignature,

View File

@ -302,16 +302,7 @@ async fn next_state(
transfer_proof, transfer_proof,
state3, state3,
} => { } => {
let signed_tx_punish = state3.signed_punish_transaction()?; let punish = state3.punish_btc(bitcoin_wallet).await;
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;
match punish { match punish {
Ok(_) => AliceState::BtcPunished, Ok(_) => AliceState::BtcPunished,

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}; use swap::protocol::{alice, bob};
/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice /// 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] #[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 { harness::setup_test(FastPunishConfig, |mut ctx| async move {
let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
let bob_swap_id = bob_swap.id; let bob_swap_id = bob_swap.id;
@ -26,12 +26,13 @@ async fn alice_punishes_after_restart_if_punish_timelock_expired() {
let alice_state = alice_swap.await??; 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 { if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state {
alice_bitcoin_wallet alice_bitcoin_wallet
.subscribe_to(state3.tx_lock) .subscribe_to(state3.tx_lock)
.await .await
.wait_until_confirmed_with(state3.punish_timelock) .wait_until_confirmed_with(state3.cancel_timelock)
.await?; .await?;
} else { } else {
panic!("Alice in unexpected state {}", alice_state); panic!("Alice in unexpected state {}", alice_state);