From 0288e004c5df164970a6370846dc8be6968f0137 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Wed, 4 Nov 2020 17:06:17 +1100 Subject: [PATCH] Make Alice watch for Monero lock transaction without transfer proof --- xmr-btc/src/alice.rs | 29 +++++++++-- xmr-btc/src/monero.rs | 12 ++++- xmr-btc/tests/e2e.rs | 12 +++-- xmr-btc/tests/harness/mod.rs | 10 +++- xmr-btc/tests/harness/wallet/monero.rs | 70 +++++++++++++++++++++++--- xmr-btc/tests/on_chain.rs | 13 +++-- 6 files changed, 126 insertions(+), 20 deletions(-) diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index fa35a140..a65a4ab1 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -61,9 +61,10 @@ pub trait ReceiveBitcoinRedeemEncsig { /// /// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will /// wait for Bob, the counterparty, to lock up the bitcoin. -pub fn action_generator( +pub fn action_generator( network: Arc>, bitcoin_client: Arc, + monero_client: Arc, // TODO: Replace this with a new, slimmer struct? State3 { a, @@ -93,10 +94,13 @@ where + Send + Sync + 'static, + M: monero::WatchForTransferImproved + Send + Sync + 'static, { + #[allow(clippy::enum_variant_names)] #[derive(Debug)] enum SwapFailed { BeforeBtcLock(Reason), + AfterBtcLock(Reason), AfterXmrLock(Reason), } @@ -146,15 +150,30 @@ where scalar: s_a.into_ed25519(), }); + let public_spend_key = S_a + S_b_monero; + let public_view_key = v.public(); + co.yield_(Action::LockXmr { amount: xmr, - public_spend_key: S_a + S_b_monero, - public_view_key: v.public(), + public_spend_key, + public_view_key, }) .await; - // TODO: Watch for LockXmr using watch-only wallet. Doing so will prevent Alice - // from cancelling/refunding unnecessarily. + let monero_joint_address = monero::Address::standard( + monero::Network::Mainnet, + public_spend_key, + public_view_key.into(), + ); + + if let Either::Right(_) = select( + monero_client.watch_for_transfer_improved(monero_joint_address, xmr, v), + poll_until_btc_has_expired.clone(), + ) + .await + { + return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)); + }; let tx_redeem_encsig = { let mut guard = network.as_ref().lock().await; diff --git a/xmr-btc/src/monero.rs b/xmr-btc/src/monero.rs index 643c4d32..114a91f5 100644 --- a/xmr-btc/src/monero.rs +++ b/xmr-btc/src/monero.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use std::ops::{Add, Sub}; pub use curve25519_dalek::scalar::Scalar; -pub use monero::*; +pub use monero::{Address, Network, PrivateKey, PublicKey}; pub const MIN_CONFIRMATIONS: u32 = 10; @@ -154,6 +154,16 @@ pub trait WatchForTransfer { ) -> Result<(), InsufficientFunds>; } +#[async_trait] +pub trait WatchForTransferImproved { + async fn watch_for_transfer_improved( + &self, + address: Address, + amount: Amount, + private_view_key: PrivateViewKey, + ) -> Result<(), InsufficientFunds>; +} + #[derive(Debug, Clone, Copy, thiserror::Error)] #[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")] pub struct InsufficientFunds { diff --git a/xmr-btc/tests/e2e.rs b/xmr-btc/tests/e2e.rs index 373c759e..f29f2088 100644 --- a/xmr-btc/tests/e2e.rs +++ b/xmr-btc/tests/e2e.rs @@ -22,9 +22,11 @@ mod tests { #[tokio::test] async fn happy_path() { let cli = Cli::default(); - let (monero, _container) = Monero::new(&cli, Some("hp".to_string()), vec![ + let (monero, _container) = Monero::new(&cli, None, vec![ "alice".to_string(), + "alice-watch-only".to_string(), "bob".to_string(), + "bob-watch-only".to_string(), ]) .await .unwrap(); @@ -97,9 +99,11 @@ mod tests { #[tokio::test] async fn both_refund() { let cli = Cli::default(); - let (monero, _container) = Monero::new(&cli, Some("br".to_string()), vec![ + let (monero, _container) = Monero::new(&cli, None, vec![ "alice".to_string(), + "alice-watch-only".to_string(), "bob".to_string(), + "bob-watch-only".to_string(), ]) .await .unwrap(); @@ -174,9 +178,11 @@ mod tests { #[tokio::test] async fn alice_punishes() { let cli = Cli::default(); - let (monero, _containers) = Monero::new(&cli, Some("ap".to_string()), vec![ + let (monero, _container) = Monero::new(&cli, None, vec![ "alice".to_string(), + "alice-watch-only".to_string(), "bob".to_string(), + "bob-watch-only".to_string(), ]) .await .unwrap(); diff --git a/xmr-btc/tests/harness/mod.rs b/xmr-btc/tests/harness/mod.rs index 3d789b8f..754d1efc 100644 --- a/xmr-btc/tests/harness/mod.rs +++ b/xmr-btc/tests/harness/mod.rs @@ -135,8 +135,14 @@ pub async fn init_test( .await .unwrap(); - let alice_monero_wallet = wallet::monero::Wallet(monero.wallet("alice").unwrap().client()); - let bob_monero_wallet = wallet::monero::Wallet(monero.wallet("bob").unwrap().client()); + let alice_monero_wallet = wallet::monero::Wallet { + inner: monero.wallet("alice").unwrap().client(), + watch_only: monero.wallet("alice-watch-only").unwrap().client(), + }; + let bob_monero_wallet = wallet::monero::Wallet { + inner: monero.wallet("bob").unwrap().client(), + watch_only: monero.wallet("bob-watch-only").unwrap().client(), + }; let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url) .await diff --git a/xmr-btc/tests/harness/wallet/monero.rs b/xmr-btc/tests/harness/wallet/monero.rs index 60b8ff79..302edc2d 100644 --- a/xmr-btc/tests/harness/wallet/monero.rs +++ b/xmr-btc/tests/harness/wallet/monero.rs @@ -1,19 +1,26 @@ use anyhow::Result; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; +use futures::TryFutureExt; +use monero::{Address, Network, PrivateKey}; use monero_harness::rpc::wallet; use std::{str::FromStr, time::Duration}; use xmr_btc::monero::{ Address, Amount, CreateWalletForOutput, InsufficientFunds, Network, PrivateKey, PrivateViewKey, - PublicKey, PublicViewKey, Transfer, TransferProof, TxHash, WatchForTransfer, + PublicKey, PublicViewKey, Transfer, TransferProof, TxHash, WatchForTransfer, WatchForTransferImproved, }; -pub struct Wallet(pub wallet::Client); +pub struct Wallet { + pub inner: wallet::Client, + /// Secondary wallet which is only used to watch for the Monero lock + /// transaction without needing a transfer proof. + pub watch_only: wallet::Client, +} impl Wallet { /// Get the balance of the primary account. pub async fn get_balance(&self) -> Result { - let amount = self.0.get_balance(0).await?; + let amount = self.inner.get_balance(0).await?; Ok(Amount::from_piconero(amount)) } @@ -31,7 +38,7 @@ impl Transfer for Wallet { Address::standard(Network::Mainnet, public_spend_key, public_view_key.into()); let res = self - .0 + .inner .transfer(0, amount.as_piconero(), &destination_address.to_string()) .await?; @@ -57,7 +64,7 @@ impl CreateWalletForOutput for Wallet { let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key); let _ = self - .0 + .inner .generate_from_keys( &address.to_string(), Some(&private_spend_key.to_string()), @@ -92,7 +99,7 @@ impl WatchForTransfer for Wallet { // in the blockchain yet, or not having enough confirmations on it. All these // errors warrant a retry, but the strategy should probably differ per case let proof = self - .0 + .inner .check_tx_key( &String::from(transfer_proof.tx_hash()), &transfer_proof.tx_key().to_string(), @@ -124,3 +131,54 @@ impl WatchForTransfer for Wallet { Ok(()) } } + +#[async_trait] +impl WatchForTransferImproved for Wallet { + async fn watch_for_transfer_improved( + &self, + address: Address, + expected_amount: Amount, + private_view_key: PrivateViewKey, + ) -> Result<(), InsufficientFunds> { + let address = address.to_string(); + let private_view_key = PrivateKey::from(private_view_key).to_string(); + let load_address = || { + self.watch_only + .generate_from_keys(&address, None, &private_view_key) + .map_err(backoff::Error::Transient) + }; + + load_address + .retry(ConstantBackoff::new(Duration::from_secs(1))) + .await + .expect("transient error is never returned"); + + let refresh = || self.watch_only.refresh().map_err(backoff::Error::Transient); + + refresh + .retry(ConstantBackoff::new(Duration::from_secs(1))) + .await + .expect("transient error is never returned"); + + let get_balance = || { + self.watch_only + .get_balance(0) + .map_err(backoff::Error::Transient) + }; + + let balance = get_balance + .retry(ConstantBackoff::new(Duration::from_secs(1))) + .await + .expect("transient error is never returned"); + let balance = Amount::from_piconero(balance); + + if balance != expected_amount { + return Err(InsufficientFunds { + expected: expected_amount, + actual: balance, + }); + } + + Ok(()) + } +} diff --git a/xmr-btc/tests/on_chain.rs b/xmr-btc/tests/on_chain.rs index 459b368e..615fa323 100644 --- a/xmr-btc/tests/on_chain.rs +++ b/xmr-btc/tests/on_chain.rs @@ -113,6 +113,7 @@ async fn swap_as_alice( let mut action_generator = alice::action_generator( network, bitcoin_wallet.clone(), + monero_wallet.clone(), state, BITCOIN_TX_LOCK_TIMEOUT, ); @@ -233,9 +234,11 @@ async fn swap_as_bob( #[tokio::test] async fn on_chain_happy_path() { let cli = Cli::default(); - let (monero, _container) = Monero::new(&cli, Some("ochp".to_string()), vec![ + let (monero, _container) = Monero::new(&cli, None, vec![ "alice".to_string(), + "alice-watch-only".to_string(), "bob".to_string(), + "bob-watch-only".to_string(), ]) .await .unwrap(); @@ -328,9 +331,11 @@ async fn on_chain_happy_path() { #[tokio::test] async fn on_chain_both_refund_if_alice_never_redeems() { let cli = Cli::default(); - let (monero, _container) = Monero::new(&cli, Some("ocbr".to_string()), vec![ + let (monero, _container) = Monero::new(&cli, None, vec![ "alice".to_string(), + "alice-watch-only".to_string(), "bob".to_string(), + "bob-watch-only".to_string(), ]) .await .unwrap(); @@ -423,9 +428,11 @@ async fn on_chain_both_refund_if_alice_never_redeems() { #[tokio::test] async fn on_chain_alice_punishes_if_bob_never_acts_after_fund() { let cli = Cli::default(); - let (monero, _container) = Monero::new(&cli, Some("ocap".to_string()), vec![ + let (monero, _container) = Monero::new(&cli, None, vec![ "alice".to_string(), + "alice-watch-only".to_string(), "bob".to_string(), + "bob-watch-only".to_string(), ]) .await .unwrap();