325: Misc cleanup r=thomaseizinger a=thomaseizinger

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

Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
bors[bot] 2021-03-18 21:57:21 +00:00 committed by GitHub
commit 113a29839f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 696 additions and 987 deletions

View File

@ -1,12 +1,12 @@
use crate::bitcoin;
use crate::bitcoin::wallet::Watchable; use crate::bitcoin::wallet::Watchable;
use crate::bitcoin::{ use crate::bitcoin::{
build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock, build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock,
TX_FEE, TX_FEE,
}; };
use ::bitcoin::util::bip143::SigHashCache; 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 anyhow::Result;
use bitcoin::Script;
use ecdsa_fun::Signature; use ecdsa_fun::Signature;
use miniscript::{Descriptor, DescriptorTrait}; use miniscript::{Descriptor, DescriptorTrait};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -149,7 +149,39 @@ impl TxCancel {
OutPoint::new(self.inner.txid(), 0) 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, self,
(A, sig_a): (PublicKey, Signature), (A, sig_a): (PublicKey, Signature),
(B, sig_b): (PublicKey, Signature), (B, sig_b): (PublicKey, Signature),

View File

@ -3,10 +3,10 @@ use crate::bitcoin::{
verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs, verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs,
Transaction, TxCancel, Transaction, TxCancel,
}; };
use crate::{bitcoin, monero};
use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::util::bip143::SigHashCache;
use ::bitcoin::{SigHash, SigHashType, Txid}; use ::bitcoin::{Script, SigHash, SigHashType, Txid};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use bitcoin::Script;
use ecdsa_fun::Signature; use ecdsa_fun::Signature;
use miniscript::{Descriptor, DescriptorTrait}; use miniscript::{Descriptor, DescriptorTrait};
use std::collections::HashMap; use std::collections::HashMap;
@ -77,7 +77,31 @@ impl TxRefund {
Ok(tx_refund) 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, &self,
candidate_transaction: Transaction, candidate_transaction: Transaction,
B: PublicKey, B: PublicKey,

View File

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

View File

@ -182,7 +182,7 @@ impl fmt::Display for TxHash {
} }
#[derive(Debug, Clone, Copy, thiserror::Error)] #[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 struct InsufficientFunds {
pub expected: Amount, pub expected: Amount,
pub actual: Amount, pub actual: Amount,

View File

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

View File

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

View File

