xmr-btc-swap/swap/src/alice/swap.rs

660 lines
23 KiB
Rust
Raw Normal View History

//! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC.
use crate::{
alice::{
2020-12-10 02:19:18 +00:00
event_loop::EventLoopHandle,
steps::{
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction,
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
},
},
2020-12-07 01:47:21 +00:00
bitcoin,
bitcoin::EncryptedSignature,
network::request_response::AliceToBob,
2020-12-07 01:47:21 +00:00
state,
state::{Alice, Swap},
storage::Database,
SwapAmounts,
};
2020-12-07 01:47:21 +00:00
use anyhow::{bail, Result};
use async_recursion::async_recursion;
use futures::{
future::{select, Either},
pin_mut,
};
use libp2p::request_response::ResponseChannel;
use rand::{CryptoRng, RngCore};
use std::{convert::TryFrom, fmt, sync::Arc};
use tracing::info;
2020-12-07 01:47:21 +00:00
use uuid::Uuid;
use xmr_btc::{
alice::{State0, State3},
bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction},
config::Config,
monero::CreateWalletForOutput,
Epoch,
};
trait Rng: RngCore + CryptoRng + Send {}
impl<T> Rng for T where T: RngCore + CryptoRng + Send {}
2020-12-01 04:38:24 +00:00
#[allow(clippy::large_enum_variant)]
pub enum AliceState {
Started {
amounts: SwapAmounts,
state0: State0,
},
Negotiated {
channel: Option<ResponseChannel<AliceToBob>>,
amounts: SwapAmounts,
state3: State3,
},
BtcLocked {
channel: Option<ResponseChannel<AliceToBob>>,
amounts: SwapAmounts,
state3: State3,
},
XmrLocked {
state3: State3,
},
EncSignLearned {
state3: State3,
encrypted_signature: EncryptedSignature,
},
BtcRedeemed,
BtcCancelled {
state3: State3,
tx_cancel: TxCancel,
},
BtcRefunded {
2020-12-02 22:48:18 +00:00
spend_key: monero::PrivateKey,
state3: State3,
},
BtcPunishable {
tx_refund: TxRefund,
state3: State3,
},
XmrRefunded,
2020-12-14 23:26:53 +00:00
T1Expired {
state3: State3,
},
Punished,
SafelyAborted,
}
impl fmt::Display for AliceState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AliceState::Started { .. } => write!(f, "started"),
AliceState::Negotiated { .. } => write!(f, "negotiated"),
AliceState::BtcLocked { .. } => write!(f, "btc_locked"),
AliceState::XmrLocked { .. } => write!(f, "xmr_locked"),
2020-12-14 00:56:14 +00:00
AliceState::EncSignLearned { .. } => write!(f, "encsig_learned"),
AliceState::BtcRedeemed => write!(f, "btc_redeemed"),
AliceState::BtcCancelled { .. } => write!(f, "btc_cancelled"),
AliceState::BtcRefunded { .. } => write!(f, "btc_refunded"),
AliceState::Punished => write!(f, "punished"),
AliceState::SafelyAborted => write!(f, "safely_aborted"),
AliceState::BtcPunishable { .. } => write!(f, "btc_punishable"),
AliceState::XmrRefunded => write!(f, "xmr_refunded"),
2020-12-14 23:26:53 +00:00
AliceState::T1Expired { .. } => write!(f, "t1 is expired"),
}
}
}
impl From<&AliceState> for state::Alice {
fn from(alice_state: &AliceState) -> Self {
match alice_state {
AliceState::Negotiated { state3, .. } => Alice::Negotiated(state3.clone()),
AliceState::BtcLocked { state3, .. } => Alice::BtcLocked(state3.clone()),
AliceState::XmrLocked { state3 } => Alice::XmrLocked(state3.clone()),
2020-12-07 01:47:21 +00:00
AliceState::EncSignLearned {
state3,
encrypted_signature,
} => Alice::EncSignLearned {
2020-12-07 01:47:21 +00:00
state: state3.clone(),
encrypted_signature: encrypted_signature.clone(),
},
AliceState::BtcRedeemed => Alice::SwapComplete,
AliceState::BtcCancelled { state3, .. } => Alice::BtcCancelled(state3.clone()),
AliceState::BtcRefunded { .. } => Alice::SwapComplete,
AliceState::BtcPunishable { state3, .. } => Alice::BtcPunishable(state3.clone()),
AliceState::XmrRefunded => Alice::SwapComplete,
2020-12-14 23:26:53 +00:00
AliceState::T1Expired { state3 } => Alice::T1Expired(state3.clone()),
AliceState::Punished => Alice::SwapComplete,
AliceState::SafelyAborted => Alice::SwapComplete,
// TODO: Potentially add support to resume swaps that are not Negotiated
AliceState::Started { .. } => {
panic!("Alice attempted to save swap before being negotiated")
}
}
2020-12-07 01:47:21 +00:00
}
}
impl TryFrom<state::Swap> for AliceState {
type Error = anyhow::Error;
fn try_from(db_state: Swap) -> Result<Self, Self::Error> {
use AliceState::*;
if let Swap::Alice(state) = db_state {
let alice_state = match state {
Alice::Negotiated(state3) => Negotiated {
channel: None,
amounts: SwapAmounts {
btc: state3.btc,
xmr: state3.xmr,
},
state3,
},
Alice::BtcLocked(state3) => BtcLocked {
channel: None,
amounts: SwapAmounts {
btc: state3.btc,
xmr: state3.xmr,
},
state3,
},
Alice::XmrLocked(state3) => XmrLocked { state3 },
Alice::BtcRedeemable { .. } => bail!("BtcRedeemable state is unexpected"),
Alice::EncSignLearned {
state,
encrypted_signature,
} => EncSignLearned {
state3: state,
encrypted_signature,
},
2020-12-14 23:26:53 +00:00
Alice::T1Expired(state3) => AliceState::T1Expired { state3 },
2020-12-07 01:47:21 +00:00
Alice::BtcCancelled(state) => {
let tx_cancel = bitcoin::TxCancel::new(
&state.tx_lock,
state.refund_timelock,
state.a.public(),
state.B,
);
BtcCancelled {
state3: state,
tx_cancel,
}
}
Alice::BtcPunishable(state) => {
let tx_cancel = bitcoin::TxCancel::new(
&state.tx_lock,
state.refund_timelock,
state.a.public(),
state.B,
);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
BtcPunishable {
tx_refund,
state3: state,
}
}
Alice::BtcRefunded {
state, spend_key, ..
} => BtcRefunded {
spend_key,
state3: state,
},
Alice::SwapComplete => {
// TODO(Franck): Better fine grain
AliceState::SafelyAborted
}
};
Ok(alice_state)
} else {
bail!("Alice swap state expected.")
}
}
}
pub async fn swap(
state: AliceState,
2020-12-10 02:55:29 +00:00
event_loop_handle: EventLoopHandle,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>,
config: Config,
2020-12-07 01:47:21 +00:00
swap_id: Uuid,
db: Database,
) -> Result<AliceState> {
run_until(
state,
is_complete,
2020-12-10 02:55:29 +00:00
event_loop_handle,
bitcoin_wallet,
monero_wallet,
config,
2020-12-07 01:47:21 +00:00
swap_id,
db,
)
.await
}
pub fn is_complete(state: &AliceState) -> bool {
matches!(
state,
AliceState::XmrRefunded
| AliceState::BtcRedeemed
| AliceState::Punished
| AliceState::SafelyAborted
)
}
pub fn is_xmr_locked(state: &AliceState) -> bool {
matches!(
state,
AliceState::XmrLocked{..}
)
}
2020-12-10 03:59:09 +00:00
pub fn is_encsig_learned(state: &AliceState) -> bool {
matches!(
state,
AliceState::EncSignLearned{..}
)
}
// State machine driver for swap execution
#[async_recursion]
2020-12-07 01:47:21 +00:00
#[allow(clippy::too_many_arguments)]
pub async fn run_until(
state: AliceState,
is_target_state: fn(&AliceState) -> bool,
2020-12-10 02:55:29 +00:00
mut event_loop_handle: EventLoopHandle,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>,
config: Config,
2020-12-07 01:47:21 +00:00
swap_id: Uuid,
db: Database,
2020-12-14 06:24:23 +00:00
// TODO: Remove EventLoopHandle!
) -> Result<AliceState> {
info!("Current state:{}", state);
if is_target_state(&state) {
Ok(state)
} else {
match state {
AliceState::Started { amounts, state0 } => {
2020-12-10 02:55:29 +00:00
let (channel, state3) =
negotiate(state0, amounts, &mut event_loop_handle, config).await?;
2020-12-07 01:47:21 +00:00
let state = AliceState::Negotiated {
channel: Some(channel),
amounts,
state3,
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
run_until(
2020-12-07 01:47:21 +00:00
state,
is_target_state,
2020-12-10 02:55:29 +00:00
event_loop_handle,
bitcoin_wallet,
monero_wallet,
config,
2020-12-07 01:47:21 +00:00
swap_id,
db,
)
.await
}
AliceState::Negotiated {
state3,
channel,
amounts,
} => {
2020-12-07 01:47:21 +00:00
let state = match channel {
Some(channel) => {
let _ = wait_for_locked_bitcoin(
state3.tx_lock.txid(),
bitcoin_wallet.clone(),
config,
)
.await?;
2020-12-07 01:47:21 +00:00
AliceState::BtcLocked {
channel: Some(channel),
amounts,
state3,
}
}
None => {
tracing::info!("Cannot resume swap from negotiated state, aborting");
// Alice did not lock Xmr yet
2020-12-07 01:47:21 +00:00
AliceState::SafelyAborted
}
2020-12-07 01:47:21 +00:00
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
2020-12-07 01:47:21 +00:00
run_until(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet,
monero_wallet,
config,
swap_id,
db,
)
.await
}
AliceState::BtcLocked {
channel,
amounts,
state3,
2020-12-07 01:47:21 +00:00
} => {
let state = match channel {
Some(channel) => {
lock_xmr(
channel,
amounts,
state3.clone(),
&mut event_loop_handle,
monero_wallet.clone(),
)
.await?;
2020-12-07 01:47:21 +00:00
AliceState::XmrLocked { state3 }
}
None => {
tracing::info!("Cannot resume swap from BTC locked state, aborting");
// Alice did not lock Xmr yet
AliceState::SafelyAborted
}
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
2020-12-07 01:47:21 +00:00
run_until(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet,
monero_wallet,
config,
swap_id,
db,
)
.await
}
AliceState::XmrLocked { state3 } => {
2020-12-14 00:56:14 +00:00
// todo: match statement and wait for t1 can probably be expressed more cleanly
2020-12-07 01:47:21 +00:00
let state = 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,
);
2020-12-07 01:47:21 +00:00
let state3_clone = state3.clone();
let t1_timeout = state3_clone.wait_for_t1(bitcoin_wallet.as_ref());
pin_mut!(wait_for_enc_sig);
pin_mut!(t1_timeout);
match select(t1_timeout, wait_for_enc_sig).await {
2020-12-14 23:26:53 +00:00
Either::Left(_) => AliceState::T1Expired { state3 },
2020-12-07 01:47:21 +00:00
Either::Right((enc_sig, _)) => AliceState::EncSignLearned {
state3,
encrypted_signature: enc_sig?,
},
}
}
2020-12-14 23:26:53 +00:00
_ => AliceState::T1Expired { state3 },
2020-12-07 01:47:21 +00:00
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
2020-12-07 01:47:21 +00:00
run_until(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
config,
swap_id,
db,
)
.await
}
AliceState::EncSignLearned {
state3,
encrypted_signature,
} => {
let signed_tx_redeem = match build_bitcoin_redeem_transaction(
encrypted_signature,
&state3.tx_lock,
state3.a.clone(),
state3.s_a,
state3.B,
&state3.redeem_address,
) {
Ok(tx) => tx,
Err(_) => {
2020-12-14 23:26:53 +00:00
state3.wait_for_t1(bitcoin_wallet.as_ref()).await?;
let state = AliceState::T1Expired { state3 };
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
return run_until(
2020-12-07 01:47:21 +00:00
state,
is_target_state,
2020-12-10 02:55:29 +00:00
event_loop_handle,
bitcoin_wallet,
monero_wallet,
config,
2020-12-07 01:47:21 +00:00
swap_id,
db,
)
.await;
}
};
// TODO(Franck): Error handling is delicate here.
// If Bob sees this transaction he can redeem Monero
// e.g. If the Bitcoin node is down then the user needs to take action.
publish_bitcoin_redeem_transaction(
signed_tx_redeem,
bitcoin_wallet.clone(),
config,
)
.await?;
2020-12-07 01:47:21 +00:00
let state = AliceState::BtcRedeemed;
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
run_until(
2020-12-07 01:47:21 +00:00
state,
is_target_state,
2020-12-10 02:55:29 +00:00
event_loop_handle,
bitcoin_wallet,
monero_wallet,
config,
2020-12-07 01:47:21 +00:00
swap_id,
db,
)
.await
}
2020-12-14 23:26:53 +00:00
AliceState::T1Expired { state3 } => {
let tx_cancel = publish_cancel_transaction(
state3.tx_lock.clone(),
state3.a.clone(),
state3.B,
state3.refund_timelock,
state3.tx_cancel_sig_bob.clone(),
bitcoin_wallet.clone(),
)
.await?;
2020-12-07 01:47:21 +00:00
let state = AliceState::BtcCancelled { state3, tx_cancel };
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
run_until(
2020-12-07 01:47:21 +00:00
state,
is_target_state,
2020-12-10 02:55:29 +00:00
event_loop_handle,
bitcoin_wallet,
monero_wallet,
config,
2020-12-07 01:47:21 +00:00
swap_id,
db,
)
.await
}
AliceState::BtcCancelled { state3, tx_cancel } => {
let tx_cancel_height = bitcoin_wallet
.transaction_block_height(tx_cancel.txid())
.await;
let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund(
&tx_cancel,
tx_cancel_height,
state3.punish_timelock,
&state3.refund_address,
bitcoin_wallet.clone(),
)
.await?;
// TODO(Franck): Review error handling
match published_refund_tx {
None => {
2020-12-07 01:47:21 +00:00
let state = AliceState::BtcPunishable { tx_refund, state3 };
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
2020-12-07 01:47:21 +00:00
swap(
state,
2020-12-10 02:55:29 +00:00
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
config,
2020-12-07 01:47:21 +00:00
swap_id,
db,
)
.await
}
Some(published_refund_tx) => {
2020-12-02 22:48:18 +00:00
let spend_key = extract_monero_private_key(
published_refund_tx,
tx_refund,
state3.s_a,
state3.a.clone(),
state3.S_b_bitcoin,
)?;
2020-12-07 01:47:21 +00:00
let state = AliceState::BtcRefunded { spend_key, state3 };
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
run_until(
2020-12-07 01:47:21 +00:00
state,
is_target_state,
2020-12-10 02:55:29 +00:00
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
config,
2020-12-07 01:47:21 +00:00
swap_id,
db,
)
.await
}
}
}
2020-12-02 22:48:18 +00:00
AliceState::BtcRefunded { spend_key, state3 } => {
let view_key = state3.v;
monero_wallet
.create_and_load_wallet_for_output(spend_key, view_key)
.await?;
2020-12-07 01:47:21 +00:00
let state = AliceState::XmrRefunded;
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
Ok(state)
}
AliceState::BtcPunishable { tx_refund, state3 } => {
let signed_tx_punish = build_bitcoin_punish_transaction(
&state3.tx_lock,
state3.refund_timelock,
&state3.punish_address,
state3.punish_timelock,
state3.tx_punish_sig_bob.clone(),
state3.a.clone(),
state3.B,
)?;
let punish_tx_finalised = publish_bitcoin_punish_transaction(
signed_tx_punish,
bitcoin_wallet.clone(),
config,
);
let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
pin_mut!(punish_tx_finalised);
pin_mut!(refund_tx_seen);
match select(punish_tx_finalised, refund_tx_seen).await {
Either::Left(_) => {
2020-12-07 01:47:21 +00:00
let state = AliceState::Punished;
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
run_until(
2020-12-07 01:47:21 +00:00
state,
is_target_state,
2020-12-10 02:55:29 +00:00
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
config,
2020-12-07 01:47:21 +00:00
swap_id,
db,
)
.await
}
Either::Right((published_refund_tx, _)) => {
2020-12-02 22:48:18 +00:00
let spend_key = extract_monero_private_key(
published_refund_tx,
tx_refund,
state3.s_a,
state3.a.clone(),
state3.S_b_bitcoin,
)?;
2020-12-07 01:47:21 +00:00
let state = AliceState::BtcRefunded { spend_key, state3 };
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
run_until(
2020-12-07 01:47:21 +00:00
state,
is_target_state,
2020-12-10 02:55:29 +00:00
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
config,
2020-12-07 01:47:21 +00:00
swap_id,
db,
)
.await
}
}
}
AliceState::XmrRefunded => Ok(AliceState::XmrRefunded),
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed),
AliceState::Punished => Ok(AliceState::Punished),
AliceState::SafelyAborted => Ok(AliceState::SafelyAborted),
}
}
}