From c539465925e1ac87ec236adbe252b72ce441dc8d Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 24 Mar 2021 18:19:58 +1100 Subject: [PATCH 1/7] Make it possible to create random public keys This is useful for tests. --- swap/src/bitcoin.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 7577633d..fef2e252 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -105,6 +105,13 @@ impl SecretKey { #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] pub struct PublicKey(Point); +impl PublicKey { + #[cfg(test)] + pub fn random() -> Self { + Self(Point::random(&mut rand::thread_rng())) + } +} + impl From for Point { fn from(from: PublicKey) -> Self { from.0 From 7f5715e147dcf668bb442f1b4ba4a2a30ff6b4cc Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 24 Mar 2021 16:31:49 +1100 Subject: [PATCH 2/7] Remove unnecessary serde implementations --- swap/src/protocol/alice/state.rs | 9 +++------ swap/src/protocol/bob/state.rs | 5 ++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index ae9e58dd..e16fd8e1 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -74,7 +74,7 @@ impl fmt::Display for AliceState { } } -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct State0 { pub a: bitcoin::SecretKey, pub s_a: monero::Scalar, @@ -82,7 +82,6 @@ pub struct State0 { pub(crate) S_a_monero: monero::PublicKey, pub(crate) S_a_bitcoin: bitcoin::PublicKey, pub dleq_proof_s_a: CrossCurveDLEQProof, - #[serde(with = "::bitcoin::util::amount::serde::as_sat")] pub btc: bitcoin::Amount, pub xmr: monero::Amount, pub cancel_timelock: CancelTimelock, @@ -168,7 +167,7 @@ impl State0 { } } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug)] pub struct State1 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, @@ -180,7 +179,6 @@ pub struct State1 { v: monero::PrivateViewKey, v_a: monero::PrivateViewKey, dleq_proof_s_a: CrossCurveDLEQProof, - #[serde(with = "::bitcoin::util::amount::serde::as_sat")] btc: bitcoin::Amount, xmr: monero::Amount, cancel_timelock: CancelTimelock, @@ -223,7 +221,7 @@ impl State1 { } } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug)] pub struct State2 { a: bitcoin::SecretKey, B: bitcoin::PublicKey, @@ -231,7 +229,6 @@ pub struct State2 { S_b_monero: monero::PublicKey, S_b_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, - #[serde(with = "::bitcoin::util::amount::serde::as_sat")] btc: bitcoin::Amount, xmr: monero::Amount, cancel_timelock: CancelTimelock, diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index dd3a3bea..2b49399c 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -69,7 +69,7 @@ impl fmt::Display for BobState { } } -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct State0 { b: bitcoin::SecretKey, s_b: monero::Scalar, @@ -77,7 +77,6 @@ pub struct State0 { S_b_bitcoin: bitcoin::PublicKey, v_b: monero::PrivateViewKey, dleq_proof_s_b: CrossCurveDLEQProof, - #[serde(with = "::bitcoin::util::amount::serde::as_sat")] btc: bitcoin::Amount, xmr: monero::Amount, cancel_timelock: CancelTimelock, @@ -170,7 +169,7 @@ impl State0 { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug)] pub struct State1 { A: bitcoin::PublicKey, b: bitcoin::SecretKey, From e13044820059b948bef2c80d3cebb1ceb2142b7b Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 24 Mar 2021 16:48:05 +1100 Subject: [PATCH 3/7] Make as many fields of Alice's states private as possible To achieve this, we need to add some pure helpers to the state structs. This has the added benefit that we can reduce the amount of code within the swap function. --- swap/src/protocol/alice/state.rs | 92 ++++++++++++++++++++++---------- swap/src/protocol/alice/swap.rs | 46 ++++------------ 2 files changed, 74 insertions(+), 64 deletions(-) diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index e16fd8e1..6507e33d 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -4,6 +4,7 @@ use crate::bitcoin::{ use crate::env::Config; use crate::monero::wallet::{TransferRequest, WatchRequest}; use crate::monero::TransferProof; +use crate::monero_ext::ScalarExt; use crate::protocol::alice::{Message1, Message3}; use crate::protocol::bob::{Message0, Message2, Message4}; use crate::protocol::CROSS_CURVE_PROOF_SYSTEM; @@ -76,18 +77,18 @@ impl fmt::Display for AliceState { #[derive(Clone, Debug, PartialEq)] pub struct State0 { - pub a: bitcoin::SecretKey, - pub s_a: monero::Scalar, - pub v_a: monero::PrivateViewKey, - pub(crate) S_a_monero: monero::PublicKey, - pub(crate) S_a_bitcoin: bitcoin::PublicKey, - pub dleq_proof_s_a: CrossCurveDLEQProof, - pub btc: bitcoin::Amount, - pub xmr: monero::Amount, - pub cancel_timelock: CancelTimelock, - pub punish_timelock: PunishTimelock, - pub redeem_address: bitcoin::Address, - pub punish_address: bitcoin::Address, + a: bitcoin::SecretKey, + s_a: monero::Scalar, + v_a: monero::PrivateViewKey, + S_a_monero: monero::PublicKey, + S_a_bitcoin: bitcoin::PublicKey, + dleq_proof_s_a: CrossCurveDLEQProof, + btc: bitcoin::Amount, + xmr: monero::Amount, + cancel_timelock: CancelTimelock, + punish_timelock: PunishTimelock, + redeem_address: bitcoin::Address, + punish_address: bitcoin::Address, } impl State0 { @@ -292,23 +293,23 @@ impl State2 { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct State3 { - pub a: bitcoin::SecretKey, - pub B: bitcoin::PublicKey, - pub s_a: monero::Scalar, - pub S_b_monero: monero::PublicKey, - pub S_b_bitcoin: bitcoin::PublicKey, + a: bitcoin::SecretKey, + B: bitcoin::PublicKey, + s_a: monero::Scalar, + S_b_monero: monero::PublicKey, + S_b_bitcoin: bitcoin::PublicKey, pub v: monero::PrivateViewKey, #[serde(with = "::bitcoin::util::amount::serde::as_sat")] - pub btc: bitcoin::Amount, - pub xmr: monero::Amount, + btc: bitcoin::Amount, + xmr: monero::Amount, pub cancel_timelock: CancelTimelock, pub punish_timelock: PunishTimelock, - pub refund_address: bitcoin::Address, - pub redeem_address: bitcoin::Address, - pub punish_address: bitcoin::Address, + refund_address: bitcoin::Address, + redeem_address: bitcoin::Address, + punish_address: bitcoin::Address, pub tx_lock: bitcoin::TxLock, - pub tx_punish_sig_bob: bitcoin::Signature, - pub tx_cancel_sig_bob: bitcoin::Signature, + tx_punish_sig_bob: bitcoin::Signature, + tx_cancel_sig_bob: bitcoin::Signature, } impl State3 { @@ -364,15 +365,48 @@ impl State3 { TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B) } - pub fn tx_punish(&self) -> TxPunish { + pub fn tx_refund(&self) -> TxRefund { + bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address) + } + + pub fn extract_monero_private_key( + &self, + published_refund_tx: bitcoin::Transaction, + ) -> Result { + self.tx_refund().extract_monero_private_key( + published_refund_tx, + self.s_a, + self.a.clone(), + self.S_b_bitcoin, + ) + } + + pub fn signed_redeem_transaction( + &self, + sig: bitcoin::EncryptedSignature, + ) -> Result { + bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address) + .complete(sig, self.a.clone(), self.s_a.to_secpfun_scalar(), self.B) + .context("Failed to complete Bitcoin redeem transaction") + } + + pub fn signed_cancel_transaction(&self) -> Result { + self.tx_cancel() + .complete_as_alice(self.a.clone(), self.B, self.tx_cancel_sig_bob.clone()) + .context("Failed to complete Bitcoin cancel transaction") + } + + pub fn signed_punish_transaction(&self) -> Result { + self.tx_punish() + .complete(self.tx_punish_sig_bob.clone(), self.a.clone(), self.B) + .context("Failed to complete Bitcoin punish transaction") + } + + fn tx_punish(&self) -> TxPunish { bitcoin::TxPunish::new( &self.tx_cancel(), &self.punish_address, self.punish_timelock, ) } - - pub fn tx_refund(&self) -> TxRefund { - bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address) - } } diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 6806f668..dc732bf7 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -1,8 +1,7 @@ //! Run an XMR/BTC swap in the role of Alice. //! Alice holds XMR and wishes receive BTC. -use crate::bitcoin::{ExpiredTimelocks, TxRedeem}; +use crate::bitcoin::ExpiredTimelocks; use crate::env::Config; -use crate::monero_ext::ScalarExt; use crate::protocol::alice; use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::AliceState; @@ -158,12 +157,7 @@ async fn next_state( } => match state3.expired_timelocks(bitcoin_wallet).await? { ExpiredTimelocks::None => { let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; - match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete( - *encrypted_signature, - state3.a.clone(), - state3.s_a.to_secpfun_scalar(), - state3.B, - ) { + match state3.signed_redeem_transaction(*encrypted_signature) { Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { Ok((_, subscription)) => match subscription.wait_until_final().await { Ok(_) => AliceState::BtcRedeemed, @@ -205,18 +199,14 @@ async fn next_state( state3, monero_wallet_restore_blockheight, } => { - let tx_cancel = state3.tx_cancel(); + let transaction = state3.signed_cancel_transaction()?; // If Bob hasn't yet broadcasted the tx cancel, we do it if bitcoin_wallet - .get_raw_transaction(tx_cancel.txid()) + .get_raw_transaction(transaction.txid()) .await .is_err() { - let transaction = tx_cancel - .complete_as_alice(state3.a.clone(), state3.B, state3.tx_cancel_sig_bob.clone()) - .context("Failed to complete Bitcoin cancel transaction")?; - if let Err(e) = bitcoin_wallet.broadcast(transaction, "cancel").await { tracing::debug!( "Assuming transaction is already broadcasted because: {:#}", @@ -243,14 +233,9 @@ async fn next_state( select! { seen_refund = tx_refund_status.wait_until_seen() => { seen_refund.context("Failed to monitor refund transaction")?; - let published_refund_tx = bitcoin_wallet.get_raw_transaction(state3.tx_refund().txid()).await?; - let spend_key = state3.tx_refund().extract_monero_private_key( - published_refund_tx, - state3.s_a, - state3.a.clone(), - state3.S_b_bitcoin, - )?; + let published_refund_tx = bitcoin_wallet.get_raw_transaction(state3.tx_refund().txid()).await?; + let spend_key = state3.extract_monero_private_key(published_refund_tx)?; AliceState::BtcRefunded { spend_key, @@ -283,11 +268,7 @@ async fn next_state( state3, monero_wallet_restore_blockheight, } => { - let signed_tx_punish = state3.tx_punish().complete( - state3.tx_punish_sig_bob.clone(), - state3.a.clone(), - state3.B, - )?; + let signed_tx_punish = state3.signed_punish_transaction()?; let punish = async { let (txid, subscription) = @@ -313,16 +294,11 @@ async fn next_state( // because a punish tx failure is not recoverable (besides re-trying) if the // refund tx was not included. - let tx_refund = state3.tx_refund(); - let published_refund_tx = - bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?; + let published_refund_tx = bitcoin_wallet + .get_raw_transaction(state3.tx_refund().txid()) + .await?; - let spend_key = tx_refund.extract_monero_private_key( - published_refund_tx, - state3.s_a, - state3.a.clone(), - state3.S_b_bitcoin, - )?; + let spend_key = state3.extract_monero_private_key(published_refund_tx)?; AliceState::BtcRefunded { spend_key, From 11b45cd8c006fdbd7421d7515865672155ce0744 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 24 Mar 2021 16:55:00 +1100 Subject: [PATCH 4/7] Move messages into `protocol` module This allows us to remove all visibility modifiers from the message fields because child modules (in this case {alice,bob}::state) can always access private fields of structs. It also moves the messages into a more natural place. Previously, they were defined within the network layer even though they are independent of the libp2p implementation. --- swap/src/protocol.rs | 44 +++++++++++++++++++++- swap/src/protocol/alice.rs | 2 - swap/src/protocol/alice/execution_setup.rs | 23 +---------- swap/src/protocol/alice/state.rs | 4 +- swap/src/protocol/bob.rs | 1 - swap/src/protocol/bob/execution_setup.rs | 26 +------------ swap/src/protocol/bob/state.rs | 4 +- 7 files changed, 46 insertions(+), 58 deletions(-) diff --git a/swap/src/protocol.rs b/swap/src/protocol.rs index faab1610..e446759c 100644 --- a/swap/src/protocol.rs +++ b/swap/src/protocol.rs @@ -1,7 +1,9 @@ +use crate::{bitcoin, monero}; use conquer_once::Lazy; use ecdsa_fun::fun::marker::Mark; +use serde::{Deserialize, Serialize}; use sha2::Sha256; -use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQ; +use sigma_fun::ext::dl_secp256k1_ed25519_eq::{CrossCurveDLEQ, CrossCurveDLEQProof}; use sigma_fun::HashTranscript; pub mod alice; @@ -18,6 +20,44 @@ pub static CROSS_CURVE_PROOF_SYSTEM: Lazy< #[derive(Debug, Copy, Clone)] pub struct StartingBalances { - pub xmr: crate::monero::Amount, + pub xmr: monero::Amount, pub btc: bitcoin::Amount, } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message0 { + B: bitcoin::PublicKey, + S_b_monero: monero::PublicKey, + S_b_bitcoin: bitcoin::PublicKey, + dleq_proof_s_b: CrossCurveDLEQProof, + v_b: monero::PrivateViewKey, + refund_address: bitcoin::Address, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message1 { + A: bitcoin::PublicKey, + S_a_monero: monero::PublicKey, + S_a_bitcoin: bitcoin::PublicKey, + dleq_proof_s_a: CrossCurveDLEQProof, + v_a: monero::PrivateViewKey, + redeem_address: bitcoin::Address, + punish_address: bitcoin::Address, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message2 { + tx_lock: bitcoin::TxLock, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message3 { + tx_cancel_sig: bitcoin::Signature, + tx_refund_encsig: bitcoin::EncryptedSignature, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message4 { + tx_punish_sig: bitcoin::Signature, + tx_cancel_sig: bitcoin::Signature, +} diff --git a/swap/src/protocol/alice.rs b/swap/src/protocol/alice.rs index 9bc6423d..5909a701 100644 --- a/swap/src/protocol/alice.rs +++ b/swap/src/protocol/alice.rs @@ -8,10 +8,8 @@ use uuid::Uuid; pub use self::behaviour::{Behaviour, OutEvent}; pub use self::event_loop::{EventLoop, EventLoopHandle}; -pub use self::execution_setup::Message1; pub use self::state::*; pub use self::swap::{run, run_until}; -pub use execution_setup::Message3; mod behaviour; pub mod event_loop; diff --git a/swap/src/protocol/alice/execution_setup.rs b/swap/src/protocol/alice/execution_setup.rs index 965b6bd6..d3a37519 100644 --- a/swap/src/protocol/alice/execution_setup.rs +++ b/swap/src/protocol/alice/execution_setup.rs @@ -1,30 +1,9 @@ -use crate::bitcoin::{EncryptedSignature, Signature}; use crate::network::cbor_request_response::BUF_SIZE; use crate::protocol::alice::{State0, State3}; -use crate::protocol::bob::{Message0, Message2, Message4}; -use crate::{bitcoin, monero}; +use crate::protocol::{Message0, Message2, Message4}; use anyhow::{Context, Error}; use libp2p::PeerId; use libp2p_async_await::BehaviourOutEvent; -use serde::{Deserialize, Serialize}; -use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message1 { - pub(crate) A: bitcoin::PublicKey, - pub(crate) S_a_monero: monero::PublicKey, - pub(crate) S_a_bitcoin: bitcoin::PublicKey, - pub(crate) dleq_proof_s_a: CrossCurveDLEQProof, - pub(crate) v_a: monero::PrivateViewKey, - pub(crate) redeem_address: bitcoin::Address, - pub(crate) punish_address: bitcoin::Address, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message3 { - pub(crate) tx_cancel_sig: Signature, - pub(crate) tx_refund_encsig: EncryptedSignature, -} #[derive(Debug)] pub enum OutEvent { diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index 6507e33d..ecaee48f 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -5,9 +5,7 @@ use crate::env::Config; use crate::monero::wallet::{TransferRequest, WatchRequest}; use crate::monero::TransferProof; use crate::monero_ext::ScalarExt; -use crate::protocol::alice::{Message1, Message3}; -use crate::protocol::bob::{Message0, Message2, Message4}; -use crate::protocol::CROSS_CURVE_PROOF_SYSTEM; +use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM}; use crate::{bitcoin, monero}; use anyhow::{anyhow, bail, Context, Result}; use monero_rpc::wallet::BlockHeight; diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index 604d879a..2f3b3dd3 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -4,7 +4,6 @@ use crate::network::{encrypted_signature, spot_price}; use crate::protocol::bob; use crate::{bitcoin, monero}; use anyhow::{anyhow, Error, Result}; -pub use execution_setup::{Message0, Message2, Message4}; use libp2p::core::Multiaddr; use libp2p::request_response::{RequestResponseEvent, RequestResponseMessage, ResponseChannel}; use libp2p::{NetworkBehaviour, PeerId}; diff --git a/swap/src/protocol/bob/execution_setup.rs b/swap/src/protocol/bob/execution_setup.rs index 6fa7491e..1c53bf22 100644 --- a/swap/src/protocol/bob/execution_setup.rs +++ b/swap/src/protocol/bob/execution_setup.rs @@ -1,35 +1,11 @@ -use crate::bitcoin::Signature; use crate::network::cbor_request_response::BUF_SIZE; -use crate::protocol::alice::{Message1, Message3}; use crate::protocol::bob::{State0, State2}; +use crate::protocol::{Message1, Message3}; use anyhow::{Context, Error, Result}; use libp2p::PeerId; use libp2p_async_await::BehaviourOutEvent; -use serde::{Deserialize, Serialize}; -use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof; use std::sync::Arc; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message0 { - pub(crate) B: crate::bitcoin::PublicKey, - pub(crate) S_b_monero: monero::PublicKey, - pub(crate) S_b_bitcoin: crate::bitcoin::PublicKey, - pub(crate) dleq_proof_s_b: CrossCurveDLEQProof, - pub(crate) v_b: crate::monero::PrivateViewKey, - pub(crate) refund_address: bitcoin::Address, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message2 { - pub(crate) tx_lock: crate::bitcoin::TxLock, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message4 { - pub(crate) tx_punish_sig: Signature, - pub(crate) tx_cancel_sig: Signature, -} - #[derive(Debug)] pub enum OutEvent { Done(Result), diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 2b49399c..c70b458e 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -6,9 +6,7 @@ use crate::monero; use crate::monero::wallet::WatchRequest; use crate::monero::{monero_private_key, TransferProof}; use crate::monero_ext::ScalarExt; -use crate::protocol::alice::{Message1, Message3}; -use crate::protocol::bob::{Message0, Message2, Message4}; -use crate::protocol::CROSS_CURVE_PROOF_SYSTEM; +use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM}; use anyhow::{anyhow, bail, Context, Result}; use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; use ecdsa_fun::nonce::Deterministic; From b9d8cbeaa264bcc7fb61c7f077921a45fc5b6bc4 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 24 Mar 2021 17:49:19 +1100 Subject: [PATCH 5/7] Rename testutils to harness This allows us to bring in a dependency named `testutils`. --- .../bob_refunds_using_cancel_and_refund_command.rs | 8 ++++---- ...g_cancel_and_refund_command_timelock_not_expired.rs | 8 ++++---- ...el_and_refund_command_timelock_not_expired_force.rs | 8 ++++---- swap/tests/happy_path.rs | 6 +++--- swap/tests/happy_path_restart_bob_after_xmr_locked.rs | 8 ++++---- swap/tests/happy_path_restart_bob_before_xmr_locked.rs | 8 ++++---- swap/tests/{testutils => harness}/bitcoind.rs | 0 swap/tests/{testutils => harness}/electrs.rs | 2 +- swap/tests/{testutils => harness}/mod.rs | 10 +++++----- swap/tests/punish.rs | 8 ++++---- 10 files changed, 33 insertions(+), 33 deletions(-) rename swap/tests/{testutils => harness}/bitcoind.rs (100%) rename swap/tests/{testutils => harness}/electrs.rs (99%) rename swap/tests/{testutils => harness}/mod.rs (99%) diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs index e04eab58..f9a7a41e 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command.rs @@ -1,13 +1,13 @@ -pub mod testutils; +pub mod harness; +use harness::bob_run_until::is_btc_locked; +use harness::FastCancelConfig; use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; -use testutils::bob_run_until::is_btc_locked; -use testutils::FastCancelConfig; #[tokio::test] async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() { - testutils::setup_test(FastCancelConfig, |mut ctx| async move { + harness::setup_test(FastCancelConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs index 8edf705d..13793009 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired.rs @@ -1,14 +1,14 @@ -pub mod testutils; +pub mod harness; use bob::cancel::Error; +use harness::bob_run_until::is_btc_locked; +use harness::SlowCancelConfig; use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; -use testutils::bob_run_until::is_btc_locked; -use testutils::SlowCancelConfig; #[tokio::test] async fn given_bob_manually_cancels_when_timelock_not_expired_errors() { - testutils::setup_test(SlowCancelConfig, |mut ctx| async move { + harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); diff --git a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs index ff46d683..8a9d7f66 100644 --- a/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs +++ b/swap/tests/bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force.rs @@ -1,13 +1,13 @@ -pub mod testutils; +pub mod harness; +use harness::bob_run_until::is_btc_locked; +use harness::SlowCancelConfig; use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; -use testutils::bob_run_until::is_btc_locked; -use testutils::SlowCancelConfig; #[tokio::test] async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() { - testutils::setup_test(SlowCancelConfig, |mut ctx| async move { + harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); diff --git a/swap/tests/happy_path.rs b/swap/tests/happy_path.rs index fb665e73..5f1ac5b3 100644 --- a/swap/tests/happy_path.rs +++ b/swap/tests/happy_path.rs @@ -1,14 +1,14 @@ -pub mod testutils; +pub mod harness; +use harness::SlowCancelConfig; use swap::protocol::{alice, bob}; -use testutils::SlowCancelConfig; use tokio::join; /// Run the following tests with RUST_MIN_STACK=10000000 #[tokio::test] async fn happy_path() { - testutils::setup_test(SlowCancelConfig, |mut ctx| async move { + harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap, _) = ctx.bob_swap().await; let bob_swap = tokio::spawn(bob::run(bob_swap)); diff --git a/swap/tests/happy_path_restart_bob_after_xmr_locked.rs b/swap/tests/happy_path_restart_bob_after_xmr_locked.rs index 6abba569..9967c117 100644 --- a/swap/tests/happy_path_restart_bob_after_xmr_locked.rs +++ b/swap/tests/happy_path_restart_bob_after_xmr_locked.rs @@ -1,13 +1,13 @@ -pub mod testutils; +pub mod harness; +use harness::bob_run_until::is_xmr_locked; +use harness::SlowCancelConfig; use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; -use testutils::bob_run_until::is_xmr_locked; -use testutils::SlowCancelConfig; #[tokio::test] async fn given_bob_restarts_after_xmr_is_locked_resume_swap() { - testutils::setup_test(SlowCancelConfig, |mut ctx| async move { + harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_xmr_locked)); diff --git a/swap/tests/happy_path_restart_bob_before_xmr_locked.rs b/swap/tests/happy_path_restart_bob_before_xmr_locked.rs index 6abba569..9967c117 100644 --- a/swap/tests/happy_path_restart_bob_before_xmr_locked.rs +++ b/swap/tests/happy_path_restart_bob_before_xmr_locked.rs @@ -1,13 +1,13 @@ -pub mod testutils; +pub mod harness; +use harness::bob_run_until::is_xmr_locked; +use harness::SlowCancelConfig; use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; -use testutils::bob_run_until::is_xmr_locked; -use testutils::SlowCancelConfig; #[tokio::test] async fn given_bob_restarts_after_xmr_is_locked_resume_swap() { - testutils::setup_test(SlowCancelConfig, |mut ctx| async move { + harness::setup_test(SlowCancelConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_xmr_locked)); diff --git a/swap/tests/testutils/bitcoind.rs b/swap/tests/harness/bitcoind.rs similarity index 100% rename from swap/tests/testutils/bitcoind.rs rename to swap/tests/harness/bitcoind.rs diff --git a/swap/tests/testutils/electrs.rs b/swap/tests/harness/electrs.rs similarity index 99% rename from swap/tests/testutils/electrs.rs rename to swap/tests/harness/electrs.rs index c4c09720..46eff15a 100644 --- a/swap/tests/testutils/electrs.rs +++ b/swap/tests/harness/electrs.rs @@ -1,4 +1,4 @@ -use crate::testutils::bitcoind; +use crate::harness::bitcoind; use bitcoin::Network; use std::collections::HashMap; use testcontainers::core::{Container, Docker, WaitForMessage}; diff --git a/swap/tests/testutils/mod.rs b/swap/tests/harness/mod.rs similarity index 99% rename from swap/tests/testutils/mod.rs rename to swap/tests/harness/mod.rs index 7df968ed..8fafe985 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/harness/mod.rs @@ -1,7 +1,7 @@ mod bitcoind; mod electrs; -use crate::testutils; +use crate::harness; use anyhow::{bail, Context, Result}; use async_trait::async_trait; use bitcoin_harness::{BitcoindRpcApi, Client}; @@ -452,7 +452,7 @@ where let env_config = C::get_config(); - let (monero, containers) = testutils::init_containers(&cli).await; + let (monero, containers) = harness::init_containers(&cli).await; let btc_amount = bitcoin::Amount::from_sat(1_000_000); let xmr_amount = monero::Amount::from_monero(btc_amount.as_btc() / FixedRate::RATE).unwrap(); @@ -470,7 +470,7 @@ where let electrs_rpc_port = containers .electrs - .get_host_port(testutils::electrs::RPC_PORT) + .get_host_port(harness::electrs::RPC_PORT) .expect("Could not map electrs rpc port"); let alice_seed = Seed::random().unwrap(); @@ -600,7 +600,7 @@ async fn init_bitcoind_container( let docker = cli.run_with_args(image, run_args); let a = docker - .get_host_port(testutils::bitcoind::RPC_PORT) + .get_host_port(harness::bitcoind::RPC_PORT) .context("Could not map bitcoind rpc port")?; let bitcoind_url = { @@ -627,7 +627,7 @@ pub async fn init_electrs_container( let bitcoind_rpc_addr = format!( "{}:{}", bitcoind_container_name, - testutils::bitcoind::RPC_PORT + harness::bitcoind::RPC_PORT ); let image = electrs::Electrs::default() .with_volume(volume) diff --git a/swap/tests/punish.rs b/swap/tests/punish.rs index 758f8e98..f058dc79 100644 --- a/swap/tests/punish.rs +++ b/swap/tests/punish.rs @@ -1,15 +1,15 @@ -pub mod testutils; +pub mod harness; +use harness::bob_run_until::is_btc_locked; +use harness::FastPunishConfig; use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; -use testutils::bob_run_until::is_btc_locked; -use testutils::FastPunishConfig; /// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice /// the encsig and fail to refund or redeem. Alice punishes. #[tokio::test] async fn alice_punishes_if_bob_never_acts_after_fund() { - testutils::setup_test(FastPunishConfig, |mut ctx| async move { + harness::setup_test(FastPunishConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); From 8576894c1074751c96be277de7f5092aeaddad15 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 24 Mar 2021 18:16:58 +1100 Subject: [PATCH 6/7] Split bitcoin::Wallet functions into various impl blocks This allows us to construct instances of bitcoin::Wallet for test purposes that use a different blockchain and database implementation. We also parameterize the electrum-client to make it possible to construct a bitcoin::Wallet for tests that doesn't have one. This is necessary because the client validates the connection as it is constructed and we don't want to provide an Electrum backend for unit tests. --- swap/src/bitcoin/lock.rs | 2 +- swap/src/bitcoin/wallet.rs | 231 ++++++++++++++++++++----------------- 2 files changed, 126 insertions(+), 107 deletions(-) diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index d2d5e7ca..edbcbd85 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -21,7 +21,7 @@ impl TxLock { pub async fn new(wallet: &Wallet, amount: Amount, A: PublicKey, B: PublicKey) -> Result { let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0); let address = lock_output_descriptor - .address(wallet.get_network().await) + .address(wallet.get_network()) .expect("can derive address from descriptor"); let psbt = wallet.send_to_address(address, amount).await?; diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index e4ba57d4..0bda71cb 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -5,11 +5,12 @@ use ::bitcoin::util::psbt::PartiallySignedTransaction; use ::bitcoin::Txid; use anyhow::{bail, Context, Result}; use bdk::blockchain::{noop_progress, Blockchain, ElectrumBlockchain}; +use bdk::database::BatchDatabase; use bdk::descriptor::Segwitv0; use bdk::electrum_client::{ElectrumApi, GetHistoryRes}; use bdk::keys::DerivableKey; use bdk::{FeeRate, KeychainKind}; -use bitcoin::Script; +use bitcoin::{Network, Script}; use reqwest::Url; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; @@ -21,10 +22,11 @@ use tokio::sync::{watch, Mutex}; const SLED_TREE_NAME: &str = "default_tree"; -pub struct Wallet { - client: Arc>, - wallet: Arc>>, +pub struct Wallet { + client: Arc>, + wallet: Arc>>, finality_confirmations: u32, + network: Network, } impl Wallet { @@ -39,7 +41,7 @@ impl Wallet { let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?; - let bdk_wallet = bdk::Wallet::new( + let wallet = bdk::Wallet::new( bdk::template::BIP84(key.clone(), KeychainKind::External), Some(bdk::template::BIP84(key, KeychainKind::Internal)), env_config.bitcoin_network, @@ -50,108 +52,19 @@ impl Wallet { let electrum = bdk::electrum_client::Client::new(electrum_rpc_url.as_str()) .context("Failed to initialize Electrum RPC client")?; + let network = wallet.network(); + Ok(Self { - wallet: Arc::new(Mutex::new(bdk_wallet)), client: Arc::new(Mutex::new(Client::new( electrum, env_config.bitcoin_sync_interval(), )?)), + wallet: Arc::new(Mutex::new(wallet)), finality_confirmations: env_config.bitcoin_finality_confirmations, + network, }) } - pub async fn balance(&self) -> Result { - let balance = self - .wallet - .lock() - .await - .get_balance() - .context("Failed to calculate Bitcoin balance")?; - - Ok(Amount::from_sat(balance)) - } - - pub async fn new_address(&self) -> Result
{ - let address = self - .wallet - .lock() - .await - .get_new_address() - .context("Failed to get new Bitcoin address")?; - - Ok(address) - } - - pub async fn get_tx(&self, txid: Txid) -> Result> { - let tx = self.wallet.lock().await.client().get_tx(&txid)?; - - Ok(tx) - } - - pub async fn transaction_fee(&self, txid: Txid) -> Result { - let fees = self - .wallet - .lock() - .await - .list_transactions(true)? - .iter() - .find(|tx| tx.txid == txid) - .context("Could not find tx in bdk wallet when trying to determine fees")? - .fees; - - Ok(Amount::from_sat(fees)) - } - - pub async fn sync(&self) -> Result<()> { - self.wallet - .lock() - .await - .sync(noop_progress(), None) - .context("Failed to sync balance of Bitcoin wallet")?; - - Ok(()) - } - - pub async fn send_to_address( - &self, - address: Address, - amount: Amount, - ) -> Result { - let wallet = self.wallet.lock().await; - - let mut tx_builder = wallet.build_tx(); - tx_builder.add_recipient(address.script_pubkey(), amount.as_sat()); - tx_builder.fee_rate(self.select_feerate()); - let (psbt, _details) = tx_builder.finish()?; - - Ok(psbt) - } - - /// Calculates the maximum "giveable" amount of this wallet. - /// - /// We define this as the maximum amount we can pay to a single output, - /// already accounting for the fees we need to spend to get the - /// transaction confirmed. - pub async fn max_giveable(&self, locking_script_size: usize) -> Result { - let wallet = self.wallet.lock().await; - - let mut tx_builder = wallet.build_tx(); - - let dummy_script = Script::from(vec![0u8; locking_script_size]); - tx_builder.set_single_recipient(dummy_script); - tx_builder.drain_wallet(); - tx_builder.fee_rate(self.select_feerate()); - let (_, details) = tx_builder.finish().context("Failed to build transaction")?; - - let max_giveable = details.sent - details.fees; - - Ok(Amount::from_sat(max_giveable)) - } - - pub async fn get_network(&self) -> bitcoin::Network { - self.wallet.lock().await.network() - } - /// Broadcast the given transaction to the network and emit a log statement /// if done so successfully. /// @@ -260,13 +173,6 @@ impl Wallet { sub } - - /// Selects an appropriate [`FeeRate`] to be used for getting transactions - /// confirmed within a reasonable amount of time. - fn select_feerate(&self) -> FeeRate { - // TODO: This should obviously not be a const :) - FeeRate::from_sat_per_vb(5.0) - } } /// Represents a subscription to the status of a given transaction. @@ -329,6 +235,119 @@ impl Subscription { } } +impl Wallet +where + D: BatchDatabase, +{ + pub async fn balance(&self) -> Result { + let balance = self + .wallet + .lock() + .await + .get_balance() + .context("Failed to calculate Bitcoin balance")?; + + Ok(Amount::from_sat(balance)) + } + + pub async fn new_address(&self) -> Result
{ + let address = self + .wallet + .lock() + .await + .get_new_address() + .context("Failed to get new Bitcoin address")?; + + Ok(address) + } + + pub async fn transaction_fee(&self, txid: Txid) -> Result { + let fees = self + .wallet + .lock() + .await + .list_transactions(true)? + .iter() + .find(|tx| tx.txid == txid) + .context("Could not find tx in bdk wallet when trying to determine fees")? + .fees; + + Ok(Amount::from_sat(fees)) + } + + pub async fn send_to_address( + &self, + address: Address, + amount: Amount, + ) -> Result { + let wallet = self.wallet.lock().await; + + let mut tx_builder = wallet.build_tx(); + tx_builder.add_recipient(address.script_pubkey(), amount.as_sat()); + tx_builder.fee_rate(self.select_feerate()); + let (psbt, _details) = tx_builder.finish()?; + + Ok(psbt) + } + + /// Calculates the maximum "giveable" amount of this wallet. + /// + /// We define this as the maximum amount we can pay to a single output, + /// already accounting for the fees we need to spend to get the + /// transaction confirmed. + pub async fn max_giveable(&self, locking_script_size: usize) -> Result { + let wallet = self.wallet.lock().await; + + let mut tx_builder = wallet.build_tx(); + + let dummy_script = Script::from(vec![0u8; locking_script_size]); + tx_builder.set_single_recipient(dummy_script); + tx_builder.drain_wallet(); + tx_builder.fee_rate(self.select_feerate()); + let (_, details) = tx_builder.finish().context("Failed to build transaction")?; + + let max_giveable = details.sent - details.fees; + + Ok(Amount::from_sat(max_giveable)) + } +} + +impl Wallet +where + B: Blockchain, + D: BatchDatabase, +{ + pub async fn get_tx(&self, txid: Txid) -> Result> { + let tx = self.wallet.lock().await.client().get_tx(&txid)?; + + Ok(tx) + } + + pub async fn sync(&self) -> Result<()> { + self.wallet + .lock() + .await + .sync(noop_progress(), None) + .context("Failed to sync balance of Bitcoin wallet")?; + + Ok(()) + } +} + +impl Wallet { + // TODO: Get rid of this by changing bounds on bdk::Wallet + pub fn get_network(&self) -> bitcoin::Network { + self.network + } + + /// Selects an appropriate [`FeeRate`] to be used for getting transactions + /// confirmed within a reasonable amount of time. + fn select_feerate(&self) -> FeeRate { + // TODO: This should obviously not be a const :) + FeeRate::from_sat_per_vb(5.0) + } +} + /// Defines a watchable transaction. /// /// For a transaction to be watchable, we need to know two things: Its @@ -350,7 +369,7 @@ impl Watchable for (Txid, Script) { } } -struct Client { +pub struct Client { electrum: bdk::electrum_client::Client, latest_block: BlockHeight, last_ping: Instant, From 52b9a78de2076ec8517b4290f7291678e609d251 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 24 Mar 2021 18:30:55 +1100 Subject: [PATCH 7/7] Alice to validate Bob's PSBT for correctness In order for the re-construction of TxLock to be meaningful, we limit `Message2` to the PSBT instead of the full struct. This is a breaking change in the network layer. The PSBT is valid if: - It has at most two outputs (we allow a change output) - One of the outputs pays the agreed upon amount to a shared output script Resolves #260. --- CHANGELOG.md | 5 + Cargo.lock | 355 +++++++++++++++++---- swap/Cargo.toml | 1 + swap/src/bitcoin.rs | 1 + swap/src/bitcoin/lock.rs | 144 ++++++++- swap/src/bitcoin/wallet.rs | 33 ++ swap/src/protocol.rs | 2 +- swap/src/protocol/alice/execution_setup.rs | 4 +- swap/src/protocol/alice/state.rs | 11 +- swap/src/protocol/bob/state.rs | 2 +- 10 files changed, 485 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8745f522..6561f042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,4 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 failing on non-english language systems preventing users from starting the swap-cli and asb. +### Security + +- Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them. + Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version. + [Unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/v0.3...HEAD diff --git a/Cargo.lock b/Cargo.lock index f4bbf4df..3ae505f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,6 +221,16 @@ dependencies = [ "keccak-hash 0.1.2", ] +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + [[package]] name = "base64" version = "0.10.1" @@ -251,9 +261,9 @@ dependencies = [ "async-trait", "bdk-macros", "bitcoin", - "electrum-client", + "electrum-client 0.7.0", "js-sys", - "log", + "log 0.4.14", "miniscript", "rand 0.7.3", "serde", @@ -273,6 +283,22 @@ dependencies = [ "syn", ] +[[package]] +name = "bdk-testutils" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d9382c8dfda457f2be9b700ffd580f12babec5d34ee39343768f65724ddd64" +dependencies = [ + "bitcoin", + "bitcoincore-rpc", + "electrum-client 0.6.0", + "log 0.4.14", + "miniscript", + "serde", + "serde_json", + "serial_test", +] + [[package]] name = "bech32" version = "0.7.3" @@ -326,7 +352,7 @@ dependencies = [ "thiserror", "tokio", "tracing", - "url", + "url 2.2.1", ] [[package]] @@ -338,6 +364,19 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoincore-rpc" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d708433972bf78bd5f909d1d288f9ac1cceeab1460edb954e962f83e1f440a3" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log 0.4.14", + "serde", + "serde_json", +] + [[package]] name = "bitcoincore-rpc-json" version = "0.13.0" @@ -886,6 +925,22 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "electrum-client" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21453800c95bb1aaa57490458c42d60c6277cb8a3e386030ec2381d5c2d4fa77" +dependencies = [ + "bitcoin", + "log 0.4.14", + "rustls 0.16.0", + "serde", + "serde_json", + "socks", + "webpki", + "webpki-roots 0.19.0", +] + [[package]] name = "electrum-client" version = "0.7.0" @@ -893,7 +948,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cab4d90cc575a7daab4cfed9e315912a88071bc47462e6be57516a2f01ccc89" dependencies = [ "bitcoin", - "log", + "log 0.4.14", "rustls 0.16.0", "serde", "serde_json", @@ -1050,7 +1105,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", - "percent-encoding", + "percent-encoding 2.1.0", ] [[package]] @@ -1194,7 +1249,7 @@ checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" dependencies = [ "serde", "typenum", - "version_check", + "version_check 0.9.3", ] [[package]] @@ -1395,6 +1450,25 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" +[[package]] +name = "hyper" +version = "0.10.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" +dependencies = [ + "base64 0.9.3", + "httparse", + "language-tags", + "log 0.3.9", + "mime 0.2.6", + "num_cpus", + "time 0.1.43", + "traitobject", + "typeable", + "unicase", + "url 1.7.2", +] + [[package]] name = "hyper" version = "0.14.5" @@ -1426,14 +1500,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" dependencies = [ "futures-util", - "hyper", - "log", + "hyper 0.14.4", + "log 0.4.14", "rustls 0.19.0", "tokio", "tokio-rustls", "webpki", ] +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.2.2" @@ -1545,6 +1630,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpc" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436f3455a8a4e9c7b14de9f1206198ee5d0bdc2db1b560339d2141093d7dd389" +dependencies = [ + "hyper 0.10.16", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "jsonrpc_client" version = "0.5.1" @@ -1556,7 +1653,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "url", + "url 2.2.1", ] [[package]] @@ -1589,6 +1686,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + [[package]] name = "lazy_static" version = "1.4.0" @@ -1634,7 +1737,7 @@ dependencies = [ "libp2p-tcp", "libp2p-yamux", "parity-multiaddr", - "parking_lot", + "parking_lot 0.11.1", "pin-project 1.0.5", "smallvec", "wasm-timer", @@ -1646,7 +1749,7 @@ version = "0.1.0" source = "git+https://github.com/comit-network/rust-libp2p-async-await#7a9006ceddd132ef5d40a597936cf15381a5cfe1" dependencies = [ "libp2p", - "log", + "log 0.4.14", ] [[package]] @@ -1664,11 +1767,11 @@ dependencies = [ "futures-timer", "lazy_static", "libsecp256k1", - "log", + "log 0.4.14", "multihash", "multistream-select", "parity-multiaddr", - "parking_lot", + "parking_lot 0.11.1", "pin-project 1.0.5", "prost", "prost-build", @@ -1691,7 +1794,7 @@ checksum = "9712eb3e9f7dcc77cc5ca7d943b6a85ce4b1faaf91a67e003442412a26d6d6f8" dependencies = [ "futures", "libp2p-core", - "log", + "log 0.4.14", "smallvec", "trust-dns-resolver", ] @@ -1706,9 +1809,9 @@ dependencies = [ "bytes", "futures", "libp2p-core", - "log", + "log 0.4.14", "nohash-hasher", - "parking_lot", + "parking_lot 0.11.1", "rand 0.7.3", "smallvec", "unsigned-varint 0.7.0", @@ -1725,7 +1828,7 @@ dependencies = [ "futures", "lazy_static", "libp2p-core", - "log", + "log 0.4.14", "prost", "prost-build", "rand 0.7.3", @@ -1747,7 +1850,7 @@ dependencies = [ "futures", "libp2p-core", "libp2p-swarm", - "log", + "log 0.4.14", "lru", "minicbor", "rand 0.7.3", @@ -1765,7 +1868,7 @@ dependencies = [ "either", "futures", "libp2p-core", - "log", + "log 0.4.14", "rand 0.7.3", "smallvec", "void", @@ -1794,7 +1897,7 @@ dependencies = [ "ipnet", "libc", "libp2p-core", - "log", + "log 0.4.14", "socket2 0.4.0", "tokio", ] @@ -1807,7 +1910,7 @@ checksum = "96d6144cc94143fb0a8dd1e7c2fbcc32a2808168bcd1d69920635424d5993b7b" dependencies = [ "futures", "libp2p-core", - "parking_lot", + "parking_lot 0.11.1", "thiserror", "yamux", ] @@ -1834,6 +1937,15 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + [[package]] name = "lock_api" version = "0.4.2" @@ -1843,6 +1955,15 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +dependencies = [ + "log 0.4.14", +] + [[package]] name = "log" version = "0.4.14" @@ -1906,6 +2027,15 @@ dependencies = [ "autocfg 1.0.1", ] +[[package]] +name = "mime" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" +dependencies = [ + "log 0.3.9", +] + [[package]] name = "mime" version = "0.3.16" @@ -1958,7 +2088,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2182a122f3b7f3f5329cb1972cee089ba2459a0a80a56935e6e674f096f8d839" dependencies = [ "libc", - "log", + "log 0.4.14", "miow", "ntapi", "winapi 0.3.9", @@ -2059,7 +2189,7 @@ checksum = "7d91ec0a2440aaff5f78ec35631a7027d50386c6163aa975f7caa0d5da4b6ff8" dependencies = [ "bytes", "futures", - "log", + "log 0.4.14", "pin-project 1.0.5", "smallvec", "unsigned-varint 0.7.0", @@ -2079,7 +2209,7 @@ checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" dependencies = [ "lexical-core", "memchr", - "version_check", + "version_check 0.9.3", ] [[package]] @@ -2208,11 +2338,11 @@ dependencies = [ "byteorder", "data-encoding", "multihash", - "percent-encoding", + "percent-encoding 2.1.0", "serde", "static_assertions 1.1.0", "unsigned-varint 0.7.0", - "url", + "url 2.2.1", ] [[package]] @@ -2227,6 +2357,16 @@ dependencies = [ "serde", ] +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.7.2", +] + [[package]] name = "parking_lot" version = "0.11.1" @@ -2234,8 +2374,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ "instant", - "lock_api", - "parking_lot_core", + "lock_api 0.4.2", + "parking_lot_core 0.8.3", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "smallvec", + "winapi 0.3.9", ] [[package]] @@ -2263,6 +2417,12 @@ dependencies = [ "regex", ] +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -2414,7 +2574,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "version_check", + "version_check 0.9.3", ] [[package]] @@ -2425,7 +2585,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "version_check", + "version_check 0.9.3", ] [[package]] @@ -2468,7 +2628,7 @@ dependencies = [ "bytes", "heck", "itertools", - "log", + "log 0.4.14", "multimap", "petgraph", "prost", @@ -2826,14 +2986,14 @@ dependencies = [ "futures-util", "http", "http-body", - "hyper", + "hyper 0.14.4", "hyper-rustls", "ipnet", "js-sys", "lazy_static", - "log", - "mime", - "percent-encoding", + "log 0.4.14", + "mime 0.3.16", + "percent-encoding 2.1.0", "pin-project-lite", "rustls 0.19.0", "serde", @@ -2841,7 +3001,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-rustls", - "url", + "url 2.2.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2925,7 +3085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" dependencies = [ "base64 0.10.1", - "log", + "log 0.4.14", "ring", "sct", "webpki", @@ -2938,7 +3098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" dependencies = [ "base64 0.13.0", - "log", + "log 0.4.14", "ring", "sct", "webpki", @@ -2961,6 +3121,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "scopeguard" version = "1.1.0" @@ -3097,6 +3263,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef5f7c7434b2f2c598adc6f9494648a1e41274a75c0ba4056f680ae0c117fd6" +dependencies = [ + "lazy_static", + "parking_lot 0.10.2", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08338d8024b227c62bd68a12c7c9883f5c66780abaef15c550dc56f46ee6515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha-1" version = "0.9.4" @@ -3196,8 +3384,8 @@ dependencies = [ "fs2", "fxhash", "libc", - "log", - "parking_lot", + "log 0.4.14", + "parking_lot 0.11.1", ] [[package]] @@ -3278,7 +3466,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2beb4d1860a61f571530b3f855a1b538d0200f7871c63331ecd6f17b1f014f8" dependencies = [ - "version_check", + "version_check 0.9.3", ] [[package]] @@ -3426,6 +3614,7 @@ dependencies = [ "backoff", "base64 0.13.0", "bdk", + "bdk-testutils", "big-bytes", "bitcoin", "bitcoin-harness", @@ -3437,7 +3626,7 @@ dependencies = [ "ecdsa_fun", "futures", "get-port", - "hyper", + "hyper 0.14.4", "libp2p", "libp2p-async-await", "miniscript", @@ -3472,7 +3661,7 @@ dependencies = [ "tracing", "tracing-futures", "tracing-subscriber", - "url", + "url 2.2.1", "uuid", "void", "zip", @@ -3545,7 +3734,7 @@ dependencies = [ "derivative", "hex 0.4.3", "hmac 0.8.1", - "log", + "log 0.4.14", "rand 0.7.3", "serde", "serde_json", @@ -3560,7 +3749,7 @@ checksum = "d5e3ed6e3598dbf32cba8cb356b881c085e0adea57597f387723430dd94b4084" dependencies = [ "hex 0.4.3", "hmac 0.10.1", - "log", + "log 0.4.14", "rand 0.8.3", "serde", "serde_json", @@ -3626,7 +3815,7 @@ dependencies = [ "standback", "stdweb", "time-macros", - "version_check", + "version_check 0.9.3", "winapi 0.3.9", ] @@ -3690,7 +3879,7 @@ dependencies = [ "mio", "num_cpus", "once_cell", - "parking_lot", + "parking_lot 0.11.1", "pin-project-lite", "signal-hook-registry", "tokio-macros", @@ -3751,7 +3940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e96bb520beab540ab664bd5a9cfeaa1fcd846fa68c830b42e2c8963071251d2" dependencies = [ "futures-util", - "log", + "log 0.4.14", "pin-project 1.0.5", "rustls 0.19.0", "tokio", @@ -3770,7 +3959,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", - "log", + "log 0.4.14", "pin-project-lite", "tokio", ] @@ -3841,7 +4030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" dependencies = [ "lazy_static", - "log", + "log 0.4.14", "tracing-core", ] @@ -3863,6 +4052,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "traitobject" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" + [[package]] name = "trust-dns-proto" version = "0.20.1" @@ -3876,16 +4071,16 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna", + "idna 0.2.2", "ipnet", "lazy_static", - "log", + "log 0.4.14", "rand 0.8.3", "smallvec", "thiserror", "tinyvec", "tokio", - "url", + "url 2.2.1", ] [[package]] @@ -3898,9 +4093,9 @@ dependencies = [ "futures-util", "ipconfig", "lazy_static", - "log", + "log 0.4.14", "lru-cache", - "parking_lot", + "parking_lot 0.11.1", "resolv-conf", "smallvec", "thiserror", @@ -3926,17 +4121,23 @@ dependencies = [ "http", "httparse", "input_buffer", - "log", + "log 0.4.14", "rand 0.8.3", "rustls 0.19.0", "sha-1", "thiserror", - "url", + "url 2.2.1", "utf-8", "webpki", "webpki-roots 0.21.0", ] +[[package]] +name = "typeable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" + [[package]] name = "typenum" version = "1.13.0" @@ -3967,6 +4168,15 @@ dependencies = [ "static_assertions 1.1.0", ] +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +dependencies = [ + "version_check 0.1.5", +] + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -4037,6 +4247,17 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", +] + [[package]] name = "url" version = "2.2.1" @@ -4044,9 +4265,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" dependencies = [ "form_urlencoded", - "idna", + "idna 0.2.2", "matches", - "percent-encoding", + "percent-encoding 2.1.0", "serde", ] @@ -4072,6 +4293,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + [[package]] name = "version_check" version = "0.9.3" @@ -4090,7 +4317,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" dependencies = [ - "log", + "log 0.4.14", "try-lock", ] @@ -4126,7 +4353,7 @@ checksum = "5b7d8b6942b8bb3a9b0e73fc79b98095a27de6fa247615e59d096754a3bc2aa8" dependencies = [ "bumpalo", "lazy_static", - "log", + "log 0.4.14", "proc-macro2", "quote", "syn", @@ -4182,7 +4409,7 @@ checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" dependencies = [ "futures", "js-sys", - "parking_lot", + "parking_lot 0.11.1", "pin-utils", "wasm-bindgen", "wasm-bindgen-futures", @@ -4332,9 +4559,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cc7bd8c983209ed5d527f44b01c41b7dc146fd960c61cf9e1d25399841dc271" dependencies = [ "futures", - "log", + "log 0.4.14", "nohash-hasher", - "parking_lot", + "parking_lot 0.11.1", "rand 0.7.3", "static_assertions 1.1.0", ] diff --git a/swap/Cargo.toml b/swap/Cargo.toml index b66a4f3e..1ddaad4f 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -64,6 +64,7 @@ tokio-tar = { path = "../tokio-tar" } zip = "0.5" [dev-dependencies] +bdk-testutils = { version = "0.3" } bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs" } get-port = "3" hyper = "0.14" diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index fef2e252..84434ffe 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -14,6 +14,7 @@ pub use crate::bitcoin::redeem::TxRedeem; pub use crate::bitcoin::refund::TxRefund; pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks}; pub use ::bitcoin::util::amount::Amount; +pub use ::bitcoin::util::psbt::PartiallySignedTransaction; pub use ::bitcoin::{Address, Network, Transaction, Txid}; pub use ecdsa_fun::adaptor::EncryptedSignature; pub use ecdsa_fun::fun::Scalar; diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index edbcbd85..63f2fb53 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -4,7 +4,8 @@ use crate::bitcoin::{ }; use ::bitcoin::util::psbt::PartiallySignedTransaction; use ::bitcoin::{OutPoint, TxIn, TxOut, Txid}; -use anyhow::Result; +use anyhow::{bail, Result}; +use bdk::database::BatchDatabase; use bitcoin::Script; use ecdsa_fun::fun::Point; use miniscript::{Descriptor, DescriptorTrait}; @@ -18,7 +19,15 @@ pub struct TxLock { } impl TxLock { - pub async fn new(wallet: &Wallet, amount: Amount, A: PublicKey, B: PublicKey) -> Result { + pub async fn new( + wallet: &Wallet, + amount: Amount, + A: PublicKey, + B: PublicKey, + ) -> Result + where + D: BatchDatabase, + { let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0); let address = lock_output_descriptor .address(wallet.get_network()) @@ -32,6 +41,56 @@ impl TxLock { }) } + /// Creates an instance of `TxLock` from a PSBT, the public keys of the + /// parties and the specified amount. + /// + /// This function validates that the given PSBT does indeed pay that + /// specified amount to a shared output. + pub fn from_psbt( + psbt: PartiallySignedTransaction, + A: PublicKey, + B: PublicKey, + btc: Amount, + ) -> Result { + let shared_output_candidate = match psbt.global.unsigned_tx.output.as_slice() { + [shared_output_candidate, _] if shared_output_candidate.value == btc.as_sat() => { + shared_output_candidate + } + [_, shared_output_candidate] if shared_output_candidate.value == btc.as_sat() => { + shared_output_candidate + } + // A single output is possible if Bob funds without any change necessary + [shared_output_candidate] if shared_output_candidate.value == btc.as_sat() => { + shared_output_candidate + } + [_, _] => { + bail!("Neither of the two provided outputs pays the right amount!"); + } + [_] => { + bail!("The provided output does not pay the right amount!"); + } + other => { + let num_outputs = other.len(); + bail!( + "PSBT has {} outputs, expected one or two. Something is fishy!", + num_outputs + ); + } + }; + + let descriptor = build_shared_output_descriptor(A.0, B.0); + let legit_shared_output_script = descriptor.script_pubkey(); + + if shared_output_candidate.script_pubkey != legit_shared_output_script { + bail!("Output script is not a shared output") + } + + Ok(TxLock { + inner: psbt, + output_descriptor: descriptor, + }) + } + pub fn lock_amount(&self) -> Amount { Amount::from_sat(self.inner.clone().extract_tx().output[self.lock_output_vout()].value) } @@ -116,3 +175,84 @@ impl Watchable for TxLock { self.output_descriptor.script_pubkey() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn given_bob_sends_good_psbt_when_reconstructing_then_succeeeds() { + let (A, B) = alice_and_bob(); + let wallet = Wallet::new_funded(50000); + let agreed_amount = Amount::from_sat(10000); + + let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await; + let result = TxLock::from_psbt(psbt, A, B, agreed_amount); + + result.expect("PSBT to be valid"); + } + + #[tokio::test] + async fn bob_can_fund_without_a_change_output() { + let (A, B) = alice_and_bob(); + let fees = 610; + let agreed_amount = Amount::from_sat(10000); + let wallet = Wallet::new_funded(agreed_amount.as_sat() + fees); + + let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await; + assert_eq!( + psbt.global.unsigned_tx.output.len(), + 1, + "psbt should only have a single output" + ); + let result = TxLock::from_psbt(psbt, A, B, agreed_amount); + + result.expect("PSBT to be valid"); + } + + #[tokio::test] + async fn given_bob_is_sending_less_than_agreed_when_reconstructing_txlock_then_fails() { + let (A, B) = alice_and_bob(); + let wallet = Wallet::new_funded(50000); + let agreed_amount = Amount::from_sat(10000); + + let bad_amount = Amount::from_sat(5000); + let psbt = bob_make_psbt(A, B, &wallet, bad_amount).await; + let result = TxLock::from_psbt(psbt, A, B, agreed_amount); + + result.expect_err("PSBT to be invalid"); + } + + #[tokio::test] + async fn given_bob_is_sending_to_a_bad_output_reconstructing_txlock_then_fails() { + let (A, B) = alice_and_bob(); + let wallet = Wallet::new_funded(50000); + let agreed_amount = Amount::from_sat(10000); + + let E = eve(); + let psbt = bob_make_psbt(E, B, &wallet, agreed_amount).await; + let result = TxLock::from_psbt(psbt, A, B, agreed_amount); + + result.expect_err("PSBT to be invalid"); + } + + /// Helper function that represents Bob's action of constructing the PSBT. + /// + /// Extracting this allows us to keep the tests concise. + async fn bob_make_psbt( + A: PublicKey, + B: PublicKey, + wallet: &Wallet<(), bdk::database::MemoryDatabase, ()>, + amount: Amount, + ) -> PartiallySignedTransaction { + TxLock::new(&wallet, amount, A, B).await.unwrap().into() + } + + fn alice_and_bob() -> (PublicKey, PublicKey) { + (PublicKey::random(), PublicKey::random()) + } + + fn eve() -> PublicKey { + PublicKey::random() + } +} diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 0bda71cb..c87c0f3e 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -348,6 +348,39 @@ impl Wallet { } } +#[cfg(test)] +impl Wallet<(), bdk::database::MemoryDatabase, ()> { + /// Creates a new, funded wallet to be used within tests. + pub fn new_funded(amount: u64) -> Self { + use bdk::database::MemoryDatabase; + use bdk::{LocalUtxo, TransactionDetails}; + use bitcoin::OutPoint; + use std::str::FromStr; + use testutils::testutils; + + let descriptors = testutils!(@descriptors ("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)")); + + let mut database = MemoryDatabase::new(); + bdk::populate_test_db!( + &mut database, + testutils! { + @tx ( (@external descriptors, 0) => amount ) (@confirmations 1) + }, + Some(100) + ); + + let wallet = + bdk::Wallet::new_offline(&descriptors.0, None, Network::Regtest, database).unwrap(); + + Self { + client: Arc::new(Mutex::new(())), + wallet: Arc::new(Mutex::new(wallet)), + finality_confirmations: 1, + network: Network::Regtest, + } + } +} + /// Defines a watchable transaction. /// /// For a transaction to be watchable, we need to know two things: Its diff --git a/swap/src/protocol.rs b/swap/src/protocol.rs index e446759c..fb3a35be 100644 --- a/swap/src/protocol.rs +++ b/swap/src/protocol.rs @@ -47,7 +47,7 @@ pub struct Message1 { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Message2 { - tx_lock: bitcoin::TxLock, + psbt: bitcoin::PartiallySignedTransaction, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/swap/src/protocol/alice/execution_setup.rs b/swap/src/protocol/alice/execution_setup.rs index d3a37519..a4b04aa0 100644 --- a/swap/src/protocol/alice/execution_setup.rs +++ b/swap/src/protocol/alice/execution_setup.rs @@ -57,7 +57,9 @@ impl Behaviour { let message2 = serde_cbor::from_slice::(&substream.read_message(BUF_SIZE).await?) .context("Failed to deserialize message2")?; - let state2 = state1.receive(message2); + let state2 = state1 + .receive(message2) + .context("Failed to receive Message2")?; substream .write_message( diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index ecaee48f..fe0f5679 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -200,8 +200,11 @@ impl State1 { } } - pub fn receive(self, msg: Message2) -> State2 { - State2 { + pub fn receive(self, msg: Message2) -> Result { + let tx_lock = bitcoin::TxLock::from_psbt(msg.psbt, self.a.public(), self.B, self.btc) + .context("Failed to re-construct TxLock from received PSBT")?; + + Ok(State2 { a: self.a, B: self.B, s_a: self.s_a, @@ -215,8 +218,8 @@ impl State1 { refund_address: self.refund_address, redeem_address: self.redeem_address, punish_address: self.punish_address, - tx_lock: msg.tx_lock, - } + tx_lock, + }) } } diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index c70b458e..512b6a25 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -188,7 +188,7 @@ pub struct State1 { impl State1 { pub fn next_message(&self) -> Message2 { Message2 { - tx_lock: self.tx_lock.clone(), + psbt: self.tx_lock.clone().into(), } }