commit
a9bb4e6bed
@ -0,0 +1,379 @@
|
|||||||
|
use crate::{
|
||||||
|
alice::{amounts, OutEvent, Swarm},
|
||||||
|
bitcoin, monero,
|
||||||
|
network::request_response::AliceToBob,
|
||||||
|
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
||||||
|
};
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use conquer_once::Lazy;
|
||||||
|
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
|
||||||
|
use futures::{
|
||||||
|
future::{select, Either},
|
||||||
|
pin_mut,
|
||||||
|
};
|
||||||
|
use libp2p::request_response::ResponseChannel;
|
||||||
|
use sha2::Sha256;
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
use tokio::time::timeout;
|
||||||
|
use xmr_btc::{
|
||||||
|
alice,
|
||||||
|
alice::{State0, State3},
|
||||||
|
bitcoin::{
|
||||||
|
poll_until_block_height_is_gte, BlockHeight, BroadcastSignedTransaction,
|
||||||
|
EncryptedSignature, GetRawTransaction, TransactionBlockHeight, TxCancel, TxLock, TxRefund,
|
||||||
|
WaitForTransactionFinality, WatchForRawTransaction,
|
||||||
|
},
|
||||||
|
cross_curve_dleq,
|
||||||
|
monero::Transfer,
|
||||||
|
};
|
||||||
|
|
||||||
|
// For each step, we are giving Bob 10 minutes to act.
|
||||||
|
static BOB_TIME_TO_ACT: Lazy<Duration> = Lazy::new(|| Duration::from_secs(10 * 60));
|
||||||
|
|
||||||
|
// The maximum we assume we need to wait from the moment the monero transaction
|
||||||
|
// is mined to the moment it reaches finality. We set 15 confirmations for now
|
||||||
|
// (based on Kraken). 1.5 multiplier in case the blockchain is slower than
|
||||||
|
// usually. Average of 2 minutes block time
|
||||||
|
static MONERO_MAX_FINALITY_TIME: Lazy<Duration> =
|
||||||
|
Lazy::new(|| Duration::from_secs_f64(15f64 * 1.5 * 2f64 * 60f64));
|
||||||
|
|
||||||
|
pub async fn negotiate(
|
||||||
|
amounts: SwapAmounts,
|
||||||
|
a: bitcoin::SecretKey,
|
||||||
|
s_a: cross_curve_dleq::Scalar,
|
||||||
|
v_a: monero::PrivateViewKey,
|
||||||
|
swarm: &mut Swarm,
|
||||||
|
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||||
|
) -> Result<(ResponseChannel<AliceToBob>, State3)> {
|
||||||
|
let event = timeout(*BOB_TIME_TO_ACT, swarm.next())
|
||||||
|
.await
|
||||||
|
.context("Failed to receive dial connection from Bob")?;
|
||||||
|
match event {
|
||||||
|
OutEvent::ConnectionEstablished(_bob_peer_id) => {}
|
||||||
|
other => bail!("Unexpected event received: {:?}", other),
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = timeout(*BOB_TIME_TO_ACT, swarm.next())
|
||||||
|
.await
|
||||||
|
.context("Failed to receive amounts from Bob")?;
|
||||||
|
let (btc, channel) = match event {
|
||||||
|
OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => (btc, channel),
|
||||||
|
other => bail!("Unexpected event received: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
if btc != amounts.btc {
|
||||||
|
bail!(
|
||||||
|
"Bob proposed a different amount; got {}, expected: {}",
|
||||||
|
btc,
|
||||||
|
amounts.btc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// TODO: get an ack from libp2p2
|
||||||
|
swarm.send_amounts(channel, amounts);
|
||||||
|
|
||||||
|
let redeem_address = bitcoin_wallet.as_ref().new_address().await?;
|
||||||
|
let punish_address = redeem_address.clone();
|
||||||
|
|
||||||
|
let state0 = State0::new(
|
||||||
|
a,
|
||||||
|
s_a,
|
||||||
|
v_a,
|
||||||
|
amounts.btc,
|
||||||
|
amounts.xmr,
|
||||||
|
REFUND_TIMELOCK,
|
||||||
|
PUNISH_TIMELOCK,
|
||||||
|
redeem_address,
|
||||||
|
punish_address,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO(Franck): Understand why this is needed.
|
||||||
|
swarm.set_state0(state0.clone());
|
||||||
|
|
||||||
|
let event = timeout(*BOB_TIME_TO_ACT, swarm.next())
|
||||||
|
.await
|
||||||
|
.context("Failed to receive message 0 from Bob")?;
|
||||||
|
let message0 = match event {
|
||||||
|
OutEvent::Message0(msg) => msg,
|
||||||
|
other => bail!("Unexpected event received: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
let state1 = state0.receive(message0)?;
|
||||||
|
|
||||||
|
let event = timeout(*BOB_TIME_TO_ACT, swarm.next())
|
||||||
|
.await
|
||||||
|
.context("Failed to receive message 1 from Bob")?;
|
||||||
|
let (msg, channel) = match event {
|
||||||
|
OutEvent::Message1 { msg, channel } => (msg, channel),
|
||||||
|
other => bail!("Unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
let state2 = state1.receive(msg);
|
||||||
|
|
||||||
|
let message1 = state2.next_message();
|
||||||
|
swarm.send_message1(channel, message1);
|
||||||
|
|
||||||
|
let event = timeout(*BOB_TIME_TO_ACT, swarm.next())
|
||||||
|
.await
|
||||||
|
.context("Failed to receive message 2 from Bob")?;
|
||||||
|
let (msg, channel) = match event {
|
||||||
|
OutEvent::Message2 { msg, channel } => (msg, channel),
|
||||||
|
other => bail!("Unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
let state3 = state2.receive(msg)?;
|
||||||
|
|
||||||
|
Ok((channel, state3))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_for_locked_bitcoin<W>(
|
||||||
|
lock_bitcoin_txid: bitcoin::Txid,
|
||||||
|
bitcoin_wallet: Arc<W>,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
W: WatchForRawTransaction + WaitForTransactionFinality,
|
||||||
|
{
|
||||||
|
// We assume we will see Bob's transaction in the mempool first.
|
||||||
|
timeout(
|
||||||
|
*BOB_TIME_TO_ACT,
|
||||||
|
bitcoin_wallet.watch_for_raw_transaction(lock_bitcoin_txid),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to find lock Bitcoin tx")?;
|
||||||
|
|
||||||
|
// // We saw the transaction in the mempool, waiting for it to be confirmed.
|
||||||
|
// bitcoin_wallet
|
||||||
|
// .wait_for_transaction_finality(lock_bitcoin_txid)
|
||||||
|
// .await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lock_xmr<W>(
|
||||||
|
channel: ResponseChannel<AliceToBob>,
|
||||||
|
amounts: SwapAmounts,
|
||||||
|
state3: State3,
|
||||||
|
swarm: &mut Swarm,
|
||||||
|
monero_wallet: Arc<W>,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
W: Transfer,
|
||||||
|
{
|
||||||
|
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey {
|
||||||
|
scalar: state3.s_a.into_ed25519(),
|
||||||
|
});
|
||||||
|
|
||||||
|
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, amounts.xmr)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO(Franck): Wait for Monero to be confirmed once
|
||||||
|
|
||||||
|
swarm.send_message2(channel, alice::Message2 {
|
||||||
|
tx_lock_proof: transfer_proof,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_for_bitcoin_encrypted_signature(swarm: &mut Swarm) -> Result<EncryptedSignature> {
|
||||||
|
let event = timeout(*MONERO_MAX_FINALITY_TIME, swarm.next())
|
||||||
|
.await
|
||||||
|
.context("Failed to receive Bitcoin encrypted signature from Bob")?;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig),
|
||||||
|
other => bail!(
|
||||||
|
"Expected Bob's Bitcoin redeem encrypted signature, got: {:?}",
|
||||||
|
other
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_bitcoin_redeem_transaction(
|
||||||
|
encrypted_signature: EncryptedSignature,
|
||||||
|
tx_lock: &TxLock,
|
||||||
|
a: bitcoin::SecretKey,
|
||||||
|
s_a: cross_curve_dleq::Scalar,
|
||||||
|
B: bitcoin::PublicKey,
|
||||||
|
redeem_address: &bitcoin::Address,
|
||||||
|
) -> Result<bitcoin::Transaction> {
|
||||||
|
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||||
|
|
||||||
|
let tx_redeem = bitcoin::TxRedeem::new(tx_lock, redeem_address);
|
||||||
|
|
||||||
|
bitcoin::verify_encsig(
|
||||||
|
B.clone(),
|
||||||
|
s_a.into_secp256k1().into(),
|
||||||
|
&tx_redeem.digest(),
|
||||||
|
&encrypted_signature,
|
||||||
|
)
|
||||||
|
.context("Invalid encrypted signature received")?;
|
||||||
|
|
||||||
|
let sig_a = a.sign(tx_redeem.digest());
|
||||||
|
let sig_b = adaptor.decrypt_signature(&s_a.into_secp256k1(), encrypted_signature);
|
||||||
|
|
||||||
|
let tx = tx_redeem
|
||||||
|
.add_signatures(&tx_lock, (a.public(), sig_a), (B, sig_b))
|
||||||
|
.context("sig_{a,b} are invalid for tx_redeem")?;
|
||||||
|
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_bitcoin_redeem_transaction<W>(
|
||||||
|
redeem_tx: bitcoin::Transaction,
|
||||||
|
bitcoin_wallet: Arc<W>,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
W: BroadcastSignedTransaction + WaitForTransactionFinality,
|
||||||
|
{
|
||||||
|
let _tx_id = bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(redeem_tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// // TODO(Franck): Not sure if we wait for finality here or just mined
|
||||||
|
// bitcoin_wallet.wait_for_transaction_finality(tx_id).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_cancel_transaction<W>(
|
||||||
|
tx_lock: TxLock,
|
||||||
|
a: bitcoin::SecretKey,
|
||||||
|
B: bitcoin::PublicKey,
|
||||||
|
refund_timelock: u32,
|
||||||
|
tx_cancel_sig_bob: bitcoin::Signature,
|
||||||
|
bitcoin_wallet: Arc<W>,
|
||||||
|
) -> Result<bitcoin::TxCancel>
|
||||||
|
where
|
||||||
|
W: GetRawTransaction + TransactionBlockHeight + BlockHeight + BroadcastSignedTransaction,
|
||||||
|
{
|
||||||
|
// First wait for t1 to expire
|
||||||
|
let tx_lock_height = bitcoin_wallet
|
||||||
|
.transaction_block_height(tx_lock.txid())
|
||||||
|
.await;
|
||||||
|
poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), tx_lock_height + refund_timelock).await;
|
||||||
|
|
||||||
|
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
|
||||||
|
|
||||||
|
// 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
|
||||||
|
.clone()
|
||||||
|
.add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), sig_b))
|
||||||
|
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||||
|
|
||||||
|
// TODO(Franck): Error handling is delicate, why can't we broadcast?
|
||||||
|
bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(tx_cancel)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO(Franck): Wait until transaction is mined and returned mined
|
||||||
|
// block height
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tx_cancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_for_bitcoin_refund<W>(
|
||||||
|
tx_cancel: &TxCancel,
|
||||||
|
cancel_tx_height: u32,
|
||||||
|
punish_timelock: u32,
|
||||||
|
refund_address: &bitcoin::Address,
|
||||||
|
bitcoin_wallet: Arc<W>,
|
||||||
|
) -> Result<(bitcoin::TxRefund, Option<bitcoin::Transaction>)>
|
||||||
|
where
|
||||||
|
W: BlockHeight + WatchForRawTransaction,
|
||||||
|
{
|
||||||
|
let punish_timelock_expired =
|
||||||
|
poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), cancel_tx_height + punish_timelock);
|
||||||
|
|
||||||
|
let tx_refund = bitcoin::TxRefund::new(tx_cancel, refund_address);
|
||||||
|
|
||||||
|
// TODO(Franck): This only checks the mempool, need to cater for the case where
|
||||||
|
// the transaction goes directly in a block
|
||||||
|
let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
|
||||||
|
|
||||||
|
pin_mut!(punish_timelock_expired);
|
||||||
|
pin_mut!(seen_refund_tx);
|
||||||
|
|
||||||
|
match select(punish_timelock_expired, seen_refund_tx).await {
|
||||||
|
Either::Left(_) => Ok((tx_refund, None)),
|
||||||
|
Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_monero_private_key(
|
||||||
|
published_refund_tx: bitcoin::Transaction,
|
||||||
|
tx_refund: TxRefund,
|
||||||
|
s_a: cross_curve_dleq::Scalar,
|
||||||
|
a: bitcoin::SecretKey,
|
||||||
|
S_b_bitcoin: bitcoin::PublicKey,
|
||||||
|
) -> Result<monero::PrivateKey> {
|
||||||
|
let s_a = monero::PrivateKey {
|
||||||
|
scalar: s_a.into_ed25519(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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.clone(), 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_bitcoin_punish_transaction(
|
||||||
|
tx_lock: &TxLock,
|
||||||
|
refund_timelock: u32,
|
||||||
|
punish_address: &bitcoin::Address,
|
||||||
|
punish_timelock: u32,
|
||||||
|
tx_punish_sig_bob: bitcoin::Signature,
|
||||||
|
a: bitcoin::SecretKey,
|
||||||
|
B: bitcoin::PublicKey,
|
||||||
|
) -> Result<bitcoin::Transaction> {
|
||||||
|
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, refund_timelock, a.public(), B.clone());
|
||||||
|
let tx_punish = bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock);
|
||||||
|
|
||||||
|
let sig_a = a.sign(tx_punish.digest());
|
||||||
|
let sig_b = tx_punish_sig_bob;
|
||||||
|
|
||||||
|
let signed_tx_punish = tx_punish
|
||||||
|
.add_signatures(&tx_cancel, (a.public(), sig_a), (B, sig_b))
|
||||||
|
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||||
|
|
||||||
|
Ok(signed_tx_punish)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_bitcoin_punish_transaction<W>(
|
||||||
|
punish_tx: bitcoin::Transaction,
|
||||||
|
bitcoin_wallet: Arc<W>,
|
||||||
|
) -> Result<bitcoin::Txid>
|
||||||
|
where
|
||||||
|
W: BroadcastSignedTransaction + WaitForTransactionFinality,
|
||||||
|
{
|
||||||
|
let txid = bitcoin_wallet
|
||||||
|
.broadcast_signed_transaction(punish_tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// todo: enable this once trait is implemented
|
||||||
|
// bitcoin_wallet.wait_for_transaction_finality(txid).await;
|
||||||
|
|
||||||
|
Ok(txid)
|
||||||
|
}
|
@ -0,0 +1,351 @@
|
|||||||
|
//! Run an XMR/BTC swap in the role of Alice.
|
||||||
|
//! Alice holds XMR and wishes receive BTC.
|
||||||
|
use crate::{
|
||||||
|
alice::{
|
||||||
|
execution::{
|
||||||
|
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
|
||||||
|
extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction,
|
||||||
|
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
|
||||||
|
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
|
||||||
|
},
|
||||||
|
Swarm,
|
||||||
|
},
|
||||||
|
bitcoin,
|
||||||
|
bitcoin::EncryptedSignature,
|
||||||
|
monero,
|
||||||
|
network::request_response::AliceToBob,
|
||||||
|
SwapAmounts,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_recursion::async_recursion;
|
||||||
|
use futures::{
|
||||||
|
future::{select, Either},
|
||||||
|
pin_mut,
|
||||||
|
};
|
||||||
|
use libp2p::request_response::ResponseChannel;
|
||||||
|
use rand::{CryptoRng, RngCore};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use xmr_btc::{
|
||||||
|
alice::State3,
|
||||||
|
bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction},
|
||||||
|
cross_curve_dleq,
|
||||||
|
monero::CreateWalletForOutput,
|
||||||
|
};
|
||||||
|
|
||||||
|
trait Rng: RngCore + CryptoRng + Send {}
|
||||||
|
|
||||||
|
impl<T> Rng for T where T: RngCore + CryptoRng + Send {}
|
||||||
|
|
||||||
|
// The same data structure is used for swap execution and recovery.
|
||||||
|
// This allows for a seamless transition from a failed swap to recovery.
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub enum AliceState {
|
||||||
|
Started {
|
||||||
|
amounts: SwapAmounts,
|
||||||
|
a: bitcoin::SecretKey,
|
||||||
|
s_a: cross_curve_dleq::Scalar,
|
||||||
|
v_a: monero::PrivateViewKey,
|
||||||
|
},
|
||||||
|
Negotiated {
|
||||||
|
channel: ResponseChannel<AliceToBob>,
|
||||||
|
amounts: SwapAmounts,
|
||||||
|
state3: State3,
|
||||||
|
},
|
||||||
|
BtcLocked {
|
||||||
|
channel: ResponseChannel<AliceToBob>,
|
||||||
|
amounts: SwapAmounts,
|
||||||
|
state3: State3,
|
||||||
|
},
|
||||||
|
XmrLocked {
|
||||||
|
state3: State3,
|
||||||
|
},
|
||||||
|
EncSignLearned {
|
||||||
|
state3: State3,
|
||||||
|
encrypted_signature: EncryptedSignature,
|
||||||
|
},
|
||||||
|
BtcRedeemed,
|
||||||
|
BtcCancelled {
|
||||||
|
state3: State3,
|
||||||
|
tx_cancel: TxCancel,
|
||||||
|
},
|
||||||
|
BtcRefunded {
|
||||||
|
tx_refund: TxRefund,
|
||||||
|
published_refund_tx: ::bitcoin::Transaction,
|
||||||
|
state3: State3,
|
||||||
|
},
|
||||||
|
BtcPunishable {
|
||||||
|
tx_refund: TxRefund,
|
||||||
|
state3: State3,
|
||||||
|
},
|
||||||
|
XmrRefunded,
|
||||||
|
WaitingToCancel {
|
||||||
|
state3: State3,
|
||||||
|
},
|
||||||
|
Punished,
|
||||||
|
SafelyAborted,
|
||||||
|
}
|
||||||
|
|
||||||
|
// State machine driver for swap execution
|
||||||
|
#[async_recursion]
|
||||||
|
pub async fn swap(
|
||||||
|
state: AliceState,
|
||||||
|
mut swarm: Swarm,
|
||||||
|
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
||||||
|
monero_wallet: Arc<crate::monero::Wallet>,
|
||||||
|
) -> Result<AliceState> {
|
||||||
|
match state {
|
||||||
|
AliceState::Started {
|
||||||
|
amounts,
|
||||||
|
a,
|
||||||
|
s_a,
|
||||||
|
v_a,
|
||||||
|
} => {
|
||||||
|
let (channel, state3) =
|
||||||
|
negotiate(amounts, a, s_a, v_a, &mut swarm, bitcoin_wallet.clone()).await?;
|
||||||
|
|
||||||
|
swap(
|
||||||
|
AliceState::Negotiated {
|
||||||
|
channel,
|
||||||
|
amounts,
|
||||||
|
state3,
|
||||||
|
},
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
AliceState::Negotiated {
|
||||||
|
state3,
|
||||||
|
channel,
|
||||||
|
amounts,
|
||||||
|
} => {
|
||||||
|
let _ = wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone()).await?;
|
||||||
|
|
||||||
|
swap(
|
||||||
|
AliceState::BtcLocked {
|
||||||
|
channel,
|
||||||
|
amounts,
|
||||||
|
state3,
|
||||||
|
},
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
AliceState::BtcLocked {
|
||||||
|
channel,
|
||||||
|
amounts,
|
||||||
|
state3,
|
||||||
|
} => {
|
||||||
|
lock_xmr(
|
||||||
|
channel,
|
||||||
|
amounts,
|
||||||
|
state3.clone(),
|
||||||
|
&mut swarm,
|
||||||
|
monero_wallet.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
swap(
|
||||||
|
AliceState::XmrLocked { state3 },
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
AliceState::XmrLocked { state3 } => {
|
||||||
|
// Our Monero is locked, we need to go through the cancellation process if this
|
||||||
|
// step fails
|
||||||
|
match wait_for_bitcoin_encrypted_signature(&mut swarm).await {
|
||||||
|
Ok(encrypted_signature) => {
|
||||||
|
swap(
|
||||||
|
AliceState::EncSignLearned {
|
||||||
|
state3,
|
||||||
|
encrypted_signature,
|
||||||
|
},
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
swap(
|
||||||
|
AliceState::WaitingToCancel { state3 },
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AliceState::EncSignLearned {
|
||||||
|
state3,
|
||||||
|
encrypted_signature,
|
||||||
|
} => {
|
||||||
|
let signed_tx_redeem = match build_bitcoin_redeem_transaction(
|
||||||
|
encrypted_signature,
|
||||||
|
&state3.tx_lock,
|
||||||
|
state3.a.clone(),
|
||||||
|
state3.s_a,
|
||||||
|
state3.B.clone(),
|
||||||
|
&state3.redeem_address,
|
||||||
|
) {
|
||||||
|
Ok(tx) => tx,
|
||||||
|
Err(_) => {
|
||||||
|
return swap(
|
||||||
|
AliceState::WaitingToCancel { state3 },
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(Franck): Error handling is delicate here.
|
||||||
|
// If Bob sees this transaction he can redeem Monero
|
||||||
|
// e.g. If the Bitcoin node is down then the user needs to take action.
|
||||||
|
publish_bitcoin_redeem_transaction(signed_tx_redeem, bitcoin_wallet.clone()).await?;
|
||||||
|
|
||||||
|
swap(
|
||||||
|
AliceState::BtcRedeemed,
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
AliceState::WaitingToCancel { state3 } => {
|
||||||
|
let tx_cancel = publish_cancel_transaction(
|
||||||
|
state3.tx_lock.clone(),
|
||||||
|
state3.a.clone(),
|
||||||
|
state3.B.clone(),
|
||||||
|
state3.refund_timelock,
|
||||||
|
state3.tx_cancel_sig_bob.clone(),
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
swap(
|
||||||
|
AliceState::BtcCancelled { state3, tx_cancel },
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
AliceState::BtcCancelled { state3, tx_cancel } => {
|
||||||
|
let tx_cancel_height = bitcoin_wallet
|
||||||
|
.transaction_block_height(tx_cancel.txid())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund(
|
||||||
|
&tx_cancel,
|
||||||
|
tx_cancel_height,
|
||||||
|
state3.punish_timelock,
|
||||||
|
&state3.refund_address,
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO(Franck): Review error handling
|
||||||
|
match published_refund_tx {
|
||||||
|
None => {
|
||||||
|
swap(
|
||||||
|
AliceState::BtcPunishable { tx_refund, state3 },
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Some(published_refund_tx) => {
|
||||||
|
swap(
|
||||||
|
AliceState::BtcRefunded {
|
||||||
|
tx_refund,
|
||||||
|
published_refund_tx,
|
||||||
|
state3,
|
||||||
|
},
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AliceState::BtcRefunded {
|
||||||
|
tx_refund,
|
||||||
|
published_refund_tx,
|
||||||
|
state3,
|
||||||
|
} => {
|
||||||
|
let spend_key = extract_monero_private_key(
|
||||||
|
published_refund_tx,
|
||||||
|
tx_refund,
|
||||||
|
state3.s_a,
|
||||||
|
state3.a.clone(),
|
||||||
|
state3.S_b_bitcoin,
|
||||||
|
)?;
|
||||||
|
let view_key = state3.v;
|
||||||
|
|
||||||
|
monero_wallet
|
||||||
|
.create_and_load_wallet_for_output(spend_key, view_key)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(AliceState::XmrRefunded)
|
||||||
|
}
|
||||||
|
AliceState::BtcPunishable { tx_refund, state3 } => {
|
||||||
|
let signed_tx_punish = build_bitcoin_punish_transaction(
|
||||||
|
&state3.tx_lock,
|
||||||
|
state3.refund_timelock,
|
||||||
|
&state3.punish_address,
|
||||||
|
state3.punish_timelock,
|
||||||
|
state3.tx_punish_sig_bob.clone(),
|
||||||
|
state3.a.clone(),
|
||||||
|
state3.B.clone(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let punish_tx_finalised =
|
||||||
|
publish_bitcoin_punish_transaction(signed_tx_punish, bitcoin_wallet.clone());
|
||||||
|
|
||||||
|
let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
|
||||||
|
|
||||||
|
pin_mut!(punish_tx_finalised);
|
||||||
|
pin_mut!(refund_tx_seen);
|
||||||
|
|
||||||
|
match select(punish_tx_finalised, refund_tx_seen).await {
|
||||||
|
Either::Left(_) => {
|
||||||
|
swap(
|
||||||
|
AliceState::Punished,
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Either::Right((published_refund_tx, _)) => {
|
||||||
|
swap(
|
||||||
|
AliceState::BtcRefunded {
|
||||||
|
tx_refund,
|
||||||
|
published_refund_tx,
|
||||||
|
state3,
|
||||||
|
},
|
||||||
|
swarm,
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
monero_wallet,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AliceState::XmrRefunded => Ok(AliceState::XmrRefunded),
|
||||||
|
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed),
|
||||||
|
AliceState::Punished => Ok(AliceState::Punished),
|
||||||
|
AliceState::SafelyAborted => Ok(AliceState::SafelyAborted),
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
use crate::{
|
||||||
|
bob::{OutEvent, Swarm},
|
||||||
|
SwapAmounts,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use libp2p::core::Multiaddr;
|
||||||
|
use rand::{CryptoRng, RngCore};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use xmr_btc::bob::State2;
|
||||||
|
|
||||||
|
pub async fn negotiate<R>(
|
||||||
|
state0: xmr_btc::bob::State0,
|
||||||
|
amounts: SwapAmounts,
|
||||||
|
swarm: &mut Swarm,
|
||||||
|
addr: Multiaddr,
|
||||||
|
mut rng: R,
|
||||||
|
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
||||||
|
) -> Result<State2>
|
||||||
|
where
|
||||||
|
R: RngCore + CryptoRng + Send,
|
||||||
|
{
|
||||||
|
libp2p::Swarm::dial_addr(swarm, addr)?;
|
||||||
|
|
||||||
|
let alice = match swarm.next().await {
|
||||||
|
OutEvent::ConnectionEstablished(alice) => alice,
|
||||||
|
other => panic!("unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
swarm.request_amounts(alice.clone(), amounts.btc.as_sat());
|
||||||
|
|
||||||
|
// todo: see if we can remove
|
||||||
|
let (_btc, _xmr) = match swarm.next().await {
|
||||||
|
OutEvent::Amounts(amounts) => (amounts.btc, amounts.xmr),
|
||||||
|
other => panic!("unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
swarm.send_message0(alice.clone(), state0.next_message(&mut rng));
|
||||||
|
let state1 = match swarm.next().await {
|
||||||
|
OutEvent::Message0(msg) => state0.receive(bitcoin_wallet.as_ref(), msg).await?,
|
||||||
|
other => panic!("unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
swarm.send_message1(alice.clone(), state1.next_message());
|
||||||
|
let state2 = match swarm.next().await {
|
||||||
|
OutEvent::Message1(msg) => state1.receive(msg)?,
|
||||||
|
other => panic!("unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
swarm.send_message2(alice.clone(), state2.next_message());
|
||||||
|
|
||||||
|
Ok(state2)
|
||||||
|
}
|
@ -0,0 +1,265 @@
|
|||||||
|
use crate::{
|
||||||
|
bob::{execution::negotiate, OutEvent, Swarm},
|
||||||
|
storage::Database,
|
||||||
|
SwapAmounts,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_recursion::async_recursion;
|
||||||
|
use libp2p::{core::Multiaddr, PeerId};
|
||||||
|
use rand::{CryptoRng, RngCore};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing::debug;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use xmr_btc::bob::{self};
|
||||||
|
|
||||||
|
// The same data structure is used for swap execution and recovery.
|
||||||
|
// This allows for a seamless transition from a failed swap to recovery.
|
||||||
|
pub enum BobState {
|
||||||
|
Started {
|
||||||
|
state0: bob::State0,
|
||||||
|
amounts: SwapAmounts,
|
||||||
|
peer_id: PeerId,
|
||||||
|
addr: Multiaddr,
|
||||||
|
},
|
||||||
|
Negotiated(bob::State2, PeerId),
|
||||||
|
BtcLocked(bob::State3, PeerId),
|
||||||
|
XmrLocked(bob::State4, PeerId),
|
||||||
|
EncSigSent(bob::State4, PeerId),
|
||||||
|
BtcRedeemed(bob::State5),
|
||||||
|
Cancelled(bob::State4),
|
||||||
|
BtcRefunded,
|
||||||
|
XmrRedeemed,
|
||||||
|
Punished,
|
||||||
|
SafelyAborted,
|
||||||
|
}
|
||||||
|
|
||||||
|
// State machine driver for swap execution
|
||||||
|
#[async_recursion]
|
||||||
|
pub async fn swap<R>(
|
||||||
|
state: BobState,
|
||||||
|
mut swarm: Swarm,
|
||||||
|
db: Database,
|
||||||
|
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
||||||
|
monero_wallet: Arc<crate::monero::Wallet>,
|
||||||
|
mut rng: R,
|
||||||
|
swap_id: Uuid,
|
||||||
|
) -> Result<BobState>
|
||||||
|
where
|
||||||
|
R: RngCore + CryptoRng + Send,
|
||||||
|
{
|
||||||
|
match state {
|
||||||
|
BobState::Started {
|
||||||
|
state0,
|
||||||
|
amounts,
|
||||||
|
peer_id,
|
||||||
|
addr,
|
||||||
|
} => {
|
||||||
|
let state2 = negotiate(
|
||||||
|
state0,
|
||||||
|
amounts,
|
||||||
|
&mut swarm,
|
||||||
|
addr,
|
||||||
|
&mut rng,
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
swap(
|
||||||
|
BobState::Negotiated(state2, peer_id),
|
||||||
|
swarm,
|
||||||
|
db,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
rng,
|
||||||
|
swap_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
BobState::Negotiated(state2, alice_peer_id) => {
|
||||||
|
// Alice and Bob have exchanged info
|
||||||
|
let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?;
|
||||||
|
// db.insert_latest_state(state);
|
||||||
|
swap(
|
||||||
|
BobState::BtcLocked(state3, alice_peer_id),
|
||||||
|
swarm,
|
||||||
|
db,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
rng,
|
||||||
|
swap_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
// Bob has locked Btc
|
||||||
|
// Watch for Alice to Lock Xmr or for t1 to elapse
|
||||||
|
BobState::BtcLocked(state3, alice_peer_id) => {
|
||||||
|
// todo: watch until t1, not indefinetely
|
||||||
|
let state4 = match swarm.next().await {
|
||||||
|
OutEvent::Message2(msg) => {
|
||||||
|
state3
|
||||||
|
.watch_for_lock_xmr(monero_wallet.as_ref(), msg)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
other => panic!("unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
swap(
|
||||||
|
BobState::XmrLocked(state4, alice_peer_id),
|
||||||
|
swarm,
|
||||||
|
db,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
rng,
|
||||||
|
swap_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
BobState::XmrLocked(state, alice_peer_id) => {
|
||||||
|
// Alice has locked Xmr
|
||||||
|
// Bob sends Alice his key
|
||||||
|
let tx_redeem_encsig = state.tx_redeem_encsig();
|
||||||
|
// Do we have to wait for a response?
|
||||||
|
// What if Alice fails to receive this? Should we always resend?
|
||||||
|
// todo: If we cannot dial Alice we should go to EncSigSent. Maybe dialing
|
||||||
|
// should happen in this arm?
|
||||||
|
swarm.send_message3(alice_peer_id.clone(), tx_redeem_encsig);
|
||||||
|
|
||||||
|
// Sadly we have to poll the swarm to get make sure the message is sent?
|
||||||
|
// FIXME: Having to wait for Alice's response here is a big problem, because
|
||||||
|
// we're stuck if she doesn't send her response back. I believe this is
|
||||||
|
// currently necessary, so we may have to rework this and/or how we use libp2p
|
||||||
|
match swarm.next().await {
|
||||||
|
OutEvent::Message3 => {
|
||||||
|
debug!("Got Message3 empty response");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected event: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
swap(
|
||||||
|
BobState::EncSigSent(state, alice_peer_id),
|
||||||
|
swarm,
|
||||||
|
db,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
rng,
|
||||||
|
swap_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
BobState::EncSigSent(state, ..) => {
|
||||||
|
// Watch for redeem
|
||||||
|
let redeem_watcher = state.watch_for_redeem_btc(bitcoin_wallet.as_ref());
|
||||||
|
let t1_timeout = state.wait_for_t1(bitcoin_wallet.as_ref());
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
val = redeem_watcher => {
|
||||||
|
swap(
|
||||||
|
BobState::BtcRedeemed(val?),
|
||||||
|
swarm,
|
||||||
|
db,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
rng,
|
||||||
|
swap_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ = t1_timeout => {
|
||||||
|
// Check whether TxCancel has been published.
|
||||||
|
// We should not fail if the transaction is already on the blockchain
|
||||||
|
if state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await.is_err() {
|
||||||
|
state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
swap(
|
||||||
|
BobState::Cancelled(state),
|
||||||
|
swarm,
|
||||||
|
db,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
rng,
|
||||||
|
swap_id
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BobState::BtcRedeemed(state) => {
|
||||||
|
// Bob redeems XMR using revealed s_a
|
||||||
|
state.claim_xmr(monero_wallet.as_ref()).await?;
|
||||||
|
swap(
|
||||||
|
BobState::XmrRedeemed,
|
||||||
|
swarm,
|
||||||
|
db,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
rng,
|
||||||
|
swap_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
BobState::Cancelled(_state) => Ok(BobState::BtcRefunded),
|
||||||
|
BobState::BtcRefunded => Ok(BobState::BtcRefunded),
|
||||||
|
BobState::Punished => Ok(BobState::Punished),
|
||||||
|
BobState::SafelyAborted => Ok(BobState::SafelyAborted),
|
||||||
|
BobState::XmrRedeemed => Ok(BobState::XmrRedeemed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // State machine driver for recovery execution
|
||||||
|
// #[async_recursion]
|
||||||
|
// pub async fn abort(state: BobState, io: Io) -> Result<BobState> {
|
||||||
|
// match state {
|
||||||
|
// BobState::Started => {
|
||||||
|
// // Nothing has been commited by either party, abort swap.
|
||||||
|
// abort(BobState::SafelyAborted, io).await
|
||||||
|
// }
|
||||||
|
// BobState::Negotiated => {
|
||||||
|
// // Nothing has been commited by either party, abort swap.
|
||||||
|
// abort(BobState::SafelyAborted, io).await
|
||||||
|
// }
|
||||||
|
// BobState::BtcLocked => {
|
||||||
|
// // Bob has locked BTC and must refund it
|
||||||
|
// // Bob waits for alice to publish TxRedeem or t1
|
||||||
|
// if unimplemented!("TxRedeemSeen") {
|
||||||
|
// // Alice has redeemed revealing s_a
|
||||||
|
// abort(BobState::BtcRedeemed, io).await
|
||||||
|
// } else if unimplemented!("T1Elapsed") {
|
||||||
|
// // publish TxCancel or see if it has been published
|
||||||
|
// abort(BobState::Cancelled, io).await
|
||||||
|
// } else {
|
||||||
|
// Err(unimplemented!())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// BobState::XmrLocked => {
|
||||||
|
// // Alice has locked Xmr
|
||||||
|
// // Wait until t1
|
||||||
|
// if unimplemented!(">t1 and <t2") {
|
||||||
|
// // Bob publishes TxCancel
|
||||||
|
// abort(BobState::Cancelled, io).await
|
||||||
|
// } else {
|
||||||
|
// // >t2
|
||||||
|
// // submit TxCancel
|
||||||
|
// abort(BobState::Punished, io).await
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// BobState::Cancelled => {
|
||||||
|
// // Bob has cancelled the swap
|
||||||
|
// // If <t2 Bob refunds
|
||||||
|
// if unimplemented!("<t2") {
|
||||||
|
// // Submit TxRefund
|
||||||
|
// abort(BobState::BtcRefunded, io).await
|
||||||
|
// } else {
|
||||||
|
// // Bob failed to refund in time and has been punished
|
||||||
|
// abort(BobState::Punished, io).await
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// BobState::BtcRedeemed => {
|
||||||
|
// // Bob uses revealed s_a to redeem XMR
|
||||||
|
// abort(BobState::XmrRedeemed, io).await
|
||||||
|
// }
|
||||||
|
// BobState::BtcRefunded => Ok(BobState::BtcRefunded),
|
||||||
|
// BobState::Punished => Ok(BobState::Punished),
|
||||||
|
// BobState::SafelyAborted => Ok(BobState::SafelyAborted),
|
||||||
|
// BobState::XmrRedeemed => Ok(BobState::XmrRedeemed),
|
||||||
|
// }
|
||||||
|
// }
|
Loading…
Reference in New Issue