From 64b021daf4959161d9761eca16e116283088fc9f Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Mon, 7 Dec 2020 12:47:21 +1100 Subject: [PATCH] Save Alice states in db --- swap/src/alice/swap.rs | 387 ++++++++++++++++++++++++++++------------- swap/src/bin/swap.rs | 5 + swap/src/recover.rs | 2 + swap/src/state.rs | 10 +- swap/tests/e2e.rs | 16 +- 5 files changed, 301 insertions(+), 119 deletions(-) diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index 380b542c..6354f6cc 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -10,11 +10,15 @@ use crate::{ wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin, }, }, + bitcoin, bitcoin::EncryptedSignature, network::request_response::AliceToBob, + state, + state::{Alice, Swap}, + storage::Database, SwapAmounts, }; -use anyhow::Result; +use anyhow::{bail, Result}; use async_recursion::async_recursion; use futures::{ future::{select, Either}, @@ -22,8 +26,13 @@ use futures::{ }; use libp2p::request_response::ResponseChannel; use rand::{CryptoRng, RngCore}; -use std::{fmt, sync::Arc}; +use std::{ + convert::{TryFrom, TryInto}, + fmt, + sync::Arc, +}; use tracing::info; +use uuid::Uuid; use xmr_btc::{ alice::{State0, State3}, bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction}, @@ -102,12 +111,124 @@ impl fmt::Display for AliceState { } } +impl TryFrom<&AliceState> for state::Swap { + type Error = anyhow::Error; + + fn try_from(alice_state: &AliceState) -> Result { + use state::{Alice::*, Swap::Alice}; + + let state = match alice_state { + AliceState::Started { .. } => bail!("Does not support storing `Started state."), + AliceState::Negotiated { state3, .. } => Negotiated(state3.clone()), + AliceState::BtcLocked { state3, .. } => BtcLocked(state3.clone()), + AliceState::XmrLocked { state3 } => XmrLocked(state3.clone()), + AliceState::EncSignLearned { + state3, + encrypted_signature, + } => EncSignLearned { + state: state3.clone(), + encrypted_signature: encrypted_signature.clone(), + }, + AliceState::BtcRedeemed => SwapComplete, + AliceState::BtcCancelled { state3, .. } => BtcCancelled(state3.clone()), + AliceState::BtcRefunded { .. } => SwapComplete, + AliceState::BtcPunishable { state3, .. } => BtcPunishable(state3.clone()), + AliceState::XmrRefunded => SwapComplete, + // TODO(Franck): it may be more efficient to store the fact that we already want to + // abort + AliceState::Cancelling { state3 } => XmrLocked(state3.clone()), + AliceState::Punished => SwapComplete, + AliceState::SafelyAborted => SwapComplete, + }; + + Ok(Alice(state)) + } +} + +impl TryFrom for AliceState { + type Error = anyhow::Error; + + fn try_from(db_state: Swap) -> Result { + use AliceState::*; + if let Swap::Alice(state) = db_state { + let alice_state = match state { + Alice::Negotiated(state3) => Negotiated { + channel: None, + amounts: SwapAmounts { + btc: state3.btc, + xmr: state3.xmr, + }, + state3, + }, + Alice::BtcLocked(state3) => BtcLocked { + channel: None, + amounts: SwapAmounts { + btc: state3.btc, + xmr: state3.xmr, + }, + state3, + }, + Alice::XmrLocked(state3) => XmrLocked { state3 }, + Alice::BtcRedeemable { .. } => bail!("BtcRedeemable state is unexpected"), + Alice::EncSignLearned { + state, + encrypted_signature, + } => EncSignLearned { + state3: state, + encrypted_signature, + }, + Alice::BtcCancelled(state) => { + let tx_cancel = bitcoin::TxCancel::new( + &state.tx_lock, + state.refund_timelock, + state.a.public(), + state.B, + ); + + BtcCancelled { + state3: state, + tx_cancel, + } + } + Alice::BtcPunishable(state) => { + 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); + BtcPunishable { + tx_refund, + state3: state, + } + } + Alice::BtcRefunded { + state, spend_key, .. + } => BtcRefunded { + spend_key, + state3: state, + }, + Alice::SwapComplete => { + // TODO(Franck): Better fine grain + AliceState::SafelyAborted + } + }; + Ok(alice_state) + } else { + bail!("Alice swap state expected.") + } + } +} + pub async fn swap( state: AliceState, event_loop_handle: EventLoopHandle, bitcoin_wallet: Arc, monero_wallet: Arc, config: Config, + swap_id: Uuid, + db: Database, ) -> Result<(AliceState, EventLoopHandle)> { run_until( state, @@ -116,6 +237,8 @@ pub async fn swap( bitcoin_wallet, monero_wallet, config, + swap_id, + db, ) .await } @@ -139,6 +262,7 @@ pub fn is_xmr_locked(state: &AliceState) -> bool { // State machine driver for swap execution #[async_recursion] +#[allow(clippy::too_many_arguments)] pub async fn run_until( state: AliceState, is_target_state: fn(&AliceState) -> bool, @@ -146,6 +270,8 @@ pub async fn run_until( bitcoin_wallet: Arc, monero_wallet: Arc, config: Config, + swap_id: Uuid, + db: Database, ) -> Result<(AliceState, EventLoopHandle)> { info!("Current state:{}", state); if is_target_state(&state) { @@ -156,17 +282,23 @@ pub async fn run_until( let (channel, state3) = negotiate(state0, amounts, &mut event_loop_handle, config).await?; + let state = AliceState::Negotiated { + channel: Some(channel), + amounts, + state3, + }; + + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; run_until( - AliceState::Negotiated { - channel: Some(channel), - amounts, - state3, - }, + state, is_target_state, event_loop_handle, bitcoin_wallet, monero_wallet, config, + swap_id, + db, ) .await } @@ -175,7 +307,7 @@ pub async fn run_until( channel, amounts, } => { - match channel { + let state = match channel { Some(channel) => { let _ = wait_for_locked_bitcoin( state3.tx_lock.txid(), @@ -184,128 +316,113 @@ pub async fn run_until( ) .await?; - run_until( - AliceState::BtcLocked { - channel: Some(channel), - amounts, - state3, - }, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - config, - ) - .await + AliceState::BtcLocked { + channel: Some(channel), + amounts, + state3, + } } None => { tracing::info!("Cannot resume swap from negotiated state, aborting"); // Alice did not lock Xmr yet - run_until( - AliceState::SafelyAborted, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - config, - ) - .await + AliceState::SafelyAborted } - } + }; + + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; + run_until( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet, + monero_wallet, + config, + swap_id, + db, + ) + .await } AliceState::BtcLocked { channel, amounts, state3, - } => match channel { - Some(channel) => { - lock_xmr( - channel, - amounts, - state3.clone(), - &mut event_loop_handle, - monero_wallet.clone(), - ) - .await?; + } => { + let state = match channel { + Some(channel) => { + lock_xmr( + channel, + amounts, + state3.clone(), + &mut event_loop_handle, + monero_wallet.clone(), + ) + .await?; - run_until( - AliceState::XmrLocked { state3 }, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - config, - ) - .await - } - None => { - tracing::info!("Cannot resume swap from BTC locked state, aborting"); + AliceState::XmrLocked { state3 } + } + None => { + tracing::info!("Cannot resume swap from BTC locked state, aborting"); - // Alice did not lock Xmr yet - run_until( - AliceState::SafelyAborted, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - config, - ) - .await - } - }, + // Alice did not lock Xmr yet + AliceState::SafelyAborted + } + }; + + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; + run_until( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet, + monero_wallet, + config, + swap_id, + db, + ) + .await + } AliceState::XmrLocked { state3 } => { // todo: match statement and wait for t1 can probably be expressed more cleanly - match state3.current_epoch(bitcoin_wallet.as_ref()).await? { + let state = match state3.current_epoch(bitcoin_wallet.as_ref()).await? { Epoch::T0 => { let wait_for_enc_sig = wait_for_bitcoin_encrypted_signature( &mut event_loop_handle, config.monero_max_finality_time, ); - let t1_timeout = state3.wait_for_t1(bitcoin_wallet.as_ref()); + let state3_clone = state3.clone(); + let t1_timeout = state3_clone.wait_for_t1(bitcoin_wallet.as_ref()); - tokio::select! { - _ = t1_timeout => { - run_until( - AliceState::Cancelling { state3 }, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - config, - ) - .await - } - enc_sig = wait_for_enc_sig => { - run_until( - AliceState::EncSignLearned { - state3, - encrypted_signature: enc_sig?, - }, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - config, - ) - .await - } + pin_mut!(wait_for_enc_sig); + pin_mut!(t1_timeout); + + match select(t1_timeout, wait_for_enc_sig).await { + Either::Left(_) => AliceState::Cancelling { state3 }, + Either::Right((enc_sig, _)) => AliceState::EncSignLearned { + state3, + encrypted_signature: enc_sig?, + }, } } - _ => { - run_until( - AliceState::Cancelling { state3 }, - is_target_state, - event_loop_handle, - bitcoin_wallet, - monero_wallet, - config, - ) - .await - } - } - } + _ => AliceState::Cancelling { state3 }, + }; + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; + run_until( + state, + is_target_state, + event_loop_handle, + bitcoin_wallet.clone(), + monero_wallet, + config, + swap_id, + db, + ) + .await + } AliceState::EncSignLearned { state3, encrypted_signature, @@ -320,13 +437,18 @@ pub async fn run_until( ) { Ok(tx) => tx, Err(_) => { + let state = AliceState::Cancelling { state3 }; + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; return run_until( - AliceState::Cancelling { state3 }, + state, is_target_state, event_loop_handle, bitcoin_wallet, monero_wallet, config, + swap_id, + db, ) .await; } @@ -342,13 +464,18 @@ pub async fn run_until( ) .await?; + let state = AliceState::BtcRedeemed; + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; run_until( - AliceState::BtcRedeemed, + state, is_target_state, event_loop_handle, bitcoin_wallet, monero_wallet, config, + swap_id, + db, ) .await } @@ -363,13 +490,18 @@ pub async fn run_until( ) .await?; + let state = AliceState::BtcCancelled { state3, tx_cancel }; + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; run_until( - AliceState::BtcCancelled { state3, tx_cancel }, + state, is_target_state, event_loop_handle, bitcoin_wallet, monero_wallet, config, + swap_id, + db, ) .await } @@ -390,13 +522,17 @@ pub async fn run_until( // TODO(Franck): Review error handling match published_refund_tx { None => { - run_until( - AliceState::BtcPunishable { tx_refund, state3 }, - is_target_state, + let state = AliceState::BtcPunishable { tx_refund, state3 }; + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; + swap( + state, event_loop_handle, bitcoin_wallet.clone(), monero_wallet, config, + swap_id, + db, ) .await } @@ -409,13 +545,18 @@ pub async fn run_until( state3.S_b_bitcoin, )?; + let state = AliceState::BtcRefunded { spend_key, state3 }; + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; run_until( - AliceState::BtcRefunded { spend_key, state3 }, + state, is_target_state, event_loop_handle, bitcoin_wallet.clone(), monero_wallet, config, + swap_id, + db, ) .await } @@ -428,7 +569,10 @@ pub async fn run_until( .create_and_load_wallet_for_output(spend_key, view_key) .await?; - Ok((AliceState::XmrRefunded, event_loop_handle)) + let state = AliceState::XmrRefunded; + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; + Ok((state, event_loop_handle)) } AliceState::BtcPunishable { tx_refund, state3 } => { let signed_tx_punish = build_bitcoin_punish_transaction( @@ -454,13 +598,18 @@ pub async fn run_until( match select(punish_tx_finalised, refund_tx_seen).await { Either::Left(_) => { + let state = AliceState::Punished; + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; run_until( - AliceState::Punished, + state, is_target_state, event_loop_handle, bitcoin_wallet.clone(), monero_wallet, config, + swap_id, + db, ) .await } @@ -472,14 +621,18 @@ pub async fn run_until( state3.a.clone(), state3.S_b_bitcoin, )?; - + let state = AliceState::BtcRefunded { spend_key, state3 }; + let db_state = (&state).try_into()?; + db.insert_latest_state(swap_id, db_state).await?; run_until( - AliceState::BtcRefunded { spend_key, state3 }, + state, is_target_state, event_loop_handle, bitcoin_wallet.clone(), monero_wallet, config, + swap_id, + db, ) .await } diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 45414bb0..6ab3e4e7 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -117,12 +117,17 @@ async fn main() -> Result<()> { let (mut event_loop, handle) = alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen_addr)?; + let swap_id = Uuid::new_v4(); + info!("Swap id: {}", swap_id); + let swap = alice::swap::swap( alice_state, handle, bitcoin_wallet.clone(), monero_wallet.clone(), config, + swap_id, + db, ); let _event_loop = tokio::spawn(async move { event_loop.run().await }); diff --git a/swap/src/recover.rs b/swap/src/recover.rs index f67ef7a6..de675ac3 100644 --- a/swap/src/recover.rs +++ b/swap/src/recover.rs @@ -157,6 +157,8 @@ pub async fn alice_recover( } }; } + 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"); diff --git a/swap/src/state.rs b/swap/src/state.rs index e59c976d..278c439c 100644 --- a/swap/src/state.rs +++ b/swap/src/state.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; -use xmr_btc::{alice, bob, monero, serde::monero_private_key}; +use xmr_btc::{alice, bitcoin::EncryptedSignature, bob, monero, serde::monero_private_key}; #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] @@ -15,10 +15,16 @@ pub enum Alice { Negotiated(alice::State3), BtcLocked(alice::State3), XmrLocked(alice::State3), + // TODO(Franck): Delete this state as it is not used in alice::swap BtcRedeemable { state: alice::State3, redeem_tx: bitcoin::Transaction, }, + EncSignLearned { + state: alice::State3, + encrypted_signature: EncryptedSignature, + }, + BtcCancelled(alice::State3), BtcPunishable(alice::State3), BtcRefunded { state: alice::State3, @@ -67,9 +73,11 @@ 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::BtcCancelled(_) => f.write_str("Bitcoin cancel transaction published"), Alice::BtcPunishable(_) => f.write_str("Bitcoin punishable"), Alice::BtcRefunded { .. } => f.write_str("Monero refundable"), Alice::SwapComplete => f.write_str("Swap complete"), + Alice::EncSignLearned { .. } => f.write_str("Encrypted signature learned"), } } } diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 7fc281ef..debebcbe 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -77,12 +77,17 @@ async fn happy_path() { ) .await; + let alice_db_datadir = tempdir().unwrap(); + let alice_db = Database::open(alice_db_datadir.path()).unwrap(); + let alice_swap_fut = alice::swap::swap( alice_state, alice_swarm_handle, alice_btc_wallet.clone(), alice_xmr_wallet.clone(), config, + Uuid::new_v4(), + alice_db, ); let _alice_swarm_fut = tokio::spawn(async move { alice_swarm_driver.run().await }); @@ -188,12 +193,17 @@ async fn alice_punishes_if_bob_never_acts_after_fund() { let _bob_swarm_fut = tokio::spawn(async move { bob_swarm_driver.run().await }); + let alice_db_datadir = tempdir().unwrap(); + let alice_db = Database::open(alice_db_datadir.path()).unwrap(); + let alice_fut = alice::swap::swap( alice_state, alice_swarm_handle, alice_btc_wallet.clone(), alice_xmr_wallet.clone(), Config::regtest(), + Uuid::new_v4(), + alice_db, ); let _alice_swarm_fut = tokio::spawn(async move { alice_swarm.run().await }); @@ -311,6 +321,8 @@ async fn both_refund() { alice_btc_wallet.clone(), alice_xmr_wallet.clone(), Config::regtest(), + todo!(), + todo!(), ); tokio::spawn(async move { alice_swarm_driver.run().await }); @@ -331,6 +343,8 @@ async fn both_refund() { alice_btc_wallet.clone(), alice_xmr_wallet.clone(), Config::regtest(), + todo!(), + todo!(), ) .await .unwrap(); @@ -408,8 +422,8 @@ async fn init_alice( xmr: xmr_to_swap, }; + let rng = &mut OsRng; let (alice_state, alice_behaviour) = { - let rng = &mut OsRng; let a = bitcoin::SecretKey::new_random(rng); let s_a = cross_curve_dleq::Scalar::random(rng); let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng);