feat: Allow for cooperative Monero redeem after Bitcoin punish has happened

This commit is contained in:
binarybaron 2024-07-03 19:47:45 +02:00
parent c5aa7edb6b
commit d7b649b7a6
25 changed files with 548 additions and 101 deletions

View file

@ -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": <state3 object from BtcLocked>} }}"
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":{<state6 object>}, "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":{<state object>}, "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

View file

@ -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);
}

View file

@ -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<cooperative_xmr_redeem_after_punish::Response>,
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<LR>,
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),
}

View file

@ -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),
};

View file

@ -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?;

View file

@ -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,

View file

@ -55,7 +55,7 @@ pub async fn refund(
AliceState::BtcRedeemTransactionPublished { .. }
| AliceState::BtcRedeemed
| AliceState::XmrRefunded
| AliceState::BtcPunished
| AliceState::BtcPunished { .. }
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),
};

View file

@ -31,7 +31,7 @@ pub async fn safely_abort(swap_id: Uuid, db: Arc<dyn Database>) -> Result<AliceS
| AliceState::BtcPunishable { .. }
| AliceState::BtcRedeemed
| AliceState::XmrRefunded
| AliceState::BtcPunished
| AliceState::BtcPunished { .. }
| AliceState::SafelyAborted => bail!(
"Cannot safely abort swap {} because it is in state {} which cannot be safely aborted",
swap_id,

View file

@ -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),

View file

@ -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())

View file

@ -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<Uuid, Response>,
encrypted_signatures: bmrng::RequestReceiverStream<EncryptedSignature, ()>,
swap_setup_requests: bmrng::RequestReceiverStream<NewSwap, Result<State2>>,
@ -36,7 +38,7 @@ pub struct EventLoop {
inflight_quote_requests: HashMap<RequestId, bmrng::Responder<BidQuote>>,
inflight_encrypted_signature_requests: HashMap<RequestId, bmrng::Responder<()>>,
inflight_swap_setup: Option<bmrng::Responder<Result<State2>>>,
inflight_cooperative_xmr_redeem_requests: HashMap<RequestId, bmrng::Responder<Response>>,
/// The sender we will use to relay incoming transfer proofs.
transfer_proof: bmrng::RequestSender<monero::TransferProof, ()>,
/// 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<monero::TransferProof, ()>,
encrypted_signature: bmrng::RequestSender<EncryptedSignature, ()>,
quote: bmrng::RequestSender<(), BidQuote>,
cooperative_xmr_redeem: bmrng::RequestSender<Uuid, Response>,
}
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<Response> {
Ok(self.cooperative_xmr_redeem.send_receive(swap_id).await?)
}
pub async fn send_encrypted_signature(
&mut self,

View file

@ -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<AliceState> for Alice {
@ -173,7 +173,9 @@ impl From<AliceState> 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<Alice> 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),
},
},
}
}

View file

@ -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<bob::State6>),
BtcPunished { tx_lock_id: bitcoin::Txid },
}
impl From<BobState> for Bob {
@ -79,13 +82,11 @@ impl From<BobState> 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<Bob> 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"),
}
}
}

View file

@ -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]

View file

@ -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;

View file

@ -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<Request, Response>;
type Message = RequestResponseMessage<Request, Response>;
pub type Behaviour = RequestResponse<CborCodec<CooperativeXmrRedeemProtocol, Request, Response>>;
#[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);

View file

@ -74,7 +74,9 @@ pub enum AliceState {
transfer_proof: TransferProof,
state3: Box<State3>,
},
BtcPunished,
BtcPunished {
state3: Box<State3>,
},
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,

View file

@ -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
)
}

View file

@ -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,
}
}
}

View file

@ -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<BobState> {
let mut current_state = swap.state;
let mut current_state = swap.state.clone();
while !is_target_state(&current_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 },
})

View file

@ -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;

View file

@ -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;

View file

@ -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<bitcoin::Amount> {
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);

View file

@ -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;