ASB manual recovery commands

Adds `cancel`, `refund`, `punish`, `redeem` and `safely-abort` commands to the ASB that can be used to trigger the specific scenario for the swap by ID.
This commit is contained in:
Daniel Karzel 2021-04-27 14:51:53 +10:00
parent efcd39eeef
commit 4deb96a3c5
No known key found for this signature in database
GPG key ID: 30C3FC2E438ADB6E
16 changed files with 601 additions and 143 deletions

View file

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

View file

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

View file

@ -0,0 +1,77 @@
use crate::bitcoin::{ExpiredTimelocks, Txid, Wallet};
use crate::database::{Database, Swap};
use crate::protocol::alice::AliceState;
use anyhow::{bail, Result};
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, thiserror::Error, Clone, Copy)]
pub enum Error {
#[error("The cancel transaction cannot be published because the cancel timelock has not expired yet. Please try again later")]
CancelTimelockNotExpiredYet,
}
pub async fn cancel(
swap_id: Uuid,
bitcoin_wallet: Arc<Wallet>,
db: Arc<Database>,
force: bool,
) -> Result<Result<(Txid, AliceState), Error>> {
let state = db.get_state(swap_id)?.try_into_alice()?.into();
let (monero_wallet_restore_blockheight, transfer_proof, state3) = match state {
// In case no XMR has been locked, move to Safely Aborted
AliceState::Started { .. }
| AliceState::BtcLocked { .. } => bail!("Cannot cancel swap {} because it is in state {} where no XMR was locked.", swap_id, state),
AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, transfer_proof, state3, }
| AliceState::XmrLocked { monero_wallet_restore_blockheight, transfer_proof, state3 }
| AliceState::XmrLockTransferProofSent { monero_wallet_restore_blockheight, transfer_proof, state3 }
// in cancel mode we do not care about the fact that we could redeem, but always wait for cancellation (leading either refund or punish)
| AliceState::EncSigLearned { monero_wallet_restore_blockheight, transfer_proof, state3, .. }
| AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, transfer_proof, state3} => {
(monero_wallet_restore_blockheight, transfer_proof, state3)
}
// The cancel tx was already published, but Alice not yet in final state
AliceState::BtcCancelled { .. }
| AliceState::BtcRefunded { .. }
| AliceState::BtcPunishable { .. }
// Alice already in final state
| AliceState::BtcRedeemed
| AliceState::XmrRefunded
| AliceState::BtcPunished
| AliceState::SafelyAborted => bail!("Cannot cancel swap {} because it is in state {} which is not cancelable", swap_id, state),
};
tracing::info!(%swap_id, "Trying to manually cancel swap");
if !force {
tracing::debug!(%swap_id, "Checking if cancel timelock is expired");
if let ExpiredTimelocks::None = state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
return Ok(Err(Error::CancelTimelockNotExpiredYet));
}
}
let txid = if let Ok(tx) = state3.check_for_tx_cancel(bitcoin_wallet.as_ref()).await {
let txid = tx.txid();
tracing::debug!(%swap_id, "Cancel transaction has already been published: {}", txid);
txid
} else {
state3.submit_tx_cancel(bitcoin_wallet.as_ref()).await?
};
let state = AliceState::BtcCancelled {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
Ok(Ok((txid, state)))
}

View file

