mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
Merge #325
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:
commit
113a29839f
@ -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),
|
||||||
|
@ -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,
|
||||||
|
@ -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 },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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?")
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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::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
|
||||||
}
|
}
|
||||||
|
@ -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?;
|
||||||
|
|
||||||
|
@ -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?;
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user