@ -42,12 +42,6 @@ pub struct EventLoop<RS> {
swap_sender: mpsc::Sender<Swap>, 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> impl<LR> EventLoop<LR>
where where
LR: LatestRate, 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 { 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 let signature = self
.recv_encrypted_signature .recv_encrypted_signature
.take() .take()
.context("Encrypted signature was already received")? .context("Encrypted signature was already received")?
.await?; .await?
.tx_redeem_encsig;
Ok(signature) 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 if self
.send_transfer_proof .send_transfer_proof
.take() .take()
.context("Transfer proof was already sent")? .context("Transfer proof was already sent")?
.send(msg) .send(TransferProof { tx_lock_proof: msg })
.is_err() .is_err()
{ {
bail!("Failed to send transfer proof, receiver no longer listening?") bail!("Failed to send transfer proof, receiver no longer listening?")

View File

@ -2,6 +2,7 @@ use crate::bitcoin::{
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund,
}; };
use crate::env::Config; use crate::env::Config;
use crate::monero::wallet::TransferRequest;
use crate::protocol::alice::{Message1, Message3}; use crate::protocol::alice::{Message1, Message3};
use crate::protocol::bob::{Message0, Message2, Message4}; use crate::protocol::bob::{Message0, Message2, Message4};
use crate::protocol::CROSS_CURVE_PROOF_SYSTEM; 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 { pub fn tx_cancel(&self) -> TxCancel {
TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B) TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B)
} }

View File

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

View File

@ -6,18 +6,13 @@ use crate::env::Config;
use crate::monero_ext::ScalarExt; use crate::monero_ext::ScalarExt;
use crate::protocol::alice; use crate::protocol::alice;
use crate::protocol::alice::event_loop::EventLoopHandle; 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::protocol::alice::AliceState;
use crate::{bitcoin, database, monero}; use crate::{bitcoin, database, monero};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use async_recursion::async_recursion; use async_recursion::async_recursion;
use futures::future::{select, Either};
use futures::pin_mut;
use rand::{CryptoRng, RngCore}; use rand::{CryptoRng, RngCore};
use std::sync::Arc; use std::sync::Arc;
use tokio::select;
use tokio::time::timeout; use tokio::time::timeout;
use tracing::{error, info}; use tracing::{error, info};
use uuid::Uuid; use uuid::Uuid;
@ -73,382 +68,275 @@ async fn run_until_internal(
) -> Result<AliceState> { ) -> Result<AliceState> {
info!("Current state: {}", state); info!("Current state: {}", state);
if is_target_state(&state) { if is_target_state(&state) {
Ok(state) return 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 let new_state = match state {
.watch_until_status(&state3.tx_lock, |status| { AliceState::Started { state3 } => {
status.is_confirmed_with(env_config.bitcoin_finality_confirmations) timeout(
}) env_config.bob_time_to_act,
.await?; bitcoin_wallet.watch_until_status(&state3.tx_lock, |status| status.has_been_seen()),
)
.await
.context("Failed to find lock Bitcoin tx")??;
let state = AliceState::BtcLocked { state3 }; bitcoin_wallet
.watch_until_status(&state3.tx_lock, |status| {
status.is_confirmed_with(env_config.bitcoin_finality_confirmations)
})
.await?;
let db_state = (&state).into(); AliceState::BtcLocked { state3 }
db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) }
.await?; AliceState::BtcLocked { state3 } => {
run_until_internal( // Record the current monero wallet block height so we don't have to scan from
state, // block 0 for scenarios where we create a refund wallet.
is_target_state, let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
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 transfer_proof = monero_wallet
.transfer(state3.lock_xmr_transfer_request())
.await?;
let state = AliceState::XmrLocked { // TODO(Franck): Wait for Monero to be confirmed once
state3, // Waiting for XMR confirmations should not be done in here, but in a separate
monero_wallet_restore_blockheight, // 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?;
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::XmrLocked { AliceState::XmrLocked {
state3, state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
} => { }
let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { }
ExpiredTimelocks::None => { AliceState::XmrLocked {
let wait_for_enc_sig = state3,
wait_for_bitcoin_encrypted_signature(&mut event_loop_handle); monero_wallet_restore_blockheight,
let state3_clone = state3.clone(); } => match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
let cancel_timelock_expires = state3_clone ExpiredTimelocks::None => {
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()); select! {
_ = state3.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => {
pin_mut!(wait_for_enc_sig); AliceState::CancelTimelockExpired {
pin_mut!(cancel_timelock_expires); state3,
monero_wallet_restore_blockheight,
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::CancelTimelockExpired { enc_sig = event_loop_handle.recv_encrypted_signature() => {
state3, tracing::info!("Received encrypted signature");
monero_wallet_restore_blockheight,
},
};
let db_state = (&state).into(); AliceState::EncSigLearned {
db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) state3,
.await?; encrypted_signature: Box::new(enc_sig?),
run_until_internal( monero_wallet_restore_blockheight,
state, }
is_target_state, }
event_loop_handle, }
bitcoin_wallet.clone(),
monero_wallet,
env_config,
swap_id,
db,
)
.await
} }
AliceState::EncSigLearned { _ => AliceState::CancelTimelockExpired {
state3, state3,
encrypted_signature,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
} => { },
let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { },
ExpiredTimelocks::None => { AliceState::EncSigLearned {
match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete( state3,
*encrypted_signature, encrypted_signature,
state3.a.clone(), monero_wallet_restore_blockheight,
state3.s_a.to_secpfun_scalar(), } => match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
state3.B, ExpiredTimelocks::None => {
) { match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete(
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { *encrypted_signature,
Ok((_, finality)) => match finality.await { state3.a.clone(),
Ok(_) => AliceState::BtcRedeemed, state3.s_a.to_secpfun_scalar(),
Err(e) => { state3.B,
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) ) {
} Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
}, Ok((_, finality)) => match finality.await {
Err(e) => { Ok(_) => AliceState::BtcRedeemed,
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) => { Err(e) => {
error!("Constructing the redeem transaction failed with {}, attempting to wait for cancellation now.", 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)
state3 }
.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) },
.await?; 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 { AliceState::CancelTimelockExpired {
state3, state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
}
} }
} }
}
_ => 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?;
let db_state = (&state).into(); AliceState::CancelTimelockExpired {
db.insert_latest_state(swap_id, database::Swap::Alice(db_state)) state3,
.await?; monero_wallet_restore_blockheight,
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, state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
} => { },
publish_cancel_transaction( },
state3.tx_lock.clone(), AliceState::CancelTimelockExpired {
state3.a.clone(), state3,
state3.B, monero_wallet_restore_blockheight,
state3.cancel_timelock, } => {
state3.tx_cancel_sig_bob.clone(), let tx_cancel = state3.tx_cancel();
&bitcoin_wallet,
)
.await?;
let state = AliceState::BtcCancelled { // If Bob hasn't yet broadcasted the tx cancel, we do it
state3, if bitcoin_wallet
monero_wallet_restore_blockheight, .get_raw_transaction(tx_cancel.txid())
};
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 .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::BtcCancelled { AliceState::BtcCancelled {
state3, state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
} => { }
let published_refund_tx = wait_for_bitcoin_refund( }
&state3.tx_cancel(), AliceState::BtcCancelled {
&state3.tx_refund(), state3,
state3.punish_timelock, monero_wallet_restore_blockheight,
&bitcoin_wallet, } => {
) 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,
}
}
_ = punish_timelock_expired => {
AliceState::BtcPunishable {
state3,
monero_wallet_restore_blockheight,
}
}
}
}
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?; .await?;
// TODO(Franck): Review error handling AliceState::XmrRefunded
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))
.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,
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,
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::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());
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
}
Either::Left((Err(e), _)) => {
bail!(e.context("Failed to monitor refund transaction"))
}
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::XmrRefunded => Ok(AliceState::XmrRefunded),
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed),
AliceState::BtcPunished => Ok(AliceState::BtcPunished),
AliceState::SafelyAborted => Ok(AliceState::SafelyAborted),
} }
} 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 => 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
} }

