From 1b13608d96e5fda007c8ad5d20f060b83cceb581 Mon Sep 17 00:00:00 2001 From: binarybaron <86064887+binarybaron@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:15:29 +0200 Subject: [PATCH] Add get_swap_expired_timelock timelock, other small refactoring - Add get_swap_expired_timelock endpoint to return expired timelock if one exists. Fails if bitcoin lock tx has not yet published or if swap is already finished. - Rename current_epoch to expired_timelock to enforce consistent method names - Add blocks left until current expired timelock expires (next timelock expires) to ExpiredTimelock struct - Change .expect() to .unwrap() in rpc server method register because those will only fail if we register the same method twice which will never happen --- swap/src/api/request.rs | 37 ++++++++++++++++++++- swap/src/bitcoin.rs | 8 +++-- swap/src/bitcoin/cancel.rs | 12 +++++++ swap/src/bitcoin/timelocks.rs | 10 ++++-- swap/src/bitcoin/wallet.rs | 59 +++++++++++++++++++++++++++++---- swap/src/protocol/alice/swap.rs | 6 ++-- swap/src/protocol/bob/state.rs | 2 +- swap/src/protocol/bob/swap.rs | 12 +++---- swap/src/rpc/methods.rs | 41 +++++++++++++++++------ 9 files changed, 155 insertions(+), 32 deletions(-) diff --git a/swap/src/api/request.rs b/swap/src/api/request.rs index a54b41be..24b56a62 100644 --- a/swap/src/api/request.rs +++ b/swap/src/api/request.rs @@ -1,5 +1,5 @@ use crate::api::Context; -use crate::bitcoin::{Amount, TxLock}; +use crate::bitcoin::{Amount, ExpiredTimelocks, TxLock}; use crate::cli::{list_sellers, EventLoop, SellerStatus}; use crate::libp2p_ext::MultiAddrExt; use crate::network::quote::{BidQuote, ZeroQuoteReceived}; @@ -115,6 +115,9 @@ pub enum Method { server_address: Option, }, GetCurrentSwap, + GetSwapExpiredTimelock { + swap_id: Uuid, + }, } impl Request { @@ -563,6 +566,38 @@ impl Request { "swap_id": SWAP_LOCK.read().await.clone() })) }, + Method::GetSwapExpiredTimelock { swap_id } => { + let swap_state: BobState = context + .db + .get_state( + swap_id, + ) + .await? + .try_into()?; + + let bitcoin_wallet = context.bitcoin_wallet.as_ref().context("Could not get Bitcoin wallet")?; + + let timelock = match swap_state { + BobState::Started { .. } + | BobState::SafelyAborted + | BobState::SwapSetupCompleted(_) => bail!("Bitcoin lock transaction has not been published yet"), + BobState::BtcLocked { state3: state, .. } + | BobState::XmrLockProofReceived { state, .. } => state.expired_timelock(bitcoin_wallet).await, + BobState::XmrLocked(state) + | BobState::EncSigSent(state) => state.expired_timelock(bitcoin_wallet).await, + BobState::CancelTimelockExpired(state) + | BobState::BtcCancelled(state) => state.expired_timelock(bitcoin_wallet).await, + BobState::BtcPunished { .. } => Ok(ExpiredTimelocks::Punish), + // swap is already finished + BobState::BtcRefunded(_) + | BobState::BtcRedeemed(_) + | BobState::XmrRedeemed { .. } => bail!("Bitcoin have already been redeemed or refunded") + }?; + + Ok(json!({ + "timelock": timelock, + })) + }, } } diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index f3e42f63..7d50354e 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -244,10 +244,14 @@ pub fn current_epoch( } if tx_lock_status.is_confirmed_with(cancel_timelock) { - return ExpiredTimelocks::Cancel; + return ExpiredTimelocks::Cancel { + blocks_left: tx_cancel_status.blocks_left_until(punish_timelock), + } } - ExpiredTimelocks::None + ExpiredTimelocks::None { + blocks_left: tx_lock_status.blocks_left_until(cancel_timelock), + } } pub mod bitcoin_address { diff --git a/swap/src/bitcoin/cancel.rs b/swap/src/bitcoin/cancel.rs index 35b6b197..aec3fe38 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap/src/bitcoin/cancel.rs @@ -24,6 +24,12 @@ use std::ops::Add; #[serde(transparent)] pub struct CancelTimelock(u32); +impl From for u32 { + fn from(cancel_timelock: CancelTimelock) -> Self { + cancel_timelock.0 + } +} + impl CancelTimelock { pub const fn new(number_of_blocks: u32) -> Self { Self(number_of_blocks) @@ -64,6 +70,12 @@ impl fmt::Display for CancelTimelock { #[serde(transparent)] pub struct PunishTimelock(u32); +impl From for u32 { + fn from(punish_timelock: PunishTimelock) -> Self { + punish_timelock.0 + } +} + impl PunishTimelock { pub const fn new(number_of_blocks: u32) -> Self { Self(number_of_blocks) diff --git a/swap/src/bitcoin/timelocks.rs b/swap/src/bitcoin/timelocks.rs index e8b72ea6..dee8b4a0 100644 --- a/swap/src/bitcoin/timelocks.rs +++ b/swap/src/bitcoin/timelocks.rs @@ -37,9 +37,13 @@ impl Add for BlockHeight { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)] pub enum ExpiredTimelocks { - None, - Cancel, + None { + blocks_left: u32, + }, + Cancel { + blocks_left: u32, + }, Punish, } diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 9a8500b6..ec3bba11 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -274,7 +274,7 @@ impl Subscription { pub async fn wait_until_confirmed_with(&self, target: T) -> Result<()> where - u32: PartialOrd, + T: Into, T: Copy, { self.wait_until(|status| status.is_confirmed_with(target)) @@ -926,10 +926,19 @@ impl Confirmed { } pub fn meets_target(&self, target: T) -> bool - where - u32: PartialOrd, + where T: Into { - self.confirmations() >= target + self.confirmations() >= target.into() + } + + pub fn blocks_left_until(&self, target: T) -> u32 + where T: Into, T: Copy + { + if self.meets_target(target) { + 0 + } else { + target.into() - self.confirmations() + } } } @@ -941,8 +950,7 @@ impl ScriptStatus { /// Check if the script has met the given confirmation target. pub fn is_confirmed_with(&self, target: T) -> bool - where - u32: PartialOrd, + where T: Into { match self { ScriptStatus::Confirmed(inner) => inner.meets_target(target), @@ -950,6 +958,18 @@ impl ScriptStatus { } } + // Calculate the number of blocks left until the target is met. + pub fn blocks_left_until(&self, target: T) -> u32 + where T: Into, T: Copy + { + match self { + ScriptStatus::Confirmed(inner) => { + inner.blocks_left_until(target) + } + _ => target.into(), + } + } + pub fn has_been_seen(&self) -> bool { matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_)) } @@ -1005,6 +1025,33 @@ mod tests { assert_eq!(confirmed.depth, 0) } + #[test] + fn given_depth_0_should_return_0_blocks_left_until_1() { + let script = ScriptStatus::Confirmed(Confirmed { depth: 0 }); + + let blocks_left = script.blocks_left_until(1); + + assert_eq!(blocks_left, 0) + } + + #[test] + fn given_depth_1_should_return_0_blocks_left_until_1() { + let script = ScriptStatus::Confirmed(Confirmed { depth: 1 }); + + let blocks_left = script.blocks_left_until(1); + + assert_eq!(blocks_left, 0) + } + + #[test] + fn given_depth_0_should_return_1_blocks_left_until_2() { + let script = ScriptStatus::Confirmed(Confirmed { depth: 0 }); + + let blocks_left = script.blocks_left_until(2); + + assert_eq!(blocks_left, 1) + } + #[test] fn given_one_BTC_and_100k_sats_per_vb_fees_should_not_hit_max() { // 400 weight = 100 vbyte diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 941e5a43..df59c6a6 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -112,7 +112,7 @@ where } AliceState::BtcLocked { state3 } => { match state3.expired_timelocks(bitcoin_wallet).await? { - ExpiredTimelocks::None => { + ExpiredTimelocks::None {..} => { // Record the current monero wallet block height so we don't have to scan from // block 0 for scenarios where we create a refund wallet. let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; @@ -135,7 +135,7 @@ where transfer_proof, state3, } => match state3.expired_timelocks(bitcoin_wallet).await? { - ExpiredTimelocks::None => { + ExpiredTimelocks::None {..} => { monero_wallet .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1)) .await @@ -221,7 +221,7 @@ where encrypted_signature, state3, } => match state3.expired_timelocks(bitcoin_wallet).await? { - ExpiredTimelocks::None => { + ExpiredTimelocks::None {..} => { let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; match state3.signed_redeem_transaction(*encrypted_signature) { Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index f683ffa7..e390ec41 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -440,7 +440,7 @@ impl State3 { self.tx_lock.txid() } - pub async fn current_epoch( + pub async fn expired_timelock( &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result { diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 66933a87..db8994fd 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -117,7 +117,7 @@ async fn next_state( } => { let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; - if let ExpiredTimelocks::None = state3.current_epoch(bitcoin_wallet).await? { + if let ExpiredTimelocks::None {..} = state3.expired_timelock(bitcoin_wallet).await? { let transfer_proof_watcher = event_loop_handle.recv_transfer_proof(); let cancel_timelock_expires = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock); @@ -156,7 +156,7 @@ async fn next_state( } => { let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; - if let ExpiredTimelocks::None = state.current_epoch(bitcoin_wallet).await? { + if let ExpiredTimelocks::None {..} = state.expired_timelock(bitcoin_wallet).await? { let watch_request = state.lock_xmr_watch_request(lock_transfer_proof); select! { @@ -185,7 +185,7 @@ async fn next_state( BobState::XmrLocked(state) => { let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; - if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet).await? { + if let ExpiredTimelocks::None {..} = state.expired_timelock(bitcoin_wallet).await? { // Alice has locked Xmr // Bob sends Alice his key @@ -209,7 +209,7 @@ async fn next_state( BobState::EncSigSent(state) => { let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; - if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet).await? { + if let ExpiredTimelocks::None {..} = state.expired_timelock(bitcoin_wallet).await? { select! { state5 = state.watch_for_redeem_btc(bitcoin_wallet) => { BobState::BtcRedeemed(state5?) @@ -269,12 +269,12 @@ async fn next_state( BobState::BtcCancelled(state) => { // Bob has cancelled the swap match state.expired_timelock(bitcoin_wallet).await? { - ExpiredTimelocks::None => { + ExpiredTimelocks::None {..} => { bail!( "Internal error: canceled state reached before cancel timelock was expired" ); } - ExpiredTimelocks::Cancel => { + ExpiredTimelocks::Cancel { .. } => { state.publish_refund_btc(bitcoin_wallet).await?; BobState::BtcRefunded(state) } diff --git a/swap/src/rpc/methods.rs b/swap/src/rpc/methods.rs index 946b5b94..98ba5185 100644 --- a/swap/src/rpc/methods.rs +++ b/swap/src/rpc/methods.rs @@ -18,19 +18,19 @@ pub fn register_modules(context: Arc) -> RpcModule> { .register_async_method("get_bitcoin_balance", |_, context| async move { get_bitcoin_balance(&context).await }) - .expect("Could not register RPC method get_bitcoin_balance"); + .unwrap(); module .register_async_method("get_history", |_, context| async move { get_history(&context).await }) - .expect("Could not register RPC method get_history"); + .unwrap(); module .register_async_method("get_raw_history", |_, context| async move { get_raw_history(&context).await }) - .expect("Could not register RPC method get_raw_history"); + .unwrap(); module .register_async_method("get_seller", |params, context| async move { @@ -42,7 +42,7 @@ pub fn register_modules(context: Arc) -> RpcModule> { get_seller(*swap_id, &context).await }) - .expect("Could not register RPC method get_seller"); + .unwrap(); module .register_async_method("get_swap_start_date", |params, context| async move { @@ -54,7 +54,7 @@ pub fn register_modules(context: Arc) -> RpcModule> { get_swap_start_date(*swap_id, &context).await }) - .expect("Could not register RPC method get_swap_start_date"); + .unwrap(); module .register_async_method("resume_swap", |params, context| async move { @@ -66,7 +66,18 @@ pub fn register_modules(context: Arc) -> RpcModule> { resume_swap(*swap_id, &context).await }) - .expect("Could not register RPC method resume_swap"); + .unwrap(); + + module.register_async_method("get_swap_expired_timelock", |params, context| async move { + let params: HashMap = params.parse()?; + + let swap_id = params.get("swap_id").ok_or_else(|| { + jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()) + })?; + + get_swap_timelock(*swap_id, &context).await + }).unwrap(); + module .register_async_method("cancel_refund_swap", |params, context| async move { let params: HashMap = params.parse()?; @@ -77,7 +88,7 @@ pub fn register_modules(context: Arc) -> RpcModule> { cancel_and_refund_swap(*swap_id, &context).await }) - .expect("Could not register RPC method cancel_refund_swap"); + .unwrap(); module .register_async_method("withdraw_btc", |params, context| async move { let params: HashMap = params.parse()?; @@ -145,7 +156,7 @@ pub fn register_modules(context: Arc) -> RpcModule> { ) .await }) - .expect("Could not register RPC method buy_xmr"); + .unwrap(); module .register_async_method("list_sellers", |params, context| async move { let params: HashMap = params.parse()?; @@ -155,10 +166,11 @@ pub fn register_modules(context: Arc) -> RpcModule> { list_sellers(rendezvous_point.clone(), &context).await }) - .expect("Could not register RPC method list_sellers"); + .unwrap(); module.register_async_method("get_current_swap", |_, context| async move { get_current_swap(&context).await - }).expect("Could not register RPC method get_current_swap"); + }).unwrap(); + module } @@ -220,6 +232,15 @@ async fn resume_swap( }, context).await } +async fn get_swap_timelock( + swap_id: Uuid, + context: &Arc, +) -> Result { + execute_request(Method::GetSwapExpiredTimelock { + swap_id + }, context).await +} + async fn cancel_and_refund_swap( swap_id: Uuid, context: &Arc,