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;
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_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)]
#[serde(deny_unknown_fields)]
pub struct Bitcoin {
pub electrum_http_url: Url,
pub electrum_rpc_url: Url,
}
@ -120,12 +118,6 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
.interact_text()?;
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())
.with_prompt("Enter Electrum RPC URL or hit return to use default")
.default(DEFAULT_ELECTRUM_RPC_URL.to_owned())
@ -144,10 +136,7 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
network: Network {
listen: listen_address,
},
bitcoin: Bitcoin {
electrum_http_url,
electrum_rpc_url,
},
bitcoin: Bitcoin { electrum_rpc_url },
monero: Monero {
wallet_rpc_url: monero_wallet_rpc_url,
},
@ -170,7 +159,6 @@ mod tests {
dir: Default::default(),
},
bitcoin: Bitcoin {
electrum_http_url: Url::from_str(DEFAULT_ELECTRUM_HTTP_URL).unwrap(),
electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(),
},
network: Network {

View File

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

View File

@ -21,9 +21,7 @@ use std::sync::Arc;
use std::time::Duration;
use structopt::StructOpt;
use swap::bitcoin::{Amount, TxLock};
use swap::cli::command::{
AliceConnectParams, Arguments, BitcoinParams, Command, Data, MoneroParams,
};
use swap::cli::command::{AliceConnectParams, Arguments, Command, Data, MoneroParams};
use swap::database::Database;
use swap::execution_params::{ExecutionParams, GetExecutionParams};
use swap::network::quote::BidQuote;
@ -95,11 +93,7 @@ async fn main() -> Result<()> {
receive_monero_address,
monero_daemon_host,
},
bitcoin_params:
BitcoinParams {
electrum_http_url,
electrum_rpc_url,
},
electrum_rpc_url,
} => {
if receive_monero_address.network != monero_network {
bail!(
@ -109,14 +103,9 @@ async fn main() -> Result<()> {
)
}
let bitcoin_wallet = init_bitcoin_wallet(
bitcoin_network,
electrum_rpc_url,
electrum_http_url,
seed,
data_dir.clone(),
)
.await?;
let bitcoin_wallet =
init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir.clone())
.await?;
let (monero_wallet, _process) = init_monero_wallet(
monero_network,
data_dir,
@ -196,24 +185,15 @@ async fn main() -> Result<()> {
receive_monero_address,
monero_daemon_host,
},
bitcoin_params:
BitcoinParams {
electrum_http_url,
electrum_rpc_url,
},
electrum_rpc_url,
} => {
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)
}
let bitcoin_wallet = init_bitcoin_wallet(
bitcoin_network,
electrum_rpc_url,
electrum_http_url,
seed,
data_dir.clone(),
)
.await?;
let bitcoin_wallet =
init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir.clone())
.await?;
let (monero_wallet, _process) = init_monero_wallet(
monero_network,
data_dir,
@ -255,20 +235,10 @@ async fn main() -> Result<()> {
Command::Cancel {
swap_id,
force,
bitcoin_params:
BitcoinParams {
electrum_http_url,
electrum_rpc_url,
},
electrum_rpc_url,
} => {
let bitcoin_wallet = init_bitcoin_wallet(
bitcoin_network,
electrum_rpc_url,
electrum_http_url,
seed,
data_dir,
)
.await?;
let bitcoin_wallet =
init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir).await?;
let resume_state = db.get_state(swap_id)?.try_into_bob()?.into();
let cancel =
@ -290,20 +260,10 @@ async fn main() -> Result<()> {
Command::Refund {
swap_id,
force,
bitcoin_params:
BitcoinParams {
electrum_http_url,
electrum_rpc_url,
},
electrum_rpc_url,
} => {
let bitcoin_wallet = init_bitcoin_wallet(
bitcoin_network,
electrum_rpc_url,
electrum_http_url,
seed,
data_dir,
)
.await?;
let bitcoin_wallet =
init_bitcoin_wallet(bitcoin_network, electrum_rpc_url, seed, data_dir).await?;
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(
network: bitcoin::Network,
electrum_rpc_url: Url,
electrum_http_url: Url,
seed: Seed,
data_dir: PathBuf,
) -> Result<bitcoin::Wallet> {
@ -332,7 +291,6 @@ async fn init_bitcoin_wallet(
let wallet = bitcoin::Wallet::new(
electrum_rpc_url.clone(),
electrum_http_url.clone(),
network,
&wallet_dir,
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 wallet::Wallet;
use crate::bitcoin::wallet::ScriptStatus;
use ::bitcoin::hashes::hex::ToHex;
use ::bitcoin::hashes::Hash;
use ::bitcoin::{secp256k1, SigHash};
@ -218,46 +219,21 @@ pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Resu
Ok(s)
}
pub async fn poll_until_block_height_is_gte(
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,
pub fn current_epoch(
cancel_timelock: CancelTimelock,
punish_timelock: PunishTimelock,
lock_tx_id: ::bitcoin::Txid,
) -> Result<ExpiredTimelocks> {
let current_block_height = bitcoin_wallet.get_block_height().await?;
let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?;
let cancel_timelock_height = lock_tx_height + cancel_timelock;
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),
tx_lock_status: ScriptStatus,
tx_cancel_status: ScriptStatus,
) -> ExpiredTimelocks {
if tx_cancel_status.is_confirmed_with(punish_timelock) {
return ExpiredTimelocks::Punish;
}
}
pub async fn wait_for_cancel_timelock_to_expire(
bitcoin_wallet: &crate::bitcoin::Wallet,
cancel_timelock: CancelTimelock,
lock_tx_id: ::bitcoin::Txid,
) -> Result<()> {
let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?;
if tx_lock_status.is_confirmed_with(cancel_timelock) {
return ExpiredTimelocks::Cancel;
}
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await?;
Ok(())
ExpiredTimelocks::None
}
#[derive(Clone, Copy, thiserror::Error, Debug)]
@ -275,3 +251,53 @@ pub struct EmptyWitnessStack;
#[derive(Clone, Copy, thiserror::Error, Debug)]
#[error("input has {0} witnesses, expected 3")]
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::{OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid};
use anyhow::Result;
use bitcoin::Script;
use ecdsa_fun::Signature;
use miniscript::{Descriptor, DescriptorTrait};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashMap;
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
/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
/// 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)]
pub struct TxCancel {
inner: Transaction,
@ -122,6 +148,16 @@ impl TxCancel {
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(
self,
(A, sig_a): (PublicKey, Signature),

View File

@ -4,6 +4,7 @@ use crate::bitcoin::{
use ::bitcoin::util::psbt::PartiallySignedTransaction;
use ::bitcoin::{OutPoint, TxIn, TxOut, Txid};
use anyhow::Result;
use bitcoin::Script;
use ecdsa_fun::fun::Point;
use miniscript::{Descriptor, DescriptorTrait};
use rand::thread_rng;
@ -55,6 +56,10 @@ impl TxLock {
.len()
}
pub fn script_pubkey(&self) -> Script {
self.output_descriptor.script_pubkey()
}
/// Retreive the index of the locked output in the transaction outputs
/// vector
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 std::convert::{TryFrom, TryInto};
use std::ops::Add;
/// 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 {
type Output = BlockHeight;
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 {
None,
Cancel,

View File

@ -4,48 +4,31 @@ use crate::execution_params::ExecutionParams;
use ::bitcoin::util::psbt::PartiallySignedTransaction;
use ::bitcoin::Txid;
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::descriptor::Segwitv0;
use bdk::electrum_client::{self, Client, ElectrumApi};
use bdk::electrum_client::{self, ElectrumApi, GetHistoryRes};
use bdk::keys::DerivableKey;
use bdk::{FeeRate, KeychainKind};
use bitcoin::Script;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
use tokio::time::interval;
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 {
inner: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>,
http_url: Url,
rpc_url: Url,
client: Arc<Mutex<Client>>,
wallet: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>,
}
impl Wallet {
pub async fn new(
electrum_rpc_url: Url,
electrum_http_url: Url,
network: bitcoin::Network,
wallet_dir: &Path,
key: impl DerivableKey<Segwitv0> + Clone,
@ -53,8 +36,9 @@ impl Wallet {
// Workaround for https://github.com/bitcoindevkit/rust-electrum-client/issues/47.
let config = electrum_client::ConfigBuilder::default().retry(2).build();
let client = Client::from_config(electrum_rpc_url.as_str(), config)
.map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?;
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))?;
let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
@ -66,16 +50,20 @@ impl Wallet {
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 {
inner: Arc::new(Mutex::new(bdk_wallet)),
http_url: electrum_http_url,
rpc_url: electrum_rpc_url,
wallet: Arc::new(Mutex::new(bdk_wallet)),
client: Arc::new(Mutex::new(Client::new(electrum, interval)?)),
})
}
pub async fn balance(&self) -> Result<Amount> {
let balance = self
.inner
.wallet
.lock()
.await
.get_balance()
@ -86,7 +74,7 @@ impl Wallet {
pub async fn new_address(&self) -> Result<Address> {
let address = self
.inner
.wallet
.lock()
.await
.get_new_address()
@ -96,13 +84,14 @@ impl Wallet {
}
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)
}
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
let fees = self
.inner
.wallet
.lock()
.await
.list_transactions(true)?
@ -117,7 +106,7 @@ impl Wallet {
}
pub async fn sync(&self) -> Result<()> {
self.inner
self.wallet
.lock()
.await
.sync(noop_progress(), None)
@ -131,7 +120,7 @@ impl Wallet {
address: Address,
amount: Amount,
) -> Result<PartiallySignedTransaction> {
let wallet = self.inner.lock().await;
let wallet = self.wallet.lock().await;
let mut tx_builder = wallet.build_tx();
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
/// transaction confirmed.
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();
@ -163,7 +152,7 @@ impl Wallet {
}
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
@ -171,7 +160,7 @@ impl Wallet {
pub async fn broadcast(&self, transaction: Transaction, kind: &str) -> Result<Txid> {
let txid = transaction.txid();
self.inner
self.wallet
.lock()
.await
.broadcast(transaction)
@ -185,7 +174,7 @@ impl Wallet {
}
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 {
bail!("PSBT is not finalized")
@ -202,106 +191,62 @@ impl Wallet {
.ok_or_else(|| anyhow!("Could not get raw tx with id: {}", txid))
}
pub async fn watch_for_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
tracing::debug!("watching for tx: {}", 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 status_of_script(&self, script: &Script, txid: &Txid) -> Result<ScriptStatus> {
self.client.lock().await.status_of_script(script, txid)
}
pub async fn get_block_height(&self) -> Result<BlockHeight> {
let url = make_blocks_tip_height_url(&self.http_url)?;
pub async fn watch_until_status(
&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 {
let height = reqwest::get(url.clone())
.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")?;
loop {
let new_status = self.client.lock().await.status_of_script(&script, &txid)?;
Ok(BlockHeight::new(height))
}
if Some(new_status) != last_status {
tracing::debug!(%txid, "Transaction is {}", new_status);
}
last_status = Some(new_status);
pub async fn transaction_block_height(&self, txid: Txid) -> Result<BlockHeight> {
let status_url = make_tx_status_url(&self.http_url, txid)?;
if status_fn(new_status) {
break;
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct TransactionStatus {
block_height: Option<u32>,
confirmed: bool,
tokio::time::sleep(Duration::from_secs(5)).await;
}
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)
})
.await
.context("Transient errors should be retried")?;
Ok(BlockHeight::new(height))
Ok(())
}
pub async fn wait_for_transaction_finality(
&self,
txid: Txid,
script_to_watch: Script,
execution_params: ExecutionParams,
) -> Result<()> {
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 { "" });
// Divide by 4 to not check too often yet still be aware of the new block early
// on.
let mut interval = interval(execution_params.bitcoin_avg_block_time / 4);
let mut seen_confirmations = 0;
loop {
let tx_block_height = self.transaction_block_height(txid).await?;
tracing::debug!("tx_block_height: {:?}", tx_block_height);
self.watch_until_status(txid, script_to_watch, |status| match status {
ScriptStatus::Confirmed(inner) => {
let confirmations = inner.confirmations();
let block_height = self.get_block_height().await?;
tracing::debug!("latest_block_height: {:?}", block_height);
if let Some(confirmations) = block_height.checked_sub(
tx_block_height
.checked_sub(BlockHeight::new(1))
.expect("transaction must be included in block with height >= 1"),
) {
tracing::debug!(%txid, "confirmations: {:?}", confirmations);
if u32::from(confirmations) >= conf_target {
break;
if confirmations > seen_confirmations {
tracing::info!(%txid, "Bitcoin tx has {} out of {} confirmation{}", confirmations, conf_target, if conf_target > 1 { "s" } else { "" });
seen_confirmations = confirmations;
}
}
interval.tick().await;
}
inner.meets_target(conf_target)
},
_ => false
})
.await?;
Ok(())
}
@ -314,42 +259,258 @@ impl Wallet {
}
}
fn make_tx_status_url(base_url: &Url, txid: Txid) -> Result<Url> {
let url = base_url.join(&format!("tx/{}/status", txid))?;
Ok(url)
struct Client {
electrum: bdk::electrum_client::Client,
latest_block: BlockHeight,
last_ping: Instant,
interval: Duration,
script_history: BTreeMap<Script, Vec<GetHistoryRes>>,
}
fn make_blocks_tip_height_url(base_url: &Url) -> Result<Url> {
let url = base_url.join("blocks/tip/height")?;
impl Client {
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)]
mod tests {
use super::*;
use crate::cli::command::DEFAULT_ELECTRUM_HTTP_URL;
#[test]
fn create_tx_status_url_from_default_base_url_success() {
let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap();
let txid = Txid::default;
fn given_depth_0_should_meet_confirmation_target_one() {
let script = ScriptStatus::Confirmed(Confirmed { depth: 0 });
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]
fn create_block_tip_height_url_from_default_base_url_success() {
let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap();
fn given_confirmations_1_should_meet_confirmation_target_one() {
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!(
url.as_str(),
"https://blockstream.info/testnet/api/blocks/tip/height"
);
assert!(confirmed)
}
#[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)]
connect_params: AliceConnectParams,
#[structopt(flatten)]
bitcoin_params: BitcoinParams,
#[structopt(long = "electrum-rpc",
help = "Provide the Bitcoin Electrum RPC URL",
default_value = DEFAULT_ELECTRUM_RPC_URL
)]
electrum_rpc_url: Url,
#[structopt(flatten)]
monero_params: MoneroParams,
@ -59,8 +62,11 @@ pub enum Command {
#[structopt(flatten)]
connect_params: AliceConnectParams,
#[structopt(flatten)]
bitcoin_params: BitcoinParams,
#[structopt(long = "electrum-rpc",
help = "Provide the Bitcoin Electrum RPC URL",
default_value = DEFAULT_ELECTRUM_RPC_URL
)]
electrum_rpc_url: Url,
#[structopt(flatten)]
monero_params: MoneroParams,
@ -76,8 +82,11 @@ pub enum Command {
#[structopt(short, long)]
force: bool,
#[structopt(flatten)]
bitcoin_params: BitcoinParams,
#[structopt(long = "electrum-rpc",
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)
Refund {
@ -90,8 +99,11 @@ pub enum Command {
#[structopt(short, long)]
force: bool,
#[structopt(flatten)]
bitcoin_params: BitcoinParams,
#[structopt(long = "electrum-rpc",
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,
}
#[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)]
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::{
current_epoch, wait_for_cancel_timelock_to_expire, CancelTimelock, ExpiredTimelocks,
PunishTimelock, TxCancel, TxPunish, TxRefund,
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund,
};
use crate::execution_params::ExecutionParams;
use crate::protocol::alice::{Message1, Message3};
@ -323,25 +322,36 @@ impl State3 {
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<()> {
wait_for_cancel_timelock_to_expire(
bitcoin_wallet,
self.cancel_timelock,
self.tx_lock.txid(),
)
.await
bitcoin_wallet
.watch_until_status(
self.tx_lock.txid(),
self.tx_lock.script_pubkey(),
|status| status.is_confirmed_with(self.cancel_timelock),
)
.await?;
Ok(())
}
pub async fn expired_timelocks(
&self,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<ExpiredTimelocks> {
current_epoch(
bitcoin_wallet,
let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B);
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.punish_timelock,
self.tx_lock.txid(),
)
.await
tx_lock_status,
tx_cancel_status,
))
}
pub fn tx_punish(&self) -> TxPunish {

View File

@ -1,12 +1,11 @@
use crate::bitcoin::{
poll_until_block_height_is_gte, BlockHeight, CancelTimelock, EncryptedSignature,
PunishTimelock, TxCancel, TxLock, TxRefund,
CancelTimelock, EncryptedSignature, PunishTimelock, TxCancel, TxLock, TxRefund,
};
use crate::protocol::alice;
use crate::protocol::alice::event_loop::EventLoopHandle;
use crate::protocol::alice::TransferProof;
use crate::{bitcoin, monero};
use anyhow::{Context, Result};
use anyhow::{bail, Context, Result};
use futures::future::{select, Either};
use futures::pin_mut;
use libp2p::PeerId;
@ -61,11 +60,11 @@ pub async fn publish_cancel_transaction(
tx_cancel_sig_bob: bitcoin::Signature,
bitcoin_wallet: &bitcoin::Wallet,
) -> Result<bitcoin::TxCancel> {
// First wait for cancel timelock to expire
let tx_lock_height = bitcoin_wallet
.transaction_block_height(tx_lock.txid())
bitcoin_wallet
.watch_until_status(tx_lock.txid(), tx_lock.script_pubkey(), |status| {
status.is_confirmed_with(cancel_timelock)
})
.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);
@ -98,26 +97,36 @@ pub async fn publish_cancel_transaction(
pub async fn wait_for_bitcoin_refund(
tx_cancel: &TxCancel,
cancel_tx_height: BlockHeight,
punish_timelock: PunishTimelock,
refund_address: &bitcoin::Address,
bitcoin_wallet: &bitcoin::Wallet,
) -> 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);
// TODO(Franck): This only checks the mempool, need to cater for the case where
// the transaction goes directly in a block
let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
let seen_refund_tx = bitcoin_wallet.watch_until_status(
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!(seen_refund_tx);
match select(punish_timelock_expired, seen_refund_tx).await {
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(
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
.context("Failed to find lock Bitcoin tx")??;
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?;
let state = AliceState::BtcLocked {
@ -213,7 +221,11 @@ async fn run_until_internal(
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
Ok(txid) => {
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;
match publishded_redeem_tx {
@ -308,13 +320,8 @@ async fn run_until_internal(
tx_cancel,
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(
&tx_cancel,
tx_cancel_height,
state3.punish_timelock,
&state3.refund_address,
&bitcoin_wallet,
@ -404,25 +411,34 @@ async fn run_until_internal(
state3.B,
)?;
let punish_script_pubkey = state3.punish_address.script_pubkey();
let punish_tx_finalised = async {
let txid = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
bitcoin_wallet
.wait_for_transaction_finality(txid, execution_params)
.wait_for_transaction_finality(txid, punish_script_pubkey, execution_params)
.await?;
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!(refund_tx_seen);
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(
published_refund_tx?,
published_refund_tx,
&tx_refund,
state3.s_a,
state3.a.clone(),
@ -448,6 +464,9 @@ async fn run_until_internal(
)
.await
}
Either::Left((Err(e), _)) => {
bail!(e.context("Failed to monitor refund transaction"))
}
Either::Right(_) => {
let state = AliceState::BtcPunished;
let db_state = (&state).into();

View File

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

View File

@ -6,7 +6,7 @@ use crate::protocol::bob;
use crate::protocol::bob::event_loop::EventLoopHandle;
use crate::protocol::bob::state::*;
use crate::{bitcoin, monero};
use anyhow::{bail, Result};
use anyhow::{bail, Context, Result};
use async_recursion::async_recursion;
use rand::rngs::OsRng;
use std::sync::Arc;
@ -99,7 +99,18 @@ async fn run_until_internal(
// Do not lock Bitcoin if not connected to Alice.
event_loop_handle.dial().await?;
// 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 db_state = state.clone().into();

View File

@ -342,10 +342,6 @@ where
.electrs
.get_host_port(testutils::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 bob_seed = Seed::random().unwrap();
@ -357,7 +353,6 @@ where
alice_starting_balances.clone(),
tempdir().unwrap().path(),
electrs_rpc_port,
electrs_http_port,
alice_seed,
execution_params,
)
@ -380,7 +375,6 @@ where
bob_starting_balances.clone(),
tempdir().unwrap().path(),
electrs_rpc_port,
electrs_http_port,
bob_seed,
execution_params,
)
@ -588,7 +582,6 @@ async fn init_test_wallets(
starting_balances: StartingBalances,
datadir: &Path,
electrum_rpc_port: u16,
electrum_http_port: u16,
seed: Seed,
execution_params: ExecutionParams,
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
@ -608,14 +601,9 @@ async fn init_test_wallets(
let input = format!("tcp://@localhost:{}", electrum_rpc_port);
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(
electrum_rpc_url,
electrum_http_url,
bitcoin::Network::Regtest,
datadir,
seed.derive_extended_private_key(bitcoin::Network::Regtest)