From 45b5af4375d704dae937f7eaf6be7ba7900ac87b Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Wed, 6 Jan 2021 16:12:35 +1100 Subject: [PATCH] WIP - use get_transfer_by_txid to extract the exact block height of tx_lock This failed on stagenet because somehow the transaction cannot be found with the given tx_hash. This might be because tx are not indexed or other problem, hard to say at this point. --- monero-harness/src/rpc/wallet.rs | 44 ++++- swap/src/alice/swap.rs | 2 +- swap/src/monero.rs | 39 +++- xmr-btc/src/alice.rs | 3 +- xmr-btc/src/bob.rs | 303 ++----------------------------- xmr-btc/src/lib.rs | 1 - xmr-btc/src/monero.rs | 16 +- 7 files changed, 107 insertions(+), 301 deletions(-) diff --git a/monero-harness/src/rpc/wallet.rs b/monero-harness/src/rpc/wallet.rs index 97c2804d..88d11c0a 100644 --- a/monero-harness/src/rpc/wallet.rs +++ b/monero-harness/src/rpc/wallet.rs @@ -186,7 +186,7 @@ impl Client { } /// Get wallet block height, this might be behind monerod height. - pub(crate) async fn block_height(&self) -> Result { + pub async fn block_height(&self) -> Result { let request = Request::new("get_height", ""); let response = self @@ -233,14 +233,43 @@ impl Client { Ok(r.result) } + pub async fn get_transfer_by_txid(&self, tx_id: &str) -> Result { + let params = GetTransferByTxidParams { + tx_id: tx_id.to_owned(), + }; + + let request = Request::new("get_transfer_by_txid", params); + + let response = self + .inner + .post(self.url.clone()) + .json(&request) + .send() + .await? + .text() + .await?; + + debug!("transfer RPC response: {}", response); + + let r: Response = serde_json::from_str(&response)?; + Ok(r.result) + } + pub async fn generate_from_keys( &self, address: &str, spend_key: &str, view_key: &str, + restore_height: Option, ) -> Result { + let restore_height = if let Some(restore_height) = restore_height { + restore_height + } else { + 0 + }; + let params = GenerateFromKeysParams { - restore_height: 0, + restore_height, filename: view_key.into(), address: address.into(), spendkey: spend_key.into(), @@ -395,6 +424,17 @@ pub struct CheckTxKey { pub received: u64, } +#[derive(Serialize, Debug, Clone)] +struct GetTransferByTxidParams { + #[serde(rename = "txid")] + tx_id: String, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +pub struct GetTransferByTxid { + pub height: u32, +} + #[derive(Clone, Debug, Serialize)] pub struct GenerateFromKeysParams { pub restore_height: u32, diff --git a/swap/src/alice/swap.rs b/swap/src/alice/swap.rs index d77f2187..345d946e 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/alice/swap.rs @@ -474,7 +474,7 @@ pub async fn run_until( let view_key = state3.v; monero_wallet - .create_and_load_wallet_for_output(spend_key, view_key) + .create_and_load_wallet_for_output(spend_key, view_key, None) .await?; let state = AliceState::XmrRefunded; diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 51111003..232c490a 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -64,6 +64,7 @@ impl CreateWalletForOutput for Wallet { &self, private_spend_key: PrivateKey, private_view_key: PrivateViewKey, + restore_height: Option, ) -> Result<()> { let public_spend_key = PublicKey::from_private_key(&private_spend_key); let public_view_key = PublicKey::from_private_key(&private_view_key.into()); @@ -76,6 +77,7 @@ impl CreateWalletForOutput for Wallet { &address.to_string(), &private_spend_key.to_string(), &PrivateKey::from(private_view_key).to_string(), + restore_height, ) .await?; @@ -95,16 +97,17 @@ impl WatchForTransfer for Wallet { transfer_proof: TransferProof, expected_amount: Amount, expected_confirmations: u32, - ) -> Result<(), InsufficientFunds> { + ) -> Result { enum Error { TxNotFound, + TransferNotFound { txid: String }, InsufficientConfirmations, InsufficientFunds { expected: Amount, actual: Amount }, } let address = Address::standard(self.network, public_spend_key, public_view_key.into()); - let res = (|| async { + let result = (|| async { // NOTE: Currently, this is conflating IO errors with the transaction not being // 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 @@ -129,15 +132,35 @@ impl WatchForTransfer for Wallet { return Err(backoff::Error::Transient(Error::InsufficientConfirmations)); } - Ok(proof) + let tx_hash = transfer_proof.tx_hash(); + let get_transfer_by_tx_id = + self.inner + .get_transfer_by_txid(&tx_hash.0) + .await + .map_err(|_| { + backoff::Error::Permanent(Error::TransferNotFound { txid: tx_hash.0 }) + })?; + let transfer_info = TransferInfo { + first_confirmation_block_height: get_transfer_by_tx_id.height, + }; + + Ok((proof, transfer_info)) }) .retry(ConstantBackoff::new(Duration::from_secs(1))) .await; - if let Err(Error::InsufficientFunds { expected, actual }) = res { - return Err(InsufficientFunds { expected, actual }); - }; - - Ok(()) + match result { + Ok((_, transfer_info)) => Ok(transfer_info), + Err(Error::InsufficientFunds { expected, actual }) => { + anyhow::bail!(InsufficientFunds { expected, actual }) + } + Err(Error::TransferNotFound { txid }) => anyhow::bail!(TransferNotFound { txid }), + Err(Error::TxNotFound) => { + unreachable!("Transient backoff error will never be returned") + } + Err(Error::InsufficientConfirmations) => { + unreachable!("Transient backoff error will never be returned") + } + } } } diff --git a/xmr-btc/src/alice.rs b/xmr-btc/src/alice.rs index ce52a320..2bd27428 100644 --- a/xmr-btc/src/alice.rs +++ b/xmr-btc/src/alice.rs @@ -884,10 +884,11 @@ impl State5 { let s = s_b.scalar + self.s_a.into_ed25519(); + // TODO: Optimized rescan height should be passed for refund as well. // NOTE: This actually generates and opens a new wallet, closing the currently // open one. monero_wallet - .create_and_load_wallet_for_output(monero::PrivateKey::from_scalar(s), self.v) + .create_and_load_wallet_for_output(monero::PrivateKey::from_scalar(s), self.v, None) .await?; Ok(()) diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 696d3cbd..b0711ed1 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -1,12 +1,10 @@ use crate::{ alice, bitcoin::{ - self, poll_until_block_height_is_gte, BroadcastSignedTransaction, BuildTxLockPsbt, - SignTxLock, TxCancel, WatchForRawTransaction, + self, BroadcastSignedTransaction, BuildTxLockPsbt, TxCancel, WatchForRawTransaction, }, monero, serde::monero_private_key, - transport::{ReceiveMessage, SendMessage}, ExpiredTimelocks, }; use anyhow::{anyhow, Result}; @@ -16,29 +14,15 @@ use ecdsa_fun::{ nonce::Deterministic, Signature, }; -use futures::{ - future::{select, Either}, - pin_mut, FutureExt, -}; -use genawaiter::sync::{Gen, GenBoxed}; use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use std::{ - convert::{TryFrom, TryInto}, - sync::Arc, - time::Duration, -}; -use tokio::{sync::Mutex, time::timeout}; -use tracing::error; +use std::convert::TryFrom; pub mod message; -use crate::{ - bitcoin::{ - current_epoch, wait_for_cancel_timelock_to_expire, GetBlockHeight, GetRawTransaction, - Network, Timelock, TransactionBlockHeight, - }, - monero::{CreateWalletForOutput, WatchForTransfer}, +use crate::bitcoin::{ + current_epoch, wait_for_cancel_timelock_to_expire, GetBlockHeight, GetRawTransaction, Network, + Timelock, TransactionBlockHeight, }; use ::bitcoin::{Transaction, Txid}; pub use message::{Message, Message0, Message1, Message2, Message3}; @@ -61,269 +45,6 @@ pub trait ReceiveTransferProof { async fn receive_transfer_proof(&mut self) -> monero::TransferProof; } -/// Perform the on-chain protocol to swap monero and bitcoin as Bob. -/// -/// This is called post handshake, after all the keys, addresses and most of the -/// signatures have been exchanged. -/// -/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will -/// wait for Bob, the caller of this function, to lock up the bitcoin. -pub fn action_generator( - network: Arc>, - monero_client: Arc, - bitcoin_client: Arc, - // TODO: Replace this with a new, slimmer struct? - State2 { - A, - b, - s_b, - S_a_monero, - S_a_bitcoin, - v, - xmr, - cancel_timelock, - redeem_address, - refund_address, - tx_lock, - tx_cancel_sig_a, - tx_refund_encsig, - .. - }: State2, - bitcoin_tx_lock_timeout: u64, -) -> GenBoxed -where - N: ReceiveTransferProof + Send + 'static, - M: monero::WatchForTransfer + Send + Sync + 'static, - B: bitcoin::GetBlockHeight - + bitcoin::TransactionBlockHeight - + bitcoin::WatchForRawTransaction - + Send - + Sync - + 'static, -{ - #[derive(Debug)] - enum SwapFailed { - BeforeBtcLock(Reason), - AfterBtcLock(Reason), - AfterBtcRedeem(Reason), - } - - /// Reason why the swap has failed. - #[derive(Debug)] - enum Reason { - /// Bob was too slow to lock the bitcoin. - InactiveBob, - /// The refund timelock has been reached. - BtcExpired, - /// Alice did not lock up enough monero in the shared output. - InsufficientXmr(monero::InsufficientFunds), - /// Could not find Bob's signature on the redeem transaction witness - /// stack. - BtcRedeemSignature, - /// Could not recover secret `s_a` from Bob's redeem transaction - /// signature. - SecretRecovery, - } - - Gen::new_boxed(|co| async move { - let swap_result: Result<(), SwapFailed> = async { - co.yield_(Action::LockBtc(tx_lock.clone())).await; - - timeout( - Duration::from_secs(bitcoin_tx_lock_timeout), - bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - ) - .await - .map(|tx| tx.txid()) - .map_err(|_| SwapFailed::BeforeBtcLock(Reason::InactiveBob))?; - - let tx_lock_height = bitcoin_client - .transaction_block_height(tx_lock.txid()) - .await; - let poll_until_btc_has_expired = poll_until_block_height_is_gte( - bitcoin_client.as_ref(), - tx_lock_height + cancel_timelock, - ) - .shared(); - pin_mut!(poll_until_btc_has_expired); - - let transfer_proof = { - let mut guard = network.as_ref().lock().await; - let transfer_proof = match select( - guard.receive_transfer_proof(), - poll_until_btc_has_expired.clone(), - ) - .await - { - Either::Left((proof, _)) => proof, - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - }; - - tracing::debug!("select returned transfer proof from message"); - - transfer_proof - }; - - let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar( - s_b.into_ed25519(), - )); - let S = S_a_monero + S_b_monero; - - match select( - monero_client.watch_for_transfer(S, v.public(), transfer_proof, xmr, 0), - poll_until_btc_has_expired.clone(), - ) - .await - { - Either::Left((Err(e), _)) => { - return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXmr(e))) - } - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - _ => {} - } - - let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); - let tx_redeem_encsig = b.encsign(S_a_bitcoin, tx_redeem.digest()); - - co.yield_(Action::SendBtcRedeemEncsig(tx_redeem_encsig.clone())) - .await; - - let tx_redeem_published = match select( - bitcoin_client.watch_for_raw_transaction(tx_redeem.txid()), - poll_until_btc_has_expired, - ) - .await - { - Either::Left((tx, _)) => tx, - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - }; - - let tx_redeem_sig = tx_redeem - .extract_signature_by_key(tx_redeem_published, b.public()) - .map_err(|_| SwapFailed::AfterBtcRedeem(Reason::BtcRedeemSignature))?; - let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig) - .map_err(|_| SwapFailed::AfterBtcRedeem(Reason::SecretRecovery))?; - let s_a = monero::private_key_from_secp256k1_scalar(s_a.into()); - - let s_b = monero::PrivateKey { - scalar: s_b.into_ed25519(), - }; - - co.yield_(Action::CreateXmrWalletForOutput { - spend_key: s_a + s_b, - view_key: v, - }) - .await; - - Ok(()) - } - .await; - - if let Err(ref err) = swap_result { - error!("swap failed: {:?}", err); - } - - if let Err(SwapFailed::AfterBtcLock(_)) = swap_result { - let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, A, b.public()); - let tx_cancel_txid = tx_cancel.txid(); - let signed_tx_cancel = { - let sig_a = tx_cancel_sig_a.clone(); - let sig_b = b.sign(tx_cancel.digest()); - - tx_cancel - .clone() - .add_signatures(&tx_lock, (A, sig_a), (b.public(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel") - }; - - co.yield_(Action::CancelBtc(signed_tx_cancel)).await; - - let _ = bitcoin_client - .watch_for_raw_transaction(tx_cancel_txid) - .await; - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); - let tx_refund_txid = tx_refund.txid(); - let signed_tx_refund = { - let adaptor = Adaptor::>::default(); - - let sig_a = - adaptor.decrypt_signature(&s_b.into_secp256k1(), tx_refund_encsig.clone()); - let sig_b = b.sign(tx_refund.digest()); - - tx_refund - .add_signatures(&tx_cancel, (A, sig_a), (b.public(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_refund") - }; - - co.yield_(Action::RefundBtc(signed_tx_refund)).await; - - let _ = bitcoin_client - .watch_for_raw_transaction(tx_refund_txid) - .await; - } - }) -} - -// There are no guarantees that send_message and receive_massage do not block -// the flow of execution. Therefore they must be paired between Alice/Bob, one -// send to one receive in the correct order. -pub async fn next_state< - R: RngCore + CryptoRng, - B: WatchForRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction + Network, - M: CreateWalletForOutput + WatchForTransfer, - T: SendMessage + ReceiveMessage, ->( - bitcoin_wallet: &B, - monero_wallet: &M, - transport: &mut T, - state: State, - rng: &mut R, -) -> Result { - match state { - State::State0(state0) => { - transport - .send_message(state0.next_message(rng).into()) - .await?; - let message0 = transport.receive_message().await?.try_into()?; - let state1 = state0.receive(bitcoin_wallet, message0).await?; - Ok(state1.into()) - } - State::State1(state1) => { - transport.send_message(state1.next_message().into()).await?; - - let message1 = transport.receive_message().await?.try_into()?; - let state2 = state1.receive(message1)?; - - let message2 = state2.next_message(); - transport.send_message(message2.into()).await?; - Ok(state2.into()) - } - State::State2(state2) => { - let state3 = state2.lock_btc(bitcoin_wallet).await?; - tracing::info!("bob has locked btc"); - - Ok(state3.into()) - } - State::State3(state3) => { - let message2 = transport.receive_message().await?.try_into()?; - let state4 = state3.watch_for_lock_xmr(monero_wallet, message2).await?; - tracing::info!("bob has seen that alice has locked xmr"); - Ok(state4.into()) - } - State::State4(state4) => { - transport.send_message(state4.next_message().into()).await?; - tracing::info!("bob is watching for redeem_btc"); - let state5 = state4.watch_for_redeem_btc(bitcoin_wallet).await?; - tracing::info!("bob has seen that alice has redeemed btc"); - state5.claim_xmr(monero_wallet).await?; - tracing::info!("bob has claimed xmr"); - Ok(state5.into()) - } - State::State5(state5) => Ok(state5.into()), - } -} - #[derive(Debug, Deserialize, Serialize)] pub enum State { State0(State0), @@ -593,7 +314,7 @@ impl State3 { )); let S = self.S_a_monero + S_b_monero; - xmr_wallet + let monero_transfer_info = xmr_wallet .watch_for_transfer( S, self.v.public(), @@ -603,6 +324,11 @@ impl State3 { ) .await?; + tracing::debug!( + "XMR lock tx 1st confirmation at block height: {}", + monero_transfer_info.first_confirmation_block_height + ); + Ok(State4 { A: self.A, b: self.b, @@ -620,6 +346,7 @@ impl State3 { tx_lock: self.tx_lock, tx_cancel_sig_a: self.tx_cancel_sig_a, tx_refund_encsig: self.tx_refund_encsig, + monero_rescan_block_height: monero_transfer_info.first_confirmation_block_height, }) } @@ -653,6 +380,7 @@ impl State3 { tx_lock: self.tx_lock.clone(), tx_cancel_sig_a: self.tx_cancel_sig_a.clone(), tx_refund_encsig: self.tx_refund_encsig.clone(), + monero_rescan_block_height: 0u32, } } @@ -693,6 +421,7 @@ pub struct State4 { pub tx_lock: bitcoin::TxLock, pub tx_cancel_sig_a: Signature, pub tx_refund_encsig: EncryptedSignature, + pub monero_rescan_block_height: u32, } impl State4 { @@ -789,6 +518,7 @@ impl State4 { tx_lock: self.tx_lock.clone(), tx_refund_encsig: self.tx_refund_encsig.clone(), tx_cancel_sig: self.tx_cancel_sig_a.clone(), + monero_rescan_block_height: self.monero_rescan_block_height, }) } @@ -886,6 +616,7 @@ pub struct State5 { pub tx_lock: bitcoin::TxLock, tx_refund_encsig: EncryptedSignature, tx_cancel_sig: Signature, + pub monero_rescan_block_height: u32, } impl State5 { @@ -902,7 +633,7 @@ impl State5 { // NOTE: This actually generates and opens a new wallet, closing the currently // open one. monero_wallet - .create_and_load_wallet_for_output(s, self.v) + .create_and_load_wallet_for_output(s, self.v, Some(self.monero_rescan_block_height)) .await?; Ok(()) diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 22fc8807..01794683 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -1,7 +1,6 @@ #![warn( unused_extern_crates, missing_debug_implementations, - missing_copy_implementations, rust_2018_idioms, clippy::cast_possible_truncation, clippy::cast_sign_loss, diff --git a/xmr-btc/src/monero.rs b/xmr-btc/src/monero.rs index 31deb392..fc4fd41c 100644 --- a/xmr-btc/src/monero.rs +++ b/xmr-btc/src/monero.rs @@ -14,7 +14,7 @@ use rust_decimal::{ }; use std::{fmt::Display, str::FromStr}; -pub const MIN_CONFIRMATIONS: u32 = 10; +pub const MIN_CONFIRMATIONS: u32 = 1; pub const PICONERO_OFFSET: u64 = 1_000_000_000_000; pub fn random_private_key(rng: &mut R) -> PrivateKey { @@ -147,6 +147,11 @@ pub struct TransferProof { tx_key: PrivateKey, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TransferInfo { + pub first_confirmation_block_height: u32, +} + impl TransferProof { pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self { Self { tx_hash, tx_key } @@ -188,7 +193,7 @@ pub trait WatchForTransfer { transfer_proof: TransferProof, amount: Amount, expected_confirmations: u32, - ) -> Result<(), InsufficientFunds>; + ) -> Result; } #[derive(Debug, Clone, Copy, thiserror::Error)] @@ -198,12 +203,19 @@ pub struct InsufficientFunds { pub actual: Amount, } +#[derive(Debug, Clone, thiserror::Error)] +#[error("Transfer with id {txid:?} not found.")] +pub struct TransferNotFound { + pub txid: String, +} + #[async_trait] pub trait CreateWalletForOutput { async fn create_and_load_wallet_for_output( &self, private_spend_key: PrivateKey, private_view_key: PrivateViewKey, + restore_height: Option, ) -> anyhow::Result<()>; }