mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
Test Alice refunds after Bob refund
Reworked Alice XmrLocked state transition handler to handle the scenario when Alice received the encsig but Bob refunds. Previously Alice was trying to redeem after receiving the encsig without checking if t1 had elapsed.
This commit is contained in:
parent
773390886b
commit
7af4b6980a
@ -29,6 +29,7 @@ use xmr_btc::{
|
||||
bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction},
|
||||
config::Config,
|
||||
monero::CreateWalletForOutput,
|
||||
Epoch,
|
||||
};
|
||||
|
||||
trait Rng: RngCore + CryptoRng + Send {}
|
||||
@ -75,7 +76,7 @@ pub enum AliceState {
|
||||
state3: State3,
|
||||
},
|
||||
XmrRefunded,
|
||||
WaitingToCancel {
|
||||
Cancelling {
|
||||
state3: State3,
|
||||
},
|
||||
Punished,
|
||||
@ -89,7 +90,7 @@ impl fmt::Display for AliceState {
|
||||
AliceState::Negotiated { .. } => write!(f, "negotiated"),
|
||||
AliceState::BtcLocked { .. } => write!(f, "btc_locked"),
|
||||
AliceState::XmrLocked { .. } => write!(f, "xmr_locked"),
|
||||
AliceState::EncSignLearned { .. } => write!(f, "encsig_sent"),
|
||||
AliceState::EncSignLearned { .. } => write!(f, "encsig_learnt"),
|
||||
AliceState::BtcRedeemed => write!(f, "btc_redeemed"),
|
||||
AliceState::BtcCancelled { .. } => write!(f, "btc_cancelled"),
|
||||
AliceState::BtcRefunded { .. } => write!(f, "btc_refunded"),
|
||||
@ -97,7 +98,7 @@ impl fmt::Display for AliceState {
|
||||
AliceState::SafelyAborted => write!(f, "safely_aborted"),
|
||||
AliceState::BtcPunishable { .. } => write!(f, "btc_punishable"),
|
||||
AliceState::XmrRefunded => write!(f, "xmr_refunded"),
|
||||
AliceState::WaitingToCancel { .. } => write!(f, "waiting_to_cancel"),
|
||||
AliceState::Cancelling { .. } => write!(f, "cancelling"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -218,31 +219,46 @@ pub async fn run_until(
|
||||
.await
|
||||
}
|
||||
AliceState::XmrLocked { state3 } => {
|
||||
// Our Monero is locked, we need to go through the cancellation process if this
|
||||
// step fails
|
||||
match wait_for_bitcoin_encrypted_signature(
|
||||
&mut event_loop_handle,
|
||||
config.monero_max_finality_time,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(encrypted_signature) => {
|
||||
run_until(
|
||||
AliceState::EncSignLearned {
|
||||
state3,
|
||||
encrypted_signature,
|
||||
},
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
config,
|
||||
)
|
||||
.await
|
||||
// todo: match statement and wait for t1 can probably expressed more cleanly
|
||||
match state3.current_epoch(bitcoin_wallet.as_ref()).await? {
|
||||
Epoch::T0 => {
|
||||
let wait_for_enc_sig = wait_for_bitcoin_encrypted_signature(
|
||||
&mut event_loop_handle,
|
||||
config.monero_max_finality_time,
|
||||
);
|
||||
let t1_timeout = state3.wait_for_t1(bitcoin_wallet.as_ref());
|
||||
|
||||
tokio::select! {
|
||||
_ = t1_timeout => {
|
||||
run_until(
|
||||
AliceState::Cancelling { state3 },
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
enc_sig = wait_for_enc_sig => {
|
||||
run_until(
|
||||
AliceState::EncSignLearned {
|
||||
state3,
|
||||
encrypted_signature: enc_sig?,
|
||||
},
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
_ => {
|
||||
run_until(
|
||||
AliceState::WaitingToCancel { state3 },
|
||||
AliceState::Cancelling { state3 },
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet,
|
||||
@ -253,6 +269,7 @@ pub async fn run_until(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AliceState::EncSignLearned {
|
||||
state3,
|
||||
encrypted_signature,
|
||||
@ -268,7 +285,7 @@ pub async fn run_until(
|
||||
Ok(tx) => tx,
|
||||
Err(_) => {
|
||||
return run_until(
|
||||
AliceState::WaitingToCancel { state3 },
|
||||
AliceState::Cancelling { state3 },
|
||||
is_target_state,
|
||||
event_loop_handle,
|
||||
bitcoin_wallet,
|
||||
@ -299,7 +316,7 @@ pub async fn run_until(
|
||||
)
|
||||
.await
|
||||
}
|
||||
AliceState::WaitingToCancel { state3 } => {
|
||||
AliceState::Cancelling { state3 } => {
|
||||
let tx_cancel = publish_cancel_transaction(
|
||||
state3.tx_lock.clone(),
|
||||
state3.a.clone(),
|
||||
|
@ -10,7 +10,10 @@ use rand::{CryptoRng, RngCore};
|
||||
use std::{fmt, sync::Arc};
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::bob::{self, Epoch};
|
||||
use xmr_btc::{
|
||||
bob::{self},
|
||||
Epoch,
|
||||
};
|
||||
|
||||
// The same data structure is used for swap execution and recovery.
|
||||
// This allows for a seamless transition from a failed swap to recovery.
|
||||
@ -27,7 +30,7 @@ pub enum BobState {
|
||||
EncSigSent(bob::State4, PeerId),
|
||||
BtcRedeemed(bob::State5),
|
||||
Cancelled(bob::State4),
|
||||
BtcRefunded,
|
||||
BtcRefunded(bob::State4),
|
||||
XmrRedeemed,
|
||||
Punished,
|
||||
SafelyAborted,
|
||||
@ -41,9 +44,9 @@ impl fmt::Display for BobState {
|
||||
BobState::BtcLocked(..) => write!(f, "btc_locked"),
|
||||
BobState::XmrLocked(..) => write!(f, "xmr_locked"),
|
||||
BobState::EncSigSent(..) => write!(f, "encsig_sent"),
|
||||
BobState::BtcRedeemed(_) => write!(f, "btc_redeemed"),
|
||||
BobState::Cancelled(_) => write!(f, "cancelled"),
|
||||
BobState::BtcRefunded => write!(f, "btc_refunded"),
|
||||
BobState::BtcRedeemed(..) => write!(f, "btc_redeemed"),
|
||||
BobState::Cancelled(..) => write!(f, "cancelled"),
|
||||
BobState::BtcRefunded(..) => write!(f, "btc_refunded"),
|
||||
BobState::XmrRedeemed => write!(f, "xmr_redeemed"),
|
||||
BobState::Punished => write!(f, "punished"),
|
||||
BobState::SafelyAborted => write!(f, "safely_aborted"),
|
||||
@ -79,7 +82,7 @@ where
|
||||
pub fn is_complete(state: &BobState) -> bool {
|
||||
matches!(
|
||||
state,
|
||||
BobState::BtcRefunded
|
||||
BobState::BtcRefunded(..)
|
||||
| BobState::XmrRedeemed
|
||||
| BobState::Punished
|
||||
| BobState::SafelyAborted
|
||||
@ -267,9 +270,9 @@ where
|
||||
Epoch::T1 => {
|
||||
state.refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||
run_until(
|
||||
BobState::BtcRefunded,
|
||||
BobState::BtcRefunded(state),
|
||||
is_target_state,
|
||||
swarm,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
@ -284,7 +287,7 @@ where
|
||||
run_until(
|
||||
BobState::Punished,
|
||||
is_target_state,
|
||||
swarm,
|
||||
event_loop_handle,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
@ -295,7 +298,7 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
BobState::BtcRefunded => Ok(BobState::BtcRefunded),
|
||||
BobState::BtcRefunded(state4) => Ok(BobState::BtcRefunded(state4)),
|
||||
BobState::Punished => Ok(BobState::Punished),
|
||||
BobState::SafelyAborted => Ok(BobState::SafelyAborted),
|
||||
BobState::XmrRedeemed => Ok(BobState::XmrRedeemed),
|
||||
|
@ -269,7 +269,7 @@ async fn both_refund() {
|
||||
|
||||
let (
|
||||
alice_state,
|
||||
mut alice_swarm,
|
||||
mut alice_swarm_driver,
|
||||
alice_swarm_handle,
|
||||
alice_btc_wallet,
|
||||
alice_xmr_wallet,
|
||||
@ -282,6 +282,7 @@ async fn both_refund() {
|
||||
xmr_to_swap,
|
||||
alice_xmr_starting_balance,
|
||||
alice_multiaddr.clone(),
|
||||
Config::regtest(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -295,6 +296,7 @@ async fn both_refund() {
|
||||
bob_btc_starting_balance,
|
||||
xmr_to_swap,
|
||||
bob_xmr_starting_balance,
|
||||
Config::regtest(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -310,7 +312,7 @@ async fn both_refund() {
|
||||
|
||||
tokio::spawn(async move { bob_swarm_driver.run().await });
|
||||
|
||||
let alice_fut = alice::swap::run_until(
|
||||
let alice_xmr_locked_fut = alice::swap::run_until(
|
||||
alice_state,
|
||||
alice::swap::is_xmr_locked,
|
||||
alice_swarm_handle,
|
||||
@ -319,11 +321,62 @@ async fn both_refund() {
|
||||
Config::regtest(),
|
||||
);
|
||||
|
||||
tokio::spawn(async move { alice_swarm.run().await });
|
||||
tokio::spawn(async move { alice_swarm_driver.run().await });
|
||||
|
||||
let ((_alice_state, _), bob_state) = try_join(alice_fut, bob_fut).await.unwrap();
|
||||
// Wait until alice has locked xmr and bob has locked btc
|
||||
let (bob_state, (alice_state, alice_swarm_handle)) =
|
||||
try_join(bob_fut, alice_xmr_locked_fut).await.unwrap();
|
||||
|
||||
assert!(matches!(bob_state, BobState::BtcRefunded));
|
||||
let bob_state4 = if let BobState::BtcRefunded(state4) = bob_state {
|
||||
state4
|
||||
} else {
|
||||
panic!("Bob in unexpected state");
|
||||
};
|
||||
|
||||
let (alice_state, _) = alice::swap::swap(
|
||||
alice_state,
|
||||
alice_swarm_handle,
|
||||
alice_btc_wallet.clone(),
|
||||
alice_xmr_wallet.clone(),
|
||||
Config::regtest(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(alice_state, AliceState::XmrRefunded));
|
||||
|
||||
let btc_alice_final = alice_btc_wallet.as_ref().balance().await.unwrap();
|
||||
let btc_bob_final = bob_btc_wallet.as_ref().balance().await.unwrap();
|
||||
|
||||
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
|
||||
// to TX_FEE
|
||||
let lock_tx_bitcoin_fee = bob_btc_wallet
|
||||
.transaction_fee(bob_state4.tx_lock_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(btc_alice_final, alice_btc_starting_balance);
|
||||
|
||||
// Alice or Bob could publish TxCancel. This means Bob could pay tx fees for
|
||||
// TxCancel and TxRefund or only TxRefund
|
||||
let btc_bob_final_alice_submitted_cancel = btc_bob_final
|
||||
== bob_btc_starting_balance
|
||||
- lock_tx_bitcoin_fee
|
||||
- bitcoin::Amount::from_sat(bitcoin::TX_FEE);
|
||||
|
||||
let btc_bob_final_bob_submitted_cancel = btc_bob_final
|
||||
== bob_btc_starting_balance
|
||||
- lock_tx_bitcoin_fee
|
||||
- bitcoin::Amount::from_sat(2 * bitcoin::TX_FEE);
|
||||
assert!(btc_bob_final_alice_submitted_cancel || btc_bob_final_bob_submitted_cancel);
|
||||
|
||||
alice_xmr_wallet.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_alice_final = alice_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
assert_eq!(xmr_alice_final, xmr_to_swap);
|
||||
|
||||
bob_xmr_wallet.as_ref().0.refresh().await.unwrap();
|
||||
let xmr_bob_final = bob_xmr_wallet.as_ref().get_balance().await.unwrap();
|
||||
assert_eq!(xmr_bob_final, bob_xmr_starting_balance);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@ -383,7 +436,6 @@ async fn init_alice(
|
||||
punish_address,
|
||||
);
|
||||
|
||||
// let msg0 = AliceToBob::Message0(self.state.next_message(&mut OsRng));
|
||||
(
|
||||
AliceState::Started {
|
||||
amounts,
|
||||
|
@ -4,6 +4,7 @@ use crate::{
|
||||
bob, monero,
|
||||
monero::{CreateWalletForOutput, Transfer},
|
||||
transport::{ReceiveMessage, SendMessage},
|
||||
Epoch,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
@ -26,8 +27,8 @@ use std::{
|
||||
};
|
||||
use tokio::{sync::Mutex, time::timeout};
|
||||
use tracing::{error, info};
|
||||
|
||||
pub mod message;
|
||||
use crate::bitcoin::{BlockHeight, TransactionBlockHeight};
|
||||
pub use message::{Message, Message0, Message1, Message2};
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -678,6 +679,37 @@ impl State3 {
|
||||
tx_cancel_sig_bob: self.tx_cancel_sig_bob,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn wait_for_t1<W>(&self, bitcoin_wallet: &W) -> Result<()>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + BlockHeight,
|
||||
{
|
||||
let tx_id = self.tx_lock.txid();
|
||||
let tx_lock_height = bitcoin_wallet.transaction_block_height(tx_id).await;
|
||||
|
||||
let t1_timeout =
|
||||
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + self.refund_timelock);
|
||||
t1_timeout.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn current_epoch<W>(&self, bitcoin_wallet: &W) -> Result<Epoch>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + BlockHeight,
|
||||
{
|
||||
let current_block_height = bitcoin_wallet.block_height().await;
|
||||
let t0 = bitcoin_wallet
|
||||
.transaction_block_height(self.tx_lock.txid())
|
||||
.await;
|
||||
let t1 = t0 + self.refund_timelock;
|
||||
let t2 = t1 + self.punish_timelock;
|
||||
|
||||
match (current_block_height < t1, current_block_height < t2) {
|
||||
(true, _) => Ok(Epoch::T0),
|
||||
(false, true) => Ok(Epoch::T1),
|
||||
(false, false) => Ok(Epoch::T2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
@ -7,6 +7,7 @@ use crate::{
|
||||
monero,
|
||||
serde::monero_private_key,
|
||||
transport::{ReceiveMessage, SendMessage},
|
||||
Epoch,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
@ -708,7 +709,6 @@ impl State4 {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.refund_timelock, self.A, self.b.public());
|
||||
|
||||
// todo: check if this is correct
|
||||
let sig_a = self.tx_cancel_sig_a.clone();
|
||||
let sig_b = self.b.sign(tx_cancel.digest());
|
||||
|
||||
@ -732,7 +732,6 @@ impl State4 {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&self.tx_lock, self.refund_timelock, self.A, self.b.public());
|
||||
|
||||
// todo: check if this is correct
|
||||
let sig_a = self.tx_cancel_sig_a.clone();
|
||||
let sig_b = self.b.sign(tx_cancel.digest());
|
||||
|
||||
@ -866,13 +865,6 @@ impl State4 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Epoch {
|
||||
T0,
|
||||
T1,
|
||||
T2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct State5 {
|
||||
A: bitcoin::PublicKey,
|
||||
|
@ -14,6 +14,13 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Epoch {
|
||||
T0,
|
||||
T1,
|
||||
T2,
|
||||
}
|
||||
|
||||
#[macro_use]
|
||||
mod utils {
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user