307: Reduce load on electrum r=thomaseizinger a=rishflab

.

Co-authored-by: rishflab <rishflab@hotmail.com>
Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
bors[bot] 2021-03-17 05:10:50 +00:00 committed by GitHub
commit 95acbc6277
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 819 additions and 610 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

@ -152,8 +152,8 @@ 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,
execution_params.bitcoin_finality_confirmations,
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: electrum_rpc_url,
BitcoinParams {
electrum_http_url,
electrum_rpc_url,
},
} => { } => {
if receive_monero_address.network != monero_network { if receive_monero_address.network != monero_network {
bail!( bail!(
@ -112,9 +106,9 @@ async fn main() -> Result<()> {
let bitcoin_wallet = init_bitcoin_wallet( let bitcoin_wallet = init_bitcoin_wallet(
bitcoin_network, bitcoin_network,
electrum_rpc_url, electrum_rpc_url,
electrum_http_url,
seed, seed,
data_dir.clone(), data_dir.clone(),
execution_params,
) )
.await?; .await?;
let (monero_wallet, _process) = init_monero_wallet( let (monero_wallet, _process) = init_monero_wallet(
@ -196,11 +190,7 @@ async fn main() -> Result<()> {
receive_monero_address, receive_monero_address,
monero_daemon_host, monero_daemon_host,
}, },
bitcoin_params: electrum_rpc_url,
BitcoinParams {
electrum_http_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)
@ -209,9 +199,9 @@ async fn main() -> Result<()> {
let bitcoin_wallet = init_bitcoin_wallet( let bitcoin_wallet = init_bitcoin_wallet(
bitcoin_network, bitcoin_network,
electrum_rpc_url, electrum_rpc_url,
electrum_http_url,
seed, seed,
data_dir.clone(), data_dir.clone(),
execution_params,
) )
.await?; .await?;
let (monero_wallet, _process) = init_monero_wallet( let (monero_wallet, _process) = init_monero_wallet(
@ -255,18 +245,14 @@ async fn main() -> Result<()> {
Command::Cancel { Command::Cancel {
swap_id, swap_id,
force, force,
bitcoin_params: electrum_rpc_url,
BitcoinParams {
electrum_http_url,
electrum_rpc_url,
},
} => { } => {
let bitcoin_wallet = init_bitcoin_wallet( let bitcoin_wallet = init_bitcoin_wallet(
bitcoin_network, bitcoin_network,
electrum_rpc_url, electrum_rpc_url,
electrum_http_url,
seed, seed,
data_dir, data_dir,
execution_params,
) )
.await?; .await?;
@ -290,32 +276,20 @@ async fn main() -> Result<()> {
Command::Refund { Command::Refund {
swap_id, swap_id,
force, force,
bitcoin_params: electrum_rpc_url,
BitcoinParams {
electrum_http_url,
electrum_rpc_url,
},
} => { } => {
let bitcoin_wallet = init_bitcoin_wallet( let bitcoin_wallet = init_bitcoin_wallet(
bitcoin_network, bitcoin_network,
electrum_rpc_url, electrum_rpc_url,
electrum_http_url,
seed, seed,
data_dir, data_dir,
execution_params,
) )
.await?; .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();
bob::refund( bob::refund(swap_id, resume_state, Arc::new(bitcoin_wallet), db, force).await??;
swap_id,
resume_state,
execution_params,
Arc::new(bitcoin_wallet),
db,
force,
)
.await??;
} }
}; };
Ok(()) Ok(())
@ -324,16 +298,16 @@ 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,
execution_params: ExecutionParams,
) -> Result<bitcoin::Wallet> { ) -> Result<bitcoin::Wallet> {
let wallet_dir = data_dir.join("wallet"); let wallet_dir = data_dir.join("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,
execution_params.bitcoin_finality_confirmations,
&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};
@ -110,6 +111,15 @@ impl From<PublicKey> for Point {
} }
} }
impl From<PublicKey> for ::bitcoin::PublicKey {
fn from(from: PublicKey) -> Self {
::bitcoin::PublicKey {
compressed: true,
key: from.0.into(),
}
}
}
impl From<Point> for PublicKey { impl From<Point> for PublicKey {
fn from(p: Point) -> Self { fn from(p: Point) -> Self {
Self(p) Self(p)
@ -209,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(
client: &crate::bitcoin::Wallet,
target: BlockHeight,
) -> Result<()> {
while client.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)]
@ -266,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

@ -1,3 +1,4 @@
use crate::bitcoin::wallet::Watchable;
use crate::bitcoin::{ use crate::bitcoin::{
build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock, build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock,
TX_FEE, TX_FEE,
@ -5,9 +6,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 +36,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,7 +70,19 @@ impl Add<PunishTimelock> for BlockHeight {
} }
} }
#[derive(Debug, Clone)] 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)]
pub struct TxCancel { pub struct TxCancel {
inner: Transaction, inner: Transaction,
digest: SigHash, digest: SigHash,
@ -180,3 +207,13 @@ impl TxCancel {
} }
} }
} }
impl Watchable for TxCancel {
fn id(&self) -> Txid {
self.txid()
}
fn script(&self) -> Script {
self.output_descriptor.script_pubkey()
}
}

View File

@ -1,9 +1,11 @@
use crate::bitcoin::wallet::Watchable;
use crate::bitcoin::{ use crate::bitcoin::{
build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet, TX_FEE, build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet, TX_FEE,
}; };
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 +57,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 {
@ -100,3 +106,13 @@ impl From<TxLock> for PartiallySignedTransaction {
from.inner from.inner
} }
} }
impl Watchable for TxLock {
fn id(&self) -> Txid {
self.txid()
}
fn script(&self) -> Script {
self.output_descriptor.script_pubkey()
}
}

View File

@ -1,8 +1,9 @@
use crate::bitcoin::{Address, PublicKey, PunishTimelock, Transaction, TxCancel}; use crate::bitcoin::wallet::Watchable;
use crate::bitcoin::{self, Address, PunishTimelock, Transaction, TxCancel, Txid};
use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::util::bip143::SigHashCache;
use ::bitcoin::{SigHash, SigHashType}; use ::bitcoin::{SigHash, SigHashType};
use anyhow::Result; use anyhow::{Context, Result};
use ecdsa_fun::Signature; use bdk::bitcoin::Script;
use miniscript::{Descriptor, DescriptorTrait}; use miniscript::{Descriptor, DescriptorTrait};
use std::collections::HashMap; use std::collections::HashMap;
@ -11,6 +12,7 @@ pub struct TxPunish {
inner: Transaction, inner: Transaction,
digest: SigHash, digest: SigHash,
cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>, cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>,
watch_script: Script,
} }
impl TxPunish { impl TxPunish {
@ -32,6 +34,7 @@ impl TxPunish {
inner: tx_punish, inner: tx_punish,
digest, digest,
cancel_output_descriptor: tx_cancel.output_descriptor.clone(), cancel_output_descriptor: tx_cancel.output_descriptor.clone(),
watch_script: punish_address.script_pubkey(),
} }
} }
@ -39,22 +42,20 @@ impl TxPunish {
self.digest self.digest
} }
pub fn add_signatures( pub fn complete(
self, self,
(A, sig_a): (PublicKey, Signature), tx_punish_sig_bob: bitcoin::Signature,
(B, sig_b): (PublicKey, Signature), a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
) -> Result<Transaction> { ) -> Result<Transaction> {
let sig_a = a.sign(self.digest());
let sig_b = tx_punish_sig_bob;
let satisfier = { let satisfier = {
let mut satisfier = HashMap::with_capacity(2); let mut satisfier = HashMap::with_capacity(2);
let A = ::bitcoin::PublicKey { let A = a.public().into();
compressed: true, let B = B.into();
key: A.0.into(),
};
let B = ::bitcoin::PublicKey {
compressed: true,
key: B.0.into(),
};
// The order in which these are inserted doesn't matter // The order in which these are inserted doesn't matter
satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All)); satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All));
@ -65,8 +66,19 @@ impl TxPunish {
let mut tx_punish = self.inner; let mut tx_punish = self.inner;
self.cancel_output_descriptor self.cancel_output_descriptor
.satisfy(&mut tx_punish.input[0], satisfier)?; .satisfy(&mut tx_punish.input[0], satisfier)
.context("Failed to satisfy inputs with given signatures")?;
Ok(tx_punish) Ok(tx_punish)
} }
} }
impl Watchable for TxPunish {
fn id(&self) -> Txid {
self.inner.txid()
}
fn script(&self) -> Script {
self.watch_script.clone()
}
}

View File

@ -1,19 +1,26 @@
use crate::bitcoin::wallet::Watchable;
use crate::bitcoin::{ use crate::bitcoin::{
verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs, verify_encsig, verify_sig, Address, EmptyWitnessStack, EncryptedSignature, NoInputs,
Transaction, TxLock, NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, TxLock,
}; };
use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::util::bip143::SigHashCache;
use ::bitcoin::{SigHash, SigHashType, Txid}; use ::bitcoin::{SigHash, SigHashType, Txid};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use bitcoin::Script;
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
use ecdsa_fun::fun::Scalar;
use ecdsa_fun::nonce::Deterministic;
use ecdsa_fun::Signature; use ecdsa_fun::Signature;
use miniscript::{Descriptor, DescriptorTrait}; use miniscript::{Descriptor, DescriptorTrait};
use sha2::Sha256;
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct TxRedeem { pub struct TxRedeem {
inner: Transaction, inner: Transaction,
digest: SigHash, digest: SigHash,
lock_output_descriptor: Descriptor<::bitcoin::PublicKey>, lock_output_descriptor: Descriptor<::bitcoin::PublicKey>,
watch_script: Script,
} }
impl TxRedeem { impl TxRedeem {
@ -33,6 +40,7 @@ impl TxRedeem {
inner: tx_redeem, inner: tx_redeem,
digest, digest,
lock_output_descriptor: tx_lock.output_descriptor.clone(), lock_output_descriptor: tx_lock.output_descriptor.clone(),
watch_script: redeem_address.script_pubkey(),
} }
} }
@ -44,17 +52,31 @@ impl TxRedeem {
self.digest self.digest
} }
pub fn add_signatures( pub fn complete(
self, mut self,
(A, sig_a): (PublicKey, Signature), encrypted_signature: EncryptedSignature,
(B, sig_b): (PublicKey, Signature), a: SecretKey,
s_a: Scalar,
B: PublicKey,
) -> Result<Transaction> { ) -> Result<Transaction> {
verify_encsig(
B,
PublicKey::from(s_a.clone()),
&self.digest(),
&encrypted_signature,
)
.context("Invalid encrypted signature received")?;
let sig_a = a.sign(self.digest());
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
let sig_b = adaptor.decrypt_signature(&s_a, encrypted_signature);
let satisfier = { let satisfier = {
let mut satisfier = HashMap::with_capacity(2); let mut satisfier = HashMap::with_capacity(2);
let A = ::bitcoin::PublicKey { let A = ::bitcoin::PublicKey {
compressed: true, compressed: true,
key: A.0.into(), key: a.public.into(),
}; };
let B = ::bitcoin::PublicKey { let B = ::bitcoin::PublicKey {
compressed: true, compressed: true,
@ -68,11 +90,11 @@ impl TxRedeem {
satisfier satisfier
}; };
let mut tx_redeem = self.inner;
self.lock_output_descriptor self.lock_output_descriptor
.satisfy(&mut tx_redeem.input[0], satisfier)?; .satisfy(&mut self.inner.input[0], satisfier)
.context("Failed to sign Bitcoin redeem transaction")?;
Ok(tx_redeem) Ok(self.inner)
} }
pub fn extract_signature_by_key( pub fn extract_signature_by_key(
@ -112,3 +134,13 @@ impl TxRedeem {
Ok(sig) Ok(sig)
} }
} }
impl Watchable for TxRedeem {
fn id(&self) -> Txid {
self.txid()
}
fn script(&self) -> Script {
self.watch_script.clone()
}
}

View File

@ -1,3 +1,4 @@
use crate::bitcoin::wallet::Watchable;
use crate::bitcoin::{ use crate::bitcoin::{
verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs, verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs,
Transaction, TxCancel, Transaction, TxCancel,
@ -5,6 +6,7 @@ use crate::bitcoin::{
use ::bitcoin::util::bip143::SigHashCache; use ::bitcoin::util::bip143::SigHashCache;
use ::bitcoin::{SigHash, SigHashType, Txid}; use ::bitcoin::{SigHash, SigHashType, Txid};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use bitcoin::Script;
use ecdsa_fun::Signature; use ecdsa_fun::Signature;
use miniscript::{Descriptor, DescriptorTrait}; use miniscript::{Descriptor, DescriptorTrait};
use std::collections::HashMap; use std::collections::HashMap;
@ -14,6 +16,7 @@ pub struct TxRefund {
inner: Transaction, inner: Transaction,
digest: SigHash, digest: SigHash,
cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>, cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>,
watch_script: Script,
} }
impl TxRefund { impl TxRefund {
@ -31,6 +34,7 @@ impl TxRefund {
inner: tx_punish, inner: tx_punish,
digest, digest,
cancel_output_descriptor: tx_cancel.output_descriptor.clone(), cancel_output_descriptor: tx_cancel.output_descriptor.clone(),
watch_script: refund_address.script_pubkey(),
} }
} }
@ -110,3 +114,13 @@ impl TxRefund {
Ok(sig) Ok(sig)
} }
} }
impl Watchable for TxRefund {
fn id(&self) -> Txid {
self.txid()
}
fn script(&self) -> Script {
self.watch_script.clone()
}
}

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

