diff --git a/daemon/src/db.rs b/daemon/src/db.rs index ab2b11c..0747253 100644 --- a/daemon/src/db.rs +++ b/daemon/src/db.rs @@ -497,7 +497,7 @@ mod tests { transition_timestamp: SystemTime::now(), }, }; - insert_new_cfd_state_by_order_id(cfd.order.id, cfd.state, &mut conn) + insert_new_cfd_state_by_order_id(cfd.order.id, cfd.state.clone(), &mut conn) .await .unwrap(); diff --git a/daemon/src/maker_cfd_actor.rs b/daemon/src/maker_cfd_actor.rs index 535673e..fc57a51 100644 --- a/daemon/src/maker_cfd_actor.rs +++ b/daemon/src/maker_cfd_actor.rs @@ -1,5 +1,8 @@ -use crate::db::{insert_cfd, insert_order, load_all_cfds, load_cfd_by_order_id, load_order_by_id}; -use crate::model::cfd::{Cfd, CfdState, CfdStateCommon, FinalizedCfd, Order, OrderId}; +use crate::db::{ + insert_cfd, insert_new_cfd_state_by_order_id, insert_order, load_all_cfds, + load_cfd_by_order_id, load_order_by_id, +}; +use crate::model::cfd::{Cfd, CfdState, CfdStateCommon, Dlc, Order, OrderId}; use crate::model::{TakerId, Usd, WalletInfo}; use crate::wallet::Wallet; use crate::wire::SetupMsg; @@ -27,7 +30,10 @@ pub enum Command { id: TakerId, }, IncProtocolMsg(SetupMsg), - CfdSetupCompleted(FinalizedCfd), + CfdSetupCompleted { + order_id: OrderId, + dlc: Dlc, + }, } pub fn new( @@ -43,7 +49,11 @@ pub fn new( mpsc::UnboundedSender, ) { let (sender, mut receiver) = mpsc::unbounded_channel(); + + // TODO: Move the contract setup into a dedicated actor and send messages to that actor that + // manages the state instead of this ugly buffer let mut current_contract_setup = None; + let mut contract_setup_message_buffer = vec![]; let mut current_order_id = None; @@ -157,6 +167,8 @@ pub fn new( .unwrap(); } maker_cfd_actor::Command::StartContractSetup { taker_id, order_id } => { + println!("CONTRACT SETUP"); + // Kick-off the CFD protocol let (sk, pk) = crate::keypair::new(&mut rand::thread_rng()); @@ -185,27 +197,78 @@ pub fn new( cfd, ); + current_contract_setup = Some(inbox.clone()); + + for msg in contract_setup_message_buffer.drain(..) { + inbox.send(msg).unwrap(); + } + + // TODO: Should we do this here or already earlier or after the spawn? + insert_new_cfd_state_by_order_id( + order_id, + CfdState::ContractSetup { + common: CfdStateCommon { + transition_timestamp: SystemTime::now(), + }, + }, + &mut conn, + ) + .await + .unwrap(); + cfd_feed_actor_inbox + .send(load_all_cfds(&mut conn).await.unwrap()) + .unwrap(); + tokio::spawn({ let sender = sender.clone(); async move { sender - .send(Command::CfdSetupCompleted(actor.await)) + .send(Command::CfdSetupCompleted { + order_id, + dlc: actor.await, + }) .unwrap() } }); - current_contract_setup = Some(inbox); } maker_cfd_actor::Command::IncProtocolMsg(msg) => { let inbox = match ¤t_contract_setup { - None => panic!("whoops"), + None => { + contract_setup_message_buffer.push(msg); + continue; + } Some(inbox) => inbox, }; inbox.send(msg).unwrap(); } - maker_cfd_actor::Command::CfdSetupCompleted(_finalized_cfd) => { - todo!("but what?") + maker_cfd_actor::Command::CfdSetupCompleted { order_id, dlc } => { + println!("Setup complete, publishing on chain now..."); + + current_contract_setup = None; + contract_setup_message_buffer = vec![]; + + insert_new_cfd_state_by_order_id( + order_id, + CfdState::PendingOpen { + common: CfdStateCommon { + transition_timestamp: SystemTime::now(), + }, + dlc, + }, + &mut conn, + ) + .await + .unwrap(); + + cfd_feed_actor_inbox + .send(load_all_cfds(&mut conn).await.unwrap()) + .unwrap(); + + // TODO: Publish on chain and only then transition to open - this might + // require saving some internal state to make sure we are able to monitor + // the publication after a restart } } } diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index e7b10e1..38d643c 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -3,12 +3,12 @@ use anyhow::Result; use bdk::bitcoin::secp256k1::{SecretKey, Signature}; use bdk::bitcoin::util::psbt::PartiallySignedTransaction; use bdk::bitcoin::{Amount, Transaction}; -use cfd_protocol::interval; use cfd_protocol::secp256k1_zkp::EcdsaAdaptorSignature; use rust_decimal::Decimal; use rust_decimal_macros::dec; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; +use std::ops::RangeInclusive; use std::time::{Duration, SystemTime}; use uuid::Uuid; @@ -131,61 +131,83 @@ pub struct CfdStateCommon { } // Note: De-/Serialize with type tag to make handling on UI easier -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", content = "payload")] pub enum CfdState { /// The taker has requested to take a CFD, but has not messaged the maker yet. /// /// This state only applies to the taker. - TakeRequested { common: CfdStateCommon }, + TakeRequested { + common: CfdStateCommon, + }, /// The taker sent an open request to the maker to open the CFD but don't have a response yet. /// /// This state applies to taker and maker. /// Initial state for the maker. - PendingTakeRequest { common: CfdStateCommon }, + PendingTakeRequest { + common: CfdStateCommon, + }, /// The maker has accepted the CFD take request, but the contract is not set up on chain yet. /// /// This state applies to taker and maker. - Accepted { common: CfdStateCommon }, + Accepted { + common: CfdStateCommon, + }, /// The maker rejected the CFD take request. /// /// This state applies to taker and maker. - Rejected { common: CfdStateCommon }, + Rejected { + common: CfdStateCommon, + }, /// State used during contract setup. /// /// This state applies to taker and maker. /// All contract setup messages between taker and maker are expected to be sent in on scope. - ContractSetup { common: CfdStateCommon }, + ContractSetup { + common: CfdStateCommon, + }, + + PendingOpen { + common: CfdStateCommon, + dlc: Dlc, + }, /// The CFD contract is set up on chain. /// /// This state applies to taker and maker. Open { common: CfdStateCommon, - settlement_timestamp: SystemTime, - #[serde(with = "::bdk::bitcoin::util::amount::serde::as_sat")] - margin: Amount, + dlc: Dlc, }, /// Requested close the position, but we have not passed that on to the blockchain yet. /// /// This state applies to taker and maker. - CloseRequested { common: CfdStateCommon }, + CloseRequested { + common: CfdStateCommon, + }, /// The close transaction (CET) was published on the Bitcoin blockchain but we don't have a /// confirmation yet. /// /// This state applies to taker and maker. - PendingClose { common: CfdStateCommon }, + PendingClose { + common: CfdStateCommon, + }, /// The close transaction is confirmed with at least one block. /// /// This state applies to taker and maker. - Closed { common: CfdStateCommon }, + Closed { + common: CfdStateCommon, + }, /// Error state /// /// This state applies to taker and maker. - Error { common: CfdStateCommon }, + Error { + common: CfdStateCommon, + }, } impl CfdState { @@ -196,6 +218,7 @@ impl CfdState { CfdState::Accepted { common } => common, CfdState::Rejected { common } => common, CfdState::ContractSetup { common } => common, + CfdState::PendingOpen { common, .. } => common, CfdState::Open { common, .. } => common, CfdState::CloseRequested { common } => common, CfdState::PendingClose { common } => common, @@ -229,6 +252,9 @@ impl Display for CfdState { CfdState::ContractSetup { .. } => { write!(f, "Contract Setup") } + CfdState::PendingOpen { .. } => { + write!(f, "Pending Open") + } CfdState::Open { .. } => { write!(f, "Open") } @@ -392,7 +418,6 @@ fn calculate_sell_margin(price: Usd, quantity: Usd) -> Result { mod tests { use super::*; use rust_decimal_macros::dec; - use std::time::UNIX_EPOCH; #[test] fn given_default_values_then_expected_liquidation_price() { @@ -458,116 +483,6 @@ mod tests { assert_eq!(sell_margin, Amount::from_btc(2.0).unwrap()); } - #[test] - fn serialize_cfd_state_snapshot() { - // This test is to prevent us from breaking the CfdState API against the database. - // We serialize the state into the database, so changes to the enum result in breaking - // program version changes. - - let fixed_timestamp = UNIX_EPOCH; - - let cfd_state = CfdState::TakeRequested { - common: CfdStateCommon { - transition_timestamp: fixed_timestamp, - }, - }; - let json = serde_json::to_string(&cfd_state).unwrap(); - assert_eq!( - json, - r#"{"type":"TakeRequested","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"# - ); - - let cfd_state = CfdState::PendingTakeRequest { - common: CfdStateCommon { - transition_timestamp: fixed_timestamp, - }, - }; - let json = serde_json::to_string(&cfd_state).unwrap(); - assert_eq!( - json, - r#"{"type":"PendingTakeRequest","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"# - ); - - let cfd_state = CfdState::Accepted { - common: CfdStateCommon { - transition_timestamp: fixed_timestamp, - }, - }; - let json = serde_json::to_string(&cfd_state).unwrap(); - assert_eq!( - json, - r#"{"type":"Accepted","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"# - ); - - let cfd_state = CfdState::ContractSetup { - common: CfdStateCommon { - transition_timestamp: fixed_timestamp, - }, - }; - let json = serde_json::to_string(&cfd_state).unwrap(); - assert_eq!( - json, - r#"{"type":"ContractSetup","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"# - ); - - let cfd_state = CfdState::Open { - common: CfdStateCommon { - transition_timestamp: fixed_timestamp, - }, - settlement_timestamp: fixed_timestamp, - margin: Amount::from_btc(0.5).unwrap(), - }; - let json = serde_json::to_string(&cfd_state).unwrap(); - assert_eq!( - json, - r#"{"type":"Open","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}},"settlement_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0},"margin":50000000}}"# - ); - - let cfd_state = CfdState::CloseRequested { - common: CfdStateCommon { - transition_timestamp: fixed_timestamp, - }, - }; - let json = serde_json::to_string(&cfd_state).unwrap(); - assert_eq!( - json, - r#"{"type":"CloseRequested","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"# - ); - - let cfd_state = CfdState::PendingClose { - common: CfdStateCommon { - transition_timestamp: fixed_timestamp, - }, - }; - let json = serde_json::to_string(&cfd_state).unwrap(); - assert_eq!( - json, - r#"{"type":"PendingClose","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"# - ); - - let cfd_state = CfdState::Closed { - common: CfdStateCommon { - transition_timestamp: fixed_timestamp, - }, - }; - let json = serde_json::to_string(&cfd_state).unwrap(); - assert_eq!( - json, - r#"{"type":"Closed","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"# - ); - - let cfd_state = CfdState::Error { - common: CfdStateCommon { - transition_timestamp: fixed_timestamp, - }, - }; - let json = serde_json::to_string(&cfd_state).unwrap(); - assert_eq!( - json, - 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; @@ -590,14 +505,14 @@ mod tests { /// /// All contained signatures are the signatures of THE OTHER PARTY. /// To use any of these transactions, we need to re-sign them with the correct secret key. -#[derive(Debug)] -pub struct FinalizedCfd { +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Dlc { pub identity: SecretKey, pub revocation: SecretKey, pub publish: SecretKey, pub lock: PartiallySignedTransaction, pub commit: (Transaction, EcdsaAdaptorSignature), - pub cets: Vec<(Transaction, EcdsaAdaptorSignature, interval::Digits)>, + pub cets: Vec<(Transaction, EcdsaAdaptorSignature, RangeInclusive)>, pub refund: (Transaction, Signature), } diff --git a/daemon/src/setup_contract_actor.rs b/daemon/src/setup_contract_actor.rs index e01498c..77beb00 100644 --- a/daemon/src/setup_contract_actor.rs +++ b/daemon/src/setup_contract_actor.rs @@ -1,4 +1,4 @@ -use crate::model::cfd::{Cfd, FinalizedCfd}; +use crate::model::cfd::{Cfd, Dlc}; use crate::wire::{Msg0, Msg1, SetupMsg}; use anyhow::{Context, Result}; use bdk::bitcoin::secp256k1::{schnorrsig, SecretKey, Signature, SECP256K1}; @@ -17,7 +17,7 @@ use tokio::sync::mpsc; /// Given an initial set of parameters, sets up the CFD contract with the other party. /// Passing OwnParams identifies whether caller is the maker or the taker. /// -/// Returns the [`FinalizedCfd`] which contains the lock transaction, ready to be signed and sent to +/// Returns the [`Dlc`] which contains the lock transaction, ready to be signed and sent to /// the other party. Signing of the lock transaction is not included in this function because we /// want the Cfd actor to own the wallet. pub fn new( @@ -26,10 +26,7 @@ pub fn new( sk: SecretKey, oracle_pk: schnorrsig::PublicKey, cfd: Cfd, -) -> ( - impl Future, - mpsc::UnboundedSender, -) { +) -> (impl Future, mpsc::UnboundedSender) { let (sender, mut receiver) = mpsc::unbounded_channel::(); let actor = async move { @@ -130,7 +127,7 @@ pub fn new( .map(|(tx, _, digits)| (digits.range(), (tx, digits))) .collect::>(); - FinalizedCfd { + Dlc { identity: sk, revocation: rev_sk, publish: publish_sk, @@ -142,7 +139,7 @@ pub fn new( .map(|(range, sig)| { let (cet, digits) = cet_by_digits.remove(&range).expect("unknown CET"); - (cet, sig, digits) + (cet, sig, digits.range()) }) .collect::>(), refund: (refund_tx, msg1.refund), diff --git a/daemon/src/taker_cfd_actor.rs b/daemon/src/taker_cfd_actor.rs index 563ed56..a86adde 100644 --- a/daemon/src/taker_cfd_actor.rs +++ b/daemon/src/taker_cfd_actor.rs @@ -2,7 +2,7 @@ use crate::db::{ insert_cfd, insert_new_cfd_state_by_order_id, insert_order, load_all_cfds, load_cfd_by_order_id, load_order_by_id, }; -use crate::model::cfd::{Cfd, CfdState, CfdStateCommon, FinalizedCfd, Order, OrderId}; +use crate::model::cfd::{Cfd, CfdState, CfdStateCommon, Dlc, Order, OrderId}; use crate::model::{Usd, WalletInfo}; use crate::wallet::Wallet; use crate::wire::SetupMsg; @@ -21,7 +21,7 @@ pub enum Command { NewOrder(Option), OrderAccepted(OrderId), IncProtocolMsg(SetupMsg), - CfdSetupCompleted(FinalizedCfd), + CfdSetupCompleted { order_id: OrderId, dlc: Dlc }, } pub fn new( @@ -101,6 +101,10 @@ pub fn new( .await .unwrap(); + out_msg_maker_inbox + .send(wire::TakerToMaker::StartContractSetup(order_id)) + .unwrap(); + cfd_feed_actor_inbox .send(load_all_cfds(&mut conn).await.unwrap()) .unwrap(); @@ -128,7 +132,10 @@ pub fn new( async move { sender - .send(Command::CfdSetupCompleted(actor.await)) + .send(Command::CfdSetupCompleted { + order_id, + dlc: actor.await, + }) .unwrap() } }); @@ -142,10 +149,33 @@ pub fn new( inbox.send(msg).unwrap(); } - Command::CfdSetupCompleted(_finalized_cfd) => { - todo!("but what?") + Command::CfdSetupCompleted { order_id, dlc } => { + println!("Setup complete, publishing on chain now..."); + + current_contract_setup = None; + + insert_new_cfd_state_by_order_id( + order_id, + CfdState::PendingOpen { + common: CfdStateCommon { + transition_timestamp: SystemTime::now(), + }, + dlc, + }, + &mut conn, + ) + .await + .unwrap(); + + cfd_feed_actor_inbox + .send(load_all_cfds(&mut conn).await.unwrap()) + .unwrap(); + + // TODO: Some code duplication with maker in this block - // Assumption: The maker publishes the CFD on chain + // TODO: Publish on chain and only then transition to open - this might + // require saving some internal state to make sure we are able to monitor + // the publication after a restart } } }