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:
rishflab 2020-12-11 16:49:19 +11:00
parent 773390886b
commit 7af4b6980a
6 changed files with 157 additions and 54 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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