mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
Extract execution details from driving loop
This commit is contained in:
parent
24631d464d
commit
9e13034e54
@ -14,6 +14,7 @@ backoff = { version = "0.2", features = ["tokio"] }
|
||||
base64 = "0.12"
|
||||
bitcoin = { version = "0.23", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version.
|
||||
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "3be644cd9512c157d3337a189298b8257ed54d04" }
|
||||
conquer-once = "0.3"
|
||||
derivative = "2"
|
||||
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] }
|
||||
futures = { version = "0.3", default-features = false }
|
||||
|
@ -184,7 +184,7 @@ pub async fn swap(
|
||||
};
|
||||
|
||||
let swap_id = Uuid::new_v4();
|
||||
db.insert_latest_state(swap_id, state::Alice::Handshaken(state3.clone()).into())
|
||||
db.insert_latest_state(swap_id, state::Alice::Negotiated(state3.clone()).into())
|
||||
.await?;
|
||||
|
||||
info!("Handshake complete, we now have State3 for Alice.");
|
||||
@ -280,11 +280,7 @@ pub async fn swap(
|
||||
|
||||
pub type Swarm = libp2p::Swarm<Behaviour>;
|
||||
|
||||
pub fn new_swarm(
|
||||
listen: Multiaddr,
|
||||
transport: SwapTransport,
|
||||
behaviour: Behaviour,
|
||||
) -> Result<Swarm> {
|
||||
fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Behaviour) -> Result<Swarm> {
|
||||
use anyhow::Context as _;
|
||||
|
||||
let local_peer_id = behaviour.peer_id();
|
||||
|
@ -1,36 +1,62 @@
|
||||
use crate::{
|
||||
alice::{amounts, OutEvent, Swarm},
|
||||
bitcoin, monero,
|
||||
network::request_response::AliceToBob,
|
||||
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
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 std::sync::Arc;
|
||||
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,
|
||||
};
|
||||
|
||||
// TODO(Franck): Make all methods here idempotent using db
|
||||
// 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: crate::bitcoin::SecretKey,
|
||||
a: bitcoin::SecretKey,
|
||||
s_a: cross_curve_dleq::Scalar,
|
||||
v_a: crate::monero::PrivateViewKey,
|
||||
v_a: monero::PrivateViewKey,
|
||||
swarm: &mut Swarm,
|
||||
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
||||
) -> Result<(ResponseChannel<AliceToBob>, SwapAmounts, State3)> {
|
||||
// Bob dials us
|
||||
match swarm.next().await {
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
// Bob sends us a request
|
||||
let (btc, channel) = match swarm.next().await {
|
||||
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),
|
||||
};
|
||||
@ -42,8 +68,17 @@ pub async fn negotiate(
|
||||
amounts.btc
|
||||
);
|
||||
}
|
||||
// TODO: get an ack from libp2p2
|
||||
swarm.send_amounts(channel, amounts);
|
||||
|
||||
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 SwapAmounts { btc, xmr } = amounts;
|
||||
|
||||
let redeem_address = bitcoin_wallet.as_ref().new_address().await?;
|
||||
@ -61,43 +96,67 @@ pub async fn negotiate(
|
||||
punish_address,
|
||||
);
|
||||
|
||||
// Bob sends us message0
|
||||
let message0 = match swarm.next().await {
|
||||
OutEvent::Message0(msg) => msg,
|
||||
other => bail!("Unexpected event received: {:?}", other),
|
||||
};
|
||||
|
||||
let state1 = state0.receive(message0)?;
|
||||
|
||||
let (state2, channel) = match swarm.next().await {
|
||||
OutEvent::Message1 { msg, channel } => {
|
||||
let state2 = state1.receive(msg);
|
||||
(state2, channel)
|
||||
}
|
||||
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 (state3, channel) = match swarm.next().await {
|
||||
OutEvent::Message2 { msg, channel } => {
|
||||
let state3 = state2.receive(msg)?;
|
||||
(state3, channel)
|
||||
}
|
||||
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),
|
||||
};
|
||||
|
||||
Ok((channel, amounts, state3))
|
||||
let state3 = state2.receive(msg)?;
|
||||
|
||||
Ok((channel, state3))
|
||||
}
|
||||
|
||||
pub async fn lock_xmr(
|
||||
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<crate::monero::Wallet>,
|
||||
) -> Result<()> {
|
||||
monero_wallet: Arc<W>,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Transfer,
|
||||
{
|
||||
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey {
|
||||
scalar: state3.s_a.into_ed25519(),
|
||||
});
|
||||
@ -109,11 +168,206 @@ pub async fn lock_xmr(
|
||||
.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,
|
||||
});
|
||||
|
||||
// TODO(Franck): Wait for Monero to be mined/finalised
|
||||
|
||||
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.clone());
|
||||
|
||||
let tx = tx_redeem
|
||||
.add_signatures(&tx_lock, (a.public(), sig_a), (B.clone(), 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 let Err(_) = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await {
|
||||
// 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.clone();
|
||||
|
||||
let signed_tx_punish = tx_punish
|
||||
.add_signatures(&tx_cancel, (a.public(), sig_a), (B.clone(), 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?;
|
||||
|
||||
bitcoin_wallet.wait_for_transaction_finality(txid).await;
|
||||
|
||||
Ok(txid)
|
||||
}
|
||||
|
@ -2,35 +2,32 @@
|
||||
//! Alice holds XMR and wishes receive BTC.
|
||||
use crate::{
|
||||
alice::{
|
||||
execution::{lock_xmr, negotiate},
|
||||
OutEvent, Swarm,
|
||||
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, TX_LOCK_MINE_TIMEOUT},
|
||||
bitcoin::EncryptedSignature,
|
||||
monero,
|
||||
network::request_response::AliceToBob,
|
||||
SwapAmounts,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::Result;
|
||||
use async_recursion::async_recursion;
|
||||
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
|
||||
use futures::{
|
||||
future::{select, Either},
|
||||
pin_mut,
|
||||
};
|
||||
use libp2p::request_response::ResponseChannel;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use sha2::Sha256;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use std::sync::Arc;
|
||||
use xmr_btc::{
|
||||
alice::State3,
|
||||
bitcoin::{
|
||||
poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction,
|
||||
TransactionBlockHeight, TxCancel, TxRefund, WaitForTransactionFinality,
|
||||
WatchForRawTransaction,
|
||||
},
|
||||
bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction},
|
||||
cross_curve_dleq,
|
||||
monero::CreateWalletForOutput,
|
||||
};
|
||||
@ -79,11 +76,6 @@ pub enum AliceState {
|
||||
tx_refund: TxRefund,
|
||||
state3: State3,
|
||||
},
|
||||
BtcPunished {
|
||||
tx_refund: TxRefund,
|
||||
punished_tx_id: bitcoin::Txid,
|
||||
state3: State3,
|
||||
},
|
||||
XmrRefunded,
|
||||
WaitingToCancel {
|
||||
state3: State3,
|
||||
@ -107,7 +99,7 @@ pub async fn swap(
|
||||
s_a,
|
||||
v_a,
|
||||
} => {
|
||||
let (channel, amounts, state3) =
|
||||
let (channel, state3) =
|
||||
negotiate(amounts, a, s_a, v_a, &mut swarm, bitcoin_wallet.clone()).await?;
|
||||
|
||||
swap(
|
||||
@ -127,12 +119,7 @@ pub async fn swap(
|
||||
channel,
|
||||
amounts,
|
||||
} => {
|
||||
timeout(
|
||||
Duration::from_secs(TX_LOCK_MINE_TIMEOUT),
|
||||
bitcoin_wallet.wait_for_transaction_finality(state3.tx_lock.txid()),
|
||||
)
|
||||
.await
|
||||
.context("Timed out, Bob did not lock Bitcoin in time")?;
|
||||
let _ = wait_for_locked_bitcoin(state3.tx_lock.txid(), bitcoin_wallet.clone()).await?;
|
||||
|
||||
swap(
|
||||
AliceState::BtcLocked {
|
||||
@ -169,48 +156,10 @@ pub async fn swap(
|
||||
.await
|
||||
}
|
||||
AliceState::XmrLocked { state3 } => {
|
||||
let encsig = timeout(
|
||||
// Give a set arbitrary time to Bob to send us `tx_redeem_encsign`
|
||||
Duration::from_secs(TX_LOCK_MINE_TIMEOUT),
|
||||
async {
|
||||
match swarm.next().await {
|
||||
OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig),
|
||||
other => Err(anyhow!(
|
||||
"Expected Bob's Bitcoin redeem encsig, got: {:?}",
|
||||
other
|
||||
)),
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Timed out, Bob did not send redeem encsign in time");
|
||||
|
||||
match encsig {
|
||||
Err(_timeout_error) => {
|
||||
// TODO(Franck): Insert in DB
|
||||
|
||||
swap(
|
||||
AliceState::WaitingToCancel { state3 },
|
||||
swarm,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Ok(Err(_unexpected_msg_error)) => {
|
||||
// TODO(Franck): Insert in DB
|
||||
|
||||
swap(
|
||||
AliceState::WaitingToCancel { state3 },
|
||||
swarm,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Ok(Ok(encrypted_signature)) => {
|
||||
// TODO(Franck): Insert in DB
|
||||
|
||||
// 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,
|
||||
@ -222,48 +171,45 @@ pub async fn swap(
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(_) => {
|
||||
swap(
|
||||
AliceState::WaitingToCancel { state3 },
|
||||
swarm,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
AliceState::EncSignLearned {
|
||||
state3,
|
||||
encrypted_signature,
|
||||
} => {
|
||||
let (signed_tx_redeem, _tx_redeem_txid) = {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
let tx_redeem = bitcoin::TxRedeem::new(&state3.tx_lock, &state3.redeem_address);
|
||||
|
||||
bitcoin::verify_encsig(
|
||||
state3.B.clone(),
|
||||
state3.s_a.into_secp256k1().into(),
|
||||
&tx_redeem.digest(),
|
||||
&encrypted_signature,
|
||||
)
|
||||
.context("Invalid encrypted signature received")?;
|
||||
|
||||
let sig_a = state3.a.sign(tx_redeem.digest());
|
||||
let sig_b = adaptor
|
||||
.decrypt_signature(&state3.s_a.into_secp256k1(), encrypted_signature.clone());
|
||||
|
||||
let tx = tx_redeem
|
||||
.add_signatures(
|
||||
&state3.tx_lock,
|
||||
(state3.a.public(), sig_a),
|
||||
(state3.B.clone(), sig_b),
|
||||
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,
|
||||
)
|
||||
.expect("sig_{a,b} to be valid signatures for tx_redeem");
|
||||
let txid = tx.txid();
|
||||
|
||||
(tx, txid)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(Franck): Insert in db
|
||||
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_redeem)
|
||||
.await?;
|
||||
|
||||
// TODO(Franck) Wait for confirmations
|
||||
// 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,
|
||||
@ -274,39 +220,15 @@ pub async fn swap(
|
||||
.await
|
||||
}
|
||||
AliceState::WaitingToCancel { state3 } => {
|
||||
let tx_lock_height = bitcoin_wallet
|
||||
.transaction_block_height(state3.tx_lock.txid())
|
||||
.await;
|
||||
poll_until_block_height_is_gte(
|
||||
bitcoin_wallet.as_ref(),
|
||||
tx_lock_height + state3.refund_timelock,
|
||||
)
|
||||
.await;
|
||||
|
||||
let tx_cancel = bitcoin::TxCancel::new(
|
||||
&state3.tx_lock,
|
||||
state3.refund_timelock,
|
||||
state3.a.public(),
|
||||
let tx_cancel = publish_cancel_transaction(
|
||||
state3.tx_lock.clone(),
|
||||
state3.a.clone(),
|
||||
state3.B.clone(),
|
||||
);
|
||||
|
||||
if let Err(_e) = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await {
|
||||
let sig_a = state3.a.sign(tx_cancel.digest());
|
||||
let sig_b = state3.tx_cancel_sig_bob.clone();
|
||||
|
||||
let tx_cancel = tx_cancel
|
||||
.clone()
|
||||
.add_signatures(
|
||||
&state3.tx_lock,
|
||||
(state3.a.public(), sig_a),
|
||||
(state3.B.clone(), sig_b),
|
||||
)
|
||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||
|
||||
bitcoin_wallet
|
||||
.broadcast_signed_transaction(tx_cancel)
|
||||
.await?;
|
||||
}
|
||||
state3.refund_timelock,
|
||||
state3.tx_cancel_sig_bob.clone(),
|
||||
bitcoin_wallet.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
swap(
|
||||
AliceState::BtcCancelled { state3, tx_cancel },
|
||||
@ -321,19 +243,18 @@ pub async fn swap(
|
||||
.transaction_block_height(tx_cancel.txid())
|
||||
.await;
|
||||
|
||||
let reached_t2 = poll_until_block_height_is_gte(
|
||||
bitcoin_wallet.as_ref(),
|
||||
tx_cancel_height + state3.punish_timelock,
|
||||
);
|
||||
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?;
|
||||
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state3.refund_address);
|
||||
let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
|
||||
|
||||
pin_mut!(reached_t2);
|
||||
pin_mut!(seen_refund_tx);
|
||||
|
||||
match select(reached_t2, seen_refund_tx).await {
|
||||
Either::Left(_) => {
|
||||
// TODO(Franck): Review error handling
|
||||
match published_refund_tx {
|
||||
None => {
|
||||
swap(
|
||||
AliceState::BtcPunishable { tx_refund, state3 },
|
||||
swarm,
|
||||
@ -342,7 +263,7 @@ pub async fn swap(
|
||||
)
|
||||
.await
|
||||
}
|
||||
Either::Right((published_refund_tx, _)) => {
|
||||
Some(published_refund_tx) => {
|
||||
swap(
|
||||
AliceState::BtcRefunded {
|
||||
tx_refund,
|
||||
@ -362,22 +283,13 @@ pub async fn swap(
|
||||
published_refund_tx,
|
||||
state3,
|
||||
} => {
|
||||
let s_a = monero::PrivateKey {
|
||||
scalar: state3.s_a.into_ed25519(),
|
||||
};
|
||||
|
||||
let tx_refund_sig = tx_refund
|
||||
.extract_signature_by_key(published_refund_tx, state3.a.public())
|
||||
.context("Failed to extract signature from Bitcoin refund tx")?;
|
||||
let tx_refund_encsig = state3
|
||||
.a
|
||||
.encsign(state3.S_b_bitcoin.clone(), tx_refund.digest());
|
||||
|
||||
let s_b = bitcoin::recover(state3.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;
|
||||
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
|
||||
@ -387,49 +299,18 @@ pub async fn swap(
|
||||
Ok(AliceState::XmrRefunded)
|
||||
}
|
||||
AliceState::BtcPunishable { tx_refund, state3 } => {
|
||||
let tx_cancel = bitcoin::TxCancel::new(
|
||||
let signed_tx_punish = build_bitcoin_punish_transaction(
|
||||
&state3.tx_lock,
|
||||
state3.refund_timelock,
|
||||
state3.a.public(),
|
||||
&state3.punish_address,
|
||||
state3.punish_timelock,
|
||||
state3.tx_punish_sig_bob.clone(),
|
||||
state3.a.clone(),
|
||||
state3.B.clone(),
|
||||
);
|
||||
let tx_punish =
|
||||
bitcoin::TxPunish::new(&tx_cancel, &state3.punish_address, state3.punish_timelock);
|
||||
let punished_tx_id = tx_punish.txid();
|
||||
)?;
|
||||
|
||||
let sig_a = state3.a.sign(tx_punish.digest());
|
||||
let sig_b = state3.tx_punish_sig_bob.clone();
|
||||
|
||||
let signed_tx_punish = tx_punish
|
||||
.add_signatures(
|
||||
&tx_cancel,
|
||||
(state3.a.public(), sig_a),
|
||||
(state3.B.clone(), sig_b),
|
||||
)
|
||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_punish)
|
||||
.await?;
|
||||
|
||||
swap(
|
||||
AliceState::BtcPunished {
|
||||
tx_refund,
|
||||
punished_tx_id,
|
||||
state3,
|
||||
},
|
||||
swarm,
|
||||
bitcoin_wallet.clone(),
|
||||
monero_wallet,
|
||||
)
|
||||
.await
|
||||
}
|
||||
AliceState::BtcPunished {
|
||||
punished_tx_id,
|
||||
tx_refund,
|
||||
state3,
|
||||
} => {
|
||||
let punish_tx_finalised = bitcoin_wallet.wait_for_transaction_finality(punished_tx_id);
|
||||
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());
|
||||
|
||||
@ -461,7 +342,6 @@ pub async fn swap(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AliceState::XmrRefunded => Ok(AliceState::XmrRefunded),
|
||||
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed),
|
||||
AliceState::Punished => Ok(AliceState::Punished),
|
||||
|
@ -3,15 +3,15 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||
use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Transaction};
|
||||
use bitcoin::util::psbt::PartiallySignedTransaction;
|
||||
use bitcoin_harness::bitcoind_rpc::PsbtBase64;
|
||||
use reqwest::Url;
|
||||
use tokio::time;
|
||||
use xmr_btc::bitcoin::{
|
||||
BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight,
|
||||
WatchForRawTransaction,
|
||||
};
|
||||
|
||||
pub use ::bitcoin::{Address, Transaction};
|
||||
pub use xmr_btc::bitcoin::*;
|
||||
|
||||
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
|
||||
@ -85,15 +85,7 @@ impl SignTxLock for Wallet {
|
||||
#[async_trait]
|
||||
impl BroadcastSignedTransaction for Wallet {
|
||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
|
||||
let txid = self.0.send_raw_transaction(transaction).await?;
|
||||
|
||||
// TODO: Instead of guessing how long it will take for the transaction to be
|
||||
// mined we should ask bitcoind for the number of confirmations on `txid`
|
||||
|
||||
// give time for transaction to be mined
|
||||
time::delay_for(Duration::from_millis(1100)).await;
|
||||
|
||||
Ok(txid)
|
||||
Ok(self.0.send_raw_transaction(transaction).await?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
use crate::{
|
||||
bob::{OutEvent, Swarm},
|
||||
Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
|
||||
Cmd, Rsp, SwapAmounts,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::sync::Arc;
|
||||
use tokio::{stream::StreamExt, sync::mpsc};
|
||||
|
||||
use xmr_btc::bob::{State0, State2};
|
||||
use xmr_btc::bob::State2;
|
||||
|
||||
pub async fn negotiate<R>(
|
||||
state0: xmr_btc::bob::State0,
|
||||
|
@ -45,7 +45,7 @@ pub async fn alice_recover(
|
||||
state: Alice,
|
||||
) -> Result<()> {
|
||||
match state {
|
||||
Alice::Handshaken(_) | Alice::BtcLocked(_) | Alice::SwapComplete => {
|
||||
Alice::Negotiated(_) | Alice::BtcLocked(_) | Alice::SwapComplete => {
|
||||
info!("Nothing to do");
|
||||
}
|
||||
Alice::XmrLocked(state) => {
|
||||
|
@ -12,7 +12,7 @@ pub enum Swap {
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub enum Alice {
|
||||
Handshaken(alice::State3),
|
||||
Negotiated(alice::State3),
|
||||
BtcLocked(alice::State3),
|
||||
XmrLocked(alice::State3),
|
||||
BtcRedeemable {
|
||||
@ -63,7 +63,7 @@ impl Display for Swap {
|
||||
impl Display for Alice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Alice::Handshaken(_) => f.write_str("Handshake complete"),
|
||||
Alice::Negotiated(_) => f.write_str("Handshake complete"),
|
||||
Alice::BtcLocked(_) => f.write_str("Bitcoin locked"),
|
||||
Alice::XmrLocked(_) => f.write_str("Monero locked"),
|
||||
Alice::BtcRedeemable { .. } => f.write_str("Bitcoin redeemable"),
|
||||
|
@ -1,11 +1,5 @@
|
||||
use bitcoin_harness::Bitcoind;
|
||||
use futures::{
|
||||
channel::{
|
||||
mpsc,
|
||||
mpsc::{Receiver, Sender},
|
||||
},
|
||||
future::try_join,
|
||||
};
|
||||
use futures::{channel::mpsc, future::try_join};
|
||||
use libp2p::Multiaddr;
|
||||
use monero_harness::Monero;
|
||||
use rand::rngs::OsRng;
|
||||
|
@ -160,15 +160,23 @@ pub async fn init_test(
|
||||
let punish_address = redeem_address.clone();
|
||||
let refund_address = bob.bitcoin_wallet.new_address().await.unwrap();
|
||||
|
||||
let alice_state0 = xmr_btc::alice::State0::new(
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK),
|
||||
punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK),
|
||||
redeem_address.clone(),
|
||||
punish_address.clone(),
|
||||
);
|
||||
let alice_state0 = {
|
||||
let a = bitcoin::SecretKey::new_random(&mut OsRng);
|
||||
let s_a = cross_curve_dleq::Scalar::random(&mut OsRng);
|
||||
let v_a = monero::PrivateViewKey::new_random(&mut OsRng);
|
||||
|
||||
xmr_btc::alice::State0::new(
|
||||
a,
|
||||
s_a,
|
||||
v_a,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK),
|
||||
punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK),
|
||||
redeem_address.clone(),
|
||||
punish_address.clone(),
|
||||
)
|
||||
};
|
||||
let bob_state0 = xmr_btc::bob::State0::new(
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
|
Loading…
Reference in New Issue
Block a user