325: Misc cleanup r=thomaseizinger a=thomaseizinger

Miscellaneous cleanups of the `swap`, `state` and `steps` modules.

Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
pull/351/head
bors[bot] 3 years ago committed by GitHub
commit 113a29839f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,12 +1,12 @@
use crate::bitcoin;
use crate::bitcoin::wallet::Watchable;
use crate::bitcoin::{
build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock,
TX_FEE,
};
use ::bitcoin::util::bip143::SigHashCache;
use ::bitcoin::{OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid};
use ::bitcoin::{OutPoint, Script, SigHash, SigHashType, TxIn, TxOut, Txid};
use anyhow::Result;
use bitcoin::Script;
use ecdsa_fun::Signature;
use miniscript::{Descriptor, DescriptorTrait};
use serde::{Deserialize, Serialize};
@ -149,7 +149,39 @@ impl TxCancel {
OutPoint::new(self.inner.txid(), 0)
}
pub fn add_signatures(
pub fn complete_as_alice(
self,
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
tx_cancel_sig_B: bitcoin::Signature,
) -> Result<Transaction> {
let sig_a = a.sign(self.digest());
let sig_b = tx_cancel_sig_B;
let tx_cancel = self
.add_signatures((a.public(), sig_a), (B, sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel");
Ok(tx_cancel)
}
pub fn complete_as_bob(
self,
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
tx_cancel_sig_A: bitcoin::Signature,
) -> Result<Transaction> {
let sig_a = tx_cancel_sig_A;
let sig_b = b.sign(self.digest());
let tx_cancel = self
.add_signatures((A, sig_a), (b.public(), sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel");
Ok(tx_cancel)
}
fn add_signatures(
self,
(A, sig_a): (PublicKey, Signature),
(B, sig_b): (PublicKey, Signature),

@ -3,10 +3,10 @@ use crate::bitcoin::{
verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs,
Transaction, TxCancel,
};
use crate::{bitcoin, monero};
use ::bitcoin::util::bip143::SigHashCache;
use ::bitcoin::{SigHash, SigHashType, Txid};
use ::bitcoin::{Script, SigHash, SigHashType, Txid};
use anyhow::{bail, Context, Result};
use bitcoin::Script;
use ecdsa_fun::Signature;
use miniscript::{Descriptor, DescriptorTrait};
use std::collections::HashMap;
@ -77,7 +77,31 @@ impl TxRefund {
Ok(tx_refund)
}
pub fn extract_signature_by_key(
pub fn extract_monero_private_key(
&self,
published_refund_tx: bitcoin::Transaction,
s_a: monero::Scalar,
a: bitcoin::SecretKey,
S_b_bitcoin: bitcoin::PublicKey,
) -> Result<monero::PrivateKey> {
let s_a = monero::PrivateKey { scalar: s_a };
let tx_refund_sig = self
.extract_signature_by_key(published_refund_tx, a.public())
.context("Failed to extract signature from Bitcoin refund tx")?;
let tx_refund_encsig = a.encsign(S_b_bitcoin, self.digest());
let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig)
.context("Failed to recover Monero secret key from Bitcoin signature")?;
let s_b = monero::private_key_from_secp256k1_scalar(s_b.into());
let spend_key = s_a + s_b;
Ok(spend_key)
}
fn extract_signature_by_key(
&self,
candidate_transaction: Transaction,
B: PublicKey,

@ -29,8 +29,8 @@ pub enum Bob {
state4: bob::State4,
},
BtcRedeemed(bob::State5),
CancelTimelockExpired(bob::State4),
BtcCancelled(bob::State4),
CancelTimelockExpired(bob::State6),
BtcCancelled(bob::State6),
Done(BobEndState),
}
@ -38,7 +38,7 @@ pub enum Bob {
pub enum BobEndState {
SafelyAborted,
XmrRedeemed { tx_lock_id: bitcoin::Txid },
BtcRefunded(Box<bob::State4>),
BtcRefunded(Box<bob::State6>),
BtcPunished { tx_lock_id: bitcoin::Txid },
}
@ -60,9 +60,9 @@ impl From<BobState> for Bob {
BobState::XmrLocked(state4) => Bob::XmrLocked { state4 },
BobState::EncSigSent(state4) => Bob::EncSigSent { state4 },
BobState::BtcRedeemed(state5) => Bob::BtcRedeemed(state5),
BobState::CancelTimelockExpired(state4) => Bob::CancelTimelockExpired(state4),
BobState::BtcCancelled(state4) => Bob::BtcCancelled(state4),
BobState::BtcRefunded(state4) => Bob::Done(BobEndState::BtcRefunded(Box::new(state4))),
BobState::CancelTimelockExpired(state6) => Bob::CancelTimelockExpired(state6),
BobState::BtcCancelled(state6) => Bob::BtcCancelled(state6),
BobState::BtcRefunded(state6) => Bob::Done(BobEndState::BtcRefunded(Box::new(state6))),
BobState::XmrRedeemed { tx_lock_id } => {
Bob::Done(BobEndState::XmrRedeemed { tx_lock_id })
}
@ -92,12 +92,12 @@ impl From<Bob> for BobState {
Bob::XmrLocked { state4 } => BobState::XmrLocked(state4),
Bob::EncSigSent { state4 } => BobState::EncSigSent(state4),
Bob::BtcRedeemed(state5) => BobState::BtcRedeemed(state5),
Bob::CancelTimelockExpired(state4) => BobState::CancelTimelockExpired(state4),
Bob::BtcCancelled(state4) => BobState::BtcCancelled(state4),
Bob::CancelTimelockExpired(state6) => BobState::CancelTimelockExpired(state6),
Bob::BtcCancelled(state6) => BobState::BtcCancelled(state6),
Bob::Done(end_state) => match end_state {
BobEndState::SafelyAborted => BobState::SafelyAborted,
BobEndState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id },
BobEndState::BtcRefunded(state4) => BobState::BtcRefunded(*state4),
BobEndState::BtcRefunded(state6) => BobState::BtcRefunded(*state6),
BobEndState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id },
},
}

@ -182,7 +182,7 @@ impl fmt::Display for TxHash {
}
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("transaction does not pay enough: expected {expected}, got {actual}")]
#[error("expected {expected}, got {actual}")]
pub struct InsufficientFunds {
pub expected: Amount,
pub actual: Amount,

@ -120,12 +120,13 @@ impl Wallet {
Ok(())
}
pub async fn transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
amount: Amount,
) -> Result<TransferProof> {
pub async fn transfer(&self, request: TransferRequest) -> Result<TransferProof> {
let TransferRequest {
public_spend_key,
public_view_key,
amount,
} = request;
let destination_address =
Address::standard(self.network, public_spend_key, public_view_key.into());
@ -149,14 +150,15 @@ impl Wallet {
))
}
pub async fn watch_for_transfer(
&self,
public_spend_key: PublicKey,
public_view_key: PublicViewKey,
transfer_proof: TransferProof,
expected: Amount,
conf_target: u32,
) -> Result<(), InsufficientFunds> {
pub async fn watch_for_transfer(&self, request: WatchRequest) -> Result<()> {
let WatchRequest {
conf_target,
public_view_key,
public_spend_key,
transfer_proof,
expected,
} = request;
let txid = transfer_proof.tx_hash();
tracing::info!(%txid, "Waiting for {} confirmation{} of Monero transaction", conf_target, if conf_target > 1 { "s" } else { "" });
@ -222,6 +224,22 @@ impl Wallet {
}
}
#[derive(Debug)]
pub struct TransferRequest {
pub public_spend_key: PublicKey,
pub public_view_key: PublicViewKey,
pub amount: Amount,
}
#[derive(Debug)]
pub struct WatchRequest {
pub public_spend_key: PublicKey,
pub public_view_key: PublicViewKey,
pub transfer_proof: TransferProof,
pub conf_target: u32,
pub expected: Amount,
}
async fn wait_for_confirmations<Fut>(
txid: String,
fetch_tx: impl Fn(String) -> Fut,

@ -19,7 +19,6 @@ mod encrypted_signature;
pub mod event_loop;
mod execution_setup;
pub mod state;
mod steps;
pub mod swap;
mod transfer_proof;

@ -42,12 +42,6 @@ pub struct EventLoop<RS> {
swap_sender: mpsc::Sender<Swap>,
}
#[derive(Debug)]
pub struct EventLoopHandle {
recv_encrypted_signature: Option<oneshot::Receiver<EncryptedSignature>>,
send_transfer_proof: Option<oneshot::Sender<TransferProof>>,
}
impl<LR> EventLoop<LR>
where
LR: LatestRate,
@ -310,22 +304,30 @@ impl LatestRate for kraken::RateUpdateStream {
}
}
#[derive(Debug)]
pub struct EventLoopHandle {
recv_encrypted_signature: Option<oneshot::Receiver<EncryptedSignature>>,
send_transfer_proof: Option<oneshot::Sender<TransferProof>>,
}
impl EventLoopHandle {
pub async fn recv_encrypted_signature(&mut self) -> Result<EncryptedSignature> {
pub async fn recv_encrypted_signature(&mut self) -> Result<bitcoin::EncryptedSignature> {
let signature = self
.recv_encrypted_signature
.take()
.context("Encrypted signature was already received")?
.await?;
.await?
.tx_redeem_encsig;
Ok(signature)
}
pub async fn send_transfer_proof(&mut self, msg: TransferProof) -> Result<()> {
pub async fn send_transfer_proof(&mut self, msg: monero::TransferProof) -> Result<()> {
if self
.send_transfer_proof
.take()
.context("Transfer proof was already sent")?
.send(msg)
.send(TransferProof { tx_lock_proof: msg })
.is_err()
{
bail!("Failed to send transfer proof, receiver no longer listening?")

@ -2,6 +2,7 @@ use crate::bitcoin::{
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund,
};
use crate::env::Config;
use crate::monero::wallet::TransferRequest;
use crate::protocol::alice::{Message1, Message3};
use crate::protocol::bob::{Message0, Message2, Message4};
use crate::protocol::CROSS_CURVE_PROOF_SYSTEM;
@ -343,6 +344,19 @@ impl State3 {
))
}
pub fn lock_xmr_transfer_request(&self) -> TransferRequest {
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a });
let public_spend_key = S_a + self.S_b_monero;
let public_view_key = self.v.public();
TransferRequest {
public_spend_key,
public_view_key,
amount: self.xmr,
}
}
pub fn tx_cancel(&self) -> TxCancel {
TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B)
}

@ -1,149 +0,0 @@
use crate::bitcoin::{
CancelTimelock, EncryptedSignature, PunishTimelock, TxCancel, TxLock, TxRefund,
};
use crate::protocol::alice;
use crate::protocol::alice::event_loop::EventLoopHandle;
use crate::protocol::alice::TransferProof;
use crate::{bitcoin, monero};
use anyhow::{bail, Context, Result};
use futures::pin_mut;
pub async fn lock_xmr(
state3: alice::State3,
event_loop_handle: &mut EventLoopHandle,
monero_wallet: &monero::Wallet,
) -> Result<()> {
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: state3.s_a });
let public_spend_key = S_a + state3.S_b_monero;
let public_view_key = state3.v.public();
let transfer_proof = monero_wallet
.transfer(public_spend_key, public_view_key, state3.xmr)
.await?;
// TODO(Franck): Wait for Monero to be confirmed once
// Waiting for XMR confirmations should not be done in here, but in a separate
// state! We have to record that Alice has already sent the transaction.
// Otherwise Alice might publish the lock tx twice!
event_loop_handle
.send_transfer_proof(TransferProof {
tx_lock_proof: transfer_proof,
})
.await?;
Ok(())
}
pub async fn wait_for_bitcoin_encrypted_signature(
event_loop_handle: &mut EventLoopHandle,
) -> Result<EncryptedSignature> {
let msg3 = event_loop_handle
.recv_encrypted_signature()
.await
.context("Failed to receive Bitcoin encrypted signature from Bob")?;
tracing::debug!("Message 3 received, returning it");
Ok(msg3.tx_redeem_encsig)
}
pub async fn publish_cancel_transaction(
tx_lock: TxLock,
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
cancel_timelock: CancelTimelock,
tx_cancel_sig_bob: bitcoin::Signature,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<()> {
bitcoin_wallet
.watch_until_status(&tx_lock, |status| status.is_confirmed_with(cancel_timelock))
.await?;
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B);
// If Bob hasn't yet broadcasted the tx cancel, we do it
if bitcoin_wallet
.get_raw_transaction(tx_cancel.txid())
.await
.is_err()
{
// TODO(Franck): Maybe the cancel transaction is already mined, in this case,
// the broadcast will error out.
let sig_a = a.sign(tx_cancel.digest());
let sig_b = tx_cancel_sig_bob.clone();
let tx_cancel = tx_cancel
.add_signatures((a.public(), sig_a), (B, sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel");
// TODO(Franck): Error handling is delicate, why can't we broadcast?
let (..) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
// TODO(Franck): Wait until transaction is mined and returned mined
// block height
}
Ok(())
}
pub async fn wait_for_bitcoin_refund(
tx_cancel: &TxCancel,
tx_refund: &TxRefund,
punish_timelock: PunishTimelock,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<Option<bitcoin::Transaction>> {
let refund_tx_id = tx_refund.txid();
let seen_refund_tx =
bitcoin_wallet.watch_until_status(tx_refund, |status| status.has_been_seen());
let punish_timelock_expired = bitcoin_wallet.watch_until_status(tx_cancel, |status| {
status.is_confirmed_with(punish_timelock)
});
pin_mut!(punish_timelock_expired);
pin_mut!(seen_refund_tx);
tokio::select! {
seen_refund = seen_refund_tx => {
match seen_refund {
Ok(()) => {
let published_refund_tx = bitcoin_wallet.get_raw_transaction(refund_tx_id).await?;
Ok(Some(published_refund_tx))
}
Err(e) => {
bail!(e.context("Failed to monitor refund transaction"))
}
}
}
_ = punish_timelock_expired => {
Ok(None)
}
}
}
pub fn extract_monero_private_key(
published_refund_tx: bitcoin::Transaction,
tx_refund: &TxRefund,
s_a: monero::Scalar,
a: bitcoin::SecretKey,
S_b_bitcoin: bitcoin::PublicKey,
) -> Result<monero::PrivateKey> {
let s_a = monero::PrivateKey { scalar: s_a };
let tx_refund_sig = tx_refund
.extract_signature_by_key(published_refund_tx, a.public())
.context("Failed to extract signature from Bitcoin refund tx")?;
let tx_refund_encsig = a.encsign(S_b_bitcoin, tx_refund.digest());
let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig)
.context("Failed to recover Monero secret key from Bitcoin signature")?;
let s_b = monero::private_key_from_secp256k1_scalar(s_b.into());
let spend_key = s_a + s_b;
Ok(spend_key)
}

@ -6,18 +6,13 @@ use crate::env::Config;
use crate::monero_ext::ScalarExt;
use crate::protocol::alice;
use crate::protocol::alice::event_loop::EventLoopHandle;
use crate::protocol::alice::steps::{
extract_monero_private_key, lock_xmr, publish_cancel_transaction,
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund,
};
use crate::protocol::alice::AliceState;
use crate::{bitcoin, database, monero};
use anyhow::{bail, Context, Result};
use async_recursion::async_recursion;
use futures::future::{select, Either};
use futures::pin_mut;
use rand::{CryptoRng, RngCore};
use std::sync::Arc;
use tokio::select;
use tokio::time::timeout;
use tracing::{error, info};
use uuid::Uuid;
@ -73,382 +68,275 @@ async fn run_until_internal(
) -> Result<AliceState> {
info!("Current state: {}", state);
if is_target_state(&state) {
Ok(state)
} else {
match state {
AliceState::Started { state3 } => {
timeout(
env_config.bob_time_to_act,
bitcoin_wallet
.watch_until_status(&state3.tx_lock, |status| status.has_been_seen()),
)
.await
.context("Failed to find lock Bitcoin tx")??;
bitcoin_wallet
.watch_until_status(&state3.tx_lock, |status| {
status.is_confirmed_with(env_config.bitcoin_finality_confirmations)
})
.await?;
let state = AliceState::BtcLocked { state3 };
let db_state = (&state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet,
monero_wallet,
env_config,
swap_id,
db,
)
.await
}
AliceState::BtcLocked { state3 } => {
// Record the current monero wallet block height so we don't have to scan from
// block 0 for scenarios where we create a refund wallet.
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
lock_xmr(*state3.clone(), &mut event_loop_handle, &monero_wallet).await?;
let state = AliceState::XmrLocked {
state3,
monero_wallet_restore_blockheight,
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet,
monero_wallet,
env_config,
swap_id,
db,
)
.await
}
return Ok(state);
}
let new_state = match state {
AliceState::Started { state3 } => {
timeout(
env_config.bob_time_to_act,
bitcoin_wallet.watch_until_status(&state3.tx_lock, |status| status.has_been_seen()),
)
.await
.context("Failed to find lock Bitcoin tx")??;
bitcoin_wallet
.watch_until_status(&state3.tx_lock, |status| {
status.is_confirmed_with(env_config.bitcoin_finality_confirmations)
})
.await?;
AliceState::BtcLocked { state3 }
}
AliceState::BtcLocked { state3 } => {
// Record the current monero wallet block height so we don't have to scan from
// block 0 for scenarios where we create a refund wallet.
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
let transfer_proof = monero_wallet
.transfer(state3.lock_xmr_transfer_request())
.await?;
// TODO(Franck): Wait for Monero to be confirmed once
// Waiting for XMR confirmations should not be done in here, but in a separate
// state! We have to record that Alice has already sent the transaction.
// Otherwise Alice might publish the lock tx twice!
event_loop_handle
.send_transfer_proof(transfer_proof)
.await?;
AliceState::XmrLocked {
state3,
monero_wallet_restore_blockheight,
} => {
let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
ExpiredTimelocks::None => {
let wait_for_enc_sig =
wait_for_bitcoin_encrypted_signature(&mut event_loop_handle);
let state3_clone = state3.clone();
let cancel_timelock_expires = state3_clone
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
pin_mut!(wait_for_enc_sig);
pin_mut!(cancel_timelock_expires);
match select(cancel_timelock_expires, wait_for_enc_sig).await {
Either::Left(_) => AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
},
Either::Right((enc_sig, _)) => AliceState::EncSigLearned {
state3,
encrypted_signature: Box::new(enc_sig?),
monero_wallet_restore_blockheight,
},
}
}
AliceState::XmrLocked {
state3,
monero_wallet_restore_blockheight,
} => match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
ExpiredTimelocks::None => {
select! {
_ = state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => {
AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
}
}
_ => AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
},
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
env_config,
swap_id,
db,
)
.await
}
AliceState::EncSigLearned {
state3,
encrypted_signature,
monero_wallet_restore_blockheight,
} => {
let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
ExpiredTimelocks::None => {
match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete(
*encrypted_signature,
state3.a.clone(),
state3.s_a.to_secpfun_scalar(),
state3.B,
) {
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
Ok((_, finality)) => match finality.await {
Ok(_) => AliceState::BtcRedeemed,
Err(e) => {
bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
}
},
Err(e) => {
error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e);
state3
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref())
.await?;
AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
}
}
},
Err(e) => {
error!("Constructing the redeem transaction failed with {}, attempting to wait for cancellation now.", e);
state3
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref())
.await?;
AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
}
}
enc_sig = event_loop_handle.recv_encrypted_signature() => {
tracing::info!("Received encrypted signature");
AliceState::EncSigLearned {
state3,
encrypted_signature: Box::new(enc_sig?),
monero_wallet_restore_blockheight,
}
}
_ => AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
},
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet,
monero_wallet,
env_config,
swap_id,
db,
)
.await
}
}
AliceState::CancelTimelockExpired {
_ => AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
} => {
publish_cancel_transaction(
state3.tx_lock.clone(),
},
},
AliceState::EncSigLearned {
state3,
encrypted_signature,
monero_wallet_restore_blockheight,
} => match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
ExpiredTimelocks::None => {
match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete(
*encrypted_signature,
state3.a.clone(),
state3.s_a.to_secpfun_scalar(),
state3.B,
state3.cancel_timelock,
state3.tx_cancel_sig_bob.clone(),
&bitcoin_wallet,
)
.await?;
let state = AliceState::BtcCancelled {
state3,
monero_wallet_restore_blockheight,
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet,
monero_wallet,
env_config,
swap_id,
db,
)
.await
}
AliceState::BtcCancelled {
state3,
monero_wallet_restore_blockheight,
} => {
let published_refund_tx = wait_for_bitcoin_refund(
&state3.tx_cancel(),
&state3.tx_refund(),
state3.punish_timelock,
&bitcoin_wallet,
)
.await?;
) {
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
Ok((_, finality)) => match finality.await {
Ok(_) => AliceState::BtcRedeemed,
Err(e) => {
bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
}
},
Err(e) => {
error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e);
state3
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref())
.await?;
// TODO(Franck): Review error handling
match published_refund_tx {
None => {
let state = AliceState::BtcPunishable {
state3,
monero_wallet_restore_blockheight,
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
}
}
},
Err(e) => {
error!("Constructing the redeem transaction failed with {}, attempting to wait for cancellation now.", e);
state3
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref())
.await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
env_config,
swap_id,
db,
)
.await
}
Some(published_refund_tx) => {
let spend_key = extract_monero_private_key(
published_refund_tx,
&state3.tx_refund(),
state3.s_a,
state3.a.clone(),
state3.S_b_bitcoin,
)?;
let state = AliceState::BtcRefunded {
spend_key,
AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
env_config,
swap_id,
db,
)
.await
}
}
}
}
AliceState::BtcRefunded {
spend_key,
_ => AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
} => {
let view_key = state3.v;
monero_wallet
.create_from(spend_key, view_key, monero_wallet_restore_blockheight)
.await?;
let state = AliceState::XmrRefunded;
let db_state = (&state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?;
Ok(state)
},
},
AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight,
} => {
let tx_cancel = state3.tx_cancel();
// If Bob hasn't yet broadcasted the tx cancel, we do it
if bitcoin_wallet
.get_raw_transaction(tx_cancel.txid())
.await
.is_err()
{
let transaction = tx_cancel
.complete_as_alice(state3.a.clone(), state3.B, state3.tx_cancel_sig_bob.clone())
.context("Failed to complete Bitcoin cancel transaction")?;
if let Err(e) = bitcoin_wallet.broadcast(transaction, "cancel").await {
tracing::debug!(
"Assuming transaction is already broadcasted because: {:#}",
e
)
}
// TODO(Franck): Wait until transaction is mined and
// returned mined block height
}
AliceState::BtcPunishable {
AliceState::BtcCancelled {
state3,
monero_wallet_restore_blockheight,
} => {
let signed_tx_punish = state3.tx_punish().complete(
state3.tx_punish_sig_bob.clone(),
state3.a.clone(),
state3.B,
)?;
let punish_tx_finalised = async {
let (txid, finality) =
bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
finality.await?;
Result::<_, anyhow::Error>::Ok(txid)
};
let tx_refund = state3.tx_refund();
let refund_tx_seen =
bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen());
pin_mut!(punish_tx_finalised);
pin_mut!(refund_tx_seen);
match select(refund_tx_seen, punish_tx_finalised).await {
Either::Left((Ok(()), _)) => {
let published_refund_tx =
bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
let spend_key = extract_monero_private_key(
published_refund_tx,
&tx_refund,
state3.s_a,
state3.a.clone(),
state3.S_b_bitcoin,
)?;
let state = AliceState::BtcRefunded {
spend_key,
state3,
monero_wallet_restore_blockheight,
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
env_config,
swap_id,
db,
)
.await
}
}
AliceState::BtcCancelled {
state3,
monero_wallet_restore_blockheight,
} => {
let tx_refund = state3.tx_refund();
let tx_cancel = state3.tx_cancel();
let seen_refund_tx =
bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen());
let punish_timelock_expired = bitcoin_wallet.watch_until_status(&tx_cancel, |status| {
status.is_confirmed_with(state3.punish_timelock)
});
select! {
seen_refund = seen_refund_tx => {
seen_refund.context("Failed to monitor refund transaction")?;
let published_refund_tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
let spend_key = tx_refund.extract_monero_private_key(
published_refund_tx,
state3.s_a,
state3.a.clone(),
state3.S_b_bitcoin,
)?;
AliceState::BtcRefunded {
spend_key,
state3,
monero_wallet_restore_blockheight,
}
Either::Left((Err(e), _)) => {
bail!(e.context("Failed to monitor refund transaction"))
}
_ = punish_timelock_expired => {
AliceState::BtcPunishable {
state3,
monero_wallet_restore_blockheight,
}
Either::Right(_) => {
let state = AliceState::BtcPunished;
let db_state = (&state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
bitcoin_wallet.clone(),
monero_wallet,
env_config,
swap_id,
db,
)
.await
}
}
}
AliceState::BtcRefunded {
spend_key,
state3,
monero_wallet_restore_blockheight,
} => {
let view_key = state3.v;
monero_wallet
.create_from(spend_key, view_key, monero_wallet_restore_blockheight)
.await?;
AliceState::XmrRefunded
}
AliceState::BtcPunishable {
state3,
monero_wallet_restore_blockheight,
} => {
let signed_tx_punish = state3.tx_punish().complete(
state3.tx_punish_sig_bob.clone(),
state3.a.clone(),
state3.B,
)?;
let punish_tx_finalised = async {
let (txid, finality) = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
finality.await?;
Result::<_, anyhow::Error>::Ok(txid)
};
let tx_refund = state3.tx_refund();
let refund_tx_seen =
bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen());
select! {
result = refund_tx_seen => {
result.context("Failed to monitor refund transaction")?;
let published_refund_tx =
bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
let spend_key = tx_refund.extract_monero_private_key(
published_refund_tx,
state3.s_a,
state3.a.clone(),
state3.S_b_bitcoin,
)?;
AliceState::BtcRefunded {
spend_key,
state3,
monero_wallet_restore_blockheight,
}
}
_ = punish_tx_finalised => {
AliceState::BtcPunished
}
}
AliceState::XmrRefunded => Ok(AliceState::XmrRefunded),
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed),
AliceState::BtcPunished => Ok(AliceState::BtcPunished),
AliceState::SafelyAborted => Ok(AliceState::SafelyAborted),
}
}
AliceState::XmrRefunded => AliceState::XmrRefunded,
AliceState::BtcRedeemed => AliceState::BtcRedeemed,
AliceState::BtcPunished => AliceState::BtcPunished,
AliceState::SafelyAborted => AliceState::SafelyAborted,
};
let db_state = (&new_state).into();
db.insert_latest_state(swap_id, database::Swap::Alice(db_state))
.await?;
run_until_internal(
new_state,
is_target_state,
event_loop_handle,
bitcoin_wallet,
monero_wallet,
env_config,
swap_id,
db,
)
.await
}