@ -1,60 +1,46 @@
use crate::bitcoin::timelocks::BlockHeight; use crate::bitcoin::timelocks::BlockHeight;
use crate::bitcoin::{Address, Amount, Transaction}; use crate::bitcoin::{Address, Amount, Transaction};
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::future::Future;
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, bitcoin_finality_confirmations: u32,
} }
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,
bitcoin_finality_confirmations: u32,
wallet_dir: &Path, wallet_dir: &Path,
key: impl DerivableKey<Segwitv0> + Clone, key: impl DerivableKey<Segwitv0> + Clone,
) -> Result<Self> { ) -> Result<Self> {
// 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 =
.map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?; 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)?; let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
@ -66,16 +52,21 @@ 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, bitcoin_finality_confirmations,
}) })
} }
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 +77,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 +87,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 +109,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 +123,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 +139,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,15 +155,28 @@ 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
/// if done so successfully. /// if done so successfully.
pub async fn broadcast(&self, transaction: Transaction, kind: &str) -> Result<Txid> { ///
/// Returns the transaction ID and a future for when the transaction meets
/// the configured finality confirmations.
pub async fn broadcast(
&self,
transaction: Transaction,
kind: &str,
) -> Result<(Txid, impl Future<Output = Result<()>> + '_)> {
let txid = transaction.txid(); let txid = transaction.txid();
self.inner // to watch for confirmations, watching a single output is enough
let watcher = self.wait_for_transaction_finality(
(txid, transaction.output[0].script_pubkey.clone()),
kind.to_owned(),
);
self.wallet
.lock() .lock()
.await .await
.broadcast(transaction) .broadcast(transaction)
@ -181,11 +186,11 @@ impl Wallet {
tracing::info!(%txid, "Published Bitcoin {} transaction", kind); tracing::info!(%txid, "Published Bitcoin {} transaction", kind);
Ok(txid) Ok((txid, watcher))
} }
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,110 +207,72 @@ 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<T>(&self, tx: &T) -> Result<ScriptStatus>
tracing::debug!("watching for tx: {}", txid); where
let tx = retry(ConstantBackoff::new(Duration::from_secs(1)), || async { T: Watchable,
let client = Client::new(self.rpc_url.as_ref()) {
.map_err(|err| backoff::Error::Permanent(Error::ElectrumClient(err)))?; self.client.lock().await.status_of_script(tx)
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<T>(
let url = make_blocks_tip_height_url(&self.http_url)?;
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")?;
Ok(BlockHeight::new(height))
}
pub async fn transaction_block_height(&self, txid: Txid) -> Result<BlockHeight> {
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)
})
.await
.context("Transient errors should be retried")?;
Ok(BlockHeight::new(height))
}
pub async fn wait_for_transaction_finality(
&self, &self,
txid: Txid, tx: &T,
execution_params: ExecutionParams, mut status_fn: impl FnMut(ScriptStatus) -> bool,
) -> Result<()> { ) -> Result<()>
let conf_target = execution_params.bitcoin_finality_confirmations; where
T: Watchable,
{
let txid = tx.id();
tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin transaction", conf_target, if conf_target > 1 { "s" } else { "" }); let mut last_status = None;
// 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);
loop { loop {
let tx_block_height = self.transaction_block_height(txid).await?; let new_status = self.client.lock().await.status_of_script(tx)?;
tracing::debug!("tx_block_height: {:?}", tx_block_height);
let block_height = self.get_block_height().await?; if Some(new_status) != last_status {
tracing::debug!("latest_block_height: {:?}", block_height); tracing::debug!(%txid, "Transaction is {}", new_status);
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;
}
} }
interval.tick().await; last_status = Some(new_status);
if status_fn(new_status) {
break;
}
tokio::time::sleep(Duration::from_secs(5)).await;
} }
Ok(()) Ok(())
} }
async fn wait_for_transaction_finality<T>(&self, tx: T, kind: String) -> Result<()>
where
T: Watchable,
{
let conf_target = self.bitcoin_finality_confirmations;
let txid = tx.id();
tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin {} transaction", conf_target, if conf_target > 1 { "s" } else { "" }, kind);
let mut seen_confirmations = 0;
self.watch_until_status(&tx, |status| match status {
ScriptStatus::Confirmed(inner) => {
let confirmations = inner.confirmations();
if confirmations > seen_confirmations {
tracing::info!(%txid, "Bitcoin {} tx has {} out of {} confirmation{}", kind, confirmations, conf_target, if conf_target > 1 { "s" } else { "" });
seen_confirmations = confirmations;
}
inner.meets_target(conf_target)
},
_ => false
})
.await?;
Ok(())
}
/// Selects an appropriate [`FeeRate`] to be used for getting transactions /// Selects an appropriate [`FeeRate`] to be used for getting transactions
/// confirmed within a reasonable amount of time. /// confirmed within a reasonable amount of time.
fn select_feerate(&self) -> FeeRate { fn select_feerate(&self) -> FeeRate {
@ -314,42 +281,285 @@ impl Wallet {
} }
} }
fn make_tx_status_url(base_url: &Url, txid: Txid) -> Result<Url> { /// Defines a watchable transaction.
let url = base_url.join(&format!("tx/{}/status", txid))?; ///
/// For a transaction to be watchable, we need to know two things: Its
Ok(url) /// transaction ID and the specific output script that is going to change.
/// A transaction can obviously have multiple outputs but our protocol purposes,
/// we are usually interested in a specific one.
pub trait Watchable {
fn id(&self) -> Txid;
fn script(&self) -> Script;
} }
fn make_blocks_tip_height_url(base_url: &Url) -> Result<Url> { impl Watchable for (Txid, Script) {
let url = base_url.join("blocks/tip/height")?; fn id(&self) -> Txid {
self.0
}
Ok(url) fn script(&self) -> Script {
self.1.clone()
}
}
struct Client {
electrum: bdk::electrum_client::Client,
latest_block: BlockHeight,
last_ping: Instant,
interval: Duration,
script_history: BTreeMap<Script, Vec<GetHistoryRes>>,
}
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(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<T>(&mut self, tx: &T) -> Result<ScriptStatus>
where
T: Watchable,
{
let txid = tx.id();
let script = tx.script();
if !self.script_history.contains_key(&script) {
self.script_history.insert(script.clone(), vec![]);
}
self.drain_notifications()?;
let history = self.script_history.entry(script).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,4 +1,4 @@
use crate::bitcoin::{EncryptedSignature, TxCancel, TxRefund}; use crate::bitcoin::EncryptedSignature;
use crate::monero; use crate::monero;
use crate::monero::monero_private_key; use crate::monero::monero_private_key;
use crate::protocol::alice; use crate::protocol::alice;
@ -177,37 +177,18 @@ impl From<Alice> for AliceState {
Alice::BtcCancelled { Alice::BtcCancelled {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
state3, state3,
} => { } => AliceState::BtcCancelled {
let tx_cancel = TxCancel::new( monero_wallet_restore_blockheight,
&state3.tx_lock, state3: Box::new(state3),
state3.cancel_timelock, },
state3.a.public(),
state3.B,
);
AliceState::BtcCancelled {
monero_wallet_restore_blockheight,
state3: Box::new(state3),
tx_cancel: Box::new(tx_cancel),
}
}
Alice::BtcPunishable { Alice::BtcPunishable {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
state3, state3,
} => { } => AliceState::BtcPunishable {
let tx_cancel = TxCancel::new( monero_wallet_restore_blockheight,
&state3.tx_lock, state3: Box::new(state3),
state3.cancel_timelock, },
state3.a.public(),
state3.B,
);
let tx_refund = TxRefund::new(&tx_cancel, &state3.refund_address);
AliceState::BtcPunishable {
monero_wallet_restore_blockheight,
tx_refund: Box::new(tx_refund),
state3: Box::new(state3),
}
}
Alice::BtcRefunded { Alice::BtcRefunded {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
state3, state3,

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, TxRefund,
}; };
use crate::execution_params::ExecutionParams; use crate::execution_params::ExecutionParams;
use crate::protocol::alice::{Message1, Message3}; use crate::protocol::alice::{Message1, Message3};
@ -37,7 +36,6 @@ pub enum AliceState {
BtcRedeemed, BtcRedeemed,
BtcCancelled { BtcCancelled {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
tx_cancel: Box<TxCancel>,
state3: Box<State3>, state3: Box<State3>,
}, },
BtcRefunded { BtcRefunded {
@ -47,7 +45,6 @@ pub enum AliceState {
}, },
BtcPunishable { BtcPunishable {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
tx_refund: Box<TxRefund>,
state3: Box<State3>, state3: Box<State3>,
}, },
XmrRefunded, XmrRefunded,
@ -323,24 +320,45 @@ 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.tx_lock, |status| {
self.cancel_timelock, status.is_confirmed_with(self.cancel_timelock)
self.tx_lock.txid(), })
) .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 = self.tx_cancel();
bitcoin_wallet,
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).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,
))
}
pub fn tx_cancel(&self) -> TxCancel {
TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B)
}
pub fn tx_punish(&self) -> TxPunish {
bitcoin::TxPunish::new(
&self.tx_cancel(),
&self.punish_address,
self.punish_timelock,
) )
.await }
pub fn tx_refund(&self) -> TxRefund {
bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address)
} }
} }

