diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a2baf45..070c4a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Introduced a cooperative Monero redeem feature for Bob to request from Alice if Bob is punished for not refunding in time. Alice can choose to cooperate but is not obligated to do so. This change is backwards compatible. To attempt recovery, resume a swap in the "Bitcoin punished" state. Success depends on Alice being active and still having a record of the swap. Note that Alice's cooperation is voluntary and recovery is not guaranteed + ## [0.13.2] - 2024-07-02 - CLI: Buffer received transfer proofs for later processing if we're currently running a different swap diff --git a/swap/migrations/20240615140942_btcpunished_update.sql b/swap/migrations/20240615140942_btcpunished_update.sql new file mode 100644 index 00000000..b4c9a8a5 --- /dev/null +++ b/swap/migrations/20240615140942_btcpunished_update.sql @@ -0,0 +1,135 @@ +-- This migration script modifies swap states to be compatible with the new state structure introduced in PR #1676. +-- The following changes are made: +-- 1. Bob: BtcPunished state now has a new attribute 'state' (type: State6), 'tx_lock_id' (type: string) remains the same +-- 2. Bob: State6 has two new attributes: 'v' (monero viewkey) and 'monero_wallet_restore_blockheight' (type: BlockHeight) +-- State6 is used in BtcPunished, CancelTimelockExpired, BtcCancelled, BtcRefunded states +-- 3. Alice: BtcPunished state now has a new attribute 'state3' (type: State3) + +-- Alice: Add new attribute 'state3' (type: State3) to the BtcPunished state by copying it from the BtcLocked state +UPDATE swap_states SET + state = json_replace( -- Replaces "{"Alice":{"Done":"BtcPunished"}}" with "{"Alice": {"Done": "BtcPunished": {"state": } }}" + state, + '$.Alice.Done', + json_object( + 'BtcPunished', + ( + SELECT json_extract(states.state, '$.Alice.BtcLocked') -- Read state3 object from BtcLocked + FROM swap_states AS states + WHERE + states.swap_id = swap_states.swap_id -- swap_states.swap_id is id of the BtcPunished row + AND json_extract(states.state, '$.Alice.BtcLocked') IS NOT NULL -- Filters out only the BtcLocked state + ) + ) + ) +WHERE json_extract(state, '$.Alice.Done') = 'BtcPunished'; -- Apply update only to BtcPunished state rows + +-- Bob: Add new attribute 'state6' (type: State6) to the BtcPunished state by copying it from the BtcCancelled state +-- and add new State6 attributes 'v' and 'monero_wallet_restore_blockheight' from the BtcLocked state +UPDATE swap_states SET + state = json_replace( + state, + '$.Bob', -- Replace '{"Bob":{"Done": {"BtcPunished": {"tx_lock_id":"..."} }}}' with {"Bob":{"BtcPunished":{"state":{}, "tx_lock_id": "..."}}} + json_object( + 'BtcPunished', -- {"Bob":{"BtcPunished":{}} + json_object( + 'state', -- {"Bob":{"BtcPunished":{"state": {}}} + json_insert( + ( -- object that we insert properties into (original state6 from BtcCancelled state) + SELECT json_extract(states.state, '$.Bob.BtcCancelled') -- Get state6 from BtcCancelled state + FROM swap_states AS states + WHERE + states.swap_id = swap_states.swap_id + AND json_extract(states.state, '$.Bob.BtcCancelled') IS NOT NULL -- Filters out only the BtcCancelled state + ), + '$.v', -- {"Bob":{"BtcPunished":{"state": {..., "v": "..."}, "tx_lock_id": "..."}}} + ( -- Get v property from BtcLocked state + SELECT json_extract(states.state, '$.Bob.BtcLocked.state3.v') + FROM swap_states AS states + WHERE + states.swap_id = swap_states.swap_id -- swap_states.swap_id is id of the BtcPunished row + AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL -- Filters out only the BtcLocked state + ), + '$.monero_wallet_restore_blockheight', -- { "Bob": { "BtcPunished":{"state": {..., "monero_wallet_restore_blockheight": {"height":...}} }, "tx_lock_id": "..."} } } + ( -- Get monero_wallet_restore_blockheight property from BtcLocked state + SELECT json_extract(states.state, '$.Bob.BtcLocked.monero_wallet_restore_blockheight') + FROM swap_states AS states + WHERE + states.swap_id = swap_states.swap_id -- swap_states.swap_id is id of the BtcPunished row, states.swap_id is id of the row that we are looking for + AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL -- Filters out only the BtcLocked state + ) + ), + 'tx_lock_id', -- Insert tx_lock_id BtcPunished -> {"Bob": {"Done": {"BtcPunished": {"state":{}, "tx_lock_id": "..."} } } + json_extract(state, '$.Bob.Done.BtcPunished.tx_lock_id') -- Gets tx_lock_id from original state row + ) + ) + ) +WHERE json_extract(state, '$.Bob.Done.BtcPunished') IS NOT NULL; -- Apply update only to BtcPunished state rows + +-- Bob: Add new State6 attributes 'v' and 'monero_wallet_restore_blockheight' to the BtcRefunded state +UPDATE swap_states SET + state = json_insert( + state, -- Object that we insert properties into (original state from the row) + '$.Bob.Done.BtcRefunded.v', -- {"Bob":{"BtcRefunded":{..., "v": "..."}}} + ( + SELECT json_extract(states.state, '$.Bob.BtcLocked.state3.v') + FROM swap_states AS states + WHERE + states.swap_id = swap_states.swap_id -- swap_states.swap_id is id of the BtcRefunded row, states.swap_id is id of the row that we are looking for + AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL + ), + '$.Bob.Done.BtcRefunded.monero_wallet_restore_blockheight', -- {"Bob":{"BtcRefunded":{..., "monero_wallet_restore_blockheight": {"height":...}} }} + ( + SELECT json_extract(states.state, '$.Bob.BtcLocked.monero_wallet_restore_blockheight') + FROM swap_states AS states + WHERE + states.swap_id = swap_states.swap_id + AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL + ) + ) +WHERE json_extract(state, '$.Bob.Done.BtcRefunded') IS NOT NULL; -- Apply update only to BtcRefunded state rows + +-- Bob: Add new State6 attributes 'v' and 'monero_wallet_restore_blockheight' to the BtcCancelled state +UPDATE swap_states SET + state = json_insert( + state, + '$.Bob.BtcCancelled.v', + ( + SELECT json_extract(states.state, '$.Bob.BtcLocked.state3.v') + FROM swap_states AS states + WHERE + states.swap_id = swap_states.swap_id + AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL + ), + '$.Bob.BtcCancelled.monero_wallet_restore_blockheight', + ( + SELECT json_extract(states.state, '$.Bob.BtcLocked.monero_wallet_restore_blockheight') + FROM swap_states AS states + WHERE + states.swap_id = swap_states.swap_id + AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL + ) + ) +WHERE json_extract(state, '$.Bob.BtcCancelled') IS NOT NULL; -- Apply update only to BtcCancelled state rows + +-- Bob: Add new State6 attributes 'v' and 'monero_wallet_restore_blockheight' to the CancelTimelockExpired state +UPDATE swap_states SET + state = json_insert( + state, + '$.Bob.CancelTimelockExpired.v', + ( + SELECT json_extract(states.state, '$.Bob.BtcLocked.state3.v') + FROM swap_states AS states + WHERE + states.swap_id = swap_states.swap_id + AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL + ), + '$.Bob.CancelTimelockExpired.monero_wallet_restore_blockheight', + ( + SELECT json_extract(states.state, '$.Bob.BtcLocked.monero_wallet_restore_blockheight') + FROM swap_states AS states + WHERE + states.swap_id = swap_states.swap_id + AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL + ) + ) +WHERE json_extract(state, '$.Bob.CancelTimelockExpired') IS NOT NULL; -- Apply update only to CancelTimelockExpired state rows \ No newline at end of file diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index 0d6de9a7..8e3ac4c3 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -1,5 +1,7 @@ use crate::asb::{Behaviour, OutEvent, Rate}; use crate::monero::Amount; +use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason; +use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected}; use crate::network::quote::BidQuote; use crate::network::swap_setup::alice::WalletSnapshot; use crate::network::transfer_proof; @@ -253,6 +255,59 @@ where channel }.boxed()); } + SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemRequested { swap_id, channel, peer }) => { + let swap_peer = self.db.get_peer_id(swap_id).await; + let swap_state = self.db.get_state(swap_id).await; + + let (swap_peer, swap_state) = match (swap_peer, swap_state) { + (Ok(peer), Ok(state)) => (peer, state), + _ => { + tracing::warn!( + swap_id = %swap_id, + received_from = %peer, + reason = "swap not found", + "Rejecting cooperative XMR redeem request" + ); + if self.swarm.behaviour_mut().cooperative_xmr_redeem.send_response(channel, Rejected { swap_id, reason: CooperativeXmrRedeemRejectReason::UnknownSwap }).is_err() { + tracing::error!(swap_id = %swap_id, "Failed to reject cooperative XMR redeem request"); + } + continue; + } + }; + + if swap_peer != peer { + tracing::warn!( + swap_id = %swap_id, + received_from = %peer, + expected_from = %swap_peer, + reason = "unexpected peer", + "Rejecting cooperative XMR redeem request" + ); + if self.swarm.behaviour_mut().cooperative_xmr_redeem.send_response(channel, Rejected { swap_id, reason: CooperativeXmrRedeemRejectReason::MaliciousRequest }).is_err() { + tracing::error!(swap_id = %swap_id, "Failed to reject cooperative XMR redeem request"); + } + continue; + } + + let State::Alice (AliceState::BtcPunished { state3 }) = swap_state else { + tracing::warn!( + swap_id = %swap_id, + reason = "swap is in invalid state", + "Rejecting cooperative XMR redeem request" + ); + if self.swarm.behaviour_mut().cooperative_xmr_redeem.send_response(channel, Rejected { swap_id, reason: CooperativeXmrRedeemRejectReason::SwapInvalidState }).is_err() { + tracing::error!(swap_id = %swap_id, "Failed to reject cooperative XMR redeem request"); + } + continue; + }; + + if self.swarm.behaviour_mut().cooperative_xmr_redeem.send_response(channel, Fullfilled { swap_id, s_a: state3.s_a }).is_err() { + tracing::error!(peer = %peer, "Failed to respond to cooperative XMR redeem request"); + continue; + } + + tracing::info!(swap_id = %swap_id, peer = %peer, "Fullfilled cooperative XMR redeem request"); + } SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::Registered { rendezvous_node, ttl, namespace })) => { tracing::info!("Successfully registered with rendezvous node: {} with namespace: {} and TTL: {:?}", rendezvous_node, namespace, ttl); } diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index 7b85f8fb..268a3cba 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -5,7 +5,9 @@ use crate::network::rendezvous::XmrBtcNamespace; use crate::network::swap_setup::alice; use crate::network::swap_setup::alice::WalletSnapshot; use crate::network::transport::authenticate_and_multiplex; -use crate::network::{encrypted_signature, quote, transfer_proof}; +use crate::network::{ + cooperative_xmr_redeem_after_punish, encrypted_signature, quote, transfer_proof, +}; use crate::protocol::alice::State3; use anyhow::{anyhow, Error, Result}; use futures::FutureExt; @@ -76,6 +78,11 @@ pub mod behaviour { channel: ResponseChannel<()>, peer: PeerId, }, + CooperativeXmrRedeemRequested { + channel: ResponseChannel, + swap_id: Uuid, + peer: PeerId, + }, Rendezvous(libp2p::rendezvous::client::Event), Failure { peer: PeerId, @@ -114,6 +121,7 @@ pub mod behaviour { pub quote: quote::Behaviour, pub swap_setup: alice::Behaviour, pub transfer_proof: transfer_proof::Behaviour, + pub cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::Behaviour, pub encrypted_signature: encrypted_signature::Behaviour, pub identify: Identify, @@ -160,6 +168,7 @@ pub mod behaviour { ), transfer_proof: transfer_proof::alice(), encrypted_signature: encrypted_signature::alice(), + cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::alice(), ping: Ping::new(PingConfig::new().with_keep_alive(true)), identify: Identify::new(identifyConfig), } diff --git a/swap/src/asb/recovery/cancel.rs b/swap/src/asb/recovery/cancel.rs index 8da1508f..f71b3e38 100644 --- a/swap/src/asb/recovery/cancel.rs +++ b/swap/src/asb/recovery/cancel.rs @@ -38,7 +38,7 @@ pub async fn cancel( // Alice already in final state | AliceState::BtcRedeemed | AliceState::XmrRefunded - | AliceState::BtcPunished + | AliceState::BtcPunished { .. } | AliceState::SafelyAborted => bail!("Swap is in state {} which is not cancelable", state), }; diff --git a/swap/src/asb/recovery/punish.rs b/swap/src/asb/recovery/punish.rs index e94abac8..e1d1ad28 100644 --- a/swap/src/asb/recovery/punish.rs +++ b/swap/src/asb/recovery/punish.rs @@ -38,7 +38,7 @@ pub async fn punish( // Alice already in final state | AliceState::BtcRedeemed | AliceState::XmrRefunded - | AliceState::BtcPunished + | AliceState::BtcPunished { .. } | AliceState::SafelyAborted => bail!(Error::SwapNotPunishable(state)), }; @@ -46,7 +46,9 @@ pub async fn punish( let txid = state3.punish_btc(&bitcoin_wallet).await?; - let state = AliceState::BtcPunished; + let state = AliceState::BtcPunished { + state3: state3.clone(), + }; db.insert_latest_state(swap_id, state.clone().into()) .await?; diff --git a/swap/src/asb/recovery/redeem.rs b/swap/src/asb/recovery/redeem.rs index e4642feb..47f4a048 100644 --- a/swap/src/asb/recovery/redeem.rs +++ b/swap/src/asb/recovery/redeem.rs @@ -81,7 +81,7 @@ pub async fn redeem( | AliceState::BtcPunishable { .. } | AliceState::BtcRedeemed | AliceState::XmrRefunded - | AliceState::BtcPunished + | AliceState::BtcPunished { .. } | AliceState::SafelyAborted => bail!( "Cannot redeem swap {} because it is in state {} which cannot be manually redeemed", swap_id, diff --git a/swap/src/asb/recovery/refund.rs b/swap/src/asb/recovery/refund.rs index 64e5c3f3..3067a8f6 100644 --- a/swap/src/asb/recovery/refund.rs +++ b/swap/src/asb/recovery/refund.rs @@ -55,7 +55,7 @@ pub async fn refund( AliceState::BtcRedeemTransactionPublished { .. } | AliceState::BtcRedeemed | AliceState::XmrRefunded - | AliceState::BtcPunished + | AliceState::BtcPunished { .. } | AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)), }; diff --git a/swap/src/asb/recovery/safely_abort.rs b/swap/src/asb/recovery/safely_abort.rs index ad162f8d..8a9e1d95 100644 --- a/swap/src/asb/recovery/safely_abort.rs +++ b/swap/src/asb/recovery/safely_abort.rs @@ -31,7 +31,7 @@ pub async fn safely_abort(swap_id: Uuid, db: Arc) -> Result bail!( "Cannot safely abort swap {} because it is in state {} which cannot be safely aborted", swap_id, diff --git a/swap/src/cli/behaviour.rs b/swap/src/cli/behaviour.rs index 2ca8448f..72dc5891 100644 --- a/swap/src/cli/behaviour.rs +++ b/swap/src/cli/behaviour.rs @@ -1,7 +1,11 @@ +use crate::monero::Scalar; +use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason; use crate::network::quote::BidQuote; use crate::network::rendezvous::XmrBtcNamespace; use crate::network::swap_setup::bob; -use crate::network::{encrypted_signature, quote, redial, transfer_proof}; +use crate::network::{ + cooperative_xmr_redeem_after_punish, encrypted_signature, quote, redial, transfer_proof, +}; use crate::protocol::bob::State2; use crate::{bitcoin, env}; use anyhow::{anyhow, Error, Result}; @@ -28,6 +32,16 @@ pub enum OutEvent { EncryptedSignatureAcknowledged { id: RequestId, }, + CooperativeXmrRedeemFulfilled { + id: RequestId, + s_a: Scalar, + swap_id: uuid::Uuid, + }, + CooperativeXmrRedeemRejected { + id: RequestId, + reason: CooperativeXmrRedeemRejectReason, + swap_id: uuid::Uuid, + }, AllRedialAttemptsExhausted { peer: PeerId, }, @@ -64,6 +78,7 @@ pub struct Behaviour { pub quote: quote::Behaviour, pub swap_setup: bob::Behaviour, pub transfer_proof: transfer_proof::Behaviour, + pub cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::Behaviour, pub encrypted_signature: encrypted_signature::Behaviour, pub redial: redial::Behaviour, pub identify: Identify, @@ -91,6 +106,7 @@ impl Behaviour { swap_setup: bob::Behaviour::new(env_config, bitcoin_wallet), transfer_proof: transfer_proof::bob(), encrypted_signature: encrypted_signature::bob(), + cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::bob(), redial: redial::Behaviour::new(alice, Duration::from_secs(2)), ping: Ping::new(PingConfig::new().with_keep_alive(true)), identify: Identify::new(identifyConfig), diff --git a/swap/src/cli/cancel_and_refund.rs b/swap/src/cli/cancel_and_refund.rs index 5484d7cb..7ab4e4b0 100644 --- a/swap/src/cli/cancel_and_refund.rs +++ b/swap/src/cli/cancel_and_refund.rs @@ -31,8 +31,16 @@ pub async fn cancel( let state = db.get_state(swap_id).await?.try_into()?; let state6 = match state { - BobState::BtcLocked { state3, .. } => state3.cancel(), - BobState::XmrLockProofReceived { state, .. } => state.cancel(), + BobState::BtcLocked { + state3, + monero_wallet_restore_blockheight, + .. + } => state3.cancel(monero_wallet_restore_blockheight), + BobState::XmrLockProofReceived { + state, + monero_wallet_restore_blockheight, + .. + } => state.cancel(monero_wallet_restore_blockheight), BobState::XmrLocked(state4) => state4.cancel(), BobState::EncSigSent(state4) => state4.cancel(), BobState::CancelTimelockExpired(state6) => state6, @@ -81,6 +89,7 @@ pub async fn cancel( // We cannot cancel because Alice has already cancelled and punished afterwards Ok(ExpiredTimelocks::Punish { .. }) => { let state = BobState::BtcPunished { + state: state6.clone(), tx_lock_id: state6.tx_lock_id(), }; db.insert_latest_state(swap_id, state.clone().into()) @@ -118,8 +127,15 @@ pub async fn refund( let state = db.get_state(swap_id).await?.try_into()?; let state6 = match state { - BobState::BtcLocked { state3, .. } => state3.cancel(), - BobState::XmrLockProofReceived { state, .. } => state.cancel(), + BobState::BtcLocked { + state3, + monero_wallet_restore_blockheight, + } => state3.cancel(monero_wallet_restore_blockheight), + BobState::XmrLockProofReceived { + state, + monero_wallet_restore_blockheight, + .. + } => state.cancel(monero_wallet_restore_blockheight), BobState::XmrLocked(state4) => state4.cancel(), BobState::EncSigSent(state4) => state4.cancel(), BobState::CancelTimelockExpired(state6) => state6, @@ -157,6 +173,7 @@ pub async fn refund( // We have been punished Ok(ExpiredTimelocks::Punish { .. }) => { let state = BobState::BtcPunished { + state: state6.clone(), tx_lock_id: state6.tx_lock_id(), }; db.insert_latest_state(swap_id, state.clone().into()) diff --git a/swap/src/cli/event_loop.rs b/swap/src/cli/event_loop.rs index fd7b4308..53ee65b8 100644 --- a/swap/src/cli/event_loop.rs +++ b/swap/src/cli/event_loop.rs @@ -1,6 +1,7 @@ use crate::bitcoin::EncryptedSignature; use crate::cli::behaviour::{Behaviour, OutEvent}; use crate::monero; +use crate::network::cooperative_xmr_redeem_after_punish::{Request, Response}; use crate::network::encrypted_signature; use crate::network::quote::BidQuote; use crate::network::swap_setup::bob::NewSwap; @@ -27,6 +28,7 @@ pub struct EventLoop { // these streams represents outgoing requests that we have to make quote_requests: bmrng::RequestReceiverStream<(), BidQuote>, + cooperative_xmr_redeem_requests: bmrng::RequestReceiverStream, encrypted_signatures: bmrng::RequestReceiverStream, swap_setup_requests: bmrng::RequestReceiverStream>, @@ -36,7 +38,7 @@ pub struct EventLoop { inflight_quote_requests: HashMap>, inflight_encrypted_signature_requests: HashMap>, inflight_swap_setup: Option>>, - + inflight_cooperative_xmr_redeem_requests: HashMap>, /// The sender we will use to relay incoming transfer proofs. transfer_proof: bmrng::RequestSender, /// The future representing the successful handling of an incoming transfer @@ -60,7 +62,7 @@ impl EventLoop { let transfer_proof = bmrng::channel_with_timeout(1, Duration::from_secs(60)); let encrypted_signature = bmrng::channel(1); let quote = bmrng::channel_with_timeout(1, Duration::from_secs(60)); - + let cooperative_xmr_redeem = bmrng::channel_with_timeout(1, Duration::from_secs(60)); let event_loop = EventLoop { swap_id, swarm, @@ -68,10 +70,12 @@ impl EventLoop { swap_setup_requests: execution_setup.1.into(), transfer_proof: transfer_proof.0, encrypted_signatures: encrypted_signature.1.into(), + cooperative_xmr_redeem_requests: cooperative_xmr_redeem.1.into(), quote_requests: quote.1.into(), inflight_quote_requests: HashMap::default(), inflight_swap_setup: None, inflight_encrypted_signature_requests: HashMap::default(), + inflight_cooperative_xmr_redeem_requests: HashMap::default(), pending_transfer_proof: OptionFuture::from(None), db, }; @@ -80,6 +84,7 @@ impl EventLoop { swap_setup: execution_setup.0, transfer_proof: transfer_proof.1, encrypted_signature: encrypted_signature.0, + cooperative_xmr_redeem: cooperative_xmr_redeem.0, quote: quote.0, }; @@ -176,6 +181,16 @@ impl EventLoop { let _ = responder.respond(()); } } + SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemFulfilled { id, swap_id, s_a }) => { + if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) { + let _ = responder.respond(Response::Fullfilled { s_a, swap_id }); + } + } + SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemRejected { id, swap_id, reason }) => { + if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) { + let _ = responder.respond(Response::Rejected { reason, swap_id }); + } + } SwarmEvent::Behaviour(OutEvent::AllRedialAttemptsExhausted { peer }) if peer == self.alice_peer_id => { tracing::error!("Exhausted all re-dial attempts to Alice"); return; @@ -234,7 +249,14 @@ impl EventLoop { let _ = self.swarm.behaviour_mut().transfer_proof.send_response(response_channel, ()); self.pending_transfer_proof = OptionFuture::from(None); - } + }, + + Some((swap_id, responder)) = self.cooperative_xmr_redeem_requests.next().fuse(), if self.is_connected_to_alice() => { + let id = self.swarm.behaviour_mut().cooperative_xmr_redeem.send_request(&self.alice_peer_id, Request { + swap_id + }); + self.inflight_cooperative_xmr_redeem_requests.insert(id, responder); + }, } } } @@ -250,6 +272,7 @@ pub struct EventLoopHandle { transfer_proof: bmrng::RequestReceiver, encrypted_signature: bmrng::RequestSender, quote: bmrng::RequestSender<(), BidQuote>, + cooperative_xmr_redeem: bmrng::RequestSender, } impl EventLoopHandle { @@ -274,6 +297,9 @@ impl EventLoopHandle { tracing::debug!("Requesting quote"); Ok(self.quote.send_receive(()).await?) } + pub async fn request_cooperative_xmr_redeem(&mut self, swap_id: Uuid) -> Result { + Ok(self.cooperative_xmr_redeem.send_receive(swap_id).await?) + } pub async fn send_encrypted_signature( &mut self, diff --git a/swap/src/database/alice.rs b/swap/src/database/alice.rs index 4ed61790..fcf3ead3 100644 --- a/swap/src/database/alice.rs +++ b/swap/src/database/alice.rs @@ -70,12 +70,12 @@ pub enum Alice { Done(AliceEndState), } -#[derive(Copy, Clone, strum::Display, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Clone, strum::Display, Debug, Deserialize, Serialize, PartialEq)] pub enum AliceEndState { SafelyAborted, BtcRedeemed, XmrRefunded, - BtcPunished, + BtcPunished { state3: alice::State3 }, } impl From for Alice { @@ -173,7 +173,9 @@ impl From for Alice { transfer_proof, state3: state3.as_ref().clone(), }, - AliceState::BtcPunished => Alice::Done(AliceEndState::BtcPunished), + AliceState::BtcPunished { state3 } => Alice::Done(AliceEndState::BtcPunished { + state3: state3.as_ref().clone(), + }), AliceState::SafelyAborted => Alice::Done(AliceEndState::SafelyAborted), } } @@ -277,7 +279,9 @@ impl From for AliceState { AliceEndState::SafelyAborted => AliceState::SafelyAborted, AliceEndState::BtcRedeemed => AliceState::BtcRedeemed, AliceEndState::XmrRefunded => AliceState::XmrRefunded, - AliceEndState::BtcPunished => AliceState::BtcPunished, + AliceEndState::BtcPunished { state3 } => AliceState::BtcPunished { + state3: Box::new(state3), + }, }, } } diff --git a/swap/src/database/bob.rs b/swap/src/database/bob.rs index 25117763..735f45a2 100644 --- a/swap/src/database/bob.rs +++ b/swap/src/database/bob.rs @@ -33,6 +33,10 @@ pub enum Bob { EncSigSent { state4: bob::State4, }, + BtcPunished { + state: bob::State6, + tx_lock_id: bitcoin::Txid, + }, BtcRedeemed(bob::State5), CancelTimelockExpired(bob::State6), BtcCancelled(bob::State6), @@ -44,7 +48,6 @@ pub enum BobEndState { SafelyAborted, XmrRedeemed { tx_lock_id: bitcoin::Txid }, BtcRefunded(Box), - BtcPunished { tx_lock_id: bitcoin::Txid }, } impl From for Bob { @@ -79,13 +82,11 @@ impl From for Bob { BobState::BtcRedeemed(state5) => Bob::BtcRedeemed(state5), BobState::CancelTimelockExpired(state6) => Bob::CancelTimelockExpired(state6), BobState::BtcCancelled(state6) => Bob::BtcCancelled(state6), + BobState::BtcPunished { state, tx_lock_id } => Bob::BtcPunished { state, tx_lock_id }, BobState::BtcRefunded(state6) => Bob::Done(BobEndState::BtcRefunded(Box::new(state6))), BobState::XmrRedeemed { tx_lock_id } => { Bob::Done(BobEndState::XmrRedeemed { tx_lock_id }) } - BobState::BtcPunished { tx_lock_id } => { - Bob::Done(BobEndState::BtcPunished { tx_lock_id }) - } BobState::SafelyAborted => Bob::Done(BobEndState::SafelyAborted), } } @@ -123,11 +124,11 @@ impl From for BobState { Bob::BtcRedeemed(state5) => BobState::BtcRedeemed(state5), Bob::CancelTimelockExpired(state6) => BobState::CancelTimelockExpired(state6), Bob::BtcCancelled(state6) => BobState::BtcCancelled(state6), + Bob::BtcPunished { state, tx_lock_id } => BobState::BtcPunished { state, tx_lock_id }, Bob::Done(end_state) => match end_state { BobEndState::SafelyAborted => BobState::SafelyAborted, BobEndState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id }, BobEndState::BtcRefunded(state6) => BobState::BtcRefunded(*state6), - BobEndState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id }, }, } } @@ -148,6 +149,7 @@ impl fmt::Display for Bob { Bob::BtcRedeemed(_) => f.write_str("Monero redeemable"), Bob::Done(end_state) => write!(f, "Done: {}", end_state), Bob::EncSigSent { .. } => f.write_str("Encrypted signature sent"), + Bob::BtcPunished { .. } => f.write_str("Bitcoin punished"), } } } diff --git a/swap/src/database/sqlite.rs b/swap/src/database/sqlite.rs index 6cddd50e..576dc362 100644 --- a/swap/src/database/sqlite.rs +++ b/swap/src/database/sqlite.rs @@ -417,9 +417,8 @@ mod tests { let db = setup_test_db().await.unwrap(); let state_1 = State::Alice(AliceState::BtcRedeemed); - let state_2 = State::Alice(AliceState::BtcPunished); - let state_3 = State::Alice(AliceState::SafelyAborted); - let state_4 = State::Bob(BobState::SafelyAborted); + let state_2 = State::Alice(AliceState::SafelyAborted); + let state_3 = State::Bob(BobState::SafelyAborted); let swap_id_1 = Uuid::new_v4(); let swap_id_2 = Uuid::new_v4(); @@ -429,10 +428,7 @@ mod tests { db.insert_latest_state(swap_id_1, state_2.clone()) .await .unwrap(); - db.insert_latest_state(swap_id_1, state_3.clone()) - .await - .unwrap(); - db.insert_latest_state(swap_id_2, state_4.clone()) + db.insert_latest_state(swap_id_2, state_3.clone()) .await .unwrap(); @@ -440,11 +436,10 @@ mod tests { assert_eq!(latest_loaded.len(), 2); - assert!(latest_loaded.contains(&(swap_id_1, state_3))); - assert!(latest_loaded.contains(&(swap_id_2, state_4))); + assert!(latest_loaded.contains(&(swap_id_1, state_2))); + assert!(latest_loaded.contains(&(swap_id_2, state_3))); assert!(!latest_loaded.contains(&(swap_id_1, state_1))); - assert!(!latest_loaded.contains(&(swap_id_1, state_2))); } #[tokio::test] diff --git a/swap/src/network.rs b/swap/src/network.rs index 22243783..527c04fc 100644 --- a/swap/src/network.rs +++ b/swap/src/network.rs @@ -1,6 +1,7 @@ mod impl_from_rr_event; pub mod cbor_request_response; +pub mod cooperative_xmr_redeem_after_punish; pub mod encrypted_signature; pub mod json_pull_codec; pub mod quote; diff --git a/swap/src/network/cooperative_xmr_redeem_after_punish.rs b/swap/src/network/cooperative_xmr_redeem_after_punish.rs new file mode 100644 index 00000000..fb9149e2 --- /dev/null +++ b/swap/src/network/cooperative_xmr_redeem_after_punish.rs @@ -0,0 +1,113 @@ +use crate::monero::Scalar; +use crate::network::cbor_request_response::CborCodec; +use crate::{asb, cli}; +use libp2p::core::ProtocolName; +use libp2p::request_response::{ + ProtocolSupport, RequestResponse, RequestResponseConfig, RequestResponseEvent, + RequestResponseMessage, +}; +use libp2p::PeerId; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +const PROTOCOL: &str = "/comit/xmr/btc/cooperative_xmr_redeem_after_punish/1.0.0"; +type OutEvent = RequestResponseEvent; +type Message = RequestResponseMessage; + +pub type Behaviour = RequestResponse>; + +#[derive(Debug, Clone, Copy, Default)] +pub struct CooperativeXmrRedeemProtocol; + +impl ProtocolName for CooperativeXmrRedeemProtocol { + fn protocol_name(&self) -> &[u8] { + PROTOCOL.as_bytes() + } +} + +#[derive(Debug, thiserror::Error, Clone, Serialize, Deserialize)] +pub enum CooperativeXmrRedeemRejectReason { + #[error("Alice does not have a record of the swap")] + UnknownSwap, + #[error("Alice rejected the request because it deemed it malicious")] + MaliciousRequest, + #[error("Alice is in a state where a cooperative redeem is not possible")] + SwapInvalidState, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Request { + pub swap_id: Uuid, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Response { + Fullfilled { + swap_id: Uuid, + s_a: Scalar, + }, + Rejected { + swap_id: Uuid, + reason: CooperativeXmrRedeemRejectReason, + }, +} +pub fn alice() -> Behaviour { + Behaviour::new( + CborCodec::default(), + vec![(CooperativeXmrRedeemProtocol, ProtocolSupport::Inbound)], + RequestResponseConfig::default(), + ) +} + +pub fn bob() -> Behaviour { + Behaviour::new( + CborCodec::default(), + vec![(CooperativeXmrRedeemProtocol, ProtocolSupport::Outbound)], + RequestResponseConfig::default(), + ) +} + +impl From<(PeerId, Message)> for asb::OutEvent { + fn from((peer, message): (PeerId, Message)) -> Self { + match message { + Message::Request { + request, channel, .. + } => Self::CooperativeXmrRedeemRequested { + swap_id: request.swap_id, + channel, + peer, + }, + Message::Response { .. } => Self::unexpected_response(peer), + } + } +} + +crate::impl_from_rr_event!(OutEvent, asb::OutEvent, PROTOCOL); + +impl From<(PeerId, Message)> for cli::OutEvent { + fn from((peer, message): (PeerId, Message)) -> Self { + match message { + Message::Request { .. } => Self::unexpected_request(peer), + Message::Response { + response, + request_id, + } => match response { + Response::Fullfilled { swap_id, s_a } => Self::CooperativeXmrRedeemFulfilled { + id: request_id, + swap_id, + s_a, + }, + Response::Rejected { + swap_id, + reason: error, + } => Self::CooperativeXmrRedeemRejected { + id: request_id, + swap_id, + reason: error, + }, + }, + } + } +} + +crate::impl_from_rr_event!(OutEvent, cli::OutEvent, PROTOCOL); diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index b4e155a6..f0acab23 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -74,7 +74,9 @@ pub enum AliceState { transfer_proof: TransferProof, state3: Box, }, - BtcPunished, + BtcPunished { + state3: Box, + }, SafelyAborted, } @@ -98,7 +100,7 @@ impl fmt::Display for AliceState { AliceState::BtcRedeemed => write!(f, "btc is redeemed"), AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"), AliceState::BtcRefunded { .. } => write!(f, "btc is refunded"), - AliceState::BtcPunished => write!(f, "btc is punished"), + AliceState::BtcPunished { .. } => write!(f, "btc is punished"), AliceState::SafelyAborted => write!(f, "safely aborted"), AliceState::BtcPunishable { .. } => write!(f, "btc is punishable"), AliceState::XmrRefunded => write!(f, "xmr is refunded"), @@ -377,7 +379,7 @@ impl State2 { pub struct State3 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, - s_a: monero::Scalar, + pub s_a: monero::Scalar, S_b_monero: monero::PublicKey, S_b_bitcoin: bitcoin::PublicKey, pub v: monero::PrivateViewKey, diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 0eef7dcd..79236563 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -362,7 +362,7 @@ where let punish = state3.punish_btc(bitcoin_wallet).await; match punish { - Ok(_) => AliceState::BtcPunished, + Ok(_) => AliceState::BtcPunished { state3 }, Err(error) => { tracing::warn!("Failed to publish punish transaction: {:#}", error); @@ -392,7 +392,7 @@ where } AliceState::XmrRefunded => AliceState::XmrRefunded, AliceState::BtcRedeemed => AliceState::BtcRedeemed, - AliceState::BtcPunished => AliceState::BtcPunished, + AliceState::BtcPunished { state3 } => AliceState::BtcPunished { state3 }, AliceState::SafelyAborted => AliceState::SafelyAborted, }) } @@ -402,7 +402,7 @@ pub(crate) fn is_complete(state: &AliceState) -> bool { state, AliceState::XmrRefunded | AliceState::BtcRedeemed - | AliceState::BtcPunished + | AliceState::BtcPunished { .. } | AliceState::SafelyAborted ) } diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 03f56d28..8fe5ca32 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -48,6 +48,7 @@ pub enum BobState { tx_lock_id: bitcoin::Txid, }, BtcPunished { + state: State6, tx_lock_id: bitcoin::Txid, }, SafelyAborted, @@ -421,11 +422,13 @@ impl State3 { } } - pub fn cancel(&self) -> State6 { + pub fn cancel(&self, monero_wallet_restore_blockheight: BlockHeight) -> State6 { State6 { A: self.A, b: self.b.clone(), s_b: self.s_b, + v: self.v, + monero_wallet_restore_blockheight, cancel_timelock: self.cancel_timelock, punish_timelock: self.punish_timelock, refund_address: self.refund_address.clone(), @@ -463,6 +466,19 @@ impl State3 { tx_cancel_status, )) } + pub fn attempt_cooperative_redeem( + &self, + s_a: monero::PrivateKey, + monero_wallet_restore_blockheight: BlockHeight, + ) -> State5 { + State5 { + s_a, + s_b: self.s_b, + v: self.v, + tx_lock: self.tx_lock.clone(), + monero_wallet_restore_blockheight, + } + } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] @@ -571,6 +587,8 @@ impl State4 { A: self.A, b: self.b, s_b: self.s_b, + v: self.v, + monero_wallet_restore_blockheight: self.monero_wallet_restore_blockheight, cancel_timelock: self.cancel_timelock, punish_timelock: self.punish_timelock, refund_address: self.refund_address, @@ -604,6 +622,43 @@ impl State5 { pub fn tx_lock_id(&self) -> bitcoin::Txid { self.tx_lock.txid() } + pub async fn redeem_xmr( + &self, + monero_wallet: &monero::Wallet, + wallet_file_name: std::string::String, + monero_receive_address: monero::Address, + ) -> Result<()> { + let (spend_key, view_key) = self.xmr_keys(); + + tracing::info!(%wallet_file_name, "Generating and opening Monero wallet from the extracted keys to redeem the Monero"); + if let Err(e) = monero_wallet + .create_from_and_load( + wallet_file_name.clone(), + spend_key, + view_key, + self.monero_wallet_restore_blockheight, + ) + .await + { + // In case we failed to refresh/sweep, when resuming the wallet might already + // exist! This is a very unlikely scenario, but if we don't take care of it we + // might not be able to ever transfer the Monero. + tracing::warn!("Failed to generate monero wallet from keys: {:#}", e); + tracing::info!(%wallet_file_name, + "Falling back to trying to open the wallet if it already exists", + ); + monero_wallet.open(wallet_file_name).await?; + } + + // Ensure that the generated wallet is synced so we have a proper balance + monero_wallet.refresh(20).await?; + // Sweep (transfer all funds) to the given address + let tx_hashes = monero_wallet.sweep_all(monero_receive_address).await?; + for tx_hash in tx_hashes { + tracing::info!(%monero_receive_address, txid=%tx_hash.0, "Successfully transferred XMR to wallet"); + } + Ok(()) + } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] @@ -611,6 +666,8 @@ pub struct State6 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, s_b: monero::Scalar, + v: monero::PrivateViewKey, + pub monero_wallet_restore_blockheight: BlockHeight, cancel_timelock: CancelTimelock, punish_timelock: PunishTimelock, refund_address: bitcoin::Address, @@ -706,4 +763,13 @@ impl State6 { pub fn tx_lock_id(&self) -> bitcoin::Txid { self.tx_lock.txid() } + pub fn attempt_cooperative_redeem(&self, s_a: monero::PrivateKey) -> State5 { + State5 { + s_a, + s_b: self.s_b, + v: self.v, + tx_lock: self.tx_lock.clone(), + monero_wallet_restore_blockheight: self.monero_wallet_restore_blockheight, + } + } } diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 7a702d77..43c54e4b 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,5 +1,6 @@ use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund}; use crate::cli::EventLoopHandle; +use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected}; use crate::network::swap_setup::bob::NewSwap; use crate::protocol::bob::state::*; use crate::protocol::{bob, Database}; @@ -12,10 +13,7 @@ use uuid::Uuid; pub fn is_complete(state: &BobState) -> bool { matches!( state, - BobState::BtcRefunded(..) - | BobState::XmrRedeemed { .. } - | BobState::BtcPunished { .. } - | BobState::SafelyAborted + BobState::BtcRefunded(..) | BobState::XmrRedeemed { .. } | BobState::SafelyAborted ) } @@ -28,7 +26,7 @@ pub async fn run_until( mut swap: bob::Swap, is_target_state: fn(&BobState) -> bool, ) -> Result { - let mut current_state = swap.state; + let mut current_state = swap.state.clone(); while !is_target_state(¤t_state) { current_state = next_state( @@ -41,10 +39,14 @@ pub async fn run_until( swap.monero_receive_address, ) .await?; - swap.db .insert_latest_state(swap.id, current_state.clone().into()) .await?; + if matches!(current_state, BobState::BtcPunished { .. }) + && matches!(swap.state, BobState::BtcPunished { .. }) + { + break; // Stops swap when cooperative redeem fails without preventing resuming swap in BtcPunished state. + }; } Ok(current_state) @@ -159,12 +161,12 @@ async fn next_state( result?; tracing::info!("Alice took too long to lock Monero, cancelling the swap"); - let state4 = state3.cancel(); + let state4 = state3.cancel(monero_wallet_restore_blockheight); BobState::CancelTimelockExpired(state4) }, } } else { - let state4 = state3.cancel(); + let state4 = state3.cancel(monero_wallet_restore_blockheight); BobState::CancelTimelockExpired(state4) } } @@ -188,17 +190,17 @@ async fn next_state( tx_lock_status.wait_until_confirmed_with(state.cancel_timelock).await?; - BobState::CancelTimelockExpired(state.cancel()) + BobState::CancelTimelockExpired(state.cancel(monero_wallet_restore_blockheight)) }, } } result = tx_lock_status.wait_until_confirmed_with(state.cancel_timelock) => { result?; - BobState::CancelTimelockExpired(state.cancel()) + BobState::CancelTimelockExpired(state.cancel(monero_wallet_restore_blockheight)) } } } else { - BobState::CancelTimelockExpired(state.cancel()) + BobState::CancelTimelockExpired(state.cancel(monero_wallet_restore_blockheight)) } } BobState::XmrLocked(state) => { @@ -257,39 +259,9 @@ async fn next_state( } } BobState::BtcRedeemed(state) => { - let (spend_key, view_key) = state.xmr_keys(); - - let wallet_file_name = swap_id.to_string(); - - tracing::info!(%wallet_file_name, "Generating and opening Monero wallet from the extracted keys to redeem the Monero"); - - if let Err(e) = monero_wallet - .create_from_and_load( - wallet_file_name.clone(), - spend_key, - view_key, - state.monero_wallet_restore_blockheight, - ) - .await - { - // In case we failed to refresh/sweep, when resuming the wallet might already - // exist! This is a very unlikely scenario, but if we don't take care of it we - // might not be able to ever transfer the Monero. - tracing::warn!("Failed to generate monero wallet from keys: {:#}", e); - tracing::info!(%wallet_file_name, - "Falling back to trying to open the wallet if it already exists", - ); - monero_wallet.open(wallet_file_name).await?; - } - - // Ensure that the generated wallet is synced so we have a proper balance - monero_wallet.refresh(20).await?; - // Sweep (transfer all funds) to the given address - let tx_hashes = monero_wallet.sweep_all(monero_receive_address).await?; - - for tx_hash in tx_hashes { - tracing::info!(%monero_receive_address, txid=%tx_hash.0, "Successfully transferred XMR to wallet"); - } + state + .redeem_xmr(monero_wallet, swap_id.to_string(), monero_receive_address) + .await?; BobState::XmrRedeemed { tx_lock_id: state.tx_lock_id(), @@ -318,12 +290,48 @@ async fn next_state( tracing::info!("You have been punished for not refunding in time"); BobState::BtcPunished { tx_lock_id: state.tx_lock_id(), + state, } } } } BobState::BtcRefunded(state4) => BobState::BtcRefunded(state4), - BobState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id }, + BobState::BtcPunished { state, tx_lock_id } => { + tracing::info!("Attempting cooperative XMR redeem"); + let response = event_loop_handle + .request_cooperative_xmr_redeem(swap_id) + .await; + + match response { + Ok(Fullfilled { s_a, .. }) => { + tracing::debug!("Alice revealed XMR key to us"); + + let s_a = monero::PrivateKey { scalar: s_a }; + let state5 = state.attempt_cooperative_redeem(s_a); + + match state5 + .redeem_xmr(monero_wallet, swap_id.to_string(), monero_receive_address) + .await + { + Ok(_) => { + return Ok(BobState::XmrRedeemed { tx_lock_id }); + } + Err(error) => { + return Err(error) + .context("Failed to redeem XMR with revealed XMR key"); + } + } + } + Ok(Rejected { reason: error, .. }) => { + return Err(error) + .context("Alice rejected our request for cooperative XMR redeem"); + } + Err(error) => { + return Err(error) + .context("Failed to request cooperative XMR redeem from Alice"); + } + }; + } BobState::SafelyAborted => BobState::SafelyAborted, BobState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id }, }) diff --git a/swap/tests/alice_manually_punishes_after_bob_dead.rs b/swap/tests/alice_manually_punishes_after_bob_dead.rs index b27904ac..aa747ec9 100644 --- a/swap/tests/alice_manually_punishes_after_bob_dead.rs +++ b/swap/tests/alice_manually_punishes_after_bob_dead.rs @@ -11,7 +11,7 @@ use swap::protocol::{alice, bob}; /// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice /// the encsig and fail to refund or redeem. Alice punishes using the cancel and -/// punish command. +/// punish command. Bob then cooperates with Alice and redeems XMR with her key. #[tokio::test] async fn alice_manually_punishes_after_bob_dead() { harness::setup_test(FastPunishConfig, |mut ctx| async move { @@ -78,9 +78,7 @@ async fn alice_manually_punishes_after_bob_dead() { assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); let bob_state = bob::run(bob_swap).await?; - - ctx.assert_bob_punished(bob_state).await; - + ctx.assert_bob_redeemed(bob_state).await; Ok(()) }) .await; diff --git a/swap/tests/alice_punishes_after_restart_bob_dead.rs b/swap/tests/alice_punishes_after_restart_bob_dead.rs index b049d681..1bf140ee 100644 --- a/swap/tests/alice_punishes_after_restart_bob_dead.rs +++ b/swap/tests/alice_punishes_after_restart_bob_dead.rs @@ -9,7 +9,7 @@ use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; /// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice -/// the encsig and fail to refund or redeem. Alice cancels and punishes. +/// the encsig and fail to refund or redeem. Alice cancels and punishes. Bob then cooperates with Alice and redeems XMR with her key. #[tokio::test] async fn alice_punishes_after_restart_if_bob_dead() { harness::setup_test(FastPunishConfig, |mut ctx| async move { @@ -58,9 +58,7 @@ async fn alice_punishes_after_restart_if_bob_dead() { assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); let bob_state = bob::run(bob_swap).await?; - - ctx.assert_bob_punished(bob_state).await; - + ctx.assert_bob_redeemed(bob_state).await; Ok(()) }) .await; diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index 028b0935..4a183084 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -652,7 +652,7 @@ impl TestContext { } pub async fn assert_alice_punished(&self, state: AliceState) { - assert!(matches!(state, AliceState::BtcPunished)); + assert!(matches!(state, AliceState::BtcPunished { .. })); assert_eventual_balance( self.alice_bitcoin_wallet.as_ref(), @@ -698,7 +698,7 @@ impl TestContext { let lock_tx_id = if let BobState::BtcRefunded(state4) = state { state4.tx_lock_id() } else { - panic!("Bob in not in btc refunded state: {:?}", state); + panic!("Bob is not in btc refunded state: {:?}", state); }; let lock_tx_bitcoin_fee = self .bob_bitcoin_wallet @@ -819,7 +819,7 @@ impl TestContext { async fn bob_punished_btc_balance(&self, state: BobState) -> Result { self.bob_bitcoin_wallet.sync().await?; - let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state { + let lock_tx_id = if let BobState::BtcPunished { tx_lock_id, .. } = state { tx_lock_id } else { bail!("Bob in not in btc punished state: {:?}", state); diff --git a/swap/tests/punish.rs b/swap/tests/punish.rs index 60eadfe3..79d93d9d 100644 --- a/swap/tests/punish.rs +++ b/swap/tests/punish.rs @@ -7,7 +7,7 @@ use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; /// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice -/// the encsig and fail to refund or redeem. Alice punishes. +/// the encsig and fail to refund or redeem. Alice punishes. Bob then cooperates with Alice and redeems XMR with her key. #[tokio::test] async fn alice_punishes_if_bob_never_acts_after_fund() { harness::setup_test(FastPunishConfig, |mut ctx| async move { @@ -32,9 +32,7 @@ async fn alice_punishes_if_bob_never_acts_after_fund() { assert!(matches!(bob_swap.state, BobState::BtcLocked { .. })); let bob_state = bob::run(bob_swap).await?; - - ctx.assert_bob_punished(bob_state).await; - + ctx.assert_bob_redeemed(bob_state).await; Ok(()) }) .await;