View File

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

View File

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

View File

@ -3,12 +3,13 @@ use crate::bitcoin::{
TxLock, Txid, TxLock, Txid,
}; };
use crate::monero; 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::monero_ext::ScalarExt;
use crate::protocol::alice::{Message1, Message3}; use crate::protocol::alice::{Message1, Message3};
use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4}; use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4};
use crate::protocol::CROSS_CURVE_PROOF_SYSTEM; 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::adaptor::{Adaptor, HashTranscript};
use ecdsa_fun::nonce::Deterministic; use ecdsa_fun::nonce::Deterministic;
use ecdsa_fun::Signature; use ecdsa_fun::Signature;
@ -34,9 +35,9 @@ pub enum BobState {
XmrLocked(State4), XmrLocked(State4),
EncSigSent(State4), EncSigSent(State4),
BtcRedeemed(State5), BtcRedeemed(State5),
CancelTimelockExpired(State4), CancelTimelockExpired(State6),
BtcCancelled(State4), BtcCancelled(State6),
BtcRefunded(State4), BtcRefunded(State6),
XmrRedeemed { XmrRedeemed {
tx_lock_id: bitcoin::Txid, tx_lock_id: bitcoin::Txid,
}, },
@ -305,30 +306,22 @@ pub struct State3 {
} }
impl State3 { impl State3 {
pub async fn watch_for_lock_xmr( pub fn lock_xmr_watch_request(&self, transfer_proof: TransferProof) -> WatchRequest {
self,
xmr_wallet: &monero::Wallet,
transfer_proof: TransferProof,
monero_wallet_restore_blockheight: BlockHeight,
) -> Result<Result<State4, InsufficientFunds>> {
let S_b_monero = let S_b_monero =
monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b)); monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b));
let S = self.S_a_monero + S_b_monero; let S = self.S_a_monero + S_b_monero;
if let Err(e) = xmr_wallet WatchRequest {
.watch_for_transfer( public_spend_key: S,
S, public_view_key: self.v.public(),
self.v.public(), transfer_proof,
transfer_proof, conf_target: self.min_monero_confirmations,
self.xmr, expected: self.xmr,
self.min_monero_confirmations,
)
.await
{
return Ok(Err(e));
} }
}
Ok(Ok(State4 { pub fn xmr_locked(self, monero_wallet_restore_blockheight: BlockHeight) -> State4 {
State4 {
A: self.A, A: self.A,
b: self.b, b: self.b,
s_b: self.s_b, s_b: self.s_b,
@ -342,7 +335,7 @@ impl State3 {
tx_cancel_sig_a: self.tx_cancel_sig_a, tx_cancel_sig_a: self.tx_cancel_sig_a,
tx_refund_encsig: self.tx_refund_encsig, tx_refund_encsig: self.tx_refund_encsig,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
})) }
} }
pub async fn wait_for_cancel_timelock_to_expire( pub async fn wait_for_cancel_timelock_to_expire(
@ -357,23 +350,17 @@ impl State3 {
Ok(()) Ok(())
} }
pub fn cancel(&self) -> State4 { pub fn cancel(&self) -> State6 {
State4 { State6 {
A: self.A, A: self.A,
b: self.b.clone(), b: self.b.clone(),
s_b: self.s_b, s_b: self.s_b,
S_a_bitcoin: self.S_a_bitcoin,
v: self.v,
cancel_timelock: self.cancel_timelock, cancel_timelock: self.cancel_timelock,
punish_timelock: self.punish_timelock, punish_timelock: self.punish_timelock,
refund_address: self.refund_address.clone(), refund_address: self.refund_address.clone(),
redeem_address: self.redeem_address.clone(),
tx_lock: self.tx_lock.clone(), tx_lock: self.tx_lock.clone(),
tx_cancel_sig_a: self.tx_cancel_sig_a.clone(), tx_cancel_sig_a: self.tx_cancel_sig_a.clone(),
tx_refund_encsig: self.tx_refund_encsig.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()) 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> { 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 = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); 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<()> { pub fn cancel(self) -> State6 {
let tx_cancel = State6 {
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); A: self.A,
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address); b: self.b,
s_b: self.s_b,
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default(); cancel_timelock: self.cancel_timelock,
punish_timelock: self.punish_timelock,
let sig_b = self.b.sign(tx_refund.digest()); refund_address: self.refund_address,
let sig_a = tx_lock: self.tx_lock,
adaptor.decrypt_signature(&self.s_b.to_secpfun_scalar(), self.tx_refund_encsig.clone()); tx_cancel_sig_a: self.tx_cancel_sig_a,
tx_refund_encsig: self.tx_refund_encsig,
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()
} }
} }
@ -567,3 +512,83 @@ impl State5 {
self.tx_lock.txid() 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()
}
}