View File

@ -1,43 +1,13 @@
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::execution_params::ExecutionParams;
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 ecdsa_fun::adaptor::{Adaptor, HashTranscript};
use ecdsa_fun::nonce::Deterministic;
use futures::future::{select, Either};
use futures::pin_mut; use futures::pin_mut;
use libp2p::PeerId; use libp2p::PeerId;
use sha2::Sha256;
use tokio::time::timeout;
// TODO(Franck): Use helper functions from xmr-btc instead of re-writing them
// here
pub async fn wait_for_locked_bitcoin(
lock_bitcoin_txid: bitcoin::Txid,
bitcoin_wallet: &bitcoin::Wallet,
execution_params: ExecutionParams,
) -> Result<()> {
// We assume we will see Bob's transaction in the mempool first.
timeout(
execution_params.bob_time_to_act,
bitcoin_wallet.watch_for_raw_transaction(lock_bitcoin_txid),
)
.await
.context("Failed to find lock Bitcoin tx")??;
// // We saw the transaction in the mempool, waiting for it to be confirmed.
bitcoin_wallet
.wait_for_transaction_finality(lock_bitcoin_txid, execution_params)
.await?;
Ok(())
}
pub async fn lock_xmr( pub async fn lock_xmr(
bob_peer_id: PeerId, bob_peer_id: PeerId,
@ -81,36 +51,6 @@ pub async fn wait_for_bitcoin_encrypted_signature(
Ok(msg3.tx_redeem_encsig) Ok(msg3.tx_redeem_encsig)
} }
pub fn build_bitcoin_redeem_transaction(
encrypted_signature: EncryptedSignature,
tx_lock: &TxLock,
a: bitcoin::SecretKey,
s_a: ecdsa_fun::fun::Scalar,
B: bitcoin::PublicKey,
redeem_address: &bitcoin::Address,
) -> Result<bitcoin::Transaction> {
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
let tx_redeem = bitcoin::TxRedeem::new(tx_lock, redeem_address);
bitcoin::verify_encsig(
B,
bitcoin::PublicKey::from(s_a.clone()),
&tx_redeem.digest(),
&encrypted_signature,
)
.context("Invalid encrypted signature received")?;
let sig_a = a.sign(tx_redeem.digest());
let sig_b = adaptor.decrypt_signature(&s_a, encrypted_signature);
let tx = tx_redeem
.add_signatures((a.public(), sig_a), (B, sig_b))
.context("Failed to sign Bitcoin redeem transaction")?;
Ok(tx)
}
pub async fn publish_cancel_transaction( pub async fn publish_cancel_transaction(
tx_lock: TxLock, tx_lock: TxLock,
a: bitcoin::SecretKey, a: bitcoin::SecretKey,
@ -118,12 +58,10 @@ pub async fn publish_cancel_transaction(
cancel_timelock: CancelTimelock, cancel_timelock: CancelTimelock,
tx_cancel_sig_bob: bitcoin::Signature, tx_cancel_sig_bob: bitcoin::Signature,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<bitcoin::TxCancel> { ) -> Result<()> {
// First wait for cancel timelock to expire bitcoin_wallet
let tx_lock_height = bitcoin_wallet .watch_until_status(&tx_lock, |status| status.is_confirmed_with(cancel_timelock))
.transaction_block_height(tx_lock.txid())
.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);
@ -140,42 +78,52 @@ pub async fn publish_cancel_transaction(
let sig_b = tx_cancel_sig_bob.clone(); let sig_b = tx_cancel_sig_bob.clone();
let tx_cancel = tx_cancel let tx_cancel = tx_cancel
.clone()
.add_signatures((a.public(), sig_a), (B, sig_b)) .add_signatures((a.public(), sig_a), (B, sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel"); .expect("sig_{a,b} to be valid signatures for tx_cancel");
// TODO(Franck): Error handling is delicate, why can't we broadcast? // TODO(Franck): Error handling is delicate, why can't we broadcast?
bitcoin_wallet.broadcast(tx_cancel, "cancel").await?; let (..) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
// TODO(Franck): Wait until transaction is mined and returned mined // TODO(Franck): Wait until transaction is mined and returned mined
// block height // block height
} }
Ok(tx_cancel) Ok(())
} }
pub async fn wait_for_bitcoin_refund( pub async fn wait_for_bitcoin_refund(
tx_cancel: &TxCancel, tx_cancel: &TxCancel,
cancel_tx_height: BlockHeight, tx_refund: &TxRefund,
punish_timelock: PunishTimelock, punish_timelock: PunishTimelock,
refund_address: &bitcoin::Address,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<(bitcoin::TxRefund, Option<bitcoin::Transaction>)> { ) -> Result<Option<bitcoin::Transaction>> {
let punish_timelock_expired = let refund_tx_id = tx_refund.txid();
poll_until_block_height_is_gte(bitcoin_wallet, cancel_tx_height + punish_timelock); let seen_refund_tx =
bitcoin_wallet.watch_until_status(tx_refund, |status| status.has_been_seen());
let tx_refund = bitcoin::TxRefund::new(tx_cancel, refund_address); let punish_timelock_expired = bitcoin_wallet.watch_until_status(tx_cancel, |status| {
status.is_confirmed_with(punish_timelock)
// 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());
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 { tokio::select! {
Either::Left(_) => Ok((tx_refund, None)), seen_refund = seen_refund_tx => {
Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx?))), match seen_refund {
Ok(()) => {
let published_refund_tx = bitcoin_wallet.get_raw_transaction(refund_tx_id).await?;
Ok(Some(published_refund_tx))
}
Err(e) => {
bail!(e.context("Failed to monitor refund transaction"))
}
}
}
_ = punish_timelock_expired => {
Ok(None)
}
} }
} }
@ -201,25 +149,3 @@ pub fn extract_monero_private_key(
Ok(spend_key) Ok(spend_key)
} }
pub fn build_bitcoin_punish_transaction(
tx_lock: &TxLock,
cancel_timelock: CancelTimelock,
punish_address: &bitcoin::Address,
punish_timelock: PunishTimelock,
tx_punish_sig_bob: bitcoin::Signature,
a: bitcoin::SecretKey,
B: bitcoin::PublicKey,
) -> Result<bitcoin::Transaction> {
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B);
let tx_punish = bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock);
let sig_a = a.sign(tx_punish.digest());
let sig_b = tx_punish_sig_bob;
let signed_tx_punish = tx_punish
.add_signatures((a.public(), sig_a), (B, sig_b))
.expect("sig_{a,b} to be valid signatures for tx_cancel");
Ok(signed_tx_punish)
}

