From 445208b475ca1094e489aaae4c762906d943c304 Mon Sep 17 00:00:00 2001 From: Philipp Hoenisch Date: Tue, 14 Sep 2021 13:40:36 +1000 Subject: [PATCH] Calculate refund timelock based on CFD term. A threshold is added. This is needed because a malicious user might "change their mind" and publish transactions at any time trying to get out of the contract. The refund timeout should ensure that the refund transaction can only be published if the oracle does not publish a signature. --- daemon/src/model/cfd.rs | 31 +++++++++++++++++++++++++++++++ daemon/src/taker_cfd_actor.rs | 25 ++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index 848f214..7522bc0 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -299,6 +299,20 @@ fn calculate_profit( Ok((Amount::ZERO, Usd::ZERO)) } +pub trait AsBlocks { + /// Calculates the duration in Bitcoin blocks. + /// + /// On Bitcoin there is a block every 10 minutes/600 seconds on average. + /// It's the caller's responsibility to round the resulting floating point number. + fn as_blocks(&self) -> f32; +} + +impl AsBlocks for Duration { + fn as_blocks(&self) -> f32 { + self.as_secs_f32() / 60.0 / 10.0 + } +} + /// Calculates the buyer's margin in BTC /// /// The margin is the initial margin and represents the collateral the buyer has to come up with to @@ -510,6 +524,23 @@ mod tests { r#"{"type":"Error","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"# ); } + + #[test] + fn test_secs_into_blocks() { + let error_margin = f32::EPSILON; + + let duration = Duration::from_secs(600); + let blocks = duration.as_blocks(); + assert!(blocks - error_margin < 1.0 && blocks + error_margin > 1.0); + + let duration = Duration::from_secs(0); + let blocks = duration.as_blocks(); + assert!(blocks - error_margin < 0.0 && blocks + error_margin > 0.0); + + let duration = Duration::from_secs(60); + let blocks = duration.as_blocks(); + assert!(blocks - error_margin < 0.1 && blocks + error_margin > 0.1); + } } /// Contains all data we've assembled about the CFD through the setup protocol. diff --git a/daemon/src/taker_cfd_actor.rs b/daemon/src/taker_cfd_actor.rs index e3176c8..6e4a744 100644 --- a/daemon/src/taker_cfd_actor.rs +++ b/daemon/src/taker_cfd_actor.rs @@ -2,7 +2,9 @@ use crate::db::{ insert_cfd, insert_cfd_offer, insert_new_cfd_state_by_offer_id, load_all_cfds, load_offer_by_id, OfferOrigin, }; -use crate::model::cfd::{Cfd, CfdOffer, CfdOfferId, CfdState, CfdStateCommon, FinalizedCfd}; +use crate::model::cfd::{ + AsBlocks, Cfd, CfdOffer, CfdOfferId, CfdState, CfdStateCommon, FinalizedCfd, +}; use crate::model::Usd; use crate::wire; use crate::wire::{AdaptorSignature, Msg0, Msg1, SetupMsg}; @@ -21,6 +23,20 @@ use std::collections::HashMap; use std::time::SystemTime; use tokio::sync::{mpsc, watch}; +/// A factor to be added to the CFD offer term for calculating the refund timelock. +/// +/// The refund timelock is important in case the oracle disappears or never publishes a signature. +/// Ideally, both users collaboratively settle in the refund scenario. This factor is important if +/// the users do not settle collaboratively. +/// `1.5` times the term as defined in CFD offer should be safe in the extreme case where a user +/// publishes the commit transaction right after the contract was initialized. In this case, the +/// oracle still has `1.0 * cfdoffer.term` time to attest and no one can publish the refund +/// transaction. +/// The downside is that if the oracle disappears: the users would only notice at the end +/// of the cfd term. In this case the users has to wait for another `1.5` times of the +/// term to get his funds back. +pub const REFUND_THRESHOLD: f32 = 1.5; + #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum Command { @@ -119,15 +135,17 @@ where .build_party_params(bitcoin::Amount::ZERO, pk) // TODO: Load correct quantity from DB .unwrap(); + let cfd = load_offer_by_id(offer_id, &mut conn).await.unwrap(); + let (actor, inbox) = setup_contract( { let inbox = out_msg_maker_inbox.clone(); - move |msg| inbox.send(wire::TakerToMaker::Protocol(msg)).unwrap() }, taker_params, sk, oracle_pk, + cfd, ); tokio::spawn({ @@ -170,6 +188,7 @@ fn setup_contract( taker: PartyParams, sk: SecretKey, oracle_pk: schnorrsig::PublicKey, + offer: CfdOffer, ) -> ( impl Future, mpsc::UnboundedSender, @@ -193,7 +212,7 @@ fn setup_contract( (maker.clone(), maker_punish), (taker.clone(), taker_punish), oracle_pk, - 0, // TODO: Calculate refund timelock based on CFD term + offer.term.mul_f32(REFUND_THRESHOLD).as_blocks().ceil() as u32, vec![], sk, )