diff --git a/swap/src/bob/swap.rs b/swap/src/bob/swap.rs index 7368bb6c..ae1b41a8 100644 --- a/swap/src/bob/swap.rs +++ b/swap/src/bob/swap.rs @@ -1,5 +1,7 @@ use crate::{ bob::{event_loop::EventLoopHandle, execution::negotiate}, + state, + state::Bob, storage::Database, SwapAmounts, }; @@ -17,6 +19,7 @@ use xmr_btc::{ // The same data structure is used for swap execution and recovery. // This allows for a seamless transition from a failed swap to recovery. +#[derive(Debug, Clone)] pub enum BobState { Started { state0: bob::State0, @@ -53,6 +56,32 @@ impl fmt::Display for BobState { } } +impl From for state::Bob { + fn from(bob_state: BobState) -> Self { + match bob_state { + BobState::Started { + state0, + amounts, + addr, + } => Bob::Started { + state0, + amounts, + addr, + }, + BobState::Negotiated(state2, peer_id) => Bob::Negotiated { state2, peer_id }, + BobState::BtcLocked(state3, peer_id) => Bob::BtcLocked { state3, peer_id }, + BobState::XmrLocked(state4, peer_id) => Bob::XmrLocked { state4, peer_id }, + BobState::EncSigSent(state4, peer_id) => Bob::EncSigSent { state4, peer_id }, + BobState::BtcRedeemed(state5) => Bob::BtcRedeemed(state5), + BobState::Cancelled(state4) => Bob::BtcCancelled(state4), + BobState::BtcRefunded(_) + | BobState::XmrRedeemed + | BobState::Punished + | BobState::SafelyAborted => Bob::SwapComplete, + } + } +} + pub async fn swap( state: BobState, event_loop_handle: EventLoopHandle, @@ -131,8 +160,13 @@ where bitcoin_wallet.clone(), ) .await?; + + let state = BobState::Negotiated(state2, alice_peer_id); + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, state::Swap::Bob(db_state)) + .await?; run_until( - BobState::Negotiated(state2, alice_peer_id), + state, is_target_state, event_loop_handle, db, @@ -146,9 +180,13 @@ where BobState::Negotiated(state2, alice_peer_id) => { // Alice and Bob have exchanged info let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?; - // db.insert_latest_state(state); + + let state = BobState::BtcLocked(state3, alice_peer_id); + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, state::Swap::Bob(db_state)) + .await?; run_until( - BobState::BtcLocked(state3, alice_peer_id), + state, is_target_state, event_loop_handle, db, @@ -168,8 +206,12 @@ where .watch_for_lock_xmr(monero_wallet.as_ref(), msg2) .await?; + let state = BobState::XmrLocked(state4, alice_peer_id); + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, state::Swap::Bob(db_state)) + .await?; run_until( - BobState::XmrLocked(state4, alice_peer_id), + state, is_target_state, event_loop_handle, db, @@ -192,8 +234,12 @@ where .send_message3(alice_peer_id.clone(), tx_redeem_encsig) .await?; + let state = BobState::EncSigSent(state, alice_peer_id); + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, state::Swap::Bob(db_state)) + .await?; run_until( - BobState::EncSigSent(state, alice_peer_id), + state, is_target_state, event_loop_handle, db, @@ -209,10 +255,14 @@ where let redeem_watcher = state.watch_for_redeem_btc(bitcoin_wallet.as_ref()); let t1_timeout = state.wait_for_t1(bitcoin_wallet.as_ref()); + // TODO(Franck): Check if db save and run_until can be factorized tokio::select! { val = redeem_watcher => { + let state = BobState::BtcRedeemed(val?); + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, state::Swap::Bob(db_state)).await?; run_until( - BobState::BtcRedeemed(val?), + state, is_target_state, event_loop_handle, db, @@ -230,15 +280,18 @@ where state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?; } + let state = BobState::Cancelled(state); + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, state::Swap::Bob(db_state)).await?; run_until( - BobState::Cancelled(state), - is_target_state, + state, + is_target_state, event_loop_handle, db, bitcoin_wallet, monero_wallet, rng, - swap_id + swap_id ) .await @@ -248,8 +301,13 @@ where BobState::BtcRedeemed(state) => { // Bob redeems XMR using revealed s_a state.claim_xmr(monero_wallet.as_ref()).await?; + + let state = BobState::XmrRedeemed; + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, state::Swap::Bob(db_state)) + .await?; run_until( - BobState::XmrRedeemed, + state, is_target_state, event_loop_handle, db, @@ -261,37 +319,31 @@ where .await } BobState::Cancelled(state) => { + // TODO // Bob has cancelled the swap - match state.current_epoch(bitcoin_wallet.as_ref()).await? { + let state = match state.current_epoch(bitcoin_wallet.as_ref()).await? { Epoch::T0 => panic!("Cancelled before t1??? Something is really wrong"), Epoch::T1 => { state.refund_btc(bitcoin_wallet.as_ref()).await?; - run_until( - BobState::BtcRefunded(state), - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .await + BobState::BtcRefunded(state) } - Epoch::T2 => { - run_until( - BobState::Punished, - is_target_state, - event_loop_handle, - db, - bitcoin_wallet, - monero_wallet, - rng, - swap_id, - ) - .await - } - } + Epoch::T2 => BobState::Punished, + }; + + let db_state = state.clone().into(); + db.insert_latest_state(swap_id, state::Swap::Bob(db_state)) + .await?; + run_until( + state, + is_target_state, + event_loop_handle, + db, + bitcoin_wallet, + monero_wallet, + rng, + swap_id, + ) + .await } BobState::BtcRefunded(state4) => Ok(BobState::BtcRefunded(state4)), BobState::Punished => Ok(BobState::Punished), diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 3b31a652..3f8ace7b 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case)] -use serde::{Deserialize, Serialize}; +use ::serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; pub mod alice; @@ -10,6 +10,7 @@ pub mod cli; pub mod monero; pub mod network; pub mod recover; +pub mod serde; pub mod state; pub mod storage; pub mod tor; @@ -31,7 +32,7 @@ pub enum Rsp { } /// XMR/BTC swap amounts. -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] // TODO(Franck): review necessity of this struct pub struct SwapAmounts { /// Amount of BTC to swap. diff --git a/swap/src/recover.rs b/swap/src/recover.rs index de675ac3..47c04362 100644 --- a/swap/src/recover.rs +++ b/swap/src/recover.rs @@ -24,9 +24,12 @@ use futures::{ }; use sha2::Sha256; use tracing::info; -use xmr_btc::bitcoin::{ - poll_until_block_height_is_gte, BroadcastSignedTransaction, TransactionBlockHeight, - WatchForRawTransaction, +use xmr_btc::{ + bitcoin::{ + poll_until_block_height_is_gte, BroadcastSignedTransaction, TransactionBlockHeight, + WatchForRawTransaction, + }, + bob::{State3, State4}, }; pub async fn recover( @@ -366,18 +369,53 @@ pub async fn bob_recover( state: Bob, ) -> Result<()> { match state { - Bob::Handshaken(_) | Bob::SwapComplete => { + Bob::Negotiated { .. } | Bob::SwapComplete => { info!("Nothing to do"); } - Bob::BtcLocked(state) | Bob::XmrLocked(state) | Bob::BtcRefundable(state) => { + 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( - &state.tx_lock, - state.refund_timelock, - state.A, - state.b.public(), - ); + 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 @@ -389,20 +427,17 @@ pub async fn bob_recover( info!("Bitcoin cancel transaction not yet published"); let tx_lock_height = bitcoin_wallet - .transaction_block_height(state.tx_lock.txid()) + .transaction_block_height(tx_lock.txid()) + .await; + poll_until_block_height_is_gte(&bitcoin_wallet, tx_lock_height + refund_timelock) .await; - poll_until_block_height_is_gte( - &bitcoin_wallet, - tx_lock_height + state.refund_timelock, - ) - .await; - let sig_a = state.tx_cancel_sig_a.clone(); - let sig_b = state.b.sign(tx_cancel.digest()); + let sig_a = tx_cancel_sig_a.clone(); + let sig_b = b.sign(tx_cancel.digest()); let tx_cancel = tx_cancel .clone() - .add_signatures(&state.tx_lock, (state.A, sig_a), (state.b.public(), sig_b)) + .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 @@ -413,15 +448,15 @@ pub async fn bob_recover( info!("Confirmed that Bitcoin cancel transaction is on the blockchain"); - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address); + let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); let signed_tx_refund = { let adaptor = Adaptor::>::default(); - let sig_a = adaptor - .decrypt_signature(&state.s_b.into_secp256k1(), state.tx_refund_encsig.clone()); - let sig_b = state.b.sign(tx_refund.digest()); + 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, (state.A, sig_a), (state.b.public(), sig_b)) + .add_signatures(&tx_cancel, (A, sig_a), (b.public(), sig_b)) .expect("sig_{a,b} to be valid signatures for tx_refund") }; @@ -459,6 +494,8 @@ pub async fn bob_recover( .await?; info!("Successfully redeemed monero") } + Bob::Started { .. } => todo!(), + Bob::EncSigSent { .. } => todo!(), }; Ok(()) diff --git a/swap/src/serde.rs b/swap/src/serde.rs new file mode 100644 index 00000000..754946bd --- /dev/null +++ b/swap/src/serde.rs @@ -0,0 +1,47 @@ +pub mod peer_id { + use libp2p::PeerId; + use serde::{de::Error, Deserialize, Deserializer, Serializer}; + + pub fn serialize(peer_id: &PeerId, serializer: S) -> Result + where + S: Serializer, + { + let string = peer_id.to_string(); + serializer.serialize_str(&string) + } + + #[allow(dead_code)] + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + let peer_id = string.parse().map_err(D::Error::custom)?; + + Ok(peer_id) + } + + #[cfg(test)] + mod tests { + use super::*; + use serde::Serialize; + use spectral::prelude::*; + + #[derive(Serialize)] + struct SerializablePeerId(#[serde(with = "super")] PeerId); + + #[test] + fn maker_id_serializes_as_expected() { + let peer_id = SerializablePeerId( + "QmfUfpC2frwFvcDzpspnfZitHt5wct6n4kpG5jzgRdsxkY" + .parse() + .unwrap(), + ); + + let got = serde_json::to_string(&peer_id).expect("failed to serialize peer id"); + + assert_that(&got) + .is_equal_to(r#""QmfUfpC2frwFvcDzpspnfZitHt5wct6n4kpG5jzgRdsxkY""#.to_string()); + } + } +} diff --git a/swap/src/state.rs b/swap/src/state.rs index 278c439c..59c64ec8 100644 --- a/swap/src/state.rs +++ b/swap/src/state.rs @@ -1,3 +1,5 @@ +use crate::SwapAmounts; +use libp2p::{core::Multiaddr, PeerId}; use serde::{Deserialize, Serialize}; use std::fmt::Display; use xmr_btc::{alice, bitcoin::EncryptedSignature, bob, monero, serde::monero_private_key}; @@ -37,11 +39,33 @@ pub enum Alice { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub enum Bob { - Handshaken(bob::State2), - BtcLocked(bob::State2), - XmrLocked(bob::State2), - BtcRedeemed(bob::State2), - BtcRefundable(bob::State2), + Started { + state0: bob::State0, + amounts: SwapAmounts, + addr: Multiaddr, + }, + Negotiated { + state2: bob::State2, + #[serde(with = "crate::serde::peer_id")] + peer_id: PeerId, + }, + BtcLocked { + state3: bob::State3, + #[serde(with = "crate::serde::peer_id")] + peer_id: PeerId, + }, + XmrLocked { + state4: bob::State4, + #[serde(with = "crate::serde::peer_id")] + peer_id: PeerId, + }, + EncSigSent { + state4: bob::State4, + #[serde(with = "crate::serde::peer_id")] + peer_id: PeerId, + }, + BtcRedeemed(bob::State5), + BtcCancelled(bob::State4), SwapComplete, } @@ -85,12 +109,14 @@ impl Display for Alice { impl Display for Bob { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Bob::Handshaken(_) => f.write_str("Handshake complete"), - Bob::BtcLocked(_) | Bob::XmrLocked(_) | Bob::BtcRefundable(_) => { + Bob::Negotiated { .. } => f.write_str("Handshake complete"), + Bob::BtcLocked { .. } | Bob::XmrLocked { .. } | Bob::BtcCancelled(_) => { f.write_str("Bitcoin refundable") } Bob::BtcRedeemed(_) => f.write_str("Monero redeemable"), Bob::SwapComplete => f.write_str("Swap complete"), + Bob::Started { .. } => f.write_str("Swap started"), + Bob::EncSigSent { .. } => f.write_str("Encrypted signature sent"), } } } diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 54f22d69..61c072ab 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -346,7 +346,7 @@ impl_from_child_enum!(State3, State); impl_from_child_enum!(State4, State); impl_from_child_enum!(State5, State); -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct State0 { b: bitcoin::SecretKey, s_b: cross_curve_dleq::Scalar, @@ -560,25 +560,25 @@ impl State2 { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct State3 { - A: bitcoin::PublicKey, - b: bitcoin::SecretKey, - s_b: cross_curve_dleq::Scalar, + pub A: bitcoin::PublicKey, + pub b: bitcoin::SecretKey, + pub s_b: cross_curve_dleq::Scalar, S_a_monero: monero::PublicKey, S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, #[serde(with = "::bitcoin::util::amount::serde::as_sat")] btc: bitcoin::Amount, xmr: monero::Amount, - refund_timelock: u32, + pub refund_timelock: u32, punish_timelock: u32, - refund_address: bitcoin::Address, + pub refund_address: bitcoin::Address, redeem_address: bitcoin::Address, punish_address: bitcoin::Address, - tx_lock: bitcoin::TxLock, - tx_cancel_sig_a: Signature, - tx_refund_encsig: EncryptedSignature, + pub tx_lock: bitcoin::TxLock, + pub tx_cancel_sig_a: Signature, + pub tx_refund_encsig: EncryptedSignature, } impl State3 { @@ -626,11 +626,11 @@ impl State3 { } } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct State4 { pub A: bitcoin::PublicKey, pub b: bitcoin::SecretKey, - s_b: cross_curve_dleq::Scalar, + pub s_b: cross_curve_dleq::Scalar, S_a_monero: monero::PublicKey, pub S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, @@ -639,12 +639,12 @@ pub struct State4 { xmr: monero::Amount, pub refund_timelock: u32, punish_timelock: u32, - refund_address: bitcoin::Address, + pub refund_address: bitcoin::Address, pub redeem_address: bitcoin::Address, punish_address: bitcoin::Address, pub tx_lock: bitcoin::TxLock, - tx_cancel_sig_a: Signature, - tx_refund_encsig: EncryptedSignature, + pub tx_cancel_sig_a: Signature, + pub tx_refund_encsig: EncryptedSignature, } impl State4 { @@ -823,25 +823,25 @@ impl State4 { } } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct State5 { A: bitcoin::PublicKey, - b: bitcoin::SecretKey, + pub b: bitcoin::SecretKey, #[serde(with = "monero_private_key")] s_a: monero::PrivateKey, - s_b: cross_curve_dleq::Scalar, + pub s_b: cross_curve_dleq::Scalar, S_a_monero: monero::PublicKey, - S_a_bitcoin: bitcoin::PublicKey, - v: monero::PrivateViewKey, + pub S_a_bitcoin: bitcoin::PublicKey, + pub v: monero::PrivateViewKey, #[serde(with = "::bitcoin::util::amount::serde::as_sat")] btc: bitcoin::Amount, xmr: monero::Amount, refund_timelock: u32, punish_timelock: u32, refund_address: bitcoin::Address, - redeem_address: bitcoin::Address, + pub redeem_address: bitcoin::Address, punish_address: bitcoin::Address, - tx_lock: bitcoin::TxLock, + pub tx_lock: bitcoin::TxLock, tx_refund_encsig: EncryptedSignature, tx_cancel_sig: Signature, }