@ -20,12 +20,12 @@ pub async fn cancel(
db: Database,
force: bool,
) -> Result<Result<(Txid, BobState), Error>> {
let state4 = match state {
let state6 = match state {
BobState::BtcLocked(state3) => state3.cancel(),
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
BobState::XmrLocked(state4) => state4,
BobState::EncSigSent(state4) => state4,
BobState::CancelTimelockExpired(state4) => state4,
BobState::XmrLocked(state4) => state4.cancel(),
BobState::EncSigSent(state4) => state4.cancel(),
BobState::CancelTimelockExpired(state6) => state6,
_ => bail!(
"Cannot cancel swap {} because it is in state {} which is not refundable.",
swap_id,
@ -34,16 +34,16 @@ pub async fn cancel(
};
if !force {
if let ExpiredTimelocks::None = state4.expired_timelock(bitcoin_wallet.as_ref()).await? {
if let ExpiredTimelocks::None = state6.expired_timelock(bitcoin_wallet.as_ref()).await? {
return Ok(Err(Error::CancelTimelockNotExpiredYet));
}
if state4
if state6
.check_for_tx_cancel(bitcoin_wallet.as_ref())
.await
.is_ok()
{
let state = BobState::BtcCancelled(state4);
let state = BobState::BtcCancelled(state6);
let db_state = state.into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
@ -51,9 +51,9 @@ pub async fn cancel(
}
}
let txid = state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
let txid = state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
let state = BobState::BtcCancelled(state4);
let state = BobState::BtcCancelled(state6);
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;

@ -16,14 +16,14 @@ pub async fn refund(
db: Database,
force: bool,
) -> Result<Result<BobState, SwapNotCancelledYet>> {
let state4 = if force {
let state6 = if force {
match state {
BobState::BtcLocked(state3) => state3.cancel(),
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
BobState::XmrLocked(state4) => state4,
BobState::EncSigSent(state4) => state4,
BobState::CancelTimelockExpired(state4) => state4,
BobState::BtcCancelled(state4) => state4,
BobState::XmrLocked(state4) => state4.cancel(),
BobState::EncSigSent(state4) => state4.cancel(),
BobState::CancelTimelockExpired(state6) => state6,
BobState::BtcCancelled(state6) => state6,
_ => bail!(
"Cannot refund swap {} because it is in state {} which is not refundable.",
swap_id,
@ -32,16 +32,16 @@ pub async fn refund(
}
} else {
match state {
BobState::BtcCancelled(state4) => state4,
BobState::BtcCancelled(state6) => state6,
_ => {
return Ok(Err(SwapNotCancelledYet(swap_id)));
}
}
};
state4.refund_btc(bitcoin_wallet.as_ref()).await?;
state6.refund_btc(bitcoin_wallet.as_ref()).await?;
let state = BobState::BtcRefunded(state4);
let state = BobState::BtcRefunded(state6);
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;

@ -3,12 +3,13 @@ use crate::bitcoin::{
TxLock, Txid,
};
use crate::monero;
use crate::monero::{monero_private_key, InsufficientFunds, TransferProof};
use crate::monero::wallet::WatchRequest;
use crate::monero::{monero_private_key, TransferProof};
use crate::monero_ext::ScalarExt;
use crate::protocol::alice::{Message1, Message3};
use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4};
use crate::protocol::CROSS_CURVE_PROOF_SYSTEM;
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, bail, Context, Result};
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
use ecdsa_fun::nonce::Deterministic;
use ecdsa_fun::Signature;
@ -34,9 +35,9 @@ pub enum BobState {
XmrLocked(State4),
EncSigSent(State4),
BtcRedeemed(State5),
CancelTimelockExpired(State4),
BtcCancelled(State4),
BtcRefunded(State4),
CancelTimelockExpired(State6),
BtcCancelled(State6),
BtcRefunded(State6),
XmrRedeemed {
tx_lock_id: bitcoin::Txid,
},
@ -305,30 +306,22 @@ pub struct State3 {
}
impl State3 {
pub async fn watch_for_lock_xmr(
self,
xmr_wallet: &monero::Wallet,
transfer_proof: TransferProof,
monero_wallet_restore_blockheight: BlockHeight,
) -> Result<Result<State4, InsufficientFunds>> {
pub fn lock_xmr_watch_request(&self, transfer_proof: TransferProof) -> WatchRequest {
let S_b_monero =
monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b));
let S = self.S_a_monero + S_b_monero;
if let Err(e) = xmr_wallet
.watch_for_transfer(
S,
self.v.public(),
transfer_proof,
self.xmr,
self.min_monero_confirmations,
)
.await
{
return Ok(Err(e));
WatchRequest {
public_spend_key: S,
public_view_key: self.v.public(),
transfer_proof,
conf_target: self.min_monero_confirmations,
expected: self.xmr,
}
}
Ok(Ok(State4 {
pub fn xmr_locked(self, monero_wallet_restore_blockheight: BlockHeight) -> State4 {
State4 {
A: self.A,
b: self.b,
s_b: self.s_b,
@ -342,7 +335,7 @@ impl State3 {
tx_cancel_sig_a: self.tx_cancel_sig_a,
tx_refund_encsig: self.tx_refund_encsig,
monero_wallet_restore_blockheight,
}))
}
}
pub async fn wait_for_cancel_timelock_to_expire(
@ -357,23 +350,17 @@ impl State3 {
Ok(())
}
pub fn cancel(&self) -> State4 {
State4 {
pub fn cancel(&self) -> State6 {
State6 {
A: self.A,
b: self.b.clone(),
s_b: self.s_b,
S_a_bitcoin: self.S_a_bitcoin,
v: self.v,
cancel_timelock: self.cancel_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address.clone(),
redeem_address: self.redeem_address.clone(),
tx_lock: self.tx_lock.clone(),
tx_cancel_sig_a: self.tx_cancel_sig_a.clone(),
tx_refund_encsig: self.tx_refund_encsig.clone(),
// For cancel scenarios the monero wallet rescan blockchain height is irrelevant for
// Bob, because Bob's cancel can only lead to refunding on Bitcoin
monero_wallet_restore_blockheight: BlockHeight { height: 0 },
}
}
@ -428,37 +415,6 @@ impl State4 {
self.b.encsign(self.S_a_bitcoin, tx_redeem.digest())
}
pub async fn check_for_tx_cancel(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<Transaction> {
let tx_cancel =
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
Ok(tx)
}
pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
let tx_cancel =
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
let sig_a = self.tx_cancel_sig_a.clone();
let sig_b = self.b.sign(tx_cancel.digest());
let tx_cancel = tx_cancel
.add_signatures((self.A, sig_a), (self.b.public(), sig_b))
.expect(
"sig_{a,b} to be valid signatures for
tx_cancel",
);
let (tx_id, _) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
Ok(tx_id)
}
pub async fn watch_for_redeem_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<State5> {
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
@ -513,29 +469,18 @@ impl State4 {
))
}
pub async fn refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> {
let tx_cancel =
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
let sig_b = self.b.sign(tx_refund.digest());
let sig_a =
adaptor.decrypt_signature(&self.s_b.to_secpfun_scalar(), self.tx_refund_encsig.clone());
let signed_tx_refund =
tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?;
let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?;
finality.await?;
Ok(())
}
pub fn tx_lock_id(&self) -> bitcoin::Txid {
self.tx_lock.txid()
pub fn cancel(self) -> State6 {
State6 {
A: self.A,
b: self.b,
s_b: self.s_b,
cancel_timelock: self.cancel_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
tx_lock: self.tx_lock,
tx_cancel_sig_a: self.tx_cancel_sig_a,
tx_refund_encsig: self.tx_refund_encsig,
}
}
}
@ -567,3 +512,83 @@ impl State5 {
self.tx_lock.txid()
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct State6 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
s_b: monero::Scalar,
cancel_timelock: CancelTimelock,
punish_timelock: PunishTimelock,
refund_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: bitcoin::EncryptedSignature,
}
impl State6 {
pub async fn expired_timelock(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<ExpiredTimelocks> {
let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
Ok(current_epoch(
self.cancel_timelock,
self.punish_timelock,
tx_lock_status,
tx_cancel_status,
))
}
pub async fn check_for_tx_cancel(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<Transaction> {
let tx_cancel =
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
Ok(tx)
}
pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
let transaction =
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public())
.complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone())
.context("Failed to complete Bitcoin cancel transaction")?;
let (tx_id, _) = bitcoin_wallet.broadcast(transaction, "cancel").await?;
Ok(tx_id)
}
pub async fn refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> {
let tx_cancel =
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
let sig_b = self.b.sign(tx_refund.digest());
let sig_a =
adaptor.decrypt_signature(&self.s_b.to_secpfun_scalar(), self.tx_refund_encsig.clone());
let signed_tx_refund =
tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?;
let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?;
finality.await?;
Ok(())
}
pub fn tx_lock_id(&self) -> bitcoin::Txid {
self.tx_lock.txid()
}
}

@ -1,7 +1,6 @@
use crate::bitcoin::ExpiredTimelocks;
use crate::database::{Database, Swap};
use crate::env::Config;
use crate::monero::InsufficientFunds;
use crate::protocol::bob;
use crate::protocol::bob::event_loop::EventLoopHandle;
use crate::protocol::bob::state::*;
@ -11,7 +10,7 @@ use async_recursion::async_recursion;
use rand::rngs::OsRng;
use std::sync::Arc;
use tokio::select;
use tracing::{trace, warn};
use tracing::trace;
use uuid::Uuid;
pub fn is_complete(state: &BobState) -> bool {
@ -63,348 +62,205 @@ async fn run_until_internal(
) -> Result<BobState> {
trace!("Current state: {}", state);
if is_target_state(&state) {
Ok(state)
} else {
match state {
BobState::Started { btc_amount } => {
let bitcoin_refund_address = bitcoin_wallet.new_address().await?;
return Ok(state);
}
event_loop_handle.dial().await?;
let new_state = match state {
BobState::Started { btc_amount } => {
let bitcoin_refund_address = bitcoin_wallet.new_address().await?;
let state2 = request_price_and_setup(
btc_amount,
&mut event_loop_handle,
env_config,
bitcoin_refund_address,
)
.await?;
event_loop_handle.dial().await?;
let state = BobState::ExecutionSetupDone(state2);
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
swap_id,
env_config,
receive_monero_address,
)
.await
}
BobState::ExecutionSetupDone(state2) => {
// Do not lock Bitcoin if not connected to Alice.
event_loop_handle.dial().await?;
// Alice and Bob have exchanged info
let (state3, tx_lock) = state2.lock_btc().await?;
let signed_tx = bitcoin_wallet
.sign_and_finalize(tx_lock.clone().into())
.await
.context("Failed to sign Bitcoin lock transaction")?;
let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
let state2 = request_price_and_setup(
btc_amount,
&mut event_loop_handle,
env_config,
bitcoin_refund_address,
)
.await?;
let state = BobState::BtcLocked(state3);
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
swap_id,
env_config,
receive_monero_address,
)
BobState::ExecutionSetupDone(state2)
}
BobState::ExecutionSetupDone(state2) => {
// Do not lock Bitcoin if not connected to Alice.
event_loop_handle.dial().await?;
// Alice and Bob have exchanged info
let (state3, tx_lock) = state2.lock_btc().await?;
let signed_tx = bitcoin_wallet
.sign_and_finalize(tx_lock.clone().into())
.await
}
// Bob has locked Btc
// Watch for Alice to Lock Xmr or for cancel timelock to elapse
BobState::BtcLocked(state3) => {
let state = if let ExpiredTimelocks::None =
state3.current_epoch(bitcoin_wallet.as_ref()).await?
{
event_loop_handle.dial().await?;
.context("Failed to sign Bitcoin lock transaction")?;
let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
let transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
let cancel_timelock_expires =
state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
BobState::BtcLocked(state3)
}
// Bob has locked Btc
// Watch for Alice to Lock Xmr or for cancel timelock to elapse
BobState::BtcLocked(state3) => {
if let ExpiredTimelocks::None = state3.current_epoch(bitcoin_wallet.as_ref()).await? {
event_loop_handle.dial().await?;
// Record the current monero wallet block height so we don't have to scan from
// block 0 once we create the redeem wallet.
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
let transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
let cancel_timelock_expires =
state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
tracing::info!("Waiting for Alice to lock Monero");
// Record the current monero wallet block height so we don't have to scan from
// block 0 once we create the redeem wallet.
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
select! {
transfer_proof = transfer_proof_watcher => {
let transfer_proof = transfer_proof?.tx_lock_proof;
tracing::info!("Waiting for Alice to lock Monero");
tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero");
select! {
transfer_proof = transfer_proof_watcher => {
let transfer_proof = transfer_proof?.tx_lock_proof;
BobState::XmrLockProofReceived {
state: state3,
lock_transfer_proof: transfer_proof,
monero_wallet_restore_blockheight
}
},
_ = cancel_timelock_expires => {
tracing::info!("Alice took too long to lock Monero, cancelling the swap");
tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero");
let state4 = state3.cancel();
BobState::CancelTimelockExpired(state4)
BobState::XmrLockProofReceived {
state: state3,
lock_transfer_proof: transfer_proof,
monero_wallet_restore_blockheight
}
}
} else {
let state4 = state3.cancel();
BobState::CancelTimelockExpired(state4)
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
swap_id,
env_config,
receive_monero_address,
)
.await
}
BobState::XmrLockProofReceived {
state,
lock_transfer_proof,
monero_wallet_restore_blockheight,
} => {
let state = if let ExpiredTimelocks::None =
state.current_epoch(bitcoin_wallet.as_ref()).await?
{
event_loop_handle.dial().await?;
let xmr_lock_watcher = state.clone().watch_for_lock_xmr(
monero_wallet.as_ref(),
lock_transfer_proof,
monero_wallet_restore_blockheight,
);
let cancel_timelock_expires =
state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
},
_ = cancel_timelock_expires => {
tracing::info!("Alice took too long to lock Monero, cancelling the swap");
select! {
state4 = xmr_lock_watcher => {
match state4? {
Ok(state4) => BobState::XmrLocked(state4),
Err(InsufficientFunds {..}) => {
warn!("The other party has locked insufficient Monero funds! Waiting for refund...");
state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()).await?;
let state4 = state.cancel();
BobState::CancelTimelockExpired(state4)
},
}
},
_ = cancel_timelock_expires => {
let state4 = state.cancel();
BobState::CancelTimelockExpired(state4)
}
let state4 = state3.cancel();
BobState::CancelTimelockExpired(state4)
}
} else {
let state4 = state.cancel();
BobState::CancelTimelockExpired(state4)
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
swap_id,
env_config,
receive_monero_address,
)
.await
}
} else {
let state4 = state3.cancel();
BobState::CancelTimelockExpired(state4)
}
BobState::XmrLocked(state) => {
let state = if let ExpiredTimelocks::None =
state.expired_timelock(bitcoin_wallet.as_ref()).await?
{
event_loop_handle.dial().await?;
// Alice has locked Xmr
// Bob sends Alice his key
let tx_redeem_encsig = state.tx_redeem_encsig();
}
BobState::XmrLockProofReceived {
state,
lock_transfer_proof,
monero_wallet_restore_blockheight,
} => {
if let ExpiredTimelocks::None = state.current_epoch(bitcoin_wallet.as_ref()).await? {
event_loop_handle.dial().await?;
let state4_clone = state.clone();
let watch_request = state.lock_xmr_watch_request(lock_transfer_proof);
let enc_sig_sent_watcher =
event_loop_handle.send_encrypted_signature(tx_redeem_encsig);
let bitcoin_wallet = bitcoin_wallet.clone();
let cancel_timelock_expires =
state4_clone.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
select! {
received_xmr = monero_wallet.watch_for_transfer(watch_request) => {
match received_xmr {
Ok(()) => BobState::XmrLocked(state.xmr_locked(monero_wallet_restore_blockheight)),
Err(e) => {
tracing::warn!("Waiting for refund because insufficient Monero have been locked! {}", e);
state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()).await?;
select! {
_ = enc_sig_sent_watcher => {
BobState::EncSigSent(state)
},
_ = cancel_timelock_expires => {
BobState::CancelTimelockExpired(state)
BobState::CancelTimelockExpired(state.cancel())
},
}
}
} else {
BobState::CancelTimelockExpired(state)
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
swap_id,
env_config,
receive_monero_address,
)
.await
}
BobState::EncSigSent(state) => {
let state = if let ExpiredTimelocks::None =
state.expired_timelock(bitcoin_wallet.as_ref()).await?
{
let state_clone = state.clone();
let redeem_watcher = state_clone.watch_for_redeem_btc(bitcoin_wallet.as_ref());
let cancel_timelock_expires =
state_clone.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref());
select! {
state5 = redeem_watcher => {
BobState::BtcRedeemed(state5?)
},
_ = cancel_timelock_expires => {
BobState::CancelTimelockExpired(state)
}
_ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => {
BobState::CancelTimelockExpired(state.cancel())
}
} else {
BobState::CancelTimelockExpired(state)
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
db,
bitcoin_wallet.clone(),
monero_wallet,
swap_id,
env_config,
receive_monero_address,
)
.await
}
} else {
BobState::CancelTimelockExpired(state.cancel())
}
BobState::BtcRedeemed(state) => {
// Bob redeems XMR using revealed s_a
state.claim_xmr(monero_wallet.as_ref()).await?;
// Ensure that the generated wallet is synced so we have a proper balance
monero_wallet.refresh().await?;
// Sweep (transfer all funds) to the given address
let tx_hashes = monero_wallet.sweep_all(receive_monero_address).await?;
}
BobState::XmrLocked(state) => {
if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet.as_ref()).await? {
event_loop_handle.dial().await?;
// Alice has locked Xmr
// Bob sends Alice his key
for tx_hash in tx_hashes {
tracing::info!("Sent XMR to {} in tx {}", receive_monero_address, tx_hash.0);
select! {
_ = event_loop_handle.send_encrypted_signature(state.tx_redeem_encsig()) => {
BobState::EncSigSent(state)
},
_ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => {
BobState::CancelTimelockExpired(state.cancel())
}
}
let state = BobState::XmrRedeemed {
tx_lock_id: state.tx_lock_id(),
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
swap_id,
env_config,
receive_monero_address,
)
.await
} else {
BobState::CancelTimelockExpired(state.cancel())
}
BobState::CancelTimelockExpired(state4) => {
if state4
.check_for_tx_cancel(bitcoin_wallet.as_ref())
.await
.is_err()
{
state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
}
BobState::EncSigSent(state) => {
if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet.as_ref()).await? {
select! {
state5 = state.watch_for_redeem_btc(bitcoin_wallet.as_ref()) => {
BobState::BtcRedeemed(state5?)
},
_ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => {
BobState::CancelTimelockExpired(state.cancel())
}
}
} else {
BobState::CancelTimelockExpired(state.cancel())
}
}
BobState::BtcRedeemed(state) => {
// Bob redeems XMR using revealed s_a
state.claim_xmr(monero_wallet.as_ref()).await?;
let state = BobState::BtcCancelled(state4);
db.insert_latest_state(swap_id, Swap::Bob(state.clone().into()))
.await?;
// Ensure that the generated wallet is synced so we have a proper balance
monero_wallet.refresh().await?;
// Sweep (transfer all funds) to the given address
let tx_hashes = monero_wallet.sweep_all(receive_monero_address).await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
swap_id,
env_config,
receive_monero_address,
)
.await
for tx_hash in tx_hashes {
tracing::info!("Sent XMR to {} in tx {}", receive_monero_address, tx_hash.0);
}
BobState::BtcCancelled(state) => {
// Bob has cancelled the swap
let state = match state.expired_timelock(bitcoin_wallet.as_ref()).await? {
ExpiredTimelocks::None => {
bail!("Internal error: canceled state reached before cancel timelock was expired");
}
ExpiredTimelocks::Cancel => {
state.refund_btc(bitcoin_wallet.as_ref()).await?;
BobState::BtcRefunded(state)
}
ExpiredTimelocks::Punish => BobState::BtcPunished {
tx_lock_id: state.tx_lock_id(),
},
};
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until_internal(
state,
is_target_state,
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
swap_id,
env_config,
receive_monero_address,
)
BobState::XmrRedeemed {
tx_lock_id: state.tx_lock_id(),
}
}
BobState::CancelTimelockExpired(state4) => {
if state4
.check_for_tx_cancel(bitcoin_wallet.as_ref())
.await
.is_err()
{
state4.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
}
BobState::BtcRefunded(state4) => Ok(BobState::BtcRefunded(state4)),
BobState::BtcPunished { tx_lock_id } => Ok(BobState::BtcPunished { tx_lock_id }),
BobState::SafelyAborted => Ok(BobState::SafelyAborted),
BobState::XmrRedeemed { tx_lock_id } => Ok(BobState::XmrRedeemed { tx_lock_id }),
BobState::BtcCancelled(state4)
}
}
BobState::BtcCancelled(state) => {
// Bob has cancelled the swap
match state.expired_timelock(bitcoin_wallet.as_ref()).await? {
ExpiredTimelocks::None => {
bail!(
"Internal error: canceled state reached before cancel timelock was expired"
);
}
ExpiredTimelocks::Cancel => {
state.refund_btc(bitcoin_wallet.as_ref()).await?;
BobState::BtcRefunded(state)
}
ExpiredTimelocks::Punish => BobState::BtcPunished {
tx_lock_id: state.tx_lock_id(),
},
}
}
BobState::BtcRefunded(state4) => BobState::BtcRefunded(state4),
BobState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id },
BobState::SafelyAborted => BobState::SafelyAborted,
BobState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id },
};
let db_state = new_state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
run_until_internal(
new_state,
is_target_state,
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
swap_id,
env_config,
receive_monero_address,
)
.await
}
pub async fn request_price_and_setup(

Loading…
Cancel
Save