diff --git a/swap/Cargo.toml b/swap/Cargo.toml index bf5fa649..e63aa5b5 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -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 } diff --git a/swap/src/alice.rs b/swap/src/alice.rs index f230cb05..adcfdfc0 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -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; -pub fn new_swarm( - listen: Multiaddr, - transport: SwapTransport, - behaviour: Behaviour, -) -> Result { +fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Behaviour) -> Result { use anyhow::Context as _; let local_peer_id = behaviour.peer_id(); diff --git a/swap/src/alice/execution.rs b/swap/src/alice/execution.rs index e798c94d..3c74d4d9 100644 --- a/swap/src/alice/execution.rs +++ b/swap/src/alice/execution.rs @@ -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 = 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 = + 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, -) -> Result<(ResponseChannel, SwapAmounts, State3)> { - // Bob dials us - match swarm.next().await { + bitcoin_wallet: Arc, +) -> Result<(ResponseChannel, 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 wait_for_locked_bitcoin( + lock_bitcoin_txid: bitcoin::Txid, + bitcoin_wallet: Arc, +) -> 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( +pub async fn lock_xmr( channel: ResponseChannel, amounts: SwapAmounts, state3: State3, swarm: &mut Swarm, - monero_wallet: Arc, -) -> Result<()> { + monero_wallet: Arc, +) -> 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 { + 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 { + let adaptor = Adaptor::>::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( + redeem_tx: bitcoin::Transaction, + bitcoin_wallet: Arc, +) -> 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( + tx_lock: TxLock, + a: bitcoin::SecretKey, + B: bitcoin::PublicKey, + refund_timelock: u32, + tx_cancel_sig_bob: bitcoin::Signature, + bitcoin_wallet: Arc, +) -> Result +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( + tx_cancel: &TxCancel, + cancel_tx_height: u32, + punish_timelock: u32, + refund_address: &bitcoin::Address, + bitcoin_wallet: Arc, +) -> Result<(bitcoin::TxRefund, Option)> +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 { + 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 { + 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( + punish_tx: bitcoin::Transaction, + bitcoin_wallet: Arc, +) -> Result +where + W: BroadcastSignedTransaction + WaitForTransactionFinality, +{ + let txid = bitcoin_wallet + .broadcast_signed_transaction(punish_tx) + .await?; + + bitcoin_wallet.wait_for_transaction_finality(txid).await; + + Ok(txid) +} diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index fa9b5288..e096eb30 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -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,37 +156,22 @@ 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 - + // 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::WaitingToCancel { state3 }, + AliceState::EncSignLearned { + state3, + encrypted_signature, + }, swarm, bitcoin_wallet, monero_wallet, ) .await } - Ok(Err(_unexpected_msg_error)) => { - // TODO(Franck): Insert in DB - + Err(_) => { swap( AliceState::WaitingToCancel { state3 }, swarm, @@ -208,62 +180,36 @@ pub async fn swap( ) .await } - Ok(Ok(encrypted_signature)) => { - // TODO(Franck): Insert in DB - - swap( - AliceState::EncSignLearned { - state3, - encrypted_signature, - }, - swarm, - bitcoin_wallet, - monero_wallet, - ) - .await - } } } AliceState::EncSignLearned { state3, encrypted_signature, } => { - let (signed_tx_redeem, _tx_redeem_txid) = { - let adaptor = Adaptor::>::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 = 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); + 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?; - 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), diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 55ed05b7..8425fb85 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -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 { - 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?) } } diff --git a/swap/src/bob/execution.rs b/swap/src/bob/execution.rs index c63739ea..52f8cda1 100644 --- a/swap/src/bob/execution.rs +++ b/swap/src/bob/execution.rs @@ -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( state0: xmr_btc::bob::State0, diff --git a/swap/src/recover.rs b/swap/src/recover.rs index b71b31fd..5f41112f 100644 --- a/swap/src/recover.rs +++ b/swap/src/recover.rs @@ -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) => { diff --git a/swap/src/state.rs b/swap/src/state.rs index 701c61e0..e59c976d 100644 --- a/swap/src/state.rs +++ b/swap/src/state.rs @@ -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"), diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index 7b318b6c..66672874 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -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; diff --git a/xmr-btc/tests/harness/mod.rs b/xmr-btc/tests/harness/mod.rs index 3d789b8f..c1efa323 100644 --- a/xmr-btc/tests/harness/mod.rs +++ b/xmr-btc/tests/harness/mod.rs @@ -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,