From 0c9caef60fc24dadef97062654d35149b65f0ab8 Mon Sep 17 00:00:00 2001 From: rishflab Date: Thu, 13 May 2021 09:50:35 +1000 Subject: [PATCH] WIP: onchain refund --- Cargo.lock | 40 ++-- monero-adaptor/src/lib.rs | 1 + swap/src/bitcoin/lock.rs | 2 +- swap/src/lib.rs | 2 +- swap/src/monero.rs | 10 +- swap/src/monero/wallet.rs | 8 +- swap/src/xmr_first_protocol.rs | 145 +-------------- swap/src/xmr_first_protocol/alice.rs | 174 +++++++----------- swap/src/xmr_first_protocol/bob.rs | 142 +++----------- .../xmr_first_protocol/state_machine/alice.rs | 115 ++++++++++++ .../xmr_first_protocol/state_machine/bob.rs | 118 ++++++++++++ .../xmr_first_protocol/state_machine/mod.rs | 2 + swap/src/xmr_first_protocol/transactions.rs | 168 +---------------- .../transactions/btc_lock.rs | 8 +- .../transactions/btc_redeem.rs | 146 +++++++++++++++ .../transactions/xmr_refund.rs | 24 ++- swap/tests/harness/mod.rs | 10 +- ...mr_first_bob_fails_to_act_alice_refunds.rs | 110 +++++++++++ swap/tests/xmr_first_happy_path.rs | 109 +++++++++++ 19 files changed, 770 insertions(+), 564 deletions(-) create mode 100644 swap/src/xmr_first_protocol/state_machine/alice.rs create mode 100644 swap/src/xmr_first_protocol/state_machine/bob.rs create mode 100644 swap/src/xmr_first_protocol/state_machine/mod.rs create mode 100644 swap/tests/xmr_first_bob_fails_to_act_alice_refunds.rs create mode 100644 swap/tests/xmr_first_happy_path.rs diff --git a/Cargo.lock b/Cargo.lock index 3271089b..bc4679f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3725,16 +3725,16 @@ dependencies = [ "atty", "backoff", "base64 0.13.0", - "bdk", - "bdk-testutils", - "big-bytes", - "bitcoin", - "bitcoin-harness", - "bmrng", - "config", - "conquer-once", - "curve25519-dalek", - "dialoguer", + "bdk", + "bdk-testutils", + "big-bytes", + "bitcoin", + "bitcoin-harness", + "bmrng", + "config", + "conquer-once", + "curve25519-dalek", + "dialoguer", "directories-next", "ecdsa_fun", "futures", @@ -3756,16 +3756,16 @@ dependencies = [ "reqwest", "rust_decimal", "serde", - "serde_cbor", - "serde_json", - "sha2 0.9.3", - "sigma_fun", - "sled", - "spectral", - "structopt", - "strum", - "tempfile", - "testcontainers 0.12.0", + "serde_cbor", + "serde_json", + "sha2 0.9.3", + "sigma_fun", + "sled", + "spectral", + "structopt", + "strum", + "tempfile", + "testcontainers 0.12.0", "thiserror", "time 0.2.26", "tokio 1.4.0", diff --git a/monero-adaptor/src/lib.rs b/monero-adaptor/src/lib.rs index 02030f65..122212a7 100644 --- a/monero-adaptor/src/lib.rs +++ b/monero-adaptor/src/lib.rs @@ -96,6 +96,7 @@ fn final_challenge( Ok((h_last, h_0)) } +#[derive(Clone)] pub struct AdaptorSignature { s_0_a: Scalar, s_0_b: Scalar, diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index 63f2fb53..6ae87af1 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct TxLock { inner: PartiallySignedTransaction, - pub(in crate::bitcoin) output_descriptor: Descriptor<::bitcoin::PublicKey>, + pub(crate) output_descriptor: Descriptor<::bitcoin::PublicKey>, } impl TxLock { diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 3bf4fdbf..3e0cbfcf 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -27,6 +27,6 @@ pub mod monero; pub mod network; pub mod protocol; pub mod seed; -mod xmr_first_protocol; +pub mod xmr_first_protocol; mod monero_ext; diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 6c9164f2..13bb3237 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -40,8 +40,8 @@ impl PrivateViewKey { Self(private_key) } - pub fn public(&self) -> PrivateViewKey { - PrivateViewKey(PublicKey::from_private_key(&self.0)) + pub fn public(&self) -> PublicViewKey { + PublicViewKey(PublicKey::from_private_key(&self.0)) } } @@ -59,14 +59,14 @@ impl From for PrivateKey { } } -impl From for PublicKey { - fn from(from: PrivateViewKey) -> Self { +impl From for PublicKey { + fn from(from: PublicViewKey) -> Self { from.0 } } #[derive(Clone, Copy, Debug)] -pub struct PrivateViewKey(PublicKey); +pub struct PublicViewKey(PublicKey); #[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)] pub struct Amount(u64); diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index 846aa827..68e5e809 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -1,5 +1,7 @@ use crate::env::Config; -use crate::monero::{Amount, InsufficientFunds, PrivateViewKey, TransferProof, TxHash}; +use crate::monero::{ + Amount, InsufficientFunds, PrivateViewKey, PublicViewKey, TransferProof, TxHash, +}; use ::monero::{Address, Network, PrivateKey, PublicKey}; use anyhow::{Context, Result}; use monero_rpc::wallet; @@ -273,14 +275,14 @@ impl Wallet { #[derive(Debug)] pub struct TransferRequest { pub public_spend_key: PublicKey, - pub public_view_key: PrivateViewKey, + pub public_view_key: PublicViewKey, pub amount: Amount, } #[derive(Debug)] pub struct WatchRequest { pub public_spend_key: PublicKey, - pub public_view_key: PrivateViewKey, + pub public_view_key: PublicViewKey, pub transfer_proof: TransferProof, pub conf_target: u64, pub expected: Amount, diff --git a/swap/src/xmr_first_protocol.rs b/swap/src/xmr_first_protocol.rs index bf23ecdd..4994c6a5 100644 --- a/swap/src/xmr_first_protocol.rs +++ b/swap/src/xmr_first_protocol.rs @@ -1,151 +1,14 @@ use crate::bitcoin::Txid; -use crate::monero::wallet::WatchRequest; -use crate::monero::{PrivateViewKey, Scalar, TransferRequest}; -use crate::xmr_first_protocol::transactions::xmr_lock::XmrLock; -use anyhow::Result; -use monero::PublicKey; -use monero_adaptor::alice::Alice2; use monero_adaptor::AdaptorSignature; -use rand::rngs::OsRng; -// start -pub struct Alice3 { - pub adaptor_sig: AdaptorSignature, - s_a: Scalar, - S_b_monero: monero::PublicKey, -} - -// published xmr_lock, watching for btc_lock -pub struct Alice4 { - pub adaptor_sig: AdaptorSignature, -} - -// published seen btc_lock, published btc_redeem -pub struct Alice5 {} - -// watching for xmr_lock -pub struct Bob3 { - xmr_swap_amount: crate::monero::Amount, - btc_swap_amount: crate::bitcoin::Amount, - xmr_lock: XmrLock, -} - -impl Bob3 { - pub fn watch_for_lock_xmr(&self, wallet: &crate::monero::Wallet) { - let req = WatchRequest { - public_spend_key: self.xmr_lock.public_spend_key, - public_view_key: self.xmr_lock.public_view_key, - transfer_proof: self.xmr_lock.transfer_proof.clone(), - conf_target: 1, - expected: self.xmr_swap_amount, - }; - wallet.watch_for_transfer(req); - } -} - -// published btc_lock, watching for xmr_redeem -pub struct Bob4; - -mod alice; -mod bob; +pub mod alice; +pub mod bob; +mod state_machine; mod transactions; -impl Alice3 { - pub fn new(alice2: Alice2, S_b_monero: PublicKey) -> Self { - Self { - adaptor_sig: alice2.adaptor_sig, - s_a: Scalar::random(&mut OsRng), - S_b_monero, - } - } - pub fn publish_xmr_lock(&self, wallet: &crate::monero::Wallet) -> Result { - let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a }); - - let public_spend_key = S_a + self.S_b_monero; - let public_view_key = self.v.public(); - - let req = TransferRequest { - public_spend_key, - public_view_key, - amount: self.xmr, - }; - - // we may have to send this to Bob - let _ = wallet.transfer(req)?; - } -} - -impl Alice4 { - pub fn watch_for_btc_lock(&self, wallet: &crate::bitcoin::Wallet) -> Result { - wallet.subscribe_to(self.btc_lock()); - } -} - pub struct SeenBtcLock { - s_0_b: monero::Scalar, + s_0_b: crate::monero::Scalar, pub adaptor_sig: AdaptorSignature, tx_lock_id: Txid, tx_lock: bitcoin::Transaction, } - -#[cfg(test)] -mod test { - use crate::monero::Scalar; - use crate::xmr_first_protocol::Alice3; - use curve25519_dalek::constants::ED25519_BASEPOINT_POINT; - use curve25519_dalek::edwards::EdwardsPoint; - use monero_adaptor::alice::Alice0; - use monero_adaptor::bob::Bob0; - use rand::rngs::OsRng; - - #[test] - fn happy_path() { - let msg_to_sign = b"hello world, monero is amazing!!"; - - let s_prime_a = Scalar::random(&mut OsRng); - let s_b = Scalar::random(&mut OsRng); - - let pk = (s_prime_a + s_b) * ED25519_BASEPOINT_POINT; - - let (r_a, R_a, R_prime_a) = { - let r_a = Scalar::random(&mut OsRng); - let R_a = r_a * ED25519_BASEPOINT_POINT; - - let pk_hashed_to_point = hash_point_to_point(pk); - - let R_prime_a = r_a * pk_hashed_to_point; - - (r_a, R_a, R_prime_a) - }; - - let mut ring = [EdwardsPoint::default(); RING_SIZE]; - ring[0] = pk; - - ring[1..].fill_with(|| { - let x = Scalar::random(&mut OsRng); - - x * ED25519_BASEPOINT_POINT - }); - - let alice = Alice0::new(ring, *msg_to_sign, R_a, R_prime_a, s_prime_a).unwrap(); - let bob = Bob0::new(ring, *msg_to_sign, R_a, R_prime_a, s_b).unwrap(); - - let msg = alice.next_message(); - let bob = bob.receive(msg); - - let msg = bob.next_message(); - let alice = alice.receive(msg).unwrap(); - - let msg = alice.next_message(); - let bob = bob.receive(msg).unwrap(); - - let msg = bob.next_message(); - let alice = alice.receive(msg); - - let sig = alice.adaptor_sig.adapt(r_a); - - assert!(sig.verify(ring, msg_to_sign).unwrap()); - - let alice = Alice::new(alice); - } -} diff --git a/swap/src/xmr_first_protocol/alice.rs b/swap/src/xmr_first_protocol/alice.rs index 01c40c5b..8f399c8c 100644 --- a/swap/src/xmr_first_protocol/alice.rs +++ b/swap/src/xmr_first_protocol/alice.rs @@ -1,115 +1,83 @@ -use std::collections::VecDeque; -use std::task::Poll; +use anyhow::Result; +use monero::PublicKey; +use rand::rngs::OsRng; -pub struct StateMachine { - state: State, - actions: VecDeque, - events: VecDeque, +use monero_adaptor::alice::Alice2; +use monero_adaptor::AdaptorSignature; + +use crate::bitcoin::TxLock; +use crate::monero::{Scalar, TransferRequest}; +use curve25519_dalek::edwards::EdwardsPoint; + +// start +pub struct Alice3 { + pub xmr_swap_amount: crate::monero::Amount, + pub btc_swap_amount: crate::bitcoin::Amount, + // pub adaptor_sig: AdaptorSignature, + pub a: crate::bitcoin::SecretKey, + pub B: crate::bitcoin::PublicKey, + pub s_a: Scalar, + pub S_b_monero: EdwardsPoint, + pub v_a: crate::monero::PrivateViewKey, } -impl StateMachine { - fn inject_event(&mut self, event: Event) { - match self.state { - State::WatchingForBtcLock => match event { - Event::BtcLockSeenInMempool => { - self.actions.push_back(Action::SignAndBroadcastBtcRedeem); - self.actions.push_back(Action::WatchForXmrRedeem); - self.state = State::WatchingForXmrRedeem; - } - Event::BtcLockTimeoutElapsed => { - self.actions.push_back(Action::BroadcastXmrRefund); - self.state = State::Aborted; - } - _ => {} - }, - State::WatchingForXmrRedeem => match event { - Event::T2Elapsed => { - self.actions.push_back(Action::BroadcastXmrRefund); - self.actions.push_back(Action::SignAndBroadcastBtcPunish); - self.state = State::Punished; - } - Event::XmrRedeemSeenInMempool => { - self.actions.push_back(Action::SignAndBroadcastBtcPunish); - self.state = State::Success; - } - _ => {} - }, - _ => {} +// published xmr_lock, watching for btc_lock +pub struct Alice4 { + a: crate::bitcoin::SecretKey, + B: crate::bitcoin::PublicKey, + btc_swap_amount: crate::bitcoin::Amount, + // pub adaptor_sig: AdaptorSignature, +} + +// published seen btc_lock, published btc_redeem +pub struct Alice5; + +impl Alice3 { + pub fn new( + S_b_monero: EdwardsPoint, + B: crate::bitcoin::PublicKey, + xmr_swap_amount: crate::monero::Amount, + btc_swap_amount: crate::bitcoin::Amount, + ) -> Self { + Self { + xmr_swap_amount, + btc_swap_amount, + // adaptor_sig: alice2.adaptor_sig, + a: crate::bitcoin::SecretKey::new_random(&mut OsRng), + B, + s_a: Scalar::random(&mut OsRng), + S_b_monero, + v_a: crate::monero::PrivateViewKey::new_random(&mut OsRng), } } + pub async fn publish_xmr_lock(&self, wallet: &crate::monero::Wallet) -> Result { + let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a }); - fn poll(&mut self) -> Poll { - if let Some(action) = self.actions.pop_front() { - Poll::Ready(action) - } else { - Poll::Pending - } - } -} + let public_spend_key = S_a + self.S_b_monero; + let public_view_key = self.v_a.public(); -#[derive(PartialEq, Debug)] -pub enum State { - WatchingForBtcLock, - WatchingForXmrRedeem, - Punished, - Success, - Aborted, -} - -pub enum Event { - BtcLockSeenInMempool, - T2Elapsed, - BtcLockTimeoutElapsed, - XmrRedeemSeenInMempool, -} - -// These actions should not fail (are retried until successful) and should be -// idempotent This allows us to greatly simplify the state machine -pub enum Action { - WatchForXmrRedeem, - SignAndBroadcastBtcPunish, - SignAndBroadcastBtcRedeem, - BroadcastXmrRefund, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn happy_path() { - let mut state_machine = StateMachine { - state: State::WatchingForBtcLock, - actions: Default::default(), - events: Default::default(), + let req = TransferRequest { + public_spend_key, + public_view_key, + amount: self.xmr_swap_amount, }; - state_machine.inject_event(Event::BtcLockSeenInMempool); - state_machine.inject_event(Event::XmrRedeemSeenInMempool); - assert_eq!(state_machine.state, State::Success); - } - #[test] - fn bob_fails_to_lock_btc() { - let mut state_machine = StateMachine { - state: State::WatchingForBtcLock, - actions: Default::default(), - events: Default::default(), - }; - state_machine.events.push_back(Event::BtcLockTimeoutElapsed); - state_machine.run(); - assert_eq!(state_machine.state, State::Aborted); - } + // we may have to send this to Bob + let _ = wallet.transfer(req).await?; - #[test] - fn bob_fails_to_redeem_xmr_before_t2() { - let mut state_machine = StateMachine { - state: State::WatchingForBtcLock, - actions: Default::default(), - events: Default::default(), - }; - state_machine.events.push_back(Event::BtcLockSeenInMempool); - state_machine.events.push_back(Event::T2Elapsed); - state_machine.run(); - assert_eq!(state_machine.state, State::Punished); + Ok(Alice4 { + a: self.a.clone(), + B: self.B, + btc_swap_amount: Default::default(), + // adaptor_sig: self.adaptor_sig.clone(), + }) + } +} + +impl Alice4 { + pub async fn watch_for_btc_lock(&self, wallet: &crate::bitcoin::Wallet) -> Result { + let btc_lock = TxLock::new(wallet, self.btc_swap_amount, self.a.public(), self.B).await?; + wallet.subscribe_to(btc_lock); + Ok(Alice5) } } diff --git a/swap/src/xmr_first_protocol/bob.rs b/swap/src/xmr_first_protocol/bob.rs index 86d913ff..0bb19a99 100644 --- a/swap/src/xmr_first_protocol/bob.rs +++ b/swap/src/xmr_first_protocol/bob.rs @@ -1,119 +1,35 @@ -use crate::monero::TransferProof; -use std::collections::VecDeque; +use anyhow::Result; +use monero::PublicKey; +use rand::rngs::OsRng; -pub struct StateMachine { - state: State, - actions: VecDeque, - events: VecDeque, +use monero_adaptor::alice::Alice2; +use monero_adaptor::AdaptorSignature; + +use crate::bitcoin::Txid; +use crate::monero::wallet::WatchRequest; +use crate::monero::{Scalar, TransferRequest}; +use crate::xmr_first_protocol::transactions::xmr_lock::XmrLock; + +// watching for xmr_lock +pub struct Bob3 { + pub xmr_swap_amount: crate::monero::Amount, + pub btc_swap_amount: crate::bitcoin::Amount, + pub xmr_lock: XmrLock, + v_b: crate::monero::PrivateViewKey, } -impl StateMachine { - fn next(&mut self, event: Event) { - match self.state { - State::WatchingForXmrLock => match event { - Event::XmrConfirmed => { - self.actions.push_back(Action::SignAndBroadcastBtcLock); - self.state = State::WaitingForBtcRedeem; - } - Event::T1Elapsed => { - self.state = State::Aborted; - } - Event::XmrRefundSeenInMempool => { - self.state = State::Aborted; - } - _ => panic!("unhandled scenario"), - }, - State::WaitingForBtcRedeem => match event { - Event::BtcRedeemSeenInMempool => { - self.actions.push_back(Action::BroadcastXmrRedeem); - self.state = State::Success; - } - Event::T1Elapsed => { - self.actions.push_back(Action::SignAndBroadcastBtcRefund); - self.state = State::Refunded; - } - Event::XmrRefundSeenInMempool => { - self.actions.push_back(Action::SignAndBroadcastBtcRefund); - self.state = State::Refunded; - } - _ => panic!("unhandled scenario"), - }, - _ => {} - } - } - - fn run(&mut self) { - while let Some(event) = self.events.pop_front() { - self.next(event); - } - } -} - -#[derive(PartialEq, Debug)] -pub enum State { - WatchingForXmrLock, - WaitingForBtcRedeem, - Success, - Refunded, - Aborted, -} - -pub enum Event { - XmrConfirmed, - // This will contain the s_a allowing bob to build xmr_redeem - BtcRedeemSeenInMempool, - XmrRefundSeenInMempool, - T1Elapsed, -} - -pub enum Action { - SignAndBroadcastBtcLock, - BroadcastXmrRedeem, - SignAndBroadcastBtcRefund, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn happy_path() { - let mut state_machine = StateMachine { - state: State::WatchingForXmrLock, - actions: Default::default(), - events: Default::default(), +impl Bob3 { + pub fn watch_for_lock_xmr(&self, wallet: &crate::monero::Wallet) { + let req = WatchRequest { + public_spend_key: self.xmr_lock.public_spend_key, + public_view_key: self.v_b.public(), + transfer_proof: self.xmr_lock.transfer_proof.clone(), + conf_target: 1, + expected: self.xmr_swap_amount, }; - state_machine.events.push_back(Event::XmrConfirmed); - state_machine - .events - .push_back(Event::BtcRedeemSeenInMempool); - state_machine.run(); - assert_eq!(state_machine.state, State::Success); - } - - #[test] - fn alice_fails_to_redeem_btc_before_t1() { - let mut state_machine = StateMachine { - state: State::WatchingForXmrLock, - actions: Default::default(), - events: Default::default(), - }; - state_machine.events.push_back(Event::XmrConfirmed); - state_machine.events.push_back(Event::T1Elapsed); - state_machine.run(); - assert_eq!(state_machine.state, State::Refunded); - } - - #[test] - fn alice_tries_to_refund_xmr_after_redeeming_btc() { - let mut state_machine = StateMachine { - state: State::WatchingForXmrLock, - actions: Default::default(), - events: Default::default(), - }; - state_machine.events.push_back(Event::XmrConfirmed); - state_machine.events.push_back(Event::T1Elapsed); - state_machine.run(); - assert_eq!(state_machine.state, State::Refunded); + wallet.watch_for_transfer(req); } } + +// published btc_lock, watching for xmr_redeem +pub struct Bob4; diff --git a/swap/src/xmr_first_protocol/state_machine/alice.rs b/swap/src/xmr_first_protocol/state_machine/alice.rs new file mode 100644 index 00000000..136af642 --- /dev/null +++ b/swap/src/xmr_first_protocol/state_machine/alice.rs @@ -0,0 +1,115 @@ +use std::collections::VecDeque; +use std::task::Poll; + +pub struct StateMachine { + state: State, + actions: VecDeque, + events: VecDeque, +} + +impl StateMachine { + fn inject_event(&mut self, event: Event) { + match self.state { + State::WatchingForBtcLock => match event { + Event::BtcLockSeenInMempool => { + self.actions.push_back(Action::SignAndBroadcastBtcRedeem); + self.actions.push_back(Action::WatchForXmrRedeem); + self.state = State::WatchingForXmrRedeem; + } + Event::BtcLockTimeoutElapsed => { + self.actions.push_back(Action::BroadcastXmrRefund); + self.state = State::Aborted; + } + _ => {} + }, + State::WatchingForXmrRedeem => match event { + Event::T2Elapsed => { + self.actions.push_back(Action::BroadcastXmrRefund); + self.actions.push_back(Action::SignAndBroadcastBtcPunish); + self.state = State::Punished; + } + Event::XmrRedeemSeenInMempool => { + self.actions.push_back(Action::SignAndBroadcastBtcPunish); + self.state = State::Success; + } + _ => {} + }, + _ => {} + } + } + + fn poll(&mut self) -> Poll { + if let Some(action) = self.actions.pop_front() { + Poll::Ready(action) + } else { + Poll::Pending + } + } +} + +#[derive(PartialEq, Debug)] +pub enum State { + WatchingForBtcLock, + WatchingForXmrRedeem, + Punished, + Success, + Aborted, +} + +pub enum Event { + BtcLockSeenInMempool, + T2Elapsed, + BtcLockTimeoutElapsed, + XmrRedeemSeenInMempool, +} + +// These actions should not fail (are retried until successful) and should be +// idempotent This allows us to greatly simplify the state machine +pub enum Action { + WatchForXmrRedeem, + SignAndBroadcastBtcPunish, + SignAndBroadcastBtcRedeem, + BroadcastXmrRefund, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn happy_path() { + let mut state_machine = StateMachine { + state: State::WatchingForBtcLock, + actions: Default::default(), + events: Default::default(), + }; + state_machine.inject_event(Event::BtcLockSeenInMempool); + state_machine.inject_event(Event::XmrRedeemSeenInMempool); + assert_eq!(state_machine.state, State::Success); + } + + #[test] + fn bob_fails_to_lock_btc() { + let mut state_machine = StateMachine { + state: State::WatchingForBtcLock, + actions: Default::default(), + events: Default::default(), + }; + state_machine.events.push_back(Event::BtcLockTimeoutElapsed); + state_machine.poll(); + assert_eq!(state_machine.state, State::Aborted); + } + + #[test] + fn bob_fails_to_redeem_xmr_before_t2() { + let mut state_machine = StateMachine { + state: State::WatchingForBtcLock, + actions: Default::default(), + events: Default::default(), + }; + state_machine.events.push_back(Event::BtcLockSeenInMempool); + state_machine.events.push_back(Event::T2Elapsed); + state_machine.run(); + assert_eq!(state_machine.state, State::Punished); + } +} diff --git a/swap/src/xmr_first_protocol/state_machine/bob.rs b/swap/src/xmr_first_protocol/state_machine/bob.rs new file mode 100644 index 00000000..02c635c2 --- /dev/null +++ b/swap/src/xmr_first_protocol/state_machine/bob.rs @@ -0,0 +1,118 @@ +use std::collections::VecDeque; + +pub struct StateMachine { + state: State, + actions: VecDeque, + events: VecDeque, +} + +impl StateMachine { + fn next(&mut self, event: Event) { + match self.state { + State::WatchingForXmrLock => match event { + Event::XmrConfirmed => { + self.actions.push_back(Action::SignAndBroadcastBtcLock); + self.state = State::WaitingForBtcRedeem; + } + Event::T1Elapsed => { + self.state = State::Aborted; + } + Event::XmrRefundSeenInMempool => { + self.state = State::Aborted; + } + _ => panic!("unhandled scenario"), + }, + State::WaitingForBtcRedeem => match event { + Event::BtcRedeemSeenInMempool => { + self.actions.push_back(Action::BroadcastXmrRedeem); + self.state = State::Success; + } + Event::T1Elapsed => { + self.actions.push_back(Action::SignAndBroadcastBtcRefund); + self.state = State::Refunded; + } + Event::XmrRefundSeenInMempool => { + self.actions.push_back(Action::SignAndBroadcastBtcRefund); + self.state = State::Refunded; + } + _ => panic!("unhandled scenario"), + }, + _ => {} + } + } + + fn run(&mut self) { + while let Some(event) = self.events.pop_front() { + self.next(event); + } + } +} + +#[derive(PartialEq, Debug)] +pub enum State { + WatchingForXmrLock, + WaitingForBtcRedeem, + Success, + Refunded, + Aborted, +} + +pub enum Event { + XmrConfirmed, + // This will contain the s_a allowing bob to build xmr_redeem + BtcRedeemSeenInMempool, + XmrRefundSeenInMempool, + T1Elapsed, +} + +pub enum Action { + SignAndBroadcastBtcLock, + BroadcastXmrRedeem, + SignAndBroadcastBtcRefund, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn happy_path() { + let mut state_machine = StateMachine { + state: State::WatchingForXmrLock, + actions: Default::default(), + events: Default::default(), + }; + state_machine.events.push_back(Event::XmrConfirmed); + state_machine + .events + .push_back(Event::BtcRedeemSeenInMempool); + state_machine.run(); + assert_eq!(state_machine.state, State::Success); + } + + #[test] + fn alice_fails_to_redeem_btc_before_t1() { + let mut state_machine = StateMachine { + state: State::WatchingForXmrLock, + actions: Default::default(), + events: Default::default(), + }; + state_machine.events.push_back(Event::XmrConfirmed); + state_machine.events.push_back(Event::T1Elapsed); + state_machine.run(); + assert_eq!(state_machine.state, State::Refunded); + } + + #[test] + fn alice_tries_to_refund_xmr_after_redeeming_btc() { + let mut state_machine = StateMachine { + state: State::WatchingForXmrLock, + actions: Default::default(), + events: Default::default(), + }; + state_machine.events.push_back(Event::XmrConfirmed); + state_machine.events.push_back(Event::T1Elapsed); + state_machine.run(); + assert_eq!(state_machine.state, State::Refunded); + } +} diff --git a/swap/src/xmr_first_protocol/state_machine/mod.rs b/swap/src/xmr_first_protocol/state_machine/mod.rs new file mode 100644 index 00000000..74cf2eb7 --- /dev/null +++ b/swap/src/xmr_first_protocol/state_machine/mod.rs @@ -0,0 +1,2 @@ +// pub mod alice; +// pub mod bob; diff --git a/swap/src/xmr_first_protocol/transactions.rs b/swap/src/xmr_first_protocol/transactions.rs index 11924c5c..aca84b73 100644 --- a/swap/src/xmr_first_protocol/transactions.rs +++ b/swap/src/xmr_first_protocol/transactions.rs @@ -1,5 +1,5 @@ pub mod btc_lock; -pub mod btc_redeem; +// pub mod btc_redeem; pub mod xmr_lock; pub mod xmr_refund; @@ -8,174 +8,10 @@ use crate::bitcoin::{ build_shared_output_descriptor, Address, Amount, PartiallySignedTransaction, PublicKey, Transaction, Txid, Wallet, TX_FEE, }; -use anyhow::Result; +use anyhow::{bail, Result}; use bdk::bitcoin::{OutPoint, Script, TxIn, TxOut}; use bdk::database::BatchDatabase; use bdk::descriptor::Descriptor; use ecdsa_fun::fun::Point; use miniscript::DescriptorTrait; use rand::thread_rng; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct BtcLock { - inner: PartiallySignedTransaction, - pub(crate) output_descriptor: Descriptor<::bitcoin::PublicKey>, -} - -impl BtcLock { - 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()) - .expect("can derive address from descriptor"); - - let psbt = wallet.send_to_address(address, amount).await?; - - Ok(Self { - inner: psbt, - output_descriptor: lock_output_descriptor, - }) - } - - /// 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(BtcLock { - 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) - } - - pub fn txid(&self) -> Txid { - self.inner.clone().extract_tx().txid() - } - - pub fn as_outpoint(&self) -> OutPoint { - // This is fine because a transaction that has that many outputs is not - // realistic - #[allow(clippy::cast_possible_truncation)] - OutPoint::new(self.txid(), self.lock_output_vout() as u32) - } - - /// Calculate the size of the script used by this transaction. - pub fn script_size() -> usize { - build_shared_output_descriptor( - Point::random(&mut thread_rng()), - Point::random(&mut thread_rng()), - ) - .script_pubkey() - .len() - } - - pub fn script_pubkey(&self) -> Script { - self.output_descriptor.script_pubkey() - } - - /// Retreive the index of the locked output in the transaction outputs - /// vector - fn lock_output_vout(&self) -> usize { - self.inner - .clone() - .extract_tx() - .output - .iter() - .position(|output| output.script_pubkey == self.output_descriptor.script_pubkey()) - .expect("transaction contains lock output") - } - - pub fn build_spend_transaction( - &self, - spend_address: &Address, - sequence: Option, - ) -> Transaction { - let previous_output = self.as_outpoint(); - - let tx_in = TxIn { - previous_output, - script_sig: Default::default(), - sequence: sequence.unwrap_or(0xFFFF_FFFF), - witness: Vec::new(), - }; - - let tx_out = TxOut { - value: self.inner.clone().extract_tx().output[self.lock_output_vout()].value - TX_FEE, - script_pubkey: spend_address.script_pubkey(), - }; - - Transaction { - version: 2, - lock_time: 0, - input: vec![tx_in], - output: vec![tx_out], - } - } -} - -impl From for PartiallySignedTransaction { - fn from(from: BtcLock) -> Self { - from.inner - } -} - -impl Watchable for BtcLock { - fn id(&self) -> Txid { - self.txid() - } - - fn script(&self) -> Script { - self.output_descriptor.script_pubkey() - } -} diff --git a/swap/src/xmr_first_protocol/transactions/btc_lock.rs b/swap/src/xmr_first_protocol/transactions/btc_lock.rs index 9712eecb..c793b113 100644 --- a/swap/src/xmr_first_protocol/transactions/btc_lock.rs +++ b/swap/src/xmr_first_protocol/transactions/btc_lock.rs @@ -3,7 +3,7 @@ use crate::bitcoin::{ build_shared_output_descriptor, Address, Amount, PartiallySignedTransaction, PublicKey, Transaction, Txid, Wallet, TX_FEE, }; -use anyhow::Result; +use anyhow::{bail, Result}; use bdk::bitcoin::{OutPoint, Script, TxIn, TxOut}; use bdk::database::BatchDatabase; use bdk::descriptor::Descriptor; @@ -11,7 +11,7 @@ use ecdsa_fun::fun::Point; use miniscript::DescriptorTrait; use rand::thread_rng; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct BtcLock { inner: PartiallySignedTransaction, pub(crate) output_descriptor: Descriptor<::bitcoin::PublicKey>, @@ -27,7 +27,7 @@ impl BtcLock { where D: BatchDatabase, { - let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0); + let lock_output_descriptor = build_shared_output_descriptor(A.into(), B.into()); let address = lock_output_descriptor .address(wallet.get_network()) .expect("can derive address from descriptor"); @@ -77,7 +77,7 @@ impl BtcLock { } }; - let descriptor = build_shared_output_descriptor(A.0, B.0); + let descriptor = build_shared_output_descriptor(A.into(), B.into()); let legit_shared_output_script = descriptor.script_pubkey(); if shared_output_candidate.script_pubkey != legit_shared_output_script { diff --git a/swap/src/xmr_first_protocol/transactions/btc_redeem.rs b/swap/src/xmr_first_protocol/transactions/btc_redeem.rs index e69de29b..30512e0d 100644 --- a/swap/src/xmr_first_protocol/transactions/btc_redeem.rs +++ b/swap/src/xmr_first_protocol/transactions/btc_redeem.rs @@ -0,0 +1,146 @@ +use crate::bitcoin::wallet::Watchable; +use crate::bitcoin::{ + verify_encsig, verify_sig, Address, EmptyWitnessStack, EncryptedSignature, NoInputs, + NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, TxLock, +}; +use ::bitcoin::util::bip143::SigHashCache; +use ::bitcoin::{SigHash, SigHashType, Txid}; +use anyhow::{bail, Context, Result}; +use bitcoin::Script; +use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; +use ecdsa_fun::fun::Scalar; +use ecdsa_fun::nonce::Deterministic; +use ecdsa_fun::Signature; +use miniscript::{Descriptor, DescriptorTrait}; +use sha2::Sha256; +use std::collections::HashMap; + +#[derive(Clone, Debug)] +pub struct TxRedeem { + inner: Transaction, + digest: SigHash, + lock_output_descriptor: Descriptor<::bitcoin::PublicKey>, + watch_script: Script, +} + +impl TxRedeem { + pub fn new(tx_lock: &TxLock, redeem_address: &Address) -> Self { + // lock_input is the shared output that is now being used as an input for the + // redeem transaction + let tx_redeem = tx_lock.build_spend_transaction(redeem_address, None); + + let digest = SigHashCache::new(&tx_redeem).signature_hash( + 0, // Only one input: lock_input (lock transaction) + &tx_lock.output_descriptor.script_code(), + tx_lock.lock_amount().as_sat(), + SigHashType::All, + ); + + Self { + inner: tx_redeem, + digest, + lock_output_descriptor: tx_lock.output_descriptor.clone(), + watch_script: redeem_address.script_pubkey(), + } + } + + pub fn txid(&self) -> Txid { + self.inner.txid() + } + + pub fn digest(&self) -> SigHash { + self.digest + } + + pub fn complete( + mut self, + encrypted_signature: EncryptedSignature, + a: SecretKey, + s_a: Scalar, + B: PublicKey, + ) -> Result { + verify_encsig( + B, + PublicKey::from(s_a.clone()), + &self.digest(), + &encrypted_signature, + ) + .context("Invalid encrypted signature received")?; + + let sig_a = a.sign(self.digest()); + let adaptor = Adaptor::, Deterministic>::default(); + let sig_b = adaptor.decrypt_signature(&s_a, encrypted_signature); + + let satisfier = { + let mut satisfier = HashMap::with_capacity(2); + + let A = ::bitcoin::PublicKey { + compressed: true, + key: a.public().into(), + }; + let B = ::bitcoin::PublicKey { + compressed: true, + key: B.into(), + }; + + // The order in which these are inserted doesn't matter + satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All)); + satisfier.insert(B, (sig_b.into(), ::bitcoin::SigHashType::All)); + + satisfier + }; + + self.lock_output_descriptor + .satisfy(&mut self.inner.input[0], satisfier) + .context("Failed to sign Bitcoin redeem transaction")?; + + Ok(self.inner) + } + + pub fn extract_signature_by_key( + &self, + candidate_transaction: Transaction, + B: PublicKey, + ) -> Result { + let input = match candidate_transaction.input.as_slice() { + [input] => input, + [] => bail!(NoInputs), + [inputs @ ..] => bail!("too many inputs"), + }; + + let sigs = match input + .witness + .iter() + .map(|vec| vec.as_slice()) + .collect::>() + .as_slice() + { + [sig_1, sig_2, _script] => [sig_1, sig_2] + .iter() + .map(|sig| { + bitcoin::secp256k1::Signature::from_der(&sig[..sig.len() - 1]) + .map(Signature::from) + }) + .collect::, _>>(), + [] => bail!(EmptyWitnessStack), + [witnesses @ ..] => bail!("not three witnesses"), + }?; + + let sig = sigs + .into_iter() + .find(|sig| verify_sig(&B, &self.digest(), &sig).is_ok()) + .context("Neither signature on witness stack verifies against B")?; + + Ok(sig) + } +} + +impl Watchable for TxRedeem { + fn id(&self) -> Txid { + self.txid() + } + + fn script(&self) -> Script { + self.watch_script.clone() + } +} diff --git a/swap/src/xmr_first_protocol/transactions/xmr_refund.rs b/swap/src/xmr_first_protocol/transactions/xmr_refund.rs index 8c05caf2..a2c56a82 100644 --- a/swap/src/xmr_first_protocol/transactions/xmr_refund.rs +++ b/swap/src/xmr_first_protocol/transactions/xmr_refund.rs @@ -1,6 +1,26 @@ -use crate::monero::PrivateViewKey; +use crate::monero::TransferRequest; +use crate::xmr_first_protocol::alice::Alice4; +use anyhow::Result; use monero_adaptor::AdaptorSignature; -pub struct XmrRedeem { +pub struct XmrRefund { adaptor: AdaptorSignature, } + +impl XmrRefund { + pub async fn publish_xmr_refund(&self, wallet: &crate::monero::Wallet) -> Result<()> { + let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a }); + + let public_spend_key = S_a + self.S_b_monero; + let public_view_key = self.v_a.public(); + + let req = TransferRequest { + public_spend_key, + public_view_key, + amount: self.xmr_swap_amount, + }; + + let _ = wallet.transfer(req).await?; + Ok(()) + } +} diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index d30e8edd..ef7ca4e9 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -1,5 +1,5 @@ mod bitcoind; -mod electrs; +pub mod electrs; use crate::harness; use anyhow::{bail, Context, Result}; @@ -625,7 +625,7 @@ fn random_prefix() -> String { chars } -async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) { +pub async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) { let prefix = random_prefix(); let bitcoind_name = format!("{}_{}", prefix, "bitcoind"); let (bitcoind, bitcoind_url) = @@ -761,7 +761,7 @@ async fn init_monero_container( } #[allow(clippy::too_many_arguments)] -async fn init_test_wallets( +pub async fn init_test_wallets( name: &str, bitcoind_url: Url, monero: &Monero, @@ -835,10 +835,10 @@ async fn init_test_wallets( // This is just to keep the containers alive #[allow(dead_code)] struct Containers<'a> { - bitcoind_url: Url, + pub bitcoind_url: Url, bitcoind: Container<'a, Cli, bitcoind::Bitcoind>, monerods: Vec>, - electrs: Container<'a, Cli, electrs::Electrs>, + pub electrs: Container<'a, Cli, electrs::Electrs>, } pub mod alice_run_until { diff --git a/swap/tests/xmr_first_bob_fails_to_act_alice_refunds.rs b/swap/tests/xmr_first_bob_fails_to_act_alice_refunds.rs new file mode 100644 index 00000000..aebc24aa --- /dev/null +++ b/swap/tests/xmr_first_bob_fails_to_act_alice_refunds.rs @@ -0,0 +1,110 @@ +pub mod harness; + +use rand::rngs::OsRng; +use swap::bitcoin::TxLock; +use swap::env::GetConfig; +use swap::monero; +use swap::protocol::alice::event_loop::FixedRate; +use swap::protocol::CROSS_CURVE_PROOF_SYSTEM; +use swap::seed::Seed; +use swap::xmr_first_protocol::alice::{publish_xmr_refund, Alice3}; +use swap::xmr_first_protocol::bob::Bob3; +use tempfile::tempdir; +use testcontainers::clients::Cli; + +#[tokio::test] +async fn refund() { + let cli = Cli::default(); + + let env_config = harness::SlowCancelConfig::get_config(); + + let (monero, containers) = harness::init_containers(&cli).await; + + let btc_swap_amount = bitcoin::Amount::from_sat(1_000_000); + let xmr_swap_amount = + monero::Amount::from_monero(btc_swap_amount.as_btc() / FixedRate::RATE).unwrap(); + + let alice_starting_balances = harness::StartingBalances { + xmr: xmr_swap_amount * 10, + btc: bitcoin::Amount::ZERO, + }; + + let electrs_rpc_port = containers + .electrs + .get_host_port(harness::electrs::RPC_PORT) + .expect("Could not map electrs rpc port"); + + let alice_seed = Seed::random().unwrap(); + let (alice_bitcoin_wallet, alice_monero_wallet) = harness::init_test_wallets( + "Alice", + containers.bitcoind_url.clone(), + &monero, + alice_starting_balances.clone(), + tempdir().unwrap().path(), + electrs_rpc_port, + &alice_seed, + env_config.clone(), + ) + .await; + + let bob_seed = Seed::random().unwrap(); + let bob_starting_balances = harness::StartingBalances { + xmr: monero::Amount::ZERO, + btc: btc_swap_amount * 10, + }; + + let (bob_bitcoin_wallet, bob_monero_wallet) = harness::init_test_wallets( + "Bob", + containers.bitcoind_url, + &monero, + bob_starting_balances.clone(), + tempdir().unwrap().path(), + electrs_rpc_port, + &bob_seed, + env_config, + ) + .await; + + let a = swap::bitcoin::SecretKey::new_random(&mut OsRng); + let b = swap::bitcoin::SecretKey::new_random(&mut OsRng); + + let s_a = monero::Scalar::random(&mut OsRng); + let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: s_a }); + + let s_b = monero::Scalar::random(&mut OsRng); + let S_b = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: s_b }); + + let (dleq_proof_s_b, (S_b_bitcoin, S_b_monero)) = + CROSS_CURVE_PROOF_SYSTEM.prove(&s_b, &mut OsRng); + + let v_a = monero::PrivateViewKey::new_random(&mut OsRng); + let v_b = monero::PrivateViewKey::new_random(&mut OsRng); + + let tx_lock = TxLock::new(&bob_bitcoin_wallet, btc_swap_amount, a.public(), b.public()).await?; + + let alice = Alice3 { + xmr_swap_amount, + btc_swap_amount, + a, + B: b.public(), + s_a, + S_b_monero: monero::PublicKey { + point: S_b_monero.compress(), + }, + v_a, + redeem_address: alice_bitcoin_wallet.new_address().await?, + }; + + let bob = Bob3 { + xmr_swap_amount, + btc_swap_amount, + tx_lock, + S: S_b, + v_b, + alice_redeem_address: bob_bitcoin_wallet.new_address().await?, + }; + + let alice = alice.publish_xmr_lock(&alice_monero_wallet).await.unwrap(); + + publish_xmr_refund(&alice_bitcoin_wallet).await.unwrap(); +} diff --git a/swap/tests/xmr_first_happy_path.rs b/swap/tests/xmr_first_happy_path.rs new file mode 100644 index 00000000..29366c35 --- /dev/null +++ b/swap/tests/xmr_first_happy_path.rs @@ -0,0 +1,109 @@ +pub mod harness; + +use curve25519_dalek::constants::ED25519_BASEPOINT_POINT; +use curve25519_dalek::edwards::EdwardsPoint; +use monero_adaptor::alice::Alice0; +use monero_adaptor::bob::Bob0; +use rand::rngs::OsRng; +use swap::env::GetConfig; +use swap::monero; +use swap::monero::{PublicKey, Scalar}; +use swap::protocol::alice::event_loop::FixedRate; +use swap::protocol::CROSS_CURVE_PROOF_SYSTEM; +use swap::seed::Seed; +use swap::xmr_first_protocol::alice::Alice3; +use swap::xmr_first_protocol::bob::Bob3; +use swap::xmr_first_protocol::{alice, bob}; +use tempfile::tempdir; +use testcontainers::clients::Cli; + +#[tokio::test] +async fn happy_path() { + let cli = Cli::default(); + + let env_config = harness::SlowCancelConfig::get_config(); + + 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(); + + let alice_starting_balances = harness::StartingBalances { + xmr: xmr_amount * 10, + btc: bitcoin::Amount::ZERO, + }; + + let electrs_rpc_port = containers + .electrs + .get_host_port(harness::electrs::RPC_PORT) + .expect("Could not map electrs rpc port"); + + let alice_seed = Seed::random().unwrap(); + let (alice_bitcoin_wallet, alice_monero_wallet) = harness::init_test_wallets( + "Alice", + containers.bitcoind_url.clone(), + &monero, + alice_starting_balances.clone(), + tempdir().unwrap().path(), + electrs_rpc_port, + &alice_seed, + env_config.clone(), + ) + .await; + + let bob_seed = Seed::random().unwrap(); + let bob_starting_balances = harness::StartingBalances { + xmr: monero::Amount::ZERO, + btc: btc_amount * 10, + }; + + let (bob_bitcoin_wallet, bob_monero_wallet) = harness::init_test_wallets( + "Bob", + containers.bitcoind_url, + &monero, + bob_starting_balances.clone(), + tempdir().unwrap().path(), + electrs_rpc_port, + &bob_seed, + env_config, + ) + .await; + + let a = crate::bitcoin::SecretKey::new_random(rng); + let b = crate::bitcoin::SecretKey::new_random(rng); + + let s_a = monero::Scalar::random(rng); + let s_b = monero::Scalar::random(rng); + + let (dleq_proof_s_b, (S_b_bitcoin, S_b_monero)) = CROSS_CURVE_PROOF_SYSTEM.prove(&s_b, rng); + + let v_a = monero::PrivateViewKey::new_random(rng); + let v_b = monero::PrivateViewKey::new_random(rng); + + let alice = Alice3 { + xmr_swap_amount: xmr_amount, + btc_swap_amount: btc_amount, + a, + B: b.public(), + s_a, + S_b_monero, + v_a, + }; + + let bob = Bob3 { + xmr_swap_amount, + btc_swap_amount, + xmr_lock, + v_b, + }; + + alice.publish_xmr_lock(&alice_monero_wallet).await.unwrap(); + + bob.watch_for_lock_xmr(&bob_monero_wallet_wallet) + .await + .unwrap(); + + alice.publish_btc_redeem(&alice_btc_wallet).await.unwrap(); + + bob.publish_xmr_redeem(&alice_monero_wallet).await.unwrap(); +}