@ -0,0 +1,125 @@
use crate::bitcoin::{self};
use crate::database::{Database, Swap};
use crate::monero;
use crate::protocol::alice::AliceState;
use anyhow::{bail, Result};
use libp2p::PeerId;
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
pub enum Error {
// Errors indicating the the swap can *currently* not be refunded but might be later
#[error("Swap is not in a cancelled state. Make sure to cancel the swap before trying to refund or use --force.")]
SwapNotCancelled,
#[error(
"Counterparty {0} did not refund the BTC yet. You can try again later or try to punish."
)]
RefundTransactionNotPublishedYet(PeerId),
// Errors indicating that the swap cannot be refunded because because it is in a abort/final
// state
#[error("Swa is in state {0} where no XMR was locked. Try aborting instead.")]
NoXmrLocked(AliceState),
#[error("Swap is in state {0} which is not refundable")]
SwapNotRefundable(AliceState),
}
pub async fn refund(
swap_id: Uuid,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
db: Arc<Database>,
force: bool,
) -> Result<Result<AliceState, Error>> {
let state = db.get_state(swap_id)?.try_into_alice()?.into();
let (monero_wallet_restore_blockheight, transfer_proof, state3) = if force {
match state {
// In case no XMR has been locked, move to Safely Aborted
AliceState::Started { .. }
| AliceState::BtcLocked { .. } => bail!(Error::NoXmrLocked(state)),
// Refund potentially possible (no knowledge of cancel transaction)
AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, transfer_proof, state3, }
| AliceState::XmrLocked { monero_wallet_restore_blockheight, transfer_proof, state3 }
| AliceState::XmrLockTransferProofSent { monero_wallet_restore_blockheight, transfer_proof, state3 }
| AliceState::EncSigLearned { monero_wallet_restore_blockheight, transfer_proof, state3, .. }
| AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, transfer_proof, state3 }
// Refund possible due to cancel transaction already being published
| AliceState::BtcCancelled { monero_wallet_restore_blockheight, transfer_proof, state3 }
| AliceState::BtcRefunded { monero_wallet_restore_blockheight, transfer_proof, state3, .. }
| AliceState::BtcPunishable { monero_wallet_restore_blockheight, transfer_proof, state3, .. } => {
(monero_wallet_restore_blockheight, transfer_proof, state3)
}
// Alice already in final state
AliceState::BtcRedeemed
| AliceState::XmrRefunded
| AliceState::BtcPunished
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),
}
} else {
match state {
AliceState::Started { .. } | AliceState::BtcLocked { .. } => {
bail!(Error::NoXmrLocked(state))
}
AliceState::BtcCancelled {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
}
| AliceState::BtcRefunded {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
..
}
| AliceState::BtcPunishable {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
..
} => (monero_wallet_restore_blockheight, transfer_proof, state3),
AliceState::BtcRedeemed
| AliceState::XmrRefunded
| AliceState::BtcPunished
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),
_ => return Ok(Err(Error::SwapNotCancelled)),
}
};
tracing::info!(%swap_id, "Trying to manually refund swap");
let spend_key = if let Ok(published_refund_tx) =
state3.fetch_tx_refund(bitcoin_wallet.as_ref()).await
{
tracing::debug!(%swap_id, "Bitcoin refund transaction found, extracting key to refund Monero");
state3.extract_monero_private_key(published_refund_tx)?
} else {
let bob_peer_id = db.get_peer_id(swap_id)?;
return Ok(Err(Error::RefundTransactionNotPublishedYet(bob_peer_id)));
};
state3
.refund_xmr(
&monero_wallet,
monero_wallet_restore_blockheight,
swap_id.to_string(),
spend_key,
transfer_proof,
)
.await?;
let state = AliceState::XmrRefunded;
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
Ok(Ok(state))
}

View file

@ -1,5 +1,6 @@
use crate::bitcoin::{
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<Transaction> {
let tx_cancel = self.tx_cancel();
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
Ok(tx)
}
pub async fn fetch_tx_refund(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Transaction> {
let tx_refund = self.tx_refund();
let tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
Ok(tx)
}
pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
let transaction = self.signed_cancel_transaction()?;
let (tx_id, _) = bitcoin_wallet.broadcast(transaction, "cancel").await?;
Ok(tx_id)
}
pub async fn refund_xmr(
&self,
monero_wallet: &monero::Wallet,
monero_wallet_restore_blockheight: BlockHeight,
file_name: String,
spend_key: monero::PrivateKey,
transfer_proof: TransferProof,
) -> Result<()> {
let view_key = self.v;
// Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations
// on the lock transaction
monero_wallet
.watch_for_transfer(self.lock_xmr_watch_request(transfer_proof, 10))
.await?;
monero_wallet
.create_from(
file_name,
spend_key,
view_key,
monero_wallet_restore_blockheight,
)
.await?;
Ok(())
}
pub fn signed_redeem_transaction(
&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?;

View file

@ -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,