From 28225f8643c25effc46671184d94ded75f795fec Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Tue, 3 Nov 2020 17:08:31 +1100 Subject: [PATCH] Implement swap recover function for Alice This introduces a lot of duplication between the binary and the library, but it's okay because this module should only be a temporary measure until we allow recovery to be handled by the original state machine. Also, fix a bug in `xmr_btc::alice::action_generator` caused by the incorrect assumption that Alice's ability to punish Bob could be determined before the cancel transaction hits the blockchain. --- swap/Cargo.toml | 1 + swap/src/lib.rs | 1 + swap/src/main.rs | 28 ++++--- swap/src/recover.rs | 187 +++++++++++++++++++++++++++++++++++++++++++ swap/src/storage.rs | 10 +-- swap/tests/e2e.rs | 15 ++-- xmr-btc/src/alice.rs | 93 +++++---------------- xmr-btc/src/bob.rs | 4 +- 8 files changed, 242 insertions(+), 97 deletions(-) create mode 100644 swap/src/recover.rs diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 71e88626..e8ab1323 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -14,6 +14,7 @@ base64 = "0.12" bitcoin = { version = "0.23", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version. bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "3be644cd9512c157d3337a189298b8257ed54d04" } derivative = "2" +ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] } futures = { version = "0.3", default-features = false } genawaiter = "0.99.1" libp2p = { version = "0.29", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] } diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 401184ed..9039aa74 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -6,6 +6,7 @@ pub mod bitcoin; pub mod bob; pub mod monero; pub mod network; +pub mod recover; pub mod state; pub mod storage; pub mod tor; diff --git a/swap/src/main.rs b/swap/src/main.rs index d7c1fe23..f5c34f89 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -16,7 +16,6 @@ use anyhow::Result; use futures::{channel::mpsc, StreamExt}; use libp2p::Multiaddr; use log::LevelFilter; -use prettytable::{row, Table}; use std::{io, io::Write, process, sync::Arc}; use structopt::StructOpt; use swap::{ @@ -31,13 +30,11 @@ use swap::{ use tempfile::tempdir; use tracing::info; -#[macro_use] -extern crate prettytable; - mod cli; mod trace; use cli::Options; +use swap::storage::Database; // TODO: Add root seed file instead of generating new seed each run. @@ -47,6 +44,9 @@ async fn main() -> Result<()> { trace::init_tracing(LevelFilter::Debug)?; + let db_dir = tempdir()?; + let db = Database::open(db_dir.path()).unwrap(); + match opt { Options::Alice { bitcoind_url, @@ -85,7 +85,11 @@ async fn main() -> Result<()> { let monero_wallet = Arc::new(monero::Wallet::new(monerod_url)); - swap_as_alice(bitcoin_wallet, monero_wallet, dblisten_addr, + swap_as_alice( + bitcoin_wallet, + monero_wallet, + db, + listen_addr, transport, behaviour, ) @@ -115,12 +119,10 @@ async fn main() -> Result<()> { let monero_wallet = Arc::new(monero::Wallet::new(monerod_url)); - let db = Database::open(db_dir.path()).unwrap(); - swap_as_bob( bitcoin_wallet, monero_wallet, - db + db, satoshis, alice_addr, transport, @@ -159,7 +161,15 @@ async fn swap_as_alice( transport: SwapTransport, behaviour: Alice, ) -> Result<()> { - alice::swap(bitcoin_wallet, monero_wallet, addr, transport, behaviour).await + alice::swap( + bitcoin_wallet, + monero_wallet, + db, + addr, + transport, + behaviour, + ) + .await } async fn swap_as_bob( diff --git a/swap/src/recover.rs b/swap/src/recover.rs new file mode 100644 index 00000000..87cb11ee --- /dev/null +++ b/swap/src/recover.rs @@ -0,0 +1,187 @@ +use crate::{ + monero::CreateWalletForOutput, + state::{Alice, Bob, Swap}, +}; +use anyhow::Result; +use futures::{ + future::{select, Either}, + pin_mut, +}; +use xmr_btc::bitcoin::{ + poll_until_block_height_is_gte, BroadcastSignedTransaction, TransactionBlockHeight, TxCancel, + TxPunish, TxRefund, WatchForRawTransaction, +}; + +pub async fn recover( + bitcoin_wallet: crate::bitcoin::Wallet, + monero_wallet: crate::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: crate::bitcoin::Wallet, + monero_wallet: crate::monero::Wallet, + state: Alice, +) -> Result<()> { + match state { + Alice::Handshaken(_) | Alice::BtcLocked(_) | Alice::SwapComplete => Ok(()), + Alice::XmrLocked(state) => { + let tx_cancel = TxCancel::new( + &state.tx_lock, + state.refund_timelock, + state.a.public(), + state.B.clone(), + ); + + // Ensure that TxCancel is on the blockchain + if bitcoin_wallet + .0 + .get_raw_transaction(tx_cancel.txid()) + .await + .is_err() + { + 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.clone(), 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?; + } + + 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 = TxRefund::new(&tx_cancel, &state.refund_address); + + match select( + bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()), + poll_until_bob_can_be_punished, + ) + .await + { + Either::Left((tx_refund_published, ..)) => { + 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.clone(), tx_refund.digest()); + + let s_b = xmr_btc::bitcoin::recover( + state.S_b_bitcoin, + tx_refund_sig, + tx_refund_encsig, + )?; + let s_b = monero::PrivateKey::from_scalar( + xmr_btc::monero::Scalar::from_bytes_mod_order(s_b.to_bytes()), + ); + + let s_a = monero::PrivateKey { + scalar: state.s_a.into_ed25519(), + }; + + monero_wallet + .create_and_load_wallet_for_output(s_a + s_b, state.v) + .await?; + } + Either::Right(_) => { + let tx_punish = + TxPunish::new(&tx_cancel, &state.punish_address, state.punish_timelock); + + let sig_a = state.a.sign(tx_punish.digest()); + let sig_b = state.tx_cancel_sig_bob.clone(); + + let sig_tx_punish = tx_punish.add_signatures( + &tx_cancel, + (state.a.public(), sig_a), + (state.B.clone(), sig_b), + )?; + + bitcoin_wallet + .broadcast_signed_transaction(sig_tx_punish) + .await?; + } + }; + + Ok(()) + } + Alice::BtcRedeemable { redeem_tx, .. } => { + bitcoin_wallet + .broadcast_signed_transaction(redeem_tx) + .await?; + Ok(()) + } + Alice::BtcPunishable(state) => { + let tx_cancel = TxCancel::new( + &state.tx_lock, + state.refund_timelock, + state.a.public(), + state.B.clone(), + ); + + let tx_punish = TxPunish::new(&tx_cancel, &state.punish_address, state.punish_timelock); + + let sig_a = state.a.sign(tx_punish.digest()); + let sig_b = state.tx_cancel_sig_bob.clone(); + + let sig_tx_punish = tx_punish.add_signatures( + &tx_cancel, + (state.a.public(), sig_a), + (state.B.clone(), sig_b), + )?; + + bitcoin_wallet + .broadcast_signed_transaction(sig_tx_punish) + .await?; + + Ok(()) + } + Alice::BtcRefunded { + view_key, + spend_key, + .. + } => { + monero_wallet + .create_and_load_wallet_for_output(spend_key, view_key) + .await?; + Ok(()) + } + } +} + +pub async fn bob_recover( + _bitcoin_wallet: crate::bitcoin::Wallet, + _monero_wallet: crate::monero::Wallet, + _state: Bob, +) -> Result<()> { + todo!() +} diff --git a/swap/src/storage.rs b/swap/src/storage.rs index 4a0664b6..47c6ce11 100644 --- a/swap/src/storage.rs +++ b/swap/src/storage.rs @@ -33,7 +33,7 @@ impl Database { .context("Could not flush db") } - pub fn get_latest_state(&self, swap_id: Uuid) -> anyhow::Result { + pub fn get_state(&self, swap_id: Uuid) -> anyhow::Result { let key = serialize(&swap_id)?; let encoded = self @@ -103,11 +103,11 @@ mod tests { .expect("Failed to save first state"); let recovered_1 = db - .get_latest_state(swap_id_1) + .get_state(swap_id_1) .expect("Failed to recover first state"); let recovered_2 = db - .get_latest_state(swap_id_2) + .get_state(swap_id_2) .expect("Failed to recover second state"); assert_eq!(recovered_1, state_1); @@ -126,7 +126,7 @@ mod tests { .await .expect("Failed to save state the first time"); let recovered = db - .get_latest_state(swap_id) + .get_state(swap_id) .expect("Failed to recover state the first time"); // We insert and recover twice to ensure database implementation allows the @@ -135,7 +135,7 @@ mod tests { .await .expect("Failed to save state the second time"); let recovered = db - .get_latest_state(swap_id) + .get_state(swap_id) .expect("Failed to recover state the second time"); assert_eq!(recovered, state); diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index df72ce36..bebb052b 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -5,7 +5,8 @@ mod e2e_test { use libp2p::Multiaddr; use monero_harness::Monero; use std::sync::Arc; - use swap::{alice, bob, network::transport::build}; + use swap::{alice, bob, network::transport::build, storage::Database}; + use tempfile::tempdir; use testcontainers::clients::Cli; // NOTE: For some reason running these tests overflows the stack. In order to @@ -48,12 +49,10 @@ mod e2e_test { .await .unwrap(); - let (monero, _container) = Monero::new(&cli, Some("swap_".to_string()), vec![ - "alice".to_string(), - "bob".to_string(), - ]) - .await - .unwrap(); + let (monero, _container) = + Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) + .await + .unwrap(); monero .init(vec![("alice", xmr_alice), ("bob", xmr_bob)]) .await @@ -68,7 +67,7 @@ mod e2e_test { let alice_transport = build(alice_behaviour.identity()).unwrap(); let db_dir = tempdir().unwrap(); - let db = Database::open(std::path::Path::new("/home/luckysori/test/xmr_btc_swap")).unwrap(); + let db = Database::open(db_dir.path()).unwrap(); let alice_swap = alice::swap( alice_btc_wallet.clone(), alice_xmr_wallet.clone(), diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index b0382e63..fa35a140 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -97,7 +97,7 @@ where #[derive(Debug)] enum SwapFailed { BeforeBtcLock(Reason), - AfterXmrLock { tx_lock_height: u32, reason: Reason }, + AfterXmrLock(Reason), } /// Reason why the swap has failed. @@ -114,9 +114,7 @@ where #[derive(Debug)] enum RefundFailed { - BtcPunishable { - tx_cancel_was_published: bool, - }, + BtcPunishable, /// Could not find Alice's signature on the refund transaction witness /// stack. BtcRefundSignature, @@ -167,12 +165,7 @@ where .await { Either::Left((encsig, _)) => encsig, - Either::Right(_) => { - return Err(SwapFailed::AfterXmrLock { - reason: Reason::BtcExpired, - tx_lock_height, - }) - } + Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)), }; tracing::debug!("select returned redeem encsig from message"); @@ -191,10 +184,7 @@ where &tx_redeem.digest(), &tx_redeem_encsig, ) - .map_err(|_| SwapFailed::AfterXmrLock { - reason: Reason::InvalidEncryptedSignature, - tx_lock_height, - })?; + .map_err(|_| SwapFailed::AfterXmrLock(Reason::InvalidEncryptedSignature))?; let sig_a = a.sign(tx_redeem.digest()); let sig_b = @@ -217,12 +207,7 @@ where .await { Either::Left(_) => {} - Either::Right(_) => { - return Err(SwapFailed::AfterXmrLock { - reason: Reason::BtcExpired, - tx_lock_height, - }) - } + Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)), }; Ok(()) @@ -233,19 +218,8 @@ where error!("swap failed: {:?}", err); } - if let Err(SwapFailed::AfterXmrLock { - reason: Reason::BtcExpired, - tx_lock_height, - }) = swap_result - { + if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result { let refund_result: Result<(), RefundFailed> = async { - let poll_until_bob_can_be_punished = poll_until_block_height_is_gte( - bitcoin_client.as_ref(), - tx_lock_height + refund_timelock + punish_timelock, - ) - .shared(); - pin_mut!(poll_until_bob_can_be_punished); - let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); let signed_tx_cancel = { @@ -260,19 +234,19 @@ where co.yield_(Action::CancelBtc(signed_tx_cancel)).await; - match select( - bitcoin_client.watch_for_raw_transaction(tx_cancel.txid()), - poll_until_bob_can_be_punished.clone(), + bitcoin_client + .watch_for_raw_transaction(tx_cancel.txid()) + .await; + + let tx_cancel_height = bitcoin_client + .transaction_block_height(tx_cancel.txid()) + .await; + let poll_until_bob_can_be_punished = poll_until_block_height_is_gte( + bitcoin_client.as_ref(), + tx_cancel_height + punish_timelock, ) - .await - { - Either::Left(_) => {} - Either::Right(_) => { - return Err(RefundFailed::BtcPunishable { - tx_cancel_was_published: false, - }) - } - }; + .shared(); + pin_mut!(poll_until_bob_can_be_punished); let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); let tx_refund_published = match select( @@ -282,11 +256,7 @@ where .await { Either::Left((tx, _)) => tx, - Either::Right(_) => { - return Err(RefundFailed::BtcPunishable { - tx_cancel_was_published: true, - }); - } + Either::Right(_) => return Err(RefundFailed::BtcPunishable), }; let s_a = monero::PrivateKey { @@ -321,32 +291,9 @@ where // transaction with his refund transaction. Alice would then need to carry on // with the refund on Monero. Doing so may be too verbose with the current, // linear approach. A different design may be required - if let Err(RefundFailed::BtcPunishable { - tx_cancel_was_published, - }) = refund_result - { + if let Err(RefundFailed::BtcPunishable) = refund_result { let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone()); - - if !tx_cancel_was_published { - let tx_cancel_txid = tx_cancel.txid(); - let signed_tx_cancel = { - let sig_a = a.sign(tx_cancel.digest()); - let sig_b = tx_cancel_sig_bob; - - tx_cancel - .clone() - .add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel") - }; - - co.yield_(Action::CancelBtc(signed_tx_cancel)).await; - - let _ = bitcoin_client - .watch_for_raw_transaction(tx_cancel_txid) - .await; - } - let tx_punish = bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock); let tx_punish_txid = tx_punish.txid(); diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 26c26374..e9b98ac8 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -507,10 +507,10 @@ pub struct State2 { btc: bitcoin::Amount, pub xmr: monero::Amount, pub refund_timelock: u32, - punish_timelock: u32, + pub punish_timelock: u32, pub refund_address: bitcoin::Address, pub redeem_address: bitcoin::Address, - punish_address: bitcoin::Address, + pub punish_address: bitcoin::Address, pub tx_lock: bitcoin::TxLock, pub tx_cancel_sig_a: Signature, pub tx_refund_encsig: EncryptedSignature,