mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-08-03 03:56:22 -04:00
feat: Allow for cooperative Monero redeem after Bitcoin punish has happened
This commit is contained in:
parent
c5aa7edb6b
commit
d7b649b7a6
25 changed files with 548 additions and 101 deletions
135
swap/migrations/20240615140942_btcpunished_update.sql
Normal file
135
swap/migrations/20240615140942_btcpunished_update.sql
Normal 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
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
||||
|
|
|
@ -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?;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -55,7 +55,7 @@ pub async fn refund(
|
|||
AliceState::BtcRedeemTransactionPublished { .. }
|
||||
| AliceState::BtcRedeemed
|
||||
| AliceState::XmrRefunded
|
||||
| AliceState::BtcPunished
|
||||
| AliceState::BtcPunished { .. }
|
||||
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
|
113
swap/src/network/cooperative_xmr_redeem_after_punish.rs
Normal file
113
swap/src/network/cooperative_xmr_redeem_after_punish.rs
Normal 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);
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¤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 },
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue