Make Alice watch for Monero lock transaction without transfer proof

This commit is contained in:
Lucas Soriano del Pino 2020-11-04 17:06:17 +11:00 committed by rishflab
parent 7371dfb055
commit 0288e004c5
6 changed files with 126 additions and 20 deletions

View File

@ -61,9 +61,10 @@ pub trait ReceiveBitcoinRedeemEncsig {
/// ///
/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will /// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will
/// wait for Bob, the counterparty, to lock up the bitcoin. /// wait for Bob, the counterparty, to lock up the bitcoin.
pub fn action_generator<N, B>( pub fn action_generator<N, B, M>(
network: Arc<Mutex<N>>, network: Arc<Mutex<N>>,
bitcoin_client: Arc<B>, bitcoin_client: Arc<B>,
monero_client: Arc<M>,
// TODO: Replace this with a new, slimmer struct? // TODO: Replace this with a new, slimmer struct?
State3 { State3 {
a, a,
@ -93,10 +94,13 @@ where
+ Send + Send
+ Sync + Sync
+ 'static, + 'static,
M: monero::WatchForTransferImproved + Send + Sync + 'static,
{ {
#[allow(clippy::enum_variant_names)]
#[derive(Debug)] #[derive(Debug)]
enum SwapFailed { enum SwapFailed {
BeforeBtcLock(Reason), BeforeBtcLock(Reason),
AfterBtcLock(Reason),
AfterXmrLock(Reason), AfterXmrLock(Reason),
} }
@ -146,15 +150,30 @@ where
scalar: s_a.into_ed25519(), scalar: s_a.into_ed25519(),
}); });
let public_spend_key = S_a + S_b_monero;
let public_view_key = v.public();
co.yield_(Action::LockXmr { co.yield_(Action::LockXmr {
amount: xmr, amount: xmr,
public_spend_key: S_a + S_b_monero, public_spend_key,
public_view_key: v.public(), public_view_key,
}) })
.await; .await;
// TODO: Watch for LockXmr using watch-only wallet. Doing so will prevent Alice let monero_joint_address = monero::Address::standard(
// from cancelling/refunding unnecessarily. 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 tx_redeem_encsig = {
let mut guard = network.as_ref().lock().await; let mut guard = network.as_ref().lock().await;

View File

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use std::ops::{Add, Sub}; use std::ops::{Add, Sub};
pub use curve25519_dalek::scalar::Scalar; pub use curve25519_dalek::scalar::Scalar;
pub use monero::*; pub use monero::{Address, Network, PrivateKey, PublicKey};
pub const MIN_CONFIRMATIONS: u32 = 10; pub const MIN_CONFIRMATIONS: u32 = 10;
@ -154,6 +154,16 @@ pub trait WatchForTransfer {
) -> Result<(), InsufficientFunds>; ) -> 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)] #[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")] #[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")]
pub struct InsufficientFunds { pub struct InsufficientFunds {

View File

@ -22,9 +22,11 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn happy_path() { async fn happy_path() {
let cli = Cli::default(); 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".to_string(),
"alice-watch-only".to_string(),
"bob".to_string(), "bob".to_string(),
"bob-watch-only".to_string(),
]) ])
.await .await
.unwrap(); .unwrap();
@ -97,9 +99,11 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn both_refund() { async fn both_refund() {
let cli = Cli::default(); 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".to_string(),
"alice-watch-only".to_string(),
"bob".to_string(), "bob".to_string(),
"bob-watch-only".to_string(),
]) ])
.await .await
.unwrap(); .unwrap();
@ -174,9 +178,11 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn alice_punishes() { async fn alice_punishes() {
let cli = Cli::default(); 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".to_string(),
"alice-watch-only".to_string(),
"bob".to_string(), "bob".to_string(),
"bob-watch-only".to_string(),
]) ])
.await .await
.unwrap(); .unwrap();

View File

