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.
This commit is contained in:
Daniel Karzel 2021-01-06 16:12:35 +11:00
parent 184f179044
commit 45b5af4375
7 changed files with 107 additions and 301 deletions

View File

@ -186,7 +186,7 @@ impl Client {
}
/// Get wallet block height, this might be behind monerod height.
pub(crate) async fn block_height(&self) -> Result<BlockHeight> {
pub async fn block_height(&self) -> Result<BlockHeight> {
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<GetTransferByTxid> {
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<GetTransferByTxid> = 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<u32>,
) -> Result<GenerateFromKeys> {
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,

View File

@ -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;

View File

@ -64,6 +64,7 @@ impl CreateWalletForOutput for Wallet {
&self,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
restore_height: Option<u32>,
) -> 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<TransferInfo> {
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")
}
}
}
}

View File

@ -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(())

View File

@ -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<N, M, B>(
network: Arc<Mutex<N>>,
monero_client: Arc<M>,
bitcoin_client: Arc<B>,
// 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<Action, (), ()>
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::<Sha256, Deterministic<Sha256>>::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<Message> + ReceiveMessage<alice::Message>,
>(
bitcoin_wallet: &B,
monero_wallet: &M,
transport: &mut T,
state: State,
rng: &mut R,
) -> Result<State> {
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(())

View File

@ -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,

View File

@ -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<R: RngCore + CryptoRng>(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<TransferInfo>;
}
#[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<u32>,
) -> anyhow::Result<()>;
}