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
This commit is contained in:
binarybaron 2023-08-12 23:15:29 +02:00
parent 433bf824f9
commit 1b13608d96
9 changed files with 155 additions and 32 deletions

View file

@ -1,5 +1,5 @@
use crate::api::Context; use crate::api::Context;
use crate::bitcoin::{Amount, TxLock}; use crate::bitcoin::{Amount, ExpiredTimelocks, TxLock};
use crate::cli::{list_sellers, EventLoop, SellerStatus}; use crate::cli::{list_sellers, EventLoop, SellerStatus};
use crate::libp2p_ext::MultiAddrExt; use crate::libp2p_ext::MultiAddrExt;
use crate::network::quote::{BidQuote, ZeroQuoteReceived}; use crate::network::quote::{BidQuote, ZeroQuoteReceived};
@ -115,6 +115,9 @@ pub enum Method {
server_address: Option<SocketAddr>, server_address: Option<SocketAddr>,
}, },
GetCurrentSwap, GetCurrentSwap,
GetSwapExpiredTimelock {
swap_id: Uuid,
},
} }
impl Request { impl Request {
@ -563,6 +566,38 @@ impl Request {
"swap_id": SWAP_LOCK.read().await.clone() "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,
}))
},
} }
} }

View file

@ -244,10 +244,14 @@ pub fn current_epoch(
} }
if tx_lock_status.is_confirmed_with(cancel_timelock) { 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 { pub mod bitcoin_address {

View file

@ -24,6 +24,12 @@ use std::ops::Add;
#[serde(transparent)] #[serde(transparent)]
pub struct CancelTimelock(u32); pub struct CancelTimelock(u32);
impl From<CancelTimelock> for u32 {
fn from(cancel_timelock: CancelTimelock) -> Self {
cancel_timelock.0
}
}
impl CancelTimelock { impl CancelTimelock {
pub const fn new(number_of_blocks: u32) -> Self { pub const fn new(number_of_blocks: u32) -> Self {
Self(number_of_blocks) Self(number_of_blocks)
@ -64,6 +70,12 @@ impl fmt::Display for CancelTimelock {
#[serde(transparent)] #[serde(transparent)]
pub struct PunishTimelock(u32); pub struct PunishTimelock(u32);
impl From<PunishTimelock> for u32 {
fn from(punish_timelock: PunishTimelock) -> Self {
punish_timelock.0
}
}
impl PunishTimelock { impl PunishTimelock {
pub const fn new(number_of_blocks: u32) -> Self { pub const fn new(number_of_blocks: u32) -> Self {
Self(number_of_blocks) Self(number_of_blocks)

View file

@ -37,9 +37,13 @@ impl Add<u32> for BlockHeight {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExpiredTimelocks { pub enum ExpiredTimelocks {
None, None {
Cancel, blocks_left: u32,
},
Cancel {
blocks_left: u32,
},
Punish, Punish,
} }

View file

@ -274,7 +274,7 @@ impl Subscription {
pub async fn wait_until_confirmed_with<T>(&self, target: T) -> Result<()> pub async fn wait_until_confirmed_with<T>(&self, target: T) -> Result<()>
where where
u32: PartialOrd<T>, T: Into<u32>,
T: Copy, T: Copy,
{ {
self.wait_until(|status| status.is_confirmed_with(target)) self.wait_until(|status| status.is_confirmed_with(target))
@ -926,10 +926,19 @@ impl Confirmed {
} }
pub fn meets_target<T>(&self, target: T) -> bool pub fn meets_target<T>(&self, target: T) -> bool
where where T: Into<u32>
u32: PartialOrd<T>,
{ {
self.confirmations() >= target self.confirmations() >= target.into()
}
pub fn blocks_left_until<T>(&self, target: T) -> u32
where T: Into<u32>, 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. /// Check if the script has met the given confirmation target.
pub fn is_confirmed_with<T>(&self, target: T) -> bool pub fn is_confirmed_with<T>(&self, target: T) -> bool
where where T: Into<u32>
u32: PartialOrd<T>,
{ {
match self { match self {
ScriptStatus::Confirmed(inner) => inner.meets_target(target), 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<T>(&self, target: T) -> u32
where T: Into<u32>, T: Copy
{
match self {
ScriptStatus::Confirmed(inner) => {
inner.blocks_left_until(target)
}
_ => target.into(),
}
}
pub fn has_been_seen(&self) -> bool { pub fn has_been_seen(&self) -> bool {
matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_)) matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_))
} }
@ -1005,6 +1025,33 @@ mod tests {
assert_eq!(confirmed.depth, 0) 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] #[test]
fn given_one_BTC_and_100k_sats_per_vb_fees_should_not_hit_max() { fn given_one_BTC_and_100k_sats_per_vb_fees_should_not_hit_max() {
// 400 weight = 100 vbyte // 400 weight = 100 vbyte

View file

@ -112,7 +112,7 @@ where
} }
AliceState::BtcLocked { state3 } => { AliceState::BtcLocked { state3 } => {
match state3.expired_timelocks(bitcoin_wallet).await? { 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 // 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. // block 0 for scenarios where we create a refund wallet.
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
@ -135,7 +135,7 @@ where
transfer_proof, transfer_proof,
state3, state3,
} => match state3.expired_timelocks(bitcoin_wallet).await? { } => match state3.expired_timelocks(bitcoin_wallet).await? {
ExpiredTimelocks::None => { ExpiredTimelocks::None {..} => {
monero_wallet monero_wallet
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1)) .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1))
.await .await
@ -221,7 +221,7 @@ where
encrypted_signature, encrypted_signature,
state3, state3,
} => match state3.expired_timelocks(bitcoin_wallet).await? { } => match state3.expired_timelocks(bitcoin_wallet).await? {
ExpiredTimelocks::None => { ExpiredTimelocks::None {..} => {
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
match state3.signed_redeem_transaction(*encrypted_signature) { match state3.signed_redeem_transaction(*encrypted_signature) {
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {

View file

@ -440,7 +440,7 @@ impl State3 {
self.tx_lock.txid() self.tx_lock.txid()
} }
pub async fn current_epoch( pub async fn expired_timelock(
&self, &self,
bitcoin_wallet: &bitcoin::Wallet, bitcoin_wallet: &bitcoin::Wallet,
) -> Result<ExpiredTimelocks> { ) -> Result<ExpiredTimelocks> {

View file

@ -117,7 +117,7 @@ async fn next_state(
} => { } => {
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; 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 transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
let cancel_timelock_expires = let cancel_timelock_expires =
tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock); 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; 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); let watch_request = state.lock_xmr_watch_request(lock_transfer_proof);
select! { select! {
@ -185,7 +185,7 @@ async fn next_state(
BobState::XmrLocked(state) => { BobState::XmrLocked(state) => {
let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; 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 // Alice has locked Xmr
// Bob sends Alice his key // Bob sends Alice his key
@ -209,7 +209,7 @@ async fn next_state(
BobState::EncSigSent(state) => { BobState::EncSigSent(state) => {
let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; 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! { select! {
state5 = state.watch_for_redeem_btc(bitcoin_wallet) => { state5 = state.watch_for_redeem_btc(bitcoin_wallet) => {
BobState::BtcRedeemed(state5?) BobState::BtcRedeemed(state5?)
@ -269,12 +269,12 @@ async fn next_state(
BobState::BtcCancelled(state) => { BobState::BtcCancelled(state) => {
// Bob has cancelled the swap // Bob has cancelled the swap
match state.expired_timelock(bitcoin_wallet).await? { match state.expired_timelock(bitcoin_wallet).await? {
ExpiredTimelocks::None => { ExpiredTimelocks::None {..} => {
bail!( bail!(
"Internal error: canceled state reached before cancel timelock was expired" "Internal error: canceled state reached before cancel timelock was expired"
); );
} }
ExpiredTimelocks::Cancel => { ExpiredTimelocks::Cancel { .. } => {
state.publish_refund_btc(bitcoin_wallet).await?; state.publish_refund_btc(bitcoin_wallet).await?;
BobState::BtcRefunded(state) BobState::BtcRefunded(state)
} }

View file

@ -18,19 +18,19 @@ pub fn register_modules(context: Arc<Context>) -> RpcModule<Arc<Context>> {
.register_async_method("get_bitcoin_balance", |_, context| async move { .register_async_method("get_bitcoin_balance", |_, context| async move {
get_bitcoin_balance(&context).await get_bitcoin_balance(&context).await
}) })
.expect("Could not register RPC method get_bitcoin_balance"); .unwrap();
module module
.register_async_method("get_history", |_, context| async move { .register_async_method("get_history", |_, context| async move {
get_history(&context).await get_history(&context).await
}) })
.expect("Could not register RPC method get_history"); .unwrap();
module module
.register_async_method("get_raw_history", |_, context| async move { .register_async_method("get_raw_history", |_, context| async move {
get_raw_history(&context).await get_raw_history(&context).await
}) })
.expect("Could not register RPC method get_raw_history"); .unwrap();
module module
.register_async_method("get_seller", |params, context| async move { .register_async_method("get_seller", |params, context| async move {
@ -42,7 +42,7 @@ pub fn register_modules(context: Arc<Context>) -> RpcModule<Arc<Context>> {
get_seller(*swap_id, &context).await get_seller(*swap_id, &context).await
}) })
.expect("Could not register RPC method get_seller"); .unwrap();
module module
.register_async_method("get_swap_start_date", |params, context| async move { .register_async_method("get_swap_start_date", |params, context| async move {
@ -54,7 +54,7 @@ pub fn register_modules(context: Arc<Context>) -> RpcModule<Arc<Context>> {
get_swap_start_date(*swap_id, &context).await get_swap_start_date(*swap_id, &context).await
}) })
.expect("Could not register RPC method get_swap_start_date"); .unwrap();
module module
.register_async_method("resume_swap", |params, context| async move { .register_async_method("resume_swap", |params, context| async move {
@ -66,7 +66,18 @@ pub fn register_modules(context: Arc<Context>) -> RpcModule<Arc<Context>> {
resume_swap(*swap_id, &context).await 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<String, Uuid> = 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 module
.register_async_method("cancel_refund_swap", |params, context| async move { .register_async_method("cancel_refund_swap", |params, context| async move {
let params: HashMap<String, Uuid> = params.parse()?; let params: HashMap<String, Uuid> = params.parse()?;
@ -77,7 +88,7 @@ pub fn register_modules(context: Arc<Context>) -> RpcModule<Arc<Context>> {
cancel_and_refund_swap(*swap_id, &context).await cancel_and_refund_swap(*swap_id, &context).await
}) })
.expect("Could not register RPC method cancel_refund_swap"); .unwrap();
module module
.register_async_method("withdraw_btc", |params, context| async move { .register_async_method("withdraw_btc", |params, context| async move {
let params: HashMap<String, String> = params.parse()?; let params: HashMap<String, String> = params.parse()?;
@ -145,7 +156,7 @@ pub fn register_modules(context: Arc<Context>) -> RpcModule<Arc<Context>> {
) )
.await .await
}) })
.expect("Could not register RPC method buy_xmr"); .unwrap();
module module
.register_async_method("list_sellers", |params, context| async move { .register_async_method("list_sellers", |params, context| async move {
let params: HashMap<String, Multiaddr> = params.parse()?; let params: HashMap<String, Multiaddr> = params.parse()?;
@ -155,10 +166,11 @@ pub fn register_modules(context: Arc<Context>) -> RpcModule<Arc<Context>> {
list_sellers(rendezvous_point.clone(), &context).await 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 { module.register_async_method("get_current_swap", |_, context| async move {
get_current_swap(&context).await get_current_swap(&context).await
}).expect("Could not register RPC method get_current_swap"); }).unwrap();
module module
} }
@ -220,6 +232,15 @@ async fn resume_swap(
}, context).await }, context).await
} }
async fn get_swap_timelock(
swap_id: Uuid,
context: &Arc<Context>,
) -> Result<serde_json::Value, jsonrpsee_core::Error> {
execute_request(Method::GetSwapExpiredTimelock {
swap_id
}, context).await
}
async fn cancel_and_refund_swap( async fn cancel_and_refund_swap(
swap_id: Uuid, swap_id: Uuid,
context: &Arc<Context>, context: &Arc<Context>,