View File

@ -1,24 +1,24 @@
//! Run an XMR/BTC swap in the role of Alice. //! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC. //! Alice holds XMR and wishes receive BTC.
use crate::bitcoin::ExpiredTimelocks; use crate::bitcoin::{ExpiredTimelocks, TxRedeem};
use crate::database::Database; use crate::database::Database;
use crate::execution_params::ExecutionParams; use crate::execution_params::ExecutionParams;
use crate::monero_ext::ScalarExt; use crate::monero_ext::ScalarExt;
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::steps::{ use crate::protocol::alice::steps::{
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, extract_monero_private_key, extract_monero_private_key, lock_xmr, publish_cancel_transaction,
lock_xmr, publish_cancel_transaction, wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund,
wait_for_bitcoin_refund, wait_for_locked_bitcoin,
}; };
use crate::protocol::alice::AliceState; use crate::protocol::alice::AliceState;
use crate::{bitcoin, database, monero}; use crate::{bitcoin, database, monero};
use anyhow::{bail, Result}; use anyhow::{bail, Context, Result};
use async_recursion::async_recursion; use async_recursion::async_recursion;
use futures::future::{select, Either}; use futures::future::{select, Either};
use futures::pin_mut; use futures::pin_mut;
use rand::{CryptoRng, RngCore}; use rand::{CryptoRng, RngCore};
use std::sync::Arc; use std::sync::Arc;
use tokio::time::timeout;
use tracing::{error, info}; use tracing::{error, info};
use uuid::Uuid; use uuid::Uuid;
@ -80,12 +80,19 @@ async fn run_until_internal(
state3, state3,
bob_peer_id, bob_peer_id,
} => { } => {
let _ = wait_for_locked_bitcoin( timeout(
state3.tx_lock.txid(), execution_params.bob_time_to_act,
&bitcoin_wallet, bitcoin_wallet
execution_params, .watch_until_status(&state3.tx_lock, |status| status.has_been_seen()),
) )
.await?; .await
.context("Failed to find lock Bitcoin tx")??;
bitcoin_wallet
.watch_until_status(&state3.tx_lock, |status| {
status.is_confirmed_with(execution_params.bitcoin_finality_confirmations)
})
.await?;
let state = AliceState::BtcLocked { let state = AliceState::BtcLocked {
bob_peer_id, bob_peer_id,
@ -198,27 +205,19 @@ async fn run_until_internal(
} => { } => {
let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? { let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
ExpiredTimelocks::None => { ExpiredTimelocks::None => {
match build_bitcoin_redeem_transaction( match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete(
*encrypted_signature, *encrypted_signature,
&state3.tx_lock,
state3.a.clone(), state3.a.clone(),
state3.s_a.to_secpfun_scalar(), state3.s_a.to_secpfun_scalar(),
state3.B, state3.B,
&state3.redeem_address,
) { ) {
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
Ok(txid) => { Ok((_, finality)) => match finality.await {
let publishded_redeem_tx = bitcoin_wallet Ok(_) => AliceState::BtcRedeemed,
.wait_for_transaction_finality(txid, execution_params) Err(e) => {
.await; bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
match publishded_redeem_tx {
Ok(_) => AliceState::BtcRedeemed,
Err(e) => {
bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
}
} }
} },
Err(e) => { Err(e) => {
error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e); error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e);
state3 state3
@ -269,7 +268,7 @@ async fn run_until_internal(
state3, state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
} => { } => {
let tx_cancel = publish_cancel_transaction( publish_cancel_transaction(
state3.tx_lock.clone(), state3.tx_lock.clone(),
state3.a.clone(), state3.a.clone(),
state3.B, state3.B,
@ -281,7 +280,6 @@ async fn run_until_internal(
let state = AliceState::BtcCancelled { let state = AliceState::BtcCancelled {
state3, state3,
tx_cancel: Box::new(tx_cancel),
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
}; };
let db_state = (&state).into(); let db_state = (&state).into();
@ -301,18 +299,12 @@ async fn run_until_internal(
} }
AliceState::BtcCancelled { AliceState::BtcCancelled {
state3, state3,
tx_cancel,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
} => { } => {
let tx_cancel_height = bitcoin_wallet let published_refund_tx = wait_for_bitcoin_refund(
.transaction_block_height(tx_cancel.txid()) &state3.tx_cancel(),
.await?; &state3.tx_refund(),
let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund(
&tx_cancel,
tx_cancel_height,
state3.punish_timelock, state3.punish_timelock,
&state3.refund_address,
&bitcoin_wallet, &bitcoin_wallet,
) )
.await?; .await?;
@ -321,7 +313,6 @@ async fn run_until_internal(
match published_refund_tx { match published_refund_tx {
None => { None => {
let state = AliceState::BtcPunishable { let state = AliceState::BtcPunishable {
tx_refund: Box::new(tx_refund),
state3, state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
}; };
@ -344,7 +335,7 @@ async fn run_until_internal(
Some(published_refund_tx) => { Some(published_refund_tx) => {
let spend_key = extract_monero_private_key( let spend_key = extract_monero_private_key(
published_refund_tx, published_refund_tx,
&tx_refund, &state3.tx_refund(),
state3.s_a, state3.s_a,
state3.a.clone(), state3.a.clone(),
state3.S_b_bitcoin, state3.S_b_bitcoin,
@ -390,39 +381,38 @@ async fn run_until_internal(
Ok(state) Ok(state)
} }
AliceState::BtcPunishable { AliceState::BtcPunishable {
tx_refund,
state3, state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
} => { } => {
let signed_tx_punish = build_bitcoin_punish_transaction( let signed_tx_punish = state3.tx_punish().complete(
&state3.tx_lock,
state3.cancel_timelock,
&state3.punish_address,
state3.punish_timelock,
state3.tx_punish_sig_bob.clone(), state3.tx_punish_sig_bob.clone(),
state3.a.clone(), state3.a.clone(),
state3.B, state3.B,
)?; )?;
let punish_tx_finalised = async { let punish_tx_finalised = async {
let txid = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; let (txid, finality) =
bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
bitcoin_wallet finality.await?;
.wait_for_transaction_finality(txid, execution_params)
.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 tx_refund = state3.tx_refund();
let refund_tx_seen =
bitcoin_wallet.watch_until_status(&tx_refund, |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 +438,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,5 @@
use crate::bitcoin::Wallet; use crate::bitcoin::Wallet;
use crate::database::{Database, Swap}; use crate::database::{Database, Swap};
use crate::execution_params::ExecutionParams;
use crate::protocol::bob::BobState; use crate::protocol::bob::BobState;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use std::sync::Arc; use std::sync::Arc;
@ -13,7 +12,6 @@ pub struct SwapNotCancelledYet(Uuid);
pub async fn refund( pub async fn refund(
swap_id: Uuid, swap_id: Uuid,
state: BobState, state: BobState,
execution_params: ExecutionParams,
bitcoin_wallet: Arc<Wallet>, bitcoin_wallet: Arc<Wallet>,
db: Database, db: Database,
force: bool, force: bool,
@ -41,9 +39,7 @@ pub async fn refund(
} }
}; };
state4 state4.refund_btc(bitcoin_wallet.as_ref()).await?;
.refund_btc(bitcoin_wallet.as_ref(), execution_params)
.await?;
let state = BobState::BtcRefunded(state4); let state = BobState::BtcRefunded(state4);
let db_state = state.clone().into(); let db_state = state.clone().into();

View File

@ -1,15 +1,14 @@
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::monero; use crate::monero;
use crate::monero::{monero_private_key, InsufficientFunds, TransferProof}; use crate::monero::{monero_private_key, InsufficientFunds, TransferProof};
use crate::monero_ext::ScalarExt; 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,31 +261,27 @@ 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 A: self.A,
.context("Failed to sign Bitcoin lock transaction")?; b: self.b,
s_b: self.s_b,
let _ = bitcoin_wallet.broadcast(signed_tx, "lock").await?; S_a_monero: self.S_a_monero,
S_a_bitcoin: self.S_a_bitcoin,
Ok(State3 { v: self.v,
A: self.A, xmr: self.xmr,
b: self.b, cancel_timelock: self.cancel_timelock,
s_b: self.s_b, punish_timelock: self.punish_timelock,
S_a_monero: self.S_a_monero, refund_address: self.refund_address,
S_a_bitcoin: self.S_a_bitcoin, redeem_address: self.redeem_address,
v: self.v, tx_lock: self.tx_lock.clone(),
xmr: self.xmr, tx_cancel_sig_a: self.tx_cancel_sig_a,
cancel_timelock: self.cancel_timelock, tx_refund_encsig: self.tx_refund_encsig,
punish_timelock: self.punish_timelock, min_monero_confirmations: self.min_monero_confirmations,
refund_address: self.refund_address, },
redeem_address: self.redeem_address, self.tx_lock,
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,
})
} }
} }
@ -354,12 +349,12 @@ 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.tx_lock, |status| {
self.cancel_timelock, status.is_confirmed_with(self.cancel_timelock)
self.tx_lock.txid(), })
) .await?;
.await Ok(())
} }
pub fn cancel(&self) -> State4 { pub fn cancel(&self) -> State4 {
@ -390,13 +385,17 @@ 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).await?;
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).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 +418,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 {
@ -437,17 +435,6 @@ impl State4 {
let tx_cancel = let tx_cancel =
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
let sig_a = self.tx_cancel_sig_a.clone();
let sig_b = self.b.sign(tx_cancel.digest());
let tx_cancel = tx_cancel
.clone()
.add_signatures((self.A, sig_a), (self.b.public(), sig_b))
.expect(
"sig_{a,b} to be valid signatures for
tx_cancel",
);
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?; let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
Ok(tx) Ok(tx)
@ -461,14 +448,13 @@ impl State4 {
let sig_b = self.b.sign(tx_cancel.digest()); let sig_b = self.b.sign(tx_cancel.digest());
let tx_cancel = tx_cancel let tx_cancel = tx_cancel
.clone()
.add_signatures((self.A, sig_a), (self.b.public(), sig_b)) .add_signatures((self.A, sig_a), (self.b.public(), sig_b))
.expect( .expect(
"sig_{a,b} to be valid signatures for "sig_{a,b} to be valid signatures for
tx_cancel", tx_cancel",
); );
let tx_id = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?; let (tx_id, _) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
Ok(tx_id) Ok(tx_id)
} }
@ -477,10 +463,12 @@ 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, |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)?;
@ -499,32 +487,33 @@ 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.tx_lock, |status| {
self.cancel_timelock, status.is_confirmed_with(self.cancel_timelock)
self.tx_lock.txid(), })
) .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).await?;
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).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(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> {
&self,
bitcoin_wallet: &bitcoin::Wallet,
execution_params: ExecutionParams,
) -> Result<()> {
let tx_cancel = let tx_cancel =
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address); let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
@ -538,11 +527,9 @@ impl State4 {
let signed_tx_refund = let signed_tx_refund =
tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?; tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?;
let txid = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?; let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?;
bitcoin_wallet finality.await?;
.wait_for_transaction_finality(txid, execution_params)
.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,16 @@ 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 (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
bitcoin_wallet
.watch_until_status(&tx_lock, |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();
@ -371,9 +380,7 @@ async fn run_until_internal(
bail!("Internal error: canceled state reached before cancel timelock was expired"); bail!("Internal error: canceled state reached before cancel timelock was expired");
} }
ExpiredTimelocks::Cancel => { ExpiredTimelocks::Cancel => {
state state.refund_btc(bitcoin_wallet.as_ref()).await?;
.refund_btc(bitcoin_wallet.as_ref(), execution_params)
.await?;
BobState::BtcRefunded(state) BobState::BtcRefunded(state)
} }
ExpiredTimelocks::Punish => BobState::BtcPunished { ExpiredTimelocks::Punish => BobState::BtcPunished {

View File

@ -48,7 +48,6 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
let bob_state = bob::refund( let bob_state = bob::refund(
bob_swap.swap_id, bob_swap.swap_id,
bob_swap.state, bob_swap.state,
bob_swap.execution_params,
bob_swap.bitcoin_wallet, bob_swap.bitcoin_wallet,
bob_swap.db, bob_swap.db,
false, false,

View File

@ -42,7 +42,6 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() {
bob::refund( bob::refund(
bob_swap.swap_id, bob_swap.swap_id,
bob_swap.state, bob_swap.state,
bob_swap.execution_params,
bob_swap.bitcoin_wallet, bob_swap.bitcoin_wallet,
bob_swap.db, bob_swap.db,
false, false,

View File

@ -40,7 +40,6 @@ async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() {
let is_error = bob::refund( let is_error = bob::refund(
bob_swap.swap_id, bob_swap.swap_id,
bob_swap.state, bob_swap.state,
bob_swap.execution_params,
bob_swap.bitcoin_wallet, bob_swap.bitcoin_wallet,
bob_swap.db, bob_swap.db,
true, true,

View File

@ -339,10 +339,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();
@ -354,7 +350,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,
) )
@ -377,7 +372,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,
) )
@ -585,7 +579,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>) {
@ -605,15 +598,11 @@ 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,
execution_params.bitcoin_finality_confirmations,
datadir, datadir,
seed.derive_extended_private_key(bitcoin::Network::Regtest) seed.derive_extended_private_key(bitcoin::Network::Regtest)
.expect("Could not create extended private key from seed"), .expect("Could not create extended private key from seed"),
@ -675,26 +664,9 @@ pub fn init_tracing() -> DefaultGuard {
// trouble when running multiple tests. // trouble when running multiple tests.
let _ = LogTracer::init(); let _ = LogTracer::init();
let global_filter = tracing::Level::WARN;
let swap_filter = tracing::Level::DEBUG;
let xmr_btc_filter = tracing::Level::DEBUG;
let monero_rpc_filter = tracing::Level::DEBUG;
let monero_harness_filter = tracing::Level::DEBUG;
let bitcoin_harness_filter = tracing::Level::INFO;
let testcontainers_filter = tracing::Level::DEBUG;
use tracing_subscriber::util::SubscriberInitExt as _; use tracing_subscriber::util::SubscriberInitExt as _;
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter(format!( .with_env_filter("warn,swap=debug,monero_harness=debug,monero_rpc=info,bitcoin_harness=info,testcontainers=info")
"{},swap={},xmr_btc={},monero_harness={},monero_rpc={},bitcoin_harness={},testcontainers={}",
global_filter,
swap_filter,
xmr_btc_filter,
monero_harness_filter,
monero_rpc_filter,
bitcoin_harness_filter,
testcontainers_filter
))
.set_default() .set_default()
} }