diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index 6389b6bd..2fd9896d 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -136,7 +136,7 @@ impl From<&AliceState> for state::Alice { AliceState::XmrRefunded => Alice::SwapComplete, // TODO(Franck): it may be more efficient to store the fact that we already want to // abort - AliceState::Cancelling { state3 } => Alice::XmrLocked(state3.clone()), + AliceState::Cancelling { state3 } => Alice::Cancelling(state3.clone()), AliceState::Punished => Alice::SwapComplete, AliceState::SafelyAborted => Alice::SwapComplete, } @@ -176,6 +176,7 @@ impl TryFrom for AliceState { state3: state, encrypted_signature, }, + Alice::Cancelling(state3) => AliceState::Cancelling { state3 }, Alice::BtcCancelled(state) => { let tx_cancel = bitcoin::TxCancel::new( &state.tx_lock, diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 9cbca0cf..801f9099 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -19,8 +19,7 @@ use std::sync::Arc; use structopt::StructOpt; use swap::{ alice, alice::swap::AliceState, bitcoin, bob, bob::swap::BobState, cli::Options, monero, - network::transport::build, recover::recover, storage::Database, trace::init_tracing, - SwapAmounts, + network::transport::build, storage::Database, trace::init_tracing, SwapAmounts, }; use tracing::{info, log::LevelFilter}; use uuid::Uuid; @@ -226,24 +225,7 @@ async fn main() -> Result<()> { // Print the table to stdout table.printstd(); } - Options::Recover { - swap_id, - bitcoind_url, - monerod_url, - bitcoin_wallet_name, - } => { - let state = db.get_state(swap_id)?; - let bitcoin_wallet = bitcoin::Wallet::new( - bitcoin_wallet_name.as_ref(), - bitcoind_url, - config.bitcoin_network, - ) - .await - .expect("failed to create bitcoin wallet"); - let monero_wallet = monero::Wallet::new(monerod_url); - - recover(bitcoin_wallet, monero_wallet, state).await?; - } + Options::Recover { .. } => todo!("implement this"), } Ok(()) diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 3f8ace7b..47e6622b 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -9,7 +9,6 @@ pub mod bob; pub mod cli; pub mod monero; pub mod network; -pub mod recover; pub mod serde; pub mod state; pub mod storage; diff --git a/swap/src/recover.rs b/swap/src/recover.rs deleted file mode 100644 index 1beae16f..00000000 --- a/swap/src/recover.rs +++ /dev/null @@ -1,505 +0,0 @@ -//! This module is used to attempt to recover an unfinished swap. -//! -//! Recovery is only supported for certain states and the strategy followed is -//! to perform the simplest steps that require no further action from the -//! counterparty. -//! -//! The quality of this module is bad because there is a lot of code -//! duplication, both within the module and with respect to -//! `xmr_btc/src/{alice,bob}.rs`. In my opinion, a better approach to support -//! swap recovery would be through the `action_generator`s themselves, but this -//! was deemed too complicated for the time being. - -use crate::{ - bitcoin, monero, - monero::CreateWalletForOutput, - state::{Alice, Bob, Swap}, -}; -use anyhow::Result; -use bitcoin_harness::BitcoindRpcApi; -use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; -use futures::{ - future::{select, Either}, - pin_mut, -}; -use sha2::Sha256; -use tracing::info; -use xmr_btc::{ - bitcoin::{ - poll_until_block_height_is_gte, BroadcastSignedTransaction, TransactionBlockHeight, - WatchForRawTransaction, - }, - bob::{State3, State4}, -}; - -pub async fn recover( - bitcoin_wallet: bitcoin::Wallet, - monero_wallet: monero::Wallet, - state: Swap, -) -> Result<()> { - match state { - Swap::Alice(state) => alice_recover(bitcoin_wallet, monero_wallet, state).await, - Swap::Bob(state) => bob_recover(bitcoin_wallet, monero_wallet, state).await, - } -} - -pub async fn alice_recover( - bitcoin_wallet: bitcoin::Wallet, - monero_wallet: monero::Wallet, - state: Alice, -) -> Result<()> { - match state { - Alice::Started { .. } - | Alice::Negotiated(_) - | Alice::BtcLocked(_) - | Alice::SwapComplete => { - info!("Nothing to do"); - } - Alice::XmrLocked(state) => { - info!("Monero still locked up"); - - let tx_cancel = bitcoin::TxCancel::new( - &state.tx_lock, - state.refund_timelock, - state.a.public(), - state.B, - ); - - info!("Checking if the Bitcoin cancel transaction has been published"); - if bitcoin_wallet - .inner - .get_raw_transaction(tx_cancel.txid()) - .await - .is_err() - { - info!("Bitcoin cancel transaction not yet published"); - - let tx_lock_height = bitcoin_wallet - .transaction_block_height(state.tx_lock.txid()) - .await; - poll_until_block_height_is_gte( - &bitcoin_wallet, - tx_lock_height + state.refund_timelock, - ) - .await; - - let sig_a = state.a.sign(tx_cancel.digest()); - let sig_b = state.tx_cancel_sig_bob.clone(); - - let tx_cancel = tx_cancel - .clone() - .add_signatures(&state.tx_lock, (state.a.public(), sig_a), (state.B, sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel"); - - // TODO: We should not fail if the transaction is already on the blockchain - bitcoin_wallet - .broadcast_signed_transaction(tx_cancel) - .await?; - } - - info!("Confirmed that Bitcoin cancel transaction is on the blockchain"); - - let tx_cancel_height = bitcoin_wallet - .transaction_block_height(tx_cancel.txid()) - .await; - let poll_until_bob_can_be_punished = poll_until_block_height_is_gte( - &bitcoin_wallet, - tx_cancel_height + state.punish_timelock, - ); - pin_mut!(poll_until_bob_can_be_punished); - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address); - - info!("Waiting for either Bitcoin refund or punish timelock"); - match select( - bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()), - poll_until_bob_can_be_punished, - ) - .await - { - Either::Left((tx_refund_published, ..)) => { - info!("Found Bitcoin refund transaction"); - - let s_a = monero::PrivateKey { - scalar: state.s_a.into_ed25519(), - }; - - let tx_refund_sig = tx_refund - .extract_signature_by_key(tx_refund_published, state.a.public())?; - let tx_refund_encsig = state.a.encsign(state.S_b_bitcoin, tx_refund.digest()); - - let s_b = bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?; - let s_b = monero::PrivateKey::from_scalar( - monero::Scalar::from_bytes_mod_order(s_b.to_bytes()), - ); - - monero_wallet - .create_and_load_wallet_for_output(s_a + s_b, state.v) - .await?; - info!("Successfully refunded monero"); - } - Either::Right(_) => { - info!("Punish timelock reached, attempting to punish Bob"); - - let tx_punish = bitcoin::TxPunish::new( - &tx_cancel, - &state.punish_address, - state.punish_timelock, - ); - - let sig_a = state.a.sign(tx_punish.digest()); - let sig_b = state.tx_punish_sig_bob.clone(); - - let sig_tx_punish = tx_punish.add_signatures( - &tx_cancel, - (state.a.public(), sig_a), - (state.B, sig_b), - )?; - - bitcoin_wallet - .broadcast_signed_transaction(sig_tx_punish) - .await?; - info!("Successfully punished Bob's inactivity by taking bitcoin"); - } - }; - } - Alice::EncSignLearned { .. } => unimplemented!("recover method is deprecated"), - Alice::BtcCancelled { .. } => unimplemented!("recover method is deprecated"), - Alice::BtcRedeemable { redeem_tx, state } => { - info!("Have the means to redeem the Bitcoin"); - - let tx_lock_height = bitcoin_wallet - .transaction_block_height(state.tx_lock.txid()) - .await; - - let block_height = bitcoin_wallet.inner.client.getblockcount().await?; - let refund_absolute_expiry = tx_lock_height + state.refund_timelock; - - info!("Checking refund timelock"); - if block_height < refund_absolute_expiry { - info!("Safe to redeem"); - - bitcoin_wallet - .broadcast_signed_transaction(redeem_tx) - .await?; - info!("Successfully redeemed bitcoin"); - } else { - info!("Refund timelock reached"); - - let tx_cancel = bitcoin::TxCancel::new( - &state.tx_lock, - state.refund_timelock, - state.a.public(), - state.B, - ); - - info!("Checking if the Bitcoin cancel transaction has been published"); - if bitcoin_wallet - .inner - .get_raw_transaction(tx_cancel.txid()) - .await - .is_err() - { - let sig_a = state.a.sign(tx_cancel.digest()); - let sig_b = state.tx_cancel_sig_bob.clone(); - - let tx_cancel = tx_cancel - .clone() - .add_signatures(&state.tx_lock, (state.a.public(), sig_a), (state.B, sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel"); - - // TODO: We should not fail if the transaction is already on the blockchain - bitcoin_wallet - .broadcast_signed_transaction(tx_cancel) - .await?; - } - - info!("Confirmed that Bitcoin cancel transaction is on the blockchain"); - - let tx_cancel_height = bitcoin_wallet - .transaction_block_height(tx_cancel.txid()) - .await; - let poll_until_bob_can_be_punished = poll_until_block_height_is_gte( - &bitcoin_wallet, - tx_cancel_height + state.punish_timelock, - ); - pin_mut!(poll_until_bob_can_be_punished); - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address); - - info!("Waiting for either Bitcoin refund or punish timelock"); - match select( - bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()), - poll_until_bob_can_be_punished, - ) - .await - { - Either::Left((tx_refund_published, ..)) => { - info!("Found Bitcoin refund transaction"); - - let s_a = monero::PrivateKey { - scalar: state.s_a.into_ed25519(), - }; - - let tx_refund_sig = tx_refund - .extract_signature_by_key(tx_refund_published, state.a.public())?; - let tx_refund_encsig = - state.a.encsign(state.S_b_bitcoin, tx_refund.digest()); - - let s_b = - bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?; - let s_b = monero::PrivateKey::from_scalar( - monero::Scalar::from_bytes_mod_order(s_b.to_bytes()), - ); - - monero_wallet - .create_and_load_wallet_for_output(s_a + s_b, state.v) - .await?; - info!("Successfully refunded monero"); - } - Either::Right(_) => { - info!("Punish timelock reached, attempting to punish Bob"); - - let tx_punish = bitcoin::TxPunish::new( - &tx_cancel, - &state.punish_address, - state.punish_timelock, - ); - - let sig_a = state.a.sign(tx_punish.digest()); - let sig_b = state.tx_punish_sig_bob.clone(); - - let sig_tx_punish = tx_punish.add_signatures( - &tx_cancel, - (state.a.public(), sig_a), - (state.B, sig_b), - )?; - - bitcoin_wallet - .broadcast_signed_transaction(sig_tx_punish) - .await?; - info!("Successfully punished Bob's inactivity by taking bitcoin"); - } - }; - } - } - Alice::BtcPunishable(state) => { - info!("Punish timelock reached, attempting to punish Bob"); - - let tx_cancel = bitcoin::TxCancel::new( - &state.tx_lock, - state.refund_timelock, - state.a.public(), - state.B, - ); - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address); - - info!("Checking if Bitcoin has already been refunded"); - - // TODO: Protect against transient errors so that we can correctly decide if the - // bitcoin has been refunded - match bitcoin_wallet - .inner - .get_raw_transaction(tx_refund.txid()) - .await - { - Ok(tx_refund_published) => { - info!("Bitcoin already refunded"); - - let s_a = monero::PrivateKey { - scalar: state.s_a.into_ed25519(), - }; - - let tx_refund_sig = tx_refund - .extract_signature_by_key(tx_refund_published, state.a.public())?; - let tx_refund_encsig = state.a.encsign(state.S_b_bitcoin, tx_refund.digest()); - - let s_b = bitcoin::recover(state.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)?; - let s_b = monero::PrivateKey::from_scalar( - monero::Scalar::from_bytes_mod_order(s_b.to_bytes()), - ); - - monero_wallet - .create_and_load_wallet_for_output(s_a + s_b, state.v) - .await?; - info!("Successfully refunded monero"); - } - Err(_) => { - info!("Bitcoin not yet refunded"); - - let tx_punish = bitcoin::TxPunish::new( - &tx_cancel, - &state.punish_address, - state.punish_timelock, - ); - - let sig_a = state.a.sign(tx_punish.digest()); - let sig_b = state.tx_punish_sig_bob.clone(); - - let sig_tx_punish = tx_punish.add_signatures( - &tx_cancel, - (state.a.public(), sig_a), - (state.B, sig_b), - )?; - - bitcoin_wallet - .broadcast_signed_transaction(sig_tx_punish) - .await?; - info!("Successfully punished Bob's inactivity by taking bitcoin"); - } - } - } - Alice::BtcRefunded { - view_key, - spend_key, - .. - } => { - info!("Bitcoin was refunded, attempting to refund monero"); - - monero_wallet - .create_and_load_wallet_for_output(spend_key, view_key) - .await?; - info!("Successfully refunded monero"); - } - }; - - Ok(()) -} - -pub async fn bob_recover( - bitcoin_wallet: crate::bitcoin::Wallet, - monero_wallet: crate::monero::Wallet, - state: Bob, -) -> Result<()> { - match state { - Bob::Negotiated { .. } | Bob::SwapComplete => { - info!("Nothing to do"); - } - Bob::BtcLocked { - state3: - State3 { - A, - b, - s_b, - refund_timelock, - refund_address, - tx_lock, - tx_cancel_sig_a, - tx_refund_encsig, - .. - }, - .. - } - | Bob::XmrLocked { - state4: - State4 { - A, - b, - s_b, - refund_timelock, - refund_address, - tx_lock, - tx_cancel_sig_a, - tx_refund_encsig, - .. - }, - .. - } - | Bob::BtcCancelled(State4 { - A, - b, - s_b, - refund_timelock, - refund_address, - tx_lock, - tx_cancel_sig_a, - tx_refund_encsig, - .. - }) => { - info!("Bitcoin may still be locked up, attempting to refund"); - - let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, A, b.public()); - - info!("Checking if the Bitcoin cancel transaction has been published"); - if bitcoin_wallet - .inner - .get_raw_transaction(tx_cancel.txid()) - .await - .is_err() - { - info!("Bitcoin cancel transaction not yet published"); - - let tx_lock_height = bitcoin_wallet - .transaction_block_height(tx_lock.txid()) - .await; - poll_until_block_height_is_gte(&bitcoin_wallet, tx_lock_height + refund_timelock) - .await; - - let sig_a = tx_cancel_sig_a.clone(); - let sig_b = b.sign(tx_cancel.digest()); - - let tx_cancel = tx_cancel - .clone() - .add_signatures(&tx_lock, (A, sig_a), (b.public(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel"); - - // TODO: We should not fail if the transaction is already on the blockchain - bitcoin_wallet - .broadcast_signed_transaction(tx_cancel) - .await?; - } - - info!("Confirmed that Bitcoin cancel transaction is on the blockchain"); - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); - let signed_tx_refund = { - let adaptor = Adaptor::>::default(); - let sig_a = - adaptor.decrypt_signature(&s_b.into_secp256k1(), tx_refund_encsig.clone()); - let sig_b = b.sign(tx_refund.digest()); - - tx_refund - .add_signatures(&tx_cancel, (A, sig_a), (b.public(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_refund") - }; - - // TODO: Check if Bitcoin has already been punished and provide a useful error - // message/log to the user if so - bitcoin_wallet - .broadcast_signed_transaction(signed_tx_refund) - .await?; - info!("Successfully refunded bitcoin"); - } - Bob::BtcRedeemed(state) => { - info!("Bitcoin was redeemed, attempting to redeem monero"); - - let tx_redeem = bitcoin::TxRedeem::new(&state.tx_lock, &state.redeem_address); - let tx_redeem_published = bitcoin_wallet - .inner - .get_raw_transaction(tx_redeem.txid()) - .await?; - - let tx_redeem_encsig = state.b.encsign(state.S_a_bitcoin, tx_redeem.digest()); - let tx_redeem_sig = - tx_redeem.extract_signature_by_key(tx_redeem_published, state.b.public())?; - - let s_a = bitcoin::recover(state.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?; - let s_a = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order( - s_a.to_bytes(), - )); - - let s_b = monero::PrivateKey { - scalar: state.s_b.into_ed25519(), - }; - - monero_wallet - .create_and_load_wallet_for_output(s_a + s_b, state.v) - .await?; - info!("Successfully redeemed monero") - } - Bob::Started { .. } => todo!(), - Bob::EncSigSent { .. } => todo!(), - }; - - Ok(()) -} diff --git a/swap/src/state.rs b/swap/src/state.rs index 13600d18..5d62b548 100644 --- a/swap/src/state.rs +++ b/swap/src/state.rs @@ -34,6 +34,7 @@ pub enum Alice { state: alice::State3, encrypted_signature: EncryptedSignature, }, + Cancelling(alice::State3), BtcCancelled(alice::State3), BtcPunishable(alice::State3), BtcRefunded { @@ -106,6 +107,7 @@ impl Display for Alice { Alice::BtcLocked(_) => f.write_str("Bitcoin locked"), Alice::XmrLocked(_) => f.write_str("Monero locked"), Alice::BtcRedeemable { .. } => f.write_str("Bitcoin redeemable"), + Alice::Cancelling(_) => f.write_str("Submitting TxCancel"), Alice::BtcCancelled(_) => f.write_str("Bitcoin cancel transaction published"), Alice::BtcPunishable(_) => f.write_str("Bitcoin punishable"), Alice::BtcRefunded { .. } => f.write_str("Monero refundable"), diff --git a/swap/tests/alice_safe_restart.rs b/swap/tests/alice_safe_restart.rs index 7a84b285..fbf2a618 100644 --- a/swap/tests/alice_safe_restart.rs +++ b/swap/tests/alice_safe_restart.rs @@ -109,10 +109,13 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { assert!(matches!(alice_state, AliceState::EncSignLearned {..})); - // todo: add db code here let alice_db = Database::open(alice_db_datadir.path()).unwrap(); let alice_state = alice_db.get_state(alice_swap_id).unwrap(); + if let swap::state::Swap::Alice(state) = alice_state.clone() { + assert!(matches!(state, swap::state::Alice::EncSignLearned {..})); + } + let (alice_state, _) = alice::swap::swap( AliceState::try_from(alice_state).unwrap(), alice_event_loop_handle,