Greatly reduce load onto the Electrum backend

We achieve our optimizations in three ways:

1. Batching calls instead of making them individually.

To get access to the batch calls, we replace all our
calls to the HTTP interface with RPC calls.

2. Never directly make network calls based on function
calls on the wallet.

Instead, inquiring about the status of a script always
just returns information based on local data. With every
call, we check when we last refreshed the local data and
do so if the data is considered to be too old. This
interval is configurable.

3. Use electrum's notification feature to get updated
with the latest blockheight.

Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
Co-authored-by: Rishab Sharma <rishflab@hotmail.com>
This commit is contained in:
rishflab 2021-03-11 18:16:00 +11:00 committed by Thomas Eizinger
parent e17cbadccb
commit e5c0158597
No known key found for this signature in database
GPG Key ID: 651AC83A6C6C8B96
16 changed files with 628 additions and 378 deletions

View File

@ -12,7 +12,6 @@ use tracing::info;
use url::Url; use url::Url;
const DEFAULT_LISTEN_ADDRESS: &str = "/ip4/0.0.0.0/tcp/9939"; const DEFAULT_LISTEN_ADDRESS: &str = "/ip4/0.0.0.0/tcp/9939";
const DEFAULT_ELECTRUM_HTTP_URL: &str = "https://blockstream.info/testnet/api/";
const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002";
const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc"; const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc";
@ -52,7 +51,6 @@ pub struct Network {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct Bitcoin { pub struct Bitcoin {
pub electrum_http_url: Url,
pub electrum_rpc_url: Url, pub electrum_rpc_url: Url,
} }
@ -120,12 +118,6 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
.interact_text()?; .interact_text()?;
let listen_address = listen_address.as_str().parse()?; let listen_address = listen_address.as_str().parse()?;
let electrum_http_url: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Electrum HTTP URL or hit return to use default")
.default(DEFAULT_ELECTRUM_HTTP_URL.to_owned())
.interact_text()?;
let electrum_http_url = Url::parse(electrum_http_url.as_str())?;
let electrum_rpc_url: String = Input::with_theme(&ColorfulTheme::default()) let electrum_rpc_url: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Electrum RPC URL or hit return to use default") .with_prompt("Enter Electrum RPC URL or hit return to use default")
.default(DEFAULT_ELECTRUM_RPC_URL.to_owned()) .default(DEFAULT_ELECTRUM_RPC_URL.to_owned())
@ -144,10 +136,7 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
network: Network { network: Network {
listen: listen_address, listen: listen_address,
}, },
bitcoin: Bitcoin { bitcoin: Bitcoin { electrum_rpc_url },
electrum_http_url,
electrum_rpc_url,
},
monero: Monero { monero: Monero {
wallet_rpc_url: monero_wallet_rpc_url, wallet_rpc_url: monero_wallet_rpc_url,
}, },
@ -170,7 +159,6 @@ mod tests {
dir: Default::default(), dir: Default::default(),
}, },
bitcoin: Bitcoin { bitcoin: Bitcoin {
electrum_http_url: Url::from_str(DEFAULT_ELECTRUM_HTTP_URL).unwrap(),
electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(),
}, },
network: Network { network: Network {

View File

@ -136,7 +136,6 @@ async fn init_wallets(
) -> Result<(bitcoin::Wallet, monero::Wallet)> { ) -> Result<(bitcoin::Wallet, monero::Wallet)> {
let bitcoin_wallet = bitcoin::Wallet::new( let bitcoin_wallet = bitcoin::Wallet::new(
config.bitcoin.electrum_rpc_url, config.bitcoin.electrum_rpc_url,
config.bitcoin.electrum_http_url,
BITCOIN_NETWORK, BITCOIN_NETWORK,
bitcoin_wallet_data_dir, bitcoin_wallet_data_dir,
key, key,

View File

@ -21,9 +21,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use structopt::StructOpt; use structopt::StructOpt;
use swap::bitcoin::{Amount, TxLock}; use swap::bitcoin::{Amount, TxLock};
use swap::cli::command::{ use swap::cli::command::{AliceConnectParams, Arguments, Command, Data, MoneroParams};
AliceConnectParams, Arguments, BitcoinParams, Command, Data, MoneroParams,
};
use swap::database::Database; use swap::database::Database;
use swap::execution_params::{ExecutionParams, GetExecutionParams}; use swap::execution_params::{ExecutionParams, GetExecutionParams};
use swap::network::quote::BidQuote; use swap::network::quote::BidQuote;
@ -95,11 +93,7 @@ async fn main() -> Result<()> {
receive_monero_address, receive_monero_address,
monero_daemon_host, monero_daemon_host,
}, },
bitcoin_params:
BitcoinParams {
electrum_http_url,
electrum_rpc_url, electrum_rpc_url,
},
} => { } => {
if receive_monero_address.network != monero_network { if receive_monero_address.network != monero_network {
bail!( bail!(
@ -109,13 +103,8 @@ async fn main() -> Result<()> {
) )
} }
let bitcoin_wallet = init_bitcoin_wallet( let bitcoin_wallet =
bitcoin_network, init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir.clone())
electrum_rpc_url,
electrum_http_url,
seed,
data_dir.clone(),
)
.await?; .await?;
let (monero_wallet, _process) = init_monero_wallet( let (monero_wallet, _process) = init_monero_wallet(
monero_network, monero_network,
@ -196,23 +185,14 @@ async fn main() -> Result<()> {
receive_monero_address, receive_monero_address,
monero_daemon_host, monero_daemon_host,
}, },
bitcoin_params:
BitcoinParams {
electrum_http_url,
electrum_rpc_url, electrum_rpc_url,
},
} => { } => {
if receive_monero_address.network != monero_network { if receive_monero_address.network != monero_network {
bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, monero_network) bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, monero_network)
} }
let bitcoin_wallet = init_bitcoin_wallet( let bitcoin_wallet =
bitcoin_network, init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir.clone())
electrum_rpc_url,
electrum_http_url,
seed,
data_dir.clone(),
)
.await?; .await?;
let (monero_wallet, _process) = init_monero_wallet( let (monero_wallet, _process) = init_monero_wallet(
monero_network, monero_network,
@ -255,20 +235,10 @@ async fn main() -> Result<()> {
Command::Cancel { Command::Cancel {
swap_id, swap_id,
force, force,
bitcoin_params:
BitcoinParams {
electrum_http_url,
electrum_rpc_url, electrum_rpc_url,
},
} => { } => {
let bitcoin_wallet = init_bitcoin_wallet( let bitcoin_wallet =
bitcoin_network, init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir).await?;
electrum_rpc_url,
electrum_http_url,
seed,
data_dir,
)
.await?;
let resume_state = db.get_state(swap_id)?.try_into_bob()?.into(); let resume_state = db.get_state(swap_id)?.try_into_bob()?.into();
let cancel = let cancel =
@ -290,20 +260,10 @@ async fn main() -> Result<()> {
Command::Refund { Command::Refund {
swap_id, swap_id,
force, force,
bitcoin_params:
BitcoinParams {
electrum_http_url,
electrum_rpc_url, electrum_rpc_url,
},
} => { } => {
let bitcoin_wallet = init_bitcoin_wallet( let bitcoin_wallet =
bitcoin_network, init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir).await?;
electrum_rpc_url,
electrum_http_url,
seed,
data_dir,
)
.await?;
let resume_state = db.get_state(swap_id)?.try_into_bob()?.into(); let resume_state = db.get_state(swap_id)?.try_into_bob()?.into();
@ -324,7 +284,6 @@ async fn main() -> Result<()> {
async fn init_bitcoin_wallet( async fn init_bitcoin_wallet(
network: bitcoin::Network, network: bitcoin::Network,
electrum_rpc_url: Url, electrum_rpc_url: Url,
electrum_http_url: Url,
seed: Seed, seed: Seed,
data_dir: PathBuf, data_dir: PathBuf,
) -> Result<bitcoin::Wallet> { ) -> Result<bitcoin::Wallet> {
@ -332,7 +291,6 @@ async fn init_bitcoin_wallet(
let wallet = bitcoin::Wallet::new( let wallet = bitcoin::Wallet::new(
electrum_rpc_url.clone(), electrum_rpc_url.clone(),
electrum_http_url.clone(),
network, network,
&wallet_dir, &wallet_dir,
seed.derive_extended_private_key(network)?, seed.derive_extended_private_key(network)?,

View File

@ -20,6 +20,7 @@ pub use ecdsa_fun::fun::Scalar;
pub use ecdsa_fun::Signature; pub use ecdsa_fun::Signature;
pub use wallet::Wallet; pub use wallet::Wallet;
use crate::bitcoin::wallet::ScriptStatus;
use ::bitcoin::hashes::hex::ToHex; use ::bitcoin::hashes::hex::ToHex;
use ::bitcoin::hashes::Hash; use ::bitcoin::hashes::Hash;
use ::bitcoin::{secp256k1, SigHash}; use ::bitcoin::{secp256k1, SigHash};
@ -218,46 +219,21 @@ pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Resu
Ok(s) Ok(s)
} }
pub async fn poll_until_block_height_is_gte( pub fn current_epoch(
wallet: &crate::bitcoin::Wallet,
target: BlockHeight,
) -> Result<()> {
while wallet.get_block_height().await? < target {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
Ok(())
}
pub async fn current_epoch(
bitcoin_wallet: &crate::bitcoin::Wallet,
cancel_timelock: CancelTimelock, cancel_timelock: CancelTimelock,
punish_timelock: PunishTimelock, punish_timelock: PunishTimelock,
lock_tx_id: ::bitcoin::Txid, tx_lock_status: ScriptStatus,
) -> Result<ExpiredTimelocks> { tx_cancel_status: ScriptStatus,
let current_block_height = bitcoin_wallet.get_block_height().await?; ) -> ExpiredTimelocks {
let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?; if tx_cancel_status.is_confirmed_with(punish_timelock) {
let cancel_timelock_height = lock_tx_height + cancel_timelock; return ExpiredTimelocks::Punish;
let punish_timelock_height = cancel_timelock_height + punish_timelock;
match (
current_block_height < cancel_timelock_height,
current_block_height < punish_timelock_height,
) {
(true, _) => Ok(ExpiredTimelocks::None),
(false, true) => Ok(ExpiredTimelocks::Cancel),
(false, false) => Ok(ExpiredTimelocks::Punish),
}
} }
pub async fn wait_for_cancel_timelock_to_expire( if tx_lock_status.is_confirmed_with(cancel_timelock) {
bitcoin_wallet: &crate::bitcoin::Wallet, return ExpiredTimelocks::Cancel;
cancel_timelock: CancelTimelock, }
lock_tx_id: ::bitcoin::Txid,
) -> Result<()> {
let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?;
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await?; ExpiredTimelocks::None
Ok(())
} }
#[derive(Clone, Copy, thiserror::Error, Debug)] #[derive(Clone, Copy, thiserror::Error, Debug)]
@ -275,3 +251,53 @@ pub struct EmptyWitnessStack;
#[derive(Clone, Copy, thiserror::Error, Debug)] #[derive(Clone, Copy, thiserror::Error, Debug)]
#[error("input has {0} witnesses, expected 3")] #[error("input has {0} witnesses, expected 3")]
pub struct NotThreeWitnesses(usize); pub struct NotThreeWitnesses(usize);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lock_confirmations_le_to_cancel_timelock_no_timelock_expired() {
let tx_lock_status = ScriptStatus::from_confirmations(4);
let tx_cancel_status = ScriptStatus::Unseen;
let expired_timelock = current_epoch(
CancelTimelock::new(5),
PunishTimelock::new(5),
tx_lock_status,
tx_cancel_status,
);
assert_eq!(expired_timelock, ExpiredTimelocks::None)
}
#[test]
fn lock_confirmations_ge_to_cancel_timelock_cancel_timelock_expired() {
let tx_lock_status = ScriptStatus::from_confirmations(5);
let tx_cancel_status = ScriptStatus::Unseen;
let expired_timelock = current_epoch(
CancelTimelock::new(5),
PunishTimelock::new(5),
tx_lock_status,
tx_cancel_status,
);
assert_eq!(expired_timelock, ExpiredTimelocks::Cancel)
}
#[test]
fn cancel_confirmations_ge_to_punish_timelock_punish_timelock_expired() {
let tx_lock_status = ScriptStatus::from_confirmations(10);
let tx_cancel_status = ScriptStatus::from_confirmations(5);
let expired_timelock = current_epoch(
CancelTimelock::new(5),
PunishTimelock::new(5),
tx_lock_status,
tx_cancel_status,
);
assert_eq!(expired_timelock, ExpiredTimelocks::Punish)
}
}

View File

@ -5,9 +5,11 @@ use crate::bitcoin::{
use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::util::bip143::SigHashCache;
use ::bitcoin::{OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid}; use ::bitcoin::{OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid};
use anyhow::Result; use anyhow::Result;
use bitcoin::Script;
use ecdsa_fun::Signature; use ecdsa_fun::Signature;
use miniscript::{Descriptor, DescriptorTrait}; use miniscript::{Descriptor, DescriptorTrait};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Add; use std::ops::Add;
@ -33,6 +35,18 @@ impl Add<CancelTimelock> for BlockHeight {
} }
} }
impl PartialOrd<CancelTimelock> for u32 {
fn partial_cmp(&self, other: &CancelTimelock) -> Option<Ordering> {
self.partial_cmp(&other.0)
}
}
impl PartialEq<CancelTimelock> for u32 {
fn eq(&self, other: &CancelTimelock) -> bool {
self.eq(&other.0)
}
}
/// Represent a timelock, expressed in relative block height as defined in /// Represent a timelock, expressed in relative block height as defined in
/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). /// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
/// E.g. The timelock expires 10 blocks after the reference transaction is /// E.g. The timelock expires 10 blocks after the reference transaction is
@ -55,6 +69,18 @@ impl Add<PunishTimelock> for BlockHeight {
} }
} }
impl PartialOrd<PunishTimelock> for u32 {
fn partial_cmp(&self, other: &PunishTimelock) -> Option<Ordering> {
self.partial_cmp(&other.0)
}
}
impl PartialEq<PunishTimelock> for u32 {
fn eq(&self, other: &PunishTimelock) -> bool {
self.eq(&other.0)
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TxCancel { pub struct TxCancel {
inner: Transaction, inner: Transaction,
@ -122,6 +148,16 @@ impl TxCancel {
OutPoint::new(self.inner.txid(), 0) OutPoint::new(self.inner.txid(), 0)
} }
/// Return the relevant script_pubkey of this transaction.
///
/// Even though a transaction can have multiple outputs, the nature of our
/// protocol is that there is only one relevant output within this one.
/// As such, subscribing or inquiring the status of this script allows us to
/// check the status of the whole transaction.
pub fn script_pubkey(&self) -> Script {
self.output_descriptor.script_pubkey()
}
pub fn add_signatures( pub fn add_signatures(
self, self,
(A, sig_a): (PublicKey, Signature), (A, sig_a): (PublicKey, Signature),

View File

@ -4,6 +4,7 @@ use crate::bitcoin::{
use ::bitcoin::util::psbt::PartiallySignedTransaction; use ::bitcoin::util::psbt::PartiallySignedTransaction;
use ::bitcoin::{OutPoint, TxIn, TxOut, Txid}; use ::bitcoin::{OutPoint, TxIn, TxOut, Txid};
use anyhow::Result; use anyhow::Result;
use bitcoin::Script;
use ecdsa_fun::fun::Point; use ecdsa_fun::fun::Point;
use miniscript::{Descriptor, DescriptorTrait}; use miniscript::{Descriptor, DescriptorTrait};
use rand::thread_rng; use rand::thread_rng;
@ -55,6 +56,10 @@ impl TxLock {
.len() .len()
} }
pub fn script_pubkey(&self) -> Script {
self.output_descriptor.script_pubkey()
}
/// Retreive the index of the locked output in the transaction outputs /// Retreive the index of the locked output in the transaction outputs
/// vector /// vector
fn lock_output_vout(&self) -> usize { fn lock_output_vout(&self) -> usize {

View File

@ -1,4 +1,7 @@
use anyhow::Context;
use bdk::electrum_client::HeaderNotification;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
use std::ops::Add; use std::ops::Add;
/// Represent a block height, or block number, expressed in absolute block /// Represent a block height, or block number, expressed in absolute block
@ -26,6 +29,19 @@ impl BlockHeight {
} }
} }
impl TryFrom<HeaderNotification> for BlockHeight {
type Error = anyhow::Error;
fn try_from(value: HeaderNotification) -> Result<Self, Self::Error> {
Ok(Self(
value
.height
.try_into()
.context("Failed to fit usize into u32")?,
))
}
}
impl Add<u32> for BlockHeight { impl Add<u32> for BlockHeight {
type Output = BlockHeight; type Output = BlockHeight;
fn add(self, rhs: u32) -> Self::Output { fn add(self, rhs: u32) -> Self::Output {
@ -33,7 +49,7 @@ impl Add<u32> for BlockHeight {
} }
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum ExpiredTimelocks { pub enum ExpiredTimelocks {
None, None,
Cancel, Cancel,

View File

@ -4,48 +4,31 @@ use crate::execution_params::ExecutionParams;
use ::bitcoin::util::psbt::PartiallySignedTransaction; use ::bitcoin::util::psbt::PartiallySignedTransaction;
use ::bitcoin::Txid; use ::bitcoin::Txid;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use backoff::backoff::Constant as ConstantBackoff;
use backoff::future::retry;
use bdk::blockchain::{noop_progress, Blockchain, ElectrumBlockchain}; use bdk::blockchain::{noop_progress, Blockchain, ElectrumBlockchain};
use bdk::descriptor::Segwitv0; use bdk::descriptor::Segwitv0;
use bdk::electrum_client::{self, Client, ElectrumApi}; use bdk::electrum_client::{self, ElectrumApi, GetHistoryRes};
use bdk::keys::DerivableKey; use bdk::keys::DerivableKey;
use bdk::{FeeRate, KeychainKind}; use bdk::{FeeRate, KeychainKind};
use bitcoin::Script; use bitcoin::Script;
use reqwest::Url; use reqwest::Url;
use serde::{Deserialize, Serialize}; use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::{Duration, Instant};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::time::interval;
const SLED_TREE_NAME: &str = "default_tree"; const SLED_TREE_NAME: &str = "default_tree";
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("Sending the request failed")]
Io(reqwest::Error),
#[error("Conversion to Integer failed")]
Parse(std::num::ParseIntError),
#[error("The transaction is not minded yet")]
NotYetMined,
#[error("Deserialization failed")]
JsonDeserialization(reqwest::Error),
#[error("Electrum client error")]
ElectrumClient(electrum_client::Error),
}
pub struct Wallet { pub struct Wallet {
inner: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>, client: Arc<Mutex<Client>>,
http_url: Url, wallet: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>,
rpc_url: Url,
} }
impl Wallet { impl Wallet {
pub async fn new( pub async fn new(
electrum_rpc_url: Url, electrum_rpc_url: Url,
electrum_http_url: Url,
network: bitcoin::Network, network: bitcoin::Network,
wallet_dir: &Path, wallet_dir: &Path,
key: impl DerivableKey<Segwitv0> + Clone, key: impl DerivableKey<Segwitv0> + Clone,
@ -53,7 +36,8 @@ impl Wallet {
// Workaround for https://github.com/bitcoindevkit/rust-electrum-client/issues/47. // Workaround for https://github.com/bitcoindevkit/rust-electrum-client/issues/47.
let config = electrum_client::ConfigBuilder::default().retry(2).build(); let config = electrum_client::ConfigBuilder::default().retry(2).build();
let client = Client::from_config(electrum_rpc_url.as_str(), config) let client =
bdk::electrum_client::Client::from_config(electrum_rpc_url.as_str(), config.clone())
.map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?; .map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?;
let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?; let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
@ -66,16 +50,20 @@ impl Wallet {
ElectrumBlockchain::from(client), ElectrumBlockchain::from(client),
)?; )?;
let electrum = bdk::electrum_client::Client::from_config(electrum_rpc_url.as_str(), config)
.map_err(|e| anyhow!("Failed to init electrum rpc client {:?}", e))?;
let interval = Duration::from_secs(5);
Ok(Self { Ok(Self {
inner: Arc::new(Mutex::new(bdk_wallet)), wallet: Arc::new(Mutex::new(bdk_wallet)),
http_url: electrum_http_url, client: Arc::new(Mutex::new(Client::new(electrum, interval)?)),
rpc_url: electrum_rpc_url,
}) })
} }
pub async fn balance(&self) -> Result<Amount> { pub async fn balance(&self) -> Result<Amount> {
let balance = self let balance = self
.inner .wallet
.lock() .lock()
.await .await
.get_balance() .get_balance()
@ -86,7 +74,7 @@ impl Wallet {
pub async fn new_address(&self) -> Result<Address> { pub async fn new_address(&self) -> Result<Address> {
let address = self let address = self
.inner .wallet
.lock() .lock()
.await .await
.get_new_address() .get_new_address()
@ -96,13 +84,14 @@ impl Wallet {
} }
pub async fn get_tx(&self, txid: Txid) -> Result<Option<Transaction>> { pub async fn get_tx(&self, txid: Txid) -> Result<Option<Transaction>> {
let tx = self.inner.lock().await.client().get_tx(&txid)?; let tx = self.wallet.lock().await.client().get_tx(&txid)?;
Ok(tx) Ok(tx)
} }
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> { pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
let fees = self let fees = self
.inner .wallet
.lock() .lock()
.await .await
.list_transactions(true)? .list_transactions(true)?
@ -117,7 +106,7 @@ impl Wallet {
} }
pub async fn sync(&self) -> Result<()> { pub async fn sync(&self) -> Result<()> {
self.inner self.wallet
.lock() .lock()
.await .await
.sync(noop_progress(), None) .sync(noop_progress(), None)
@ -131,7 +120,7 @@ impl Wallet {
address: Address, address: Address,
amount: Amount, amount: Amount,
) -> Result<PartiallySignedTransaction> { ) -> Result<PartiallySignedTransaction> {
let wallet = self.inner.lock().await; let wallet = self.wallet.lock().await;
let mut tx_builder = wallet.build_tx(); let mut tx_builder = wallet.build_tx();
tx_builder.add_recipient(address.script_pubkey(), amount.as_sat()); tx_builder.add_recipient(address.script_pubkey(), amount.as_sat());
@ -147,7 +136,7 @@ impl Wallet {
/// already accounting for the fees we need to spend to get the /// already accounting for the fees we need to spend to get the
/// transaction confirmed. /// transaction confirmed.
pub async fn max_giveable(&self, locking_script_size: usize) -> Result<Amount> { pub async fn max_giveable(&self, locking_script_size: usize) -> Result<Amount> {
let wallet = self.inner.lock().await; let wallet = self.wallet.lock().await;
let mut tx_builder = wallet.build_tx(); let mut tx_builder = wallet.build_tx();
@ -163,7 +152,7 @@ impl Wallet {
} }
pub async fn get_network(&self) -> bitcoin::Network { pub async fn get_network(&self) -> bitcoin::Network {
self.inner.lock().await.network() self.wallet.lock().await.network()
} }
/// Broadcast the given transaction to the network and emit a log statement /// Broadcast the given transaction to the network and emit a log statement
@ -171,7 +160,7 @@ impl Wallet {
pub async fn broadcast(&self, transaction: Transaction, kind: &str) -> Result<Txid> { pub async fn broadcast(&self, transaction: Transaction, kind: &str) -> Result<Txid> {
let txid = transaction.txid(); let txid = transaction.txid();
self.inner self.wallet
.lock() .lock()
.await .await
.broadcast(transaction) .broadcast(transaction)
@ -185,7 +174,7 @@ impl Wallet {
} }
pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result<Transaction> { pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result<Transaction> {
let (signed_psbt, finalized) = self.inner.lock().await.sign(psbt, None)?; let (signed_psbt, finalized) = self.wallet.lock().await.sign(psbt, None)?;
if !finalized { if !finalized {
bail!("PSBT is not finalized") bail!("PSBT is not finalized")
@ -202,106 +191,62 @@ impl Wallet {
.ok_or_else(|| anyhow!("Could not get raw tx with id: {}", txid)) .ok_or_else(|| anyhow!("Could not get raw tx with id: {}", txid))
} }
pub async fn watch_for_raw_transaction(&self, txid: Txid) -> Result<Transaction> { pub async fn status_of_script(&self, script: &Script, txid: &Txid) -> Result<ScriptStatus> {
tracing::debug!("watching for tx: {}", txid); self.client.lock().await.status_of_script(script, txid)
let tx = retry(ConstantBackoff::new(Duration::from_secs(1)), || async {
let client = Client::new(self.rpc_url.as_ref())
.map_err(|err| backoff::Error::Permanent(Error::ElectrumClient(err)))?;
let tx = client.transaction_get(&txid).map_err(|err| match err {
electrum_client::Error::Protocol(err) => {
tracing::debug!("Received protocol error {} from Electrum, retrying...", err);
backoff::Error::Transient(Error::NotYetMined)
}
err => backoff::Error::Permanent(Error::ElectrumClient(err)),
})?;
Result::<_, backoff::Error<Error>>::Ok(tx)
})
.await
.context("Transient errors should be retried")?;
Ok(tx)
} }
pub async fn get_block_height(&self) -> Result<BlockHeight> { pub async fn watch_until_status(
let url = make_blocks_tip_height_url(&self.http_url)?; &self,
txid: Txid,
script: Script,
mut status_fn: impl FnMut(ScriptStatus) -> bool,
) -> Result<()> {
let mut last_status = None;
let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async { loop {
let height = reqwest::get(url.clone()) let new_status = self.client.lock().await.status_of_script(&script, &txid)?;
.await
.map_err(Error::Io)?
.text()
.await
.map_err(Error::Io)?
.parse::<u32>()
.map_err(|err| backoff::Error::Permanent(Error::Parse(err)))?;
Result::<_, backoff::Error<Error>>::Ok(height)
})
.await
.context("Transient errors should be retried")?;
Ok(BlockHeight::new(height)) if Some(new_status) != last_status {
tracing::debug!(%txid, "Transaction is {}", new_status);
}
last_status = Some(new_status);
if status_fn(new_status) {
break;
} }
pub async fn transaction_block_height(&self, txid: Txid) -> Result<BlockHeight> { tokio::time::sleep(Duration::from_secs(5)).await;
let status_url = make_tx_status_url(&self.http_url, txid)?;
#[derive(Serialize, Deserialize, Debug, Clone)]
struct TransactionStatus {
block_height: Option<u32>,
confirmed: bool,
} }
let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async {
let block_height = reqwest::get(status_url.clone())
.await
.map_err(|err| backoff::Error::Transient(Error::Io(err)))?
.json::<TransactionStatus>()
.await
.map_err(|err| backoff::Error::Permanent(Error::JsonDeserialization(err)))?
.block_height
.ok_or(backoff::Error::Transient(Error::NotYetMined))?;
Result::<_, backoff::Error<Error>>::Ok(block_height) Ok(())
})
.await
.context("Transient errors should be retried")?;
Ok(BlockHeight::new(height))
} }
pub async fn wait_for_transaction_finality( pub async fn wait_for_transaction_finality(
&self, &self,
txid: Txid, txid: Txid,
script_to_watch: Script,
execution_params: ExecutionParams, execution_params: ExecutionParams,
) -> Result<()> { ) -> Result<()> {
let conf_target = execution_params.bitcoin_finality_confirmations; let conf_target = execution_params.bitcoin_finality_confirmations;
tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin transaction", conf_target, if conf_target > 1 { "s" } else { "" }); tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin transaction", conf_target, if conf_target > 1 { "s" } else { "" });
// Divide by 4 to not check too often yet still be aware of the new block early let mut seen_confirmations = 0;
// on.
let mut interval = interval(execution_params.bitcoin_avg_block_time / 4);
loop { self.watch_until_status(txid, script_to_watch, |status| match status {
let tx_block_height = self.transaction_block_height(txid).await?; ScriptStatus::Confirmed(inner) => {
tracing::debug!("tx_block_height: {:?}", tx_block_height); let confirmations = inner.confirmations();
let block_height = self.get_block_height().await?; if confirmations > seen_confirmations {
tracing::debug!("latest_block_height: {:?}", block_height); tracing::info!(%txid, "Bitcoin tx has {} out of {} confirmation{}", confirmations, conf_target, if conf_target > 1 { "s" } else { "" });
seen_confirmations = confirmations;
}
if let Some(confirmations) = block_height.checked_sub( inner.meets_target(conf_target)
tx_block_height },
.checked_sub(BlockHeight::new(1)) _ => false
.expect("transaction must be included in block with height >= 1"), })
) { .await?;
tracing::debug!(%txid, "confirmations: {:?}", confirmations);
if u32::from(confirmations) >= conf_target {
break;
}
}
interval.tick().await;
}
Ok(()) Ok(())
} }
@ -314,42 +259,258 @@ impl Wallet {
} }
} }
fn make_tx_status_url(base_url: &Url, txid: Txid) -> Result<Url> { struct Client {
let url = base_url.join(&format!("tx/{}/status", txid))?; electrum: bdk::electrum_client::Client,
latest_block: BlockHeight,
Ok(url) last_ping: Instant,
interval: Duration,
script_history: BTreeMap<Script, Vec<GetHistoryRes>>,
} }
fn make_blocks_tip_height_url(base_url: &Url) -> Result<Url> { impl Client {
let url = base_url.join("blocks/tip/height")?; fn new(electrum: bdk::electrum_client::Client, interval: Duration) -> Result<Self> {
let latest_block = electrum.block_headers_subscribe().map_err(|e| {
anyhow!(
"Electrum client failed to subscribe to header notifications: {:?}",
e
)
})?;
Ok(url) Ok(Self {
electrum,
latest_block: BlockHeight::try_from(latest_block)?,
last_ping: Instant::now(),
interval,
script_history: Default::default(),
})
}
/// Ping the electrum server unless we already did within the set interval.
///
/// Returns a boolean indicating whether we actually pinged the server.
fn ping(&mut self) -> bool {
if self.last_ping.elapsed() <= self.interval {
return false;
}
match self.electrum.ping() {
Ok(()) => {
self.last_ping = Instant::now();
true
}
Err(error) => {
tracing::debug!(?error, "Failed to ping electrum server");
false
}
}
}
fn drain_notifications(&mut self) -> Result<()> {
let pinged = self.ping();
if !pinged {
return Ok(());
}
self.drain_blockheight_notifications()?;
self.update_script_histories()?;
Ok(())
}
fn status_of_script(&mut self, script: &Script, txid: &Txid) -> Result<ScriptStatus> {
if !self.script_history.contains_key(script) {
self.script_history.insert(script.clone(), vec![]);
}
self.drain_notifications()?;
let history = self.script_history.entry(script.clone()).or_default();
let history_of_tx = history
.iter()
.filter(|entry| &entry.tx_hash == txid)
.collect::<Vec<_>>();
match history_of_tx.as_slice() {
[] => Ok(ScriptStatus::Unseen),
[remaining @ .., last] => {
if !remaining.is_empty() {
tracing::warn!("Found more than a single history entry for script. This is highly unexpected and those history entries will be ignored.")
}
if last.height <= 0 {
Ok(ScriptStatus::InMempool)
} else {
Ok(ScriptStatus::Confirmed(
Confirmed::from_inclusion_and_latest_block(
u32::try_from(last.height)?,
u32::from(self.latest_block),
),
))
}
}
}
}
fn drain_blockheight_notifications(&mut self) -> Result<()> {
let latest_block = std::iter::from_fn(|| self.electrum.block_headers_pop().transpose())
.last()
.transpose()
.map_err(|e| anyhow!("Failed to pop header notification: {:?}", e))?;
if let Some(new_block) = latest_block {
tracing::debug!(
"Got notification for new block at height {}",
new_block.height
);
self.latest_block = BlockHeight::try_from(new_block)?;
}
Ok(())
}
fn update_script_histories(&mut self) -> Result<()> {
let histories = self
.electrum
.batch_script_get_history(self.script_history.keys())
.map_err(|e| anyhow!("Failed to get script histories {:?}", e))?;
if histories.len() != self.script_history.len() {
bail!(
"Expected {} history entries, received {}",
self.script_history.len(),
histories.len()
);
}
let scripts = self.script_history.keys().cloned();
let histories = histories.into_iter();
self.script_history = scripts.zip(histories).collect::<BTreeMap<_, _>>();
Ok(())
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum ScriptStatus {
Unseen,
InMempool,
Confirmed(Confirmed),
}
impl ScriptStatus {
pub fn from_confirmations(confirmations: u32) -> Self {
match confirmations {
0 => Self::InMempool,
confirmations => Self::Confirmed(Confirmed::new(confirmations - 1)),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Confirmed {
/// The depth of this transaction within the blockchain.
///
/// Will be zero if the transaction is included in the latest block.
depth: u32,
}
impl Confirmed {
pub fn new(depth: u32) -> Self {
Self { depth }
}
/// Compute the depth of a transaction based on its inclusion height and the
/// latest known block.
///
/// Our information about the latest block might be outdated. To avoid an
/// overflow, we make sure the depth is 0 in case the inclusion height
/// exceeds our latest known block,
pub fn from_inclusion_and_latest_block(inclusion_height: u32, latest_block: u32) -> Self {
let depth = latest_block.saturating_sub(inclusion_height);
Self { depth }
}
pub fn confirmations(&self) -> u32 {
self.depth + 1
}
pub fn meets_target<T>(&self, target: T) -> bool
where
u32: PartialOrd<T>,
{
self.confirmations() >= target
}
}
impl ScriptStatus {
/// Check if the script has any confirmations.
pub fn is_confirmed(&self) -> bool {
matches!(self, ScriptStatus::Confirmed(_))
}
/// Check if the script has met the given confirmation target.
pub fn is_confirmed_with<T>(&self, target: T) -> bool
where
u32: PartialOrd<T>,
{
match self {
ScriptStatus::Confirmed(inner) => inner.meets_target(target),
_ => false,
}
}
pub fn has_been_seen(&self) -> bool {
matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_))
}
}
impl fmt::Display for ScriptStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScriptStatus::Unseen => write!(f, "unseen"),
ScriptStatus::InMempool => write!(f, "in mempool"),
ScriptStatus::Confirmed(inner) => {
write!(f, "confirmed with {} blocks", inner.confirmations())
}
}
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::cli::command::DEFAULT_ELECTRUM_HTTP_URL;
#[test] #[test]
fn create_tx_status_url_from_default_base_url_success() { fn given_depth_0_should_meet_confirmation_target_one() {
let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap(); let script = ScriptStatus::Confirmed(Confirmed { depth: 0 });
let txid = Txid::default;
let url = make_tx_status_url(&base_url, txid()).unwrap(); let confirmed = script.is_confirmed_with(1);
assert_eq!(url.as_str(), "https://blockstream.info/testnet/api/tx/0000000000000000000000000000000000000000000000000000000000000000/status"); assert!(confirmed)
} }
#[test] #[test]
fn create_block_tip_height_url_from_default_base_url_success() { fn given_confirmations_1_should_meet_confirmation_target_one() {
let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap(); let script = ScriptStatus::from_confirmations(1);
let url = make_blocks_tip_height_url(&base_url).unwrap(); let confirmed = script.is_confirmed_with(1);
assert_eq!( assert!(confirmed)
url.as_str(), }
"https://blockstream.info/testnet/api/blocks/tip/height"
); #[test]
fn given_inclusion_after_lastest_known_block_at_least_depth_0() {
let included_in = 10;
let latest_block = 9;
let confirmed = Confirmed::from_inclusion_and_latest_block(included_in, latest_block);
assert_eq!(confirmed.depth, 0)
} }
} }

View File

@ -40,8 +40,11 @@ pub enum Command {
#[structopt(flatten)] #[structopt(flatten)]
connect_params: AliceConnectParams, connect_params: AliceConnectParams,
#[structopt(flatten)] #[structopt(long = "electrum-rpc",
bitcoin_params: BitcoinParams, help = "Provide the Bitcoin Electrum RPC URL",
default_value = DEFAULT_ELECTRUM_RPC_URL
)]
electrum_rpc_url: Url,
#[structopt(flatten)] #[structopt(flatten)]
monero_params: MoneroParams, monero_params: MoneroParams,
@ -59,8 +62,11 @@ pub enum Command {
#[structopt(flatten)] #[structopt(flatten)]
connect_params: AliceConnectParams, connect_params: AliceConnectParams,
#[structopt(flatten)] #[structopt(long = "electrum-rpc",
bitcoin_params: BitcoinParams, help = "Provide the Bitcoin Electrum RPC URL",
default_value = DEFAULT_ELECTRUM_RPC_URL
)]
electrum_rpc_url: Url,
#[structopt(flatten)] #[structopt(flatten)]
monero_params: MoneroParams, monero_params: MoneroParams,
@ -76,8 +82,11 @@ pub enum Command {
#[structopt(short, long)] #[structopt(short, long)]
force: bool, force: bool,
#[structopt(flatten)] #[structopt(long = "electrum-rpc",
bitcoin_params: BitcoinParams, help = "Provide the Bitcoin Electrum RPC URL",
default_value = DEFAULT_ELECTRUM_RPC_URL
)]
electrum_rpc_url: Url,
}, },
/// Try to cancel a swap and refund my BTC (expert users only) /// Try to cancel a swap and refund my BTC (expert users only)
Refund { Refund {
@ -90,8 +99,11 @@ pub enum Command {
#[structopt(short, long)] #[structopt(short, long)]
force: bool, force: bool,
#[structopt(flatten)] #[structopt(long = "electrum-rpc",
bitcoin_params: BitcoinParams, help = "Provide the Bitcoin Electrum RPC URL",
default_value = DEFAULT_ELECTRUM_RPC_URL
)]
electrum_rpc_url: Url,
}, },
} }
@ -128,21 +140,6 @@ pub struct MoneroParams {
pub monero_daemon_host: String, pub monero_daemon_host: String,
} }
#[derive(structopt::StructOpt, Debug)]
pub struct BitcoinParams {
#[structopt(long = "electrum-http",
help = "Provide the Bitcoin Electrum HTTP URL",
default_value = DEFAULT_ELECTRUM_HTTP_URL
)]
pub electrum_http_url: Url,
#[structopt(long = "electrum-rpc",
help = "Provide the Bitcoin Electrum RPC URL",
default_value = DEFAULT_ELECTRUM_RPC_URL
)]
pub electrum_rpc_url: Url,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Data(pub PathBuf); pub struct Data(pub PathBuf);

1
swap/src/cli/config.rs Normal file
View File

@ -0,0 +1 @@

View File

@ -1,6 +1,5 @@
use crate::bitcoin::{ use crate::bitcoin::{
current_epoch, wait_for_cancel_timelock_to_expire, CancelTimelock, ExpiredTimelocks, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund,
PunishTimelock, TxCancel, TxPunish, TxRefund,
}; };
use crate::execution_params::ExecutionParams; use crate::execution_params::ExecutionParams;
use crate::protocol::alice::{Message1, Message3}; use crate::protocol::alice::{Message1, Message3};
@ -323,25 +322,36 @@ impl State3 {
&self, &self,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<()> { ) -> Result<()> {
wait_for_cancel_timelock_to_expire( bitcoin_wallet
bitcoin_wallet, .watch_until_status(
self.cancel_timelock,
self.tx_lock.txid(), self.tx_lock.txid(),
self.tx_lock.script_pubkey(),
|status| status.is_confirmed_with(self.cancel_timelock),
) )
.await .await?;
Ok(())
} }
pub async fn expired_timelocks( pub async fn expired_timelocks(
&self, &self,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<ExpiredTimelocks> { ) -> Result<ExpiredTimelocks> {
current_epoch( let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B);
bitcoin_wallet,
let tx_lock_status = bitcoin_wallet
.status_of_script(&self.tx_lock.script_pubkey(), &self.tx_lock.txid())
.await?;
let tx_cancel_status = bitcoin_wallet
.status_of_script(&tx_cancel.script_pubkey(), &tx_cancel.txid())
.await?;
Ok(current_epoch(
self.cancel_timelock, self.cancel_timelock,
self.punish_timelock, self.punish_timelock,
self.tx_lock.txid(), tx_lock_status,
) tx_cancel_status,
.await ))
} }
pub fn tx_punish(&self) -> TxPunish { pub fn tx_punish(&self) -> TxPunish {

View File

@ -1,12 +1,11 @@
use crate::bitcoin::{ use crate::bitcoin::{
poll_until_block_height_is_gte, BlockHeight, CancelTimelock, EncryptedSignature, CancelTimelock, EncryptedSignature, PunishTimelock, TxCancel, TxLock, TxRefund,
PunishTimelock, TxCancel, TxLock, TxRefund,
}; };
use crate::protocol::alice; use crate::protocol::alice;
use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::event_loop::EventLoopHandle;
use crate::protocol::alice::TransferProof; use crate::protocol::alice::TransferProof;
use crate::{bitcoin, monero}; use crate::{bitcoin, monero};
use anyhow::{Context, Result}; use anyhow::{bail, Context, Result};
use futures::future::{select, Either}; use futures::future::{select, Either};
use futures::pin_mut; use futures::pin_mut;
use libp2p::PeerId; use libp2p::PeerId;
@ -61,11 +60,11 @@ pub async fn publish_cancel_transaction(
tx_cancel_sig_bob: bitcoin::Signature, tx_cancel_sig_bob: bitcoin::Signature,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<bitcoin::TxCancel> { ) -> Result<bitcoin::TxCancel> {
// First wait for cancel timelock to expire bitcoin_wallet
let tx_lock_height = bitcoin_wallet .watch_until_status(tx_lock.txid(), tx_lock.script_pubkey(), |status| {
.transaction_block_height(tx_lock.txid()) status.is_confirmed_with(cancel_timelock)
})
.await?; .await?;
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await?;
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B); let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B);
@ -98,26 +97,36 @@ pub async fn publish_cancel_transaction(
pub async fn wait_for_bitcoin_refund( pub async fn wait_for_bitcoin_refund(
tx_cancel: &TxCancel, tx_cancel: &TxCancel,
cancel_tx_height: BlockHeight,
punish_timelock: PunishTimelock, punish_timelock: PunishTimelock,
refund_address: &bitcoin::Address, refund_address: &bitcoin::Address,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<(bitcoin::TxRefund, Option<bitcoin::Transaction>)> { ) -> Result<(bitcoin::TxRefund, Option<bitcoin::Transaction>)> {
let punish_timelock_expired =
poll_until_block_height_is_gte(bitcoin_wallet, cancel_tx_height + punish_timelock);
let tx_refund = bitcoin::TxRefund::new(tx_cancel, refund_address); let tx_refund = bitcoin::TxRefund::new(tx_cancel, refund_address);
// TODO(Franck): This only checks the mempool, need to cater for the case where let seen_refund_tx = bitcoin_wallet.watch_until_status(
// the transaction goes directly in a block tx_refund.txid(),
let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); refund_address.script_pubkey(),
|status| status.has_been_seen(),
);
let punish_timelock_expired =
bitcoin_wallet.watch_until_status(tx_cancel.txid(), tx_cancel.script_pubkey(), |status| {
status.is_confirmed_with(punish_timelock)
});
pin_mut!(punish_timelock_expired); pin_mut!(punish_timelock_expired);
pin_mut!(seen_refund_tx); pin_mut!(seen_refund_tx);
match select(punish_timelock_expired, seen_refund_tx).await { match select(punish_timelock_expired, seen_refund_tx).await {
Either::Left(_) => Ok((tx_refund, None)), Either::Left(_) => Ok((tx_refund, None)),
Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx?))), Either::Right((Ok(()), _)) => {
let published_refund_tx = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
Ok((tx_refund, Some(published_refund_tx)))
}
Either::Right((Err(e), _)) => {
bail!(e.context("Failed to monitor refund transaction"))
}
} }
} }

View File

@ -82,13 +82,21 @@ async fn run_until_internal(
} => { } => {
timeout( timeout(
execution_params.bob_time_to_act, execution_params.bob_time_to_act,
bitcoin_wallet.watch_for_raw_transaction(state3.tx_lock.txid()), bitcoin_wallet.watch_until_status(
state3.tx_lock.txid(),
state3.tx_lock.script_pubkey(),
|status| status.has_been_seen(),
),
) )
.await .await
.context("Failed to find lock Bitcoin tx")??; .context("Failed to find lock Bitcoin tx")??;
bitcoin_wallet bitcoin_wallet
.wait_for_transaction_finality(state3.tx_lock.txid(), execution_params) .wait_for_transaction_finality(
state3.tx_lock.txid(),
state3.tx_lock.script_pubkey(),
execution_params,
)
.await?; .await?;
let state = AliceState::BtcLocked { let state = AliceState::BtcLocked {
@ -213,7 +221,11 @@ async fn run_until_internal(
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
Ok(txid) => { Ok(txid) => {
let publishded_redeem_tx = bitcoin_wallet let publishded_redeem_tx = bitcoin_wallet
.wait_for_transaction_finality(txid, execution_params) .wait_for_transaction_finality(
txid,
state3.redeem_address.script_pubkey(),
execution_params,
)
.await; .await;
match publishded_redeem_tx { match publishded_redeem_tx {
@ -308,13 +320,8 @@ async fn run_until_internal(
tx_cancel, tx_cancel,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
} => { } => {
let tx_cancel_height = bitcoin_wallet
.transaction_block_height(tx_cancel.txid())
.await?;
let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund( let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund(
&tx_cancel, &tx_cancel,
tx_cancel_height,
state3.punish_timelock, state3.punish_timelock,
&state3.refund_address, &state3.refund_address,
&bitcoin_wallet, &bitcoin_wallet,
@ -404,25 +411,34 @@ async fn run_until_internal(
state3.B, state3.B,
)?; )?;
let punish_script_pubkey = state3.punish_address.script_pubkey();
let punish_tx_finalised = async { let punish_tx_finalised = async {
let txid = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; let txid = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
bitcoin_wallet bitcoin_wallet
.wait_for_transaction_finality(txid, execution_params) .wait_for_transaction_finality(txid, punish_script_pubkey, execution_params)
.await?; .await?;
Result::<_, anyhow::Error>::Ok(txid) Result::<_, anyhow::Error>::Ok(txid)
}; };
let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid()); let refund_tx_seen = bitcoin_wallet.watch_until_status(
tx_refund.txid(),
state3.refund_address.script_pubkey(),
|status| status.has_been_seen(),
);
pin_mut!(punish_tx_finalised); pin_mut!(punish_tx_finalised);
pin_mut!(refund_tx_seen); pin_mut!(refund_tx_seen);
match select(refund_tx_seen, punish_tx_finalised).await { match select(refund_tx_seen, punish_tx_finalised).await {
Either::Left((published_refund_tx, _)) => { Either::Left((Ok(()), _)) => {
let published_refund_tx =
bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
let spend_key = extract_monero_private_key( let spend_key = extract_monero_private_key(
published_refund_tx?, published_refund_tx,
&tx_refund, &tx_refund,
state3.s_a, state3.s_a,
state3.a.clone(), state3.a.clone(),
@ -448,6 +464,9 @@ async fn run_until_internal(
) )
.await .await
} }
Either::Left((Err(e), _)) => {
bail!(e.context("Failed to monitor refund transaction"))
}
Either::Right(_) => { Either::Right(_) => {
let state = AliceState::BtcPunished; let state = AliceState::BtcPunished;
let db_state = (&state).into(); let db_state = (&state).into();

View File

@ -1,6 +1,6 @@
use crate::bitcoin::{ use crate::bitcoin::{
self, current_epoch, wait_for_cancel_timelock_to_expire, CancelTimelock, ExpiredTimelocks, self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
PunishTimelock, Transaction, TxCancel, Txid, TxLock, Txid,
}; };
use crate::execution_params::ExecutionParams; use crate::execution_params::ExecutionParams;
use crate::monero; use crate::monero;
@ -9,7 +9,7 @@ use crate::monero_ext::ScalarExt;
use crate::protocol::alice::{Message1, Message3}; use crate::protocol::alice::{Message1, Message3};
use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4}; use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4};
use crate::protocol::CROSS_CURVE_PROOF_SYSTEM; use crate::protocol::CROSS_CURVE_PROOF_SYSTEM;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Result};
use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
use ecdsa_fun::nonce::Deterministic; use ecdsa_fun::nonce::Deterministic;
use ecdsa_fun::Signature; use ecdsa_fun::Signature;
@ -262,15 +262,9 @@ impl State2 {
} }
} }
pub async fn lock_btc(self, bitcoin_wallet: &bitcoin::Wallet) -> Result<State3> { pub async fn lock_btc(self) -> Result<(State3, TxLock)> {
let signed_tx = bitcoin_wallet Ok((
.sign_and_finalize(self.tx_lock.clone().into()) State3 {
.await
.context("Failed to sign Bitcoin lock transaction")?;
let _ = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
Ok(State3 {
A: self.A, A: self.A,
b: self.b, b: self.b,
s_b: self.s_b, s_b: self.s_b,
@ -282,11 +276,13 @@ impl State2 {
punish_timelock: self.punish_timelock, punish_timelock: self.punish_timelock,
refund_address: self.refund_address, refund_address: self.refund_address,
redeem_address: self.redeem_address, redeem_address: self.redeem_address,
tx_lock: self.tx_lock, tx_lock: self.tx_lock.clone(),
tx_cancel_sig_a: self.tx_cancel_sig_a, tx_cancel_sig_a: self.tx_cancel_sig_a,
tx_refund_encsig: self.tx_refund_encsig, tx_refund_encsig: self.tx_refund_encsig,
min_monero_confirmations: self.min_monero_confirmations, min_monero_confirmations: self.min_monero_confirmations,
}) },
self.tx_lock,
))
} }
} }
@ -354,12 +350,14 @@ impl State3 {
&self, &self,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<()> { ) -> Result<()> {
wait_for_cancel_timelock_to_expire( bitcoin_wallet
bitcoin_wallet, .watch_until_status(
self.cancel_timelock,
self.tx_lock.txid(), self.tx_lock.txid(),
self.tx_lock.script_pubkey(),
|status| status.is_confirmed_with(self.cancel_timelock),
) )
.await .await?;
Ok(())
} }
pub fn cancel(&self) -> State4 { pub fn cancel(&self) -> State4 {
@ -390,13 +388,21 @@ impl State3 {
&self, &self,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<ExpiredTimelocks> { ) -> Result<ExpiredTimelocks> {
current_epoch( let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
bitcoin_wallet,
let tx_lock_status = bitcoin_wallet
.status_of_script(&self.tx_lock.script_pubkey(), &self.tx_lock.txid())
.await?;
let tx_cancel_status = bitcoin_wallet
.status_of_script(&tx_cancel.script_pubkey(), &tx_cancel.txid())
.await?;
Ok(current_epoch(
self.cancel_timelock, self.cancel_timelock,
self.punish_timelock, self.punish_timelock,
self.tx_lock.txid(), tx_lock_status,
) tx_cancel_status,
.await ))
} }
} }
@ -419,10 +425,9 @@ pub struct State4 {
impl State4 { impl State4 {
pub fn next_message(&self) -> EncryptedSignature { pub fn next_message(&self) -> EncryptedSignature {
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); EncryptedSignature {
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); tx_redeem_encsig: self.tx_redeem_encsig(),
}
EncryptedSignature { tx_redeem_encsig }
} }
pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature { pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature {
@ -466,10 +471,16 @@ impl State4 {
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
let tx_redeem_candidate = bitcoin_wallet bitcoin_wallet
.watch_for_raw_transaction(tx_redeem.txid()) .watch_until_status(
tx_redeem.txid(),
self.redeem_address.script_pubkey(),
|status| status.has_been_seen(),
)
.await?; .await?;
let tx_redeem_candidate = bitcoin_wallet.get_raw_transaction(tx_redeem.txid()).await?;
let tx_redeem_sig = let tx_redeem_sig =
tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?; tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?;
let s_a = bitcoin::recover(self.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?; let s_a = bitcoin::recover(self.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?;
@ -488,25 +499,36 @@ impl State4 {
&self, &self,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<()> { ) -> Result<()> {
wait_for_cancel_timelock_to_expire( bitcoin_wallet
bitcoin_wallet, .watch_until_status(
self.cancel_timelock,
self.tx_lock.txid(), self.tx_lock.txid(),
self.tx_lock.script_pubkey(),
|status| status.is_confirmed_with(self.cancel_timelock),
) )
.await .await?;
Ok(())
} }
pub async fn expired_timelock( pub async fn expired_timelock(
&self, &self,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<ExpiredTimelocks> { ) -> Result<ExpiredTimelocks> {
current_epoch( let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
bitcoin_wallet,
let tx_lock_status = bitcoin_wallet
.status_of_script(&self.tx_lock.script_pubkey(), &self.tx_lock.txid())
.await?;
let tx_cancel_status = bitcoin_wallet
.status_of_script(&tx_cancel.script_pubkey(), &tx_cancel.txid())
.await?;
Ok(current_epoch(
self.cancel_timelock, self.cancel_timelock,
self.punish_timelock, self.punish_timelock,
self.tx_lock.txid(), tx_lock_status,
) tx_cancel_status,
.await ))
} }
pub async fn refund_btc( pub async fn refund_btc(
@ -530,7 +552,11 @@ impl State4 {
let txid = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; let txid = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?;
bitcoin_wallet bitcoin_wallet
.wait_for_transaction_finality(txid, execution_params) .wait_for_transaction_finality(
txid,
self.refund_address.script_pubkey(),
execution_params,
)
.await?; .await?;
Ok(()) Ok(())

View File

@ -6,7 +6,7 @@ use crate::protocol::bob;
use crate::protocol::bob::event_loop::EventLoopHandle; use crate::protocol::bob::event_loop::EventLoopHandle;
use crate::protocol::bob::state::*; use crate::protocol::bob::state::*;
use crate::{bitcoin, monero}; use crate::{bitcoin, monero};
use anyhow::{bail, Result}; use anyhow::{bail, Context, Result};
use async_recursion::async_recursion; use async_recursion::async_recursion;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use std::sync::Arc; use std::sync::Arc;
@ -99,7 +99,18 @@ async fn run_until_internal(
// Do not lock Bitcoin if not connected to Alice. // Do not lock Bitcoin if not connected to Alice.
event_loop_handle.dial().await?; event_loop_handle.dial().await?;
// Alice and Bob have exchanged info // Alice and Bob have exchanged info
let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?; let (state3, tx_lock) = state2.lock_btc().await?;
let signed_tx = bitcoin_wallet
.sign_and_finalize(tx_lock.clone().into())
.await
.context("Failed to sign Bitcoin lock transaction")?;
let tx_lock_id = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
bitcoin_wallet
.watch_until_status(tx_lock_id, tx_lock.script_pubkey(), |status| {
status.is_confirmed()
})
.await?;
let state = BobState::BtcLocked(state3); let state = BobState::BtcLocked(state3);
let db_state = state.clone().into(); let db_state = state.clone().into();

View File

@ -342,10 +342,6 @@ where
.electrs .electrs
.get_host_port(testutils::electrs::RPC_PORT) .get_host_port(testutils::electrs::RPC_PORT)
.expect("Could not map electrs rpc port"); .expect("Could not map electrs rpc port");
let electrs_http_port = containers
.electrs
.get_host_port(testutils::electrs::HTTP_PORT)
.expect("Could not map electrs http port");
let alice_seed = Seed::random().unwrap(); let alice_seed = Seed::random().unwrap();
let bob_seed = Seed::random().unwrap(); let bob_seed = Seed::random().unwrap();
@ -357,7 +353,6 @@ where
alice_starting_balances.clone(), alice_starting_balances.clone(),
tempdir().unwrap().path(), tempdir().unwrap().path(),
electrs_rpc_port, electrs_rpc_port,
electrs_http_port,
alice_seed, alice_seed,
execution_params, execution_params,
) )
@ -380,7 +375,6 @@ where
bob_starting_balances.clone(), bob_starting_balances.clone(),
tempdir().unwrap().path(), tempdir().unwrap().path(),
electrs_rpc_port, electrs_rpc_port,
electrs_http_port,
bob_seed, bob_seed,
execution_params, execution_params,
) )
@ -588,7 +582,6 @@ async fn init_test_wallets(
starting_balances: StartingBalances, starting_balances: StartingBalances,
datadir: &Path, datadir: &Path,
electrum_rpc_port: u16, electrum_rpc_port: u16,
electrum_http_port: u16,
seed: Seed, seed: Seed,
execution_params: ExecutionParams, execution_params: ExecutionParams,
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) { ) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
@ -608,14 +601,9 @@ async fn init_test_wallets(
let input = format!("tcp://@localhost:{}", electrum_rpc_port); let input = format!("tcp://@localhost:{}", electrum_rpc_port);
Url::parse(&input).unwrap() Url::parse(&input).unwrap()
}; };
let electrum_http_url = {
let input = format!("http://@localhost:{}", electrum_http_port);
Url::parse(&input).unwrap()
};
let btc_wallet = swap::bitcoin::Wallet::new( let btc_wallet = swap::bitcoin::Wallet::new(
electrum_rpc_url, electrum_rpc_url,
electrum_http_url,
bitcoin::Network::Regtest, bitcoin::Network::Regtest,
datadir, datadir,
seed.derive_extended_private_key(bitcoin::Network::Regtest) seed.derive_extended_private_key(bitcoin::Network::Regtest)