@ -135,8 +135,14 @@ pub async fn init_test(
.await .await
.unwrap(); .unwrap();
let alice_monero_wallet = wallet::monero::Wallet(monero.wallet("alice").unwrap().client()); let alice_monero_wallet = wallet::monero::Wallet {
let bob_monero_wallet = wallet::monero::Wallet(monero.wallet("bob").unwrap().client()); 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) let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url)
.await .await

View File

@ -1,19 +1,26 @@
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use futures::TryFutureExt;
use monero::{Address, Network, PrivateKey};
use monero_harness::rpc::wallet; use monero_harness::rpc::wallet;
use std::{str::FromStr, time::Duration}; use std::{str::FromStr, time::Duration};
use xmr_btc::monero::{ use xmr_btc::monero::{
Address, Amount, CreateWalletForOutput, InsufficientFunds, Network, PrivateKey, PrivateViewKey, 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 { impl Wallet {
/// Get the balance of the primary account. /// Get the balance of the primary account.
pub async fn get_balance(&self) -> Result<Amount> { pub async fn get_balance(&self) -> Result<Amount> {
let amount = self.0.get_balance(0).await?; let amount = self.inner.get_balance(0).await?;
Ok(Amount::from_piconero(amount)) Ok(Amount::from_piconero(amount))
} }
@ -31,7 +38,7 @@ impl Transfer for Wallet {
Address::standard(Network::Mainnet, public_spend_key, public_view_key.into()); Address::standard(Network::Mainnet, public_spend_key, public_view_key.into());
let res = self let res = self
.0 .inner
.transfer(0, amount.as_piconero(), &destination_address.to_string()) .transfer(0, amount.as_piconero(), &destination_address.to_string())
.await?; .await?;
@ -57,7 +64,7 @@ impl CreateWalletForOutput for Wallet {
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key); let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key);
let _ = self let _ = self
.0 .inner
.generate_from_keys( .generate_from_keys(
&address.to_string(), &address.to_string(),
Some(&private_spend_key.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 // 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 // errors warrant a retry, but the strategy should probably differ per case
let proof = self let proof = self
.0 .inner
.check_tx_key( .check_tx_key(
&String::from(transfer_proof.tx_hash()), &String::from(transfer_proof.tx_hash()),
&transfer_proof.tx_key().to_string(), &transfer_proof.tx_key().to_string(),
@ -124,3 +131,54 @@ impl WatchForTransfer for Wallet {
Ok(()) 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(())
}
}

View File

@ -113,6 +113,7 @@ async fn swap_as_alice(
let mut action_generator = alice::action_generator( let mut action_generator = alice::action_generator(
network, network,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
monero_wallet.clone(),
state, state,
BITCOIN_TX_LOCK_TIMEOUT, BITCOIN_TX_LOCK_TIMEOUT,
); );
@ -233,9 +234,11 @@ async fn swap_as_bob(
#[tokio::test] #[tokio::test]
async fn on_chain_happy_path() { async fn on_chain_happy_path() {
let cli = Cli::default(); 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".to_string(),
"alice-watch-only".to_string(),
"bob".to_string(), "bob".to_string(),
"bob-watch-only".to_string(),
]) ])
.await .await
.unwrap(); .unwrap();
@ -328,9 +331,11 @@ async fn on_chain_happy_path() {
#[tokio::test] #[tokio::test]
async fn on_chain_both_refund_if_alice_never_redeems() { async fn on_chain_both_refund_if_alice_never_redeems() {
let cli = Cli::default(); 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".to_string(),
"alice-watch-only".to_string(),
"bob".to_string(), "bob".to_string(),
"bob-watch-only".to_string(),
]) ])
.await .await
.unwrap(); .unwrap();
@ -423,9 +428,11 @@ async fn on_chain_both_refund_if_alice_never_redeems() {
#[tokio::test] #[tokio::test]
async fn on_chain_alice_punishes_if_bob_never_acts_after_fund() { async fn on_chain_alice_punishes_if_bob_never_acts_after_fund() {
let cli = Cli::default(); 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".to_string(),
"alice-watch-only".to_string(),
"bob".to_string(), "bob".to_string(),
"bob-watch-only".to_string(),
]) ])
.await .await
.unwrap(); .unwrap();