View File

@ -1,7 +1,6 @@
use crate::bitcoin::ExpiredTimelocks; use crate::bitcoin::ExpiredTimelocks;
use crate::database::{Database, Swap}; use crate::database::{Database, Swap};
use crate::env::Config; use crate::env::Config;
use crate::monero::InsufficientFunds;
use crate::protocol::bob; use crate::protocol::bob;
use crate::protocol::bob::event_loop::EventLoopHandle; use crate::protocol::bob::event_loop::EventLoopHandle;
use crate::protocol::bob::state::*; use crate::protocol::bob::state::*;
@ -11,7 +10,7 @@ use async_recursion::async_recursion;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use std::sync::Arc; use std::sync::Arc;
use tokio::select; use tokio::select;
use tracing::{trace, warn}; use tracing::trace;
use uuid::Uuid; use uuid::Uuid;
pub fn is_complete(state: &BobState) -> bool { pub fn is_complete(state: &BobState) -> bool {
@ -63,348 +62,205 @@ async fn run_until_internal(
) -> Result<BobState> { ) -> Result<BobState> {
trace!("Current state: {}", state); trace!("Current state: {}", state);
if is_target_state(&state) { if is_target_state(&state) {
Ok(state) return Ok(state);
} else {
match state {
BobState::Started { btc_amount } => {
let bitcoin_refund_address = bitcoin_wallet.new_address().await?;
event_loop_handle.dial().await?;
let state2 = request_price_and_setup(
btc_amount,
&mut event_loop_handle,
env_config,
bitcoin_refund_address,
)
.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 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,
)
.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?;
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());
// 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?;
tracing::info!("Waiting for Alice to lock Monero");
select! {
transfer_proof = transfer_proof_watcher => {
let transfer_proof = transfer_proof?.tx_lock_proof;
tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero");
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");
let state4 = state3.cancel();
BobState::CancelTimelockExpired(state4)
}
}
} 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());
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)
}
}
} 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
}
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();
let state4_clone = state.clone();
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! {
_ = enc_sig_sent_watcher => {
BobState::EncSigSent(state)
},
_ = cancel_timelock_expires => {
BobState::CancelTimelockExpired(state)
}
}
} 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)
}
}
} 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
}
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?;
for tx_hash in tx_hashes {
tracing::info!("Sent XMR to {} in tx {}", receive_monero_address, tx_hash.0);
}
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
}
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?;
}
let state = BobState::BtcCancelled(state4);
db.insert_latest_state(swap_id, Swap::Bob(state.clone().into()))
.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::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,
)
.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 }),
}
} }
let new_state = match state {
BobState::Started { btc_amount } => {
let bitcoin_refund_address = bitcoin_wallet.new_address().await?;
event_loop_handle.dial().await?;
let state2 = request_price_and_setup(
btc_amount,
&mut event_loop_handle,
env_config,
bitcoin_refund_address,
)
.await?;
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
.context("Failed to sign Bitcoin lock transaction")?;
let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
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?;
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());
// 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?;
tracing::info!("Waiting for Alice to lock Monero");
select! {
transfer_proof = transfer_proof_watcher => {
let transfer_proof = transfer_proof?.tx_lock_proof;
tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero");
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");
let state4 = state3.cancel();
BobState::CancelTimelockExpired(state4)
}
}
} else {
let state4 = state3.cancel();
BobState::CancelTimelockExpired(state4)
}
}
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 watch_request = state.lock_xmr_watch_request(lock_transfer_proof);
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?;
BobState::CancelTimelockExpired(state.cancel())
},
}
}
_ = state.wait_for_cancel_timelock_to_expire(bitcoin_wallet.as_ref()) => {
BobState::CancelTimelockExpired(state.cancel())
}
}
} else {
BobState::CancelTimelockExpired(state.cancel())
}
}
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
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())
}
}
} else {
BobState::CancelTimelockExpired(state.cancel())
}
}
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?;
// 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?;
for tx_hash in tx_hashes {
tracing::info!("Sent XMR to {} in tx {}", receive_monero_address, tx_hash.0);
}
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::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( pub async fn request_price_and_setup(